CoolFace
Apppublic

kaan1233/bistelligence-api

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
server.py1018 linesDownload Raw Back to root
1"""
2BISTelligence API v2 - Ana Sunucu
3Modüler yapı: Route'lar routes/ altında, skor hesaplama scoring.py'de, state app_state.py'de.
4"""
5import logging
6import sys
7
8import logging
9import sys
10
11# --- Structured Logging Setup ---
12logging.basicConfig(
13    level=logging.INFO,
14    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
15    datefmt="%H:%M:%S",
16    stream=sys.stdout
17)
18logger = logging.getLogger("bistelligence")
19
20class EndpointFilter(logging.Filter):
21    def filter(self, record: logging.LogRecord) -> bool:
22        # Args içinde 404 status code varsa direkt engelle (Uvicorn tuple olarak gönderir)
23        if record.args and 404 in record.args:
24            return False
25            
26        # Veya mesaj string'i içinde geçiyorsa
27        try:
28            msg = record.getMessage()
29            if " 404 " in msg or "404 Not Found" in msg:
30                return False
31        except:
32            pass
33            
34        return True
35
36# Global loglara filtre (Uvicorn CLI ezmeden önce)
37logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
38logging.getLogger("uvicorn.error").addFilter(EndpointFilter())
39logging.getLogger("uvicorn").addFilter(EndpointFilter())
40
41# Yahoo Finance'in kendi içine gömülü (hardcoded) HTML loglarını kapatıyoruz
42import yfinance as yf
43yf_logger = logging.getLogger('yfinance')
44yf_logger.setLevel(logging.CRITICAL)
45
46from fastapi import FastAPI, WebSocket, WebSocketDisconnect
47from fastapi.staticfiles import StaticFiles
48from fastapi.responses import JSONResponse
49from fastapi.middleware.cors import CORSMiddleware
50from fastapi.encoders import jsonable_encoder
51from fastapi import Request, HTTPException, Security, Depends
52from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
53import pandas as pd
54import json
55import asyncio
56from datetime import datetime, timedelta
57import os
58import time
59import math
60import numpy as np
61
62os.environ['TZ'] = 'Europe/Istanbul'
63if hasattr(time, 'tzset'):
64    time.tzset()
65
66import firebase_admin
67from firebase_admin import credentials, firestore, auth
68
69# --- App State ---
70import app_state as state
71
72# --- Yardımcı Fonksiyonlar ---
73def clean_nans(obj):
74    if isinstance(obj, dict):
75        return {k: clean_nans(v) for k, v in obj.items()}
76    elif isinstance(obj, list):
77        return [clean_nans(item) for item in obj]
78    elif isinstance(obj, float):
79        if math.isnan(obj) or np.isnan(obj) or np.isinf(obj):
80            return None
81    elif isinstance(obj, np.floating):
82        if np.isnan(obj) or np.isinf(obj):
83            return None
84    return obj
85
86
87# --- Yazılabilir Yol (consecutive_recs.json) ---
88_default_recs_path = "consecutive_recs.json"
89try:
90    with open(_default_recs_path + ".test", "w") as _f:
91        _f.write("test")
92    os.remove(_default_recs_path + ".test")
93    CONSECUTIVE_RECS_FILE = _default_recs_path
94except (IOError, PermissionError):
95    import tempfile
96    CONSECUTIVE_RECS_FILE = os.path.join(tempfile.gettempdir(), "consecutive_recs.json")
97    logger.warning(f"consecutive_recs.json yazılamıyor, fallback: {CONSECUTIVE_RECS_FILE}")
98
99
100def load_consecutive_recs():
101    if os.path.exists(CONSECUTIVE_RECS_FILE):
102        try:
103            with open(CONSECUTIVE_RECS_FILE, "r", encoding="utf-8") as f:
104                return json.load(f)
105        except Exception:
106            return {}
107    return {}
108
109
110def save_consecutive_recs(data):
111    try:
112        with open(CONSECUTIVE_RECS_FILE, "w", encoding="utf-8") as f:
113            json.dump(data, f)
114    except (IOError, PermissionError) as e:
115        logger.warning(f"consecutive_recs kaydedilemedi: {e}")
116
117# --- Signal Timestamps ---
118_default_timestamps_path = "signal_timestamps.json"
119try:
120    with open(_default_timestamps_path + ".test", "w") as _f:
121        _f.write("test")
122    os.remove(_default_timestamps_path + ".test")
123    SIGNAL_TIMESTAMPS_FILE = _default_timestamps_path
124except (IOError, PermissionError):
125    import tempfile
126    SIGNAL_TIMESTAMPS_FILE = os.path.join(tempfile.gettempdir(), "signal_timestamps.json")
127    logger.warning(f"signal_timestamps.json yazılamıyor, fallback: {SIGNAL_TIMESTAMPS_FILE}")
128
129def load_signal_timestamps():
130    if os.path.exists(SIGNAL_TIMESTAMPS_FILE):
131        try:
132            with open(SIGNAL_TIMESTAMPS_FILE, "r", encoding="utf-8") as f:
133                return json.load(f)
134        except Exception:
135            return {}
136    return {}
137
138def save_signal_timestamps(data):
139    try:
140        with open(SIGNAL_TIMESTAMPS_FILE, "w", encoding="utf-8") as f:
141            json.dump(data, f)
142    except (IOError, PermissionError) as e:
143        logger.warning(f"signal_timestamps kaydedilemedi: {e}")
144
145
146# --- Firebase Setup ---
147firebase_creds_env = os.getenv("FIREBASE_CREDENTIALS")
148
149try:
150    if firebase_creds_env:
151        cred_dict = json.loads(firebase_creds_env)
152        cred = credentials.Certificate(cred_dict)
153        firebase_admin.initialize_app(cred)
154        state.firebase_db = firestore.client()
155        logger.info("Firebase Admin SDK (Env Var) başarıyla başlatıldı.")
156    elif os.path.exists("firebase-adminsdk.json"):
157        cred = credentials.Certificate("firebase-adminsdk.json")
158        firebase_admin.initialize_app(cred)
159        state.firebase_db = firestore.client()
160        logger.info("Firebase Admin SDK (Dosya) başarıyla başlatıldı.")
161    else:
162        logger.warning("Firebase Credentials bulunamadı. Bulut veritabanı aktif değil.")
163except Exception as e:
164    logger.error(f"Firebase başlatılamadı: {e}")
165
166
167# --- WebSocket Manager ---
168class ConnectionManager:
169    def __init__(self):
170        self.active_connections: list[WebSocket] = []
171
172    async def connect(self, websocket: WebSocket):
173        await websocket.accept()
174        self.active_connections.append(websocket)
175
176    def disconnect(self, websocket: WebSocket):
177        if websocket in self.active_connections:
178            self.active_connections.remove(websocket)
179
180    async def broadcast(self, message: dict):
181        dead = []
182        for conn in self.active_connections:
183            try:
184                await conn.send_json(message)
185            except Exception:
186                dead.append(conn)
187        for conn in dead:
188            self.disconnect(conn)
189
190ws_manager = ConnectionManager()
191
192
193# --- Auth ---
194security = HTTPBearer(auto_error=False)
195
196def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
197    if not credentials:
198        return {"uid": "local_user"}
199    if state.firebase_db is None:
200        return {"uid": "local_user"}
201    token = credentials.credentials
202    try:
203        decoded_token = auth.verify_id_token(token)
204        return decoded_token
205    except Exception:
206        raise HTTPException(status_code=401, detail="Geçersiz yetkilendirme")
207
208
209# --- Data Imports ---
210from data_fetcher import fetch_stock_data, fetch_market_index, fetch_latest_prices, BIST100_TICKERS, ALL_BIST_TICKERS
211TARGET_TICKERS = ALL_BIST_TICKERS
212
213from analyzer import check_market_regime, get_signal_label
214from scoring import calculate_hybrid_score
215from ml_model import get_or_train_global_model, predict_multi_horizon, get_ml_dashboard_data, _model_cache
216from sector_analyzer import analyze_sectors, get_sector_rotation_summary, get_ticker_sector
217import yfinance as yf
218import algo_config_manager
219
220# --- FastAPI App ---
221app = FastAPI(title="BISTelligence API")
222
223app.add_middleware(
224    CORSMiddleware,
225    allow_origins=["*"],
226    allow_credentials=False,
227    allow_methods=["*"],
228    allow_headers=["*"],
229)
230
231# --- Route'ları Dahil Et ---
232from routes.portfolio_routes import router as portfolio_router
233from routes.ai_routes import router as ai_router
234from routes.backtest_routes import router as backtest_router
235
236# Auth dependency'yi route modüllerine enjekte et (FastAPI Dependency Override)
237from routes import portfolio_routes, ai_routes
238app.dependency_overrides[portfolio_routes._get_current_user] = get_current_user
239app.dependency_overrides[ai_routes._get_current_user] = get_current_user
240
241app.include_router(portfolio_router)
242app.include_router(ai_router)
243app.include_router(backtest_router)
244
245
246# =======================================================
247# ANALİZ MOTORU
248# =======================================================
249def run_analysis():
250    try:
251        _run_analysis_inner()
252    except Exception as e:
253        import traceback
254        traceback.print_exc()
255        logger.critical(f"run_analysis çöktü: {e}")
256    finally:
257        state.last_update_time = datetime.now().isoformat()
258        if state.system_status.get("status") != "ready":
259            state.system_status.update({"status": "ready", "progress": 100, "message": "Sistem Hazır (kısmi veri ile)"})
260            logger.warning("Analiz kısmen tamamlandı, sistem kısmi veri ile hazır.")
261
262
263def _run_analysis_inner():
264    global TARGET_TICKERS
265    
266    try:
267        cfg = algo_config_manager.get_config(state.firebase_db)
268        custom_tickers = cfg.get("custom_tickers", [])
269        if custom_tickers:
270            # Sadece geçerli, boş olmayan stringleri al
271            valid_custom = [t.strip().upper() for t in custom_tickers if isinstance(t, str) and t.strip()]
272            # ".IS" eki yoksa ekle (Yahoo Finance formatı için)
273            valid_custom = [t if t.endswith(".IS") else f"{t}.IS" for t in valid_custom]
274            
275            # ALL_BIST_TICKERS ile birleştir, benzersiz olanları tut ve listeye çevir
276            from data_fetcher import ALL_BIST_TICKERS
277            TARGET_TICKERS = sorted(list(set(ALL_BIST_TICKERS + valid_custom)))
278            logger.info(f"Özel hisseler sisteme eklendi: {valid_custom}")
279    except Exception as e:
280        logger.warning(f"Özel hisseler yüklenirken hata oluştu: {e}")
281
282    print("=" * 60)
283    print(f"BİST Finansal API v2 - Saatlik Güncelleme ({datetime.now().strftime('%H:%M:%S')})")
284    print("=" * 60)
285
286    # [1/3] Endeks Verisi
287    print("\n[1/3] Endeks verisi çekiliyor...")
288    state.system_status.update({"status": "loading", "progress": 5, "message": "Endeks verisi çekiliyor..."})
289    idx = fetch_market_index()
290    state.market_regime = check_market_regime(idx)
291    print(f"  Piyasa Rejimi: {state.market_regime.get('regime', 'Bilinmiyor')}")
292
293    # [2/3] Hisse Verileri
294    print(f"\n[2/3] {len(TARGET_TICKERS)} hisse verisi çekiliyor...")
295    state.system_status.update({"progress": 10, "message": f"{len(TARGET_TICKERS)} hisse verisi çekiliyor..."})
296    state.stock_data = fetch_stock_data(TARGET_TICKERS)
297    print(f"  {len(state.stock_data)} hisse başarıyla indirildi.")
298
299    # Alternatif Yatırım Verileri (Döviz/Altın)
300    try:
301        print("  Alternatif yatırım verileri (Döviz/Altın) çekiliyor...")
302        from data_fetcher import _yf_lock
303        import yfinance as yf
304        with _yf_lock:
305            state.currency_data = yf.download(["TRY=X", "EURTRY=X", "GC=F"], period="10y")["Close"].ffill().bfill()
306        if state.currency_data.index.tzinfo is not None:
307            state.currency_data.index = state.currency_data.index.tz_localize(None)
308    except Exception as e:
309        logger.warning(f"Currency data fetch failed: {e}")
310
311    # [3/4] Temel Analiz
312    from data_fetcher import fetch_fundamentals
313    print("\n[3/4] Temel analiz verileri çekiliyor...")
314    state.system_status.update({"progress": 30, "message": "Temel analiz verileri çekiliyor..."})
315    fundamental_data = fetch_fundamentals(TARGET_TICKERS)
316
317    # [4/4] Şelale Filtreleme (TA -> Funda -> ML -> AI)
318    print(f"\n[4/4] Şelale Filtreleme (TA -> Funda -> ML -> AI) başlıyor...")
319    state.system_status.update({"progress": 35, "message": "Şelale filtreleme ve Yapay Zeka analizleri yapılıyor..."})
320
321    global_model = get_or_train_global_model(state.stock_data)
322
323    res = []
324    analyzed = 0
325    failed = 0
326
327    for ticker in TARGET_TICKERS:
328        if ticker not in state.stock_data:
329            continue
330        try:
331            df = state.stock_data[ticker]
332            if df is None or df.empty:
333                continue
334
335            # Hibrit skor hesapla (scoring.py)
336            result = calculate_hybrid_score(df, ticker, state.market_regime, global_model, fundamental_data)
337
338            res.append({
339                "ticker": ticker,
340                "price": round(result["current_price"], 2),
341                "score": result["score"],
342                "signal": result["signal"],
343                "ml_conf": result["ml_conf"],
344                "tavan_prob": result.get("tavan_probability", 0),
345                "ml_detail": result["ml_detail"],
346                "stop_loss": round(result["details"].get("stop_loss", 0), 2),
347                "target_price": round(result["details"].get("target_price", 0), 2),
348                "risk_reward": result["details"].get("risk_reward", 0),
349                "categories": result["details"].get("categories", {}),
350                "reasons": result["reasons"],
351                "rsi": round(result["details"].get("rsi", 0), 1) if result["details"].get("rsi") is not None else None,
352                "sector": get_ticker_sector(ticker),
353            })
354            analyzed += 1
355            if analyzed % 10 == 0:
356                print(f"  {analyzed} hisse analiz edildi...")
357                state.system_status["progress"] = 35 + int((analyzed / len(TARGET_TICKERS)) * 50)
358        except Exception as e:
359            failed += 1
360            logger.warning(f"Analiz hatası ({ticker}): {e}")
361
362    # HİSTEREZİS (YAPIŞKANLIK)
363    for r in res:
364        r['sort_score'] = r['score']
365        if r['ticker'] in state.previous_top_tickers:
366            r['sort_score'] += 5.0
367
368    state.recommendations = sorted(res, key=lambda x: (x['sort_score'], x['ml_conf']), reverse=True)
369    
370    # --- GEMINI HABER / HİSSİYAT FİLTRESİ (Sadece En İyi 10 Hisse İçin) ---
371    print("\n[5/5] En iyi 10 hisse için Gemini 1.5 Flash ile haber analizi yapılıyor...")
372    state.system_status.update({"progress": 90, "message": "Gemini 1.5 Flash ile canlı haber analizi (NLP) yapılıyor..."})
373    import yfinance as yf
374    import gemini_advisor
375    
376    for r in state.recommendations[:10]:
377        try:
378            t = yf.Ticker(r['ticker'])
379            raw_news = t.news
380            if raw_news:
381                # Sadece son 5 haberi analiz et
382                sentiment = gemini_advisor.analyze_news_sentiment(r['ticker'], raw_news[:5])
383                ai_score = sentiment.get("score", 50)
384                ai_reason = sentiment.get("reason", "Nötr haber")
385                
386                r["news_score"] = ai_score
387                r["news_reason"] = ai_reason
388                
389                # Sinyal Skoruna Etkisi (Veto & Bonus)
390                if ai_score < 35:
391                    r["score"] -= 100  # VETO
392                    r["sort_score"] -= 100
393                    r["reasons"].append(f"❌ Gemini Haber Vetosu ({ai_score}): {ai_reason}")
394                elif ai_score > 75:
395                    r["score"] += 10
396                    r["sort_score"] += 10
397                    r["reasons"].append(f"🚀 Gemini Haber Bonusu ({ai_score}): {ai_reason}")
398                else:
399                    r["reasons"].append(f"📰 Gemini Haber Nötr ({ai_score}): {ai_reason}")
400            else:
401                r["news_score"] = 50
402                r["news_reason"] = "Haber bulunamadı"
403                
404        except Exception as e:
405            logger.warning(f"Gemini haber hatası {r['ticker']}: {e}")
406            r["news_score"] = 50
407            r["news_reason"] = "API Hatası"
408
409    # Skoru değişenler olabileceği için tekrar sırala
410    state.recommendations = sorted(state.recommendations, key=lambda x: (x['sort_score'], x['ml_conf']), reverse=True)
411    state.previous_top_tickers = set([r['ticker'] for r in state.recommendations[:15]])
412
413    # TAKİP: Üst üste önerilme gün sayısı
414    consec_data = load_consecutive_recs()
415    current_date_str = datetime.now().strftime("%Y-%m-%d")
416    top_tickers = [r['ticker'] for r in state.recommendations[:15]]
417
418    for r in state.recommendations:
419        tck = r['ticker']
420        if tck in top_tickers:
421            if tck in consec_data:
422                if consec_data[tck]["last_date"] != current_date_str:
423                    consec_data[tck]["count"] += 1
424                    consec_data[tck]["last_date"] = current_date_str
425            else:
426                consec_data[tck] = {"count": 1, "last_date": current_date_str}
427        else:
428            if tck in consec_data and consec_data[tck]["last_date"] != current_date_str:
429                consec_data[tck]["count"] = 0
430
431        r["consecutive_days"] = consec_data.get(tck, {}).get("count", 0)
432
433    save_consecutive_recs(consec_data)
434
435    # TAKİP: Sinyal Üretim ve Güncelleme Zamanları
436    timestamps_data = load_signal_timestamps()
437    current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
438
439    for r in state.recommendations:
440        tck = r['ticker']
441        score = r.get("score", 0)
442        tavan_prob = r.get("tavan_prob", 0)
443        signal_val = r.get("signal", "")
444        signal_label = signal_val.get("label", "") if isinstance(signal_val, dict) else str(signal_val)
445
446        if tck not in timestamps_data:
447            timestamps_data[tck] = {
448                "created_at": current_time_str,
449                "updated_at": current_time_str,
450                "last_score": score,
451                "last_tavan_prob": tavan_prob,
452                "last_signal": signal_label,
453                "initial_price": r.get("price", 0)
454            }
455        else:
456            prev = timestamps_data[tck]
457            
458            # Sinyal YÖNÜ değiştiyse (örn: SAT'tan AL'a), bu yeni bir sinyaldir, fiyatı ve yaratılma zamanını sıfırla
459            if prev.get("last_signal") != signal_label:
460                timestamps_data[tck]["created_at"] = current_time_str
461                timestamps_data[tck]["initial_price"] = r.get("price", 0)
462
463            if prev.get("last_score") != score or prev.get("last_tavan_prob") != tavan_prob or prev.get("last_signal") != signal_label:
464                timestamps_data[tck]["updated_at"] = current_time_str
465                timestamps_data[tck]["last_score"] = score
466                timestamps_data[tck]["last_tavan_prob"] = tavan_prob
467                timestamps_data[tck]["last_signal"] = signal_label
468
469        r["created_at"] = timestamps_data[tck]["created_at"]
470        r["updated_at"] = timestamps_data[tck]["updated_at"]
471        r["initial_price"] = timestamps_data[tck].get("initial_price", r.get("price", 0))
472
473    save_signal_timestamps(timestamps_data)
474
475    # Gemini Elite Report
476    print(f"\n[4/4] Elit Hisseler Yapay Zekaya Gönderiliyor...")
477    from gemini_advisor import fetch_elite_report
478    top_elites = [r for r in state.recommendations if r['score'] >= 50 and r['ml_conf'] >= 50][:5]
479
480    today_str = datetime.now().strftime("%Y-%m-%d")
481    if state.last_ai_report_date != today_str:
482        if top_elites:
483            elite_text = ", ".join([f"{r['ticker']} (Puan: {r['score']}, ML Güven: %{r['ml_conf']})" for r in top_elites])
484            state.elite_ai_report = fetch_elite_report(elite_text, state.market_regime)
485            print("  Gemini raporu başarıyla alındı.")
486        else:
487            state.elite_ai_report = "Bugün piyasada hem Teknik Analiz hem de Yapay Zeka barajını geçebilen 'Elit' bir hisse bulunamadı. Nakitte beklemek en güvenli seçenek olabilir."
488            print("  Uygun elit hisse bulunamadı.")
489        state.last_ai_report_date = today_str
490    else:
491        print("  Gemini AI Raporu bugün zaten çekildi, eski rapor kullanılıyor.")
492
493    print("\n[5/5] AI Model Portföyü İşlemleri Manuel Olarak Tetiklenmeyi Bekliyor...")
494
495    print(f"\n{'=' * 60}")
496    print(f"Sistem Hazır! {analyzed} hisse analiz edildi. ({failed} hata)")
497    state.system_status.update({"status": "ready", "progress": 100, "message": "Sistem Hazır!"})
498    print(f"{'=' * 60}")
499
500    # Portföy Snapshot Al
501    from routes.portfolio_routes import take_portfolio_snapshot
502    take_portfolio_snapshot()
503    
504    from model_portfolio_bot import take_bot_portfolio_snapshot
505    take_bot_portfolio_snapshot(state.firebase_db)
506
507    # Firebase Cache
508    try:
509        def convert_numpy(obj):
510            if isinstance(obj, np.bool_): return bool(obj)
511            if isinstance(obj, np.integer): return int(obj)
512            if isinstance(obj, np.floating): return float(obj)
513            if isinstance(obj, np.ndarray): return obj.tolist()
514            if isinstance(obj, dict): return {k: convert_numpy(v) for k, v in obj.items()}
515            if isinstance(obj, list): return [convert_numpy(v) for v in obj]
516            return obj
517
518        cache_data = {
519            "timestamp": time.time(),
520            "market_regime": convert_numpy(state.market_regime),
521            "recommendations": convert_numpy(state.recommendations),
522            "elite_ai_report": state.elite_ai_report
523        }
524        if state.firebase_db:
525            state.firebase_db.collection("system_data").document("ai_cache").set(cache_data)
526            print("  Sonuçlar başarıyla Firebase ai_cache belgesine kaydedildi.")
527    except Exception as e:
528        logger.warning(f"Firebase Cache kaydedilemedi: {e}")
529
530
531# =======================================================
532# ZAMANLAYICI
533# =======================================================
534def schedule_bot_runner():
535    import threading
536    import time
537    import pytz
538    from datetime import datetime
539    import app_state as state
540    import model_portfolio_bot
541
542    def bot_job():
543        while True:
544            time.sleep(60)  # Her dakika kontrol et
545            tz = pytz.timezone('Europe/Istanbul')
546            now = datetime.now(tz)
547            
548            market_open = now.replace(hour=10, minute=15, second=0, microsecond=0)
549            market_close = now.replace(hour=18, minute=15, second=0, microsecond=0)
550            
551            if now.weekday() < 5 and market_open <= now <= market_close:
552                portfolio = model_portfolio_bot.get_bot_portfolio(state.firebase_db)
553                if portfolio.get("pending_run", False):
554                    logger.info("[AI BOT] Bekleyen islem (pending_run) bulundu. Anlik fiyatlar cekiliyor...")
555                    try:
556                        import data_fetcher
557                        data_fetcher._stock_cache.clear()
558                        data_fetcher._index_cache.clear()
559                        from ml_model import _feature_cache
560                        _feature_cache.clear()
561                        
562                        run_analysis()
563                        
564                        logger.info("[AI BOT] Analiz tamamlandi, bot calistiriliyor...")
565                        model_portfolio_bot.check_and_execute_pending_run(state.recommendations, state.firebase_db)
566                    except Exception as e:
567                        logger.error(f"[AI BOT] Bekleyen islem sirasinda hata: {e}")
568
569    t = threading.Thread(target=bot_job, daemon=True)
570    t.start()
571def schedule_hourly_update():
572    import threading
573
574    def update_job():
575        while True:
576            import pytz
577            time.sleep(900)  # 15 dakika
578
579            tz = pytz.timezone('Europe/Istanbul')
580            now = datetime.now(tz)
581            if now.weekday() < 5 and 10 <= now.hour < 18:
582                try:
583                    import data_fetcher
584                    data_fetcher._stock_cache.clear()
585                    data_fetcher._index_cache.clear()
586                    # Feature cache'i de temizle
587                    from ml_model import _feature_cache
588                    _feature_cache.clear()
589                    run_analysis()
590                except Exception as e:
591                    logger.error(f"Periyodik güncelleme başarısız: {e}")
592            else:
593                print(f"[BİLGİ] Piyasa kapalı ({now.strftime('%H:%M')}), güncelleme atlandı.")
594
595    t = threading.Thread(target=update_job, daemon=True)
596    t.start()
597
598
599# =======================================================
600# API ENDPOINTS (server.py'de kalan basit route'lar)
601# =======================================================
602@app.get("/api/system-status")
603def get_system_status():
604    return {**state.system_status, "last_update_time": state.last_update_time}
605
606
607@app.get("/api/dashboard")
608def get_dashboard():
609    stats = {
610        "analyzed_count": len(state.stock_data) if state.stock_data else 0,
611        "strong_signals": len([r for r in state.recommendations if r['score'] >= 60]),
612        "total_bullish": len([r for r in state.recommendations if r['score'] >= 50]),
613        "elite_ai_report": state.elite_ai_report,
614        "last_update_time": state.last_update_time,
615    }
616    return {"status": "success", "regime": state.market_regime, "stats": stats, "recommendations": state.recommendations[:20]}
617
618
619@app.get("/api/macro-data")
620def get_macro_data():
621    try:
622        if state.currency_data is None or state.currency_data.empty:
623            return {"status": "error"}
624
625        idx_val = "N/A"
626        idx = fetch_market_index()
627        if idx is not None and not idx.empty:
628            idx_val = f"{idx['Close'].iloc[-1]:.2f}"
629
630        usd = state.currency_data["TRY=X"].iloc[-1] if "TRY=X" in state.currency_data and not state.currency_data["TRY=X"].empty else 0
631        eur = state.currency_data["EURTRY=X"].iloc[-1] if "EURTRY=X" in state.currency_data and not state.currency_data["EURTRY=X"].empty else 0
632        gold = state.currency_data["GC=F"].iloc[-1] if "GC=F" in state.currency_data and not state.currency_data["GC=F"].empty else 0
633
634        return {
635            "status": "success",
636            "data": {
637                "xu100": idx_val,
638                "usd": f"{usd:.2f}" if usd else "N/A",
639                "eur": f"{eur:.2f}" if eur else "N/A",
640                "gold": f"${gold:.2f}" if gold else "N/A"
641            }
642        }
643    except Exception as e:
644        logger.error(f"Macro data hatası: {e}", exc_info=True)
645        return {"status": "error", "message": str(e)}
646
647
648@app.get("/api/market")
649def get_market():
650    return {
651        "regime": state.market_regime.get("regime", "Bilinmiyor"),
652        "description": state.market_regime.get("description", ""),
653        "level": state.market_regime.get("level", "unknown"),
654        "volatility": state.market_regime.get("volatility", None),
655        "trend_strength": state.market_regime.get("trend_strength", None),
656        "total_tickers": len(TARGET_TICKERS),
657    }
658
659
660@app.get("/api/recommendations")
661def get_recs():
662    return clean_nans(state.recommendations)
663
664
665@app.get("/api/market-movers")
666def get_market_movers():
667    daily = []
668    weekly = []
669    volume = []
670
671    for ticker, df in state.stock_data.items():
672        if len(df) < 6:
673            continue
674        try:
675            curr_price = float(df['Close'].iloc[-1])
676            prev_price = float(df['Close'].iloc[-2])
677            week_price = float(df['Close'].iloc[-6])
678            vol = float(df['Volume'].iloc[-1])
679
680            daily_pct = ((curr_price - prev_price) / prev_price) * 100 if prev_price > 0 else 0
681            weekly_pct = ((curr_price - week_price) / week_price) * 100 if week_price > 0 else 0
682            vol_tl = (vol * curr_price) / 1_000_000
683
684            daily.append({"ticker": ticker, "change": daily_pct, "price": curr_price})
685            weekly.append({"ticker": ticker, "change": weekly_pct, "price": curr_price})
686            volume.append({"ticker": ticker, "volume": vol_tl, "price": curr_price})
687        except Exception:
688            continue
689
690    daily_sorted = sorted(daily, key=lambda x: x["change"], reverse=True)
691    weekly_sorted = sorted(weekly, key=lambda x: x["change"], reverse=True)
692    volume_sorted = sorted(volume, key=lambda x: x["volume"], reverse=True)
693
694    return clean_nans({
695        "status": "success",
696        "daily_gainers": [x for x in daily_sorted if x["change"] >= 5][:10],
697        "daily_losers": [x for x in reversed(daily_sorted) if x["change"] <= -5][:10],
698        "weekly_gainers": weekly_sorted[:10],
699        "weekly_losers": list(reversed(weekly_sorted))[:10],
700        "volume_leaders": volume_sorted[:10]
701    })
702
703
704@app.get("/api/tickers")
705def get_tickers():
706    return ALL_BIST_TICKERS
707
708
709# =======================================================
710# STOCK DETAIL (Tekil hisse - scoring.py kullanır)
711# =======================================================
712@app.get("/api/stock/{ticker}")
713def get_stock_detail(ticker: str):
714    """Tek bir hisse için detaylı analiz verileri döndürür."""
715    try:
716        if ticker not in state.stock_data:
717            new_data = fetch_stock_data([ticker])
718            if ticker in new_data:
719                state.stock_data[ticker] = new_data[ticker]
720            else:
721                return {"status": "error", "message": f"{ticker} için veri bulunamadı."}
722
723        df = state.stock_data[ticker]
724        from data_fetcher import fetch_fundamentals
725        fundamental_data = fetch_fundamentals([ticker])
726
727        # Global model varsa onu kullan, yoksa per-ticker model eğit
728        global_model = _model_cache.get("GLOBAL_MODEL")
729        if global_model is None:
730            from ml_model import train_model
731            global_model = train_model(df, ticker)
732
733        # Hibrit skor (scoring.py - tek ortak fonksiyon)
734        result = calculate_hybrid_score(df, ticker, state.market_regime, global_model, fundamental_data)
735
736        # Multi-horizon prediction
737        multi_horizon = predict_multi_horizon(global_model, df) if global_model else {}
738
739        # Son 100 günlük fiyat geçmişi
740        price_history = []
741        last_100 = df.tail(100)
742        for idx_row, row in last_100.iterrows():
743            date_str = str(idx_row.date()) if hasattr(idx_row, 'date') else str(idx_row)
744            price_history.append({
745                "date": date_str,
746                "open": round(float(row['Open']), 2) if 'Open' in row else round(float(row['Close']), 2),
747                "high": round(float(row['High']), 2) if 'High' in row else round(float(row['Close']), 2),
748                "low": round(float(row['Low']), 2) if 'Low' in row else round(float(row['Close']), 2),
749                "close": round(float(row['Close']), 2),
750                "volume": int(row['Volume']) if 'Volume' in row else 0,
751            })
752
753        return clean_nans({
754            "status": "success",
755            "ticker": ticker,
756            "sector": get_ticker_sector(ticker),
757            "price": round(result["current_price"], 2),
758            "score": result["score"],
759            "signal": result["signal"],
760            "categories": result["details"].get("categories", {}),
761            "reasons": result["reasons"],
762            "stop_loss": round(result["details"].get("stop_loss", 0), 2),
763            "target_price": round(result["details"].get("target_price", 0), 2),
764            "risk_reward": result["details"].get("risk_reward", 0),
765            "rsi": round(result["details"].get("rsi", 0), 1) if result["details"].get("rsi") is not None else None,
766            "fibonacci": result["details"].get("fibonacci", {}),
767            "ml": result["ml_detail"] | {"confidence": result["ml_conf"], "feature_importance": result["feature_importance"], "tavan_probability": result.get("tavan_probability", 0)},
768            "multi_horizon": multi_horizon,
769            "price_history": price_history
770        })
771    except Exception as e:
772        logger.error(f"Hisse detay hatası ({ticker}): {e}", exc_info=True)
773        return {"status": "error", "message": f"Hisse detay hatası: {str(e)}"}
774
775
776# =======================================================
777# FUNDAMENTALS
778# =======================================================
779@app.get("/api/fundamentals/{ticker}")
780def get_fundamentals(ticker: str):
781    try:
782        from data_fetcher import fetch_fundamentals
783        fund_data_dict = fetch_fundamentals([ticker])
784        info = fund_data_dict.get(ticker, {})
785
786        if not info:
787            return {"status": "error", "message": "Veri bulunamadı."}
788
789        data = {
790            "market_cap": info.get("marketCap", "N/A"),
791            "trailing_pe": info.get("trailingPE", "N/A"),
792            "forward_pe": info.get("forwardPE", "N/A"),
793            "price_to_book": info.get("priceToBook", "N/A"),
794            "dividend_yield": info.get("dividendYield", "N/A"),
795            "beta": info.get("beta", "N/A"),
796            "profit_margins": info.get("profitMargins", "N/A"),
797            "fifty_two_week_high": info.get("fiftyTwoWeekHigh", "N/A"),
798            "fifty_two_week_low": info.get("fiftyTwoWeekLow", "N/A"),
799            "sector": info.get("sector", "N/A"),
800            "industry": info.get("industry", "N/A"),
801        }
802        return {"status": "success", "fundamentals": data}
803    except Exception as e:
804        return {"status": "error", "message": str(e)}
805
806
807# =======================================================
808# SCREENER (DÜZELTİLDİ - Artık cached recommendations kullanır)
809# =======================================================
810@app.get("/api/screener")
811def get_screener():
812    """Hisse tarama - cached recommendations verisinden okur, hızlı hesaplama yapar."""
813    results = []
814    import pandas_ta as ta
815
816    for rec in state.recommendations:
817        ticker = rec.get("ticker", "")
818        if ticker not in state.stock_data:
819            continue
820        df = state.stock_data[ticker]
821        if df is None or len(df) < 50:
822            continue
823
824        try:
825            # Hız Optimizasyonu: Sadece son 100 veriyi kullan
826            df_slice = df.tail(100).copy()
827            close_price = float(df_slice['Close'].iloc[-1])
828            
829            rsi_series = ta.rsi(df_slice['Close'], length=14)
830            rsi = float(rsi_series.iloc[-1]) if rsi_series is not None and not rsi_series.empty and not pd.isna(rsi_series.iloc[-1]) else 0.0
831            
832            macd_series = ta.macd(df_slice['Close'])
833            macd = 0.0
834            macd_signal = 0.0
835            if macd_series is not None and not macd_series.empty:
836                macd = float(macd_series.iloc[-1, 0]) if not pd.isna(macd_series.iloc[-1, 0]) else 0.0
837                macd_signal = float(macd_series.iloc[-1, 2]) if not pd.isna(macd_series.iloc[-1, 2]) else 0.0
838                
839            adx_series = ta.adx(df_slice['High'], df_slice['Low'], df_slice['Close'])
840            adx = 0.0
841            if adx_series is not None and not adx_series.empty:
842                adx = float(adx_series.iloc[-1, 0]) if not pd.isna(adx_series.iloc[-1, 0]) else 0.0
843                
844            vol_avg = df_slice['Volume'].rolling(20).mean().iloc[-1]
845            last_vol = df_slice['Volume'].iloc[-1]
846            volume_ratio = float(last_vol / vol_avg) if vol_avg and vol_avg > 0 and not pd.isna(vol_avg) else 0.0
847
848            macd_status = "Al" if macd > macd_signal else "Sat"
849
850            results.append({
851                "ticker": ticker,
852                "sector": rec.get("sector", get_ticker_sector(ticker)),
853                "close": round(close_price, 2),
854                "rsi": round(rsi, 2),
855                "macd": round(macd, 2),
856                "macd_status": macd_status,
857                "adx": round(adx, 2),
858                "volume_ratio": round(volume_ratio, 2),
859                "ml_score": round(float(rec.get("ml_conf", 0)), 2)
860            })
861        except Exception:
862            continue
863
864    return JSONResponse(jsonable_encoder(clean_nans({"status": "success", "data": results})))
865
866
867# =======================================================
868# SECTORS
869# =======================================================
870@app.get("/api/sectors")
871def get_sectors():
872    sectors = analyze_sectors(state.stock_data)
873    rotation = get_sector_rotation_summary(sectors)
874    return JSONResponse(jsonable_encoder({"sectors": sectors, "rotation": rotation}))
875
876
877# =======================================================
878# ALGORITHM CONFIG
879# =======================================================
880@app.get("/api/algo-config")
881def get_algo_config():
882    try:
883        data = algo_config_manager.get_config(state.firebase_db)
884        return {"status": "success", "data": data}
885    except Exception as e:
886        return {"status": "error", "message": str(e)}
887
888
889@app.post("/api/algo-config")
890def save_algo_config(req: dict):
891    try:
892        algo_config_manager.save_config(req.dict() if hasattr(req, 'dict') else req, state.firebase_db)
893        asyncio.create_task(trigger_reanalysis())
894        return {"status": "success", "message": "Algoritma ayarları kaydedildi. Analiz yeniden başlatılıyor..."}
895    except Exception as e:
896        return {"status": "error", "message": str(e)}
897
898
899async def trigger_reanalysis():
900    logger.info("Kullanıcı algoritma ayarlarını değiştirdi, analiz baştan başlıyor...")
901    await asyncio.sleep(1)
902    import threading
903    threading.Thread(target=run_analysis, daemon=True).start()
904
905
906# =======================================================
907# ML DASHBOARD (Yeni Özellik)
908# =======================================================
909@app.get("/api/ml-dashboard")
910def get_ml_dashboard():
911    """ML model performans metrikleri dashboard'u."""
912    dashboard = get_ml_dashboard_data()
913    
914    # Recommendation'lardan ek istatistikler ekle
915    if state.recommendations:
916        ml_confs = [r.get("ml_conf", 0) for r in state.recommendations if r.get("ml_conf", 0) > 0]
917        if ml_confs:
918            dashboard["prediction_stats"] = {
919                "total_predictions": len(ml_confs),
920                "avg_confidence": round(sum(ml_confs) / len(ml_confs), 1),
921                "bullish_count": len([c for c in ml_confs if c > 50]),
922                "bearish_count": len([c for c in ml_confs if c <= 50]),
923                "high_conviction": len([c for c in ml_confs if c > 70]),
924            }
925    
926    return {"status": "success", "data": dashboard}
927
928
929# =======================================================
930# WEBSOCKET
931# =======================================================
932@app.websocket("/ws")
933async def websocket_endpoint(websocket: WebSocket):
934    await ws_manager.connect(websocket)
935    try:
936        while True:
937            data = await websocket.receive_text()
938    except WebSocketDisconnect:
939        ws_manager.disconnect(websocket)
940
941
942async def live_data_broadcaster():
943    """Arka planda canlı fiyatları WebSocket üzerinden yayınlar."""
944    while True:
945        try:
946            if ws_manager.active_connections:
947                top_tickers = [r['ticker'] for r in state.recommendations[:20]] if state.recommendations else []
948                if top_tickers:
949                    loop = asyncio.get_event_loop()
950                    df = await loop.run_in_executor(None, lambda: yf.download(top_tickers, period="1d", progress=False)["Close"])
951                    if not df.empty:
952                        updates = []
953                        for ticker in top_tickers:
954                            try:
955                                if isinstance(df, pd.DataFrame) and ticker in df.columns:
956                                    last_price = float(df[ticker].iloc[-1])
957                                elif isinstance(df, pd.Series) and len(top_tickers) == 1:
958                                    last_price = float(df.iloc[-1])
959                                else:
960                                    continue
961                                    
962                                if not pd.isna(last_price):
963                                    updates.append({"ticker": ticker, "price": round(last_price, 2)})
964                            except Exception:
965                                pass
966                        if updates:
967                            await ws_manager.broadcast({"type": "live_prices", "data": updates})
968            await asyncio.sleep(20)
969        except Exception as e:
970            logger.error(f"WebSocket Broadcaster Hatası: {e}")
971            await asyncio.sleep(20)
972
973
974# =======================================================
975# STARTUP (Tek bir startup event - duplicate kaldırıldı)
976# =======================================================
977@app.on_event("startup")
978async def startup_event():
979    # Uvicorn handler'larına zorla filtre uygula (CLI ezmesini geçersiz kılmak için)
980    import logging
981    filt = EndpointFilter()
982    for name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]:
983        lgr = logging.getLogger(name)
984        lgr.addFilter(filt)
985        for handler in lgr.handlers:
986            handler.addFilter(filt)
987
988    import threading
989    threading.Thread(target=run_analysis, daemon=True).start()
990    schedule_bot_runner()
991    schedule_hourly_update()
992    asyncio.create_task(live_data_broadcaster())
993
994
995# =======================================================
996# CLIENT LOGS
997# =======================================================
998from pydantic import BaseModel
999
1000class ClientLog(BaseModel):
1001    time: str
1002    type: str
1003    msg: str
1004    source: str = ""
1005
1006@app.post("/api/client-log")
1007async def receive_client_log(log: ClientLog):
1008    logger.warning(f"[FRONTEND] {log.time} | {log.type} | {log.msg} | {log.source}")
1009    return {"status": "ok"}
1010
1011
1012# Statik dosyaları sunma
1013app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend")
1014
1015if __name__ == "__main__":
1016    import uvicorn
1017    uvicorn.run("server:app", host="127.0.0.1", port=8000, reload=True)
1018