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