diff --git a/backend/app/themes.py b/backend/app/themes.py index ef5dc90..fbe35fd 100644 --- a/backend/app/themes.py +++ b/backend/app/themes.py @@ -17,6 +17,7 @@ were the same timestamp — see `frequency` on each scored theme. from __future__ import annotations +import statistics from dataclasses import dataclass from typing import Optional @@ -200,8 +201,22 @@ def symbol_breakdown( d = fac.get("dividend_yield") or 0.0 g = float(g) if g is not None else 0.0 raw_siamchart = g + d * 2.0 - # z-score against the full universe (same as build_siamchart_score) - siamchart_score = build_siamchart_score(factor_view).get(symbol, 0.0) + + # z-score against the full universe (same as build_siamchart_score); capture + # the population stats so the view can show HOW -2.8 became -0.588. + siamchart_map = build_siamchart_score(factor_view) + siamchart_score = siamchart_map.get(symbol, 0.0) + # recompute the population of raw scores to expose mean / stdev + raw_values = [] + for f in factor_view.get("factors", []): + if not f.get("symbol"): + continue + gg = f.get("eps_growth_yoy") + dd = f.get("dividend_yield") or 0.0 + gg = float(gg) if gg is not None else 0.0 + raw_values.append(gg + dd * 2.0) + pop_mean = statistics.mean(raw_values) if raw_values else 0.0 + pop_stdev = statistics.pstdev(raw_values) if raw_values else 0.0 combined = round(weight_theme * theme_score + weight_siamchart * siamchart_score, 3) @@ -217,7 +232,22 @@ def symbol_breakdown( "dividend_yield": fac.get("dividend_yield"), "raw_growth_plus_yield_2x": round(raw_siamchart, 3), }, + "siamchart_z_note": { + "formula": "z = (raw_i - mean) / stdev", + "raw_i": round(raw_siamchart, 3), + "population_mean": round(pop_mean, 3), + "population_stdev": round(pop_stdev, 3), + "universe_size": len(raw_values), + }, + "combined_calc": [ + {"label": "คะแนนธีม", "value": round(theme_score, 3), "weight": weight_theme, + "note": "ค่าเฉลี่ยของ theme surprise ที่หุ้นนี้อยู่ใน", "term": f"{theme_score:.3f} × {weight_theme}"}, + {"label": "คะแนนพื้นฐาน", "value": round(siamchart_score, 3), "weight": weight_siamchart, + "note": f"z-score ของ ((EPS growth {g:+.1f}%) + ปันผล {d:.1f}%×2) เมื่อเทียบทั้ง universe", + "term": f"{siamchart_score:.3f} × {weight_siamchart}"}, + ], "combined_score": combined, + "combined_formula": f"({theme_score:.3f} × {weight_theme}) + ({siamchart_score:.3f} × {weight_siamchart}) = {combined}", "weights": {"theme": weight_theme, "siamchart": weight_siamchart}, "fundamentals": { "pe": fac.get("pe"), diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ab910e1..cfd4de9 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -93,6 +93,7 @@ const filteredFactorRows = computed(() => { // Numeric accessor used for sorting columns. function factorValue(row, key) { if (key === 'signal_score') return row.signal_score ?? (row.signal_side === 'LONG' ? 9999 : 0) + if (key === 'combined') return boardBySymbol.value[row.symbol]?.combined ?? -9999 if (key === 'symbol') return row.symbol if (key === 'dividend_yield') return row.dividend_yield ?? -1 if (key === 'eps_growth_yoy') return row.eps_growth_yoy ?? -1 @@ -418,7 +419,7 @@ onMounted(loadDashboard)

ภาพรวม alternative factors ไทย ไปจนถึงสัญญาณลงทุนที่อธิบายได้ — research + paper only

-
{{ summary?.data_health?.source_mode === 'bot' ? 'ข้อมูลจริง BOT' : 'ข้อมูลจำลอง (fixture)' }}
+
{{ realData ? 'ข้อมูลจริงจากแหล่งไทย' : 'ข้อมูลจำลอง (fixture)' }}
ข้อมูล {{ summary?.as_of || '—' }}
@@ -679,14 +680,17 @@ onMounted(loadDashboard) diff --git a/frontend/src/style.css b/frontend/src/style.css index 16cfab2..22d0a3a 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -56,6 +56,8 @@ h2 { margin-top: 7px; font-size: 17px; letter-spacing: -.025em; } .subtitle { max-width: 560px; margin-top: 12px; color: var(--muted); font-size: 13px; line-height: 1.7; } .topbar-meta { text-align: right; color: var(--muted); font: 10px 'DM Mono', monospace; } .freshness-pill { display: inline-flex; align-items: center; gap: 8px; padding: 7px 10px; border: 1px solid rgba(82,214,189,.25); border-radius: 99px; color: var(--mint); background: var(--mint-soft); } +.freshness-pill.pill-fixture { border-color: rgba(230,185,108,.35); color: var(--amber); background: var(--amber-soft); } +.freshness-pill.pill-fixture .freshness-dot { background: var(--amber); box-shadow: 0 0 12px var(--amber); } .as-of { margin-top: 10px; } .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 12px; } @@ -93,6 +95,9 @@ h2 { margin-top: 7px; font-size: 17px; letter-spacing: -.025em; } .signal-panel { padding: 0; overflow: hidden; } .signal-header { padding: 22px; border-bottom: 1px solid var(--line); } +.signal-header > div:first-child { flex: 1 1 auto; min-width: 0; } +.stock-panel .signal-header { align-items: center; flex-wrap: wrap; } +.stock-panel .panel-subtitle { max-width: 620px; } .strategy-meta { color: var(--faint); font: 10px 'DM Mono', monospace; } .strategy-meta span { color: var(--line-bright); padding: 0 5px; } .table-wrap { overflow-x: auto; } @@ -172,6 +177,15 @@ tbody tr:hover { background: rgba(255,255,255,.025); } .score-table td { padding: 7px 6px; border-bottom: 1px solid var(--line-weak, rgba(255,255,255,.05)); } .score-table tr.score-total td { border-top: 1px solid var(--line-bright); font-weight: 700; color: var(--mint); } +/* calculation steps in the symbol modal */ +.calc-box { background: rgba(0,0,0,.25); border: 1px solid var(--line-bright); border-radius: 8px; padding: 12px; } +.calc-line { font: 700 14px 'DM Mono', monospace; color: var(--mint); padding: 6px 0 10px; border-bottom: 1px dashed var(--line-bright); margin-bottom: 8px; } +.calc-step { padding: 6px 0; border-bottom: 1px solid var(--line-weak, rgba(255,255,255,.05)); } +.calc-step-head { display: flex; justify-content: space-between; font-size: 13px; } +.calc-step-head strong { color: var(--text); font-family: 'DM Mono', monospace; } +.calc-step-note { font-size: 11px; color: var(--faint); margin-top: 3px; line-height: 1.5; } +.calc-z { font-size: 11px; color: var(--faint); margin-top: 8px; line-height: 1.6; } + .thesis-list { display: flex; flex-direction: column; gap: 10px; margin: 12px 0; } .thesis-row { display: flex; gap: 10px; align-items: baseline; padding-bottom: 8px; border-bottom: 1px solid var(--line-weak, rgba(255,255,255,.05)); } .thesis-theme { min-width: 130px; color: var(--accent); }