kaan1233/bistelligence-api
0
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 