ticker-validate: add /api/ticker/validate endpoint

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-27 14:41:33 +02:00
parent ae3f104fa7
commit 3bb62763ea
2 changed files with 140 additions and 0 deletions

View file

@ -0,0 +1,78 @@
"""Per-ticker validation + historical-price endpoints.
These power the dashboard's "Add a position" form. Neither endpoint
persists holdings they wrap the existing Yahoo chart fetcher and
optionally seed anonymous ticker_universe (validate only).
Both endpoints are gated behind ``require_paid`` so they match the rest
of the import surface.
"""
from __future__ import annotations
from datetime import datetime, timezone
import httpx
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import require_auth
from app.db import get_session, utcnow
from app.logging import get_logger
from app.models import Quote as QuoteModel
from app.services.access import require_paid
from app.services.market import (
UA, YAHOO_CHART, Quote, _yahoo_range_covering, fetch_yahoo,
)
from app.services.ticker_universe import upsert_tickers
log = get_logger("ticker_validate")
router = APIRouter(dependencies=[Depends(require_auth)])
@router.get(
"/ticker/validate",
dependencies=[Depends(require_paid)],
)
async def validate_ticker(
symbol: str,
session: AsyncSession = Depends(get_session),
) -> dict:
"""Live quote for one ticker.
Returns ``{ok: true, symbol, price, currency, as_of}`` on success
or ``{ok: false, error}`` when the symbol isn't recognised. Seeds
ticker_universe + writes a Quote row as a side-effect on success
so the dashboard's /api/universe call picks it up on the next
refresh."""
symbol = symbol.strip().upper()[:32]
if not symbol:
return {"ok": False, "error": "symbol required"}
async with httpx.AsyncClient(follow_redirects=True, timeout=15) as client:
quote = await fetch_yahoo(client, symbol, symbol, "")
if quote.error or quote.price is None:
log.info("ticker.validate.miss", symbol=symbol,
error=(quote.error or "no price")[:120])
return {"ok": False, "error": "Symbol not recognised"}
# Side-effect: seed the universe + write the quote so /api/universe
# has data on the next minute-cycle refresh.
await upsert_tickers(session, [symbol])
session.add(QuoteModel(
symbol=quote.symbol, source=quote.source, label=quote.label,
group_name="universe", price=quote.price, currency=quote.currency,
as_of=quote.as_of, changes=quote.changes or None, error=None,
fetched_at=utcnow(),
))
await session.commit()
return {
"ok": True,
"symbol": quote.symbol,
"price": quote.price,
"currency": quote.currency,
"as_of": quote.as_of,
}