0e3c4b8d47
- Add KellyCalculator (Domain/Betting): pure fractional-Kelly stake from win probability, decimal odds, bankroll, and fraction (default quarter-Kelly). Returns 0 on non-positive edge; truncates the suggestion down to 2 decimals so it never exceeds the computed figure. 19 unit tests. - MyBets: add a page-local stake helper (bankroll + win-probability inputs) that suggests a quarter-Kelly stake from the form's rate, with an Apply button and a no-edge message. Win probability is user-supplied, not derived from a signal. - Localization (en+ru) for the Kelly helper and the export-hub keys (shared resx).
83 lines
4.0 KiB
C#
83 lines
4.0 KiB
C#
namespace Marathon.Domain.Betting;
|
||
|
||
/// <summary>
|
||
/// Pure fractional-Kelly stake sizing for a single back bet at decimal odds.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// The Kelly criterion maximises the long-run growth rate of a bankroll by staking
|
||
/// a fraction of it proportional to the edge. For decimal odds <c>o</c> and an
|
||
/// estimated win probability <c>p</c>, the full-Kelly fraction of bankroll is:
|
||
/// </para>
|
||
/// <code>f* = (p·o − 1) / (o − 1)</code>
|
||
/// <para>
|
||
/// When <c>f*</c> is zero or negative there is no positive expected value, and the
|
||
/// suggested stake is <c>0</c> — the calculator never recommends betting into a
|
||
/// negative-EV price. Most disciplined bettors stake a <i>fraction</i> of full
|
||
/// Kelly (e.g. quarter-Kelly, <c>fraction = 0.25</c>) to cut variance and blunt the
|
||
/// impact of probability-estimation error; full Kelly is famously over-aggressive
|
||
/// once <c>p</c> is even slightly wrong.
|
||
/// </para>
|
||
/// <para>
|
||
/// The win probability is an input the bettor supplies — it is intentionally NOT
|
||
/// derived from an anomaly score here, so the calculator stays a pure, reusable
|
||
/// money-management primitive independent of any signal source.
|
||
/// </para>
|
||
/// </remarks>
|
||
public static class KellyCalculator
|
||
{
|
||
/// <summary>Default Kelly fraction: quarter-Kelly — the conventional variance-safe choice.</summary>
|
||
public const decimal DefaultFraction = 0.25m;
|
||
|
||
/// <summary>
|
||
/// Full-Kelly fraction of bankroll <c>(p·o − 1)/(o − 1)</c>. May be negative or
|
||
/// zero, signalling no positive edge. Exposed for callers that want the raw
|
||
/// figure (e.g. to display the edge) rather than a clamped stake.
|
||
/// </summary>
|
||
/// <param name="winProbability">Estimated win probability in the closed interval [0, 1].</param>
|
||
/// <param name="decimalOdds">Decimal odds, strictly greater than 1.0.</param>
|
||
public static decimal FullKellyFraction(decimal winProbability, decimal decimalOdds)
|
||
{
|
||
if (winProbability is < 0m or > 1m)
|
||
throw new ArgumentOutOfRangeException(nameof(winProbability), winProbability, "Must be in [0, 1].");
|
||
if (decimalOdds <= 1m)
|
||
throw new ArgumentOutOfRangeException(nameof(decimalOdds), decimalOdds, "Decimal odds must be greater than 1.0.");
|
||
|
||
return (winProbability * decimalOdds - 1m) / (decimalOdds - 1m);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Suggested stake using fractional Kelly, rounded down to two decimals so the
|
||
/// suggestion is never larger than the theoretical figure. Returns <c>0</c> when
|
||
/// there is no positive edge.
|
||
/// </summary>
|
||
/// <param name="winProbability">Estimated win probability in the open interval (0, 1).</param>
|
||
/// <param name="decimalOdds">Decimal odds, strictly greater than 1.0.</param>
|
||
/// <param name="bankroll">Total bankroll; must be non-negative.</param>
|
||
/// <param name="fraction">Kelly fraction in (0, 1]; defaults to <see cref="DefaultFraction"/>.</param>
|
||
public static decimal SuggestStake(
|
||
decimal winProbability,
|
||
decimal decimalOdds,
|
||
decimal bankroll,
|
||
decimal fraction = DefaultFraction)
|
||
{
|
||
if (winProbability is <= 0m or >= 1m)
|
||
throw new ArgumentOutOfRangeException(nameof(winProbability), winProbability, "Must be in the open interval (0, 1).");
|
||
if (bankroll < 0m)
|
||
throw new ArgumentOutOfRangeException(nameof(bankroll), bankroll, "Must be non-negative.");
|
||
if (fraction is <= 0m or > 1m)
|
||
throw new ArgumentOutOfRangeException(nameof(fraction), fraction, "Kelly fraction must be in (0, 1].");
|
||
|
||
// FullKellyFraction validates decimalOdds.
|
||
var full = FullKellyFraction(winProbability, decimalOdds);
|
||
if (full <= 0m)
|
||
return 0m;
|
||
|
||
var stake = fraction * full * bankroll;
|
||
|
||
// Truncate (floor toward zero) to two decimals so a stake suggestion never
|
||
// exceeds the computed figure — a conservative bias for real-money sizing.
|
||
return Math.Truncate(stake * 100m) / 100m;
|
||
}
|
||
}
|