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:
co-authored by
Claude Opus 4.8
parent
b259f310d3
commit
54ed1a414e
@@ -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);
|
_positionRepo.DeleteDemo(account.AccountId, signal.TokenId);
|
||||||
|
|
||||||
decimal exitUsd = TradeMath.ExitValue(openPos.Size, signal.Price);
|
// Phase 4.2: realistischer Demo-Close (Exit-Slippage + Fees), statt zum
|
||||||
decimal realizedPnl = TradeMath.RealizedPnl(openPos.Size, signal.Price, openPos.AmountUsd);
|
// 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;
|
_state.GlobalPnl += realizedPnl;
|
||||||
account.UpdateBalance(account.AvailableBalance + exitUsd);
|
account.UpdateBalance(account.AvailableBalance + exitUsd);
|
||||||
@@ -652,10 +658,11 @@ namespace PolyTraderSharp.Services
|
|||||||
Outcome = signal.Outcome,
|
Outcome = signal.Outcome,
|
||||||
Side = signal.Side,
|
Side = signal.Side,
|
||||||
EntryPrice = openPos.EntryPrice,
|
EntryPrice = openPos.EntryPrice,
|
||||||
ExitPrice = signal.Price,
|
ExitPrice = demoExitPrice,
|
||||||
Size = openPos.Size,
|
Size = openPos.Size,
|
||||||
RealizedPnl = realizedPnl,
|
RealizedPnl = realizedPnl,
|
||||||
PnlPercent = TradeMath.PnlPercent(realizedPnl, openPos.AmountUsd),
|
PnlPercent = TradeMath.PnlPercent(realizedPnl, openPos.AmountUsd),
|
||||||
|
TotalFees = totalFees,
|
||||||
OpenedAt = openPos.OpenedAt,
|
OpenedAt = openPos.OpenedAt,
|
||||||
ClosedAt = DateTime.UtcNow,
|
ClosedAt = DateTime.UtcNow,
|
||||||
ExitReason = signal.Reason
|
ExitReason = signal.Reason
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using PolyTrader.Modules.CopyTrading.Logic;
|
||||||
|
using Xunit;
|
||||||
|
using static PolyTrader.Modules.CopyTrading.Logic.DemoModel;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sicherheitsnetz für den realistischeren Demo-Modus (Phase 4.2): Exit-Slippage + Fees.
|
||||||
|
/// </summary>
|
||||||
|
public class DemoModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ExitFillPrice_is_below_signal_by_half_spread()
|
||||||
|
{
|
||||||
|
Assert.Equal(0.495m, ExitFillPrice(0.50m, 0.005m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.011, 0.005, 0.01)] // Clamp Untergrenze
|
||||||
|
[InlineData(0.20, 0.30, 0.01)] // stark negativ -> Untergrenze
|
||||||
|
[InlineData(0.90, 0.005, 0.895)] // normal im Band
|
||||||
|
[InlineData(0.999, 0.005, 0.99)] // Clamp Obergrenze (0.994 -> 0.99)
|
||||||
|
public void ExitFillPrice_clamps_to_valid_range(double signal, double half, double expected)
|
||||||
|
{
|
||||||
|
Assert.Equal((decimal)expected, ExitFillPrice((decimal)signal, (decimal)half));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CloseWithFees_subtracts_slippage_and_roundtrip_fee()
|
||||||
|
{
|
||||||
|
// size 100, signal 0.60 -> exit 0.595, exitUsd 59.5
|
||||||
|
// fees = 1% von 50 (Entry) + 1% von 59.5 (Exit) = 0.5 + 0.595 = 1.095
|
||||||
|
// pnl = 59.5 - 50 - 1.095 = 8.405
|
||||||
|
var (exitUsd, pnl, fees) = CloseWithFees(size: 100m, signalPrice: 0.60m, entryAmountUsd: 50m, feeBps: 100, halfSpread: 0.005m);
|
||||||
|
Assert.Equal(59.5m, exitUsd);
|
||||||
|
Assert.Equal(1.095m, fees);
|
||||||
|
Assert.Equal(8.405m, pnl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CloseWithFees_zero_fee_only_slippage()
|
||||||
|
{
|
||||||
|
var (exitUsd, pnl, fees) = CloseWithFees(100m, 0.60m, 50m, feeBps: 0, halfSpread: 0.005m);
|
||||||
|
Assert.Equal(0m, fees);
|
||||||
|
Assert.Equal(exitUsd - 50m, pnl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user