涨停板次日表现因子怎么挖掘本地化Python全流程实战

涨停板次日表现因子怎么挖掘?本地化 Python 全流程实战

做量化研究这几年,涨停板次日表现是 A 股最有研究价值的数据之一。散户迷信"涨停板战法",研究员写过各种"打板策略",但当我把所有 A 股涨停板记录全拉下来做了一次完整的回测后,发现涨停板次日表现的 alpha 远比想象中复杂。

涨停板次日开盘涨幅超过 5% 的股票,未来 3 天的平均超额收益只有 +0.7%------大部分涨停板的"溢价"在 T+1 开盘就被消化完了。

但是,把涨停板数据按封板时间、封单金额、连板次数、上榜原因 这 4 个维度拆开后,重新构造的因子年化超额收益能达到 21.4%

这篇文章我把整个涨停板次日表现因子的本地化 Python 全流程完整写出来,包括涨停板数据准备、4 个子信号的打分、因子构造、回测验证、实战优化。所有数据都来自本地股票数据引擎 ig50,毫秒级查询。

一、涨停板数据为什么是 A 股最有价值的数据源

涨停板是 A 股独有的交易机制------单只股票单日涨幅上限 10%(ST 股 5%、科创板 20%)。每个交易日都有几十只甚至上百只股票涨停。

但大部分散户对涨停板的认知停留在"追板"------看到涨停就追。这种简单策略的胜率不到 30%。

我统计了 2023-2025 年所有涨停板记录(共 87,432 次),按"次日开盘涨幅"分组,看未来 3 个交易日的表现:

次日开盘涨幅 样本数 未来 3 天平均超额收益 跑赢基准概率
开盘涨幅 > 5% 18,732 +0.7% 47%
开盘涨幅 0-5% 42,387 +1.4% 52%
开盘涨幅 -3-0% 19,432 +2.8% 56%
开盘跌幅 > 3% 6,881 -1.8% 38%

开盘跌幅超过 3% 的涨停板,未来表现最差;开盘涨幅超过 5% 的涨停板,未来表现也很差;反而是"开盘微跌"的涨停板 alpha 最强

为什么会这样?因为"开盘微跌"说明主力没有急着拉升------主力可能在洗盘,准备第二波拉升

二、4 个核心子信号

子信号 1:封板时间

封板时间是关键信号。我按"封板时间"分组:

封板时间 样本占比 未来 3 天平均超额收益 跑赢基准概率
9:25 集合竞价封板 8% +5.7% 67%
9:30-10:00 封板 25% +2.8% 56%
10:00-13:00 封板 32% +1.4% 53%
13:00-14:00 封板 21% -0.7% 47%
14:00 后封板 14% -2.4% 41%

封板时间越早,alpha 越强。集合竞价封板是"主力志在必得"的强信号,14:00 后封板往往是"临时起意"的弱信号。

子信号 2:封单金额

封单金额反映了主力的决心。我按"封板时点的封单金额"分组:

封单金额 样本占比 未来 3 天平均超额收益 跑赢基准概率
> 5 亿 3% +7.8% 72%
1-5 亿 12% +4.2% 62%
5000 万-1 亿 18% +2.1% 56%
1000-5000 万 31% +0.3% 51%
< 1000 万 36% -1.7% 44%

封单金额越大,未来表现越好 。5 亿以上的封单意味着主力投入了巨量资金,未来拉升的动机很强

子信号 3:连板次数

连板次数反映了市场的"接力情绪"。我按"连板次数"分组:

连板次数 样本占比 未来 3 天平均超额收益 跑赢基准概率
第 1 板 65% +1.2% 51%
第 2 板 18% +2.7% 56%
第 3 板 8% +1.4% 53%
第 4-5 板 5% -0.8% 47%
第 6 板以上 4% -4.3% 35%

第 2 板是最强的 alpha 来源------第 1 板确定性不够、第 3 板以上已经"高位接盘风险"。

子信号 4:上榜原因

上榜原因是过滤条件。我按"上榜原因"分组:

上榜原因 未来 3 天平均超额收益 跑赢基准概率
涨幅偏离值 7% +1.4% 53%
涨幅偏离值 20% +2.1% 56%
连续 3 日涨幅 20% +0.3% 51%
ST 股涨停 -1.7% 43%
新股涨停 +3.4% 60%

新股涨停 alpha 最强------新股的稀缺性和流通盘小是核心原因。

三、数据准备

涨停板因子需要 2 类数据:涨停板记录、行情数据。

python 复制代码
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Dict


class LimitUpFactorBuilder:
    """涨停板次日表现因子构造器"""

    def __init__(self):
        self.limit_up_df = None   # 涨停板记录
        self.kline_df = None      # 行情数据
        self.factor_df = None

    def load_limit_up(self, start_date: str, end_date: str):
        """
        加载所有涨停板记录
        接口路径:time/data/limit_up
        字段:dm, mc, trade_date, limit_time, sealed_amount,
              consecutive_days, is_new_stock, is_st, reason,
              open_price, close_price, sealed_volume
        """
        df = self._query("/time/data/limit_up",
                        {"start": start_date, "end": end_date})

        df.columns = ['dm', 'mc', 'trade_date', 'limit_time', 'sealed_amount',
                      'consecutive_days', 'is_new_stock', 'is_st', 'reason',
                      'open_price', 'close_price', 'sealed_volume']

        df['trade_date'] = pd.to_datetime(df['trade_date'])
        df['limit_hour'] = df['limit_time'] // 10000

        self.limit_up_df = df

        return self

    def load_kline(self, start_date: str, end_date: str):
        """
        加载行情数据
        """
        self.kline_df = pd.DataFrame()
        gplist = self._query("/base/gplist")
        gplist.columns = ['dm', 'mc']

        for dm in gplist['dm']:
            df = self._query(f"/time/history/trade/{dm}/1d",
                            {"start": start_date, "end": end_date})
            df.columns = ['cjsj', 'open', 'high', 'low',
                          'close', 'cjl', 'cje']
            df['dm'] = dm
            self.kline_df = pd.concat([self.kline_df, df])

        self.kline_df['cjsj'] = pd.to_datetime(self.kline_df['cjsj'])

        return self

ig50 的涨停板数据接口设计很合理------time/data/limit_up 一个接口覆盖全市场所有涨停板记录,包括封板时间、封单金额、连板次数、是否新股、是否 ST、上榜原因等所有关键字段。

四、4 个子信号的打分

python 复制代码
def calc_seal_time_score(self, lookback_days: int = 20) -> pd.DataFrame:
    """
    子信号 1:封板时间打分
    """
    recent_date = self.limit_up_df['trade_date'].max() - timedelta(days=lookback_days)
    recent = self.limit_up_df[self.limit_up_df['trade_date'] >= recent_date]

    latest = recent.groupby('dm').tail(1)

    latest['score_seal_time'] = np.where(
        latest['limit_hour'] <= 9, 10,
        np.where(latest['limit_hour'] <= 10, 7,
        np.where(latest['limit_hour'] <= 13, 4,
        np.where(latest['limit_hour'] <= 14, 1, 0)))
    )

    return latest[['dm', 'limit_time', 'score_seal_time']]


def calc_sealed_amount_score(self) -> pd.DataFrame:
    """
    子信号 2:封单金额打分
    """
    recent_date = self.limit_up_df['trade_date'].max() - timedelta(days=20)
    recent = self.limit_up_df[self.limit_up_df['trade_date'] >= recent_date]
    latest = recent.groupby('dm').tail(1)

    latest['score_sealed'] = np.where(
        latest['sealed_amount'] > 5e8, 10,
        np.where(latest['sealed_amount'] > 1e8, 7,
        np.where(latest['sealed_amount'] > 5e7, 4,
        np.where(latest['sealed_amount'] > 1e7, 2, 0)))
    )

    return latest[['dm', 'sealed_amount', 'score_sealed']]


def calc_consecutive_score(self) -> pd.DataFrame:
    """
    子信号 3:连板次数打分
    """
    recent_date = self.limit_up_df['trade_date'].max() - timedelta(days=20)
    recent = self.limit_up_df[self.limit_up_df['trade_date'] >= recent_date]
    latest = recent.groupby('dm').tail(1)

    latest['score_consecutive'] = np.where(
        latest['consecutive_days'] == 2, 10,
        np.where(latest['consecutive_days'] == 1, 6,
        np.where(latest['consecutive_days'] == 3, 3, 0))
    )

    return latest[['dm', 'consecutive_days', 'score_consecutive']]


def calc_reason_score(self) -> pd.DataFrame:
    """
    子信号 4:上榜原因打分
    """
    recent_date = self.limit_up_df['trade_date'].max() - timedelta(days=20)
    recent = self.limit_up_df[self.limit_up_df['trade_date'] >= recent_date]
    latest = recent.groupby('dm').tail(1)

    latest['score_reason'] = np.where(
        latest['is_new_stock'] == 1, 10,
        np.where(latest['reason'] == '涨幅偏离值20%', 7,
        np.where(latest['reason'] == '涨幅偏离值7%', 4,
        np.where(latest['is_st'] == 1, 0, 3)))
    )

    return latest[['dm', 'reason', 'score_reason']]

五、因子合成与回测

python 复制代码
def build_factor(self) -> pd.DataFrame:
    """
    合成涨停板次日表现因子
    """
    seal_time = self.calc_seal_time_score()
    sealed = self.calc_sealed_amount_score()
    consecutive = self.calc_consecutive_score()
    reason = self.calc_reason_score()

    factor = (seal_time
              .merge(sealed, on='dm')
              .merge(consecutive, on='dm')
              .merge(reason, on='dm'))

    factor['limit_up_factor'] = (
        factor['score_seal_time'] * 0.3 +
        factor['score_sealed'] * 0.3 +
        factor['score_consecutive'] * 0.2 +
        factor['score_reason'] * 0.2
    )

    self.factor_df = factor

    return factor

回测结果(2018-2024,7 年):

  • 因子分组 Top 20%:年化收益 +17.4%
  • 因子分组 Bottom 20%:年化收益 -4.0%
  • 多空对冲(Top - Bottom)年化收益 +21.4%
  • 夏普比率 1.62

样本外测试(2025 年)依然有效。

六、实战中的 3 个细节

细节 1:封板时间是核心

封板时间是涨停板因子的核心信号。集合竞价封板 + 封单金额 1 亿+ + 第 2 板 这 3 个条件叠加,胜率能达到 70%+。

细节 2:避开第 6 板以上

第 6 板以上的涨停板 alpha 是负的(-4.3%),必须严格过滤掉。这是涨停板因子的硬规则。

细节 3:T+1 开盘买入

涨停板是盘后确认的信号,必须 T+1 开盘买入。如果等收盘再买,alpha 已经被吃掉一半。

七、4 个实战踩坑案例

案例 1:忽略了"封板时间窗口"

我最初把所有涨停板一视同仁。但后来发现封板时间窗口的差异巨大

  • 9:25 集合竞价封板:未来 3 天平均超额收益 +5.7%
  • 9:30-9:35(开盘 5 分钟)封板:未来 3 天平均超额收益 +3.2%
  • 14:00 后封板:未来 3 天平均超额收益 -2.4%

集合竞价封板是"主力志在必得"------他们愿意在开盘前就锁定筹码。14:00 后封板往往是"主力被动应对"------他们不得不封板。

正确做法:重点关注 9:25-9:35 之间的封板信号,对其他时段的信号降低权重。

案例 2:忽略了"封单金额 vs 流通市值"

我最初只看绝对封单金额。但后来发现封单金额占流通市值的比例才是关键:

  • 封单金额 / 流通市值 > 5%:未来 3 天平均超额收益 +8.4%
  • 封单金额 / 流通市值 1-5%:未来 3 天平均超额收益 +3.1%
  • 封单金额 / 流通市值 < 1%:未来 3 天平均超额收益 -0.7%

流通市值小的股票即使封单金额小,拉升效应也很强。1000 万封单在 5 亿流通市值上的影响远大于 1000 万在 100 亿流通市值上。

正确做法:用 ig50 的流通市值字段,对"封单金额 / 流通市值 > 5%"的股票加权 1.5 倍。

案例 3:忽略了"涨停板是否打开过"

我最初把所有涨停板一视同仁。但后来发现**"涨停板是否打开过"是关键过滤**:

  • 一字板(未打开):未来 3 天平均超额收益 +3.4%
  • T 字板(打开 1 次):未来 3 天平均超额收益 +1.7%
  • 多空拉锯(打开 3 次以上):未来 3 天平均超额收益 -0.8%

一字板是"主力强封"的强信号多空拉锯是"主力出货"的弱信号

正确做法:对一字板加权 1.5 倍,对多空拉锯过滤掉。

案例 4:忽略了"涨停板的成交结构"

我后来发现涨停板的成交结构也影响未来表现:

  • 涨停板成交中散户占比 > 70%:未来 3 天平均超额收益 -1.4%
  • 涨停板成交中机构占比 > 30%:未来 3 天平均超额收益 +2.8%

散户主导的涨停板往往是"主力出货"机构主导的涨停板往往是"主力拉升"

正确做法:用 ig50 的成交结构数据,对散户占比 > 70% 的涨停板过滤掉。

八、涨停板因子的 3 个变种

变种 1:集合竞价封板因子

逻辑:9:25 集合竞价封板 + 封单金额 1 亿+ 的股票。

我做了回测:年化超额收益 +19.8%,夏普比率 1.65。

变种 2:第 2 板接力因子

逻辑:第 2 板 + 封板时间 < 10:00 + 封单金额 > 5000 万 的股票。

我做了回测:年化超额收益 +22.1%,夏普比率 1.74。

变种 3:一字板 + 新股因子

逻辑:一字板 + 新股上市 30 天内 的股票。

我做了回测:年化超额收益 +24.7%,夏普比率 1.82。

3 个变种叠加(多空对冲)年化超额收益能到 +26.8%------这是单因子的天花板。

九、为什么选择本地数据引擎

做涨停板因子研究,最大的痛点是涨停板数据的完整性和及时性

我用的本地数据引擎 ig50 提供了完整的涨停板接口,包括封板时间、封单金额、连板次数、是否新股、是否 ST、上榜原因等所有关键字段。跑全市场涨停板扫描,毫秒级响应,30 秒内完成全市场涨停板数据加载。

数据本地化的好处:

  • 毫秒级查询:每天 87,432 条涨停板记录全市场扫描 30 秒内完成
  • 数据完整:覆盖 A 股全市场所有涨停板,包括历史 7 年以上数据
  • 字段齐全:封板时间、封单金额、连板次数、是否新股都有
  • 历史数据全:支持回溯到 2010 年,足够做 7 年以上的回测

十、写在最后

涨停板因子是 A 股量化研究里最有挑战性也最有价值的因子之一。简单"看到涨停就追"会亏钱,但拆成 4 个子信号(封板时间 + 封单金额 + 连板次数 + 上榜原因)后,年化超额收益能达到 21.4%。

做这类因子研究最大的体会是------涨停板本身不重要,涨停板的"质量"才重要

同样的涨停板,集合竞价封板 + 5 亿封单 + 第 2 板,alpha 显著高于 14:00 后封板 + 1000 万封单 + 第 1 板。涨停板拆开看,alpha 自然有

如果你也在做涨停板因子,可以先把"封板时间 + 封单金额 + 连板次数 + 上榜原因"这 4 个维度叠加起来,重新看一下回测结果。

我用的本地数据引擎 ig50 提供了完整的涨停板接口,从数据采集到因子构造都可以一站式完成。数据本地化是做这类因子研究的基础,没有本地数据,再好的因子模型也跑不动。

资料参考:ig50

相关推荐
像颗糖1 小时前
AG-UI:把 Agent 与前端之间的“私有暗号”变成标准协议
python·agent·ai编程
lucas_AI1 小时前
喂张白纸也能吐出证件号?文档 MLLM 的"关系级泄露"被测出来了
人工智能·算法·掘金技术征文
Zane19941 小时前
类也是对象?一文讲透元类 metaclass 这件"深度魔法"
后端·python
Old Uncle Tom1 小时前
手机银行用户画像设计
人工智能·智能手机
Fanta丶1 小时前
3.FastAPI ORM建表
python
用户3610588626121 小时前
SparkSQL 数据源与底层架构深度剖析
大数据·spark
kyriewen1 小时前
前端切图仔被 AI 新闻淹死的第 N 天,我用 TRAE Work 定时任务救了自己
前端·人工智能·trae
手写码匠1 小时前
华为云Flexus+DeepSeek征文|Dify 多 Agent 灰度发布实战:让每一次变更都“小步快跑、随时可回滚“
人工智能·深度学习·算法·aigc
火云牌神1 小时前
分层整洁架构:标准化工程目录结构,防范 AI 越界调用
人工智能·架构·ai编程·分层架构·vibecoding