""" API-ready versions of stock data functions Streamlit bağımlılıkları olmayan versiyonlar """ import pandas as pd from datetime import datetime, timedelta import numpy as np from typing import Any, Dict, Optional, Tuple import json import urllib.request import urllib.error def _get_yfinance(): try: import yfinance as yf # type: ignore return yf except Exception as exc: raise RuntimeError( "yfinance is not available or is incompatible with this Python runtime" ) from exc def _normalize_yahoo_symbol(symbol: str) -> str: sym = str(symbol or "").strip().upper() if not sym: return "" if sym.endswith(".IS"): return sym return f"{sym}.IS" def _probe_yahoo_chart_status( yahoo_symbol: str, period: str, interval: str, timeout_seconds: int = 10, ) -> Dict[str, Any]: url = ( f"https://query1.finance.yahoo.com/v8/finance/chart/{yahoo_symbol}" f"?range={period}&interval={interval}" ) req = urllib.request.Request( url, headers={ "User-Agent": "Mozilla/5.0", "Accept": "application/json,text/plain;q=0.9,*/*;q=0.8", }, method="GET", ) try: with urllib.request.urlopen(req, timeout=timeout_seconds) as resp: status = int(getattr(resp, "status", 200)) ct = str(resp.headers.get("content-type") or "") body = resp.read(512) or b"" body_preview = body.decode("utf-8", errors="replace") parsed_ok = False if "json" in ct.lower(): try: json.loads(body_preview) parsed_ok = True except Exception: parsed_ok = False return { "ok": status == 200, "http_status": status, "content_type": ct, "parsed_json_preview": parsed_ok, "body_preview": body_preview, "url": url, } except urllib.error.HTTPError as e: body = (e.read(512) or b"") if hasattr(e, "read") else b"" body_preview = body.decode("utf-8", errors="replace") return { "ok": False, "http_status": int(getattr(e, "code", 0) or 0), "content_type": str(getattr(e, "headers", {}).get("content-type") if getattr(e, "headers", None) else ""), "body_preview": body_preview, "url": url, } except Exception as e: return { "ok": False, "http_status": None, "error": f"{type(e).__name__}: {e}", "url": url, } def _fetch_yahoo_chart_df( yahoo_symbol: str, period: str, interval: str, timeout_seconds: int = 20, ) -> Optional[pd.DataFrame]: url = ( f"https://query1.finance.yahoo.com/v8/finance/chart/{yahoo_symbol}" f"?range={period}&interval={interval}" ) req = urllib.request.Request( url, headers={ "User-Agent": "Mozilla/5.0", "Accept": "application/json,text/plain;q=0.9,*/*;q=0.8", }, method="GET", ) with urllib.request.urlopen(req, timeout=timeout_seconds) as resp: raw = resp.read() or b"" payload = json.loads(raw.decode("utf-8", errors="strict")) chart = (payload or {}).get("chart") or {} results = chart.get("result") or [] if not results: return None r0 = results[0] or {} timestamps = r0.get("timestamp") or [] indicators = r0.get("indicators") or {} quotes = indicators.get("quote") or [] if not timestamps or not quotes: return None q0 = quotes[0] or {} open_ = q0.get("open") or [] high = q0.get("high") or [] low = q0.get("low") or [] close = q0.get("close") or [] volume = q0.get("volume") or [] df = pd.DataFrame( { "Open": open_, "High": high, "Low": low, "Close": close, "Volume": volume, } ) df.index = pd.to_datetime(pd.Series(timestamps, dtype="int64"), unit="s", utc=True) for col in ["Open", "High", "Low", "Close", "Volume"]: df[col] = pd.to_numeric(df[col], errors="coerce") df = df.dropna(subset=["Close"]) df["Volume"] = df["Volume"].fillna(0.0) if df.empty: return None return df def get_stock_data_with_status_for_api( symbol: str, period: str = "1y", interval: str = "1d", ) -> Tuple[Optional[pd.DataFrame], Dict[str, Any]]: yahoo_symbol = _normalize_yahoo_symbol(symbol) if not yahoo_symbol: return None, {"ok": False, "error_type": "invalid_symbol"} status: Dict[str, Any] = { "ok": False, "symbol": str(symbol or ""), "yahoo_symbol": yahoo_symbol, "period": period, "interval": interval, "source": "yahoo_finance", } try: yf = _get_yfinance() stock = yf.Ticker(yahoo_symbol) data = stock.history(period=period, interval=interval) if data is not None and len(data) > 0: try: fast_info = stock.fast_info if hasattr(fast_info, "last_price") and fast_info.last_price: last_idx = data.index[-1] data.loc[last_idx, "Close"] = fast_info.last_price except Exception: try: info = stock.info last_idx = data.index[-1] for key in ["regularMarketPrice", "currentPrice", "previousClose"]: if key in info and info[key]: data.loc[last_idx, "Close"] = info[key] break except Exception: pass status["ok"] = True status["error_type"] = None return data, status probe = _probe_yahoo_chart_status(yahoo_symbol, period=period, interval=interval) status["probe"] = { k: probe.get(k) for k in ["ok", "http_status", "content_type", "parsed_json_preview", "body_preview", "url", "error"] if k in probe } http_status = probe.get("http_status") if http_status == 429: status["error_type"] = "rate_limited" return None, status try: chart_df = _fetch_yahoo_chart_df(yahoo_symbol, period=period, interval=interval) if chart_df is not None and not chart_df.empty: status["ok"] = True status["error_type"] = None status["source"] = "yahoo_chart" return chart_df, status except Exception as e: status["chart_fallback_error"] = f"{type(e).__name__}: {e}" for backup_period in ["6mo", "3mo", "1mo"]: try: data2 = stock.history(period=backup_period, interval=interval) if data2 is not None and not data2.empty: status["ok"] = True status["error_type"] = None status["period"] = backup_period return data2, status except Exception: continue try: chart_df2 = _fetch_yahoo_chart_df(yahoo_symbol, period=backup_period, interval=interval) if chart_df2 is not None and not chart_df2.empty: status["ok"] = True status["error_type"] = None status["source"] = "yahoo_chart" status["period"] = backup_period return chart_df2, status except Exception: continue status["error_type"] = status.get("error_type") or "no_data" return None, status except Exception as e: status["error_type"] = "exception" status["error"] = f"{type(e).__name__}: {e}" return None, status def get_stock_data_for_api(symbol, period="1y", interval="1d"): """ API için cache'siz veri çekme fonksiyonu """ try: data, _status = get_stock_data_with_status_for_api(symbol, period=period, interval=interval) return data except Exception as e: print(f"API veri hatası ({symbol}): {e}") return None def get_market_summary_for_api(): """ API için BIST 100 özet bilgisi """ try: yf = _get_yfinance() xu100 = yf.Ticker("XU100.IS") data = xu100.history(period="5d") if data.empty: return None latest = data.iloc[-1] previous = data.iloc[-2] if len(data) > 1 else latest change = latest['Close'] - previous['Close'] change_pct = (change / previous['Close']) * 100 # Safely format date try: date_str = data.index[-1].strftime('%Y-%m-%d') # type: ignore except (AttributeError, TypeError): date_str = str(data.index[-1])[:10] return { 'index': 'XU100', 'value': float(latest['Close']), 'change': float(change), 'change_percent': float(change_pct), 'high': float(data['High'].max()), 'low': float(data['Low'].min()), 'volume': int(latest['Volume']), 'date': date_str } except Exception as e: print(f"Market summary hatası: {e}") return None def get_popular_stocks(): """ Popüler/likit hisseleri döndürür. NOTE: Hardcoded/toy list is intentionally avoided. We derive a bounded universe from official BIST index constituents. """ try: from data.index_constituents import get_index_constituents res = get_index_constituents("bist30") return res.symbols or [] except Exception: return []