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>
63 lines
2.2 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|