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 + ? "" + : `