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:
@@ -113,6 +113,7 @@ export interface BasisArbConfig {
|
||||
refreshIntervalMs: number;
|
||||
maxLogEntries: number;
|
||||
takerFeeRate: number;
|
||||
arbAmount: number; // base asset amount to arb (e.g., ASTER amount when ASTERUSDT)
|
||||
}
|
||||
|
||||
export type GridDirection = "both" | "long" | "short";
|
||||
@@ -158,6 +159,7 @@ export const basisConfig: BasisArbConfig = {
|
||||
refreshIntervalMs: parseNumber(process.env.BASIS_REFRESH_INTERVAL_MS, 1000),
|
||||
maxLogEntries: parseNumber(process.env.BASIS_MAX_LOG_ENTRIES, 200),
|
||||
takerFeeRate: parseNumber(process.env.BASIS_TAKER_FEE_RATE, 0.0004),
|
||||
arbAmount: parseNumber(process.env.ARB_AMOUNT, parseNumber(process.env.TRADE_AMOUNT, 0)),
|
||||
};
|
||||
|
||||
const resolveGridDirection = (raw: string | undefined, fallback: GridDirection): GridDirection => {
|
||||
|
||||
@@ -19,6 +19,10 @@ export interface BasisArbSnapshot {
|
||||
fundingRate: number | null;
|
||||
nextFundingTime: number | null;
|
||||
fundingLastUpdate: number | null;
|
||||
fundingIncomePerFunding: number | null; // USDT per funding event
|
||||
fundingIncomePerDay: number | null; // USDT per day (assuming 3 fundings/day)
|
||||
takerFeesPerRoundTrip: number | null; // USDT cost to open both legs
|
||||
fundingCountToBreakeven: number | null; // number of fundings to cover fees
|
||||
spread: number | null;
|
||||
spreadBps: number | null;
|
||||
netSpread: number | null;
|
||||
@@ -97,6 +101,8 @@ export class BasisArbEngine {
|
||||
private spotAccountInFlight = false;
|
||||
private futuresAccountInFlight = false;
|
||||
private stopped = false;
|
||||
private lastEntrySignalAt = 0;
|
||||
private lastExitSignalAt = 0;
|
||||
|
||||
constructor(config: BasisArbConfig, exchange: ExchangeAdapter, deps: BasisArbDependencies = {}) {
|
||||
this.config = config;
|
||||
@@ -291,6 +297,8 @@ export class BasisArbEngine {
|
||||
}
|
||||
|
||||
private emitUpdate(): void {
|
||||
// Evaluate entry/exit signals before emitting so the snapshot includes new log lines
|
||||
this.evaluateSignals();
|
||||
this.events.emit("update", this.buildSnapshot(), (error) => {
|
||||
this.tradeLog.push("error", `推送订阅失败: ${String(error)}`);
|
||||
});
|
||||
@@ -307,6 +315,12 @@ export class BasisArbEngine {
|
||||
const spreadBps = this.computeSpreadBps(spread, spotAsk);
|
||||
const netSpread = this.computeNetSpread(futuresBid, spotAsk);
|
||||
const netSpreadBps = this.computeSpreadBps(netSpread, spotAsk);
|
||||
const perFundingIncome = this.computeFundingIncomeUSDT(fundingRate, spotAsk);
|
||||
const perDayIncome = perFundingIncome != null ? perFundingIncome * 3 : null; // 3 times/day typical
|
||||
const takerFeesPerRoundTrip = this.computeRoundTripFeesUSDT(spotAsk);
|
||||
const fundingCountToBreakeven = perFundingIncome && perFundingIncome > 0 && takerFeesPerRoundTrip != null
|
||||
? takerFeesPerRoundTrip / perFundingIncome
|
||||
: null;
|
||||
const opportunity = netSpread != null && netSpread >= 0;
|
||||
const lastUpdated = Math.max(
|
||||
futuresBid != null && this.futures.updatedAt ? this.futures.updatedAt : 0,
|
||||
@@ -327,6 +341,10 @@ export class BasisArbEngine {
|
||||
fundingRate,
|
||||
nextFundingTime,
|
||||
fundingLastUpdate: this.funding.updatedAt,
|
||||
fundingIncomePerFunding: perFundingIncome,
|
||||
fundingIncomePerDay: perDayIncome,
|
||||
takerFeesPerRoundTrip,
|
||||
fundingCountToBreakeven,
|
||||
spread,
|
||||
spreadBps,
|
||||
netSpread,
|
||||
@@ -361,4 +379,58 @@ export class BasisArbEngine {
|
||||
const buySpotNet = Number(spotAsk) * (1 + effectiveFee);
|
||||
return sellFuturesNet - buySpotNet;
|
||||
}
|
||||
|
||||
private evaluateSignals(): void {
|
||||
// Require both futures and spot feeds before computing signals
|
||||
if (!this.feedReady.futures || !this.feedReady.spot) return;
|
||||
const now = this.now();
|
||||
const futuresBid = this.futures.bid;
|
||||
const spotAsk = this.spot.ask;
|
||||
const spread = this.computeSpread(futuresBid, spotAsk);
|
||||
const spreadBps = this.computeSpreadBps(spread, spotAsk);
|
||||
const fundingRate = this.funding.rate;
|
||||
const nextFundingTime = this.funding.nextFundingTime;
|
||||
const msUntilFunding = typeof nextFundingTime === "number" ? nextFundingTime - now : null;
|
||||
|
||||
// Entry signal: positive bp and next funding >= 10 minutes away
|
||||
if (Number.isFinite(spreadBps ?? NaN) && (spreadBps as number) > 0 && Number.isFinite(msUntilFunding ?? NaN) && (msUntilFunding as number) >= 10 * 60 * 1000) {
|
||||
if (now - this.lastEntrySignalAt >= 60 * 1000) { // debounce 60s
|
||||
this.lastEntrySignalAt = now;
|
||||
const bpTxt = (spreadBps as number).toFixed(2);
|
||||
const minutes = Math.floor(((msUntilFunding as number) / 60000));
|
||||
this.tradeLog.push("entry", `入场机会: 价差 ${bpTxt} bp | 距下次资金费约 ${minutes} 分钟`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exit signal: funding rate negative and within 10 minutes before collection
|
||||
if (Number.isFinite(fundingRate ?? NaN) && (fundingRate as number) < 0 && Number.isFinite(msUntilFunding ?? NaN) && (msUntilFunding as number) > 0 && (msUntilFunding as number) <= 10 * 60 * 1000) {
|
||||
if (now - this.lastExitSignalAt >= 60 * 1000) { // debounce 60s
|
||||
this.lastExitSignalAt = now;
|
||||
const minutes = Math.max(0, Math.floor(((msUntilFunding as number) / 60000)));
|
||||
this.tradeLog.push("exit", `出场机会: 资金费率为负 | 距收取约 ${minutes} 分钟`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private computeFundingIncomeUSDT(fundingRate: number | null, spotAsk: number | null): number | null {
|
||||
if (!Number.isFinite(fundingRate ?? NaN)) return null;
|
||||
const price = Number.isFinite(spotAsk ?? NaN) ? Number(spotAsk) : null;
|
||||
const amount = Number.isFinite(this.config.arbAmount ?? NaN) ? Number(this.config.arbAmount) : null;
|
||||
if (price == null || amount == null) return null;
|
||||
// Funding income per event for a delta-neutral hedge ~ rate * notional
|
||||
// Notional in USDT = amount * price
|
||||
const notional = amount * price;
|
||||
const rate = Number(fundingRate);
|
||||
return notional * rate;
|
||||
}
|
||||
|
||||
private computeRoundTripFeesUSDT(spotAsk: number | null): number | null {
|
||||
const price = Number.isFinite(spotAsk ?? NaN) ? Number(spotAsk) : null;
|
||||
const amount = Number.isFinite(this.config.arbAmount ?? NaN) ? Number(this.config.arbAmount) : null;
|
||||
if (price == null || amount == null) return null;
|
||||
const notional = amount * price;
|
||||
// Two taker trades (sell futures, buy spot) → fees on both legs
|
||||
const perSide = (this.config.takerFeeRate ?? 0) * notional;
|
||||
return perSide * 2;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-5
@@ -85,6 +85,10 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
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);
|
||||
@@ -118,6 +122,8 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
<Text color="yellow">资金费率</Text>
|
||||
<Text>当前资金费率: {fundingRatePct}</Text>
|
||||
<Text color="gray">资金费率更新时间: {fundingUpdated} | 下次结算时间: {nextFundingTime}</Text>
|
||||
<Text>单次资金费率收益(估): {fundingIncomePerFunding} | 日收益(估): {fundingIncomePerDay}</Text>
|
||||
<Text>双边吃单手续费(估): {takerFeesPerRoundTrip} | 回本所需资金费率次数: {fundingCountToBreakeven}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
@@ -158,11 +164,14 @@ export function BasisApp({ onExit }: BasisAppProps) {
|
||||
<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>
|
||||
))
|
||||
lastLogs.map((entry, index) => {
|
||||
const color = entry.type === "entry" ? "green" : entry.type === "exit" ? "red" : undefined;
|
||||
return (
|
||||
<Text key={`${entry.time}-${index}`} color={color}>
|
||||
[{entry.time}] [{entry.type}] {entry.detail}
|
||||
</Text>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text color="gray">暂无日志</Text>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user