H4 – Rundungs-Dust-Reject-Schleife: - SellLogic.RoundToTick (0.001, AwayFromZero, wie CalculateExactOrderAmounts). Die Leiter platziert Preise gerundet -> usdc = size × Preis geht exakt auf (kein Dust). - ProcessLadderAsync bricht ab, wenn pos.Size < MinShares (Dust-Rest gehalten, ExitPending=false), statt endlos Sub-Minimum-Orders zu schicken. M1 – GlobalPnl-Doppelzaehlung: in beiden Live-Close-Bloecken jetzt NUR innerhalb des _processedClosures-Dedup-Guards gebucht (API-Lag zaehlte sonst doppelt). M2 – TokenId in beiden Live-Close-Records gesetzt (sonst greift die Dedup nach Neustart nicht). M3 (Minimum) – TradeId-Init: serverseitiges GetMaxTradeId() statt Full-Table-Find(_=>true).Max(); Fehlschlag wird laut geloggt statt still geschluckt. (Autoincrement-Migration bewusst als Follow-up aufgeschoben – Schema-Aenderung an der Trade-Persistenz erst im Zielland live verifizieren.) M4 – MongoExportParser ProfitTarget-Default 50 -> 9999 (Take-Profit bleibt dormant). M6 – Fee-Satz (real oder Kategorie-Fallback, FeeModel.ResolveBps) in alle signierten Orders: Engine-BUY, SELL-Leiter (Start/Step/Floor), PreRedeem (WSS + REST). API-gated, im Zielland verifizieren. Doku – stale [Description]: ProfitTarget/SellFloorPct als IMPLEMENTIERT markiert. Tests: +10 (RoundToTick, Dust-Abbruch, ResolveBps). Build 0 Fehler, 233 gruen, --smoke-ui ok. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
1.9 KiB
C#
57 lines
1.9 KiB
C#
using PolyTrader.Modules.CopyTrading.Logic;
|
|
using Xunit;
|
|
using static PolyTrader.Modules.CopyTrading.Logic.FeeModel;
|
|
|
|
namespace PolyTrader.Tests
|
|
{
|
|
/// <summary>
|
|
/// Sicherheitsnetz für die Fee-Logik (Phase 0.2): Kategorie-Fallback + Fee-Berechnung.
|
|
/// </summary>
|
|
public class FeeModelTests
|
|
{
|
|
[Theory]
|
|
[InlineData("Sports", 75)]
|
|
[InlineData("SPORTS", 75)] // case-insensitive
|
|
[InlineData("Crypto", 180)]
|
|
[InlineData("Geopolitics", 0)]
|
|
[InlineData("Politics", 100)]
|
|
[InlineData("Finance", 100)]
|
|
[InlineData("Economics", 100)]
|
|
[InlineData("Weird-Unknown", 100)] // konservativer Default
|
|
[InlineData("", 100)]
|
|
[InlineData(null, 100)]
|
|
public void FallbackBps_maps_category(string? category, int expected)
|
|
{
|
|
Assert.Equal(expected, FallbackBps(category));
|
|
}
|
|
|
|
[Fact]
|
|
public void FeeUsd_is_notional_times_bps()
|
|
{
|
|
Assert.Equal(1.0m, FeeUsd(100m, 100)); // 1,0 % von 100
|
|
Assert.Equal(0.9m, FeeUsd(50m, 180)); // 1,8 % von 50
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(100, 0)] // kein Satz
|
|
[InlineData(0, 100)] // kein Notional
|
|
[InlineData(-5, 100)] // negatives Notional
|
|
public void FeeUsd_zero_cases(double notional, int bps)
|
|
{
|
|
Assert.Equal(0m, FeeUsd((decimal)notional, bps));
|
|
}
|
|
|
|
// ----- ResolveBps (M6) -----
|
|
|
|
[Theory]
|
|
[InlineData(120, "Sports", 120)] // realer Satz gewinnt
|
|
[InlineData(0, "Sports", 75)] // kein realer Satz -> Kategorie-Fallback
|
|
[InlineData(0, "Crypto", 180)]
|
|
[InlineData(0, null, 100)] // unbekannt -> Default
|
|
public void ResolveBps_prefers_actual_then_fallback(int takerFeeBps, string? category, int expected)
|
|
{
|
|
Assert.Equal(expected, ResolveBps(takerFeeBps, category));
|
|
}
|
|
}
|
|
}
|