import React, { useEffect, useRef, useState } from "react"; import { Box, Text, useInput } from "ink"; import { tradingConfig } from "../config"; import { AsterExchangeAdapter } from "../exchanges/aster-adapter"; import { TrendEngine, type TrendEngineSnapshot } from "../core/trend-engine"; import { formatNumber } from "../utils/format"; import { DataTable, type TableColumn } from "./components/DataTable"; const READY_MESSAGE = "正在等待交易所推送数据…"; interface TrendAppProps { onExit: () => void; } const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); export function TrendApp({ onExit }: TrendAppProps) { const [snapshot, setSnapshot] = useState(null); const [error, setError] = useState(null); const engineRef = useRef(null); useInput( (input, key) => { if (key.escape) { engineRef.current?.stop(); onExit(); } }, { isActive: inputSupported } ); useEffect(() => { const apiKey = process.env.ASTER_API_KEY; const apiSecret = process.env.ASTER_API_SECRET; if (!apiKey || !apiSecret) { setError(new Error("缺少 ASTER_API_KEY 或 ASTER_API_SECRET 环境变量")); return; } try { const adapter = new AsterExchangeAdapter({ apiKey, apiSecret, symbol: tradingConfig.symbol, }); const engine = new TrendEngine(tradingConfig, adapter); engineRef.current = engine; setSnapshot(engine.getSnapshot()); const handler = (next: TrendEngineSnapshot) => { setSnapshot({ ...next, tradeLog: [...next.tradeLog] }); }; engine.on("update", handler); engine.start(); return () => { engine.off("update", handler); engine.stop(); }; } catch (err) { console.error(err); setError(err instanceof Error ? err : new Error(String(err))); } }, []); if (error) { return ( 启动失败: {error.message} 请检查环境变量和网络连通性。 ); } if (!snapshot) { return ( 正在初始化趋势策略… ); } const { position, tradeLog, openOrders, trend, ready, lastPrice, sma30, sessionVolume } = snapshot; const hasPosition = Math.abs(position.positionAmt) > 1e-5; const lastLogs = tradeLog.slice(-5); const sortedOrders = [...openOrders].sort((a, b) => (Number(b.updateTime ?? 0) - Number(a.updateTime ?? 0)) || Number(b.orderId) - Number(a.orderId)); const orderRows = sortedOrders.slice(0, 8).map((order) => ({ id: order.orderId, side: order.side, type: order.type, price: order.price, qty: order.origQty, filled: order.executedQty, status: order.status, })); const orderColumns: TableColumn[] = [ { key: "id", header: "ID", align: "right", minWidth: 6 }, { key: "side", header: "Side", minWidth: 4 }, { key: "type", header: "Type", minWidth: 10 }, { key: "price", header: "Price", align: "right", minWidth: 10 }, { key: "qty", header: "Qty", align: "right", minWidth: 8 }, { key: "filled", header: "Filled", align: "right", minWidth: 8 }, { key: "status", header: "Status", minWidth: 10 }, ]; return ( Trend Strategy Dashboard 交易对: {snapshot.symbol} | 最近价格: {formatNumber(lastPrice, 2)} | SMA30: {formatNumber(sma30, 2)} | 趋势: {trend} 状态: {ready ? "实时运行" : READY_MESSAGE} | 按 Esc 返回策略选择 持仓 {hasPosition ? ( <> 方向: {position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(position.positionAmt), 4)} | 开仓价: {formatNumber(position.entryPrice, 2)} 浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.unrealized, 4)} USDT ) : ( 当前无持仓 )} 绩效 累计交易次数: {snapshot.totalTrades} | 累计收益: {formatNumber(snapshot.totalProfit, 4)} USDT 累计成交量: {formatNumber(sessionVolume, 2)} USDT {snapshot.lastOpenSignal.side ? ( 最近开仓信号: {snapshot.lastOpenSignal.side} @ {formatNumber(snapshot.lastOpenSignal.price, 2)} ) : null} 当前挂单 {orderRows.length > 0 ? ( ) : ( 暂无挂单 )} 最近交易与事件 {lastLogs.length > 0 ? ( lastLogs.map((item, index) => ( [{item.time}] [{item.type}] {item.detail} )) ) : ( 暂无日志 )} ); }