Add 期期 close mode (全平/到期平) with scheme-C env toggle.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -145,16 +145,17 @@
|
||||
|
||||
### 5.2 期期对冲
|
||||
|
||||
| 事件 | 盈利方 | 亏损方 | 计划是否结束 |
|
||||
|------|--------|--------|--------------|
|
||||
| **标的价到达用户目标价 S\*** | **自动平仓** | **不平**,持有至到期 | 平盈利腿后计划可标 `closing`;**全部腿终态后结束**(亏损腿到期后结账) |
|
||||
| **到期且整体无盈利** | — | 到期结算 | **算结束**;合计记 **总亏损**(通常 ≈ −全部权利金,或到期结算净值 < 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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -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",
|
||||
|
||||
Vendored
+6
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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'))}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user