use crate::book::protocol::command::Side;
use crate::types::{Money, OddsX10000, RunnerId};
fn clamp_i128_to_i64(v: i128) -> i64 {
if v > i64::MAX as i128 {
i64::MAX
} else if v < i64::MIN as i128 {
i64::MIN
} else {
v as i64
}
}
fn profit_quanta(stake: Money, odds: OddsX10000) -> i64 {
let stake_i = stake.0 as i128;
let odds_i = odds.0 as i128;
let profit = (stake_i * (odds_i - 10_000)) / 10_000;
clamp_i128_to_i64(profit)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HedgeLeg {
pub runner_id: RunnerId,
pub odds: OddsX10000,
pub stake: Money,
pub side: Side,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HedgeResult {
Success {
legs: Vec<HedgeLeg>,
worst_case_pnl: Money,
},
NoSolution { reason: &'static str },
}
#[derive(Debug, Clone)]
pub struct HedgeInput {
pub target_runner: RunnerId,
pub user_side: Side,
pub user_odds: OddsX10000,
pub user_stake: Money,
pub other_runners: Vec<(RunnerId, OddsX10000, Money)>, pub max_loss: Money,
}
pub fn calculate_3runner_hedge(input: &HedgeInput) -> HedgeResult {
if input.other_runners.len() != 2 {
return HedgeResult::NoSolution {
reason: "Expected exactly 2 other runners for 3-runner market",
};
}
match input.user_side {
Side::Yes => calculate_back_hedge(input),
Side::No => calculate_lay_hedge(input),
}
}
fn calculate_back_hedge(input: &HedgeInput) -> HedgeResult {
let (runner_b, odds_b, size_b) = input.other_runners[0];
let (runner_c, odds_c, size_c) = input.other_runners[1];
let ob = odds_b.to_decimal();
let oc = odds_c.to_decimal();
if ob <= 1.0 || oc <= 1.0 {
return HedgeResult::NoSolution {
reason: "Invalid odds (must be > 1.0)",
};
}
let s = input.user_stake.0 as f64;
let slack = input.max_loss.0 as f64;
let liability = profit_quanta(input.user_stake, input.user_odds) as f64;
let min_hedge_required = (liability - slack).max(0.0);
let target = min_hedge_required;
let max_tb = (s + target + slack) / ob;
let max_tc = (s + target + slack) / oc;
let min_tb = (((oc - 1.0) * target - s - slack) / oc).max(0.0);
let min_tc = (((ob - 1.0) * target - s - slack) / ob).max(0.0);
if min_tb > max_tb || min_tc > max_tc || min_tb + min_tc > target {
return HedgeResult::NoSolution {
reason: "Insufficient hedge capacity within risk tolerance",
};
}
let (tb, tc) = find_valid_split_with_bounds(target, min_tb, max_tb, min_tc, max_tc);
if tb < 0.0 || tc < 0.0 {
return HedgeResult::NoSolution {
reason: "Could not find non-negative hedge stakes",
};
}
let tb_money = Money((tb.round() as i64).max(0));
let tc_money = Money((tc.round() as i64).max(0));
if tb_money.0 > size_b.0 {
return HedgeResult::NoSolution {
reason: "Insufficient liquidity on runner B",
};
}
if tc_money.0 > size_c.0 {
return HedgeResult::NoSolution {
reason: "Insufficient liquidity on runner C",
};
}
let pnl_a = -profit_quanta(input.user_stake, input.user_odds) + tb_money.0 + tc_money.0;
let pnl_b = input.user_stake.0 + tc_money.0 - profit_quanta(tb_money, odds_b);
let pnl_c = input.user_stake.0 + tb_money.0 - profit_quanta(tc_money, odds_c);
let worst_pnl = pnl_a.min(pnl_b).min(pnl_c);
if worst_pnl < -(input.max_loss.0) {
return HedgeResult::NoSolution {
reason: "Calculated hedge exceeds risk tolerance",
};
}
HedgeResult::Success {
legs: vec![
HedgeLeg {
runner_id: runner_b,
odds: odds_b,
stake: tb_money,
side: Side::No, },
HedgeLeg {
runner_id: runner_c,
odds: odds_c,
stake: tc_money,
side: Side::No,
},
],
worst_case_pnl: Money(worst_pnl),
}
}
fn calculate_lay_hedge(input: &HedgeInput) -> HedgeResult {
let (runner_b, odds_b, size_b) = input.other_runners[0];
let (runner_c, odds_c, size_c) = input.other_runners[1];
let ob = odds_b.to_decimal();
let oc = odds_c.to_decimal();
if ob <= 1.0 || oc <= 1.0 {
return HedgeResult::NoSolution {
reason: "Invalid odds (must be > 1.0)",
};
}
let s = input.user_stake.0 as f64;
let slack = input.max_loss.0 as f64;
let profit_if_a_wins = profit_quanta(input.user_stake, input.user_odds) as f64;
let max_total = profit_if_a_wins;
let result = find_lay_hedge_stakes(s, slack, ob, oc, max_total);
let (tb, tc) = match result {
Some((tb, tc)) => (tb, tc),
None => {
return HedgeResult::NoSolution {
reason: "No valid hedge exists for LAY order",
};
}
};
let profit_budget = profit_quanta(input.user_stake, input.user_odds);
let tb_floor = tb.floor().max(0.0) as i64;
let tb_ceil = tb.ceil().max(0.0) as i64;
let tc_floor = tc.floor().max(0.0) as i64;
let tc_ceil = tc.ceil().max(0.0) as i64;
let tb_min = tb_floor.saturating_sub(2);
let tb_max = tb_ceil.saturating_add(2);
let tc_min = tc_floor.saturating_sub(2);
let tc_max = tc_ceil.saturating_add(2);
let mut best: Option<(Money, Money, i64)> = None; let mut blocked_b = false;
let mut blocked_c = false;
for tb_i in tb_min..=tb_max {
for tc_i in tc_min..=tc_max {
let tb_money = Money(tb_i);
let tc_money = Money(tc_i);
if tb_money.0.saturating_add(tc_money.0) > profit_budget {
continue;
}
let pnl_a = profit_budget - tb_money.0 - tc_money.0;
let pnl_b = -input.user_stake.0 + profit_quanta(tb_money, odds_b) - tc_money.0;
let pnl_c = -input.user_stake.0 - tb_money.0 + profit_quanta(tc_money, odds_c);
let worst_pnl = pnl_a.min(pnl_b).min(pnl_c);
if worst_pnl < -(input.max_loss.0) {
continue;
}
if tb_money.0 > size_b.0 {
blocked_b = true;
continue;
}
if tc_money.0 > size_c.0 {
blocked_c = true;
continue;
}
best = match best {
None => Some((tb_money, tc_money, worst_pnl)),
Some((best_tb, best_tc, best_worst)) => {
let best_total = best_tb.0 + best_tc.0;
let total = tb_money.0 + tc_money.0;
if worst_pnl > best_worst || (worst_pnl == best_worst && total < best_total) {
Some((tb_money, tc_money, worst_pnl))
} else {
Some((best_tb, best_tc, best_worst))
}
}
};
}
}
let Some((tb_money, tc_money, worst_pnl)) = best else {
if blocked_b || blocked_c {
return HedgeResult::NoSolution {
reason: "Insufficient liquidity",
};
}
return HedgeResult::NoSolution {
reason: "Calculated hedge exceeds risk tolerance",
};
};
HedgeResult::Success {
legs: vec![
HedgeLeg {
runner_id: runner_b,
odds: odds_b,
stake: tb_money,
side: Side::Yes, },
HedgeLeg {
runner_id: runner_c,
odds: odds_c,
stake: tc_money,
side: Side::Yes,
},
],
worst_case_pnl: Money(worst_pnl),
}
}
fn find_lay_hedge_stakes(
s: f64,
slack: f64,
ob: f64,
oc: f64,
max_total: f64,
) -> Option<(f64, f64)> {
let min_tb_base = (s - slack) / (ob - 1.0);
let min_tc_base = (s - slack) / (oc - 1.0);
if min_tb_base <= 0.0 && min_tc_base <= 0.0 {
let total = max_total.min(s); return Some((total / 2.0, total / 2.0));
}
let mut tb = min_tb_base.max(0.0);
let mut tc = min_tc_base.max(0.0);
for _ in 0..10 {
let new_tb = ((s + tc - slack) / (ob - 1.0)).max(0.0);
let new_tc = ((s + tb - slack) / (oc - 1.0)).max(0.0);
if (new_tb - tb).abs() < 0.01 && (new_tc - tc).abs() < 0.01 {
break;
}
tb = new_tb;
tc = new_tc;
}
if tb + tc > max_total {
return None;
}
if tb < 0.0 || tc < 0.0 {
return None;
}
Some((tb, tc))
}
fn find_valid_split_with_bounds(
target: f64,
min_tb: f64,
max_tb: f64,
min_tc: f64,
max_tc: f64,
) -> (f64, f64) {
let tb_lower = min_tb.max(target - max_tc);
let tb_upper = max_tb.min(target - min_tc);
if tb_lower > tb_upper {
return (-1.0, -1.0); }
let tb = (tb_lower + tb_upper) / 2.0;
let tc = target - tb;
(tb, tc)
}