Park partial hedge plans for manual leg complete instead of auto-close.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1198,6 +1198,51 @@
|
||||
return "上破 " + fmt(p.target_price_up || p.target_price) + " · 下破 " + fmt(p.target_price_down || p.target_price);
|
||||
}
|
||||
|
||||
function activeStatusLabel(p) {
|
||||
if ((p.status || "") === "partial") {
|
||||
return '<span class="hp-plan-partial">半腿待补</span>';
|
||||
}
|
||||
if ((p.status || "") === "opening") {
|
||||
return '<span class="hp-plan-active">开仓中</span>';
|
||||
}
|
||||
return '<span class="hp-plan-active">进行中</span>';
|
||||
}
|
||||
|
||||
function completeLegButtonHtml(p) {
|
||||
if ((p.status || "") !== "partial") return "";
|
||||
const role = p.missing_leg || "";
|
||||
let label = "";
|
||||
if (role === "perp") label = "补开永续";
|
||||
else if (role === "option_b") label = "补开腿B";
|
||||
else if (role === "option_hedge" || role === "option_a") label = "补开期权";
|
||||
else return "";
|
||||
return (
|
||||
'<button type="button" class="primary hp-btn-complete" data-id="' +
|
||||
p.id +
|
||||
'" data-role="' +
|
||||
role +
|
||||
'">' +
|
||||
label +
|
||||
"</button>"
|
||||
);
|
||||
}
|
||||
|
||||
async function completeMissingLeg(planId) {
|
||||
if (!window.confirm("确认补开缺失腿并真实下单?")) return;
|
||||
try {
|
||||
const d = await apiJson("/api/hedge-plan/" + planId + "/complete-leg", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
alert("补开成功 #" + (d.plan_id || planId) + " · 已进入进行中");
|
||||
void loadActivePlans();
|
||||
void loadGates();
|
||||
} catch (e) {
|
||||
alert(e.message || String(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadActivePlans() {
|
||||
const tbody = $("hp-active-tbody");
|
||||
if (!tbody) return;
|
||||
@@ -1225,12 +1270,14 @@
|
||||
"\"><code>" +
|
||||
contracts +
|
||||
"</code></td><td>" +
|
||||
'<span class="hp-plan-active">进行中</span>' +
|
||||
activeStatusLabel(p) +
|
||||
"</td><td>" +
|
||||
activeTargetLabel(p) +
|
||||
"</td><td>" +
|
||||
(p.opened_at || "—") +
|
||||
'</td><td class="hp-hist-actions"><button type="button" class="btn-secondary hp-btn-detail" data-id="' +
|
||||
'</td><td class="hp-hist-actions">' +
|
||||
completeLegButtonHtml(p) +
|
||||
'<button type="button" class="btn-secondary hp-btn-detail" data-id="' +
|
||||
p.id +
|
||||
'">成交细节</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
@@ -1240,6 +1287,11 @@
|
||||
void showPlanDetail(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll(".hp-btn-complete").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
void completeMissingLeg(Number(btn.getAttribute("data-id")));
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="err">' + (e.message || e) + "</td></tr>";
|
||||
}
|
||||
@@ -1532,7 +1584,18 @@
|
||||
});
|
||||
setGateLine(d.gates);
|
||||
closePreviewModal();
|
||||
alert("计划已启动 #" + (d.plan_id || "") + (d.dry_run ? " (dry_run)" : ""));
|
||||
if (d.partial) {
|
||||
alert(
|
||||
(d.msg || "半腿失败,已挂待补") +
|
||||
(d.plan_id ? "\n计划 #" + d.plan_id : "") +
|
||||
"\n请到「进行中的计划」补开缺失腿"
|
||||
);
|
||||
state.tab = "active";
|
||||
syncTabUI();
|
||||
void loadActivePlans();
|
||||
} else {
|
||||
alert("计划已启动 #" + (d.plan_id || "") + (d.dry_run ? " (dry_run)" : ""));
|
||||
}
|
||||
void loadGates();
|
||||
} catch (e) {
|
||||
alert(e.message || String(e));
|
||||
|
||||
@@ -3249,6 +3249,16 @@ html[data-theme="light"] .opt-be-dist-down {
|
||||
color: #7ee787;
|
||||
font-weight: 700;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-plan-partial {
|
||||
color: #ffb454;
|
||||
font-weight: 700;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-hist-actions .hp-btn-complete {
|
||||
margin-right: 6px;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.72rem;
|
||||
min-height: 26px;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-pnl-neg {
|
||||
color: #ff8a8a;
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -92,6 +92,7 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
|
||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
|
||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
|
||||
"MAX_ACTIVE_HEDGE_PLANS",
|
||||
"HEDGE_PLAN_MONITOR_POLL_SECONDS",
|
||||
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
|
||||
|
||||
Vendored
+11
-1
@@ -151,9 +151,18 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = {
|
||||
"对冲与期权互斥门控",
|
||||
"默认 true;开启时:有对冲计划则不可单独开期权,有单独期权则不可启动对冲;关闭后两边可同时开",
|
||||
),
|
||||
(
|
||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
|
||||
"半腿失败改手动补开",
|
||||
"默认 true;开启时半腿失败不自动平,计划挂 partial,页面可补开永续/腿B;并强制关闭下方自动平",
|
||||
),
|
||||
("MAX_ACTIVE_HEDGE_PLANS", "最大同时活跃计划数", "建议 1"),
|
||||
("HEDGE_PLAN_MONITOR_POLL_SECONDS", "对冲监控轮询(秒)", "默认 15"),
|
||||
("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", "半腿失败时自动平期权", ""),
|
||||
(
|
||||
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
|
||||
"半腿失败时自动平期权",
|
||||
"默认 true;若上方「半腿失败改手动补开」开启则本项强制无效(不会自动平)",
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -169,6 +178,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
|
||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true",
|
||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -128,6 +128,22 @@ def update_plan(conn: sqlite3.Connection, plan_id: int, **fields: Any) -> None:
|
||||
conn.execute(f"UPDATE hedge_plans SET {sets} WHERE id=?", [*fields.values(), plan_id])
|
||||
|
||||
|
||||
def update_leg(conn: sqlite3.Connection, leg_id: int, **fields: Any) -> None:
|
||||
if not fields:
|
||||
return
|
||||
sets = ", ".join(f"{k}=?" for k in fields)
|
||||
conn.execute(f"UPDATE hedge_plan_legs SET {sets} WHERE id=?", [*fields.values(), int(leg_id)])
|
||||
|
||||
|
||||
def missing_leg_role(legs: list[dict[str, Any]]) -> Optional[str]:
|
||||
for leg in legs or []:
|
||||
if str(leg.get("status") or "").strip().lower() == "pending":
|
||||
role = str(leg.get("leg_role") or "").strip()
|
||||
if role:
|
||||
return role
|
||||
return None
|
||||
|
||||
|
||||
def list_plans(
|
||||
conn: sqlite3.Connection,
|
||||
*,
|
||||
@@ -184,15 +200,17 @@ def legs_contract_summary(legs: list[dict[str, Any]]) -> str:
|
||||
parts: list[str] = []
|
||||
for leg in legs:
|
||||
role = str(leg.get("leg_role") or "")
|
||||
st = str(leg.get("status") or "").strip().lower()
|
||||
suffix = "(待补)" if st == "pending" else ""
|
||||
if role == "perp":
|
||||
name = str(leg.get("symbol") or "永续")
|
||||
parts.append(f"永续 {name}")
|
||||
parts.append(f"永续 {name}{suffix}")
|
||||
else:
|
||||
inst = str(leg.get("inst_id") or "")
|
||||
ot = str(leg.get("opt_type") or "").upper()
|
||||
strike = leg.get("strike")
|
||||
label = inst or (f"{ot}{strike}" if ot or strike is not None else role)
|
||||
parts.append(label)
|
||||
parts.append(f"{label}{suffix}")
|
||||
return " · ".join(parts) if parts else "—"
|
||||
|
||||
|
||||
@@ -203,6 +221,7 @@ def attach_legs_to_plans(conn: sqlite3.Connection, plans: list[dict[str, Any]])
|
||||
row = dict(p)
|
||||
row["legs"] = legs
|
||||
row["contracts_summary"] = legs_contract_summary(legs)
|
||||
row["missing_leg"] = missing_leg_role(legs)
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
@@ -23,6 +23,18 @@ def open_order_mode() -> str:
|
||||
return v if v in ("options_first", "perp_first") else "options_first"
|
||||
|
||||
|
||||
def manual_complete_on_partial() -> bool:
|
||||
"""半腿失败后挂 partial 并手动补开(默认 true)."""
|
||||
return _env_bool("HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL", True)
|
||||
|
||||
|
||||
def partial_auto_close_enabled() -> bool:
|
||||
"""手动补开开启时强制关闭自动平,避免吃买卖价差."""
|
||||
if manual_complete_on_partial():
|
||||
return False
|
||||
return _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True)
|
||||
|
||||
|
||||
def build_po_path_plan(body: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""永期下单路径清单(不交易)."""
|
||||
mode = open_order_mode()
|
||||
@@ -257,6 +269,49 @@ def _sell_option(
|
||||
return order if order.get("ok") else order
|
||||
|
||||
|
||||
def _notify_partial(cfg: dict[str, Any], plan_type: str, msg: str, results: list[dict[str, Any]]) -> None:
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail
|
||||
|
||||
notify_partial_fail(cfg, plan_type=plan_type, msg=msg, results=results)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _park_partial(
|
||||
cfg: dict[str, Any],
|
||||
*,
|
||||
plan_type: str,
|
||||
body: dict[str, Any],
|
||||
missing_leg: str,
|
||||
msg: str,
|
||||
path: list[dict[str, Any]],
|
||||
results: list[dict[str, Any]],
|
||||
persist: Optional[Callable[..., Any]],
|
||||
dry_run: bool,
|
||||
**filled: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""半腿失败:保留已成腿,挂 partial 供手动补开."""
|
||||
if not dry_run:
|
||||
_notify_partial(cfg, plan_type, msg, results)
|
||||
out: dict[str, Any] = {
|
||||
"ok": True,
|
||||
"partial": True,
|
||||
"status": "partial",
|
||||
"dry_run": dry_run,
|
||||
"plan_type": plan_type,
|
||||
"missing_leg": missing_leg,
|
||||
"msg": msg,
|
||||
"path": path,
|
||||
"results": results,
|
||||
"opened_at": _now(),
|
||||
**filled,
|
||||
}
|
||||
if persist and not dry_run:
|
||||
out["plan_id"] = persist(out, body)
|
||||
return out
|
||||
|
||||
|
||||
def execute_perp_options_start(
|
||||
cfg: dict[str, Any],
|
||||
body: dict[str, Any],
|
||||
@@ -278,6 +333,27 @@ def execute_perp_options_start(
|
||||
)
|
||||
results.append({"step": step["step"], **opt_res})
|
||||
if not opt_res.get("ok"):
|
||||
# 永续已成、期权失败 → 可挂 partial 等补开期权
|
||||
if (
|
||||
perp_res
|
||||
and perp_res.get("ok")
|
||||
and not dry_run
|
||||
and manual_complete_on_partial()
|
||||
and persist
|
||||
):
|
||||
return _park_partial(
|
||||
cfg,
|
||||
plan_type="perp_options",
|
||||
body=body,
|
||||
missing_leg="option_hedge",
|
||||
msg="永续已开、期权失败。计划已挂半腿待补,请在「进行中」补开期权",
|
||||
path=path,
|
||||
results=results,
|
||||
persist=persist,
|
||||
dry_run=dry_run,
|
||||
option=None,
|
||||
perp=perp_res,
|
||||
)
|
||||
return {"ok": False, "msg": opt_res.get("msg") or "期权开仓失败", "path": path, "results": results}
|
||||
else:
|
||||
perp_res = _open_perp(
|
||||
@@ -292,24 +368,45 @@ def execute_perp_options_start(
|
||||
)
|
||||
results.append({"step": step["step"], **perp_res})
|
||||
if not perp_res.get("ok"):
|
||||
# 半腿补偿:期权已成 + 配置允许则平期权
|
||||
if opt_res and opt_res.get("ok") and not dry_run and _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True):
|
||||
if opt_res and opt_res.get("ok") and not dry_run and partial_auto_close_enabled():
|
||||
close_r = _sell_option(
|
||||
cfg,
|
||||
inst_id=str(opt_res.get("inst_id") or body.get("opt_inst_id") or ""),
|
||||
sheets=float(opt_res.get("sheets") or body.get("sheets") or 1),
|
||||
)
|
||||
results.append({"step": "options_auto_close_on_perp_fail", **close_r})
|
||||
msg = perp_res.get("msg") or "永续开仓失败"
|
||||
_notify_partial(cfg, "perp_options", msg, results)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": msg,
|
||||
"path": path,
|
||||
"results": results,
|
||||
"partial": True,
|
||||
}
|
||||
if (
|
||||
opt_res
|
||||
and opt_res.get("ok")
|
||||
and not dry_run
|
||||
and manual_complete_on_partial()
|
||||
and persist
|
||||
):
|
||||
return _park_partial(
|
||||
cfg,
|
||||
plan_type="perp_options",
|
||||
body=body,
|
||||
missing_leg="perp",
|
||||
msg="期权已开、永续失败。计划已挂半腿待补,请在「进行中」补开永续",
|
||||
path=path,
|
||||
results=results,
|
||||
persist=persist,
|
||||
dry_run=dry_run,
|
||||
option=opt_res,
|
||||
perp=None,
|
||||
)
|
||||
msg = perp_res.get("msg") or "永续开仓失败"
|
||||
if not dry_run:
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail
|
||||
|
||||
notify_partial_fail(
|
||||
cfg, plan_type="perp_options", msg=msg, results=results
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_notify_partial(cfg, "perp_options", msg, results)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": msg,
|
||||
@@ -351,17 +448,35 @@ def execute_options_options_start(
|
||||
b_res = _buy_option(cfg, inst_id=str(leg_b.get("inst_id") or ""), sheets=float(leg_b.get("sheets") or 1), dry_run=dry_run)
|
||||
results.append({"step": "options_buy_limit", "leg": "b", **b_res})
|
||||
if not b_res.get("ok"):
|
||||
if not dry_run and _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True):
|
||||
if not dry_run and partial_auto_close_enabled():
|
||||
close_r = _sell_option(cfg, inst_id=str(a_res.get("inst_id") or ""), sheets=float(a_res.get("sheets") or 1))
|
||||
results.append({"step": "options_auto_close_leg_a", **close_r})
|
||||
msg = b_res.get("msg") or "腿B开仓失败"
|
||||
_notify_partial(cfg, "options_options", msg, results)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": msg,
|
||||
"path": path,
|
||||
"results": results,
|
||||
"partial": True,
|
||||
}
|
||||
if not dry_run and manual_complete_on_partial() and persist:
|
||||
return _park_partial(
|
||||
cfg,
|
||||
plan_type="options_options",
|
||||
body=body,
|
||||
missing_leg="option_b",
|
||||
msg="腿A已开、腿B失败。计划已挂半腿待补,请在「进行中」补开腿B",
|
||||
path=path,
|
||||
results=results,
|
||||
persist=persist,
|
||||
dry_run=dry_run,
|
||||
leg_a=a_res,
|
||||
leg_b=None,
|
||||
)
|
||||
msg = b_res.get("msg") or "腿B开仓失败"
|
||||
if not dry_run:
|
||||
try:
|
||||
from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail
|
||||
|
||||
notify_partial_fail(cfg, plan_type="options_options", msg=msg, results=results)
|
||||
except Exception:
|
||||
pass
|
||||
_notify_partial(cfg, "options_options", msg, results)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": msg,
|
||||
@@ -384,6 +499,73 @@ def execute_options_options_start(
|
||||
return out
|
||||
|
||||
|
||||
def execute_complete_missing_leg(
|
||||
cfg: dict[str, Any],
|
||||
plan: dict[str, Any],
|
||||
legs: list[dict[str, Any]],
|
||||
start_body: dict[str, Any],
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""对 partial 计划补开缺失腿;成功后由调用方把计划升为 active."""
|
||||
missing = None
|
||||
for leg in legs:
|
||||
if str(leg.get("status") or "").lower() == "pending":
|
||||
missing = leg
|
||||
break
|
||||
if not missing:
|
||||
return {"ok": False, "msg": "没有待补开的腿"}
|
||||
role = str(missing.get("leg_role") or "")
|
||||
results: list[dict[str, Any]] = []
|
||||
if role == "perp":
|
||||
res = _open_perp(
|
||||
cfg,
|
||||
symbol=str(start_body.get("exchange_symbol") or missing.get("symbol") or ""),
|
||||
direction=str(start_body.get("direction") or "long"),
|
||||
contracts=float(start_body.get("contracts") or missing.get("size") or 0),
|
||||
leverage=int(start_body.get("leverage") or 10),
|
||||
tp=float(start_body["tp"]),
|
||||
sl=float(start_body["sl"]),
|
||||
dry_run=dry_run,
|
||||
)
|
||||
results.append({"step": "perp_market_open", "complete": True, **res})
|
||||
if not res.get("ok"):
|
||||
return {"ok": False, "msg": res.get("msg") or "补开永续失败", "results": results, "leg_role": role}
|
||||
return {
|
||||
"ok": True,
|
||||
"leg_role": role,
|
||||
"leg_id": missing.get("id"),
|
||||
"results": results,
|
||||
"fill": res,
|
||||
"opened_at": _now(),
|
||||
}
|
||||
if role in ("option_hedge", "option_b", "option_a"):
|
||||
if role == "option_b":
|
||||
src = start_body.get("leg_b") or {}
|
||||
inst = str(src.get("inst_id") or missing.get("inst_id") or "")
|
||||
sheets = float(src.get("sheets") or missing.get("size") or 1)
|
||||
elif role == "option_a":
|
||||
src = start_body.get("leg_a") or {}
|
||||
inst = str(src.get("inst_id") or missing.get("inst_id") or "")
|
||||
sheets = float(src.get("sheets") or missing.get("size") or 1)
|
||||
else:
|
||||
inst = str(start_body.get("opt_inst_id") or missing.get("inst_id") or "")
|
||||
sheets = float(start_body.get("sheets") or missing.get("size") or 1)
|
||||
res = _buy_option(cfg, inst_id=inst, sheets=sheets, dry_run=dry_run)
|
||||
results.append({"step": "options_buy_limit", "complete": True, "leg_role": role, **res})
|
||||
if not res.get("ok"):
|
||||
return {"ok": False, "msg": res.get("msg") or "补开期权失败", "results": results, "leg_role": role}
|
||||
return {
|
||||
"ok": True,
|
||||
"leg_role": role,
|
||||
"leg_id": missing.get("id"),
|
||||
"results": results,
|
||||
"fill": res,
|
||||
"opened_at": _now(),
|
||||
}
|
||||
return {"ok": False, "msg": f"未知待补腿: {role}"}
|
||||
|
||||
|
||||
def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
|
||||
pt = (plan_type or "").strip().lower()
|
||||
if pt == "perp_options":
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -300,4 +300,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/hedge_plan.js?v=21"></script>
|
||||
<script src="/static/hedge_plan.js?v=22"></script>
|
||||
|
||||
Reference in New Issue
Block a user