borsa / nextjs-app /src /components /TopMLPredictions.tsx
veteroner's picture
fix: make sync endpoint market-aware — prevent US scan results from overwriting BIST file
ce7f322
'use client';
import { useCallback, useEffect, useState } from 'react';
import { Brain, TrendingUp, TrendingDown, Loader2 } from 'lucide-react';
import Link from 'next/link';
import { fetchJson } from '@/lib/http';
import { useMarket } from '@/contexts/MarketContext';
interface TopPrediction {
symbol: string;
name: string;
current_price: number;
predicted_price: number;
prediction_change: number;
confidence: number;
signal: 'BUY' | 'SELL' | 'HOLD';
}
function asNumber(value: unknown, fallback = 0): number {
const n = typeof value === 'number' ? value : Number(value);
return Number.isFinite(n) ? n : fallback;
}
function fmt2(value: unknown): string {
return asNumber(value, 0).toFixed(2);
}
export default function TopMLPredictions() {
const { market } = useMarket();
const isUS = market === 'us';
const [predictions, setPredictions] = useState<TopPrediction[]>([]);
const [loading, setLoading] = useState(true);
const fetchTopPredictions = useCallback(async () => {
try {
setLoading(true);
const symbols = isUS
? (() => {
const universe = fetchJson<Record<string, unknown>>(
`/api/universe?name=us_popular`,
{ method: 'GET' },
{ timeoutMs: 20000, retries: 1 }
);
return universe;
})()
: fetchJson<Record<string, unknown>>(
`/api/popular-stocks`,
{ method: 'GET' },
{ timeoutMs: 12000, retries: 1 }
);
const stocksData = await symbols;
const symbolList = (Array.isArray(stocksData.symbols) ? stocksData.symbols : Array.isArray(stocksData.stocks) ? stocksData.stocks : []) as string[];
const selectedSymbols = symbolList.slice(0, 10);
// Get predictions from HF
const data = await fetchJson<Record<string, unknown>>(
`/api/ml-predictions`,
{ method: 'POST' },
{ timeoutMs: 30000, retries: 0, jsonBody: { symbols: selectedSymbols, days_ahead: 7, model: 'ensemble', market } }
);
const preds = Array.isArray(data.predictions) ? data.predictions as Record<string, unknown>[] : [];
const combined: TopPrediction[] = preds.map((pred: Record<string, unknown>) => {
const currentPrice = asNumber(pred.current_price ?? pred.last_price, 0);
const predictedPrice = asNumber(
pred.predicted_price ?? pred.prediction_7d ?? pred.prediction_30d,
currentPrice
);
const predictionChange = asNumber(
pred.prediction_change ?? pred.predicted_change_pct,
currentPrice ? ((predictedPrice - currentPrice) / currentPrice) * 100 : 0
);
// Backend now sends 0-100; if a 0-1 slips through, convert.
let confidence = asNumber(pred.confidence, 0);
if (confidence > 0 && confidence <= 1) confidence = confidence * 100;
const signal = (pred.signal || 'HOLD') as TopPrediction['signal'];
return {
symbol: String(pred.symbol || ''),
name: String(pred.name || pred.symbol || ''),
current_price: currentPrice,
predicted_price: predictedPrice,
prediction_change: predictionChange,
confidence,
signal,
};
});
// Sort by signal (BUY first) and confidence
const sorted = combined.sort((a: TopPrediction, b: TopPrediction) => {
if (a.signal === 'BUY' && b.signal !== 'BUY') return -1;
if (a.signal !== 'BUY' && b.signal === 'BUY') return 1;
return (b.confidence || 0) - (a.confidence || 0);
});
setPredictions(sorted.slice(0, 5)); // Top 5
} catch (error) {
console.error('Failed to fetch predictions:', error);
setPredictions([]);
} finally {
setLoading(false);
}
}, [isUS, market]);
useEffect(() => {
fetchTopPredictions();
}, [fetchTopPredictions]);
if (loading) {
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center gap-2 mb-4">
<Brain className="w-5 h-5 text-purple-600" />
<h2 className="text-lg font-semibold">Top ML Tahminleri</h2>
</div>
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Brain className="w-5 h-5 text-purple-600" />
<h2 className="text-lg font-semibold">Top ML Tahminleri</h2>
</div>
<button
onClick={fetchTopPredictions}
className="text-sm text-purple-600 hover:text-purple-800"
>
Yenile
</button>
</div>
<div className="space-y-3">
{predictions.length === 0 && (
<div className="text-sm text-gray-500 text-center py-6">
ML tahminleri şu an üretilemedi. Biraz sonra tekrar deneyin.
</div>
)}
{predictions.map((pred) => (
<Link
key={pred.symbol}
href={`/stocks/${pred.symbol}`}
className="block p-4 rounded-lg border hover:border-purple-300 hover:bg-purple-50 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-gray-900">{pred.symbol}</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
pred.signal === 'BUY'
? 'bg-green-100 text-green-800'
: pred.signal === 'SELL'
? 'bg-red-100 text-red-800'
: 'bg-gray-100 text-gray-800'
}`}>
{pred.signal}
</span>
</div>
<div className="text-sm text-gray-600 mt-1">{pred.name}</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-600">
Güven: <span className="font-semibold">{Math.round(asNumber(pred.confidence, 0))}%</span>
</div>
<div className={`text-sm font-semibold mt-1 flex items-center justify-end gap-1 ${
pred.prediction_change >= 0 ? 'text-green-600' : 'text-red-600'
}`}>
{pred.prediction_change >= 0 ? (
<TrendingUp className="w-4 h-4" />
) : (
<TrendingDown className="w-4 h-4" />
)}
<span>
{pred.prediction_change >= 0 ? '+' : ''}
{fmt2(pred.prediction_change)}%
</span>
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-gray-100 flex justify-between text-xs text-gray-500">
<span>Mevcut: {isUS ? '$' : '₺'}{fmt2(pred.current_price)}</span>
<span>Tahmin: ₺{fmt2(pred.predicted_price)}</span>
</div>
</Link>
))}
</div>
<div className="mt-4 text-xs text-gray-500 text-center">
Teknik gösterge bazlı ML analizi ile oluşturulmuştur
</div>
</div>
);
}