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:
co-authored by
Claude Opus 4.8
parent
e22a6e3091
commit
b378bc3499
@@ -49,8 +49,10 @@ namespace PolyTraderSharp
|
|||||||
public ConcurrentDictionary<int, List<TraderAnalyticsResult>> TraderAnalyticsCache { get; } = new();
|
public ConcurrentDictionary<int, List<TraderAnalyticsResult>> TraderAnalyticsCache { get; } = new();
|
||||||
|
|
||||||
// Tracks when live orders were placed for stale order cleanup.
|
// Tracks when live orders were placed for stale order cleanup.
|
||||||
// Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId)
|
// Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId, Side "BUY"/"SELL").
|
||||||
public ConcurrentDictionary<string, (DateTime PlacedAt, int SourceTraderId)> PendingOrderTimestamps { get; } = new();
|
// 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<string, (DateTime PlacedAt, int SourceTraderId, string Side)> PendingOrderTimestamps { get; } = new();
|
||||||
|
|
||||||
// Master Trader Position Tracker (Key: "{TraderId}_{TokenId}", Value: (Shares, LastUpdated))
|
// Master Trader Position Tracker (Key: "{TraderId}_{TokenId}", Value: (Shares, LastUpdated))
|
||||||
// Entscheidet, ob ein SELL-Signal ein Teilverkauf (ignorieren) oder ein Voll-Exit (kopieren) ist.
|
// Entscheidet, ob ein SELL-Signal ein Teilverkauf (ignorieren) oder ein Voll-Exit (kopieren) ist.
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
// Track order placement time for stale order cleanup
|
// Track order placement time for stale order cleanup
|
||||||
string orderKey = $"{account.AccountId}_{signal.TokenId}";
|
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
|
// Initialize master position tracking with signal size if not yet tracked
|
||||||
// The background sync will update with the real value within 30 seconds
|
// 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}";
|
string orderKey = $"{account.AccountId}_{signal.TokenId}";
|
||||||
if (_copyState.PendingOrderTimestamps.TryGetValue(orderKey, out var pendingInfo))
|
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,
|
AccountId = account.AccountId,
|
||||||
SourceTraderId = signal.TraderId,
|
SourceTraderId = signal.TraderId,
|
||||||
IsDemo = account.IsDemo,
|
IsDemo = account.IsDemo,
|
||||||
|
TokenId = signal.TokenId,
|
||||||
MarketSlug = signal.MarketSlug,
|
MarketSlug = signal.MarketSlug,
|
||||||
MarketQuestion = signal.MarketQuestion,
|
MarketQuestion = signal.MarketQuestion,
|
||||||
Outcome = signal.Outcome,
|
Outcome = signal.Outcome,
|
||||||
@@ -705,14 +708,14 @@ namespace PolyTraderSharp.Services
|
|||||||
Attempt = 1,
|
Attempt = 1,
|
||||||
LastActionAt = DateTime.UtcNow
|
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.");
|
_logger.Trade($"✅ [LIVE SELL-LEITER platziert] {account.Name} | GTC-Limit {firstLimit:F3} für {openPos.Size:F2} Shares.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Startorder fehlgeschlagen: Position bleibt (ExitPending zurücksetzen), Cooldown.
|
// Startorder fehlgeschlagen: Position bleibt (ExitPending zurücksetzen), Cooldown.
|
||||||
openPos.ExitPending = false;
|
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.");
|
_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.CurrentLimit = next;
|
||||||
ladder.Attempt++;
|
ladder.Attempt++;
|
||||||
ladder.LastActionAt = DateTime.UtcNow;
|
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.");
|
_logger.Trade($"✅ [SELL-LEITER Stufe {ladder.Attempt}] {account.Name} | neues GTC-Limit {next:F3} für {pos.Size:F2} Shares.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -639,6 +639,7 @@ namespace PolyTraderSharp.Services
|
|||||||
ExitReason = exitReason
|
ExitReason = exitReason
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_state.GlobalPnl += realizedPnl; // Konsistenz: wie in PollLiveAccountsAsync (Anzeige driftete sonst)
|
||||||
_processedClosures.TryAdd(duplicateKey, true);
|
_processedClosures.TryAdd(duplicateKey, true);
|
||||||
_closedTradeWriter.TryWrite(ctRecord);
|
_closedTradeWriter.TryWrite(ctRecord);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user