Skip to content

19.1 涨跌停板策略

中国 A 股市场最显著的特征之一就是涨跌停板制度(Price Limit)。这一机制对市场流动性、价格发现过程以及交易策略设计都有深远影响。理解涨跌停板制度下的市场微观结构是 A 股量化策略开发的基础。

涨跌停板制度概述

制度规则

A 股主板(含中小板)的涨跌停板限制为 ±10%,创业板(科创板)为 ±20%,北交所为 ±30%。ST 和 *ST 股票为 ±5%。新股上市首日和恢复上市首日不受涨跌停限制。

涨跌停板的价格计算以前一交易日的收盘价为基准:

$$P_{\text{涨停}} = P_{\text{前收}} \times (1 + \text{limit}) \quad \text{(四舍五入到分)}$$ $$P_{\text{跌停}} = P_{\text{前收}} \times (1 - \text{limit}) \quad \text{(四舍五入到分)}$$
名词解释:封板

"封板"是指股票价格触及涨停(或跌停)后,买(卖)盘大量堆积,导致价格被"封锁"在涨跌停板价格上。封板期间的成交只能以涨跌停价格进行,且只有与封板方向相同的订单才能成交。

对市场微观结构的影响

流动性中断(Liquidity Disruption):当股票触及涨跌停板时,正常的连续竞价被中断。买方(涨停时)或卖方(跌停时)的流动性急剧收缩,订单在封板价格上排队等待成交。

价格发现延迟(Price Discovery Delay):涨跌停板人为地阻止了价格在一个交易日内的充分调整。原本应该在当日完成的价格变动,被迫延迟到随后的交易日。这导致了:

  • 波动率溢出(Volatility Spillover):被压抑的价格变动能量在后续交易日释放,导致异于常模的波动率。
  • 收益率自相关:涨停板的次日往往有正向收益(动量效应),跌停板的次日往往有负向收益(反转效应可能更强)。

磁吸效应(Magnet Effect):当价格接近涨跌停线时,交易者担心价格触及涨跌停后无法交易,可能加速买入/卖出,推动价格加速触碰涨跌停板。

涨停板排队策略

涨停板排队策略是 A 股最具特色的量化策略之一。其核心逻辑是:在股票涨停封板后,判断该封板的"质量"(封板强度),对于高质量封板的股票排队买入,博取次日的高开溢价。

封板强度量化

封板强度的核心量化指标:

封单量/成交额比(封成比)

$$\text{封成比} = \frac{\text{涨停价买一挂单量}}{\text{当日累计成交量}}$$

封成比越高,说明买盘意愿越强,封板质量越高。经验上封成比 > 1.0 的封板具有较好的持续性。

撤单率:在封板期间,部分挂出的买单会被撤掉(尤其是主力资金测试市场反应的试探性挂单)。撤单率反映了封板资金的坚定程度。

$$\text{撤单率} = \frac{\text{封板期间累计撤单量}}{\text{封板期间累计挂单量} + \text{封板期间累计撤单量}}$$

撤单率过高(>30%)通常意味着封板资金信心不足,属于"假封板"。

封板时间:封板时间越早(如开盘即封板),说明买方力量越强,次日溢价可能性越大。

开板次数:封板后又被打开的次数。多次开板表明多空分歧大,封板质量差。

封板时价格位置:封板发生在股价的相对低位(如长期横盘后的突破)还是高位(如连续涨停后),对后续走势有截然不同的含义。

信号生成逻辑

PYTHON162 行 · 5.5 KB
📄此处有展示代码162 行 · 5.5 KB展开 ▼
python
import numpy as np
import pandas as pd


class LimitUpSignalGenerator:
    """涨停板信号生成器"""
    
    def __init__(self, tick_data: pd.DataFrame, daily_data: pd.DataFrame):
        """
        Parameters
        ----------
        tick_data : pd.DataFrame
            tick级别数据,含买卖盘口
        daily_data : pd.DataFrame
            日频数据,含开盘价、收盘价、成交量等
        """
        self.tick_data = tick_data
        self.daily_data = daily_data
        
    def identify_limit_up_events(self, limit_pct: float = 0.10) -> pd.DataFrame:
        """识别涨停事件"""
        daily = self.daily_data.copy()
        
        # 计算涨停价格
        daily['limit_up_price'] = (daily['pre_close'] * (1 + limit_pct)).round(2)
        daily['limit_down_price'] = (daily['pre_close'] * (1 - limit_pct)).round(2)
        
        # 识别涨停日:收盘价 == 涨停价且成交量 > 0
        daily['is_limit_up'] = (daily['close'] >= daily['limit_up_price'] - 0.001) & \
                                (daily['volume'] > 0)
        
        return daily[['is_limit_up', 'limit_up_price']]
    
    def compute_seal_strength(self, stock_code: str, date: str) -> dict:
        """计算封板强度指标"""
        ticks = self.tick_data[
            (self.tick_data['code'] == stock_code) & 
            (self.tick_data['date'] == date)
        ].copy()
        
        if len(ticks) == 0:
            return {}
        
        # 找到封板区间(价格 == 涨停价 的 tick)
        limit_up_price = self.daily_data.loc[
            (self.daily_data['code'] == stock_code) & 
            (self.daily_data['date'] == date), 'limit_up_price'
        ].iloc[0]
        
        sealed_ticks = ticks[abs(ticks['price'] - limit_up_price) < 0.01]
        
        if len(sealed_ticks) == 0:
            return {'is_sealed': False}
        
        # 封板开始时间
        seal_start = sealed_ticks['time'].min()
        seal_duration = sealed_ticks['time'].max() - sealed_ticks['time'].min()
        
        # 封单量(买一挂单量)
        avg_bid_volume = sealed_ticks['bid_volume_1'].mean()
        
        # 累计成交量
        total_volume = ticks['volume'].sum()
        
        # 封成比
        seal_volume_ratio = avg_bid_volume / total_volume if total_volume > 0 else 0
        
        # 撤单分析:比较挂单量的变化
        bid_changes = sealed_ticks['bid_volume_1'].diff()
        cancellations = abs(bid_changes[bid_changes < 0].sum())
        total_bid = sealed_ticks['bid_volume_1'].sum()
        cancel_rate = cancellations / (total_bid + cancellations) if (total_bid + cancellations) > 0 else 0
        
        # 开板检测:价格曾离开涨停价又回来
        seal_breaches = 0
        prev_sealed = False
        for _, row in ticks.iterrows():
            is_at_limit = abs(row['price'] - limit_up_price) < 0.01
            if prev_sealed and not is_at_limit:
                seal_breaches += 1
            prev_sealed = is_at_limit
        
        return {
            'is_sealed': True,
            'seal_start_time': seal_start,
            'seal_duration': seal_duration,
            'seal_volume_ratio': seal_volume_ratio,
            'cancel_rate': cancel_rate,
            'seal_breaches': seal_breaches,
            'avg_bid_volume': avg_bid_volume
        }
    
    def generate_signal(self, stock_code: str, date: str) -> dict:
        """生成涨停板交易信号"""
        strength = self.compute_seal_strength(stock_code, date)
        
        if not strength.get('is_sealed'):
            return {'action': 'NONE', 'reason': '未涨停'}
        
        score = 0
        
        # 封成比评分(0-40分)
        if strength['seal_volume_ratio'] > 2.0:
            score += 40
        elif strength['seal_volume_ratio'] > 1.0:
            score += 30
        elif strength['seal_volume_ratio'] > 0.5:
            score += 15
        
        # 撤单率评分(0-25分)
        if strength['cancel_rate'] < 0.10:
            score += 25
        elif strength['cancel_rate'] < 0.20:
            score += 15
        elif strength['cancel_rate'] < 0.30:
            score += 5
        
        # 开板次数评分(0-20分)
        if strength['seal_breaches'] == 0:
            score += 20
        elif strength['seal_breaches'] == 1:
            score += 10
        
        # 封板时间评分(0-15分,越早越好)
        seal_hour = strength['seal_start_time'].hour
        if seal_hour < 10:
            score += 15
        elif seal_hour < 11:
            score += 10
        elif seal_hour < 14:
            score += 5
        
        # 决策
        if score >= 70:
            action = 'BUY'
            confidence = 'HIGH'
        elif score >= 50:
            action = 'BUY'
            confidence = 'MEDIUM'
        else:
            action = 'NONE'
            confidence = 'LOW'
        
        return {
            'action': action,
            'confidence': confidence,
            'score': score,
            'strength_metrics': strength,
            'expected_next_day_return': self._estimate_next_day_return(score)
        }
    
    def _estimate_next_day_return(self, score: float) -> float:
        """
        估计次日溢价。基于历史数据拟合评分到次日收益的映射。
        此处使用简化的经验公式。
        """
        if score >= 70:
            return 0.025  # 2.5% 预期次日收益
        elif score >= 50:
            return 0.012
        else:
            return -0.005
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:类 `LimitUpSignalGenerator`(涨停板信号生成器)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

策略逻辑

涨停板排队的典型策略逻辑:

  1. 盘中监控:实时扫描触及涨停板的股票。
  2. 封板质量评估:计算封成比、撤单率、开板次数等指标。
  3. 选择性排队:只在高评分封板上挂涨停价买单。注意:挂单时间越早,在涨停价队列中位置越靠前,成交概率越高。
  4. 次日卖出:默认次日开盘卖出。如果次日继续涨停(一字板),可以继续持有。
  5. 回撤控制:如果次日开盘大跌(如低开超过 2%),立即止损。

跌停板避险与逆向策略

跌停板不仅影响多头持仓的风险管理,也创造了逆向投资的机会。

跌停板避险

持有个股跌停时,持仓可能无法在当日卖出止损,导致亏损跨夜放大。应对策略:

  • 预警减仓:在股票接近跌停(如跌幅达到 8%)但尚未封板时提前卖出,避免跌停后无法平仓。
  • 跌停板计价:在策略回测中,跌停日的平仓价格不应取收盘价或中间价,而应以跌停板价格计算,且应考虑成交概率。

跌停板逆向策略

跌停板逆向策略的逻辑是:市场在恐慌性抛售中可能过度反应,跌停板作为一种极端的卖出信号,可能标志着价格见底。

入场条件

  • 股票连续跌停(通常 2-3 个跌停板后)
  • 跌停板被打开(成交量放大,有大资金接盘)
  • 基本面没有实质性恶化(排除财务造假等实质性利空)
PYTHON36 行 · 1.3 KB
📄此处有展示代码36 行 · 1.3 KB展开 ▼
python
def limit_down_reversal_signal(daily_data: pd.DataFrame, 
                                consecutive_limit_downs: int = 2) -> pd.Series:
    """
    跌停板逆向策略信号
    
    逻辑:连续跌停后首次开板,可能是恐慌情绪的逆转点
    """
    daily = daily_data.copy()
    
    # 计算跌停价
    daily['limit_down'] = daily['pre_close'] * 0.90
    
    # 识别跌停日
    daily['is_limit_down'] = daily['close'] <= daily['limit_down'] + 0.001
    
    # 连续跌停计数
    daily['consecutive_ld'] = daily.groupby('code')['is_limit_down'].transform(
        lambda x: x * (x.groupby((x != x.shift()).cumsum()).cumcount() + 1)
    )
    
    # 信号条件:
    # 1. 前一日连续跌停 >= consecutive_limit_downs
    # 2. 当日不再跌停(开板)
    # 3. 当日成交量放大(相对前5日均值的1.5倍)
    prev_consecutive = daily.groupby('code')['consecutive_ld'].shift(1)
    
    avg_vol_5d = daily.groupby('code')['volume'].transform(
        lambda x: x.rolling(5).mean()
    )
    
    signal = (prev_consecutive >= consecutive_limit_downs) & \
             (~daily['is_limit_down']) & \
             (daily['volume'] > avg_vol_5d * 1.5) & \
             (daily['close'] > daily['open'])  # 当日收阳
    
    return signal.astype(int)
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `limit_down_reversal_signal`(跌停板逆向策略信号)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

涨跌停板数据的时间序列特征

研究涨跌停板的时间序列特征,有助于理解市场情绪和风险状况:

PYTHON46 行 · 2.0 KB
📄此处有展示代码46 行 · 2.0 KB展开 ▼
python
class LimitBoardTimeSeriesAnalysis:
    """涨跌停板时间序列分析"""
    
    def __init__(self, daily_data: pd.DataFrame):
        self.daily = daily_data
        
    def daily_limit_up_ratio(self) -> pd.Series:
        """每日涨停股票占比"""
        limit_up_counts = self.daily[self.daily['is_limit_up']].groupby('date').size()
        total_counts = self.daily.groupby('date').size()
        return (limit_up_counts / total_counts).fillna(0)
    
    def limit_up_concentration(self) -> pd.Series:
        """
        涨停板集中度指数:涨停股票集中在少数行业还是分散在各行业
        
        高集中度通常对应主题驱动行情,低集中度对应普涨行情
        """
        limit_up_stocks = self.daily[self.daily['is_limit_up']]
        
        # 使用赫芬达尔指数(HHI)衡量行业集中度
        hhi = limit_up_stocks.groupby(['date', 'sector']).size() \
                             .groupby('date').apply(
            lambda x: ((x / x.sum()) ** 2).sum()
        )
        return hhi
    
    def limit_board_market_sentiment(self) -> pd.DataFrame:
        """
        涨跌停板市场情绪指标
        
        计算涨停数、跌停数、涨跌停比,作为市场情绪的量化代理
        """
        daily_stats = pd.DataFrame()
        daily_stats['limit_up_count'] = self.daily[self.daily['is_limit_up']].groupby('date').size()
        daily_stats['limit_down_count'] = self.daily[self.daily['is_limit_down']].groupby('date').size()
        
        daily_stats['up_down_ratio'] = daily_stats['limit_up_count'] / \
                                        daily_stats['limit_down_count'].replace(0, np.nan)
        
        # 情绪指标Z-score
        daily_stats['sentiment_z'] = (daily_stats['up_down_ratio'] - 
                                       daily_stats['up_down_ratio'].rolling(60).mean()) / \
                                      daily_stats['up_down_ratio'].rolling(60).std()
        
        return daily_stats
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:类 `LimitBoardTimeSeriesAnalysis`(涨跌停板时间序列分析)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

涨跌停板策略是 A 股量化中最具本土特色的策略类别。随着注册制改革深化和市场成熟度提升,涨停板的频率和溢价水平在长期趋势中可能下降,但其作为市场微观结构的核心特征仍将持续发挥作用。掌握涨停板策略的分析框架,不仅有助于开发具体的交易策略,也有助于理解 A 股市场的整体运行逻辑。