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计算的核心流程:

ADR计算核心流程 原始数据 日期、代码、涨跌幅、状态 数据清洗 剔除停牌股、新股 每日统计 涨跌家数 计算ADR 上涨家数 / 总交易家数 滚动平滑 N日移动平均 注意事项 • 停牌股不参与计算 • 新股需设置观察期 • 窗口大小影响灵敏度 • 关注覆盖率指标 • 平盘股建议剔除

4.8 实战中的几个坑

最后分享几个我踩过的坑:

  1. 数据对齐问题:不同数据源的日期格式可能不一样,一定要统一成datetime格式。
  2. 退市股票:退市前最后几天,成交量会异常,建议在退市公告日就剔除。
  3. 涨跌停板:涨停的股票算上涨,跌停的算下跌,这个没问题。但要注意,有些数据源会把涨停股的涨跌幅标为9.98%(因为四舍五入),判断时用>0即可,不要用==。
  4. 覆盖率检查:我每次算完ADR,都会检查一下coverage_ratio(参与计算的股票占比)。如果某天这个值低于80%,那天的ADR就不可信。

嗯,ADR的计算看起来简单,但真正做好需要处理很多细节。把停牌和新股处理好,你的ADR指标就已经比市面上80%的代码要靠谱了。


专注资料整理