borsa / data /stock_data_api.py
veteroner's picture
chore: Vercel proxy, Supabase edge fn, vscode settings, deno config
3452823
"""
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 []