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