From 0725a81dbdc9bb0487c5ee8849700ff36e022b44 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 20 Jul 2026 20:49:41 +0200 Subject: [PATCH] UI Slice 3: CopyTrading - Zeilenfaerbung, Filter, Offene-Trades-Tab, Master-Grid-Fix - Geschlossene Trades: Zeilenfaerbung nach PnL-% (TradeRowColoring, pur+getestet: <0 rot, 0-10% hellgruen, >10% gruen) via DataBindingComplete. Filter-Panel (Designer): Markt (Text), Master-Trader (Combo, dynamisch), Ergebnis (Alle/Gewinner/Verlierer), Von/Bis (optionale DateTimePicker mit Checkbox), Zuruecksetzen. Summary zeigt gefiltert/gesamt. - Neuer Tab 'Offene Trades' (OpenTradesView, designerfaehig): alle offenen Positionen aus dem Laufzeit-State mit Entry/Aktuell/Wert/Buchgewinn/-% und Status (offen/Exit laeuft), Zeilenfaerbung nach Buchgewinn-%, Summenzeile. In CopyTradingMainForm zwischen Master-Trader und Geschlossene eingehaengt. - Master-Trader-Grid: AutoSizeColumnsMode=Fill + FillWeights -> Spaltenbreiten teilen sich immer die Breite (Fix der 'verbuggten' Breiten); RowHeader war bereits aus. Tests: +8 (TradeRowColoring-Schwellen). Build 0 Fehler, 396 Tests gruen, --smoke-ui gruen. Co-Authored-By: Claude Opus 4.8 --- Properties/Resources.Designer.cs | 10 + Properties/Resources.resx | 17 +- Resources/warning.png | Bin 0 -> 1904 bytes .../Logic/TradeRowColoring.cs | 18 ++ .../Ui/ClosedTradesView.Designer.cs | 141 ++++++++++ .../Ui/ClosedTradesView.cs | 80 +++++- .../Ui/CopyTradingMainForm.Designer.cs | 26 ++ .../Ui/CopyTradingMainForm.cs | 2 + .../Ui/MasterTradersView.Designer.cs | 1 + .../Ui/MasterTradersView.cs | 15 ++ .../Ui/OpenTradesView.Designer.cs | 241 ++++++++++++++++++ .../Ui/OpenTradesView.cs | 123 +++++++++ .../PolyTrader.Tests/TradeRowColoringTests.cs | 29 +++ 13 files changed, 694 insertions(+), 9 deletions(-) create mode 100644 Resources/warning.png create mode 100644 src/PolyTrader.Modules.CopyTrading/Logic/TradeRowColoring.cs create mode 100644 src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.Designer.cs create mode 100644 src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.cs create mode 100644 tests/PolyTrader.Tests/TradeRowColoringTests.cs diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index 91ec227..3e77472 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -259,5 +259,15 @@ namespace PolyTraderSharp.Properties { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap warning { + get { + object obj = ResourceManager.GetObject("warning", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } } } diff --git a/Properties/Resources.resx b/Properties/Resources.resx index f13c12e..27bfc0f 100644 --- a/Properties/Resources.resx +++ b/Properties/Resources.resx @@ -121,23 +121,23 @@ ..\Resources\system_time.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\error_log.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\diskette.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\money.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - ..\Resources\traffic_lights_green.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - ..\Resources\money_add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\token_quantifier.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - ..\Resources\error_log.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\traffic_lights_green.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ..\Resources\cancel.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -148,6 +148,9 @@ ..\Resources\delete.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\dashboard.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\coins_in_hand.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -175,7 +178,7 @@ ..\Resources\money_dollar.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - ..\Resources\dashboard.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\warning.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a \ No newline at end of file diff --git a/Resources/warning.png b/Resources/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..311e72652b2e264dc48791d7719a8f8309f5f345 GIT binary patch literal 1904 zcmaJ?Yfuws6piRmhKlkM5vwkXVo{UL1_ERuJd)*+noz(f4@C*d0uhp3vl|kksKKZx zh(eWusE7`zsH<&?z#87-|V)? zh%gi$KCVIvAhqv;VazgCv?moymuqK6q&NG!DkPo{J-4aNvo;7J%DQpr-VXiTQa zT+@pAGZ+&=r8Jg`m532Ju42i|7?xf|&};_7KTuD|-Ffah{*CVum3ZrCzUX`vU5j`J#r;E^Y^ELzm?;unf zANL~U4`Op54u?r2m}I7!lIfXh(rruu#YnkE zNl;2$4VV>WNw}8cgS69sQcw{eWYy&RGSLNt^fCg1S!{DkV?c@I|Dh_?2Q*1VV;}wg zr!XnaBrqr%BXO-pP9I#dn>iGL2sN0D!ZlJHPamsdWGYVKWGYSo!X;c548$na3S39N z!%HNHNKH~QwHyw~TXgrBrytsuUVLbLOig#YhHmQ};^Y?kw)Be7V5$D@xfn^fG|DmHX-6r%t59Gw zYVC0}WNwtvwl4Re#8HZNn4Cr;3ygeQ}=bcyW*X!S9ai-Zu`bPk8K}Jo~0splj8FRc#%J zM|XdP_VW$#Atp7<%fZqP?0)g*t>QbCLX-O_A!s|D2|%5{z@6@H&BefQ;u%fg_3oL) zmJC_r_6ct2;*5gZ)Mju;!i?CQw~Ht2J+~Ta!7sPkbSJdIU`F9>Jy&HHdbfA$kZnW# zjw46ziGk1SzPG)6(LZ^z1C!;}|FG^9>U_R;4-j~+^xKgs54-*P+JD%!JE5o|@0EIV zt_z#8f4S2rUepuXxw+p_`OL|6U2E0CoPg7bE@Dvm?(``d3&L z?0(Y{u`95-r2fW}>=|Cuq8sj9nLzow=a-00&R@=Uef3Q0mn*F&IkwWln(HO(rQ-Yn zCs+HHokez8Yb%$&-bGHe0!Aj^p85tQ#FvuuPlBfJZXeCHyr`Wi8(gtr^_H0UM^@SX zsg)^JNt$%Rl%|U$wdM9rH#m%8UyTJ4G@Y2)dPKvMe)zS zeVH9i#RtPVaS2 zxr7}KGW3yO7ixcFg|Oxo&acmVSmK)_{;WH%^tXnhzK)<&lQ;KR-ll|%3s)?^HGpcKuuP@xm?deA{Fbn>}u#_Zsi$w1M z7iXt!&zt?q_;Y$y2YU0BHSRjUVlte!v^uJ$B;3}_;R>v^%Z@DYn;v0b*&AP z@}|Q-C9JHOQucUo&VFBA6R$Te6WCDfbhd1xBUmK7v@y%TdKSl1>^GnVSAQ?~#tn$N zv@pl{u`$Q~*xfbLc+Ktmgq^MP9l)|d+~S~ZEZNUkKa+|HA6`>sdHLMsr|G4mLlZHn zNoxxlhD2R4u9^O`dLN;`e6i=+*4#=|Ee@XaVw=UEmVr2CZs literal 0 HcmV?d00001 diff --git a/src/PolyTrader.Modules.CopyTrading/Logic/TradeRowColoring.cs b/src/PolyTrader.Modules.CopyTrading/Logic/TradeRowColoring.cs new file mode 100644 index 0000000..e6f607e --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Logic/TradeRowColoring.cs @@ -0,0 +1,18 @@ +using System.Drawing; + +namespace PolyTrader.Modules.CopyTrading.Logic +{ + /// + /// Reine Zeilenfärbung nach realisiertem/unrealisiertem PnL-Prozent (getestet, damit die + /// Schwellen belastbar sind): Verlust rot, 0–10 % hellgrün, > 10 % kräftiges Grün. + /// + public static class TradeRowColoring + { + public static readonly Color Loss = Color.FromArgb(245, 200, 200); // < 0 % – rot + public static readonly Color SmallWin = Color.FromArgb(212, 240, 212); // 0–10 % – hellgrün + public static readonly Color BigWin = Color.FromArgb(140, 214, 140); // > 10 % – grün + + public static Color ForPnlPercent(decimal pnlPercent) => + pnlPercent < 0m ? Loss : (pnlPercent > 10m ? BigWin : SmallWin); + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.Designer.cs b/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.Designer.cs index 85ce0aa..d3c3494 100644 --- a/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.Designer.cs +++ b/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.Designer.cs @@ -35,9 +35,22 @@ namespace PolyTrader.Modules.CopyTrading.Ui this.colClosedAt = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.statusPanel = new System.Windows.Forms.Panel(); this.lblSummary = new System.Windows.Forms.Label(); + this.pnlFilters = new System.Windows.Forms.Panel(); + this.lblMarkt = new System.Windows.Forms.Label(); + this.tbMarket = new System.Windows.Forms.TextBox(); + this.lblMaster = new System.Windows.Forms.Label(); + this.cbMaster = new System.Windows.Forms.ComboBox(); + this.lblErgebnis = new System.Windows.Forms.Label(); + this.cbResult = new System.Windows.Forms.ComboBox(); + this.lblVon = new System.Windows.Forms.Label(); + this.dtFrom = new System.Windows.Forms.DateTimePicker(); + this.lblBis = new System.Windows.Forms.Label(); + this.dtTo = new System.Windows.Forms.DateTimePicker(); + this.btnReset = new System.Windows.Forms.Button(); this.toolbar.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).BeginInit(); this.statusPanel.SuspendLayout(); + this.pnlFilters.SuspendLayout(); this.SuspendLayout(); // // toolbar @@ -210,12 +223,126 @@ namespace PolyTrader.Modules.CopyTrading.Ui this.lblSummary.Text = "—"; this.lblSummary.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // + // pnlFilters + // + this.pnlFilters.Controls.Add(this.lblMarkt); + this.pnlFilters.Controls.Add(this.tbMarket); + this.pnlFilters.Controls.Add(this.lblMaster); + this.pnlFilters.Controls.Add(this.cbMaster); + this.pnlFilters.Controls.Add(this.lblErgebnis); + this.pnlFilters.Controls.Add(this.cbResult); + this.pnlFilters.Controls.Add(this.lblVon); + this.pnlFilters.Controls.Add(this.dtFrom); + this.pnlFilters.Controls.Add(this.lblBis); + this.pnlFilters.Controls.Add(this.dtTo); + this.pnlFilters.Controls.Add(this.btnReset); + this.pnlFilters.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlFilters.Location = new System.Drawing.Point(0, 25); + this.pnlFilters.Name = "pnlFilters"; + this.pnlFilters.Size = new System.Drawing.Size(1100, 34); + this.pnlFilters.TabIndex = 3; + // + // lblMarkt + // + this.lblMarkt.AutoSize = true; + this.lblMarkt.Location = new System.Drawing.Point(6, 9); + this.lblMarkt.Name = "lblMarkt"; + this.lblMarkt.Size = new System.Drawing.Size(41, 15); + this.lblMarkt.Text = "Markt:"; + // + // tbMarket + // + this.tbMarket.Location = new System.Drawing.Point(51, 6); + this.tbMarket.Name = "tbMarket"; + this.tbMarket.PlaceholderText = "Suchtext …"; + this.tbMarket.Size = new System.Drawing.Size(200, 23); + this.tbMarket.TabIndex = 0; + // + // lblMaster + // + this.lblMaster.AutoSize = true; + this.lblMaster.Location = new System.Drawing.Point(263, 9); + this.lblMaster.Name = "lblMaster"; + this.lblMaster.Size = new System.Drawing.Size(48, 15); + this.lblMaster.Text = "Master:"; + // + // cbMaster + // + this.cbMaster.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbMaster.Location = new System.Drawing.Point(315, 6); + this.cbMaster.Name = "cbMaster"; + this.cbMaster.Size = new System.Drawing.Size(170, 23); + this.cbMaster.TabIndex = 1; + // + // lblErgebnis + // + this.lblErgebnis.AutoSize = true; + this.lblErgebnis.Location = new System.Drawing.Point(497, 9); + this.lblErgebnis.Name = "lblErgebnis"; + this.lblErgebnis.Size = new System.Drawing.Size(58, 15); + this.lblErgebnis.Text = "Ergebnis:"; + // + // cbResult + // + this.cbResult.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbResult.Items.AddRange(new object[] { "Alle", "Gewinner", "Verlierer" }); + this.cbResult.Location = new System.Drawing.Point(559, 6); + this.cbResult.Name = "cbResult"; + this.cbResult.Size = new System.Drawing.Size(110, 23); + this.cbResult.TabIndex = 2; + // + // lblVon + // + this.lblVon.AutoSize = true; + this.lblVon.Location = new System.Drawing.Point(681, 9); + this.lblVon.Name = "lblVon"; + this.lblVon.Size = new System.Drawing.Size(31, 15); + this.lblVon.Text = "Von:"; + // + // dtFrom + // + this.dtFrom.Checked = false; + this.dtFrom.Format = System.Windows.Forms.DateTimePickerFormat.Short; + this.dtFrom.Location = new System.Drawing.Point(714, 6); + this.dtFrom.Name = "dtFrom"; + this.dtFrom.ShowCheckBox = true; + this.dtFrom.Size = new System.Drawing.Size(120, 23); + this.dtFrom.TabIndex = 3; + // + // lblBis + // + this.lblBis.AutoSize = true; + this.lblBis.Location = new System.Drawing.Point(840, 9); + this.lblBis.Name = "lblBis"; + this.lblBis.Size = new System.Drawing.Size(26, 15); + this.lblBis.Text = "Bis:"; + // + // dtTo + // + this.dtTo.Checked = false; + this.dtTo.Format = System.Windows.Forms.DateTimePickerFormat.Short; + this.dtTo.Location = new System.Drawing.Point(868, 6); + this.dtTo.Name = "dtTo"; + this.dtTo.ShowCheckBox = true; + this.dtTo.Size = new System.Drawing.Size(120, 23); + this.dtTo.TabIndex = 4; + // + // btnReset + // + this.btnReset.Location = new System.Drawing.Point(994, 5); + this.btnReset.Name = "btnReset"; + this.btnReset.Size = new System.Drawing.Size(90, 25); + this.btnReset.TabIndex = 5; + this.btnReset.Text = "Zurücksetzen"; + this.btnReset.UseVisualStyleBackColor = true; + // // ClosedTradesView // this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.dgvTrades); this.Controls.Add(this.statusPanel); + this.Controls.Add(this.pnlFilters); this.Controls.Add(this.toolbar); this.Name = "ClosedTradesView"; this.Size = new System.Drawing.Size(1100, 600); @@ -223,6 +350,8 @@ namespace PolyTrader.Modules.CopyTrading.Ui this.toolbar.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).EndInit(); this.statusPanel.ResumeLayout(false); + this.pnlFilters.ResumeLayout(false); + this.pnlFilters.PerformLayout(); this.ResumeLayout(false); this.PerformLayout(); } @@ -247,5 +376,17 @@ namespace PolyTrader.Modules.CopyTrading.Ui private System.Windows.Forms.DataGridViewTextBoxColumn colClosedAt; private System.Windows.Forms.Panel statusPanel; private System.Windows.Forms.Label lblSummary; + private System.Windows.Forms.Panel pnlFilters; + private System.Windows.Forms.Label lblMarkt; + private System.Windows.Forms.TextBox tbMarket; + private System.Windows.Forms.Label lblMaster; + private System.Windows.Forms.ComboBox cbMaster; + private System.Windows.Forms.Label lblErgebnis; + private System.Windows.Forms.ComboBox cbResult; + private System.Windows.Forms.Label lblVon; + private System.Windows.Forms.DateTimePicker dtFrom; + private System.Windows.Forms.Label lblBis; + private System.Windows.Forms.DateTimePicker dtTo; + private System.Windows.Forms.Button btnReset; } } diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs b/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs index 8b98021..a8e6348 100644 --- a/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs +++ b/src/PolyTrader.Modules.CopyTrading/Ui/ClosedTradesView.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Windows.Forms; +using PolyTrader.Modules.CopyTrading.Logic; using PolyTrader.Modules.CopyTrading.Persistence; using PolyTraderSharp; using PolyTraderSharp.Models; @@ -19,6 +21,9 @@ namespace PolyTrader.Modules.CopyTrading.Ui private TradingState? _state; private CopyTradingState? _copyState; + private List _allRows = new(); + private bool _loading; + // Parameterloser Konstruktor für den WinForms-Designer. public ClosedTradesView() { @@ -33,6 +38,18 @@ namespace PolyTrader.Modules.CopyTrading.Ui colClosedAt.DefaultCellStyle.Format = "dd.MM.yyyy HH:mm"; tsRefresh.Click += (_, _) => LoadData(); + + // Zeilenfärbung nach PnL-% (grün/hellgrün/rot) nach jedem (Neu-)Binden. + dgvTrades.DataBindingComplete += (_, _) => ColorRows(); + + // Filter: Markt (Text), Master (Combo), Ergebnis (Win/Loss), Datum (Von/Bis, optional). + tbMarket.TextChanged += (_, _) => ApplyFilter(); + cbMaster.SelectedIndexChanged += (_, _) => ApplyFilter(); + cbResult.SelectedIndexChanged += (_, _) => ApplyFilter(); + dtFrom.ValueChanged += (_, _) => ApplyFilter(); + dtTo.ValueChanged += (_, _) => ApplyFilter(); + btnReset.Click += (_, _) => ResetFilters(); + if (cbResult.Items.Count > 0) cbResult.SelectedIndex = 0; } /// Injiziert die Abhängigkeiten (nach der DI-Auflösung) und lädt die Daten. @@ -48,7 +65,7 @@ namespace PolyTrader.Modules.CopyTrading.Ui { if (_tradeLog == null) return; - var rows = _tradeLog.Find(_ => true) + _allRows = _tradeLog.Find(_ => true) .OrderByDescending(t => t.ClosedAt) .Select(t => new ClosedTradeRow { @@ -75,12 +92,71 @@ namespace PolyTrader.Modules.CopyTrading.Ui }) .ToList(); + PopulateMasters(); + ApplyFilter(); + } + + private void PopulateMasters() + { + _loading = true; + string prev = cbMaster.SelectedItem as string ?? "Alle"; + cbMaster.Items.Clear(); + cbMaster.Items.Add("Alle"); + foreach (var m in _allRows.Select(r => r.SourceTraderName) + .Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderBy(s => s)) + cbMaster.Items.Add(m); + int idx = cbMaster.Items.IndexOf(prev); + cbMaster.SelectedIndex = idx >= 0 ? idx : 0; + _loading = false; + } + + private void ApplyFilter() + { + if (_loading) return; + + IEnumerable q = _allRows; + + string market = tbMarket.Text.Trim(); + if (market.Length > 0) + q = q.Where(r => (r.MarketQuestion?.Contains(market, StringComparison.OrdinalIgnoreCase) ?? false) + || (r.MarketSlug?.Contains(market, StringComparison.OrdinalIgnoreCase) ?? false)); + + if (cbMaster.SelectedItem is string master && master != "Alle") + q = q.Where(r => r.SourceTraderName == master); + + string result = cbResult.SelectedItem as string ?? "Alle"; + if (result == "Gewinner") q = q.Where(r => r.RealizedPnl > 0m); + else if (result == "Verlierer") q = q.Where(r => r.RealizedPnl < 0m); + + if (dtFrom.Checked) { DateTime f = dtFrom.Value.Date; q = q.Where(r => r.ClosedAt >= f); } + if (dtTo.Checked) { DateTime t = dtTo.Value.Date.AddDays(1).AddTicks(-1); q = q.Where(r => r.ClosedAt <= t); } + + var rows = q.ToList(); dgvTrades.DataSource = new BindingList(rows); decimal totalPnl = rows.Sum(x => x.RealizedPnl); int wins = rows.Count(x => x.RealizedPnl > 0); double winrate = rows.Count > 0 ? (double)wins / rows.Count * 100 : 0; - lblSummary.Text = $"{rows.Count} Trades | PnL gesamt: {totalPnl:F2} USDC | Winrate: {winrate:F1}%"; + lblSummary.Text = $"{rows.Count} / {_allRows.Count} Trades | PnL: {totalPnl:F2} USDC | Winrate: {winrate:F1}%"; + } + + private void ResetFilters() + { + _loading = true; + tbMarket.Text = string.Empty; + if (cbMaster.Items.Count > 0) cbMaster.SelectedIndex = 0; + if (cbResult.Items.Count > 0) cbResult.SelectedIndex = 0; + dtFrom.Checked = false; + dtTo.Checked = false; + _loading = false; + ApplyFilter(); + } + + private void ColorRows() + { + foreach (DataGridViewRow row in dgvTrades.Rows) + if (row.DataBoundItem is ClosedTradeRow r) + row.DefaultCellStyle.BackColor = TradeRowColoring.ForPnlPercent(r.PnlPercent); } private string ResolveAccount(int accountId, bool isDemo) diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.Designer.cs b/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.Designer.cs index a5071ca..91eee2c 100644 --- a/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.Designer.cs +++ b/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.Designer.cs @@ -19,13 +19,16 @@ namespace PolyTrader.Modules.CopyTrading.Ui { this.tabControl = new System.Windows.Forms.TabControl(); this.tabMasters = new System.Windows.Forms.TabPage(); + this.tabOpen = new System.Windows.Forms.TabPage(); this.tabClosed = new System.Windows.Forms.TabPage(); this.tabSettings = new System.Windows.Forms.TabPage(); this.masterTradersView = new PolyTrader.Modules.CopyTrading.Ui.MasterTradersView(); + this.openTradesView = new PolyTrader.Modules.CopyTrading.Ui.OpenTradesView(); this.closedTradesView = new PolyTrader.Modules.CopyTrading.Ui.ClosedTradesView(); this.accountSettingsView = new PolyTrader.Modules.CopyTrading.Ui.AccountSettingsView(); this.tabControl.SuspendLayout(); this.tabMasters.SuspendLayout(); + this.tabOpen.SuspendLayout(); this.tabClosed.SuspendLayout(); this.tabSettings.SuspendLayout(); this.SuspendLayout(); @@ -33,6 +36,7 @@ namespace PolyTrader.Modules.CopyTrading.Ui // tabControl // this.tabControl.Controls.Add(this.tabMasters); + this.tabControl.Controls.Add(this.tabOpen); this.tabControl.Controls.Add(this.tabClosed); this.tabControl.Controls.Add(this.tabSettings); this.tabControl.Dock = System.Windows.Forms.DockStyle.Fill; @@ -53,6 +57,25 @@ namespace PolyTrader.Modules.CopyTrading.Ui this.tabMasters.Text = "Master-Trader"; this.tabMasters.UseVisualStyleBackColor = true; // + // tabOpen + // + this.tabOpen.Controls.Add(this.openTradesView); + this.tabOpen.Location = new System.Drawing.Point(4, 24); + this.tabOpen.Name = "tabOpen"; + this.tabOpen.Padding = new System.Windows.Forms.Padding(3); + this.tabOpen.Size = new System.Drawing.Size(1192, 672); + this.tabOpen.TabIndex = 3; + this.tabOpen.Text = "Offene Trades"; + this.tabOpen.UseVisualStyleBackColor = true; + // + // openTradesView + // + this.openTradesView.Dock = System.Windows.Forms.DockStyle.Fill; + this.openTradesView.Location = new System.Drawing.Point(3, 3); + this.openTradesView.Name = "openTradesView"; + this.openTradesView.Size = new System.Drawing.Size(1186, 666); + this.openTradesView.TabIndex = 0; + // // tabClosed // this.tabClosed.Controls.Add(this.closedTradesView); @@ -111,6 +134,7 @@ namespace PolyTrader.Modules.CopyTrading.Ui this.Text = "Copytrading"; this.tabControl.ResumeLayout(false); this.tabMasters.ResumeLayout(false); + this.tabOpen.ResumeLayout(false); this.tabClosed.ResumeLayout(false); this.tabSettings.ResumeLayout(false); this.ResumeLayout(false); @@ -120,9 +144,11 @@ namespace PolyTrader.Modules.CopyTrading.Ui private System.Windows.Forms.TabControl tabControl; private System.Windows.Forms.TabPage tabMasters; + private System.Windows.Forms.TabPage tabOpen; private System.Windows.Forms.TabPage tabClosed; private System.Windows.Forms.TabPage tabSettings; private MasterTradersView masterTradersView; + private OpenTradesView openTradesView; private ClosedTradesView closedTradesView; private AccountSettingsView accountSettingsView; } diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.cs b/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.cs index d6e69d0..345c82d 100644 --- a/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.cs +++ b/src/PolyTrader.Modules.CopyTrading/Ui/CopyTradingMainForm.cs @@ -29,6 +29,8 @@ namespace PolyTrader.Modules.CopyTrading.Ui masterTradersView.Initialize( services.GetRequiredService(), state, copyState); + openTradesView.Initialize(state, copyState); + closedTradesView.Initialize( services.GetRequiredService(), state, copyState); diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.Designer.cs b/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.Designer.cs index ca82405..01544e0 100644 --- a/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.Designer.cs +++ b/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.Designer.cs @@ -102,6 +102,7 @@ namespace PolyTrader.Modules.CopyTrading.Ui this.grid.AllowUserToAddRows = false; this.grid.AllowUserToDeleteRows = false; this.grid.AutoGenerateColumns = false; + this.grid.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; this.grid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.grid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { this.colId, diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs b/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs index 0f0c88e..0493ef0 100644 --- a/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs +++ b/src/PolyTrader.Modules.CopyTrading/Ui/MasterTradersView.cs @@ -35,6 +35,21 @@ namespace PolyTrader.Modules.CopyTrading.Ui colCopyPf.DefaultCellStyle.Format = "F2"; colCopyAvg.DefaultCellStyle.Format = "F2"; + // Spaltenbreiten proportional (grid-weites Fill): keine überlaufenden Fixbreiten mehr, + // die Spalten teilen sich immer exakt die verfügbare Breite (Fix des „verbuggten" Grids). + colId.FillWeight = 40; + colWallet.FillWeight = 150; + colName.FillWeight = 200; + colCategory.FillWeight = 90; + colActive.FillWeight = 50; + colTrades.FillWeight = 85; + colWinrate.FillWeight = 90; + colPnl.FillWeight = 90; + colCopyPnl.FillWeight = 110; + colCopyPf.FillWeight = 95; + colCopyAvg.FillWeight = 95; + colCopyCount.FillWeight = 95; + tsNew.Click += (_, _) => AddNew(); tsSave.Click += (_, _) => SaveCurrent(); tsDelete.Click += (_, _) => DeleteCurrent(); diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.Designer.cs b/src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.Designer.cs new file mode 100644 index 0000000..4c79126 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.Designer.cs @@ -0,0 +1,241 @@ +namespace PolyTrader.Modules.CopyTrading.Ui +{ + partial class OpenTradesView + { + private System.ComponentModel.IContainer components = null; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Vom Komponenten-Designer generierter Code + + private void InitializeComponent() + { + this.toolbar = new System.Windows.Forms.ToolStrip(); + this.tsRefresh = new System.Windows.Forms.ToolStripButton(); + this.dgvOpen = new System.Windows.Forms.DataGridView(); + this.colAccount = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colTrader = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colMarket = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colOutcome = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colSide = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colEntry = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colCurrent = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colSize = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colAmount = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colValue = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colUnrealized = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colUnrealizedPct = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.colStatus = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.statusPanel = new System.Windows.Forms.Panel(); + this.lblSummary = new System.Windows.Forms.Label(); + this.toolbar.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.dgvOpen)).BeginInit(); + this.statusPanel.SuspendLayout(); + this.SuspendLayout(); + // + // toolbar + // + this.toolbar.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden; + this.toolbar.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.tsRefresh }); + this.toolbar.Location = new System.Drawing.Point(0, 0); + this.toolbar.Name = "toolbar"; + this.toolbar.Size = new System.Drawing.Size(1100, 25); + this.toolbar.TabIndex = 0; + // + // tsRefresh + // + this.tsRefresh.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text; + this.tsRefresh.Name = "tsRefresh"; + this.tsRefresh.Size = new System.Drawing.Size(90, 22); + this.tsRefresh.Text = "Aktualisieren"; + // + // dgvOpen + // + this.dgvOpen.AllowUserToAddRows = false; + this.dgvOpen.AllowUserToDeleteRows = false; + this.dgvOpen.AutoGenerateColumns = false; + this.dgvOpen.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; + this.dgvOpen.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dgvOpen.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.colAccount, this.colTrader, this.colMarket, this.colOutcome, this.colSide, + this.colEntry, this.colCurrent, this.colSize, this.colAmount, this.colValue, + this.colUnrealized, this.colUnrealizedPct, this.colStatus}); + this.dgvOpen.Dock = System.Windows.Forms.DockStyle.Fill; + this.dgvOpen.Location = new System.Drawing.Point(0, 25); + this.dgvOpen.Name = "dgvOpen"; + this.dgvOpen.ReadOnly = true; + this.dgvOpen.RowHeadersVisible = false; + this.dgvOpen.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect; + this.dgvOpen.Size = new System.Drawing.Size(1100, 549); + this.dgvOpen.TabIndex = 1; + // + // colAccount + // + this.colAccount.DataPropertyName = "AccountName"; + this.colAccount.HeaderText = "Account"; + this.colAccount.Name = "colAccount"; + this.colAccount.ReadOnly = true; + this.colAccount.FillWeight = 120; + // + // colTrader + // + this.colTrader.DataPropertyName = "SourceTraderName"; + this.colTrader.HeaderText = "Master-Trader"; + this.colTrader.Name = "colTrader"; + this.colTrader.ReadOnly = true; + this.colTrader.FillWeight = 120; + // + // colMarket + // + this.colMarket.DataPropertyName = "MarketQuestion"; + this.colMarket.HeaderText = "Markt"; + this.colMarket.Name = "colMarket"; + this.colMarket.ReadOnly = true; + this.colMarket.FillWeight = 240; + // + // colOutcome + // + this.colOutcome.DataPropertyName = "Outcome"; + this.colOutcome.HeaderText = "Outcome"; + this.colOutcome.Name = "colOutcome"; + this.colOutcome.ReadOnly = true; + this.colOutcome.FillWeight = 80; + // + // colSide + // + this.colSide.DataPropertyName = "Side"; + this.colSide.HeaderText = "Side"; + this.colSide.Name = "colSide"; + this.colSide.ReadOnly = true; + this.colSide.FillWeight = 55; + // + // colEntry + // + this.colEntry.DataPropertyName = "EntryPrice"; + this.colEntry.HeaderText = "Entry"; + this.colEntry.Name = "colEntry"; + this.colEntry.ReadOnly = true; + this.colEntry.FillWeight = 65; + // + // colCurrent + // + this.colCurrent.DataPropertyName = "CurrentPrice"; + this.colCurrent.HeaderText = "Aktuell"; + this.colCurrent.Name = "colCurrent"; + this.colCurrent.ReadOnly = true; + this.colCurrent.FillWeight = 65; + // + // colSize + // + this.colSize.DataPropertyName = "Size"; + this.colSize.HeaderText = "Size"; + this.colSize.Name = "colSize"; + this.colSize.ReadOnly = true; + this.colSize.FillWeight = 70; + // + // colAmount + // + this.colAmount.DataPropertyName = "AmountUsd"; + this.colAmount.HeaderText = "Einsatz"; + this.colAmount.Name = "colAmount"; + this.colAmount.ReadOnly = true; + this.colAmount.FillWeight = 80; + // + // colValue + // + this.colValue.DataPropertyName = "CurrentValueUsd"; + this.colValue.HeaderText = "Wert"; + this.colValue.Name = "colValue"; + this.colValue.ReadOnly = true; + this.colValue.FillWeight = 80; + // + // colUnrealized + // + this.colUnrealized.DataPropertyName = "UnrealizedPnl"; + this.colUnrealized.HeaderText = "Buchgewinn"; + this.colUnrealized.Name = "colUnrealized"; + this.colUnrealized.ReadOnly = true; + this.colUnrealized.FillWeight = 90; + // + // colUnrealizedPct + // + this.colUnrealizedPct.DataPropertyName = "UnrealizedPct"; + this.colUnrealizedPct.HeaderText = "Buchgew. %"; + this.colUnrealizedPct.Name = "colUnrealizedPct"; + this.colUnrealizedPct.ReadOnly = true; + this.colUnrealizedPct.FillWeight = 80; + // + // colStatus + // + this.colStatus.DataPropertyName = "Status"; + this.colStatus.HeaderText = "Status"; + this.colStatus.Name = "colStatus"; + this.colStatus.ReadOnly = true; + this.colStatus.FillWeight = 90; + // + // statusPanel + // + this.statusPanel.Controls.Add(this.lblSummary); + this.statusPanel.Dock = System.Windows.Forms.DockStyle.Bottom; + this.statusPanel.Location = new System.Drawing.Point(0, 574); + this.statusPanel.Name = "statusPanel"; + this.statusPanel.Size = new System.Drawing.Size(1100, 26); + this.statusPanel.TabIndex = 2; + // + // lblSummary + // + this.lblSummary.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblSummary.Location = new System.Drawing.Point(0, 0); + this.lblSummary.Name = "lblSummary"; + this.lblSummary.Padding = new System.Windows.Forms.Padding(8, 0, 0, 0); + this.lblSummary.Size = new System.Drawing.Size(1100, 26); + this.lblSummary.TabIndex = 0; + this.lblSummary.Text = "—"; + this.lblSummary.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // OpenTradesView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.dgvOpen); + this.Controls.Add(this.statusPanel); + this.Controls.Add(this.toolbar); + this.Name = "OpenTradesView"; + this.Size = new System.Drawing.Size(1100, 600); + this.toolbar.ResumeLayout(false); + this.toolbar.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.dgvOpen)).EndInit(); + this.statusPanel.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + } + + #endregion + + private System.Windows.Forms.ToolStrip toolbar; + private System.Windows.Forms.ToolStripButton tsRefresh; + private System.Windows.Forms.DataGridView dgvOpen; + private System.Windows.Forms.DataGridViewTextBoxColumn colAccount; + private System.Windows.Forms.DataGridViewTextBoxColumn colTrader; + private System.Windows.Forms.DataGridViewTextBoxColumn colMarket; + private System.Windows.Forms.DataGridViewTextBoxColumn colOutcome; + private System.Windows.Forms.DataGridViewTextBoxColumn colSide; + private System.Windows.Forms.DataGridViewTextBoxColumn colEntry; + private System.Windows.Forms.DataGridViewTextBoxColumn colCurrent; + private System.Windows.Forms.DataGridViewTextBoxColumn colSize; + private System.Windows.Forms.DataGridViewTextBoxColumn colAmount; + private System.Windows.Forms.DataGridViewTextBoxColumn colValue; + private System.Windows.Forms.DataGridViewTextBoxColumn colUnrealized; + private System.Windows.Forms.DataGridViewTextBoxColumn colUnrealizedPct; + private System.Windows.Forms.DataGridViewTextBoxColumn colStatus; + private System.Windows.Forms.Panel statusPanel; + private System.Windows.Forms.Label lblSummary; + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.cs b/src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.cs new file mode 100644 index 0000000..ad94ea0 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Ui/OpenTradesView.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Windows.Forms; +using PolyTrader.Modules.CopyTrading.Logic; +using PolyTraderSharp; +using PolyTraderSharp.Models; + +namespace PolyTrader.Modules.CopyTrading.Ui +{ + /// + /// Listet ALLE aktuell offenen Positionen (Portfolios der Accounts) mit Buchgewinn/-verlust. + /// Quelle ist der Laufzeit-State (), nicht die DB – so ist die + /// Anzeige live. Zeilenfärbung nach Buchgewinn-% (). + /// Layout im Designer (OpenTradesView.Designer.cs). + /// + public partial class OpenTradesView : UserControl + { + private TradingState? _state; + private CopyTradingState? _copyState; + + public OpenTradesView() + { + InitializeComponent(); + + colEntry.DefaultCellStyle.Format = "F3"; + colCurrent.DefaultCellStyle.Format = "F3"; + colSize.DefaultCellStyle.Format = "F2"; + colAmount.DefaultCellStyle.Format = "F2"; + colValue.DefaultCellStyle.Format = "F2"; + colUnrealized.DefaultCellStyle.Format = "F2"; + colUnrealizedPct.DefaultCellStyle.Format = "F1"; + + tsRefresh.Click += (_, _) => LoadData(); + dgvOpen.DataBindingComplete += (_, _) => ColorRows(); + } + + public void Initialize(TradingState state, CopyTradingState copyState) + { + _state = state; + _copyState = copyState; + LoadData(); + } + + private void LoadData() + { + if (_state == null) return; + + var rows = _state.Accounts.Values + .SelectMany(a => a.OpenPositions.Values.Select(p => (acc: a, pos: p))) + .OrderByDescending(x => x.pos.CurrentValueUsd) + .Select(x => + { + decimal unrealized = x.pos.CurrentValueUsd - x.pos.AmountUsd; + decimal pct = x.pos.AmountUsd > 0m ? unrealized / x.pos.AmountUsd * 100m : 0m; + return new OpenTradeRow + { + AccountName = ResolveAccount(x.acc.AccountId, x.pos.IsDemo), + SourceTraderName = ResolveTrader(x.pos.SourceTraderId, x.pos.SourceTraderName), + MarketQuestion = x.pos.MarketQuestion, + Outcome = x.pos.Outcome, + Side = x.pos.Side, + EntryPrice = x.pos.EntryPrice, + CurrentPrice = x.pos.CurrentPrice, + Size = x.pos.Size, + AmountUsd = x.pos.AmountUsd, + CurrentValueUsd = x.pos.CurrentValueUsd, + UnrealizedPnl = unrealized, + UnrealizedPct = pct, + Status = x.pos.ExitPending ? "Exit läuft" : "offen" + }; + }) + .ToList(); + + dgvOpen.DataSource = new BindingList(rows); + + decimal totalValue = rows.Sum(r => r.CurrentValueUsd); + decimal totalUnrealized = rows.Sum(r => r.UnrealizedPnl); + lblSummary.Text = $"{rows.Count} offene Positionen | Wert: {totalValue:F2} USDC | Buchgewinn: {totalUnrealized:F2} USDC"; + } + + private void ColorRows() + { + foreach (DataGridViewRow row in dgvOpen.Rows) + if (row.DataBoundItem is OpenTradeRow r) + row.DefaultCellStyle.BackColor = TradeRowColoring.ForPnlPercent(r.UnrealizedPct); + } + + private string ResolveAccount(int accountId, bool isDemo) + { + string suffix = isDemo ? " (Demo)" : ""; + if (_state != null && _state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name)) + return acc.Name + suffix; + return $"#{accountId}{suffix}"; + } + + private string ResolveTrader(int traderId, string fallback) + { + if (_copyState != null && _copyState.Traders.TryGetValue(traderId, out var t) && !string.IsNullOrEmpty(t.DisplayName)) + return t.DisplayName; + return !string.IsNullOrEmpty(fallback) ? fallback : (traderId > 0 ? $"#{traderId}" : "System"); + } + + /// Anzeige-Zeile für offene Positionen. + private sealed class OpenTradeRow + { + public string AccountName { get; set; } = string.Empty; + public string SourceTraderName { get; set; } = string.Empty; + public string MarketQuestion { get; set; } = string.Empty; + public string Outcome { get; set; } = string.Empty; + public string Side { get; set; } = string.Empty; + public decimal EntryPrice { get; set; } + public decimal CurrentPrice { get; set; } + public decimal Size { get; set; } + public decimal AmountUsd { get; set; } + public decimal CurrentValueUsd { get; set; } + public decimal UnrealizedPnl { get; set; } + public decimal UnrealizedPct { get; set; } + public string Status { get; set; } = string.Empty; + } + } +} diff --git a/tests/PolyTrader.Tests/TradeRowColoringTests.cs b/tests/PolyTrader.Tests/TradeRowColoringTests.cs new file mode 100644 index 0000000..5864944 --- /dev/null +++ b/tests/PolyTrader.Tests/TradeRowColoringTests.cs @@ -0,0 +1,29 @@ +using PolyTrader.Modules.CopyTrading.Logic; +using Xunit; + +namespace PolyTrader.Tests +{ + /// Sicherheitsnetz für die PnL-Zeilenfärbung (Slice 3): Schwellen -10/0/10. + public class TradeRowColoringTests + { + [Theory] + [InlineData(-0.01, "loss")] + [InlineData(-25.0, "loss")] + [InlineData(0.0, "small")] // Break-even = hellgrün (kein Verlust) + [InlineData(5.0, "small")] + [InlineData(10.0, "small")] // genau 10 % noch hellgrün + [InlineData(10.01, "big")] + [InlineData(80.0, "big")] + public void Color_matches_threshold(double pct, string expected) + { + var c = TradeRowColoring.ForPnlPercent((decimal)pct); + var want = expected switch + { + "loss" => TradeRowColoring.Loss, + "big" => TradeRowColoring.BigWin, + _ => TradeRowColoring.SmallWin + }; + Assert.Equal(want, c); + } + } +}