Skip to main content

polyfill_rs/
types.rs

1//! Core types for the Polymarket client
2//!
3//! This module defines all the stable public types used throughout the client.
4//! These types are optimized for latency-sensitive trading environments.
5
6use alloy_primitives::{Address, U256};
7use chrono::{DateTime, Utc};
8use rust_decimal::prelude::ToPrimitive;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12// ============================================================================
13// FIXED-POINT OPTIMIZATION FOR HOT PATH PERFORMANCE
14// ============================================================================
15//
16// Instead of using rust_decimal::Decimal everywhere (which allocates),
17// I've used fixed-point integers for the performance-critical order book operations.
18//
19// Why this matters:
20// - Decimal operations can be 10-100x slower than integer operations
21// - Decimal allocates memory for each calculation
22// - In an order book like this we process thousands of price updates per second
23// - Most prices can be represented as integer ticks (e.g., $0.6543 = 6543 ticks)
24//
25// The strategy:
26// 1. Convert Decimal to fixed-point on ingress (when data comes in)
27// 2. Do all hot-path calculations with integers
28// 3. Convert back to Decimal only at the edges (API responses, user display)
29//
30// This is like how video games handle positions, they use integers internally
31// for speed, but show floating-point coordinates to players.
32/// Each tick represents 0.0001 (1/10,000) of the base unit
33/// Examples:
34/// - $0.6543 = 6543 ticks
35/// - $1.0000 = 10000 ticks  
36/// - $0.0001 = 1 tick (minimum price increment)
37///
38/// Why u32?
39/// - Can represent prices from $0.0001 to $429,496.7295 (way more than needed)
40/// - Fits in CPU register for fast operations
41/// - No sign bit needed since prices are always positive
42pub type Price = u32;
43
44/// Quantity/size represented as fixed-point integer for performance
45///
46/// Each unit represents 0.0001 (1/10,000) of a token
47/// Examples:
48/// - 100.0 tokens = 1,000,000 units
49/// - 0.0001 tokens = 1 unit (minimum size increment)
50///
51/// Why i64?
52/// - Can represent quantities from -922,337,203,685.4775 to +922,337,203,685.4775
53/// - Signed because we need to handle both buys (+) and sells (-)
54/// - Large enough for any realistic trading size
55pub type Qty = i64;
56
57/// Scale factor for converting between Decimal and fixed-point
58///
59/// We use 10,000 (1e4) as our scale factor, giving us 4 decimal places of precision.
60/// This is perfect for most prediction markets where prices are between $0.01-$0.99
61/// and we need precision to the nearest $0.0001.
62pub const SCALE_FACTOR: i64 = 10_000;
63
64/// Maximum valid price in ticks (prevents overflow)
65/// This represents $429,496.7295 which is way higher than any prediction market price
66pub const MAX_PRICE_TICKS: Price = Price::MAX;
67
68/// Minimum valid price in ticks (1 tick = $0.0001)
69pub const MIN_PRICE_TICKS: Price = 1;
70
71/// Maximum valid quantity (prevents overflow in calculations)
72pub const MAX_QTY: Qty = Qty::MAX / 2; // Leave room for intermediate calculations
73
74// ============================================================================
75// CONVERSION FUNCTIONS BETWEEN DECIMAL AND FIXED-POINT
76// ============================================================================
77//
78// These functions handle the conversion between the external Decimal API
79// and our internal fixed-point representation. They're designed to be fast
80// and handle edge cases gracefully.
81
82/// Convert a Decimal price to fixed-point ticks
83///
84/// This is called when we receive price data from the API or user input.
85/// We quantize the price to the nearest tick to ensure all prices are
86/// aligned to our internal representation.
87///
88/// Examples:
89/// - decimal_to_price(Decimal::from_str("0.6543")) = Ok(6543)
90/// - decimal_to_price(Decimal::from_str("1.0000")) = Ok(10000)
91/// - decimal_to_price(Decimal::from_str("0.00005")) = Ok(1) // Rounds up to min tick
92pub fn decimal_to_price(decimal: Decimal) -> std::result::Result<Price, &'static str> {
93    // Convert to fixed-point by multiplying by scale factor
94    let scaled = decimal * Decimal::from(SCALE_FACTOR);
95
96    // Round to nearest integer (this handles tick alignment automatically)
97    let rounded = scaled.round();
98
99    // Convert to u64 first to handle the conversion safely
100    let as_u64 = rounded.to_u64().ok_or("Price too large or negative")?;
101
102    // Check bounds
103    if as_u64 < MIN_PRICE_TICKS as u64 {
104        return Ok(MIN_PRICE_TICKS); // Clamp to minimum
105    }
106    if as_u64 > MAX_PRICE_TICKS as u64 {
107        return Err("Price exceeds maximum");
108    }
109
110    Ok(as_u64 as Price)
111}
112
113/// Convert fixed-point ticks back to Decimal price
114///
115/// This is called when we need to return price data to the API or display to users.
116/// It's the inverse of decimal_to_price().
117///
118/// Examples:
119/// - price_to_decimal(6543) = Decimal::from_str("0.6543")
120/// - price_to_decimal(10000) = Decimal::from_str("1.0000")
121pub fn price_to_decimal(ticks: Price) -> Decimal {
122    Decimal::from(ticks) / Decimal::from(SCALE_FACTOR)
123}
124
125/// Convert a Decimal quantity to fixed-point units
126///
127/// Similar to decimal_to_price but handles signed quantities.
128/// Quantities can be negative (for sells or position changes).
129///
130/// Examples:
131/// - decimal_to_qty(Decimal::from_str("100.0")) = Ok(1000000)
132/// - decimal_to_qty(Decimal::from_str("-50.5")) = Ok(-505000)
133pub fn decimal_to_qty(decimal: Decimal) -> std::result::Result<Qty, &'static str> {
134    let scaled = decimal * Decimal::from(SCALE_FACTOR);
135    let rounded = scaled.round();
136
137    let as_i64 = rounded.to_i64().ok_or("Quantity too large")?;
138
139    if as_i64.abs() > MAX_QTY {
140        return Err("Quantity exceeds maximum");
141    }
142
143    Ok(as_i64)
144}
145
146/// Convert fixed-point units back to Decimal quantity
147///
148/// Examples:
149/// - qty_to_decimal(1000000) = Decimal::from_str("100.0")
150/// - qty_to_decimal(-505000) = Decimal::from_str("-50.5")
151pub fn qty_to_decimal(units: Qty) -> Decimal {
152    Decimal::from(units) / Decimal::from(SCALE_FACTOR)
153}
154
155/// Check if a price is properly tick-aligned
156///
157/// This is used to validate incoming price data. In a well-behaved system,
158/// all prices should already be tick-aligned, but we check anyway to catch
159/// bugs or malicious data.
160///
161/// A price is tick-aligned if it's an exact multiple of the minimum tick size.
162/// Since we use integer ticks internally, this just checks if the price
163/// converts cleanly to our internal representation.
164pub fn is_price_tick_aligned(decimal: Decimal, tick_size_decimal: Decimal) -> bool {
165    // Convert tick size to our internal representation
166    let tick_size_ticks = match decimal_to_price(tick_size_decimal) {
167        Ok(ticks) => ticks,
168        Err(_) => return false,
169    };
170
171    // Convert the price to ticks
172    let price_ticks = match decimal_to_price(decimal) {
173        Ok(ticks) => ticks,
174        Err(_) => return false,
175    };
176
177    // Check if price is a multiple of tick size
178    // If tick_size_ticks is 0, we consider everything aligned (no restrictions)
179    if tick_size_ticks == 0 {
180        return true;
181    }
182
183    price_ticks % tick_size_ticks == 0
184}
185
186/// Trading side for orders
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
188#[allow(clippy::upper_case_acronyms)]
189pub enum Side {
190    BUY = 0,
191    SELL = 1,
192}
193
194impl Side {
195    pub fn as_str(&self) -> &'static str {
196        match self {
197            Side::BUY => "BUY",
198            Side::SELL => "SELL",
199        }
200    }
201
202    pub fn opposite(&self) -> Self {
203        match self {
204            Side::BUY => Side::SELL,
205            Side::SELL => Side::BUY,
206        }
207    }
208}
209
210/// Order type specifications
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
212#[allow(clippy::upper_case_acronyms)]
213pub enum OrderType {
214    #[default]
215    GTC,
216    FOK,
217    GTD,
218}
219
220impl OrderType {
221    pub fn as_str(&self) -> &'static str {
222        match self {
223            OrderType::GTC => "GTC",
224            OrderType::FOK => "FOK",
225            OrderType::GTD => "GTD",
226        }
227    }
228}
229
230/// Order status in the system
231#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
232pub enum OrderStatus {
233    #[serde(rename = "LIVE")]
234    Live,
235    #[serde(rename = "CANCELLED")]
236    Cancelled,
237    #[serde(rename = "FILLED")]
238    Filled,
239    #[serde(rename = "PARTIAL")]
240    Partial,
241    #[serde(rename = "EXPIRED")]
242    Expired,
243}
244
245/// Market snapshot representing current state
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct MarketSnapshot {
248    pub token_id: String,
249    pub market_id: String,
250    pub timestamp: DateTime<Utc>,
251    pub bid: Option<Decimal>,
252    pub ask: Option<Decimal>,
253    pub mid: Option<Decimal>,
254    pub spread: Option<Decimal>,
255    pub last_price: Option<Decimal>,
256    pub volume_24h: Option<Decimal>,
257}
258
259/// Order book level (price/size pair) - EXTERNAL API VERSION
260///
261/// This is what we expose to users and serialize to JSON.
262/// It uses Decimal for precision and human readability.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct BookLevel {
265    #[serde(with = "rust_decimal::serde::str")]
266    pub price: Decimal,
267    #[serde(with = "rust_decimal::serde::str")]
268    pub size: Decimal,
269}
270
271/// Order book level (price/size pair) - INTERNAL HOT PATH VERSION
272///
273/// This is what we use internally for maximum performance.
274/// All order book operations use this to avoid Decimal overhead.
275///
276/// The performance difference is huge:
277/// - BookLevel: ~50ns per operation (Decimal math + allocation)
278/// - FastBookLevel: ~2ns per operation (integer math, no allocation)
279///
280/// That's a 25x speedup on the critical path
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct FastBookLevel {
283    pub price: Price, // Price in ticks (u32)
284    pub size: Qty,    // Size in fixed-point units (i64)
285}
286
287impl FastBookLevel {
288    /// Create a new fast book level
289    pub fn new(price: Price, size: Qty) -> Self {
290        Self { price, size }
291    }
292
293    /// Convert to external BookLevel for API responses
294    /// This is only called at the edges when we need to return data to users
295    pub fn to_book_level(self) -> BookLevel {
296        BookLevel {
297            price: price_to_decimal(self.price),
298            size: qty_to_decimal(self.size),
299        }
300    }
301
302    /// Create from external BookLevel (with validation)
303    /// This is called when we receive data from the API
304    pub fn from_book_level(level: &BookLevel) -> std::result::Result<Self, &'static str> {
305        let price = decimal_to_price(level.price)?;
306        let size = decimal_to_qty(level.size)?;
307        Ok(Self::new(price, size))
308    }
309
310    /// Calculate notional value (price * size) in fixed-point
311    /// Returns the result scaled appropriately to avoid overflow
312    ///
313    /// This is much faster than the Decimal equivalent:
314    /// - Decimal: price.mul(size) -> ~20ns + allocation
315    /// - Fixed-point: (price as i64 * size) / SCALE_FACTOR -> ~1ns, no allocation
316    pub fn notional(self) -> i64 {
317        // Convert price to i64 to avoid overflow in multiplication
318        let price_i64 = self.price as i64;
319        // Multiply and scale back down (we scaled both price and size up by SCALE_FACTOR)
320        (price_i64 * self.size) / SCALE_FACTOR
321    }
322}
323
324/// Full order book state
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct OrderBook {
327    /// Token ID
328    pub token_id: String,
329    /// Timestamp
330    pub timestamp: DateTime<Utc>,
331    /// Bid orders
332    pub bids: Vec<BookLevel>,
333    /// Ask orders
334    pub asks: Vec<BookLevel>,
335    /// Sequence number
336    pub sequence: u64,
337}
338
339/// Order book delta for streaming updates - EXTERNAL API VERSION
340///
341/// This is what we receive from WebSocket streams and REST API calls.
342/// It uses Decimal for compatibility with external systems.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct OrderDelta {
345    pub token_id: String,
346    pub timestamp: DateTime<Utc>,
347    pub side: Side,
348    pub price: Decimal,
349    pub size: Decimal, // 0 means remove level
350    pub sequence: u64,
351}
352
353/// Order book delta for streaming updates - INTERNAL HOT PATH VERSION
354///
355/// This is what we use internally for processing order book updates.
356/// Converting to this format on ingress gives us massive performance gains.
357///
358/// Why the performance matters:
359/// - We might process 10,000+ deltas per second in active markets
360/// - Each delta triggers multiple calculations (spread, impact, etc.)
361/// - Using integers instead of Decimal can make the difference between
362///   keeping up with the market feed vs falling behind
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub struct FastOrderDelta {
365    pub token_id_hash: u64, // Hash of token_id for fast lookup (avoids string comparisons)
366    pub timestamp: DateTime<Utc>,
367    pub side: Side,
368    pub price: Price, // Price in ticks
369    pub size: Qty,    // Size in fixed-point units (0 means remove level)
370    pub sequence: u64,
371}
372
373impl FastOrderDelta {
374    /// Create from external OrderDelta with validation and tick alignment
375    ///
376    /// This is where we enforce tick alignment - if the incoming price
377    /// doesn't align to valid ticks, we either reject it or round it.
378    /// This prevents bad data from corrupting our order book.
379    pub fn from_order_delta(
380        delta: &OrderDelta,
381        tick_size: Option<Decimal>,
382    ) -> std::result::Result<Self, &'static str> {
383        // Validate tick alignment if we have a tick size
384        if let Some(tick_size) = tick_size {
385            if !is_price_tick_aligned(delta.price, tick_size) {
386                return Err("Price not aligned to tick size");
387            }
388        }
389
390        // Convert to fixed-point with validation
391        let price = decimal_to_price(delta.price)?;
392        let size = decimal_to_qty(delta.size)?;
393
394        // Hash the token_id for fast lookups
395        // This avoids string comparisons in the hot path
396        let token_id_hash = {
397            use std::collections::hash_map::DefaultHasher;
398            use std::hash::{Hash, Hasher};
399            let mut hasher = DefaultHasher::new();
400            delta.token_id.hash(&mut hasher);
401            hasher.finish()
402        };
403
404        Ok(Self {
405            token_id_hash,
406            timestamp: delta.timestamp,
407            side: delta.side,
408            price,
409            size,
410            sequence: delta.sequence,
411        })
412    }
413
414    /// Convert back to external OrderDelta (for API responses)
415    /// We need the original token_id since we only store the hash
416    pub fn to_order_delta(self, token_id: String) -> OrderDelta {
417        OrderDelta {
418            token_id,
419            timestamp: self.timestamp,
420            side: self.side,
421            price: price_to_decimal(self.price),
422            size: qty_to_decimal(self.size),
423            sequence: self.sequence,
424        }
425    }
426
427    /// Check if this delta removes a level (size is zero)
428    pub fn is_removal(self) -> bool {
429        self.size == 0
430    }
431}
432
433/// Trade execution event
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct FillEvent {
436    pub id: String,
437    pub order_id: String,
438    pub token_id: String,
439    pub side: Side,
440    pub price: Decimal,
441    pub size: Decimal,
442    pub timestamp: DateTime<Utc>,
443    pub maker_address: Address,
444    pub taker_address: Address,
445    pub fee: Decimal,
446}
447
448/// Order creation parameters
449#[derive(Debug, Clone)]
450pub struct OrderRequest {
451    pub token_id: String,
452    pub side: Side,
453    pub price: Decimal,
454    pub size: Decimal,
455    pub order_type: OrderType,
456    pub expiration: Option<DateTime<Utc>>,
457    pub client_id: Option<String>,
458}
459
460/// Market order parameters
461#[derive(Debug, Clone)]
462pub struct MarketOrderRequest {
463    pub token_id: String,
464    pub side: Side,
465    pub amount: Decimal, // USD amount for buys, token amount for sells
466    pub slippage_tolerance: Option<Decimal>,
467    pub client_id: Option<String>,
468}
469
470/// Order state in the system
471#[derive(Debug, Clone, Serialize, Deserialize)]
472pub struct Order {
473    pub id: String,
474    pub token_id: String,
475    pub side: Side,
476    pub price: Decimal,
477    pub original_size: Decimal,
478    pub filled_size: Decimal,
479    pub remaining_size: Decimal,
480    pub status: OrderStatus,
481    pub order_type: OrderType,
482    pub created_at: DateTime<Utc>,
483    pub updated_at: DateTime<Utc>,
484    pub expiration: Option<DateTime<Utc>>,
485    pub client_id: Option<String>,
486}
487
488/// API credentials for authentication
489#[derive(Debug, Clone, Serialize, Deserialize, Default)]
490pub struct ApiCredentials {
491    #[serde(rename = "apiKey")]
492    pub api_key: String,
493    pub secret: String,
494    pub passphrase: String,
495}
496
497/// Configuration for order creation
498#[derive(Debug, Clone)]
499pub struct OrderOptions {
500    pub tick_size: Option<Decimal>,
501    pub neg_risk: Option<bool>,
502    pub fee_rate_bps: Option<u32>,
503}
504
505/// Extra arguments for order creation
506#[derive(Debug, Clone)]
507pub struct ExtraOrderArgs {
508    pub fee_rate_bps: u32,
509    pub nonce: U256,
510    pub taker: String,
511}
512
513impl Default for ExtraOrderArgs {
514    fn default() -> Self {
515        Self {
516            fee_rate_bps: 0,
517            nonce: U256::ZERO,
518            taker: "0x0000000000000000000000000000000000000000".to_string(),
519        }
520    }
521}
522
523/// Market order arguments
524#[derive(Debug, Clone)]
525pub struct MarketOrderArgs {
526    pub token_id: String,
527    pub amount: Decimal,
528}
529
530/// Signed order request ready for submission
531#[derive(Debug, Clone, Serialize, Deserialize)]
532#[serde(rename_all = "camelCase")]
533pub struct SignedOrderRequest {
534    pub salt: u64,
535    pub maker: String,
536    pub signer: String,
537    pub taker: String,
538    pub token_id: String,
539    pub maker_amount: String,
540    pub taker_amount: String,
541    pub expiration: String,
542    pub nonce: String,
543    pub fee_rate_bps: String,
544    pub side: String,
545    pub signature_type: u8,
546    pub signature: String,
547}
548
549/// Post order wrapper
550#[derive(Debug, Serialize)]
551#[serde(rename_all = "camelCase")]
552pub struct PostOrder {
553    pub order: SignedOrderRequest,
554    pub owner: String,
555    pub order_type: OrderType,
556}
557
558impl PostOrder {
559    pub fn new(order: SignedOrderRequest, owner: String, order_type: OrderType) -> Self {
560        Self {
561            order,
562            owner,
563            order_type,
564        }
565    }
566}
567
568/// Market information
569#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct Market {
571    pub condition_id: String,
572    pub tokens: [Token; 2],
573    pub rewards: Rewards,
574    pub min_incentive_size: Option<String>,
575    pub max_incentive_spread: Option<String>,
576    pub active: bool,
577    pub closed: bool,
578    pub question_id: String,
579    pub minimum_order_size: Decimal,
580    pub minimum_tick_size: Decimal,
581    pub description: String,
582    pub category: Option<String>,
583    pub end_date_iso: Option<String>,
584    pub game_start_time: Option<String>,
585    pub question: String,
586    pub market_slug: String,
587    pub seconds_delay: Decimal,
588    pub icon: String,
589    pub fpmm: String,
590    // Additional fields from API
591    #[serde(default)]
592    pub enable_order_book: bool,
593    #[serde(default)]
594    pub archived: bool,
595    #[serde(default)]
596    pub accepting_orders: bool,
597    #[serde(default)]
598    pub accepting_order_timestamp: Option<String>,
599    #[serde(default)]
600    pub maker_base_fee: Decimal,
601    #[serde(default)]
602    pub taker_base_fee: Decimal,
603    #[serde(default)]
604    pub notifications_enabled: bool,
605    #[serde(default)]
606    pub neg_risk: bool,
607    #[serde(default)]
608    pub neg_risk_market_id: String,
609    #[serde(default)]
610    pub neg_risk_request_id: String,
611    #[serde(default)]
612    pub image: String,
613    #[serde(default)]
614    pub is_50_50_outcome: bool,
615}
616
617/// Token information within a market
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct Token {
620    pub token_id: String,
621    pub outcome: String,
622    pub price: Decimal,
623    #[serde(default)]
624    pub winner: bool,
625}
626
627/// Client configuration for PolyfillClient
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct ClientConfig {
630    /// Base URL for the API
631    pub base_url: String,
632    /// Chain ID for the network
633    pub chain_id: u64,
634    /// Private key for signing (optional)
635    pub private_key: Option<String>,
636    /// API credentials (optional)
637    pub api_credentials: Option<ApiCredentials>,
638    /// Maximum slippage tolerance
639    pub max_slippage: Option<Decimal>,
640    /// Fee rate in basis points
641    pub fee_rate: Option<Decimal>,
642    /// Request timeout
643    pub timeout: Option<std::time::Duration>,
644    /// Maximum number of connections
645    pub max_connections: Option<usize>,
646}
647
648impl Default for ClientConfig {
649    fn default() -> Self {
650        Self {
651            base_url: "https://clob.polymarket.com".to_string(),
652            chain_id: 137, // Polygon mainnet
653            private_key: None,
654            api_credentials: None,
655            timeout: Some(std::time::Duration::from_secs(30)),
656            max_connections: Some(100),
657            max_slippage: None,
658            fee_rate: None,
659        }
660    }
661}
662
663/// WebSocket authentication for Polymarket API user channel.
664///
665/// Polymarket's CLOB WebSocket expects the same L2 API credentials used for HTTP calls:
666/// `{ apiKey, secret, passphrase }`.
667pub type WssAuth = ApiCredentials;
668
669/// WebSocket subscription request
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct WssSubscription {
672    /// Channel type: "market" or "user"
673    #[serde(rename = "type")]
674    pub channel_type: String,
675    /// Operation type: "subscribe" or "unsubscribe"
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub operation: Option<String>,
678    /// Array of markets (condition IDs) for USER channel
679    #[serde(default)]
680    pub markets: Vec<String>,
681    /// Array of asset IDs (token IDs) for MARKET channel
682    /// Note: Field name is "assets_ids" (with 's') per Polymarket API spec
683    #[serde(rename = "assets_ids", default)]
684    pub asset_ids: Vec<String>,
685    /// Request initial state dump
686    #[serde(skip_serializing_if = "Option::is_none")]
687    pub initial_dump: Option<bool>,
688    /// Enable custom features (best_bid_ask, new_market, market_resolved)
689    #[serde(skip_serializing_if = "Option::is_none")]
690    pub custom_feature_enabled: Option<bool>,
691    /// Authentication information (only for USER channel)
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub auth: Option<WssAuth>,
694}
695
696/// WebSocket message types for streaming (official Polymarket `event_type` format).
697#[derive(Debug, Clone, Serialize, Deserialize)]
698#[serde(tag = "event_type")]
699pub enum StreamMessage {
700    /// Full or incremental orderbook update
701    #[serde(rename = "book")]
702    Book(BookUpdate),
703    /// Price change notification (single or batched)
704    #[serde(rename = "price_change")]
705    PriceChange(PriceChange),
706    /// Tick size change notification
707    #[serde(rename = "tick_size_change")]
708    TickSizeChange(TickSizeChange),
709    /// Last trade price update
710    #[serde(rename = "last_trade_price")]
711    LastTradePrice(LastTradePrice),
712    /// Best bid/ask update (requires `custom_feature_enabled`)
713    #[serde(rename = "best_bid_ask")]
714    BestBidAsk(BestBidAsk),
715    /// New market created (requires `custom_feature_enabled`)
716    #[serde(rename = "new_market")]
717    NewMarket(NewMarket),
718    /// Market resolved (requires `custom_feature_enabled`)
719    #[serde(rename = "market_resolved")]
720    MarketResolved(MarketResolved),
721    /// User trade execution (authenticated channel)
722    #[serde(rename = "trade")]
723    Trade(TradeMessage),
724    /// User order update (authenticated channel)
725    #[serde(rename = "order")]
726    Order(OrderMessage),
727    /// Forward-compatible catch-all for new/unknown event types.
728    #[serde(other)]
729    Unknown,
730}
731
732/// Orderbook update message (full snapshot or delta).
733#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct BookUpdate {
735    pub asset_id: String,
736    pub market: String,
737    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
738    pub timestamp: u64,
739    #[serde(
740        default,
741        deserialize_with = "crate::decode::deserializers::vec_from_null"
742    )]
743    pub bids: Vec<OrderSummary>,
744    #[serde(
745        default,
746        deserialize_with = "crate::decode::deserializers::vec_from_null"
747    )]
748    pub asks: Vec<OrderSummary>,
749    #[serde(default)]
750    pub hash: Option<String>,
751}
752
753/// Unified wire format for `price_change` events.
754#[derive(Debug, Clone, Serialize, Deserialize)]
755pub struct PriceChange {
756    pub market: String,
757    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
758    pub timestamp: u64,
759    #[serde(
760        default,
761        deserialize_with = "crate::decode::deserializers::vec_from_null"
762    )]
763    pub price_changes: Vec<PriceChangeEntry>,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize)]
767pub struct PriceChangeEntry {
768    pub asset_id: String,
769    pub price: Decimal,
770    #[serde(
771        default,
772        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
773    )]
774    pub size: Option<Decimal>,
775    pub side: Side,
776    #[serde(default)]
777    pub hash: Option<String>,
778    #[serde(
779        default,
780        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
781    )]
782    pub best_bid: Option<Decimal>,
783    #[serde(
784        default,
785        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
786    )]
787    pub best_ask: Option<Decimal>,
788}
789
790/// Tick size change event.
791#[derive(Debug, Clone, Serialize, Deserialize)]
792pub struct TickSizeChange {
793    pub asset_id: String,
794    pub market: String,
795    pub old_tick_size: Decimal,
796    pub new_tick_size: Decimal,
797    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
798    pub timestamp: u64,
799}
800
801/// Last trade price update.
802#[derive(Debug, Clone, Serialize, Deserialize)]
803pub struct LastTradePrice {
804    pub asset_id: String,
805    pub market: String,
806    pub price: Decimal,
807    #[serde(default)]
808    pub side: Option<Side>,
809    #[serde(
810        default,
811        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
812    )]
813    pub size: Option<Decimal>,
814    #[serde(
815        default,
816        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
817    )]
818    pub fee_rate_bps: Option<Decimal>,
819    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
820    pub timestamp: u64,
821}
822
823/// Best bid/ask update.
824#[derive(Debug, Clone, Serialize, Deserialize)]
825pub struct BestBidAsk {
826    pub market: String,
827    pub asset_id: String,
828    pub best_bid: Decimal,
829    pub best_ask: Decimal,
830    pub spread: Decimal,
831    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
832    pub timestamp: u64,
833}
834
835/// New market created event.
836#[derive(Debug, Clone, Serialize, Deserialize)]
837pub struct NewMarket {
838    pub id: String,
839    pub question: String,
840    pub market: String,
841    pub slug: String,
842    pub description: String,
843    #[serde(rename = "assets_ids", alias = "asset_ids")]
844    pub asset_ids: Vec<String>,
845    #[serde(
846        default,
847        deserialize_with = "crate::decode::deserializers::vec_from_null"
848    )]
849    pub outcomes: Vec<String>,
850    #[serde(default)]
851    pub event_message: Option<EventMessage>,
852    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
853    pub timestamp: u64,
854}
855
856/// Market resolved event.
857#[derive(Debug, Clone, Serialize, Deserialize)]
858pub struct MarketResolved {
859    pub id: String,
860    #[serde(default)]
861    pub question: Option<String>,
862    pub market: String,
863    #[serde(default)]
864    pub slug: Option<String>,
865    #[serde(default)]
866    pub description: Option<String>,
867    #[serde(rename = "assets_ids", alias = "asset_ids")]
868    pub asset_ids: Vec<String>,
869    #[serde(
870        default,
871        deserialize_with = "crate::decode::deserializers::vec_from_null"
872    )]
873    pub outcomes: Vec<String>,
874    pub winning_asset_id: String,
875    pub winning_outcome: String,
876    #[serde(default)]
877    pub event_message: Option<EventMessage>,
878    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
879    pub timestamp: u64,
880}
881
882/// Event message object for market events.
883#[derive(Debug, Clone, Serialize, Deserialize)]
884pub struct EventMessage {
885    pub id: String,
886    pub ticker: String,
887    pub slug: String,
888    pub title: String,
889    pub description: String,
890}
891
892/// User trade execution message.
893#[derive(Debug, Clone, Serialize, Deserialize)]
894pub struct TradeMessage {
895    pub id: String,
896    pub market: String,
897    pub asset_id: String,
898    pub side: Side,
899    pub size: Decimal,
900    pub price: Decimal,
901    #[serde(default)]
902    pub status: Option<String>,
903    #[serde(rename = "type", default)]
904    pub msg_type: Option<String>,
905    #[serde(
906        default,
907        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
908    )]
909    pub last_update: Option<u64>,
910    #[serde(
911        default,
912        alias = "match_time",
913        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
914    )]
915    pub matchtime: Option<u64>,
916    #[serde(
917        default,
918        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
919    )]
920    pub timestamp: Option<u64>,
921}
922
923/// User order update message.
924#[derive(Debug, Clone, Serialize, Deserialize)]
925pub struct OrderMessage {
926    pub id: String,
927    pub market: String,
928    pub asset_id: String,
929    pub side: Side,
930    pub price: Decimal,
931    #[serde(rename = "type", default)]
932    pub msg_type: Option<String>,
933    #[serde(
934        default,
935        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
936    )]
937    pub original_size: Option<Decimal>,
938    #[serde(
939        default,
940        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string"
941    )]
942    pub size_matched: Option<Decimal>,
943    #[serde(
944        default,
945        deserialize_with = "crate::decode::deserializers::optional_number_from_string"
946    )]
947    pub timestamp: Option<u64>,
948    #[serde(default)]
949    pub associate_trades: Option<Vec<String>>,
950    #[serde(default)]
951    pub status: Option<String>,
952}
953
954/// Subscription parameters for streaming
955#[derive(Debug, Clone, Serialize, Deserialize)]
956pub struct Subscription {
957    pub token_ids: Vec<String>,
958    pub channels: Vec<String>,
959}
960
961/// WebSocket channel types
962#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
963pub enum WssChannelType {
964    #[serde(rename = "USER")]
965    User,
966    #[serde(rename = "MARKET")]
967    Market,
968}
969
970impl WssChannelType {
971    pub fn as_str(&self) -> &'static str {
972        match self {
973            WssChannelType::User => "USER",
974            WssChannelType::Market => "MARKET",
975        }
976    }
977}
978
979/// Price quote response
980#[derive(Debug, Clone, Serialize, Deserialize)]
981pub struct Quote {
982    pub token_id: String,
983    pub side: Side,
984    #[serde(with = "rust_decimal::serde::str")]
985    pub price: Decimal,
986    pub timestamp: DateTime<Utc>,
987}
988
989/// Balance information
990#[derive(Debug, Clone, Serialize, Deserialize)]
991pub struct Balance {
992    pub token_id: String,
993    pub available: Decimal,
994    pub locked: Decimal,
995    pub total: Decimal,
996}
997
998/// Performance metrics for monitoring
999#[derive(Debug, Clone)]
1000pub struct Metrics {
1001    pub orders_per_second: f64,
1002    pub avg_latency_ms: f64,
1003    pub error_rate: f64,
1004    pub uptime_pct: f64,
1005}
1006
1007// Type aliases for common patterns
1008pub type TokenId = String;
1009pub type OrderId = String;
1010pub type MarketId = String;
1011pub type ClientId = String;
1012
1013/// Parameters for querying open orders
1014#[derive(Debug, Clone)]
1015pub struct OpenOrderParams {
1016    pub id: Option<String>,
1017    pub asset_id: Option<String>,
1018    pub market: Option<String>,
1019}
1020
1021impl OpenOrderParams {
1022    pub fn to_query_params(&self) -> Vec<(&str, &String)> {
1023        let mut params = Vec::with_capacity(3);
1024
1025        if let Some(x) = &self.id {
1026            params.push(("id", x));
1027        }
1028
1029        if let Some(x) = &self.asset_id {
1030            params.push(("asset_id", x));
1031        }
1032
1033        if let Some(x) = &self.market {
1034            params.push(("market", x));
1035        }
1036        params
1037    }
1038}
1039
1040/// Parameters for querying trades
1041#[derive(Debug, Clone)]
1042pub struct TradeParams {
1043    pub id: Option<String>,
1044    pub maker_address: Option<String>,
1045    pub market: Option<String>,
1046    pub asset_id: Option<String>,
1047    pub before: Option<u64>,
1048    pub after: Option<u64>,
1049}
1050
1051impl TradeParams {
1052    pub fn to_query_params(&self) -> Vec<(&str, String)> {
1053        let mut params = Vec::with_capacity(6);
1054
1055        if let Some(x) = &self.id {
1056            params.push(("id", x.clone()));
1057        }
1058
1059        if let Some(x) = &self.asset_id {
1060            params.push(("asset_id", x.clone()));
1061        }
1062
1063        if let Some(x) = &self.market {
1064            params.push(("market", x.clone()));
1065        }
1066
1067        if let Some(x) = &self.maker_address {
1068            params.push(("maker_address", x.clone()));
1069        }
1070
1071        if let Some(x) = &self.before {
1072            params.push(("before", x.to_string()));
1073        }
1074
1075        if let Some(x) = &self.after {
1076            params.push(("after", x.to_string()));
1077        }
1078
1079        params
1080    }
1081}
1082
1083/// Open order information
1084#[derive(Debug, Clone, Serialize, Deserialize)]
1085pub struct OpenOrder {
1086    pub associate_trades: Vec<String>,
1087    pub id: String,
1088    pub status: String,
1089    pub market: String,
1090    #[serde(with = "rust_decimal::serde::str")]
1091    pub original_size: Decimal,
1092    pub outcome: String,
1093    pub maker_address: String,
1094    pub owner: String,
1095    #[serde(with = "rust_decimal::serde::str")]
1096    pub price: Decimal,
1097    pub side: Side,
1098    #[serde(with = "rust_decimal::serde::str")]
1099    pub size_matched: Decimal,
1100    pub asset_id: String,
1101    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1102    pub expiration: u64,
1103    #[serde(rename = "type", alias = "order_type", alias = "orderType", default)]
1104    pub order_type: OrderType,
1105    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1106    pub created_at: u64,
1107}
1108
1109/// Balance allowance information
1110#[derive(Debug, Clone, Serialize, Deserialize)]
1111pub struct BalanceAllowance {
1112    pub asset_id: String,
1113    #[serde(with = "rust_decimal::serde::str")]
1114    pub balance: Decimal,
1115    #[serde(with = "rust_decimal::serde::str")]
1116    pub allowance: Decimal,
1117}
1118
1119/// Parameters for balance allowance queries (from reference implementation)
1120#[derive(Default)]
1121pub struct BalanceAllowanceParams {
1122    pub asset_type: Option<AssetType>,
1123    pub token_id: Option<String>,
1124    pub signature_type: Option<u8>,
1125}
1126
1127impl BalanceAllowanceParams {
1128    pub fn to_query_params(&self) -> Vec<(&str, String)> {
1129        let mut params = Vec::with_capacity(3);
1130
1131        if let Some(x) = &self.asset_type {
1132            params.push(("asset_type", x.to_string()));
1133        }
1134
1135        if let Some(x) = &self.token_id {
1136            params.push(("token_id", x.to_string()));
1137        }
1138
1139        if let Some(x) = &self.signature_type {
1140            params.push(("signature_type", x.to_string()));
1141        }
1142        params
1143    }
1144
1145    pub fn set_signature_type(&mut self, s: u8) {
1146        self.signature_type = Some(s);
1147    }
1148}
1149
1150/// Asset type enum for balance allowance queries
1151#[allow(clippy::upper_case_acronyms)]
1152pub enum AssetType {
1153    COLLATERAL,
1154    CONDITIONAL,
1155}
1156
1157impl std::fmt::Display for AssetType {
1158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1159        match self {
1160            AssetType::COLLATERAL => write!(f, "COLLATERAL"),
1161            AssetType::CONDITIONAL => write!(f, "CONDITIONAL"),
1162        }
1163    }
1164}
1165
1166/// Notification preferences
1167#[derive(Debug, Clone, Serialize, Deserialize)]
1168pub struct NotificationParams {
1169    pub signature: String,
1170    pub timestamp: u64,
1171}
1172
1173/// Batch midpoint request
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1175pub struct BatchMidpointRequest {
1176    pub token_ids: Vec<String>,
1177}
1178
1179/// Batch midpoint response
1180#[derive(Debug, Clone, Serialize, Deserialize)]
1181pub struct BatchMidpointResponse {
1182    pub midpoints: std::collections::HashMap<String, Option<Decimal>>,
1183}
1184
1185/// Batch price request
1186#[derive(Debug, Clone, Serialize, Deserialize)]
1187pub struct BatchPriceRequest {
1188    pub token_ids: Vec<String>,
1189}
1190
1191/// Price information for a token
1192#[derive(Debug, Clone, Serialize, Deserialize)]
1193pub struct TokenPrice {
1194    pub token_id: String,
1195    #[serde(skip_serializing_if = "Option::is_none")]
1196    pub bid: Option<Decimal>,
1197    #[serde(skip_serializing_if = "Option::is_none")]
1198    pub ask: Option<Decimal>,
1199    #[serde(skip_serializing_if = "Option::is_none")]
1200    pub mid: Option<Decimal>,
1201}
1202
1203/// Batch price response
1204#[derive(Debug, Clone, Serialize, Deserialize)]
1205pub struct BatchPriceResponse {
1206    pub prices: Vec<TokenPrice>,
1207}
1208
1209// Additional types for API compatibility with reference implementation
1210#[derive(Debug, Deserialize)]
1211pub struct ApiKeysResponse {
1212    #[serde(rename = "apiKeys")]
1213    pub api_keys: Vec<String>,
1214}
1215
1216#[derive(Debug, Deserialize)]
1217pub struct MidpointResponse {
1218    #[serde(with = "rust_decimal::serde::str")]
1219    pub mid: Decimal,
1220}
1221
1222#[derive(Debug, Deserialize)]
1223pub struct PriceResponse {
1224    #[serde(with = "rust_decimal::serde::str")]
1225    pub price: Decimal,
1226}
1227
1228// ============================================================================
1229// PRICE HISTORY (ANALYTICS)
1230// ============================================================================
1231
1232/// Time bucket for the `/prices-history` endpoint.
1233///
1234/// Note: this endpoint uses a confusing query parameter name (`market`) but expects an
1235/// outcome asset id (`token_id` / `asset_id`) in **decimal string** form.
1236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1237pub enum PricesHistoryInterval {
1238    OneMinute,
1239    OneHour,
1240    SixHours,
1241    OneDay,
1242    OneWeek,
1243}
1244
1245impl PricesHistoryInterval {
1246    pub const fn as_str(self) -> &'static str {
1247        match self {
1248            Self::OneMinute => "1m",
1249            Self::OneHour => "1h",
1250            Self::SixHours => "6h",
1251            Self::OneDay => "1d",
1252            Self::OneWeek => "1w",
1253        }
1254    }
1255}
1256
1257/// Raw response from `/prices-history`.
1258///
1259/// We intentionally keep `history` entries as `serde_json::Value` because the upstream API has
1260/// no stable public schema here and currently may return empty history for many markets.
1261#[derive(Debug, Clone, Serialize, Deserialize)]
1262pub struct PricesHistoryResponse {
1263    pub history: Vec<serde_json::Value>,
1264}
1265
1266#[derive(Debug, Deserialize)]
1267pub struct SpreadResponse {
1268    #[serde(with = "rust_decimal::serde::str")]
1269    pub spread: Decimal,
1270}
1271
1272#[derive(Debug, Deserialize)]
1273pub struct TickSizeResponse {
1274    #[serde(with = "rust_decimal::serde::str")]
1275    pub minimum_tick_size: Decimal,
1276}
1277
1278#[derive(Debug, Deserialize)]
1279pub struct NegRiskResponse {
1280    pub neg_risk: bool,
1281}
1282
1283#[derive(Debug, Serialize, Deserialize)]
1284pub struct BookParams {
1285    pub token_id: String,
1286    pub side: Side,
1287}
1288
1289#[derive(Debug, Deserialize)]
1290pub struct OrderBookSummary {
1291    pub market: String,
1292    pub asset_id: String,
1293    #[serde(default)]
1294    pub hash: Option<String>,
1295    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1296    pub timestamp: u64,
1297    #[serde(
1298        default,
1299        deserialize_with = "crate::decode::deserializers::vec_from_null"
1300    )]
1301    pub bids: Vec<OrderSummary>,
1302    #[serde(
1303        default,
1304        deserialize_with = "crate::decode::deserializers::vec_from_null"
1305    )]
1306    pub asks: Vec<OrderSummary>,
1307    pub min_order_size: Decimal,
1308    pub neg_risk: bool,
1309    pub tick_size: Decimal,
1310    #[serde(
1311        default,
1312        deserialize_with = "crate::decode::deserializers::optional_decimal_from_string_default_on_error"
1313    )]
1314    pub last_trade_price: Option<Decimal>,
1315}
1316
1317#[derive(Debug, Clone, Serialize, Deserialize)]
1318pub struct OrderSummary {
1319    #[serde(with = "rust_decimal::serde::str")]
1320    pub price: Decimal,
1321    #[serde(with = "rust_decimal::serde::str")]
1322    pub size: Decimal,
1323}
1324
1325#[derive(Debug, Serialize, Deserialize)]
1326pub struct MarketsResponse {
1327    pub limit: usize,
1328    pub count: usize,
1329    pub next_cursor: Option<String>,
1330    pub data: Vec<Market>,
1331}
1332
1333#[derive(Debug, Serialize, Deserialize)]
1334pub struct SimplifiedMarketsResponse {
1335    pub limit: usize,
1336    pub count: usize,
1337    pub next_cursor: Option<String>,
1338    pub data: Vec<SimplifiedMarket>,
1339}
1340
1341/// Simplified market structure for batch operations
1342#[derive(Debug, Serialize, Deserialize)]
1343pub struct SimplifiedMarket {
1344    pub condition_id: String,
1345    pub tokens: [Token; 2],
1346    pub rewards: Rewards,
1347    pub min_incentive_size: Option<String>,
1348    pub max_incentive_spread: Option<String>,
1349    pub active: bool,
1350    pub closed: bool,
1351}
1352
1353/// Rewards structure for markets
1354#[derive(Debug, Clone, Serialize, Deserialize)]
1355pub struct Rewards {
1356    pub rates: Option<serde_json::Value>,
1357    // API returns these as plain numbers, not strings
1358    pub min_size: Decimal,
1359    pub max_spread: Decimal,
1360    #[serde(default)]
1361    pub event_start_date: Option<String>,
1362    #[serde(default)]
1363    pub event_end_date: Option<String>,
1364    #[serde(skip_serializing_if = "Option::is_none", default)]
1365    pub in_game_multiplier: Option<Decimal>,
1366    #[serde(skip_serializing_if = "Option::is_none", default)]
1367    pub reward_epoch: Option<Decimal>,
1368}
1369
1370// For compatibility with reference implementation
1371pub type ClientResult<T> = anyhow::Result<T>;
1372
1373/// Result type used throughout the client
1374pub type Result<T> = std::result::Result<T, crate::errors::PolyfillError>;
1375
1376// Type aliases for 100% compatibility with baseline implementation
1377pub type ApiCreds = ApiCredentials;
1378pub type CreateOrderOptions = OrderOptions;
1379pub type OrderArgs = OrderRequest;