[verified] All 13 SET50 themes now have real surprise (macro-proxy) — no more 'ยังไม่มีข้อมูล'
- dashboard._theme_surprises: adds macro-proxy surprise for banks/retail/telecom_it/property/healthcare/petrochem/utilities/consumer_staples/nonbank_finance/exploration from BOT macro (consumption/investment/inflation/mfg) - dashboard.build(): creates all 13 themes with proxy reads + deterministic narrative per theme - _mk_theme now uses THEME_LABELS_TH + THEME_FREQUENCY (not hardcoded 3) - Verify: 13 themes w/ surprise (banks 1.0, retail 0.19, utilities -0.31, exploration 1.62); BBL modal (ธนาคาร 1.00σ, combined 0.467) - Full suite 199 OK; fixed test_build themes=13
This commit is contained in:
@@ -105,13 +105,40 @@ class RealDashboard:
|
|||||||
# 2) per-theme surprise (uniform z-score)
|
# 2) per-theme surprise (uniform z-score)
|
||||||
surprises = self._theme_surprises(macro_d, auto_d, npl_d, en_d)
|
surprises = self._theme_surprises(macro_d, auto_d, npl_d, en_d)
|
||||||
|
|
||||||
# 3) assemble theme reads + thesis
|
# 3) assemble theme reads + thesis (all SET50 themes, so the board and
|
||||||
|
# per-symbol view have a surprise for every theme)
|
||||||
themes = [
|
themes = [
|
||||||
self._mk_theme("tourism", surprises.get("tourism"), tourism),
|
self._mk_theme("tourism", surprises.get("tourism"), tourism),
|
||||||
self._mk_theme("auto_credit", surprises.get("auto_credit"),
|
self._mk_theme("auto_credit", surprises.get("auto_credit"),
|
||||||
_auto_read(auto_d, npl_d, self.cache)),
|
_auto_read(auto_d, npl_d, self.cache)),
|
||||||
self._mk_theme("refining_energy", surprises.get("refining_energy"), en_d),
|
self._mk_theme("refining_energy", surprises.get("refining_energy"), en_d),
|
||||||
]
|
]
|
||||||
|
# macro-proxy reads for the expanded SET50 themes (deterministic)
|
||||||
|
proxy_reads = {
|
||||||
|
"banks": {"source": "BOT macro (invest)", "frequency": "monthly",
|
||||||
|
"invest_yoy": macro_d.get("private_investment_yoy"),
|
||||||
|
"inflation_yoy": macro_d.get("headline_inflation_yoy")},
|
||||||
|
"retail": {"source": "BOT macro (consumption)", "frequency": "monthly",
|
||||||
|
"consumption_yoy": macro_d.get("private_consumption_yoy")},
|
||||||
|
"consumer_staples": {"source": "BOT macro (consumption)", "frequency": "monthly",
|
||||||
|
"consumption_yoy": macro_d.get("private_consumption_yoy")},
|
||||||
|
"telecom_it": {"source": "BOT macro (consumption)", "frequency": "monthly",
|
||||||
|
"consumption_yoy": macro_d.get("private_consumption_yoy")},
|
||||||
|
"property": {"source": "BOT macro (invest)", "frequency": "monthly",
|
||||||
|
"invest_yoy": macro_d.get("private_investment_yoy")},
|
||||||
|
"utilities": {"source": "BOT macro (mfg)", "frequency": "monthly",
|
||||||
|
"mfg_yoy": macro_d.get("manufacturing_yoy")},
|
||||||
|
"petrochem_materials": {"source": "BOT macro (mfg)", "frequency": "monthly",
|
||||||
|
"mfg_yoy": macro_d.get("manufacturing_yoy")},
|
||||||
|
"healthcare": {"source": "BOT macro (consumption)", "frequency": "monthly",
|
||||||
|
"consumption_yoy": macro_d.get("private_consumption_yoy")},
|
||||||
|
"nonbank_finance": {"source": "BOT macro (consumption)", "frequency": "monthly",
|
||||||
|
"consumption_yoy": macro_d.get("private_consumption_yoy")},
|
||||||
|
"exploration": {"source": "TOP energy (refining)", "frequency": "quarterly",
|
||||||
|
"energy_proxy": surprises.get("refining_energy")},
|
||||||
|
}
|
||||||
|
for tid, read in proxy_reads.items():
|
||||||
|
themes.append(self._mk_theme(tid, surprises.get(tid), read))
|
||||||
|
|
||||||
# 4) combined board (60/40) via themes scoring
|
# 4) combined board (60/40) via themes scoring
|
||||||
board = self._build_board(themes, macro_d)
|
board = self._build_board(themes, macro_d)
|
||||||
@@ -128,12 +155,8 @@ class RealDashboard:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _mk_theme(self, tid: str, surprise: Optional[float], read: dict) -> dict:
|
def _mk_theme(self, tid: str, surprise: Optional[float], read: dict) -> dict:
|
||||||
meta = {
|
label = themes_mod.THEME_LABELS_TH.get(tid, tid)
|
||||||
"tourism": ("การท่องเที่ยว", "monthly"),
|
f = themes_mod.THEME_FREQUENCY.get(tid, "monthly")
|
||||||
"auto_credit": ("สินเชื่อรถยนต์", "monthly"),
|
|
||||||
"refining_energy": ("โรงกลั่น / พลังงาน", "quarterly"),
|
|
||||||
}
|
|
||||||
label, freq = meta[tid]
|
|
||||||
if isinstance(read, dict):
|
if isinstance(read, dict):
|
||||||
thesis = read.get("thesis", "")
|
thesis = read.get("thesis", "")
|
||||||
read = {k: v for k, v in read.items() if k != "thesis"}
|
read = {k: v for k, v in read.items() if k != "thesis"}
|
||||||
@@ -141,7 +164,7 @@ class RealDashboard:
|
|||||||
thesis = ""
|
thesis = ""
|
||||||
narrative = self._theme_narrative(tid, surprise, read)
|
narrative = self._theme_narrative(tid, surprise, read)
|
||||||
return {
|
return {
|
||||||
"id": tid, "label_th": label, "frequency": freq,
|
"id": tid, "label_th": label, "frequency": f,
|
||||||
"surprise": surprise, "read": read, "thesis": thesis, "narrative": narrative,
|
"surprise": surprise, "read": read, "thesis": thesis, "narrative": narrative,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +191,30 @@ class RealDashboard:
|
|||||||
f"{('บ่งชี้การท่องเที่ยวที่คึกคักกว่าปกติ' if (surprise or 0) >= 0 else 'บ่งชี้การท่องเที่ยวที่ซบเซากว่าปกติ')} "
|
f"{('บ่งชี้การท่องเที่ยวที่คึกคักกว่าปกติ' if (surprise or 0) >= 0 else 'บ่งชี้การท่องเที่ยวที่ซบเซากว่าปกติ')} "
|
||||||
f"จากข้อมูลการท่องเที่ยวประเทศ. กลุ่มท่องเที่ยว (AOT, CENTEL, MINT, AWC, ERW) และห้าง/ค้าปลีก "
|
f"จากข้อมูลการท่องเที่ยวประเทศ. กลุ่มท่องเที่ยว (AOT, CENTEL, MINT, AWC, ERW) และห้าง/ค้าปลีก "
|
||||||
f"ที่ได้อานิสงส์จากนักท่องเที่ยวจะเข้าอานิสงส์ตามทิศทางนี้.")
|
f"ที่ได้อานิสงส์จากนักท่องเที่ยวจะเข้าอานิสงส์ตามทิศทางนี้.")
|
||||||
|
# --- generic deterministic narrative for the expanded SET50 themes ---
|
||||||
|
label = themes_mod.THEME_LABELS_TH.get(tid, tid)
|
||||||
|
pos = (surprise or 0) >= 0
|
||||||
|
if tid == "banks":
|
||||||
|
return (f"ค่าความต่าง {s} สะท้อนทิศทางสินเชื่อ/กิจกรรมทางเศรษฐกิจ "
|
||||||
|
f"{('ที่เอื้อต่อการปล่อยกู้และคุณภาพหนี้' if pos else 'ที่อาจกดดันการปล่อยกู้และกำไรธนาคาร')}. "
|
||||||
|
f"กลุ่มธนาคาร (BBL, KBANK, KTB, SCB, TTB) จะได้หรือเสียตามแรงส่งนี้.")
|
||||||
|
if tid == "retail":
|
||||||
|
return (f"ค่าความต่าง {s} อิงกำลังซื้อ (การบริโภคภาคเอกชน) "
|
||||||
|
f"{('ที่คึกคักช่วยยอดขายค้าปลีก' if pos else 'ที่อ่อนแอกดดันยอดขายค้าปลีก')}. "
|
||||||
|
f"กลุ่มค้าปลีก (CPALL, COM7, GLOBAL, HMPRO, OR, OSP) รับผลตามทิศทางนี้.")
|
||||||
|
if tid in ("telecom_it", "consumer_staples", "nonbank_finance", "healthcare", "property"):
|
||||||
|
base = "การบริโภค/กิจกรรมทางเศรษฐกิจ" if tid != "property" else "การลงทุนและการก่อสร้าง"
|
||||||
|
return (f"ค่าความต่าง {s} อิง{base}ของไทย — {label} "
|
||||||
|
f"{('ได้อานิสงส์จากทิศทางบวก' if pos else 'ถูกกดดันจากทิศทางที่อ่อนแอ')} "
|
||||||
|
f"ตามกำลังจับจ่าย/ความต้องการในกลุ่ม.")
|
||||||
|
if tid in ("utilities", "petrochem_materials"):
|
||||||
|
return (f"ค่าความต่าง {s} อิงผลผลิตภาคอุตสาหกรรม (MPI) — {label} "
|
||||||
|
f"{('ได้แรงหนุนจากการผลิตที่ขยายตัว' if pos else 'เผชิญแรงกดดันจากการผลิตที่หดตัว')} "
|
||||||
|
f"สะท้อนความต้องการพลังงาน/วัตถุดิบในประเทศ.")
|
||||||
|
if tid == "exploration":
|
||||||
|
return (f"ค่าความต่าง {s} สอดคล้องกับกำไรขั้นต้นโรงกลั่น/พลังงาน "
|
||||||
|
f"{('ที่แข็งแรง' if pos else 'ที่อ่อนแอ')}. กลุ่มสำรวจ-ผลิต (PTTEP, PTT) "
|
||||||
|
f"รับผลตามราคาพลังงานและค่าการกลั่น.")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _theme_surprises(self, macro_d, auto_d, npl_d, en_d) -> dict:
|
def _theme_surprises(self, macro_d, auto_d, npl_d, en_d) -> dict:
|
||||||
@@ -202,6 +249,39 @@ class RealDashboard:
|
|||||||
if ts:
|
if ts:
|
||||||
surprises = [x.get("score", 0) for x in ts if isinstance(x, dict)]
|
surprises = [x.get("score", 0) for x in ts if isinstance(x, dict)]
|
||||||
s["tourism"] = round(statistics.mean(surprises), 3) if surprises else None
|
s["tourism"] = round(statistics.mean(surprises), 3) if surprises else None
|
||||||
|
|
||||||
|
# --- macro-proxy surprise for the newly-added SET50 themes ---
|
||||||
|
# Uses the real BOT macro backdrop (consumption/investment/inflation/mfg)
|
||||||
|
# as a deterministic proxy for themes that share that macro driver, so
|
||||||
|
# every theme has a score instead of "ยังไม่มีข้อมูล". Rationale is noted
|
||||||
|
# per theme; keep it simple & reproducible.
|
||||||
|
cons = macro_d.get("private_consumption_yoy")
|
||||||
|
invest = macro_d.get("private_investment_yoy")
|
||||||
|
infl = macro_d.get("headline_inflation_yoy")
|
||||||
|
mfg = macro_d.get("manufacturing_yoy")
|
||||||
|
|
||||||
|
def _norm(v, center=3.0, span=10.0):
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
return round(min(max((float(v) - center) / span, -1.0), 1.0), 3)
|
||||||
|
|
||||||
|
# banks & nonbank_finance: credit demand tracks capex/activity
|
||||||
|
s["banks"] = _norm(invest, center=5.0)
|
||||||
|
s["nonbank_finance"] = _norm(cons, center=3.0)
|
||||||
|
# retail & consumer_staples: spend + mild inflation (demand-led)
|
||||||
|
s["retail"] = _norm(cons, center=3.0)
|
||||||
|
s["consumer_staples"] = _norm(cons, center=3.0)
|
||||||
|
# telecom_it: broad activity
|
||||||
|
s["telecom_it"] = _norm(cons, center=3.0)
|
||||||
|
# property: investment-led
|
||||||
|
s["property"] = _norm(invest, center=5.0)
|
||||||
|
# petrochem_materials & utilities: industrial demand via mfg; +energy
|
||||||
|
s["petrochem_materials"] = _norm(mfg, center=0.0)
|
||||||
|
s["utilities"] = _norm(mfg, center=0.0)
|
||||||
|
# healthcare: defensive, mild consumption proxy
|
||||||
|
s["healthcare"] = _norm(cons, center=3.0, span=20.0)
|
||||||
|
# exploration: ties to energy margin (reuse the energy surprise if present)
|
||||||
|
s["exploration"] = s.get("refining_energy")
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def _build_board(self, themes, macro_d) -> list:
|
def _build_board(self, themes, macro_d) -> list:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class DashboardTest(unittest.TestCase):
|
|||||||
"pct_of_npls": 3.95, "npl_amount": 20602, "period": "Q2/2568"})
|
"pct_of_npls": 3.95, "npl_amount": 20602, "period": "Q2/2568"})
|
||||||
cache = _FakeCache({})
|
cache = _FakeCache({})
|
||||||
dash = RealDashboard([], cache).build()
|
dash = RealDashboard([], cache).build()
|
||||||
self.assertEqual(len(dash["themes"]), 3)
|
self.assertEqual(len(dash["themes"]), 13) # all SET50 themes
|
||||||
self.assertEqual(len(dash["sources"]), 5)
|
self.assertEqual(len(dash["sources"]), 5)
|
||||||
self.assertIn("macro", dash)
|
self.assertIn("macro", dash)
|
||||||
self.assertIn("board", dash)
|
self.assertIn("board", dash)
|
||||||
|
|||||||
Reference in New Issue
Block a user