From d778e20c23402865846a58e85ccc88af03d71d65 Mon Sep 17 00:00:00 2001 From: discountry Date: Tue, 7 Oct 2025 16:06:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=8E=B0=E8=B4=A7?= =?UTF-8?q?=E5=92=8C=E5=90=88=E7=BA=A6=E8=B4=A6=E6=88=B7=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E5=8A=9F=E8=83=BD=E8=87=B3=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E5=A5=97=E5=88=A9=E5=BC=95=E6=93=8E=EF=BC=8C=E6=9B=B4=E6=96=B0?= =?UTF-8?q?UI=E4=BB=A5=E6=98=BE=E7=A4=BA=E8=B4=A6=E6=88=B7=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/strategy/basis-arb-engine.ts | 79 ++++++++++++++++++++++++++++++++ src/ui/BasisApp.tsx | 29 ++++++++++++ 2 files changed, 108 insertions(+) diff --git a/src/strategy/basis-arb-engine.ts b/src/strategy/basis-arb-engine.ts index e523db0..32be520 100644 --- a/src/strategy/basis-arb-engine.ts +++ b/src/strategy/basis-arb-engine.ts @@ -30,6 +30,8 @@ export interface BasisArbSnapshot { spot: boolean; funding: boolean; }; + spotBalances: Array<{ asset: string; free: number; locked: number }>; + futuresBalances: Array<{ asset: string; wallet: number; available: number }>; opportunity: boolean; } @@ -60,6 +62,18 @@ interface FundingState { updatedAt: number | null; } +interface SpotBalanceStateEntry { + asset: string; + free: number; + locked: number; +} + +interface FuturesBalanceStateEntry { + asset: string; + wallet: number; + available: number; +} + export class BasisArbEngine { private readonly events = new StrategyEventEmitter(); private readonly tradeLog: ReturnType; @@ -72,12 +86,16 @@ export class BasisArbEngine { 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 spotBalances: SpotBalanceStateEntry[] = []; + private futuresBalances: FuturesBalanceStateEntry[] = []; private readonly feedReady = { futures: false, spot: false, funding: false }; private timer: ReturnType | null = null; private spotInFlight = false; private fundingInFlight = false; + private spotAccountInFlight = false; + private futuresAccountInFlight = false; private stopped = false; constructor(config: BasisArbConfig, exchange: ExchangeAdapter, deps: BasisArbDependencies = {}) { @@ -95,9 +113,13 @@ export class BasisArbEngine { this.timer = setInterval(() => { void this.pollSpot(); void this.pollFunding(); + void this.pollSpotAccount(); + void this.pollFuturesAccount(); }, Math.max(this.config.refreshIntervalMs, 200)); void this.pollSpot(); void this.pollFunding(); + void this.pollSpotAccount(); + void this.pollFuturesAccount(); } stop(): void { @@ -197,6 +219,61 @@ export class BasisArbEngine { } } + private async pollSpotAccount(): Promise { + if (this.spotAccountInFlight || this.stopped) return; + this.spotAccountInFlight = true; + try { + // Spot balances via spot REST + const account: any = await (this.spotClient as any).getAccount?.(); + const balances = Array.isArray(account?.balances) ? account.balances : []; + const next: SpotBalanceStateEntry[] = []; + for (const b of balances) { + const asset = String(b.asset ?? ""); + const free = Number(b.free ?? 0); + const locked = Number(b.locked ?? 0); + if (!asset) continue; + if (Math.abs(free) > 0 || Math.abs(locked) > 0) { + next.push({ asset, free, locked }); + } + } + next.sort((a, b) => a.asset.localeCompare(b.asset)); + this.spotBalances = next; + this.emitUpdate(); + } catch (error) { + this.tradeLog.push("error", `获取现货余额失败: ${String(error instanceof Error ? error.message : error)}`); + } finally { + this.spotAccountInFlight = false; + } + } + + private async pollFuturesAccount(): Promise { + if (this.futuresAccountInFlight || this.stopped) return; + this.futuresAccountInFlight = true; + try { + // Futures balances via futures REST + const rest = new AsterRestClient(); + const account: any = await rest.getAccount(); + const assets = Array.isArray(account?.assets) ? account.assets : []; + const next: FuturesBalanceStateEntry[] = []; + for (const a of assets) { + const asset = String(a.asset ?? ""); + const wallet = Number(a.walletBalance ?? a.wb ?? 0); + const available = Number(a.availableBalance ?? a.bc ?? 0); + if (!asset) continue; + if (Math.abs(wallet) > 0 || Math.abs(available) > 0) { + next.push({ asset, wallet, available }); + } + } + next.sort((a, b) => a.asset.localeCompare(b.asset)); + this.futuresBalances = next; + this.emitUpdate(); + } catch (error) { + this.tradeLog.push("error", `获取合约余额失败: ${String(error instanceof Error ? error.message : error)}`); + } finally { + this.futuresAccountInFlight = false; + } + } + private applySpotTicker(ticker: AsterSpotBookTicker): void { const bid = Number(ticker.bidPrice); const ask = Number(ticker.askPrice); @@ -257,6 +334,8 @@ export class BasisArbEngine { lastUpdated: lastUpdated > 0 ? lastUpdated : null, tradeLog: this.tradeLog.all(), feedStatus: { ...this.feedReady }, + spotBalances: [...this.spotBalances], + futuresBalances: [...this.futuresBalances], opportunity, }; } diff --git a/src/ui/BasisApp.tsx b/src/ui/BasisApp.tsx index c5bd41e..bf23425 100644 --- a/src/ui/BasisApp.tsx +++ b/src/ui/BasisApp.tsx @@ -87,6 +87,8 @@ export function BasisApp({ onExit }: BasisAppProps) { const nextFundingTime = snapshot.nextFundingTime ? new Date(snapshot.nextFundingTime).toLocaleTimeString() : "-"; 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 ( @@ -118,6 +120,33 @@ export function BasisApp({ onExit }: BasisAppProps) { 资金费率更新时间: {fundingUpdated} | 下次结算时间: {nextFundingTime} + + + 现货账户余额(非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