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, portfolio::types::*, utils::counter::symbol_to_counter_id};
struct InnerPortfolioContext {
http_cli: HttpClient,
log_subscriber: Arc<dyn Subscriber + Send + Sync>,
}
impl Drop for InnerPortfolioContext {
fn drop(&mut self) {
dispatcher::with_default(&self.log_subscriber.clone().into(), || {
tracing::info!("portfolio context dropped");
});
}
}
#[derive(Clone)]
pub struct PortfolioContext(Arc<InnerPortfolioContext>);
impl PortfolioContext {
pub fn new(config: Arc<Config>) -> Self {
let log_subscriber = config.create_log_subscriber("portfolio");
dispatcher::with_default(&log_subscriber.clone().into(), || {
tracing::info!(language = ?config.language, "creating portfolio context");
});
let ctx = Self(Arc::new(InnerPortfolioContext {
http_cli: config.create_http_client(),
log_subscriber,
}));
dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
tracing::info!("portfolio 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)
}
pub async fn exchange_rate(&self) -> Result<ExchangeRates> {
#[derive(Serialize)]
struct Empty {}
self.get("/v1/asset/exchange_rates", Empty {}).await
}
pub async fn profit_analysis(
&self,
start: Option<String>,
end: Option<String>,
) -> Result<ProfitAnalysis> {
let start_ts = date_to_unix_opt(start.as_deref());
let end_ts = date_to_unix_end_opt(end.as_deref());
#[derive(Serialize)]
struct SummaryQuery {
#[serde(skip_serializing_if = "Option::is_none")]
start: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<i64>,
}
#[derive(Serialize)]
struct SublistQuery {
profit_or_loss: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
start: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<i64>,
}
let (summary, sublist) = tokio::join!(
self.get::<ProfitAnalysisSummary, _>(
"/v1/portfolio/profit-analysis-summary",
SummaryQuery {
start: start_ts,
end: end_ts
}
),
self.get::<ProfitAnalysisSublist, _>(
"/v1/portfolio/profit-analysis-sublist",
SublistQuery {
profit_or_loss: "all",
start: start_ts,
end: end_ts
}
),
);
Ok(ProfitAnalysis {
summary: summary?,
sublist: sublist?,
})
}
pub async fn profit_analysis_by_market(
&self,
market: Option<String>,
start: Option<String>,
end: Option<String>,
currency: Option<String>,
page: u32,
size: u32,
) -> Result<ProfitAnalysisByMarket> {
#[derive(Serialize)]
struct Query {
page: u32,
size: u32,
#[serde(skip_serializing_if = "Option::is_none")]
market: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
start: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
currency: Option<String>,
}
self.get(
"/v1/portfolio/profit-analysis/by-market",
Query {
page,
size,
market,
start: date_to_unix_opt(start.as_deref()),
end: date_to_unix_end_opt(end.as_deref()),
currency,
},
)
.await
}
pub async fn profit_analysis_detail(
&self,
symbol: impl Into<String>,
start: Option<String>,
end: Option<String>,
) -> Result<ProfitAnalysisDetail> {
#[derive(Serialize)]
struct Query {
counter_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
start: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<i64>,
}
self.get(
"/v1/portfolio/profit-analysis/detail",
Query {
counter_id: symbol_to_counter_id(&symbol.into()),
start: date_to_unix_opt(start.as_deref()),
end: date_to_unix_end_opt(end.as_deref()),
},
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn profit_analysis_flows(
&self,
symbol: impl Into<String>,
page: u32,
size: u32,
derivative: bool,
start: Option<String>,
end: Option<String>,
) -> Result<ProfitAnalysisFlows> {
#[derive(Serialize)]
struct Query {
counter_id: String,
page: u32,
size: u32,
derivative: bool,
#[serde(skip_serializing_if = "Option::is_none")]
start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
end: Option<String>,
}
self.get(
"/v1/portfolio/profit-analysis/flows",
Query {
counter_id: symbol_to_counter_id(&symbol.into()),
page,
size,
derivative,
start,
end,
},
)
.await
}
}
fn date_to_unix_opt(date: Option<&str>) -> Option<i64> {
date.and_then(|d| {
let parts: Vec<&str> = d.split('-').collect();
if parts.len() == 3 {
let y: i32 = parts[0].parse().ok()?;
let m: u8 = parts[1].parse().ok()?;
let d: u8 = parts[2].parse().ok()?;
let date = time::Date::from_calendar_date(y, time::Month::try_from(m).ok()?, d).ok()?;
let dt = date.midnight().assume_utc();
Some(dt.unix_timestamp())
} else {
None
}
})
}
fn date_to_unix_end_opt(date: Option<&str>) -> Option<i64> {
date_to_unix_opt(date).map(|ts| ts + 86399)
}