Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""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),
|
||||
}
|
||||
Reference in New Issue
Block a user