Rank2
This commit is contained in:
Binary file not shown.
2473
main/train/Rank2_polars.ipynb
Normal file
2473
main/train/Rank2_polars.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
BIN
main/utils/__pycache__/data_process.cpython-313.pyc
Normal file
BIN
main/utils/__pycache__/data_process.cpython-313.pyc
Normal file
Binary file not shown.
BIN
main/utils/__pycache__/indicators.cpython-313.pyc
Normal file
BIN
main/utils/__pycache__/indicators.cpython-313.pyc
Normal file
Binary file not shown.
268
main/utils/data_process.py
Normal file
268
main/utils/data_process.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import statsmodels.api as sm # 用于中性化回归
|
||||
from tqdm import tqdm
|
||||
|
||||
epsilon = 1e-10 # 防止除零
|
||||
|
||||
def zscore_standardize(train_df: pd.DataFrame, test_df: pd.DataFrame, features: list, epsilon: float = 1e-10):
|
||||
"""
|
||||
对指定特征列进行截面 Z-Score 标准化 (原地修改)。
|
||||
方法: Z = (value - cross_sectional_mean) / (cross_sectional_std + epsilon)
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。
|
||||
features (list): 需要处理的特征列名列表。
|
||||
epsilon (float): 防止除以零的小常数。
|
||||
|
||||
WARNING: 此函数会原地修改输入的 DataFrame 'df'。
|
||||
"""
|
||||
print("开始截面 Z-Score 标准化...")
|
||||
if not all(col in train_df.columns for col in features):
|
||||
missing = [col for col in features if col not in train_df.columns]
|
||||
print(f"错误: DataFrame 中缺少以下特征列: {missing}。跳过标准化处理。")
|
||||
return
|
||||
|
||||
|
||||
for col in tqdm(features, desc="Standardizing"):
|
||||
try:
|
||||
# 使用 transform 计算截面均值和标准差
|
||||
mean = train_df[col].transform('mean')
|
||||
std = train_df[col].transform('std')
|
||||
|
||||
# 计算 Z-Score 并原地赋值
|
||||
train_df[col] = (train_df[col] - mean) / (std + epsilon)
|
||||
test_df[col] = (test_df[col] - mean) / (std + epsilon)
|
||||
|
||||
except KeyError:
|
||||
print(f"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的标准化处理。")
|
||||
except Exception as e:
|
||||
print(f"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的标准化处理。")
|
||||
|
||||
print("截面 Z-Score 标准化完成。")
|
||||
|
||||
|
||||
# --- 1. 中位数去极值 (MAD) ---
|
||||
|
||||
def cs_mad_filter(df: pd.DataFrame,
|
||||
features: list,
|
||||
k: float = 3.0,
|
||||
scale_factor: float = 1.4826):
|
||||
"""
|
||||
对指定特征列进行截面 MAD 去极值处理 (原地修改)。
|
||||
|
||||
方法: 对每日截面数据,计算 median 和 MAD,
|
||||
将超出 [median - k * scale * MAD, median + k * scale * MAD] 范围的值
|
||||
替换为边界值 (Winsorization)。
|
||||
scale_factor=1.4826 使得 MAD 约等于正态分布的标准差。
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。
|
||||
features (list): 需要处理的特征列名列表。
|
||||
k (float): MAD 的倍数,用于确定边界。默认为 3.0。
|
||||
scale_factor (float): MAD 的缩放因子。默认为 1.4826。
|
||||
|
||||
WARNING: 此函数会原地修改输入的 DataFrame 'df'。
|
||||
"""
|
||||
print(f"开始截面 MAD 去极值处理 (k={k})...")
|
||||
if not all(col in df.columns for col in features):
|
||||
missing = [col for col in features if col not in df.columns]
|
||||
print(f"错误: DataFrame 中缺少以下特征列: {missing}。跳过去极值处理。")
|
||||
return
|
||||
|
||||
grouped = df.groupby('trade_date')
|
||||
|
||||
for col in tqdm(features, desc="MAD Filtering"):
|
||||
try:
|
||||
# 计算截面中位数
|
||||
median = grouped[col].transform('median')
|
||||
# 计算截面 MAD (Median Absolute Deviation from Median)
|
||||
mad = (df[col] - median).abs().groupby(df['trade_date']).transform('median')
|
||||
|
||||
# 计算上下边界
|
||||
lower_bound = median - k * scale_factor * mad
|
||||
upper_bound = median + k * scale_factor * mad
|
||||
|
||||
# 原地应用 clip
|
||||
df[col] = np.clip(df[col], lower_bound, upper_bound)
|
||||
|
||||
except KeyError:
|
||||
print(f"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的 MAD 处理。")
|
||||
except Exception as e:
|
||||
print(f"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的 MAD 处理。")
|
||||
|
||||
print("截面 MAD 去极值处理完成。")
|
||||
|
||||
|
||||
# --- 2. 行业市值中性化 ---
|
||||
|
||||
def cs_neutralize_industry_cap(df: pd.DataFrame,
|
||||
features: list,
|
||||
industry_col: str = 'cat_l2_code',
|
||||
market_cap_col: str = 'circ_mv'):
|
||||
"""
|
||||
对指定特征列进行截面行业和对数市值中性化 (原地修改)。
|
||||
使用 OLS 回归: feature ~ 1 + log(market_cap) + C(industry)
|
||||
将回归残差写回原特征列。
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date', features 列,
|
||||
industry_col, market_cap_col。
|
||||
features (list): 需要处理的特征列名列表。
|
||||
industry_col (str): 行业分类列名。
|
||||
market_cap_col (str): 流通市值列名。
|
||||
|
||||
WARNING: 此函数会原地修改输入的 DataFrame 'df' 的 features 列。
|
||||
计算量较大,可能耗时较长。
|
||||
需要安装 statsmodels 库 (pip install statsmodels)。
|
||||
"""
|
||||
print("开始截面行业市值中性化...")
|
||||
required_cols = features + ['trade_date', industry_col, market_cap_col]
|
||||
if not all(col in df.columns for col in required_cols):
|
||||
missing = [col for col in required_cols if col not in df.columns]
|
||||
print(f"错误: DataFrame 中缺少必需列: {missing}。无法进行中性化。")
|
||||
return
|
||||
|
||||
# 预处理:计算 log 市值,处理 industry code 可能的 NaN
|
||||
log_cap_col = '_log_market_cap'
|
||||
df[log_cap_col] = np.log1p(df[market_cap_col]) # log1p 处理 0 值
|
||||
# df[industry_col] = df[industry_col].cat.add_categories('UnknownIndustry')
|
||||
# df[industry_col] = df[industry_col].fillna('UnknownIndustry') # 填充行业 NaN
|
||||
# df[industry_col] = df[industry_col].astype('category') # 转为类别,ols 会自动处理
|
||||
|
||||
dates = df['trade_date'].unique()
|
||||
all_residuals = [] # 用于收集所有日期的残差
|
||||
|
||||
for date in tqdm(dates, desc="Neutralizing"):
|
||||
daily_data = df.loc[df['trade_date'] == date, features + [log_cap_col, industry_col]].copy() # 使用 .loc 获取副本
|
||||
|
||||
# 准备自变量 X (常数项 + log市值 + 行业哑变量)
|
||||
X = daily_data[[log_cap_col]]
|
||||
X = sm.add_constant(X, prepend=True) # 添加常数项
|
||||
# 创建行业哑变量 (drop_first=True 避免共线性)
|
||||
industry_dummies = pd.get_dummies(daily_data[industry_col], prefix=industry_col, drop_first=True)
|
||||
industry_dummies = industry_dummies.astype(int)
|
||||
X = pd.concat([X, industry_dummies], axis=1)
|
||||
|
||||
daily_residuals = daily_data[[col for col in features]].copy() # 创建用于存储残差的df
|
||||
|
||||
for col in features:
|
||||
Y = daily_data[col]
|
||||
|
||||
# 处理 NaN 值,确保 X 和 Y 在相同位置有有效值
|
||||
valid_mask = Y.notna() & X.notna().all(axis=1)
|
||||
if valid_mask.sum() < (X.shape[1] + 1): # 数据点不足以估计模型
|
||||
print(f"警告: 日期 {date}, 特征 {col} 有效数据不足 ({valid_mask.sum()}个),无法中性化,填充 NaN。")
|
||||
daily_residuals[col] = np.nan
|
||||
continue
|
||||
|
||||
Y_valid = Y[valid_mask]
|
||||
X_valid = X[valid_mask]
|
||||
|
||||
# 执行 OLS 回归
|
||||
try:
|
||||
model = sm.OLS(Y_valid.to_numpy(), X_valid.to_numpy())
|
||||
results = model.fit()
|
||||
# 将残差填回对应位置
|
||||
daily_residuals.loc[valid_mask, col] = results.resid
|
||||
daily_residuals.loc[~valid_mask, col] = np.nan # 原本无效的位置填充 NaN
|
||||
except Exception as e:
|
||||
print(f"警告: 日期 {date}, 特征 {col} 回归失败: {e},填充 NaN。")
|
||||
daily_residuals[col] = np.nan
|
||||
break
|
||||
|
||||
all_residuals.append(daily_residuals)
|
||||
|
||||
# 合并所有日期的残差结果
|
||||
if all_residuals:
|
||||
residuals_df = pd.concat(all_residuals)
|
||||
# 将残差结果更新回原始 df (原地修改)
|
||||
# 使用 update 比 merge 更适合基于索引的原地更新
|
||||
# 确保 residuals_df 的索引与 df 中对应部分一致
|
||||
df.update(residuals_df)
|
||||
else:
|
||||
print("没有有效的残差结果可以合并。")
|
||||
|
||||
|
||||
# 清理临时列
|
||||
df.drop(columns=[log_cap_col], inplace=True)
|
||||
print("截面行业市值中性化完成。")
|
||||
|
||||
|
||||
# --- 3. Z-Score 标准化 ---
|
||||
|
||||
def cs_zscore_standardize(df: pd.DataFrame, features: list, epsilon: float = 1e-10):
|
||||
"""
|
||||
对指定特征列进行截面 Z-Score 标准化 (原地修改)。
|
||||
方法: Z = (value - cross_sectional_mean) / (cross_sectional_std + epsilon)
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。
|
||||
features (list): 需要处理的特征列名列表。
|
||||
epsilon (float): 防止除以零的小常数。
|
||||
|
||||
WARNING: 此函数会原地修改输入的 DataFrame 'df'。
|
||||
"""
|
||||
print("开始截面 Z-Score 标准化...")
|
||||
if not all(col in df.columns for col in features):
|
||||
missing = [col for col in features if col not in df.columns]
|
||||
print(f"错误: DataFrame 中缺少以下特征列: {missing}。跳过标准化处理。")
|
||||
return
|
||||
|
||||
grouped = df.groupby('trade_date')
|
||||
|
||||
for col in tqdm(features, desc="Standardizing"):
|
||||
try:
|
||||
# 使用 transform 计算截面均值和标准差
|
||||
mean = grouped[col].transform('mean')
|
||||
std = grouped[col].transform('std')
|
||||
|
||||
# 计算 Z-Score 并原地赋值
|
||||
df[col] = (df[col] - mean) / (std + epsilon)
|
||||
|
||||
except KeyError:
|
||||
print(f"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的标准化处理。")
|
||||
except Exception as e:
|
||||
print(f"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的标准化处理。")
|
||||
|
||||
print("截面 Z-Score 标准化完成。")
|
||||
|
||||
def fill_nan_with_daily_median(df: pd.DataFrame, feature_columns: list[str]) -> pd.DataFrame:
|
||||
"""
|
||||
对指定特征列进行每日截面中位数填充缺失值 (NaN)。
|
||||
|
||||
参数:
|
||||
df (pd.DataFrame): 包含多日数据的DataFrame,需要包含 'trade_date' 和 feature_columns 中的列。
|
||||
feature_columns (list[str]): 需要进行缺失值填充的特征列名称列表。
|
||||
|
||||
返回:
|
||||
pd.DataFrame: 包含缺失值填充后特征列的DataFrame。在输入DataFrame的副本上操作。
|
||||
"""
|
||||
processed_df = df.copy() # 在副本上操作,保留原始数据
|
||||
|
||||
# 确保 trade_date 是 datetime 类型以便正确分组
|
||||
processed_df['trade_date'] = pd.to_datetime(processed_df['trade_date'])
|
||||
|
||||
def _fill_daily_nan(group):
|
||||
# group 是某一个交易日的 DataFrame
|
||||
|
||||
# 遍历指定的特征列
|
||||
for feature_col in feature_columns:
|
||||
# 检查列是否存在于当前分组中
|
||||
if feature_col in group.columns:
|
||||
# 计算当日该特征的中位数
|
||||
median_val = group[feature_col].median()
|
||||
|
||||
# 使用当日中位数填充该特征列的 NaN 值
|
||||
# inplace=True 会直接修改 group DataFrame
|
||||
group[feature_col].fillna(median_val, inplace=True)
|
||||
# else:
|
||||
# print(f"Warning: Feature column '{feature_col}' not found in daily group for {group['trade_date'].iloc[0]}. Skipping.")
|
||||
|
||||
return group
|
||||
|
||||
# 按交易日期分组,并应用每日填充函数
|
||||
# group_keys=False 避免将分组键添加到结果索引中
|
||||
filled_df = processed_df.groupby('trade_date', group_keys=False).apply(_fill_daily_nan)
|
||||
|
||||
return filled_df
|
||||
130
main/utils/indicators.py
Normal file
130
main/utils/indicators.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
def calculate_sharpe_sortino_labels_efficient(
|
||||
df: pd.DataFrame,
|
||||
N_days: int = 5, # 未来考察的天数,用于计算收益率序列
|
||||
risk_free_rate_annual: float = 0.02, # 年化无风险利率 (例如 0.02 表示 2%)
|
||||
annualization_factor: float = np.sqrt(252), # 年化因子,日数据通常用sqrt(252)
|
||||
min_periods_for_std: int = 2 # 计算标准差所需的最小有效数据点数
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
高效计算每只股票在每个交易日未来 N 日的年化夏普比率和索提诺比率,作为训练的Label。
|
||||
函数会获取未来 N 天的每日收益率序列,并基于此序列计算夏普/索提诺。
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 输入 DataFrame,需包含 'ts_code', 'trade_date', 'close' 列。
|
||||
'close' 列必须是股票收盘价。
|
||||
N_days (int): 未来考察的天数。例如,N_days=5 表示考察未来5个交易日。
|
||||
risk_free_rate_annual (float): 年化无风险利率。
|
||||
annualization_factor (float): 用于将日收益率转化为年化收益率的因子 (例如 np.sqrt(252))。
|
||||
min_periods_for_std (int): 计算标准差所需的最小有效数据点数。
|
||||
如果未来N天有效数据少于此值,则 Label 为 NaN。
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: 原始DataFrame,新增 'sharpe_ratio_label' 和 'sortino_ratio_label' 列。
|
||||
注意:数据末尾 N_days 行的 Label 将为 NaN。
|
||||
"""
|
||||
df_copy = df
|
||||
|
||||
# 确保数据已按股票和日期排序,这对于高效计算至关重要
|
||||
df_copy = df_copy.sort_values(by=['ts_code', 'trade_date'])
|
||||
|
||||
# # 计算每日收益率
|
||||
# df_copy['pct_chg'] = df_copy.groupby('ts_code')['close'].pct_change()
|
||||
|
||||
# 将年化无风险利率转换为每日无风险利率
|
||||
daily_risk_free_rate = risk_free_rate_annual / 252 # 假设每年252个交易日
|
||||
|
||||
def _calculate_metrics_for_window(returns_series):
|
||||
"""
|
||||
辅助函数:计算单个滚动窗口内的夏普和索提诺比率。
|
||||
这个函数将被 apply 调用于每个 (N_days) 收益率序列。
|
||||
"""
|
||||
if len(returns_series.dropna()) < min_periods_for_std:
|
||||
return np.nan, np.nan # 数据不足,返回NaN
|
||||
|
||||
excess_returns = returns_series - daily_risk_free_rate
|
||||
|
||||
# --- 夏普比率 ---
|
||||
mean_excess_return = excess_returns.mean()
|
||||
std_dev_returns = returns_series.std() # 夏普比率使用总收益率的标准差
|
||||
|
||||
sharpe = np.nan
|
||||
if std_dev_returns != 0:
|
||||
sharpe = (mean_excess_return / std_dev_returns) * annualization_factor
|
||||
|
||||
# --- 索提诺比率 ---
|
||||
downside_returns = returns_series[returns_series < daily_risk_free_rate]
|
||||
sortino = np.nan
|
||||
if not downside_returns.empty:
|
||||
downside_deviation = np.sqrt(np.mean((downside_returns - daily_risk_free_rate)**2))
|
||||
if downside_deviation != 0:
|
||||
sortino = (mean_excess_return / downside_deviation) * annualization_factor
|
||||
|
||||
return sharpe, sortino
|
||||
|
||||
# 使用 groupby().rolling() 结合 apply 来高效计算
|
||||
# 注意: 这里使用 shift(-N_days) 来获取未来 N_days 的窗口
|
||||
# 同时 rolling 窗口需要是 N_days 大小,并对 pct_chg 序列进行操作
|
||||
# 结果会被放置在窗口的末尾,但我们实际上需要它对应到窗口的起始位置
|
||||
# 因此,我们先计算,然后将其向上 shift N_days + 1 (或者根据 rolling 的行为调整)
|
||||
|
||||
# 获取未来 N_days 的每日收益率序列
|
||||
# 为了避免在每一行中循环,我们构造一个辅助的DataFrame,其中包含未来N天的收益率
|
||||
# 这里使用一个更高效的思路:对每只股票,生成一个N天的收益率列表
|
||||
|
||||
# 使用 groupby().apply() 和 list comprehension 来构建未来N天的收益率列表
|
||||
# 为了获得未来N天的收益率,需要对pct_chg进行shift(-N_days)
|
||||
# 然后再对每个N_days的窗口进行处理
|
||||
|
||||
# 方法一:先获取未来N天的收益率列,然后进行滑动窗口计算 (更推荐,效率高)
|
||||
# 创建一个辅助列,表示未来 N 天的每日收益率列表
|
||||
# 注意:这里需要 N_days 长度的窗口,且数据点至少为 min_periods_for_std
|
||||
|
||||
# 对每只股票进行分组,然后计算未来 N 天的夏普/索提诺
|
||||
# 这仍然涉及到对每个时间点获取未来N天的收益序列,Pandas 的 rolling 默认是向后看的
|
||||
# 为了实现向后看 N_days (即从当前日期 t 预测 t+1 到 t+N_days),我们需要一些技巧
|
||||
|
||||
# 最直接且通常高效的方法是:
|
||||
# 1. 计算日收益率。
|
||||
# 2. 对每个股票,对日收益率进行反向滚动,然后计算指标。
|
||||
# 或者更简单地,使用 shift(-N_days) 创建“未来的”日收益率列,然后进行滚动。
|
||||
|
||||
# 为了简化且保持效率,我们可以这样做:
|
||||
# 1. 计算每个股票的每日收益率。
|
||||
# 2. 对于每个 (ts_code, trade_date) 行,我们将查看其未来的 N_days 每日收益率。
|
||||
# 这在 Pandas 中不直接通过 .rolling() 实现,因为 rolling 是“当前及之前”的窗口。
|
||||
# 我们可以用 groupby().apply() 配合自定义函数来模拟这个“未来窗口”的行为。
|
||||
|
||||
def _apply_metrics_per_stock(stock_df_group):
|
||||
"""
|
||||
对单只股票的数据帧进行处理,计算其每个日期的未来 N 日夏普/索提诺比率。
|
||||
"""
|
||||
sharpe_labels = [np.nan] * len(stock_df_group)
|
||||
sortino_labels = [np.nan] * len(stock_df_group)
|
||||
|
||||
# 遍历当前股票的每个日期,获取未来的 N_days 收益率序列
|
||||
for i in range(len(stock_df_group) - N_days):
|
||||
# 获取未来 N_days 的每日收益率序列
|
||||
# 从当前日期 t 的下一天 (t+1) 开始,到 t+N_days
|
||||
future_pct_chgs = stock_df_group['pct_chg'].iloc[i+1 : i+1+N_days].dropna()
|
||||
|
||||
# 计算夏普和索提诺
|
||||
sharpe, sortino = _calculate_metrics_for_window(future_pct_chgs)
|
||||
|
||||
# 赋值到对应日期
|
||||
sharpe_labels[i] = sharpe
|
||||
sortino_labels[i] = sortino
|
||||
|
||||
stock_df_group['sharpe_ratio_label'] = sharpe_labels
|
||||
stock_df_group['sortino_ratio_label'] = sortino_labels
|
||||
return stock_df_group
|
||||
|
||||
# 对每个股票分组应用这个函数
|
||||
df_result = df_copy.groupby('ts_code', group_keys=False).apply(_apply_metrics_per_stock)
|
||||
|
||||
# # 清理不再需要的中间列
|
||||
# df_result = df_result.drop(columns=['pct_chg'])
|
||||
|
||||
return df_result
|
||||
1462
predictions_test.tsv
1462
predictions_test.tsv
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user