RF-Slice 1: ResolutionFarming-Modul-Skelett + geldkritische pure Logik + Tests

Neues Strategiemodul PolyTrader.Modules.ResolutionFarming (IPolyTraderModule,
Name=ResolutionFarming, DbPrefix=rf_), in Solution/App/Tests + beide Modul-Listen
in Program.cs eingebunden. Skelett laedt (RegisterServices/UI noch no-op).

Geldkritische Entscheidungslogik pur und vollstaendig unit-getestet:
- FarmingRiskEngine: Netto-Edge nach Fees (NetEdge/NetEdgePct/HasEdge), Positionsgroesse
  unter Markt-/Cluster-/Gesamt-Exposure-Limits (AllowedPositionUsd), Kill-Switch, Tages-Drossel.
- FarmingScanner: Preisband, Kategorie-Whitelist, Blacklist, Cluster-Key (korrelierte
  Favoriten teilen einen Cluster), Kandidaten-Score.
- FarmingFillModel: Shares fuer Budget (2-Dezimal-Floor), Resolve-PnL (Auszahlung - Kosten -
  Entry-Fee, Maker/Taker), Einstiegskosten inkl. Fee. Nutzt Core.FeeModel.
- RfSettings (rf_settings) mit konservativen Defaults + PropertyGrid-Attributen.

39 neue Tests. Build 0 Fehler, 283 Tests gruen, --smoke-ui ok.
Persistenz (DbContext/Migration/Repos), Scanner-/Monitor-Jobs, Execution und UI folgen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-09 21:43:34 +02:00
co-authored by Claude Opus 4.8
parent 2f5270a2f3
commit c9eb11afe4
13 changed files with 570 additions and 2 deletions
@@ -0,0 +1,60 @@
using PolyTrader.Modules.ResolutionFarming.Logic;
using Xunit;
using static PolyTrader.Modules.ResolutionFarming.Logic.FarmingScanner;
namespace PolyTrader.Tests
{
/// <summary>Sicherheitsnetz für die Scanner-Filter (Preisband, Kategorie, Blacklist, Cluster-Key, Score).</summary>
public class FarmingScannerTests
{
[Theory]
[InlineData(0.90, 0.90, 0.98, true)] // untere Grenze inkl.
[InlineData(0.98, 0.90, 0.98, true)] // obere Grenze inkl.
[InlineData(0.89, 0.90, 0.98, false)]
[InlineData(0.99, 0.90, 0.98, false)]
public void InPriceBand_is_inclusive(double ask, double min, double max, bool expected)
{
Assert.Equal(expected, InPriceBand((decimal)ask, (decimal)min, (decimal)max));
}
[Theory]
[InlineData("Sports", "Sports,Politics", true)]
[InlineData("sports", "Sports,Politics", true)] // case-insensitiv
[InlineData("Crypto", "Sports,Politics", false)]
[InlineData("Sports", "", true)] // leere Whitelist = alles
[InlineData("", "Sports,Politics", false)] // leere Kategorie
public void CategoryAllowed_matches_whitelist(string category, string whitelist, bool expected)
{
Assert.Equal(expected, CategoryAllowed(category, whitelist));
}
[Theory]
[InlineData("us-election-dispute", null, "dispute,uma", true)]
[InlineData("nfl-week1", "sports,nfl", "uma", false)]
[InlineData("anything", null, "", false)] // leere Blacklist
public void IsBlacklisted_matches_slug_or_tags(string slug, string? tags, string blacklist, bool expected)
{
Assert.Equal(expected, IsBlacklisted(slug, tags, blacklist));
}
[Fact]
public void ClusterKey_prefers_event_slug_then_market_slug()
{
Assert.Equal("epl-2026-03-14", ClusterKey("EPL-2026-03-14", "arsenal-win"));
Assert.Equal("arsenal-win", ClusterKey(null, "Arsenal-Win"));
Assert.Equal("arsenal-win", ClusterKey(" ", "arsenal-win"));
Assert.Equal("", ClusterKey(null, null));
}
[Fact]
public void Score_increases_with_edge_and_earlier_resolution()
{
// mehr Edge -> höherer Score
Assert.True(Score(5m, 24) > Score(2m, 24));
// frühere Auflösung -> höherer Score (Bindungsbonus)
Assert.True(Score(3m, 1) > Score(3m, 40));
// negativer Edge zählt als 0, aber Zeitbonus bleibt >= 0
Assert.True(Score(-5m, 10) >= 0m);
}
}
}