Add HEDGE_PO_MODE instance modes with shared sim/live path.
Lock one mode per instance/subaccount, cross-asset BTC option/ETH perp sizing, combo-U exit, and ship docs/tests/audit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -164,6 +164,11 @@ HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true
|
||||
HEDGE_PLAN_LIVE_ORDER=false
|
||||
# 永期子模式:true=以期权为主;false=保险模式(页面标题前标识,不可页内切换)
|
||||
HEDGE_PLAN_OPTION_PRIMARY=true
|
||||
# 永期实例模式(一实例一种;对应一个子账户;模拟/实盘同路径;改后须重启 PM2)
|
||||
# eth=ETH期权+ETH永续 | btc=BTC期权+BTC永续 | btc_eth=BTC期权+ETH永续(名义×K)
|
||||
HEDGE_PO_MODE=eth
|
||||
# 仅 btc_eth:永续名义 = 期权名义 × K(默认 0.5);比例=1/K
|
||||
HEDGE_CROSS_NOTIONAL_K=0.5
|
||||
HEDGE_PLAN_OPEN_ORDER=options_first
|
||||
HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true
|
||||
HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false
|
||||
|
||||
@@ -61,10 +61,13 @@ python app.py
|
||||
- `SIM_DEFAULT_MODE=sim`(默认模拟盘)
|
||||
- `OKX_OPTIONS_MARGIN_MODE=coin`(固定)
|
||||
- `OKX_TRADE_MODE=perp_options|options_options`(本仓仅对冲)
|
||||
- `HEDGE_PO_MODE=eth|btc|btc_eth`(一实例一模式一子账户;跨币见 `HEDGE_CROSS_NOTIONAL_K`)
|
||||
- `STANDALONE_OPTIONS_UI=false`(默认;勿开,除非临时救急)
|
||||
- `SIM_INITIAL_EQUITY_USDT=10000`
|
||||
- `APP_PORT=5010`
|
||||
|
||||
永期三模式说明与审计清单见 [docs/永期对冲三模式.md](docs/永期对冲三模式.md)。
|
||||
|
||||
## 风险
|
||||
|
||||
实盘风险自负。默认模拟盘;切 `live` 前请核对 API 权限与资金。
|
||||
|
||||
@@ -6878,6 +6878,9 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
_standalone_options_ui = standalone_options_ui_enabled()
|
||||
_hedge_mode_on = _okx_trade_mode in ("perp_options", "options_options")
|
||||
_show_perp_funds = show_perp_funds_enabled(exchange_key="okx")
|
||||
from lib.hedge_plan.hedge_po_mode_lib import public_mode_payload as _po_mode_public
|
||||
|
||||
_hedge_po_mode_payload = _po_mode_public()
|
||||
template_ctx = dict(
|
||||
page=page,
|
||||
key=key_list,
|
||||
@@ -7000,7 +7003,16 @@ def render_main_page(page="trade", embed_mode=None):
|
||||
options_compound_full_cap_usdc=float(
|
||||
os.getenv("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC") or "300"
|
||||
),
|
||||
options_default_underly=OKX_OPTIONS_DEFAULT_UNDERLY,
|
||||
options_default_underly=_hedge_po_mode_payload.get("option_underlying")
|
||||
or OKX_OPTIONS_DEFAULT_UNDERLY,
|
||||
hedge_po_mode=_hedge_po_mode_payload.get("po_mode"),
|
||||
hedge_po_mode_label=_hedge_po_mode_payload.get("po_mode_label"),
|
||||
hedge_po_mode_short=_hedge_po_mode_payload.get("po_mode_short"),
|
||||
hedge_option_underlying=_hedge_po_mode_payload.get("option_underlying"),
|
||||
hedge_perp_underlying=_hedge_po_mode_payload.get("perp_underlying"),
|
||||
hedge_cross_asset=bool(_hedge_po_mode_payload.get("cross_asset")),
|
||||
hedge_cross_notional_k=_hedge_po_mode_payload.get("cross_notional_k"),
|
||||
hedge_option_perp_ratio=_hedge_po_mode_payload.get("option_perp_ratio"),
|
||||
options_chain_ask_liq_filter=os.getenv(
|
||||
"OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED", "true"
|
||||
).lower()
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
# 永期对冲三模式(一实例 · 一模式 · 一子账户)
|
||||
|
||||
> 模拟盘与实盘 **同一执行路径**;差别仅在 API Key / `LIVE_TRADING_ENABLED` / 模拟开关。
|
||||
> 实现入口:`lib/hedge_plan/hedge_po_mode_lib.py`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 定稿口径
|
||||
|
||||
| 模式 `HEDGE_PO_MODE` | 期权标的 | 永续标的 | 名义关系 |
|
||||
|----------------------|----------|----------|----------|
|
||||
| `eth` | ETH | ETH | 永续名义 = 期权名义 ÷ 比例(默认比例 2 → 一半) |
|
||||
| `btc` | BTC | BTC | 同上 |
|
||||
| `btc_eth` | BTC | ETH | 永续名义(U) = 期权名义(U) × `HEDGE_CROSS_NOTIONAL_K`(默认 **0.5**) |
|
||||
|
||||
### 方向(以期权为主,强制)
|
||||
|
||||
- 买 **Call** → 永续 **空**
|
||||
- 买 **Put** → 永续 **多**
|
||||
|
||||
### 出场(强制)
|
||||
|
||||
- 仅 **组合盈利金额(U)**
|
||||
- 口径:`期权净利(币) × 期权指数 + 永续净利(U)`(扣开平双边手续费)
|
||||
- 与持仓卡 `combo.net_usdt`、监控出场门控一致;**禁止**币与 U 直接相加
|
||||
|
||||
### 部署约束
|
||||
|
||||
- **一个实例一种模式对应一个子账户**;账户之间互不干涉
|
||||
- **禁止**同一子账户挂两个实例
|
||||
- **禁止**运行中热切 `HEDGE_PO_MODE`(须换实例 / 换子账户 / 重启)
|
||||
- 模拟 → 实盘:同模式、同定仓/出场逻辑,只换密钥与 live/sim 开关
|
||||
|
||||
---
|
||||
|
||||
## 2. 环境变量
|
||||
|
||||
```env
|
||||
# 必配(建议显式写出);非法值启动时钳制并打日志
|
||||
HEDGE_PO_MODE=eth # eth | btc | btc_eth
|
||||
HEDGE_CROSS_NOTIONAL_K=0.5 # 仅 btc_eth;永续名义/期权名义
|
||||
|
||||
# 会与模式同步:期权链默认标的 = 模式的期权腿
|
||||
OKX_OPTIONS_DEFAULT_UNDERLY=ETH
|
||||
|
||||
# 以期权为主(实例模式会强制 true)
|
||||
HEDGE_PLAN_OPTION_PRIMARY=true
|
||||
|
||||
# 真下单:模拟可开;实盘须再开 LIVE_TRADING_ENABLED
|
||||
HEDGE_PLAN_LIVE_ORDER=true
|
||||
LIVE_TRADING_ENABLED=false
|
||||
```
|
||||
|
||||
改 `HEDGE_PO_MODE` / `HEDGE_CROSS_NOTIONAL_K` 后须 **重启 PM2**(启动时 `clamp_hedge_po_mode_on_startup`)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 多实例部署示例
|
||||
|
||||
| 进程 / 域名 | `.env` | 子账户 |
|
||||
|-------------|--------|--------|
|
||||
| 实例 A | `HEDGE_PO_MODE=eth` | OKX 子账户「ETH对冲」 |
|
||||
| 实例 B | `HEDGE_PO_MODE=btc` | OKX 子账户「BTC对冲」 |
|
||||
| 实例 C | `HEDGE_PO_MODE=btc_eth` | OKX 子账户「跨币」 |
|
||||
|
||||
模拟盘同样拆三套(或三库),**不要**一个模拟进程里切三种模式共一把钥匙。
|
||||
|
||||
---
|
||||
|
||||
## 4. 代码落点
|
||||
|
||||
| 模块 | 作用 |
|
||||
|------|------|
|
||||
| `hedge_po_mode_lib.py` | 模式规范化、启动钳制、跨币名义、组合 U、body 钳制 |
|
||||
| `hedge_plan_option_primary_lib.py` | `size_from_premium` / `contracts_from_option_sheets` 支持双指数 |
|
||||
| `hedge_plan_orders_lib.py` | 启动前定仓刷新、校验按模式钳制 |
|
||||
| `hedge_plan_monitor_lib.py` | 出场门控用组合 U;跨币指数分腿 |
|
||||
| `hedge_plan_register.py` | 安装时钳制;行情/期权链锁定标的;落库 `po_mode`/`perp_underlying` |
|
||||
| `hedge_plan.js` + panel | 页内锁定标的;展示模式徽标;强制盈利金额出场 |
|
||||
| env schema / UI manifest | `HEDGE_PO_MODE`、`HEDGE_CROSS_NOTIONAL_K` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试
|
||||
|
||||
```bash
|
||||
python -m unittest tests.test_hedge_po_mode tests.test_hedge_plan_option_primary -v
|
||||
```
|
||||
|
||||
覆盖:模式规范化、启动钳制、跨币名义半仓、组合 U 口径、body 校验。
|
||||
|
||||
---
|
||||
|
||||
## 6. 审计清单(模拟 / 实盘通用)
|
||||
|
||||
启动后日志应出现:
|
||||
|
||||
```text
|
||||
[HEDGE_PO_MODE] mode=... option=... perp=... k=... (一实例一模式一子账户)
|
||||
```
|
||||
|
||||
| # | 检查项 | 期望 |
|
||||
|---|--------|------|
|
||||
| 1 | `/api/hedge-plan/po-mode` | `po_mode` / `option_underlying` / `perp_underlying` / `exit_mode=profit` |
|
||||
| 2 | 对冲页顶栏徽标 | 显示模式名;跨币显示「期权BTC/永续ETH」 |
|
||||
| 3 | ETH/BTC 切换钮 | 永期页禁用,与模式期权标的一致 |
|
||||
| 4 | 期权链 | 仅模式期权标的;拉错标的 API 400 |
|
||||
| 5 | 永续行情 `exchange_symbol` | 前缀=模式永续标的 |
|
||||
| 6 | 手动开仓 body | 服务端重写 `underlying`/`exit_mode`/`option_perp_ratio`(跨币) |
|
||||
| 7 | 定仓(跨币) | 期权名义U × K ≈ 永续名义U |
|
||||
| 8 | 持仓卡 `combo.net_usdt` | 期权币×期权指数 + 永续U |
|
||||
| 9 | 监控出场 | 达 `profit_target_usdt` 才平;比较的是组合 U |
|
||||
| 10 | 卖回币 | 卖 **期权标的**币(跨币卖 BTC) |
|
||||
| 11 | 模拟 vs 实盘 | 同一套 orders/monitor;仅 Key 与 live 开关不同 |
|
||||
| 12 | 子账户 | 三模式三 Key;无交叉下单 |
|
||||
|
||||
快速审计脚本:
|
||||
|
||||
```bash
|
||||
python scripts/audit_hedge_po_mode.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 实盘上线顺序建议
|
||||
|
||||
1. 模拟实例 `HEDGE_PO_MODE=eth` 走通开平与组合 U 出场
|
||||
2. 同模式换实盘子账户 Key,`LIVE_TRADING_ENABLED=true` + `HEDGE_PLAN_LIVE_ORDER=true`
|
||||
3. 再开 `btc` / `btc_eth` 独立实例灰度
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- [对冲计划-以期权为主.md](./对冲计划-以期权为主.md)
|
||||
- [对冲计划策略与P0校验.md](./对冲计划策略与P0校验.md)
|
||||
- [对冲计划开发方案.md](./对冲计划开发方案.md)
|
||||
+106
-35
@@ -18,11 +18,25 @@
|
||||
return "perp_options";
|
||||
}
|
||||
|
||||
const poMode = (root.getAttribute("data-po-mode") || "eth").toLowerCase();
|
||||
const optionUnderly = (root.getAttribute("data-option-underly") || root.getAttribute("data-default-underly") || "ETH").toUpperCase();
|
||||
const perpUnderly = (root.getAttribute("data-perp-underly") || optionUnderly).toUpperCase();
|
||||
const crossAsset = root.getAttribute("data-cross-asset") === "1";
|
||||
const lockedRatio = (function () {
|
||||
const raw = root.getAttribute("data-option-perp-ratio");
|
||||
const n = raw != null && raw !== "" ? Number(raw) : NaN;
|
||||
return !Number.isNaN(n) && n > 0 ? n : 2;
|
||||
})();
|
||||
|
||||
const state = {
|
||||
tab: pickDefaultTab(),
|
||||
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
|
||||
posTab: "live",
|
||||
underlying: root.getAttribute("data-default-underly") || "ETH",
|
||||
underlying: optionUnderly,
|
||||
poMode: poMode,
|
||||
optionUnderly: optionUnderly,
|
||||
perpUnderly: perpUnderly,
|
||||
crossAsset: crossAsset,
|
||||
moneyFilter: root.getAttribute("data-option-primary") !== "0" ? "all" : "itm",
|
||||
ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值
|
||||
ooRecommend: null, // atm_straddle | double_otm | null
|
||||
@@ -41,9 +55,9 @@
|
||||
ooCloseMode: "close_all",
|
||||
direction: "long",
|
||||
optionPrimary: root.getAttribute("data-option-primary") !== "0",
|
||||
exitMode: "profit", // profit=盈利金额出场 | target=目标位出场
|
||||
exitMode: "profit", // 实例模式强制组合盈利金额(U)
|
||||
opLevTouched: false,
|
||||
opRatioTouched: false,
|
||||
opRatioTouched: crossAsset, // 跨币锁比例,视为已设定
|
||||
perpLevTouched: false,
|
||||
tradingUsdc: null,
|
||||
fundingUsdc: null,
|
||||
@@ -58,6 +72,32 @@
|
||||
previewPlanType: null,
|
||||
};
|
||||
|
||||
function lockPoUnderlyingUI() {
|
||||
const opt = state.optionUnderly || "ETH";
|
||||
document.querySelectorAll(".hp-uly-btn").forEach(function (b) {
|
||||
const uly = (b.getAttribute("data-uly") || "").toUpperCase();
|
||||
const on = uly === opt;
|
||||
b.classList.toggle("active", on);
|
||||
b.disabled = true;
|
||||
b.title = "由 HEDGE_PO_MODE 锁定(" + (root.getAttribute("data-po-mode-label") || poMode) + ")";
|
||||
});
|
||||
const badge = $("hp-po-mode-badge");
|
||||
if (badge) {
|
||||
const perp = state.perpUnderly || opt;
|
||||
badge.textContent =
|
||||
(root.getAttribute("data-po-mode-label") || poMode) +
|
||||
(crossAsset ? " · 期权" + opt + "/永续" + perp : "");
|
||||
}
|
||||
const ratioEl = $("hp-opt-perp-ratio");
|
||||
if (ratioEl && lockedRatio > 0) {
|
||||
ratioEl.value = String(lockedRatio);
|
||||
if (crossAsset) {
|
||||
ratioEl.readOnly = true;
|
||||
ratioEl.title = "跨币模式由 HEDGE_CROSS_NOTIONAL_K 锁定";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDirection() {
|
||||
return (state.direction || "long").toLowerCase() === "short" ? "short" : "long";
|
||||
}
|
||||
@@ -421,7 +461,7 @@
|
||||
|
||||
function computeOpSizing(ask, ctMult, sheetsOverride) {
|
||||
const budget = numInput("hp-premium-budget", 0);
|
||||
const ratio = numInput("hp-opt-perp-ratio", 2);
|
||||
const ratio = numInput("hp-opt-perp-ratio", lockedRatio || 2);
|
||||
const cs = Number((state.market && state.market.contract_size) || 0.01);
|
||||
const usable = budget * 0.95;
|
||||
const a = Number(ask || 0);
|
||||
@@ -443,16 +483,32 @@
|
||||
if (!(sheets > 0)) return null;
|
||||
eth = Math.round(sheets * ct * 100) / 100;
|
||||
}
|
||||
const perpEth = eth / ratio;
|
||||
const contracts = perpEth / cs;
|
||||
// 跨币:期权币×期权指数×(1/ratio) / 永续指数 = 永续币
|
||||
let perpQty = eth / ratio;
|
||||
let contracts = perpQty / cs;
|
||||
let optNotional = null;
|
||||
let perpNotional = null;
|
||||
if (state.crossAsset) {
|
||||
const optIdx = Number((state.chain && state.chain.index_px) || 0);
|
||||
const perpIdx = Number((state.market && (state.market.index_px || state.market.mark)) || 0);
|
||||
if (optIdx > 0 && perpIdx > 0) {
|
||||
optNotional = eth * optIdx;
|
||||
perpNotional = optNotional / ratio;
|
||||
perpQty = perpNotional / perpIdx;
|
||||
contracts = perpQty / cs;
|
||||
}
|
||||
}
|
||||
return {
|
||||
usable: usable,
|
||||
eth_qty: eth,
|
||||
perp_qty: perpEth,
|
||||
perp_qty: perpQty,
|
||||
sheets: sheets,
|
||||
contracts: contracts,
|
||||
premium_est: a > 0 ? a * sheets * ct : 0,
|
||||
ratio: ratio,
|
||||
option_notional_usdt: optNotional,
|
||||
perp_notional_usdt: perpNotional,
|
||||
cross_asset: !!state.crossAsset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -483,24 +539,32 @@
|
||||
|
||||
function formatOpSizingHtml(sized, amtPrec) {
|
||||
if (!sized) return "";
|
||||
const coin = (state.underlying || "ETH").toUpperCase();
|
||||
const prec = amtPrec != null ? amtPrec : coin === "BTC" ? 4 : 2;
|
||||
return (
|
||||
const optCoin = (state.optionUnderly || state.underlying || "ETH").toUpperCase();
|
||||
const perpCoin = (state.perpUnderly || state.underlying || "ETH").toUpperCase();
|
||||
const prec = amtPrec != null ? amtPrec : perpCoin === "BTC" ? 4 : 2;
|
||||
let html =
|
||||
"期权 <strong>" +
|
||||
fmt(sized.eth_qty, 2) +
|
||||
"</strong> " +
|
||||
coin +
|
||||
optCoin +
|
||||
" / " +
|
||||
sized.sheets +
|
||||
" 张 → 永续 <strong>" +
|
||||
fmt(sized.perp_qty, 4) +
|
||||
"</strong> " +
|
||||
coin +
|
||||
perpCoin +
|
||||
" (" +
|
||||
fmt(sized.contracts, prec) +
|
||||
" 张) · 比例 " +
|
||||
fmt(sized.ratio, 2)
|
||||
);
|
||||
fmt(sized.ratio, 2);
|
||||
if (sized.cross_asset && sized.option_notional_usdt != null) {
|
||||
html +=
|
||||
" · 名义U " +
|
||||
fmt(sized.option_notional_usdt, 0) +
|
||||
"/" +
|
||||
fmt(sized.perp_notional_usdt, 0);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function defaultPerpLeverage(uly) {
|
||||
@@ -1881,9 +1945,12 @@
|
||||
|
||||
async function loadMarket() {
|
||||
const dir = getDirection();
|
||||
// 永续行情按实例模式永续标的(跨币=ETH);期权链仍用 optionUnderly
|
||||
const mktBase =
|
||||
state.mode === "perp_options" ? state.perpUnderly || state.underlying : state.underlying;
|
||||
const d = await apiJson(
|
||||
"/api/hedge-plan/market?base=" +
|
||||
encodeURIComponent(state.underlying) +
|
||||
encodeURIComponent(mktBase) +
|
||||
"&direction=" +
|
||||
encodeURIComponent(dir) +
|
||||
"&option_primary=" +
|
||||
@@ -2523,7 +2590,12 @@
|
||||
}
|
||||
|
||||
function setUnderlying(uly, forceReload) {
|
||||
const next = (uly || "ETH").toUpperCase();
|
||||
let next = (uly || "ETH").toUpperCase();
|
||||
// 永期:标的由 HEDGE_PO_MODE 锁定,页内不可切换
|
||||
if (state.mode === "perp_options") {
|
||||
next = state.optionUnderly || next;
|
||||
if (!forceReload && next === state.underlying) return;
|
||||
}
|
||||
const changed = next !== state.underlying;
|
||||
state.underlying = next;
|
||||
syncUnderlyingUI();
|
||||
@@ -2813,6 +2885,9 @@
|
||||
});
|
||||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
if (b.classList.contains("hp-uly-btn") && state.mode === "perp_options") {
|
||||
return; // 永期标的由实例模式锁定
|
||||
}
|
||||
setUnderlying(b.getAttribute("data-uly") || "ETH", true);
|
||||
});
|
||||
});
|
||||
@@ -3011,7 +3086,8 @@
|
||||
}
|
||||
document.querySelectorAll(".hp-po-exit-mode").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
state.exitMode = b.getAttribute("data-exit") === "target" ? "target" : "profit";
|
||||
// 实例模式强制组合盈利金额(U)出场
|
||||
state.exitMode = "profit";
|
||||
syncExitModeUI();
|
||||
updatePerpPnlHint();
|
||||
});
|
||||
@@ -3763,23 +3839,14 @@
|
||||
setPoOrderErr("请选择期权并填写张数(≥1)", "hp-order-sheets");
|
||||
throw new Error("请选择期权并填写张数(≥1)");
|
||||
}
|
||||
const exitMode = state.exitMode === "target" ? "target" : "profit";
|
||||
const exitMode = "profit";
|
||||
let optPts = 0;
|
||||
let perpPts = 0;
|
||||
let profitTarget = 0;
|
||||
if (exitMode === "profit") {
|
||||
profitTarget = numInput("hp-profit-target", NaN);
|
||||
if (!(profitTarget > 0)) {
|
||||
setPoOrderErr("请填写目标盈利金额(USDT,扣双边手续费后净利)", "hp-profit-target");
|
||||
throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)");
|
||||
}
|
||||
} else {
|
||||
optPts = numInput("hp-opt-target-pts", NaN);
|
||||
perpPts = numInput("hp-perp-target-pts", NaN);
|
||||
if (!(optPts > 0) || !(perpPts > 0)) {
|
||||
setPoOrderErr("请填写期权/永续目标位点数(须大于0)", !(optPts > 0) ? "hp-opt-target-pts" : "hp-perp-target-pts");
|
||||
throw new Error("请填写期权/永续目标位点数(须大于0)");
|
||||
}
|
||||
profitTarget = numInput("hp-profit-target", NaN);
|
||||
if (!(profitTarget > 0)) {
|
||||
setPoOrderErr("请填写目标盈利金额(USDT,组合净利折U)", "hp-profit-target");
|
||||
throw new Error("请填写目标盈利金额(USDT,组合净利折U)");
|
||||
}
|
||||
if (!(state.market && state.market.exchange_symbol)) {
|
||||
setPoOrderErr("永续行情未就绪,请先点「刷新行情」");
|
||||
@@ -3805,9 +3872,12 @@
|
||||
market_order: 1,
|
||||
watch_entry: 0,
|
||||
exit_mode: exitMode,
|
||||
underlying: state.underlying,
|
||||
po_mode: state.poMode,
|
||||
underlying: state.optionUnderly || state.underlying,
|
||||
option_underlying: state.optionUnderly || state.underlying,
|
||||
perp_underlying: state.perpUnderly || state.underlying,
|
||||
direction: getDirection(),
|
||||
entry: entry,
|
||||
entry: Number((state.market && state.market.mark) || entry),
|
||||
contracts: sized.contracts,
|
||||
sheets: sized.sheets,
|
||||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||||
@@ -3818,10 +3888,10 @@
|
||||
ask: state.selected.ask,
|
||||
index_px: entry,
|
||||
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
|
||||
leverage: defaultPerpLeverage(state.underlying),
|
||||
leverage: defaultPerpLeverage(state.perpUnderly || state.underlying),
|
||||
option_leverage: 0,
|
||||
premium_budget: Math.max(premEst / 0.95, premEst),
|
||||
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
|
||||
option_perp_ratio: numInput("hp-opt-perp-ratio", lockedRatio),
|
||||
option_target_points: optPts || 0,
|
||||
perp_target_points: perpPts || 0,
|
||||
profit_target_usdt: profitTarget || 0,
|
||||
@@ -3929,6 +3999,7 @@
|
||||
if (state.tab === "perp_options") void refreshPoStrategyStatus();
|
||||
}
|
||||
|
||||
lockPoUnderlyingUI();
|
||||
syncTabUI();
|
||||
syncUnderlyingUI();
|
||||
syncMoneyUI();
|
||||
|
||||
@@ -3625,6 +3625,22 @@ html[data-theme="light"] .opt-be-dist-down {
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-mode-badge {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text, #1a1a1a);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-uly-lock {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-uly-lock .hp-uly-btn:disabled {
|
||||
opacity: 0.75;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.hedge-plan-page-wrap .hp-po-gate-inline {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.35;
|
||||
|
||||
Vendored
+7
@@ -109,6 +109,8 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"MAX_ACTIVE_HEDGE_PLANS",
|
||||
"HEDGE_PLAN_LIVE_ORDER",
|
||||
"HEDGE_PLAN_OPTION_PRIMARY",
|
||||
"HEDGE_PO_MODE",
|
||||
"HEDGE_CROSS_NOTIONAL_K",
|
||||
"HEDGE_PLAN_OPEN_ORDER",
|
||||
"HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
|
||||
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
|
||||
@@ -177,6 +179,11 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {
|
||||
("true", "以期权为主"),
|
||||
("false", "保险模式"),
|
||||
),
|
||||
"HEDGE_PO_MODE": (
|
||||
("eth", "ETH永期(期权+永续同ETH)"),
|
||||
("btc", "BTC永期(期权+永续同BTC)"),
|
||||
("btc_eth", "跨币(BTC期权+ETH永续)"),
|
||||
),
|
||||
"SIM_DEFAULT_MODE": (
|
||||
("sim", "模拟盘"),
|
||||
("live", "实盘"),
|
||||
|
||||
Vendored
+13
-1
@@ -279,10 +279,20 @@ _HEDGE_COMMON_FIELDS: list[tuple[str, str, str]] = [
|
||||
]
|
||||
|
||||
_HEDGE_PO_FIELDS: list[tuple[str, str, str]] = [
|
||||
(
|
||||
"HEDGE_PO_MODE",
|
||||
"永期实例模式",
|
||||
"eth/btc/btc_eth;一实例一种模式对应一个子账户;模拟与实盘同路径;改后须重启",
|
||||
),
|
||||
(
|
||||
"HEDGE_CROSS_NOTIONAL_K",
|
||||
"跨币永续名义系数",
|
||||
"仅 btc_eth;默认 0.5=永续名义为期权名义一半;改后须重启",
|
||||
),
|
||||
(
|
||||
"HEDGE_PLAN_OPTION_PRIMARY",
|
||||
"永期模式(以期权为主/保险)",
|
||||
"默认 true=以期权为主;false=保险模式;页面标题前显示标识,不可在页内切换",
|
||||
"实例模式强制以期权为主;一般保持 true",
|
||||
),
|
||||
("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
|
||||
("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
|
||||
@@ -364,6 +374,8 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"HEDGE_PLAN_OO_BIAS_RATIO": "0.7",
|
||||
"HEDGE_PLAN_BUDGET_BUFFER": "0.95",
|
||||
"HEDGE_PLAN_OPTION_PRIMARY": "true",
|
||||
"HEDGE_PO_MODE": "eth",
|
||||
"HEDGE_CROSS_NOTIONAL_K": "0.5",
|
||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true",
|
||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
|
||||
"OKX_OPTIONS_CLOSE_GATE_MODE": "premium",
|
||||
|
||||
@@ -90,6 +90,9 @@ def init_hedge_plan_tables(conn: sqlite3.Connection) -> None:
|
||||
# 出场:profit=盈利金额(扣双边费);target=目标位点数
|
||||
_ensure_column(conn, "hedge_plans", "exit_mode", "TEXT")
|
||||
_ensure_column(conn, "hedge_plans", "profit_target_usdt", "REAL")
|
||||
# 永期实例模式:eth|btc|btc_eth;跨币时 underlying=期权标的,perp_underlying=永续标的
|
||||
_ensure_column(conn, "hedge_plans", "po_mode", "TEXT")
|
||||
_ensure_column(conn, "hedge_plans", "perp_underlying", "TEXT")
|
||||
_ensure_column(conn, "hedge_plan_legs", "ct_mult", "REAL")
|
||||
|
||||
|
||||
|
||||
@@ -695,6 +695,7 @@ def _tick_po_option_primary(
|
||||
target_hit,
|
||||
)
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import _close_perp
|
||||
from lib.hedge_plan.hedge_po_mode_lib import combo_net_usdt
|
||||
|
||||
perp = next((x for x in legs if x.get("leg_role") == "perp"), None)
|
||||
opt = next((x for x in legs if x.get("leg_role") == "option_hedge"), None)
|
||||
@@ -714,7 +715,9 @@ def _tick_po_option_primary(
|
||||
profit_target = _plan_profit_target(plan)
|
||||
if strike is None or strike <= 0:
|
||||
return None
|
||||
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
||||
# 期权指数:计划 underlying(跨币=BTC);目标位同口径
|
||||
opt_uly = str(plan.get("underlying") or plan.get("option_underlying") or "ETH").upper()
|
||||
idx = _index_px(cfg, opt_uly)
|
||||
if idx is None:
|
||||
return None
|
||||
|
||||
@@ -743,6 +746,13 @@ def _tick_po_option_primary(
|
||||
mark = _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last"))
|
||||
except Exception:
|
||||
mark = None
|
||||
# 跨币:永续标记用 ETH,不可回退到 BTC 指数
|
||||
if mark is None:
|
||||
perp_uly = str(plan.get("perp_underlying") or "").upper()
|
||||
if not perp_uly and symbol:
|
||||
perp_uly = symbol.split("/")[0].upper()
|
||||
if perp_uly and perp_uly != opt_uly:
|
||||
mark = _index_px(cfg, perp_uly)
|
||||
mark = mark or idx
|
||||
entry = _sf(plan.get("entry_mark")) or _sf(perp.get("avg_open")) or mark
|
||||
cs = float(cfg.get("default_contract_size") or 0.01)
|
||||
@@ -787,10 +797,11 @@ def _tick_po_option_primary(
|
||||
)
|
||||
perp_px = _sf(book.get("vwap")) or mark or entry
|
||||
|
||||
# 优先期权腿平仓(须买一流动性);盈利金额模式要求扣双边费后净利≥目标
|
||||
# 优先期权腿平仓(须买一流动性);盈利金额模式要求扣双边费后组合净利(U)≥目标
|
||||
if hit_opt:
|
||||
liq_ok, liq_msg = option_bid_liquidity_ok(bid, bid_sz, need_sheets=sheets)
|
||||
net = None
|
||||
net_usdt = None
|
||||
if liq_ok:
|
||||
net = estimate_combo_net_pnl(
|
||||
view_side=view,
|
||||
@@ -806,17 +817,19 @@ def _tick_po_option_primary(
|
||||
contracts=contracts,
|
||||
contract_size=cs,
|
||||
)
|
||||
net_usdt = combo_net_usdt(net, option_index=float(idx))
|
||||
if exit_mode == "profit":
|
||||
can_opt_exit = bool(
|
||||
liq_ok and net is not None and float(net.get("net") or 0) + 1e-9 >= profit_target
|
||||
liq_ok and net_usdt is not None and float(net_usdt) + 1e-9 >= profit_target
|
||||
)
|
||||
else:
|
||||
can_opt_exit = bool(liq_ok and net is not None and float(net.get("net") or 0) > 0)
|
||||
# 目标位模式:组合 U 净利须 > 0(与持仓卡同口径)
|
||||
can_opt_exit = bool(liq_ok and net_usdt is not None and float(net_usdt) > 0)
|
||||
if can_opt_exit:
|
||||
reason = "profit_target_usdt" if exit_mode == "profit" else "opt_target_points"
|
||||
close_r = _sell_option(cfg, inst_id=str(opt.get("inst_id") or ""), sheets=sheets)
|
||||
if close_r.get("already_flat"):
|
||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=net["opt_net"])
|
||||
opt_pnl_coin = _option_leg_pnl_after_close(cfg, opt, fallback=float(net["opt_net"]))
|
||||
elif not close_r.get("ok") or not close_r.get("fully_closed", True):
|
||||
notify_hedge(
|
||||
cfg,
|
||||
@@ -829,10 +842,12 @@ def _tick_po_option_primary(
|
||||
update_plan(conn, int(plan["id"]), close_reason="opt_target_pending")
|
||||
return {"plan_id": plan["id"], "retry": True, "close": close_r}
|
||||
else:
|
||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=net["opt_net"])
|
||||
opt_pnl_coin = _option_leg_pnl_after_close(cfg, opt, fallback=float(net["opt_net"]))
|
||||
# 币本位期权盈亏折 U(×期权指数),与持仓卡/出场门控同口径
|
||||
opt_pnl_u = round(float(opt_pnl_coin) * float(idx), 4)
|
||||
conn.execute(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), round(opt_pnl, 4), opt["id"]),
|
||||
("closed", reason, _now(), opt_pnl_u, opt["id"]),
|
||||
)
|
||||
perp_close = _close_perp(
|
||||
cfg, symbol=symbol, direction=perp_dir, contracts=contracts, dry_run=False
|
||||
@@ -853,14 +868,14 @@ def _tick_po_option_primary(
|
||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||
("closed", reason, _now(), round(perp_pnl, 4), perp["id"]),
|
||||
)
|
||||
total = float(opt_pnl) + float(perp_pnl)
|
||||
total = float(net_usdt) if net_usdt is not None else (opt_pnl_u + perp_pnl)
|
||||
update_plan(
|
||||
conn,
|
||||
int(plan["id"]),
|
||||
status="closed",
|
||||
close_reason=reason,
|
||||
realized_pnl_perp=round(perp_pnl, 4),
|
||||
realized_pnl_options=round(opt_pnl, 4),
|
||||
realized_pnl_options=opt_pnl_u,
|
||||
realized_pnl_total=round(total, 4),
|
||||
stats_bucket="opt_primary",
|
||||
closed_at=_now(),
|
||||
@@ -872,6 +887,7 @@ def _tick_po_option_primary(
|
||||
"close_reason": reason,
|
||||
"total": total,
|
||||
"net": net,
|
||||
"net_usdt": net_usdt,
|
||||
"sell_coin": sell_r,
|
||||
}
|
||||
if exit_mode == "profit":
|
||||
@@ -881,16 +897,18 @@ def _tick_po_option_primary(
|
||||
"msg": (
|
||||
liq_msg
|
||||
if not liq_ok
|
||||
else f"净利未达目标 {profit_target:g}(当前 {float((net or {}).get('net') or 0):.2f})"
|
||||
else f"组合净利(U)未达目标 {profit_target:g}(当前 {float(net_usdt or 0):.2f})"
|
||||
),
|
||||
"net": net,
|
||||
"net_usdt": net_usdt,
|
||||
}
|
||||
if not hit_perp:
|
||||
return {
|
||||
"plan_id": plan["id"],
|
||||
"skip": True,
|
||||
"msg": (liq_msg if not liq_ok else "净利≤0,继续持有"),
|
||||
"msg": (liq_msg if not liq_ok else "组合净利(U)≤0,继续持有"),
|
||||
"net": net,
|
||||
"net_usdt": net_usdt,
|
||||
}
|
||||
|
||||
if not hit_perp:
|
||||
|
||||
@@ -141,8 +141,14 @@ def size_from_premium(
|
||||
ratio: float,
|
||||
contract_size: float,
|
||||
exec_factor: float = PREMIUM_EXEC_FACTOR,
|
||||
option_index: Optional[float] = None,
|
||||
perp_index: Optional[float] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""权利金×0.95 → ETH 两位小数 → 期权张 → 永续跟比例."""
|
||||
"""权利金×0.95 → 币两位小数 → 期权张 → 永续跟比例.
|
||||
|
||||
同币:永续币 = 期权币 / ratio.
|
||||
跨币(传入 option_index 与 perp_index):永续名义U = 期权名义U × (1/ratio).
|
||||
"""
|
||||
budget = float(premium_budget or 0)
|
||||
a = float(ask or 0)
|
||||
ct = float(ct_mult or 0.01)
|
||||
@@ -155,23 +161,25 @@ def size_from_premium(
|
||||
"msg": "定仓参数无效",
|
||||
"usable_premium": round(usable, 4),
|
||||
"eth_qty": 0.0,
|
||||
"coin_qty": 0.0,
|
||||
"sheets": 0.0,
|
||||
"perp_eth": 0.0,
|
||||
"contracts": 0.0,
|
||||
}
|
||||
# ask 为每 1 币权利金;ETH 数量 = usable / ask
|
||||
eth_qty = floor2(usable / a)
|
||||
if eth_qty <= 0:
|
||||
# ask 为每 1 币权利金;币数量 = usable / ask
|
||||
coin_qty = floor2(usable / a)
|
||||
if coin_qty <= 0:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": "权利金不足以买入 0.01 ETH 名义期权",
|
||||
"msg": "权利金不足以买入 0.01 币名义期权",
|
||||
"usable_premium": round(usable, 4),
|
||||
"eth_qty": 0.0,
|
||||
"coin_qty": 0.0,
|
||||
"sheets": 0.0,
|
||||
"perp_eth": 0.0,
|
||||
"contracts": 0.0,
|
||||
}
|
||||
sheets = eth_qty / ct
|
||||
sheets = coin_qty / ct
|
||||
# 张数向下取整到整数张(OKX 期权常见整张)
|
||||
sheets_i = float(math.floor(sheets + 1e-12))
|
||||
if sheets_i <= 0:
|
||||
@@ -179,27 +187,132 @@ def size_from_premium(
|
||||
"ok": False,
|
||||
"msg": "换算期权张数不足 1 张",
|
||||
"usable_premium": round(usable, 4),
|
||||
"eth_qty": eth_qty,
|
||||
"eth_qty": coin_qty,
|
||||
"coin_qty": coin_qty,
|
||||
"sheets": 0.0,
|
||||
"perp_eth": 0.0,
|
||||
"contracts": 0.0,
|
||||
}
|
||||
# 用整张回写 ETH,保持与下单一致
|
||||
eth_qty = round(sheets_i * ct, 2)
|
||||
perp_eth = eth_qty / r
|
||||
# 用整张回写币量,保持与下单一致
|
||||
coin_qty = round(sheets_i * ct, 2)
|
||||
oi = _sf(option_index)
|
||||
pi = _sf(perp_index)
|
||||
cross = oi is not None and pi is not None and oi > 0 and pi > 0 and abs(oi - pi) > 1e-6
|
||||
if cross or (oi is not None and pi is not None and oi > 0 and pi > 0):
|
||||
from lib.hedge_plan.hedge_po_mode_lib import size_cross_notional
|
||||
|
||||
cross_sz = size_cross_notional(
|
||||
option_coin_qty=coin_qty,
|
||||
option_index=float(oi),
|
||||
perp_index=float(pi),
|
||||
contract_size=cs,
|
||||
notional_k=(1.0 / r) if r > 0 else None,
|
||||
)
|
||||
if not cross_sz.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": cross_sz.get("msg") or "跨币定仓失败",
|
||||
"usable_premium": round(usable, 4),
|
||||
"eth_qty": coin_qty,
|
||||
"coin_qty": coin_qty,
|
||||
"sheets": sheets_i,
|
||||
"perp_eth": 0.0,
|
||||
"contracts": 0.0,
|
||||
}
|
||||
perp_coin = float(cross_sz["perp_coin"])
|
||||
contracts = float(cross_sz["contracts"])
|
||||
premium_est = a * sheets_i * ct
|
||||
return {
|
||||
"ok": True,
|
||||
"msg": "",
|
||||
"usable_premium": round(usable, 4),
|
||||
"eth_qty": coin_qty,
|
||||
"coin_qty": coin_qty,
|
||||
"sheets": sheets_i,
|
||||
"perp_eth": round(perp_coin, 6),
|
||||
"contracts": contracts,
|
||||
"premium_est": round(premium_est, 4),
|
||||
"ratio": r,
|
||||
"exec_factor": float(exec_factor or PREMIUM_EXEC_FACTOR),
|
||||
"option_notional_usdt": cross_sz.get("option_notional_usdt"),
|
||||
"perp_notional_usdt": cross_sz.get("perp_notional_usdt"),
|
||||
"notional_k": cross_sz.get("notional_k"),
|
||||
"cross_asset": bool(cross),
|
||||
}
|
||||
perp_eth = coin_qty / r
|
||||
contracts = perp_eth / cs
|
||||
premium_est = a * sheets_i * ct
|
||||
return {
|
||||
"ok": True,
|
||||
"msg": "",
|
||||
"usable_premium": round(usable, 4),
|
||||
"eth_qty": eth_qty,
|
||||
"eth_qty": coin_qty,
|
||||
"coin_qty": coin_qty,
|
||||
"sheets": sheets_i,
|
||||
"perp_eth": round(perp_eth, 6),
|
||||
"contracts": contracts,
|
||||
"premium_est": round(premium_est, 4),
|
||||
"ratio": r,
|
||||
"exec_factor": float(exec_factor or PREMIUM_EXEC_FACTOR),
|
||||
"cross_asset": False,
|
||||
}
|
||||
|
||||
|
||||
def contracts_from_option_sheets(
|
||||
*,
|
||||
sheets: float,
|
||||
ct_mult: float,
|
||||
ratio: float,
|
||||
contract_size: float,
|
||||
option_index: Optional[float] = None,
|
||||
perp_index: Optional[float] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""手动张数 → 永续张数(同币或跨币名义)."""
|
||||
sheets_i = float(int(float(sheets or 0)))
|
||||
ct = float(ct_mult or 0.01)
|
||||
r = float(ratio or 0)
|
||||
cs = float(contract_size or 0.01)
|
||||
if sheets_i <= 0 or ct <= 0 or r <= 0 or cs <= 0:
|
||||
return {"ok": False, "msg": "定仓参数无效", "coin_qty": 0.0, "contracts": 0.0}
|
||||
coin_qty = round(sheets_i * ct, 2)
|
||||
oi = _sf(option_index)
|
||||
pi = _sf(perp_index)
|
||||
if oi is not None and pi is not None and oi > 0 and pi > 0:
|
||||
from lib.hedge_plan.hedge_po_mode_lib import size_cross_notional
|
||||
|
||||
cross_sz = size_cross_notional(
|
||||
option_coin_qty=coin_qty,
|
||||
option_index=float(oi),
|
||||
perp_index=float(pi),
|
||||
contract_size=cs,
|
||||
notional_k=(1.0 / r) if r > 0 else None,
|
||||
)
|
||||
if not cross_sz.get("ok"):
|
||||
return {"ok": False, "msg": cross_sz.get("msg") or "跨币定仓失败", "coin_qty": coin_qty}
|
||||
return {
|
||||
"ok": True,
|
||||
"msg": "",
|
||||
"sheets": sheets_i,
|
||||
"coin_qty": coin_qty,
|
||||
"eth_qty": coin_qty,
|
||||
"contracts": float(cross_sz["contracts"]),
|
||||
"perp_coin": float(cross_sz["perp_coin"]),
|
||||
"option_notional_usdt": cross_sz.get("option_notional_usdt"),
|
||||
"perp_notional_usdt": cross_sz.get("perp_notional_usdt"),
|
||||
"ratio": r,
|
||||
"cross_asset": abs(float(oi) - float(pi)) > 1e-6,
|
||||
}
|
||||
perp_coin = coin_qty / r
|
||||
return {
|
||||
"ok": True,
|
||||
"msg": "",
|
||||
"sheets": sheets_i,
|
||||
"coin_qty": coin_qty,
|
||||
"eth_qty": coin_qty,
|
||||
"contracts": perp_coin / cs,
|
||||
"perp_coin": perp_coin,
|
||||
"ratio": r,
|
||||
"cross_asset": False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -777,6 +777,16 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
|
||||
if q.get("ct_mult") is not None:
|
||||
body["ct_mult"] = float(q.get("ct_mult") or 0.01)
|
||||
if is_option_primary(body):
|
||||
from lib.hedge_plan.hedge_plan_option_primary_lib import contracts_from_option_sheets
|
||||
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||
apply_mode_to_start_body,
|
||||
get_hedge_po_mode,
|
||||
is_cross_asset,
|
||||
option_underlying,
|
||||
perp_underlying,
|
||||
)
|
||||
|
||||
body.update(apply_mode_to_start_body(body))
|
||||
cs = float(body.get("contract_size") or 0.01)
|
||||
get_cs = cfg.get("get_contract_size")
|
||||
sym = str(body.get("exchange_symbol") or "")
|
||||
@@ -789,20 +799,41 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
|
||||
ct = float(body.get("ct_mult") or q.get("ct_mult") or 0.01)
|
||||
ratio = float(body.get("option_perp_ratio") or 2)
|
||||
body["contract_size"] = cs
|
||||
opt_idx = None
|
||||
perp_idx = None
|
||||
try:
|
||||
from lib.exchange.okx_public_http_lib import fetch_index_px
|
||||
|
||||
mode = str(body.get("po_mode") or get_hedge_po_mode())
|
||||
opt_idx = float(fetch_index_px(option_underlying(mode)) or 0) or None
|
||||
if is_cross_asset(mode):
|
||||
perp_idx = float(fetch_index_px(perp_underlying(mode)) or 0) or None
|
||||
else:
|
||||
perp_idx = opt_idx
|
||||
except Exception:
|
||||
opt_idx = perp_idx = None
|
||||
# 页面传入的 entry/index 作回退
|
||||
if opt_idx is None:
|
||||
try:
|
||||
opt_idx = float(body.get("index_px") or body.get("entry") or 0) or None
|
||||
except (TypeError, ValueError):
|
||||
opt_idx = None
|
||||
if perp_idx is None:
|
||||
perp_idx = opt_idx
|
||||
if market and float(body.get("sheets") or 0) > 0:
|
||||
# 手动下单:以张数为准,永续按比例
|
||||
sheets_i = float(int(float(body["sheets"])))
|
||||
eth = round(sheets_i * ct, 2)
|
||||
body["sheets"] = sheets_i
|
||||
body["eth_qty"] = eth
|
||||
body["contracts"] = (eth / ratio) / cs if ratio > 0 and cs > 0 else 0
|
||||
sized = {
|
||||
"ok": True,
|
||||
"sheets": sheets_i,
|
||||
"eth_qty": eth,
|
||||
"contracts": body["contracts"],
|
||||
"ratio": ratio,
|
||||
}
|
||||
sized = contracts_from_option_sheets(
|
||||
sheets=float(body["sheets"]),
|
||||
ct_mult=ct,
|
||||
ratio=ratio,
|
||||
contract_size=cs,
|
||||
option_index=opt_idx,
|
||||
perp_index=perp_idx,
|
||||
)
|
||||
if not sized.get("ok"):
|
||||
return {"ok": False, "msg": sized.get("msg") or "定仓失败", "quote": q, "sizing": sized}
|
||||
body["sheets"] = sized["sheets"]
|
||||
body["eth_qty"] = sized.get("coin_qty") or sized.get("eth_qty")
|
||||
body["contracts"] = sized["contracts"]
|
||||
else:
|
||||
sized = size_from_premium(
|
||||
premium_budget=float(body.get("premium_budget") or 0),
|
||||
@@ -810,21 +841,39 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
|
||||
ct_mult=ct,
|
||||
ratio=ratio,
|
||||
contract_size=cs,
|
||||
option_index=opt_idx,
|
||||
perp_index=perp_idx,
|
||||
)
|
||||
if not sized.get("ok"):
|
||||
return {"ok": False, "msg": sized.get("msg") or "定仓失败", "quote": q, "sizing": sized}
|
||||
body["sheets"] = sized["sheets"]
|
||||
body["contracts"] = sized["contracts"]
|
||||
body["eth_qty"] = sized["eth_qty"]
|
||||
body["eth_qty"] = sized.get("coin_qty") or sized["eth_qty"]
|
||||
# 深度不足则缩量
|
||||
ask_sz = float(q.get("ask_sz") or 0)
|
||||
if ask_sz > 0 and float(body["sheets"]) > ask_sz:
|
||||
body["sheets"] = float(int(ask_sz))
|
||||
if body["sheets"] <= 0:
|
||||
return {"ok": False, "msg": "卖一深度不足 1 张", "quote": q, "sizing": sized}
|
||||
eth = round(float(body["sheets"]) * ct, 2)
|
||||
body["eth_qty"] = eth
|
||||
body["contracts"] = (eth / ratio) / cs if ratio > 0 and cs > 0 else 0
|
||||
resized = contracts_from_option_sheets(
|
||||
sheets=float(body["sheets"]),
|
||||
ct_mult=ct,
|
||||
ratio=ratio,
|
||||
contract_size=cs,
|
||||
option_index=opt_idx,
|
||||
perp_index=perp_idx,
|
||||
)
|
||||
if not resized.get("ok"):
|
||||
return {"ok": False, "msg": resized.get("msg") or "缩量定仓失败", "quote": q}
|
||||
body["eth_qty"] = resized.get("coin_qty") or resized.get("eth_qty")
|
||||
body["contracts"] = resized["contracts"]
|
||||
sized = resized
|
||||
cross_note = ""
|
||||
if sized.get("cross_asset") or sized.get("option_notional_usdt"):
|
||||
cross_note = (
|
||||
f" · 名义U 期权{float(sized.get('option_notional_usdt') or 0):.0f}"
|
||||
f"/永续{float(sized.get('perp_notional_usdt') or 0):.0f}"
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"ask": float(q["ask"]),
|
||||
@@ -837,10 +886,12 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
|
||||
"msg": (
|
||||
f"手动定仓: {body.get('eth_qty')}币 / "
|
||||
f"{body.get('sheets')}张期权 / {float(body.get('contracts') or 0):.4f}张永续 @{q['ask']}"
|
||||
f"{cross_note}"
|
||||
if market
|
||||
else (
|
||||
f"期权为主定仓: 权利金×0.95→{body.get('eth_qty')}ETH / "
|
||||
f"期权为主定仓: 权利金×0.95→{body.get('eth_qty')}币 / "
|
||||
f"{body.get('sheets')}张期权 / {float(body.get('contracts') or 0):.4f}张永续 @{q['ask']}"
|
||||
f"{cross_note}"
|
||||
)
|
||||
),
|
||||
}
|
||||
@@ -1184,7 +1235,16 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
|
||||
is_option_primary,
|
||||
validate_option_primary_start,
|
||||
)
|
||||
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||
apply_mode_to_start_body,
|
||||
validate_body_matches_mode,
|
||||
)
|
||||
|
||||
# 模拟/实盘同路径:启动前按实例模式钳制
|
||||
body.update(apply_mode_to_start_body(body))
|
||||
mode_err = validate_body_matches_mode(body)
|
||||
if mode_err:
|
||||
return mode_err
|
||||
if is_option_primary(body):
|
||||
from lib.hedge_plan.hedge_plan_option_primary_lib import validate_option_primary_watch
|
||||
|
||||
|
||||
@@ -39,8 +39,13 @@ def attach_hedge_plan_templates(app: Flask, repo_root: str) -> None:
|
||||
|
||||
|
||||
def install_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None:
|
||||
from lib.hedge_plan.hedge_po_mode_lib import clamp_hedge_po_mode_on_startup, public_mode_payload
|
||||
|
||||
attach_hedge_plan_templates(app, repo_root)
|
||||
mode_summary = clamp_hedge_po_mode_on_startup()
|
||||
cfg = _build_cfg(app_module)
|
||||
cfg["po_mode"] = mode_summary.get("mode")
|
||||
cfg["po_mode_payload"] = public_mode_payload(mode_summary.get("mode"))
|
||||
app.extensions["hedge_plan_cfg"] = cfg
|
||||
register_hedge_plan_routes(app, cfg)
|
||||
_maybe_start_monitor(cfg)
|
||||
@@ -98,6 +103,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"),
|
||||
"perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
|
||||
"options_account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "期权账户").strip(),
|
||||
"po_mode": (os.getenv("HEDGE_PO_MODE") or "").strip().lower(),
|
||||
"trade_budget_usdc": float(
|
||||
os.getenv("OKX_OPTIONS_COIN_BUDGET_USDT")
|
||||
or os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC")
|
||||
@@ -318,16 +324,25 @@ def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
||||
perp_direction_for_view,
|
||||
)
|
||||
|
||||
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||
apply_mode_to_start_body,
|
||||
get_hedge_po_mode,
|
||||
option_underlying,
|
||||
perp_underlying,
|
||||
)
|
||||
|
||||
body = apply_mode_to_start_body(body)
|
||||
opt_primary = is_option_primary(body)
|
||||
view = str(body.get("direction") or "long")
|
||||
perp_dir = (
|
||||
str((perp or {}).get("direction") or "")
|
||||
or (perp_direction_for_view(view) if opt_primary else view)
|
||||
)
|
||||
po_mode = str(body.get("po_mode") or get_hedge_po_mode())
|
||||
plan_row = {
|
||||
"plan_type": "perp_options",
|
||||
"status": "partial" if is_partial else "active",
|
||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
||||
"underlying": str(body.get("underlying") or option_underlying(po_mode)).upper(),
|
||||
"direction": view,
|
||||
"entry_mark": float(body.get("entry") or 0),
|
||||
"tp": float(body.get("tp") or 0) if not opt_primary else 0,
|
||||
@@ -343,11 +358,12 @@ def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any
|
||||
"note": (result.get("msg") or "")[:500] if is_partial else None,
|
||||
"option_primary": 1 if opt_primary else 0,
|
||||
"perp_direction": perp_dir,
|
||||
"po_mode": po_mode,
|
||||
"perp_underlying": str(body.get("perp_underlying") or perp_underlying(po_mode)).upper(),
|
||||
}
|
||||
if opt_primary:
|
||||
exit_mode = str(body.get("exit_mode") or "target").strip().lower()
|
||||
if exit_mode not in ("profit", "target"):
|
||||
exit_mode = "target"
|
||||
# 实例模式强制组合盈利金额出场
|
||||
exit_mode = "profit"
|
||||
plan_row.update(
|
||||
{
|
||||
"option_target_points": float(body.get("option_target_points") or 0),
|
||||
@@ -410,18 +426,26 @@ def _persist_po_watching(cfg: dict[str, Any], body: dict[str, Any]) -> int:
|
||||
"""以期权为主:只落库盯盘计划,不下单."""
|
||||
from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, insert_plan
|
||||
from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view
|
||||
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||
apply_mode_to_start_body,
|
||||
get_hedge_po_mode,
|
||||
option_underlying,
|
||||
perp_underlying,
|
||||
)
|
||||
|
||||
body = apply_mode_to_start_body(body)
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_hedge_plan_tables(conn)
|
||||
view = str(body.get("direction") or "long")
|
||||
money = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower()
|
||||
po_mode = str(body.get("po_mode") or get_hedge_po_mode())
|
||||
plan_id = insert_plan(
|
||||
conn,
|
||||
{
|
||||
"plan_type": "perp_options",
|
||||
"status": "watching",
|
||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
||||
"underlying": str(body.get("underlying") or option_underlying(po_mode)).upper(),
|
||||
"direction": view,
|
||||
"entry_mark": float(body.get("index_px") or body.get("entry") or 0) or None,
|
||||
"tp": 0,
|
||||
@@ -445,6 +469,10 @@ def _persist_po_watching(cfg: dict[str, Any], body: dict[str, Any]) -> int:
|
||||
"min_option_hours": float(body.get("min_option_hours") or 36),
|
||||
"option_moneyness": money,
|
||||
"option_leverage": float(body.get("option_leverage") or 0),
|
||||
"exit_mode": "profit",
|
||||
"profit_target_usdt": float(body.get("profit_target_usdt") or 0),
|
||||
"po_mode": po_mode,
|
||||
"perp_underlying": str(body.get("perp_underlying") or perp_underlying(po_mode)).upper(),
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
@@ -650,41 +678,51 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
@app.route("/api/hedge-plan/market")
|
||||
@lr
|
||||
def api_hedge_market():
|
||||
base = (request.args.get("base") or cfg.get("default_underly") or "ETH").strip().upper()
|
||||
if base not in ("BTC", "ETH"):
|
||||
return jsonify({"ok": False, "msg": "对冲计划仅支持 BTC/ETH"}), 400
|
||||
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||
get_hedge_po_mode,
|
||||
option_underlying,
|
||||
perp_underlying,
|
||||
public_mode_payload,
|
||||
)
|
||||
|
||||
mode = get_hedge_po_mode()
|
||||
mode_payload = public_mode_payload(mode)
|
||||
# 永续行情按实例模式的永续标的;跨币忽略前端乱传的 base
|
||||
base = perp_underlying(mode)
|
||||
req_base = (request.args.get("base") or "").strip().upper()
|
||||
if req_base in ("BTC", "ETH") and req_base == base:
|
||||
pass
|
||||
elif req_base and req_base != base:
|
||||
# 兼容旧前端:仍返回正确永续,并提示
|
||||
pass
|
||||
direction = (request.args.get("direction") or "long").strip().lower()
|
||||
if direction not in ("long", "short"):
|
||||
direction = "long"
|
||||
option_primary = (request.args.get("option_primary") or "").strip().lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
)
|
||||
# 实例模式强制以期权为主
|
||||
option_primary = True
|
||||
data, err = _fetch_perp_market(cfg, base)
|
||||
if err:
|
||||
return jsonify({"ok": False, "msg": err}), 400
|
||||
sizing_mode = load_position_sizing_mode()
|
||||
gates = _gates_dict(cfg, "perp_options")
|
||||
if option_primary:
|
||||
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
||||
opt_type_for_view,
|
||||
perp_direction_for_view,
|
||||
)
|
||||
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
||||
opt_type_for_view,
|
||||
perp_direction_for_view,
|
||||
)
|
||||
|
||||
suggested = opt_type_for_view(direction)
|
||||
perp_dir = perp_direction_for_view(direction)
|
||||
acct_note = "以期权为主:看法腿买期权,永续反向对冲"
|
||||
else:
|
||||
suggested = "P" if direction == "long" else "C"
|
||||
perp_dir = direction
|
||||
acct_note = "永续腿使用合约(交易)账户可用 USDT"
|
||||
suggested = opt_type_for_view(direction)
|
||||
perp_dir = perp_direction_for_view(direction)
|
||||
acct_note = (
|
||||
f"实例模式 {mode_payload['po_mode_label']}:期权 {option_underlying(mode)}"
|
||||
f" + 永续 {base};一子账户互不干涉"
|
||||
)
|
||||
out = {
|
||||
"ok": True,
|
||||
"base": base,
|
||||
"option_underlying": option_underlying(mode),
|
||||
"perp_underlying": base,
|
||||
"direction": direction,
|
||||
"option_primary": option_primary,
|
||||
"option_primary": True,
|
||||
"suggested_opt_type": suggested,
|
||||
"perp_direction": perp_dir,
|
||||
**data,
|
||||
@@ -693,9 +731,17 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
"account_kind": "perp",
|
||||
"account_label": cfg.get("perp_account_label") or "合约账户",
|
||||
"account_note": acct_note,
|
||||
**mode_payload,
|
||||
}
|
||||
return jsonify(out)
|
||||
|
||||
@app.route("/api/hedge-plan/po-mode")
|
||||
@lr
|
||||
def api_hedge_po_mode():
|
||||
from lib.hedge_plan.hedge_po_mode_lib import public_mode_payload
|
||||
|
||||
return jsonify({"ok": True, **public_mode_payload()})
|
||||
|
||||
@app.route("/api/hedge-plan/options-chain")
|
||||
@lr
|
||||
def api_hedge_options_chain():
|
||||
@@ -704,7 +750,19 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
ex = cfg.get("exchange_options")
|
||||
if ex is None:
|
||||
return jsonify({"ok": False, "msg": "期权交易所未初始化"}), 400
|
||||
u = (request.args.get("underlying") or cfg.get("default_underly") or "ETH").upper()
|
||||
from lib.hedge_plan.hedge_po_mode_lib import get_hedge_po_mode, option_underlying
|
||||
|
||||
# 期权链锁定实例模式的期权标的(跨币=BTC)
|
||||
u = option_underlying(get_hedge_po_mode())
|
||||
req_u = (request.args.get("underlying") or "").strip().upper()
|
||||
if req_u in ("BTC", "ETH") and req_u != u:
|
||||
return jsonify(
|
||||
{
|
||||
"ok": False,
|
||||
"msg": f"本实例模式期权标的为 {u},不可拉 {req_u} 链",
|
||||
"option_underlying": u,
|
||||
}
|
||||
), 400
|
||||
# 热更新:链展示天数每次读 env
|
||||
chain_max_dte = float(
|
||||
os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS")
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
"""永期对冲实例模式(env: HEDGE_PO_MODE).
|
||||
|
||||
一实例一种模式,对应一个 OKX 子账户;模拟盘与实盘同一执行路径.
|
||||
|
||||
eth → 期权 ETH + 永续 ETH
|
||||
btc → 期权 BTC + 永续 BTC
|
||||
btc_eth → 期权 BTC + 永续 ETH;永续名义 = 期权名义 × HEDGE_CROSS_NOTIONAL_K(默认 0.5)
|
||||
|
||||
方向(以期权为主):Call→永续空 / Put→永续多.
|
||||
出场:组合盈利金额(U),扣开平双边手续费.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MODE_ETH = "eth"
|
||||
MODE_BTC = "btc"
|
||||
MODE_BTC_ETH = "btc_eth"
|
||||
VALID_PO_MODES = frozenset({MODE_ETH, MODE_BTC, MODE_BTC_ETH})
|
||||
|
||||
DEFAULT_CROSS_NOTIONAL_K = 0.5
|
||||
|
||||
_ALIASES = {
|
||||
"eth_eth": MODE_ETH,
|
||||
"e": MODE_ETH,
|
||||
"eth-only": MODE_ETH,
|
||||
"btc_btc": MODE_BTC,
|
||||
"b": MODE_BTC,
|
||||
"btc-only": MODE_BTC,
|
||||
"cross": MODE_BTC_ETH,
|
||||
"btc-eth": MODE_BTC_ETH,
|
||||
"btc/eth": MODE_BTC_ETH,
|
||||
"跨币": MODE_BTC_ETH,
|
||||
}
|
||||
|
||||
|
||||
def _sf(v: Any) -> Optional[float]:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def normalize_hedge_po_mode(raw: Optional[str]) -> str:
|
||||
s = str(raw or "").strip().lower().replace("-", "_").replace("/", "_")
|
||||
if s in VALID_PO_MODES:
|
||||
return s
|
||||
if s in _ALIASES:
|
||||
return _ALIASES[s]
|
||||
return ""
|
||||
|
||||
|
||||
def infer_hedge_po_mode_from_underly() -> str:
|
||||
u = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper()
|
||||
if u == "BTC":
|
||||
return MODE_BTC
|
||||
return MODE_ETH
|
||||
|
||||
|
||||
def get_hedge_po_mode() -> str:
|
||||
m = normalize_hedge_po_mode(os.getenv("HEDGE_PO_MODE"))
|
||||
if m:
|
||||
return m
|
||||
return infer_hedge_po_mode_from_underly()
|
||||
|
||||
|
||||
def cross_notional_k() -> float:
|
||||
"""跨币:永续名义 / 期权名义.默认 0.5."""
|
||||
v = _sf(os.getenv("HEDGE_CROSS_NOTIONAL_K"))
|
||||
if v is None or v <= 0:
|
||||
return DEFAULT_CROSS_NOTIONAL_K
|
||||
# 合理钳制,避免误配把永续打到极端
|
||||
return max(0.05, min(float(v), 5.0))
|
||||
|
||||
|
||||
def default_option_perp_ratio(mode: Optional[str] = None) -> float:
|
||||
"""UI/定仓用比例:期权名义 ÷ 比例 = 永续名义 → ratio = 1/k."""
|
||||
m = mode or get_hedge_po_mode()
|
||||
if m == MODE_BTC_ETH:
|
||||
k = cross_notional_k()
|
||||
return round(1.0 / k, 6) if k > 0 else 2.0
|
||||
return 2.0
|
||||
|
||||
|
||||
def option_underlying(mode: Optional[str] = None) -> str:
|
||||
m = mode or get_hedge_po_mode()
|
||||
if m in (MODE_BTC, MODE_BTC_ETH):
|
||||
return "BTC"
|
||||
return "ETH"
|
||||
|
||||
|
||||
def perp_underlying(mode: Optional[str] = None) -> str:
|
||||
m = mode or get_hedge_po_mode()
|
||||
if m == MODE_BTC:
|
||||
return "BTC"
|
||||
return "ETH"
|
||||
|
||||
|
||||
def is_cross_asset(mode: Optional[str] = None) -> bool:
|
||||
return (mode or get_hedge_po_mode()) == MODE_BTC_ETH
|
||||
|
||||
|
||||
def mode_label(mode: Optional[str] = None) -> str:
|
||||
m = mode or get_hedge_po_mode()
|
||||
return {
|
||||
MODE_ETH: "ETH永期",
|
||||
MODE_BTC: "BTC永期",
|
||||
MODE_BTC_ETH: "跨币BTC期权/ETH永续",
|
||||
}.get(m, m or "—")
|
||||
|
||||
|
||||
def mode_short_label(mode: Optional[str] = None) -> str:
|
||||
m = mode or get_hedge_po_mode()
|
||||
return {
|
||||
MODE_ETH: "ETH",
|
||||
MODE_BTC: "BTC",
|
||||
MODE_BTC_ETH: "BTC→ETH",
|
||||
}.get(m, m or "—")
|
||||
|
||||
|
||||
def clamp_hedge_po_mode_on_startup() -> dict[str, Any]:
|
||||
"""启动钳制:规范化 HEDGE_PO_MODE,并同步默认期权标的.
|
||||
|
||||
不写回 .env 文件(避免热更/多实例踩踏);仅 os.environ 进程内生效.
|
||||
返回审计摘要供日志/测试.
|
||||
"""
|
||||
raw = (os.getenv("HEDGE_PO_MODE") or "").strip()
|
||||
normalized = normalize_hedge_po_mode(raw) if raw else ""
|
||||
warnings: list[str] = []
|
||||
if raw and not normalized:
|
||||
warnings.append(f"非法 HEDGE_PO_MODE={raw!r},回退推断")
|
||||
mode = infer_hedge_po_mode_from_underly()
|
||||
elif normalized:
|
||||
mode = normalized
|
||||
else:
|
||||
mode = infer_hedge_po_mode_from_underly()
|
||||
warnings.append(f"未配置 HEDGE_PO_MODE,按 OKX_OPTIONS_DEFAULT_UNDERLY 推断为 {mode}")
|
||||
|
||||
os.environ["HEDGE_PO_MODE"] = mode
|
||||
opt_u = option_underlying(mode)
|
||||
prev_u = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "").strip().upper()
|
||||
if prev_u != opt_u:
|
||||
os.environ["OKX_OPTIONS_DEFAULT_UNDERLY"] = opt_u
|
||||
if prev_u:
|
||||
warnings.append(f"同步 OKX_OPTIONS_DEFAULT_UNDERLY {prev_u}→{opt_u}")
|
||||
|
||||
k = cross_notional_k()
|
||||
os.environ["HEDGE_CROSS_NOTIONAL_K"] = str(k)
|
||||
summary = {
|
||||
"ok": True,
|
||||
"mode": mode,
|
||||
"label": mode_label(mode),
|
||||
"option_underlying": opt_u,
|
||||
"perp_underlying": perp_underlying(mode),
|
||||
"cross_notional_k": k,
|
||||
"option_perp_ratio": default_option_perp_ratio(mode),
|
||||
"warnings": warnings,
|
||||
}
|
||||
for w in warnings:
|
||||
logger.warning("[HEDGE_PO_MODE] %s", w)
|
||||
logger.info(
|
||||
"[HEDGE_PO_MODE] mode=%s option=%s perp=%s k=%s (一实例一模式一子账户)",
|
||||
mode,
|
||||
opt_u,
|
||||
summary["perp_underlying"],
|
||||
k,
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
def apply_mode_to_start_body(body: dict[str, Any], *, mode: Optional[str] = None) -> dict[str, Any]:
|
||||
"""启动前按实例模式钳制 body(模拟/实盘同路径)."""
|
||||
m = mode or get_hedge_po_mode()
|
||||
out = dict(body or {})
|
||||
opt_u = option_underlying(m)
|
||||
perp_u = perp_underlying(m)
|
||||
out["po_mode"] = m
|
||||
out["underlying"] = opt_u
|
||||
out["option_underlying"] = opt_u
|
||||
out["perp_underlying"] = perp_u
|
||||
# 以期权为主 + 组合盈利金额出场
|
||||
out["option_primary"] = True
|
||||
out["exit_mode"] = "profit"
|
||||
ratio = default_option_perp_ratio(m)
|
||||
# 允许用户略调比例,但跨币默认锁到 1/k;若未填则写入默认
|
||||
if out.get("option_perp_ratio") in (None, ""):
|
||||
out["option_perp_ratio"] = ratio
|
||||
elif m == MODE_BTC_ETH:
|
||||
# 跨币强制名义系数,避免页内误改导致与子账户策略不一致
|
||||
out["option_perp_ratio"] = ratio
|
||||
# 永续 symbol:若未传或与模式不符则按 perp 标的重建
|
||||
sym = str(out.get("exchange_symbol") or "").upper()
|
||||
want_prefix = f"{perp_u}/"
|
||||
if not sym or not sym.startswith(want_prefix):
|
||||
out["exchange_symbol"] = f"{perp_u}/USDT:USDT"
|
||||
return out
|
||||
|
||||
|
||||
def validate_body_matches_mode(body: dict[str, Any], *, mode: Optional[str] = None) -> Optional[str]:
|
||||
m = mode or get_hedge_po_mode()
|
||||
opt_u = option_underlying(m)
|
||||
perp_u = perp_underlying(m)
|
||||
uly = str(body.get("underlying") or body.get("option_underlying") or "").upper()
|
||||
if uly and uly != opt_u:
|
||||
return f"本实例模式 {mode_label(m)} 期权标的须为 {opt_u},收到 {uly}"
|
||||
inst = str(body.get("opt_inst_id") or "").upper()
|
||||
if inst and not inst.startswith(f"{opt_u}-"):
|
||||
return f"期权合约须为 {opt_u} 标的(本实例 {mode_label(m)})"
|
||||
sym = str(body.get("exchange_symbol") or "").upper()
|
||||
if sym and not sym.startswith(f"{perp_u}/"):
|
||||
return f"永续合约须为 {perp_u}(本实例 {mode_label(m)})"
|
||||
return None
|
||||
|
||||
|
||||
def combo_net_usdt(
|
||||
net: dict[str, Any] | None,
|
||||
*,
|
||||
option_index: float,
|
||||
) -> Optional[float]:
|
||||
"""币本位期权净利×期权指数 + 永续 U 净利 → 组合 U.
|
||||
|
||||
监控出场与持仓卡必须同口径;禁止把币与 U 直接相加.
|
||||
"""
|
||||
if not net:
|
||||
return None
|
||||
idx = float(option_index or 0)
|
||||
if idx <= 0:
|
||||
return None
|
||||
opt_coin = float(net.get("opt_net") or 0)
|
||||
perp_u = float(net.get("perp_net") or 0)
|
||||
return round(opt_coin * idx + perp_u, 4)
|
||||
|
||||
|
||||
def size_cross_notional(
|
||||
*,
|
||||
option_coin_qty: float,
|
||||
option_index: float,
|
||||
perp_index: float,
|
||||
contract_size: float,
|
||||
notional_k: Optional[float] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""跨币/同币统一:期权币名义 → U 名义 → 永续张数.
|
||||
|
||||
同币时 option_index≈perp_index,等价于 coin_qty * k / cs.
|
||||
"""
|
||||
k = float(notional_k if notional_k is not None else cross_notional_k())
|
||||
oc = float(option_coin_qty or 0)
|
||||
oi = float(option_index or 0)
|
||||
pi = float(perp_index or 0)
|
||||
cs = float(contract_size or 0.01)
|
||||
if oc <= 0 or oi <= 0 or pi <= 0 or cs <= 0 or k <= 0:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": "跨币定仓参数无效",
|
||||
"option_notional_usdt": 0.0,
|
||||
"perp_notional_usdt": 0.0,
|
||||
"perp_coin": 0.0,
|
||||
"contracts": 0.0,
|
||||
"notional_k": k,
|
||||
}
|
||||
opt_n = oc * oi
|
||||
perp_n = opt_n * k
|
||||
perp_coin = perp_n / pi
|
||||
contracts = perp_coin / cs
|
||||
return {
|
||||
"ok": True,
|
||||
"msg": "",
|
||||
"option_notional_usdt": round(opt_n, 4),
|
||||
"perp_notional_usdt": round(perp_n, 4),
|
||||
"perp_coin": round(perp_coin, 8),
|
||||
"contracts": contracts,
|
||||
"notional_k": k,
|
||||
}
|
||||
|
||||
|
||||
def public_mode_payload(mode: Optional[str] = None) -> dict[str, Any]:
|
||||
m = mode or get_hedge_po_mode()
|
||||
return {
|
||||
"po_mode": m,
|
||||
"po_mode_label": mode_label(m),
|
||||
"po_mode_short": mode_short_label(m),
|
||||
"option_underlying": option_underlying(m),
|
||||
"perp_underlying": perp_underlying(m),
|
||||
"cross_asset": is_cross_asset(m),
|
||||
"cross_notional_k": cross_notional_k() if is_cross_asset(m) else None,
|
||||
"option_perp_ratio": default_option_perp_ratio(m),
|
||||
"exit_mode": "profit",
|
||||
"option_primary": True,
|
||||
"instance_rule": "一实例一种模式对应一个子账户;模拟盘与实盘同路径;账户互不干涉",
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
<div class="hedge-plan-page-wrap" style="grid-column:1/-1" id="hedge-plan-root"
|
||||
data-default-underly="{{ options_default_underly | default('ETH') }}"
|
||||
data-default-underly="{{ hedge_option_underlying or options_default_underly | default('ETH') }}"
|
||||
data-po-mode="{{ hedge_po_mode | default('eth') }}"
|
||||
data-po-mode-label="{{ hedge_po_mode_label | default('ETH永期') }}"
|
||||
data-option-underly="{{ hedge_option_underlying or options_default_underly | default('ETH') }}"
|
||||
data-perp-underly="{{ hedge_perp_underlying or options_default_underly | default('ETH') }}"
|
||||
data-cross-asset="{{ '1' if hedge_cross_asset else '0' }}"
|
||||
data-cross-k="{{ hedge_cross_notional_k if hedge_cross_notional_k is not none else '' }}"
|
||||
data-option-perp-ratio="{{ hedge_option_perp_ratio | default(2) }}"
|
||||
data-hedge-enabled="{{ '1' if hedge_plan_enabled else '0' }}"
|
||||
data-options-enabled="{{ '1' if options_enabled else '0' }}"
|
||||
data-show-perp="{{ '1' if hedge_plan_show_perp_options else '0' }}"
|
||||
@@ -24,6 +31,7 @@
|
||||
{# 顶栏「对冲计划」已改为导航「永期/期期对冲」;页内不再重复大标题 #}
|
||||
<div class="hp-page-toolbar">
|
||||
<button type="button" class="btn-secondary" id="hp-refresh" title="刷新永续行情与期权链">刷新行情</button>
|
||||
<span class="hp-po-mode-badge" id="hp-po-mode-badge" title="一实例一种模式对应一个子账户;模拟与实盘同路径">{{ hedge_po_mode_label | default('ETH永期') }}</span>
|
||||
<span class="muted hp-po-gate-inline" id="hp-po-gate-inline" aria-live="polite"></span>
|
||||
</div>
|
||||
{# 模式由导航/env 决定,页内不再显示「永期对冲」Tab #}
|
||||
@@ -46,19 +54,22 @@
|
||||
<details class="tip-collapse hp-rule-collapse">
|
||||
<summary class="tip-collapse-summary">规则说明</summary>
|
||||
<div class="tip-collapse-body rule-tip">
|
||||
<p><strong>手动选约</strong>:左侧选 Call/Put 与到期,点「选择」弹窗下单。买 Call → 永续空;买 Put → 永续多。比例默认 <strong>2</strong>(期权名义÷比例=永续名义)。</p>
|
||||
<p><strong>实例模式</strong>:<code>HEDGE_PO_MODE</code>=<strong>{{ hedge_po_mode | default('eth') }}</strong>({{ hedge_po_mode_label | default('ETH永期') }})。一实例一种模式对应一个子账户;模拟盘与实盘同一执行路径;账户互不干涉。不可页内切换。</p>
|
||||
<p><strong>腿组合</strong>:期权 <strong>{{ hedge_option_underlying or 'ETH' }}</strong> + 永续 <strong>{{ hedge_perp_underlying or 'ETH' }}</strong>{% if hedge_cross_asset %};跨币名义系数 K=<strong>{{ hedge_cross_notional_k or 0.5 }}</strong>(永续名义=期权名义×K){% endif %}。买 Call → 永续空;买 Put → 永续多。比例默认 <strong>{{ hedge_option_perp_ratio | default(2) }}</strong>。</p>
|
||||
<p><strong>手动下单</strong>:开仓按<strong>卖一限价</strong>(非交易所市价)。先开期权(按张数买币并+5%滑点),再开永续。永续杠杆取 env:BTC=<code>BTC_LEVERAGE</code>/ETH=<code>ALT_LEVERAGE</code>。</p>
|
||||
<p><strong>出场</strong>:盈利金额(扣开平双边手续费后净利)或目标位点数二选一。平仓先期权(须买一流动性)再永续,再把币全部卖回 USDT。一账户同时仅一笔对冲仓。</p>
|
||||
<p><strong>开仓校验</strong>:已选合约 · 张数≥1 · 出场条件已填 · 门禁「开仓:可」· 账户有足够交易 USDT。</p>
|
||||
<p><strong>出场</strong>:仅<strong>组合盈利金额(U)</strong>(期权净利×期权指数 + 永续净利,扣开平双边手续费)。平仓先期权(须买一流动性)再永续,再把期权标的币卖回 USDT。一账户同时仅一笔对冲仓。</p>
|
||||
<p><strong>开仓校验</strong>:已选合约 · 张数≥1 · 盈利目标已填 · 门禁「开仓:可」· 账户有足够交易 USDT。</p>
|
||||
<p><strong>期权链</strong>:默认显示约 <strong>5</strong> 档(平值 + 实值2 + 虚值2);勾选「展开全部」查看该到期更多行权价。</p>
|
||||
<p><strong>账户</strong>:期权币本位 · 交易/资金计 USDT · 永续全仓 / 期权逐仓(余额见下方划转)。</p>
|
||||
<p><strong>组合净盈亏</strong>:扣开平双边手续费;永续按平仓方向盘口均价估值。</p>
|
||||
<p><strong>组合净盈亏</strong>:扣开平双边手续费;永续按平仓方向盘口均价估值;一律折 U。</p>
|
||||
<p class="muted" id="hp-po-pair-hint">买 Call + 永续空 · 点「选择」弹窗下单。</p>
|
||||
</div>
|
||||
</details>
|
||||
<div class="form-row hp-opt-toolbar hp-po-opt-toolbar options-chain-toolbar">
|
||||
<button type="button" class="btn-secondary hp-uly-btn active" data-uly="ETH">ETH</button>
|
||||
<button type="button" class="btn-secondary hp-uly-btn" data-uly="BTC">BTC</button>
|
||||
<span class="hp-uly-lock" id="hp-uly-lock" title="由 HEDGE_PO_MODE 锁定,不可页内切换">
|
||||
<button type="button" class="btn-secondary hp-uly-btn active" data-uly="ETH" disabled>ETH</button>
|
||||
<button type="button" class="btn-secondary hp-uly-btn" data-uly="BTC" disabled>BTC</button>
|
||||
</span>
|
||||
<select id="hp-exp-select"><option value="">选择到期日</option></select>
|
||||
<span class="hp-oo-seg hp-po-dir-seg" role="group" aria-label="期权类型">
|
||||
<button type="button" class="btn-secondary hp-po-dir is-selected" data-dir="long" title="买 Call → 永续空"><span class="hp-oo-check" aria-hidden="true">✓</span>看涨 Call</button>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""审计永期实例模式配置与纯函数口径(不连交易所下单).
|
||||
|
||||
用法:
|
||||
python scripts/audit_hedge_po_mode.py
|
||||
HEDGE_PO_MODE=btc_eth python scripts/audit_hedge_po_mode.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 仓库根
|
||||
_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if _ROOT not in sys.path:
|
||||
sys.path.insert(0, _ROOT)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||
apply_mode_to_start_body,
|
||||
clamp_hedge_po_mode_on_startup,
|
||||
combo_net_usdt,
|
||||
public_mode_payload,
|
||||
size_cross_notional,
|
||||
validate_body_matches_mode,
|
||||
)
|
||||
|
||||
print("=== HEDGE_PO_MODE 审计 ===")
|
||||
summary = clamp_hedge_po_mode_on_startup()
|
||||
payload = public_mode_payload(summary["mode"])
|
||||
ok = True
|
||||
|
||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
||||
nonlocal ok
|
||||
mark = "PASS" if cond else "FAIL"
|
||||
if not cond:
|
||||
ok = False
|
||||
print(f"[{mark}] {name}" + (f" — {detail}" if detail else ""))
|
||||
|
||||
check("启动钳制", summary.get("ok") is True, str(summary.get("mode")))
|
||||
check("期权/永续标的已填", bool(payload.get("option_underlying") and payload.get("perp_underlying")))
|
||||
check("出场强制 profit", payload.get("exit_mode") == "profit")
|
||||
check("以期权为主", payload.get("option_primary") is True)
|
||||
check("实例规则文案", "子账户" in str(payload.get("instance_rule") or ""))
|
||||
|
||||
if payload.get("po_mode") == "btc_eth":
|
||||
check("跨币期权=BTC", payload.get("option_underlying") == "BTC")
|
||||
check("跨币永续=ETH", payload.get("perp_underlying") == "ETH")
|
||||
k = float(payload.get("cross_notional_k") or 0)
|
||||
check("K 在合理区间", 0.05 <= k <= 5.0, f"k={k}")
|
||||
sz = size_cross_notional(
|
||||
option_coin_qty=0.1,
|
||||
option_index=100_000,
|
||||
perp_index=2_500,
|
||||
contract_size=0.01,
|
||||
notional_k=k,
|
||||
)
|
||||
check("跨币定仓样例", bool(sz.get("ok")), str(sz.get("contracts")))
|
||||
expect = 0.1 * 100_000 * k / 2_500 / 0.01
|
||||
check("定仓≈名义公式", abs(float(sz["contracts"]) - expect) < 1e-6, f"{sz['contracts']} vs {expect}")
|
||||
|
||||
body = apply_mode_to_start_body(
|
||||
{
|
||||
"direction": "long",
|
||||
"opt_inst_id": f"{payload['option_underlying']}-USD-260831-1000-C",
|
||||
"exchange_symbol": f"{payload['perp_underlying']}/USDT:USDT",
|
||||
"profit_target_usdt": 10,
|
||||
"sheets": 1,
|
||||
"contracts": 1,
|
||||
"option_perp_ratio": payload.get("option_perp_ratio"),
|
||||
}
|
||||
)
|
||||
check("body 钳制后校验通过", validate_body_matches_mode(body) is None)
|
||||
|
||||
# 组合 U:币*指数 + 永续U
|
||||
fake_net = {"opt_net": 0.01, "perp_net": 5.0, "net": 5.01}
|
||||
u = combo_net_usdt(fake_net, option_index=100_000)
|
||||
check("组合U不混单位", u == 1005.0, f"got {u}")
|
||||
|
||||
if summary.get("warnings"):
|
||||
print("--- warnings ---")
|
||||
for w in summary["warnings"]:
|
||||
print(" ", w)
|
||||
|
||||
print("===", "全部通过" if ok else "存在失败", "===")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,5 +1,7 @@
|
||||
"""永期「以期权为主」定仓/方向/目标位/校验."""
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
||||
PREMIUM_EXEC_FACTOR,
|
||||
@@ -114,6 +116,7 @@ class TestOptionPrimary(unittest.TestCase):
|
||||
body = {
|
||||
"option_primary": True,
|
||||
"watch_entry": 0,
|
||||
"market_order": 1,
|
||||
"direction": "long",
|
||||
"contracts": 1,
|
||||
"opt_inst_id": "ETH-USD-260831-1900-C",
|
||||
@@ -121,6 +124,8 @@ class TestOptionPrimary(unittest.TestCase):
|
||||
"sheets": 2,
|
||||
"exchange_symbol": "ETH/USDT:USDT",
|
||||
"premium_budget": 100,
|
||||
"exit_mode": "profit",
|
||||
"profit_target_usdt": 20,
|
||||
"option_target_points": 50,
|
||||
"perp_target_points": 30,
|
||||
"option_perp_ratio": 2,
|
||||
@@ -132,11 +137,16 @@ class TestOptionPrimary(unittest.TestCase):
|
||||
"min_option_hours": 36,
|
||||
"hours_to_expiry": 40,
|
||||
"option_leverage": 100,
|
||||
"underlying": "ETH",
|
||||
}
|
||||
self.assertIsNone(validate_option_primary_start(body))
|
||||
self.assertIsNone(validate_start_body("perp_options", body))
|
||||
bad = dict(body, opt_type="P")
|
||||
self.assertIsNotNone(validate_start_body("perp_options", bad))
|
||||
with patch.dict(os.environ, {"HEDGE_PO_MODE": "eth"}, clear=False):
|
||||
from lib.hedge_plan.hedge_po_mode_lib import clamp_hedge_po_mode_on_startup
|
||||
|
||||
clamp_hedge_po_mode_on_startup()
|
||||
self.assertIsNone(validate_start_body("perp_options", body))
|
||||
bad = dict(body, opt_type="P")
|
||||
self.assertIsNotNone(validate_start_body("perp_options", bad))
|
||||
|
||||
def test_pick_candidate_respects_leverage(self):
|
||||
chain = {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
"""永期实例模式 HEDGE_PO_MODE:钳制/定仓/校验/组合U口径."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
||||
contracts_from_option_sheets,
|
||||
estimate_combo_net_pnl,
|
||||
size_from_premium,
|
||||
)
|
||||
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||
MODE_BTC,
|
||||
MODE_BTC_ETH,
|
||||
MODE_ETH,
|
||||
apply_mode_to_start_body,
|
||||
clamp_hedge_po_mode_on_startup,
|
||||
combo_net_usdt,
|
||||
default_option_perp_ratio,
|
||||
get_hedge_po_mode,
|
||||
normalize_hedge_po_mode,
|
||||
option_underlying,
|
||||
perp_underlying,
|
||||
public_mode_payload,
|
||||
size_cross_notional,
|
||||
validate_body_matches_mode,
|
||||
)
|
||||
|
||||
|
||||
class TestHedgePoMode(unittest.TestCase):
|
||||
def test_normalize_aliases(self):
|
||||
self.assertEqual(normalize_hedge_po_mode("ETH"), MODE_ETH)
|
||||
self.assertEqual(normalize_hedge_po_mode("btc-eth"), MODE_BTC_ETH)
|
||||
self.assertEqual(normalize_hedge_po_mode("跨币"), MODE_BTC_ETH)
|
||||
self.assertEqual(normalize_hedge_po_mode("nope"), "")
|
||||
|
||||
def test_underlyings(self):
|
||||
self.assertEqual(option_underlying(MODE_ETH), "ETH")
|
||||
self.assertEqual(perp_underlying(MODE_ETH), "ETH")
|
||||
self.assertEqual(option_underlying(MODE_BTC), "BTC")
|
||||
self.assertEqual(perp_underlying(MODE_BTC), "BTC")
|
||||
self.assertEqual(option_underlying(MODE_BTC_ETH), "BTC")
|
||||
self.assertEqual(perp_underlying(MODE_BTC_ETH), "ETH")
|
||||
|
||||
def test_cross_ratio_from_k(self):
|
||||
with patch.dict(os.environ, {"HEDGE_CROSS_NOTIONAL_K": "0.5"}, clear=False):
|
||||
self.assertAlmostEqual(default_option_perp_ratio(MODE_BTC_ETH), 2.0)
|
||||
|
||||
def test_clamp_startup_and_sync_underly(self):
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"HEDGE_PO_MODE": "btc_eth", "OKX_OPTIONS_DEFAULT_UNDERLY": "ETH"},
|
||||
clear=False,
|
||||
):
|
||||
summary = clamp_hedge_po_mode_on_startup()
|
||||
self.assertEqual(summary["mode"], MODE_BTC_ETH)
|
||||
self.assertEqual(os.environ["HEDGE_PO_MODE"], MODE_BTC_ETH)
|
||||
self.assertEqual(os.environ["OKX_OPTIONS_DEFAULT_UNDERLY"], "BTC")
|
||||
self.assertEqual(get_hedge_po_mode(), MODE_BTC_ETH)
|
||||
|
||||
def test_clamp_invalid_falls_back(self):
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"HEDGE_PO_MODE": "weird", "OKX_OPTIONS_DEFAULT_UNDERLY": "BTC"},
|
||||
clear=False,
|
||||
):
|
||||
summary = clamp_hedge_po_mode_on_startup()
|
||||
self.assertEqual(summary["mode"], MODE_BTC)
|
||||
self.assertTrue(summary["warnings"])
|
||||
|
||||
def test_apply_and_validate_body(self):
|
||||
with patch.dict(os.environ, {"HEDGE_PO_MODE": "btc_eth", "HEDGE_CROSS_NOTIONAL_K": "0.5"}):
|
||||
clamp_hedge_po_mode_on_startup()
|
||||
body = apply_mode_to_start_body(
|
||||
{
|
||||
"direction": "long",
|
||||
"underlying": "ETH",
|
||||
"opt_inst_id": "BTC-USD-260831-90000-C",
|
||||
"exchange_symbol": "ETH/USDT:USDT",
|
||||
"profit_target_usdt": 20,
|
||||
}
|
||||
)
|
||||
self.assertEqual(body["underlying"], "BTC")
|
||||
self.assertEqual(body["perp_underlying"], "ETH")
|
||||
self.assertEqual(body["exit_mode"], "profit")
|
||||
self.assertTrue(body["option_primary"])
|
||||
self.assertAlmostEqual(float(body["option_perp_ratio"]), 2.0)
|
||||
self.assertIsNone(validate_body_matches_mode(body))
|
||||
bad = dict(body, opt_inst_id="ETH-USD-260831-2000-C")
|
||||
self.assertIn("BTC", validate_body_matches_mode(bad) or "")
|
||||
|
||||
def test_size_cross_notional_half(self):
|
||||
# 0.1 BTC @ 100k → 10k U 期权名义; k=0.5 → 5k ETH 名义 @ 2500 → 2 ETH → 200 张(cs=0.01)
|
||||
sz = size_cross_notional(
|
||||
option_coin_qty=0.1,
|
||||
option_index=100_000,
|
||||
perp_index=2500,
|
||||
contract_size=0.01,
|
||||
notional_k=0.5,
|
||||
)
|
||||
self.assertTrue(sz["ok"])
|
||||
self.assertAlmostEqual(sz["option_notional_usdt"], 10_000.0)
|
||||
self.assertAlmostEqual(sz["perp_notional_usdt"], 5_000.0)
|
||||
self.assertAlmostEqual(sz["perp_coin"], 2.0)
|
||||
self.assertAlmostEqual(sz["contracts"], 200.0)
|
||||
|
||||
def test_size_from_premium_cross_matches_notional(self):
|
||||
# ask=0.01 BTC → 预算 1 BTC 权利金(币); usable 0.95 → coin=0.95 → sheets
|
||||
# 用整张回写后再跨币
|
||||
sized = size_from_premium(
|
||||
premium_budget=1000, # 若 ask 用币报价则预算单位需一致;此处用大预算保证 ≥1 张
|
||||
ask=0.01,
|
||||
ct_mult=0.01,
|
||||
ratio=2,
|
||||
contract_size=0.01,
|
||||
option_index=100_000,
|
||||
perp_index=2_500,
|
||||
)
|
||||
self.assertTrue(sized["ok"], sized.get("msg"))
|
||||
self.assertTrue(sized.get("cross_asset") or sized.get("option_notional_usdt"))
|
||||
# coin_qty = sheets * 0.01; 名义路径
|
||||
self.assertGreater(float(sized["contracts"]), 0)
|
||||
|
||||
def test_contracts_from_sheets_same_vs_cross(self):
|
||||
same = contracts_from_option_sheets(
|
||||
sheets=10, ct_mult=0.01, ratio=2, contract_size=0.01
|
||||
)
|
||||
self.assertTrue(same["ok"])
|
||||
# 0.1 币 / 2 / 0.01 = 5 张
|
||||
self.assertAlmostEqual(same["contracts"], 5.0)
|
||||
|
||||
cross = contracts_from_option_sheets(
|
||||
sheets=10,
|
||||
ct_mult=0.01,
|
||||
ratio=2,
|
||||
contract_size=0.01,
|
||||
option_index=100_000,
|
||||
perp_index=2_500,
|
||||
)
|
||||
self.assertTrue(cross["ok"])
|
||||
# 0.1 BTC * 100k * 0.5 / 2500 / 0.01 = 200
|
||||
self.assertAlmostEqual(cross["contracts"], 200.0)
|
||||
|
||||
def test_combo_net_usdt_not_mix_units(self):
|
||||
net = estimate_combo_net_pnl(
|
||||
view_side="long",
|
||||
strike=90000,
|
||||
index_px=91000,
|
||||
ask_open=0.01,
|
||||
bid=0.012,
|
||||
sheets=1,
|
||||
ct_mult=0.01,
|
||||
perp_direction="short",
|
||||
perp_entry=2500,
|
||||
perp_mark=2450,
|
||||
contracts=10,
|
||||
contract_size=0.01,
|
||||
fee=0.0,
|
||||
option_fee=0.0,
|
||||
)
|
||||
# opt_net 为币;perp_net 为 U;禁止直接用 net['net'] 出门控
|
||||
u = combo_net_usdt(net, option_index=91000)
|
||||
self.assertIsNotNone(u)
|
||||
self.assertAlmostEqual(u, float(net["opt_net"]) * 91000 + float(net["perp_net"]), places=4)
|
||||
|
||||
def test_public_payload(self):
|
||||
with patch.dict(os.environ, {"HEDGE_PO_MODE": "eth"}):
|
||||
clamp_hedge_po_mode_on_startup()
|
||||
p = public_mode_payload()
|
||||
self.assertEqual(p["po_mode"], MODE_ETH)
|
||||
self.assertEqual(p["exit_mode"], "profit")
|
||||
self.assertIn("子账户", p["instance_rule"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user