Skip to main content

bot_core/
commands.rs

1//! Strategy commands sent to the engine.
2//!
3//! Commands are the write-side API for strategies. A strategy never calls an
4//! exchange directly; it emits commands through [`StrategyContext`], and the
5//! engine turns those commands into exchange writes and later execution events.
6//!
7//! # Example
8//!
9//! ```
10//! use bot_core::{
11//!     Command, Environment, ExchangeId, ExchangeInstance, InstrumentId, OrderSide, PlaceOrder,
12//!     Price, Qty,
13//! };
14//! use rust_decimal::Decimal;
15//!
16//! let exchange = ExchangeInstance::new(ExchangeId::new("hyperliquid"), Environment::Testnet);
17//! let order = PlaceOrder::limit(
18//!     exchange,
19//!     InstrumentId::new("BTC-PERP"),
20//!     OrderSide::Buy,
21//!     Price::new(Decimal::new(65_000, 0)),
22//!     Qty::new(Decimal::new(1, 3)),
23//! );
24//!
25//! let command = Command::PlaceOrder(order);
26//! assert_eq!(command.instrument().unwrap().as_str(), "BTC-PERP");
27//! ```
28//!
29//! [`StrategyContext`]: crate::StrategyContext
30
31use crate::types::*;
32use serde::{Deserialize, Serialize};
33
34/// Commands that strategies can emit.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub enum Command {
37    /// Submit one limit order.
38    PlaceOrder(PlaceOrder),
39    /// Submit several limit orders as one logical batch.
40    PlaceOrders(Vec<PlaceOrder>),
41    /// Cancel one tracked order by client order ID.
42    CancelOrder(CancelOrder),
43    /// Cancel all tracked orders, optionally scoped to one instrument.
44    CancelAll(CancelAll),
45    /// Request strategy stop with a reason
46    StopStrategy(StopStrategy),
47}
48
49impl Command {
50    /// Get the client order ID if applicable (returns first one for batch)
51    pub fn client_id(&self) -> Option<&ClientOrderId> {
52        match self {
53            Command::PlaceOrder(c) => Some(&c.client_id),
54            Command::PlaceOrders(orders) => orders.first().map(|o| &o.client_id),
55            Command::CancelOrder(c) => Some(&c.client_id),
56            Command::CancelAll(_) => None,
57            Command::StopStrategy(_) => None,
58        }
59    }
60
61    /// Get the instrument if applicable (returns first one for batch)
62    pub fn instrument(&self) -> Option<&InstrumentId> {
63        match self {
64            Command::PlaceOrder(c) => Some(&c.instrument),
65            Command::PlaceOrders(orders) => orders.first().map(|o| &o.instrument),
66            Command::CancelOrder(_) => None,
67            Command::CancelAll(c) => c.instrument.as_ref(),
68            Command::StopStrategy(_) => None,
69        }
70    }
71}
72
73/// Place a new order
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct PlaceOrder {
76    /// Client-generated unique order ID
77    pub client_id: ClientOrderId,
78    /// Target exchange instance
79    pub exchange: ExchangeInstance,
80    /// Instrument to trade
81    pub instrument: InstrumentId,
82    /// Buy or Sell
83    pub side: OrderSide,
84    /// Limit price
85    pub price: Price,
86    /// Quantity
87    pub qty: Qty,
88    /// Time in force (GTC, IOC, FOK)
89    pub tif: TimeInForce,
90    /// Post-only (maker only)
91    pub post_only: bool,
92    /// Reduce-only (close position only)
93    pub reduce_only: bool,
94}
95
96impl PlaceOrder {
97    /// Create a new PlaceOrder command with defaults
98    pub fn limit(
99        exchange: ExchangeInstance,
100        instrument: InstrumentId,
101        side: OrderSide,
102        price: Price,
103        qty: Qty,
104    ) -> Self {
105        Self {
106            client_id: ClientOrderId::generate(),
107            exchange,
108            instrument,
109            side,
110            price,
111            qty,
112            tif: TimeInForce::Gtc,
113            post_only: false,
114            reduce_only: false,
115        }
116    }
117
118    /// Set time in force
119    pub fn with_tif(mut self, tif: TimeInForce) -> Self {
120        self.tif = tif;
121        self
122    }
123
124    /// Set post-only
125    pub fn post_only(mut self) -> Self {
126        self.post_only = true;
127        self
128    }
129
130    /// Set reduce-only
131    pub fn reduce_only(mut self) -> Self {
132        self.reduce_only = true;
133        self
134    }
135
136    /// Set a specific client order ID
137    pub fn with_client_id(mut self, client_id: ClientOrderId) -> Self {
138        self.client_id = client_id;
139        self
140    }
141}
142
143/// Cancel an existing order
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct CancelOrder {
146    /// Target exchange instance
147    pub exchange: ExchangeInstance,
148    /// Client order ID to cancel
149    pub client_id: ClientOrderId,
150}
151
152impl CancelOrder {
153    /// Create a command that cancels one order by client order ID.
154    ///
155    /// # Example
156    ///
157    /// ```
158    /// use bot_core::{CancelOrder, ClientOrderId, Environment, ExchangeId, ExchangeInstance};
159    ///
160    /// let exchange = ExchangeInstance::new(ExchangeId::new("hyperliquid"), Environment::Mainnet);
161    /// let cancel = CancelOrder::new(exchange, ClientOrderId::new("client-1"));
162    ///
163    /// assert_eq!(cancel.client_id.as_str(), "client-1");
164    /// ```
165    pub fn new(exchange: ExchangeInstance, client_id: ClientOrderId) -> Self {
166        Self {
167            exchange,
168            client_id,
169        }
170    }
171}
172
173/// Cancel all orders (optionally for a specific instrument)
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct CancelAll {
176    /// Target exchange instance
177    pub exchange: ExchangeInstance,
178    /// Optionally limit to a specific instrument
179    pub instrument: Option<InstrumentId>,
180}
181
182impl CancelAll {
183    /// Create a command that cancels every open order on an exchange instance.
184    ///
185    /// Use [`CancelAll::for_instrument`] when the cancellation should be
186    /// limited to one instrument.
187    pub fn new(exchange: ExchangeInstance) -> Self {
188        Self {
189            exchange,
190            instrument: None,
191        }
192    }
193
194    /// Create a command that cancels all open orders for one instrument.
195    ///
196    /// # Example
197    ///
198    /// ```
199    /// use bot_core::{CancelAll, Environment, ExchangeId, ExchangeInstance, InstrumentId};
200    ///
201    /// let exchange = ExchangeInstance::new(ExchangeId::new("hyperliquid"), Environment::Testnet);
202    /// let cancel = CancelAll::for_instrument(exchange, InstrumentId::new("ETH-PERP"));
203    ///
204    /// assert_eq!(cancel.instrument.unwrap().as_str(), "ETH-PERP");
205    /// ```
206    pub fn for_instrument(exchange: ExchangeInstance, instrument: InstrumentId) -> Self {
207        Self {
208            exchange,
209            instrument: Some(instrument),
210        }
211    }
212}
213
214/// Stop strategy command - requests the engine to stop a strategy
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct StopStrategy {
217    /// The strategy requesting to stop
218    pub strategy_id: StrategyId,
219    /// Reason for stopping
220    pub reason: String,
221}
222
223impl StopStrategy {
224    /// Create a command requesting strategy shutdown.
225    ///
226    /// The engine decides when the stop callback runs; this command only records
227    /// the strategy and reason.
228    pub fn new(strategy_id: StrategyId, reason: impl Into<String>) -> Self {
229        Self {
230            strategy_id,
231            reason: reason.into(),
232        }
233    }
234}