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}