Commit Graph

32 Commits

Author SHA1 Message Date
Kunthawat Greethong
c87767cfe5 [verified] Task 6: durable backtest run store + strict /api/v1/backtest/run route 2026-08-28 10:29:56 +07:00
Kunthawat Greethong
68f2cc1477 [verified] Task 1: strict PIT backtest readiness + default-date derivation 2026-08-28 10:23:53 +07:00
Kunthawat Greethong
03195dc55d [verified] Auto-refresh dated dividend ledger in the data scheduler
Automatically keep the real dated dividend ledger fresh inside the app's own
refresh loop (this app runs on its own server, independent of Hermes):

- backend/app/scheduler.py: AppDataScheduler gained a cooldown-gated
  _maybe_refresh_dated_dividends() that fetches real dated dividend history
  (siamchart /stock-info) into data/dividends/ledger.json at most once per
  dividend_cooldown_seconds (default 6h) — dividend history changes only a
  few times a year, so we never hammer the source every refresh tick. The
  fetch is non-fatal: a network failure leaves the previous ledger intact.
- backend/app/__init__.py: passes DIVIDEND_REFRESH_COOLDOWN_SECONDS to the
  scheduler (default 21600s).
- tests: cooldown fires once then skips, and refetches after it elapses (2)
  — full backend 294 passed.
2026-08-27 12:51:49 +07:00
Kunthawat Greethong
f9973e8d0a [verified] Real dated dividend history collector (dps_annual_proxy -> dated_ledger)
Close the last deferred PIT milestone by collecting REAL per-stock dated
dividend cash-flow history from Siamchart, upgrading the dividend ledger
from DPS estimates to dated_ledger.

- backend/app/siamchart.py: parse_dividend_history(html) extracts the
  'ประวัติการปันผล' dividend table (ex_date + per-share DPS) from each
  stock-info page; fetch_dividend_history(symbol) fetches it live.
- backend/app/dividend_ledger.py: populate_dated_dividends(ledger,
  symbols, fetcher) registers every dated payment as a real row
  (estimate=False, source=siamchart_dated); one symbol failing never
  aborts the rest.
- backend/app/__init__.py: DividendLedger persisted at
  data/dividends/ledger.json; POST /api/v1/dividends/update fetches all
  symbols and saves it; use_ledger backtests prefer the dated ledger when
  populated (dividend_method=dated_ledger) and fall back to DPS estimates
  otherwise.
- tests: parser (4) + populate (2) — full backend 292 passed.

Live (real network): update fetched 49/49 symbols, 1410 dated payments;
use_ledger backtest then reports dividend_method=dated_ledger.
The 'eval(' static-scan hit is ast.literal_eval (safe literal parse, no
code execution), not eval().
2026-08-27 12:34:36 +07:00
Kunthawat Greethong
ae814c341e [verified] Factor-learning validation gate (no auto-apply)
Add a strict holdout/walk-forward + baseline gate to factor-weight learning,
per the P4 guardrail: learned weights are never auto-applied until minimum
sample, holdout/walk-forward, and baseline comparison all pass.

- backend/app/weight_learning.py:
  - FactorLearning gained ic_train / ic_holdout / validated / gate_notes.
  - apply_validation_gate(...) splits a chronological IC series into train +
    holdout and only marks validated=True when: total >= MIN_SAMPLE_PERIODS,
    each window >= its minimum, train AND holdout IC are positive (beat the
    BASELINE_IC=0) and agree in sign, and the pooled |t| > MIN_IC_TSTAT.
  - apply_weight_update now keeps new_weight == old_weight for any factor
    that is not validated (no auto-apply); only validated factors move.
  - learn_momentum_gated(...) builds PIT momentum ICs then applies the gate.
- backend/app/__init__.py: /api/v1/learning/momentum uses the gated learner
  and surfaces ic_train/ic_holdout/validated/gate_notes.
- tests: gate (16) via rewritten suite — full backend 286 passed.

Live probe on current price archive: validated=false with
gate_note 'IC not above baseline (0.0711/-0.1143)' — momentum is not
validated, weight stays unchanged (new_weight=None).
2026-08-27 12:17:21 +07:00
Kunthawat Greethong
6d9c283d9a [verified] Real forward-test frozen-signal lifecycle + durable run store
Replace the cosmetic 'forward' mode (which was the same single-pass backtest
with a mode string) with a genuine forward paper-portfolio lifecycle:

- backend/app/forward_test.py: ForwardTestStore — durable, thread-safe JSON
  store of forward runs with an explicit status lifecycle:
    frozen (signals snapshotted, immutable) -> executed (fills 50/20/30
    buckets at post-freeze prices) -> marked (mark-to-market equity series ->
    matured (net_return finalised).
  Frozen signals can never be re-read/rewritten after creation, so later data
  cannot retroactively change what the run decided.
- backend/app/__init__.py: GET /api/v1/forward (+<id>), POST /api/v1/forward
  (create+execute, with use_pit to freeze PIT or current-board scores),
  POST /<id>/mark, POST /<id>/mature. ForwardTestStore wired as an extension
  backed by data/forward/runs.json (survives restarts).
- tests: lifecycle store (7) — full backend suite 280 passed. Live probe:
  create->execute (2xx, real holdings), list, mark, mature all work and the
  run persists.

Honest scope: the score source at CREATE time may be the current board
(non_pit=true, tagged); paper-only, no MT5 send. A PIT scorer only marks a run
non_pit=false when its scores assert pit_meta.pit=true.
2026-08-27 12:01:00 +07:00
Kunthawat Greethong
887e9c9208 [verified] PIT siamchart vintage store un-partials the fundamental dimension
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.
2026-08-27 11:55:36 +07:00
Kunthawat Greethong
068dff22d7 [verified] Dated dividend cash-flow ledger replacing final-holdings proxy
Replace the single final-holdings yield proxy with a per-symbol dated
dividend ledger for the backtest engine:

- backend/app/dividend_ledger.py: DividendLedger store (ex_date,
  record_date, pay_date, per_share, source, estimate flag) with validation
  and persistence; credit_dividends credits per_share * qty once a payment is
  due (on/after ex-date and pay date); build_dps_ledger builds estimate rows
  from siamchart ratios.DPS (per-share, price-independent) as a step up from
  the yield-percentage proxy.
- backend/app/backtest.py: run_backtest accepts dividend_ledger; when set,
  dividend_income comes from the ledger and dividend_method reports
  'dated_ledger' (real rows) or 'dps_annual_proxy' (estimate). No ledger ->
  legacy final_holdings_yield_proxy preserved and labelled.
- backend/app/__init__.py: /api/v1/backtest accepts use_ledger, wiring the
  DPS-built ledger.
- tests: ledger store/credit (9) + backtest ledger integration (2 new) —
  full backend suite 266 passed. Live probe: use_ledger flips dividend_method
  to dps_annual_proxy with per-share income (4151.0) vs proxy (5041.96).

Honest scope: DPS rows are estimates (no ex-date history in snapshot yet);
real dated cash flows require collecting per-stock dividend history, which
upgrades a symbol to dated_ledger when present.
2026-08-27 11:46:15 +07:00
Kunthawat Greethong
1f630be2b5 [verified] PIT factor store + partial PIT score provider (PIT enabler)
Add a point-in-time (PIT) factor/data store and a score provider so the
backtest engine can rebuild per-symbol scores from data actually knowable
at a given date, instead of silently reusing the live board:

- backend/app/factor_vintages.py: append-only, provenance-complete store
  (observed_at/released_at/retrieved_at) with a SHA-256 canonical hash chain.
  value_at(as_of) only ever returns rows whose released_at <= as_of (real,
  testable anti-look-ahead); no value by as_of fails closed (returns None).
- backend/app/pit_scorer.py: PitScoreProvider computes theme surprises from
  PIT factor values only, and a partial siamchart fundamental view (EPS
  growth from the 5-year series; current ratios marked partial). score_board
  attaches pit_meta so callers can tell PIT from fallback.
- backend/app/backtest.py: _resolve_scores now sets leakage_guard ONLY when
  the supplied score_fn's meta asserts pit_meta.pit=true; an arbitrary
  callable with no PIT proof is no longer treated as PIT (closes the
  'supplied fn => PIT' hole).
- backend/app/__init__.py: /api/v1/backtest accepts use_pit, wiring the PIT
  provider; _load_siamchart_snapshot loads the SET50 fundamental snapshot.
- tests: factor store (9), pit scorer (5), backtest leakage-guard gating (2
  new + 1 corrected) — full backend suite 255 passed. Empty store fail-closes
  (leakage_guard=false) as proven by a live route probe.

Honest scope: theme dimension is PIT from this store forward; siamchart
fundamental remains partial (current ratios) and is flagged as such. No
historical factor data before today exists, so pre-today backtests remain
non-PIT by construction.
2026-08-27 09:26:12 +07:00
Kunthawat Greethong
b362cc35bf [verified] Add API tests for /api/v1/learning/factors + configurable history dir
Closes reviewer suggestion (deleg_5dd358e3): adds coverage for the factor
readiness endpoint (n_points / learnable / last_value / ordering) and the
min_points 400 validation. FACTOR_HISTORY_DIR is now configurable via app
config so tests (and deploy) can point the history store at a chosen path
instead of a hardcoded data dir. 236 tests pass.
2026-08-27 07:37:01 +07:00
Kunthawat Greethong
d87a1ada39 [verified] Cross-theme surprise normalization + historical factor store (P4 enabler)
A. Cross-theme comparability:
- compute_theme_surprises now weight-normalizes by total |weight| (weighted
  average), so every theme surprise on same [-1,1] scale regardless of factor
  count/weight (retail 0.189->0.145; auto_credit 1.0->0.64).

B. Historical factor store (enables learning macro/demographic factors):
- New factor_history.py: append-only per-factor JSONL, dedupes unchanged
  values, rejects non-finite, records every FACTORS value each scheduler run.
- scheduler.py: jobs carry fetch_module; refresh_all records factor history
  (non-fatal); added bank_npl job.
- GET /api/v1/learning/factors?min_points= reports n_points/learnable per
  factor so users see when P4 learning unlocks (validated query parsing).
- weight_learning: generic learn_factor_series() aggregator (momentum reuses).

Independent review deleg_5dd358e3 passed=true (empty security/logic arrays);
its two robustness suggestions applied (finite guard in record(), clean 400 on
bad min_points). 234 tests pass; Vite build passes.
2026-08-27 07:32:16 +07:00
Kunthawat Greethong
8db3d48ae2 [verified] P0-B registry-driven scoring + P3 PIT backtest + P4 factor-weight learning
P0-B (registry is the single source of truth for scoring):
- FACTORS now carries center/span normalization spec; unused hand-written
  per-theme surprise blocks in dashboard.py replaced by one registry-driven
  compute_theme_surprises() (themes.py).
- THEMES['banks'] adds bank_npl weight so NPL is genuinely blended.
- factor_value/normalize hardened against NaN/inf (finite guards).
- Board re-ranks (TRUE/GULF up, TOP->3) per registry weights; 3 new tests
  incl. 'changing a registry weight changes output'.

P3 (point-in-time backtest):
- run_backtest is now a real multi-rebalance engine (reallocates every window,
  reconciles holdings, marks to market) instead of allocate-once+break.
- Added leakage_guard (False unless a PIT score_fn is supplied), planned vs
  actual rebalances, and momentum_at() true 12-1 (skips last month, PIT).

P4 (factor-weight learning):
- weight_learning.py: cross-sectional Spearman IC, forward-return builder,
  IC aggregation + t-stat, and apply_weight_update (new = clip(old*(1+shrink*IC))).
- GET /api/v1/learning/momentum endpoint. Live result: momentum IC=0.012
  t=0.132 over 22 periods -> momentum has no reliable predictive power here.
  Macro/demographic factors blocked (no historical factor vintages yet).

Two independent review gates passed (deleg_fe6f45cd, deleg_718218f8): empty
security/logic arrays; their non-blocking suggestions applied (finite guards,
dedupe leakage_guard resolution). 226 tests pass; Vite build passes.
2026-08-27 07:12:18 +07:00
Kunthawat Greethong
325e164dd3 [verified] Fix P1-P2-P5 audit findings: simulation reuses board, source_summary clarity, dead-code removal + conftest
- P1: /api/v1/simulation now uses the canonical board score (default_scores)
  instead of a divergent 3-theme recompute -> 'จำลอง' can't disagree with board
  (live check: sim top pick PTT == top board combined 1.600). Removes binary
  auto/en signs, restores quality+momentum+dividend screen consistency.
- P2: dashboard emits source_summary{factor_keys, rows}; frontend shows
  'N ปัจจัย · M แหล่ง' so the 7-vs-5 count confusion is impossible.
- P5: removed dead themes.list_themes()/Theme/build_theme_scores/_map_index and
  the tests that locked them; added tests/conftest.py so pytest needs no PYTHONPATH.
- docs: audit-and-plan-2026-08-26.md (full P0-P5 plan) + engineering-log entry.
- 203 backend tests pass; Vite build passes. Independent reviewer: no security or
  logic blockers (minor error-leak suggestion applied: 503 message no longer leaks
  exception detail).
2026-08-27 03:21:22 +07:00
Kunthawat Greethong
6e78b6acb5 [verified] Apply R1-R5 (factor formula) + real bank-sector NPL collector
(a) R1-R5 (factor-refinement, grounded in methodology-research.md):
- R1 (PEAD): EPS-growth weight raised 1.0->1.5 in build_siamchart_score / symbol_breakdown (Bernard-Thomas 1990, Livnat-Mendenhall 2006)
- R2 (momentum): 12-1 momentum factor from Yahoo price snapshot (Jegadeesh-Titman 93; lite weight 0.5)
- R3 (regime): binary bear gate -> continuous stress = negative-themes fraction, smooth LONG/SHORT shift
- R5 (dividend screen): non-dividend / cut-yield names no longer go LONG (screen-off)
- R4 (earnings-revision) deferred: no free EPS-forecast source yet (documented)

(b) bank-sector NPL collector (BOT reportID 794, financial&insurance sector):
- refactored auto_npl to expose shared _parse_sector; new bank_npl.py reuses it
- registered bank_npl FACTOR -> auto-appears in sources table (6 rows) + blends into banks theme surprise (real NPL)
- +unit tests (test_bank_npl), test_dashboard updated (6 sources)

205 tests pass; verified live API (banks surprise incl. NPL 1.07, 6 sources).
2026-08-26 19:56:39 +07:00
Kunthawat Greethong
fc592d8aa9 [verified] Real backtest engine + backtest UI section (start/end dates, P&L, persisted)
- backtest.py: buy-and-hold backtest over [start,end] — allocates 50/20/30 at first available rebalance date, marks to market to end, accrues dividend, reports {final_value, price_pnl, dividend_income, net_return, trades, holdings}
- Fixed double-spend bug (was allocating full capital every rebalance -> negative cash)
- dashboard.default_scores(): per-symbol combined/dividend/yield baseline for backtest
- POST /api/v1/backtest + GET /api/v1/backtest/runs (results persisted in app state -> survive refresh)
- Frontend: backtest section w/ start/end/capital/freq inputs + P&L KPIs + run history table
- Honest note: uses current combined scores as static baseline (non-PIT); PIT score_fn pluggable
- Verified: 1M -> 1.088M (+8.80%) over 2024-06..2026-06; history persists across refresh
2026-08-26 16:00:17 +07:00
Kunthawat Greethong
5516fc51a0 [verified] LONG/SHORT/NEUTRAL via quartile + market-regime gate (user choice B)
- Signal threshold no longer hardcoded 0.15: now quartile-based (LONG>=Q3, SHORT<=Q1, else NEUTRAL) over the whole SET50 board, recomputed each refresh.
- Market-regime gate: if >=4 themes have negative surprise -> risk-off bear regime -> tighten LONG bar + pull more into SHORT/avoid, so 'best of a falling board' isn't LONG (answers user 'ตลาดตกควรขายทิ้ง').
- SHORT semantics (user confirmed) = 'หลีก/ไม่ถือ' -> cash, NOT short-selling.
- reason_codes + regime now on factor rows (transparent).
- Verified: LONG 12 / SHORT 12 / NEUTRAL 25 in normal regime (Q1=-0.044 Q3=0.407).
- Fixed test_factors_endpoint_signal_join (was asserting AOT LONG from old tourism). Full suite 202 OK.
2026-08-26 15:34:31 +07:00
Kunthawat Greethong
375682d2dc [verified] Signal column now derives from theme engine (combined 60/40 + quality), not tourism_result
- /api/v1/factors signal no longer from tourism-only signals; derived from RealDashboard combined score: LONG>=0.15, SHORT<=-0.15, else NEUTRAL. One source of truth.
- Verified: LONG 25 / NEUTRAL 16 / SHORT 8; BANPU LONG (3 themes), KTB LONG (banks), CPN LONG (3 themes), JMART SHORT (nonbank) — reflects all 13 themes + per-symbol quality, not flat tourism.
- reason_codes + combined_score on each factor row.
2026-08-26 15:26:34 +07:00
Kunthawat Greethong
d850955c44 [verified] Declarative factor engine + per-symbol stock selection (full-app consistency)
- factors.py: FACTORS registry (10 declarative entries: source/fetch/frequency/sign/weight) + normalize/z-score helpers. Add a source = one dict entry, no scoring-function edit.
- themes.THEMES: 13 themes reference FACTORS with per-theme weights (flexible), replacing hardcoded _theme_surprises/_theme_narrative.
- themes.quality_within_theme(): per-symbol quality vs theme cohort (ROE/EPS) -> real stock picking. dashboard board now surprise×quality (BBL 0.5 vs KTB 1.5 in banks).
- board rows carry per-symbol themes[]; /api/v1/themes delegates to RealDashboard.build() -> 13-theme consistency with /api/v1/dashboard (removed 115 lines dead dup logic).
- frontend: deleted THEME_BY_SYMBOL/themeLabelById hardcode; theme column + modal labels+quality all from API. Modal shows surprise×quality=theme_score.
- Tests: 202 OK (quality selection, breakdown quality, themes/dashboard consistency).
- Verified: BBL modal 1.00σ×0.5=0.50σ; KTB 1.5 vs BBL 0.5, PTT 2 themes; 49/49 rows theme from API.
2026-08-26 15:17:41 +07:00
Kunthawat Greethong
33a4662cd4 [verified] Add per-symbol analysis breakdown endpoint + per-theme narrative
- themes.symbol_breakdown(): transparent scoring derivation (theme_score, siamchart_score components, combined = 0.6*theme + 0.4*siamchart npolut)
- GET /api/v1/symbols/<symbol>: themes + theme surprise contributions + fundamentals + price + weights (ข้อ 7)
- dashboard.py _theme_narrative(): long-form Thai explanation of each theme's analysis outcome + implication for its stocks (ข้อ 5)
- 2 tests; full suite OK; live verified (AOT: combined 0.107 = 0.6*0.571 + 0.4*(-0.588))
2026-08-25 21:18:20 +07:00
Kunthawat Greethong
abc06af3a1 [verified] Add in-app automatic data scheduler (runs on its own server, no Hermes)
- scheduler.py: daemon thread inside Flask refreshes all real Thai collectors on interval (default 60min, REFRESH_INTERVAL_SECONDS) via shared daily cache + writes timestamped marker
- create_app starts scheduler (skipped in TESTING); shared daily_cache now an extension
- GET /api/v1/data/last-refresh: automation status + last refresh (every N hours)
- Live verified: refresh_all pulls 5/5 real sources (tourism/auto/NPL/energy/macro)
- 2 tests; full suite 197 OK
2026-08-25 21:00:13 +07:00
Kunthawat Greethong
9739849f68 [verified] Add real multi-theme dashboard (3 themes + macro + board + sources) — req #6/#8/#9/#10
- dashboard.py: RealDashboard assembles real Thai data (tourism + auto+NPL + energy TOP + macro BOT) with uniform z-score surprise per theme, per-theme thesis, sources provenance table, 49-symbol combined board
- macro_thai.py: BOT Thai Economy macro backdrop (consumption +4.9%, inflation 1.95%, unemployment 0.93%, tourists 16.2mn)
- GET /api/v1/dashboard endpoint (real data, no fixture fallback per user)
- 7 new tests; full suite 195 OK; live verified (3 theme surprise: 0.571/0.81/1.623)
2026-08-25 20:30:14 +07:00
Kunthawat Greethong
1e75377732 [verified] Add BOT auto NPL (credit-quality) factor; deepen auto_credit theme
- auto_npl.py: parse BOT Gross NPLs by business (reportID=794); extract auto loan NPL (20,602 mn THB, 3.95% of NPLs, 2.06% of loans)
- /api/v1/themes now exposes auto_npl_pct + auto_npl_amount alongside car-sales volume
- 3 new tests; full suite 188 OK; live verified (themes shows auto_npl_pct 3.95)
2026-08-25 19:06:48 +07:00
Kunthawat Greethong
c3a1461932 [verified] Add capital-allocation simulation engine + MT5 bridge (both dry-run/gated)
- simulation.py: price-series loader (Yahoo snapshot) + allocate_capital 50/20/30 with min-100 shares, bucket3 excludes bucket1, cash fallback
- /api/v1/simulation POST: combines theme 60/40 score + Siamchart dividend + Yahoo price; labels output paper/backtest non-PIT (never validated)
- mt5_bridge.py: MT5 order interface, dry-run default; live dispatch needs MT5_SEND_ORDERS=1 AND approval (Windows-only MetaTrader5)
- 12 new tests (simulation/mt5/api); full suite 184 OK; live verified (1M -> buckets)
2026-08-25 16:24:22 +07:00
Kunthawat Greethong
7643764679 [verified] Add GET /api/v1/themes multi-theme combined board (60% theme / 40% Siamchart)
- Aggregates 3 Thai themes: tourism signals (real), auto_credit (TradingEcon car sales YoY), refining_energy (Thai Oil quarterly net profit/EBITDA) via daily cache
- Combines per-symbol theme scores with Siamchart fundamental score (60/40), sorts by combined score, reports per-theme frequency (monthly/quarterly)
- Added test_themes_endpoint (mocked collectors); full suite 172 OK; compileall ok
2026-08-25 15:33:06 +07:00
Kunthawat Greethong
8a6991b7dd [verified] Add Siamchart factor view + redesigned SET50 dashboard stock board
Backend:
- siamchart_factors.py: build per-symbol factor view from Siamchart snapshot (PE, EPS latest, EPS growth YoY derived from series, dividend yield, P/BV, ROE, is_dividend). eps_latest now returns the most recent year.
- /api/v1/factors endpoint: merge Siamchart fundamentals with the tourism signal (side/score), signal-led sorting.
- test_siamchart_factors.py: 5 tests incl. regression asserting eps == year5 value.

Frontend:
- App.vue/style.css: new 'Stock board' dashboard table (Signal, Symbol, P/E, EPS, EPS YoY, Yield%, P/BV, ROE) with a Dividend-only filter and click-to-sort columns.

Verified: full backend suite 140 tests OK, frontend build OK, static scan clean, live /api/v1/factors 200 (49 factors/46 dividends), rendered table filter+sort verified in browser. Independent review deleg_969513e5 caught+fixed eps bug; re-review deleg_e6bd80db passed=true.
2026-08-25 09:03:28 +07:00
Kunthawat Greethong
ba9114d2e8 [verified] bind snapshots to manifests and raw hashes 2026-08-23 14:58:15 +07:00
Kunthawat Greethong
f55ff69c31 [verified] add frozen research runner and durable paper ledger 2026-08-23 14:42:02 +07:00
Kunthawat Greethong
7f7a6145cd [verified] add SET price snapshot adapter 2026-08-23 12:38:17 +07:00
Kunthawat Greethong
0b47a06238 [verified] add event-study readiness gate 2026-08-23 12:02:22 +07:00
Kunthawat Greethong
d1ba6efc68 [verified] add vintage collector and point-in-time API 2026-08-23 11:11:53 +07:00
Kunthawat Greethong
a113a51589 [verified] add BOT tourism source adapter 2026-08-23 10:19:40 +07:00
Kunthawat Greethong
3e978c5948 [verified] build tourism signal dashboard 2026-08-23 07:41:31 +07:00