止盈止损策略¶
策略概述¶
miniQMT 采用动态止盈止损策略,包含两个阶段:
- 首次止盈:盈利达到阈值后,卖出部分持仓(60%)
- 动态止盈:根据持仓期间最高价动态调整止盈位
首次止盈¶
触发条件:盈利比例 >= INITIAL_TAKE_PROFIT_RATIO(6%)
执行动作:卖出 INITIAL_TAKE_PROFIT_RATIO_PERCENTAGE(60%)持仓
回撤触发:达到首次止盈阈值后,从高点回落 INITIAL_TAKE_PROFIT_PULLBACK_RATIO(0.5%)时执行卖出
标记持久化
实盘模式下,profit_triggered 只在 take_profit_half 成交回报确认后写入内存与 SQLite;模拟模式仍在模拟成交成功后立即标记。这样可以避免委托已报但未成交时,系统误以为已经完成首次止盈并提前进入动态止盈阶段。
在途委托防重
首次止盈、动态止盈和止损信号在入队与执行前都会检查同一股票是否已有本地跟踪委托或 QMT 活跃委托。已有在途卖单时,本轮信号会被阻断,等待成交回报、撤单或超时重挂结果,避免重复卖出。
动态止盈¶
首次止盈后,系统持续跟踪持仓期间的最高价,按档位动态调整止盈位。
档位表:
| 最高浮盈比例 | 止盈位系数 | 示例(成本 10 元) |
|---|---|---|
| 5% | × 96% | 最高 10.50 → 止盈 10.08 |
| 10% | × 93% | 最高 11.00 → 止盈 10.23 |
| 15% | × 90% | 最高 11.50 → 止盈 10.35 |
| 20% | × 87% | 最高 12.00 → 止盈 10.44 |
| 30% | × 85% | 最高 13.00 → 止盈 11.05 |
| 40% | × 83% | 最高 14.00 → 止盈 11.62 |
| 50% | × 80% | 最高 15.00 → 止盈 12.00 |
工作流程:
每 3 秒持仓监控循环:
1. 更新 current_price → 如果 > highest_price,更新 highest_price
2. 计算当前浮盈比例 = (highest_price - cost_price) / cost_price
3. 查找对应档位的止盈位系数
4. 计算止盈价格 = highest_price × 系数
5. 如果 current_price < 止盈价格 → 触发卖出信号
信号检测与执行的开关门控¶
止盈止损遵循"检测与执行分离"设计,但信号入队本身也受开关门控,避免关闭自动交易时仍反复检测刷屏:
| 开关 | 作用 |
|---|---|
ENABLE_AUTO_OPERATION |
全局自动操作总开关,关闭时所有自动策略不产生新交易动作 |
ENABLE_DYNAMIC_STOP_PROFIT |
动态止盈止损检测开关(全局) |
ENABLE_AUTO_TRADING |
动态止盈止损执行开关(全局) |
positions.stop_profit_enabled |
个股级动态止盈止损开关(默认开启) |
持仓监控线程仅在 ENABLE_DYNAMIC_STOP_PROFIT 且 ENABLE_AUTO_TRADING 同时开启,且该股 stop_profit_enabled 为真时,才检测动态止盈止损信号并写入 latest_signals 队列(_detect_and_enqueue_dynamic_signal)。
为何检测也要门控
若仅按总开关检测、按执行开关执行,则当"允许自动止盈"(ENABLE_AUTO_TRADING)关闭而持仓持续满足止盈条件时,会形成"监控检测 → 策略因自动交易关闭而清除 → 监控再检测"的每 3 秒死循环,日志刷屏(曾出现单账户 take_profit_full 一天刷屏近 2 万行)。因此关闭执行开关时直接跳过检测,并清理残留动态信号(保留 grid_ 网格信号)。网格交易走独立分支(ENABLE_GRID_TRADING),不受此门控影响。
个股级开关¶
全局开关是总闸,个股开关是其下的细粒度闸门,二者是 AND 关系:
- 全局关 → 全部股票都不检测
- 全局开 + 个股关 → 仅该股跳过检测,并清理其残留动态信号(不影响网格信号,也不影响其他股票)
- 新增列默认值为
1(开启),旧持仓行为完全不变
Web 端操作:web1.0「当前持仓列表」末列「自动止盈」提供拨动开关,可随时暂停/恢复单只股票的动态止盈止损。开关状态持久化到 SQLite,重启后自动恢复。
对应接口:POST /api/holdings/stop_profit(参数 stock_code、enabled)。
网关模式:xtquant_manager 的独立止盈监控(stop_profit.py)同样遵守该开关——每轮检测前从账号 data_<账号>/trading.db 读取一次开关表,被关闭的股票直接跳过。读取失败或旧库缺列时一律按"开启"处理,保证向后兼容。
信号保活与时效兜底 [v3.8.6]¶
latest_signals 是覆盖式队列(每股仅存一个信号),监控线程每 3 秒检测一次,而策略线程单只股票的实际消费周期约 10 + 持仓数 + 股票池数 秒。首次止盈属于「跨过即触发」的瞬时信号,这个频率差会导致:
价格冲到 +6% 入队 → 3 秒后回踩到 +5.9%,检测返回
None→ 信号被删除 → 策略线程再来取时队列已空,该卖 60% 的单子整个消失且不会补触发,直到价格再次上穿。
止损因价格持续跌破会每轮重触发,风险较低;风险最高的是首次止盈和动态止盈位下移这类瞬时信号。
| 机制 | 配置 | 作用 |
|---|---|---|
| 信号保活 | ENABLE_DYNAMIC_SIGNAL_KEEPALIVE / DYNAMIC_SIGNAL_KEEPALIVE_SECONDS |
已入队未消费的动态信号在窗口内不因「本轮无信号」被删除 |
| 时效兜底 | DYNAMIC_SIGNAL_MAX_AGE_SECONDS |
执行前检查信号年龄,超龄拒绝并返回 signal_expired |
两者必须成对使用
信号执行时用的是信号生成时的 current_price 快照,而 validate_trading_signal() 对止盈类信号没有价格漂移防护。只做保活不加年龄上限,等于把「丢单」换成「以过旧的价格下单」——后者更危险。时效兜底即为此设置,设计参照网格侧已验证的 _validate_grid_signal_before_execute。
grid_ 前缀信号走独立链路,不受保活影响,也不被此处的时效检查重复拦截。
止损¶
触发条件:盈利比例 <= STOP_LOSS_RATIO(-7.5%)
执行动作:全部卖出
止损与止盈独立判断,优先级更高。止损信号一经检测立即执行,不受 profit_triggered 标记影响。
委托超时与重挂¶
动态止盈止损卖出委托由 pending_orders 跟踪。超过配置阈值仍未成交时,系统会先撤销旧委托;若 PENDING_ORDER_AUTO_REORDER=True,再按 PENDING_ORDER_REORDER_PRICE_MODE 重新挂单。
best 对手价模式下,卖单优先使用买三价;如果买三价为 0 或缺失,会按买一价、最新价、收盘价、原信号价逐级降级。降级仍失败时放弃自动重挂并写入错误日志,等待人工处理。
关键数据库字段¶
| 字段 | 类型 | 说明 |
|---|---|---|
profit_triggered |
bool | 是否已触发首次止盈 |
highest_price |
float | 持仓期间最高价 |
stop_loss_price |
float | 止损价格 |
profit_breakout_triggered |
bool | 是否已突破止盈阈值(首次止盈的回撤监控状态) |
breakout_highest_price |
float | 突破止盈阈值后的最高价 |
open_date |
str | 开仓日期 |
stop_profit_enabled |
int | 个股级动态止盈止损开关(1=开启,默认值;0=暂停) |
这些字段保存在 SQLite positions 表中,系统重启后自动恢复。
旧库自动迁移
stop_profit_enabled 由 data_manager._migrate_legacy_schema() 幂等补齐(PRAGMA table_info + ALTER TABLE ... DEFAULT 1),已有持仓行自动填充为 1,无需手工处理数据库。
配置示例¶
# config.py — 激进型配置
STOP_LOSS_RATIO = -0.05 # 止损收紧到 -5%
INITIAL_TAKE_PROFIT_RATIO = 0.04 # 首次止盈提前到 4%
INITIAL_TAKE_PROFIT_RATIO_PERCENTAGE = 0.5 # 首次卖出 50%
# config.py — 保守型配置
STOP_LOSS_RATIO = -0.10 # 止损放宽到 -10%
INITIAL_TAKE_PROFIT_RATIO = 0.08 # 首次止盈延后到 8%
INITIAL_TAKE_PROFIT_RATIO_PERCENTAGE = 0.7 # 首次卖出 70%