Skip to main content

px_core/models/
order.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::str::FromStr;
5
6/// Order time-in-force / execution type.
7///
8/// Normalized across all exchanges:
9/// - `Gtc` (good-til-cancelled) — rests on the book until filled or cancelled.
10/// - `Ioc` (immediate-or-cancel) — fills what it can immediately, cancels the rest.
11/// - `Fok` (fill-or-kill) — must fill entirely in one shot or is cancelled.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[serde(rename_all = "lowercase")]
15pub enum OrderType {
16    Gtc,
17    Ioc,
18    Fok,
19}
20
21impl FromStr for OrderType {
22    type Err = String;
23
24    fn from_str(s: &str) -> Result<Self, Self::Err> {
25        match s.trim().to_ascii_lowercase().as_str() {
26            "gtc" => Ok(Self::Gtc),
27            "ioc" => Ok(Self::Ioc),
28            "fok" => Ok(Self::Fok),
29            other => Err(format!(
30                "invalid order_type '{other}' (allowed: gtc, ioc, fok)"
31            )),
32        }
33    }
34}
35
36impl fmt::Display for OrderType {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            Self::Gtc => f.write_str("gtc"),
40            Self::Ioc => f.write_str("ioc"),
41            Self::Fok => f.write_str("fok"),
42        }
43    }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[serde(rename_all = "lowercase")]
49pub enum OrderSide {
50    Buy,
51    Sell,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
56#[serde(rename_all = "lowercase")]
57pub enum LiquidityRole {
58    Maker,
59    Taker,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[serde(rename_all = "snake_case")]
65pub enum OrderStatus {
66    Pending,
67    Open,
68    Filled,
69    PartiallyFilled,
70    Cancelled,
71    Rejected,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
76// TODO(order-fees): Add fee fields (e.g. `fee: Option<f64>`, `fee_rate_bps: Option<u32>`).
77// Kalshi returns fees in create_order and fill responses — capture them here.
78// Polymarket fees are protocol-level and can be computed from trade data.
79// OpenPX does not charge fees; only the underlying exchange does.
80pub struct Order {
81    pub id: String,
82    pub market_id: String,
83    pub outcome: String,
84    pub side: OrderSide,
85    pub price: f64,
86    pub size: f64,
87    pub filled: f64,
88    pub status: OrderStatus,
89    pub created_at: DateTime<Utc>,
90    #[serde(default)]
91    pub updated_at: Option<DateTime<Utc>>,
92}
93
94impl Order {
95    pub fn remaining(&self) -> f64 {
96        self.size - self.filled
97    }
98
99    pub fn is_active(&self) -> bool {
100        matches!(
101            self.status,
102            OrderStatus::Open | OrderStatus::PartiallyFilled
103        )
104    }
105
106    pub fn is_filled(&self) -> bool {
107        self.status == OrderStatus::Filled || self.filled >= self.size
108    }
109
110    pub fn fill_percentage(&self) -> f64 {
111        if self.size == 0.0 {
112            return 0.0;
113        }
114        self.filled / self.size
115    }
116}
117
118/// A single fill (trade execution) from a user's order.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
121pub struct Fill {
122    pub fill_id: String,
123    pub order_id: String,
124    pub market_id: String,
125    pub outcome: String,
126    pub side: OrderSide,
127    pub price: f64,
128    pub size: f64,
129    pub is_taker: bool,
130    pub fee: f64,
131    pub created_at: DateTime<Utc>,
132}
133
134// TODO(fill-sim): Add local fill simulation for backtesting strategies offline.
135// polyfill-rs has FillEngine that simulates order execution against a local orderbook copy:
136//   - execute_market_order(order, book) → FillResult with fills, fees, slippage check
137//   - execute_limit_order(order, book) → checks immediate fillability
138//   - Configurable: min_fill_size, max_slippage_pct, fee_rate_bps
139//   - Tracks fill history with get_fills(order_id), get_stats()
140// Pro Traders (user type B) would use this for backtesting without hitting live APIs.
141// Could be implemented as a standalone utility crate or SDK-side helper.
142//
143// See also: TODO(historical-orderbook) for the data
144// ingestion side. NautilusTrader (nautechsystems/nautilus_trader) takes a similar approach:
145// L2 snapshots replayed as CLEAR+ADD delta sequences into a simulated matching engine.
146// Key caveat: without real trade tape, fill simulation is approximate — no queue priority
147// or true latency modeling. Good for strategy development, not precise PnL attribution.