Skip to main content

px_core/exchange/
manifest.rs

1use crate::models::MarketStatus;
2
3/// Complete auditable manifest for an exchange.
4/// When opened, shows SOURCE and TRANSFORMATION side-by-side.
5#[derive(Debug, Clone)]
6pub struct ExchangeManifest {
7    // ========================================
8    // SECTION 1: CONNECTION AUDIT (Where do we go?)
9    // ========================================
10    /// Exchange identifier (e.g., "kalshi", "polymarket")
11    pub id: &'static str,
12
13    /// Human-readable exchange name
14    pub name: &'static str,
15
16    /// Base API URL
17    pub base_url: &'static str,
18
19    /// Markets list endpoint (relative to base_url)
20    pub markets_endpoint: &'static str,
21
22    /// Pagination configuration
23    pub pagination: PaginationConfig,
24
25    /// Rate limiting configuration
26    pub rate_limit: RateLimitConfig,
27
28    // ========================================
29    // SECTION 2: DATA AUDIT (How do we map it?)
30    // ========================================
31    /// Field mappings from raw exchange JSON to Market
32    pub field_mappings: &'static [FieldMapping],
33
34    /// Status value mappings (exchange status -> MarketStatus)
35    pub status_map: &'static [(&'static str, MarketStatus)],
36}
37
38impl ExchangeManifest {
39    /// Look up the MarketStatus for a given exchange status string.
40    /// Status map entries should be lowercase at definition time for O(n) without allocation.
41    pub fn map_status(&self, exchange_status: &str) -> Option<MarketStatus> {
42        self.status_map
43            .iter()
44            .find(|(s, _)| s.eq_ignore_ascii_case(exchange_status))
45            .map(|(_, status)| *status)
46    }
47
48    /// Get a field mapping by unified field name.
49    pub fn get_field_mapping(&self, unified_field: &str) -> Option<&FieldMapping> {
50        self.field_mappings
51            .iter()
52            .find(|m| m.unified_field == unified_field)
53    }
54}
55
56#[derive(Debug, Clone, Copy)]
57pub struct PaginationConfig {
58    /// Pagination style: Cursor, Offset, Page
59    pub style: PaginationStyle,
60    /// Maximum items per page
61    pub max_page_size: usize,
62    /// Query param name for limit
63    pub limit_param: &'static str,
64    /// Query param name for cursor/offset
65    pub cursor_param: &'static str,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum PaginationStyle {
70    /// Cursor-based pagination (Kalshi)
71    Cursor,
72    /// Offset-based pagination (Polymarket)
73    Offset,
74    /// Page-number pagination (Opinion, 1-indexed)
75    PageNumber,
76    /// No pagination supported - endpoint returns all data in single call
77    None,
78}
79
80/// Endpoint category for per-endpoint rate limiting.
81/// Each exchange maps these to its actual documented API rate limits.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[repr(u8)]
84pub enum RateLimitCategory {
85    /// Read operations: fetch markets, orderbook, positions, balance, trades, price history
86    Read = 0,
87    /// Write operations: create order, cancel order
88    Write = 1,
89    /// Bulk/data operations: market fetcher, paginated full-catalog fetches
90    Bulk = 2,
91}
92
93impl RateLimitCategory {
94    pub const COUNT: usize = 3;
95    pub const ALL: [RateLimitCategory; 3] = [
96        RateLimitCategory::Read,
97        RateLimitCategory::Write,
98        RateLimitCategory::Bulk,
99    ];
100}
101
102/// Rate limit for a single endpoint category.
103#[derive(Debug, Clone, Copy)]
104pub struct EndpointRateLimit {
105    pub category: RateLimitCategory,
106    pub requests_per_second: u32,
107    pub burst: u32,
108}
109
110/// Per-category rate limiting configuration for an exchange.
111/// Categories not listed in `limits` inherit from `default_rps`/`default_burst`.
112#[derive(Debug, Clone, Copy)]
113pub struct RateLimitConfig {
114    /// Fallback rate for any category not explicitly listed.
115    pub default_rps: u32,
116    /// Default burst for any category not explicitly listed.
117    pub default_burst: u32,
118    /// Per-category overrides (searched linearly; at most 3 entries).
119    pub limits: &'static [EndpointRateLimit],
120}
121
122impl RateLimitConfig {
123    /// Look up (rps, burst) for a given category.
124    pub const fn get(&self, category: RateLimitCategory) -> (u32, u32) {
125        let mut i = 0;
126        while i < self.limits.len() {
127            if self.limits[i].category as u8 == category as u8 {
128                return (self.limits[i].requests_per_second, self.limits[i].burst);
129            }
130            i += 1;
131        }
132        (self.default_rps, self.default_burst)
133    }
134
135    /// Convenience: get requests_per_second for a category.
136    pub const fn rps(&self, category: RateLimitCategory) -> u32 {
137        self.get(category).0
138    }
139
140    /// Backwards-compat: overall requests_per_second (uses Read category).
141    pub const fn requests_per_second(&self) -> u32 {
142        self.default_rps
143    }
144}
145
146/// Mapping from raw exchange JSON field to unified field.
147#[derive(Debug, Clone)]
148pub struct FieldMapping {
149    /// Target field in Market
150    pub unified_field: &'static str,
151    /// Source path(s) in raw JSON (fallback chain)
152    pub source_paths: &'static [&'static str],
153    /// Transformation to apply
154    pub transform: Transform,
155    /// Whether field can be null
156    pub nullable: bool,
157}
158
159/// Transformation to apply when mapping a field.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum Transform {
162    /// No transformation - use value directly
163    Direct,
164    /// Divide by 100 (Kalshi prices: cents -> decimal)
165    CentsToDollars,
166    /// Unix timestamp (seconds) to DateTime
167    UnixSecsToDateTime,
168    /// Unix timestamp (milliseconds) to DateTime
169    UnixMillisToDateTime,
170    /// ISO8601 string to DateTime
171    Iso8601ToDateTime,
172    /// String/Float -> i64
173    ParseInt,
174    /// String -> f64
175    ParseFloat,
176    /// Extract element at index from JSON array
177    JsonArrayIndex(usize),
178    /// Nested path extraction (dot-notation handled by source_paths)
179    NestedPath,
180}