深度总结一下这份海通证券的研报《选股因子系列研究(五十八)——知情交易与主买主卖》,并基于其核心思想尝试复现策略

好的,我们来深度总结一下这份海通证券的研报,并基于其核心思想尝试复现策略。

报告来源与标题

  • 报告来源: 海通证券 (Haitong Securities)
  • 报告标题: 选股因子系列研究(五十八)——知情交易与主买主卖
  • 发布日期: 2020年02月14日
  • 分析师: 冯佳睿, 袁林青

目录

  1. 报告核心观点总结
  2. 因子构建方法详解
  3. 因子表现分析
  4. 策略复现与Python代码
  5. 风险提示

1. 报告核心观点总结

该报告在前期《选股因子系列研究(五十七)——基于主动买入行为的选股因子》的基础上,进一步引入了知情交易概率模型(借鉴Chang, S.S. (2014)的动态日内方法思路,而非传统的PIN模型)来过滤投资者的主动买卖行为,旨在区分出由掌握未公开信息的“知情”交易者驱动的主动买卖,从而构建更有效的选股因子。

核心观点包括:

  1. 知情交易过滤: 利用日内分钟收益率回归模型剥离可预期收益,将残差(预期外收益)与主动买卖方向结合,识别潜在的知情交易(预期外收益为正时的主动卖出,预期外收益为负时的主动买入)。
  2. 知情主卖因子有效性:
    • 知情主卖占比(特别是全天和开盘后时段计算的)与股票未来收益呈显著负相关(知情卖出越多,未来表现越差)。
    • 正交处理(剔除市值、估值、换手率等常见因子影响)后,开盘后知情主卖占比(占同时段成交额) 表现尤为稳健,月均IC绝对值接近0.03,ICIR绝对值达3.5,多空月均收益0.94%,多头月均收益0.57%。
  3. 知情主买因子有效性:
    • 正交处理后,收盘前知情主买占比 与股票未来收益呈现较为显著的负相关(这与直觉相悖,报告推测可能与收盘前过度反应有关)。
    • 其中,收盘前知情主买占比(占全天成交额) 表现相对更强,因子月度多空收益达1.28%,月度胜率超80%,多头月均收益0.56%。
  4. 知情净主买因子有效性:
    • 正交处理后,开盘后知情净主买占比 具有一定的正相关选股能力(知情净买入越多,未来表现越好)。
    • 开盘后知情净主买占比(占全天成交额 或 占同时段成交额)月均IC约为0.02,ICIR在2.8-3.0之间,月度胜率超80%。
  5. 适用范围: 因子在中证800和中证500指数内表现较好,但在沪深300指数内选股能力有所减弱。

2. 因子构建方法详解

2.1 数据准备
  • 分钟级数据: 股票的分钟 K 线数据(开高低收、成交量、成交额)。
  • 逐笔成交数据: 用于区分主动买单(外盘)和主动卖单(内盘),并计算每分钟的主动买入金额和主动卖出金额。报告中称为“主买成交金额”和“主卖成交金额”。
  • 时间窗口: 报告回测期为 2013.01.04 ~ 2020.01.23。计算知情交易识别模型时,使用了过去20个交易日的分钟数据。
  • 计算时段: 报告分别计算了基于不同日内时段的因子:
    • 全天: 9:30 - 14:56
    • 开盘后: 9:30 - 9:59
    • 盘中: 10:00 - 14:26
    • 收盘前: 14:27 - 14:56
2.2 知情交易的识别

核心在于构建一个分钟收益率的回归模型,以分离预期外收益。模型形式如下 (Page 5):

R_i,T,j = ν_0 + Σ[k=1 to 4](γ_1,k * D_T,k^Weekday) + Σ[k=1 to 3](γ_2,k * D_T,k,j^Period) + γ_3 * R_i,T,j-1 + ε_i,T,j

其中:

  • R_i,T,j: 股票 i 在 T 日第 j 分钟的收益率。
  • ν_0: 常数项。
  • D_T,k^Weekday: 星期虚拟变量 (k=1,2,3,4 代表周一至周四)。
  • D_T,k,j^Period: 时间段虚拟变量 (k=1 代表开盘后30分钟,k=2 代表盘中时段,k=3 代表收盘前30分钟)。
  • R_i,T,j-1: 上一分钟的收益率(滞后项)。
  • ε_i,T,j: 回归残差,即预期外收益 (Unexpected Return)

回归设定: 使用某股票过去20个交易日的分钟数据进行上述回归,得到模型的系数(ν, γ)。然后用这些系数预测当期 (T日) 每分钟 j 的预期收益。

知情交易判定:

  • 知情主卖 (Informed Sell): 如果 T 日第 j 分钟的实际收益率 R_i,T,j 大于模型预测的预期收益(即残差 ε_i,T,j > 0),则该分钟发生的主动卖出金额被认为是“知情主卖”金额。
  • 知情主买 (Informed Buy): 如果 T 日第 j 分钟的实际收益率 R_i,T,j 小于模型预测的预期收益(即残差 ε_i,T,j < 0),则该分钟发生的主动买入金额被认为是“知情主买”金额。
2.3 因子计算

基于识别出的知情主买/主卖金额,计算日度因子值。以“开盘后”时段为例(9:30-9:59):

  • 开盘后知情主卖占比 (占全天成交额): Sum(开盘后每分钟的知情主卖金额) / Sum(全天每分钟的成交额)
  • 开盘后知情主卖占比 (占同时段成交额): Sum(开盘后每分钟的知情主卖金额) / Sum(开盘后每分钟的成交额)
  • 开盘后知情主卖占比 (占同时段主卖额): Sum(开盘后每分钟的知情主卖金额) / Sum(开盘后每分钟的主动卖出金额)

类似地,可以计算:

  • 知情主买占比(分母可为全天成交额、同时段成交额、同时段主买额)
  • 知情净主买占比(分子为 知情主买金额 - 知情主卖金额,分母可为全天成交额、同时段成交额、同时段净主买额)

报告对全天、开盘后、盘中、收盘前四个时段都进行了上述计算,产生了多种因子变体。

3. 因子表现分析

报告对因子进行了详细的回测分析(IC、ICIR、分组收益、分年度表现等),并进行了正交化处理。

3.1 知情主卖占比因子
  • 方向: 负相关。值越高,未来收益越差。
  • 显著性: 全天和开盘后时段计算的因子普遍显著。
  • 最优变体(正交后): 开盘后知情主卖占比 (占同时段成交额)。ICIR绝对值达3.5,多空组合年化收益约12.3%(月均0.94% * 12),多头组合年化超额约6.8%(月均0.57% * 12)。
3.2 知情主买占比因子
  • 方向(正交后): 收盘前计算的因子呈现负相关。值越高,未来收益越差。
  • 显著性: 正交后,仅收盘前时段的因子表现较好。
  • 最优变体(正交后): 收盘前知情主买占比 (占全天成交额)。ICIR绝对值达3.05,多空组合年化收益约16.1%(月均1.28% * 12),多头组合年化超额约6.7%(月均0.56% * 12)。
3.3 知情净主买占比因子
  • 方向: 正相关。值越高,未来收益越好。
  • 显著性: 正交后,开盘后时段计算的因子表现较好。
  • 最优变体(正交后): 开盘后知情净主买占比 (占全天成交额 或 占同时段成交额)。ICIR约2.8-3.0,多空组合年化收益约9.3% - 10.3%,多头组合年化超额约4% - 6%。
3.4 因子在不同指数范围的表现
  • 知情主卖因子在中证800/500内仍有效,但在沪深300内效果减弱。
  • 知情主买因子(收盘前)在中证800内仍有一定效果,但在沪深300内效果不佳。
  • 知情净主买因子(开盘后)在中证800/500内仍有效,但随范围缩小(进入沪深300),效果也减弱。

这表明因子在中小盘股中区分度可能更高。

4. 策略复现与Python代码

4.1 策略思路

基于报告结论,我们可以选择表现较优且逻辑相对清晰的因子进行复现。例如:

  1. 策略一(负向选股): 做空 开盘后知情主卖占比 (占同时段成交额) 最高的股票组合,同时做多该因子最低的股票组合。
  2. 策略二(正向选股): 做多 开盘后知情净主买占比 (占同时段成交额) 最高的股票组合,同时做空该因子最低的股票组合。

以下代码将重点演示如何计算这两个因子,完整的策略回测需要更复杂的框架(如数据管理、组合构建、交易成本模拟、业绩归因等)。

4.2 所需数据
  • 股票池: 例如中证800成分股。
  • 历史分钟数据: 包含时间(datetime)、开盘价(open)、最高价(high)、最低价(low)、收盘价(close)、成交量(volume)、成交额(amount)。需要至少回溯20个交易日的分钟数据用于回归。
  • 历史逐笔数据或已处理好的分钟级主动买卖额: 用于计算每分钟的主动买入金额 (active_buy_amount) 和主动卖出金额 (active_sell_amount)。若无逐笔,可用通用算法近似(如比较成交价与买一卖一价)。 本代码假设此数据已存在。
  • 交易日历: 用于识别交易日和获取日期信息。
  • 因子数据(用于正交化,可选): 市值、BP、换手率等常见风格因子的日度数据。
4.3 Python代码实现
import pandas as pd
import numpy as np
import statsmodels.api as sm
from datetime import time, timedelta

# 假设我们有以下函数或数据获取方式:
# get_minute_data(stock_code, start_date, end_date): -> pd.DataFrame with columns ['datetime', 'open', 'close', 'volume', 'amount', 'active_buy_amount', 'active_sell_amount']
# get_trading_calendar(start_date, end_date): -> list of pd.Timestamp representing trading days
# get_stock_pool(date): -> list of stock codes

def calculate_minute_returns(minute_data):
    """计算分钟收益率"""
    minute_data['return'] = minute_data['close'].pct_change().fillna(0)
    return minute_data

def prepare_regression_data(stock_code, date, history_window=20):
    """准备用于回归的过去N天分钟数据"""
    trading_calendar = get_trading_calendar('2010-01-01', date) # 假设有足够历史
    end_date = date - timedelta(days=1) # 回归用过去的数据
    start_date = trading_calendar[trading_calendar.index(end_date) - history_window]

    hist_minute_data = get_minute_data(stock_code, start_date, end_date)
    if hist_minute_data is None or hist_minute_data.empty:
        return None

    hist_minute_data = calculate_minute_returns(hist_minute_data)

    # 创建虚拟变量
    hist_minute_data['weekday'] = hist_minute_data['datetime'].dt.weekday
    hist_minute_data['minute_of_day'] = hist_minute_data['datetime'].dt.time

    # 周一到周四 (0-3)
    for i in range(4):
        hist_minute_data[f'weekday_{i}'] = (hist_minute_data['weekday'] == i).astype(int)

    # 时间段虚拟变量 (报告中是开盘后30min, 盘中, 收盘前30min)
    # 为简化,这里用具体时间段,需根据实际交易规则精确定义
    hist_minute_data['period_open'] = ((hist_minute_data['minute_of_day'] >= time(9, 30)) & (hist_minute_data['minute_of_day'] <= time(9, 59))).astype(int)
    hist_minute_data['period_mid'] = ((hist_minute_data['minute_of_day'] >= time(10, 0)) & (hist_minute_data['minute_of_day'] <= time(14, 26))).astype(int)
    hist_minute_data['period_close'] = ((hist_minute_data['minute_of_day'] >= time(14, 27)) & (hist_minute_data['minute_of_day'] <= time(14, 56))).astype(int) # 假设14:57是收盘

    # 滞后收益率
    hist_minute_data['return_lag1'] = hist_minute_data['return'].shift(1).fillna(0)

    # 清理数据,去除开盘第一分钟(若其滞后项无意义)或非交易时间数据
    # ... (具体清理逻辑)

    # 选择用于回归的列
    # 注意:应移除一个weekday虚拟变量和一个period虚拟变量以避免完全共线性
    predictors = ['const', 'weekday_0', 'weekday_1', 'weekday_2', 'weekday_3',
                  'period_open', 'period_mid', # 'period_close' 省略
                  'return_lag1']
    hist_minute_data['const'] = 1.0

    # 确保数据有效性
    hist_minute_data = hist_minute_data.dropna(subset=['return'] + predictors)
    if hist_minute_data.empty:
         return None

    X = hist_minute_data[predictors]
    y = hist_minute_data['return']

    return X, y

def fit_regression_model(X, y):
    """拟合OLS回归模型"""
    if X is None or y is None or X.empty or y.empty:
        return None
    try:
        model = sm.OLS(y, X, missing='drop') # drop missing rows if any
        results = model.fit()
        return results
    except Exception as e:
        print(f"Regression failed: {e}")
        return None

def calculate_informed_trades(stock_code, date, regression_results):
    """计算当天的知情交易额"""
    if regression_results is None:
        return None, None, None

    current_minute_data = get_minute_data(stock_code, date, date)
    if current_minute_data is None or current_minute_data.empty:
        return None, None, None

    current_minute_data = calculate_minute_returns(current_minute_data)

    # 创建与回归时相同的虚拟变量
    current_minute_data['weekday'] = current_minute_data['datetime'].dt.weekday
    current_minute_data['minute_of_day'] = current_minute_data['datetime'].dt.time
    for i in range(4):
        current_minute_data[f'weekday_{i}'] = (current_minute_data['weekday'] == i).astype(int)
    current_minute_data['period_open'] = ((current_minute_data['minute_of_day'] >= time(9, 30)) & (current_minute_data['minute_of_day'] <= time(9, 59))).astype(int)
    current_minute_data['period_mid'] = ((current_minute_data['minute_of_day'] >= time(10, 0)) & (current_minute_data['minute_of_day'] <= time(14, 26))).astype(int)
    current_minute_data['period_close'] = ((current_minute_data['minute_of_day'] >= time(14, 27)) & (current_minute_data['minute_of_day'] <= time(14, 56))).astype(int)
    current_minute_data['return_lag1'] = current_minute_data['return'].shift(1).fillna(0)
    current_minute_data['const'] = 1.0

    predictors = regression_results.model.exog_names # 获取模型使用的确切变量名
    X_current = current_minute_data[predictors]

    # 预测预期收益
    try:
        predicted_return = regression_results.predict(X_current)
    except Exception as e:
         print(f"Prediction failed for {stock_code} on {date}: {e}")
         # Handle cases where prediction fails (e.g., missing data for a predictor)
         # One way is to fill NA in X_current if possible, or skip prediction
         return None, None, None # Or return the original data with NaNs for residuals

    current_minute_data['predicted_return'] = predicted_return
    current_minute_data['residual'] = current_minute_data['return'] - current_minute_data['predicted_return']

    # 识别知情交易
    # 确保 active_buy_amount 和 active_sell_amount 列存在
    if 'active_buy_amount' not in current_minute_data.columns or \
       'active_sell_amount' not in current_minute_data.columns:
        print(f"Missing active trade amount columns for {stock_code} on {date}")
        return None, None, None # Or handle differently based on data availability

    current_minute_data['informed_sell_amount'] = np.where(
        current_minute_data['residual'] > 0,
        current_minute_data['active_sell_amount'],
        0
    )
    current_minute_data['informed_buy_amount'] = np.where(
        current_minute_data['residual'] < 0,
        current_minute_data['active_buy_amount'],
        0
    )

    # 筛选出开盘后时段的数据 (9:30 - 9:59)
    opening_period_data = current_minute_data[
        (current_minute_data['minute_of_day'] >= time(9, 30)) &
        (current_minute_data['minute_of_day'] <= time(9, 59))
    ].copy() # Use .copy() to avoid SettingWithCopyWarning

    # 计算所需聚合值
    opening_informed_sell = opening_period_data['informed_sell_amount'].sum()
    opening_informed_buy = opening_period_data['informed_buy_amount'].sum()
    opening_period_amount = opening_period_data['amount'].sum()
    # opening_period_active_sell = opening_period_data['active_sell_amount'].sum() # 用于计算其他变体
    # opening_period_active_buy = opening_period_data['active_buy_amount'].sum() # 用于计算其他变体
    # total_day_amount = current_minute_data['amount'].sum() # 用于计算其他变体

    # 计算因子
    # 因子1: 开盘后知情主卖占比 (占同时段成交额)
    factor_informed_sell_ratio_open = opening_informed_sell / opening_period_amount if opening_period_amount > 0 else 0

    # 因子2: 开盘后知情净主买占比 (占同时段成交额)
    factor_informed_net_buy_ratio_open = (opening_informed_buy - opening_informed_sell) / opening_period_amount if opening_period_amount > 0 else 0

    return factor_informed_sell_ratio_open, factor_informed_net_buy_ratio_open, current_minute_data # 返回计算好的因子和带标记的分钟数据

def calculate_daily_factors(date, stock_pool):
    """计算指定日期所有股票的因子值"""
    daily_factors = {}
    for stock in stock_pool:
        print(f"Processing {stock} for {date.strftime('%Y-%m-%d')}...")
        # 1. 准备历史数据用于回归
        X_hist, y_hist = prepare_regression_data(stock, date, history_window=20)
        if X_hist is None:
            print(f"Skipping {stock}: Insufficient history data.")
            continue

        # 2. 拟合回归模型
        regression_model = fit_regression_model(X_hist, y_hist)
        if regression_model is None:
            print(f"Skipping {stock}: Regression failed.")
            continue

        # 3. 计算当天的知情交易和因子
        factor1, factor2, _ = calculate_informed_trades(stock, date, regression_model)
        if factor1 is not None and factor2 is not None:
             daily_factors[stock] = {'InformedSellRatioOpen': factor1,
                                      'InformedNetBuyRatioOpen': factor2}
        else:
             print(f"Skipping {stock}: Factor calculation failed.")


    return pd.DataFrame.from_dict(daily_factors, orient='index')

# --- 主程序入口示例 ---
if __name__ == '__main__':
    # 设定日期和股票池
    target_date = pd.Timestamp('2020-01-22') # 示例日期
    # stock_list = get_stock_pool(target_date) # 获取当天的股票池

    # !!! 注意:以下为伪代码/占位符,实际需要接入数据源 !!!
    # --- 伪代码开始 ---
    def get_minute_data(stock_code, start_date, end_date):
        # 这里应连接数据库或API获取数据
        # 返回 Pandas DataFrame, 包含 datetime, open, close, volume, amount, active_buy_amount, active_sell_amount
        # 为演示,返回一个空 DataFrame 或包含随机数据的 DataFrame
        print(f"Fetching minute data for {stock_code} from {start_date} to {end_date} (Placeholder)")
        # Example structure (replace with real data loading)
        dates = pd.date_range(start=start_date, end=end_date, freq='B')
        all_data = []
        for d in dates:
             # Generate 240 minutes of data per day
             times = pd.date_range(start=d.replace(hour=9, minute=31, second=0), periods=240, freq='T')
             df = pd.DataFrame({
                 'datetime': times,
                 'open': np.random.rand(240) * 100,
                 'close': np.random.rand(240) * 100,
                 'volume': np.random.randint(100, 10000, 240),
                 'amount': np.random.randint(10000, 1000000, 240),
                 'active_buy_amount': np.random.randint(5000, 500000, 240),
                 'active_sell_amount': np.random.randint(5000, 500000, 240)
             })
             # Filter trading hours (e.g., remove lunch break if needed)
             df = df[(df['datetime'].dt.time >= time(9, 30)) & (df['datetime'].dt.time <= time(11, 30)) |
                     (df['datetime'].dt.time >= time(13, 0)) & (df['datetime'].dt.time <= time(14, 57))] # Adjusted end time
             all_data.append(df)
        if not all_data:
             return pd.DataFrame()
        return pd.concat(all_data).reset_index(drop=True)


    def get_trading_calendar(start_date, end_date):
        # 返回交易日历列表
        return pd.bdate_range(start_date, end_date).tolist()

    stock_list = ['stock_A', 'stock_B', 'stock_C'] # 示例股票列表
    # --- 伪代码结束 ---

    # 计算因子
    factors_df = calculate_daily_factors(target_date, stock_list)

    print("\nCalculated Factors:")
    print(factors_df)

    # 后续步骤 (需要额外代码):
    # 1. 将因子值保存到数据库或文件
    # 2. 进行因子正交化 (可选, 使用 statsmodels 或 sklearn)
    # 3. 构建投资组合 (例如,根据因子值排序分组)
    # 4. 进行回测,计算组合收益、基准比较、风险指标等
4.4 注意事项
  1. 数据质量和来源: 复现效果极大依赖于分钟数据和准确的主动买卖额数据。如果无法获取逐笔数据来精确计算,使用通用算法近似会引入噪音。
  2. 计算效率: 对全市场股票计算该因子(尤其是历史回归部分)计算量巨大,需要高效的数据处理和并行计算能力。
  3. 回归模型的稳定性: 报告中使用的回归模型较为简单,可能存在遗漏变量问题。其系数的稳定性也值得关注。因子效果可能受市场状态影响。
  4. 正交化: 报告强调了正交化后的因子表现。实际应用中,需要实现因子正交化步骤,剔除常见风格因子的影响,得到更纯粹的alpha。
  5. 回测框架: 上述代码仅实现了因子计算的核心逻辑,完整的策略需要成熟的回测框架来处理头寸管理、交易成本、滑点、分红配股等现实问题。
  6. 参数敏感性: 回归窗口长度(20天)、时间段划分(开盘后30分钟等)等参数选择可能影响因子效果,可进行参数敏感性测试。
  7. 知情交易识别的假设: 该方法的核心假设是“预期外收益”反映了信息含量。正残差对应坏消息(知情者卖出),负残差对应好消息(知情者买入)。这个假设在特定市场或股票上可能不完全成立。

5. 风险提示

报告原文已提示:市场系统性风险、资产流动性风险以及政策变动风险会对策略表现产生较大影响。此外,模型风险(依赖特定模型识别知情交易)、数据风险(数据质量影响因子准确性)和过拟合风险(参数和模型选择可能基于历史数据优化)也需注意。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值