Add sideways max-loss to perpetual-options calculator.
Show premium wipeout plus flat round-trip perp fees as case C for both size and points modes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -65,6 +65,19 @@ API:`POST /api/calculator/perp-options`
|
|||||||
组合净利 = 期权净利 + 永续亏损
|
组合净利 = 期权净利 + 永续亏损
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**C · 横盘(最大亏损)**
|
||||||
|
|
||||||
|
波动≈0、期权到期无内在价值:
|
||||||
|
|
||||||
|
```text
|
||||||
|
永续盈亏 ≈ 0
|
||||||
|
永续开平手续费 = 2 × 现价 × 1 × 0.05% (同价开平)
|
||||||
|
最大亏损 = 权利金总额 + 永续开平手续费
|
||||||
|
组合净利 = −最大亏损
|
||||||
|
```
|
||||||
|
|
||||||
|
忽略资金费 / Theta 过程中的中间态;口径与「权利金按全亏」一致。
|
||||||
|
|
||||||
### 手测示例
|
### 手测示例
|
||||||
|
|
||||||
现价 1800、波动 50 点、目标盈利 15、期权杠杆 100、永续杠杆 10:
|
现价 1800、波动 50 点、目标盈利 15、期权杠杆 100、永续杠杆 10:
|
||||||
@@ -126,6 +139,7 @@ move = (目标 + 权利金) / (期权币数 − 1)
|
|||||||
|------|----------------|
|
|------|----------------|
|
||||||
| A 永续方向对(净利=15) | ≈52.83 |
|
| A 永续方向对(净利=15) | ≈52.83 |
|
||||||
| B 组合净利=15 | 51.00 |
|
| B 组合净利=15 | 51.00 |
|
||||||
|
| C 横盘最大亏损 | 37.80(权利金 36 + 同价开平费 1.8) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,25 @@ def _move_for_perp_correct(*, spot: float, target: float, premium: float, fee_ra
|
|||||||
return (float(target) + float(premium) + 2.0 * float(spot) * float(fee_rate)) / denom
|
return (float(target) + float(premium) + 2.0 * float(spot) * float(fee_rate)) / denom
|
||||||
|
|
||||||
|
|
||||||
|
def _case_sideways(*, spot: float, premium_total: float) -> dict[str, Any]:
|
||||||
|
"""横盘/到期无方向:永续≈0,期权权利金全亏,另计永续开平同价手续费.
|
||||||
|
|
||||||
|
最大亏损(正数) = 权利金总额 + 开平手续费(exit=entry)
|
||||||
|
组合净利 = −最大亏损
|
||||||
|
"""
|
||||||
|
fee_flat = estimate_roundtrip_fee_usdt(spot, spot, qty=PERP_COINS, contract_size=1.0)
|
||||||
|
prem = float(premium_total)
|
||||||
|
max_loss = prem + float(fee_flat)
|
||||||
|
return {
|
||||||
|
"label": "横盘",
|
||||||
|
"perp_pnl_u": 0.0,
|
||||||
|
"premium_u": round(prem, 8),
|
||||||
|
"fee_u": round(float(fee_flat), 8),
|
||||||
|
"max_loss_u": round(max_loss, 8),
|
||||||
|
"net_u": round(-max_loss, 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def calc_perp_options_hedge(
|
def calc_perp_options_hedge(
|
||||||
*,
|
*,
|
||||||
base: str = "ETH",
|
base: str = "ETH",
|
||||||
@@ -186,6 +205,7 @@ def calc_perp_options_hedge(
|
|||||||
"perp_pnl_u": round(perp_loss, 8),
|
"perp_pnl_u": round(perp_loss, 8),
|
||||||
"portfolio_net_u": round(portfolio_net, 8),
|
"portfolio_net_u": round(portfolio_net, 8),
|
||||||
},
|
},
|
||||||
|
"case_sideways": _case_sideways(spot=s, premium_total=premium_total),
|
||||||
}, None
|
}, None
|
||||||
|
|
||||||
|
|
||||||
@@ -316,6 +336,7 @@ def calc_perp_options_points(
|
|||||||
"portfolio_error": port_err,
|
"portfolio_error": port_err,
|
||||||
"premium_u": round(premium_total, 8),
|
"premium_u": round(premium_total, 8),
|
||||||
},
|
},
|
||||||
|
"case_sideways": _case_sideways(spot=s, premium_total=premium_total),
|
||||||
}, None
|
}, None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -572,6 +572,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSidewaysCase(sw) {
|
||||||
|
sw = sw || {};
|
||||||
|
return (
|
||||||
|
'<section class="calc-po-case">' +
|
||||||
|
"<h4>情景 C · 横盘(最大亏损)</h4>" +
|
||||||
|
'<div class="calc-summary">' +
|
||||||
|
"<div><span>永续盈亏</span><strong>" +
|
||||||
|
fmtU(sw.perp_pnl_u != null ? sw.perp_pnl_u : 0) +
|
||||||
|
"</strong></div>" +
|
||||||
|
"<div><span>权利金(全亏)</span><strong>" +
|
||||||
|
fmt(sw.premium_u, 2) +
|
||||||
|
"U</strong></div>" +
|
||||||
|
"<div><span>永续开平手续费</span><strong>" +
|
||||||
|
fmt(sw.fee_u, 2) +
|
||||||
|
"U</strong></div>" +
|
||||||
|
"<div><span>最大亏损</span><strong class=\"" +
|
||||||
|
pnlClass(sw.net_u) +
|
||||||
|
'">' +
|
||||||
|
fmt(sw.max_loss_u, 2) +
|
||||||
|
"U</strong></div>" +
|
||||||
|
"<div><span>组合净利</span><strong class=\"" +
|
||||||
|
pnlClass(sw.net_u) +
|
||||||
|
'">' +
|
||||||
|
fmtU(sw.net_u) +
|
||||||
|
"</strong></div>" +
|
||||||
|
"</div></section>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPerpOptionsResult(data) {
|
function renderPerpOptionsResult(data) {
|
||||||
const box = $("calc-po-result");
|
const box = $("calc-po-result");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
@@ -614,6 +643,11 @@
|
|||||||
"<div><span>永续保证金</span><strong>" +
|
"<div><span>永续保证金</span><strong>" +
|
||||||
fmt(data.perp_margin_u, 2) +
|
fmt(data.perp_margin_u, 2) +
|
||||||
"U</strong></div>" +
|
"U</strong></div>" +
|
||||||
|
"<div><span>横盘最大亏损</span><strong class=\"" +
|
||||||
|
pnlClass((data.case_sideways || {}).net_u) +
|
||||||
|
'">' +
|
||||||
|
fmt((data.case_sideways || {}).max_loss_u, 2) +
|
||||||
|
"U</strong></div>" +
|
||||||
"<div><span>开仓参考</span><strong>" +
|
"<div><span>开仓参考</span><strong>" +
|
||||||
esc(capitalHint) +
|
esc(capitalHint) +
|
||||||
"</strong></div>" +
|
"</strong></div>" +
|
||||||
@@ -663,7 +697,9 @@
|
|||||||
'">' +
|
'">' +
|
||||||
fmtU(b.portfolio_net_u) +
|
fmtU(b.portfolio_net_u) +
|
||||||
"</strong></div>" +
|
"</strong></div>" +
|
||||||
"</div></section></div>";
|
"</div></section>" +
|
||||||
|
renderSidewaysCase(data.case_sideways) +
|
||||||
|
"</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPerpOptionsPointsResult(data) {
|
function renderPerpOptionsPointsResult(data) {
|
||||||
@@ -743,6 +779,11 @@
|
|||||||
"<div><span>永续保证金</span><strong>" +
|
"<div><span>永续保证金</span><strong>" +
|
||||||
fmt(data.perp_margin_u, 2) +
|
fmt(data.perp_margin_u, 2) +
|
||||||
"U</strong></div>" +
|
"U</strong></div>" +
|
||||||
|
"<div><span>横盘最大亏损</span><strong class=\"" +
|
||||||
|
pnlClass((data.case_sideways || {}).net_u) +
|
||||||
|
'">' +
|
||||||
|
fmt((data.case_sideways || {}).max_loss_u, 2) +
|
||||||
|
"U</strong></div>" +
|
||||||
"<div><span>开仓参考</span><strong>" +
|
"<div><span>开仓参考</span><strong>" +
|
||||||
esc(capitalHint) +
|
esc(capitalHint) +
|
||||||
"</strong></div>" +
|
"</strong></div>" +
|
||||||
@@ -774,6 +815,7 @@
|
|||||||
"</strong></div>" +
|
"</strong></div>" +
|
||||||
"</div></section>" +
|
"</div></section>" +
|
||||||
caseB +
|
caseB +
|
||||||
|
renderSidewaysCase(data.case_sideways) +
|
||||||
"</div>";
|
"</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -966,7 +966,7 @@
|
|||||||
<div class="calc-pane-split">
|
<div class="calc-pane-split">
|
||||||
<div class="calc-input-panel">
|
<div class="calc-input-panel">
|
||||||
<h2>永期对冲计算器</h2>
|
<h2>永期对冲计算器</h2>
|
||||||
<p class="calc-hint">永续固定 1 币;单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).「推仓位」由波动反推期权数量;「推点数」按永续:期权比例反推达目标盈利所需波动.</p>
|
<p class="calc-hint">永续固定 1 币;单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).「推仓位」由波动反推期权数量;「推点数」按永续:期权比例反推达目标盈利所需波动.横盘最大亏损 = 权利金全亏 + 永续开平同价手续费.</p>
|
||||||
<form id="calc-po-form" class="calc-form">
|
<form id="calc-po-form" class="calc-form">
|
||||||
<div class="calc-form-grid">
|
<div class="calc-form-grid">
|
||||||
<label class="calc-field">
|
<label class="calc-field">
|
||||||
@@ -1751,7 +1751,7 @@
|
|||||||
<script src="/assets/chart_draw.js?v=20260720-option-day-1600"></script>
|
<script src="/assets/chart_draw.js?v=20260720-option-day-1600"></script>
|
||||||
<script src="/assets/chart.js?v=20260720-option-day-1600"></script>
|
<script src="/assets/chart.js?v=20260720-option-day-1600"></script>
|
||||||
<script src="/assets/plan.js?v=20260720-autofill"></script>
|
<script src="/assets/plan.js?v=20260720-autofill"></script>
|
||||||
<script src="/assets/calculator.js?v=20260728-po-2dp"></script>
|
<script src="/assets/calculator.js?v=20260729-po-sideways"></script>
|
||||||
<script src="/assets/compare.js?v=20260723-compare"></script>
|
<script src="/assets/compare.js?v=20260723-compare"></script>
|
||||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||||
<script src="/assets/archive.js?v=20260724-opt-archive"></script>
|
<script src="/assets/archive.js?v=20260724-opt-archive"></script>
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ class HubPerpOptionsCalcTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], coins * 32.0 - 50.0, places=6)
|
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], coins * 32.0 - 50.0, places=6)
|
||||||
self.assertAlmostEqual(data["perp_margin_u"], 180.0, places=6)
|
self.assertAlmostEqual(data["perp_margin_u"], 180.0, places=6)
|
||||||
self.assertTrue(data["capital_ok"])
|
self.assertTrue(data["capital_ok"])
|
||||||
|
sw = data["case_sideways"]
|
||||||
|
self.assertAlmostEqual(sw["premium_u"], data["premium_total_u"], places=6)
|
||||||
|
self.assertAlmostEqual(sw["fee_u"], 1.8, places=6)
|
||||||
|
self.assertAlmostEqual(sw["max_loss_u"], data["premium_total_u"] + 1.8, places=6)
|
||||||
|
self.assertAlmostEqual(sw["net_u"], -sw["max_loss_u"], places=6)
|
||||||
|
|
||||||
def test_pct_mode(self):
|
def test_pct_mode(self):
|
||||||
data, err = calc_perp_options_hedge(
|
data, err = calc_perp_options_hedge(
|
||||||
@@ -113,6 +118,12 @@ class HubPerpOptionsCalcTests(unittest.TestCase):
|
|||||||
# 组合净利=目标: (15+36)/(2-1) = 51
|
# 组合净利=目标: (15+36)/(2-1) = 51
|
||||||
self.assertAlmostEqual(data["case_b"]["move_points_portfolio"], 51.0, places=6)
|
self.assertAlmostEqual(data["case_b"]["move_points_portfolio"], 51.0, places=6)
|
||||||
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], 15.0, places=6)
|
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], 15.0, places=6)
|
||||||
|
# 横盘:权利金36 + 同价开平费 2*1800*0.0005=1.8 → 37.8
|
||||||
|
sw = data["case_sideways"]
|
||||||
|
self.assertAlmostEqual(sw["premium_u"], 36.0, places=6)
|
||||||
|
self.assertAlmostEqual(sw["fee_u"], 1.8, places=6)
|
||||||
|
self.assertAlmostEqual(sw["max_loss_u"], 37.8, places=6)
|
||||||
|
self.assertAlmostEqual(sw["net_u"], -37.8, places=6)
|
||||||
|
|
||||||
def test_points_ratio_1_to_1_no_portfolio(self):
|
def test_points_ratio_1_to_1_no_portfolio(self):
|
||||||
data, err = calc_perp_options(
|
data, err = calc_perp_options(
|
||||||
|
|||||||
Reference in New Issue
Block a user