[verified] Task 7: frontend PIT readiness gate + event-driven backtest report
This commit is contained in:
18
frontend/dist/assets/index-CUQk4rwt.js
vendored
Normal file
18
frontend/dist/assets/index-CUQk4rwt.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
frontend/dist/assets/index-DDItjwYy.js
vendored
18
frontend/dist/assets/index-DDItjwYy.js
vendored
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0b1018" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -12,13 +12,16 @@ const selectedSymbol = ref(null)
|
||||
const symbolDetail = ref(null)
|
||||
const symbolLoading = ref(false)
|
||||
// backtest
|
||||
const btStart = ref('2024-06-01')
|
||||
const btEnd = ref('2026-06-01')
|
||||
const btStart = ref('')
|
||||
const btEnd = ref('')
|
||||
const btCapital = ref(1000000)
|
||||
const btFreq = ref('monthly')
|
||||
const btFreq = ref('event')
|
||||
const btLoading = ref(false)
|
||||
const btResult = ref(null)
|
||||
const btRuns = ref([])
|
||||
// strict PIT backtest readiness (Task 1)
|
||||
const btReadiness = ref(null)
|
||||
const btUseLedger = ref(true)
|
||||
const simMode = ref('backtest')
|
||||
const simLoading = ref(false)
|
||||
const simResult = ref(null)
|
||||
@@ -390,16 +393,26 @@ function closeSymbolDetail() {
|
||||
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() {
|
||||
btLoading.value = true
|
||||
btResult.value = null
|
||||
try {
|
||||
btResult.value = await fetchJson('/api/v1/backtest', {
|
||||
btResult.value = await fetchJson('/api/v1/backtest/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
start: btStart.value, end: btEnd.value,
|
||||
capital: Number(btCapital.value), freq: btFreq.value,
|
||||
capital: Number(btCapital.value), use_ledger: btUseLedger.value,
|
||||
}),
|
||||
})
|
||||
await loadBacktestRuns()
|
||||
@@ -410,7 +423,7 @@ async function runBacktest() {
|
||||
}
|
||||
}
|
||||
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 = [] }
|
||||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -774,36 +787,54 @@ onMounted(async () => { await loadDashboard(); await loadBacktestRuns() })
|
||||
<div>
|
||||
<div class="section-kicker">การย้อนทดสอบ</div>
|
||||
<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 class="backtest-controls">
|
||||
<label>ตั้งแต่ <input type="date" v-model="btStart" /></label>
|
||||
<label>ถึง <input type="date" v-model="btEnd" /></label>
|
||||
<label>ทุน <input type="number" v-model.number="btCapital" step="100000" /></label>
|
||||
<label>ความถี่
|
||||
<select v-model="btFreq">
|
||||
<option value="monthly">รายเดือน</option>
|
||||
<option value="quarterly">รายไตรมาส</option>
|
||||
</select>
|
||||
<label class="checkbox-label" style="display:flex;align-items:center;gap:6px">
|
||||
<input type="checkbox" v-model="btUseLedger" /> ใช้ ledger ปันผลตามวันที่จริง
|
||||
</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 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"><span>กำไรจากราคา</span><strong :class="pnlClass(btResult.price_pnl)">{{ formatNumber(btResult.price_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>มูลค่าสุดท้าย</span><strong>{{ formatNumber(btResult.final_value) }} บาท</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>กำไรจากราคา (unrealized)</span><strong :class="pnlClass(btResult.unrealized_trading_pnl)">{{ formatNumber(btResult.unrealized_trading_pnl) }} บาท</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>
|
||||
<div class="bt-meta muted-cell">Trades: {{ btResult.trades }} · ช่วง {{ 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 muted-cell">คำเตือน: ผลนี้ใช้คะแนนปัจจุบันย้อนหลัง จึงเป็น descriptive non-PIT และไม่ใช่หลักฐานประสิทธิภาพกลยุทธ์</div>
|
||||
<div v-if="Object.keys(btResult.holdings || {}).length" class="bt-holdings">
|
||||
<div class="bt-meta muted-cell">Rebalances: {{ btResult.rebalances }} · ปันผลตาม: {{ btResult.dividend_timing }} · ช่วง {{ btResult.start }} → {{ btResult.end }}</div>
|
||||
<div v-if="btResult.leakage_guard" class="bt-meta">✅ strict PIT (leakage guard active)</div>
|
||||
<div v-else class="bt-meta muted-cell">คำเตือน: ไม่ได้พิสูจน์ point-in-time (non-PIT)</div>
|
||||
<div v-if="btResult.holdings && btResult.holdings.length" class="bt-holdings">
|
||||
<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 v-else class="empty-research">กำหนดช่วงวันแล้วกด 'รัน Backtest' เพื่อดูผล (กำไร/ขาดทุนจากราคา + ปันผล)</div>
|
||||
|
||||
Reference in New Issue
Block a user