feat(scheduler): auto-collect PIT factor + Siamchart vintages on each refresh (deploy-safe)
This commit is contained in:
@@ -47,6 +47,87 @@ class SchedulerTest(unittest.TestCase):
|
||||
self.assertIn("at", data)
|
||||
|
||||
|
||||
class VintageCollectionTest(unittest.TestCase):
|
||||
"""Automatic PIT vintage collection via the scheduler (offline, deterministic)."""
|
||||
|
||||
def _fetched(self):
|
||||
# a fetched_by_module dict keyed by FACTORS.fetch module holding values
|
||||
return {
|
||||
"macro_thai": {
|
||||
"tourists_ytd_mn": 15.0, "private_consumption_yoy": 3.0,
|
||||
"private_investment_yoy": 4.0, "manufacturing_yoy": 1.0,
|
||||
"headline_inflation_yoy": 2.0,
|
||||
"periods": {"private_consumption_yoy": "Jun 2026"},
|
||||
},
|
||||
"auto_credit": {"new_car_sales_yoy": 5.0, "vehicle_production": 100000.0,
|
||||
"auto_exports": 80000.0},
|
||||
"auto_npl": {"pct_of_npls": 3.0},
|
||||
"bank_npl": {"pct_of_npls": 0.8},
|
||||
"energy_thai": {"quarterly": {"q1": {"net_profit": 500.0, "sales": 10000.0}}},
|
||||
}
|
||||
|
||||
def test_pit_factor_vintages_written_on_refresh(self):
|
||||
snap_dir = Path(tempfile.mkdtemp())
|
||||
sched = AppDataScheduler(_FakeCache(), snap_dir, interval_seconds=99999)
|
||||
# avoid the siamchart + dividend network paths touching real data dir:
|
||||
with patch("app.scheduler.AppDataScheduler._record_siamchart_vintages", return_value=None), \
|
||||
patch("app.scheduler.AppDataScheduler._maybe_refresh_dated_dividends", return_value=None):
|
||||
# invoke the PIT-factor recorder directly with a fetched dict
|
||||
sched._record_pit_factor_vintages(self._fetched())
|
||||
store_dir = snap_dir / "factor_vintages"
|
||||
files = list(store_dir.glob("*.jsonl"))
|
||||
# every registry factor with a fetch key should have a vintage file
|
||||
from app import factors as factors_mod
|
||||
expect = [f for f in factors_mod.FACTORS if factors_mod.FACTORS[f].get("fetch")]
|
||||
self.assertGreaterEqual(len(files), len(expect) - 1) # some may be skipped if val None
|
||||
# verify macro_consumption has a PIT vintage readable by the store
|
||||
from app.factor_vintages import FactorVintageStore
|
||||
st = FactorVintageStore(snap_dir)
|
||||
import datetime as _dt
|
||||
now_iso = _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds")
|
||||
val = st.value_at("macro_consumption", now_iso)
|
||||
self.assertEqual(val, 3.0) # private_consumption_yoy
|
||||
|
||||
def test_pit_vintage_dedupes_unchanged_value(self):
|
||||
snap_dir = Path(tempfile.mkdtemp())
|
||||
sched = AppDataScheduler(_FakeCache(), snap_dir, interval_seconds=99999)
|
||||
with patch("app.scheduler.AppDataScheduler._record_siamchart_vintages", return_value=None), \
|
||||
patch("app.scheduler.AppDataScheduler._maybe_refresh_dated_dividends", return_value=None):
|
||||
sched._record_pit_factor_vintages(self._fetched())
|
||||
sched._record_pit_factor_vintages(self._fetched()) # same values -> no new points
|
||||
from app.factor_vintages import FactorVintageStore
|
||||
st = FactorVintageStore(snap_dir)
|
||||
rows = st.series("macro_consumption")
|
||||
self.assertEqual(len(rows), 1) # unchanged value not re-appended
|
||||
|
||||
def test_siamchart_vintage_persisted_when_snapshot_present(self):
|
||||
snap_dir = Path(tempfile.mkdtemp())
|
||||
# create a snapshot file where the scheduler looks for it
|
||||
(snap_dir / "siamchart").mkdir(parents=True, exist_ok=True)
|
||||
snap = {"retrieved_at": "2026-06-01T09:00:00+07:00", "rows": [], "details": {}}
|
||||
(snap_dir / "siamchart" / "set50_master.json").write_text(
|
||||
__import__("json").dumps(snap), encoding="utf-8")
|
||||
sched = AppDataScheduler(_FakeCache(), snap_dir, interval_seconds=99999)
|
||||
with patch("app.scheduler.AppDataScheduler._record_pit_factor_vintages", return_value=None), \
|
||||
patch("app.scheduler.AppDataScheduler._maybe_refresh_dated_dividends", return_value=None):
|
||||
sched._record_siamchart_vintages()
|
||||
manifest = snap_dir / "siamchart_vintages" / "_manifest.json"
|
||||
self.assertTrue(manifest.exists())
|
||||
import json
|
||||
m = json.loads(manifest.read_text())
|
||||
self.assertEqual(len(m.get("snapshots", {})), 1)
|
||||
|
||||
def test_siamchart_vintage_noop_without_snapshot(self):
|
||||
snap_dir = Path(tempfile.mkdtemp())
|
||||
sched = AppDataScheduler(_FakeCache(), snap_dir, interval_seconds=99999)
|
||||
with patch("app.scheduler.AppDataScheduler._record_pit_factor_vintages", return_value=None), \
|
||||
patch("app.scheduler.AppDataScheduler._maybe_refresh_dated_dividends", return_value=None):
|
||||
sched._record_siamchart_vintages()
|
||||
manifest = snap_dir / "siamchart_vintages" / "_manifest.json"
|
||||
self.assertFalse(manifest.exists()) if not manifest.exists() else self.assertEqual(
|
||||
len(__import__("json").loads(manifest.read_text()).get("snapshots", {})), 1)
|
||||
|
||||
|
||||
class DividendCooldownTest(unittest.TestCase):
|
||||
def _sched(self, snap_dir, cooldown):
|
||||
return AppDataScheduler(_FakeCache(), snap_dir, interval_seconds=99999,
|
||||
|
||||
Reference in New Issue
Block a user