Skip to main content

longbridge/market/
context.rs

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::{
8    Config, Result,
9    market::types::*,
10    utils::counter::{index_symbol_to_counter_id, symbol_to_counter_id},
11};
12
13struct InnerMarketContext {
14    http_cli: HttpClient,
15    log_subscriber: Arc<dyn Subscriber + Send + Sync>,
16}
17
18impl Drop for InnerMarketContext {
19    fn drop(&mut self) {
20        dispatcher::with_default(&self.log_subscriber.clone().into(), || {
21            tracing::info!("market context dropped");
22        });
23    }
24}
25
26/// Market data context — broker holdings, A/H premium, trade statistics,
27/// market anomalies, index constituents and more.
28#[derive(Clone)]
29pub struct MarketContext(Arc<InnerMarketContext>);
30
31impl MarketContext {
32    /// Create a [`MarketContext`]
33    pub fn new(config: Arc<Config>) -> Self {
34        let log_subscriber = config.create_log_subscriber("market");
35        dispatcher::with_default(&log_subscriber.clone().into(), || {
36            tracing::info!(language = ?config.language, "creating market context");
37        });
38        let ctx = Self(Arc::new(InnerMarketContext {
39            http_cli: config.create_http_client(),
40            log_subscriber,
41        }));
42        dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
43            tracing::info!("market context created");
44        });
45        ctx
46    }
47
48    /// Returns the log subscriber
49    #[inline]
50    pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
51        self.0.log_subscriber.clone()
52    }
53
54    async fn get<R, Q>(&self, path: &'static str, query: Q) -> Result<R>
55    where
56        R: DeserializeOwned + Send + Sync + 'static,
57        Q: Serialize + Send + Sync,
58    {
59        Ok(self
60            .0
61            .http_cli
62            .request(Method::GET, path)
63            .query_params(query)
64            .response::<Json<R>>()
65            .send()
66            .with_subscriber(self.0.log_subscriber.clone())
67            .await?
68            .0)
69    }
70
71    // ── market_status ─────────────────────────────────────────────
72
73    /// Get current trading status for all markets.
74    ///
75    /// Path: `GET /v1/quote/market-status`
76    pub async fn market_status(&self) -> Result<MarketStatusResponse> {
77        #[derive(Serialize)]
78        struct Empty {}
79        self.get("/v1/quote/market-status", Empty {}).await
80    }
81
82    // ── broker_holding ────────────────────────────────────────────
83
84    /// Get top broker holdings (buy/sell leaders) for a security.
85    ///
86    /// Path: `GET /v1/quote/broker-holding`
87    pub async fn broker_holding(
88        &self,
89        symbol: impl Into<String>,
90        period: BrokerHoldingPeriod,
91    ) -> Result<BrokerHoldingTop> {
92        let period_str = match period {
93            BrokerHoldingPeriod::Rct1 => "rct_1",
94            BrokerHoldingPeriod::Rct5 => "rct_5",
95            BrokerHoldingPeriod::Rct20 => "rct_20",
96            BrokerHoldingPeriod::Rct60 => "rct_60",
97        };
98        #[derive(Serialize)]
99        struct Query {
100            counter_id: String,
101            #[serde(rename = "type")]
102            period: &'static str,
103        }
104        self.get(
105            "/v1/quote/broker-holding",
106            Query {
107                counter_id: symbol_to_counter_id(&symbol.into()),
108                period: period_str,
109            },
110        )
111        .await
112    }
113
114    /// Get full broker holding details for a security.
115    ///
116    /// Path: `GET /v1/quote/broker-holding/detail`
117    pub async fn broker_holding_detail(
118        &self,
119        symbol: impl Into<String>,
120    ) -> Result<BrokerHoldingDetail> {
121        #[derive(Serialize)]
122        struct Query {
123            counter_id: String,
124        }
125        self.get(
126            "/v1/quote/broker-holding/detail",
127            Query {
128                counter_id: symbol_to_counter_id(&symbol.into()),
129            },
130        )
131        .await
132    }
133
134    /// Get daily holding history for a specific broker.
135    ///
136    /// Path: `GET /v1/quote/broker-holding/daily`
137    pub async fn broker_holding_daily(
138        &self,
139        symbol: impl Into<String>,
140        broker_id: impl Into<String>,
141    ) -> Result<BrokerHoldingDailyHistory> {
142        #[derive(Serialize)]
143        struct Query {
144            counter_id: String,
145            parti_number: String,
146        }
147        self.get(
148            "/v1/quote/broker-holding/daily",
149            Query {
150                counter_id: symbol_to_counter_id(&symbol.into()),
151                parti_number: broker_id.into(),
152            },
153        )
154        .await
155    }
156
157    // ── ah_premium ────────────────────────────────────────────────
158
159    /// Get A/H premium K-line data for a dual-listed security.
160    ///
161    /// Path: `GET /v1/quote/ahpremium/klines`
162    pub async fn ah_premium(
163        &self,
164        symbol: impl Into<String>,
165        period: AhPremiumPeriod,
166        count: u32,
167    ) -> Result<AhPremiumKlines> {
168        #[derive(Serialize)]
169        struct Query {
170            counter_id: String,
171            line_type: &'static str,
172            line_num: u32,
173        }
174        self.get(
175            "/v1/quote/ahpremium/klines",
176            Query {
177                counter_id: symbol_to_counter_id(&symbol.into()),
178                line_type: period.to_line_type(),
179                line_num: count,
180            },
181        )
182        .await
183    }
184
185    /// Get A/H premium intraday data for a dual-listed security.
186    ///
187    /// Path: `GET /v1/quote/ahpremium/timeshares`
188    pub async fn ah_premium_intraday(
189        &self,
190        symbol: impl Into<String>,
191    ) -> Result<AhPremiumIntraday> {
192        #[derive(Serialize)]
193        struct Query {
194            counter_id: String,
195            days: &'static str,
196        }
197        self.get(
198            "/v1/quote/ahpremium/timeshares",
199            Query {
200                counter_id: symbol_to_counter_id(&symbol.into()),
201                days: "1",
202            },
203        )
204        .await
205    }
206
207    // ── trade_stats ───────────────────────────────────────────────
208
209    /// Get buy/sell/neutral trade statistics for a security.
210    ///
211    /// Path: `GET /v1/quote/trades-statistics`
212    pub async fn trade_stats(&self, symbol: impl Into<String>) -> Result<TradeStatsResponse> {
213        #[derive(Serialize)]
214        struct Query {
215            counter_id: String,
216        }
217        self.get(
218            "/v1/quote/trades-statistics",
219            Query {
220                counter_id: symbol_to_counter_id(&symbol.into()),
221            },
222        )
223        .await
224    }
225
226    // ── anomaly ───────────────────────────────────────────────────
227
228    /// Get market anomaly alerts (unusual price/volume events).
229    ///
230    /// Path: `GET /v1/quote/changes`
231    pub async fn anomaly(&self, market: impl Into<String>) -> Result<AnomalyResponse> {
232        #[derive(Serialize)]
233        struct Query {
234            market: String,
235            category: &'static str,
236        }
237        self.get(
238            "/v1/quote/changes",
239            Query {
240                market: market.into().to_uppercase(),
241                category: "0",
242            },
243        )
244        .await
245    }
246
247    // ── constituent ───────────────────────────────────────────────
248
249    /// Get constituent stocks for an index.
250    ///
251    /// `symbol` should be an index symbol such as `"HSI.HK"`.
252    ///
253    /// Path: `GET /v1/quote/index-constituents`
254    pub async fn constituent(&self, symbol: impl Into<String>) -> Result<IndexConstituents> {
255        #[derive(Serialize)]
256        struct Query {
257            counter_id: String,
258        }
259        self.get(
260            "/v1/quote/index-constituents",
261            Query {
262                counter_id: index_symbol_to_counter_id(&symbol.into()),
263            },
264        )
265        .await
266    }
267}