1542 lines
71 KiB
TypeScript
1542 lines
71 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useMemo, useEffect } from 'react';
|
||
import { useSandboxStore } from '@/lib/store';
|
||
import { predictCryptoTrend, calculateBetaPosterior } from '@/lib/math/statistics';
|
||
import 'katex/dist/katex.min.css';
|
||
import { BlockMath, InlineMath } from 'react-katex';
|
||
import CryptoMathModal from './CryptoMathModal';
|
||
import CryptoBlueprintModal from './CryptoBlueprintModal';
|
||
import {
|
||
Cpu, Search, RefreshCw, BarChart2, TrendingUp, AlertCircle, Info,
|
||
ChevronDown, ChevronUp, ArrowUpRight, ArrowDownRight, Compass, ShieldAlert, Sparkles,
|
||
BookOpen, Check, EyeOff
|
||
} 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'>;
|
||
isHidden?: boolean;
|
||
}
|
||
|
||
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 const SYSTEM_DATE = 1781714824000; // 2026-06-17T18:47:04+02:00
|
||
|
||
export function isHorizonPending(fcTimestamp: number, hKey: 'T1' | 'T5' | 'T10'): boolean {
|
||
const locks = {
|
||
T1: 24 * 60 * 60 * 1000,
|
||
T5: 5 * 24 * 60 * 60 * 1000,
|
||
T10: 10 * 24 * 60 * 60 * 1000
|
||
};
|
||
const now = SYSTEM_DATE;
|
||
return (now - fcTimestamp) < locks[hKey];
|
||
}
|
||
|
||
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 mockForecasts: Forecast[] = [
|
||
{
|
||
id: 'mock-1',
|
||
ticker: 'BTC',
|
||
entryPrice: 65000,
|
||
resolved: false,
|
||
timestamp: SYSTEM_DATE - 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: SYSTEM_DATE - 86400 * 1000 * 2,
|
||
T5: SYSTEM_DATE + 86400 * 1000 * 2,
|
||
T10: SYSTEM_DATE + 86400 * 1000 * 7
|
||
},
|
||
results: {
|
||
rf_T1: 'SUCCESS', gb_T1: 'SUCCESS', lr_T1: 'SUCCESS', svm_T1: 'SUCCESS', mlp_T1: 'SUCCESS'
|
||
}
|
||
}
|
||
];
|
||
setForecasts(mockForecasts);
|
||
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(mockForecasts));
|
||
}
|
||
} else {
|
||
const mockForecasts: Forecast[] = [
|
||
{
|
||
id: 'mock-1',
|
||
ticker: 'BTC',
|
||
entryPrice: 65000,
|
||
resolved: false,
|
||
timestamp: SYSTEM_DATE - 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: SYSTEM_DATE - 86400 * 1000 * 2,
|
||
T5: SYSTEM_DATE + 86400 * 1000 * 2,
|
||
T10: SYSTEM_DATE + 86400 * 1000 * 7
|
||
},
|
||
results: {
|
||
rf_T1: 'SUCCESS', gb_T1: 'SUCCESS', lr_T1: 'SUCCESS', svm_T1: 'SUCCESS', mlp_T1: '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 = !isHorizonPending(fc.timestamp, 'T1') ? fc.results?.[`${est.id}_T1`] : undefined;
|
||
const rT5 = !isHorizonPending(fc.timestamp, 'T5') ? fc.results?.[`${est.id}_T5`] : undefined;
|
||
const rT10 = !isHorizonPending(fc.timestamp, 'T10') ? fc.results?.[`${est.id}_T10`] : undefined;
|
||
|
||
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) => {
|
||
const isFullyResolved = ESTIMATORS.every(est =>
|
||
HORIZONS.every(h => f.results && f.results[`${est.id}_${h.id}`] !== undefined && !isHorizonPending(f.timestamp, h.id))
|
||
);
|
||
if (isFullyResolved) return f;
|
||
|
||
const currentPrice = pricesMap[f.ticker] || pricesMap[`${f.ticker}-USD`];
|
||
if (!currentPrice) return f;
|
||
|
||
const now = SYSTEM_DATE;
|
||
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] && !isHorizonPending(f.timestamp, hKey)) {
|
||
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 && !isHorizonPending(f.timestamp, h.id))
|
||
);
|
||
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 & 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 & 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 & 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 & Res</th>
|
||
<th className="p-2 text-center">T+5 Forecast & Res</th>
|
||
<th className="p-2 text-center">T+10 Forecast & 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>
|
||
<th className="p-2 text-center w-[50px]">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{(() => {
|
||
const visibleForecasts = filteredForecasts.filter(fc => !fc.isHidden);
|
||
if (visibleForecasts.length === 0) {
|
||
return (
|
||
<tr>
|
||
<td colSpan={11} className="p-4 text-center text-slate-500 italic">No forecasts registered for {feedbackFilterAsset} yet.</td>
|
||
</tr>
|
||
);
|
||
}
|
||
return visibleForecasts.map((fc) => {
|
||
const now = SYSTEM_DATE;
|
||
|
||
const getHorizonStatus = (hKey: 'T1' | 'T5' | 'T10') => {
|
||
const targetTime = fc.targetTimes[hKey];
|
||
const pending = isHorizonPending(fc.timestamp, hKey);
|
||
|
||
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 && !pending) {
|
||
return (
|
||
<span className="text-emerald-400 font-bold">
|
||
{successes}/5 OK
|
||
</span>
|
||
);
|
||
}
|
||
if (now >= targetTime && !pending) {
|
||
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') => {
|
||
if (isHorizonPending(fc.timestamp, hKey)) {
|
||
return <span className="text-slate-500">-</span>;
|
||
}
|
||
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.keys(fc.results).forEach((rKey) => {
|
||
const hKey = rKey.endsWith('_T1') ? 'T1' : rKey.endsWith('_T5') ? 'T5' : 'T10';
|
||
if (!isHorizonPending(fc.timestamp, hKey)) {
|
||
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>
|
||
<td className="p-2 text-center">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
const updated = forecasts.map(item => item.id === fc.id ? { ...item, isHidden: true } : item);
|
||
setForecasts(updated);
|
||
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(updated));
|
||
}}
|
||
className="text-slate-500 hover:text-rose-400 transition-colors p-1 cursor-pointer"
|
||
title="Hide Row"
|
||
>
|
||
<EyeOff className="w-3.5 h-3.5" />
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{expandedRows[fc.id] && (
|
||
<tr className="bg-slate-900/40 border-b border-slate-800">
|
||
<td colSpan={11} 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 && !isHorizonPending(fc.timestamp, hKey) && (
|
||
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 & 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 < -0.04%' and 'Open Interest > 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>
|
||
);
|
||
}
|