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}