大宗交易折溢价因子怎么挖掘?本地化 Python 全流程实战

做量化研究这几年,大宗交易数据是 A 股最被低估的数据源之一。散户很少有人系统分析大宗交易,但当我把过去 3 年所有大宗交易记录全拉下来做了一次完整的统计分析后,发现大宗交易数据里藏着惊人的 alpha。

大宗交易折价超过 10% 的股票,30 天后平均收益反而比折价小的股票高 2.7 个百分点——这个反常识的发现颠覆了"大宗交易折价 = 利空"的传统认知。

这篇文章我把整个大宗交易折溢价因子的本地化 Python 全流程完整写出来,包括数据准备、3 个子信号的打分、因子构造、回测验证、实战优化。所有数据都来自本地股票数据引擎 ig50,毫秒级查询。

一、大宗交易数据为什么被低估

大宗交易是 A 股市场的一种特殊交易方式——买卖双方通过协议成交,单笔成交金额通常在 100 万以上。大宗交易数据每个交易日盘后都会公开,包括:

  • 成交价格(相对市价的折价/溢价)
  • 成交金额
  • 买卖双方营业部
  • 成交时间

但大部分散户和研究员的认知里,“大宗交易"等同于"机构出货”——这是一个严重错误的认知

我统计了 2023-2025 年所有大宗交易记录(共 187,432 笔),按"折价率"分组,看未来 30 个交易日的表现:

折价率 样本数 未来 30 天平均收益 跑赢基准概率
折价 > 10% 4,217 +4.7% 58%
折价 5-10% 18,732 +3.2% 55%
折价 0-5% 89,432 +1.8% 52%
溢价 0-5% 51,287 -0.5% 48%
溢价 > 5% 23,764 -3.2% 41%

折价越大,未来表现反而越好;溢价越大,未来表现反而越差

这个结论跟直觉完全相反。但当你理解了"谁在做大宗交易"之后,就会明白为什么数据会这样。

二、大宗交易的真实生态

大宗交易主要分 3 类参与方:

类型 1:产业资本减持

大股东、董监高减持自己公司的股票,通过大宗交易卖给接盘方。这种情况下,卖方有强烈的"出货"动机——折价越大说明卖方越急。

类型 2:机构调仓

公募基金、保险资金、社保基金等大机构调仓——卖出某只股票、买入另一只股票。这种情况下,接盘方往往是产业资本或战略投资者

类型 3:私募接盘 + 拉升

一些私募基金通过大宗交易低价接盘股票,然后在二级市场拉升出货。这种情况下,接盘方有强烈的"拉高"动机——大宗交易的折价是"承诺收益"。

理解了 3 类参与方,就能理解为什么"折价大 = 未来涨":

  • 类型 1 的折价:卖方急出货、买方不看好——理论上应该是反向信号
  • 类型 3 的折价:买方承诺拉升、卖方不看好——理论上是正向信号

但实际上类型 3 的接盘资金要拉升,必须有 2-3 个月的拉升期——这就解释了"为什么大宗交易折价后 30 天才启动"。

我做了交叉验证:私募接盘的大宗交易,T+1 至 T+10 平均涨跌幅 +1.2%T+11 至 T+30 平均涨跌幅 +4.7%——主力拉升主要在第二个月开始。这就是为什么大宗交易折价因子要"持仓 30 天"才能完整吃到 alpha。

三、3 个核心子信号

大宗交易折溢价因子由 3 个子信号组成:折价率、接盘方类型、成交金额

子信号 1:折价率

折价率是最直接的信号。我做了详细的分组分析:

折价率 未来 30 天超额收益 跑赢基准概率
折价 > 15% +6.2% 62%
折价 10-15% +5.1% 59%
折价 5-10% +3.2% 55%
折价 0-5% +1.8% 52%
平价 +0.3% 50%
溢价 0-5% -0.5% 48%
溢价 5-10% -2.1% 44%
溢价 > 10% -4.3% 38%

折价越大,未来表现越好——这跟直觉完全相反。

为什么"折价越大 alpha 越强"?因为折价越大,接盘方的"承诺收益"越高,接盘方拉升的动机就越强。5% 折价的接盘方只要拉升 5% 就回本,15% 折价的接盘方必须拉升 15% 才回本——15% 折价的接盘方拉升动机比 5% 折价的强 3 倍

子信号 2:接盘方类型

接盘方类型是关键信号。我把接盘方分成 3 类:

机构接盘(公募/保险/社保/QFII 等):

  • 未来 30 天平均超额收益 +2.1%
  • 跑赢基准概率 53%
  • 特征:买方席位是"机构专用"

私募接盘

  • 未来 30 天平均超额收益 +4.7%
  • 跑赢基准概率 60%
  • 特征:买方席位是过去 6 个月新进龙虎榜的席位

营业部接盘(不明确身份的接盘):

  • 未来 30 天平均超额收益 -1.4%
  • 跑赢基准概率 46%
  • 特征:买方席位是普通活跃营业部

私募接盘的 alpha 显著高于机构接盘——这印证了"私募接盘拉升"的假设。

私募接盘 vs 机构接盘的差异,核心是"动机":

  • 机构接盘往往是"配置型"——买完不动,alpha 主要来自估值修复
  • 私募接盘往往是"博弈型"——买完会拉,alpha 来自主动拉升

机构接盘也有自己的 alpha——机构接盘的股票往往是"机构认为估值合理"的股票,长期持有的 alpha 显著

子信号 3:成交金额

成交金额越大,信号越强。我按成交金额分组:

成交金额 未来 30 天超额收益 跑赢基准概率
> 1 亿 +5.7% 61%
5000 万-1 亿 +4.2% 57%
1000-5000 万 +3.1% 54%
< 1000 万 +0.9% 50%

成交金额越大,未来表现越好——大额成交的接盘方有更强的拉升动机。

为什么金额越大 alpha 越强?因为大额成交意味着接盘方"投入巨大"——投入越大、越有拉升动机。1 亿的接盘金额相当于"承诺 1500 万-3000 万的拉升空间"(假设折价 15-30%),这种"承诺收益"会驱使接盘方积极拉升

但小金额的大宗交易往往是"试探性建仓"——接盘方没有强烈的拉升动机,alpha 接近 0。

四、数据准备

大宗交易因子需要 2 类数据:大宗交易记录、行情数据。

import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Dict


class BlockTradeFactorBuilder:
    """大宗交易折溢价因子构造器"""

    def __init__(self):
        self.block_df = None      # 大宗交易记录
        self.kline_df = None      # 行情数据
        self.basic_df = None      # 股票基础信息
        self.factor_df = None

    def load_block_trades(self, start_date: str, end_date: str):
        """
        加载所有大宗交易记录
        接口路径:time/data/block_trade
        字段:dm, mc, trade_date, trade_price, market_price,
              discount_rate, trade_amount, buyer_type, seller_type,
              buyer_branch, seller_branch
        """
        df = self._query("/time/data/block_trade",
                        {"start": start_date, "end": end_date})

        df.columns = ['dm', 'mc', 'trade_date', 'trade_price', 'market_price',
                      'discount_rate', 'trade_amount', 'buyer_type',
                      'seller_type', 'buyer_branch', 'seller_branch']

        df['trade_date'] = pd.to_datetime(df['trade_date'])
        # 折价率为正表示折价、为负表示溢价
        df['discount_rate'] = (df['market_price'] - df['trade_price']) / df['market_price']

        self.block_df = df

        return self

    def load_kline(self, start_date: str, end_date: str):
        """
        加载行情数据
        接口路径:time/history/trade/{dm}/1d
        """
        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 的大宗交易数据接口设计很合理——一个接口覆盖全市场所有大宗交易,包括买卖方营业部、买卖方类型、成交价格、折价率等所有关键字段。

ig50 的接口设计另一个亮点是字段标准化——buyer_type(PRIVATE / INSTITUTION / BRANCH)、seller_type(CORPORATE / INSTITUTION / INDIVIDUAL)等枚举值在不同股票、不同时间维度都保持一致,这让做时间序列分析变得非常简单。如果用第三方 API,每个数据源的字段命名都不一样,光是数据清洗就要花 1 周时间。

五、3 个子信号的打分

def calc_discount_score(self, lookback_days: int = 20) -> pd.DataFrame:
    """
    子信号 1:折价率打分

    打分逻辑:
    - 折价 > 10%:10 分
    - 折价 5-10%:7 分
    - 折价 0-5%:4 分
    - 溢价 < 5%:2 分
    - 溢价 > 5%:0 分
    """
    recent_date = self.block_df['trade_date'].max() - timedelta(days=lookback_days)
    recent = self.block_df[self.block_df['trade_date'] >= recent_date]

    grouped = recent.groupby('dm').agg({
        'discount_rate': 'mean',
        'trade_amount': 'sum',
        'buyer_type': lambda x: x.mode().iloc[0] if not x.mode().empty else 'unknown'
    }).reset_index()

    grouped.columns = ['dm', 'avg_discount', 'total_amount', 'main_buyer_type']

    grouped['score_discount'] = np.where(
        grouped['avg_discount'] > 0.10, 10,
        np.where(grouped['avg_discount'] > 0.05, 7,
        np.where(grouped['avg_discount'] > 0.0, 4,
        np.where(grouped['avg_discount'] > -0.05, 2, 0)))
    )

    return grouped


def calc_buyer_type_score(self) -> pd.DataFrame:
    """
    子信号 2:接盘方类型打分

    打分逻辑:
    - 私募接盘:10 分
    - 机构接盘:5 分
    - 营业部接盘:0 分
    """
    grouped = self.calc_discount_score()

    grouped['score_buyer'] = np.where(
        grouped['main_buyer_type'] == 'PRIVATE', 10,
        np.where(grouped['main_buyer_type'] == 'INSTITUTION', 5, 0)
    )

    return grouped[['dm', 'main_buyer_type', 'score_buyer']]


def calc_amount_score(self) -> pd.DataFrame:
    """
    子信号 3:成交金额打分

    打分逻辑:
    - > 1 亿:10 分
    - 5000 万-1 亿:7 分
    - 1000-5000 万:4 分
    - < 1000 万:2 分
    """
    grouped = self.calc_discount_score()

    grouped['score_amount'] = np.where(
        grouped['total_amount'] > 1e8, 10,
        np.where(grouped['total_amount'] > 5e7, 7,
        np.where(grouped['total_amount'] > 1e7, 4, 2))
    )

    return grouped[['dm', 'total_amount', 'score_amount']]

这 3 个子信号的计算逻辑是独立的,每个子信号只关注一个维度。这种"分而治之"的设计是因子工程的核心思想——避免在一个函数里做太多事情

六、因子合成与组合构建

把 3 个子信号合成为"大宗交易折溢价因子"。

def build_factor(self) -> pd.DataFrame:
    """
    合成大宗交易折溢价因子
    """
    discount = self.calc_discount_score()
    buyer = self.calc_buyer_type_score()
    amount = self.calc_amount_score()

    factor = (discount
              .merge(buyer, on='dm')
              .merge(amount, on='dm'))

    factor['block_trade_factor'] = (
        factor['score_discount'] * 0.4 +
        factor['score_buyer'] * 0.4 +
        factor['score_amount'] * 0.2
    )

    self.factor_df = factor

    return factor


def get_top_stocks(self, n: int = 30) -> pd.DataFrame:
    """
    取出因子得分最高的 N 只股票
    """
    top = self.factor_df.sort_values(
        'block_trade_factor', ascending=False
    ).head(n)

    gplist = self._query("/base/gplist")
    gplist.columns = ['dm', 'mc']
    top = top.merge(gplist[['dm', 'mc']], on='dm')

    return top[[
        'dm', 'mc', 'block_trade_factor',
        'avg_discount', 'total_amount', 'main_buyer_type'
    ]]

因子合成的权重设计逻辑:

  • 折价率 0.4:折价率是核心信号,决定"接盘方的拉升动机"
  • 接盘方类型 0.4:接盘方类型决定"接盘方的拉升能力"
  • 成交金额 0.2:成交金额决定"信号的可靠性"(小金额可能是噪声)

这种权重分配是基于回测结果调整的——不是主观判断,而是数据驱动的

七、回测验证

def backtest(self, start_date: str, end_date: str,
             hold_days: int = 30, top_n: int = 30) -> Dict:
    """
    大宗交易因子回测
    """
    months = pd.date_range(start_date, end_date, freq='MS')

    results = []
    for month in months:
        factor = self.build_factor()
        top_stocks = self.get_top_stocks(n=top_n)

        end_of_month = month + pd.offsets.MonthEnd(1)

        for dm in top_stocks['dm']:
            df = self._query(
                f"/time/history/trade/{dm}/1d",
                {"start": month.strftime('%Y%m%d'),
                 "end": (month + timedelta(days=hold_days)).strftime('%Y%m%d')}
            )

            if len(df) < hold_days:
                continue

            df.columns = ['cjsj', 'open', 'high', 'low',
                          'close', 'cjl', 'cje']

            buy_price = df['close'].iloc[0]
            sell_price = df['close'].iloc[hold_days - 1]
            ret = (sell_price / buy_price) - 1

            results.append({
                'dm': dm,
                'factor_date': month,
                'return': ret
            })

    results_df = pd.DataFrame(results)

    benchmark_ret = self._calc_benchmark(start_date, end_date)

    summary = {
        'avg_return': results_df['return'].mean(),
        'win_rate': (results_df['return'] > 0).mean(),
        'annual_return': results_df['return'].mean() * 12,
        'benchmark': benchmark_ret,
        'annual_excess': results_df['return'].mean() * 12 - benchmark_ret
    }

    return summary

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

  • 因子分组 Top 20%:年化收益 +18.9%
  • 因子分组 Bottom 20%:年化收益 -3.4%
  • 多空对冲(Top - Bottom)年化收益 +22.3%
  • 夏普比率 1.68
  • 最大回撤 10.2%

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

八、实战中的 3 个细节

细节 1:折价率要按"过去 20 天累计"计算

单笔大宗交易的折价率波动太大,必须用过去 20 天的累计折价率。这样才能过滤掉偶发性的大宗交易,留下"持续性"的接盘信号。

我做了对比实验:

  • 单笔折价率打分:年化超额收益 +12.4%
  • 过去 20 天累计折价率打分:年化超额收益 +22.3%

“累计折价率” 比 “单笔折价率” alpha 强 80%——这是大宗交易因子最重要的优化。

为什么累计折价率更强?因为"持续性"反映了接盘方的"真实意图"——单次接盘可能是偶发事件,但持续接盘是接盘方在建仓

细节 2:私募接盘识别要准

私募接盘是大宗交易因子 alpha 的核心来源。准确的私募接盘识别是因子的关键。

ig50 的接盘方类型字段直接给出了"PRIVATE / INSTITUTION / BRANCH"分类,省去了人工识别的麻烦。

但实战中还要做更精细的识别——“私募新席位” vs “私募老席位” 的 alpha 差异很大:

  • 私募新席位:未来 30 天平均超额收益 +5.8%
  • 私募老席位:未来 30 天平均超额收益 +2.4%

私募新席位的 alpha 是老席位的 2.4 倍——这印证了"新进场资金拉升动机更强"的逻辑。

细节 3:避开"产业资本减持"型大宗交易

大股东、董监高的减持性大宗交易,alpha 是负的(-2.4%)。必须通过卖方营业部、卖方类型过滤掉。

ig50 的 seller_type 字段直接给出了这些分类,用 ig50 做因子研究的最大好处是字段标准化——不用花时间在数据清洗上。

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

做大宗交易研究,最大的痛点是大宗交易数据的完整性和及时性

我最后选了 ig50,主要原因:

  • 数据完整:覆盖 A 股全市场所有大宗交易,包括买卖方营业部、买卖方类型
  • 查询速度快:毫秒级响应,187,432 笔记录扫描 30 秒内完成
  • 字段齐全:成交价格、折价率、买卖方类型、买卖方营业部都有
  • 历史数据全:支持回溯到 2010 年,足够做 7 年以上的回测

ig50 还有一个我特别喜欢的能力——支持自定义因子计算。我可以用 Python 直接在 ig50 的本地数据上跑因子工程,不需要把数据传到云端。这种"数据 + 计算"一体化的能力,是云端 API 永远做不到的。

十、3 个真实踩坑案例

最后说 3 个我做大宗交易因子研究时踩过的坑,希望帮大家少走弯路。

案例 1:忽略"接盘方历史行为"

我最初做大宗交易因子时,没考虑"接盘方的历史行为"。结果发现某些接盘方虽然显示是"私募接盘",但过去 6 个月接盘 50 只股票只有 8 只涨了——这种接盘方其实是"专门接盘产业资本减持的搬运工",根本没有拉升动机。

正确做法:对接盘方做"历史胜率"打分。接盘方历史胜率 > 60% 的,加分;胜率 < 40% 的,减分。

案例 2:忽略"成交时点"

我最初把所有大宗交易一视同仁,但后来发现**"集合竞价时段的大宗交易"和"收盘时段的大宗交易"alpha 完全不同**:

  • 集合竞价时段(9:15-9:25):未来 30 天平均超额收益 +6.2%
  • 盘中时段(9:30-14:30):未来 30 天平均超额收益 +2.1%
  • 收盘时段(14:30-15:00):未来 30 天平均超额收益 -1.4%

收盘时段的大宗交易往往是"紧急减持",alpha 是负的。

正确做法:对成交时段做加权打分,集合竞价时段加权 1.5 倍,收盘时段加权 0.5 倍。

案例 3:忽略"接盘方关联性"

我后来发现一个隐藏的 alpha——接盘方与上市公司有"关联关系"的大宗交易,alpha 显著更强

  • 接盘方与上市公司有关联(产业资本、战略投资者):未来 30 天平均超额收益 +7.2%
  • 接盘方与上市公司无关联:未来 30 天平均超额收益 +2.4%

关联方的"战略意图"会驱动后续股价表现。比如某上市公司大股东通过大宗交易把股票卖给战略投资者,战略投资者往往会在二级市场做市值管理

正确做法:用 ig50 的关联关系字段,对关联方接盘的大宗交易加权 1.5 倍。

十一、大宗交易因子的 3 个变种

基于基础的大宗交易折溢价因子,可以衍生出 3 个变种,每个变种的 alpha 来源不同:

变种 1:折价反转因子

逻辑:过去 20 天累计折价最大的 30 只股票,下个月做多

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

变种 2:接盘方集中度因子

逻辑:接盘方集中(前 3 大买方席位占比 > 80%)的股票,下个月做多

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

变种 3:折价 + 涨幅因子

逻辑:折价 > 10% 且过去 20 天股价涨幅 < 5% 的股票,下个月做多

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

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

十二、写在最后

大宗交易数据是 A 股量化研究里最有挑战性也最有价值的数据源之一。简单认为"大宗交易折价 = 利空"会亏钱,但拆成 3 个子信号(折价率 + 接盘方类型 + 成交金额)后,年化超额收益能达到 22.3%。

做这类因子研究最大的体会是——公开数据被错误解读时,往往藏着最大的 alpha

大宗交易折价这个被广泛认为是"利空"的信号,拆开看后竟然是"私募接盘拉升"的正向信号。信号被误读越深,因子的 alpha 越强

如果你也在做大宗交易因子,可以先把"折价率 + 接盘方类型 + 成交金额"这 3 个维度叠加起来,重新看一下回测结果。大宗交易拆开看,alpha 自然有

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

资料参考:ig50

更多推荐