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/// Time-in-force. Options: `gtc` (rests on book), `ioc` (fill-now-or-cancel-rest), `fok` (all-or-nothing).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9#[serde(rename_all = "lowercase")]
10pub enum OrderType {
11    #[default]
12    Gtc,
13    Ioc,
14    Fok,
15}
16
17impl FromStr for OrderType {
18    type Err = String;
19
20    fn from_str(s: &str) -> Result<Self, Self::Err> {
21        match s.trim().to_ascii_lowercase().as_str() {
22            "gtc" => Ok(Self::Gtc),
23            "ioc" => Ok(Self::Ioc),
24            "fok" => Ok(Self::Fok),
25            other => Err(format!(
26                "invalid order_type '{other}' (allowed: gtc, ioc, fok)"
27            )),
28        }
29    }
30}
31
32impl fmt::Display for OrderType {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::Gtc => f.write_str("gtc"),
36            Self::Ioc => f.write_str("ioc"),
37            Self::Fok => f.write_str("fok"),
38        }
39    }
40}
41
42/// Order direction. Options: `buy`, `sell`.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[serde(rename_all = "lowercase")]
46pub enum OrderSide {
47    Buy,
48    Sell,
49}
50
51/// Outcome targeted by an order. Options: `yes`, `no`, or `label: "<name>"` for categorical Polymarket markets.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
54#[serde(rename_all = "snake_case")]
55pub enum OrderOutcome {
56    Yes,
57    No,
58    Label(String),
59}
60
61/// Whether a fill provided liquidity (`maker`) or took it (`taker`).
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[serde(rename_all = "lowercase")]
65pub enum LiquidityRole {
66    Maker,
67    Taker,
68}
69
70/// Order lifecycle state. Options: `pending`, `open`, `filled`, `partially_filled`, `cancelled`, `rejected`.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[serde(rename_all = "snake_case")]
74pub enum OrderStatus {
75    Pending,
76    Open,
77    Filled,
78    PartiallyFilled,
79    Cancelled,
80    Rejected,
81}
82
83/// Input for `create_order`.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
86pub struct CreateOrderRequest {
87    /// The orderable asset — Kalshi market ticker or Polymarket CTF token id (e.g. `"KXBTCD-25APR1517"`).
88    pub asset_id: String,
89    /// Outcome to trade. Options: `yes`, `no`, or `label: "<name>"` (Polymarket categorical markets only).
90    pub outcome: OrderOutcome,
91    /// Order direction. Options: `buy`, `sell`.
92    pub side: OrderSide,
93    /// Limit price as YES probability in `(0, 1)` (e.g. `0.62`).
94    pub price: f64,
95    /// Order size in contracts (e.g. `100.0`).
96    pub size: f64,
97    /// Time-in-force; defaults to `gtc` (options: `gtc`, `ioc`, `fok`).
98    #[serde(default)]
99    pub order_type: OrderType,
100}
101
102/// An order on the unified surface.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105pub struct Order {
106    /// Globally-unique exchange order id (e.g. `"a1b2c3d4-..."`).
107    pub id: String,
108    /// Unified market ticker the order belongs to (e.g. `"KXBTCD-25APR1517"`).
109    pub market_ticker: String,
110    /// Outcome label as published by the exchange (e.g. `"Yes"`, `"No"`, or a categorical label).
111    pub outcome: String,
112    /// Order direction. Options: `buy`, `sell`.
113    pub side: OrderSide,
114    /// Limit price as YES probability in `(0, 1)` (e.g. `0.62`).
115    pub price: f64,
116    /// Order size in contracts (e.g. `100.0`).
117    pub size: f64,
118    /// Cumulative filled size in contracts (e.g. `25.0`).
119    pub filled: f64,
120    /// Volume-weighted per-contract fee in quote dollars; `null` on Polymarket and on unfilled orders.
121    #[serde(default)]
122    pub fee: Option<f64>,
123    /// Order lifecycle state. Options: `pending`, `open`, `filled`, `partially_filled`, `cancelled`, `rejected`.
124    pub status: OrderStatus,
125    /// Order creation time (UTC) (e.g. `"2026-04-25T12:00:00Z"`).
126    pub created_at: DateTime<Utc>,
127    /// Last update time (UTC); `null` if untouched since creation.
128    #[serde(default)]
129    pub updated_at: Option<DateTime<Utc>>,
130}
131
132impl Order {
133    pub fn remaining(&self) -> f64 {
134        self.size - self.filled
135    }
136
137    pub fn is_active(&self) -> bool {
138        matches!(
139            self.status,
140            OrderStatus::Open | OrderStatus::PartiallyFilled
141        )
142    }
143
144    pub fn is_filled(&self) -> bool {
145        self.status == OrderStatus::Filled || self.filled >= self.size
146    }
147
148    pub fn fill_percentage(&self) -> f64 {
149        if self.size == 0.0 {
150            return 0.0;
151        }
152        self.filled / self.size
153    }
154}
155
156/// A single fill (trade execution) from one of the caller's orders.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159pub struct Fill {
160    /// Globally-unique fill id (e.g. `"f-9c2..."`).
161    pub fill_id: String,
162    /// Parent order id (e.g. `"a1b2c3d4-..."`).
163    pub order_id: String,
164    /// Unified market ticker the fill belongs to (e.g. `"KXBTCD-25APR1517"`).
165    pub market_ticker: String,
166    /// Outcome label as published by the exchange (e.g. `"Yes"`, `"No"`).
167    pub outcome: String,
168    /// Order direction. Options: `buy`, `sell`.
169    pub side: OrderSide,
170    /// Fill price as YES probability in `(0, 1)` (e.g. `0.62`).
171    pub price: f64,
172    /// Filled size in contracts (e.g. `25.0`).
173    pub size: f64,
174    /// `true` if the caller took liquidity, `false` if they made it.
175    pub is_taker: bool,
176    /// Fee paid in quote dollars (e.g. `0.07`).
177    pub fee: f64,
178    /// Fill execution time (UTC) (e.g. `"2026-04-25T12:00:00Z"`).
179    pub created_at: DateTime<Utc>,
180}
181
182// TODO(fill-sim): Add local fill simulation for backtesting strategies offline.
183// Sketch of a FillEngine that simulates order execution against a local
184// orderbook copy:
185//   - execute_market_order(order, book) → FillResult with fills, fees, slippage check
186//   - execute_limit_order(order, book) → checks immediate fillability
187//   - Configurable: min_fill_size, max_slippage_pct, fee_rate_bps
188//   - Tracks fill history with get_fills(order_id), get_stats()
189// Pro Traders (user type B) would use this for backtesting without hitting live APIs.
190// Could be implemented as a standalone utility crate or SDK-side helper.
191//
192// See also: TODO(historical-orderbook) for the data
193// ingestion side. NautilusTrader (nautechsystems/nautilus_trader) takes a similar approach:
194// L2 snapshots replayed as CLEAR+ADD delta sequences into a simulated matching engine.
195// Key caveat: without real trade tape, fill simulation is approximate — no queue priority
196// or true latency modeling. Good for strategy development, not precise PnL attribution.