Skip to main content

bullet_rust_sdk/
trading.rs

1//! High-level convenience methods for common trading operations.
2//!
3//! These methods handle `CallMessage` construction, transaction signing, and
4//! submission internally — reducing a typical order flow from ~15 lines to ~5.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use bullet_rust_sdk::*;
10//!
11//! let client = Client::builder()
12//!     .network(Network::Mainnet)
13//!     .keypair(keypair)
14//!     .build()
15//!     .await?;
16//!
17//! // Place a limit buy
18//! let market_id = client.market_id("BTC-USD").unwrap();
19//! let resp = client.place_orders(
20//!     market_id,
21//!     vec![NewOrderArgs::limit(price, size, Side::Bid)],
22//!     false,
23//!     None,
24//! ).await?;
25//! ```
26
27use bullet_exchange_interface::decimals::PositiveDecimal;
28use bullet_exchange_interface::message::{AmendOrderArgs, CancelOrderArgs, NewOrderArgs};
29use bullet_exchange_interface::types::{MarketId, OrderType, Side};
30
31use crate::generated::types::SubmitTxResponse;
32use crate::types::{CallMessage, UserAction};
33use crate::{Client, SDKError, SDKResult, Transaction};
34
35// ── Order construction helpers ──────────────────────────────────────────────
36
37/// Extension constructors for [`NewOrderArgs`].
38///
39/// Removes the 4-field boilerplate from simple orders. For advanced fields
40/// (`reduce_only`, `client_order_id`, `pending_tpsl_pair`), construct
41/// `NewOrderArgs` directly.
42///
43/// ```ignore
44/// use bullet_rust_sdk::*;
45///
46/// let order = NewOrderArgs::limit(price, size, Side::Bid);
47/// client.place_orders(market_id, vec![order], false, None).await?;
48/// ```
49pub trait NewOrderExt {
50    /// Create a limit order. Defaults: `reduce_only: false`, no client order ID, no TP/SL.
51    fn limit(price: PositiveDecimal, size: PositiveDecimal, side: Side) -> Self;
52    /// Create a post-only (maker) order. Rejected if it would cross the book.
53    fn post_only(price: PositiveDecimal, size: PositiveDecimal, side: Side) -> Self;
54    /// Create an immediate-or-cancel order (market-order equivalent).
55    ///
56    /// Fills what it can at the given price, cancels the rest.
57    fn ioc(price: PositiveDecimal, size: PositiveDecimal, side: Side) -> Self;
58}
59
60impl NewOrderExt for NewOrderArgs {
61    fn limit(price: PositiveDecimal, size: PositiveDecimal, side: Side) -> Self {
62        new_order(price, size, side, OrderType::Limit)
63    }
64
65    fn post_only(price: PositiveDecimal, size: PositiveDecimal, side: Side) -> Self {
66        new_order(price, size, side, OrderType::PostOnly)
67    }
68
69    fn ioc(price: PositiveDecimal, size: PositiveDecimal, side: Side) -> Self {
70        new_order(price, size, side, OrderType::ImmediateOrCancel)
71    }
72}
73
74fn new_order(
75    price: PositiveDecimal,
76    size: PositiveDecimal,
77    side: Side,
78    order_type: OrderType,
79) -> NewOrderArgs {
80    NewOrderArgs {
81        price,
82        size,
83        side,
84        order_type,
85        reduce_only: false,
86        client_order_id: None,
87        pending_tpsl_pair: None,
88    }
89}
90
91impl Client {
92    /// Place orders on a market. Signs and submits the transaction.
93    ///
94    /// # Arguments
95    ///
96    /// * `market_id` — Numeric market ID (resolve via `client.market_id("BTC-USD")`)
97    /// * `orders` — One or more orders to place
98    /// * `replace` — If `true`, cancel existing orders before placing new ones
99    /// * `sub_account_index` — `None` for the main account, `Some(n)` for a sub-account
100    ///
101    /// # Example
102    ///
103    /// ```ignore
104    /// use bullet_rust_sdk::*;
105    ///
106    /// let market_id = client.market_id("BTC-USD").unwrap();
107    /// let price = PositiveDecimal::try_from(rust_decimal::Decimal::from(50000))?;
108    /// let size = PositiveDecimal::try_from(rust_decimal::Decimal::new(1, 3))?;
109    /// let resp = client.place_orders(
110    ///     market_id,
111    ///     vec![NewOrderArgs::limit(price, size, Side::Bid)],
112    ///     false,
113    ///     None,
114    /// ).await?;
115    /// println!("TX: {}, status: {:?}", resp.id, resp.status);
116    /// ```
117    pub async fn place_orders(
118        &self,
119        market_id: MarketId,
120        orders: Vec<NewOrderArgs>,
121        replace: bool,
122        sub_account_index: Option<u8>,
123    ) -> SDKResult<SubmitTxResponse> {
124        let call_msg = CallMessage::User(UserAction::PlaceOrders {
125            market_id,
126            orders,
127            replace,
128            sub_account_index,
129        });
130        let signed = Transaction::builder().call_message(call_msg).client(self).build()?;
131        self.send_transaction(&signed).await
132    }
133
134    /// Cancel specific orders on a market. Signs and submits the transaction.
135    ///
136    /// Cancel by exchange-assigned `OrderId`, client-assigned `ClientOrderId`, or both.
137    ///
138    /// # Example
139    ///
140    /// ```ignore
141    /// use bullet_rust_sdk::*;
142    ///
143    /// let resp = client.cancel_orders(
144    ///     MarketId(0),
145    ///     vec![CancelOrderArgs {
146    ///         order_id: Some(OrderId(12345)),
147    ///         client_order_id: None,
148    ///     }],
149    ///     None,
150    /// ).await?;
151    /// ```
152    pub async fn cancel_orders(
153        &self,
154        market_id: MarketId,
155        orders: Vec<CancelOrderArgs>,
156        sub_account_index: Option<u8>,
157    ) -> SDKResult<SubmitTxResponse> {
158        let call_msg =
159            CallMessage::User(UserAction::CancelOrders { market_id, orders, sub_account_index });
160        let signed = Transaction::builder().call_message(call_msg).client(self).build()?;
161        self.send_transaction(&signed).await
162    }
163
164    /// Cancel all orders on a specific market. Signs and submits the transaction.
165    ///
166    /// # Example
167    ///
168    /// ```ignore
169    /// let resp = client.cancel_market_orders(MarketId(0), None).await?;
170    /// ```
171    pub async fn cancel_market_orders(
172        &self,
173        market_id: MarketId,
174        sub_account_index: Option<u8>,
175    ) -> SDKResult<SubmitTxResponse> {
176        let call_msg =
177            CallMessage::User(UserAction::CancelMarketOrders { market_id, sub_account_index });
178        let signed = Transaction::builder().call_message(call_msg).client(self).build()?;
179        self.send_transaction(&signed).await
180    }
181
182    /// Cancel all orders across all markets. Signs and submits the transaction.
183    ///
184    /// # Example
185    ///
186    /// ```ignore
187    /// let resp = client.cancel_all_orders(None).await?;
188    /// ```
189    pub async fn cancel_all_orders(
190        &self,
191        sub_account_index: Option<u8>,
192    ) -> SDKResult<SubmitTxResponse> {
193        let call_msg = CallMessage::User(UserAction::CancelAllOrders { sub_account_index });
194        let signed = Transaction::builder().call_message(call_msg).client(self).build()?;
195        self.send_transaction(&signed).await
196    }
197
198    // ── Account query convenience methods ─────────────────────────────────
199    //
200    // These derive the account address from the client's keypair so you
201    // don't have to format it manually on every call.
202
203    /// Get the base58 address derived from the client's keypair.
204    ///
205    /// Returns `Err(SDKError::MissingKeypair)` if no keypair is configured.
206    ///
207    /// # Example
208    ///
209    /// ```ignore
210    /// let address = client.address()?;
211    /// println!("My address: {address}"); // e.g. "5Hq3...xyz"
212    /// ```
213    pub fn address(&self) -> SDKResult<String> {
214        let kp = self.keypair().ok_or(SDKError::MissingKeypair)?;
215        Ok(kp.address())
216    }
217
218    /// Query open orders for the client's own account on a symbol.
219    ///
220    /// Convenience wrapper around `query_open_orders` that derives the
221    /// address from the client's keypair.
222    ///
223    /// # Example
224    ///
225    /// ```ignore
226    /// let orders = client.my_open_orders("BTC-USD").await?;
227    /// for o in &orders {
228    ///     println!("{}: {} {} @ {}", o.order_id, o.side, o.orig_qty, o.price);
229    /// }
230    /// ```
231    pub async fn my_open_orders(
232        &self,
233        symbol: &str,
234    ) -> SDKResult<Vec<crate::generated::types::BinanceOrder>> {
235        let address = self.address()?;
236        let resp = self.query_open_orders(&address, symbol).await?;
237        Ok(resp.into_inner())
238    }
239
240    /// Query account info (positions, margins) for the client's own account.
241    ///
242    /// Convenience wrapper around `account_info` that derives the address
243    /// from the client's keypair and unwraps the response.
244    pub async fn my_account(&self) -> SDKResult<crate::generated::types::Account> {
245        let address = self.address()?;
246        let resp = self.account_info(&address).await?;
247        Ok(resp.into_inner())
248    }
249
250    /// Query balances for the client's own account.
251    ///
252    /// Convenience wrapper around `account_balance` that derives the address
253    /// from the client's keypair and unwraps the response.
254    pub async fn my_balances(&self) -> SDKResult<Vec<crate::generated::types::Balance>> {
255        let address = self.address()?;
256        let resp = self.account_balance(&address).await?;
257        Ok(resp.into_inner())
258    }
259
260    // ── Order management ────────────────────────────────────────────────
261
262    /// Amend (cancel + replace) existing orders. Signs and submits the transaction.
263    ///
264    /// Each [`AmendOrderArgs`] pairs a [`CancelOrderArgs`] with a [`NewOrderArgs`],
265    /// atomically replacing the cancelled order with a new one.
266    ///
267    /// # Example
268    ///
269    /// ```ignore
270    /// use bullet_rust_sdk::*;
271    ///
272    /// let resp = client.amend_orders(
273    ///     market_id,
274    ///     vec![AmendOrderArgs {
275    ///         cancel: CancelOrderArgs {
276    ///             order_id: Some(OrderId(12345)),
277    ///             client_order_id: None,
278    ///         },
279    ///         place: NewOrderArgs::limit(new_price, new_size, Side::Bid),
280    ///     }],
281    ///     None,
282    /// ).await?;
283    /// ```
284    pub async fn amend_orders(
285        &self,
286        market_id: MarketId,
287        orders: Vec<AmendOrderArgs>,
288        sub_account_index: Option<u8>,
289    ) -> SDKResult<SubmitTxResponse> {
290        let call_msg =
291            CallMessage::User(UserAction::AmendOrders { market_id, orders, sub_account_index });
292        let signed = Transaction::builder().call_message(call_msg).client(self).build()?;
293        self.send_transaction(&signed).await
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use std::str::FromStr;
300
301    use rust_decimal::Decimal;
302
303    use super::*;
304
305    fn dec(s: &str) -> PositiveDecimal {
306        PositiveDecimal::try_from(Decimal::from_str(s).unwrap()).unwrap()
307    }
308
309    #[test]
310    fn limit_order_defaults() {
311        let order = NewOrderArgs::limit(dec("50000"), dec("0.1"), Side::Bid);
312        assert_eq!(order.order_type, OrderType::Limit);
313        assert_eq!(order.side, Side::Bid);
314        assert!(!order.reduce_only);
315        assert!(order.client_order_id.is_none());
316        assert!(order.pending_tpsl_pair.is_none());
317    }
318
319    #[test]
320    fn post_only_order_defaults() {
321        let order = NewOrderArgs::post_only(dec("50000"), dec("0.1"), Side::Ask);
322        assert_eq!(order.order_type, OrderType::PostOnly);
323        assert_eq!(order.side, Side::Ask);
324        assert!(!order.reduce_only);
325        assert!(order.client_order_id.is_none());
326    }
327
328    #[test]
329    fn ioc_order_defaults() {
330        let order = NewOrderArgs::ioc(dec("50000"), dec("0.1"), Side::Bid);
331        assert_eq!(order.order_type, OrderType::ImmediateOrCancel);
332        assert!(!order.reduce_only);
333        assert!(order.client_order_id.is_none());
334    }
335}