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()
{