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.
78 lines
2.7 KiB
C#
78 lines
2.7 KiB
C#
using Marathon.Application.Abstractions;
|
|
using Marathon.Domain.Entities;
|
|
using Marathon.Domain.Enums;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Marathon.Infrastructure.Persistence.Repositories;
|
|
|
|
internal sealed class PaperBetRepository : IPaperBetRepository
|
|
{
|
|
private readonly MarathonDbContext _db;
|
|
|
|
public PaperBetRepository(MarathonDbContext db) => _db = db;
|
|
|
|
public async Task<PaperBet?> GetAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var idStr = key.ToString();
|
|
var entity = await _db.PaperBets.AsNoTracking()
|
|
.FirstOrDefaultAsync(b => b.Id == idStr, ct);
|
|
return entity is null ? null : Mapping.ToDomain(entity);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PaperBet>> ListAsync(CancellationToken ct = default)
|
|
{
|
|
var entities = await _db.PaperBets.AsNoTracking()
|
|
.OrderByDescending(b => b.OpenedAt)
|
|
.ToListAsync(ct);
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<PaperBet>> ListByOutcomeAsync(BetOutcome outcome, CancellationToken ct = default)
|
|
{
|
|
var outcomeInt = (int)outcome;
|
|
var entities = await _db.PaperBets.AsNoTracking()
|
|
.Where(b => b.Outcome == outcomeInt)
|
|
.ToListAsync(ct);
|
|
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
|
}
|
|
|
|
public async Task<IReadOnlySet<Guid>> GetExistingAnomalyIdsAsync(
|
|
IReadOnlyCollection<Guid> anomalyIds, CancellationToken ct = default)
|
|
{
|
|
if (anomalyIds.Count == 0)
|
|
return new HashSet<Guid>();
|
|
|
|
var idStrings = anomalyIds.Select(id => id.ToString()).ToList();
|
|
var existing = await _db.PaperBets.AsNoTracking()
|
|
.Where(b => idStrings.Contains(b.AnomalyId))
|
|
.Select(b => b.AnomalyId)
|
|
.ToListAsync(ct);
|
|
|
|
return existing.Select(Guid.Parse).ToHashSet();
|
|
}
|
|
|
|
public async Task AddAsync(PaperBet entity, CancellationToken ct = default)
|
|
{
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
await _db.PaperBets.AddAsync(efEntity, ct);
|
|
}
|
|
|
|
public Task UpdateAsync(PaperBet entity, CancellationToken ct = default)
|
|
{
|
|
var efEntity = Mapping.ToEntity(entity);
|
|
_db.PaperBets.Update(efEntity);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task DeleteAsync(Guid key, CancellationToken ct = default)
|
|
{
|
|
var idStr = key.ToString();
|
|
var entity = await _db.PaperBets.FirstOrDefaultAsync(b => b.Id == idStr, ct);
|
|
if (entity is not null)
|
|
_db.PaperBets.Remove(entity);
|
|
}
|
|
|
|
public async Task SaveChangesAsync(CancellationToken ct = default) =>
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|