mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
更新 MakerEngine、OffsetMakerEngine 和 TrendEngine,重构订单同步逻辑,优化撤单和止损判断,新增订单计划生成和安全撤单功能,提升代码可读性和维护性。
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
import type { AsterOrder } from "../../exchanges/types";
|
||||||
|
|
||||||
|
export interface OrderTarget {
|
||||||
|
side: "BUY" | "SELL";
|
||||||
|
price: number;
|
||||||
|
amount: number;
|
||||||
|
reduceOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeOrderPlan(
|
||||||
|
openOrders: AsterOrder[],
|
||||||
|
targets: OrderTarget[],
|
||||||
|
tolerance: number
|
||||||
|
): { toCancel: AsterOrder[]; toPlace: OrderTarget[] } {
|
||||||
|
const unmatched = new Set(targets.map((_, idx) => idx));
|
||||||
|
const toCancel: AsterOrder[] = [];
|
||||||
|
|
||||||
|
for (const order of openOrders) {
|
||||||
|
const price = Number(order.price);
|
||||||
|
if (!Number.isFinite(price)) {
|
||||||
|
toCancel.push(order);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const reduceOnly = order.reduceOnly === true;
|
||||||
|
const matchedIndex = targets.findIndex((target, index) => {
|
||||||
|
return (
|
||||||
|
unmatched.has(index) &&
|
||||||
|
target.side === order.side &&
|
||||||
|
target.reduceOnly === reduceOnly &&
|
||||||
|
Math.abs(price - target.price) <= tolerance
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (matchedIndex >= 0) {
|
||||||
|
unmatched.delete(matchedIndex);
|
||||||
|
} else {
|
||||||
|
toCancel.push(order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toPlace = [...unmatched]
|
||||||
|
.map((idx) => targets[idx])
|
||||||
|
.filter((t): t is OrderTarget => t !== undefined && t.amount > 1e-5);
|
||||||
|
|
||||||
|
return { toCancel, toPlace };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ExchangeAdapter } from "../../exchanges/adapter";
|
||||||
|
import type { AsterOrder } from "../../exchanges/types";
|
||||||
|
import { isUnknownOrderError } from "../../utils/errors";
|
||||||
|
|
||||||
|
export async function safeCancelOrder(
|
||||||
|
exchange: ExchangeAdapter,
|
||||||
|
symbol: string,
|
||||||
|
order: AsterOrder,
|
||||||
|
onResolved: (orderId: number) => void,
|
||||||
|
onUnknown: () => void,
|
||||||
|
onError: (err: unknown) => void
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await exchange.cancelOrder({ symbol, orderId: order.orderId });
|
||||||
|
onResolved(order.orderId);
|
||||||
|
} catch (error) {
|
||||||
|
if (isUnknownOrderError(error)) onUnknown();
|
||||||
|
else onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
+47
-83
@@ -10,12 +10,17 @@ import { toPrice1Decimal } from "../utils/math";
|
|||||||
import { createTradeLog, type TradeLogEntry } from "../state/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../state/trade-log";
|
||||||
import { isUnknownOrderError } from "../utils/errors";
|
import { isUnknownOrderError } from "../utils/errors";
|
||||||
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
||||||
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
|
import { getTopPrices, getMidOrLast } from "../utils/price";
|
||||||
|
import { shouldStopLoss } from "../utils/risk";
|
||||||
import {
|
import {
|
||||||
marketClose,
|
marketClose,
|
||||||
placeOrder,
|
placeOrder,
|
||||||
unlockOperating,
|
unlockOperating,
|
||||||
} from "./order-coordinator";
|
} from "./order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
|
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
|
||||||
|
import { makeOrderPlan } from "./lib/order-plan";
|
||||||
|
import { safeCancelOrder } from "./lib/orders";
|
||||||
|
|
||||||
interface DesiredOrder {
|
interface DesiredOrder {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
@@ -221,17 +226,14 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const depth = this.depthSnapshot!;
|
const depth = this.depthSnapshot!;
|
||||||
const bidLevel = depth.bids?.[0];
|
const { topBid, topAsk } = getTopPrices(depth);
|
||||||
const askLevel = depth.asks?.[0];
|
if (topBid == null || topAsk == null) {
|
||||||
const topBid = bidLevel ? Number(bidLevel[0]) : undefined;
|
|
||||||
const topAsk = askLevel ? Number(askLevel[0]) : undefined;
|
|
||||||
if (!Number.isFinite(topBid) || !Number.isFinite(topAsk)) {
|
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bidPrice = toPrice1Decimal(topBid! - this.config.bidOffset);
|
const bidPrice = toPrice1Decimal(topBid - this.config.bidOffset);
|
||||||
const askPrice = toPrice1Decimal(topAsk! + this.config.askOffset);
|
const askPrice = toPrice1Decimal(topAsk + this.config.askOffset);
|
||||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||||
const absPosition = Math.abs(position.positionAmt);
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
const desired: DesiredOrder[] = [];
|
const desired: DesiredOrder[] = [];
|
||||||
@@ -290,52 +292,36 @@ export class MakerEngine {
|
|||||||
|
|
||||||
private async syncOrders(targets: DesiredOrder[]): Promise<void> {
|
private async syncOrders(targets: DesiredOrder[]): Promise<void> {
|
||||||
const tolerance = this.config.priceChaseThreshold;
|
const tolerance = this.config.priceChaseThreshold;
|
||||||
const unmatched = new Set(targets.map((_, idx) => idx));
|
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(o.orderId));
|
||||||
const toCancel: AsterOrder[] = [];
|
const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance);
|
||||||
|
|
||||||
for (const order of this.openOrders) {
|
|
||||||
const price = Number(order.price);
|
|
||||||
if (!Number.isFinite(price)) {
|
|
||||||
toCancel.push(order);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const reduceOnly = order.reduceOnly === true;
|
|
||||||
if (this.pendingCancelOrders.has(order.orderId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const matchedIndex = targets.findIndex((target, index) => {
|
|
||||||
if (!unmatched.has(index)) return false;
|
|
||||||
if (target.side !== order.side) return false;
|
|
||||||
if (target.reduceOnly !== reduceOnly) return false;
|
|
||||||
return Math.abs(price - target.price) <= tolerance;
|
|
||||||
});
|
|
||||||
if (matchedIndex >= 0) {
|
|
||||||
unmatched.delete(matchedIndex);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
toCancel.push(order);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const order of toCancel) {
|
for (const order of toCancel) {
|
||||||
|
if (this.pendingCancelOrders.has(order.orderId)) continue;
|
||||||
this.pendingCancelOrders.add(order.orderId);
|
this.pendingCancelOrders.add(order.orderId);
|
||||||
try {
|
await safeCancelOrder(
|
||||||
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
|
this.exchange,
|
||||||
this.tradeLog.push("order", `撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`);
|
this.config.symbol,
|
||||||
} catch (error) {
|
order,
|
||||||
if (isUnknownOrderError(error)) {
|
() => {
|
||||||
|
this.tradeLog.push(
|
||||||
|
"order",
|
||||||
|
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
|
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
} else {
|
},
|
||||||
|
(error) => {
|
||||||
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const index of unmatched) {
|
for (const target of toPlace) {
|
||||||
const target = targets[index];
|
|
||||||
if (!target) continue;
|
if (!target) continue;
|
||||||
if (target.amount < EPS) continue;
|
if (target.amount < EPS) continue;
|
||||||
try {
|
try {
|
||||||
@@ -376,20 +362,10 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
this.entryPricePendingLogged = false;
|
this.entryPricePendingLogged = false;
|
||||||
|
|
||||||
const pnl = position.positionAmt > 0
|
const pnl = computePositionPnl(position, bidPrice, askPrice);
|
||||||
? (bidPrice - position.entryPrice) * absPosition
|
const triggerStop = shouldStopLoss(position, bidPrice, askPrice, this.config.lossLimit);
|
||||||
: (position.entryPrice - askPrice) * absPosition;
|
|
||||||
const unrealized = Number.isFinite(position.unrealizedProfit)
|
|
||||||
? position.unrealizedProfit
|
|
||||||
: null;
|
|
||||||
const derivedLoss = pnl < -this.config.lossLimit;
|
|
||||||
const snapshotLoss = Boolean(
|
|
||||||
unrealized != null &&
|
|
||||||
unrealized < -this.config.lossLimit &&
|
|
||||||
pnl <= 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (derivedLoss || snapshotLoss) {
|
if (triggerStop) {
|
||||||
// 价格操纵保护:只有平仓方向价格与标记价格在阈值内才允许市价平仓
|
// 价格操纵保护:只有平仓方向价格与标记价格在阈值内才允许市价平仓
|
||||||
const closeSideIsSell = position.positionAmt > 0;
|
const closeSideIsSell = position.positionAmt > 0;
|
||||||
const closeSidePrice = closeSideIsSell ? bidPrice : askPrice;
|
const closeSidePrice = closeSideIsSell ? bidPrice : askPrice;
|
||||||
@@ -430,20 +406,24 @@ export class MakerEngine {
|
|||||||
for (const order of this.openOrders) {
|
for (const order of this.openOrders) {
|
||||||
if (this.pendingCancelOrders.has(order.orderId)) continue;
|
if (this.pendingCancelOrders.has(order.orderId)) continue;
|
||||||
this.pendingCancelOrders.add(order.orderId);
|
this.pendingCancelOrders.add(order.orderId);
|
||||||
try {
|
await safeCancelOrder(
|
||||||
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
|
this.exchange,
|
||||||
} catch (error) {
|
this.config.symbol,
|
||||||
if (isUnknownOrderError(error)) {
|
order,
|
||||||
|
() => {
|
||||||
|
// 成功撤销不记录日志,保持现有行为
|
||||||
|
},
|
||||||
|
() => {
|
||||||
this.tradeLog.push("order", "订单已不存在,撤销跳过");
|
this.tradeLog.push("order", "订单已不存在,撤销跳过");
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
} else {
|
},
|
||||||
|
(error) => {
|
||||||
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
// 与偏移做市保持一致:移除本地缓存中的异常订单,等待订单流推送重建
|
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,23 +447,15 @@ export class MakerEngine {
|
|||||||
|
|
||||||
private buildSnapshot(): MakerEngineSnapshot {
|
private buildSnapshot(): MakerEngineSnapshot {
|
||||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||||
const bid = this.depthSnapshot?.bids?.[0]?.[0];
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
const ask = this.depthSnapshot?.asks?.[0]?.[0];
|
const spread = topBid != null && topAsk != null ? topAsk - topBid : null;
|
||||||
const bidNum = Number(bid);
|
const pnl = computePositionPnl(position, topBid, topAsk);
|
||||||
const askNum = Number(ask);
|
|
||||||
const spread = Number.isFinite(bidNum) && Number.isFinite(askNum) ? askNum - bidNum : null;
|
|
||||||
const priceForPnl = position.positionAmt > 0 ? bidNum : askNum;
|
|
||||||
const pnl = Number.isFinite(priceForPnl)
|
|
||||||
? (position.positionAmt > 0
|
|
||||||
? (priceForPnl! - position.entryPrice) * Math.abs(position.positionAmt)
|
|
||||||
: (position.entryPrice - priceForPnl!) * Math.abs(position.positionAmt))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: this.isReady(),
|
ready: this.isReady(),
|
||||||
symbol: this.config.symbol,
|
symbol: this.config.symbol,
|
||||||
topBid: Number.isFinite(bidNum) ? bidNum : null,
|
topBid: topBid,
|
||||||
topAsk: Number.isFinite(askNum) ? askNum : null,
|
topAsk: topAsk,
|
||||||
spread,
|
spread,
|
||||||
position,
|
position,
|
||||||
pnl,
|
pnl,
|
||||||
@@ -515,14 +487,6 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getReferencePrice(): number | null {
|
private getReferencePrice(): number | null {
|
||||||
const bid = Number(this.depthSnapshot?.bids?.[0]?.[0]);
|
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
|
||||||
const ask = Number(this.depthSnapshot?.asks?.[0]?.[0]);
|
|
||||||
if (Number.isFinite(bid) && Number.isFinite(ask)) return (bid + ask) / 2;
|
|
||||||
if (this.tickerSnapshot) {
|
|
||||||
const last = Number(this.tickerSnapshot.lastPrice);
|
|
||||||
if (Number.isFinite(last)) return last;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-100
@@ -10,6 +10,10 @@ import { toPrice1Decimal } from "../utils/math";
|
|||||||
import { createTradeLog } from "../state/trade-log";
|
import { createTradeLog } from "../state/trade-log";
|
||||||
import { isUnknownOrderError } from "../utils/errors";
|
import { isUnknownOrderError } from "../utils/errors";
|
||||||
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
import { getPosition, type PositionSnapshot } from "../utils/strategy";
|
||||||
|
import { computeDepthStats } from "../utils/depth";
|
||||||
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
|
import { getTopPrices, getMidOrLast } from "../utils/price";
|
||||||
|
import { shouldStopLoss } from "../utils/risk";
|
||||||
import {
|
import {
|
||||||
marketClose,
|
marketClose,
|
||||||
placeOrder,
|
placeOrder,
|
||||||
@@ -17,6 +21,8 @@ import {
|
|||||||
} from "./order-coordinator";
|
} from "./order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
|
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator";
|
||||||
import type { MakerEngineSnapshot } from "./maker-engine";
|
import type { MakerEngineSnapshot } from "./maker-engine";
|
||||||
|
import { makeOrderPlan } from "./lib/order-plan";
|
||||||
|
import { safeCancelOrder } from "./lib/orders";
|
||||||
|
|
||||||
interface DesiredOrder {
|
interface DesiredOrder {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
@@ -219,11 +225,8 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const depth = this.depthSnapshot!;
|
const depth = this.depthSnapshot!;
|
||||||
const bidLevel = depth.bids?.[0];
|
const { topBid, topAsk } = getTopPrices(depth);
|
||||||
const askLevel = depth.asks?.[0];
|
if (topBid == null || topAsk == null) {
|
||||||
const topBid = bidLevel ? Number(bidLevel[0]) : undefined;
|
|
||||||
const topAsk = askLevel ? Number(askLevel[0]) : undefined;
|
|
||||||
if (!Number.isFinite(topBid) || !Number.isFinite(topAsk)) {
|
|
||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -310,27 +313,8 @@ export class OffsetMakerEngine {
|
|||||||
skipSellSide: boolean;
|
skipSellSide: boolean;
|
||||||
imbalance: "balanced" | "buy_dominant" | "sell_dominant";
|
imbalance: "balanced" | "buy_dominant" | "sell_dominant";
|
||||||
} {
|
} {
|
||||||
const topBids = (depth.bids ?? []).slice(0, 10);
|
// Keep existing behavior: 10 levels, ratio threshold 3x
|
||||||
const topAsks = (depth.asks ?? []).slice(0, 10);
|
return computeDepthStats(depth, 10, 3);
|
||||||
const buySum = topBids.reduce((total, [price, qty]) => {
|
|
||||||
const size = Number(qty);
|
|
||||||
return Number.isFinite(size) ? total + size : total;
|
|
||||||
}, 0);
|
|
||||||
const sellSum = topAsks.reduce((total, [price, qty]) => {
|
|
||||||
const size = Number(qty);
|
|
||||||
return Number.isFinite(size) ? total + size : total;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const skipSellSide = sellSum === 0 || sellSum * 3 < buySum;
|
|
||||||
const skipBuySide = buySum === 0 || buySum * 3 < sellSum;
|
|
||||||
let imbalance: "balanced" | "buy_dominant" | "sell_dominant" = "balanced";
|
|
||||||
if (buySum > sellSum * 3) {
|
|
||||||
imbalance = "buy_dominant";
|
|
||||||
} else if (sellSum > buySum * 3) {
|
|
||||||
imbalance = "sell_dominant";
|
|
||||||
}
|
|
||||||
|
|
||||||
return { buySum, sellSum, skipBuySide, skipSellSide, imbalance };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleImbalanceExit(
|
private async handleImbalanceExit(
|
||||||
@@ -384,53 +368,38 @@ export class OffsetMakerEngine {
|
|||||||
|
|
||||||
private async syncOrders(targets: DesiredOrder[]): Promise<void> {
|
private async syncOrders(targets: DesiredOrder[]): Promise<void> {
|
||||||
const tolerance = this.config.priceChaseThreshold;
|
const tolerance = this.config.priceChaseThreshold;
|
||||||
const unmatched = new Set(targets.map((_, idx) => idx));
|
const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(o.orderId));
|
||||||
const toCancel: AsterOrder[] = [];
|
const { toCancel, toPlace } = makeOrderPlan(availableOrders, targets, tolerance);
|
||||||
|
|
||||||
for (const order of this.openOrders) {
|
|
||||||
const price = Number(order.price);
|
|
||||||
if (!Number.isFinite(price)) {
|
|
||||||
toCancel.push(order);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const reduceOnly = order.reduceOnly === true;
|
|
||||||
if (this.pendingCancelOrders.has(order.orderId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const matchedIndex = targets.findIndex((target, index) => {
|
|
||||||
if (!unmatched.has(index)) return false;
|
|
||||||
if (target.side !== order.side) return false;
|
|
||||||
if (target.reduceOnly !== reduceOnly) return false;
|
|
||||||
return Math.abs(price - target.price) <= tolerance;
|
|
||||||
});
|
|
||||||
if (matchedIndex >= 0) {
|
|
||||||
unmatched.delete(matchedIndex);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
toCancel.push(order);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const order of toCancel) {
|
for (const order of toCancel) {
|
||||||
|
if (this.pendingCancelOrders.has(order.orderId)) continue;
|
||||||
this.pendingCancelOrders.add(order.orderId);
|
this.pendingCancelOrders.add(order.orderId);
|
||||||
try {
|
await safeCancelOrder(
|
||||||
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
|
this.exchange,
|
||||||
this.tradeLog.push("order", `撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`);
|
this.config.symbol,
|
||||||
} catch (error) {
|
order,
|
||||||
if (isUnknownOrderError(error)) {
|
() => {
|
||||||
|
this.tradeLog.push(
|
||||||
|
"order",
|
||||||
|
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
|
||||||
|
);
|
||||||
|
// 保持与原逻辑一致:成功撤销不立即修改本地 openOrders,等待订单流重建
|
||||||
|
},
|
||||||
|
() => {
|
||||||
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
|
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
} else {
|
},
|
||||||
|
(error) => {
|
||||||
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
// 避免同一轮内重复操作同一张已出错的本地挂单,直接从本地缓存移除,等待下一次订单推送重建
|
// 避免同一轮内重复操作同一张已出错的本地挂单,直接从本地缓存移除,等待下一次订单推送重建
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const index of unmatched) {
|
for (const target of toPlace) {
|
||||||
const target = targets[index];
|
|
||||||
if (!target) continue;
|
if (!target) continue;
|
||||||
if (target.amount < EPS) continue;
|
if (target.amount < EPS) continue;
|
||||||
try {
|
try {
|
||||||
@@ -471,20 +440,10 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
this.entryPricePendingLogged = false;
|
this.entryPricePendingLogged = false;
|
||||||
|
|
||||||
const pnl = position.positionAmt > 0
|
const pnl = computePositionPnl(position, bidPrice, askPrice);
|
||||||
? (bidPrice - position.entryPrice) * absPosition
|
const triggerStop = shouldStopLoss(position, bidPrice, askPrice, this.config.lossLimit);
|
||||||
: (position.entryPrice - askPrice) * absPosition;
|
|
||||||
const unrealized = Number.isFinite(position.unrealizedProfit)
|
|
||||||
? position.unrealizedProfit
|
|
||||||
: null;
|
|
||||||
const derivedLoss = pnl < -this.config.lossLimit;
|
|
||||||
const snapshotLoss = Boolean(
|
|
||||||
unrealized != null &&
|
|
||||||
unrealized < -this.config.lossLimit &&
|
|
||||||
pnl <= 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (derivedLoss || snapshotLoss) {
|
if (triggerStop) {
|
||||||
this.tradeLog.push(
|
this.tradeLog.push(
|
||||||
"stop",
|
"stop",
|
||||||
`触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT`
|
`触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT`
|
||||||
@@ -522,20 +481,25 @@ export class OffsetMakerEngine {
|
|||||||
for (const order of this.openOrders) {
|
for (const order of this.openOrders) {
|
||||||
if (this.pendingCancelOrders.has(order.orderId)) continue;
|
if (this.pendingCancelOrders.has(order.orderId)) continue;
|
||||||
this.pendingCancelOrders.add(order.orderId);
|
this.pendingCancelOrders.add(order.orderId);
|
||||||
try {
|
await safeCancelOrder(
|
||||||
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: order.orderId });
|
this.exchange,
|
||||||
} catch (error) {
|
this.config.symbol,
|
||||||
if (isUnknownOrderError(error)) {
|
order,
|
||||||
|
() => {
|
||||||
|
// 与原逻辑保持一致:成功撤销不记录日志且不修改本地 openOrders
|
||||||
|
},
|
||||||
|
() => {
|
||||||
this.tradeLog.push("order", "订单已不存在,撤销跳过");
|
this.tradeLog.push("order", "订单已不存在,撤销跳过");
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
} else {
|
},
|
||||||
|
(error) => {
|
||||||
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
|
||||||
this.pendingCancelOrders.delete(order.orderId);
|
this.pendingCancelOrders.delete(order.orderId);
|
||||||
// 与同步撤单路径保持一致,移除本地异常订单,等待订单流重建
|
// 与同步撤单路径保持一致,移除本地异常订单,等待订单流重建
|
||||||
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,23 +523,15 @@ export class OffsetMakerEngine {
|
|||||||
|
|
||||||
private buildSnapshot(): OffsetMakerEngineSnapshot {
|
private buildSnapshot(): OffsetMakerEngineSnapshot {
|
||||||
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
const position = getPosition(this.accountSnapshot, this.config.symbol);
|
||||||
const bid = this.depthSnapshot?.bids?.[0]?.[0];
|
const { topBid, topAsk } = getTopPrices(this.depthSnapshot);
|
||||||
const ask = this.depthSnapshot?.asks?.[0]?.[0];
|
const spread = topBid != null && topAsk != null ? topAsk - topBid : null;
|
||||||
const bidNum = Number(bid);
|
const pnl = computePositionPnl(position, topBid, topAsk);
|
||||||
const askNum = Number(ask);
|
|
||||||
const spread = Number.isFinite(bidNum) && Number.isFinite(askNum) ? askNum - bidNum : null;
|
|
||||||
const priceForPnl = position.positionAmt > 0 ? bidNum : askNum;
|
|
||||||
const pnl = Number.isFinite(priceForPnl)
|
|
||||||
? (position.positionAmt > 0
|
|
||||||
? (priceForPnl! - position.entryPrice) * Math.abs(position.positionAmt)
|
|
||||||
: (position.entryPrice - priceForPnl!) * Math.abs(position.positionAmt))
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready: this.isReady(),
|
ready: this.isReady(),
|
||||||
symbol: this.config.symbol,
|
symbol: this.config.symbol,
|
||||||
topBid: Number.isFinite(bidNum) ? bidNum : null,
|
topBid: topBid,
|
||||||
topAsk: Number.isFinite(askNum) ? askNum : null,
|
topAsk: topAsk,
|
||||||
spread,
|
spread,
|
||||||
position,
|
position,
|
||||||
pnl,
|
pnl,
|
||||||
@@ -612,13 +568,6 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getReferencePrice(): number | null {
|
private getReferencePrice(): number | null {
|
||||||
const bid = Number(this.depthSnapshot?.bids?.[0]?.[0]);
|
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot);
|
||||||
const ask = Number(this.depthSnapshot?.asks?.[0]?.[0]);
|
|
||||||
if (Number.isFinite(bid) && Number.isFinite(ask)) return (bid + ask) / 2;
|
|
||||||
if (this.tickerSnapshot) {
|
|
||||||
const last = Number(this.tickerSnapshot.lastPrice);
|
|
||||||
if (Number.isFinite(last)) return last;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
getSMA,
|
getSMA,
|
||||||
type PositionSnapshot,
|
type PositionSnapshot,
|
||||||
} from "../utils/strategy";
|
} from "../utils/strategy";
|
||||||
|
import { computePositionPnl } from "../utils/pnl";
|
||||||
|
import { getTopPrices, getMidOrLast } from "../utils/price";
|
||||||
import {
|
import {
|
||||||
marketClose,
|
marketClose,
|
||||||
placeMarketOrder,
|
placeMarketOrder,
|
||||||
@@ -601,11 +603,7 @@ export class TrendEngine {
|
|||||||
: price < sma30
|
: price < sma30
|
||||||
? "做空"
|
? "做空"
|
||||||
: "无信号";
|
: "无信号";
|
||||||
const pnl = price != null && position
|
const pnl = price != null ? computePositionPnl(position, price, price) : 0;
|
||||||
? (position.positionAmt > 0
|
|
||||||
? (price - position.entryPrice) * Math.abs(position.positionAmt)
|
|
||||||
: (position.entryPrice - price) * Math.abs(position.positionAmt))
|
|
||||||
: 0;
|
|
||||||
return {
|
return {
|
||||||
ready: this.isReady(),
|
ready: this.isReady(),
|
||||||
symbol: this.config.symbol,
|
symbol: this.config.symbol,
|
||||||
@@ -646,17 +644,7 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getReferencePrice(): number | null {
|
private getReferencePrice(): number | null {
|
||||||
if (this.tickerSnapshot) {
|
return getMidOrLast(this.depthSnapshot, this.tickerSnapshot) ?? (this.lastPrice != null && Number.isFinite(this.lastPrice) ? this.lastPrice : null);
|
||||||
const last = Number(this.tickerSnapshot.lastPrice);
|
|
||||||
if (Number.isFinite(last)) return last;
|
|
||||||
}
|
|
||||||
if (this.depthSnapshot) {
|
|
||||||
const bid = Number(this.depthSnapshot.bids?.[0]?.[0]);
|
|
||||||
const ask = Number(this.depthSnapshot.asks?.[0]?.[0]);
|
|
||||||
if (Number.isFinite(bid) && Number.isFinite(ask)) return (bid + ask) / 2;
|
|
||||||
}
|
|
||||||
if (this.lastPrice != null && Number.isFinite(this.lastPrice)) return this.lastPrice;
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { AsterDepth } from "../exchanges/types";
|
||||||
|
|
||||||
|
export type DepthImbalance = "balanced" | "buy_dominant" | "sell_dominant";
|
||||||
|
|
||||||
|
export function computeDepthStats(
|
||||||
|
depth: AsterDepth,
|
||||||
|
levels = 10,
|
||||||
|
ratio = 3
|
||||||
|
): {
|
||||||
|
buySum: number;
|
||||||
|
sellSum: number;
|
||||||
|
skipBuySide: boolean;
|
||||||
|
skipSellSide: boolean;
|
||||||
|
imbalance: DepthImbalance;
|
||||||
|
} {
|
||||||
|
const topBids = (depth.bids ?? []).slice(0, levels);
|
||||||
|
const topAsks = (depth.asks ?? []).slice(0, levels);
|
||||||
|
|
||||||
|
const buySum = topBids.reduce((total, level) => {
|
||||||
|
const qty = Number(level?.[1]);
|
||||||
|
return Number.isFinite(qty) ? total + qty : total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const sellSum = topAsks.reduce((total, level) => {
|
||||||
|
const qty = Number(level?.[1]);
|
||||||
|
return Number.isFinite(qty) ? total + qty : total;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const skipSellSide = sellSum === 0 || sellSum * ratio < buySum;
|
||||||
|
const skipBuySide = buySum === 0 || buySum * ratio < sellSum;
|
||||||
|
|
||||||
|
let imbalance: DepthImbalance = "balanced";
|
||||||
|
if (buySum > sellSum * ratio) {
|
||||||
|
imbalance = "buy_dominant";
|
||||||
|
} else if (sellSum > buySum * ratio) {
|
||||||
|
imbalance = "sell_dominant";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buySum, sellSum, skipBuySide, skipSellSide, imbalance };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { PositionSnapshot } from "./strategy";
|
||||||
|
|
||||||
|
export function computePositionPnl(
|
||||||
|
position: PositionSnapshot,
|
||||||
|
bestBid?: number | null,
|
||||||
|
bestAsk?: number | null
|
||||||
|
): number {
|
||||||
|
const priceForPnl = position.positionAmt > 0 ? bestBid : bestAsk;
|
||||||
|
if (!Number.isFinite(priceForPnl as number)) return 0;
|
||||||
|
const absAmt = Math.abs(position.positionAmt);
|
||||||
|
return position.positionAmt > 0
|
||||||
|
? ((priceForPnl as number) - position.entryPrice) * absAmt
|
||||||
|
: (position.entryPrice - (priceForPnl as number)) * absAmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { AsterDepth, AsterTicker } from "../exchanges/types";
|
||||||
|
|
||||||
|
export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null; topAsk: number | null } {
|
||||||
|
const bid = Number(depth?.bids?.[0]?.[0]);
|
||||||
|
const ask = Number(depth?.asks?.[0]?.[0]);
|
||||||
|
return {
|
||||||
|
topBid: Number.isFinite(bid) ? bid : null,
|
||||||
|
topAsk: Number.isFinite(ask) ? ask : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | null): number | null {
|
||||||
|
const { topBid, topAsk } = getTopPrices(depth);
|
||||||
|
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
|
||||||
|
const last = Number(ticker?.lastPrice);
|
||||||
|
return Number.isFinite(last) ? last : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { PositionSnapshot } from "./strategy";
|
||||||
|
|
||||||
|
export function shouldStopLoss(
|
||||||
|
position: PositionSnapshot,
|
||||||
|
bestBid: number,
|
||||||
|
bestAsk: number,
|
||||||
|
lossLimit: number
|
||||||
|
): boolean {
|
||||||
|
const absPosition = Math.abs(position.positionAmt);
|
||||||
|
if (absPosition < 1e-5) return false;
|
||||||
|
|
||||||
|
const pnl = position.positionAmt > 0
|
||||||
|
? (bestBid - position.entryPrice) * absPosition
|
||||||
|
: (position.entryPrice - bestAsk) * absPosition;
|
||||||
|
|
||||||
|
const unrealized = Number.isFinite(position.unrealizedProfit)
|
||||||
|
? (position.unrealizedProfit as number)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const derivedLoss = pnl < -lossLimit;
|
||||||
|
const snapshotLoss = Boolean(unrealized != null && unrealized < -lossLimit && pnl <= 0);
|
||||||
|
|
||||||
|
return derivedLoss || snapshotLoss;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user