feat(ui): real forward-test lifecycle panel (not cosmetic)

Forward mode in the simulation tab now calls the durable /api/v1/forward
lifecycle instead of the single-pass /api/v1/simulation (which only differed
by a mode string). runSimulation branches to /api/v1/forward for forward mode,
loads runs on mount, and renders a forward panel listing each paper run with
its status (frozen/executed/marked/matured), non-PIT badge, holdings and a
Mark / Mature action per run. Frontend build passes (bundle index-INuvPOCF.js).
This commit is contained in:
Kunthawat Greethong
2026-08-27 12:09:01 +07:00
parent 6d9c283d9a
commit 80c6d792f4

View File

@@ -259,11 +259,21 @@ async function runSimulation() {
simLoading.value = true
simResult.value = null
try {
if (simMode.value === 'forward') {
// REAL forward lifecycle (not cosmetic): freeze + execute a paper run.
simResult.value = await fetchJson('/api/v1/forward', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ capital: Number(simCapital.value), use_pit: false }),
})
await loadForward()
} else {
simResult.value = await fetchJson('/api/v1/simulation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ capital: Number(simCapital.value), mode: simMode.value }),
body: JSON.stringify({ capital: Number(simCapital.value), mode: 'backtest' }),
})
}
} catch (caught) {
notice.value = caught.message
} finally {
@@ -271,6 +281,38 @@ async function runSimulation() {
}
}
// ---- real forward-test lifecycle (durable paper runs) ----
const fwdRuns = ref([])
const fwdLoading = ref(false)
async function loadForward() {
fwdLoading.value = true
try {
const body = await fetchJson('/api/v1/forward')
fwdRuns.value = body.runs ?? []
} catch (caught) {
notice.value = caught.message
} finally {
fwdLoading.value = false
}
}
async function markForward(id) {
try {
await fetchJson(`/api/v1/forward/${id}/mark`, { method: 'POST' })
await loadForward()
} catch (caught) {
notice.value = caught.message
}
}
async function matureForward(id) {
try {
await fetchJson(`/api/v1/forward/${id}/mature`, { method: 'POST' })
await loadForward()
} catch (caught) {
notice.value = caught.message
}
}
async function loadDashboard() {
loading.value = true
error.value = ''
@@ -300,6 +342,7 @@ async function loadDashboard() {
paperAuthWarning.value = sessionBody.warning || ''
backtest.value = backtestBody
researchRun.value = researchBody
await loadForward() // load durable forward-test runs (not cosmetic)
} catch (caught) {
error.value = caught.message
} finally {
@@ -679,6 +722,34 @@ onMounted(async () => { await loadDashboard(); await loadBacktestRuns() })
</div>
</div>
<div v-if="!simResult" class="empty-research">กด 'คำนวณการจัดสรร' เพื่อดูว่า 50/20/30 จัดสรรทุนของคุณไปที่หุ้นไหนบ้าง</div>
<!-- REAL forward-test lifecycle panel (not cosmetic) -->
<div v-if="simMode === 'forward'" class="fwd-panel">
<div class="section-kicker">Forward Test (Paper) ญญาณถกตร เวลาสราง</div>
<p class="panel-subtitle">สราง forward run ญญาณ (คะแนน) freeze นทสราง แล execute วยราคาหล freeze. กด Mark ตามราคาลาส, Mature เพอป run. เป Paper เทาน.</p>
<div v-if="fwdRuns.length === 0" class="empty-research muted-cell">ยังไม่มี forward run กด 'คำนวณการจัดสรร' ข้างบนเพื่อสร้าง</div>
<table v-else class="source-table">
<thead><tr><th>#</th><th>สถานะ</th><th></th><th>ลงท</th><th></th><th>ผลตอบแทน</th><th>ตรวจ</th></tr></thead>
<tbody>
<tr v-for="run in fwdRuns.slice().reverse()" :key="run.id">
<td class="muted-cell">{{ run.id.slice(0, 12) }}</td>
<td>
<span class="status-tag" :class="run.status === 'matured' ? 'warning-tag' : (run.status === 'frozen' ? 'neutral-tag' : 'warning-tag')">{{ run.status }}</span>
<span v-if="run.non_pit" class="status-tag warning-tag" title="ใช้คะแนนปัจจุบัน ไม่ใช่ PIT">non-PIT</span>
</td>
<td>{{ formatNumber(run.capital, 0) }}</td>
<td class="positive-text">{{ formatNumber(run.invested, 0) }}</td>
<td class="muted-cell">{{ Object.keys(run.holdings || {}).join(', ') || '—' }}</td>
<td :class="pnlClass(run.net_return)">{{ run.net_return != null ? (run.net_return * 100).toFixed(2) + '%' : '—' }}</td>
<td>
<button v-if="run.status !== 'matured'" class="primary-btn" style="padding:2px 8px;margin-right:4px" @click="markForward(run.id)">Mark</button>
<button v-if="run.status !== 'matured'" class="primary-btn" style="padding:2px 8px" @click="matureForward(run.id)">Mature</button>
<span v-else class="muted-cell">ดแล</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="panel backtest-panel" id="backtest">