diff --git a/src/PolyTrader.Modules.CopyTrading/Models/CopyTradingAccountSettings.cs b/src/PolyTrader.Modules.CopyTrading/Models/CopyTradingAccountSettings.cs
index 5f4a209..a00af37 100644
--- a/src/PolyTrader.Modules.CopyTrading/Models/CopyTradingAccountSettings.cs
+++ b/src/PolyTrader.Modules.CopyTrading/Models/CopyTradingAccountSettings.cs
@@ -7,6 +7,9 @@ namespace PolyTraderSharp.Models
/// Bewusst getrennt vom Core- (allgemeine Account-Daten):
/// Diese Werte gehören dem Copytrading-Modul und werden im Modul-View bearbeitet.
/// Persistiert in Tabelle "mod_copytrading_account_settings" (Key = AccountId).
+ ///
+ /// Die [Description]-Attribute erscheinen im PropertyGrid als Erklärung – bitte bei
+ /// jeder Verhaltensänderung mitpflegen, damit die Bedeutung nachvollziehbar bleibt.
///
public class CopyTradingAccountSettings
{
@@ -14,33 +17,86 @@ namespace PolyTraderSharp.Models
public int AccountId { get; set; }
[Category("01. Risk Management")]
+ [DisplayName("Limit pro Markt (%)")]
+ [Description("Maximaler Einsatz in EINEN einzelnen Markt, als Prozent des Account-Guthabens (TotalBalance). " +
+ "Beispiel: 5 = höchstens 5 % des Guthabens in einem Markt. Begrenzt das Klumpenrisiko pro Markt.")]
public decimal PerMarketLimit { get; set; } = 5.0m;
[Category("01. Risk Management")]
- public decimal MaxPriceDifference { get; set; } = 2.0m;
+ [DisplayName("Limit pro Master (%)")]
+ [Description("Maximaler gleichzeitiger Gesamteinsatz in Trades EINES Master-Traders, als Prozent des Guthabens. " +
+ "Beispiel: 10 = höchstens 10 % des Guthabens gleichzeitig einem Master folgen. Begrenzt den Schaden, " +
+ "wenn ein einzelner Master schlecht läuft.")]
+ public decimal PerMasterLimit { get; set; } = 10.0m;
[Category("01. Risk Management")]
+ [DisplayName("Max. Kaufpreis (0–1)")]
+ [Description("Absolute Preis-Obergrenze pro Share auf der 0–1-Skala (1.00 = 100 ¢). Kauf wird übersprungen, wenn der " +
+ "Master über diesem Preis kauft; zusätzlich wird unser Limitpreis hierauf gedeckelt. " +
+ "Beispiel: 0.98 = nichts über 98 ¢ kaufen (nahe 1.00 ist das Aufwärtspotenzial gering, das Risiko asymmetrisch).")]
public decimal MaxBuyPrice { get; set; } = 0.98m;
[Category("01. Risk Management")]
- public decimal ProfitTarget { get; set; } = 50.0m;
+ [DisplayName("Max. Preisaufschlag BUY (%)")]
+ [Description("Wie viel Prozent ÜBER dem Einstiegspreis des Masters wir beim BUY maximal limitieren, um einen Fill zu " +
+ "bekommen (gedeckelt durch 'Max. Kaufpreis'). Beispiel: 2 = unser Limit liegt 2 % über dem Master-Preis. " +
+ "Höher = mehr/schnellere Fills, aber schlechtere Einstiegspreise (mehr Slippage).")]
+ public decimal MaxPriceDifference { get; set; } = 2.0m;
- [Category("01. Risk Management")]
- public decimal PreRedeemLimit { get; set; } = 0.0m;
-
- [Category("01. Risk Management")]
- public decimal PerMasterLimit { get; set; } = 10.0m;
-
- [Category("02. Time Limits")]
+ [Category("02. Time Limits (Exposure nach Restlaufzeit)")]
+ [DisplayName("Limit < 6h Restlaufzeit (%)")]
+ [Description("Exposure-Deckel für Märkte, die in WENIGER als 6 Stunden auflaufen (Zeit bis Marktschluss), als Prozent " +
+ "des Guthabens. Die Summe aller offenen Positionen in dieser Zeitklasse darf diesen Anteil nicht übersteigen. " +
+ "Schnell auflaufende Märkte sind binärer/riskanter – daher meist niedriger.")]
public decimal perMaxTime6h { get; set; } = 20.0m;
- [Category("02. Time Limits")]
+ [Category("02. Time Limits (Exposure nach Restlaufzeit)")]
+ [DisplayName("Limit 6–24h Restlaufzeit (%)")]
+ [Description("Exposure-Deckel für Märkte, die in 6 bis 24 Stunden auflaufen, als Prozent des Guthabens.")]
public decimal perMaxTime24h { get; set; } = 20.0m;
- [Category("02. Time Limits")]
+ [Category("02. Time Limits (Exposure nach Restlaufzeit)")]
+ [DisplayName("Limit 24–72h Restlaufzeit (%)")]
+ [Description("Exposure-Deckel für Märkte, die in 24 bis 72 Stunden auflaufen, als Prozent des Guthabens.")]
public decimal perMaxTime72h { get; set; } = 20.0m;
- [Category("02. Time Limits")]
+ [Category("02. Time Limits (Exposure nach Restlaufzeit)")]
+ [DisplayName("Limit > 72h / kein Enddatum (%)")]
+ [Description("Exposure-Deckel für Märkte, die in MEHR als 72 Stunden auflaufen oder kein Enddatum haben, als Prozent " +
+ "des Guthabens. Meist höher, da mehr Zeit zur Erholung bleibt.")]
public decimal perMaxTimeNone { get; set; } = 40.0m;
+
+ [Category("03. Exit / Take-Profit")]
+ [DisplayName("Take-Profit (% Gewinn)")]
+ [Description("Take-Profit-Schwelle in Prozent Gewinn: erreicht der aktuelle Preis >= Einstiegspreis × (1 + Wert/100), " +
+ "wird verkauft. AKTUELL PROJEKTWEIT AUF 9999 = praktisch deaktiviert (Entscheidung: erst implementieren, " +
+ "dann bewusst scharf schalten). Die Verkaufslogik folgt in Phase 0.3 (über die SELL-Eskalationsleiter).")]
+ public decimal ProfitTarget { get; set; } = 9999m;
+
+ [Category("03. Exit / Take-Profit")]
+ [DisplayName("Pre-Redeem-Schwelle (0–1)")]
+ [Description("Auto-Redeem: erreicht der Marktpreis diesen Wert, wird die Position vorzeitig glattgestellt (near-resolution, " +
+ "spart das Warten bis zur Auflösung). 0 = deaktiviert. WICHTIG: Der Code vergleicht gegen den 0–1-Preis " +
+ "(z. B. 0.995 = 99,5 ¢). Migrierte Alt-Werte wie 99.5 triggern NIE und sollten auf z. B. 0.995 korrigiert werden.")]
+ public decimal PreRedeemLimit { get; set; } = 0.0m;
+
+ [Category("03. Exit / Take-Profit")]
+ [DisplayName("SELL-Floor (%) [Phase 0.1]")]
+ [Description("Untergrenze der SELL-Eskalationsleiter: absoluter Mindest-Verkaufspreis = Master-Exit-Preis × (1 − Wert/100). " +
+ "Unterhalb wird NICHT mehr verkauft, sondern die Position gehalten + Benachrichtigung. Schützt davor, in ein " +
+ "leergeräumtes Orderbuch als 'Exit-Liquidity' zu verkaufen. NOCH NICHT VERDRAHTET (Phase 0.1).")]
+ public decimal SellFloorPct { get; set; } = 15.0m;
+
+ [Category("04. Orderbuch / Ausführung")]
+ [DisplayName("Max. Spread BUY (%) [Phase 1.3]")]
+ [Description("Pre-Trade-Orderbuch-Check: ist der Spread (Ask − Bid) breiter als dieser Prozentsatz, wird der BUY " +
+ "übersprungen (illiquider/riskanter Markt, Sniping-Verdacht). NOCH NICHT VERDRAHTET (Phase 1.3).")]
+ public decimal MaxSpreadPct { get; set; } = 5.0m;
+
+ [Category("04. Orderbuch / Ausführung")]
+ [DisplayName("Min. Verkaufsquote (%) [Phase 2]")]
+ [Description("Mindest-Verkaufsquote des Masters, ab der wir einen Teilverkauf spiegeln. Verkäufe unter dieser Quote gelten " +
+ "als Rauschen (Day-Trader-Zappeln) und werden ignoriert. NOCH NICHT VERDRAHTET (Phase 2).")]
+ public decimal MinSellRatioPct { get; set; } = 10.0m;
}
}
diff --git a/src/PolyTrader.Modules.CopyTrading/Models/TrackedTrader.cs b/src/PolyTrader.Modules.CopyTrading/Models/TrackedTrader.cs
index 9b3ba69..1245357 100644
--- a/src/PolyTrader.Modules.CopyTrading/Models/TrackedTrader.cs
+++ b/src/PolyTrader.Modules.CopyTrading/Models/TrackedTrader.cs
@@ -4,49 +4,96 @@ using System.Collections.Generic;
namespace PolyTraderSharp.Models
{
+ ///
+ /// Ein überwachter Master-Trader, dessen Trades kopiert werden. Neben Identifikation und
+ /// Statistik enthält er per-Master-Verhaltenseinstellungen (Auto-Pause, Maker-Einstieg).
+ /// Die [Description]-Attribute erscheinen im PropertyGrid als Erklärung.
+ ///
public class TrackedTrader
{
[Browsable(false)]
public int Id { get; set; }
[Category("01. Identification")]
+ [DisplayName("Wallet-Adresse")]
+ [Description("Polygon-Wallet des Master-Traders. Diese Adresse wird on-chain (Alchemy-WSS) auf Käufe/Verkäufe überwacht.")]
public string WalletAddress { get; set; } = string.Empty;
[Category("01. Identification")]
+ [DisplayName("Anzeigename")]
+ [Description("Frei wählbarer Name zur Wiedererkennung in UI und Logs.")]
public string DisplayName { get; set; } = string.Empty;
[Category("02. Categorization")]
+ [DisplayName("Kategorie")]
+ [Description("Einordnung des Master-Typs (z. B. HF, INSIDER, SNIPER, HOLDER). Beeinflusst teils das Verhalten " +
+ "(z. B. engere SELL-Limits für 'HF'). Frei setzbar; für Verhaltensregeln bitte konsistente Werte verwenden.")]
public string Category { get; set; } = "NEW_BIG_BET";
[Category("02. Categorization")]
+ [DisplayName("Beschreibung")]
+ [Description("Freitext-Notiz zum Master (z. B. Herkunft, beobachtete Stärken/Schwächen).")]
public string Description { get; set; } = string.Empty;
[Category("02. Categorization")]
+ [DisplayName("Begründung / Reasoning")]
+ [Description("Warum kopieren wir diesen Master? Wird auch vom Auto-Kill-Switch mit Zeitstempel befüllt, wenn er " +
+ "automatisch pausiert wird (Phase 3.3).")]
public string Reasoning { get; set; } = string.Empty;
[Category("03. General")]
+ [DisplayName("Aktiv")]
+ [Description("Nur aktive Master werden kopiert und on-chain überwacht. Wird bei ausgelöster Auto-Pause automatisch " +
+ "auf 'false' gesetzt; Reaktivierung erfolgt bewusst nur manuell.")]
public bool IsActive { get; set; } = true;
[Category("03. General")]
+ [DisplayName("Ausgeblendet")]
+ [Description("Blendet den Master in der Standard-Ansicht aus, ohne ihn zu löschen (Archiv). Kopiert wird trotzdem nur, " +
+ "wenn 'Aktiv' gesetzt ist.")]
public bool IsHidden { get; set; } = false;
+ [Category("05. Copytrading-Verhalten")]
+ [DisplayName("Auto-Pause aktiv")]
+ [Description("Kill-Switch für diesen Master: Läuft die Copy-Performance der letzten Trades unter die Schwelle, wird der " +
+ "Master automatisch deaktiviert (Aktiv = false) samt Benachrichtigung, damit nachts nicht weiter Verlust " +
+ "aufläuft. STANDARDMÄSSIG AN; für einzelne Master bewusst abschaltbar, wenn du trotz Schwäche dranbleiben " +
+ "willst. Reaktivierung immer manuell. (Auslöse-Logik: Phase 3.3.)")]
+ public bool AutoPauseEnabled { get; set; } = true;
+
+ [Category("05. Copytrading-Verhalten")]
+ [DisplayName("Maker-Einstieg")]
+ [Description("Wenn aktiv, werden BUYs als GTC-Limit AUF dem Best-Bid platziert (Maker) statt teuer über dem Ask (Taker). " +
+ "Sinnvoll für langsame Master (Haltedauer Stunden/Tage): spart Taker-Fees und Spread. Kein Fill nach der " +
+ "konfigurierten Zeit → Eskalation auf Taker oder Verwerfen. (Ausführungs-Logik: Phase 4.)")]
+ public bool MakerEntry { get; set; } = false;
+
// Stats
[Category("04. Statistics")]
[ReadOnly(true)]
+ [DisplayName("Trades (7 Tage)")]
+ [Description("Anzahl der vom Analytics-Job erfassten Master-Trades der letzten 7 Tage (Kennzahl, kein Einstellwert).")]
public int TotalTrades { get; set; } = 0;
[Category("04. Statistics")]
[ReadOnly(true)]
+ [DisplayName("Gewinner-Trades")]
+ [Description("Anzahl profitabler Master-Trades im Auswertungsfenster (Kennzahl).")]
public int WinningTrades { get; set; } = 0;
[Category("04. Statistics")]
[ReadOnly(true)]
+ [DisplayName("Winrate (%)")]
+ [Description("Anteil profitabler Trades. ACHTUNG: allein irreführend – Favoriten-Käufer haben hohe Winrate und können " +
+ "trotzdem negativ sein. Profit-Faktor/Ø-PnL sind aussagekräftiger (Phase 3).")]
public double Winrate30t { get; set; } = 0.0;
[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.")]
public double TotalPnl { get; set; } = 0.0;
-
+
[Browsable(false)]
public HashSet AssignedAccountIds { get; set; } = new();
}
diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContext.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContext.cs
index 01d3134..ea2f161 100644
--- a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContext.cs
+++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/CopyTradingDbContext.cs
@@ -86,6 +86,9 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
e.Property(x => x.perMaxTime24h).HasPrecision(18, 6);
e.Property(x => x.perMaxTime72h).HasPrecision(18, 6);
e.Property(x => x.perMaxTimeNone).HasPrecision(18, 6);
+ e.Property(x => x.SellFloorPct).HasPrecision(18, 6);
+ e.Property(x => x.MaxSpreadPct).HasPrecision(18, 6);
+ e.Property(x => x.MinSellRatioPct).HasPrecision(18, 6);
});
b.Entity(e =>
diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260707073736_AddCopytradingBehaviorSettings.Designer.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260707073736_AddCopytradingBehaviorSettings.Designer.cs
new file mode 100644
index 0000000..1e1b8ba
--- /dev/null
+++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260707073736_AddCopytradingBehaviorSettings.Designer.cs
@@ -0,0 +1,270 @@
+//
+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("20260707073736_AddCopytradingBehaviorSettings")]
+ partial class AddCopytradingBehaviorSettings
+ {
+ ///
+ 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("TradeId")
+ .HasColumnType("int");
+
+ b.Property("AccountId")
+ .HasColumnType("int");
+
+ b.Property("ClosedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EntryPrice")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("ExitPrice")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("ExitReason")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("IsDemo")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("MarketQuestion")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("varchar(1000)");
+
+ b.Property("MarketSlug")
+ .IsRequired()
+ .HasMaxLength(300)
+ .HasColumnType("varchar(300)");
+
+ b.Property("OpenedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Outcome")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("PnlPercent")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("RealizedPnl")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("Side")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("varchar(10)");
+
+ b.Property("Size")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("SourceTraderId")
+ .HasColumnType("int");
+
+ b.Property("TokenId")
+ .IsRequired()
+ .HasMaxLength(120)
+ .HasColumnType("varchar(120)");
+
+ b.Property("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("AccountId")
+ .HasColumnType("int");
+
+ b.Property("MaxBuyPrice")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("MaxPriceDifference")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("MaxSpreadPct")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("MinSellRatioPct")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("PerMarketLimit")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("PerMasterLimit")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("PreRedeemLimit")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("ProfitTarget")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("SellFloorPct")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("perMaxTime24h")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("perMaxTime6h")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("perMaxTime72h")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)");
+
+ b.Property("ClosedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RealizedPnl")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("TokenId")
+ .IsRequired()
+ .HasMaxLength(120)
+ .HasColumnType("varchar(120)");
+
+ b.Property("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("Id")
+ .HasColumnType("int");
+
+ b.Property("AssignedAccountIds")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("AutoPauseEnabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("varchar(1000)");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("IsActive")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsHidden")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("MakerEntry")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Reasoning")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("varchar(1000)");
+
+ b.Property("TotalPnl")
+ .HasColumnType("double");
+
+ b.Property("TotalTrades")
+ .HasColumnType("int");
+
+ b.Property("WalletAddress")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)");
+
+ b.Property("WinningTrades")
+ .HasColumnType("int");
+
+ b.Property("Winrate30t")
+ .HasColumnType("double");
+
+ b.HasKey("Id");
+
+ b.ToTable("mod_copytrading_traders", (string)null);
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260707073736_AddCopytradingBehaviorSettings.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260707073736_AddCopytradingBehaviorSettings.cs
new file mode 100644
index 0000000..3e0de1b
--- /dev/null
+++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/20260707073736_AddCopytradingBehaviorSettings.cs
@@ -0,0 +1,83 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
+{
+ ///
+ public partial class AddCopytradingBehaviorSettings : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "AutoPauseEnabled",
+ table: "mod_copytrading_traders",
+ type: "tinyint(1)",
+ nullable: false,
+ defaultValue: true);
+
+ migrationBuilder.AddColumn(
+ name: "MakerEntry",
+ table: "mod_copytrading_traders",
+ type: "tinyint(1)",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "MaxSpreadPct",
+ table: "mod_copytrading_account_settings",
+ type: "decimal(18,6)",
+ precision: 18,
+ scale: 6,
+ nullable: false,
+ defaultValue: 5m);
+
+ migrationBuilder.AddColumn(
+ name: "MinSellRatioPct",
+ table: "mod_copytrading_account_settings",
+ type: "decimal(18,6)",
+ precision: 18,
+ scale: 6,
+ nullable: false,
+ defaultValue: 10m);
+
+ migrationBuilder.AddColumn(
+ name: "SellFloorPct",
+ table: "mod_copytrading_account_settings",
+ type: "decimal(18,6)",
+ precision: 18,
+ scale: 6,
+ nullable: false,
+ defaultValue: 15m);
+
+ // Entscheidung Richard: ProfitTarget zunächst überall auf 9999 (praktisch deaktiviert),
+ // bis die Take-Profit-Logik (Phase 0.3) scharf geschaltet wird.
+ migrationBuilder.Sql("UPDATE mod_copytrading_account_settings SET ProfitTarget = 9999;");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "AutoPauseEnabled",
+ table: "mod_copytrading_traders");
+
+ migrationBuilder.DropColumn(
+ name: "MakerEntry",
+ table: "mod_copytrading_traders");
+
+ migrationBuilder.DropColumn(
+ name: "MaxSpreadPct",
+ table: "mod_copytrading_account_settings");
+
+ migrationBuilder.DropColumn(
+ name: "MinSellRatioPct",
+ table: "mod_copytrading_account_settings");
+
+ migrationBuilder.DropColumn(
+ name: "SellFloorPct",
+ table: "mod_copytrading_account_settings");
+ }
+ }
+}
diff --git a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/CopyTradingDbContextModelSnapshot.cs b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/CopyTradingDbContextModelSnapshot.cs
index baf2604..183cadc 100644
--- a/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/CopyTradingDbContextModelSnapshot.cs
+++ b/src/PolyTrader.Modules.CopyTrading/Persistence/Ef/Migrations/CopyTradingDbContextModelSnapshot.cs
@@ -120,6 +120,14 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
+ b.Property("MaxSpreadPct")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
+ b.Property("MinSellRatioPct")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
b.Property("PerMarketLimit")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
@@ -136,6 +144,10 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
+ b.Property("SellFloorPct")
+ .HasPrecision(18, 6)
+ .HasColumnType("decimal(18,6)");
+
b.Property("perMaxTime24h")
.HasPrecision(18, 6)
.HasColumnType("decimal(18,6)");
@@ -196,6 +208,9 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
.IsRequired()
.HasColumnType("text");
+ b.Property("AutoPauseEnabled")
+ .HasColumnType("tinyint(1)");
+
b.Property("Category")
.IsRequired()
.HasMaxLength(64)
@@ -217,6 +232,9 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
b.Property("IsHidden")
.HasColumnType("tinyint(1)");
+ b.Property("MakerEntry")
+ .HasColumnType("tinyint(1)");
+
b.Property("Reasoning")
.IsRequired()
.HasMaxLength(1000)
diff --git a/tests/PolyTrader.Tests/DbContextModelTests.cs b/tests/PolyTrader.Tests/DbContextModelTests.cs
index 4f2c7e1..870b966 100644
--- a/tests/PolyTrader.Tests/DbContextModelTests.cs
+++ b/tests/PolyTrader.Tests/DbContextModelTests.cs
@@ -48,6 +48,9 @@ namespace PolyTrader.Tests
[InlineData(nameof(CopyTradingAccountSettings.PerMarketLimit))]
[InlineData(nameof(CopyTradingAccountSettings.PerMasterLimit))]
[InlineData(nameof(CopyTradingAccountSettings.perMaxTimeNone))]
+ [InlineData(nameof(CopyTradingAccountSettings.SellFloorPct))]
+ [InlineData(nameof(CopyTradingAccountSettings.MaxSpreadPct))]
+ [InlineData(nameof(CopyTradingAccountSettings.MinSellRatioPct))]
public void Settings_money_fields_have_precision_18_6(string name)
{
using var ctx = CtCtx();
diff --git a/tests/PolyTrader.Tests/SettingsDefaultsTests.cs b/tests/PolyTrader.Tests/SettingsDefaultsTests.cs
new file mode 100644
index 0000000..6aeed0e
--- /dev/null
+++ b/tests/PolyTrader.Tests/SettingsDefaultsTests.cs
@@ -0,0 +1,53 @@
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using PolyTraderSharp.Models;
+using Xunit;
+
+namespace PolyTrader.Tests
+{
+ ///
+ /// Fixiert die entscheidungsrelevanten Default-Werte und stellt sicher, dass alle
+ /// einstellbaren Settings eine [Description] als Erklärung tragen (Richards Vorgabe).
+ ///
+ public class SettingsDefaultsTests
+ {
+ [Fact]
+ public void ProfitTarget_defaults_to_9999_inactive()
+ {
+ Assert.Equal(9999m, new CopyTradingAccountSettings().ProfitTarget);
+ }
+
+ [Fact]
+ public void New_account_settings_have_expected_defaults()
+ {
+ var s = new CopyTradingAccountSettings();
+ Assert.Equal(15.0m, s.SellFloorPct);
+ Assert.Equal(5.0m, s.MaxSpreadPct);
+ Assert.Equal(10.0m, s.MinSellRatioPct);
+ }
+
+ [Fact]
+ public void Master_auto_pause_is_on_by_default_maker_off()
+ {
+ var t = new TrackedTrader();
+ Assert.True(t.AutoPauseEnabled);
+ Assert.False(t.MakerEntry);
+ }
+
+ [Theory]
+ [InlineData(typeof(CopyTradingAccountSettings))]
+ [InlineData(typeof(TrackedTrader))]
+ public void All_browsable_writable_settings_have_a_description(System.Type type)
+ {
+ var missing = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(p => p.CanWrite)
+ .Where(p => p.GetCustomAttribute()?.Browsable != false)
+ .Where(p => string.IsNullOrWhiteSpace(p.GetCustomAttribute()?.Description))
+ .Select(p => p.Name)
+ .ToList();
+
+ Assert.True(missing.Count == 0, $"Ohne [Description]: {string.Join(", ", missing)}");
+ }
+ }
+}