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