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#[derive(
102    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
103)]
104pub struct Open {
105    pub id: OrderId,
106    pub time_exchange: DateTime<Utc>,
107    pub filled_quantity: Decimal,
108}
109
110impl Open {
111    pub fn quantity_remaining(&self, initial_quantity: Decimal) -> Decimal {
112        initial_quantity - self.filled_quantity
113    }
114}
115
116/// Metadata for a fully filled order.
117///
118/// Unlike [`Open`], this represents an order that has completed execution
119/// and is no longer active on the exchange.
120#[derive(
121    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
122)]
123pub struct Filled {
124    pub id: OrderId,
125    pub time_exchange: DateTime<Utc>,
126    pub filled_quantity: Decimal,
127    /// Volume-weighted average execution price across all fills.
128    ///
129    /// `Some` when the exchange provides it in the response, `None` otherwise.
130    /// When `None`, downstream consumers should compute from individual fill events.
131    pub avg_price: Option<Decimal>,
132}
133
134#[derive(
135    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize, Constructor,
136)]
137pub struct CancelInFlight {
138    pub order: Option<Open>,
139}
140
141#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
142pub enum InactiveOrderState<AssetKey, InstrumentKey> {
143    Cancelled(Cancelled),
144    FullyFilled(Filled),
145    OpenFailed(OrderError<AssetKey, InstrumentKey>),
146    Expired(Expired),
147}
148
149/// Metadata for a cancelled order.
150///
151/// Includes `filled_quantity` to handle IOC (Immediate-Or-Cancel) orders
152/// that partially fill before the remainder is cancelled.
153#[derive(
154    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
155)]
156pub struct Cancelled {
157    pub id: OrderId,
158    pub time_exchange: DateTime<Utc>,
159    /// Quantity filled before the order was cancelled.
160    ///
161    /// Zero for orders cancelled with no fills (e.g., GTC limit order cancelled by user).
162    /// Non-zero for IOC orders that partially filled before cancellation.
163    pub filled_quantity: Decimal,
164}
165
166/// Metadata for an expired order.
167///
168/// Includes `filled_quantity` to handle GTD (Good-Till-Date) orders
169/// that partially fill before expiration.
170#[derive(
171    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
172)]
173pub struct Expired {
174    pub id: OrderId,
175    pub time_exchange: DateTime<Utc>,
176    /// Quantity filled before the order expired.
177    pub filled_quantity: Decimal,
178}