Files
position-calculator/index.html

433 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>合约仓位计算器</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0e1419;
--panel:#161d27;
--panel-2:#1b2430;
--border:#283340;
--border-hi:#36465a;
--text:#e6edf3;
--muted:#7d8896;
--faint:#566374;
--long:#16c784;
--short:#ea3943;
--gold:#f0b90b;
--mono:"JetBrains Mono",ui-monospace,"SF Mono","Menlo",monospace;
--sans:"Inter",system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
background:var(--bg);
color:var(--text);
font-family:var(--sans);
line-height:1.5;
padding:24px 16px 64px;
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
}
.mono,input[type=number],.val,.v,.lev-table,.tag,.result-head .risk b,.big .lbl,.pair .lbl,.eyebrow,.card h2,.lev-table th{
font-variant-numeric:tabular-nums;
font-feature-settings:"tnum" 1,"cv01" 1,"ss01" 1;
}
.wrap{max-width:880px;margin:0 auto}
header{margin-bottom:24px}
.eyebrow{
font-family:var(--mono);font-size:11px;letter-spacing:.18em;
text-transform:uppercase;color:var(--gold);margin-bottom:6px;
}
h1{font-size:22px;font-weight:700;letter-spacing:-.02em}
h1 .sym{color:var(--muted);font-weight:400;font-family:var(--mono)}
.sub{color:var(--muted);font-size:13px;margin-top:4px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:20px}
@media(max-width:640px){.grid{grid-template-columns:1fr}}
.card{
background:var(--panel);border:1px solid var(--border);
border-radius:12px;padding:18px;
}
.card h2{
font-size:11px;letter-spacing:.14em;text-transform:uppercase;
color:var(--faint);font-weight:600;margin-bottom:14px;font-family:var(--mono);
}
.field{margin-bottom:14px}
.field:last-child{margin-bottom:0}
label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
input[type=number]{
width:100%;background:var(--panel-2);border:1px solid var(--border);
color:var(--text);font-family:var(--mono);font-size:15px;
padding:9px 12px;border-radius:8px;outline:none;
transition:border-color .15s;
}
input[type=number]:focus{border-color:var(--border-hi)}
input[type=number]:disabled{opacity:.5;cursor:not-allowed}
input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.side-toggle{display:flex;gap:8px}
.side-toggle button{
flex:1;background:var(--panel-2);border:1px solid var(--border);
color:var(--muted);padding:9px;border-radius:8px;cursor:pointer;
font-size:14px;font-weight:600;transition:all .15s;
}
.side-toggle button.on[data-side=long]{background:rgba(22,199,132,.14);border-color:var(--long);color:var(--long)}
.side-toggle button.on[data-side=short]{background:rgba(234,57,67,.14);border-color:var(--short);color:var(--short)}
.sl-mode{display:flex;gap:6px;margin-bottom:8px}
.sl-mode button{
background:transparent;border:1px solid var(--border);color:var(--faint);
padding:4px 10px;border-radius:6px;cursor:pointer;font-size:11px;
font-family:var(--mono);transition:all .15s;
}
.sl-mode button.on{border-color:var(--gold);color:var(--gold)}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
/* results */
.result-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px}
.result-head .risk{font-size:12px;color:var(--muted)}
.result-head .risk b{color:var(--short);font-family:var(--mono);font-weight:600}
.big{margin-bottom:16px}
.big .lbl{font-size:11px;color:var(--faint);text-transform:uppercase;letter-spacing:.1em;font-family:var(--mono);margin-bottom:3px}
.big .val{font-family:var(--mono);font-size:27px;font-weight:700;letter-spacing:-.02em}
.big .val .u{font-size:13px;color:var(--muted);font-weight:400;margin-left:4px}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:16px}
.pair .lbl{font-size:11px;color:var(--faint);text-transform:uppercase;letter-spacing:.08em;font-family:var(--mono);margin-bottom:2px}
.pair .v{font-family:var(--mono);font-size:16px;font-weight:600}
.pair .v .u{font-size:11px;color:var(--muted);font-weight:400}
.lev-table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:13px}
.lev-table th{
text-align:right;font-weight:500;color:var(--faint);font-size:10px;
text-transform:uppercase;letter-spacing:.08em;padding:0 0 8px;
}
.lev-table th:first-child{text-align:left}
.lev-table td{padding:7px 0;border-top:1px solid var(--border);text-align:right}
.lev-table td:first-child{text-align:left;color:var(--gold);font-weight:600}
.lev-table .liq{color:var(--muted)}
.safe{color:var(--long)}
.danger{color:var(--short)}
.tag{font-size:10px;padding:1px 6px;border-radius:4px;font-family:var(--mono)}
.tag.ok{background:rgba(22,199,132,.12);color:var(--long)}
.tag.no{background:rgba(234,57,67,.12);color:var(--short)}
.note{
margin-top:18px;padding:12px 14px;background:var(--panel);
border:1px solid var(--border);border-radius:10px;
font-size:12px;color:var(--muted);line-height:1.6;
}
.note b{color:var(--text);font-weight:600}
.full{grid-column:1/-1}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Position Sizer · 逐仓</div>
<h1>合约仓位计算器 <span class="sym">/ 通用合约</span></h1>
<div class="sub">先定亏损上限,再反推仓位。杠杆只决定爆仓距离,不决定你亏多少。</div>
</header>
<div class="grid">
<!-- INPUTS -->
<div class="card">
<h2>输入</h2>
<div class="field">
<label>方向</label>
<div class="side-toggle" id="side">
<button data-side="long" class="on">做多 Long</button>
<button data-side="short">做空 Short</button>
</div>
</div>
<div class="field">
<label>账户余额 (USDT)</label>
<input type="number" id="balance" value="200" step="any">
</div>
<div class="field">
<label>单笔风险 (% of 余额)</label>
<input type="number" id="riskPct" value="5" step="any">
</div>
<div class="field">
<label>入场价</label>
<input type="number" id="entry" value="0.08505" step="any">
</div>
<div class="field">
<label>止损</label>
<div class="sl-mode" id="slMode">
<button data-mode="pct" class="on">按百分比</button>
<button data-mode="price">按价格</button>
</div>
<div class="row2">
<input type="number" id="slPct" value="2" step="any" placeholder="止损 %">
<input type="number" id="slPrice" step="any" placeholder="止损价" disabled>
</div>
</div>
<div class="field">
<label>止盈</label>
<div class="sl-mode" id="tpMode">
<button data-mode="rr" class="on">按盈亏比</button>
<button data-mode="price">按价格</button>
</div>
<div class="row2">
<input type="number" id="tpRR" value="2" step="any" placeholder="盈亏比 R如 2">
<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 -->
<div class="card">
<h2>仓位</h2>
<div class="result-head">
<span class="risk">最大亏损 <b id="riskAmt">10.00 U</b></span>
<span class="risk" id="slShow"></span>
</div>
<div class="big">
<div class="lbl">建议仓位 · 合约数量</div>
<div class="val" id="qty"></div>
</div>
<div class="pair">
<div>
<div class="lbl">名义价值</div>
<div class="v" id="notional"></div>
</div>
<div>
<div class="lbl">盈亏比 R:R</div>
<div class="v" id="rr"></div>
</div>
</div>
<div class="pair">
<div>
<div class="lbl">止损价</div>
<div class="v" id="slPriceShow"></div>
</div>
<div>
<div class="lbl">止盈价</div>
<div class="v" id="tpPriceShow"></div>
</div>
</div>
<div class="pair" style="margin-bottom:0">
<div>
<div class="lbl">若止盈 · 盈利</div>
<div class="v" id="tpProfit"></div>
</div>
<div>
<div class="lbl">止盈幅度</div>
<div class="v" id="tpPctShow"></div>
</div>
</div>
</div>
<!-- LEVERAGE LADDER -->
<div class="card full">
<h2>杠杆阶梯 · 保证金 & 爆仓校验</h2>
<table class="lev-table">
<thead>
<tr>
<th>杠杆</th>
<th>占用保证金</th>
<th>爆仓价</th>
<th>爆仓距离</th>
<th>状态</th>
</tr>
</thead>
<tbody id="ladder"></tbody>
</table>
</div>
</div>
<div class="note">
<b>说明:</b>真正把亏损锁定在风险金额的是<b>止损单</b>,不是杠杆。止盈默认<b>按盈亏比自动反算</b>(止盈点位差 = 止损点位差 × R
开启<b>点位差对称</b>后,输入止盈或止损价,另一个会按相同点位差自动镜像(即 1:1。所有输入会自动记住上次的值。
表中「状态」校验爆仓价是否在止损之外——<span class="safe">安全</span>表示会先触发止损、不会被强平;<span class="danger">危险</span>表示杠杆过高、爆仓在止损之前。
爆仓价为<b>逐仓简化估算</b>爆仓距离≈1/杠杆,忽略维持保证金与手续费),实盘以交易所显示为准。这只是数学计算,不构成投资建议。
</div>
</div>
<script>
(function(){
const $=id=>document.getElementById(id);
const levs=[3,5,10,20,25,50,75,100];
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; setOn('side',side); calc();
});
// sl mode toggle
$('slMode').addEventListener('click',e=>{
const b=e.target.closest('button'); if(!b)return;
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; if(symOn){ symOn=false; applySym(); }
applyTpMode(); calc();
});
// 点位差对称 toggle
$('symBtn').addEventListener('click',()=>{ symOn=!symOn; applySym(); 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}):'—';
// 仓位数量:按量级自适应小数位(大币种如 BTC 量很小,小币种如 DOGE 量上千)
const fmtQty=n=>{
if(!isFinite(n)) return '—';
const d = n>=1000?0 : n>=100?1 : n>=1?2 : n>=0.01?4 : 6;
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(); 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(); save(); return; }
slPrice = side==='long' ? entry*(1-slPctVal/100) : entry*(1+slPctVal/100);
}else{
slPrice=+$('slPrice').value;
if(!(slPrice>0)){ clearOut(); save(); return; }
slPctVal = Math.abs(entry-slPrice)/entry*100;
}
const dist=Math.abs(entry-slPrice);
if(!(dist>0)){ clearOut(); save(); return; }
const qty = risk/dist; // 合约数量
const notional = qty*entry; // 名义价值 U
$('qty').innerHTML = fmtQty(qty)+' <span class="u">币</span>';
$('notional').innerHTML = fmtN(notional)+' <span class="u">U</span>';
$('slPriceShow').textContent = fmtP(slPrice);
$('slShow').textContent = '止损 '+fmtN(slPctVal)+'%';
// take profit — 两种模式:按盈亏比反算 / 按价格
let tpPrice, rr;
if(tpMode==='rr'){
rr=+$('tpRR').value; // 目标盈亏比
if(rr>0){
// 止盈点位差 = 止损点位差 × 盈亏比,按方向反算止盈价
tpPrice = side==='long' ? entry + dist*rr : entry - dist*rr;
$('tp').value = (+tpPrice.toPrecision(8)); // 同步显示到价格框,方便核对/切换
}
}else{
tpPrice=+$('tp').value;
if(tpPrice>0) rr = Math.abs(tpPrice-entry)/dist;
}
if(tpPrice>0 && rr>0){
const tpDist=Math.abs(tpPrice-entry);
const profit=qty*tpDist;
const tpPct=tpDist/entry*100;
$('rr').textContent = '1 : '+fmtN(rr);
$('tpPriceShow').textContent = fmtP(tpPrice);
$('tpProfit').innerHTML = '+'+fmtN(profit)+' <span class="u">U</span>';
$('tpPctShow').textContent = fmtN(tpPct)+'%';
}else{
$('rr').textContent='—'; $('tpPriceShow').textContent='—';
$('tpProfit').textContent='—'; $('tpPctShow').textContent='—';
}
// leverage ladder
const rows=levs.map(L=>{
const margin=notional/L;
const liq = side==='long' ? entry*(1-1/L) : entry*(1+1/L);
const liqDistPct = 100/L;
// safe if liquidation is farther than stop-loss
const safe = liqDistPct > slPctVal;
return `<tr>
<td>${L}x</td>
<td>${fmtN(margin)} U</td>
<td class="liq">${fmtP(liq)}</td>
<td>${fmtN(liqDistPct)}%</td>
<td><span class="tag ${safe?'ok':'no'}">${safe?'安全':'危险'}</span></td>
</tr>`;
}).join('');
$('ladder').innerHTML=rows;
save();
}
function clearOut(){
['qty','notional','rr','slPriceShow','tpPriceShow','tpProfit','tpPctShow'].forEach(id=>$(id).textContent='—');
$('slShow').textContent=''; $('ladder').innerHTML='';
}
// 初始化:先恢复上次输入,再应用状态,最后计算
restore();
setOn('side',side);
applySlMode(); applyTpMode(); applySym();
calc();
})();
</script>
</body>
</html>