1use std::sync::Arc;
2
3use longbridge_httpcli::{HttpClient, Json, Method};
4use serde::{Serialize, de::DeserializeOwned};
5use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
6
7use crate::{Config, Result, dca::types::*, utils::counter::symbol_to_counter_id};
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 .query_params(query)
59 .response::<Json<R>>()
60 .send()
61 .with_subscriber(self.0.log_subscriber.clone())
62 .await?
63 .0)
64 }
65
66 async fn post<R, B>(&self, path: &'static str, body: B) -> Result<R>
67 where
68 R: DeserializeOwned + Send + Sync + 'static,
69 B: std::fmt::Debug + Serialize + Send + Sync + 'static,
70 {
71 Ok(self
72 .0
73 .http_cli
74 .request(Method::POST, path)
75 .body(Json(body))
76 .response::<Json<R>>()
77 .send()
78 .with_subscriber(self.0.log_subscriber.clone())
79 .await?
80 .0)
81 }
82
83 pub async fn list(&self, status: Option<DCAStatus>, symbol: Option<String>) -> Result<DcaList> {
87 #[derive(Serialize)]
88 struct Query {
89 page: i32,
90 limit: i32,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 status: Option<DCAStatus>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 counter_id: Option<String>,
95 }
96 self.get(
97 "/v1/dailycoins/query",
98 Query {
99 page: 1,
100 limit: 100,
101 status,
102 counter_id: symbol.map(|s| symbol_to_counter_id(&s)),
103 },
104 )
105 .await
106 }
107
108 pub async fn create(
112 &self,
113 symbol: impl Into<String>,
114 amount: impl Into<String>,
115 frequency: DCAFrequency,
116 day_of_week: Option<String>,
117 day_of_month: Option<u32>,
118 allow_margin: bool,
119 ) -> Result<DcaCreateResult> {
120 let cid = symbol_to_counter_id(&symbol.into());
121 let mut body = serde_json::json!({
122 "counter_id": cid,
123 "per_invest_amount": amount.into(),
124 "invest_frequency": frequency,
125 "allow_margin_finance": if allow_margin { 1 } else { 0 }
126 });
127 if let Some(dow) = day_of_week {
128 body["invest_day_of_week"] = serde_json::Value::String(dow);
129 }
130 if let Some(dom) = day_of_month {
131 body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
132 }
133 self.post("/v1/dailycoins/create", body).await
134 }
135
136 pub async fn update(
140 &self,
141 plan_id: impl Into<String>,
142 amount: Option<String>,
143 frequency: Option<DCAFrequency>,
144 day_of_week: Option<String>,
145 day_of_month: Option<u32>,
146 allow_margin: Option<bool>,
147 ) -> Result<DcaCreateResult> {
148 let mut body = serde_json::json!({ "plan_id": plan_id.into() });
149 if let Some(a) = amount {
150 body["per_invest_amount"] = serde_json::Value::String(a);
151 }
152 if let Some(f) = frequency {
153 body["invest_frequency"] = serde_json::to_value(f).unwrap_or_default();
154 }
155 if let Some(dow) = day_of_week {
156 body["invest_day_of_week"] = serde_json::Value::String(dow);
157 }
158 if let Some(dom) = day_of_month {
159 body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
160 }
161 if let Some(m) = allow_margin {
162 body["allow_margin_finance"] =
163 serde_json::Value::Number((if m { 1 } else { 0 }).into());
164 }
165 self.post::<DcaCreateResult, _>("/v1/dailycoins/update", body)
166 .await
167 }
168
169 pub async fn pause(&self, plan_id: impl Into<String>) -> Result<()> {
171 self.post::<serde_json::Value, _>(
172 "/v1/dailycoins/toggle",
173 serde_json::json!({ "plan_id": plan_id.into(), "status": "Suspended" }),
174 )
175 .await?;
176 Ok(())
177 }
178
179 pub async fn resume(&self, plan_id: impl Into<String>) -> Result<()> {
181 self.post::<serde_json::Value, _>(
182 "/v1/dailycoins/toggle",
183 serde_json::json!({ "plan_id": plan_id.into(), "status": "Active" }),
184 )
185 .await?;
186 Ok(())
187 }
188
189 pub async fn stop(&self, plan_id: impl Into<String>) -> Result<()> {
191 self.post::<serde_json::Value, _>(
192 "/v1/dailycoins/toggle",
193 serde_json::json!({ "plan_id": plan_id.into(), "status": "Finished" }),
194 )
195 .await?;
196 Ok(())
197 }
198
199 pub async fn history(
203 &self,
204 plan_id: impl Into<String>,
205 page: i32,
206 limit: i32,
207 ) -> Result<DcaHistoryResponse> {
208 #[derive(Serialize)]
209 struct Query {
210 plan_id: String,
211 page: i32,
212 limit: i32,
213 }
214 self.get(
215 "/v1/dailycoins/query-records",
216 Query {
217 plan_id: plan_id.into(),
218 page,
219 limit,
220 },
221 )
222 .await
223 }
224
225 pub async fn stats(&self, symbol: Option<String>) -> Result<DcaStats> {
229 #[derive(Serialize)]
230 struct Query {
231 #[serde(skip_serializing_if = "Option::is_none")]
232 counter_id: Option<String>,
233 }
234 self.get(
235 "/v1/dailycoins/statistic",
236 Query {
237 counter_id: symbol.map(|s| symbol_to_counter_id(&s)),
238 },
239 )
240 .await
241 }
242
243 pub async fn check_support(&self, symbols: Vec<String>) -> Result<DcaSupportList> {
247 let counter_ids: Vec<String> = symbols.iter().map(|s| symbol_to_counter_id(s)).collect();
248 self.post(
249 "/v1/dailycoins/batch-check-support",
250 serde_json::json!({ "counter_ids": counter_ids }),
251 )
252 .await
253 }
254
255 pub async fn calc_date(
260 &self,
261 symbol: impl Into<String>,
262 frequency: DCAFrequency,
263 day_of_week: Option<String>,
264 day_of_month: Option<u32>,
265 ) -> Result<DcaCalcDateResult> {
266 let mut body = serde_json::json!({
267 "counter_id": symbol_to_counter_id(&symbol.into()),
268 "invest_frequency": frequency,
269 });
270 if let Some(dow) = day_of_week {
271 body["invest_day_of_week"] = serde_json::Value::String(dow);
272 }
273 if let Some(dom) = day_of_month {
274 body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
275 }
276 self.post("/v1/dailycoins/calc-trd-date", body).await
277 }
278
279 pub async fn set_reminder(&self, hours: impl Into<String>) -> Result<()> {
285 #[derive(serde::Deserialize)]
286 struct Empty {}
287 self.post::<Empty, _>(
288 "/v1/dailycoins/update-alter-hours",
289 serde_json::json!({ "alter_hours": hours.into() }),
290 )
291 .await?;
292 Ok(())
293 }
294}