Python量化交易实战:从零搭建你的第一个多因子选股模型(附完整代码)
如果你对金融市场充满好奇,厌倦了凭感觉买卖股票,想用理性和数据来指导投资决策,那么量化交易可能就是你的答案。但很多初学者往往被复杂的数学公式和庞大的金融理论体系吓退,觉得量化是“高岭之花”,遥不可及。其实,量化投资的精髓在于将投资逻辑转化为计算机可执行的代码,而Python正是连接这两者的最佳桥梁。今天,我们就抛开那些晦涩的理论,直接动手,用Python从零开始,一步步构建一个真正能跑起来、能分析、能回测的多因子选股模型。这不仅仅是一篇教程,更是一次完整的实战演练,你将亲手触摸到量化策略的骨架与血肉。
1. 环境准备与数据获取:打好地基
在开始编写任何策略代码之前,一个稳定、高效的开发环境是必不可少的。对于量化分析,我们强烈推荐使用 Anaconda 来管理Python环境,它能轻松处理各种科学计算库的依赖关系。
1.1 创建专属的量化环境
打开你的终端(Windows上是Anaconda Prompt或CMD,Mac/Linux是Terminal),执行以下命令来创建一个名为 quant_env 的独立环境,并安装核心库:
# 创建新环境,指定Python版本为3.9(兼容性较好)
conda create -n quant_env python=3.9
# 激活环境
conda activate quant_env
# 安装量化分析核心三件套
pip install pandas numpy matplotlib
# 安装金融数据获取和回测框架(这里以Tushare和Backtrader为例,也可选择akshare、vn.py等)
pip install tushare backtrader
注意:Tushare是一个优秀的国内金融数据接口,但需要注册获取token。你也可以选择akshare,它无需注册,数据源更丰富。本文示例将使用Tushare,请提前在官网注册并获取你的token。
1.2 构建你的第一个数据获取脚本
数据是量化策略的“燃料”。我们首先需要获取股票的历史行情和基本面数据。创建一个名为 data_fetcher.py 的Python文件。
import tushare as ts
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
# 1. 初始化Tushare Pro接口(替换为你的token)
pro = ts.pro_api('你的tushare_token_here')
def fetch_stock_basic():
"""获取全市场股票基础信息"""
print("正在获取股票基础信息列表...")
# 获取当前正常上市交易的股票
df_basic = pro.stock_basic(exchange='', list_status='L', fields='ts_code,symbol,name,area,industry,list_date')
print(f"共获取到 {len(df_basic)} 只上市股票")
# 保存到CSV,方便后续使用
df_basic.to_csv('./data/stock_basic.csv', index=False)
return df_basic
def fetch_daily_data(ts_code, start_date, end_date):
"""获取单只股票的日线行情数据"""
try:
df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)
df.sort_index(inplace=True) # 按日期排序
return df
except Exception as e:
print(f"获取 {ts_code} 数据失败: {e}")
return pd.DataFrame()
def fetch_valuation_data(ts_code, start_date, end_date):
"""获取单只股票的每日估值指标(市盈率PE、市净率PB等)"""
try:
# 这里以daily_basic接口为例,它包含市值、PE、PB、换手率等
df = pro.daily_basic(ts_code=ts_code, start_date=start_date, end_date=end_date,
fields='trade_date,close,total_mv,pe_ttm,pb,ps_ttm,dv_ttm,turnover_rate')
df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)
df.sort_index(inplace=True)
return df
except Exception as e:
print(f"获取 {ts_code} 估值数据失败: {e}")
return pd.DataFrame()
def build_initial_dataset():
"""构建初始数据集:这里以沪深300成分股为例,获取近3年数据"""
# 确保数据目录存在
os.makedirs('./data', exist_ok=True)
# 获取沪深300成分股列表(作为我们的股票池)
print("获取沪深300指数成分股...")
df_hs300 = pro.index_weight(index_code='000300.SH', start_date='20220101')
hs300_codes = df_hs300['con_code'].unique().tolist()
print(f"沪深300成分股数量: {len(hs300_codes)}")
# 定义时间范围
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=3*365)).strftime('%Y%m%d') # 3年数据
all_data = {}
valuation_data = {}
# 示例:先获取前10只股票的数据进行演示(避免请求过于频繁)
sample_codes = hs300_codes[:10]
for idx, ts_code in enumerate(sample_codes, 1):
print(f"正在处理 [{idx}/{len(sample_codes)}]: {ts_code}")
daily_df = fetch_daily_data(ts_code, start_date, end_date)
val_df = fetch_valuation_data(ts_code, start_date, end_date)
if not daily_df.empty:
all_data[ts_code] = daily_df
if not val_df.empty:
valuation_data[ts_code] = val_df
# 将数据保存到本地
import pickle
with open('./data/daily_data_sample.pkl', 'wb') as f:
pickle.dump(all_data, f)
with open('./data/valuation_data_sample.pkl', 'wb') as f:
pickle.dump(valuation_data, f)
print("初始数据集构建完成!数据已保存至 ./data/ 目录下。")
return all_data, valuation_data
if __name__ == '__main__':
# 执行数据获取
fetch_stock_basic()
daily_data, val_data = build_initial_dataset()
运行这个脚本,你将在 ./data 目录下得到几个文件:股票列表、行情数据和估值数据。这是你策略分析的起点。数据质量直接决定策略的上限,因此在实际项目中,你需要考虑数据清洗(处理缺失值、异常值)、复权(前复权或后复权以保证价格连续性)等问题。
2. 因子挖掘与有效性检验:寻找Alpha的“钥匙”
多因子模型的核心在于“因子”。因子可以理解为衡量股票某种特征的指标,例如估值水平、成长性、盈利能力、市场情绪等。我们的目标是找到那些能稳定、显著预测未来股票收益的因子。
2.1 计算常见因子
我们基于获取到的日线数据和基本面数据,计算几个经典因子。创建一个新的文件 factor_calculator.py。
import pandas as pd
import numpy as np
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
class FactorCalculator:
def __init__(self, price_data_dict, valuation_data_dict):
"""
初始化因子计算器
:param price_data_dict: 字典,key为股票代码,value为日线DataFrame
:param valuation_data_dict: 字典,key为股票代码,value为估值DataFrame
"""
self.price_data = price_data_dict
self.val_data = valuation_data_dict
self.all_codes = list(price_data_dict.keys())
def calculate_momentum(self, window=20):
"""计算动量因子:过去N日的收益率"""
momentum_factors = {}
for code in self.all_codes:
df = self.price_data[code].copy()
if len(df) < window:
continue
# 使用复权收盘价计算收益率
df['returns'] = df['close'].pct_change()
# 过去window日的累计收益率作为动量因子
df['momentum'] = df['close'].pct_change(window)
momentum_factors[code] = df[['momentum']]
return momentum_factors
def calculate_valuation_factor(self, factor_name='pe_ttm'):
"""计算估值类因子,如市盈率PE、市净率PB的倒数(通常估值越低越好)"""
valuation_factors = {}
for code in self.all_codes:
if code not in self.val_data:
continue
df = self.val_data[code].copy()
if factor_name not in df.columns:
continue
# 对于PE、PB,通常取其倒数(EP、BP),代表收益率,值越大越好
if factor_name in ['pe_ttm', 'pb']:
# 处理无穷大或零值
df[f'inv_{factor_name}'] = 1 / df[factor_name].replace([np.inf, -np.inf], np.nan)
valuation_factors[code] = df[[f'inv_{factor_name}']]
else:
valuation_factors[code] = df[[factor_name]]
return valuation_factors
def calculate_liquidity_factor(self, window=10):
"""计算流动性因子:过去N日的平均换手率"""
liquidity_factors = {}
for code in self.all_codes:
if code not in self.val_data:
continue
df = self.val_data[code].copy()
if 'turnover_rate' not in df.columns:
continue
df['avg_turnover'] = df['turnover_rate'].rolling(window=window, min_periods=5).mean()
liquidity_factors[code] = df[['avg_turnover']]
return liquidity_factors
def calculate_volatility_factor(self, window=20):
"""计算波动率因子:过去N日收益率的标准差(代表风险)"""
vol_factors = {}
for code in self.all_codes:
df = self.price_data[code].copy()
if len(df) < window:
continue
df['returns'] = df['close'].pct_change()
df['volatility'] = df['returns'].rolling(window=window, min_periods=10).std()
vol_factors[code] = df[['volatility']]
return vol_factors
def calculate_size_factor(self):
"""计算规模因子:总市值的对数(通常小市值股票可能有超额收益)"""
size_factors = {}
for code in self.all_codes:
if code not in self.val_data:
continue

&spm=1001.2101.3001.5002&articleId=152917265&d=1&t=3&u=4f8f827e362641a2ae1bc6c5d5c3420b)
4万+

被折叠的 条评论
为什么被折叠?



