[verified] Task 7: frontend PIT readiness gate + event-driven backtest report

This commit is contained in:
Kunthawat Greethong
2026-08-28 10:32:10 +07:00
parent c87767cfe5
commit d73a58b3d1
4 changed files with 73 additions and 42 deletions

18
frontend/dist/assets/index-CUQk4rwt.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0b1018" /> <meta name="theme-color" content="#0b1018" />
<title>SET50 Signal Lab</title> <title>SET50 Signal Lab</title>
<script type="module" crossorigin src="/assets/index-DDItjwYy.js"></script> <script type="module" crossorigin src="/assets/index-CUQk4rwt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-z73iDem3.css"> <link rel="stylesheet" crossorigin href="/assets/index-z73iDem3.css">
</head> </head>
<body> <body>

View File

@@ -12,13 +12,16 @@ const selectedSymbol = ref(null)
const symbolDetail = ref(null) const symbolDetail = ref(null)
const symbolLoading = ref(false) const symbolLoading = ref(false)
// backtest // backtest
const btStart = ref('2024-06-01') const btStart = ref('')
const btEnd = ref('2026-06-01') const btEnd = ref('')
const btCapital = ref(1000000) const btCapital = ref(1000000)
const btFreq = ref('monthly') const btFreq = ref('event')
const btLoading = ref(false) const btLoading = ref(false)
const btResult = ref(null) const btResult = ref(null)
const btRuns = ref([]) const btRuns = ref([])
// strict PIT backtest readiness (Task 1)
const btReadiness = ref(null)
const btUseLedger = ref(true)
const simMode = ref('backtest') const simMode = ref('backtest')
const simLoading = ref(false) const simLoading = ref(false)
const simResult = ref(null) const simResult = ref(null)
@@ -390,16 +393,26 @@ function closeSymbolDetail() {
symbolDetail.value = null symbolDetail.value = null
} }
async function loadBacktestReadiness() {
try {
const body = await fetchJson('/api/v1/backtest/readiness')
btReadiness.value = body
// apply recommended defaults only when the user hasn't already picked dates
if (!btStart.value && body.recommended_start) btStart.value = body.recommended_start
if (!btEnd.value && body.recommended_end) btEnd.value = body.recommended_end
} catch { btReadiness.value = null }
}
async function runBacktest() { async function runBacktest() {
btLoading.value = true btLoading.value = true
btResult.value = null btResult.value = null
try { try {
btResult.value = await fetchJson('/api/v1/backtest', { btResult.value = await fetchJson('/api/v1/backtest/run', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
start: btStart.value, end: btEnd.value, start: btStart.value, end: btEnd.value,
capital: Number(btCapital.value), freq: btFreq.value, capital: Number(btCapital.value), use_ledger: btUseLedger.value,
}), }),
}) })
await loadBacktestRuns() await loadBacktestRuns()
@@ -410,7 +423,7 @@ async function runBacktest() {
} }
} }
async function loadBacktestRuns() { async function loadBacktestRuns() {
try { btRuns.value = (await fetchJson('/api/v1/backtest/runs')).runs || [] } try { btRuns.value = (await fetchJson('/api/v1/backtest/run')).runs || [] }
catch { btRuns.value = [] } catch { btRuns.value = [] }
} }
const pnlClass = (net) => net != null ? (net >= 0 ? 'positive-text' : 'negative-text') : '' const pnlClass = (net) => net != null ? (net >= 0 ? 'positive-text' : 'negative-text') : ''
@@ -476,7 +489,7 @@ async function recordPaperEntry() {
} }
} }
onMounted(async () => { await loadDashboard(); await loadBacktestRuns() }) onMounted(async () => { await loadDashboard(); await Promise.all([loadBacktestRuns(), loadBacktestReadiness()]) })
</script> </script>
<template> <template>
@@ -774,36 +787,54 @@ onMounted(async () => { await loadDashboard(); await loadBacktestRuns() })
<div> <div>
<div class="section-kicker">การยอนทดสอบ</div> <div class="section-kicker">การยอนทดสอบ</div>
<h2>Backtest (อนทดสอบ)</h2> <h2>Backtest (อนทดสอบ)</h2>
<p class="panel-subtitle">กำหนดชวงว แลวระบบจดสรร 50/20/30 นทเร ลงทนและจนถงวนสนส สรปกำไร/ขาดทนจากราคา + เงนปนผล.</p> <p class="panel-subtitle">กำหนดชวงว แลวระบบจดสรร 50/20/30 นทเร ลงท และปรบพอรตตามขอมลทเผยแพรใหม (event-driven) จนถงวนสนส สรปกำไร/ขาดทนจากราคา + เงนปนผล. าธรรมเนยม 0.3% อรายการ และปนผลเขาบญชใน 30 นหล ex-date.</p>
</div> </div>
</div> </div>
<div class="backtest-controls"> <div class="backtest-controls">
<label>งแต <input type="date" v-model="btStart" /></label> <label>งแต <input type="date" v-model="btStart" /></label>
<label> <input type="date" v-model="btEnd" /></label> <label> <input type="date" v-model="btEnd" /></label>
<label> <input type="number" v-model.number="btCapital" step="100000" /></label> <label> <input type="number" v-model.number="btCapital" step="100000" /></label>
<label>ความถ <label class="checkbox-label" style="display:flex;align-items:center;gap:6px">
<select v-model="btFreq"> <input type="checkbox" v-model="btUseLedger" /> ใช ledger นผลตามวนทจร
<option value="monthly">รายเดอน</option>
<option value="quarterly">รายไตรมาส</option>
</select>
</label> </label>
<button class="primary-btn" :disabled="btLoading" @click="runBacktest">{{ btLoading ? 'กำลังย้อนทดสอบ' : 'รัน Backtest' }}</button> <button class="primary-btn" :disabled="btLoading || (btReadiness && !btReadiness.ready)" @click="runBacktest">{{ btLoading ? 'กำลังย้อนทดสอบ' : 'รัน Backtest' }}</button>
</div>
<div v-if="btReadiness && !btReadiness.ready" class="state-card warning-state">
<strong>งรนยอนทดสอบแบบ strict PIT ไมได ขาดขอม coverage:</strong>
<div class="muted-cell" style="margin-top:4px">{{ (btReadiness.missing || []).slice(0, 8).join(', ') }}{{ (btReadiness.missing || []).length > 8 ? '…' : '' }}</div>
<div class="muted-cell" style="margin-top:2px">นเรมทแนะนำ: {{ btReadiness.recommended_start || '—' }} · นสนส: {{ btReadiness.recommended_end || '—' }}</div>
</div> </div>
<div v-if="btResult?.error" class="state-card error-state">{{ btResult.error }}</div> <div v-if="btResult?.error" class="state-card error-state">{{ btResult.error }}</div>
<div v-else-if="btResult" class="backtest-results"> <div v-else-if="btResult && !btResult.error" class="backtest-results">
<div class="bt-kpi-grid"> <div class="bt-kpi-grid">
<div class="bt-kpi"><span>กำไรจากราคา</span><strong :class="pnlClass(btResult.price_pnl)">{{ formatNumber(btResult.price_pnl) }} บาท</strong></div> <div class="bt-kpi"><span>กำไรจากราคา (realized)</span><strong :class="pnlClass(btResult.realized_trading_pnl)">{{ formatNumber(btResult.realized_trading_pnl) }} บาท</strong></div>
<div class="bt-kpi"><span>{{ dividendMethodLabel(btResult.dividend_method) }}<span class="status-tag" :class="dividendBadge(btResult.dividend_method).cls" :style="dividendBadge(btResult.dividend_method).style || undefined" style="margin-left:6px">{{ dividendBadge(btResult.dividend_method).label }}</span></span><strong class="positive-text">{{ formatNumber(btResult.dividend_income) }} บาท</strong></div> <div class="bt-kpi"><span>กำไรจากราคา (unrealized)</span><strong :class="pnlClass(btResult.unrealized_trading_pnl)">{{ formatNumber(btResult.unrealized_trading_pnl) }} บาท</strong></div>
<div class="bt-kpi"><span>ลคาสดทาย</span><strong>{{ formatNumber(btResult.final_value) }} บาท</strong></div> <div class="bt-kpi"><span>เงนปนผลทได</span><strong class="positive-text">{{ formatNumber(btResult.dividend_cash_received) }} บาท</strong></div>
<div class="bt-kpi"><span>าธรรมเนยม (0.3%)</span><strong class="negative-text">{{ formatNumber(btResult.transaction_costs) }} บาท</strong></div>
<div class="bt-kpi"><span>เงนปนผลคางร</span><strong>{{ formatNumber(btResult.dividend_receivable) }} บาท</strong></div>
<div class="bt-kpi"><span>ลคาสดทาย (equity)</span><strong>{{ formatNumber(btResult.final_equity) }} บาท</strong></div>
<div class="bt-kpi"><span>ผลตอบแทนสทธ</span><strong :class="pnlClass(btResult.net_return)">{{ (btResult.net_return * 100).toFixed(2) }}%</strong></div> <div class="bt-kpi"><span>ผลตอบแทนสทธ</span><strong :class="pnlClass(btResult.net_return)">{{ (btResult.net_return * 100).toFixed(2) }}%</strong></div>
</div> </div>
<div class="bt-meta muted-cell">Trades: {{ btResult.trades }} · วง {{ btResult.start }} {{ btResult.end }}</div> <div class="bt-meta muted-cell">Rebalances: {{ btResult.rebalances }} · นผลตาม: {{ btResult.dividend_timing }} · วง {{ btResult.start }} {{ btResult.end }}</div>
<div class="bt-meta muted-cell">*{{ dividendFootnote(btResult.dividend_method) }}</div> <div v-if="btResult.leakage_guard" class="bt-meta"> strict PIT (leakage guard active)</div>
<div v-if="!btResult.leakage_guard" class="bt-meta muted-cell">คำเตอน: ผลนี้ใช้คะแนนปัจจุบันย้อนหลัง จึงเป็น descriptive non-PIT และไม่ใช่หลักฐานประสิทธิภาพกลยุทธ์</div> <div v-else class="bt-meta muted-cell">คำเตอน: ไมไดจน point-in-time (non-PIT)</div>
<div v-if="Object.keys(btResult.holdings || {}).length" class="bt-holdings"> <div v-if="btResult.holdings && btResult.holdings.length" class="bt-holdings">
<strong>พอรตสดทาย:</strong> <strong>พอรตสดทาย:</strong>
<span v-for="(qty, sym) in btResult.holdings" :key="sym" class="theme-tag">{{ sym }} {{ qty }} หุ้น</span> <table class="source-table" style="margin-top:6px">
<thead><tr><th></th><th>จำนวน</th><th>นทนเฉล</th><th>ราคาลาส</th><th>ลค</th><th>กำไร unrealized</th></tr></thead>
<tbody>
<tr v-for="h in btResult.holdings" :key="h.symbol">
<td class="muted-cell">{{ h.symbol }}</td>
<td>{{ h.qty }}</td>
<td>{{ formatNumber(h.average_cost, 2) }}</td>
<td>{{ formatNumber(h.last_price, 2) }}</td>
<td>{{ formatNumber(h.market_value) }}</td>
<td :class="pnlClass(h.unrealized_pnl)">{{ formatNumber(h.unrealized_pnl) }}</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
<div v-else class="empty-research">กำหนดชวงวนแลวกด 'รัน Backtest' เพอดผล (กำไร/ขาดทนจากราคา + นผล)</div> <div v-else class="empty-research">กำหนดชวงวนแลวกด 'รัน Backtest' เพอดผล (กำไร/ขาดทนจากราคา + นผล)</div>