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)
}
const MAX_HEDGE_SCAN_POINTS: i64 = 200_000;
fn scan_stride(max_inclusive: i64) -> i64 {
if max_inclusive <= 0 {
return 1;
}
let span = max_inclusive.saturating_add(1);
if span <= MAX_HEDGE_SCAN_POINTS {
1
} else {
(span + MAX_HEDGE_SCAN_POINTS - 1) / MAX_HEDGE_SCAN_POINTS
}
}
fn scan_range_with_stride(start: i64, end: i64, stride: i64, mut f: impl FnMut(i64)) {
if start > end {
return;
}
let step = stride.max(1);
let mut x = start;
let mut last = start.saturating_sub(1);
while x <= end {
f(x);
last = x;
let next = x.saturating_add(step);
if next <= x {
break;
}
x = next;
}
if last != end {
f(end);
}
}
#[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];
if odds_b.0 <= 10_000 || odds_c.0 <= 10_000 {
return HedgeResult::NoSolution {
reason: "Invalid odds (must be > 1.0)",
};
}
let s = input.user_stake.0;
let slack = input.max_loss.0.max(0);
let size_b = size_b.0.max(0);
let size_c = size_c.0.max(0);
let liability = profit_quanta(input.user_stake, input.user_odds).max(0);
let min_hedge_required = liability.saturating_sub(slack);
if size_b.saturating_add(size_c) < min_hedge_required {
return HedgeResult::NoSolution {
reason: "Insufficient liquidity",
};
}
let mut best: Option<(i64, i64, i64)> = None; let scan_b_end = size_b.min(
max_stake_for_profit_limit(s.saturating_add(size_c).saturating_add(slack), odds_b).max(0),
);
let scan_c_end = size_c.min(
max_stake_for_profit_limit(s.saturating_add(size_b).saturating_add(slack), odds_c).max(0),
);
let scan_tb = scan_b_end <= scan_c_end;
if scan_tb {
let eval_tb = |tb: i64, best: &mut Option<(i64, i64, i64)>| {
let lower_a = min_hedge_required.saturating_sub(tb);
let lower_b = profit_quanta(Money(tb), odds_b).saturating_sub(s.saturating_add(slack));
let tc_min = lower_a.max(lower_b).max(0);
let tc_max = size_c.min(max_stake_for_profit_limit(
s.saturating_add(tb).saturating_add(slack),
odds_c,
));
if tc_min > tc_max {
return;
}
let tc = tc_min;
let worst_pnl = back_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
if worst_pnl < -slack {
return;
}
choose_better_candidate(best, tb, tc, worst_pnl);
};
let stride = scan_stride(scan_b_end);
scan_range_with_stride(0, scan_b_end, stride, |tb| eval_tb(tb, &mut best));
if stride > 1 {
let offset = stride / 2;
if offset > 0 {
scan_range_with_stride(offset.min(scan_b_end), scan_b_end, stride, |tb| {
eval_tb(tb, &mut best)
});
}
let center = best
.map(|(tb, _, _)| tb)
.unwrap_or(min_hedge_required.clamp(0, scan_b_end));
let lo = center.saturating_sub(stride).max(0);
let hi = center.saturating_add(stride).min(scan_b_end);
scan_range_with_stride(lo, hi, 1, |tb| eval_tb(tb, &mut best));
}
} else {
let eval_tc = |tc: i64, best: &mut Option<(i64, i64, i64)>| {
let lower_a = min_hedge_required.saturating_sub(tc);
let lower_c = profit_quanta(Money(tc), odds_c).saturating_sub(s.saturating_add(slack));
let tb_min = lower_a.max(lower_c).max(0);
let tb_max = size_b.min(max_stake_for_profit_limit(
s.saturating_add(tc).saturating_add(slack),
odds_b,
));
if tb_min > tb_max {
return;
}
let tb = tb_min;
let worst_pnl = back_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
if worst_pnl < -slack {
return;
}
choose_better_candidate(best, tb, tc, worst_pnl);
};
let stride = scan_stride(scan_c_end);
scan_range_with_stride(0, scan_c_end, stride, |tc| eval_tc(tc, &mut best));
if stride > 1 {
let offset = stride / 2;
if offset > 0 {
scan_range_with_stride(offset.min(scan_c_end), scan_c_end, stride, |tc| {
eval_tc(tc, &mut best)
});
}
let center = best
.map(|(_, tc, _)| tc)
.unwrap_or(min_hedge_required.clamp(0, scan_c_end));
let lo = center.saturating_sub(stride).max(0);
let hi = center.saturating_add(stride).min(scan_c_end);
scan_range_with_stride(lo, hi, 1, |tc| eval_tc(tc, &mut best));
}
}
let Some((tb, tc, worst_pnl)) = best else {
return HedgeResult::NoSolution {
reason: "No valid hedge exists for BACK order",
};
};
HedgeResult::Success {
legs: vec![
HedgeLeg {
runner_id: runner_b,
odds: odds_b,
stake: Money(tb),
side: Side::No, },
HedgeLeg {
runner_id: runner_c,
odds: odds_c,
stake: Money(tc),
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];
if odds_b.0 <= 10_000 || odds_c.0 <= 10_000 {
return HedgeResult::NoSolution {
reason: "Invalid odds (must be > 1.0)",
};
}
let s = input.user_stake.0;
let slack = input.max_loss.0.max(0);
let size_b = size_b.0.max(0);
let size_c = size_c.0.max(0);
let liability = profit_quanta(input.user_stake, input.user_odds).max(0);
let unhedged_worst = liability.min(-s);
if unhedged_worst >= -slack {
return HedgeResult::Success {
legs: vec![
HedgeLeg {
runner_id: runner_b,
odds: odds_b,
stake: Money(0),
side: Side::Yes,
},
HedgeLeg {
runner_id: runner_c,
odds: odds_c,
stake: Money(0),
side: Side::Yes,
},
],
worst_case_pnl: Money(unhedged_worst),
};
}
let mut best: Option<(i64, i64, i64)> = None; let max_total_hedge = liability.saturating_add(slack);
let scan_b_end = size_b.min(max_total_hedge);
let scan_c_end = size_c.min(max_total_hedge);
let scan_tb = scan_b_end <= scan_c_end;
if scan_tb {
let eval_tb = |tb: i64, best: &mut Option<(i64, i64, i64)>| {
let tc_min =
min_stake_for_profit_at_least(s.saturating_add(tb).saturating_sub(slack), odds_c);
let tc_max = size_c
.min(liability.saturating_add(slack).saturating_sub(tb))
.min(
profit_quanta(Money(tb), odds_b)
.saturating_add(slack)
.saturating_sub(s),
);
if tc_min > tc_max {
return;
}
let tc = tc_min.max(0);
if tc > tc_max {
return;
}
let worst_pnl = lay_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
if worst_pnl < -slack {
return;
}
choose_better_candidate(best, tb, tc, worst_pnl);
};
let stride = scan_stride(scan_b_end);
scan_range_with_stride(0, scan_b_end, stride, |tb| eval_tb(tb, &mut best));
if stride > 1 {
let offset = stride / 2;
if offset > 0 {
scan_range_with_stride(offset.min(scan_b_end), scan_b_end, stride, |tb| {
eval_tb(tb, &mut best)
});
}
let center = best.map(|(tb, _, _)| tb).unwrap_or(0);
let lo = center.saturating_sub(stride).max(0);
let hi = center.saturating_add(stride).min(scan_b_end);
scan_range_with_stride(lo, hi, 1, |tb| eval_tb(tb, &mut best));
}
} else {
let eval_tc = |tc: i64, best: &mut Option<(i64, i64, i64)>| {
let tb_min =
min_stake_for_profit_at_least(s.saturating_add(tc).saturating_sub(slack), odds_b);
let tb_max = size_b
.min(liability.saturating_add(slack).saturating_sub(tc))
.min(
profit_quanta(Money(tc), odds_c)
.saturating_add(slack)
.saturating_sub(s),
);
if tb_min > tb_max {
return;
}
let tb = tb_min.max(0);
if tb > tb_max {
return;
}
let worst_pnl = lay_worst_pnl(liability, s, odds_b, odds_c, tb, tc);
if worst_pnl < -slack {
return;
}
choose_better_candidate(best, tb, tc, worst_pnl);
};
let stride = scan_stride(scan_c_end);
scan_range_with_stride(0, scan_c_end, stride, |tc| eval_tc(tc, &mut best));
if stride > 1 {
let offset = stride / 2;
if offset > 0 {
scan_range_with_stride(offset.min(scan_c_end), scan_c_end, stride, |tc| {
eval_tc(tc, &mut best)
});
}
let center = best.map(|(_, tc, _)| tc).unwrap_or(0);
let lo = center.saturating_sub(stride).max(0);
let hi = center.saturating_add(stride).min(scan_c_end);
scan_range_with_stride(lo, hi, 1, |tc| eval_tc(tc, &mut best));
}
}
let Some((tb, tc, worst_pnl)) = best else {
return HedgeResult::NoSolution {
reason: "No valid hedge exists for LAY order",
};
};
HedgeResult::Success {
legs: vec![
HedgeLeg {
runner_id: runner_b,
odds: odds_b,
stake: Money(tb),
side: Side::Yes, },
HedgeLeg {
runner_id: runner_c,
odds: odds_c,
stake: Money(tc),
side: Side::Yes,
},
],
worst_case_pnl: Money(worst_pnl),
}
}
fn choose_better_candidate(best: &mut Option<(i64, i64, i64)>, tb: i64, tc: i64, worst_pnl: i64) {
let total = tb.saturating_add(tc);
match best {
None => *best = Some((tb, tc, worst_pnl)),
Some((best_tb, best_tc, best_worst)) => {
let best_total = best_tb.saturating_add(*best_tc);
if worst_pnl > *best_worst || (worst_pnl == *best_worst && total < best_total) {
*best = Some((tb, tc, worst_pnl));
}
}
}
}
fn max_stake_for_profit_limit(limit: i64, odds: OddsX10000) -> i64 {
if limit < 0 {
return -1;
}
let k = odds.0 as i128 - 10_000;
if k <= 0 {
return i64::MAX;
}
let num = ((limit as i128 + 1) * 10_000).saturating_sub(1);
clamp_i128_to_i64(num / k)
}
fn min_stake_for_profit_at_least(required: i64, odds: OddsX10000) -> i64 {
if required <= 0 {
return 0;
}
let k = odds.0 as i128 - 10_000;
if k <= 0 {
return i64::MAX;
}
let num = (required as i128) * 10_000;
clamp_i128_to_i64((num + k - 1) / k)
}
fn back_worst_pnl(
liability: i64,
user_stake: i64,
odds_b: OddsX10000,
odds_c: OddsX10000,
tb: i64,
tc: i64,
) -> i64 {
let pnl_a = -liability + tb + tc;
let pnl_b = user_stake + tc - profit_quanta(Money(tb), odds_b);
let pnl_c = user_stake + tb - profit_quanta(Money(tc), odds_c);
pnl_a.min(pnl_b).min(pnl_c)
}
fn lay_worst_pnl(
liability: i64,
user_stake: i64,
odds_b: OddsX10000,
odds_c: OddsX10000,
tb: i64,
tc: i64,
) -> i64 {
let pnl_a = liability - tb - tc;
let pnl_b = -user_stake + profit_quanta(Money(tb), odds_b) - tc;
let pnl_c = -user_stake - tb + profit_quanta(Money(tc), odds_c);
pnl_a.min(pnl_b).min(pnl_c)
}