factor优化(暂存版)

This commit is contained in:
2025-10-14 09:44:46 +08:00
parent 44315b2c76
commit 7862b9739a
9 changed files with 804 additions and 4427 deletions

View File

@@ -6,7 +6,9 @@
import polars as pl
import numpy as np
from typing import Dict, List, Optional, Any
from operator_framework import StockWiseOperator, OperatorConfig
from tqdm import tqdm
from main.factor.operator_framework import StockWiseOperator, OperatorConfig
from scipy.stats import linregress
@@ -14,6 +16,8 @@ class PriceMinusDeductionPriceOperator(StockWiseOperator):
"""价格减抵扣价算子"""
def __init__(self, n: int = 10):
if n <= 0:
raise ValueError("n must be positive")
config = OperatorConfig(
name=f"price_minus_deduction_price_{n}",
description=f"{n}日价格减抵扣价",
@@ -24,21 +28,22 @@ class PriceMinusDeductionPriceOperator(StockWiseOperator):
super().__init__(config)
self.n = n
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算价格减抵扣价"""
# 抵扣价是n-1周期前的价格
deduction_price = pl.col('close').shift(self.n - 1)
# 计算差值
price_diff = pl.col('close') - deduction_price
return stock_df.with_columns(price_diff.alias(f'price_minus_deduction_price_{self.n}'))
def get_factor_name(self) -> str:
return f'price_minus_deduction_price_{self.n}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
# 抵扣价是 n 日前的价格(更合理),若坚持 n-1 则保留
deduction_price = group_df['close'].shift(self.n) # 建议用 n不是 n-1
price_diff = group_df['close'] - deduction_price
return price_diff.alias(self.get_factor_name())
class PriceDeductionPriceDiffRatioToSMAOperator(StockWiseOperator):
"""价格抵扣价差值相对SMA比率算子"""
def __init__(self, n: int = 10):
if n <= 0:
raise ValueError("n must be positive")
config = OperatorConfig(
name=f"price_deduction_price_diff_ratio_to_sma_{n}",
description=f"{n}日价格抵扣价差值相对SMA比率",
@@ -49,27 +54,23 @@ class PriceDeductionPriceDiffRatioToSMAOperator(StockWiseOperator):
super().__init__(config)
self.n = n
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算价格抵扣价差值相对SMA比率"""
# 计算n日SMA
sma = pl.col('close').rolling_mean(window=self.n)
# 抵扣价
deduction_price = pl.col('close').shift(self.n - 1)
# 计算差值
diff = pl.col('close') - deduction_price
# 计算比率 (处理除零)
def get_factor_name(self) -> str:
return f'price_deduction_price_diff_ratio_to_sma_{self.n}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
sma = group_df['close'].rolling_mean(window_size=self.n)
deduction_price = group_df['close'].shift(self.n)
diff = group_df['close'] - deduction_price
ratio = diff / (sma + 1e-8)
return stock_df.with_columns(ratio.alias(f'price_deduction_price_diff_ratio_to_sma_{self.n}'))
return ratio.alias(self.get_factor_name())
class CatPriceVsSmaVsDeductionPriceOperator(StockWiseOperator):
"""价格vsSMAvs抵扣价分类算子"""
def __init__(self, n: int = 10):
if n <= 0:
raise ValueError("n must be positive")
config = OperatorConfig(
name=f"cat_price_vs_sma_vs_deduction_price_{n}",
description=f"{n}日价格vsSMAvs抵扣价分类",
@@ -80,40 +81,35 @@ class CatPriceVsSmaVsDeductionPriceOperator(StockWiseOperator):
super().__init__(config)
self.n = n
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算价格vsSMAvs抵扣价分类"""
# 计算n日SMA
sma = pl.col('close').rolling_mean(window=self.n)
def get_factor_name(self) -> str:
return f'cat_price_vs_sma_vs_deduction_price_{self.n}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
sma = group_df['close'].rolling_mean(window_size=self.n)
deduction_price = group_df['close'].shift(self.n)
# 抵扣价
deduction_price = pl.col('close').shift(self.n - 1)
cond1 = (group_df['close'] > sma) & (deduction_price > sma)
cond2 = (group_df['close'] < sma) & (deduction_price < sma)
cond3 = (group_df['close'] > sma) & (deduction_price <= sma)
cond4 = (group_df['close'] <= sma) & (deduction_price > sma)
# 定义条件
conditions = [
# 1: 当前价 > SMA 且 抵扣价 > SMA
(pl.col('close') > sma) & (deduction_price > sma),
# 2: 当前价 < SMA 且 抵扣价 < SMA
(pl.col('close') < sma) & (deduction_price < sma),
# 3: 当前价 > SMA 且 抵扣价 <= SMA
(pl.col('close') > sma) & (deduction_price <= sma),
# 4: 当前价 <= SMA 且 抵扣价 > SMA
(pl.col('close') <= sma) & (deduction_price > sma),
]
choices = [1, 2, 3, 4]
# 使用select函数进行分类
classification = pl.select(conditions=conditions, choices=choices, default=0)
return stock_df.with_columns(
classification.alias(f'cat_price_vs_sma_vs_deduction_price_{self.n}')
classification = (
pl.when(cond1).then(1)
.when(cond2).then(2)
.when(cond3).then(3)
.when(cond4).then(4)
.otherwise(0)
)
return classification.alias(self.get_factor_name())
# ✅ 修复:使用 rolling_map
class VolatilitySlopeOperator(StockWiseOperator):
"""波动率斜率算子"""
def __init__(self, long_window: int = 20, short_window: int = 5):
if long_window <= 0 or short_window <= 0:
raise ValueError("Windows must be positive")
config = OperatorConfig(
name=f"volatility_slope_{long_window}_{short_window}",
description=f"{long_window}日波动率{short_window}日斜率",
@@ -125,34 +121,40 @@ class VolatilitySlopeOperator(StockWiseOperator):
self.long_window = long_window
self.short_window = short_window
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算波动率斜率"""
# 计算长期波动率
long_vol = pl.col('pct_chg').rolling_std(window=self.long_window)
def get_factor_name(self) -> str:
return f'volatility_slope_{self.long_window}_{self.short_window}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
# 先计算长期波动率(标准差)
long_vol = group_df['pct_chg'].rolling_std(window_size=self.long_window)
# 计算斜率函数
def calculate_slope(series):
if len(series) < 2:
return 0
x = np.arange(len(series))
slope, _, _, _, _ = linregress(x, series)
return slope
# 定义斜率函数(输入是 numpy array
def slope_func(window_vals: np.ndarray) -> float:
if len(window_vals) < 2 or pl.Series(window_vals).is_null().any():
return 0.0
x = np.arange(len(window_vals))
try:
slope, _, _, _, _ = linregress(x, window_vals)
return slope if np.isfinite(slope) else 0.0
except:
return 0.0
# 计算斜率
volatility_slope = long_vol.rolling_apply(
function=calculate_slope,
window_size=self.short_window
)
return stock_df.with_columns(
volatility_slope.alias(f'volatility_slope_{self.long_window}_{self.short_window}')
# 对波动率序列应用 rolling_map
volatility_slope = long_vol.rolling_map(
function=slope_func,
window_size=self.short_window,
min_periods=2 # 至少2点才能算斜率
)
return volatility_slope.alias(self.get_factor_name())
# ✅ 修复:使用 rolling_map
class TurnoverRateTrendStrengthOperator(StockWiseOperator):
"""换手率趋势强度算子"""
def __init__(self, window: int = 5):
if window <= 0:
raise ValueError("Window must be positive")
config = OperatorConfig(
name=f"turnover_trend_strength_{window}",
description=f"{window}日换手率趋势强度",
@@ -163,31 +165,34 @@ class TurnoverRateTrendStrengthOperator(StockWiseOperator):
super().__init__(config)
self.window = window
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算换手率趋势强度"""
# 计算斜率函数
def calculate_slope(series):
if len(series) < 2:
return 0
x = np.arange(len(series))
slope, _, _, _, _ = linregress(x, series)
return slope
def get_factor_name(self) -> str:
return f'turnover_trend_strength_{self.window}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
def slope_func(window_vals: np.ndarray) -> float:
if len(window_vals) < 2 or pl.Series(window_vals).is_null().any():
return 0.0
x = np.arange(len(window_vals))
try:
slope, _, _, _, _ = linregress(x, window_vals)
return slope if np.isfinite(slope) else 0.0
except:
return 0.0
# 计算换手率斜率
trend_strength = pl.col('turnover_rate').rolling_apply(
function=calculate_slope,
window_size=self.window
)
return stock_df.with_columns(
trend_strength.alias(f'turnover_trend_strength_{self.window}')
trend_strength = group_df['turnover_rate'].rolling_map(
function=slope_func,
window_size=self.window,
min_periods=2
)
return trend_strength.alias(self.get_factor_name())
class FreeFloatTurnoverSurgeOperator(StockWiseOperator):
"""自由流通股换手率激增算子"""
def __init__(self, window: int = 10):
if window <= 0:
raise ValueError("Window must be positive")
config = OperatorConfig(
name=f"ff_turnover_surge_{window}",
description=f"{window}日自由流通股换手率激增",
@@ -198,21 +203,21 @@ class FreeFloatTurnoverSurgeOperator(StockWiseOperator):
super().__init__(config)
self.window = window
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算自由流通股换手率激增"""
# 计算均值
avg_turnover = pl.col('turnover_rate').rolling_mean(window=self.window)
# 计算激增比率
surge_ratio = pl.col('turnover_rate') / (avg_turnover + 1e-8)
return stock_df.with_columns(surge_ratio.alias(f'ff_turnover_surge_{self.window}'))
def get_factor_name(self) -> str:
return f'ff_turnover_surge_{self.window}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
avg_turnover = group_df['turnover_rate'].rolling_mean(window_size=self.window)
surge_ratio = group_df['turnover_rate'] / (avg_turnover + 1e-8)
return surge_ratio.alias(self.get_factor_name())
class PriceVolumeTrendCoherenceOperator(StockWiseOperator):
"""价量趋势一致性算子"""
def __init__(self, price_window: int = 5, volume_window: int = 20):
if price_window <= 0 or volume_window <= 0:
raise ValueError("Windows must be positive")
config = OperatorConfig(
name=f"price_volume_coherence_{price_window}_{volume_window}",
description=f"{price_window}日价格{volume_window}日成交量趋势一致性",
@@ -224,25 +229,19 @@ class PriceVolumeTrendCoherenceOperator(StockWiseOperator):
self.price_window = price_window
self.volume_window = volume_window
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算价量趋势一致性"""
# 计算价格上涨占比
def price_up_ratio(series):
return (series.diff() > 0).rolling_mean(window=self.price_window)
def get_factor_name(self) -> str:
return f'price_volume_coherence_{self.price_window}_{self.volume_window}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
price_up = (group_df['close'].diff() > 0).cast(pl.Int8)
price_up_ratio = price_up.rolling_mean(window_size=self.price_window)
price_up = pl.col('close').apply(price_up_ratio)
vol_avg = group_df['vol'].rolling_mean(window_size=self.volume_window)
vol_above = (group_df['vol'] > vol_avg).cast(pl.Int8)
vol_above_ratio = vol_above.rolling_mean(window_size=self.price_window)
# 计算成交量高于均值占比
vol_avg = pl.col('vol').rolling_mean(window=self.volume_window)
vol_above_avg = pl.col('vol') > vol_avg
vol_above_ratio = vol_above_avg.cast(int).rolling_mean(window=self.price_window)
# 计算一致性
coherence = price_up * vol_above_ratio
return stock_df.with_columns(
coherence.alias(f'price_volume_coherence_{self.price_window}_{self.volume_window}')
)
coherence = price_up_ratio * vol_above_ratio
return coherence.alias(self.get_factor_name())
class FreeFloatToTotalTurnoverRatioOperator(StockWiseOperator):
@@ -258,19 +257,21 @@ class FreeFloatToTotalTurnoverRatioOperator(StockWiseOperator):
)
super().__init__(config)
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算自由流通股对总换手率比率"""
# 假设turnover_rate是自由流通股换手率
# 计算比率 (简化处理)
ratio = pl.col('turnover_rate') / (pl.col('turnover_rate') + 1e-8)
return stock_df.with_columns(ratio.alias('ff_to_total_turnover_ratio'))
def get_factor_name(self) -> str:
return 'ff_to_total_turnover_ratio'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
# 实际业务中可能需要 total_turnover_rate这里简化
ratio = pl.lit(1.0) # 或根据实际逻辑修改
return ratio.alias('ff_to_total_turnover_ratio')
class VarianceOperator(StockWiseOperator):
"""方差算子"""
def __init__(self, window: int):
if window <= 0:
raise ValueError("Window must be positive")
config = OperatorConfig(
name=f"variance_{window}",
description=f"{window}日方差",
@@ -281,12 +282,12 @@ class VarianceOperator(StockWiseOperator):
super().__init__(config)
self.window = window
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算方差"""
# 计算方差
variance = pl.col('pct_chg').rolling_var(window=self.window)
return stock_df.with_columns(variance.alias(f'variance_{self.window}'))
def get_factor_name(self) -> str:
return f'variance_{self.window}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
variance = group_df['pct_chg'].rolling_var(window_size=self.window)
return variance.alias(self.get_factor_name())
class LimitUpDownOperator(StockWiseOperator):
@@ -302,26 +303,12 @@ class LimitUpDownOperator(StockWiseOperator):
)
super().__init__(config)
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算涨跌停因子"""
# 判断是否涨停
up_limit = pl.col('close') == pl.col('up_limit')
# 判断是否跌停
down_limit = pl.col('close') == pl.col('down_limit')
# 计算10日涨停计数
up_count_10d = up_limit.cast(int).rolling_sum(window=10)
# 计算10日跌停计数
down_count_10d = down_limit.cast(int).rolling_sum(window=10)
return stock_df.with_columns([
up_limit.alias('cat_up_limit'),
down_limit.alias('cat_down_limit'),
up_count_10d.alias('up_limit_count_10d'),
down_count_10d.alias('down_limit_count_10d')
])
def get_factor_name(self) -> str:
return 'cat_up_limit'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
up_limit = (group_df['close'] == group_df['up_limit']).cast(pl.Int8)
return up_limit.alias('cat_up_limit')
class ConsecutiveUpLimitOperator(StockWiseOperator):
@@ -337,19 +324,21 @@ class ConsecutiveUpLimitOperator(StockWiseOperator):
)
super().__init__(config)
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算连续涨停天数"""
# 计算连续涨停
# 简化处理,实际应用中需要更复杂的逻辑
consecutive = pl.col('cat_up_limit').cast(int)
return stock_df.with_columns(consecutive.alias('consecutive_up_limit'))
def get_factor_name(self) -> str:
return 'consecutive_up_limit'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
# 简化版:实际连续计数需用 cumsum + groupby trick
# 这里先返回原始值,后续可优化
return group_df['cat_up_limit'].alias('consecutive_up_limit')
class MomentumFactorOperator(StockWiseOperator):
"""动量因子算子"""
def __init__(self, alpha: float = 0.5):
if not (0 <= alpha <= 1):
raise ValueError("alpha should be between 0 and 1")
config = OperatorConfig(
name=f"momentum_factor_{alpha}",
description=f"动量因子(alpha={alpha})",
@@ -360,12 +349,12 @@ class MomentumFactorOperator(StockWiseOperator):
super().__init__(config)
self.alpha = alpha
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算动量因子"""
# 计算动量因子
momentum = pl.col('volume_change_rate') + self.alpha * pl.col('turnover_deviation')
return stock_df.with_columns(momentum.alias(f'momentum_factor_{self.alpha}'))
def get_factor_name(self) -> str:
return f'momentum_factor_{self.alpha}'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
momentum = group_df['volume_change_rate'] + self.alpha * group_df['turnover_deviation']
return momentum.alias(self.get_factor_name())
class ResonanceFactorOperator(StockWiseOperator):
@@ -381,28 +370,28 @@ class ResonanceFactorOperator(StockWiseOperator):
)
super().__init__(config)
def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
"""计算共振因子"""
# 计算共振因子
resonance = pl.col('volume_ratio') * pl.col('pct_chg')
return stock_df.with_columns(resonance.alias('resonance_factor'))
def get_factor_name(self) -> str:
return 'resonance_factor'
def calc_factor(self, group_df: pl.DataFrame, **kwargs) -> pl.Series:
resonance = group_df['volume_ratio'] * group_df['pct_chg']
return resonance.alias('resonance_factor')
# 动量因子集合
MOMENTUM_OPERATORS = [
PriceMinusDeductionPriceOperator(),
PriceDeductionPriceDiffRatioToSMAOperator(),
CatPriceVsSmaVsDeductionPriceOperator(),
VolatilitySlopeOperator(),
TurnoverRateTrendStrengthOperator(5),
PriceMinusDeductionPriceOperator(10),
PriceDeductionPriceDiffRatioToSMAOperator(10),
CatPriceVsSmaVsDeductionPriceOperator(10),
# VolatilitySlopeOperator(20, 5),
# TurnoverRateTrendStrengthOperator(5),
FreeFloatTurnoverSurgeOperator(10),
PriceVolumeTrendCoherenceOperator(),
PriceVolumeTrendCoherenceOperator(5, 20),
FreeFloatToTotalTurnoverRatioOperator(),
VarianceOperator(20),
LimitUpDownOperator(),
ConsecutiveUpLimitOperator(),
MomentumFactorOperator(),
# MomentumFactorOperator(0.5),
ResonanceFactorOperator(),
]
@@ -410,19 +399,12 @@ MOMENTUM_OPERATORS = [
def apply_momentum_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
"""
应用所有动量因子
Args:
df: 输入的Polars DataFrame
operators: 要应用的算子列表如果为None则使用默认列表
Returns:
添加了动量因子的DataFrame
"""
if operators is None:
operators = MOMENTUM_OPERATORS
result_df = df
for operator in operators:
result_df = operator(result_df)
for operator in tqdm(operators, desc="Applying momentum factors"):
result_df = operator.apply(result_df)
return result_df