mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
feat: 添加现货和合约账户余额获取功能至基础套利引擎,更新UI以显示账户余额信息
This commit is contained in:
@@ -30,6 +30,8 @@ export interface BasisArbSnapshot {
|
|||||||
spot: boolean;
|
spot: boolean;
|
||||||
funding: boolean;
|
funding: boolean;
|
||||||
};
|
};
|
||||||
|
spotBalances: Array<{ asset: string; free: number; locked: number }>;
|
||||||
|
futuresBalances: Array<{ asset: string; wallet: number; available: number }>;
|
||||||
opportunity: boolean;
|
opportunity: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +62,18 @@ interface FundingState {
|
|||||||
updatedAt: number | null;
|
updatedAt: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SpotBalanceStateEntry {
|
||||||
|
asset: string;
|
||||||
|
free: number;
|
||||||
|
locked: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FuturesBalanceStateEntry {
|
||||||
|
asset: string;
|
||||||
|
wallet: number;
|
||||||
|
available: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class BasisArbEngine {
|
export class BasisArbEngine {
|
||||||
private readonly events = new StrategyEventEmitter<BasisArbEvent, BasisArbSnapshot>();
|
private readonly events = new StrategyEventEmitter<BasisArbEvent, BasisArbSnapshot>();
|
||||||
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
private readonly tradeLog: ReturnType<typeof createTradeLog>;
|
||||||
@@ -72,12 +86,16 @@ export class BasisArbEngine {
|
|||||||
private readonly futures: DepthState = { bid: null, ask: null, updatedAt: null };
|
private readonly futures: DepthState = { bid: null, ask: null, updatedAt: null };
|
||||||
private readonly spot: SpotState = { 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 funding: FundingState = { rate: null, nextFundingTime: null, updatedAt: null };
|
||||||
|
private spotBalances: SpotBalanceStateEntry[] = [];
|
||||||
|
private futuresBalances: FuturesBalanceStateEntry[] = [];
|
||||||
|
|
||||||
private readonly feedReady = { futures: false, spot: false, funding: false };
|
private readonly feedReady = { futures: false, spot: false, funding: false };
|
||||||
|
|
||||||
private timer: ReturnType<typeof setInterval> | null = null;
|
private timer: ReturnType<typeof setInterval> | null = null;
|
||||||
private spotInFlight = false;
|
private spotInFlight = false;
|
||||||
private fundingInFlight = false;
|
private fundingInFlight = false;
|
||||||
|
private spotAccountInFlight = false;
|
||||||
|
private futuresAccountInFlight = false;
|
||||||
private stopped = false;
|
private stopped = false;
|
||||||
|
|
||||||
constructor(config: BasisArbConfig, exchange: ExchangeAdapter, deps: BasisArbDependencies = {}) {
|
constructor(config: BasisArbConfig, exchange: ExchangeAdapter, deps: BasisArbDependencies = {}) {
|
||||||
@@ -95,9 +113,13 @@ export class BasisArbEngine {
|
|||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
void this.pollSpot();
|
void this.pollSpot();
|
||||||
void this.pollFunding();
|
void this.pollFunding();
|
||||||
|
void this.pollSpotAccount();
|
||||||
|
void this.pollFuturesAccount();
|
||||||
}, Math.max(this.config.refreshIntervalMs, 200));
|
}, Math.max(this.config.refreshIntervalMs, 200));
|
||||||
void this.pollSpot();
|
void this.pollSpot();
|
||||||
void this.pollFunding();
|
void this.pollFunding();
|
||||||
|
void this.pollSpotAccount();
|
||||||
|
void this.pollFuturesAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
@@ -197,6 +219,61 @@ export class BasisArbEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async pollSpotAccount(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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 {
|
private applySpotTicker(ticker: AsterSpotBookTicker): void {
|
||||||
const bid = Number(ticker.bidPrice);
|
const bid = Number(ticker.bidPrice);
|
||||||
const ask = Number(ticker.askPrice);
|
const ask = Number(ticker.askPrice);
|
||||||
@@ -257,6 +334,8 @@ export class BasisArbEngine {
|
|||||||
lastUpdated: lastUpdated > 0 ? lastUpdated : null,
|
lastUpdated: lastUpdated > 0 ? lastUpdated : null,
|
||||||
tradeLog: this.tradeLog.all(),
|
tradeLog: this.tradeLog.all(),
|
||||||
feedStatus: { ...this.feedReady },
|
feedStatus: { ...this.feedReady },
|
||||||
|
spotBalances: [...this.spotBalances],
|
||||||
|
futuresBalances: [...this.futuresBalances],
|
||||||
opportunity,
|
opportunity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
|||||||
const nextFundingTime = snapshot.nextFundingTime ? new Date(snapshot.nextFundingTime).toLocaleTimeString() : "-";
|
const nextFundingTime = snapshot.nextFundingTime ? new Date(snapshot.nextFundingTime).toLocaleTimeString() : "-";
|
||||||
const feedStatus = snapshot.feedStatus;
|
const feedStatus = snapshot.feedStatus;
|
||||||
const lastLogs = snapshot.tradeLog.slice(-5);
|
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 (
|
return (
|
||||||
<Box flexDirection="column" paddingX={1}>
|
<Box flexDirection="column" paddingX={1}>
|
||||||
@@ -118,6 +120,33 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
|||||||
<Text color="gray">资金费率更新时间: {fundingUpdated} | 下次结算时间: {nextFundingTime}</Text>
|
<Text color="gray">资金费率更新时间: {fundingUpdated} | 下次结算时间: {nextFundingTime}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box flexDirection="row" marginBottom={1}>
|
||||||
|
<Box flexDirection="column" marginRight={4}>
|
||||||
|
<Text color="cyan">现货账户余额(非0)</Text>
|
||||||
|
{spotBalances.length ? (
|
||||||
|
spotBalances.map((b) => (
|
||||||
|
<Text key={`spot-${b.asset}`}>
|
||||||
|
{b.asset}: 可用 {formatNumber(b.free, 8)} | 冻结 {formatNumber(b.locked, 8)}
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text color="gray">无</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Text color="cyan">合约账户余额(非0)</Text>
|
||||||
|
{futuresBalances.length ? (
|
||||||
|
futuresBalances.map((b) => (
|
||||||
|
<Text key={`fut-${b.asset}`}>
|
||||||
|
{b.asset}: 钱包 {formatNumber(b.wallet, 8)} | 可用 {formatNumber(b.available, 8)}
|
||||||
|
</Text>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text color="gray">无</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text color={snapshot.opportunity ? "greenBright" : "redBright"}>套利差价(卖期货 / 买现货)</Text>
|
<Text color={snapshot.opportunity ? "greenBright" : "redBright"}>套利差价(卖期货 / 买现货)</Text>
|
||||||
<Text color={snapshot.opportunity ? "green" : undefined}>毛价差: {spread} USDT | {spreadBps} bp</Text>
|
<Text color={snapshot.opportunity ? "green" : undefined}>毛价差: {spread} USDT | {spreadBps} bp</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user