mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 01:08:07 +00:00
feat: 更新网格引擎,增强订单消失分类处理逻辑,添加临时阻塞机制以避免重复开仓
This commit is contained in:
@@ -93,6 +93,11 @@ export class GridEngine {
|
|||||||
private readonly pendingShortLevels = new Set<number>();
|
private readonly pendingShortLevels = new Set<number>();
|
||||||
private readonly closeKeyBySourceLevel = new Map<number, string>();
|
private readonly closeKeyBySourceLevel = new Map<number, string>();
|
||||||
private lastKeyMeta = new Map<string, { side: "BUY" | "SELL"; level: number; reduceOnly: boolean }>();
|
private lastKeyMeta = new Map<string, { side: "BUY" | "SELL"; level: number; reduceOnly: boolean }>();
|
||||||
|
private lastOrderIdsByKey = new Map<string, Set<string>>();
|
||||||
|
// When an order at a level disappears but account delta hasn't arrived yet,
|
||||||
|
// temporarily block re-opening at that level to avoid immediate re-placement.
|
||||||
|
private readonly levelReopenBlockUntil = new Map<number, number>();
|
||||||
|
private static readonly DISAPPEAR_REOPEN_COOLDOWN_MS = 2000;
|
||||||
private sidesLocked = false;
|
private sidesLocked = false;
|
||||||
private startupCleaned = false;
|
private startupCleaned = false;
|
||||||
private initialCloseHandled = false;
|
private initialCloseHandled = false;
|
||||||
@@ -445,6 +450,12 @@ export class GridEngine {
|
|||||||
|
|
||||||
private async syncGridSimple(price: number): Promise<void> {
|
private async syncGridSimple(price: number): Promise<void> {
|
||||||
const activeOrders = this.openOrders.filter((o) => o.symbol === this.config.symbol && o.type === "LIMIT");
|
const activeOrders = this.openOrders.filter((o) => o.symbol === this.config.symbol && o.type === "LIMIT");
|
||||||
|
// Build lookup for all recent orders by id (including non-active) to read final statuses
|
||||||
|
const allOrdersById = new Map<string, AsterOrder>();
|
||||||
|
for (const o of this.openOrders) {
|
||||||
|
if (o.symbol !== this.config.symbol) continue;
|
||||||
|
allOrdersById.set(String(o.orderId), o);
|
||||||
|
}
|
||||||
|
|
||||||
// Detect fills by comparing previous keys vs current snapshot
|
// Detect fills by comparing previous keys vs current snapshot
|
||||||
const prevKeys = this.lastOpenOrderKeys;
|
const prevKeys = this.lastOpenOrderKeys;
|
||||||
@@ -452,6 +463,7 @@ export class GridEngine {
|
|||||||
|
|
||||||
const currentKeys = new Set<string>();
|
const currentKeys = new Set<string>();
|
||||||
const keyToMeta = new Map<string, { side: "BUY" | "SELL"; level: number; reduceOnly: boolean }>();
|
const keyToMeta = new Map<string, { side: "BUY" | "SELL"; level: number; reduceOnly: boolean }>();
|
||||||
|
const currentOrderIdsByKey = new Map<string, Set<string>>();
|
||||||
for (const o of activeOrders) {
|
for (const o of activeOrders) {
|
||||||
const key = this.getOrderKey(o.side, this.normalizePrice(o.price), o.reduceOnly === true);
|
const key = this.getOrderKey(o.side, this.normalizePrice(o.price), o.reduceOnly === true);
|
||||||
currentKeys.add(key);
|
currentKeys.add(key);
|
||||||
@@ -459,6 +471,10 @@ export class GridEngine {
|
|||||||
if (level != null) {
|
if (level != null) {
|
||||||
keyToMeta.set(key, { side: o.side, level, reduceOnly: o.reduceOnly === true });
|
keyToMeta.set(key, { side: o.side, level, reduceOnly: o.reduceOnly === true });
|
||||||
}
|
}
|
||||||
|
const id = String(o.orderId);
|
||||||
|
const set = currentOrderIdsByKey.get(key) ?? new Set<string>();
|
||||||
|
set.add(id);
|
||||||
|
currentOrderIdsByKey.set(key, set);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const prevKey of prevKeys) {
|
for (const prevKey of prevKeys) {
|
||||||
@@ -482,20 +498,49 @@ export class GridEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (handledByCloseTracking) continue;
|
if (handledByCloseTracking) continue;
|
||||||
// Otherwise, treat disappearance as potential open fill: confirm by position delta increase
|
// Use order records to classify disappearance: filled vs canceled
|
||||||
if (meta) {
|
if (meta) {
|
||||||
const absNow = Math.abs(this.position.positionAmt);
|
const prevIds = this.lastOrderIdsByKey.get(prevKey);
|
||||||
if (absNow > this.lastAbsPositionAmt + EPSILON) {
|
let classified: "filled" | "canceled" | "unknown" = "unknown";
|
||||||
|
if (prevIds && prevIds.size) {
|
||||||
|
for (const id of prevIds) {
|
||||||
|
const rec = allOrdersById.get(id);
|
||||||
|
if (!rec) continue;
|
||||||
|
const status = String(rec.status || "").toUpperCase();
|
||||||
|
const executed = Number(rec.executedQty);
|
||||||
|
if (status === "FILLED" || (executed > EPSILON && ["CANCELED", "CANCELLED", "EXPIRED", "REJECTED"].includes(status) === false)) {
|
||||||
|
classified = "filled";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (["CANCELED", "CANCELLED", "EXPIRED", "REJECTED"].includes(status) && executed <= EPSILON) {
|
||||||
|
classified = "canceled";
|
||||||
|
// do not break; keep searching for any filled among duplicates; but canceled is acceptable if no filled found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (classified === "unknown") {
|
||||||
|
// fallback to position delta confirmation
|
||||||
|
const absNow = Math.abs(this.position.positionAmt);
|
||||||
|
if (absNow > this.lastAbsPositionAmt + EPSILON) {
|
||||||
|
classified = "filled";
|
||||||
|
} else {
|
||||||
|
classified = "canceled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (classified === "filled") {
|
||||||
if (meta.side === "BUY") this.pendingLongLevels.add(meta.level);
|
if (meta.side === "BUY") this.pendingLongLevels.add(meta.level);
|
||||||
else this.pendingShortLevels.add(meta.level);
|
else this.pendingShortLevels.add(meta.level);
|
||||||
} else {
|
} else if (classified === "canceled") {
|
||||||
// Likely canceled or expired; do not mark pending
|
// no action; allow immediate re-open (do not block)
|
||||||
}
|
}
|
||||||
|
// ensure any previous block is cleared if exists
|
||||||
|
this.levelReopenBlockUntil.delete(meta.level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.lastOpenOrderKeys = currentKeys;
|
this.lastOpenOrderKeys = currentKeys;
|
||||||
// persist metadata for next tick to detect fills
|
// persist metadata for next tick to detect fills
|
||||||
this.lastKeyMeta = keyToMeta;
|
this.lastKeyMeta = keyToMeta;
|
||||||
|
this.lastOrderIdsByKey = currentOrderIdsByKey;
|
||||||
|
|
||||||
// Desired open orders according to locked sides
|
// Desired open orders according to locked sides
|
||||||
const desired: DesiredGridOrder[] = [];
|
const desired: DesiredGridOrder[] = [];
|
||||||
@@ -513,6 +558,7 @@ export class GridEngine {
|
|||||||
for (const level of this.buyLevelIndices) {
|
for (const level of this.buyLevelIndices) {
|
||||||
const levelPrice = this.gridLevels[level]!;
|
const levelPrice = this.gridLevels[level]!;
|
||||||
if (levelPrice >= price - halfTick) continue;
|
if (levelPrice >= price - halfTick) continue;
|
||||||
|
if (this.isLevelTemporarilyBlocked(level)) continue; // skip during cooldown
|
||||||
if (this.pendingLongLevels.has(level)) continue; // wait until close filled
|
if (this.pendingLongLevels.has(level)) continue; // wait until close filled
|
||||||
const key = this.getOrderKey("BUY", this.formatPrice(levelPrice), false);
|
const key = this.getOrderKey("BUY", this.formatPrice(levelPrice), false);
|
||||||
const targetMax = this.isCloseDesiredForSideAtLevel("BUY", level) ? 2 : 1;
|
const targetMax = this.isCloseDesiredForSideAtLevel("BUY", level) ? 2 : 1;
|
||||||
@@ -524,6 +570,7 @@ export class GridEngine {
|
|||||||
for (const level of this.sellLevelIndices) {
|
for (const level of this.sellLevelIndices) {
|
||||||
const levelPrice = this.gridLevels[level]!;
|
const levelPrice = this.gridLevels[level]!;
|
||||||
if (levelPrice <= price + halfTick) continue;
|
if (levelPrice <= price + halfTick) continue;
|
||||||
|
if (this.isLevelTemporarilyBlocked(level)) continue; // skip during cooldown
|
||||||
if (this.pendingShortLevels.has(level)) continue;
|
if (this.pendingShortLevels.has(level)) continue;
|
||||||
const key = this.getOrderKey("SELL", this.formatPrice(levelPrice), false);
|
const key = this.getOrderKey("SELL", this.formatPrice(levelPrice), false);
|
||||||
const targetMax = this.isCloseDesiredForSideAtLevel("SELL", level) ? 2 : 1;
|
const targetMax = this.isCloseDesiredForSideAtLevel("SELL", level) ? 2 : 1;
|
||||||
@@ -737,6 +784,21 @@ export class GridEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private blockLevelReopen(level: number): void {
|
||||||
|
const until = this.now() + GridEngine.DISAPPEAR_REOPEN_COOLDOWN_MS;
|
||||||
|
this.levelReopenBlockUntil.set(level, until);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLevelTemporarilyBlocked(level: number): boolean {
|
||||||
|
const until = this.levelReopenBlockUntil.get(level);
|
||||||
|
if (!Number.isFinite(until ?? NaN)) return false;
|
||||||
|
if (this.now() >= Number(until)) {
|
||||||
|
this.levelReopenBlockUntil.delete(level);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private buildLevelMeta(referencePrice?: number | null): void {
|
private buildLevelMeta(referencePrice?: number | null): void {
|
||||||
this.levelMeta.length = 0;
|
this.levelMeta.length = 0;
|
||||||
this.buyLevelIndices.length = 0;
|
this.buyLevelIndices.length = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user