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()