问题描述
解决方案
在聚宽(JoinQuant)平台进行期货交易时,为了防止因行情剧烈波动导致账户权益不足而被交易所强行平仓,我们需要实时监控账户的风险度。
通常,风险度 = 当前持仓保证金 / 账户总资产。
当风险度接近 100%(即保证金占用了所有资产,可用资金为0)时,就会面临强平风险。
以下是一个完整的策略代码示例,展示了如何在 handle_data 中实时计算风险度,并在风险过高(例如超过 80%)时自动执行减仓或平仓操作。
核心逻辑说明
- 获取账户信息:通过
context.portfolio获取当前的margin(已用保证金)和total_value(总资产)。 - 计算风险度:
risk_ratio = context.portfolio.margin / context.portfolio.total_value。 - 风控触发:如果
risk_ratio超过设定的阈值(如 0.8),则触发平仓逻辑。 - 平仓逻辑:遍历当前持有的多单和空单,依次平仓,直到风险降低。
策略代码
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
# 1. 初始化配置
# 设定账户为期货账户,初始资金 50万
set_subportfolios([SubPortfolioConfig(cash=500000, type='futures')])
# 设定基准(这里以螺纹钢主力为例,实际可根据需要调整)
set_benchmark('RB9999.XSGE')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 2. 设定全局变量
# 设定风控阈值,当持仓保证金占比超过 80% 时触发强平保护
g.risk_limit = 0.8
# 定义要操作的合约(示例:螺纹钢主力)
g.future_code = 'RB9999.XSGE'
# 3. 运行频率设置
# 每天开盘时运行一次,用于获取主力合约代码等
run_daily(before_market_open, time='open')
# 盘中每分钟运行,进行交易和风控检查
run_daily(handle_trade_and_risk, time='every_bar')
def before_market_open(context):
# 获取当月的主力合约(如果是仿真或实盘,建议指定具体合约代码)
g.current_contract = get_dominant_future('RB')
log.info('今日主力合约: %s' % g.current_contract)
def handle_trade_and_risk(context):
"""
主逻辑函数:包含交易逻辑和风控检查
"""
# --- 第一步:执行风控检查 ---
# 在进行任何开仓操作前,先检查账户资金安全
check_risk_management(context)
# --- 第二步:执行交易策略 (示例策略) ---
# 如果刚刚触发了风控平仓,建议暂停开仓,这里简单演示随机开仓逻辑
# 仅作演示:如果当前无持仓,且风险度安全,则开多单
if context.portfolio.margin / context.portfolio.total_value < 0.5:
if not context.portfolio.positions:
# 保证金充足时,开仓
order(g.current_contract, 2, side='long')
log.info("保证金充足,尝试开仓: %s" % g.current_contract)
def check_risk_management(context):
"""
风控核心函数:检查保证金比例,过高则强平
"""
# 获取当前账户总资产
total_value = context.portfolio.total_value
# 获取当前持仓占用的保证金
used_margin = context.portfolio.margin
# 防止分母为0的极端情况
if total_value == 0:
return
# 计算当前风险度 (占用保证金 / 总资产)
current_risk = used_margin / total_value
# 打印当前资金状况
# log.info("当前总资产: %.2f, 已用保证金: %.2f, 风险度: %.2f%%" % (total_value, used_margin, current_risk * 100))
# 判断是否超过阈值
if current_risk > g.risk_limit:
log.warn("【风险预警】当前风险度 %.2f%% 超过阈值 %.2f%%,触发强制减仓!" % (current_risk * 100, g.risk_limit * 100))
# 执行平仓操作
close_positions_for_risk_control(context)
def close_positions_for_risk_control(context):
"""
强制平仓函数:平掉所有持仓以释放保证金
实际策略中可以写得更复杂,比如只平掉亏损最多的,或者减仓一半
"""
# 获取多单持仓
long_positions = context.portfolio.long_positions
# 获取空单持仓
short_positions = context.portfolio.short_positions
# 遍历多单进行平仓
for code, pos in long_positions.items():
if pos.total_amount > 0:
log.info("正在平多单: %s, 数量: %d" % (code, pos.total_amount))
# 平多单:卖出平仓
order_target(code, 0, side='long')
# 遍历空单进行平仓
for code, pos in short_positions.items():
if pos.total_amount > 0:
log.info("正在平空单: %s, 数量: %d" % (code, pos.total_amount))
# 平空单:买入平仓
order_target(code, 0, side='short')
代码详解
set_subportfolios: 在initialize中必须将账户类型设置为'futures',否则无法进行期货交易和保证金计算。- 风险度计算:
context.portfolio.margin: 当前持仓占用的保证金。context.portfolio.total_value: 账户当前的动态权益(包含现金和持仓盈亏)。current_risk = used_margin / total_value: 这是最核心的指标。如果该值等于 1.0,说明你的钱全部用来交保证金了,稍微波动一下就会被强平。
check_risk_management函数:- 该函数被放在
handle_trade_and_risk的最开始执行。这意味着在每一分钟(或每一个Tick)开始交易前,先检查账户是否安全。 - 如果
current_risk > g.risk_limit(0.8),即保证金占用超过80%,系统会判定为危险,并调用平仓函数。
- 该函数被放在
close_positions_for_risk_control函数:- 使用
context.portfolio.long_positions和short_positions获取当前所有持仓。 - 使用
order_target(code, 0, side='...')将目标仓位调整为 0,即全平。 - 进阶提示:在实盘中,你可能不希望一次性全平,可以修改逻辑为“每次减少 1 手”或者“优先平亏损仓位”。
- 使用
替代方案:使用 API 自带函数
聚宽 API 提供了一个便捷函数 is_dangerous,也可以用来做简单的判断:
# 检查子账户(通常索引为0)的风险
# 参数 0.2 表示:如果 (可用资金 / 总资产) < 20% 则返回 True
# 或者理解为:如果 (保证金占用 > 80%) 则返回 True
if context.subportfolios[0].is_dangerous(0.2):
log.warn("账户资金危险,执行强平...")
close_positions_for_risk_control(context)
注:手动计算(如完整代码所示)通常比使用 is_dangerous 更灵活,因为你可以精确控制是大于 80% 还是 90% 报警,以及打印具体的数值。
Q&A
Q: 为什么风险度阈值建议设在 0.8 (80%) 左右?
A: 期货公司通常会在风险度达到 100% 时强平。设置在 80% 可以给你留出缓冲空间,避免因行情瞬间跳空导致直接穿仓。
Q: 这段代码能用于实盘吗?
A: 逻辑上是通用的。但在实盘中,建议增加更复杂的平仓逻辑(例如分批平仓),而不是一次性清空所有仓位,以减少冲击成本。
Q: 如何只平掉亏损最大的仓位?
A: 在 close_positions_for_risk_control 中,可以遍历 pos.value 和 pos.price 计算浮动盈亏,将持仓列表按亏损幅度排序,然后只平掉列表中的第一个。