feat: cockpit
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -66,6 +66,51 @@ def watchlist_quotes():
|
||||
return {"items": items}
|
||||
|
||||
|
||||
_HISTORY_PERIODS = {
|
||||
"1w": {"yf": "5d", "days": 7},
|
||||
"1m": {"yf": "1mo", "days": 30},
|
||||
"3m": {"yf": "3mo", "days": 90},
|
||||
"6m": {"yf": "6mo", "days": 180},
|
||||
"1y": {"yf": "1y", "days": 365},
|
||||
"5y": {"yf": "5y", "days": 1825},
|
||||
"max": {"yf": "max", "days": 3650},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/history/{ticker}")
|
||||
def watchlist_history(ticker: str, period: str = Query("3m")):
|
||||
"""Daily close series for the Watchlist card's chart — Saxo-sourced if this
|
||||
instrument has a saxo_quote_symbol link (see saxo-quote-link below), yfinance
|
||||
otherwise. Same source-of-truth split as /quotes above, just returning a series
|
||||
instead of a single latest point."""
|
||||
from services.database import get_instruments_watchlist, get_saxo_catalog_by_symbol
|
||||
from services.saxo_client import get_price_history
|
||||
import yfinance as yf
|
||||
|
||||
ticker = ticker.strip().upper()
|
||||
spec = _HISTORY_PERIODS.get(period.lower(), _HISTORY_PERIODS["3m"])
|
||||
|
||||
row = next((r for r in get_instruments_watchlist() if r["ticker"] == ticker), None)
|
||||
saxo_quote_symbol = row.get("saxo_quote_symbol") if row else None
|
||||
|
||||
if saxo_quote_symbol:
|
||||
try:
|
||||
entry = get_saxo_catalog_by_symbol(saxo_quote_symbol)
|
||||
asset_type = entry["asset_type"] if entry else "FxSpot"
|
||||
bars = get_price_history(saxo_quote_symbol, asset_type, days=spec["days"])
|
||||
return {"ticker": ticker, "source": "saxo", "bars": [{"date": b["date"], "close": b["close"]} for b in bars]}
|
||||
except Exception as e:
|
||||
logger.info(f"[watchlist/history] Saxo history failed for '{saxo_quote_symbol}', falling back to yfinance: {e}")
|
||||
|
||||
try:
|
||||
hist = yf.Ticker(ticker).history(period=spec["yf"], interval="1d", auto_adjust=True)
|
||||
hist = hist.dropna(subset=["Close"])
|
||||
bars = [{"date": idx.strftime("%Y-%m-%d"), "close": round(float(c), 6)} for idx, c in hist["Close"].items()]
|
||||
return {"ticker": ticker, "source": "yfinance", "bars": bars}
|
||||
except Exception as e:
|
||||
return {"ticker": ticker, "source": "none", "bars": [], "error": str(e)}
|
||||
|
||||
|
||||
@router.post("/{ticker}")
|
||||
def add_ticker(ticker: str):
|
||||
"""Adds a tracked instrument. yfinance validation is best-effort, not a gate — an
|
||||
|
||||
@@ -256,6 +256,16 @@ def portfolio_risk():
|
||||
return _sanitize(result)
|
||||
|
||||
|
||||
@router.get("/portfolio-risk-radar")
|
||||
def portfolio_risk_radar():
|
||||
"""5-axis risk radar (Concentration/Volatility/Correlation/Exposure/Drawdown) for the
|
||||
Cockpit's Risk card. Separate from /portfolio-risk above — this one makes live
|
||||
yfinance calls (per-position volatility + a correlation matrix), heavier and slower,
|
||||
so it's not bundled into the lighter endpoint other pages may poll more often."""
|
||||
from services.portfolio_risk import compute_portfolio_risk_radar
|
||||
return _sanitize(compute_portfolio_risk_radar())
|
||||
|
||||
|
||||
class TradeCheckRequest(BaseModel):
|
||||
underlying: str
|
||||
strategy: str
|
||||
|
||||
Reference in New Issue
Block a user