[verified] Fix dashboard: real-data badge + sortable คะแนนรวม + modal calc steps
- badge now driven by /api/v1/dashboard availability (ข้อมูลจริงจากแหล่งไทย), not stale fixture summary (#request) - factorValue handles 'combined' -> คะแนนรวม column now sorts + toggles asc/desc - symbol modal shows full calculation: combined_formula, per-step calc (ธีม/พื้นฐาน + weights), z-score derivation with population mean/stdev/universe size - panel-header layout fixes (full-width subtitle, header breathing room) - Verified: badge real, sort 0.11->1.43, modal calc steps render
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
<p class="subtitle">ภาพรวม alternative factors ไทย ไปจนถึงสัญญาณลงทุนที่อธิบายได้ — research + paper only</p>
|
||||
</div>
|
||||
<div class="topbar-meta">
|
||||
<div class="freshness-pill"><span class="freshness-dot"></span>{{ summary?.data_health?.source_mode === 'bot' ? 'ข้อมูลจริง BOT' : 'ข้อมูลจำลอง (fixture)' }}</div>
|
||||
<div class="freshness-pill" :class="realData ? 'pill-live' : 'pill-fixture'"><span class="freshness-dot"></span>{{ realData ? 'ข้อมูลจริงจากแหล่งไทย' : 'ข้อมูลจำลอง (fixture)' }}</div>
|
||||
<div class="as-of">ข้อมูล {{ summary?.as_of || '—' }}</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -679,14 +680,17 @@ onMounted(loadDashboard)
|
||||
</div>
|
||||
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">ที่มาของคะแนนรวม (โปร่งใส)</div>
|
||||
<table class="score-table">
|
||||
<tbody>
|
||||
<tr><td>คะแนนธีม (ธีม {{ symbolDetail.themes?.length || 0 }} ธีม)</td><td>{{ formatNumber(symbolDetail.theme_score) }}</td><td>× {{ symbolDetail.weights?.theme }}</td></tr>
|
||||
<tr><td>คะแนนพื้นฐาน (EPS + yield)</td><td>{{ formatNumber(symbolDetail.siamchart_score) }}</td><td>× {{ symbolDetail.weights?.siamchart }}</td></tr>
|
||||
<tr class="score-total"><td>คะแนนรวม = 60%×ธีม + 40%×พื้นฐาน</td><td>{{ formatNumber(symbolDetail.combined_score) }}</td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal-section-title">ขั้นตอนการคำนวณคะแนนรวม</div>
|
||||
<div class="calc-box">
|
||||
<div class="calc-line">{{ symbolDetail.combined_formula }}</div>
|
||||
<div v-for="item in symbolDetail.combined_calc" :key="item.label" class="calc-step">
|
||||
<div class="calc-step-head"><span>{{ item.label }}</span><strong>{{ formatNumber(item.value) }} × {{ item.weight }}</strong></div>
|
||||
<div class="calc-step-note">{{ item.note }}</div>
|
||||
</div>
|
||||
<div v-if="symbolDetail.siamchart_z_note" class="calc-z">
|
||||
คะแนนพื้นฐานได้จาก z-score: z = (ค่า{{ symbolDetail.siamchart_z_note.raw_i }} − ค่าเฉลี่ย {{ symbolDetail.siamchart_z_note.population_mean }}) / ค่าเบี่ยงเบน {{ symbolDetail.siamchart_z_note.population_stdev }}<br/>เทียบกับ {{ symbolDetail.siamchart_z_note.universe_size }} หุ้นใน SET50
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-sub">ราคาล่าสุด: {{ symbolDetail.price?.latest != null ? formatNumber(symbolDetail.price.latest) : '—' }} ({{ symbolDetail.price?.date || '—' }})</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user