Closes #022 - Isolated crypto basis arbitrage bot integration
This commit is contained in:
13
DEV_LOG.md
13
DEV_LOG.md
@@ -262,6 +262,19 @@ This document tracks all modifications, npm packages, active compilation states,
|
||||
* **Active Bugs**: None.
|
||||
* **Type Checker Status**: Verified 100% clean type verification (`npx tsc --noEmit` returns exit code 0).
|
||||
|
||||
---
|
||||
|
||||
## [2026-06-14] - Isolated Crypto Basis Arbitrage Bot Integration (#ISSUE-022)
|
||||
|
||||
### Added
|
||||
* **Perpetual Futures Query Integration**: Implemented `fetchBinanceFuturesArbitrageData` in [/api/finance/route.ts](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/app/api/finance/route.ts) that queries `GET /fapi/v1/premiumIndex` on Binance USDS-M Futures in parallel with Spot Price fetching.
|
||||
* **Basis Spread & APY Calculations**: Server-side mathematical computation of the absolute basis spread (\(\text{Spread} = \text{Price}_{\text{Futures}} - \text{Price}_{\text{Spot}}\)) and theoretical compounding 8-hour APY (\(\text{APY} = (1 + F_{\text{8h}})^{1095} - 1\)).
|
||||
* **Glassmorphic Basis Arbitrage Matrix View**: Integrated an isolated sub-tab interface in [CryptoDemo.tsx](file:///c:/Users/jannr/.gemini/antigravity/scratch/investment-sandbox/components/modules/crypto/CryptoDemo.tsx) mapping BTC, ETH, and SOL spot vs futures price, raw spread, 8h funding rate, and compounding APY. Bound to an isolated `basisData` state hook loaded on a 15-second background polling cycle.
|
||||
|
||||
### Active Bugs / Compile Status
|
||||
* **Active Bugs**: None.
|
||||
* **Type Checker Status**: Verified 100% clean type verification (`npx tsc --noEmit` returns exit code 0).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -300,6 +300,41 @@ async function fetchBinanceFundingRate(symbol: string): Promise<number> {
|
||||
return symbol.includes('BTC') ? -0.015 : symbol.includes('ETH') ? 0.045 : 0.082;
|
||||
}
|
||||
|
||||
async function fetchBinanceFuturesArbitrageData(symbol: string): Promise<{ fundingRate: number; futuresPrice: number }> {
|
||||
const symbolMap: Record<string, string> = {
|
||||
'BTC-USD': 'BTCUSDT',
|
||||
'ETH-USD': 'ETHUSDT',
|
||||
'SOL-USD': 'SOLUSDT',
|
||||
'BTC': 'BTCUSDT',
|
||||
'ETH': 'ETHUSDT',
|
||||
'SOL': 'SOLUSDT'
|
||||
};
|
||||
const binanceSymbol = symbolMap[symbol] || `${symbol.replace('-USD', '')}USDT`;
|
||||
let fundingRate = symbol.includes('BTC') ? -0.015 : symbol.includes('ETH') ? 0.045 : 0.082;
|
||||
let futuresPrice = 0;
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://fapi.binance.com/fapi/v1/premiumIndex?symbol=${binanceSymbol}`, {
|
||||
cache: 'no-store',
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data) {
|
||||
if (data.lastFundingRate !== undefined) {
|
||||
fundingRate = parseFloat(data.lastFundingRate) * 100; // convert to % (e.g. 0.0001 -> 0.01%)
|
||||
}
|
||||
if (data.markPrice !== undefined) {
|
||||
futuresPrice = parseFloat(data.markPrice);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to fetch Binance premium index for ${symbol}:`, err);
|
||||
}
|
||||
return { fundingRate, futuresPrice };
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const isDevMode = process.env.DEV_MODE === 'true';
|
||||
const { searchParams } = new URL(request.url);
|
||||
@@ -378,6 +413,18 @@ export async function GET(request: Request) {
|
||||
const slice90 = validPrices.slice(-90);
|
||||
const peak90 = Math.max(...slice90);
|
||||
const priceChange = (currentPrice - peak90) / peak90;
|
||||
|
||||
let futuresPrice = 0;
|
||||
let fundingRate = -0.015;
|
||||
const binanceData = await fetchBinanceFuturesArbitrageData('BTC-USD');
|
||||
fundingRate = binanceData.fundingRate;
|
||||
futuresPrice = binanceData.futuresPrice;
|
||||
if (futuresPrice === 0) {
|
||||
futuresPrice = currentPrice * 1.0008; // Fallback 0.08% premium
|
||||
}
|
||||
const basisSpread = futuresPrice - currentPrice;
|
||||
const fundingDec = fundingRate / 100;
|
||||
const basisApy = (Math.pow(1 + fundingDec, 1095) - 1) * 100;
|
||||
|
||||
return {
|
||||
ticker,
|
||||
@@ -389,7 +436,11 @@ export async function GET(request: Request) {
|
||||
maDeviation,
|
||||
dist52w,
|
||||
rsi14,
|
||||
returns: returns.slice(-90)
|
||||
returns: returns.slice(-90),
|
||||
futuresPrice,
|
||||
basisSpread,
|
||||
basisApy,
|
||||
fundingRate
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -449,6 +500,24 @@ export async function GET(request: Request) {
|
||||
const peak90 = Math.max(...slice90);
|
||||
const priceChange = (currentPrice - peak90) / peak90;
|
||||
|
||||
let futuresPrice = 0;
|
||||
let fundingRate = ticker.includes('BTC') ? -0.015 : ticker.includes('ETH') ? 0.045 : 0.082;
|
||||
let basisSpread = 0;
|
||||
let basisApy = 0;
|
||||
|
||||
if (ticker === 'BTC-USD' || ticker === 'ETH-USD' || ticker === 'SOL-USD') {
|
||||
const binanceData = await fetchBinanceFuturesArbitrageData(ticker);
|
||||
fundingRate = binanceData.fundingRate;
|
||||
futuresPrice = binanceData.futuresPrice;
|
||||
if (futuresPrice === 0) {
|
||||
const premiumPercent = ticker.includes('BTC') ? 0.0008 : ticker.includes('ETH') ? 0.0012 : 0.0018;
|
||||
futuresPrice = currentPrice * (1 + premiumPercent);
|
||||
}
|
||||
basisSpread = futuresPrice - currentPrice;
|
||||
const fundingDec = fundingRate / 100;
|
||||
basisApy = (Math.pow(1 + fundingDec, 1095) - 1) * 100;
|
||||
}
|
||||
|
||||
return {
|
||||
ticker,
|
||||
name: result.meta?.longName || result.meta?.shortName || `${ticker} Corp.`,
|
||||
@@ -459,7 +528,11 @@ export async function GET(request: Request) {
|
||||
maDeviation,
|
||||
dist52w,
|
||||
rsi14,
|
||||
returns: returns.slice(-90) // return last 90 days of returns to keep payload slim
|
||||
returns: returns.slice(-90), // return last 90 days of returns to keep payload slim
|
||||
futuresPrice,
|
||||
basisSpread,
|
||||
basisApy,
|
||||
fundingRate
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error(`Error fetching ticker ${ticker}:`, err.message);
|
||||
@@ -483,6 +556,10 @@ export async function GET(request: Request) {
|
||||
dist52w: number;
|
||||
rsi14: number;
|
||||
returns: number[];
|
||||
futuresPrice?: number;
|
||||
basisSpread?: number;
|
||||
basisApy?: number;
|
||||
fundingRate?: number;
|
||||
}>;
|
||||
|
||||
// Rank results based on the requested scan mode
|
||||
@@ -508,14 +585,17 @@ export async function GET(request: Request) {
|
||||
|
||||
let cryptoDetails = {};
|
||||
if (isCrypto) {
|
||||
const fundingRate = await fetchBinanceFundingRate(res.ticker);
|
||||
const fundingRate = res.fundingRate !== undefined ? res.fundingRate : await fetchBinanceFundingRate(res.ticker);
|
||||
const cleanTicker = res.ticker.replace('-USD', '');
|
||||
cryptoDetails = {
|
||||
fundingRate,
|
||||
openInterestChange: cleanTicker === 'BTC' ? 8.2 : cleanTicker === 'ETH' ? -3.5 : 14.5,
|
||||
longShortRatio: cleanTicker === 'BTC' ? 0.92 : cleanTicker === 'ETH' ? 1.34 : 1.62,
|
||||
whaleInflow: cleanTicker === 'BTC' ? 480 : cleanTicker === 'ETH' ? -120 : 1250,
|
||||
exchangeReserves: cleanTicker === 'BTC' ? -1.4 : cleanTicker === 'ETH' ? 0.8 : -2.8
|
||||
exchangeReserves: cleanTicker === 'BTC' ? -1.4 : cleanTicker === 'ETH' ? 0.8 : -2.8,
|
||||
futuresPrice: res.futuresPrice,
|
||||
basisSpread: res.basisSpread,
|
||||
basisApy: res.basisApy
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,15 @@ interface Forecast {
|
||||
results?: Record<string, 'SUCCESS' | 'FAILURE'>;
|
||||
}
|
||||
|
||||
interface BasisArbitrageData {
|
||||
ticker: string;
|
||||
spotPrice: number;
|
||||
futuresPrice: number;
|
||||
basisSpread: number;
|
||||
fundingRate: number;
|
||||
basisApy: number;
|
||||
}
|
||||
|
||||
export default function CryptoDemo() {
|
||||
const { addModelTrial } = useSandboxStore();
|
||||
|
||||
@@ -126,6 +135,12 @@ export default function CryptoDemo() {
|
||||
const [isShieldActive, setIsShieldActive] = useState(true);
|
||||
const [coins, setCoins] = useState<Record<string, CoinData>>(defaultCoins);
|
||||
const [feedbackFilterAsset, setFeedbackFilterAsset] = useState<'BTC' | 'ETH' | 'SOL'>('BTC');
|
||||
const [rightColTab, setRightColTab] = useState<'radar' | 'basis'>('radar');
|
||||
const [basisData, setBasisData] = useState<BasisArbitrageData[]>([
|
||||
{ ticker: 'BTC', spotPrice: 69450, futuresPrice: 69505.5, basisSpread: 55.5, fundingRate: -0.015, basisApy: -15.15 },
|
||||
{ ticker: 'ETH', spotPrice: 3820, futuresPrice: 3824.58, basisSpread: 4.58, fundingRate: 0.045, basisApy: 63.60 },
|
||||
{ ticker: 'SOL', spotPrice: 184.20, futuresPrice: 184.54, basisSpread: 0.34, fundingRate: 0.082, basisApy: 145.45 }
|
||||
]);
|
||||
|
||||
// Safely load counters and forecasts from localStorage on client mount
|
||||
useEffect(() => {
|
||||
@@ -313,6 +328,24 @@ export default function CryptoDemo() {
|
||||
});
|
||||
return updatedCoins;
|
||||
});
|
||||
|
||||
const basisList: BasisArbitrageData[] = [];
|
||||
results.forEach((r: any) => {
|
||||
const cleanTicker = r.ticker.replace('-USD', '');
|
||||
if (cleanTicker === 'BTC' || cleanTicker === 'ETH' || cleanTicker === 'SOL') {
|
||||
basisList.push({
|
||||
ticker: cleanTicker,
|
||||
spotPrice: r.currentPrice || 0,
|
||||
futuresPrice: r.futuresPrice || 0,
|
||||
basisSpread: r.basisSpread || 0,
|
||||
fundingRate: r.fundingRate || 0,
|
||||
basisApy: r.basisApy || 0
|
||||
});
|
||||
}
|
||||
});
|
||||
if (basisList.length > 0) {
|
||||
setBasisData(basisList);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch crypto prices:", err);
|
||||
}
|
||||
@@ -796,60 +829,122 @@ export default function CryptoDemo() {
|
||||
{/* Right Column: Multi-Model Ensemble & Walk-Forward Radar Table */}
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-6">
|
||||
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Compass className="text-cyan-400 w-5 h-5" /> Walk-Forward Ensemble Radar
|
||||
</h3>
|
||||
{loadingEnsemble && (
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setRightColTab('radar')}
|
||||
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all ${rightColTab === 'radar' ? 'text-white border-b-2 border-cyan-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
>
|
||||
<Compass className="text-cyan-400 w-4 h-4" /> Walk-Forward Ensemble Radar
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRightColTab('basis')}
|
||||
className={`text-sm font-bold flex items-center gap-2 pb-1 transition-all ${rightColTab === 'basis' ? 'text-white border-b-2 border-cyan-500' : 'text-slate-400 hover:text-slate-200'}`}
|
||||
>
|
||||
<TrendingUp className="text-cyan-400 w-4 h-4" /> Basis Arbitrage Matrix
|
||||
</button>
|
||||
</div>
|
||||
{loadingEnsemble && rightColTab === 'radar' && (
|
||||
<RefreshCw className="w-4 h-4 text-cyan-400 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-400 leading-relaxed">
|
||||
Displays predictions and live calibration metrics (<InlineMath math="E[\theta] = \alpha / (\alpha + \beta)" />) across 15 independent trackers.
|
||||
</div>
|
||||
{rightColTab === 'radar' ? (
|
||||
<>
|
||||
<div className="text-xs text-slate-400 leading-relaxed">
|
||||
Displays predictions and live calibration metrics (<InlineMath math="E[\theta] = \alpha / (\alpha + \beta)" />) across 15 independent trackers.
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-slate-850 bg-slate-950/40">
|
||||
<table className="w-full border-collapse text-left text-[11px] font-mono">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
|
||||
<th className="p-2">Estimator</th>
|
||||
<th className="p-2 text-center">T+1</th>
|
||||
<th className="p-2 text-center">T+5</th>
|
||||
<th className="p-2 text-center">T+10</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ESTIMATORS.map((est) => (
|
||||
<tr key={est.id} className="border-b border-slate-900 hover:bg-slate-850/10">
|
||||
<td className="p-2 font-semibold text-slate-300">{est.name}</td>
|
||||
{HORIZONS.map((h) => {
|
||||
const trackerKey = `${est.id}_${h.id}`;
|
||||
const tracker = trackers[trackerKey] || { alpha: 1, beta: 1 };
|
||||
const prob = getPredictionProb(activeTicker, est.id, h.id);
|
||||
const direction = prob > 0.5 ? 'UP' : 'DOWN';
|
||||
const expValue = tracker.alpha / (tracker.alpha + tracker.beta);
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-slate-850 bg-slate-950/40">
|
||||
<table className="w-full border-collapse text-left text-[11px] font-mono">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
|
||||
<th className="p-2">Estimator</th>
|
||||
<th className="p-2 text-center">T+1</th>
|
||||
<th className="p-2 text-center">T+5</th>
|
||||
<th className="p-2 text-center">T+10</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ESTIMATORS.map((est) => (
|
||||
<tr key={est.id} className="border-b border-slate-900 hover:bg-slate-850/10">
|
||||
<td className="p-2 font-semibold text-slate-300">{est.name}</td>
|
||||
{HORIZONS.map((h) => {
|
||||
const trackerKey = `${est.id}_${h.id}`;
|
||||
const tracker = trackers[trackerKey] || { alpha: 1, beta: 1 };
|
||||
const prob = getPredictionProb(activeTicker, est.id, h.id);
|
||||
const direction = prob > 0.5 ? 'UP' : 'DOWN';
|
||||
const expValue = tracker.alpha / (tracker.alpha + tracker.beta);
|
||||
|
||||
return (
|
||||
<td key={h.id} className="p-2 text-center border-l border-slate-900">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`font-bold ${direction === 'UP' ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{direction === 'UP' ? '▲' : '▼'} {(prob * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-500 mt-0.5">
|
||||
{tracker.alpha}/{tracker.beta}
|
||||
</span>
|
||||
<span className="text-[9px] text-cyan-400 font-semibold mt-0.5">
|
||||
E: {(expValue * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-xs text-slate-400 leading-relaxed">
|
||||
Monitors basis spread and compounding funding rate APY (<InlineMath math="\text{APY} = (1 + F_{\text{8h}})^{1095} - 1" />) for cash-and-carry arbitrage.
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-slate-850 bg-slate-950/40">
|
||||
<table className="w-full border-collapse text-left text-[11px] font-mono">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
|
||||
<th className="p-2">Ticker</th>
|
||||
<th className="p-2">Spot</th>
|
||||
<th className="p-2">Futures</th>
|
||||
<th className="p-2 text-right">Spread</th>
|
||||
<th className="p-2 text-center">Funding</th>
|
||||
<th className="p-2 text-right">APY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{basisData.map((data) => {
|
||||
const isPositive = data.basisSpread >= 0;
|
||||
const isPositiveApy = data.basisApy >= 0;
|
||||
return (
|
||||
<td key={h.id} className="p-2 text-center border-l border-slate-900">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`font-bold ${direction === 'UP' ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{direction === 'UP' ? '▲' : '▼'} {(prob * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-500 mt-0.5">
|
||||
{tracker.alpha}/{tracker.beta}
|
||||
</span>
|
||||
<span className="text-[9px] text-cyan-400 font-semibold mt-0.5">
|
||||
E: {(expValue * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<tr key={data.ticker} className="border-b border-slate-900 hover:bg-slate-850/10">
|
||||
<td className="p-2 font-bold text-cyan-400">{data.ticker}</td>
|
||||
<td className="p-2 text-slate-300">
|
||||
${data.spotPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className="p-2 text-slate-300">
|
||||
${data.futuresPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className={`p-2 text-right font-semibold ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{isPositive ? '+' : ''}${data.basisSpread.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</td>
|
||||
<td className={`p-2 text-center font-mono ${data.fundingRate >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{data.fundingRate >= 0 ? '+' : ''}{data.fundingRate.toFixed(4)}%
|
||||
</td>
|
||||
<td className={`p-2 text-right font-bold ${isPositiveApy ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{isPositiveApy ? '+' : ''}{data.basisApy.toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Model Calibration Log & Simulation */}
|
||||
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user