Files
set50-system/.hermes/plans/2026-08-25_092044-multi-theme-dashboard-and-simulation.md

10 KiB

Multi-Theme Dashboard + Capital Simulation Engine Implementation Plan

For Hermes: Execute task-by-task. Use todo progress 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: tourism signal (backend/app/tourism.py::compute_tourism_signal{theme, theme_surprise, observations, signals, as_of}) + siamchart factor view (backend/app/siamchart_factors.py::build_factor_view → per-symbol PE/EPS/Yield/P/BV/ROE/is_dividend).
  • /api/v1/factors already merges Siamchart fundamentals with tourism signal per symbol (added in commit 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 testbackend/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 board semantics but restructure nav:
    • Dashboard (#/) — combined overview + stock recommendation
    • Simulation (#/simulation) — separate tab
    • per-theme (#/theme/:id) — theme detail
  • 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/setSort from 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 {{ }}, no v-html.
  • Per-theme 404: unknown theme_id must 404, not silently render empty.

Verification (final)

  1. PYTHONPATH=backend .venv/bin/python -W error -m unittest discover -s backend/tests → all OK (existing 140 + new).
  2. cd frontend && npm run build → success.
  3. Browser: dashboard lists themes; #/simulation with capital input returns an allocation table; dividend filter + sort still work.
  4. git diff --check, static scan (no secrets/eval/exec/pickle).
  5. 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.