"""Fetch retail stock quotes — delegates to tools-api when available.""" from __future__ import annotations import json import os from typing import Any from urllib.request import Request, urlopen USER_AGENT = "Foodlinkk-MarketIntel/1.0" TOOLS = os.getenv("TOOLS_API_URL", "http://tools-api:8700").rstrip("/") RETAIL_STOCKS = [ {"symbol": "AD.AS", "name": "Ahold Delhaize", "chain": "Albert Heijn / Gall", "market": "Euronext"}, {"symbol": "TSCO.L", "name": "Tesco", "chain": "Tesco UK", "market": "LSE"}, {"symbol": "CAR.PA", "name": "Carrefour", "chain": "Carrefour EU", "market": "Euronext Paris"}, {"symbol": "SBRY.L", "name": "Sainsbury's", "chain": "Sainsbury's", "market": "LSE"}, {"symbol": "MKS.L", "name": "Marks & Spencer", "chain": "M&S Food", "market": "LSE"}, {"symbol": "WMT", "name": "Walmart", "chain": "Global benchmark", "market": "NYSE"}, {"symbol": "ULVR.L", "name": "Unilever", "chain": "FMCG / food", "market": "LSE"}, ] def _fetch_chart(symbol: str) -> dict[str, Any]: url = ( f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}" f"?interval=1d&range=1mo&includePrePost=false" ) req = Request(url, headers={"User-Agent": USER_AGENT}) with urlopen(req, timeout=12) as resp: payload = json.loads(resp.read().decode()) result = (payload.get("chart") or {}).get("result") or [] if not result: return {} meta = result[0].get("meta") or {} closes = (result[0].get("indicators") or {}).get("quote") or [{}] close_series = closes[0].get("close") or [] valid = [c for c in close_series if c is not None] sparkline = valid[-14:] if len(valid) >= 14 else valid prev = valid[-2] if len(valid) >= 2 else None last = valid[-1] if valid else meta.get("regularMarketPrice") change_pct = meta.get("regularMarketChangePercent") if change_pct is None and prev and last and prev: change_pct = ((last - prev) / prev) * 100 return { "price": meta.get("regularMarketPrice") or last, "currency": meta.get("currency") or "EUR", "change_pct": round(float(change_pct or 0), 2), "sparkline": [round(float(v), 2) for v in sparkline], "market_state": meta.get("marketState") or "CLOSED", } def fetch_retail_quotes() -> list[dict[str, Any]]: try: req = Request(f"{TOOLS}/retail/market/stocks", headers={"User-Agent": USER_AGENT}) with urlopen(req, timeout=15) as resp: data = json.loads(resp.read().decode()) if data.get("items"): return data["items"] except Exception: pass items: list[dict[str, Any]] = [] for stock in RETAIL_STOCKS: row = dict(stock) try: chart = _fetch_chart(stock["symbol"]) row.update(chart) row["trend"] = "up" if (row.get("change_pct") or 0) >= 0 else "down" except Exception: row["price"] = None row["change_pct"] = 0 row["sparkline"] = [] row["trend"] = "flat" items.append(row) return items def market_summary(quotes: list[dict[str, Any]] | None = None) -> dict[str, Any]: quotes = quotes or fetch_retail_quotes() valid = [q for q in quotes if q.get("price") is not None] avg_change = sum(float(q.get("change_pct") or 0) for q in valid) / len(valid) if valid else 0 best = max(valid, key=lambda q: float(q.get("change_pct") or 0), default=None) worst = min(valid, key=lambda q: float(q.get("change_pct") or 0), default=None) return { "avg_change_pct": round(avg_change, 2), "best_performer": best, "worst_performer": worst, "quote_count": len(valid), }