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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user