[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:
@@ -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
|
||||
|
||||
@@ -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"]))
|
||||
|
||||
@@ -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)
|
||||
<div v-else-if="symbolDetail?.error" class="state-card error-state">{{ symbolDetail.error }}</div>
|
||||
<div v-else-if="symbolDetail" class="modal-body">
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">ธีมที่เกี่ยวข้อง</div>
|
||||
<div class="modal-section-title">ธีมที่เกี่ยวข้อง (คะแนนต่อธีม)</div>
|
||||
<div v-if="symbolDetail.themes?.length" class="modal-themes">
|
||||
<span v-for="t in symbolDetail.themes" :key="t" class="theme-tag large">{{ themeLabelById[t] || t }}</span>
|
||||
<span class="modal-sub">surprise ต่อธีม</span>
|
||||
<div v-for="c in symbolDetail.theme_contributions" :key="c.theme" class="contrib-line">
|
||||
<span>{{ themeLabelById[c.theme] || c.theme }}</span>
|
||||
<strong>{{ formatNumber(c.surprise) }}σ</strong>
|
||||
<span class="contrib-name">{{ c.label_th || themeLabelById[c.theme] || c.theme }}</span>
|
||||
<strong v-if="c.surprise != null">{{ formatNumber(c.surprise) }}σ</strong>
|
||||
<strong v-else class="muted-cell">ยังไม่มีข้อมูล</strong>
|
||||
</div>
|
||||
<div class="modal-sub">คะแนนธีม = ค่าเฉลี่ยของค่าเหล่านี้ ที่หุ้นนี้อยู่ใน (เฉพาะธีมที่มีข้อมูล)</div>
|
||||
</div>
|
||||
<div v-else class="muted-cell">ไม่มีธีมที่ชัดเจน (หุ้นนอกแผนที่ธีม)</div>
|
||||
<div v-else class="muted-cell">หุ้นนี้ยังไม่ได้จัดอยู่ในธีมใด (จะอัปเดตเมื่อเพิ่มธีม)</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-section">
|
||||
|
||||
Reference in New Issue
Block a user