- themes.py: 3-theme registry (tourism monthly, auto_credit monthly, refining_energy quarterly) with Thai labels + frequency; curated SET50 symbol->theme exposure map; z-normalized theme scoring and Siamchart fundamental score; 60/40 combined score (multi-theme mean) - Frequency recorded per theme so consumers don't mix different-cadence factors as same-timestamp - 8 tests; full suite pass
73 lines
2.8 KiB
Python
73 lines
2.8 KiB
Python
"""Tests for the multi-theme registry + scoring."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from app import themes
|
|
|
|
|
|
class ThemesTest(unittest.TestCase):
|
|
def test_list_themes_has_three(self) -> None:
|
|
t = themes.list_themes()
|
|
self.assertEqual(len(t), 3)
|
|
ids = {x.id for x in t}
|
|
self.assertEqual(ids, {"tourism", "auto_credit", "refining_energy"})
|
|
for x in t:
|
|
self.assertTrue(x.label_th) # Thai label present
|
|
|
|
def test_exposure_mapping_contains_expected(self) -> None:
|
|
self.assertIn("AOT", themes.THEME_SYMBOLS["tourism"])
|
|
self.assertIn("PTT", themes.THEME_SYMBOLS["refining_energy"])
|
|
self.assertIn("TISCO", themes.THEME_SYMBOLS["auto_credit"])
|
|
|
|
def test_frequency_recorded_per_theme(self) -> None:
|
|
self.assertEqual(themes.THEME_FREQUENCY["refining_energy"], "quarterly")
|
|
self.assertEqual(themes.THEME_FREQUENCY["tourism"], "monthly")
|
|
|
|
def test_build_theme_scores_z_normalizes(self) -> None:
|
|
signals = [
|
|
{"symbol": "A", "score": 10.0},
|
|
{"symbol": "B", "score": 5.0},
|
|
{"symbol": "C", "score": 0.0},
|
|
]
|
|
scores = themes.build_theme_scores("tourism", signals)
|
|
self.assertAlmostEqual(scores["A"], 1.224, places=2)
|
|
self.assertAlmostEqual(scores["B"], 0.0, places=2)
|
|
self.assertAlmostEqual(scores["C"], -1.224, places=2)
|
|
|
|
def test_siamchart_score_uses_growth_and_yield(self) -> None:
|
|
factors = {
|
|
"factors": [
|
|
{"symbol": "X", "eps_growth_yoy": 10.0, "dividend_yield": 2.0},
|
|
{"symbol": "Y", "eps_growth_yoy": -5.0, "dividend_yield": 0.0},
|
|
]
|
|
}
|
|
sc = themes.build_siamchart_score(factors)
|
|
# X = 10 + 2*2 = 14 ; Y = -5 -> X higher
|
|
self.assertGreater(sc["X"], sc["Y"])
|
|
|
|
def test_combine_60_40(self) -> None:
|
|
theme_scores = [{"A": 1.0, "B": -1.0}]
|
|
siamchart = {"A": 2.0, "B": 0.0}
|
|
merged = themes.combine_score(theme_scores, siamchart)
|
|
# A: 0.6*1.0 + 0.4*2.0 = 1.4 ; B: 0.6*(-1) + 0.4*0 = -0.6
|
|
self.assertAlmostEqual(merged["A"]["combined"], 1.4, places=5)
|
|
self.assertAlmostEqual(merged["B"]["combined"], -0.6, places=5)
|
|
self.assertAlmostEqual(merged["A"]["theme_score"], 1.0, places=5)
|
|
|
|
def test_multiple_themes_average(self) -> None:
|
|
# Symbol in two themes -> theme_score is the mean.
|
|
t1 = {"A": 1.0}
|
|
t2 = {"A": 3.0}
|
|
merged = themes.combine_score([t1, t2], {})
|
|
self.assertAlmostEqual(merged["A"]["theme_score"], 2.0, places=5)
|
|
|
|
def test_missing_sym_siamchart_gets_theme_only(self) -> None:
|
|
merged = themes.combine_score([{"A": 2.0}], {})
|
|
self.assertAlmostEqual(merged["A"]["combined"], 0.6 * 2.0, places=5)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|