feat: 添加期现套利策略支持,更新相关配置和界面,增强 Aster 现货 API 客户端功能

This commit is contained in:
discountry
2025-10-07 00:23:24 +08:00
parent 1bd3e7edc6
commit d7a95ceb36
11 changed files with 3971 additions and 39 deletions
+134
View File
@@ -0,0 +1,134 @@
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<BasisArbSnapshot | null>(null);
const [error, setError] = useState<Error | null>(null);
const engineRef = useRef<BasisArbEngine | null>(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 (
<Box flexDirection="column" padding={1}>
<Text color="red">: {error.message}</Text>
<Text color="gray"> Esc </Text>
</Box>
);
}
if (!snapshot) {
return (
<Box padding={1}>
<Text></Text>
</Box>
);
}
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 feedStatus = snapshot.feedStatus;
const lastLogs = snapshot.tradeLog.slice(-5);
return (
<Box flexDirection="column" paddingX={1}>
<Box flexDirection="column" marginBottom={1}>
<Text color="cyanBright">Basis Arbitrage Dashboard</Text>
<Text>
: {exchangeName} : {snapshot.futuresSymbol} : {snapshot.spotSymbol}
</Text>
<Text color="gray"> Esc 数据状态: 期货({feedStatus.futures ? "OK" : "--"}) ({feedStatus.spot ? "OK" : "--"})</Text>
<Text color="gray">: {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>
</Box>
<Box flexDirection="column">
<Text color="greenBright"></Text>
<Text>: {spotBid} : {spotAsk}</Text>
<Text color="gray">: {spotUpdated}</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 ? "green" : "red"}>
taker ({(basisConfig.takerFeeRate * 100).toFixed(4)}% × ): {netSpread} USDT {netSpreadBps} bp
</Text>
</Box>
<Box flexDirection="column">
<Text color="yellow"></Text>
{lastLogs.length ? (
lastLogs.map((entry, index) => (
<Text key={`${entry.time}-${index}`}>
[{entry.time}] [{entry.type}] {entry.detail}
</Text>
))
) : (
<Text color="gray"></Text>
)}
</Box>
</Box>
);
}