Phase 3: Copy-Score + Auto-Pause-Kill-Switch (Trader-Intelligence)

Aus UNSEREN geschlossenen Trades (nicht der externen Data-API) berechnet -> jetzt machbar.

- TraderScore (pure, getestet): Compute (CopyPnl/ProfitFactor/AvgPnlPerTrade/Count
  aus realisierten PnLs) + ShouldAutoPause (enabled & count>=minTrades & pnl<=-threshold).
- TrackedTrader: CopyPnl30d/CopyProfitFactor/CopyAvgPnlPerTrade/CopyTradeCount30d
  (mit Erklärungen) + Migration AddTraderCopyScore (auf MySQL angewendet).
- CopyTradingState: globale Auto-Pause-Config (AutoPauseMinTrades 10,
  AutoPauseDrawdownUsd 10). Per-Master-Schalter TrackedTrader.AutoPauseEnabled.
- MasterTraderAnalyticsJob.UpdateCopyScoresAndAutoPauseAsync (entkoppelt von der
  flakigen Master-History-API): Copy-Score je Master (30T), harte Auto-Pause bei
  Verlust über Schwelle (IsActive=false, Reasoning+Zeitstempel, Threema; Reaktivierung
  nur manuell). Injiziert ICopyTradeLogRepository + ThreemaService.
- MasterTradersView: 4 Copy-Score-Spalten.

193 Tests gruen. Build/Smoke gruen.
OFFEN (3.2, API-abhaengig, Zielland): Sniper-Metriken (MedianHold/SellWithin5Min)
aus der Data-API (Portierung analyze_snipers.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-07 18:19:41 +02:00
co-authored by Claude Opus 4.8
parent cda5b055aa
commit 4619edbb99
11 changed files with 626 additions and 3 deletions
@@ -1,4 +1,5 @@
using System;
using PolyTrader.Modules.CopyTrading.Logic;
using PolyTrader.Modules.CopyTrading.Persistence;
using System.Collections.Generic;
using System.Linq;
@@ -17,16 +18,20 @@ namespace PolyTraderSharp.Services
private readonly TerminalLogger _logger;
private readonly IMasterTraderHistoryRepository _historyRepo;
private readonly ITrackedTraderRepository _traderRepo;
private readonly ICopyTradeLogRepository _tradeLog;
private readonly ThreemaService _threema;
private readonly JobStatusRow _jobStatus;
private readonly PolymarketApiService _api;
public MasterTraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMasterTraderHistoryRepository historyRepo, ITrackedTraderRepository traderRepo, JobManager jobManager, PolymarketApiService api)
public MasterTraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMasterTraderHistoryRepository historyRepo, ITrackedTraderRepository traderRepo, ICopyTradeLogRepository tradeLog, ThreemaService threema, JobManager jobManager, PolymarketApiService api)
{
_state = state;
_copyState = copyState;
_logger = logger;
_historyRepo = historyRepo;
_traderRepo = traderRepo;
_tradeLog = tradeLog;
_threema = threema;
_api = api;
_jobStatus = new JobStatusRow
@@ -187,11 +192,61 @@ namespace PolyTraderSharp.Services
}
_logger.Info("✅ Master-Trader Historien-Analyse abgeschlossen.");
// Phase 3: Copy-Score + Auto-Pause entkoppelt von der (flakigen) Master-History-API,
// da aus UNSEREN geschlossenen Trades berechnet.
await UpdateCopyScoresAndAutoPauseAsync();
}
catch (Exception ex)
{
_logger.Error($"MasterTraderAnalyticsJob Exception: {ex}");
}
}
/// <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.
/// </summary>
private async Task UpdateCopyScoresAndAutoPauseAsync()
{
var since = DateTime.UtcNow.AddDays(-30);
foreach (var trader in _copyState.Traders.Values.ToList())
{
try
{
var pnls = _tradeLog
.Find(t => t.SourceTraderId == trader.Id && t.ClosedAt >= since && !t.IsDemo)
.Select(t => t.RealizedPnl);
var m = TraderScore.Compute(pnls);
trader.CopyPnl30d = m.CopyPnl;
trader.CopyProfitFactor = m.ProfitFactor;
trader.CopyAvgPnlPerTrade = m.AvgPnlPerTrade;
trader.CopyTradeCount30d = m.TradeCount;
bool pause = trader.IsActive && TraderScore.ShouldAutoPause(
trader.AutoPauseEnabled, m.TradeCount, m.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."); }
catch (Exception ex) { _logger.Error($"Threema Auto-Pause-Benachrichtigung fehlgeschlagen: {ex.Message}"); }
}
_copyState.Traders[trader.Id] = trader; // Hot-Path-State synchron halten
_traderRepo.Update(trader);
}
catch (Exception ex)
{
_logger.Error($"Copy-Score/Auto-Pause für '{trader.DisplayName}' fehlgeschlagen: {ex.Message}");
}
}
}
}
}