Skip to main content

brokapi_bybit/
raw.rs

1//! Direct access to any Bybit API method (the "back door"): same key pair, same endpoint, same [`BrokerLimiter`] as the
2//! connector it came from.
3//!
4//! The connectors wrap what the shared traits can express. Everything else (amending an order in place, cancelling all
5//! orders, the batch methods, the take profit and stop loss of a position, leverage and margin mode, the transaction
6//! log, the demo funds, the other topics of the streams) is reachable here. Get a handle with `raw()` on either
7//! connector.
8//!
9//! Rules of a call:
10//!
11//! 1. The quota is taken first, by the operation name (`"CancelAll"`), from the route in `limits.toml`. A name without a
12//!    route spends the `"*"` default route, so a call is never free. A refusal is [`Error::RateLimited`] and the
13//!    request is not sent.
14//! 2. The closure gets the connector's [`BybitHttpClient`]: it signs and sends. One closure is one API method.
15//! 3. [`call`](BybitRaw::call) is for reads. [`call_command`](BybitRaw::call_command) is for anything that changes the
16//!    account: a break, a timeout or a 5xx is [`Error::Unknown`], and it is never retried.
17//! 4. [`connect_ws`](BybitRaw::connect_ws) opens one more session of a stream with the same key.
18//!
19//! The raw layer does not know about the connector's order state: orders placed or amended through it are not tracked
20//! (no events of the connector, no `reconcile`). Use the shared traits for orders.
21//!
22//! # Examples
23//!
24//! The amendment of an order in place keeps its queue place and its client id. The shared `replace` is not offered
25//! (it would have to give the order a new client id), the exchange method is here:
26//!
27//! ```
28//! # #[tokio::main(flavor = "current_thread")]
29//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
30//! # let server = brokapi_mocks::bybit::MockServer::start(Default::default()).await;
31//! # let cfg = brokapi_bybit_protocol::config::BybitConfig::for_mock("mock-key", "mock-secret", &server.http_base);
32//! use brokapi_bybit::limits::default_limits;
33//! use brokapi_bybit::{UserDataConfig, UserDataConnector};
34//! use brokapi_bybit_protocol::http::models::CreateOrderRequest;
35//! use brokapi_core::{OrderExecutor, ShutdownMode};
36//! use brokapi_runtime::BrokerLimiter;
37//!
38//! let (tx, _events) = tokio::sync::mpsc::channel(256);
39//! let ud = UserDataConnector::start(UserDataConfig::new(cfg, "bybit-ud"), BrokerLimiter::new(&default_limits()), tx).await?;
40//! ud.reconcile(&[]).await?;
41//! let raw = ud.raw();
42//! let order = raw
43//!     .call_command("PlaceOrder", |http| async move {
44//!         http.create_order(&CreateOrderRequest {
45//!             category: "linear".into(),
46//!             symbol: "MOCKUSDT".into(),
47//!             side: "Buy".into(),
48//!             order_type: "Limit".into(),
49//!             qty: "10".parse().unwrap(),
50//!             price: Some("1".parse().unwrap()),
51//!             ..Default::default()
52//!         })
53//!         .await
54//!     })
55//!     .await?;
56//! assert!(!order.order_id.is_empty());
57//! raw.call_command("CancelAll", |http| async move { http.cancel_all("linear", Some("MOCKUSDT"), "USDT").await })
58//!     .await?;
59//! ud.shutdown(ShutdownMode::Immediate).await;
60//! # Ok(())
61//! # }
62//! ```
63
64use std::future::Future;
65use std::sync::Arc;
66
67use brokapi_bybit_protocol::error::BybitApiError;
68use brokapi_bybit_protocol::http::client::BybitHttpClient;
69use brokapi_bybit_protocol::ws::models::WsIncoming;
70use brokapi_bybit_protocol::ws::session::{WsOptions, WsSession, WsStream};
71use brokapi_core::Error;
72use brokapi_runtime::BrokerLimiter;
73use kanal::AsyncSender;
74
75use crate::error::{map_command_err, map_err};
76
77/// A handle to the whole API with the quota of the connector it came from.
78#[derive(Clone)]
79pub struct BybitRaw {
80    http: BybitHttpClient,
81    limiter: Arc<BrokerLimiter>,
82}
83
84impl BybitRaw {
85    pub(crate) fn new(http: BybitHttpClient, limiter: Arc<BrokerLimiter>) -> Self {
86        Self { http, limiter }
87    }
88
89    /// Runs one read. The quota of `method` is taken first.
90    ///
91    /// # Errors
92    ///
93    /// [`Error::RateLimited`] when the quota is spent (nothing is sent), else the error of the call.
94    pub async fn call<T, F, Fut>(&self, method: &str, f: F) -> Result<T, Error>
95    where
96        F: FnOnce(BybitHttpClient) -> Fut,
97        Fut: Future<Output = Result<T, BybitApiError>>,
98    {
99        self.limiter.acquire(method, brokapi_runtime::now())?;
100        f(self.http.clone()).await.map_err(map_err)
101    }
102
103    /// Runs one command that changes the account. A lost answer is [`Error::Unknown`], and the call is never repeated.
104    ///
105    /// # Errors
106    ///
107    /// As [`BybitRaw::call`], with the unknown outcome of a command.
108    pub async fn call_command<T, F, Fut>(&self, method: &str, f: F) -> Result<T, Error>
109    where
110        F: FnOnce(BybitHttpClient) -> Fut,
111        Fut: Future<Output = Result<T, BybitApiError>>,
112    {
113        self.limiter.acquire(method, brokapi_runtime::now())?;
114        f(self.http.clone()).await.map_err(map_command_err)
115    }
116
117    /// Opens one more session of a stream with the same key. The frames arrive in `event_tx`.
118    ///
119    /// # Errors
120    ///
121    /// [`Error::RateLimited`] when the quota for connections is spent.
122    pub fn connect_ws(
123        &self,
124        stream: WsStream,
125        event_tx: AsyncSender<WsIncoming>,
126        options: WsOptions,
127    ) -> Result<WsSession, Error> {
128        self.limiter.acquire("ConnectWs", brokapi_runtime::now())?;
129        Ok(WsSession::spawn(
130            self.http.clone(),
131            stream,
132            event_tx,
133            options,
134        ))
135    }
136}