三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于聚宽jqdatasdk的量化选股框架:从多因子策略到本地化实践

基于聚宽jqdatasdk的量化选股框架:从多因子策略到本地化实践

1. 项目概述:从零搭建一个可复用的选股框架

如果你对量化投资感兴趣,想自己动手写点策略但又觉得无从下手,那今天聊的这个基于聚宽社区和jqdatasdk的简单选股策略,可能就是你的第一块敲门砖。这玩意儿不是什么能预测未来的“圣杯”,它的核心价值在于,提供了一个清晰、完整、可运行的代码骨架,让你能亲手把书本上的财务指标、技术指标,变成一行行能跑出股票列表的代码。我见过太多新手卡在数据获取、回测框架对接这些前期准备上,折腾半天热情就耗光了。而这个项目,恰恰帮你绕过了这些“脏活累活”,直接聚焦在策略逻辑本身——也就是你最该花心思的地方。

简单说,jqdatasdk是聚宽量化平台提供的一个本地数据获取工具包,它让你能在自己的 Python 环境里,像调用本地库一样方便地获取A股行情、财务、基本面等数据。而“聚宽社区”则是一个宝库,里面充满了其他开发者分享的策略思路和代码片段。我们这个项目,就是利用jqdatasdk从本地获取数据,实现一个在聚宽社区里常见的、逻辑清晰的选股策略,并输出可执行的股票列表。整个过程完全在本地完成,不依赖聚宽的在线回测系统,自由度更高,也更适合用来验证想法、做快速研究。

它适合谁呢?首先是量化投资的初学者,你想知道一个策略从想法到代码的完整路径是怎样的。其次是有一定Python基础,但对金融市场和量化工具还比较陌生的程序员,这个项目能帮你建立起数据与策略之间的桥梁。最后,哪怕你是个有经验的投资者,想快速测试某个选股条件(比如“市盈率低于20且最近一个月涨幅超过5%的股票”),这套框架也能让你在几分钟内得到结果,比手动翻财报和K线图高效太多了。

2. 策略核心逻辑与设计思路拆解

2.1 策略逻辑:多因子过滤的“漏斗模型”

一个典型的简单选股策略,其核心思想可以概括为“漏斗模型”。我们从一个庞大的股票池(比如全部A股)开始,像过筛子一样,一层一层地施加筛选条件,最终留下符合所有条件的少数标的。这个过程的严谨性和逻辑性,直接决定了策略的有效性。

我设计的这个示例策略,包含了三个常见的筛选维度,它们分别代表了不同的投资视角:

  1. 市值过滤:选取市值大于一定规模的股票。这通常是为了规避流动性过差的小盘股,降低交易冲击成本和非系统性风险。在实际操作中,你可以设定一个阈值,比如只选择市值排名在前70%的股票。
  2. 估值过滤:选取市盈率(PE)在一定合理范围内的股票。这是价值投资的经典思路,避免买入估值过高的标的。例如,可以设定 PE > 0(排除亏损股)且 PE < 30。
  3. 动量过滤:选取近期股价有一定上涨动能的股票。这是趋势跟踪思想的体现。我们可以计算股票在过去一段时间(比如20个交易日)的涨跌幅,并筛选出涨幅为正的股票。

将这三个条件用“与”逻辑结合,就构成了一个“中大市值、估值合理、且近期有上涨势头”的选股策略。这个逻辑本身并不复杂,但关键在于如何准确、高效地用代码实现每一个过滤条件,这正是jqdatasdk发挥价值的地方。

2.2 为什么选择 jqdatasdk 与本地化开发?

市面上数据源很多,为什么偏偏用jqdatasdk?这背后有几个很实际的考量。

首先,数据质量与规范性。聚宽的数据在国内量化圈子里是经过多年检验的,涵盖了行情、财务、指数、宏观等各个方面,并且做了大量的清洗和复权处理(比如前复权)。对于初学者乃至大多数专业开发者来说,直接使用经过处理的高质量数据,能避免在数据清洗上踩坑,把精力集中在策略逻辑上。自己从原始交易所数据开始处理,是个工程量巨大且容易出错的事情。

其次,本地开发的灵活性与隐私性。使用jqdatasdk意味着所有计算都在你自己的电脑上进行。你的策略代码、核心参数、选股结果完全掌握在自己手里,没有上传到任何第三方平台的风险。这对于策略保密性要求高的个人或机构来说,是一个重要优势。同时,本地运行也让你可以自由地结合其他Python库(如pandas,numpy,scikit-learn等)进行更复杂的数据分析和建模,不受在线平台的功能限制。

最后,学习路径的平滑性。聚宽社区有海量的策略和讨论,很多代码思路可以直接借鉴或修改。使用jqdatasdk能让你的本地代码与社区资源保持“语言相通”,方便你学习、调试和优化。当你熟悉了这套本地模式后,如果想进行大规模历史回测,也可以相对平滑地迁移到聚宽的在线回测系统,或者其他的回测框架(如backtrader,Zipline等)中。

注意jqdatasdk需要注册聚宽账号并获取认证凭证才能使用,它有每日调用次数的限制(免费版足够个人学习研究)。这意味着它不适合超高频率(如秒级、分钟级)的海量数据调用场景,但对于日级、周级的选股策略开发和验证,完全绰绰有余。

3. 环境搭建与核心工具详解

3.1 Python环境与必要库的安装

工欲善其事,必先利其器。一个干净、独立的Python环境是开始一切项目的好习惯,它能避免不同项目间的库版本冲突。我强烈推荐使用condavenv来创建虚拟环境。

# 使用 conda 创建环境(假设你安装了Anaconda或Miniconda) conda create -n quant_env python=3.8 conda activate quant_env # 或者使用 venv python -m venv quant_env # Windows 激活 quant_env\Scripts\activate # Linux/Mac 激活 source quant_env/bin/activate

环境激活后,开始安装核心库。除了jqdatasdk,我们还会用到数据分析三件套:

pip install jqdatasdk pandas numpy
  • pandas: 数据处理的基石,我们的股票数据几乎都会以DataFrame的形式存在和操作。
  • numpy: 提供高效的数值计算,在计算指标(如收益率、均值)时常用。

有时候,为了数据可视化,我们可能还会安装matplotlibseaborn,但在这个纯选股项目中不是必须的。

3.2 jqdatasdk 的配置与认证

安装好后,第一件事就是认证。你需要先去聚宽官网注册账号,然后在“我的账号” -> “认证信息”中查看你的username(通常是手机号或邮箱)和password

在你的Python脚本或Jupyter Notebook的开头,你需要这样进行认证:

import jqdatasdk as jq import pandas as pd import numpy as np from datetime import datetime, timedelta # 替换为你的聚宽账号和密码 username = ‘your_username‘ password = ‘your_password‘ # 进行认证 jq.auth(username, password) # 认证成功后,可以尝试获取一条数据测试 try: # 获取贵州茅台的最新股价 price = jq.get_price(‘000001.XSHE‘, end_date=datetime.now(), count=1, fields=[‘close‘]) print(‘认证成功!最新数据示例:\n‘, price) except Exception as e: print(‘认证或数据获取失败:‘, e)

认证成功后,这个会话在有效期内(通常一天)都可以调用数据接口。记得千万不要将你的账号密码直接硬编码在提交到公开仓库的代码里!一个更好的做法是使用环境变量或配置文件来管理敏感信息。

# 示例:使用环境变量(推荐) import os username = os.environ.get(‘JQ_USERNAME‘) password = os.environ.get(‘JQ_PASSWORD‘) jq.auth(username, password)

3.3 理解聚宽的数据结构:证券代码与查询字段

这是新手最容易困惑的地方之一。聚宽有自己的证券代码命名规范,并且不同的数据有不同的查询字段(fields)。

证券代码格式股票代码.交易所后缀

  • 000001.XSHE: 平安银行,深交所。XSHE代表深圳证券交易所。
  • 600519.XSHG: 贵州茅台,上交所。XSHG代表上海证券交易所。
  • 对于指数,也有类似格式,如000300.XSHG代表沪深300指数。

在编写策略时,我们通常先获取一个股票池的列表。jqdatasdk提供了get_all_securities函数,但更常用的是get_index_stocks来获取某个指数的成分股,这样更符合实际投资场景。

# 获取沪深300指数的最新成分股列表 stock_list = jq.get_index_stocks(‘000300.XSHG‘) print(f‘沪深300成分股数量:{len(stock_list)}‘) print(stock_list[:5]) # 打印前5个

查询字段:不同的函数需要指定不同的fields。例如:

  • get_price: 常用[‘open‘, ‘close‘, ‘high‘, ‘low‘, ‘volume‘, ‘money‘]
  • get_fundamentals: 查询财务数据,字段极其丰富,如pe_ratio(市盈率),market_cap(总市值),circulating_market_cap(流通市值) 等。务必查阅官方文档来找到你需要的具体字段名。

理解并熟练运用这些代码和字段,是写好策略的第一步。我建议新建一个代码片段专门用来测试和熟悉各个接口的返回格式,这会让你后续的编码顺畅很多。

4. 策略代码的逐行实现与解析

现在,我们进入核心环节,将之前设计的“市值-估值-动量”三层过滤逻辑,转化为可执行的代码。我会把代码分成几个功能模块,并详细解释每一行代码的作用和背后的考量。

4.1 模块一:数据获取与预处理函数

首先,我们封装一个函数,用于批量获取股票在某个日期的市值和市盈率数据。财务数据通常有发布延迟,所以我们查询的日期(query_date)最好比当前日期早一些,确保数据已经更新。

def get_fundamental_data(stock_list, query_date): """ 批量获取指定日期股票的总市值和市盈率(PE) 参数: stock_list: list, 股票代码列表 query_date: datetime.date 或 str, 查询日期 返回: pd.DataFrame, 包含股票代码、总市值、市盈率的 DataFrame """ # 构建查询对象,q 是 jqdatasdk 的查询入口 q = jq.query( jq.valuation.code, # 股票代码 jq.valuation.market_cap, # 总市值(单位:万元) jq.valuation.pe_ratio # 市盈率(PE) ).filter( jq.valuation.code.in_(stock_list) # 过滤出我们股票池里的股票 ) # 执行查询,获取指定日期的数据 df_fund = jq.get_fundamentals(q, query_date) # 重命名列,让它们更易懂 df_fund = df_fund.rename(columns={‘code‘: ‘stock_code‘, ‘market_cap‘: ‘total_market_cap‘, ‘pe_ratio‘: ‘pe‘}) # 设置股票代码为索引,方便后续合并数据 df_fund.set_index(‘stock_code‘, inplace=True) return df_fund

关键点解析

  1. jq.query(...).filter(...)是聚宽财务数据查询的标准范式,非常像SQL。jq.valuation是一个包含众多估值指标的表。
  2. market_cap的单位是万元,这是聚宽数据的统一单位,在后续计算阈值时要注意。
  3. get_fundamentals的第二个参数是日期,它返回的是该日期所能获取到的最新财务数据。对于市值和PE,通常是每日更新。

接下来,封装一个获取股票价格数据,用于计算动量的函数。

def get_price_data_for_momentum(stock_list, end_date, lookback_days=20): """ 获取股票价格数据,用于计算过去一段时间的涨跌幅(动量) 参数: stock_list: list, 股票代码列表 end_date: datetime.date 或 str, 计算截止日期 lookback_days: int, 回溯的天数(交易日) 返回: pd.DataFrame, 索引为股票代码,列包含 ‘start_price‘, ‘end_price‘, ‘return‘ """ # 计算开始日期 start_date = end_date - timedelta(days=lookback_days * 2) # 多取一些天数,确保覆盖足够的交易日 # 初始化一个字典来存储结果 momentum_data = {} for stock in stock_list: try: # 获取股票在指定时间段内的日线收盘价 price_df = jq.get_price(stock, start_date=start_date, end_date=end_date, fields=[‘close‘]) if price_df.empty or len(price_df) < lookback_days: # 如果数据不足,则跳过该股票(可能是新股或停牌) continue # 获取回溯期开始和结束的收盘价 # 注意:get_price 返回的数据按日期升序排列 start_price = price_df[‘close‘].iloc[-lookback_days] # 第 -lookback_days 天(20天前) end_price = price_df[‘close‘].iloc[-1] # 最后一天(end_date) # 计算期间收益率 period_return = (end_price - start_price) / start_price momentum_data[stock] = { ‘start_price‘: start_price, ‘end_price‘: end_price, ‘return‘: period_return } except Exception as e: # 记录错误,但继续处理其他股票 print(f“获取 {stock} 价格数据时出错:{e}“) continue # 将字典转换为 DataFrame df_momentum = pd.DataFrame.from_dict(momentum_data, orient=‘index‘) df_momentum.index.name = ‘stock_code‘ return df_momentum

关键点解析

  1. 我们采用循环获取单只股票数据的方式,而不是批量获取。这是因为get_price支持单只股票的多日数据获取,且逻辑清晰。对于几百只股票的池子,这样操作在性能上是可以接受的。如果股票池很大(如上万只),则需要考虑更高效的批量获取方式或向量化计算。
  2. start_date我们故意多取了一些天数(lookback_days * 2),这是为了确保即使中间有节假日,我们也能取到足够lookback_days个交易日的数据,这是一种稳健的做法。
  3. 对每只股票进行了try-except异常捕获,并检查数据是否足够。这是因为股票可能在新股上市、长期停牌等情况下,数据不完整。一个健壮的策略必须能处理这些边缘情况,而不是直接崩溃。

4.2 模块二:三层过滤逻辑的实现

有了数据,我们就可以实现过滤逻辑了。我们将每一步过滤的结果都保存下来,方便调试和查看中间结果。

def apply_filters(df_fund, df_momentum): """ 应用三层过滤条件,筛选符合条件的股票 参数: df_fund: 基本面DataFrame,来自 get_fundamental_data df_momentum: 动量DataFrame,来自 get_price_data_for_momentum 返回: pd.DataFrame, 最终符合条件的股票列表及关键指标 """ # 第一步:合并基本面数据和动量数据 # 使用 inner join,只保留两者都有的股票(即既有财务数据又有价格数据的股票) df_combined = pd.merge(df_fund, df_momentum, left_index=True, right_index=True, how=‘inner‘) print(f“合并后初始股票数量:{len(df_combined)}“) # 第二步:市值过滤 - 选择市值大于100亿元(即1,000,000万元)的股票 df_size_filtered = df_combined[df_combined[‘total_market_cap‘] > 1000000].copy() print(f“市值过滤后(>100亿)股票数量:{len(df_size_filtered)}“) # 第三步:估值过滤 - 选择市盈率大于0且小于30的股票 # PE > 0 是为了排除亏损企业(PE为负) df_pe_filtered = df_size_filtered[(df_size_filtered[‘pe‘] > 0) & (df_size_filtered[‘pe‘] < 30)].copy() print(f“估值过滤后(0<PE<30)股票数量:{len(df_pe_filtered)}“) # 第四步:动量过滤 - 选择过去20个交易日收益率为正的股票 df_momentum_filtered = df_pe_filtered[df_pe_filtered[‘return‘] > 0].copy() print(f“动量过滤后(20日收益>0)股票数量:{len(df_momentum_filtered)}“) # 对最终结果进行排序,例如按市值从大到小排序 df_final = df_momentum_filtered.sort_values(by=‘total_market_cap‘, ascending=False) # 可以添加一些衍生列,方便阅读 df_final[‘total_market_cap_billion‘] = df_final[‘total_market_cap‘] / 100000 # 转换为亿元 df_final[‘return_pct‘] = df_final[‘return‘] * 100 # 转换为百分比 return df_final

关键点解析

  1. pd.merge是合并两个DataFrame的关键操作。left_index=Trueright_index=True表示我们用索引(股票代码)进行合并。how=‘inner‘表示取交集,这是最严格的方式,确保后续分析的数据完整性。
  2. 在每次过滤后,我们都使用了.copy()来创建数据副本。这是一个好习惯,可以避免后续操作对原始数据的误修改,也避免了 Pandas 可能产生的SettingWithCopyWarning警告。
  3. 过滤条件使用了布尔索引,这是 Pandas 筛选数据的标准且高效的方式。
  4. 我们在每个过滤步骤后都打印了股票数量,这就像给漏斗的每一层都装了个计数器,能清晰看到每一层筛掉了多少股票,对于理解和调试策略逻辑至关重要。
  5. 最后,我们添加了市值(亿元)和收益率(百分比)的衍生列,让输出结果更直观。排序则让结果更有条理,通常我们会关注市值最大的那些标的。

4.3 模块三:主程序流程与结果输出

最后,我们把所有模块串联起来,形成一个完整的脚本。

def main(): """ 主函数:执行完整的选股流程 """ # 1. 设置参数 query_date = ‘2023-10-27‘ # 假设我们分析这个日期的数据 query_date_dt = datetime.strptime(query_date, ‘%Y-%m-%d‘).date() lookback_days = 20 # 2. 获取股票池(这里以沪深300成分股为例) print(“正在获取股票池...“) index_code = ‘000300.XSHG‘ stock_pool = jq.get_index_stocks(index_code, date=query_date) print(f“初始股票池({index_code}成分股)数量:{len(stock_pool)}“) # 3. 获取基本面数据 print(“\n正在获取基本面数据(市值、PE)...“) df_fundamentals = get_fundamental_data(stock_pool, query_date) # 4. 获取动量数据 print(“\n正在计算动量数据(20日收益率)...“) df_momentum = get_price_data_for_momentum(stock_pool, query_date_dt, lookback_days) # 5. 应用过滤条件 print(“\n开始应用筛选条件...“) final_stocks_df = apply_filters(df_fundamentals, df_momentum) # 6. 输出结果 print(“\n“ + “=“*50) print(“选股策略执行完毕!“) print(f“最终符合条件的股票数量:{len(final_stocks_df)}“) print(“=“*50 + “\n“) if not final_stocks_df.empty: # 打印前10只股票详情 print(“前10只符合条件的股票详情:“) # 选择要展示的列 display_columns = [‘total_market_cap_billion‘, ‘pe‘, ‘return_pct‘, ‘start_price‘, ‘end_price‘] print(final_stocks_df[display_columns].head(10).to_string(float_format=“%.2f“)) # 也可以将结果保存到CSV文件,方便后续分析或导入到其他软件 output_file = f‘selected_stocks_{query_date}.csv‘ final_stocks_df.to_csv(output_file, encoding=‘utf-8-sig‘) # 使用utf-8-sig避免中文乱码 print(f“\n完整结果已保存至文件:{output_file}“) else: print(“警告:没有股票满足所有筛选条件!“) # 7. (可选)简单统计摘要 if not final_stocks_df.empty: print(“\n--- 筛选结果统计摘要 ---“) print(f“平均市值(亿元): {final_stocks_df[‘total_market_cap_billion‘].mean():.2f}“) print(f“平均市盈率(PE): {final_stocks_df[‘pe‘].mean():.2f}“) print(f“平均20日收益率(%): {final_stocks_df[‘return_pct‘].mean():.2f}“) print(f“最大20日收益率(%): {final_stocks_df[‘return_pct‘].max():.2f}“) print(f“最小20日收益率(%): {final_stocks_df[‘return_pct‘].min():.2f}“) if __name__ == “__main__“: main()

这个main函数清晰地勾勒出了整个策略的执行流程:设定日期、获取股票池、抓取数据、执行过滤、输出结果。它就像一个自动化流水线,你只需要调整入口的参数(比如query_date,index_code, 过滤阈值),就能得到不同市场环境下的选股列表。

5. 策略优化、扩展与实战心得

一个能跑通的策略只是起点。要让策略真正具备参考价值,甚至产生阿尔法,我们还需要深入思考如何优化和扩展它。这里分享几个我实践中总结的方向和心得。

5.1 参数优化与敏感性分析

我们策略中的1000000(100亿市值)、30(PE上限)、20(动量周期)这些数字,都不是金科玉律。它们需要根据不同的市场风格(牛市、熊市、震荡市)和不同的投资目标进行调整。

如何进行参数优化?一个朴素但有效的方法是进行网格搜索(Grid Search)。例如,我们可以测试市值阈值在 [50亿, 100亿, 200亿],PE上限在 [20, 25, 30, 35],动量周期在 [10, 20, 30] 的所有组合。对于每一组参数,在历史的一段时期内(例如过去3年)按月或按周运行策略,统计每次选股后这些股票在未来一段时间(例如未来1个月)的平均收益、胜率(跑赢基准的比例)、最大回撤等指标。

# 伪代码示例:参数扫描循环框架 市值阈值列表 = [500000, 1000000, 2000000] # 单位:万元 pe上限列表 = [20, 25, 30, 35] 动量周期列表 = [10, 20, 30] 结果记录 = [] for 市值 in 市值阈值列表: for pe in pe上限列表: for 周期 in 动量周期列表: # 在历史多个时间点运行策略 历史收益列表 = [] for 日期 in 历史日期序列: stocks = 运行策略(日期, 市值阈值=市值, pe上限=pe, 动量周期=周期) 未来收益 = 计算这些股票在未来N天的平均收益 历史收益列表.append(未来收益) # 计算该参数组的综合表现 平均收益 = np.mean(历史收益列表) 收益波动 = np.std(历史收益列表) 结果记录.append({‘市值阈值‘: 市值, ‘PE上限‘: pe, ‘动量周期‘: 周期, ‘平均收益‘: 平均收益, ‘波动‘: 收益波动}) # 找出表现最好的参数组

这个过程计算量较大,但能帮你理解策略对参数的敏感度。切记,在历史数据上表现最优的参数,在未来不一定有效。要警惕过拟合(Overfitting)。一个稳健的策略,其表现应对参数的小幅变动不敏感,且在样本外测试(例如用最近一年的数据测试之前三年优化出的参数)中仍有不错表现。

5.2 因子扩展与逻辑深化

我们的三层过滤只是入门。聚宽jqdatasdk提供了上百个财务和估值指标,你可以轻松地引入更多因子,构建更复杂的模型。

  • 质量因子:引入净资产收益率(ROE)、总资产收益率(ROA)、毛利率等,筛选盈利能力强的公司。jq.income_statementjq.balance_sheet表中可以找到这些数据。
  • 成长因子:计算营业收入增长率、净利润增长率,寻找高成长性标的。这需要获取多个报告期的数据进行计算。
  • 技术因子:除了简单的区间收益率,可以引入 RSI(相对强弱指数)、MACD(异同移动平均线)、布林带等技术指标,这些可以通过ta-lib库结合价格数据计算。
  • 风险因子:计算股票的历史波动率、Beta值(相对于市场的敏感度),用于控制组合风险。

引入新因子后,策略逻辑可以从简单的“与”过滤,升级为打分卡模型回归模型

  • 打分卡:对每个因子,根据股票在该因子上的表现进行排序打分(例如,PE最低的10%得10分,次低的10%得9分,以此类推),然后将所有因子的分数加权求和,选出总分最高的股票。
  • 回归模型:使用历史数据,建立因子对未来收益的线性或非线性回归模型,用模型预测股票的预期收益,并选择预期收益最高的。

5.3 回测与绩效评估框架搭建

选股策略的终极检验是历史回测。虽然jqdatasdk本身不提供回测引擎,但我们可以基于它获取的数据,搭建一个简单的回测框架。

一个最简单的回测思路是:

  1. 确定回测周期:例如 2018-01-01 到 2023-10-27。
  2. 确定调仓周期:例如每月第一个交易日调仓。
  3. 模拟交易:在每个调仓日,运行我们的选股策略,得到股票列表。假设我们等权重买入这些股票,并卖出不再列表中的原有持仓。
  4. 计算净值曲线:记录每日投资组合的总市值变化,并与基准(如沪深300指数)进行比较。
  5. 计算绩效指标:年化收益率、夏普比率、最大回撤、胜率、信息比率等。
# 一个极简的回测循环框架示例 初始资金 = 1000000 当前资金 = 初始资金 持仓 = {} # 字典,记录 {股票代码: 持仓股数} 净值序列 = [1.0] # 归一化净值,从1开始 基准净值序列 = [1.0] # 基准净值 for 调仓日期 in 所有调仓日列表: # 1. 卖出不在新名单中的股票 selected_stocks = 运行选股策略(调仓日期) # 返回股票代码列表 for stock in list(持仓.keys()): if stock not in selected_stocks: 卖出价格 = 获取调仓日收盘价(stock) 当前资金 += 持仓[stock] * 卖出价格 del 持仓[stock] # 2. 等权重买入新名单中的股票 if selected_stocks: 每只股票分配资金 = 当前资金 / len(selected_stocks) for stock in selected_stocks: 买入价格 = 获取调仓日收盘价(stock) 可买股数 = int(每只股票分配资金 / 买入价格 / 100) * 100 # 假设A股按手买 持仓[stock] = 持仓.get(stock, 0) + 可买股数 当前资金 -= 可买股数 * 买入价格 # 3. 计算当日组合总市值和净值 当日总市值 = 当前资金 for stock, shares in 持仓.items(): 当日价格 = 获取当日收盘价(stock) 当日总市值 += shares * 当日价格 当日净值 = 当日总市值 / 初始资金 净值序列.append(当日净值) # 4. 计算基准净值 基准指数价格 = 获取基准指数当日收盘价(‘000300.XSHG‘) ... # 计算基准净值

搭建这样一个框架后,你才能客观地回答:“我的策略在过去几年真的能赚钱吗?比简单持有指数强吗?风险有多大?”

5.4 实战中的注意事项与避坑指南

  1. 数据日期对齐问题(巨坑!):财务数据的发布日期远晚于报告期。比如2023年三季报,可能在10月底才发布。如果你在10月1日用query_date=‘2023-10-01‘去查询pe_ratio,你拿到的是基于当时已发布的最新财报计算出的PE,很可能是2023年中报的数据。这意味着你的“实时”选股实际上用的是滞后的财务信息。在回测中,必须模拟这种滞后,避免使用未来数据(Look-ahead Bias)。通常的解决方法是,在T日选股时,使用T-1个交易日所能获得的最新财务数据(聚宽函数中的date参数本身就代表“获取该日期及之前的最新数据”)。
  2. 停牌与退市处理:我们的get_price_data_for_momentum函数简单跳过了数据不足的股票,这处理了停牌。但对于退市股,在历史回测中如果买入了,需要在后续计算中将其价值归零。在实际操作中,需要更精细的处理逻辑。
  3. 交易成本:上述简单回测忽略了印花税、佣金和滑点。一个严肃的策略必须考虑这些成本,它们会显著侵蚀收益,尤其是对于换手率较高的策略。
  4. 流动性考量:我们虽然用市值过滤了,但更精细的做法是结合日均成交金额(money字段)进行过滤,避免买入成交量极低、难以按理想价格成交的股票。
  5. 代码健壮性:网络请求可能失败,数据可能缺失,API可能有调用频率限制。在生产环境中,你的代码需要包含重试机制、错误日志记录和告警功能。

这个基于聚宽jqdatasdk的简单选股策略项目,就像给你了一套乐高积木的基础颗粒和搭建手册。手册教你搭出了一座稳固的小房子(三层过滤策略),但房子的外观、内部结构、能否变成城堡或飞船,完全取决于你如何组合、扩展这些颗粒。真正的量化工作,大部分时间都花在数据清洗、逻辑验证、风险控制和绩效分析上,而不仅仅是想出那个“神奇”的选股公式。从这个项目出发,去尝试修改参数、添加因子、搭建回测,你才会真正触摸到量化投资的门道。

← 返回列表