Files
position-calculator/index.html

333 lines
12 KiB
HTML
Raw 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::-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">/ DOGE-USDT</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>
<input type="number" id="tp" step="any" placeholder="留空则不计算">
</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">建议仓位 · DOGE 数量</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" style="margin-bottom:0">
<div>
<div class="lbl">止损价</div>
<div class="v" id="slPriceShow"></div>
</div>
<div>
<div class="lbl">若止盈 · 盈利</div>
<div class="v" id="tpProfit"></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>,不是杠杆。表中「状态」校验爆仓价是否在止损之外——
<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];
let side='long', slMode='pct';
// 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();
});
// 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();
});
['balance','riskPct','entry','slPct','slPrice','tp'].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}):'—';
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; }
// resolve stop-loss price + distance
let slPrice, slPctVal;
if(slMode==='pct'){
slPctVal=+$('slPct').value;
if(!(slPctVal>0)){ clearOut(); return; }
slPrice = side==='long' ? entry*(1-slPctVal/100) : entry*(1+slPctVal/100);
}else{
slPrice=+$('slPrice').value;
if(!(slPrice>0)){ clearOut(); return; }
slPctVal = Math.abs(entry-slPrice)/entry*100;
}
const dist=Math.abs(entry-slPrice);
if(!(dist>0)){ clearOut(); return; }
const qty = risk/dist; // DOGE 数量
const notional = qty*entry; // 名义价值 U
$('qty').innerHTML = fmtN(qty,0)+' <span class="u">DOGE</span>';
$('notional').innerHTML = fmtN(notional)+' <span class="u">U</span>';
$('slPriceShow').textContent = fmtP(slPrice);
$('slShow').textContent = '止损 '+fmtN(slPctVal)+'%';
// take profit / RR
const tp=+$('tp').value;
if(tp>0){
const tpDist=Math.abs(tp-entry);
const profit=qty*tpDist;
const rr=tpDist/dist;
$('rr').textContent = '1 : '+fmtN(rr);
$('tpProfit').innerHTML = '+'+fmtN(profit)+' <span class="u">U</span>';
}else{ $('rr').textContent='—'; $('tpProfit').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;
}
function clearOut(){
['qty','notional','rr','slPriceShow','tpProfit'].forEach(id=>$(id).textContent='—');
$('slShow').textContent=''; $('ladder').innerHTML='';
}
calc();
})();
</script>
</body>
</html>