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>
109 lines
4.1 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|