From 54776d564720dcf73c40035cbf3e8cec372a2f21 Mon Sep 17 00:00:00 2001 From: Kunthawat Greethong Date: Mon, 31 Aug 2026 19:00:14 +0700 Subject: [PATCH] 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. --- backend/app/simulation.py | 12 +++++++----- backend/tests/test_simulation.py | 12 ++++++++++++ .../2026-08-29-data-source-expansion-and-ui-fix.md | 12 ++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/backend/app/simulation.py b/backend/app/simulation.py index 04b91e2..2e78e91 100644 --- a/backend/app/simulation.py +++ b/backend/app/simulation.py @@ -135,13 +135,15 @@ def allocate_capital( if not candidates: raise SimulationError("no candidates to allocate") - # "ทำกำไร" = a price likely to rise in the next 3-6 months, measured by a - # POSITIVE price-trend momentum AND a positive theme signal. This is the - # owner's definition — NOT EPS growth / combined score. Buckets 1 & 2 rank by - # momentum among that pool; bucket 3 ranks purely by dividend yield. + # "ทำกำไร" = price likely to rise (POSITIVE momentum) + positive theme signal + # + a NON-NEGATIVE combined score (owner: a stock with a negative overall + # score must not be recommended in the profit buckets). This is the owner's + # definition — NOT EPS-growth ranking. Buckets 1 & 2 rank by momentum within + # this gated pool; bucket 3 ranks purely by dividend yield. profit_pool = [c for c in candidates if (c.theme_signal is None or c.theme_signal > 0.0) - and (c.momentum is None or c.momentum > 0.0)] + and (c.momentum is None or c.momentum > 0.0) + and c.combined_score > 0.0] by_momentum = sorted(profit_pool, key=lambda c: -(c.momentum or 0.0)) by_yield = sorted( (c for c in candidates if c.is_dividend and c.dividend_yield > 0), diff --git a/backend/tests/test_simulation.py b/backend/tests/test_simulation.py index 04bcab5..5f3007a 100644 --- a/backend/tests/test_simulation.py +++ b/backend/tests/test_simulation.py @@ -142,6 +142,18 @@ class MomentumSelectionTest(unittest.TestCase): self.assertEqual(b2, ["UP"]) # DOWN (falling price) excluded self.assertNotIn("DOWN", b2) + def test_negative_combined_excluded_from_profit_buckets(self): + # Owner rule: a stock whose OVERALL (combined) score is negative must not + # be recommended in the profit buckets, even with positive momentum/theme. + cands = [ + self._cand("NEG_A", 10.0, -0.05, True, 2.0, +2.0, 0.6), # momentum up, theme +, but combined negative + self._cand("POS_B", 10.0, 0.20, True, 2.0, +1.5, 0.6), # combined positive + ] + res = allocate_capital(1_000_000, cands) + b1 = [o.symbol for o in res.orders if o.bucket == 1] + self.assertNotIn("NEG_A", b1) # combined negative -> excluded + self.assertIn("POS_B", b1) + class PriceSnapshotTest(unittest.TestCase): """Regression: load_price_snapshot must pick the snapshot retrieved MOST diff --git a/docs/engineering-log/2026-08-29-data-source-expansion-and-ui-fix.md b/docs/engineering-log/2026-08-29-data-source-expansion-and-ui-fix.md index 48d30ff..da86ed4 100644 --- a/docs/engineering-log/2026-08-29-data-source-expansion-and-ui-fix.md +++ b/docs/engineering-log/2026-08-29-data-source-expansion-and-ui-fix.md @@ -156,3 +156,15 @@ constants + inline formula repeated in 3 places to a single declarative source: 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.