10 KiB
Multi-Theme Dashboard + Capital Simulation Engine Implementation Plan
For Hermes: Execute task-by-task. Use
todoprogress tracking and per-task commits. TDD for logic, visual verification for UI/CSS.
Goal: Turn the current single-theme (tourism) demo into a multi-theme dashboard (combined overview page + per-theme detail pages) plus a separate Simulation tab that runs capital allocation (50/20/30) ranked by signal score, using the existing Siamchart fundamental snapshot and the existing Yahoo price history (2024-01-03 → 2026-08-24).
Architecture: A theme registry that each alternative factor registers into (tourism, siamchart-fundamental). The dashboard combines all themes' signals into a recommendation; each theme has its own detail page. The simulation engine consumes the combined signal scores + 2.5yr price history + dividend yields to allocate a user-entered capital across three buckets (min 100 shares/share).
Tech Stack: Existing Flask backend (backend/app/), Vite+Vue frontend (frontend/src/). New modules backend/app/themes.py, backend/app/simulation.py. New route /api/v1/simulation. Frontend route-based dashboard (#/, #/theme/:id, #/simulation).
Current-state findings (from exploration)
- Themes today:
tourismsignal (backend/app/tourism.py::compute_tourism_signal→{theme, theme_surprise, observations, signals, as_of}) +siamchartfactor view (backend/app/siamchart_factors.py::build_factor_view→ per-symbol PE/EPS/Yield/P/BV/ROE/is_dividend). /api/v1/factorsalready merges Siamchart fundamentals with tourism signal per symbol (added incommit 8a6991b).- Price history EXISTS:
backend/data/prices/snapshots/prices-yahoo-chart-2024-01-01-2026-08-24-a1cf5b3c2ecf.json—{benchmark_symbol: "SET50", series: {SYM: {bars: [{date, open, high, low, close, adjusted_close, volume}]}}}covering 2024-01-03 → 2026-08-24 for 8 symbols (AOT, AWC, BEM, CPALL, CPN, CRC, MINT, PTT) + index_SET. ~1.3 MB per vintage. - Fundamental snapshot:
backend/data/siamchart/set50_master.json— per-symbol EPS/PE/Yield%/is_dividend (49 symbols). - Paper ledger (
backend/app/paper.py) is a manual record-a-fill tool — NOT the automated allocation we now build.
PART A — Multi-Theme Backend Architecture
Task A1: Create a theme registry
Objective: A single source enumerating all available themes and their factor/signal providers.
Files:
- Create:
backend/app/themes.py
Step 1: Write test → backend/tests/test_themes.py
def test_registry_lists_themes():
from app.themes import list_themes
themes = list_themes()
assert {"id": "tourism"} in themes
assert {"id": "siamchart"} in themes
Run: PYTHONPATH=backend .venv/bin/python -W error -m unittest tests.test_themes -v
Expected: FAIL (module missing).
Step 2: Implement themes.py
THEMES = [
{"id": "tourism", "label": "Tourism Pulse", "kind": "signal",
"source": "bot_tourism", "provider": "compute_tourism_signal"},
{"id": "siamchart", "label": "Siamchart Fundamentals", "kind": "fundamental",
"source": "siamchart", "provider": "siamchart_factors"},
]
def list_themes():
return THEMES
Step 3: rerun test → PASS.
Step 4: Commit: git add backend/app/themes.py backend/tests/test_themes.py && git commit -m "feat(backend): theme registry"
Task A2: Combined signal aggregation endpoint
Objective: /api/v1/dashboard returns an aggregate across all themes: per-symbol combined score (sum across themes that have a signal), side, the themes that fired, plus carousel info per theme.
Files:
- Modify:
backend/app/__init__.py - Create:
backend/tests/test_dashboard.py
Step 1: Test — combined aggregation with a mock theme.
Step 2: Implement an aggregate function in themes.py that consumes each theme's signals (tourism signals + siamchart scores) and combines per symbol.
Step 3: PASS + commit feat(backend): aggregated multi-theme dashboard endpoint
Task A3: Per-theme detail endpoint
Objective: /api/v1/themes/<theme_id> returns that theme's observations/surprise/signals for the detail page.
Files:
- Modify:
backend/app/__init__.py - Create: tests in
test_dashboard.py
Step: route dispatch to the theme's provider; validate theme_id against registry; 404 unknown. PASS + commit.
PART B — Simulation / Capital Allocation Engine
Task B1: Price-history loader
Objective: Load the latest Yahoo price snapshot series for a symbol as date→adjusted_close.
Files:
- Create:
backend/app/simulation.py - Create:
backend/tests/test_simulation.py
Step 1: Test
def test_load_price_series(tmp_path):
snap = {"benchmark_symbol":"SET50","series":{"PTT":{"bars":[
{"date":"2024-01-03","adjusted_close":50.0},
{"date":"2024-01-04","adjusted_close":51.0}]}}}
# write temp snapshot, call loader
series = load_price_series(snap, "PTT")
assert series == {"2024-01-03":50.0,"2024-01-04":51.0}
Step 2: Implement load_price_series(snapshot, symbol) → dict[date, adjusted_close]; reject unknown symbol with SimulationError. PASS + commit feat(backend): price series loader.
Task B2: Capital allocation core
Objective: Given capital, per-symbol signal score + is_dividend + dividend yield, allocate across three buckets:
- Bucket 1 (50%): highest signal score among dividend-paying names
- Bucket 2 (20%): highest signal score among non-dividend names (combine with leftover/overflow from bucket 1 shortfall)
- Bucket 3 (30%): highest dividend yield among names not already in bucket 1 (ignore profit)
Each bucket buys from its ranked list, minimum 100 shares per symbol, at latest price; leftover cash (from price-bucket rounding) flows to next bucket / stays unallocated.
Files: backend/app/simulation.py, backend/tests/test_simulation.py
Step 1: Test test_allocate_buckets_basic — small capital, verify 50/20/30 money split, min-100-share floor, ranking by score.
Step 2: Test edge: insufficient capital for min 100 shares in a bucket → skip to next; no infinite loop.
Step 3: Implement allocate_capital(capital, factors, prices) returning per-bucket orders + unallocated cash.
Step 4: PASS + commit feat(backend): capital allocation engine (50/20/30)
Task B3: Simulation endpoint
Objective: POST /api/v1/simulation takes {capital} and returns the allocation plan (per bucket, per-symbol qty/price/notional, total invested, unallocated).
Files: backend/app/__init__.py, tests.
Step: wire allocate_capital to the endpoint; validate capital>0 finite; 400 on bad input. PASS + commit feat(backend): simulation endpoint.
PART C — Frontend Multi-Theme UI
Task C1: Route-based shell (Dashboard + theme detail + simulation)
Objective: App.vue becomes route-aware (hash routing): #/ dashboard, #/theme/:id, #/simulation. Sidebar nav splits into these.
Files: frontend/src/App.vue, frontend/src/style.css
- Keep
Stock board/Signal boardsemantics but restructure nav:- Dashboard (
#/) — combined overview + stock recommendation - Simulation (
#/simulation) — separate tab - per-theme (
#/theme/:id) — theme detail
- Dashboard (
- TDD skipped (visual); verify via dev server + browser.
Verify: npm run build passes; open dev server; nav switches views.
Task C2: Combined Dashboard page
Objective: Overview aggregating all themes: theme surprise cards, combined stock recommendation table (with dividend filter + sortable columns — reuse existing logic), theme list with links.
Files: frontend/src/App.vue
- Reuse
sortedFactorRows/dividendOnly/setSortfrom current stock board. - Add theme-surprise strip + "combined" context.
Task C3: Theme detail page
Objective: #/theme/:id shows that theme's observations (surprise bars), source lineage (provenance), and its own signal table.
Files: frontend/src/App.vue
- Fetch
/api/v1/themes/:id.
Task C4: Simulation tab UI
Objective: #/simulation — input capital, POST /api/v1/simulation, render 3 buckets (each with the buy list, qty, notional; min-100 clarity), unallocated cash, and a note on the data limits (revised history, not PIT).
Files: frontend/src/App.vue, style.css
- Verify via browser after wiring.
Migration pitfalls this plan must respect (from plan skill)
- Multi-theme wiring: do not hardcode "tourism" prose; pull labels from
/api/v1/themes. - Simulation honesty: clearly label output as a paper/backtest on revised vendor history (
point_in_time=false) — never a validated PIT claim. - Bucket edge cases: min-100-share floor can leave a bucket with zero capital allocated → must flow to next bucket gracefully (tested).
- XSS: all UI via Vue
{{ }}, nov-html. - Per-theme 404: unknown theme_id must 404, not silently render empty.
Verification (final)
PYTHONPATH=backend .venv/bin/python -W error -m unittest discover -s backend/tests→ all OK (existing 140 + new).cd frontend && npm run build→ success.- Browser: dashboard lists themes;
#/simulationwith capital input returns an allocation table; dividend filter + sort still work. git diff --check, static scan (no secrets/eval/exec/pickle).- Independent code review (fail-closed JSON) before commit + push.
Open questions / assumptions (call out if wrong)
- "Combined signal score": sum of normalized scores across themes that provide a signal for a symbol. If only one theme fires, its score is the combined score. (Assumed; the user said "signal score from factor combined".)
- Dividend filter for bucket 1 vs bucket 3: bucket 1 = dividend-paying AND high score; bucket 3 = high dividend yield regardless of score, excluding bucket-1 names. User: "จ่ายเงินปันผลมากที่สุด (ไม่ซ้ำกับข้อ 1 และไม่สนเรื่องโอกาสทำกำไร)". Implemented as exclusion, not a forced different-name set.
- Prices: only 8 symbols have Yahoo history (AOT, AWC, BEM, CPALL, CPN, CRC, MINT, PTT) — the simulation can only allocate among symbols with both a price series AND a factor score. Symbols without a price series are skipped with a warning.
- Min 100 shares — quantity may exceed cash in bucket for high-priced names; we buy floor(available_cash / price / 100) * 100 shares, leftover flows onward.