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;
}
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(
private exchange: IExchangeConnector,
@ -1642,13 +1671,14 @@ export class TradeExecutor {
// We use a local variable for the order volume.
} else {
const dustThreshold = this.getDustQtyThreshold();
if (pos.size <= dustThreshold) {
const fallbackExitPrice = Number.isFinite(Number(currentPriceHint)) && Number(currentPriceHint) > 0
? Number(currentPriceHint)
: (this.apiServer?.getState?.().symbols?.[symbol]?.price || pos.entryPrice);
logger.warn(`[Hardening] Auto-finalizing dust remainder for ${symbol}: local=${pos.size}, exchange=${exchangeQty}, threshold=${dustThreshold}.`);
const fallbackExitPrice = Number.isFinite(Number(currentPriceHint)) && Number(currentPriceHint) > 0
? Number(currentPriceHint)
: (this.apiServer?.getState?.().symbols?.[symbol]?.price || pos.entryPrice);
if (this.isDustRemainder(pos.size, fallbackExitPrice)) {
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);
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 };
}
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 remainingSize = Math.max(0, Number((pos.size - appliedQty).toFixed(8)));
const resolvedExitPrice = Number.isFinite(exitPrice) && exitPrice > 0 ? exitPrice : pos.entryPrice;
// Fully closed lifecycle: finalize and clear local position state.
if (remainingSize <= 1e-8) {
await this.finalizeTrade(symbol, resolvedExitPrice, reason, selectedTradeId);
return {
success: true,
fullyClosed: true,
appliedQty,
remainingSize: 0
};
}
const appliedQty = Math.min(requestedFillQty, pos.size);
const remainingSize = Math.max(0, Number((pos.size - appliedQty).toFixed(8)));
const resolvedExitPrice = Number.isFinite(exitPrice) && exitPrice > 0 ? exitPrice : pos.entryPrice;
const dustQtyThreshold = this.getDustQtyThreshold();
const dustNotionalThreshold = this.getDustNotionalThresholdUsd();
const shouldAutoCloseDustRemainder = this.isDustRemainder(remainingSize, resolvedExitPrice);
// Fully closed lifecycle: finalize and clear local position state.
if (remainingSize <= 1e-8 || shouldAutoCloseDustRemainder) {
if (remainingSize > 1e-8) {
logger.warn(`[Hardening] Auto-finalizing post-exit dust remainder for ${symbol}: remaining=${remainingSize}, qty_threshold=${dustQtyThreshold}, notional_threshold_usd=${dustNotionalThreshold}, price=${resolvedExitPrice}.`);
}
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.
const pnl = (resolvedExitPrice - pos.entryPrice) * appliedQty * (pos.side === SignalDirection.BUY ? 1 : -1);