mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
feat: 优化订单去重和取消逻辑,确保在处理STOP订单时避免精度损失
This commit is contained in:
@@ -89,7 +89,14 @@ export async function deduplicateOrders(
|
||||
side: string,
|
||||
log: LogHandler
|
||||
): Promise<void> {
|
||||
const sameTypeOrders = openOrders.filter((o) => o.type === type && o.side === side);
|
||||
// Treat STOP orders on some exchanges (e.g., Lighter) as LIMIT with stopPrice populated.
|
||||
const sameTypeOrders = openOrders.filter((o) => {
|
||||
const normalizedType = String(o.type).toUpperCase();
|
||||
const isStopLike = Number.isFinite(Number(o.stopPrice)) && Number(o.stopPrice) > 0;
|
||||
const matchesStop = type === "STOP_MARKET" && isStopLike && o.side === side;
|
||||
const exactMatch = normalizedType === type && o.side === side;
|
||||
return exactMatch || matchesStop;
|
||||
});
|
||||
if (sameTypeOrders.length <= 1) return;
|
||||
sameTypeOrders.sort((a, b) => {
|
||||
const ta = b.updateTime || b.time || 0;
|
||||
@@ -241,7 +248,6 @@ export async function placeStopLossOrder(
|
||||
}
|
||||
const priceTick = opts?.priceTick ?? 0.1;
|
||||
const qtyStep = opts?.qtyStep ?? 0.001;
|
||||
const isAster = String((adapter as any).id ?? "").toLowerCase() === "aster";
|
||||
const params: CreateOrderParams = {
|
||||
symbol,
|
||||
side,
|
||||
@@ -249,14 +255,10 @@ export async function placeStopLossOrder(
|
||||
stopPrice: roundDownToTick(stopPrice, priceTick),
|
||||
closePosition: "true",
|
||||
timeInForce: "GTC",
|
||||
// Do not round down stop quantity here to avoid underflow to exchange min; gateway will quantize precisely
|
||||
quantity,
|
||||
triggerType: "STOP_LOSS",
|
||||
};
|
||||
// For Aster futures, STOP orders must not include reduceOnly; omit it specifically.
|
||||
if (!isAster) {
|
||||
params.reduceOnly = "true";
|
||||
}
|
||||
|
||||
// Avoid forcing price for STOP_MARKET globally; keep this exchange-specific in gateways
|
||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||
lockOperating(locks, timers, pendings, type, log);
|
||||
@@ -299,8 +301,6 @@ export async function placeTrailingStopOrder(
|
||||
symbol,
|
||||
side,
|
||||
type,
|
||||
// Do not round down trailing-stop quantity to avoid underflowing small positions to zero;
|
||||
// let the exchange adapter handle precise quantization.
|
||||
quantity,
|
||||
reduceOnly: "true",
|
||||
activationPrice: roundDownToTick(activationPrice, priceTick),
|
||||
@@ -351,8 +351,6 @@ export async function marketClose(
|
||||
side,
|
||||
type,
|
||||
quantity: safeQty,
|
||||
reduceOnly: "true",
|
||||
// Hint exchanges (like Paradex) to close the whole position and tolerate omitted size
|
||||
closePosition: "true",
|
||||
};
|
||||
await deduplicateOrders(adapter, symbol, openOrders, locks, timers, pendings, type, side, log);
|
||||
|
||||
@@ -108,13 +108,14 @@ export class LighterExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder({ orderId: params.orderId });
|
||||
// Accept both clientOrderId and order_index as strings; forward as-is to preserve precision
|
||||
await this.gateway.cancelOrder({ orderId: String(params.orderId) });
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
for (const orderId of params.orderIdList) {
|
||||
await this.gateway.cancelOrder({ orderId });
|
||||
await this.gateway.cancelOrder({ orderId: String(orderId) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -312,7 +312,14 @@ export class LighterGateway {
|
||||
await this.ensureInitialized();
|
||||
const marketIndex = params.marketIndex ?? this.marketId;
|
||||
if (marketIndex == null) throw new Error("Market index unknown");
|
||||
const indexValue = BigInt(typeof params.orderId === "string" ? Number(params.orderId) : params.orderId);
|
||||
// Parse order id to BigInt without precision loss; prefer string input
|
||||
let indexValue: bigint;
|
||||
if (typeof params.orderId === "string") {
|
||||
indexValue = BigInt(params.orderId);
|
||||
} else {
|
||||
// Fallback for numeric ids (may be unsafe if beyond 2^53-1)
|
||||
indexValue = BigInt(Math.trunc(params.orderId));
|
||||
}
|
||||
const { apiKeyIndex, nonce } = this.nonceManager.next();
|
||||
try {
|
||||
const signed = await this.signer.signCancelOrder({
|
||||
@@ -323,6 +330,11 @@ export class LighterGateway {
|
||||
});
|
||||
const auth = await this.ensureAuthToken();
|
||||
await this.http.sendTransaction(signed.txType, signed.txInfo, { authToken: auth });
|
||||
// Optimistically remove the order locally to avoid stale duplicates until WS confirms
|
||||
const key = String(params.orderId);
|
||||
this.orderMap.delete(key);
|
||||
this.orders = Array.from(this.orderMap.values());
|
||||
this.emitOrders();
|
||||
} catch (error) {
|
||||
this.nonceManager.acknowledgeFailure(apiKeyIndex);
|
||||
throw error;
|
||||
|
||||
@@ -78,7 +78,8 @@ export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterO
|
||||
? "SELL"
|
||||
: "BUY";
|
||||
return {
|
||||
orderId: order.order_index,
|
||||
// Use string order id to avoid precision loss; prefer on-chain order_index for cancellation
|
||||
orderId: String(order.order_index ?? order.client_order_index ?? ""),
|
||||
clientOrderId: String(order.client_order_index ?? order.order_index ?? ""),
|
||||
symbol,
|
||||
side,
|
||||
|
||||
Reference in New Issue
Block a user