Phase 0.4: Konsistenz-Fixes (Spam-Blockade, GlobalPnl, Demo-TokenId)

- SELL-Spam-Blockade greift nur noch bei kürzlichem SELL: PendingOrderTimestamps
  bekommt eine Side ("BUY"/"SELL"); ein vorheriger BUY blockiert einen legitimen
  schnellen SELL des Masters nicht mehr (alle Schreibstellen aktualisiert).
- GlobalPnl: PollClosedAccountsAsync addiert den realisierten PnL jetzt auch
  (wie PollLiveAccountsAsync) -> Anzeige driftet nicht mehr.
- Demo-ClosedTrade erhält TokenId (der _processedClosures-Preload filtert darauf;
  fehlte -> Duplikat-/Dedup-Risiko nach Neustart).

163 Tests gruen. Build/Smoke gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-07 17:46:26 +02:00
co-authored by Claude Opus 4.8
parent e22a6e3091
commit b378bc3499
4 changed files with 15 additions and 9 deletions
@@ -555,7 +555,7 @@ namespace PolyTraderSharp.Services
// Track order placement time for stale order cleanup
string orderKey = $"{account.AccountId}_{signal.TokenId}";
_copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId);
_copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId, "BUY");
// Initialize master position tracking with signal size if not yet tracked
// The background sync will update with the real value within 30 seconds
@@ -570,9 +570,11 @@ namespace PolyTraderSharp.Services
string orderKey = $"{account.AccountId}_{signal.TokenId}";
if (_copyState.PendingOrderTimestamps.TryGetValue(orderKey, out var pendingInfo))
{
if ((DateTime.UtcNow - pendingInfo.PlacedAt).TotalSeconds < 20)
// Nur ein KÜRZLICHER SELL blockiert (Doppel-SELL/Retry-Spam); ein vorheriger
// BUY blockiert einen legitimen schnellen SELL des Masters NICHT mehr.
if (pendingInfo.Side == "SELL" && (DateTime.UtcNow - pendingInfo.PlacedAt).TotalSeconds < 20)
{
return; // Spam-Blockade: Die Order wurde in den letzten 20 Sekunden bereits versendet
return; // Spam-Blockade: SELL wurde in den letzten 20 Sekunden bereits versendet
}
}
@@ -633,6 +635,7 @@ namespace PolyTraderSharp.Services
AccountId = account.AccountId,
SourceTraderId = signal.TraderId,
IsDemo = account.IsDemo,
TokenId = signal.TokenId,
MarketSlug = signal.MarketSlug,
MarketQuestion = signal.MarketQuestion,
Outcome = signal.Outcome,
@@ -705,14 +708,14 @@ namespace PolyTraderSharp.Services
Attempt = 1,
LastActionAt = DateTime.UtcNow
};
_copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId);
_copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId, "SELL");
_logger.Trade($"✅ [LIVE SELL-LEITER platziert] {account.Name} | GTC-Limit {firstLimit:F3} für {openPos.Size:F2} Shares.");
}
else
{
// Startorder fehlgeschlagen: Position bleibt (ExitPending zurücksetzen), Cooldown.
openPos.ExitPending = false;
_copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow.AddSeconds(-15), signal.TraderId);
_copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow.AddSeconds(-15), signal.TraderId, "SELL");
_logger.TradeReasoning($"❌ [LIVE SELL-LEITER] Startorder fehlgeschlagen: {result}. Position bleibt im Portfolio; neuer Versuch beim nächsten Signal/Sync.");
}
}
@@ -122,7 +122,7 @@ namespace PolyTraderSharp.Services
ladder.CurrentLimit = next;
ladder.Attempt++;
ladder.LastActionAt = DateTime.UtcNow;
_copyState.PendingOrderTimestamps[key] = (DateTime.UtcNow, ladder.SourceTraderId);
_copyState.PendingOrderTimestamps[key] = (DateTime.UtcNow, ladder.SourceTraderId, "SELL");
_logger.Trade($"✅ [SELL-LEITER Stufe {ladder.Attempt}] {account.Name} | neues GTC-Limit {next:F3} für {pos.Size:F2} Shares.");
}
else
@@ -639,9 +639,10 @@ namespace PolyTraderSharp.Services
ExitReason = exitReason
};
_state.GlobalPnl += realizedPnl; // Konsistenz: wie in PollLiveAccountsAsync (Anzeige driftete sonst)
_processedClosures.TryAdd(duplicateKey, true);
_closedTradeWriter.TryWrite(ctRecord);
_logger.Info($"🏆 Trade {ctRecord.MarketQuestion} synchronisiert (Hintergrund)! PnL: ${realizedPnl:F2}");
}
}