feat(hedge): option-primary watch entry with leverage gate

Start strategy arms a watching plan instead of opening immediately; list filters by leverage; type is a dropdown defaulting to OTM.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-09 09:27:42 +08:00
parent 6ab27cebcd
commit eca6d091e9
10 changed files with 608 additions and 39 deletions
+159 -2
View File
@@ -387,6 +387,134 @@ def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
conn.close()
def _persist_po_watching(cfg: dict[str, Any], body: dict[str, Any]) -> int:
"""以期权为主:只落库盯盘计划,不下单."""
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_plan
from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view
conn = cfg["get_db"]()
try:
init_hedge_plan_tables(conn)
view = str(body.get("direction") or "long")
money = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower()
plan_id = insert_plan(
conn,
{
"plan_type": "perp_options",
"status": "watching",
"underlying": str(body.get("underlying") or "ETH").upper(),
"direction": view,
"entry_mark": float(body.get("index_px") or body.get("entry") or 0) or None,
"tp": 0,
"sl": 0,
"sizing_mode_at_open": None,
"perp_size": None,
"margin": None,
"leverage": float(body.get("leverage") or 100),
"premium_total": 0,
"preview_json": _start_body_json(body),
"close_reason": None,
"opened_at": None,
"note": "盯盘中:等待杠杆/间隔达标后自动开仓",
"option_primary": 1,
"perp_direction": perp_direction_for_view(view),
"option_target_points": float(body.get("option_target_points") or 0),
"perp_target_points": float(body.get("perp_target_points") or 0),
"option_perp_ratio": float(body.get("option_perp_ratio") or 0),
"premium_budget": float(body.get("premium_budget") or 0),
"strike_interval": float(body.get("strike_interval") or 15),
"min_option_hours": float(body.get("min_option_hours") or 36),
"option_moneyness": money,
"option_leverage": float(body.get("option_leverage") or 0),
},
)
conn.commit()
return plan_id
finally:
conn.close()
def _activate_watching_po(
cfg: dict[str, Any],
conn: Any,
plan_id: int,
result: dict[str, Any],
body: dict[str, Any],
) -> None:
"""盯盘命中后:写入腿并把 watching → active/partial."""
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, insert_leg, update_plan
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view
is_partial = bool(result.get("partial"))
missing = str(result.get("missing_leg") or "") if is_partial else ""
opt = result.get("option") or {}
perp = result.get("perp") or {}
if is_partial:
opt_ok = missing != "option_hedge" and bool(result.get("option"))
perp_ok = missing != "perp" and bool(result.get("perp"))
else:
opt_ok = True
perp_ok = True
premium = float((opt or {}).get("premium") or 0) if opt_ok else 0.0
view = str(body.get("direction") or "long")
perp_dir = (
str((perp or {}).get("direction") or "")
or perp_direction_for_view(view)
)
update_plan(
conn,
int(plan_id),
status="partial" if is_partial else "active",
entry_mark=float(body.get("entry") or body.get("index_px") or 0) or None,
perp_size=float((perp or {}).get("contracts") or body.get("contracts") or 0),
leverage=float(body.get("leverage") or 100),
premium_total=premium,
preview_json=_start_body_json(body, missing or None),
close_reason="partial_fail" if is_partial else None,
opened_at=result.get("opened_at"),
note=(result.get("msg") or "")[:500] if is_partial else "盯盘达标已开仓",
perp_direction=perp_dir,
)
insert_leg(
conn,
{
"plan_id": int(plan_id),
"leg_role": "perp",
"symbol": str(body.get("exchange_symbol") or ""),
"side": perp_dir,
"size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
"avg_open": float(body.get("entry") or 0) if perp_ok else None,
"status": "open" if perp_ok else "pending",
"exchange_ord_id": str((perp or {}).get("exchange_ord_id") or ""),
"opened_at": result.get("opened_at") if perp_ok else None,
},
)
insert_leg(
conn,
{
"plan_id": int(plan_id),
"leg_role": "option_hedge",
"inst_id": str((opt or {}).get("inst_id") or body.get("opt_inst_id") or ""),
"opt_type": str((opt or {}).get("opt_type") or body.get("opt_type") or ""),
"strike": (opt or {}).get("strike") or body.get("strike"),
"side": "buy",
"size": float((opt or {}).get("sheets") or body.get("sheets") or 1),
"avg_open": float((opt or {}).get("ask") or body.get("ask") or 0) if opt_ok else None,
"premium": premium if opt_ok else 0,
"ct_mult": float(body.get("ct_mult") or (opt or {}).get("ct_mult") or 0.01),
"status": "open" if opt_ok else "pending",
"exchange_ord_id": str((opt or {}).get("exchange_ord_id") or ""),
"opened_at": result.get("opened_at") if opt_ok else None,
},
)
if not is_partial:
plan = get_plan(conn, int(plan_id))
legs = get_plan_legs(conn, int(plan_id))
if plan:
notify_plan_start(cfg, conn, plan, legs)
def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
from lib.hedge_plan.hedge_plan_db import (
get_plan,
@@ -702,6 +830,33 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
body["leverage"] = int(cfg.get("btc_leverage") or 10)
else:
body["leverage"] = int(cfg.get("alt_leverage") or 5)
# 以期权为主:策略启动=盯盘,不现场开仓
if plan_type == "perp_options":
from lib.hedge_plan.hedge_plan_option_primary_lib import is_option_primary
watch = body.get("watch_entry")
watch_on = watch in (None, "", True, 1, "1", "true", "yes", "on")
if is_option_primary(body) and watch_on:
if dry_run:
return jsonify(
{
"ok": True,
"dry_run": True,
"watching": True,
"msg": "dry_run:将创建盯盘计划(不落库)",
"gates": gates,
}
)
plan_id = _persist_po_watching(cfg, body)
return jsonify(
{
"ok": True,
"watching": True,
"plan_id": plan_id,
"msg": "已启动盯盘,杠杆/间隔达标后自动开仓",
"gates": gates,
}
)
if plan_type == "options_options":
out = execute_options_options_start(
cfg,
@@ -904,17 +1059,19 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
try:
init_hedge_plan_tables(conn)
rows = []
for status in ("opening", "active", "partial"):
for status in ("watching", "opening", "active", "partial"):
rows.extend(list_plans(conn, status=status, limit=80))
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
for row in rows:
if str(row.get("status") or "") == "watching":
continue
try:
reconcile_unfilled_option_legs(cfg, conn, int(row["id"]))
except Exception:
pass
# 校正后可能 status 变化,重新拉一遍
rows = []
for status in ("opening", "active", "partial"):
for status in ("watching", "opening", "active", "partial"):
rows.extend(list_plans(conn, status=status, limit=80))
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
plans = attach_legs_to_plans(conn, rows)