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)}"); + } + } +}