i18n: route the remaining 300 hardcoded strings through the table

Grid, offset-maker, liquidity-maker and maker-points wrote their trade log and
Telegram alerts as Chinese literals, so LANG=en changed the menu but not a
single runtime message. src/ now holds zero user-facing Chinese literals.

Reuses rather than duplicates: the four engines' subscription boilerplate maps
onto the existing log.subscribe.* / log.process.* keys, and the spot-maker
wording shared by offset-maker and liquidity-maker became one log.spotMaker.*
set instead of two.

Exchange gateways were treated differently on purpose: aster's exception and
console.error text is developer diagnostics, not UI, and its eight sibling
gateways already used English — so those were translated to English rather than
added to the table.

Adds tests/i18n-coverage.test.ts to hold the line: it fails on any new CJK
literal in src/, on a key missing either language, and on a duplicate key.
Several existing tests asserted the Chinese literals, which is what let the gap
persist; they now assert the resolved key and hold in either language.

214 new keys (300 -> 514). 275 pass; tsc and oxlint clean.
This commit is contained in:
discountry
2026-07-29 21:35:19 +08:00
parent b9331c516a
commit 22b9c5a39d
14 changed files with 964 additions and 273 deletions
+17 -17
View File
@@ -534,7 +534,7 @@ export class AsterSpotRestClient {
try {
response = await fetch(url, init);
} catch (error) {
throw new Error(`[AsterSpotRestClient] 请求失败 ${String(error)}`);
throw new Error(`[AsterSpotRestClient] request failed: ${String(error)}`);
}
const text = await response.text();
if (!response.ok) {
@@ -546,7 +546,7 @@ export class AsterSpotRestClient {
try {
return JSON.parse(text) as T;
} catch {
throw new Error(`[AsterSpotRestClient] 无法解析响应: ${text.slice(0, 200)}`);
throw new Error(`[AsterSpotRestClient] could not parse response: ${text.slice(0, 200)}`);
}
}
}
@@ -789,7 +789,7 @@ export class AsterRestClient {
try {
response = await fetch(url);
} catch (error) {
throw new Error(`[AsterRestClient] 获取交易规则失败 ${String(error)}`);
throw new Error(`[AsterRestClient] failed to fetch exchange info: ${String(error)}`);
}
const text = await response.text();
if (!response.ok) {
@@ -798,7 +798,7 @@ export class AsterRestClient {
try {
return JSON.parse(text) as AsterFuturesExchangeInfo;
} catch {
throw new Error(`[AsterRestClient] 无法解析交易规则响应: ${text.slice(0, 200)}`);
throw new Error(`[AsterRestClient] could not parse exchange info: ${text.slice(0, 200)}`);
}
}
@@ -863,7 +863,7 @@ export class AsterRestClient {
try {
response = await fetch(url);
} catch (error) {
throw new Error(`[AsterRestClient] 获取K线失败 ${String(error)}`);
throw new Error(`[AsterRestClient] failed to fetch klines: ${String(error)}`);
}
const text = await response.text();
if (!response.ok) {
@@ -873,7 +873,7 @@ export class AsterRestClient {
const payload = JSON.parse(text) as any[];
return payload.map((entry) => fromRestKline(entry, interval, upper));
} catch {
throw new Error(`[AsterRestClient] 无法解析K线响应: ${text.slice(0, 200)}`);
throw new Error(`[AsterRestClient] could not parse klines: ${text.slice(0, 200)}`);
}
}
@@ -892,7 +892,7 @@ export class AsterRestClient {
try {
response = await fetch(url);
} catch (error) {
throw new Error(`[AsterRestClient] 获取资金费率失败 ${String(error)}`);
throw new Error(`[AsterRestClient] failed to fetch funding rate: ${String(error)}`);
}
const text = await response.text();
if (!response.ok) {
@@ -903,7 +903,7 @@ export class AsterRestClient {
// The response shape mirrors Binance: { symbol, markPrice, indexPrice, lastFundingRate, nextFundingTime, time }
return payload;
} catch {
throw new Error(`[AsterRestClient] 无法解析资金费率响应: ${text.slice(0, 200)}`);
throw new Error(`[AsterRestClient] could not parse funding rate: ${text.slice(0, 200)}`);
}
}
@@ -937,7 +937,7 @@ export class AsterRestClient {
try {
response = await fetch(url, init);
} catch (error) {
throw new Error(`[AsterRestClient] 请求失败 ${String(error)}`);
throw new Error(`[AsterRestClient] request failed: ${String(error)}`);
}
const text = await response.text();
if (!response.ok) {
@@ -946,7 +946,7 @@ export class AsterRestClient {
try {
return JSON.parse(text) as T;
} catch {
throw new Error(`[AsterRestClient] 无法解析响应: ${text.slice(0, 200)}`);
throw new Error(`[AsterRestClient] could not parse response: ${text.slice(0, 200)}`);
}
}
@@ -1037,7 +1037,7 @@ export class AsterPublicStreams {
try {
payload = JSON.parse(event.data);
} catch (error) {
console.error("[AsterPublicStreams] 无法解析消息", error, event.data);
console.error("[AsterPublicStreams] could not parse message", error, event.data);
return;
}
} else {
@@ -1198,7 +1198,7 @@ export class AsterUserStream {
try {
payload = JSON.parse(event.data);
} catch (error) {
console.error("[AsterUserStream] 无法解析消息", error, event.data);
console.error("[AsterUserStream] could not parse message", error, event.data);
return;
}
} else {
@@ -1509,7 +1509,7 @@ export class AsterGateway {
positions = latestPositions;
}
} catch (positionError) {
console.error("[AsterGateway] 刷新持仓失败", positionError);
console.error("[AsterGateway] failed to refresh positions", positionError);
}
const normalizedPositions = clonePositions(positions);
const snapshot: AccountSnapshot = {
@@ -1521,7 +1521,7 @@ export class AsterGateway {
this.accountSnapshot = snapshot;
this.accountEvent.emit(snapshot);
} catch (error) {
console.error("[AsterGateway] 刷新账户信息失败", error);
console.error("[AsterGateway] failed to refresh account", error);
}
try {
const orders = await this.rest.getOpenOrders();
@@ -1529,7 +1529,7 @@ export class AsterGateway {
orders.forEach((order) => mergeOrderSnapshot(this.openOrders, order));
this.ordersEvent.emit(Array.from(this.openOrders.values()));
} catch (error) {
console.error("[AsterGateway] 刷新挂单失败", error);
console.error("[AsterGateway] failed to refresh open orders", error);
}
}
@@ -1573,7 +1573,7 @@ export class AsterGateway {
this.accountSnapshot = nextSnapshot;
this.accountEvent.emit(nextSnapshot);
} catch (error) {
console.error("[AsterGateway] 同步持仓失败", error);
console.error("[AsterGateway] failed to sync positions", error);
} finally {
this.positionSyncInFlight = false;
}
@@ -1608,7 +1608,7 @@ export class AsterGateway {
try {
exchangeInfo = await this.loadExchangeInfo();
} catch (error) {
console.error("[AsterGateway] 获取交易规则失败", error);
console.error("[AsterGateway] failed to fetch exchange info", error);
return null;
}
const symbols = exchangeInfo?.symbols ?? [];
+1 -1
View File
@@ -177,7 +177,7 @@ function loadSignatureProviderFromEnv(
return loaded.default as GrvtSignatureProvider;
}
console.warn(
`[GrvtExchangeAdapter] 模块 ${resolved} 未导出签名函数 (function default export)`
`[GrvtExchangeAdapter] module ${resolved} does not export a signing function (function default export)`
);
} catch (error) {
const log = logger ?? ((ctx, err) => console.error(`[GrvtExchangeAdapter] ${ctx}`, err));
+519
View File
@@ -860,6 +860,525 @@ const translations: Record<string, TranslationEntry> = {
zh: "切换逐仓模式失败: {error}",
en: "Failed to switch to isolated margin: {error}",
},
// --- strategy/grid-logic ---
"log.grid.entryFilled": {
zh: "ENTRY 成交: {side} @ {price} (线 {level})",
en: "ENTRY filled: {side} @ {price} (level {level})",
},
"log.grid.orphanExitFilled": {
zh: "孤儿 EXIT 成交: {side} @ {price}",
en: "Orphan EXIT filled: {side} @ {price}",
},
"log.grid.exitFilled": {
zh: "EXIT 成交: {side} @ {price} (释放线 {level})",
en: "EXIT filled: {side} @ {price} (level {level} released)",
},
"log.grid.entryCancelled": {
zh: "ENTRY 撤销: {side} @ {price} (线 {level})",
en: "ENTRY cancelled: {side} @ {price} (level {level})",
},
"log.grid.exitCancelled": {
zh: "EXIT 撤销: {side} @ {price} (线 {level})",
en: "EXIT cancelled: {side} @ {price} (level {level})",
},
"log.grid.orderVanished": {
zh: "订单消失待判定: {intent} {side} @ {price}",
en: "Order vanished, outcome unknown: {intent} {side} @ {price}",
},
"log.grid.belowLowerBound": {
zh: "价格跌破网格下边界 {pct}%",
en: "Price fell {pct}% below the grid's lower bound",
},
"log.grid.aboveUpperBound": {
zh: "价格突破网格上边界 {pct}%",
en: "Price rose {pct}% above the grid's upper bound",
},
"log.grid.coverageAuditClose": {
zh: "覆盖审计: 未覆盖 {qty} 且{cause},市价平仓",
en: "Coverage audit: {qty} uncovered and {cause}; closing at market",
},
"log.grid.causeOutOfRange": { zh: "价格已出区间", en: "price left the range" },
"log.grid.causeLossExceeded": { zh: "浮亏超限", en: "unrealised loss exceeded the limit" },
"log.grid.coverageAuditReason": { zh: "覆盖审计止损", en: "Coverage audit stop" },
"log.grid.coverageAuditRepost": {
zh: "覆盖审计: 未覆盖 {qty},补挂平仓单 @ {price}",
en: "Coverage audit: {qty} uncovered; reposting exit @ {price}",
},
"log.grid.shiftOutOfRange": {
zh: "价格越界,启动移格: {reason}",
en: "Price out of range; starting grid shift: {reason}",
},
"log.grid.shiftAnchorDrift": {
zh: "价格偏离锚定价超阈值,启动移格 (anchor={anchor} → {price})",
en: "Price drifted past the anchor threshold; starting grid shift (anchor={anchor} → {price})",
},
"log.grid.adoptOrphanExit": {
zh: "收编平仓方向挂单为孤儿 EXIT: {side} @ {price}",
en: "Adopted an unattributed exit-side order as orphan EXIT: {side} @ {price}",
},
"log.grid.cancelUnattributable": {
zh: "撤销无法归属的挂单: {side} @ {price}",
en: "Cancelling unattributable order: {side} @ {price}",
},
"log.grid.inflightMatched": {
zh: "inflight 归属确认: {intent} {side} @ {price}",
en: "In-flight order matched: {intent} {side} @ {price}",
},
"log.grid.cancelStaleVersion": {
zh: "撤销过期网格版本挂单: {clientOrderId}",
en: "Cancelling order from a stale grid version: {clientOrderId}",
},
"log.grid.orphanResidual": {
zh: "对账残余孤儿仓位: {qty}",
en: "Reconciliation left an orphan position: {qty}",
},
// --- strategy/grid-engine ---
"log.gridEngine.configInvalid": { zh: "配置无效,已暂停网格", en: "Invalid config; grid paused" },
"log.gridEngine.wsDisconnected": {
zh: "WebSocket 断连 ({symbol}),冻结网格下单",
en: "WebSocket disconnected ({symbol}); freezing grid orders",
},
"log.gridEngine.wsReconnected": {
zh: "WebSocket 重连成功 ({symbol}),下一轮执行对账",
en: "WebSocket reconnected ({symbol}); reconciling next tick",
},
"log.gridEngine.loadStateFailed": {
zh: "加载网格状态失败: {error}",
en: "Failed to load grid state: {error}",
},
"log.gridEngine.stateRestored": {
zh: "已从磁盘恢复网格状态: gridVersion={gridVersion} anchor={anchor} 区间=[{lower}, {upper}]{shift}",
en: "Restored grid state from disk: gridVersion={gridVersion} anchor={anchor} range=[{lower}, {upper}]{shift}",
},
"log.gridEngine.stateRestoredShift": {
zh: " 移格续跑({phase})",
en: " resuming shift ({phase})",
},
"log.gridEngine.fingerprintMismatch": {
zh: "磁盘网格状态与当前配置指纹不一致,全新建格并执行孤儿扫描",
en: "Stored grid state does not match the current config; rebuilding and scanning for orphans",
},
"log.gridEngine.gridCreated": {
zh: "以锚定价 {anchor} 建立网格 ({mode})",
en: "Grid created at anchor {anchor} ({mode})",
},
"log.gridEngine.initFailed": { zh: "网格初始化失败: {error}", en: "Grid init failed: {error}" },
"log.gridEngine.reconcileEvent": { zh: "[对账:{source}] {event}", en: "[reconcile:{source}] {event}" },
"log.gridEngine.reconcileCancelled": {
zh: "[对账:{source}] 撤销 {count} 个无法归属的挂单",
en: "[reconcile:{source}] cancelled {count} unattributable orders",
},
"log.gridEngine.reconcileCancelFailed": {
zh: "[对账:{source}] 撤单失败: {error}",
en: "[reconcile:{source}] cancel failed: {error}",
},
"log.gridEngine.reconcileOrdersFailed": {
zh: "[对账:{source}] REST 查询挂单失败: {error}",
en: "[reconcile:{source}] REST open-order query failed: {error}",
},
"log.gridEngine.reconcileAccountFailed": {
zh: "[对账:{source}] REST 查询账户失败: {error}",
en: "[reconcile:{source}] REST account query failed: {error}",
},
"log.gridEngine.tickFailed": { zh: "网格轮询异常: {error}", en: "Grid tick failed: {error}" },
"log.gridEngine.shiftStarting": {
zh: "启动智能移格,目标锚定价 {anchor}",
en: "Starting grid shift to anchor {anchor}",
},
"log.gridEngine.orderFeedStalled": {
zh: "订单流疑似停滞(下单后长时间未反映),暂停新下单",
en: "Order feed looks stalled (placements are not showing up); pausing new orders",
},
"log.gridEngine.placeFailed": {
zh: "挂单失败 ({side} @ {price}): {error}",
en: "Failed to place order ({side} @ {price}): {error}",
},
"log.gridEngine.closeSlippageBlocked": {
zh: "市价平仓滑点守卫触发 ({reason}): close={close} mark={mark} 偏离 {pct}% > {limit}%,暂缓",
en: "Market close blocked by slippage guard ({reason}): close={close} mark={mark} deviates {pct}% > {limit}%; holding off",
},
"log.gridEngine.closed": { zh: "市价平仓 {side} {qty} ({reason})", en: "Market close {side} {qty} ({reason})" },
"log.gridEngine.closeFailed": {
zh: "市价平仓失败 ({reason}): {error}",
en: "Market close failed ({reason}): {error}",
},
"log.gridEngine.shiftCancelRequested": {
zh: "移格: 已请求撤销全部挂单",
en: "Shift: requested cancellation of all orders",
},
"log.gridEngine.shiftCancelFailed": { zh: "移格撤单失败: {error}", en: "Shift cancel failed: {error}" },
"log.gridEngine.shiftCloseReason": { zh: "移格平仓", en: "Grid shift close" },
"log.gridEngine.shiftCloseDeferred": {
zh: "移格: 平仓被滑点守卫暂缓,下轮重试",
en: "Shift: close deferred by the slippage guard; retrying next tick",
},
"log.gridEngine.shiftDone": {
zh: "移格完成: 新锚定价 {anchor},区间 [{lower}, {upper}]gridVersion={gridVersion}",
en: "Shift complete: anchor {anchor}, range [{lower}, {upper}], gridVersion={gridVersion}",
},
"log.gridEngine.stopCancelledFlat": {
zh: "已撤销交易所兜底止损单(仓位归零)",
en: "Cancelled the exchange stop order (position is flat)",
},
"log.gridEngine.stopCancelFailed": {
zh: "撤销兜底止损单失败: {error}",
en: "Failed to cancel the exchange stop: {error}",
},
"log.gridEngine.stopCancelStaleFailed": {
zh: "撤销旧兜底止损单失败: {error}",
en: "Failed to cancel the previous exchange stop: {error}",
},
"log.gridEngine.stopPlaceFailed": {
zh: "挂兜底止损单失败: {error}",
en: "Failed to place the exchange stop: {error}",
},
"log.gridEngine.haltStarting": {
zh: "{reason},开始执行撤单与平仓",
en: "{reason}; cancelling orders and closing out",
},
"log.gridEngine.allCancelled": { zh: "已撤销全部网格挂单", en: "Cancelled all grid orders" },
"log.gridEngine.cancelAllFailed": {
zh: "撤销网格挂单失败: {error}",
en: "Failed to cancel grid orders: {error}",
},
"log.gridEngine.stopCloseDeferred": {
zh: "止损平仓被滑点守卫暂缓,下轮重试",
en: "Stop close deferred by the slippage guard; retrying next tick",
},
"log.gridEngine.resumed": {
zh: "价格重新回到网格区间,恢复网格运行 (gridVersion={gridVersion})",
en: "Price re-entered the grid range; resuming (gridVersion={gridVersion})",
},
"log.gridEngine.saveStateFailed": {
zh: "保存网格状态失败: {error}",
en: "Failed to save grid state: {error}",
},
// --- offset-maker / liquidity-maker (shared wording) ---
"log.subscribe.klineFail": { zh: "订阅K线失败: {error}", en: "Failed to subscribe klines: {error}" },
"log.process.klineError": { zh: "K线推送处理异常: {error}", en: "Kline update handler error: {error}" },
"log.spotMaker.belowMinSellHold": {
zh: "现货持仓低于最小卖单量,暂不挂卖单",
en: "Spot balance is below the minimum sell size; holding off on sell orders",
},
"log.spotMaker.belowMinSellSkip": {
zh: "现货持仓低于最小卖单量,跳过卖单",
en: "Spot balance is below the minimum sell size; skipping the sell order",
},
"log.spotMaker.buyOnlyOnGreenCandle": {
zh: "现货买入仅在1m阳线,当前跳过买单",
en: "Spot buys only on a green 1m candle; skipping the buy order",
},
"log.spotMaker.quoteBalanceShort": {
zh: "现货可用报价资产不足,跳过买单",
en: "Not enough quote asset available; skipping the buy order",
},
"log.spotMaker.baseBalanceShort": {
zh: "现货可用基础资产不足,跳过卖单",
en: "Not enough base asset available; skipping the sell order",
},
"log.spotMaker.spreadTooTightBuy": {
zh: "跳过买单:价差不足以构造maker价格",
en: "Skipping the buy order: the spread is too tight for a maker price",
},
"log.spotMaker.spreadTooTightSell": {
zh: "跳过卖单:价差不足以构造maker价格",
en: "Skipping the sell order: the spread is too tight for a maker price",
},
"log.spotMaker.sellBelowMinNotional": {
zh: "现货卖单低于最小成交量,跳过挂单等待累积",
en: "Sell size is below the venue minimum; waiting to accumulate",
},
"log.spotMaker.belowMinCloseSkipStop": {
zh: "现货持仓低于最小平仓数量,跳过止损检查",
en: "Spot position is below the minimum close size; skipping the stop check",
},
"log.spotMaker.rateLimitCloseMissing": {
zh: "限频强制平仓时订单已不存在",
en: "Order already gone during the rate-limit forced close",
},
"log.spotMaker.rateLimitCloseFailed": {
zh: "限频强制平仓失败: {error}",
en: "Rate-limit forced close failed: {error}",
},
"log.spotMaker.startupCleanup": { zh: "启动时清理历史挂单", en: "Cancelling stale orders on startup" },
"log.spotMaker.startupCleanupGone": {
zh: "历史挂单已消失,跳过启动清理",
en: "Stale orders already gone; skipping startup cleanup",
},
"log.spotMaker.startupCancelFailed": {
zh: "启动撤单失败: {error}",
en: "Startup cancel failed: {error}",
},
"log.spotMaker.cancelMismatched": {
zh: "撤销不匹配订单 {side} @ {price} reduceOnly={reduceOnly}",
en: "Cancelling mismatched order {side} @ {price} reduceOnly={reduceOnly}",
},
"log.spotMaker.cancelAlreadySettled": {
zh: "撤销时发现订单已被成交/取消,忽略",
en: "Order was already filled or cancelled; ignoring",
},
"log.spotMaker.cancelFailed": { zh: "撤销订单失败: {error}", en: "Failed to cancel order: {error}" },
"log.spotMaker.orderMissingOnCancel": {
zh: "订单已不存在,撤销跳过",
en: "Order no longer exists; skipping cancel",
},
"log.spotMaker.dustCloseFailed": {
zh: "小额市价平仓失败: {error}",
en: "Dust market close failed: {error}",
},
"log.spotMaker.dustClose": {
zh: "小额仓位使用市价平仓 {side} 数量 {qty}",
en: "Closing dust position at market: {side} qty {qty}",
},
"log.spotMaker.placeFailed": {
zh: "挂单失败({side} {price}): {error}",
en: "Failed to place order ({side} {price}): {error}",
},
"log.spotMaker.spotStop": {
zh: "现货止损,当前仓位={qty} PnL={pnl} USDT",
en: "Spot stop-loss: position={qty} PnL={pnl} USDT",
},
"log.spotMaker.spotStopFailed": { zh: "现货止损失败: {error}", en: "Spot stop-loss failed: {error}" },
"log.spotMaker.stopCloseMissing": {
zh: "止损平仓时订单已不存在",
en: "Order already gone while closing on stop",
},
"log.spotMaker.stopCloseFailed": { zh: "止损平仓失败: {error}", en: "Stop close failed: {error}" },
"log.spotMaker.entryPricePending": {
zh: "做市持仓均价未同步,等待账户快照刷新后再执行止损判断",
en: "Entry price not synced yet; waiting for an account refresh before evaluating the stop",
},
"log.spotMaker.stopTriggered": {
zh: "触发止损,方向={direction} 当前亏损={pnl} USDT",
en: "Stop-loss triggered: direction={direction} loss={pnl} USDT",
},
"log.spotMaker.updateHandlerError": {
zh: "更新回调处理异常: {error}",
en: "Update handler error: {error}",
},
"log.spotMaker.snapshotDispatchError": {
zh: "快照或更新分发异常: {error}",
en: "Snapshot/update dispatch error: {error}",
},
"log.offsetMaker.tickFailed": { zh: "偏移做市循环异常: {error}", en: "Offset maker tick failed: {error}" },
"log.offsetMaker.imbalanceClose": {
zh: "深度极端不平衡({buySum} vs {sellSum}), 市价平仓 {side}",
en: "Extreme depth imbalance ({buySum} vs {sellSum}); closing {side} at market",
},
"log.offsetMaker.imbalanceCloseMissing": {
zh: "深度不平衡平仓时订单已不存在",
en: "Order already gone during the imbalance close",
},
"log.offsetMaker.imbalanceCloseFailed": {
zh: "深度不平衡平仓失败: {error}",
en: "Imbalance close failed: {error}",
},
"log.liquidityMaker.tickFailed": {
zh: "流动性做市循环异常: {error}",
en: "Liquidity maker tick failed: {error}",
},
"log.liquidityMaker.fillDetected": {
zh: "检测到成交: {side} {qty} @ {price}",
en: "Fill detected: {side} {qty} @ {price}",
},
"log.liquidityMaker.exitRaisedToBreakeven": {
zh: "平仓价调整为入场价+1tick以确保不亏本: {price}",
en: "Exit raised to entry+1 tick to stay at or above breakeven: {price}",
},
"log.liquidityMaker.exitLoweredToBreakeven": {
zh: "平仓价调整为入场价-1tick以确保不亏本: {price}",
en: "Exit lowered to entry-1 tick to stay at or above breakeven: {price}",
},
// --- strategy/maker-points-engine ---
"log.mp.binanceError": { zh: "Binance {context} 异常: {error}", en: "Binance {context} error: {error}" },
"log.mp.binanceDisconnected": { zh: "Binance 深度连接断开", en: "Binance depth feed disconnected" },
"log.mp.binanceStale": { zh: "Binance 深度数据过时", en: "Binance depth data is stale" },
"log.mp.binanceRecovered": { zh: "Binance 深度连接恢复", en: "Binance depth feed recovered" },
"log.mp.wsDisconnected": {
zh: "WebSocket 断连 ({symbol}),启动断连保护",
en: "WebSocket disconnected ({symbol}); engaging disconnect protection",
},
"log.mp.wsReconnected": {
zh: "WebSocket 重连成功 ({symbol}),开始重连保护流程",
en: "WebSocket reconnected ({symbol}); running reconnect protection",
},
"log.mp.reconnectFoundOrders": {
zh: "重连后查询到 {count} 个挂单",
en: "Found {count} open orders after reconnecting",
},
"log.mp.reconnectCancelled": { zh: "重连保护:已取消所有挂单", en: "Reconnect protection: cancelled all orders" },
"log.mp.reconnectCancelPartial": {
zh: "重连保护:取消挂单未完全成功,将在下次循环重试",
en: "Reconnect protection: cancellation incomplete; retrying next tick",
},
"log.mp.reconnectFailed": { zh: "重连保护流程失败: {error}", en: "Reconnect protection failed: {error}" },
"log.mp.closeOnlyEntered": { zh: "进入平仓模式,仅挂 reduce-only", en: "Entered close-only mode; reduce-only quotes" },
"log.mp.closeOnlyExited": { zh: "退出平仓模式", en: "Left close-only mode" },
"log.mp.depthImbalancePause": {
zh: "Binance 深度失衡,暂停 {summary} 挂单",
en: "Binance depth imbalance; pausing {summary} quotes",
},
"log.mp.depthImbalanceResume": { zh: "Binance 深度恢复,继续挂单", en: "Binance depth recovered; resuming quotes" },
"log.mp.rateLimited": { zh: "限频触发,暂停挂单: {error}", en: "Rate limited; pausing quotes: {error}" },
"log.mp.tickFailed": { zh: "MakerPoints 主循环异常: {error}", en: "MakerPoints tick failed: {error}" },
"log.mp.precisionErrorResync": {
zh: "检测到精度错误,重新同步: {error}",
en: "Precision error detected; resyncing: {error}",
},
"log.mp.placeFailed": { zh: "挂单失败 {side} @ {price}: {error}", en: "Failed to place {side} @ {price}: {error}" },
"log.mp.unexpectedOrders": {
zh: "发现 {count} 个未预期挂单,执行强制取消",
en: "Found {count} unexpected orders; force-cancelling",
},
"log.mp.forceCancelled": {
zh: "已强制取消所有挂单,重置本地状态",
en: "Force-cancelled all orders and reset local state",
},
"log.mp.verifyOrdersFailed": { zh: "验证挂单状态失败: {error}", en: "Failed to verify order state: {error}" },
"log.mp.stopTriggered": {
zh: "触发止损: 实时未实现亏损 {pnl} USDT",
en: "Stop-loss triggered: live unrealised loss {pnl} USDT",
},
"log.mp.stopSucceeded": { zh: "止损成功: 仓位已清零", en: "Stop-loss done: position is flat" },
"log.mp.stopOrderMissing": {
zh: "止损平仓时订单已不存在,继续检查仓位",
en: "Order already gone during the stop close; rechecking the position",
},
"log.mp.stopPrecisionResync": {
zh: "止损平仓精度错误,重新同步: {error}",
en: "Precision error during the stop close; resyncing: {error}",
},
"log.mp.stopRetry": {
zh: "止损平仓失败 (重试 {attempt}/{max}): {error}",
en: "Stop close failed (retry {attempt}/{max}): {error}",
},
"log.mp.stopRetriesExhausted": {
zh: "止损重试已达上限 ({max} 次),请手动检查仓位",
en: "Stop retries exhausted ({max}); check the position manually",
},
"log.mp.updateHandlerError": { zh: "更新监听异常: {error}", en: "Update listener error: {error}" },
"log.mp.snapshotError": { zh: "快照生成异常: {error}", en: "Snapshot build error: {error}" },
"log.mp.noTargets": { zh: "暂无目标挂单", en: "No target orders" },
"log.mp.targets": { zh: "目标挂单: {summary}", en: "Target orders: {summary}" },
"log.mp.skipThinDepth": {
zh: "跳过 {side} {bps}bps 挂单: 深度 {depth} BTC < {min} BTC",
en: "Skipping {side} {bps}bps quote: depth {depth} BTC < {min} BTC",
},
"log.mp.depthRecovered": {
zh: "{side} {bps}bps 深度恢复,继续挂单",
en: "{side} {bps}bps depth recovered; resuming quotes",
},
"log.mp.insufficientBalance": {
zh: "余额不足,暂停挂单 {seconds}s: {detail}",
en: "Insufficient balance; pausing quotes for {seconds}s: {detail}",
},
"log.mp.balanceRecovered": { zh: "余额恢复,继续挂单", en: "Balance recovered; resuming quotes" },
"log.mp.defenseEntered": {
zh: "数据过时检测: {summary},进入防御模式",
en: "Stale-data check: {summary}; entering defense mode",
},
"log.mp.defenseExited": {
zh: "数据推送恢复正常,退出防御模式",
en: "Data feeds recovered; leaving defense mode",
},
"log.mp.defenseForceCancelled": {
zh: "防御模式: 已强制取消所有挂单",
en: "Defense mode: force-cancelled all orders",
},
"log.mp.defenseCancelPartial": {
zh: "防御模式: 取消挂单未完全成功,将继续重试",
en: "Defense mode: cancellation incomplete; will retry",
},
"log.mp.defenseCancelled": { zh: "防御模式: 已取消所有挂单", en: "Defense mode: cancelled all orders" },
"log.mp.defenseOrdersGone": { zh: "防御模式: 挂单已不存在", en: "Defense mode: orders already gone" },
"log.mp.defenseCancelFailed": {
zh: "防御模式取消挂单失败: {error}",
en: "Defense mode cancel failed: {error}",
},
"log.mp.defensePollStarted": {
zh: "防御模式: 启动 REST 数据轮询",
en: "Defense mode: started REST polling",
},
"log.mp.defensePollStopped": {
zh: "防御模式: 停止 REST 数据轮询",
en: "Defense mode: stopped REST polling",
},
"log.mp.defensePositionStillBad": {
zh: "防御模式: 仓位数据仍异常: {issues}",
en: "Defense mode: position data still invalid: {issues}",
},
"log.mp.defenseEmptySnapshot": {
zh: "防御模式: REST 获取账户快照为空",
en: "Defense mode: REST returned an empty account snapshot",
},
"log.mp.defenseFoundOrders": {
zh: "防御模式: 发现 {count} 个挂单,执行取消",
en: "Defense mode: found {count} open orders; cancelling",
},
"log.mp.defenseQueryFailed": {
zh: "防御模式查询挂单失败: {error}",
en: "Defense mode open-order query failed: {error}",
},
"log.mp.defensePollFailed": {
zh: "防御模式 REST 轮询失败: {error}",
en: "Defense mode REST poll failed: {error}",
},
"notify.mp.disconnectTitle": { zh: "连接断开", en: "Disconnected" },
"notify.mp.disconnectBody": {
zh: "WebSocket 断连,正在尝试取消所有挂单",
en: "WebSocket disconnected; cancelling all open orders",
},
"notify.mp.reconnectTitle": { zh: "重连完成", en: "Reconnected" },
"notify.mp.reconnectBody": {
zh: "WebSocket 重连成功,已清理挂单状态",
en: "WebSocket reconnected; order state cleaned up",
},
"notify.mp.stopTitle": { zh: "止损触发", en: "Stop-loss triggered" },
"notify.mp.stopBody": {
zh: "实时未实现亏损 {pnl} USDT,强制平仓",
en: "Live unrealised loss {pnl} USDT; forcing a close",
},
"notify.mp.defenseTitle": { zh: "防御模式", en: "Defense mode" },
"notify.mp.defenseBody": {
zh: "数据推送中断: {summary},已取消所有挂单",
en: "Data feed interrupted: {summary}; cancelled all open orders",
},
"notify.mp.defenseClearedTitle": { zh: "防御模式解除", en: "Defense mode cleared" },
"notify.mp.defenseClearedBody": {
zh: "数据推送恢复正常,恢复正常交易",
en: "Data feeds are healthy again; resuming normal trading",
},
"notify.mp.openTitle": { zh: "开仓", en: "Position opened" },
"notify.mp.closeTitle": { zh: "平仓", en: "Position closed" },
"notify.mp.closeTitleTokenExpired": { zh: "Token过期平仓", en: "Token-expiry close" },
"notify.mp.increaseTitle": { zh: "加仓", en: "Position increased" },
"notify.mp.reduceTitle": { zh: "减仓", en: "Position reduced" },
"notify.mp.reverseTitle": { zh: "反向开仓", en: "Position reversed" },
"notify.mp.openBody": { zh: "{direction} {qty}", en: "{direction} {qty}" },
"notify.mp.closeBody": { zh: "已平仓 {qty} ({direction})", en: "Closed {qty} ({direction})" },
"notify.mp.increaseBody": {
zh: "{direction} +{delta} → {qty}",
en: "{direction} +{delta} → {qty}",
},
"notify.mp.reduceBody": { zh: "{direction} -{delta} → {qty}", en: "{direction} -{delta} → {qty}" },
"notify.mp.reverseBody": { zh: "{transition} {qty}", en: "{transition} {qty}" },
"common.direction.longToShort": { zh: "多→空", en: "long → short" },
"common.direction.shortToLong": { zh: "空→多", en: "short → long" },
// --- strategy/maker-points-defense (stale-reason summary) ---
"defense.reason.depth": { zh: "StandX深度({seconds}s)", en: "StandX depth ({seconds}s)" },
"defense.reason.account": { zh: "StandX账户({seconds}s)", en: "StandX account ({seconds}s)" },
"defense.reason.accountInvalid": {
zh: "StandX仓位数据异常({issues})",
en: "StandX position data invalid ({issues})",
},
"defense.reason.rest": { zh: "StandX REST错误({count}次)", en: "StandX REST errors ({count})" },
"defense.reason.marginMode": { zh: "保证金模式({mode})", en: "Margin mode ({mode})" },
"defense.reason.binanceDepth": { zh: "Binance深度({seconds}s)", en: "Binance depth ({seconds}s)" },
"defense.reason.binanceBook": {
zh: "Binance簿记异常({reason})",
en: "Binance order book unhealthy ({reason})",
},
"defense.reason.unknown": { zh: "unknown", en: "unknown" },
};
const formatTemplate = (template: string, params: Record<string, unknown>): string => {
+1 -1
View File
@@ -521,7 +521,7 @@ export class BasisArbEngine {
const spotTs = snapshot.spotLastUpdate ?? 0;
if (futTs <= readyAt || spotTs <= readyAt) return;
const now = this.now();
// Use net spread after taker fees to match UI's "扣除 taker 手续费" bp
// Use net spread after taker fees to match the bp figure the UI labels as taker-fee-adjusted
const spreadBps = snapshot.netSpreadBps;
const fundingRate = snapshot.fundingRate;
const nextFundingTime = snapshot.nextFundingTime;
+67 -48
View File
@@ -15,6 +15,7 @@ import {
type OrderPendingMap,
type OrderTimerMap,
} from "../core/order-coordinator";
import { t } from "../i18n";
import { StrategyEventEmitter } from "./common/event-emitter";
import { createPrecisionSyncer, type PrecisionSyncer } from "./common/precision-syncer";
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
@@ -186,7 +187,7 @@ export class GridEngine {
this.configValid = this.validateConfig();
this.running = this.configValid;
if (!this.configValid) {
this.stopReason = "配置无效,已暂停网格";
this.stopReason = t("log.gridEngine.configInvalid");
this.log("error", this.stopReason);
}
this.precision = createPrecisionSyncer(this.exchange, this.config, this.config.qtyStep, this.log);
@@ -306,15 +307,15 @@ export class GridEngine {
this.accountVersion += 1;
if (!this.feedArrived.account) {
this.feedArrived.account = true;
this.log("info", "账户快照已同步");
this.log("info", t("log.account.snapshotSynced"));
}
this.feedStatus.account = true;
this.emitUpdate();
},
this.log,
{
subscribeFail: (error) => `订阅账户失败: ${extractMessage(error)}`,
processFail: (error) => `账户推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.accountFail", { error: extractMessage(error) }),
processFail: (error) => t("log.process.accountError", { error: extractMessage(error) }),
}
);
@@ -329,7 +330,7 @@ export class GridEngine {
this.ordersFeedLastAt = this.now();
if (!this.feedArrived.orders) {
this.feedArrived.orders = true;
this.log("info", "订单快照已同步");
this.log("info", t("log.order.snapshotReturned"));
}
this.feedStatus.orders = true;
void this.attemptInit();
@@ -337,8 +338,8 @@ export class GridEngine {
},
this.log,
{
subscribeFail: (error) => `订阅订单失败: ${extractMessage(error)}`,
processFail: (error) => `订单推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.orderFail", { error: extractMessage(error) }),
processFail: (error) => t("log.process.orderError", { error: extractMessage(error) }),
}
);
@@ -348,14 +349,14 @@ export class GridEngine {
this.depthSnapshot = depth;
if (!this.feedArrived.depth) {
this.feedArrived.depth = true;
this.log("info", "盘口深度已同步");
this.log("info", t("log.depth.ready"));
}
this.feedStatus.depth = true;
},
this.log,
{
subscribeFail: (error) => `订阅深度失败: ${extractMessage(error)}`,
processFail: (error) => `深度推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.depthFail", { error: extractMessage(error) }),
processFail: (error) => t("log.process.depthError", { error: extractMessage(error) }),
}
);
@@ -366,7 +367,7 @@ export class GridEngine {
this.tickerLastAt = this.now();
if (!this.feedArrived.ticker) {
this.feedArrived.ticker = true;
this.log("info", "行情推送已同步");
this.log("info", t("log.ticker.ready"));
}
this.feedStatus.ticker = true;
void this.attemptInit();
@@ -374,8 +375,8 @@ export class GridEngine {
},
this.log,
{
subscribeFail: (error) => `订阅行情失败: ${extractMessage(error)}`,
processFail: (error) => `行情推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.tickerFail", { error: extractMessage(error) }),
processFail: (error) => t("log.process.tickerError", { error: extractMessage(error) }),
}
);
}
@@ -385,11 +386,11 @@ export class GridEngine {
this.exchange.onConnectionEvent((event, symbol) => {
if (event === "disconnected") {
this.frozen = true;
this.log("warn", `WebSocket 断连 (${symbol}),冻结网格下单`);
this.log("warn", t("log.gridEngine.wsDisconnected", { symbol }));
} else if (event === "reconnected") {
this.frozen = false;
this.restReconcilePending = true;
this.log("info", `WebSocket 重连成功 (${symbol}),下一轮执行对账`);
this.log("info", t("log.gridEngine.wsReconnected", { symbol }));
}
this.emitUpdate();
});
@@ -482,7 +483,7 @@ export class GridEngine {
try {
stored = await loadGridState(this.config.symbol);
} catch (err) {
this.log("error", `加载网格状态失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.loadStateFailed", { error: extractMessage(err) }));
}
}
const meta = this.stateMeta();
@@ -490,20 +491,27 @@ export class GridEngine {
this.state = fromStored(stored, this.logicSettings(), price);
this.log(
"info",
`已从磁盘恢复网格状态: gridVersion=${this.state.gridVersion} anchor=${this.state.anchorPrice} ` +
`区间=[${this.state.lowerPrice}, ${this.state.upperPrice}]${this.state.shift ? ` 移格续跑(${this.state.shift.phase})` : ""}`
t("log.gridEngine.stateRestored", {
gridVersion: this.state.gridVersion,
anchor: this.state.anchorPrice,
lower: this.state.lowerPrice,
upper: this.state.upperPrice,
shift: this.state.shift
? t("log.gridEngine.stateRestoredShift", { phase: this.state.shift.phase })
: "",
})
);
} else {
if (stored) {
this.log("warn", "磁盘网格状态与当前配置指纹不一致,全新建格并执行孤儿扫描");
this.log("warn", t("log.gridEngine.fingerprintMismatch"));
}
this.state = createInitialState(this.logicSettings(), price);
this.log("info", `以锚定价 ${this.state.anchorPrice} 建立网格 (${this.tradeMode})`);
this.log("info", t("log.gridEngine.gridCreated", { anchor: this.state.anchorPrice, mode: this.tradeMode }));
}
await this.applyReconcile(this.openOrders, "startup");
this.initDone = true;
} catch (err) {
this.log("error", `网格初始化失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.initFailed", { error: extractMessage(err) }));
this.initStarted = false;
}
this.emitUpdate();
@@ -523,7 +531,7 @@ export class GridEngine {
now: this.now(),
});
for (const event of result.events) {
this.log("info", `[对账:${source}] ${event}`);
this.log("info", t("log.gridEngine.reconcileEvent", { source, event }));
}
if (result.cancelOrderIds.length > 0) {
try {
@@ -531,10 +539,10 @@ export class GridEngine {
symbol: this.config.symbol,
orderIdList: result.cancelOrderIds,
});
this.log("order", `[对账:${source}] 撤销 ${result.cancelOrderIds.length} 个无法归属的挂单`);
this.log("order", t("log.gridEngine.reconcileCancelled", { source, count: result.cancelOrderIds.length }));
} catch (err) {
if (!isUnknownOrderError(err)) {
this.log("error", `[对账:${source}] 撤单失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.reconcileCancelFailed", { source, error: extractMessage(err) }));
}
}
}
@@ -550,7 +558,7 @@ export class GridEngine {
const fetched = await this.exchange.queryOpenOrders();
orders = fetched.filter((order) => order.symbol === this.config.symbol);
} catch (err) {
this.log("error", `[对账:${source}] REST 查询挂单失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.reconcileOrdersFailed", { source, error: extractMessage(err) }));
}
}
if (this.exchange.queryAccountSnapshot) {
@@ -561,7 +569,7 @@ export class GridEngine {
this.accountVersion += 1;
}
} catch (err) {
this.log("error", `[对账:${source}] REST 查询账户失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.reconcileAccountFailed", { source, error: extractMessage(err) }));
}
}
if (orders) {
@@ -645,7 +653,7 @@ export class GridEngine {
this.schedulePersist();
}
} catch (error) {
this.log("error", `网格轮询异常: ${extractMessage(error)}`);
this.log("error", t("log.gridEngine.tickFailed", { error: extractMessage(error) }));
} finally {
this.processing = false;
this.emitUpdate();
@@ -661,7 +669,7 @@ export class GridEngine {
}
if (action.kind === "BEGIN_SHIFT") {
// 移格标记已由 planTick 写入 state,落盘后由下个 tick 开始执行
this.log("warn", `启动智能移格,目标锚定价 ${action.targetAnchor}`);
this.log("warn", t("log.gridEngine.shiftStarting", { anchor: action.targetAnchor }));
await this.persistNow();
return;
}
@@ -692,7 +700,7 @@ export class GridEngine {
) {
if (now - this.lastStalenessLogAt > 30_000) {
this.lastStalenessLogAt = now;
this.log("warn", "订单流疑似停滞(下单后长时间未反映),暂停新下单");
this.log("warn", t("log.gridEngine.orderFeedStalled"));
}
return false;
}
@@ -741,7 +749,7 @@ export class GridEngine {
clientOrderId,
});
} catch (error) {
this.log("error", `挂单失败 (${action.side} @ ${action.price}): ${extractMessage(error)}`);
this.log("error", t("log.gridEngine.placeFailed", { side: action.side, price: action.price, error: extractMessage(error) }));
}
state.inflight = null;
@@ -799,7 +807,13 @@ export class GridEngine {
if (pctDiff > limitPct) {
this.log(
"warn",
`市价平仓滑点守卫触发 (${reason}): close=${closeSidePrice} mark=${mark} 偏离 ${(pctDiff * 100).toFixed(2)}% > ${(limitPct * 100).toFixed(2)}%,暂缓`
t("log.gridEngine.closeSlippageBlocked", {
reason,
close: closeSidePrice,
mark,
pct: (pctDiff * 100).toFixed(2),
limit: (limitPct * 100).toFixed(2),
})
);
return false;
}
@@ -816,10 +830,10 @@ export class GridEngine {
},
qtyStep: this.config.qtyStep
});
this.log("close", `市价平仓 ${side} ${qty} (${reason})`);
this.log("close", t("log.gridEngine.closed", { side, qty, reason }));
return true;
} catch (error) {
this.log("error", `市价平仓失败 (${reason}): ${extractMessage(error)}`);
this.log("error", t("log.gridEngine.closeFailed", { reason, error: extractMessage(error) }));
return false;
} finally {
unlockOperating(this.locks, this.timers, this.pendings, "MARKET");
@@ -843,9 +857,9 @@ export class GridEngine {
try {
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
state.exchangeStop = null;
this.log("order", "移格: 已请求撤销全部挂单");
this.log("order", t("log.gridEngine.shiftCancelRequested"));
} catch (err) {
this.log("error", `移格撤单失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.shiftCancelFailed", { error: extractMessage(err) }));
}
} else if (step.kind === "CLOSE_POSITION") {
// 平仓单已提交但仓位回报未到时不重复提交
@@ -853,19 +867,24 @@ export class GridEngine {
this.shiftCloseAccountVersion === this.accountVersion &&
this.now() - this.shiftCloseAt < 10_000;
if (!awaitingFill) {
const done = await this.guardedMarketClose(step.side, step.qty, "移格平仓");
const done = await this.guardedMarketClose(step.side, step.qty, t("log.gridEngine.shiftCloseReason"));
if (done) {
this.shiftCloseAccountVersion = this.accountVersion;
this.shiftCloseAt = this.now();
} else {
this.log("info", "移格: 平仓被滑点守卫暂缓,下轮重试");
this.log("info", t("log.gridEngine.shiftCloseDeferred"));
}
}
} else if (step.kind === "REBUILD") {
applyRebuild(state, this.logicSettings(), step.anchor);
this.log(
"info",
`移格完成: 新锚定价 ${step.anchor},区间 [${state.lowerPrice.toFixed(4)}, ${state.upperPrice.toFixed(4)}]gridVersion=${state.gridVersion}`
t("log.gridEngine.shiftDone", {
anchor: step.anchor,
lower: state.lowerPrice.toFixed(4),
upper: state.upperPrice.toFixed(4),
gridVersion: state.gridVersion,
})
);
}
this.lastUpdated = this.now();
@@ -905,10 +924,10 @@ export class GridEngine {
if (live) {
try {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: existing.orderId });
this.log("order", "已撤销交易所兜底止损单(仓位归零)");
this.log("order", t("log.gridEngine.stopCancelledFlat"));
} catch (err) {
if (!isUnknownOrderError(err)) {
this.log("error", `撤销兜底止损单失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.stopCancelFailed", { error: extractMessage(err) }));
}
}
}
@@ -932,7 +951,7 @@ export class GridEngine {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: existing.orderId });
} catch (err) {
if (!isUnknownOrderError(err)) {
this.log("error", `撤销旧兜底止损单失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.stopCancelStaleFailed", { error: extractMessage(err) }));
return;
}
}
@@ -961,7 +980,7 @@ export class GridEngine {
this.schedulePersist();
}
} catch (err) {
this.log("error", `挂兜底止损单失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.stopPlaceFailed", { error: extractMessage(err) }));
}
}
@@ -972,12 +991,12 @@ export class GridEngine {
private async haltGrid(reason: string): Promise<void> {
const state = this.state;
this.stopReason = reason;
this.log("warn", `${reason},开始执行撤单与平仓`);
this.log("warn", t("log.gridEngine.haltStarting", { reason }));
try {
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
this.log("order", "已撤销全部网格挂单");
this.log("order", t("log.gridEngine.allCancelled"));
} catch (error) {
this.log("error", `撤销网格挂单失败: ${extractMessage(error)}`);
this.log("error", t("log.gridEngine.cancelAllFailed", { error: extractMessage(error) }));
}
if (state) state.exchangeStop = null;
const qty = this.position.positionAmt;
@@ -985,7 +1004,7 @@ export class GridEngine {
const closed = await this.guardedMarketClose(qty > 0 ? "SELL" : "BUY", Math.abs(qty), reason);
if (!closed) {
// 滑点守卫暂缓:保持 running,下个 tick 重新触发层①重试
this.log("warn", "止损平仓被滑点守卫暂缓,下轮重试");
this.log("warn", t("log.gridEngine.stopCloseDeferred"));
return;
}
}
@@ -1038,7 +1057,7 @@ export class GridEngine {
this.running = true;
this.stopReason = null;
this.initDone = true;
this.log("info", `价格重新回到网格区间,恢复网格运行 (gridVersion=${nextVersion})`);
this.log("info", t("log.gridEngine.resumed", { gridVersion: nextVersion }));
await this.persistNow();
this.start();
}
@@ -1064,7 +1083,7 @@ export class GridEngine {
try {
await saveGridState(toStored(state, this.stateMeta(), this.now()));
} catch (err) {
this.log("error", `保存网格状态失败: ${extractMessage(err)}`);
this.log("error", t("log.gridEngine.saveStateFailed", { error: extractMessage(err) }));
}
}
+7 -2
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { t } from "../i18n";
import {
ORPHAN_LEVEL,
applyRebuild,
@@ -552,8 +553,12 @@ describe("resolveAwaiting", () => {
describe("checkPriceStop", () => {
it("triggers below lower and above upper thresholds", () => {
const state = createInitialState(settings, 141.4);
expect(checkPriceStop(state, settings, 98.9)).toContain("跌破");
expect(checkPriceStop(state, settings, 202.1)).toContain("突破");
expect(checkPriceStop(state, settings, 98.9)).toBe(
t("log.grid.belowLowerBound", { pct: "1.10" })
);
expect(checkPriceStop(state, settings, 202.1)).toBe(
t("log.grid.aboveUpperBound", { pct: "1.05" })
);
expect(checkPriceStop(state, settings, 150)).toBeNull();
expect(checkPriceStop(state, settings, 99.5)).toBeNull(); // 1% 容忍内
});
+24 -18
View File
@@ -1,3 +1,4 @@
import { t } from "../i18n";
// 网格纯逻辑:无 I/O、无 Date.now、无 adapter 引用。所有时间通过参数传入。
// 引擎每 tick 调 planTick(state, settings, input) 得到 actions,由引擎负责执行。
@@ -417,11 +418,11 @@ function applyFilled(
level.phase = "holding";
level.holdQty = qty;
delete level.entryOrderId;
events.push(`ENTRY 成交: ${intent.side} @ ${intent.price} (线 ${intent.level})`);
events.push(t("log.grid.entryFilled", { side: intent.side, price: intent.price, level: intent.level }));
}
} else {
if (intent.level === ORPHAN_LEVEL) {
events.push(`孤儿 EXIT 成交: ${intent.side} @ ${intent.price}`);
events.push(t("log.grid.orphanExitFilled", { side: intent.side, price: intent.price }));
return;
}
const level = state.levels[intent.level];
@@ -429,7 +430,7 @@ function applyFilled(
level.phase = "idle";
level.holdQty = 0;
delete level.exitOrderId;
events.push(`EXIT 成交: ${intent.side} @ ${intent.price} (释放线 ${intent.level})`);
events.push(t("log.grid.exitFilled", { side: intent.side, price: intent.price, level: intent.level }));
}
}
}
@@ -441,7 +442,7 @@ function applyCanceled(state: GridLogicState, intent: OrderIntentRecord, events:
level.phase = "idle";
delete level.entryOrderId;
}
events.push(`ENTRY 撤销: ${intent.side} @ ${intent.price} (线 ${intent.level})`);
events.push(t("log.grid.entryCancelled", { side: intent.side, price: intent.price, level: intent.level }));
} else {
if (intent.level === ORPHAN_LEVEL) return;
const level = state.levels[intent.level];
@@ -449,7 +450,7 @@ function applyCanceled(state: GridLogicState, intent: OrderIntentRecord, events:
level.phase = "holding";
delete level.exitOrderId;
}
events.push(`EXIT 撤销: ${intent.side} @ ${intent.price} (线 ${intent.level})`);
events.push(t("log.grid.exitCancelled", { side: intent.side, price: intent.price, level: intent.level }));
}
}
@@ -502,7 +503,9 @@ export function processOrderSnapshot(
applyCanceled(state, intent, events);
} else {
setAwaiting(state, intent, input);
events.push(`订单消失待判定: ${intent.intent} ${intent.side} @ ${intent.price}`);
events.push(
t("log.grid.orderVanished", { intent: intent.intent, side: intent.side, price: intent.price })
);
}
state.intents.delete(id);
state.seenOrderIds.delete(id);
@@ -669,10 +672,10 @@ export function checkPriceStop(
const lowerTrigger = state.lowerPrice * (1 - settings.stopLossPct);
const upperTrigger = state.upperPrice * (1 + settings.stopLossPct);
if (price <= lowerTrigger) {
return `价格跌破网格下边界 ${((1 - price / state.lowerPrice) * 100).toFixed(2)}%`;
return t("log.grid.belowLowerBound", { pct: ((1 - price / state.lowerPrice) * 100).toFixed(2) });
}
if (price >= upperTrigger) {
return `价格突破网格上边界 ${((price / state.upperPrice - 1) * 100).toFixed(2)}%`;
return t("log.grid.aboveUpperBound", { pct: ((price / state.upperPrice - 1) * 100).toFixed(2) });
}
return null;
}
@@ -731,17 +734,20 @@ export function auditExitCoverage(
state.uncoveredSince = input.now;
if (outOfRange || deepLoss) {
events.push(
`覆盖审计: 未覆盖 ${uncovered.toFixed(6)}${outOfRange ? "价格已出区间" : "浮亏超限"},市价平仓`
t("log.grid.coverageAuditClose", {
qty: uncovered.toFixed(6),
cause: outOfRange ? t("log.grid.causeOutOfRange") : t("log.grid.causeLossExceeded"),
})
);
return {
uncoveredQty: uncovered,
action: { kind: "MARKET_CLOSE", side: exitSide, qty: uncovered, reason: "覆盖审计止损" },
action: { kind: "MARKET_CLOSE", side: exitSide, qty: uncovered, reason: t("log.grid.coverageAuditReason") },
events,
};
}
// 最近可盈利线补挂孤儿 EXIT
const targetPrice = findNearestProfitableExitPrice(state, pos > 0 ? "long" : "short", entry, input.price);
events.push(`覆盖审计: 未覆盖 ${uncovered.toFixed(6)},补挂平仓单 @ ${targetPrice}`);
events.push(t("log.grid.coverageAuditRepost", { qty: uncovered.toFixed(6), price: targetPrice }));
return {
uncoveredQty: uncovered,
action: {
@@ -918,7 +924,7 @@ export function planTick(
if (settings.shiftEnabled && !state.shift) {
beginShift(state, input.price, input.now);
actions.push({ kind: "BEGIN_SHIFT", targetAnchor: input.price });
events.push(`价格越界,启动移格: ${stopReason}`);
events.push(t("log.grid.shiftOutOfRange", { reason: stopReason }));
return { actions, events, stateChanged: true, uncoveredQty: 0 };
}
actions.push({ kind: "HALT", reason: stopReason });
@@ -929,7 +935,7 @@ export function planTick(
if (shouldShift(state, settings, input.price, input.now)) {
beginShift(state, input.price, input.now);
actions.push({ kind: "BEGIN_SHIFT", targetAnchor: input.price });
events.push(`价格偏离锚定价超阈值,启动移格 (anchor=${state.anchorPrice}${input.price})`);
events.push(t("log.grid.shiftAnchorDrift", { anchor: state.anchorPrice, price: input.price }));
return { actions, events, stateChanged: true, uncoveredQty: 0 };
}
@@ -1034,11 +1040,11 @@ export function reconcile(
gridVersion: state.gridVersion,
createdAt: input.now,
});
events.push(`收编平仓方向挂单为孤儿 EXIT: ${order.side} @ ${order.price}`);
events.push(t("log.grid.adoptOrphanExit", { side: order.side, price: order.price }));
return;
}
cancelOrderIds.push(order.orderId);
events.push(`撤销无法归属的挂单: ${order.side} @ ${order.price}`);
events.push(t("log.grid.cancelUnattributable", { side: order.side, price: order.price }));
};
for (const order of input.activeOrders) {
@@ -1079,7 +1085,7 @@ export function reconcile(
rec.intent === "ENTRY" ? adoptEntry(order, rec.level, intent) : adoptExit(order, rec.level, intent);
state.inflight = null;
if (ok) {
events.push(`inflight 归属确认: ${rec.intent} ${rec.side} @ ${rec.price}`);
events.push(t("log.grid.inflightMatched", { intent: rec.intent, side: rec.side, price: rec.price }));
continue;
}
fallbackAdopt(order, remaining);
@@ -1090,7 +1096,7 @@ export function reconcile(
if (parsed) {
if (parsed.gridVersion != null && parsed.gridVersion !== state.gridVersion) {
cancelOrderIds.push(order.orderId);
events.push(`撤销过期网格版本挂单: ${order.clientOrderId}`);
events.push(t("log.grid.cancelStaleVersion", { clientOrderId: order.clientOrderId }));
continue;
}
const intent: OrderIntentRecord = {
@@ -1231,7 +1237,7 @@ export function reconcile(
diff = 0;
}
if (Math.abs(diff) > eps) {
events.push(`对账残余孤儿仓位: ${diff.toFixed(6)}`);
events.push(t("log.grid.orphanResidual", { qty: diff.toFixed(6) }));
// 立即进入层②处置(跳过宽限期)
state.uncoveredSince = input.now - 86_400_000;
}
+60 -48
View File
@@ -26,6 +26,7 @@ import type { MakerEngineSnapshot } from "./maker-engine";
import { makeOrderPlan } from "../core/lib/order-plan";
import { safeCancelOrder } from "../core/lib/orders";
import { RateLimitController } from "../core/lib/rate-limit";
import { t } from "../i18n";
import { StrategyEventEmitter } from "./common/event-emitter";
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
import { SessionVolumeTracker } from "./common/session-volume";
@@ -233,8 +234,8 @@ export class LiquidityMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅账户失败: ${String(error)}`,
processFail: (error) => `账户推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.accountFail", { error: String(error) }),
processFail: (error) => t("log.process.accountError", { error: String(error) }),
}
);
@@ -277,8 +278,8 @@ export class LiquidityMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅订单失败: ${String(error)}`,
processFail: (error) => `订单推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.orderFail", { error: String(error) }),
processFail: (error) => t("log.process.orderError", { error: String(error) }),
}
);
@@ -291,8 +292,8 @@ export class LiquidityMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅深度失败: ${String(error)}`,
processFail: (error) => `深度推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.depthFail", { error: String(error) }),
processFail: (error) => t("log.process.depthError", { error: String(error) }),
}
);
@@ -305,8 +306,8 @@ export class LiquidityMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅Ticker失败: ${String(error)}`,
processFail: (error) => `价格推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.tickerFail", { error: String(error) }),
processFail: (error) => t("log.process.tickerError", { error: String(error) }),
}
);
@@ -325,8 +326,8 @@ export class LiquidityMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅K线失败: ${String(error)}`,
processFail: (error) => `K线推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.klineFail", { error: String(error) }),
processFail: (error) => t("log.process.klineError", { error: String(error) }),
}
);
}
@@ -364,7 +365,11 @@ export class LiquidityMakerEngine {
this.tradeLog.push(
"order",
`检测到成交: ${order.side} ${filledQty.toFixed(6)} @ ${avgPrice.toFixed(this.getPriceDecimals())}`
t("log.liquidityMaker.fillDetected", {
side: order.side,
qty: filledQty.toFixed(6),
price: avgPrice.toFixed(this.getPriceDecimals()),
})
);
}
}
@@ -487,13 +492,13 @@ export class LiquidityMakerEngine {
// 无法卖出,跳过卖单,允许买单累计
this.lastSellPriceViable = false;
if (!skipSellSide) {
this.tradeLog.push("info", "现货持仓低于最小卖单量,暂不挂卖单");
this.tradeLog.push("info", t("log.spotMaker.belowMinSellHold"));
}
}
if (!skipBuySide && canEnter) {
if (!allowSpotBuy) {
if (this.lastBuyPriceViable) {
this.tradeLog.push("info", "现货买入仅在1m阳线,当前跳过买单");
this.tradeLog.push("info", t("log.spotMaker.buyOnlyOnGreenCandle"));
this.lastBuyPriceViable = false;
}
} else {
@@ -510,8 +515,8 @@ export class LiquidityMakerEngine {
this.lastBuyPriceViable = false;
const reason =
buyAmount < EPS && isSpotMarket
? "现货可用报价资产不足,跳过买单"
: "跳过买单:价差不足以构造maker价格";
? t("log.spotMaker.quoteBalanceShort")
: t("log.spotMaker.spreadTooTightBuy");
this.tradeLog.push("info", reason);
}
}
@@ -524,7 +529,7 @@ export class LiquidityMakerEngine {
// 持仓低于最小卖单量,跳过卖单,等待累积
if (this.lastSellPriceViable) {
this.lastSellPriceViable = false;
this.tradeLog.push("info", "现货持仓低于最小卖单量,跳过卖单");
this.tradeLog.push("info", t("log.spotMaker.belowMinSellSkip"));
}
} else {
const desiredSellAmount =
@@ -542,8 +547,8 @@ export class LiquidityMakerEngine {
this.lastSellPriceViable = false;
const reason =
sellAmount < EPS && isSpotMarket
? "现货可用基础资产不足,跳过卖单"
: "跳过卖单:价差不足以构造maker价格";
? t("log.spotMaker.baseBalanceShort")
: t("log.spotMaker.spreadTooTightSell");
this.tradeLog.push("info", reason);
}
}
@@ -554,7 +559,7 @@ export class LiquidityMakerEngine {
if (!skipBuySide && canEnter) {
if (isSpotMarket && !allowSpotBuy) {
if (this.lastBuyPriceViable) {
this.tradeLog.push("info", "现货买入仅在1m阳线,当前跳过买单");
this.tradeLog.push("info", t("log.spotMaker.buyOnlyOnGreenCandle"));
this.lastBuyPriceViable = false;
}
} else if (bidPrice != null) {
@@ -567,7 +572,7 @@ export class LiquidityMakerEngine {
const baseWallet = balancesForSpot?.baseWallet ?? baseAvail;
if (Math.max(baseAvail, baseWallet) + EPS < minSell) {
this.lastSellPriceViable = false;
this.tradeLog.push("info", "现货持仓低于最小卖单量,跳过卖单");
this.tradeLog.push("info", t("log.spotMaker.belowMinSellSkip"));
}
}
if (askPrice != null) {
@@ -622,7 +627,7 @@ export class LiquidityMakerEngine {
await this.enforceRateLimitStop();
this.tradeLog.push("warn", `LiquidityMakerEngine 429: ${String(error)}`);
} else {
this.tradeLog.push("error", `流动性做市循环异常: ${String(error)}`);
this.tradeLog.push("error", t("log.liquidityMaker.tickFailed", { error: String(error) }));
}
this.emitUpdate();
} finally {
@@ -679,13 +684,13 @@ export class LiquidityMakerEngine {
// 多头平仓:卖价必须 >= 入场价
if (targetPrice < entryPrice) {
targetPrice = entryPrice + this.precision.priceTick;
this.tradeLog.push("info", `平仓价调整为入场价+1tick以确保不亏本: ${targetPrice.toFixed(priceDecimals)}`);
this.tradeLog.push("info", t("log.liquidityMaker.exitRaisedToBreakeven", { price: targetPrice.toFixed(priceDecimals) }));
}
} else {
// 空头平仓:买价必须 <= 入场价
if (targetPrice > entryPrice) {
targetPrice = entryPrice - this.precision.priceTick;
this.tradeLog.push("info", `平仓价调整为入场价-1tick以确保不亏本: ${targetPrice.toFixed(priceDecimals)}`);
this.tradeLog.push("info", t("log.liquidityMaker.exitLoweredToBreakeven", { price: targetPrice.toFixed(priceDecimals) }));
}
}
}
@@ -727,9 +732,9 @@ export class LiquidityMakerEngine {
});
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
this.tradeLog.push("order", t("log.spotMaker.rateLimitCloseMissing"));
} else {
this.tradeLog.push("error", `限频强制平仓失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.rateLimitCloseFailed", { error: String(error) }));
}
}
}
@@ -747,18 +752,18 @@ export class LiquidityMakerEngine {
unlockOperating(this.locks, this.timers, this.pending, "LIMIT");
this.openOrders = [];
this.emitUpdate();
this.tradeLog.push("order", "启动时清理历史挂单");
this.tradeLog.push("order", t("log.spotMaker.startupCleanup"));
this.initialOrderResetDone = true;
return true;
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "历史挂单已消失,跳过启动清理");
this.tradeLog.push("order", t("log.spotMaker.startupCleanupGone"));
this.initialOrderResetDone = true;
this.openOrders = [];
this.emitUpdate();
return true;
}
this.tradeLog.push("error", `启动撤单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.startupCancelFailed", { error: String(error) }));
return false;
}
}
@@ -857,17 +862,21 @@ export class LiquidityMakerEngine {
() => {
this.tradeLog.push(
"order",
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
t("log.spotMaker.cancelMismatched", {
side: order.side,
price: order.price,
reduceOnly: order.reduceOnly,
})
);
// 保持与原逻辑一致:成功撤销不立即修改本地 openOrders,等待订单流重建
},
() => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.tradeLog.push("order", t("log.spotMaker.cancelAlreadySettled"));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.cancelFailed", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
// 避免同一轮内重复操作同一张已出错的本地挂单,直接从本地缓存移除,等待下一次订单推送重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
@@ -887,7 +896,7 @@ export class LiquidityMakerEngine {
// Skip placing sells that would be bumped by venue minimums
if (this.lastSellPriceViable) {
this.lastSellPriceViable = false;
this.tradeLog.push("info", "现货卖单低于最小成交量,跳过挂单等待累积");
this.tradeLog.push("info", t("log.spotMaker.sellBelowMinNotional"));
}
continue;
}
@@ -920,10 +929,10 @@ export class LiquidityMakerEngine {
if (isRateLimitError(dustError)) {
throw dustError;
}
this.tradeLog.push("error", `小额市价平仓失败: ${String(dustError)}`);
this.tradeLog.push("error", t("log.spotMaker.dustCloseFailed", { error: String(dustError) }));
}
if (dustClosed) continue;
this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.placeFailed", { side: target.side, price: target.price, error: String(error) }));
}
}
}
@@ -939,7 +948,7 @@ export class LiquidityMakerEngine {
const minStopQty = Number.isFinite(this.precision.minBaseAmount) ? this.precision.minBaseAmount! : null;
if (minStopQty != null && minStopQty > 0 && absPosition + EPS < minStopQty) {
if (!this.lastSpotStopSkipped) {
this.tradeLog.push("info", "现货持仓低于最小平仓数量,跳过止损检查");
this.tradeLog.push("info", t("log.spotMaker.belowMinCloseSkipStop"));
this.lastSpotStopSkipped = true;
}
return;
@@ -948,7 +957,7 @@ export class LiquidityMakerEngine {
const pnl = computePositionPnl(position, bidPrice, askPrice);
const triggerStop = shouldStopLoss(position, bidPrice, askPrice, this.config.lossLimit);
if (!triggerStop) return;
this.tradeLog.push("stop", `现货止损,当前仓位=${absPosition.toFixed(6)} PnL=${pnl.toFixed(4)} USDT`);
this.tradeLog.push("stop", t("log.spotMaker.spotStop", { qty: absPosition.toFixed(6), pnl: pnl.toFixed(4) }));
try {
// 尽力撤销所有未完成挂单,避免锁定基础资产导致余额不足
await this.exchange.cancelAllOrders({ symbol: this.config.symbol }).catch(() => {});
@@ -967,9 +976,9 @@ export class LiquidityMakerEngine {
} catch (error) {
if (isRateLimitError(error)) throw error;
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "止损平仓时订单已不存在");
this.tradeLog.push("order", t("log.spotMaker.stopCloseMissing"));
} else {
this.tradeLog.push("error", `现货止损失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.spotStopFailed", { error: String(error) }));
}
}
return;
@@ -980,7 +989,7 @@ export class LiquidityMakerEngine {
const hasEntryPrice = Number.isFinite(position.entryPrice) && Math.abs(position.entryPrice) > 1e-8;
if (!hasEntryPrice) {
if (!this.entryPricePendingLogged) {
this.tradeLog.push("info", "做市持仓均价未同步,等待账户快照刷新后再执行止损判断");
this.tradeLog.push("info", t("log.spotMaker.entryPricePending"));
this.entryPricePendingLogged = true;
}
return;
@@ -993,7 +1002,10 @@ export class LiquidityMakerEngine {
if (triggerStop) {
this.tradeLog.push(
"stop",
`触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT`
t("log.spotMaker.stopTriggered", {
direction: position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
pnl: pnl.toFixed(4),
})
);
try {
await this.flushOrders();
@@ -1010,9 +1022,9 @@ export class LiquidityMakerEngine {
});
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "止损平仓时订单已不存在");
this.tradeLog.push("order", t("log.spotMaker.stopCloseMissing"));
} else {
this.tradeLog.push("error", `止损平仓失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.stopCloseFailed", { error: String(error) }));
}
}
}
@@ -1031,12 +1043,12 @@ export class LiquidityMakerEngine {
// 与原逻辑保持一致:成功撤销不记录日志且不修改本地 openOrders
},
() => {
this.tradeLog.push("order", "订单已不存在,撤销跳过");
this.tradeLog.push("order", t("log.spotMaker.orderMissingOnCancel"));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.cancelFailed", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
// 与同步撤单路径保持一致,移除本地异常订单,等待订单流重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
@@ -1056,10 +1068,10 @@ export class LiquidityMakerEngine {
try {
const snapshot = this.buildSnapshot();
this.events.emit("update", snapshot, (error) => {
this.tradeLog.push("error", `更新回调处理异常: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.updateHandlerError", { error: String(error) }));
});
} catch (err) {
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
this.tradeLog.push("error", t("log.spotMaker.snapshotDispatchError", { error: String(err) }));
}
}
@@ -1253,13 +1265,13 @@ export class LiquidityMakerEngine {
},
qtyStep: this.precision.qtyStep
});
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
this.tradeLog.push("order", t("log.spotMaker.dustClose", { side: target.side, qty: absQty.toFixed(6) }));
return true;
} catch (closeError) {
if (isRateLimitError(closeError)) {
throw closeError;
}
this.tradeLog.push("error", `小额市价平仓失败: ${String(closeError)}`);
this.tradeLog.push("error", t("log.spotMaker.dustCloseFailed", { error: String(closeError) }));
return false;
}
}
+5 -4
View File
@@ -8,6 +8,7 @@ import {
evaluateDefense,
type DefenseInputs,
} from "./maker-points-defense";
import { t } from "../i18n";
const NOW = 1_700_000_000_000;
@@ -149,18 +150,18 @@ describe("describeDefenseReasons", () => {
restConsecutiveErrors: 4,
})
);
expect(summary).toContain("StandX深度(7s)");
expect(summary).toContain("StandX REST错误(4次)");
expect(summary).toContain(t("defense.reason.depth", { seconds: 7 }));
expect(summary).toContain(t("defense.reason.rest", { count: 4 }));
});
it("falls back to unknown when nothing is flagged", () => {
expect(describeDefenseReasons(defenseReasonsFor({}))).toBe("unknown");
expect(describeDefenseReasons(defenseReasonsFor({}))).toBe(t("defense.reason.unknown"));
});
it("omits the Binance book reason when there is none", () => {
const summary = describeDefenseReasons(
defenseReasonsFor({ binanceUnhealthy: true, binanceHealthReason: null })
);
expect(summary).toBe("unknown");
expect(summary).toBe(t("defense.reason.unknown"));
});
});
+23 -8
View File
@@ -7,6 +7,7 @@
* so the rule that decides "is our market data trustworthy" can be tested
* without a live adapter.
*/
import { t } from "../i18n";
/** A feed older than this is considered stale. */
export const DATA_STALE_THRESHOLD_MS = 5_000;
@@ -164,17 +165,31 @@ export function describeDefenseReasons(reasons: DefenseReasons): string {
const items: string[] = [];
const seconds = (ms: number) => Math.round(ms / 1000);
if (reasons.depthStale) items.push(`StandX深度(${seconds(reasons.depthAge)}s)`);
if (reasons.accountStale) items.push(`StandX账户(${seconds(reasons.accountAge)}s)`);
if (reasons.depthStale) items.push(t("defense.reason.depth", { seconds: seconds(reasons.depthAge) }));
if (reasons.accountStale) {
items.push(t("defense.reason.account", { seconds: seconds(reasons.accountAge) }));
}
if (reasons.accountInvalid) {
items.push(`StandX仓位数据异常(${reasons.accountIssues.join(",") || "unknown"})`);
items.push(
t("defense.reason.accountInvalid", {
issues: reasons.accountIssues.join(",") || t("defense.reason.unknown"),
})
);
}
if (reasons.restUnhealthy) {
items.push(t("defense.reason.rest", { count: reasons.restConsecutiveErrors }));
}
if (reasons.marginModeNotIsolated) {
items.push(
t("defense.reason.marginMode", { mode: reasons.marginMode ?? t("defense.reason.unknown") })
);
}
if (reasons.binanceStale) {
items.push(t("defense.reason.binanceDepth", { seconds: seconds(reasons.binanceAge) }));
}
if (reasons.restUnhealthy) items.push(`StandX REST错误(${reasons.restConsecutiveErrors}次)`);
if (reasons.marginModeNotIsolated) items.push(`保证金模式(${reasons.marginMode ?? "unknown"})`);
if (reasons.binanceStale) items.push(`Binance深度(${seconds(reasons.binanceAge)}s)`);
if (reasons.binanceUnhealthy && reasons.binanceHealthReason) {
items.push(`Binance簿记异常(${reasons.binanceHealthReason})`);
items.push(t("defense.reason.binanceBook", { reason: reasons.binanceHealthReason }));
}
return items.length > 0 ? items.join(", ") : "unknown";
return items.length > 0 ? items.join(", ") : t("defense.reason.unknown");
}
+103 -77
View File
@@ -234,7 +234,7 @@ export class MakerPointsEngine {
: 3,
speedMs: 100,
logger: (context, error) => {
this.tradeLog.push("warn", `Binance ${context} 异常: ${extractMessage(error)}`);
this.tradeLog.push("warn", t("log.mp.binanceError", { context, error: extractMessage(error) }));
},
});
this.binanceDepth.onUpdate(() => {
@@ -246,13 +246,13 @@ export class MakerPointsEngine {
this.binanceDepth.onConnectionChange((state) => {
if (state === "disconnected") {
this.feedStatus.binance = false;
this.tradeLog.push("warn", "Binance 深度连接断开");
this.tradeLog.push("warn", t("log.mp.binanceDisconnected"));
} else if (state === "stale") {
this.feedStatus.binance = false;
this.tradeLog.push("warn", "Binance 深度数据过时");
this.tradeLog.push("warn", t("log.mp.binanceStale"));
} else if (state === "connected") {
this.feedStatus.binance = true;
this.tradeLog.push("info", "Binance 深度连接恢复");
this.tradeLog.push("info", t("log.mp.binanceRecovered"));
}
this.emitUpdate();
});
@@ -501,13 +501,13 @@ export class MakerPointsEngine {
*/
private handleDisconnect(symbol: string): void {
this._standxConnectionState = "disconnected";
this.tradeLog.push("warn", `WebSocket 断连 (${symbol}),启动断连保护`);
this.tradeLog.push("warn", t("log.mp.wsDisconnected", { symbol }));
this.notify({
type: "token_expired",
level: "warn",
symbol: this.config.symbol,
title: "连接断开",
message: "WebSocket 断连,正在尝试取消所有挂单",
title: t("notify.mp.disconnectTitle"),
message: t("notify.mp.disconnectBody"),
details: { symbol },
});
}
@@ -519,26 +519,26 @@ export class MakerPointsEngine {
private async handleReconnect(symbol: string): Promise<void> {
this._standxConnectionState = "connected";
this.reconnectResetPending = true;
this.tradeLog.push("info", `WebSocket 重连成功 (${symbol}),开始重连保护流程`);
this.tradeLog.push("info", t("log.mp.wsReconnected", { symbol }));
try {
// 查询真实挂单状态
if (this.exchange.queryOpenOrders) {
const realOrders = await this.exchange.queryOpenOrders();
this.tradeLog.push("info", `重连后查询到 ${realOrders.length} 个挂单`);
this.tradeLog.push("info", t("log.mp.reconnectFoundOrders", { count: realOrders.length }));
if (realOrders.length > 0) {
// 取消所有挂单
if (this.exchange.forceCancelAllOrders) {
const success = await this.exchange.forceCancelAllOrders();
if (success) {
this.tradeLog.push("order", "重连保护:已取消所有挂单");
this.tradeLog.push("order", t("log.mp.reconnectCancelled"));
} else {
this.tradeLog.push("warn", "重连保护:取消挂单未完全成功,将在下次循环重试");
this.tradeLog.push("warn", t("log.mp.reconnectCancelPartial"));
}
} else {
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
this.tradeLog.push("order", "重连保护:已取消所有挂单");
this.tradeLog.push("order", t("log.mp.reconnectCancelled"));
}
}
}
@@ -561,12 +561,12 @@ export class MakerPointsEngine {
type: "position_opened",
level: "info",
symbol: this.config.symbol,
title: "重连完成",
message: "WebSocket 重连成功,已清理挂单状态",
title: t("notify.mp.reconnectTitle"),
message: t("notify.mp.reconnectBody"),
details: { symbol },
});
} catch (error) {
this.tradeLog.push("error", `重连保护流程失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.mp.reconnectFailed", { error: extractMessage(error) }));
} finally {
this.reconnectResetPending = false;
}
@@ -682,7 +682,7 @@ export class MakerPointsEngine {
absPosition >= closeThreshold - EPS);
const prevCloseOnly = this.lastCloseOnly;
if (closeOnly !== prevCloseOnly) {
this.tradeLog.push("info", closeOnly ? "进入平仓模式,仅挂 reduce-only" : "退出平仓模式");
this.tradeLog.push("info", closeOnly ? t("log.mp.closeOnlyEntered") : t("log.mp.closeOnlyExited"));
this.lastCloseOnly = closeOnly;
}
@@ -697,9 +697,9 @@ export class MakerPointsEngine {
if (skipBuy !== prevSkipBuy || skipSell !== prevSkipSell) {
if (skipBuy || skipSell) {
const summary = `${skipBuy ? "BUY" : ""}${skipBuy && skipSell ? "/" : ""}${skipSell ? "SELL" : ""}`;
this.tradeLog.push("info", `Binance 深度失衡,暂停 ${summary} 挂单`);
this.tradeLog.push("info", t("log.mp.depthImbalancePause", { summary }));
} else {
this.tradeLog.push("info", "Binance 深度恢复,继续挂单");
this.tradeLog.push("info", t("log.mp.depthImbalanceResume"));
}
this.lastSkipBuy = skipBuy;
this.lastSkipSell = skipSell;
@@ -748,9 +748,9 @@ export class MakerPointsEngine {
if (isRateLimitError(error)) {
hadRateLimit = true;
this.rateLimit.registerRateLimit("maker-points");
this.tradeLog.push("warn", `限频触发,暂停挂单: ${extractMessage(error)}`);
this.tradeLog.push("warn", t("log.mp.rateLimited", { error: extractMessage(error) }));
} else {
this.tradeLog.push("error", `MakerPoints 主循环异常: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.mp.tickFailed", { error: extractMessage(error) }));
}
this.emitUpdate();
} finally {
@@ -1004,18 +1004,18 @@ export class MakerPointsEngine {
unlockOperating(this.locks, this.timers, this.pending, "LIMIT");
this.openOrders = [];
this.emitUpdate();
this.tradeLog.push("order", "启动时清理历史挂单");
this.tradeLog.push("order", t("log.spotMaker.startupCleanup"));
this.initialOrderResetDone = true;
return true;
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "历史挂单已消失,跳过启动清理");
this.tradeLog.push("order", t("log.spotMaker.startupCleanupGone"));
this.initialOrderResetDone = true;
this.openOrders = [];
this.emitUpdate();
return true;
}
this.tradeLog.push("error", `启动撤单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.startupCancelFailed", { error: String(error) }));
return false;
}
}
@@ -1047,16 +1047,20 @@ export class MakerPointsEngine {
() => {
this.tradeLog.push(
"order",
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
t("log.spotMaker.cancelMismatched", {
side: order.side,
price: order.price,
reduceOnly: order.reduceOnly,
})
);
},
() => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.tradeLog.push("order", t("log.spotMaker.cancelAlreadySettled"));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.cancelFailed", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
@@ -1096,12 +1100,12 @@ export class MakerPointsEngine {
break;
}
if (isPrecisionError(error)) {
this.tradeLog.push("warn", `检测到精度错误,重新同步: ${extractMessage(error)}`);
this.tradeLog.push("warn", t("log.mp.precisionErrorResync", { error: extractMessage(error) }));
this.precision.refresh();
}
this.tradeLog.push(
"error",
`挂单失败 ${target.side} @ ${target.price}: ${extractMessage(error)}`
t("log.mp.placeFailed", { side: target.side, price: target.price, error: extractMessage(error) })
);
}
}
@@ -1147,7 +1151,7 @@ export class MakerPointsEngine {
if (unexpectedOrders.length > 0) {
this.tradeLog.push(
"warn",
`发现 ${unexpectedOrders.length} 个未预期挂单,执行强制取消`
t("log.mp.unexpectedOrders", { count: unexpectedOrders.length })
);
// 强制取消所有挂单
@@ -1160,7 +1164,7 @@ export class MakerPointsEngine {
// 重置本地状态
this.openOrders = [];
this.pendingCancelOrders.clear();
this.tradeLog.push("order", "已强制取消所有挂单,重置本地状态");
this.tradeLog.push("order", t("log.mp.forceCancelled"));
return true;
}
@@ -1170,7 +1174,7 @@ export class MakerPointsEngine {
this.openOrders = this.openOrders.filter((o) => realOrderIds.has(String(o.orderId)));
}
} catch (error) {
this.tradeLog.push("error", `验证挂单状态失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.mp.verifyOrdersFailed", { error: extractMessage(error) }));
}
return false;
@@ -1207,14 +1211,14 @@ export class MakerPointsEngine {
// 不在这里设置冷却期,只有成功平仓后才设置
this.tradeLog.push(
"stop",
`触发止损: 实时未实现亏损 ${realtimePnl.toFixed(4)} USDT`
t("log.mp.stopTriggered", { pnl: realtimePnl.toFixed(4) })
);
this.notify({
type: "stop_loss",
level: "error",
symbol: this.config.symbol,
title: "止损触发",
message: `实时未实现亏损 ${realtimePnl.toFixed(4)} USDT,强制平仓`,
title: t("notify.mp.stopTitle"),
message: t("notify.mp.stopBody", { pnl: realtimePnl.toFixed(4) }),
details: {
side: position.positionAmt > 0 ? "LONG" : "SHORT",
size: absPosition,
@@ -1242,7 +1246,7 @@ export class MakerPointsEngine {
// 仓位已清零,止损成功
if (currentAbsPosition < EPS) {
this.tradeLog.push("stop", "止损成功: 仓位已清零");
this.tradeLog.push("stop", t("log.mp.stopSucceeded"));
this.stopLossCooldownUntil = Date.now() + STOP_LOSS_COOLDOWN_MS;
break;
}
@@ -1269,12 +1273,12 @@ export class MakerPointsEngine {
} catch (error) {
retryCount++;
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "止损平仓时订单已不存在,继续检查仓位");
this.tradeLog.push("order", t("log.mp.stopOrderMissing"));
} else if (isPrecisionError(error)) {
this.tradeLog.push("warn", `止损平仓精度错误,重新同步: ${extractMessage(error)}`);
this.tradeLog.push("warn", t("log.mp.stopPrecisionResync", { error: extractMessage(error) }));
this.precision.refresh();
} else {
this.tradeLog.push("error", `止损平仓失败 (重试 ${retryCount}/${maxRetries}): ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.mp.stopRetry", { attempt: retryCount, max: maxRetries, error: extractMessage(error) }));
}
// 失败后等待一段时间再重试
@@ -1285,7 +1289,7 @@ export class MakerPointsEngine {
}
if (retryCount >= maxRetries) {
this.tradeLog.push("error", `止损重试已达上限 (${maxRetries} 次),请手动检查仓位`);
this.tradeLog.push("error", t("log.mp.stopRetriesExhausted", { max: maxRetries }));
// 达到重试上限后设置冷却期,避免持续重试
this.stopLossCooldownUntil = Date.now() + STOP_LOSS_COOLDOWN_MS;
}
@@ -1312,12 +1316,12 @@ export class MakerPointsEngine {
// No log on successful cancel
},
() => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.tradeLog.push("order", t("log.spotMaker.cancelAlreadySettled"));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.cancelFailed", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
@@ -1343,10 +1347,10 @@ export class MakerPointsEngine {
try {
const snapshot = this.buildSnapshot();
this.events.emit("update", snapshot, (error) => {
this.tradeLog.push("error", `更新监听异常: ${String(error)}`);
this.tradeLog.push("error", t("log.mp.updateHandlerError", { error: String(error) }));
});
} catch (err) {
this.tradeLog.push("error", `快照生成异常: ${String(err)}`);
this.tradeLog.push("error", t("log.mp.snapshotError", { error: String(err) }));
}
}
@@ -1443,7 +1447,7 @@ export class MakerPointsEngine {
private logDesiredOrders(desired: DesiredOrder[]): void {
if (!desired.length) {
if (this.lastDesiredSummary !== "none") {
this.tradeLog.push("info", "暂无目标挂单");
this.tradeLog.push("info", t("log.mp.noTargets"));
this.lastDesiredSummary = "none";
}
return;
@@ -1452,7 +1456,7 @@ export class MakerPointsEngine {
.map((order) => `${order.side}@${order.price}${order.reduceOnly ? "(RO)" : ""}`)
.join(" | ");
if (summary !== this.lastDesiredSummary) {
this.tradeLog.push("info", `目标挂单: ${summary}`);
this.tradeLog.push("info", t("log.mp.targets", { summary }));
this.lastDesiredSummary = summary;
}
}
@@ -1471,7 +1475,7 @@ export class MakerPointsEngine {
if (!alreadySkipped) {
this.tradeLog.push(
"info",
`跳过 ${side} ${bps}bps 挂单: 深度 ${depthQty.toFixed(4)} BTC < ${minDepth} BTC`
t("log.mp.skipThinDepth", { side, bps, depth: depthQty.toFixed(4), min: minDepth })
);
this.thinDepthSkipStatus[key] = true;
}
@@ -1483,7 +1487,7 @@ export class MakerPointsEngine {
private resetThinDepthSkip(side: "BUY" | "SELL", bps: number): void {
const key = `${side}_${bps}`;
if (this.thinDepthSkipStatus[key]) {
this.tradeLog.push("info", `${side} ${bps}bps 深度恢复,继续挂单`);
this.tradeLog.push("info", t("log.mp.depthRecovered", { side, bps }));
this.thinDepthSkipStatus[key] = false;
}
}
@@ -1499,14 +1503,14 @@ export class MakerPointsEngine {
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.tradeLog.push("warn", t("log.mp.insufficientBalance", { seconds, detail }));
this.insufficientBalanceNotified = true;
}
private applyInsufficientBalanceState(now: number): boolean {
const active = now < this.insufficientBalanceCooldownUntil;
if (!active && this.insufficientBalanceNotified) {
this.tradeLog.push("info", "余额恢复,继续挂单");
this.tradeLog.push("info", t("log.mp.balanceRecovered"));
this.insufficientBalanceNotified = false;
this.lastInsufficientMessage = null;
}
@@ -1532,8 +1536,11 @@ export class MakerPointsEngine {
type: "position_opened",
level: "info",
symbol: this.config.symbol,
title: "开仓",
message: `${currentSide === "LONG" ? "做多" : "做空"} ${Math.abs(currentAmt).toFixed(6)}`,
title: t("notify.mp.openTitle"),
message: t("notify.mp.openBody", {
direction: currentSide === "LONG" ? t("trend.label.long") : t("trend.label.short"),
qty: Math.abs(currentAmt).toFixed(6),
}),
details: {
side: currentSide,
size: Math.abs(currentAmt),
@@ -1542,13 +1549,18 @@ export class MakerPointsEngine {
});
} else if (currentSide === "FLAT" && prevSide !== "FLAT") {
const pnl = position.unrealizedProfit;
const closeType = this.tokenExpiry.closeOnlyMode ? "Token过期平仓" : "平仓";
const closeType = this.tokenExpiry.closeOnlyMode
? t("notify.mp.closeTitleTokenExpired")
: t("notify.mp.closeTitle");
this.notify({
type: "position_closed",
level: "success",
symbol: this.config.symbol,
title: closeType,
message: `已平仓 ${Math.abs(prevAmt).toFixed(6)} (${prevSide === "LONG" ? "多" : "空"})`,
message: t("notify.mp.closeBody", {
qty: Math.abs(prevAmt).toFixed(6),
direction: prevSide === "LONG" ? t("common.direction.long") : t("common.direction.short"),
}),
details: {
prevSide,
closedSize: Math.abs(prevAmt),
@@ -1562,8 +1574,12 @@ export class MakerPointsEngine {
type: "order_filled",
level: "info",
symbol: this.config.symbol,
title: "加仓",
message: `${currentSide === "LONG" ? "做多" : "做空"} +${absChange.toFixed(6)}${Math.abs(currentAmt).toFixed(6)}`,
title: t("notify.mp.increaseTitle"),
message: t("notify.mp.increaseBody", {
direction: currentSide === "LONG" ? t("trend.label.long") : t("trend.label.short"),
delta: absChange.toFixed(6),
qty: Math.abs(currentAmt).toFixed(6),
}),
details: {
side: currentSide,
added: absChange,
@@ -1575,8 +1591,12 @@ export class MakerPointsEngine {
type: "order_filled",
level: "info",
symbol: this.config.symbol,
title: "减仓",
message: `${currentSide === "LONG" ? "多" : "空"} -${absChange.toFixed(6)}${Math.abs(currentAmt).toFixed(6)}`,
title: t("notify.mp.reduceTitle"),
message: t("notify.mp.reduceBody", {
direction: currentSide === "LONG" ? t("common.direction.long") : t("common.direction.short"),
delta: absChange.toFixed(6),
qty: Math.abs(currentAmt).toFixed(6),
}),
details: {
side: currentSide,
reduced: absChange,
@@ -1589,8 +1609,14 @@ export class MakerPointsEngine {
type: "position_opened",
level: "info",
symbol: this.config.symbol,
title: "反向开仓",
message: `${prevSide === "LONG" ? "多→空" : "空→多"} ${Math.abs(currentAmt).toFixed(6)}`,
title: t("notify.mp.reverseTitle"),
message: t("notify.mp.reverseBody", {
transition:
prevSide === "LONG"
? t("common.direction.longToShort")
: t("common.direction.shortToLong"),
qty: Math.abs(currentAmt).toFixed(6),
}),
details: {
prevSide,
newSide: currentSide,
@@ -1646,7 +1672,7 @@ export class MakerPointsEngine {
this.defenseMode = true;
const staleSummary = describeDefenseReasons(reasons);
this.tradeLog.push("warn", `数据过时检测: ${staleSummary},进入防御模式`);
this.tradeLog.push("warn", t("log.mp.defenseEntered", { summary: staleSummary }));
// 发送通知
if (!this.defenseModeNotified) {
@@ -1654,8 +1680,8 @@ export class MakerPointsEngine {
type: "token_expired",
level: "warn",
symbol: this.config.symbol,
title: "防御模式",
message: `数据推送中断: ${staleSummary},已取消所有挂单`,
title: t("notify.mp.defenseTitle"),
message: t("notify.mp.defenseBody", { summary: staleSummary }),
details: reasons,
});
this.defenseModeNotified = true;
@@ -1675,14 +1701,14 @@ export class MakerPointsEngine {
this.defenseMode = false;
this.defenseModeNotified = false;
this.tradeLog.push("info", "数据推送恢复正常,退出防御模式");
this.tradeLog.push("info", t("log.mp.defenseExited"));
this.notify({
type: "position_opened",
level: "info",
symbol: this.config.symbol,
title: "防御模式解除",
message: "数据推送恢复正常,恢复正常交易",
title: t("notify.mp.defenseClearedTitle"),
message: t("notify.mp.defenseClearedBody"),
details: {},
});
@@ -1704,13 +1730,13 @@ export class MakerPointsEngine {
if (this.exchange.forceCancelAllOrders) {
const success = await this.exchange.forceCancelAllOrders();
if (success) {
this.tradeLog.push("order", "防御模式: 已强制取消所有挂单");
this.tradeLog.push("order", t("log.mp.defenseForceCancelled"));
} else {
this.tradeLog.push("warn", "防御模式: 取消挂单未完全成功,将继续重试");
this.tradeLog.push("warn", t("log.mp.defenseCancelPartial"));
}
} else {
await this.exchange.cancelAllOrders({ symbol: this.config.symbol });
this.tradeLog.push("order", "防御模式: 已取消所有挂单");
this.tradeLog.push("order", t("log.mp.defenseCancelled"));
}
// 重置本地挂单状态
@@ -1719,11 +1745,11 @@ export class MakerPointsEngine {
unlockOperating(this.locks, this.timers, this.pending, "LIMIT");
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "防御模式: 挂单已不存在");
this.tradeLog.push("order", t("log.mp.defenseOrdersGone"));
this.openOrders = [];
this.pendingCancelOrders.clear();
} else {
this.tradeLog.push("error", `防御模式取消挂单失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.mp.defenseCancelFailed", { error: extractMessage(error) }));
}
}
}
@@ -1736,7 +1762,7 @@ export class MakerPointsEngine {
if (this.defenseRestPollActive) return;
this.defenseRestPollActive = true;
this.tradeLog.push("info", "防御模式: 启动 REST 数据轮询");
this.tradeLog.push("info", t("log.mp.defensePollStarted"));
const poll = async () => {
if (!this.defenseRestPollActive || !this.defenseMode) return;
@@ -1748,10 +1774,10 @@ export class MakerPointsEngine {
this.applyAccountSnapshot(nextAccount);
const health = validateAccountSnapshotForSymbol(nextAccount, this.config.symbol);
if (!health.ok) {
this.tradeLog.push("warn", `防御模式: 仓位数据仍异常: ${health.issues.join(",")}`);
this.tradeLog.push("warn", t("log.mp.defensePositionStillBad", { issues: health.issues.join(",") }));
}
} else {
this.tradeLog.push("warn", "防御模式: REST 获取账户快照为空");
this.tradeLog.push("warn", t("log.mp.defenseEmptySnapshot"));
}
}
@@ -1776,11 +1802,11 @@ export class MakerPointsEngine {
this.feedStatus.orders = true;
if (realOrders.length > 0) {
this.tradeLog.push("warn", `防御模式: 发现 ${realOrders.length} 个挂单,执行取消`);
this.tradeLog.push("warn", t("log.mp.defenseFoundOrders", { count: realOrders.length }));
await this.defenseCancelAllOrders();
}
} catch (error) {
this.tradeLog.push("error", `防御模式查询挂单失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.mp.defenseQueryFailed", { error: extractMessage(error) }));
// 查询失败时仍然尝试撤销所有挂单(宁可多撤,也不遗留)
await this.defenseCancelAllOrders();
}
@@ -1791,7 +1817,7 @@ export class MakerPointsEngine {
// 检查止损条件(使用当前账户快照中的数据)
// checkStopLoss 会继续运行,使用最后收到的数据进行止损判断
} catch (error) {
this.tradeLog.push("error", `防御模式 REST 轮询失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.mp.defensePollFailed", { error: extractMessage(error) }));
}
// 继续下一次轮询
@@ -1813,7 +1839,7 @@ export class MakerPointsEngine {
clearTimeout(this.defenseRestPollTimer);
this.defenseRestPollTimer = null;
}
this.tradeLog.push("info", "防御模式: 停止 REST 数据轮询");
this.tradeLog.push("info", t("log.mp.defensePollStopped"));
}
}
+60 -48
View File
@@ -27,6 +27,7 @@ import type { MakerEngineSnapshot } from "./maker-engine";
import { makeOrderPlan } from "../core/lib/order-plan";
import { safeCancelOrder } from "../core/lib/orders";
import { RateLimitController } from "../core/lib/rate-limit";
import { t } from "../i18n";
import { StrategyEventEmitter } from "./common/event-emitter";
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
import { SessionVolumeTracker } from "./common/session-volume";
@@ -223,8 +224,8 @@ export class OffsetMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅账户失败: ${String(error)}`,
processFail: (error) => `账户推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.accountFail", { error: String(error) }),
processFail: (error) => t("log.process.accountError", { error: String(error) }),
}
);
@@ -252,8 +253,8 @@ export class OffsetMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅订单失败: ${String(error)}`,
processFail: (error) => `订单推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.orderFail", { error: String(error) }),
processFail: (error) => t("log.process.orderError", { error: String(error) }),
}
);
@@ -266,8 +267,8 @@ export class OffsetMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅深度失败: ${String(error)}`,
processFail: (error) => `深度推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.depthFail", { error: String(error) }),
processFail: (error) => t("log.process.depthError", { error: String(error) }),
}
);
@@ -280,8 +281,8 @@ export class OffsetMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅Ticker失败: ${String(error)}`,
processFail: (error) => `价格推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.tickerFail", { error: String(error) }),
processFail: (error) => t("log.process.tickerError", { error: String(error) }),
}
);
@@ -300,8 +301,8 @@ export class OffsetMakerEngine {
},
log,
{
subscribeFail: (error) => `订阅K线失败: ${String(error)}`,
processFail: (error) => `K线推送处理异常: ${String(error)}`,
subscribeFail: (error) => t("log.subscribe.klineFail", { error: String(error) }),
processFail: (error) => t("log.process.klineError", { error: String(error) }),
}
);
}
@@ -421,13 +422,13 @@ export class OffsetMakerEngine {
// 无法卖出,跳过卖单,允许买单累计
this.lastSellPriceViable = false;
if (!skipSellSide) {
this.tradeLog.push("info", "现货持仓低于最小卖单量,暂不挂卖单");
this.tradeLog.push("info", t("log.spotMaker.belowMinSellHold"));
}
}
if (!skipBuySide && canEnter) {
if (!allowSpotBuy) {
if (this.lastBuyPriceViable) {
this.tradeLog.push("info", "现货买入仅在1m阳线,当前跳过买单");
this.tradeLog.push("info", t("log.spotMaker.buyOnlyOnGreenCandle"));
this.lastBuyPriceViable = false;
}
} else {
@@ -444,8 +445,8 @@ export class OffsetMakerEngine {
this.lastBuyPriceViable = false;
const reason =
buyAmount < EPS && isSpotMarket
? "现货可用报价资产不足,跳过买单"
: "跳过买单:价差不足以构造maker价格";
? t("log.spotMaker.quoteBalanceShort")
: t("log.spotMaker.spreadTooTightBuy");
this.tradeLog.push("info", reason);
}
}
@@ -456,7 +457,7 @@ export class OffsetMakerEngine {
// 持仓低于最小卖单量,跳过卖单,等待累积
if (this.lastSellPriceViable) {
this.lastSellPriceViable = false;
this.tradeLog.push("info", "现货持仓低于最小卖单量,跳过卖单");
this.tradeLog.push("info", t("log.spotMaker.belowMinSellSkip"));
}
} else {
const desiredSellAmount =
@@ -474,8 +475,8 @@ export class OffsetMakerEngine {
this.lastSellPriceViable = false;
const reason =
sellAmount < EPS && isSpotMarket
? "现货可用基础资产不足,跳过卖单"
: "跳过卖单:价差不足以构造maker价格";
? t("log.spotMaker.baseBalanceShort")
: t("log.spotMaker.spreadTooTightSell");
this.tradeLog.push("info", reason);
}
}
@@ -485,7 +486,7 @@ export class OffsetMakerEngine {
if (!skipBuySide && canEnter) {
if (isSpotMarket && !allowSpotBuy) {
if (this.lastBuyPriceViable) {
this.tradeLog.push("info", "现货买入仅在1m阳线,当前跳过买单");
this.tradeLog.push("info", t("log.spotMaker.buyOnlyOnGreenCandle"));
this.lastBuyPriceViable = false;
}
} else if (bidPrice != null) {
@@ -500,7 +501,7 @@ export class OffsetMakerEngine {
this.sellableBase(balancesForSpot) + EPS < minSell;
if (belowMinSell) {
this.lastSellPriceViable = false;
this.tradeLog.push("info", "现货持仓低于最小卖单量,跳过卖单");
this.tradeLog.push("info", t("log.spotMaker.belowMinSellSkip"));
} else if (askPrice != null) {
desired.push({ side: "SELL", price: askPrice, amount: this.config.tradeAmount, reduceOnly: false });
}
@@ -543,7 +544,7 @@ export class OffsetMakerEngine {
await this.enforceRateLimitStop();
this.tradeLog.push("warn", `OffsetMakerEngine 429: ${String(error)}`);
} else {
this.tradeLog.push("error", `偏移做市循环异常: ${String(error)}`);
this.tradeLog.push("error", t("log.offsetMaker.tickFailed", { error: String(error) }));
}
this.emitUpdate();
} finally {
@@ -580,9 +581,9 @@ export class OffsetMakerEngine {
});
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "限频强制平仓时订单已不存在");
this.tradeLog.push("order", t("log.spotMaker.rateLimitCloseMissing"));
} else {
this.tradeLog.push("error", `限频强制平仓失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.rateLimitCloseFailed", { error: String(error) }));
}
}
}
@@ -600,18 +601,18 @@ export class OffsetMakerEngine {
unlockOperating(this.locks, this.timers, this.pending, "LIMIT");
this.openOrders = [];
this.emitUpdate();
this.tradeLog.push("order", "启动时清理历史挂单");
this.tradeLog.push("order", t("log.spotMaker.startupCleanup"));
this.initialOrderResetDone = true;
return true;
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "历史挂单已消失,跳过启动清理");
this.tradeLog.push("order", t("log.spotMaker.startupCleanupGone"));
this.initialOrderResetDone = true;
this.openOrders = [];
this.emitUpdate();
return true;
}
this.tradeLog.push("error", `启动撤单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.startupCancelFailed", { error: String(error) }));
return false;
}
}
@@ -647,7 +648,11 @@ export class OffsetMakerEngine {
const closeSidePrice = side === "SELL" ? bid : ask;
this.tradeLog.push(
"stop",
`深度极端不平衡(${buySum.toFixed(4)} vs ${sellSum.toFixed(4)}), 市价平仓 ${side}`
t("log.offsetMaker.imbalanceClose", {
buySum: buySum.toFixed(4),
sellSum: sellSum.toFixed(4),
side,
})
);
try {
await this.flushOrders();
@@ -664,9 +669,9 @@ export class OffsetMakerEngine {
});
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "深度不平衡平仓时订单已不存在");
this.tradeLog.push("order", t("log.offsetMaker.imbalanceCloseMissing"));
} else {
this.tradeLog.push("error", `深度不平衡平仓失败: ${String(error)}`);
this.tradeLog.push("error", t("log.offsetMaker.imbalanceCloseFailed", { error: String(error) }));
}
}
return true;
@@ -712,17 +717,21 @@ export class OffsetMakerEngine {
() => {
this.tradeLog.push(
"order",
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
t("log.spotMaker.cancelMismatched", {
side: order.side,
price: order.price,
reduceOnly: order.reduceOnly,
})
);
// 保持与原逻辑一致:成功撤销不立即修改本地 openOrders,等待订单流重建
},
() => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.tradeLog.push("order", t("log.spotMaker.cancelAlreadySettled"));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.cancelFailed", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
// 避免同一轮内重复操作同一张已出错的本地挂单,直接从本地缓存移除,等待下一次订单推送重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
@@ -742,7 +751,7 @@ export class OffsetMakerEngine {
// Skip placing sells that would be bumped by venue minimums
if (this.lastSellPriceViable) {
this.lastSellPriceViable = false;
this.tradeLog.push("info", "现货卖单低于最小成交量,跳过挂单等待累积");
this.tradeLog.push("info", t("log.spotMaker.sellBelowMinNotional"));
}
continue;
}
@@ -775,10 +784,10 @@ export class OffsetMakerEngine {
if (isRateLimitError(dustError)) {
throw dustError;
}
this.tradeLog.push("error", `小额市价平仓失败: ${String(dustError)}`);
this.tradeLog.push("error", t("log.spotMaker.dustCloseFailed", { error: String(dustError) }));
}
if (dustClosed) continue;
this.tradeLog.push("error", `挂单失败(${target.side} ${target.price}): ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.placeFailed", { side: target.side, price: target.price, error: String(error) }));
}
}
}
@@ -794,7 +803,7 @@ export class OffsetMakerEngine {
const minStopQty = Number.isFinite(this.precision.minBaseAmount) ? this.precision.minBaseAmount! : null;
if (minStopQty != null && minStopQty > 0 && absPosition + EPS < minStopQty) {
if (!this.lastSpotStopSkipped) {
this.tradeLog.push("info", "现货持仓低于最小平仓数量,跳过止损检查");
this.tradeLog.push("info", t("log.spotMaker.belowMinCloseSkipStop"));
this.lastSpotStopSkipped = true;
}
return;
@@ -803,7 +812,7 @@ export class OffsetMakerEngine {
const pnl = computePositionPnl(position, bidPrice, askPrice);
const triggerStop = shouldStopLoss(position, bidPrice, askPrice, this.config.lossLimit);
if (!triggerStop) return;
this.tradeLog.push("stop", `现货止损,当前仓位=${absPosition.toFixed(6)} PnL=${pnl.toFixed(4)} USDT`);
this.tradeLog.push("stop", t("log.spotMaker.spotStop", { qty: absPosition.toFixed(6), pnl: pnl.toFixed(4) }));
try {
// 尽力撤销所有未完成挂单,避免锁定基础资产导致余额不足
await this.exchange.cancelAllOrders({ symbol: this.config.symbol }).catch(() => {});
@@ -822,9 +831,9 @@ export class OffsetMakerEngine {
} catch (error) {
if (isRateLimitError(error)) throw error;
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "止损平仓时订单已不存在");
this.tradeLog.push("order", t("log.spotMaker.stopCloseMissing"));
} else {
this.tradeLog.push("error", `现货止损失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.spotStopFailed", { error: String(error) }));
}
}
return;
@@ -835,7 +844,7 @@ export class OffsetMakerEngine {
const hasEntryPrice = Number.isFinite(position.entryPrice) && Math.abs(position.entryPrice) > 1e-8;
if (!hasEntryPrice) {
if (!this.entryPricePendingLogged) {
this.tradeLog.push("info", "做市持仓均价未同步,等待账户快照刷新后再执行止损判断");
this.tradeLog.push("info", t("log.spotMaker.entryPricePending"));
this.entryPricePendingLogged = true;
}
return;
@@ -848,7 +857,10 @@ export class OffsetMakerEngine {
if (triggerStop) {
this.tradeLog.push(
"stop",
`触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT`
t("log.spotMaker.stopTriggered", {
direction: position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
pnl: pnl.toFixed(4),
})
);
try {
await this.flushOrders();
@@ -865,9 +877,9 @@ export class OffsetMakerEngine {
});
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "止损平仓时订单已不存在");
this.tradeLog.push("order", t("log.spotMaker.stopCloseMissing"));
} else {
this.tradeLog.push("error", `止损平仓失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.stopCloseFailed", { error: String(error) }));
}
}
}
@@ -886,12 +898,12 @@ export class OffsetMakerEngine {
// 与原逻辑保持一致:成功撤销不记录日志且不修改本地 openOrders
},
() => {
this.tradeLog.push("order", "订单已不存在,撤销跳过");
this.tradeLog.push("order", t("log.spotMaker.orderMissingOnCancel"));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
},
(error) => {
this.tradeLog.push("error", `撤销订单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.cancelFailed", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
// 与同步撤单路径保持一致,移除本地异常订单,等待订单流重建
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
@@ -911,10 +923,10 @@ export class OffsetMakerEngine {
try {
const snapshot = this.buildSnapshot();
this.events.emit("update", snapshot, (error) => {
this.tradeLog.push("error", `更新回调处理异常: ${String(error)}`);
this.tradeLog.push("error", t("log.spotMaker.updateHandlerError", { error: String(error) }));
});
} catch (err) {
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
this.tradeLog.push("error", t("log.spotMaker.snapshotDispatchError", { error: String(err) }));
}
}
@@ -1117,13 +1129,13 @@ export class OffsetMakerEngine {
},
qtyStep: this.precision.qtyStep
});
this.tradeLog.push("order", `小额仓位使用市价平仓 ${target.side} 数量 ${absQty.toFixed(6)}`);
this.tradeLog.push("order", t("log.spotMaker.dustClose", { side: target.side, qty: absQty.toFixed(6) }));
return true;
} catch (closeError) {
if (isRateLimitError(closeError)) {
throw closeError;
}
this.tradeLog.push("error", `小额市价平仓失败: ${String(closeError)}`);
this.tradeLog.push("error", t("log.spotMaker.dustCloseFailed", { error: String(closeError) }));
return false;
}
}
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { t } from "../src/i18n";
import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
@@ -103,7 +104,11 @@ describe("MakerPointsEngine Binance depth health defense", () => {
expect((engine as any).defenseMode).toBe(true);
const logs = ((engine as any).tradeLog.all() as Array<{ detail: string }>).map((entry) => entry.detail);
expect(logs.some((detail) => detail.includes("Binance簿记异常(orderbook_not_ready)"))).toBe(true);
expect(
logs.some((detail) =>
detail.includes(t("defense.reason.binanceBook", { reason: "orderbook_not_ready" }))
)
).toBe(true);
engine.stop();
});
+71
View File
@@ -0,0 +1,71 @@
import { describe, expect, it } from "vitest";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import { t } from "../src/i18n";
const SRC = join(import.meta.dirname, "..", "src");
const I18N_FILE = join(SRC, "i18n", "index.ts");
function walk(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
walk(full, out);
} else if (/\.tsx?$/.test(entry)) {
out.push(full);
}
}
return out;
}
/** A string or template literal containing a CJK character. */
const CJK_IN_LITERAL = /["`][^"`\n]*[一-龥][^"`\n]*["`]/;
describe("i18n coverage", () => {
it("keeps user-facing text out of source files", () => {
// Chinese literals outside the translation table cannot be shown in English,
// which is how the order log, grid events and defense alerts stayed
// untranslatable for so long.
const offenders: string[] = [];
for (const file of walk(SRC)) {
if (file === I18N_FILE) continue;
if (file.endsWith(".test.ts") || file.endsWith(".test.tsx")) continue;
const lines = readFileSync(file, "utf8").split("\n");
lines.forEach((line, index) => {
if (line.trimStart().startsWith("//") || line.trimStart().startsWith("*")) return;
if (CJK_IN_LITERAL.test(line)) {
offenders.push(`${file.slice(SRC.length + 1)}:${index + 1} ${line.trim()}`);
}
});
}
expect(offenders).toEqual([]);
});
it("gives every key both a zh and an en translation", () => {
const source = readFileSync(I18N_FILE, "utf8");
const table = source.slice(
source.indexOf("const translations"),
source.indexOf("const formatTemplate")
);
const keys = [...table.matchAll(/^ {2}"([\w.]+)":/gm)].map((m) => m[1]!);
expect(keys.length).toBeGreaterThan(400);
const duplicates = keys.filter((key, index) => keys.indexOf(key) !== index);
expect(duplicates).toEqual([]);
for (const key of keys) {
expect(t(key, {}, "zh"), `${key} missing zh`).not.toBe(key);
expect(t(key, {}, "en"), `${key} missing en`).not.toBe(key);
}
});
it("substitutes placeholders in both languages", () => {
expect(t("log.order.closePlaced", { side: "BUY" }, "zh")).toContain("BUY");
expect(t("log.order.closePlaced", { side: "BUY" }, "en")).toContain("BUY");
expect(t("log.order.closePlaced", { side: "BUY" }, "en")).not.toContain("{side}");
});
it("leaves an unknown placeholder visible rather than printing undefined", () => {
expect(t("log.order.closePlaced", {}, "en")).toContain("{side}");
});
});