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)]
181pub enum OrderKind {
182    Market,
183    Limit,
184    /// Stop (market) order - triggers a market order when trigger_price is reached.
185    #[display("Stop({trigger_price})")]
186    Stop {
187        trigger_price: Decimal,
188    },
189    /// Stop-limit order - triggers a limit order at Order.price when trigger_price is reached.
190    #[display("StopLimit({trigger_price})")]
191    StopLimit {
192        trigger_price: Decimal,
193    },
194    /// Trailing stop order - stop price trails the market by a specified offset.
195    #[display("TrailingStop({offset}, {offset_type})")]
196    TrailingStop {
197        offset: Decimal,
198        offset_type: TrailingOffsetType,
199    },
200    /// Trailing stop-limit order - when triggered, submits a limit order offset from the stop.
201    #[display("TrailingStopLimit({offset}, {offset_type}, {limit_offset})")]
202    TrailingStopLimit {
203        offset: Decimal,
204        offset_type: TrailingOffsetType,
205        /// Offset from the triggered stop price to set the limit price.
206        limit_offset: Decimal,
207    },
208}
209
210#[derive(
211    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
212)]
213pub enum TimeInForce {
214    GoodUntilCancelled {
215        post_only: bool,
216    },
217    GoodUntilEndOfDay,
218    FillOrKill,
219    ImmediateOrCancel,
220    /// Good until a specific date/time. Order expires if not filled by the specified time.
221    #[display("GoodTillDate({expiry})")]
222    GoodTillDate {
223        expiry: DateTime<Utc>,
224    },
225    /// Execute at market open (OPG). Valid for various order types.
226    AtOpen,
227    /// Execute at market close. Only valid with Market (→ MOC) or Limit (→ LOC) orders.
228    AtClose,
229}
230
231impl<ExchangeKey, InstrumentKey> From<&OrderRequestOpen<ExchangeKey, InstrumentKey>>
232    for Order<ExchangeKey, InstrumentKey, ActiveOrderState>
233where
234    ExchangeKey: Clone,
235    InstrumentKey: Clone,
236{
237    fn from(value: &OrderRequestOpen<ExchangeKey, InstrumentKey>) -> Self {
238        let OrderRequestOpen {
239            key,
240            state:
241                RequestOpen {
242                    side,
243                    price,
244                    quantity,
245                    kind,
246                    time_in_force,
247                    position_id: _,
248                    reduce_only: _, // used by adapters (e.g., Alpaca) to derive position_intent
249                },
250        } = value;
251
252        Self {
253            key: key.clone(),
254            side: *side,
255            price: *price,
256            quantity: *quantity,
257            kind: *kind,
258            time_in_force: *time_in_force,
259            state: ActiveOrderState::OpenInFlight(OpenInFlight),
260        }
261    }
262}
263
264impl<ExchangeKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
265    for Order<ExchangeKey, InstrumentKey, ActiveOrderState>
266{
267    fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
268        let Order {
269            key,
270            side,
271            price,
272            quantity,
273            kind,
274            time_in_force,
275            state,
276        } = value;
277
278        Self {
279            key,
280            side,
281            price,
282            quantity,
283            kind,
284            time_in_force,
285            state: ActiveOrderState::Open(state),
286        }
287    }
288}
289
290impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
291    for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
292{
293    fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
294        let Order {
295            key,
296            side,
297            price,
298            quantity,
299            kind,
300            time_in_force,
301            state,
302        } = value;
303
304        Self {
305            key,
306            side,
307            price,
308            quantity,
309            kind,
310            time_in_force,
311            state: OrderState::Active(ActiveOrderState::Open(state)),
312        }
313    }
314}
315
316impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Cancelled>>
317    for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
318{
319    fn from(value: Order<ExchangeKey, InstrumentKey, Cancelled>) -> Self {
320        let Order {
321            key,
322            side,
323            price,
324            quantity,
325            kind,
326            time_in_force,
327            state,
328        } = value;
329
330        Self {
331            key,
332            side,
333            price,
334            quantity,
335            kind,
336            time_in_force,
337            state: OrderState::Inactive(InactiveOrderState::Cancelled(state)),
338        }
339    }
340}