DB: - New table macro_gauge_snapshots (daily snapshot of all 28+ gauges + dominant + scores) - save_macro_gauge_snapshot / get_macro_gauge_snapshot_at / get_macro_gauge_history - Auto-save once per calendar day on every macro-regime fetch (not just force=True) API: - GET /api/market/macro-gauges/at?date=YYYY-MM-DD — nearest snapshot ≤ date - GET /api/market/macro-gauges/history?days=N Detector (_check_macro_gauges in Eco Desk): - Regime transition events (goldilocks→stagflation etc.) with severity scoring - Yield curve inversion / désinversion (slope_10y3m sign change) - DXY shock (% change over lookback window) - Credit stress (HYG drop threshold) - Gold/Copper ratio regime crossings InstrumentDashboard: - macroAtDate state: fetches /api/market/macro-gauges/at when crosshair date ≠ last date - RegimeCard uses historical macro regime when on a past date - MacroGaugePanel: full breakdown of all gauges by bloc (liquidité, crédit, énergie...) visible only when on a historical date — shows value + change_pct + regime scores bar AIDesks: added fundamental + sentiment to AIDesk type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
171 lines
6.0 KiB
Python
171 lines
6.0 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
|
|
|
|
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)
|