99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import List
|
|
|
|
router = APIRouter(prefix="/api/watchlist", tags=["watchlist"])
|
|
|
|
# Intentionally duplicated from market_data.py to keep this system fully decoupled
|
|
# from the other instrument-list mechanisms (market_watchlist, INSTRUMENT_MODELS,
|
|
# instruments.json, options_vol watchlist-tickers).
|
|
_QUOTE_TYPE_TO_ASSET_CLASS = {
|
|
"CURRENCY": "forex",
|
|
"FUTURE": "energy",
|
|
"INDEX": "indices",
|
|
"ETF": "etfs",
|
|
"EQUITY": "equities",
|
|
"MUTUALFUND": "etfs",
|
|
}
|
|
|
|
|
|
@router.get("/")
|
|
def list_watchlist():
|
|
from services.database import get_instruments_watchlist
|
|
return get_instruments_watchlist()
|
|
|
|
|
|
@router.get("/quotes")
|
|
def watchlist_quotes():
|
|
from services.database import get_instruments_watchlist
|
|
from services.data_fetcher import get_quote_with_volatility
|
|
items = []
|
|
for row in get_instruments_watchlist():
|
|
q = get_quote_with_volatility(row["ticker"]) or {}
|
|
items.append({
|
|
**row,
|
|
"price": q.get("price"),
|
|
"change_pct": q.get("change_pct"),
|
|
"volatility_pct": q.get("volatility_pct"),
|
|
"volatility_change_pct": q.get("volatility_change_pct"),
|
|
})
|
|
return {"items": items}
|
|
|
|
|
|
@router.post("/{ticker}")
|
|
def add_ticker(ticker: str):
|
|
from services.data_fetcher import get_quote
|
|
from services.database import get_instruments_watchlist, add_instrument_watchlist
|
|
import yfinance as yf
|
|
ticker = ticker.strip().upper()
|
|
if any(row["ticker"] == ticker for row in get_instruments_watchlist()):
|
|
raise HTTPException(409, f"'{ticker}' is already in the watchlist")
|
|
q = get_quote(ticker)
|
|
if not q or not q.get("price"):
|
|
raise HTTPException(400, f"Ticker '{ticker}' not found on yfinance")
|
|
name = ticker
|
|
asset_class = "unknown"
|
|
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(), "unknown")
|
|
except Exception:
|
|
pass
|
|
add_instrument_watchlist(ticker, name, asset_class)
|
|
return {"ticker": ticker, "name": name, "asset_class": asset_class, "price": q["price"]}
|
|
|
|
|
|
@router.delete("/{ticker}")
|
|
def remove_ticker(ticker: str):
|
|
from services.database import remove_instrument_watchlist
|
|
remove_instrument_watchlist(ticker.strip().upper())
|
|
return {"removed": ticker.strip().upper()}
|
|
|
|
|
|
class ReorderBody(BaseModel):
|
|
tickers: List[str]
|
|
|
|
|
|
@router.put("/reorder")
|
|
def reorder(body: ReorderBody):
|
|
from services.database import reorder_instruments_watchlist
|
|
reorder_instruments_watchlist(body.tickers)
|
|
return {"ok": True}
|
|
|
|
|
|
class SaxoLinkBody(BaseModel):
|
|
saxo_symbol: str | None = None
|
|
|
|
|
|
@router.put("/{ticker}/saxo-link")
|
|
def set_saxo_link(ticker: str, body: SaxoLinkBody):
|
|
"""Link this tracked instrument to a Saxo watchlist symbol (or pass null to unlink)
|
|
so Options Lab's Saxo section shows broker data for it. Adds the symbol to
|
|
services.saxo_scheduler's watchlist automatically if it wasn't already there."""
|
|
from services.database import set_instrument_watchlist_saxo_symbol
|
|
ok = set_instrument_watchlist_saxo_symbol(ticker, body.saxo_symbol)
|
|
if not ok:
|
|
raise HTTPException(404, f"'{ticker}' is not in the instruments watchlist")
|
|
return {"ticker": ticker.strip().upper(), "saxo_symbol": (body.saxo_symbol or "").strip().upper() or None}
|