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