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:
co-authored by
Claude Opus 4.8
parent
8252219163
commit
bdcd3641e4
@@ -542,7 +542,11 @@ namespace PolyTraderSharp.Services
|
||||
|
||||
// Maker-Einstieg (Phase 4.1) ruht als GTC-Limit; sonst GTD (Taker-nah).
|
||||
string buyOrderType = (trader != null && trader.MakerEntry) ? "GTC" : "GTD";
|
||||
var result = await _clob.PlaceOrderAsync(account, signal.TokenId, signal.Side, exactUsdc, orderPrice, buyOrderType, _state.DebugOrderPayloadLog, isNegRisk);
|
||||
// M6: realen Taker-Satz (bzw. Kategorie-Fallback) in die signierte Order schreiben.
|
||||
int buyFeeBps = _state.MarketCache.TryGetValue(signal.TokenId, out var buyMd)
|
||||
? FeeModel.ResolveBps(buyMd.TakerFeeBps, buyMd.Category)
|
||||
: FeeModel.FallbackBps(null);
|
||||
var result = await _clob.PlaceOrderAsync(account, signal.TokenId, signal.Side, exactUsdc, orderPrice, buyOrderType, _state.DebugOrderPayloadLog, isNegRisk, buyFeeBps);
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using PolyTrader.Core.Persistence;
|
||||
using PolyTrader.Modules.CopyTrading.Logic;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -230,8 +231,11 @@ namespace PolyTraderSharp.Services
|
||||
decimal expectedFillPrice = _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit;
|
||||
decimal amountUsdc = Math.Max(pos.Size * expectedFillPrice, 0.01m);
|
||||
|
||||
// Fire and forget SELL via ClobClient
|
||||
var result = await _clob.PlaceOrderAsync(acc, pos.TokenId, "SELL", amountUsdc, expectedFillPrice, "GTC", false, false);
|
||||
// Fire and forget SELL via ClobClient. M6: realen Taker-Satz (bzw. Fallback) mitsenden.
|
||||
int redeemFeeBps = _state.MarketCache.TryGetValue(pos.TokenId, out var rMd)
|
||||
? FeeModel.ResolveBps(rMd.TakerFeeBps, rMd.Category)
|
||||
: FeeModel.FallbackBps(null);
|
||||
var result = await _clob.PlaceOrderAsync(acc, pos.TokenId, "SELL", amountUsdc, expectedFillPrice, "GTC", false, false, redeemFeeBps);
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
|
||||
@@ -65,6 +65,9 @@ namespace PolyTraderSharp.Services
|
||||
firstLimit = Math.Clamp(firstLimit, 0.01m, 0.99m);
|
||||
floor = Math.Clamp(floor, 0.01m, 0.99m);
|
||||
if (floor > firstLimit) floor = firstLimit; // Floor nie über dem Startlimit
|
||||
// H4: Preise auf Tick runden, damit usdc = size × Preis exakt aufgeht (kein Dust-Rest).
|
||||
firstLimit = SellLogic.RoundToTick(firstLimit);
|
||||
floor = SellLogic.RoundToTick(floor);
|
||||
|
||||
var exact = PolymarketClobClient.CalculateExactOrderAmounts(pos.Size * firstLimit, firstLimit, firstLimit, "SELL");
|
||||
if (exact.shares <= 0)
|
||||
@@ -114,7 +117,11 @@ namespace PolyTraderSharp.Services
|
||||
$" Referenz: {referencePrice:F3} | Startlimit: {firstLimit:F3} | Floor: {floor:F3}\n" +
|
||||
$" Stufen: {(isHf ? "HF ~20s" : "~120s")}/Schritt, {SellLogic.LadderStepPct}% relativ");
|
||||
|
||||
var result = await _clob.PlaceOrderAsync(account, pos.TokenId, "SELL", pos.Size * firstLimit, firstLimit, "GTC", _state.DebugOrderPayloadLog, isNegRisk);
|
||||
// M6: realen Taker-Satz (bzw. Kategorie-Fallback) in die signierte Order schreiben.
|
||||
int feeBps = _state.MarketCache.TryGetValue(pos.TokenId, out var startMd)
|
||||
? FeeModel.ResolveBps(startMd.TakerFeeBps, startMd.Category)
|
||||
: FeeModel.FallbackBps(null);
|
||||
var result = await _clob.PlaceOrderAsync(account, pos.TokenId, "SELL", pos.Size * firstLimit, firstLimit, "GTC", _state.DebugOrderPayloadLog, isNegRisk, feeBps);
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
@@ -170,6 +177,18 @@ namespace PolyTraderSharp.Services
|
||||
return;
|
||||
}
|
||||
|
||||
// H4: Dust-Rest unter dem Polymarket-Minimum lässt sich nie als Order platzieren. Leiter
|
||||
// beenden statt endlos Sub-Minimum-Orders zu schicken (ewige API-Rejects; ExitPending bliebe
|
||||
// sonst für immer). Der Mikro-Rest wird bewusst gehalten und geloggt.
|
||||
if (pos.Size < CopyTradingRisk.MinShares)
|
||||
{
|
||||
_copyState.ExitLadders.TryRemove(key, out _);
|
||||
pos.ExitPending = false;
|
||||
if (!account.IsDemo) _positionRepo.UpsertLive(account.AccountId, pos);
|
||||
_logger.Warning($"🧹 [SELL-LEITER Dust] {account.Name} | {ladder.MarketQuestion}: Restgröße {pos.Size:F2} < Minimum {CopyTradingRisk.MinShares} Shares – Leiter beendet, Rest gehalten.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Noch nicht Zeit für die nächste Stufe?
|
||||
double waited = (DateTime.UtcNow - ladder.LastActionAt).TotalSeconds;
|
||||
if (waited < SellLogic.LadderIntervalSeconds(ladder.IsHf)) return;
|
||||
@@ -196,10 +215,11 @@ namespace PolyTraderSharp.Services
|
||||
if (!orderResting)
|
||||
{
|
||||
bool isNegFloor = _state.MarketCache.TryGetValue(ladder.TokenId, out var mdFloor) && mdFloor.NegRisk;
|
||||
int floorFeeBps = mdFloor != null ? FeeModel.ResolveBps(mdFloor.TakerFeeBps, mdFloor.Category) : FeeModel.FallbackBps(null);
|
||||
_logger.Warning($"🔁 [SELL-LEITER Floor] {account.Name} | {ladder.MarketQuestion}: keine ruhende SELL-Order am Floor {ladder.Floor:F3} – platziere neu.");
|
||||
try
|
||||
{
|
||||
var r = await _clob.PlaceOrderAsync(account, ladder.TokenId, "SELL", pos.Size * ladder.Floor, ladder.Floor, "GTC", _state.DebugOrderPayloadLog, isNegFloor);
|
||||
var r = await _clob.PlaceOrderAsync(account, ladder.TokenId, "SELL", pos.Size * ladder.Floor, ladder.Floor, "GTC", _state.DebugOrderPayloadLog, isNegFloor, floorFeeBps);
|
||||
if (r == "OK")
|
||||
_copyState.PendingOrderTimestamps[key] = (DateTime.UtcNow, ladder.SourceTraderId, "SELL");
|
||||
else
|
||||
@@ -225,8 +245,9 @@ namespace PolyTraderSharp.Services
|
||||
}
|
||||
|
||||
// Eine Stufe tiefer: alte Order canceln, tiefer neu platzieren.
|
||||
decimal next = SellLogic.NextPrice(ladder.CurrentLimit, SellLogic.LadderStepPct, ladder.Floor);
|
||||
decimal next = SellLogic.RoundToTick(SellLogic.NextPrice(ladder.CurrentLimit, SellLogic.LadderStepPct, ladder.Floor));
|
||||
bool isNegRisk = _state.MarketCache.TryGetValue(ladder.TokenId, out var md) && md.NegRisk;
|
||||
int stepFeeBps = md != null ? FeeModel.ResolveBps(md.TakerFeeBps, md.Category) : FeeModel.FallbackBps(null);
|
||||
|
||||
_logger.Trade($"🪜 [SELL-LEITER Stufe {ladder.Attempt + 1}] {account.Name} | {ladder.MarketQuestion}\n" +
|
||||
$" Limit {ladder.CurrentLimit:F3} → {next:F3} (Floor {ladder.Floor:F3}, {(ladder.IsHf ? "HF" : "Standard")})");
|
||||
@@ -236,7 +257,7 @@ namespace PolyTraderSharp.Services
|
||||
await _clob.CancelConflictingOrdersAsync(account, ladder.TokenId, next, "SELL");
|
||||
|
||||
decimal usdc = pos.Size * next;
|
||||
var result = await _clob.PlaceOrderAsync(account, ladder.TokenId, "SELL", usdc, next, "GTC", _state.DebugOrderPayloadLog, isNegRisk);
|
||||
var result = await _clob.PlaceOrderAsync(account, ladder.TokenId, "SELL", usdc, next, "GTC", _state.DebugOrderPayloadLog, isNegRisk, stepFeeBps);
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
|
||||
@@ -460,7 +460,11 @@ namespace PolyTraderSharp.Services
|
||||
{
|
||||
decimal expectedFillPrice = _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit;
|
||||
decimal amountUsdc = Math.Max(pos.Size * expectedFillPrice, 0.01m);
|
||||
var result = await _clob.PlaceOrderAsync(acc, pos.TokenId, "SELL", amountUsdc, expectedFillPrice, "GTC", false, false);
|
||||
// M6: realen Taker-Satz (bzw. Kategorie-Fallback) in die signierte Order schreiben.
|
||||
int redeemFeeBps = _state.MarketCache.TryGetValue(pos.TokenId, out var rMd)
|
||||
? FeeModel.ResolveBps(rMd.TakerFeeBps, rMd.Category)
|
||||
: FeeModel.FallbackBps(null);
|
||||
var result = await _clob.PlaceOrderAsync(acc, pos.TokenId, "SELL", amountUsdc, expectedFillPrice, "GTC", false, false, redeemFeeBps);
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
@@ -915,12 +919,14 @@ namespace PolyTraderSharp.Services
|
||||
|
||||
if (matchedClose.Value.TryGetProperty("realizedPnl", out var rPnlProp)) realizedPnl = ParseDecimal(rPnlProp);
|
||||
|
||||
_state.GlobalPnl += realizedPnl;
|
||||
decimal exitPrice = removedPos.Size > 0 ? (removedPos.AmountUsd + realizedPnl) / removedPos.Size : 0m;
|
||||
|
||||
|
||||
string duplicateKey = $"{acc.AccountId}_{removedPos.TokenId}";
|
||||
if (!_processedClosures.ContainsKey(duplicateKey))
|
||||
{
|
||||
// M1: GlobalPnl NUR innerhalb des Dedup-Guards buchen – sonst Doppelzählung
|
||||
// beim API-Lag (re-added Position → späterer matchedClose zählt erneut).
|
||||
_state.GlobalPnl += realizedPnl;
|
||||
_logger.Info($"🏆 Live Market {removedPos.MarketQuestion} geschlossen! PnL: ${(realizedPnl):F2}");
|
||||
|
||||
string orderKey = $"{acc.AccountId}_{removedPos.TokenId}";
|
||||
@@ -933,6 +939,7 @@ namespace PolyTraderSharp.Services
|
||||
AccountId = acc.AccountId,
|
||||
SourceTraderId = removedPos.SourceTraderId,
|
||||
IsDemo = false,
|
||||
TokenId = removedPos.TokenId, // M2: sonst fällt der Record aus dem _processedClosures-Preload (Dedup nach Neustart)
|
||||
MarketSlug = removedPos.MarketSlug,
|
||||
MarketQuestion = removedPos.MarketQuestion,
|
||||
Outcome = removedPos.Outcome,
|
||||
@@ -966,11 +973,11 @@ namespace PolyTraderSharp.Services
|
||||
decimal exitUsd = removedPos.Size * exitPrice;
|
||||
decimal realizedPnl = exitUsd - removedPos.AmountUsd;
|
||||
|
||||
_state.GlobalPnl += realizedPnl;
|
||||
|
||||
string duplicateKey = $"{acc.AccountId}_{removedPos.TokenId}";
|
||||
if (!_processedClosures.ContainsKey(duplicateKey))
|
||||
{
|
||||
// M1: GlobalPnl NUR innerhalb des Dedup-Guards buchen (siehe oben).
|
||||
_state.GlobalPnl += realizedPnl;
|
||||
_logger.Info($"🏆 Live Market {removedPos.MarketQuestion} aufgelöst (Fallback)! Auszahlung: ${(exitPrice * removedPos.Size):F2}");
|
||||
|
||||
string orderKey = $"{acc.AccountId}_{removedPos.TokenId}";
|
||||
@@ -983,6 +990,7 @@ namespace PolyTraderSharp.Services
|
||||
AccountId = acc.AccountId,
|
||||
SourceTraderId = removedPos.SourceTraderId,
|
||||
IsDemo = false,
|
||||
TokenId = removedPos.TokenId, // M2: sonst greift die Dedup nach Neustart nicht
|
||||
MarketSlug = removedPos.MarketSlug,
|
||||
MarketQuestion = removedPos.MarketQuestion,
|
||||
Outcome = removedPos.Outcome,
|
||||
|
||||
Reference in New Issue
Block a user