Files
2026-11-04 09:49:21 +07:00

115 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="th">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>PrimeThai · MT5 Desk</title>
<style>
:root{color-scheme:dark;--bg:#0b1018;--panel:#131c28;--line:#253348;--ink:#edf3fc;--muted:#9aaac0;--green:#70dfbb;--red:#ff8f9d;--amber:#f7cc7e}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.65 system-ui,"Segoe UI",sans-serif}main{max-width:1450px;margin:auto;padding:30px}header{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:20px}.eyebrow{color:var(--green);font-size:12px;letter-spacing:2px}h1{font-size:28px;letter-spacing:-1px;margin:3px 0}h2{font-size:18px;margin:0 0 8px}p{margin:6px 0}.muted,.small{color:var(--muted)}.small{font-size:12px}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}button,a.button{border:1px solid var(--line);background:#1b293a;color:var(--ink);border-radius:8px;padding:9px 13px;cursor:pointer;font:inherit;text-decoration:none}button:hover{border-color:var(--green)}button:disabled{opacity:.45;cursor:default}.primary{background:#2c745f;color:#fff;border-color:#48937a}.danger{background:#603040;color:#fff}.mini{font-size:12px;padding:5px 8px}.badge{font-size:12px;border:1px solid var(--line);border-radius:30px;padding:5px 10px;color:var(--muted)}.positive{color:var(--green)}.negative{color:var(--red)}.warning{color:var(--amber)}
.banner{background:#152235;border:1px solid #2a4464;border-radius:10px;padding:14px 18px;margin:14px 0}.banner.warning{background:#302719;border-color:#69502b}.banner.error{background:#311d29;border-color:#743c4c;color:var(--red)}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:22px 0}.metric,.panel{background:var(--panel);border:1px solid var(--line);border-radius:12px}.metric{padding:18px}.metric strong{display:block;font-size:27px;margin:8px 0 2px;font-variant-numeric:tabular-nums}.label{color:var(--muted);font-size:13px}.panel{padding:20px;margin:16px 0}.panel-head{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:16px}nav{display:flex;gap:8px;border-bottom:1px solid var(--line);padding-bottom:12px;overflow:auto}nav button{white-space:nowrap;background:transparent}nav button.active{background:#203e3a;border-color:#468c75;color:var(--green)}[hidden]{display:none!important}.scroll{overflow:auto}table{width:100%;border-collapse:collapse;white-space:nowrap;font-size:13px}th,td{padding:12px 10px;border-bottom:1px solid var(--line);text-align:left}th{font-size:12px;color:var(--muted);font-weight:500}td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}.symbol{font-weight:700;color:#fff}.empty{text-align:center;color:var(--muted);padding:38px}.two{display:grid;grid-template-columns:1fr 1fr;gap:16px}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}label{color:var(--muted);font-size:13px;display:block}input{display:block;width:100%;border:1px solid var(--line);background:#0b1420;color:#fff;padding:10px;border-radius:7px;font:inherit;margin:5px 0 12px}dialog{background:var(--panel);color:var(--ink);border:1px solid #54738e;border-radius:14px;padding:25px;max-width:620px;width:calc(100% - 28px)}dialog::backdrop{background:#000a}.toast{position:fixed;right:22px;bottom:22px;background:#263d38;border:1px solid var(--green);padding:14px 20px;border-radius:10px;max-width:550px;z-index:10}.toast.error{background:#472534;border-color:var(--red)}progress{height:5px;width:100%;accent-color:var(--green)}.chart{height:220px;width:100%;display:block}details{margin:10px 0}summary{cursor:pointer;color:var(--muted)}footer{margin:24px 0;font-size:12px;color:var(--muted)}
@media(max-width:850px){main{padding:18px}header,.panel-head{align-items:flex-start;flex-direction:column}.metrics{grid-template-columns:1fr 1fr}.two{grid-template-columns:1fr}.grid{grid-template-columns:1fr}.metric strong{font-size:22px}h1{font-size:24px}}
</style>
</head>
<body><main>
<header><div><div class="eyebrow">PRIMETHAI / LOCAL MT5 DESK</div><h1>ดูแลพอร์ตจากบัญชีจริง</h1><div class="small" id="connection">กำลังอ่านข้อมูล MT5…</div></div><div class="actions"><span class="badge" id="live-state">อ่านพอร์ต</span><button id="trading-mode">เปิดการส่งจริง</button><button id="sync">อัปเดตพอร์ต</button><button id="research-refresh">คำนวณสัญญาณใหม่</button></div></header>
<div class="banner" id="job"><span id="job-message">กำลังเริ่มแอป</span><progress id="job-progress" hidden></progress></div>
<p class="small" id="runtime-state"></p>
<article class="panel"><div class="panel-head"><div><h2 id="auto-title">สถานะตลาดและรอบส่งคำสั่ง</h2><p id="market-status">กำลังอ่านเวลาตลาด…</p><p id="auto-status" class="warning"></p></div><button id="pause-auto" class="danger">หยุดส่งอัตโนมัติ</button></div><p class="small" id="auto-details"></p><p class="small" id="auto-cycle"></p><p class="small" id="feed-status"></p><div class="banner warning" id="execution-warning" hidden></div></article>
<div class="banner warning" id="warnings" hidden></div>
<section class="metrics">
<article class="metric"><div class="label">Equity ที่โบรกเกอร์รายงาน</div><strong id="equity">—</strong><div class="small" id="equity-note">จากบัญชี MT5</div></article>
<article class="metric"><div class="label">เงินว่างสำหรับแผนซื้อ</div><strong id="cash">—</strong><div class="small">หักคำสั่งซื้อค้างและเผื่อค่าธรรมเนียมในแผน</div></article>
<article class="metric"><div class="label">กำไร / ขาดทุนค้าง</div><strong id="profit">—</strong><div class="small">ตาม MT5 · ยังไม่รวมค่าธรรมเนียมซื้อขาย</div></article>
<article class="metric"><div class="label">หุ้นที่ถือ / คำสั่งค้าง</div><strong id="counts">—</strong><div class="small" id="asof">กำลังตรวจวันสัญญาณ</div></article>
</section>
<nav><button data-page="signals" class="active">สัญญาณและแผนวันนี้</button><button data-page="portfolio">พอร์ตและคำสั่งจริง</button><button data-page="compare">เทียบ MT5 กับระบบเดิม</button><button data-page="backtest">ผล Backtest</button><button data-page="history">ประวัติและสถานะ</button></nav>
<section data-content="signals">
<article class="panel"><div class="panel-head"><div><h2>Donchian 55/20 + EMA 50/200</h2><p class="small">ซื้อเมื่อโมเดลใดโมเดลหนึ่งเลือกหุ้น · ขนาดขั้นต่ำ 100 หุ้น · ต้นทุนและจำนวนที่ถืออ่านจาก MT5</p></div><span class="badge" id="signal-date">กำลังอ่านข้อมูล</span></div>
<div class="scroll"><table><thead><tr><th>หุ้น / ลำดับ</th><th>โมเดล</th><th class="num">ถืออยู่</th><th class="num">ราคาสัญญาณ</th><th class="num">เสนอขายล่าสุด</th><th class="num">ต่างจากสัญญาณ</th><th class="num">จำนวนในแผน</th><th>การจัดสรร</th><th>จัดการ</th></tr></thead><tbody id="signals-table"></tbody></table></div>
<p class="small" style="margin-top:14px" id="plan-note">การข้ามไม่เปลี่ยนเงินสดหรือหุ้นจริง ระบบจะนำงบในแผนไปพิจารณาตัวถัดไป และคืนรายการเมื่อมีสัญญาณวันใหม่</p></article>
</section>
<section data-content="portfolio" hidden>
<article class="panel"><div class="panel-head"><div><h2>หุ้นที่ถือจริง</h2><p class="small">ต้นทุนและผลซื้อขายดึงจากโบรกเกอร์ ไม่สร้างรายการซื้อประมาณการเพิ่ม</p></div></div><div class="scroll"><table><thead><tr><th>หุ้น</th><th class="num">จำนวน</th><th class="num">ต้นทุนต่อหุ้น</th><th class="num">ราคาประเมิน</th><th class="num">กำไรค้าง</th><th>ที่มาของรายการ</th></tr></thead><tbody id="holdings-table"></tbody></table></div></article>
<article class="panel"><h2>คำสั่งที่ยังรอจับคู่</h2><p class="small">แสดงทุกคำสั่งในบัญชี ยกเลิกได้เฉพาะคำสั่งที่แอปนี้ส่ง</p><div class="scroll"><table><thead><tr><th>หุ้น</th><th>ประเภท</th><th class="num">เหลือ</th><th class="num">ราคา</th><th>เจ้าของคำสั่ง</th><th>จัดการ</th></tr></thead><tbody id="orders-table"></tbody></table></div></article>
</section>
<section data-content="compare" hidden>
<article class="panel"><div class="panel-head"><div><h2>สัญญาณจากสองแหล่งข้อมูลตรงกันหรือไม่</h2><p class="small">ใช้วันสิ้นสุดเดียวกัน ช่วง warm-up เดียวกัน และกฎกลยุทธ์ชุดเดียวกัน</p></div><span class="badge" id="compare-date">รอการเปรียบเทียบ</span></div><p id="compare-summary">กำลังจัดเตรียมข้อมูล…</p><div class="scroll"><table><thead><tr><th>ข้อมูลที่เทียบ</th><th class="num">จำนวนแท่งที่เทียบ</th><th class="num">ตรงกัน</th><th class="num">แตกต่าง</th><th class="num">ตรงกัน (%)</th></tr></thead><tbody id="data-compare-table"></tbody></table></div><p class="small" id="compare-coverage"></p></article>
<article class="panel"><h2>รายชื่อ โมเดล และลำดับจัดสรรงบ</h2><div class="scroll"><table><thead><tr><th>หุ้น</th><th>MT5</th><th>SiamChart / ระบบเดิม</th><th>โมเดล</th><th class="num">ลำดับ MT5</th><th class="num">ลำดับเดิม</th><th class="num">คะแนน MT5</th><th class="num">คะแนนเดิม</th></tr></thead><tbody id="signal-compare-table"></tbody></table></div><button class="mini" id="download-compare" style="margin-top:14px">ดาวน์โหลดรายงานเปรียบเทียบ</button><details><summary>ตัวอย่างราคาปิดที่แตกต่าง</summary><div class="scroll"><table><thead><tr><th>วัน</th><th>หุ้น</th><th class="num">MT5</th><th class="num">SiamChart</th></tr></thead><tbody id="difference-table"></tbody></table></div></details></article>
</section>
<section data-content="backtest" hidden>
<article class="panel"><h2>Backtest บนช่วงวันที่เดียวกัน</h2><p class="small">ทุนเริ่มต้น 10,000 บาท · ขั้นต่ำ 100 หุ้น · ใช้ commission และ slippage สมมติฐานเดิม · ผลจำลองไม่ใช่กำไรในบัญชีจริง</p><div class="scroll"><table><thead><tr><th>แหล่งข้อมูล</th><th>ช่วงเวลา</th><th class="num">ผลตอบแทน</th><th class="num">ขาดทุนสูงสุด</th><th class="num">จำนวน fills</th></tr></thead><tbody id="backtest-table"></tbody></table></div><svg id="equity-chart" class="chart" role="img" aria-label="เปรียบเทียบมูลค่าพอร์ตจำลอง"></svg><p class="small">เส้นสีเขียว = MT5 · เส้นสีทอง = SiamChart · ใช้เฉพาะช่วงวันร่วมกัน</p></article>
<article class="panel"><h2>Backtest จาก MT5 ถึงวันล่าสุด</h2><div id="latest-backtest"></div></article>
</section>
<section data-content="history" hidden>
<article class="panel"><h2>รอบส่งคำสั่งอัตโนมัติ</h2><p class="small">ส่งได้หนึ่งครั้งต่อหุ้นต่อวัน แม้เริ่มแอปใหม่ · คำสั่งที่ไม่ทราบผลหรือส่งไม่ผ่านต้องตรวจสอบก่อน · Limit มีผลภายในวัน</p><div class="scroll"><table><thead><tr><th>วัน</th><th>หุ้น</th><th>รายการ</th><th>สถานะ</th><th>รายละเอียด</th></tr></thead><tbody id="executions-table"></tbody></table></div></article>
<article class="panel"><h2>ประวัติซื้อขายจริงล่าสุด</h2><div class="scroll"><table><thead><tr><th>เวลาไทย</th><th>หุ้น</th><th>รายการ</th><th class="num">จำนวน</th><th class="num">ราคาจริง</th><th class="num">ค่าธรรมเนียม</th><th class="num">กำไรที่รับรู้</th></tr></thead><tbody id="deals-table"></tbody></table></div></article>
<article class="panel"><h2>คำสั่งที่ตรวจหรือส่งจากแอปนี้</h2><p class="small">หากขึ้น unknown หรือ sending ค้าง ให้ตรวจบัญชี MT5 ก่อน แอปจะไม่ส่งคำสั่งเดิมซ้ำอัตโนมัติ</p><div class="scroll"><table><thead><tr><th>เวลา</th><th>หุ้น</th><th>สถานะ</th><th>รายละเอียด</th></tr></thead><tbody id="intents-table"></tbody></table></div></article>
<article class="panel"><h2>ข้อมูลที่เก็บในเครื่อง</h2><p id="history-state">กำลังตรวจประวัติราคา…</p><p class="small">ราคาหลักเป็น MT5 D1 และ real volume · ดัชนี SET มาจาก MT5 · ปันผลและแตกหุ้นอ้างอิงข้อมูลเดิมที่มีอยู่ · ข้อมูลส่วนตัวเก็บแยกในแอป MT5</p><details><summary>ข้อมูลที่ยังดึงไม่ได้</summary><pre id="history-errors" style="white-space:pre-wrap;font-size:12px"></pre></details></article>
</section>
<footer>แอป MT5 แยกจากระบบเดิม · สัญญาณใช้ราคาปิดรายวัน · รอบอัตโนมัติส่งจริงตามแผนที่อนุญาตในช่วงเวลาตลาด · ต้องเปิดเครื่อง แอป และ MT5 ค้างไว้ · ปุ่มหยุดไม่ยกเลิกคำสั่งที่โบรกเกอร์รับไปแล้ว</footer>
</main>
<dialog id="order-dialog"><h2 id="order-title">ตรวจคำสั่ง</h2><p class="small">ส่งเป็น Limit มีผลภายในวัน ตามราคาและจำนวนที่ยืนยัน ผลจับคู่จริงจะอ่านกลับจาก MT5</p><div class="grid"><label>หุ้น<input id="order-symbol" readonly></label><label>จำนวนหุ้น<input id="order-quantity" type="number" min="100" step="100"></label><label>ราคา Limit<input id="order-price" type="number" min="0.01" step="0.01"></label></div><div class="banner" id="preview-result" hidden></div><div class="actions"><button id="preview-order">ตรวจคำสั่งกับโบรกเกอร์</button><button class="danger" id="send-order" hidden>ยืนยันส่งคำสั่งจริง</button><button id="close-dialog">ปิด</button></div><p class="small" id="live-help"></p></dialog>
<div id="toast" class="toast" hidden></div>
<script>
const TOKEN='__APP_TOKEN__';const $=id=>document.getElementById(id);let state=null,loading=false,preview=null,currentSide=null,toastTimer;
const money=v=>v==null?'—':Number(v).toLocaleString('th-TH',{minimumFractionDigits:2,maximumFractionDigits:2});
const num=v=>Number(v||0).toLocaleString('th-TH');const pct=v=>v==null?'—':(v>=0?'+':'')+Number(v).toFixed(2)+'%';
const timestamp=v=>v?new Date(typeof v==='number'?v*1000:v).toLocaleString('th-TH',{timeZone:'Asia/Bangkok'}):'—';
const labels={BUY:'ซื้อเพิ่ม',SELL:'พิจารณาขาย',HOLD:'ถือ',BUDGET:'งบไม่พอ',SKIPPED:'ข้ามรอบนี้',PENDING:'รอคำสั่งเดิม',WAIT:'รอข้อมูล'};
function toast(message,error=false){clearTimeout(toastTimer);$('toast').textContent=message;$('toast').className='toast'+(error?' error':'');$('toast').hidden=false;toastTimer=setTimeout(()=>$('toast').hidden=true,10000);}
async function api(path,body){const r=await fetch('/api/'+path,{method:body===undefined?'GET':'POST',headers:{'X-App-Token':TOKEN,'Content-Type':'application/json'},...(body===undefined?{}:{body:JSON.stringify(body)})});const result=await r.json();if(!r.ok)throw new Error(result.error||'คำขอไม่สำเร็จ');return result;}
function button(text,fn,cls='mini'){const b=document.createElement('button');b.textContent=text;b.className=cls;b.onclick=async()=>{b.disabled=true;try{await fn();}catch(e){toast(e.message,true);}finally{b.disabled=false;}};return b;}
function cell(text,cls=''){const td=document.createElement('td');td.textContent=text==null?'—':String(text);td.className=cls;return td;}
function table(id,rows,columns){const body=$(id);body.replaceChildren();if(!rows?.length){const tr=document.createElement('tr');const td=cell('ยังไม่มีรายการ','empty');td.colSpan=columns.length;tr.append(td);body.append(tr);return;}for(const row of rows){const tr=document.createElement('tr');for(const col of columns){const value=col(row);if(value instanceof Node)tr.append(value);else tr.append(cell(value));}body.append(tr);}}
const n=v=>cell(money(v),'num');const p=v=>cell(pct(v),'num '+(v>=0?'positive':'negative'));
function chart(left,right){const svg=$('equity-chart');svg.replaceChildren();const all=[...(left||[]),...(right||[])];if(all.length<2)return;const days=[...new Set(all.map(r=>r.day))].sort();const lo=Math.min(...all.map(r=>r.equity)),hi=Math.max(...all.map(r=>r.equity));svg.setAttribute('viewBox','0 0 1000 220');for(const [series,color] of [[left,'#70dfbb'],[right,'#f7cc7e']]){if(!series?.length)continue;const line=document.createElementNS('http://www.w3.org/2000/svg','polyline');line.setAttribute('points',series.map(r=>`${25+950*days.indexOf(r.day)/Math.max(1,days.length-1)},${195-170*(r.equity-lo)/Math.max(1,hi-lo)}`).join(' '));line.setAttribute('fill','none');line.setAttribute('stroke',color);line.setAttribute('stroke-width','2');svg.append(line);}}
function render(d){state=d;const s=d.snapshot||{},a=s.account||{},r=d.research||{},c=r.comparison||{},job=d.job||{};
const paused=(!r.data_quality?.calendar_source)||(job.running&&['history','archive','signals','comparison'].includes(job.stage));const planRows=(d.plan?.rows||[]).map(x=>paused?{...x,action:'WAIT',quantity:0,reason:'รอคำนวณและตรวจข้อมูลให้เสร็จก่อน'}:x);$('sync').disabled=!!job.running;$('research-refresh').disabled=!!job.running;$('trading-mode').disabled=paused&&!d.live_enabled;
$('connection').textContent=(a.server||'ยังไม่เชื่อมต่อ')+' · '+(a.login_masked||'')+' · อัปเดต '+timestamp(s.updated_at);
const runtime=d.runtime||{},supervisor=d.supervisor||{};
$('runtime-state').textContent=(runtime.supervised?(runtime.launch_mode==='terminal'?'Terminal ดูแลการรันแอป · เปิดหน้าต่างนี้ค้างไว้':'Windows ดูแลการรันแอปและเปิดกลับเมื่อหยุด'):'เปิดแอปโดยตรง · ไม่มีตัวเปิดกลับ')+' · เริ่ม '+timestamp(runtime.started_at)+' · ตรวจการทำงานล่าสุด '+timestamp(runtime.heartbeat_at)+(supervisor.restarts?' · เปิดกลับแล้ว '+supervisor.restarts+' ครั้ง':'');
$('live-state').textContent=d.live_enabled?'เปิดการส่งคำสั่งจริง':'โหมดอ่านพอร์ตและตรวจคำสั่ง';$('live-state').className='badge '+(d.live_enabled?'warning':'positive');
const auto=d.automation||{},market=auto.market||{};
$('market-status').textContent=(market.message||'รอข้อมูลตลาด')+' · เวลาไทย '+timestamp(market.now);
$('auto-status').textContent=auto.message||'ยังไม่ตั้งรอบอัตโนมัติ';
$('auto-details').textContent=`เริ่ม ${auto.start_date||'ยังไม่กำหนด'}${auto.end_date?' ถึง '+auto.end_date:' · ทุกวันทำการ'} · ส่งเฉพาะ ${market.order_windows||'—'} น. · ตรวจรอบทุก ${auto.interval_seconds||15} วินาที · อ่านพอร์ตทุก ${auto.portfolio_seconds||60} วินาที · คำนวณราคาปิดหลัง ${auto.eod_time||'20:30'} น. · ปฏิทินยืนยันปี ${(market.calendar_years||[]).join(', ')} (ปีที่ไม่มีปฏิทินจะหยุดส่ง)`;
$('auto-cycle').textContent=(auto.last_cycle?'ตรวจแผนล่าสุด '+timestamp(auto.last_cycle.at)+' · '+auto.last_cycle.message:'ยังไม่ถึงรอบส่งอัตโนมัติ')+' · วิเคราะห์สัญญาณล่าสุด '+timestamp(r.updated_at);
const benchmark=s.quotes?.SET;
$('feed-status').textContent='ราคา SET ล่าสุด '+timestamp(benchmark?.time)+' · ฐานเวลา server เทียบ UTC '+(d.server_utc_offset_minutes||0)+' นาที · การเปิดส่งจริงยังต้องผ่านการตรวจราคา บัญชี เงินว่าง และโบรกเกอร์';
const blocked=auto.last_cycle?.status==='blocked';$('execution-warning').hidden=!blocked;
if(blocked)$('execution-warning').textContent='รอบส่งล่าสุดถูกกั้น ('+timestamp(auto.last_cycle.at)+'): '+auto.last_cycle.message+' · การอัปเดตข้อมูลสำเร็จไม่ได้หมายความว่าส่งคำสั่งแล้ว';
$('pause-auto').textContent=auto.paused?'เปิดรอบอัตโนมัติต่อ':'หยุดส่งอัตโนมัติ';$('pause-auto').disabled=!auto.enabled&&!auto.paused;
$('trading-mode').textContent=d.live_enabled?'ปิดการส่งจริง':'เปิดการส่งจริง';
$('job-message').textContent=job.message||'รออ่านพอร์ต';$('job').className='banner'+(job.stage==='failed'?' error':'');$('job-progress').hidden=!job.running;if(job.running){$('job-progress').max=job.total||1;$('job-progress').value=job.done||0;}
const warnings=[...(s.warnings||[])];if(d.connection_error)warnings.push('อ่าน MT5 ล่าสุดไม่สำเร็จ: '+d.connection_error.message+' · ข้อมูลที่เห็นเป็นข้อมูลก่อนหน้า');if(d.history?.failures?.length)warnings.push('ยังดึงประวัติไม่ได้ '+d.history.failures.length+' หุ้น ดูรายละเอียดในประวัติและสถานะ');if(c.as_of&&r.signal_as_of!==c.as_of)warnings.push('สัญญาณ MT5 ถึง '+r.signal_as_of+'; ข้อมูลเดิมเทียบได้ถึง '+c.as_of+' จึงแสดงผลเทียบเฉพาะวันร่วมกัน');$('warnings').hidden=!warnings.length;$('warnings').textContent=warnings.join(' · ');
$('equity').textContent=money(a.equity);$('equity-note').textContent=Math.abs(a.equity_difference||0)>.02?'ยอดต่างจากรวมราคาหุ้น '+money(a.equity_difference)+' บาท':'ตรงกับยอดเงินสดและมูลค่าหุ้น';$('cash').textContent=money(d.plan?.cash_budget);$('profit').textContent=money(a.profit);$('profit').className=a.profit>=0?'positive':'negative';$('counts').textContent=num(s.positions?.length)+' / '+num(s.orders?.length);$('asof').textContent='สัญญาณจากราคาปิด '+(r.signal_as_of||'กำลังคำนวณ');$('signal-date').textContent=r.signal_as_of||'รอประวัติราคา';
table('signals-table',planRows,[x=>cell(x.symbol+(x.rank?' · #'+x.rank:''),'symbol'),x=>x.source_model?.replaceAll('donchian_55_20','Donchian').replaceAll('ema50_200_cross','EMA')||'นอกเป้าหมาย',x=>cell(num(x.held),'num'),x=>n(x.reference_close),x=>{const td=n(x.ask);td.title='ราคาเสนอ ณ '+timestamp(x.quote_time);return td;},x=>cell(pct(x.premium_pct),'num '+(x.premium_pct>state.max_premium_pct?'warning':'muted')),x=>cell(num(x.quantity),'num'),x=>{const td=cell(labels[x.action]||x.action,x.action==='BUY'?'positive':x.action==='SELL'?'warning':'muted');td.title=x.reason||'';return td;},x=>{const td=document.createElement('td');const group=document.createElement('div');group.className='actions';if(['BUY','SELL'].includes(x.action))group.append(button(x.action==='BUY'?'ซื้อเพิ่ม':'ตรวจขาย',()=>openOrder(x),x.action==='BUY'?'mini primary':'mini'));if(!paused&&x.source_model&&x.action!=='PENDING')group.append(button(x.action==='SKIPPED'?'กลับมาพิจารณา':'ซื้อไม่ทัน',async()=>{await api('skip',{symbol:x.symbol,skip:x.action!=='SKIPPED'});await load();}));td.append(group);return td;}]);
table('holdings-table',s.positions,[x=>cell(x.symbol,'symbol'),x=>cell(num(x.volume),'num'),x=>n(x.price_open),x=>n(x.price_current),x=>cell(money(x.profit),'num '+(x.profit>=0?'positive':'negative')),x=>x.magic===d.app_magic?'แอป MT5 นี้':x.magic?'โปรแกรมอื่น':'รายการเดิม / ทำเอง']);
table('orders-table',s.orders,[x=>x.symbol,x=>({2:'Buy Limit',3:'Sell Limit',4:'Buy Stop',5:'Sell Stop'})[x.type]||x.type,x=>num(x.volume_current),x=>money(x.price_open),x=>x.magic===d.app_magic?'แอปนี้':'ภายนอกแอป',x=>{const td=document.createElement('td');if(x.magic===d.app_magic&&d.live_enabled)td.append(button('ยกเลิก',async()=>{if(!confirm(`ยกเลิกคำสั่ง ${x.symbol} #${x.ticket} จริงหรือไม่?`))return;await api('cancel',{ticket:x.ticket,confirm:'CANCEL_LIVE'});toast('ส่งคำขอยกเลิกแล้ว ตรวจสถานะจาก MT5');await load();}));else td.textContent='—';return td;}]);
$('compare-date').textContent=c.as_of||'กำลังเปรียบเทียบ';$('compare-summary').textContent=c.error||(!c.as_of?'รอประวัติ MT5 ครบก่อนคำนวณ':(c.same_signal_membership_and_model?'รายชื่อหุ้นและโมเดลตรงกัน':'พบความต่างของรายชื่อหุ้นหรือโมเดล')+` · MT5 ${c.mt5_count} ตัว / เดิม ${c.siamchart_count} ตัว · หุ้นร่วมกัน ${c.common_signal_count} ตัว · ลำดับคะแนน${c.same_rankings?'ตรงกัน':'ต่างกัน'}`);
table('data-compare-table',Object.entries(c.ohlcv||{}).map(([field,x])=>({field,...x})),[x=>x.field,x=>num(x.compared),x=>num(x.matched),x=>num(x.different),x=>money(x.match_pct)]);
$('compare-coverage').textContent=c.as_of?`${num(c.shared_sessions)} วันร่วมกัน · ${num(c.shared_symbols)} หุ้นร่วมกัน · SET ต่างกัน ${num(c.set_different)} วัน · ไม่มีใน MT5 ${num(c.missing_from_mt5?.length)} หุ้น · แท่งขาดจาก MT5 ${num(c.bars_only_siamchart)} แท่ง · เป้าหมายย้อนหลังต่างกัน ${num(c.historical_target_days_different)} จาก ${num(c.historical_target_days_compared)} วัน`:'';
table('signal-compare-table',c.signals,[x=>x.symbol,x=>x.mt5||'ไม่เลือก',x=>x.siamchart||'ไม่เลือก',x=>cell(x.same?'ตรงกัน':'ต่างกัน',x.same?'positive':'warning'),x=>x.mt5_rank||'—',x=>x.siamchart_rank||'—',x=>n(x.mt5_allocation_score),x=>n(x.siamchart_allocation_score)]);
table('difference-table',c.close_difference_samples,[x=>x.day,x=>x.symbol,x=>n(x.mt5),x=>n(x.siamchart)]);
const bs=[['MT5',c.backtest_mt5],['SiamChart',c.backtest_siamchart]].filter(x=>x[1]);table('backtest-table',bs,[x=>x[0],x=>x[1].metrics.start+' – '+x[1].metrics.end,x=>p(x[1].metrics.total_return*100),x=>p(x[1].metrics.max_drawdown*100),x=>num(x[1].metrics.fills_full_run)]);chart(c.backtest_mt5?.curve,c.backtest_siamchart?.curve);
const latest=r.backtest_mt5_latest?.metrics;$('latest-backtest').textContent=latest?`${latest.start} – ${latest.end} · ผลตอบแทน ${pct(latest.total_return*100)} · ขาดทุนสูงสุด ${pct(latest.max_drawdown*100)} · มูลค่าปลายงวด ${money(latest.ending_equity||latest.equity_end)} บาท`:'รอการคำนวณ';
const deals=(s.deals||[]).filter(x=>[0,1].includes(x.type)).slice(-40).reverse();table('deals-table',deals,[x=>timestamp(x.time),x=>x.symbol,x=>x.type===0?'ซื้อ':'ขาย',x=>num(x.volume),x=>n(x.price),x=>n(-(x.commission+x.fee)),x=>n(x.profit)]);
table('intents-table',d.intents,[x=>timestamp(x.created),x=>{try{return JSON.parse(x.request).symbol||'ยกเลิก';}catch{return '—';}},x=>x.status,x=>{try{const z=JSON.parse(x.result||'{}');return z.comment||z.error||'ตรวจคำสั่งแล้ว ยังไม่ส่ง';}catch{return '—';}}]);
table('executions-table',d.executions,[x=>x.day,x=>x.symbol,x=>x.side==='BUY'?'ซื้อ':'ขาย',x=>x.status,x=>{try{const z=JSON.parse(x.detail||'{}');return z.error||z.result?.comment||z.status||'จองรอบแล้ว';}catch{return '—';}}]);
const h=d.history?.bar_summary;$('history-state').textContent=h?`เก็บ ${num(h.rows)} แท่ง · ${num(h.symbols)} สัญลักษณ์ · ${h.first_day} ถึง ${h.last_day}`:'กำลังดาวน์โหลดประวัติครั้งแรก';$('history-errors').textContent=JSON.stringify(d.history?.failures||[],null,2);
}
async function load(){if(loading)return;loading=true;try{render(await api('state'));}catch(e){$('job-message').textContent='อ่านแอปไม่สำเร็จ: '+e.message;$('job').className='banner error';}finally{loading=false;}}
function openOrder(row){currentSide=row.action;preview=null;$('order-title').textContent=(currentSide==='BUY'?'ซื้อ ':'ขาย ')+row.symbol;$('order-symbol').value=row.symbol;$('order-quantity').value=row.quantity;$('order-price').value=row.limit_price||row.current||'';$('preview-result').hidden=true;$('send-order').hidden=true;$('live-help').textContent=state.live_enabled?'ทุกคำสั่งส่งเข้าบัญชีจริง โปรดตรวจจำนวนและราคา':'ขณะนี้ตรวจคำสั่งได้ แต่การส่งคำสั่งจริงยังปิดอยู่';$('order-dialog').showModal();}
for(const id of ['order-quantity','order-price'])$(id).oninput=()=>{preview=null;$('send-order').hidden=true;$('preview-result').hidden=true;};
$('preview-order').onclick=async()=>{try{preview=await api('preview',{symbol:$('order-symbol').value,side:currentSide,quantity:Number($('order-quantity').value),price:Number($('order-price').value)});$('preview-result').textContent=`ตรวจผ่าน: ${currentSide==='BUY'?'ซื้อ':'ขาย'} ${preview.request.symbol} ${num(preview.request.volume)} หุ้น ราคา ${money(preview.request.price)} · ยืนยันภายใน 60 วินาที`;$('preview-result').className='banner';$('preview-result').hidden=false;$('send-order').hidden=!preview.live_enabled;}catch(e){$('preview-result').textContent=e.message;$('preview-result').className='banner error';$('preview-result').hidden=false;}};
$('send-order').onclick=async()=>{if(!preview||!confirm('ยืนยันส่งคำสั่งนี้เข้าบัญชีจริงหรือไม่?'))return;$('send-order').disabled=true;try{const result=await api('submit',{id:preview.id,confirm:'SEND_LIVE'});toast(result.status==='accepted'?'โบรกเกอร์รับคำสั่งแล้ว รอผลจับคู่ในหน้าพอร์ต':'โบรกเกอร์ไม่รับคำสั่ง: '+(result.result?.comment||result.status),result.status!=='accepted');$('order-dialog').close();await load();}catch(e){toast(e.message,true);}finally{preview=null;$('send-order').disabled=false;$('send-order').hidden=true;}};
$('close-dialog').onclick=()=>$('order-dialog').close();
$('sync').onclick=async()=>{try{await api('refresh',{});await load();}catch(e){toast(e.message,true);}};
$('trading-mode').onclick=async()=>{const enable=!state?.live_enabled;const message=enable?`เปิดให้แอปส่งคำสั่งเข้าบัญชีจริง ${state?.snapshot?.account?.login_masked||''} หรือไม่?\n\nรอบอัตโนมัติต้องเปิดแยกที่สถานะตลาด`: 'ปิดการส่งคำสั่งใหม่และหยุดรอบอัตโนมัติหรือไม่? คำสั่งที่โบรกเกอร์รับไปแล้วยังรอจับคู่ตามเดิม';if(!confirm(message))return;try{await api('trading',{enabled:enable,confirm:enable?'ENABLE_LIVE':'DISABLE_LIVE'});await load();}catch(e){toast(e.message,true);}};
$('pause-auto').onclick=async()=>{const paused=!state?.automation?.paused;if(!paused&&!confirm('เปิดส่งคำสั่งจริงอัตโนมัติตามสัญญาณและเงินว่าง ทุกวันทำการตั้งแต่วันที่กำหนด โดยไม่ต้องยืนยันแต่ละรายการหรือไม่?'))return;try{await api('automation',{paused,confirm:paused?'PAUSE_AUTO':'ENABLE_AUTO'});await load();}catch(e){toast(e.message,true);}};
$('research-refresh').onclick=async()=>{try{await api('refresh',{research:true});await load();}catch(e){toast(e.message,true);}};
document.querySelectorAll('nav button').forEach(b=>b.onclick=()=>{document.querySelectorAll('nav button').forEach(x=>x.classList.toggle('active',x===b));document.querySelectorAll('[data-content]').forEach(x=>x.hidden=x.dataset.content!==b.dataset.page);});
$('download-compare').onclick=()=>{const blob=new Blob([JSON.stringify(state?.research?.comparison||{},null,2)],{type:'application/json'});const a=document.createElement('a');const url=URL.createObjectURL(blob);a.href=url;a.download='mt5-comparison.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
load();setInterval(load,5000);
</script></body></html>