[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="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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user