Implement internationalization support by adding translation functionality and updating UI components to use translated strings. Add language configuration in .env.example and integrate translations across various strategy and UI components.

This commit is contained in:
discountry
2025-12-09 00:35:28 +08:00
parent 03df1006cc
commit 85e7f245c0
17 changed files with 1166 additions and 295 deletions
+25 -12
View File
@@ -5,6 +5,7 @@ import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/client"
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { StrategyEventEmitter } from "./common/event-emitter";
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
import { t } from "../i18n";
export interface BasisArbSnapshot {
ready: boolean;
@@ -159,8 +160,8 @@ export class BasisArbEngine {
},
log,
{
subscribeFail: (error) => `订阅期货深度失败: ${String(error)}`,
processFail: (error) => `处理期货深度异常: ${String(error)}`,
subscribeFail: (error) => t("log.basis.subscribeFuturesDepthFail", { error: String(error) }),
processFail: (error) => t("log.basis.processFuturesDepthError", { error: String(error) }),
}
);
}
@@ -179,7 +180,7 @@ export class BasisArbEngine {
this.futures.updatedAt = depth.eventTime ?? depth.tradeTime ?? this.now();
if (!this.feedReady.futures) {
this.feedReady.futures = true;
this.tradeLog.push("info", `期货深度已就绪 (${this.config.futuresSymbol})`);
this.tradeLog.push("info", t("log.basis.futuresReady", { symbol: this.config.futuresSymbol }));
}
if (this.feedReady.futures && this.feedReady.spot && this.marketReadyAt == null) {
this.marketReadyAt = this.now();
@@ -197,7 +198,10 @@ export class BasisArbEngine {
this.applySpotTicker(ticker);
} catch (error) {
this.feedReady.spot = false;
this.tradeLog.push("error", `获取现货盘口失败: ${String(error instanceof Error ? error.message : error)}`);
this.tradeLog.push(
"error",
t("log.basis.spotDepthError", { error: String(error instanceof Error ? error.message : error) })
);
} finally {
this.spotInFlight = false;
}
@@ -217,13 +221,16 @@ export class BasisArbEngine {
this.funding.updatedAt = typeof ts === "number" ? ts : this.now();
if (!this.feedReady.funding) {
this.feedReady.funding = true;
this.tradeLog.push("info", `资金费率已就绪 (${this.config.futuresSymbol})`);
this.tradeLog.push("info", t("log.basis.fundingReady", { symbol: this.config.futuresSymbol }));
}
this.emitUpdate();
}
} catch (error) {
this.feedReady.funding = false;
this.tradeLog.push("error", `获取资金费率失败: ${String(error instanceof Error ? error.message : error)}`);
this.tradeLog.push(
"error",
t("log.basis.fundingError", { error: String(error instanceof Error ? error.message : error) })
);
} finally {
this.fundingInFlight = false;
}
@@ -250,7 +257,10 @@ export class BasisArbEngine {
this.spotBalances = next;
this.emitUpdate();
} catch (error) {
this.tradeLog.push("error", `获取现货余额失败: ${String(error instanceof Error ? error.message : error)}`);
this.tradeLog.push(
"error",
t("log.basis.spotBalanceError", { error: String(error instanceof Error ? error.message : error) })
);
} finally {
this.spotAccountInFlight = false;
}
@@ -278,7 +288,10 @@ export class BasisArbEngine {
this.futuresBalances = next;
this.emitUpdate();
} catch (error) {
this.tradeLog.push("error", `获取合约余额失败: ${String(error instanceof Error ? error.message : error)}`);
this.tradeLog.push(
"error",
t("log.basis.futuresBalanceError", { error: String(error instanceof Error ? error.message : error) })
);
} finally {
this.futuresAccountInFlight = false;
}
@@ -295,7 +308,7 @@ export class BasisArbEngine {
this.spot.updatedAt = ticker.time ?? this.now();
if (!this.feedReady.spot) {
this.feedReady.spot = true;
this.tradeLog.push("info", `现货盘口已就绪 (${this.config.spotSymbol})`);
this.tradeLog.push("info", t("log.basis.spotReady", { symbol: this.config.spotSymbol }));
}
if (this.feedReady.futures && this.feedReady.spot && this.marketReadyAt == null) {
this.marketReadyAt = this.now();
@@ -308,7 +321,7 @@ export class BasisArbEngine {
const snapshot = this.buildSnapshot();
this.evaluateSignals(snapshot);
this.events.emit("update", snapshot, (error) => {
this.tradeLog.push("error", `推送订阅失败: ${String(error)}`);
this.tradeLog.push("error", t("log.basis.pushError", { error: String(error) }));
});
}
@@ -410,7 +423,7 @@ export class BasisArbEngine {
this.lastEntrySignalAt = now;
const bpTxt = (spreadBps as number).toFixed(2);
const minutes = Math.floor(((msUntilFunding as number) / 60000));
this.tradeLog.push("entry", `入场机会: 扣费后价差 ${bpTxt} bp 距下次资金费约 ${minutes} 分钟`);
this.tradeLog.push("entry", t("log.basis.entryOpportunity", { bp: bpTxt, minutes }));
}
}
@@ -419,7 +432,7 @@ export class BasisArbEngine {
if (now - this.lastExitSignalAt >= 60 * 1000) { // debounce 60s
this.lastExitSignalAt = now;
const minutes = Math.max(0, Math.floor(((msUntilFunding as number) / 60000)));
this.tradeLog.push("exit", `出场机会: 资金费率为负 | 距收取约 ${minutes} 分钟`);
this.tradeLog.push("exit", t("log.basis.exitOpportunity", { minutes }));
}
}
}
+35 -24
View File
@@ -19,6 +19,7 @@ import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
import { extractMessage, isUnknownOrderError } from "../utils/errors";
import { formatPriceToString } from "../utils/math";
import { computePositionPnl } from "../utils/pnl";
import { t } from "../i18n";
export interface GuardianEngineSnapshot {
ready: boolean;
@@ -108,8 +109,8 @@ export class GuardianEngine {
},
log,
{
subscribeFail: (error) => `订阅账户失败: ${String(error)}`,
processFail: (error) => `账户推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.accountFail", { error: String(error) }),
processFail: (error) => t("log.process.accountError", { error: extractMessage(error) }),
}
);
@@ -135,8 +136,8 @@ export class GuardianEngine {
},
log,
{
subscribeFail: (error) => `订阅订单失败: ${String(error)}`,
processFail: (error) => `订单推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.orderFail", { error: String(error) }),
processFail: (error) => t("log.process.orderError", { error: extractMessage(error) }),
}
);
@@ -148,8 +149,8 @@ export class GuardianEngine {
},
log,
{
subscribeFail: (error) => `订阅Ticker失败: ${String(error)}`,
processFail: (error) => `价格推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.tickerFail", { error: String(error) }),
processFail: (error) => t("log.process.tickerError", { error: extractMessage(error) }),
}
);
}
@@ -179,7 +180,7 @@ export class GuardianEngine {
}
await this.ensureProtection();
} catch (error) {
this.tradeLog.push("error", `Guardian 执行异常: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.guardian.executeError", { error: extractMessage(error) }));
} finally {
this.processing = false;
this.emitUpdate();
@@ -200,7 +201,7 @@ export class GuardianEngine {
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.guardian.entryPricePending"));
this.entryPricePendingLogged = true;
}
return;
@@ -210,7 +211,7 @@ export class GuardianEngine {
const price = this.getLastPrice();
if (!Number.isFinite(price)) {
if (!this.priceUnavailableLogged) {
this.tradeLog.push("info", "行情尚未就绪,等待最新价格以同步止损");
this.tradeLog.push("info", t("log.guardian.pricePending"));
this.priceUnavailableLogged = true;
}
return;
@@ -406,7 +407,7 @@ export class GuardianEngine {
this.lastStopAttempt.price = stopPrice;
this.lastStopAttempt.at = now;
} catch (err) {
this.tradeLog.push("error", `挂止损单失败: ${String(err)}`);
this.tradeLog.push("error", t("log.guardian.placeStopFail", { error: String(err) }));
this.lastStopAttempt.side = side;
this.lastStopAttempt.price = stopPrice;
this.lastStopAttempt.at = now;
@@ -430,10 +431,10 @@ export class GuardianEngine {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: currentOrder.orderId });
} catch (err) {
if (isUnknownOrderError(err)) {
this.tradeLog.push("order", "原止损单已不存在,跳过撤销");
this.tradeLog.push("order", t("log.guardian.stopMissingSkip"));
this.openOrders = this.openOrders.filter((o) => o.orderId !== currentOrder.orderId);
} else {
this.tradeLog.push("error", `取消原止损单失败: ${String(err)}`);
this.tradeLog.push("error", t("log.guardian.cancelStopFail", { error: String(err) }));
}
}
try {
@@ -462,10 +463,15 @@ export class GuardianEngine {
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
if (order) {
this.tradeLog.push("stop", `移动止损到 ${formatPriceToString(nextStopPrice, this.resolvePriceDecimals())}`);
this.tradeLog.push(
"stop",
t("log.guardian.moveStop", {
price: formatPriceToString(nextStopPrice, this.resolvePriceDecimals()),
})
);
}
} catch (err) {
this.tradeLog.push("error", `移动止损失败: ${String(err)}`);
this.tradeLog.push("error", t("log.guardian.moveStopFail", { error: String(err) }));
try {
const position = getPosition(this.accountSnapshot, this.config.symbol);
const quantity = Math.abs(position.positionAmt);
@@ -494,11 +500,13 @@ export class GuardianEngine {
if (restored && Number.isFinite(existingStopPrice)) {
this.tradeLog.push(
"order",
`恢复原止损 @ ${formatPriceToString(existingStopPrice, this.resolvePriceDecimals())}`
t("log.guardian.restoreStop", {
price: formatPriceToString(existingStopPrice, this.resolvePriceDecimals()),
})
);
}
} catch (recoverErr) {
this.tradeLog.push("error", `恢复原止损失败: ${String(recoverErr)}`);
this.tradeLog.push("error", t("log.guardian.restoreStopFail", { error: String(recoverErr) }));
}
}
}
@@ -531,7 +539,7 @@ export class GuardianEngine {
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
} catch (err) {
this.tradeLog.push("error", `挂动态止盈失败: ${String(err)}`);
this.tradeLog.push("error", t("log.guardian.trailingFail", { error: String(err) }));
}
}
@@ -541,12 +549,12 @@ export class GuardianEngine {
const orderIdList = protectiveOrders.map((order) => order.orderId);
try {
await this.exchange.cancelOrders({ symbol: this.config.symbol, orderIdList });
this.tradeLog.push("order", `清理遗留保护单: ${orderIdList.join(",")}`);
this.tradeLog.push("order", t("log.guardian.cleanupOrders", { ids: orderIdList.join(",") }));
} catch (err) {
if (isUnknownOrderError(err)) {
this.tradeLog.push("order", "保护单已不存在,跳过清理");
this.tradeLog.push("order", t("log.guardian.protectiveMissing"));
} else {
this.tradeLog.push("error", `清理保护单失败: ${String(err)}`);
this.tradeLog.push("error", t("log.guardian.cleanupFail", { error: String(err) }));
}
}
}
@@ -625,10 +633,10 @@ export class GuardianEngine {
try {
const snapshot = this.buildSnapshot();
this.events.emit("update", snapshot, (error) => {
this.tradeLog.push("error", `更新分发异常: ${String(error)}`);
this.tradeLog.push("error", t("log.guardian.dispatchError", { error: String(error) }));
});
} catch (err) {
this.tradeLog.push("error", `构建快照失败: ${String(err)}`);
this.tradeLog.push("error", t("log.guardian.snapshotFail", { error: String(err) }));
}
}
@@ -668,12 +676,15 @@ export class GuardianEngine {
if (updated) {
this.tradeLog.push(
"info",
`已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`
t("log.guardian.precisionSynced", {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
})
);
}
})
.catch((error) => {
this.tradeLog.push("error", `同步精度失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.guardian.precisionFailed", { error: extractMessage(error) }));
this.precisionSync = null;
setTimeout(() => this.syncPrecision(), 2000);
});
+54 -39
View File
@@ -27,6 +27,7 @@ import { RateLimitController } from "../core/lib/rate-limit";
import { StrategyEventEmitter } from "./common/event-emitter";
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
import { SessionVolumeTracker } from "./common/session-volume";
import { t } from "../i18n";
interface DesiredOrder {
side: "BUY" | "SELL";
@@ -164,7 +165,7 @@ export class MakerEngine {
const position = getPosition(snapshot, this.config.symbol);
this.sessionVolume.update(position, this.getReferencePrice());
if (!this.feedArrived.account) {
this.tradeLog.push("info", "账户快照已同步");
this.tradeLog.push("info", t("log.account.snapshotSynced"));
this.feedArrived.account = true;
}
this.feedStatus.account = true;
@@ -172,8 +173,8 @@ export class MakerEngine {
},
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) }),
}
);
@@ -192,7 +193,7 @@ export class MakerEngine {
}
this.initialOrderSnapshotReady = true;
if (!this.feedArrived.orders) {
this.tradeLog.push("info", "订单快照已返回");
this.tradeLog.push("info", t("log.order.snapshotReturned"));
this.feedArrived.orders = true;
}
this.feedStatus.orders = true;
@@ -200,8 +201,8 @@ export class MakerEngine {
},
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) }),
}
);
@@ -210,7 +211,7 @@ export class MakerEngine {
(depth) => {
this.depthSnapshot = depth;
if (!this.feedArrived.depth) {
this.tradeLog.push("info", "获得最新深度行情");
this.tradeLog.push("info", t("log.depth.ready"));
this.feedArrived.depth = true;
}
this.feedStatus.depth = true;
@@ -218,8 +219,8 @@ export class MakerEngine {
},
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) }),
}
);
@@ -228,7 +229,7 @@ export class MakerEngine {
(ticker) => {
this.tickerSnapshot = ticker;
if (!this.feedArrived.ticker) {
this.tradeLog.push("info", "Ticker 已就绪");
this.tradeLog.push("info", t("log.ticker.ready"));
this.feedArrived.ticker = true;
}
this.feedStatus.ticker = true;
@@ -236,8 +237,8 @@ export class MakerEngine {
},
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) }),
}
);
@@ -331,9 +332,9 @@ export class MakerEngine {
hadRateLimit = true;
this.rateLimit.registerRateLimit("maker");
await this.enforceRateLimitStop();
this.tradeLog.push("warn", `MakerEngine 429: ${String(error)}`);
this.tradeLog.push("warn", t("log.maker.rateLimit429", { error: String(error) }));
} else {
this.tradeLog.push("error", `做市循环异常: ${String(error)}`);
this.tradeLog.push("error", t("log.maker.loopError", { error: String(error) }));
}
this.emitUpdate();
} finally {
@@ -367,18 +368,18 @@ export class MakerEngine {
unlockOperating(this.locks, this.timers, this.pending, "LIMIT");
this.openOrders = [];
this.emitUpdate();
this.tradeLog.push("order", "启动时清理历史挂单");
this.tradeLog.push("order", t("log.maker.cleanOrdersStart"));
this.initialOrderResetDone = true;
return true;
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "历史挂单已消失,跳过启动清理");
this.tradeLog.push("order", t("log.maker.cleanOrdersMissing"));
this.initialOrderResetDone = true;
this.openOrders = [];
this.emitUpdate();
return true;
}
this.tradeLog.push("error", `启动撤单失败: ${String(error)}`);
this.tradeLog.push("error", t("log.maker.cleanOrdersFail", { error: String(error) }));
return false;
}
}
@@ -401,16 +402,20 @@ export class MakerEngine {
() => {
this.tradeLog.push(
"order",
`撤销不匹配订单 ${order.side} @ ${order.price} reduceOnly=${order.reduceOnly}`
t("log.maker.cancelMismatched", {
side: order.side,
price: order.price,
reduceOnly: order.reduceOnly,
})
);
},
() => {
this.tradeLog.push("order", "撤销时发现订单已被成交/取消,忽略");
this.tradeLog.push("order", t("log.maker.cancelMissing"));
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.maker.cancelFail", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
@@ -449,7 +454,11 @@ export class MakerEngine {
}
this.tradeLog.push(
"error",
`挂单失败(${target.side} ${target.price}): ${extractMessage(error)}`
t("log.maker.placeFail", {
side: target.side,
price: target.price,
error: extractMessage(error),
})
);
}
}
@@ -462,7 +471,7 @@ export class MakerEngine {
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.maker.avgPending"));
this.entryPricePendingLogged = true;
}
return;
@@ -478,7 +487,10 @@ export class MakerEngine {
const closeSidePrice = closeSideIsSell ? bidPrice : askPrice;
this.tradeLog.push(
"stop",
`触发止损,方向=${position.positionAmt > 0 ? "多" : "空"} 当前亏损=${pnl.toFixed(4)} USDT`
t("log.maker.stopTriggered", {
direction: position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
pnl: pnl.toFixed(4),
})
);
try {
await this.flushOrders();
@@ -501,9 +513,9 @@ export class MakerEngine {
);
} catch (error) {
if (isUnknownOrderError(error)) {
this.tradeLog.push("order", "止损平仓时订单已不存在");
this.tradeLog.push("order", t("log.maker.stopOrderMissing"));
} else {
this.tradeLog.push("error", `止损平仓失败: ${String(error)}`);
this.tradeLog.push("error", t("log.maker.stopCloseFail", { error: String(error) }));
}
}
}
@@ -522,12 +534,12 @@ export class MakerEngine {
// 成功撤销不记录日志,保持现有行为
},
() => {
this.tradeLog.push("order", "订单已不存在,撤销跳过");
this.tradeLog.push("order", t("log.maker.orderMissing"));
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.maker.cancelFail", { error: String(error) }));
this.pendingCancelOrders.delete(String(order.orderId));
this.openOrders = this.openOrders.filter((existing) => existing.orderId !== order.orderId);
}
@@ -559,12 +571,15 @@ export class MakerEngine {
if (updated) {
this.tradeLog.push(
"info",
`已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`
t("log.common.precisionSynced", {
priceTick: precision.priceTick,
qtyStep: precision.qtyStep,
})
);
}
})
.catch((error) => {
this.tradeLog.push("error", `同步精度失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.common.precisionFailed", { error: extractMessage(error) }));
this.precisionSync = null;
setTimeout(() => this.syncPrecision(), 2000);
});
@@ -581,10 +596,10 @@ export class MakerEngine {
try {
const snapshot = this.buildSnapshot();
this.events.emit("update", snapshot, (error) => {
this.tradeLog.push("error", `更新回调处理异常: ${String(error)}`);
this.tradeLog.push("error", t("log.maker.updateHandlerError", { error: String(error) }));
});
} catch (err) {
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
this.tradeLog.push("error", t("log.maker.snapshotDispatchError", { error: String(err) }));
}
}
@@ -619,19 +634,19 @@ export class MakerEngine {
private logReadinessBlockers(): void {
if (!this.feedStatus.account && !this.readinessLogged.account) {
this.tradeLog.push("info", "等待账户快照同步,尚未开始做市");
this.tradeLog.push("info", t("log.maker.waitAccount"));
this.readinessLogged.account = true;
}
if (!this.feedStatus.depth && !this.readinessLogged.depth) {
this.tradeLog.push("info", "等待深度行情推送,尚未开始做市");
this.tradeLog.push("info", t("log.maker.waitDepth"));
this.readinessLogged.depth = true;
}
if (!this.feedStatus.ticker && !this.readinessLogged.ticker) {
this.tradeLog.push("info", "等待Ticker推送,尚未开始做市");
this.tradeLog.push("info", t("log.maker.waitTicker"));
this.readinessLogged.ticker = true;
}
if (!this.feedStatus.orders && !this.readinessLogged.orders) {
this.tradeLog.push("info", "等待订单快照返回,尚未执行初始化撤单");
this.tradeLog.push("info", t("log.maker.waitOrders"));
this.readinessLogged.orders = true;
}
}
@@ -648,7 +663,7 @@ export class MakerEngine {
private logDesiredOrders(desired: DesiredOrder[]): void {
if (!desired.length) {
if (this.lastDesiredSummary !== "none") {
this.tradeLog.push("info", "当前无目标挂单,等待下一次刷新");
this.tradeLog.push("info", t("log.maker.noTargets"));
this.lastDesiredSummary = "none";
}
return;
@@ -657,7 +672,7 @@ export class MakerEngine {
.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.maker.targetsSummary", { summary }));
this.lastDesiredSummary = summary;
}
}
@@ -673,14 +688,14 @@ export class MakerEngine {
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.maker.balanceThrottle", { 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.maker.balanceResumed"));
this.insufficientBalanceNotified = false;
this.lastInsufficientMessage = null;
}
+77 -43
View File
@@ -35,6 +35,7 @@ import { RateLimitController } from "../core/lib/rate-limit";
import { StrategyEventEmitter } from "./common/event-emitter";
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
import { SessionVolumeTracker } from "./common/session-volume";
import { t } from "../i18n";
export interface TrendEngineSnapshot {
ready: boolean;
@@ -175,8 +176,8 @@ export class TrendEngine {
},
log,
{
subscribeFail: (error) => `订阅账户失败: ${String(error)}`,
processFail: (error) => `账户推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.accountFail", { error: String(error) }),
processFail: (error) => t("log.process.accountError", { error: extractMessage(error) }),
}
);
@@ -209,8 +210,8 @@ export class TrendEngine {
},
log,
{
subscribeFail: (error) => `订阅订单失败: ${String(error)}`,
processFail: (error) => `订单推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.orderFail", { error: String(error) }),
processFail: (error) => t("log.process.orderError", { error: extractMessage(error) }),
}
);
@@ -222,8 +223,8 @@ export class TrendEngine {
},
log,
{
subscribeFail: (error) => `订阅深度失败: ${String(error)}`,
processFail: (error) => `深度推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.depthFail", { error: String(error) }),
processFail: (error) => t("log.process.depthError", { error: extractMessage(error) }),
}
);
@@ -235,8 +236,8 @@ export class TrendEngine {
},
log,
{
subscribeFail: (error) => `订阅Ticker失败: ${String(error)}`,
processFail: (error) => `价格推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.subscribe.tickerFail", { error: String(error) }),
processFail: (error) => t("log.process.tickerError", { error: extractMessage(error) }),
}
);
@@ -251,8 +252,8 @@ export class TrendEngine {
},
log,
{
subscribeFail: (error) => `订阅K线失败: ${String(error)}`,
processFail: (error) => `K线推送处理异常: ${extractMessage(error)}`,
subscribeFail: (error) => t("log.kline.subscribeFail", { error: String(error) }),
processFail: (error) => t("log.kline.processError", { error: extractMessage(error) }),
}
);
}
@@ -287,7 +288,12 @@ export class TrendEngine {
const closes = this.klineSnapshot.slice(-5).map((k) => Number(k.close).toFixed(2));
this.tradeLog.push(
"info",
`K线不足 ${count}/${minKlines},最近收盘(${closes.length}): ${closes.join(", ")}`
t("log.trend.klineInsufficient", {
count,
min: minKlines,
recentCount: closes.length,
recent: closes.join(", "),
})
);
this.klineInsufficientLogged = true;
}
@@ -297,7 +303,7 @@ export class TrendEngine {
const closes = this.klineSnapshot.slice(-5).map((k) => Number(k.close).toFixed(2));
this.tradeLog.push(
"info",
`K线就绪 ${count} 根,可计算 SMA30。最近收盘: ${closes.join(", ")}`
t("log.trend.klineReady", { count, recent: closes.join(", ") })
);
this.klineReadyLogged = true;
}
@@ -361,16 +367,16 @@ export class TrendEngine {
hadRateLimit = true;
this.rateLimit.registerRateLimit("trend");
await this.enforceRateLimitStop();
this.tradeLog.push("warn", `TrendEngine 429: ${String(error)}`);
this.tradeLog.push("warn", t("log.trend.rateLimit429", { error: String(error) }));
} else {
this.tradeLog.push("error", `策略循环异常: ${String(error)}`);
this.tradeLog.push("error", t("log.trend.loopError", { error: String(error) }));
}
this.emitUpdate();
} finally {
try {
this.rateLimit.onCycleComplete(hadRateLimit);
} catch (rateLimitError) {
this.tradeLog.push("error", `限频控制器状态更新失败: ${String(rateLimitError)}`);
this.tradeLog.push("error", t("log.trend.rateLimitUpdateError", { error: String(rateLimitError) }));
} finally {
this.processing = false;
}
@@ -395,11 +401,15 @@ export class TrendEngine {
if (hasPosition) {
this.tradeLog.push(
"info",
`检测到已有持仓: ${position.positionAmt > 0 ? "多" : "空"} ${Math.abs(position.positionAmt).toFixed(4)} @ ${position.entryPrice.toFixed(2)}`
t("log.trend.detectPosition", {
direction: position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
amount: Math.abs(position.positionAmt).toFixed(4),
price: position.entryPrice.toFixed(2),
})
);
}
if (this.openOrders.length > 0) {
this.tradeLog.push("info", `检测到已有挂单 ${this.openOrders.length} 笔,将按策略规则接管`);
this.tradeLog.push("info", t("log.trend.detectOrders", { count: this.openOrders.length }));
}
this.startupLogged = true;
}
@@ -415,12 +425,12 @@ export class TrendEngine {
// 止损后的冷却期:60s 内不允许基于 SMA 穿越再次入场
if (this.lastStopLossAt != null && now - this.lastStopLossAt < 60_000) {
const remaining = Math.max(0, 60_000 - (now - this.lastStopLossAt));
this.tradeLog.push("info", `止损后冷却中 ${(remaining / 1000).toFixed(0)}s,忽略入场信号`);
this.tradeLog.push("info", t("log.trend.stopCooldown", { seconds: (remaining / 1000).toFixed(0) }));
return;
}
// 同一分钟只允许一次入场
if (this.lastEntryMinute != null && this.lastEntryMinute === currentMinute) {
this.tradeLog.push("info", "本分钟已入场,忽略新的 SMA 入场信号");
this.tradeLog.push("info", t("log.trend.alreadyEntered"));
return;
}
if (
@@ -431,7 +441,10 @@ export class TrendEngine {
if (now - this.lastBollingerBlockLogged > 15_000) {
this.tradeLog.push(
"info",
`布林带宽度不足:${Number(currentBandwidth).toFixed(4)} < ${this.config.minBollingerBandwidth},忽略入场信号`
t("log.trend.bandwidthBlocked", {
bandwidth: Number(currentBandwidth).toFixed(4),
minBandwidth: this.config.minBollingerBandwidth,
})
);
this.lastBollingerBlockLogged = now;
}
@@ -450,22 +463,22 @@ export class TrendEngine {
this.openOrders = [];
} catch (err) {
if (isUnknownOrderError(err)) {
this.tradeLog.push("order", "撤单时部分订单已不存在,忽略");
this.tradeLog.push("order", t("log.trend.cancelMissing"));
this.cancelAllRequested = true;
// 与成功撤单路径保持一致,立即清空本地缓存,等待订单流推送重建
this.pendingCancelOrders.clear();
this.openOrders = [];
} else {
this.tradeLog.push("error", `撤销挂单失败: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.cancelFail", { error: String(err) }));
this.cancelAllRequested = false;
}
}
}
if (this.lastPrice > currentSma && currentPrice < currentSma) {
await this.submitMarketOrder("SELL", currentPrice, "下穿SMA30,市价开空");
await this.submitMarketOrder("SELL", currentPrice, t("log.trend.crossDown"));
this.lastEntryMinute = currentMinute;
} else if (this.lastPrice < currentSma && currentPrice > currentSma) {
await this.submitMarketOrder("BUY", currentPrice, "上穿SMA30,市价开多");
await this.submitMarketOrder("BUY", currentPrice, t("log.trend.crossUp"));
this.lastEntryMinute = currentMinute;
}
}
@@ -493,7 +506,7 @@ export class TrendEngine {
this.tradeLog.push("open", `${reason}: ${side} @ ${price}`);
this.lastOpenPlan = { side, price };
} catch (err) {
this.tradeLog.push("error", `市价下单失败: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.marketOrderFail", { error: String(err) }));
}
}
@@ -504,7 +517,7 @@ export class TrendEngine {
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.trend.entryPricePending"));
this.entryPricePendingLogged = true;
}
return { closed: false, pnl: position.unrealizedProfit };
@@ -697,7 +710,7 @@ export class TrendEngine {
orderIdSet.forEach((id) => this.pendingCancelOrders.add(id));
} catch (err) {
if (isUnknownOrderError(err)) {
this.tradeLog.push("order", "止损前撤单发现订单已不存在");
this.tradeLog.push("order", t("log.trend.stopPreCancelMissing"));
// 清理本地缓存,避免重复对同一订单执行撤单
for (const id of orderIdSet) {
this.pendingCancelOrders.delete(id);
@@ -720,7 +733,12 @@ export class TrendEngine {
if (pctDiff > limitPct) {
this.tradeLog.push(
"info",
`市价平仓保护触发:closePx=${Number(closeSidePrice).toFixed(2)} mark=${mark.toFixed(2)} 偏离 ${(pctDiff * 100).toFixed(2)}% > ${(limitPct * 100).toFixed(2)}%`
t("log.trend.marketCloseGuard", {
closePx: Number(closeSidePrice).toFixed(2),
mark: mark.toFixed(2),
pctDiff: (pctDiff * 100).toFixed(2),
limitPct: (limitPct * 100).toFixed(2),
})
);
return { closed: false, pnl };
}
@@ -747,14 +765,14 @@ export class TrendEngine {
{ qtyStep: this.config.qtyStep }
);
result.closed = true;
this.tradeLog.push("close", `止损平仓: ${direction === "long" ? "SELL" : "BUY"}`);
this.tradeLog.push("close", t("log.trend.stopClose", { side: direction === "long" ? "SELL" : "BUY" }));
// 记录止损时间以便短期内抑制再次入场
this.lastStopLossAt = Date.now();
} catch (err) {
if (isUnknownOrderError(err)) {
this.tradeLog.push("order", "止损平仓时目标订单已不存在");
this.tradeLog.push("order", t("log.trend.targetStopMissing"));
} else {
this.tradeLog.push("error", `止损平仓失败: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.stopCloseFail", { error: String(err) }));
}
return result;
}
@@ -808,7 +826,7 @@ export class TrendEngine {
);
this.lastStopAttempt = { side, price: stopPrice, at: Date.now() };
} catch (err) {
this.tradeLog.push("error", `挂止损单失败: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.placeStopFail", { error: String(err) }));
// 记录尝试以避免在错误被抛回时立即再次重复尝试
this.lastStopAttempt = { side, price: stopPrice, at: Date.now() };
}
@@ -833,11 +851,11 @@ export class TrendEngine {
await this.exchange.cancelOrder({ symbol: this.config.symbol, orderId: currentOrder.orderId });
} catch (err) {
if (isUnknownOrderError(err)) {
this.tradeLog.push("order", "原止损单已不存在,跳过撤销");
this.tradeLog.push("order", t("log.trend.stopMissingSkip"));
// 订单已不存在,移除本地记录,防止后续重复匹配
this.openOrders = this.openOrders.filter((o) => o.orderId !== currentOrder.orderId);
} else {
this.tradeLog.push("error", `取消原止损单失败: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.cancelStopFail", { error: String(err) }));
}
}
// 仅在成功创建新止损单后记录“移动止损”日志
@@ -867,10 +885,18 @@ export class TrendEngine {
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
if (order) {
this.tradeLog.push("stop", `移动止损到 ${formatPriceToString(nextStopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))}`);
this.tradeLog.push(
"stop",
t("log.trend.moveStop", {
price: formatPriceToString(
nextStopPrice,
Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)))
),
})
);
}
} catch (err) {
this.tradeLog.push("error", `移动止损失败: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.moveStopFail", { error: String(err) }));
// 回滚策略:尝试用原价恢复止损,以避免出现短时间内无止损保护
try {
const position = getPosition(this.accountSnapshot, this.config.symbol);
@@ -902,11 +928,19 @@ export class TrendEngine {
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
if (restored) {
this.tradeLog.push("order", `恢复原止损 @ ${formatPriceToString(existingStopPrice, Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick))))}`);
this.tradeLog.push(
"order",
t("log.trend.restoreStop", {
price: formatPriceToString(
existingStopPrice,
Math.max(0, Math.floor(Math.log10(1 / this.config.priceTick)))
),
})
);
}
}
} catch (recoverErr) {
this.tradeLog.push("error", `恢复原止损失败: ${String(recoverErr)}`);
this.tradeLog.push("error", t("log.trend.restoreStopFail", { error: String(recoverErr) }));
}
}
}
@@ -939,7 +973,7 @@ export class TrendEngine {
{ priceTick: this.config.priceTick, qtyStep: this.config.qtyStep }
);
} catch (err) {
this.tradeLog.push("error", `挂动态止盈失败: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.trailingFail", { error: String(err) }));
}
}
@@ -968,12 +1002,12 @@ export class TrendEngine {
if (updated) {
this.tradeLog.push(
"info",
`已同步交易精度: priceTick=${precision.priceTick} qtyStep=${precision.qtyStep}`
t("log.trend.precisionSynced", { priceTick: precision.priceTick, qtyStep: precision.qtyStep })
);
}
})
.catch((error) => {
this.tradeLog.push("error", `同步精度失败: ${extractMessage(error)}`);
this.tradeLog.push("error", t("log.trend.precisionFailed", { error: extractMessage(error) }));
this.precisionSync = null;
setTimeout(() => this.syncPrecision(), 2000);
});
@@ -983,10 +1017,10 @@ export class TrendEngine {
try {
const snapshot = this.buildSnapshot();
this.events.emit("update", snapshot, (error) => {
this.tradeLog.push("error", `更新回调处理异常: ${String(error)}`);
this.tradeLog.push("error", t("log.trend.updateHandlerError", { error: String(error) }));
});
} catch (err) {
this.tradeLog.push("error", `快照或更新分发异常: ${String(err)}`);
this.tradeLog.push("error", t("log.trend.snapshotDispatchError", { error: String(err) }));
}
}