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));
+ }
+ }
+}