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.
79 lines
2.9 KiB
Python
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()
|