diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index b01fc5f..20a7890 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -135,6 +135,10 @@ HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true # 方案C:期期页面显示「平仓模式」(到期平/全平);关则固定到期平.默认开启,页面默认选全平 HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true +# 期期「做多/做空」拆分口径:budget=按权利金预算(默认);sheets=先算同张数 n 再按比例拆 +HEDGE_PLAN_OO_BIAS_SPLIT_BY=budget +# 期期「做多/做空」主腿占比(0~1,默认 0.7=7:3);做多主腿=Call,做空主腿=Put +HEDGE_PLAN_OO_BIAS_RATIO=0.7 # 对冲与单独期权互斥(默认 true):有对冲计划不可单独开期权;有单独期权不可启动对冲;false=可同时开 HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE=true # 半腿失败改手动补开(默认 true):不自动平已成腿,计划挂 partial,页面补开;开启时下方自动平强制无效 diff --git a/docs/对冲计划开发方案.md b/docs/对冲计划开发方案.md index 2d1012f..219c1fa 100644 --- a/docs/对冲计划开发方案.md +++ b/docs/对冲计划开发方案.md @@ -96,8 +96,11 @@ - 预算:`B = min(交易户 USDC × OKX_OPTIONS_BUDGET_BUFFER, OKX_OPTIONS_TRADE_BUDGET_USDC)`(默认 buffer=0.95). - 自动张数(选齐两腿后写入,可手改): - **同张数**(默认):最大 `n` 使 `n×(cost_A+cost_B) ≤ B`,两腿均填 `n` - - **均分预算**:各用 `B/2` 反推张数(两腿可不同) + - **做多 / 做空**:须一 Call 一 Put;主:次默认 **7:3**(`HEDGE_PLAN_OO_BIAS_RATIO`,可改) + - 做多:主腿=Call;做空:主腿=Put + - 拆分口径 `HEDGE_PLAN_OO_BIAS_SPLIT_BY`:`budget`(默认,按权利金预算拆) / `sheets`(先算同张数 `n` 再按比例拆张数) - 另受各自卖一深度上限约束 + - 已移除页面「均分」;后端仍兼容旧 `split_budget` 入参(预算对半) ### 4.2 目标价 diff --git a/docs/更新文档.md b/docs/更新文档.md index 6e495d7..ce895cf 100644 --- a/docs/更新文档.md +++ b/docs/更新文档.md @@ -4,6 +4,37 @@ --- +## 2026-07-19 · 期期张数:做多/做空替代均分 + env 拆分口径 + +### 修改原因 + +期期「均分」与方向偏好无关;需要按 Call/Put 7:3(可配)做偏多/偏空自动张数,并可用 env 在「预算金额 / 张数」两种拆法间切换。 + +### 修改的地方 + +| 文件 | 改动摘要 | +|------|----------| +| `lib/hedge_plan/hedge_plan_calc_lib.py` | `long_bias`/`short_bias`;`budget`/`sheets` + `bias_ratio` | +| `lib/hedge_plan/hedge_plan_register.py` | gates 下发 `oo_bias_split_by` / `oo_bias_ratio` | +| `lib/hedge_plan/templates/hedge_plan_panel.html` | 张数段:同张数 / 做多 / 做空 | +| `lib/common/static/hedge_plan.js` | 前端建议张数与 env 同步 | +| `crypto_monitor_okx/.env.example` + env UI/schema | `HEDGE_PLAN_OO_BIAS_SPLIT_BY`、`HEDGE_PLAN_OO_BIAS_RATIO` | +| `docs/系统说明.md` 等 | 同步操作与配置说明 | + +### 达成的目标 + +1. 默认仍为同张数。 +2. 做多=Call 主占比、做空=Put 主占比;默认比例 0.7,口径默认预算金额。 +3. `sheets` 口径:先算同张数 `n` 再拆。 + +### 交付之后的验收 + +1. 期期页可见「同张数 / 做多 / 做空」,无「均分」。 +2. env 配置可改口径与比例;生产 OKX `.env` 已补齐键。 +3. 选一 Call 一 Put 后自动张数符合比例;非 C+P 时提示。 + +--- + ## 2026-07-17 · 修复 pip>=26 部署依赖安装失败 ### 修改原因 diff --git a/docs/期权开平仓与监控说明.md b/docs/期权开平仓与监控说明.md index 7e8a97d..d0999f9 100644 --- a/docs/期权开平仓与监控说明.md +++ b/docs/期权开平仓与监控说明.md @@ -83,6 +83,7 @@ 5. **无市价强平**:盘口真空时系统**不会**市价砸盘,仓位可能留到到期. 6. **目标位只看指数**:触达后仍受买一/2×门控约束,可能「到价却平不掉」. 7. **对冲计划腿**:期权腿退出规则见对冲方案;独立期权页平仓勿与计划状态脱节. +8. **期期自动张数**:对冲计划页为「同张数 / 做多 / 做空」(已无均分);做多/做空按 Call·Put 比例拆,口径与比例见 env `HEDGE_PLAN_OO_BIAS_SPLIT_BY`、`HEDGE_PLAN_OO_BIAS_RATIO`. --- @@ -91,3 +92,4 @@ - [期权用法.md](./期权用法.md) — 资金兑划与页面操作 - [期权方案.md](./期权方案.md) — env 与架构 - [对冲计划开发方案.md](./对冲计划开发方案.md) — 永期/期期与期权腿 +- [系统说明.md](./系统说明.md) — 实例操作与门禁总手册 diff --git a/docs/期权用法.md b/docs/期权用法.md index ec25efb..8466b2a 100644 --- a/docs/期权用法.md +++ b/docs/期权用法.md @@ -78,16 +78,26 @@ OKX_OPTIONS_API_PASSPHRASE=... 需已配置 `WECHAT_WEBHOOK`. -## 6. 与永续的关系 +## 6. 与永续 / 对冲计划的关系 | | 永续(子账户) | 期权(主账户) | |--|----------------|----------------| | API | `OKX_API_*` | `OKX_OPTIONS_API_*` | -| 页面 | 实盘下单 / 关键位 | 期权 | +| 页面 | 实盘下单 / 关键位 | 期权 · 对冲计划 | | 资金顶栏 | USDT 资金户+交易户 | 期权页单独显示 USDC 等 | 两套资金 **不合并** 显示. +**期期对冲张数**(对冲计划页,与单独开期权共用预算算法): + +| 模式 | 说明 | +|------|------| +| 同张数(默认) | 两腿同 `n`,总权利金 ≤ 预算 | +| 做多 | Call:Put 按主腿占比(默认 7:3) | +| 做空 | Put:Call 按主腿占比(默认 7:3) | + +拆分口径与比例见 env:`HEDGE_PLAN_OO_BIAS_SPLIT_BY`(`budget` 默认 / `sheets`)、`HEDGE_PLAN_OO_BIAS_RATIO`(默认 `0.7`)。细则见 [对冲计划开发方案.md](./对冲计划开发方案.md) §4.1、[系统说明.md](./系统说明.md)。 + ## 7. 配置说明 | 变量 | 默认 | 含义 | @@ -97,6 +107,8 @@ OKX_OPTIONS_API_PASSPHRASE=... | `OKX_OPTIONS_MAX_DTE_DAYS` | 2 | 最多选几天内到期 | | `OKX_OPTIONS_ITM_MAX_DIST_USD` | 30 | 轻度实值:价内不超过多少 USD | | `OKX_OPTIONS_PROFIT_ALERT_RATIO` | 1.0 | 浮盈/权利金 ≥ 此值推送 | +| `HEDGE_PLAN_OO_BIAS_SPLIT_BY` | budget | 期期做多/做空:按预算或按张数拆 | +| `HEDGE_PLAN_OO_BIAS_RATIO` | 0.7 | 期期做多/做空主腿占比 | ## 8. 期权复盘(含对冲) diff --git a/docs/系统说明.md b/docs/系统说明.md index 5849eb5..f6e0b26 100644 --- a/docs/系统说明.md +++ b/docs/系统说明.md @@ -122,12 +122,14 @@ ### 3.3 操作:期期 1. 填上破 / 下破目标;指数价作参考。 -2. 张数模式:**同张数**(默认)或 **均分预算**;平仓模式:**全平**(默认)或 **到期平**(若 env 打开方案 C)。 -3. T 型报价选用两腿;可先划转 USDC。 +2. 张数模式:**同张数**(默认)、**做多**、**做空**;平仓模式:**全平**(默认)或 **到期平**(若 env 打开方案 C)。 +3. T 型报价选用两腿(做多/做空须一 Call 一 Put);可先划转 USDC。 4. **计算** → 情景测算 → **启动计划**。 逻辑摘要: +- **同张数**:最大 `n` 使两腿各 `n` 张且总权利金 ≤ 预算。 +- **做多 / 做空**:主腿与次腿按 env 比例(默认 7:3)分配;做多主腿=Call,做空主腿=Put。拆分口径由 `HEDGE_PLAN_OO_BIAS_SPLIT_BY` 决定:`budget`(默认,按权利金预算拆)或 `sheets`(先算同张数 `n` 再拆张数)。 - 达目标价:通常只平盈利腿。 - **全平**:盈利腿平掉后立刻尝试清另一腿(无 2× 权利金门控,失败会重试)。 - **到期平**:残腿持有至到期再结。 @@ -320,6 +322,8 @@ A:可能是仅提醒类型、门控未过、RR 不足、满仓/日上限,或 | 半腿失败改手动补开 | 见 3.4 | | 半腿失败时自动平期权 | 手动补开开启时强制无效 | | 期期平仓模式(方案 C) | 页面是否出现「全平 / 到期平」 | +| 期期做多做空拆分口径 | `budget` 预算金额(默认)/ `sheets` 张数 | +| 期期做多做空主腿占比 | 默认 `0.7`(即 7:3) | 含「需重启」标记的项保存后要用「保存并重启」;对冲多数开关可热更,以页面标注为准。 diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 930cabc..adc8404 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -29,6 +29,8 @@ legB: null, market: null, ooSheetsMode: "same_sheets", + ooBiasSplitBy: "budget", + ooBiasRatio: 0.7, ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0", ooCloseMode: "close_all", direction: "long", @@ -335,6 +337,20 @@ return Math.min(out, Math.floor(d + 1e-12)); } + function normalizeOptCP(ot) { + const u = String(ot || "").toUpperCase(); + if (u.indexOf("C") === 0) return "C"; + if (u.indexOf("P") === 0) return "P"; + return ""; + } + + function ooSizeModeLabel(mode) { + if (mode === "long_bias") return "做多"; + if (mode === "short_bias") return "做空"; + if (mode === "split_budget") return "均分"; + return "同张数"; + } + function suggestOoSheetsLocal(budget, mode) { const costA = unitCost(state.legA); const costB = unitCost(state.legB); @@ -344,24 +360,61 @@ if (!(costA > 0) || !(costB > 0)) { return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "缺少有效卖一价,无法建议张数" }; } + let ratio = Number(state.ooBiasRatio); + if (!(ratio > 0) || !(ratio < 1)) ratio = 0.7; + const splitBy = state.ooBiasSplitBy === "sheets" ? "sheets" : "budget"; + const pair = costA + costB; + let nSame = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0; + nSame = capByDepth(nSame, state.legA && state.legA.ask_sz); + nSame = capByDepth(nSame, state.legB && state.legB.ask_sz); + let nA = 0; let nB = 0; - if (mode === "split_budget") { + if (mode === "long_bias" || mode === "short_bias") { + const aCP = normalizeOptCP(state.legA && state.legA.opt_type); + const bCP = normalizeOptCP(state.legB && state.legB.opt_type); + if (!((aCP === "C" && bCP === "P") || (aCP === "P" && bCP === "C"))) { + return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "做多/做空需一腿 Call、一腿 Put" }; + } + const callIsA = aCP === "C"; + const majorIsCall = mode === "long_bias"; + let nCall = 0; + let nPut = 0; + if (splitBy === "sheets") { + if (nSame < 2) { + return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "同张数规模不足 2,无法按比例拆分" }; + } + let majorN = Math.round(nSame * ratio); + majorN = Math.max(1, Math.min(majorN, nSame - 1)); + const minorN = nSame - majorN; + nCall = majorIsCall ? majorN : minorN; + nPut = majorIsCall ? minorN : majorN; + } else { + const majBudget = budget * ratio; + const minBudget = budget * (1 - ratio); + const costCall = callIsA ? costA : costB; + const costPut = callIsA ? costB : costA; + if (majorIsCall) { + nCall = Math.floor(majBudget / costCall + 1e-12); + nPut = Math.floor(minBudget / costPut + 1e-12); + } else { + nPut = Math.floor(majBudget / costPut + 1e-12); + nCall = Math.floor(minBudget / costCall + 1e-12); + } + } + nA = callIsA ? nCall : nPut; + nB = callIsA ? nPut : nCall; + nA = capByDepth(nA, state.legA && state.legA.ask_sz); + nB = capByDepth(nB, state.legB && state.legB.ask_sz); + } else if (mode === "split_budget") { const half = budget / 2; nA = Math.floor(half / costA + 1e-12); nB = Math.floor(half / costB + 1e-12); + nA = capByDepth(nA, state.legA && state.legA.ask_sz); + nB = capByDepth(nB, state.legB && state.legB.ask_sz); } else { - const pair = costA + costB; - const n = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0; - nA = n; - nB = n; - } - nA = capByDepth(nA, state.legA && state.legA.ask_sz); - nB = capByDepth(nB, state.legB && state.legB.ask_sz); - if (mode !== "split_budget") { - const n = Math.min(nA, nB); - nA = n; - nB = n; + nA = nSame; + nB = nSame; } const premium = costA * nA + costB * nB; const ok = nA >= 1 && nB >= 1; @@ -400,7 +453,7 @@ const line = $("hp-oo-budget-line"); if (!line) return; const b = resolveOoBudget(); - const sizeLabel = state.ooSheetsMode === "split_budget" ? "均分" : "同张数"; + const sizeLabel = ooSizeModeLabel(state.ooSheetsMode); const closeLabel = !state.ooCloseModeEnabled ? "" : state.ooCloseMode === "hold_expiry" @@ -447,8 +500,15 @@ } else if (d.oo_close_mode_default && !state._ooCloseModeTouched) { state.ooCloseMode = d.oo_close_mode_default === "hold_expiry" ? "hold_expiry" : "close_all"; } + if (d.oo_bias_split_by != null) { + state.ooBiasSplitBy = d.oo_bias_split_by === "sheets" ? "sheets" : "budget"; + } + if (d.oo_bias_ratio != null && Number(d.oo_bias_ratio) > 0 && Number(d.oo_bias_ratio) < 1) { + state.ooBiasRatio = Number(d.oo_bias_ratio); + } syncOoCloseModeUI(); setGateLine(d); + if (state.mode === "options_options") autoFillOoSheets(); } catch (e) { setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false }); } diff --git a/lib/env/env_schema.py b/lib/env/env_schema.py index bbc3ff2..0f9cb9e 100644 --- a/lib/env/env_schema.py +++ b/lib/env/env_schema.py @@ -91,6 +91,8 @@ HOT_RELOAD_EXACT = frozenset({ "HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", "HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", "HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", + "HEDGE_PLAN_OO_BIAS_SPLIT_BY", + "HEDGE_PLAN_OO_BIAS_RATIO", "HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE", "HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL", "MAX_ACTIVE_HEDGE_PLANS", @@ -121,6 +123,10 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = { ("long_only", "仅做多"), ("short_only", "仅做空"), ), + "HEDGE_PLAN_OO_BIAS_SPLIT_BY": ( + ("budget", "预算金额"), + ("sheets", "张数"), + ), } _SELECT_ALIASES: dict[str, dict[str, str]] = { diff --git a/lib/env/env_ui_manifest.py b/lib/env/env_ui_manifest.py index b29a05b..14134d4 100644 --- a/lib/env/env_ui_manifest.py +++ b/lib/env/env_ui_manifest.py @@ -146,6 +146,16 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = { "期期平仓模式(方案C)", "默认 true;开启后页面可选「到期平/全平」(盈利腿平后另一腿);关闭则固定到期平", ), + ( + "HEDGE_PLAN_OO_BIAS_SPLIT_BY", + "期期做多做空拆分口径", + "默认预算金额;budget=按权利金预算按比例分两腿;sheets=先算同张数 n 再按比例拆张数", + ), + ( + "HEDGE_PLAN_OO_BIAS_RATIO", + "期期做多做空主腿占比", + "默认 0.7(即 7:3);做多主腿=Call,做空主腿=Put;须在 0~1 之间", + ), ( "HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE", "对冲与期权互斥门控", @@ -177,6 +187,8 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = { "HEDGE_PLAN_SHOW_PERP_OPTIONS": "true", "HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true", "HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true", + "HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget", + "HEDGE_PLAN_OO_BIAS_RATIO": "0.7", "HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true", "HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true", } diff --git a/lib/hedge_plan/hedge_plan_calc_lib.py b/lib/hedge_plan/hedge_plan_calc_lib.py index 5008f3b..44a114b 100644 --- a/lib/hedge_plan/hedge_plan_calc_lib.py +++ b/lib/hedge_plan/hedge_plan_calc_lib.py @@ -150,6 +150,54 @@ def _cap_sheets_by_ask_depth(sheets: int, ask_sz: Any) -> int: return min(n, int(math.floor(float(depth) + 1e-12))) +def _normalize_oo_sheets_mode(mode: str) -> str: + m = (mode or "same_sheets").strip().lower() + if m in ("long_bias", "bias_long", "long", "做多"): + return "long_bias" + if m in ("short_bias", "bias_short", "short", "做空"): + return "short_bias" + # 旧「均分」兼容:按预算 50/50(页面已移除) + if m in ("split", "equal_budget", "split_budget", "均分"): + return "split_budget" + return "same_sheets" + + +def _normalize_oo_bias_split_by(raw: Any) -> str: + v = str(raw or "budget").strip().lower() + if v in ("sheets", "qty", "quantity", "张数"): + return "sheets" + return "budget" + + +def _clamp_oo_bias_ratio(raw: Any, default: float = 0.7) -> float: + try: + r = float(raw) + except (TypeError, ValueError): + r = float(default) + if r <= 0 or r >= 1: + r = float(default) + return r + + +def _oo_call_put_leg_index(opt_type_a: str, opt_type_b: str) -> tuple[Optional[str], Optional[str], str]: + """返回 (call_side, put_side, err);side 为 'a'/'b'.""" + a = (opt_type_a or "").strip().upper() + b = (opt_type_b or "").strip().upper() + if a.startswith("C"): + a = "C" + elif a.startswith("P"): + a = "P" + if b.startswith("C"): + b = "C" + elif b.startswith("P"): + b = "P" + if {a, b} != {"C", "P"}: + return None, None, "做多/做空需一腿 Call、一腿 Put" + call_side = "a" if a == "C" else "b" + put_side = "b" if call_side == "a" else "a" + return call_side, put_side, "" + + def suggest_oo_sheets( *, mode: str, @@ -157,63 +205,89 @@ def suggest_oo_sheets( ask_a: float, ct_mult_a: float = 0.01, ask_sz_a: Any = None, + opt_type_a: str = "", ask_b: float, ct_mult_b: float = 0.01, ask_sz_b: Any = None, + opt_type_b: str = "", + bias_split_by: str = "budget", + bias_ratio: float = 0.7, ) -> dict[str, Any]: - """期期建议张数:same_sheets(同张数,默认) / split_budget(均分预算).""" + """期期建议张数:same_sheets / long_bias / short_bias(及旧 split_budget).""" import math - m = (mode or "same_sheets").strip().lower() - if m in ("split", "equal_budget", "split_budget", "均分"): - m = "split_budget" - else: - m = "same_sheets" + m = _normalize_oo_sheets_mode(mode) + split_by = _normalize_oo_bias_split_by(bias_split_by) + ratio = _clamp_oo_bias_ratio(bias_ratio) budget = max(0.0, float(budget_usdc or 0.0)) cost_a = option_unit_cost_usdc(ask=ask_a, ct_mult=ct_mult_a) cost_b = option_unit_cost_usdc(ask=ask_b, ct_mult=ct_mult_b) + + def _fail(msg: str, n_a: int = 0, n_b: int = 0) -> dict[str, Any]: + return { + "mode": m, + "sheets_a": n_a, + "sheets_b": n_b, + "cost_a": round(cost_a, 8), + "cost_b": round(cost_b, 8), + "premium_est": round(cost_a * n_a + cost_b * n_b, 6), + "ok": False, + "msg": msg, + "bias_split_by": split_by, + "bias_ratio": ratio, + } + if budget <= 0: - return { - "mode": m, - "sheets_a": 0, - "sheets_b": 0, - "cost_a": round(cost_a, 8), - "cost_b": round(cost_b, 8), - "premium_est": 0.0, - "ok": False, - "msg": "可用预算为 0", - } + return _fail("可用预算为 0") if cost_a <= 0 or cost_b <= 0: - return { - "mode": m, - "sheets_a": 0, - "sheets_b": 0, - "cost_a": round(cost_a, 8), - "cost_b": round(cost_b, 8), - "premium_est": 0.0, - "ok": False, - "msg": "缺少有效卖一价,无法建议张数", - } - if m == "split_budget": + return _fail("缺少有效卖一价,无法建议张数") + + pair = cost_a + cost_b + n_same = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0 + n_same = _cap_sheets_by_ask_depth(n_same, ask_sz_a) + n_same = _cap_sheets_by_ask_depth(n_same, ask_sz_b) + + if m == "same_sheets": + n_a = n_same + n_b = n_same + elif m == "split_budget": half = budget / 2.0 n_a = int(math.floor(half / cost_a + 1e-12)) n_b = int(math.floor(half / cost_b + 1e-12)) + n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a) + n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b) else: - pair = cost_a + cost_b - n = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0 - n_a = n - n_b = n - n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a) - n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b) - if m == "same_sheets": - n = min(n_a, n_b) - n_a = n - n_b = n + call_side, put_side, err = _oo_call_put_leg_index(opt_type_a, opt_type_b) + if err: + return _fail(err) + major_is_call = m == "long_bias" + if split_by == "sheets": + if n_same < 2: + return _fail("同张数规模不足 2,无法按比例拆分") + major_n = int(round(n_same * ratio)) + major_n = max(1, min(major_n, n_same - 1)) + minor_n = n_same - major_n + n_call = major_n if major_is_call else minor_n + n_put = minor_n if major_is_call else major_n + else: + maj_budget = budget * ratio + min_budget = budget * (1.0 - ratio) + cost_call = cost_a if call_side == "a" else cost_b + cost_put = cost_b if call_side == "a" else cost_a + if major_is_call: + n_call = int(math.floor(maj_budget / cost_call + 1e-12)) if cost_call > 0 else 0 + n_put = int(math.floor(min_budget / cost_put + 1e-12)) if cost_put > 0 else 0 + else: + n_put = int(math.floor(maj_budget / cost_put + 1e-12)) if cost_put > 0 else 0 + n_call = int(math.floor(min_budget / cost_call + 1e-12)) if cost_call > 0 else 0 + n_a = n_call if call_side == "a" else n_put + n_b = n_put if call_side == "a" else n_call + n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a) + n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b) + prem = cost_a * n_a + cost_b * n_b ok = n_a >= 1 and n_b >= 1 - msg = "" - if not ok: - msg = "预算不够开 1+1(或卖一深度不足)" + msg = "" if ok else "预算不够开 1+1(或卖一深度不足)" return { "mode": m, "sheets_a": n_a, @@ -223,6 +297,8 @@ def suggest_oo_sheets( "premium_est": round(prem, 6), "ok": ok, "msg": msg, + "bias_split_by": split_by, + "bias_ratio": ratio, } diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py index 4b37de8..51c67e5 100644 --- a/lib/hedge_plan/hedge_plan_register.py +++ b/lib/hedge_plan/hedge_plan_register.py @@ -120,6 +120,18 @@ def _oo_close_mode_enabled() -> bool: return _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True) +def _oo_bias_split_by() -> str: + from lib.hedge_plan.hedge_plan_calc_lib import _normalize_oo_bias_split_by + + return _normalize_oo_bias_split_by(os.getenv("HEDGE_PLAN_OO_BIAS_SPLIT_BY") or "budget") + + +def _oo_bias_ratio() -> float: + from lib.hedge_plan.hedge_plan_calc_lib import _clamp_oo_bias_ratio + + return _clamp_oo_bias_ratio(os.getenv("HEDGE_PLAN_OO_BIAS_RATIO") or "0.7") + + def _normalize_oo_close_mode(raw: Any) -> str: """方案C关闭时强制 hold_expiry;开启时默认 close_all.""" if not _oo_close_mode_enabled(): @@ -192,6 +204,8 @@ 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" + g["oo_bias_split_by"] = _oo_bias_split_by() + g["oo_bias_ratio"] = _oo_bias_ratio() return g diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index 459f4b8..79f2d85 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -135,7 +135,8 @@ 张数