diff --git a/docs/对冲计划-以期权为主.md b/docs/对冲计划-以期权为主.md index 3ca47e4..30948bc 100644 --- a/docs/对冲计划-以期权为主.md +++ b/docs/对冲计划-以期权为主.md @@ -59,7 +59,7 @@ contracts = perp_eth / contract_size | `lib/hedge_plan/hedge_plan_option_primary_lib.py` | 定仓/方向/目标/净利/校验 | | `hedge_plan_orders_lib.py` | 路径、开平永续、启动前定仓刷新 | | `hedge_plan_monitor_lib.py` | `_tick_po_option_primary*` | -| `hedge_plan_register.py` / `hedge_plan_db.py` | preview/start/persist 列 | +| `hedge_plan_register.py` / `hedge_plan_db.py` | preview/start/persist;`options-chain?option_primary&min_hours&strike_interval` | | `hedge_plan.js` + `hedge_plan_panel.html` | 开关与左右卡 | ## 6. 测试 diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index bd2147d..5f2a13c 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -1036,9 +1036,16 @@ } async function loadChain() { - const d = await apiJson( - "/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying) - ); + let url = + "/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying); + if (isOptionPrimary()) { + url += + "&option_primary=1&min_hours=" + + encodeURIComponent(String(numInput("hp-min-hours", 36))) + + "&strike_interval=" + + encodeURIComponent(String(numInput("hp-strike-interval", 15))); + } + const d = await apiJson(url); state.chain = d; const idx = $("hp-index-line"); if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2); diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py index f7a6358..cd32cab 100644 --- a/lib/hedge_plan/hedge_plan_register.py +++ b/lib/hedge_plan/hedge_plan_register.py @@ -559,6 +559,57 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None: ) except Exception as e: return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500 + # 以期权为主:可选最短剩余小时 / 行权间隔过滤(也回写 hours_to_expiry 供前端) + option_primary = (request.args.get("option_primary") or "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + min_hours = None + strike_interval = None + try: + if request.args.get("min_hours") not in (None, ""): + min_hours = float(request.args.get("min_hours")) + except (TypeError, ValueError): + min_hours = 36.0 if option_primary else None + try: + if request.args.get("strike_interval") not in (None, ""): + strike_interval = float(request.args.get("strike_interval")) + except (TypeError, ValueError): + strike_interval = 15.0 if option_primary else None + if option_primary and min_hours is None: + min_hours = 36.0 + if option_primary and strike_interval is None: + strike_interval = 15.0 + if min_hours is not None or strike_interval is not None: + from lib.hedge_plan.hedge_plan_option_primary_lib import hours_to_expiry_from_ms + + idx = None + try: + idx = float(chain.get("index_px") or 0) or None + except (TypeError, ValueError): + idx = None + filtered = [] + for exp in chain.get("expiries") or []: + h = hours_to_expiry_from_ms(exp.get("exp_time")) + if min_hours is not None and h is not None and h < min_hours: + continue + contracts = [] + for c in exp.get("contracts") or []: + row = dict(c) + row["hours_to_expiry"] = h + if strike_interval is not None and idx and idx > 0: + try: + k = float(row.get("strike") or 0) + except (TypeError, ValueError): + k = 0.0 + if k > 0 and abs(k - idx) > strike_interval + 1e-9: + continue + contracts.append(row) + if contracts: + filtered.append({**exp, "contracts": contracts, "hours_to_expiry": h}) + chain = {**chain, "expiries": filtered} opt_acct = _options_account_snapshot(cfg) return jsonify( { @@ -572,6 +623,9 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None: "options_account": opt_acct, "trade_budget_usdc": cfg.get("trade_budget_usdc"), "budget_buffer": cfg.get("budget_buffer"), + "option_primary": option_primary, + "min_hours": min_hours, + "strike_interval": strike_interval, } ) diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index ef0c780..c39ffde 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -368,4 +368,4 @@ - + diff --git a/tests/test_hedge_plan_option_primary.py b/tests/test_hedge_plan_option_primary.py index bf3a2be..6cefe8f 100644 --- a/tests/test_hedge_plan_option_primary.py +++ b/tests/test_hedge_plan_option_primary.py @@ -139,6 +139,14 @@ class TestOptionPrimary(unittest.TestCase): self.assertTrue(out["option_primary"]) self.assertEqual(len(out["scenarios"]), 2) + def test_hours_to_expiry_from_ms(self): + from lib.hedge_plan.hedge_plan_option_primary_lib import hours_to_expiry_from_ms + + now = 1_700_000_000_000.0 + # +40h in ms + h = hours_to_expiry_from_ms(now + 40 * 3600 * 1000, now_ms=now) + self.assertAlmostEqual(h, 40.0, places=3) + if __name__ == "__main__": unittest.main()