From b378bc349992e0fe1c60fc68e09ab21b53e9ff3c Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 7 Jul 2026 17:46:26 +0200 Subject: [PATCH] Phase 0.4: Konsistenz-Fixes (Spam-Blockade, GlobalPnl, Demo-TokenId) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../CopyTradingState.cs | 6 ++++-- .../Services/CopyTradingEngine.cs | 13 ++++++++----- .../Services/SellLadderService.cs | 2 +- .../Services/TraderMonitorService.cs | 3 ++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs b/src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs index 7780306..2b1249d 100644 --- a/src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs +++ b/src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs @@ -49,8 +49,10 @@ namespace PolyTraderSharp public ConcurrentDictionary> TraderAnalyticsCache { get; } = new(); // Tracks when live orders were placed for stale order cleanup. - // Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId) - public ConcurrentDictionary PendingOrderTimestamps { get; } = new(); + // Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId, Side "BUY"/"SELL"). + // Side ermöglicht, dass die 20s-Blockade im SELL-Pfad nur greift, wenn zuletzt ein + // SELL platziert wurde (ein vorheriger BUY blockiert einen legitimen SELL nicht mehr). + public ConcurrentDictionary PendingOrderTimestamps { get; } = new(); // Master Trader Position Tracker (Key: "{TraderId}_{TokenId}", Value: (Shares, LastUpdated)) // Entscheidet, ob ein SELL-Signal ein Teilverkauf (ignorieren) oder ein Voll-Exit (kopieren) ist. diff --git a/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs b/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs index 192c2cb..08e9544 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs @@ -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."); } } diff --git a/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs b/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs index 8eff2c6..96715df 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/SellLadderService.cs @@ -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 diff --git a/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs b/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs index 3dcd9b6..f3c4172 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/TraderMonitorService.cs @@ -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}"); } }