From 1dffc9e253b570e0b02464c2a9ea2490ff3d4847 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 7 Jul 2026 11:09:41 +0200 Subject: [PATCH] Testnetz: geldkritische Trade-Mathematik extrahiert (TradeMath) + getestet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue pure Klasse TradeMath (Logic/): ExitValue, RealizedPnl, PnlPercent (Null-Guard), WeightedAverageEntryPrice (Div-durch-0-Schutz). - CopyTradingEngine ruft sie jetzt im Demo-Close-PnL und bei der Positions- Aufstockung (Demo + Live, gewichteter Einstiegspreis) – verhaltensneutral. - TradeMathTests (6 Faelle: Gewinn/Verlust, %-Basis, 0-Kapital/0-Size-Guards). Gesamt 160 Tests gruen. Build/Smoke gruen. (Phase-0.1-SELL-Wiring weiterhin bewusst zurueckgestellt bis zum Server-Test; pure Leiter liegt getestet bereit.) Co-Authored-By: Claude Opus 4.8 --- .../Logic/TradeMath.cs | 29 +++++++++++ .../Services/CopyTradingEngine.cs | 10 ++-- tests/PolyTrader.Tests/TradeMathTests.cs | 51 +++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 src/PolyTrader.Modules.CopyTrading/Logic/TradeMath.cs create mode 100644 tests/PolyTrader.Tests/TradeMathTests.cs diff --git a/src/PolyTrader.Modules.CopyTrading/Logic/TradeMath.cs b/src/PolyTrader.Modules.CopyTrading/Logic/TradeMath.cs new file mode 100644 index 0000000..cb1f228 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/Logic/TradeMath.cs @@ -0,0 +1,29 @@ +namespace PolyTrader.Modules.CopyTrading.Logic +{ + /// + /// Reine, geldkritische Trade-Mathematik (realisierter PnL, PnL-%, gewichteter + /// Einstiegspreis). Aus der Engine herausgezogen, damit diese Rechnungen fest + /// unit-getestet sind – Rundungs-/Vorzeichenfehler hier verfälschen direkt PnL und + /// Kontostand. Verhalten 1:1 aus der Engine übernommen (mit defensivem Null-Guard). + /// + public static class TradeMath + { + /// Erlös eines Verkaufs: Anzahl Shares × Verkaufspreis. + public static decimal ExitValue(decimal sizeShares, decimal exitPrice) => sizeShares * exitPrice; + + /// Realisierter Gewinn/Verlust = Erlös − eingesetztes Kapital. + public static decimal RealizedPnl(decimal sizeShares, decimal exitPrice, decimal entryAmountUsd) => + ExitValue(sizeShares, exitPrice) - entryAmountUsd; + + /// PnL in Prozent des eingesetzten Kapitals; 0, wenn kein Kapital eingesetzt war. + public static decimal PnlPercent(decimal realizedPnl, decimal entryAmountUsd) => + entryAmountUsd > 0m ? realizedPnl / entryAmountUsd * 100m : 0m; + + /// + /// Gewichteter Durchschnitts-Einstiegspreis nach Aufstockung einer Position: + /// Gesamt-USDC / Gesamt-Shares. Bei Size ≤ 0 wird 0 zurückgegeben (Div-durch-0-Schutz). + /// + public static decimal WeightedAverageEntryPrice(decimal totalAmountUsd, decimal totalSize) => + totalSize > 0m ? totalAmountUsd / totalSize : 0m; + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs b/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs index c272ea0..34f8020 100644 --- a/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs +++ b/src/PolyTrader.Modules.CopyTrading/Services/CopyTradingEngine.cs @@ -497,7 +497,7 @@ namespace PolyTraderSharp.Services { old.Size += pos.Size; old.AmountUsd += pos.AmountUsd; - old.EntryPrice = old.AmountUsd / old.Size; // weighted average + old.EntryPrice = TradeMath.WeightedAverageEntryPrice(old.AmountUsd, old.Size); // weighted average return old; }); @@ -541,7 +541,7 @@ namespace PolyTraderSharp.Services { old.Size += pos.Size; old.AmountUsd += pos.AmountUsd; - old.EntryPrice = old.AmountUsd / old.Size; + old.EntryPrice = TradeMath.WeightedAverageEntryPrice(old.AmountUsd, old.Size); return old; }); @@ -612,8 +612,8 @@ namespace PolyTraderSharp.Services { _positionRepo.DeleteDemo(account.AccountId, signal.TokenId); - decimal exitUsd = openPos.Size * signal.Price; - decimal realizedPnl = exitUsd - openPos.AmountUsd; + decimal exitUsd = TradeMath.ExitValue(openPos.Size, signal.Price); + decimal realizedPnl = TradeMath.RealizedPnl(openPos.Size, signal.Price, openPos.AmountUsd); _state.GlobalPnl += realizedPnl; account.UpdateBalance(account.AvailableBalance + exitUsd); @@ -633,7 +633,7 @@ namespace PolyTraderSharp.Services ExitPrice = signal.Price, Size = openPos.Size, RealizedPnl = realizedPnl, - PnlPercent = openPos.AmountUsd > 0 ? (realizedPnl / openPos.AmountUsd * 100m) : 0m, + PnlPercent = TradeMath.PnlPercent(realizedPnl, openPos.AmountUsd), OpenedAt = openPos.OpenedAt, ClosedAt = DateTime.UtcNow, ExitReason = signal.Reason diff --git a/tests/PolyTrader.Tests/TradeMathTests.cs b/tests/PolyTrader.Tests/TradeMathTests.cs new file mode 100644 index 0000000..b335dda --- /dev/null +++ b/tests/PolyTrader.Tests/TradeMathTests.cs @@ -0,0 +1,51 @@ +using PolyTrader.Modules.CopyTrading.Logic; +using Xunit; +using static PolyTrader.Modules.CopyTrading.Logic.TradeMath; + +namespace PolyTrader.Tests +{ + /// + /// Sicherheitsnetz für die geldkritische Trade-Mathematik (PnL, PnL-%, gewichteter Entry). + /// + public class TradeMathTests + { + [Fact] + public void ExitValue_is_shares_times_price() + { + Assert.Equal(99m, ExitValue(100m, 0.99m)); + } + + [Fact] + public void RealizedPnl_profit_and_loss() + { + Assert.Equal(49m, RealizedPnl(sizeShares: 100m, exitPrice: 0.99m, entryAmountUsd: 50m)); + Assert.Equal(-20m, RealizedPnl(sizeShares: 100m, exitPrice: 0.40m, entryAmountUsd: 60m)); + } + + [Fact] + public void PnlPercent_relative_to_entry_capital() + { + Assert.Equal(98m, PnlPercent(realizedPnl: 49m, entryAmountUsd: 50m)); + Assert.Equal(-20m, PnlPercent(realizedPnl: -20m, entryAmountUsd: 100m)); + } + + [Fact] + public void PnlPercent_zero_entry_returns_zero_not_divide_by_zero() + { + Assert.Equal(0m, PnlPercent(49m, 0m)); + } + + [Fact] + public void WeightedAverageEntryPrice_is_amount_over_size() + { + Assert.Equal(0.5m, WeightedAverageEntryPrice(totalAmountUsd: 100m, totalSize: 200m)); + Assert.Equal(0.51m, WeightedAverageEntryPrice(51m, 100m)); + } + + [Fact] + public void WeightedAverageEntryPrice_zero_size_returns_zero() + { + Assert.Equal(0m, WeightedAverageEntryPrice(100m, 0m)); + } + } +}