fix(hedge): filter options-chain by min hours and strike interval for option-primary

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-09 08:38:11 +08:00
parent afe361ce47
commit e7e733d9a9
5 changed files with 74 additions and 5 deletions
+1 -1
View File
@@ -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. 测试
+10 -3
View File
@@ -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);
+54
View File
@@ -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,
}
)
@@ -368,4 +368,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=37"></script>
<script src="/static/hedge_plan.js?v=38"></script>
+8
View File
@@ -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()