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:
+1
-1
@@ -59,7 +59,7 @@ contracts = perp_eth / contract_size
|
|||||||
| `lib/hedge_plan/hedge_plan_option_primary_lib.py` | 定仓/方向/目标/净利/校验 |
|
| `lib/hedge_plan/hedge_plan_option_primary_lib.py` | 定仓/方向/目标/净利/校验 |
|
||||||
| `hedge_plan_orders_lib.py` | 路径、开平永续、启动前定仓刷新 |
|
| `hedge_plan_orders_lib.py` | 路径、开平永续、启动前定仓刷新 |
|
||||||
| `hedge_plan_monitor_lib.py` | `_tick_po_option_primary*` |
|
| `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` | 开关与左右卡 |
|
| `hedge_plan.js` + `hedge_plan_panel.html` | 开关与左右卡 |
|
||||||
|
|
||||||
## 6. 测试
|
## 6. 测试
|
||||||
|
|||||||
@@ -1036,9 +1036,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadChain() {
|
async function loadChain() {
|
||||||
const d = await apiJson(
|
let url =
|
||||||
"/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying)
|
"/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;
|
state.chain = d;
|
||||||
const idx = $("hp-index-line");
|
const idx = $("hp-index-line");
|
||||||
if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2);
|
if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2);
|
||||||
|
|||||||
@@ -559,6 +559,57 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500
|
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)
|
opt_acct = _options_account_snapshot(cfg)
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
@@ -572,6 +623,9 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
"options_account": opt_acct,
|
"options_account": opt_acct,
|
||||||
"trade_budget_usdc": cfg.get("trade_budget_usdc"),
|
"trade_budget_usdc": cfg.get("trade_budget_usdc"),
|
||||||
"budget_buffer": cfg.get("budget_buffer"),
|
"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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/hedge_plan.js?v=37"></script>
|
<script src="/static/hedge_plan.js?v=38"></script>
|
||||||
|
|||||||
@@ -139,6 +139,14 @@ class TestOptionPrimary(unittest.TestCase):
|
|||||||
self.assertTrue(out["option_primary"])
|
self.assertTrue(out["option_primary"])
|
||||||
self.assertEqual(len(out["scenarios"]), 2)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user