Files
maraphon-app/src/Marathon.Infrastructure/Persistence/PersistenceModule.cs
T
alexei.dolgolyov f622dadf95 feat(paper-trading): forward-test ledger engine
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.
2026-05-29 02:25:54 +03:00

64 lines
2.6 KiB
C#

using Marathon.Application.Abstractions;
using Marathon.Application.Storage;
using Marathon.Infrastructure.Export;
using Marathon.Infrastructure.Persistence.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Marathon.Infrastructure.Persistence;
/// <summary>
/// DI extension that wires up the persistence layer (DbContext, repositories, exporter).
/// Call this from the host's DI setup — do NOT call from DependencyInjection.cs (Phase 4).
/// </summary>
public static class PersistenceModule
{
/// <summary>
/// Registers EF Core DbContext, all repositories and the Excel exporter.
/// Reads <c>Storage:DatabasePath</c> from <paramref name="config"/>.
/// </summary>
public static IServiceCollection AddMarathonPersistence(
this IServiceCollection services,
IConfiguration config)
{
services.AddOptions<StorageOptions>()
.Bind(config.GetSection(StorageOptions.SectionName))
.ValidateOnStart();
services.AddDbContext<MarathonDbContext>((sp, opts) =>
{
var storageOptions = sp.GetRequiredService<IOptions<StorageOptions>>().Value;
var dbPath = storageOptions.DatabasePath;
// Ensure the directory exists
var dir = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
// Configure SQLite with WAL journal mode
opts.UseSqlite(
$"Data Source={dbPath}",
sqliteOpts => sqliteOpts.CommandTimeout(30));
});
// Register initializer — the HOST must resolve this at startup and call InitializeAsync().
// Example in Program.cs:
// using var scope = app.Services.CreateScope();
// await scope.ServiceProvider.GetRequiredService<MarathonDbContextInitializer>().InitializeAsync();
services.AddScoped<MarathonDbContextInitializer>();
services.AddScoped<IEventRepository, EventRepository>();
services.AddScoped<ISnapshotRepository, SnapshotRepository>();
services.AddScoped<IResultRepository, ResultRepository>();
services.AddScoped<IAnomalyRepository, AnomalyRepository>();
services.AddScoped<IPlacedBetRepository, PlacedBetRepository>();
services.AddScoped<ISavedStrategyRepository, SavedStrategyRepository>();
services.AddScoped<IPaperBetRepository, PaperBetRepository>();
services.AddScoped<IExcelExporter, ExcelExporter>();
return services;
}
}