[verified] Add Thai Simulation tab (capital allocation 50/20/30); remove legacy paper-ledger section

- Simulation panel (07): capital input (บาท), mode (backtest/forward), calls /api/v1/simulation, renders 3 buckets with orders (symbol/qty/price/notional), invested + unallocated cash, honest non-PIT label
- Removed legacy 'Paper ledger' section (superseded by simulation/forward-test per user)
- Sidebar thumb จำลอง; responsive 1-col mobile
- Verified: npm build + browser (3 bucket cards render, ลงทุนรวม/เงินสดเหลือ show)
This commit is contained in:
Kunthawat Greethong
2026-08-25 16:28:12 +07:00
parent c3a1461932
commit e24023cf42
2 changed files with 118 additions and 21 deletions

View File

@@ -6,6 +6,10 @@ const observations = ref(null)
const signalData = ref(null)
const factorData = ref(null)
const themeData = ref(null)
const simCapital = ref(1000000)
const simMode = ref('backtest')
const simLoading = ref(false)
const simResult = ref(null)
const dividendOnly = ref(false)
const sortKey = ref('signal_score')
const sortDirection = ref('desc')
@@ -189,6 +193,27 @@ async function runResearch() {
}
}
const simOrders = computed(() => simResult.value?.orders ?? [])
const simInvested = computed(() => simResult.value?.invested ?? 0)
const simUnallocated = computed(() => simResult.value?.unallocated_cash ?? 0)
const bucketOrders = (n) => simOrders.value.filter((o) => o.bucket === n)
async function runSimulation() {
simLoading.value = true
simResult.value = null
try {
simResult.value = await fetchJson('/api/v1/simulation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ capital: Number(simCapital.value), mode: simMode.value }),
})
} catch (caught) {
notice.value = caught.message
} finally {
simLoading.value = false
}
}
async function loadDashboard() {
loading.value = true
error.value = ''
@@ -312,6 +337,7 @@ onMounted(loadDashboard)
<a class="nav-item" href="#factors"><span class="nav-glyph"></span>จจ</a>
<a class="nav-item" href="#lineage"><span class="nav-glyph"></span>มาขอม</a>
<a class="nav-item" href="#research-run"><span class="nav-glyph"></span>งานว</a>
<a class="nav-item" href="#simulation"><span class="nav-glyph"></span>จำลอง</a>
</nav>
<div class="sidebar-footer">
@@ -555,6 +581,75 @@ onMounted(loadDashboard)
<div v-if="!researchRun" class="empty-research">ยังไม่มี frozen research run. รันการตรวจเพื่อเก็บ input กับ gate decision ปัจจุบัน.</div>
</section>
<section class="panel sim-panel" id="simulation">
<div class="panel-header signal-header">
<div>
<div class="section-kicker">07 / การจำลองการลงท</div>
<h2>ดสรรท (Simulation)</h2>
<p class="panel-subtitle">กรอกท และระบบจดสรรตามสดสวน 50 / 20 / 30 นททำกำไรไดมากสดแลวจายปนผล, นทำกำไรแตไมนผล, และหนปนผลสงส (ไม) นต 100 นตอต.</p>
</div>
<span class="status-tag neutral-tag">{{ simResult ? 'ใช้ได้' : 'รอใส่ทุน' }}</span>
</div>
<div class="sim-controls">
<div class="sim-field">
<label> (บาท)</label>
<input v-model="simCapital" type="number" min="1000" step="1000" />
</div>
<div class="sim-field">
<label>โหมด</label>
<select v-model="simMode">
<option value="backtest">Backtest</option>
<option value="forward">Forward test</option>
</select>
</div>
<button class="primary-button" :disabled="simLoading" @click="runSimulation">{{ simLoading ? 'กำลังคำนวณ' : 'คำนวณการจัดสรร' }}</button>
</div>
<div v-if="simResult" class="sim-result">
<div class="sim-sums">
<div class="sim-sum"><span>ลงทนรวม</span><strong>{{ formatNumber(simInvested, 0) }} บาท</strong></div>
<div class="sim-sum"><span>เงนสดเหล</span><strong>{{ formatNumber(simUnallocated, 0) }} บาท</strong></div>
</div>
<div class="sim-note">{{ simResult.data_note }}</div>
<div class="sim-buckets">
<div class="sim-bucket">
<div class="sim-bucket-head"><span class="sim-bucket-tag b1">50%</span><strong>ทำกำไร + ายปนผล</strong></div>
<table class="sim-order-table">
<tbody v-if="bucketOrders(1).length">
<tr v-for="o in bucketOrders(1)" :key="'b1'+o.symbol">
<td>{{ o.symbol }}</td><td class="muted-cell">qty {{ o.qty }}</td><td class="score-cell">@ {{ formatNumber(o.price) }}</td><td class="score-cell">{{ formatNumber(o.notional, 0) }}</td>
</tr>
</tbody>
<tbody v-else><tr><td class="muted-cell">ไมนทเขาเกณฑ</td></tr></tbody>
</table>
</div>
<div class="sim-bucket">
<div class="sim-bucket-head"><span class="sim-bucket-tag b2">20%</span><strong>ทำกำไร ไมนผล</strong></div>
<table class="sim-order-table">
<tbody v-if="bucketOrders(2).length">
<tr v-for="o in bucketOrders(2)" :key="'b2'+o.symbol">
<td>{{ o.symbol }}</td><td class="muted-cell">qty {{ o.qty }}</td><td class="score-cell">@ {{ formatNumber(o.price) }}</td><td class="score-cell">{{ formatNumber(o.notional, 0) }}</td>
</tr>
</tbody>
<tbody v-else><tr><td class="muted-cell">ไมนทเขาเกณฑ</td></tr></tbody>
</table>
</div>
<div class="sim-bucket">
<div class="sim-bucket-head"><span class="sim-bucket-tag b3">30%</span><strong>นผลสงส (ไม)</strong></div>
<table class="sim-order-table">
<tbody v-if="bucketOrders(3).length">
<tr v-for="o in bucketOrders(3)" :key="'b3'+o.symbol">
<td>{{ o.symbol }}</td><td class="muted-cell">qty {{ o.qty }}</td><td class="score-cell">@ {{ formatNumber(o.price) }}</td><td class="score-cell">{{ formatNumber(o.notional, 0) }}</td>
</tr>
</tbody>
<tbody v-else><tr><td class="muted-cell">ไมนทเขาเกณฑ</td></tr></tbody>
</table>
</div>
</div>
</div>
<div v-if="!simResult" class="empty-research">กด 'คำนวณการจัดสรร' เพื่อดูว่า 50/20/30 จัดสรรทุนของคุณไปที่หุ้นไหนบ้าง</div>
</section>
<section class="bottom-grid">
<article class="panel thesis-panel">
<div class="section-kicker">05 / นทกการวเคราะห</div>
@@ -562,27 +657,6 @@ onMounted(loadDashboard)
<p>อม tourism {{ summary.theme_surprise >= 0 ? 'สูงกว่า' : 'ต่ำกว่า' }}าทคาด เปนตวกำหนดวาหนตวไหนได score บวกหรอลบ ระบบหยดกอนส execution งตองมคน review valuation, price-in, liquidity และ risk.</p>
<div class="thesis-rule"><span></span>Surprise × Exposure × Confidence</div>
</article>
<article class="panel ledger-panel">
<div class="panel-header">
<div><div class="section-kicker">06 / Simulation</div><h2>Paper ledger</h2></div>
<span class="status-tag neutral-tag">ภายในเทาน</span>
</div>
<p>นท assumed fill เพอทดสอบพฤตกรรม portfolio ไม webhook หร order.</p>
<div v-if="paperAuthWarning" class="paper-auth-warning">{{ paperAuthWarning }}</div>
<div v-if="notice" class="notice" :class="notice.includes('recorded') || notice.includes('unlocked') ? 'notice-success' : 'notice-error'">{{ notice }}</div>
<div v-if="!paperAuthenticated && paperAuthMode === 'token' && paperAuthEnabled" class="auth-form">
<div class="auth-copy">การเขยน paper กลอก. กรอก local operator token (ใชสราง HttpOnly session เทาน).</div>
<input v-model="paperToken" type="password" autocomplete="current-password" placeholder="Paper-session token" aria-label="Paper-session token" />
<button class="primary-button" :disabled="unlocking" @click="unlockPaper">{{ unlocking ? 'กำลังปลดล็อก' : 'ปลดล็อก paper ledger' }}</button>
</div>
<div v-else-if="!paperAuthenticated && paperAuthMode === 'token' && !paperAuthEnabled" class="empty-ledger">Paper writes ถูกปิดโดย server configuration.</div>
<div v-else-if="paperAuthenticated && selectedSignal" class="entry-form">
<div class="selected-entry"><strong>{{ selectedSignal.symbol }}</strong><span>{{ selectedSignal.side }} · target {{ (selectedSignal.target_weight * 100).toFixed(1) }}%</span></div>
<input v-model="assumedPrice" type="number" min="0.01" step="0.01" placeholder="Assumed fill price" aria-label="Assumed fill price" />
<button class="primary-button" :disabled="submitting" @click="recordPaperEntry">{{ submitting ? 'กำลังบันทึก' : 'บันทึก paper entry' }}</button>
</div>
<div v-else class="empty-ledger">เลอกสญญาณขางบนเพอบนท paper entry.</div>
</article>
</section>
</template>
</main>

View File

@@ -124,6 +124,29 @@ tbody tr:hover { background: rgba(255,255,255,.025); }
.theme-read-error { font-size: 11px; color: var(--danger, #e5484d); }
@media (max-width: 720px) { .theme-grid { grid-template-columns: 1fr; } }
/* ---- Simulation panel ---- */
.sim-panel { margin-top: 18px; }
.sim-controls { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 14px; }
.sim-field { display: flex; flex-direction: column; gap: 4px; }
.sim-field label { font-size: 11px; color: var(--faint); }
.sim-field input, .sim-field select { background: var(--card); border: 1px solid var(--line-bright); color: var(--text); padding: 7px 9px; border-radius: 5px; font-size: 12px; }
.sim-result { display: flex; flex-direction: column; gap: 12px; }
.sim-sums { display: flex; gap: 24px; }
.sim-sum span { display: block; font-size: 11px; color: var(--faint); }
.sim-sum strong { font-size: 20px; }
.sim-note { font-size: 11px; color: var(--amber); font-style: italic; }
.sim-buckets { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.sim-bucket { border: 1px solid var(--line-bright); border-radius: 8px; padding: 10px; }
.sim-bucket-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.sim-bucket-tag { font: 700 10px 'DM Mono', monospace; padding: 2px 6px; border-radius: 4px; }
.sim-bucket-tag.b1 { background: var(--accent-soft, rgba(63,161,255,.15)); color: var(--accent); }
.sim-bucket-tag.b2 { background: var(--amber-soft, rgba(245,158,11,.15)); color: var(--amber); }
.sim-bucket-tag.b3 { background: var(--mint-soft, rgba(94,234,212,.15)); color: var(--mint); }
.sim-order-table { width: 100%; border-collapse: collapse; }
.sim-order-table td { padding: 3px 4px; font-size: 12px; }
@media (max-width: 720px) { .sim-buckets { grid-template-columns: 1fr; } }
.row-action, .primary-button { padding: 8px 10px; border: 1px solid var(--line-bright); border-radius: 5px; color: var(--text); background: transparent; font-size: 10px; white-space: nowrap; transition: .2s ease; }
.row-action:hover { color: var(--mint); border-color: var(--mint); }