feat: Kelly criterion stake sizing (domain + MyBets helper)
- 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).
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
@page "/my-bets"
|
||||
@using Marathon.Application.Betting
|
||||
@using Marathon.Domain.Betting
|
||||
@implements IDisposable
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
@inject IBetJournalService Service
|
||||
@@ -184,6 +185,50 @@
|
||||
data-test="journal-add-stake" />
|
||||
</div>
|
||||
|
||||
<div class="m-journal__form-field m-journal__form-field--wide" data-test="journal-kelly">
|
||||
<label class="m-journal__form-label">@L["Journal.Kelly.Title"]</label>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:var(--m-space-3); align-items:flex-end;">
|
||||
<div style="flex:1 1 150px;">
|
||||
<span class="m-journal__form-hint">@L["Journal.Kelly.Bankroll"]</span>
|
||||
<MudNumericField T="decimal?"
|
||||
@bind-Value="_kellyBankroll"
|
||||
Min="0m" Step="50m"
|
||||
Variant="Variant.Outlined"
|
||||
data-test="journal-kelly-bankroll" />
|
||||
</div>
|
||||
<div style="flex:1 1 150px;">
|
||||
<span class="m-journal__form-hint">@L["Journal.Kelly.Probability"]</span>
|
||||
<MudNumericField T="decimal?"
|
||||
@bind-Value="_kellyProbabilityPercent"
|
||||
Min="0m" Max="100m" Step="1m"
|
||||
Variant="Variant.Outlined"
|
||||
data-test="journal-kelly-prob" />
|
||||
</div>
|
||||
</div>
|
||||
@if (KellySuggestion is { } suggestion)
|
||||
{
|
||||
@if (suggestion > 0m)
|
||||
{
|
||||
<div style="margin-top:var(--m-space-2); display:flex; align-items:center; gap:var(--m-space-3); flex-wrap:wrap;">
|
||||
<span class="m-journal__form-hint" data-test="journal-kelly-suggestion">
|
||||
@string.Format(CultureInfo.CurrentCulture, L["Journal.Kelly.Suggestion"].Value, suggestion)
|
||||
</span>
|
||||
<button type="button" class="m-chip" @onclick="ApplyKellyStake" data-test="journal-kelly-apply">
|
||||
@L["Journal.Kelly.Apply"]
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="m-journal__form-hint" data-test="journal-kelly-noedge">@L["Journal.Kelly.NoEdge"]</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="m-journal__form-hint">@L["Journal.Kelly.Hint"]</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="m-journal__form-field m-journal__form-field--wide">
|
||||
<label class="m-journal__form-label">@L["Journal.Field.Notes"]</label>
|
||||
<MudTextField T="string"
|
||||
@@ -644,6 +689,33 @@
|
||||
private string? _formError;
|
||||
private Guid? _pendingDeleteId;
|
||||
private AddBetForm _form = new();
|
||||
|
||||
// Kelly stake-helper state — page-local (not persisted with the bet). Bankroll
|
||||
// intentionally survives form resets so it carries across successive entries.
|
||||
private decimal? _kellyBankroll;
|
||||
private decimal? _kellyProbabilityPercent;
|
||||
|
||||
/// <summary>
|
||||
/// Quarter-Kelly suggested stake from the entered bankroll + win probability and
|
||||
/// the form's current rate. Null when inputs are incomplete/invalid; 0 when the
|
||||
/// price carries no positive edge.
|
||||
/// </summary>
|
||||
private decimal? KellySuggestion
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_kellyBankroll is not { } bankroll || bankroll <= 0m) return null;
|
||||
if (_kellyProbabilityPercent is not { } pct || pct <= 0m || pct >= 100m) return null;
|
||||
if (_form.Rate <= 1m) return null;
|
||||
return KellyCalculator.SuggestStake(pct / 100m, _form.Rate, bankroll, KellyCalculator.DefaultFraction);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyKellyStake()
|
||||
{
|
||||
if (KellySuggestion is { } s && s > 0m)
|
||||
_form.Stake = s;
|
||||
}
|
||||
private CancellationTokenSource? _loadCts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
|
||||
Reference in New Issue
Block a user