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)]
212#[allow(clippy::upper_case_acronyms)]
213pub enum OrderType {
214    GTC,
215    FOK,
216    GTD,
217}
218
219impl OrderType {
220    pub fn as_str(&self) -> &'static str {
221        match self {
222            OrderType::GTC => "GTC",
223            OrderType::FOK => "FOK",
224            OrderType::GTD => "GTD",
225        }
226    }
227}
228
229/// Order status in the system
230#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
231pub enum OrderStatus {
232    #[serde(rename = "LIVE")]
233    Live,
234    #[serde(rename = "CANCELLED")]
235    Cancelled,
236    #[serde(rename = "FILLED")]
237    Filled,
238    #[serde(rename = "PARTIAL")]
239    Partial,
240    #[serde(rename = "EXPIRED")]
241    Expired,
242}
243
244/// Market snapshot representing current state
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct MarketSnapshot {
247    pub token_id: String,
248    pub market_id: String,
249    pub timestamp: DateTime<Utc>,
250    pub bid: Option<Decimal>,
251    pub ask: Option<Decimal>,
252    pub mid: Option<Decimal>,
253    pub spread: Option<Decimal>,
254    pub last_price: Option<Decimal>,
255    pub volume_24h: Option<Decimal>,
256}
257
258/// Order book level (price/size pair) - EXTERNAL API VERSION
259///
260/// This is what we expose to users and serialize to JSON.
261/// It uses Decimal for precision and human readability.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct BookLevel {
264    #[serde(with = "rust_decimal::serde::str")]
265    pub price: Decimal,
266    #[serde(with = "rust_decimal::serde::str")]
267    pub size: Decimal,
268}
269
270/// Order book level (price/size pair) - INTERNAL HOT PATH VERSION
271///
272/// This is what we use internally for maximum performance.
273/// All order book operations use this to avoid Decimal overhead.
274///
275/// The performance difference is huge:
276/// - BookLevel: ~50ns per operation (Decimal math + allocation)
277/// - FastBookLevel: ~2ns per operation (integer math, no allocation)
278///
279/// That's a 25x speedup on the critical path
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub struct FastBookLevel {
282    pub price: Price, // Price in ticks (u32)
283    pub size: Qty,    // Size in fixed-point units (i64)
284}
285
286impl FastBookLevel {
287    /// Create a new fast book level
288    pub fn new(price: Price, size: Qty) -> Self {
289        Self { price, size }
290    }
291
292    /// Convert to external BookLevel for API responses
293    /// This is only called at the edges when we need to return data to users
294    pub fn to_book_level(self) -> BookLevel {
295        BookLevel {
296            price: price_to_decimal(self.price),
297            size: qty_to_decimal(self.size),
298        }
299    }
300
301    /// Create from external BookLevel (with validation)
302    /// This is called when we receive data from the API
303    pub fn from_book_level(level: &BookLevel) -> std::result::Result<Self, &'static str> {
304        let price = decimal_to_price(level.price)?;
305        let size = decimal_to_qty(level.size)?;
306        Ok(Self::new(price, size))
307    }
308
309    /// Calculate notional value (price * size) in fixed-point
310    /// Returns the result scaled appropriately to avoid overflow
311    ///
312    /// This is much faster than the Decimal equivalent:
313    /// - Decimal: price.mul(size) -> ~20ns + allocation
314    /// - Fixed-point: (price as i64 * size) / SCALE_FACTOR -> ~1ns, no allocation
315    pub fn notional(self) -> i64 {
316        // Convert price to i64 to avoid overflow in multiplication
317        let price_i64 = self.price as i64;
318        // Multiply and scale back down (we scaled both price and size up by SCALE_FACTOR)
319        (price_i64 * self.size) / SCALE_FACTOR
320    }
321}
322
323/// Full order book state
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct OrderBook {
326    /// Token ID
327    pub token_id: String,
328    /// Timestamp
329    pub timestamp: DateTime<Utc>,
330    /// Bid orders
331    pub bids: Vec<BookLevel>,
332    /// Ask orders
333    pub asks: Vec<BookLevel>,
334    /// Sequence number
335    pub sequence: u64,
336}
337
338/// Order book delta for streaming updates - EXTERNAL API VERSION
339///
340/// This is what we receive from WebSocket streams and REST API calls.
341/// It uses Decimal for compatibility with external systems.
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct OrderDelta {
344    pub token_id: String,
345    pub timestamp: DateTime<Utc>,
346    pub side: Side,
347    pub price: Decimal,
348    pub size: Decimal, // 0 means remove level
349    pub sequence: u64,
350}
351
352/// Order book delta for streaming updates - INTERNAL HOT PATH VERSION
353///
354/// This is what we use internally for processing order book updates.
355/// Converting to this format on ingress gives us massive performance gains.
356///
357/// Why the performance matters:
358/// - We might process 10,000+ deltas per second in active markets
359/// - Each delta triggers multiple calculations (spread, impact, etc.)
360/// - Using integers instead of Decimal can make the difference between
361///   keeping up with the market feed vs falling behind
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub struct FastOrderDelta {
364    pub token_id_hash: u64, // Hash of token_id for fast lookup (avoids string comparisons)
365    pub timestamp: DateTime<Utc>,
366    pub side: Side,
367    pub price: Price, // Price in ticks
368    pub size: Qty,    // Size in fixed-point units (0 means remove level)
369    pub sequence: u64,
370}
371
372impl FastOrderDelta {
373    /// Create from external OrderDelta with validation and tick alignment
374    ///
375    /// This is where we enforce tick alignment - if the incoming price
376    /// doesn't align to valid ticks, we either reject it or round it.
377    /// This prevents bad data from corrupting our order book.
378    pub fn from_order_delta(
379        delta: &OrderDelta,
380        tick_size: Option<Decimal>,
381    ) -> std::result::Result<Self, &'static str> {
382        // Validate tick alignment if we have a tick size
383        if let Some(tick_size) = tick_size {
384            if !is_price_tick_aligned(delta.price, tick_size) {
385                return Err("Price not aligned to tick size");
386            }
387        }
388
389        // Convert to fixed-point with validation
390        let price = decimal_to_price(delta.price)?;
391        let size = decimal_to_qty(delta.size)?;
392
393        // Hash the token_id for fast lookups
394        // This avoids string comparisons in the hot path
395        let token_id_hash = {
396            use std::collections::hash_map::DefaultHasher;
397            use std::hash::{Hash, Hasher};
398            let mut hasher = DefaultHasher::new();
399            delta.token_id.hash(&mut hasher);
400            hasher.finish()
401        };
402
403        Ok(Self {
404            token_id_hash,
405            timestamp: delta.timestamp,
406            side: delta.side,
407            price,
408            size,
409            sequence: delta.sequence,
410        })
411    }
412
413    /// Convert back to external OrderDelta (for API responses)
414    /// We need the original token_id since we only store the hash
415    pub fn to_order_delta(self, token_id: String) -> OrderDelta {
416        OrderDelta {
417            token_id,
418            timestamp: self.timestamp,
419            side: self.side,
420            price: price_to_decimal(self.price),
421            size: qty_to_decimal(self.size),
422            sequence: self.sequence,
423        }
424    }
425
426    /// Check if this delta removes a level (size is zero)
427    pub fn is_removal(self) -> bool {
428        self.size == 0
429    }
430}
431
432/// Trade execution event
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct FillEvent {
435    pub id: String,
436    pub order_id: String,
437    pub token_id: String,
438    pub side: Side,
439    pub price: Decimal,
440    pub size: Decimal,
441    pub timestamp: DateTime<Utc>,
442    pub maker_address: Address,
443    pub taker_address: Address,
444    pub fee: Decimal,
445}
446
447/// Order creation parameters
448#[derive(Debug, Clone)]
449pub struct OrderRequest {
450    pub token_id: String,
451    pub side: Side,
452    pub price: Decimal,
453    pub size: Decimal,
454    pub order_type: OrderType,
455    pub expiration: Option<DateTime<Utc>>,
456    pub client_id: Option<String>,
457}
458
459/// Market order parameters
460#[derive(Debug, Clone)]
461pub struct MarketOrderRequest {
462    pub token_id: String,
463    pub side: Side,
464    pub amount: Decimal, // USD amount for buys, token amount for sells
465    pub slippage_tolerance: Option<Decimal>,
466    pub client_id: Option<String>,
467}
468
469/// Order state in the system
470#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct Order {
472    pub id: String,
473    pub token_id: String,
474    pub side: Side,
475    pub price: Decimal,
476    pub original_size: Decimal,
477    pub filled_size: Decimal,
478    pub remaining_size: Decimal,
479    pub status: OrderStatus,
480    pub order_type: OrderType,
481    pub created_at: DateTime<Utc>,
482    pub updated_at: DateTime<Utc>,
483    pub expiration: Option<DateTime<Utc>>,
484    pub client_id: Option<String>,
485}
486
487/// API credentials for authentication
488#[derive(Debug, Clone, Serialize, Deserialize, Default)]
489pub struct ApiCredentials {
490    #[serde(rename = "apiKey")]
491    pub api_key: String,
492    pub secret: String,
493    pub passphrase: String,
494}
495
496/// Configuration for order creation
497#[derive(Debug, Clone)]
498pub struct OrderOptions {
499    pub tick_size: Option<Decimal>,
500    pub neg_risk: Option<bool>,
501    pub fee_rate_bps: Option<u32>,
502}
503
504/// Extra arguments for order creation
505#[derive(Debug, Clone)]
506pub struct ExtraOrderArgs {
507    pub fee_rate_bps: u32,
508    pub nonce: U256,
509    pub taker: String,
510}
511
512impl Default for ExtraOrderArgs {
513    fn default() -> Self {
514        Self {
515            fee_rate_bps: 0,
516            nonce: U256::ZERO,
517            taker: "0x0000000000000000000000000000000000000000".to_string(),
518        }
519    }
520}
521
522/// Market order arguments
523#[derive(Debug, Clone)]
524pub struct MarketOrderArgs {
525    pub token_id: String,
526    pub amount: Decimal,
527}
528
529/// Signed order request ready for submission
530#[derive(Debug, Clone, Serialize, Deserialize)]
531#[serde(rename_all = "camelCase")]
532pub struct SignedOrderRequest {
533    pub salt: u64,
534    pub maker: String,
535    pub signer: String,
536    pub taker: String,
537    pub token_id: String,
538    pub maker_amount: String,
539    pub taker_amount: String,
540    pub expiration: String,
541    pub nonce: String,
542    pub fee_rate_bps: String,
543    pub side: String,
544    pub signature_type: u8,
545    pub signature: String,
546}
547
548/// Post order wrapper
549#[derive(Debug, Serialize)]
550#[serde(rename_all = "camelCase")]
551pub struct PostOrder {
552    pub order: SignedOrderRequest,
553    pub owner: String,
554    pub order_type: OrderType,
555}
556
557impl PostOrder {
558    pub fn new(order: SignedOrderRequest, owner: String, order_type: OrderType) -> Self {
559        Self {
560            order,
561            owner,
562            order_type,
563        }
564    }
565}
566
567/// Market information
568#[derive(Debug, Clone, Serialize, Deserialize)]
569pub struct Market {
570    pub condition_id: String,
571    pub tokens: [Token; 2],
572    pub rewards: Rewards,
573    pub min_incentive_size: Option<String>,
574    pub max_incentive_spread: Option<String>,
575    pub active: bool,
576    pub closed: bool,
577    pub question_id: String,
578    pub minimum_order_size: Decimal,
579    pub minimum_tick_size: Decimal,
580    pub description: String,
581    pub category: Option<String>,
582    pub end_date_iso: Option<String>,
583    pub game_start_time: Option<String>,
584    pub question: String,
585    pub market_slug: String,
586    pub seconds_delay: Decimal,
587    pub icon: String,
588    pub fpmm: String,
589    // Additional fields from API
590    #[serde(default)]
591    pub enable_order_book: bool,
592    #[serde(default)]
593    pub archived: bool,
594    #[serde(default)]
595    pub accepting_orders: bool,
596    #[serde(default)]
597    pub accepting_order_timestamp: Option<String>,
598    #[serde(default)]
599    pub maker_base_fee: Decimal,
600    #[serde(default)]
601    pub taker_base_fee: Decimal,
602    #[serde(default)]
603    pub notifications_enabled: bool,
604    #[serde(default)]
605    pub neg_risk: bool,
606    #[serde(default)]
607    pub neg_risk_market_id: String,
608    #[serde(default)]
609    pub neg_risk_request_id: String,
610    #[serde(default)]
611    pub image: String,
612    #[serde(default)]
613    pub is_50_50_outcome: bool,
614}
615
616/// Token information within a market
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct Token {
619    pub token_id: String,
620    pub outcome: String,
621    pub price: Decimal,
622    #[serde(default)]
623    pub winner: bool,
624}
625
626/// Client configuration for PolyfillClient
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct ClientConfig {
629    /// Base URL for the API
630    pub base_url: String,
631    /// Chain ID for the network
632    pub chain_id: u64,
633    /// Private key for signing (optional)
634    pub private_key: Option<String>,
635    /// API credentials (optional)
636    pub api_credentials: Option<ApiCredentials>,
637    /// Maximum slippage tolerance
638    pub max_slippage: Option<Decimal>,
639    /// Fee rate in basis points
640    pub fee_rate: Option<Decimal>,
641    /// Request timeout
642    pub timeout: Option<std::time::Duration>,
643    /// Maximum number of connections
644    pub max_connections: Option<usize>,
645}
646
647impl Default for ClientConfig {
648    fn default() -> Self {
649        Self {
650            base_url: "https://clob.polymarket.com".to_string(),
651            chain_id: 137, // Polygon mainnet
652            private_key: None,
653            api_credentials: None,
654            timeout: Some(std::time::Duration::from_secs(30)),
655            max_connections: Some(100),
656            max_slippage: None,
657            fee_rate: None,
658        }
659    }
660}
661
662/// WebSocket authentication for Polymarket API
663#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct WssAuth {
665    /// User's Ethereum address
666    pub address: String,
667    /// EIP-712 signature
668    pub signature: String,
669    /// Unix timestamp
670    pub timestamp: u64,
671    /// Nonce for replay protection
672    pub nonce: String,
673}
674
675/// WebSocket subscription request
676#[derive(Debug, Clone, Serialize, Deserialize)]
677pub struct WssSubscription {
678    /// Channel type: "market" or "user"
679    #[serde(rename = "type")]
680    pub channel_type: String,
681    /// Operation type: "subscribe" or "unsubscribe"
682    #[serde(skip_serializing_if = "Option::is_none")]
683    pub operation: Option<String>,
684    /// Array of markets (condition IDs) for USER channel
685    #[serde(default)]
686    pub markets: Vec<String>,
687    /// Array of asset IDs (token IDs) for MARKET channel
688    /// Note: Field name is "assets_ids" (with 's') per Polymarket API spec
689    #[serde(rename = "assets_ids", default)]
690    pub asset_ids: Vec<String>,
691    /// Request initial state dump
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub initial_dump: Option<bool>,
694    /// Enable custom features (best_bid_ask, new_market, market_resolved)
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub custom_feature_enabled: Option<bool>,
697    /// Authentication information (only for USER channel)
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub auth: Option<WssAuth>,
700}
701
702/// WebSocket message types for streaming
703#[derive(Debug, Clone, Serialize, Deserialize)]
704#[serde(tag = "type")]
705pub enum StreamMessage {
706    #[serde(rename = "book_update")]
707    BookUpdate { data: OrderDelta },
708    #[serde(rename = "trade")]
709    Trade { data: FillEvent },
710    #[serde(rename = "order_update")]
711    OrderUpdate { data: Order },
712    #[serde(rename = "heartbeat")]
713    Heartbeat { timestamp: DateTime<Utc> },
714    /// User channel events
715    #[serde(rename = "user_order_update")]
716    UserOrderUpdate { data: Order },
717    #[serde(rename = "user_trade")]
718    UserTrade { data: FillEvent },
719    /// Market channel events
720    #[serde(rename = "market_book_update")]
721    MarketBookUpdate { data: OrderDelta },
722    #[serde(rename = "market_trade")]
723    MarketTrade { data: FillEvent },
724}
725
726/// Subscription parameters for streaming
727#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct Subscription {
729    pub token_ids: Vec<String>,
730    pub channels: Vec<String>,
731}
732
733/// WebSocket channel types
734#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
735pub enum WssChannelType {
736    #[serde(rename = "USER")]
737    User,
738    #[serde(rename = "MARKET")]
739    Market,
740}
741
742impl WssChannelType {
743    pub fn as_str(&self) -> &'static str {
744        match self {
745            WssChannelType::User => "USER",
746            WssChannelType::Market => "MARKET",
747        }
748    }
749}
750
751/// Price quote response
752#[derive(Debug, Clone, Serialize, Deserialize)]
753pub struct Quote {
754    pub token_id: String,
755    pub side: Side,
756    #[serde(with = "rust_decimal::serde::str")]
757    pub price: Decimal,
758    pub timestamp: DateTime<Utc>,
759}
760
761/// Balance information
762#[derive(Debug, Clone, Serialize, Deserialize)]
763pub struct Balance {
764    pub token_id: String,
765    pub available: Decimal,
766    pub locked: Decimal,
767    pub total: Decimal,
768}
769
770/// Performance metrics for monitoring
771#[derive(Debug, Clone)]
772pub struct Metrics {
773    pub orders_per_second: f64,
774    pub avg_latency_ms: f64,
775    pub error_rate: f64,
776    pub uptime_pct: f64,
777}
778
779// Type aliases for common patterns
780pub type TokenId = String;
781pub type OrderId = String;
782pub type MarketId = String;
783pub type ClientId = String;
784
785/// Parameters for querying open orders
786#[derive(Debug, Clone)]
787pub struct OpenOrderParams {
788    pub id: Option<String>,
789    pub asset_id: Option<String>,
790    pub market: Option<String>,
791}
792
793impl OpenOrderParams {
794    pub fn to_query_params(&self) -> Vec<(&str, &String)> {
795        let mut params = Vec::with_capacity(3);
796
797        if let Some(x) = &self.id {
798            params.push(("id", x));
799        }
800
801        if let Some(x) = &self.asset_id {
802            params.push(("asset_id", x));
803        }
804
805        if let Some(x) = &self.market {
806            params.push(("market", x));
807        }
808        params
809    }
810}
811
812/// Parameters for querying trades
813#[derive(Debug, Clone)]
814pub struct TradeParams {
815    pub id: Option<String>,
816    pub maker_address: Option<String>,
817    pub market: Option<String>,
818    pub asset_id: Option<String>,
819    pub before: Option<u64>,
820    pub after: Option<u64>,
821}
822
823impl TradeParams {
824    pub fn to_query_params(&self) -> Vec<(&str, String)> {
825        let mut params = Vec::with_capacity(6);
826
827        if let Some(x) = &self.id {
828            params.push(("id", x.clone()));
829        }
830
831        if let Some(x) = &self.asset_id {
832            params.push(("asset_id", x.clone()));
833        }
834
835        if let Some(x) = &self.market {
836            params.push(("market", x.clone()));
837        }
838
839        if let Some(x) = &self.maker_address {
840            params.push(("maker_address", x.clone()));
841        }
842
843        if let Some(x) = &self.before {
844            params.push(("before", x.to_string()));
845        }
846
847        if let Some(x) = &self.after {
848            params.push(("after", x.to_string()));
849        }
850
851        params
852    }
853}
854
855/// Open order information
856#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct OpenOrder {
858    pub associate_trades: Vec<String>,
859    pub id: String,
860    pub status: String,
861    pub market: String,
862    #[serde(with = "rust_decimal::serde::str")]
863    pub original_size: Decimal,
864    pub outcome: String,
865    pub maker_address: String,
866    pub owner: String,
867    #[serde(with = "rust_decimal::serde::str")]
868    pub price: Decimal,
869    pub side: Side,
870    #[serde(with = "rust_decimal::serde::str")]
871    pub size_matched: Decimal,
872    pub asset_id: String,
873    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
874    pub expiration: u64,
875    #[serde(rename = "type")]
876    pub order_type: OrderType,
877    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
878    pub created_at: u64,
879}
880
881/// Balance allowance information
882#[derive(Debug, Clone, Serialize, Deserialize)]
883pub struct BalanceAllowance {
884    pub asset_id: String,
885    #[serde(with = "rust_decimal::serde::str")]
886    pub balance: Decimal,
887    #[serde(with = "rust_decimal::serde::str")]
888    pub allowance: Decimal,
889}
890
891/// Parameters for balance allowance queries (from reference implementation)
892#[derive(Default)]
893pub struct BalanceAllowanceParams {
894    pub asset_type: Option<AssetType>,
895    pub token_id: Option<String>,
896    pub signature_type: Option<u8>,
897}
898
899impl BalanceAllowanceParams {
900    pub fn to_query_params(&self) -> Vec<(&str, String)> {
901        let mut params = Vec::with_capacity(3);
902
903        if let Some(x) = &self.asset_type {
904            params.push(("asset_type", x.to_string()));
905        }
906
907        if let Some(x) = &self.token_id {
908            params.push(("token_id", x.to_string()));
909        }
910
911        if let Some(x) = &self.signature_type {
912            params.push(("signature_type", x.to_string()));
913        }
914        params
915    }
916
917    pub fn set_signature_type(&mut self, s: u8) {
918        self.signature_type = Some(s);
919    }
920}
921
922/// Asset type enum for balance allowance queries
923#[allow(clippy::upper_case_acronyms)]
924pub enum AssetType {
925    COLLATERAL,
926    CONDITIONAL,
927}
928
929impl std::fmt::Display for AssetType {
930    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
931        match self {
932            AssetType::COLLATERAL => write!(f, "COLLATERAL"),
933            AssetType::CONDITIONAL => write!(f, "CONDITIONAL"),
934        }
935    }
936}
937
938/// Notification preferences
939#[derive(Debug, Clone, Serialize, Deserialize)]
940pub struct NotificationParams {
941    pub signature: String,
942    pub timestamp: u64,
943}
944
945/// Batch midpoint request
946#[derive(Debug, Clone, Serialize, Deserialize)]
947pub struct BatchMidpointRequest {
948    pub token_ids: Vec<String>,
949}
950
951/// Batch midpoint response
952#[derive(Debug, Clone, Serialize, Deserialize)]
953pub struct BatchMidpointResponse {
954    pub midpoints: std::collections::HashMap<String, Option<Decimal>>,
955}
956
957/// Batch price request
958#[derive(Debug, Clone, Serialize, Deserialize)]
959pub struct BatchPriceRequest {
960    pub token_ids: Vec<String>,
961}
962
963/// Price information for a token
964#[derive(Debug, Clone, Serialize, Deserialize)]
965pub struct TokenPrice {
966    pub token_id: String,
967    #[serde(skip_serializing_if = "Option::is_none")]
968    pub bid: Option<Decimal>,
969    #[serde(skip_serializing_if = "Option::is_none")]
970    pub ask: Option<Decimal>,
971    #[serde(skip_serializing_if = "Option::is_none")]
972    pub mid: Option<Decimal>,
973}
974
975/// Batch price response
976#[derive(Debug, Clone, Serialize, Deserialize)]
977pub struct BatchPriceResponse {
978    pub prices: Vec<TokenPrice>,
979}
980
981// Additional types for API compatibility with reference implementation
982#[derive(Debug, Deserialize)]
983pub struct ApiKeysResponse {
984    #[serde(rename = "apiKeys")]
985    pub api_keys: Vec<String>,
986}
987
988#[derive(Debug, Deserialize)]
989pub struct MidpointResponse {
990    #[serde(with = "rust_decimal::serde::str")]
991    pub mid: Decimal,
992}
993
994#[derive(Debug, Deserialize)]
995pub struct PriceResponse {
996    #[serde(with = "rust_decimal::serde::str")]
997    pub price: Decimal,
998}
999
1000#[derive(Debug, Deserialize)]
1001pub struct SpreadResponse {
1002    #[serde(with = "rust_decimal::serde::str")]
1003    pub spread: Decimal,
1004}
1005
1006#[derive(Debug, Deserialize)]
1007pub struct TickSizeResponse {
1008    #[serde(with = "rust_decimal::serde::str")]
1009    pub minimum_tick_size: Decimal,
1010}
1011
1012#[derive(Debug, Deserialize)]
1013pub struct NegRiskResponse {
1014    pub neg_risk: bool,
1015}
1016
1017#[derive(Debug, Serialize, Deserialize)]
1018pub struct BookParams {
1019    pub token_id: String,
1020    pub side: Side,
1021}
1022
1023#[derive(Debug, Deserialize)]
1024pub struct OrderBookSummary {
1025    pub market: String,
1026    pub asset_id: String,
1027    pub hash: String,
1028    #[serde(deserialize_with = "crate::decode::deserializers::number_from_string")]
1029    pub timestamp: u64,
1030    pub bids: Vec<OrderSummary>,
1031    pub asks: Vec<OrderSummary>,
1032}
1033
1034#[derive(Debug, Deserialize)]
1035pub struct OrderSummary {
1036    #[serde(with = "rust_decimal::serde::str")]
1037    pub price: Decimal,
1038    #[serde(with = "rust_decimal::serde::str")]
1039    pub size: Decimal,
1040}
1041
1042#[derive(Debug, Serialize, Deserialize)]
1043pub struct MarketsResponse {
1044    pub limit: usize,
1045    pub count: usize,
1046    pub next_cursor: Option<String>,
1047    pub data: Vec<Market>,
1048}
1049
1050#[derive(Debug, Serialize, Deserialize)]
1051pub struct SimplifiedMarketsResponse {
1052    pub limit: usize,
1053    pub count: usize,
1054    pub next_cursor: Option<String>,
1055    pub data: Vec<SimplifiedMarket>,
1056}
1057
1058/// Simplified market structure for batch operations
1059#[derive(Debug, Serialize, Deserialize)]
1060pub struct SimplifiedMarket {
1061    pub condition_id: String,
1062    pub tokens: [Token; 2],
1063    pub rewards: Rewards,
1064    pub min_incentive_size: Option<String>,
1065    pub max_incentive_spread: Option<String>,
1066    pub active: bool,
1067    pub closed: bool,
1068}
1069
1070/// Rewards structure for markets
1071#[derive(Debug, Clone, Serialize, Deserialize)]
1072pub struct Rewards {
1073    pub rates: Option<serde_json::Value>,
1074    // API returns these as plain numbers, not strings
1075    pub min_size: Decimal,
1076    pub max_spread: Decimal,
1077    #[serde(default)]
1078    pub event_start_date: Option<String>,
1079    #[serde(default)]
1080    pub event_end_date: Option<String>,
1081    #[serde(skip_serializing_if = "Option::is_none", default)]
1082    pub in_game_multiplier: Option<Decimal>,
1083    #[serde(skip_serializing_if = "Option::is_none", default)]
1084    pub reward_epoch: Option<Decimal>,
1085}
1086
1087// For compatibility with reference implementation
1088pub type ClientResult<T> = anyhow::Result<T>;
1089
1090/// Result type used throughout the client
1091pub type Result<T> = std::result::Result<T, crate::errors::PolyfillError>;
1092
1093// Type aliases for 100% compatibility with baseline implementation
1094pub type ApiCreds = ApiCredentials;
1095pub type CreateOrderOptions = OrderOptions;
1096pub type OrderArgs = OrderRequest;