mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
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:
@@ -1,3 +1,6 @@
|
||||
# UI language (zh | en)
|
||||
LANG=zh
|
||||
|
||||
# Exchange selection
|
||||
EXCHANGE=aster # Pick aster (default) or grvt/lighter/backpack/paradex
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { resolveExchangeId, type SupportedExchangeId } from "./exchanges/create-adapter";
|
||||
import { language, type Language } from "./i18n";
|
||||
|
||||
export interface TradingConfig {
|
||||
symbol: string;
|
||||
@@ -211,3 +212,5 @@ export function isBasisStrategyEnabled(): boolean {
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes";
|
||||
}
|
||||
|
||||
export const uiLanguage: Language = language;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LogHandler } from "../order-coordinator";
|
||||
import { t } from "../../i18n";
|
||||
|
||||
type RateLimitState = "normal" | "degraded" | "paused";
|
||||
|
||||
@@ -32,13 +33,13 @@ export class RateLimitController {
|
||||
private suppressEntries(source?: string): void {
|
||||
if (this.entriesSuppressed) return;
|
||||
this.entriesSuppressed = true;
|
||||
this.log("info", `${source ? `${source} ` : ""}限频期间暂停新开仓`);
|
||||
this.log("info", t("rate.limit.suppress", { source: source ? `${source} ` : "" }));
|
||||
}
|
||||
|
||||
private allowEntries(): void {
|
||||
if (!this.entriesSuppressed) return;
|
||||
this.entriesSuppressed = false;
|
||||
this.log("info", "限频恢复,允许重新开仓");
|
||||
this.log("info", t("rate.limit.resumeEntries"));
|
||||
}
|
||||
|
||||
beforeCycle(): RateLimitDecision {
|
||||
@@ -47,7 +48,7 @@ export class RateLimitController {
|
||||
if (this.pausedUntil != null && now >= this.pausedUntil) {
|
||||
this.state = "degraded";
|
||||
this.pausedUntil = null;
|
||||
this.log("info", "限频暂停结束,继续以降频模式运行");
|
||||
this.log("info", t("rate.limit.pausedEnd"));
|
||||
} else {
|
||||
this.lastCycleAt = now;
|
||||
return "paused";
|
||||
@@ -69,7 +70,7 @@ export class RateLimitController {
|
||||
this.state = "degraded";
|
||||
this.log(
|
||||
"warn",
|
||||
`${source ? `${source} ` : ""}触发 429,降频至 ${(this.currentInterval() / 1000).toFixed(2)}s`
|
||||
t("rate.limit.hit", { source: source ? `${source} ` : "", interval: (this.currentInterval() / 1000).toFixed(2) })
|
||||
);
|
||||
this.lastCycleAt = now;
|
||||
this.suppressEntries(source);
|
||||
@@ -80,7 +81,7 @@ export class RateLimitController {
|
||||
this.pausedUntil = now + this.pauseMs;
|
||||
this.log(
|
||||
"warn",
|
||||
`${source ? `${source} ` : ""}连续 429,暂停请求 ${(this.pauseMs / 1000).toFixed(0)}s`
|
||||
t("rate.limit.consecutive", { source: source ? `${source} ` : "", seconds: (this.pauseMs / 1000).toFixed(0) })
|
||||
);
|
||||
this.suppressEntries(source);
|
||||
return;
|
||||
@@ -88,7 +89,7 @@ export class RateLimitController {
|
||||
this.pausedUntil = now + this.pauseMs;
|
||||
this.log(
|
||||
"warn",
|
||||
`${source ? `${source} ` : ""}限频仍在持续,延长暂停 ${(this.pauseMs / 1000).toFixed(0)}s`
|
||||
t("rate.limit.still", { source: source ? `${source} ` : "", seconds: (this.pauseMs / 1000).toFixed(0) })
|
||||
);
|
||||
this.suppressEntries(source);
|
||||
}
|
||||
@@ -99,7 +100,7 @@ export class RateLimitController {
|
||||
const now = Date.now();
|
||||
if (now - this.lastRateLimitAt >= this.recoveryMs) {
|
||||
this.state = "normal";
|
||||
this.log("info", "限频恢复,重置为正常请求频率");
|
||||
this.log("info", t("rate.limit.reset"));
|
||||
this.allowEntries();
|
||||
this.lastRateLimitAt = 0;
|
||||
}
|
||||
@@ -119,4 +120,3 @@ export class RateLimitController {
|
||||
return this.baseInterval;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AsterCredentials } from "./aster-adapter";
|
||||
import type { LighterCredentials } from "./lighter/adapter";
|
||||
import type { BackpackCredentials } from "./backpack/adapter";
|
||||
import type { ParadexCredentials } from "./paradex/adapter";
|
||||
import { t } from "../i18n";
|
||||
|
||||
interface BuildAdapterOptions {
|
||||
symbol: string;
|
||||
@@ -41,7 +42,7 @@ function resolveAsterCredentials(): AsterCredentials {
|
||||
const apiKey = process.env.ASTER_API_KEY;
|
||||
const apiSecret = process.env.ASTER_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
throw new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量");
|
||||
throw new Error(t("env.missingAster"));
|
||||
}
|
||||
return { apiKey, apiSecret };
|
||||
}
|
||||
@@ -50,11 +51,11 @@ function resolveLighterCredentials(symbol: string): LighterCredentials {
|
||||
const accountIndexRaw = process.env.LIGHTER_ACCOUNT_INDEX;
|
||||
const apiPrivateKey = process.env.LIGHTER_API_PRIVATE_KEY;
|
||||
if (!accountIndexRaw || !apiPrivateKey) {
|
||||
throw new Error("缺少 LIGHTER_ACCOUNT_INDEX 或 LIGHTER_API_PRIVATE_KEY 环境变量");
|
||||
throw new Error(t("env.missingLighter"));
|
||||
}
|
||||
const accountIndex = Number(accountIndexRaw);
|
||||
if (!Number.isInteger(accountIndex)) {
|
||||
throw new Error("LIGHTER_ACCOUNT_INDEX 必须是整数");
|
||||
throw new Error(t("env.lighterIndexInteger"));
|
||||
}
|
||||
const credentials: LighterCredentials = {
|
||||
displaySymbol: symbol,
|
||||
@@ -76,7 +77,7 @@ function resolveBackpackCredentials(symbol: string): BackpackCredentials {
|
||||
const apiKey = process.env.BACKPACK_API_KEY;
|
||||
const apiSecret = process.env.BACKPACK_API_SECRET;
|
||||
if (!apiKey || !apiSecret) {
|
||||
throw new Error("缺少 BACKPACK_API_KEY 或 BACKPACK_API_SECRET 环境变量");
|
||||
throw new Error(t("env.missingBackpack"));
|
||||
}
|
||||
const credentials: BackpackCredentials = {
|
||||
apiKey,
|
||||
@@ -94,13 +95,13 @@ function resolveParadexCredentials(): ParadexCredentials {
|
||||
const walletAddress = process.env.PARADEX_WALLET_ADDRESS;
|
||||
|
||||
if (!privateKey || !walletAddress) {
|
||||
throw new Error("Paradex 需要配置 PARADEX_PRIVATE_KEY 与 PARADEX_WALLET_ADDRESS");
|
||||
throw new Error(t("env.missingParadex"));
|
||||
}
|
||||
if (!isHex32(privateKey)) {
|
||||
throw new Error("PARADEX_PRIVATE_KEY 必须是 0x 开头的 32 字节十六进制字符串");
|
||||
throw new Error(t("env.invalidParadexPrivateKey"));
|
||||
}
|
||||
if (!isHexAddress(walletAddress)) {
|
||||
throw new Error("PARADEX_WALLET_ADDRESS 必须是有效的 0x 开头 40 字节十六进制地址");
|
||||
throw new Error(t("env.invalidParadexAddress"));
|
||||
}
|
||||
|
||||
const credentials: ParadexCredentials = {
|
||||
|
||||
@@ -0,0 +1,629 @@
|
||||
export type Language = "zh" | "en";
|
||||
|
||||
const normalizeLanguage = (value: string | undefined): Language => {
|
||||
if (!value) return "zh";
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "en" || normalized.startsWith("en-") || normalized.startsWith("en_")) return "en";
|
||||
if (normalized === "zh" || normalized.startsWith("zh")) return "zh";
|
||||
if (normalized === "english") return "en";
|
||||
if (normalized === "chinese") return "zh";
|
||||
return "zh";
|
||||
};
|
||||
|
||||
export const language: Language = normalizeLanguage(process.env.LANG);
|
||||
|
||||
type TranslationValue = string | ((params: Record<string, unknown>, lang: Language) => string);
|
||||
|
||||
type TranslationEntry = {
|
||||
zh: TranslationValue;
|
||||
en: TranslationValue;
|
||||
};
|
||||
|
||||
const translations: Record<string, TranslationEntry> = {
|
||||
"app.strategy.trend.label": { zh: "趋势跟随策略 (SMA30)", en: "Trend Following (SMA30)" },
|
||||
"app.strategy.trend.desc": {
|
||||
zh: "监控均线信号,自动进出场并维护止损/止盈",
|
||||
en: "Monitors SMA signals, automates entries/exits, maintains stops.",
|
||||
},
|
||||
"app.strategy.guardian.label": { zh: "Guardian 防守策略", en: "Guardian Protection" },
|
||||
"app.strategy.guardian.desc": {
|
||||
zh: "不主动开仓,只为现有仓位补挂/移动止损,防止裸奔",
|
||||
en: "Does not open positions; only manages stops for existing positions.",
|
||||
},
|
||||
"app.strategy.maker.label": { zh: "做市刷单策略", en: "Maker Market Making" },
|
||||
"app.strategy.maker.desc": {
|
||||
zh: "双边挂单提供流动性,自动追价与风控止损",
|
||||
en: "Places two-sided quotes, auto-chases and risk-manages stops.",
|
||||
},
|
||||
"app.strategy.grid.label": { zh: "基础网格策略", en: "Grid Strategy" },
|
||||
"app.strategy.grid.desc": {
|
||||
zh: "在上下边界之间布设等比网格,自动加仓与减仓",
|
||||
en: "Places geometric grids between bounds, auto scale-in/out.",
|
||||
},
|
||||
"app.strategy.offset.label": { zh: "偏移做市策略", en: "Offset Maker Strategy" },
|
||||
"app.strategy.offset.desc": {
|
||||
zh: "根据盘口深度自动偏移挂单并在极端不平衡时撤退",
|
||||
en: "Offsets quotes by depth, retreats on extreme imbalance.",
|
||||
},
|
||||
"app.strategy.basis.label": { zh: "期现套利策略", en: "Basis Arbitrage" },
|
||||
"app.strategy.basis.desc": {
|
||||
zh: "监控期货与现货盘口差价,辅助发现套利机会",
|
||||
en: "Monitors futures/spot spread to surface arbitrage windows.",
|
||||
},
|
||||
"app.integrity.warning": {
|
||||
zh: "警告: 版权校验失败,当前版本可能被篡改。",
|
||||
en: "Warning: Copyright integrity check failed; build may be tampered.",
|
||||
},
|
||||
"app.pickStrategy": { zh: "请选择要运行的策略", en: "Select a strategy to run" },
|
||||
"app.pickHint": {
|
||||
zh: "使用 ↑/↓ 选择,回车开始,Ctrl+C 退出。",
|
||||
en: "Use ↑/↓ to choose, Enter to start, Ctrl+C to exit.",
|
||||
},
|
||||
"common.waiting": { zh: "等待", en: "Waiting" },
|
||||
"common.startFailed": { zh: "启动失败: {message}", en: "Failed to start: {message}" },
|
||||
"common.checkEnv": {
|
||||
zh: "请检查环境变量和网络连通性。",
|
||||
en: "Please check environment variables and network connectivity.",
|
||||
},
|
||||
"common.initializing": { zh: "正在初始化{target}…", en: "Initializing {target}..." },
|
||||
"common.statusWithBack": {
|
||||
zh: "状态: {status} | 按 Esc 返回策略选择",
|
||||
en: "Status: {status} | Press Esc to return to menu.",
|
||||
},
|
||||
"common.backHint": { zh: "按 Esc 返回策略选择", en: "Press Esc to return to menu." },
|
||||
"common.section.position": { zh: "持仓", en: "Position" },
|
||||
"common.section.performance": { zh: "绩效", en: "Performance" },
|
||||
"common.section.orders": { zh: "当前挂单", en: "Open Orders" },
|
||||
"common.section.recent": { zh: "最近事件", en: "Recent Events" },
|
||||
"common.section.recentTrades": { zh: "最近交易与事件", en: "Recent Trades & Events" },
|
||||
"common.noPosition": { zh: "当前无持仓", en: "No open position" },
|
||||
"common.noOrders": { zh: "暂无挂单", en: "No open orders" },
|
||||
"common.noLogs": { zh: "暂无日志", en: "No logs yet" },
|
||||
"common.direction.long": { zh: "多", en: "Long" },
|
||||
"common.direction.short": { zh: "空", en: "Short" },
|
||||
"common.enabled": { zh: "启用", en: "Enabled" },
|
||||
"common.disabled": { zh: "关闭", en: "Disabled" },
|
||||
"status.live": { zh: "实时运行", en: "Live" },
|
||||
"status.running": { zh: "运行中", en: "Running" },
|
||||
"status.paused": { zh: "暂停", en: "Paused" },
|
||||
"status.waitingData": { zh: "等待市场数据", en: "Waiting for market data" },
|
||||
"trend.name": { zh: "趋势策略", en: "trend strategy" },
|
||||
"trend.title": { zh: "趋势策略仪表盘", en: "Trend Strategy Dashboard" },
|
||||
"trend.headerLine": {
|
||||
zh: "交易所: {exchange} | 交易对: {symbol} | 最近价格: {lastPrice} | SMA30: {sma} | 趋势: {trend}",
|
||||
en: "Exchange: {exchange} | Symbol: {symbol} | Last: {lastPrice} | SMA30: {sma} | Trend: {trend}",
|
||||
},
|
||||
"trend.statusLine": {
|
||||
zh: "状态: {status} | 按 Esc 返回策略选择",
|
||||
en: "Status: {status} | Press Esc to return to menu.",
|
||||
},
|
||||
"trend.positionLine": {
|
||||
zh: "方向: {direction} | 数量: {qty} | 开仓价: {entry}",
|
||||
en: "Direction: {direction} | Size: {qty} | Entry: {entry}",
|
||||
},
|
||||
"trend.pnlLine": {
|
||||
zh: "浮动盈亏: {pnl} USDT | 账户未实现盈亏: {unrealized} USDT",
|
||||
en: "Floating PnL: {pnl} USDT | Account Unrealized: {unrealized} USDT",
|
||||
},
|
||||
"trend.performanceLine": {
|
||||
zh: "累计交易次数: {trades} | 累计收益: {profit} USDT",
|
||||
en: "Total trades: {trades} | Total profit: {profit} USDT",
|
||||
},
|
||||
"trend.volumeLine": { zh: "累计成交量: {volume} USDT", en: "Total volume: {volume} USDT" },
|
||||
"trend.lastSignal": {
|
||||
zh: "最近开仓信号: {side} @ {price}",
|
||||
en: "Last entry signal: {side} @ {price}",
|
||||
},
|
||||
"trend.readyMessage": { zh: "正在等待交易所推送数据…", en: "Waiting for exchange feeds..." },
|
||||
"trend.label.long": { zh: "做多", en: "Long" },
|
||||
"trend.label.short": { zh: "做空", en: "Short" },
|
||||
"trend.label.none": { zh: "无信号", en: "No signal" },
|
||||
"guardian.name": { zh: "Guardian 策略", en: "Guardian strategy" },
|
||||
"guardian.title": { zh: "Guardian 策略仪表盘", en: "Guardian Strategy Dashboard" },
|
||||
"guardian.readyMessage": { zh: "正在等待行情/账户推送…", en: "Waiting for market/account feeds..." },
|
||||
"guardian.startFailed": {
|
||||
zh: "Guardian 策略启动失败: {message}",
|
||||
en: "Guardian strategy failed to start: {message}",
|
||||
},
|
||||
"guardian.initializing": { zh: "正在初始化 Guardian 策略…", en: "Initializing Guardian strategy..." },
|
||||
"guardian.headerLine": {
|
||||
zh: "交易所: {exchange} | 交易对: {symbol} | 最近价格: {lastPrice} | 状态: {status}",
|
||||
en: "Exchange: {exchange} | Symbol: {symbol} | Last: {lastPrice} | Status: {status}",
|
||||
},
|
||||
"guardian.hint": {
|
||||
zh: "策略只会维护止损/止盈,不会主动开仓。按 Esc 返回菜单。",
|
||||
en: "Maintains stops/take-profit only; does not open positions. Press Esc to return.",
|
||||
},
|
||||
"guardian.positionTitle": { zh: "当前仓位与风控", en: "Position & Protection" },
|
||||
"guardian.positionLine": {
|
||||
zh: "方向: {direction} | 数量: {qty} | 开仓价: {entry} | 浮动盈亏: {pnl} USDT",
|
||||
en: "Direction: {direction} | Size: {qty} | Entry: {entry} | Floating PnL: {pnl} USDT",
|
||||
},
|
||||
"guardian.stopLine": {
|
||||
zh: "目标止损价: {targetStop} | 当前止损单: {stopOrder} | 动态止盈触发: {trailingTrigger} | 动态止盈单: {trailingOrder}",
|
||||
en: "Target stop: {targetStop} | Active stop: {stopOrder} | Trailing trigger: {trailingTrigger} | Trailing order: {trailingOrder}",
|
||||
},
|
||||
"guardian.status.protecting": { zh: "已挂止损", en: "Stop placed" },
|
||||
"guardian.status.pending": { zh: "缺少止损,正在同步", en: "Missing stop, syncing" },
|
||||
"guardian.status.listening": { zh: "监听中", en: "Listening" },
|
||||
"guardian.stateLabel": { zh: "Guardian 状态: {state}", en: "Guardian status: {state}" },
|
||||
"guardian.noPosition": {
|
||||
zh: "当前无持仓,Guardian 正在监听新的仓位变化。",
|
||||
en: "No open position; Guardian is listening for new positions.",
|
||||
},
|
||||
"guardian.noProtectiveOrders": { zh: "暂无保护类挂单", en: "No protective orders" },
|
||||
"maker.name": { zh: "做市策略", en: "market-making strategy" },
|
||||
"maker.title": { zh: "做市策略仪表盘", en: "Maker Strategy Dashboard" },
|
||||
"maker.initializing": { zh: "正在初始化做市策略…", en: "Initializing maker strategy..." },
|
||||
"maker.headerLine": {
|
||||
zh: "交易所: {exchange} | 交易对: {symbol} | 买一价: {bid} | 卖一价: {ask} | 点差: {spread}",
|
||||
en: "Exchange: {exchange} | Symbol: {symbol} | Best Bid: {bid} | Best Ask: {ask} | Spread: {spread}",
|
||||
},
|
||||
"maker.dataStatus": { zh: "数据状态:", en: "Data status:" },
|
||||
"maker.feed.account": { zh: "账户", en: "Account" },
|
||||
"maker.feed.orders": { zh: "订单", en: "Orders" },
|
||||
"maker.feed.depth": { zh: "深度", en: "Depth" },
|
||||
"maker.feed.ticker": { zh: "Ticker", en: "Ticker" },
|
||||
"maker.positionLine": {
|
||||
zh: "方向: {direction} | 数量: {qty} | 开仓价: {entry}",
|
||||
en: "Direction: {direction} | Size: {qty} | Entry: {entry}",
|
||||
},
|
||||
"maker.pnlLine": {
|
||||
zh: "浮动盈亏: {pnl} USDT | 账户未实现盈亏: {accountPnl} USDT",
|
||||
en: "Floating PnL: {pnl} USDT | Account Unrealized: {accountPnl} USDT",
|
||||
},
|
||||
"maker.targetOrders": { zh: "目标挂单", en: "Target Orders" },
|
||||
"maker.noTargetOrders": { zh: "暂无目标挂单", en: "No target orders" },
|
||||
"offset.name": { zh: "偏移做市策略", en: "offset maker strategy" },
|
||||
"offset.title": { zh: "偏移做市策略仪表盘", en: "Offset Maker Strategy Dashboard" },
|
||||
"offset.initializing": { zh: "正在初始化偏移做市策略…", en: "Initializing offset maker strategy..." },
|
||||
"offset.headerLine": {
|
||||
zh: "交易所: {exchange} | 交易对: {symbol} | 买一价: {bid} | 卖一价: {ask} | 点差: {spread}",
|
||||
en: "Exchange: {exchange} | Symbol: {symbol} | Best Bid: {bid} | Best Ask: {ask} | Spread: {spread}",
|
||||
},
|
||||
"offset.depthLine": {
|
||||
zh: "买10档累计: {buy} | 卖10档累计: {sell} | 状态: {status}",
|
||||
en: "Top 10 bid sum: {buy} | Top 10 ask sum: {sell} | Status: {status}",
|
||||
},
|
||||
"offset.strategyStatus": {
|
||||
zh: "当前挂单策略: BUY {buyStatus} | SELL {sellStatus} | 按 Esc 返回策略选择",
|
||||
en: "Quote status: BUY {buyStatus} | SELL {sellStatus} | Press Esc to return to menu",
|
||||
},
|
||||
"offset.imbalance.balanced": { zh: "均衡", en: "Balanced" },
|
||||
"offset.imbalance.buy": { zh: "买盘占优", en: "Bid dominant" },
|
||||
"offset.imbalance.sell": { zh: "卖盘占优", en: "Ask dominant" },
|
||||
"grid.name": { zh: "网格策略", en: "grid strategy" },
|
||||
"grid.title": { zh: "网格策略仪表盘", en: "Grid Strategy Dashboard" },
|
||||
"grid.initializing": { zh: "正在初始化网格策略…", en: "Initializing grid strategy..." },
|
||||
"grid.headerLine": {
|
||||
zh: "交易所: {exchange} | 交易对: {symbol} | 状态: {status} | 方向: {direction}",
|
||||
en: "Exchange: {exchange} | Symbol: {symbol} | Status: {status} | Direction: {direction}",
|
||||
},
|
||||
"grid.priceLine": {
|
||||
zh: "实时价格: {lastPrice} | 下界: {lower} | 上界: {upper} | 网格数量: {count}",
|
||||
en: "Last price: {lastPrice} | Lower: {lower} | Upper: {upper} | Grid count: {count}",
|
||||
},
|
||||
"grid.dataStatus": { zh: "数据状态:", en: "Data status:" },
|
||||
"grid.stopReason": { zh: "暂停原因: {reason}", en: "Pause reason: {reason}" },
|
||||
"grid.configTitle": { zh: "网格配置", en: "Grid Config" },
|
||||
"grid.configSize": {
|
||||
zh: "单笔数量: {orderSize} | 最大仓位: {maxPosition}",
|
||||
en: "Order size: {orderSize} | Max position: {maxPosition}",
|
||||
},
|
||||
"grid.configRisk": {
|
||||
zh: "止损阈值: {stopLoss}% | 重启阈值: {restart}% | 自动重启: {autoRestart}",
|
||||
en: "Stop loss: {stopLoss}% | Restart trigger: {restart}% | Auto restart: {autoRestart}",
|
||||
},
|
||||
"grid.refreshInterval": { zh: "刷新间隔: {interval} ms", en: "Refresh interval: {interval} ms" },
|
||||
"grid.positionLine": {
|
||||
zh: "当前持仓: {direction} | 数量: {qty} | 均价: {avgPrice}",
|
||||
en: "Position: {direction} | Size: {qty} | Avg price: {avgPrice}",
|
||||
},
|
||||
"grid.unrealizedLine": {
|
||||
zh: "未实现盈亏: {pnl} | 标记价: {mark}",
|
||||
en: "Unrealized PnL: {pnl} | Mark: {mark}",
|
||||
},
|
||||
"grid.linesTitle": { zh: "网格线", en: "Grid Lines" },
|
||||
"grid.noLines": { zh: "暂无网格线", en: "No grid lines" },
|
||||
"grid.direction.both": { zh: "双向", en: "Both" },
|
||||
"grid.direction.long": { zh: "多", en: "Long" },
|
||||
"grid.direction.short": { zh: "空", en: "Short" },
|
||||
"basis.onlyAster": {
|
||||
zh: "期现套利策略目前仅支持 Aster 交易所。请设置 EXCHANGE=aster 后重试。",
|
||||
en: "Basis arbitrage currently supports only Aster. Set EXCHANGE=aster and retry.",
|
||||
},
|
||||
"basis.startFailed": {
|
||||
zh: "无法启动期现套利策略: {message}",
|
||||
en: "Unable to start basis arbitrage: {message}",
|
||||
},
|
||||
"basis.initializing": { zh: "正在初始化期现套利监控…", en: "Initializing basis arbitrage monitor..." },
|
||||
"basis.title": { zh: "期现套利仪表盘", en: "Basis Arbitrage Dashboard" },
|
||||
"basis.headerLine": {
|
||||
zh: "交易所: {exchange} | 期货合约: {futures} | 现货交易对: {spot}",
|
||||
en: "Exchange: {exchange} | Futures: {futures} | Spot: {spot}",
|
||||
},
|
||||
"basis.statusLine": {
|
||||
zh: "按 Esc 返回策略选择 | 数据状态: 期货({futuresStatus}) 现货({spotStatus}) 资金费率({fundingStatus})",
|
||||
en: "Press Esc to return | Feeds: Futures({futuresStatus}) Spot({spotStatus}) Funding({fundingStatus})",
|
||||
},
|
||||
"basis.lastUpdated": { zh: "最近更新时间: {time}", en: "Last updated: {time}" },
|
||||
"basis.section.futures": { zh: "期货盘口", en: "Futures Book" },
|
||||
"basis.section.spot": { zh: "现货盘口", en: "Spot Book" },
|
||||
"basis.bookLine": { zh: "买一: {bid} | 卖一: {ask}", en: "Best bid: {bid} | Best ask: {ask}" },
|
||||
"basis.updatedAt": { zh: "更新时间: {time}", en: "Updated: {time}" },
|
||||
"basis.section.funding": { zh: "资金费率", en: "Funding" },
|
||||
"basis.fundingRate": { zh: "当前资金费率: {rate}", en: "Current funding rate: {rate}" },
|
||||
"basis.fundingTimes": {
|
||||
zh: "资金费率更新时间: {updated} | 下次结算时间: {next}",
|
||||
en: "Funding updated: {updated} | Next settlement: {next}",
|
||||
},
|
||||
"basis.fundingIncome": {
|
||||
zh: "单次资金费率收益(估): {per} | 日收益(估): {perDay}",
|
||||
en: "Est. income per funding: {per} | Est. daily income: {perDay}",
|
||||
},
|
||||
"basis.takerFees": {
|
||||
zh: "双边吃单手续费(估): {fees} | 回本所需资金费率次数: {count}",
|
||||
en: "Est. taker fees (round trip): {fees} | Funding counts to breakeven: {count}",
|
||||
},
|
||||
"basis.spotBalanceTitle": { zh: "现货账户余额(非0)", en: "Spot balances (non-zero)" },
|
||||
"basis.futuresBalanceTitle": { zh: "合约账户余额(非0)", en: "Futures balances (non-zero)" },
|
||||
"basis.balanceLine": { zh: "{asset}: 可用 {free} | 冻结 {locked}", en: "{asset}: Free {free} | Locked {locked}" },
|
||||
"basis.futuresBalanceLine": {
|
||||
zh: "{asset}: 钱包 {wallet} | 可用 {available}",
|
||||
en: "{asset}: Wallet {wallet} | Available {available}",
|
||||
},
|
||||
"basis.none": { zh: "无", en: "None" },
|
||||
"basis.spreadTitle": { zh: "套利差价(卖期货 / 买现货)", en: "Arb spread (sell futures / buy spot)" },
|
||||
"basis.spreadLine": { zh: "毛价差: {spread} USDT | {bps} bp", en: "Gross spread: {spread} USDT | {bps} bp" },
|
||||
"basis.netSpreadLine": {
|
||||
zh: "扣除 taker 手续费 ({feePct}% × 双边): {net} USDT | {netBps} bp",
|
||||
en: "Net after taker fee ({feePct}% x round trip): {net} USDT | {netBps} bp",
|
||||
},
|
||||
"rate.limit.suppress": {
|
||||
zh: "{source}限频期间暂停新开仓",
|
||||
en: "{source}Rate limit active, pausing new entries",
|
||||
},
|
||||
"rate.limit.resumeEntries": { zh: "限频恢复,允许重新开仓", en: "Rate limit cleared, resuming entries" },
|
||||
"rate.limit.pausedEnd": { zh: "限频暂停结束,继续以降频模式运行", en: "Pause ended; running in degraded mode" },
|
||||
"rate.limit.hit": {
|
||||
zh: "{source}触发 429,降频至 {interval}s",
|
||||
en: "{source}429 detected, slowing to {interval}s",
|
||||
},
|
||||
"rate.limit.consecutive": {
|
||||
zh: "{source}连续 429,暂停请求 {seconds}s",
|
||||
en: "{source}Consecutive 429s, pausing requests for {seconds}s",
|
||||
},
|
||||
"rate.limit.still": {
|
||||
zh: "{source}限频仍在持续,延长暂停 {seconds}s",
|
||||
en: "{source}Rate limit persists, extending pause {seconds}s",
|
||||
},
|
||||
"rate.limit.reset": { zh: "限频恢复,重置为正常请求频率", en: "Rate limit cleared, reset to normal cadence" },
|
||||
"env.missingAster": {
|
||||
zh: "缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量",
|
||||
en: "Missing ASTER_API_KEY or ASTER_API_SECRET",
|
||||
},
|
||||
"env.missingLighter": {
|
||||
zh: "缺少 LIGHTER_ACCOUNT_INDEX 或 LIGHTER_API_PRIVATE_KEY 环境变量",
|
||||
en: "Missing LIGHTER_ACCOUNT_INDEX or LIGHTER_API_PRIVATE_KEY",
|
||||
},
|
||||
"env.lighterIndexInteger": {
|
||||
zh: "LIGHTER_ACCOUNT_INDEX 必须是整数",
|
||||
en: "LIGHTER_ACCOUNT_INDEX must be an integer",
|
||||
},
|
||||
"env.missingBackpack": {
|
||||
zh: "缺少 BACKPACK_API_KEY 或 BACKPACK_API_SECRET 环境变量",
|
||||
en: "Missing BACKPACK_API_KEY or BACKPACK_API_SECRET",
|
||||
},
|
||||
"env.missingParadex": {
|
||||
zh: "Paradex 需要配置 PARADEX_PRIVATE_KEY 与 PARADEX_WALLET_ADDRESS",
|
||||
en: "Paradex requires PARADEX_PRIVATE_KEY and PARADEX_WALLET_ADDRESS",
|
||||
},
|
||||
"env.invalidParadexPrivateKey": {
|
||||
zh: "PARADEX_PRIVATE_KEY 必须是 0x 开头的 32 字节十六进制字符串",
|
||||
en: "PARADEX_PRIVATE_KEY must be a 0x-prefixed 32-byte hex string",
|
||||
},
|
||||
"env.invalidParadexAddress": {
|
||||
zh: "PARADEX_WALLET_ADDRESS 必须是有效的 0x 开头 40 字节十六进制地址",
|
||||
en: "PARADEX_WALLET_ADDRESS must be a valid 0x-prefixed 40-byte hex address",
|
||||
},
|
||||
"log.subscribe.accountFail": {
|
||||
zh: "订阅账户失败: {error}",
|
||||
en: "Failed to subscribe account: {error}",
|
||||
},
|
||||
"log.process.accountError": {
|
||||
zh: "账户推送处理异常: {error}",
|
||||
en: "Account stream processing error: {error}",
|
||||
},
|
||||
"log.subscribe.orderFail": {
|
||||
zh: "订阅订单失败: {error}",
|
||||
en: "Failed to subscribe orders: {error}",
|
||||
},
|
||||
"log.process.orderError": {
|
||||
zh: "订单推送处理异常: {error}",
|
||||
en: "Order stream processing error: {error}",
|
||||
},
|
||||
"log.subscribe.tickerFail": {
|
||||
zh: "订阅Ticker失败: {error}",
|
||||
en: "Failed to subscribe ticker: {error}",
|
||||
},
|
||||
"log.process.tickerError": {
|
||||
zh: "价格推送处理异常: {error}",
|
||||
en: "Price stream processing error: {error}",
|
||||
},
|
||||
"log.guardian.executeError": {
|
||||
zh: "Guardian 执行异常: {error}",
|
||||
en: "Guardian runtime error: {error}",
|
||||
},
|
||||
"log.guardian.entryPricePending": {
|
||||
zh: "持仓均价尚未同步,等待交易所账户快照更新后再补挂止损",
|
||||
en: "Entry price not synced yet; waiting for account snapshot before placing stop.",
|
||||
},
|
||||
"log.guardian.pricePending": {
|
||||
zh: "行情尚未就绪,等待最新价格以同步止损",
|
||||
en: "Market data not ready; waiting for latest price to sync stop.",
|
||||
},
|
||||
"log.guardian.placeStopFail": {
|
||||
zh: "挂止损单失败: {error}",
|
||||
en: "Failed to place stop order: {error}",
|
||||
},
|
||||
"log.guardian.stopMissingSkip": {
|
||||
zh: "原止损单已不存在,跳过撤销",
|
||||
en: "Existing stop missing, skipping cancel.",
|
||||
},
|
||||
"log.guardian.cancelStopFail": {
|
||||
zh: "取消原止损单失败: {error}",
|
||||
en: "Failed to cancel existing stop: {error}",
|
||||
},
|
||||
"log.guardian.moveStop": {
|
||||
zh: "移动止损到 {price}",
|
||||
en: "Moved stop to {price}",
|
||||
},
|
||||
"log.guardian.moveStopFail": {
|
||||
zh: "移动止损失败: {error}",
|
||||
en: "Failed to move stop: {error}",
|
||||
},
|
||||
"log.guardian.restoreStop": {
|
||||
zh: "恢复原止损 @ {price}",
|
||||
en: "Restored original stop @ {price}",
|
||||
},
|
||||
"log.guardian.restoreStopFail": {
|
||||
zh: "恢复原止损失败: {error}",
|
||||
en: "Failed to restore original stop: {error}",
|
||||
},
|
||||
"log.guardian.trailingFail": {
|
||||
zh: "挂动态止盈失败: {error}",
|
||||
en: "Failed to place trailing stop: {error}",
|
||||
},
|
||||
"log.guardian.cleanupOrders": {
|
||||
zh: "清理遗留保护单: {ids}",
|
||||
en: "Cleaning leftover protective orders: {ids}",
|
||||
},
|
||||
"log.guardian.protectiveMissing": {
|
||||
zh: "保护单已不存在,跳过清理",
|
||||
en: "Protective orders already gone; skipping cleanup.",
|
||||
},
|
||||
"log.guardian.cleanupFail": {
|
||||
zh: "清理保护单失败: {error}",
|
||||
en: "Failed to clean protective orders: {error}",
|
||||
},
|
||||
"log.guardian.dispatchError": {
|
||||
zh: "更新分发异常: {error}",
|
||||
en: "Update dispatch error: {error}",
|
||||
},
|
||||
"log.guardian.snapshotFail": {
|
||||
zh: "构建快照失败: {error}",
|
||||
en: "Failed to build snapshot: {error}",
|
||||
},
|
||||
"log.guardian.precisionSynced": {
|
||||
zh: "已同步交易精度: priceTick={priceTick} qtyStep={qtyStep}",
|
||||
en: "Synced precision: priceTick={priceTick} qtyStep={qtyStep}",
|
||||
},
|
||||
"log.guardian.precisionFailed": {
|
||||
zh: "同步精度失败: {error}",
|
||||
en: "Failed to sync precision: {error}",
|
||||
},
|
||||
"log.basis.subscribeFuturesDepthFail": {
|
||||
zh: "订阅期货深度失败: {error}",
|
||||
en: "Failed to subscribe futures depth: {error}",
|
||||
},
|
||||
"log.basis.processFuturesDepthError": {
|
||||
zh: "处理期货深度异常: {error}",
|
||||
en: "Error processing futures depth: {error}",
|
||||
},
|
||||
"log.basis.futuresReady": {
|
||||
zh: "期货深度已就绪 ({symbol})",
|
||||
en: "Futures depth ready ({symbol})",
|
||||
},
|
||||
"log.basis.spotDepthError": {
|
||||
zh: "获取现货盘口失败: {error}",
|
||||
en: "Failed to fetch spot orderbook: {error}",
|
||||
},
|
||||
"log.basis.fundingReady": {
|
||||
zh: "资金费率已就绪 ({symbol})",
|
||||
en: "Funding rate ready ({symbol})",
|
||||
},
|
||||
"log.basis.fundingError": {
|
||||
zh: "获取资金费率失败: {error}",
|
||||
en: "Failed to fetch funding rate: {error}",
|
||||
},
|
||||
"log.basis.spotBalanceError": {
|
||||
zh: "获取现货余额失败: {error}",
|
||||
en: "Failed to fetch spot balance: {error}",
|
||||
},
|
||||
"log.basis.futuresBalanceError": {
|
||||
zh: "获取合约余额失败: {error}",
|
||||
en: "Failed to fetch futures balance: {error}",
|
||||
},
|
||||
"log.basis.spotReady": { zh: "现货盘口已就绪 ({symbol})", en: "Spot orderbook ready ({symbol})" },
|
||||
"log.basis.pushError": { zh: "推送订阅失败: {error}", en: "Subscription push failed: {error}" },
|
||||
"log.basis.entryOpportunity": {
|
||||
zh: "入场机会: 扣费后价差 {bp} bp | 距下次资金费约 {minutes} 分钟",
|
||||
en: "Entry opportunity: net spread {bp} bp | ~{minutes} mins to next funding",
|
||||
},
|
||||
"log.basis.exitOpportunity": {
|
||||
zh: "出场机会: 资金费率为负 | 距收取约 {minutes} 分钟",
|
||||
en: "Exit opportunity: funding negative | ~{minutes} mins to settlement",
|
||||
},
|
||||
"log.account.snapshotSynced": { zh: "账户快照已同步", en: "Account snapshot synced" },
|
||||
"log.order.snapshotReturned": { zh: "订单快照已返回", en: "Order snapshot received" },
|
||||
"log.depth.ready": { zh: "获得最新深度行情", en: "Latest depth ready" },
|
||||
"log.ticker.ready": { zh: "Ticker 已就绪", en: "Ticker ready" },
|
||||
"log.subscribe.depthFail": { zh: "订阅深度失败: {error}", en: "Failed to subscribe depth: {error}" },
|
||||
"log.process.depthError": {
|
||||
zh: "深度推送处理异常: {error}",
|
||||
en: "Depth stream processing error: {error}",
|
||||
},
|
||||
"log.maker.loopError": { zh: "做市循环异常: {error}", en: "Maker loop error: {error}" },
|
||||
"log.maker.cleanOrdersStart": { zh: "启动时清理历史挂单", en: "Cleaning legacy orders at startup" },
|
||||
"log.maker.cleanOrdersMissing": {
|
||||
zh: "历史挂单已消失,跳过启动清理",
|
||||
en: "Legacy orders already gone, skipping startup cleanup",
|
||||
},
|
||||
"log.maker.cleanOrdersFail": { zh: "启动撤单失败: {error}", en: "Failed to cancel at startup: {error}" },
|
||||
"log.maker.cancelMismatched": {
|
||||
zh: "撤销不匹配订单 {side} @ {price} reduceOnly={reduceOnly}",
|
||||
en: "Cancel unmatched order {side} @ {price} reduceOnly={reduceOnly}",
|
||||
},
|
||||
"log.maker.cancelMissing": {
|
||||
zh: "撤销时发现订单已被成交/取消,忽略",
|
||||
en: "Order already filled/canceled, ignoring cancel",
|
||||
},
|
||||
"log.maker.cancelFail": { zh: "撤销订单失败: {error}", en: "Failed to cancel order: {error}" },
|
||||
"log.maker.placeFail": {
|
||||
zh: "挂单失败({side} {price}): {error}",
|
||||
en: "Failed to place order ({side} {price}): {error}",
|
||||
},
|
||||
"log.maker.avgPending": {
|
||||
zh: "做市持仓均价未同步,等待账户快照刷新后再执行止损判断",
|
||||
en: "Maker entry price not synced; waiting for account snapshot before stop check",
|
||||
},
|
||||
"log.maker.stopTriggered": {
|
||||
zh: "触发止损,方向={direction} 当前亏损={pnl} USDT",
|
||||
en: "Stop triggered direction={direction} current loss={pnl} USDT",
|
||||
},
|
||||
"log.maker.stopOrderMissing": { zh: "止损平仓时订单已不存在", en: "Stop close order missing" },
|
||||
"log.maker.stopCloseFail": { zh: "止损平仓失败: {error}", en: "Failed to close on stop: {error}" },
|
||||
"log.maker.orderMissing": { zh: "订单已不存在,撤销跳过", en: "Order already gone, skipping cancel" },
|
||||
"log.common.precisionSynced": {
|
||||
zh: "已同步交易精度: priceTick={priceTick} qtyStep={qtyStep}",
|
||||
en: "Synced precision: priceTick={priceTick} qtyStep={qtyStep}",
|
||||
},
|
||||
"log.common.precisionFailed": { zh: "同步精度失败: {error}", en: "Failed to sync precision: {error}" },
|
||||
"log.maker.updateHandlerError": { zh: "更新回调处理异常: {error}", en: "Update handler error: {error}" },
|
||||
"log.maker.snapshotDispatchError": {
|
||||
zh: "快照或更新分发异常: {error}",
|
||||
en: "Snapshot/update dispatch error: {error}",
|
||||
},
|
||||
"log.maker.waitAccount": { zh: "等待账户快照同步,尚未开始做市", en: "Waiting for account snapshot before quoting" },
|
||||
"log.maker.waitDepth": { zh: "等待深度行情推送,尚未开始做市", en: "Waiting for depth stream before quoting" },
|
||||
"log.maker.waitTicker": { zh: "等待Ticker推送,尚未开始做市", en: "Waiting for ticker stream before quoting" },
|
||||
"log.maker.waitOrders": {
|
||||
zh: "等待订单快照返回,尚未执行初始化撤单",
|
||||
en: "Waiting for order snapshot before startup cancels",
|
||||
},
|
||||
"log.maker.noTargets": { zh: "当前无目标挂单,等待下一次刷新", en: "No target orders; waiting for next refresh" },
|
||||
"log.maker.targetsSummary": { zh: "目标挂单: {summary}", en: "Target orders: {summary}" },
|
||||
"log.maker.balanceThrottle": {
|
||||
zh: "余额不足,暂停新挂单 {seconds}s: {detail}",
|
||||
en: "Insufficient balance, pausing new orders for {seconds}s: {detail}",
|
||||
},
|
||||
"log.maker.balanceResumed": {
|
||||
zh: "余额检测恢复,重新尝试挂单",
|
||||
en: "Balance check recovered, retrying orders",
|
||||
},
|
||||
"log.maker.rateLimit429": { zh: "MakerEngine 429: {error}", en: "MakerEngine 429: {error}" },
|
||||
"log.kline.subscribeFail": { zh: "订阅K线失败: {error}", en: "Failed to subscribe klines: {error}" },
|
||||
"log.kline.processError": { zh: "K线推送处理异常: {error}", en: "Kline stream processing error: {error}" },
|
||||
"log.trend.klineInsufficient": {
|
||||
zh: "K线不足 {count}/{min},最近收盘({recentCount}): {recent}",
|
||||
en: "Insufficient klines {count}/{min}, recent closes ({recentCount}): {recent}",
|
||||
},
|
||||
"log.trend.klineReady": {
|
||||
zh: "K线就绪 {count} 根,可计算 SMA30。最近收盘: {recent}",
|
||||
en: "Klines ready {count} bars; SMA30 available. Recent closes: {recent}",
|
||||
},
|
||||
"log.trend.rateLimit429": { zh: "TrendEngine 429: {error}", en: "TrendEngine 429: {error}" },
|
||||
"log.trend.loopError": { zh: "策略循环异常: {error}", en: "Strategy loop error: {error}" },
|
||||
"log.trend.rateLimitUpdateError": {
|
||||
zh: "限频控制器状态更新失败: {error}",
|
||||
en: "Rate limit controller update failed: {error}",
|
||||
},
|
||||
"log.trend.detectPosition": {
|
||||
zh: "检测到已有持仓: {direction} {amount} @ {price}",
|
||||
en: "Detected existing position: {direction} {amount} @ {price}",
|
||||
},
|
||||
"log.trend.detectOrders": {
|
||||
zh: "检测到已有挂单 {count} 笔,将按策略规则接管",
|
||||
en: "Detected {count} existing orders; taking over per strategy rules",
|
||||
},
|
||||
"log.trend.stopCooldown": {
|
||||
zh: "止损后冷却中 {seconds}s,忽略入场信号",
|
||||
en: "Post-stop cooldown {seconds}s; ignoring entry signals",
|
||||
},
|
||||
"log.trend.alreadyEntered": {
|
||||
zh: "本分钟已入场,忽略新的 SMA 入场信号",
|
||||
en: "Entry already executed this minute; ignoring new SMA signal",
|
||||
},
|
||||
"log.trend.bandwidthBlocked": {
|
||||
zh: "布林带宽度不足:{bandwidth} < {minBandwidth},忽略入场信号",
|
||||
en: "Bollinger bandwidth too low: {bandwidth} < {minBandwidth}, ignoring entry",
|
||||
},
|
||||
"log.trend.cancelMissing": { zh: "撤单时部分订单已不存在,忽略", en: "Some orders missing during cancel; ignore" },
|
||||
"log.trend.cancelFail": { zh: "撤销挂单失败: {error}", en: "Failed to cancel orders: {error}" },
|
||||
"log.trend.crossDown": { zh: "下穿SMA30,市价开空", en: "Crossed below SMA30, market sell" },
|
||||
"log.trend.crossUp": { zh: "上穿SMA30,市价开多", en: "Crossed above SMA30, market buy" },
|
||||
"log.trend.marketOrderFail": { zh: "市价下单失败: {error}", en: "Market order failed: {error}" },
|
||||
"log.trend.entryPricePending": {
|
||||
zh: "持仓均价尚未同步,等待交易所账户快照更新后再执行风控",
|
||||
en: "Entry price not synced; waiting for account snapshot before risk checks",
|
||||
},
|
||||
"log.trend.stopPreCancelMissing": { zh: "止损前撤单发现订单已不存在", en: "Stop pre-close cancel found missing order" },
|
||||
"log.trend.marketCloseGuard": {
|
||||
zh: "市价平仓保护触发:closePx={closePx} mark={mark} 偏离 {pctDiff}% > {limitPct}%",
|
||||
en: "Market close guard triggered: closePx={closePx} mark={mark} deviation {pctDiff}% > {limitPct}%",
|
||||
},
|
||||
"log.trend.stopClose": { zh: "止损平仓: {side}", en: "Stop close: {side}" },
|
||||
"log.trend.targetStopMissing": { zh: "止损平仓时目标订单已不存在", en: "Target order missing during stop close" },
|
||||
"log.trend.stopCloseFail": { zh: "止损平仓失败: {error}", en: "Failed to close position on stop: {error}" },
|
||||
"log.trend.placeStopFail": { zh: "挂止损单失败: {error}", en: "Failed to place stop order: {error}" },
|
||||
"log.trend.stopMissingSkip": { zh: "原止损单已不存在,跳过撤销", en: "Existing stop missing, skipping cancel" },
|
||||
"log.trend.cancelStopFail": { zh: "取消原止损单失败: {error}", en: "Failed to cancel existing stop: {error}" },
|
||||
"log.trend.moveStop": { zh: "移动止损到 {price}", en: "Moved stop to {price}" },
|
||||
"log.trend.moveStopFail": { zh: "移动止损失败: {error}", en: "Failed to move stop: {error}" },
|
||||
"log.trend.restoreStop": { zh: "恢复原止损 @ {price}", en: "Restored original stop @ {price}" },
|
||||
"log.trend.restoreStopFail": { zh: "恢复原止损失败: {error}", en: "Failed to restore original stop: {error}" },
|
||||
"log.trend.trailingFail": { zh: "挂动态止盈失败: {error}", en: "Failed to place trailing stop: {error}" },
|
||||
"log.trend.precisionSynced": {
|
||||
zh: "已同步交易精度: priceTick={priceTick} qtyStep={qtyStep}",
|
||||
en: "Synced precision: priceTick={priceTick} qtyStep={qtyStep}",
|
||||
},
|
||||
"log.trend.precisionFailed": { zh: "同步精度失败: {error}", en: "Failed to sync precision: {error}" },
|
||||
"log.trend.updateHandlerError": { zh: "更新回调处理异常: {error}", en: "Update handler error: {error}" },
|
||||
"log.trend.snapshotDispatchError": { zh: "快照或更新分发异常: {error}", en: "Snapshot/update dispatch error: {error}" },
|
||||
};
|
||||
|
||||
const formatTemplate = (template: string, params: Record<string, unknown>): string => {
|
||||
return template.replace(/\{(\w+)\}/g, (_match, key) => {
|
||||
const value = params[key];
|
||||
return value === undefined || value === null ? `{${key}}` : String(value);
|
||||
});
|
||||
};
|
||||
|
||||
export type TranslationKey = keyof typeof translations | string;
|
||||
|
||||
export function t(key: TranslationKey, params: Record<string, unknown> = {}, lang: Language = language): string {
|
||||
const entry = translations[key as keyof typeof translations];
|
||||
const value = entry ? entry[lang] ?? entry.zh : null;
|
||||
if (typeof value === "function") {
|
||||
return value(params, lang);
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return Object.keys(params).length ? formatTemplate(value, params) : value;
|
||||
}
|
||||
// Fallback: return key to surface missing translations
|
||||
return String(key);
|
||||
}
|
||||
|
||||
export function isEnglish(lang: Language = language): boolean {
|
||||
return lang === "en";
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-15
@@ -9,6 +9,7 @@ import { BasisApp } from "./BasisApp";
|
||||
import { isBasisStrategyEnabled } from "../config";
|
||||
import { loadCopyrightFragments, verifyCopyrightIntegrity } from "../utils/copyright";
|
||||
import { resolveExchangeId } from "../exchanges/create-adapter";
|
||||
import { t } from "../i18n";
|
||||
|
||||
interface StrategyOption {
|
||||
id: "trend" | "guardian" | "maker" | "offset-maker" | "basis" | "grid";
|
||||
@@ -20,32 +21,32 @@ interface StrategyOption {
|
||||
const BASE_STRATEGIES: StrategyOption[] = [
|
||||
{
|
||||
id: "trend",
|
||||
label: "趋势跟随策略 (SMA30)",
|
||||
description: "监控均线信号,自动进出场并维护止损/止盈",
|
||||
label: t("app.strategy.trend.label"),
|
||||
description: t("app.strategy.trend.desc"),
|
||||
component: TrendApp,
|
||||
},
|
||||
{
|
||||
id: "guardian",
|
||||
label: "Guardian 防守策略",
|
||||
description: "不主动开仓,只为现有仓位补挂/移动止损,防止裸奔",
|
||||
label: t("app.strategy.guardian.label"),
|
||||
description: t("app.strategy.guardian.desc"),
|
||||
component: GuardianApp,
|
||||
},
|
||||
{
|
||||
id: "maker",
|
||||
label: "做市刷单策略",
|
||||
description: "双边挂单提供流动性,自动追价与风控止损",
|
||||
label: t("app.strategy.maker.label"),
|
||||
description: t("app.strategy.maker.desc"),
|
||||
component: MakerApp,
|
||||
},
|
||||
{
|
||||
id: "grid",
|
||||
label: "基础网格策略",
|
||||
description: "在上下边界之间布设等比网格,自动加仓与减仓",
|
||||
label: t("app.strategy.grid.label"),
|
||||
description: t("app.strategy.grid.desc"),
|
||||
component: GridApp,
|
||||
},
|
||||
{
|
||||
id: "offset-maker",
|
||||
label: "偏移做市策略",
|
||||
description: "根据盘口深度自动偏移挂单并在极端不平衡时撤退",
|
||||
label: t("app.strategy.offset.label"),
|
||||
description: t("app.strategy.offset.desc"),
|
||||
component: OffsetMakerApp,
|
||||
},
|
||||
];
|
||||
@@ -66,8 +67,8 @@ export function App() {
|
||||
...BASE_STRATEGIES,
|
||||
{
|
||||
id: "basis" as const,
|
||||
label: "期现套利策略",
|
||||
description: "监控期货与现货盘口差价,辅助发现套利机会",
|
||||
label: t("app.strategy.basis.label"),
|
||||
description: t("app.strategy.basis.desc"),
|
||||
component: BasisApp,
|
||||
},
|
||||
];
|
||||
@@ -99,13 +100,13 @@ export function App() {
|
||||
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
||||
<Text color="gray">{copyright.bannerText}</Text>
|
||||
{integrityOk ? null : (
|
||||
<Text color="red">警告: 版权校验失败,当前版本可能被篡改。</Text>
|
||||
<Text color="red">{t("app.integrity.warning")}</Text>
|
||||
)}
|
||||
<Box height={1}>
|
||||
<Text color="gray">────────────────────────────────────────────────────</Text>
|
||||
</Box>
|
||||
<Text color="cyanBright">请选择要运行的策略</Text>
|
||||
<Text color="gray">使用 ↑/↓ 选择,回车开始,Ctrl+C 退出。</Text>
|
||||
<Text color="cyanBright">{t("app.pickStrategy")}</Text>
|
||||
<Text color="gray">{t("app.pickHint")}</Text>
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{strategies.map((strategy, index) => {
|
||||
const active = index === cursor;
|
||||
|
||||
+62
-34
@@ -5,6 +5,7 @@ import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-a
|
||||
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import { BasisArbEngine, type BasisArbSnapshot } from "../strategy/basis-arb-engine";
|
||||
import { formatNumber } from "../utils/format";
|
||||
import { t } from "../i18n";
|
||||
|
||||
interface BasisAppProps {
|
||||
onExit: () => void;
|
||||
@@ -31,7 +32,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (exchangeId !== "aster") {
|
||||
setError(new Error("期现套利策略目前仅支持 Aster 交易所。请设置 EXCHANGE=aster 后重试。"));
|
||||
setError(new Error(t("basis.onlyAster")));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -57,8 +58,8 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="red">无法启动期现套利策略: {error.message}</Text>
|
||||
<Text color="gray">按 Esc 返回菜单。</Text>
|
||||
<Text color="red">{t("basis.startFailed", { message: error.message })}</Text>
|
||||
<Text color="gray">{t("common.backHint")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -66,7 +67,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<Box padding={1}>
|
||||
<Text>正在初始化期现套利监控…</Text>
|
||||
<Text>{t("basis.initializing")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -85,10 +86,15 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
const fundingRatePct = snapshot.fundingRate != null ? `${(snapshot.fundingRate * 100).toFixed(4)}%` : "-";
|
||||
const fundingUpdated = snapshot.fundingLastUpdate ? new Date(snapshot.fundingLastUpdate).toLocaleTimeString() : "-";
|
||||
const nextFundingTime = snapshot.nextFundingTime ? new Date(snapshot.nextFundingTime).toLocaleTimeString() : "-";
|
||||
const fundingIncomePerFunding = snapshot.fundingIncomePerFunding != null ? `${formatNumber(snapshot.fundingIncomePerFunding, 4)} USDT` : "-";
|
||||
const fundingIncomePerDay = snapshot.fundingIncomePerDay != null ? `${formatNumber(snapshot.fundingIncomePerDay, 4)} USDT` : "-";
|
||||
const takerFeesPerRoundTrip = snapshot.takerFeesPerRoundTrip != null ? `${formatNumber(snapshot.takerFeesPerRoundTrip, 4)} USDT` : "-";
|
||||
const fundingCountToBreakeven = snapshot.fundingCountToBreakeven != null ? `${formatNumber(snapshot.fundingCountToBreakeven, 2)} 次` : "-";
|
||||
const fundingIncomePerFunding =
|
||||
snapshot.fundingIncomePerFunding != null ? `${formatNumber(snapshot.fundingIncomePerFunding, 4)} USDT` : "-";
|
||||
const fundingIncomePerDay =
|
||||
snapshot.fundingIncomePerDay != null ? `${formatNumber(snapshot.fundingIncomePerDay, 4)} USDT` : "-";
|
||||
const takerFeesPerRoundTrip =
|
||||
snapshot.takerFeesPerRoundTrip != null ? `${formatNumber(snapshot.takerFeesPerRoundTrip, 4)} USDT` : "-";
|
||||
const fundingCountToBreakeven =
|
||||
snapshot.fundingCountToBreakeven != null ? formatNumber(snapshot.fundingCountToBreakeven, 2) : "-";
|
||||
const feePct = (basisConfig.takerFeeRate * 100).toFixed(4);
|
||||
const feedStatus = snapshot.feedStatus;
|
||||
const lastLogs = snapshot.tradeLog.slice(-5);
|
||||
const spotBalances = (snapshot.spotBalances ?? []).filter((b) => Math.abs(b.free) > 0 || Math.abs(b.locked) > 0);
|
||||
@@ -97,72 +103,94 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="cyanBright">Basis Arbitrage Dashboard</Text>
|
||||
<Text color="cyanBright">{t("basis.title")}</Text>
|
||||
<Text>
|
||||
交易所: {exchangeName} | 期货合约: {snapshot.futuresSymbol} | 现货交易对: {snapshot.spotSymbol}
|
||||
{t("basis.headerLine", {
|
||||
exchange: exchangeName,
|
||||
futures: snapshot.futuresSymbol,
|
||||
spot: snapshot.spotSymbol,
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">按 Esc 返回策略选择 | 数据状态: 期货({feedStatus.futures ? "OK" : "--"}) 现货({feedStatus.spot ? "OK" : "--"}) 资金费率({feedStatus.funding ? "OK" : "--"})</Text>
|
||||
<Text color="gray">最近更新时间: {lastUpdated}</Text>
|
||||
<Text color="gray">
|
||||
{t("basis.statusLine", {
|
||||
futuresStatus: feedStatus.futures ? "OK" : "--",
|
||||
spotStatus: feedStatus.spot ? "OK" : "--",
|
||||
fundingStatus: feedStatus.funding ? "OK" : "--",
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">{t("basis.lastUpdated", { time: lastUpdated })}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="column" marginRight={4}>
|
||||
<Text color="greenBright">期货盘口</Text>
|
||||
<Text>买一: {futuresBid} | 卖一: {futuresAsk}</Text>
|
||||
<Text color="gray">更新时间: {futuresUpdated}</Text>
|
||||
<Text color="greenBright">{t("basis.section.futures")}</Text>
|
||||
<Text>{t("basis.bookLine", { bid: futuresBid, ask: futuresAsk })}</Text>
|
||||
<Text color="gray">{t("basis.updatedAt", { time: futuresUpdated })}</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color="greenBright">现货盘口</Text>
|
||||
<Text>买一: {spotBid} | 卖一: {spotAsk}</Text>
|
||||
<Text color="gray">更新时间: {spotUpdated}</Text>
|
||||
<Text color="greenBright">{t("basis.section.spot")}</Text>
|
||||
<Text>{t("basis.bookLine", { bid: spotBid, ask: spotAsk })}</Text>
|
||||
<Text color="gray">{t("basis.updatedAt", { time: spotUpdated })}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">资金费率</Text>
|
||||
<Text>当前资金费率: {fundingRatePct}</Text>
|
||||
<Text color="gray">资金费率更新时间: {fundingUpdated} | 下次结算时间: {nextFundingTime}</Text>
|
||||
<Text>单次资金费率收益(估): {fundingIncomePerFunding} | 日收益(估): {fundingIncomePerDay}</Text>
|
||||
<Text>双边吃单手续费(估): {takerFeesPerRoundTrip} | 回本所需资金费率次数: {fundingCountToBreakeven}</Text>
|
||||
<Text color="yellow">{t("basis.section.funding")}</Text>
|
||||
<Text>{t("basis.fundingRate", { rate: fundingRatePct })}</Text>
|
||||
<Text color="gray">
|
||||
{t("basis.fundingTimes", { updated: fundingUpdated, next: nextFundingTime })}
|
||||
</Text>
|
||||
<Text>{t("basis.fundingIncome", { per: fundingIncomePerFunding, perDay: fundingIncomePerDay })}</Text>
|
||||
<Text>{t("basis.takerFees", { fees: takerFeesPerRoundTrip, count: fundingCountToBreakeven })}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="column" marginRight={4}>
|
||||
<Text color="cyan">现货账户余额(非0)</Text>
|
||||
<Text color="cyan">{t("basis.spotBalanceTitle")}</Text>
|
||||
{spotBalances.length ? (
|
||||
spotBalances.map((b) => (
|
||||
<Text key={`spot-${b.asset}`}>
|
||||
{b.asset}: 可用 {formatNumber(b.free, 8)} | 冻结 {formatNumber(b.locked, 8)}
|
||||
{t("basis.balanceLine", {
|
||||
asset: b.asset,
|
||||
free: formatNumber(b.free, 8),
|
||||
locked: formatNumber(b.locked, 8),
|
||||
})}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color="gray">无</Text>
|
||||
<Text color="gray">{t("basis.none")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color="cyan">合约账户余额(非0)</Text>
|
||||
<Text color="cyan">{t("basis.futuresBalanceTitle")}</Text>
|
||||
{futuresBalances.length ? (
|
||||
futuresBalances.map((b) => (
|
||||
<Text key={`fut-${b.asset}`}>
|
||||
{b.asset}: 钱包 {formatNumber(b.wallet, 8)} | 可用 {formatNumber(b.available, 8)}
|
||||
{t("basis.futuresBalanceLine", {
|
||||
asset: b.asset,
|
||||
wallet: formatNumber(b.wallet, 8),
|
||||
available: formatNumber(b.available, 8),
|
||||
})}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color="gray">无</Text>
|
||||
<Text color="gray">{t("basis.none")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={snapshot.opportunity ? "greenBright" : "redBright"}>套利差价(卖期货 / 买现货)</Text>
|
||||
<Text color={snapshot.opportunity ? "green" : undefined}>毛价差: {spread} USDT | {spreadBps} bp</Text>
|
||||
<Text color={snapshot.opportunity ? "greenBright" : "redBright"}>{t("basis.spreadTitle")}</Text>
|
||||
<Text color={snapshot.opportunity ? "green" : undefined}>
|
||||
{t("basis.spreadLine", { spread, bps: spreadBps })}
|
||||
</Text>
|
||||
<Text color={snapshot.opportunity ? "green" : "red"}>
|
||||
扣除 taker 手续费 ({(basisConfig.takerFeeRate * 100).toFixed(4)}% × 双边): {netSpread} USDT | {netSpreadBps} bp
|
||||
{t("basis.netSpreadLine", { feePct, net: netSpread, netBps: netSpreadBps })}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">最近事件</Text>
|
||||
<Text color="yellow">{t("common.section.recent")}</Text>
|
||||
{lastLogs.length ? (
|
||||
lastLogs.map((entry, index) => {
|
||||
const color = entry.type === "entry" ? "green" : entry.type === "exit" ? "red" : undefined;
|
||||
@@ -173,7 +201,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text color="gray">暂无日志</Text>
|
||||
<Text color="gray">{t("common.noLogs")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+69
-27
@@ -6,6 +6,7 @@ import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import { GridEngine, type GridEngineSnapshot } from "../strategy/grid-engine";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
import { formatNumber } from "../utils/format";
|
||||
import { t } from "../i18n";
|
||||
|
||||
interface GridAppProps {
|
||||
onExit: () => void;
|
||||
@@ -59,8 +60,8 @@ export function GridApp({ onExit }: GridAppProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="red">启动失败: {error.message}</Text>
|
||||
<Text color="gray">请检查环境变量和网络连通性。</Text>
|
||||
<Text color="red">{t("common.startFailed", { message: error.message })}</Text>
|
||||
<Text color="gray">{t("common.checkEnv")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -68,17 +69,17 @@ export function GridApp({ onExit }: GridAppProps) {
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<Box padding={1}>
|
||||
<Text>正在初始化网格策略…</Text>
|
||||
<Text>{t("grid.initializing")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const feedStatus = snapshot.feedStatus;
|
||||
const feedEntries: Array<{ key: keyof typeof feedStatus; label: string }> = [
|
||||
{ key: "account", label: "账户" },
|
||||
{ key: "orders", label: "订单" },
|
||||
{ key: "depth", label: "深度" },
|
||||
{ key: "ticker", label: "行情" },
|
||||
{ key: "account", label: t("maker.feed.account") },
|
||||
{ key: "orders", label: t("maker.feed.orders") },
|
||||
{ key: "depth", label: t("maker.feed.depth") },
|
||||
{ key: "ticker", label: t("maker.feed.ticker") },
|
||||
];
|
||||
const stopReason = snapshot.running ? null : snapshot.stopReason;
|
||||
const lastLogs = snapshot.tradeLog.slice(-5);
|
||||
@@ -112,71 +113,112 @@ export function GridApp({ onExit }: GridAppProps) {
|
||||
price: order.price,
|
||||
amount: formatNumber(order.amount, 4),
|
||||
}));
|
||||
const statusLabel = snapshot.running ? t("status.running") : t("status.paused");
|
||||
const directionLabel =
|
||||
snapshot.direction === "both"
|
||||
? t("grid.direction.both")
|
||||
: snapshot.direction === "long"
|
||||
? t("grid.direction.long")
|
||||
: t("grid.direction.short");
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="cyanBright">Grid Strategy Dashboard</Text>
|
||||
<Text color="cyanBright">{t("grid.title")}</Text>
|
||||
<Text>
|
||||
交易所: {exchangeName} | 交易对: {snapshot.symbol} | 状态: {snapshot.running ? "运行中" : "暂停"} | 方向: {snapshot.direction}
|
||||
{t("grid.headerLine", {
|
||||
exchange: exchangeName,
|
||||
symbol: snapshot.symbol,
|
||||
status: statusLabel,
|
||||
direction: directionLabel,
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
实时价格: {formatNumber(snapshot.lastPrice, 4)} | 下界: {formatNumber(snapshot.lowerPrice, 4)} | 上界: {formatNumber(snapshot.upperPrice, 4)} | 网格数量: {snapshot.gridLines.length}
|
||||
{t("grid.priceLine", {
|
||||
lastPrice: formatNumber(snapshot.lastPrice, 4),
|
||||
lower: formatNumber(snapshot.lowerPrice, 4),
|
||||
upper: formatNumber(snapshot.upperPrice, 4),
|
||||
count: snapshot.gridLines.length,
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">数据状态:
|
||||
<Text color="gray">
|
||||
{t("grid.dataStatus")}
|
||||
{feedEntries.map((entry, index) => (
|
||||
<Text key={entry.key} color={feedStatus[entry.key] ? "green" : "red"}>
|
||||
{index === 0 ? " " : " "}
|
||||
{entry.label}
|
||||
</Text>
|
||||
))}
|
||||
| 按 Esc 返回策略选择
|
||||
{" | "}
|
||||
{t("common.backHint")}
|
||||
</Text>
|
||||
{stopReason ? <Text color="yellow">暂停原因: {stopReason}</Text> : null}
|
||||
{stopReason ? <Text color="yellow">{t("grid.stopReason", { reason: stopReason })}</Text> : null}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="column" marginRight={4}>
|
||||
<Text color="greenBright">网格配置</Text>
|
||||
<Text color="greenBright">{t("grid.configTitle")}</Text>
|
||||
<Text>
|
||||
单笔数量: {formatNumber(gridConfig.orderSize, 6)} | 最大仓位: {formatNumber(gridConfig.maxPositionSize, 6)}
|
||||
{t("grid.configSize", {
|
||||
orderSize: formatNumber(gridConfig.orderSize, 6),
|
||||
maxPosition: formatNumber(gridConfig.maxPositionSize, 6),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
止损阈值: {(gridConfig.stopLossPct * 100).toFixed(2)}% | 重启阈值: {(gridConfig.restartTriggerPct * 100).toFixed(2)}% | 自动重启: {gridConfig.autoRestart ? "启用" : "关闭"}
|
||||
{t("grid.configRisk", {
|
||||
stopLoss: (gridConfig.stopLossPct * 100).toFixed(2),
|
||||
restart: (gridConfig.restartTriggerPct * 100).toFixed(2),
|
||||
autoRestart: gridConfig.autoRestart ? t("common.enabled") : t("common.disabled"),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
刷新间隔: {gridConfig.refreshIntervalMs} ms
|
||||
{t("grid.refreshInterval", { interval: gridConfig.refreshIntervalMs })}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color="greenBright">持仓</Text>
|
||||
<Text color="greenBright">{t("common.section.position")}</Text>
|
||||
{hasPosition ? (
|
||||
<>
|
||||
<Text>
|
||||
当前持仓: {position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(position.positionAmt), 6)} | 均价: {formatNumber(position.entryPrice, 4)}
|
||||
{t("grid.positionLine", {
|
||||
direction: position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
|
||||
qty: formatNumber(Math.abs(position.positionAmt), 6),
|
||||
avgPrice: formatNumber(position.entryPrice, 4),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
未实现盈亏: {formatNumber(position.unrealizedProfit, 4)} | 标记价: {formatNumber(position.markPrice, 4)}
|
||||
{t("grid.unrealizedLine", {
|
||||
pnl: formatNumber(position.unrealizedProfit, 4),
|
||||
mark: formatNumber(position.markPrice, 4),
|
||||
})}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text color="gray">当前无持仓</Text>
|
||||
<Text color="gray">{t("common.noPosition")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">网格线</Text>
|
||||
{gridRows.length > 0 ? <DataTable columns={gridColumns} rows={gridRows} /> : <Text color="gray">暂无网格线</Text>}
|
||||
<Text color="yellow">{t("grid.linesTitle")}</Text>
|
||||
{gridRows.length > 0 ? (
|
||||
<DataTable columns={gridColumns} rows={gridRows} />
|
||||
) : (
|
||||
<Text color="gray">{t("grid.noLines")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">目标挂单</Text>
|
||||
{desiredRows.length > 0 ? <DataTable columns={desiredColumns} rows={desiredRows} /> : <Text color="gray">暂无目标挂单</Text>}
|
||||
<Text color="yellow">{t("maker.targetOrders")}</Text>
|
||||
{desiredRows.length > 0 ? (
|
||||
<DataTable columns={desiredColumns} rows={desiredRows} />
|
||||
) : (
|
||||
<Text color="gray">{t("maker.noTargetOrders")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">最近事件</Text>
|
||||
<Text color="yellow">{t("common.section.recent")}</Text>
|
||||
{lastLogs.length > 0 ? (
|
||||
lastLogs.map((item, index) => (
|
||||
<Text key={`${item.time}-${index}`}>
|
||||
@@ -184,7 +226,7 @@ export function GridApp({ onExit }: GridAppProps) {
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color="gray">暂无日志</Text>
|
||||
<Text color="gray">{t("common.noLogs")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+39
-16
@@ -6,12 +6,13 @@ import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import { GuardianEngine, type GuardianEngineSnapshot } from "../strategy/guardian-engine";
|
||||
import { formatNumber } from "../utils/format";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
import { t } from "../i18n";
|
||||
|
||||
interface GuardianAppProps {
|
||||
onExit: () => void;
|
||||
}
|
||||
|
||||
const READY_MESSAGE = "正在等待行情/账户推送…";
|
||||
const READY_MESSAGE = t("guardian.readyMessage");
|
||||
const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY);
|
||||
|
||||
export function GuardianApp({ onExit }: GuardianAppProps) {
|
||||
@@ -55,8 +56,8 @@ export function GuardianApp({ onExit }: GuardianAppProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="red">Guardian 策略启动失败: {error.message}</Text>
|
||||
<Text color="gray">请检查环境变量和网络连通性。</Text>
|
||||
<Text color="red">{t("guardian.startFailed", { message: error.message })}</Text>
|
||||
<Text color="gray">{t("common.checkEnv")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -64,7 +65,7 @@ export function GuardianApp({ onExit }: GuardianAppProps) {
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<Box padding={1}>
|
||||
<Text>正在初始化 Guardian 策略…</Text>
|
||||
<Text>{t("guardian.initializing")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -97,43 +98,65 @@ export function GuardianApp({ onExit }: GuardianAppProps) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} paddingY={0}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="cyanBright">Guardian Strategy Dashboard</Text>
|
||||
<Text color="cyanBright">{t("guardian.title")}</Text>
|
||||
<Text>
|
||||
交易所: {exchangeName} | 交易对: {snapshot.symbol} | 最近价格: {formatNumber(snapshot.lastPrice, 2)} | 状态: {ready ? "实时运行" : READY_MESSAGE}
|
||||
{t("guardian.headerLine", {
|
||||
exchange: exchangeName,
|
||||
symbol: snapshot.symbol,
|
||||
lastPrice: formatNumber(snapshot.lastPrice, 2),
|
||||
status: ready ? t("status.live") : READY_MESSAGE,
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">策略只会维护止损/止盈,不会主动开仓。按 Esc 返回菜单。</Text>
|
||||
<Text color="gray">{t("guardian.hint")}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="greenBright">当前仓位与风控</Text>
|
||||
<Text color="greenBright">{t("guardian.positionTitle")}</Text>
|
||||
{hasPosition ? (
|
||||
<>
|
||||
<Text>
|
||||
方向: {position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(position.positionAmt), 4)} | 开仓价: {formatNumber(position.entryPrice, 2)} | 浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT
|
||||
{t("guardian.positionLine", {
|
||||
direction: position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
|
||||
qty: formatNumber(Math.abs(position.positionAmt), 4),
|
||||
entry: formatNumber(position.entryPrice, 2),
|
||||
pnl: formatNumber(snapshot.pnl, 4),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
目标止损价: {formatNumber(snapshot.targetStopPrice, 2)} | 当前止损单: {formatNumber(stopOrderPrice, 2)} | 动态止盈触发: {formatNumber(snapshot.trailingActivationPrice, 2)} | 动态止盈单: {formatNumber(trailingActivate, 2)}
|
||||
{t("guardian.stopLine", {
|
||||
targetStop: formatNumber(snapshot.targetStopPrice, 2),
|
||||
stopOrder: formatNumber(stopOrderPrice, 2),
|
||||
trailingTrigger: formatNumber(snapshot.trailingActivationPrice, 2),
|
||||
trailingOrder: formatNumber(trailingActivate, 2),
|
||||
})}
|
||||
</Text>
|
||||
<Text color={snapshot.requiresStop ? "yellow" : "gray"}>
|
||||
Guardian 状态: {guardStatus === "protecting" ? "已挂止损" : guardStatus === "pending" ? "缺少止损,正在同步" : "监听中"}
|
||||
{t("guardian.stateLabel", {
|
||||
state:
|
||||
guardStatus === "protecting"
|
||||
? t("guardian.status.protecting")
|
||||
: guardStatus === "pending"
|
||||
? t("guardian.status.pending")
|
||||
: t("guardian.status.listening"),
|
||||
})}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text color="gray">当前无持仓,Guardian 正在监听新的仓位变化。</Text>
|
||||
<Text color="gray">{t("guardian.noPosition")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">当前挂单</Text>
|
||||
<Text color="yellow">{t("common.section.orders")}</Text>
|
||||
{orderRows.length > 0 ? (
|
||||
<DataTable columns={orderColumns} rows={orderRows} />
|
||||
) : (
|
||||
<Text color="gray">暂无保护类挂单</Text>
|
||||
<Text color="gray">{t("guardian.noProtectiveOrders")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">最近事件</Text>
|
||||
<Text color="yellow">{t("common.section.recent")}</Text>
|
||||
{lastLogs.length > 0 ? (
|
||||
lastLogs.map((item, index) => (
|
||||
<Text key={`${item.time}-${index}`}>
|
||||
@@ -141,7 +164,7 @@ export function GuardianApp({ onExit }: GuardianAppProps) {
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color="gray">暂无日志</Text>
|
||||
<Text color="gray">{t("common.noLogs")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+38
-22
@@ -6,6 +6,7 @@ import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import { MakerEngine, type MakerEngineSnapshot } from "../strategy/maker-engine";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
import { formatNumber } from "../utils/format";
|
||||
import { t } from "../i18n";
|
||||
|
||||
interface MakerAppProps {
|
||||
onExit: () => void;
|
||||
@@ -54,8 +55,8 @@ export function MakerApp({ onExit }: MakerAppProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="red">启动失败: {error.message}</Text>
|
||||
<Text color="gray">请检查环境变量和网络连通性。</Text>
|
||||
<Text color="red">{t("common.startFailed", { message: error.message })}</Text>
|
||||
<Text color="gray">{t("common.checkEnv")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export function MakerApp({ onExit }: MakerAppProps) {
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<Box padding={1}>
|
||||
<Text>正在初始化做市策略…</Text>
|
||||
<Text>{t("maker.initializing")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -113,22 +114,29 @@ export function MakerApp({ onExit }: MakerAppProps) {
|
||||
const lastLogs = snapshot.tradeLog.slice(-5);
|
||||
const feedStatus = snapshot.feedStatus;
|
||||
const feedEntries: Array<{ key: keyof typeof feedStatus; label: string }> = [
|
||||
{ key: "account", label: "账户" },
|
||||
{ key: "orders", label: "订单" },
|
||||
{ key: "depth", label: "深度" },
|
||||
{ key: "ticker", label: "Ticker" },
|
||||
{ key: "account", label: t("maker.feed.account") },
|
||||
{ key: "orders", label: t("maker.feed.orders") },
|
||||
{ key: "depth", label: t("maker.feed.depth") },
|
||||
{ key: "ticker", label: t("maker.feed.ticker") },
|
||||
];
|
||||
const readyStatus = snapshot.ready ? t("status.live") : t("status.waitingData");
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="cyanBright">Maker Strategy Dashboard</Text>
|
||||
<Text color="cyanBright">{t("maker.title")}</Text>
|
||||
<Text>
|
||||
交易所: {exchangeName} | 交易对: {snapshot.symbol} | 买一价: {formatNumber(topBid, priceDigits)} | 卖一价: {formatNumber(topAsk, priceDigits)} | 点差: {spreadDisplay}
|
||||
{t("maker.headerLine", {
|
||||
exchange: exchangeName,
|
||||
symbol: snapshot.symbol,
|
||||
bid: formatNumber(topBid, priceDigits),
|
||||
ask: formatNumber(topAsk, priceDigits),
|
||||
spread: spreadDisplay,
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">状态: {snapshot.ready ? "实时运行" : "等待市场数据"} | 按 Esc 返回策略选择</Text>
|
||||
<Text color="gray">{t("trend.statusLine", { status: readyStatus })}</Text>
|
||||
<Text>
|
||||
数据状态:
|
||||
{t("maker.dataStatus")}
|
||||
{feedEntries.map((entry, index) => (
|
||||
<Text key={entry.key} color={feedStatus[entry.key] ? "green" : "red"}>
|
||||
{index === 0 ? " " : " "}
|
||||
@@ -140,44 +148,52 @@ export function MakerApp({ onExit }: MakerAppProps) {
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="column" marginRight={4}>
|
||||
<Text color="greenBright">持仓</Text>
|
||||
<Text color="greenBright">{t("common.section.position")}</Text>
|
||||
{hasPosition ? (
|
||||
<>
|
||||
<Text>
|
||||
方向: {snapshot.position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(snapshot.position.positionAmt), 4)} | 开仓价: {formatNumber(snapshot.position.entryPrice, priceDigits)}
|
||||
{t("maker.positionLine", {
|
||||
direction:
|
||||
snapshot.position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
|
||||
qty: formatNumber(Math.abs(snapshot.position.positionAmt), 4),
|
||||
entry: formatNumber(snapshot.position.entryPrice, priceDigits),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.accountUnrealized, 4)} USDT
|
||||
{t("maker.pnlLine", {
|
||||
pnl: formatNumber(snapshot.pnl, 4),
|
||||
accountPnl: formatNumber(snapshot.accountUnrealized, 4),
|
||||
})}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text color="gray">当前无持仓</Text>
|
||||
<Text color="gray">{t("common.noPosition")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color="greenBright">目标挂单</Text>
|
||||
<Text color="greenBright">{t("maker.targetOrders")}</Text>
|
||||
{desiredRows.length > 0 ? (
|
||||
<DataTable columns={desiredColumns} rows={desiredRows} />
|
||||
) : (
|
||||
<Text color="gray">暂无目标挂单</Text>
|
||||
<Text color="gray">{t("maker.noTargetOrders")}</Text>
|
||||
)}
|
||||
<Text>
|
||||
累计成交量: {formatNumber(snapshot.sessionVolume, 2)} USDT
|
||||
{t("trend.volumeLine", { volume: formatNumber(snapshot.sessionVolume, 2) })}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">当前挂单</Text>
|
||||
<Text color="yellow">{t("common.section.orders")}</Text>
|
||||
{openOrderRows.length > 0 ? (
|
||||
<DataTable columns={openOrderColumns} rows={openOrderRows} />
|
||||
) : (
|
||||
<Text color="gray">暂无挂单</Text>
|
||||
<Text color="gray">{t("common.noOrders")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">最近事件</Text>
|
||||
<Text color="yellow">{t("common.section.recent")}</Text>
|
||||
{lastLogs.length > 0 ? (
|
||||
lastLogs.map((item, index) => (
|
||||
<Text key={`${item.time}-${index}`}>
|
||||
@@ -185,7 +201,7 @@ export function MakerApp({ onExit }: MakerAppProps) {
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color="gray">暂无日志</Text>
|
||||
<Text color="gray">{t("common.noLogs")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+48
-24
@@ -6,6 +6,7 @@ import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import { OffsetMakerEngine, type OffsetMakerEngineSnapshot } from "../strategy/offset-maker-engine";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
import { formatNumber } from "../utils/format";
|
||||
import { t } from "../i18n";
|
||||
|
||||
interface OffsetMakerAppProps {
|
||||
onExit: () => void;
|
||||
@@ -54,8 +55,8 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="red">启动失败: {error.message}</Text>
|
||||
<Text color="gray">请检查环境变量和网络连通性。</Text>
|
||||
<Text color="red">{t("common.startFailed", { message: error.message })}</Text>
|
||||
<Text color="gray">{t("common.checkEnv")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) {
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<Box padding={1}>
|
||||
<Text>正在初始化偏移做市策略…</Text>
|
||||
<Text>{t("offset.initializing")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -113,68 +114,91 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) {
|
||||
];
|
||||
|
||||
const lastLogs = snapshot.tradeLog.slice(-5);
|
||||
const imbalanceLabel = snapshot.depthImbalance === "balanced"
|
||||
? "均衡"
|
||||
: snapshot.depthImbalance === "buy_dominant"
|
||||
? "买盘占优"
|
||||
: "卖盘占优";
|
||||
const imbalanceLabel =
|
||||
snapshot.depthImbalance === "balanced"
|
||||
? t("offset.imbalance.balanced")
|
||||
: snapshot.depthImbalance === "buy_dominant"
|
||||
? t("offset.imbalance.buy")
|
||||
: t("offset.imbalance.sell");
|
||||
const readyStatus = snapshot.ready ? t("status.live") : t("status.waitingData");
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="cyanBright">Offset Maker Strategy Dashboard</Text>
|
||||
<Text color="cyanBright">{t("offset.title")}</Text>
|
||||
<Text>
|
||||
交易所: {exchangeName} | 交易对: {snapshot.symbol} | 买一价: {formatNumber(topBid, priceDigits)} | 卖一价: {formatNumber(topAsk, priceDigits)} | 点差: {spreadDisplay}
|
||||
{t("offset.headerLine", {
|
||||
exchange: exchangeName,
|
||||
symbol: snapshot.symbol,
|
||||
bid: formatNumber(topBid, priceDigits),
|
||||
ask: formatNumber(topAsk, priceDigits),
|
||||
spread: spreadDisplay,
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
买10档累计: {formatNumber(snapshot.buyDepthSum10, 4)} | 卖10档累计: {formatNumber(snapshot.sellDepthSum10, 4)} | 状态: {imbalanceLabel}
|
||||
{t("offset.depthLine", {
|
||||
buy: formatNumber(snapshot.buyDepthSum10, 4),
|
||||
sell: formatNumber(snapshot.sellDepthSum10, 4),
|
||||
status: imbalanceLabel,
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
当前挂单策略: BUY {snapshot.skipBuySide ? "暂停" : "启用"} | SELL {snapshot.skipSellSide ? "暂停" : "启用"} | 按 Esc 返回策略选择
|
||||
{t("offset.strategyStatus", {
|
||||
buyStatus: snapshot.skipBuySide ? t("common.disabled") : t("common.enabled"),
|
||||
sellStatus: snapshot.skipSellSide ? t("common.disabled") : t("common.enabled"),
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">状态: {snapshot.ready ? "实时运行" : "等待市场数据"}</Text>
|
||||
<Text color="gray">{t("trend.statusLine", { status: readyStatus })}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="column" marginRight={4}>
|
||||
<Text color="greenBright">持仓</Text>
|
||||
<Text color="greenBright">{t("common.section.position")}</Text>
|
||||
{hasPosition ? (
|
||||
<>
|
||||
<Text>
|
||||
方向: {snapshot.position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(snapshot.position.positionAmt), 4)} | 开仓价: {formatNumber(snapshot.position.entryPrice, priceDigits)}
|
||||
{t("maker.positionLine", {
|
||||
direction:
|
||||
snapshot.position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
|
||||
qty: formatNumber(Math.abs(snapshot.position.positionAmt), 4),
|
||||
entry: formatNumber(snapshot.position.entryPrice, priceDigits),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.accountUnrealized, 4)} USDT
|
||||
{t("maker.pnlLine", {
|
||||
pnl: formatNumber(snapshot.pnl, 4),
|
||||
accountPnl: formatNumber(snapshot.accountUnrealized, 4),
|
||||
})}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text color="gray">当前无持仓</Text>
|
||||
<Text color="gray">{t("common.noPosition")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color="greenBright">目标挂单</Text>
|
||||
<Text color="greenBright">{t("maker.targetOrders")}</Text>
|
||||
{desiredRows.length > 0 ? (
|
||||
<DataTable columns={desiredColumns} rows={desiredRows} />
|
||||
) : (
|
||||
<Text color="gray">暂无目标挂单</Text>
|
||||
<Text color="gray">{t("maker.noTargetOrders")}</Text>
|
||||
)}
|
||||
<Text>
|
||||
累计成交量: {formatNumber(snapshot.sessionVolume, 2)} USDT
|
||||
{t("trend.volumeLine", { volume: formatNumber(snapshot.sessionVolume, 2) })}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">当前挂单</Text>
|
||||
<Text color="yellow">{t("common.section.orders")}</Text>
|
||||
{openOrderRows.length > 0 ? (
|
||||
<DataTable columns={openOrderColumns} rows={openOrderRows} />
|
||||
) : (
|
||||
<Text color="gray">暂无挂单</Text>
|
||||
<Text color="gray">{t("common.noOrders")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">最近事件</Text>
|
||||
<Text color="yellow">{t("common.section.recent")}</Text>
|
||||
{lastLogs.length > 0 ? (
|
||||
lastLogs.map((item, index) => (
|
||||
<Text key={`${item.time}-${index}`}>
|
||||
@@ -182,7 +206,7 @@ export function OffsetMakerApp({ onExit }: OffsetMakerAppProps) {
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color="gray">暂无日志</Text>
|
||||
<Text color="gray">{t("common.noLogs")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+42
-20
@@ -4,10 +4,11 @@ import { tradingConfig } from "../config";
|
||||
import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter";
|
||||
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||
import { TrendEngine, type TrendEngineSnapshot } from "../strategy/trend-engine";
|
||||
import { formatNumber } from "../utils/format";
|
||||
import { formatNumber, formatTrendLabel } from "../utils/format";
|
||||
import { DataTable, type TableColumn } from "./components/DataTable";
|
||||
import { t } from "../i18n";
|
||||
|
||||
const READY_MESSAGE = "正在等待交易所推送数据…";
|
||||
const READY_MESSAGE = t("trend.readyMessage");
|
||||
|
||||
interface TrendAppProps {
|
||||
onExit: () => void;
|
||||
@@ -56,8 +57,8 @@ export function TrendApp({ onExit }: TrendAppProps) {
|
||||
if (error) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="red">启动失败: {error.message}</Text>
|
||||
<Text color="gray">请检查环境变量和网络连通性。</Text>
|
||||
<Text color="red">{t("common.startFailed", { message: error.message })}</Text>
|
||||
<Text color="gray">{t("common.checkEnv")}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +66,7 @@ export function TrendApp({ onExit }: TrendAppProps) {
|
||||
if (!snapshot) {
|
||||
return (
|
||||
<Box padding={1}>
|
||||
<Text>正在初始化趋势策略…</Text>
|
||||
<Text>{t("common.initializing", { target: t("trend.name") })}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -96,56 +97,77 @@ export function TrendApp({ onExit }: TrendAppProps) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} paddingY={0}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="cyanBright">Trend Strategy Dashboard</Text>
|
||||
<Text color="cyanBright">{t("trend.title")}</Text>
|
||||
<Text>
|
||||
交易所: {exchangeName} | 交易对: {snapshot.symbol} | 最近价格: {formatNumber(lastPrice, 2)} | SMA30: {formatNumber(sma30, 2)} | 趋势: {trend}
|
||||
{t("trend.headerLine", {
|
||||
exchange: exchangeName,
|
||||
symbol: snapshot.symbol,
|
||||
lastPrice: formatNumber(lastPrice, 2),
|
||||
sma: formatNumber(sma30, 2),
|
||||
trend: formatTrendLabel(trend),
|
||||
})}
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
{t("trend.statusLine", { status: ready ? t("status.live") : READY_MESSAGE })}
|
||||
</Text>
|
||||
<Text color="gray">状态: {ready ? "实时运行" : READY_MESSAGE} | 按 Esc 返回策略选择</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="column" marginRight={4}>
|
||||
<Text color="greenBright">持仓</Text>
|
||||
<Text color="greenBright">{t("common.section.position")}</Text>
|
||||
{hasPosition ? (
|
||||
<>
|
||||
<Text>
|
||||
方向: {position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(position.positionAmt), 4)} | 开仓价: {formatNumber(position.entryPrice, 2)}
|
||||
{t("trend.positionLine", {
|
||||
direction: position.positionAmt > 0 ? t("common.direction.long") : t("common.direction.short"),
|
||||
qty: formatNumber(Math.abs(position.positionAmt), 4),
|
||||
entry: formatNumber(position.entryPrice, 2),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.unrealized, 4)} USDT
|
||||
{t("trend.pnlLine", {
|
||||
pnl: formatNumber(snapshot.pnl, 4),
|
||||
unrealized: formatNumber(snapshot.unrealized, 4),
|
||||
})}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text color="gray">当前无持仓</Text>
|
||||
<Text color="gray">{t("common.noPosition")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color="greenBright">绩效</Text>
|
||||
<Text color="greenBright">{t("common.section.performance")}</Text>
|
||||
<Text>
|
||||
累计交易次数: {snapshot.totalTrades} | 累计收益: {formatNumber(snapshot.totalProfit, 4)} USDT
|
||||
{t("trend.performanceLine", {
|
||||
trades: snapshot.totalTrades,
|
||||
profit: formatNumber(snapshot.totalProfit, 4),
|
||||
})}
|
||||
</Text>
|
||||
<Text>
|
||||
累计成交量: {formatNumber(sessionVolume, 2)} USDT
|
||||
{t("trend.volumeLine", { volume: formatNumber(sessionVolume, 2) })}
|
||||
</Text>
|
||||
{snapshot.lastOpenSignal.side ? (
|
||||
<Text color="gray">
|
||||
最近开仓信号: {snapshot.lastOpenSignal.side} @ {formatNumber(snapshot.lastOpenSignal.price, 2)}
|
||||
{t("trend.lastSignal", {
|
||||
side: snapshot.lastOpenSignal.side,
|
||||
price: formatNumber(snapshot.lastOpenSignal.price, 2),
|
||||
})}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">当前挂单</Text>
|
||||
<Text color="yellow">{t("common.section.orders")}</Text>
|
||||
{orderRows.length > 0 ? (
|
||||
<DataTable columns={orderColumns} rows={orderRows} />
|
||||
) : (
|
||||
<Text color="gray">暂无挂单</Text>
|
||||
<Text color="gray">{t("common.noOrders")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text color="yellow">最近交易与事件</Text>
|
||||
<Text color="yellow">{t("common.section.recentTrades")}</Text>
|
||||
{lastLogs.length > 0 ? (
|
||||
lastLogs.map((item, index) => (
|
||||
<Text key={`${item.time}-${index}`}>
|
||||
@@ -153,7 +175,7 @@ export function TrendApp({ onExit }: TrendAppProps) {
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text color="gray">暂无日志</Text>
|
||||
<Text color="gray">{t("common.noLogs")}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+10
-4
@@ -1,8 +1,14 @@
|
||||
import { t } from "../i18n";
|
||||
|
||||
export type TrendLabel = "做多" | "做空" | "无信号";
|
||||
|
||||
export function formatTrendLabel(trend: TrendLabel): string {
|
||||
if (trend === "做多") return t("trend.label.long");
|
||||
if (trend === "做空") return t("trend.label.short");
|
||||
return t("trend.label.none");
|
||||
}
|
||||
|
||||
export function formatNumber(value: number | null | undefined, digits = 4, fallback = "-"): string {
|
||||
if (value == null || Number.isNaN(value)) return fallback;
|
||||
return Number(value).toFixed(digits);
|
||||
}
|
||||
|
||||
export function formatTrendLabel(trend: "做多" | "做空" | "无信号"): string {
|
||||
return trend;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user