🚀 AI 一键生成 joinquant 策略代码

如何在 JoinQuant 中实现一个多因子选股模型?(附源码)

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

问题描述

请帮我实现一个多因子选股模型。

解决方案

这是一个基于聚宽(JoinQuant)平台的多因子选股策略模板。

策略思路

该策略选取了三个经典的因子类别构建投资组合:

  1. 估值因子:市盈率(PE_ratio),选取低估值股票。
  2. 成长因子:净利润增长率(inc_net_profit_year_on_year),选取高成长股票。
  3. 质量因子:净资产收益率(ROE),选取盈利能力强的股票。

执行逻辑:

  • 股票池:沪深300成分股。
  • 调仓频率:按月调仓(每月第一个交易日)。
  • 数据处理
    • 过滤停牌、ST股票、涨跌停股票。
    • 去极值:处理异常数据。
    • 标准化:将不同量纲的因子转换为标准分(Z-Score)。
    • 打分:等权重加权求和(低PE得分高,高增长得分高,高ROE得分高)。
  • 交易:选取总分最高的排名前20只股票,等权重买入。

策略代码

# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
from jqdata import *

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')
    
    # 设定全局变量
    g.stock_pool = '000300.XSHG'  # 股票池:沪深300
    g.stock_num = 20              # 持仓数量
    
    # 按月运行,每月第一个交易日开盘后运行
    run_monthly(rebalance, monthday=1, time='09:30')

def rebalance(context):
    """
    调仓主函数
    """
    # 1. 获取股票池
    check_out_list = get_index_stocks(g.stock_pool)
    
    # 2. 过滤掉停牌、ST、退市的股票
    check_out_list = filter_paused_and_st(check_out_list, context.current_dt)
    
    # 3. 获取因子数据
    # 选取因子:市盈率(PE)、净资产收益率(ROE)、净利润增长率
    q = query(
        valuation.code,
        valuation.pe_ratio,                 # 估值因子:市盈率
        indicator.roe,                      # 质量因子:ROE
        indicator.inc_net_profit_year_on_year # 成长因子:净利润增长率
    ).filter(
        valuation.code.in_(check_out_list)
    )
    
    df = get_fundamentals(q, date=context.previous_date)
    
    # 4. 数据预处理
    # 过滤掉PE为负(亏损)的股票
    df = df[df['pe_ratio'] > 0]
    # 将code设为index
    df.set_index('code', inplace=True)
    
    # 5. 因子处理与打分
    # 对PE进行倒数处理,因为PE越低越好,倒数后越大越好,方便统一方向
    df['pe_ratio'] = 1 / df['pe_ratio']
    
    # 去极值 (Winsorization)
    for col in ['pe_ratio', 'roe', 'inc_net_profit_year_on_year']:
        df[col] = winsorize(df[col])
        
    # 标准化 (Standardization)
    for col in ['pe_ratio', 'roe', 'inc_net_profit_year_on_year']:
        df[col] = standardize(df[col])
        
    # 计算总分 (等权重相加)
    df['score'] = df['pe_ratio'] + df['roe'] + df['inc_net_profit_year_on_year']
    
    # 6. 选股
    # 按分数降序排列,取前N只
    buy_list = df.sort_values('score', ascending=False).head(g.stock_num).index.tolist()
    
    # 7. 执行交易
    trade(context, buy_list)

def filter_paused_and_st(stock_list, current_dt):
    """
    过滤停牌、ST、退市股票
    """
    curr_data = get_current_data()
    target_list = []
    for stock in stock_list:
        # 过滤停牌
        if curr_data[stock].paused:
            continue
        # 过滤ST
        if curr_data[stock].is_st:
            continue
        # 过滤涨跌停(可选,防止买不进卖不出)
        if curr_data[stock].day_open == curr_data[stock].high_limit or curr_data[stock].day_open == curr_data[stock].low_limit:
            continue
        target_list.append(stock)
    return target_list

def winsorize(series):
    """
    去极值:3倍标准差法
    """
    mean = series.mean()
    std = series.std()
    upper = mean + 3 * std
    lower = mean - 3 * std
    # 将超过上下限的值替换为边界值
    series = series.clip(lower, upper)
    return series

def standardize(series):
    """
    标准化:Z-Score标准化
    """
    mean = series.mean()
    std = series.std()
    # 防止除以0
    if std == 0:
        return series
    return (series - mean) / std

def trade(context, buy_list):
    """
    交易执行函数
    """
    # 获取当前持仓
    holdings = list(context.portfolio.positions.keys())
    
    # 卖出不在买入列表中的股票
    for stock in holdings:
        if stock not in buy_list:
            order_target_value(stock, 0)
            
    # 买入股票
    if len(buy_list) > 0:
        # 等权重分配资金
        value_per_stock = context.portfolio.total_value / len(buy_list)
        for stock in buy_list:
            order_target_value(stock, value_per_stock)

常见问题 (Q&A)

Q1: 为什么代码中要对市盈率 (PE) 取倒数?
A1: 在多因子模型中,我们通常希望因子的方向是一致的(例如都是值越大越好)。ROE和净利润增长率通常是越高越好,而PE通常是越低越好(代表估值便宜)。为了统一方向,我们对PE取倒数(即 EP Ratio),这样处理后,数值越大代表估值越低,便于后续直接加权求和。

Q2: 什么是去极值 (Winsorization) 和标准化 (Standardization)?
A2:

  • 去极值:防止某些股票的因子值异常大或异常小(例如某公司净利润突然暴增100倍),导致该因子在打分时权重过大,掩盖了其他因子的作用。代码中使用了3倍标准差法将异常值拉回到边界。
  • 标准化:不同因子的单位不同(例如PE是倍数,增长率是百分比)。标准化将它们转化为均值为0、标准差为1的分布,使不同量纲的因子可以直接相加。

Q3: 如何更换股票池或因子?
A3:

  • 更换股票池:修改 initialize 函数中的 g.stock_pool,例如改为 '000905.XSHG' (中证500)。
  • 更换因子:修改 rebalance 函数中 query 部分的字段,例如加入 valuation.pb_ratio (市净率),并记得在后续的数据处理环节(去极值、标准化、打分)中加入新因子的列名。

Q4: 为什么使用 order_target_value 而不是 order
A4: order_target_value 会自动计算需要买入或卖出的数量,使该股票的持仓市值达到指定目标。这在定期调仓策略中非常有用,因为它会自动处理因股价波动导致的权重偏移,实现“再平衡”。