i18n(core): route order-coordinator and token/margin guards through t()

These files bypassed the i18n table entirely, so a user running LANG=en still
got Chinese order logs. Migrating them surfaced structural duplication too: the
five order paths each spelled out their own 'quantity invalid' string, now one
key parameterised by an order-kind label.

Also fixes a type that carried display text as its domain: TrendLabel was
'做多' | '做空' | '无信号', so the engine's snapshot value *was* the Chinese
string and English rendering depended on matching it. Now 'long' | 'short' |
'none', translated at the edge.

Order-coordinator tests asserted the Chinese literals, which is exactly what
made the gap invisible; they now assert the resolved key so they hold in either
language.

39 new translation keys. 271 pass; tsc and oxlint clean.
This commit is contained in:
discountry
2026-07-29 21:26:57 +08:00
parent 448a2f2615
commit b9331c516a
11 changed files with 227 additions and 62 deletions
+49 -29
View File
@@ -10,6 +10,7 @@ import {
import { roundDownToTick, roundQtyDownToStep } from "../utils/math"; import { roundDownToTick, roundQtyDownToStep } from "../utils/math";
import { isUnknownOrderError } from "../utils/errors"; import { isUnknownOrderError } from "../utils/errors";
import { isOrderPriceAllowedByMark } from "../utils/strategy"; import { isOrderPriceAllowedByMark } from "../utils/strategy";
import { t } from "../i18n";
export type OrderLockMap = Record<string, boolean>; export type OrderLockMap = Record<string, boolean>;
export type OrderTimerMap = Record<string, ReturnType<typeof setTimeout> | null>; export type OrderTimerMap = Record<string, ReturnType<typeof setTimeout> | null>;
@@ -90,7 +91,7 @@ function enforceMarkPriceGuard(
toCheckPrice: number | null | undefined, toCheckPrice: number | null | undefined,
guard: OrderGuardOptions | undefined, guard: OrderGuardOptions | undefined,
log: LogHandler, log: LogHandler,
context: string kind: string
): boolean { ): boolean {
if (!guard || guard.maxPct == null) return true; if (!guard || guard.maxPct == null) return true;
const allowed = isOrderPriceAllowedByMark({ const allowed = isOrderPriceAllowedByMark({
@@ -104,7 +105,13 @@ function enforceMarkPriceGuard(
const markStr = Number.isFinite(Number(guard.markPrice)) ? Number(guard.markPrice).toFixed(2) : String(guard.markPrice); const markStr = Number.isFinite(Number(guard.markPrice)) ? Number(guard.markPrice).toFixed(2) : String(guard.markPrice);
log( log(
"info", "info",
`${context} 保护触发:side=${side} price=${priceStr} mark=${markStr} 超过 ${(guard.maxPct! * 100).toFixed(2)}%` t("log.order.markGuardBlocked", {
kind,
side,
price: priceStr,
mark: markStr,
pct: (guard.maxPct! * 100).toFixed(2),
})
); );
return false; return false;
} }
@@ -137,7 +144,7 @@ export function lockOperating(
timers[type] = setTimeout(() => { timers[type] = setTimeout(() => {
locks[type] = false; locks[type] = false;
pendings[type] = null; pendings[type] = null;
log("info", `${type} 操作超时自动解锁`); log("info", t("log.order.lockTimeout", { type }));
}, timeout); }, timeout);
} }
@@ -182,12 +189,12 @@ export async function deduplicateOrders(
try { try {
lockOperating(locks, timers, pendings, type, log); lockOperating(locks, timers, pendings, type, log);
await adapter.cancelOrders({ symbol, orderIdList }); await adapter.cancelOrders({ symbol, orderIdList });
log("order", `去重撤销重复 ${type} 单: ${orderIdList.join(",")}`); log("order", t("log.order.dedupeCancelled", { type, ids: orderIdList.join(",") }));
} catch (err) { } catch (err) {
if (isUnknownOrderError(err)) { if (isUnknownOrderError(err)) {
log("order", "去重时发现订单已不存在,跳过删除"); log("order", t("log.order.dedupeGone"));
} else { } else {
log("error", `去重撤单失败: ${String(err)}`); log("error", t("log.order.dedupeFailed", { error: String(err) }));
} }
} finally { } finally {
unlockOperating(locks, timers, pendings, type); unlockOperating(locks, timers, pendings, type);
@@ -203,10 +210,10 @@ export async function placeOrder(
const type = "LIMIT"; const type = "LIMIT";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
const priceNum = Number(request.price); const priceNum = Number(request.price);
if (!enforceMarkPriceGuard(side, priceNum, guard, log, "限价单")) return; if (!enforceMarkPriceGuard(side, priceNum, guard, log, t("order.kind.limit"))) return;
const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP); const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP);
if (quantity <= 0) { if (quantity <= 0) {
log("error", "限价单数量无效,跳过下单"); log("error", t("log.order.invalidQuantity", { kind: t("order.kind.limit") }));
return; return;
} }
if (!request.skipDedupe) { if (!request.skipDedupe) {
@@ -229,12 +236,21 @@ export async function placeOrder(
clientOrderId: request.clientOrderId, clientOrderId: request.clientOrderId,
}); });
pendings[type] = String(order.orderId); pendings[type] = String(order.orderId);
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${request.slPrice ? ` sl=${request.slPrice}` : ""}`); log(
"order",
t("log.order.limitPlaced", {
side,
price: priceNum,
quantity,
reduceOnly,
sl: request.slPrice ? ` sl=${request.slPrice}` : "",
})
);
return order; return order;
} catch (err) { } catch (err) {
unlockOperating(locks, timers, pendings, type); unlockOperating(locks, timers, pendings, type);
if (isUnknownOrderError(err)) { if (isUnknownOrderError(err)) {
log("order", "订单已成交或被撤销,跳过新单"); log("order", t("log.order.limitGone"));
return undefined; return undefined;
} }
throw err; throw err;
@@ -249,10 +265,10 @@ export async function placeMarketOrder(
const { side, openOrders, guard, reduceOnly = false } = request; const { side, openOrders, guard, reduceOnly = false } = request;
const type = "MARKET"; const type = "MARKET";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return; if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, t("order.kind.market"))) return;
const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP); const quantity = normalizeQuantity(request.amount, request.qtyStep ?? DEFAULT_QTY_STEP);
if (quantity <= 0) { if (quantity <= 0) {
log("error", "市价单数量无效,跳过下单"); log("error", t("log.order.invalidQuantity", { kind: t("order.kind.market") }));
return; return;
} }
await deduplicateOrders(ctx, openOrders, type, side); await deduplicateOrders(ctx, openOrders, type, side);
@@ -268,12 +284,12 @@ export async function placeMarketOrder(
closePosition, closePosition,
}); });
pendings[type] = String(order.orderId); pendings[type] = String(order.orderId);
log("order", `市价单: ${side} 数量 ${quantity} reduceOnly=${reduceOnly}`); log("order", t("log.order.marketPlaced", { side, quantity, reduceOnly }));
return order; return order;
} catch (err) { } catch (err) {
unlockOperating(locks, timers, pendings, type); unlockOperating(locks, timers, pendings, type);
if (isUnknownOrderError(err)) { if (isUnknownOrderError(err)) {
log("order", "市价单失败但订单已不存在,忽略"); log("order", t("log.order.marketGone"));
return undefined; return undefined;
} }
throw err; throw err;
@@ -288,21 +304,21 @@ export async function placeStopLossOrder(
const { side, openOrders, guard, stopPrice, lastPrice } = request; const { side, openOrders, guard, stopPrice, lastPrice } = request;
const type = "STOP_MARKET"; const type = "STOP_MARKET";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return; if (!enforceMarkPriceGuard(side, stopPrice, guard, log, t("order.kind.stop"))) return;
if (lastPrice != null) { if (lastPrice != null) {
if (side === "SELL" && stopPrice >= lastPrice) { if (side === "SELL" && stopPrice >= lastPrice) {
log("error", `止损价 ${stopPrice} 高于或等于当前价 ${lastPrice},取消挂单`); log("error", t("log.order.stopAboveLast", { stopPrice, lastPrice }));
return; return;
} }
if (side === "BUY" && stopPrice <= lastPrice) { if (side === "BUY" && stopPrice <= lastPrice) {
log("error", `止损价 ${stopPrice} 低于或等于当前价 ${lastPrice},取消挂单`); log("error", t("log.order.stopBelowLast", { stopPrice, lastPrice }));
return; return;
} }
} }
const normalizedStop = roundDownToTick(stopPrice, request.priceTick ?? DEFAULT_PRICE_TICK); const normalizedStop = roundDownToTick(stopPrice, request.priceTick ?? DEFAULT_PRICE_TICK);
const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP); const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP);
if (normalizedQty <= 0) { if (normalizedQty <= 0) {
log("error", "止损单数量无效,跳过下单"); log("error", t("log.order.invalidQuantity", { kind: t("order.kind.stop") }));
return; return;
} }
@@ -322,12 +338,12 @@ export async function placeStopLossOrder(
triggerType: "STOP_LOSS", triggerType: "STOP_LOSS",
}); });
pendings[type] = String(order.orderId); pendings[type] = String(order.orderId);
log("stop", `挂止损单: ${side} STOP_MARKET @ ${normalizedStop}`); log("stop", t("log.order.stopPlaced", { side, stopPrice: normalizedStop }));
return order; return order;
} catch (err) { } catch (err) {
unlockOperating(locks, timers, pendings, type); unlockOperating(locks, timers, pendings, type);
if (isUnknownOrderError(err)) { if (isUnknownOrderError(err)) {
log("order", "止损单已失效,跳过"); log("order", t("log.order.stopGone"));
return undefined; return undefined;
} }
throw err; throw err;
@@ -343,14 +359,14 @@ export async function placeTrailingStopOrder(
const type = "TRAILING_STOP_MARKET"; const type = "TRAILING_STOP_MARKET";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
if (!adapter.supportsTrailingStops()) { if (!adapter.supportsTrailingStops()) {
log("error", "当前交易所不支持动态止盈单"); log("error", t("log.order.trailingUnsupported"));
return; return;
} }
if (!enforceMarkPriceGuard(side, activationPrice, guard, log, "动态止盈单")) return; if (!enforceMarkPriceGuard(side, activationPrice, guard, log, t("order.kind.trailing"))) return;
const normalizedActivation = roundDownToTick(activationPrice, request.priceTick ?? DEFAULT_PRICE_TICK); const normalizedActivation = roundDownToTick(activationPrice, request.priceTick ?? DEFAULT_PRICE_TICK);
const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP); const normalizedQty = normalizeQuantity(request.quantity, request.qtyStep ?? DEFAULT_QTY_STEP);
if (normalizedQty <= 0) { if (normalizedQty <= 0) {
log("error", "动态止盈单数量无效,跳过下单"); log("error", t("log.order.invalidQuantity", { kind: t("order.kind.trailing") }));
return; return;
} }
await deduplicateOrders(ctx, openOrders, type, side); await deduplicateOrders(ctx, openOrders, type, side);
@@ -369,13 +385,17 @@ export async function placeTrailingStopOrder(
pendings[type] = String(order.orderId); pendings[type] = String(order.orderId);
log( log(
"order", "order",
`挂动态止盈单: ${side} activation=${normalizedActivation} callbackRate=${callbackRate}` t("log.order.trailingPlaced", {
side,
activation: normalizedActivation,
callbackRate,
})
); );
return order; return order;
} catch (err) { } catch (err) {
unlockOperating(locks, timers, pendings, type); unlockOperating(locks, timers, pendings, type);
if (isUnknownOrderError(err)) { if (isUnknownOrderError(err)) {
log("order", "动态止盈单已失效,跳过"); log("order", t("log.order.trailingGone"));
return undefined; return undefined;
} }
throw err; throw err;
@@ -387,7 +407,7 @@ export async function marketClose(ctx: OrderContext, request: MarketCloseRequest
const { side, openOrders, guard, qtyStep } = request; const { side, openOrders, guard, qtyStep } = request;
const type = "MARKET"; const type = "MARKET";
if (isOperating(locks, type)) return; if (isOperating(locks, type)) return;
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价平仓")) return; if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, t("order.kind.close"))) return;
const rawQuantity = Math.abs(request.quantity); const rawQuantity = Math.abs(request.quantity);
let normalizedQty = qtyStep != null ? normalizeQuantity(rawQuantity, qtyStep) : rawQuantity; let normalizedQty = qtyStep != null ? normalizeQuantity(rawQuantity, qtyStep) : rawQuantity;
@@ -400,7 +420,7 @@ export async function marketClose(ctx: OrderContext, request: MarketCloseRequest
} }
} }
if (normalizedQty <= 0) { if (normalizedQty <= 0) {
log("error", "市价平仓数量无效,跳过下单"); log("error", t("log.order.invalidQuantity", { kind: t("order.kind.close") }));
return; return;
} }
@@ -416,11 +436,11 @@ export async function marketClose(ctx: OrderContext, request: MarketCloseRequest
closePosition: true, closePosition: true,
}); });
pendings[type] = String(order.orderId); pendings[type] = String(order.orderId);
log("close", `市价平仓: ${side}`); log("close", t("log.order.closePlaced", { side }));
} catch (err) { } catch (err) {
unlockOperating(locks, timers, pendings, type); unlockOperating(locks, timers, pendings, type);
if (isUnknownOrderError(err)) { if (isUnknownOrderError(err)) {
log("order", "市场平仓时订单已不存在"); log("order", t("log.order.closeGone"));
return; return;
} }
throw err; throw err;
+135
View File
@@ -725,6 +725,141 @@ const translations: Record<string, TranslationEntry> = {
"log.trend.trailingFail": { zh: "挂动态止盈失败: {error}", en: "Failed to place trailing stop: {error}" }, "log.trend.trailingFail": { zh: "挂动态止盈失败: {error}", en: "Failed to place trailing stop: {error}" },
"log.trend.updateHandlerError": { zh: "更新回调处理异常: {error}", en: "Update handler error: {error}" }, "log.trend.updateHandlerError": { zh: "更新回调处理异常: {error}", en: "Update handler error: {error}" },
"log.trend.snapshotDispatchError": { zh: "快照或更新分发异常: {error}", en: "Snapshot/update dispatch error: {error}" }, "log.trend.snapshotDispatchError": { zh: "快照或更新分发异常: {error}", en: "Snapshot/update dispatch error: {error}" },
// --- core/order-coordinator ---
"order.kind.limit": { zh: "限价单", en: "Limit order" },
"order.kind.market": { zh: "市价单", en: "Market order" },
"order.kind.stop": { zh: "止损单", en: "Stop order" },
"order.kind.trailing": { zh: "动态止盈单", en: "Trailing stop order" },
"order.kind.close": { zh: "市价平仓", en: "Market close" },
"log.order.markGuardBlocked": {
zh: "{kind} 保护触发:side={side} price={price} mark={mark} 超过 {pct}%",
en: "{kind} blocked by mark-price guard: side={side} price={price} mark={mark} exceeds {pct}%",
},
"log.order.lockTimeout": {
zh: "{type} 操作超时自动解锁",
en: "{type} operation timed out; lock released",
},
"log.order.dedupeCancelled": {
zh: "去重撤销重复 {type} 单: {ids}",
en: "Cancelled duplicate {type} orders: {ids}",
},
"log.order.dedupeGone": {
zh: "去重时发现订单已不存在,跳过删除",
en: "Order already gone while deduplicating; skipping cancel",
},
"log.order.dedupeFailed": {
zh: "去重撤单失败: {error}",
en: "Failed to cancel duplicates: {error}",
},
"log.order.invalidQuantity": {
zh: "{kind}数量无效,跳过下单",
en: "{kind} quantity is invalid; skipping",
},
"log.order.limitPlaced": {
zh: "挂限价单: {side} @ {price} 数量 {quantity} reduceOnly={reduceOnly}{sl}",
en: "Placed limit order: {side} @ {price} qty {quantity} reduceOnly={reduceOnly}{sl}",
},
"log.order.limitGone": {
zh: "订单已成交或被撤销,跳过新单",
en: "Order already filled or cancelled; skipping new order",
},
"log.order.marketPlaced": {
zh: "市价单: {side} 数量 {quantity} reduceOnly={reduceOnly}",
en: "Market order: {side} qty {quantity} reduceOnly={reduceOnly}",
},
"log.order.marketGone": {
zh: "市价单失败但订单已不存在,忽略",
en: "Market order failed but the order is already gone; ignoring",
},
"log.order.stopAboveLast": {
zh: "止损价 {stopPrice} 高于或等于当前价 {lastPrice},取消挂单",
en: "Stop price {stopPrice} is at or above the last price {lastPrice}; not placing",
},
"log.order.stopBelowLast": {
zh: "止损价 {stopPrice} 低于或等于当前价 {lastPrice},取消挂单",
en: "Stop price {stopPrice} is at or below the last price {lastPrice}; not placing",
},
"log.order.stopPlaced": {
zh: "挂止损单: {side} STOP_MARKET @ {stopPrice}",
en: "Placed stop order: {side} STOP_MARKET @ {stopPrice}",
},
"log.order.stopGone": { zh: "止损单已失效,跳过", en: "Stop order no longer valid; skipping" },
"log.order.trailingUnsupported": {
zh: "当前交易所不支持动态止盈单",
en: "This exchange does not support trailing stop orders",
},
"log.order.trailingPlaced": {
zh: "挂动态止盈单: {side} activation={activation} callbackRate={callbackRate}",
en: "Placed trailing stop: {side} activation={activation} callbackRate={callbackRate}",
},
"log.order.trailingGone": {
zh: "动态止盈单已失效,跳过",
en: "Trailing stop no longer valid; skipping",
},
"log.order.closePlaced": { zh: "市价平仓: {side}", en: "Market close: {side}" },
"log.order.closeGone": {
zh: "市场平仓时订单已不存在",
en: "Order already gone while closing at market",
},
// --- utils/standx-token-expiry ---
"token.expiringSoon": {
zh: "StandX Token 将在 {minutes} 分钟后过期",
en: "StandX token expires in {minutes} minutes",
},
"token.expiredCancelling": {
zh: "StandX Token 已过期,正在取消所有挂单",
en: "StandX token expired; cancelling all open orders",
},
"token.expiredWithPosition": {
zh: "StandX Token 已过期,仅保留平仓/止损逻辑",
en: "StandX token expired; only close/stop logic remains active",
},
"token.expiredSilent": {
zh: "StandX Token 已过期,进入静默数据接收模式",
en: "StandX token expired; entering silent data-only mode",
},
"log.token.closeOnlyForced": {
zh: "Token 过期,强制进入平仓模式,仅允许 reduce-only 订单",
en: "Token expired; forcing close-only mode, reduce-only orders only",
},
"log.token.silentEntered": {
zh: "进入静默数据接收模式,不再进行任何交易操作",
en: "Entered silent data-only mode; no further trading actions",
},
"log.token.ordersCancelled": {
zh: "Token 过期,已撤销所有挂单",
en: "Token expired; cancelled all open orders",
},
"log.token.cancelOrderMissing": {
zh: "Token 过期撤单时订单已不存在",
en: "Order already gone while cancelling after token expiry",
},
"log.token.cancelFailed": {
zh: "Token 过期撤单失败: {error}",
en: "Failed to cancel orders after token expiry: {error}",
},
"notify.token.title": { zh: "Token 已过期", en: "Token expired" },
"notify.token.closeOnly": {
zh: "Token 已过期,进入平仓模式,不再开新仓",
en: "Token expired; entering close-only mode, no new positions",
},
"notify.token.silent": {
zh: "Token 已过期,策略进入静默模式",
en: "Token expired; strategy entering silent mode",
},
// --- strategy/common/isolated-margin-guard ---
"log.margin.switched": {
zh: "已切换为逐仓模式 (isolated),恢复策略运行",
en: "Switched to isolated margin; resuming strategy",
},
"log.margin.switchUnconfirmed": {
zh: "逐仓模式切换未确认,当前模式: {mode}",
en: "Isolated margin switch unconfirmed; current mode: {mode}",
},
"log.margin.switchFailed": {
zh: "切换逐仓模式失败: {error}",
en: "Failed to switch to isolated margin: {error}",
},
}; };
const formatTemplate = (template: string, params: Record<string, unknown>): string => { const formatTemplate = (template: string, params: Record<string, unknown>): string => {
+4 -3
View File
@@ -1,5 +1,6 @@
import type { AccountSnapshot } from "../../exchanges/types"; import type { AccountSnapshot } from "../../exchanges/types";
import { extractMessage } from "../../utils/errors"; import { extractMessage } from "../../utils/errors";
import { t } from "../../i18n";
import type { LogHandler } from "./subscriptions"; import type { LogHandler } from "./subscriptions";
/** Snapshot refreshes to wait through before giving up on the switch (~5s total). */ /** Snapshot refreshes to wait through before giving up on the switch (~5s total). */
@@ -74,15 +75,15 @@ export class IsolatedMarginGuard {
this.deps.applySnapshot(next); this.deps.applySnapshot(next);
} }
if (this.currentMode() === "isolated") { if (this.currentMode() === "isolated") {
this.deps.log("info", "已切换为逐仓模式 (isolated),恢复策略运行"); this.deps.log("info", t("log.margin.switched"));
return true; return true;
} }
await sleep(CONFIRM_INTERVAL_MS); await sleep(CONFIRM_INTERVAL_MS);
} }
this.deps.log("warn", `逐仓模式切换未确认,当前模式: ${this.currentMode() ?? "unknown"}`); this.deps.log("warn", t("log.margin.switchUnconfirmed", { mode: this.currentMode() ?? "unknown" }));
return false; return false;
} catch (error) { } catch (error) {
this.deps.log("error", `切换逐仓模式失败: ${extractMessage(error)}`); this.deps.log("error", t("log.margin.switchFailed", { error: extractMessage(error) }));
return false; return false;
} finally { } finally {
this.ensuring = null; this.ensuring = null;
+6 -5
View File
@@ -6,6 +6,7 @@ import {
type TokenExpiryState, type TokenExpiryState,
type TokenExpiryStatus, type TokenExpiryStatus,
} from "../../utils/standx-token-expiry"; } from "../../utils/standx-token-expiry";
import { t } from "../../i18n";
import type { LogHandler } from "./subscriptions"; import type { LogHandler } from "./subscriptions";
/** What the engine should do for the rest of this tick. */ /** What the engine should do for the rest of this tick. */
@@ -76,13 +77,13 @@ export class TokenExpiryGuard {
if (status.state === "expired_with_position") { if (status.state === "expired_with_position") {
if (!this.closeOnly) { if (!this.closeOnly) {
this.closeOnly = true; this.closeOnly = true;
this.deps.log("info", "Token 过期,强制进入平仓模式,仅允许 reduce-only 订单"); this.deps.log("info", t("log.token.closeOnlyForced"));
} }
return { halt: false, closeOnly: true }; return { halt: false, closeOnly: true };
} }
if (status.state === "silent" && previousState !== "silent") { if (status.state === "silent" && previousState !== "silent") {
this.deps.log("info", "进入静默数据接收模式,不再进行任何交易操作"); this.deps.log("info", t("log.token.silentEntered"));
} }
return { halt: true, closeOnly: this.closeOnly }; return { halt: true, closeOnly: this.closeOnly };
} }
@@ -120,18 +121,18 @@ export class TokenExpiryGuard {
if (this.cancelDone || openOrderCount === 0) return; if (this.cancelDone || openOrderCount === 0) return;
try { try {
await this.deps.cancelAllOrders(); await this.deps.cancelAllOrders();
this.deps.log("order", "Token 过期,已撤销所有挂单"); this.deps.log("order", t("log.token.ordersCancelled"));
this.deps.onOrdersCancelled(); this.deps.onOrdersCancelled();
this.cancelDone = true; this.cancelDone = true;
} catch (error) { } catch (error) {
if (isUnknownOrderError(error)) { if (isUnknownOrderError(error)) {
// Nothing left to cancel is the outcome we wanted. // Nothing left to cancel is the outcome we wanted.
this.deps.log("order", "Token 过期撤单时订单已不存在"); this.deps.log("order", t("log.token.cancelOrderMissing"));
this.cancelDone = true; this.cancelDone = true;
return; return;
} }
// Leave cancelDone false so the next tick retries. // Leave cancelDone false so the next tick retries.
this.deps.log("error", `Token 过期撤单失败: ${extractMessage(error)}`); this.deps.log("error", t("log.token.cancelFailed", { error: extractMessage(error) }));
} }
} }
} }
+2 -4
View File
@@ -213,10 +213,8 @@ export class MakerPointsEngine {
type: "token_expired", type: "token_expired",
level: "warn", level: "warn",
symbol: this.config.symbol, symbol: this.config.symbol,
title: "Token 已过期", title: t("notify.token.title"),
message: hasPosition message: hasPosition ? t("notify.token.closeOnly") : t("notify.token.silent"),
? "Token 已过期,进入平仓模式,不再开新仓"
: "Token 已过期,策略进入静默模式",
details: { hasPosition, hasOpenOrders, state }, details: { hasPosition, hasOpenOrders, state },
}), }),
cancelAllOrders: () => this.exchange.cancelAllOrders({ symbol: this.config.symbol }), cancelAllOrders: () => this.exchange.cancelAllOrders({ symbol: this.config.symbol }),
+10 -8
View File
@@ -32,6 +32,7 @@ import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { decryptCopyright } from "../utils/copyright"; import { decryptCopyright } from "../utils/copyright";
import { isRateLimitError } from "../utils/errors"; import { isRateLimitError } from "../utils/errors";
import { RateLimitController } from "../core/lib/rate-limit"; import { RateLimitController } from "../core/lib/rate-limit";
import type { TrendLabel } from "../utils/format";
import { StrategyEventEmitter } from "./common/event-emitter"; import { StrategyEventEmitter } from "./common/event-emitter";
import { createPrecisionSyncer, type PrecisionSyncer } from "./common/precision-syncer"; import { createPrecisionSyncer, type PrecisionSyncer } from "./common/precision-syncer";
import { safeSubscribe, type LogHandler } from "./common/subscriptions"; import { safeSubscribe, type LogHandler } from "./common/subscriptions";
@@ -44,7 +45,7 @@ export interface TrendEngineSnapshot {
lastPrice: number | null; lastPrice: number | null;
sma30: number | null; sma30: number | null;
bollingerBandwidth: number | null; bollingerBandwidth: number | null;
trend: "做多" | "做空" | "无信号"; trend: TrendLabel;
position: PositionSnapshot; position: PositionSnapshot;
pnl: number; pnl: number;
unrealized: number; unrealized: number;
@@ -978,13 +979,14 @@ export class TrendEngine {
const position = getPosition(this.accountSnapshot, this.config.symbol); const position = getPosition(this.accountSnapshot, this.config.symbol);
const price = this.tickerSnapshot ? Number(this.tickerSnapshot.lastPrice) : null; const price = this.tickerSnapshot ? Number(this.tickerSnapshot.lastPrice) : null;
const sma30 = this.lastSma30; const sma30 = this.lastSma30;
const trend = price == null || sma30 == null const trend: TrendLabel =
? "无信号" price == null || sma30 == null
: price > sma30 ? "none"
? "做多" : price > sma30
: price < sma30 ? "long"
? "做空" : price < sma30
: "无信号"; ? "short"
: "none";
const pnl = price != null ? computePositionPnl(position, price, price) : 0; const pnl = price != null ? computePositionPnl(position, price, price) : 0;
return { return {
ready: this.isReady(), ready: this.isReady(),
+4 -3
View File
@@ -1,10 +1,11 @@
import { t } from "../i18n"; import { t } from "../i18n";
export type TrendLabel = "做多" | "做空" | "无信号"; /** Direction the trend engine reports; a domain value, not display text. */
export type TrendLabel = "long" | "short" | "none";
export function formatTrendLabel(trend: TrendLabel): string { export function formatTrendLabel(trend: TrendLabel): string {
if (trend === "做多") return t("trend.label.long"); if (trend === "long") return t("trend.label.long");
if (trend === "做空") return t("trend.label.short"); if (trend === "short") return t("trend.label.short");
return t("trend.label.none"); return t("trend.label.none");
} }
+5 -4
View File
@@ -1,4 +1,5 @@
import { isStandxTokenExpired, getStandxTokenExpiryInfo, standxTokenConfig } from "../config"; import { isStandxTokenExpired, getStandxTokenExpiryInfo, standxTokenConfig } from "../config";
import { t } from "../i18n";
export type TokenExpiryState = "active" | "expired" | "expired_with_position" | "silent"; export type TokenExpiryState = "active" | "expired" | "expired_with_position" | "silent";
@@ -68,18 +69,18 @@ export function formatTokenExpiryMessage(status: TokenExpiryStatus): string | nu
if (!status.expired) { if (!status.expired) {
if (status.remainingMs != null && status.remainingMs < 3600_000) { if (status.remainingMs != null && status.remainingMs < 3600_000) {
const mins = Math.ceil(status.remainingMs / 60_000); const mins = Math.ceil(status.remainingMs / 60_000);
return `StandX Token 将在 ${mins} 分钟后过期`; return t("token.expiringSoon", { minutes: mins });
} }
return null; return null;
} }
switch (status.state) { switch (status.state) {
case "expired": case "expired":
return "StandX Token 已过期,正在取消所有挂单"; return t("token.expiredCancelling");
case "expired_with_position": case "expired_with_position":
return "StandX Token 已过期,仅保留平仓/止损逻辑"; return t("token.expiredWithPosition");
case "silent": case "silent":
return "StandX Token 已过期,进入静默数据接收模式"; return t("token.expiredSilent");
default: default:
return null; return null;
} }
+2 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { IsolatedMarginGuard } from "../src/strategy/common/isolated-margin-guard"; import { IsolatedMarginGuard } from "../src/strategy/common/isolated-margin-guard";
import type { AccountSnapshot } from "../src/exchanges/types"; import type { AccountSnapshot } from "../src/exchanges/types";
import { t } from "../src/i18n";
const SYMBOL = "BTC-USD"; const SYMBOL = "BTC-USD";
@@ -71,7 +72,7 @@ describe("IsolatedMarginGuard", () => {
}); });
expect(await guard.ensureIsolated()).toBe(true); expect(await guard.ensureIsolated()).toBe(true);
expect(changeMarginMode).toHaveBeenCalledWith({ symbol: SYMBOL, marginMode: "isolated" }); expect(changeMarginMode).toHaveBeenCalledWith({ symbol: SYMBOL, marginMode: "isolated" });
expect(logs.some(([, detail]) => detail.includes("已切换为逐仓模式"))).toBe(true); expect(logs.some(([, detail]) => detail === t("log.margin.switched"))).toBe(true);
}); });
it("gives up after the confirm attempts run out", async () => { it("gives up after the confirm attempts run out", async () => {
+8 -4
View File
@@ -1,6 +1,7 @@
import { describe, expect, it, vi, beforeEach, afterAll } from "vitest"; import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter"; import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { Order } from "../src/exchanges/types"; import type { Order } from "../src/exchanges/types";
import { t } from "../src/i18n";
import type { import type {
OrderContext, OrderContext,
OrderLockMap, OrderLockMap,
@@ -84,7 +85,7 @@ describe("order-coordinator", () => {
]; ];
await deduplicateOrders(ctx, openOrders, "LIMIT", "BUY"); await deduplicateOrders(ctx, openOrders, "LIMIT", "BUY");
expect(adapter.cancelOrders).toHaveBeenCalledWith({ symbol: "BTCUSDT", orderIdList: [2] }); expect(adapter.cancelOrders).toHaveBeenCalledWith({ symbol: "BTCUSDT", orderIdList: [2] });
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("去重撤销重复")); expect(log).toHaveBeenCalledWith("order", t("log.order.dedupeCancelled", { type: "LIMIT", ids: "2" }));
}); });
it("places limit orders and records pending id", async () => { it("places limit orders and records pending id", async () => {
@@ -112,7 +113,7 @@ describe("order-coordinator", () => {
lastPrice: 100, lastPrice: 100,
}); });
expect(adapter.createOrder).toHaveBeenCalled(); expect(adapter.createOrder).toHaveBeenCalled();
expect(log).toHaveBeenCalledWith("stop", expect.stringContaining("STOP_MARKET")); expect(log).toHaveBeenCalledWith("stop", t("log.order.stopPlaced", { side: "SELL", stopPrice: 99 }));
}); });
it("places trailing stop order", async () => { it("places trailing stop order", async () => {
@@ -125,7 +126,10 @@ describe("order-coordinator", () => {
callbackRate: 0.2, callbackRate: 0.2,
}); });
expect(adapter.createOrder).toHaveBeenCalled(); expect(adapter.createOrder).toHaveBeenCalled();
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("挂动态止盈单")); expect(log).toHaveBeenCalledWith(
"order",
t("log.order.trailingPlaced", { side: "SELL", activation: 101, callbackRate: 0.2 })
);
}); });
it("market close cancels open orders before placing close order", async () => { it("market close cancels open orders before placing close order", async () => {
@@ -136,7 +140,7 @@ describe("order-coordinator", () => {
quantity: 1, quantity: 1,
}); });
expect(adapter.createOrder).toHaveBeenCalled(); expect(adapter.createOrder).toHaveBeenCalled();
expect(log).toHaveBeenCalledWith("close", expect.stringContaining("市价平仓")); expect(log).toHaveBeenCalledWith("close", t("log.order.closePlaced", { side: "SELL" }));
}); });
it("unlockOperating clears timers and pending", () => { it("unlockOperating clears timers and pending", () => {
+2 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, it, vi, afterEach } from "vitest"; import { describe, expect, it, vi, afterEach } from "vitest";
import { standxTokenConfig } from "../src/config"; import { standxTokenConfig } from "../src/config";
import { t } from "../src/i18n";
import { TokenExpiryGuard } from "../src/strategy/common/token-expiry-guard"; import { TokenExpiryGuard } from "../src/strategy/common/token-expiry-guard";
const HOUR_MS = 3_600_000; const HOUR_MS = 3_600_000;
@@ -83,7 +84,7 @@ describe("TokenExpiryGuard", () => {
await guard.evaluate({ positionAmt: 0, openOrderCount: 0 }); await guard.evaluate({ positionAmt: 0, openOrderCount: 0 });
await guard.evaluate({ positionAmt: 0, openOrderCount: 0 }); await guard.evaluate({ positionAmt: 0, openOrderCount: 0 });
const entryLogs = logs.filter( const entryLogs = logs.filter(
([type, detail]) => type === "info" && detail.includes("静默数据接收模式") ([type, detail]) => type === "info" && detail === t("log.token.silentEntered")
); );
expect(entryLogs).toHaveLength(1); expect(entryLogs).toHaveLength(1);
}); });