#monte-carlo #quant #racing #simulation #finish #model #events

bentobox

A fast, allocation-free Monte Carlo model of a top-N podium finish in racing events

2 releases

0.1.1 Oct 11, 2023
0.1.0 Oct 11, 2023

#2075 in Algorithms

MIT license

14KB
290 lines

bentobox

A fast, allocation-free Monte Carlo model of a top-N podium finish in racing events. Derives probabilities for placing in arbitrary positions given only win probabilities. Also derives joint probability of multiple runners with arbitrary (exact and top-N) placings.

Performance

Circa 20M simulations/sec on a top-4 podium over 14 runners using the tinyrand RNG. Roughly 80% of time is spent in the RNG routine.

Example

Sourced from examples/multi.rs.

use bentobox::mc;
use bentobox::probs::SliceExt;
use bentobox::selection::Selection;
use tinyrand::StdRand;
use bentobox::capture::{CaptureMut, Capture};

// probs taken from a popular website
let mut probs = vec![
    1.0 / 11.0,
    1.0 / 41.0,
    1.0 / 18.0,
    1.0 / 12.0,
    1.0 / 91.0,
    1.0 / 101.0,
    1.0 / 4.8,
    1.0 / 14.0,
    1.0 / 2.9,
    1.0 / 91.0,
    1.0 / 9.0,
    1.0 / 91.0,
    1.0 / 5.0,
    1.0 / 21.0,
];

// force probs to sum to 1 and extract the approximate overround used (multiplicative method assumed)
let overround = probs.normalize();

println!("fair probs: {probs:?}");
println!("overround: {overround:.3}");

// create an MC engine for reuse
let mut engine = mc::MonteCarloEngine::default()
    .with_iterations(10_000)
    .with_probabilities(Capture::Borrowed(&probs))
    .with_podium_places(4)
    .with_rand(CaptureMut::Owned(StdRand::default()));

// simulate top-N rankings for all runners
// NOTE: rankings and runner numbers are zero-based
for runner in 0..probs.len() {
    println!("runner: {runner}");
    for rank in 0..4 {
        let frac = engine.simulate(&vec![Selection::Top { runner, rank }]);
        println!(
            "    rank: 0~{rank}, prob: {}, fair price: {:.3}, market odds: {:.3}",
            frac.quotient(),
            1.0 / frac.quotient(),
            1.0 / frac.quotient() / overround
        );
    }
}

// simulate a same-race multi for a chosen selection vector
let selections = vec![
    Selection::Top { runner: 0, rank: 0 },
    Selection::Top { runner: 1, rank: 1 },
    Selection::Top { runner: 2, rank: 2 },
];
let frac = engine.simulate(&selections);
println!(
    "probability of {selections:?}: {}, fair price: {:.3}, market odds: {:.3}",
    frac.quotient(),
    1.0 / frac.quotient(),
    1.0 / frac.quotient() / overround
);

Dependencies

~66KB