Add an append-only, hash-chained store of every collected Siamchart
fundamental snapshot so the 40% fundamental dimension can be reconstructed
at a historical date instead of always reading the latest snapshot:
- backend/app/siamchart_vintages.py: SiamchartVintageStore persists each
snapshot under its retrieved_at with a SHA-256 canonical hash chain
(tamper/reorder detectable); snapshot_at(as_of) returns the newest
snapshot whose retrieved_at <= as_of (anti-look-ahead), and fails closed
(returns {}) when none is knowable yet. Deduplicates identical
retrieved_at+body persists.
- backend/app/pit_scorer.py: PitScoreProvider accepts siamchart_store; when
wired, siamchart_factor_view reads the snapshot knowable at as_of
(pit_grade='pit') instead of the current snapshot (pit_grade='current').
score_board no longer forces partial_pit when a store is present — the
fundamental dimension is PIT; the theme dimension still fails closed
(pit=false) unless every theme factor has a released PIT value by as_of.
- backend/app/__init__.py: /api/v1/backtest use_pit seeds the first vintage
from the current snapshot (idempotent) and wires the store.
- tests: store (6) + scorer-with-store anti-look-ahead (1) — full backend
suite 273 passed.
Honest scope: snapshots are stored whole and reconstructible forward;
EPS year-keys inside a snapshot are not tied to calendar years, so EPS
growth stays latest-vs-prior (not fiscal-year-pinned). No history before the
first collected snapshot exists.
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