fix(api): auto-close dust exit remainders

This commit is contained in:
root 2026-05-06 16:39:13 +00:00
parent 9e3f99e7a9
commit d8941c5ad0

View File

@ -128,6 +128,35 @@ export class TradeExecutor {
} }
return 0.0001; return 0.0001;
} }
private getDustNotionalThresholdUsd(): number {
const configured = Number(config.MIN_NOTIONAL_USD || 10);
if (Number.isFinite(configured) && configured > 0) {
return configured;
}
return 10;
}
private isDustRemainder(qty: number, referencePrice?: number): boolean {
const normalizedQty = Number(qty);
if (!(Number.isFinite(normalizedQty) && normalizedQty > 0)) {
return true;
}
if (normalizedQty <= this.getDustQtyThreshold()) {
return true;
}
const normalizedPrice = Number(referencePrice);
if (Number.isFinite(normalizedPrice) && normalizedPrice > 0) {
const remainingNotionalUsd = normalizedQty * normalizedPrice;
if (remainingNotionalUsd <= this.getDustNotionalThresholdUsd()) {
return true;
}
}
return false;
}
constructor( constructor(
private exchange: IExchangeConnector, private exchange: IExchangeConnector,
@ -1642,13 +1671,14 @@ export class TradeExecutor {
// We use a local variable for the order volume. // We use a local variable for the order volume.
} else { } else {
const dustThreshold = this.getDustQtyThreshold(); const dustThreshold = this.getDustQtyThreshold();
if (pos.size <= dustThreshold) { const fallbackExitPrice = Number.isFinite(Number(currentPriceHint)) && Number(currentPriceHint) > 0
const fallbackExitPrice = Number.isFinite(Number(currentPriceHint)) && Number(currentPriceHint) > 0 ? Number(currentPriceHint)
? Number(currentPriceHint) : (this.apiServer?.getState?.().symbols?.[symbol]?.price || pos.entryPrice);
: (this.apiServer?.getState?.().symbols?.[symbol]?.price || pos.entryPrice); if (this.isDustRemainder(pos.size, fallbackExitPrice)) {
logger.warn(`[Hardening] Auto-finalizing dust remainder for ${symbol}: local=${pos.size}, exchange=${exchangeQty}, threshold=${dustThreshold}.`); const dustNotionalThreshold = this.getDustNotionalThresholdUsd();
logger.warn(`[Hardening] Auto-finalizing dust remainder for ${symbol}: local=${pos.size}, exchange=${exchangeQty}, qty_threshold=${dustThreshold}, notional_threshold_usd=${dustNotionalThreshold}.`);
await this.finalizeTrade(symbol, fallbackExitPrice, `${reason} (dust auto-close)`, positionTradeId); await this.finalizeTrade(symbol, fallbackExitPrice, `${reason} (dust auto-close)`, positionTradeId);
this.setExitLifecycle(symbol, 'filled', reason, `dust_autoclose_threshold=${dustThreshold}`); this.setExitLifecycle(symbol, 'filled', reason, `dust_autoclose_qty=${dustThreshold};dust_autoclose_notional_usd=${dustNotionalThreshold}`);
return { success: true, exitPrice: fallbackExitPrice }; return { success: true, exitPrice: fallbackExitPrice };
} }
logger.error(`[Guardrail] Aborting SELL Exit for ${symbol}: Insufficient exchange balance (Requested: ${pos.size}, Exchange: ${exchangeQty}).`); logger.error(`[Guardrail] Aborting SELL Exit for ${symbol}: Insufficient exchange balance (Requested: ${pos.size}, Exchange: ${exchangeQty}).`);
@ -1889,20 +1919,26 @@ export class TradeExecutor {
}; };
} }
const appliedQty = Math.min(requestedFillQty, pos.size); const appliedQty = Math.min(requestedFillQty, pos.size);
const remainingSize = Math.max(0, Number((pos.size - appliedQty).toFixed(8))); const remainingSize = Math.max(0, Number((pos.size - appliedQty).toFixed(8)));
const resolvedExitPrice = Number.isFinite(exitPrice) && exitPrice > 0 ? exitPrice : pos.entryPrice; const resolvedExitPrice = Number.isFinite(exitPrice) && exitPrice > 0 ? exitPrice : pos.entryPrice;
const dustQtyThreshold = this.getDustQtyThreshold();
// Fully closed lifecycle: finalize and clear local position state. const dustNotionalThreshold = this.getDustNotionalThresholdUsd();
if (remainingSize <= 1e-8) { const shouldAutoCloseDustRemainder = this.isDustRemainder(remainingSize, resolvedExitPrice);
await this.finalizeTrade(symbol, resolvedExitPrice, reason, selectedTradeId);
return { // Fully closed lifecycle: finalize and clear local position state.
success: true, if (remainingSize <= 1e-8 || shouldAutoCloseDustRemainder) {
fullyClosed: true, if (remainingSize > 1e-8) {
appliedQty, logger.warn(`[Hardening] Auto-finalizing post-exit dust remainder for ${symbol}: remaining=${remainingSize}, qty_threshold=${dustQtyThreshold}, notional_threshold_usd=${dustNotionalThreshold}, price=${resolvedExitPrice}.`);
remainingSize: 0 }
}; await this.finalizeTrade(symbol, resolvedExitPrice, reason, selectedTradeId);
} return {
success: true,
fullyClosed: true,
appliedQty,
remainingSize: 0
};
}
// Partial exit lifecycle: persist realized slice and keep monitoring remainder. // Partial exit lifecycle: persist realized slice and keep monitoring remainder.
const pnl = (resolvedExitPrice - pos.entryPrice) * appliedQty * (pos.side === SignalDirection.BUY ? 1 : -1); const pnl = (resolvedExitPrice - pos.entryPrice) * appliedQty * (pos.side === SignalDirection.BUY ? 1 : -1);