feat: 止盈/止损点位差对称联动 + 输入框记忆上次输入(localStorage)

This commit is contained in:
2026-06-12 09:13:44 +00:00
parent 6bbc75d063
commit f703c026cc

View File

@@ -183,6 +183,12 @@
<input type="number" id="tp" step="any" placeholder="止盈价" disabled> <input type="number" id="tp" step="any" placeholder="止盈价" disabled>
</div> </div>
</div> </div>
<div class="field">
<label>点位差对称联动</label>
<div class="sl-mode" id="symMode" style="margin-bottom:0">
<button id="symBtn" type="button">🔗 点位差对称:关</button>
</div>
</div>
</div> </div>
<!-- RESULTS --> <!-- RESULTS -->
@@ -247,8 +253,8 @@
</div> </div>
<div class="note"> <div class="note">
<b>说明:</b>真正把亏损锁定在风险金额的是<b>止损单</b>,不是杠杆。止盈默认<b>按盈亏比自动反算</b>—— <b>说明:</b>真正把亏损锁定在风险金额的是<b>止损单</b>,不是杠杆。止盈默认<b>按盈亏比自动反算</b>(止盈点位差 = 止损点位差 × R
止盈点位差 = 止损点位差 × 盈亏比 R按方向算出止盈价也可切「按价格」手动输入 开启<b>点位差对称</b>后,输入止盈或止损价,另一个会按相同点位差自动镜像(即 1:1。所有输入会自动记住上次的值
表中「状态」校验爆仓价是否在止损之外——<span class="safe">安全</span>表示会先触发止损、不会被强平;<span class="danger">危险</span>表示杠杆过高、爆仓在止损之前。 表中「状态」校验爆仓价是否在止损之外——<span class="safe">安全</span>表示会先触发止损、不会被强平;<span class="danger">危险</span>表示杠杆过高、爆仓在止损之前。
爆仓价为<b>逐仓简化估算</b>爆仓距离≈1/杠杆,忽略维持保证金与手续费),实盘以交易所显示为准。这只是数学计算,不构成投资建议。 爆仓价为<b>逐仓简化估算</b>爆仓距离≈1/杠杆,忽略维持保证金与手续费),实盘以交易所显示为准。这只是数学计算,不构成投资建议。
</div> </div>
@@ -258,36 +264,44 @@
(function(){ (function(){
const $=id=>document.getElementById(id); const $=id=>document.getElementById(id);
const levs=[3,5,10,20,25,50,75,100]; 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 toggle
$('side').addEventListener('click',e=>{ $('side').addEventListener('click',e=>{
const b=e.target.closest('button'); if(!b)return; const b=e.target.closest('button'); if(!b)return;
side=b.dataset.side; side=b.dataset.side; setOn('side',side); calc();
[...$('side').children].forEach(x=>x.classList.toggle('on',x===b));
calc();
}); });
// sl mode toggle // sl mode toggle
$('slMode').addEventListener('click',e=>{ $('slMode').addEventListener('click',e=>{
const b=e.target.closest('button'); if(!b)return; const b=e.target.closest('button'); if(!b)return;
slMode=b.dataset.mode; slMode=b.dataset.mode; if(symOn){ symOn=false; applySym(); }
[...$('slMode').children].forEach(x=>x.classList.toggle('on',x===b)); applySlMode(); calc();
$('slPct').disabled = slMode!=='pct';
$('slPrice').disabled = slMode!=='price';
calc();
}); });
// take-profit mode toggle // take-profit mode toggle
$('tpMode').addEventListener('click',e=>{ $('tpMode').addEventListener('click',e=>{
const b=e.target.closest('button'); if(!b)return; const b=e.target.closest('button'); if(!b)return;
tpMode=b.dataset.mode; tpMode=b.dataset.mode; if(symOn){ symOn=false; applySym(); }
[...$('tpMode').children].forEach(x=>x.classList.toggle('on',x===b)); applyTpMode(); calc();
$('tpRR').disabled = tpMode!=='rr';
$('tp').disabled = tpMode!=='price';
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 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}):'—'; 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}); 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(){ function calc(){
const bal=+$('balance').value, riskPct=+$('riskPct').value, entry=+$('entry').value; const bal=+$('balance').value, riskPct=+$('riskPct').value, entry=+$('entry').value;
const risk = bal*riskPct/100; const risk = bal*riskPct/100;
$('riskAmt').textContent = isFinite(risk)? fmtN(risk)+' U' : '—'; $('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 // resolve stop-loss price + distance
let slPrice, slPctVal; let slPrice, slPctVal;
if(slMode==='pct'){ if(slMode==='pct'){
slPctVal=+$('slPct').value; 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); slPrice = side==='long' ? entry*(1-slPctVal/100) : entry*(1+slPctVal/100);
}else{ }else{
slPrice=+$('slPrice').value; slPrice=+$('slPrice').value;
if(!(slPrice>0)){ clearOut(); return; } if(!(slPrice>0)){ clearOut(); save(); return; }
slPctVal = Math.abs(entry-slPrice)/entry*100; slPctVal = Math.abs(entry-slPrice)/entry*100;
} }
const dist=Math.abs(entry-slPrice); const dist=Math.abs(entry-slPrice);
if(!(dist>0)){ clearOut(); return; } if(!(dist>0)){ clearOut(); save(); return; }
const qty = risk/dist; // 合约数量 const qty = risk/dist; // 合约数量
const notional = qty*entry; // 名义价值 U const notional = qty*entry; // 名义价值 U
@@ -370,6 +413,7 @@
</tr>`; </tr>`;
}).join(''); }).join('');
$('ladder').innerHTML=rows; $('ladder').innerHTML=rows;
save();
} }
function clearOut(){ function clearOut(){
@@ -377,6 +421,10 @@
$('slShow').textContent=''; $('ladder').innerHTML=''; $('slShow').textContent=''; $('ladder').innerHTML='';
} }
// 初始化:先恢复上次输入,再应用状态,最后计算
restore();
setOn('side',side);
applySlMode(); applyTpMode(); applySym();
calc(); calc();
})(); })();
</script> </script>