feat: 更新网格引擎,重构订单处理逻辑,移除减仓订单相关处理,增强平仓订单管理

This commit is contained in:
discountry
2025-10-07 22:15:47 +08:00
parent 39a97110dd
commit e524c225f2
+46 -52
View File
@@ -94,7 +94,7 @@ export class GridEngine {
private lastOpenOrderKeys = new Set<string>(); 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 reduceOnlyKeyBySourceLevel = 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 sidesLocked = false; private sidesLocked = false;
private startupCleaned = false; private startupCleaned = false;
@@ -472,45 +472,28 @@ export class GridEngine {
for (const prevKey of prevKeys) { for (const prevKey of prevKeys) {
if (currentKeys.has(prevKey)) continue; if (currentKeys.has(prevKey)) continue;
const meta = prevMeta.get(prevKey); const meta = prevMeta.get(prevKey);
// If we can't resolve from current snapshot, try to decode by stored mapping // First, check if this missing key corresponds to a tracked close order
if (!meta) { let handledByCloseTracking = false;
// For reduce-only disappearance, check mapping for (const [source, closeKey] of this.closeKeyBySourceLevel.entries()) {
for (const [source, roKey] of this.reduceOnlyKeyBySourceLevel.entries()) { if (closeKey === prevKey) {
if (roKey === prevKey) { handledByCloseTracking = true;
// Determine if disappearance was a fill or a manual cancel via position change const absNow = Math.abs(this.position.positionAmt);
const absNow = Math.abs(this.position.positionAmt); if (absNow + EPSILON < this.lastAbsPositionAmt) {
if (absNow + EPSILON < this.lastAbsPositionAmt) { // filled: clear pending mapping
// treated as filled: clear pending mapping this.pendingLongLevels.delete(source);
this.pendingLongLevels.delete(source); this.pendingShortLevels.delete(source);
this.pendingShortLevels.delete(source); this.closeKeyBySourceLevel.delete(source);
this.reduceOnlyKeyBySourceLevel.delete(source); } else {
} else { // canceled: keep pending mapping so desired will re-place close order
// treated as canceled: keep pending so we re-arm close order
}
break;
} }
break;
} }
continue;
} }
if (!meta.reduceOnly) { if (handledByCloseTracking) continue;
// Otherwise, treat as an open order disappearance and mark pending
if (meta) {
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 {
// reduce-only disappeared: decide filled vs canceled by observing position delta
for (const [source, roKey] of this.reduceOnlyKeyBySourceLevel.entries()) {
if (roKey === prevKey) {
const absNow = Math.abs(this.position.positionAmt);
if (absNow + EPSILON < this.lastAbsPositionAmt) {
// filled: clear pending mapping
this.pendingLongLevels.delete(source);
this.pendingShortLevels.delete(source);
this.reduceOnlyKeyBySourceLevel.delete(source);
} else {
// canceled: keep pending mapping so desired will re-place close order
}
break;
}
}
} }
} }
this.lastOpenOrderKeys = currentKeys; this.lastOpenOrderKeys = currentKeys;
@@ -544,31 +527,29 @@ export class GridEngine {
desired.push({ level, side: "SELL", price: this.formatPrice(levelPrice), amount: this.config.orderSize, reduceOnly: false }); desired.push({ level, side: "SELL", price: this.formatPrice(levelPrice), amount: this.config.orderSize, reduceOnly: false });
} }
// reduce-only close orders for pending levels // close orders for pending levels (now non-reduce-only)
for (const source of this.pendingLongLevels) { for (const source of this.pendingLongLevels) {
const target = this.levelMeta[source]?.closeTarget; const target = this.levelMeta[source]?.closeTarget;
if (target == null) continue; if (target == null) continue;
const priceStr = this.formatPrice(this.gridLevels[target]!); const priceStr = this.formatPrice(this.gridLevels[target]!);
const roKey = this.getOrderKey("SELL", priceStr, true); // always desire a close order; allow placement even if an identical open exists
if (!activeKeySet.has(roKey)) { desired.push({ level: target, side: "SELL", price: priceStr, amount: this.config.orderSize, reduceOnly: false });
desired.push({ level: target, side: "SELL", price: priceStr, amount: this.config.orderSize, reduceOnly: true });
}
} }
for (const source of this.pendingShortLevels) { for (const source of this.pendingShortLevels) {
const target = this.levelMeta[source]?.closeTarget; const target = this.levelMeta[source]?.closeTarget;
if (target == null) continue; if (target == null) continue;
const priceStr = this.formatPrice(this.gridLevels[target]!); const priceStr = this.formatPrice(this.gridLevels[target]!);
const roKey = this.getOrderKey("BUY", priceStr, true); desired.push({ level: target, side: "BUY", price: priceStr, amount: this.config.orderSize, reduceOnly: false });
if (!activeKeySet.has(roKey)) {
desired.push({ level: target, side: "BUY", price: priceStr, amount: this.config.orderSize, reduceOnly: true });
}
} }
// Place desired orders // Place desired orders
this.desiredOrders = desired; this.desiredOrders = desired;
for (const d of desired) { for (const d of desired) {
const key = this.getOrderKey(d.side, d.price, d.reduceOnly); const key = this.getOrderKey(d.side, d.price, d.reduceOnly);
if (activeKeySet.has(key)) continue; const isCloseDesired =
(d.side === "SELL" && this.isTargetOfPendingLong(d.level)) ||
(d.side === "BUY" && this.isTargetOfPendingShort(d.level));
if (activeKeySet.has(key) && !isCloseDesired) continue;
try { try {
const placed = await placeOrder( const placed = await placeOrder(
this.exchange, this.exchange,
@@ -581,13 +562,12 @@ export class GridEngine {
d.price, d.price,
d.amount, d.amount,
this.log, this.log,
d.reduceOnly, false,
undefined, undefined,
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep, skipDedupe: true } { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep, skipDedupe: true }
); );
if (placed && d.reduceOnly) { if (placed && isCloseDesired) {
this.reduceOnlyKeyBySourceLevel.set( this.closeKeyBySourceLevel.set(
// map by source: find the source whose target equals d.level
this.findSourceForCloseTarget(d.level, d.side), this.findSourceForCloseTarget(d.level, d.side),
key key
); );
@@ -621,6 +601,20 @@ export class GridEngine {
return targetLevel; // fallback return targetLevel; // fallback
} }
private isTargetOfPendingLong(targetLevel: number): boolean {
for (const source of this.pendingLongLevels) {
if (this.levelMeta[source]?.closeTarget === targetLevel) return true;
}
return false;
}
private isTargetOfPendingShort(targetLevel: number): boolean {
for (const source of this.pendingShortLevels) {
if (this.levelMeta[source]?.closeTarget === targetLevel) return true;
}
return false;
}
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,7 +818,7 @@ export class GridEngine {
priceStr, priceStr,
Math.abs(qty), Math.abs(qty),
this.log, this.log,
true, false,
undefined, undefined,
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep, skipDedupe: true } { priceTick: this.config.priceTick, qtyStep: this.config.qtyStep, skipDedupe: true }
); );
@@ -833,8 +827,8 @@ export class GridEngine {
const source = this.findSourceForInitialPosition(side); const source = this.findSourceForInitialPosition(side);
if (side === "SELL") this.pendingLongLevels.add(source); if (side === "SELL") this.pendingLongLevels.add(source);
else this.pendingShortLevels.add(source); else this.pendingShortLevels.add(source);
this.reduceOnlyKeyBySourceLevel.set(source, this.getOrderKey(side, priceStr, true)); this.closeKeyBySourceLevel.set(source, this.getOrderKey(side, priceStr, false));
this.log("order", `为已有仓位挂出一次性仓单 ${side} @ ${priceStr}`); this.log("order", `为已有仓位挂出一次性仓单 ${side} @ ${priceStr}`);
} }
} catch (error) { } catch (error) {
this.log("error", `启动阶段挂减仓单失败: ${extractMessage(error)}`); this.log("error", `启动阶段挂减仓单失败: ${extractMessage(error)}`);