9.7 KiB
Architecture Plan — Additional Data Sources per Theme + Use-in-Analysis Enforcement
Status: SUPERSEDED / largely IMPLEMENTED (2026-08-29). The use-in-analysis invariant + sign fix are done and committed (
fcc0da9); the external-sector gap (this plan's BOT report-60 target) is covered bythai_trade.py(TradingEconomics current-account/exports/imports); commercial-loan coverage is provided byte_loan_growth(TE loans-to-fin-corp) andte_consumer_credit. See2026-08-29_data-source-expansion-phase-2.mdfor the actual delivered work and the remaining backlog (REIC/EPPO/NBTC/energy-breadth). Author: Macky. Date: 2026-08-29.
Goal
Close the "few sources per theme" gap AND lock the user's rule — every data point fetched must feed analysis, not just display. This is the follow-up the declarative factor engine (2026-08-26 plan) explicitly deferred: "no new collectors yet; sources are added as follow-up."
Two deliverables:
- Wiring convention that guarantees use-in-analysis — a build/test pattern so a new source cannot be added without being consumed by
compute_theme_surprises. - Two new high-feasibility Thai sources, wired end-to-end, verified against live pages: BOT Balance of Payments (report 60) and BOT Commercial-bank loans (reportID confirmed at build). A medium-feasibility source (REIC house-price/transfer) is scoped but flagged for a feasibility spike before committing.
Why this is needed (evidence — verified against live sources this session)
- 6 of 13 themes (
telecom_it,property,healthcare,consumer_staples,petrochem_materials,retail) rest on one BOT macro page. Source count is genuinely thin. - Found + fixed (this session, prior commit pending):
core_inflation_yoyandunemployment_pctwere fetched but never used; worse, everysign:-1factor (NPL/inflation/unemployment) used a negative theme weight → double-negative → higher NPL raised the theme score. Both fixed infactors.py/themes.pywith regression tests. This plan prevents recurrence and adds genuinely new providers, not just new fields on the same page. - Feasibility verified live: BOT
ReportPage.aspx?reportID=60(Balance of Payments) responds to the exactdgExcelform-POST flow already implemented inbot_tourism.py(200, parseable — exports/imports/current-account series extracted this session). REIC homepage is a portal — data lives on subpages/endpoints (medium feasibility).
Why the current architecture makes this cheap
Adding a factor already needs no scoring-function change (factors.py registry + a theme factor line). The new work is only:
- a collector module exposing
.to_dict(), - registry + theme wiring,
- a scheduler
_REFRESH_JOBSentry (so it collects + appears in source-health log), - a
_source_labelprovenance entry, - tests.
Decision Gates (approve before code)
- G0 — Approve the 2 source choices + the mandatory-use test contract (below). Exit → build B0.
- G1 — Collector parses a frozen fixture; factor value extracted; used-in-analysis test passes. Exit → wire.
- G2 —
/api/v1/dashboardshows the new source row +source_summaryfactor_keys increases; board surprises change with it. Exit → done.
New collectors
B0 — Thailand External Sector / Trade (thai_trade.py) [high feasibility — VERIFIED current]
- PIVOT from BOT BOP report 60: probing live this session showed the BOT statistics portal's
reportID=60only carries data through 2011 (itsdrpToYearoptions cap at2011xxxx) — stale, not fit for a 2026 platform. Per the plan's gate ("do not ship a factor reading a wrong/stale series") we source the external sector from TradingEconomics Thailand trade, which is live through June 2026 and reuses the exactauto_credit.pyscraping pattern (same site/infobox):https://tradingeconomics.com/thailand/current-account→ current-account balance (USD mn, June 2026 verified)https://tradingeconomics.com/thailand/exports→ exports (USD mn, 2026 verified)https://tradingeconomics.com/thailand/imports→ imports (USD mn, 2026 verified)
- Factors (declarative additions):
external_current_account— current-account balance → sign +1 (strong external position supportive). Feeds tourism, exploration, petrochem_materials, retail.external_exports— exports USD mn → sign +1. Feeds petrochem_materials, exploration, telecom_it.external_imports— imports USD mn (domestic demand) → sign +1. Feeds retail, consumer_staples, property.
- Frequency: monthly. Scheduler cadence: monthly.
- Test: frozen fixture (one value sentence) → parse → assert current account / exports / imports; used-in-analysis test asserts a change in the fed themes.
B1 — BOT Commercial-bank loans (bot_bank_loans.py) [high feasibility — flow VERIFIED, reportID TBD]
- Same
dgExceladapter, new reportID. The exact BOT report for commercial-bank loans/deposits is confirmed accessible in principle (report 60 proved the flow); its numeric reportID is discovered at build time from BOT's statistics index / report search (do NOT hardcode an unverified ID). - Fields expected: total loans outstanding, deposits, NPL (commercial banks), loan growth YoY as derived factor.
- Factors:
bank_loan_growth— commercial-bank loan growth YoY → sign +1. Feeds banks (primary), nonbank_finance, property (mortgage-linked).bank_deposit_growth— deposit growth → sign +1 (funding base). Feeds banks.
- Frequency: monthly (BOT reports these monthly). Scheduler cadence: monthly.
- Gate: if the found reportID/data is not cleanly loan-related, fall back to BOT "credit by sector" report and re-scope the factor — do not ship a factor reading a wrong series.
B2 — REIC house-price / transfer index (reic_housing.py) [MEDIUM feasibility — SPIKE FIRST]
- Homepage is a portal (verified). The house-price index / residential transfer data live on a subpage or a data endpoint (likely JSON/PDF). Spike task before G0 sign-off on this one: locate the real data URL, confirm it is scrapeable without an API key.
- If feasible → factor
reic_housing_price_index/reic_transfer_value_yoy→ sign +1 → feeds property. - If the only clean access is a PDF chart with no numeric series → defer (do not ship a display-only scrape). This is the exact "แค่ดึงข้อมูลมาเฉย ๆ" the user forbids.
Wiring checklist per source (all must land)
backend/app/<module>.py— dataclass +to_dict()+fetch_<module>()(+ frozen-fixture parse fn).factors.py— FACTORS entries (source/frequency/fetch/value_key/sign/center/span).themes.py— factor lines with positive weights on the sign-correct themes (the sign lives in the factor).scheduler.py—_REFRESH_JOBSentry (module/fn/fetch_module/frequency) so it collects + shows in the health log.dashboard.py—_build_sources_source_labelmap entry (provenance table).__init__.py— no endpoint change needed (dashboard drives the UI); verify/api/v1/dashboardpicks it up (G2).
Use-in-analysis enforcement (the user's rule, made a test)
Two contracts, added as tests so "fetched but not used" can never silently return:
- Contract A (value_key resolves): every
FACTORS.value_keymust be a real field the registered fetch module emits. Already added (test_every_factor_value_key_resolves_to_a_fetched_field). Extend it as new modules land. - Contract B (new factor moves scores): for each new factor, a test feeds a neutral vs a hot value and asserts the target theme surprise changes in the right direction (bearish factors lower it). Pattern already added (
test_new_macro_factors_actually_move_scores,test_bearish_factors_move_score_the_right_way). One such test per new factor.
This dual contract is the build guarantee that fulfills "มีแหล่งข้อมูลใหม่ → ต้องใช้ในการวิเคราะห์จริง".
Data Model sketch
# factors.py
"macro_current_account": {"source":"BOT BOP","frequency":"monthly",
"fetch":"bot_bop","value_key":"current_account_balance_mb",
"sign":1, "center":0.0, "span":80000.0},
"bank_loan_growth": {"source":"BOT CB loans","frequency":"monthly",
"fetch":"bot_bank_loans","value_key":"loan_growth_yoy",
"sign":1, "center":5.0, "span":10.0},
# themes.py — positive weights; direction is in the factor
"banks": { "factors": [ {"key":"macro_investment","weight":1.0},
{"key":"bank_loan_growth","weight":0.8},
... ] },
Non-Goals (v1)
- No new factors on the existing single BOT macro page beyond the two already wired (core inflation, unemployment) — avoid the "one page, many pseudo-independent factors" trap.
- No live trading / MT5 changes. No LLM in scoring. No API-key sources (free/scrapeable only).
- Do not ship a REIC scrape unless a clean numeric series exists.
Acceptance (definition of done)
- Thailand external-sector (TradingEconomics trade) collector + 3 factors land, verified against a frozen fixture and a live fetch (current through 2026 — not the stale BOT-60 series).
- BOT commercial-bank-loan collector + factors land (reportID confirmed; else scoped fallback).
- REIC either lands with a real series or is explicitly deferred (no display-only scrape).
- Every new factor has Contract A + Contract B tests green.
- Full backend suite (352 → target ≥ 366) +
npm run buildpass. /api/v1/dashboardsources+source_summary.factor_keysincrease; board surprises visibly move with a hot value.