f622dadf95
Adds a background forward-test engine that records flat-stake "paper" bets for directional anomalies as they fire and settles them when results arrive, measuring the detector's live, out-of-sample edge — the antidote to backtest overfitting. The results UI is a follow-up. - Domain: PaperBet entity (Rate>1 / Stake>0 invariants, Open factory, SettleAgainst — Won pays stake x rate, else Lost) + AnomalyEvidenceSide.RateFor. - Application: OpenPaperBetsUseCase (directional + score gate, dedups by AnomalyId, picks the post-flip favourite and its locked-in rate) and SettlePaperBetsUseCase (Won when pick == winner else Lost; ungraded events stay open; batched result lookup). - Infrastructure: PaperBetEntity + config (TEXT decimals, unique AnomalyId index, Outcome index), repository, mapping, additive AddPaperBets migration, and PaperTradingWorker (config-gated, baseline since-marker, open+settle per cycle). - Config: PaperTradingOptions / appsettings PaperTrading (Enabled:false default). - 25 tests: domain settlement, both use cases, and a real-SQLite round-trip incl. the unique-AnomalyId double-open backstop.
82 lines
2.4 KiB
C#
82 lines
2.4 KiB
C#
using FluentAssertions;
|
|
using Marathon.Domain.Entities;
|
|
using Marathon.Domain.Enums;
|
|
using Marathon.Domain.ValueObjects;
|
|
|
|
namespace Marathon.Domain.Tests.Entities;
|
|
|
|
public sealed class PaperBetTests
|
|
{
|
|
private static readonly DateTimeOffset Opened = new(2026, 5, 20, 18, 0, 0, TimeSpan.FromHours(3));
|
|
|
|
private static PaperBet OpenBet(Side pick = Side.Side1, decimal rate = 2.00m, decimal stake = 10m) =>
|
|
PaperBet.Open(Guid.NewGuid(), new EventId("26000001"), pick, rate, stake, Opened);
|
|
|
|
[Fact]
|
|
public void Open_CreatesPendingBet_WithIdentity()
|
|
{
|
|
var bet = OpenBet();
|
|
|
|
bet.Id.Should().NotBe(Guid.Empty);
|
|
bet.Outcome.Should().Be(BetOutcome.Pending);
|
|
bet.IsOpen.Should().BeTrue();
|
|
bet.SettledAt.Should().BeNull();
|
|
bet.Payout.Should().BeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1.0)]
|
|
[InlineData(0.5)]
|
|
public void Constructor_Throws_When_RateNotAboveOne(double rate)
|
|
{
|
|
var act = () => OpenBet(rate: (decimal)rate);
|
|
act.Should().Throw<ArgumentOutOfRangeException>();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(-5)]
|
|
public void Constructor_Throws_When_StakeNotPositive(double stake)
|
|
{
|
|
var act = () => OpenBet(stake: (decimal)stake);
|
|
act.Should().Throw<ArgumentOutOfRangeException>();
|
|
}
|
|
|
|
[Fact]
|
|
public void SettleAgainst_Win_PaysStakeTimesRate()
|
|
{
|
|
var bet = OpenBet(pick: Side.Side1, rate: 2.00m, stake: 10m);
|
|
var settledAt = Opened.AddHours(2);
|
|
|
|
var settled = bet.SettleAgainst(Side.Side1, settledAt);
|
|
|
|
settled.Outcome.Should().Be(BetOutcome.Won);
|
|
settled.Payout.Should().Be(20m);
|
|
settled.SettledAt.Should().Be(settledAt);
|
|
settled.IsOpen.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void SettleAgainst_Loss_PaysZero()
|
|
{
|
|
var settled = OpenBet(pick: Side.Side1).SettleAgainst(Side.Side2, Opened.AddHours(2));
|
|
|
|
settled.Outcome.Should().Be(BetOutcome.Lost);
|
|
settled.Payout.Should().Be(0m);
|
|
}
|
|
|
|
[Fact]
|
|
public void SettleAgainst_PreservesIdentityStakeAndRate()
|
|
{
|
|
var bet = OpenBet(rate: 2.00m, stake: 10m);
|
|
|
|
var settled = bet.SettleAgainst(Side.Side1, Opened.AddHours(1));
|
|
|
|
settled.Id.Should().Be(bet.Id);
|
|
settled.AnomalyId.Should().Be(bet.AnomalyId);
|
|
settled.EventId.Should().Be(bet.EventId);
|
|
settled.Stake.Should().Be(10m);
|
|
settled.Rate.Should().Be(2.00m);
|
|
}
|
|
}
|