Slice 3 (Fable-Fixes): K3 Demo-Resolution + M5 Demo-Score/Auto-Pause
K3 – Demo-Auto-Close bei Marktauflösung funktioniert wieder: - System-SELLs (TraderId==0) sind vom Ownership-Check ausgenommen. Regel in reine CopyTradingRisk.IsAuthorizedSell(signalTraderId, positionSourceTraderId) extrahiert (System ODER Besitzer) und in der Engine genutzt. Vorher lehnte der Check jedes System-Close ab (Copy-Positionen haben SourceTraderId>0) -> Demo-Positionen schlossen bei Resolution nie, 30s-Loop feuerte endlos. - Resolved-Cache (_resolvedDemoMarkets) in PollDemoExpirationsAsync: aufgeloeste Maerkte werden nicht erneut angefragt (kein Log-Spam / keine unnoetige API-Last). M5 – Auto-Pause in der Demo-Phase nicht mehr wirkungslos: - Anzeige-Spalten (CopyPnl30d etc.) zeigen Live-Kennzahlen, sobald vorhanden, sonst Demo-Kennzahlen -> Demo-Validierung ist sichtbar statt leer. - Harte Auto-Pause wirkt bewusst NUR auf Live-Ergebnissen. - Kadenz: stuendlicher Light-Check (Auto-Pause) statt nur alle 12h; schwere History-Analyse bleibt 12h (schliessen sich pro Tick aus, kein Doppellauf). Tests: 5 neue (IsAuthorizedSell). Build 0 Fehler, 223 Tests gruen, --smoke-ui ok. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a3d3d50681
commit
8252219163
@@ -32,6 +32,16 @@ namespace PolyTrader.Modules.CopyTrading.Logic
|
||||
public static bool IsBelowPolymarketMinimum(decimal shares, decimal usdc) =>
|
||||
shares < MinShares || usdc < MinUsdc;
|
||||
|
||||
/// <summary>
|
||||
/// Darf ein SELL diese Position schließen? Erlaubt, wenn das Signal vom besitzenden Master
|
||||
/// stammt (<paramref name="positionSourceTraderId"/> == <paramref name="signalTraderId"/>)
|
||||
/// ODER es ein System-Signal ist (<paramref name="signalTraderId"/> == 0, z.B. Demo-Auto-Close
|
||||
/// bei Marktauflösung). K3: ohne die System-Ausnahme würden Copy-Positionen (SourceTraderId > 0)
|
||||
/// System-Closes IMMER ablehnen → Demo-Positionen schließen bei Resolution nie.
|
||||
/// </summary>
|
||||
public static bool IsAuthorizedSell(int signalTraderId, int positionSourceTraderId) =>
|
||||
signalTraderId == 0 || positionSourceTraderId == signalTraderId;
|
||||
|
||||
/// <summary>
|
||||
/// Limit-Preis einer BUY-Order: HF-Trader bekommen einen festen Aufschlag von 0.005,
|
||||
/// sonst einen prozentualen Aufschlag (<paramref name="maxPriceDifferencePct"/>) über dem
|
||||
|
||||
@@ -613,9 +613,13 @@ namespace PolyTraderSharp.Services
|
||||
}
|
||||
|
||||
bool removed = account.OpenPositions.TryRemove(signal.TokenId, out var openPos);
|
||||
|
||||
// Defense-in-depth: Verify the removed position actually belongs to this trader
|
||||
if (removed && openPos != null && openPos.SourceTraderId != signal.TraderId)
|
||||
|
||||
// Defense-in-depth: Verify the removed position actually belongs to this trader.
|
||||
// K3: System-Signale (TraderId == 0, z.B. Demo-Auto-Close bei Marktauflösung) sind vom
|
||||
// Ownership-Check ausgenommen (CopyTradingRisk.IsAuthorizedSell) – sie gehören keinem
|
||||
// Master und würden sonst IMMER abgelehnt (Copy-Positionen haben SourceTraderId > 0),
|
||||
// sodass Demo-Positionen bei Resolution nie schließen und der 30s-Loop endlos feuert.
|
||||
if (removed && openPos != null && !CopyTradingRisk.IsAuthorizedSell(signal.TraderId, openPos.SourceTraderId))
|
||||
{
|
||||
// Wrong trader! Put the position back and treat as not found.
|
||||
account.OpenPositions.TryAdd(signal.TokenId, openPos);
|
||||
|
||||
@@ -56,14 +56,28 @@ namespace PolyTraderSharp.Services
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(20), stoppingToken); // Start after other jobs
|
||||
|
||||
// M5: Auto-Pause reagiert stündlich (Light-Check aus UNSEREN Trades), die schwere
|
||||
// Master-History-Analyse (flakige Data-API) läuft weiter nur alle 12h. Beide schließen
|
||||
// sich pro Tick aus – RunHistoryAnalyticsAsync ruft den Copy-Score selbst auf, also
|
||||
// kein Doppellauf.
|
||||
DateTime lastHeavyRun = DateTime.MinValue;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (_jobStatus.IsEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_jobStatus.StatusText = "Running (Scheduled)...";
|
||||
await RunHistoryAnalyticsAsync();
|
||||
if ((DateTime.UtcNow - lastHeavyRun) >= TimeSpan.FromHours(12))
|
||||
{
|
||||
_jobStatus.StatusText = "Running (History + Auto-Pause)...";
|
||||
await RunHistoryAnalyticsAsync(); // enthält Copy-Score + Auto-Pause
|
||||
lastHeavyRun = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_jobStatus.StatusText = "Running (Auto-Pause-Check)...";
|
||||
await UpdateCopyScoresAndAutoPauseAsync(); // stündlicher Light-Check
|
||||
}
|
||||
_jobStatus.LastRun = DateTime.Now;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -81,9 +95,9 @@ namespace PolyTraderSharp.Services
|
||||
_jobStatus.StatusText = "Paused";
|
||||
}
|
||||
|
||||
// Run twice a day (every 12 hours)
|
||||
_jobStatus.NextRun = DateTime.Now.AddHours(12);
|
||||
await Task.Delay(TimeSpan.FromHours(12), stoppingToken);
|
||||
// Light-Check-Kadenz: stündlich (Auto-Pause reagiert schneller).
|
||||
_jobStatus.NextRun = DateTime.Now.AddHours(1);
|
||||
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,9 +218,12 @@ namespace PolyTraderSharp.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 3.1/3.3: Berechnet je Master den Copy-Score (letzte 30 Tage aus unseren
|
||||
/// geschlossenen Copy-Trades) und pausiert Master hart, die über genügend Trades ins Minus
|
||||
/// laufen (nur wenn deren AutoPauseEnabled gesetzt ist). Reaktivierung bewusst nur manuell.
|
||||
/// Phase 3.1/3.3 + M5: Berechnet je Master den Copy-Score (letzte 30 Tage aus unseren
|
||||
/// geschlossenen Copy-Trades). Die Anzeige-Spalten zeigen Live-Kennzahlen, sobald es
|
||||
/// Live-Trades gibt, sonst Demo-Kennzahlen – damit die Demo-Validierungsphase nicht blind ist.
|
||||
/// Die HARTE Auto-Pause wirkt bewusst NUR auf Live-Ergebnissen (Demo hat andere Fill-Annahmen
|
||||
/// und ist Validierung, kein Kill-Kriterium; in der Demo-Phase pausiert der User manuell).
|
||||
/// Nur Master mit gesetztem AutoPauseEnabled; Reaktivierung ausschließlich manuell.
|
||||
/// </summary>
|
||||
private async Task UpdateCopyScoresAndAutoPauseAsync()
|
||||
{
|
||||
@@ -216,26 +233,32 @@ namespace PolyTraderSharp.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var pnls = _tradeLog
|
||||
.Find(t => t.SourceTraderId == trader.Id && t.ClosedAt >= since && !t.IsDemo)
|
||||
.Select(t => t.RealizedPnl);
|
||||
var m = TraderScore.Compute(pnls);
|
||||
var closed = _tradeLog
|
||||
.Find(t => t.SourceTraderId == trader.Id && t.ClosedAt >= since)
|
||||
.ToList();
|
||||
var liveMetrics = TraderScore.Compute(closed.Where(t => !t.IsDemo).Select(t => t.RealizedPnl));
|
||||
|
||||
trader.CopyPnl30d = m.CopyPnl;
|
||||
trader.CopyProfitFactor = m.ProfitFactor;
|
||||
trader.CopyAvgPnlPerTrade = m.AvgPnlPerTrade;
|
||||
trader.CopyTradeCount30d = m.TradeCount;
|
||||
// Anzeige: Live sobald vorhanden, sonst Demo (Validierungssignal statt leerer Spalten).
|
||||
var display = liveMetrics.TradeCount > 0
|
||||
? liveMetrics
|
||||
: TraderScore.Compute(closed.Where(t => t.IsDemo).Select(t => t.RealizedPnl));
|
||||
|
||||
trader.CopyPnl30d = display.CopyPnl;
|
||||
trader.CopyProfitFactor = display.ProfitFactor;
|
||||
trader.CopyAvgPnlPerTrade = display.AvgPnlPerTrade;
|
||||
trader.CopyTradeCount30d = display.TradeCount;
|
||||
|
||||
// Auto-Pause NUR auf Live-Kennzahlen.
|
||||
bool pause = trader.IsActive && TraderScore.ShouldAutoPause(
|
||||
trader.AutoPauseEnabled, m.TradeCount, m.CopyPnl,
|
||||
trader.AutoPauseEnabled, liveMetrics.TradeCount, liveMetrics.CopyPnl,
|
||||
_copyState.AutoPauseMinTrades, _copyState.AutoPauseDrawdownUsd);
|
||||
|
||||
if (pause)
|
||||
{
|
||||
trader.IsActive = false;
|
||||
trader.Reasoning = $"[Auto-Pause {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC] Copy-PnL {m.CopyPnl:F2} USDC über {m.TradeCount} Trades (30T) unter Schwelle (-{_copyState.AutoPauseDrawdownUsd:F0}). Reaktivierung manuell.";
|
||||
_logger.Warning($"🛑 [AUTO-PAUSE] Master '{trader.DisplayName}' deaktiviert. Copy-PnL {m.CopyPnl:F2} / {m.TradeCount} Trades. Reaktivierung nur manuell.");
|
||||
try { await _threema.SendMessageAsync($"🛑 Auto-Pause: Master '{trader.DisplayName}' deaktiviert.\nCopy-PnL 30T: {m.CopyPnl:F2} USDC über {m.TradeCount} Trades.\nReaktivierung manuell."); }
|
||||
trader.Reasoning = $"[Auto-Pause {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC] Copy-PnL {liveMetrics.CopyPnl:F2} USDC über {liveMetrics.TradeCount} Live-Trades (30T) unter Schwelle (-{_copyState.AutoPauseDrawdownUsd:F0}). Reaktivierung manuell.";
|
||||
_logger.Warning($"🛑 [AUTO-PAUSE] Master '{trader.DisplayName}' deaktiviert. Live-Copy-PnL {liveMetrics.CopyPnl:F2} / {liveMetrics.TradeCount} Trades. Reaktivierung nur manuell.");
|
||||
try { await _threema.SendMessageAsync($"🛑 Auto-Pause: Master '{trader.DisplayName}' deaktiviert.\nLive-Copy-PnL 30T: {liveMetrics.CopyPnl:F2} USDC über {liveMetrics.TradeCount} Trades.\nReaktivierung manuell."); }
|
||||
catch (Exception ex) { _logger.Error($"Threema Auto-Pause-Benachrichtigung fehlgeschlagen: {ex.Message}"); }
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace PolyTraderSharp.Services
|
||||
private readonly ConcurrentDictionary<string, DateTime> _processedTxHashes = new();
|
||||
private DateTime _lastHashCleanup = DateTime.UtcNow;
|
||||
private readonly ConcurrentDictionary<string, bool> _processedClosures = new();
|
||||
// K3: Bereits als aufgelöst erkannte Demo-Märkte (Key: TokenId). Verhindert, dass der
|
||||
// 30s-Loop denselben resolved Markt mehrfach anfasst (Log-Spam + unnötige API-Last).
|
||||
private readonly ConcurrentDictionary<string, bool> _resolvedDemoMarkets = new();
|
||||
private readonly ConcurrentDictionary<string, DateTime> _lastPolled = new();
|
||||
private readonly ConcurrentDictionary<string, bool> _activeWssPolls = new();
|
||||
private DateTime _lastLivePoll = DateTime.MinValue;
|
||||
@@ -498,9 +501,14 @@ namespace PolyTraderSharp.Services
|
||||
|
||||
foreach (var pos in checkPositions)
|
||||
{
|
||||
// K3: Bereits aufgelöste Märkte nicht erneut anfragen (verhindert Endlos-Loop +
|
||||
// API-Last, falls der Close noch nicht durch den Engine-Sync entfernt wurde).
|
||||
if (_resolvedDemoMarkets.ContainsKey(pos.TokenId)) continue;
|
||||
|
||||
var (isClosed, isWinner) = await _api.CheckMarketResolutionAsync(pos.MarketSlug, pos.TokenId);
|
||||
if (isClosed)
|
||||
{
|
||||
_resolvedDemoMarkets[pos.TokenId] = true;
|
||||
decimal exitPrice = isWinner ? 1.0m : 0.0m;
|
||||
_logger.Info($"🏆 Demo Market {pos.MarketQuestion} aufgelöst! Auszahlung: ${(exitPrice * pos.Size):F2}");
|
||||
|
||||
|
||||
@@ -203,5 +203,18 @@ namespace PolyTrader.Tests
|
||||
{
|
||||
Assert.Equal(expected, IsBelowPolymarketMinimum((decimal)shares, (decimal)usdc));
|
||||
}
|
||||
|
||||
// ---------------- IsAuthorizedSell (K3) ----------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 7, true)] // System-Signal schließt fremde Copy-Position (Demo-Resolution)
|
||||
[InlineData(0, 0, true)] // System-Signal, keine Herkunft
|
||||
[InlineData(7, 7, true)] // Master schließt eigene Position
|
||||
[InlineData(7, 9, false)] // Master 7 darf Position von Master 9 nicht schließen
|
||||
[InlineData(9, 0, false)] // echter Master darf herrenlose Position nicht kapern
|
||||
public void IsAuthorizedSell_allows_owner_and_system(int signalTraderId, int positionSourceTraderId, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsAuthorizedSell(signalTraderId, positionSourceTraderId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user