Files
set50-system/backend/tests/test_factor_history.py
Kunthawat Greethong d87a1ada39 [verified] Cross-theme surprise normalization + historical factor store (P4 enabler)
A. Cross-theme comparability:
- compute_theme_surprises now weight-normalizes by total |weight| (weighted
  average), so every theme surprise on same [-1,1] scale regardless of factor
  count/weight (retail 0.189->0.145; auto_credit 1.0->0.64).

B. Historical factor store (enables learning macro/demographic factors):
- New factor_history.py: append-only per-factor JSONL, dedupes unchanged
  values, rejects non-finite, records every FACTORS value each scheduler run.
- scheduler.py: jobs carry fetch_module; refresh_all records factor history
  (non-fatal); added bank_npl job.
- GET /api/v1/learning/factors?min_points= reports n_points/learnable per
  factor so users see when P4 learning unlocks (validated query parsing).
- weight_learning: generic learn_factor_series() aggregator (momentum reuses).

Independent review deleg_5dd358e3 passed=true (empty security/logic arrays);
its two robustness suggestions applied (finite guard in record(), clean 400 on
bad min_points). 234 tests pass; Vite build passes.
2026-08-27 07:32:16 +07:00

79 lines
2.9 KiB
Python

"""Tests for the historical factor store (P4 enabler)."""
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from app.factor_history import FactorHistory, FactorHistoryError
class FactorHistoryTest(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.dir = Path(self._tmp.name)
self.fh = FactorHistory(self.dir)
def tearDown(self):
self._tmp.cleanup()
def test_record_and_series_roundtrip(self):
self.assertTrue(self.fh.record("macro_consumption", 4.9, ts="2026-01-01T00:00:00+00:00"))
self.assertTrue(self.fh.record("macro_consumption", 5.2, ts="2026-02-01T00:00:00+00:00"))
series = self.fh.series("macro_consumption")
self.assertEqual(len(series), 2)
self.assertEqual(series[0]["value"], 4.9)
self.assertEqual(series[1]["value"], 5.2)
def test_duplicate_value_not_rewritten(self):
self.assertTrue(self.fh.record("f", 1.0, ts="2026-01-01T00:00:00+00:00"))
self.assertFalse(self.fh.record("f", 1.0, ts="2026-02-01T00:00:00+00:00"))
self.assertEqual(len(self.fh.series("f")), 1)
def test_none_value_not_recorded(self):
self.assertFalse(self.fh.record("f", None))
self.assertEqual(len(self.fh.series("f")), 0)
def test_non_finite_not_recorded(self):
self.assertFalse(self.fh.record("f", float("nan")))
self.assertFalse(self.fh.record("f", float("inf")))
self.assertEqual(len(self.fh.series("f")), 0)
def test_last_value(self):
self.fh.record("f", 3.0)
self.fh.record("f", 4.0)
self.assertEqual(self.fh.last_value("f"), 4.0)
self.assertIsNone(self.fh.last_value("missing"))
class RecordAllTest(unittest.TestCase):
def test_record_all_uses_fetched_dicts(self):
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as td:
fh = FactorHistory(Path(td))
fetched = {
"macro_thai": {
"private_consumption_yoy": 4.9,
"private_investment_yoy": 18.1,
"headline_inflation_yoy": 1.95,
"manufacturing_yoy": -3.1,
"tourists_ytd_mn": 16.2,
},
"auto_credit": {"new_car_sales_yoy": 20.07,
"vehicle_production": 117383.0, "auto_exports": 81526.0},
"auto_npl": {"pct_of_npls": 3.95},
"bank_npl": {"pct_of_npls": 1.07},
}
writes = fh.record_all(fetched)
# Every registered factor with a fetch module present gets a write.
self.assertGreaterEqual(len([w for w in writes.values() if w]), 5)
# macro_consumption should have a recorded value 4.9 norm path.
self.assertEqual(fh.last_value("macro_consumption"), 4.9)
if __name__ == "__main__":
unittest.main()