Skip to main content

rustrade_execution/order/
state.rs

1use crate::{error::OrderError, order::id::OrderId};
2use chrono::{DateTime, Utc};
3use derive_more::{Constructor, From};
4use rust_decimal::Decimal;
5use rustrade_instrument::{
6    asset::{AssetIndex, name::AssetNameExchange},
7    instrument::{InstrumentIndex, name::InstrumentNameExchange},
8};
9use serde::{Deserialize, Serialize};
10
11/// Convenient type alias for an [`OrderState`] keyed with [`AssetNameExchange`]
12/// and [`InstrumentNameExchange`].
13pub type UnindexedOrderState = OrderState<AssetNameExchange, InstrumentNameExchange>;
14
15#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
16pub enum OrderState<AssetKey = AssetIndex, InstrumentKey = InstrumentIndex> {
17    Active(ActiveOrderState),
18    Inactive(InactiveOrderState<AssetKey, InstrumentKey>),
19}
20
21impl<AssetKey, InstrumentKey> OrderState<AssetKey, InstrumentKey> {
22    pub fn active<S>(state: S) -> Self
23    where
24        S: Into<ActiveOrderState>,
25    {
26        OrderState::Active(state.into())
27    }
28
29    pub fn inactive<S>(state: S) -> Self
30    where
31        S: Into<InactiveOrderState<AssetKey, InstrumentKey>>,
32    {
33        OrderState::Inactive(state.into())
34    }
35
36    pub fn fully_filled(filled: Filled) -> Self {
37        Self::Inactive(InactiveOrderState::FullyFilled(filled))
38    }
39
40    pub fn expired(expired: Expired) -> Self {
41        Self::Inactive(InactiveOrderState::Expired(expired))
42    }
43
44    pub fn time_exchange(&self) -> Option<DateTime<Utc>> {
45        match self {
46            Self::Active(active) => match active {
47                ActiveOrderState::OpenInFlight(_) => None,
48                ActiveOrderState::Open(state) => Some(state.time_exchange),
49                ActiveOrderState::CancelInFlight(state) => {
50                    state.order.as_ref().map(|order| order.time_exchange)
51                }
52            },
53            Self::Inactive(inactive) => match inactive {
54                InactiveOrderState::Cancelled(state) => Some(state.time_exchange),
55                InactiveOrderState::FullyFilled(state) => Some(state.time_exchange),
56                InactiveOrderState::Expired(state) => Some(state.time_exchange),
57                InactiveOrderState::OpenFailed(_) => None,
58            },
59        }
60    }
61
62    /// Returns `true` if the order was not rejected at placement.
63    ///
64    /// Returns `true` for all states except `Inactive(OpenFailed(_))`:
65    /// - `Active(_)` — order is working on the exchange
66    /// - `Inactive(FullyFilled(_))` — order completed successfully
67    /// - `Inactive(Cancelled(_))` — order was accepted then cancelled
68    /// - `Inactive(Expired(_))` — order was accepted then expired
69    ///
70    /// This is the opposite of [`is_failed()`](Self::is_failed).
71    pub fn is_accepted(&self) -> bool {
72        !self.is_failed()
73    }
74
75    /// Returns `true` if the order failed to open.
76    pub fn is_failed(&self) -> bool {
77        matches!(self, Self::Inactive(InactiveOrderState::OpenFailed(_)))
78    }
79}
80
81#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
82pub enum ActiveOrderState {
83    OpenInFlight(OpenInFlight),
84    Open(Open),
85    CancelInFlight(CancelInFlight),
86}
87
88impl ActiveOrderState {
89    pub fn open_meta(&self) -> Option<&Open> {
90        match self {
91            Self::OpenInFlight(_) => None,
92            Self::Open(open) => Some(open),
93            Self::CancelInFlight(cancel) => cancel.order.as_ref(),
94        }
95    }
96}
97
98#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
99pub struct OpenInFlight;
100
101/// An order the exchange reports as working, in the state the exchange last reported it.
102///
103/// Successive `Open` values for one order describe that order over time -- an acknowledgement,
104/// then each partial fill -- so consumers order them by [`Open::time_exchange`] and discard one
105/// older than the state they already hold. Producers must stamp it accordingly; see that field.
106#[derive(
107    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
108)]
109pub struct Open {
110    pub id: OrderId,
111    /// When the exchange last reported this state -- **not** when the order was created.
112    ///
113    /// ## Producer obligation
114    ///
115    /// A client building this from a venue response must use the venue's last-update field
116    /// (Binance `updateTime`, Alpaca `updated_at`) rather than its creation field. Creation time
117    /// is identical across every snapshot of one order, so a snapshot stamped with it cannot be
118    /// ordered against the states that followed it and is discarded as stale by any consumer
119    /// applying the rule above -- silently, and precisely for the partially-filled orders a
120    /// reconciliation fetch exists to repair.
121    pub time_exchange: DateTime<Utc>,
122    /// Cumulative quantity filled across every execution against this order, as the exchange
123    /// reports it -- not the size of the most recent execution.
124    pub filled_quantity: Decimal,
125}
126
127impl Open {
128    pub fn quantity_remaining(&self, initial_quantity: Decimal) -> Decimal {
129        initial_quantity - self.filled_quantity
130    }
131}
132
133/// Metadata for a fully filled order.
134///
135/// Unlike [`Open`], this represents an order that has completed execution
136/// and is no longer active on the exchange.
137#[derive(
138    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
139)]
140pub struct Filled {
141    pub id: OrderId,
142    pub time_exchange: DateTime<Utc>,
143    pub filled_quantity: Decimal,
144    /// Volume-weighted average execution price across all fills.
145    ///
146    /// `Some` when the exchange provides it in the response, `None` otherwise.
147    /// When `None`, downstream consumers should compute from individual fill events.
148    pub avg_price: Option<Decimal>,
149}
150
151#[derive(
152    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize, Constructor,
153)]
154pub struct CancelInFlight {
155    pub order: Option<Open>,
156}
157
158#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
159pub enum InactiveOrderState<AssetKey, InstrumentKey> {
160    Cancelled(Cancelled),
161    FullyFilled(Filled),
162    OpenFailed(OrderError<AssetKey, InstrumentKey>),
163    Expired(Expired),
164}
165
166/// Metadata for a cancelled order.
167///
168/// Includes `filled_quantity` to handle IOC (Immediate-Or-Cancel) orders
169/// that partially fill before the remainder is cancelled.
170#[derive(
171    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
172)]
173pub struct Cancelled {
174    pub id: OrderId,
175    /// Cancellation timestamp.
176    ///
177    /// Normally the venue-reported cancel time. Some venues omit a timestamp on their cancel
178    /// response (e.g. Binance margin cancels carry no `transactTime`); for those the client falls
179    /// back to the local receive time, which can differ from the true venue cancel time by network
180    /// latency. Consumers building fill ledgers or P&L should not assume sub-second venue accuracy.
181    pub time_exchange: DateTime<Utc>,
182    /// Quantity filled before the order was cancelled.
183    ///
184    /// Zero for orders cancelled with no fills (e.g., GTC limit order cancelled by user).
185    /// Non-zero for IOC orders that partially filled before cancellation.
186    pub filled_quantity: Decimal,
187}
188
189/// Metadata for an expired order.
190///
191/// Includes `filled_quantity` to handle GTD (Good-Till-Date) orders
192/// that partially fill before expiration.
193#[derive(
194    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
195)]
196pub struct Expired {
197    pub id: OrderId,
198    pub time_exchange: DateTime<Utc>,
199    /// Quantity filled before the order expired.
200    pub filled_quantity: Decimal,
201}