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}