(a) R1-R5 (factor-refinement, grounded in methodology-research.md): - R1 (PEAD): EPS-growth weight raised 1.0->1.5 in build_siamchart_score / symbol_breakdown (Bernard-Thomas 1990, Livnat-Mendenhall 2006) - R2 (momentum): 12-1 momentum factor from Yahoo price snapshot (Jegadeesh-Titman 93; lite weight 0.5) - R3 (regime): binary bear gate -> continuous stress = negative-themes fraction, smooth LONG/SHORT shift - R5 (dividend screen): non-dividend / cut-yield names no longer go LONG (screen-off) - R4 (earnings-revision) deferred: no free EPS-forecast source yet (documented) (b) bank-sector NPL collector (BOT reportID 794, financial&insurance sector): - refactored auto_npl to expose shared _parse_sector; new bank_npl.py reuses it - registered bank_npl FACTOR -> auto-appears in sources table (6 rows) + blends into banks theme surprise (real NPL) - +unit tests (test_bank_npl), test_dashboard updated (6 sources) 205 tests pass; verified live API (banks surprise incl. NPL 1.07, 6 sources).
71 lines
2.8 KiB
Python
71 lines
2.8 KiB
Python
"""Tests for the real multi-theme dashboard assembly."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from app.dashboard import RealDashboard, _auto_read, _zscore
|
|
|
|
|
|
class _FakeCache:
|
|
"""Minimal cache that invokes the fetcher each call."""
|
|
def __init__(self, data: dict):
|
|
self.data = data
|
|
def fetch_or_stale(self, key, fetcher):
|
|
val = self.data.get(key)
|
|
if val is not None:
|
|
return val
|
|
return fetcher()
|
|
|
|
|
|
class DashboardTest(unittest.TestCase):
|
|
def test_zscore_centers(self):
|
|
self.assertAlmostEqual(_zscore(1.0, 1.0, 1.0), 0.0)
|
|
self.assertAlmostEqual(_zscore(2.0, 1.0, 1.0), 1.0)
|
|
|
|
def test_auto_read_multisource(self):
|
|
auto = {"new_car_sales_yoy": 20.07, "total_vehicle_sales": 59000,
|
|
"vehicle_production": 120000, "auto_exports": 80000}
|
|
npl = {"pct_of_npls": 3.95, "npl_amount": 20602}
|
|
read = _auto_read(auto, npl, _FakeCache({}))
|
|
self.assertEqual(read["new_car_sales_yoy"], 20.07)
|
|
self.assertEqual(read["auto_npl_pct"], 3.95)
|
|
self.assertIn("thesis", read)
|
|
|
|
@patch("app.auto_credit.fetch_auto_credit")
|
|
@patch("app.auto_npl.fetch_auto_npl")
|
|
@patch("app.energy_thai.fetch_energy_thai")
|
|
@patch("app.macro_thai.fetch_macro_thai")
|
|
@patch("app.bank_npl.fetch_bank_npl")
|
|
def test_build_returns_structure(self, bnpl, macro, energy, npl, auto):
|
|
class _Factory:
|
|
def __init__(self, data): self._data = data
|
|
def to_dict(self): return self._data
|
|
macro.return_value = _Factory({
|
|
"private_consumption_yoy": 4.9, "headline_inflation_yoy": 1.95, "periods": {}})
|
|
energy.return_value = _Factory({
|
|
"quarterly": {"Q2/2026": {"net_profit": 8000.0, "ebitda": 9000.0}}})
|
|
auto.return_value = _Factory({
|
|
"new_car_sales_yoy": 20.07, "total_vehicle_sales": 59000})
|
|
npl.return_value = _Factory({
|
|
"pct_of_npls": 3.95, "npl_amount": 20602, "period": "Q2/2568"})
|
|
bnpl.return_value = _Factory({
|
|
"pct_of_npls": 1.07, "npl_amount": 5598, "period": ""})
|
|
cache = _FakeCache({})
|
|
dash = RealDashboard([], cache).build()
|
|
self.assertEqual(len(dash["themes"]), 13) # all SET50 themes
|
|
self.assertEqual(len(dash["sources"]), 6) # auto-derived from FACTORS (bank_npl added)
|
|
self.assertIn("macro", dash)
|
|
self.assertIn("board", dash)
|
|
|
|
def test_auto_read_npl_piece(self):
|
|
read = _auto_read({"new_car_sales_yoy": -3.0, "total_vehicle_sales": 20000},
|
|
{"pct_of_npls": 6.0, "npl_amount": 90000}, _FakeCache({}))
|
|
self.assertEqual(read["new_car_sales_yoy"], -3.0)
|
|
self.assertEqual(read["auto_npl_pct"], 6.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|