Skip to main content

paft_prediction/
instrument.rs

1//! Logical identity for prediction events, markets, and outcome instruments.
2
3use crate::error::PredictionError;
4use crate::identifiers::{
5    PredictionEventId, PredictionMarketId, PredictionOutcomeId, PredictionSeriesId, PredictionVenue,
6};
7use serde::{Deserialize, Deserializer, Serialize, de};
8use std::fmt;
9
10/// Venue-namespaced key for a recurring prediction series/group.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[cfg_attr(feature = "dataframe", derive(df_derive_macros::ToDataFrame))]
13pub struct PredictionSeriesKey {
14    /// Prediction venue that issued the series identifier.
15    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
16    pub venue: PredictionVenue,
17    /// Provider-native recurring series/group identifier.
18    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
19    pub series_id: PredictionSeriesId,
20}
21
22impl PredictionSeriesKey {
23    /// Construct a key from already-validated venue and series id values.
24    #[must_use]
25    pub const fn from_parts(venue: PredictionVenue, series_id: PredictionSeriesId) -> Self {
26        Self { venue, series_id }
27    }
28
29    /// Construct a key from string inputs.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`PredictionError`] if either input fails validation.
34    pub fn new(venue: &str, series_id: &str) -> Result<Self, PredictionError> {
35        Ok(Self {
36            venue: venue.parse()?,
37            series_id: PredictionSeriesId::new(series_id)?,
38        })
39    }
40
41    /// Returns a collision-resistant, venue-namespaced identity key.
42    #[must_use]
43    pub fn unique_key(&self) -> String {
44        component_key(self.venue.as_str(), "series", self.series_id.as_str())
45    }
46}
47
48impl fmt::Display for PredictionSeriesKey {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "{}:{}", self.venue, self.series_id)
51    }
52}
53
54/// Venue-namespaced key for a prediction event/grouping container.
55#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[cfg_attr(feature = "dataframe", derive(df_derive_macros::ToDataFrame))]
57pub struct PredictionEventKey {
58    /// Prediction venue that issued the event identifier.
59    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
60    pub venue: PredictionVenue,
61    /// Provider-native event/group identifier.
62    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
63    pub event_id: PredictionEventId,
64}
65
66impl PredictionEventKey {
67    /// Construct a key from already-validated venue and event id values.
68    #[must_use]
69    pub const fn from_parts(venue: PredictionVenue, event_id: PredictionEventId) -> Self {
70        Self { venue, event_id }
71    }
72
73    /// Construct a key from string inputs.
74    ///
75    /// # Errors
76    ///
77    /// Returns [`PredictionError`] if either input fails validation.
78    pub fn new(venue: &str, event_id: &str) -> Result<Self, PredictionError> {
79        Ok(Self {
80            venue: venue.parse()?,
81            event_id: PredictionEventId::new(event_id)?,
82        })
83    }
84
85    /// Returns a collision-resistant, venue-namespaced identity key.
86    #[must_use]
87    pub fn unique_key(&self) -> String {
88        component_key(self.venue.as_str(), "event", self.event_id.as_str())
89    }
90}
91
92impl fmt::Display for PredictionEventKey {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{}:{}", self.venue, self.event_id)
95    }
96}
97
98/// Venue-namespaced key for a prediction market of any shape.
99#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
100#[cfg_attr(feature = "dataframe", derive(df_derive_macros::ToDataFrame))]
101pub struct PredictionMarketKey {
102    /// Prediction venue that issued the market identifier.
103    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
104    pub venue: PredictionVenue,
105    /// Provider-native market/claim identifier.
106    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
107    pub market_id: PredictionMarketId,
108}
109
110impl PredictionMarketKey {
111    /// Construct a key from already-validated venue and market id values.
112    #[must_use]
113    pub const fn from_parts(venue: PredictionVenue, market_id: PredictionMarketId) -> Self {
114        Self { venue, market_id }
115    }
116
117    /// Construct a key from string inputs.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`PredictionError`] if either input fails validation.
122    pub fn new(venue: &str, market_id: &str) -> Result<Self, PredictionError> {
123        Ok(Self {
124            venue: venue.parse()?,
125            market_id: PredictionMarketId::new(market_id)?,
126        })
127    }
128
129    /// Returns a collision-resistant, venue-namespaced identity key.
130    #[must_use]
131    pub fn unique_key(&self) -> String {
132        component_key(self.venue.as_str(), "market", self.market_id.as_str())
133    }
134}
135
136impl fmt::Display for PredictionMarketKey {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "{}:{}", self.venue, self.market_id)
139    }
140}
141
142/// Venue-namespaced key for an atomic binary yes/no market.
143#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[cfg_attr(feature = "dataframe", derive(df_derive_macros::ToDataFrame))]
145pub struct BinaryMarketKey {
146    /// Prediction venue that issued the market identifier.
147    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
148    pub venue: PredictionVenue,
149    /// Provider-native atomic binary market/claim identifier.
150    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
151    pub market_id: PredictionMarketId,
152}
153
154impl BinaryMarketKey {
155    /// Construct a binary market key from already-validated parts.
156    #[must_use]
157    pub const fn from_parts(venue: PredictionVenue, market_id: PredictionMarketId) -> Self {
158        Self { venue, market_id }
159    }
160
161    /// Construct a binary market key from string inputs.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`PredictionError`] if either input fails validation.
166    pub fn new(venue: &str, market_id: &str) -> Result<Self, PredictionError> {
167        Ok(Self {
168            venue: venue.parse()?,
169            market_id: PredictionMarketId::new(market_id)?,
170        })
171    }
172
173    /// Returns this binary key as a shape-agnostic market key.
174    #[must_use]
175    pub fn to_market_key(&self) -> PredictionMarketKey {
176        PredictionMarketKey {
177            venue: self.venue.clone(),
178            market_id: self.market_id.clone(),
179        }
180    }
181
182    /// Returns a synthetic YES outcome instrument for this binary market.
183    ///
184    /// This is intended for venues whose provider-native binary instruments are
185    /// naturally identified as `YES`/`NO`, such as Kalshi-style adapters. Venues
186    /// with provider-issued outcome ids, such as Polymarket CLOB token ids,
187    /// should carry those concrete ids in [`BinaryOutcomeInstruments`].
188    #[must_use]
189    pub fn synthetic_yes_instrument(&self) -> OutcomeInstrument {
190        self.synthetic_outcome_instrument("YES")
191    }
192
193    /// Returns a synthetic NO outcome instrument for this binary market.
194    ///
195    /// This is intended for venues whose provider-native binary instruments are
196    /// naturally identified as `YES`/`NO`, such as Kalshi-style adapters. Venues
197    /// with provider-issued outcome ids, such as Polymarket CLOB token ids,
198    /// should carry those concrete ids in [`BinaryOutcomeInstruments`].
199    #[must_use]
200    pub fn synthetic_no_instrument(&self) -> OutcomeInstrument {
201        self.synthetic_outcome_instrument("NO")
202    }
203
204    fn synthetic_outcome_instrument(&self, outcome_id: &'static str) -> OutcomeInstrument {
205        OutcomeInstrument {
206            venue: self.venue.clone(),
207            market_id: self.market_id.clone(),
208            outcome_id: PredictionOutcomeId::from_static_unchecked(outcome_id),
209        }
210    }
211
212    /// Returns a collision-resistant, venue-namespaced identity key.
213    #[must_use]
214    pub fn unique_key(&self) -> String {
215        component_key(self.venue.as_str(), "market", self.market_id.as_str())
216    }
217}
218
219impl fmt::Display for BinaryMarketKey {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        write!(f, "{}:{}", self.venue, self.market_id)
222    }
223}
224
225/// Tradable outcome share/token/contract identity.
226#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
227#[cfg_attr(feature = "dataframe", derive(df_derive_macros::ToDataFrame))]
228pub struct OutcomeInstrument {
229    /// Prediction venue that issued the outcome instrument identifier.
230    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
231    pub venue: PredictionVenue,
232    /// Provider-native market/claim identifier this outcome belongs to.
233    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
234    pub market_id: PredictionMarketId,
235    /// Provider-native outcome instrument identifier.
236    #[cfg_attr(feature = "dataframe", df_derive(as_str))]
237    pub outcome_id: PredictionOutcomeId,
238}
239
240impl OutcomeInstrument {
241    /// Construct an outcome instrument from already-validated parts.
242    #[must_use]
243    pub const fn from_parts(
244        venue: PredictionVenue,
245        market_id: PredictionMarketId,
246        outcome_id: PredictionOutcomeId,
247    ) -> Self {
248        Self {
249            venue,
250            market_id,
251            outcome_id,
252        }
253    }
254
255    /// Construct an outcome instrument from string inputs.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`PredictionError`] if any input fails validation.
260    pub fn new(venue: &str, market_id: &str, outcome_id: &str) -> Result<Self, PredictionError> {
261        Ok(Self {
262            venue: venue.parse()?,
263            market_id: PredictionMarketId::new(market_id)?,
264            outcome_id: PredictionOutcomeId::new(outcome_id)?,
265        })
266    }
267
268    /// Returns the market key this outcome instrument belongs to.
269    #[must_use]
270    pub fn market_key(&self) -> PredictionMarketKey {
271        PredictionMarketKey {
272            venue: self.venue.clone(),
273            market_id: self.market_id.clone(),
274        }
275    }
276
277    /// Returns a collision-resistant, venue-namespaced identity key.
278    #[must_use]
279    pub fn unique_key(&self) -> String {
280        let venue = self.venue.as_str();
281        let market = self.market_id.as_str();
282        let outcome = self.outcome_id.as_str();
283        format!(
284            "venue:{}:{}|market:{}:{}|outcome:{}:{}",
285            venue.len(),
286            venue,
287            market.len(),
288            market,
289            outcome.len(),
290            outcome
291        )
292    }
293}
294
295impl fmt::Display for OutcomeInstrument {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        write!(f, "{}:{}/{}", self.venue, self.market_id, self.outcome_id)
298    }
299}
300
301/// Required YES and NO outcome instruments for an atomic binary market.
302#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
303pub struct BinaryOutcomeInstruments {
304    /// Tradable YES claim/share/token/contract.
305    yes: OutcomeInstrument,
306    /// Tradable NO claim/share/token/contract.
307    no: OutcomeInstrument,
308}
309
310impl BinaryOutcomeInstruments {
311    /// Construct binary outcome instruments from already-validated values.
312    ///
313    /// # Errors
314    ///
315    /// Returns [`PredictionError::MismatchedOutcomeInstrumentMarket`] when the
316    /// instruments do not belong to the same venue and market, or
317    /// [`PredictionError::DuplicateBinaryOutcomeInstrument`] when they use the
318    /// same provider-native outcome id.
319    pub fn new(yes: OutcomeInstrument, no: OutcomeInstrument) -> Result<Self, PredictionError> {
320        if yes.venue != no.venue || yes.market_id != no.market_id {
321            return Err(PredictionError::MismatchedOutcomeInstrumentMarket {
322                yes_venue: yes.venue.to_string(),
323                yes_market_id: yes.market_id.to_string(),
324                no_venue: no.venue.to_string(),
325                no_market_id: no.market_id.to_string(),
326            });
327        }
328
329        if yes.outcome_id == no.outcome_id {
330            return Err(PredictionError::DuplicateBinaryOutcomeInstrument {
331                venue: yes.venue.to_string(),
332                market_id: yes.market_id.to_string(),
333                outcome_id: yes.outcome_id.to_string(),
334            });
335        }
336
337        Ok(Self { yes, no })
338    }
339
340    /// Return the tradable YES claim/share/token/contract.
341    #[must_use]
342    pub const fn yes(&self) -> &OutcomeInstrument {
343        &self.yes
344    }
345
346    /// Return the tradable NO claim/share/token/contract.
347    #[must_use]
348    pub const fn no(&self) -> &OutcomeInstrument {
349        &self.no
350    }
351
352    /// Consume the pair and return the YES and NO instruments.
353    #[must_use]
354    pub fn into_parts(self) -> (OutcomeInstrument, OutcomeInstrument) {
355        (self.yes, self.no)
356    }
357
358    /// Construct synthetic `YES`/`NO` instruments for a binary market key.
359    ///
360    /// Use this for venues whose provider-native outcome identity is stable as
361    /// `YES`/`NO`. Use [`Self::new`] with concrete provider ids when the venue
362    /// exposes distinct outcome identifiers.
363    #[must_use]
364    pub fn synthetic_for_market(key: &BinaryMarketKey) -> Self {
365        Self {
366            yes: key.synthetic_yes_instrument(),
367            no: key.synthetic_no_instrument(),
368        }
369    }
370}
371
372impl<'de> Deserialize<'de> for BinaryOutcomeInstruments {
373    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
374    where
375        D: Deserializer<'de>,
376    {
377        #[derive(Deserialize)]
378        struct BinaryOutcomeInstrumentsShadow {
379            yes: OutcomeInstrument,
380            no: OutcomeInstrument,
381        }
382
383        let shadow = BinaryOutcomeInstrumentsShadow::deserialize(deserializer)?;
384        Self::new(shadow.yes, shadow.no).map_err(de::Error::custom)
385    }
386}
387
388fn component_key(venue: &str, role: &str, id: &str) -> String {
389    format!(
390        "venue:{}:{}|{}:{}:{}",
391        venue.len(),
392        venue,
393        role,
394        id.len(),
395        id
396    )
397}