Closes #020 - Ticker Data Real-Time Alignment & ML Handbook Integration

This commit is contained in:
Antigravity Agent
2026-06-14 13:25:42 +02:00
parent 9c5cd78801
commit 118c626fe0
6 changed files with 299 additions and 44 deletions

View File

@@ -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 &amp; Res</th>
<th className="p-2 text-center">T+5 Forecast &amp; Res</th>
<th className="p-2 text-center">T+10 Forecast &amp; Res</th>
<th className="p-2">Status</th>
<th className="p-2 text-right">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 ? (