feat: KEY_AUTO_ORDER_ENABLED 三所统一开关,联动关键位自动单与开仓类型

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-05 23:39:06 +08:00
parent f688a504e1
commit 21894334f8
13 changed files with 457 additions and 29 deletions
+16
View File
@@ -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 共用)
# =============================================================================
+31 -7
View File
@@ -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
@@ -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` 一致。
+16
View File
@@ -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 共用)
# =============================================================================
+31 -7
View File
@@ -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
@@ -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` 一致。
+16
View File
@@ -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
+31 -7
View File
@@ -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
@@ -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` 一致。
+19 -3
View File
@@ -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`
+164
View File
@@ -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",
)
@@ -145,15 +145,17 @@
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
{{ trade_policy_symbol('symbol', 'key-symbol') }}
<select name="type" id="key-type-select" required>
{% if position_sizing_mode != 'full_margin' %}
{% if key_auto_order_enabled|default(false) and position_sizing_mode != 'full_margin' %}
<option value="箱体突破">箱体突破</option>
<option value="收敛突破">收敛突破</option>
<option value="斐波回调0.618">斐波回调0.618</option>
<option value="斐波回调0.786">斐波回调0.786</option>
<option value="假突破">假突破(BTC/ETH</option>
{% endif %}
{% if key_auto_order_enabled|default(false) %}
<option value="回调触价开仓">回调触价开仓</option>
<option value="突破触价开仓">突破触价开仓</option>
{% endif %}
<option value="关键支撑阻力">关键支撑阻力</option>
</select>
{{ trade_policy_direction('direction', 'key-direction') }}
+75
View File
@@ -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()