From 581755029a6e66600c5e972c3956b83c68b895b3 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 7 Jul 2026 11:03:44 +0200 Subject: [PATCH] =?UTF-8?q?Testnetz:=20Micro-Order-Minimum=20+=20SELL-Prop?= =?UTF-8?q?ortionalit=C3=A4t=20extrahiert;=20SELL-Leiter=20(Phase=200.1)?= =?UTF-8?q?=20vorbereitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Weiter im Muster extrahieren->testen (verhaltensneutral, clob.md): - CopyTradingRisk.IsBelowPolymarketMinimum (Shares < 5.5 || USDC < 0.10) – BUY-Micro-Order-Filter verdrahtet + Tests. - SellLogic.SellProportion (signalSize/(remaining+signalSize), Denominator<=0 -> 0) + Konstante MinSignificantSellRatio (0.30); SELL-Proportionalitätscheck der Engine ruft sie jetzt (Charakterisierung, sichert Phase-2-Umbau ab). - SellLogic-Eskalationsleiter (Phase 0.1, NOCH NICHT verdrahtet): FirstLimit (HF-fest/prozentual), Floor (SellFloorPct), NextPrice (relative Stufe, auf Floor geclamped), IsAtFloor. Reine Grundlage fuer das spaetere SELL-Wiring. - Tests: CopyTradingRisk-Minimum (5 Faelle) + SellLogicTests (Proportion + Leiter). Gesamt 154 gruen. Build/Smoke gruen. Co-Authored-By: Claude Opus 4.8 --- .../Logic/CopyTradingRisk.cs | 14 +++ .../Logic/SellLogic.cs | 55 ++++++++++++ .../Services/CopyTradingEngine.cs | 7 +- .../PolyTrader.Tests/CopyTradingRiskTests.cs | 13 +++ tests/PolyTrader.Tests/SellLogicTests.cs | 89 +++++++++++++++++++ 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs create mode 100644 tests/PolyTrader.Tests/SellLogicTests.cs diff --git a/src/PolyTrader.Modules.CopyTrading/Logic/CopyTradingRisk.cs b/src/PolyTrader.Modules.CopyTrading/Logic/CopyTradingRisk.cs index 4fd4f20..cade186 100644 --- a/src/PolyTrader.Modules.CopyTrading/Logic/CopyTradingRisk.cs +++ b/src/PolyTrader.Modules.CopyTrading/Logic/CopyTradingRisk.cs @@ -18,6 +18,20 @@ namespace PolyTrader.Modules.CopyTrading.Logic /// Fester Limit-Aufschlag für HF-Trader (0,5 ¢). public const decimal HfLimitOffset = 0.005m; + /// Polymarket-Mindestgröße: wir filtern schon knapp über 5 Shares, um API-Fehler + /// ("Size lower than the minimum: 5") zu vermeiden. + public const decimal MinShares = 5.5m; + + /// Absolute USDC-Untergrenze pro Order. + public const decimal MinUsdc = 0.10m; + + /// + /// Liegt eine berechnete Order unter dem Polymarket-Minimum (Shares ODER USDC)? + /// Solche Mikro-Orders werden vor dem API-Call verworfen. + /// + public static bool IsBelowPolymarketMinimum(decimal shares, decimal usdc) => + shares < MinShares || usdc < MinUsdc; + /// /// Limit-Preis einer BUY-Order: HF-Trader bekommen einen festen Aufschlag von 0.005, /// sonst einen prozentualen Aufschlag () über dem diff --git a/src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs b/src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs new file mode 100644 index 0000000..855ef88 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs @@ -0,0 +1,55 @@ +using System; + +namespace PolyTrader.Modules.CopyTrading.Logic +{ + /// + /// Reine SELL-Entscheidungs-/Preislogik. Teil davon (Proportionalität) ist 1:1 aus der + /// aktuellen Engine übernommen (Charakterisierung, damit der Phase-2-Umbau abgesichert ist); + /// die Eskalationsleiter ist die Grundlage für Phase 0.1 (noch NICHT in der Engine verdrahtet). + /// + public static class SellLogic + { + // ----- Proportionalität (aktuelles Verhalten, Schwelle hart 30 %) ----- + + /// Ab dieser Verkaufsquote des Masters kopieren wir den SELL (aktuell hart 30 %). + public const decimal MinSignificantSellRatio = 0.30m; + + /// + /// Anteil, den dieser SELL am Master-Bestand VOR dem Verkauf ausmacht: + /// signalSize / (masterSharesRemaining + signalSize). Denominator ≤ 0 → 0. + /// + public static decimal SellProportion(decimal masterSharesRemaining, decimal signalSize) + { + decimal total = masterSharesRemaining + signalSize; + if (total <= 0m) return 0m; + return signalSize / total; + } + + // ----- Eskalationsleiter (Phase 0.1, noch nicht verdrahtet) ----- + + /// + /// Erster SELL-Limit-Preis (GTD): HF-Trader fester Abschlag von 0.005 unter dem Master-Exit, + /// sonst prozentualer Abschlag (). + /// + public static decimal FirstLimit(decimal referencePrice, bool isHf, decimal maxPriceDifferencePct) => + isHf ? referencePrice - CopyTradingRisk.HfLimitOffset + : referencePrice * (1.0m - maxPriceDifferencePct / 100.0m); + + /// + /// Absolute Untergrenze der Leiter: referencePrice × (1 − SellFloorPct/100). + /// Darunter wird nicht mehr verkauft (Position halten statt Exit-Liquidity zu werden). + /// + public static decimal Floor(decimal referencePrice, decimal sellFloorPct) => + referencePrice * (1.0m - sellFloorPct / 100.0m); + + /// + /// Nächste Stufe nach ausbleibendem Fill: aktuellen Preis um % + /// relativ senken, aber nie unter den . + /// + public static decimal NextPrice(decimal currentPrice, decimal stepPct, decimal floor) => + Math.Max(currentPrice * (1.0m - stepPct / 100.0m), floor); + + /// Leiter erschöpft: aktueller Preis hat den Floor erreicht/unterschritten. + public static bool IsAtFloor(decimal currentPrice, decimal floor) => currentPrice <= floor; + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs b/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs index 7e884ee..c272ea0 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs @@ -383,7 +383,7 @@ namespace PolyTraderSharp.Services // ===== MICRO-ORDER FILTER: Polymarket Minimum Size Enforcement ===== // Polymarket lehnt Orders mit < 5 Shares ab ("Size lower than the minimum: 5"). // Statt die API zu belasten und Fehler-Logs zu erzeugen, filtern wir hier sofort. - if (exact.shares < 5.5m || exact.usdc < 0.10m) + if (CopyTradingRisk.IsBelowPolymarketMinimum(exact.shares, exact.usdc)) { _logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] gestoppt:\n" + $" Konto: {account.Name}\n" + @@ -431,8 +431,9 @@ namespace PolyTraderSharp.Services if (masterShares > 0 && signal.Size > 0) { // Calculate what percentage of the master's known position this SELL represents - decimal sellRatio = signal.Size / (masterShares + signal.Size); // +signal.Size because the position was already reduced - if (sellRatio < 0.30m) + // (Proportionalität: SellLogic, unit-getestet) + decimal sellRatio = SellLogic.SellProportion(masterShares, signal.Size); + if (sellRatio < SellLogic.MinSignificantSellRatio) { _logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" + $" Konto: {account.Name}\n" + diff --git a/tests/PolyTrader.Tests/CopyTradingRiskTests.cs b/tests/PolyTrader.Tests/CopyTradingRiskTests.cs index a595b4a..f9e1bcb 100644 --- a/tests/PolyTrader.Tests/CopyTradingRiskTests.cs +++ b/tests/PolyTrader.Tests/CopyTradingRiskTests.cs @@ -190,5 +190,18 @@ namespace PolyTrader.Tests // available großzügig, damit die Stufen-Caps nicht durch Verfügbarkeit greifen Assert.Equal((decimal)expected, MaxPerMarket((decimal)balance, 100000m, 5m)); } + + // ---------------- IsBelowPolymarketMinimum ---------------- + + [Theory] + [InlineData(5.5, 1.0, false)] // genau an der Share-Grenze -> ok + [InlineData(5.4, 1.0, true)] // zu wenige Shares + [InlineData(6.0, 0.10, false)] // genau an der USDC-Grenze -> ok + [InlineData(6.0, 0.09, true)] // zu wenig USDC + [InlineData(100.0, 50.0, false)] + public void IsBelowPolymarketMinimum_enforces_share_and_usdc_floor(double shares, double usdc, bool expected) + { + Assert.Equal(expected, IsBelowPolymarketMinimum((decimal)shares, (decimal)usdc)); + } } } diff --git a/tests/PolyTrader.Tests/SellLogicTests.cs b/tests/PolyTrader.Tests/SellLogicTests.cs new file mode 100644 index 0000000..e827e68 --- /dev/null +++ b/tests/PolyTrader.Tests/SellLogicTests.cs @@ -0,0 +1,89 @@ +using PolyTrader.Modules.CopyTrading.Logic; +using Xunit; +using static PolyTrader.Modules.CopyTrading.Logic.SellLogic; + +namespace PolyTrader.Tests +{ + /// + /// Sicherheitsnetz für die SELL-Logik: Proportionalität (aktuelles Verhalten, Phase-2-Umbau) + /// und die Eskalationsleiter (Phase 0.1 – Preis-Stufen, Floor-Clamping). + /// + public class SellLogicTests + { + // ---------------- SellProportion ---------------- + + [Fact] + public void SellProportion_is_share_over_total_before_sell() + { + // Master hält noch 300, verkauft 200 -> 200/500 = 0.40 + Assert.Equal(0.40m, SellProportion(masterSharesRemaining: 300m, signalSize: 200m)); + } + + [Fact] + public void SellProportion_small_partial_is_below_threshold() + { + // 498 Rest, 2 verkauft -> 0.004 < 0.30 + var ratio = SellProportion(498m, 2m); + Assert.True(ratio < MinSignificantSellRatio); + } + + [Fact] + public void SellProportion_full_exit_is_one() + { + Assert.Equal(1.0m, SellProportion(0m, 10m)); + } + + [Theory] + [InlineData(0, 0)] // gar nichts + [InlineData(-5, 5)] // Denominator 0 + [InlineData(-10, 5)] // Denominator negativ + public void SellProportion_guards_non_positive_denominator(double remaining, double signal) + { + Assert.Equal(0m, SellProportion((decimal)remaining, (decimal)signal)); + } + + // ---------------- Eskalationsleiter ---------------- + + [Fact] + public void FirstLimit_normal_trader_applies_percentage_discount() + { + // 0.99 * (1 - 2%) = 0.9702 + Assert.Equal(0.9702m, FirstLimit(0.99m, isHf: false, maxPriceDifferencePct: 2m)); + } + + [Fact] + public void FirstLimit_hf_trader_applies_fixed_half_cent_discount() + { + Assert.Equal(0.985m, FirstLimit(0.99m, isHf: true, maxPriceDifferencePct: 99m)); + } + + [Fact] + public void Floor_is_reference_minus_floor_pct() + { + // 0.99 * (1 - 15%) = 0.8415 + Assert.Equal(0.8415m, Floor(0.99m, 15m)); + } + + [Fact] + public void NextPrice_steps_down_relative_when_above_floor() + { + // 0.90 * (1 - 3%) = 0.873, über Floor 0.80 + Assert.Equal(0.873m, NextPrice(0.90m, stepPct: 3m, floor: 0.80m)); + } + + [Fact] + public void NextPrice_clamps_to_floor() + { + // 0.82 * 0.97 = 0.7954 -> unter Floor -> 0.80 + Assert.Equal(0.80m, NextPrice(0.82m, 3m, 0.80m)); + } + + [Fact] + public void IsAtFloor_is_inclusive() + { + Assert.True(IsAtFloor(0.80m, 0.80m)); + Assert.True(IsAtFloor(0.79m, 0.80m)); + Assert.False(IsAtFloor(0.81m, 0.80m)); + } + } +}