Fix perp manual order watch_entry bug; slim dialog UI; nav shows 永期/期期.

Manual market_order was validated as watch (missing watch_entry=0). Collapse order calc into details, remove page head card, show mode name in nav.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 16:19:15 +08:00
parent 78b909761d
commit 7d29181a04
8 changed files with 177 additions and 61 deletions
+62 -16
View File
@@ -1002,23 +1002,46 @@
}
function setGateLine(gates) {
const text = (function () {
if (!gates) return "";
const parts = [
"计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"),
"测算:" + (gates.can_preview ? "可" : "否"),
"开仓:" + (gates.can_start ? "可" : "否"),
];
if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; "));
return parts.join(" · ");
})();
const el = $("hp-gate-line");
if (!el) return;
if (!gates) {
el.textContent = "";
return;
}
const parts = [
"计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"),
"测算:" + (gates.can_preview ? "可" : "否"),
"开仓:" + (gates.can_start ? "可" : "否"),
];
if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; "));
el.textContent = parts.join(" · ");
state.canStart = !!gates.can_start;
if (el) el.textContent = text;
const inline = $("hp-po-gate-inline");
if (inline) inline.textContent = text;
state.canStart = !!(gates && gates.can_start);
syncPreviewStartBtn();
}
function setPoOrderErr(msg, focusId) {
const err = $("hp-po-order-err");
if (err) {
if (msg) {
err.textContent = msg;
err.removeAttribute("hidden");
} else {
err.textContent = "";
err.setAttribute("hidden", "");
}
}
if (focusId && $(focusId)) {
try {
$(focusId).focus();
$(focusId).classList.add("hp-input-invalid");
setTimeout(function () {
if ($(focusId)) $(focusId).classList.remove("hp-input-invalid");
}, 2400);
} catch (e) {}
}
}
function syncPreviewStartBtn() {
const start = $("hp-preview-start");
if (!start) return;
@@ -3150,19 +3173,41 @@
} else {
if (!state.selected) throw new Error("请选用期权腿");
if (isOptionPrimary()) {
setPoOrderErr("");
const sized = currentOpSizing();
if (!sized || !(sized.sheets > 0)) throw new Error("请选择期权并填写张数");
if (!sized || !(sized.sheets > 0)) {
setPoOrderErr("请选择期权并填写张数(≥1)", "hp-order-sheets");
throw new Error("请选择期权并填写张数(≥1)");
}
const exitMode = state.exitMode === "target" ? "target" : "profit";
let optPts = 0;
let perpPts = 0;
let profitTarget = 0;
if (exitMode === "profit") {
profitTarget = numInput("hp-profit-target", NaN);
if (!(profitTarget > 0)) throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)");
if (!(profitTarget > 0)) {
setPoOrderErr("请填写目标盈利金额(USDT,扣双边手续费后净利)", "hp-profit-target");
throw new Error("请填写目标盈利金额(USDT,扣双边手续费后净利)");
}
} else {
optPts = numInput("hp-opt-target-pts", NaN);
perpPts = numInput("hp-perp-target-pts", NaN);
if (!(optPts > 0) || !(perpPts > 0)) throw new Error("请填写期权/永续目标位点数(须大于0)");
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)) {
setPoOrderErr("永续行情未就绪,请先点「刷新行情」");
throw new Error("永续行情未就绪,请先点「刷新行情」");
}
if (!state.canStart) {
const gateTxt =
($("hp-po-gate-inline") && $("hp-po-gate-inline").textContent) ||
($("hp-gate-line") && $("hp-gate-line").textContent) ||
"请先刷新行情/检查账户";
setPoOrderErr("门禁未满足,不可开仓: " + gateTxt);
throw new Error("门禁未满足,不可开仓: " + gateTxt);
}
const exp = currentExp("hp-exp-select");
const entry = indexPx() || Number((state.market && state.market.mark) || 0);
@@ -3174,6 +3219,7 @@
plan_type: "perp_options",
option_primary: true,
market_order: 1,
watch_entry: 0,
exit_mode: exitMode,
underlying: state.underlying,
direction: getDirection(),
+55 -1
View File
@@ -3454,12 +3454,66 @@ html[data-theme="light"] .opt-be-dist-down {
.hedge-plan-page-wrap .hp-oo-size-mode.is-selected .hp-oo-check,
.hedge-plan-page-wrap .hp-oo-close-mode.is-selected .hp-oo-check,
.hedge-plan-page-wrap .hp-po-dir.is-selected .hp-oo-check,
.hedge-plan-page-wrap .hp-po-exit-mode.is-selected .hp-oo-check,
.hedge-plan-page-wrap .hp-oo-size-mode.active .hp-oo-check,
.hedge-plan-page-wrap .hp-oo-close-mode.active .hp-oo-check,
.hedge-plan-page-wrap .hp-po-dir.active .hp-oo-check {
.hedge-plan-page-wrap .hp-po-dir.active .hp-oo-check,
.hedge-plan-page-wrap .hp-po-exit-mode.active .hp-oo-check {
display: inline;
color: var(--accent, #00d4ff);
}
.hedge-plan-page-wrap .hp-po-exit-mode .hp-oo-check {
display: none;
margin-right: 4px;
font-weight: 700;
}
.hedge-plan-page-wrap .hp-po-exit-mode.is-selected,
.hedge-plan-page-wrap .hp-po-exit-mode.active {
border-color: var(--accent, #00d4ff);
color: var(--text, #fff);
background: rgba(0, 212, 255, 0.16);
box-shadow: inset 0 0 0 1px rgba(0, 212, 255, 0.35);
font-weight: 700;
}
.hedge-plan-page-wrap .hp-page-toolbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin: 0 0 12px;
}
.hedge-plan-page-wrap .hp-po-gate-inline {
font-size: 0.82rem;
line-height: 1.35;
}
.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-quote-compact {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
align-items: baseline;
font-size: 0.84rem;
}
.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-quote-one {
margin: 2px 0 0;
font-size: 0.78rem;
}
.hedge-plan-page-wrap .hp-po-order-dialog .hp-po-order-calc {
margin-top: 4px;
}
.hedge-plan-page-wrap .hp-po-order-err {
margin: 6px 0 0;
font-size: 0.82rem;
}
.hedge-plan-page-wrap .hp-input-invalid {
border-color: #e85d5d !important;
box-shadow: 0 0 0 1px rgba(232, 93, 93, 0.45);
}
.hedge-plan-page-wrap .hp-po-field--wide {
grid-column: 1 / -1;
}
.opt-order-dialog.hp-po-order-dialog {
width: min(96vw, 560px);
}
.hedge-plan-page-wrap .hp-po-top {
display: flex;
flex-wrap: wrap;
+6 -4
View File
@@ -1180,11 +1180,13 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
if is_option_primary(body):
from lib.hedge_plan.hedge_plan_option_primary_lib import validate_option_primary_watch
# 以期权为主默认盯盘启动(非现场开仓);显式 watch_entry=0 才走即开校验
# 手动下单(market_order)或显式 watch_entry=0 → 即开校验;否则盯盘
market = body.get("market_order") in (True, 1, "1", "true", "yes", "on")
watch = body.get("watch_entry")
if watch in (None, "", True, 1, "1", "true", "yes", "on"):
return validate_option_primary_watch(body)
return validate_option_primary_start(body)
watch_off = watch in (False, 0, "0", "false", "no", "off")
if market or watch_off:
return validate_option_primary_start(body)
return validate_option_primary_watch(body)
need = ("direction", "entry", "tp", "sl", "contracts", "opt_inst_id", "sheets", "exchange_symbol")
for k in need:
if body.get(k) in (None, ""):
+6 -2
View File
@@ -863,12 +863,16 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
body["leverage"] = 100
else:
body["leverage"] = int(cfg.get("alt_leverage") or 5)
# 以期权为主:策略启动=盯盘,不现场开仓
# 以期权为主:默认盯盘;手动下单(market_order)或 watch_entry=0 则现场开仓
if plan_type == "perp_options":
from lib.hedge_plan.hedge_plan_option_primary_lib import is_option_primary
market = body.get("market_order") in (True, 1, "1", "true", "yes", "on")
watch = body.get("watch_entry")
watch_on = watch in (None, "", True, 1, "1", "true", "yes", "on")
watch_off = watch in (False, 0, "0", "false", "no", "off")
watch_on = (not market) and (not watch_off) and (
watch in (None, "", True, 1, "1", "true", "yes", "on")
)
if is_option_primary(body) and watch_on:
if dry_run:
return jsonify(
+44 -34
View File
@@ -21,30 +21,37 @@
<div class="flash" style="margin-bottom:12px">永期与期期 Tab 均已隐藏:请在 env「期权/对冲模式」切换交易模式.</div>
{% endif %}
<div class="card hp-head-card">
<div class="hp-head-row">
<h2 class="hp-title">对冲计划 <span class="muted hp-title-sub">测算 · 下单</span>
<a class="muted" href="/options/guide" target="_blank" rel="noopener" style="font-size:13px;font-weight:500;margin-left:8px">期权开平仓与监控说明</a>
</h2>
<button type="button" class="btn-secondary" id="hp-refresh" title="刷新永续行情与期权链">刷新行情</button>
</div>
<div class="hp-tabs" role="tablist" aria-label="对冲计划分类">
{% if hedge_plan_show_perp_options %}
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="perp_options">永期对冲</button>
{% endif %}
{% if hedge_plan_show_options_options %}
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="options_options">期期对冲</button>
{% endif %}
</div>
<p class="muted" id="hp-gate-line"></p>
<p class="muted hp-acct-hint" id="hp-acct-hint">永续腿→合约账户 · 期权腿→期权账户</p>
{# 顶栏「对冲计划」已改为导航「永期/期期对冲」;页内不再重复大标题 #}
<div class="hp-page-toolbar">
<button type="button" class="btn-secondary" id="hp-refresh" title="刷新永续行情与期权链">刷新行情</button>
<span class="muted hp-po-gate-inline" id="hp-po-gate-inline" aria-live="polite"></span>
</div>
<div class="hp-tabs" role="tablist" aria-label="对冲模式" hidden>
{% if hedge_plan_show_perp_options %}
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="perp_options">永期对冲</button>
{% endif %}
{% if hedge_plan_show_options_options %}
<button type="button" class="hp-tab" role="tab" aria-selected="false" data-tab="options_options">期期对冲</button>
{% endif %}
</div>
<p class="muted" id="hp-gate-line" hidden></p>
<p class="muted hp-acct-hint" id="hp-acct-hint" hidden>永续腿→合约账户 · 期权腿→期权账户</p>
<div id="hp-tab-perp_options" class="hp-tab-panel" role="tabpanel">
<div class="options-dual-grid" id="hp-po-layout">
<!-- 左:选约(点选择→弹窗下单,同期权页) -->
<!-- 左:选约(点选择→弹窗下单) -->
<div class="card hp-po-left-card hp-po-select-card options-order-card">
<h2>手动选约 · <span id="hp-opt-type-label">Call</span> <span class="muted hp-acct-tag" id="hp-opt-acct-tag">币本位</span></h2>
<details class="tip-collapse hp-rule-collapse">
<summary class="tip-collapse-summary">规则说明</summary>
<div class="tip-collapse-body rule-tip">
<p><strong>手动选约</strong>:左侧选 Call/Put 与到期,点「选择」弹窗下单。买 Call → 永续空;买 Put → 永续多。比例默认 <strong>2</strong>(期权名义÷比例=永续名义)。</p>
<p><strong>手动下单</strong>:开仓按<strong>卖一限价</strong>(非交易所市价)。先开期权(按张数买币并+5%滑点),再开永续。永续杠杆取 env:BTC=<code>BTC_LEVERAGE</code>/ETH=<code>ALT_LEVERAGE</code>。</p>
<p><strong>出场</strong>:盈利金额(扣开平双边手续费后净利)或目标位点数二选一。平仓先期权(须买一流动性)再永续,再把币全部卖回 USDT。一账户同时仅一笔对冲仓。</p>
<p><strong>开仓校验</strong>:已选合约 · 张数≥1 · 出场条件已填 · 门禁「开仓:可」· 账户有足够交易 USDT。</p>
<p class="muted" id="hp-po-pair-hint">买 Call + 永续空 · 点「选择」弹窗下单。</p>
</div>
</details>
<div class="form-row hp-opt-toolbar hp-po-opt-toolbar options-chain-toolbar">
<button type="button" class="btn-secondary hp-uly-btn active" data-uly="ETH">ETH</button>
<button type="button" class="btn-secondary hp-uly-btn" data-uly="BTC">BTC</button>
@@ -68,7 +75,6 @@
<button type="button" class="btn-secondary" id="hp-load-chain">刷新链</button>
<span id="hp-index-line" class="hp-po-index" aria-live="polite">指数 —</span>
</div>
<p class="muted hp-po-chain-hint" id="hp-po-pair-hint">买 Call + 永续空 · 点「选择」弹窗下单;比例默认 2(期权 1 → 永续 0.5)。</p>
<input type="hidden" id="hp-min-hours" value="0" />
<input type="hidden" id="hp-strike-interval" value="0" />
<input type="hidden" id="hp-opt-leverage" value="0" />
@@ -95,7 +101,7 @@
</div>
<div id="hp-opt-bal-line" class="muted hp-po-meta hp-opt-bal-line"></div>
<!-- 弹窗下单(同期权页) -->
<!-- 弹窗下单 -->
<div id="hp-order-panel-host" class="opt-order-backdrop" hidden aria-hidden="true">
<div id="hp-order-panel" class="opt-order-dialog hp-po-order-dialog" role="dialog" aria-modal="true" aria-labelledby="hp-order-dialog-title" style="display:none">
<div class="opt-order-dialog-head">
@@ -105,15 +111,15 @@
<div class="opt-order-layout">
<div class="opt-order-main">
<div id="hp-sel-inst" class="options-order-inst">—</div>
<p class="muted hp-po-meta" id="hp-premium-line"></p>
<div class="hp-po-quote-head">
<div class="hp-po-quote-compact">
<span id="hp-po-mark" class="hp-po-mark" aria-live="polite">标记 —</span>
<span class="muted hp-acct-tag" id="hp-perp-acct-tag">合约账户</span>
<span class="muted" id="hp-premium-line"></span>
</div>
<p id="hp-perp-quote" class="muted hp-po-meta">加载中…</p>
<p id="hp-perp-quote" class="muted hp-po-meta hp-po-quote-one">加载中…</p>
<p id="hp-po-perp-quote-right" class="muted hp-po-meta" hidden></p>
<span id="hp-po-mode-badge" class="hp-po-mode-badge" hidden>手动下单</span>
<span id="hp-po-card-title" hidden>下单</span>
<span class="muted hp-acct-tag" id="hp-perp-acct-tag" hidden>合约账户</span>
<div class="hp-po-fields hidden" id="hp-po-fields-insurance" hidden>
<label class="hp-po-field">
@@ -151,19 +157,17 @@
<input type="number" step="1" id="hp-perp-leverage" value="{{ hedge_plan_eth_leverage | default(5) }}" readonly autocomplete="off" inputmode="numeric" data-lpignore="true" data-1p-ignore="true" data-form-type="other" title="BTC=BTC_LEVERAGE ETH=ALT_LEVERAGE" />
</label>
</div>
<p class="muted hp-po-max-line" id="hp-po-max-line">期权开仓最大 —</p>
<p class="muted hp-po-perp-auto" id="hp-po-perp-auto">永续开仓 —</p>
</section>
<section class="hp-po-section">
<h3 class="hp-po-section-title">出场条件</h3>
<h3 class="hp-po-section-title">出场条件 <span class="muted" style="font-weight:500;font-size:.78rem">必填其一</span></h3>
<div class="hp-oo-seg hp-po-exit-seg" role="group" aria-label="出场方式">
<button type="button" class="btn-secondary hp-po-exit-mode is-selected" data-exit="profit"><span class="hp-oo-check" aria-hidden="true">✓</span>盈利金额出场</button>
<button type="button" class="btn-secondary hp-po-exit-mode" data-exit="target"><span class="hp-oo-check" aria-hidden="true">✓</span>目标位出场</button>
</div>
<div class="hp-po-fields hp-po-fields--section" id="hp-po-exit-profit">
<label class="hp-po-field">
<label class="hp-po-field hp-po-field--wide">
<span class="hp-po-field-lab">目标盈利 <em>USDT</em></span>
<input type="number" step="any" min="0.01" id="hp-profit-target" placeholder="扣双边手续费后净利" autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other" />
<input type="number" step="any" min="0.01" id="hp-profit-target" placeholder="必填 · 扣双边手续费后净利" autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other" required />
</label>
</div>
<div class="hp-po-fields hp-po-fields--section hidden" id="hp-po-exit-target" hidden>
@@ -176,11 +180,17 @@
<input type="number" step="any" id="hp-perp-target-pts" placeholder="点数" autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other" />
</label>
</div>
<p class="err hp-po-order-err" id="hp-po-order-err" hidden></p>
</section>
</div>
<div class="hp-po-summary">
<div id="hp-perp-pnl-line" class="hp-po-pnl"></div>
<div id="hp-sizing-line" class="muted hp-po-sizing"></div>
<details class="tip-collapse hp-rule-collapse hp-po-order-calc">
<summary class="tip-collapse-summary">仓位测算明细</summary>
<div class="tip-collapse-body">
<p class="muted hp-po-max-line" id="hp-po-max-line">期权开仓最大 —</p>
<p class="muted hp-po-perp-auto" id="hp-po-perp-auto">永续开仓 —</p>
<div id="hp-perp-pnl-line" class="hp-po-pnl"></div>
<div id="hp-sizing-line" class="muted hp-po-sizing"></div>
</div>
</details>
</div>
<div class="opt-order-dialog-actions">
<span id="hp-po-strategy-status" class="hp-po-strategy-status" aria-live="polite"></span>
@@ -416,4 +426,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=51"></script>
<script src="/static/hedge_plan.js?v=52"></script>
+1 -1
View File
@@ -44,7 +44,7 @@ DISPLAY_LABELS: dict[str, str] = {
"show_nav_env_config": "env配置",
"show_nav_options": "期权",
"show_nav_options_review": "期权复盘",
"show_nav_hedge_plan": "对冲计划",
"show_nav_hedge_plan": "永期/期期对冲",
"show_settings_transfer": "资金划转",
"show_settings_export": "数据导出",
"show_settings_password": "账户密码修改",
+2 -2
View File
@@ -38,7 +38,7 @@
<a href="/options/review" data-embed-tab="options_review" class="{% if initial_tab == 'options_review' %}active{% endif %}">期权复盘</a>
{% endif %}
{% if hedge_plan_nav_visible and display.show_nav_hedge_plan %}
<a href="/hedge-plan" data-embed-tab="hedge_plan" class="{% if initial_tab == 'hedge_plan' %}active{% endif %}">对冲计划</a>
<a href="/hedge-plan" data-embed-tab="hedge_plan" class="{% if initial_tab == 'hedge_plan' %}active{% endif %}">{% if hedge_plan_show_options_options %}期期对冲{% else %}永期对冲{% endif %}</a>
{% endif %}
{% if display.show_nav_risk_policy %}
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if initial_tab == 'risk_policy' %}active{% endif %}">风控说明</a>
@@ -84,7 +84,7 @@
<a href="/options" class="inst-m-tab{% if initial_tab == 'options' %} active{% endif %}" data-embed-tab="options">期权</a>
{% endif %}
{% if hedge_plan_nav_visible and display.show_nav_hedge_plan %}
<a href="/hedge-plan" class="inst-m-tab{% if initial_tab == 'hedge_plan' %} active{% endif %}" data-embed-tab="hedge_plan">对冲</a>
<a href="/hedge-plan" class="inst-m-tab{% if initial_tab == 'hedge_plan' %} active{% endif %}" data-embed-tab="hedge_plan">{% if hedge_plan_show_options_options %}期期{% else %}永期{% endif %}</a>
{% endif %}
<button type="button" class="inst-m-tab" data-embed-tab="more" id="inst-m-tab-more" aria-haspopup="dialog" aria-expanded="false">更多</button>
</nav>
+1 -1
View File
@@ -161,7 +161,7 @@
<a href="/options/review" class="{% if page == 'options_review' %}active{% endif %}">期权复盘</a>
{% endif %}
{% if hedge_plan_nav_visible and display.show_nav_hedge_plan %}
<a href="/hedge-plan" class="{% if page == 'hedge_plan' %}active{% endif %}">对冲计划</a>
<a href="/hedge-plan" class="{% if page == 'hedge_plan' %}active{% endif %}">{% if hedge_plan_show_options_options %}期期对冲{% else %}永期对冲{% endif %}</a>
{% endif %}
{% if display.show_nav_risk_policy %}
<a href="/risk_policy" class="{% if page == 'risk_policy' %}active{% endif %}">风控说明</a>