问题描述
解决方案
这是一个非常实用的风控需求。在量化交易中,这通常被称为最大回撤止损(Max Drawdown Stop-loss)。
策略逻辑分析
- 记录最高权益(High Water Mark):我们需要一个全局变量(例如
g.max_value)来记录策略运行以来的资金最高点。 - 实时计算回撤:在每一个交易时刻(或每天收盘),用
(最高权益 - 当前权益) / 最高权益计算当前的回撤幅度。 - 触发清仓:当回撤幅度大于 10%(0.1)时,执行清仓操作,并停止后续的买入行为。
代码实现
下面是一个完整的 JoinQuant 策略代码示例。为了演示效果,我加入了一个简单的买入逻辑(全仓买入沪深300 ETF),以便让资金产生波动从而触发回撤逻辑。
# -*- coding: utf-8 -*-
import jqdata
def initialize(context):
"""
初始化函数
"""
# 设置基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 设置手续费(股票:买入万三,卖出万三加千一印花税)
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# --- 风控相关变量初始化 ---
# 记录策略运行以来的最高总资产,初始化为初始资金
g.max_value = context.portfolio.starting_cash
# 标记是否触发了止损,触发后不再开仓
g.stop_trading = False
# 设定回撤阈值 (10%)
g.drawdown_limit = 0.10
# 设定要操作的标的(这里以沪深300ETF为例用于演示)
g.security = '510300.XSHG'
# 每天开盘时运行
run_daily(market_open, time='every_bar')
def market_open(context):
"""
每日/每分钟 交易逻辑
"""
# 1. 更新最高权益 (High Water Mark)
# context.portfolio.total_value 包含 现金 + 持仓市值
if context.portfolio.total_value > g.max_value:
g.max_value = context.portfolio.total_value
# 2. 计算当前回撤
# 回撤 = 1 - (当前净值 / 历史最高净值)
current_drawdown = 1 - (context.portfolio.total_value / g.max_value)
# 3. 检查是否触发风控
if current_drawdown > g.drawdown_limit and not g.stop_trading:
log.warn("警告:当前回撤 {:.2%} 超过阈值 {:.2%},触发强制清仓!".format(current_drawdown, g.drawdown_limit))
log.info("历史最高资产: {:.2f}, 当前资产: {:.2f}".format(g.max_value, context.portfolio.total_value))
# 执行清仓操作
clear_all_positions(context)
# 设置停止交易标志,防止后续代码继续买入
g.stop_trading = True
return
# 如果已经触发了止损,则直接返回,不再执行后续策略逻辑
if g.stop_trading:
return
# --- 下面是正常的策略买卖逻辑 (示例) ---
# 简单的示例:如果有现金且没持仓,就全仓买入
if g.security not in context.portfolio.positions and context.portfolio.available_cash > 0:
order_value(g.security, context.portfolio.available_cash)
log.info("正常买入: %s" % g.security)
def clear_all_positions(context):
"""
清仓函数:卖出所有持仓
"""
# 获取当前所有持仓的标的列表
positions = context.portfolio.positions.keys()
for security in positions:
# 获取该标的可卖出数量
amount = context.portfolio.positions[security].closeable_amount
if amount > 0:
# 卖出所有可卖持仓
order_target(security, 0)
log.info("风控清仓卖出: %s" % security)
代码关键点解析
-
g.max_value(最高水位线):- 我们在
initialize中将其初始化为context.portfolio.starting_cash。 - 在
market_open的每一帧中,我们检查context.portfolio.total_value(当前总资产)是否创新高,如果创新高则更新g.max_value。
- 我们在
-
回撤计算公式:
current_drawdown = 1 - (当前总资产 / 历史最高总资产)。- 例如:最高有过100万,现在剩下85万,回撤就是
1 - 0.85 = 0.15(15%)。
-
g.stop_trading(熔断开关):- 一旦回撤超过 10%,我们将此变量设为
True。 - 在正常的买入逻辑之前,必须检查
if g.stop_trading: return。如果不加这个判断,策略可能会在清仓后的下一分钟或第二天因为满足买入条件而再次买入,导致风控失效。
- 一旦回撤超过 10%,我们将此变量设为
-
clear_all_positions(清仓函数):- 遍历
context.portfolio.positions,对每一个持仓标的调用order_target(security, 0),将其持仓调整为 0。
- 遍历
常见问题 (Q&A)
Q: 这个策略是按天回撤还是盘中实时回撤?
A: 取决于你回测的频率。
- 如果在回测设置中选择 “分钟” 频率,且代码中使用
run_daily(..., time='every_bar')或handle_data,那么它是盘中实时监控的。一旦盘中某分钟资产跌破阈值,立即清仓。 - 如果在回测设置中选择 “天” 频率,那么它是在每天收盘后(或开盘前,取决于代码位置)计算一次回撤。
Q: 清仓后,如果我想在未来某个时间点(比如回撤修复后)重新开始交易怎么办?
A: 目前的代码是永久停止交易。如果你希望在特定条件下(例如大盘企稳、或者手动重启)恢复交易,你需要编写恢复逻辑。例如:
# 示例:如果当前资产回升到最高点的 95% 以上,恢复交易
if g.stop_trading:
if context.portfolio.total_value > g.max_value * 0.95:
g.stop_trading = False
log.info("资产回升,解除风控限制,恢复交易。")
Q: 为什么有时候清仓不彻底?
A: A股是 T+1 交易制度。如果你当天刚买入的股票(持仓在 today_amount 中,而 closeable_amount 为 0),当天是无法卖出的。order_target 只能卖出 closeable_amount。如果需要处理这种情况,通常只能等到第二天继续执行清仓逻辑。