mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
feat: 添加资金费率获取功能至基础套利引擎,更新UI以显示资金费率信息
This commit is contained in:
@@ -827,6 +827,36 @@ export class AsterRestClient {
|
||||
}
|
||||
}
|
||||
|
||||
async getPremiumIndex(symbol: string): Promise<{
|
||||
symbol: string;
|
||||
markPrice?: string;
|
||||
indexPrice?: string;
|
||||
lastFundingRate?: string;
|
||||
fundingRate?: string;
|
||||
nextFundingTime?: number;
|
||||
time?: number;
|
||||
}> {
|
||||
const upper = symbol.toUpperCase();
|
||||
const url = `${FUTURES_REST_BASE}/fapi/v1/premiumIndex?symbol=${encodeURIComponent(upper)}`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url);
|
||||
} catch (error) {
|
||||
throw new Error(`[AsterRestClient] 获取资金费率失败 ${String(error)}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} ${text}`);
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(text) as any;
|
||||
// The response shape mirrors Binance: { symbol, markPrice, indexPrice, lastFundingRate, nextFundingTime, time }
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throw new Error(`[AsterRestClient] 无法解析资金费率响应: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getListenKey(): Promise<string> {
|
||||
const response = await this.signedRequest<ListenKeyResponse>({ path: "/fapi/v1/listenKey", method: "POST", params: {} });
|
||||
return response.listenKey;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BasisArbConfig } from "../config";
|
||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||
import type { AsterDepth, AsterSpotBookTicker } from "../exchanges/types";
|
||||
import { AsterSpotRestClient } from "../exchanges/aster/client";
|
||||
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/client";
|
||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||
@@ -16,6 +16,9 @@ export interface BasisArbSnapshot {
|
||||
spotAsk: number | null;
|
||||
futuresLastUpdate: number | null;
|
||||
spotLastUpdate: number | null;
|
||||
fundingRate: number | null;
|
||||
nextFundingTime: number | null;
|
||||
fundingLastUpdate: number | null;
|
||||
spread: number | null;
|
||||
spreadBps: number | null;
|
||||
netSpread: number | null;
|
||||
@@ -25,6 +28,7 @@ export interface BasisArbSnapshot {
|
||||
feedStatus: {
|
||||
futures: boolean;
|
||||
spot: boolean;
|
||||
funding: boolean;
|
||||
};
|
||||
opportunity: boolean;
|
||||
}
|
||||
@@ -34,6 +38,7 @@ type BasisArbListener = (snapshot: BasisArbSnapshot) => void;
|
||||
|
||||
interface BasisArbDependencies {
|
||||
spotClient?: Pick<AsterSpotRestClient, "getBookTicker">;
|
||||
futuresClient?: Pick<AsterRestClient, "getPremiumIndex">;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
@@ -49,27 +54,37 @@ interface SpotState {
|
||||
updatedAt: number | null;
|
||||
}
|
||||
|
||||
interface FundingState {
|
||||
rate: number | null;
|
||||
nextFundingTime: number | null;
|
||||
updatedAt: number | null;
|
||||
}
|
||||
|
||||
export class BasisArbEngine {
|
||||
private readonly events = new StrategyEventEmitter<BasisArbEvent, BasisArbSnapshot>();
|
||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||
private readonly spotClient: Pick<AsterSpotRestClient, "getBookTicker">;
|
||||
private readonly futuresClient: Pick<AsterRestClient, "getPremiumIndex">;
|
||||
private readonly now: () => number;
|
||||
private readonly config: BasisArbConfig;
|
||||
private readonly exchange: ExchangeAdapter;
|
||||
|
||||
private readonly futures: DepthState = { bid: null, ask: null, updatedAt: null };
|
||||
private readonly spot: SpotState = { bid: null, ask: null, updatedAt: null };
|
||||
private readonly funding: FundingState = { rate: null, nextFundingTime: null, updatedAt: null };
|
||||
|
||||
private readonly feedReady = { futures: false, spot: false };
|
||||
private readonly feedReady = { futures: false, spot: false, funding: false };
|
||||
|
||||
private timer: ReturnType<typeof setInterval> | null = null;
|
||||
private spotInFlight = false;
|
||||
private fundingInFlight = false;
|
||||
private stopped = false;
|
||||
|
||||
constructor(config: BasisArbConfig, exchange: ExchangeAdapter, deps: BasisArbDependencies = {}) {
|
||||
this.config = config;
|
||||
this.exchange = exchange;
|
||||
this.spotClient = deps.spotClient ?? new AsterSpotRestClient();
|
||||
this.futuresClient = deps.futuresClient ?? new AsterRestClient();
|
||||
this.now = deps.now ?? (() => Date.now());
|
||||
this.tradeLog = createTradeLog(this.config.maxLogEntries);
|
||||
this.bootstrap();
|
||||
@@ -79,8 +94,10 @@ export class BasisArbEngine {
|
||||
if (this.timer) return;
|
||||
this.timer = setInterval(() => {
|
||||
void this.pollSpot();
|
||||
void this.pollFunding();
|
||||
}, Math.max(this.config.refreshIntervalMs, 200));
|
||||
void this.pollSpot();
|
||||
void this.pollFunding();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
@@ -154,6 +171,32 @@ export class BasisArbEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private async pollFunding(): Promise<void> {
|
||||
if (this.fundingInFlight || this.stopped) return;
|
||||
this.fundingInFlight = true;
|
||||
try {
|
||||
const data = await this.futuresClient.getPremiumIndex(this.config.futuresSymbol);
|
||||
const rateRaw = (data.lastFundingRate ?? data.fundingRate) as string | undefined;
|
||||
const rate = rateRaw !== undefined ? Number(rateRaw) : NaN;
|
||||
const ts = (data.time ?? data.nextFundingTime ?? this.now()) as number | undefined;
|
||||
if (Number.isFinite(rate)) {
|
||||
this.funding.rate = Number(rateRaw);
|
||||
this.funding.nextFundingTime = typeof data.nextFundingTime === "number" ? data.nextFundingTime : null;
|
||||
this.funding.updatedAt = typeof ts === "number" ? ts : this.now();
|
||||
if (!this.feedReady.funding) {
|
||||
this.feedReady.funding = true;
|
||||
this.tradeLog.push("info", `资金费率已就绪 (${this.config.futuresSymbol})`);
|
||||
}
|
||||
this.emitUpdate();
|
||||
}
|
||||
} catch (error) {
|
||||
this.feedReady.funding = false;
|
||||
this.tradeLog.push("error", `获取资金费率失败: ${String(error instanceof Error ? error.message : error)}`);
|
||||
} finally {
|
||||
this.fundingInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
private applySpotTicker(ticker: AsterSpotBookTicker): void {
|
||||
const bid = Number(ticker.bidPrice);
|
||||
const ask = Number(ticker.askPrice);
|
||||
@@ -181,6 +224,8 @@ export class BasisArbEngine {
|
||||
const futuresAsk = this.futures.ask;
|
||||
const spotBid = this.spot.bid;
|
||||
const spotAsk = this.spot.ask;
|
||||
const fundingRate = this.funding.rate;
|
||||
const nextFundingTime = this.funding.nextFundingTime;
|
||||
const spread = this.computeSpread(futuresBid, spotAsk);
|
||||
const spreadBps = this.computeSpreadBps(spread, spotAsk);
|
||||
const netSpread = this.computeNetSpread(futuresBid, spotAsk);
|
||||
@@ -188,7 +233,8 @@ export class BasisArbEngine {
|
||||
const opportunity = netSpread != null && netSpread >= 0;
|
||||
const lastUpdated = Math.max(
|
||||
futuresBid != null && this.futures.updatedAt ? this.futures.updatedAt : 0,
|
||||
spotBid != null && this.spot.updatedAt ? this.spot.updatedAt : 0
|
||||
spotBid != null && this.spot.updatedAt ? this.spot.updatedAt : 0,
|
||||
fundingRate != null && this.funding.updatedAt ? this.funding.updatedAt : 0
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -201,6 +247,9 @@ export class BasisArbEngine {
|
||||
spotAsk,
|
||||
futuresLastUpdate: this.futures.updatedAt,
|
||||
spotLastUpdate: this.spot.updatedAt,
|
||||
fundingRate,
|
||||
nextFundingTime,
|
||||
fundingLastUpdate: this.funding.updatedAt,
|
||||
spread,
|
||||
spreadBps,
|
||||
netSpread,
|
||||
|
||||
+10
-1
@@ -82,6 +82,9 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
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 feedStatus = snapshot.feedStatus;
|
||||
const lastLogs = snapshot.tradeLog.slice(-5);
|
||||
|
||||
@@ -92,7 +95,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
<Text>
|
||||
交易所: {exchangeName} | 期货合约: {snapshot.futuresSymbol} | 现货交易对: {snapshot.spotSymbol}
|
||||
</Text>
|
||||
<Text color="gray">按 Esc 返回策略选择 | 数据状态: 期货({feedStatus.futures ? "OK" : "--"}) 现货({feedStatus.spot ? "OK" : "--"})</Text>
|
||||
<Text color="gray">按 Esc 返回策略选择 | 数据状态: 期货({feedStatus.futures ? "OK" : "--"}) 现货({feedStatus.spot ? "OK" : "--"}) 资金费率({feedStatus.funding ? "OK" : "--"})</Text>
|
||||
<Text color="gray">最近更新时间: {lastUpdated}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -109,6 +112,12 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="yellow">资金费率</Text>
|
||||
<Text>当前资金费率: {fundingRatePct}</Text>
|
||||
<Text color="gray">资金费率更新时间: {fundingUpdated} | 下次结算时间: {nextFundingTime}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={snapshot.opportunity ? "greenBright" : "redBright"}>套利差价(卖期货 / 买现货)</Text>
|
||||
<Text color={snapshot.opportunity ? "green" : undefined}>毛价差: {spread} USDT | {spreadBps} bp</Text>
|
||||
|
||||
Reference in New Issue
Block a user