Files
RichardandClaude Opus 4.8 4619edbb99 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>
2026-07-07 18:19:41 +02:00

63 lines
2.2 KiB
C#

using System.Linq;
using PolyTrader.Modules.CopyTrading.Logic;
using Xunit;
using static PolyTrader.Modules.CopyTrading.Logic.TraderScore;
namespace PolyTrader.Tests
{
/// <summary>
/// Sicherheitsnetz für die Copy-Score-Kennzahlen und den Auto-Pause-Kill-Switch (Phase 3).
/// </summary>
public class TraderScoreTests
{
[Fact]
public void Compute_metrics_from_mixed_pnls()
{
var m = Compute(new[] { 10m, -3m, 5m, -2m });
Assert.Equal(4, m.TradeCount);
Assert.Equal(10m, m.CopyPnl);
Assert.Equal(3.0m, m.ProfitFactor); // grossProfit 15 / grossLoss 5
Assert.Equal(2.5m, m.AvgPnlPerTrade); // 10 / 4
}
[Fact]
public void Compute_empty_is_all_zero()
{
var m = Compute(Enumerable.Empty<decimal>());
Assert.Equal(0, m.TradeCount);
Assert.Equal(0m, m.CopyPnl);
Assert.Equal(0m, m.ProfitFactor);
Assert.Equal(0m, m.AvgPnlPerTrade);
}
[Fact]
public void Compute_all_wins_uses_no_loss_sentinel()
{
var m = Compute(new[] { 5m, 5m });
Assert.Equal(NoLossProfitFactor, m.ProfitFactor);
Assert.Equal(10m, m.CopyPnl);
}
[Fact]
public void Compute_all_losses_profit_factor_zero()
{
var m = Compute(new[] { -5m, -5m });
Assert.Equal(0m, m.ProfitFactor);
Assert.Equal(-10m, m.CopyPnl);
Assert.Equal(-5m, m.AvgPnlPerTrade);
}
[Theory]
[InlineData(false, 20, -50, 10, 10, false)] // deaktiviert
[InlineData(true, 5, -50, 10, 10, false)] // zu wenige Trades
[InlineData(true, 12, -15, 10, 10, true)] // Verlust über Schwelle
[InlineData(true, 12, -5, 10, 10, false)] // Verlust unter Schwelle
[InlineData(true, 12, 20, 10, 10, false)] // im Plus
[InlineData(true, 12, -10, 10, 10, true)] // genau auf der Schwelle
public void ShouldAutoPause_rules(bool enabled, int count, double pnl, int minTrades, double threshold, bool expected)
{
Assert.Equal(expected, ShouldAutoPause(enabled, count, (decimal)pnl, minTrades, (decimal)threshold));
}
}
}