报告深度总结
目录
- 核心观点
- 因子定义
- 因子有效性检验
- 3.1 月度选股能力
- 3.2 不同指数范围影响
- 3.3 不同调仓频率影响
- 因子相关性分析
- 4.1 与常规因子相关性
- 4.2 因子间相关性
- 因子在组合构建中的应用
- 总结与风险提示
- 策略Python代码示例 (含中文注释)
1. 核心观点
本报告系统梳理了前期基于逐笔成交数据挖掘出的多个高频选股因子,并检验了它们的有效性。核心结论是:这些基于逐笔成交数据的因子(逐笔因子),在经过与常规风格因子(行业、市值、估值、换手率、反转、波动率)正交处理后,依然表现出显著且稳健的月度选股能力,能够为量化策略提供增量信息。
2. 因子定义
报告重点分析了以下几个利用逐笔成交数据计算的因子(具体计算细节参考原文及前期报告):
- 大买成交金额占比: 定义为某只股票当日"大额买单"成交的总金额占该股票当日总成交金额的比例。衡量大资金买入意愿。
- 买单集中度: 基于买单金额分布计算的赫芬达尔指数(HHI)类似指标,衡量买单金额是否集中在少数大单上。计算方式为:(Σ 买单金额_k^2) / (总成交金额^2)。
- 盘中主买占比(占全天成交): 定义为交易日核心时段(10:00-14:26)"主动性买单"成交金额占全天总成交金额的比例。衡量盘中主动买入强度。
- 开盘后日内净主买强度: 定义为开盘后一小时内(9:30-9:59)“净主动买入金额”(主动买-主动卖)的均值除以其标准差。衡量开盘时段买卖力量对比的稳定性和强度。
- 开盘后知情主卖占比(占同时段成交额): 定义为开盘后一小时内(9:30-9:59)"知情主动卖出金额"占该时段总成交额的比例,并乘以-1(使IC为正)。可能捕捉开盘时信息交易者的卖出行为。
- 收盘前知情主买占比(占全天成交额): 定义为收盘前半小时(14:27-14:56)"知情主动买入金额"占全天总成交额的比例,并乘以-1(使IC为正)。可能捕捉收盘前信息交易者的买入行为。
注意: “大单”、“主动性”、“知情” 的具体判定标准依赖于成交数据的详细标签(如内外盘、订单号、订单大小等),在本报告中未完全展开,需参考前期报告或进行合理假设。计算时剔除了ST及上市不足6个月的股票。
3. 因子有效性检验
- 3.1 月度选股能力:
- 正交前: “买单集中度”、“开盘后知情主卖占比”、“尾盘知情主买占比” 表现尤为突出,IC和多空收益显著。
- 正交后: 所有因子在全市场范围内均表现出显著的月度选股能力。月均IC在0.03-0.04之间,稳定性较好,除"买单集中度"外,其余因子年化ICIR均超过2.0。分组收益单调性良好,多空组合和因子溢价表现稳健。大买成交金额占比、开盘后知情主卖占比、尾盘知情主买占比的多头超额收益相对更强。
- 3.2 不同指数范围影响:
- 因子在不同指数(中证800、中证500、沪深300)内表现存在差异。
- 中证800: 大买成交金额占比、盘中主买占比、尾盘知情主买占比表现依然较好。
- 中证500: 除大买成交金额占比外,其余因子选股能力有所减弱,但部分因子仍有一定效果。
- 沪深300: 大买成交金额占比和盘中主买占比表现较显著。
- 3.3 不同调仓频率影响:
- 月度 -> 半月度: 因子ICIR和多空年化收益普遍提升,部分因子的多头效应改善(如大买成交占比、开盘后知情主卖占比)。
- 半月度 -> 周度: 仅大买成交金额占比和尾盘知情主买占比的选股能力进一步提升,其余因子多空收益反而略有下降。报告推测这与因子计算窗口(周度调仓使用周度数据计算)有关,可能引入了更多噪音或因子本身信息衰减特性。
4. 因子相关性分析
- 4.1 与常规因子相关性:
- 正交前: 逐笔因子与市值、估值、换手率、前期涨跌幅(反转)等因子存在一定相关性(如大买成交金额占比与市值、动量、换手正相关;买单集中度与市值、换手负相关),验证了正交处理的必要性。与波动率、盈利因子相关性较弱。
- 4.2 因子间相关性:
- 正交后: 部分因子间仍存在一定相关性。例如,“开盘后日内净主买强度”与“开盘后知情主卖占比”相关性较高(0.47);“大买成交金额占比”与“买单集中度”存在一定正相关(0.24)。提示在多因子模型中使用时需注意共线性问题。
5. 因子在组合构建中的应用
- 以中证500指数增强组合为例,在包含常规因子(及分钟级高频因子)的基础模型上,分别加入单个逐笔因子进行测试。
- 结果显示,部分逐笔因子(如买单集中度、开盘后日内净主买强度、尾盘知情主买占比)的引入,能够显著提升组合的年化超额收益,即使基础模型已经包含了其他高频信息。
- 但并非所有有效因子都能带来组合提升。 因子对组合的增益效果取决于原有模型的因子构成、收益预测模型、风险模型等多种因素。提示投资者需要根据自身模型情况审慎选择。
6. 总结与风险提示
- 总结: 基于逐笔成交数据的高频因子在正交后具有显著且稳健的选股能力,可以作为对传统多因子模型的有效补充,提升策略表现。因子的有效性受指数范围、调仓频率影响,因子间也存在一定相关性。
- 风险提示: 市场系统性风险、资产流动性风险、模型失效风险、以及政策变动风险可能对策略表现产生较大影响。逐笔数据的处理和清洗质量也至关重要。
7. 策略Python代码示例 (含中文注释)
重要声明:
- 以下代码为概念性示例,旨在演示报告中因子计算和回测的基本思路。
- 无法直接运行产生报告结果,因为它缺少:
- 真实的逐笔成交数据 (Tick Data): 这是计算这些因子的基础,通常体积庞大且需要专门处理。
- 具体的因子定义细节: 如"大单"、“主动”、"知情"的精确判定逻辑。
- 海通证券内部的因子库和风控模型: 用于正交化和组合构建。
- 完整的市场数据: 如股票价格、市值、行业、估值等用于正交化和回测。
- 代码中的因子计算部分是高度简化和假设的,仅用于说明逻辑。
- 未包含交易成本。
import pandas as pd
import numpy as np
import statsmodels.api as sm
import matplotlib.pyplot as plt
import matplotlib
from io import BytesIO
# 设置 Matplotlib 支持中文显示
# 注意:这需要系统中安装了支持中文的字体,例如 'SimHei' 或 'Microsoft YaHei'
# 如果没有,请先安装字体,或者替换成你系统中已有的中文字体名称
try:
matplotlib.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体
matplotlib.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题
except Exception as e:
print(f"警告:设置中文字体失败,图形中的中文可能无法正常显示。请确保已安装 SimHei 或类似中文字体。错误:{e}")
# --- 0. 数据加载的占位符函数 ---
# 实际应用中,这些函数需要加载真实的逐笔数据、日线价格、因子暴露等
def load_tick_data(stock_code, date):
"""加载指定股票和日期的假设性逐笔数据。"""
# 占位符:返回一个包含预期列名的空DataFrame
# 真实的逐笔数据应包含:时间戳, 价格, 成交量, 买卖标志, 订单号等
print(f"信息:占位符 - 正在加载 {stock_code} 在 {date} 的逐笔数据")
# 示例结构 - 这里需要真实的逐笔数据
# columns = ['timestamp', 'price', 'volume', 'bs_flag', 'order_size', 'trade_type']
# return pd.DataFrame(columns=columns)
return pd.DataFrame() # 概念演示返回空
def load_daily_data(dates):
"""加载假设性的每日股票数据 (收益率, 市值, 行业等)。"""
print(f"信息:占位符 - 正在加载日期范围 {dates[0]} 到 {dates[-1]} 的每日数据")
# 示例结构 - 这里需要真实的每日数据
# index = pd.MultiIndex.from_product([dates, ['stockA', 'stockB', ...]], names=['date', 'code'])
# columns = ['return', 'market_cap', 'industry', 'bp', 'momentum', ...]
# return pd.DataFrame(index=index, columns=columns)
# 简化用于演示:
index = pd.MultiIndex.from_product([dates, ['stockA', 'stockB', 'stockC']], names=['date', 'code'])
data = {
'return': np.random.randn(len(index)) * 0.01, # 每日收益率
'market_cap': np.random.rand(len(index)) * 1e10 + 1e9, # 总市值
'industry': np.random.choice(['科技', '金融', '零售'], len(index)), # 行业
# 如果需要,添加其他用于正交化的因子
}
return pd.DataFrame(data, index=index)
def load_precomputed_factors(factor_names, dates):
"""加载假设性的预计算因子值。"""
print(f"信息:占位符 - 正在加载预计算因子: {factor_names}")
index = pd.MultiIndex.from_product([dates, ['stockA', 'stockB', 'stockC']], names=['date', 'code'])
data = {factor: np.random.randn(len(index)) for factor in factor_names}
return pd.DataFrame(data, index=index)
# --- 1. 因子计算函数 (概念性 - 需要真实的逐笔数据) ---
def calculate_tick_factors_for_day(stock_code, date):
"""
为单个股票在单日计算所有逐笔因子。
这是高度概念化的,需要真实的逐笔数据和因子定义。
"""
tick_data = load_tick_data(stock_code, date)
if tick_data.empty:
print(f"警告:{stock_code} 在 {date} 没有逐笔数据,返回NaN")
# 根据需要处理,返回NaN或0
factor_names = [
'large_buy_ratio', 'buy_concentration', 'intraday_main_buy_ratio',
'post_open_net_strength', 'post_open_informed_sell', 'pre_close_informed_buy'
]
return pd.Series(index=factor_names, data=np.nan)
# --- 假设性计算 (仅为说明) ---
# 这些步骤需要用基于逐笔数据字段和精确因子定义的实际逻辑替换
# (例如:大单阈值、主动成交标志、知情交易逻辑)
total_turnover = (tick_data['price'] * tick_data['volume']).sum() if not tick_data.empty else 0 # 当日总成交额
if total_turnover == 0:
factor_names = [ # 确保在此处定义 factor_names
'large_buy_ratio', 'buy_concentration', 'intraday_main_buy_ratio',
'post_open_net_strength', 'post_open_informed_sell', 'pre_close_informed_buy'
]
return pd.Series(index=factor_names, data=np.nan) # 避免除零错误
# 示例:大买单成交占比 (假设)
large_order_threshold = 1_000_000 # 示例阈值(金额)
# 假设 tick_data 有 'bs_flag' ('B'代表买) 和 'price', 'volume' 列
large_buys = tick_data[(tick_data['bs_flag'] == 'B') & (tick_data['price'] * tick_data['volume'] > large_order_threshold)]
large_buy_amount = (large_buys['price'] * large_buys['volume']).sum()
large_buy_ratio = large_buy_amount / total_turnover if total_turnover else 0
# 示例:买单集中度 (假设 - 需要订单聚合)
# 假设存在 'order_no' 列,可以按买单聚合交易量
# buy_orders = tick_data[tick_data['bs_flag'] == 'B'].groupby('order_no').agg({'price': 'mean', 'volume': 'sum'})
# buy_order_values = buy_orders['price'] * buy_orders['volume']
# buy_concentration = (buy_order_values**2).sum() / (total_turnover**2) if total_turnover else 0
buy_concentration = np.nan # 占位符 - 需要订单数据
# 示例:盘中主买占比 (假设)
# intraday_ticks = tick_data[tick_data['timestamp'].between(f'{date} 10:00:00', f'{date} 14:26:00')]
# 假设存在 'trade_type' == 'ActiveBuy' 标志
# intraday_main_buy_amount = (intraday_ticks[intraday_ticks['trade_type'] == 'ActiveBuy']['price'] * intraday_ticks['volume']).sum()
# intraday_main_buy_ratio = intraday_main_buy_amount / total_turnover if total_turnover else 0
intraday_main_buy_ratio = np.nan # 占位符
# 示例:开盘后净主买强度 (假设)
# post_open_ticks = tick_data[tick_data['timestamp'].between(f'{date} 09:30:00', f'{date} 09:59:59')]
# 假设存在每笔的 'net_active_buy_value' (主动买金额 - 主动卖金额)
# net_values = post_open_ticks['net_active_buy_value']
# post_open_net_strength = net_values.mean() / net_values.std() if net_values.std() > 1e-9 else 0 # 避免std为0
post_open_net_strength = np.nan # 占位符
# 示例:开盘后知情主卖占比 (假设)
# 假设存在每笔的 'informed_sell_value'
# post_open_informed_sell_amount = post_open_ticks['informed_sell_value'].sum()
# post_open_turnover = (post_open_ticks['price'] * post_open_ticks['volume']).sum()
# post_open_informed_sell = -1 * (post_open_informed_sell_amount / post_open_turnover) if post_open_turnover else 0 # 乘以-1使IC为正
post_open_informed_sell = np.nan # 占位符
# 示例:收盘前知情主买占比 (假设)
# pre_close_ticks = tick_data[tick_data['timestamp'].between(f'{date} 14:27:00', f'{date} 14:56:00')]
# 假设存在每笔的 'informed_buy_value'
# pre_close_informed_buy_amount = pre_close_ticks['informed_buy_value'].sum()
# pre_close_informed_buy = -1 * (pre_close_informed_buy_amount / total_turnover) if total_turnover else 0 # 乘以-1使IC为正
pre_close_informed_buy = np.nan # 占位符
# --- 合并结果 ---
factors = pd.Series({
'large_buy_ratio': large_buy_ratio,
'buy_concentration': buy_concentration,
'intraday_main_buy_ratio': intraday_main_buy_ratio,
'post_open_net_strength': post_open_net_strength,
'post_open_informed_sell': post_open_informed_sell,
'pre_close_informed_buy': pre_close_informed_buy
})
return factors
def calculate_all_factors(stock_list, dates):
"""为所有股票在所有日期计算逐笔因子。"""
all_factors_list = []
total_tasks = len(dates) * len(stock_list)
completed_tasks = 0
print(f"开始计算因子,总计 {total_tasks} 个任务...")
for date in dates:
for stock in stock_list:
# 在此应用过滤器(ST, 上市日期等)- 此处未展示
factors = calculate_tick_factors_for_day(stock, date)
factors['code'] = stock
factors['date'] = date
all_factors_list.append(factors)
completed_tasks += 1
if completed_tasks % 100 == 0 or completed_tasks == total_tasks: # 每100个或最后一个任务打印进度
print(f"因子计算进度: {completed_tasks}/{total_tasks} ({completed_tasks/total_tasks:.1%})")
factor_df = pd.DataFrame(all_factors_list)
factor_df = factor_df.set_index(['date', 'code'])
print("因子计算完成。")
return factor_df
# --- 2. 因子预处理 ---
def winsorize_series(series, lower=0.01, upper=0.99):
"""对 pandas Series 进行 Winsorize 处理(去极值)。"""
q_lower = series.quantile(lower)
q_upper = series.quantile(upper)
return series.clip(lower=q_lower, upper=q_upper)
def standardize_series(series):
"""对 pandas Series 进行标准化处理 (z-score)。"""
mean = series.mean()
std = series.std()
if std < 1e-9 : # 防止除以过小的标准差
return series - mean # 如果标准差接近0,则只中心化
return (series - mean) / std
def preprocess_factors(factor_df):
"""对因子进行截面预处理(去极值、标准化)。"""
print("开始因子预处理(去极值、标准化)...")
# 按日期分组,对每个因子列(截面)进行处理
processed_df = factor_df.groupby(level='date').transform(lambda x: standardize_series(winsorize_series(x)))
print("因子预处理完成。")
return processed_df
def orthogonalize_factor(factor_series, style_factor_df):
"""
使用截面回归将目标因子对风格因子进行正交化。
factor_series: 目标因子的 Series,MultiIndex (date, code)。
style_factor_df: 风格因子的 DataFrame,MultiIndex (date, code),可包含行业哑变量。
"""
print(f"开始对因子 {factor_series.name} 进行正交化...")
# 对齐索引
common_index = factor_series.index.intersection(style_factor_df.index)
factor_series = factor_series.loc[common_index]
style_factor_df = style_factor_df.loc[common_index]
residuals_list = [] # 用于存储每个截面的残差
# 添加行业哑变量 (如果 'industry' 列存在)
style_factor_df_with_dummies = style_factor_df.copy()
if 'industry' in style_factor_df_with_dummies.columns:
try:
print("正在添加行业哑变量...")
industry_dummies = pd.get_dummies(style_factor_df_with_dummies['industry'], prefix='行业', drop_first=True)
style_factor_df_with_dummies = pd.concat([style_factor_df_with_dummies.drop('industry', axis=1), industry_dummies], axis=1)
print("行业哑变量添加完成。")
except Exception as e:
print(f"创建行业哑变量时出错: {e}。可能只有一个行业。")
# 如果只有一个行业或出错,则不添加哑变量
if 'industry' in style_factor_df_with_dummies.columns:
style_factor_df_with_dummies = style_factor_df_with_dummies.drop('industry', axis=1)
# 按日期进行截面回归
dates_processed = 0
total_dates = len(factor_series.index.levels[0])
for date, group_factor in factor_series.groupby(level='date'):
if date in style_factor_df_with_dummies.index:
group_styles = style_factor_df_with_dummies.loc[date]
# 再次对齐当天的股票索引,以防万一
common_stocks = group_factor.index.get_level_values('code').intersection(group_styles.index)
group_factor = group_factor.loc[(date, common_stocks)]
group_styles = group_styles.loc[common_stocks]
if group_factor.empty or group_styles.empty:
residuals_list.append(pd.Series(dtype=float, index=group_factor.index)) # 添加空 Series
continue
# 准备 OLS 回归数据
y = group_factor.values
# 对风格因子填充NaN(考虑更复杂的填充方法),并添加常数项
X = group_styles.fillna(group_styles.mean()).values # 用截面均值填充NaN
X = sm.add_constant(X, prepend=True) # 添加常数项
# 检查自由度是否足够
if len(y) > X.shape[1] and np.linalg.matrix_rank(X) == X.shape[1]: # 确保X是满秩的
try:
model = sm.OLS(y, X)
results = model.fit()
resids = pd.Series(results.resid, index=group_factor.index)
residuals_list.append(resids)
except Exception as e:
print(f"日期 {date} 的 OLS 回归失败: {e}。跳过。")
# 回归失败时,添加NaN Series
residuals_list.append(pd.Series(np.nan, index=group_factor.index))
else:
if len(y) <= X.shape[1]:
print(f"警告:日期 {date} 的 OLS 数据点不足 ({len(y)} <= {X.shape[1]})。跳过。")
else:
print(f"警告:日期 {date} 的风格因子矩阵 X 不是满秩。跳过。")
# 数据不足或X非满秩时,添加NaN Series
residuals_list.append(pd.Series(np.nan, index=group_factor.index))
else:
# 如果当天没有风格因子数据,也添加NaN Series
residuals_list.append(pd.Series(np.nan, index=group_factor.index))
dates_processed += 1
if dates_processed % 50 == 0 or dates_processed == total_dates:
print(f"正交化进度: {dates_processed}/{total_dates} ({dates_processed/total_dates:.1%})")
if not residuals_list:
print("警告:未能计算任何残差。")
return pd.Series(dtype=float)
# 合并所有截面的残差
orthogonalized_factor = pd.concat(residuals_list)
orthogonalized_factor.name = factor_series.name + "_ortho" # 给正交化后的因子命名
print(f"因子 {factor_series.name} 正交化完成。")
return orthogonalized_factor
# --- 3. 回测框架 (简化版) ---
def run_backtest(factor_data, returns_data, factor_name, rebalance_dates, n_quantiles=10):
"""
运行简单的分位数回测。
factor_data: 包含因子值的 DataFrame,MultiIndex (date, code)。
returns_data: 包含收益率的 DataFrame,MultiIndex (date, code),列名为 'return'。
factor_name: 用于回测的特定因子列名。
rebalance_dates: 进行投资组合再平衡的日期列表。
n_quantiles: 投资组合分组的数量。
"""
print(f"开始基于因子 {factor_name} 进行分位数回测 (分为 {n_quantiles} 组)...")
# 初始化存储每日组合收益率的字典
quantile_daily_returns = {q: [] for q in range(1, n_quantiles + 1)}
all_backtest_dates = returns_data.index.get_level_values('date').unique()
# 确保因子数据在需要时可用,向前填充缺失值(假设因子值在调仓期间保持不变)
factor_data_unstacked = factor_data.unstack() # code作为列
factor_data_ffilled = factor_data_unstacked.reindex(all_backtest_dates).ffill() # 用所有交易日填充并向前填充
factor_data_aligned = factor_data_ffilled.stack(dropna=False) # 转回 MultiIndex
# 按调仓周期循环
for i, rebal_date in enumerate(rebalance_dates):
# 确定当前调仓周期的开始和结束日期
start_holding_date = rebal_date + pd.Timedelta(days=1) # 持仓从调仓日后一天开始
end_holding_date = rebalance_dates[i+1] if i + 1 < len(rebalance_dates) else all_backtest_dates.max()
# 获取调仓日的因子值
if rebal_date not in factor_data_aligned.index.get_level_values('date'):
print(f"警告:调仓日 {rebal_date} 因子数据不可用。跳过此周期。")
continue
current_factor = factor_data_aligned.loc[rebal_date][factor_name].dropna()
if current_factor.empty:
print(f"警告:调仓日 {rebal_date} 没有有效的因子值。跳过此周期。")
continue
# --- 投资组合构建 ---
# 根据因子值进行分组 (截面排序)
try:
# 使用 pd.qcut 进行等分位数分组,duplicates='drop'处理相同因子值
quantiles = pd.qcut(current_factor, n_quantiles, labels=False, duplicates='drop') + 1
except ValueError: # 处理无法分组的情况(例如股票数少于组数)
print(f"警告:无法在 {rebal_date} 将股票分为 {n_quantiles} 组。跳过此周期。")
continue
# --- 计算持仓期收益率 ---
# 选取当前持仓期的所有交易日
holding_period_dates = all_backtest_dates[(all_backtest_dates >= start_holding_date) & (all_backtest_dates <= end_holding_date)]
if holding_period_dates.empty:
continue # 如果持仓期没有交易日,跳过
# 获取持仓期的收益率数据
period_returns = returns_data.loc[pd.IndexSlice[holding_period_dates, :]]['return'].unstack() # code 作为列
# 计算每个分位数组合的等权重日收益率
for q in range(1, n_quantiles + 1):
stocks_in_quantile = quantiles[quantiles == q].index
# 选择那些在当前分位且在收益率数据中有记录的股票
relevant_returns = period_returns[period_returns.columns.intersection(stocks_in_quantile)]
if not relevant_returns.empty:
# 计算每日等权重收益率
daily_quantile_return = relevant_returns.mean(axis=1)
quantile_daily_returns[q].append(daily_quantile_return)
# else: 如果这个分位没有股票有收益率数据(例如停牌或数据缺失)
# --- 聚合结果 ---
portfolio_ts = {}
print("回测计算完成,正在聚合结果...")
for q in range(1, n_quantiles + 1):
if quantile_daily_returns[q]:
# 合并每个分位的所有日收益率序列
portfolio_ts[f'Q{q}'] = pd.concat(quantile_daily_returns[q]).sort_index()
else:
portfolio_ts[f'Q{q}'] = pd.Series(dtype=float) # 如果某组没数据,创建空Series
portfolio_returns_df = pd.DataFrame(portfolio_ts)
# 计算多空组合收益率 (最高分位 - 最低分位)
if f'Q{n_quantiles}' in portfolio_returns_df and f'Q1' in portfolio_returns_df:
portfolio_returns_df['多空组合'] = portfolio_returns_df[f'Q{n_quantiles}'] - portfolio_returns_df['Q1']
print("回测结果聚合完成。")
return portfolio_returns_df
def calculate_performance_metrics(returns_series):
"""计算简单的绩效指标。"""
if returns_series.empty or returns_series.isnull().all():
return pd.Series({'年化收益率': np.nan, '年化波动率': np.nan, '夏普比率': np.nan, '最大回撤': np.nan})
# 计算累计收益率和最大回撤
cumulative_returns = (1 + returns_series.fillna(0)).cumprod()
peak = cumulative_returns.expanding(min_periods=1).max()
drawdown = (cumulative_returns - peak) / peak
max_drawdown = drawdown.min()
# 计算年化指标
total_return = cumulative_returns.iloc[-1] - 1
if returns_series.index.empty:
n_years = 0
else:
n_years = (returns_series.index.max() - returns_series.index.min()).days / 365.25
if n_years <= 0 : n_years = 1 / 252 # 对于单日或无时间跨度的情况,按1天算
annualized_return = (1 + total_return) ** (1 / n_years) - 1
annualized_volatility = returns_series.std() * np.sqrt(252) # 假设是日收益率, 每年252交易日
sharpe_ratio = annualized_return / annualized_volatility if annualized_volatility > 1e-9 else 0 # 避免除零
return pd.Series({
'年化收益率': annualized_return,
'年化波动率': annualized_volatility,
'夏普比率': sharpe_ratio,
'最大回撤': max_drawdown
})
def plot_cumulative_returns(returns_df, title="投资组合累计收益率"):
"""绘制投资组合的累计收益率曲线。"""
print(f"正在绘制: {title}")
plt.figure(figsize=(12, 7))
(1 + returns_df.fillna(0)).cumprod().plot(ax=plt.gca()) # 在当前图形轴上绘图
plt.title(title)
plt.ylabel("累计收益率")
plt.xlabel("日期")
plt.grid(True)
plt.tight_layout() # 调整布局防止标签重叠
# plt.show() # 不直接显示,以便可以保存
# 保存到内存,以便可以返回图像对象
buf = BytesIO()
plt.savefig(buf, format='png')
plt.close() # 关闭图形,防止重复显示
buf.seek(0)
print("绘图完成。")
return buf # 返回包含图像数据的 BytesIO 对象
# --- 4. 主执行流程 (概念性) ---
if __name__ == "__main__":
print("--- 开始概念性逐笔因子回测 ---")
# --- 配置参数 ---
start_date = pd.to_datetime("2019-01-01") # 缩短日期范围用于演示
end_date = pd.to_datetime("2019-12-31")
all_dates = pd.date_range(start_date, end_date, freq='B') # 交易日历
rebalance_dates = pd.date_range(start_date, end_date, freq='BM') # 月末调仓
stocks = ['stockA', 'stockB', 'stockC'] # 示例股票列表
raw_factor_name = 'large_buy_ratio' # 要测试的原始因子名称
target_factor_name = raw_factor_name + '_ortho' # 正交化后的因子名称
# --- 数据加载 (使用占位符) ---
print("\n--- 正在加载数据 (占位符) ---")
daily_data = load_daily_data(all_dates)
# 假设 'return' 列是日收益率,用于回测评价
returns_data = daily_data[['return']].copy()
# --- 因子计算 / 加载 ---
# 选项 1: 从头计算 (需要真实的逐笔数据和上面的实现)
# print("\n--- 正在计算因子 (概念性) ---")
# raw_factor_df = calculate_all_factors(stocks, all_dates)
# 选项 2: 加载预计算的原始因子 (使用占位符)
print("\n--- 正在加载预计算因子 (占位符) ---")
raw_factor_df = load_precomputed_factors([raw_factor_name], all_dates)
# --- 因子预处理 ---
print("\n--- 正在预处理因子 ---")
processed_factor_df = preprocess_factors(raw_factor_df)
# --- 因子正交化 ---
print("\n--- 正在进行因子正交化 ---")
# 加载风格因子 (使用 daily_data 中的示例列)
# 确保风格因子也进行了预处理 (去极值、标准化)
style_factors_to_use = ['market_cap'] # 添加 'industry', 'bp' 等风格因子
style_factor_df = daily_data[style_factors_to_use].copy()
# 预处理风格因子 (示例 - 截面标准化)
style_factor_df = style_factor_df.groupby(level='date').transform(lambda x: standardize_series(winsorize_series(x)))
# 如果需要,包含行业哑变量
if 'industry' in daily_data.columns:
style_factor_df['industry'] = daily_data['industry'] # 添加行业列以供正交化函数处理
orthogonalized_factor = orthogonalize_factor(processed_factor_df[raw_factor_name], style_factor_df)
# 将正交化后的因子放入用于回测的DataFrame
factor_data_for_backtest = pd.DataFrame({target_factor_name: orthogonalized_factor})
# --- 执行回测 ---
print("\n--- 正在运行回测 ---")
# 检查是否有有效的因子数据用于回测
if not factor_data_for_backtest.empty and not factor_data_for_backtest[target_factor_name].isnull().all():
portfolio_returns = run_backtest(
factor_data=factor_data_for_backtest, # 使用正交化后的因子数据
returns_data=returns_data, # 使用加载的日收益率数据
factor_name=target_factor_name, # 指定回测用的因子列名
rebalance_dates=rebalance_dates, # 指定调仓日期
n_quantiles=5 # 分成5组进行回测
)
# --- 绩效分析 ---
print("\n--- 正在计算绩效指标 ---")
# 对每个投资组合(包括多空组合)计算绩效指标
performance_summary = portfolio_returns.apply(calculate_performance_metrics)
print("回测绩效总结:")
print(performance_summary)
# --- 结果可视化 ---
print("\n--- 正在绘制结果图表 ---")
# 绘制所有分位数组合的累计收益率
img_buffer_quantiles = plot_cumulative_returns(
portfolio_returns.drop('多空组合', axis=1, errors='ignore'), # 排除多空组合
title=f"因子分位数组合累计收益率 - {target_factor_name}"
)
# 绘制多空组合的累计收益率
if '多空组合' in portfolio_returns.columns:
img_buffer_ls = plot_cumulative_returns(
portfolio_returns[['多空组合']],
title=f"多空组合累计收益率 - {target_factor_name}"
)
# 此处可以添加保存图像到文件的代码,例如:
# with open(f"{target_factor_name}_quantiles.png", "wb") as f:
# f.write(img_buffer_quantiles.getbuffer())
# if '多空组合' in portfolio_returns.columns:
# with open(f"{target_factor_name}_long_short.png", "wb") as f:
# f.write(img_buffer_ls.getbuffer())
else:
print("错误:经过处理后,没有有效的因子数据可用于回测。")
print("\n--- 概念性回测结束 ---")