Files
set50-system/.hermes/plans/2026-08-25_full-app-consistency-refactor.md

5.2 KiB
Raw Blame History

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:

  1. Theme registry + labels live in 3 places that diverge (themes.py correct, dashboard.py maco-proxy, __init__.py old /api/v1/themes still 3 themes, App.vue hardcoded THEME_BY_SYMBOL/themeLabelById).
  2. Selection is not real: _build_board assigns the SAME surprise to 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) -> float that 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 by quality_within_theme so symbols in the same theme differentiate.
  • Expose quality + theme_score breakdown in /api/v1/symbols/<x> so it's transparent.

Task B1 — single source of truth:

  • themes.py add theme_of(symbol), label_of(theme), quality_within_theme.
  • dashboard.build() returns symbol_theme_map + board rows carry per-symbol themes + theme_score.

Task B3 — remove old /api/v1/themes (3 themes, dup logic):

  • Delegate to RealDashboard.build() so /api/v1/themes == /api/v1/dashboard theme 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 quality within 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/dashboard theme 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__.py
  • frontend/src/App.vue
  • backend/tests/test_themes.py, test_dashboard.py, test_api.py

Acceptance (definition of done)

  1. Two banks (BBL strong, TTB weak) get DIFFERENT theme score within banks theme — shows real stock selection, not flat.
  2. Editing themes.py propagates to /api/v1/themes, /api/v1/dashboard, /api/v1/symbols, frontend — no frontend change needed.
  3. /api/v1/themes == /api/v1/dashboard theme set (consistency test).
  4. Modal explains: theme surprise × firm quality = theme score per symbol.
  5. 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)

  1. Changing a theme in themes.py propagates to /api/v1/themes, /api/v1/dashboard, /api/v1/symbols/<x>, and the frontend column/modal — with NO change to frontend code.
  2. No duplicate theme mapping in frontend.
  3. 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.