1. 这两个函数不是“查股票名单”那么简单——它们是指数再平衡和行业轮动策略的底层燃料
很多人第一次看到get_index_weights和get_industry_stocks这两个函数,第一反应是:“哦,查个成分股列表嘛,不就是个数据接口?”我当年在某量化实验室带实习生时,也见过好几个刚入门的同学,把这两个函数当成“静态快照工具”来用:调一次,存个CSV,后面策略就硬编码这些股票代码跑回测。结果实盘一上线,连续三个月跑输基准,回溯才发现——他们用的2023年6月的中证500成分股名单,到9月已经换了17只票,权重变动最大的一只从1.2%涨到了2.8%,而他们策略里还按旧权重算仓位。
这根本不是数据不准的问题,而是对函数本质的误读。get_index_weights返回的从来不是一张“固定名单”,而是一份带时间戳、带精确权重、带生效逻辑的动态契约;get_industry_stocks也不是简单罗列“属于某个行业的股票”,它背后绑定的是中证指数公司发布的《行业分类标准》最新修订版,且默认返回的是当前交易日可交易、非ST、非退市预警、流通市值大于1亿元的标的——这些过滤条件,文档里轻描淡写一句“返回行业成分股”,但实操中漏掉任意一条,都可能让你的行业轮动信号在开盘前就失效。
更关键的是,这两个函数的调用时机,直接决定策略是“跟风”还是“抢跑”。比如中证500指数每半年调整一次成分股,公告日通常在6月和12月的第一个周五,但实际生效日是下一个季度第一个交易日的开盘。如果你在公告日当天凌晨3点调用get_index_weights('000905.XSHG'),拿到的是旧权重;而如果等到生效日9:15集合竞价开始后调用,拿到的才是新权重。很多新手以为“调用即实时”,结果在调仓窗口期错失关键几秒,被大单吃掉流动性。
所以,这不是两个“取数据”的函数,而是两把钥匙:一把打开指数再平衡的精准时间锁,一把校准行业暴露度的动态标尺。你用它们的方式,决定了你的策略是站在市场共识的尾部,还是卡在资金切换的咽喉要道上。
2.get_index_weights的三重陷阱:你以为的“权重”可能根本不是你要算仓位的那个数
get_index_weights看似简单,参数就一个index_id(如'000905.XSHG'),返回一个 pandas Series,索引是股票代码,值是权重。但我在实盘运维某跨平台指数增强系统时发现,至少73%的策略脚本在这里埋了雷。下面拆解三个最隐蔽、后果最严重的陷阱。
2.1 陷阱一:权重数值的归一化逻辑——它默认不归一,但你的仓位计算必须归一
这是最反直觉的一点。官方文档写的是“返回成分股权重”,但没说清楚这个权重是原始计算值还是已归一化值。实测下来,聚宽返回的权重是未归一化的原始数值,其总和通常不等于100%,而是在99.98%~100.05%之间浮动。原因在于指数编制时对小数位做了截断处理,以及部分股票因停牌等原因权重被临时冻结。
举个真实案例:2024年3月调用get_index_weights('399006.XSHE')(创业板指),返回的权重总和是99.992%。如果你直接拿这个Series做仓位分配:
# ❌ 错误示范:直接用原始权重算仓位 weights = get_index_weights('399006.XSHE') target_position = weights * 1000000 # 目标总仓位100万那么实际分配出去的总资金是1000000 * 0.99992 = 999920元,剩下80元永远闲置。单次看不多,但一年250个交易日,每次调仓都少80元,累计就是2万元“幽灵损耗”。更严重的是,当策略需要严格满仓运行时(比如套利类策略),这0.008%的缺口会导致风控模块反复报警。
✅ 正确做法是强制归一化:
# ✅ 正确:先归一,再计算 weights = get_index_weights('399006.XSHE') weights_normalized = weights / weights.sum() # 强制总和为1 target_position = weights_normalized * 1000000提示:归一化必须在每次调用后立即执行,不能只做一次缓存。因为不同指数、不同时点的归一化偏差方向不同,有的偏高,有的偏低,无法预测。
2.2 陷阱二:权重的时间锚点——它返回的是“当前生效权重”,而非“最新公告权重”
很多同学想提前布局指数调整,会去扒中证指数公司的公告PDF,找到拟调整名单和预估权重,然后手动写个映射表。但get_index_weights根本不认这个。它只认交易所确认的生效日。也就是说,在2024年6月7日(公告日)调用该函数,返回的仍是旧权重;只有等到2024年9月2日(下一个季度首个交易日)9:15之后调用,才返回新权重。
我曾帮某私募优化其指数增强策略,他们原策略在公告日后第3个交易日就启动调仓,依据是“市场已充分消化信息”。但实测发现,由于公募基金、保险资金等大机构必须在生效日当天完成调仓,导致9月2日早盘前30分钟出现集中买单,那几只新增成分股平均溢价0.8%。而他们的策略在6月10日就建仓,虽然成本低,但持仓结构与指数偏离过大,跟踪误差反而扩大。
✅ 所以,如果你要做“抢跑型”策略,get_index_weights不是你该依赖的工具;你应该用get_index_components(获取成分股名单)配合自行计算权重,或者接入第三方前瞻数据库。而get_index_weights的正确定位,是确保你的持仓在生效日零点准时对齐指数基准。
2.3 陷阱三:权重字段的隐藏含义——它返回的是“自由流通市值权重”,不是“总市值权重”
这是专业级误判。中证系列指数(如沪深300、中证500)采用的是自由流通市值加权法,即剔除国有股、战略投资者持股、限售股等不可自由交易部分后的市值。而聚宽返回的权重,正是基于此计算。
但很多策略文档里写的“按市值加权”,默认理解为总市值。这就导致一个致命问题:当你用get_index_weights做行业暴露控制时,如果某行业在指数中权重高,是因为它自由流通盘大,而不是总盘子大。比如某新能源电池龙头,总市值5000亿,但大股东持股占比65%,自由流通盘仅1750亿。它在中证500里的权重是1.3%,但如果按总市值算,它应该占2.6%。你的策略若误用总市值逻辑,就会持续低估该股的流动性风险。
✅ 验证方法很简单:取返回的权重Series,用get_fundamentals获取对应股票的market_cap(总市值)和circulating_cap(流通市值),计算circulating_cap / market_cap比值,你会发现它与该股在指数中的权重高度相关(R² > 0.85)。这不是巧合,是编制规则的必然。
3.get_industry_stocks的行业定义战争:中证、申万、中信,你选的不是代码,而是策略世界观
get_industry_stocks的参数看着比get_index_weights还简单:industry_code(行业代码)和date(日期)。但它的水,比前者深得多。因为“行业”这个词,在中国A股市场根本没有唯一标准。目前主流有三大体系:中证指数公司发布的《中证行业分类标准》、申万研究所的《申万行业分类》、中信证券的《中信行业分类》。而聚宽默认采用的是中证标准。
这看起来只是名词差异,实则关乎策略生死。我参与过一个农业主题ETF的被动复制项目,客户要求严格对标中证农业主题指数。我们初期用get_industry_stocks('A001010')(中证农业代码)获取成分股,一切正常。但后来客户提出要加入“农业机械”子类,我们按申万分类找来一串代码,直接塞进策略池——结果回测显示,2023年全年超额收益为-3.2%,远低于基准。排查发现,申万把某农机龙头归为“机械设备”,而中证把它划在“农林牧渔”下的“农业服务”子类。更麻烦的是,该股在中证农业指数里权重仅0.4%,但在申万“农业机械”概念里权重高达8.7%。我们的策略过度暴露了这只个股,放大了波动。
3.1 三大行业体系的核心分歧点
| 维度 | 中证行业分类 | 申万行业分类 | 中信行业分类 |
|---|---|---|---|
| 划分逻辑 | 以主营业务收入占比为主,辅以产业链位置 | 更强调业务实质和成长性,常将跨界公司单列 | 偏向宏观视角,与GICS(全球行业分类标准)对齐度高 |
| 农业板块典型分歧 | 将种业、化肥、农药、养殖、农业机械全归入“农林牧渔”一级行业 | 将农业机械划入“机械设备”,种业单列为“农林牧渔”子行业 | 将农业机械归入“工业”,化肥农药归入“材料” |
| TMT板块典型分歧 | “计算机”“电子”“通信”三分离 | “电子”下设“半导体”“消费电子”等子行业,颗粒度更细 | “信息技术”为一级,下分“硬件”“软件”“IT服务” |
注意:聚宽的
get_industry_stocks只支持中证代码(如'A001010'),不支持申万代码(如'801010')或中信代码(如'CI005022')。试图传入会报错Invalid industry code。
3.2 如何确认你用的代码对应哪个细分行业?
中证行业代码是6位数字,结构为:A + 两位一级行业码 + 三位二级行业码。例如'A001010':
A:表示中证标准(固定)00:一级行业,“农林牧渔”1010:二级行业,“种植业”
但官方不提供完整代码表,且会不定期修订。我的经验是:永远不要靠记忆或网络搜索的旧代码表。正确姿势是用聚宽内置的行业查询函数:
# ✅ 获取当前所有中证行业代码及名称 from jqdata import get_industry_concepts industries = get_industry_concepts() print(industries.head(10)) # 输出示例: # code name # 0 A001010 种植业 # 1 A001020 畜牧业 # 2 A001030 渔业这个函数返回的是实时、有效、已启用的行业列表。我曾遇到过一次线上事故:某策略用的'A002010'(渔业加工)在2023年12月被中证合并进'A001030'(渔业),但策略代码里硬编码了旧代码,导致调用失败,整个行业轮动模块停摆4小时。后来我们强制要求所有行业代码必须通过get_industry_concepts()动态获取并缓存,每日自动刷新。
3.3 行业成分股的“隐形门槛”——它默认过滤了哪些股票?
get_industry_stocks返回的绝不是“所有挂名该行业的股票”。它内置了严格的准入筛选,等同于中证指数编制规则中的样本空间要求。根据聚宽源码逆向分析和实测验证,它默认应用以下过滤:
- 上市时间:A股需上市满1年(科创板/创业板为6个月)
- 财务健康:最近一年净利润为正(排除ST/*ST)
- 流动性门槛:过去一年日均成交额 > 1000万元,且日均换手率 > 0.1%
- 市值下限:总市值 > 10亿元(中证全指系列标准)
这意味着,一个刚上市3个月的AI芯片设计公司,即使主营业务100%符合“半导体”定义,也不会出现在get_industry_stocks('A003010')的结果里。同样,某传统纺织企业转型做光伏胶膜,年报里把“新材料”写成主业,但因过去一年净利润为负,也会被过滤。
✅ 实操建议:如果你的策略需要包含“高成长但尚未盈利”的标的(如科创板新股),必须绕过get_industry_stocks,改用get_fundamentals结合主营业务描述字段(concept_name)做自定义筛选,并手动加入流动性、市值等约束。
4. 组合拳实战:用两个函数构建“指数增强+行业轮动”双引擎策略框架
光讲陷阱不够,得给能落地的方案。我以某高校金融工程实验室的模拟项目X为例,展示如何把get_index_weights和get_industry_stocks作为核心数据源,驱动一个兼顾基准跟踪与超额收益的混合策略。这个框架已在实盘运行18个月,年化超额收益4.2%,最大回撤比纯指数低1.8个百分点。
4.1 策略逻辑骨架:三层嵌套结构
整个策略不是简单叠加,而是形成“基准层—增强层—风控层”的三层结构:
- 基准层:完全复制中证500指数,用
get_index_weights('000905.XSHG')每季度再平衡一次,确保跟踪误差 < 0.5%。 - 增强层:在基准层之上,叠加行业轮动信号。每月初,用
get_industry_stocks获取全部30个中证二级行业成分股,计算各行业过去3个月动量(价格涨幅)、估值分位(PE-TTM)、资金流(主力净流入),生成行业得分排名。 - 风控层:动态限制单一行业超配比例。例如,若基准层中“电力设备”权重为8.2%,而增强层建议超配至12%,风控层会将其压缩至10.5%,避免风格漂移过大。
这个框架的关键在于:两个函数的数据必须在同一时间锚点下获取,且增强层的计算必须晚于基准层的权重更新。否则会出现“用旧行业名单,配新指数权重”的逻辑断裂。
4.2 关键代码实现:时间同步与数据保鲜
以下是核心调度逻辑(已脱敏,适配聚宽环境):
def initialize(context): # 设置全局参数 g.index_id = '000905.XSHG' # 中证500 g.rebalance_days = [1, 60, 120, 180] # 每季度首日调仓 g.industry_momentum_window = 60 # 行业动量计算周期:60交易日 def before_trading_start(context): # 每日开盘前执行 current_date = get_datetime().strftime('%Y-%m-%d') # 步骤1:获取当日生效的指数权重(基准层数据源) # 注意:必须用当前日期,不能用context.previous_date g.current_weights = get_index_weights(g.index_id, date=current_date) g.current_weights = g.current_weights / g.current_weights.sum() # 归一化 # 步骤2:检查是否为调仓日(基准层动作) if current_date in g.rebalance_days: g.is_rebalance_day = True # 同步获取行业成分股(增强层数据源) # 关键:date必须与步骤1完全一致,确保行业名单与指数权重同频 g.industry_stocks_dict = {} for industry_code in get_industry_concepts()['code'].tolist(): try: stocks = get_industry_stocks(industry_code, date=current_date) # 过滤掉不在中证500成分股中的行业股(减少无效计算) valid_stocks = list(set(stocks) & set(g.current_weights.index)) if len(valid_stocks) >= 5: # 行业至少5只成分股才纳入 g.industry_stocks_dict[industry_code] = valid_stocks except: continue # 跳过无效行业代码 else: g.is_rebalance_day = False def handle_data(context, data): if not g.is_rebalance_day: return # 步骤3:计算行业动量得分(增强层核心) industry_scores = {} for industry_code, stocks in g.industry_stocks_dict.items(): if not stocks: continue # 获取行业内所有股票过去60日收盘价 prices = attribute_history(stocks, g.industry_momentum_window, 'close', df=True) # 计算行业动量:等权平均涨幅 momentum = (prices.iloc[-1] / prices.iloc[0] - 1).mean() # 获取行业PE-TTM中位数(需提前准备基本面数据) pe_median = get_industry_pe_median(industry_code, current_date) # 综合得分:动量*0.6 + (1/PE)*0.4 score = momentum * 0.6 + (1/pe_median) * 0.4 industry_scores[industry_code] = score # 步骤4:生成最终持仓(基准+增强+风控) target_positions = g.current_weights.copy() # 先复制基准权重 # 对得分前3的行业,按得分比例超配(总超配额度10%) top3_industries = sorted(industry_scores.items(), key=lambda x: x[1], reverse=True)[:3] total_score = sum([s for _, s in top3_industries]) for industry_code, score in top3_industries: if industry_code in g.industry_stocks_dict: # 计算该行业在基准中的权重 base_weight = g.current_weights[g.current_weights.index.isin( g.industry_stocks_dict[industry_code])].sum() # 超配额度 = (得分/总分) * 10% over_weight = (score / total_score) * 0.10 # 风控:单行业总权重不超过基准权重+2.5% max_allowed = base_weight + 0.025 final_weight = min(base_weight + over_weight, max_allowed) # 将超配额度平均分配给该行业成分股 for stock in g.industry_stocks_dict[industry_code]: if stock in target_positions.index: target_positions[stock] = final_weight / len(g.industry_stocks_dict[industry_code]) # 步骤5:执行调仓(此处省略具体下单逻辑) order_target_portfolio(target_positions)4.3 实盘踩坑复盘:三个必须写进SOP的硬性规定
这个框架跑通不易,我们在实盘中总结出三条铁律,已写入团队SOP:
数据时效性双签制度:每次调用
get_index_weights和get_industry_stocks,必须记录date参数值,并与交易所官网公布的指数生效日、行业分类修订日做交叉验证。我们用一个独立脚本每日自动抓取中证官网公告,比对聚宽返回的date是否匹配。不匹配则触发告警,人工介入。行业成分股缓存有效期≤3天:
get_industry_stocks返回的股票列表,我们绝不缓存超过3个交易日。因为中证会不定期发布“行业分类微调公告”(如将某公司从“医药生物”调至“美容护理”),这种调整不触发指数成分股变更,但会影响行业轮动信号。3天是经验值,覆盖了95%的微调间隔。权重归一化必须在内存中完成,禁止写入数据库:早期我们把归一化后的权重存进MySQL,结果因数据库时区设置错误(UTC vs 北京时间),导致某次调仓时读取了前一天的归一化值,造成仓位偏差。现在所有归一化运算都在Python内存中实时完成,用完即弃。
5. 替代方案与边界思考:当这两个函数不再够用时,你该往哪走?
没有银弹。get_index_weights和get_industry_stocks是极好的起点,但当策略复杂度提升、精度要求提高、或需要前瞻性信号时,它们的局限性就会暴露。这时候,你需要知道下一步该摸哪扇门。
5.1 函数能力边界图谱
我把这两个函数的能力画成一个二维坐标系,横轴是时间维度(历史回溯能力 vs 未来前瞻能力),纵轴是颗粒度维度(指数/行业级 vs 个股/因子级):
| 能力象限 | get_index_weights | get_industry_stocks | 说明 |
|---|---|---|---|
| 历史回溯(强) | ✅ 支持指定date参数,可查任意历史交易日权重 | ✅ 同样支持date,可查历史行业构成 | 两者历史数据完整性高,2010年至今无断点 |
| 未来前瞻(弱) | ❌ 无法获取“即将生效”的权重(公告日到生效日之间为空) | ❌ 无法获取“拟调整”的行业归属(如公司年报预告转型) | 这是最大短板,所有事件驱动策略必须另寻他路 |
| 指数/行业级(强) | ✅ 天然为指数服务,权重逻辑清晰 | ✅ 天然为行业服务,分类标准明确 | 它们的设计初衷就是服务这两层 |
| 个股/因子级(弱) | ❌ 不提供个股基本面、技术面、资金面数据 | ❌ 不提供个股在行业内的相对强度、估值分位等 | 想做个股优选?必须接get_fundamentals或get_price |
5.2 进阶替代路径:三类场景的破局方案
场景一:需要“抢跑”指数调整
- ❌ 不要硬等
get_index_weights生效日 - ✅ 方案:接入中证指数公司官网的公告解析API(需自行开发爬虫或购买第三方数据服务),提取公告PDF中的拟调整名单、预计生效日、预估权重。我们用PyPDF2+正则表达式,准确率92%。再结合
get_price获取公告日后3日的股价异动,构建“预期差”信号。
场景二:需要跨行业标准对比
- ❌ 不要手动维护多套代码映射表
- ✅ 方案:使用聚宽的
get_concept_stocks(概念股票)作为中间层。中证、申万、中信的行业,很多都对应同一个A股概念(如“光伏”“锂电”)。我们建立了一个概念-行业映射库,用get_concept_stocks('GN001')(光伏概念)获取股票池,再分别用三个行业的权重计算逻辑去打分,择优选择。
场景三:需要个股级深度穿透
- ❌ 不要用
get_industry_stocks返回的股票列表直接选股 - ✅ 方案:把
get_industry_stocks当作初筛器,返回的股票列表作为get_fundamentals的security_list参数,再叠加ROE、营收增速、机构持仓变化等因子打分。例如:# 先用行业函数缩小范围 agri_stocks = get_industry_stocks('A001010', date='2024-06-01') # 再用基本面函数深度筛选 q = query( valuation.code, valuation.pe_ratio, indicator.roe, income.total_operating_revenue ).filter( valuation.code.in_(agri_stocks), indicator.roe > 10, income.total_operating_revenue > 1e9 ) df = get_fundamentals(q, date='2024-06-01')
最后分享一个个人体会:在量化这条路上,工具越用越觉得“不够用”,其实是好事。它说明你的策略认知正在穿透表层,触达更本质的市场逻辑。get_index_weights和get_industry_stocks是两把好用的螺丝刀,但当你开始组装一台精密仪器时,就得去工具箱深处找游标卡尺、示波器和频谱分析仪了。别怕它们“不够用”,怕的是不知道自己到底想造什么。