Add env toggles to show/hide perp and options hedge plan tabs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-19 00:43:04 +08:00
parent 9e1343981d
commit f21e4d1166
10 changed files with 109 additions and 6 deletions
+3
View File
@@ -125,6 +125,9 @@ OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
# 对冲计划(仅 OKX;前端 env「对冲计划」;详见 docs/对冲计划开发方案.md)
# =============================================================================
HEDGE_PLAN_ENABLED=false
# 页面 Tab 显示(默认全部显示,可单独关闭;不影响已有进行中/历史计划)
HEDGE_PLAN_SHOW_PERP_OPTIONS=true
HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true
HEDGE_PLAN_LIVE_ORDER=false
HEDGE_PLAN_OPEN_ORDER=options_first
HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true
+4
View File
@@ -6785,6 +6785,10 @@ def render_main_page(page="trade", embed_mode=None):
options_nav_visible=True,
hedge_plan_enabled=os.getenv("HEDGE_PLAN_ENABLED", "false").lower() in ("1", "true", "yes", "on"),
hedge_plan_nav_visible=os.getenv("HEDGE_PLAN_ENABLED", "false").lower() in ("1", "true", "yes", "on"),
hedge_plan_show_perp_options=os.getenv("HEDGE_PLAN_SHOW_PERP_OPTIONS", "true").lower()
in ("1", "true", "yes", "on"),
hedge_plan_show_options_options=os.getenv("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", "true").lower()
in ("1", "true", "yes", "on"),
options_trade_budget=OKX_OPTIONS_TRADE_BUDGET_USDC,
options_default_underly=OKX_OPTIONS_DEFAULT_UNDERLY,
risk_status=risk_status,
+4
View File
@@ -553,6 +553,8 @@ realized_pnl_total = pnl_option_close - abs(pnl_perp_sl)
| 变量 | 前端标签 | 默认 | 控件 | 热更新 | 说明 |
|------|----------|------|------|--------|------|
| `HEDGE_PLAN_ENABLED` | 启用对冲计划 | false | bool | 热更优先 | 总开关:导航 + API |
| `HEDGE_PLAN_SHOW_PERP_OPTIONS` | 显示永期对冲 | true | bool | 热更 | 关则隐藏永期 Tab,不可测算/开仓 |
| `HEDGE_PLAN_SHOW_OPTIONS_OPTIONS` | 显示期期对冲 | true | bool | 热更 | 关则隐藏期期 Tab,不可测算/开仓 |
| `HEDGE_PLAN_LIVE_ORDER` | 允许对冲真实下单 | false | bool | 热更 | 关则只测算/草稿 |
| `HEDGE_PLAN_OPEN_ORDER` | 永期开仓顺序 | options_first | select:`options_first`/`perp_first` | 热更 | 默认先期权后永续 |
| `HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS` | 永期止损后强制平期权 | true | bool | 热更 | **保护机制,默认 true** |
@@ -577,6 +579,8 @@ realized_pnl_total = pnl_option_close - abs(pnl_perp_sl)
```env
# --- 对冲计划(仅 OKX;前端 env「对冲计划」) ---
HEDGE_PLAN_ENABLED=false
HEDGE_PLAN_SHOW_PERP_OPTIONS=true
HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true
HEDGE_PLAN_LIVE_ORDER=false
HEDGE_PLAN_OPEN_ORDER=options_first
HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true
+33 -4
View File
@@ -5,9 +5,22 @@
const root = document.getElementById("hedge-plan-root");
if (!root) return;
function flagOn(attr) {
return root.getAttribute(attr) !== "0";
}
const showPerp = flagOn("data-show-perp");
const showOo = flagOn("data-show-oo");
function pickDefaultTab() {
if (showPerp) return "perp_options";
if (showOo) return "options_options";
return "active";
}
const state = {
tab: "perp_options",
mode: "perp_options",
tab: pickDefaultTab(),
mode: showPerp ? "perp_options" : showOo ? "options_options" : "perp_options",
underlying: root.getAttribute("data-default-underly") || "ETH",
moneyFilter: "all",
chain: null,
@@ -103,7 +116,10 @@
}
function syncTabUI() {
const tab = state.tab || "perp_options";
let tab = state.tab || pickDefaultTab();
if (tab === "perp_options" && !showPerp) tab = pickDefaultTab();
if (tab === "options_options" && !showOo) tab = pickDefaultTab();
state.tab = tab;
document.querySelectorAll(".hp-tab").forEach(function (b) {
const on = b.getAttribute("data-tab") === tab;
b.classList.toggle("active", on);
@@ -120,6 +136,16 @@
if (tab === "perp_options" || tab === "options_options") {
state.mode = tab;
}
const hint = $("hp-acct-hint");
if (hint) {
if (tab === "options_options") {
hint.textContent = "期期双腿→期权账户";
} else if (tab === "perp_options") {
hint.textContent = "永续腿→合约账户 · 期权腿→期权账户";
} else {
hint.textContent = "进行中/历史含永期与期期;显示开关只影响新建测算 Tab";
}
}
}
function setGateLine(gates) {
@@ -716,7 +742,10 @@
function bind() {
document.querySelectorAll(".hp-tab").forEach(function (b) {
b.addEventListener("click", function () {
state.tab = b.getAttribute("data-tab") || "perp_options";
const next = b.getAttribute("data-tab") || pickDefaultTab();
if (next === "perp_options" && !showPerp) return;
if (next === "options_options" && !showOo) return;
state.tab = next;
syncTabUI();
if (state.tab === "perp_options" || state.tab === "options_options") {
void loadGates();
+2
View File
@@ -83,6 +83,8 @@ HOT_RELOAD_EXACT = frozenset({
"APP_AUTH_DISABLED",
"WECHAT_WEBHOOK",
"HEDGE_PLAN_ENABLED",
"HEDGE_PLAN_SHOW_PERP_OPTIONS",
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS",
"HEDGE_PLAN_LIVE_ORDER",
"HEDGE_PLAN_OPEN_ORDER",
"HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
+4
View File
@@ -134,6 +134,8 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = {
"exchanges": frozenset({"okx"}),
"fields": [
("HEDGE_PLAN_ENABLED", "启用对冲计划", "关闭则隐藏导航且不可开仓"),
("HEDGE_PLAN_SHOW_PERP_OPTIONS", "显示永期对冲", "默认 true;关闭后隐藏永期 Tab,不可测算/开仓"),
("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", "显示期期对冲", "默认 true;关闭后隐藏期期 Tab,不可测算/开仓"),
("HEDGE_PLAN_LIVE_ORDER", "允许对冲真实下单", "再与实盘 LIVE_TRADING_ENABLED 同开才可启动永期"),
("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
@@ -153,6 +155,8 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
"RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
}
+12
View File
@@ -334,6 +334,8 @@ def gate_status(
live_trading: bool = False,
active_count: int = 0,
max_active: int = 1,
show_perp_options: bool = True,
show_options_options: bool = True,
) -> dict[str, Any]:
from lib.trade.position_sizing_lib import is_full_margin_mode
@@ -349,6 +351,14 @@ def gate_status(
can_preview = False
can_start = False
reasons.append("期权模块未启用")
if pt == "perp_options" and not show_perp_options:
can_preview = False
can_start = False
reasons.append("永期对冲已隐藏(HEDGE_PLAN_SHOW_PERP_OPTIONS)")
if pt == "options_options" and not show_options_options:
can_preview = False
can_start = False
reasons.append("期期对冲已隐藏(HEDGE_PLAN_SHOW_OPTIONS_OPTIONS)")
if not live_order:
can_start = False
reasons.append("未允许对冲真实下单(HEDGE_PLAN_LIVE_ORDER)")
@@ -379,6 +389,8 @@ def gate_status(
"live_trading": live_trading,
"active_count": active_count,
"max_active": max_active,
"show_perp_options": bool(show_perp_options),
"show_options_options": bool(show_options_options),
"can_preview": can_preview,
"can_start": can_start,
"reasons": reasons,
+10
View File
@@ -106,6 +106,14 @@ def _hedge_enabled() -> bool:
return _env_bool("HEDGE_PLAN_ENABLED", False)
def _show_perp_options() -> bool:
return _env_bool("HEDGE_PLAN_SHOW_PERP_OPTIONS", True)
def _show_options_options() -> bool:
return _env_bool("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", True)
def _live_order() -> bool:
return _env_bool("HEDGE_PLAN_LIVE_ORDER", False)
@@ -140,6 +148,8 @@ def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
live_trading=bool(cfg.get("live_trading")) or _env_bool("LIVE_TRADING_ENABLED", False),
active_count=active,
max_active=_max_active(),
show_perp_options=_show_perp_options(),
show_options_options=_show_options_options(),
)
+11 -2
View File
@@ -2,6 +2,8 @@
data-default-underly="{{ options_default_underly | default('ETH') }}"
data-hedge-enabled="{{ '1' if hedge_plan_enabled else '0' }}"
data-options-enabled="{{ '1' if options_enabled else '0' }}"
data-show-perp="{{ '1' if hedge_plan_show_perp_options | default(true) else '0' }}"
data-show-oo="{{ '1' if hedge_plan_show_options_options | default(true) else '0' }}"
data-sizing-mode="{{ position_sizing_mode | default('risk') }}"
data-is-full-margin="{{ '1' if position_sizing_mode == 'full_margin' else '0' }}">
{% if not hedge_plan_enabled %}
@@ -10,6 +12,9 @@
{% if not options_enabled %}
<div class="flash" style="margin-bottom:12px">期权模块未启用,无法拉期权链.请先配置期权账户.</div>
{% endif %}
{% if hedge_plan_enabled and not (hedge_plan_show_perp_options | default(true)) and not (hedge_plan_show_options_options | default(true)) %}
<div class="flash" style="margin-bottom:12px">永期与期期 Tab 均已隐藏:可在 <code>env配置 → 对冲计划</code> 打开显示开关;进行中/历史仍可查看.</div>
{% endif %}
<div class="card hp-head-card">
<div class="hp-head-row">
@@ -19,8 +24,12 @@
<button type="button" class="btn-secondary" id="hp-refresh" title="刷新永续行情与期权链">刷新行情</button>
</div>
<div class="hp-tabs" role="tablist" aria-label="对冲计划分类">
<button type="button" class="hp-tab active" role="tab" aria-selected="true" data-tab="perp_options">永期对冲</button>
{% if hedge_plan_show_perp_options | default(true) %}
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="perp_options">永期对冲</button>
{% endif %}
{% if hedge_plan_show_options_options | default(true) %}
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="options_options">期期对冲</button>
{% endif %}
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="active">进行中的计划</button>
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="history">历史记录</button>
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="stats">统计分析</button>
@@ -255,4 +264,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=13"></script>
<script src="/static/hedge_plan.js?v=14"></script>
+26
View File
@@ -69,6 +69,32 @@ class TestHedgePlanCalc(unittest.TestCase):
self.assertFalse(g["can_start"])
self.assertTrue(any("全仓" in r for r in g["reasons"]))
def test_gate_hidden_plan_type_blocks_preview(self):
g = gate_status(
hedge_enabled=True,
sizing_mode="full_margin",
plan_type="perp_options",
options_enabled=True,
live_order=True,
live_trading=True,
show_perp_options=False,
)
self.assertFalse(g["can_preview"])
self.assertFalse(g["can_start"])
self.assertTrue(any("隐藏" in r for r in g["reasons"]))
self.assertFalse(g["show_perp_options"])
self.assertTrue(g["show_options_options"])
g2 = gate_status(
hedge_enabled=True,
sizing_mode="risk",
plan_type="options_options",
options_enabled=True,
show_options_options=False,
)
self.assertFalse(g2["can_preview"])
self.assertTrue(any("期期" in r for r in g2["reasons"]))
def test_oo_expiry_loss_flag(self):
a = {"opt_type": "C", "strike": 3300, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5}
b = {"opt_type": "P", "strike": 3100, "sheets": 1, "ct_mult": 0.01, "premium_paid": 5}