feat: 止盈/止损点位差对称联动 + 输入框记忆上次输入(localStorage)
This commit is contained in:
92
index.html
92
index.html
@@ -183,6 +183,12 @@
|
||||
<input type="number" id="tp" step="any" placeholder="止盈价" disabled>
|
||||
</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>
|
||||
|
||||
<!-- RESULTS -->
|
||||
@@ -247,8 +253,8 @@
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<b>说明:</b>真正把亏损锁定在风险金额的是<b>止损单</b>,不是杠杆。止盈默认<b>按盈亏比自动反算</b>——
|
||||
止盈点位差 = 止损点位差 × 盈亏比 R,按方向算出止盈价;也可切「按价格」手动输入。
|
||||
<b>说明:</b>真正把亏损锁定在风险金额的是<b>止损单</b>,不是杠杆。止盈默认<b>按盈亏比自动反算</b>(止盈点位差 = 止损点位差 × R);
|
||||
开启<b>点位差对称</b>后,输入止盈或止损价,另一个会按相同点位差自动镜像(即 1:1)。所有输入会自动记住上次的值。
|
||||
表中「状态」校验爆仓价是否在止损之外——<span class="safe">安全</span>表示会先触发止损、不会被强平;<span class="danger">危险</span>表示杠杆过高、爆仓在止损之前。
|
||||
爆仓价为<b>逐仓简化估算</b>(爆仓距离≈1/杠杆,忽略维持保证金与手续费),实盘以交易所显示为准。这只是数学计算,不构成投资建议。
|
||||
</div>
|
||||
@@ -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 @@
|
||||
</tr>`;
|
||||
}).join('');
|
||||
$('ladder').innerHTML=rows;
|
||||
save();
|
||||
}
|
||||
|
||||
function clearOut(){
|
||||
@@ -377,6 +421,10 @@
|
||||
$('slShow').textContent=''; $('ladder').innerHTML='';
|
||||
}
|
||||
|
||||
// 初始化:先恢复上次输入,再应用状态,最后计算
|
||||
restore();
|
||||
setOn('side',side);
|
||||
applySlMode(); applyTpMode(); applySym();
|
||||
calc();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user