Phase 4.2 / 0.2: realistischer Demo-Modus (Exit-Slippage + Fees)

- DemoModel (pure, getestet): ExitFillPrice (Signalpreis minus halber Spread,
  geclamped) + CloseWithFees (Erlös zum Fill-Preis minus Round-Trip-Fee).
- CopyTradingEngine Demo-Close nutzt es: ExitPrice = realistischer Fill statt
  Signalpreis, RealizedPnl netto nach Fees, ClosedTrade.TotalFees befuellt.
  -> Demo-PnL ist nicht mehr systematisch geschoent (Master-Validierung brauchbar).
- DemoModelTests (Fill-Clamping, Round-Trip-Fee, 0-Fee).

207 Tests gruen. Build/Smoke gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-07 18:49:38 +02:00
co-authored by Claude Opus 4.8
parent b259f310d3
commit 54ed1a414e
3 changed files with 98 additions and 3 deletions
@@ -0,0 +1,40 @@
using System;
namespace PolyTrader.Modules.CopyTrading.Logic
{
/// <summary>
/// Reine Logik für einen realistischeren Demo-Modus (Phase 4.2). Demo füllte bisher zum
/// Signalpreis ohne Slippage/Fees → Ergebnisse systematisch geschönt und als Validierung
/// neuer Master unbrauchbar. Hier: Exit-Fill mit halbem Spread + Fees (aus <see cref="FeeModel"/>),
/// damit Demo- und Live-PnL grob vergleichbar werden.
/// </summary>
public static class DemoModel
{
/// <summary>Fallback-Halbspread (¢), wenn kein Orderbuch verfügbar ist (Phase 1.2).</summary>
public const decimal FallbackHalfSpread = 0.005m;
/// <summary>
/// Simulierter SELL-Fill-Preis im Demo: der Verkäufer trifft den Bid, also unter dem
/// Signalpreis um den halben Spread. Auf [0.01, 0.99] geclamped.
/// </summary>
public static decimal ExitFillPrice(decimal signalPrice, decimal halfSpread)
{
decimal p = signalPrice - halfSpread;
return Math.Clamp(p, 0.01m, 0.99m);
}
/// <summary>
/// Realistischer Demo-Close: Erlös zum (leicht schlechteren) Fill-Preis, minus Round-Trip-Fee
/// (Entry- und Exit-Leg). Liefert (exitUsd, realizedPnl, totalFees).
/// </summary>
public static (decimal exitUsd, decimal realizedPnl, decimal totalFees) CloseWithFees(
decimal size, decimal signalPrice, decimal entryAmountUsd, int feeBps, decimal halfSpread)
{
decimal exitPrice = ExitFillPrice(signalPrice, halfSpread);
decimal exitUsd = size * exitPrice;
decimal totalFees = FeeModel.FeeUsd(entryAmountUsd, feeBps) + FeeModel.FeeUsd(exitUsd, feeBps);
decimal realizedPnl = exitUsd - entryAmountUsd - totalFees;
return (exitUsd, realizedPnl, totalFees);
}
}
}
@@ -633,8 +633,14 @@ namespace PolyTraderSharp.Services
{
_positionRepo.DeleteDemo(account.AccountId, signal.TokenId);
decimal exitUsd = TradeMath.ExitValue(openPos.Size, signal.Price);
decimal realizedPnl = TradeMath.RealizedPnl(openPos.Size, signal.Price, openPos.AmountUsd);
// Phase 4.2: realistischer Demo-Close (Exit-Slippage + Fees), statt zum
// Signalpreis ohne Kosten sonst ist Demo als Master-Validierung wertlos.
int demoFeeBps = _state.MarketCache.TryGetValue(signal.TokenId, out var demoMd)
? (demoMd.TakerFeeBps > 0 ? demoMd.TakerFeeBps : FeeModel.FallbackBps(demoMd.Category))
: FeeModel.FallbackBps(null);
var (exitUsd, realizedPnl, totalFees) = DemoModel.CloseWithFees(
openPos.Size, signal.Price, openPos.AmountUsd, demoFeeBps, DemoModel.FallbackHalfSpread);
decimal demoExitPrice = DemoModel.ExitFillPrice(signal.Price, DemoModel.FallbackHalfSpread);
_state.GlobalPnl += realizedPnl;
account.UpdateBalance(account.AvailableBalance + exitUsd);
@@ -652,10 +658,11 @@ namespace PolyTraderSharp.Services
Outcome = signal.Outcome,
Side = signal.Side,
EntryPrice = openPos.EntryPrice,
ExitPrice = signal.Price,
ExitPrice = demoExitPrice,
Size = openPos.Size,
RealizedPnl = realizedPnl,
PnlPercent = TradeMath.PnlPercent(realizedPnl, openPos.AmountUsd),
TotalFees = totalFees,
OpenedAt = openPos.OpenedAt,
ClosedAt = DateTime.UtcNow,
ExitReason = signal.Reason