Skip to main content

px_exchange_kalshi/
normalize.rs

1//! Canonical Kalshi trade normalization functions.
2//!
3//! Kalshi APIs return prices as either decimals (0.01–0.99) or cent-like
4//! integers (1–99). These helpers normalize both forms to probability space
5//! [0, 1] and canonicalize outcome strings.
6
7/// Normalize a raw Kalshi price to [0, 1] probability.
8///
9/// Returns `None` for non-finite, zero, or out-of-range values.
10pub fn normalize_kalshi_trade_price(raw: f64) -> Option<f64> {
11    if !raw.is_finite() {
12        return None;
13    }
14    let mut p = raw;
15    if p >= 1.0 {
16        p /= 100.0;
17    }
18    // Avoid floating-point representation artifacts in API payloads.
19    p = (p * 1_000_000.0).round() / 1_000_000.0;
20    if p > 0.0 && p < 1.0 {
21        Some(p)
22    } else {
23        None
24    }
25}
26
27/// Canonicalize a Kalshi outcome string to `"Yes"` or `"No"`.
28///
29/// Returns `None` for unrecognized outcomes.
30pub fn normalize_kalshi_outcome(outcome: Option<&str>) -> Option<String> {
31    outcome.and_then(|o| match o.trim().to_ascii_lowercase().as_str() {
32        "yes" => Some("Yes".to_string()),
33        "no" => Some("No".to_string()),
34        _ => None,
35    })
36}
37
38/// Trait for types whose Kalshi price/outcome/size fields can be normalized
39/// in-place. Implemented for `MarketTrade` and `TradeParquetRow`.
40pub trait KalshiTradeLike {
41    fn price(&self) -> f64;
42    fn set_price(&mut self, p: f64);
43    fn yes_price(&self) -> Option<f64>;
44    fn set_yes_price(&mut self, p: Option<f64>);
45    fn no_price(&self) -> Option<f64>;
46    fn set_no_price(&mut self, p: Option<f64>);
47    fn outcome(&self) -> Option<&str>;
48    fn set_outcome(&mut self, o: Option<String>);
49    fn size(&self) -> f64;
50}
51
52/// Normalize price, yes_price, no_price, outcome, and validate size on any
53/// `KalshiTradeLike`. Returns `None` if the main price is invalid or size <= 0.
54pub fn normalize_kalshi_trade<T: KalshiTradeLike>(mut trade: T) -> Option<T> {
55    trade.set_price(normalize_kalshi_trade_price(trade.price())?);
56    trade.set_yes_price(trade.yes_price().and_then(normalize_kalshi_trade_price));
57    trade.set_no_price(trade.no_price().and_then(normalize_kalshi_trade_price));
58    trade.set_outcome(normalize_kalshi_outcome(trade.outcome()));
59    if trade.size() <= 0.0 {
60        return None;
61    }
62    Some(trade)
63}
64
65impl KalshiTradeLike for px_core::MarketTrade {
66    fn price(&self) -> f64 {
67        self.price
68    }
69    fn set_price(&mut self, p: f64) {
70        self.price = p;
71    }
72    fn yes_price(&self) -> Option<f64> {
73        self.yes_price
74    }
75    fn set_yes_price(&mut self, p: Option<f64>) {
76        self.yes_price = p;
77    }
78    fn no_price(&self) -> Option<f64> {
79        self.no_price
80    }
81    fn set_no_price(&mut self, p: Option<f64>) {
82        self.no_price = p;
83    }
84    fn outcome(&self) -> Option<&str> {
85        self.outcome.as_deref()
86    }
87    fn set_outcome(&mut self, o: Option<String>) {
88        self.outcome = o;
89    }
90    fn size(&self) -> f64 {
91        self.size
92    }
93}