diff --git a/brumby-soccer/src/bin/soc_prices.rs b/brumby-soccer/src/bin/soc_prices.rs index 7a2ad8e..e058a03 100644 --- a/brumby-soccer/src/bin/soc_prices.rs +++ b/brumby-soccer/src/bin/soc_prices.rs @@ -113,7 +113,8 @@ async fn main() -> Result<(), Box> { | OfferType::TotalGoals(_, _) | OfferType::CorrectScore(_) | OfferType::AsianHandicap(_, _) - | OfferType::DrawNoBet(_) => 1.0, + | OfferType::DrawNoBet(_) + | OfferType::SplitHandicap(_, _, _) => 1.0, OfferType::AnytimeGoalscorer | OfferType::FirstGoalscorer | OfferType::PlayerShotsOnTarget(_) @@ -169,6 +170,7 @@ async fn main() -> Result<(), Box> { | OfferType::TotalGoals(_, _) | OfferType::AsianHandicap(_, _) | OfferType::DrawNoBet(_) + | OfferType::SplitHandicap(_, _, _) | OfferType::CorrectScore(_) | OfferType::FirstGoalscorer | OfferType::AnytimeGoalscorer @@ -338,11 +340,7 @@ fn fit_offer(offer_type: OfferType, map: &HashMap, normal: f64) -> } } -fn sort_tuples(tuples: I) -> Vec<(K, V)> -where - I: IntoIterator, - K: Ord, -{ +fn sort_tuples(tuples: impl IntoIterator) -> Vec<(K, V)> { let tuples = tuples.into_iter(); let mut tuples = tuples.collect::>(); tuples.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); diff --git a/brumby-soccer/src/domain.rs b/brumby-soccer/src/domain.rs index de9793d..8a0ed6e 100644 --- a/brumby-soccer/src/domain.rs +++ b/brumby-soccer/src/domain.rs @@ -53,6 +53,19 @@ impl DrawHandicap { DrawHandicap::Behind(by) => WinHandicap::BehindUnder(*by) } } + + pub fn flip(&self) -> DrawHandicap { + match self { + DrawHandicap::Ahead(by) => { + if *by > 0 { + DrawHandicap::Behind(*by) + } else { + DrawHandicap::Ahead(0) + } + }, + DrawHandicap::Behind(by) => DrawHandicap::Ahead(*by) + } + } } #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] @@ -83,6 +96,7 @@ pub enum OfferType { CorrectScore(Period), AsianHandicap(Period, WinHandicap), DrawNoBet(DrawHandicap), + SplitHandicap(Period, DrawHandicap, WinHandicap), AnytimeGoalscorer, FirstGoalscorer, PlayerShotsOnTarget(Over), @@ -94,8 +108,9 @@ impl OfferType { OfferType::HeadToHead(_, _) => OfferCategory::HeadToHead, OfferType::TotalGoals(_, _) => OfferCategory::TotalGoals, OfferType::CorrectScore(_) => OfferCategory::CorrectScore, - OfferType::DrawNoBet(_) => OfferCategory::DrawNoBet, OfferType::AsianHandicap(_, _) => OfferCategory::AsianHandicap, + OfferType::DrawNoBet(_) => OfferCategory::DrawNoBet, + OfferType::SplitHandicap(_, _, _) => OfferCategory::SplitHandicap, OfferType::AnytimeGoalscorer => OfferCategory::AnytimeGoalscorer, OfferType::FirstGoalscorer => OfferCategory::FirstGoalscorer, OfferType::PlayerShotsOnTarget(_) => OfferCategory::PlayerShotsOnTarget, @@ -104,7 +119,7 @@ impl OfferType { } pub fn is_auxiliary(&self) -> bool { - matches!(self, OfferType::DrawNoBet(_)) + matches!(self, OfferType::DrawNoBet(_) | OfferType::SplitHandicap(_, _, _)) } } @@ -115,6 +130,7 @@ pub enum OfferCategory { CorrectScore, AsianHandicap, DrawNoBet, + SplitHandicap, AnytimeGoalscorer, FirstGoalscorer, PlayerShotsOnTarget, @@ -137,6 +153,7 @@ pub enum Player { pub enum Outcome { Win(Side, WinHandicap), Draw(DrawHandicap), + SplitWin(Side, DrawHandicap, WinHandicap), Under(u8), Over(u8), Score(Score), diff --git a/brumby-soccer/src/interval/query.rs b/brumby-soccer/src/interval/query.rs index bbc1fa1..82f5310 100644 --- a/brumby-soccer/src/interval/query.rs +++ b/brumby-soccer/src/interval/query.rs @@ -7,8 +7,8 @@ mod anytime_assist; mod anytime_goalscorer; mod correct_score; mod first_goalscorer; -mod win_draw; mod total_goals; +mod win_draw; #[derive(Debug)] pub enum QuerySpec { @@ -27,11 +27,13 @@ pub fn requirements(offer_type: &OfferType) -> Expansions { OfferType::TotalGoals(period, _) => total_goals::requirements(period), OfferType::CorrectScore(period) => correct_score::requirements(period), OfferType::AsianHandicap(period, _) => win_draw::requirements(period), - OfferType::DrawNoBet(_) => panic!("unsupported auxiliary {offer_type:?}"), OfferType::FirstGoalscorer => first_goalscorer::requirements(), OfferType::AnytimeGoalscorer => anytime_goalscorer::requirements(), OfferType::PlayerShotsOnTarget(_) => unimplemented!(), OfferType::AnytimeAssist => anytime_assist::requirements(), + OfferType::DrawNoBet(_) | OfferType::SplitHandicap(_, _, _) => { + panic!("unsupported auxiliary {offer_type:?}") + } } } @@ -47,27 +49,36 @@ pub fn prepare( OfferType::TotalGoals(_, _) => total_goals::prepare(), OfferType::CorrectScore(_) => correct_score::prepare(), OfferType::AsianHandicap(_, _) => win_draw::prepare(), - OfferType::DrawNoBet(_) => panic!("unsupported auxiliary {offer_type:?}"), OfferType::FirstGoalscorer => first_goalscorer::prepare(outcome, player_lookup), OfferType::AnytimeGoalscorer => anytime_goalscorer::prepare(outcome, player_lookup), OfferType::PlayerShotsOnTarget(_) => unimplemented!(), OfferType::AnytimeAssist => anytime_assist::prepare(outcome, player_lookup), + OfferType::DrawNoBet(_) | OfferType::SplitHandicap(_, _, _) => { + panic!("unsupported auxiliary {offer_type:?}") + } } } #[must_use] #[inline] -pub fn filter(offer_type: &OfferType, outcome: &Outcome, query: &QuerySpec, prospect: &Prospect) -> bool { +pub fn filter( + offer_type: &OfferType, + outcome: &Outcome, + query: &QuerySpec, + prospect: &Prospect, +) -> bool { match offer_type { OfferType::HeadToHead(period, _) => win_draw::filter(period, outcome, prospect), OfferType::TotalGoals(period, _) => total_goals::filter(period, outcome, prospect), OfferType::CorrectScore(period) => correct_score::filter(period, outcome, prospect), OfferType::AsianHandicap(period, _) => win_draw::filter(period, outcome, prospect), - OfferType::DrawNoBet(_) => panic!("unsupported auxiliary {offer_type:?}"), OfferType::AnytimeGoalscorer => anytime_goalscorer::filter(query, prospect), OfferType::FirstGoalscorer => first_goalscorer::filter(query, prospect), OfferType::PlayerShotsOnTarget(_) => unimplemented!(), OfferType::AnytimeAssist => anytime_assist::filter(query, prospect), + OfferType::DrawNoBet(_) | OfferType::SplitHandicap(_, _, _) => { + panic!("unsupported auxiliary {offer_type:?}") + } } } @@ -97,7 +108,11 @@ pub fn isolate_set( let queries = selections .iter() .map(|(offer_type, outcome)| { - (offer_type, outcome, prepare(offer_type, outcome, player_lookup)) + ( + offer_type, + outcome, + prepare(offer_type, outcome, player_lookup), + ) }) .collect::>(); prospects @@ -113,9 +128,9 @@ pub fn isolate_set( #[cfg(test)] mod tests { - use brumby::sv; use crate::domain::{DrawHandicap, Period, Score, Side, WinHandicap}; - use crate::interval::{explore, Config, BivariateProbs, TeamProbs, UnivariateProbs}; + use crate::interval::{explore, BivariateProbs, Config, TeamProbs, UnivariateProbs}; + use brumby::sv; use super::*; @@ -125,9 +140,20 @@ mod tests { &Config { intervals: 4, team_probs: TeamProbs { - h1_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 }, - h2_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 }, - assists: UnivariateProbs { home: 1.0, away: 1.0 }, + h1_goals: BivariateProbs { + home: 0.25, + away: 0.25, + common: 0.25, + }, + h2_goals: BivariateProbs { + home: 0.25, + away: 0.25, + common: 0.25, + }, + assists: UnivariateProbs { + home: 1.0, + away: 1.0, + }, }, player_probs: sv![], prune_thresholds: Default::default(), @@ -167,9 +193,20 @@ mod tests { &Config { intervals: 4, team_probs: TeamProbs { - h1_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 }, - h2_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 }, - assists: UnivariateProbs { home: 1.0, away: 1.0 }, + h1_goals: BivariateProbs { + home: 0.25, + away: 0.25, + common: 0.25, + }, + h2_goals: BivariateProbs { + home: 0.25, + away: 0.25, + common: 0.25, + }, + assists: UnivariateProbs { + home: 1.0, + away: 1.0, + }, }, player_probs: sv![], prune_thresholds: Default::default(), @@ -229,9 +266,20 @@ mod tests { &Config { intervals: 4, team_probs: TeamProbs { - h1_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 }, - h2_goals: BivariateProbs { home: 0.25, away: 0.25, common: 0.25 }, - assists: UnivariateProbs { home: 1.0, away: 1.0 }, + h1_goals: BivariateProbs { + home: 0.25, + away: 0.25, + common: 0.25, + }, + h2_goals: BivariateProbs { + home: 0.25, + away: 0.25, + common: 0.25, + }, + assists: UnivariateProbs { + home: 1.0, + away: 1.0, + }, }, player_probs: sv![], prune_thresholds: Default::default(), diff --git a/brumby-soccer/src/model.rs b/brumby-soccer/src/model.rs index 72561e4..38906ed 100644 --- a/brumby-soccer/src/model.rs +++ b/brumby-soccer/src/model.rs @@ -17,7 +17,9 @@ use brumby::sv; use brumby::timed::Timed; use crate::domain::validation::{InvalidOffer, InvalidOutcome, MissingOutcome, UnvalidatedOffer}; -use crate::domain::{Offer, OfferCategory, OfferType, Outcome, Over, Period, Player, Side}; +use crate::domain::{ + DrawHandicap, Offer, OfferCategory, OfferType, Outcome, Over, Period, Player, Side, WinHandicap, +}; use crate::interval; use crate::interval::query::{isolate, requirements}; use crate::interval::{ @@ -249,6 +251,16 @@ impl Model { &self.offers } + pub fn insert_offer(&mut self, offer: Offer) { + self.offers.insert(offer.offer_type.clone(), offer); + } + + fn get_offer(&self, offer_type: &OfferType) -> Result<&Offer, MissingOffer> { + self.offers + .get(&offer_type) + .ok_or(MissingOffer::Type(offer_type.clone())) + } + pub fn derive( &mut self, stubs: &[Stub], @@ -274,7 +286,7 @@ impl Model { start.elapsed(), caching_context.stats ); - self.offers.insert(offer.offer_type.clone(), offer); + self.insert_offer(offer); } } for stub in auxiliary_stubs { @@ -284,12 +296,13 @@ impl Model { stub.outcomes.len() ); let offer = match stub.offer_type { - OfferType::DrawNoBet(_) => { - self.derive_dnb(stub, price_bounds)? + OfferType::DrawNoBet(_) => self.derive_draw_no_bet(stub, price_bounds)?, + OfferType::SplitHandicap(_, _, _) => { + self.derive_split_handicap(stub, price_bounds)? } _ => unreachable!(), }; - self.offers.insert(offer.offer_type.clone(), offer); + self.insert_offer(offer); } Ok(caching_context.stats) }) @@ -409,7 +422,7 @@ impl Model { } #[inline(always)] - fn derive_dnb( + fn derive_draw_no_bet( &mut self, stub: &Stub, price_bounds: &PriceBounds, @@ -420,16 +433,12 @@ impl Model { }; let source_offer_type = OfferType::HeadToHead(Period::FullTime, draw_handicap.clone()); - let source_offer = - self.offers - .get(&source_offer_type) - .ok_or(SingleDerivationError::MissingOffer(MissingOffer::Type( - source_offer_type, - )))?; + let source_offer = self.get_offer(&source_offer_type)?; let home_outcome = Outcome::Win(Side::Home, draw_handicap.to_win_handicap()); let home_prob = source_offer.get_probability(&home_outcome).unwrap(); - let away_outcome = Outcome::Win(Side::Away, draw_handicap.to_win_handicap().flip_european()); + let away_outcome = + Outcome::Win(Side::Away, draw_handicap.to_win_handicap().flip_european()); let away_prob = source_offer.get_probability(&away_outcome).unwrap(); let mut probs = [home_prob, away_prob]; @@ -444,6 +453,86 @@ impl Model { }) } + #[inline(always)] + fn derive_split_handicap( + &mut self, + stub: &Stub, + price_bounds: &PriceBounds, + ) -> Result { + let (period, draw_handicap, win_handicap) = match stub.offer_type { + OfferType::SplitHandicap(ref period, ref draw_handicap, ref win_handicap) => { + (period, draw_handicap, win_handicap) + } + _ => unreachable!(), + }; + + let euro_offer_type = OfferType::HeadToHead(period.clone(), draw_handicap.clone()); + let euro_offer = self.get_offer(&euro_offer_type)?; + let asian_offer_type = OfferType::AsianHandicap(period.clone(), win_handicap.clone()); + let asian_offer = self.get_offer(&asian_offer_type)?; + + let draw_prob = euro_offer + .get_probability(&Outcome::Draw(draw_handicap.clone())) + .unwrap(); + let (home_prob, away_prob) = match (draw_handicap, win_handicap) { + (DrawHandicap::Ahead(ahead), WinHandicap::AheadOver(ahead_over)) => { + if ahead == ahead_over { + // -x.25 case + let asian_win_prob = asian_offer + .get_probability(&Outcome::Win(Side::Home, win_handicap.clone())) + .unwrap(); + let home_prob = asian_win_prob / (1.0 - 0.5 * draw_prob); + (home_prob, 1.0 - home_prob) + } else { + // -x.75 case + assert_eq!(*ahead, ahead_over + 1); + let euro_win_prob = euro_offer + .get_probability(&Outcome::Win(Side::Home, draw_handicap.to_win_handicap())) + .unwrap(); + let home_prob = (euro_win_prob + 0.5 * draw_prob) / (1.0 - 0.5 * draw_prob); + (home_prob, 1.0 - home_prob) + } + } + (_, WinHandicap::BehindUnder(behind_under)) => { + let behind = match draw_handicap { + DrawHandicap::Ahead(0) => 0, // Behind(0) is always written as Ahead(0) by convention + DrawHandicap::Behind(by) => *by, + _ => unreachable!() + }; + if behind == *behind_under { + // +x.75 case + let euro_win_prob = euro_offer + .get_probability(&Outcome::Win(Side::Away, draw_handicap.to_win_handicap().flip_european())) + .unwrap(); + let away_prob = (euro_win_prob + 0.5 * draw_prob) / (1.0 - 0.5 * draw_prob); + (1.0 - away_prob, away_prob) + } else { + // +x.25 case + assert_eq!(behind + 1, *behind_under); + let asian_win_prob = asian_offer + .get_probability(&Outcome::Win(Side::Away, win_handicap.flip_asian())) + .unwrap(); + let away_prob = asian_win_prob / (1.0 - 0.5 * draw_prob); + (1.0 - away_prob, away_prob) + } + } + _ => unreachable!(), + }; + + let home_outcome = Outcome::SplitWin(Side::Home, draw_handicap.clone(), win_handicap.clone()); + let away_outcome = Outcome::SplitWin(Side::Away, draw_handicap.flip(), win_handicap.flip_asian()); + let mut probs = [home_prob, away_prob]; + probs.normalise(stub.normal); + // trace!("DNB probs: {probs:?}, sum: {:.6}", probs.sum()); + let market = Market::frame(&stub.overround, probs.to_vec(), price_bounds); + let outcomes = [home_outcome, away_outcome]; + Ok(Offer { + offer_type: stub.offer_type.clone(), + outcomes: HashLookup::from(outcomes.to_vec()), + market, + }) + } + pub fn derive_multi( &self, selections: &[(OfferType, Outcome)], @@ -506,7 +595,10 @@ impl Model { } #[inline(always)] - fn scan_prefix(sorted_selections: &[DetailedSelection], exploration: &Exploration) -> ScanPrefixResult { + fn scan_prefix( + sorted_selections: &[DetailedSelection], + exploration: &Exploration, + ) -> ScanPrefixResult { let mut keep = Vec::with_capacity(sorted_selections.len()); let mut drop = vec![]; let mut lowest_prob = f64::MAX; @@ -514,15 +606,10 @@ impl Model { for end_index in 1..=sorted_selections.len() { let prefix = sorted_selections[0..end_index] .iter() - .map(|selection| { - (selection.offer_type.clone(), selection.outcome.clone()) - }) + .map(|selection| (selection.offer_type.clone(), selection.outcome.clone())) .collect::>(); - let prob = query::isolate_set( - &prefix, - &exploration.prospects, - &exploration.player_lookup, - ); + let prob = + query::isolate_set(&prefix, &exploration.prospects, &exploration.player_lookup); // if LOG { trace!("fringe prefix: {prefix:?}, prob: {prob:.3}"); } let tail = &sorted_selections[end_index - 1]; if prob < lowest_prob { @@ -553,7 +640,9 @@ impl Model { let mut query_elapsed = Duration::default(); for (offer_type, outcome) in selections { if offer_type.is_auxiliary() { - return Err(MultiDerivationError::AuxiliaryOffer(AuxiliaryOffer { offer_type: offer_type.clone() })); + return Err(MultiDerivationError::AuxiliaryOffer(AuxiliaryOffer { + offer_type: offer_type.clone(), + })); } self.collect_requirements( @@ -657,7 +746,8 @@ impl Model { // if LOG { // trace!("price: {single_price:.3}, prob: {single_prob:.3}, overround: {single_overround:.3}"); // } - let mut fringe_sorted_selections = Vec::with_capacity(scan_result.keep.len() + 1); + let mut fringe_sorted_selections = + Vec::with_capacity(scan_result.keep.len() + 1); fringe_sorted_selections.clone_from(&scan_result.keep); fringe_sorted_selections.push(selection); sort_selections_by_increasing_prob(&mut fringe_sorted_selections); @@ -709,7 +799,8 @@ impl Model { let fringe_exploration = fringe_exploration.unwrap(); let query_start = Instant::now(); - let fringe_scan_result = scan_prefix(&fringe_sorted_selections, &fringe_exploration); + let fringe_scan_result = + scan_prefix(&fringe_sorted_selections, &fringe_exploration); query_elapsed += query_start.elapsed(); let probability = fringe_scan_result.lowest_prob; @@ -893,3 +984,6 @@ fn frame_prices_from_exploration( market, } } + +#[cfg(test)] +mod tests; diff --git a/brumby-soccer/src/model/tests.rs b/brumby-soccer/src/model/tests.rs new file mode 100644 index 0000000..8518176 --- /dev/null +++ b/brumby-soccer/src/model/tests.rs @@ -0,0 +1,106 @@ +use rustc_hash::FxHashMap; +use stanza::renderer::console::Console; +use stanza::renderer::Renderer; +use crate::domain::{DrawHandicap, Offer, OfferType, Outcome, Period, Side, WinHandicap}; +use crate::model::{Config, Model, Stub}; +use brumby::hash_lookup::HashLookup; +use brumby::market::{Market, Overround, OverroundMethod, PriceBounds}; +use crate::print; + +const SINGLE_PRICE_BOUNDS: PriceBounds = 1.001..=1001.0; +const OVERROUND: Overround = Overround { + method: OverroundMethod::Multiplicative, + value: 1.0, +}; +const EPSILON: f64 = 1e06; + +fn create_test_model() -> Model { + Model::try_from(Config { + intervals: 8, + max_total_goals: 8, + }) + .unwrap() +} + +fn insert_head_to_head(model: &mut Model, draw_handicap: DrawHandicap, fair_prices: Vec) { + let outcomes = HashLookup::from(vec![ + Outcome::Win(Side::Home, draw_handicap.to_win_handicap()), + Outcome::Draw(draw_handicap.clone()), + Outcome::Win(Side::Away, draw_handicap.to_win_handicap().flip_european()), + ]); + model.insert_offer(Offer { + offer_type: OfferType::HeadToHead(Period::FullTime, draw_handicap), + outcomes, + market: Market::fit( + &OVERROUND.method, + fair_prices, + 1.0, + ), + }); +} + +fn insert_asian_handicap(model: &mut Model, win_handicap: WinHandicap, fair_prices: Vec) { + let outcomes = HashLookup::from(vec![ + Outcome::Win(Side::Home, win_handicap.clone()), + Outcome::Win(Side::Away, win_handicap.flip_asian()), + ]); + model.insert_offer(Offer { + offer_type: OfferType::AsianHandicap(Period::FullTime, win_handicap), + outcomes, + market: Market::fit( + &OverroundMethod::Multiplicative, + fair_prices, + 1.0, + ), + }); +} + +fn stub_split_handicap(draw_handicap: DrawHandicap, win_handicap: WinHandicap) -> Stub { + let outcomes = HashLookup::from([ + Outcome::SplitWin(Side::Home, draw_handicap.clone(), win_handicap.clone()), + Outcome::SplitWin(Side::Away, draw_handicap.flip(), win_handicap.flip_asian()) + ]); + Stub { + offer_type: OfferType::SplitHandicap(Period::FullTime, draw_handicap, win_handicap), + outcomes, + normal: 1.0, + overround: OVERROUND.clone(), + } +} + +#[test] +pub fn split_handicap_evenly_matched() { + let mut model = create_test_model(); + insert_head_to_head(&mut model, DrawHandicap::Ahead(2), vec![12.26, 8.2, 1.25]); + insert_head_to_head(&mut model, DrawHandicap::Ahead(1), vec![4.91, 4.88, 1.68]); + insert_head_to_head(&mut model, DrawHandicap::Ahead(0), vec![2.44, 4.13, 2.85]); + insert_head_to_head(&mut model, DrawHandicap::Behind(1), vec![1.53, 5.33, 6.15]); + insert_asian_handicap(&mut model, WinHandicap::AheadOver(0), vec![2.44, 1.68]); + insert_asian_handicap(&mut model, WinHandicap::BehindUnder(1), vec![1.53, 2.85]); + + model.derive(&[ + stub_split_handicap(DrawHandicap::Ahead(1), WinHandicap::AheadOver(0)), + stub_split_handicap(DrawHandicap::Ahead(0), WinHandicap::AheadOver(0)), + stub_split_handicap(DrawHandicap::Ahead(0), WinHandicap::BehindUnder(1)), + stub_split_handicap(DrawHandicap::Behind(1), WinHandicap::BehindUnder(1)), + ], &SINGLE_PRICE_BOUNDS).unwrap(); + + let offer = model.offers().get(&OfferType::SplitHandicap(Period::FullTime, DrawHandicap::Ahead(1), WinHandicap::AheadOver(0))).unwrap(); + // assert_slice_f64_relative(&[2.156, 1.865], &offer.market.prices, EPSILON); + + print_offers(model.offers()); +} + +fn print_offers(offers: &FxHashMap) { + for (_, offer) in sort_tuples(offers) { + let table = print::tabulate_offer(offer); + println!("{:?}:\n{}", offer.offer_type, Console::default().render(&table)) + } +} + +fn sort_tuples(tuples: impl IntoIterator) -> Vec<(K, V)> { + let tuples = tuples.into_iter(); + let mut tuples = tuples.collect::>(); + tuples.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); + tuples +} \ No newline at end of file diff --git a/brumby/src/market.rs b/brumby/src/market.rs index e411d7e..3813596 100644 --- a/brumby/src/market.rs +++ b/brumby/src/market.rs @@ -1,9 +1,11 @@ +use std::ops::RangeInclusive; + +use anyhow::bail; +use serde::{Deserialize, Serialize}; + use crate::opt; use crate::opt::UnivariateDescentConfig; use crate::probs::SliceExt; -use anyhow::bail; -use serde::{Deserialize, Serialize}; -use std::ops::RangeInclusive; pub type PriceBounds = RangeInclusive;