Slice 4 (Fable-Fixes): H4/M1/M2/M3-min/M4/M6 + Doku

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>
This commit is contained in:
Richard
2026-07-09 14:03:00 +02:00
co-authored by Claude Opus 4.8
parent 8252219163
commit bdcd3641e4
14 changed files with 148 additions and 22 deletions
+12
View File
@@ -40,5 +40,17 @@ namespace PolyTrader.Tests
{
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));
}
}
}
@@ -192,5 +192,22 @@ namespace PolyTrader.Tests
Assert.False(copy.ExitLadders.ContainsKey("1_" + Tok)); // Leiter beendet
Assert.Empty(clob.Placed);
}
[Fact]
public async Task Ladder_aborts_on_dust_remainder_below_minimum()
{
// H4: Dust-Rest unter dem Minimum -> Leiter beenden statt endloser Sub-Minimum-Rejects.
var (svc, copy, state, clob, _) = Build();
var (account, pos) = LiveAccountWithPosition(state, size: 3m); // < MinShares (5.5)
pos.ExitPending = true;
var ladder = FloorLadder();
copy.ExitLadders["1_" + Tok] = ladder;
await svc.ProcessLadderAsync("1_" + Tok, ladder);
Assert.False(copy.ExitLadders.ContainsKey("1_" + Tok)); // Leiter beendet
Assert.False(pos.ExitPending); // Rest handelbar/gehalten
Assert.Empty(clob.Placed); // keine Sub-Minimum-Order
}
}
}
+22
View File
@@ -177,5 +177,27 @@ namespace PolyTrader.Tests
Assert.Equal(floor, price); // endet exakt auf dem Floor (geclamped)
Assert.InRange(steps, 1, 20); // terminiert in wenigen Schritten
}
// ----- RoundToTick (H4) -----
[Theory]
[InlineData(0.9702, 0.970)] // typischer Startlimit-Wert -> 3 Dezimalen
[InlineData(0.84150, 0.842)] // AwayFromZero an der Hälfte
[InlineData(0.5, 0.500)]
[InlineData(0.123456, 0.123)]
public void RoundToTick_rounds_to_three_decimals(double input, double expected)
{
Assert.Equal((decimal)expected, RoundToTick((decimal)input));
}
[Fact]
public void RoundToTick_makes_usdc_over_price_yield_exact_size()
{
// Kern von H4: mit gerundetem Preis geht size = usdc / price exakt auf.
decimal size = 100m;
decimal price = RoundToTick(NextPrice(0.9702m, LadderStepPct, 0.80m));
decimal usdc = size * price;
Assert.Equal(size, usdc / price);
}
}
}