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