Skip to content

Commit

Permalink
Deriving non-player markets
Browse files Browse the repository at this point in the history
  • Loading branch information
ekoutanov committed Dec 19, 2023
1 parent aaf6a40 commit ac92e7d
Show file tree
Hide file tree
Showing 22 changed files with 586 additions and 397 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ chrono = "0.4.31"
clap = { version = "4.4.6", features = ["derive"] }
racing_scraper = "0.0.18"
serde_json = "1.0.107"
stanza = "0.3.0"
stanza = "0.4.0"
tinyrand = "0.5.0"
tokio={version="1.32.0",features=["full"]}
tracing = "0.1.37"
Expand Down
4 changes: 2 additions & 2 deletions brumby-soccer/benches/cri_interval.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use criterion::{criterion_group, criterion_main, Criterion};

use brumby_soccer::interval;
use brumby_soccer::interval::{IntervalConfig, PruneThresholds, BivariateProbs, TeamProbs, UnivariateProbs};
use brumby_soccer::interval::{Config, PruneThresholds, BivariateProbs, TeamProbs, UnivariateProbs};

fn criterion_benchmark(c: &mut Criterion) {
fn run(intervals: u8, max_total_goals: u16) -> usize {
interval::explore(
&IntervalConfig {
&Config {
intervals,
team_probs: TeamProbs {
h1_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 },
Expand Down
4 changes: 2 additions & 2 deletions brumby-soccer/benches/cri_isolate.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use brumby_soccer::domain::{OfferType, OutcomeType, Player, Side};
use criterion::{criterion_group, criterion_main, Criterion};

use brumby_soccer::interval::{explore, Exploration, IntervalConfig, BivariateProbs, PruneThresholds, PlayerProbs, TeamProbs, UnivariateProbs};
use brumby_soccer::interval::{explore, Exploration, Config, BivariateProbs, PruneThresholds, PlayerProbs, TeamProbs, UnivariateProbs};
use brumby_soccer::interval::query::isolate;

fn criterion_benchmark(c: &mut Criterion) {
let player = Player::Named(Side::Home, "Markos".into());
fn prepare(intervals: u8, max_total_goals: u16, player: Player) -> Exploration {
explore(
&IntervalConfig {
&Config {
intervals,
team_probs: TeamProbs {
h1_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 },
Expand Down
100 changes: 88 additions & 12 deletions brumby-soccer/src/bin/soc_prices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@ use std::path::PathBuf;
use anyhow::bail;
use clap::Parser;
use rustc_hash::FxHashMap;
use stanza::renderer::console::Console;
use stanza::renderer::Renderer;
use tracing::{debug, info};

use brumby::hash_lookup::HashLookup;
use brumby::market::{Market, OverroundMethod, PriceBounds};
use brumby::probs::SliceExt;
use brumby::tables;
use brumby_soccer::data::{download_by_id, ContestSummary, SoccerFeedId};
use brumby_soccer::domain::{Offer, OfferType, OutcomeType, Over, Period};
use brumby_soccer::model::{Model, score_fitter};
use brumby_soccer::domain::{Offer, OfferType, OutcomeType};
use brumby_soccer::fit::{ErrorType, FittingErrors};
use brumby_soccer::model::score_fitter::ScoreFitter;
use brumby_soccer::model::{score_fitter, Model, Stub};
use brumby_soccer::{fit, model, print};

const OVERROUND_METHOD: OverroundMethod = OverroundMethod::OddsRatio;
const SINGLE_PRICE_BOUNDS: PriceBounds = 1.01..=301.0;
const FIRST_GOALSCORER_BOOKSUM: f64 = 1.5;
const INTERVALS: usize = 18;
const INTERVALS: u8 = 18;
const INCREMENTAL_OVERROUND: f64 = 0.01;
// const MAX_TOTAL_GOALS_HALF: u16 = 4;
const MAX_TOTAL_GOALS_FULL: u16 = 8;
const MAX_TOTAL_GOALS: u16 = 8;
const GOALSCORER_MIN_PROB: f64 = 0.0;
// const ERROR_TYPE: ErrorType = ErrorType::SquaredRelative;

Expand Down Expand Up @@ -55,8 +60,6 @@ impl Args {
}
}

const INCREMENTAL_OVERROUND: f64 = 0.01;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
if env::var("RUST_BACKTRACE").is_err() {
Expand All @@ -73,11 +76,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
let contest = read_contest_data(&args).await?;
info!("contest.name: {}", contest.name);

let offers = contest
let sample_offers = contest
.offerings
.iter()
.map(|(offer_type, prices)| {
debug!("sourced {offer_type:?} with {} outcomes, σ={:.3}", prices.len(), implied_booksum(prices.values()));
debug!(
"sourced {offer_type:?} with {} outcomes, σ={:.3}",
prices.len(),
implied_booksum(prices.values())
);
let normal = match &offer_type {
OfferType::HeadToHead(_)
| OfferType::TotalGoals(_, _)
Expand All @@ -90,16 +97,74 @@ async fn main() -> Result<(), Box<dyn Error>> {
let implied_booksum = implied_booksum(prices.values());
let expected_overround = prices.len() as f64 * INCREMENTAL_OVERROUND;
implied_booksum / expected_overround
},
}
};
let offer = fit_offer(offer_type.clone(), prices, normal);
(offer_type.clone(), offer)
})
.collect::<FxHashMap<_, _>>();

let mut model = Model::new();
let mut model = Model::try_from(model::Config {
intervals: INTERVALS,
max_total_goals: MAX_TOTAL_GOALS,
})?;

let score_fitter = ScoreFitter::try_from(score_fitter::Config::default())?;
score_fitter.fit(&mut model, &offers)?;
score_fitter.fit(&mut model, &sample_offers)?;

let stubs = sample_offers
.iter()
.filter(|(offer_type, _)| {
matches!(
offer_type,
OfferType::HeadToHead(_) | OfferType::TotalGoals(_, _) | OfferType::CorrectScore(_)
)
})
.map(|(_, offer)| Stub {
offer_type: offer.offer_type.clone(),
outcomes: offer.outcomes.clone(),
normal: offer.market.fair_booksum(),
overround: offer.market.overround.clone(),
})
.collect::<Vec<_>>();
model.derive(&stubs, &SINGLE_PRICE_BOUNDS)?;

{
let fitting_errors = model
.offers
.values()
.map(|fitted| {
let sample = sample_offers.get(&fitted.offer_type).unwrap();
(
&sample.offer_type,
FittingErrors {
rmse: fit::compute_error(
&sample.market.prices,
&fitted.market.prices,
&ErrorType::SquaredAbsolute,
),
rmsre: fit::compute_error(
&sample.market.prices,
&fitted.market.prices,
&ErrorType::SquaredRelative,
),
},
)
})
.collect::<Vec<_>>();

let fitting_errors = print::tabulate_errors(&sort_tuples(fitting_errors));
let overrounds = print::tabulate_overrounds(
&sort_tuples(model.offers())
.iter()
.map(|(_, offer)| *offer)
.collect::<Vec<_>>(),
);
info!(
"Fitting errors and overrounds:\n{}",
Console::default().render(&tables::merge(&[fitting_errors, overrounds]))
);
}

Ok(())
}
Expand All @@ -124,6 +189,17 @@ fn fit_offer(offer_type: OfferType, map: &HashMap<OutcomeType, f64>, normal: f64
}
}

fn sort_tuples<K, V, I>(tuples: I) -> Vec<(K, V)>
where
I: IntoIterator<Item = (K, V)>,
K: Ord,
{
let tuples = tuples.into_iter();
let mut tuples = tuples.collect::<Vec<_>>();
tuples.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
tuples
}

async fn read_contest_data(args: &Args) -> anyhow::Result<ContestSummary> {
let contest = {
if let Some(_) = args.file.as_ref() {
Expand Down
60 changes: 32 additions & 28 deletions brumby-soccer/src/bin/soc_prices2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ use brumby_soccer::domain::{Offer, OfferType, OutcomeType, Over, Period, Score};
use brumby_soccer::fit::{away_booksum, home_booksum, ErrorType, FittingErrors};
use brumby_soccer::interval::query::isolate;
use brumby_soccer::interval::{
explore, BivariateProbs, Expansions, Exploration, IntervalConfig, PlayerProbs, PruneThresholds,
explore, BivariateProbs, Expansions, Exploration, Config, PlayerProbs, PruneThresholds,
TeamProbs, UnivariateProbs,
};
use brumby_soccer::{fit, print};

const OVERROUND_METHOD: OverroundMethod = OverroundMethod::OddsRatio;
const SINGLE_PRICE_BOUNDS: PriceBounds = 1.01..=301.0;
const FIRST_GOALSCORER_BOOKSUM: f64 = 1.5;
const INTERVALS: usize = 18;
// const MAX_TOTAL_GOALS_HALF: u16 = 4;
const INTERVALS: u8 = 18;
const MAX_TOTAL_GOALS_HALF: u16 = 6;
const MAX_TOTAL_GOALS_FULL: u16 = 8;
const GOALSCORER_MIN_PROB: f64 = 0.0;
// const ERROR_TYPE: ErrorType = ErrorType::SquaredRelative;
Expand Down Expand Up @@ -129,16 +129,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
1.0,
);

let (ft_search_outcome, lambdas) = fit::fit_scoregrid_full(&ft_h2h, &ft_goals);
let (ft_search_outcome, lambdas) = fit::fit_scoregrid_full(&ft_h2h, &ft_goals, INTERVALS, MAX_TOTAL_GOALS_FULL);
const H1_RATIO: f64 = 0.425;
println!("*** fitting H1 ***");
let h1_home_goals_estimate = (lambdas[0] + lambdas[2]) * H1_RATIO;
let h1_away_goals_estimate = (lambdas[1] + lambdas[2]) * H1_RATIO;
let h1_search_outcome = fit::fit_scoregrid_half(h1_home_goals_estimate, h1_away_goals_estimate, &[&h1_h2h, &h1_goals]);
let h1_search_outcome = fit::fit_scoregrid_half(h1_home_goals_estimate, h1_away_goals_estimate, &[&h1_h2h, &h1_goals], INTERVALS, MAX_TOTAL_GOALS_HALF);
println!("*** fitting H2 ***");
let h2_home_goals_estimate = (lambdas[0] + lambdas[2]) * (1.0 - H1_RATIO);
let h2_away_goals_estimate = (lambdas[1] + lambdas[2]) * (1.0 - H1_RATIO);
let h2_search_outcome = fit::fit_scoregrid_half(h2_home_goals_estimate, h2_away_goals_estimate, &[&h2_h2h, &h2_goals]);
let h2_search_outcome = fit::fit_scoregrid_half(h2_home_goals_estimate, h2_away_goals_estimate, &[&h2_h2h, &h2_goals], INTERVALS, MAX_TOTAL_GOALS_HALF);

let mut adj_optimal_h1 = [0.0; 3];
let mut adj_optimal_h2 = [0.0; 3];
Expand Down Expand Up @@ -432,13 +432,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
&BivariateProbs::from(adj_optimal_h2.as_slice()),
&first_gs,
draw_prob,
INTERVALS,
MAX_TOTAL_GOALS_FULL
);

let mut fitted_first_goalscorer_probs = Vec::with_capacity(first_gs.outcomes.len());
for (player, prob) in &fitted_goalscorer_probs {
let exploration = explore(
&IntervalConfig {
intervals: INTERVALS as u8,
&Config {
intervals: INTERVALS,
team_probs: TeamProbs {
h1_goals: BivariateProbs::from(adj_optimal_h1.as_slice()),
h2_goals: BivariateProbs::from(adj_optimal_h2.as_slice()),
Expand Down Expand Up @@ -467,7 +469,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
first_goalscorer: true,
},
},
0..INTERVALS as u8,
0..INTERVALS,
);
let isolated_prob = isolate(
&OfferType::FirstGoalscorer,
Expand Down Expand Up @@ -516,8 +518,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
for (player, prob) in &fitted_goalscorer_probs {
fitted_anytime_goalscorer_outcomes.push(OutcomeType::Player(player.clone()));
let exploration = explore(
&IntervalConfig {
intervals: INTERVALS as u8,
&Config {
intervals: INTERVALS,
team_probs: TeamProbs {
h1_goals: BivariateProbs::from(adj_optimal_h1.as_slice()),
h2_goals: BivariateProbs::from(adj_optimal_h2.as_slice()),
Expand Down Expand Up @@ -546,7 +548,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
first_goalscorer: false,
},
},
0..INTERVALS as u8,
0..INTERVALS,
);
let isolated_prob = isolate(
&OfferType::AnytimeGoalscorer,
Expand Down Expand Up @@ -623,15 +625,17 @@ async fn main() -> Result<(), Box<dyn Error>> {
&anytime_assist,
draw_prob,
anytime_assist.market.fair_booksum(),
INTERVALS,
MAX_TOTAL_GOALS_FULL
);

let mut fitted_anytime_assist_outcomes = HashLookup::with_capacity(fitted_assist_probs.len());
let mut fitted_anytime_assist_probs = Vec::with_capacity(fitted_assist_probs.len());
for (player, prob) in &fitted_assist_probs {
fitted_anytime_assist_outcomes.push(OutcomeType::Player(player.clone()));
let exploration = explore(
&IntervalConfig {
intervals: INTERVALS as u8,
&Config {
intervals: INTERVALS,
team_probs: TeamProbs {
h1_goals: BivariateProbs::from(adj_optimal_h1.as_slice()),
h2_goals: BivariateProbs::from(adj_optimal_h2.as_slice()),
Expand All @@ -657,7 +661,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
first_goalscorer: false,
},
},
0..INTERVALS as u8,
0..INTERVALS,
);
let isolated_prob = isolate(
&OfferType::AnytimeAssist,
Expand Down Expand Up @@ -739,16 +743,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
);

let fitted_markets = [
fitted_h1_h2h,
fitted_h1_goals,
fitted_h2_h2h,
fitted_h2_goals,
fitted_ft_h2h,
fitted_ft_goals_ou,
fitted_ft_correct_score,
fitted_first_goalscorer,
fitted_anytime_goalscorer,
fitted_anytime_assist,
&fitted_h1_h2h,
&fitted_h1_goals,
&fitted_h2_h2h,
&fitted_h2_goals,
&fitted_ft_h2h,
&fitted_ft_goals_ou,
&fitted_ft_correct_score,
&fitted_first_goalscorer,
&fitted_anytime_goalscorer,
&fitted_anytime_assist,
];

let table_overrounds = print::tabulate_overrounds(&fitted_markets);
Expand Down Expand Up @@ -984,8 +988,8 @@ fn fit_offer(offer_type: OfferType, map: &HashMap<OutcomeType, f64>, normal: f64

fn explore_scores(h1_goals: BivariateProbs, h2_goals: BivariateProbs) -> Exploration {
explore(
&IntervalConfig {
intervals: INTERVALS as u8,
&Config {
intervals: INTERVALS,
team_probs: TeamProbs {
h1_goals,
h2_goals,
Expand All @@ -1008,7 +1012,7 @@ fn explore_scores(h1_goals: BivariateProbs, h2_goals: BivariateProbs) -> Explora
first_goalscorer: false,
},
},
0..INTERVALS as u8,
0..INTERVALS,
)
}

Expand Down
6 changes: 0 additions & 6 deletions brumby-soccer/src/domain/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ 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;
Expand Down Expand Up @@ -56,10 +54,6 @@ impl OfferType {
_ => Ok(()),
}
}

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

#[derive(Debug, Error)]
Expand Down
4 changes: 2 additions & 2 deletions brumby-soccer/src/domain/error/head_to_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub fn validate_outcomes(
match offer_type {
OfferType::HeadToHead(_) => {
error::OutcomesCompleteAssertion {
outcomes: &create_outcomes(),
outcomes: &valid_outcomes(),
}
.check(outcomes, offer_type)?;
Ok(())
Expand All @@ -29,7 +29,7 @@ pub fn validate_probs(offer_type: &OfferType, probs: &[f64]) -> Result<(), Inval
}
}

pub fn create_outcomes() -> [OutcomeType; 3] {
fn valid_outcomes() -> [OutcomeType; 3] {
[
OutcomeType::Win(Side::Home),
OutcomeType::Win(Side::Away),
Expand Down
Loading

0 comments on commit ac92e7d

Please sign in to comment.