From 21894334f857cd42b066069626e7290ff87f0b2b Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 5 Jul 2026 23:39:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20KEY=5FAUTO=5FORDER=5FENABLED=20?= =?UTF-8?q?=E4=B8=89=E6=89=80=E7=BB=9F=E4=B8=80=E5=BC=80=E5=85=B3=EF=BC=8C?= =?UTF-8?q?=E8=81=94=E5=8A=A8=E5=85=B3=E9=94=AE=E4=BD=8D=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8D=95=E4=B8=8E=E5=BC=80=E4=BB=93=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- crypto_monitor_binance/.env.example | 16 ++ crypto_monitor_binance/app.py | 38 +++- crypto_monitor_binance/关键位自动下单说明.md | 21 ++- crypto_monitor_gate/.env.example | 16 ++ crypto_monitor_gate/app.py | 38 +++- crypto_monitor_gate/关键位自动下单说明.md | 19 +- crypto_monitor_okx/.env.example | 16 ++ crypto_monitor_okx/app.py | 38 +++- crypto_monitor_okx/关键位自动下单说明.md | 19 +- docs/position-sizing-mode.md | 22 ++- lib/key_monitor/key_auto_order_lib.py | 164 ++++++++++++++++++ lib/strategy/templates/key_monitor_panel.html | 4 +- tests/test_key_auto_order_lib.py | 75 ++++++++ 13 files changed, 457 insertions(+), 29 deletions(-) create mode 100644 lib/key_monitor/key_auto_order_lib.py create mode 100644 tests/test_key_auto_order_lib.py diff --git a/crypto_monitor_binance/.env.example b/crypto_monitor_binance/.env.example index d6294e7..8c40cd4 100644 --- a/crypto_monitor_binance/.env.example +++ b/crypto_monitor_binance/.env.example @@ -93,6 +93,22 @@ BINANCE_TRIGGER_WORKING_TYPE=CONTRACT_PRICE # 平仓盈亏估算:false=按仓位历史口径(已实现盈亏+手续费,不含资金费);true=含资金费 # BINANCE_PNL_INCLUDE_FUNDING=false +# ============================================================================= +# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2) +# ============================================================================= +# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价) +# +# POSITION_SIZING_MODE=risk(以损定仓) +# false → 不执行任何关键位自动单;支撑/阻力提醒、人工下单、顺势加仓不受影响 +# true → 允许关键位全套自动(含触价) +# +# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换) +# false → 不执行触价自动单 +# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止 +# +# 顺势加仓、趋势回调不受本开关控制;全仓模式下策略自动仍禁止。 +KEY_AUTO_ORDER_ENABLED=false + # ============================================================================= # 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用) # ============================================================================= diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 45912ed..3bcc415 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -176,6 +176,12 @@ from lib.trade.trade_policy_app_lib import ( default_symbol_for_policy, trade_policy_template_context, ) +from lib.key_monitor.key_auto_order_lib import ( + check_monitor_type_add_allowed, + effective_entry_reason_options, + effective_stats_segment_defs, + load_key_auto_order_enabled, +) from lib.key_monitor.key_monitor_full_margin_lib import ( monitor_type_disallowed_in_full_margin, purge_disallowed_key_monitors, @@ -363,6 +369,7 @@ FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0")) AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8")) # 计仓模式:risk=以损定仓(默认);full_margin=合约可用保证金×比例全仓杠杆(仅 env 切换,须无仓) POSITION_SIZING_MODE = load_position_sizing_mode() +KEY_AUTO_ORDER_ENABLED = load_key_auto_order_enabled() TRADE_POLICY = load_trade_policy() WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10")) AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120")) @@ -1842,7 +1849,10 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): return dm, wm, mm segments = [] - for seg_key, seg_title, _meta in STATS_SEGMENT_DEFS: + seg_defs = effective_stats_segment_defs( + STATS_SEGMENT_DEFS, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED + ) + for seg_key, seg_title, _meta in seg_defs: dm, wm, mm = slice_metrics(seg_key) segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm}) @@ -5919,6 +5929,8 @@ def _execute_trigger_entry_cross(conn, row): def check_trigger_entry_key_monitors(): + if not KEY_AUTO_ORDER_ENABLED: + return conn = get_db() placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) rows = conn.execute( @@ -5983,6 +5995,8 @@ def check_trigger_entry_key_monitors(): def check_fib_key_monitors(): + if not KEY_AUTO_ORDER_ENABLED: + return conn = get_db() rows = conn.execute("SELECT * FROM key_monitors").fetchall() for r in rows: @@ -6217,6 +6231,9 @@ def check_key_monitors(): print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}") continue + if not KEY_AUTO_ORDER_ENABLED: + continue + direction = (r["direction"] or "long").lower() if direction == KEY_DIRECTION_WATCH: continue @@ -7239,8 +7256,15 @@ def render_main_page(page="trade", embed_mode=None): breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, price_fmt=format_price_for_symbol, funds_fmt=format_funds_u, - entry_reason_options=list(ENTRY_REASON_OPTIONS), + entry_reason_options=list( + effective_entry_reason_options( + ENTRY_REASON_OPTIONS, + POSITION_SIZING_MODE, + KEY_AUTO_ORDER_ENABLED, + ) + ), entry_reason_other_value=ENTRY_REASON_OTHER, + key_auto_order_enabled=KEY_AUTO_ORDER_ENABLED, journal_chart_tf_choices=JOURNAL_CHART_TF_CHOICES, journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1, journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2, @@ -8108,11 +8132,11 @@ def add_key(): if mt not in allowed_types: flash("监控类型无效") return redirect("/key_monitor") - if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt): - flash( - "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" - "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" - ) + ok_mt, mt_msg = check_monitor_type_add_allowed( + mt, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED + ) + if not ok_mt: + flash(mt_msg) return redirect("/key_monitor") skip_volume_rank = is_false_breakout_key_monitor_type(mt) rank, total = None, None diff --git a/crypto_monitor_binance/关键位自动下单说明.md b/crypto_monitor_binance/关键位自动下单说明.md index 5bddb8c..d69283d 100644 --- a/crypto_monitor_binance/关键位自动下单说明.md +++ b/crypto_monitor_binance/关键位自动下单说明.md @@ -1,7 +1,24 @@ # 关键位监控说明(自动开仓 + 人工盯盘) -**适用:`crypto_monitor_gate`(Gate U 本位永续)** -Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `key_monitor_lib.py`。 +**适用:Gate / Binance / OKX 三所实例(共用 `lib/key_monitor/key_auto_order_lib.py`)** + +## 环境开关 `KEY_AUTO_ORDER_ENABLED`(默认 `false`) + +| 计仓模式 | 开关 | 关键位程序自动单 | +|----------|------|------------------| +| `risk`(以损定仓) | `false` | **全部关闭**(含触价);支撑/阻力微信提醒仍可用 | +| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动(旧行为) | +| `full_margin`(全仓) | `false` | 全部关闭(含触价) | +| `full_margin` | `true` | **仅触价**自动;箱体/斐波等仍禁止 | + +**不受本开关影响:** 人工实盘下单、关键支撑/阻力提醒、**顺势加仓**(`risk` 下)、趋势回调(`risk` 下)。全仓模式下策略自动仍禁止。 + +修改 `.env` 后须 **重启 PM2**。复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项。 + +--- + +**适用:`crypto_monitor_binance`(Binance U 本位)** +Gate / OKX 见各自目录下同名文档;共享逻辑在 `lib/key_monitor/`。 本文档与 `.env`、`check_key_monitors`、`add_key`、`_key_hard_checks`、`_process_key_rs_level_alert` 一致。 diff --git a/crypto_monitor_gate/.env.example b/crypto_monitor_gate/.env.example index 3fca0a4..540cbdd 100644 --- a/crypto_monitor_gate/.env.example +++ b/crypto_monitor_gate/.env.example @@ -93,6 +93,22 @@ GATE_TPSL_LAST_PRICE_GAP_PCT=0.05 # 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 Gate·模拟) # EXCHANGE_DISPLAY_NAME=Gate.io +# ============================================================================= +# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2) +# ============================================================================= +# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价) +# +# POSITION_SIZING_MODE=risk(以损定仓) +# false → 不执行任何关键位自动单;支撑/阻力提醒、人工下单、顺势加仓不受影响 +# true → 允许关键位全套自动(含触价) +# +# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换) +# false → 不执行触价自动单 +# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止 +# +# 顺势加仓、趋势回调不受本开关控制;全仓模式下策略自动仍禁止。 +KEY_AUTO_ORDER_ENABLED=false + # ============================================================================= # 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用) # ============================================================================= diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index b8fe944..9f78ddd 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -175,6 +175,12 @@ from lib.trade.trade_policy_app_lib import ( default_symbol_for_policy, trade_policy_template_context, ) +from lib.key_monitor.key_auto_order_lib import ( + check_monitor_type_add_allowed, + effective_entry_reason_options, + effective_stats_segment_defs, + load_key_auto_order_enabled, +) from lib.key_monitor.key_monitor_full_margin_lib import ( monitor_type_disallowed_in_full_margin, purge_disallowed_key_monitors, @@ -353,6 +359,7 @@ FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0")) # 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账 AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8")) POSITION_SIZING_MODE = load_position_sizing_mode() +KEY_AUTO_ORDER_ENABLED = load_key_auto_order_enabled() TRADE_POLICY = load_trade_policy() WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10")) AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120")) @@ -1839,7 +1846,10 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): return dm, wm, mm segments = [] - for seg_key, seg_title, _meta in STATS_SEGMENT_DEFS: + seg_defs = effective_stats_segment_defs( + STATS_SEGMENT_DEFS, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED + ) + for seg_key, seg_title, _meta in seg_defs: dm, wm, mm = slice_metrics(seg_key) segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm}) @@ -5560,6 +5570,8 @@ def _execute_trigger_entry_cross(conn, row): def check_trigger_entry_key_monitors(): + if not KEY_AUTO_ORDER_ENABLED: + return conn = get_db() placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) rows = conn.execute( @@ -5624,6 +5636,8 @@ def check_trigger_entry_key_monitors(): def check_fib_key_monitors(): + if not KEY_AUTO_ORDER_ENABLED: + return conn = get_db() rows = conn.execute("SELECT * FROM key_monitors").fetchall() for r in rows: @@ -5858,6 +5872,9 @@ def check_key_monitors(): print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}") continue + if not KEY_AUTO_ORDER_ENABLED: + continue + direction = (r["direction"] or "long").lower() if direction == KEY_DIRECTION_WATCH: continue @@ -7018,8 +7035,15 @@ def render_main_page(page="trade", embed_mode=None): funds_fmt=format_usdt, usdt_fmt=format_usdt, signed_usdt_fmt=format_signed_usdt, - entry_reason_options=list(ENTRY_REASON_OPTIONS), + entry_reason_options=list( + effective_entry_reason_options( + ENTRY_REASON_OPTIONS, + POSITION_SIZING_MODE, + KEY_AUTO_ORDER_ENABLED, + ) + ), entry_reason_other_value=ENTRY_REASON_OTHER, + key_auto_order_enabled=KEY_AUTO_ORDER_ENABLED, journal_chart_tf_choices=JOURNAL_CHART_TF_CHOICES, journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1, journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2, @@ -7905,11 +7929,11 @@ def add_key(): if mt not in allowed_types: flash("监控类型无效") return redirect("/key_monitor") - if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt): - flash( - "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" - "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" - ) + ok_mt, mt_msg = check_monitor_type_add_allowed( + mt, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED + ) + if not ok_mt: + flash(mt_msg) return redirect("/key_monitor") skip_volume_rank = is_false_breakout_key_monitor_type(mt) rank, total = None, None diff --git a/crypto_monitor_gate/关键位自动下单说明.md b/crypto_monitor_gate/关键位自动下单说明.md index 5bddb8c..3a600ff 100644 --- a/crypto_monitor_gate/关键位自动下单说明.md +++ b/crypto_monitor_gate/关键位自动下单说明.md @@ -1,7 +1,24 @@ # 关键位监控说明(自动开仓 + 人工盯盘) +**适用:Gate / Binance / OKX 三所实例(共用 `lib/key_monitor/key_auto_order_lib.py`)** + +## 环境开关 `KEY_AUTO_ORDER_ENABLED`(默认 `false`) + +| 计仓模式 | 开关 | 关键位程序自动单 | +|----------|------|------------------| +| `risk`(以损定仓) | `false` | **全部关闭**(含触价);支撑/阻力微信提醒仍可用 | +| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动(旧行为) | +| `full_margin`(全仓) | `false` | 全部关闭(含触价) | +| `full_margin` | `true` | **仅触价**自动;箱体/斐波等仍禁止 | + +**不受本开关影响:** 人工实盘下单、关键支撑/阻力提醒、**顺势加仓**(`risk` 下)、趋势回调(`risk` 下)。全仓模式下策略自动仍禁止。 + +修改 `.env` 后须 **重启 PM2**。复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项。 + +--- + **适用:`crypto_monitor_gate`(Gate U 本位永续)** -Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `key_monitor_lib.py`。 +Binance / OKX 见各自目录下同名文档;共享逻辑在 `lib/key_monitor/`。 本文档与 `.env`、`check_key_monitors`、`add_key`、`_key_hard_checks`、`_process_key_rs_level_alert` 一致。 diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 27e9009..7e31a88 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -89,6 +89,22 @@ OKX_POSITION_INST_TYPE=SWAP # 单次拉取历史仓位条数上限(OKX 每页最多 100,程序会分页) # EXCHANGE_POSITION_HISTORY_LIMIT=200 +# ============================================================================= +# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2) +# ============================================================================= +# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价) +# +# POSITION_SIZING_MODE=risk(以损定仓) +# false → 不执行任何关键位自动单;支撑/阻力提醒、人工下单、顺势加仓不受影响 +# true → 允许关键位全套自动(含触价) +# +# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换) +# false → 不执行触价自动单 +# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止 +# +# 顺势加仓、趋势回调不受本开关控制;全仓模式下策略自动仍禁止。 +KEY_AUTO_ORDER_ENABLED=false + # 关键位监控:5m收线突破过滤参数 KLINE_TIMEFRAME=5m KEY_BREAKOUT_LIMIT_PCT=1.5 diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index ebc88b9..0efab2b 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -174,6 +174,12 @@ from lib.trade.trade_policy_app_lib import ( default_symbol_for_policy, trade_policy_template_context, ) +from lib.key_monitor.key_auto_order_lib import ( + check_monitor_type_add_allowed, + effective_entry_reason_options, + effective_stats_segment_defs, + load_key_auto_order_enabled, +) from lib.key_monitor.key_monitor_full_margin_lib import ( monitor_type_disallowed_in_full_margin, purge_disallowed_key_monitors, @@ -325,6 +331,7 @@ FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0")) # 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日(与 OKX 日界一致便于对账) AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8")) POSITION_SIZING_MODE = load_position_sizing_mode() +KEY_AUTO_ORDER_ENABLED = load_key_auto_order_enabled() TRADE_POLICY = load_trade_policy() WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10")) AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120")) @@ -1815,7 +1822,10 @@ def compute_stats_bundle(conn, trading_day, now_dt=None): return dm, wm, mm segments = [] - for seg_key, seg_title, _meta in STATS_SEGMENT_DEFS: + seg_defs = effective_stats_segment_defs( + STATS_SEGMENT_DEFS, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED + ) + for seg_key, seg_title, _meta in seg_defs: dm, wm, mm = slice_metrics(seg_key) segments.append({"key": seg_key, "title": seg_title, "day": dm, "week": wm, "month": mm}) @@ -5135,6 +5145,8 @@ def _execute_trigger_entry_cross(conn, row): def check_trigger_entry_key_monitors(): + if not KEY_AUTO_ORDER_ENABLED: + return conn = get_db() placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) rows = conn.execute( @@ -5199,6 +5211,8 @@ def check_trigger_entry_key_monitors(): def check_fib_key_monitors(): + if not KEY_AUTO_ORDER_ENABLED: + return conn = get_db() rows = conn.execute("SELECT * FROM key_monitors").fetchall() for r in rows: @@ -5672,6 +5686,9 @@ def check_key_monitors(): print(f"[key_rs_level_alert] {sym} id={r['id']}: {e}") continue + if not KEY_AUTO_ORDER_ENABLED: + continue + direction = (r["direction"] or "long").lower() if direction == KEY_DIRECTION_WATCH: continue @@ -6576,8 +6593,15 @@ def render_main_page(page="trade", embed_mode=None): breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER, breakeven_offset_pct=BREAKEVEN_OFFSET_PCT, price_fmt=format_price_for_symbol, - entry_reason_options=list(ENTRY_REASON_OPTIONS), + entry_reason_options=list( + effective_entry_reason_options( + ENTRY_REASON_OPTIONS, + POSITION_SIZING_MODE, + KEY_AUTO_ORDER_ENABLED, + ) + ), entry_reason_other_value=ENTRY_REASON_OTHER, + key_auto_order_enabled=KEY_AUTO_ORDER_ENABLED, journal_chart_tf_choices=JOURNAL_CHART_TF_CHOICES, journal_chart_default_tf1=JOURNAL_CHART_DEFAULT_TF1, journal_chart_default_tf2=JOURNAL_CHART_DEFAULT_TF2, @@ -7512,11 +7536,11 @@ def add_key(): if mt not in allowed_types: flash("监控类型无效") return redirect("/key_monitor") - if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt): - flash( - "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" - "可使用「回调/突破触价开仓」或阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)。" - ) + ok_mt, mt_msg = check_monitor_type_add_allowed( + mt, POSITION_SIZING_MODE, KEY_AUTO_ORDER_ENABLED + ) + if not ok_mt: + flash(mt_msg) return redirect("/key_monitor") skip_volume_rank = is_false_breakout_key_monitor_type(mt) rank, total = None, None diff --git a/crypto_monitor_okx/关键位自动下单说明.md b/crypto_monitor_okx/关键位自动下单说明.md index 5bddb8c..3a600ff 100644 --- a/crypto_monitor_okx/关键位自动下单说明.md +++ b/crypto_monitor_okx/关键位自动下单说明.md @@ -1,7 +1,24 @@ # 关键位监控说明(自动开仓 + 人工盯盘) +**适用:Gate / Binance / OKX 三所实例(共用 `lib/key_monitor/key_auto_order_lib.py`)** + +## 环境开关 `KEY_AUTO_ORDER_ENABLED`(默认 `false`) + +| 计仓模式 | 开关 | 关键位程序自动单 | +|----------|------|------------------| +| `risk`(以损定仓) | `false` | **全部关闭**(含触价);支撑/阻力微信提醒仍可用 | +| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动(旧行为) | +| `full_margin`(全仓) | `false` | 全部关闭(含触价) | +| `full_margin` | `true` | **仅触价**自动;箱体/斐波等仍禁止 | + +**不受本开关影响:** 人工实盘下单、关键支撑/阻力提醒、**顺势加仓**(`risk` 下)、趋势回调(`risk` 下)。全仓模式下策略自动仍禁止。 + +修改 `.env` 后须 **重启 PM2**。复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项。 + +--- + **适用:`crypto_monitor_gate`(Gate U 本位永续)** -Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `key_monitor_lib.py`。 +Binance / OKX 见各自目录下同名文档;共享逻辑在 `lib/key_monitor/`。 本文档与 `.env`、`check_key_monitors`、`add_key`、`_key_hard_checks`、`_process_key_rs_level_alert` 一致。 diff --git a/docs/position-sizing-mode.md b/docs/position-sizing-mode.md index 17a2d01..0fb688f 100644 --- a/docs/position-sizing-mode.md +++ b/docs/position-sizing-mode.md @@ -9,6 +9,9 @@ # full_margin = 全仓杠杆(合约可用保证金 × 比例) POSITION_SIZING_MODE=risk FULL_MARGIN_BUFFER_RATIO=0.98 + +# 关键位程序自动单(默认 false,详见各所 关键位自动下单说明.md) +KEY_AUTO_ORDER_ENABLED=false ``` 切换为全仓杠杆前:**交易所须无持仓**(`MAX_ACTIVE_POSITIONS` 默认 1,全仓模式会强制单仓)。 @@ -17,11 +20,13 @@ FULL_MARGIN_BUFFER_RATIO=0.98 | 模式 | 保证金计算 | 杠杆 | 允许入口 | |------|------------|------|----------| -| `risk` | `RISK_PERCENT` × 交易资金,按止损距离反推 | 表单可选 / 同步交易所 | 实盘人工、关键位自动、趋势回调、顺势加仓 | -| `full_margin` | **合约账户可用 USDT × `FULL_MARGIN_BUFFER_RATIO`**(保留 2 位小数) | BTC/ETH **10x**,其它 **5x**(与 `BTC_LEVERAGE`/`ALT_LEVERAGE` 一致) | **实盘人工下单**、**关键位触价开仓**;阻力/支撑仅提醒 | +| `risk` | `RISK_PERCENT` × 交易资金,按止损距离反推 | 表单可选 / 同步交易所 | 实盘人工、关键位自动(须 `KEY_AUTO_ORDER_ENABLED=true`)、趋势回调、顺势加仓 | +| `full_margin` | **合约账户可用 USDT × `FULL_MARGIN_BUFFER_RATIO`**(保留 2 位小数) | BTC/ETH **10x**,其它 **5x**(与 `BTC_LEVERAGE`/`ALT_LEVERAGE` 一致) | **实盘人工下单**、**关键位触价**(须 `KEY_AUTO_ORDER_ENABLED=true`);阻力/支撑仅提醒 | 全仓模式下: +- **`KEY_AUTO_ORDER_ENABLED=false`(默认)** 时,触价程序自动单也不执行。 +- **`KEY_AUTO_ORDER_ENABLED=true`** 时,仅触价可程序自动开仓;箱体/斐波等仍禁止。 - 仍校验 **计划盈亏比**(实盘用 `MANUAL_MIN_PLANNED_RR`;触价开仓用 `KEY_AUTO_MIN_PLANNED_RR`)。 - 下单张数由 `prepare_order_amount` + 交易所 `amount_to_precision` 决定。 - `order_monitors.initial_stop_loss` 仍记录**开仓时**止损快照;交易记录复盘以该快照为准。 @@ -32,7 +37,18 @@ FULL_MARGIN_BUFFER_RATIO=0.98 - 关键位:箱体突破、收敛突破、斐波、假突破(添加时拒绝;已存在则启动时撤销)。 - 趋势回调、顺势加仓(策略入口返回明确错误)。 -**允许:** 关键位 **回调触价开仓** / **突破触价开仓**(程序盯价、触达/穿越计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。 +**允许(须 `KEY_AUTO_ORDER_ENABLED=true`):** 关键位 **回调触价开仓** / **突破触价开仓**(程序盯价、触达/穿越计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。 + +## `KEY_AUTO_ORDER_ENABLED`(三所统一,默认 `false`) + +| 计仓 | 开关 | 效果 | +|------|------|------| +| `risk` | `false` | 关闭全部关键位程序自动单(含触价);顺势加仓不受影响 | +| `risk` | `true` | 关键位全套自动(旧行为) | +| `full_margin` | `false` | 关闭触价自动 | +| `full_margin` | `true` | 仅触价自动 | + +详见各实例目录 `关键位自动下单说明.md`。 ## 用脚本更新三所 `.env` diff --git a/lib/key_monitor/key_auto_order_lib.py b/lib/key_monitor/key_auto_order_lib.py new file mode 100644 index 0000000..3adfe98 --- /dev/null +++ b/lib/key_monitor/key_auto_order_lib.py @@ -0,0 +1,164 @@ +"""关键位程序自动下单开关(三所共用,与 POSITION_SIZING_MODE 联动)。""" +from __future__ import annotations + +import os +from typing import Any, Optional, Sequence, Tuple + +from lib.key_monitor.fib_key_monitor_lib import is_fib_key_monitor_type +from lib.key_monitor.false_breakout_key_monitor_lib import is_false_breakout_key_monitor_type +from lib.key_monitor.key_monitor_full_margin_lib import monitor_type_disallowed_in_full_margin +from lib.key_monitor.key_monitor_lib import KEY_MONITOR_AUTO_TYPES, KEY_MONITOR_RS_TYPES +from lib.key_monitor.trigger_entry_key_monitor_lib import is_trigger_entry_key_monitor_type +from lib.trade.position_sizing_lib import is_full_margin_mode + +KEY_ENTRY_REASON_OPTIONS: Tuple[str, ...] = ( + "关键位箱体突破", + "关键位收敛突破", + "关键位斐波0.618", + "关键位斐波0.786", + "关键位假突破", + "关键位回调触价开仓", + "关键位突破触价开仓", +) + +KEY_ENTRY_REASON_TRIGGER_OPTIONS: frozenset[str] = frozenset( + { + "关键位回调触价开仓", + "关键位突破触价开仓", + } +) + +KEY_STATS_SEGMENT_KEYS: frozenset[str] = frozenset( + { + "key_box", + "key_conv", + "key_fib618", + "key_fib786", + "key_false_breakout", + "key_trigger", + } +) + +KEY_STATS_TRIGGER_ONLY: frozenset[str] = frozenset({"key_trigger"}) + +TREND_MANUAL_ENTRY_REASON_COUNT = 5 + + +def _env_bool(raw: Optional[str], default: bool = False) -> bool: + if raw is None: + return default + return (raw or "").strip().lower() in ("1", "true", "yes", "on") + + +def load_key_auto_order_enabled(env: Optional[dict] = None) -> bool: + e = env if env is not None else os.environ + return _env_bool(e.get("KEY_AUTO_ORDER_ENABLED"), default=False) + + +def is_key_level_entry_reason(reason: str) -> bool: + return (reason or "").strip() in KEY_ENTRY_REASON_OPTIONS + + +def visible_key_entry_reasons(sizing_mode: str, key_auto_enabled: bool) -> Tuple[str, ...]: + if not key_auto_enabled: + return () + if is_full_margin_mode(sizing_mode): + return tuple(x for x in KEY_ENTRY_REASON_OPTIONS if x in KEY_ENTRY_REASON_TRIGGER_OPTIONS) + return KEY_ENTRY_REASON_OPTIONS + + +def effective_entry_reason_options( + all_options: Sequence[str], + sizing_mode: str, + key_auto_enabled: bool, + *, + trend_manual_count: int = TREND_MANUAL_ENTRY_REASON_COUNT, +) -> Tuple[str, ...]: + """复盘/表单下拉:按开关与计仓模式裁剪关键位开仓类型。""" + opts = list(all_options) + if len(opts) <= trend_manual_count: + return tuple(opts) + key_visible = set(visible_key_entry_reasons(sizing_mode, key_auto_enabled)) + out: list[str] = [] + for i, item in enumerate(opts): + if i < trend_manual_count: + out.append(item) + elif is_key_level_entry_reason(item): + if item in key_visible: + out.append(item) + else: + out.append(item) + return tuple(out) + + +def effective_stats_segment_defs( + segment_defs: Sequence[Tuple[str, str, Any]], + sizing_mode: str, + key_auto_enabled: bool, +) -> Tuple[Tuple[str, str, Any], ...]: + if not key_auto_enabled: + hidden = KEY_STATS_SEGMENT_KEYS + elif is_full_margin_mode(sizing_mode): + hidden = KEY_STATS_SEGMENT_KEYS - KEY_STATS_TRIGGER_ONLY + else: + hidden = frozenset() + return tuple(x for x in segment_defs if x[0] not in hidden) + + +def is_key_auto_monitor_type(monitor_type: str) -> bool: + mt = (monitor_type or "").strip() + if mt in KEY_MONITOR_AUTO_TYPES: + return True + if is_fib_key_monitor_type(mt): + return True + if is_false_breakout_key_monitor_type(mt): + return True + if is_trigger_entry_key_monitor_type(mt): + return True + return False + + +def is_rs_key_monitor_type(monitor_type: str) -> bool: + return (monitor_type or "").strip() in KEY_MONITOR_RS_TYPES + + +def check_monitor_type_add_allowed( + monitor_type: str, + sizing_mode: str, + key_auto_enabled: bool, +) -> Tuple[bool, str]: + mt = (monitor_type or "").strip() + if is_rs_key_monitor_type(mt): + return True, "" + if not key_auto_enabled: + return False, ( + "已关闭关键位程序自动单(KEY_AUTO_ORDER_ENABLED=false);" + "仅可添加「关键支撑阻力」(微信提醒,不下单)。" + ) + if is_full_margin_mode(sizing_mode): + if monitor_type_disallowed_in_full_margin(mt): + return False, ( + "全仓杠杆模式下不可添加箱体/收敛突破、斐波或假突破监控;" + "可使用「回调/突破触价开仓」或关键支撑阻力(仅提醒)。" + ) + if not is_key_auto_monitor_type(mt) and not is_rs_key_monitor_type(mt): + return False, "监控类型无效" + return True, "" + + +def key_auto_order_env_comment_lines() -> Tuple[str, ...]: + return ( + "# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2)", + "# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价)", + "#", + "# POSITION_SIZING_MODE=risk(以损定仓)", + "# false → 不执行任何关键位自动单;支撑/阻力提醒、人工下单、顺势加仓不受影响", + "# true → 允许关键位全套自动(含触价)", + "#", + "# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换)", + "# false → 不执行触价自动单", + "# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止", + "#", + "# 顺势加仓、趋势回调不受本开关控制;全仓模式下策略自动仍禁止。", + "KEY_AUTO_ORDER_ENABLED=false", + ) diff --git a/lib/strategy/templates/key_monitor_panel.html b/lib/strategy/templates/key_monitor_panel.html index c6d27b6..772459f 100644 --- a/lib/strategy/templates/key_monitor_panel.html +++ b/lib/strategy/templates/key_monitor_panel.html @@ -145,15 +145,17 @@ {% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %} {{ trade_policy_symbol('symbol', 'key-symbol') }} {{ trade_policy_direction('direction', 'key-direction') }} diff --git a/tests/test_key_auto_order_lib.py b/tests/test_key_auto_order_lib.py new file mode 100644 index 0000000..cb391dd --- /dev/null +++ b/tests/test_key_auto_order_lib.py @@ -0,0 +1,75 @@ +"""key_auto_order_lib 单元测试。""" +import unittest + +from lib.key_monitor.key_auto_order_lib import ( + check_monitor_type_add_allowed, + effective_entry_reason_options, + effective_stats_segment_defs, + load_key_auto_order_enabled, +) +from lib.trade.position_sizing_lib import MODE_FULL_MARGIN, MODE_RISK + +FULL_OPTS = ( + "趋势A", + "趋势B", + "趋势C", + "趋势D", + "趋势E", + "关键位箱体突破", + "关键位收敛突破", + "关键位斐波0.618", + "关键位斐波0.786", + "关键位假突破", + "关键位回调触价开仓", + "关键位突破触价开仓", + "趋势回调", + "顺势加仓", +) + +STATS_DEFS = ( + ("all", "全部", {}), + ("key_box", "箱体", {}), + ("key_trigger", "触价", {}), +) + + +class KeyAutoOrderLibTest(unittest.TestCase): + def test_load_default_false(self): + self.assertFalse(load_key_auto_order_enabled({"KEY_AUTO_ORDER_ENABLED": "false"})) + self.assertFalse(load_key_auto_order_enabled({})) + self.assertTrue(load_key_auto_order_enabled({"KEY_AUTO_ORDER_ENABLED": "true"})) + + def test_entry_reason_off(self): + out = effective_entry_reason_options(FULL_OPTS, MODE_RISK, False) + self.assertNotIn("关键位箱体突破", out) + self.assertNotIn("关键位回调触价开仓", out) + self.assertIn("顺势加仓", out) + + def test_entry_reason_risk_on(self): + out = effective_entry_reason_options(FULL_OPTS, MODE_RISK, True) + self.assertIn("关键位箱体突破", out) + self.assertIn("关键位回调触价开仓", out) + + def test_entry_reason_full_margin_on(self): + out = effective_entry_reason_options(FULL_OPTS, MODE_FULL_MARGIN, True) + self.assertNotIn("关键位箱体突破", out) + self.assertIn("关键位回调触价开仓", out) + + def test_stats_segments_off(self): + segs = effective_stats_segment_defs(STATS_DEFS, MODE_RISK, False) + keys = {x[0] for x in segs} + self.assertIn("all", keys) + self.assertNotIn("key_box", keys) + + def test_add_key_rs_always(self): + ok, _ = check_monitor_type_add_allowed("关键支撑阻力", MODE_RISK, False) + self.assertTrue(ok) + + def test_add_key_trigger_off(self): + ok, msg = check_monitor_type_add_allowed("回调触价开仓", MODE_RISK, False) + self.assertFalse(ok) + self.assertIn("KEY_AUTO_ORDER_ENABLED", msg) + + +if __name__ == "__main__": + unittest.main()