1use crate::error::{
4 BinaryMarketOutcomeMismatch, MultiOutcomeMarketOutcomeMismatch, PredictionError,
5};
6use crate::identifiers::{PredictionOutcomeId, validate_opaque_identifier};
7use crate::instrument::{
8 BinaryMarketKey, BinaryOutcomeInstruments, OutcomeInstrument, PredictionEventKey,
9 PredictionMarketKey, PredictionSeriesKey,
10};
11use crate::price::{NonZeroContractQuantity, NonZeroOutcomePayout, OutcomePayout, PriceGrid};
12use chrono::{DateTime, Utc};
13use paft_decimal::Decimal;
14use paft_money::Currency;
15use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
16use smol_str::SmolStr;
17use std::{collections::HashSet, fmt, str::FromStr};
18
19macro_rules! opaque_metadata_code {
20 (
21 $(#[$meta:meta])*
22 pub struct $name:ident;
23 kind = $kind:literal;
24 modeled_by = $modeled_by:path;
25 ) => {
26 $(#[$meta])*
27 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
28 pub struct $name(SmolStr);
29
30 impl $name {
31 pub fn new(input: &str) -> Result<Self, PredictionError> {
40 let value = validate_opaque_identifier($kind, input)?;
41 if $modeled_by(value.as_str()) {
42 return Err(PredictionError::modeled_metadata_code($kind, input.to_string()));
43 }
44
45 Ok(Self(value))
46 }
47
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 self.0.as_str()
52 }
53 }
54
55 impl AsRef<str> for $name {
56 fn as_ref(&self) -> &str {
57 self.as_str()
58 }
59 }
60
61 impl FromStr for $name {
62 type Err = PredictionError;
63
64 fn from_str(input: &str) -> Result<Self, Self::Err> {
65 Self::new(input)
66 }
67 }
68
69 impl fmt::Display for $name {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(self.as_str())
72 }
73 }
74
75 impl Serialize for $name {
76 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
77 where
78 S: Serializer,
79 {
80 serializer.serialize_str(self.as_str())
81 }
82 }
83
84 impl<'de> Deserialize<'de> for $name {
85 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86 where
87 D: Deserializer<'de>,
88 {
89 let raw = String::deserialize(deserializer)?;
90 Self::new(&raw).map_err(de::Error::custom)
91 }
92 }
93 };
94}
95
96opaque_metadata_code!(
97 pub struct OtherEventStructure;
99 kind = "event structure code";
100 modeled_by = is_modeled_event_structure_code;
101);
102
103opaque_metadata_code!(
104 pub struct OtherLinkedBinaryRelation;
106 kind = "linked binary relation code";
107 modeled_by = is_modeled_linked_binary_relation_code;
108);
109
110opaque_metadata_code!(
111 pub struct OtherClaimDescriptor;
113 kind = "claim descriptor code";
114 modeled_by = is_modeled_claim_descriptor_code;
115);
116
117opaque_metadata_code!(
118 pub struct OtherPredictionMarketStatus;
120 kind = "prediction market status code";
121 modeled_by = is_modeled_prediction_market_status_code;
122);
123
124opaque_metadata_code!(
125 pub struct OtherBinarySettlement;
127 kind = "binary settlement code";
128 modeled_by = is_modeled_binary_settlement_code;
129);
130
131fn is_modeled_code(input: &str, modeled: &[&str]) -> bool {
132 modeled.iter().any(|code| input.eq_ignore_ascii_case(code))
133}
134
135fn is_modeled_event_structure_code(input: &str) -> bool {
136 is_modeled_code(
137 input,
138 &[
139 "single_market",
140 "independent_claims",
141 "mutually_exclusive",
142 "ordered_buckets",
143 "linked_binary_claims",
144 "composite",
145 ],
146 )
147}
148
149fn is_modeled_linked_binary_relation_code(input: &str) -> bool {
150 is_modeled_code(
151 input,
152 &["sum_to_one", "negative_risk_conversion", "composite_legs"],
153 )
154}
155
156fn is_modeled_claim_descriptor_code(input: &str) -> bool {
157 is_modeled_code(
158 input,
159 &["text", "categorical", "numeric_range", "composite"],
160 )
161}
162
163fn is_modeled_prediction_market_status_code(input: &str) -> bool {
164 is_modeled_code(
165 input,
166 &[
167 "upcoming",
168 "open",
169 "paused",
170 "closed",
171 "resolved",
172 "cancelled",
173 ],
174 )
175}
176
177fn is_modeled_binary_settlement_code(input: &str) -> bool {
178 is_modeled_code(input, &["yes", "no", "payout_vector", "void"])
179}
180
181macro_rules! open_string_metadata_enum {
182 (
183 $name:ident, $other:ident, $kind:literal;
184 {
185 $($code:literal => $variant:ident),+ $(,)?
186 }
187 ) => {
188 impl $name {
189 #[must_use]
191 pub fn code(&self) -> &str {
192 match self {
193 $(Self::$variant => $code,)+
194 Self::Other(value) => value.as_str(),
195 }
196 }
197
198 pub fn try_from_str(input: &str) -> Result<Self, PredictionError> {
205 let value = validate_opaque_identifier($kind, input)?;
206 $(if value.as_str().eq_ignore_ascii_case($code) {
207 return Ok(Self::$variant);
208 })+
209
210 $other::new(value.as_str()).map(Self::Other)
211 }
212 }
213
214 impl FromStr for $name {
215 type Err = PredictionError;
216
217 fn from_str(input: &str) -> Result<Self, Self::Err> {
218 Self::try_from_str(input)
219 }
220 }
221
222 impl fmt::Display for $name {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 f.write_str(self.code())
225 }
226 }
227
228 impl Serialize for $name {
229 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
230 where
231 S: Serializer,
232 {
233 serializer.serialize_str(self.code())
234 }
235 }
236
237 impl<'de> Deserialize<'de> for $name {
238 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
239 where
240 D: Deserializer<'de>,
241 {
242 let raw = String::deserialize(deserializer)?;
243 Self::try_from_str(&raw).map_err(de::Error::custom)
244 }
245 }
246 };
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
251#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
252#[non_exhaustive]
253pub enum EventStructure {
254 SingleMarket,
256 IndependentClaims,
258 MutuallyExclusive {
260 exhaustive: bool,
262 },
263 OrderedBuckets {
265 exhaustive: bool,
267 },
268 LinkedBinaryClaims {
270 relation: LinkedBinaryRelation,
272 },
273 Composite,
275 Other {
277 value: OtherEventStructure,
279 },
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Hash)]
284#[non_exhaustive]
285pub enum LinkedBinaryRelation {
286 SumToOne,
288 NegativeRiskConversion,
290 CompositeLegs,
292 Other(OtherLinkedBinaryRelation),
294}
295
296open_string_metadata_enum!(
297 LinkedBinaryRelation, OtherLinkedBinaryRelation, "linked binary relation code";
298 {
299 "sum_to_one" => SumToOne,
300 "negative_risk_conversion" => NegativeRiskConversion,
301 "composite_legs" => CompositeLegs,
302 }
303);
304
305#[derive(Debug, Clone, PartialEq, Eq, Hash)]
307#[non_exhaustive]
308pub enum PredictionMarketStatus {
309 Upcoming,
311 Open,
313 Paused,
315 Closed,
317 Resolved,
319 Cancelled,
321 Other(OtherPredictionMarketStatus),
323}
324
325open_string_metadata_enum!(
326 PredictionMarketStatus, OtherPredictionMarketStatus, "prediction market status code";
327 {
328 "upcoming" => Upcoming,
329 "open" => Open,
330 "paused" => Paused,
331 "closed" => Closed,
332 "resolved" => Resolved,
333 "cancelled" => Cancelled,
334 }
335);
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
339#[serde(deny_unknown_fields)]
340pub struct BinaryPayoutVector {
341 pub yes: OutcomePayout,
343 pub no: OutcomePayout,
345}
346
347impl BinaryPayoutVector {
348 #[must_use]
350 pub const fn new(yes: OutcomePayout, no: OutcomePayout) -> Self {
351 Self { yes, no }
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
357#[serde(
358 tag = "kind",
359 content = "value",
360 rename_all = "snake_case",
361 deny_unknown_fields
362)]
363#[non_exhaustive]
364pub enum BinarySettlement {
365 Yes,
367 No,
369 PayoutVector(BinaryPayoutVector),
371 Void,
373 Other(OtherBinarySettlement),
375}
376
377impl BinarySettlement {
378 #[must_use]
380 pub const fn resolved_payouts(
381 &self,
382 winning_payout: NonZeroOutcomePayout,
383 ) -> Option<BinaryPayoutVector> {
384 match self {
385 Self::Yes => Some(BinaryPayoutVector {
386 yes: winning_payout.to_payout(),
387 no: OutcomePayout::ZERO,
388 }),
389 Self::No => Some(BinaryPayoutVector {
390 yes: OutcomePayout::ZERO,
391 no: winning_payout.to_payout(),
392 }),
393 Self::PayoutVector(vector) => Some(*vector),
394 Self::Void | Self::Other(_) => None,
395 }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
401#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
402#[non_exhaustive]
403pub enum ClaimDescriptor {
404 Text {
406 description: String,
408 },
409 Categorical {
411 label: String,
413 },
414 NumericRange {
416 range: NumericRange,
418 label: Option<String>,
420 },
421 Composite,
423 Other {
425 value: OtherClaimDescriptor,
427 },
428}
429
430#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
432pub struct NumericRange {
433 lower: NumericBound,
435 upper: NumericBound,
437 unit: Option<String>,
439}
440
441impl NumericRange {
442 pub fn new(
450 lower: NumericBound,
451 upper: NumericBound,
452 unit: Option<String>,
453 ) -> Result<Self, PredictionError> {
454 let range = Self { lower, upper, unit };
455 range.validate()?;
456 Ok(range)
457 }
458
459 #[must_use]
461 pub const fn lower(&self) -> &NumericBound {
462 &self.lower
463 }
464
465 #[must_use]
467 pub const fn upper(&self) -> &NumericBound {
468 &self.upper
469 }
470
471 #[must_use]
473 pub fn unit(&self) -> Option<&str> {
474 self.unit.as_deref()
475 }
476
477 #[must_use]
479 pub fn into_parts(self) -> (NumericBound, NumericBound, Option<String>) {
480 (self.lower, self.upper, self.unit)
481 }
482
483 pub fn validate(&self) -> Result<(), PredictionError> {
491 let Some((lower, lower_included)) = self.lower.finite_value() else {
492 return Ok(());
493 };
494 let Some((upper, upper_included)) = self.upper.finite_value() else {
495 return Ok(());
496 };
497
498 if lower > upper {
499 return Err(PredictionError::InvalidNumericRange {
500 reason: "lower bound must be less than or equal to upper bound",
501 });
502 }
503
504 if lower == upper && !(lower_included && upper_included) {
505 return Err(PredictionError::InvalidNumericRange {
506 reason: "zero-width finite ranges must include both endpoints",
507 });
508 }
509
510 Ok(())
511 }
512}
513
514impl<'de> Deserialize<'de> for NumericRange {
515 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
516 where
517 D: Deserializer<'de>,
518 {
519 #[derive(Deserialize)]
520 #[serde(deny_unknown_fields)]
521 struct NumericRangeShadow {
522 lower: NumericBound,
523 upper: NumericBound,
524 unit: Option<String>,
525 }
526
527 let shadow = NumericRangeShadow::deserialize(deserializer)?;
528 Self::new(shadow.lower, shadow.upper, shadow.unit).map_err(de::Error::custom)
529 }
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
534#[serde(rename_all = "snake_case")]
535#[non_exhaustive]
536pub enum NumericBound {
537 Included(#[serde(with = "paft_decimal::serde::canonical_str")] Decimal),
539 Excluded(#[serde(with = "paft_decimal::serde::canonical_str")] Decimal),
541 Unbounded,
543}
544
545impl NumericBound {
546 const fn finite_value(&self) -> Option<(&Decimal, bool)> {
547 match self {
548 Self::Included(value) => Some((value, true)),
549 Self::Excluded(value) => Some((value, false)),
550 Self::Unbounded => None,
551 }
552 }
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
557#[serde(bound(deserialize = "E: Default + Deserialize<'de>, M: Default + Deserialize<'de>"))]
558pub struct GenericPredictionEvent<E = (), M = ()> {
559 pub key: PredictionEventKey,
561 pub title: String,
563 pub subtitle: Option<String>,
565 pub category: Option<String>,
567 pub series_key: Option<PredictionSeriesKey>,
569 pub structure: EventStructure,
575 pub markets: Vec<GenericPredictionMarket<M>>,
577 #[serde(flatten, default = "Default::default")]
579 pub provider: E,
580}
581
582impl<E: Default, M> GenericPredictionEvent<E, M> {
583 #[must_use]
589 pub fn new(key: PredictionEventKey, title: String, structure: EventStructure) -> Self {
590 Self {
591 key,
592 title,
593 subtitle: None,
594 category: None,
595 series_key: None,
596 structure,
597 markets: Vec::new(),
598 provider: E::default(),
599 }
600 }
601}
602
603impl<E, M> GenericPredictionEvent<E, M> {
604 pub fn validate_structure(&self) -> Result<(), PredictionError> {
615 validate_event_structure(&self.structure, self.markets.len())
616 }
617}
618
619fn validate_event_structure(
620 structure: &EventStructure,
621 market_count: usize,
622) -> Result<(), PredictionError> {
623 let invalid = |structure, reason| PredictionError::InvalidEventStructure {
624 structure,
625 market_count,
626 reason,
627 };
628
629 match structure {
630 EventStructure::SingleMarket if market_count != 1 => Err(invalid(
631 "single_market",
632 "expected exactly one contained market",
633 )),
634 EventStructure::MutuallyExclusive { .. } if market_count < 2 => Err(invalid(
635 "mutually_exclusive",
636 "expected at least two contained markets",
637 )),
638 EventStructure::OrderedBuckets { .. } if market_count < 2 => Err(invalid(
639 "ordered_buckets",
640 "expected at least two contained markets",
641 )),
642 EventStructure::LinkedBinaryClaims { .. } if market_count < 2 => Err(invalid(
643 "linked_binary_claims",
644 "expected at least two contained markets",
645 )),
646 EventStructure::SingleMarket
647 | EventStructure::IndependentClaims
648 | EventStructure::MutuallyExclusive { .. }
649 | EventStructure::OrderedBuckets { .. }
650 | EventStructure::LinkedBinaryClaims { .. }
651 | EventStructure::Composite
652 | EventStructure::Other { .. } => Ok(()),
653 }
654}
655
656pub type PredictionEvent = GenericPredictionEvent<(), ()>;
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
661#[serde(tag = "kind", content = "market", rename_all = "snake_case")]
662#[serde(bound(deserialize = "M: Default + Deserialize<'de>"))]
663#[non_exhaustive]
664#[expect(
667 clippy::large_enum_variant,
668 reason = "market metadata variants stay inline for direct construction and pattern matching"
669)]
670pub enum GenericPredictionMarket<M = ()> {
671 Binary(GenericBinaryMarket<M>),
673 MultiOutcome(GenericMultiOutcomeMarket<M>),
675 Scalar(GenericScalarMarket<M>),
677}
678
679pub type PredictionMarket = GenericPredictionMarket<()>;
681
682#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
684pub struct GenericBinaryMarket<M = ()> {
685 key: BinaryMarketKey,
687 outcomes: BinaryOutcomeInstruments,
689 pub event_key: Option<PredictionEventKey>,
691 pub title: String,
693 pub yes_label: Option<String>,
695 pub no_label: Option<String>,
697 pub claim: ClaimDescriptor,
699 pub status: PredictionMarketStatus,
701 pub collateral_currency: Currency,
703 pub winning_payout: NonZeroOutcomePayout,
705 pub price_grid: Option<PriceGrid>,
707 pub min_order_quantity: Option<NonZeroContractQuantity>,
709 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
711 pub open_time: Option<DateTime<Utc>>,
712 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
714 pub close_time: Option<DateTime<Utc>>,
715 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
717 pub settlement_time: Option<DateTime<Utc>>,
718 pub settlement: Option<BinarySettlement>,
720 #[serde(flatten, default = "Default::default")]
722 pub provider: M,
723}
724
725impl<M: Default> GenericBinaryMarket<M> {
726 pub fn new(
733 key: BinaryMarketKey,
734 outcomes: BinaryOutcomeInstruments,
735 title: String,
736 claim: ClaimDescriptor,
737 status: PredictionMarketStatus,
738 collateral_currency: Currency,
739 winning_payout: NonZeroOutcomePayout,
740 ) -> Result<Self, PredictionError> {
741 validate_binary_market_outcomes(&key, &outcomes)?;
742
743 Ok(Self {
744 key,
745 outcomes,
746 event_key: None,
747 title,
748 yes_label: None,
749 no_label: None,
750 claim,
751 status,
752 collateral_currency,
753 winning_payout,
754 price_grid: None,
755 min_order_quantity: None,
756 open_time: None,
757 close_time: None,
758 settlement_time: None,
759 settlement: None,
760 provider: M::default(),
761 })
762 }
763}
764
765impl<'de, M> Deserialize<'de> for GenericBinaryMarket<M>
766where
767 M: Default + Deserialize<'de>,
768{
769 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
770 where
771 D: Deserializer<'de>,
772 {
773 #[derive(Deserialize)]
774 struct GenericBinaryMarketShadow<M> {
775 key: BinaryMarketKey,
776 outcomes: BinaryOutcomeInstruments,
777 event_key: Option<PredictionEventKey>,
778 title: String,
779 yes_label: Option<String>,
780 no_label: Option<String>,
781 claim: ClaimDescriptor,
782 status: PredictionMarketStatus,
783 collateral_currency: Currency,
784 winning_payout: NonZeroOutcomePayout,
785 price_grid: Option<PriceGrid>,
786 min_order_quantity: Option<NonZeroContractQuantity>,
787 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
788 open_time: Option<DateTime<Utc>>,
789 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
790 close_time: Option<DateTime<Utc>>,
791 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
792 settlement_time: Option<DateTime<Utc>>,
793 settlement: Option<BinarySettlement>,
794 #[serde(flatten, default = "Default::default")]
795 provider: M,
796 }
797
798 let shadow = GenericBinaryMarketShadow::deserialize(deserializer)?;
799 validate_binary_market_outcomes(&shadow.key, &shadow.outcomes)
800 .map_err(de::Error::custom)?;
801
802 Ok(Self {
803 key: shadow.key,
804 outcomes: shadow.outcomes,
805 event_key: shadow.event_key,
806 title: shadow.title,
807 yes_label: shadow.yes_label,
808 no_label: shadow.no_label,
809 claim: shadow.claim,
810 status: shadow.status,
811 collateral_currency: shadow.collateral_currency,
812 winning_payout: shadow.winning_payout,
813 price_grid: shadow.price_grid,
814 min_order_quantity: shadow.min_order_quantity,
815 open_time: shadow.open_time,
816 close_time: shadow.close_time,
817 settlement_time: shadow.settlement_time,
818 settlement: shadow.settlement,
819 provider: shadow.provider,
820 })
821 }
822}
823
824impl<M> GenericBinaryMarket<M> {
825 #[must_use]
827 pub const fn key(&self) -> &BinaryMarketKey {
828 &self.key
829 }
830
831 #[must_use]
833 pub const fn outcomes(&self) -> &BinaryOutcomeInstruments {
834 &self.outcomes
835 }
836
837 #[must_use]
839 pub fn resolved_payouts(&self) -> Option<BinaryPayoutVector> {
840 self.settlement
841 .as_ref()?
842 .resolved_payouts(self.winning_payout)
843 }
844}
845
846fn validate_binary_market_outcomes(
847 key: &BinaryMarketKey,
848 outcomes: &BinaryOutcomeInstruments,
849) -> Result<(), PredictionError> {
850 let yes = outcomes.yes();
851 let no = outcomes.no();
852 if yes.venue == key.venue
853 && no.venue == key.venue
854 && yes.market_id == key.market_id
855 && no.market_id == key.market_id
856 {
857 return Ok(());
858 }
859
860 Err(PredictionError::MismatchedBinaryMarketOutcomes(Box::new(
861 BinaryMarketOutcomeMismatch {
862 key_venue: key.venue.to_string(),
863 key_market_id: key.market_id.to_string(),
864 yes_venue: yes.venue.to_string(),
865 yes_market_id: yes.market_id.to_string(),
866 no_venue: no.venue.to_string(),
867 no_market_id: no.market_id.to_string(),
868 },
869 )))
870}
871
872pub type BinaryMarket = GenericBinaryMarket<()>;
874
875#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
877pub struct GenericMultiOutcomeMarket<M = ()> {
878 key: PredictionMarketKey,
880 pub event_key: Option<PredictionEventKey>,
882 pub title: String,
884 outcomes: Vec<OutcomeDescriptor>,
886 pub status: PredictionMarketStatus,
888 pub collateral_currency: Currency,
890 pub unit_payout: OutcomePayout,
892 pub price_grid: Option<PriceGrid>,
894 pub min_order_quantity: Option<NonZeroContractQuantity>,
896 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
898 pub open_time: Option<DateTime<Utc>>,
899 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
901 pub close_time: Option<DateTime<Utc>>,
902 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
904 pub settlement_time: Option<DateTime<Utc>>,
905 resolution: Option<PredictionOutcomeId>,
907 #[serde(flatten, default = "Default::default")]
909 pub provider: M,
910}
911
912impl<M: Default> GenericMultiOutcomeMarket<M> {
913 pub fn new(
920 key: PredictionMarketKey,
921 title: String,
922 outcomes: Vec<OutcomeDescriptor>,
923 status: PredictionMarketStatus,
924 collateral_currency: Currency,
925 unit_payout: OutcomePayout,
926 ) -> Result<Self, PredictionError> {
927 validate_multi_outcome_market(&key, &outcomes, None)?;
928
929 Ok(Self {
930 key,
931 event_key: None,
932 title,
933 outcomes,
934 status,
935 collateral_currency,
936 unit_payout,
937 price_grid: None,
938 min_order_quantity: None,
939 open_time: None,
940 close_time: None,
941 settlement_time: None,
942 resolution: None,
943 provider: M::default(),
944 })
945 }
946}
947
948impl<M> GenericMultiOutcomeMarket<M> {
949 #[must_use]
951 pub const fn key(&self) -> &PredictionMarketKey {
952 &self.key
953 }
954
955 #[must_use]
957 pub fn outcomes(&self) -> &[OutcomeDescriptor] {
958 &self.outcomes
959 }
960
961 #[must_use]
963 pub const fn resolution(&self) -> Option<&PredictionOutcomeId> {
964 self.resolution.as_ref()
965 }
966
967 pub fn set_resolution(
974 &mut self,
975 resolution: Option<PredictionOutcomeId>,
976 ) -> Result<(), PredictionError> {
977 validate_multi_outcome_market_resolution(&self.key, &self.outcomes, resolution.as_ref())?;
978 self.resolution = resolution;
979 Ok(())
980 }
981}
982
983impl<'de, M> Deserialize<'de> for GenericMultiOutcomeMarket<M>
984where
985 M: Default + Deserialize<'de>,
986{
987 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
988 where
989 D: Deserializer<'de>,
990 {
991 #[derive(Deserialize)]
992 struct GenericMultiOutcomeMarketShadow<M> {
993 key: PredictionMarketKey,
994 event_key: Option<PredictionEventKey>,
995 title: String,
996 outcomes: Vec<OutcomeDescriptor>,
997 status: PredictionMarketStatus,
998 collateral_currency: Currency,
999 unit_payout: OutcomePayout,
1000 price_grid: Option<PriceGrid>,
1001 min_order_quantity: Option<NonZeroContractQuantity>,
1002 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
1003 open_time: Option<DateTime<Utc>>,
1004 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
1005 close_time: Option<DateTime<Utc>>,
1006 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
1007 settlement_time: Option<DateTime<Utc>>,
1008 resolution: Option<PredictionOutcomeId>,
1009 #[serde(flatten, default = "Default::default")]
1010 provider: M,
1011 }
1012
1013 let shadow = GenericMultiOutcomeMarketShadow::deserialize(deserializer)?;
1014 validate_multi_outcome_market(&shadow.key, &shadow.outcomes, shadow.resolution.as_ref())
1015 .map_err(de::Error::custom)?;
1016
1017 Ok(Self {
1018 key: shadow.key,
1019 event_key: shadow.event_key,
1020 title: shadow.title,
1021 outcomes: shadow.outcomes,
1022 status: shadow.status,
1023 collateral_currency: shadow.collateral_currency,
1024 unit_payout: shadow.unit_payout,
1025 price_grid: shadow.price_grid,
1026 min_order_quantity: shadow.min_order_quantity,
1027 open_time: shadow.open_time,
1028 close_time: shadow.close_time,
1029 settlement_time: shadow.settlement_time,
1030 resolution: shadow.resolution,
1031 provider: shadow.provider,
1032 })
1033 }
1034}
1035
1036fn validate_multi_outcome_market(
1037 key: &PredictionMarketKey,
1038 outcomes: &[OutcomeDescriptor],
1039 resolution: Option<&PredictionOutcomeId>,
1040) -> Result<(), PredictionError> {
1041 if outcomes.len() < 2 {
1042 return Err(PredictionError::TooFewMultiOutcomeMarketOutcomes {
1043 count: outcomes.len(),
1044 });
1045 }
1046
1047 let mut outcome_ids = HashSet::with_capacity(outcomes.len());
1048 for outcome in outcomes {
1049 let instrument = &outcome.instrument;
1050 if instrument.venue != key.venue || instrument.market_id != key.market_id {
1051 return Err(PredictionError::MismatchedMultiOutcomeMarketOutcome(
1052 Box::new(MultiOutcomeMarketOutcomeMismatch {
1053 key_venue: key.venue.to_string(),
1054 key_market_id: key.market_id.to_string(),
1055 outcome_venue: instrument.venue.to_string(),
1056 outcome_market_id: instrument.market_id.to_string(),
1057 outcome_id: instrument.outcome_id.to_string(),
1058 }),
1059 ));
1060 }
1061
1062 if !outcome_ids.insert(&instrument.outcome_id) {
1063 return Err(PredictionError::DuplicateMultiOutcomeMarketOutcome {
1064 venue: instrument.venue.to_string(),
1065 market_id: instrument.market_id.to_string(),
1066 outcome_id: instrument.outcome_id.to_string(),
1067 });
1068 }
1069 }
1070
1071 validate_multi_outcome_market_resolution(key, outcomes, resolution)
1072}
1073
1074fn validate_multi_outcome_market_resolution(
1075 key: &PredictionMarketKey,
1076 outcomes: &[OutcomeDescriptor],
1077 resolution: Option<&PredictionOutcomeId>,
1078) -> Result<(), PredictionError> {
1079 let Some(resolution) = resolution else {
1080 return Ok(());
1081 };
1082
1083 if outcomes
1084 .iter()
1085 .any(|outcome| outcome.instrument.outcome_id == *resolution)
1086 {
1087 return Ok(());
1088 }
1089
1090 Err(PredictionError::InvalidMultiOutcomeMarketResolution {
1091 venue: key.venue.to_string(),
1092 market_id: key.market_id.to_string(),
1093 outcome_id: resolution.to_string(),
1094 })
1095}
1096
1097pub type MultiOutcomeMarket = GenericMultiOutcomeMarket<()>;
1099
1100#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1102pub struct OutcomeDescriptor {
1103 pub instrument: OutcomeInstrument,
1105 pub label: String,
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1111pub struct GenericScalarMarket<M = ()> {
1112 pub key: PredictionMarketKey,
1114 pub event_key: Option<PredictionEventKey>,
1116 pub title: String,
1118 pub unit: Option<String>,
1120 pub status: PredictionMarketStatus,
1122 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
1124 pub open_time: Option<DateTime<Utc>>,
1125 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
1127 pub close_time: Option<DateTime<Utc>>,
1128 #[serde(default, with = "chrono::serde::ts_milliseconds_option")]
1130 pub settlement_time: Option<DateTime<Utc>>,
1131 #[serde(default, with = "paft_decimal::serde::option_canonical_str")]
1133 pub resolution_value: Option<Decimal>,
1134 #[serde(flatten, default = "Default::default")]
1136 pub provider: M,
1137}
1138
1139pub type ScalarMarket = GenericScalarMarket<()>;