mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
feat: 更新网格引擎,重构订单处理逻辑,移除减仓订单相关处理,增强平仓订单管理
This commit is contained in:
+46
-52
@@ -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)}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user