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:
co-authored by
Claude Opus 4.8
parent
cda5b055aa
commit
4619edbb99
@@ -18,6 +18,10 @@ namespace PolyTraderSharp
|
||||
// Copytrading-Risk-Regel: mindestens 6 Shares pro Order erzwingen.
|
||||
public bool SixSharesMinimum { get; set; } = true;
|
||||
|
||||
// Auto-Pause-Kill-Switch (Phase 3.3), global. Pro-Master-Schalter: TrackedTrader.AutoPauseEnabled.
|
||||
public int AutoPauseMinTrades { get; set; } = 10; // Mindest-Stichprobe vor Pause
|
||||
public decimal AutoPauseDrawdownUsd { get; set; } = 10m; // Copy-PnL (30T) darunter -> Pause
|
||||
|
||||
// Kopierte Master-Trader (TraderId -> TrackedTrader)
|
||||
public ConcurrentDictionary<int, TrackedTrader> Traders { get; } = new();
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// Reine Trader-Intelligence-Logik (Phase 3): Kennzahlen dazu, was WIR mit einem Master
|
||||
/// verdient haben (Copy-PnL aus unseren geschlossenen Trades), plus die Auto-Pause-Entscheidung.
|
||||
/// Bewusst pur/testbar; die Datenbeschaffung (Filter SourceTraderId + Zeitfenster) macht der Job.
|
||||
/// </summary>
|
||||
public static class TraderScore
|
||||
{
|
||||
/// <summary>Sentinel für Profit-Faktor ohne jeden Verlust (sonst Division durch 0).</summary>
|
||||
public const decimal NoLossProfitFactor = 999m;
|
||||
|
||||
public readonly record struct CopyMetrics(int TradeCount, decimal CopyPnl, decimal ProfitFactor, decimal AvgPnlPerTrade);
|
||||
|
||||
/// <summary>
|
||||
/// Berechnet Copy-Kennzahlen aus den realisierten PnLs unserer Trades eines Masters
|
||||
/// (bereits gefiltert auf Master + Zeitfenster).
|
||||
/// </summary>
|
||||
public static CopyMetrics Compute(IEnumerable<decimal> realizedPnls)
|
||||
{
|
||||
int count = 0;
|
||||
decimal sum = 0m, grossProfit = 0m, grossLoss = 0m;
|
||||
|
||||
foreach (var pnl in realizedPnls)
|
||||
{
|
||||
count++;
|
||||
sum += pnl;
|
||||
if (pnl > 0m) grossProfit += pnl;
|
||||
else if (pnl < 0m) grossLoss += -pnl;
|
||||
}
|
||||
|
||||
decimal profitFactor =
|
||||
grossLoss > 0m ? grossProfit / grossLoss :
|
||||
grossProfit > 0m ? NoLossProfitFactor : 0m;
|
||||
|
||||
decimal avg = count > 0 ? sum / count : 0m;
|
||||
return new CopyMetrics(count, sum, profitFactor, avg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-Pause-Kill-Switch (Phase 3.3): pausiert einen Master hart, wenn er über genügend
|
||||
/// Trades ins Minus läuft. Reaktivierung bewusst nur manuell (hier nicht abgebildet).
|
||||
/// </summary>
|
||||
/// <param name="autoPauseEnabled">Per-Master-Flag (Default an).</param>
|
||||
/// <param name="tradeCount">Anzahl Copy-Trades im Fenster.</param>
|
||||
/// <param name="copyPnl">Summierter Copy-PnL im Fenster.</param>
|
||||
/// <param name="minTrades">Mindestanzahl Trades, bevor pausiert wird (Rausch-Schutz).</param>
|
||||
/// <param name="drawdownThresholdUsd">Max. erlaubter Verlust (positiv); darunter → Pause.</param>
|
||||
public static bool ShouldAutoPause(bool autoPauseEnabled, int tradeCount, decimal copyPnl, int minTrades, decimal drawdownThresholdUsd)
|
||||
{
|
||||
if (!autoPauseEnabled) return false;
|
||||
if (tradeCount < minTrades) return false;
|
||||
return copyPnl <= -drawdownThresholdUsd;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,9 +91,35 @@ namespace PolyTraderSharp.Models
|
||||
[Category("04. Statistics")]
|
||||
[ReadOnly(true)]
|
||||
[DisplayName("PnL (7 Tage)")]
|
||||
[Description("Realisierter Gewinn/Verlust des MASTERS im Fenster (nicht unser Copy-Ergebnis). Copy-PnL folgt in Phase 3.")]
|
||||
[Description("Realisierter Gewinn/Verlust des MASTERS im Fenster (nicht unser Copy-Ergebnis).")]
|
||||
public double TotalPnl { get; set; } = 0.0;
|
||||
|
||||
// --- Copy-Score (Phase 3): was WIR mit diesem Master verdient haben, letzte 30 Tage ---
|
||||
|
||||
[Category("05. Copy-Score (30 Tage)")]
|
||||
[ReadOnly(true)]
|
||||
[DisplayName("Copy-PnL (30T)")]
|
||||
[Description("Summierter realisierter Gewinn/Verlust UNSERER kopierten Trades dieses Masters der letzten 30 Tage (inkl. unserer Slippage/Fees). Primäre Rentabilitätskennzahl – aussagekräftiger als die Master-eigene Winrate.")]
|
||||
public decimal CopyPnl30d { get; set; }
|
||||
|
||||
[Category("05. Copy-Score (30 Tage)")]
|
||||
[ReadOnly(true)]
|
||||
[DisplayName("Profit-Faktor (30T)")]
|
||||
[Description("Bruttogewinn / Bruttoverlust unserer Copy-Trades (30T). > 1 = profitabel, < 1 = Verlust. 999 = bisher kein Verlust. Robuster als die reine Winrate.")]
|
||||
public decimal CopyProfitFactor { get; set; }
|
||||
|
||||
[Category("05. Copy-Score (30 Tage)")]
|
||||
[ReadOnly(true)]
|
||||
[DisplayName("Ø PnL/Trade (30T)")]
|
||||
[Description("Durchschnittlicher Copy-PnL pro Trade (30T). Negativ = dieser Master kostet uns im Schnitt Geld.")]
|
||||
public decimal CopyAvgPnlPerTrade { get; set; }
|
||||
|
||||
[Category("05. Copy-Score (30 Tage)")]
|
||||
[ReadOnly(true)]
|
||||
[DisplayName("Copy-Trades (30T)")]
|
||||
[Description("Anzahl unserer geschlossenen Copy-Trades dieses Masters in den letzten 30 Tagen (Stichprobengröße für die obigen Kennzahlen).")]
|
||||
public int CopyTradeCount30d { get; set; }
|
||||
|
||||
[Browsable(false)]
|
||||
public HashSet<int> AssignedAccountIds { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
|
||||
e.Property(x => x.Category).HasMaxLength(64);
|
||||
e.Property(x => x.Description).HasMaxLength(1000);
|
||||
e.Property(x => x.Reasoning).HasMaxLength(1000);
|
||||
e.Property(x => x.CopyPnl30d).HasPrecision(18, 6);
|
||||
e.Property(x => x.CopyProfitFactor).HasPrecision(18, 6);
|
||||
e.Property(x => x.CopyAvgPnlPerTrade).HasPrecision(18, 6);
|
||||
|
||||
var comparer = new ValueComparer<HashSet<int>>(
|
||||
(a, c) => (a == null && c == null) || (a != null && c != null && a.SetEquals(c)),
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
{
|
||||
[DbContext(typeof(CopyTradingDbContext))]
|
||||
[Migration("20260707160622_AddTraderCopyScore")]
|
||||
partial class AddTraderCopyScore
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.ClosedTrade", b =>
|
||||
{
|
||||
b.Property<int>("TradeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("EntryPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("ExitPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("ExitReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("MarketQuestion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<DateTime>("OpenedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<decimal>("PnlPercent")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("RealizedPnl")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("SourceTraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<decimal>("TotalFees")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.HasKey("TradeId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("SourceTraderId");
|
||||
|
||||
b.HasIndex("TokenId");
|
||||
|
||||
b.ToTable("mod_copytrading_closed_trades", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.CopyTradingAccountSettings", b =>
|
||||
{
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MaxBuyPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MaxPriceDifference")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MaxSpreadPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MinSellRatioPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PerMarketLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PerMasterLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PreRedeemLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("ProfitTarget")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("SellFloorPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime24h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime6h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime72h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTimeNone")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.HasKey("AccountId");
|
||||
|
||||
b.ToTable("mod_copytrading_account_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.MasterTraderHistoryRecord", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<DateTime>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("RealizedPnl")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClosedAt");
|
||||
|
||||
b.HasIndex("TraderId");
|
||||
|
||||
b.ToTable("mod_copytrading_mt_history", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.TrackedTrader", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("AssignedAccountIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("AutoPauseEnabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("CopyAvgPnlPerTrade")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("CopyPnl30d")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("CopyProfitFactor")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("CopyTradeCount30d")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("MakerEntry")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Reasoning")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<double>("TotalPnl")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.Property<int>("TotalTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("WalletAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("WinningTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double>("Winrate30t")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("mod_copytrading_traders", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTraderCopyScore : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "CopyAvgPnlPerTrade",
|
||||
table: "mod_copytrading_traders",
|
||||
type: "decimal(18,6)",
|
||||
precision: 18,
|
||||
scale: 6,
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "CopyPnl30d",
|
||||
table: "mod_copytrading_traders",
|
||||
type: "decimal(18,6)",
|
||||
precision: 18,
|
||||
scale: 6,
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "CopyProfitFactor",
|
||||
table: "mod_copytrading_traders",
|
||||
type: "decimal(18,6)",
|
||||
precision: 18,
|
||||
scale: 6,
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CopyTradeCount30d",
|
||||
table: "mod_copytrading_traders",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CopyAvgPnlPerTrade",
|
||||
table: "mod_copytrading_traders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CopyPnl30d",
|
||||
table: "mod_copytrading_traders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CopyProfitFactor",
|
||||
table: "mod_copytrading_traders");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CopyTradeCount30d",
|
||||
table: "mod_copytrading_traders");
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -216,6 +216,21 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("CopyAvgPnlPerTrade")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("CopyPnl30d")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("CopyProfitFactor")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("CopyTradeCount30d")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ namespace PolyTrader.Modules.CopyTrading.Ui
|
||||
this.colTrades = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.colWinrate = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.colPnl = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.colCopyPnl = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.colCopyPf = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.colCopyAvg = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.colCopyCount = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.splitter = new System.Windows.Forms.Splitter();
|
||||
this.rightPanel = new System.Windows.Forms.Panel();
|
||||
this.pgDetail = new System.Windows.Forms.PropertyGrid();
|
||||
@@ -107,7 +111,11 @@ namespace PolyTrader.Modules.CopyTrading.Ui
|
||||
this.colActive,
|
||||
this.colTrades,
|
||||
this.colWinrate,
|
||||
this.colPnl});
|
||||
this.colPnl,
|
||||
this.colCopyPnl,
|
||||
this.colCopyPf,
|
||||
this.colCopyAvg,
|
||||
this.colCopyCount});
|
||||
this.grid.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.grid.Location = new System.Drawing.Point(0, 25);
|
||||
this.grid.MultiSelect = false;
|
||||
@@ -182,6 +190,38 @@ namespace PolyTrader.Modules.CopyTrading.Ui
|
||||
this.colPnl.ReadOnly = true;
|
||||
this.colPnl.Width = 100;
|
||||
//
|
||||
// colCopyPnl
|
||||
//
|
||||
this.colCopyPnl.DataPropertyName = "CopyPnl30d";
|
||||
this.colCopyPnl.HeaderText = "Copy-PnL (30T)";
|
||||
this.colCopyPnl.Name = "colCopyPnl";
|
||||
this.colCopyPnl.ReadOnly = true;
|
||||
this.colCopyPnl.Width = 110;
|
||||
//
|
||||
// colCopyPf
|
||||
//
|
||||
this.colCopyPf.DataPropertyName = "CopyProfitFactor";
|
||||
this.colCopyPf.HeaderText = "Profit-Faktor";
|
||||
this.colCopyPf.Name = "colCopyPf";
|
||||
this.colCopyPf.ReadOnly = true;
|
||||
this.colCopyPf.Width = 100;
|
||||
//
|
||||
// colCopyAvg
|
||||
//
|
||||
this.colCopyAvg.DataPropertyName = "CopyAvgPnlPerTrade";
|
||||
this.colCopyAvg.HeaderText = "Ø PnL/Trade";
|
||||
this.colCopyAvg.Name = "colCopyAvg";
|
||||
this.colCopyAvg.ReadOnly = true;
|
||||
this.colCopyAvg.Width = 100;
|
||||
//
|
||||
// colCopyCount
|
||||
//
|
||||
this.colCopyCount.DataPropertyName = "CopyTradeCount30d";
|
||||
this.colCopyCount.HeaderText = "Copy-Trades";
|
||||
this.colCopyCount.Name = "colCopyCount";
|
||||
this.colCopyCount.ReadOnly = true;
|
||||
this.colCopyCount.Width = 90;
|
||||
//
|
||||
// splitter
|
||||
//
|
||||
this.splitter.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
@@ -292,6 +332,10 @@ namespace PolyTrader.Modules.CopyTrading.Ui
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colTrades;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colWinrate;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colPnl;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyPnl;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyPf;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyAvg;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn colCopyCount;
|
||||
private System.Windows.Forms.Splitter splitter;
|
||||
private System.Windows.Forms.Panel rightPanel;
|
||||
private System.Windows.Forms.PropertyGrid pgDetail;
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace PolyTrader.Modules.CopyTrading.Ui
|
||||
|
||||
colWinrate.DefaultCellStyle.Format = "F1";
|
||||
colPnl.DefaultCellStyle.Format = "F2";
|
||||
colCopyPnl.DefaultCellStyle.Format = "F2";
|
||||
colCopyPf.DefaultCellStyle.Format = "F2";
|
||||
colCopyAvg.DefaultCellStyle.Format = "F2";
|
||||
|
||||
tsNew.Click += (_, _) => AddNew();
|
||||
tsSave.Click += (_, _) => SaveCurrent();
|
||||
|
||||
Reference in New Issue
Block a user