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>
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user