Slice 1 (Fable-Fixes): Leiter-Ownership – K1/H2/H1

Wer darf die ruhende Leiter-Order anfassen? Nur die Leiter selbst.

- H1 (Race): StartLadderAsync macht ZUERST einen atomaren Claim
  (ExitLadders.TryAdd). Master-SELL (Engine) und Profit-Target (Sync) sind
  damit idempotent – kein Doppel-GTC / keine Fehlerkaskade. Verliert ein
  Aufrufer den Claim, wird die (evtl. von der Engine entfernte) Position als
  ExitPending zurueckgestellt – kein Waise. Order-Fehler gibt den Claim zurueck.
- K1a (Floor-Deadlock): CleanupStaleOpenOrdersAsync ueberspringt Keys mit
  aktiver Leiter (ExitLadders.ContainsKey) – cancelt die Floor-Order nicht mehr.
- K1b (Floor-Robustheit): ProcessLadderAsync prueft am Floor via GetOpenOrders,
  ob die SELL-Order noch ruht; falls nicht (Cleanup/extern/Teil-Fill), neu
  platzieren statt bis zum Neustart unverkaeuflich zu haengen.
- H2 (Engine-Cancel): Pre-Signal-CancelConflictingOrders wird uebersprungen,
  wenn fuer den Markt eine Leiter aktiv ist – zerschiesst die Leiter-Order nicht.

Tests: 8 neue Integrationstests (SellLadderService ueber gemockten IClobClient):
atomarer/paralleler Claim, Waisen-Schutz, Claim-Rueckgabe, Floor-Neuplatzierung,
Floor-Halten bei ruhender Order, Leiter-Ende bei gefuellter Position.
Verifikation: Build 0 Fehler, 215 Tests gruen, --smoke-ui ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-09 12:43:17 +02:00
co-authored by Claude Opus 4.8
parent ad8f7b0d03
commit 0200a726e7
5 changed files with 304 additions and 18 deletions
@@ -264,7 +264,15 @@ namespace PolyTraderSharp.Services
// Identische Preise bleiben bestehen. Abweichende verhindern ungültiges Blockieren von Funds.
if (!account.IsDemo && !string.IsNullOrEmpty(signal.TokenId))
{
if (account.HasOpenLimitOrders)
// H2: Läuft für diesen Markt eine SELL-Eskalationsleiter, gehört die ruhende Order der
// Leiter. Der Pre-Signal-Cleanup würde sie sonst bedingungslos canceln danach liegt bis
// zur nächsten Leiter-Stufe (20/120s) keine Order im Markt und die Leiter merkt nichts.
string ladderKey = $"{account.AccountId}_{signal.TokenId}";
if (_copyState.ExitLadders.ContainsKey(ladderKey))
{
_logger.TradeReasoning($"⏭️ [Order-Cleanup übersprungen] {account.Name} | {signal.MarketQuestion}: aktive SELL-Leiter besitzt die Order.");
}
else if (account.HasOpenLimitOrders)
{
await _clob.CancelConflictingOrdersAsync(account, signal.TokenId, signal.Price, signal.Side);
}