using PolyTrader.Modules.ResolutionFarming.Logic; using Xunit; using static PolyTrader.Modules.ResolutionFarming.Logic.FarmingRiskEngine; namespace PolyTrader.Tests { /// /// 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." /// 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)); } } }