Files
set50-system/docs/engineering-log/2026-08-29-data-source-expansion-and-ui-fix.md
Kunthawat Greethong 54776d5647 fix(suggestion): profit buckets require combined_score > 0 (owner rule)
Owner: a stock with a NEGATIVE overall (combined) score must not be recommended
in the profit buckets, even with positive momentum + theme. BGRIM/TTB had
combined -0.029/-0.066 yet still landed in bucket 1.

- allocate_capital profit_pool now gates on combined_score > 0 (in addition to
  momentum>0 + theme>0), still ranking by momentum within that pool.
- Verified: bucket 1 now picks PTTGC (+0.209, mom +1.85) instead of BGRIM/TTB;
  BANPU in bucket 2; ADVANC/SCB/LH in bucket 3 by yield. Full suite 377 green.
2026-08-31 19:00:14 +07:00

12 KiB
Raw Permalink Blame History

2026-08-29 — Data-source expansion (Phase A) + frontend padding fix + sign bug

Scope

Two-part request from the owner:

  1. More + genuinely-used data sources per theme — the owner felt the source count was too few and insisted any newly-fetched data must feed the analysis (theme surprise), not just the provenance table.
  2. Frontend padding/margin issues — several panels had misaligned spacing.

What changed (all verified)

Area Change Evidence
factors.py Wired 2 fetched-but-unused BOT fields into the registry: macro_core_inflation (core inflation, sign 1) and macro_unemployment (sign 1) → they now actually move themes Before, macro_thai.to_dict() emitted 7 fields but only 5 were registered factors — 2 were dead display data
themes.py Deepened banks/retail/consumer_staples/healthcare/nonbank_finance with the new factors (positive weights) direction tests
Sign bug fix Found + fixed a double-negative: several factors carry sign: -1 (NPL, inflation, unemployment) while theme weights were ALSO negative → higher NPL/inflation RAISED the theme score (inverted). Flipped all 16 theme weights to positive; direction now lives only in sign. Proven: auto-NPL 3.95→7.0 previously raised auto_credit 0.64→0.799; now lowers it. test_bearish_factors_move_score_the_right_way
NEW te_thailand.py New TradingEconomics collector parsing 2 pages (interest-rate + consumer-confidence) → 6 factors: interest rate, loans-to-fin-corp, consumer credit, household-debt/GDP, retail-sales-YoY, consumer confidence live fetch: rate 1.0%, retail -14.5% YoY, confidence 51.8
themes.py Wired new TE factors into banks (rate/loan), retail + consumer_staples (retail sales + confidence), nonbank_finance (credit + debt + confidence) direction tests
scheduler.py / dashboard.py Registered te_thailand in _REFRESH_JOBS + dashboard cached-fetch + _build_sources provenance dashboard live build: 8 sources
frontend/App.vue Theme cards now show the new reads (banks→interest rate, retail→retail sales YoY, nonbank→household debt %GDP) build clean
frontend/style.css padding/margin + undefined-variable fix — added missing design tokens (--card/--foreground/--accent/--font/...) that the theme/modal components reference but :root never declared (they rendered transparent/wrong-color); zeroed section-panel padding so .signal-header is the single top-spacing source (was double 22px+22px on theme/lineage/health/sim/backtest panels) build clean

Key decisions

  • One new module (te_thailand.py) covers 4 plan items (A1 rate/credit, A2 consumer credit, A3 retail sales, A4 confidence) because all four series live on just two TradingEconomics pages — avoids 4 fragile scrapers.
  • Every source must feed a surprise: tightened test_every_factor_value_key_resolves_to_a_fetched_field so a factor whose value_key the collector never emits fails the suite — no dead factors.
  • Sign convention locked: sign = factor direction, theme weights = positive magnitude. Regression test locks NPL/inflation/unemployment move correctly.

Verified (evidence)

  • Full backend suite: 360 tests OK (up from 352 — 8 new test_te_thailand).
  • Live dashboard build: 13 themes, 8 sources, new TE row; retail surprise 0.165 (correctly negative from retail-sales 14.5% YoY) vs old BOT-only (positive).
  • Frontend npm run build clean.
  • Independent code review: see delegation verdict (fail-closed).

Phase B (same day, follow-up "ทำต่อได้เลย")

Extended the same te_thailand module with 2 more TradingEconomics pages:

  • te_property_prices (residential property prices +1.26% YoY) → property theme
  • te_business_confidence (46.7) → telecom_it + property + healthcare +2 FACTORS, +2 theme wrings, +4 tests (parse + direction for both). Full suite now 362 tests OK; frontend build clean; live fetch confirmed (1.26 / 46.7). Cleared a stale daily-cache te_thailand entry so the new fields show immediately.

Phase C (same day, follow-up "ทำ phase B ต่อได้เลย" — energy breadth)

Feasibility spike of the remaining Phase B sources found most are JS-rendered or anti-bot (recorded in plan), but IRPC performance-highlights is server-rendered and clean. Added:

  • backend/app/energy_irpc.py — collector parsing IRPC net-profit/EBITDA/ROE margin rows [2024,2025,3M26], latest period (3M26: net margin +10.27%)
  • factor energy_irpc_net_margin (sign +1) wired into refining_energy / exploration / utilities (2nd real Thai refiner beyond TOP)
  • scheduler job + dashboard fetch + _build_sources row (now 9 sources)
  • tests: parse (incl. paren-negatives), value-key resolution, direction
  • frontend theme card shows "กำไรสุทธิ IRPC 10.27%" Full suite 368 tests OK; frontend build clean; live dashboard refining_energy surprise 0.764 driven partly by IRPC margin. Independent review deleg_474555e4 passed: true (parser/paren/value-key/wiring all correct, no security/logic errors). Suggestion applied: comment noting ebitda/roe are display-only. Author also flagged that a single upstream page failure (strict _fetch_with_cache → DashboardError) takes down the whole board — consistent with the documented no-fixture-fallback policy; left as an open decision, not silently changed.

Deferred (feasibility blocked): REIC (JS SPA/XHR), EPPO (JS/WordPress), NBTC (403 anti-bot), PTTEP (JS shell), PTT/BCP (404/DNS). Need browser/XHR approach, not plain-HTML — recorded in the plan as a separate effort.

Phase D (same day — 3 follow-up asks)

  1. Flexible scoring (Q2): _fetch_with_cache now degrades instead of raising DashboardError — source down + no cache = return {} (theme drops that source's factors); previously-good value present = daily cache returns stale. Board no longer crashes on any single upstream failure (verified: all-sources- down still builds 13 themes). DashboardError class now unused by build().
  2. Per-source calc detail (Q3): new themes.factor_source_breakdown(fetched, theme) surfaced in dashboard as fetch_data + factor_sources, and in the per-symbol modal as symbolDetail.factor_sources — the owner sees, per theme, each factor's source → raw → normalized → weight → contribution (e.g. retail: te_thailand ยอดขายปลีก -14.5 → -1.0 × 0.7 = -0.7). Backend test added.
  3. HAR feasibility for deferred sources (Q1): captured REIC via har-derived-api-client (Playwright drive → HAR → derived XHR endpoint POST /Home/Web_All_Num_View). Spike shows the method WORKS (browser drives the JS SPA, XHR endpoint derivable) BUT the homepage XHR returned an empty body — actual property data needs a deeper interaction (a real Transfer page click), so a full REIC collector is a larger follow-up, not a quick add. Finishing: commit Q2+Q3 with suite 369 green; Q1 recorded as feasible-but- needs-deeper-capture and left as a decision for the owner.
  • Note: a concurrent process also landed thai_trade.py (external-sector exports/imports/current-account) and external_* factors mid-session; its 3 initially-broken tests were fixed to reach the 360-green baseline here.

Flexible PIT backtest (owner rule — 2026-08-30)

Owner clarified the backtest contract: it must run as soon as there is enough data to estimate an investment, NOT only when EVERY factor has PIT history. Some sources may not provide deep history — scoring is deliberately flexible (a theme scores from whatever subset of factors was knowable that day).

Implemented (commit 2026-08-30):

  • backtest_readiness.evaluate_readiness: readiness is now a usable window (price + Siamchart + >=1 released factor), not all-factors-present. Missing factors are still reported in missing for transparency but no longer block. recommended_start = oldest executable price (the oldest history held); recommended_end = last complete trading day (yesterday Bangkok).
  • pit_scorer.theme_surprise_report: flexible — skips any factor with no release by as_of (no blanket theme block); blocked only when NO factor has a value. pit_meta.partial_pit reflects themes that scored from a partial subset.
  • Verified: /api/v1/backtest/readiness → ready=true, recommended 2024-01-03 → 2026-08-29. POST /api/v1/backtest/run (default window) → 201 full result (final_equity 1,117,243.95 on 1M), no 400 from missing factors.
  • Tests: test_backtest_readiness updated to earliest-runnable semantics; full suite 369 green.

Capital-allocation selection rework (owner rule — 2026-08-31)

Owner clarified what the 3 buckets mean and flagged that the old selection (rank by combined = 60/40 theme+siamchart where siamchart was EPS-growth dominated) picked names like BTS whose EPS was up 137% but whose PRICE was flat/ falling — NOT the owner's "ทำกำไร" (price likely to rise in 3-6 months).

New definition implemented:

  • "ทำกำไร" is now measured by a price-trend (momentum) score, not EPS growth. Added themes.price_trend_score() blending ~3/6/12-month momentum (trading days), z-scored across the universe (heavy weight on 3/6m per the 3-6 month tenure).
  • Buckets 1 & 2 rank by momentum, gated on theme_signal > 0 (mean surprise of the symbol's themes). theme_signal=None (backtest path) is not gated, so the PIT backtest still allocates.
  • Bucket 3 unchanged: highest dividend yield, ignores score/momentum.
  • Suggestion endpoint passes real momentum + theme_signal from the live board.
  • Verified: suggestion now picks CRC+BEM (dividend, rising price) in bucket 1; PTT/MINT (falling price, theme positive) slide to bucket 3 by yield, not bucket 1. Full suite 372 green (3 new momentum/gate tests).

Price-snapshot load fix + momentum gate (owner "เพื่อทดสอบ logic" — 2026-08-31)

  • load_price_snapshot was picking the LAST snapshot by FILENAME (lexicographic), which selected a stale 9-symbol collection over the full 50-symbol universe (2024-01-01.. sorts after 2023-08-28..). Now it picks the snapshot with the latest source.retrieved_at, so the full 50-symbol SET50 universe loads.
  • Suggestion now allocates across all 50 names; verified it picks rising-price, theme-positive dividend names in B1 (BGRIM, TTB), non-dividend rising in B2 (BANPU), and yield-top names in B3 (ADVANC, SCB, LH).
  • Added momentum>0 gate to the profit buckets (a falling-price name must NOT be picked as "ทำกำไร"), while keeping momentum/theme_signal as Optional so the PIT backtest path (which doesn't provide them) still allocates.
  • Regression tests: load_price_snapshot picks most-recently-retrieved; negative momentum excluded from profit buckets. Full suite 374 green.

Refactor: momentum is a factor INSIDE the Siamchart formula (owner rule — 2026-08-31)

The owner insisted momentum must be entered INTO the score formula (a first-class factor), not bolted on outside it. Refactored themes.py from 3 loose weight constants + inline formula repeated in 3 places to a single declarative source:

  • _SIAMCHART_WEIGHTS = {eps_growth:1.5, dividend_yield:2.0, momentum:0.5} (config).
  • siamchart_raw_score(g,d,m) = single source of the formula; momentum is a term INSIDE it. All three call sites (build_siamchart_score, symbol_breakdown raw, symbol_breakdown population) now use it — no more duplicated arithmetic.
  • Purely a refactor: outputs unchanged (0.6/0.4 weighting intact), momentum already weighted 0.5 inside the formula.
  • Added tests: raw score formula incl. momentum term; momentum raises siamchart score given identical fundamentals. Full suite 376 green.

Profit buckets now require a positive combined score (owner rule — 2026-08-31)

The owner flagged that BGRIM/TTB still got recommended in bucket 1 even though their combined score was negative (-0.029 / -0.066): the profit pool only gated on momentum>0 + theme>0, so a stock with good price trend but weak fundamentals (EPS/yield lagging the cohort) could still be suggested.

  • allocate_capital profit_pool now ALSO requires combined_score > 0 — a stock whose overall score is negative is never recommended in buckets 1/2, while still ranking by momentum within that gated pool.
  • Verified: suggestion now picks PTTGC (combined +0.209, mom +1.85) in bucket 1 instead of BGRIM/TTB; BANPU in bucket 2 (combined +0.70); ADVANC/SCB/LH in bucket 3 by yield. Full suite 377 green.