Replace 期期 equal-split with long/short bias sizing and env ratio controls.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -135,6 +135,10 @@ HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false
|
|||||||
HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true
|
HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true
|
||||||
# 方案C:期期页面显示「平仓模式」(到期平/全平);关则固定到期平.默认开启,页面默认选全平
|
# 方案C:期期页面显示「平仓模式」(到期平/全平);关则固定到期平.默认开启,页面默认选全平
|
||||||
HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true
|
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=可同时开
|
# 对冲与单独期权互斥(默认 true):有对冲计划不可单独开期权;有单独期权不可启动对冲;false=可同时开
|
||||||
HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE=true
|
HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE=true
|
||||||
# 半腿失败改手动补开(默认 true):不自动平已成腿,计划挂 partial,页面补开;开启时下方自动平强制无效
|
# 半腿失败改手动补开(默认 true):不自动平已成腿,计划挂 partial,页面补开;开启时下方自动平强制无效
|
||||||
|
|||||||
+4
-1
@@ -96,8 +96,11 @@
|
|||||||
- 预算:`B = min(交易户 USDC × OKX_OPTIONS_BUDGET_BUFFER, OKX_OPTIONS_TRADE_BUDGET_USDC)`(默认 buffer=0.95).
|
- 预算:`B = min(交易户 USDC × OKX_OPTIONS_BUDGET_BUFFER, OKX_OPTIONS_TRADE_BUDGET_USDC)`(默认 buffer=0.95).
|
||||||
- 自动张数(选齐两腿后写入,可手改):
|
- 自动张数(选齐两腿后写入,可手改):
|
||||||
- **同张数**(默认):最大 `n` 使 `n×(cost_A+cost_B) ≤ B`,两腿均填 `n`
|
- **同张数**(默认):最大 `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 目标价
|
### 4.2 目标价
|
||||||
|
|
||||||
|
|||||||
@@ -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 部署依赖安装失败
|
## 2026-07-17 · 修复 pip>=26 部署依赖安装失败
|
||||||
|
|
||||||
### 修改原因
|
### 修改原因
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
5. **无市价强平**:盘口真空时系统**不会**市价砸盘,仓位可能留到到期.
|
5. **无市价强平**:盘口真空时系统**不会**市价砸盘,仓位可能留到到期.
|
||||||
6. **目标位只看指数**:触达后仍受买一/2×门控约束,可能「到价却平不掉」.
|
6. **目标位只看指数**:触达后仍受买一/2×门控约束,可能「到价却平不掉」.
|
||||||
7. **对冲计划腿**:期权腿退出规则见对冲方案;独立期权页平仓勿与计划状态脱节.
|
7. **对冲计划腿**:期权腿退出规则见对冲方案;独立期权页平仓勿与计划状态脱节.
|
||||||
|
8. **期期自动张数**:对冲计划页为「同张数 / 做多 / 做空」(已无均分);做多/做空按 Call·Put 比例拆,口径与比例见 env `HEDGE_PLAN_OO_BIAS_SPLIT_BY`、`HEDGE_PLAN_OO_BIAS_RATIO`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -91,3 +92,4 @@
|
|||||||
- [期权用法.md](./期权用法.md) — 资金兑划与页面操作
|
- [期权用法.md](./期权用法.md) — 资金兑划与页面操作
|
||||||
- [期权方案.md](./期权方案.md) — env 与架构
|
- [期权方案.md](./期权方案.md) — env 与架构
|
||||||
- [对冲计划开发方案.md](./对冲计划开发方案.md) — 永期/期期与期权腿
|
- [对冲计划开发方案.md](./对冲计划开发方案.md) — 永期/期期与期权腿
|
||||||
|
- [系统说明.md](./系统说明.md) — 实例操作与门禁总手册
|
||||||
|
|||||||
+14
-2
@@ -78,16 +78,26 @@ OKX_OPTIONS_API_PASSPHRASE=...
|
|||||||
|
|
||||||
需已配置 `WECHAT_WEBHOOK`.
|
需已配置 `WECHAT_WEBHOOK`.
|
||||||
|
|
||||||
## 6. 与永续的关系
|
## 6. 与永续 / 对冲计划的关系
|
||||||
|
|
||||||
| | 永续(子账户) | 期权(主账户) |
|
| | 永续(子账户) | 期权(主账户) |
|
||||||
|--|----------------|----------------|
|
|--|----------------|----------------|
|
||||||
| API | `OKX_API_*` | `OKX_OPTIONS_API_*` |
|
| API | `OKX_API_*` | `OKX_OPTIONS_API_*` |
|
||||||
| 页面 | 实盘下单 / 关键位 | 期权 |
|
| 页面 | 实盘下单 / 关键位 | 期权 · 对冲计划 |
|
||||||
| 资金顶栏 | USDT 资金户+交易户 | 期权页单独显示 USDC 等 |
|
| 资金顶栏 | 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. 配置说明
|
## 7. 配置说明
|
||||||
|
|
||||||
| 变量 | 默认 | 含义 |
|
| 变量 | 默认 | 含义 |
|
||||||
@@ -97,6 +107,8 @@ OKX_OPTIONS_API_PASSPHRASE=...
|
|||||||
| `OKX_OPTIONS_MAX_DTE_DAYS` | 2 | 最多选几天内到期 |
|
| `OKX_OPTIONS_MAX_DTE_DAYS` | 2 | 最多选几天内到期 |
|
||||||
| `OKX_OPTIONS_ITM_MAX_DIST_USD` | 30 | 轻度实值:价内不超过多少 USD |
|
| `OKX_OPTIONS_ITM_MAX_DIST_USD` | 30 | 轻度实值:价内不超过多少 USD |
|
||||||
| `OKX_OPTIONS_PROFIT_ALERT_RATIO` | 1.0 | 浮盈/权利金 ≥ 此值推送 |
|
| `OKX_OPTIONS_PROFIT_ALERT_RATIO` | 1.0 | 浮盈/权利金 ≥ 此值推送 |
|
||||||
|
| `HEDGE_PLAN_OO_BIAS_SPLIT_BY` | budget | 期期做多/做空:按预算或按张数拆 |
|
||||||
|
| `HEDGE_PLAN_OO_BIAS_RATIO` | 0.7 | 期期做多/做空主腿占比 |
|
||||||
|
|
||||||
## 8. 期权复盘(含对冲)
|
## 8. 期权复盘(含对冲)
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -122,12 +122,14 @@
|
|||||||
### 3.3 操作:期期
|
### 3.3 操作:期期
|
||||||
|
|
||||||
1. 填上破 / 下破目标;指数价作参考。
|
1. 填上破 / 下破目标;指数价作参考。
|
||||||
2. 张数模式:**同张数**(默认)或 **均分预算**;平仓模式:**全平**(默认)或 **到期平**(若 env 打开方案 C)。
|
2. 张数模式:**同张数**(默认)、**做多**、**做空**;平仓模式:**全平**(默认)或 **到期平**(若 env 打开方案 C)。
|
||||||
3. T 型报价选用两腿;可先划转 USDC。
|
3. T 型报价选用两腿(做多/做空须一 Call 一 Put);可先划转 USDC。
|
||||||
4. **计算** → 情景测算 → **启动计划**。
|
4. **计算** → 情景测算 → **启动计划**。
|
||||||
|
|
||||||
逻辑摘要:
|
逻辑摘要:
|
||||||
|
|
||||||
|
- **同张数**:最大 `n` 使两腿各 `n` 张且总权利金 ≤ 预算。
|
||||||
|
- **做多 / 做空**:主腿与次腿按 env 比例(默认 7:3)分配;做多主腿=Call,做空主腿=Put。拆分口径由 `HEDGE_PLAN_OO_BIAS_SPLIT_BY` 决定:`budget`(默认,按权利金预算拆)或 `sheets`(先算同张数 `n` 再拆张数)。
|
||||||
- 达目标价:通常只平盈利腿。
|
- 达目标价:通常只平盈利腿。
|
||||||
- **全平**:盈利腿平掉后立刻尝试清另一腿(无 2× 权利金门控,失败会重试)。
|
- **全平**:盈利腿平掉后立刻尝试清另一腿(无 2× 权利金门控,失败会重试)。
|
||||||
- **到期平**:残腿持有至到期再结。
|
- **到期平**:残腿持有至到期再结。
|
||||||
@@ -320,6 +322,8 @@ A:可能是仅提醒类型、门控未过、RR 不足、满仓/日上限,或
|
|||||||
| 半腿失败改手动补开 | 见 3.4 |
|
| 半腿失败改手动补开 | 见 3.4 |
|
||||||
| 半腿失败时自动平期权 | 手动补开开启时强制无效 |
|
| 半腿失败时自动平期权 | 手动补开开启时强制无效 |
|
||||||
| 期期平仓模式(方案 C) | 页面是否出现「全平 / 到期平」 |
|
| 期期平仓模式(方案 C) | 页面是否出现「全平 / 到期平」 |
|
||||||
|
| 期期做多做空拆分口径 | `budget` 预算金额(默认)/ `sheets` 张数 |
|
||||||
|
| 期期做多做空主腿占比 | 默认 `0.7`(即 7:3) |
|
||||||
|
|
||||||
含「需重启」标记的项保存后要用「保存并重启」;对冲多数开关可热更,以页面标注为准。
|
含「需重启」标记的项保存后要用「保存并重启」;对冲多数开关可热更,以页面标注为准。
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
legB: null,
|
legB: null,
|
||||||
market: null,
|
market: null,
|
||||||
ooSheetsMode: "same_sheets",
|
ooSheetsMode: "same_sheets",
|
||||||
|
ooBiasSplitBy: "budget",
|
||||||
|
ooBiasRatio: 0.7,
|
||||||
ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0",
|
ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0",
|
||||||
ooCloseMode: "close_all",
|
ooCloseMode: "close_all",
|
||||||
direction: "long",
|
direction: "long",
|
||||||
@@ -335,6 +337,20 @@
|
|||||||
return Math.min(out, Math.floor(d + 1e-12));
|
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) {
|
function suggestOoSheetsLocal(budget, mode) {
|
||||||
const costA = unitCost(state.legA);
|
const costA = unitCost(state.legA);
|
||||||
const costB = unitCost(state.legB);
|
const costB = unitCost(state.legB);
|
||||||
@@ -344,24 +360,61 @@
|
|||||||
if (!(costA > 0) || !(costB > 0)) {
|
if (!(costA > 0) || !(costB > 0)) {
|
||||||
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "缺少有效卖一价,无法建议张数" };
|
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 nA = 0;
|
||||||
let nB = 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;
|
const half = budget / 2;
|
||||||
nA = Math.floor(half / costA + 1e-12);
|
nA = Math.floor(half / costA + 1e-12);
|
||||||
nB = Math.floor(half / costB + 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 {
|
} else {
|
||||||
const pair = costA + costB;
|
nA = nSame;
|
||||||
const n = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0;
|
nB = nSame;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
const premium = costA * nA + costB * nB;
|
const premium = costA * nA + costB * nB;
|
||||||
const ok = nA >= 1 && nB >= 1;
|
const ok = nA >= 1 && nB >= 1;
|
||||||
@@ -400,7 +453,7 @@
|
|||||||
const line = $("hp-oo-budget-line");
|
const line = $("hp-oo-budget-line");
|
||||||
if (!line) return;
|
if (!line) return;
|
||||||
const b = resolveOoBudget();
|
const b = resolveOoBudget();
|
||||||
const sizeLabel = state.ooSheetsMode === "split_budget" ? "均分" : "同张数";
|
const sizeLabel = ooSizeModeLabel(state.ooSheetsMode);
|
||||||
const closeLabel = !state.ooCloseModeEnabled
|
const closeLabel = !state.ooCloseModeEnabled
|
||||||
? ""
|
? ""
|
||||||
: state.ooCloseMode === "hold_expiry"
|
: state.ooCloseMode === "hold_expiry"
|
||||||
@@ -447,8 +500,15 @@
|
|||||||
} else if (d.oo_close_mode_default && !state._ooCloseModeTouched) {
|
} else if (d.oo_close_mode_default && !state._ooCloseModeTouched) {
|
||||||
state.ooCloseMode = d.oo_close_mode_default === "hold_expiry" ? "hold_expiry" : "close_all";
|
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();
|
syncOoCloseModeUI();
|
||||||
setGateLine(d);
|
setGateLine(d);
|
||||||
|
if (state.mode === "options_options") autoFillOoSheets();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false });
|
setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false });
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+6
@@ -91,6 +91,8 @@ HOT_RELOAD_EXACT = frozenset({
|
|||||||
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
|
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
|
||||||
"HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
|
"HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
|
||||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
|
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
|
||||||
|
"HEDGE_PLAN_OO_BIAS_SPLIT_BY",
|
||||||
|
"HEDGE_PLAN_OO_BIAS_RATIO",
|
||||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
||||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
|
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
|
||||||
"MAX_ACTIVE_HEDGE_PLANS",
|
"MAX_ACTIVE_HEDGE_PLANS",
|
||||||
@@ -121,6 +123,10 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {
|
|||||||
("long_only", "仅做多"),
|
("long_only", "仅做多"),
|
||||||
("short_only", "仅做空"),
|
("short_only", "仅做空"),
|
||||||
),
|
),
|
||||||
|
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": (
|
||||||
|
("budget", "预算金额"),
|
||||||
|
("sheets", "张数"),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
_SELECT_ALIASES: dict[str, dict[str, str]] = {
|
_SELECT_ALIASES: dict[str, dict[str, str]] = {
|
||||||
|
|||||||
Vendored
+12
@@ -146,6 +146,16 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = {
|
|||||||
"期期平仓模式(方案C)",
|
"期期平仓模式(方案C)",
|
||||||
"默认 true;开启后页面可选「到期平/全平」(盈利腿平后另一腿);关闭则固定到期平",
|
"默认 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",
|
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
||||||
"对冲与期权互斥门控",
|
"对冲与期权互斥门控",
|
||||||
@@ -177,6 +187,8 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
|||||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
||||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "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_OPTIONS_MUTUAL_EXCLUSIVE": "true",
|
||||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
|
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)))
|
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(
|
def suggest_oo_sheets(
|
||||||
*,
|
*,
|
||||||
mode: str,
|
mode: str,
|
||||||
@@ -157,63 +205,89 @@ def suggest_oo_sheets(
|
|||||||
ask_a: float,
|
ask_a: float,
|
||||||
ct_mult_a: float = 0.01,
|
ct_mult_a: float = 0.01,
|
||||||
ask_sz_a: Any = None,
|
ask_sz_a: Any = None,
|
||||||
|
opt_type_a: str = "",
|
||||||
ask_b: float,
|
ask_b: float,
|
||||||
ct_mult_b: float = 0.01,
|
ct_mult_b: float = 0.01,
|
||||||
ask_sz_b: Any = None,
|
ask_sz_b: Any = None,
|
||||||
|
opt_type_b: str = "",
|
||||||
|
bias_split_by: str = "budget",
|
||||||
|
bias_ratio: float = 0.7,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""期期建议张数:same_sheets(同张数,默认) / split_budget(均分预算)."""
|
"""期期建议张数:same_sheets / long_bias / short_bias(及旧 split_budget)."""
|
||||||
import math
|
import math
|
||||||
|
|
||||||
m = (mode or "same_sheets").strip().lower()
|
m = _normalize_oo_sheets_mode(mode)
|
||||||
if m in ("split", "equal_budget", "split_budget", "均分"):
|
split_by = _normalize_oo_bias_split_by(bias_split_by)
|
||||||
m = "split_budget"
|
ratio = _clamp_oo_bias_ratio(bias_ratio)
|
||||||
else:
|
|
||||||
m = "same_sheets"
|
|
||||||
budget = max(0.0, float(budget_usdc or 0.0))
|
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_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)
|
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:
|
if budget <= 0:
|
||||||
return {
|
return _fail("可用预算为 0")
|
||||||
"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",
|
|
||||||
}
|
|
||||||
if cost_a <= 0 or cost_b <= 0:
|
if cost_a <= 0 or cost_b <= 0:
|
||||||
return {
|
return _fail("缺少有效卖一价,无法建议张数")
|
||||||
"mode": m,
|
|
||||||
"sheets_a": 0,
|
pair = cost_a + cost_b
|
||||||
"sheets_b": 0,
|
n_same = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0
|
||||||
"cost_a": round(cost_a, 8),
|
n_same = _cap_sheets_by_ask_depth(n_same, ask_sz_a)
|
||||||
"cost_b": round(cost_b, 8),
|
n_same = _cap_sheets_by_ask_depth(n_same, ask_sz_b)
|
||||||
"premium_est": 0.0,
|
|
||||||
"ok": False,
|
if m == "same_sheets":
|
||||||
"msg": "缺少有效卖一价,无法建议张数",
|
n_a = n_same
|
||||||
}
|
n_b = n_same
|
||||||
if m == "split_budget":
|
elif m == "split_budget":
|
||||||
half = budget / 2.0
|
half = budget / 2.0
|
||||||
n_a = int(math.floor(half / cost_a + 1e-12))
|
n_a = int(math.floor(half / cost_a + 1e-12))
|
||||||
n_b = int(math.floor(half / cost_b + 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:
|
else:
|
||||||
pair = cost_a + cost_b
|
call_side, put_side, err = _oo_call_put_leg_index(opt_type_a, opt_type_b)
|
||||||
n = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0
|
if err:
|
||||||
n_a = n
|
return _fail(err)
|
||||||
n_b = n
|
major_is_call = m == "long_bias"
|
||||||
n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
|
if split_by == "sheets":
|
||||||
n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)
|
if n_same < 2:
|
||||||
if m == "same_sheets":
|
return _fail("同张数规模不足 2,无法按比例拆分")
|
||||||
n = min(n_a, n_b)
|
major_n = int(round(n_same * ratio))
|
||||||
n_a = n
|
major_n = max(1, min(major_n, n_same - 1))
|
||||||
n_b = n
|
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
|
prem = cost_a * n_a + cost_b * n_b
|
||||||
ok = n_a >= 1 and n_b >= 1
|
ok = n_a >= 1 and n_b >= 1
|
||||||
msg = ""
|
msg = "" if ok else "预算不够开 1+1(或卖一深度不足)"
|
||||||
if not ok:
|
|
||||||
msg = "预算不够开 1+1(或卖一深度不足)"
|
|
||||||
return {
|
return {
|
||||||
"mode": m,
|
"mode": m,
|
||||||
"sheets_a": n_a,
|
"sheets_a": n_a,
|
||||||
@@ -223,6 +297,8 @@ def suggest_oo_sheets(
|
|||||||
"premium_est": round(prem, 6),
|
"premium_est": round(prem, 6),
|
||||||
"ok": ok,
|
"ok": ok,
|
||||||
"msg": msg,
|
"msg": msg,
|
||||||
|
"bias_split_by": split_by,
|
||||||
|
"bias_ratio": ratio,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,18 @@ def _oo_close_mode_enabled() -> bool:
|
|||||||
return _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True)
|
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:
|
def _normalize_oo_close_mode(raw: Any) -> str:
|
||||||
"""方案C关闭时强制 hold_expiry;开启时默认 close_all."""
|
"""方案C关闭时强制 hold_expiry;开启时默认 close_all."""
|
||||||
if not _oo_close_mode_enabled():
|
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 = _gates_dict(cfg, plan_type)
|
||||||
g["oo_close_mode_enabled"] = _oo_close_mode_enabled()
|
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_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
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,8 @@
|
|||||||
<span class="hp-oo-ctrl-lab">张数</span>
|
<span class="hp-oo-ctrl-lab">张数</span>
|
||||||
<div class="hp-oo-seg" role="group" aria-label="自动张数">
|
<div class="hp-oo-seg" role="group" aria-label="自动张数">
|
||||||
<button type="button" class="btn-secondary hp-oo-size-mode is-selected" data-oo-size="same_sheets" title="两腿同张数,总权利金≤预算"><span class="hp-oo-check" aria-hidden="true">✓</span>同张数</button>
|
<button type="button" class="btn-secondary hp-oo-size-mode is-selected" data-oo-size="same_sheets" title="两腿同张数,总权利金≤预算"><span class="hp-oo-check" aria-hidden="true">✓</span>同张数</button>
|
||||||
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="split_budget" title="预算对半拆到两腿"><span class="hp-oo-check" aria-hidden="true">✓</span>均分</button>
|
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="long_bias" title="偏多:Call 占比更高(比例见 env)"><span class="hp-oo-check" aria-hidden="true">✓</span>做多</button>
|
||||||
|
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="short_bias" title="偏空:Put 占比更高(比例见 env)"><span class="hp-oo-check" aria-hidden="true">✓</span>做空</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hp-oo-ctrl" id="hp-oo-close-mode-row">
|
<div class="hp-oo-ctrl" id="hp-oo-close-mode-row">
|
||||||
@@ -300,4 +301,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/hedge_plan.js?v=22"></script>
|
<script src="/static/hedge_plan.js?v=23"></script>
|
||||||
|
|||||||
@@ -153,19 +153,57 @@ class TestHedgePlanCalc(unittest.TestCase):
|
|||||||
self.assertEqual(s["sheets_b"], 5)
|
self.assertEqual(s["sheets_b"], 5)
|
||||||
self.assertTrue(s["ok"])
|
self.assertTrue(s["ok"])
|
||||||
|
|
||||||
def test_suggest_oo_split_budget(self):
|
def test_suggest_oo_long_bias_budget(self):
|
||||||
# cost_a=1, cost_b=2; half=5 → nA=5, nB=2
|
# cost_call=1, cost_put=1; budget 10 → call 7U / put 3U → 7 / 3
|
||||||
s = suggest_oo_sheets(
|
s = suggest_oo_sheets(
|
||||||
mode="split_budget",
|
mode="long_bias",
|
||||||
budget_usdc=10,
|
budget_usdc=10,
|
||||||
ask_a=100,
|
ask_a=100,
|
||||||
ct_mult_a=0.01,
|
ct_mult_a=0.01,
|
||||||
ask_b=200,
|
opt_type_a="C",
|
||||||
|
ask_b=100,
|
||||||
ct_mult_b=0.01,
|
ct_mult_b=0.01,
|
||||||
|
opt_type_b="P",
|
||||||
|
bias_split_by="budget",
|
||||||
|
bias_ratio=0.7,
|
||||||
)
|
)
|
||||||
self.assertEqual(s["mode"], "split_budget")
|
self.assertEqual(s["mode"], "long_bias")
|
||||||
self.assertEqual(s["sheets_a"], 5)
|
self.assertEqual(s["sheets_a"], 7)
|
||||||
self.assertEqual(s["sheets_b"], 2)
|
self.assertEqual(s["sheets_b"], 3)
|
||||||
|
self.assertTrue(s["ok"])
|
||||||
|
|
||||||
|
def test_suggest_oo_short_bias_sheets(self):
|
||||||
|
# same n=5; short → put major 4 / call minor 1 (round 5*0.7=4)
|
||||||
|
s = suggest_oo_sheets(
|
||||||
|
mode="short_bias",
|
||||||
|
budget_usdc=10,
|
||||||
|
ask_a=100,
|
||||||
|
ct_mult_a=0.01,
|
||||||
|
opt_type_a="C",
|
||||||
|
ask_b=100,
|
||||||
|
ct_mult_b=0.01,
|
||||||
|
opt_type_b="P",
|
||||||
|
bias_split_by="sheets",
|
||||||
|
bias_ratio=0.7,
|
||||||
|
)
|
||||||
|
self.assertEqual(s["mode"], "short_bias")
|
||||||
|
self.assertEqual(s["sheets_a"], 1)
|
||||||
|
self.assertEqual(s["sheets_b"], 4)
|
||||||
|
|
||||||
|
def test_suggest_oo_bias_requires_call_put(self):
|
||||||
|
s = suggest_oo_sheets(
|
||||||
|
mode="long_bias",
|
||||||
|
budget_usdc=10,
|
||||||
|
ask_a=100,
|
||||||
|
ct_mult_a=0.01,
|
||||||
|
opt_type_a="C",
|
||||||
|
ask_b=100,
|
||||||
|
ct_mult_b=0.01,
|
||||||
|
opt_type_b="C",
|
||||||
|
bias_split_by="budget",
|
||||||
|
)
|
||||||
|
self.assertFalse(s["ok"])
|
||||||
|
self.assertIn("Call", s["msg"])
|
||||||
|
|
||||||
def test_suggest_oo_depth_cap(self):
|
def test_suggest_oo_depth_cap(self):
|
||||||
s = suggest_oo_sheets(
|
s = suggest_oo_sheets(
|
||||||
|
|||||||
Reference in New Issue
Block a user