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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2f5270a2f3
commit
c9eb11afe4
@@ -0,0 +1,104 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user