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

1511 lines
69 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'>;
}
interface BasisArbitrageData {
ticker: string;
spotPrice: number;
futuresPrice: number;
basisSpread: number;
fundingRate: number;
basisApy: number;
}
function formatRemainingTime(seconds: number): string {
if (seconds <= 0) return 'Resolving...';
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const parts: string[] = [];
if (days > 0) {
parts.push(`${days} Tag${days > 1 ? 'e' : ''}`);
}
if (hours > 0) {
parts.push(`${hours} Std`);
}
if (days === 0 && minutes > 0) {
parts.push(`${minutes} Min`);
}
if (days === 0 && hours === 0 && seconds % 60 > 0) {
parts.push(`${seconds % 60} Sek`);
}
return `Verbleibend: ${parts.join(', ')}`;
}
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);
const [feedbackFilterAsset, setFeedbackFilterAsset] = useState<'BTC' | 'ETH' | 'SOL'>('BTC');
const [rightColTab, setRightColTab] = useState<'radar' | 'basis'>('radar');
const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({});
const [showRadarExplanation, setShowRadarExplanation] = useState(false);
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(() => {
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));
}
}, []);
// Mathematical hooks scanning full historical forecasts array inside state/LocalStorage
const globalMetrics = useMemo(() => {
let t1Success = 0, t1Total = 0;
let t5Success = 0, t5Total = 0;
let t10Success = 0, t10Total = 0;
const estimatorHits: Record<string, { hits: number; total: number }> = {
rf: { hits: 0, total: 0 },
gb: { hits: 0, total: 0 },
lr: { hits: 0, total: 0 },
svm: { hits: 0, total: 0 },
mlp: { hits: 0, total: 0 }
};
forecasts.forEach((fc) => {
if (fc.results) {
ESTIMATORS.forEach((est) => {
const rT1 = fc.results?.[`${est.id}_T1`];
const rT5 = fc.results?.[`${est.id}_T5`];
const rT10 = fc.results?.[`${est.id}_T10`];
if (rT1 !== undefined) {
t1Total++;
estimatorHits[est.id].total++;
if (rT1 === 'SUCCESS') {
t1Success++;
estimatorHits[est.id].hits++;
}
}
if (rT5 !== undefined) {
t5Total++;
estimatorHits[est.id].total++;
if (rT5 === 'SUCCESS') {
t5Success++;
estimatorHits[est.id].hits++;
}
}
if (rT10 !== undefined) {
t10Total++;
estimatorHits[est.id].total++;
if (rT10 === 'SUCCESS') {
t10Success++;
estimatorHits[est.id].hits++;
}
}
});
}
});
const t1Rate = t1Total > 0 ? (t1Success / t1Total) * 100 : 0;
const t5Rate = t5Total > 0 ? (t5Success / t5Total) * 100 : 0;
const t10Rate = t10Total > 0 ? (t10Success / t10Total) * 100 : 0;
const estimatorStats = ESTIMATORS.map((est) => {
const stats = estimatorHits[est.id] || { hits: 0, total: 0 };
const rate = stats.total > 0 ? (stats.hits / stats.total) * 100 : 0;
return {
id: est.id,
name: est.name,
hits: stats.hits,
total: stats.total,
rate
};
});
return {
totalLogs: forecasts.length,
resolvedLogs: forecasts.filter(f => f.resolved).length,
t1: { success: t1Success, total: t1Total, rate: t1Rate },
t5: { success: t5Success, total: t5Total, rate: t5Rate },
t10: { success: t10Success, total: t10Total, rate: t10Rate },
estimators: estimatorStats
};
}, [forecasts]);
// 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;
});
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);
}
};
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]);
const filteredForecasts = useMemo(() => {
return forecasts.filter((f) => f.ticker === feedbackFilterAsset);
}, [forecasts, feedbackFilterAsset]);
// Helper to fetch/load prediction probabilities
const getPredictionProb = (ticker: string, estimator: string, horizon: string): number => {
const cleanTicker = ticker.replace('-USD', '');
if (ensemblePredictions && ensemblePredictions[cleanTicker] && ensemblePredictions[cleanTicker][estimator]) {
return ensemblePredictions[cleanTicker][estimator][horizon] ?? 0.5;
}
// Fallback static predictions
const defaultMapping: Record<string, Record<string, Record<string, number>>> = {
BTC: {
rf: { T1: 0.528, T5: 0.539, T10: 0.59 },
gb: { T1: 0.54, T5: 0.513, T10: 0.733 },
lr: { T1: 0.542, T5: 0.595, T10: 0.641 },
svm: { T1: 0.487, T5: 0.477, T10: 0.528 },
mlp: { T1: 0.36, T5: 1.0, T10: 0.998 }
},
ETH: {
rf: { T1: 0.508, T5: 0.549, T10: 0.59 },
gb: { T1: 0.55, T5: 0.513, T10: 0.703 },
lr: { T1: 0.542, T5: 0.575, T10: 0.651 },
svm: { T1: 0.477, T5: 0.477, T10: 0.528 },
mlp: { T1: 0.36, T5: 0.99, T10: 1.018 }
},
SOL: {
rf: { T1: 0.558, T5: 0.539, T10: 0.57 },
gb: { T1: 0.52, T5: 0.533, T10: 0.733 },
lr: { T1: 0.552, T5: 0.595, T10: 0.631 },
svm: { T1: 0.487, T5: 0.507, T10: 0.528 },
mlp: { T1: 0.38, T5: 1.0, T10: 0.978 }
}
};
const assetKey = defaultMapping[cleanTicker] ? cleanTicker : '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 logCoin = customCoins[feedbackFilterAsset] || coins[feedbackFilterAsset] || defaultCoins[feedbackFilterAsset];
const entryPrice = parseFloat(logCoin.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(logCoin.ticker, est.id, 'T1'),
T5: getPredictionProb(logCoin.ticker, est.id, 'T5'),
T10: getPredictionProb(logCoin.ticker, est.id, 'T10')
};
});
const now = Date.now();
const newForecast: Forecast = {
id: 'fc-' + now,
ticker: logCoin.ticker,
entryPrice,
resolved: false,
timestamp: now,
predictions: predictionsMap,
targetTimes: {
T1: now + 24 * 60 * 60 * 1000, // resolves in 24 hours
T5: now + 5 * 24 * 60 * 60 * 1000, // resolves in 5 days
T10: now + 10 * 24 * 60 * 60 * 1000 // resolves in 10 days
}
};
const nextForecasts = [newForecast, ...forecasts];
setForecasts(nextForecasts);
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(nextForecasts));
setLearningLoopLog(`Registered active multi-model forecast for ${logCoin.ticker} at $${entryPrice}. Evaluating T+1 (24h), T+5 (5d), and T+10 (10d).`);
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="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 lg:grid-cols-4 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>
{/* SECTION 2.5: Multi-Model Ensemble & Walk-Forward Radar Table (Centered, Full Width) */}
<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 mt-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-slate-800 pb-3 gap-3">
<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>
<div className="flex items-center gap-3">
{rightColTab === 'radar' && (
<button
onClick={() => setShowRadarExplanation(!showRadarExplanation)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-950 border border-slate-850 hover:bg-slate-900 text-xs font-semibold text-cyan-400 hover:text-cyan-300 transition-all cursor-pointer"
>
<Info className="w-3.5 h-3.5" /> Explain Calibration
</button>
)}
{loadingEnsemble && rightColTab === 'radar' && (
<RefreshCw className="w-4 h-4 text-cyan-400 animate-spin" />
)}
</div>
</div>
{showRadarExplanation && rightColTab === 'radar' && (
<div className="p-4 rounded-xl border border-cyan-950 bg-cyan-950/15 text-xs text-slate-350 space-y-2 animate-fadeIn">
<h5 className="font-bold text-cyan-400 text-sm">Calibration Variable Definitions:</h5>
<p className="leading-relaxed">
<strong>Hit Ratio Counter (Successes vs. Failures)</strong>: Tracks the running count of correct directional predictions (<InlineMath math="\alpha" />) against incorrect ones (<InlineMath math="\beta" />) since initialization.
</p>
<p className="leading-relaxed">
<strong>Bayesian Confidence (<InlineMath math="\\mathbb{E}[\\theta]" />)</strong>: Represents the posterior probability expectation that the model is correct, calculated using conjugate Beta updating:
</p>
<div className="py-1 overflow-x-auto">
<BlockMath math="\\mathbb{E}[\\theta] = \\frac{\\alpha}{\\alpha + \\beta}" />
</div>
<p className="leading-relaxed">
This mathematical calibration dampens overconfident signals when models suffer from historical drift.
</p>
</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-sm md:text-base font-mono">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40 text-xs md:text-sm">
<th className="p-3">Estimator</th>
<th className="p-3 text-center">T+1</th>
<th className="p-3 text-center">T+5</th>
<th className="p-3 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 text-xs md:text-sm">
<td className="p-3 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-3 text-center border-l border-slate-900">
<div className="flex flex-col items-center space-y-1.5 py-1">
<span className={`font-bold text-sm md:text-base ${direction === 'UP' ? 'text-emerald-400' : 'text-rose-400'}`}>
{direction === 'UP' ? '▲ UP' : '▼ DOWN'} {(prob * 100).toFixed(0)}%
</span>
<span className="text-[10px] md:text-xs text-slate-400 bg-slate-950/60 px-2.5 py-0.5 rounded border border-slate-850/80" title="Hit Ratio Counter: Successes / Failures">
Hit Ratio Counter: {tracker.alpha} True / {tracker.beta} Total
</span>
<span className="text-[10px] md:text-xs text-cyan-400 font-bold bg-cyan-950/20 px-2.5 py-0.5 rounded border border-cyan-900/30">
Bayesian Confidence: {(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-xs md:text-sm font-mono">
<thead>
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40 text-xs md:text-sm">
<th className="p-3">Ticker</th>
<th className="p-3">Spot</th>
<th className="p-3">Futures</th>
<th className="p-3 text-right">Spread</th>
<th className="p-3 text-center">Funding</th>
<th className="p-3 text-right">APY</th>
</tr>
</thead>
<tbody>
{basisData.map((data) => {
const isPositive = data.basisSpread >= 0;
const isPositiveApy = data.basisApy >= 0;
return (
<tr key={data.ticker} className="border-b border-slate-900 hover:bg-slate-850/10 text-xs md:text-sm">
<td className="p-3 font-bold text-cyan-400">{data.ticker}</td>
<td className="p-3 text-slate-300">
${data.spotPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td className="p-3 text-slate-300">
${data.futuresPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td className={`p-3 text-right font-semibold ${isPositive ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositive ? '+' : ''}${data.basisSpread.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td className={`p-3 text-center font-mono ${data.fundingRate >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
{data.fundingRate >= 0 ? '+' : ''}{data.fundingRate.toFixed(4)}%
</td>
<td className={`p-3 text-right font-bold ${isPositiveApy ? 'text-emerald-400' : 'text-rose-400'}`}>
{isPositiveApy ? '+' : ''}{data.basisApy.toFixed(2)}%
</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 cursor-pointer"
>
+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 cursor-pointer"
>
+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>
{/* SECTION 3: Active Forecasts (Feedback Loop - Centered, Full Width) */}
<div className="bg-slate-950/30 rounded-xl p-6 border border-slate-850 space-y-4 mt-6">
<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] cursor-pointer"
>
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>
)}
{/* Sleek Dynamic Asset-Selector Tab-Bar */}
<div className="flex items-center gap-1.5 p-1 bg-slate-900/60 border border-slate-800/80 rounded-xl w-fit">
{(['BTC', 'ETH', 'SOL'] as const).map((asset) => (
<button
key={asset}
onClick={() => setFeedbackFilterAsset(asset)}
className={`px-4 py-1.5 rounded-lg text-xs font-mono font-bold transition-all cursor-pointer ${
feedbackFilterAsset === asset
? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/30'
: 'text-slate-400 hover:text-slate-200 border border-transparent'
}`}
>
{asset}
</button>
))}
</div>
<div className="overflow-x-auto max-h-80 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">Log Date/Time</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-center">T+1 Acc</th>
<th className="p-2 text-center">T+5 Acc</th>
<th className="p-2 text-center">T+10 Acc</th>
</tr>
</thead>
<tbody>
{filteredForecasts.length === 0 ? (
<tr>
<td colSpan={10} className="p-4 text-center text-slate-500 italic">No forecasts registered for {feedbackFilterAsset} yet.</td>
</tr>
) : (
filteredForecasts.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">{formatRemainingTime(secondsLeft)}</span>;
};
const renderHorizonCell = (hKey: 'T1' | 'T5' | 'T10') => {
let avgProb = 0.5;
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++;
}
});
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>
</td>
);
};
const getHorizonAccuracy = (hKey: 'T1' | 'T5' | 'T10') => {
let resolvedCountH = 0;
let successCountH = 0;
ESTIMATORS.forEach((est) => {
const rKey = `${est.id}_${hKey}`;
if (fc.results && fc.results[rKey]) {
resolvedCountH++;
if (fc.results[rKey] === 'SUCCESS') {
successCountH++;
}
}
});
if (resolvedCountH > 0) {
const pct = ((successCountH / resolvedCountH) * 100).toFixed(0);
return (
<span className={successCountH / resolvedCountH >= 0.5 ? 'text-emerald-400 font-bold' : 'text-rose-400 font-bold'}>
{pct}% ({successCountH}/{resolvedCountH})
</span>
);
}
return <span className="text-slate-500">-</span>;
};
let resolvedCount = 0;
if (fc.results) {
Object.values(fc.results).forEach(() => {
resolvedCount++;
});
}
let statusText = 'PENDING';
if (resolvedCount === 15) {
statusText = 'RESOLVED';
} else if (resolvedCount > 0) {
statusText = `PARTIAL (${resolvedCount}/15)`;
}
const formattedDate = new Date(fc.timestamp).toLocaleDateString() + ' ' + new Date(fc.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
return (
<React.Fragment key={fc.id}>
<tr
onClick={() => setExpandedRows(prev => ({ ...prev, [fc.id]: !prev[fc.id] }))}
className="border-b border-slate-900 hover:bg-slate-850/20 cursor-pointer transition-colors"
>
<td className="p-2 text-slate-200 font-bold flex items-center gap-1.5 min-w-[90px]">
{expandedRows[fc.id] ? <ChevronUp className="w-3.5 h-3.5 text-cyan-400 shrink-0" /> : <ChevronDown className="w-3.5 h-3.5 text-slate-500 shrink-0" />}
<span>{fc.ticker}</span>
</td>
<td className="p-2 text-slate-450 text-[10px] whitespace-nowrap">{formattedDate}</td>
<td className="p-2 text-slate-350 font-semibold">${fc.entryPrice.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}</td>
{renderHorizonCell('T1')}
{renderHorizonCell('T5')}
{renderHorizonCell('T10')}
<td className="p-2 text-slate-400 text-[10px] font-semibold">{statusText}</td>
<td className="p-2 text-center text-xs">{getHorizonAccuracy('T1')}</td>
<td className="p-2 text-center text-xs">{getHorizonAccuracy('T5')}</td>
<td className="p-2 text-center text-xs">{getHorizonAccuracy('T10')}</td>
</tr>
{expandedRows[fc.id] && (
<tr className="bg-slate-900/40 border-b border-slate-800">
<td colSpan={10} className="p-4">
<div className="space-y-3">
<div className="text-xs font-bold text-cyan-400 uppercase tracking-wider">
Detailed Estimator Forecast Matrix
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-4">
{ESTIMATORS.map((est) => {
return (
<div key={est.id} className="p-3 rounded-lg border border-slate-800 bg-slate-950/60 space-y-2 text-xs">
<div className="font-bold text-slate-200 border-b border-slate-900 pb-1">
{est.name}
</div>
<div className="space-y-1 font-mono text-[10px] md:text-[11px]">
{(['T1', 'T5', 'T10'] as const).map((hKey) => {
const prob = fc.predictions[est.id]?.[hKey] ?? 0.5;
const direction = prob > 0.5 ? 'UP' : 'DOWN';
const rKey = `${est.id}_${hKey}`;
const res = fc.results?.[rKey];
return (
<div key={hKey} className="flex justify-between items-center">
<span className="text-slate-500 font-bold">{hKey}:</span>
<span className="flex items-center gap-1">
<span className={direction === 'UP' ? 'text-emerald-400' : 'text-rose-400'}>
{direction} ({(prob * 100).toFixed(0)}%)
</span>
{res && (
res === 'SUCCESS' ? (
<span className="text-emerald-400 font-bold" title="Correct Forecast"></span>
) : (
<span className="text-rose-500 font-bold" title="Incorrect Forecast"></span>
)
)}
</span>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* SECTION 3.5: All-Time Meta-Statistics Panel (#ISSUE-024-STATS) */}
<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 mt-6">
<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" /> 📈 Global Performance Metrics (All-Time Logs)
</h3>
<span className="text-xs text-slate-400 font-mono bg-slate-950/60 px-3 py-1 rounded-lg border border-slate-850">
Total Logs: <span className="text-cyan-400 font-bold">{globalMetrics.totalLogs}</span>
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Section A: Horizon Efficiency */}
<div className="space-y-4">
<h4 className="text-xs font-bold text-cyan-400 uppercase tracking-wider">
Section A: Horizon Efficiency
</h4>
<p className="text-xs text-slate-400 leading-relaxed">
Cumulative success rates mapped across forecasting horizons for all active models.
</p>
<div className="space-y-3 font-mono">
{/* T+1 */}
<div className="p-3 bg-slate-950/40 rounded-xl border border-slate-850 space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-slate-300 font-semibold">T+1 Horizon Efficiency</span>
<span className="text-emerald-400 font-bold">
{globalMetrics.t1.rate.toFixed(1)}%
</span>
</div>
<div className="w-full bg-slate-800 rounded-full h-1.5">
<div
className="bg-emerald-500 h-1.5 rounded-full transition-all duration-500"
style={{ width: `${Math.min(100, globalMetrics.t1.rate)}%` }}
/>
</div>
<div className="flex justify-between text-[10px] text-slate-500">
<span>Cumulative Hits</span>
<span>{globalMetrics.t1.success} / {globalMetrics.t1.total} Predictions</span>
</div>
</div>
{/* T+5 */}
<div className="p-3 bg-slate-950/40 rounded-xl border border-slate-850 space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-slate-300 font-semibold">T+5 Horizon Efficiency</span>
<span className="text-emerald-400 font-bold">
{globalMetrics.t5.rate.toFixed(1)}%
</span>
</div>
<div className="w-full bg-slate-800 rounded-full h-1.5">
<div
className="bg-emerald-500 h-1.5 rounded-full transition-all duration-500"
style={{ width: `${Math.min(100, globalMetrics.t5.rate)}%` }}
/>
</div>
<div className="flex justify-between text-[10px] text-slate-500">
<span>Cumulative Hits</span>
<span>{globalMetrics.t5.success} / {globalMetrics.t5.total} Predictions</span>
</div>
</div>
{/* T+10 */}
<div className="p-3 bg-slate-950/40 rounded-xl border border-slate-850 space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-slate-300 font-semibold">T+10 Horizon Efficiency</span>
<span className="text-emerald-400 font-bold">
{globalMetrics.t10.rate.toFixed(1)}%
</span>
</div>
<div className="w-full bg-slate-800 rounded-full h-1.5">
<div
className="bg-emerald-500 h-1.5 rounded-full transition-all duration-500"
style={{ width: `${Math.min(100, globalMetrics.t10.rate)}%` }}
/>
</div>
<div className="flex justify-between text-[10px] text-slate-500">
<span>Cumulative Hits</span>
<span>{globalMetrics.t10.success} / {globalMetrics.t10.total} Predictions</span>
</div>
</div>
</div>
</div>
{/* Section B: Estimator Hit Distribution */}
<div className="space-y-4">
<h4 className="text-xs font-bold text-cyan-400 uppercase tracking-wider">
Section B: Estimator Hit Distribution
</h4>
<p className="text-xs text-slate-400 leading-relaxed">
Historical predictive accuracy isolated by model architecture across all timelines.
</p>
<div className="divide-y divide-slate-850 font-mono">
{globalMetrics.estimators.map((est) => (
<div key={est.id} className="py-2.5 flex justify-between items-center text-xs first:pt-0 last:pb-0">
<span className="font-semibold text-slate-300">{est.name}</span>
<div className="flex items-center gap-3">
<span className="text-slate-400">
{est.hits}/{est.total} Hits
</span>
<span className={`px-2 py-0.5 rounded font-bold text-[11px] ${est.rate >= 50 ? 'bg-emerald-950/50 text-emerald-400 border border-emerald-900/40' : 'bg-rose-950/50 text-rose-400 border border-rose-900/40'}`}>
{est.rate.toFixed(1)}%
</span>
</div>
</div>
))}
</div>
</div>
</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>
);
}