diff --git a/backend/app/themes.py b/backend/app/themes.py index fbe35fd..695fe00 100644 --- a/backend/app/themes.py +++ b/backend/app/themes.py @@ -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 diff --git a/backend/tests/test_themes.py b/backend/tests/test_themes.py index 7551aff..cf20551 100644 --- a/backend/tests/test_themes.py +++ b/backend/tests/test_themes.py @@ -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"])) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index cfd4de9..48d24ed 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -58,21 +58,31 @@ const signalSummary = computed(() => { return { long: s?.long ?? 0, short: s?.short ?? 0, neutral: s?.neutral ?? 0, total: s?.total ?? 0 } }) const freqLabel = (f) => ({ monthly: 'รายเดือน', quarterly: 'รายไตรมาส', annual: 'รายปี', daily: 'รายวัน' })[f] || f -// theme id -> Thai label (for the theme column) +// theme id -> Thai label (for the theme column). Mirrors backend THEME_LABELS_TH. const themeLabelById = { - tourism: 'ท่องเที่ยว', auto_credit: 'รถยนต์/สินเชื่อ', refining_energy: 'พลังงาน', + tourism: 'ท่องเที่ยว', auto_credit: 'รถยนต์/สินเชื่อ', refining_energy: 'พลังงาน/โรงกลั่น', + banks: 'ธนาคาร', retail: 'ค้าปลีก', telecom_it: 'สื่อสาร/ไอที', property: 'อสังหาริมทรัพย์', + healthcare: 'โรงพยาบาล', petrochem_materials: 'ปิโตรเคมี/วัสดุ', consumer_staples: 'อาหาร/อุปโภค', + utilities: 'สาธารณูปโภค', nonbank_finance: 'การเงินนอกธนาคาร', exploration: 'สำรวจ/ผลิตพลังงาน', +} +// which themes a symbol belongs to (mirrors backend THEME_SYMBOLS for full SET50) +const THEME_BY_SYMBOL = { + 'AOT':'tourism','CENTEL':'tourism','MINT':'tourism','AWC':'tourism','CPN':'tourism','CRC':'tourism','BEM':'tourism','BTS':'tourism', + 'MTC':'auto_credit','SAWAD':'auto_credit','TISCO':'auto_credit', + 'BANPU':'utilities','GPSC':'utilities','PTT':'utilities','PTTGC':'refining_energy','TOP':'refining_energy','IVL':'petrochem_materials', + 'BBL':'banks','KBANK':'banks','KTB':'banks','SCB':'banks','TTB':'banks', + 'COM7':'retail','CPALL':'retail','GLOBAL':'retail','HMPRO':'retail','OR':'retail','OSP':'retail', + 'ADVANC':'telecom_it','TRUE':'telecom_it','DELTA':'telecom_it', + 'LH':'property','BDMS':'healthcare','BH':'healthcare','SCC':'petrochem_materials','SCGP':'petrochem_materials', + 'CPF':'consumer_staples','TU':'consumer_staples','CBG':'consumer_staples', + 'BGRIM':'utilities','EGCO':'utilities','RATCH':'utilities','GULF':'utilities','EA':'utilities', + 'JMT':'nonbank_finance','JMART':'nonbank_finance','KTC':'nonbank_finance','TIDLOR':'nonbank_finance', + 'PTTEP':'exploration', } -// which themes a symbol belongs to (id + th label) function symbolThemes(symbol) { - const out = [] - for (const t of dashboardThemes.value) { - if ((t.id === 'tourism' && ['AOT','CENTEL','MINT','AWC','ERW','CPN','CRC','BEM','BTS','SNNP','MAJOR','DHOUSE'].includes(symbol)) || - (t.id === 'auto_credit' && ['KKP','TISCO','TCAP','THANI','MTC','SAWAD','NSI','GLAND','THG','ASI','TGPRO','AEONTS','TK'].includes(symbol)) || - (t.id === 'refining_energy' && ['PTT','PTTGC','TOP','IRPC','SPRC','BCP','ESSO','BANPU','GPSC'].includes(symbol))) { - out.push(themeLabelById[t.id]) - } - } - return out + const id = THEME_BY_SYMBOL[symbol] + if (!id) return [] + return [themeLabelById[id] || id] } const factorAvailable = computed(() => factorData.value?.available ?? false) const dividendCount = computed(() => factorData.value?.dividend_count ?? 0) @@ -655,16 +665,16 @@ onMounted(loadDashboard)
{{ symbolDetail.error }}