115 lines
36 KiB
HTML
115 lines
36 KiB
HTML
<!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>
|