Files
investment-sandbox/components/modules/crypto/CryptoDemo.tsx

1098 lines
49 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 CryptoBlueprintModal from './CryptoBlueprintModal';
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 [isBlueprintModalOpen, setIsBlueprintModalOpen] = 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);
const [isShieldActive, setIsShieldActive] = useState(true);
const [coins, setCoins] = useState<Record<string, CoinData>>(defaultCoins);
// 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);
setIsShieldActive(data.isShieldActive !== undefined ? data.isShieldActive : true);
}
} 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));
}
}, []);
// Poll live price, 24h change, and indicators from the backend API
useEffect(() => {
const fetchCryptoPrices = async () => {
try {
const res = await fetch('/api/finance?region=crypto');
if (!res.ok) return;
const data = await res.json();
const results = data.results || [];
setCoins(prevCoins => {
const updatedCoins = { ...prevCoins };
results.forEach((r: any) => {
const cleanTicker = r.ticker.replace('-USD', '');
if (cleanTicker === 'BTC' || cleanTicker === 'ETH' || cleanTicker === 'SOL') {
const currentPrice = r.currentPrice;
const dayChangePercent = r.dayChange * 100;
// Bind API indicators directly
const fundingRate = r.fundingRate !== undefined ? r.fundingRate : (cleanTicker === 'BTC' ? -0.015 : cleanTicker === 'ETH' ? 0.045 : 0.082);
const openInterestChange = r.openInterestChange !== undefined ? r.openInterestChange : (cleanTicker === 'BTC' ? 8.2 : cleanTicker === 'ETH' ? -3.5 : 14.5);
const longShortRatio = r.longShortRatio !== undefined ? r.longShortRatio : (cleanTicker === 'BTC' ? 0.92 : cleanTicker === 'ETH' ? 1.34 : 1.62);
const whaleInflow = r.whaleInflow !== undefined ? r.whaleInflow : (cleanTicker === 'BTC' ? 480 : cleanTicker === 'ETH' ? -120 : 1250);
const exchangeReserves = r.exchangeReserves !== undefined ? r.exchangeReserves : (cleanTicker === 'BTC' ? -1.4 : cleanTicker === 'ETH' ? 0.8 : -2.8);
// Scale liquidations dynamically relative to the current real price
const liqLongVal = currentPrice * (cleanTicker === 'BTC' ? 0.982 : cleanTicker === 'ETH' ? 0.971 : 0.955);
const liqShortVal = currentPrice * (cleanTicker === 'BTC' ? 1.015 : cleanTicker === 'ETH' ? 1.026 : 1.045);
updatedCoins[cleanTicker] = {
ticker: cleanTicker,
name: cleanTicker === 'BTC' ? 'Bitcoin' : cleanTicker === 'ETH' ? 'Ethereum' : 'Solana',
price: `$${currentPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
change24h: parseFloat(dayChangePercent.toFixed(2)),
fundingRate,
openInterestChange,
longShortRatio,
whaleInflow,
exchangeReserves,
liqLong: `$${liqLongVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
liqShort: `$${liqShortVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
};
}
});
return updatedCoins;
});
} catch (err) {
console.error("Failed to fetch crypto prices:", err);
}
};
fetchCryptoPrices();
const interval = setInterval(fetchCryptoPrices, 15000); // Poll every 15s
return () => clearInterval(interval);
}, []);
// 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] || coins[activeTicker] || coins['BTC'];
}, [activeTicker, customCoins, coins]);
// 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 (coins[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">
{isShieldActive ? (
<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 animate-pulse">
<ShieldAlert className="w-4 h-4 text-amber-400" />
<span>SYSTEM-AUTARK (OFFLINE-CORE)</span>
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-3 py-2.5 rounded-xl text-xs font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 shadow-[0_0_15px_rgba(16,185,129,0.15)] h-11">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-ping" />
<span>LIVE-API ENDPUNKT</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>
<button
onClick={() => setIsBlueprintModalOpen(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"
>
<Cpu className="w-3.5 h-3.5" />
<span> Operational Blueprint</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 = coins[tick] || 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 text-center">T+1 Forecast &amp; Res</th>
<th className="p-2 text-center">T+5 Forecast &amp; Res</th>
<th className="p-2 text-center">T+10 Forecast &amp; Res</th>
<th className="p-2">Status</th>
<th className="p-2 text-right">Accuracy</th>
</tr>
</thead>
<tbody>
{forecasts.length === 0 ? (
<tr>
<td colSpan={7} className="p-4 text-center text-slate-500 italic">No forecasts registered yet.</td>
</tr>
) : (
forecasts.map((fc) => {
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 OK
</span>
);
}
if (isPast) {
return <span className="text-cyan-400 animate-pulse">Resolving...</span>;
}
const secondsLeft = Math.max(0, Math.ceil((targetTime - now) / 1000));
return <span className="text-slate-500 font-normal">{secondsLeft}s</span>;
};
const renderHorizonCell = (hKey: 'T1' | 'T5' | 'T10') => {
let avgProb = 0.5;
const modelProbs: string[] = [];
if (fc.predictions) {
let sum = 0;
let count = 0;
ESTIMATORS.forEach((est) => {
const prob = fc.predictions[est.id]?.[hKey];
if (prob !== undefined) {
sum += prob;
count++;
modelProbs.push(`${est.id.toUpperCase()}:${(prob * 100).toFixed(0)}%`);
}
});
if (count > 0) avgProb = sum / count;
}
const avgDir = avgProb > 0.5 ? 'UP' : 'DOWN';
return (
<td className="p-2 text-center border-r border-slate-900/40 last:border-r-0">
<div className="flex flex-col items-center gap-1">
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${avgDir === 'UP' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-rose-500/10 text-rose-400'}`}>
{avgDir} {(avgProb * 100).toFixed(0)}%
</span>
<div className="text-[10px] font-semibold">
{getHorizonStatus(hKey)}
</div>
<div className="text-[8px] text-slate-500 flex flex-wrap justify-center gap-1 max-w-[130px] leading-tight font-sans">
{modelProbs.join(' | ')}
</div>
</div>
</td>
);
};
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>
{renderHorizonCell('T1')}
{renderHorizonCell('T5')}
{renderHorizonCell('T10')}
<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)} />
<CryptoBlueprintModal isOpen={isBlueprintModalOpen} onClose={() => setIsBlueprintModalOpen(false)} />
</div>
);
}