Add end-plan action; never show unfilled option legs as open.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1319,7 +1319,7 @@
|
|||||||
role +
|
role +
|
||||||
'">' +
|
'">' +
|
||||||
label +
|
label +
|
||||||
"</button>"
|
"</button> "
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,6 +1339,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function endActivePlan(planId) {
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"确认结束计划 #" +
|
||||||
|
planId +
|
||||||
|
"?\n不会自动平仓;未成交/待补腿将标为未成交取消。\n已有持仓请自行平掉。"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const d = await apiJson("/api/hedge-plan/" + planId + "/end", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
alert(d.msg || "计划已结束 #" + (d.plan_id || planId));
|
||||||
|
void loadActivePlans();
|
||||||
|
void loadHistory();
|
||||||
|
void loadGates();
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function legStatusLabel(st) {
|
||||||
|
const map = {
|
||||||
|
open: "持仓中",
|
||||||
|
pending: "待补/未成交",
|
||||||
|
cancelled: "未成交取消",
|
||||||
|
canceled: "未成交取消",
|
||||||
|
closed: "已平仓",
|
||||||
|
};
|
||||||
|
return map[st] || st || "—";
|
||||||
|
}
|
||||||
|
|
||||||
async function loadActivePlans() {
|
async function loadActivePlans() {
|
||||||
const tbody = $("hp-active-tbody");
|
const tbody = $("hp-active-tbody");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@@ -1375,7 +1411,10 @@
|
|||||||
completeLegButtonHtml(p) +
|
completeLegButtonHtml(p) +
|
||||||
'<button type="button" class="btn-secondary hp-btn-detail" data-id="' +
|
'<button type="button" class="btn-secondary hp-btn-detail" data-id="' +
|
||||||
p.id +
|
p.id +
|
||||||
'">成交细节</button></td>';
|
'">成交细节</button> ' +
|
||||||
|
'<button type="button" class="btn-secondary hp-btn-end" data-id="' +
|
||||||
|
p.id +
|
||||||
|
'">结束计划</button></td>';
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) {
|
tbody.querySelectorAll(".hp-btn-detail").forEach(function (btn) {
|
||||||
@@ -1388,6 +1427,11 @@
|
|||||||
void completeMissingLeg(Number(btn.getAttribute("data-id")));
|
void completeMissingLeg(Number(btn.getAttribute("data-id")));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
tbody.querySelectorAll(".hp-btn-end").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
void endActivePlan(Number(btn.getAttribute("data-id")));
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
tbody.innerHTML = '<tr><td colspan="8" class="err">' + (e.message || e) + "</td></tr>";
|
tbody.innerHTML = '<tr><td colspan="8" class="err">' + (e.message || e) + "</td></tr>";
|
||||||
}
|
}
|
||||||
@@ -1409,8 +1453,10 @@
|
|||||||
hold_to_expiry: "持有至到期",
|
hold_to_expiry: "持有至到期",
|
||||||
expiry: "到期",
|
expiry: "到期",
|
||||||
manual: "人工结束",
|
manual: "人工结束",
|
||||||
|
manual_end: "人工结束",
|
||||||
partial_fail: "半腿失败",
|
partial_fail: "半腿失败",
|
||||||
cancelled: "已取消",
|
cancelled: "已取消",
|
||||||
|
unfilled: "未成交",
|
||||||
};
|
};
|
||||||
return map[r] || r || "—";
|
return map[r] || r || "—";
|
||||||
}
|
}
|
||||||
@@ -1514,7 +1560,7 @@
|
|||||||
html += "<td>" + fmt(leg.size, leg.leg_role === "perp" ? 4 : 0) + "</td>";
|
html += "<td>" + fmt(leg.size, leg.leg_role === "perp" ? 4 : 0) + "</td>";
|
||||||
html += "<td>" + fmt(leg.avg_open, 4) + "</td>";
|
html += "<td>" + fmt(leg.avg_open, 4) + "</td>";
|
||||||
html += "<td>" + (leg.premium != null ? fmt(leg.premium, 4) : "—") + "</td>";
|
html += "<td>" + (leg.premium != null ? fmt(leg.premium, 4) : "—") + "</td>";
|
||||||
html += "<td>" + (leg.status || "—") + "</td>";
|
html += "<td>" + legStatusLabel(leg.status) + "</td>";
|
||||||
html += "<td>" + fmt(leg.realized_pnl, 4) + "</td>";
|
html += "<td>" + fmt(leg.realized_pnl, 4) + "</td>";
|
||||||
html += "<td><code class=\"hp-ord\">" + (leg.exchange_ord_id || "—") + "</code></td>";
|
html += "<td><code class=\"hp-ord\">" + (leg.exchange_ord_id || "—") + "</code></td>";
|
||||||
html += "<td>" + reasonLabel(leg.close_reason) + "</td>";
|
html += "<td>" + reasonLabel(leg.close_reason) + "</td>";
|
||||||
|
|||||||
@@ -201,7 +201,12 @@ def legs_contract_summary(legs: list[dict[str, Any]]) -> str:
|
|||||||
for leg in legs:
|
for leg in legs:
|
||||||
role = str(leg.get("leg_role") or "")
|
role = str(leg.get("leg_role") or "")
|
||||||
st = str(leg.get("status") or "").strip().lower()
|
st = str(leg.get("status") or "").strip().lower()
|
||||||
suffix = "(待补)" if st == "pending" else ""
|
if st == "pending":
|
||||||
|
suffix = "(待补)"
|
||||||
|
elif st in ("cancelled", "canceled"):
|
||||||
|
suffix = "(未成交)"
|
||||||
|
else:
|
||||||
|
suffix = ""
|
||||||
if role == "perp":
|
if role == "perp":
|
||||||
name = str(leg.get("symbol") or "永续")
|
name = str(leg.get("symbol") or "永续")
|
||||||
parts.append(f"永续 {name}{suffix}")
|
parts.append(f"永续 {name}{suffix}")
|
||||||
|
|||||||
@@ -647,3 +647,142 @@ def dump_preview(preview: Any) -> str:
|
|||||||
return json.dumps(preview, ensure_ascii=False)[:8000]
|
return json.dumps(preview, ensure_ascii=False)[:8000]
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _live_option_pos_sheets(ex: Any, inst_id: str) -> float:
|
||||||
|
from lib.exchange.okx_options_lib import fetch_option_positions
|
||||||
|
|
||||||
|
inst_id = (inst_id or "").strip()
|
||||||
|
if not inst_id or ex is None:
|
||||||
|
return 0.0
|
||||||
|
rows = fetch_option_positions(ex)
|
||||||
|
if rows is None:
|
||||||
|
return -1.0 # API 失败:未知
|
||||||
|
for r in rows:
|
||||||
|
if str(r.get("instId") or "").strip() != inst_id:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return abs(float(r.get("pos") or 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_plan_status_after_leg_fix(conn: Any, plan_id: int) -> None:
|
||||||
|
"""腿状态校正后:有 open + pending → partial."""
|
||||||
|
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, update_plan
|
||||||
|
|
||||||
|
plan = get_plan(conn, int(plan_id))
|
||||||
|
if not plan:
|
||||||
|
return
|
||||||
|
pst = str(plan.get("status") or "")
|
||||||
|
if pst not in ("opening", "active", "partial"):
|
||||||
|
return
|
||||||
|
legs = get_plan_legs(conn, int(plan_id))
|
||||||
|
statuses = [str(l.get("status") or "").lower() for l in legs]
|
||||||
|
n_open = sum(1 for s in statuses if s == "open")
|
||||||
|
n_pending = sum(1 for s in statuses if s == "pending")
|
||||||
|
if n_pending and n_open:
|
||||||
|
update_plan(conn, int(plan_id), status="partial", close_reason="partial_fail")
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_unfilled_option_legs(cfg: dict[str, Any], conn: Any, plan_id: int) -> list[str]:
|
||||||
|
"""未成交却标 open 的期权腿 → pending(可补开);不显示成持仓."""
|
||||||
|
from lib.hedge_plan.hedge_plan_db import get_plan_legs, update_leg
|
||||||
|
from lib.exchange.okx_options_lib import fetch_option_order
|
||||||
|
|
||||||
|
ex = cfg.get("exchange_options")
|
||||||
|
notes: list[str] = []
|
||||||
|
legs = get_plan_legs(conn, int(plan_id))
|
||||||
|
for leg in legs:
|
||||||
|
role = str(leg.get("leg_role") or "")
|
||||||
|
if not role.startswith("option"):
|
||||||
|
continue
|
||||||
|
st = str(leg.get("status") or "").lower()
|
||||||
|
if st != "open":
|
||||||
|
continue
|
||||||
|
inst = str(leg.get("inst_id") or "").strip()
|
||||||
|
oid = str(leg.get("exchange_ord_id") or "").strip()
|
||||||
|
leg_id = int(leg["id"])
|
||||||
|
sheets = _live_option_pos_sheets(ex, inst)
|
||||||
|
if sheets < 0:
|
||||||
|
continue # 查仓失败不改
|
||||||
|
if sheets >= 1:
|
||||||
|
continue
|
||||||
|
# 无实仓:再看订单是否已成交(仍挂单只改 pending,不撤单)
|
||||||
|
if ex is not None and inst and oid:
|
||||||
|
od = fetch_option_order(ex, inst_id=inst, ord_id=oid)
|
||||||
|
if od.get("ok"):
|
||||||
|
acc = float(od.get("acc_fill_sz") or 0)
|
||||||
|
ostate = str(od.get("state") or "")
|
||||||
|
if acc >= 1 or ostate == "filled":
|
||||||
|
continue # 有成交但仓位暂未同步,暂不改
|
||||||
|
update_leg(
|
||||||
|
conn,
|
||||||
|
leg_id,
|
||||||
|
status="pending",
|
||||||
|
close_reason=None,
|
||||||
|
closed_at=None,
|
||||||
|
avg_open=None,
|
||||||
|
premium=0,
|
||||||
|
)
|
||||||
|
notes.append(f"{inst} 无成交却标open→pending")
|
||||||
|
if notes:
|
||||||
|
_sync_plan_status_after_leg_fix(conn, int(plan_id))
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
def execute_manual_end_plan(cfg: dict[str, Any], conn: Any, plan_id: int) -> dict[str, Any]:
|
||||||
|
"""人工结束进行中计划:不自动平仓;未成交腿标 cancelled."""
|
||||||
|
from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, update_leg, update_plan
|
||||||
|
from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_end
|
||||||
|
from lib.exchange.okx_options_lib import cancel_option_order
|
||||||
|
|
||||||
|
plan = get_plan(conn, int(plan_id))
|
||||||
|
if not plan:
|
||||||
|
return {"ok": False, "msg": "计划不存在"}
|
||||||
|
st = str(plan.get("status") or "")
|
||||||
|
if st not in ("opening", "active", "partial"):
|
||||||
|
return {"ok": False, "msg": f"当前状态 {st or '—'} 不可结束"}
|
||||||
|
|
||||||
|
notes = reconcile_unfilled_option_legs(cfg, conn, int(plan_id))
|
||||||
|
ex = cfg.get("exchange_options")
|
||||||
|
legs = get_plan_legs(conn, int(plan_id))
|
||||||
|
for leg in legs:
|
||||||
|
lst = str(leg.get("status") or "").lower()
|
||||||
|
inst = str(leg.get("inst_id") or "").strip()
|
||||||
|
oid = str(leg.get("exchange_ord_id") or "").strip()
|
||||||
|
if lst == "pending":
|
||||||
|
if ex is not None and inst and oid:
|
||||||
|
cancel_option_order(ex, inst_id=inst, ord_id=oid)
|
||||||
|
update_leg(
|
||||||
|
conn,
|
||||||
|
int(leg["id"]),
|
||||||
|
status="cancelled",
|
||||||
|
close_reason="manual_end",
|
||||||
|
closed_at=_now(),
|
||||||
|
avg_open=None,
|
||||||
|
premium=0,
|
||||||
|
)
|
||||||
|
notes.append(f"{inst or leg.get('leg_role')} 待补→cancelled")
|
||||||
|
|
||||||
|
update_plan(
|
||||||
|
conn,
|
||||||
|
int(plan_id),
|
||||||
|
status="closed",
|
||||||
|
close_reason="manual",
|
||||||
|
closed_at=_now(),
|
||||||
|
note=((plan.get("note") or "") + " · 人工结束(不平仓)").strip(" ·")[:500],
|
||||||
|
)
|
||||||
|
plan2 = get_plan(conn, int(plan_id))
|
||||||
|
if plan2:
|
||||||
|
try:
|
||||||
|
notify_plan_end(cfg, conn, plan2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"plan_id": int(plan_id),
|
||||||
|
"msg": "计划已结束(未自动平仓;有持仓请自行平掉)",
|
||||||
|
"notes": notes,
|
||||||
|
}
|
||||||
|
|||||||
@@ -588,6 +588,24 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
out["gates"] = gates
|
out["gates"] = gates
|
||||||
return jsonify(out), (200 if out.get("ok") else 400)
|
return jsonify(out), (200 if out.get("ok") else 400)
|
||||||
|
|
||||||
|
@app.route("/api/hedge-plan/<int:plan_id>/end", methods=["POST"])
|
||||||
|
@lr
|
||||||
|
def api_hedge_end_plan(plan_id: int):
|
||||||
|
"""人工结束进行中计划:不自动平仓;未成交腿改为 cancelled."""
|
||||||
|
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables
|
||||||
|
from lib.hedge_plan.hedge_plan_orders_lib import execute_manual_end_plan
|
||||||
|
|
||||||
|
conn = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
init_hedge_plan_tables(conn)
|
||||||
|
out = execute_manual_end_plan(cfg, conn, plan_id)
|
||||||
|
if not out.get("ok"):
|
||||||
|
return jsonify(out), 400
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return jsonify(out)
|
||||||
|
|
||||||
@app.route("/api/hedge-plan/<int:plan_id>/complete-leg", methods=["POST"])
|
@app.route("/api/hedge-plan/<int:plan_id>/complete-leg", methods=["POST"])
|
||||||
@lr
|
@lr
|
||||||
def api_hedge_complete_leg(plan_id: int):
|
def api_hedge_complete_leg(plan_id: int):
|
||||||
@@ -742,6 +760,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
init_hedge_plan_tables,
|
init_hedge_plan_tables,
|
||||||
list_plans,
|
list_plans,
|
||||||
)
|
)
|
||||||
|
from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs
|
||||||
|
|
||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
try:
|
||||||
@@ -750,6 +769,16 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
for status in ("opening", "active", "partial"):
|
for status in ("opening", "active", "partial"):
|
||||||
rows.extend(list_plans(conn, status=status, limit=80))
|
rows.extend(list_plans(conn, status=status, limit=80))
|
||||||
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
|
rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True)
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
reconcile_unfilled_option_legs(cfg, conn, int(row["id"]))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 校正后可能 status 变化,重新拉一遍
|
||||||
|
rows = []
|
||||||
|
for status in ("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)
|
plans = attach_legs_to_plans(conn, rows)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
@@ -779,6 +808,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
init_hedge_plan_tables,
|
init_hedge_plan_tables,
|
||||||
legs_contract_summary,
|
legs_contract_summary,
|
||||||
)
|
)
|
||||||
|
from lib.hedge_plan.hedge_plan_orders_lib import reconcile_unfilled_option_legs
|
||||||
|
|
||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
try:
|
||||||
@@ -786,6 +816,10 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
plan = get_plan(conn, plan_id)
|
plan = get_plan(conn, plan_id)
|
||||||
if not plan:
|
if not plan:
|
||||||
return jsonify({"ok": False, "msg": "计划不存在"}), 404
|
return jsonify({"ok": False, "msg": "计划不存在"}), 404
|
||||||
|
# 打开细节时校正:无成交却标 open → cancelled
|
||||||
|
if str(plan.get("status") or "") in ("opening", "active", "partial"):
|
||||||
|
reconcile_unfilled_option_legs(cfg, conn, plan_id)
|
||||||
|
plan = get_plan(conn, plan_id) or plan
|
||||||
legs = get_plan_legs(conn, plan_id)
|
legs = get_plan_legs(conn, plan_id)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -301,4 +301,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/hedge_plan.js?v=27"></script>
|
<script src="/static/hedge_plan.js?v=28"></script>
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""对冲计划:人工结束 + 未成交不显示 open."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from lib.hedge_plan.hedge_plan_db import (
|
||||||
|
get_plan,
|
||||||
|
get_plan_legs,
|
||||||
|
init_hedge_plan_tables,
|
||||||
|
insert_leg,
|
||||||
|
insert_plan,
|
||||||
|
legs_contract_summary,
|
||||||
|
)
|
||||||
|
from lib.hedge_plan.hedge_plan_orders_lib import (
|
||||||
|
execute_manual_end_plan,
|
||||||
|
reconcile_unfilled_option_legs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mem():
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
init_hedge_plan_tables(conn)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
class TestHedgePlanEnd(unittest.TestCase):
|
||||||
|
def test_legs_summary_marks_unfilled(self):
|
||||||
|
s = legs_contract_summary(
|
||||||
|
[
|
||||||
|
{"leg_role": "option_a", "inst_id": "ETH-C", "status": "pending"},
|
||||||
|
{"leg_role": "option_b", "inst_id": "ETH-P", "status": "open"},
|
||||||
|
{"leg_role": "option_a", "inst_id": "X", "status": "cancelled"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertIn("(待补)", s)
|
||||||
|
self.assertIn("(未成交)", s)
|
||||||
|
|
||||||
|
def test_reconcile_ghost_open_to_pending(self):
|
||||||
|
conn = _mem()
|
||||||
|
pid = insert_plan(
|
||||||
|
conn,
|
||||||
|
{
|
||||||
|
"plan_type": "options_options",
|
||||||
|
"status": "active",
|
||||||
|
"underlying": "ETH",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
insert_leg(
|
||||||
|
conn,
|
||||||
|
{
|
||||||
|
"plan_id": pid,
|
||||||
|
"leg_role": "option_a",
|
||||||
|
"inst_id": "ETH-USD_UM-260720-1870-C",
|
||||||
|
"status": "open",
|
||||||
|
"exchange_ord_id": "1",
|
||||||
|
"avg_open": 0.01,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
insert_leg(
|
||||||
|
conn,
|
||||||
|
{
|
||||||
|
"plan_id": pid,
|
||||||
|
"leg_role": "option_b",
|
||||||
|
"inst_id": "ETH-USD_UM-260720-1870-P",
|
||||||
|
"status": "open",
|
||||||
|
"exchange_ord_id": "2",
|
||||||
|
"avg_open": 0.02,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cfg = {"exchange_options": object()}
|
||||||
|
with mock.patch(
|
||||||
|
"lib.hedge_plan.hedge_plan_orders_lib._live_option_pos_sheets",
|
||||||
|
side_effect=lambda ex, inst: 0.0 if "C" in inst else 50.0,
|
||||||
|
), mock.patch(
|
||||||
|
"lib.exchange.okx_options_lib.fetch_option_order",
|
||||||
|
return_value={"ok": True, "state": "canceled", "acc_fill_sz": 0},
|
||||||
|
):
|
||||||
|
notes = reconcile_unfilled_option_legs(cfg, conn, pid)
|
||||||
|
self.assertTrue(notes)
|
||||||
|
legs = {l["leg_role"]: l for l in get_plan_legs(conn, pid)}
|
||||||
|
self.assertEqual(legs["option_a"]["status"], "pending")
|
||||||
|
self.assertEqual(legs["option_b"]["status"], "open")
|
||||||
|
self.assertEqual(get_plan(conn, pid)["status"], "partial")
|
||||||
|
|
||||||
|
def test_manual_end_no_flat(self):
|
||||||
|
conn = _mem()
|
||||||
|
pid = insert_plan(
|
||||||
|
conn,
|
||||||
|
{
|
||||||
|
"plan_type": "options_options",
|
||||||
|
"status": "partial",
|
||||||
|
"underlying": "ETH",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
insert_leg(
|
||||||
|
conn,
|
||||||
|
{
|
||||||
|
"plan_id": pid,
|
||||||
|
"leg_role": "option_a",
|
||||||
|
"inst_id": "ETH-C",
|
||||||
|
"status": "pending",
|
||||||
|
"exchange_ord_id": "9",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
insert_leg(
|
||||||
|
conn,
|
||||||
|
{
|
||||||
|
"plan_id": pid,
|
||||||
|
"leg_role": "option_b",
|
||||||
|
"inst_id": "ETH-P",
|
||||||
|
"status": "open",
|
||||||
|
"exchange_ord_id": "8",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cfg = {"exchange_options": object()}
|
||||||
|
with mock.patch(
|
||||||
|
"lib.hedge_plan.hedge_plan_orders_lib.reconcile_unfilled_option_legs",
|
||||||
|
return_value=[],
|
||||||
|
), mock.patch(
|
||||||
|
"lib.exchange.okx_options_lib.cancel_option_order",
|
||||||
|
return_value={"ok": True},
|
||||||
|
) as cancel, mock.patch(
|
||||||
|
"lib.hedge_plan.hedge_plan_notify_lib.notify_plan_end",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
out = execute_manual_end_plan(cfg, conn, pid)
|
||||||
|
self.assertTrue(out.get("ok"))
|
||||||
|
self.assertEqual(get_plan(conn, pid)["status"], "closed")
|
||||||
|
self.assertEqual(get_plan(conn, pid)["close_reason"], "manual")
|
||||||
|
legs = {l["leg_role"]: l for l in get_plan_legs(conn, pid)}
|
||||||
|
self.assertEqual(legs["option_a"]["status"], "cancelled")
|
||||||
|
self.assertEqual(legs["option_b"]["status"], "open") # 已有持仓不动
|
||||||
|
cancel.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user