[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))
This commit is contained in:
Kunthawat Greethong
2026-08-25 21:18:20 +07:00
parent bfa9b08af6
commit 33a4662cd4
4 changed files with 165 additions and 1 deletions

View File

@@ -70,3 +70,36 @@ class ThemesTest(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class SymbolBreakdownTest(unittest.TestCase):
def test_breakdown_shows_components(self):
factor_view = {
"factors": [
{"symbol": "AOT", "eps_growth_yoy": 10.0, "dividend_yield": 2.0,
"pe": 20.0, "eps": 5.0, "pbv": 2.0, "roe": 15.0, "is_dividend": True,
"company_name": "Airports"},
]
}
theme_surprises = {"tourism": 0.57, "auto_credit": 0.81, "refining_energy": 1.62}
from app import themes
d = themes.symbol_breakdown("AOT", factor_view=factor_view, theme_surprises=theme_surprises,
latest_price=67.0, price_date="2026-08-21")
self.assertEqual(d["symbol"], "AOT")
self.assertEqual(d["themes"], ["tourism"]) # AOT in tourism map
self.assertEqual(d["theme_contributions"][0]["surprise"], 0.57)
self.assertIn("siamchart_components", d)
self.assertEqual(d["weights"], {"theme": 0.6, "siamchart": 0.4})
self.assertIsInstance(d["combined_score"], float)
self.assertEqual(d["price"]["latest"], 67.0)
self.assertIn("fundamentals", d)
def test_breakdown_symbol_without_theme(self):
factor_view = {"factors": [{"symbol": "BANPU", "eps_growth_yoy": -2.0,
"dividend_yield": 0.0, "is_dividend": False,
"company_name": "BANPU"}]}
from app import themes
d = themes.symbol_breakdown("BANPU", factor_view=factor_view, theme_surprises={})
self.assertEqual(d["theme_score"], 0.0)
self.assertEqual(d["themes"], ["refining_energy"]) # BANPU in energy map
self.assertEqual(d["theme_contributions"], []) # but no surprise set -> 0