From 4904a86e03af3864321eec7cc5c894839aac5918 Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 6 Jul 2026 02:26:22 +0800 Subject: [PATCH] fix: hide hub close/entrust controls for intraday discipline profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose intraday_discipline via hub meta and block manual close/tpsl APIs; Gate instance card no longer shows 全平/平仓/委托. Co-authored-by: Cursor --- crypto_monitor_binance/app.py | 2 + crypto_monitor_gate/app.py | 2 + crypto_monitor_okx/app.py | 2 + docs/strategy/gate-intraday.md | 2 +- lib/trade/entry_model_lib.py | 10 +++++ manual_trading_hub/hub.py | 34 ++++++++++++++ manual_trading_hub/static/app.js | 77 +++++++++++++++++++++++--------- tests/test_entry_model_lib.py | 12 ++++- 8 files changed, 119 insertions(+), 22 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 4b13310..72e71e8 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -178,6 +178,7 @@ from lib.trade.entry_model_lib import ( build_intraday_entry_reason_options, build_trend_div_entry_reason_options, enrich_entry_model_display, + hub_meta_entry_context, migrate_entry_model_columns, order_entry_template_context, parse_manual_order_style_fields, @@ -9687,6 +9688,7 @@ def _hub_meta_bundle(): "btc_leverage": BTC_LEVERAGE, "alt_leverage": ALT_LEVERAGE, "trade_policy": trade_policy_template_context(TRADE_POLICY), + **hub_meta_entry_context(TRADE_POLICY), } diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 47cdd62..06eece6 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -177,6 +177,7 @@ from lib.trade.entry_model_lib import ( build_intraday_entry_reason_options, build_trend_div_entry_reason_options, enrich_entry_model_display, + hub_meta_entry_context, migrate_entry_model_columns, order_entry_template_context, parse_manual_order_style_fields, @@ -9522,6 +9523,7 @@ def _hub_meta_bundle(): "btc_leverage": BTC_LEVERAGE, "alt_leverage": ALT_LEVERAGE, "trade_policy": trade_policy_template_context(TRADE_POLICY), + **hub_meta_entry_context(TRADE_POLICY), } diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index b2b8df0..d65e808 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -176,6 +176,7 @@ from lib.trade.entry_model_lib import ( build_intraday_entry_reason_options, build_trend_div_entry_reason_options, enrich_entry_model_display, + hub_meta_entry_context, migrate_entry_model_columns, order_entry_template_context, parse_manual_order_style_fields, @@ -9065,6 +9066,7 @@ def _hub_meta_bundle(): "btc_leverage": BTC_LEVERAGE, "alt_leverage": ALT_LEVERAGE, "trade_policy": trade_policy_template_context(TRADE_POLICY), + **hub_meta_entry_context(TRADE_POLICY), } diff --git a/docs/strategy/gate-intraday.md b/docs/strategy/gate-intraday.md index de76db2..f1dd87a 100644 --- a/docs/strategy/gate-intraday.md +++ b/docs/strategy/gate-intraday.md @@ -263,7 +263,7 @@ TRADING_DAY_RESET_HOUR=8 |----|------| | 开仓类型 | 界面 **`假破` / `结构突破`**(code:`liquidity_false_break` / `structure_breakout`);**无** trend/swing 手选 | | 写入字段 | `trade_records.entry_model` / 复盘下拉同两项 | -| 隐藏操作 | 日内 profile 下 **隐藏** 平仓、委托、移动保本(**实例页 + 中控**,共用 lib 判断) | +| 隐藏操作 | 日内 profile 下 **隐藏** 平仓、委托、移动保本(**实例页 + 中控**,`intraday_discipline` / `order_entry_profile=intraday`) | | 隐藏表单项 | 不展示 1h/2h/4h 时间平仓、移动保本勾选(避免与 §9.3 混用) | | 后端可选 | 严格模式下拒绝 `del_order` / 改委托 API | diff --git a/lib/trade/entry_model_lib.py b/lib/trade/entry_model_lib.py index 210f1fc..6ebff88 100644 --- a/lib/trade/entry_model_lib.py +++ b/lib/trade/entry_model_lib.py @@ -207,6 +207,7 @@ def order_entry_template_context(policy: TradePolicy) -> dict: opts = entry_model_options() return { "order_entry_profile": profile, + "intraday_discipline": profile == PROFILE_INTRADAY, "entry_model_options": [ {"code": o.code, "label": o.label, "trade_style": o.trade_style, "help": o.help} for o in opts @@ -215,6 +216,15 @@ def order_entry_template_context(policy: TradePolicy) -> dict: } +def hub_meta_entry_context(policy: TradePolicy) -> dict: + """供 /api/hub/meta:中控按 profile 隐藏平仓/委托等。""" + profile = order_entry_profile(policy) + return { + "order_entry_profile": profile, + "intraday_discipline": profile == PROFILE_INTRADAY, + } + + def migrate_entry_model_columns(conn) -> None: for table in ("order_monitors", "trade_records"): try: diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index d0fb3c2..9115adb 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -2050,6 +2050,27 @@ def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict ) +_INTRADAY_CLOSE_BLOCK_MSG = ( + "日内账户禁止中控手动平仓/改委托,请等待计划止损/止盈或整点强制清仓" +) + + +def _meta_intraday_discipline(meta: dict | None) -> bool: + if not isinstance(meta, dict): + return False + if meta.get("intraday_discipline") is True: + return True + return meta.get("order_entry_profile") == "intraday" + + +async def _fetch_exchange_intraday_discipline( + client: httpx.AsyncClient, ex: dict +) -> bool: + data = await _fetch_flask_json(client, ex, "/api/hub/meta") + meta = (data or {}).get("meta") if isinstance(data, dict) else None + return _meta_intraday_discipline(meta if isinstance(meta, dict) else None) + + async def _fetch_exchange_flask_bundle( client: httpx.AsyncClient, ex: dict, *, trading_day: str | None = None ) -> tuple[dict | None, dict | None, list | None, dict | None, dict | None, dict | None]: @@ -2428,6 +2449,8 @@ async def api_close_position(exchange_id: str, body: ClosePositionBody): raise HTTPException(status_code=400, detail="side 须为 long 或 short") url = f"{ex['agent_url'].rstrip('/')}/emergency/close-position" async with httpx.AsyncClient() as client: + if await _fetch_exchange_intraday_discipline(client, ex): + raise HTTPException(status_code=403, detail=_INTRADAY_CLOSE_BLOCK_MSG) r = await client.post( url, headers=_agent_headers(), @@ -2464,6 +2487,8 @@ async def api_place_tpsl(exchange_id: str, body: PlaceTpslBody): raise HTTPException(status_code=404, detail="账户未启用") url = f"{ex['agent_url'].rstrip('/')}/orders/place-tpsl" async with httpx.AsyncClient() as client: + if await _fetch_exchange_intraday_discipline(client, ex): + raise HTTPException(status_code=403, detail=_INTRADAY_CLOSE_BLOCK_MSG) r = await client.post( url, headers=_agent_headers(), @@ -2521,6 +2546,8 @@ async def api_close_exchange(exchange_id: str): raise HTTPException(status_code=404, detail="账户未启用") url = f"{ex['agent_url'].rstrip('/')}/emergency/close-all" async with httpx.AsyncClient() as client: + if await _fetch_exchange_intraday_discipline(client, ex): + raise HTTPException(status_code=403, detail=_INTRADAY_CLOSE_BLOCK_MSG) r = await client.post(url, headers=_agent_headers(), timeout=120.0) try: body = r.json() @@ -2557,6 +2584,13 @@ async def api_close_all(body: CloseAllBody | None = Body(default=None)): async with httpx.AsyncClient() as client: async def one(ex: dict): + if await _fetch_exchange_intraday_discipline(client, ex): + return { + "id": ex["id"], + "name": ex["name"], + "skipped": True, + "reason": _INTRADAY_CLOSE_BLOCK_MSG, + } url = f"{ex['agent_url'].rstrip('/')}/emergency/close-all" try: r = await client.post(url, headers=_agent_headers(), timeout=120.0) diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index ebb2208..0145e83 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -2804,6 +2804,24 @@ ); } + function isIntradayDisciplineRow(row) { + const m = row && row.meta; + if (!m || typeof m !== "object") return false; + if (m.intraday_discipline === true) return true; + return m.order_entry_profile === "intraday"; + } + + function forceCloseHeadBadgeHtml(state) { + if (!state || !state.enabled) return ""; + return forceCloseSymbolBadgeHtml({ + force_close_enabled: true, + force_close_label: state.label || "强制清仓", + force_close_countdown: state.countdown || "--:--:--", + force_close_at_ms: state.next_at_ms, + force_close_active: state.active, + }); + } + function renderTrendDcaTable(t, tickMap) { const levels = resolveTrendDcaLevels(t); if (!levels.length) return ""; @@ -2979,7 +2997,7 @@ `; } - function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) { + function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap, intradayDiscipline) { const symbol = pos.symbol || ""; const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """); const side = (pos.side || "long").toLowerCase(); @@ -2999,6 +3017,7 @@ const tp = tpsl.tp; const tpMonitored = tpsl.tp_monitored; const isTrend = isTrendContext(mo, trendPlan); + const intraday = !!intradayDiscipline; const rr = resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan); const beSecured = isBreakevenSecured(side, entry, mo, cond, pos); const upnl = resolveTrendFloatingPnl(pos, trendPlan); @@ -3044,29 +3063,34 @@ if (riskLine) meta.push(riskLine); const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl); if (latestRiskLine) meta.push(latestRiskLine); - const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true; - meta.push( - `移动保本:${beOn ? "开" : "关"}` - ); + if (!intraday) { + const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true; + meta.push( + `移动保本:${beOn ? "开" : "关"}` + ); + } } else { meta.push("来源: 交易所持仓"); meta.push("风格: —"); - meta.push(`移动保本:关`); + if (!intraday) meta.push(`移动保本:关`); } const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : ""; const tcSymBadge = !isTrend && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : ""; const fcSymBadge = !isTrend && mo.force_close_enabled ? forceCloseSymbolBadgeHtml(mo) : ""; const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan); + const headActions = intraday + ? "" + : `
+ + +
`; return `
${tcSymBadge}${fcSymBadge}${symBeBadge} ${sideCn}
-
- - -
+ ${headActions}
${meta.map((m) => `${m}`).join("")}
@@ -3218,7 +3242,10 @@ !isTrendContext(mo, trendPlan) && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : ""; const fcBadge = !isTrendContext(mo, trendPlan) && mo.force_close_enabled ? forceCloseSymbolBadgeHtml(mo) : ""; - const actionCell = `
+ const intraday = !!options.intradayDiscipline; + const actionCell = intraday + ? "" + : `
`; @@ -3329,7 +3356,8 @@ .join("")}
`; } - function renderGridPositionsTable(exchangeId, exchangeKey, positions, orders, trends, tickMap) { + function renderGridPositionsTable(exchangeId, exchangeKey, positions, orders, trends, tickMap, intradayDiscipline) { + const intraday = !!intradayDiscipline; const rows = positions .map((p) => renderPositionTableRow( @@ -3339,7 +3367,7 @@ findMonitorOrder(orders, p.symbol, p.side), findTrendPlan(trends, p.symbol, p.side), tickMap, - { compact: true } + { compact: true, intradayDiscipline: intraday } ) ) .join(""); @@ -3362,6 +3390,7 @@ function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) { const tickMap = buildPriceTickMap(row); + const intraday = isIntradayDisciplineRow(row); let inner = renderAccountStatRow(row, ag); inner += `
交易所持仓 · ${pos.length} 仓
`; if (pos.length) { @@ -3371,13 +3400,16 @@ pos, orders, trends, - tickMap + tickMap, + intraday ); } else { inner += '
无持仓
'; } inner += renderCardStrategyStats(row, hm, flaskOk); - inner += `
点击标题栏进入全屏 · 委托 / 关键位 / 下单监控 / 趋势回调 / 顺势加仓
`; + inner += intraday + ? `
日内纪律:禁手动平仓/改委托 · 整点强制清仓${row.force_close && row.force_close.enabled ? " · " + esc(row.force_close.label || "强制清仓") : ""}
` + : `
点击标题栏进入全屏 · 委托 / 关键位 / 下单监控 / 趋势回调 / 顺势加仓
`; return inner; } @@ -3396,9 +3428,11 @@ kmap[k.id] = k; }); const flaskOpen = row.flask_url_browser || row.flask_url; + const intraday = isIntradayDisciplineRow(row); + const fcHeadBadge = intraday ? forceCloseHeadBadgeHtml(row.force_close) : ""; let html = `
-

${esc(row.name)}

+

${esc(row.name)}${fcHeadBadge ? " " + fcHeadBadge : ""}

${esc(flaskOpen || "")}
@@ -3407,7 +3441,7 @@ ${flaskOpen ? `下单` : ""} ${flaskOpen ? `监控位` : ""} ${flaskOpen ? `复盘` : ""} - + ${intraday ? "" : ``}
`; if (!row.http_ok || ag.ok === false) { @@ -3427,7 +3461,8 @@ p, findMonitorOrder(orders, p.symbol, p.side), findTrendPlan(trends, p.symbol, p.side), - tickMap + tickMap, + intraday ); }); } else { @@ -3740,12 +3775,14 @@ const openReview = flaskOpen ? `复盘` : ""; + const intraday = isIntradayDisciplineRow(row); + const fcHeadBadge = intraday ? forceCloseHeadBadgeHtml(row.force_close) : ""; return `
-
${esc(row.name)}${formatRiskStatusBadge(hm.risk_status)}
+
${esc(row.name)}${fcHeadBadge}${formatRiskStatusBadge(hm.risk_status)}
${esc(flaskOpen || "")}
@@ -3754,7 +3791,7 @@ ${openTrade} ${openKey} ${openReview} - + ${intraday ? "" : ``}
${inner}
diff --git a/tests/test_entry_model_lib.py b/tests/test_entry_model_lib.py index 787d6e9..7f304f6 100644 --- a/tests/test_entry_model_lib.py +++ b/tests/test_entry_model_lib.py @@ -6,6 +6,7 @@ from lib.trade.entry_model_lib import ( ENTRY_MODEL_SMALL_DIV, build_trend_div_entry_reason_options, entry_model_label, + hub_meta_entry_context, is_intraday_trading_profile, parse_manual_order_style_fields, resolve_trade_record_entry_reason, @@ -54,7 +55,16 @@ class TestEntryModelLib(unittest.TestCase): self.assertEqual(code, ENTRY_MODEL_SMALL_DIV) self.assertEqual(style, "swing") - def test_parse_intraday_uses_trade_style(self): + def test_hub_meta_intraday(self): + policy = load_trade_policy( + { + "TRADE_SYMBOL_RESTRICT_ENABLED": "true", + "TRADE_SYMBOL_WHITELIST": "BTC,ETH", + } + ) + ctx = hub_meta_entry_context(policy) + self.assertTrue(ctx["intraday_discipline"]) + self.assertEqual(ctx["order_entry_profile"], "intraday") policy = TradePolicy(False, "both", True, ("BTC", "ETH")) style, code, err = parse_manual_order_style_fields(policy, {"trade_style": "swing"}) self.assertIsNone(err)