From b259f310d3025dabd234099b0060e056605d336c Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 7 Jul 2026 18:21:42 +0200 Subject: [PATCH] Phase 2 (Fundament): proportionale Teilverkauf-Logik pure + getestet - SellLogic.SharesToSell (pure): spiegelt die Verkaufsquote des Masters. 0 = ignorieren (< MinSellRatioPct = Rauschen), = ourShares = Voll-Exit (Master ganz raus oder Rest/Teil unter Polymarket-Minimum -> kein Dust), sonst proportional. - 7 Tests (Teilverkauf, Rausch-Schwelle, Voll-Exit-Faelle, Dust-Grenzen). Wiring in die Engine bewusst zurueckgestellt: braucht Partial-Fill-Handling (Phase 1.1 CLOB User-Channel, API-abhaengig -> Zielland). Aktuelles Voll-Exit- Verhalten unveraendert. 200 Tests gruen. Build gruen. Co-Authored-By: Claude Opus 4.8 --- .../Logic/SellLogic.cs | 20 ++++++++ tests/PolyTrader.Tests/SellLogicTests.cs | 47 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs b/src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs index d2b5300..e588c24 100644 --- a/src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs +++ b/src/PolyTrader.Modules.CopyTrading/Logic/SellLogic.cs @@ -25,6 +25,26 @@ namespace PolyTrader.Modules.CopyTrading.Logic return signalSize / total; } + // ----- Proportionaler Teilverkauf (Phase 2, pure Logik – Wiring braucht Partial-Fill) ----- + + /// + /// Wie viele unserer Shares sollen wir verkaufen, um die Verkaufsquote des Masters zu spiegeln? + /// 0 = ignorieren (Verkauf unter % = Rauschen); + /// = = Voll-Exit (Master ganz raus ODER Restbestand/Teil unter + /// Polymarket-Minimum → kein Dust); sonst der proportionale Anteil. + /// + public static decimal SharesToSell(decimal ourShares, decimal sellRatio, decimal minShares, decimal minSellRatioPct) + { + if (ourShares <= 0m) return 0m; + if (sellRatio < minSellRatioPct / 100m) return 0m; // Rauschen (Day-Trader-Zappeln) + if (sellRatio >= 1m) return ourShares; // Master voll raus → wir auch + + decimal target = ourShares * sellRatio; + decimal remaining = ourShares - target; + if (target < minShares || remaining < minShares) return ourShares; // Dust vermeiden → Voll-Exit + return target; + } + // ----- Eskalationsleiter (Phase 0.1, noch nicht verdrahtet) ----- /// diff --git a/tests/PolyTrader.Tests/SellLogicTests.cs b/tests/PolyTrader.Tests/SellLogicTests.cs index 331010f..61a74a3 100644 --- a/tests/PolyTrader.Tests/SellLogicTests.cs +++ b/tests/PolyTrader.Tests/SellLogicTests.cs @@ -111,6 +111,53 @@ namespace PolyTrader.Tests Assert.False(IsProfitTargetReached(0.9m, 0m, 10m)); } + // ---------------- SharesToSell (Phase 2) ---------------- + + [Fact] + public void SharesToSell_mirrors_partial_ratio() + { + // 40 % von 100, Rest 60 -> Teilverkauf 40 + Assert.Equal(40m, SharesToSell(ourShares: 100m, sellRatio: 0.40m, minShares: 5.5m, minSellRatioPct: 10m)); + } + + [Fact] + public void SharesToSell_ignores_noise_below_min_ratio() + { + Assert.Equal(0m, SharesToSell(100m, 0.05m, 5.5m, 10m)); + } + + [Fact] + public void SharesToSell_full_exit_when_master_fully_out() + { + Assert.Equal(100m, SharesToSell(100m, 1.0m, 5.5m, 10m)); + } + + [Fact] + public void SharesToSell_full_exit_when_partial_would_be_dust() + { + // 30 % von 10 = 3 Shares < Minimum -> Voll-Exit + Assert.Equal(10m, SharesToSell(10m, 0.30m, 5.5m, 10m)); + } + + [Fact] + public void SharesToSell_full_exit_when_remainder_would_be_dust() + { + // 98 % von 100 -> Rest 2 < Minimum -> Voll-Exit + Assert.Equal(100m, SharesToSell(100m, 0.98m, 5.5m, 10m)); + } + + [Fact] + public void SharesToSell_zero_position_is_zero() + { + Assert.Equal(0m, SharesToSell(0m, 0.5m, 5.5m, 10m)); + } + + [Fact] + public void SharesToSell_exactly_at_min_ratio_is_not_ignored() + { + Assert.Equal(10m, SharesToSell(100m, 0.10m, 5.5m, 10m)); + } + [Fact] public void Ladder_walks_down_in_steps_until_floor() {