| """ |
| 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 |
|
|
| 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 |
| |
| |
| try: |
| date_str = data.index[-1].strftime('%Y-%m-%d') |
| 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 [] |
|
|