Files
RichardandClaude Opus 4.8 c9eb11afe4 RF-Slice 1: ResolutionFarming-Modul-Skelett + geldkritische pure Logik + Tests
Neues Strategiemodul PolyTrader.Modules.ResolutionFarming (IPolyTraderModule,
Name=ResolutionFarming, DbPrefix=rf_), in Solution/App/Tests + beide Modul-Listen
in Program.cs eingebunden. Skelett laedt (RegisterServices/UI noch no-op).

Geldkritische Entscheidungslogik pur und vollstaendig unit-getestet:
- FarmingRiskEngine: Netto-Edge nach Fees (NetEdge/NetEdgePct/HasEdge), Positionsgroesse
  unter Markt-/Cluster-/Gesamt-Exposure-Limits (AllowedPositionUsd), Kill-Switch, Tages-Drossel.
- FarmingScanner: Preisband, Kategorie-Whitelist, Blacklist, Cluster-Key (korrelierte
  Favoriten teilen einen Cluster), Kandidaten-Score.
- FarmingFillModel: Shares fuer Budget (2-Dezimal-Floor), Resolve-PnL (Auszahlung - Kosten -
  Entry-Fee, Maker/Taker), Einstiegskosten inkl. Fee. Nutzt Core.FeeModel.
- RfSettings (rf_settings) mit konservativen Defaults + PropertyGrid-Attributen.

39 neue Tests. Build 0 Fehler, 283 Tests gruen, --smoke-ui ok.
Persistenz (DbContext/Migration/Repos), Scanner-/Monitor-Jobs, Execution und UI folgen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:43:34 +02:00

105 lines
3.8 KiB
C#

using PolyTrader.Modules.ResolutionFarming.Logic;
using Xunit;
using static PolyTrader.Modules.ResolutionFarming.Logic.FarmingRiskEngine;
namespace PolyTrader.Tests
{
/// <summary>
/// Sicherheitsnetz für die geldkritische Risiko-Logik des ResolutionFarming (Netto-Edge nach Fees,
/// Positionsgröße unter Cluster-/Exposure-Limits, Kill-Switch). „Das Risikomodell IST die Strategie."
/// </summary>
public class FarmingRiskEngineTests
{
// ----- NetEdge / NetEdgePct -----
[Fact]
public void NetEdge_subtracts_ask_and_per_share_fee()
{
// Ask 0.95, Fee 100 bps: (1-0.95) - 0.95*0.01 = 0.05 - 0.0095 = 0.0405
Assert.Equal(0.0405m, NetEdge(0.95m, 100));
// Fee 0 (Maker): reine Marge
Assert.Equal(0.05m, NetEdge(0.95m, 0));
}
[Theory]
[InlineData(0.0)] // ungültig
[InlineData(1.0)] // ungültig
[InlineData(1.5)] // ungültig
public void NetEdge_invalid_ask_is_negative(double ask)
{
Assert.True(NetEdge((decimal)ask, 0) < 0m);
}
// ----- HasEdge -----
[Theory]
[InlineData(0.95, 0, 1.5, true)] // 5.26% > 1.5%
[InlineData(0.95, 0, 6.0, false)] // 5.26% < 6%
[InlineData(0.99, 180, 1.5, false)] // Krypto-Fee frisst die Marge -> negativ
[InlineData(0.90, 75, 1.5, true)] // Sports-Fee, komfortabler Edge
public void HasEdge_respects_threshold_after_fees(double ask, int feeBps, double minEdgePct, bool expected)
{
Assert.Equal(expected, HasEdge((decimal)ask, feeBps, (decimal)minEdgePct));
}
// ----- AllowedPositionUsd -----
[Fact]
public void AllowedPosition_is_min_of_all_limits()
{
// bankroll 1000: cluster 10% -> 100, total 60% -> 600, market 25. Kein Bestand -> 25.
Assert.Equal(25m, AllowedPositionUsd(25m, 10m, 60m, 1000m, 0m, 0m, 0m));
}
[Fact]
public void AllowedPosition_market_cap_binds()
{
// Bestand im Markt 20 von 25 -> nur 5 Rest.
Assert.Equal(5m, AllowedPositionUsd(25m, 10m, 60m, 1000m, 0m, 0m, 20m));
}
[Fact]
public void AllowedPosition_cluster_cap_binds()
{
// Cluster-Exposure 95 von 100 -> 5 Rest.
Assert.Equal(5m, AllowedPositionUsd(25m, 10m, 60m, 1000m, 0m, 95m, 0m));
}
[Fact]
public void AllowedPosition_total_cap_binds()
{
// Gesamt-Exposure 590 von 600 -> 10 Rest.
Assert.Equal(10m, AllowedPositionUsd(25m, 10m, 60m, 1000m, 590m, 0m, 0m));
}
[Fact]
public void AllowedPosition_exhausted_or_zero_bankroll_returns_zero()
{
Assert.Equal(0m, AllowedPositionUsd(25m, 10m, 60m, 1000m, 0m, 0m, 25m)); // Markt voll
Assert.Equal(0m, AllowedPositionUsd(25m, 10m, 60m, 0m, 0m, 0m, 0m)); // keine Bankroll
Assert.Equal(0m, AllowedPositionUsd(25m, 10m, 60m, 1000m, 600m, 0m, 0m)); // Gesamt voll
}
// ----- Kill-Switch / Tages-Drossel -----
[Theory]
[InlineData(-10.0, 10.0, true)] // genau an der Schwelle
[InlineData(-11.0, 10.0, true)] // darunter
[InlineData(-9.0, 10.0, false)] // darüber
[InlineData(-50.0, 0.0, false)] // deaktiviert (0)
public void ShouldKill_on_daily_loss(double pnl, double threshold, bool expected)
{
Assert.Equal(expected, ShouldKill((decimal)pnl, (decimal)threshold));
}
[Theory]
[InlineData(19, 20, false)]
[InlineData(20, 20, true)]
[InlineData(21, 20, true)]
public void DailyLimitReached_counts_new_positions(int today, int max, bool expected)
{
Assert.Equal(expected, DailyLimitReached(today, max));
}
}
}