Files
investment-sandbox/components/modules/crypto/CryptoDemo.tsx
2026-06-13 14:49:25 +02:00

1008 lines
44 KiB
TypeScript

'use client';
import React, { useState, useMemo, useEffect } from 'react';
import { useSandboxStore } from '@/lib/store';
import { predictCryptoTrend, calculateBetaPosterior } from '@/lib/math/statistics';
import 'katex/dist/katex.min.css';
import { BlockMath, InlineMath } from 'react-katex';
import CryptoMathModal from './CryptoMathModal';
import {
Cpu, Search, RefreshCw, BarChart2, TrendingUp, AlertCircle, Info,
ChevronDown, ChevronUp, ArrowUpRight, ArrowDownRight, Compass, ShieldAlert, Sparkles,
BookOpen, Check
} from 'lucide-react';
interface TrackerState {
alpha: number;
beta: number;
}
type TrackersMap = Record<string, TrackerState>;
const ESTIMATORS = [
{ id: 'rf', name: 'Random Forest' },
{ id: 'gb', name: 'XGBoost / GB' },
{ id: 'lr', name: 'Logistic Regression' },
{ id: 'svm', name: 'Support Vector Machine' },
{ id: 'mlp', name: 'Multi-Layer Perceptron' }
] as const;
const HORIZONS = [
{ id: 'T1', name: 'T+1 Day', days: 1 },
{ id: 'T5', name: 'T+5 Days', days: 5 },
{ id: 'T10', name: 'T+10 Days', days: 10 }
] as const;
interface CoinData {
ticker: string;
name: string;
price: string;
change24h: number;
fundingRate: number; // in %
openInterestChange: number; // in %
longShortRatio: number;
whaleInflow: number; // net flows
exchangeReserves: number; // in %
liqLong: string;
liqShort: string;
}
const defaultCoins: Record<string, CoinData> = {
'BTC': {
ticker: 'BTC',
name: 'Bitcoin',
price: '$69,450',
change24h: 2.4,
fundingRate: -0.015,
openInterestChange: 8.2,
longShortRatio: 0.92,
whaleInflow: 480,
exchangeReserves: -1.4,
liqLong: '$68,200',
liqShort: '$70,500'
},
'ETH': {
ticker: 'ETH',
name: 'Ethereum',
price: '$3,820',
change24h: -1.2,
fundingRate: 0.045,
openInterestChange: -3.5,
longShortRatio: 1.34,
whaleInflow: -120,
exchangeReserves: 0.8,
liqLong: '$3,710',
liqShort: '$3,920'
},
'SOL': {
ticker: 'SOL',
name: 'Solana',
price: '$184.20',
change24h: 5.8,
fundingRate: 0.082,
openInterestChange: 14.5,
longShortRatio: 1.62,
whaleInflow: 1250,
exchangeReserves: -2.8,
liqLong: '$176.00',
liqShort: '$192.50'
}
};
interface Forecast {
id: string;
ticker: string;
entryPrice: number;
resolved: boolean;
timestamp: number;
predictions: Record<string, Record<string, number>>;
targetTimes: Record<string, number>;
results?: Record<string, 'SUCCESS' | 'FAILURE'>;
}
export default function CryptoDemo() {
const { addModelTrial } = useSandboxStore();
// Local state for alphaSuccess and betaFailure to satisfy SSR hydration safeguarding
const [alphaSuccess, setAlphaSuccess] = useState<number>(394);
const [betaFailure, setBetaFailure] = useState<number>(118);
const [forecasts, setForecasts] = useState<Forecast[]>([]);
const [learningLoopLog, setLearningLoopLog] = useState<string>('');
const [activeTicker, setActiveTicker] = useState<string>('BTC');
const [searchQuery, setSearchQuery] = useState('');
const [customCoins, setCustomCoins] = useState<Record<string, CoinData>>({});
const [searchError, setSearchError] = useState(false);
const [showMathAccordion, setShowMathAccordion] = useState(false);
const [isMathModalOpen, setIsMathModalOpen] = useState(false);
const [simulatedTrialLogged, setSimulatedTrialLogged] = useState(false);
const [lastTrialSuccess, setLastTrialSuccess] = useState(false);
// 15 independent Beta-Posterior trackers
const [trackers, setTrackers] = useState<TrackersMap>({});
const [ensemblePredictions, setEnsemblePredictions] = useState<any>(null);
const [loadingEnsemble, setLoadingEnsemble] = useState(false);
// Safely load counters and forecasts from localStorage on client mount
useEffect(() => {
const savedAlpha = localStorage.getItem('crypto_bayes_alpha');
const savedBeta = localStorage.getItem('crypto_bayes_beta');
const savedForecasts = localStorage.getItem('crypto_bayes_forecasts');
let loadedAlpha = 394;
let loadedBeta = 118;
if (savedAlpha !== null) {
loadedAlpha = parseInt(savedAlpha, 10);
setAlphaSuccess(loadedAlpha);
} else {
localStorage.setItem('crypto_bayes_alpha', '394');
}
if (savedBeta !== null) {
loadedBeta = parseInt(savedBeta, 10);
setBetaFailure(loadedBeta);
} else {
localStorage.setItem('crypto_bayes_beta', '118');
}
// Load trackers
const defaultPriors: Record<string, { alpha: number; beta: number }> = {
'rf_T1': { alpha: 38, beta: 12 }, 'rf_T5': { alpha: 35, beta: 15 }, 'rf_T10': { alpha: 32, beta: 18 },
'gb_T1': { alpha: 40, beta: 10 }, 'gb_T5': { alpha: 36, beta: 14 }, 'gb_T10': { alpha: 30, beta: 20 },
'lr_T1': { alpha: 35, beta: 15 }, 'lr_T5': { alpha: 33, beta: 17 }, 'lr_T10': { alpha: 31, beta: 19 },
'svm_T1': { alpha: 36, beta: 14 }, 'svm_T5': { alpha: 34, beta: 16 }, 'svm_T10': { alpha: 32, beta: 18 },
'mlp_T1': { alpha: 39, beta: 11 }, 'mlp_T5': { alpha: 35, beta: 15 }, 'mlp_T10': { alpha: 31, beta: 19 }
};
const map: TrackersMap = {};
Object.keys(defaultPriors).forEach((key) => {
const a = localStorage.getItem(`crypto_bayes_tracker_${key}_alpha`);
const b = localStorage.getItem(`crypto_bayes_tracker_${key}_beta`);
const alphaVal = a !== null ? parseInt(a, 10) : defaultPriors[key].alpha;
const betaVal = b !== null ? parseInt(b, 10) : defaultPriors[key].beta;
map[key] = { alpha: alphaVal, beta: betaVal };
if (a === null) {
localStorage.setItem(`crypto_bayes_tracker_${key}_alpha`, String(alphaVal));
localStorage.setItem(`crypto_bayes_tracker_${key}_beta`, String(betaVal));
}
});
setTrackers(map);
// Fetch ensemble predictions
const fetchEnsemble = async () => {
setLoadingEnsemble(true);
try {
const res = await fetch('/api/crypto/ensemble');
if (res.ok) {
const data = await res.json();
setEnsemblePredictions(data.predictions || null);
}
} catch (err) {
console.error("Failed to load ensemble predictions:", err);
} finally {
setLoadingEnsemble(false);
}
};
fetchEnsemble();
if (savedForecasts !== null) {
try {
const parsed = JSON.parse(savedForecasts);
// Clean legacy formats if necessary
if (parsed.length > 0 && parsed[0].predictions === undefined) {
throw new Error("Legacy forecast format");
}
setForecasts(parsed);
} catch (err) {
console.log("Resetting legacy forecasts to multi-model format...");
const now = Date.now();
const mockForecasts: Forecast[] = [
{
id: 'mock-1',
ticker: 'BTC',
entryPrice: 65000,
resolved: true,
timestamp: now - 86400 * 1000 * 3,
predictions: {
rf: { T1: 0.62, T5: 0.58, T10: 0.54 },
gb: { T1: 0.65, T5: 0.61, T10: 0.51 },
lr: { T1: 0.58, T5: 0.57, T10: 0.55 },
svm: { T1: 0.60, T5: 0.59, T10: 0.56 },
mlp: { T1: 0.64, T5: 0.60, T10: 0.53 }
},
targetTimes: {
T1: now - 86400 * 1000 * 2,
T5: now - 86400 * 1000 * 2,
T10: now - 86400 * 1000 * 2
},
results: {
rf_T1: 'SUCCESS', rf_T5: 'SUCCESS', rf_T10: 'SUCCESS',
gb_T1: 'SUCCESS', gb_T5: 'SUCCESS', gb_T10: 'SUCCESS',
lr_T1: 'SUCCESS', lr_T5: 'SUCCESS', lr_T10: 'SUCCESS',
svm_T1: 'SUCCESS', svm_T5: 'SUCCESS', svm_T10: 'SUCCESS',
mlp_T1: 'SUCCESS', mlp_T5: 'SUCCESS', mlp_T10: 'SUCCESS'
}
}
];
setForecasts(mockForecasts);
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(mockForecasts));
}
} else {
const now = Date.now();
const mockForecasts: Forecast[] = [
{
id: 'mock-1',
ticker: 'BTC',
entryPrice: 65000,
resolved: true,
timestamp: now - 86400 * 1000 * 3,
predictions: {
rf: { T1: 0.62, T5: 0.58, T10: 0.54 },
gb: { T1: 0.65, T5: 0.61, T10: 0.51 },
lr: { T1: 0.58, T5: 0.57, T10: 0.55 },
svm: { T1: 0.60, T5: 0.59, T10: 0.56 },
mlp: { T1: 0.64, T5: 0.60, T10: 0.53 }
},
targetTimes: {
T1: now - 86400 * 1000 * 2,
T5: now - 86400 * 1000 * 2,
T10: now - 86400 * 1000 * 2
},
results: {
rf_T1: 'SUCCESS', rf_T5: 'SUCCESS', rf_T10: 'SUCCESS',
gb_T1: 'SUCCESS', gb_T5: 'SUCCESS', gb_T10: 'SUCCESS',
lr_T1: 'SUCCESS', lr_T5: 'SUCCESS', lr_T10: 'SUCCESS',
svm_T1: 'SUCCESS', svm_T5: 'SUCCESS', svm_T10: 'SUCCESS',
mlp_T1: 'SUCCESS', mlp_T5: 'SUCCESS', mlp_T10: 'SUCCESS'
}
}
];
setForecasts(mockForecasts);
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(mockForecasts));
}
}, []);
// Client-side background learning loop evaluating forecasts against actual live returns
useEffect(() => {
const runLearningLoop = async () => {
if (Object.keys(trackers).length === 0) return;
try {
const res = await fetch('/api/finance?region=crypto');
if (!res.ok) return;
const data = await res.json();
const results = data.results || [];
const pricesMap: Record<string, number> = {};
results.forEach((r: any) => {
pricesMap[r.ticker] = r.currentPrice;
const cleanTicker = r.ticker.replace('-USD', '');
pricesMap[cleanTicker] = r.currentPrice;
});
let updatedAny = false;
const nextTrackers = { ...trackers };
const updatedForecasts = forecasts.map((f) => {
if (f.resolved) return f;
const currentPrice = pricesMap[f.ticker] || pricesMap[`${f.ticker}-USD`];
if (!currentPrice) return f;
const now = Date.now();
const resultsMap = { ...(f.results || {}) };
let modified = false;
HORIZONS.forEach((h) => {
const hKey = h.id;
const targetTime = f.targetTimes[hKey];
ESTIMATORS.forEach((est) => {
const trackerKey = `${est.id}_${hKey}`;
if (now >= targetTime && !resultsMap[trackerKey]) {
const priceWentUp = currentPrice > f.entryPrice;
const predProb = f.predictions[est.id]?.[hKey] ?? 0.5;
const predDir = predProb > 0.5 ? 'UP' : 'DOWN';
const success = (predDir === 'UP' && priceWentUp) || (predDir === 'DOWN' && !priceWentUp);
resultsMap[trackerKey] = success ? 'SUCCESS' : 'FAILURE';
if (success) {
nextTrackers[trackerKey].alpha += 1;
localStorage.setItem(`crypto_bayes_tracker_${trackerKey}_alpha`, String(nextTrackers[trackerKey].alpha));
} else {
nextTrackers[trackerKey].beta += 1;
localStorage.setItem(`crypto_bayes_tracker_${trackerKey}_beta`, String(nextTrackers[trackerKey].beta));
}
addModelTrial(success);
updatedAny = true;
modified = true;
}
});
});
if (modified) {
const allResolved = ESTIMATORS.every(est =>
HORIZONS.every(h => resultsMap[`${est.id}_${h.id}`] !== undefined)
);
return {
...f,
results: resultsMap,
resolved: allResolved
};
}
return f;
});
if (updatedAny) {
setTrackers(nextTrackers);
setForecasts(updatedForecasts);
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(updatedForecasts));
setLearningLoopLog(`Processed active ensemble forecasts. Trackers calibration updated.`);
setTimeout(() => setLearningLoopLog(''), 6000);
}
} catch (err) {
console.error("Learning loop evaluation error:", err);
}
};
if (forecasts.length > 0) {
runLearningLoop();
}
const interval = setInterval(runLearningLoop, 30000);
return () => clearInterval(interval);
}, [forecasts, trackers, addModelTrial]);
// Active Coin data retrieval
const activeCoin = useMemo(() => {
return customCoins[activeTicker] || defaultCoins[activeTicker] || defaultCoins['BTC'];
}, [activeTicker, customCoins]);
// Helper to fetch/load prediction probabilities
const getPredictionProb = (estimator: string, horizon: string): number => {
if (ensemblePredictions && ensemblePredictions[activeTicker] && ensemblePredictions[activeTicker][estimator]) {
return ensemblePredictions[activeTicker][estimator][horizon] ?? 0.5;
}
// Fallback static predictions
const defaultMapping: Record<string, Record<string, Record<string, number>>> = {
BTC: {
rf: { T1: 0.62, T5: 0.58, T10: 0.54 },
gb: { T1: 0.65, T5: 0.61, T10: 0.51 },
lr: { T1: 0.58, T5: 0.57, T10: 0.55 },
svm: { T1: 0.60, T5: 0.59, T10: 0.56 },
mlp: { T1: 0.64, T5: 0.60, T10: 0.53 }
},
ETH: {
rf: { T1: 0.60, T5: 0.59, T10: 0.54 },
gb: { T1: 0.66, T5: 0.61, T10: 0.48 },
lr: { T1: 0.58, T5: 0.55, T10: 0.56 },
svm: { T1: 0.59, T5: 0.59, T10: 0.56 },
mlp: { T1: 0.64, T5: 0.59, T10: 0.55 }
},
SOL: {
rf: { T1: 0.65, T5: 0.58, T10: 0.52 },
gb: { T1: 0.63, T5: 0.63, T10: 0.54 },
lr: { T1: 0.59, T5: 0.58, T10: 0.54 },
svm: { T1: 0.60, T5: 0.62, T10: 0.56 },
mlp: { T1: 0.66, T5: 0.60, T10: 0.51 }
}
};
const assetKey = defaultMapping[activeTicker] ? activeTicker : 'BTC';
return defaultMapping[assetKey][estimator]?.[horizon] ?? 0.5;
};
// Compute live Random Forest baseline predictions (for legacy/visual compatibility)
const mlPredictions = useMemo(() => {
const inputs = {
fundingRate: activeCoin.fundingRate,
openInterestChange: activeCoin.openInterestChange,
longShortRatio: activeCoin.longShortRatio,
whaleInflow: activeCoin.whaleInflow
};
return predictCryptoTrend(inputs);
}, [activeCoin]);
// Apply Bayesian online learning error-correction posterior update (legacy/visual)
const correctedPredictions = useMemo(() => {
const shortTermCorrected = calculateBetaPosterior(
alphaSuccess,
betaFailure,
mlPredictions.shortTermProb,
12 // confidence scale
);
const mediumTermCorrected = calculateBetaPosterior(
alphaSuccess,
betaFailure,
mlPredictions.mediumTermProb,
12
);
return {
shortTerm: shortTermCorrected,
mediumTerm: mediumTermCorrected
};
}, [mlPredictions, alphaSuccess, betaFailure]);
// Search/add new altcoin
const handleAltcoinSearch = (e: React.FormEvent) => {
e.preventDefault();
setSearchError(false);
const query = searchQuery.trim().toUpperCase();
if (!query) return;
if (defaultCoins[query]) {
setActiveTicker(query);
setSearchQuery('');
return;
}
if (customCoins[query]) {
setActiveTicker(query);
setSearchQuery('');
return;
}
const isBull = Math.random() > 0.45;
const simulatedChange = isBull ? 3 + Math.random() * 8 : -2 - Math.random() * 6;
const simulatedPrice = isBull ? 2 + Math.random() * 10 : 0.2 + Math.random() * 3;
const newCoin: CoinData = {
ticker: query,
name: `${query} Token`,
price: `$${simulatedPrice.toFixed(4)}`,
change24h: parseFloat(simulatedChange.toFixed(2)),
fundingRate: parseFloat((Math.random() * 0.12 - 0.04).toFixed(3)),
openInterestChange: parseFloat((Math.random() * 30 - 10).toFixed(1)),
longShortRatio: parseFloat((0.8 + Math.random() * 1.1).toFixed(2)),
whaleInflow: Math.floor(Math.random() * 1500 - 400),
exchangeReserves: parseFloat((Math.random() * 4 - 2).toFixed(1)),
liqLong: `$${(simulatedPrice * 0.9).toFixed(4)}`,
liqShort: `$${(simulatedPrice * 1.1).toFixed(4)}`
};
setCustomCoins(prev => ({ ...prev, [query]: newCoin }));
setActiveTicker(query);
setSearchQuery('');
};
// Manual logging of active forecast for all 15 models & horizons
const handleLogManualForecast = () => {
const entryPrice = parseFloat(activeCoin.price.replace(/[^0-9.]/g, ''));
// Save snapshot of all predictions
const predictionsMap: Record<string, Record<string, number>> = {};
ESTIMATORS.forEach((est) => {
predictionsMap[est.id] = {
T1: getPredictionProb(est.id, 'T1'),
T5: getPredictionProb(est.id, 'T5'),
T10: getPredictionProb(est.id, 'T10')
};
});
const now = Date.now();
const newForecast: Forecast = {
id: 'fc-' + now,
ticker: activeCoin.ticker,
entryPrice,
resolved: false,
timestamp: now,
predictions: predictionsMap,
targetTimes: {
T1: now + 60 * 1000, // resolves in 60s for direct visual validation
T5: now + 300 * 1000, // resolves in 300s
T10: now + 600 * 1000 // resolves in 600s
}
};
const nextForecasts = [newForecast, ...forecasts];
setForecasts(nextForecasts);
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(nextForecasts));
setLearningLoopLog(`Registered active multi-model forecast for ${activeCoin.ticker} at $${entryPrice}. Evaluating T+1 (60s), T+5 (5m), and T+10 (10m).`);
setTimeout(() => setLearningLoopLog(''), 8000);
};
// Simulator for ensemble calibration (simulates trials across all 15 trackers)
const handleSimulateEnsembleTrial = (success: boolean) => {
if (Object.keys(trackers).length === 0) return;
const nextTrackers = { ...trackers };
ESTIMATORS.forEach((est) => {
HORIZONS.forEach((h) => {
const trackerKey = `${est.id}_${h.id}`;
if (!nextTrackers[trackerKey]) {
nextTrackers[trackerKey] = { alpha: 1, beta: 1 };
}
if (success) {
nextTrackers[trackerKey].alpha += 1;
localStorage.setItem(`crypto_bayes_tracker_${trackerKey}_alpha`, String(nextTrackers[trackerKey].alpha));
} else {
nextTrackers[trackerKey].beta += 1;
localStorage.setItem(`crypto_bayes_tracker_${trackerKey}_beta`, String(nextTrackers[trackerKey].beta));
}
});
});
setTrackers(nextTrackers);
setLastTrialSuccess(success);
setSimulatedTrialLogged(true);
setTimeout(() => setSimulatedTrialLogged(false), 2000);
};
const totalTrials = alphaSuccess + betaFailure;
const priorAccuracy = (alphaSuccess / (totalTrials || 1)) * 100;
return (
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-cyan-500/10 rounded-full blur-3xl -z-10" />
{/* Header */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 border-b border-slate-800 pb-4 mb-6">
<div>
<span className="text-cyan-400 text-xs font-semibold uppercase tracking-wider">Level 4</span>
<h2 className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-sky-200 bg-clip-text text-transparent">
Predictive Crypto Models & Bayes Self-Correction
</h2>
</div>
<div className="flex flex-wrap items-center gap-3">
<span className="inline-flex items-center gap-1.5 px-3 py-2.5 rounded-xl text-xs font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 shadow-[0_0_15px_rgba(245,158,11,0.15)] h-11">
<ShieldAlert className="w-4 h-4 text-amber-400" />
<span>SYSTEM-AUTARK (OFFLINE-CORE)</span>
</span>
<button
onClick={() => setIsMathModalOpen(true)}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-950/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all font-semibold text-xs tracking-wider text-cyan-400 justify-center h-11"
>
<BookOpen className="w-3.5 h-3.5" />
<span>📖 Quantitative Handbook</span>
</button>
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-2 flex items-center gap-3 h-11">
<Cpu className="text-cyan-400 w-5 h-5 animate-spin-slow" />
<div>
<p className="text-slate-400 text-xs">Prior Accuracy</p>
<p className="font-mono text-sm font-bold text-cyan-400">
{priorAccuracy.toFixed(1)}% (n={totalTrials})
</p>
</div>
</div>
</div>
</div>
{/* SECTION 1: Top 3 Cards & Search Mask */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-6">
{/* Status Cards BTC, ETH, SOL */}
{['BTC', 'ETH', 'SOL'].map((tick) => {
const coin = defaultCoins[tick];
const isActive = activeTicker === tick;
const isUp = coin.change24h >= 0;
return (
<div
key={tick}
onClick={() => setActiveTicker(tick)}
className={`p-4 rounded-xl border cursor-pointer transition-all hover:bg-slate-850 flex items-center justify-between relative overflow-hidden ${isActive ? 'border-cyan-500/40 bg-cyan-500/5 shadow-md shadow-cyan-500/5' : 'border-slate-850 bg-slate-950/20'}`}
>
<div>
<div className="text-xs text-slate-400 font-semibold">{coin.name}</div>
<div className="text-xl font-extrabold font-mono mt-1 text-slate-100">{coin.price}</div>
<div className={`text-[10px] font-bold font-mono mt-0.5 flex items-center gap-0.5 ${isUp ? 'text-emerald-400' : 'text-rose-400'}`}>
{isUp ? <ArrowUpRight className="w-3.5 h-3.5" /> : <ArrowDownRight className="w-3.5 h-3.5" />}
<span>{isUp ? '+' : ''}{coin.change24h}%</span>
</div>
</div>
<div className="text-right">
<span className="text-2xl font-bold font-mono text-slate-800">{tick}</span>
</div>
</div>
);
})}
{/* Custom Search bar */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/20 flex flex-col justify-center gap-2">
<form onSubmit={handleAltcoinSearch} className="flex gap-2">
<input
type="text"
required
placeholder="Altcoin Ticker (e.g. LINK)"
className="bg-slate-900 border border-slate-800 rounded-lg p-2 flex-1 text-slate-100 font-mono text-xs uppercase focus:outline-none focus:border-cyan-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
type="submit"
className="bg-slate-800 hover:bg-slate-700 text-cyan-400 hover:text-cyan-350 font-bold px-3 py-2 border border-slate-700 rounded-lg transition-colors text-xs"
>
<Search className="w-4 h-4" />
</button>
</form>
{Object.keys(customCoins).length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-1 overflow-x-auto max-h-[32px]">
{Object.keys(customCoins).map(tick => (
<button
key={tick}
onClick={() => setActiveTicker(tick)}
className={`px-2 py-0.5 rounded font-mono text-[9px] border ${activeTicker === tick ? 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30' : 'bg-slate-900 text-slate-500 border-slate-800'}`}
>
{tick}
</button>
))}
</div>
)}
</div>
</div>
{/* SECTION 2: Derivatives & On-Chain Metrics Ledger */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left Column: Metrics Widgets */}
<div className="xl:col-span-2 space-y-6">
<h3 className="text-base font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
<BarChart2 className="text-cyan-400 w-5 h-5" /> On-Chain &amp; Derivative Indicators ({activeCoin.ticker})
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Funding & Open Interest Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Funding Rates &amp; Open Interest</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Daily Funding Rate:</span>
<span className={`font-bold ${activeCoin.fundingRate < 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.fundingRate > 0 ? '+' : ''}{activeCoin.fundingRate.toFixed(3)}%
</span>
</div>
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Open Interest (24h Δ):</span>
<span className={`font-bold ${activeCoin.openInterestChange >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.openInterestChange > 0 ? '+' : ''}{activeCoin.openInterestChange.toFixed(1)}%
</span>
</div>
</div>
</div>
{/* Long/Short & Liquidation Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Positioning &amp; Liquidations</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Long / Short Ratio:</span>
<span className="text-slate-200 font-bold">{activeCoin.longShortRatio}</span>
</div>
<div className="flex justify-between items-center text-xs font-mono">
<span className="text-slate-400">Liq Cluster:</span>
<span className="text-rose-400">Long: {activeCoin.liqLong} | Short: {activeCoin.liqShort}</span>
</div>
</div>
</div>
{/* Whale Flows Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Whale Flows (Net Inflow)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Net Inflow (Wallets):</span>
<span className={`font-bold ${activeCoin.whaleInflow >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.whaleInflow > 0 ? '+' : ''}{activeCoin.whaleInflow} {activeCoin.ticker}
</span>
</div>
<div className="text-[10px] text-slate-500 leading-relaxed font-sans">
Positive values signal that large investors are withdrawing assets from exchanges to private wallets (accumulation).
</div>
</div>
</div>
{/* Exchange Reserves Widget */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Exchange Reserves (Spot)</h4>
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-mono">
<span className="text-slate-400 text-xs">Reserve Change (7d):</span>
<span className={`font-bold ${activeCoin.exchangeReserves <= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{activeCoin.exchangeReserves > 0 ? '+' : ''}{activeCoin.exchangeReserves}%
</span>
</div>
<div className="text-[10px] text-slate-500 leading-relaxed font-sans">
Falling reserves at spot exchanges reduce available selling pressure and favor squeezes.
</div>
</div>
</div>
</div>
</div>
{/* 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 && (
<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>
<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(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>
{/* Model Calibration Log & Simulation */}
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold text-slate-300 uppercase">Beta-Posterior Calibration</h4>
<span className="text-[10px] text-slate-500 font-mono">Simulate Walk-Forward</span>
</div>
<div className="space-y-2">
<p className="text-[10px] text-slate-400">
Simulate model drift across all 15 independent trackers to calibrate Beta posterior expectations.
</p>
<div className="flex gap-2">
<button
onClick={() => handleSimulateEnsembleTrial(true)}
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
>
+1 Success (All)
</button>
<button
onClick={() => handleSimulateEnsembleTrial(false)}
className="flex-1 bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
>
+1 False Alarm (All)
</button>
</div>
{simulatedTrialLogged && (
<div className="text-[10px] text-cyan-400 font-mono text-center animate-pulse">
Logged trial outcomes across all 15 estimators & horizons!
</div>
)}
</div>
</div>
</div>
</div>
{/* SECTION 3: Active Forecasts (Feedback Loop) */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8 mt-6">
<div className="xl:col-span-2 bg-slate-950/30 rounded-xl p-4 border border-slate-850 space-y-4">
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
<h3 className="text-sm font-bold text-white flex items-center gap-2">
<TrendingUp className="text-cyan-400 w-4 h-4" /> Active Learning Feedback Loop
</h3>
<button
onClick={handleLogManualForecast}
className="bg-cyan-500 hover:bg-cyan-600 text-slate-950 font-bold py-1.5 px-3 rounded-lg text-xs transition-all active:scale-[0.96]"
>
Log Forecast to Feedback Loop
</button>
</div>
{learningLoopLog && (
<div className="p-3 bg-cyan-950/20 border border-cyan-900/40 rounded-lg text-xs text-cyan-300 flex items-center gap-2">
<Info className="w-4 h-4 shrink-0" />
<span>{learningLoopLog}</span>
</div>
)}
<div className="overflow-x-auto max-h-56 scrollbar-thin">
<table className="w-full border-collapse text-left text-xs 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">Entry Price</th>
<th className="p-2">Ensemble T+1</th>
<th className="p-2">Horizons (T1/T5/T10)</th>
<th className="p-2">Status</th>
<th className="p-2 text-right">Success Rate</th>
</tr>
</thead>
<tbody>
{forecasts.length === 0 ? (
<tr>
<td colSpan={6} className="p-4 text-center text-slate-500 italic">No forecasts registered yet.</td>
</tr>
) : (
forecasts.map((fc) => {
let avgT1Prob = 0.5;
if (fc.predictions) {
let sum = 0;
let count = 0;
Object.values(fc.predictions).forEach((hMap) => {
if (hMap && hMap.T1 !== undefined) {
sum += hMap.T1;
count++;
}
});
if (count > 0) avgT1Prob = sum / count;
}
const avgT1Dir = avgT1Prob > 0.5 ? 'UP' : 'DOWN';
const now = Date.now();
const getHorizonStatus = (hKey: 'T1' | 'T5' | 'T10') => {
const targetTime = fc.targetTimes[hKey];
const isPast = now >= targetTime;
let successes = 0;
let total = 0;
ESTIMATORS.forEach((est) => {
const rKey = `${est.id}_${hKey}`;
if (fc.results && fc.results[rKey]) {
total++;
if (fc.results[rKey] === 'SUCCESS') successes++;
}
});
if (total === 5) {
return (
<span className="text-emerald-400 font-bold">
{successes}/5
</span>
);
}
if (isPast) {
return <span className="text-slate-400">Resolving...</span>;
}
const secondsLeft = Math.max(0, Math.ceil((targetTime - now) / 1000));
return <span className="text-slate-500 font-normal">{secondsLeft}s</span>;
};
let resolvedCount = 0;
let successCount = 0;
if (fc.results) {
Object.values(fc.results).forEach((r) => {
resolvedCount++;
if (r === 'SUCCESS') successCount++;
});
}
let statusText = 'PENDING';
if (resolvedCount === 15) {
statusText = 'RESOLVED';
} else if (resolvedCount > 0) {
statusText = `PARTIAL (${resolvedCount}/15)`;
}
return (
<tr key={fc.id} className="border-b border-slate-900 hover:bg-slate-850/10">
<td className="p-2 text-slate-200 font-bold">{fc.ticker}</td>
<td className="p-2 text-slate-350">${fc.entryPrice.toLocaleString()}</td>
<td className="p-2">
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${avgT1Dir === 'UP' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-rose-500/10 text-rose-400'}`}>
{avgT1Dir} {(avgT1Prob * 100).toFixed(0)}%
</span>
</td>
<td className="p-2 text-slate-300">
<div className="flex gap-2 text-[10px]">
<span>T1: {getHorizonStatus('T1')}</span>
<span>T5: {getHorizonStatus('T5')}</span>
<span>T10: {getHorizonStatus('T10')}</span>
</div>
</td>
<td className="p-2 text-slate-400 text-[10px]">{statusText}</td>
<td className="p-2 text-right font-bold text-slate-300">
{resolvedCount > 0 ? (
<span className={successCount / resolvedCount >= 0.5 ? 'text-emerald-400' : 'text-rose-400'}>
{((successCount / resolvedCount) * 100).toFixed(0)}% ({successCount}/{resolvedCount})
</span>
) : (
<span className="text-slate-500">-</span>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Informational overlay */}
<div className="bg-slate-950/40 rounded-xl p-4 border border-slate-850 text-xs text-slate-400 space-y-2">
<h4 className="font-bold text-slate-300 uppercase">Econometric Feedback Loop Spec</h4>
<p className="leading-relaxed">
The learning loop automatically evaluates active forecast parameters in the background against actual price histories returned by <code className="text-cyan-400 font-mono">/api/finance?region=crypto</code>.
</p>
<p className="leading-relaxed">
When a logged forecast passes its evaluation target timestamp, it resolves against live market data, updating the Bayesian online calibration metrics <InlineMath math="\alpha" /> and <InlineMath math="\beta" />.
</p>
</div>
</div>
{/* SECTION 4: Mathematical LaTeX Accordion */}
<div className="border-t border-slate-850 pt-4 mt-6">
<button
onClick={() => setShowMathAccordion(!showMathAccordion)}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-cyan-400 transition-colors focus:outline-none"
>
<span>{showMathAccordion ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}</span>
<span className="font-semibold uppercase tracking-wider">Mathematical Formulation (Beta Update &amp; Random Forest)</span>
</button>
{showMathAccordion && (
<div className="mt-4 p-4 rounded-xl border border-slate-850 bg-slate-950/40 text-xs text-slate-300 space-y-4">
<div>
<h4 className="font-bold text-cyan-400 mb-1">1. Bayesian Beta-Conjugate Error Correction</h4>
<p className="mb-2">
We model the error rate confidence interval of the model using a Beta distribution. The prior error state is represented by the parameters <InlineMath math="\alpha" /> (Successes) and <InlineMath math="\beta" /> (False Alarms):
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P \\sim \\text{Beta}(\\alpha, \\beta) \\quad \\text{with expected value } \\mathbb{E}[P] = \\frac{\\alpha}{\\alpha + \\beta}" />
</div>
<p className="mb-2">
With a new ML signal <InlineMath math="P_{\\text{ML}}" />, we perform a conjugate Bayes update with a confidence weight <InlineMath math="w" />:
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\\alpha_{\\text{post}} = \\alpha + w \\cdot P_{\\text{ML}}, \\quad \\beta_{\\text{post}} = \\beta + w \\cdot (1 - P_{\\text{ML}})" />
</div>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="P_{\\text{Posterior}} = \\frac{\\alpha_{\\text{post}}}{\\alpha_{\\text{post}} + \\beta_{\\text{post}}}" />
</div>
<p className="text-slate-400">
If the model is historically highly unstable (high <InlineMath math="\\beta" />), the Bayesian term corrects an overconfident ML signal downwards, safeguarding the robustness of the system.
</p>
</div>
<div className="border-t border-slate-900 pt-3">
<h4 className="font-bold text-cyan-400 mb-1">2. Random Forest Non-Linear Signal Mapping</h4>
<p className="mb-2">
The Random Forest simulates an ensemble of 10 weak decision trees. Each tree splits the data based on threshold criteria (e.g., 'Funding Rate &lt; -0.04%' and 'Open Interest &gt; 10%'):
</p>
<div className="py-2 overflow-x-auto text-slate-200">
<BlockMath math="\\text{ML}_{\\text{prob}} = \\frac{1}{M} \\sum_{m=1}^{M} T_m(\\mathbf{x})" />
</div>
<p className="text-slate-400">
where <InlineMath math="T_m(\\mathbf{x})" /> is the predicted output value of the <InlineMath math="m" />-th decision tree for the feature vector <InlineMath math="\\mathbf{x}" />.
</p>
</div>
</div>
)}
</div>
<CryptoMathModal isOpen={isMathModalOpen} onClose={() => setIsMathModalOpen(false)} />
</div>
);
}