import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text, useInput } from "ink"; import { basisConfig } from "../config"; import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-adapter"; import { buildAdapterFromEnv } from "../exchanges/resolve-from-env"; import { BasisArbEngine, type BasisArbSnapshot } from "../strategy/basis-arb-engine"; import { formatNumber } from "../utils/format"; interface BasisAppProps { onExit: () => void; } const inputSupported = Boolean(process.stdin && (process.stdin as any).isTTY); export function BasisApp({ onExit }: BasisAppProps) { 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(() => { if (exchangeId !== "aster") { setError(new Error("期现套利策略目前仅支持 Aster 交易所。请设置 EXCHANGE=aster 后重试。")); return; } try { const adapter = buildAdapterFromEnv({ exchangeId, symbol: basisConfig.futuresSymbol }); const engine = new BasisArbEngine(basisConfig, adapter); engineRef.current = engine; setSnapshot(engine.getSnapshot()); const handler = (next: BasisArbSnapshot) => { 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} 按 Esc 返回菜单。 ); } if (!snapshot) { return ( 正在初始化期现套利监控… ); } const futuresBid = formatNumber(snapshot.futuresBid, 4); const futuresAsk = formatNumber(snapshot.futuresAsk, 4); const spotBid = formatNumber(snapshot.spotBid, 4); const spotAsk = formatNumber(snapshot.spotAsk, 4); const spread = formatNumber(snapshot.spread, 4); const spreadBps = formatNumber(snapshot.spreadBps, 2); const netSpread = formatNumber(snapshot.netSpread, 4); const netSpreadBps = formatNumber(snapshot.netSpreadBps, 2); const lastUpdated = snapshot.lastUpdated ? new Date(snapshot.lastUpdated).toLocaleTimeString() : "-"; const futuresUpdated = snapshot.futuresLastUpdate ? new Date(snapshot.futuresLastUpdate).toLocaleTimeString() : "-"; const spotUpdated = snapshot.spotLastUpdate ? new Date(snapshot.spotLastUpdate).toLocaleTimeString() : "-"; 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 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); const futuresBalances = (snapshot.futuresBalances ?? []).filter((b) => Math.abs(b.wallet) > 0); return ( Basis Arbitrage Dashboard 交易所: {exchangeName} | 期货合约: {snapshot.futuresSymbol} | 现货交易对: {snapshot.spotSymbol} 按 Esc 返回策略选择 | 数据状态: 期货({feedStatus.futures ? "OK" : "--"}) 现货({feedStatus.spot ? "OK" : "--"}) 资金费率({feedStatus.funding ? "OK" : "--"}) 最近更新时间: {lastUpdated} 期货盘口 买一: {futuresBid} | 卖一: {futuresAsk} 更新时间: {futuresUpdated} 现货盘口 买一: {spotBid} | 卖一: {spotAsk} 更新时间: {spotUpdated} 资金费率 当前资金费率: {fundingRatePct} 资金费率更新时间: {fundingUpdated} | 下次结算时间: {nextFundingTime} 单次资金费率收益(估): {fundingIncomePerFunding} | 日收益(估): {fundingIncomePerDay} 双边吃单手续费(估): {takerFeesPerRoundTrip} | 回本所需资金费率次数: {fundingCountToBreakeven} 现货账户余额(非0) {spotBalances.length ? ( spotBalances.map((b) => ( {b.asset}: 可用 {formatNumber(b.free, 8)} | 冻结 {formatNumber(b.locked, 8)} )) ) : ( )} 合约账户余额(非0) {futuresBalances.length ? ( futuresBalances.map((b) => ( {b.asset}: 钱包 {formatNumber(b.wallet, 8)} | 可用 {formatNumber(b.available, 8)} )) ) : ( )} 套利差价(卖期货 / 买现货) 毛价差: {spread} USDT | {spreadBps} bp 扣除 taker 手续费 ({(basisConfig.takerFeeRate * 100).toFixed(4)}% × 双边): {netSpread} USDT | {netSpreadBps} bp 最近事件 {lastLogs.length ? ( lastLogs.map((entry, index) => { const color = entry.type === "entry" ? "green" : entry.type === "exit" ? "red" : undefined; return ( [{entry.time}] [{entry.type}] {entry.detail} ); }) ) : ( 暂无日志 )} ); }