
简介本资源是一套面向金融量化研究与Python编程实践的券商研报策略复现方案主要服务于计算机、人工智能、电子信息及金融工程等专业的高校师生与行业从业者解决研报逻辑落地难、因子构建缺实操、策略回测无框架等实际问题。压缩包共253个文件含40个可直接运行的.py源码、38个含完整推导与可视化分析的.ipynb笔记本、56份原始研报与学术文献PDF以及支撑回测的CSV数据集如factors_frame.csv、shibor_db.csv、industry_zx_l2.csv等整体151.45MB结构按专题组织便于分模块学习与二次开发。已有63人下载学习所有代码均通过功能测试覆盖因子合成、行业轮动、利率插值、期权定价、申万/中信行业映射等典型量化场景并附带清晰说明文档与数据字段注释支持初学者理解研报逻辑链也便于进阶用户快速拓展信号生成或替换底层数据源。 做量化的人案头少不了两类东西券商研报和Python脚本。研报负责给出“值得关注”的方向Python负责把方向变成可回测、可验证的结果。但凡是动手复现过券商研报量化策略的朋友大概率都有同一种体验研报看完觉得逻辑很顺代码一写就发现哪哪都是缺口调仓频率没写股票池没定义交易费用没提连“低估值”用的是PE还是PB都得靠猜。这篇就是想把我自己从“对着PDF抄公式”到“用Python搭一套能跑的复现流程”的真实路径拆出来聊聊券商研报量化策略的代码复现与实现方法。我会从研报的“隐性假设”讲起带你把文字翻译成策略参数搭出可回测的Python骨架再用一个小市值案例走完整条路最后告诉你结果对不上研报时到底该查哪里。1. 研报和代码之间隔着的“隐性假设”才是复现成败的分水岭好多第一次做复现的人会把研报里的策略段落当成完整需求文档。这是误解的开始。券商研报的本质是“观点输出”不是“代码交付”。你看到的策略描述往往是分析师研究完数据之后得出的结论至于背后有多少数据细节、行业假设甚至一些拍脑袋的参数都被压缩在几十行文字里了。1.1 研报里能直接转成代码的通常是这四类信息拿一篇常见的行业轮动策略报告举例里面大概率会出现这样的句子“以月度频率选取ROE连续三季提升且市盈率处于近五年40%分位以下的公司构建等权组合相对沪深300获取超额收益。”这句话能进代码的信息有四个调仓频率月度。选股条件ROE连续三季提升这是财务指标的时间序列条件。估值条件PE处于近五年40%分位以下这是时间序列分位数条件。组合构建等权比较基准是沪深300。这些信息可以转换为确定的函数和参数没有太多歧义。真正需要动手写代码的部分就是把这些文字变成“每天扫一遍、月末排序、按条件过滤、等权买入”的逻辑。但这只是复现的第一层后面还有更麻烦的东西。1.2 真正容易翻车的是“研报里没说”的部分研报里没说清楚的东西才是决定复现结果是否接近原文的隐形变量。我总结过一份高频差异来源清单交易成本研报常写“考虑交易成本后”具体是单边成本多少、有无滑点完全不透露。调仓时点说“月度调仓”但月末最后一天收盘价买还是月初开盘价买长期累积下来净值差异非常大。股票池口径写“全市场”实际大概率是剔除ST、剔除上市不满一年、剔除停牌股之后的口径。估值数据时点PE是动态还是静态财务数据用最新一期还是上年报处理方式不同因子值直接就是两套数据。复权口径前复权、后复权、不复权三套价格序列算出来的收益完全不一样。这些隐性假设如果不先想清楚代码逻辑写得再漂亮结果也可能和研报差得离谱。所以我现在复现任何一份研报第一件事不是打开Python去装akshare、tushare而是拿一张纸把研报里所有短句里的名词、数字、时间条件全部摘出来然后逐条标上三个状态可直接编码、需要假设、无法实现。这个过程看着笨实际上能帮你省掉后面好几次推倒重来。1.3 交易成本假设差异到底有多大算一笔账用券商报告里最常见的“月度调仓、等权持仓”策略来估算。假设每一次调仓都把全部持仓换掉换手率按100%算。单边费用包括佣金、印花税、滑点买卖加一起双边成本很容易到千分之二到千分之三。如果按千分之三算一年12次调仓额外成本大约是12乘以0.3%等于3.6%。换句话说一个年化收益10%的策略被交易成本吃完之后只剩6.4%如果研报按千分之一算、你按千分之三算光这一项就差了2.4个百分点。再加上市值因子、财务时点的差异最终复现结果“看着像两个策略”就完全解释得通了。提示交易成本参数建议用“双边加总”而不是“单边”来计算这个口径在研报里最容易混。2. 复现前先建一张“参数翻译表”避免反复返工我会在动手写代码之前先建一张“参数翻译表”这是过去一年里我觉得最值得养成的习惯。这张表的作用是把研报口语翻译成Python里可执行的参数定义同时记录我做的每一个假设方便后面翻旧账。2.1 参数翻译表的字段设计表的核心字段就五个研报原文、策略参数、Python变量名、参数值、备注。下面是我列过的一个真实样例研报原文策略参数Python变量/函数名参数值备注“按月调仓”调仓频率rebalance_freq“ME”月末最后一个交易日收盘“市盈率处于近五年40%分位以下”分位阈值pe_pct_threshold0.4按个股自身滚动5年窗口“剔除上市不满一年的股票”上市天数下限min_listing_days365不含上市首日“相对沪深300获取超额收益”基准benchmark“000300.SH”用全收益指数口径“考虑交易成本”双边成本cost_bps20单位为基点初始按0.2%参数翻译表做完了我会再把它塞进一个JSON配置文件后面所有代码都只读这个文件。这样别人拿到你的代码改配置就能跑不用在代码里翻常量。{ rebalance_freq: ME, pe_pct_threshold: 0.4, min_listing_days: 365, benchmark: 000300.SH, cost_bps: 20, stock_pool: all_a_share_ex_st }2.2 数据源选择与合规边界研报复现必然要面对数据问题。先泼一盆冷水千万别想着去爬券商研报PDF自动抽取策略文字目前并没有公开的、能稳定做这件事的工具而且研报内容本身有版权批量抓取是在给自己挖坑。我现在的做法是人工读研报把策略关键段落手工摘出来再用前面那张参数翻译表结构化。行情和财务数据可以放心用公开接口比如akshare、tushare pro、baostock拿到之后统一落成parquet或CSV放到本地目录回测阶段完全离线跑速度和安全性都可控。2.3 参数敏感度检查复现前先跑“三档参数”一份高质量研报的量化策略应该在参数上有一定“冗余度”。比如PE分位选股研报用40%分位我会顺手把30%、50%也跑一遍。如果三档参数下的年化收益和最大回撤差得不多说明这个策略逻辑本身是靠因子在赚钱不是靠某个恰好卡住的数字在赚钱如果分位数调整2个百分点结果就从年化40%变成-10%那这个复现大概率是白费工夫因为你永远不可能和研报作者用完全相同的数据库。这种“参数敏感度检查”不应该算加分项而应该算必备项。3. Python最小可回测系统从本地数据到绩效统计的完整骨架研报复现和自创策略不一样它要求你快速验证一个别人已经给定逻辑的想法。所以我不建议一上来就接事件驱动那套重框架先用一个足够简单、但能覆盖“数据—因子—回测—绩效”四个环节的最小系统跑通等后面逻辑变复杂了再升级。下面是我的骨架。3.1 数据层先保证数据干净再谈策略所有回测问题最后都会归结到数据问题。我会把数据清洗整理成统一格式date交易日期code股票代码close后复权收盘价volume成交量amount成交额mktcap总市值pe / pb估值指标roe / revenue_yoy财务指标以akshare为例拉取A股日线行情的代码很简单import akshare as ak df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20180101, end_date20241231, adjustqfq )但我要提醒你adjustqfq前复权适合看K线不适合直接做回测。前复权价格会随着每次除权除息而整体调整历史价格导致同一只股票在不同时间点拉出来的历史价格是变的。做量化回测更稳妥的做法是拿“后复权价格”或者“不变权的价格除权除息事件”自己重算。真实复现时我通常选择后复权价格并在参数表里把复权口径明确写清楚。3.2 因子层把研报条件写成可测试的函数每个策略条件都应该是一个独立函数输入DataFrame输出一个信号列。这样既可以单独调试也能组合进回测。举例研报说“PE低于个股近五年40%分位”代码可以写成import pandas as pd def calc_pe_ts_signal(daily: pd.DataFrame, lookback: int 5, pct: float 0.4) - pd.DataFrame: df daily.copy() df df.sort_values([code, date]) # 滚动5年窗口按自然日近似252*5天 df[pe_rank_ts] df.groupby(code)[pe].transform( lambda x: x.rolling(windowlookback * 252, min_periods60).rank(pctTrue) ) df[pe_signal] (df[pe_rank_ts] pct).astype(int) return df这个函数的核心是groupby(code)[pe].rolling().rank(pctTrue)它是在每只股票内部做时间序列分位。研报里写“处于近五年40%分位”时指的通常是这个意思很多人图省事直接用全市场横截面分位结果自然对不上。拿到因子函数之后一定要单独验证逻辑检查字段空值、分位区间是否在[0,1]、是否用了未来数据。因子层和策略逻辑剥离是后面能快速换研报策略的基础。3.3 回测层先向量化再逐日精细策略逻辑简单时优先用向量化回测速度快代码短。以月度调仓为例先算出每个调仓日应该持有哪些股票然后把这批股票下一个调仓周期内每天的日收益取等权平均拼接成组合日收益序列。核心逻辑大致是这样的def monthly_rebalance_backtest(factor_df, hold_days20): # factor_df: date, code, signal, daily_return rebalance_dates sorted(factor_df[date].unique())[::hold_days] nav_list [] for i, date in enumerate(rebalance_dates): selected factor_df[factor_df[date] date] selected selected[selected[signal] 1] if i 1 len(rebalance_dates): next_date rebalance_dates[i 1] rets selected[[code, daily_return]] port_ret rets[daily_return].mean() nav_list.append({date: date, nav: port_ret}) return pd.DataFrame(nav_list)这个版本足够应付研报里常见的选股逻辑但有个天然问题它假设调仓日当天所有股票都能以收盘价买入忽略停牌、涨跌停买不进的情况。等你发现复现结果偏高时八成得回来加规则调仓日停牌的剔除、涨停的跳过、一字跌停的也跳过。升级方法是在逐日循环里检查每个持仓标的当日是否可交易这个后面讲体检点时再细说。3.4 绩效层把净值曲线变成可对比的成绩单研报复现最关键的是对比所以绩效模块不能只算一条净值曲线要同时输出基准和超额。我通常会输出一张汇总表指标计算公式含义年化收益率(最终净值)^(252/交易天数)-1不同周期可比的收益年化波动率日收益std*sqrt(252)风险水平夏普比率(年化收益-无风险利率)/年化波动率风险调整后收益最大回撤max(1-净值/累计净值峰值)最难熬的时期信息比率超额年化/超额波动相对基准的稳定度代码可以直接用numpy写import numpy as np import pandas as pd def performance_stats(nav_series: pd.Series, freq: int 252, rf: float 0.02) - dict: nav_series nav_series.dropna() ret nav_series.pct_change().dropna() annual_ret (nav_series.iloc[-1] / nav_series.iloc[0]) ** (freq / len(ret)) - 1 annual_vol ret.std() * np.sqrt(freq) sharpe (annual_ret - rf) / annual_vol cum_max nav_series.cummax() drawdown (nav_series - cum_max) / cum_max max_dd drawdown.min() return { 年化收益率: annual_ret, 年化波动率: annual_vol, 夏普比率: sharpe, 最大回撤: max_dd, }绩效统计做完我会先把策略净值曲线和基准曲线画在一起再用一张表格和研报里的收益特征对比。注意绝大多数研报给的净值不是严格日度更新有些只是月度数据对比时得先用同样的频率对齐不能拿日频净值和研报月频净值直接比均值。4. 完整案例把“小市值低估值”研报策略从文字跑到净值曲线前面讲了骨架这一节用一个非常常见的策略来走完整流程小市值加低估值选股。这个策略在券商研报里出现频率极高逻辑不复杂但完整跑一遍能展示复现过程中所有关键环节。4.1 从文字到参数的还原假设某篇研报的策略原文大约是这样“每月最后一个交易日剔除ST、剔除上市不满60个交易日的股票在全市场选取市值最小的60只股票同时要求市盈率PE大于0且小于全市场当日PE中位数等权构建组合持有至下月调仓。”翻译成参数表如下调仓频率每月最后一个交易日收盘后剔除条件一股票名称含ST或*ST剔除条件二上市交易日数少于60选中数量60只估值过滤PE 0 且 PE 当日全市场中位数权重方式等权交易成本双边0.2%基准沪深300这里“盘中实时市值”和“当日收盘市值”会有细微差别我选择用调仓日收盘后的市值避免用到当天不可得的信息。4.2 核心代码实现第一段数据读入与基础清洗。假设我已经把本地数据整理成data目录下按年存储的parquet文件。import pandas as pd from pathlib import Path def load_all_data(data_dir./data): dfs [] for f in Path(data_dir).glob(*.parquet): dfs.append(pd.read_parquet(f)) df pd.concat(dfs, ignore_indexTrue) df[date] pd.to_datetime(df[date]) df df.sort_values([date, code]).reset_index(dropTrue) return df def add_filter_cols(df): df[is_st] df[name].str.contains(ST, naFalse) # 假设数据中已有 ipo_date 字段用来计算上市天数 df[listed_days] (df[date] - df[ipo_date]).dt.days return df第二段月末因子计算与选股。def monthly_momentum_strategy(df, top_n60): # 取每个自然月末最后一条记录 monthly df.set_index(date).resample(ME).last().reset_index() snapshot monthly.copy() snapshot snapshot[~snapshot[is_st]] snapshot snapshot[snapshot[listed_days] 60] snapshot snapshot[snapshot[pe] 0] pe_median snapshot.groupby(date)[pe].transform(median) snapshot snapshot[snapshot[pe] pe_median] snapshot snapshot.sort_values([date, mktcap], ascending[True, True]) snapshot[rank] snapshot.groupby(date).cumcount() selected snapshot[snapshot[rank] top_n] return selected[[date, code]]注意这里有个隐藏细节resample(ME).last()取的是每个自然月的最后一笔数据不是严格意义上的“最后一个交易日收盘数据”。如果数据里有非交易时间戳或者停牌导致的缺失月末快照可能取到错误的日子。更稳妥的办法是先把交易日历整理出来用每个月的最后一个交易日去过滤。第三段组合收益回测。假设我已经有了每只股票的日收益率列daily_return接下来把选出来的股票与下个调仓周期内的日收益对齐等权平均得到组合日收益。def backtest_selected(selected, df, hold_days21): dates sorted(selected[date].unique()) nav_list [] for i, d in enumerate(dates): if i 1 len(dates): break start, end d, dates[i 1] codes selected.loc[selected[date] d, code].tolist() ret_sub df[(df[code].isin(codes)) (df[date] start) (df[date] end)] daily_ret ret_sub.groupby(date)[daily_return].mean() nav_list.append(daily_ret) port_ret pd.concat(nav_list).sort_index() nav (1 port_ret).cumprod() return nav这个回测版只能算教学版因为它没有处理调仓后停牌、涨跌停等问题。但作为复现的第一版它足够帮你看清策略的收益轮廓。4.3 结果解读小市值策略的甜蜜与风险用我本地的A股历史数据跑下来这类小市值策略通常呈现几个特点长期年化收益可能显著高于沪深300但最大回撤也可能超出大多数人的心理承受能力尤其在某些年份会出现连续阴跌。这背后的逻辑是小市值因子确实长期有效但它赚的是流动性溢价和风险补偿不是无风险收益。复现这样的策略不能只看年化收益高就兴奋要看回撤曲线你能不能拿得住。如果你自己试的时候发现回撤大得离谱先别急着调参数先去看看是不是停牌和涨跌停没有过滤。5. 复现结果和研报对不上多数是这五个体检点在捣乱这是复现过程中最折磨人的阶段。你的代码逻辑完全按照研报翻译结果净值曲线就是和研报里的差一大截。别急着怀疑代码先按下面这个顺序做体检。5.1 复权方式最隐蔽的差异来源前面提过前复权价格会变化。具体到复现场景研报作者可能用的是Wind或Choice里的“后复权”或“等比前复权”而你用akshare的qfq两者在长期回测中的差异会累积。建议把复权因子单独存下来用“后复权价格”做回测同时记录每只股票的除权除息日期方便后面处理分红再投资。这个点通常不会让代码报错但会让净值曲线产生肉眼可见的偏差。5.2 交易成本和滑点往高估一点复现实在研报作者出于展示效果交易成本通常按比较低的标准算。我自己的默认值会按双边0.2%到0.3%来设若策略换手率很高这个设置会让收益明显下降。滑点在小市值股票上特别大因为买卖盘口薄按收盘价成交是小市值策略复现结果虚高的头号原因。建议对成交额很小的股票做可交易性过滤比如个股日成交额低于5000万就不买这样更贴近实盘。5.3 未来函数财务数据的“时间滞后”一定要留很多研报里的财务因子乍一看是季报ROE、营收增速但你在合并数据时如果简单地用“最新财报数值”拼接行情日期就会把尚未披露的财报数据提前用进去了。正确做法是把财报的“报告期”和“实际披露日期”拆开只有当日期大于等于披露日期时才能使用该报告期数据。akshare有专门的财报披露计划接口建议把它合并进数据管道里。这也是复现结果偏高最常见的原因之一。5.4 涨跌停与停牌回测里的“幽灵成交”涨跌停和停牌对复现的影响非常直接涨停时你想买买不进跌停时你想卖卖不掉停牌时什么都做不了。教学版回测代码假设所有交易都能成交最终收益会比真实情况更高。处理办法是在调仓逻辑里加入“可交易状态”判断如果收盘价等于涨停价则过滤买入等于跌停价则过滤卖出停牌当天直接不处理该标的。这一步做完你的净值曲线通常会更难看一点但也更真实。5.5 基准和回测区间先对齐再做比较最后这个体检点看起来最基础却最容易忽略。研报通常展示的是某个特定区间的累计超额收益比如从2020年到2024年的月度调仓组合净值。如果你复现时用了2015年的数据或者用了不同的沪深300价格序列比如不包含分红的价格指数与含分红的全收益指数结果当然对不上。建议先在同一历史区间、同一基准口径下把收益曲线画在一起再谈差异修复。6. 样本外测试复现不是终点策略体检才是当你终于跑出一版和研报对得上的结果别急着发截图这只是把别人的话重复了一遍。更有价值的工作是用样本外测试判断这个策略是否值得进一步研究。6.1 从时间轴上切一刀样本内与样本外把数据按时间切分成两部分前70%用来复现参数与验证逻辑后30%完全不参与任何调参作为最终检验。如果样本外收益和回撤特征明显变差说明这个策略对参数和区间很敏感研报里的漂亮数字极可能是过拟合的结果。我见过很多策略样本内年化30%、样本外直接失效这不是代码问题是策略本身缺乏稳健性。代码里只需要加上一个简单的时间条件train df[df[date] 2022-07-01] test df[df[date] 2022-07-01]然后分别在两个区间上跑同一套回测逻辑看结果差异。6.2 参数扰动组合把关键参数都摇一摇除了时间切片还可以做几组参数扰动股票数量30、50、80分位数30%、40%、50%调仓周期20、40天。把这些参数组合跑出来画成一张热力图或简单表格看收益分布是否平滑。如果某一组参数旁边收益突变说明策略存在参数悬崖深入研究的价值就有限。这个检查特别适合用于回应别人“你这个复现和研报差多少”这类问题你心里有数那个漂亮结果只在一个很窄的参数窗口里成立。6.3 容量与流动性检查小市值策略的“天花板”在哪研报里的策略大多没有考虑资金容量尤其是小市值策略。买60只市值最小的股票听起来美好但如果你管理的资金上亿很可能买完就把股价抬起来回测失去意义。我建议在回测中加入一个简单检查每日成交额乘以一个流动性折扣系数比如1%得到当日可交易金额再看你的目标持仓市值是否超过这个上限。下面是一段可复用的检查逻辑def capacity_check(selected, df, liquidity_ratio0.01): # selected: date, code # df: date, code, amount, mktcap merged selected.merge(df[[date, code, amount, mktcap]], on[date, code]) merged[tradable_amount] merged[amount] * liquidity_ratio merged[target_value] merged[mktcap] * 0.01 # 假设买1%流通市值 merged[capacity_ok] merged[tradable_amount] merged[target_value] return merged[capacity_ok].mean()这个检查在复现小市值、微小盘策略时几乎是必须的。你会发现很多研报策略在超小市值区间的可容纳资金非常有限。6.4 对研报复现这件事本身的体会最后说点我自己的感受。研报复现不是一个“拿到代码就结束”的工程它更像一次和研报作者的隔空对话你要把他没说出口的数据口径、成本假设、时点选择一个个猜出来再用Python验证你的猜测对不对。这个过程里Python帮你把文字变成了可执行、可对比、可证伪的东西但真正值钱的是你对每一个隐性假设的判断能力。我现在拿到一份研报反而不会再追求“原样复现一个一模一样的结果”了。更实用的做法是先复现出和研报一致的大致轮廓然后马上转向参数敏感度、样本外表现、容量限制这些“体检项”判断这个策略值不值得继续养着。复现能给到你的最好结果不是一张可以截图炫耀的净值曲线而是一套你自己能掌控、能解释、能优化的策略实现方法。最后再分享一个小习惯每次复现完我会顺手把参数翻译表、数据源版本、回测结果三样东西放在同一个目录下标注好日期。这个习惯已经帮我避免过好几次“三个月后想不起当初数据怎么洗的”这种尴尬了。本文还有配套的精品资源点击获取