From f703c026cc459e9344139970db72ffdaf21efd87 Mon Sep 17 00:00:00 2001 From: yellowdog Date: Fri, 12 Jun 2026 09:13:44 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=AD=A2=E7=9B=88/=E6=AD=A2=E6=8D=9F?= =?UTF-8?q?=E7=82=B9=E4=BD=8D=E5=B7=AE=E5=AF=B9=E7=A7=B0=E8=81=94=E5=8A=A8?= =?UTF-8?q?=20+=20=E8=BE=93=E5=85=A5=E6=A1=86=E8=AE=B0=E5=BF=86=E4=B8=8A?= =?UTF-8?q?=E6=AC=A1=E8=BE=93=E5=85=A5(localStorage)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 92 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 22 deletions(-) diff --git a/index.html b/index.html index a0a3e3d..1b65c68 100644 --- a/index.html +++ b/index.html @@ -183,6 +183,12 @@ +
+ +
+ +
+
@@ -247,8 +253,8 @@
- 说明:真正把亏损锁定在风险金额的是止损单,不是杠杆。止盈默认按盈亏比自动反算—— - 止盈点位差 = 止损点位差 × 盈亏比 R,按方向算出止盈价;也可切「按价格」手动输入。 + 说明:真正把亏损锁定在风险金额的是止损单,不是杠杆。止盈默认按盈亏比自动反算(止盈点位差 = 止损点位差 × R); + 开启点位差对称后,输入止盈或止损价,另一个会按相同点位差自动镜像(即 1:1)。所有输入会自动记住上次的值。 表中「状态」校验爆仓价是否在止损之外——安全表示会先触发止损、不会被强平;危险表示杠杆过高、爆仓在止损之前。 爆仓价为逐仓简化估算(爆仓距离≈1/杠杆,忽略维持保证金与手续费),实盘以交易所显示为准。这只是数学计算,不构成投资建议。
@@ -258,36 +264,44 @@ (function(){ const $=id=>document.getElementById(id); const levs=[3,5,10,20,25,50,75,100]; - let side='long', slMode='pct', tpMode='rr'; + const KEY='posCalc.v1'; + const FIELDS=['balance','riskPct','entry','slPct','slPrice','tp','tpRR']; + let side='long', slMode='pct', tpMode='rr', symOn=false, lastPrice='sl'; + + const setOn=(group,val)=>[...$(group).children].forEach(x=>x.classList.toggle('on', x.dataset.mode===val || x.dataset.side===val)); + function applySlMode(){ setOn('slMode',slMode); $('slPct').disabled=slMode!=='pct'; $('slPrice').disabled=slMode!=='price'; } + function applyTpMode(){ setOn('tpMode',tpMode); $('tpRR').disabled=tpMode!=='rr'; $('tp').disabled=tpMode!=='price'; } + function applySym(){ + $('symBtn').classList.toggle('on',symOn); + $('symBtn').textContent = symOn ? '🔗 点位差对称:开' : '🔗 点位差对称:关'; + if(symOn){ slMode='price'; tpMode='price'; applySlMode(); applyTpMode(); } + } // side toggle $('side').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b)return; - side=b.dataset.side; - [...$('side').children].forEach(x=>x.classList.toggle('on',x===b)); - calc(); + side=b.dataset.side; setOn('side',side); calc(); }); // sl mode toggle $('slMode').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b)return; - slMode=b.dataset.mode; - [...$('slMode').children].forEach(x=>x.classList.toggle('on',x===b)); - $('slPct').disabled = slMode!=='pct'; - $('slPrice').disabled = slMode!=='price'; - calc(); + slMode=b.dataset.mode; if(symOn){ symOn=false; applySym(); } + applySlMode(); calc(); }); - // take-profit mode toggle $('tpMode').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b)return; - tpMode=b.dataset.mode; - [...$('tpMode').children].forEach(x=>x.classList.toggle('on',x===b)); - $('tpRR').disabled = tpMode!=='rr'; - $('tp').disabled = tpMode!=='price'; - calc(); + tpMode=b.dataset.mode; if(symOn){ symOn=false; applySym(); } + applyTpMode(); calc(); }); + // 点位差对称 toggle + $('symBtn').addEventListener('click',()=>{ symOn=!symOn; applySym(); calc(); }); - ['balance','riskPct','entry','slPct','slPrice','tp','tpRR'].forEach(id=>$(id).addEventListener('input',calc)); + // 记录最后编辑的价格框,对称时以它为基准镜像另一个 + $('slPrice').addEventListener('input',()=>{ lastPrice='sl'; }); + $('tp').addEventListener('input',()=>{ lastPrice='tp'; }); + + FIELDS.forEach(id=>$(id).addEventListener('input',calc)); const fmtN=(n,d=2)=>isFinite(n)?n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}):'—'; const fmtP=n=>isFinite(n)?n.toLocaleString('en-US',{minimumFractionDigits:5,maximumFractionDigits:5}):'—'; @@ -298,26 +312,55 @@ return n.toLocaleString('en-US',{minimumFractionDigits:0,maximumFractionDigits:d}); }; + // 持久化:记住上次输入而非固定默认值 + function save(){ + try{ + const d={ side, slMode, tpMode, symOn, lastPrice }; + FIELDS.forEach(id=>d[id]=$(id).value); + localStorage.setItem(KEY, JSON.stringify(d)); + }catch(e){} + } + function restore(){ + let d; try{ d=JSON.parse(localStorage.getItem(KEY)||'null'); }catch(e){ d=null; } + if(!d) return; + FIELDS.forEach(id=>{ if(d[id]!==undefined && d[id]!=='') $(id).value=d[id]; }); + if(d.side) side=d.side; + if(d.slMode) slMode=d.slMode; + if(d.tpMode) tpMode=d.tpMode; + symOn=!!d.symOn; lastPrice=d.lastPrice||'sl'; + } + function calc(){ const bal=+$('balance').value, riskPct=+$('riskPct').value, entry=+$('entry').value; const risk = bal*riskPct/100; $('riskAmt').textContent = isFinite(risk)? fmtN(risk)+' U' : '—'; - if(!(entry>0)||!(risk>0)){ clearOut(); return; } + if(!(entry>0)||!(risk>0)){ clearOut(); save(); return; } + + // 对称联动:以最后编辑的价格框为准,关于入场价镜像出另一个(等距,多空自适应) + if(symOn){ + if(lastPrice==='tp'){ + const t=+$('tp').value; + if(t>0) $('slPrice').value = (+(2*entry - t).toPrecision(8)); + }else{ + const s=+$('slPrice').value; + if(s>0) $('tp').value = (+(2*entry - s).toPrecision(8)); + } + } // resolve stop-loss price + distance let slPrice, slPctVal; if(slMode==='pct'){ slPctVal=+$('slPct').value; - if(!(slPctVal>0)){ clearOut(); return; } + if(!(slPctVal>0)){ clearOut(); save(); return; } slPrice = side==='long' ? entry*(1-slPctVal/100) : entry*(1+slPctVal/100); }else{ slPrice=+$('slPrice').value; - if(!(slPrice>0)){ clearOut(); return; } + if(!(slPrice>0)){ clearOut(); save(); return; } slPctVal = Math.abs(entry-slPrice)/entry*100; } const dist=Math.abs(entry-slPrice); - if(!(dist>0)){ clearOut(); return; } + if(!(dist>0)){ clearOut(); save(); return; } const qty = risk/dist; // 合约数量 const notional = qty*entry; // 名义价值 U @@ -370,6 +413,7 @@ `; }).join(''); $('ladder').innerHTML=rows; + save(); } function clearOut(){ @@ -377,6 +421,10 @@ $('slShow').textContent=''; $('ladder').innerHTML=''; } + // 初始化:先恢复上次输入,再应用状态,最后计算 + restore(); + setOn('side',side); + applySlMode(); applyTpMode(); applySym(); calc(); })();