Testnetz: geldkritische Trade-Mathematik extrahiert (TradeMath) + getestet
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
581755029a
commit
1dffc9e253
@@ -0,0 +1,29 @@
|
||||
namespace PolyTrader.Modules.CopyTrading.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public static class TradeMath
|
||||
{
|
||||
/// <summary>Erlös eines Verkaufs: Anzahl Shares × Verkaufspreis.</summary>
|
||||
public static decimal ExitValue(decimal sizeShares, decimal exitPrice) => sizeShares * exitPrice;
|
||||
|
||||
/// <summary>Realisierter Gewinn/Verlust = Erlös − eingesetztes Kapital.</summary>
|
||||
public static decimal RealizedPnl(decimal sizeShares, decimal exitPrice, decimal entryAmountUsd) =>
|
||||
ExitValue(sizeShares, exitPrice) - entryAmountUsd;
|
||||
|
||||
/// <summary>PnL in Prozent des eingesetzten Kapitals; 0, wenn kein Kapital eingesetzt war.</summary>
|
||||
public static decimal PnlPercent(decimal realizedPnl, decimal entryAmountUsd) =>
|
||||
entryAmountUsd > 0m ? realizedPnl / entryAmountUsd * 100m : 0m;
|
||||
|
||||
/// <summary>
|
||||
/// Gewichteter Durchschnitts-Einstiegspreis nach Aufstockung einer Position:
|
||||
/// Gesamt-USDC / Gesamt-Shares. Bei Size ≤ 0 wird 0 zurückgegeben (Div-durch-0-Schutz).
|
||||
/// </summary>
|
||||
public static decimal WeightedAverageEntryPrice(decimal totalAmountUsd, decimal totalSize) =>
|
||||
totalSize > 0m ? totalAmountUsd / totalSize : 0m;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using PolyTrader.Modules.CopyTrading.Logic;
|
||||
using Xunit;
|
||||
using static PolyTrader.Modules.CopyTrading.Logic.TradeMath;
|
||||
|
||||
namespace PolyTrader.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Sicherheitsnetz für die geldkritische Trade-Mathematik (PnL, PnL-%, gewichteter Entry).
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user