问题描述
解决方案
这是一个基于聚宽(JoinQuant)平台的多因子选股策略模板。
策略思路
该策略选取了三个经典的因子类别构建投资组合:
- 估值因子:市盈率(PE_ratio),选取低估值股票。
- 成长因子:净利润增长率(inc_net_profit_year_on_year),选取高成长股票。
- 质量因子:净资产收益率(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 会自动计算需要买入或卖出的数量,使该股票的持仓市值达到指定目标。这在定期调仓策略中非常有用,因为它会自动处理因股价波动导致的权重偏移,实现“再平衡”。