Files
PolyTraderSharp/tests/PolyTrader.Tests/FarmingExecutionPlannerTests.cs
T
RichardandClaude Opus 4.8 1c3a364df2 RF-Slice 5: Demo-Execution + Resolution-Monitor (Phase RF-2)
Schliesst die Demo-Handelsschleife des ResolutionFarming:
- FarmingExecutionPlanner (pur): waehlt aus akzeptierten Kandidaten die zu oeffnenden
  Positionen + Groesse, priorisiert nach Score, unter Markt-/Cluster-/Gesamt-Limits,
  Kill-Switch und Tages-Drossel; dedupliziert Token, schreibt Exposure im Lauf fort.
- FarmingResolution (pur): baut aus Position + Ergebnis den RfClosedTrade (PnL/Fees/Redeem-Status).
- FarmingExecutionService: Demo-Einstieg (Maker-Fill 0 Fee via FarmingFillModel) -> rf_positions.
  Live-Execution bewusst geloggt/uebersprungen (Zielland). Demo-Balance NICHT mutiert
  (kein Shared-Account-Konflikt; PnL fliesst ueber rf_closed_trades).
- FarmingResolutionMonitorService: schliesst aufgeloeste Positionen, bucht GlobalPnl,
  Dual-Write ins Core-Trade-Log (Dashboard). Auflösungsstatus via IMarketResolutionSource.
- NullMarketResolutionSource als Default (nichts loest auf), bis Live-Data-API verdrahtet ist.

15 neue Tests (Planner 8, Resolution 3, Execution-Service 2, Monitor 2). Build 0 Fehler,
311 Tests gruen, --smoke-ui ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 11:40:30 +02:00

109 lines
4.1 KiB
C#

using System.Collections.Generic;
using System.Linq;
using PolyTrader.Modules.ResolutionFarming.Logic;
using PolyTrader.Modules.ResolutionFarming.Models;
using Xunit;
namespace PolyTrader.Tests
{
/// <summary>Sicherheitsnetz für die reine Entry-Planung (Score-Priorisierung unter allen Risiko-Limits).</summary>
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<RfPosition>(), 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<RfPosition> { 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<RfPosition>(), 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<RfPosition>(), 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<RfPosition>(),
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<RfPosition>(), 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<RfPosition>(), 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<RfPosition>(), Settings(), bankrollUsd: 0m, 0, 0m);
Assert.Empty(plan);
}
}
}