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

171 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.