Skip to main content

rustrade_execution/order/
mod.rs

1use crate::order::{
2    id::StrategyId,
3    request::{OrderRequestCancel, OrderRequestOpen, RequestCancel, RequestOpen},
4    state::UnindexedOrderState,
5};
6use chrono::{DateTime, Utc};
7use derive_more::{Constructor, Display};
8use id::ClientOrderId;
9use rust_decimal::Decimal;
10use rustrade_instrument::{
11    Side,
12    asset::{AssetIndex, name::AssetNameExchange},
13    exchange::{ExchangeId, ExchangeIndex},
14    instrument::{InstrumentIndex, name::InstrumentNameExchange},
15};
16use serde::{Deserialize, Serialize};
17use state::{ActiveOrderState, Cancelled, InactiveOrderState, Open, OpenInFlight, OrderState};
18
19/// `Order` related identifiers.
20pub mod id;
21
22/// `Order` states.
23///
24/// eg/ `OpenInFlight`, `Open`, `Rejected`, `Expired`, etc.
25pub mod state;
26
27/// Order open and cancel request types.
28///
29/// ie/ `OrderRequestOpen` & `OrderRequestCancel`.
30pub mod request;
31
32/// Bracket order types for the [`BracketOrderClient`](crate::client::BracketOrderClient) trait.
33pub mod bracket;
34
35/// Convenient type alias for an [`Order`] keyed with [`ExchangeId`] and [`InstrumentNameExchange`].
36pub type UnindexedOrder = Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>;
37
38/// Convenient type alias for an [`OrderKey`] keyed with [`ExchangeId`]
39/// and [`InstrumentNameExchange`].
40pub type UnindexedOrderKey = OrderKey<ExchangeId, InstrumentNameExchange>;
41
42/// Convenient type alias for an [`OrderSnapshot`] keyed with [`ExchangeId`], [`AssetNameExchange`],
43/// and [`InstrumentNameExchange`].
44pub type UnindexedOrderSnapshot = Order<
45    ExchangeId,
46    InstrumentNameExchange,
47    OrderState<AssetNameExchange, InstrumentNameExchange>,
48>;
49
50/// Convenient type alias for an [`Order`] [`OrderState`] snapshot.
51pub type OrderSnapshot<
52    ExchangeKey = ExchangeIndex,
53    AssetKey = AssetIndex,
54    InstrumentKey = InstrumentIndex,
55> = Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>;
56
57#[derive(
58    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
59)]
60
61pub struct OrderEvent<State, ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> {
62    pub key: OrderKey<ExchangeKey, InstrumentKey>,
63    pub state: State,
64}
65
66#[derive(
67    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
68)]
69pub struct OrderKey<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> {
70    pub exchange: ExchangeKey,
71    pub instrument: InstrumentKey,
72    pub strategy: StrategyId,
73    pub cid: ClientOrderId,
74}
75
76#[derive(
77    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
78)]
79pub struct Order<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex, State = OrderState> {
80    pub key: OrderKey<ExchangeKey, InstrumentKey>,
81    pub side: Side,
82    /// Limit price for the order. `None` for Market/Stop/TrailingStop orders.
83    pub price: Option<Decimal>,
84    pub quantity: Decimal,
85    pub kind: OrderKind,
86    pub time_in_force: TimeInForce,
87    pub state: State,
88}
89
90impl<ExchangeKey, AssetKey, InstrumentKey>
91    Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
92{
93    pub fn to_active(&self) -> Option<Order<ExchangeKey, InstrumentKey, ActiveOrderState>>
94    where
95        ExchangeKey: Clone,
96        InstrumentKey: Clone,
97    {
98        let OrderState::Active(state) = &self.state else {
99            return None;
100        };
101
102        Some(Order {
103            key: self.key.clone(),
104            side: self.side,
105            price: self.price,
106            quantity: self.quantity,
107            kind: self.kind,
108            time_in_force: self.time_in_force,
109            state: state.clone(),
110        })
111    }
112
113    pub fn to_inactive(
114        &self,
115    ) -> Option<Order<ExchangeKey, InstrumentKey, InactiveOrderState<AssetKey, InstrumentKey>>>
116    where
117        ExchangeKey: Clone,
118        AssetKey: Clone,
119        InstrumentKey: Clone,
120    {
121        let OrderState::Inactive(state) = &self.state else {
122            return None;
123        };
124
125        Some(Order {
126            key: self.key.clone(),
127            side: self.side,
128            price: self.price,
129            quantity: self.quantity,
130            kind: self.kind,
131            time_in_force: self.time_in_force,
132            state: state.clone(),
133        })
134    }
135}
136
137impl<ExchangeKey, InstrumentKey> Order<ExchangeKey, InstrumentKey, ActiveOrderState>
138where
139    ExchangeKey: Clone,
140    InstrumentKey: Clone,
141{
142    pub fn to_request_cancel(&self) -> Option<OrderRequestCancel<ExchangeKey, InstrumentKey>> {
143        let Order { key, state, .. } = self;
144
145        let request_cancel = match state {
146            ActiveOrderState::OpenInFlight(_) => RequestCancel { id: None },
147            ActiveOrderState::Open(open) => RequestCancel {
148                id: Some(open.id.clone()),
149            },
150            _ => return None,
151        };
152
153        Some(OrderRequestCancel {
154            key: key.clone(),
155            state: request_cancel,
156        })
157    }
158}
159
160/// Specifies how the trailing offset is measured for trailing stop orders.
161///
162/// Different exchanges support different offset types:
163/// - IBKR: Absolute (dollar amount) and Percentage
164/// - Binance: BasisPoints (1/100th of 1%)
165/// - Alpaca/Coinbase: Do not support trailing stops
166#[derive(
167    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
168)]
169pub enum TrailingOffsetType {
170    /// Absolute dollar/currency amount (e.g., $2.00 trailing distance).
171    Absolute,
172    /// Percentage of the current price (e.g., 5% trailing distance).
173    Percentage,
174    /// Basis points (1/100th of 1%). Used by Binance.
175    BasisPoints,
176}
177
178#[derive(
179    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
180)]
181#[non_exhaustive]
182pub enum OrderKind {
183    Market,
184    Limit,
185    /// Stop (market) order - triggers a market order when trigger_price is reached.
186    #[display("Stop({trigger_price})")]
187    Stop {
188        trigger_price: Decimal,
189    },
190    /// Stop-limit order - triggers a limit order at Order.price when trigger_price is reached.
191    #[display("StopLimit({trigger_price})")]
192    StopLimit {
193        trigger_price: Decimal,
194    },
195    /// Take-profit (market) order - triggers a market order when trigger_price is reached.
196    /// Opposite of Stop: triggers when price moves favorably (above for longs, below for shorts).
197    #[display("TakeProfit({trigger_price})")]
198    TakeProfit {
199        trigger_price: Decimal,
200    },
201    /// Take-profit limit order - triggers a limit order at Order.price when trigger_price is reached.
202    #[display("TakeProfitLimit({trigger_price})")]
203    TakeProfitLimit {
204        trigger_price: Decimal,
205    },
206    /// Trailing stop order - stop price trails the market by a specified offset.
207    #[display("TrailingStop({offset}, {offset_type})")]
208    TrailingStop {
209        offset: Decimal,
210        offset_type: TrailingOffsetType,
211    },
212    /// Trailing stop-limit order - when triggered, submits a limit order offset from the stop.
213    #[display("TrailingStopLimit({offset}, {offset_type}, {limit_offset})")]
214    TrailingStopLimit {
215        offset: Decimal,
216        offset_type: TrailingOffsetType,
217        /// Offset from the triggered stop price to set the limit price.
218        limit_offset: Decimal,
219    },
220}
221
222#[derive(
223    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
224)]
225pub enum TimeInForce {
226    GoodUntilCancelled {
227        post_only: bool,
228    },
229    GoodUntilEndOfDay,
230    FillOrKill,
231    ImmediateOrCancel,
232    /// Good until a specific date/time. Order expires if not filled by the specified time.
233    #[display("GoodTillDate({expiry})")]
234    GoodTillDate {
235        expiry: DateTime<Utc>,
236    },
237    /// Execute at market open (OPG). Valid for various order types.
238    AtOpen,
239    /// Execute at market close. Only valid with Market (→ MOC) or Limit (→ LOC) orders.
240    AtClose,
241}
242
243impl<ExchangeKey, InstrumentKey> From<&OrderRequestOpen<ExchangeKey, InstrumentKey>>
244    for Order<ExchangeKey, InstrumentKey, ActiveOrderState>
245where
246    ExchangeKey: Clone,
247    InstrumentKey: Clone,
248{
249    fn from(value: &OrderRequestOpen<ExchangeKey, InstrumentKey>) -> Self {
250        let OrderRequestOpen {
251            key,
252            state:
253                RequestOpen {
254                    side,
255                    price,
256                    quantity,
257                    kind,
258                    time_in_force,
259                    position_id: _,
260                    reduce_only: _, // used by adapters (e.g., Alpaca) to derive position_intent
261                },
262        } = value;
263
264        Self {
265            key: key.clone(),
266            side: *side,
267            price: *price,
268            quantity: *quantity,
269            kind: *kind,
270            time_in_force: *time_in_force,
271            state: ActiveOrderState::OpenInFlight(OpenInFlight),
272        }
273    }
274}
275
276impl<ExchangeKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
277    for Order<ExchangeKey, InstrumentKey, ActiveOrderState>
278{
279    fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
280        let Order {
281            key,
282            side,
283            price,
284            quantity,
285            kind,
286            time_in_force,
287            state,
288        } = value;
289
290        Self {
291            key,
292            side,
293            price,
294            quantity,
295            kind,
296            time_in_force,
297            state: ActiveOrderState::Open(state),
298        }
299    }
300}
301
302impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
303    for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
304{
305    fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
306        let Order {
307            key,
308            side,
309            price,
310            quantity,
311            kind,
312            time_in_force,
313            state,
314        } = value;
315
316        Self {
317            key,
318            side,
319            price,
320            quantity,
321            kind,
322            time_in_force,
323            state: OrderState::Active(ActiveOrderState::Open(state)),
324        }
325    }
326}
327
328impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Cancelled>>
329    for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
330{
331    fn from(value: Order<ExchangeKey, InstrumentKey, Cancelled>) -> Self {
332        let Order {
333            key,
334            side,
335            price,
336            quantity,
337            kind,
338            time_in_force,
339            state,
340        } = value;
341
342        Self {
343            key,
344            side,
345            price,
346            quantity,
347            kind,
348            time_in_force,
349            state: OrderState::Inactive(InactiveOrderState::Cancelled(state)),
350        }
351    }
352}
353
354#[cfg(test)]
355#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
356mod tests {
357    use super::*;
358    use rust_decimal_macros::dec;
359
360    #[test]
361    fn order_kind_display_take_profit() {
362        let tp = OrderKind::TakeProfit {
363            trigger_price: dec!(100.50),
364        };
365        assert_eq!(tp.to_string(), "TakeProfit(100.50)");
366    }
367
368    #[test]
369    fn order_kind_display_take_profit_limit() {
370        let tpl = OrderKind::TakeProfitLimit {
371            trigger_price: dec!(200.25),
372        };
373        assert_eq!(tpl.to_string(), "TakeProfitLimit(200.25)");
374    }
375
376    #[test]
377    fn order_kind_serde_roundtrip_take_profit() {
378        let original = OrderKind::TakeProfit {
379            trigger_price: dec!(150.00),
380        };
381        let json = serde_json::to_string(&original).unwrap();
382        let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
383        assert_eq!(original, deserialized);
384    }
385
386    #[test]
387    fn order_kind_serde_roundtrip_take_profit_limit() {
388        let original = OrderKind::TakeProfitLimit {
389            trigger_price: dec!(250.75),
390        };
391        let json = serde_json::to_string(&original).unwrap();
392        let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
393        assert_eq!(original, deserialized);
394    }
395}