Closes #020 - Ticker Data Real-Time Alignment & ML Handbook Integration
This commit is contained in:
@@ -24,8 +24,8 @@ export default function CryptoBlueprintModal({ isOpen, onClose }: CryptoBlueprin
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/85 backdrop-blur-md p-4 sm:p-6 md:p-8 animate-fade-in">
|
||||
<div className="bg-slate-900 border border-slate-800/80 rounded-3xl w-full max-w-4xl h-[80vh] flex flex-col overflow-hidden shadow-2xl relative text-slate-350">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-900/85 backdrop-blur-md flex items-start justify-center p-4 sm:p-6 md:p-8 animate-fade-in">
|
||||
<div className="bg-slate-900 border border-slate-800/80 rounded-3xl w-full max-w-4xl my-auto max-h-full flex flex-col overflow-hidden shadow-2xl relative text-slate-350">
|
||||
|
||||
{/* Modal Header */}
|
||||
<div className="flex justify-between items-center px-6 py-4 bg-slate-950/45 border-b border-slate-800/60">
|
||||
|
||||
@@ -124,6 +124,7 @@ export default function CryptoDemo() {
|
||||
const [ensemblePredictions, setEnsemblePredictions] = useState<any>(null);
|
||||
const [loadingEnsemble, setLoadingEnsemble] = useState(false);
|
||||
const [isShieldActive, setIsShieldActive] = useState(true);
|
||||
const [coins, setCoins] = useState<Record<string, CoinData>>(defaultCoins);
|
||||
|
||||
// Safely load counters and forecasts from localStorage on client mount
|
||||
useEffect(() => {
|
||||
@@ -266,6 +267,61 @@ export default function CryptoDemo() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Poll live price, 24h change, and indicators from the backend API
|
||||
useEffect(() => {
|
||||
const fetchCryptoPrices = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/finance?region=crypto');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const results = data.results || [];
|
||||
|
||||
setCoins(prevCoins => {
|
||||
const updatedCoins = { ...prevCoins };
|
||||
results.forEach((r: any) => {
|
||||
const cleanTicker = r.ticker.replace('-USD', '');
|
||||
if (cleanTicker === 'BTC' || cleanTicker === 'ETH' || cleanTicker === 'SOL') {
|
||||
const currentPrice = r.currentPrice;
|
||||
const dayChangePercent = r.dayChange * 100;
|
||||
|
||||
// Bind API indicators directly
|
||||
const fundingRate = r.fundingRate !== undefined ? r.fundingRate : (cleanTicker === 'BTC' ? -0.015 : cleanTicker === 'ETH' ? 0.045 : 0.082);
|
||||
const openInterestChange = r.openInterestChange !== undefined ? r.openInterestChange : (cleanTicker === 'BTC' ? 8.2 : cleanTicker === 'ETH' ? -3.5 : 14.5);
|
||||
const longShortRatio = r.longShortRatio !== undefined ? r.longShortRatio : (cleanTicker === 'BTC' ? 0.92 : cleanTicker === 'ETH' ? 1.34 : 1.62);
|
||||
const whaleInflow = r.whaleInflow !== undefined ? r.whaleInflow : (cleanTicker === 'BTC' ? 480 : cleanTicker === 'ETH' ? -120 : 1250);
|
||||
const exchangeReserves = r.exchangeReserves !== undefined ? r.exchangeReserves : (cleanTicker === 'BTC' ? -1.4 : cleanTicker === 'ETH' ? 0.8 : -2.8);
|
||||
|
||||
// Scale liquidations dynamically relative to the current real price
|
||||
const liqLongVal = currentPrice * (cleanTicker === 'BTC' ? 0.982 : cleanTicker === 'ETH' ? 0.971 : 0.955);
|
||||
const liqShortVal = currentPrice * (cleanTicker === 'BTC' ? 1.015 : cleanTicker === 'ETH' ? 1.026 : 1.045);
|
||||
|
||||
updatedCoins[cleanTicker] = {
|
||||
ticker: cleanTicker,
|
||||
name: cleanTicker === 'BTC' ? 'Bitcoin' : cleanTicker === 'ETH' ? 'Ethereum' : 'Solana',
|
||||
price: `$${currentPrice.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
|
||||
change24h: parseFloat(dayChangePercent.toFixed(2)),
|
||||
fundingRate,
|
||||
openInterestChange,
|
||||
longShortRatio,
|
||||
whaleInflow,
|
||||
exchangeReserves,
|
||||
liqLong: `$${liqLongVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
|
||||
liqShort: `$${liqShortVal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
};
|
||||
}
|
||||
});
|
||||
return updatedCoins;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch crypto prices:", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCryptoPrices();
|
||||
const interval = setInterval(fetchCryptoPrices, 15000); // Poll every 15s
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Client-side background learning loop evaluating forecasts against actual live returns
|
||||
useEffect(() => {
|
||||
const runLearningLoop = async () => {
|
||||
@@ -359,8 +415,8 @@ export default function CryptoDemo() {
|
||||
|
||||
// Active Coin data retrieval
|
||||
const activeCoin = useMemo(() => {
|
||||
return customCoins[activeTicker] || defaultCoins[activeTicker] || defaultCoins['BTC'];
|
||||
}, [activeTicker, customCoins]);
|
||||
return customCoins[activeTicker] || coins[activeTicker] || coins['BTC'];
|
||||
}, [activeTicker, customCoins, coins]);
|
||||
|
||||
// Helper to fetch/load prediction probabilities
|
||||
const getPredictionProb = (estimator: string, horizon: string): number => {
|
||||
@@ -434,7 +490,7 @@ export default function CryptoDemo() {
|
||||
const query = searchQuery.trim().toUpperCase();
|
||||
if (!query) return;
|
||||
|
||||
if (defaultCoins[query]) {
|
||||
if (coins[query]) {
|
||||
setActiveTicker(query);
|
||||
setSearchQuery('');
|
||||
return;
|
||||
@@ -592,7 +648,7 @@ export default function CryptoDemo() {
|
||||
|
||||
{/* Status Cards BTC, ETH, SOL */}
|
||||
{['BTC', 'ETH', 'SOL'].map((tick) => {
|
||||
const coin = defaultCoins[tick];
|
||||
const coin = coins[tick] || defaultCoins[tick];
|
||||
const isActive = activeTicker === tick;
|
||||
const isUp = coin.change24h >= 0;
|
||||
return (
|
||||
@@ -851,34 +907,22 @@ export default function CryptoDemo() {
|
||||
<tr className="border-b border-slate-800 text-slate-400 font-semibold bg-slate-900/40">
|
||||
<th className="p-2">Ticker</th>
|
||||
<th className="p-2">Entry Price</th>
|
||||
<th className="p-2">Ensemble T+1</th>
|
||||
<th className="p-2">Horizons (T1/T5/T10)</th>
|
||||
<th className="p-2 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-right">Success Rate</th>
|
||||
<th className="p-2 text-right">Accuracy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forecasts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="p-4 text-center text-slate-500 italic">No forecasts registered yet.</td>
|
||||
<td colSpan={7} className="p-4 text-center text-slate-500 italic">No forecasts registered yet.</td>
|
||||
</tr>
|
||||
) : (
|
||||
forecasts.map((fc) => {
|
||||
let avgT1Prob = 0.5;
|
||||
if (fc.predictions) {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
Object.values(fc.predictions).forEach((hMap) => {
|
||||
if (hMap && hMap.T1 !== undefined) {
|
||||
sum += hMap.T1;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
if (count > 0) avgT1Prob = sum / count;
|
||||
}
|
||||
const avgT1Dir = avgT1Prob > 0.5 ? 'UP' : 'DOWN';
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const getHorizonStatus = (hKey: 'T1' | 'T5' | 'T10') => {
|
||||
const targetTime = fc.targetTimes[hKey];
|
||||
const isPast = now >= targetTime;
|
||||
@@ -896,17 +940,52 @@ export default function CryptoDemo() {
|
||||
if (total === 5) {
|
||||
return (
|
||||
<span className="text-emerald-400 font-bold">
|
||||
{successes}/5
|
||||
{successes}/5 OK
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isPast) {
|
||||
return <span className="text-slate-400">Resolving...</span>;
|
||||
return <span className="text-cyan-400 animate-pulse">Resolving...</span>;
|
||||
}
|
||||
const secondsLeft = Math.max(0, Math.ceil((targetTime - now) / 1000));
|
||||
return <span className="text-slate-500 font-normal">{secondsLeft}s</span>;
|
||||
};
|
||||
|
||||
const renderHorizonCell = (hKey: 'T1' | 'T5' | 'T10') => {
|
||||
let avgProb = 0.5;
|
||||
const modelProbs: string[] = [];
|
||||
if (fc.predictions) {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
ESTIMATORS.forEach((est) => {
|
||||
const prob = fc.predictions[est.id]?.[hKey];
|
||||
if (prob !== undefined) {
|
||||
sum += prob;
|
||||
count++;
|
||||
modelProbs.push(`${est.id.toUpperCase()}:${(prob * 100).toFixed(0)}%`);
|
||||
}
|
||||
});
|
||||
if (count > 0) avgProb = sum / count;
|
||||
}
|
||||
const avgDir = avgProb > 0.5 ? 'UP' : 'DOWN';
|
||||
|
||||
return (
|
||||
<td className="p-2 text-center border-r border-slate-900/40 last:border-r-0">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${avgDir === 'UP' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-rose-500/10 text-rose-400'}`}>
|
||||
{avgDir} {(avgProb * 100).toFixed(0)}%
|
||||
</span>
|
||||
<div className="text-[10px] font-semibold">
|
||||
{getHorizonStatus(hKey)}
|
||||
</div>
|
||||
<div className="text-[8px] text-slate-500 flex flex-wrap justify-center gap-1 max-w-[130px] leading-tight font-sans">
|
||||
{modelProbs.join(' | ')}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
let resolvedCount = 0;
|
||||
let successCount = 0;
|
||||
if (fc.results) {
|
||||
@@ -927,18 +1006,9 @@ export default function CryptoDemo() {
|
||||
<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>
|
||||
{renderHorizonCell('T1')}
|
||||
{renderHorizonCell('T5')}
|
||||
{renderHorizonCell('T10')}
|
||||
<td className="p-2 text-slate-400 text-[10px]">{statusText}</td>
|
||||
<td className="p-2 text-right font-bold text-slate-300">
|
||||
{resolvedCount > 0 ? (
|
||||
|
||||
@@ -26,8 +26,8 @@ export default function CryptoMathModal({ isOpen, onClose }: CryptoMathModalProp
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/85 backdrop-blur-md p-4 sm:p-6 md:p-8 animate-fade-in">
|
||||
<div className="bg-slate-900 border border-slate-800/80 rounded-3xl w-full max-w-4xl h-[80vh] flex flex-col overflow-hidden shadow-2xl relative text-slate-300">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/85 backdrop-blur-md flex items-start justify-center p-4 sm:p-6 md:p-8 animate-fade-in">
|
||||
<div className="bg-slate-900 border border-slate-800/80 rounded-3xl w-full max-w-4xl my-auto max-h-full flex flex-col overflow-hidden shadow-2xl relative text-slate-300">
|
||||
|
||||
{/* Modal Header */}
|
||||
<div className="flex justify-between items-center px-6 py-4 bg-slate-950/45 border-b border-slate-800/60">
|
||||
@@ -181,6 +181,70 @@ export default function CryptoMathModal({ isOpen, onClose }: CryptoMathModalProp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section G: Ensemble Estimator Specifications */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-bold text-cyan-400 uppercase tracking-wider font-mono">G. Ensemble Estimator Specifications</h4>
|
||||
<p className="text-xs leading-relaxed text-slate-400">
|
||||
The Walk-Forward Ensemble Radar aggregates forecasting signals from 5 independent machine learning estimators optimized for distinct predictive roles across three temporal horizons:
|
||||
</p>
|
||||
<div className="bg-slate-950/40 p-5 rounded-2xl border border-slate-800/60 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-200 block">1. Random Forest (RF)</span>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Utilizes bootstrap aggregation (bagging) of uncorrelated decision trees to map non-linear feature interactions. It constructs a robust ensemble prediction:
|
||||
</p>
|
||||
<BlockMath math="\hat{P}_{\text{RF}}(y=1 \mid \mathbf{x}) = \frac{1}{B} \sum_{b=1}^{B} f_b(\mathbf{x})" />
|
||||
<p className="text-[10px] text-slate-500 font-mono">
|
||||
Optimized for multi-regime boundary separation and filtering out high-volatility futures noise.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-200 block">2. XGBoost / Gradient Boosting (GB)</span>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Fits sequential decision trees to minimize the residual classification loss via gradient descent:
|
||||
</p>
|
||||
<BlockMath math="\mathcal{L}^{(t)} = \sum_{i=1}^{n} l\left(y_i, \hat{y}_i^{(t-1)} + f_t(\mathbf{x}_i)\right) + \Omega(f_t)" />
|
||||
<p className="text-[10px] text-slate-500 font-mono">
|
||||
Highly responsive to short-term micro-trends, making it the primary signal anchor for the T+1 horizon.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-200 block">3. Logistic Regression with ElasticNet (LR)</span>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Serves as the linear baseline anchor, regularized with combined L1 (Lasso) and L2 (Ridge) penalties:
|
||||
</p>
|
||||
<BlockMath math="\min_{\mathbf{w}, c} \frac{1}{n} \sum_{i=1}^{n} \log\left(1 + e^{-y_i (\mathbf{w}^T \mathbf{x}_i + c)}\right) + r \lambda \|\mathbf{w}\|_1 + \frac{1-r}{2} \lambda \|\mathbf{w}\|_2^2" />
|
||||
<p className="text-[10px] text-slate-500 font-mono">
|
||||
Prevents wild regime-extrapolation decay and ensures structural stability during major trend shifts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-200 block">4. Support Vector Machine (SVM)</span>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Projects the feature space into a high-dimensional Hilbert space using a Radial Basis Function (RBF) kernel:
|
||||
</p>
|
||||
<BlockMath math="K(\mathbf{x}_i, \mathbf{x}_j) = \exp\left(-\gamma \|\mathbf{x}_i - \mathbf{x}_j\|^2\right)" />
|
||||
<p className="text-[10px] text-slate-500 font-mono">
|
||||
Isolates non-linear hyperplane separation boundaries, targeting multi-dimensional trend-reversal thresholds for the T+5 horizon.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-200 block">5. Multi-Layer Perceptron (MLP)</span>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
A deep feedforward neural network mapping complex cross-correlations across hidden layers using backpropagation:
|
||||
</p>
|
||||
<BlockMath math="\mathbf{a}^{(l)} = \sigma\left(\mathbf{W}^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)}\right)" />
|
||||
<p className="text-[10px] text-slate-500 font-mono">
|
||||
Extracts intricate temporal patterns and deep feature interactions, optimized for the medium-term T+10 forecasting horizon.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user