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

This commit is contained in:
discountry
2025-12-09 00:35:28 +08:00
parent 03df1006cc
commit 85e7f245c0
17 changed files with 1166 additions and 295 deletions
+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>