简介:这份德意志银行2024年发布的中国股权策略研究报告,由特许金融分析师Peter Milliken撰写,面向关注中国股市走势的投资者与金融研究者,帮助其理解市场趋势变化与潜在机会。报告围绕香港股市连续四年下跌、沪深300三年表现不佳的背景,探讨本轮行情究竟是短期反弹还是趋势转折,并分析GDP增长与市场表现的相关性、货币政策与资金注入的作用、中国6.5万亿美元现金储备释放对股市吸引力的影响,以及中国市场在全球投资组合中被低估的现状。报告还统计了标普500、Stoxx 600与沪深300自成立以来的多次十日大涨及其后一年、三年回报,用以说明强势反弹往往预示持续上涨。资源为1个PDF文件,压缩包约1.05MB,已有89人学习,适合需要研判中国权益资产配置与市场周期的读者参考。
1. 一份被当成“看图说话”的研报,其实藏着一套可复现的数据工作流
很多人拿到《China Equity Strategy Trend change》这类卖方研报,第一反应是翻结论页,看多还是看空,然后关掉。但如果你把它当成一个数据项目来拆,会发现 Peter Milliken 这份 2024 年 10 月的报告,本质上是一套“用历史极值反推当前状态”的分析框架:连续四年下跌的恒指、三年下跌的沪深 300、6.5 万亿美元居民超额存款、CSI300 成分股普遍净现金,这些都不是观点,而是可以被复算的指标。对做量化、做宏观策略、甚至做数据可视化的 IT 从业者来说,这份 PDF 的价值不在“买不买”,而在于它示范了如何把零散的市场统计量组织成一条可验证的推理链。下面我按拆项目的思路,把它从数据源、指标构造、复现脚本到排错完整走一遍。
2. 研报里的核心指标拆解与数据口径对齐
2.1 为什么“连续下跌年数”比“跌幅”更值得先算
报告开篇最抓眼球的不是某个点位,而是“HK had four down years in a row, CSI300 three”。这个统计量看起来简单,但口径很容易踩坑。它统计的是日历年年度收益率,不是滚动 12 个月,也不是从高点回撤。恒指 2020 到 2023 连续四年收跌,沪深 300 是 2021 到 2023 连续三年收跌。如果你用滚动窗口去算,结论会完全不一样,因为 2020 年沪深 300 是上涨的。
复现这个指标,第一步是把指数收盘价整理成年度序列,再算同比。常见做法是用 pandas 的resample('Y').last()取每年最后一个交易日收盘,然后pct_change()。注意 A 股和港股交易日历不同,跨市场对齐时不要用inner join把数据砍掉,应该各自算完年度收益再合并。
import pandas as pd # df 需包含 date, index_code, close 三列,date 为交易日 df['date'] = pd.to_datetime(df['date']) def annual_return(sub): # 取每年最后一个交易日的收盘价 yearly = sub.set_index('date')['close'].resample('Y').last() # 计算年度收益率 return yearly.pct_change() result = {} for code, sub in df.groupby('index_code'): result[code] = annual_return(sub) annual_df = pd.DataFrame(result) print(annual_df.tail(8))逻辑说明:resample('Y').last()保证取的是自然年最后交易日,避免用resample('A')在某些 pandas 版本里把年末标签错位。pct_change()默认算的是相邻年度变化,正好对应“年度收益率”。参数上,如果你的数据源给的是复权价,直接用;如果是不复权价,恒指和沪深 300 都是价格指数,不需要额外复权,但要注意分红再投资的 Total Return 版本会略有差异,报告用的是价格指数口径。
2.2 10 日反弹统计:一个容易被误读的样本
报告里那张“10 calendar rallies”表格,列了 S&P500、Stoxx600、CSI300 自成立以来 10 个交易日涨幅超过一定阈值后的 1 年和 3 年回报。这张表的技术含量不在计算,而在样本筛选规则。它没有写清楚阈值是多少,但从数据看,入选的都是 10 日涨幅在 13% 到 26% 之间的案例。如果你自己复现,需要先定义“10 日反弹”的触发条件。
我一般会这样处理:先算 10 个交易日滚动收益率,再取历史分位数,比如前 5% 作为“强反弹”阈值。这样比硬编码一个百分比更稳健,因为不同指数的波动率差异很大。CSI300 的 10 日涨幅 15% 可能对应 95 分位,而 S&P500 的 15% 可能已经是 99 分位。
# 计算 10 日滚动收益率 df['ret_10d'] = df.groupby('index_code')['close'].pct_change(10) # 按指数分组,取 95 分位作为阈值 thresholds = df.groupby('index_code')['ret_10d'].quantile(0.95) print(thresholds) # 标记触发日 df['signal'] = df.apply( lambda r: r['ret_10d'] > thresholds[r['index_code']], axis=1 )参数说明:pct_change(10)里的 10 是交易日,不是自然日。报告标题写的是“10 calendar rallies”,但实际计算大概率用的是交易日,因为金融市场统计默认交易日。如果你要用自然日,需要先按日历日填充再算,但指数在非交易日没有价格,填充会引入偏差,不建议。
2.3 估值分位数的标准差口径
报告里那张亚洲市场估值表,用了“standard deviation from 1-yr / 5-yr / 20-yr average”来表达贵贱。这个口径比单纯看 PE 绝对值更有信息量,因为它把当前估值放回了历史分布里。以恒指为例,当前 PB 1.2,20 年平均 1.2,标准差偏离 -0.5,说明虽然绝对值不高,但相对自身历史并不极端。
复现时要注意:标准差偏离 = (当前值 - 历史均值) / 历史标准差。历史窗口的选择直接影响结论。1 年窗口对近期波动敏感,20 年窗口更稳定但可能包含结构性变化。常见做法是同时算三个窗口,交叉验证。
| 指标 | 当前值 | 1年均值 | 5年均值 | 20年均值 | 1年偏离 | 5年偏离 | 20年偏离 |
|---|---|---|---|---|---|---|---|
| 恒指 PB | 1.2 | 1.0 | 1.1 | 1.2 | 2.0 | 0.5 | -0.5 |
| 恒指 PE | 10.9 | 9.5 | 11.0 | 11.6 | 1.7 | -0.1 | -0.5 |
| 沪深300 PB | 1.6 | 1.5 | 1.8 | 2.3 | 1.8 | -0.8 | -0.6 |
这张表里最值得注意的不是当前值,而是偏离方向的一致性。恒指 PB 的 1 年偏离是 +2.0,20 年偏离是 -0.5,说明短期涨了一波,但长期看还在均值下方。这种“短强长弱”的组合,在策略上通常对应“反弹但未反转”的判断。
3. 用 Python 复现研报里的资金面与估值信号
3.1 居民存款与股市市值的比值构造
报告反复强调一个数字:中国家庭存款自疫情以来增加了 6.5 万亿美元,总存款超过 20 万亿美元,而港股加 A 股总市值约 10 万亿美元。这个比值的含义是:场外资金池是场内市值的两倍。一旦存款利率下行到 1% 附近,而股市日波动就能到 1%,资金迁移的动机就会变强。
复现这个指标,数据源通常用央行公布的“住户存款”月度数据,以及交易所公布的市值数据。注意单位统一,央行数据是人民币,市值数据也是人民币,不要混用美元。报告里用美元是为了方便国际读者,自己做的时候用本币更准。
# deposits: 住户存款月度数据,单位亿元 # market_cap: 沪深300 + 恒指总市值,单位亿元 ratio = deposits['amount'] / market_cap['total'] # 计算历史分位 ratio_pct = ratio.rank(pct=True) print(f"当前存款/市值比值分位: {ratio_pct.iloc[-1]:.2%}")逻辑说明:这个比值越高,说明场外资金相对场内市值越充裕。但要注意,存款不会全部进股市,所以比值本身不是买入信号,而是一个“潜在弹药量”的度量。参数上,如果你用月度数据,市值也要取月末值,避免时间错配。
3.2 CSI300 净现金筛选:一个可落地的选股条件
报告提到 CSI300 成分股倾向于净现金,尤其是权重股。这个结论可以直接转成一个筛选条件:净负债/EBITDA 小于 0,或者现金及等价物大于有息负债。对做基本面量化的团队来说,这是一个低门槛的防御性因子。
-- 假设有财务表 financials 和成分表 csi300 SELECT f.stock_code, f.stock_name, f.cash_and_equivalents, f.interest_bearing_debt, (f.cash_and_equivalents - f.interest_bearing_debt) AS net_cash FROM financials f JOIN csi300 c ON f.stock_code = c.stock_code WHERE f.report_date = '2024-06-30' AND (f.cash_and_equivalents - f.interest_bearing_debt) > 0 ORDER BY net_cash DESC;参数说明:report_date取最近一期财报,通常用中报或年报。net_cash > 0就是净现金条件。如果你想更严格,可以加一个net_cash / market_cap > 0.1,筛出净现金占市值超过 10% 的公司。这类公司在市场下跌时抗跌性更强,因为账上现金厚,不需要再融资。
3.3 估值偏离度的批量计算脚本
把前面几个指标串起来,可以写一个批量计算脚本,输入是指数代码列表,输出是每个指数的估值偏离度和年度收益序列。这个脚本可以直接跑在本地,数据源用 akshare 或 tushare 都行。
import akshare as ak def valuation_deviation(index_code, start='2004-01-01'): # 获取指数估值数据 df = ak.index_value_hist_funddb(symbol=index_code, indicator="市盈率") df['date'] = pd.to_datetime(df['date']) df = df[df['date'] >= start].set_index('date') current = df['value'].iloc[-1] result = {} for window, label in [(252, '1y'), (1260, '5y'), (5040, '20y')]: hist = df['value'].iloc[-window:] mean, std = hist.mean(), hist.std() result[label] = (current - mean) / std if std else 0 return result for code in ['000300', 'HSI']: print(code, valuation_deviation(code))逻辑说明:index_value_hist_funddb是 akshare 里获取指数估值历史的接口,indicator可以换成“市净率”。窗口用交易日数量近似,252 约等于 1 年,1260 约等于 5 年,5040 约等于 20 年。标准差偏离为负说明当前估值低于历史均值,为正则高于。注意不同数据源的估值口径可能不同,交叉验证时以同一数据源为准。
4. 复现过程中的数据坑与排错清单
4.1 指数年度收益的“首年缺失”问题
用pct_change()算年度收益时,第一年一定是 NaN,因为前面没有基期。如果你从 2004 年开始取数据,2004 年的收益是算不出来的,实际有效数据从 2005 年开始。报告里说“CSI300 three years running”,指的是 2021、2022、2023 三个完整年度,2024 年当时还没结束,所以不计入。自己做的时候要明确“完整年度”的定义,避免把当年未结束的涨跌算进去。
4.2 港股与 A 股交易日历差异导致的错位
恒指和沪深 300 的节假日安排不同,直接按日期合并会出现大量 NaN。常见做法是各自算完指标再按年度或月度对齐,不要试图按日对齐。如果你要做日频相关性分析,用outer join后ffill()填充,但要注意填充会引入自相关,做统计检验时要调整。
4.3 估值分位数的“负值”解读
标准差偏离为负,不代表估值一定便宜,只代表低于历史均值。如果历史均值本身被高估期拉高了,负偏离可能只是回归正常。报告里恒指 20 年 PE 偏离 -0.5,但当前 PE 10.9 对应 20 年均值 11.6,差距并不大。所以看这个指标时,要同时看绝对值和偏离度,不能只看一个。
提示:用 akshare 或 tushare 拉估值数据时,注意接口的复权方式和频率。部分接口给的是月度数据,直接用来算日频偏离会失真。
4.4 存款数据的频率对齐
住户存款是月度数据,股市市值是日频数据。做比值时,统一取月末值。如果市值数据在月末是非交易日,取最近一个交易日。不要用月内平均值,因为存款是时点值,不是流量。
5. 把研报结论转成可回测的信号:一个具体技巧
报告的核心判断是“趋势变化,不只是反弹”,翻译成可回测的语言,就是:在连续多年下跌后,出现强反弹信号时,后续 1 到 3 年回报的分布是否显著为正。这个可以用事件研究法来验证。
具体做法:定义事件为“指数连续 3 年下跌后,首次出现 10 日涨幅超过 95 分位”。然后统计事件发生后 252 个交易日和 756 个交易日的收益分布。如果样本量够,可以算胜率和中位数回报。报告里那张表的中位数 1 年回报是 22%,3 年是 26%,说明历史上这类信号后续表现偏正,但样本只有 20 个左右,统计功效有限。
# 事件研究:连续3年下跌后的强反弹 def event_study(df, index_code): sub = df[df['index_code'] == index_code].copy() sub['year'] = sub['date'].dt.year yearly = sub.groupby('year')['close'].last().pct_change() # 找连续3年下跌的年份 down3 = (yearly < 0) & (yearly.shift(1) < 0) & (yearly.shift(2) < 0) event_years = yearly[down3].index.tolist() results = [] for y in event_years: # 该年之后首次触发强反弹的日期 after = sub[sub['year'] > y] trigger = after[after['ret_10d'] > after['ret_10d'].quantile(0.95)] if trigger.empty: continue t0 = trigger.index[0] pos = sub.index.get_loc(t0) for horizon in [252, 756]: if pos + horizon < len(sub): ret = sub['close'].iloc[pos + horizon] / sub['close'].iloc[pos] - 1 results.append({'event_year': y, 'horizon': horizon, 'return': ret}) return pd.DataFrame(results)逻辑说明:down3标记连续三年下跌的年份,event_years是这些年份的列表。对每个事件年,找之后首次触发强反弹的交易日作为事件日,然后算 252 和 756 个交易日后的收益。参数上,quantile(0.95)是强反弹阈值,可以改成 0.9 或 0.99 做敏感性测试。这个脚本的输出可以直接和报告里的中位数回报对比,验证口径是否一致。
注意:事件研究法的样本量通常很小,连续三年下跌本身就是低频事件,所以结论的置信区间会很宽。不要把它当成精确预测,而是当成一个“历史类比”的参考。
最后说一个实操细节:报告里的数据截止到 2024 年 10 月,如果你现在跑同样的脚本,2024 年之后的走势已经出来了,可以拿来验证报告发布后的实际表现。这种“用新数据检验旧结论”的做法,比单纯读报告更有价值,也是把一份 PDF 变成自己分析框架的最短路径。
本文还有配套的精品资源,点击获取