Park partial hedge plans for manual leg complete instead of auto-close.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||
@@ -223,6 +223,18 @@ def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
|
||||
cfg["hedge_monitor_thread"] = t
|
||||
|
||||
|
||||
def _start_body_json(body: dict[str, Any], missing_leg: Optional[str] = None) -> str:
|
||||
import json
|
||||
|
||||
try:
|
||||
return json.dumps(
|
||||
{"start_body": body, "missing_leg": missing_leg},
|
||||
ensure_ascii=False,
|
||||
)[:8000]
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int:
|
||||
from lib.hedge_plan.hedge_plan_db import (
|
||||
get_plan,
|
||||
@@ -236,25 +248,36 @@ def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
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 {}
|
||||
premium = float(opt.get("premium") or 0)
|
||||
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
|
||||
plan_id = insert_plan(
|
||||
conn,
|
||||
{
|
||||
"plan_type": "perp_options",
|
||||
"status": "active",
|
||||
"status": "partial" if is_partial else "active",
|
||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
||||
"direction": str(body.get("direction") or "long"),
|
||||
"entry_mark": float(body.get("entry") or 0),
|
||||
"tp": float(body.get("tp") or 0),
|
||||
"sl": float(body.get("sl") or 0),
|
||||
"sizing_mode_at_open": load_position_sizing_mode(),
|
||||
"perp_size": float(perp.get("contracts") or body.get("contracts") or 0),
|
||||
"perp_size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
|
||||
"margin": body.get("margin"),
|
||||
"leverage": float(body.get("leverage") or 10),
|
||||
"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 None,
|
||||
},
|
||||
)
|
||||
insert_leg(
|
||||
@@ -264,11 +287,11 @@ def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
||||
"leg_role": "perp",
|
||||
"symbol": str(body.get("exchange_symbol") or ""),
|
||||
"side": str(body.get("direction") or "long"),
|
||||
"size": float(perp.get("contracts") or body.get("contracts") or 0),
|
||||
"avg_open": float(body.get("entry") or 0),
|
||||
"status": "open",
|
||||
"exchange_ord_id": str(perp.get("exchange_ord_id") or ""),
|
||||
"opened_at": result.get("opened_at"),
|
||||
"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(
|
||||
@@ -276,24 +299,25 @@ def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
||||
{
|
||||
"plan_id": plan_id,
|
||||
"leg_role": "option_hedge",
|
||||
"inst_id": str(opt.get("inst_id") or body.get("opt_inst_id") or ""),
|
||||
"opt_type": str(opt.get("opt_type") or body.get("opt_type") or ""),
|
||||
"strike": opt.get("strike") or body.get("strike"),
|
||||
"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.get("sheets") or body.get("sheets") or 1),
|
||||
"avg_open": float(opt.get("ask") or 0),
|
||||
"premium": premium,
|
||||
"status": "open",
|
||||
"exchange_ord_id": str(opt.get("exchange_ord_id") or ""),
|
||||
"opened_at": result.get("opened_at"),
|
||||
"size": float((opt or {}).get("sheets") or body.get("sheets") or 1),
|
||||
"avg_open": float((opt or {}).get("ask") or 0) if opt_ok else None,
|
||||
"premium": premium if opt_ok else 0,
|
||||
"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,
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
plan = get_plan(conn, plan_id)
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
if plan:
|
||||
notify_plan_start(cfg, conn, plan, legs)
|
||||
conn.commit()
|
||||
if not is_partial:
|
||||
plan = get_plan(conn, plan_id)
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
if plan:
|
||||
notify_plan_start(cfg, conn, plan, legs)
|
||||
conn.commit()
|
||||
return plan_id
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -312,14 +336,20 @@ def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
is_partial = bool(result.get("partial"))
|
||||
missing = str(result.get("missing_leg") or "") if is_partial else ""
|
||||
a = result.get("leg_a") or {}
|
||||
b = result.get("leg_b") or {}
|
||||
premium = float(a.get("premium") or 0) + float(b.get("premium") or 0)
|
||||
a_ok = True if not is_partial else bool(result.get("leg_a"))
|
||||
b_ok = True if not is_partial else (missing != "option_b" and bool(result.get("leg_b")))
|
||||
premium = (float(a.get("premium") or 0) if a_ok else 0.0) + (
|
||||
float(b.get("premium") or 0) if b_ok else 0.0
|
||||
)
|
||||
plan_id = insert_plan(
|
||||
conn,
|
||||
{
|
||||
"plan_type": "options_options",
|
||||
"status": "active",
|
||||
"status": "partial" if is_partial else "active",
|
||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
||||
"target_price": float(
|
||||
body.get("target_price_up")
|
||||
@@ -339,33 +369,40 @@ def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
||||
"sizing_mode_at_open": load_position_sizing_mode(),
|
||||
"premium_total": premium,
|
||||
"oo_close_mode": _normalize_oo_close_mode(body.get("oo_close_mode")),
|
||||
"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 None,
|
||||
},
|
||||
)
|
||||
for role, res, src in (("option_a", a, body.get("leg_a") or {}), ("option_b", b, body.get("leg_b") or {})):
|
||||
for role, res, src, ok in (
|
||||
("option_a", a, body.get("leg_a") or {}, a_ok),
|
||||
("option_b", b, body.get("leg_b") or {}, b_ok),
|
||||
):
|
||||
insert_leg(
|
||||
conn,
|
||||
{
|
||||
"plan_id": plan_id,
|
||||
"leg_role": role,
|
||||
"inst_id": str(res.get("inst_id") or src.get("inst_id") or ""),
|
||||
"opt_type": str(res.get("opt_type") or src.get("opt_type") or ""),
|
||||
"strike": res.get("strike") or src.get("strike"),
|
||||
"inst_id": str((res or {}).get("inst_id") or src.get("inst_id") or ""),
|
||||
"opt_type": str((res or {}).get("opt_type") or src.get("opt_type") or ""),
|
||||
"strike": (res or {}).get("strike") or src.get("strike"),
|
||||
"side": "buy",
|
||||
"size": float(res.get("sheets") or src.get("sheets") or 1),
|
||||
"avg_open": float(res.get("ask") or 0),
|
||||
"premium": float(res.get("premium") or 0),
|
||||
"status": "open",
|
||||
"exchange_ord_id": str(res.get("exchange_ord_id") or ""),
|
||||
"opened_at": result.get("opened_at"),
|
||||
"size": float((res or {}).get("sheets") or src.get("sheets") or 1),
|
||||
"avg_open": float((res or {}).get("ask") or 0) if ok else None,
|
||||
"premium": float((res or {}).get("premium") or 0) if ok else 0,
|
||||
"status": "open" if ok else "pending",
|
||||
"exchange_ord_id": str((res or {}).get("exchange_ord_id") or ""),
|
||||
"opened_at": result.get("opened_at") if ok else None,
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
plan = get_plan(conn, plan_id)
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
if plan:
|
||||
notify_plan_start(cfg, conn, plan, legs)
|
||||
conn.commit()
|
||||
if not is_partial:
|
||||
plan = get_plan(conn, plan_id)
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
if plan:
|
||||
notify_plan_start(cfg, conn, plan, legs)
|
||||
conn.commit()
|
||||
return plan_id
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -537,6 +574,112 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
out["gates"] = gates
|
||||
return jsonify(out), (200 if out.get("ok") else 400)
|
||||
|
||||
@app.route("/api/hedge-plan/<int:plan_id>/complete-leg", methods=["POST"])
|
||||
@lr
|
||||
def api_hedge_complete_leg(plan_id: int):
|
||||
"""半腿待补:手动补开缺失腿,成功后升为 active."""
|
||||
import json
|
||||
|
||||
from lib.hedge_plan.hedge_plan_db import (
|
||||
get_plan,
|
||||
get_plan_legs,
|
||||
init_hedge_plan_tables,
|
||||
update_leg,
|
||||
update_plan,
|
||||
)
|
||||
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import execute_complete_missing_leg
|
||||
|
||||
body = request.get_json(silent=True) or {}
|
||||
dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False)
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
plan = get_plan(conn, plan_id)
|
||||
if not plan:
|
||||
return jsonify({"ok": False, "msg": "计划不存在"}), 404
|
||||
if str(plan.get("status") or "") != "partial":
|
||||
return jsonify({"ok": False, "msg": "仅半腿待补(partial)计划可补开"}), 400
|
||||
legs = get_plan_legs(conn, plan_id)
|
||||
start_body: dict[str, Any] = {}
|
||||
try:
|
||||
meta = json.loads(plan.get("preview_json") or "{}")
|
||||
if isinstance(meta, dict):
|
||||
start_body = dict(meta.get("start_body") or {})
|
||||
except Exception:
|
||||
start_body = {}
|
||||
if not start_body:
|
||||
return jsonify({"ok": False, "msg": "缺少开仓参数,无法补开"}), 400
|
||||
# 允许请求体覆盖少量字段
|
||||
for k in ("contracts", "leverage", "sheets", "tp", "sl"):
|
||||
if body.get(k) not in (None, ""):
|
||||
start_body[k] = body.get(k)
|
||||
out = execute_complete_missing_leg(
|
||||
cfg, plan, legs, start_body, dry_run=dry_run
|
||||
)
|
||||
if not out.get("ok"):
|
||||
return jsonify(out), 400
|
||||
if dry_run:
|
||||
return jsonify(out)
|
||||
fill = out.get("fill") or {}
|
||||
leg_id = out.get("leg_id")
|
||||
role = str(out.get("leg_role") or "")
|
||||
opened_at = out.get("opened_at")
|
||||
if leg_id:
|
||||
if role == "perp":
|
||||
update_leg(
|
||||
conn,
|
||||
int(leg_id),
|
||||
status="open",
|
||||
size=float(fill.get("contracts") or start_body.get("contracts") or 0),
|
||||
avg_open=float(start_body.get("entry") or plan.get("entry_mark") or 0),
|
||||
exchange_ord_id=str(fill.get("exchange_ord_id") or ""),
|
||||
opened_at=opened_at,
|
||||
)
|
||||
update_plan(
|
||||
conn,
|
||||
plan_id,
|
||||
status="active",
|
||||
close_reason=None,
|
||||
note=None,
|
||||
perp_size=float(fill.get("contracts") or start_body.get("contracts") or 0),
|
||||
)
|
||||
else:
|
||||
prem = float(fill.get("premium") or 0)
|
||||
update_leg(
|
||||
conn,
|
||||
int(leg_id),
|
||||
status="open",
|
||||
size=float(fill.get("sheets") or start_body.get("sheets") or 1),
|
||||
avg_open=float(fill.get("ask") or 0),
|
||||
premium=prem,
|
||||
exchange_ord_id=str(fill.get("exchange_ord_id") or ""),
|
||||
opened_at=opened_at,
|
||||
inst_id=str(fill.get("inst_id") or ""),
|
||||
)
|
||||
old_prem = float(plan.get("premium_total") or 0)
|
||||
update_plan(
|
||||
conn,
|
||||
plan_id,
|
||||
status="active",
|
||||
close_reason=None,
|
||||
note=None,
|
||||
premium_total=old_prem + prem,
|
||||
)
|
||||
conn.commit()
|
||||
plan2 = get_plan(conn, plan_id)
|
||||
legs2 = get_plan_legs(conn, plan_id)
|
||||
if plan2:
|
||||
notify_plan_start(cfg, conn, plan2, legs2)
|
||||
conn.commit()
|
||||
out["plan_id"] = plan_id
|
||||
out["status"] = "active"
|
||||
out["plan"] = plan2
|
||||
out["legs"] = legs2
|
||||
return jsonify(out)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@app.route("/api/hedge-plan/list")
|
||||
@lr
|
||||
def api_hedge_list():
|
||||
|
||||
Reference in New Issue
Block a user