Kunthawat Greethong 1b971ac72d [verified] Fix backtest accounting identity + honest UI disclosure
Correct the multi-rebalance backtest accounting so ending wealth is
capital + price_pnl + dividend_income with no double counting:
- price_pnl now measures equity change excluding dividends (was reusing
  ending holdings value as 'price profit')
- dividend proxy is included in final_value and net_return, exposed as
  dividend_method=final_holdings_yield_proxy
- regression tests: flat price => zero price_pnl; flat + dividend =>
  dividend-only return; rising no-dividend => correct bucket P&L;
  multi-rebalance accounting identity
- UI (result card + saved-run history) labels dividends as ประมาณการปันผล
  (Proxy) and shows descriptive non-PIT badge when leakage_guard=false

Backend 239 tests passed; targeted backtest 11 passed; frontend build,
npm audit (0), static scan and diff check passed; fresh independent
review deleg_10918fed passed with empty blocker arrays.
Backtest remains descriptive non-PIT (leakage_guard=false) with the
default current-score scorer.
2026-08-27 09:00:39 +07:00

SET50 Alternative Data Platform

Tourism-first vertical slice for a deterministic SET50 alternative-data research system. The app can run against a clearly-labelled fixture or fetch a point-in-time Tourism Indicators vintage from the Bank of Thailand report backed by the Ministry of Tourism and Sports.

Current scope:

fixture or BOT source observation
  → Tourism Pulse surprise
  → versioned exposure score
  → ranked target weights
  → English dashboard
  → internal paper ledger

No external webhook receiver and no live MT5 execution are enabled.

Run the backend

python -m venv .venv
.venv/bin/pip install -r backend/requirements.txt
PAPER_WRITE_TOKEN=local-paper-token TOURISM_SOURCE=fixture PYTHONPATH=backend .venv/bin/python backend/run.py

Health check:

curl http://127.0.0.1:5000/api/v1/health

Run the dashboard

In a second terminal:

cd frontend
npm install
npm run dev -- --host 127.0.0.1

Open http://127.0.0.1:5173.

The frontend reads the live API through Vite's /api proxy. Paper writes require the operator to unlock an HttpOnly browser session using the backend PAPER_WRITE_TOKEN; the token is never embedded in the frontend bundle. The paper-entry action records an assumed fill in the local paper ledger only; it never sends an order.

For HTTPS/non-local deployment, set PAPER_COOKIE_SECURE=1. Paper entries persist atomically under backend/data/paper/ledger.json by default. Sessions remain in-memory and single-process; use a shared session store before running multiple workers or replicas.

Run with the real BOT Tourism source

Use the BOT-backed adapter when network access is available:

PAPER_WRITE_TOKEN=local-paper-token TOURISM_SOURCE=bot PYTHONPATH=backend .venv/bin/python backend/run.py

At startup the adapter performs a read-only GET/POST against the BOT Tourism Indicators report, parses the available monthly history, computes the latest year-over-year arrival observation against a trailing 12-point baseline, and stores the raw HTML plus normalized snapshot under backend/data/ (ignored by git). The dashboard labels provisional BOT data as provisional, not high.

Data-health and replay endpoints:

GET /api/v1/data-health
GET /api/v1/vintages?as_of=<ISO-8601 timestamp>
GET /api/v1/replay/tourism?vintage_id=<vintage_id>
GET /api/v1/backtest/tourism?min_events=12
GET /api/v1/prices/health
POST /api/v1/research/tourism/run
GET /api/v1/research/tourism/latest

Source: https://app.bot.or.th/BTWS_STAT/statistics/ReportPage.aspx?reportID=875&language=eng

Collect a vintage manually

The collector is intentionally one-shot and idempotent. Run it after a source update; it preserves raw bytes, normalized snapshots, and a manifest under backend/data/:

PYTHONPATH=backend .venv/bin/python backend/scripts/collect_tourism_vintage.py --root backend/data

Repeated collection of the same source hash keeps one vintage_id and increments seen_count without changing first_seen_at. A new hash for the same publication timestamp is recorded as a separate revised vintage.

Tests and build

PYTHONPATH=backend .venv/bin/python -m unittest discover -s backend/tests -v
cd frontend && npm run build

Current M2 boundary

  • English UI and analysis vocabulary
  • Research mode and paper mode only
  • Tourism Pulse fixture adapter and BOT Tourism Indicators adapter
  • Data lineage: source, publication time, retrieval time, vintage
  • Raw response hash and normalized snapshot persistence
  • Immutable vintage manifest with first-seen/revision metadata
  • Read-only data-health, vintage timeline and vintage replay endpoints
  • Yahoo-backed daily price snapshot contract with SET symbol mapping
  • Deterministic event-study engine with benchmark and cost inputs
  • Next-trading-session execution anchor by default; revisions are deduplicated by source and publication timestamp
  • Backtest readiness gate that blocks without independent vintages and prices
  • Frozen, replayable Tourism research-run reports with immutable input manifests
  • Durable local paper ledger across backend restarts
  • Deterministic surprise × exposure × confidence score
  • Paper ledger endpoint
  • No LLM call yet; the deterministic result is the source of truth
  • No webhook receiver yet
  • No MT5 bridge yet

The next implementation step is replacing or supplementing revised vendor history with a point-in-time daily price source, while continuing to collect independent BOT releases.

The event-study gate is now exposed through /api/v1/backtest/tourism. It returns HTTP 409 with status=blocked when the independent-vintage minimum is not met, and it explicitly reports that a point-in-time daily price series is still required. The pure engine accepts events, daily prices, benchmark prices, event windows, and cost assumptions; it does not fetch or invent market prices.

Run a frozen research check

The research runner persists either a deterministic event-study result or a blocked report. A blocked report is still useful: it records the exact vintage IDs, hashes, price snapshot, configuration, and gate reason that prevented the study.

From the CLI:

PYTHONPATH=backend .venv/bin/python backend/scripts/run_tourism_research.py \
  --data-root backend/data \
  --min-events 12 \
  --windows 1 3 5 20 \
  --cost-bps 20 \
  --execution-lag-sessions 1

From the API:

POST /api/v1/research/tourism/run
GET  /api/v1/research/tourism/latest

The default live state intentionally returns status=blocked: the current archive has one independent BOT release and the Yahoo price snapshot is marked point_in_time=false. The runner never converts that data into a backtest by inference.

Collect daily price snapshots

The initial research price provider uses Yahoo Finance Chart API with SET ticker mappings. It stores OHLCV plus adjusted close for the eight exposure names and ^SET.BK as SET50. This is revised vendor history, not point-in-time market data, so the snapshot is visible for data plumbing but cannot unlock the backtest gate:

PYTHONPATH=backend .venv/bin/python backend/scripts/collect_prices.py \
  --root backend/data/prices \
  --start 2024-01-01 \
  --end 2026-08-24

Inspect the latest price snapshot:

GET /api/v1/prices/health
Description
No description provided
Readme 2.6 MiB
Languages
Python 90.1%
Vue 6.4%
CSS 3.1%
Dockerfile 0.2%
Shell 0.1%