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);
}
}
}