Skip to content

Commit

Permalink
ScoreFitter work
Browse files Browse the repository at this point in the history
  • Loading branch information
ekoutanov committed Dec 18, 2023
1 parent 1ab5e13 commit aaf6a40
Show file tree
Hide file tree
Showing 13 changed files with 1,576 additions and 1,168 deletions.
1,078 changes: 35 additions & 1,043 deletions brumby-soccer/src/bin/soc_prices.rs

Large diffs are not rendered by default.

1,132 changes: 1,132 additions & 0 deletions brumby-soccer/src/bin/soc_prices2.rs

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions brumby-soccer/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use racing_scraper::sports::{get_sports_contest, Provider};
use rustc_hash::FxHashMap;
use thiserror::Error;
use brumby::feed_id::FeedId;

#[derive(Debug)]
pub struct ContestSummary {
pub id: String,
pub name: String,
pub offerings: HashMap<OfferType, HashMap<OutcomeType, f64>>,
pub offerings: FxHashMap<OfferType, HashMap<OutcomeType, f64>>,
}

impl From<ContestModel> for ContestSummary {
fn from(external: ContestModel) -> Self {
let id = external.id;
let name = external.name;
let mut offerings = HashMap::with_capacity(external.markets.len());
let mut offerings = FxHashMap::with_capacity_and_hasher(external.markets.len(), Default::default());
for market in external.markets {
match market {
SoccerMarket::CorrectScore(markets) => {
Expand Down
26 changes: 26 additions & 0 deletions brumby-soccer/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ pub enum OfferType {
PlayerShotsOnTarget(Over),
AnytimeAssist
}
impl OfferType {
pub fn category(&self) -> OfferCategory {
match self {
OfferType::HeadToHead(_) => OfferCategory::HeadToHead,
OfferType::TotalGoals(_, _) => OfferCategory::TotalGoals,
OfferType::CorrectScore(_) => OfferCategory::CorrectScore,
OfferType::DrawNoBet => OfferCategory::DrawNoBet,
OfferType::AnytimeGoalscorer => OfferCategory::AnytimeGoalscorer,
OfferType::FirstGoalscorer => OfferCategory::FirstGoalscorer,
OfferType::PlayerShotsOnTarget(_) => OfferCategory::PlayerShotsOnTarget,
OfferType::AnytimeAssist => OfferCategory::AnytimeAssist
}
}
}

#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum OfferCategory {
HeadToHead,
TotalGoals,
CorrectScore,
DrawNoBet,
AnytimeGoalscorer,
FirstGoalscorer,
PlayerShotsOnTarget,
AnytimeAssist
}

#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum Side {
Expand Down
90 changes: 60 additions & 30 deletions brumby-soccer/src/domain/error.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
use crate::domain::{Offer, OfferType, OutcomeType};
use crate::domain::{Offer, OfferType, OutcomeType, Player};
use brumby::hash_lookup::HashLookup;
use brumby::probs::SliceExt;
use std::fmt::{Display, Formatter};
use std::ops::RangeInclusive;
use rustc_hash::FxHashMap;
use thiserror::Error;
use crate::interval::PlayerProbs;

mod head_to_head;
mod total_goals;

#[derive(Debug, Error)]
pub enum InvalidOffer {
#[error("misaligned offer: {0}")]
#[error("{0}")]
MisalignedOffer(#[from] MisalignedOffer),

#[error("{0}")]
MissingOutcome(#[from] MissingOutcome),
InvalidOutcome(#[from] InvalidOutcome),

#[error("{0}")]
ExtraneousOutcome(#[from] ExtraneousOutcome),

#[error("wrong booksum: {0}")]
WrongBooksum(#[from] WrongBooksum),
}

Expand All @@ -30,16 +29,47 @@ impl Offer {
&self.market.probs,
&self.offer_type,
)?;
self.offer_type.validate(&self.outcomes)?;
match self.offer_type {
OfferType::TotalGoals(_, _) => total_goals::validate(self),
OfferType::HeadToHead(_) => head_to_head::validate(self),
OfferType::TotalGoals(_, _) => total_goals::validate_probs(&self.offer_type, &self.market.probs),
OfferType::HeadToHead(_) => head_to_head::validate_probs(&self.offer_type, &self.market.probs),
_ => Ok(()),
}
}
}


#[derive(Debug, Error)]
pub enum InvalidOutcome {
#[error("{0}")]
MissingOutcome(#[from] MissingOutcome),

#[error("{0}")]
ExtraneousOutcome(#[from] ExtraneousOutcome),
}

impl OfferType {
pub fn validate(&self, outcomes: &HashLookup<OutcomeType>) -> Result<(), InvalidOutcome> {
match self {
OfferType::TotalGoals(_, _) => total_goals::validate_outcomes(self, outcomes),
OfferType::HeadToHead(_) => head_to_head::validate_outcomes(self, outcomes),
_ => Ok(()),
}
}

pub fn create_outcomes(&self, player_probs: &FxHashMap<Player, PlayerProbs>) {
todo!()
}
}

#[derive(Debug, Error)]
#[error("expected {assertion}, got {actual} for {offer_type:?}")]
pub enum FitError {
#[error("missing offer {0:?}")]
MissingOffer(OfferType),
}

#[derive(Debug, Error)]
#[error("expected booksum in {assertion}, got {actual} for {offer_type:?}")]
pub struct WrongBooksum {
pub assertion: BooksumAssertion,
pub actual: f64,
Expand Down Expand Up @@ -166,23 +196,23 @@ impl<F: FnMut(&OutcomeType) -> bool> OutcomesMatchAssertion<F> {
}
}

#[derive(Debug, Error)]
pub enum IncompleteOutcomes {
#[error("{0}")]
MissingOutcome(#[from] MissingOutcome),

#[error("{0}")]
ExtraneousOutcome(#[from] ExtraneousOutcome),
}

impl From<IncompleteOutcomes> for InvalidOffer {
fn from(value: IncompleteOutcomes) -> Self {
match value {
IncompleteOutcomes::MissingOutcome(nested) => nested.into(),
IncompleteOutcomes::ExtraneousOutcome(nested) => nested.into(),
}
}
}
// #[derive(Debug, Error)]
// pub enum IncompleteOutcomes {
// #[error("{0}")]
// MissingOutcome(#[from] MissingOutcome),
//
// #[error("{0}")]
// ExtraneousOutcome(#[from] ExtraneousOutcome),
// }
//
// impl From<IncompleteOutcomes> for InvalidOutcome {
// fn from(value: IncompleteOutcomes) -> Self {
// match value {
// IncompleteOutcomes::MissingOutcome(nested) => nested.into(),
// IncompleteOutcomes::ExtraneousOutcome(nested) => nested.into(),
// }
// }
// }

#[derive(Debug)]
pub struct OutcomesCompleteAssertion<'a> {
Expand All @@ -193,7 +223,7 @@ impl<'a> OutcomesCompleteAssertion<'a> {
&self,
outcomes: &HashLookup<OutcomeType>,
offer_type: &OfferType,
) -> Result<(), IncompleteOutcomes> {
) -> Result<(), InvalidOutcome> {
OutcomesIntactAssertion {
outcomes: self.outcomes,
}
Expand Down Expand Up @@ -228,7 +258,7 @@ mod tests {
market: Market::frame(&Overround::fair(), vec![0.4], &PRICE_BOUNDS),
};
assert_eq!(
"misaligned offer: 2:1 outcomes:probabilities mapped for TotalGoals(FullTime, Over(2))",
"2:1 outcomes:probabilities mapped for TotalGoals(FullTime, Over(2))",
offer.validate().unwrap_err().to_string()
);
}
Expand Down Expand Up @@ -258,7 +288,7 @@ mod tests {
.check(&[1.0 - 0.011], &OfferType::FirstGoalscorer)
.unwrap_err();
assert_eq!(
"expected 1.0..=2.0 ± 0.01, got 0.989 for FirstGoalscorer",
"expected booksum in 1.0..=2.0 ± 0.01, got 0.989 for FirstGoalscorer",
err.to_string()
);
}
Expand All @@ -267,7 +297,7 @@ mod tests {
.check(&[2.0 + 0.011], &OfferType::FirstGoalscorer)
.unwrap_err();
assert_eq!(
"expected 1.0..=2.0 ± 0.01, got 2.011 for FirstGoalscorer",
"expected booksum in 1.0..=2.0 ± 0.01, got 2.011 for FirstGoalscorer",
err.to_string()
);
}
Expand Down
86 changes: 68 additions & 18 deletions brumby-soccer/src/domain/error/head_to_head.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
use crate::domain::{error, Offer, OfferType, OutcomeType, Side};
use crate::domain::error::InvalidOffer;
use brumby::hash_lookup::HashLookup;

pub fn validate(offer: &Offer) -> Result<(), InvalidOffer> {
match &offer.offer_type {
use crate::domain::{error, OfferType, OutcomeType, Side};
use crate::domain::error::{InvalidOffer, InvalidOutcome};

pub fn validate_outcomes(
offer_type: &OfferType,
outcomes: &HashLookup<OutcomeType>,
) -> Result<(), InvalidOutcome> {
match offer_type {
OfferType::HeadToHead(_) => {
error::BooksumAssertion::with_default_tolerance(1.0..=1.0)
.check(&offer.market.probs, &offer.offer_type)?;
error::OutcomesCompleteAssertion {
outcomes: &[OutcomeType::Win(Side::Home), OutcomeType::Win(Side::Away), OutcomeType::Draw],
outcomes: &create_outcomes(),
}
.check(&offer.outcomes, &offer.offer_type)?;
.check(outcomes, offer_type)?;
Ok(())
}
_ => panic!("{:?} unsupported", offer.offer_type),
_ => unreachable!(),
}
}

pub fn validate_probs(offer_type: &OfferType, probs: &[f64]) -> Result<(), InvalidOffer> {
match offer_type {
OfferType::HeadToHead(_) => {
error::BooksumAssertion::with_default_tolerance(1.0..=1.0).check(probs, offer_type)?;
Ok(())
}
_ => unreachable!(),
}
}

pub fn create_outcomes() -> [OutcomeType; 3] {
[
OutcomeType::Win(Side::Home),
OutcomeType::Win(Side::Away),
OutcomeType::Draw,
]
}

#[cfg(test)]
mod tests {
use std::ops::RangeInclusive;

use brumby::hash_lookup::HashLookup;
use brumby::market::{Market, Overround};

use crate::domain::{Over, Period};
use crate::domain::{Offer, Period};

use super::*;

Expand All @@ -34,7 +55,11 @@ mod tests {
fn valid() {
let offer = Offer {
offer_type: OFFER_TYPE,
outcomes: HashLookup::from(vec![OutcomeType::Win(Side::Home), OutcomeType::Win(Side::Away), OutcomeType::Draw]),
outcomes: HashLookup::from(vec![
OutcomeType::Win(Side::Home),
OutcomeType::Win(Side::Away),
OutcomeType::Draw,
]),
market: Market::frame(&Overround::fair(), vec![0.4, 0.4, 0.2], &PRICE_BOUNDS),
};
offer.validate().unwrap();
Expand All @@ -44,29 +69,54 @@ mod tests {
fn wrong_booksum() {
let offer = Offer {
offer_type: OFFER_TYPE,
outcomes: HashLookup::from(vec![OutcomeType::Win(Side::Home), OutcomeType::Win(Side::Away), OutcomeType::Draw]),
outcomes: HashLookup::from(vec![
OutcomeType::Win(Side::Home),
OutcomeType::Win(Side::Away),
OutcomeType::Draw,
]),
market: Market::frame(&Overround::fair(), vec![0.4, 0.4, 0.1], &PRICE_BOUNDS),
};
assert_eq!("wrong booksum: expected 1.0..=1.0 ± 0.000001, got 0.9 for HeadToHead(FullTime)", offer.validate().unwrap_err().to_string());
assert_eq!(
"expected booksum in 1.0..=1.0 ± 0.000001, got 0.9 for HeadToHead(FullTime)",
offer.validate().unwrap_err().to_string()
);
}

#[test]
fn missing_outcome() {
let offer = Offer {
offer_type: OFFER_TYPE,
outcomes: HashLookup::from(vec![OutcomeType::Win(Side::Home), OutcomeType::Win(Side::Away)]),
outcomes: HashLookup::from(vec![
OutcomeType::Win(Side::Home),
OutcomeType::Win(Side::Away),
]),
market: Market::frame(&Overround::fair(), vec![0.4, 0.6], &PRICE_BOUNDS),
};
assert_eq!("Draw missing from HeadToHead(FullTime)", offer.validate().unwrap_err().to_string());
assert_eq!(
"Draw missing from HeadToHead(FullTime)",
offer.validate().unwrap_err().to_string()
);
}

#[test]
fn extraneous_outcome() {
let offer = Offer {
offer_type: OFFER_TYPE,
outcomes: HashLookup::from(vec![OutcomeType::Win(Side::Home), OutcomeType::Win(Side::Away), OutcomeType::Draw, OutcomeType::None]),
market: Market::frame(&Overround::fair(), vec![0.4, 0.5, 0.05, 0.05], &PRICE_BOUNDS),
outcomes: HashLookup::from(vec![
OutcomeType::Win(Side::Home),
OutcomeType::Win(Side::Away),
OutcomeType::Draw,
OutcomeType::None,
]),
market: Market::frame(
&Overround::fair(),
vec![0.4, 0.5, 0.05, 0.05],
&PRICE_BOUNDS,
),
};
assert_eq!("None does not belong in HeadToHead(FullTime)", offer.validate().unwrap_err().to_string());
assert_eq!(
"None does not belong in HeadToHead(FullTime)",
offer.validate().unwrap_err().to_string()
);
}
}
Loading

0 comments on commit aaf6a40

Please sign in to comment.