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}