Files
SET100-Trading-system/primethai/backtest.html
2026-09-29 14:08:43 +07:00

177 lines
20 KiB
HTML

<!doctype html>
<html lang="th">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PrimeThai | Strategy desk</title>
<style>
:root { color-scheme: dark; --bg:#0b1118; --panel:#121c27; --panel2:#172432; --line:#263645; --muted:#91a2b3; --text:#edf3f7; --gold:#f1bd65; --green:#62d6a1; --red:#ff7c7c; --blue:#75b9f4; }
* { box-sizing:border-box; }
body { margin:0; background:radial-gradient(ellipse at 15% -20%,#20364a 0,transparent 43%),var(--bg); color:var(--text); font:15px/1.5 "Segoe UI",Tahoma,sans-serif; }
main { width:min(1180px,calc(100% - 36px)); margin:34px auto 64px; }
header { display:flex; align-items:flex-start; justify-content:space-between; gap:20px; margin-bottom:22px; }
nav { display:flex; align-items:center; flex-wrap:wrap; gap:9px; }
nav form { margin:0; }
.eyebrow { color:var(--gold); font-size:12px; letter-spacing:.16em; text-transform:uppercase; font-weight:700; }
h1 { margin:4px 0 6px; font-size:clamp(25px,4vw,38px); line-height:1.2; }
h2 { margin:0; font-size:18px; }
p { margin:0; }
.sub { color:var(--muted); }
button { border:1px solid #d5a850; border-radius:9px; background:var(--gold); color:#1a1b1b; font:inherit; font-size:14px; font-weight:700; padding:11px 15px; cursor:pointer; white-space:nowrap; }
button:hover { background:#ffd488; }
button:disabled { opacity:.65; cursor:wait; }
.status { display:flex; align-items:center; gap:9px; border:1px solid #315b4d; background:#10271f; color:#b8f2d7; padding:11px 14px; border-radius:10px; margin:0 0 18px; }
.dot { width:8px; height:8px; border-radius:50%; background:var(--green); box-shadow:0 0 12px #62d6a177; flex:none; }
.grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; }
.card,.panel { background:linear-gradient(145deg,#15212d,#111a24); border:1px solid var(--line); border-radius:13px; }
.card { padding:16px 17px; min-height:110px; }
.label { color:var(--muted); font-size:13px; }
.value { font-size:27px; font-weight:720; letter-spacing:-.025em; margin-top:5px; }
.detail { color:var(--muted); font-size:12px; margin-top:2px; }
.positive { color:var(--green); } .negative { color:var(--red); }
.two-col { display:grid; grid-template-columns:minmax(0,1.5fr) minmax(270px,.8fr); gap:12px; margin-top:12px; }
.panel { padding:18px; min-width:0; }
.panel-head { display:flex; justify-content:space-between; align-items:center; gap:16px; margin-bottom:14px; }
.badge { display:inline-block; border:1px solid #425366; border-radius:999px; padding:4px 9px; color:#c8d5e0; font-size:11px; white-space:nowrap; }
#curve { display:block; width:100%; height:225px; }
.chart-labels { display:flex; justify-content:space-between; color:var(--muted); font-size:12px; }
.metric-list { display:grid; gap:0; }
.metric-row { display:flex; justify-content:space-between; gap:12px; padding:11px 0; border-bottom:1px solid #22313e; }
.metric-row:last-child { border:0; }
.metric-row span:first-child { color:var(--muted); }
.signals { margin-top:12px; }
.table-wrap { overflow:auto; }
table { width:100%; border-collapse:collapse; min-width:730px; }
th,td { text-align:left; padding:11px 10px; border-bottom:1px solid #263542; }
th { color:var(--muted); font-size:12px; font-weight:600; }
td { font-size:14px; }
td.num,th.num { text-align:right; font-variant-numeric:tabular-nums; }
.symbol { color:#fff; font-weight:700; letter-spacing:.03em; }
.source { color:#b9c8d6; font-size:12px; }
.empty { text-align:center; color:var(--muted); padding:27px 8px; }
.notes { margin-top:12px; color:var(--muted); font-size:13px; }
.notes ul { margin:8px 0 0; padding-left:19px; }
.notes li { margin:5px 0; }
.refresh-line { color:var(--muted); font-size:12px; margin-top:9px; min-height:18px; }
footer { color:#708293; font-size:12px; margin-top:20px; }
@media(max-width:800px) { .grid{grid-template-columns:repeat(2,minmax(0,1fr))}.two-col{grid-template-columns:1fr}header{align-items:center} }
@media(max-width:520px) { main{width:calc(100% - 22px);margin:20px auto 42px}header{align-items:flex-start;flex-direction:column}.grid{gap:8px}.card{padding:13px;min-height:100px}.value{font-size:23px}.panel{padding:14px} }
</style>
</head>
<body>
<main>
<header>
<div>
<div class="eyebrow">PrimeThai · Research dashboard</div>
<h1>Donchian 55/20 + EMA 50/200</h1>
<p class="sub"><a href="/" style="color:#62d6a1">← กลับหน้าพอร์ตจริง</a> · หน้าผล backtest และสัญญาณย้อนหลัง</p>
</div>
<nav>
<button id="refresh" type="button">ดึงข้อมูลล่าสุดและคำนวณสัญญาณ</button>
<form action="/logout" method="post"><button type="submit">ออกจากระบบ</button></form>
</nav>
</header>
<div class="status"><span class="dot"></span><span>Paper / manual review เท่านั้น · ไม่มีการส่งคำสั่งซื้อขายผ่านโบรกเกอร์</span></div>
<section class="grid" aria-label="ผล backtest">
<article class="card"><div class="label">ข้อมูลราคาปิดล่าสุด</div><div id="asof" class="value">—</div><div class="detail">สัญญาณสำหรับวันซื้อขายถัดไป</div></article>
<article class="card"><div class="label">ผลตอบแทน YTD · ต้นทุนฐาน</div><div id="return" class="value">—</div><div id="ending" class="detail">—</div></article>
<article class="card"><div class="label">ขาดทุนสูงสุดจากจุดสูงสุด</div><div id="drawdown" class="value">—</div><div class="detail">คำนวณจากมูลค่าพอร์ตใน backtest</div></article>
<article class="card"><div class="label">ผลตอบแทนเมื่อต้นทุนสูงขึ้น</div><div id="stress" class="value">—</div><div class="detail">ค่าคอมมิชชัน 0.30% + slippage 15 bps/ข้าง</div></article>
</section>
<section class="two-col">
<article class="panel">
<div class="panel-head"><h2>เส้นมูลค่าพอร์ต</h2><span class="badge" id="capital">เริ่มเงินสด 1,000,000 บาท</span></div>
<svg id="curve" viewBox="0 0 800 225" role="img" aria-label="กราฟมูลค่าพอร์ตจาก backtest"></svg>
<div class="chart-labels"><span id="chart-start">—</span><span id="chart-end">—</span></div>
</article>
<article class="panel">
<div class="panel-head"><h2>รายละเอียดรอบทดสอบ</h2><span class="badge">หุ้นละ 100 หุ้น</span></div>
<div class="metric-list">
<div class="metric-row"><span>จำนวนวันซื้อขาย</span><strong id="sessions">—</strong></div>
<div class="metric-row"><span>มูลค่าพอร์ตปลายงวด</span><strong id="end-value">—</strong></div>
<div class="metric-row"><span>ต้นทุนซื้อขายที่จำลอง</span><strong id="costs">—</strong></div>
<div class="metric-row"><span>จำนวนรายการซื้อ/ขาย</span><strong id="fills">—</strong></div>
<div class="metric-row"><span>ซื้อ+ขาย เทียบเงินตั้งต้น</span><strong id="turnover">—</strong></div>
<div class="metric-row"><span>จำนวนหุ้นเฉลี่ยในพอร์ต</span><strong id="positions">—</strong></div>
<div class="metric-row"><span>สัดส่วนเงินลงทุนเฉลี่ย</span><strong id="exposure">—</strong></div>
</div>
<div class="refresh-line" id="updated">กำลังอ่านรายงานในเครื่อง…</div>
</article>
</section>
<section class="panel signals">
<div class="panel-head"><div><h2>เป้าหมายถือหุ้นรอบถัดไป</h2><div class="sub" style="font-size:13px;margin-top:3px">จำนวนคำนวณจากราคาปิดและพอร์ตตัวอย่าง 1 ล้านบาท · เป็น target basket ไม่ใช่คำสั่งซื้อ</div></div><span class="badge">ยังไม่เทียบกับพอร์ตที่ถือจริง</span></div>
<div class="table-wrap"><table>
<thead><tr><th>หุ้น</th><th>โมเดลที่ส่งสัญญาณ</th><th class="num">น้ำหนัก</th><th class="num">ราคาปิดอ้างอิง</th><th class="num">หุ้นโดยประมาณ</th><th>ข้อมูลถึงวันที่</th><th class="num">วันซื้อขายที่ข้อมูลค้าง</th></tr></thead>
<tbody id="signals"><tr><td colspan="6" class="empty">ยังไม่มีรายงาน · กด “คำนวณใหม่” เพื่อเริ่ม</td></tr></tbody>
</table></div>
</section>
<section class="panel notes">
<h2>กติกาและข้อจำกัดที่ควรรู้</h2>
<ul>
<li>ซื้อเมื่อ Donchian 55/20 หรือ EMA 50/200 เลือกหุ้น; หุ้นที่ติดทั้งสองโมเดลนับครั้งเดียว และแบ่งน้ำหนักเท่ากัน สูงสุด 20 หุ้น</li>
<li>ตารางด้านบนแสดงเป้าหมายถือหุ้น ไม่ได้แสดงจำนวนที่ควรซื้อหรือขายสุทธิ; ถ้ามีหุ้นอยู่แล้วต้องเทียบกับพอร์ตจริงก่อนส่งคำสั่ง</li>
<li>backtest เริ่มพอร์ตเงินสดต้นปี 2026 และใช้ข้อมูลก่อนหน้าเพื่อ warm-up ตัวชี้วัด ผลช่วง ม.ค.–มิ.ย. 2026 ไม่ใช่ holdout อิสระ</li>
<li>ผลตอบแทน YTD ยังสั้น ไม่รับประกันผลในอนาคต; turnover สูง และยังไม่มีแบบจำลอง market impact, สภาพคล่อง/คิวซื้อขายจริง หรือขั้นตอนจัดการคำสั่ง</li>
<li>ข้อมูล corporate actions ครอบคลุมไม่ครบทุกหุ้น; ราคาปิดอ้างอิงอาจต่างจากราคาเปิดจริง จึงต้องตรวจราคาและขนาดสถานะใหม่ก่อนตัดสินใจ</li>
</ul>
<div class="refresh-line" id="source">แหล่งข้อมูล EOD: SiamChart พร้อม Yahoo Finance สำรอง · เซิร์ฟเวอร์ดึงอัตโนมัติทุกวันจันทร์–ศุกร์หลัง 20:30 น. เวลาไทย</div>
<div class="refresh-line" id="refresh-state"></div>
</section>
<footer>บริการดึง SiamChart EOD พร้อม Yahoo Finance สำรองทุกวันจันทร์–ศุกร์ · ไม่มีการเชื่อมต่อหรือส่งคำสั่งไปยังโบรกเกอร์</footer>
</main>
<script>
const nativeFetch=window.fetch.bind(window);
window.fetch=async(...args)=>{const response=await nativeFetch(...args);if(response.status===401){const next=encodeURIComponent(location.pathname+location.hash);location.replace('/login?error=expired&next='+next);}return response;};
const $ = (id) => document.getElementById(id);
const fmtMoney = (v) => Number.isFinite(Number(v)) ? '฿' + Number(v).toLocaleString('th-TH',{maximumFractionDigits:0}) : '—';
const fmtPct = (v) => Number.isFinite(Number(v)) ? (Number(v) >= 0 ? '+' : '') + (Number(v)*100).toFixed(1) + '%' : '—';
const dateThai = (value) => value ? new Date(value + 'T00:00:00').toLocaleDateString('th-TH',{day:'numeric',month:'short',year:'numeric'}) : '—';
function drawCurve(rows) {
const svg=$('curve'); svg.replaceChildren();
if (!rows || rows.length < 2) {
const t=document.createElementNS('http://www.w3.org/2000/svg','text'); t.setAttribute('x','400');t.setAttribute('y','116');t.setAttribute('text-anchor','middle');t.setAttribute('fill','#91a2b3');t.textContent='ยังไม่มีข้อมูลกราฟ';svg.append(t);return;
}
const W=800,H=225,L=52,R=12,T=12,B=24,vals=rows.map(x=>x.equity),lo=Math.min(...vals),hi=Math.max(...vals),span=hi-lo||1;
for(let i=0;i<4;i++) { const y=T+(H-T-B)*i/3; const line=document.createElementNS(svg.namespaceURI,'line');line.setAttribute('x1',L);line.setAttribute('x2',W-R);line.setAttribute('y1',y);line.setAttribute('y2',y);line.setAttribute('stroke','#263645');line.setAttribute('stroke-dasharray','3 5');svg.append(line);const txt=document.createElementNS(svg.namespaceURI,'text');txt.setAttribute('x','0');txt.setAttribute('y',y+4);txt.setAttribute('fill','#91a2b3');txt.setAttribute('font-size','11');txt.textContent=((hi-(hi-lo)*i/3)/1000000).toFixed(2)+'m';svg.append(txt); }
const points=rows.map((p,i)=>`${L+(W-L-R)*i/(rows.length-1)},${T+(H-T-B)*(1-(p.equity-lo)/span)}`).join(' ');
const area=document.createElementNS(svg.namespaceURI,'polygon');area.setAttribute('points',`${L},${H-B} ${points} ${W-R},${H-B}`);area.setAttribute('fill','#62d6a11a');svg.append(area);
const poly=document.createElementNS(svg.namespaceURI,'polyline');poly.setAttribute('points',points);poly.setAttribute('fill','none');poly.setAttribute('stroke','#62d6a1');poly.setAttribute('stroke-width','3');poly.setAttribute('stroke-linejoin','round');poly.setAttribute('stroke-linecap','round');svg.append(poly);
$('chart-start').textContent=dateThai(rows[0].date);$('chart-end').textContent=dateThai(rows[rows.length-1].date);
}
function renderSignals(rows) {
const body=$('signals');body.replaceChildren();
if(!rows || !rows.length) {const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=7;td.className='empty';td.textContent='ไม่มีหุ้นที่ผ่านสัญญาณ ณ ราคาปิดล่าสุด';tr.append(td);body.append(tr);return;}
for(const row of rows) {const tr=document.createElement('tr');const cols=[row.symbol,row.source_model,fmtPct(row.target_weight),fmtMoney(row.reference_close),Number(row.indicative_shares_at_last_close||0).toLocaleString('th-TH'),dateThai(row.last_bar_date),Number(row.stale_sessions||0).toLocaleString('th-TH')];cols.forEach((value,i)=>{const td=document.createElement('td');td.textContent=value;if(i===0)td.className='symbol';if(i>=2&&i<=4)td.classList.add('num');if(i===1)td.className='source';if(i===6){td.classList.add('num');if(Number(row.stale_sessions||0)>0)td.classList.add('negative');}tr.append(td);});body.append(tr);}
}
async function load() {
try {
const response=await fetch('/api/dashboard',{cache:'no-store'}),d=await response.json();
if(!d.ready){$('updated').textContent='ยังไม่มีผลคำนวณ กดปุ่ม “ดึงข้อมูลล่าสุดและคำนวณสัญญาณ”';$('refresh-state').textContent=d.refresh?.error||(d.refresh?.running?'กำลังตรวจไฟล์ EOD และคำนวณ backtest':'');$('refresh').disabled=Boolean(d.refresh?.running);$('refresh').textContent=d.refresh?.running?'กำลังคำนวณ…':'ดึงข้อมูลล่าสุดและคำนวณสัญญาณ';return;}
const b=d.base,s=d.stress;
$('capital').textContent='เงินตั้งต้น '+fmtMoney(d.initial_equity);
$('asof').textContent=dateThai(d.data_as_of || b.end);$('return').textContent=fmtPct(b.total_return);$('return').className='value '+(b.total_return>=0?'positive':'negative');$('ending').textContent='ปลายงวด '+fmtMoney(b.final_equity ?? d.initial_equity*(1+b.total_return));$('drawdown').textContent=fmtPct(b.max_drawdown);$('drawdown').className='value negative';$('stress').textContent=fmtPct(s?.total_return);$('stress').className='value '+((s?.total_return||0)>=0?'positive':'negative');
$('sessions').textContent=Number(b.sessions||0).toLocaleString('th-TH');$('end-value').textContent=fmtMoney(b.final_equity ?? d.initial_equity*(1+b.total_return));$('costs').textContent=fmtMoney(b.transaction_costs_full_run);$('fills').textContent=Number(b.fills_full_run||0).toLocaleString('th-TH');$('turnover').textContent=(Number(b.turnover_pct_initial||0)/100).toFixed(1)+' เท่า';$('positions').textContent=Number(b.average_positions||0).toFixed(1)+' หุ้น';$('exposure').textContent=fmtPct(b.average_exposure);
$('updated').textContent=`ทดสอบ ${dateThai(b.start)} ถึง ${dateThai(b.end)} · โมเดล ${d.model}`;
$('source').textContent='แหล่งข้อมูล EOD: '+(d.price_history?.latest_source||'กำลังตรวจแหล่งข้อมูล')+' · '+dateThai(d.data_as_of||b.end);
const status=[d.download_status];
if(d.price_history?.sessions)status.push('เก็บข้อมูล '+Number(d.price_history.sessions).toLocaleString('th-TH')+' วัน · ล่าสุดจาก '+(d.price_history.latest_source||'แหล่งเดิม'));
if(d.automatic_download?.attempted_at)status.push('ตรวจ SiamChart ล่าสุด '+new Date(d.automatic_download.attempted_at).toLocaleString('th-TH'));
if(d.backup_download?.attempted_at)status.push('ตรวจ Yahoo Finance สำรองล่าสุด '+new Date(d.backup_download.attempted_at).toLocaleString('th-TH'));
if(d.archive_issues?.length)status.push('ข้อสังเกตจากไฟล์ข้อมูล: '+d.archive_issues.join(' · '));
$('refresh-state').textContent=status.filter(Boolean).join(' · ');
drawCurve(d.equity_curve);renderSignals(d.signals);
if(d.refresh?.running){$('refresh').disabled=true;$('refresh').textContent='กำลังคำนวณ…';$('refresh-state').textContent='กำลังตรวจไฟล์ EOD และคำนวณ backtest';}
else {$('refresh').disabled=false;$('refresh').textContent='ดึงข้อมูลล่าสุดและคำนวณสัญญาณ';if(d.refresh?.error)$('refresh-state').textContent='คำนวณไม่สำเร็จ: '+d.refresh.error;}
} catch(err) {$('updated').textContent='ติดต่อบริการภายในเครื่องไม่ได้: '+err.message;}
}
$('refresh').addEventListener('click',async()=>{ $('refresh').disabled=true;$('refresh').textContent='กำลังเริ่ม…';try{const r=await fetch('/api/refresh',{method:'POST'});const j=await r.json();$('refresh-state').textContent=j.message;await load();if(j.started){const timer=setInterval(async()=>{await load();const x=await (await fetch('/api/dashboard')).json();if(!x.refresh?.running){clearInterval(timer);load();}},5000);}}catch(e){$('refresh-state').textContent=e.message;$('refresh').disabled=false;}});
load();
</script>
</body>
</html>