4、计算涨跌比(ADR):Python实现ADR计算,滚动窗口处理,处理停牌与新股数据
涨跌比(ADR),说白了就是看市场上今天有多少股票在涨,多少在跌。这个指标很直观——涨的家数多,市场情绪就好;跌的家数多,那就要小心了。我个人习惯把ADR当作市场的「体温计」,每天开盘前扫一眼,心里就有个底。
但真正动手算的时候,你会发现坑不少。停牌的股票怎么处理?新股刚上市怎么算?滚动窗口怎么设计才合理?嗯,这些细节我都在实战中踩过,今天一次性讲清楚。
4.1 ADR的核心逻辑
ADR的计算公式其实很简单:
ADR = 上涨家数 / (上涨家数 + 下跌家数)
注意:平盘家数通常不计入分母,但有些算法会把它算进去。我个人建议剔除平盘,因为平盘不代表方向。
举个例子:今天全市场有3000只股票交易,其中1800只上涨,1200只下跌。那么ADR = 1800 / (1800 + 1200) = 0.6。这个值大于0.5,说明多头占优。
但单日的ADR波动太大,没什么参考价值。所以我们通常用滚动窗口来平滑——比如5日、10日、20日的平均ADR。这样能过滤掉单日的噪音,看到趋势。
4.2 数据准备:你需要什么?
要算ADR,你至少需要以下数据:
- 股票代码:每只股票的唯一标识
- 交易日期:按日粒度
- 涨跌幅:或者收盘价(用来算涨跌)
- 交易状态:正常交易、停牌、退市等
我在项目中遇到过一个问题:有些数据源会把停牌股票的涨跌幅标为0,这就会导致ADR失真。你想想看,一只停牌的股票,它今天根本没交易,怎么能算「平盘」呢?所以数据清洗这一步很关键。
4.3 Python实现:基础版
我们先写一个最基础的版本,不考虑停牌和新股的问题。
import pandas as pd
import numpy as np
def calculate_adr_basic(df, window=5):
"""
基础版ADR计算
df: 包含日期、股票代码、涨跌幅的DataFrame
window: 滚动窗口大小
"""
# 标记涨跌
df['is_up'] = (df['pct_chg'] > 0).astype(int)
df['is_down'] = (df['pct_chg'] < 0).astype(int)
# 按日期分组,统计每日涨跌家数
daily = df.groupby('trade_date').agg(
up_count=('is_up', 'sum'),
down_count=('is_down', 'sum')
).reset_index()
# 计算每日ADR
daily['adr'] = daily['up_count'] / (daily['up_count'] + daily['down_count'])
# 滚动平均
daily['adr_smooth'] = daily['adr'].rolling(window=window).mean()
return daily
这个版本能跑,但问题很多。比如停牌的股票会被算成「没涨没跌」,然后被排除在外。实际上停牌股根本不应该出现在分母里。
4.4 处理停牌数据
停牌的处理,说白了就一句话:停牌期间,这只股票不参与ADR计算。
怎么判断停牌?通常数据源会有一个字段,比如trade_status,值为'停牌'或'Suspended'。如果没有这个字段,你可以通过成交量来判断——成交量连续为0且价格不变,大概率是停牌。
避坑指南:我曾经用过一个数据源,停牌股票的涨跌幅字段是NaN,但成交量字段却是0。我一开始没注意,直接用dropna()把停牌股全删了,结果发现某天全市场只剩500只股票在交易,ADR算出来严重失真。后来我改成只剔除「停牌且无交易」的股票,才解决问题。
改进后的代码:
def calculate_adr_with_suspension(df, window=5):
"""
处理停牌后的ADR计算
"""
# 只保留正常交易的股票
df_trading = df[df['trade_status'] == '正常'].copy()
# 标记涨跌
df_trading['is_up'] = (df_trading['pct_chg'] > 0).astype(int)
df_trading['is_down'] = (df_trading['pct_chg'] < 0).astype(int)
# 按日期统计
daily = df_trading.groupby('trade_date').agg(
total_trading=('stock_code', 'count'),
up_count=('is_up', 'sum'),
down_count=('is_down', 'sum')
).reset_index()
# 计算ADR
daily['adr'] = daily['up_count'] / (daily['up_count'] + daily['down_count'])
daily['adr_smooth'] = daily['adr'].rolling(window=window).mean()
return daily
4.5 新股的处理
新股的问题更隐蔽。新股上市第一天,通常会有巨大的涨幅(比如44%),这会严重拉高当天的上涨家数。如果你不做处理,新股上市那天ADR会突然飙升,给你一种「市场很强」的错觉。
我的处理原则是:新股上市前20个交易日,不纳入ADR计算。为什么是20个交易日?因为新股通常需要一个月左右才能「消化」掉上市初期的异常波动。
小技巧:你可以用上市日期字段来判断。如果某只股票的当前日期距离上市日期小于20个交易日,就把它剔除。我习惯把这个阈值设成参数,方便回测时调整。
完整版代码:
def calculate_adr_full(df, window=5, ipo_days=20):
"""
完整版ADR计算:处理停牌+新股
df: 必须包含 trade_date, stock_code, pct_chg, trade_status, list_date
"""
# 剔除停牌股
df_trading = df[df['trade_status'] == '正常'].copy()
# 剔除新股(上市不满ipo_days天)
df_trading['days_since_list'] = (df_trading['trade_date'] - df_trading['list_date']).dt.days
df_mature = df_trading[df_trading['days_since_list'] >= ipo_days].copy()
# 标记涨跌
df_mature['is_up'] = (df_mature['pct_chg'] > 0).astype(int)
df_mature['is_down'] = (df_mature['pct_chg'] < 0).astype(int)
# 按日期统计
daily = df_mature.groupby('trade_date').agg(
total_trading=('stock_code', 'count'),
up_count=('is_up', 'sum'),
down_count=('is_down', 'sum')
).reset_index()
# 计算ADR
daily['adr'] = daily['up_count'] / (daily['up_count'] + daily['down_count'])
daily['adr_smooth'] = daily['adr'].rolling(window=window).mean()
# 补充:计算参与计算的股票数量占比
total_stocks = df.groupby('trade_date')['stock_code'].nunique().reset_index()
total_stocks.columns = ['trade_date', 'total_stocks']
daily = daily.merge(total_stocks, on='trade_date', how='left')
daily['coverage_ratio'] = daily['total_trading'] / daily['total_stocks']
return daily
4.6 滚动窗口的细节
滚动窗口看起来简单,但有几个细节要注意:
- 窗口大小:5日适合短线,20日适合中线。我一般用10日作为默认值。
- 缺失值处理:前N天数据不够时,ADR会是NaN。你可以选择填充0,或者直接跳过。
- 权重问题:等权还是加权?我个人习惯用等权,因为简单且有效。加权(比如越近的权重越大)理论上更好,但实际效果差异不大。
4.7 知识体系图
下面这张图总结了ADR计算的核心流程:
4.8 实战中的几个坑
最后分享几个我踩过的坑:
- 数据对齐问题:不同数据源的日期格式可能不一样,一定要统一成datetime格式。
- 退市股票:退市前最后几天,成交量会异常,建议在退市公告日就剔除。
- 涨跌停板:涨停的股票算上涨,跌停的算下跌,这个没问题。但要注意,有些数据源会把涨停股的涨跌幅标为9.98%(因为四舍五入),判断时用>0即可,不要用==。
- 覆盖率检查:我每次算完ADR,都会检查一下
coverage_ratio(参与计算的股票占比)。如果某天这个值低于80%,那天的ADR就不可信。
嗯,ADR的计算看起来简单,但真正做好需要处理很多细节。把停牌和新股处理好,你的ADR指标就已经比市面上80%的代码要靠谱了。