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.