[verified] Rebuild dashboard: real multi-theme (3 themes + macro + sources table) — user 10-point review
- Remove obsolete KPIs (Theme surprise card, Paper ledger, คุณภาพข้อมูล, ประตู backtest) -> now 2 cards: สัญญาณ (long/short/neutral) + แหล่งข้อมูล - Theme panel: 3 themes with uniform surprise (0.57/0.81/1.62σ) + per-theme thesis (#8,#10) - Add macro backdrop panel (การบริโภค/การลงทุน/เงินเฟ้อ/การว่างงาน/นักท่องเที่ยว) from BOT Thai Economy (#6) - Add sources lineage table (ที่มาข้อมูล: ข้อมูล/แหล่ง/ช่วง/อัปเดต) (#9) - Merge signal+fundamental into one table with combined column (#7) - Live verified via /api/v1/dashboard (real data, 49-symbol board)
This commit is contained in:
@@ -6,6 +6,7 @@ const observations = ref(null)
|
||||
const signalData = ref(null)
|
||||
const factorData = ref(null)
|
||||
const themeData = ref(null)
|
||||
const dashData = ref(null)
|
||||
const simCapital = ref(1000000)
|
||||
const simMode = ref('backtest')
|
||||
const simLoading = ref(false)
|
||||
@@ -35,6 +36,24 @@ const observationRows = computed(() => observations.value?.observations ?? [])
|
||||
|
||||
// Combined factor rows (Siamchart fundamentals + tourism signal).
|
||||
const factorRows = computed(() => factorData.value?.factors ?? [])
|
||||
// real multi-theme dashboard (3 themes + macro + sources)
|
||||
const dashboardThemes = computed(() => dashData.value?.themes ?? [])
|
||||
const dashboardSources = computed(() => dashData.value?.sources ?? [])
|
||||
const dashboardMacro = computed(() => dashData.value?.macro ?? {})
|
||||
const sourceCount = computed(() => dashboardSources.value.length)
|
||||
const realData = computed(() => dashData.value?.available ?? false)
|
||||
// merged stock board: from /api/v1/dashboard board (combined real) + siamchart
|
||||
const combinedRows = computed(() => dashData.value?.board ?? factorRows.value)
|
||||
const boardBySymbol = computed(() => {
|
||||
const m = {}
|
||||
for (const row of combinedRows.value) m[row.symbol] = row
|
||||
return m
|
||||
})
|
||||
// per-theme surprise labels for theme cards + thesis
|
||||
const signalSummary = computed(() => {
|
||||
const s = summary.value?.signal_summary
|
||||
return { long: s?.long ?? 0, short: s?.short ?? 0, neutral: s?.neutral ?? 0, total: s?.total ?? 0 }
|
||||
})
|
||||
const factorAvailable = computed(() => factorData.value?.available ?? false)
|
||||
const dividendCount = computed(() => factorData.value?.dividend_count ?? 0)
|
||||
const signalScoreCount = computed(() => factorRows.value.filter((f) => f.signal_side === 'LONG' || f.signal_side === 'SHORT').length)
|
||||
@@ -218,12 +237,13 @@ async function loadDashboard() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const [summaryBody, observationBody, signalBody, factorBody, themeBody, ledgerBody, sessionBody, backtestBody, researchBody] = await Promise.all([
|
||||
const [summaryBody, observationBody, signalBody, factorBody, themeBody, dashBody, ledgerBody, sessionBody, backtestBody, researchBody] = await Promise.all([
|
||||
fetchJson('/api/v1/dashboard/summary'),
|
||||
fetchJson('/api/v1/factors/tourism/observations'),
|
||||
fetchJson('/api/v1/signals'),
|
||||
fetchJson('/api/v1/factors'),
|
||||
fetchJson('/api/v1/themes'),
|
||||
fetchJson('/api/v1/dashboard'),
|
||||
fetchJson('/api/v1/paper/ledger'),
|
||||
fetchJson('/api/v1/auth/paper', { credentials: 'include' }),
|
||||
fetchBacktestReadiness(),
|
||||
@@ -234,6 +254,7 @@ async function loadDashboard() {
|
||||
signalData.value = signalBody
|
||||
factorData.value = factorBody
|
||||
themeData.value = themeBody
|
||||
dashData.value = dashBody
|
||||
ledger.value = ledgerBody
|
||||
paperAuthenticated.value = Boolean(sessionBody.authenticated)
|
||||
paperAuthMode.value = sessionBody.mode || 'token'
|
||||
@@ -371,29 +392,18 @@ onMounted(loadDashboard)
|
||||
<template v-else>
|
||||
<section class="kpi-grid" aria-label="Signal summary">
|
||||
<article class="kpi-card accent-card">
|
||||
<div class="kpi-label">Theme surprise</div>
|
||||
<div class="kpi-value">{{ summary.theme_surprise > 0 ? '+' : '' }}{{ formatNumber(summary.theme_surprise) }}<span class="kpi-unit">σ</span></div>
|
||||
<div class="kpi-foot" :class="summary.theme_surprise >= 0 ? 'positive-text' : 'negative-text'">{{ summary.theme_surprise >= 0 ? 'สูงกว่า' : 'ต่ำกว่า' }} ค่าตามฤดูกาล</div>
|
||||
</article>
|
||||
<article class="kpi-card">
|
||||
<div class="kpi-label">สัญญาณที่ใช้งาน</div>
|
||||
<div class="kpi-value">{{ summary.signal_summary.total }}</div>
|
||||
<div class="kpi-foot"><span class="long-count">{{ summary.signal_summary.long }} ซื้อ</span> · <span class="short-count">{{ summary.signal_summary.short }} ขาย</span></div>
|
||||
<div class="kpi-value">{{ signalSummary.long }}</div>
|
||||
<div class="kpi-foot">
|
||||
<span class="long-count">{{ signalSummary.long }} ซื้อ</span> ·
|
||||
<span class="short-count">{{ signalSummary.short }} ขาย</span> ·
|
||||
<span class="neutral-count">{{ signalSummary.neutral }} เป็นกลาง</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="kpi-card">
|
||||
<div class="kpi-label">สมุดบันทึก paper</div>
|
||||
<div class="kpi-value">{{ ledger.entries.length }}</div>
|
||||
<div class="kpi-foot">ไม่มีการเชื่อมต่อ external</div>
|
||||
</article>
|
||||
<article class="kpi-card">
|
||||
<div class="kpi-label">คุณภาพข้อมูล</div>
|
||||
<div class="kpi-value quality-value">{{ displayLabel(summary.data_health.status) }}</div>
|
||||
<div class="kpi-foot">Vintage {{ summary.data_health.vintage_id }}</div>
|
||||
</article>
|
||||
<article class="kpi-card">
|
||||
<div class="kpi-label">ประตู backtest (gate)</div>
|
||||
<div class="kpi-value quality-value">{{ displayLabel(backtest?.status) }}</div>
|
||||
<div class="kpi-foot">{{ backtest?.available_events || 0 }} / {{ backtest?.required_events || 0 }} vintages · {{ displayLabel(backtest?.price_snapshot?.quality) || 'ไม่มี price snapshot' }}</div>
|
||||
<div class="kpi-label">แหล่งข้อมูลที่ใช้</div>
|
||||
<div class="kpi-value">{{ sourceCount }}</div>
|
||||
<div class="kpi-foot">ข้อมูลจริงจากแหล่งไทย {{ realData? '(จริง)' : '—' }}</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -407,32 +417,35 @@ onMounted(loadDashboard)
|
||||
<span class="status-tag">รวม {{ combinedCount }} symbols</span>
|
||||
</div>
|
||||
<div class="theme-grid">
|
||||
<article v-for="theme in themes" :key="theme.id" class="theme-card">
|
||||
<article v-for="theme in dashboardThemes" :key="theme.id" class="theme-card">
|
||||
<div class="theme-card-head">
|
||||
<span class="theme-chip">{{ theme.frequency }}</span>
|
||||
<span class="theme-label-en">{{ theme.label_en }}</span>
|
||||
<span class="theme-label-th">{{ theme.label_th }}</span>
|
||||
</div>
|
||||
<div class="theme-surprise">
|
||||
<span class="theme-surprise-label">ความต่าง (surprise)</span>
|
||||
<span class="theme-surprise-value">{{ theme.surprise != null ? formatNumber(theme.surprise, 2) + 'σ' : '—' }}</span>
|
||||
</div>
|
||||
<div class="theme-label-th">{{ theme.label_th }}</div>
|
||||
<div class="theme-source">{{ theme.source }}</div>
|
||||
<div class="theme-read">
|
||||
<template v-if="theme.read && !theme.read.error">
|
||||
<div v-if="theme.id === 'tourism'">
|
||||
<div class="theme-read-value">{{ theme.read.surprise != null ? '+' + formatNumber(theme.read.surprise, 2) + 'σ' : '—' }}</div>
|
||||
<div class="theme-read-meta">theme surprise · as of {{ theme.read.as_of }}</div>
|
||||
</div>
|
||||
<div v-else-if="theme.id === 'auto_credit'">
|
||||
<div class="theme-read-value">{{ theme.read.new_car_sales_yoy != null ? (theme.read.new_car_sales_yoy >= 0 ? '+' : '') + formatNumber(theme.read.new_car_sales_yoy) + '%' : '—' }}</div>
|
||||
<div class="theme-read-meta">ยอดขายรถ YoY · {{ theme.read.total_vehicle_sales ? formatNumber(theme.read.total_vehicle_sales, 0) + ' คัน' : '' }}</div>
|
||||
</div>
|
||||
<div v-else-if="theme.id === 'refining_energy'">
|
||||
<div class="theme-read-value">{{ theme.read.net_profit != null ? formatNumber(theme.read.net_profit / 1000, 1) + ' พันล้าน' : '—' }}</div>
|
||||
<div class="theme-read-meta">TOP กำไรสุทธิ (ล้านบาท) · {{ theme.read.as_of }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="theme-read-error">{{ theme.read?.error || 'ไม่พบข้อมูล' }}</div>
|
||||
<div v-if="theme.id === 'auto_credit' && theme.read.new_car_sales_yoy != null" class="theme-read-value">{{ formatNumber(theme.read.new_car_sales_yoy) }}% YoY ยอดขายรถ</div>
|
||||
<div v-else-if="theme.id === 'auto_credit' && theme.read.auto_npl_pct != null" class="theme-read-value">NPL {{ formatNumber(theme.read.auto_npl_pct) }}%</div>
|
||||
<div v-else-if="theme.id === 'refining_energy' && (theme.read.quarterly || theme.read.net_profit)" class="theme-read-value">กำไรสุทธิ TOP (รายไตรมาส)</div>
|
||||
<div v-else-if="theme.id === 'tourism'" class="theme-read-value">signal tourism {{ theme.surprise != null ? formatNumber(theme.surprise,2) : '—' }}σ</div>
|
||||
</div>
|
||||
<div v-if="theme.thesis" class="theme-thesis">{{ theme.thesis }}</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="Object.keys(dashboardMacro).length" class="macro-panel">
|
||||
<div class="section-kicker">ภาพรวมประเทศไทย (macro)</div>
|
||||
<div class="macro-chips">
|
||||
<span class="macro-chip">การบริโภคภาคเอกชน <strong>{{ dashboardMacro.private_consumption_yoy }}%</strong></span>
|
||||
<span class="macro-chip">การลงทุนเอกชน <strong>{{ dashboardMacro.private_investment_yoy }}%</strong></span>
|
||||
<span class="macro-chip">เงินเฟ้อ <strong>{{ dashboardMacro.headline_inflation_yoy }}%</strong></span>
|
||||
<span class="macro-chip">การว่างงาน <strong>{{ dashboardMacro.unemployment_pct }}%</strong></span>
|
||||
<span class="macro-chip">นักท่องเที่ยว YTD <strong>{{ dashboardMacro.tourists_ytd_mn }} ล้าน</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel stock-panel" id="stocks">
|
||||
@@ -440,7 +453,7 @@ onMounted(loadDashboard)
|
||||
<div>
|
||||
<div class="section-kicker">Alternative factors × SET50</div>
|
||||
<h2>ตารางหุ้น</h2>
|
||||
<p class="panel-subtitle">มูลค่าพื้นฐานจาก Siamchart รวมกับสัญญาณ tourism — คลิกหัวตารางเพื่อเรียง; เปิด <em>เฉพาะหุ้นปันผล</em> เพื่อกรองหุ้นที่จ่ายปันผล.</p>
|
||||
<p class="panel-subtitle">ตารางเดียวรวมทุกธีม — สัญญาณ + คะแนนรวม (60% ธีม / 40% พื้นฐาน) + มูลค่าพื้นฐานจาก Siamchart. เรียงได้โดยคลิกหัวตาราง; เปิด <em>เฉพาะหุ้นปันผล</em> เพื่อกรองหุ้นที่จ่ายปันผล.</p>
|
||||
</div>
|
||||
<div class="stock-controls">
|
||||
<label class="toggle-filter">
|
||||
@@ -456,6 +469,7 @@ onMounted(loadDashboard)
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" :class="{ active: sortKey === 'signal_score' }" @click="setSort('signal_score')">สัญญาณ {{ sortIndicator('signal_score') }}</th>
|
||||
<th class="sortable" :class="{ active: sortKey === 'combined' }" @click="setSort('combined')">รวม {{ sortIndicator('combined') }}</th>
|
||||
<th class="sortable" :class="{ active: sortKey === 'symbol' }" @click="setSort('symbol')">หุ้น {{ sortIndicator('symbol') }}</th>
|
||||
<th class="sortable" :class="{ active: sortKey === 'pe' }" @click="setSort('pe')">P/E {{ sortIndicator('pe') }}</th>
|
||||
<th class="sortable" :class="{ active: sortKey === 'eps' }" @click="setSort('eps')">EPS {{ sortIndicator('eps') }}</th>
|
||||
@@ -468,6 +482,7 @@ onMounted(loadDashboard)
|
||||
<tbody>
|
||||
<tr v-for="factor in sortedFactorRows" :key="factor.symbol">
|
||||
<td><span v-if="factor.signal_side" class="side-pill" :class="factor.signal_side.toLowerCase()">{{ factor.signal_side }}</span><span v-else class="muted-cell">—</span></td>
|
||||
<td class="combined-cell">{{ boardBySymbol[factor.symbol]?.combined != null ? formatNumber(boardBySymbol[factor.symbol].combined) : '—' }}</td>
|
||||
<td><strong class="symbol-name">{{ factor.symbol }}</strong></td>
|
||||
<td class="score-cell">{{ factor.pe != null ? formatNumber(factor.pe) : '—' }}</td>
|
||||
<td>{{ factor.eps != null ? formatNumber(factor.eps) : '—' }}</td>
|
||||
@@ -481,6 +496,34 @@ onMounted(loadDashboard)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel lineage-panel" id="lineage">
|
||||
<div class="panel-header signal-header">
|
||||
<div>
|
||||
<div class="section-kicker">ที่มาของข้อมูล</div>
|
||||
<h2>แหล่งข้อมูลทั้งหมด</h2>
|
||||
<p class="panel-subtitle">รายการแหล่งข้อมูลจริงที่ใช้ — ดึงมาเมื่อใด และข้อมูลชุดไหน ข้อมูลทั้งหมดจากแหล่งไทย.</p>
|
||||
</div>
|
||||
<span class="status-tag">{{ sourceCount }} แหล่ง</span>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="source-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ข้อมูล</th><th>แหล่ง</th><th>ช่วงข้อมูล</th><th>อัปเดต</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(s, i) in dashboardSources" :key="i">
|
||||
<td>{{ s['จาก'] }}</td>
|
||||
<td class="source-name">{{ s['แหล่ง'] }}</td>
|
||||
<td class="muted-cell">{{ s['ข้อมูล'] }}</td>
|
||||
<td class="muted-cell">{{ s['dึงมาเมื่อ'] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hero-grid" id="factors">
|
||||
<article class="panel pulse-panel">
|
||||
<div class="panel-header">
|
||||
|
||||
@@ -125,6 +125,25 @@ tbody tr:hover { background: rgba(255,255,255,.025); }
|
||||
|
||||
@media (max-width: 720px) { .theme-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ---- Rebuilt theme panel: surprise + thesis ---- */
|
||||
.theme-surprise { display: flex; flex-direction: column; gap: 2px; margin: 8px 0 6px; }
|
||||
.theme-surprise-label { font-size: 10px; color: var(--faint); }
|
||||
.theme-surprise-value { font-size: 22px; font-weight: 700; color: var(--accent); }
|
||||
.theme-thesis { font-size: 11px; color: var(--text-2, #99a); border-top: 1px dashed var(--line-bright); padding-top: 6px; margin-top: 6px; }
|
||||
|
||||
.macro-panel { margin-top: 14px; border-top: 1px solid var(--line-bright); padding-top: 12px; }
|
||||
.macro-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.macro-chip { background: var(--card); border: 1px solid var(--line-bright); border-radius: 6px; padding: 6px 9px; font-size: 11px; }
|
||||
.macro-chip strong { color: var(--mint); }
|
||||
|
||||
/* ---- Lineage / sources table ---- */
|
||||
.lineage-panel { margin-top: 18px; }
|
||||
.source-table { width: 100%; border-collapse: collapse; }
|
||||
.source-table th { text-align: left; font: 700 10px 'DM Mono', monospace; color: var(--faint); padding: 6px 8px; border-bottom: 1px solid var(--line-bright); }
|
||||
.source-table td { padding: 7px 8px; font-size: 12px; border-bottom: 1px solid var(--line-weak, rgba(255,255,255,.04)); }
|
||||
.source-name { color: var(--accent); }
|
||||
.combined-cell { color: var(--mint); font-weight: 600; font-family: 'DM Mono', monospace; }
|
||||
|
||||
/* ---- Simulation panel ---- */
|
||||
.sim-panel { margin-top: 18px; }
|
||||
.sim-controls { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 14px; }
|
||||
|
||||
Reference in New Issue
Block a user