using System.Collections.Generic; using System.Linq; using PolyTrader.Modules.ResolutionFarming.Logic; using PolyTrader.Modules.ResolutionFarming.Models; using Xunit; namespace PolyTrader.Tests { /// Sicherheitsnetz für die reine Entry-Planung (Score-Priorisierung unter allen Risiko-Limits). public class FarmingExecutionPlannerTests { private static RfSettings Settings(decimal maxPerMarket = 25m, decimal clusterPct = 10m, decimal totalPct = 60m, int maxPerDay = 20, decimal killUsd = 0m) => new() { MaxPerMarketUsd = maxPerMarket, MaxPerClusterPct = clusterPct, MaxTotalExposurePct = totalPct, MaxNewPositionsPerDay = maxPerDay, DailyLossKillSwitchUsd = killUsd }; private static RfCandidate Cand(string token, decimal score, string cluster = "c", decimal ask = 0.95m) => new() { Accepted = true, TokenId = token, Score = score, ClusterKey = cluster, Ask = ask }; [Fact] public void Plan_ranks_by_score_and_sizes_to_limits() { var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 3m, "c1"), Cand("b", 5m, "c2") }, new List(), Settings(), bankrollUsd: 1000m, newPositionsToday: 0, realizedDailyPnlUsd: 0m); Assert.Equal(2, plan.Count); Assert.Equal("b", plan[0].candidate.TokenId); // höherer Score zuerst Assert.Equal(25m, plan[0].sizeUsd); // min(Markt 25, Cluster 100, Gesamt 600) } [Fact] public void Plan_skips_already_held_tokens() { var open = new List { new() { TokenId = "a", ClusterKey = "c1", AmountUsd = 10m } }; var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 9m, "c1") }, open, Settings(), 1000m, 0, 0m); Assert.Empty(plan); } [Fact] public void Plan_cluster_limit_caps_second_open_in_same_cluster() { // Cluster 3% von 1000 = 30. Erste 25, zweite nur noch 5. var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 5m, "cx"), Cand("b", 4m, "cx") }, new List(), Settings(clusterPct: 3m), 1000m, 0, 0m); Assert.Equal(2, plan.Count); Assert.Equal(25m, plan[0].sizeUsd); Assert.Equal(5m, plan[1].sizeUsd); } [Fact] public void Plan_total_exposure_limit_caps() { // Gesamt 3% von 1000 = 30. Erste 25, zweite 5. var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 5m, "c1"), Cand("b", 4m, "c2") }, new List(), Settings(totalPct: 3m), 1000m, 0, 0m); Assert.Equal(25m, plan[0].sizeUsd); Assert.Equal(5m, plan[1].sizeUsd); } [Fact] public void Plan_kill_switch_blocks_all() { var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 5m) }, new List(), Settings(killUsd: 10m), 1000m, 0, realizedDailyPnlUsd: -10m); Assert.Empty(plan); } [Fact] public void Plan_daily_limit_stops_new_opens() { var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 5m, "c1"), Cand("b", 4m, "c2") }, new List(), Settings(maxPerDay: 1), 1000m, newPositionsToday: 0, realizedDailyPnlUsd: 0m); Assert.Single(plan); } [Fact] public void Plan_dedupes_duplicate_candidate_tokens() { var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 5m, "c1"), Cand("a", 3m, "c1") }, new List(), Settings(), 1000m, 0, 0m); Assert.Single(plan); Assert.Equal(5m, plan[0].candidate.Score); // höherer Score gewinnt } [Fact] public void Plan_empty_when_bankroll_zero() { var plan = FarmingExecutionPlanner.Plan( new[] { Cand("a", 5m) }, new List(), Settings(), bankrollUsd: 0m, 0, 0m); Assert.Empty(plan); } } }