Skip to main content

longport/trade/
context.rs

1use std::sync::Arc;
2
3use longport_httpcli::{HttpClient, Json, Method};
4use longport_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, CashFlow, EstimateMaxPurchaseQuantityOptions, Execution,
14        FundPositionsResponse, GetCashFlowOptions, GetFundPositionsOptions,
15        GetHistoryExecutionsOptions, GetHistoryOrdersOptions, GetStockPositionsOptions,
16        GetTodayExecutionsOptions, GetTodayOrdersOptions, MarginRatio, Order, OrderDetail,
17        PushEvent, ReplaceOrderOptions, StockPositionsResponse, SubmitOrderOptions, TopicType,
18        core::{Command, Core},
19    },
20};
21
22#[derive(Debug, Deserialize)]
23struct EmptyResponse {}
24
25/// Response for submit order request
26#[derive(Debug, Serialize, Deserialize)]
27pub struct SubmitOrderResponse {
28    /// Order id
29    pub order_id: String,
30}
31
32/// Response for estimate maximum purchase quantity
33#[derive(Debug, Serialize, Deserialize)]
34pub struct EstimateMaxPurchaseQuantityResponse {
35    /// Cash available quantity
36    #[serde(with = "serde_utils::decimal_empty_is_0")]
37    pub cash_max_qty: Decimal,
38    /// Margin available quantity
39    #[serde(with = "serde_utils::decimal_empty_is_0")]
40    pub margin_max_qty: Decimal,
41}
42
43struct InnerTradeContext {
44    command_tx: mpsc::UnboundedSender<Command>,
45    http_cli: HttpClient,
46    log_subscriber: Arc<dyn Subscriber + Send + Sync>,
47    /// Kept alive only so the background `Core::run` task can observe the
48    /// context being dropped (via this channel closing) and stop reconnecting.
49    _shutdown_tx: mpsc::UnboundedSender<()>,
50}
51
52impl Drop for InnerTradeContext {
53    fn drop(&mut self) {
54        dispatcher::with_default(&self.log_subscriber.clone().into(), || {
55            tracing::info!("trade context dropped");
56        });
57    }
58}
59
60/// Trade context
61#[derive(Clone)]
62pub struct TradeContext(Arc<InnerTradeContext>);
63
64impl TradeContext {
65    /// Create a `TradeContext`
66    pub fn new(config: Arc<Config>) -> (Self, mpsc::UnboundedReceiver<PushEvent>) {
67        let log_subscriber = config.create_log_subscriber("trade");
68
69        dispatcher::with_default(&log_subscriber.clone().into(), || {
70            tracing::info!(language = ?config.language, "creating trade context");
71        });
72
73        let http_cli = config.create_http_client();
74        let (command_tx, command_rx) = mpsc::unbounded_channel();
75        let (push_tx, push_rx) = mpsc::unbounded_channel();
76        let (shutdown_tx, shutdown_rx) = mpsc::unbounded_channel();
77        let core = Core::new(config, command_rx, push_tx);
78        crate::runtime::RUNTIME.handle().spawn(
79            core.run(shutdown_rx)
80                .with_subscriber(log_subscriber.clone()),
81        );
82
83        dispatcher::with_default(&log_subscriber.clone().into(), || {
84            tracing::info!("trade context created");
85        });
86
87        (
88            TradeContext(Arc::new(InnerTradeContext {
89                http_cli,
90                command_tx,
91                log_subscriber,
92                _shutdown_tx: shutdown_tx,
93            })),
94            push_rx,
95        )
96    }
97
98    /// Returns the log subscriber
99    #[inline]
100    pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
101        self.0.log_subscriber.clone()
102    }
103
104    /// Subscribe
105    ///
106    /// Reference: <https://open.longport.com/en/docs/trade/trade-push#subscribe>
107    ///
108    /// # Examples
109    ///
110    /// ```no_run
111    /// use std::sync::Arc;
112    ///
113    /// use longport::{
114    ///     Config, decimal,
115    ///     oauth::OAuthBuilder,
116    ///     trade::{OrderSide, OrderType, SubmitOrderOptions, TimeInForceType, TradeContext},
117    /// };
118    ///
119    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
120    /// let oauth = OAuthBuilder::new("your-client-id")
121    ///     .build(|url| println!("Visit: {url}"))
122    ///     .await?;
123    /// let config = Arc::new(Config::from_oauth(oauth));
124    /// let (ctx, mut receiver) = TradeContext::new(config);
125    ///
126    /// let opts = SubmitOrderOptions::new(
127    ///     "700.HK",
128    ///     OrderType::LO,
129    ///     OrderSide::Buy,
130    ///     decimal!(200),
131    ///     TimeInForceType::Day,
132    /// )
133    /// .submitted_price(decimal!(50i32));
134    /// let resp = ctx.submit_order(opts).await?;
135    /// println!("{:?}", resp);
136    ///
137    /// while let Some(event) = receiver.recv().await {
138    ///     println!("{:?}", event);
139    /// }
140    ///
141    /// # Ok::<_, Box<dyn std::error::Error>>(())
142    /// # });
143    /// ```
144    pub async fn subscribe<I>(&self, topics: I) -> Result<()>
145    where
146        I: IntoIterator<Item = TopicType>,
147    {
148        let (reply_tx, reply_rx) = oneshot::channel();
149        self.0
150            .command_tx
151            .send(Command::Subscribe {
152                topics: topics.into_iter().collect(),
153                reply_tx,
154            })
155            .map_err(|_| WsClientError::ClientClosed)?;
156        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
157    }
158
159    /// Unsubscribe
160    ///
161    /// Reference: <https://open.longport.com/en/docs/trade/trade-push#cancel-subscribe>
162    pub async fn unsubscribe<I>(&self, topics: I) -> Result<()>
163    where
164        I: IntoIterator<Item = TopicType>,
165    {
166        let (reply_tx, reply_rx) = oneshot::channel();
167        self.0
168            .command_tx
169            .send(Command::Unsubscribe {
170                topics: topics.into_iter().collect(),
171                reply_tx,
172            })
173            .map_err(|_| WsClientError::ClientClosed)?;
174        reply_rx.await.map_err(|_| WsClientError::ClientClosed)?
175    }
176
177    /// Get history executions
178    ///
179    /// Reference: <https://open.longport.com/en/docs/trade/execution/history_executions>
180    ///
181    /// # Examples
182    ///
183    /// ```no_run
184    /// use std::sync::Arc;
185    ///
186    /// use longport::{
187    ///     oauth::OAuthBuilder,
188    ///     trade::{GetHistoryExecutionsOptions, TradeContext},
189    ///     Config,
190    /// };
191    /// use time::macros::datetime;
192    ///
193    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
194    /// let oauth = OAuthBuilder::new("your-client-id")
195    ///     .build(|url| println!("Visit: {url}"))
196    ///     .await?;
197    /// let config = Arc::new(Config::from_oauth(oauth));
198    /// let (ctx, _) = TradeContext::new(config);
199    ///
200    /// let opts = GetHistoryExecutionsOptions::new()
201    ///     .symbol("700.HK")
202    ///     .start_at(datetime!(2022-05-09 0:00 UTC))
203    ///     .end_at(datetime!(2022-05-12 0:00 UTC));
204    /// let resp = ctx.history_executions(opts).await?;
205    /// println!("{:?}", resp);
206    /// # Ok::<_, Box<dyn std::error::Error>>(())
207    /// # });
208    /// ```
209    pub async fn history_executions(
210        &self,
211        options: impl Into<Option<GetHistoryExecutionsOptions>>,
212    ) -> Result<Vec<Execution>> {
213        #[derive(Deserialize)]
214        struct Response {
215            trades: Vec<Execution>,
216        }
217
218        Ok(self
219            .0
220            .http_cli
221            .request(Method::GET, "/v1/trade/execution/history")
222            .query_params(options.into().unwrap_or_default())
223            .response::<Json<Response>>()
224            .send()
225            .with_subscriber(self.0.log_subscriber.clone())
226            .await?
227            .0
228            .trades)
229    }
230
231    /// Get today executions
232    ///
233    /// Reference: <https://open.longport.com/en/docs/trade/execution/today_executions>
234    ///
235    /// # Examples
236    ///
237    /// ```no_run
238    /// use std::sync::Arc;
239    ///
240    /// use longport::{
241    ///     Config,
242    ///     oauth::OAuthBuilder,
243    ///     trade::{GetTodayExecutionsOptions, TradeContext},
244    /// };
245    ///
246    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
247    /// let oauth = OAuthBuilder::new("your-client-id")
248    ///     .build(|url| println!("Visit: {url}"))
249    ///     .await?;
250    /// let config = Arc::new(Config::from_oauth(oauth));
251    /// let (ctx, _) = TradeContext::new(config);
252    ///
253    /// let opts = GetTodayExecutionsOptions::new().symbol("700.HK");
254    /// let resp = ctx.today_executions(opts).await?;
255    /// println!("{:?}", resp);
256    /// # Ok::<_, Box<dyn std::error::Error>>(())
257    /// # });
258    /// ```
259    pub async fn today_executions(
260        &self,
261        options: impl Into<Option<GetTodayExecutionsOptions>>,
262    ) -> Result<Vec<Execution>> {
263        #[derive(Deserialize)]
264        struct Response {
265            trades: Vec<Execution>,
266        }
267
268        Ok(self
269            .0
270            .http_cli
271            .request(Method::GET, "/v1/trade/execution/today")
272            .query_params(options.into().unwrap_or_default())
273            .response::<Json<Response>>()
274            .send()
275            .with_subscriber(self.0.log_subscriber.clone())
276            .await?
277            .0
278            .trades)
279    }
280
281    /// Get history orders
282    ///
283    /// Reference: <https://open.longport.com/en/docs/trade/order/history_orders>
284    ///
285    /// # Examples
286    ///
287    /// ```no_run
288    /// use std::sync::Arc;
289    ///
290    /// use longport::{
291    ///     oauth::OAuthBuilder,
292    ///     trade::{GetHistoryOrdersOptions, OrderSide, OrderStatus, TradeContext},
293    ///     Config, Market,
294    /// };
295    /// use time::macros::datetime;
296    ///
297    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
298    /// let oauth = OAuthBuilder::new("your-client-id")
299    ///     .build(|url| println!("Visit: {url}"))
300    ///     .await?;
301    /// let config = Arc::new(Config::from_oauth(oauth));
302    /// let (ctx, _) = TradeContext::new(config);
303    ///
304    /// let opts = GetHistoryOrdersOptions::new()
305    ///     .symbol("700.HK")
306    ///     .status([OrderStatus::Filled, OrderStatus::New])
307    ///     .side(OrderSide::Buy)
308    ///     .market(Market::HK)
309    ///     .start_at(datetime!(2022-05-09 0:00 UTC))
310    ///     .end_at(datetime!(2022-05-12 0:00 UTC));
311    /// let resp = ctx.history_orders(opts).await?;
312    /// println!("{:?}", resp);
313    /// # Ok::<_, Box<dyn std::error::Error>>(())
314    /// # });
315    /// ```
316    pub async fn history_orders(
317        &self,
318        options: impl Into<Option<GetHistoryOrdersOptions>>,
319    ) -> Result<Vec<Order>> {
320        #[derive(Deserialize)]
321        struct Response {
322            orders: Vec<Order>,
323        }
324
325        Ok(self
326            .0
327            .http_cli
328            .request(Method::GET, "/v1/trade/order/history")
329            .query_params(options.into().unwrap_or_default())
330            .response::<Json<Response>>()
331            .send()
332            .with_subscriber(self.0.log_subscriber.clone())
333            .await?
334            .0
335            .orders)
336    }
337
338    /// Get today orders
339    ///
340    /// Reference: <https://open.longport.com/en/docs/trade/order/today_orders>
341    ///
342    /// # Examples
343    ///
344    /// ```no_run
345    /// use std::sync::Arc;
346    ///
347    /// use longport::{
348    ///     Config, Market,
349    ///     oauth::OAuthBuilder,
350    ///     trade::{GetTodayOrdersOptions, OrderSide, OrderStatus, TradeContext},
351    /// };
352    ///
353    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
354    /// let oauth = OAuthBuilder::new("your-client-id")
355    ///     .build(|url| println!("Visit: {url}"))
356    ///     .await?;
357    /// let config = Arc::new(Config::from_oauth(oauth));
358    /// let (ctx, _) = TradeContext::new(config);
359    ///
360    /// let opts = GetTodayOrdersOptions::new()
361    ///     .symbol("700.HK")
362    ///     .status([OrderStatus::Filled, OrderStatus::New])
363    ///     .side(OrderSide::Buy)
364    ///     .market(Market::HK);
365    /// let resp = ctx.today_orders(opts).await?;
366    /// println!("{:?}", resp);
367    /// # Ok::<_, Box<dyn std::error::Error>>(())
368    /// # });
369    /// ```
370    pub async fn today_orders(
371        &self,
372        options: impl Into<Option<GetTodayOrdersOptions>>,
373    ) -> Result<Vec<Order>> {
374        #[derive(Deserialize)]
375        struct Response {
376            orders: Vec<Order>,
377        }
378
379        Ok(self
380            .0
381            .http_cli
382            .request(Method::GET, "/v1/trade/order/today")
383            .query_params(options.into().unwrap_or_default())
384            .response::<Json<Response>>()
385            .send()
386            .with_subscriber(self.0.log_subscriber.clone())
387            .await?
388            .0
389            .orders)
390    }
391
392    /// Replace order
393    ///
394    /// Reference: <https://open.longport.com/en/docs/trade/order/replace>
395    ///
396    /// # Examples
397    ///
398    /// ```no_run
399    /// use std::sync::Arc;
400    ///
401    /// use longport::{
402    ///     Config, decimal,
403    ///     oauth::OAuthBuilder,
404    ///     trade::{ReplaceOrderOptions, TradeContext},
405    /// };
406    ///
407    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
408    /// let oauth = OAuthBuilder::new("your-client-id")
409    ///     .build(|url| println!("Visit: {url}"))
410    ///     .await?;
411    /// let config = Arc::new(Config::from_oauth(oauth));
412    /// let (ctx, _) = TradeContext::new(config);
413    ///
414    /// let opts =
415    ///     ReplaceOrderOptions::new("709043056541253632", decimal!(100)).price(decimal!(300i32));
416    /// let resp = ctx.replace_order(opts).await?;
417    /// println!("{:?}", resp);
418    /// # Ok::<_, Box<dyn std::error::Error>>(())
419    /// # });
420    /// ```
421    pub async fn replace_order(&self, options: ReplaceOrderOptions) -> Result<()> {
422        Ok(self
423            .0
424            .http_cli
425            .request(Method::PUT, "/v1/trade/order")
426            .body(Json(options))
427            .response::<Json<EmptyResponse>>()
428            .send()
429            .with_subscriber(self.0.log_subscriber.clone())
430            .await
431            .map(|_| ())?)
432    }
433
434    /// Submit order
435    ///
436    /// Reference: <https://open.longport.com/en/docs/trade/order/submit>
437    ///
438    /// # Examples
439    ///
440    /// ```no_run
441    /// use std::sync::Arc;
442    ///
443    /// use longport::{
444    ///     Config, decimal,
445    ///     oauth::OAuthBuilder,
446    ///     trade::{OrderSide, OrderType, SubmitOrderOptions, TimeInForceType, TradeContext},
447    /// };
448    ///
449    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
450    /// let oauth = OAuthBuilder::new("your-client-id")
451    ///     .build(|url| println!("Visit: {url}"))
452    ///     .await?;
453    /// let config = Arc::new(Config::from_oauth(oauth));
454    /// let (ctx, _) = TradeContext::new(config);
455    ///
456    /// let opts = SubmitOrderOptions::new(
457    ///     "700.HK",
458    ///     OrderType::LO,
459    ///     OrderSide::Buy,
460    ///     decimal!(200),
461    ///     TimeInForceType::Day,
462    /// )
463    /// .submitted_price(decimal!(50i32));
464    /// let resp = ctx.submit_order(opts).await?;
465    /// println!("{:?}", resp);
466    /// # Ok::<_, Box<dyn std::error::Error>>(())
467    /// # });
468    /// ```
469    pub async fn submit_order(&self, options: SubmitOrderOptions) -> Result<SubmitOrderResponse> {
470        let resp: SubmitOrderResponse = self
471            .0
472            .http_cli
473            .request(Method::POST, "/v1/trade/order")
474            .body(Json(options))
475            .response::<Json<_>>()
476            .send()
477            .with_subscriber(self.0.log_subscriber.clone())
478            .await?
479            .0;
480        _ = self.0.command_tx.send(Command::SubmittedOrder {
481            order_id: resp.order_id.clone(),
482        });
483        Ok(resp)
484    }
485
486    /// Cancel order
487    ///
488    /// Reference: <https://open.longport.com/en/docs/trade/order/withdraw>
489    ///
490    /// # Examples
491    ///
492    /// ```no_run
493    /// use std::sync::Arc;
494    ///
495    /// use longport::{Config, oauth::OAuthBuilder, trade::TradeContext};
496    ///
497    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
498    /// let oauth = OAuthBuilder::new("your-client-id")
499    ///     .build(|url| println!("Visit: {url}"))
500    ///     .await?;
501    /// let config = Arc::new(Config::from_oauth(oauth));
502    /// let (ctx, _) = TradeContext::new(config);
503    ///
504    /// ctx.cancel_order("709043056541253632").await?;
505    /// # Ok::<_, Box<dyn std::error::Error>>(())
506    /// # });
507    /// ```
508    pub async fn cancel_order(&self, order_id: impl Into<String>) -> Result<()> {
509        #[derive(Debug, Serialize)]
510        struct Request {
511            order_id: String,
512        }
513
514        Ok(self
515            .0
516            .http_cli
517            .request(Method::DELETE, "/v1/trade/order")
518            .response::<Json<EmptyResponse>>()
519            .query_params(Request {
520                order_id: order_id.into(),
521            })
522            .send()
523            .with_subscriber(self.0.log_subscriber.clone())
524            .await
525            .map(|_| ())?)
526    }
527
528    /// Get account balance
529    ///
530    /// Reference: <https://open.longport.com/en/docs/trade/asset/account>
531    ///
532    /// # Examples
533    ///
534    /// ```no_run
535    /// use std::sync::Arc;
536    ///
537    /// use longport::{Config, oauth::OAuthBuilder, trade::TradeContext};
538    ///
539    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
540    /// let oauth = OAuthBuilder::new("your-client-id")
541    ///     .build(|url| println!("Visit: {url}"))
542    ///     .await?;
543    /// let config = Arc::new(Config::from_oauth(oauth));
544    /// let (ctx, _) = TradeContext::new(config);
545    ///
546    /// let resp = ctx.account_balance(None).await?;
547    /// println!("{:?}", resp);
548    /// # Ok::<_, Box<dyn std::error::Error>>(())
549    /// # });
550    /// ```
551    pub async fn account_balance(&self, currency: Option<&str>) -> Result<Vec<AccountBalance>> {
552        #[derive(Debug, Serialize)]
553        struct Request<'a> {
554            currency: Option<&'a str>,
555        }
556
557        #[derive(Debug, Deserialize)]
558        struct Response {
559            list: Vec<AccountBalance>,
560        }
561
562        Ok(self
563            .0
564            .http_cli
565            .request(Method::GET, "/v1/asset/account")
566            .query_params(Request { currency })
567            .response::<Json<Response>>()
568            .send()
569            .with_subscriber(self.0.log_subscriber.clone())
570            .await?
571            .0
572            .list)
573    }
574
575    /// Get cash flow
576    ///
577    /// Reference: <https://open.longport.com/en/docs/trade/asset/cashflow>
578    ///
579    /// # Examples
580    ///
581    /// ```no_run
582    /// use std::sync::Arc;
583    ///
584    /// use longport::{
585    ///     oauth::OAuthBuilder,
586    ///     trade::{GetCashFlowOptions, TradeContext},
587    ///     Config,
588    /// };
589    /// use time::macros::datetime;
590    ///
591    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
592    /// let oauth = OAuthBuilder::new("your-client-id")
593    ///     .build(|url| println!("Visit: {url}"))
594    ///     .await?;
595    /// let config = Arc::new(Config::from_oauth(oauth));
596    /// let (ctx, _) = TradeContext::new(config);
597    ///
598    /// let opts = GetCashFlowOptions::new(datetime!(2022-05-09 0:00 UTC), datetime!(2022-05-12 0:00 UTC));
599    /// let resp = ctx.cash_flow(opts).await?;
600    /// println!("{:?}", resp);
601    /// # Ok::<_, Box<dyn std::error::Error>>(())
602    /// # });
603    /// ```
604    pub async fn cash_flow(&self, options: GetCashFlowOptions) -> Result<Vec<CashFlow>> {
605        #[derive(Debug, Deserialize)]
606        struct Response {
607            list: Vec<CashFlow>,
608        }
609
610        Ok(self
611            .0
612            .http_cli
613            .request(Method::GET, "/v1/asset/cashflow")
614            .query_params(options)
615            .response::<Json<Response>>()
616            .send()
617            .with_subscriber(self.0.log_subscriber.clone())
618            .await?
619            .0
620            .list)
621    }
622
623    /// Get fund positions
624    ///
625    /// Reference: <https://open.longport.com/en/docs/trade/asset/fund>
626    ///
627    /// # Examples
628    ///
629    /// ```no_run
630    /// use std::sync::Arc;
631    ///
632    /// use longport::{Config, oauth::OAuthBuilder, trade::TradeContext};
633    ///
634    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
635    /// let oauth = OAuthBuilder::new("your-client-id")
636    ///     .build(|url| println!("Visit: {url}"))
637    ///     .await?;
638    /// let config = Arc::new(Config::from_oauth(oauth));
639    /// let (ctx, _) = TradeContext::new(config);
640    ///
641    /// let resp = ctx.fund_positions(None).await?;
642    /// println!("{:?}", resp);
643    /// # Ok::<_, Box<dyn std::error::Error>>(())
644    /// # });
645    /// ```
646    pub async fn fund_positions(
647        &self,
648        opts: impl Into<Option<GetFundPositionsOptions>>,
649    ) -> Result<FundPositionsResponse> {
650        Ok(self
651            .0
652            .http_cli
653            .request(Method::GET, "/v1/asset/fund")
654            .query_params(opts.into().unwrap_or_default())
655            .response::<Json<FundPositionsResponse>>()
656            .send()
657            .with_subscriber(self.0.log_subscriber.clone())
658            .await?
659            .0)
660    }
661
662    /// Get stock positions
663    ///
664    /// Reference: <https://open.longport.com/en/docs/trade/asset/stock>
665    ///
666    /// # Examples
667    ///
668    /// ```no_run
669    /// use std::sync::Arc;
670    ///
671    /// use longport::{Config, oauth::OAuthBuilder, trade::TradeContext};
672    ///
673    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
674    /// let oauth = OAuthBuilder::new("your-client-id")
675    ///     .build(|url| println!("Visit: {url}"))
676    ///     .await?;
677    /// let config = Arc::new(Config::from_oauth(oauth));
678    /// let (ctx, _) = TradeContext::new(config);
679    ///
680    /// let resp = ctx.stock_positions(None).await?;
681    /// println!("{:?}", resp);
682    /// # Ok::<_, Box<dyn std::error::Error>>(())
683    /// # });
684    /// ```
685    pub async fn stock_positions(
686        &self,
687        opts: impl Into<Option<GetStockPositionsOptions>>,
688    ) -> Result<StockPositionsResponse> {
689        Ok(self
690            .0
691            .http_cli
692            .request(Method::GET, "/v1/asset/stock")
693            .query_params(opts.into().unwrap_or_default())
694            .response::<Json<StockPositionsResponse>>()
695            .send()
696            .with_subscriber(self.0.log_subscriber.clone())
697            .await?
698            .0)
699    }
700
701    /// Get margin ratio
702    ///
703    /// Reference: <https://open.longport.com/en/docs/trade/asset/margin_ratio>
704    ///
705    /// # Examples
706    ///
707    /// ```no_run
708    /// use std::sync::Arc;
709    ///
710    /// use longport::{Config, oauth::OAuthBuilder, trade::TradeContext};
711    ///
712    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
713    /// let oauth = OAuthBuilder::new("your-client-id")
714    ///     .build(|url| println!("Visit: {url}"))
715    ///     .await?;
716    /// let config = Arc::new(Config::from_oauth(oauth));
717    /// let (ctx, _) = TradeContext::new(config);
718    ///
719    /// let resp = ctx.margin_ratio("700.HK").await?;
720    /// println!("{:?}", resp);
721    /// # Ok::<_, Box<dyn std::error::Error>>(())
722    /// # });
723    /// ```
724    pub async fn margin_ratio(&self, symbol: impl Into<String>) -> Result<MarginRatio> {
725        #[derive(Debug, Serialize)]
726        struct Request {
727            symbol: String,
728        }
729
730        Ok(self
731            .0
732            .http_cli
733            .request(Method::GET, "/v1/risk/margin-ratio")
734            .query_params(Request {
735                symbol: symbol.into(),
736            })
737            .response::<Json<MarginRatio>>()
738            .send()
739            .with_subscriber(self.0.log_subscriber.clone())
740            .await?
741            .0)
742    }
743
744    /// Get order detail
745    ///
746    /// Reference: <https://open.longport.com/en/docs/trade/order/order_detail>
747    ///
748    /// # Examples
749    ///
750    /// ```no_run
751    /// use std::sync::Arc;
752    ///
753    /// use longport::{
754    ///     Config, Market,
755    ///     oauth::OAuthBuilder,
756    ///     trade::{GetHistoryOrdersOptions, OrderSide, OrderStatus, TradeContext},
757    /// };
758    /// use time::macros::datetime;
759    ///
760    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
761    /// let oauth = OAuthBuilder::new("your-client-id")
762    ///     .build(|url| println!("Visit: {url}"))
763    ///     .await?;
764    /// let config = Arc::new(Config::from_oauth(oauth));
765    /// let (ctx, _) = TradeContext::new(config);
766    ///
767    /// let resp = ctx.order_detail("701276261045858304").await?;
768    /// println!("{:?}", resp);
769    /// # Ok::<_, Box<dyn std::error::Error>>(())
770    /// # });
771    /// ```
772    pub async fn order_detail(&self, order_id: impl Into<String>) -> Result<OrderDetail> {
773        #[derive(Debug, Serialize)]
774        struct Request {
775            order_id: String,
776        }
777
778        Ok(self
779            .0
780            .http_cli
781            .request(Method::GET, "/v1/trade/order")
782            .response::<Json<OrderDetail>>()
783            .query_params(Request {
784                order_id: order_id.into(),
785            })
786            .send()
787            .with_subscriber(self.0.log_subscriber.clone())
788            .await?
789            .0)
790    }
791
792    /// Estimating the maximum purchase quantity for Hong Kong and US stocks,
793    /// warrants, and options
794    ///
795    ///
796    /// Reference: <https://open.longport.com/en/docs/trade/order/estimate_available_buy_limit>
797    ///
798    /// # Examples
799    ///
800    /// ```no_run
801    /// use std::sync::Arc;
802    ///
803    /// use longport::{
804    ///     Config,
805    ///     oauth::OAuthBuilder,
806    ///     trade::{EstimateMaxPurchaseQuantityOptions, OrderSide, OrderType, TradeContext},
807    /// };
808    /// use time::macros::datetime;
809    ///
810    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
811    /// let oauth = OAuthBuilder::new("your-client-id")
812    ///     .build(|url| println!("Visit: {url}"))
813    ///     .await?;
814    /// let config = Arc::new(Config::from_oauth(oauth));
815    /// let (ctx, _) = TradeContext::new(config);
816    ///
817    /// let resp = ctx
818    ///     .estimate_max_purchase_quantity(EstimateMaxPurchaseQuantityOptions::new(
819    ///         "700.HK",
820    ///         OrderType::LO,
821    ///         OrderSide::Buy,
822    ///     ))
823    ///     .await?;
824    /// println!("{:?}", resp);
825    /// # Ok::<_, Box<dyn std::error::Error>>(())
826    /// # });
827    /// ```
828    pub async fn estimate_max_purchase_quantity(
829        &self,
830        opts: EstimateMaxPurchaseQuantityOptions,
831    ) -> Result<EstimateMaxPurchaseQuantityResponse> {
832        Ok(self
833            .0
834            .http_cli
835            .request(Method::GET, "/v1/trade/estimate/buy_limit")
836            .query_params(opts)
837            .response::<Json<EstimateMaxPurchaseQuantityResponse>>()
838            .send()
839            .with_subscriber(self.0.log_subscriber.clone())
840            .await?
841            .0)
842    }
843}