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环境是开始一切项目的好习惯,它能避免不同项目间的库版本冲突。我强烈推荐使用 conda venv 来创建虚拟环境。

# 使用 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 : 提供高效的数值计算,在计算指标(如收益率、均值)时常用。

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

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

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐