🚀 AI 一键生成 joinquant 策略代码

JoinQuant 教程:如何使用 portfolio_optimizer 实现多重自定义约束?

joinquant | 发布时间: 2025/12/11
以下内容由 EasyQuant 生成。

问题描述

我如何构建一个投资组合,要求它在最小化风险的同时,必须保证金融行业的持仓比例在10%到20%之间,并且整体换手率不能超过30%?

解决方案

在聚宽(JoinQuant)平台上,构建一个满足特定约束(如行业占比、换手率限制)并追求最小化风险的投资组合,可以使用 jqlib.optimizer 库中的 投资组合优化器 (portfolio_optimizer) 来实现。

核心思路

  1. 优化目标:使用 MinVariance(最小化组合方差)来实现风险最小化。
  2. 行业约束:使用 IndustryConstraint 来限制金融行业的权重在 10% 到 20% 之间。我们需要知道金融行业的代码(聚宽一级行业中金融指数代码通常为 'HY007')。
  3. 换手率约束:使用 TurnoverConstraint 来限制当次调仓相对于当前持仓的换手率不超过 30%。
  4. 权重约束:通常还需要一个 WeightConstraint 来保证总仓位在合理区间(例如 95%~100%)。

策略代码实现

以下是一个完整的策略示例。该策略以沪深300成分股为股票池,每月调仓一次,在满足上述约束的前提下最小化风险。

# -*- coding: utf-8 -*-
# 导入函数库
from jqdata import *
from jqlib.optimizer import *
import pandas as pd

def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 过滤掉order系列API产生的比error级别低的log
    log.set_level('order', 'error')
    
    # 设定交易费率
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
    
    # 设定按月运行,每月第一个交易日
    run_monthly(market_open, monthday=1, time='09:30')

def market_open(context):
    # 1. 确定股票池:这里选取沪深300成分股
    stock_pool = get_index_stocks('000300.XSHG')
    
    # 2. 获取当前持仓权重,用于计算换手率约束
    # 如果是空仓,current_portfolio 为 None 或空 Series
    current_positions = context.portfolio.positions
    if len(current_positions) > 0:
        total_value = context.portfolio.total_value
        current_weights = pd.Series({code: pos.value / total_value for code, pos in current_positions.items()})
    else:
        current_weights = None

    # 3. 设定优化目标和约束条件
    
    # 目标:最小化组合方差(风险最小化)
    # count=250 表示使用过去250个交易日的数据来计算协方差矩阵
    target = MinVariance(count=250)
    
    constraints = []
    
    # 约束1:总权重限制在 95% 到 100% 之间(保持高仓位)
    constraints.append(WeightConstraint(low=0.95, high=1.0))
    
    # 约束2:金融行业持仓比例在 10% 到 20% 之间
    # 'HY007' 是聚宽一级行业中的金融指数代码
    constraints.append(IndustryConstraint(industry_code='HY007', low=0.1, high=0.2))
    
    # 约束3:整体换手率不能超过 30%
    # 注意:如果是第一次调仓(current_weights为None),此约束可能不生效或被视为从0仓位建仓
    if current_weights is not None:
        constraints.append(TurnoverConstraint(limit=0.3, current_portfolio=current_weights))
    
    # 4. 运行优化器
    # 注意:date应使用前一个交易日,避免未来函数
    try:
        optimized_weights = portfolio_optimizer(
            date=context.previous_date,
            securities=stock_pool,
            target=target,
            constraints=constraints,
            # 单只股票权重上限设为10%,防止过度集中
            bounds=[Bound(0.0, 0.1)], 
            ftol=1e-09,
            return_none_if_fail=True
        )
    except Exception as e:
        log.error("优化器运行异常: %s" % e)
        optimized_weights = None

    # 5. 执行调仓
    if optimized_weights is None:
        log.warn("组合优化失败,本次不调仓")
        return
    
    log.info("优化成功,开始调仓")
    
    # 获取当前总资产
    total_value = context.portfolio.total_value
    
    # 先卖出不在优化结果中的股票
    for stock in context.portfolio.positions:
        if stock not in optimized_weights.index:
            order_target_value(stock, 0)
    
    # 根据优化后的权重买入/调整股票
    for stock, weight in optimized_weights.items():
        target_value = total_value * weight
        # 只有当目标价值大于0时才下单
        if target_value > 0:
            order_target_value(stock, target_value)

代码关键点解析

  1. MinVariance(count=250):

    • 这是核心目标函数。它会根据过去 250 个交易日的历史数据计算协方差矩阵,寻找波动率(方差)最小的组合。
  2. IndustryConstraint(industry_code='HY007', low=0.1, high=0.2):

    • 这是行业约束。'HY007' 是聚宽一级行业分类中的“金融指数”。
    • 该行代码强制优化器选出的组合中,属于金融行业的股票权重之和必须落在 0.1 (10%) 到 0.2 (20%) 之间。
    • 提示:您可以在聚宽的研究环境中通过 get_industries(name='jq_l1') 查看所有行业代码。
  3. TurnoverConstraint(limit=0.3, current_portfolio=current_weights):

    • 这是换手率约束。它需要传入当前的持仓权重 (current_portfolio)。
    • 优化器会计算 sum(abs(新权重 - 旧权重)),并确保这个值(双边换手率的一半,即单边换手)不超过 limit。这在资金量大或为了控制交易成本时非常有用。
  4. portfolio_optimizer:

    • 这是执行优化的主函数。如果因为约束条件过于苛刻(例如市场剧烈波动导致无法在换手率限制内满足行业占比),优化可能会失败并返回 None。代码中加入了 if optimized_weights is None 的判断来处理这种情况,避免程序报错。

Q&A

Q: 如何查找其他行业的代码?
A: 您可以在聚宽的研究模块中运行以下代码来查看聚宽一级行业列表:

from jqdata import *
df = get_industries(name='jq_l1')
print(df)

找到对应的行业代码(如医药、科技等)替换代码中的 'HY007' 即可。

Q: 为什么优化器有时候会返回 None?
A: 当约束条件之间存在冲突,或者在给定的约束下无法找到可行解时(例如要求换手率极低,但为了满足行业占比必须大幅调仓),优化器会失败。建议在 portfolio_optimizer 中设置 return_none_if_fail=True 并做好异常处理。

Q: MinVariance 是如何衡量风险的?
A: 它使用的是历史波动率。默认参数 count=250 表示使用过去一年的日收益率数据来计算协方差矩阵。如果您希望对近期波动更敏感,可以减小 count 的值(例如 60)。