StockPred-Backend / app /services /market_data.py
Anurag33Gaikwad's picture
Update app/services/market_data.py
64b1b6e verified
Raw
History Blame Contribute Delete
5.33 kB
from typing import Tuple
import time
import pandas as pd
import requests
from fastapi import HTTPException
from app.config import get_settings
from app.indicators import add_technical_indicators
settings = get_settings()
ALPHA_VANTAGE_URL = "https://www.alphavantage.co/query"
_CACHE: dict[str, dict] = {}
_CACHE_TTL = 60 # seconds (safe for AV free tier)
# -------------------------
# Helpers
# -------------------------
def is_crypto(ticker: str) -> bool:
t = ticker.upper()
return "-" in t or t in {"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE"}
def _safe_get_json(params: dict) -> dict:
try:
resp = requests.get(ALPHA_VANTAGE_URL, params=params, timeout=10)
return resp.json()
except Exception:
raise HTTPException(
status_code=503,
detail="Market data provider unavailable",
)
def _detect_ohlcv_columns(df: pd.DataFrame) -> pd.DataFrame:
"""
Robust OHLCV resolver for Alpha Vantage crypto.
Works for:
- 1a / 1b
- USD / non-USD
- future schema changes
"""
def find(keywords):
for c in df.columns:
name = c.lower()
if all(k in name for k in keywords):
return c
return None
open_col = find(["open"])
high_col = find(["high"])
low_col = find(["low"])
close_col = find(["close"])
volume_col = find(["volume"])
if not all([open_col, high_col, low_col, close_col, volume_col]):
raise HTTPException(
status_code=500,
detail=f"Unsupported crypto data format from Alpha Vantage: {list(df.columns)}",
)
df = df[[open_col, high_col, low_col, close_col, volume_col]]
df.columns = ["Open", "High", "Low", "Close", "Volume"]
return df
# -------------------------
# STOCK DATA
# -------------------------
def _fetch_stock_history(ticker: str) -> pd.DataFrame:
params = {
"function": "TIME_SERIES_DAILY",
"symbol": ticker,
"apikey": settings.alpha_vantage_api_key,
"outputsize": "compact", # FREE tier only
}
data = _safe_get_json(params)
if "Time Series (Daily)" not in data:
if "Note" in data or "Information" in data:
raise HTTPException(429, "Alpha Vantage rate limit exceeded")
raise HTTPException(400, "Invalid stock ticker symbol")
df = pd.DataFrame.from_dict(
data["Time Series (Daily)"], orient="index"
)
df.rename(
columns={
"1. open": "Open",
"2. high": "High",
"3. low": "Low",
"4. close": "Close",
"5. volume": "Volume",
},
inplace=True,
)
df = df[["Open", "High", "Low", "Close", "Volume"]]
df = df.apply(pd.to_numeric, errors="coerce").dropna()
df.index = pd.to_datetime(df.index)
df.sort_index(inplace=True)
if len(df) < settings.history_window + 50:
raise HTTPException(400, "Not enough historical stock data")
return df
# -------------------------
# CRYPTO DATA
# -------------------------
def _fetch_crypto_history(ticker: str) -> pd.DataFrame:
if "-" in ticker:
symbol, market = ticker.split("-", 1)
else:
symbol, market = ticker, "USD"
params = {
"function": "DIGITAL_CURRENCY_DAILY",
"symbol": symbol.upper(),
"market": market.upper(),
"apikey": settings.alpha_vantage_api_key,
}
data = _safe_get_json(params)
if "Time Series (Digital Currency Daily)" not in data:
if "Note" in data or "Information" in data:
raise HTTPException(429, "Alpha Vantage rate limit exceeded")
raise HTTPException(400, "Invalid crypto ticker symbol")
df = pd.DataFrame.from_dict(
data["Time Series (Digital Currency Daily)"], orient="index"
)
df = _detect_ohlcv_columns(df)
df = df.apply(pd.to_numeric, errors="coerce")
df.dropna(subset=["Open", "High", "Low", "Close"], inplace=True)
df.index = pd.to_datetime(df.index)
df.sort_index(inplace=True)
if len(df) < settings.history_window + 50:
raise HTTPException(400, "Not enough historical crypto data")
return df
# -------------------------
# Cached unified fetcher
# -------------------------
def fetch_raw_history(ticker: str) -> pd.DataFrame:
key = ticker.upper()
now = time.time()
if key in _CACHE and now - _CACHE[key]["ts"] < _CACHE_TTL:
return _CACHE[key]["df"]
df = (
_fetch_crypto_history(key)
if is_crypto(key)
else _fetch_stock_history(key)
)
_CACHE[key] = {"df": df, "ts": now}
return df
# -------------------------
# Public API
# -------------------------
def get_enriched_history(ticker: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
df_raw = fetch_raw_history(ticker)
df_tech = add_technical_indicators(df_raw)
if len(df_tech) < settings.history_window:
raise HTTPException(
status_code=400,
detail="Insufficient data after technical indicator calculation",
)
return df_raw, df_tech
def last_n_candles(df: pd.DataFrame, n: int) -> list[dict]:
return [
{
"date": idx.strftime("%Y-%m-%d"),
"price": float(row["Close"]),
}
for idx, row in df.tail(n).iterrows()
]