from fastapi import APIRouter, Query from typing import Optional, Dict, Any from datetime import datetime from services.data_fetcher import get_all_quotes, get_historical, compute_historical_iv, WATCHLIST _macro_cache: Dict[str, Any] = {} router = APIRouter(prefix="/api/market", tags=["market"]) @router.get("/quotes") def quotes_all(): return get_all_quotes() @router.get("/quote/{symbol}") def quote_single(symbol: str): from services.data_fetcher import get_quote return get_quote(symbol) @router.get("/history/{symbol}") def history( symbol: str, period: str = Query("1y", description="1d,5d,1mo,3mo,6mo,1y,2y,5y"), interval: str = Query("1d", description="1m,5m,15m,1h,1d,1wk,1mo"), ): return get_historical(symbol, period, interval) @router.get("/iv/{symbol}") def implied_vol(symbol: str, window: int = 30): iv = compute_historical_iv(symbol, window) return {"symbol": symbol, "iv": iv, "window": window} @router.get("/watchlist") def watchlist(): return WATCHLIST @router.get("/custom-tickers") def list_custom_tickers(): from services.database import get_market_custom_tickers return get_market_custom_tickers() _QUOTE_TYPE_TO_ASSET_CLASS = { "CURRENCY": "forex", "FUTURE": "energy", "INDEX": "indices", "ETF": "etfs", "EQUITY": "equities", "MUTUALFUND": "etfs", } @router.post("/custom-tickers/{ticker}") def add_custom_ticker(ticker: str): from services.data_fetcher import get_quote from services.database import add_market_custom_ticker import yfinance as yf ticker = ticker.strip().upper() q = get_quote(ticker) if not q or not q.get("price"): from fastapi import HTTPException raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance") name = ticker asset_class = "custom" try: info = yf.Ticker(ticker).fast_info name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or ticker quote_type = getattr(info, "quote_type", "") or "" asset_class = _QUOTE_TYPE_TO_ASSET_CLASS.get(quote_type.upper(), "custom") except Exception: pass add_market_custom_ticker(ticker, name, asset_class) return {"ticker": ticker, "name": name, "asset_class": asset_class, "price": q["price"]} @router.delete("/custom-tickers/{ticker}") def remove_custom_ticker(ticker: str): from services.database import remove_market_custom_ticker remove_market_custom_ticker(ticker.strip().upper()) return {"removed": ticker.upper()} @router.get("/validate") def validate_ticker(symbol: str = Query(..., description="Ticker to validate against yfinance")): """Check if a ticker is fetchable. Returns valid=True + live price/name, or valid=False + reason.""" from services.data_fetcher import get_quote import yfinance as yf symbol = symbol.strip().upper() q = get_quote(symbol) if q and q.get("price"): # Try to get a display name try: info = yf.Ticker(symbol).fast_info name = getattr(info, "long_name", None) or getattr(info, "short_name", None) or symbol except Exception: name = symbol return {"valid": True, "symbol": symbol, "name": name, "price": q["price"], "change_pct": q.get("change_pct")} return {"valid": False, "symbol": symbol, "reason": f"Ticker '{symbol}' not found on yfinance — check the symbol (e.g. GC=F for Gold, ^GSPC for S&P 500)"} @router.get("/macro-regime") def macro_regime(force: bool = False): """Macro gauge values + 5-scenario scoring. Cached 15 min.""" from services.data_fetcher import get_macro_gauges, score_macro_scenarios now = datetime.utcnow() if not force and _macro_cache.get("data") and _macro_cache.get("ts"): age = (now - _macro_cache["ts"]).total_seconds() if age < 900: return {**_macro_cache["data"], "cached": True, "cache_age_sec": int(age)} gauges = get_macro_gauges() scenarios = score_macro_scenarios(gauges) result: Dict[str, Any] = { "gauges": gauges, "scenarios": scenarios, "fetched_at": now.isoformat(), "cached": False, } _macro_cache["data"] = result _macro_cache["ts"] = now today = now.strftime("%Y-%m-%d") if force: # Build a compact gauge summary (key → value + change_pct) for the journal gauges_summary = { k: {"value": v.get("value"), "change_pct": v.get("change_pct"), "label": v.get("label")} for k, v in gauges.items() if v.get("value") is not None or v.get("change_pct") is not None } from services.database import log_macro_regime log_macro_regime( dominant=scenarios.get("dominant", "incertain"), scores=scenarios.get("scores", {}), reasons=scenarios.get("reasons", {}), gauges_summary=gauges_summary, ) # Always persist full snapshot once per calendar day (all gauges + regime) from services.database import save_macro_gauge_snapshot, macro_gauge_snapshot_exists_today if force or not macro_gauge_snapshot_exists_today(): save_macro_gauge_snapshot( snapshot_date=today, gauges=gauges, dominant=scenarios.get("dominant", "incertain"), regime_scores=scenarios.get("scores", {}), ) return result @router.get("/macro-gauges/at") def macro_gauges_at(date: str = Query(..., description="YYYY-MM-DD")): """Return the macro gauge snapshot at or before a given date.""" from services.database import get_macro_gauge_snapshot_at snap = get_macro_gauge_snapshot_at(date) if not snap: return {"snapshot_date": None, "gauges": {}, "dominant": "incertain", "regime_scores": {}} return snap @router.get("/macro-gauges/history") def macro_gauges_history(days: int = Query(30, ge=1, le=365)): """Return gauge snapshots for the last N days (daily, most recent first).""" from services.database import get_macro_gauge_history return get_macro_gauge_history(days=days)