fix(api): auto-close dust exit remainders
This commit is contained in:
parent
9e3f99e7a9
commit
d8941c5ad0
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user