import type { MakerPointsConfig } from "../config"; import type { ExchangeAdapter } from "../exchanges/adapter"; import type { AsterAccountSnapshot, AsterDepth, AsterOrder, AsterTicker, } from "../exchanges/types"; import { formatPriceToString } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { extractMessage, isInsufficientBalanceError, isPrecisionError, isRateLimitError, isUnknownOrderError } from "../utils/errors"; import { isOrderActiveStatus } from "../utils/order-status"; import { getPosition, parseSymbolParts, validateAccountSnapshotForSymbol } from "../utils/strategy"; import type { PositionSnapshot } from "../utils/strategy"; import { computePositionPnl, computeStopLossPnl } from "../utils/pnl"; import { getDepthBetweenPrices, getMidOrLast, getTopPrices } from "../utils/price"; import { marketClose, placeOrder, unlockOperating, } from "../core/order-coordinator"; import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator"; import { makeOrderPlan } from "../core/lib/order-plan"; import { safeCancelOrder } from "../core/lib/orders"; import { RateLimitController } from "../core/lib/rate-limit"; import { StrategyEventEmitter } from "./common/event-emitter"; import { safeSubscribe, type LogHandler } from "./common/subscriptions"; import { SessionVolumeTracker } from "./common/session-volume"; import { BinanceDepthTracker, type BinanceDepthSnapshot } from "./common/binance-depth"; import { buildBpsTargets } from "./maker-points-logic"; import { t } from "../i18n"; import { checkStandxTokenExpiry, formatTokenExpiryMessage, isTokenExpiryConfigured, type TokenExpiryState, } from "../utils/standx-token-expiry"; import { createTelegramNotifier, type NotificationSender, type TradeNotification, } from "../notifications"; interface DesiredOrder { side: "BUY" | "SELL"; price: string; amount: number; reduceOnly: boolean; } export interface MakerPointsSnapshot { ready: boolean; symbol: string; topBid: number | null; topAsk: number | null; spread: number | null; priceDecimals: number; position: PositionSnapshot; pnl: number; accountUnrealized: number; sessionVolume: number; openOrders: AsterOrder[]; desiredOrders: DesiredOrder[]; tradeLog: TradeLogEntry[]; lastUpdated: number | null; feedStatus: { account: boolean; orders: boolean; depth: boolean; ticker: boolean; binance: boolean; }; binanceDepth: BinanceDepthSnapshot | null; bandDepths: Array<{ band: "0-10" | "10-30" | "30-100"; bps: number; buyDepth: number | null; sellDepth: number | null; enabled: boolean; }>; quoteStatus: { closeOnly: boolean; skipBuy: boolean; skipSell: boolean; }; } type MakerPointsEvent = "update"; type MakerPointsListener = (snapshot: MakerPointsSnapshot) => void; const EPS = 1e-5; const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000; const STOP_LOSS_COOLDOWN_MS = 5_000; const STOP_LOSS_CHECK_INTERVAL_MS = 250; // 止损检查最大间隔 const STOP_LOSS_RETRY_INTERVAL_MS = 500; // 止损失败后重试间隔 const DATA_STALE_THRESHOLD_MS = 5_000; // 数据过时阈值(5秒) const DEFENSE_MODE_CHECK_INTERVAL_MS = 1000; // 防御模式检查间隔 const ACCOUNT_DATA_STALE_THRESHOLD_MS = 20_000; // 账户数据长期无更新阈值(会先尝试通过 REST 补拉验证,不直接进入防御模式) const STANDX_REST_ERROR_DEFENSE_THRESHOLD = 3; const STANDX_MARGIN_MODE_CHECK_INTERVAL_MS = 500; const STANDX_MARGIN_MODE_MAX_ATTEMPTS = 10; const ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS = 5_000; export class MakerPointsEngine { private accountSnapshot: AsterAccountSnapshot | null = null; private depthSnapshot: AsterDepth | null = null; private tickerSnapshot: AsterTicker | null = null; private openOrders: AsterOrder[] = []; private readonly locks: OrderLockMap = {}; private readonly timers: OrderTimerMap = {}; private readonly pending: OrderPendingMap = {}; private readonly pendingCancelOrders = new Set(); private readonly tradeLog: ReturnType; private readonly events = new StrategyEventEmitter(); private readonly sessionVolume = new SessionVolumeTracker(); private readonly rateLimit: RateLimitController; private readonly binanceDepth: BinanceDepthTracker; private readonly notifier: NotificationSender; private priceTick: number = 0.1; private qtyStep: number = 0.001; private precisionSync: Promise | null = null; private timer: ReturnType | null = null; private stopLossTimer: ReturnType | null = null; private processing = false; private stopLossProcessing = false; private stopLossCooldownUntil = 0; private desiredOrders: DesiredOrder[] = []; private accountUnrealized = 0; private initialOrderSnapshotReady = false; private initialOrderResetDone = false; private lastDesiredSummary: string | null = null; private lastCloseOnly = false; private lastSkipBuy = false; private lastSkipSell = false; private lastQuoteBid1: number | null = null; private lastQuoteAsk1: number | null = null; // 跟踪各档位深度是否足够的状态 (按 bps 值索引) private lastDepthOkStatus: Record = {}; private readinessLogged = { account: false, depth: false, ticker: false, orders: false, }; private feedStatus = { account: false, depth: false, ticker: false, orders: false, binance: false, }; private insufficientBalanceCooldownUntil = 0; private insufficientBalanceNotified = false; private lastInsufficientMessage: string | null = null; private tokenExpiryState: TokenExpiryState = "active"; private tokenExpiryLogged = false; private tokenExpiryCancelDone = false; private tokenExpiredCloseOnlyMode = false; private tokenExpiryNotified = false; private lastPositionAmt = 0; private lastPositionSide: "LONG" | "SHORT" | "FLAT" = "FLAT"; // 连接保护相关状态(用于断连/重连事件处理) private _standxConnectionState: "connected" | "disconnected" = "connected"; private reconnectResetPending = false; private lastRepriceQueryTime = 0; private readonly repriceQueryIntervalMs = 3000; // 最小查询间隔 // ========== 数据过时防御模式 ========== // 各数据源最后更新时间 private lastStandxDepthTime = 0; private lastStandxAccountTime = 0; private lastBinanceDepthTime = 0; private accountStaleRestProbeInFlight: Promise | null = null; private accountStaleRestProbeLastAttempt = 0; private accountStaleRestProbeConsecutiveFailures = 0; // 防御模式状态 private defenseMode = false; private defenseModeNotified = false; private defenseModeTimer: ReturnType | null = null; // 防御模式下的 REST 轮询定时器 private defenseRestPollTimer: ReturnType | null = null; private defenseRestPollActive = false; private standxRestConsecutiveErrors = 0; private standxRestUnhealthy = false; private standxRestLastError: string | null = null; private marginModeEnsuring: Promise | null = null; constructor(private readonly config: MakerPointsConfig, private readonly exchange: ExchangeAdapter) { this.tradeLog = createTradeLog(this.config.maxLogEntries); this.rateLimit = new RateLimitController(this.config.refreshIntervalMs, (type, detail) => this.tradeLog.push(type, detail) ); this.notifier = createTelegramNotifier(); this.priceTick = Math.max(1e-9, this.config.priceTick); this.qtyStep = Math.max(1e-9, this.config.qtyStep); this.binanceDepth = new BinanceDepthTracker(resolveBinanceSymbol(this.config.symbol), { baseUrl: process.env.BINANCE_SPOT_WS_URL ?? process.env.BINANCE_WS_URL, levels: 20, ratio: 9, speedMs: 100, logger: (context, error) => { this.tradeLog.push("warn", `Binance ${context} 异常: ${extractMessage(error)}`); }, }); this.binanceDepth.onUpdate(() => { this.feedStatus.binance = true; this.lastBinanceDepthTime = Date.now(); this.emitUpdate(); }); // 监听 Binance 连接状态变化 this.binanceDepth.onConnectionChange((state) => { if (state === "disconnected") { this.feedStatus.binance = false; this.tradeLog.push("warn", "Binance 深度连接断开"); } else if (state === "stale") { this.tradeLog.push("warn", "Binance 深度数据过时"); } else if (state === "connected") { this.feedStatus.binance = true; this.tradeLog.push("info", "Binance 深度连接恢复"); } this.emitUpdate(); }); this.syncPrecision(); this.bootstrap(); } start(): void { if (this.timer) return; // 初始化数据时间戳 const now = Date.now(); this.lastStandxDepthTime = now; this.lastStandxAccountTime = now; this.lastBinanceDepthTime = now; this.timer = setInterval(() => { void this.tick(); }, this.config.refreshIntervalMs); if (!this.stopLossTimer) { this.stopLossTimer = setInterval(() => { void this.checkStopLoss(); }, Math.min(STOP_LOSS_CHECK_INTERVAL_MS, this.config.refreshIntervalMs)); } // 启动防御模式检测定时器 if (!this.defenseModeTimer) { this.defenseModeTimer = setInterval(() => { this.checkDataStaleAndDefense(); }, DEFENSE_MODE_CHECK_INTERVAL_MS); } this.binanceDepth.start(); } stop(): void { if (this.timer) { clearInterval(this.timer); this.timer = null; } if (this.stopLossTimer) { clearInterval(this.stopLossTimer); this.stopLossTimer = null; } if (this.defenseModeTimer) { clearInterval(this.defenseModeTimer); this.defenseModeTimer = null; } this.stopDefenseRestPoll(); this.binanceDepth.stop(); } on(event: MakerPointsEvent, handler: MakerPointsListener): void { this.events.on(event, handler); } off(event: MakerPointsEvent, handler: MakerPointsListener): void { this.events.off(event, handler); } getSnapshot(): MakerPointsSnapshot { return this.buildSnapshot(); } private bootstrap(): void { const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail); this.setupRestHealthProtection(); safeSubscribe( this.exchange.watchAccount.bind(this.exchange), (snapshot) => { this.applyAccountSnapshot(snapshot); }, log, { subscribeFail: (error) => t("log.subscribe.accountFail", { error: String(error) }), processFail: (error) => t("log.process.accountError", { error: String(error) }), } ); safeSubscribe( this.exchange.watchOrders.bind(this.exchange), (orders) => { this.syncLocksWithOrders(orders); this.openOrders = Array.isArray(orders) ? orders.filter( (order) => order.type !== "MARKET" && order.symbol === this.config.symbol && isOrderActiveStatus(order.status) ) : []; const currentIds = new Set(this.openOrders.map((order) => String(order.orderId))); for (const id of Array.from(this.pendingCancelOrders)) { if (!currentIds.has(id)) { this.pendingCancelOrders.delete(id); } } this.initialOrderSnapshotReady = true; this.feedStatus.orders = true; this.emitUpdate(); }, log, { subscribeFail: (error) => t("log.subscribe.orderFail", { error: String(error) }), processFail: (error) => t("log.process.orderError", { error: String(error) }), } ); safeSubscribe( this.exchange.watchDepth.bind(this.exchange, this.config.symbol), (depth) => { this.depthSnapshot = depth; this.lastStandxDepthTime = Date.now(); this.feedStatus.depth = true; this.emitUpdate(); }, log, { subscribeFail: (error) => t("log.subscribe.depthFail", { error: String(error) }), processFail: (error) => t("log.process.depthError", { error: String(error) }), } ); safeSubscribe( this.exchange.watchTicker.bind(this.exchange, this.config.symbol), (ticker) => { this.tickerSnapshot = ticker; this.feedStatus.ticker = true; this.emitUpdate(); }, log, { subscribeFail: (error) => t("log.subscribe.tickerFail", { error: String(error) }), processFail: (error) => t("log.process.tickerError", { error: String(error) }), } ); // 注册连接事件监听(如果交易所支持) this.setupConnectionProtection(); } private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void { this.accountSnapshot = snapshot; // StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime this.lastStandxAccountTime = this.exchange.id === "standx" && Number.isFinite(snapshot.updateTime) && snapshot.updateTime > 0 ? snapshot.updateTime : Date.now(); this.accountStaleRestProbeConsecutiveFailures = 0; const totalUnrealized = Number(snapshot.totalUnrealizedProfit ?? "0"); if (Number.isFinite(totalUnrealized)) { this.accountUnrealized = totalUnrealized; } const position = getPosition(snapshot, this.config.symbol); this.sessionVolume.update(position, this.getReferencePrice()); this.detectPositionChange(position); this.feedStatus.account = true; this.emitUpdate(); } private maybeProbeStandxAccountSnapshot(now: number): void { if (this.exchange.id !== "standx") return; if (!this.exchange.queryAccountSnapshot) return; if (this.defenseMode) return; if (this.accountStaleRestProbeInFlight) return; if (this.accountStaleRestProbeLastAttempt > 0 && now - this.accountStaleRestProbeLastAttempt < ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS) { return; } this.accountStaleRestProbeLastAttempt = now; this.accountStaleRestProbeInFlight = (async () => { try { const next = await this.exchange.queryAccountSnapshot?.(); if (next) { this.applyAccountSnapshot(next); } else { this.accountStaleRestProbeConsecutiveFailures += 1; } } catch { this.accountStaleRestProbeConsecutiveFailures += 1; } finally { this.accountStaleRestProbeInFlight = null; } })(); } /** * 设置连接保护机制 * 监听断连/重连事件,实现保护逻辑 */ private setupConnectionProtection(): void { if (!this.exchange.onConnectionEvent) return; this.exchange.onConnectionEvent((event, symbol) => { if (event === "disconnected") { this.handleDisconnect(symbol); } else if (event === "reconnected") { this.handleReconnect(symbol); } }); } private setupRestHealthProtection(): void { if (!this.exchange.onRestHealthEvent) return; this.exchange.onRestHealthEvent((state, info) => { if (state === "unhealthy") { this.standxRestUnhealthy = true; this.standxRestConsecutiveErrors = Math.max(this.standxRestConsecutiveErrors, info.consecutiveErrors); this.standxRestLastError = info.error ?? this.standxRestLastError; if (!this.defenseMode && this.standxRestConsecutiveErrors >= STANDX_REST_ERROR_DEFENSE_THRESHOLD) { this.enterDefenseMode({ standxDepthStale: false, binanceStale: false, standxAccountStale: false, accountInvalid: false, standxRestUnhealthy: true, standxRestConsecutiveErrors: this.standxRestConsecutiveErrors, standxRestLastError: this.standxRestLastError, marginModeNotIsolated: false, marginMode: this.getStandxMarginMode(this.accountSnapshot), standxDepthAge: 0, binanceAge: 0, standxAccountAge: 0, accountIssues: [], }); } } else if (state === "healthy") { this.standxRestUnhealthy = false; this.standxRestConsecutiveErrors = 0; this.standxRestLastError = null; } }); } /** * 处理断连事件 */ private handleDisconnect(symbol: string): void { this._standxConnectionState = "disconnected"; this.tradeLog.push("warn", `WebSocket 断连 (${symbol}),启动断连保护`); this.notify({ type: "token_expired", level: "warn", symbol: this.config.symbol, title: "连接断开", message: "WebSocket 断连,正在尝试取消所有挂单", details: { symbol }, }); } /** * 处理重连事件 * 重连后需要重新查询挂单并取消所有挂单 */ private async handleReconnect(symbol: string): Promise { this._standxConnectionState = "connected"; this.reconnectResetPending = true; this.tradeLog.push("info", `WebSocket 重连成功 (${symbol}),开始重连保护流程`); try { // 查询真实挂单状态 if (this.exchange.queryOpenOrders) { const realOrders = await this.exchange.queryOpenOrders(); this.tradeLog.push("info", `重连后查询到 ${realOrders.length} 个挂单`); if (realOrders.length > 0) { // 取消所有挂单 if (this.exchange.forceCancelAllOrders) { const success = await this.exchange.forceCancelAllOrders(); if (success) { this.tradeLog.push("order", "重连保护:已取消所有挂单"); } else { this.tradeLog.push("warn", "重连保护:取消挂单未完全成功,将在下次循环重试"); } } else { await this.exchange.cancelAllOrders({ symbol: this.config.symbol }); this.tradeLog.push("order", "重连保护:已取消所有挂单"); } } } // 重置本地挂单状态 this.openOrders = []; this.pendingCancelOrders.clear(); unlockOperating(this.locks, this.timers, this.pending, "LIMIT"); // 重置 reprice 基准,强制下一次重新计算 this.lastQuoteBid1 = null; this.lastQuoteAsk1 = null; this.desiredOrders = []; this.lastDesiredSummary = null; // 标记启动重置需要重新执行 this.initialOrderResetDone = false; this.notify({ type: "position_opened", level: "info", symbol: this.config.symbol, title: "重连完成", message: "WebSocket 重连成功,已清理挂单状态", details: { symbol }, }); } catch (error) { this.tradeLog.push("error", `重连保护流程失败: ${extractMessage(error)}`); } finally { this.reconnectResetPending = false; } } private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void { const list = Array.isArray(orders) ? orders : []; Object.keys(this.pending).forEach((type) => { const pendingId = this.pending[type]; if (!pendingId) return; const match = list.find((order) => String(order.orderId) === pendingId); if (!match || (match.status && match.status !== "NEW" && match.status !== "PARTIALLY_FILLED")) { unlockOperating(this.locks, this.timers, this.pending, type); } }); } private isReady(): boolean { return Boolean( this.feedStatus.account && this.feedStatus.depth && this.feedStatus.ticker && this.feedStatus.orders ); } private async tick(): Promise { if (this.processing) return; // 重连处理期间不执行主循环,避免状态竞争 if (this.reconnectResetPending) return; // 止损执行期间不执行主循环,避免订单冲突 if (this.stopLossProcessing) return; // 防御模式下不执行正常挂单逻辑 if (this.defenseMode) return; this.processing = true; let hadRateLimit = false; try { const decision = this.rateLimit.beforeCycle(); if (decision === "paused") { this.emitUpdate(); return; } if (decision === "skip") { return; } if (!this.isReady()) { this.logReadinessBlockers(); this.emitUpdate(); return; } if (!(await this.ensureStandxIsolatedMarginMode())) { const current = this.getStandxMarginMode(this.accountSnapshot); this.enterDefenseMode({ standxDepthStale: false, binanceStale: false, standxAccountStale: false, accountInvalid: false, standxRestUnhealthy: false, standxRestConsecutiveErrors: this.standxRestConsecutiveErrors, standxRestLastError: this.standxRestLastError, marginModeNotIsolated: true, marginMode: current, standxDepthAge: 0, binanceAge: 0, standxAccountAge: 0, accountIssues: [], }); this.emitUpdate(); return; } const accountHealth = validateAccountSnapshotForSymbol(this.accountSnapshot, this.config.symbol); if (!accountHealth.ok && !this.defenseMode) { this.enterDefenseMode({ standxDepthStale: false, binanceStale: false, standxAccountStale: false, accountInvalid: true, standxRestUnhealthy: false, standxRestConsecutiveErrors: this.standxRestConsecutiveErrors, standxRestLastError: this.standxRestLastError, marginModeNotIsolated: false, marginMode: this.getStandxMarginMode(this.accountSnapshot), standxDepthAge: 0, binanceAge: 0, standxAccountAge: 0, accountIssues: accountHealth.issues, }); this.emitUpdate(); return; } this.resetReadinessFlags(); if (!(await this.ensureStartupOrderReset())) { this.emitUpdate(); return; } const position = getPosition(this.accountSnapshot, this.config.symbol); const absPosition = Math.abs(position.positionAmt); if (await this.handleTokenExpiry(position, absPosition)) { this.emitUpdate(); return; } const depth = this.depthSnapshot!; const { topBid, topAsk } = getTopPrices(depth); if (topBid == null || topAsk == null) { this.emitUpdate(); return; } const closeThreshold = Number(this.config.closeThreshold); const closeOnly = this.tokenExpiredCloseOnlyMode || (Number.isFinite(closeThreshold) && closeThreshold > 0 && absPosition >= closeThreshold - EPS); const prevCloseOnly = this.lastCloseOnly; if (closeOnly !== prevCloseOnly) { this.tradeLog.push("info", closeOnly ? "进入平仓模式,仅挂 reduce-only" : "退出平仓模式"); this.lastCloseOnly = closeOnly; } const binanceSnapshot = this.binanceDepth.getSnapshot(); const rawSkipBuy = this.config.enableBinanceDepthCancel && Boolean(binanceSnapshot?.skipBuySide); const rawSkipSell = this.config.enableBinanceDepthCancel && Boolean(binanceSnapshot?.skipSellSide); const skipBuy = closeOnly ? false : rawSkipBuy; const skipSell = closeOnly ? false : rawSkipSell; const prevSkipBuy = this.lastSkipBuy; const prevSkipSell = this.lastSkipSell; if (skipBuy !== prevSkipBuy || skipSell !== prevSkipSell) { if (skipBuy || skipSell) { const summary = `${skipBuy ? "BUY" : ""}${skipBuy && skipSell ? "/" : ""}${skipSell ? "SELL" : ""}`; this.tradeLog.push("info", `Binance 深度失衡,暂停 ${summary} 挂单`); } else { this.tradeLog.push("info", "Binance 深度恢复,继续挂单"); } this.lastSkipBuy = skipBuy; this.lastSkipSell = skipSell; } const closeOnlyChanged = closeOnly !== prevCloseOnly; const skipChanged = skipBuy !== prevSkipBuy || skipSell !== prevSkipSell; const repriceNeeded = closeOnly ? true : this.shouldReprice(topBid, topAsk); const depthStatusChanged = this.checkDepthStatusChanged(depth, topBid, topAsk); const shouldRecompute = closeOnly || repriceNeeded || closeOnlyChanged || skipChanged || depthStatusChanged || this.desiredOrders.length === 0; const desired = shouldRecompute ? closeOnly ? this.buildCloseOnlyOrders(position, topBid, topAsk) : this.buildDesiredOrders({ bid1: topBid, ask1: topAsk, skipBuy, skipSell, depth, }) : this.desiredOrders; if (shouldRecompute) { if (closeOnly) { this.lastQuoteBid1 = null; this.lastQuoteAsk1 = null; } else { this.lastQuoteBid1 = topBid; this.lastQuoteAsk1 = topAsk; } } this.desiredOrders = desired; this.logDesiredOrders(desired); this.sessionVolume.update(position, this.getReferencePrice()); await this.syncOrders(desired, closeOnly); this.emitUpdate(); } catch (error) { if (isRateLimitError(error)) { hadRateLimit = true; this.rateLimit.registerRateLimit("maker-points"); this.tradeLog.push("warn", `限频触发,暂停挂单: ${extractMessage(error)}`); } else { this.tradeLog.push("error", `MakerPoints 主循环异常: ${extractMessage(error)}`); } this.emitUpdate(); } finally { this.rateLimit.onCycleComplete(hadRateLimit); this.processing = false; } } private buildDesiredOrders(params: { bid1: number; ask1: number; skipBuy: boolean; skipSell: boolean; depth: AsterDepth | null; }): DesiredOrder[] { const { bid1, ask1, skipBuy, skipSell, depth } = params; const targets = buildBpsTargets({ band0To10: this.config.enableBand0To10, band10To30: this.config.enableBand10To30, band30To100: this.config.enableBand30To100, }).sort((a, b) => b - a); if (!targets.length) return []; const priceDecimals = this.getPriceDecimals(); const desired: DesiredOrder[] = []; const minDepth = this.config.filterMinDepth; const getAmountForBps = (bps: number): number => { if (bps <= 10) return Number(this.config.band0To10Amount); if (bps <= 30) return Number(this.config.band10To30Amount); return Number(this.config.band30To100Amount); }; for (const bps of targets) { const amount = getAmountForBps(bps); if (!Number.isFinite(amount) || amount <= 0) continue; // 所有档位都检查深度 const shouldCheckDepth = minDepth > 0; if (!skipBuy) { const price = bid1 * (1 - bps / 10000); if (Number.isFinite(price) && price > 0) { if (shouldCheckDepth) { const depthQty = getDepthBetweenPrices(depth, "BUY", price); if (depthQty < minDepth) { this.logThinDepthSkip("BUY", bps, depthQty, minDepth); } else { this.resetThinDepthSkip("BUY", bps); desired.push({ side: "BUY", price: formatPriceToString(price, priceDecimals), amount, reduceOnly: false, }); } } else { desired.push({ side: "BUY", price: formatPriceToString(price, priceDecimals), amount, reduceOnly: false, }); } } } if (!skipSell) { const price = ask1 * (1 + bps / 10000); if (Number.isFinite(price) && price > 0) { if (shouldCheckDepth) { const depthQty = getDepthBetweenPrices(depth, "SELL", price); if (depthQty < minDepth) { this.logThinDepthSkip("SELL", bps, depthQty, minDepth); } else { this.resetThinDepthSkip("SELL", bps); desired.push({ side: "SELL", price: formatPriceToString(price, priceDecimals), amount, reduceOnly: false, }); } } else { desired.push({ side: "SELL", price: formatPriceToString(price, priceDecimals), amount, reduceOnly: false, }); } } } } return desired; } /** * 检查各档位的深度状态是否发生变化 * 当深度从足够变为不足,或从不足变为足够时,需要触发重新计算 */ private checkDepthStatusChanged( depth: AsterDepth | null, bid1: number, ask1: number ): boolean { const minDepth = this.config.filterMinDepth; if (minDepth <= 0) return false; // 获取启用的所有档位 const targets = buildBpsTargets({ band0To10: this.config.enableBand0To10, band10To30: this.config.enableBand10To30, band30To100: this.config.enableBand30To100, }); let changed = false; for (const bps of targets) { const buyPrice = bid1 * (1 - bps / 10000); const sellPrice = ask1 * (1 + bps / 10000); const buyDepthQty = getDepthBetweenPrices(depth, "BUY", buyPrice); const sellDepthQty = getDepthBetweenPrices(depth, "SELL", sellPrice); const currentBuyOk = buyDepthQty >= minDepth; const currentSellOk = sellDepthQty >= minDepth; const lastStatus = this.lastDepthOkStatus[bps]; if (lastStatus) { if (lastStatus.buy !== currentBuyOk || lastStatus.sell !== currentSellOk) { changed = true; } } this.lastDepthOkStatus[bps] = { buy: currentBuyOk, sell: currentSellOk }; } return changed; } private buildCloseOnlyOrders( position: PositionSnapshot, bid1: number, ask1: number ): DesiredOrder[] { const absPosition = Math.abs(position.positionAmt); if (absPosition < EPS) return []; const priceDecimals = this.getPriceDecimals(); if (position.positionAmt > 0) { return [ { side: "SELL", price: formatPriceToString(bid1, priceDecimals), amount: absPosition, reduceOnly: true, }, ]; } return [ { side: "BUY", price: formatPriceToString(ask1, priceDecimals), amount: absPosition, reduceOnly: true, }, ]; } private shouldReprice(bid1: number, ask1: number): boolean { const threshold = Number(this.config.minRepriceBps); if (!Number.isFinite(threshold) || threshold <= 0) return true; if (!Number.isFinite(bid1) || !Number.isFinite(ask1)) return false; if (!Number.isFinite(this.lastQuoteBid1 ?? NaN) || !Number.isFinite(this.lastQuoteAsk1 ?? NaN)) { return true; } if ((this.lastQuoteBid1 ?? 0) <= 0 || (this.lastQuoteAsk1 ?? 0) <= 0) return true; const bidMove = Math.abs(bid1 - (this.lastQuoteBid1 ?? bid1)) / (this.lastQuoteBid1 ?? bid1) * 10000; const askMove = Math.abs(ask1 - (this.lastQuoteAsk1 ?? ask1)) / (this.lastQuoteAsk1 ?? ask1) * 10000; return bidMove >= threshold || askMove >= threshold; } private async ensureStartupOrderReset(): Promise { if (this.initialOrderResetDone) return true; if (!this.initialOrderSnapshotReady) return false; if (!this.openOrders.length) { this.initialOrderResetDone = true; return true; } try { await this.exchange.cancelAllOrders({ symbol: this.config.symbol }); this.pendingCancelOrders.clear(); unlockOperating(this.locks, this.timers, this.pending, "LIMIT"); this.openOrders = []; this.emitUpdate(); this.tradeLog.push("order", "启动时清理历史挂单"); this.initialOrderResetDone = true; return true; } catch (error) { if (isUnknownOrderError(error)) { this.tradeLog.push("order", "历史挂单已消失,跳过启动清理"); this.initialOrderResetDone = true; this.openOrders = []; this.emitUpdate(); return true; } this.tradeLog.push("error", `启动撤单失败: ${String(error)}`); return false; } } private async syncOrders(targets: DesiredOrder[], _closeOnly: boolean): Promise { // 止损执行期间不进行挂单操作,避免订单冲突 if (this.stopLossProcessing) return; // 重连处理期间不进行挂单操作 if (this.reconnectResetPending) return; // 价格变化保护:如果需要 reprice 且距上次查询已过足够时间,先查询真实挂单 const shouldVerifyOrders = await this.verifyOrdersIfNeeded(); if (shouldVerifyOrders) { // 如果发现有未预期的挂单,先取消所有挂单 return; } const availableOrders = this.openOrders.filter((o) => !this.pendingCancelOrders.has(String(o.orderId))); const openOrders = availableOrders.filter((order) => isOrderActiveStatus(order.status)); const { toCancel, toPlace } = makeOrderPlan(openOrders, targets); for (const order of toCancel) { if (this.pendingCancelOrders.has(String(order.orderId))) continue; this.pendingCancelOrders.add(String(order.orderId)); await safeCancelOrder( this.exchange, this.config.symbol, order, () => { this.tradeLog.push( "order", `撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}` ); }, () => { this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略"); this.pendingCancelOrders.delete(String(order.orderId)); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); }, (error) => { this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); this.pendingCancelOrders.delete(String(order.orderId)); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); } ); } const insufficientActive = this.applyInsufficientBalanceState(Date.now()); if (this.rateLimit.shouldBlockEntries() || insufficientActive) { return; } for (const target of toPlace) { if (!target) continue; if (target.amount < EPS) continue; try { // reduce-only 订单不能设置 tp/sl,仅开仓单设置止损 const priceNum = Number(target.price); const slPrice = target.reduceOnly ? undefined : target.side === "BUY" ? priceNum - 1 : priceNum + 1; await placeOrder( this.exchange, this.config.symbol, this.openOrders, this.locks, this.timers, this.pending, target.side, target.price, target.amount, (type, detail) => this.tradeLog.push(type, detail), target.reduceOnly, undefined, { priceTick: this.priceTick, qtyStep: this.qtyStep, skipDedupe: true, slPrice, } ); } catch (error) { if (isInsufficientBalanceError(error)) { this.registerInsufficientBalance(error); break; } if (isPrecisionError(error)) { this.tradeLog.push("warn", `检测到精度错误,重新同步: ${extractMessage(error)}`); this.syncPrecision(true); } this.tradeLog.push( "error", `挂单失败 ${target.side} @ ${target.price}: ${extractMessage(error)}` ); } } } /** * 验证真实挂单状态,防止取消请求丢失 * 在每次 reprice 时查询真实挂单,发现未预期的挂单时取消所有挂单 * @returns true 表示发现问题并执行了取消操作,调用方应跳过本轮挂单 */ private async verifyOrdersIfNeeded(): Promise { // 如果交易所不支持查询挂单,跳过验证 if (!this.exchange.queryOpenOrders) return false; // 限制查询频率 const now = Date.now(); if (now - this.lastRepriceQueryTime < this.repriceQueryIntervalMs) { return false; } try { const realOrders = await this.exchange.queryOpenOrders(); this.lastRepriceQueryTime = now; // 比较真实挂单与本地记录 const realOrderIds = new Set(realOrders.map((o) => String(o.orderId))); const localOrderIds = new Set(this.openOrders.map((o) => String(o.orderId))); // 查找本地以为已取消但实际还存在的订单 const unexpectedOrders = realOrders.filter((order) => { const orderId = String(order.orderId); // 如果本地没有这个订单,说明我们以为它已经被取消了 if (!localOrderIds.has(orderId)) { return true; } // 如果本地记录这个订单在等待取消,但实际还存在 if (this.pendingCancelOrders.has(orderId)) { return true; } return false; }); if (unexpectedOrders.length > 0) { this.tradeLog.push( "warn", `发现 ${unexpectedOrders.length} 个未预期挂单,执行强制取消` ); // 强制取消所有挂单 if (this.exchange.forceCancelAllOrders) { await this.exchange.forceCancelAllOrders(); } else { await this.exchange.cancelAllOrders({ symbol: this.config.symbol }); } // 重置本地状态 this.openOrders = []; this.pendingCancelOrders.clear(); this.tradeLog.push("order", "已强制取消所有挂单,重置本地状态"); return true; } // 更新本地挂单状态以匹配真实状态 if (realOrders.length !== this.openOrders.length) { // 移除本地记录中不存在于服务器的订单 this.openOrders = this.openOrders.filter((o) => realOrderIds.has(String(o.orderId))); } } catch (error) { this.tradeLog.push("error", `验证挂单状态失败: ${extractMessage(error)}`); } return false; } /** * 使用实时深度数据计算仓位的未实现盈亏 * 优先使用实时数据,回退到账户快照数据 */ private computeRealtimePnl(position: PositionSnapshot): number | null { const { topBid, topAsk } = getTopPrices(this.depthSnapshot); return computeStopLossPnl(position, topBid, topAsk); } private async checkStopLoss(): Promise { if (this.stopLossProcessing) return; const lossLimit = Number(this.config.stopLossUsd); if (!Number.isFinite(lossLimit) || lossLimit <= 0) return; if (!this.accountSnapshot) return; const position = getPosition(this.accountSnapshot, this.config.symbol); const absPosition = Math.abs(position.positionAmt); if (absPosition < EPS) return; // 使用实时计算的 PnL const realtimePnl = this.computeRealtimePnl(position); if (realtimePnl == null) return; const now = Date.now(); if (now < this.stopLossCooldownUntil) return; if (realtimePnl > -lossLimit) return; this.stopLossProcessing = true; // 不在这里设置冷却期,只有成功平仓后才设置 this.tradeLog.push( "stop", `触发止损: 实时未实现亏损 ${realtimePnl.toFixed(4)} USDT` ); this.notify({ type: "stop_loss", level: "error", symbol: this.config.symbol, title: "止损触发", message: `实时未实现亏损 ${realtimePnl.toFixed(4)} USDT,强制平仓`, details: { side: position.positionAmt > 0 ? "LONG" : "SHORT", size: absPosition, unrealizedPnl: realtimePnl, lossLimit: -lossLimit, }, }); // 循环重试止损,直到仓位为0 await this.executeStopLossWithRetry(position.positionAmt > 0 ? "SELL" : "BUY"); } /** * 执行止损平仓,失败后自动重试直到仓位为0 */ private async executeStopLossWithRetry(side: "BUY" | "SELL"): Promise { const maxRetries = 10; let retryCount = 0; try { while (retryCount < maxRetries) { // 每次重试前重新检查仓位 const currentPosition = getPosition(this.accountSnapshot, this.config.symbol); const currentAbsPosition = Math.abs(currentPosition.positionAmt); // 仓位已清零,止损成功 if (currentAbsPosition < EPS) { this.tradeLog.push("stop", "止损成功: 仓位已清零"); this.stopLossCooldownUntil = Date.now() + STOP_LOSS_COOLDOWN_MS; break; } try { // 强制解锁 MARKET 类型,确保不被之前的操作阻塞 unlockOperating(this.locks, this.timers, this.pending, "MARKET"); // 先取消所有挂单 await this.flushOrders(); // 执行市价平仓 await marketClose( this.exchange, this.config.symbol, this.openOrders, this.locks, this.timers, this.pending, side, currentAbsPosition, (type, detail) => this.tradeLog.push(type, detail), undefined, { qtyStep: this.qtyStep } ); // 等待一小段时间让账户数据更新 await this.sleep(STOP_LOSS_RETRY_INTERVAL_MS); } catch (error) { retryCount++; if (isUnknownOrderError(error)) { this.tradeLog.push("order", "止损平仓时订单已不存在,继续检查仓位"); } else if (isPrecisionError(error)) { this.tradeLog.push("warn", `止损平仓精度错误,重新同步: ${extractMessage(error)}`); this.syncPrecision(true); } else { this.tradeLog.push("error", `止损平仓失败 (重试 ${retryCount}/${maxRetries}): ${extractMessage(error)}`); } // 失败后等待一段时间再重试 if (retryCount < maxRetries) { await this.sleep(STOP_LOSS_RETRY_INTERVAL_MS); } } } if (retryCount >= maxRetries) { this.tradeLog.push("error", `止损重试已达上限 (${maxRetries} 次),请手动检查仓位`); // 达到重试上限后设置冷却期,避免持续重试 this.stopLossCooldownUntil = Date.now() + STOP_LOSS_COOLDOWN_MS; } } finally { this.stopLossProcessing = false; this.emitUpdate(); } } private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } private async flushOrders(): Promise { if (!this.openOrders.length) return; for (const order of this.openOrders) { if (this.pendingCancelOrders.has(String(order.orderId))) continue; this.pendingCancelOrders.add(String(order.orderId)); await safeCancelOrder( this.exchange, this.config.symbol, order, () => { // No log on successful cancel }, () => { this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略"); this.pendingCancelOrders.delete(String(order.orderId)); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); }, (error) => { this.tradeLog.push("error", `撤销订单失败: ${String(error)}`); this.pendingCancelOrders.delete(String(order.orderId)); this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId); } ); } } private syncPrecision(force = false): void { if (this.precisionSync && !force) return; const getPrecision = this.exchange.getPrecision?.bind(this.exchange); if (!getPrecision) return; this.precisionSync = getPrecision() .then((precision) => { this.precisionSync = null; if (!precision) return; let updated = false; if (Number.isFinite(precision.priceTick) && precision.priceTick > 0) { if (Math.abs(precision.priceTick - this.priceTick) > 1e-12) { this.priceTick = precision.priceTick; this.config.priceTick = precision.priceTick; updated = true; } } if (Number.isFinite(precision.qtyStep) && precision.qtyStep > 0) { if (Math.abs(precision.qtyStep - this.qtyStep) > 1e-12) { this.qtyStep = precision.qtyStep; updated = true; } } if (updated) { this.tradeLog.push( "info", t("log.common.precisionSynced", { priceTick: precision.priceTick, qtyStep: precision.qtyStep, }) ); } }) .catch((error) => { this.tradeLog.push("error", t("log.common.precisionFailed", { error: extractMessage(error) })); this.precisionSync = null; setTimeout(() => this.syncPrecision(), 2000); }); } private getPriceDecimals(): number { const tick = Math.max(1e-9, this.priceTick); const raw = Math.log10(1 / tick); if (!Number.isFinite(raw)) return 0; return Math.max(0, Math.floor(raw + 1e-9)); } private emitUpdate(): void { try { const snapshot = this.buildSnapshot(); this.events.emit("update", snapshot, (error) => { this.tradeLog.push("error", `更新监听异常: ${String(error)}`); }); } catch (err) { this.tradeLog.push("error", `快照生成异常: ${String(err)}`); } } private buildSnapshot(): MakerPointsSnapshot { const position = getPosition(this.accountSnapshot, this.config.symbol); const { topBid, topAsk } = getTopPrices(this.depthSnapshot); const spread = topBid != null && topAsk != null ? topAsk - topBid : null; const pnl = computePositionPnl(position, topBid, topAsk); const bandDepths = this.computeBandDepths(topBid, topAsk); return { ready: this.isReady(), symbol: this.config.symbol, topBid, topAsk, spread, priceDecimals: this.getPriceDecimals(), position, pnl, accountUnrealized: this.accountUnrealized, sessionVolume: this.sessionVolume.value, openOrders: this.openOrders, desiredOrders: this.desiredOrders, tradeLog: this.tradeLog.all(), lastUpdated: Date.now(), feedStatus: { ...this.feedStatus }, binanceDepth: this.binanceDepth.getSnapshot(), bandDepths, quoteStatus: { closeOnly: this.lastCloseOnly, skipBuy: this.lastSkipBuy, skipSell: this.lastSkipSell, }, }; } private computeBandDepths(topBid: number | null, topAsk: number | null): MakerPointsSnapshot["bandDepths"] { const bands: MakerPointsSnapshot["bandDepths"] = [ { band: "0-10", bps: 9, buyDepth: null, sellDepth: null, enabled: this.config.enableBand0To10 }, { band: "10-30", bps: 29, buyDepth: null, sellDepth: null, enabled: this.config.enableBand10To30 }, { band: "30-100", bps: 99, buyDepth: null, sellDepth: null, enabled: this.config.enableBand30To100 }, ]; if (!this.depthSnapshot || topBid == null || topAsk == null) { return bands; } return bands.map((band) => { const buyPrice = topBid * (1 - band.bps / 10000); const sellPrice = topAsk * (1 + band.bps / 10000); const buyDepth = getDepthBetweenPrices(this.depthSnapshot, "BUY", buyPrice); const sellDepth = getDepthBetweenPrices(this.depthSnapshot, "SELL", sellPrice); return { ...band, buyDepth, sellDepth }; }); } private getReferencePrice(): number | null { return getMidOrLast(this.depthSnapshot, this.tickerSnapshot); } private notify(notification: TradeNotification): void { this.notifier.send(notification); } private logReadinessBlockers(): void { if (!this.feedStatus.account && !this.readinessLogged.account) { this.tradeLog.push("info", t("log.maker.waitAccount")); this.readinessLogged.account = true; } if (!this.feedStatus.depth && !this.readinessLogged.depth) { this.tradeLog.push("info", t("log.maker.waitDepth")); this.readinessLogged.depth = true; } if (!this.feedStatus.ticker && !this.readinessLogged.ticker) { this.tradeLog.push("info", t("log.maker.waitTicker")); this.readinessLogged.ticker = true; } if (!this.feedStatus.orders && !this.readinessLogged.orders) { this.tradeLog.push("info", t("log.maker.waitOrders")); this.readinessLogged.orders = true; } } private resetReadinessFlags(): void { this.readinessLogged = { account: false, depth: false, ticker: false, orders: false, }; } private logDesiredOrders(desired: DesiredOrder[]): void { if (!desired.length) { if (this.lastDesiredSummary !== "none") { this.tradeLog.push("info", "暂无目标挂单"); this.lastDesiredSummary = "none"; } return; } const summary = desired .map((order) => `${order.side}@${order.price}${order.reduceOnly ? "(RO)" : ""}`) .join(" | "); if (summary !== this.lastDesiredSummary) { this.tradeLog.push("info", `目标挂单: ${summary}`); this.lastDesiredSummary = summary; } } // 跟踪各档位的深度跳过状态 (按 bps 和 side 索引) private thinDepthSkipStatus: Record = {}; /** * 记录因深度不足而跳过挂单的日志 * 使用状态跟踪避免重复日志 */ private logThinDepthSkip(side: "BUY" | "SELL", bps: number, depthQty: number, minDepth: number): void { const key = `${side}_${bps}`; const alreadySkipped = this.thinDepthSkipStatus[key]; if (!alreadySkipped) { this.tradeLog.push( "info", `跳过 ${side} ${bps}bps 挂单: 深度 ${depthQty.toFixed(4)} BTC < ${minDepth} BTC` ); this.thinDepthSkipStatus[key] = true; } } /** * 当深度恢复时重置跳过状态,允许下次再次记录 */ private resetThinDepthSkip(side: "BUY" | "SELL", bps: number): void { const key = `${side}_${bps}`; if (this.thinDepthSkipStatus[key]) { this.tradeLog.push("info", `${side} ${bps}bps 深度恢复,继续挂单`); this.thinDepthSkipStatus[key] = false; } } private registerInsufficientBalance(error: unknown): void { const now = Date.now(); const detail = extractMessage(error); const alreadyActive = now < this.insufficientBalanceCooldownUntil; if (alreadyActive && detail === this.lastInsufficientMessage) { this.insufficientBalanceCooldownUntil = now + INSUFFICIENT_BALANCE_COOLDOWN_MS; return; } this.insufficientBalanceCooldownUntil = now + INSUFFICIENT_BALANCE_COOLDOWN_MS; this.lastInsufficientMessage = detail; const seconds = Math.ceil(INSUFFICIENT_BALANCE_COOLDOWN_MS / 1000); this.tradeLog.push("warn", `余额不足,暂停挂单 ${seconds}s: ${detail}`); this.insufficientBalanceNotified = true; } private applyInsufficientBalanceState(now: number): boolean { const active = now < this.insufficientBalanceCooldownUntil; if (!active && this.insufficientBalanceNotified) { this.tradeLog.push("info", "余额恢复,继续挂单"); this.insufficientBalanceNotified = false; this.lastInsufficientMessage = null; } return active; } private detectPositionChange(position: PositionSnapshot): void { const currentAmt = position.positionAmt; const currentSide: "LONG" | "SHORT" | "FLAT" = currentAmt > EPS ? "LONG" : currentAmt < -EPS ? "SHORT" : "FLAT"; const prevAmt = this.lastPositionAmt; const prevSide = this.lastPositionSide; if (Math.abs(currentAmt - prevAmt) < EPS && currentSide === prevSide) { return; } const absChange = Math.abs(currentAmt - prevAmt); const reference = this.getReferencePrice() ?? 0; if (prevSide === "FLAT" && currentSide !== "FLAT") { this.notify({ type: "position_opened", level: "info", symbol: this.config.symbol, title: "开仓", message: `${currentSide === "LONG" ? "做多" : "做空"} ${Math.abs(currentAmt).toFixed(6)}`, details: { side: currentSide, size: Math.abs(currentAmt), price: reference > 0 ? reference : null, }, }); } else if (currentSide === "FLAT" && prevSide !== "FLAT") { const pnl = position.unrealizedProfit; const closeType = this.tokenExpiredCloseOnlyMode ? "Token过期平仓" : "平仓"; this.notify({ type: "position_closed", level: "success", symbol: this.config.symbol, title: closeType, message: `已平仓 ${Math.abs(prevAmt).toFixed(6)} (${prevSide === "LONG" ? "多" : "空"})`, details: { prevSide, closedSize: Math.abs(prevAmt), pnl: Number.isFinite(pnl) ? pnl : null, }, }); } else if (currentSide === prevSide && absChange > EPS) { const isIncrease = Math.abs(currentAmt) > Math.abs(prevAmt); if (isIncrease) { this.notify({ type: "order_filled", level: "info", symbol: this.config.symbol, title: "加仓", message: `${currentSide === "LONG" ? "做多" : "做空"} +${absChange.toFixed(6)} → ${Math.abs(currentAmt).toFixed(6)}`, details: { side: currentSide, added: absChange, totalSize: Math.abs(currentAmt), }, }); } else { this.notify({ type: "order_filled", level: "info", symbol: this.config.symbol, title: "减仓", message: `${currentSide === "LONG" ? "多" : "空"} -${absChange.toFixed(6)} → ${Math.abs(currentAmt).toFixed(6)}`, details: { side: currentSide, reduced: absChange, totalSize: Math.abs(currentAmt), }, }); } } else if (currentSide !== prevSide && currentSide !== "FLAT" && prevSide !== "FLAT") { this.notify({ type: "position_opened", level: "info", symbol: this.config.symbol, title: "反向开仓", message: `${prevSide === "LONG" ? "多→空" : "空→多"} ${Math.abs(currentAmt).toFixed(6)}`, details: { prevSide, newSide: currentSide, size: Math.abs(currentAmt), }, }); } this.lastPositionAmt = currentAmt; this.lastPositionSide = currentSide; } private async handleTokenExpiry(position: PositionSnapshot, _absPosition: number): Promise { if (!isTokenExpiryConfigured()) { return false; } const expiryStatus = checkStandxTokenExpiry({ positionAmt: position.positionAmt, openOrderCount: this.openOrders.length, }); if (!expiryStatus.expired) { if (this.tokenExpiryState !== "active") { this.tokenExpiryState = "active"; this.tokenExpiryLogged = false; this.tokenExpiryCancelDone = false; this.tokenExpiredCloseOnlyMode = false; this.tokenExpiryNotified = false; } return false; } const prevState = this.tokenExpiryState; this.tokenExpiryState = expiryStatus.state; if (!this.tokenExpiryLogged) { const message = formatTokenExpiryMessage(expiryStatus); if (message) { this.tradeLog.push("warn", message); } this.tokenExpiryLogged = true; } if (!this.tokenExpiryNotified) { this.notify({ type: "token_expired", level: "warn", symbol: this.config.symbol, title: "Token 已过期", message: expiryStatus.hasPosition ? "Token 已过期,进入平仓模式,不再开新仓" : "Token 已过期,策略进入静默模式", details: { hasPosition: expiryStatus.hasPosition, hasOpenOrders: expiryStatus.hasOpenOrders, state: expiryStatus.state, }, }); this.tokenExpiryNotified = true; } if (!this.tokenExpiryCancelDone && this.openOrders.length > 0) { try { await this.exchange.cancelAllOrders({ symbol: this.config.symbol }); this.tradeLog.push("order", "Token 过期,已撤销所有挂单"); this.openOrders = []; this.tokenExpiryCancelDone = true; } catch (error) { if (isUnknownOrderError(error)) { this.tradeLog.push("order", "Token 过期撤单时订单已不存在"); this.tokenExpiryCancelDone = true; } else { this.tradeLog.push("error", `Token 过期撤单失败: ${extractMessage(error)}`); } } } if (expiryStatus.state === "expired_with_position") { if (!this.tokenExpiredCloseOnlyMode) { this.tokenExpiredCloseOnlyMode = true; this.tradeLog.push("info", "Token 过期,强制进入平仓模式,仅允许 reduce-only 订单"); } return false; } if (expiryStatus.state === "silent") { if (prevState !== "silent") { this.tradeLog.push("info", "进入静默数据接收模式,不再进行任何交易操作"); } return true; } return true; } // ========== 数据过时防御模式方法 ========== /** * 检查数据是否过时,进入或退出防御模式 * StandX 账户数据在 WS 推送异常时会通过 REST 补拉;长期无更新通常意味着 WS/REST 均异常,应进入防御模式 */ private checkDataStaleAndDefense(): void { const now = Date.now(); const standxDepthStale = this.lastStandxDepthTime > 0 && (now - this.lastStandxDepthTime) > DATA_STALE_THRESHOLD_MS; const binanceStale = this.lastBinanceDepthTime > 0 && (now - this.lastBinanceDepthTime) > DATA_STALE_THRESHOLD_MS; const standxAccountAge = this.lastStandxAccountTime > 0 ? now - this.lastStandxAccountTime : 0; const standxAccountStaleByAge = this.lastStandxAccountTime > 0 && standxAccountAge > ACCOUNT_DATA_STALE_THRESHOLD_MS; if (standxAccountStaleByAge) { this.maybeProbeStandxAccountSnapshot(now); } const standxAccountStale = standxAccountStaleByAge && // WS 推送间隔可能较长,先给 REST 补拉一次机会;只有补拉失败后才进入防御模式 this.accountStaleRestProbeConsecutiveFailures > 0 && this.accountStaleRestProbeInFlight == null; const accountHealth = validateAccountSnapshotForSymbol(this.accountSnapshot, this.config.symbol); const accountInvalid = this.accountSnapshot != null && !accountHealth.ok; const standxRestUnhealthy = this.standxRestUnhealthy && this.standxRestConsecutiveErrors >= STANDX_REST_ERROR_DEFENSE_THRESHOLD; const marginMode = this.getStandxMarginMode(this.accountSnapshot); const marginModeNotIsolated = this.exchange.id === "standx" && marginMode != null && marginMode !== "isolated"; const shouldDefend = standxDepthStale || binanceStale || standxAccountStale || accountInvalid || standxRestUnhealthy || marginModeNotIsolated; if (shouldDefend && !this.defenseMode) { // 进入防御模式 this.enterDefenseMode({ standxDepthStale, binanceStale, standxAccountStale, accountInvalid, standxRestUnhealthy, standxRestConsecutiveErrors: this.standxRestConsecutiveErrors, standxRestLastError: this.standxRestLastError, marginModeNotIsolated, marginMode, standxDepthAge: this.lastStandxDepthTime > 0 ? now - this.lastStandxDepthTime : 0, binanceAge: this.lastBinanceDepthTime > 0 ? now - this.lastBinanceDepthTime : 0, standxAccountAge, accountIssues: accountInvalid ? accountHealth.issues : [], }); } else if (!shouldDefend && this.defenseMode) { // 退出防御模式 this.exitDefenseMode(); } } /** * 进入防御模式 * 取消所有挂单,启动 REST 轮询保护仓位 */ private enterDefenseMode(staleInfo: { standxDepthStale: boolean; binanceStale: boolean; standxAccountStale: boolean; accountInvalid: boolean; standxRestUnhealthy: boolean; standxRestConsecutiveErrors: number; standxRestLastError: string | null; marginModeNotIsolated: boolean; marginMode: string | null; standxDepthAge: number; binanceAge: number; standxAccountAge: number; accountIssues: string[]; }): void { this.defenseMode = true; // 构建过时信息描述 const staleItems: string[] = []; if (staleInfo.standxDepthStale) { staleItems.push(`StandX深度(${Math.round(staleInfo.standxDepthAge / 1000)}s)`); } if (staleInfo.standxAccountStale) { staleItems.push(`StandX账户(${Math.round(staleInfo.standxAccountAge / 1000)}s)`); } if (staleInfo.accountInvalid) { staleItems.push(`StandX仓位数据异常(${staleInfo.accountIssues.join(",") || "unknown"})`); } if (staleInfo.standxRestUnhealthy) { staleItems.push(`StandX REST错误(${staleInfo.standxRestConsecutiveErrors}次)`); } if (staleInfo.marginModeNotIsolated) { staleItems.push(`保证金模式(${staleInfo.marginMode ?? "unknown"})`); } if (staleInfo.binanceStale) { staleItems.push(`Binance深度(${Math.round(staleInfo.binanceAge / 1000)}s)`); } this.tradeLog.push("warn", `数据过时检测: ${staleItems.join(", ")},进入防御模式`); // 发送通知 if (!this.defenseModeNotified) { this.notify({ type: "token_expired", level: "warn", symbol: this.config.symbol, title: "防御模式", message: `数据推送中断: ${staleItems.join(", ")},已取消所有挂单`, details: staleInfo, }); this.defenseModeNotified = true; } // 立即取消所有挂单 void this.defenseCancelAllOrders(); // 启动 REST 轮询保护仓位 this.startDefenseRestPoll(); } /** * 退出防御模式 */ private exitDefenseMode(): void { this.defenseMode = false; this.defenseModeNotified = false; this.tradeLog.push("info", "数据推送恢复正常,退出防御模式"); this.notify({ type: "position_opened", level: "info", symbol: this.config.symbol, title: "防御模式解除", message: "数据推送恢复正常,恢复正常交易", details: {}, }); // 停止 REST 轮询 this.stopDefenseRestPoll(); // 重置本地状态,强制下一轮重新计算挂单 this.desiredOrders = []; this.lastDesiredSummary = null; this.lastQuoteBid1 = null; this.lastQuoteAsk1 = null; } /** * 防御模式下取消所有挂单 */ private async defenseCancelAllOrders(): Promise { try { if (this.exchange.forceCancelAllOrders) { const success = await this.exchange.forceCancelAllOrders(); if (success) { this.tradeLog.push("order", "防御模式: 已强制取消所有挂单"); } else { this.tradeLog.push("warn", "防御模式: 取消挂单未完全成功,将继续重试"); } } else { await this.exchange.cancelAllOrders({ symbol: this.config.symbol }); this.tradeLog.push("order", "防御模式: 已取消所有挂单"); } // 重置本地挂单状态 this.openOrders = []; this.pendingCancelOrders.clear(); unlockOperating(this.locks, this.timers, this.pending, "LIMIT"); } catch (error) { if (isUnknownOrderError(error)) { this.tradeLog.push("order", "防御模式: 挂单已不存在"); this.openOrders = []; this.pendingCancelOrders.clear(); } else { this.tradeLog.push("error", `防御模式取消挂单失败: ${extractMessage(error)}`); } } } /** * 启动防御模式下的 REST 轮询 * 使用 REST API 拉取数据,确保止损逻辑能正常工作 */ private startDefenseRestPoll(): void { if (this.defenseRestPollActive) return; this.defenseRestPollActive = true; this.tradeLog.push("info", "防御模式: 启动 REST 数据轮询"); const poll = async () => { if (!this.defenseRestPollActive || !this.defenseMode) return; try { if (this.exchange.queryAccountSnapshot) { const nextAccount = await this.exchange.queryAccountSnapshot(); if (nextAccount) { this.applyAccountSnapshot(nextAccount); const health = validateAccountSnapshotForSymbol(nextAccount, this.config.symbol); if (!health.ok) { this.tradeLog.push("warn", `防御模式: 仓位数据仍异常: ${health.issues.join(",")}`); } } else { this.tradeLog.push("warn", "防御模式: REST 获取账户快照为空"); } } // 防御模式下也尝试修复保证金模式(StandX) if (this.exchange.id === "standx") { await this.ensureStandxIsolatedMarginMode(); } // 防御模式下持续通过 REST 刷新挂单,并尽力撤销所有挂单(避免本地状态/WS 丢失导致遗留挂单) if (this.exchange.queryOpenOrders) { try { const realOrders = await this.exchange.queryOpenOrders(); this.openOrders = Array.isArray(realOrders) ? realOrders.filter( (order) => order.type !== "MARKET" && order.symbol === this.config.symbol && isOrderActiveStatus(order.status) ) : []; this.pendingCancelOrders.clear(); this.feedStatus.orders = true; if (realOrders.length > 0) { this.tradeLog.push("warn", `防御模式: 发现 ${realOrders.length} 个挂单,执行取消`); await this.defenseCancelAllOrders(); } } catch (error) { this.tradeLog.push("error", `防御模式查询挂单失败: ${extractMessage(error)}`); // 查询失败时仍然尝试撤销所有挂单(宁可多撤,也不遗留) await this.defenseCancelAllOrders(); } } else { await this.defenseCancelAllOrders(); } // 检查止损条件(使用当前账户快照中的数据) // checkStopLoss 会继续运行,使用最后收到的数据进行止损判断 } catch (error) { this.tradeLog.push("error", `防御模式 REST 轮询失败: ${extractMessage(error)}`); } // 继续下一次轮询 if (this.defenseRestPollActive && this.defenseMode) { this.defenseRestPollTimer = setTimeout(() => void poll(), 2000); } }; void poll(); } private getStandxMarginMode(snapshot: AsterAccountSnapshot | null): string | null { if (this.exchange.id !== "standx") return null; const positions = snapshot?.positions ?? []; const match = positions.find((pos) => pos.symbol === this.config.symbol); const raw = (match as any)?.marginType ?? (match as any)?.margin_mode; const mode = typeof raw === "string" ? raw.trim().toLowerCase() : ""; return mode ? mode : null; } private async ensureStandxIsolatedMarginMode(): Promise { if (this.exchange.id !== "standx") return true; const currentMode = this.getStandxMarginMode(this.accountSnapshot); if (currentMode === "isolated") return true; const change = this.exchange.changeMarginMode?.bind(this.exchange); const queryAccount = this.exchange.queryAccountSnapshot?.bind(this.exchange); if (!change || !queryAccount) return false; if (this.marginModeEnsuring) { return false; } this.marginModeEnsuring = (async () => { try { await change({ symbol: this.config.symbol, marginMode: "isolated" }); for (let attempt = 0; attempt < STANDX_MARGIN_MODE_MAX_ATTEMPTS; attempt++) { const next = await queryAccount(); if (next) { this.applyAccountSnapshot(next); } const mode = this.getStandxMarginMode(this.accountSnapshot); if (mode === "isolated") { this.tradeLog.push("info", "已切换为逐仓模式 (isolated),恢复策略运行"); return true; } await this.sleep(STANDX_MARGIN_MODE_CHECK_INTERVAL_MS); } this.tradeLog.push("warn", `逐仓模式切换未确认,当前模式: ${this.getStandxMarginMode(this.accountSnapshot) ?? "unknown"}`); return false; } catch (error) { this.tradeLog.push("error", `切换逐仓模式失败: ${extractMessage(error)}`); return false; } finally { this.marginModeEnsuring = null; } })(); return await this.marginModeEnsuring; } /** * 停止防御模式下的 REST 轮询 */ private stopDefenseRestPoll(): void { if (!this.defenseRestPollActive) return; this.defenseRestPollActive = false; if (this.defenseRestPollTimer) { clearTimeout(this.defenseRestPollTimer); this.defenseRestPollTimer = null; } this.tradeLog.push("info", "防御模式: 停止 REST 数据轮询"); } } function resolveBinanceSymbol(symbol: string): string { const parts = parseSymbolParts(symbol); const base = (parts.base ?? symbol).replace(/[^a-zA-Z0-9]/g, "").toUpperCase(); return base ? `${base}USDT` : "BTCUSDT"; }