From 22b9c5a39d50894fd31fffeec819f37b4cb98bb9 Mon Sep 17 00:00:00 2001 From: discountry Date: Wed, 29 Jul 2026 21:35:19 +0800 Subject: [PATCH] i18n: route the remaining 300 hardcoded strings through the table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/exchanges/aster/gateway.ts | 34 +- src/exchanges/grvt/adapter.ts | 2 +- src/i18n/index.ts | 519 ++++++++++++++++++ src/strategy/basis-arb-engine.ts | 2 +- src/strategy/grid-engine.ts | 115 ++-- src/strategy/grid-logic.test.ts | 9 +- src/strategy/grid-logic.ts | 42 +- src/strategy/liquidity-maker-engine.ts | 108 ++-- src/strategy/maker-points-defense.test.ts | 9 +- src/strategy/maker-points-defense.ts | 31 +- src/strategy/maker-points-engine.ts | 180 +++--- src/strategy/offset-maker-engine.ts | 108 ++-- .../binance-depth-health-integration.test.ts | 7 +- tests/i18n-coverage.test.ts | 71 +++ 14 files changed, 964 insertions(+), 273 deletions(-) create mode 100644 tests/i18n-coverage.test.ts diff --git a/src/exchanges/aster/gateway.ts b/src/exchanges/aster/gateway.ts index 1b046c9..9cd01ab 100644 --- a/src/exchanges/aster/gateway.ts +++ b/src/exchanges/aster/gateway.ts @@ -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 ?? []; diff --git a/src/exchanges/grvt/adapter.ts b/src/exchanges/grvt/adapter.ts index 8b05f44..646941e 100644 --- a/src/exchanges/grvt/adapter.ts +++ b/src/exchanges/grvt/adapter.ts @@ -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)); diff --git a/src/i18n/index.ts b/src/i18n/index.ts index db45890..cc5be82 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -860,6 +860,525 @@ const translations: Record = { 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 => { diff --git a/src/strategy/basis-arb-engine.ts b/src/strategy/basis-arb-engine.ts index 6769569..efd85cf 100644 --- a/src/strategy/basis-arb-engine.ts +++ b/src/strategy/basis-arb-engine.ts @@ -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; diff --git a/src/strategy/grid-engine.ts b/src/strategy/grid-engine.ts index a72edc1..fe91fa5 100644 --- a/src/strategy/grid-engine.ts +++ b/src/strategy/grid-engine.ts @@ -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 { 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) })); } } diff --git a/src/strategy/grid-logic.test.ts b/src/strategy/grid-logic.test.ts index af154e1..93c654f 100644 --- a/src/strategy/grid-logic.test.ts +++ b/src/strategy/grid-logic.test.ts @@ -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% 容忍内 }); diff --git a/src/strategy/grid-logic.ts b/src/strategy/grid-logic.ts index ea4931b..3461e7b 100644 --- a/src/strategy/grid-logic.ts +++ b/src/strategy/grid-logic.ts @@ -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; } diff --git a/src/strategy/liquidity-maker-engine.ts b/src/strategy/liquidity-maker-engine.ts index aabf8ca..4526ac8 100644 --- a/src/strategy/liquidity-maker-engine.ts +++ b/src/strategy/liquidity-maker-engine.ts @@ -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; } } diff --git a/src/strategy/maker-points-defense.test.ts b/src/strategy/maker-points-defense.test.ts index d395b53..e209a07 100644 --- a/src/strategy/maker-points-defense.test.ts +++ b/src/strategy/maker-points-defense.test.ts @@ -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")); }); }); diff --git a/src/strategy/maker-points-defense.ts b/src/strategy/maker-points-defense.ts index 29b8d7f..c748d28 100644 --- a/src/strategy/maker-points-defense.ts +++ b/src/strategy/maker-points-defense.ts @@ -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"); } diff --git a/src/strategy/maker-points-engine.ts b/src/strategy/maker-points-engine.ts index 83c875c..e8b9ad8 100644 --- a/src/strategy/maker-points-engine.ts +++ b/src/strategy/maker-points-engine.ts @@ -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 { 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")); } } diff --git a/src/strategy/offset-maker-engine.ts b/src/strategy/offset-maker-engine.ts index bbc31eb..e0a1550 100644 --- a/src/strategy/offset-maker-engine.ts +++ b/src/strategy/offset-maker-engine.ts @@ -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; } } diff --git a/tests/binance-depth-health-integration.test.ts b/tests/binance-depth-health-integration.test.ts index cd5ee7a..f83beb7 100644 --- a/tests/binance-depth-health-integration.test.ts +++ b/tests/binance-depth-health-integration.test.ts @@ -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(); }); diff --git a/tests/i18n-coverage.test.ts b/tests/i18n-coverage.test.ts new file mode 100644 index 0000000..29d3c97 --- /dev/null +++ b/tests/i18n-coverage.test.ts @@ -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}"); + }); +});