1use std::sync::Arc;
2
3use longbridge_httpcli::{DcRegion, HttpClient, Json, Method};
4use serde::{Serialize, de::DeserializeOwned};
5use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
6
7use crate::{Config, Result, dca::types::*};
8
9struct InnerDCAContext {
10 http_cli: HttpClient,
11 log_subscriber: Arc<dyn Subscriber + Send + Sync>,
12}
13
14impl Drop for InnerDCAContext {
15 fn drop(&mut self) {
16 dispatcher::with_default(&self.log_subscriber.clone().into(), || {
17 tracing::info!("dca context dropped");
18 });
19 }
20}
21
22#[derive(Clone)]
24pub struct DCAContext(Arc<InnerDCAContext>);
25
26impl DCAContext {
27 pub fn new(config: Arc<Config>) -> Self {
29 let log_subscriber = config.create_log_subscriber("dca");
30 dispatcher::with_default(&log_subscriber.clone().into(), || {
31 tracing::info!(language = ?config.language, "creating dca context");
32 });
33 let ctx = Self(Arc::new(InnerDCAContext {
34 http_cli: config.create_http_client(),
35 log_subscriber,
36 }));
37 dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
38 tracing::info!("dca context created");
39 });
40 ctx
41 }
42
43 #[inline]
45 pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
46 self.0.log_subscriber.clone()
47 }
48
49 async fn get<R, Q>(&self, path: &'static str, query: Q) -> Result<R>
50 where
51 R: DeserializeOwned + Send + Sync + 'static,
52 Q: Serialize + Send + Sync,
53 {
54 Ok(self
55 .0
56 .http_cli
57 .request(Method::GET, path)
58 .dc_restrict(DcRegion::Ap)
60 .query_params(query)
61 .response::<Json<R>>()
62 .send()
63 .with_subscriber(self.0.log_subscriber.clone())
64 .await?
65 .0)
66 }
67
68 async fn post<R, B>(&self, path: &'static str, body: B) -> Result<R>
69 where
70 R: DeserializeOwned + Send + Sync + 'static,
71 B: std::fmt::Debug + Serialize + Send + Sync + 'static,
72 {
73 Ok(self
74 .0
75 .http_cli
76 .request(Method::POST, path)
77 .dc_restrict(DcRegion::Ap)
79 .body(Json(body))
80 .response::<Json<R>>()
81 .send()
82 .with_subscriber(self.0.log_subscriber.clone())
83 .await?
84 .0)
85 }
86
87 pub async fn list(&self, status: Option<DCAStatus>, symbol: Option<String>) -> Result<DcaList> {
91 #[derive(Serialize)]
92 struct Query {
93 page: i32,
94 limit: i32,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 status: Option<DCAStatus>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 symbol: Option<String>,
99 }
100 self.get(
101 "/v1/dailycoins/query",
102 Query {
103 page: 1,
104 limit: 100,
105 status,
106 symbol,
107 },
108 )
109 .await
110 }
111
112 pub async fn create(
116 &self,
117 symbol: impl Into<String>,
118 amount: impl Into<String>,
119 frequency: DCAFrequency,
120 day_of_week: Option<String>,
121 day_of_month: Option<u32>,
122 allow_margin: bool,
123 ) -> Result<DcaCreateResult> {
124 let mut body = serde_json::json!({
125 "symbol": symbol.into(),
126 "per_invest_amount": amount.into(),
127 "invest_frequency": frequency,
128 "allow_margin_finance": if allow_margin { 1 } else { 0 }
129 });
130 if let Some(dow) = day_of_week {
131 body["invest_day_of_week"] = serde_json::Value::String(dow);
132 }
133 if let Some(dom) = day_of_month {
134 body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
135 }
136 self.post("/v1/dailycoins/create", body).await
137 }
138
139 pub async fn update(
143 &self,
144 plan_id: impl Into<String>,
145 amount: Option<String>,
146 frequency: Option<DCAFrequency>,
147 day_of_week: Option<String>,
148 day_of_month: Option<u32>,
149 allow_margin: Option<bool>,
150 ) -> Result<DcaCreateResult> {
151 let mut body = serde_json::json!({ "plan_id": plan_id.into() });
152 if let Some(a) = amount {
153 body["per_invest_amount"] = serde_json::Value::String(a);
154 }
155 if let Some(f) = frequency {
156 body["invest_frequency"] = serde_json::to_value(f).unwrap_or_default();
157 }
158 if let Some(dow) = day_of_week {
159 body["invest_day_of_week"] = serde_json::Value::String(dow);
160 }
161 if let Some(dom) = day_of_month {
162 body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
163 }
164 if let Some(m) = allow_margin {
165 body["allow_margin_finance"] =
166 serde_json::Value::Number((if m { 1 } else { 0 }).into());
167 }
168 self.post::<DcaCreateResult, _>("/v1/dailycoins/update", body)
169 .await
170 }
171
172 pub async fn pause(&self, plan_id: impl Into<String>) -> Result<()> {
174 self.post::<serde_json::Value, _>(
175 "/v1/dailycoins/toggle",
176 serde_json::json!({ "plan_id": plan_id.into(), "status": "Suspended" }),
177 )
178 .await?;
179 Ok(())
180 }
181
182 pub async fn resume(&self, plan_id: impl Into<String>) -> Result<()> {
184 self.post::<serde_json::Value, _>(
185 "/v1/dailycoins/toggle",
186 serde_json::json!({ "plan_id": plan_id.into(), "status": "Active" }),
187 )
188 .await?;
189 Ok(())
190 }
191
192 pub async fn stop(&self, plan_id: impl Into<String>) -> Result<()> {
194 self.post::<serde_json::Value, _>(
195 "/v1/dailycoins/toggle",
196 serde_json::json!({ "plan_id": plan_id.into(), "status": "Finished" }),
197 )
198 .await?;
199 Ok(())
200 }
201
202 pub async fn history(
206 &self,
207 plan_id: impl Into<String>,
208 page: i32,
209 limit: i32,
210 ) -> Result<DcaHistoryResponse> {
211 #[derive(Serialize)]
212 struct Query {
213 plan_id: String,
214 page: i32,
215 limit: i32,
216 }
217 self.get(
218 "/v1/dailycoins/query-records",
219 Query {
220 plan_id: plan_id.into(),
221 page,
222 limit,
223 },
224 )
225 .await
226 }
227
228 pub async fn stats(&self, symbol: Option<String>) -> Result<DcaStats> {
232 #[derive(Serialize)]
233 struct Query {
234 #[serde(skip_serializing_if = "Option::is_none")]
235 symbol: Option<String>,
236 }
237 self.get("/v1/dailycoins/statistic", Query { symbol }).await
238 }
239
240 pub async fn check_support(&self, symbols: Vec<String>) -> Result<DcaSupportList> {
244 self.post(
245 "/v1/dailycoins/batch-check-support",
246 serde_json::json!({ "symbols": symbols }),
247 )
248 .await
249 }
250
251 pub async fn calc_date(
256 &self,
257 symbol: impl Into<String>,
258 frequency: DCAFrequency,
259 day_of_week: Option<String>,
260 day_of_month: Option<u32>,
261 ) -> Result<DcaCalcDateResult> {
262 let mut body = serde_json::json!({
263 "symbol": symbol.into(),
264 "invest_frequency": frequency,
265 });
266 if let Some(dow) = day_of_week {
267 body["invest_day_of_week"] = serde_json::Value::String(dow);
268 }
269 if let Some(dom) = day_of_month {
270 body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
271 }
272 self.post("/v1/dailycoins/calc-trd-date", body).await
273 }
274
275 pub async fn set_reminder(&self, hours: impl Into<String>) -> Result<()> {
281 #[derive(serde::Deserialize)]
282 struct Empty {}
283 self.post::<Empty, _>(
284 "/v1/dailycoins/update-alter-hours",
285 serde_json::json!({ "alter_hours": hours.into() }),
286 )
287 .await?;
288 Ok(())
289 }
290}