feat: options sheet sizing, ITM/OTM labels, and 14-day chain view
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -50,16 +50,19 @@ def calc_order_size(
|
||||
budget_usdc: float | None = None,
|
||||
budget_buffer: float = 0.95,
|
||||
eth_amount: float | None = None,
|
||||
sheets: int | None = None,
|
||||
budget_cap: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
返回 sheets, eth_amount, total_premium。
|
||||
mode: budget_full 或 eth_amount。
|
||||
mode: budget_full / eth_amount / sheets。
|
||||
"""
|
||||
if quote_per_unit <= 0:
|
||||
return {"ok": False, "msg": "卖一价无效", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
|
||||
|
||||
if eth_amount is not None and eth_amount > 0:
|
||||
if sheets is not None and int(sheets) > 0:
|
||||
sheets = int(sheets)
|
||||
elif eth_amount is not None and eth_amount > 0:
|
||||
sheets = sheets_from_eth_amount(eth_amount, ct_mult)
|
||||
elif budget_usdc is not None and budget_usdc > 0:
|
||||
eff = float(budget_usdc) * float(budget_buffer)
|
||||
@@ -68,7 +71,7 @@ def calc_order_size(
|
||||
return {"ok": False, "msg": "无法计算单张权利金", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
|
||||
sheets = int(math.floor(eff / per_sheet))
|
||||
else:
|
||||
return {"ok": False, "msg": "请指定预算或 ETH 数量", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
|
||||
return {"ok": False, "msg": "请指定预算、币数量或张数", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
|
||||
|
||||
if sheets < min_sz:
|
||||
per = premium_per_sheet(quote_per_unit, ct_mult)
|
||||
@@ -110,3 +113,22 @@ def is_shallow_itm(
|
||||
return False
|
||||
return (strike - index_px) <= max_dist_usd
|
||||
return False
|
||||
|
||||
|
||||
def option_moneyness(*, opt_type: str, strike: float, index_px: float) -> str:
|
||||
"""返回 itm / otm / atm。"""
|
||||
o = (opt_type or "").upper()
|
||||
if strike is None or index_px is None or index_px <= 0:
|
||||
return "unknown"
|
||||
atm_band = max(index_px * 0.002, 2.0)
|
||||
if abs(strike - index_px) <= atm_band:
|
||||
return "atm"
|
||||
if o == "C":
|
||||
return "itm" if strike < index_px else "otm"
|
||||
if o == "P":
|
||||
return "itm" if strike > index_px else "otm"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def option_moneyness_label(moneyness: str) -> str:
|
||||
return {"itm": "实值", "otm": "虚值", "atm": "平值"}.get((moneyness or "").lower(), "")
|
||||
|
||||
@@ -84,6 +84,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"budget_buffer": _env_float("OKX_OPTIONS_BUDGET_BUFFER", 0.95),
|
||||
"default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
|
||||
"max_dte_days": _env_float("OKX_OPTIONS_MAX_DTE_DAYS", 2.0),
|
||||
"chain_max_dte_days": _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", 14.0),
|
||||
"itm_max_dist": _env_float("OKX_OPTIONS_ITM_MAX_DIST_USD", 30.0),
|
||||
"td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "cross").strip(),
|
||||
"allow_market_close": _env_bool("OKX_OPTIONS_ALLOW_MARKET_CLOSE", False),
|
||||
@@ -136,11 +137,11 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
chain = cfg["build_option_chain"](
|
||||
ex,
|
||||
u,
|
||||
max_dte_days=cfg["max_dte_days"],
|
||||
itm_only=True,
|
||||
max_dte_days=cfg["chain_max_dte_days"],
|
||||
itm_only=False,
|
||||
itm_max_dist_usd=cfg["itm_max_dist"],
|
||||
)
|
||||
return jsonify({"ok": True, **chain})
|
||||
return jsonify({"ok": True, **chain, "chain_max_dte_days": cfg["chain_max_dte_days"]})
|
||||
|
||||
@app.route("/api/options/quote")
|
||||
@lr
|
||||
@@ -160,20 +161,27 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
mode = (request.args.get("mode") or "budget_full").strip()
|
||||
budget = cfg["trade_budget"]
|
||||
eth_amount = None
|
||||
sheet_count = None
|
||||
try:
|
||||
if request.args.get("eth_amount"):
|
||||
eth_amount = float(request.args.get("eth_amount"))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
if request.args.get("sheets"):
|
||||
sheet_count = int(request.args.get("sheets"))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if ask is None or ask <= 0:
|
||||
return jsonify({**q, "ok": False, "msg": "暂无卖一价"})
|
||||
sizing = calc_order_size(
|
||||
quote_per_unit=float(ask),
|
||||
ct_mult=float(ct_mult),
|
||||
min_sz=int(min_sz),
|
||||
budget_usdc=budget if mode != "eth_amount" else None,
|
||||
budget_usdc=budget if mode == "budget_full" else None,
|
||||
budget_buffer=cfg["budget_buffer"],
|
||||
eth_amount=eth_amount if mode == "eth_amount" else None,
|
||||
sheets=sheet_count if mode == "sheets" else None,
|
||||
budget_cap=cfg["trade_budget"],
|
||||
)
|
||||
return jsonify(
|
||||
@@ -206,18 +214,25 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
ct_mult = float(q.get("ct_mult") or 0.01)
|
||||
min_sz = int(q.get("min_sz") or 1)
|
||||
eth_amount = None
|
||||
sheet_count = None
|
||||
if mode == "eth_amount":
|
||||
try:
|
||||
eth_amount = float(data.get("eth_amount"))
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"ok": False, "msg": "ETH 数量无效"})
|
||||
elif mode == "sheets":
|
||||
try:
|
||||
sheet_count = int(data.get("sheets"))
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"ok": False, "msg": "张数无效"})
|
||||
sizing = calc_order_size(
|
||||
quote_per_unit=float(ask),
|
||||
ct_mult=ct_mult,
|
||||
min_sz=min_sz,
|
||||
budget_usdc=cfg["trade_budget"] if mode != "eth_amount" else None,
|
||||
budget_usdc=cfg["trade_budget"] if mode == "budget_full" else None,
|
||||
budget_buffer=cfg["budget_buffer"],
|
||||
eth_amount=eth_amount,
|
||||
sheets=sheet_count,
|
||||
budget_cap=cfg["trade_budget"],
|
||||
)
|
||||
if not sizing.get("ok"):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% if not options_enabled %}
|
||||
<div class="flash" style="margin-bottom:12px">期权 API 未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code> 及主账户 <code>OKX_OPTIONS_API_*</code>,然后 <code>pm2 restart crypto_okx --update-env</code>。</div>
|
||||
{% endif %}
|
||||
<p class="muted options-hint">资金账户兑换 USDT→USDC 后,划转到交易账户即可买入。报价单位为每 1 ETH/BTC;1 张 = 0.01 ETH/BTC。表格中点「买入」直接下单;或点「选择」后在下方确认张数再买入。</p>
|
||||
<p class="muted options-hint">资金账户兑换 USDT→USDC 后,划转到交易账户即可买入。报价单位为每 1 ETH/BTC;1 张 = 0.01 ETH/BTC。期权链展示近 <span id="opt-chain-dte">14</span> 日到期合约,标注实值/虚值;下单可指定张数。</p>
|
||||
|
||||
<div class="options-funds-grid">
|
||||
<div class="options-funds-col">
|
||||
@@ -72,6 +72,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>行权价</th>
|
||||
<th>类型</th>
|
||||
<th>合约</th>
|
||||
<th>卖一</th>
|
||||
<th>买一</th>
|
||||
@@ -79,7 +80,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="opt-strike-tbody">
|
||||
<tr><td colspan="5" class="muted">请选择到期日</td></tr>
|
||||
<tr><td colspan="6" class="muted">请选择到期日</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -94,8 +95,10 @@
|
||||
<div><span class="k">ETH/BTC 数量</span><span id="opt-order-eth" class="v">—</span></div>
|
||||
<div><span class="k">预估权利金</span><span id="opt-order-premium" class="v">—</span></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label><input type="radio" name="opt-size-mode" value="budget_full" checked> 按单笔上限打满</label>
|
||||
<div class="form-row options-order-mode-row">
|
||||
<label><input type="radio" name="opt-size-mode" value="sheets" checked> 指定张数</label>
|
||||
<input type="number" id="opt-sheets-amount" min="1" step="1" value="1" placeholder="张数">
|
||||
<label><input type="radio" name="opt-size-mode" value="budget_full"> 按单笔上限打满</label>
|
||||
<label><input type="radio" name="opt-size-mode" value="eth_amount"> 指定币数量</label>
|
||||
<input type="number" id="opt-eth-amount" min="0.01" step="0.01" placeholder="如 0.5" style="display:none">
|
||||
<input type="text" id="opt-signal-note" placeholder="备注(关键位说明)">
|
||||
@@ -128,4 +131,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_panel.js?v=2"></script>
|
||||
<script src="/static/options_panel.js?v=3"></script>
|
||||
|
||||
Reference in New Issue
Block a user