[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:
@@ -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>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user