import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text, useInput } from "ink"; import { makerConfig } from "../config"; import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter"; 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"; interface MakerAppProps { onExit: () => void; } const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); export function MakerApp({ onExit }: MakerAppProps) { const [snapshot, setSnapshot] = useState(null); const [error, setError] = useState(null); const engineRef = useRef(null); const exchangeId = useMemo(() => resolveExchangeId(), []); const exchangeName = useMemo(() => getExchangeDisplayName(exchangeId), [exchangeId]); useInput( (input, key) => { if (key.escape) { engineRef.current?.stop(); onExit(); } }, { isActive: inputSupported } ); useEffect(() => { try { const adapter = buildAdapterFromEnv({ exchangeId, symbol: makerConfig.symbol }); const engine = new MakerEngine(makerConfig, adapter); engineRef.current = engine; setSnapshot(engine.getSnapshot()); const handler = (next: MakerEngineSnapshot) => { 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))); } }, [exchangeId]); if (error) { return ( 启动失败: {error.message} 请检查环境变量和网络连通性。 ); } if (!snapshot) { return ( 正在初始化做市策略… ); } const topBid = snapshot.topBid; const topAsk = snapshot.topAsk; const spreadDisplay = snapshot.spread != null ? `${snapshot.spread.toFixed(4)} USDT` : "-"; const hasPosition = Math.abs(snapshot.position.positionAmt) > 1e-5; const sortedOrders = [...snapshot.openOrders].sort((a, b) => (Number(b.updateTime ?? 0) - Number(a.updateTime ?? 0)) || Number(b.orderId) - Number(a.orderId)); const openOrderRows = sortedOrders.slice(0, 8).map((order) => ({ id: order.orderId, side: order.side, price: order.price, qty: order.origQty, filled: order.executedQty, reduceOnly: order.reduceOnly ? "yes" : "no", status: order.status, })); const openOrderColumns: TableColumn[] = [ { key: "id", header: "ID", align: "right", minWidth: 6 }, { key: "side", header: "Side", minWidth: 4 }, { 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: "reduceOnly", header: "RO", minWidth: 4 }, { key: "status", header: "Status", minWidth: 10 }, ]; const desiredRows = snapshot.desiredOrders.map((order, index) => ({ index: index + 1, side: order.side, price: order.price, amount: order.amount, reduceOnly: order.reduceOnly ? "yes" : "no", })); const desiredColumns: TableColumn[] = [ { key: "index", header: "#", align: "right", minWidth: 2 }, { key: "side", header: "Side", minWidth: 4 }, { key: "price", header: "Price", align: "right", minWidth: 10 }, { key: "amount", header: "Qty", align: "right", minWidth: 8 }, { key: "reduceOnly", header: "RO", minWidth: 4 }, ]; 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" }, ]; return ( Maker Strategy Dashboard 交易所: {exchangeName} | 交易对: {snapshot.symbol} | 买一价: {formatNumber(topBid, 2)} | 卖一价: {formatNumber(topAsk, 2)} | 点差: {spreadDisplay} 状态: {snapshot.ready ? "实时运行" : "等待市场数据"} | 按 Esc 返回策略选择 数据状态: {feedEntries.map((entry, index) => ( {index === 0 ? " " : " "} {entry.label} ))} 持仓 {hasPosition ? ( <> 方向: {snapshot.position.positionAmt > 0 ? "多" : "空"} | 数量: {formatNumber(Math.abs(snapshot.position.positionAmt), 4)} | 开仓价: {formatNumber(snapshot.position.entryPrice, 2)} 浮动盈亏: {formatNumber(snapshot.pnl, 4)} USDT | 账户未实现盈亏: {formatNumber(snapshot.accountUnrealized, 4)} USDT ) : ( 当前无持仓 )} 目标挂单 {desiredRows.length > 0 ? ( ) : ( 暂无目标挂单 )} 累计成交量: {formatNumber(snapshot.sessionVolume, 2)} USDT 当前挂单 {openOrderRows.length > 0 ? ( ) : ( 暂无挂单 )} 最近事件 {lastLogs.length > 0 ? ( lastLogs.map((item, index) => ( [{item.time}] [{item.type}] {item.detail} )) ) : ( 暂无日志 )} ); }