mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dcf13481d | ||
|
|
3fd0f715a1 | ||
|
|
85e7f245c0 | ||
|
|
03df1006cc | ||
|
|
3099cb1319 |
@@ -1,3 +1,6 @@
|
||||
# UI language (zh | en)
|
||||
LANG=zh
|
||||
|
||||
# Exchange selection
|
||||
EXCHANGE=aster # Pick aster (default) or grvt/lighter/backpack/paradex
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# ritmex-bot
|
||||
|
||||
> For English users, please see [README_en.md](README_en.md).
|
||||
|
||||
Please set `LANG=en` in `.env` for English interface.
|
||||
|
||||
A Bun-powered multi-exchange perpetuals workstation that ships an SMA30 trend engine, a Guardian stop sentinel, and two market-making modes. It offers instant restarts, realtime market data, structured logging, and an Ink-based CLI dashboard.
|
||||
|
||||
基于 Bun 的多交易所永续合约量化终端,内置趋势跟随(SMA30)、Guardian 防守与做市策略,支持快速恢复、实时行情订阅、日志追踪与 CLI 仪表盘。
|
||||
|
||||
如果您希望获取优惠并支持本项目,请考虑使用以下注册链接:
|
||||
@@ -14,7 +20,6 @@
|
||||
* [Apex 手续费优惠注册链接](https://join.omni.apex.exchange/SEA)
|
||||
|
||||
## 文档索引
|
||||
- [English README](README_en.md)
|
||||
- [简明上手指南(零基础)](simple-readme.md)
|
||||
- [基础网格策略使用教程](grid-trading.md)
|
||||
|
||||
|
||||
@@ -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