| '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); |
| |
| |
| 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 |
| ); |
|
|
| |
| 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, |
| }; |
| }); |
| |
| |
| 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)); |
| } 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> |
| ); |
| } |
|
|