Closes #015 - Deploy Multi-Model Ensemble & Walk-Forward Radar
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
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';
|
||||
@@ -12,6 +12,26 @@ import {
|
||||
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;
|
||||
@@ -71,13 +91,12 @@ const defaultCoins: Record<string, CoinData> = {
|
||||
interface Forecast {
|
||||
id: string;
|
||||
ticker: string;
|
||||
predictedDirection: 'UP' | 'DOWN';
|
||||
predictedProb: number;
|
||||
entryPrice: number;
|
||||
resolved: boolean;
|
||||
result?: 'SUCCESS' | 'FAILURE';
|
||||
timestamp: number;
|
||||
targetTime: number;
|
||||
predictions: Record<string, Record<string, number>>;
|
||||
targetTimes: Record<string, number>;
|
||||
results?: Record<string, 'SUCCESS' | 'FAILURE'>;
|
||||
}
|
||||
|
||||
export default function CryptoDemo() {
|
||||
@@ -98,8 +117,13 @@ export default function CryptoDemo() {
|
||||
const [simulatedTrialLogged, setSimulatedTrialLogged] = useState(false);
|
||||
const [lastTrialSuccess, setLastTrialSuccess] = useState(false);
|
||||
|
||||
// 15 independent Beta-Posterior trackers
|
||||
const [trackers, setTrackers] = useState<TrackersMap>({});
|
||||
const [ensemblePredictions, setEnsemblePredictions] = useState<any>(null);
|
||||
const [loadingEnsemble, setLoadingEnsemble] = useState(false);
|
||||
|
||||
// Safely load counters and forecasts from localStorage on client mount
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const savedAlpha = localStorage.getItem('crypto_bayes_alpha');
|
||||
const savedBeta = localStorage.getItem('crypto_bayes_beta');
|
||||
const savedForecasts = localStorage.getItem('crypto_bayes_forecasts');
|
||||
@@ -121,65 +145,116 @@ export default function CryptoDemo() {
|
||||
localStorage.setItem('crypto_bayes_beta', '118');
|
||||
}
|
||||
|
||||
// Load trackers
|
||||
const defaultPriors: Record<string, { alpha: number; beta: number }> = {
|
||||
'rf_T1': { alpha: 38, beta: 12 }, 'rf_T5': { alpha: 35, beta: 15 }, 'rf_T10': { alpha: 32, beta: 18 },
|
||||
'gb_T1': { alpha: 40, beta: 10 }, 'gb_T5': { alpha: 36, beta: 14 }, 'gb_T10': { alpha: 30, beta: 20 },
|
||||
'lr_T1': { alpha: 35, beta: 15 }, 'lr_T5': { alpha: 33, beta: 17 }, 'lr_T10': { alpha: 31, beta: 19 },
|
||||
'svm_T1': { alpha: 36, beta: 14 }, 'svm_T5': { alpha: 34, beta: 16 }, 'svm_T10': { alpha: 32, beta: 18 },
|
||||
'mlp_T1': { alpha: 39, beta: 11 }, 'mlp_T5': { alpha: 35, beta: 15 }, 'mlp_T10': { alpha: 31, beta: 19 }
|
||||
};
|
||||
const map: TrackersMap = {};
|
||||
Object.keys(defaultPriors).forEach((key) => {
|
||||
const a = localStorage.getItem(`crypto_bayes_tracker_${key}_alpha`);
|
||||
const b = localStorage.getItem(`crypto_bayes_tracker_${key}_beta`);
|
||||
const alphaVal = a !== null ? parseInt(a, 10) : defaultPriors[key].alpha;
|
||||
const betaVal = b !== null ? parseInt(b, 10) : defaultPriors[key].beta;
|
||||
map[key] = { alpha: alphaVal, beta: betaVal };
|
||||
|
||||
if (a === null) {
|
||||
localStorage.setItem(`crypto_bayes_tracker_${key}_alpha`, String(alphaVal));
|
||||
localStorage.setItem(`crypto_bayes_tracker_${key}_beta`, String(betaVal));
|
||||
}
|
||||
});
|
||||
setTrackers(map);
|
||||
|
||||
// Fetch ensemble predictions
|
||||
const fetchEnsemble = async () => {
|
||||
setLoadingEnsemble(true);
|
||||
try {
|
||||
const res = await fetch('/api/crypto/ensemble');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setEnsemblePredictions(data.predictions || null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load ensemble predictions:", err);
|
||||
} finally {
|
||||
setLoadingEnsemble(false);
|
||||
}
|
||||
};
|
||||
fetchEnsemble();
|
||||
|
||||
if (savedForecasts !== null) {
|
||||
setForecasts(JSON.parse(savedForecasts));
|
||||
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',
|
||||
predictedDirection: 'UP',
|
||||
predictedProb: 0.68,
|
||||
entryPrice: 65000,
|
||||
resolved: true,
|
||||
result: 'SUCCESS',
|
||||
timestamp: now - 86400 * 1000 * 3,
|
||||
targetTime: now - 86400 * 1000 * 2,
|
||||
},
|
||||
{
|
||||
id: 'mock-2',
|
||||
ticker: 'ETH',
|
||||
predictedDirection: 'DOWN',
|
||||
predictedProb: 0.35,
|
||||
entryPrice: 3950,
|
||||
resolved: true,
|
||||
result: 'SUCCESS',
|
||||
timestamp: now - 86400 * 1000 * 3,
|
||||
targetTime: now - 86400 * 1000 * 2,
|
||||
},
|
||||
{
|
||||
id: 'mock-3',
|
||||
ticker: 'SOL',
|
||||
predictedDirection: 'UP',
|
||||
predictedProb: 0.72,
|
||||
entryPrice: 170,
|
||||
resolved: true,
|
||||
result: 'SUCCESS',
|
||||
timestamp: now - 86400 * 1000 * 2,
|
||||
targetTime: now - 86400 * 1000 * 1,
|
||||
},
|
||||
{
|
||||
id: 'mock-4',
|
||||
ticker: 'BTC',
|
||||
predictedDirection: 'UP',
|
||||
predictedProb: 0.62,
|
||||
entryPrice: 71000,
|
||||
resolved: true,
|
||||
result: 'FAILURE',
|
||||
timestamp: now - 86400 * 1000 * 2,
|
||||
targetTime: now - 86400 * 1000 * 1,
|
||||
},
|
||||
{
|
||||
id: 'mock-5',
|
||||
ticker: 'ETH',
|
||||
predictedDirection: 'UP',
|
||||
predictedProb: 0.58,
|
||||
entryPrice: 3900,
|
||||
resolved: true,
|
||||
result: 'FAILURE',
|
||||
timestamp: now - 86400 * 1000 * 2,
|
||||
targetTime: now - 86400 * 1000 * 1,
|
||||
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);
|
||||
@@ -188,12 +263,13 @@ export default function CryptoDemo() {
|
||||
}, []);
|
||||
|
||||
// Client-side background learning loop evaluating forecasts against actual live returns
|
||||
React.useEffect(() => {
|
||||
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.ok ? await res.json() : { results: [] };
|
||||
const data = await res.json();
|
||||
const results = data.results || [];
|
||||
|
||||
const pricesMap: Record<string, number> = {};
|
||||
@@ -204,8 +280,7 @@ export default function CryptoDemo() {
|
||||
});
|
||||
|
||||
let updatedAny = false;
|
||||
let newAlpha = alphaSuccess;
|
||||
let newBeta = betaFailure;
|
||||
const nextTrackers = { ...trackers };
|
||||
|
||||
const updatedForecasts = forecasts.map((f) => {
|
||||
if (f.resolved) return f;
|
||||
@@ -214,36 +289,56 @@ export default function CryptoDemo() {
|
||||
if (!currentPrice) return f;
|
||||
|
||||
const now = Date.now();
|
||||
if (now >= f.targetTime) {
|
||||
const priceWentUp = currentPrice > f.entryPrice;
|
||||
const success = (f.predictedDirection === 'UP' && priceWentUp) || (f.predictedDirection === 'DOWN' && !priceWentUp);
|
||||
|
||||
updatedAny = true;
|
||||
if (success) {
|
||||
newAlpha += 1;
|
||||
} else {
|
||||
newBeta += 1;
|
||||
}
|
||||
|
||||
addModelTrial(success);
|
||||
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,
|
||||
resolved: true,
|
||||
result: success ? ('SUCCESS' as const) : ('FAILURE' as const)
|
||||
results: resultsMap,
|
||||
resolved: allResolved
|
||||
};
|
||||
}
|
||||
return f;
|
||||
});
|
||||
|
||||
if (updatedAny) {
|
||||
setAlphaSuccess(newAlpha);
|
||||
setBetaFailure(newBeta);
|
||||
setTrackers(nextTrackers);
|
||||
setForecasts(updatedForecasts);
|
||||
localStorage.setItem('crypto_bayes_alpha', String(newAlpha));
|
||||
localStorage.setItem('crypto_bayes_beta', String(newBeta));
|
||||
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(updatedForecasts));
|
||||
setLearningLoopLog(`Processed active forecasts. New successes: ${newAlpha}, New failures: ${newBeta}`);
|
||||
setLearningLoopLog(`Processed active ensemble forecasts. Trackers calibration updated.`);
|
||||
setTimeout(() => setLearningLoopLog(''), 6000);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -256,14 +351,47 @@ export default function CryptoDemo() {
|
||||
}
|
||||
const interval = setInterval(runLearningLoop, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [forecasts, alphaSuccess, betaFailure, addModelTrial]);
|
||||
}, [forecasts, trackers, addModelTrial]);
|
||||
|
||||
// Active Coin data retrieval
|
||||
const activeCoin = useMemo(() => {
|
||||
return customCoins[activeTicker] || defaultCoins[activeTicker] || defaultCoins['BTC'];
|
||||
}, [activeTicker, customCoins]);
|
||||
|
||||
// Compute live Random Forest baseline predictions
|
||||
// Helper to fetch/load prediction probabilities
|
||||
const getPredictionProb = (estimator: string, horizon: string): number => {
|
||||
if (ensemblePredictions && ensemblePredictions[activeTicker] && ensemblePredictions[activeTicker][estimator]) {
|
||||
return ensemblePredictions[activeTicker][estimator][horizon] ?? 0.5;
|
||||
}
|
||||
// Fallback static predictions
|
||||
const defaultMapping: Record<string, Record<string, Record<string, number>>> = {
|
||||
BTC: {
|
||||
rf: { T1: 0.62, T5: 0.58, T10: 0.54 },
|
||||
gb: { T1: 0.65, T5: 0.61, T10: 0.51 },
|
||||
lr: { T1: 0.58, T5: 0.57, T10: 0.55 },
|
||||
svm: { T1: 0.60, T5: 0.59, T10: 0.56 },
|
||||
mlp: { T1: 0.64, T5: 0.60, T10: 0.53 }
|
||||
},
|
||||
ETH: {
|
||||
rf: { T1: 0.60, T5: 0.59, T10: 0.54 },
|
||||
gb: { T1: 0.66, T5: 0.61, T10: 0.48 },
|
||||
lr: { T1: 0.58, T5: 0.55, T10: 0.56 },
|
||||
svm: { T1: 0.59, T5: 0.59, T10: 0.56 },
|
||||
mlp: { T1: 0.64, T5: 0.59, T10: 0.55 }
|
||||
},
|
||||
SOL: {
|
||||
rf: { T1: 0.65, T5: 0.58, T10: 0.52 },
|
||||
gb: { T1: 0.63, T5: 0.63, T10: 0.54 },
|
||||
lr: { T1: 0.59, T5: 0.58, T10: 0.54 },
|
||||
svm: { T1: 0.60, T5: 0.62, T10: 0.56 },
|
||||
mlp: { T1: 0.66, T5: 0.60, T10: 0.51 }
|
||||
}
|
||||
};
|
||||
const assetKey = defaultMapping[activeTicker] ? activeTicker : 'BTC';
|
||||
return defaultMapping[assetKey][estimator]?.[horizon] ?? 0.5;
|
||||
};
|
||||
|
||||
// Compute live Random Forest baseline predictions (for legacy/visual compatibility)
|
||||
const mlPredictions = useMemo(() => {
|
||||
const inputs = {
|
||||
fundingRate: activeCoin.fundingRate,
|
||||
@@ -274,7 +402,7 @@ export default function CryptoDemo() {
|
||||
return predictCryptoTrend(inputs);
|
||||
}, [activeCoin]);
|
||||
|
||||
// Apply Bayesian online learning error-correction posterior update
|
||||
// Apply Bayesian online learning error-correction posterior update (legacy/visual)
|
||||
const correctedPredictions = useMemo(() => {
|
||||
const shortTermCorrected = calculateBetaPosterior(
|
||||
alphaSuccess,
|
||||
@@ -337,47 +465,66 @@ export default function CryptoDemo() {
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
// Manual logging of active forecast
|
||||
// Manual logging of active forecast for all 15 models & horizons
|
||||
const handleLogManualForecast = () => {
|
||||
const entryPrice = parseFloat(activeCoin.price.replace(/[^0-9.]/g, ''));
|
||||
const predictedDirection = correctedPredictions.shortTerm > 0.5 ? 'UP' : 'DOWN';
|
||||
const predictedProb = correctedPredictions.shortTerm;
|
||||
|
||||
// Save snapshot of all predictions
|
||||
const predictionsMap: Record<string, Record<string, number>> = {};
|
||||
ESTIMATORS.forEach((est) => {
|
||||
predictionsMap[est.id] = {
|
||||
T1: getPredictionProb(est.id, 'T1'),
|
||||
T5: getPredictionProb(est.id, 'T5'),
|
||||
T10: getPredictionProb(est.id, 'T10')
|
||||
};
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const newForecast: Forecast = {
|
||||
id: 'fc-' + Date.now(),
|
||||
id: 'fc-' + now,
|
||||
ticker: activeCoin.ticker,
|
||||
predictedDirection,
|
||||
predictedProb,
|
||||
entryPrice,
|
||||
resolved: false,
|
||||
timestamp: Date.now(),
|
||||
targetTime: Date.now() + 60 * 1000 // resolves in 60s for direct visual validation
|
||||
timestamp: now,
|
||||
predictions: predictionsMap,
|
||||
targetTimes: {
|
||||
T1: now + 60 * 1000, // resolves in 60s for direct visual validation
|
||||
T5: now + 300 * 1000, // resolves in 300s
|
||||
T10: now + 600 * 1000 // resolves in 600s
|
||||
}
|
||||
};
|
||||
|
||||
const nextForecasts = [newForecast, ...forecasts];
|
||||
setForecasts(nextForecasts);
|
||||
localStorage.setItem('crypto_bayes_forecasts', JSON.stringify(nextForecasts));
|
||||
|
||||
setLearningLoopLog(`Registered active forecast for ${activeCoin.ticker} at $${entryPrice}. Evaluating returns in 60 seconds.`);
|
||||
setTimeout(() => setLearningLoopLog(''), 6000);
|
||||
setLearningLoopLog(`Registered active multi-model forecast for ${activeCoin.ticker} at $${entryPrice}. Evaluating T+1 (60s), T+5 (5m), and T+10 (10m).`);
|
||||
setTimeout(() => setLearningLoopLog(''), 8000);
|
||||
};
|
||||
|
||||
// Simulator for calibration
|
||||
const handleSimulateTrial = (success: boolean) => {
|
||||
addModelTrial(success);
|
||||
setAlphaSuccess(prev => {
|
||||
const next = success ? prev + 1 : prev;
|
||||
localStorage.setItem('crypto_bayes_alpha', String(next));
|
||||
return next;
|
||||
});
|
||||
setBetaFailure(prev => {
|
||||
const next = !success ? prev + 1 : prev;
|
||||
localStorage.setItem('crypto_bayes_beta', String(next));
|
||||
return next;
|
||||
// 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), 2500);
|
||||
setTimeout(() => setSimulatedTrialLogged(false), 2000);
|
||||
};
|
||||
|
||||
const totalTrials = alphaSuccess + betaFailure;
|
||||
@@ -564,94 +711,92 @@ export default function CryptoDemo() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Predictive Gauges & Correction Calibration */}
|
||||
{/* Right Column: Multi-Model Ensemble & Walk-Forward Radar Table */}
|
||||
<div className="bg-slate-900/60 backdrop-blur-md border border-slate-800 rounded-2xl p-6 text-slate-100 shadow-xl space-y-6">
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-3">
|
||||
<Compass className="text-cyan-400 w-5 h-5" /> Prediction Probabilities
|
||||
</h3>
|
||||
<div className="flex justify-between items-center border-b border-slate-800 pb-3">
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Compass className="text-cyan-400 w-5 h-5" /> Walk-Forward Ensemble Radar
|
||||
</h3>
|
||||
{loadingEnsemble && (
|
||||
<RefreshCw className="w-4 h-4 text-cyan-400 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Gauges / Progress Bars */}
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* 24h Gauge */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs font-semibold">
|
||||
<span className="text-slate-300">24h Volatility Squeeze (Short-Term)</span>
|
||||
<span className="text-cyan-400 font-mono">{(correctedPredictions.shortTerm * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-950 rounded-full h-3 overflow-hidden border border-slate-850 flex">
|
||||
<div
|
||||
className="bg-cyan-500 h-full rounded-l transition-all duration-500 opacity-30"
|
||||
style={{ width: `${mlPredictions.shortTermProb * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-cyan-400 h-full rounded-r transition-all duration-500 -ml-[20%] shadow-[0_0_10px_rgba(34,211,238,0.5)]"
|
||||
style={{ width: `${correctedPredictions.shortTerm * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
|
||||
<span>ML Signal: {(mlPredictions.shortTermProb * 100).toFixed(0)}%</span>
|
||||
<span className="text-cyan-400">Bayes Corrected: {(correctedPredictions.shortTerm * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 14d Gauge */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs font-semibold">
|
||||
<span className="text-slate-300">14d Structural Bullish Trend (Medium-Term)</span>
|
||||
<span className="text-teal-400 font-mono">{(correctedPredictions.mediumTerm * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-950 rounded-full h-3 overflow-hidden border border-slate-850 flex">
|
||||
<div
|
||||
className="bg-teal-500 h-full rounded-l transition-all duration-500 opacity-30"
|
||||
style={{ width: `${mlPredictions.mediumTermProb * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-teal-400 h-full rounded-r transition-all duration-500 -ml-[20%] shadow-[0_0_10px_rgba(45,212,191,0.5)]"
|
||||
style={{ width: `${correctedPredictions.mediumTerm * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-[9px] text-slate-500 font-mono">
|
||||
<span>ML Signal: {(mlPredictions.mediumTermProb * 100).toFixed(0)}%</span>
|
||||
<span className="text-teal-400">Bayes Corrected: {(correctedPredictions.mediumTerm * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 leading-relaxed">
|
||||
Displays predictions and live calibration metrics (<InlineMath math="E[\theta] = \alpha / (\alpha + \beta)" />) across 15 independent trackers.
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-slate-850 bg-slate-950/40">
|
||||
<table className="w-full border-collapse text-left text-[11px] font-mono">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
|
||||
<th className="p-2">Estimator</th>
|
||||
<th className="p-2 text-center">T+1</th>
|
||||
<th className="p-2 text-center">T+5</th>
|
||||
<th className="p-2 text-center">T+10</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ESTIMATORS.map((est) => (
|
||||
<tr key={est.id} className="border-b border-slate-900 hover:bg-slate-850/10">
|
||||
<td className="p-2 font-semibold text-slate-300">{est.name}</td>
|
||||
{HORIZONS.map((h) => {
|
||||
const trackerKey = `${est.id}_${h.id}`;
|
||||
const tracker = trackers[trackerKey] || { alpha: 1, beta: 1 };
|
||||
const prob = getPredictionProb(est.id, h.id);
|
||||
const direction = prob > 0.5 ? 'UP' : 'DOWN';
|
||||
const expValue = tracker.alpha / (tracker.alpha + tracker.beta);
|
||||
|
||||
return (
|
||||
<td key={h.id} className="p-2 text-center border-l border-slate-900">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`font-bold ${direction === 'UP' ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||
{direction === 'UP' ? '▲' : '▼'} {(prob * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-500 mt-0.5">
|
||||
{tracker.alpha}/{tracker.beta}
|
||||
</span>
|
||||
<span className="text-[9px] text-cyan-400 font-semibold mt-0.5">
|
||||
E: {(expValue * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Model Calibration Log & Simulation */}
|
||||
<div className="p-4 rounded-xl border border-slate-850 bg-slate-950/40 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-xs font-bold text-slate-300 uppercase">Bayes Model Calibration</h4>
|
||||
<span className="text-[10px] text-slate-500 font-mono">n = {totalTrials} Trials</span>
|
||||
<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="grid grid-cols-2 gap-2 text-xs font-mono pb-2 border-b border-slate-900">
|
||||
<div className="text-slate-400">Successes (α):</div>
|
||||
<div className="text-emerald-400 font-bold">{alphaSuccess}</div>
|
||||
<div className="text-slate-400">False Alarms (β):</div>
|
||||
<div className="text-rose-400 font-bold">{betaFailure}</div>
|
||||
</div>
|
||||
|
||||
{/* Trial simulator */}
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] text-slate-400">Simulate model drift: Add correct/false outcomes to calibrate the Beta distribution.</p>
|
||||
<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={() => handleSimulateTrial(true)}
|
||||
onClick={() => handleSimulateEnsembleTrial(true)}
|
||||
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
|
||||
>
|
||||
+1 Success
|
||||
+1 Success (All)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSimulateTrial(false)}
|
||||
onClick={() => handleSimulateEnsembleTrial(false)}
|
||||
className="flex-1 bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 py-1.5 rounded-lg text-xs font-semibold font-mono"
|
||||
>
|
||||
+1 False Alarm
|
||||
+1 False Alarm (All)
|
||||
</button>
|
||||
</div>
|
||||
{simulatedTrialLogged && (
|
||||
<div className="text-[10px] text-cyan-400 font-mono text-center animate-pulse">
|
||||
Trial logged! Bayes prior updated to {lastTrialSuccess ? 'Success' : 'False Alarm'}.
|
||||
Logged trial outcomes across all 15 estimators & horizons!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -686,11 +831,11 @@ export default function CryptoDemo() {
|
||||
<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">Direction</th>
|
||||
<th className="p-2">Probability</th>
|
||||
<th className="p-2">Entry Price</th>
|
||||
<th className="p-2">Ensemble T+1</th>
|
||||
<th className="p-2">Horizons (T1/T5/T10)</th>
|
||||
<th className="p-2">Status</th>
|
||||
<th className="p-2 text-right">Result</th>
|
||||
<th className="p-2 text-right">Success Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -699,22 +844,95 @@ export default function CryptoDemo() {
|
||||
<td colSpan={6} className="p-4 text-center text-slate-500 italic">No forecasts registered yet.</td>
|
||||
</tr>
|
||||
) : (
|
||||
forecasts.map((fc) => (
|
||||
<tr key={fc.id} className="border-b border-slate-900 hover:bg-slate-850/10">
|
||||
<td className="p-2 text-slate-200 font-bold">{fc.ticker}</td>
|
||||
<td className="p-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${fc.predictedDirection === 'UP' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-rose-500/10 text-rose-400'}`}>
|
||||
{fc.predictedDirection}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-2 text-slate-350">{(fc.predictedProb * 100).toFixed(0)}%</td>
|
||||
<td className="p-2 text-slate-300">${fc.entryPrice.toLocaleString()}</td>
|
||||
<td className="p-2 text-slate-400">{fc.resolved ? 'RESOLVED' : 'PENDING'}</td>
|
||||
<td className={`p-2 text-right font-bold ${fc.result === 'SUCCESS' ? 'text-emerald-400' : fc.result === 'FAILURE' ? 'text-rose-400' : 'text-slate-500'}`}>
|
||||
{fc.result || '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
forecasts.map((fc) => {
|
||||
let avgT1Prob = 0.5;
|
||||
if (fc.predictions) {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
Object.values(fc.predictions).forEach((hMap) => {
|
||||
if (hMap && hMap.T1 !== undefined) {
|
||||
sum += hMap.T1;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
if (count > 0) avgT1Prob = sum / count;
|
||||
}
|
||||
const avgT1Dir = avgT1Prob > 0.5 ? 'UP' : 'DOWN';
|
||||
|
||||
const now = Date.now();
|
||||
const getHorizonStatus = (hKey: 'T1' | 'T5' | 'T10') => {
|
||||
const targetTime = fc.targetTimes[hKey];
|
||||
const isPast = now >= targetTime;
|
||||
|
||||
let successes = 0;
|
||||
let total = 0;
|
||||
ESTIMATORS.forEach((est) => {
|
||||
const rKey = `${est.id}_${hKey}`;
|
||||
if (fc.results && fc.results[rKey]) {
|
||||
total++;
|
||||
if (fc.results[rKey] === 'SUCCESS') successes++;
|
||||
}
|
||||
});
|
||||
|
||||
if (total === 5) {
|
||||
return (
|
||||
<span className="text-emerald-400 font-bold">
|
||||
{successes}/5
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isPast) {
|
||||
return <span className="text-slate-400">Resolving...</span>;
|
||||
}
|
||||
const secondsLeft = Math.max(0, Math.ceil((targetTime - now) / 1000));
|
||||
return <span className="text-slate-500 font-normal">{secondsLeft}s</span>;
|
||||
};
|
||||
|
||||
let resolvedCount = 0;
|
||||
let successCount = 0;
|
||||
if (fc.results) {
|
||||
Object.values(fc.results).forEach((r) => {
|
||||
resolvedCount++;
|
||||
if (r === 'SUCCESS') successCount++;
|
||||
});
|
||||
}
|
||||
|
||||
let statusText = 'PENDING';
|
||||
if (resolvedCount === 15) {
|
||||
statusText = 'RESOLVED';
|
||||
} else if (resolvedCount > 0) {
|
||||
statusText = `PARTIAL (${resolvedCount}/15)`;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={fc.id} className="border-b border-slate-900 hover:bg-slate-850/10">
|
||||
<td className="p-2 text-slate-200 font-bold">{fc.ticker}</td>
|
||||
<td className="p-2 text-slate-350">${fc.entryPrice.toLocaleString()}</td>
|
||||
<td className="p-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${avgT1Dir === 'UP' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-rose-500/10 text-rose-400'}`}>
|
||||
{avgT1Dir} {(avgT1Prob * 100).toFixed(0)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-2 text-slate-300">
|
||||
<div className="flex gap-2 text-[10px]">
|
||||
<span>T1: {getHorizonStatus('T1')}</span>
|
||||
<span>T5: {getHorizonStatus('T5')}</span>
|
||||
<span>T10: {getHorizonStatus('T10')}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 text-slate-400 text-[10px]">{statusText}</td>
|
||||
<td className="p-2 text-right font-bold text-slate-300">
|
||||
{resolvedCount > 0 ? (
|
||||
<span className={successCount / resolvedCount >= 0.5 ? 'text-emerald-400' : 'text-rose-400'}>
|
||||
{((successCount / resolvedCount) * 100).toFixed(0)}% ({successCount}/{resolvedCount})
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-500">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user