问题描述
解决方案
这是一个关于量化交易策略风控的具体实现问题。
核心逻辑
要实现基于账户最大回撤的止损,需要维护一个“高水位线”(High Water Mark),即策略运行以来的最高账户总资产。
- 获取当前总资产:在每个
handlebar周期(或定时器中),通过get_trade_detail_data获取账户的动态权益(总资产)。 - 更新高水位线:如果当前总资产高于历史记录的最高资产,则更新高水位线。
- 计算回撤:
当前回撤 = 1 - (当前总资产 / 高水位线)。 - 触发止损:如果当前回撤超过设定的阈值(例如 10%),则执行清仓操作,并停止策略开新仓。
代码实现
以下是完整的策略代码示例。该代码适用于 QMT 的实盘或模拟盘模式(Run Mode)。
# -*- coding: gbk -*-
import time
def init(ContextInfo):
# ================= 策略参数设置 =================
# 设置资金账号 (请修改为您实际的资金账号)
ContextInfo.accID = '6000000000'
# 账号类型: 'STOCK'股票, 'FUTURE'期货, 'CREDIT'信用
ContextInfo.accType = 'STOCK'
# 最大回撤止损阈值 (例如 0.10 代表 10%)
ContextInfo.drawdown_limit = 0.10
# ================= 全局变量初始化 =================
# 记录账户历史最高权益 (高水位线)
ContextInfo.max_balance = 0.0
# 标记是否已经触发止损
ContextInfo.is_stop_triggered = False
# 绑定账号,用于接收回报
ContextInfo.set_account(ContextInfo.accID)
print("策略初始化完成,账户回撤止损阈值设置为: {:.2%}".format(ContextInfo.drawdown_limit))
def handlebar(ContextInfo):
# 如果已经触发了止损,不再执行任何逻辑 (或者可以在这里只执行平仓确认逻辑)
if ContextInfo.is_stop_triggered:
return
# 1. 获取账户资金信息
# get_trade_detail_data 返回的是一个列表,包含账户对象
account_list = get_trade_detail_data(ContextInfo.accID, ContextInfo.accType, 'account')
if not account_list:
print("未获取到账户信息,跳过本次检查")
return
# 获取账户对象
account_obj = account_list[0]
# m_dBalance 为总资产(动态权益)
current_balance = account_obj.m_dBalance
# 2. 初始化或更新高水位线 (Max Balance)
if ContextInfo.max_balance == 0:
ContextInfo.max_balance = current_balance
elif current_balance > ContextInfo.max_balance:
ContextInfo.max_balance = current_balance
# 3. 计算当前回撤
# 回撤 = 1 - (当前净值 / 最高净值)
current_drawdown = 0.0
if ContextInfo.max_balance > 0:
current_drawdown = 1 - (current_balance / ContextInfo.max_balance)
# 打印当前状态 (可选,用于调试)
# print(f"当前资产: {current_balance}, 历史最高: {ContextInfo.max_balance}, 当前回撤: {current_drawdown:.2%}")
# 4. 判断是否超过阈值
if current_drawdown >= ContextInfo.drawdown_limit:
print("【警告】当前回撤 {:.2%} 已超过阈值 {:.2%},触发账户级止损!开始清仓...".format(
current_drawdown, ContextInfo.drawdown_limit))
# 执行清仓操作
clean_all_positions(ContextInfo)
# 标记止损已触发,防止重复下单或继续开仓
ContextInfo.is_stop_triggered = True
def clean_all_positions(ContextInfo):
"""
清空所有持仓
"""
# 获取当前持仓列表
position_list = get_trade_detail_data(ContextInfo.accID, ContextInfo.accType, 'position')
if not position_list:
print("当前无持仓,无需平仓。")
return
for pos in position_list:
# 获取持仓数量 (m_nVolume 为当前拥股)
volume = pos.m_nVolume
# 获取证券代码
stock_code = pos.m_strInstrumentID
# 如果持仓量为0,跳过
if volume <= 0:
continue
print(f"正在平仓: {stock_code}, 数量: {volume}")
# 下单平仓
# opType: 24 (股票卖出)
# orderType: 1101 (单股、单账号、普通、股/手方式下单)
# accountID: 资金账号
# orderCode: 股票代码
# prType: 14 (对手价,保证尽快成交) 或者 5 (最新价)
# price: -1 (对手价模式下价格无效,填-1即可)
# volume: 数量
passorder(24, 1101, ContextInfo.accID, stock_code, 14, -1, volume, ContextInfo)
print("清仓指令发送完毕。")
# 必须实现的空函数,防止报错(如果策略中没有用到)
def stop(ContextInfo):
pass
代码详解
-
init函数:- 设置了
ContextInfo.drawdown_limit(回撤阈值)。 - 初始化
ContextInfo.max_balance为 0,用于记录策略启动后的最高资产。 - 设置
ContextInfo.is_stop_triggered标志位,一旦止损触发,将其置为True,防止策略在后续的 K 线中继续运行开仓逻辑或重复发送平仓单。
- 设置了
-
handlebar函数:- 获取资产:使用
get_trade_detail_data(..., 'account')获取账户详情。m_dBalance属性代表当前的总资产(现金+持仓市值)。 - 计算回撤:逻辑是
1 - (当前资产 / 历史最高资产)。 - 触发条件:当回撤大于等于设定的阈值时,调用
clean_all_positions函数。
- 获取资产:使用
-
clean_all_positions函数:- 使用
get_trade_detail_data(..., 'position')获取当前所有持仓。 - 遍历持仓列表,读取
m_strInstrumentID(代码)和m_nVolume(持仓量)。 - 使用
passorder函数发送卖出指令。 - 注意:这里使用了
prType=14(对手价),目的是在止损时尽快成交,而不是挂单等待。
- 使用
注意事项
- 实盘与回测的区别:
- 上述代码主要针对实盘/模拟盘(Run Mode),因为它依赖
get_trade_detail_data来获取真实的账户资金。 - 如果是回测模式,通常使用
ContextInfo.capital加上累计盈亏来计算净值,或者使用ContextInfo.get_net_value(barpos)接口。
- 上述代码主要针对实盘/模拟盘(Run Mode),因为它依赖
- 高水位线的起点:
- 代码中
max_balance是从策略启动那一刻开始记录的。如果你希望基于账户昨天的静态权益开始计算,可以在init中尝试获取m_dPreBalance(期初权益)作为初始高水位线。
- 代码中
- 风控后的恢复:
- 当前逻辑是触发止损后
is_stop_triggered变为True,策略实际上就“死”了(不再执行逻辑)。如果需要人工干预后重启,需要重新运行策略或添加手动重置标志位的逻辑。
- 当前逻辑是触发止损后
Q&A
Q: 为什么使用 get_trade_detail_data 而不是 ContextInfo.capital?
A: ContextInfo.capital 通常用于回测时的初始资金设置。在实盘或模拟交易中,资金是动态变化的,且可能包含非策略产生的资金变动(如出入金),get_trade_detail_data 能获取柜台实时的总资产数据,更加准确。
Q: passorder 中的 prType=14 是什么意思?
A: prType=14 代表“对手价”。在股票卖出时,对手价通常指“买一价”。使用对手价下单可以提高成交概率,适合止损这种需要快速离场的场景。
Q: 如何防止因为数据瞬间波动导致的误止损?
A: 可以在判断回撤时增加一个确认机制,例如连续 3 个 tick(分笔)都检测到回撤超标才触发,或者在计算回撤时使用最近几分钟的资产均值。但在极端行情下,立即止损通常是更安全的选择。