Phase 0.1: SELL-Eskalationsleiter statt Market-Dump (CLOB-kritisch)

Behebt die April-Verlustquelle: SELLs wurden als Market-Order mit 0.01-Limit ins
oft leergeraeumte Orderbuch geworfen -> wir wurden zur Exit-Liquidity. Jetzt:
GTC-Limit nahe am Master-Exit, stufenweises Nachpreisen bis zum Floor.

- Position.ExitPending (runtime-only, EF-ignoriert): Position wird bei SELL NICHT
  mehr optimistisch entfernt, sondern als ExitPending zurueckgestellt (kein
  Doppel-SELL, Limits rechnen korrekt; Sync schliesst nach bestaetigtem Fill).
- CopyTradingState.ExitLadders + ExitLadderState (transienter Leiter-Zustand).
- SellLogic (pure, getestet): FirstLimit (HF-fest/prozentual), Floor (SellFloorPct),
  NextPrice (relative Stufe, auf Floor geclamped), IsAtFloor, LadderStepPct (3%),
  LadderIntervalSeconds (HF 20s / sonst 120s).
- SellLadderService (BackgroundService): senkt offene Exit-Limits stufenweise
  (cancel via CancelConflictingOrdersAsync -> tiefer neu platzieren), am Floor ohne
  Fill Position halten + Threema-Benachrichtigung. Fills erkennt der bestehende Sync.
- CopyTradingEngine SELL-Live-Pfad ruft die Leiter; Demo-Pfad unveraendert.
  Doppel-SELL-Guard ueber ExitPending. Umfangreiches Logging (kein Live-Test moeglich).

163 Tests gruen. Build/Smoke gruen. Backup-Rollback: Commit 1dffc9e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-07 17:40:40 +02:00
co-authored by Claude Opus 4.8
parent 1dffc9e253
commit e22a6e3091
8 changed files with 266 additions and 25 deletions
+28
View File
@@ -85,5 +85,33 @@ namespace PolyTrader.Tests
Assert.True(IsAtFloor(0.79m, 0.80m));
Assert.False(IsAtFloor(0.81m, 0.80m));
}
[Theory]
[InlineData(true, 20)] // HF-Trader: schnelles Nachpreisen
[InlineData(false, 120)] // Standard
public void LadderIntervalSeconds_hf_is_faster(bool isHf, int expected)
{
Assert.Equal(expected, LadderIntervalSeconds(isHf));
}
[Fact]
public void Ladder_walks_down_in_steps_until_floor()
{
// Simuliert den Service: von Startlimit in 3%-Schritten bis zum Floor, dann Stopp.
decimal reference = 0.99m;
decimal price = FirstLimit(reference, isHf: false, maxPriceDifferencePct: 2m); // 0.9702
decimal floor = Floor(reference, 15m); // 0.8415
int steps = 0;
while (!IsAtFloor(price, floor) && steps < 100)
{
price = NextPrice(price, LadderStepPct, floor);
steps++;
}
Assert.True(IsAtFloor(price, floor));
Assert.Equal(floor, price); // endet exakt auf dem Floor (geclamped)
Assert.InRange(steps, 1, 20); // terminiert in wenigen Schritten
}
}
}