问题描述
解决方案
这是一个非常实用的风控需求。在 QMT 中实现基于资金曲线回撤的自动清仓(止损),核心逻辑如下:
- 获取当前总资产:通过
get_trade_detail_data获取账户的动态权益(总资产)。 - 记录权益高点(High Water Mark):在策略运行过程中,不断更新并记录历史最高资产净值。
- 计算回撤:$回撤幅度 = (最高资产 - 当前资产) / 最高资产$。
- 触发清仓:当回撤幅度 $\ge 20%$ 时,遍历当前持仓并发出市价全平指令,同时停止策略的开仓逻辑。
以下是完整的策略代码实现。
策略代码
# -*- coding: gbk -*-
import time
def init(ContextInfo):
# ================= 配置区域 =================
# 请在此处填写您的资金账号
ContextInfo.accID = '6000000000'
# 账号类型:'STOCK'股票, 'FUTURE'期货
ContextInfo.accountType = 'STOCK'
# 止损阈值 (20%)
ContextInfo.stop_loss_threshold = 0.20
# ===========================================
# 绑定账号,用于接收回报和下单
ContextInfo.set_account(ContextInfo.accID)
# 初始化全局变量
ContextInfo.max_capital = 0.0 # 记录历史最高权益
ContextInfo.is_triggered = False # 标记是否已经触发了清仓
print("策略初始化完成,开始监控资金曲线...")
def handlebar(ContextInfo):
# 如果已经触发过清仓,则不再执行任何逻辑(防止清仓后又开仓)
if ContextInfo.is_triggered:
return
# 1. 获取账户资金信息
# get_trade_detail_data 返回的是一个列表,通常取第一个对象
account_obj_list = get_trade_detail_data(ContextInfo.accID, ContextInfo.accountType, 'ACCOUNT')
if not account_obj_list:
print("未获取到账户信息,请检查账号配置或连接状态")
return
account_info = account_obj_list[0]
# m_dBalance 代表总资产(动态权益)
current_capital = account_info.m_dBalance
# 2. 更新历史最高权益 (High Water Mark)
if ContextInfo.max_capital == 0:
ContextInfo.max_capital = current_capital
else:
if current_capital > ContextInfo.max_capital:
ContextInfo.max_capital = current_capital
# 3. 计算当前回撤
if ContextInfo.max_capital > 0:
drawdown = (ContextInfo.max_capital - current_capital) / ContextInfo.max_capital
else:
drawdown = 0.0
# 打印资金监控日志 (可选,避免日志过多可注释掉)
# print(f"当前资产: {current_capital:.2f}, 历史最高: {ContextInfo.max_capital:.2f}, 当前回撤: {drawdown*100:.2f}%")
# 4. 判断是否达到止损阈值
if drawdown >= ContextInfo.stop_loss_threshold:
msg = f"【警告】当前回撤 {drawdown*100:.2f}% 已达到阈值 {ContextInfo.stop_loss_threshold*100}%,触发清仓止损!"
print(msg)
ContextInfo.paint('回撤报警', 1, -1, 0, 'red', 'noaxis') # 在界面上画图提示
# 执行清仓操作
clear_all_positions(ContextInfo)
# 标记为已触发,停止后续逻辑
ContextInfo.is_triggered = True
def clear_all_positions(ContextInfo):
"""
清仓函数:遍历所有持仓并市价卖出
"""
# 获取当前持仓列表
positions = get_trade_detail_data(ContextInfo.accID, ContextInfo.accountType, 'POSITION')
if not positions:
print("当前无持仓,无需操作。")
return
print(f"开始执行清仓,共 {len(positions)} 只标的...")
for pos in positions:
stock_code = pos.m_strInstrumentID + "." + pos.m_strExchangeID # 拼接代码,如 000001.SZ
volume = pos.m_nVolume # 持仓数量
# 过滤掉数量为0的持仓(有时会有残余记录)
if volume <= 0:
continue
print(f"正在平仓: {stock_code}, 数量: {volume}")
# 下单参数:
# 24: 卖出
# 1101: 单股单账号普通交易
# ContextInfo.accID: 账号
# stock_code: 代码
# 14: 对手价 (为了快速成交,也可以用 5:最新价 或 44:对手方最优)
# -1: 价格 (市价/对手价模式下无效,填-1即可)
# volume: 数量
passorder(24, 1101, ContextInfo.accID, stock_code, 14, -1, volume, ContextInfo)
print("清仓指令发送完毕。")
代码逻辑详解
-
init函数:- 设置了
ContextInfo.accID,请务必修改为您实际的资金账号。 - 初始化了
max_capital(最高权益)和is_triggered(触发标记)。is_triggered的作用是防止清仓后,策略的其他逻辑(如果有)又去买入股票,或者重复发送清仓指令。
- 设置了
-
handlebar函数:- 这是 QMT 的核心驱动函数,行情更新时会调用。
- 获取资金:使用
get_trade_detail_data(..., 'ACCOUNT')获取账户对象,其中m_dBalance属性即为当前的总资产(现金+持仓市值)。 - 计算回撤:逻辑是
(历史最高 - 当前) / 历史最高。 - 触发风控:一旦回撤比例
>= 0.20,调用clear_all_positions函数。
-
clear_all_positions函数:- 使用
get_trade_detail_data(..., 'POSITION')获取当前所有持仓。 - 遍历持仓列表,使用
passorder函数发送卖出指令。 - 下单价格类型:代码中使用了
14(对手价),这是为了在止损时能尽快成交。如果是在极速变动的行情中,也可以考虑使用市价委托(需根据具体券商支持情况调整prType参数)。
- 使用
注意事项
- 实盘/模拟盘模式:此代码依赖
get_trade_detail_data,因此必须在实盘或模拟交易模式下运行,不能在纯回测模式下直接使用此逻辑(回测模式下资金获取方式不同)。 - 数据频率:
handlebar的运行频率取决于您订阅行情的频率。建议在主图上运行,或者订阅全推行情,以确保资金监控的实时性。 - 手动干预:代码触发清仓后会设置
is_triggered = True停止运行。如果您后续入金了或者想重新开启策略,需要手动停止并重新运行策略以重置该变量。
Q&A
Q: 如何在回测中实现这个功能?
A: 回测中不需要使用 get_trade_detail_data。在回测模式下,您可以使用 ContextInfo.capital 获取初始资金,结合 ContextInfo.get_net_value(ContextInfo.barpos) 获取当前净值来计算总资产和回撤。
Q: 为什么使用 passorder 而不是 order_target_value?
A: passorder 是 QMT 最底层的下单接口,对于清仓这种操作,它能更精确地控制下单类型(如使用对手价或市价)和指定具体的资金账号,在实盘风控场景下更可靠。
Q: 清仓后如何自动停止策略?
A: 代码中通过 if ContextInfo.is_triggered: return 实现了逻辑上的停止。如果您希望彻底停止 Python 引擎,可以在清仓后调用 ContextInfo.stop()(注意:这可能会导致整个策略线程停止)。