Files
OpenFin/backend/routers/market_data.py
OpenSquared 33097e1812 fix: custom tickers now placed in their natural asset-class tab
- Detect yfinance quote_type (CURRENCY→forex, FUTURE→energy, INDEX→indices,
  ETF→etfs, EQUITY→equities) when adding a custom ticker and persist it in
  market_watchlist.asset_class
- get_all_quotes() merges custom tickers into their proper group (e.g. EURUSD=X
  appears under Forex) instead of always under a separate "Custom" group
- "Custom" tab only shows tickers whose type couldn't be detected
- Add market_watchlist.asset_class migration; ensure backtest_lab_runs and
  market_watchlist are always created at init_db() time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 22:11:31 +02:00

142 lines
4.8 KiB
Python

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
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,
)
return result