好的,我们来整理一下这份海通证券的研报。
目录
- 报告来源与标题
- 报告核心观点总结
- 策略核心逻辑与因子构建
- Python 核心代码示例
- 4.1 模拟输入数据
- 4.2 因子计算核心函数
- 4.3 示例调用
- 报告关键结果与回测表现
- 指数增强测试与风险提示
1. 报告来源与标题
- 来源: 海通证券 (Haitong Securities)
- 标题: 选股因子系列研究(八十五)——买卖单主动成交中的隐藏信息
- 日期: 2022年10月24日
- 作者: 冯佳睿, 袁林青
2. 报告核心观点总结
该报告深入研究了基于逐笔成交数据还原出的买卖单信息,重点关注了订单的主动成交度(即订单金额中主动吃掉对手盘挂单的比例)。与前期研究不同,该报告不仅区分了大、中、小单,还计算了各类订单的主动成交度,并发现小单主动成交度具有显著的选股能力。
核心发现包括:
- 小单主动成交度因子表现突出: 无论是小买单还是小卖单,其主动成交度越高,股票未来超额收益表现越好。其中,小买单主动成交度因子的月度选股能力略强(月均IC约0.05),多空收益稳健(月度多空收益1.78%,月均多头超额0.80%)。
- 盘中数据更优: 使用盘中(10:00-15:00)数据计算的小买单主动成交度因子,选股能力比使用全天或开盘后数据更强。
- 正交后依然有效: 在对行业、市值、换手率、反转等常见风格因子进行正交处理后,小买单主动成交度因子(全天和盘中)依然保持显著的选股能力,尽管多空收益有所下滑。
- 不同股票池表现: 原始因子在中证800指数成分股以外的股票池中IC更高,选股能力更强。正交后,因子在不同股票池(中证800内/外)的选股能力差距明显缩小,但在中证800外的多头超额收益依然更好。周度调仓下结论类似。
- 指数增强有效性: 在不包含深度学习高频因子的基础模型中,将小买单主动成交度因子加入中证500和中证1000指数增强组合,能够带来0.5%-1.5%(CSI 500)或0.5%-2.0%(CSI 1000)的年化超额收益提升。但若基础模型已包含深度学习因子,则该因子的增量贡献不稳定,依赖于具体的风控模型参数。
3. 策略核心逻辑与因子构建
策略的核心是构建并利用“小单主动成交度”因子进行选股。
- 数据基础: 使用逐笔成交数据(Tick Data),包含时间、价格、成交量、成交金额、买卖订单号(Buy/Sell Order ID)、以及成交方向标识(BS Flag,B代表主动买,S代表主动卖)。
- 订单还原: 通过聚合相同买单号或卖单号的逐笔成交记录,还原出每个独立的买单或卖单的总成交金额。
- 订单规模划分(大/中/小单):
- 对每只股票,计算其过去20个交易日还原后订单金额的滚动均值(Mean)和标准差(Std)。(注意:原文此处描述与表述“基于个股20日成交分布滚动计算”略有模糊,这里理解为基于还原后的订单金额分布)。
- 大单 (Large): 订单总金额 > Mean + 1 * Std
- 小单 (Small): 订单总金额 < Mean
- 中单 (Medium): Mean <= 订单总金额 <= Mean + 1 * Std
- 主动成交金额计算:
- 对于一个还原后的买单,其主动成交金额是所有构成该订单且BS Flag为 ‘B’ 的逐笔成交记录的金额之和。
- 对于一个还原后的卖单,其主动成交金额是所有构成该订单且BS Flag为 ‘S’ 的逐笔成交记录的金额之和。
- 主动成交度计算:
- 对每个股票每日,分别计算所有小买单、小卖单(以及中单、大单)的主动成交金额之和与总成交金额之和。
小买单主动成交度 = Sum(小买单主动成交金额) / Sum(小买单总成交金额)
小卖单主动成交度 = Sum(小卖单主动成交金额) / Sum(小卖单总成交金额)
- (报告重点关注小买单主动成交度)
- 因子生成:
- 计算每日的“小买单主动成交度”。
- 使用多日滚动平均(报告未明确具体天数,常用如5日、10日、20日)平滑日度因子值,得到最终用于选股的因子。
- (可选但推荐)正交化: 将滚动平滑后的因子对行业、市值、换手率、反转等因子进行回归取残差,以剔除风格影响。
- 策略应用:
- 在每个调仓期(如月末、周末),计算所有股票的最新因子值。
- 根据因子值对股票进行排序。
- 构建投资组合,例如买入因子值最高的 N% 的股票(多头),卖出因子值最低的 N% 的股票(空头,如果做多空策略)。或者在指数增强策略中,根据因子值对成分股进行超配或低配。
4. Python 核心代码示例
以下代码提供了一个简化的核心因子计算逻辑的Python示例。请注意: 这段代码是示意性的,需要真实的、处理好的逐笔数据才能运行,并且省略了数据获取、清洗、完整的滚动计算、正交化和回测框架。
import pandas as pd
import numpy as np
# --- 4.1 模拟输入数据 ---
# 假设我们有处理好的某只股票几天的逐笔数据 DataFrame
# 实际应用中需要读取真实的逐笔数据
def simulate_tick_data(days=5, num_trades_per_day=1000):
"""模拟生成一只股票几天的逐笔数据"""
all_ticks = []
base_price = 10.0
order_id_counter = 1
trade_id_counter = 1
for day in range(days):
date_str = f"2023-01-{day+1:02d}"
day_ticks = []
# 模拟一些大中小订单
num_large = 5
num_medium = 20
num_small = 50
for _ in range(num_large): # 大买单
total_amount = np.random.uniform(80000, 150000)
num_ticks_order = np.random.randint(5, 15)
buy_ord_id = order_id_counter
order_id_counter += 1
active_pct = np.random.uniform(0.6, 0.95) # 大单主动性高
active_amount = total_amount * active_pct
passive_amount = total_amount * (1 - active_pct)
current_amount = 0
for i in range(num_ticks_order):
tick_amount = total_amount / num_ticks_order # 简化处理
price = base_price + np.random.normal(0, 0.02)
volume = tick_amount / price
bs_flag = 'B' if current_amount < active_amount else 'S' # 模拟主动/被动成交
sell_ord_id = order_id_counter # 分配对手单号
order_id_counter += 1
day_ticks.append([date_str, trade_id_counter, price, volume, tick_amount, buy_ord_id, sell_ord_id, bs_flag])
trade_id_counter += 1
current_amount += tick_amount
# 简化模拟中/小单过程... (为简洁起见,下面只用随机生成ticks)
for i in range(num_trades_per_day - num_large * 10): # 简化模拟,非精确
price = base_price + np.random.normal(0, 0.02)
tick_amount = np.random.uniform(100, 50000) # 覆盖大中小
volume = tick_amount / price
buy_ord_id = order_id_counter + np.random.randint(0, 50)
sell_ord_id = order_id_counter + np.random.randint(50, 100)
bs_flag = np.random.choice(['B', 'S'])
day_ticks.append([date_str, trade_id_counter, price, volume, tick_amount, buy_ord_id, sell_ord_id, bs_flag])
trade_id_counter += 1
order_id_counter += 100 # 增加order id计数器
all_ticks.extend(day_ticks)
base_price *= (1 + np.random.normal(0, 0.01)) # 价格日间波动
ticks_df = pd.DataFrame(all_ticks, columns=['date', 'trade_id', 'price', 'volume', 'amount', 'buy_order_id', 'sell_order_id', 'bs_flag'])
ticks_df['date'] = pd.to_datetime(ticks_df['date'])
# 确保ID是整数类型,便于分组
ticks_df[['buy_order_id', 'sell_order_id']] = ticks_df[['buy_order_id', 'sell_order_id']].astype(int)
return ticks_df
# --- 4.2 因子计算核心函数 ---
def calculate_small_order_activity_factor(ticks_df, rolling_window=20, factor_smooth_window=5):
"""
计算小买单主动成交度因子 (示意性,非完整回测)
Args:
ticks_df (pd.DataFrame): 包含多日单只股票的逐笔数据
rolling_window (int): 计算订单金额分布的滚动窗口
factor_smooth_window (int): 对最终日度因子进行平滑的窗口
Returns:
pd.DataFrame: 包含每日因子值的DataFrame
"""
daily_factors = {}
all_order_amounts = {} # 存储历史订单金额用于计算滚动阈值
# 确保按日期排序
ticks_df = ticks_df.sort_values(by='date')
dates = ticks_df['date'].unique()
for current_date in dates:
print(f"Processing date: {current_date.date()}")
daily_ticks = ticks_df[ticks_df['date'] == current_date].copy()
if daily_ticks.empty:
continue
# 1. 订单还原与主动成交额计算
buy_orders = daily_ticks.groupby('buy_order_id').agg(
total_amount=('amount', 'sum'),
active_amount=('amount', lambda x: x[daily_ticks.loc[x.index, 'bs_flag'] == 'B'].sum()) # 主动买入金额
).reset_index().rename(columns={'buy_order_id': 'order_id'})
buy_orders['order_type'] = 'buy'
sell_orders = daily_ticks.groupby('sell_order_id').agg(
total_amount=('amount', 'sum'),
active_amount=('amount', lambda x: x[daily_ticks.loc[x.index, 'bs_flag'] == 'S'].sum()) # 主动卖出金额
).reset_index().rename(columns={'sell_order_id': 'order_id'})
sell_orders['order_type'] = 'sell'
reconstructed_orders = pd.concat([buy_orders, sell_orders], ignore_index=True)
reconstructed_orders = reconstructed_orders[reconstructed_orders['total_amount'] > 0] # 过滤掉无效订单
# 存储当日订单金额用于未来滚动计算
all_order_amounts[current_date] = reconstructed_orders['total_amount'].copy()
# 2. 计算订单规模阈值 (需要历史数据)
# 获取用于计算阈值的历史日期范围
start_date_threshold = current_date - pd.Timedelta(days=rolling_window * 2) # 넉넉하게 과거 데이터 선택
relevant_dates = sorted([d for d in all_order_amounts.keys() if d < current_date and d >= start_date_threshold])
# 取最近rolling_window个交易日的数据
hist_dates_for_threshold = relevant_dates[-(rolling_window-1):] if len(relevant_dates) >= rolling_window-1 else relevant_dates
if not hist_dates_for_threshold:
print(f" Skipping threshold calculation for {current_date.date()} due to insufficient history.")
daily_factors[current_date] = np.nan # 历史数据不足,无法计算因子
continue
# 合并历史订单金额
hist_amounts = pd.concat([all_order_amounts[d] for d in hist_dates_for_threshold] + [all_order_amounts[current_date]], ignore_index=True)
if hist_amounts.empty:
print(f" Skipping threshold calculation for {current_date.date()} due to empty historical amounts.")
daily_factors[current_date] = np.nan
continue
# 计算均值和标准差
mean_amount = hist_amounts.mean()
std_amount = hist_amounts.std()
# 定义阈值
threshold_small = mean_amount
threshold_large = mean_amount + 1 * std_amount
# 3. 订单分类
def classify_order(amount):
if amount < threshold_small:
return 'Small'
elif amount > threshold_large:
return 'Large'
else:
return 'Medium'
reconstructed_orders['size_category'] = reconstructed_orders['total_amount'].apply(classify_order)
# 4. 计算小买单主动成交度
small_buy_orders = reconstructed_orders[(reconstructed_orders['order_type'] == 'buy') & (reconstructed_orders['size_category'] == 'Small')]
total_small_buy_amount = small_buy_orders['total_amount'].sum()
active_small_buy_amount = small_buy_orders['active_amount'].sum()
if total_small_buy_amount > 0:
small_buy_activity_degree = active_small_buy_amount / total_small_buy_amount
else:
small_buy_activity_degree = np.nan # 当天没有小买单
daily_factors[current_date] = small_buy_activity_degree
print(f" Date: {current_date.date()}, Small Buy Activity Degree: {small_buy_activity_degree:.4f}, Num Small Buys: {len(small_buy_orders)}")
# 5. 因子平滑
factor_series = pd.Series(daily_factors).sort_index()
smoothed_factor = factor_series.rolling(window=factor_smooth_window, min_periods=max(1, factor_smooth_window // 2)).mean()
result_df = pd.DataFrame({
'raw_factor': factor_series,
'smoothed_factor': smoothed_factor
})
return result_df
# --- 4.3 示例调用 ---
if __name__ == "__main__":
# 模拟数据 (实际应读取数据)
simulated_ticks = simulate_tick_data(days=30, num_trades_per_day=500) # 模拟30天的数据
# 计算因子 (对这只模拟股票)
# 注意:需要足够长的历史数据才能准确计算滚动阈值
factor_result = calculate_small_order_activity_factor(simulated_ticks, rolling_window=20, factor_smooth_window=5)
print("\nFactor Calculation Result (Last 10 days):")
print(factor_result.tail(10))
# --- 后续步骤 ---
# 1. 对股票池中所有股票重复以上计算过程
# 2. (可选) 进行因子正交化处理
# 3. 使用 smoothed_factor 或 正交化后的因子 进行回测
# - 截面排序
# - 构建投资组合 (如 Top 10% vs Bottom 10%)
# - 计算收益率、IC、Sharpe Ratio等指标
代码说明:
simulate_tick_data
: 生成用于演示的假数据,实际应用需替换为真实数据接口。calculate_small_order_activity_factor
:- 按日处理数据。
- 使用
groupby()
还原买单和卖单,并计算其总金额和主动成交金额。 - 存储每日订单金额
all_order_amounts
用于滚动计算历史分布。 - 根据过去
rolling_window
天的订单金额分布计算均值和标准差,确定大/中/小单阈值。(注意:这里简化处理,实际滚动计算需要更严谨的数据管理)。 - 对当日订单进行分类。
- 筛选出小买单,计算总的主动成交额和总成交额,得到日度因子值。
- 使用
rolling().mean()
对日度因子进行平滑。
if __name__ == "__main__":
部分展示了如何调用函数并打印结果。- 重要: 此代码仅为核心逻辑示意,距离实盘或完整回测相差甚远,缺少错误处理、数据管理、多股票并行、正交化、回测引擎等关键部分。滚动阈值的计算方式也可能需要根据实际数据情况调整。
5. 报告关键结果与回测表现
- 因子有效性: 小单(尤其小买单)主动成交度因子具有显著且稳健的月度和周度选股能力(正相关)。
- IC值: 全天小买单主动成交度月均IC为0.050,盘中小买单为0.053。正交后仍保持较高水平(如全天0.032,盘中0.033)。周度IC略低但依然显著。
- 多空与多头: 因子分组收益单调性好,多空组合表现稳健。全天小买单因子月度多空年化约21%(1.78%*12),多头年化超额约9.6%(0.80%*12)。盘中因子表现更优。正交后收益有所下降但依然可观。
- 时间段: 因子在2014、2018、2019年表现稍弱,但2020年后逐年增强,尤其在2022年表现突出。正交后年度表现更稳定。
- 与其他因子相关性: 与市值、反转、换手率、BP等常见因子相关性不高(绝对值多在0.3以下),说明因子具有较好的独立性。与大单买入意愿类因子有一定正相关性。
6. 指数增强测试与风险提示
- 增强效果:
- 在中证500和中证1000增强策略中,若不包含深度学习高频因子,引入小买单主动成交度因子能显著提升超额收益(年化提升0.5%-2.0%不等,视约束条件而定)。
- 若基础模型已包含深度学习高频因子,该因子的增量效果不确定,有时甚至可能不提升或略微降低收益,具体效果依赖于风控模型(如个股偏离、行业偏离、因子敞口约束等)的选择。
- 风险提示:
- 市场系统性风险。
- 资产流动性风险(尤其小市值股票)。
- 政策变动风险。
- 模型失效风险(因子在未来可能失效)。
- 数据噪声风险(逐笔数据质量影响)。
希望这份总结和代码示例对您理解报告内容有所帮助!