"""Hourly Trading 212 snapshot. One Portfolio row per portfolio name (currently just 'pie'); one PortfolioSnapshot per run; N Position rows.""" from __future__ import annotations import asyncio import httpx from sqlalchemy import select from app.config import get_settings from app.db import utcnow from app.jobs._helpers import job_lifecycle, log from app.models import Portfolio, PortfolioSnapshot, Position from app.services.trading212 import Trading212 PORTFOLIO_NAME = "pie" # only one for now; multi-portfolio extension is schema-ready async def run() -> None: async with job_lifecycle("portfolio_job") as (session, jr): if jr.status == "skipped": return s = get_settings() if not (s.API_KEY and s.SECRET_KEY): log.warning("portfolio_job.skipped_no_creds") jr.status = "skipped" return t212 = Trading212() async with httpx.AsyncClient(follow_redirects=True) as client: summary = await t212.summary(client) positions = await t212.positions(client) # The instruments call is heavy (~5 MB / 17k rows) but it's our # only path to a human-readable name per ticker. Once per hour is # fine; later we could cache to disk. try: instruments = await t212.instruments(client) name_by_ticker = { i["ticker"]: i.get("name") or i.get("shortName") or i["ticker"] for i in (instruments or []) } except Exception: name_by_ticker = {} portfolio = ( await session.execute( select(Portfolio).where(Portfolio.name == PORTFOLIO_NAME) ) ).scalar_one_or_none() if portfolio is None: portfolio = Portfolio( name=PORTFOLIO_NAME, source="trading212", currency=summary.get("currency", "GBP"), ) session.add(portfolio) await session.flush() # need id for FK cash = (summary.get("cash") or {}) investments = (summary.get("investments") or {}) snap = PortfolioSnapshot( portfolio_id=portfolio.id, snapshot_at=utcnow(), total_value=summary.get("totalValue"), cash=cash.get("availableToTrade"), invested=investments.get("currentValue"), raw_json=summary, ) session.add(snap) await session.flush() for p in positions or []: tkr = p.get("ticker", "") session.add(Position( snapshot_id=snap.id, ticker=tkr, name=name_by_ticker.get(tkr), quantity=p.get("quantity"), average_price=p.get("averagePrice"), current_price=p.get("currentPrice"), ppl=p.get("ppl"), )) await session.commit() jr.items_written = len(positions or []) + 1 log.info("portfolio_job.done", positions=len(positions or [])) if __name__ == "__main__": asyncio.run(run())