use std::sync::Arc;
use longbridge_httpcli::{HttpClient, Json, Method};
use serde::{Serialize, de::DeserializeOwned};
use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
use crate::{Config, Result, dca::types::*, utils::counter::symbol_to_counter_id};
struct InnerDCAContext {
http_cli: HttpClient,
log_subscriber: Arc<dyn Subscriber + Send + Sync>,
}
impl Drop for InnerDCAContext {
fn drop(&mut self) {
dispatcher::with_default(&self.log_subscriber.clone().into(), || {
tracing::info!("dca context dropped");
});
}
}
#[derive(Clone)]
pub struct DCAContext(Arc<InnerDCAContext>);
impl DCAContext {
pub fn new(config: Arc<Config>) -> Self {
let log_subscriber = config.create_log_subscriber("dca");
dispatcher::with_default(&log_subscriber.clone().into(), || {
tracing::info!(language = ?config.language, "creating dca context");
});
let ctx = Self(Arc::new(InnerDCAContext {
http_cli: config.create_http_client(),
log_subscriber,
}));
dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
tracing::info!("dca context created");
});
ctx
}
#[inline]
pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
self.0.log_subscriber.clone()
}
async fn get<R, Q>(&self, path: &'static str, query: Q) -> Result<R>
where
R: DeserializeOwned + Send + Sync + 'static,
Q: Serialize + Send + Sync,
{
Ok(self
.0
.http_cli
.request(Method::GET, path)
.query_params(query)
.response::<Json<R>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0)
}
async fn post<R, B>(&self, path: &'static str, body: B) -> Result<R>
where
R: DeserializeOwned + Send + Sync + 'static,
B: std::fmt::Debug + Serialize + Send + Sync + 'static,
{
Ok(self
.0
.http_cli
.request(Method::POST, path)
.body(Json(body))
.response::<Json<R>>()
.send()
.with_subscriber(self.0.log_subscriber.clone())
.await?
.0)
}
pub async fn list(&self, status: Option<DCAStatus>, symbol: Option<String>) -> Result<DcaList> {
#[derive(Serialize)]
struct Query {
page: i32,
limit: i32,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<DCAStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
counter_id: Option<String>,
}
self.get(
"/v1/dailycoins/query",
Query {
page: 1,
limit: 100,
status,
counter_id: symbol.map(|s| symbol_to_counter_id(&s)),
},
)
.await
}
pub async fn create(
&self,
symbol: impl Into<String>,
amount: impl Into<String>,
frequency: DCAFrequency,
day_of_week: Option<String>,
day_of_month: Option<u32>,
allow_margin: bool,
) -> Result<DcaCreateResult> {
let cid = symbol_to_counter_id(&symbol.into());
let mut body = serde_json::json!({
"counter_id": cid,
"per_invest_amount": amount.into(),
"invest_frequency": frequency,
"allow_margin_finance": if allow_margin { 1 } else { 0 }
});
if let Some(dow) = day_of_week {
body["invest_day_of_week"] = serde_json::Value::String(dow);
}
if let Some(dom) = day_of_month {
body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
}
self.post("/v1/dailycoins/create", body).await
}
pub async fn update(
&self,
plan_id: impl Into<String>,
amount: Option<String>,
frequency: Option<DCAFrequency>,
day_of_week: Option<String>,
day_of_month: Option<u32>,
allow_margin: Option<bool>,
) -> Result<DcaCreateResult> {
let mut body = serde_json::json!({ "plan_id": plan_id.into() });
if let Some(a) = amount {
body["per_invest_amount"] = serde_json::Value::String(a);
}
if let Some(f) = frequency {
body["invest_frequency"] = serde_json::to_value(f).unwrap_or_default();
}
if let Some(dow) = day_of_week {
body["invest_day_of_week"] = serde_json::Value::String(dow);
}
if let Some(dom) = day_of_month {
body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
}
if let Some(m) = allow_margin {
body["allow_margin_finance"] =
serde_json::Value::Number((if m { 1 } else { 0 }).into());
}
self.post::<DcaCreateResult, _>("/v1/dailycoins/update", body)
.await
}
pub async fn pause(&self, plan_id: impl Into<String>) -> Result<()> {
self.post::<serde_json::Value, _>(
"/v1/dailycoins/toggle",
serde_json::json!({ "plan_id": plan_id.into(), "status": "Suspended" }),
)
.await?;
Ok(())
}
pub async fn resume(&self, plan_id: impl Into<String>) -> Result<()> {
self.post::<serde_json::Value, _>(
"/v1/dailycoins/toggle",
serde_json::json!({ "plan_id": plan_id.into(), "status": "Active" }),
)
.await?;
Ok(())
}
pub async fn stop(&self, plan_id: impl Into<String>) -> Result<()> {
self.post::<serde_json::Value, _>(
"/v1/dailycoins/toggle",
serde_json::json!({ "plan_id": plan_id.into(), "status": "Finished" }),
)
.await?;
Ok(())
}
pub async fn history(
&self,
plan_id: impl Into<String>,
page: i32,
limit: i32,
) -> Result<DcaHistoryResponse> {
#[derive(Serialize)]
struct Query {
plan_id: String,
page: i32,
limit: i32,
}
self.get(
"/v1/dailycoins/query-records",
Query {
plan_id: plan_id.into(),
page,
limit,
},
)
.await
}
pub async fn stats(&self, symbol: Option<String>) -> Result<DcaStats> {
#[derive(Serialize)]
struct Query {
#[serde(skip_serializing_if = "Option::is_none")]
counter_id: Option<String>,
}
self.get(
"/v1/dailycoins/statistic",
Query {
counter_id: symbol.map(|s| symbol_to_counter_id(&s)),
},
)
.await
}
pub async fn check_support(&self, symbols: Vec<String>) -> Result<DcaSupportList> {
let counter_ids: Vec<String> = symbols.iter().map(|s| symbol_to_counter_id(s)).collect();
self.post(
"/v1/dailycoins/batch-check-support",
serde_json::json!({ "counter_ids": counter_ids }),
)
.await
}
pub async fn calc_date(
&self,
symbol: impl Into<String>,
frequency: DCAFrequency,
day_of_week: Option<String>,
day_of_month: Option<u32>,
) -> Result<DcaCalcDateResult> {
let mut body = serde_json::json!({
"counter_id": symbol_to_counter_id(&symbol.into()),
"invest_frequency": frequency,
});
if let Some(dow) = day_of_week {
body["invest_day_of_week"] = serde_json::Value::String(dow);
}
if let Some(dom) = day_of_month {
body["invest_day_of_month"] = serde_json::Value::String(dom.to_string());
}
self.post("/v1/dailycoins/calc-trd-date", body).await
}
pub async fn set_reminder(&self, hours: impl Into<String>) -> Result<()> {
#[derive(serde::Deserialize)]
struct Empty {}
self.post::<Empty, _>(
"/v1/dailycoins/update-alter-hours",
serde_json::json!({ "alter_hours": hours.into() }),
)
.await?;
Ok(())
}
}