Skip to main content

longbridge/trade/
context.rs

1use std::sync::Arc;
2
3use longbridge_httpcli::{DcRegion, HttpClient, Json, Method};
4use longbridge_wscli::WsClientError;
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use tokio::sync::{mpsc, oneshot};
8use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
9
10use crate::{
11    Config, Result, serde_utils,
12    trade::{
13        AccountBalance, AllExecutionsResponse, CancelOrderOptions, CashFlow,
14        EstimateMaxPurchaseQuantityOptions, Execution, FundPositionsResponse,
15        GetAllExecutionsOptions, GetCashFlowOptions, GetFundPositionsOptions,
16        GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetOrderDetailOptions,
17        GetStockPositionsOptions, GetTodayExecutionsOptions, GetTodayOrdersOptions,
18        GetUSHistoryOrders, GetUSRealizedPLOptions, MarginRatio, Order, OrderDetail, OrderSide,
19        PushEvent, QueryUSOrdersOptions, QueryUSOrdersResponse, ReplaceOrderOptions,
20        StockPositionsResponse, SubmitOrderOptions, TopicType, USAssetOverview,
21        USOrderDetailResponse, USRealizedPL,
22        core::{Command, Core},
23    },
24};
25
26#[derive(Debug, Deserialize)]
27struct EmptyResponse {}
28
29/// Response for submit order request
30#[derive(Debug, Serialize, Deserialize)]
31pub struct SubmitOrderResponse {
32    /// Order id
33    pub order_id: String,
34}
35
36/// Response for estimate maximum purchase quantity
37#[derive(Debug, Serialize, Deserialize)]
38pub struct EstimateMaxPurchaseQuantityResponse {
39    /// Cash available quantity
40    #[serde(with = "serde_utils::decimal_empty_is_0")]
41    pub cash_max_qty: Decimal,
42    /// Margin available quantity
43    #[serde(with = "serde_utils::decimal_empty_is_0")]
44    pub margin_max_qty: Decimal,
45}
46
47struct InnerTradeContext {
48    command_tx: mpsc::UnboundedSender<Command>,
49    http_cli: HttpClient,
50    log_subscriber: Arc<dyn Subscriber + Send + Sync>,
51}
52
53impl Drop for InnerTradeContext {
54    fn drop(&mut self) {
55        dispatcher::with_default(&self.log_subscriber.clone().into(), || {
56            tracing::info!("trade context dropped");
57        });
58    }
59}
60
61/// Trade context
62#[derive(Clone)]
63pub struct TradeContext(Arc<InnerTradeContext>);
64
65impl TradeContext {
66    /// Create a `TradeContext`
67    pub fn new(config: Arc<Config>) -> (Self, mpsc::UnboundedReceiver<PushEvent>) {
68        let log_subscriber = config.create_log_subscriber("trade");
69
70        dispatcher::with_default(&log_subscriber.clone().into(), || {
71            tracing::info!(language = ?config.language, "creating trade context");
72        });
73
74        let http_cli = config.create_http_client();
75        let (command_tx, command_rx) = mpsc::unbounded_channel();
76        let (push_tx, push_rx) = mpsc::unbounded_channel();
77        let core = Core::new(config, command_rx, push_tx);
78        crate::runtime::RUNTIME
79            .handle()
80            .spawn(core.run().with_subscriber(log_subscriber.clone()));
81
82        dispatcher::with_default(&log_subscriber.clone().into(), || {
83            tracing::info!("trade context created");
84        });
85
86        (
87            TradeContext(Arc::new(InnerTradeContext {
88                http_cli,
89                command_tx,
90                log_subscriber,
91            })),
92            push_rx,
93        )
94    }
95
96    /// Returns the log subscriber
97    #[inline]
98    pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
99        self.0.log_subscriber.clone()
100    }
101
102    /// Subscribe
103    ///
104    /// Reference: <https://open.longbridge.com/en/docs/trade/trade-push#subscribe>
105    ///
106    /// # Examples
107    ///
108    /// ```no_run
109    /// use std::sync::Arc;
110    ///
111    /// use longbridge::{
112    ///     Config, decimal,
113    ///     oauth::OAuthBuilder,
114    ///     trade::{OrderSide, OrderType, SubmitOrderOptions, TimeInForceType, TradeContext},
115    /// };
116    ///
117    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
118    /// let oauth = OAuthBuilder::new("your-client-id")
119    ///     .build(|url| println!("Visit: {url}"))
120    ///     .await?;
121    /// let config = Arc::new(Config::from_oauth(oauth));
122    /// let (ctx, mut receiver) = TradeContext::new(config);
123    ///
124    /// let opts = SubmitOrderOptions::new(
125    ///     "700.HK",
126    ///     OrderType::LO,
127    ///     OrderSide::Buy,
128    ///     decimal!(200),
129    ///     TimeInForceType::Day,
130    /// )
131    /// .submitted_price(decimal!(50i32));
132    /// let resp = ctx.submit_order(opts).await?;
133    /// println!("{:?}", resp);
134    ///
135    /// while let Some(event) = receiver.recv().await {
136    ///     println!("{:?}", event);
137    /// }
138    ///
139    /// # Ok::<_, Box<dyn std::error::Error>>(())
140    /// # });
141    /// ```
142    pub async fn subscribe<I>(&self, topics: I) -> Result<()>
143    where
144        I: IntoIterator<Item = TopicType>,
145    {
146        let (reply_tx, reply_rx) = oneshot::channel();
147        self.0
148            .command_tx
149            .send(Command::Subscribe {
150                topics: topics.into_iter().collect(),
151                reply_tx,
152            })
153            .map_err(|_| WsClientError::ClientClosed)?;
154        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
155    }
156
157    /// Unsubscribe
158    ///
159    /// Reference: <https://open.longbridge.com/en/docs/trade/trade-push#cancel-subscribe>
160    pub async fn unsubscribe<I>(&self, topics: I) -> Result<()>
161    where
162        I: IntoIterator<Item = TopicType>,
163    {
164        let (reply_tx, reply_rx) = oneshot::channel();
165        self.0
166            .command_tx
167            .send(Command::Unsubscribe {
168                topics: topics.into_iter().collect(),
169                reply_tx,
170            })
171            .map_err(|_| WsClientError::ClientClosed)?;
172        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
173    }
174
175    /// Get history executions
176    ///
177    /// Reference: <https://open.longbridge.com/en/docs/trade/execution/history_executions>
178    ///
179    /// # Examples
180    ///
181    /// ```no_run
182    /// use std::sync::Arc;
183    ///
184    /// use longbridge::{
185    ///     oauth::OAuthBuilder,
186    ///     trade::{GetHistoryExecutionsOptions, TradeContext},
187    ///     Config,
188    /// };
189    /// use time::macros::datetime;
190    ///
191    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
192    /// let oauth = OAuthBuilder::new("your-client-id")
193    ///     .build(|url| println!("Visit: {url}"))
194    ///     .await?;
195    /// let config = Arc::new(Config::from_oauth(oauth));
196    /// let (ctx, _) = TradeContext::new(config);
197    ///
198    /// let opts = GetHistoryExecutionsOptions::new()
199    ///     .symbol("700.HK")
200    ///     .start_at(datetime!(2022-05-09 0:00 UTC))
201    ///     .end_at(datetime!(2022-05-12 0:00 UTC));
202    /// let resp = ctx.history_executions(opts).await?;
203    /// println!("{:?}", resp);
204    /// # Ok::<_, Box<dyn std::error::Error>>(())
205    /// # });
206    /// ```
207    pub async fn history_executions(
208        &self,
209        options: impl Into<Option<GetHistoryExecutionsOptions>>,
210    ) -> Result<Vec<Execution>> {
211        #[derive(Deserialize)]
212        struct Response {
213            trades: Vec<Execution>,
214        }
215
216        Ok(self
217            .0
218            .http_cli
219            .request(Method::GET, "/v1/trade/execution/history")
220            .query_params(options.into().unwrap_or_default())
221            .response::<Json<Response>>()
222            .send()
223            .with_subscriber(self.0.log_subscriber.clone())
224            .await?
225            .0
226            .trades)
227    }
228
229    /// Get today executions
230    ///
231    /// Reference: <https://open.longbridge.com/en/docs/trade/execution/today_executions>
232    ///
233    /// # Examples
234    ///
235    /// ```no_run
236    /// use std::sync::Arc;
237    ///
238    /// use longbridge::{
239    ///     Config,
240    ///     oauth::OAuthBuilder,
241    ///     trade::{GetTodayExecutionsOptions, TradeContext},
242    /// };
243    ///
244    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
245    /// let oauth = OAuthBuilder::new("your-client-id")
246    ///     .build(|url| println!("Visit: {url}"))
247    ///     .await?;
248    /// let config = Arc::new(Config::from_oauth(oauth));
249    /// let (ctx, _) = TradeContext::new(config);
250    ///
251    /// let opts = GetTodayExecutionsOptions::new().symbol("700.HK");
252    /// let resp = ctx.today_executions(opts).await?;
253    /// println!("{:?}", resp);
254    /// # Ok::<_, Box<dyn std::error::Error>>(())
255    /// # });
256    /// ```
257    pub async fn today_executions(
258        &self,
259        options: impl Into<Option<GetTodayExecutionsOptions>>,
260    ) -> Result<Vec<Execution>> {
261        #[derive(Deserialize)]
262        struct Response {
263            trades: Vec<Execution>,
264        }
265
266        Ok(self
267            .0
268            .http_cli
269            .request(Method::GET, "/v1/trade/execution/today")
270            .query_params(options.into().unwrap_or_default())
271            .response::<Json<Response>>()
272            .send()
273            .with_subscriber(self.0.log_subscriber.clone())
274            .await?
275            .0
276            .trades)
277    }
278
279    // TODO: temporarily disabled — restore when API is available
280    // Get all executions
281    //
282    // Reference: <https://open.longbridge.com/en/docs/trade/execution/all_executions>
283    // pub async fn all_executions(
284    // &self,
285    // options: impl Into<Option<GetAllExecutionsOptions>>,
286    // ) -> Result<AllExecutionsResponse> {
287    // Ok(self
288    // .0
289    // .http_cli
290    // .request(Method::GET, "/v3/trade/execution/all")
291    // .query_params(options.into().unwrap_or_default())
292    // .response::<Json<AllExecutionsResponse>>()
293    // .send()
294    // .with_subscriber(self.0.log_subscriber.clone())
295    // .await?
296    // .0)
297    // }
298
299    /// Get history orders
300    ///
301    /// Reference: <https://open.longbridge.com/en/docs/trade/order/history_orders>
302    ///
303    /// # Examples
304    ///
305    /// ```no_run
306    /// use std::sync::Arc;
307    ///
308    /// use longbridge::{
309    ///     oauth::OAuthBuilder,
310    ///     trade::{GetHistoryOrdersOptions, OrderSide, OrderStatus, TradeContext},
311    ///     Config, Market,
312    /// };
313    /// use time::macros::datetime;
314    ///
315    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
316    /// let oauth = OAuthBuilder::new("your-client-id")
317    ///     .build(|url| println!("Visit: {url}"))
318    ///     .await?;
319    /// let config = Arc::new(Config::from_oauth(oauth));
320    /// let (ctx, _) = TradeContext::new(config);
321    ///
322    /// let opts = GetHistoryOrdersOptions::new()
323    ///     .symbol("700.HK")
324    ///     .status([OrderStatus::Filled, OrderStatus::New])
325    ///     .side(OrderSide::Buy)
326    ///     .market(Market::HK)
327    ///     .start_at(datetime!(2022-05-09 0:00 UTC))
328    ///     .end_at(datetime!(2022-05-12 0:00 UTC));
329    /// let resp = ctx.history_orders(opts).await?;
330    /// println!("{:?}", resp);
331    /// # Ok::<_, Box<dyn std::error::Error>>(())
332    /// # });
333    /// ```
334    pub async fn history_orders(
335        &self,
336        options: impl Into<Option<GetHistoryOrdersOptions>>,
337    ) -> Result<Vec<Order>> {
338        #[derive(Deserialize)]
339        struct Response {
340            orders: Vec<Order>,
341        }
342
343        Ok(self
344            .0
345            .http_cli
346            .request(Method::GET, "/v1/trade/order/history")
347            .query_params(options.into().unwrap_or_default())
348            .response::<Json<Response>>()
349            .send()
350            .with_subscriber(self.0.log_subscriber.clone())
351            .await?
352            .0
353            .orders)
354    }
355
356    /// Get today orders
357    ///
358    /// Reference: <https://open.longbridge.com/en/docs/trade/order/today_orders>
359    ///
360    /// # Examples
361    ///
362    /// ```no_run
363    /// use std::sync::Arc;
364    ///
365    /// use longbridge::{
366    ///     Config, Market,
367    ///     oauth::OAuthBuilder,
368    ///     trade::{GetTodayOrdersOptions, OrderSide, OrderStatus, TradeContext},
369    /// };
370    ///
371    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
372    /// let oauth = OAuthBuilder::new("your-client-id")
373    ///     .build(|url| println!("Visit: {url}"))
374    ///     .await?;
375    /// let config = Arc::new(Config::from_oauth(oauth));
376    /// let (ctx, _) = TradeContext::new(config);
377    ///
378    /// let opts = GetTodayOrdersOptions::new()
379    ///     .symbol("700.HK")
380    ///     .status([OrderStatus::Filled, OrderStatus::New])
381    ///     .side(OrderSide::Buy)
382    ///     .market(Market::HK);
383    /// let resp = ctx.today_orders(opts).await?;
384    /// println!("{:?}", resp);
385    /// # Ok::<_, Box<dyn std::error::Error>>(())
386    /// # });
387    /// ```
388    pub async fn today_orders(
389        &self,
390        options: impl Into<Option<GetTodayOrdersOptions>>,
391    ) -> Result<Vec<Order>> {
392        #[derive(Deserialize)]
393        struct Response {
394            orders: Vec<Order>,
395        }
396
397        Ok(self
398            .0
399            .http_cli
400            .request(Method::GET, "/v1/trade/order/today")
401            .query_params(options.into().unwrap_or_default())
402            .response::<Json<Response>>()
403            .send()
404            .with_subscriber(self.0.log_subscriber.clone())
405            .await?
406            .0
407            .orders)
408    }
409
410    /// Replace order
411    ///
412    /// Reference: <https://open.longbridge.com/en/docs/trade/order/replace>
413    ///
414    /// # Examples
415    ///
416    /// ```no_run
417    /// use std::sync::Arc;
418    ///
419    /// use longbridge::{
420    ///     Config, decimal,
421    ///     oauth::OAuthBuilder,
422    ///     trade::{ReplaceOrderOptions, TradeContext},
423    /// };
424    ///
425    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
426    /// let oauth = OAuthBuilder::new("your-client-id")
427    ///     .build(|url| println!("Visit: {url}"))
428    ///     .await?;
429    /// let config = Arc::new(Config::from_oauth(oauth));
430    /// let (ctx, _) = TradeContext::new(config);
431    ///
432    /// let opts =
433    ///     ReplaceOrderOptions::new("709043056541253632", decimal!(100)).price(decimal!(300i32));
434    /// let resp = ctx.replace_order(opts).await?;
435    /// println!("{:?}", resp);
436    /// # Ok::<_, Box<dyn std::error::Error>>(())
437    /// # });
438    /// ```
439    pub async fn replace_order(&self, options: ReplaceOrderOptions) -> Result<()> {
440        Ok(self
441            .0
442            .http_cli
443            .request(Method::PUT, "/v1/trade/order")
444            .body(Json(options))
445            .response::<Json<EmptyResponse>>()
446            .send()
447            .with_subscriber(self.0.log_subscriber.clone())
448            .await
449            .map(|_| ())?)
450    }
451
452    /// Submit order
453    ///
454    /// Reference: <https://open.longbridge.com/en/docs/trade/order/submit>
455    ///
456    /// # Examples
457    ///
458    /// ```no_run
459    /// use std::sync::Arc;
460    ///
461    /// use longbridge::{
462    ///     Config, decimal,
463    ///     oauth::OAuthBuilder,
464    ///     trade::{OrderSide, OrderType, SubmitOrderOptions, TimeInForceType, TradeContext},
465    /// };
466    ///
467    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
468    /// let oauth = OAuthBuilder::new("your-client-id")
469    ///     .build(|url| println!("Visit: {url}"))
470    ///     .await?;
471    /// let config = Arc::new(Config::from_oauth(oauth));
472    /// let (ctx, _) = TradeContext::new(config);
473    ///
474    /// let opts = SubmitOrderOptions::new(
475    ///     "700.HK",
476    ///     OrderType::LO,
477    ///     OrderSide::Buy,
478    ///     decimal!(200),
479    ///     TimeInForceType::Day,
480    /// )
481    /// .submitted_price(decimal!(50i32));
482    /// let resp = ctx.submit_order(opts).await?;
483    /// println!("{:?}", resp);
484    /// # Ok::<_, Box<dyn std::error::Error>>(())
485    /// # });
486    /// ```
487    pub async fn submit_order(&self, options: SubmitOrderOptions) -> Result<SubmitOrderResponse> {
488        let resp: SubmitOrderResponse = self
489            .0
490            .http_cli
491            .request(Method::POST, "/v1/trade/order")
492            .body(Json(options))
493            .response::<Json<_>>()
494            .send()
495            .with_subscriber(self.0.log_subscriber.clone())
496            .await?
497            .0;
498        _ = self.0.command_tx.send(Command::SubmittedOrder {
499            order_id: resp.order_id.clone(),
500        });
501        Ok(resp)
502    }
503
504    /// Cancel order
505    ///
506    /// Reference: <https://open.longbridge.com/en/docs/trade/order/withdraw>
507    ///
508    /// # Examples
509    ///
510    /// ```no_run
511    /// use std::sync::Arc;
512    ///
513    /// use longbridge::{Config, oauth::OAuthBuilder, trade::TradeContext};
514    ///
515    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
516    /// let oauth = OAuthBuilder::new("your-client-id")
517    ///     .build(|url| println!("Visit: {url}"))
518    ///     .await?;
519    /// let config = Arc::new(Config::from_oauth(oauth));
520    /// let (ctx, _) = TradeContext::new(config);
521    ///
522    /// ctx.cancel_order("709043056541253632").await?;
523    /// # Ok::<_, Box<dyn std::error::Error>>(())
524    /// # });
525    /// ```
526    pub async fn cancel_order(&self, options: impl Into<CancelOrderOptions>) -> Result<()> {
527        Ok(self
528            .0
529            .http_cli
530            .request(Method::DELETE, "/v1/trade/order")
531            .response::<Json<EmptyResponse>>()
532            .query_params(options.into())
533            .send()
534            .with_subscriber(self.0.log_subscriber.clone())
535            .await
536            .map(|_| ())?)
537    }
538
539    /// Get account balance
540    ///
541    /// Reference: <https://open.longbridge.com/en/docs/trade/asset/account>
542    ///
543    /// # Examples
544    ///
545    /// ```no_run
546    /// use std::sync::Arc;
547    ///
548    /// use longbridge::{Config, oauth::OAuthBuilder, trade::TradeContext};
549    ///
550    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
551    /// let oauth = OAuthBuilder::new("your-client-id")
552    ///     .build(|url| println!("Visit: {url}"))
553    ///     .await?;
554    /// let config = Arc::new(Config::from_oauth(oauth));
555    /// let (ctx, _) = TradeContext::new(config);
556    ///
557    /// let resp = ctx.account_balance(None).await?;
558    /// println!("{:?}", resp);
559    /// # Ok::<_, Box<dyn std::error::Error>>(())
560    /// # });
561    /// ```
562    pub async fn account_balance(&self, currency: Option<&str>) -> Result<Vec<AccountBalance>> {
563        #[derive(Debug, Serialize)]
564        struct Request<'a> {
565            currency: Option<&'a str>,
566        }
567
568        #[derive(Debug, Deserialize)]
569        struct Response {
570            list: Vec<AccountBalance>,
571        }
572
573        Ok(self
574            .0
575            .http_cli
576            .request(Method::GET, "/v1/asset/account")
577            .query_params(Request { currency })
578            .response::<Json<Response>>()
579            .send()
580            .with_subscriber(self.0.log_subscriber.clone())
581            .await?
582            .0
583            .list)
584    }
585
586    /// Get cash flow
587    ///
588    /// Reference: <https://open.longbridge.com/en/docs/trade/asset/cashflow>
589    ///
590    /// # Examples
591    ///
592    /// ```no_run
593    /// use std::sync::Arc;
594    ///
595    /// use longbridge::{
596    ///     oauth::OAuthBuilder,
597    ///     trade::{GetCashFlowOptions, TradeContext},
598    ///     Config,
599    /// };
600    /// use time::macros::datetime;
601    ///
602    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
603    /// let oauth = OAuthBuilder::new("your-client-id")
604    ///     .build(|url| println!("Visit: {url}"))
605    ///     .await?;
606    /// let config = Arc::new(Config::from_oauth(oauth));
607    /// let (ctx, _) = TradeContext::new(config);
608    ///
609    /// let opts = GetCashFlowOptions::new(datetime!(2022-05-09 0:00 UTC), datetime!(2022-05-12 0:00 UTC));
610    /// let resp = ctx.cash_flow(opts).await?;
611    /// println!("{:?}", resp);
612    /// # Ok::<_, Box<dyn std::error::Error>>(())
613    /// # });
614    /// ```
615    pub async fn cash_flow(&self, options: GetCashFlowOptions) -> Result<Vec<CashFlow>> {
616        #[derive(Debug, Deserialize)]
617        struct Response {
618            list: Vec<CashFlow>,
619        }
620
621        Ok(self
622            .0
623            .http_cli
624            .request(Method::GET, "/v1/asset/cashflow")
625            .query_params(options)
626            .response::<Json<Response>>()
627            .send()
628            .with_subscriber(self.0.log_subscriber.clone())
629            .await?
630            .0
631            .list)
632    }
633
634    /// Get fund positions
635    ///
636    /// Reference: <https://open.longbridge.com/en/docs/trade/asset/fund>
637    ///
638    /// # Examples
639    ///
640    /// ```no_run
641    /// use std::sync::Arc;
642    ///
643    /// use longbridge::{Config, oauth::OAuthBuilder, trade::TradeContext};
644    ///
645    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
646    /// let oauth = OAuthBuilder::new("your-client-id")
647    ///     .build(|url| println!("Visit: {url}"))
648    ///     .await?;
649    /// let config = Arc::new(Config::from_oauth(oauth));
650    /// let (ctx, _) = TradeContext::new(config);
651    ///
652    /// let resp = ctx.fund_positions(None).await?;
653    /// println!("{:?}", resp);
654    /// # Ok::<_, Box<dyn std::error::Error>>(())
655    /// # });
656    /// ```
657    pub async fn fund_positions(
658        &self,
659        opts: impl Into<Option<GetFundPositionsOptions>>,
660    ) -> Result<FundPositionsResponse> {
661        Ok(self
662            .0
663            .http_cli
664            .request(Method::GET, "/v1/asset/fund")
665            .query_params(opts.into().unwrap_or_default())
666            .response::<Json<FundPositionsResponse>>()
667            .send()
668            .with_subscriber(self.0.log_subscriber.clone())
669            .await?
670            .0)
671    }
672
673    /// Get stock positions
674    ///
675    /// Reference: <https://open.longbridge.com/en/docs/trade/asset/stock>
676    ///
677    /// # Examples
678    ///
679    /// ```no_run
680    /// use std::sync::Arc;
681    ///
682    /// use longbridge::{Config, oauth::OAuthBuilder, trade::TradeContext};
683    ///
684    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
685    /// let oauth = OAuthBuilder::new("your-client-id")
686    ///     .build(|url| println!("Visit: {url}"))
687    ///     .await?;
688    /// let config = Arc::new(Config::from_oauth(oauth));
689    /// let (ctx, _) = TradeContext::new(config);
690    ///
691    /// let resp = ctx.stock_positions(None).await?;
692    /// println!("{:?}", resp);
693    /// # Ok::<_, Box<dyn std::error::Error>>(())
694    /// # });
695    /// ```
696    pub async fn stock_positions(
697        &self,
698        opts: impl Into<Option<GetStockPositionsOptions>>,
699    ) -> Result<StockPositionsResponse> {
700        Ok(self
701            .0
702            .http_cli
703            .request(Method::GET, "/v1/asset/stock")
704            .query_params(opts.into().unwrap_or_default())
705            .response::<Json<StockPositionsResponse>>()
706            .send()
707            .with_subscriber(self.0.log_subscriber.clone())
708            .await?
709            .0)
710    }
711
712    /// Get margin ratio
713    ///
714    /// Reference: <https://open.longbridge.com/en/docs/trade/asset/margin_ratio>
715    ///
716    /// # Examples
717    ///
718    /// ```no_run
719    /// use std::sync::Arc;
720    ///
721    /// use longbridge::{Config, oauth::OAuthBuilder, trade::TradeContext};
722    ///
723    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
724    /// let oauth = OAuthBuilder::new("your-client-id")
725    ///     .build(|url| println!("Visit: {url}"))
726    ///     .await?;
727    /// let config = Arc::new(Config::from_oauth(oauth));
728    /// let (ctx, _) = TradeContext::new(config);
729    ///
730    /// let resp = ctx.margin_ratio("700.HK").await?;
731    /// println!("{:?}", resp);
732    /// # Ok::<_, Box<dyn std::error::Error>>(())
733    /// # });
734    /// ```
735    pub async fn margin_ratio(&self, symbol: impl Into<String>) -> Result<MarginRatio> {
736        #[derive(Debug, Serialize)]
737        struct Request {
738            symbol: String,
739        }
740
741        Ok(self
742            .0
743            .http_cli
744            .request(Method::GET, "/v1/risk/margin-ratio")
745            .query_params(Request {
746                symbol: symbol.into(),
747            })
748            .response::<Json<MarginRatio>>()
749            .send()
750            .with_subscriber(self.0.log_subscriber.clone())
751            .await?
752            .0)
753    }
754
755    /// Get order detail
756    ///
757    /// Reference: <https://open.longbridge.com/en/docs/trade/order/order_detail>
758    ///
759    /// # Examples
760    ///
761    /// ```no_run
762    /// use std::sync::Arc;
763    ///
764    /// use longbridge::{
765    ///     Config, Market,
766    ///     oauth::OAuthBuilder,
767    ///     trade::{GetHistoryOrdersOptions, OrderSide, OrderStatus, TradeContext},
768    /// };
769    /// use time::macros::datetime;
770    ///
771    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
772    /// let oauth = OAuthBuilder::new("your-client-id")
773    ///     .build(|url| println!("Visit: {url}"))
774    ///     .await?;
775    /// let config = Arc::new(Config::from_oauth(oauth));
776    /// let (ctx, _) = TradeContext::new(config);
777    ///
778    /// let resp = ctx.order_detail("701276261045858304").await?;
779    /// println!("{:?}", resp);
780    /// # Ok::<_, Box<dyn std::error::Error>>(())
781    /// # });
782    /// ```
783    pub async fn order_detail(
784        &self,
785        options: impl Into<GetOrderDetailOptions>,
786    ) -> Result<OrderDetail> {
787        Ok(self
788            .0
789            .http_cli
790            .request(Method::GET, "/v1/trade/order")
791            .response::<Json<OrderDetail>>()
792            .query_params(options.into())
793            .send()
794            .with_subscriber(self.0.log_subscriber.clone())
795            .await?
796            .0)
797    }
798
799    /// Estimating the maximum purchase quantity for Hong Kong and US stocks,
800    /// warrants, and options
801    ///
802    ///
803    /// Reference: <https://open.longbridge.com/en/docs/trade/order/estimate_available_buy_limit>
804    ///
805    /// # Examples
806    ///
807    /// ```no_run
808    /// use std::sync::Arc;
809    ///
810    /// use longbridge::{
811    ///     Config,
812    ///     oauth::OAuthBuilder,
813    ///     trade::{EstimateMaxPurchaseQuantityOptions, OrderSide, OrderType, TradeContext},
814    /// };
815    /// use time::macros::datetime;
816    ///
817    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
818    /// let oauth = OAuthBuilder::new("your-client-id")
819    ///     .build(|url| println!("Visit: {url}"))
820    ///     .await?;
821    /// let config = Arc::new(Config::from_oauth(oauth));
822    /// let (ctx, _) = TradeContext::new(config);
823    ///
824    /// let resp = ctx
825    ///     .estimate_max_purchase_quantity(EstimateMaxPurchaseQuantityOptions::new(
826    ///         "700.HK",
827    ///         OrderType::LO,
828    ///         OrderSide::Buy,
829    ///     ))
830    ///     .await?;
831    /// println!("{:?}", resp);
832    /// # Ok::<_, Box<dyn std::error::Error>>(())
833    /// # });
834    /// ```
835    pub async fn estimate_max_purchase_quantity(
836        &self,
837        opts: EstimateMaxPurchaseQuantityOptions,
838    ) -> Result<EstimateMaxPurchaseQuantityResponse> {
839        Ok(self
840            .0
841            .http_cli
842            .request(Method::GET, "/v1/trade/estimate/buy_limit")
843            .query_params(opts)
844            .response::<Json<EstimateMaxPurchaseQuantityResponse>>()
845            .send()
846            .with_subscriber(self.0.log_subscriber.clone())
847            .await?
848            .0)
849    }
850
851    // ── US-market APIs ────────────────────────────────────────────────────────
852
853    /// Query the paginated US order list.
854    ///
855    /// Path: `POST /v1/us/orders/query`
856    ///
857    /// US token required.
858    pub async fn us_query_orders(&self, opts: GetUSHistoryOrders) -> Result<QueryUSOrdersResponse> {
859        use std::time::{SystemTime, UNIX_EPOCH};
860
861        use crate::utils::counter::symbol_to_counter_id;
862
863        let now = SystemTime::now()
864            .duration_since(UNIX_EPOCH)
865            .unwrap_or_default()
866            .as_secs() as i64;
867
868        let action = match opts.side {
869            OrderSide::Buy => 1,
870            OrderSide::Sell => 2,
871            _ => 0,
872        };
873
874        let counter_ids = opts
875            .symbol
876            .as_deref()
877            .filter(|s| !s.is_empty())
878            .map(|s| vec![symbol_to_counter_id(s)])
879            .unwrap_or_default();
880
881        let start_at = if opts.start_at == 0 {
882            (now - 90 * 24 * 3600) as f64
883        } else {
884            opts.start_at as f64
885        };
886        let end_at = if opts.end_at == 0 {
887            now as f64
888        } else {
889            opts.end_at as f64
890        };
891        let page = if opts.page <= 0 { 1 } else { opts.page };
892        let limit = if opts.limit <= 0 { 20 } else { opts.limit };
893
894        let body = super::types::USQueryOrdersBody {
895            account_channel: String::new(),
896            action,
897            start_at,
898            end_at,
899            counter_ids,
900            security_types: vec![],
901            query_type: opts.query_type,
902            page,
903            limit,
904            query_version: now as f64,
905        };
906
907        Ok(self
908            .0
909            .http_cli
910            .request(Method::POST, "/v1/us/orders/query")
911            .dc_restrict(DcRegion::Us)
912            .body(Json(body))
913            .response::<Json<QueryUSOrdersResponse>>()
914            .send()
915            .with_subscriber(self.0.log_subscriber.clone())
916            .await?
917            .0)
918    }
919
920    /// Get US order detail.
921    ///
922    /// Path: `GET /v1/us/orders/{order_id}`
923    ///
924    /// US token required.
925    pub async fn us_order_detail(
926        &self,
927        order_id: impl Into<String>,
928    ) -> Result<USOrderDetailResponse> {
929        let order_id = order_id.into();
930        let path = format!("/v1/us/orders/{order_id}");
931
932        Ok(self
933            .0
934            .http_cli
935            .request(Method::GET, path.as_str())
936            .dc_restrict(DcRegion::Us)
937            .response::<Json<USOrderDetailResponse>>()
938            .send()
939            .with_subscriber(self.0.log_subscriber.clone())
940            .await?
941            .0)
942    }
943
944    /// Get the full US account asset snapshot (stocks, options, crypto, buying
945    /// power).
946    ///
947    /// Path: `GET /v1/us/assets/overview`
948    ///
949    /// US token required.
950    pub async fn us_asset_overview(&self) -> Result<USAssetOverview> {
951        Ok(self
952            .0
953            .http_cli
954            .request(Method::GET, "/v1/us/assets/overview")
955            .dc_restrict(DcRegion::Us)
956            .response::<Json<USAssetOverview>>()
957            .send()
958            .with_subscriber(self.0.log_subscriber.clone())
959            .await?
960            .0)
961    }
962
963    /// Get realized profit-and-loss for the US account.
964    ///
965    /// `currency`: required, e.g. `"USD"`.
966    /// `category`: optional filter — `"ALL"`, `"STOCK"`, `"OPTION"`, or
967    /// `"CRYPTO"`.
968    ///
969    /// Path: `GET /v1/us/assets/pl/realized`
970    ///
971    /// US token required.
972    pub async fn us_realized_pl(&self, opts: GetUSRealizedPLOptions) -> Result<USRealizedPL> {
973        #[derive(Serialize)]
974        struct Query {
975            currency: String,
976            #[serde(skip_serializing_if = "Option::is_none")]
977            category: Option<String>,
978        }
979
980        let currency = if opts.currency.is_empty() {
981            "USD".to_string()
982        } else {
983            opts.currency
984        };
985        let category = if opts.category.is_empty() {
986            None
987        } else {
988            Some(opts.category)
989        };
990
991        Ok(self
992            .0
993            .http_cli
994            .request(Method::GET, "/v1/us/assets/pl/realized")
995            .dc_restrict(DcRegion::Us)
996            .query_params(Query { currency, category })
997            .response::<Json<USRealizedPL>>()
998            .send()
999            .with_subscriber(self.0.log_subscriber.clone())
1000            .await?
1001            .0)
1002    }
1003}