2.4 复现案例在A股复现一个经典价值因子代码数据为了完成本章节的编写花钱买了数据权限并对代码做了多次优化所有代码都已经过测试。与各位同学分享预祝有意做量化的同学账户长红财源滚滚。一、引言从理论到代码的“第一公里”本节是本书的第一个端到端实战案例。我们将选择金融学中最著名的价值因子Value Factor使用账面市值比BM作为代理指标在A股市场进行完整复现。目标通过本案例你将掌握数据获取与清洗使用Tushare获取行情与财务数据并严格处理复权与财务发布日期。因子计算正确计算BM比率严防未来函数。组合构建实现投资组合排序法构建多空组合。绩效评估计算年化收益、夏普比率、最大回撤并可视化净值曲线。核心警示本案例旨在演示标准研究流程。A股价值因子在2016年后表现复杂结果可能不如经典文献中完美这正是市场进化的体现。二、案例设计价值因子与BM指标1. 因子逻辑价值投资的核心是“买入便宜货”。学术上通常用高账面市值比High Book-to-Market Ratio来定义“便宜”账面价值Book Equity公司净资产代表企业内在价值。市值Market Equity市场给予的价格。BM 账面价值 / 市值。BM越高股票越“便宜”。假设高BM价值股的未来收益显著高于低BM成长股。2. 数据需求与时间对齐关键行情数据月度收盘价、市值用于计算收益率和权重。财务数据年报/中报的股东权益合计Book Equity。核心陷阱财务数据的信息发布日期远晚于报告期截止日。例如2023年年报截止12月31日通常在2024年4月才公告。在回测中在t月只能使用t月之前已公告的数据。3. 回测框架股票池全A股剔除ST、退市、金融行业、上市不足1年、停牌股票。频率月度调仓每月末计算因子下月持有。分组按BM值分为5组Quintile做多Top组价值做空Bottom组成长。样本期2010年1月 – 2025年12月覆盖牛熊周期。三、代码实战四步实现因子复现我们将使用Python Tushare作为数据源。请确保已安装tushare,pandas,numpy,matplotlib。步骤1数据获取与清洗严防未来函数importtimeimporttushareastsimportpandasaspdimportnumpyasnpimportmatplotlib.pyplotaspltfrompathlibimportPath# 设置中文字体plt.rcParams[font.sans-serif][SimHei]# Windows 黑体# 或者 [Microsoft YaHei] 微软雅黑# 或者 [KaiTi] 楷体# 解决负号显示问题plt.rcParams[axes.unicode_minus]False# 初始化Tushare Pro需注册获取tokentokenYOUR_TUSHARE_TOKENts.set_token(token)prots.pro_api()# 样本期设置start_date20100101end_date20251231# 1. 获取股票基础信息用于筛选主板/创业板等defget_stock_basic():stock_basicpro.stock_basic(exchange,list_statusL,fieldsts_code,name,list_date,industry)stock_basicstock_basic[~stock_basic[industry].str.contains(银行|保险|证券,naFalse)]# 粗略剔除金融股returnstock_basic stock_basicget_stock_basic()# 2. 获取交易日历defget_trade_calendar(start_date,end_date):df_calendarpro.trade_cal(exchange,start_datestart_date,end_dateend_date)df_calendar[cal_date]pd.to_datetime(df_calendar[cal_date])df_calendar[pretrade_date]pd.to_datetime(df_calendar[pretrade_date])returndf_calendar[df_calendar[is_open]1][[cal_date,pretrade_date]]df_calendarget_trade_calendar(start_date,end_date)# 构建月度截面# 按月分组取最大日期df_datesdf_calendar.groupby(pd.Grouper(keycal_date,freqME)).agg(me_date(cal_date,max)).reset_index(dropTrue)# 3. 获取日行情用于计算月收益率和市值# 注意Tushare的daily接口返回未复权价格需使用adj_factor进行复权defget_daily(ts_code,start_date,end_date):daily_pathPath(rE:\AppData\quant_trade\factor_sample\daily)daily_filedaily_path/f{ts_code}.parquet# 如果有缓存则从文件读取而且只读取有限列ifdaily_file.exists():# print(f文件存在{daily_file})df_dailypd.read_parquet(daily_file,columns[ts_code,trade_date,adj_close],filters[(trade_date,,pd.to_datetime(start_date)),(trade_date,,pd.to_datetime(end_date))])else:# print(ftushare下载{ts_code})df_dailypro.daily(ts_codets_code,start_datestart_date,end_dateend_date)iflen(df_daily)0:df_daily[trade_date]pd.to_datetime(df_daily[trade_date])# 计算复权因子df_daily[adj_factor](df_daily[pre_close].shift(1)/df_daily[close]).iloc[1:].cumprod()df_daily.at[0,adj_factor]1# 计算复权收盘价df_daily[adj_close]df_daily[close]*df_daily[adj_factor]# 保存缓存数据df_daily.to_parquet(daily_file,indexFalse,compressionzstd,compression_level3# 1-223 是速度与压缩比的平衡点)time.sleep(1)returndf_daily# 计算月度截面数据收益率defget_me_daily(df_dates:pd.DataFrame,df_daily:pd.DataFrame)-pd.DataFrame: 使用月度日历将K线转为月度截面数据并计算收益率 df_dates月度日历数据 df_daily日K线 # 月度截面df_me_dailypd.merge(df_dates,df_daily[[ts_code,trade_date,adj_close]],left_onme_date,right_ontrade_date,howleft).drop(me_date,axis1)# 计算收益率df_me_daily[me_profit]df_me_daily[adj_close].shift(-1)/df_me_daily[adj_close]-1# 删除空行df_me_daily.dropna(inplaceTrue)returndf_me_daily# 4. 获取财务数据关键获取实际公告日期# 使用利润表income获取报告期和公告日再关联资产负债表balancesheet获取股东权益defget_financial(ts_code,start_date,end_date):financial_pathPath(rE:\AppData\quant_trade\stocks_all\financial)income_filefinancial_path/income.parquetbalance_filefinancial_path/balancesheet.parquetifincome_file.exists():df_incomepd.read_parquet(income_file,columns[ts_code,ann_date,end_date,f_ann_date,report_type],filters[(ts_code,,ts_code),(ann_date,,start_date),(ann_date,,end_date)])else:df_incomepro.income(ts_codets_code,start_datestart_date,end_dateend_date,fieldsts_code,ann_date,end_date,f_ann_date,report_type)time.sleep(1)ifbalance_file.exists():df_balancepd.read_parquet(balance_file,columns[ts_code,end_date,report_type,total_hldr_eqy_inc_min_int,total_share],filters[(ts_code,,ts_code),(ann_date,,start_date),(ann_date,,end_date)])else:df_balancepro.balancesheet(ts_codets_code,start_datestart_date,end_dateend_date,fieldsts_code,end_date,report_type,total_hldr_eqy_inc_min_int,total_share)time.sleep(1)# 合并财务数据保留公告日期df_financialpd.merge(df_income,df_balance,on[ts_code,end_date,report_type],howinner)df_financial[ann_date]pd.to_datetime(df_financial[ann_date])df_financial[end_date]pd.to_datetime(df_financial[end_date])returndf_financial说明原始数据从tushare下载下载K线数据和财务数据使用了本地缓存K线数据比较多使用了月度截面数据以减少数据量。步骤2因子计算BM Ratio# 1. 构建月度截面数据框架每月末截面defcalc_bm_data(dates,df_daily_all,df_financial_all):bm_data_list[]fordateindates:# 获取当前截面所有股票的市值使用月末复权收盘价 * 总股本daily_slicedf_daily_all[df_daily_all[trade_date]date]ifdaily_slice.empty:continue# 获取在此日期之前已公告的最新财务数据严防未来函数# 筛选条件公告日(ann_date) 当前月末(date)financial_availabledf_financial_all[df_financial_all[ann_date]date]# 对每个股票取最近一个报告期的股东权益financial_latestfinancial_available.sort_values(end_date).groupby(ts_code).last().reset_index()# 合并当前市值与最新财务数据mergedpd.merge(daily_slice,financial_latest,onts_code,howinner)# 计算BM因子股东权益 / 总市值总股本 * 收盘价merged[bm]merged[total_hldr_eqy_inc_min_int]/(merged[total_share]*merged[adj_close])# 筛选有效数据BM0且市值大于10亿剔除微盘股噪音mergedmerged[(merged[bm]0)(merged[total_share]*merged[adj_close]1e9)]bm_data_list.append(merged[[ts_code,trade_date,adj_close,me_profit,bm]])# 拼接所有截面bm_datapd.concat(bm_data_list)bm_databm_data.sort_values([trade_date,ts_code]).reset_index(dropTrue)returnbm_data# 加载所有K线daily_list[]forts_codeinstock_basic[ts_code]:df_dailyget_daily(ts_code,start_date,end_date)# 过滤次新股iflen(df_daily)252:continuedf_me_dailyget_me_daily(df_dates,df_daily)daily_list.append(df_me_daily)df_daily_allpd.concat(daily_list,ignore_indexTrue)df_daily_alldf_daily_all.sort_values([trade_date,ts_code]).reset_index(dropTrue)# 加载所有财务数据financial_list[]forts_codeindf_daily_all[ts_code].unique():df_financialget_financial(ts_code,start_date,end_date)financial_list.append(df_financial)df_financial_allpd.concat(financial_list,ignore_indexTrue)df_financial_alldf_financial_all.sort_values([end_date,ts_code]).reset_index(dropTrue)# 计算因子df_bm_datacalc_bm_data(df_dates[me_date],df_daily_all,df_financial_all)步骤3投资组合排序与收益计算# 初始化结果容器portfolio_returns[]group_labels[Q1 (Growth),Q2,Q3,Q4,Q5 (Value)]fordateindf_dates[me_date]:slice_datadf_bm_data[df_bm_data[trade_date]date]iflen(slice_data)100:# 股票数量太少则跳过continue# 1. 按BM值排序并分为5组slice_dataslice_data.sort_values(bm)slice_data[group]pd.qcut(slice_data[bm],q5,labelsgroup_labels,duplicatesdrop)# 2. 计算各组合等权平均收益group_profitslice_data[[me_profit,group]].groupby(bygroup,observedFalse).mean()[me_profit].to_dict()group_profit[trade_date]date portfolio_returns.append(group_profit)# 转换为DataFramereturns_dfpd.DataFrame(portfolio_returns).set_index(trade_date)returns_df[Long_Short]returns_df[Q5 (Value)]-returns_df[Q1 (Growth)]# 多空组合步骤4绩效分析与可视化# 1. 计算累计净值cumulative_returns(1returns_df).cumprod()# 2. 关键绩效指标defcalculate_stats(series,freq12):计算年化收益、波动、夏普、最大回撤annual_return(1series.mean())**freq-1annual_volseries.std()*np.sqrt(freq)sharpeannual_return/annual_volifannual_vol0else0# 最大回撤cum(1series).cumprod()rolling_maxcum.expanding().max()drawdown(cum-rolling_max)/rolling_max max_dddrawdown.min()returnannual_return,annual_vol,sharpe,max_ddprint( A股价值因子BM实证结果 )forcolinreturns_df.columns:ann_ret,ann_vol,sharpe,max_ddcalculate_stats(returns_df[col])print(f{col}: 年化收益{ann_ret:.2%}, 年化波动{ann_vol:.2%}, 夏普{sharpe:.2f}, 最大回撤{max_dd:.2%})# 3. 绘制净值曲线plt.figure(figsize(12,6))forcolincumulative_returns.columns:plt.plot(cumulative_returns.index,cumulative_returns[col],labelcol,linewidth2)plt.title(A股价值因子BM分组净值曲线2010-2023)plt.xlabel(日期)plt.ylabel(累计净值)plt.legend()plt.grid(True,alpha0.3)plt.show() A股价值因子BM实证结果 Q1 (Growth): 年化收益2.47%, 年化波动29.74%, 夏普0.08, 最大回撤-78.10% Q2: 年化收益10.02%, 年化波动28.92%, 夏普0.35, 最大回撤-61.09% Q3: 年化收益12.44%, 年化波动27.59%, 夏普0.45, 最大回撤-53.27% Q4: 年化收益14.41%, 年化波动26.34%, 夏普0.55, 最大回撤-45.15% Q5 (Value): 年化收益16.34%, 年化波动24.20%, 夏普0.68, 最大回撤-34.84% Long_Short: 年化收益13.56%, 年化波动15.70%, 夏普0.86, 最大回撤-23.36%四、结果解读与A股特性分析运行上述代码后你可能会得到类似下表的典型结果数值仅为示意实际结果取决于数据清洗严格程度组合年化收益率夏普比率最大回撤单调性Q1 (成长)2.47%0.08-78.10%-Q210.2%0.35-61.09%-Q312.44%0.45-53.27%通常存在Q414.41%0.55-45.15%单调递增Q5 (价值)16.34%0.68-34.84%显著多空 (V-G)13.56%0.86-23.36%-A股实证结论与讨论价值溢价存在但非“圣杯”在长周期内高BM组合价值股的平均收益显著高于低BM组合成长股多空组合年化收益约13%验证了价值因子的基本有效性。周期性回撤价值因子在A股并非每年都有效。在2013-2015年创业板牛市、成长风格极致和2020-2021年核心资产、赛道股泡沫价值因子会经历长达数年的深度回撤。这是因子投资必须承受的风格风险。与市值因子的纠缠在A股小市值股票往往BM较高。若未控制市值价值因子可能混杂了小市值溢价。严谨的研究需进行分层分析如按市值分两层后再看BM效应或Fama-MacBeth回归控制市值。五、本节小结本案例完成了从数据到结论的完整闭环流程标准化严格遵循了“数据获取→清洗→因子计算→组合排序→绩效评估”的工业化流程。防错机制重点处理了财务发布日期对齐彻底杜绝未来函数。现实认知通过实证结果你应认识到因子投资是“概率优势”而非“稳赚不赔”。价值因子在A股有效但具有显著的周期性和风格依赖性。代码优化方向引入市值加权组合构建。增加交易成本双边千分之三的粗略估计。进行子样本分析如2010-2015 vs 2016-2023以观察因子衰减。接下来我们将进入第二部分《第二部分A股因子库构建与验证》在第二部分我将学习如何系统化管理多个因子并组合它们以构建更稳健的策略。