From 9685a9177f6d8ed9d30ce1973b3269dd2b85068a Mon Sep 17 00:00:00 2001 From: Emil Koutanov Date: Mon, 4 Dec 2023 11:45:04 +1100 Subject: [PATCH] Integrated with soccer scaper data --- Cargo.toml | 2 +- src/bin/datadump.rs | 32 ++++---- src/bin/departure.rs | 8 +- src/bin/evaluate.rs | 12 +-- src/bin/fractional.rs | 6 +- src/bin/prices.rs | 4 +- src/bin/soccer.rs | 126 +++++++++++++++++++++++++------ src/interval.rs | 130 ++++++++++++++++---------------- src/lib.rs | 3 +- src/{data.rs => racing_data.rs} | 69 ++++++++--------- src/scoregrid.rs | 6 +- src/soccer_data.rs | 125 ++++++++++++++++++++++++++++++ 12 files changed, 368 insertions(+), 155 deletions(-) rename src/{data.rs => racing_data.rs} (88%) create mode 100644 src/soccer_data.rs diff --git a/Cargo.toml b/Cargo.toml index 3ca74da..cef2f4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ exclude = ["/images", "/bin", "/.idea", "/.github", "/coverage", "/doc", "/examp anyhow = "1.0.75" chrono = "0.4.31" clap = { version = "4.4.6", features = ["derive"] } -racing_scraper = "0.0.8" +racing_scraper = "0.0.10" serde_json = "1.0.107" stanza = "0.3.0" tinyrand = "0.5.0" diff --git a/src/bin/datadump.rs b/src/bin/datadump.rs index 410d602..3180e14 100644 --- a/src/bin/datadump.rs +++ b/src/bin/datadump.rs @@ -1,22 +1,24 @@ -use anyhow::anyhow; -use brumby::csv::{CsvWriter, Record}; -use brumby::{data, model}; -use brumby::data::{EventDetailExt, PredicateClosures}; -use brumby::market::{Market, OverroundMethod}; -use brumby::model::cf::Factor; -use brumby::model::fit; -use brumby::model::fit::FitOptions; -use brumby::probs::SliceExt; -use clap::Parser; -use racing_scraper::models::EventType; use std::collections::HashSet; use std::env; use std::error::Error; use std::path::PathBuf; use std::time::Instant; + +use anyhow::anyhow; +use clap::Parser; +use racing_scraper::models::EventType; use strum::{EnumCount, IntoEnumIterator}; use tracing::{debug, info}; +use brumby::{model, racing_data}; +use brumby::csv::{CsvWriter, Record}; +use brumby::market::{Market, OverroundMethod}; +use brumby::model::cf::Factor; +use brumby::model::fit; +use brumby::model::fit::FitOptions; +use brumby::probs::SliceExt; +use brumby::racing_data::{PredicateClosures, RaceSummary}; + const OVERROUND_METHOD: OverroundMethod = OverroundMethod::Multiplicative; #[derive(Debug, clap::Parser, Clone)] @@ -73,12 +75,12 @@ fn main() -> Result<(), Box> { let mut predicates = vec![]; if let Some(race_type) = args.race_type { - predicates.push(data::Predicate::Type { race_type }); + predicates.push(racing_data::Predicate::Type { race_type }); } if let Some(cutoff_worst) = args.departure { - predicates.push(data::Predicate::Departure { cutoff_worst }) + predicates.push(racing_data::Predicate::Departure { cutoff_worst }) } - let race_files = data::read_from_dir(args.dir.unwrap(), PredicateClosures::from(predicates))?; + let race_files = racing_data::read_from_dir(args.dir.unwrap(), PredicateClosures::from(predicates))?; let total_num_races = race_files.len(); let mut unique_races = HashSet::new(); let mut duplicate_races = 0; @@ -94,7 +96,7 @@ fn main() -> Result<(), Box> { race_file.file.to_str().unwrap(), index + 1, ); - let race = race_file.race.summarise(); + let race = RaceSummary::from(race_file.race); let markets: Vec<_> = (0..race.prices.rows()) .map(|rank| { let prices = race.prices.row_slice(rank).to_vec(); diff --git a/src/bin/departure.rs b/src/bin/departure.rs index 09c9a7f..ab2fb2c 100644 --- a/src/bin/departure.rs +++ b/src/bin/departure.rs @@ -12,8 +12,8 @@ use stanza::style::{HAlign, Header, MinWidth, Styles}; use stanza::table::{Cell, Col, Row, Table}; use tracing::{debug, info}; -use brumby::data; -use brumby::data::{EventDetailExt, PlacePriceDeparture, PredicateClosures}; +use brumby::racing_data; +use brumby::racing_data::{EventDetailExt, PlacePriceDeparture, PredicateClosures}; const TOP_SUBSET: usize = 25; @@ -58,9 +58,9 @@ fn main() -> Result<(), Box> { let start_time = Instant::now(); let mut predicates = vec![]; if let Some(race_type) = args.race_type { - predicates.push(data::Predicate::Type { race_type }); + predicates.push(racing_data::Predicate::Type { race_type }); } - let races = data::read_from_dir(args.dir.unwrap(), PredicateClosures::from(predicates))?; + let races = racing_data::read_from_dir(args.dir.unwrap(), PredicateClosures::from(predicates))?; let mut assessments = Vec::with_capacity(races.len()); let num_races = races.len(); diff --git a/src/bin/evaluate.rs b/src/bin/evaluate.rs index ff67cd1..c3e4b97 100644 --- a/src/bin/evaluate.rs +++ b/src/bin/evaluate.rs @@ -13,8 +13,8 @@ use stanza::style::{HAlign, Header, MinWidth, Styles}; use stanza::table::{Cell, Col, Row, Table}; use tracing::{debug, info}; -use brumby::data; -use brumby::data::{EventDetailExt, PlacePriceDeparture, PredicateClosures, RaceSummary}; +use brumby::racing_data; +use brumby::racing_data::{EventDetailExt, PlacePriceDeparture, PredicateClosures, RaceSummary}; use brumby::file::ReadJsonFile; use brumby::market::{Market, OverroundMethod}; use brumby::model::cf::Coefficients; @@ -68,12 +68,12 @@ fn main() -> Result<(), Box> { let start_time = Instant::now(); let mut predicates = vec![]; if let Some(race_type) = args.race_type { - predicates.push(data::Predicate::Type { race_type }); + predicates.push(racing_data::Predicate::Type { race_type }); } if let Some(cutoff_worst) = args.departure { - predicates.push(data::Predicate::Departure { cutoff_worst }) + predicates.push(racing_data::Predicate::Departure { cutoff_worst }) } - let races = data::read_from_dir(args.dir.unwrap(), PredicateClosures::from(predicates))?; + let races = racing_data::read_from_dir(args.dir.unwrap(), PredicateClosures::from(predicates))?; let mut configs = HashMap::new(); for race_type in [EventType::Thoroughbred, EventType::Greyhound] { @@ -107,7 +107,7 @@ fn main() -> Result<(), Box> { index + 1 ); let departure = race_file.race.place_price_departure(); - let race = race_file.race.summarise(); + let race = RaceSummary::from(race_file.race); let calibrator = Fitter::try_from(configs[&race.race_type].clone())?; let sample_top_n = TopN { markets: (0..race.prices.rows()) diff --git a/src/bin/fractional.rs b/src/bin/fractional.rs index 3991877..96f6be1 100644 --- a/src/bin/fractional.rs +++ b/src/bin/fractional.rs @@ -11,7 +11,7 @@ use stanza::style::{HAlign, MinWidth, Separator, Styles}; use stanza::table::{Col, Row, Table}; use tracing::{debug, info}; -use brumby::data::{download_by_id, EventDetailExt, RaceSummary}; +use brumby::racing_data::{download_by_id, RaceSummary}; use brumby::file::ReadJsonFile; use brumby::market::{Market, Overround, OverroundMethod}; use brumby::model; @@ -177,11 +177,11 @@ async fn main() -> Result<(), Box> { async fn read_race_data(args: &Args) -> anyhow::Result { if let Some(path) = args.file.as_ref() { let event_detail = EventDetail::read_json_file(path)?; - return Ok(event_detail.summarise()); + return Ok(event_detail.into()); } if let Some(&id) = args.download.as_ref() { let event_detail = download_by_id(id).await?; - return Ok(event_detail.summarise()); + return Ok(event_detail.into()); } unreachable!() } diff --git a/src/bin/prices.rs b/src/bin/prices.rs index cd441dc..dd667b8 100644 --- a/src/bin/prices.rs +++ b/src/bin/prices.rs @@ -11,7 +11,7 @@ use stanza::style::{HAlign, MinWidth, Separator, Styles}; use stanza::table::{Col, Row, Table}; use tracing::{debug, info}; -use brumby::data::{download_by_id, EventDetailExt, RaceSummary}; +use brumby::racing_data::{download_by_id, RaceSummary}; use brumby::display::DisplaySlice; use brumby::file::ReadJsonFile; use brumby::market::{Market, Overround, OverroundMethod}; @@ -243,5 +243,5 @@ async fn read_race_data(args: &Args) -> anyhow::Result { unreachable!() } }; - Ok(event_detail.summarise()) + Ok(event_detail.into()) } diff --git a/src/bin/soccer.rs b/src/bin/soccer.rs index b385121..e58564d 100644 --- a/src/bin/soccer.rs +++ b/src/bin/soccer.rs @@ -1,11 +1,17 @@ use std::collections::{BTreeMap, HashMap}; +use std::env; +use std::error::Error; +use std::path::PathBuf; use std::time::Instant; +use anyhow::bail; +use clap::Parser; use HAlign::Left; use stanza::renderer::console::Console; use stanza::renderer::Renderer; use stanza::style::{HAlign, MinWidth, Styles}; use stanza::table::{Col, Row, Table}; +use tracing::{debug, info}; use brumby::entity::{MarketType, OutcomeType, Over, Player, Score, Side}; use brumby::entity::Player::Named; use brumby::interval::{explore, IntervalConfig, isolate}; @@ -15,15 +21,38 @@ use brumby::market::{Market, Overround, OverroundMethod, PriceBounds}; use brumby::opt::{hypergrid_search, HypergridSearchConfig, HypergridSearchOutcome}; use brumby::probs::SliceExt; use brumby::scoregrid; +use brumby::soccer_data::{ContestSummary, download_by_id}; const OVERROUND_METHOD: OverroundMethod = OverroundMethod::OddsRatio; -const SINGLE_PRICE_BOUNDS: PriceBounds = 1.04..=200.0; -const ZERO_INFLATION: f64 = 0.015; -const INTERVALS: usize = 6; +const SINGLE_PRICE_BOUNDS: PriceBounds = 1.04..=301.0; +const ZERO_INFLATION: f64 = 0.0; +const INTERVALS: usize = 12; const MAX_TOTAL_GOALS: u16 = 8; +const ERROR_TYPE: ErrorType = ErrorType::SquaredAbsolute; type Odds = HashMap; +#[derive(Debug, clap::Parser, Clone)] +struct Args { + /// file to source the contest data from + #[clap(short = 'f', long)] + file: Option, + + /// download contest data by ID + #[clap(short = 'd', long)] + download: Option, +} +impl Args { + fn validate(&self) -> anyhow::Result<()> { + if self.file.is_none() && self.download.is_none() + || self.file.is_some() && self.download.is_some() + { + bail!("either the -f or the -d flag must be specified"); + } + Ok(()) + } +} + fn verona_vs_leece() -> HashMap { let h2h = HashMap::from([ (OutcomeType::Win(Side::Home), 2.7), @@ -239,13 +268,28 @@ fn atlanta_vs_sporting_lisbon() -> HashMap { ]) } -pub fn main() { - let ext_markets = atlanta_vs_sporting_lisbon(); - let correct_score_prices = ext_markets[&MarketType::CorrectScore].clone(); - let h2h_prices = ext_markets[&MarketType::HeadToHead].clone(); - let goals_ou_prices = ext_markets[&MarketType::TotalGoalsOverUnder(Over(2))].clone(); - let first_gs = ext_markets[&MarketType::FirstGoalscorer].clone(); - let anytime_gs = ext_markets[&MarketType::AnytimeGoalscorer].clone(); +#[tokio::main] +async fn main() -> Result<(), Box> { + if env::var("RUST_BACKTRACE").is_err() { + env::set_var("RUST_BACKTRACE", "full") + } + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", "info") + } + tracing_subscriber::fmt::init(); + + let args = Args::parse(); + args.validate()?; + debug!("args: {args:?}"); + let contest = read_contest_data(&args).await?; + info!("contest.name: {}", contest.name); + + // let ext_markets = atlanta_vs_sporting_lisbon(); + let correct_score_prices = contest.offerings[&MarketType::CorrectScore].clone(); + let h2h_prices = contest.offerings[&MarketType::HeadToHead].clone(); + let goals_ou_prices = contest.offerings[&MarketType::TotalGoalsOverUnder(Over(2))].clone(); + let first_gs = contest.offerings[&MarketType::FirstGoalscorer].clone(); + let anytime_gs = contest.offerings[&MarketType::AnytimeGoalscorer].clone(); let h2h = fit_market(MarketType::HeadToHead, &h2h_prices); // println!("h2h: {h2h:?}"); @@ -261,6 +305,7 @@ pub fn main() { println!("*** fitting scoregrid ***"); let start = Instant::now(); let search_outcome = fit_scoregrid(&[&h2h, &goals_ou]); + // let search_outcome = fit_scoregrid(&[&correct_score]); let elapsed = start.elapsed(); println!("{elapsed:?} elapsed: search outcome: {search_outcome:?}"); @@ -316,8 +361,10 @@ pub fn main() { Console::default().render(&table_first_goalscorer) ); + let mut fitted_anytime_goalscorer_outcomes = vec![]; let mut fitted_anytime_goalscorer_probs = vec![]; for (player, prob) in &fitted_goalscorer_probs { + fitted_anytime_goalscorer_outcomes.push(OutcomeType::Player(player.clone())); let exploration = explore(&IntervalConfig { intervals: INTERVALS as u8, home_prob: search_outcome.optimal_values[0], @@ -330,6 +377,9 @@ pub fn main() { fitted_anytime_goalscorer_probs.push(isolated_prob); // println!("anytime scorer {player:?}, prob: {isolated_prob:.3}"); } + fitted_anytime_goalscorer_outcomes.push(OutcomeType::None); + fitted_anytime_goalscorer_probs.push(scoregrid[(0, 0)]); + // println!("anytime scorer {:?}, prob: {:.3}", OutcomeType::None, scoregrid[(0, 0)]); fitted_anytime_goalscorer_probs.scale(1.0 / (1.0 - scoregrid[(0, 0)])); let anytime_goalscorer_booksum = fitted_anytime_goalscorer_probs.sum(); @@ -337,7 +387,7 @@ pub fn main() { let anytime_goalscorer_overround = Market::fit(&OVERROUND_METHOD, anytime_gs.market.prices.clone(), anytime_goalscorer_booksum); let fitted_anytime_goalscorer = LabelledMarket { market_type: MarketType::AnytimeGoalscorer, - outcomes: anytime_gs.outcomes.clone(), + outcomes: fitted_anytime_goalscorer_outcomes, market: Market::frame(&anytime_goalscorer_overround.overround, fitted_anytime_goalscorer_probs, &SINGLE_PRICE_BOUNDS), }; let table_anytime_goalscorer = print_market(&fitted_anytime_goalscorer); @@ -394,7 +444,7 @@ pub fn main() { .map(|(key, sample, fitted)| { ( *key, - compute_msre(&sample.market.prices, &fitted.market.prices).sqrt(), + compute_error(&sample.market.prices, &fitted.market.prices), ) }) .collect::>(); @@ -409,6 +459,8 @@ pub fn main() { "Market overrounds:\n{}", Console::default().render(&table_overrounds) ); + + Ok(()) } fn fit_market(market_type: MarketType, map: &HashMap) -> LabelledMarket { @@ -418,7 +470,7 @@ fn fit_market(market_type: MarketType, map: &HashMap) -> Label .iter() .map(|(outcome, _)| (*outcome).clone()) .collect::>(); - let prices = entries.iter().map(|(_, &price)| price).collect::>(); + let prices = entries.iter().map(|(_, &price)| price).collect(); let market = Market::fit(&OVERROUND_METHOD, prices, 1.0); LabelledMarket { market_type, outcomes, market } } @@ -449,9 +501,7 @@ fn fit_scoregrid(markets: &[&LabelledMarket]) -> HypergridSearchOutcome { for (index, outcome) in market.outcomes.iter().enumerate() { let fitted_prob = outcome.gather(&scoregrid); let sample_prob = market.market.probs[index]; - let relative_error = (sample_prob - fitted_prob) / sample_prob; - residual += relative_error.powi(2); - // residual += (sample_prob - fitted_prob).powi(2); + residual += ERROR_TYPE.calculate(sample_prob, fitted_prob); } } residual @@ -478,11 +528,28 @@ fn fit_first_goalscorer(optimal_scoring_probs: &[f64], player: &Player, expected players: vec![(player.clone(), values[0])], }); let isolated_prob = isolate(&MarketType::FirstGoalscorer, &OutcomeType::Player(player.clone()), &exploration.prospects, &exploration.player_lookup); - ((isolated_prob - expected_prob)/expected_prob).powi(2) + ERROR_TYPE.calculate(expected_prob, isolated_prob) }, ) } +enum ErrorType { + SquaredRelative, + SquaredAbsolute +} +impl ErrorType { + fn calculate(&self, expected: f64, sample: f64) -> f64 { + match self { + ErrorType::SquaredRelative => ((expected - sample)/sample).powi(2), + ErrorType::SquaredAbsolute => (expected - sample).powi(2) + } + } + + fn reverse(&self, error: f64) -> f64 { + error.sqrt() + } +} + /// Intervals. fn interval_scoregrid(interval_home_prob: f64, interval_away_prob: f64, interval_common_prob: f64) -> Matrix { let dim = usize::min(MAX_TOTAL_GOALS as usize, INTERVALS) + 1; @@ -533,18 +600,19 @@ fn frame_prices(scoregrid: &Matrix, outcomes: &[OutcomeType], overround: &O Market::frame(overround, probs, &SINGLE_PRICE_BOUNDS) } -fn compute_msre(sample_prices: &[f64], fitted_prices: &[f64]) -> f64 { - let mut sq_rel_error = 0.0; +fn compute_error(sample_prices: &[f64], fitted_prices: &[f64]) -> f64 { + let mut error_sum = 0.0; let mut counted = 0; for (index, sample_price) in sample_prices.iter().enumerate() { let fitted_price: f64 = fitted_prices[index]; if fitted_price.is_finite() { counted += 1; - let relative_error = (sample_price - fitted_price) / sample_price; - sq_rel_error += relative_error.powi(2); + let (sample_prob, fitted_prob) = (1.0 / sample_price, 1.0 / fitted_price); + error_sum += ERROR_TYPE.calculate(sample_prob, fitted_prob); } } - sq_rel_error / counted as f64 + let mean_error = error_sum / counted as f64; + ERROR_TYPE.reverse(mean_error) } fn print_market(market: &LabelledMarket) -> Table { @@ -590,4 +658,18 @@ fn print_overrounds(markets: &[LabelledMarket]) -> Table { )); } table +} + +async fn read_contest_data(args: &Args) -> anyhow::Result { + let contest = { + if let Some(_) = args.file.as_ref() { + //ContestModel::read_json_file(path)? + unimplemented!() + } else if let Some(id) = args.download.as_ref() { + download_by_id(id.clone()).await? + } else { + unreachable!() + } + }; + Ok(contest.into()) } \ No newline at end of file diff --git a/src/interval.rs b/src/interval.rs index fff816a..83eabd0 100644 --- a/src/interval.rs +++ b/src/interval.rs @@ -1,8 +1,6 @@ use rustc_hash::FxHashMap; -use strum::IntoEnumIterator; use crate::entity::{MarketType, OutcomeType, Player, Score, Side}; -use crate::scoregrid::GoalEvent; #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Prospect { @@ -90,60 +88,73 @@ pub fn explore(config: &IntervalConfig) -> Exploration { for _ in 0..config.intervals { let mut next_prospects = init_prospects(current_prospects.len() * 4); - for goal_event in GoalEvent::iter() { - match goal_event { - GoalEvent::Neither => { + + for (current_prospect, current_prob) in current_prospects { + // let partial_goals = if partial.home_scorer.is_some() { 1 } else { 0 } + // + if partial.away_scorer.is_some() { 1 } else { 0 }; + // // let partial_goals = partial.home_scorer.map(|_| 1).unwrap_or(0) + // // + partial.away_scorer.map(|_| 1).unwrap_or(0); + // if current_prospect.score.total() + partial_goals > config.max_total_goals { + // continue; + // } + + // neither team scores + let partial = PartialProspect { + home_scorer: None, + away_scorer: None, + first_scoring_side: None, + prob: neither_prob, + }; + merge(¤t_prospect, current_prob, partial, &mut next_prospects); + + // at least one more goal allowed before pruning + if current_prospect.score.total() < config.max_total_goals { + // only the home team scores + for (player_index, player_prob) in &home_scorers { let partial = PartialProspect { - home_scorer: None, + home_scorer: Some(*player_index), away_scorer: None, - first_scoring_side: None, - prob: neither_prob, + first_scoring_side: Some(&Side::Home), + prob: config.home_prob * player_prob, }; - pruned += merge(config, ¤t_prospects, partial, &mut next_prospects); - } - GoalEvent::Home => { - for (player_index, player_prob) in &home_scorers { - let partial = PartialProspect { - home_scorer: Some(*player_index), - away_scorer: None, - first_scoring_side: Some(&Side::Home), - prob: config.home_prob * player_prob, - }; - pruned += merge(config, ¤t_prospects, partial, &mut next_prospects); - } + merge(¤t_prospect, current_prob, partial, &mut next_prospects); } - GoalEvent::Away => { - for (player_index, player_prob) in &away_scorers { - let partial = PartialProspect { - home_scorer: None, - away_scorer: Some(*player_index), - first_scoring_side: Some(&Side::Away), - prob: config.away_prob * player_prob, - }; - pruned += merge(config, ¤t_prospects, partial, &mut next_prospects); - } + + // only the away team scores + for (player_index, player_prob) in &away_scorers { + let partial = PartialProspect { + home_scorer: None, + away_scorer: Some(*player_index), + first_scoring_side: Some(&Side::Away), + prob: config.away_prob * player_prob, + }; + merge(¤t_prospect, current_prob, partial, &mut next_prospects); } - GoalEvent::Both => { - for (home_player_index, home_player_prob) in &home_scorers { - for (away_player_index, away_player_prob) in &away_scorers { - for first_scoring_side in [&Side::Home, &Side::Away] { - let partial = PartialProspect { - home_scorer: Some(*home_player_index), - away_scorer: Some(*away_player_index), - first_scoring_side: Some(first_scoring_side), - prob: config.common_prob - * home_player_prob - * away_player_prob - * 0.5, - }; - pruned += - merge(config, ¤t_prospects, partial, &mut next_prospects); - } + } else { + pruned += current_prob * (config.home_prob + config.away_prob); + } + + // at least two more goals allowed before pruning + if current_prospect.score.total() + 1 < config.max_total_goals { + // both teams score + for (home_player_index, home_player_prob) in &home_scorers { + for (away_player_index, away_player_prob) in &away_scorers { + for first_scoring_side in [&Side::Home, &Side::Away] { + let partial = PartialProspect { + home_scorer: Some(*home_player_index), + away_scorer: Some(*away_player_index), + first_scoring_side: Some(first_scoring_side), + prob: config.common_prob * home_player_prob * away_player_prob * 0.5, + }; + merge(¤t_prospect, current_prob, partial, &mut next_prospects); } } } - }; + } else { + pruned += current_prob * config.common_prob; + } } + current_prospects = next_prospects; } @@ -154,26 +165,15 @@ pub fn explore(config: &IntervalConfig) -> Exploration { } } -#[must_use] +#[inline] fn merge( - config: &IntervalConfig, - current_prospects: &Prospects, + current_prospect: &Prospect, + current_prob: f64, partial: PartialProspect, next_prospects: &mut Prospects, -) -> f64 { - let mut pruned = 0.0; - for (current, current_prob) in current_prospects { - let merged_prob = *current_prob * partial.prob; - let partial_goals = if partial.home_scorer.is_some() { 1 } else { 0 } - + if partial.away_scorer.is_some() { 1 } else { 0 }; - // let partial_goals = partial.home_scorer.map(|_| 1).unwrap_or(0) - // + partial.away_scorer.map(|_| 1).unwrap_or(0); - if current.score.total() + partial_goals > config.max_total_goals { - pruned += merged_prob; - continue; - } - - let mut merged = current.clone(); +) { + let merged_prob = current_prob * partial.prob; + let mut merged = current_prospect.clone(); if let Some(scorer) = partial.home_scorer { merged.stats[scorer].goals += 1; merged.score.home += 1; @@ -192,8 +192,6 @@ fn merge( .entry(merged) .and_modify(|prob| *prob += merged_prob) .or_insert(merged_prob); - } - pruned } #[must_use] diff --git a/src/lib.rs b/src/lib.rs index 44c9e13..7ae7987 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ pub mod capture; pub mod comb; pub mod csv; -pub mod data; pub mod display; pub mod entity; pub mod factorial; @@ -23,8 +22,10 @@ pub mod opt; pub mod poisson; pub mod print; pub mod probs; +pub mod racing_data; pub mod scoregrid; pub mod selection; +pub mod soccer_data; pub mod timed; #[cfg(test)] diff --git a/src/data.rs b/src/racing_data.rs similarity index 88% rename from src/data.rs rename to src/racing_data.rs index 96e5cc5..bfc3080 100644 --- a/src/data.rs +++ b/src/racing_data.rs @@ -17,43 +17,9 @@ pub struct PlacePriceDeparture { } pub trait EventDetailExt { - fn summarise(self) -> RaceSummary; fn place_price_departure(&self) -> PlacePriceDeparture; } impl EventDetailExt for EventDetail { - fn summarise(self) -> RaceSummary { - let mut prices = Matrix::allocate(PODIUM, self.runners.len()); - for rank in 0..PODIUM { - let row_slice = prices.row_slice_mut(rank); - for (runner_index, runner_data) in self.runners.iter().enumerate() { - row_slice[runner_index] = match &runner_data.prices { - None => f64::INFINITY, - Some(prices) => { - let price = match rank { - 0 => prices.win, - 1 => prices.top2, - 2 => prices.top3, - 3 => prices.top4, - _ => unimplemented!(), - }; - price as f64 - } - } - } - } - RaceSummary { - id: self.id, - race_name: self.race_name, - meeting_name: self.meeting_name, - race_type: self.race_type, - race_number: self.race_number, - capture_time: self.capture_time, - places_paying: self.places_paying as usize, - class_name: self.class_name, - prices, - } - } - fn place_price_departure(&self) -> PlacePriceDeparture { let mut sum_sq = 0.; let mut worst_sq = 0.; @@ -90,6 +56,41 @@ impl EventDetailExt for EventDetail { } } +impl From for RaceSummary { + fn from(external: EventDetail) -> Self { + let mut prices = Matrix::allocate(PODIUM, external.runners.len()); + for rank in 0..PODIUM { + let row_slice = prices.row_slice_mut(rank); + for (runner_index, runner_data) in external.runners.iter().enumerate() { + row_slice[runner_index] = match &runner_data.prices { + None => f64::INFINITY, + Some(prices) => { + let price = match rank { + 0 => prices.win, + 1 => prices.top2, + 2 => prices.top3, + 3 => prices.top4, + _ => unimplemented!(), + }; + price as f64 + } + } + } + } + Self { + id: external.id, + race_name: external.race_name, + meeting_name: external.meeting_name, + race_type: external.race_type, + race_number: external.race_number, + capture_time: external.capture_time, + places_paying: external.places_paying as usize, + class_name: external.class_name, + prices, + } + } +} + #[derive(Debug)] pub struct RaceSummary { pub id: u64, diff --git a/src/scoregrid.rs b/src/scoregrid.rs index 93503e9..8eb2c76 100644 --- a/src/scoregrid.rs +++ b/src/scoregrid.rs @@ -326,7 +326,11 @@ impl OutcomeType { } fn gather_correct_score(score: &Score, scoregrid: &Matrix) -> f64 { - scoregrid[(score.home as usize, score.away as usize)] + if (score.home as usize) < scoregrid.rows() && (score.away as usize) < scoregrid.cols() { + scoregrid[(score.home as usize, score.away as usize)] + } else { + 0.0 + } } } diff --git a/src/soccer_data.rs b/src/soccer_data.rs new file mode 100644 index 0000000..6e355ec --- /dev/null +++ b/src/soccer_data.rs @@ -0,0 +1,125 @@ +use crate::entity::{MarketType, OutcomeType, Over, Player, Score, Side}; +use racing_scraper::get_sports_contest; +use racing_scraper::sports::soccer::contest_model::ContestModel; +use racing_scraper::sports::soccer::market_model::{HomeAway, Scorer, SoccerMarket}; +use std::collections::HashMap; + +#[derive(Debug)] +pub struct ContestSummary { + pub id: String, + pub name: String, + pub offerings: HashMap>, +} + +impl From for ContestSummary { + fn from(external: ContestModel) -> Self { + let id = external.id; + let name = external.name; + let mut offerings = HashMap::with_capacity(external.markets.len()); + for market in external.markets { + match market { + SoccerMarket::CorrectScore(markets) => { + offerings.insert( + MarketType::CorrectScore, + HashMap::from_iter(markets.iter().map(|market| { + ( + OutcomeType::Score(Score { + home: market.score.home as u8, + away: market.score.away as u8, + }), + market.odds, + ) + })), + ); + } + SoccerMarket::TotalGoalsOverUnder(market, line) => { + let (over, under) = (line.floor() as u8, line.ceil() as u8); + offerings.insert( + MarketType::TotalGoalsOverUnder(Over(over)), + HashMap::from([ + (OutcomeType::Over(over), market.over), + (OutcomeType::Under(under), market.under), + ]), + ); + } + SoccerMarket::H2H(h2h) => { + offerings.insert( + MarketType::HeadToHead, + HashMap::from([ + (OutcomeType::Win(Side::Home), h2h.home), + (OutcomeType::Win(Side::Away), h2h.away), + (OutcomeType::Draw, h2h.draw), + ]), + ); + } + SoccerMarket::AnyTimeGoalScorer(scorers) => { + offerings.insert( + MarketType::AnytimeGoalscorer, + HashMap::from_iter(scorers.into_iter().map(|scorer| { + let OutcomeOdds(outcome_type, odds) = OutcomeOdds::from(scorer); + (outcome_type, odds) + })), + ); + } + SoccerMarket::FirstGoalScorer(scorers) => { + offerings.insert( + MarketType::FirstGoalscorer, + HashMap::from_iter(scorers.into_iter().map(|scorer| { + let OutcomeOdds(outcome_type, odds) = OutcomeOdds::from(scorer); + (outcome_type, odds) + })), + ); + } + SoccerMarket::CorrectScoreFirstHalf(_) => { + //TODO + } + SoccerMarket::CorrectScoreSecondHalf(_) => { + //TODO + } + SoccerMarket::TotalGoalsOddEven(_) => { + //TODO + } + SoccerMarket::FirstHalfGoalsOddEven(_) => { + //TODO + } + SoccerMarket::SecondHalfGoalOddEven(_) => { + //TODO + } + SoccerMarket::Score2GoalsOrMore(_) => { + //TODO + } + } + } + Self { + id, + name, + offerings, + } + } +} + +impl From for Side { + fn from(home_way: HomeAway) -> Self { + match home_way { + HomeAway::Home => Side::Home, + HomeAway::Away => Side::Away, + } + } +} + +struct OutcomeOdds(OutcomeType, f64); + +impl From for OutcomeOdds { + fn from(scorer: Scorer) -> Self { + let outcome_type = match scorer.side { + None => OutcomeType::None, + Some(side) => OutcomeType::Player(Player::Named(side.into(), scorer.name)), + }; + OutcomeOdds(outcome_type, scorer.odds) + } +} + +pub async fn download_by_id(id: String) -> anyhow::Result { + let contest = get_sports_contest(id).await?; + Ok(contest) +}