5.2 KiB
Full-App Consistency Refactor — single source of truth + per-symbol selection
For Hermes: Execute after user approval. Fixes two debts at once: (a) theme mapping/data duplicated & diverged across backend/frontend; (b) theme selection is FLAT — every symbol in a theme gets the same surprise score, so there's no real "เลือกหุ้น" (which name benefits most within theme).
Root problems:
- Theme registry + labels live in 3 places that diverge (
themes.pycorrect,dashboard.pymaco-proxy,__init__.pyold/api/v1/themesstill 3 themes,App.vuehardcodedTHEME_BY_SYMBOL/themeLabelById). - Selection is not real:
_build_boardassigns the SAMEsurpriseto every symbol in a theme (flat). A strong BBL vs weak TTB get identical theme score. The user wants each theme to analyze and pick — i.e. differentiate per-symbol quality/benefit within the theme.
What "theme analyzes & selects stocks" means (per-theme pipeline)
For EVERY theme, a symbol in that theme gets: theme_score_for_symbol = theme_surprise (macro/theme tailwind, common to all in theme) × firm_quality (per-symbol relative strength within the theme)
firm_quality is deterministic, derived from the symbol's own fundamentals
relative to its THEME cohort (not the whole market), e.g.:
- banks: ROE, NIM proxy (ROE/EPS), loan book health (EPS growth)
- energy: PTT/TOP margins via ROE + yield
- retail: margin/EPS growth
- generic: ROE vs theme median, EPS growth vs theme median, dividend floor This way within a hot theme the better-run names rank higher → real stock picking.
We generalize the existing 60/40 so theme_score is no longer flat: combined = 0.6 * (theme_surprise × quality) + 0.4 * siamchart_score
Refactor plan
Backend
Task B0 — per-symbol theme quality (selection):
- Add
themes.quality_within_theme(symbol, theme_id, factor_view) -> floatthat computes a symbol's relative quality within its theme cohort (ROE/EPS-growth vs theme median, dividend floor). deterministic, unit-testable. - In
dashboard._build_board, when applying a theme surprise to a symbol, multiply byquality_within_themeso symbols in the same theme differentiate. - Expose
quality+theme_score breakdownin/api/v1/symbols/<x>so it's transparent.
Task B1 — single source of truth:
themes.pyaddtheme_of(symbol),label_of(theme),quality_within_theme.dashboard.build()returnssymbol_theme_map+ board rows carry per-symbolthemes+theme_score.
Task B3 — remove old /api/v1/themes (3 themes, dup logic):
- Delegate to
RealDashboard.build()so/api/v1/themes==/api/v1/dashboardtheme set (13, same labels/surprises). Remove dup auto/energy sign code.
Task B4 — single fetch path:
- RealDashboard.build() is the one place that fetches auto/energy/macro; endpoints + scheduler consume it (no dupe fetch).
Frontend
Task F1 — delete hardcoded THEME_BY_SYMBOL/themeLabelById:
- theme column + modal come from
/api/v1/dashboard(board[].themes, themes[].label_th) +/api/v1/symbols/<x>breakdown. No local map.
Task F2 — modal shows per-symbol selection logic:
- Add to symbol view: the
qualitywithin each theme + how theme surprise × quality = theme score, so stock-picking is explainable.
Tests
test_themes: quality_within_theme differentiates (strong vs weak name), unit.test_dashboard/test_api:/api/v1/themes==/api/v1/dashboardtheme set; board rows have per-symbol themes; two symbols in same theme get different theme_score when quality differs.
Files
backend/app/themes.py,backend/app/dashboard.py,backend/app/__init__.pyfrontend/src/App.vuebackend/tests/test_themes.py,test_dashboard.py,test_api.py
Acceptance (definition of done)
- Two banks (BBL strong, TTB weak) get DIFFERENT theme score within banks theme — shows real stock selection, not flat.
- Editing themes.py propagates to
/api/v1/themes,/api/v1/dashboard,/api/v1/symbols, frontend — no frontend change needed. /api/v1/themes==/api/v1/dashboardtheme set (consistency test).- Modal explains: theme surprise × firm quality = theme score per symbol.
- npm run build + full backend suite pass; browser shows BBL>TTB theme score when quality differs.
Files
backend/app/themes.py(add theme_of/label_of helpers)backend/app/dashboard.py(symbol_theme_map in build; board row.themes)backend/app/__init__.py(themes endpoint → delegate; de-dup; board.themes)frontend/src/App.vue(remove hardcode maps; derive from API)backend/tests/test_dashboard.py,backend/tests/test_api.py(consistency tests)
Acceptance (definition of done)
- Changing a theme in
themes.pypropagates to/api/v1/themes,/api/v1/dashboard,/api/v1/symbols/<x>, and the frontend column/modal — with NO change to frontend code. - No duplicate theme mapping in frontend.
npm run build+ full backend suite pass; browser shows BBL=ธนาคาร from API.
Open question / tradeoff
- One cache/fetch per data refresh vs per-day: keep single
RealDashboard.build()as the one fetch path (scheduler + endpoints both call it) so data is consistent within a cycle. Accept minor latency first call.