Append the PIT-work session to HANDOFF and engineering-log: factor vintages store, partial PIT score provider, honest leakage gating, / api/v1/backtest use_pit wiring, 255-test verification, and the honest scope (no pre-2026-08-27 factor history; siamchart fundamental partial).
21 KiB
Handoff — Tourism Vertical Slice
Project
- Path:
/Users/kunthawat/Gitea/set50-alternative-data-platform - Branch:
main - Verified code commit: pending; final M2.9 independent remediation review passed; no commit or push performed
- Current milestone: M2.9 forward price observations and explicit research modes complete/blocked; observation-integrity follow-up implemented; exploratory research is usable, validated backtest remains blocked
- Mode: research + paper only
- Frontend: Vue 3 + Vite
- Backend: Flask
0.5.0 - Current runtime source: BOT Tourism Indicators (
TOURISM_SOURCE=bot)
Completed
- Tourism snapshot schema with source, publication, retrieval, vintage and raw hash fields.
- Deterministic Tourism Pulse score: standardized surprise × exposure × confidence.
- BOT ASP.NET form adapter with monthly history parsing and trailing 12-point YoY baseline.
- Atomic raw HTML and normalized snapshot persistence under ignored
backend/data/. - Read-only
/api/v1/data-healthendpoint. - Safe
/api/v1/replay/tourism?vintage_id=...endpoint with identity and path validation. - Immutable
VintageStoremanifest with first-seen, last-seen, seen-count and revision metadata. - One-shot collector:
backend/scripts/collect_tourism_vintage.py. - Point-in-time vintage query:
GET /api/v1/vintages?as_of=<ISO-8601>. - Deterministic event-study engine with window, benchmark and cost calculations.
- Backtest readiness gate:
GET /api/v1/backtest/tourism?min_events=12. - Yahoo-backed daily price snapshot with SET symbol mapping and adjusted-close bars.
- Price health:
GET /api/v1/prices/health. - Forward price observation archive:
GET /api/v1/prices/observations; repeated payloads reuse immutable snapshots while recording retrieval observations, first/last-seen times, prior hashes, revision status and structured diffs. - Frozen research runner:
POST /api/v1/research/tourism/runandGET /api/v1/research/tourism/latest. - Research runner modes: explicit
validated(PIT fail-closed) andexploratory(revised-history descriptive only); the UI calls exploratory mode explicitly and showsresult_scope. - Headless runner:
backend/scripts/run_tourism_research.py. - Durable atomic paper ledger under
backend/data/paper/ledger.jsonwhen configured. - Raw and normalized snapshot integrity binding with explicit
sha256-json-canonical-v1metadata. - Replay now loads through the canonical
VintageStore.load_snapshot()path and fails closed on tampered snapshots. - Research reports carry canonical content hashes; manifest entries carry their own hashes binding report filename, report hash, and metadata.
- Research reports from pre-hash trusted stores can be migrated explicitly with
backend/scripts/migrate_research_report_hashes.py; automatic legacy reads remain disabled. - Research input lineage records normalized snapshot hashes and hash algorithms alongside raw payload hashes.
- PIT daily price contract
pit-daily-v1is implemented: explicit provider release/evidence metadata, per-series IANA timezone, per-bar session date/known-at/OHLC/adjusted-close/volume, and immutable manifest/hash binding are required beforepoint_in_time=trueis accepted. - PIT event-study mode rejects missing or market-locally future-known prices; the research runner enables this check for PIT snapshots.
- Public price-provider feasibility is documented in
docs/engineering-log/2026-08-24-price-provider-feasibility.md; SET Historical Data, SETSMART, SMART Marketplace, ICE SET, LSEG Tick History/S3 Direct, Databento, and Exchange Data International (EDI) have all been checked, but no candidate has yet supplied enough evidence to passpit-daily-v1, so no new adapter was added. EDI is the closest conditional price-feed lead because its public docs expose SET coverage, a confirmed ETA, and correction/re-push behavior, but its public contract still lacks immutable release metadata. - VintageStore rejects non-object snapshots, invalid UTF-8 manifests/snapshots, and raw-payload read failures with
VintageStoreError. - Tourism replay computation rejects non-list/non-object observation/exposure shapes as controlled semantic failures, so malformed exposure snapshots fail closed instead of leaking
AttributeError. - Manifest JSON shape and every
list_runs()/latest()entry are validated before metadata is returned or used for selection. - Ranked target weights and LONG/SHORT/NEUTRAL classification.
- English dashboard with live/provisional source label, sign-aware surprise copy and lineage fields.
- Explicit loopback-only paper demo mode with a visible warning, protected token mode for shared/network use, and internal paper ledger.
- No external webhook or MT5 integration.
Independent M1 review passed with no concrete security or logic blockers. Non-blocking backlog: add schema-drift, duplicate/reordered-row, and malformed-vintage regression fixtures.
Current live vintage
source_id: bot.ec_ei_028_s2
published_at: 2026-07-31T14:30:00+07:00
as_of: 2026-06-30
status: provisional
available_periods: 138
history_points: 12
vintage_id: bot.ec_ei_028_s2-2026-07-31-665981f88b4a
seen_count: 16
raw_payload_hash: 665981f88b4a30c5bd30026cf1e96279c244ad83725558f4136d952b29756a31
theme_surprise: -0.35114754
Verified commands and live checks
PYTHONPATH=backend .venv/bin/python -W error -m unittest discover -s backend/tests -v
Ran 125 tests ... OK
PYTHONPATH=backend .venv/bin/python -m compileall -q backend/app backend/scripts backend/tests
passed
npm run build
Vite build completed successfully.
npm audit --omit=dev --audit-level=high
found 0 vulnerabilities
git diff --check
passed
python3 ~/.hermes/skills/research/grounded-citations/scripts/sources.py verify docs/engineering-log/2026-08-24-price-provider-feasibility.md --strict --evidence
citations OK; 11 cited sources, all with evidence quotes
GET /api/v1/health
HTTP 200; {"mode":"research","status":"ok","version":"0.5.0"}
GET /api/v1/data-health
HTTP 200; source_mode=bot, status=provisional, replayable=true
GET /api/v1/replay/tourism?vintage_id=bot.ec_ei_028_s2-2026-07-31-665981f88b4a
HTTP 200; replay theme surprise matched live summary exactly.
GET /api/v1/vintages?as_of=2026-07-01T00:00:00Z
HTTP 200; count=0
GET /api/v1/vintages?as_of=2026-08-01T00:00:00Z
HTTP 200; count=1; manifest seen_count=16
GET /api/v1/backtest/tourism?min_events=12
HTTP 409; status=blocked, available_events=1, required_events=12, price_series_required=true
GET /api/v1/prices/health
HTTP 200; available=true, quality=revised_vendor_history, point_in_time=false, symbols=9
POST /api/v1/research/tourism/run (validated default)
HTTP 200; report status=blocked, reason=insufficient_vintages, immutable run_id persisted
POST /api/v1/research/tourism/run (mode=exploratory, min_events=1)
HTTP 200; revised-price report runs only as result_scope=non_pit_descriptive_only
GET /api/v1/research/tourism/latest
HTTP 200; same run_id returned on replay, report_hash present
GET /api/v1/prices/observations
HTTP 200 on empty archive; populated archives return bound observation IDs, prior hashes and revision diffs
BOT collector rerun
same published release; seen_count=16; independent releases remain=1
M2.9 remediation status
125 tests and current-data health smoke test pass; observation-integrity follow-up covers same-raw/normalized mismatch, malformed manifest entries, first-capture temporal ordering, and duplicate equal-time observations; focused independent review `deleg_0e2282c8` returned schema-valid `passed=true` with empty security/logic blocker arrays; no commit or push performed
Paper writes support two explicit policies: local PAPER_AUTH_MODE=demo requires a loopback bind and allows paper-only writes without a session token while showing a warning; protected PAPER_AUTH_MODE=token uses a server-side token exchange and HttpOnly paper_session cookie. The token is not embedded in the frontend bundle.
Known limitations
- BOT data is provisional and may be revised. The vintage/hash contract preserves the fetched version, but this is not a final-data guarantee.
- Only foreign-arrival YoY is live in this slice; occupancy and airport passenger metrics are not yet connected.
- Snapshot storage is local filesystem and single-process; shared persistence is required before multi-worker deployment.
- No investment edge, transaction-cost result, or backtest conclusion has been established.
- One independent source release is not enough for a valid event study; current historical rows are not treated as point-in-time vintages.
- The event-study engine is deterministic and tested. Its PIT mode now enforces per-bar known-at semantics in the market timezone. Yahoo price history is connected for plumbing, but it is revised vendor history, not point-in-time data.
- The PIT contract is implemented but provider release-time evidence is still absent. No configuration-only promotion of revised history is allowed;
point_in_timeremains false. - SET Historical Data, SETSMART, and SMART Marketplace document historical/API availability but do not publicly document the release-vintage and known-at semantics required by the PIT contract. ICE SET is a strong commercial candidate; LSEG S3 Direct has the strongest public PIT claim but remains conditional on SET coverage and vintage evidence. Databento lists the SET venue and documents PIT corporate-action history, but its public price-data material does not establish price release IDs, client availability/
known_at, or correction-vintage replay. EDI documents confirmed/unconfirmed republish andRepush1/2/3correction files for SET-covered EOD pricing and publishes a confirmed Thailand ETA, but also states that it has no EOD time-series database; immutable release IDs, actual release timestamps, retention, and retrievable manifests remain unproven. All candidates require a provider evidence packet before implementation. - Validated research runner is operational and replayable, but correctly emits a blocked report until both evidence gates pass. Exploratory runner is operational against revised history only with an explicit descriptive-only scope.
- Snapshot loads fail closed when raw files, normalized payloads or manifest metadata do not match their recorded hashes.
- Research report and manifest-entry loads fail closed when persisted content or bound metadata is tampered; pre-existing research volumes require integrity metadata migration before they can be loaded.
- Independent integrity review gate passed for the staged remediation scope under the local threat model:
deleg_c227c439found a persist-wide manifest-validation gap in the prior staged tree; it is now fixed and covered by a regression test. The originaldeleg_43348a3bresult was semantically positive but schema-invalid because it usedfindings; schema correctiondeleg_a1c721d2returned the exact required four-key verdict withpassed=trueand all arrays empty. Earlier timeouts/stale reviews (deleg_cf1da27d,deleg_feb40653,deleg_87a0f686) and interrupted parallel reviewdeleg_e2d54c30are non-approving and contribute no additional evidence. - The hash boundary protects local artifacts against corruption/partial writes; hostile host-level rewrite of code, manifests and runtime environment is outside this local threat model.
- Revisions are deduplicated by canonical
(source_id, published_at); the latest observed revision is selected once. - Event-study default execution is the next trading session, including weekend/holiday event dates.
- Browser screenshot verification remains blocked by the Chrome remote-debugging permission prompt; served HTML/source, live API, fresh Vite build and replay integrity were verified instead.
- Paper demo startup rejects non-loopback
PAPER_BIND_HOST; token mode remains the required policy for shared/network deployment. - Fresh bounded independent review of the paper-auth scope passed with
security_concerns=[]andlogic_errors=[]; its remaining items are non-blocking test suggestions. Integrity remediation is tracked separately and passed via the schema-corrected exact verdict. - M2.9 remediation now covers malformed provider/config totalization, non-PIT series/raw lineage, PIT numeric overflow handling, UTC/equal-time predecessor ordering, same-raw/normalized mismatch rejection, malformed snapshot manifest rejection, first-capture observation timing, contract-backed observation count/timing/reference checks, descriptive-only exploratory status, and human-readable UI reason labels. Focused independent review
deleg_0e2282c8returned schema-validpassed=truewith empty security and logic blocker arrays; its only suggestion is to retain regression coverage. Process-level locking and focused helper/UI coverage remain deferred. No commit or push has been made.
Exact next action
Keep collecting independent BOT releases until 12 genuinely distinct (source_id, published_at) releases exist. In parallel request one complete market-data provider evidence packet (EDI first, or ICE/LSEG/SET fallback): raw sample, provider release ID/time, explicit known-at semantics, correction/revision replay example, SET symbol coverage, timezone/session rules, and immutable archive/manifest evidence. Databento is currently a reference/corporate-actions lead only and must not be used to claim PIT price bars. Revised Yahoo history remains point_in_time=false. Do not raise the PIT flag or promote a PIT adapter until that packet passes pit-daily-v1. The operator can run exploratory research now, but only validated mode may support a historical backtest claim. Keep local paper work in explicit demo mode and use token mode before any shared/network deployment. Add a trusted deployment signing key before treating the app as a multi-user or hostile-host service.
NEVER include API keys, tokens, passwords, secrets, credentials, or connection strings in the summary — replace any that appear with [REDACTED].
Session 2026-08-27 — Backtest Accounting Remediation
Branch: main · Base HEAD: b362cc3 · Commit/push: not performed
Completed
- Added RED/GREEN flat-price accounting invariants in
backend/tests/test_backtest.py. - Corrected general backtest price P&L to compare ending pre-dividend equity with initial capital.
- Included the final-holdings dividend-yield proxy in
final_valueandnet_return. - Added machine-readable
dividend_method=final_holdings_yield_proxy. - Updated the Vue backtest panel to label estimated dividends and warn when
leakage_guard=false.
Verified
- RED: flat-price regressions failed with incorrect
price_pnl=20,000and49,000. - GREEN: flat/no-dividend, flat/proxy-dividend, rising/no-dividend, and multi-rebalance accounting checks passed; targeted suite 11 passed.
- Full backend suite: 239 passed.
- Python compileall passed.
- Vite production build passed.
- npm audit reported 0 high-severity vulnerabilities.
- Added-line security/dangerous-pattern scan and
git diff --checkpassed. - Independent review cycle 1 failed closed on missing proxy/non-PIT disclosure in saved-run history. A fresh fix agent added
ประมาณการปันผล (Proxy)and per-rowdescriptive non-PITbadges. Fresh final reviewdeleg_10918fedinspected the current post-hardening diff and returnedpassed=truewith empty security, logic, and suggestion arrays.
Known limitations
dividend_incomeis still estimated from final holdings and final dividend yield; it is not a dated cash-flow ledger.- The default public backtest still applies current scores historically, so
leakage_guard=falseand the result remains descriptive non-PIT. - A supplied scorer toggles
leakage_guard=true, but provenance and release-time semantics are not independently certified. - Backtest run history remains in process memory.
Exact next action
Implement dated dividend events and a provenance-validated PIT score provider before using results as strategy-performance evidence. Do not reset or stage unrelated pre-existing working-tree changes.
Session 2026-08-27 — PIT factor store + partial PIT score provider (verified + pushed)
Branch: main · HEAD: 1f630be (pushed 1b971ac..1f630be)
Completed this session (verified + pushed)
- PIT factor store
backend/app/factor_vintages.py: append-only, provenance-complete store withobserved_at/released_at/retrieved_atand a SHA-256 canonical hash chain (tamper/reorder detectable).value_at(as_of)returns only rows whosereleased_at <= as_of— a real testable anti-look-ahead guarantee; no value released byas_offails closed (None /have_pit_value=False). Provenance invariants enforced:released >= observed,retrieved >= released, non-finite / naive-timestamp rejected. - Partial PIT score provider
backend/app/pit_scorer.py:PitScoreProvider.score_board(as_of)rebuilds the per-symbol board from PIT factor values (theme 60% dimension) plus a partial siamchart view (EPS growth from the 5-year series; current ratios flaggedpartial_pit).make_pit_score_fnadapts it to the engine'sScoreFn; date-onlyas_ofnormalized to midnight +07:00. - Honest leakage gating
backend/app/backtest.py:_resolve_scoresnow setsleakage_guard=Trueonly when the supplied score_fn's per-symbol meta assertspit_meta.pit=True. An arbitrary callable with no PIT proof is no longer treated as PIT — closes the "supplied fn ⇒ PIT" hole. - API
backend/app/__init__.py:POST /api/v1/backtestacceptsuse_pit, wiring the PIT provider;_load_siamchart_snapshotloads the SET50 fundamental snapshot. Empty factor store fail-closes (live probe:leakage_guard=false).
Verified
- Backend full suite 255 passed (was 239); new tests 14 (factor store 9 + pit scorer 5) + 2 backtest leakage-gating + 1 corrected.
- compileall,
git diff --check, static secret/dangerous scan all clean. - Live route probe:
use_pitwith empty store → HTTP 200,leakage_guard=false, real 2-rebalance run.
Honest scope / known limitations
- Theme dimension is PIT from this store forward; no factor history exists before 2026-08-27, so any pre-today backtest remains non-PIT by construction.
- Siamchart fundamental is partial: EPS 5-year growth is PIT-grade, but ratios (Yield %, PE, P/BV, ROE, DPS) are current-snapshot only and the snapshot has no dated vintage chain yet. Results are flagged
partial_pit, never fully-PIT. dividend_incomestill uses the final-holdings yield proxy (not a dated dividend ledger); that is the next milestone.
Exact next action
Implement the dated dividend cash-flow ledger (ex-date / record date / per-share / qty held) replacing the final-holdings proxy. After that (or in parallel), add a siamchart vintage chain so the fundamental dimension stops being partial. Do not reset or stage unrelated pre-existing working-tree changes.
Session 2026-08-25 — Multi-theme + Simulation (appended)
Branch: main · HEAD: aae1d13 (pushed 8a6991b..aae1d13)
Completed this session (verified + pushed)
- 3 Thai alternative-factor themes: tourism (BOT), auto_credit (TradingEconomics Thai car sales), refining_energy (Thai Oil TOP quarterly) — all real Thai data, daily cache.
- Theme registry + scoring 60/40 (theme / Siamchart fundamental);
GET /api/v1/themesreturns combined board + per-theme frequency (monthly/quarterly). - Capital-allocation simulation:
POST /api/v1/simulation— 50/20/30 buckets, min 100 shares, bucket3 = highest dividend yield ignoring score (fixaae1d13), cash fallback, paper/backtest non-PIT label. - MT5 bridge interface (
app/mt5_bridge.py): dry-run default; live dispatch needsMT5_SEND_ORDERS=1AND approval (Windows-only MetaTrader5). - Thai frontend: theme panel, stock board (Siamchart fundamentals + signal, dividend filter, sortable), simulation tab (capital input → 3 buckets), removed legacy Paper ledger section.
- Dashboards fully localized to Thai (technical terms kept EN per user).
Verified
- Backend suite 185 tests OK; compileall ok; static secret scan clean;
git diff --checkclean. - Independent reviews:
deleg_1682c760(blocker: bucket3 ranked by score) → fixed →deleg_43b165e0passed=true. - Live:
/api/v1/themes200;/api/v1/simulation200 (1M → b1 AOT/MINT/AWC, b3 CRC).
Blockers / pending
- Price universe = 8 symbols (AOT,AWC,BEM,CPALL,CPN,CRC,MINT,PTT,SET50) from Yahoo snapshot; simulation can only allocate within these. Next action: extend price collector to full SET50.
bot_regionalonly north enabled (NE=955 is 0.0 BE-2570 placeholder); other regions pending.- M2.9 legacy files (prices.py, research.py, event_study.py, vintages.py, etc.) still uncommitted/dirty — unrelated, untouched this session.
Exact next action
Extend price-history collection to full SET50 universe so simulation allocates across all names (currently 8). Follow engineering-handoff at session end.