🚀 AI 一键生成 joinquant 策略代码
立即体验

如何在聚宽(JoinQuant)中编写基于均线过滤与线性回归趋势质量的 ETF 轮动策略?

joinquant | 发布时间: 2026/8/10
以下内容由 EasyQuant 生成。

问题描述

JoinQuant ETF 轮动:如何用趋势过滤和线性回归筛选强势 ETF?
如何在聚宽中从多个行业 ETF 候选池里用20日均线过滤,并根据25日对数价格线性回归的趋势质量选择前五只持有?

解决方案

策略核心逻辑设计

本策略属于动量轮动与趋势跟踪相结合的量化投资策略。其核心逻辑分为以下三个步骤:

  1. 候选 ETF 池定义:精选代表不同行业或宽基的 ETF 标的(如沪深300 ETF、创业板 ETF、半导体 ETF、医药 ETF 等)。
  2. 趋势过滤(20日均线):只有当前价格高于其 20 日简单移动平均线(MA20)的 ETF 才能进入候选,以此过滤掉处于下行通道的标的。
  3. 趋势质量筛选(25日对数价格线性回归)
    • 对候选 ETF 过去 25 个交易日的收盘价取自然对数($\ln(P)$)。
    • 以时间序列($0, 1, 2, ..., 24$)作为自变量 $X$,对数价格作为因变量 $Y$ 进行一元线性回归。
    • 趋势质量指标:利用回归斜率(Slope,代表涨幅速度)乘以拟合优度($R^2$,代表趋势的平稳与确定性)作为最终得分:$\text{Score} = \text{Slope} \times R^2$。
    • 按照得分从高到低排序,选择前 5 只得分最高的 ETF,等权重买入或持有。若符合条件的不足 5 只,则有多少买多少,剩余资金留存现金。

聚宽(JoinQuant)策略源码实现

请在聚宽回测环境中运行以下 Python 3 策略代码:

import numpy as np
import pandas as pd
from scipy.stats import linregress
from jqdata import *

def initialize(context):
    # 1. 策略基本设置
    set_benchmark('000300.XSHG')
    set_option('use_real_price', True)
    log.set_level('order', 'info')
    
    # 2. 定义行业与宽基 ETF 候选池
    g.etf_pool = [
        '510300.XSHG', # 沪深300 ETF
        '510500.XSHG', # 中证500 ETF
        '159915.XSHE', # 创业板 ETF
        '518880.XSHG', # 黄金 ETF
        '512880.XSHG', # 证券 ETF
        '512010.XSHG', # 医药 ETF
        '512660.XSHG', # 军工 ETF
        '515000.XSHG', # 科技 ETF
        '512480.XSHG', # 半导体 ETF
        '159928.XSHE', # 消费 ETF
    ]
    
    # 3. 设定调仓参数
    g.max_hold = 5  # 最大持有数量
    g.ma_window = 20  # 均线过滤周期
    g.reg_window = 25  # 线性回归周期
    
    # 4. 每周第一个交易日开盘运行调仓
    run_weekly(my_rebalance, weekday=1, time='09:30')

def my_rebalance(context):
    # 获取历史数据
    # 需要获取最大窗口(25天)的数据
    hist = history(g.reg_window, unit='1d', field='close', security_list=g.etf_pool, df=True)
    
    candidates = {}
    
    for etf in g.etf_pool:
        prices = hist[etf].values
        if len(prices) < g.reg_window or np.isnan(prices).any():
            continue
            
        # 步骤 1: 20日均线过滤
        ma20 = prices[-g.ma_window:].mean()
        current_price = prices[-1]
        
        if current_price > ma20:
            # 步骤 2: 25日对数价格线性回归
            log_prices = np.log(prices)
            x = np.arange(len(log_prices))
            
            # 使用 scipy 计算线性回归
            slope, intercept, r_value, p_value, std_err = linregress(x, log_prices)
            r_squared = r_value ** 2
            
            # 计算趋势质量得分 (斜率 * R2)
            # 只有斜率为正(即上涨趋势)才考虑
            if slope > 0:
                score = slope * r_squared
                candidates[etf] = score

    # 步骤 3: 排序并选择前 5 只
    sorted_candidates = sorted(candidates.items(), key=lambda item: item[1], reverse=True)
    target_etfs = [item[0] for item in sorted_candidates[:g.max_hold]]
    
    log.info(f"今日通过过滤与筛选的 ETF 目标持仓: {target_etfs}")
    
    # 步骤 4: 执行调仓操作(卖出不在目标持仓中的,等权买入目标持仓)
    current_positions = list(context.portfolio.positions.keys())
    
    # 1. 卖出不在目标列表中的 ETF
    for etf in current_positions:
        if etf not in target_etfs:
            order_target(etf, 0)
            log.info(f"卖出清仓: {etf}")
            
    # 2. 计算等权分配资金
    if len(target_etfs) > 0:
        target_value = context.portfolio.total_value / len(target_etfs)
        for etf in target_etfs:
            order_target_value(etf, target_value)
            log.info(f"调仓/买入: {etf} 至目标价值 {target_value:.2f} 元")

策略优势与优化建议

  1. 双重过滤机制:20日均线作为大趋势过滤器,能有效避免在熊市或行业暴跌中买入“看似便宜”但仍在下跌的 ETF;线性回归则确保了买入的是上涨最稳健、速度最快的标的。
  2. 对数价格回归的科学性:对价格取对数可以消除高价 ETF 与低价 ETF 之间的绝对价格差影响,使得斜率(Slope)代表的是百分比收益率的变化速度,更加公平。
  3. 拟合度 $R^2$ 的妙用:单纯看斜率容易选到暴涨暴跌、波动极大的标的。乘以 $R^2$(拟合优度)后,策略会倾向于选择沿着稳定趋势温和上涨的 ETF,降低了回撤风险。