Add 期期 close mode (全平/到期平) with scheme-C env toggle.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-19 08:06:27 +08:00
parent b3548240cc
commit 3119486105
13 changed files with 272 additions and 15 deletions
+2
View File
@@ -133,6 +133,8 @@ HEDGE_PLAN_OPEN_ORDER=options_first
HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true
HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false
HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true
# 方案C:期期页面显示「平仓模式」(到期平/全平);关则固定到期平.默认开启,页面默认选全平
HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true
MAX_ACTIVE_HEDGE_PLANS=1
HEDGE_PLAN_MONITOR_POLL_SECONDS=15
HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION=true
+2
View File
@@ -6789,6 +6789,8 @@ def render_main_page(page="trade", embed_mode=None):
in ("1", "true", "yes", "on"),
hedge_plan_show_options_options=os.getenv("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", "true").lower()
in ("1", "true", "yes", "on"),
hedge_plan_oo_close_mode_enabled=os.getenv("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", "true").lower()
in ("1", "true", "yes", "on"),
options_trade_budget=OKX_OPTIONS_TRADE_BUDGET_USDC,
options_default_underly=OKX_OPTIONS_DEFAULT_UNDERLY,
risk_status=risk_status,
+12 -9
View File
@@ -145,16 +145,17 @@
### 5.2 期期对冲
| 事件 | 盈利方 | 亏损方 | 计划是否结束 |
|------|--------|--------|--------------|
| **标的价到达用户目标价 S\*** | **自动平仓** | **不平**,持有至到期 | 平盈利腿后计划可标 `closing`;**全部腿终态后结束**(亏损腿到期后结账) |
| **到期且整体无盈利** | — | 到期结算 | **算结束**;合计记 **总亏损**(通常 ≈ −全部权利金,或到期结算净值 &lt; 0 的合计) |
| 到期时组合合计仍盈利 | — | 到期结算 | **算结束**;按实际结算盈亏入账 |
| 未达 S\* 至到期 | 两腿均到期 | | 同上,按结算合计结束 |
| 事件 | 盈利方 | 另一腿(残腿) | 计划是否结束 |
|------|--------|--------------|--------------|
| **标的价到达目标 + 平仓模式=到期平** | **自动平仓** | **不平**,持有至到期(`hold_expiry`) | 平盈利腿后仍 `active`;残腿到期后结账 |
| **标的价到达目标 + 平仓模式=全平**(默认) | **自动平仓** | **随即买一清残腿**(无 2×门控,失败则每轮重试) | 两腿都平完后 `closed` |
| **到期且整体无盈利** | — | 到期结算 | **算结束**;合计记 **总亏损** |
| **到期时组合合计仍盈利** | — | 到期结算 | **算结束**;按实际结算盈亏入账 |
判定「整体无盈利」:到期(或计划收口)时 `realized_pnl_total ≤ 0`(含双腿权利金全损).
盈利方判定规则仍按前文(触达 S\* 时按浮盈较大一侧平仓;皆亏则等到期).
- 界面「平仓模式」仅控制**盈利腿已平之后**另一腿的处理;须 `HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true`(默认开)才显示,页面默认选 **全平**.
- 关闭方案C开关时行为固定为 **到期平**.
- 判定「整体无盈利」:到期(或计划收口)时 `realized_pnl_total ≤ 0`(含双腿权利金全损).
- 盈利方判定:触达上破/下破时按浮盈较大一侧平仓;皆亏则等到期.
### 5.2.1 期权腿实盘平仓执行(与期权页共用)
@@ -564,6 +565,7 @@ realized_pnl_total = pnl_option_close - abs(pnl_perp_sl)
| `HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS` | 永期止损后强制平期权 | true | bool | 热更 | **保护机制,默认 true** |
| `HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS` | 永期止盈后强制平期权 | false | bool | 热更 | **默认 false,保险腿不平** |
| `HEDGE_PLAN_OO_CLOSE_WINNER_ONLY` | 期期只平盈利腿 | true | bool | 热更 | 达目标价只平盈利方 |
| `HEDGE_PLAN_OO_CLOSE_MODE_ENABLED` | 期期平仓模式(方案C) | true | bool | 热更 | 开:页面可选到期平/全平;关:固定到期平 |
| `MAX_ACTIVE_HEDGE_PLANS` | 最大同时活跃计划数 | 1 | number | 热更 | 建议保持 1 |
| `HEDGE_PLAN_MONITOR_POLL_SECONDS` | 对冲监控轮询(秒) | 15 | number | 热更 | 侦测 TP/SL/目标价 |
| `HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION` | 半腿失败时自动平期权 | true | bool | 热更 | 期权成、永续败时的补偿 |
@@ -590,6 +592,7 @@ HEDGE_PLAN_OPEN_ORDER=options_first
HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true
HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false
HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true
HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true
MAX_ACTIVE_HEDGE_PLANS=1
HEDGE_PLAN_MONITOR_POLL_SECONDS=15
HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION=true
+34
View File
@@ -29,6 +29,8 @@
legB: null,
market: null,
ooSheetsMode: "same_sheets",
ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0",
ooCloseMode: "close_all",
tradingUsdc: null,
tradeBudgetUsdc: null,
budgetBuffer: 0.95,
@@ -295,6 +297,19 @@
});
}
function syncOoCloseModeUI() {
const enabled = !!state.ooCloseModeEnabled;
const row = $("hp-oo-close-mode-row");
const hint = $("hp-oo-close-mode-hint");
if (row) row.classList.toggle("hidden", !enabled);
if (hint) hint.classList.toggle("hidden", !enabled);
if (!enabled) state.ooCloseMode = "hold_expiry";
document.querySelectorAll(".hp-oo-close-mode").forEach(function (b) {
const on = b.getAttribute("data-oo-close") === state.ooCloseMode;
b.classList.toggle("active", on);
});
}
function updateOoBudgetLine(extra) {
const line = $("hp-oo-budget-line");
if (!line) return;
@@ -331,6 +346,15 @@
async function loadGates() {
try {
const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode));
if (d.oo_close_mode_enabled != null) {
state.ooCloseModeEnabled = !!d.oo_close_mode_enabled;
}
if (!state.ooCloseModeEnabled) {
state.ooCloseMode = "hold_expiry";
} else if (d.oo_close_mode_default && !state._ooCloseModeTouched) {
state.ooCloseMode = d.oo_close_mode_default === "hold_expiry" ? "hold_expiry" : "close_all";
}
syncOoCloseModeUI();
setGateLine(d);
} catch (e) {
setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false });
@@ -953,6 +977,14 @@
autoFillOoSheets();
});
});
document.querySelectorAll(".hp-oo-close-mode").forEach(function (b) {
b.addEventListener("click", function () {
if (!state.ooCloseModeEnabled) return;
state.ooCloseMode = b.getAttribute("data-oo-close") || "close_all";
state._ooCloseModeTouched = true;
syncOoCloseModeUI();
});
});
if ($("hp-preview-btn"))
$("hp-preview-btn").addEventListener("click", function () {
state.mode = "perp_options";
@@ -1346,6 +1378,7 @@
target_price_up: up,
target_price_down: down,
target_price: up,
oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry",
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
};
@@ -1410,6 +1443,7 @@
syncUnderlyingUI();
syncMoneyUI();
syncOoSizeModeUI();
syncOoCloseModeUI();
updateOoBudgetLine(null);
bind();
void refreshAll();
+6 -1
View File
@@ -3265,11 +3265,16 @@ html[data-theme="light"] .opt-be-dist-down {
gap: 8px;
margin: 6px 0 2px;
}
.hedge-plan-page-wrap .hp-oo-size-mode.active {
.hedge-plan-page-wrap .hp-oo-size-mode.active,
.hedge-plan-page-wrap .hp-oo-close-mode.active {
border-color: var(--accent, #00d4ff);
color: var(--text, #fff);
background: rgba(0, 212, 255, 0.12);
}
.hedge-plan-page-wrap #hp-oo-close-mode-row.hidden,
.hedge-plan-page-wrap #hp-oo-close-mode-hint.hidden {
display: none !important;
}
.hedge-plan-page-wrap #hp-oo-budget-line.hp-oo-budget-warn {
color: #ff8a8a;
}
+1
View File
@@ -90,6 +90,7 @@ HOT_RELOAD_EXACT = frozenset({
"HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
"HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
"MAX_ACTIVE_HEDGE_PLANS",
"HEDGE_PLAN_MONITOR_POLL_SECONDS",
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
+6
View File
@@ -141,6 +141,11 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = {
("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", "永期止盈后强制平期权", "默认 false,保险腿不平"),
("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", "期期只平盈利腿", "达目标价只平盈利方"),
(
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
"期期平仓模式(方案C)",
"默认 true;开启后页面可选「到期平/全平」(盈利腿平后另一腿);关闭则固定到期平",
),
("MAX_ACTIVE_HEDGE_PLANS", "最大同时活跃计划数", "建议 1"),
("HEDGE_PLAN_MONITOR_POLL_SECONDS", "对冲监控轮询(秒)", "默认 15"),
("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", "半腿失败时自动平期权", ""),
@@ -157,6 +162,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
}
+2
View File
@@ -72,6 +72,8 @@ def init_hedge_plan_tables(conn: sqlite3.Connection) -> None:
)
_ensure_column(conn, "hedge_plans", "target_price_up", "REAL")
_ensure_column(conn, "hedge_plans", "target_price_down", "REAL")
# close_all=盈利腿平后清残腿;hold_expiry=残腿持有至到期(现状)
_ensure_column(conn, "hedge_plans", "oo_close_mode", "TEXT")
def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None:
+160 -2
View File
@@ -92,6 +92,55 @@ def _notify_end_reload(cfg: dict[str, Any], conn: Any, plan_id: int) -> None:
notify_plan_end(cfg, conn, plan)
def resolve_oo_rest_close_mode(plan: dict[str, Any]) -> str:
"""盈利腿平后另一腿:close_all(全平) / hold_expiry(到期平).
- 方案C关闭 强制到期平
- 计划未写 oo_close_mode(旧单) 到期平,避免误清残腿
- 新开仓默认写入 close_all
"""
if not _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True):
return "hold_expiry"
raw = plan.get("oo_close_mode")
if raw is None or str(raw).strip() == "":
return "hold_expiry"
v = str(raw).strip().lower()
if v in ("hold_expiry", "hold_to_expiry", "expiry", "到期平"):
return "hold_expiry"
return "close_all"
def _oo_option_legs(legs: list[dict[str, Any]], *, statuses: tuple[str, ...]) -> list[dict[str, Any]]:
out = []
for x in legs:
if not str(x.get("leg_role") or "").startswith("option"):
continue
if str(x.get("status") or "") in statuses:
out.append(x)
return out
def _finalize_oo_all_closed(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]], *, reason: str
) -> dict[str, Any]:
closed_opts = _oo_option_legs(legs, statuses=("closed",))
total_opts = sum(float(x.get("realized_pnl") or 0) for x in closed_opts)
close_reason = reason or "oo_rest_closed"
bucket = "oo_target" if total_opts > 0 else "oo_expiry_loss"
update_plan(
conn,
int(plan["id"]),
status="closed",
close_reason=close_reason,
realized_pnl_options=round(total_opts, 4),
realized_pnl_total=round(total_opts, 4),
stats_bucket=bucket,
closed_at=_now(),
)
_notify_end_reload(cfg, conn, int(plan["id"]))
return {"plan_id": plan["id"], "close_reason": close_reason, "total": total_opts}
def _tick_one(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> Optional[dict[str, Any]]:
pt = plan.get("plan_type")
legs = get_plan_legs(conn, int(plan["id"]))
@@ -101,6 +150,9 @@ def _tick_one(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> Optional[
return r
if pt == "options_options":
r = _tick_oo_expiry(cfg, conn, plan, legs)
if r:
return r
r = _tick_oo_close_rest(cfg, conn, plan, legs)
if r:
return r
return _tick_oo_target(cfg, conn, plan, legs)
@@ -232,10 +284,88 @@ def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[di
return {"plan_id": plan["id"], "close_reason": reason, "total": total}
def _estimate_leg_close_pnl(leg: dict[str, Any], idx: Optional[float], bid: Optional[float]) -> float:
"""残腿平仓盈亏估算:优先买一回收 − 权利金;无买一则用内在价值."""
premium = float(leg.get("premium") or 0)
sheets = float(leg.get("size") or 1)
ct = float(leg.get("ct_mult") or 0.01)
if bid is not None and float(bid) > 0:
return float(bid) * sheets * ct - premium
if idx is None:
return -premium
strike = _sf(leg.get("strike")) or 0
o = (leg.get("opt_type") or "").upper()
intrinsic = max(0.0, idx - strike) if o == "C" else max(0.0, strike - idx)
return intrinsic * sheets * ct - premium
def _tick_oo_close_rest(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
"""盈利腿已平后:全平模式清残腿(无2×门控,买一失败则下轮重试)."""
if resolve_oo_rest_close_mode(plan) != "close_all":
return None
open_legs = _oo_option_legs(legs, statuses=("open",))
closed_legs = _oo_option_legs(legs, statuses=("closed",))
# 至少已平一条,且仍有残腿;避免双腿都还 open 时误清
if len(closed_legs) < 1 or len(open_legs) < 1:
return None
reason0 = str(plan.get("close_reason") or "")
allowed_reasons = (
"target_win_leg",
"target_up_win_leg",
"target_down_win_leg",
"oo_rest_closing",
"",
)
if reason0 not in allowed_reasons and not (
len(closed_legs) >= 1 and len(open_legs) == 1
):
return None
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
acted = False
for leg in list(open_legs):
close_r = _sell_option(
cfg, inst_id=str(leg.get("inst_id") or ""), sheets=float(leg.get("size") or 1)
)
if not close_r.get("ok"):
notify_hedge(
cfg,
build_hedge_alert_message(
title="期期全平·残腿平仓失败(将重试)",
plan_id=plan.get("id"),
detail=str(close_r.get("msg") or close_r),
),
)
update_plan(conn, int(plan["id"]), close_reason="oo_rest_closing")
return {"plan_id": plan["id"], "msg": "残腿平仓失败", "close": close_r, "retry": True}
bid = _sf(close_r.get("bid"))
pnl = _estimate_leg_close_pnl(leg, idx, bid)
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
("closed", "oo_rest_close", _now(), round(pnl, 4), leg["id"]),
)
leg["status"] = "closed"
leg["realized_pnl"] = round(pnl, 4)
acted = True
if not acted:
return None
legs2 = get_plan_legs(conn, int(plan["id"]))
still_open = _oo_option_legs(legs2, statuses=("open", "hold_to_expiry"))
if still_open:
update_plan(conn, int(plan["id"]), close_reason="oo_rest_closing")
return {"plan_id": plan["id"], "msg": "残腿部分已平,继续重试", "remaining": len(still_open)}
return _finalize_oo_all_closed(
cfg, conn, plan, legs2, reason="oo_rest_closed"
)
def _tick_oo_target(
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
) -> Optional[dict[str, Any]]:
"""期期:触及上破或下破目标价时平盈利腿."""
"""期期:触及上破或下破目标价时平盈利腿;按平仓模式处理另一腿."""
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
if idx is None:
return None
@@ -261,7 +391,7 @@ def _tick_oo_target(
return None
if not _env_bool("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", True):
return None
open_legs = [x for x in legs if x.get("status") == "open" and str(x.get("leg_role") or "").startswith("option")]
open_legs = _oo_option_legs(legs, statuses=("open",))
if len(open_legs) < 2:
return None
winners = []
@@ -292,17 +422,45 @@ def _tick_oo_target(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
("closed", reason, _now(), best_pnl, best["id"]),
)
rest_mode = resolve_oo_rest_close_mode(plan)
update_plan(conn, int(plan["id"]), close_reason=reason)
mid = dict(plan)
mid["close_reason"] = reason
mid["status"] = "active"
mid["oo_close_mode"] = rest_mode
notify_plan_end(cfg, conn, mid)
# 全平:同轮尝试清残腿;失败则下轮 _tick_oo_close_rest 重试
if rest_mode == "close_all":
legs2 = get_plan_legs(conn, int(plan["id"]))
rest = _tick_oo_close_rest(cfg, conn, mid, legs2)
out = {
"plan_id": plan["id"],
"close_reason": reason,
"hit_side": hit_side,
"closed_leg": best.get("id"),
"index": idx,
"oo_close_mode": rest_mode,
}
if rest:
out["rest"] = rest
return out
# 到期平:显式标记残腿 hold_to_expiry
for leg in open_legs:
if int(leg.get("id") or 0) == int(best.get("id") or 0):
continue
conn.execute(
"UPDATE hedge_plan_legs SET status=? WHERE id=?",
("hold_to_expiry", leg["id"]),
)
return {
"plan_id": plan["id"],
"close_reason": reason,
"hit_side": hit_side,
"closed_leg": best.get("id"),
"index": idx,
"oo_close_mode": rest_mode,
}
+9 -1
View File
@@ -81,6 +81,8 @@ def build_hedge_end_message(plan: dict[str, Any]) -> str:
"target_win_leg": "期期已平盈利腿(中间态)",
"target_up_win_leg": "期期上破·已平盈利腿",
"target_down_win_leg": "期期下破·已平盈利腿",
"oo_rest_closing": "期期全平·清残腿中",
"oo_rest_closed": "期期全平·两腿已平",
"oo_expiry_loss": "期期到期无盈利·总亏损",
"oo_expiry_win": "期期到期仍盈利",
"expiry": "到期收口",
@@ -150,14 +152,20 @@ def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> boo
"target_win_leg",
"target_up_win_leg",
"target_down_win_leg",
"oo_rest_closing",
) and (plan.get("status") or "") != "closed":
side = "上破" if "up" in str(plan.get("close_reason")) else (
"下破" if "down" in str(plan.get("close_reason")) else "目标价"
)
mode = (plan.get("oo_close_mode") or "").strip().lower()
if mode in ("close_all", "全平"):
rest_txt = "另一腿将全平(买一清残腿,无2×门控,失败重试)"
else:
rest_txt = "另一腿到期平(持有至到期结算)"
notify_hedge(
cfg,
build_hedge_alert_message(
title=f"期期{side}已平盈利腿,亏损腿继续持有至到期",
title=f"期期{side}已平盈利腿 · {rest_txt}",
plan_id=plan.get("id"),
detail=(
f"上破 {_fmt(plan.get('target_price_up') or plan.get('target_price'))}"
+23 -1
View File
@@ -116,6 +116,20 @@ def _show_options_options() -> bool:
return _env_bool("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", True)
def _oo_close_mode_enabled() -> bool:
return _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True)
def _normalize_oo_close_mode(raw: Any) -> str:
"""方案C关闭时强制 hold_expiry;开启时默认 close_all."""
if not _oo_close_mode_enabled():
return "hold_expiry"
v = str(raw or "close_all").strip().lower()
if v in ("hold_expiry", "hold_to_expiry", "expiry", "到期平"):
return "hold_expiry"
return "close_all"
def _live_order() -> bool:
return _env_bool("HEDGE_PLAN_LIVE_ORDER", False)
@@ -155,6 +169,13 @@ def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
)
def _gates_public(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
g = _gates_dict(cfg, plan_type)
g["oo_close_mode_enabled"] = _oo_close_mode_enabled()
g["oo_close_mode_default"] = "close_all" if _oo_close_mode_enabled() else "hold_expiry"
return g
def _maybe_start_monitor(cfg: dict[str, Any]) -> None:
if not _hedge_enabled():
return
@@ -298,6 +319,7 @@ 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")),
"opened_at": result.get("opened_at"),
},
)
@@ -347,7 +369,7 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
@lr
def api_hedge_gates():
plan_type = (request.args.get("plan_type") or "perp_options").strip()
return jsonify({"ok": True, **_gates_dict(cfg, plan_type)})
return jsonify({"ok": True, **_gates_public(cfg, plan_type)})
@app.route("/api/hedge-plan/market")
@lr
@@ -4,6 +4,7 @@
data-options-enabled="{{ '1' if options_enabled else '0' }}"
data-show-perp="{{ '1' if hedge_plan_show_perp_options | default(true) else '0' }}"
data-show-oo="{{ '1' if hedge_plan_show_options_options | default(true) else '0' }}"
data-oo-close-mode-enabled="{{ '1' if hedge_plan_oo_close_mode_enabled | default(true) else '0' }}"
data-sizing-mode="{{ position_sizing_mode | default('risk') }}"
data-is-full-margin="{{ '1' if position_sizing_mode == 'full_margin' else '0' }}">
{% if not hedge_plan_enabled %}
@@ -143,6 +144,12 @@
<button type="button" class="btn-secondary hp-oo-size-mode active" data-oo-size="same_sheets">同张数</button>
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="split_budget">均分预算</button>
</div>
<div class="form-row hp-oo-size-row" id="hp-oo-close-mode-row">
<span class="muted" title="仅控制盈利腿平掉后的另一腿">平仓模式</span>
<button type="button" class="btn-secondary hp-oo-close-mode active" data-oo-close="close_all">全平</button>
<button type="button" class="btn-secondary hp-oo-close-mode" data-oo-close="hold_expiry">到期平</button>
</div>
<p class="muted" id="hp-oo-close-mode-hint">盈利腿平后:全平=立刻买一清另一腿(无2×,失败重试);到期平=另一腿持有至到期</p>
<p class="muted" id="hp-oo-budget-line"></p>
<div id="hp-oo-legs" class="hp-oo-legs">
<div class="hp-oo-leg-row" data-leg="a">
@@ -270,4 +277,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=15"></script>
<script src="/static/hedge_plan.js?v=16"></script>
+7
View File
@@ -12,6 +12,7 @@ from lib.hedge_plan.hedge_plan_calc_lib import (
resolve_oo_budget_usdc,
suggest_oo_sheets,
)
from lib.hedge_plan.hedge_plan_monitor_lib import resolve_oo_rest_close_mode
class TestHedgePlanCalc(unittest.TestCase):
@@ -180,6 +181,12 @@ class TestHedgePlanCalc(unittest.TestCase):
self.assertEqual(s["sheets_a"], 2)
self.assertEqual(s["sheets_b"], 2)
def test_oo_rest_close_mode_default_close_all(self):
self.assertEqual(resolve_oo_rest_close_mode({"oo_close_mode": "close_all"}), "close_all")
self.assertEqual(resolve_oo_rest_close_mode({"oo_close_mode": "hold_expiry"}), "hold_expiry")
# 旧计划无字段:保持到期平,避免部署后误清残腿
self.assertEqual(resolve_oo_rest_close_mode({}), "hold_expiry")
if __name__ == "__main__":
unittest.main()