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
|
HEDGE_PLAN_LIVE_ORDER=false
|
||||||
# 永期子模式:true=以期权为主;false=保险模式(页面标题前标识,不可页内切换)
|
# 永期子模式:true=以期权为主;false=保险模式(页面标题前标识,不可页内切换)
|
||||||
HEDGE_PLAN_OPTION_PRIMARY=true
|
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_OPEN_ORDER=options_first
|
||||||
HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true
|
HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true
|
||||||
HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false
|
HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false
|
||||||
|
|||||||
@@ -61,10 +61,13 @@ python app.py
|
|||||||
- `SIM_DEFAULT_MODE=sim`(默认模拟盘)
|
- `SIM_DEFAULT_MODE=sim`(默认模拟盘)
|
||||||
- `OKX_OPTIONS_MARGIN_MODE=coin`(固定)
|
- `OKX_OPTIONS_MARGIN_MODE=coin`(固定)
|
||||||
- `OKX_TRADE_MODE=perp_options|options_options`(本仓仅对冲)
|
- `OKX_TRADE_MODE=perp_options|options_options`(本仓仅对冲)
|
||||||
|
- `HEDGE_PO_MODE=eth|btc|btc_eth`(一实例一模式一子账户;跨币见 `HEDGE_CROSS_NOTIONAL_K`)
|
||||||
- `STANDALONE_OPTIONS_UI=false`(默认;勿开,除非临时救急)
|
- `STANDALONE_OPTIONS_UI=false`(默认;勿开,除非临时救急)
|
||||||
- `SIM_INITIAL_EQUITY_USDT=10000`
|
- `SIM_INITIAL_EQUITY_USDT=10000`
|
||||||
- `APP_PORT=5010`
|
- `APP_PORT=5010`
|
||||||
|
|
||||||
|
永期三模式说明与审计清单见 [docs/永期对冲三模式.md](docs/永期对冲三模式.md)。
|
||||||
|
|
||||||
## 风险
|
## 风险
|
||||||
|
|
||||||
实盘风险自负。默认模拟盘;切 `live` 前请核对 API 权限与资金。
|
实盘风险自负。默认模拟盘;切 `live` 前请核对 API 权限与资金。
|
||||||
|
|||||||
@@ -6878,6 +6878,9 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
_standalone_options_ui = standalone_options_ui_enabled()
|
_standalone_options_ui = standalone_options_ui_enabled()
|
||||||
_hedge_mode_on = _okx_trade_mode in ("perp_options", "options_options")
|
_hedge_mode_on = _okx_trade_mode in ("perp_options", "options_options")
|
||||||
_show_perp_funds = show_perp_funds_enabled(exchange_key="okx")
|
_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(
|
template_ctx = dict(
|
||||||
page=page,
|
page=page,
|
||||||
key=key_list,
|
key=key_list,
|
||||||
@@ -7000,7 +7003,16 @@ def render_main_page(page="trade", embed_mode=None):
|
|||||||
options_compound_full_cap_usdc=float(
|
options_compound_full_cap_usdc=float(
|
||||||
os.getenv("OKX_OPTIONS_COMPOUND_FULL_CAP_USDC") or "300"
|
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(
|
options_chain_ask_liq_filter=os.getenv(
|
||||||
"OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED", "true"
|
"OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED", "true"
|
||||||
).lower()
|
).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)
|
||||||
+104
-33
@@ -18,11 +18,25 @@
|
|||||||
return "perp_options";
|
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 = {
|
const state = {
|
||||||
tab: pickDefaultTab(),
|
tab: pickDefaultTab(),
|
||||||
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
|
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
|
||||||
posTab: "live",
|
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",
|
moneyFilter: root.getAttribute("data-option-primary") !== "0" ? "all" : "itm",
|
||||||
ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值
|
ooMoneyFilter: "atm_otm", // 期期锁定:平值+虚值
|
||||||
ooRecommend: null, // atm_straddle | double_otm | null
|
ooRecommend: null, // atm_straddle | double_otm | null
|
||||||
@@ -41,9 +55,9 @@
|
|||||||
ooCloseMode: "close_all",
|
ooCloseMode: "close_all",
|
||||||
direction: "long",
|
direction: "long",
|
||||||
optionPrimary: root.getAttribute("data-option-primary") !== "0",
|
optionPrimary: root.getAttribute("data-option-primary") !== "0",
|
||||||
exitMode: "profit", // profit=盈利金额出场 | target=目标位出场
|
exitMode: "profit", // 实例模式强制组合盈利金额(U)
|
||||||
opLevTouched: false,
|
opLevTouched: false,
|
||||||
opRatioTouched: false,
|
opRatioTouched: crossAsset, // 跨币锁比例,视为已设定
|
||||||
perpLevTouched: false,
|
perpLevTouched: false,
|
||||||
tradingUsdc: null,
|
tradingUsdc: null,
|
||||||
fundingUsdc: null,
|
fundingUsdc: null,
|
||||||
@@ -58,6 +72,32 @@
|
|||||||
previewPlanType: null,
|
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() {
|
function getDirection() {
|
||||||
return (state.direction || "long").toLowerCase() === "short" ? "short" : "long";
|
return (state.direction || "long").toLowerCase() === "short" ? "short" : "long";
|
||||||
}
|
}
|
||||||
@@ -421,7 +461,7 @@
|
|||||||
|
|
||||||
function computeOpSizing(ask, ctMult, sheetsOverride) {
|
function computeOpSizing(ask, ctMult, sheetsOverride) {
|
||||||
const budget = numInput("hp-premium-budget", 0);
|
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 cs = Number((state.market && state.market.contract_size) || 0.01);
|
||||||
const usable = budget * 0.95;
|
const usable = budget * 0.95;
|
||||||
const a = Number(ask || 0);
|
const a = Number(ask || 0);
|
||||||
@@ -443,16 +483,32 @@
|
|||||||
if (!(sheets > 0)) return null;
|
if (!(sheets > 0)) return null;
|
||||||
eth = Math.round(sheets * ct * 100) / 100;
|
eth = Math.round(sheets * ct * 100) / 100;
|
||||||
}
|
}
|
||||||
const perpEth = eth / ratio;
|
// 跨币:期权币×期权指数×(1/ratio) / 永续指数 = 永续币
|
||||||
const contracts = perpEth / cs;
|
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 {
|
return {
|
||||||
usable: usable,
|
usable: usable,
|
||||||
eth_qty: eth,
|
eth_qty: eth,
|
||||||
perp_qty: perpEth,
|
perp_qty: perpQty,
|
||||||
sheets: sheets,
|
sheets: sheets,
|
||||||
contracts: contracts,
|
contracts: contracts,
|
||||||
premium_est: a > 0 ? a * sheets * ct : 0,
|
premium_est: a > 0 ? a * sheets * ct : 0,
|
||||||
ratio: ratio,
|
ratio: ratio,
|
||||||
|
option_notional_usdt: optNotional,
|
||||||
|
perp_notional_usdt: perpNotional,
|
||||||
|
cross_asset: !!state.crossAsset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,24 +539,32 @@
|
|||||||
|
|
||||||
function formatOpSizingHtml(sized, amtPrec) {
|
function formatOpSizingHtml(sized, amtPrec) {
|
||||||
if (!sized) return "";
|
if (!sized) return "";
|
||||||
const coin = (state.underlying || "ETH").toUpperCase();
|
const optCoin = (state.optionUnderly || state.underlying || "ETH").toUpperCase();
|
||||||
const prec = amtPrec != null ? amtPrec : coin === "BTC" ? 4 : 2;
|
const perpCoin = (state.perpUnderly || state.underlying || "ETH").toUpperCase();
|
||||||
return (
|
const prec = amtPrec != null ? amtPrec : perpCoin === "BTC" ? 4 : 2;
|
||||||
|
let html =
|
||||||
"期权 <strong>" +
|
"期权 <strong>" +
|
||||||
fmt(sized.eth_qty, 2) +
|
fmt(sized.eth_qty, 2) +
|
||||||
"</strong> " +
|
"</strong> " +
|
||||||
coin +
|
optCoin +
|
||||||
" / " +
|
" / " +
|
||||||
sized.sheets +
|
sized.sheets +
|
||||||
" 张 → 永续 <strong>" +
|
" 张 → 永续 <strong>" +
|
||||||
fmt(sized.perp_qty, 4) +
|
fmt(sized.perp_qty, 4) +
|
||||||
"</strong> " +
|
"</strong> " +
|
||||||
coin +
|
perpCoin +
|
||||||
" (" +
|
" (" +
|
||||||
fmt(sized.contracts, prec) +
|
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) {
|
function defaultPerpLeverage(uly) {
|
||||||
@@ -1881,9 +1945,12 @@
|
|||||||
|
|
||||||
async function loadMarket() {
|
async function loadMarket() {
|
||||||
const dir = getDirection();
|
const dir = getDirection();
|
||||||
|
// 永续行情按实例模式永续标的(跨币=ETH);期权链仍用 optionUnderly
|
||||||
|
const mktBase =
|
||||||
|
state.mode === "perp_options" ? state.perpUnderly || state.underlying : state.underlying;
|
||||||
const d = await apiJson(
|
const d = await apiJson(
|
||||||
"/api/hedge-plan/market?base=" +
|
"/api/hedge-plan/market?base=" +
|
||||||
encodeURIComponent(state.underlying) +
|
encodeURIComponent(mktBase) +
|
||||||
"&direction=" +
|
"&direction=" +
|
||||||
encodeURIComponent(dir) +
|
encodeURIComponent(dir) +
|
||||||
"&option_primary=" +
|
"&option_primary=" +
|
||||||
@@ -2523,7 +2590,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setUnderlying(uly, forceReload) {
|
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;
|
const changed = next !== state.underlying;
|
||||||
state.underlying = next;
|
state.underlying = next;
|
||||||
syncUnderlyingUI();
|
syncUnderlyingUI();
|
||||||
@@ -2813,6 +2885,9 @@
|
|||||||
});
|
});
|
||||||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
||||||
b.addEventListener("click", function () {
|
b.addEventListener("click", function () {
|
||||||
|
if (b.classList.contains("hp-uly-btn") && state.mode === "perp_options") {
|
||||||
|
return; // 永期标的由实例模式锁定
|
||||||
|
}
|
||||||
setUnderlying(b.getAttribute("data-uly") || "ETH", true);
|
setUnderlying(b.getAttribute("data-uly") || "ETH", true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3011,7 +3086,8 @@
|
|||||||
}
|
}
|
||||||
document.querySelectorAll(".hp-po-exit-mode").forEach(function (b) {
|
document.querySelectorAll(".hp-po-exit-mode").forEach(function (b) {
|
||||||
b.addEventListener("click", function () {
|
b.addEventListener("click", function () {
|
||||||
state.exitMode = b.getAttribute("data-exit") === "target" ? "target" : "profit";
|
// 实例模式强制组合盈利金额(U)出场
|
||||||
|
state.exitMode = "profit";
|
||||||
syncExitModeUI();
|
syncExitModeUI();
|
||||||
updatePerpPnlHint();
|
updatePerpPnlHint();
|
||||||
});
|
});
|
||||||
@@ -3763,23 +3839,14 @@
|
|||||||
setPoOrderErr("请选择期权并填写张数(≥1)", "hp-order-sheets");
|
setPoOrderErr("请选择期权并填写张数(≥1)", "hp-order-sheets");
|
||||||
throw new Error("请选择期权并填写张数(≥1)");
|
throw new Error("请选择期权并填写张数(≥1)");
|
||||||
}
|
}
|
||||||
const exitMode = state.exitMode === "target" ? "target" : "profit";
|
const exitMode = "profit";
|
||||||
let optPts = 0;
|
let optPts = 0;
|
||||||
let perpPts = 0;
|
let perpPts = 0;
|
||||||
let profitTarget = 0;
|
let profitTarget = 0;
|
||||||
if (exitMode === "profit") {
|
|
||||||
profitTarget = numInput("hp-profit-target", NaN);
|
profitTarget = numInput("hp-profit-target", NaN);
|
||||||
if (!(profitTarget > 0)) {
|
if (!(profitTarget > 0)) {
|
||||||
setPoOrderErr("请填写目标盈利金额(USDT,扣双边手续费后净利)", "hp-profit-target");
|
setPoOrderErr("请填写目标盈利金额(USDT,组合净利折U)", "hp-profit-target");
|
||||||
throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)");
|
throw new Error("请填写目标盈利金额(USDT,组合净利折U)");
|
||||||
}
|
|
||||||
} 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)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!(state.market && state.market.exchange_symbol)) {
|
if (!(state.market && state.market.exchange_symbol)) {
|
||||||
setPoOrderErr("永续行情未就绪,请先点「刷新行情」");
|
setPoOrderErr("永续行情未就绪,请先点「刷新行情」");
|
||||||
@@ -3805,9 +3872,12 @@
|
|||||||
market_order: 1,
|
market_order: 1,
|
||||||
watch_entry: 0,
|
watch_entry: 0,
|
||||||
exit_mode: exitMode,
|
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(),
|
direction: getDirection(),
|
||||||
entry: entry,
|
entry: Number((state.market && state.market.mark) || entry),
|
||||||
contracts: sized.contracts,
|
contracts: sized.contracts,
|
||||||
sheets: sized.sheets,
|
sheets: sized.sheets,
|
||||||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||||||
@@ -3818,10 +3888,10 @@
|
|||||||
ask: state.selected.ask,
|
ask: state.selected.ask,
|
||||||
index_px: entry,
|
index_px: entry,
|
||||||
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
|
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
|
||||||
leverage: defaultPerpLeverage(state.underlying),
|
leverage: defaultPerpLeverage(state.perpUnderly || state.underlying),
|
||||||
option_leverage: 0,
|
option_leverage: 0,
|
||||||
premium_budget: Math.max(premEst / 0.95, premEst),
|
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,
|
option_target_points: optPts || 0,
|
||||||
perp_target_points: perpPts || 0,
|
perp_target_points: perpPts || 0,
|
||||||
profit_target_usdt: profitTarget || 0,
|
profit_target_usdt: profitTarget || 0,
|
||||||
@@ -3929,6 +3999,7 @@
|
|||||||
if (state.tab === "perp_options") void refreshPoStrategyStatus();
|
if (state.tab === "perp_options") void refreshPoStrategyStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lockPoUnderlyingUI();
|
||||||
syncTabUI();
|
syncTabUI();
|
||||||
syncUnderlyingUI();
|
syncUnderlyingUI();
|
||||||
syncMoneyUI();
|
syncMoneyUI();
|
||||||
|
|||||||
@@ -3625,6 +3625,22 @@ html[data-theme="light"] .opt-be-dist-down {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin: 0 0 12px;
|
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 {
|
.hedge-plan-page-wrap .hp-po-gate-inline {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
|
|||||||
Vendored
+7
@@ -109,6 +109,8 @@ HOT_RELOAD_EXACT = frozenset({
|
|||||||
"MAX_ACTIVE_HEDGE_PLANS",
|
"MAX_ACTIVE_HEDGE_PLANS",
|
||||||
"HEDGE_PLAN_LIVE_ORDER",
|
"HEDGE_PLAN_LIVE_ORDER",
|
||||||
"HEDGE_PLAN_OPTION_PRIMARY",
|
"HEDGE_PLAN_OPTION_PRIMARY",
|
||||||
|
"HEDGE_PO_MODE",
|
||||||
|
"HEDGE_CROSS_NOTIONAL_K",
|
||||||
"HEDGE_PLAN_OPEN_ORDER",
|
"HEDGE_PLAN_OPEN_ORDER",
|
||||||
"HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
|
"HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
|
||||||
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
|
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
|
||||||
@@ -177,6 +179,11 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {
|
|||||||
("true", "以期权为主"),
|
("true", "以期权为主"),
|
||||||
("false", "保险模式"),
|
("false", "保险模式"),
|
||||||
),
|
),
|
||||||
|
"HEDGE_PO_MODE": (
|
||||||
|
("eth", "ETH永期(期权+永续同ETH)"),
|
||||||
|
("btc", "BTC永期(期权+永续同BTC)"),
|
||||||
|
("btc_eth", "跨币(BTC期权+ETH永续)"),
|
||||||
|
),
|
||||||
"SIM_DEFAULT_MODE": (
|
"SIM_DEFAULT_MODE": (
|
||||||
("sim", "模拟盘"),
|
("sim", "模拟盘"),
|
||||||
("live", "实盘"),
|
("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_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",
|
"HEDGE_PLAN_OPTION_PRIMARY",
|
||||||
"永期模式(以期权为主/保险)",
|
"永期模式(以期权为主/保险)",
|
||||||
"默认 true=以期权为主;false=保险模式;页面标题前显示标识,不可在页内切换",
|
"实例模式强制以期权为主;一般保持 true",
|
||||||
),
|
),
|
||||||
("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
|
("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
|
||||||
("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
|
("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_OO_BIAS_RATIO": "0.7",
|
||||||
"HEDGE_PLAN_BUDGET_BUFFER": "0.95",
|
"HEDGE_PLAN_BUDGET_BUFFER": "0.95",
|
||||||
"HEDGE_PLAN_OPTION_PRIMARY": "true",
|
"HEDGE_PLAN_OPTION_PRIMARY": "true",
|
||||||
|
"HEDGE_PO_MODE": "eth",
|
||||||
|
"HEDGE_CROSS_NOTIONAL_K": "0.5",
|
||||||
"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",
|
||||||
"OKX_OPTIONS_CLOSE_GATE_MODE": "premium",
|
"OKX_OPTIONS_CLOSE_GATE_MODE": "premium",
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ def init_hedge_plan_tables(conn: sqlite3.Connection) -> None:
|
|||||||
# 出场:profit=盈利金额(扣双边费);target=目标位点数
|
# 出场:profit=盈利金额(扣双边费);target=目标位点数
|
||||||
_ensure_column(conn, "hedge_plans", "exit_mode", "TEXT")
|
_ensure_column(conn, "hedge_plans", "exit_mode", "TEXT")
|
||||||
_ensure_column(conn, "hedge_plans", "profit_target_usdt", "REAL")
|
_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")
|
_ensure_column(conn, "hedge_plan_legs", "ct_mult", "REAL")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -695,6 +695,7 @@ def _tick_po_option_primary(
|
|||||||
target_hit,
|
target_hit,
|
||||||
)
|
)
|
||||||
from lib.hedge_plan.hedge_plan_orders_lib import _close_perp
|
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)
|
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)
|
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)
|
profit_target = _plan_profit_target(plan)
|
||||||
if strike is None or strike <= 0:
|
if strike is None or strike <= 0:
|
||||||
return None
|
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:
|
if idx is None:
|
||||||
return 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"))
|
mark = _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last"))
|
||||||
except Exception:
|
except Exception:
|
||||||
mark = None
|
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
|
mark = mark or idx
|
||||||
entry = _sf(plan.get("entry_mark")) or _sf(perp.get("avg_open")) or mark
|
entry = _sf(plan.get("entry_mark")) or _sf(perp.get("avg_open")) or mark
|
||||||
cs = float(cfg.get("default_contract_size") or 0.01)
|
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
|
perp_px = _sf(book.get("vwap")) or mark or entry
|
||||||
|
|
||||||
# 优先期权腿平仓(须买一流动性);盈利金额模式要求扣双边费后净利≥目标
|
# 优先期权腿平仓(须买一流动性);盈利金额模式要求扣双边费后组合净利(U)≥目标
|
||||||
if hit_opt:
|
if hit_opt:
|
||||||
liq_ok, liq_msg = option_bid_liquidity_ok(bid, bid_sz, need_sheets=sheets)
|
liq_ok, liq_msg = option_bid_liquidity_ok(bid, bid_sz, need_sheets=sheets)
|
||||||
net = None
|
net = None
|
||||||
|
net_usdt = None
|
||||||
if liq_ok:
|
if liq_ok:
|
||||||
net = estimate_combo_net_pnl(
|
net = estimate_combo_net_pnl(
|
||||||
view_side=view,
|
view_side=view,
|
||||||
@@ -806,17 +817,19 @@ def _tick_po_option_primary(
|
|||||||
contracts=contracts,
|
contracts=contracts,
|
||||||
contract_size=cs,
|
contract_size=cs,
|
||||||
)
|
)
|
||||||
|
net_usdt = combo_net_usdt(net, option_index=float(idx))
|
||||||
if exit_mode == "profit":
|
if exit_mode == "profit":
|
||||||
can_opt_exit = bool(
|
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:
|
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:
|
if can_opt_exit:
|
||||||
reason = "profit_target_usdt" if exit_mode == "profit" else "opt_target_points"
|
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)
|
close_r = _sell_option(cfg, inst_id=str(opt.get("inst_id") or ""), sheets=sheets)
|
||||||
if close_r.get("already_flat"):
|
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):
|
elif not close_r.get("ok") or not close_r.get("fully_closed", True):
|
||||||
notify_hedge(
|
notify_hedge(
|
||||||
cfg,
|
cfg,
|
||||||
@@ -829,10 +842,12 @@ def _tick_po_option_primary(
|
|||||||
update_plan(conn, int(plan["id"]), close_reason="opt_target_pending")
|
update_plan(conn, int(plan["id"]), close_reason="opt_target_pending")
|
||||||
return {"plan_id": plan["id"], "retry": True, "close": close_r}
|
return {"plan_id": plan["id"], "retry": True, "close": close_r}
|
||||||
else:
|
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(
|
conn.execute(
|
||||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
"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(
|
perp_close = _close_perp(
|
||||||
cfg, symbol=symbol, direction=perp_dir, contracts=contracts, dry_run=False
|
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=?",
|
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||||
("closed", reason, _now(), round(perp_pnl, 4), perp["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(
|
update_plan(
|
||||||
conn,
|
conn,
|
||||||
int(plan["id"]),
|
int(plan["id"]),
|
||||||
status="closed",
|
status="closed",
|
||||||
close_reason=reason,
|
close_reason=reason,
|
||||||
realized_pnl_perp=round(perp_pnl, 4),
|
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),
|
realized_pnl_total=round(total, 4),
|
||||||
stats_bucket="opt_primary",
|
stats_bucket="opt_primary",
|
||||||
closed_at=_now(),
|
closed_at=_now(),
|
||||||
@@ -872,6 +887,7 @@ def _tick_po_option_primary(
|
|||||||
"close_reason": reason,
|
"close_reason": reason,
|
||||||
"total": total,
|
"total": total,
|
||||||
"net": net,
|
"net": net,
|
||||||
|
"net_usdt": net_usdt,
|
||||||
"sell_coin": sell_r,
|
"sell_coin": sell_r,
|
||||||
}
|
}
|
||||||
if exit_mode == "profit":
|
if exit_mode == "profit":
|
||||||
@@ -881,16 +897,18 @@ def _tick_po_option_primary(
|
|||||||
"msg": (
|
"msg": (
|
||||||
liq_msg
|
liq_msg
|
||||||
if not liq_ok
|
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": net,
|
||||||
|
"net_usdt": net_usdt,
|
||||||
}
|
}
|
||||||
if not hit_perp:
|
if not hit_perp:
|
||||||
return {
|
return {
|
||||||
"plan_id": plan["id"],
|
"plan_id": plan["id"],
|
||||||
"skip": True,
|
"skip": True,
|
||||||
"msg": (liq_msg if not liq_ok else "净利≤0,继续持有"),
|
"msg": (liq_msg if not liq_ok else "组合净利(U)≤0,继续持有"),
|
||||||
"net": net,
|
"net": net,
|
||||||
|
"net_usdt": net_usdt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not hit_perp:
|
if not hit_perp:
|
||||||
|
|||||||
@@ -141,8 +141,14 @@ def size_from_premium(
|
|||||||
ratio: float,
|
ratio: float,
|
||||||
contract_size: float,
|
contract_size: float,
|
||||||
exec_factor: float = PREMIUM_EXEC_FACTOR,
|
exec_factor: float = PREMIUM_EXEC_FACTOR,
|
||||||
|
option_index: Optional[float] = None,
|
||||||
|
perp_index: Optional[float] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""权利金×0.95 → ETH 两位小数 → 期权张 → 永续跟比例."""
|
"""权利金×0.95 → 币两位小数 → 期权张 → 永续跟比例.
|
||||||
|
|
||||||
|
同币:永续币 = 期权币 / ratio.
|
||||||
|
跨币(传入 option_index 与 perp_index):永续名义U = 期权名义U × (1/ratio).
|
||||||
|
"""
|
||||||
budget = float(premium_budget or 0)
|
budget = float(premium_budget or 0)
|
||||||
a = float(ask or 0)
|
a = float(ask or 0)
|
||||||
ct = float(ct_mult or 0.01)
|
ct = float(ct_mult or 0.01)
|
||||||
@@ -155,23 +161,25 @@ def size_from_premium(
|
|||||||
"msg": "定仓参数无效",
|
"msg": "定仓参数无效",
|
||||||
"usable_premium": round(usable, 4),
|
"usable_premium": round(usable, 4),
|
||||||
"eth_qty": 0.0,
|
"eth_qty": 0.0,
|
||||||
|
"coin_qty": 0.0,
|
||||||
"sheets": 0.0,
|
"sheets": 0.0,
|
||||||
"perp_eth": 0.0,
|
"perp_eth": 0.0,
|
||||||
"contracts": 0.0,
|
"contracts": 0.0,
|
||||||
}
|
}
|
||||||
# ask 为每 1 币权利金;ETH 数量 = usable / ask
|
# ask 为每 1 币权利金;币数量 = usable / ask
|
||||||
eth_qty = floor2(usable / a)
|
coin_qty = floor2(usable / a)
|
||||||
if eth_qty <= 0:
|
if coin_qty <= 0:
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"msg": "权利金不足以买入 0.01 ETH 名义期权",
|
"msg": "权利金不足以买入 0.01 币名义期权",
|
||||||
"usable_premium": round(usable, 4),
|
"usable_premium": round(usable, 4),
|
||||||
"eth_qty": 0.0,
|
"eth_qty": 0.0,
|
||||||
|
"coin_qty": 0.0,
|
||||||
"sheets": 0.0,
|
"sheets": 0.0,
|
||||||
"perp_eth": 0.0,
|
"perp_eth": 0.0,
|
||||||
"contracts": 0.0,
|
"contracts": 0.0,
|
||||||
}
|
}
|
||||||
sheets = eth_qty / ct
|
sheets = coin_qty / ct
|
||||||
# 张数向下取整到整数张(OKX 期权常见整张)
|
# 张数向下取整到整数张(OKX 期权常见整张)
|
||||||
sheets_i = float(math.floor(sheets + 1e-12))
|
sheets_i = float(math.floor(sheets + 1e-12))
|
||||||
if sheets_i <= 0:
|
if sheets_i <= 0:
|
||||||
@@ -179,27 +187,132 @@ def size_from_premium(
|
|||||||
"ok": False,
|
"ok": False,
|
||||||
"msg": "换算期权张数不足 1 张",
|
"msg": "换算期权张数不足 1 张",
|
||||||
"usable_premium": round(usable, 4),
|
"usable_premium": round(usable, 4),
|
||||||
"eth_qty": eth_qty,
|
"eth_qty": coin_qty,
|
||||||
|
"coin_qty": coin_qty,
|
||||||
"sheets": 0.0,
|
"sheets": 0.0,
|
||||||
"perp_eth": 0.0,
|
"perp_eth": 0.0,
|
||||||
"contracts": 0.0,
|
"contracts": 0.0,
|
||||||
}
|
}
|
||||||
# 用整张回写 ETH,保持与下单一致
|
# 用整张回写币量,保持与下单一致
|
||||||
eth_qty = round(sheets_i * ct, 2)
|
coin_qty = round(sheets_i * ct, 2)
|
||||||
perp_eth = eth_qty / r
|
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
|
contracts = perp_eth / cs
|
||||||
premium_est = a * sheets_i * ct
|
premium_est = a * sheets_i * ct
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"msg": "",
|
"msg": "",
|
||||||
"usable_premium": round(usable, 4),
|
"usable_premium": round(usable, 4),
|
||||||
"eth_qty": eth_qty,
|
"eth_qty": coin_qty,
|
||||||
|
"coin_qty": coin_qty,
|
||||||
"sheets": sheets_i,
|
"sheets": sheets_i,
|
||||||
"perp_eth": round(perp_eth, 6),
|
"perp_eth": round(perp_eth, 6),
|
||||||
"contracts": contracts,
|
"contracts": contracts,
|
||||||
"premium_est": round(premium_est, 4),
|
"premium_est": round(premium_est, 4),
|
||||||
"ratio": r,
|
"ratio": r,
|
||||||
"exec_factor": float(exec_factor or PREMIUM_EXEC_FACTOR),
|
"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:
|
if q.get("ct_mult") is not None:
|
||||||
body["ct_mult"] = float(q.get("ct_mult") or 0.01)
|
body["ct_mult"] = float(q.get("ct_mult") or 0.01)
|
||||||
if is_option_primary(body):
|
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)
|
cs = float(body.get("contract_size") or 0.01)
|
||||||
get_cs = cfg.get("get_contract_size")
|
get_cs = cfg.get("get_contract_size")
|
||||||
sym = str(body.get("exchange_symbol") or "")
|
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)
|
ct = float(body.get("ct_mult") or q.get("ct_mult") or 0.01)
|
||||||
ratio = float(body.get("option_perp_ratio") or 2)
|
ratio = float(body.get("option_perp_ratio") or 2)
|
||||||
body["contract_size"] = cs
|
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:
|
if market and float(body.get("sheets") or 0) > 0:
|
||||||
# 手动下单:以张数为准,永续按比例
|
sized = contracts_from_option_sheets(
|
||||||
sheets_i = float(int(float(body["sheets"])))
|
sheets=float(body["sheets"]),
|
||||||
eth = round(sheets_i * ct, 2)
|
ct_mult=ct,
|
||||||
body["sheets"] = sheets_i
|
ratio=ratio,
|
||||||
body["eth_qty"] = eth
|
contract_size=cs,
|
||||||
body["contracts"] = (eth / ratio) / cs if ratio > 0 and cs > 0 else 0
|
option_index=opt_idx,
|
||||||
sized = {
|
perp_index=perp_idx,
|
||||||
"ok": True,
|
)
|
||||||
"sheets": sheets_i,
|
if not sized.get("ok"):
|
||||||
"eth_qty": eth,
|
return {"ok": False, "msg": sized.get("msg") or "定仓失败", "quote": q, "sizing": sized}
|
||||||
"contracts": body["contracts"],
|
body["sheets"] = sized["sheets"]
|
||||||
"ratio": ratio,
|
body["eth_qty"] = sized.get("coin_qty") or sized.get("eth_qty")
|
||||||
}
|
body["contracts"] = sized["contracts"]
|
||||||
else:
|
else:
|
||||||
sized = size_from_premium(
|
sized = size_from_premium(
|
||||||
premium_budget=float(body.get("premium_budget") or 0),
|
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,
|
ct_mult=ct,
|
||||||
ratio=ratio,
|
ratio=ratio,
|
||||||
contract_size=cs,
|
contract_size=cs,
|
||||||
|
option_index=opt_idx,
|
||||||
|
perp_index=perp_idx,
|
||||||
)
|
)
|
||||||
if not sized.get("ok"):
|
if not sized.get("ok"):
|
||||||
return {"ok": False, "msg": sized.get("msg") or "定仓失败", "quote": q, "sizing": sized}
|
return {"ok": False, "msg": sized.get("msg") or "定仓失败", "quote": q, "sizing": sized}
|
||||||
body["sheets"] = sized["sheets"]
|
body["sheets"] = sized["sheets"]
|
||||||
body["contracts"] = sized["contracts"]
|
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)
|
ask_sz = float(q.get("ask_sz") or 0)
|
||||||
if ask_sz > 0 and float(body["sheets"]) > ask_sz:
|
if ask_sz > 0 and float(body["sheets"]) > ask_sz:
|
||||||
body["sheets"] = float(int(ask_sz))
|
body["sheets"] = float(int(ask_sz))
|
||||||
if body["sheets"] <= 0:
|
if body["sheets"] <= 0:
|
||||||
return {"ok": False, "msg": "卖一深度不足 1 张", "quote": q, "sizing": sized}
|
return {"ok": False, "msg": "卖一深度不足 1 张", "quote": q, "sizing": sized}
|
||||||
eth = round(float(body["sheets"]) * ct, 2)
|
resized = contracts_from_option_sheets(
|
||||||
body["eth_qty"] = eth
|
sheets=float(body["sheets"]),
|
||||||
body["contracts"] = (eth / ratio) / cs if ratio > 0 and cs > 0 else 0
|
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 {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"ask": float(q["ask"]),
|
"ask": float(q["ask"]),
|
||||||
@@ -837,10 +886,12 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
|
|||||||
"msg": (
|
"msg": (
|
||||||
f"手动定仓: {body.get('eth_qty')}币 / "
|
f"手动定仓: {body.get('eth_qty')}币 / "
|
||||||
f"{body.get('sheets')}张期权 / {float(body.get('contracts') or 0):.4f}张永续 @{q['ask']}"
|
f"{body.get('sheets')}张期权 / {float(body.get('contracts') or 0):.4f}张永续 @{q['ask']}"
|
||||||
|
f"{cross_note}"
|
||||||
if market
|
if market
|
||||||
else (
|
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"{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,
|
is_option_primary,
|
||||||
validate_option_primary_start,
|
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):
|
if is_option_primary(body):
|
||||||
from lib.hedge_plan.hedge_plan_option_primary_lib import validate_option_primary_watch
|
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:
|
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)
|
attach_hedge_plan_templates(app, repo_root)
|
||||||
|
mode_summary = clamp_hedge_po_mode_on_startup()
|
||||||
cfg = _build_cfg(app_module)
|
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
|
app.extensions["hedge_plan_cfg"] = cfg
|
||||||
register_hedge_plan_routes(app, cfg)
|
register_hedge_plan_routes(app, cfg)
|
||||||
_maybe_start_monitor(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"),
|
"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(),
|
"perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
|
||||||
"options_account_label": (os.getenv("OKX_OPTIONS_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(
|
"trade_budget_usdc": float(
|
||||||
os.getenv("OKX_OPTIONS_COIN_BUDGET_USDT")
|
os.getenv("OKX_OPTIONS_COIN_BUDGET_USDT")
|
||||||
or os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC")
|
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,
|
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)
|
opt_primary = is_option_primary(body)
|
||||||
view = str(body.get("direction") or "long")
|
view = str(body.get("direction") or "long")
|
||||||
perp_dir = (
|
perp_dir = (
|
||||||
str((perp or {}).get("direction") or "")
|
str((perp or {}).get("direction") or "")
|
||||||
or (perp_direction_for_view(view) if opt_primary else view)
|
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_row = {
|
||||||
"plan_type": "perp_options",
|
"plan_type": "perp_options",
|
||||||
"status": "partial" if is_partial else "active",
|
"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,
|
"direction": view,
|
||||||
"entry_mark": float(body.get("entry") or 0),
|
"entry_mark": float(body.get("entry") or 0),
|
||||||
"tp": float(body.get("tp") or 0) if not opt_primary else 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,
|
"note": (result.get("msg") or "")[:500] if is_partial else None,
|
||||||
"option_primary": 1 if opt_primary else 0,
|
"option_primary": 1 if opt_primary else 0,
|
||||||
"perp_direction": perp_dir,
|
"perp_direction": perp_dir,
|
||||||
|
"po_mode": po_mode,
|
||||||
|
"perp_underlying": str(body.get("perp_underlying") or perp_underlying(po_mode)).upper(),
|
||||||
}
|
}
|
||||||
if opt_primary:
|
if opt_primary:
|
||||||
exit_mode = str(body.get("exit_mode") or "target").strip().lower()
|
# 实例模式强制组合盈利金额出场
|
||||||
if exit_mode not in ("profit", "target"):
|
exit_mode = "profit"
|
||||||
exit_mode = "target"
|
|
||||||
plan_row.update(
|
plan_row.update(
|
||||||
{
|
{
|
||||||
"option_target_points": float(body.get("option_target_points") or 0),
|
"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_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_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"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
try:
|
||||||
init_hedge_plan_tables(conn)
|
init_hedge_plan_tables(conn)
|
||||||
view = str(body.get("direction") or "long")
|
view = str(body.get("direction") or "long")
|
||||||
money = str(body.get("moneyness") or body.get("option_moneyness") or "otm").strip().lower()
|
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(
|
plan_id = insert_plan(
|
||||||
conn,
|
conn,
|
||||||
{
|
{
|
||||||
"plan_type": "perp_options",
|
"plan_type": "perp_options",
|
||||||
"status": "watching",
|
"status": "watching",
|
||||||
"underlying": str(body.get("underlying") or "ETH").upper(),
|
"underlying": str(body.get("underlying") or option_underlying(po_mode)).upper(),
|
||||||
"direction": view,
|
"direction": view,
|
||||||
"entry_mark": float(body.get("index_px") or body.get("entry") or 0) or None,
|
"entry_mark": float(body.get("index_px") or body.get("entry") or 0) or None,
|
||||||
"tp": 0,
|
"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),
|
"min_option_hours": float(body.get("min_option_hours") or 36),
|
||||||
"option_moneyness": money,
|
"option_moneyness": money,
|
||||||
"option_leverage": float(body.get("option_leverage") or 0),
|
"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()
|
conn.commit()
|
||||||
@@ -650,24 +678,33 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
@app.route("/api/hedge-plan/market")
|
@app.route("/api/hedge-plan/market")
|
||||||
@lr
|
@lr
|
||||||
def api_hedge_market():
|
def api_hedge_market():
|
||||||
base = (request.args.get("base") or cfg.get("default_underly") or "ETH").strip().upper()
|
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||||
if base not in ("BTC", "ETH"):
|
get_hedge_po_mode,
|
||||||
return jsonify({"ok": False, "msg": "对冲计划仅支持 BTC/ETH"}), 400
|
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()
|
direction = (request.args.get("direction") or "long").strip().lower()
|
||||||
if direction not in ("long", "short"):
|
if direction not in ("long", "short"):
|
||||||
direction = "long"
|
direction = "long"
|
||||||
option_primary = (request.args.get("option_primary") or "").strip().lower() in (
|
# 实例模式强制以期权为主
|
||||||
"1",
|
option_primary = True
|
||||||
"true",
|
|
||||||
"yes",
|
|
||||||
"on",
|
|
||||||
)
|
|
||||||
data, err = _fetch_perp_market(cfg, base)
|
data, err = _fetch_perp_market(cfg, base)
|
||||||
if err:
|
if err:
|
||||||
return jsonify({"ok": False, "msg": err}), 400
|
return jsonify({"ok": False, "msg": err}), 400
|
||||||
sizing_mode = load_position_sizing_mode()
|
sizing_mode = load_position_sizing_mode()
|
||||||
gates = _gates_dict(cfg, "perp_options")
|
gates = _gates_dict(cfg, "perp_options")
|
||||||
if option_primary:
|
|
||||||
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
||||||
opt_type_for_view,
|
opt_type_for_view,
|
||||||
perp_direction_for_view,
|
perp_direction_for_view,
|
||||||
@@ -675,16 +712,17 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
suggested = opt_type_for_view(direction)
|
suggested = opt_type_for_view(direction)
|
||||||
perp_dir = perp_direction_for_view(direction)
|
perp_dir = perp_direction_for_view(direction)
|
||||||
acct_note = "以期权为主:看法腿买期权,永续反向对冲"
|
acct_note = (
|
||||||
else:
|
f"实例模式 {mode_payload['po_mode_label']}:期权 {option_underlying(mode)}"
|
||||||
suggested = "P" if direction == "long" else "C"
|
f" + 永续 {base};一子账户互不干涉"
|
||||||
perp_dir = direction
|
)
|
||||||
acct_note = "永续腿使用合约(交易)账户可用 USDT"
|
|
||||||
out = {
|
out = {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"base": base,
|
"base": base,
|
||||||
|
"option_underlying": option_underlying(mode),
|
||||||
|
"perp_underlying": base,
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
"option_primary": option_primary,
|
"option_primary": True,
|
||||||
"suggested_opt_type": suggested,
|
"suggested_opt_type": suggested,
|
||||||
"perp_direction": perp_dir,
|
"perp_direction": perp_dir,
|
||||||
**data,
|
**data,
|
||||||
@@ -693,9 +731,17 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
"account_kind": "perp",
|
"account_kind": "perp",
|
||||||
"account_label": cfg.get("perp_account_label") or "合约账户",
|
"account_label": cfg.get("perp_account_label") or "合约账户",
|
||||||
"account_note": acct_note,
|
"account_note": acct_note,
|
||||||
|
**mode_payload,
|
||||||
}
|
}
|
||||||
return jsonify(out)
|
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")
|
@app.route("/api/hedge-plan/options-chain")
|
||||||
@lr
|
@lr
|
||||||
def api_hedge_options_chain():
|
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")
|
ex = cfg.get("exchange_options")
|
||||||
if ex is None:
|
if ex is None:
|
||||||
return jsonify({"ok": False, "msg": "期权交易所未初始化"}), 400
|
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
|
# 热更新:链展示天数每次读 env
|
||||||
chain_max_dte = float(
|
chain_max_dte = float(
|
||||||
os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS")
|
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"
|
<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-hedge-enabled="{{ '1' if hedge_plan_enabled else '0' }}"
|
||||||
data-options-enabled="{{ '1' if options_enabled else '0' }}"
|
data-options-enabled="{{ '1' if options_enabled else '0' }}"
|
||||||
data-show-perp="{{ '1' if hedge_plan_show_perp_options else '0' }}"
|
data-show-perp="{{ '1' if hedge_plan_show_perp_options else '0' }}"
|
||||||
@@ -24,6 +31,7 @@
|
|||||||
{# 顶栏「对冲计划」已改为导航「永期/期期对冲」;页内不再重复大标题 #}
|
{# 顶栏「对冲计划」已改为导航「永期/期期对冲」;页内不再重复大标题 #}
|
||||||
<div class="hp-page-toolbar">
|
<div class="hp-page-toolbar">
|
||||||
<button type="button" class="btn-secondary" id="hp-refresh" title="刷新永续行情与期权链">刷新行情</button>
|
<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>
|
<span class="muted hp-po-gate-inline" id="hp-po-gate-inline" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
{# 模式由导航/env 决定,页内不再显示「永期对冲」Tab #}
|
{# 模式由导航/env 决定,页内不再显示「永期对冲」Tab #}
|
||||||
@@ -46,19 +54,22 @@
|
|||||||
<details class="tip-collapse hp-rule-collapse">
|
<details class="tip-collapse hp-rule-collapse">
|
||||||
<summary class="tip-collapse-summary">规则说明</summary>
|
<summary class="tip-collapse-summary">规则说明</summary>
|
||||||
<div class="tip-collapse-body rule-tip">
|
<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>:开仓按<strong>卖一限价</strong>(非交易所市价)。先开期权(按张数买币并+5%滑点),再开永续。永续杠杆取 env:BTC=<code>BTC_LEVERAGE</code>/ETH=<code>ALT_LEVERAGE</code>。</p>
|
||||||
<p><strong>出场</strong>:盈利金额(扣开平双边手续费后净利)或目标位点数二选一。平仓先期权(须买一流动性)再永续,再把币全部卖回 USDT。一账户同时仅一笔对冲仓。</p>
|
<p><strong>出场</strong>:仅<strong>组合盈利金额(U)</strong>(期权净利×期权指数 + 永续净利,扣开平双边手续费)。平仓先期权(须买一流动性)再永续,再把期权标的币卖回 USDT。一账户同时仅一笔对冲仓。</p>
|
||||||
<p><strong>开仓校验</strong>:已选合约 · 张数≥1 · 出场条件已填 · 门禁「开仓:可」· 账户有足够交易 USDT。</p>
|
<p><strong>开仓校验</strong>:已选合约 · 张数≥1 · 盈利目标已填 · 门禁「开仓:可」· 账户有足够交易 USDT。</p>
|
||||||
<p><strong>期权链</strong>:默认显示约 <strong>5</strong> 档(平值 + 实值2 + 虚值2);勾选「展开全部」查看该到期更多行权价。</p>
|
<p><strong>期权链</strong>:默认显示约 <strong>5</strong> 档(平值 + 实值2 + 虚值2);勾选「展开全部」查看该到期更多行权价。</p>
|
||||||
<p><strong>账户</strong>:期权币本位 · 交易/资金计 USDT · 永续全仓 / 期权逐仓(余额见下方划转)。</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>
|
<p class="muted" id="hp-po-pair-hint">买 Call + 永续空 · 点「选择」弹窗下单。</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<div class="form-row hp-opt-toolbar hp-po-opt-toolbar options-chain-toolbar">
|
<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>
|
<span class="hp-uly-lock" id="hp-uly-lock" title="由 HEDGE_PO_MODE 锁定,不可页内切换">
|
||||||
<button type="button" class="btn-secondary hp-uly-btn" data-uly="BTC">BTC</button>
|
<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>
|
<select id="hp-exp-select"><option value="">选择到期日</option></select>
|
||||||
<span class="hp-oo-seg hp-po-dir-seg" role="group" aria-label="期权类型">
|
<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>
|
<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
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
from lib.hedge_plan.hedge_plan_option_primary_lib import (
|
||||||
PREMIUM_EXEC_FACTOR,
|
PREMIUM_EXEC_FACTOR,
|
||||||
@@ -114,6 +116,7 @@ class TestOptionPrimary(unittest.TestCase):
|
|||||||
body = {
|
body = {
|
||||||
"option_primary": True,
|
"option_primary": True,
|
||||||
"watch_entry": 0,
|
"watch_entry": 0,
|
||||||
|
"market_order": 1,
|
||||||
"direction": "long",
|
"direction": "long",
|
||||||
"contracts": 1,
|
"contracts": 1,
|
||||||
"opt_inst_id": "ETH-USD-260831-1900-C",
|
"opt_inst_id": "ETH-USD-260831-1900-C",
|
||||||
@@ -121,6 +124,8 @@ class TestOptionPrimary(unittest.TestCase):
|
|||||||
"sheets": 2,
|
"sheets": 2,
|
||||||
"exchange_symbol": "ETH/USDT:USDT",
|
"exchange_symbol": "ETH/USDT:USDT",
|
||||||
"premium_budget": 100,
|
"premium_budget": 100,
|
||||||
|
"exit_mode": "profit",
|
||||||
|
"profit_target_usdt": 20,
|
||||||
"option_target_points": 50,
|
"option_target_points": 50,
|
||||||
"perp_target_points": 30,
|
"perp_target_points": 30,
|
||||||
"option_perp_ratio": 2,
|
"option_perp_ratio": 2,
|
||||||
@@ -132,8 +137,13 @@ class TestOptionPrimary(unittest.TestCase):
|
|||||||
"min_option_hours": 36,
|
"min_option_hours": 36,
|
||||||
"hours_to_expiry": 40,
|
"hours_to_expiry": 40,
|
||||||
"option_leverage": 100,
|
"option_leverage": 100,
|
||||||
|
"underlying": "ETH",
|
||||||
}
|
}
|
||||||
self.assertIsNone(validate_option_primary_start(body))
|
self.assertIsNone(validate_option_primary_start(body))
|
||||||
|
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))
|
self.assertIsNone(validate_start_body("perp_options", body))
|
||||||
bad = dict(body, opt_type="P")
|
bad = dict(body, opt_type="P")
|
||||||
self.assertIsNotNone(validate_start_body("perp_options", bad))
|
self.assertIsNotNone(validate_start_body("perp_options", bad))
|
||||||
|
|||||||
@@ -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