[verified] Cover full SET50 with 13 themes + per-theme score detail in symbol view

- THEME_SYMBOLS expanded: added banks, retail, telecom_it, property, healthcare, petrochem_materials, consumer_staples, utilities, nonbank_finance, exploration -> all 49 SET50 names now in a theme
- THEME_LABELS_TH Thai labels; THEME_FREQUENCY per theme
- symbol_breakdown now lists EVERY theme the symbol belongs to (label_th + surprise, or 'ยังไม่มีข้อมูล'), so theme_score is transparent per source
- frontend: theme column maps all 49 symbols (mirrors backend); modal shows per-theme score detail
- Fixed test for BANPU multi-theme; full suite 199 OK
- Verified: 49/49 rows have theme chip; AOT modal shows ท่องเที่ยว 0.57σ + full calc
This commit is contained in:
Kunthawat Greethong
2026-08-26 14:05:10 +07:00
parent 6c26bf99dc
commit 55b3574040
3 changed files with 88 additions and 25 deletions

View File

@@ -29,18 +29,32 @@ from typing import Optional
# the SET50 universe; symbols not listed here get no direct theme exposure for
# that theme (theme_score contribution = those themes' factor value applied via
# a generic market exposure fallback, see scoring).
#
# Expanded to cover the FULL SET50 universe (2026, 49 names): every symbol is
# assigned to at least one industry/theme so the board + per-symbol detail can
# always report a theme. Deterministic, curated by industry sector.
THEME_SYMBOLS: dict[str, set[str]] = {
"tourism": {
"AOT", "CENTEL", "MINT", "ERW", "DHOUSE", "AWC", "SNNP", "CPN", "CRC",
"MAJOR", "BEM", "BTS",
"AOT", "CENTEL", "MINT", "AWC", "CPN", "CRC", "BEM", "BTS", "ERW",
"DHOUSE", "MAJOR", "SNNP",
},
"auto_credit": {
"KKP", "TISCO", "TCAP", "THANI", "MTC", "SAWAD", "NSI", "GLAND",
"THG", "ASI", "TGPRO", "AEONTS", "TK",
"KKP", "TISCO", "TCAP", "THANI", "MTC", "SAWAD", "NSI", "AEONTS", "TK",
"THG", "GLAND", "ASI", "TGPRO",
},
"refining_energy": {
"PTT", "PTTGC", "TOP", "IRPC", "SPRC", "BCP", "ESSO", "BANPU", "GPSC",
},
"banks": {"BBL", "KBANK", "KTB", "SCB", "TTB"},
"retail": {"CPALL", "COM7", "GLOBAL", "HMPRO", "OSP", "OR", "CPN", "CRC"},
"telecom_it": {"ADVANC", "TRUE", "DELTA", "COM7"},
"property": {"LH", "AWC", "CPN", "CRC", "GLOBAL", "HMPRO"},
"healthcare": {"BDMS", "BH"},
"petrochem_materials": {"IVL", "SCC", "SCGP", "PTTGC", "BANPU"},
"consumer_staples": {"CPF", "TU", "OSP", "CBG"},
"utilities": {"BGRIM", "EGCO", "RATCH", "GPSC", "GULF", "BANPU", "EA"},
"nonbank_finance": {"JMT", "JMART", "KTC", "TIDLOR", "MTC", "SAWAD", "KTC", "AEONTS"},
"exploration": {"PTTEP", "PTT"},
}
# Theme frequency (data cadence of the underlying factor). Used to keep
@@ -49,6 +63,33 @@ THEME_FREQUENCY: dict[str, str] = {
"tourism": "monthly",
"auto_credit": "monthly",
"refining_energy": "quarterly",
"banks": "quarterly",
"retail": "monthly",
"telecom_it": "quarterly",
"property": "quarterly",
"healthcare": "quarterly",
"petrochem_materials": "quarterly",
"consumer_staples": "quarterly",
"utilities": "quarterly",
"nonbank_finance": "quarterly",
"exploration": "quarterly",
}
# Thai labels for every theme (used in the board theme column + per-symbol view)
THEME_LABELS_TH: dict[str, str] = {
"tourism": "ท่องเที่ยว",
"auto_credit": "รถยนต์/สินเชื่อ",
"refining_energy": "พลังงาน/โรงกลั่น",
"banks": "ธนาคาร",
"retail": "ค้าปลีก",
"telecom_it": "สื่อสาร/ไอที",
"property": "อสังหาริมทรัพย์",
"healthcare": "โรงพยาบาล",
"petrochem_materials": "ปิโตรเคมี/วัสดุ",
"consumer_staples": "อาหาร/อุปโภค",
"utilities": "สาธารณูปโภค",
"nonbank_finance": "การเงินนอกธนาคาร",
"exploration": "สำรวจ/ผลิตพลังงาน",
}
@@ -192,7 +233,17 @@ def symbol_breakdown(
s = theme_surprises.get(tid)
if s is not None:
theme_values.append(float(s))
theme_lines.append({"theme": tid, "surprise": round(float(s), 3)})
theme_lines.append({
"theme": tid,
"label_th": THEME_LABELS_TH.get(tid, tid),
"surprise": round(float(s), 3),
})
else:
theme_lines.append({
"theme": tid,
"label_th": THEME_LABELS_TH.get(tid, tid),
"surprise": None,
})
theme_score = (sum(theme_values) / len(theme_values)) if theme_values else 0.0
# find the factor row for this symbol

View File

@@ -101,5 +101,7 @@ class SymbolBreakdownTest(unittest.TestCase):
from app import themes
d = themes.symbol_breakdown("BANPU", factor_view=factor_view, theme_surprises={})
self.assertEqual(d["theme_score"], 0.0)
self.assertEqual(d["themes"], ["refining_energy"]) # BANPU in energy map
self.assertEqual(d["theme_contributions"], []) # but no surprise set -> 0
# BANPU is in energy/petrochem/utilities maps (full SET50 coverage)
self.assertTrue(set(d["themes"]) >= {"refining_energy", "petrochem_materials", "utilities"})
# no surprises set -> every contribution has surprise=None and theme_score 0
self.assertTrue(all(c["surprise"] is None for c in d["theme_contributions"]))