mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
feat: 更新网格引擎,移除未使用的函数与临时阻塞逻辑,增强订单意图管理与出口数量限制以优化订单处理
This commit is contained in:
+63
-79
@@ -87,20 +87,18 @@ export class GridEngine {
|
|||||||
private readonly levelMeta: LevelMeta[] = [];
|
private readonly levelMeta: LevelMeta[] = [];
|
||||||
private readonly buyLevelIndices: number[] = [];
|
private readonly buyLevelIndices: number[] = [];
|
||||||
private readonly sellLevelIndices: number[] = [];
|
private readonly sellLevelIndices: number[] = [];
|
||||||
private lastOpenOrderKeys = new Set<string>();
|
|
||||||
private readonly pendingLongLevels = new Set<number>();
|
private readonly pendingLongLevels = new Set<number>();
|
||||||
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 }>();
|
|
||||||
private lastOrderIdsByKey = new Map<string, Set<string>>();
|
|
||||||
private prevActiveIds: Set<string> = new Set<string>();
|
private prevActiveIds: Set<string> = new Set<string>();
|
||||||
private orderIntentById = new Map<string, { side: "BUY" | "SELL"; price: string; level: number; intent: "ENTRY" | "EXIT" }>();
|
private orderIntentById = new Map<string, { side: "BUY" | "SELL"; price: string; level: number; intent: "ENTRY" | "EXIT"; sourceLevel?: number }>();
|
||||||
// When an order at a level disappears but account delta hasn't arrived yet,
|
// 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.
|
// 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;
|
|
||||||
// Track levels awaiting classification after disappearance until next account snapshot confirms
|
// Track levels awaiting classification after disappearance until next account snapshot confirms
|
||||||
private readonly awaitingClassification = new Map<number, { accountVerAtStart: number; absAtStart: number }>();
|
|
||||||
private sidesLocked = false;
|
private sidesLocked = false;
|
||||||
private startupCleaned = false;
|
private startupCleaned = false;
|
||||||
private initialCloseHandled = false;
|
private initialCloseHandled = false;
|
||||||
@@ -364,15 +362,7 @@ export class GridEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private hasActiveOrders(): boolean {
|
// removed unused hasActiveOrders
|
||||||
return this.openOrders.some((order) => {
|
|
||||||
if (order.symbol !== this.config.symbol) return false;
|
|
||||||
if (order.type && order.type !== "LIMIT") return false;
|
|
||||||
const status = typeof order.status === "string" ? order.status.toUpperCase() : null;
|
|
||||||
if (!status) return true;
|
|
||||||
return !["FILLED", "CANCELED", "CANCELLED", "REJECTED", "EXPIRED"].includes(status);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private deferPositionAlignment(): void {}
|
private deferPositionAlignment(): void {}
|
||||||
|
|
||||||
@@ -485,20 +475,24 @@ export class GridEngine {
|
|||||||
for (const id of disappeared) {
|
for (const id of disappeared) {
|
||||||
const meta = this.orderIntentById.get(id);
|
const meta = this.orderIntentById.get(id);
|
||||||
if (!meta) continue;
|
if (!meta) continue;
|
||||||
// Classify by position delta or final observable status
|
// Classify by final observable status or position delta (direction-aware)
|
||||||
let classified: "filled" | "canceled" | "unknown" = "unknown";
|
let classified: "filled" | "canceled" | "unknown" = "unknown";
|
||||||
const rec = allOrdersById.get(id);
|
const rec = allOrdersById.get(id);
|
||||||
if (rec) {
|
if (rec) {
|
||||||
const status = String(rec.status || "").toUpperCase();
|
const status = String(rec.status || "").toUpperCase();
|
||||||
const executed = Number(rec.executedQty);
|
const executed = Number(rec.executedQty || 0);
|
||||||
if (["CANCELED", "CANCELLED", "EXPIRED", "REJECTED"].includes(status) && executed <= EPSILON) {
|
if (status === "FILLED" || executed > EPSILON) {
|
||||||
|
classified = "filled";
|
||||||
|
} else if (["CANCELED", "CANCELLED", "EXPIRED", "REJECTED"].includes(status)) {
|
||||||
classified = "canceled";
|
classified = "canceled";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (classified === "unknown") {
|
if (classified === "unknown") {
|
||||||
const absNow = Math.abs(this.position.positionAmt);
|
const absNow = Math.abs(this.position.positionAmt);
|
||||||
if (absNow > this.lastAbsPositionAmt + EPSILON) classified = "filled";
|
const expectUp = meta.intent === "ENTRY"; // ENTRY should increase abs position; EXIT should decrease
|
||||||
else classified = "canceled"; // best-effort default if no exposure change
|
if (expectUp && absNow > this.lastAbsPositionAmt + EPSILON) classified = "filled";
|
||||||
|
else if (!expectUp && absNow + EPSILON < this.lastAbsPositionAmt) classified = "filled";
|
||||||
|
else classified = "canceled"; // conservative default
|
||||||
}
|
}
|
||||||
if (classified === "filled") {
|
if (classified === "filled") {
|
||||||
if (meta.intent === "ENTRY") {
|
if (meta.intent === "ENTRY") {
|
||||||
@@ -517,41 +511,23 @@ export class GridEngine {
|
|||||||
this.closeKeyBySourceLevel.set(meta.level, exitKey);
|
this.closeKeyBySourceLevel.set(meta.level, exitKey);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// EXIT filled
|
// EXIT filled -> clear using sourceLevel
|
||||||
this.pendingLongLevels.delete(meta.level);
|
const src = meta.sourceLevel ?? this.findSourceForCloseTarget(meta.level, meta.side);
|
||||||
this.pendingShortLevels.delete(meta.level);
|
this.pendingLongLevels.delete(src);
|
||||||
this.closeKeyBySourceLevel.delete(meta.level);
|
this.pendingShortLevels.delete(src);
|
||||||
|
this.closeKeyBySourceLevel.delete(src);
|
||||||
}
|
}
|
||||||
} else if (classified === "canceled") {
|
} else if (classified === "canceled") {
|
||||||
// allow re-open; clear temporary blocks
|
// if it was EXIT we also drop mapping by source
|
||||||
this.awaitingClassification.delete(meta.level);
|
if (meta.intent === "EXIT") {
|
||||||
this.levelReopenBlockUntil.delete(meta.level);
|
const src = meta.sourceLevel ?? this.findSourceForCloseTarget(meta.level, meta.side);
|
||||||
// if it was EXIT we also drop mapping
|
this.closeKeyBySourceLevel.delete(src);
|
||||||
if (meta.intent === "EXIT") this.closeKeyBySourceLevel.delete(meta.level);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update prevActiveIds for next tick
|
// Update prevActiveIds for next tick
|
||||||
this.prevActiveIds = currIds;
|
this.prevActiveIds = currIds;
|
||||||
|
|
||||||
// Resolve awaiting classifications only after a new account snapshot
|
|
||||||
if (this.awaitingClassification.size) {
|
|
||||||
for (const [level, info] of Array.from(this.awaitingClassification.entries())) {
|
|
||||||
const absNow = Math.abs(this.position.positionAmt);
|
|
||||||
if (this.accountVersion > info.accountVerAtStart && absNow > info.absAtStart + EPSILON) {
|
|
||||||
// treat as filled
|
|
||||||
const side = this.levelMeta[level]?.side === "BUY" ? "BUY" : "SELL";
|
|
||||||
if (side === "BUY") this.pendingLongLevels.add(level);
|
|
||||||
else this.pendingShortLevels.add(level);
|
|
||||||
this.awaitingClassification.delete(level);
|
|
||||||
this.levelReopenBlockUntil.delete(level);
|
|
||||||
} else if (this.accountVersion > info.accountVerAtStart && !(absNow > info.absAtStart + EPSILON)) {
|
|
||||||
// after new account snapshot without increased exposure -> canceled
|
|
||||||
this.awaitingClassification.delete(level);
|
|
||||||
this.levelReopenBlockUntil.delete(level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desired open orders according to locked sides
|
// Desired open orders according to locked sides
|
||||||
const desired: DesiredGridOrder[] = [];
|
const desired: DesiredGridOrder[] = [];
|
||||||
const desiredKeySet = new Set<string>();
|
const desiredKeySet = new Set<string>();
|
||||||
@@ -580,7 +556,6 @@ 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), "ENTRY");
|
const key = this.getOrderKey("BUY", this.formatPrice(levelPrice), "ENTRY");
|
||||||
if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue;
|
if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue;
|
||||||
@@ -595,7 +570,6 @@ 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), "ENTRY");
|
const key = this.getOrderKey("SELL", this.formatPrice(levelPrice), "ENTRY");
|
||||||
if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue;
|
if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue;
|
||||||
@@ -641,6 +615,14 @@ export class GridEngine {
|
|||||||
for (const d of desired) {
|
for (const d of desired) {
|
||||||
const isClose = d.intent === "EXIT" || (d.side === "SELL" && this.isTargetOfPendingLong(d.level)) || (d.side === "BUY" && this.isTargetOfPendingShort(d.level));
|
const isClose = d.intent === "EXIT" || (d.side === "SELL" && this.isTargetOfPendingLong(d.level)) || (d.side === "BUY" && this.isTargetOfPendingShort(d.level));
|
||||||
const intent: "ENTRY" | "EXIT" = isClose ? "EXIT" : "ENTRY";
|
const intent: "ENTRY" | "EXIT" = isClose ? "EXIT" : "ENTRY";
|
||||||
|
// Cap EXIT quantity to avoid over-closing and accidental flip
|
||||||
|
if (intent === "EXIT") {
|
||||||
|
const capped = this.capExitQty(d.amount, d.side);
|
||||||
|
if (capped <= EPSILON) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
d.amount = capped;
|
||||||
|
}
|
||||||
const key = this.getOrderKey(d.side, d.price, intent);
|
const key = this.getOrderKey(d.side, d.price, intent);
|
||||||
if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue;
|
if ((plannedKeyCounts.get(key) ?? 0) >= 1) continue;
|
||||||
try {
|
try {
|
||||||
@@ -662,7 +644,16 @@ export class GridEngine {
|
|||||||
if (placed) {
|
if (placed) {
|
||||||
plannedKeyCounts.set(key, (plannedKeyCounts.get(key) ?? 0) + 1);
|
plannedKeyCounts.set(key, (plannedKeyCounts.get(key) ?? 0) + 1);
|
||||||
if (placed.orderId != null) {
|
if (placed.orderId != null) {
|
||||||
this.orderIntentById.set(String(placed.orderId), { side: d.side, price: d.price, level: d.level, intent });
|
const record: { side: "BUY" | "SELL"; price: string; level: number; intent: "ENTRY" | "EXIT"; sourceLevel?: number } = {
|
||||||
|
side: d.side,
|
||||||
|
price: d.price,
|
||||||
|
level: d.level,
|
||||||
|
intent,
|
||||||
|
};
|
||||||
|
if (intent === "EXIT") {
|
||||||
|
record.sourceLevel = this.findSourceForCloseTarget(d.level, d.side);
|
||||||
|
}
|
||||||
|
this.orderIntentById.set(String(placed.orderId), record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (placed && isClose) {
|
if (placed && isClose) {
|
||||||
@@ -681,6 +672,25 @@ export class GridEngine {
|
|||||||
this.lastAbsPositionAmt = Math.abs(this.position.positionAmt);
|
this.lastAbsPositionAmt = Math.abs(this.position.positionAmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private capExitQty(desiredQty: number, side: "BUY" | "SELL"): number {
|
||||||
|
const absPos = Math.abs(this.position.positionAmt);
|
||||||
|
if (absPos <= EPSILON) return 0;
|
||||||
|
let pendingExitQty = 0;
|
||||||
|
for (const o of this.openOrders) {
|
||||||
|
if (o.symbol !== this.config.symbol) continue;
|
||||||
|
if (o.type !== "LIMIT") continue;
|
||||||
|
if (o.side !== side) continue; // same side as this EXIT order
|
||||||
|
const meta = this.orderIntentById.get(String(o.orderId));
|
||||||
|
if (!meta || meta.intent !== "EXIT") continue;
|
||||||
|
const orig = Number(o.origQty || 0);
|
||||||
|
const exec = Number(o.executedQty || 0);
|
||||||
|
const remaining = Math.max(orig - exec, 0);
|
||||||
|
pendingExitQty += remaining;
|
||||||
|
}
|
||||||
|
const remain = Math.max(absPos - pendingExitQty, 0);
|
||||||
|
return Math.min(desiredQty, remain);
|
||||||
|
}
|
||||||
|
|
||||||
private findSourceForCloseTarget(targetLevel: number, side: "BUY" | "SELL"): number {
|
private findSourceForCloseTarget(targetLevel: number, side: "BUY" | "SELL"): number {
|
||||||
// side here is reduce-only side at target level; source is opposite side level which maps to this target
|
// side here is reduce-only side at target level; source is opposite side level which maps to this target
|
||||||
if (side === "SELL") {
|
if (side === "SELL") {
|
||||||
@@ -714,17 +724,6 @@ export class GridEngine {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isCloseDesiredForSideAtLevel(side: "BUY" | "SELL", level: number): boolean {
|
|
||||||
if (side === "SELL") {
|
|
||||||
return this.isTargetOfPendingLong(level);
|
|
||||||
}
|
|
||||||
return this.isTargetOfPendingShort(level);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isTargetOfPendingShortOrLong(targetLevel: number): boolean {
|
|
||||||
return this.isTargetOfPendingLong(targetLevel) || this.isTargetOfPendingShort(targetLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeGridLevels(): number[] {
|
private computeGridLevels(): number[] {
|
||||||
if (!this.configValid) return [];
|
if (!this.configValid) return [];
|
||||||
const { lowerPrice, upperPrice, gridLevels } = this.config;
|
const { lowerPrice, upperPrice, gridLevels } = this.config;
|
||||||
@@ -824,21 +823,6 @@ 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;
|
||||||
@@ -958,7 +942,7 @@ export class GridEngine {
|
|||||||
const exitKey = this.getOrderKey(side, priceStr, "EXIT");
|
const exitKey = this.getOrderKey(side, priceStr, "EXIT");
|
||||||
this.closeKeyBySourceLevel.set(source, exitKey);
|
this.closeKeyBySourceLevel.set(source, exitKey);
|
||||||
if (placed.orderId != null) {
|
if (placed.orderId != null) {
|
||||||
this.orderIntentById.set(String(placed.orderId), { side, price: priceStr, level: nearest, intent: "EXIT" });
|
this.orderIntentById.set(String(placed.orderId), { side, price: priceStr, level: nearest, intent: "EXIT", sourceLevel: source });
|
||||||
}
|
}
|
||||||
this.log("order", `为已有仓位挂出一次性平仓单 ${side} @ ${priceStr}`);
|
this.log("order", `为已有仓位挂出一次性平仓单 ${side} @ ${priceStr}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user