From 55e94fe05917be7710f2af0175876f083eddacc2 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 5 Aug 2026 08:16:02 +0800 Subject: [PATCH] Restore Gate intraday close/TP-SL controls and document force-close rules. Co-authored-by: Cursor --- docs/env配置说明.md | 4 +-- docs/strategy/gate-intraday.md | 29 ++++++++-------- .../templates/embed_boot_scripts.html | 7 ++-- .../templates/embed_page_fragment.html | 6 ---- lib/instance/templates/index.html | 13 ++----- .../order_monitor_rule_tips_gate.html | 17 ++++++++++ manual_trading_hub/hub.py | 34 ------------------- manual_trading_hub/static/app.js | 14 +++----- manual_trading_hub/static/index.html | 2 +- 9 files changed, 44 insertions(+), 82 deletions(-) diff --git a/docs/env配置说明.md b/docs/env配置说明.md index 176dc6a..bcb1e26 100644 --- a/docs/env配置说明.md +++ b/docs/env配置说明.md @@ -107,8 +107,8 @@ AI 相关环境变量(`AI_PROVIDER`,`OPENAI_*`,`OLLAMA_*`,`AI_MODEL`,`AI_TIMEOUT | 切点前禁止新开仓 | | | 最大同时持仓 | | | 人工最低盈亏比 | | -| 强制清仓开关 | | -| 强制清仓整点(北京) | | +| 强制清仓开关 | `FORCE_CLOSE_ENABLED`;开启后在指定北京整点小时内,市价平掉本地 active 监控仓 | +| 强制清仓整点(北京) | `FORCE_CLOSE_BJ_HOUR`(0–23);例 `8` 表示 08:00~08:59;仅扫监控仓,不含交易所裸仓 | --- diff --git a/docs/strategy/gate-intraday.md b/docs/strategy/gate-intraday.md index 661a929..049df7f 100644 --- a/docs/strategy/gate-intraday.md +++ b/docs/strategy/gate-intraday.md @@ -186,21 +186,22 @@ - **禁止** 盘中亏着 **手点平仓** 充当止损(破坏统计与连错规则). - 若违规手动平亏:**视为当日纪律失败,建议停手**;复盘结果 **不得** 记为「止损」糊弄统计. -### 9.3 时间出场:仅 0 点(程序已实现) +### 9.3 时间出场:整点强制清仓(程序已实现,可开关) -- **唯一** 时间类出场:**当日 0:00(北京时间)前必须空仓**(赚赔都平). - **不使用** 下单表单里的 1h / 2h / 4h「开仓后 N 小时平」(`time_close`);与本策略无关. -- **程序兜底**(三所共用,Gate 已启用): +- **程序兜底**(三所共用;Gate 可用 env 开关): | env | 说明 | |-----|------| -| `FORCE_CLOSE_ENABLED=true` | 开启整点强制清仓 | -| `FORCE_CLOSE_BJ_HOUR=0` | 北京时间 **0 点那一小时**(00:00~00:59)执行 | +| `FORCE_CLOSE_ENABLED` | `true` 开启 / `false` 关闭整点强制清仓 | +| `FORCE_CLOSE_BJ_HOUR` | 北京时间整点小时(如 `0`=`00:00~00:59`,`8`=`08:00~08:59`) | - 实现:`force_close_before_reset()`(各实例 `app.py` 后台循环调用). -- 行为:对该小时仍 **active** 的 `order_monitors` **市价全平**,取消交易所触发单,写交易记录. -- **系统结果字段**:`result = 强制清仓`;备注含「北京时间 0:00 整点风控清仓」. -- **策略口语「0 点平仓」= 系统「强制清仓」**,统计连错时按 §8 盈亏判定,不按字段名区分. +- 行为:开启时,在该整点小时内对仍 **active** 的 `order_monitors` **市价全平**,取消交易所触发单,写交易记录. +- **系统结果字段**:`result = 强制清仓`;备注含「北京时间 X:00 整点风控清仓」. +- **仅扫本地监控仓**;交易所裸仓且无 active 监控时**不会**被此逻辑平掉. +- UI:开仓规则说明「平仓 / 委托 / 强制清仓」折叠区 + 顶栏徽章(开启时). +- 持仓卡可手动「平仓 / 委托 / 撤止盈止损」(与纪律策略并行;策略上仍不建议亏着手平充当止损). > **与 `TRADING_DAY_RESET_HOUR=8` 无关**:后者只切 **交易日**(统计,8 点前禁开等),**不会**自动平仓. @@ -244,16 +245,16 @@ | 项 | 说明 | |----|------| | 日内 profile 判定 | `is_intraday_trading_profile()`(`lib/trade/entry_model_lib.py`) | -| 0 点强制清仓 | `FORCE_CLOSE_ENABLED` + `FORCE_CLOSE_BJ_HOUR`;`force_close_before_reset()`;结果 **`强制清仓`** | -| UI 标识 | 顶栏 **强制清仓 已开启** 徽章 + 持仓卡片 **倒计时**(三所 + 中控) | +| 整点强制清仓 | `FORCE_CLOSE_ENABLED` + `FORCE_CLOSE_BJ_HOUR`;`force_close_before_reset()`;结果 **`强制清仓`** | +| UI 标识 | 顶栏徽章(开启时) + 开仓规则说明折叠区;持仓卡可手动平仓/委托 | | 交易记录展示 | 三所 UI / 中控:`强制清仓` 与止损同类 badge | -| 三所统一 | 币安 / OKX / Gate 同一函数与 env;**将来改日内只需各所 `.env` 打开,无需改代码** | +| 三所统一 | 币安 / OKX / Gate 同一函数与 env;**改日内只需各所 `.env`**,无需改代码 | -Gate 当前建议 env(节选): +Gate 示例 env(节选;是否开启按账户纪律决定): ```env -FORCE_CLOSE_ENABLED=true -FORCE_CLOSE_BJ_HOUR=0 +FORCE_CLOSE_ENABLED=false +FORCE_CLOSE_BJ_HOUR=8 TRADING_DAY_RESET_HOUR=8 ``` diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html index b2e3d70..7c2edba 100644 --- a/lib/instance/templates/embed_boot_scripts.html +++ b/lib/instance/templates/embed_boot_scripts.html @@ -741,13 +741,10 @@ function paintExchangeTpslRow(orderId, tpsl){ const tpText = document.getElementById(`ex-tp-text-${orderId}`); const slBtn = document.getElementById(`ex-sl-cancel-${orderId}`); const tpBtn = document.getElementById(`ex-tp-cancel-${orderId}`); - const intraday = (document.body && document.body.getAttribute("data-intraday-discipline")) === "1"; if(slText) slText.innerText = formatExTpslLine('sl', data.sl); if(tpText) tpText.innerText = formatExTpslLine('tp', data.tp); - if(!intraday){ - if(slBtn) slBtn.disabled = !(data.sl && data.sl.order_id); - if(tpBtn) tpBtn.disabled = !(data.tp && data.tp.order_id); - } + if(slBtn) slBtn.disabled = !(data.sl && data.sl.order_id); + if(tpBtn) tpBtn.disabled = !(data.tp && data.tp.order_id); } function toggleTpslModalMode(){ const mode = (document.getElementById('tpsl-modal-mode')||{}).value || 'price'; diff --git a/lib/instance/templates/embed_page_fragment.html b/lib/instance/templates/embed_page_fragment.html index 285148a..e49a528 100644 --- a/lib/instance/templates/embed_page_fragment.html +++ b/lib/instance/templates/embed_page_fragment.html @@ -175,10 +175,8 @@ {{ '做多' if o.direction == 'long' else '做空' }}
- {% if not intraday_discipline %} 平仓 - {% endif %}
@@ -238,15 +236,11 @@
交易所止盈止损
止损:加载中… - {% if not intraday_discipline %} - {% endif %}
止盈:加载中… - {% if not intraday_discipline %} - {% endif %}
diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html index 1858cb3..79bdbb1 100644 --- a/lib/instance/templates/index.html +++ b/lib/instance/templates/index.html @@ -252,10 +252,8 @@ {{ '做多' if o.direction == 'long' else '做空' }}
- {% if not intraday_discipline %} 平仓 - {% endif %}
@@ -315,15 +313,11 @@
交易所止盈止损
止损:加载中… - {% if not intraday_discipline %} - {% endif %}
止盈:加载中… - {% if not intraday_discipline %} - {% endif %}
@@ -1190,13 +1184,10 @@ function paintExchangeTpslRow(orderId, tpsl){ const tpText = document.getElementById(`ex-tp-text-${orderId}`); const slBtn = document.getElementById(`ex-sl-cancel-${orderId}`); const tpBtn = document.getElementById(`ex-tp-cancel-${orderId}`); - const intraday = (document.body && document.body.getAttribute("data-intraday-discipline")) === "1"; if(slText) slText.innerText = formatExTpslLine('sl', data.sl); if(tpText) tpText.innerText = formatExTpslLine('tp', data.tp); - if(!intraday){ - if(slBtn) slBtn.disabled = !(data.sl && data.sl.order_id); - if(tpBtn) tpBtn.disabled = !(data.tp && data.tp.order_id); - } + if(slBtn) slBtn.disabled = !(data.sl && data.sl.order_id); + if(tpBtn) tpBtn.disabled = !(data.tp && data.tp.order_id); } function toggleTpslModalMode(){ const mode = (document.getElementById('tpsl-modal-mode')||{}).value || 'price'; diff --git a/lib/strategy/templates/order_monitor_rule_tips_gate.html b/lib/strategy/templates/order_monitor_rule_tips_gate.html index 45c75a7..1ff7139 100644 --- a/lib/strategy/templates/order_monitor_rule_tips_gate.html +++ b/lib/strategy/templates/order_monitor_rule_tips_gate.html @@ -7,6 +7,23 @@ 人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1 +
+ 平仓 / 委托 / 强制清仓 +
+
    +
  • 止盈止损委托:开仓后挂交易所条件止盈/止损;持仓卡可查看状态,「委托」可重挂,「撤单」只撤对应条件单、不平仓。
  • +
  • 手动平仓:持仓卡「平仓」会市价平仓并撤销该合约条件单,交易记录记为「手动平仓」。
  • +
  • 强制清仓: + {% if force_close is defined and force_close.enabled %} + 已开启 · 北京时间 {{ force_close.hour_label }} 整点起该小时内,本地状态为 active 的下单监控会被市价清仓,结果记「强制清仓」。 + {% else %} + 当前已关闭(FORCE_CLOSE_ENABLED=false);开启后按 FORCE_CLOSE_BJ_HOUR 在北京时间该整点小时内清掉 active 监控仓。 + {% endif %} + 仅影响本系统监控中的仓;交易所裸仓且无本地监控时不会被此逻辑平掉。 +
  • +
+
+
计仓与保本说明
diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 2f545b0..46cbe6f 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -2565,27 +2565,6 @@ 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]: @@ -2980,8 +2959,6 @@ 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(), @@ -3018,8 +2995,6 @@ 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(), @@ -3077,8 +3052,6 @@ 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() @@ -3115,13 +3088,6 @@ 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 c36b148..0e048f1 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -3164,7 +3164,7 @@ const px = orderTriggerOrPrice(o); const trig = px != null ? fmtSymbolPrice(px, symbol, tickMap) : "—"; const cancelBtn = - !intraday && oid && o.channel !== "plan" + oid && o.channel !== "plan" ? `` : ""; const planHint = o.channel === "plan" ? '(下单监控)' : ""; @@ -3491,9 +3491,7 @@ 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 - ? "" - : `
+ const headActions = `
`; @@ -3657,9 +3655,7 @@ const fcBadge = !isTrendContext(mo, trendPlan) && mo.force_close_enabled ? forceCloseSymbolBadgeHtml(mo) : ""; const intraday = !!options.intradayDiscipline; - const actionCell = intraday - ? "" - : `
+ const actionCell = `
`; @@ -4128,7 +4124,7 @@ ${flaskOpen ? `监控位` : ""} ${flaskOpen ? `复盘` : ""} ${flaskOpen && (row.capabilities || []).includes("options") ? `期权` : ""} - ${intraday ? "" : ``} +
`; if (!row.http_ok || ag.ok === false) { @@ -4519,7 +4515,7 @@ : ""; const intraday = isIntradayDisciplineRow(row); const fcHeadBadge = intraday && !isOkxOptionsCard ? forceCloseHeadBadgeHtml(row.force_close) : ""; - const showCloseAll = !intraday && !isOkxOptionsCard; + const showCloseAll = !isOkxOptionsCard; const layoutCls = opts.okxSplit ? " card-monitor-okx-split" : opts.splitSide || okxPart diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index d631f0a..c20b568 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -1767,6 +1767,6 @@ - +