From b1c8a91ed6ddebf8c056c8b1d4025686e69a6e6c Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Fri, 28 Aug 2026 20:28:25 +0700 Subject: [PATCH] feat(scheduler): daily Siamchart SET50 board collection (writes master + vintage, feeds dashboard) --- backend/app/scheduler.py | 58 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index d28e29a..655d25b 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -53,7 +53,7 @@ _FREQ_SECONDS: dict[str, int] = { # Data that legitimately changes every day (or faster) is refreshed separately # at a daily cadence rather than on the slow factor loop. _DAILY_JOBS: List[dict] = [ - {"key": "siamchart_vintages", "label": "Siamchart SET50 snapshot (vintage)", "fn": "_record_siamchart_vintages", "frequency": "daily"}, + {"key": "siamchart_vintages", "label": "Siamchart SET50 snapshot (vintage)", "fn": "_collect_siamchart_daily", "frequency": "daily"}, {"key": "price_snapshot", "label": "ราคาหุ้น SET50 (Yahoo)", "fn": "_refresh_price_snapshot", "frequency": "daily"}, ] @@ -279,6 +279,62 @@ class AppDataScheduler: except Exception: # noqa: BLE001 log.exception("set50 siamchart vintage persist failed (non-fatal)") + def _collect_siamchart_daily(self) -> None: + """Daily: fetch the Siamchart SET50 fundamental board, write the master + snapshot, and persist a vintage (so dashboard + strict backtest are fed). + + This is what actually puts stocks on the dashboard page: on a fresh + deploy the volume starts empty (no ``set50_master.json``), so the board + stays blank until the first successful daily collection. We fetch the + group table (``fetch_financial``) then each symbol's stock-info page + for ratios/income (``fetch_stock_info``), write ``set50_master.json`` + (the exact shape ``_load_siamchart_snapshot``/``siamchart_factors`` + consume), and finally record a vintage. + + Failures are logged to the source-health panel via the caller's + exception handling; on network error the previous master is retained. + """ + import datetime as _dt + import json + from . import siamchart + from .siamchart_vintages import SiamchartVintageStore, SiamchartVintageError + + rows = siamchart.fetch_financial("SET50") + details: dict = {} + for row in rows: + try: + info = siamchart.fetch_stock_info(row.symbol) + details[row.symbol] = info.to_dict() + except siamchart.SiamchartError as exc: + log.warning("set50 stock-info %s failed: %s", row.symbol, exc) + details[row.symbol] = {"error": str(exc)} + + now_utc = _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + payload = { + "source": "siamchart", + "retrieved_at": now_utc, + "mode": "group", + "group": "SET50", + "url": siamchart.build_url("SET50"), + "count": len(rows), + "rows": [row.to_dict() for row in rows], + "details": details, + "details_count": len(details), + } + out = self.data_root / "siamchart" / "set50_master.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2), + encoding="utf-8") + log.info("set50 siamchart board collected (%d rows, %d details)", + len(rows), len(details)) + + # persist a vintage so the strict backtest fundamental is PIT + try: + store = SiamchartVintageStore(self.data_root) + store.persist(payload) + except SiamchartVintageError as exc: + log.warning("set50 siamchart vintage persist failed after collect: %s", exc) + def _maybe_refresh_dated_dividends(self) -> None: """Cooldown-gated fetch of real dated dividend history into the ledger.