| | """ |
| | 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 [] |
| |
|