Spaces:
Sleeping
Sleeping
| 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() | |
| ] | |