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;
|
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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user