From 44315b2c76f0f73119b6815de462ff85f182490b Mon Sep 17 00:00:00 2001
From: liaozhaorun <1300336796@qq.com>
Date: Mon, 13 Oct 2025 21:42:35 +0800
Subject: [PATCH] =?UTF-8?q?factor=E4=BC=98=E5=8C=96=EF=BC=8C=E6=94=B9?=
=?UTF-8?q?=E4=B8=BApolars?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/factor/operator_base.py | 196 ++
main/factor/operator_framework.py | 250 ++
main/factor/polars_chip_factors.py | 312 +++
main/factor/polars_complex_factors.py | 648 +++++
main/factor/polars_factors.py | 237 ++
main/factor/polars_momentum_factors.py | 428 ++++
main/factor/polars_money_flow_factors.py | 245 ++
main/factor/polars_sentiment_factors.py | 365 +++
main/factor/polars_technical_factors.py | 488 ++++
main/factor/polars_volatility_factors.py | 419 ++++
main/factor/polars_volume_factors.py | 480 ++++
main/train/Classify/Classify2.ipynb | 2860 ++++++++++++++++++++++
12 files changed, 6928 insertions(+)
create mode 100644 main/factor/operator_base.py
create mode 100644 main/factor/operator_framework.py
create mode 100644 main/factor/polars_chip_factors.py
create mode 100644 main/factor/polars_complex_factors.py
create mode 100644 main/factor/polars_factors.py
create mode 100644 main/factor/polars_momentum_factors.py
create mode 100644 main/factor/polars_money_flow_factors.py
create mode 100644 main/factor/polars_sentiment_factors.py
create mode 100644 main/factor/polars_technical_factors.py
create mode 100644 main/factor/polars_volatility_factors.py
create mode 100644 main/factor/polars_volume_factors.py
create mode 100644 main/train/Classify/Classify2.ipynb
diff --git a/main/factor/operator_base.py b/main/factor/operator_base.py
new file mode 100644
index 0000000..1b14b5a
--- /dev/null
+++ b/main/factor/operator_base.py
@@ -0,0 +1,196 @@
+"""
+因子算子基础框架 - 简化版本
+提供股票截面和日期截面两个基础函数
+"""
+
+import polars as pl
+from typing import Callable, Any, Optional, Union
+import logging
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+def apply_stockwise(
+ df: pl.DataFrame,
+ operator_func: Callable[[pl.DataFrame, Any], pl.DataFrame],
+ *args,
+ **kwargs
+) -> pl.DataFrame:
+ """
+ 在股票截面上应用算子函数
+
+ Args:
+ df: 输入的polars DataFrame,必须包含ts_code和trade_date列
+ operator_func: 算子函数,接收单个股票的数据和参数,返回处理后的DataFrame
+ *args, **kwargs: 传递给算子函数的额外参数
+
+ Returns:
+ 处理后的完整DataFrame
+ """
+ # 验证必需列
+ required_cols = ['ts_code', 'trade_date']
+ missing_cols = [col for col in required_cols if col not in df.columns]
+ if missing_cols:
+ raise ValueError(f"缺少必需列: {missing_cols}")
+
+ # 获取股票列表
+ stock_list = df['ts_code'].unique().to_list()
+ results = []
+
+ # 按股票分组处理
+ for ts_code in stock_list:
+ try:
+ # 获取单个股票的数据并按日期排序
+ stock_df = df.filter(pl.col('ts_code') == ts_code).sort('trade_date')
+
+ # 应用算子函数
+ result_df = operator_func(stock_df, *args, **kwargs)
+ results.append(result_df)
+
+ except Exception as e:
+ logger.error(f"股票 {ts_code} 处理失败: {e}")
+ # 失败时返回原始数据
+ stock_df = df.filter(pl.col('ts_code') == ts_code).sort('trade_date')
+ results.append(stock_df)
+
+ # 合并结果并排序
+ if results:
+ return pl.concat(results).sort(['ts_code', 'trade_date'])
+ else:
+ return df
+
+
+def apply_datewise(
+ df: pl.DataFrame,
+ operator_func: Callable[[pl.DataFrame, Any], pl.DataFrame],
+ *args,
+ **kwargs
+) -> pl.DataFrame:
+ """
+ 在日期截面上应用算子函数
+
+ Args:
+ df: 输入的polars DataFrame,必须包含ts_code和trade_date列
+ operator_func: 算子函数,接收单个日期的数据和参数,返回处理后的DataFrame
+ *args, **kwargs: 传递给算子函数的额外参数
+
+ Returns:
+ 处理后的完整DataFrame
+ """
+ # 验证必需列
+ required_cols = ['ts_code', 'trade_date']
+ missing_cols = [col for col in required_cols if col not in df.columns]
+ if missing_cols:
+ raise ValueError(f"缺少必需列: {missing_cols}")
+
+ # 获取日期列表
+ date_list = df['trade_date'].unique().to_list()
+ results = []
+
+ # 按日期分组处理
+ for trade_date in date_list:
+ try:
+ # 获取单个日期的数据
+ date_df = df.filter(pl.col('trade_date') == trade_date)
+
+ # 应用算子函数
+ result_df = operator_func(date_df, *args, **kwargs)
+ results.append(result_df)
+
+ except Exception as e:
+ logger.error(f"日期 {trade_date} 处理失败: {e}")
+ # 失败时返回原始数据
+ date_df = df.filter(pl.col('trade_date') == trade_date)
+ results.append(date_df)
+
+ # 合并结果并排序
+ if results:
+ return pl.concat(results).sort(['ts_code', 'trade_date'])
+ else:
+ return df
+
+
+# 常用算子函数示例
+def rolling_mean_operator(df: pl.DataFrame, column: str, window: int, output_col: str = None) -> pl.DataFrame:
+ """
+ 滚动均值算子 - 股票截面
+
+ Args:
+ df: 单个股票的数据
+ column: 要计算均值的列
+ window: 窗口大小
+ output_col: 输出列名,默认为f'{column}_mean_{window}'
+
+ Returns:
+ 添加均值列的DataFrame
+ """
+ if output_col is None:
+ output_col = f'{column}_mean_{window}'
+
+ return df.with_columns(
+ pl.col(column).rolling_mean(window_size=window).alias(output_col)
+ )
+
+
+def rolling_std_operator(df: pl.DataFrame, column: str, window: int, output_col: str = None) -> pl.DataFrame:
+ """
+ 滚动标准差算子 - 股票截面
+
+ Args:
+ df: 单个股票的数据
+ column: 要计算标准差的列
+ window: 窗口大小
+ output_col: 输出列名,默认为f'{column}_std_{window}'
+
+ Returns:
+ 添加标准差列的DataFrame
+ """
+ if output_col is None:
+ output_col = f'{column}_std_{window}'
+
+ return df.with_columns(
+ pl.col(column).rolling_std(window_size=window).alias(output_col)
+ )
+
+
+def rank_operator(df: pl.DataFrame, column: str, ascending: bool = True, output_col: str = None) -> pl.DataFrame:
+ """
+ 排名算子 - 日期截面
+
+ Args:
+ df: 单个日期的数据
+ column: 要排名的列
+ ascending: 是否升序
+ output_col: 输出列名,默认为f'{column}_rank'
+
+ Returns:
+ 添加排名列的DataFrame
+ """
+ if output_col is None:
+ output_col = f'{column}_rank'
+
+ return df.with_columns(
+ pl.col(column).rank(method='dense', descending=not ascending).alias(output_col)
+ )
+
+
+def pct_change_operator(df: pl.DataFrame, column: str, periods: int = 1, output_col: str = None) -> pl.DataFrame:
+ """
+ 百分比变化算子 - 股票截面
+
+ Args:
+ df: 单个股票的数据
+ column: 要计算变化的列
+ periods: 期数
+ output_col: 输出列名,默认为f'{column}_pct_change_{periods}'
+
+ Returns:
+ 添加变化率列的DataFrame
+ """
+ if output_col is None:
+ output_col = f'{column}_pct_change_{periods}'
+
+ return df.with_columns(
+ ((pl.col(column) / pl.col(column).shift(periods)) - 1).alias(output_col)
+ )
diff --git a/main/factor/operator_framework.py b/main/factor/operator_framework.py
new file mode 100644
index 0000000..eaca01e
--- /dev/null
+++ b/main/factor/operator_framework.py
@@ -0,0 +1,250 @@
+"""
+因子算子框架 - 使用Polars实现统一的因子计算
+避免数据泄露,支持切面计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Callable, Optional, Union, Any
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+import logging
+
+# 配置日志
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class OperatorConfig:
+ """算子配置"""
+ name: str
+ description: str
+ required_columns: List[str]
+ output_columns: List[str]
+ parameters: Dict[str, Any]
+
+
+class DataSlice:
+ """数据切面基类"""
+
+ def __init__(self, df: pl.DataFrame):
+ self.df = df
+ self.validate_data()
+
+ def validate_data(self):
+ """验证数据格式"""
+ required_cols = ['ts_code', 'trade_date']
+ missing_cols = [col for col in required_cols if col not in self.df.columns]
+ if missing_cols:
+ raise ValueError(f"缺少必需列: {missing_cols}")
+
+ def get_stock_slice(self, ts_code: str) -> pl.DataFrame:
+ """获取单个股票的数据切面"""
+ return self.df.filter(pl.col('ts_code') == ts_code).sort('trade_date')
+
+ def get_date_slice(self, trade_date: str) -> pl.DataFrame:
+ """获取单个日期的数据切面"""
+ return self.df.filter(pl.col('trade_date') == trade_date)
+
+ def get_stock_list(self) -> List[str]:
+ """获取股票列表"""
+ return self.df['ts_code'].unique().to_list()
+
+ def get_date_list(self) -> List[str]:
+ """获取日期列表"""
+ return self.df['trade_date'].unique().to_list()
+
+
+class BaseOperator(ABC):
+ """算子基类"""
+
+ def __init__(self, config: OperatorConfig):
+ self.config = config
+ self.name = config.name
+ self.required_columns = config.required_columns
+ self.output_columns = config.output_columns
+
+ def validate_input(self, df: pl.DataFrame) -> bool:
+ """验证输入数据"""
+ missing_cols = [col for col in self.required_columns if col not in df.columns]
+ if missing_cols:
+ logger.warning(f"算子 {self.name} 缺少必需列: {missing_cols}")
+ return False
+ return True
+
+ @abstractmethod
+ def apply(self, df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """应用算子"""
+ pass
+
+ def __call__(self, df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """调用算子"""
+ if not self.validate_input(df):
+ # 返回原始数据,添加NaN列
+ for col in self.output_columns:
+ df = df.with_columns(pl.lit(None).alias(col))
+ return df
+
+ try:
+ return self.apply(df, **kwargs)
+ except Exception as e:
+ logger.error(f"算子 {self.name} 应用失败: {e}")
+ # 返回原始数据,添加NaN列
+ for col in self.output_columns:
+ df = df.with_columns(pl.lit(None).alias(col))
+ return df
+
+
+class StockWiseOperator(BaseOperator):
+ """股票切面算子 - 按股票分组计算"""
+
+ def apply(self, df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """按股票分组应用算子"""
+ stock_list = df['ts_code'].unique().to_list()
+ results = []
+
+ for ts_code in stock_list:
+ stock_df = df.filter(pl.col('ts_code') == ts_code).sort('trade_date')
+ try:
+ result_df = self.apply_stock(stock_df, **kwargs)
+ results.append(result_df)
+ except Exception as e:
+ logger.error(f"股票 {ts_code} 算子应用失败: {e}")
+ # 为失败的股票添加NaN列
+ for col in self.output_columns:
+ stock_df = stock_df.with_columns(pl.lit(None).alias(col))
+ results.append(stock_df)
+
+ return pl.concat(results).sort(['ts_code', 'trade_date'])
+
+ @abstractmethod
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """应用到单个股票数据"""
+ pass
+
+
+class DateWiseOperator(BaseOperator):
+ """日期切面算子 - 按日期分组计算"""
+
+ def apply(self, df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """按日期分组应用算子"""
+ date_list = df['trade_date'].unique().to_list()
+ results = []
+
+ for trade_date in date_list:
+ date_df = df.filter(pl.col('trade_date') == trade_date)
+ try:
+ result_df = self.apply_date(date_df, **kwargs)
+ results.append(result_df)
+ except Exception as e:
+ logger.error(f"日期 {trade_date} 算子应用失败: {e}")
+ # 为失败的日期添加NaN列
+ for col in self.output_columns:
+ date_df = date_df.with_columns(pl.lit(None).alias(col))
+ results.append(date_df)
+
+ return pl.concat(results).sort(['ts_code', 'trade_date'])
+
+ @abstractmethod
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """应用到单个日期数据"""
+ pass
+
+
+class RollingOperator(StockWiseOperator):
+ """滚动窗口算子基类"""
+
+ def __init__(self, config: OperatorConfig, window: int, min_periods: Optional[int] = None):
+ super().__init__(config)
+ self.window = window
+ self.min_periods = min_periods or max(1, window // 2)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """应用滚动窗口计算"""
+ return self.apply_rolling(stock_df, **kwargs)
+
+ @abstractmethod
+ def apply_rolling(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """滚动窗口计算逻辑"""
+ pass
+
+
+# 基础算子实现
+class ReturnOperator(RollingOperator):
+ """收益率算子"""
+
+ def __init__(self, periods: int = 1):
+ config = OperatorConfig(
+ name=f"return_{periods}",
+ description=f"{periods}期收益率",
+ required_columns=['close'],
+ output_columns=[f'return_{periods}'],
+ parameters={'periods': periods}
+ )
+ super().__init__(config, window=periods + 1)
+ self.periods = periods
+
+ def apply_rolling(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算收益率"""
+ return stock_df.with_columns(
+ (pl.col('close') / pl.col('close').shift(self.periods) - 1).alias(f'return_{self.periods}')
+ )
+
+
+class VolatilityOperator(RollingOperator):
+ """波动率算子"""
+
+ def __init__(self, window: int = 20):
+ config = OperatorConfig(
+ name=f"volatility_{window}",
+ description=f"{window}日波动率",
+ required_columns=['pct_chg'],
+ output_columns=[f'volatility_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config, window=window)
+
+ def apply_rolling(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算波动率"""
+ return stock_df.with_columns(
+ pl.col('pct_chg').rolling_std(window=self.window).alias(f'volatility_{self.window}')
+ )
+
+
+class MeanOperator(RollingOperator):
+ """均值算子"""
+
+ def __init__(self, column: str, window: int):
+ config = OperatorConfig(
+ name=f"mean_{column}_{window}",
+ description=f"{column}的{window}日均值",
+ required_columns=[column],
+ output_columns=[f'mean_{column}_{window}'],
+ parameters={'column': column, 'window': window}
+ )
+ super().__init__(config, window=window)
+ self.column = column
+
+ def apply_rolling(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算均值"""
+ return stock_df.with_columns(
+ pl.col(self.column).rolling_mean(window=self.window).alias(f'mean_{self.column}_{self.window}')
+ )
+
+
+class RankOperator(DateWiseOperator):
+ """排名算子"""
+
+ def __init__(self, column: str, ascending: bool = True):
+ config = OperatorConfig(
+ name=f"rank_{column}",
+ description=f"{column}的排名",
+ required_columns=[column],
+ output_columns=[f'rank_{column}'],
+ parameters={'column': column, 'ascending': ascending}
+ )
+ super().__init__(config)
+ self.column = column
+ self.ascending = ascending
+
diff --git a/main/factor/polars_chip_factors.py b/main/factor/polars_chip_factors.py
new file mode 100644
index 0000000..d393699
--- /dev/null
+++ b/main/factor/polars_chip_factors.py
@@ -0,0 +1,312 @@
+"""
+筹码分布因子 - 使用Polars实现
+包含筹码集中度、分布偏度、浮筹比例等相关因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, OperatorConfig
+
+
+class ChipConcentrationOperator(StockWiseOperator):
+ """筹码集中度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="chip_concentration",
+ description="筹码集中度",
+ required_columns=['cost_95pct', 'cost_5pct', 'close'],
+ output_columns=['chip_concentration_range'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算筹码集中度"""
+ epsilon = 1e-8
+
+ # 计算筹码集中度范围,相对于当前价格标准化
+ concentration_range = (pl.col('cost_95pct') - pl.col('cost_5pct')) / (pl.col('close') + epsilon)
+
+ return stock_df.with_columns(concentration_range.alias('chip_concentration_range'))
+
+
+class ChipSkewnessOperator(StockWiseOperator):
+ """筹码分布偏度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="chip_skewness",
+ description="筹码分布偏度",
+ required_columns=['weight_avg', 'cost_50pct'],
+ output_columns=['chip_skewness'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算筹码分布偏度"""
+ epsilon = 1e-8
+
+ # 计算偏度:(加权平均成本 - 中位数成本) / 中位数成本
+ skewness = (pl.col('weight_avg') - pl.col('cost_50pct')) / (pl.col('cost_50pct') + epsilon)
+
+ return stock_df.with_columns(skewness.alias('chip_skewness'))
+
+
+class FloatingChipProxyOperator(StockWiseOperator):
+ """浮筹比例代理算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="floating_chip_proxy",
+ description="浮筹比例代理",
+ required_columns=['close', 'cost_15pct', 'winner_rate'],
+ output_columns=['floating_chip_proxy'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算浮筹比例代理"""
+ # 计算价格与15%成本线的距离
+ price_dist_cost15 = (pl.col('close') - pl.col('cost_15pct')) / pl.col('close')
+
+ # 计算浮筹代理:获利盘比例 * max(0, 价格距离)
+ floating_proxy = pl.col('winner_rate') * pl.max_horizontal(0, price_dist_cost15)
+
+ return stock_df.with_columns(floating_proxy.alias('floating_chip_proxy'))
+
+
+class CostSupportChangeOperator(StockWiseOperator):
+ """成本支撑强度变化算子"""
+
+ def __init__(self, n: int = 1):
+ config = OperatorConfig(
+ name=f"cost_support_change_{n}",
+ description=f"{n}日成本支撑强度变化",
+ required_columns=['cost_15pct'],
+ output_columns=[f'cost_support_15pct_change_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成本支撑强度变化"""
+ # 计算百分比变化
+ pct_change = pl.col('cost_15pct').pct_change(self.n) * 100
+
+ return stock_df.with_columns(pct_change.alias(f'cost_support_15pct_change_{self.n}'))
+
+
+class WinnerPriceZoneOperator(StockWiseOperator):
+ """获利盘压力/支撑区分类算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="winner_price_zone",
+ description="获利盘压力/支撑区分类",
+ required_columns=['close', 'cost_85pct', 'cost_15pct', 'cost_50pct', 'winner_rate'],
+ output_columns=['cat_winner_price_zone'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算获利盘压力/支撑区分类"""
+ # 定义条件
+ conditions = [
+ # 1: 高风险区 (高位 & 高获利盘)
+ (pl.col('close') > pl.col('cost_85pct')) & (pl.col('winner_rate') > 0.8),
+ # 2: 低潜力区 (低位 & 低获利盘)
+ (pl.col('close') < pl.col('cost_15pct')) & (pl.col('winner_rate') < 0.2),
+ # 3: 中上获利区 (中高位 & 多数获利)
+ (pl.col('close') > pl.col('cost_50pct')) & (pl.col('winner_rate') > 0.5),
+ # 4: 中下亏损区 (中低位 & 多数亏损)
+ (pl.col('close') < pl.col('cost_50pct')) & (pl.col('winner_rate') < 0.5),
+ ]
+
+ choices = [1, 2, 3, 4]
+
+ # 使用select函数进行分类
+ zone_classification = pl.select(
+ conditions=conditions,
+ choices=choices,
+ default=0 # 0: 其他情况
+ )
+
+ return stock_df.with_columns(zone_classification.alias('cat_winner_price_zone'))
+
+
+class FlowChipConsistencyOperator(StockWiseOperator):
+ """主力行为与筹码结构一致性算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="flow_chip_consistency",
+ description="主力行为与筹码结构一致性",
+ required_columns=['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol',
+ 'close', 'cost_15pct', 'cost_50pct'],
+ output_columns=['flow_chip_consistency'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算主力行为与筹码结构一致性"""
+ # 计算大单净买入量
+ lg_elg_net_buy_vol = (
+ pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')
+ )
+
+ # 判断价格是否接近下方筹码密集区
+ price_near_low_support = (
+ (pl.col('close') > pl.col('cost_15pct')) &
+ (pl.col('close') < pl.col('cost_50pct'))
+ )
+
+ # 计算一致性:主力净买入 * 价格位置指示器
+ consistency = lg_elg_net_buy_vol * price_near_low_support.cast(int)
+
+ return stock_df.with_columns(consistency.alias('flow_chip_consistency'))
+
+
+class ProfitTakingVsAbsorptionOperator(StockWiseOperator):
+ """获利了结压力/承接盘强度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="profit_taking_vs_absorb",
+ description="获利了结压力vs承接盘强度",
+ required_columns=['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol',
+ 'winner_rate'],
+ output_columns=['profit_taking_vs_absorb'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算获利了结压力vs承接盘强度"""
+ # 计算大单净买入量
+ lg_elg_net_buy_vol = (
+ pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')
+ )
+
+ # 判断高获利盘
+ high_winner_rate_flag = (pl.col('winner_rate') > 0.7).cast(int)
+
+ # 计算因子:主力净买入 * 高获利盘指示器
+ # 正值表示高获利盘下主力仍在买入(承接),负值表示主力在卖出(了结)
+ factor = lg_elg_net_buy_vol * high_winner_rate_flag
+
+ return stock_df.with_columns(factor.alias('profit_taking_vs_absorb'))
+
+
+class ChipConcentrationChangeOperator(StockWiseOperator):
+ """筹码集中度变化算子"""
+
+ def __init__(self, n: int = 20):
+ config = OperatorConfig(
+ name=f"chip_conc_std_{n}",
+ description=f"{n}日筹码集中度变化",
+ required_columns=['cost_85pct', 'cost_15pct', 'weight_avg'],
+ output_columns=[f'chip_conc_std_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算筹码集中度变化"""
+ epsilon = 1e-8
+
+ # 计算成本区间标准化值
+ cost_range_norm = (pl.col('cost_85pct') - pl.col('cost_15pct')) / (pl.col('weight_avg') + epsilon)
+
+ # 计算滚动标准差
+ conc_std = cost_range_norm.rolling_std(window=self.n)
+
+ return stock_df.with_columns(conc_std.alias(f'chip_conc_std_{self.n}'))
+
+
+class CostBreakoutConfirmationOperator(StockWiseOperator):
+ """成本突破确认算子"""
+
+ def __init__(self, m: int = 5):
+ config = OperatorConfig(
+ name=f"cost_break_confirm_cnt_{m}",
+ description=f"{m}日成本突破确认",
+ required_columns=['close', 'cost_85pct', 'cost_15pct',
+ 'buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol'],
+ output_columns=[f'cost_break_confirm_cnt_{m}'],
+ parameters={'m': m}
+ )
+ super().__init__(config)
+ self.m = m
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成本突破确认"""
+ # 获取前一日的成本位
+ prev_cost_85 = pl.col('cost_85pct').shift(1)
+ prev_cost_15 = pl.col('cost_15pct').shift(1)
+
+ # 判断突破
+ break_up = pl.col('close') > prev_cost_85
+ break_down = pl.col('close') < prev_cost_15
+
+ # 计算大单净流
+ net_lg_flow_vol = (
+ pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')
+ )
+
+ # 判断确认信号
+ confirm_up = break_up & (net_lg_flow_vol > 0)
+ confirm_down = break_down & (net_lg_flow_vol < 0)
+
+ # 计算净确认信号
+ net_confirm = confirm_up.cast(int) - confirm_down.cast(int)
+
+ # 计算m日累计
+ confirm_cnt = net_confirm.rolling_sum(window=self.m)
+
+ return stock_df.with_columns(confirm_cnt.alias(f'cost_break_confirm_cnt_{self.m}'))
+
+
+# 筹码分布因子集合
+CHIP_DISTRIBUTION_OPERATORS = [
+ ChipConcentrationOperator(),
+ ChipSkewnessOperator(),
+ FloatingChipProxyOperator(),
+ CostSupportChangeOperator(),
+ WinnerPriceZoneOperator(),
+ FlowChipConsistencyOperator(),
+ ProfitTakingVsAbsorptionOperator(),
+ ChipConcentrationChangeOperator(),
+ CostBreakoutConfirmationOperator(),
+]
+
+
+def apply_chip_distribution_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """
+ 应用所有筹码分布因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ operators: 要应用的算子列表,如果为None则使用默认列表
+
+ Returns:
+ 添加了筹码分布因子的DataFrame
+ """
+ if operators is None:
+ operators = CHIP_DISTRIBUTION_OPERATORS
+
+ result_df = df
+ for operator in operators:
+ result_df = operator(result_df)
+
+ return result_df
diff --git a/main/factor/polars_complex_factors.py b/main/factor/polars_complex_factors.py
new file mode 100644
index 0000000..d07c878
--- /dev/null
+++ b/main/factor/polars_complex_factors.py
@@ -0,0 +1,648 @@
+"""
+复杂组合因子 - 使用Polars实现
+包含复杂的组合因子和高级因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, DateWiseOperator, OperatorConfig
+
+
+# 时间序列因子
+class LargeFlowMomentumCorrelationOperator(StockWiseOperator):
+ """大单资金流与价格动量相关性算子"""
+
+ def __init__(self, n: int = 20, m: int = 60):
+ config = OperatorConfig(
+ name=f"lg_flow_mom_corr_{n}_{m}",
+ description=f"{n}日大单资金流与{m}日价格动量相关性",
+ required_columns=['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol',
+ 'close', 'vol'],
+ output_columns=[f'lg_flow_mom_corr_{n}_{m}'],
+ parameters={'n': n, 'm': m}
+ )
+ super().__init__(config)
+ self.n = n
+ self.m = m
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算大单资金流与价格动量相关性"""
+ # 计算大单净额
+ net_lg_flow_val = (
+ (pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')) * pl.col('close')
+ )
+
+ # 计算滚动净大单流
+ rolling_net_lg_flow = net_lg_flow_val.rolling_sum(window=self.n)
+
+ # 计算价格动量
+ price_mom = pl.col('close').pct_change(self.n)
+
+ # 计算相关性
+ correlation = rolling_net_lg_flow.rolling_corr(price_mom, window=self.m)
+
+ return stock_df.with_columns(
+ correlation.alias(f'lg_flow_mom_corr_{self.n}_{self.m}')
+ )
+
+
+class LargeBuyConsolidationOperator(StockWiseOperator):
+ """大单买入盘整期算子"""
+
+ def __init__(self, n: int = 20, vol_quantile: float = 0.2):
+ config = OperatorConfig(
+ name=f"lg_buy_consolidation_{n}",
+ description=f"{n}日大单买入盘整期",
+ required_columns=['close', 'buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol',
+ 'sell_elg_vol', 'vol'],
+ output_columns=[f'lg_buy_consolidation_{n}'],
+ parameters={'n': n, 'vol_quantile': vol_quantile}
+ )
+ super().__init__(config)
+ self.n = n
+ self.vol_quantile = vol_quantile
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算大单买入盘整期"""
+ epsilon = 1e-8
+
+ # 计算收盘价滚动标准差
+ rolling_std = pl.col('close').rolling_std(window=self.n)
+
+ # 计算大单净流比率
+ net_lg_flow_ratio = (
+ (pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')) /
+ (pl.col('vol') + epsilon)
+ )
+
+ # 计算滚动均值
+ rolling_mean_ratio = net_lg_flow_ratio.rolling_mean(window=self.n)
+
+ return stock_df.with_columns(
+ rolling_mean_ratio.alias(f'lg_buy_consolidation_{self.n}')
+ )
+
+
+class IntradayLargeFlowCorrelationOperator(StockWiseOperator):
+ """日内趋势与大单流相关性算子"""
+
+ def __init__(self, n: int = 20):
+ config = OperatorConfig(
+ name=f"intraday_lg_flow_corr_{n}",
+ description=f"{n}日日内趋势与大单流相关性",
+ required_columns=['high', 'low', 'close', 'buy_lg_vol', 'buy_elg_vol',
+ 'sell_lg_vol', 'sell_elg_vol'],
+ output_columns=[f'intraday_lg_flow_corr_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算日内趋势与大单流相关性"""
+ # 这是一个复杂的因子,简化处理
+ # 实际实现需要更复杂的日内数据
+ placeholder = pl.lit(None).cast(float)
+
+ return stock_df.with_columns(
+ placeholder.alias(f'intraday_lg_flow_corr_{self.n}')
+ )
+
+
+class ProfitPressureOperator(StockWiseOperator):
+ """获利压力指数算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="profit_pressure",
+ description="获利压力指数",
+ required_columns=['close', 'cost_85pct', 'cost_95pct', 'winner_rate'],
+ output_columns=['profit_pressure'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算获利压力指数"""
+ epsilon = 1e-8
+
+ # 计算盈利幅度
+ profit_margin_85 = (pl.col('close') / (pl.col('cost_85pct') + epsilon)) - 1
+ profit_margin_95 = (pl.col('close') / (pl.col('cost_95pct') + epsilon)) - 1
+
+ # 计算压力指数
+ pressure = pl.col('winner_rate') * 0.5 * (profit_margin_85 + profit_margin_95)
+
+ return stock_df.with_columns(pressure.alias('profit_pressure'))
+
+
+class UnderwaterResistanceOperator(StockWiseOperator):
+ """套牢盘阻力算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="underwater_resistance",
+ description="套牢盘阻力",
+ required_columns=['close', 'winner_rate', 'cost_15pct'],
+ output_columns=['underwater_resistance'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算套牢盘阻力"""
+ epsilon = 1e-8
+
+ # 计算套牢比例
+ underwater_ratio = 1.0 - pl.col('winner_rate')
+
+ # 计算与成本的距离
+ dist_to_cost_15 = pl.max_horizontal(0, pl.col('cost_15pct') - pl.col('close')) / (pl.col('close') + epsilon)
+
+ # 计算阻力
+ resistance = underwater_ratio * dist_to_cost_15
+
+ return stock_df.with_columns(resistance.alias('underwater_resistance'))
+
+
+class ProfitDecayOperator(StockWiseOperator):
+ """盈利预期衰减算子"""
+
+ def __init__(self, n: int = 20):
+ config = OperatorConfig(
+ name=f"profit_decay_{n}",
+ description=f"{n}日盈利预期衰减",
+ required_columns=['close', 'winner_rate'],
+ output_columns=[f'profit_decay_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算盈利预期衰减"""
+ # 计算n日收益率
+ ret_n = pl.col('close').pct_change(self.n)
+
+ # 计算winner_rate变化
+ winner_rate_change = pl.col('winner_rate').diff(self.n)
+
+ # 计算衰减因子
+ decay = ret_n / (winner_rate_change + 1e-8)
+
+ return stock_df.with_columns(decay.alias(f'profit_decay_{self.n}'))
+
+
+class PullbackStrongOperator(StockWiseOperator):
+ """强势股回调深度算子"""
+
+ def __init__(self, n: int = 20, m: int = 20, gain_thresh: float = 0.2):
+ config = OperatorConfig(
+ name=f"pullback_strong_{n}_{m}",
+ description=f"{n}日{m}期强势股回调深度",
+ required_columns=['high', 'close'],
+ output_columns=[f'pullback_strong_{n}_{m}'],
+ parameters={'n': n, 'm': m, 'gain_thresh': gain_thresh}
+ )
+ super().__init__(config)
+ self.n = n
+ self.m = m
+ self.gain_thresh = gain_thresh
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算强势股回调深度"""
+ # 计算n日最高价
+ high_n = pl.col('high').rolling_max(window=self.n)
+
+ # 计算回调深度
+ pullback_depth = (high_n - pl.col('close')) / high_n
+
+ # 计算近期涨幅
+ recent_gain = (pl.col('close') / pl.col('close').shift(self.m)) - 1
+
+ # 计算回调因子
+ pullback_factor = pullback_depth / (recent_gain + 1e-8)
+
+ return stock_df.with_columns(pullback_factor.alias(f'pullback_strong_{self.n}_{self.m}'))
+
+
+class HurstExponentFlowOperator(StockWiseOperator):
+ """资金流Hurst指数算子"""
+
+ def __init__(self, n: int = 60, flow_col: str = 'net_mf_vol'):
+ config = OperatorConfig(
+ name=f"hurst_{flow_col}_{n}",
+ description=f"{n}日{flow_col}Hurst指数",
+ required_columns=[flow_col],
+ output_columns=[f'hurst_{flow_col}_{n}'],
+ parameters={'n': n, 'flow_col': flow_col}
+ )
+ super().__init__(config)
+ self.n = n
+ self.flow_col = flow_col
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算Hurst指数"""
+ # Hurst指数计算复杂,这里使用占位符
+ # 实际实现需要专门的Hurst指数计算库
+ placeholder = pl.lit(None).cast(float)
+
+ return stock_df.with_columns(
+ placeholder.alias(f'hurst_{self.flow_col}_{self.n}')
+ )
+
+
+class VolWeightedHistoricalPositionOperator(StockWiseOperator):
+ """成交量加权历史位置算子"""
+
+ def __init__(self, n: int = 20):
+ config = OperatorConfig(
+ name=f"vol_wgt_hist_pos_{n}",
+ description=f"{n}日成交量加权历史位置",
+ required_columns=['close', 'his_high', 'his_low', 'vol'],
+ output_columns=[f'vol_wgt_hist_pos_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量加权历史位置"""
+ # 计算历史位置
+ hist_pos = (pl.col('close') - pl.col('his_low')) / (pl.col('his_high') - pl.col('his_low'))
+ hist_pos = hist_pos.clip(0, 1)
+
+ # 计算成交量相对强度
+ rolling_mean_vol = pl.col('vol').rolling_mean(window=self.n)
+ vol_rel_strength = pl.col('vol') / rolling_mean_vol
+
+ # 计算加权位置
+ weighted_pos = hist_pos * vol_rel_strength
+
+ return stock_df.with_columns(weighted_pos.alias(f'vol_wgt_hist_pos_{self.n}'))
+
+
+# 横截面因子
+class CrossSectionalRankOperator(DateWiseOperator):
+ """横截面排名算子"""
+
+ def __init__(self, column: str, ascending: bool = True):
+ config = OperatorConfig(
+ name=f"cs_rank_{column}",
+ description=f"{column}横截面排名",
+ required_columns=[column],
+ output_columns=[f'cs_rank_{column}'],
+ parameters={'column': column, 'ascending': ascending}
+ )
+ super().__init__(config)
+ self.column = column
+ self.ascending = ascending
+
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算横截面排名"""
+ # 计算排名
+ rank_col = pl.col(self.column).rank(method='dense', descending=not self.ascending)
+
+ # 转换为百分比排名
+ pct_rank = rank_col / rank_col.max()
+
+ return date_df.with_columns(pct_rank.alias(f'cs_rank_{self.column}'))
+
+
+class CrossSectionalNetLargeFlowRankOperator(DateWiseOperator):
+ """横截面大单净额排名算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="cs_rank_net_lg_flow_val",
+ description="横截面大单净额排名",
+ required_columns=['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol', 'close'],
+ output_columns=['cs_rank_net_lg_flow_val'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算横截面大单净额排名"""
+ # 计算大单净额
+ net_lg_flow_val = (
+ (pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')) * pl.col('close')
+ )
+
+ # 计算排名
+ rank_col = net_lg_flow_val.rank(method='dense', descending=True)
+ pct_rank = rank_col / rank_col.max()
+
+ return date_df.with_columns(pct_rank.alias('cs_rank_net_lg_flow_val'))
+
+
+class CrossSectionalFlowDivergenceRankOperator(DateWiseOperator):
+ """横截面流向背离度排名算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="cs_rank_flow_divergence",
+ description="横截面流向背离度排名",
+ required_columns=['buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'buy_elg_vol',
+ 'sell_lg_vol', 'sell_elg_vol', 'vol'],
+ output_columns=['cs_rank_flow_divergence'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算横截面流向背离度排名"""
+ epsilon = 1e-8
+
+ # 计算大单比率
+ lg_ratio = (
+ (pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')) /
+ (pl.col('vol') + epsilon)
+ )
+
+ # 计算小单比率
+ sm_ratio = (pl.col('buy_sm_vol') - pl.col('sell_sm_vol')) / (pl.col('vol') + epsilon)
+
+ # 计算背离度
+ divergence = lg_ratio - sm_ratio
+
+ # 计算排名
+ rank_col = divergence.rank(method='dense', descending=True)
+ pct_rank = rank_col / rank_col.max()
+
+ return date_df.with_columns(pct_rank.alias('cs_rank_flow_divergence'))
+
+
+class CrossSectionalRelativeProfitMarginRankOperator(DateWiseOperator):
+ """横截面相对盈利幅度排名算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="cs_rank_rel_profit_margin",
+ description="横截面相对盈利幅度排名",
+ required_columns=['close', 'weight_avg'],
+ output_columns=['cs_rank_rel_profit_margin'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算横截面相对盈利幅度排名"""
+ # 计算盈利幅度
+ profit_margin = (pl.col('close') - pl.col('weight_avg')) / pl.col('close')
+
+ # 计算排名
+ rank_col = profit_margin.rank(method='dense', descending=True)
+ pct_rank = rank_col / rank_col.max()
+
+ return date_df.with_columns(pct_rank.alias('cs_rank_rel_profit_margin'))
+
+
+class CrossSectionalCostBreadthRankOperator(DateWiseOperator):
+ """横截面成本分布宽度排名算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="cs_rank_cost_breadth",
+ description="横截面成本分布宽度排名",
+ required_columns=['cost_85pct', 'cost_15pct', 'weight_avg'],
+ output_columns=['cs_rank_cost_breadth'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算横截面成本分布宽度排名"""
+ epsilon = 1e-8
+
+ # 计算成本宽度
+ cost_breadth = (pl.col('cost_85pct') - pl.col('cost_15pct')) / (pl.col('weight_avg') + epsilon)
+
+ # 计算排名
+ rank_col = cost_breadth.rank(method='dense', descending=True)
+ pct_rank = rank_col / rank_col.max()
+
+ return date_df.with_columns(pct_rank.alias('cs_rank_cost_breadth'))
+
+
+class CrossSectionalWinnerRateRankOperator(DateWiseOperator):
+ """横截面获利盘比例排名算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="cs_rank_winner_rate",
+ description="横截面获利盘比例排名",
+ required_columns=['winner_rate'],
+ output_columns=['cs_rank_winner_rate'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算横截面获利盘比例排名"""
+ # 计算排名
+ rank_col = pl.col('winner_rate').rank(method='dense', descending=True)
+ pct_rank = rank_col / rank_col.max()
+
+ return date_df.with_columns(pct_rank.alias('cs_rank_winner_rate'))
+
+
+class CrossSectionalVolumeRatioRankOperator(DateWiseOperator):
+ """横截面量比排名算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="cs_rank_volume_ratio",
+ description="横截面量比排名",
+ required_columns=['volume_ratio'],
+ output_columns=['cs_rank_volume_ratio'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_date(self, date_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算横截面量比排名"""
+ # 计算排名
+ rank_col = pl.col('volume_ratio').rank(method='dense', descending=True)
+ pct_rank = rank_col / rank_col.max()
+
+ return date_df.with_columns(pct_rank.alias('cs_rank_volume_ratio'))
+
+
+# 复杂组合因子
+class ComplexFactorDEAPOperator(StockWiseOperator):
+ """DEAP复杂因子算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="complex_factor_deap_1",
+ description="DEAP复杂组合因子",
+ required_columns=['pullback_strong_20_20', 'log_close', 'industry_return_5',
+ 'vol_adj_roc_20', 'vol_drop_profit_cnt_5', 'nonlinear_mv_volume',
+ 'alpha_007', 'lg_buy_consolidation_20', 'net_mf_vol', 'std_return_5',
+ 'arbr', 'industry_act_factor5', 'industry_act_factor1', 'low_cost_dev',
+ 'mv_weighted_turnover', 'act_factor4', 'vol', 'lg_elg_buy_prop',
+ 'intraday_lg_flow_corr_20'],
+ output_columns=['complex_factor_deap_1'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算DEAP复杂因子"""
+ try:
+ # 安全除法函数
+ def safe_divide(a, b, default_val=0):
+ return pl.when(b.abs() > 1e-8).then(a / b).otherwise(default_val)
+
+ # 计算组件D
+ d_term1_div = safe_divide(pl.col('log_close'), pl.col('industry_return_5'))
+ d_term1 = pl.col('pullback_strong_20_20') * d_term1_div
+
+ d_term2_sub = pl.col('nonlinear_mv_volume') - pl.col('alpha_007')
+ d_term2_add = pl.col('vol_adj_roc_20') + pl.col('vol_drop_profit_cnt_5')
+ d_term2 = safe_divide(d_term2_add, d_term2_sub)
+
+ temp_d = d_term1 - d_term2
+
+ # 计算组件A
+ a_term1 = temp_d * pl.col('lg_buy_consolidation_20')
+ a_term2 = a_term1 + pl.col('lg_buy_consolidation_20')
+ temp_a = a_term2 + pl.col('pullback_strong_20_20')
+
+ # 计算组件F
+ f_term1 = pl.col('net_mf_vol') + pl.col('std_return_5')
+ f_term2 = pl.col('arbr') - pl.col('industry_act_factor5')
+ temp_f = f_term1 * f_term2
+
+ # 计算组件H
+ h_term1 = pl.col('industry_act_factor1') + pl.col('low_cost_dev')
+ h_term2 = pl.col('mv_weighted_turnover') * pl.col('act_factor4')
+ temp_h = h_term1 + h_term2
+
+ # 计算组件B
+ b_term1 = temp_f + pl.col('vol')
+ b_term2 = b_term1 + temp_h
+ temp_b = safe_divide(b_term2, pl.col('lg_elg_buy_prop'))
+
+ # 计算组件C
+ c_term1 = safe_divide(
+ pl.col('intraday_lg_flow_corr_20').fill_null(0),
+ pl.col('lg_elg_buy_prop')
+ )
+ temp_c = safe_divide(c_term1, pl.col('lg_elg_buy_prop'))
+
+ # 计算最终因子
+ final_term1 = safe_divide(temp_a, temp_b)
+ complex_factor = final_term1 - temp_c
+
+ return stock_df.with_columns(complex_factor.alias('complex_factor_deap_1'))
+
+ except Exception as e:
+ # 如果计算失败,填充NaN
+ print(f"Error calculating complex_factor_deap_1: {e}")
+ return stock_df.with_columns(pl.lit(None).cast(float).alias('complex_factor_deap_1'))
+
+
+# 因子集合
+COMPLEX_OPERATORS = [
+ LargeFlowMomentumCorrelationOperator(),
+ LargeBuyConsolidationOperator(),
+ IntradayLargeFlowCorrelationOperator(),
+ ProfitPressureOperator(),
+ UnderwaterResistanceOperator(),
+ ProfitDecayOperator(),
+ PullbackStrongOperator(),
+ HurstExponentFlowOperator(),
+ VolWeightedHistoricalPositionOperator(),
+ CrossSectionalRankOperator('close'),
+ CrossSectionalNetLargeFlowRankOperator(),
+ CrossSectionalFlowDivergenceRankOperator(),
+ CrossSectionalRelativeProfitMarginRankOperator(),
+ CrossSectionalCostBreadthRankOperator(),
+ CrossSectionalWinnerRateRankOperator(),
+ CrossSectionalVolumeRatioRankOperator(),
+ ComplexFactorDEAPOperator(),
+]
+
+
+def apply_complex_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """
+ 应用所有复杂组合因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ operators: 要应用的算子列表,如果为None则使用默认列表
+
+ Returns:
+ 添加了复杂组合因子的DataFrame
+ """
+ if operators is None:
+ operators = COMPLEX_OPERATORS
+
+ result_df = df
+ for operator in operators:
+ result_df = operator(result_df)
+
+ return result_df
+
+
+# 主应用函数
+def apply_all_factors(df: pl.DataFrame,
+ factor_categories: List[str] = None) -> pl.DataFrame:
+ """
+ 应用所有类别的因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ factor_categories: 要应用的因子类别列表,如果为None则应用所有类别
+
+ Returns:
+ 添加了所有因子的DataFrame
+ """
+ if factor_categories is None:
+ factor_categories = ['money_flow', 'chip', 'volatility', 'volume',
+ 'technical', 'sentiment', 'momentum', 'complex']
+
+ result_df = df
+
+ # 导入所有因子模块
+ from polars_money_flow_factors import apply_money_flow_factors
+ from polars_chip_factors import apply_chip_distribution_factors
+ from polars_volatility_factors import apply_volatility_factors
+ from polars_volume_factors import apply_volume_factors
+ from polars_technical_factors import apply_technical_factors
+ from polars_sentiment_factors import apply_sentiment_factors
+ from polars_momentum_factors import apply_momentum_factors
+
+ # 应用各类因子
+ if 'money_flow' in factor_categories:
+ result_df = apply_money_flow_factors(result_df)
+
+ if 'chip' in factor_categories:
+ result_df = apply_chip_distribution_factors(result_df)
+
+ if 'volatility' in factor_categories:
+ result_df = apply_volatility_factors(result_df)
+
+ if 'volume' in factor_categories:
+ result_df = apply_volume_factors(result_df)
+
+ if 'technical' in factor_categories:
+ result_df = apply_technical_factors(result_df)
+
+ if 'sentiment' in factor_categories:
+ result_df = apply_sentiment_factors(result_df)
+
+ if 'momentum' in factor_categories:
+ result_df = apply_momentum_factors(result_df)
+
+ if 'complex' in factor_categories:
+ result_df = apply_complex_factors(result_df)
+
+ return result_df
diff --git a/main/factor/polars_factors.py b/main/factor/polars_factors.py
new file mode 100644
index 0000000..816f476
--- /dev/null
+++ b/main/factor/polars_factors.py
@@ -0,0 +1,237 @@
+"""
+Polars因子主入口 - 整合所有Polars-based因子计算
+提供统一的接口来应用所有类别的因子
+"""
+
+import polars as pl
+from typing import Dict, List, Optional, Any
+import logging
+
+# 配置日志
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+# 因子类别映射
+FACTOR_CATEGORIES = {
+ 'money_flow': '资金流因子',
+ 'chip': '筹码分布因子',
+ 'volatility': '波动率因子',
+ 'volume': '成交量因子',
+ 'technical': '技术指标因子',
+ 'sentiment': '情绪因子',
+ 'momentum': '动量因子',
+ 'complex': '复杂组合因子'
+}
+
+
+def apply_money_flow_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用资金流因子"""
+ try:
+ from polars_money_flow_factors import apply_money_flow_factors as _apply_money_flow
+ return _apply_money_flow(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入资金流因子模块: {e}")
+ return df
+
+
+def apply_chip_distribution_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用筹码分布因子"""
+ try:
+ from polars_chip_factors import apply_chip_distribution_factors as _apply_chip
+ return _apply_chip(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入筹码分布因子模块: {e}")
+ return df
+
+
+def apply_volatility_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用波动率因子"""
+ try:
+ from polars_volatility_factors import apply_volatility_factors as _apply_volatility
+ return _apply_volatility(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入波动率因子模块: {e}")
+ return df
+
+
+def apply_volume_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用成交量因子"""
+ try:
+ from polars_volume_factors import apply_volume_factors as _apply_volume
+ return _apply_volume(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入成交量因子模块: {e}")
+ return df
+
+
+def apply_technical_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用技术指标因子"""
+ try:
+ from polars_technical_factors import apply_technical_factors as _apply_technical
+ return _apply_technical(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入技术指标因子模块: {e}")
+ return df
+
+
+def apply_sentiment_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用情绪因子"""
+ try:
+ from polars_sentiment_factors import apply_sentiment_factors as _apply_sentiment
+ return _apply_sentiment(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入情绪因子模块: {e}")
+ return df
+
+
+def apply_momentum_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用动量因子"""
+ try:
+ from polars_momentum_factors import apply_momentum_factors as _apply_momentum
+ return _apply_momentum(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入动量因子模块: {e}")
+ return df
+
+
+def apply_complex_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """应用复杂组合因子"""
+ try:
+ from polars_complex_factors import apply_complex_factors as _apply_complex
+ return _apply_complex(df, operators)
+ except ImportError as e:
+ logger.warning(f"无法导入复杂组合因子模块: {e}")
+ return df
+
+
+def apply_all_factors(df: pl.DataFrame,
+ factor_categories: List[str] = None,
+ exclude_categories: List[str] = None) -> pl.DataFrame:
+ """
+ 应用所有类别的因子
+
+ Args:
+ df: 输入的Polars DataFrame,必须包含必需的列
+ factor_categories: 要应用的因子类别列表,如果为None则应用所有类别
+ exclude_categories: 要排除的因子类别列表
+
+ Returns:
+ 添加了所有因子的DataFrame
+ """
+ if factor_categories is None:
+ factor_categories = list(FACTOR_CATEGORIES.keys())
+
+ if exclude_categories:
+ factor_categories = [cat for cat in factor_categories if cat not in exclude_categories]
+
+ logger.info(f"开始应用因子类别: {factor_categories}")
+
+ result_df = df
+ total_factors = 0
+
+ # 因子类别到函数的映射
+ factor_functions = {
+ 'money_flow': apply_money_flow_factors,
+ 'chip': apply_chip_distribution_factors,
+ 'volatility': apply_volatility_factors,
+ 'volume': apply_volume_factors,
+ 'technical': apply_technical_factors,
+ 'sentiment': apply_sentiment_factors,
+ 'momentum': apply_momentum_factors,
+ 'complex': apply_complex_factors
+ }
+
+ for category in factor_categories:
+ if category not in factor_functions:
+ logger.warning(f"未知的因子类别: {category}")
+ continue
+
+ logger.info(f"应用{FACTOR_CATEGORIES[category]}...")
+
+ try:
+ before_cols = len(result_df.columns)
+ result_df = factor_functions[category](result_df)
+ after_cols = len(result_df.columns)
+ new_factors = after_cols - before_cols
+
+ logger.info(f"{FACTOR_CATEGORIES[category]}应用完成,新增{new_factors}个因子")
+ total_factors += new_factors
+
+ except Exception as e:
+ logger.error(f"应用{FACTOR_CATEGORIES[category]}时出错: {e}")
+ continue
+
+ logger.info(f"因子应用完成,总共新增{total_factors}个因子")
+ return result_df
+
+
+def get_factor_info() -> Dict[str, Any]:
+ """
+ 获取因子信息
+
+ Returns:
+ 包含因子类别信息的字典
+ """
+ return {
+ 'categories': FACTOR_CATEGORIES,
+ 'total_categories': len(FACTOR_CATEGORIES),
+ 'category_descriptions': list(FACTOR_CATEGORIES.values())
+ }
+
+
+def validate_required_columns(df: pl.DataFrame, factor_categories: List[str] = None) -> Dict[str, List[str]]:
+ """
+ 验证DataFrame是否包含必需的列
+
+ Args:
+ df: 输入的Polars DataFrame
+ factor_categories: 要验证的因子类别列表
+
+ Returns:
+ 包含缺失列信息的字典
+ """
+ if factor_categories is None:
+ factor_categories = list(FACTOR_CATEGORIES.keys())
+
+ missing_columns = {}
+
+ # 基础必需列
+ base_required = ['ts_code', 'trade_date']
+ missing_base = [col for col in base_required if col not in df.columns]
+ if missing_base:
+ missing_columns['base'] = missing_base
+
+ # 各因子类别的必需列
+ category_requirements = {
+ 'money_flow': ['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol', 'vol'],
+ 'chip': ['cost_95pct', 'cost_85pct', 'cost_50pct', 'cost_15pct', 'cost_5pct',
+ 'winner_rate', 'weight_avg', 'close'],
+ 'volatility': ['pct_chg'],
+ 'volume': ['vol', 'turnover_rate', 'volume_ratio', 'amount'],
+ 'technical': ['open', 'high', 'low', 'close', 'vol'],
+ 'sentiment': ['pct_chg', 'vol', 'volume_ratio'],
+ 'momentum': ['close', 'turnover_rate'],
+ 'complex': ['close', 'vol', 'pct_chg', 'turnover_rate', 'winner_rate']
+ }
+
+ for category in factor_categories:
+ if category in category_requirements:
+ required_cols = category_requirements[category]
+ missing_cols = [col for col in required_cols if col not in df.columns]
+ if missing_cols:
+ missing_columns[category] = missing_cols
+
+ return missing_columns
+
+
+# 向后兼容的函数名
+apply_factors = apply_all_factors
+
+
+if __name__ == "__main__":
+ # 测试代码
+ print("Polars因子系统已加载")
+ print("可用的因子类别:")
+ for key, description in FACTOR_CATEGORIES.items():
+ print(f" {key}: {description}")
diff --git a/main/factor/polars_momentum_factors.py b/main/factor/polars_momentum_factors.py
new file mode 100644
index 0000000..a2d7179
--- /dev/null
+++ b/main/factor/polars_momentum_factors.py
@@ -0,0 +1,428 @@
+"""
+动量因子 - 使用Polars实现
+包含动量、趋势、均线等相关因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, OperatorConfig
+from scipy.stats import linregress
+
+
+class PriceMinusDeductionPriceOperator(StockWiseOperator):
+ """价格减抵扣价算子"""
+
+ def __init__(self, n: int = 10):
+ config = OperatorConfig(
+ name=f"price_minus_deduction_price_{n}",
+ description=f"{n}日价格减抵扣价",
+ required_columns=['close'],
+ output_columns=[f'price_minus_deduction_price_{n}'],
+ parameters={'n': n}
+ )
+ 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}'))
+
+
+class PriceDeductionPriceDiffRatioToSMAOperator(StockWiseOperator):
+ """价格抵扣价差值相对SMA比率算子"""
+
+ def __init__(self, n: int = 10):
+ config = OperatorConfig(
+ name=f"price_deduction_price_diff_ratio_to_sma_{n}",
+ description=f"{n}日价格抵扣价差值相对SMA比率",
+ required_columns=['close'],
+ output_columns=[f'price_deduction_price_diff_ratio_to_sma_{n}'],
+ parameters={'n': n}
+ )
+ 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
+
+ # 计算比率 (处理除零)
+ ratio = diff / (sma + 1e-8)
+
+ return stock_df.with_columns(ratio.alias(f'price_deduction_price_diff_ratio_to_sma_{self.n}'))
+
+
+class CatPriceVsSmaVsDeductionPriceOperator(StockWiseOperator):
+ """价格vsSMAvs抵扣价分类算子"""
+
+ def __init__(self, n: int = 10):
+ config = OperatorConfig(
+ name=f"cat_price_vs_sma_vs_deduction_price_{n}",
+ description=f"{n}日价格vsSMAvs抵扣价分类",
+ required_columns=['close'],
+ output_columns=[f'cat_price_vs_sma_vs_deduction_price_{n}'],
+ parameters={'n': n}
+ )
+ 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)
+
+ # 抵扣价
+ deduction_price = pl.col('close').shift(self.n - 1)
+
+ # 定义条件
+ 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}')
+ )
+
+
+class VolatilitySlopeOperator(StockWiseOperator):
+ """波动率斜率算子"""
+
+ def __init__(self, long_window: int = 20, short_window: int = 5):
+ config = OperatorConfig(
+ name=f"volatility_slope_{long_window}_{short_window}",
+ description=f"{long_window}日波动率{short_window}日斜率",
+ required_columns=['pct_chg'],
+ output_columns=[f'volatility_slope_{long_window}_{short_window}'],
+ parameters={'long_window': long_window, 'short_window': short_window}
+ )
+ super().__init__(config)
+ 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 calculate_slope(series):
+ if len(series) < 2:
+ return 0
+ x = np.arange(len(series))
+ slope, _, _, _, _ = linregress(x, series)
+ return slope
+
+ # 计算斜率
+ 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}')
+ )
+
+
+class TurnoverRateTrendStrengthOperator(StockWiseOperator):
+ """换手率趋势强度算子"""
+
+ def __init__(self, window: int = 5):
+ config = OperatorConfig(
+ name=f"turnover_trend_strength_{window}",
+ description=f"{window}日换手率趋势强度",
+ required_columns=['turnover_rate'],
+ output_columns=[f'turnover_trend_strength_{window}'],
+ parameters={'window': window}
+ )
+ 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
+
+ # 计算换手率斜率
+ 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}')
+ )
+
+
+class FreeFloatTurnoverSurgeOperator(StockWiseOperator):
+ """自由流通股换手率激增算子"""
+
+ def __init__(self, window: int = 10):
+ config = OperatorConfig(
+ name=f"ff_turnover_surge_{window}",
+ description=f"{window}日自由流通股换手率激增",
+ required_columns=['turnover_rate'],
+ output_columns=[f'ff_turnover_surge_{window}'],
+ parameters={'window': window}
+ )
+ 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}'))
+
+
+class PriceVolumeTrendCoherenceOperator(StockWiseOperator):
+ """价量趋势一致性算子"""
+
+ def __init__(self, price_window: int = 5, volume_window: int = 20):
+ config = OperatorConfig(
+ name=f"price_volume_coherence_{price_window}_{volume_window}",
+ description=f"{price_window}日价格{volume_window}日成交量趋势一致性",
+ required_columns=['close', 'vol'],
+ output_columns=[f'price_volume_coherence_{price_window}_{volume_window}'],
+ parameters={'price_window': price_window, 'volume_window': volume_window}
+ )
+ super().__init__(config)
+ 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)
+
+ price_up = pl.col('close').apply(price_up_ratio)
+
+ # 计算成交量高于均值占比
+ 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}')
+ )
+
+
+class FreeFloatToTotalTurnoverRatioOperator(StockWiseOperator):
+ """自由流通股对总换手率比率算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="ff_to_total_turnover_ratio",
+ description="自由流通股对总换手率比率",
+ required_columns=['turnover_rate'],
+ output_columns=['ff_to_total_turnover_ratio'],
+ parameters={}
+ )
+ 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'))
+
+
+class VarianceOperator(StockWiseOperator):
+ """方差算子"""
+
+ def __init__(self, window: int):
+ config = OperatorConfig(
+ name=f"variance_{window}",
+ description=f"{window}日方差",
+ required_columns=['pct_chg'],
+ output_columns=[f'variance_{window}'],
+ parameters={'window': window}
+ )
+ 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}'))
+
+
+class LimitUpDownOperator(StockWiseOperator):
+ """涨跌停算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="limit_up_down",
+ description="涨跌停因子",
+ required_columns=['close', 'up_limit', 'down_limit'],
+ output_columns=['cat_up_limit', 'cat_down_limit', 'up_limit_count_10d', 'down_limit_count_10d'],
+ parameters={}
+ )
+ 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')
+ ])
+
+
+class ConsecutiveUpLimitOperator(StockWiseOperator):
+ """连续涨停算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="consecutive_up_limit",
+ description="连续涨停天数",
+ required_columns=['cat_up_limit'],
+ output_columns=['consecutive_up_limit'],
+ parameters={}
+ )
+ 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'))
+
+
+class MomentumFactorOperator(StockWiseOperator):
+ """动量因子算子"""
+
+ def __init__(self, alpha: float = 0.5):
+ config = OperatorConfig(
+ name=f"momentum_factor_{alpha}",
+ description=f"动量因子(alpha={alpha})",
+ required_columns=['volume_change_rate', 'turnover_deviation'],
+ output_columns=[f'momentum_factor_{alpha}'],
+ parameters={'alpha': alpha}
+ )
+ 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}'))
+
+
+class ResonanceFactorOperator(StockWiseOperator):
+ """共振因子算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="resonance_factor",
+ description="共振因子",
+ required_columns=['volume_ratio', 'pct_chg'],
+ output_columns=['resonance_factor'],
+ parameters={}
+ )
+ 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'))
+
+
+# 动量因子集合
+MOMENTUM_OPERATORS = [
+ PriceMinusDeductionPriceOperator(),
+ PriceDeductionPriceDiffRatioToSMAOperator(),
+ CatPriceVsSmaVsDeductionPriceOperator(),
+ VolatilitySlopeOperator(),
+ TurnoverRateTrendStrengthOperator(5),
+ FreeFloatTurnoverSurgeOperator(10),
+ PriceVolumeTrendCoherenceOperator(),
+ FreeFloatToTotalTurnoverRatioOperator(),
+ VarianceOperator(20),
+ LimitUpDownOperator(),
+ ConsecutiveUpLimitOperator(),
+ MomentumFactorOperator(),
+ ResonanceFactorOperator(),
+]
+
+
+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)
+
+ return result_df
diff --git a/main/factor/polars_money_flow_factors.py b/main/factor/polars_money_flow_factors.py
new file mode 100644
index 0000000..74dfc86
--- /dev/null
+++ b/main/factor/polars_money_flow_factors.py
@@ -0,0 +1,245 @@
+"""
+资金流因子 - 使用Polars实现
+包含主力资金流、散户资金流等相关因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, OperatorConfig
+
+
+class MoneyFlowIntensityOperator(StockWiseOperator):
+ """主力资金流强度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="money_flow_intensity",
+ description="主力资金流强度",
+ required_columns=['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol', 'vol'],
+ output_columns=['flow_lg_elg_intensity'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算主力资金流强度"""
+ epsilon = 1e-8
+
+ # 计算大单+超大单净买入量
+ lg_elg_net_buy_vol = (
+ pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')
+ )
+
+ # 计算资金流强度
+ flow_intensity = lg_elg_net_buy_vol / (pl.col('vol') + epsilon)
+
+ return stock_df.with_columns(flow_intensity.alias('flow_lg_elg_intensity'))
+
+
+class FlowDivergenceRatioOperator(StockWiseOperator):
+ """散户与主力背离度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="flow_divergence_ratio",
+ description="散户与主力背离度比率",
+ required_columns=['buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'buy_elg_vol',
+ 'sell_lg_vol', 'sell_elg_vol'],
+ output_columns=['flow_divergence_ratio'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算散户与主力背离度比率"""
+ epsilon = 1e-8
+
+ # 计算小单净买入量
+ sm_net_buy_vol = pl.col('buy_sm_vol') - pl.col('sell_sm_vol')
+
+ # 计算大单+超大单净买入量
+ lg_elg_net_buy_vol = (
+ pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')
+ )
+
+ # 计算背离度比率,处理分母为0的情况
+ divergence_ratio = sm_net_buy_vol / (
+ lg_elg_net_buy_vol + pl.when(lg_elg_net_buy_vol == 0).then(epsilon).otherwise(0) + epsilon
+ )
+
+ return stock_df.with_columns(divergence_ratio.alias('flow_divergence_ratio'))
+
+
+class FlowStructureChangeOperator(StockWiseOperator):
+ """资金流结构变动算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="flow_structure_change",
+ description="资金流结构变动",
+ required_columns=['buy_sm_vol', 'buy_lg_vol', 'buy_elg_vol'],
+ output_columns=['flow_struct_buy_change'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算资金流结构变动"""
+ epsilon = 1e-8
+
+ # 计算总买入量
+ total_buy_vol = pl.col('buy_sm_vol') + pl.col('buy_lg_vol') + pl.col('buy_elg_vol')
+
+ # 计算大单+超大单买入占比
+ lg_elg_buy_prop = (pl.col('buy_lg_vol') + pl.col('buy_elg_vol')) / (total_buy_vol + epsilon)
+
+ # 计算1日变化
+ struct_change = lg_elg_buy_prop.diff()
+
+ return stock_df.with_columns(struct_change.alias('flow_struct_buy_change'))
+
+
+class FlowAccelerationOperator(StockWiseOperator):
+ """资金流加速度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="flow_acceleration",
+ description="资金流加速度",
+ required_columns=['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol'],
+ output_columns=['flow_lg_elg_accel'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算资金流加速度"""
+ # 计算大单+超大单净买入量
+ lg_elg_net_buy_vol = (
+ pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')
+ )
+
+ # 计算一阶变化
+ first_diff = lg_elg_net_buy_vol.diff()
+
+ # 计算二阶变化(加速度)
+ acceleration = first_diff.diff()
+
+ return stock_df.with_columns(acceleration.alias('flow_lg_elg_accel'))
+
+
+class LargeFlowMomentumCorrelationOperator(StockWiseOperator):
+ """大单资金流与价格动量相关性算子"""
+
+ def __init__(self, n: int = 20, m: int = 60):
+ config = OperatorConfig(
+ name=f"lg_flow_mom_corr_{n}_{m}",
+ description=f"{n}日大单资金流与{m}日价格动量相关性",
+ required_columns=['buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol', 'sell_elg_vol',
+ 'close', 'vol'],
+ output_columns=[f'lg_flow_mom_corr_{n}_{m}'],
+ parameters={'n': n, 'm': m}
+ )
+ super().__init__(config)
+ self.n = n
+ self.m = m
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算大单资金流与价格动量相关性"""
+ # 计算大单净额
+ net_lg_flow_val = (
+ (pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')) * pl.col('close')
+ )
+
+ # 计算滚动净大单流
+ rolling_net_lg_flow = net_lg_flow_val.rolling_sum(window=self.n)
+
+ # 计算价格动量
+ price_mom = pl.col('close').pct_change(self.n)
+
+ # 计算相关性
+ # Polars的rolling_corr需要两个表达式
+ correlation = rolling_net_lg_flow.rolling_corr(price_mom, window=self.m)
+
+ return stock_df.with_columns(
+ correlation.alias(f'lg_flow_mom_corr_{self.n}_{self.m}')
+ )
+
+
+class LargeBuyConsolidationOperator(StockWiseOperator):
+ """大单买入盘整期算子"""
+
+ def __init__(self, n: int = 20, vol_quantile: float = 0.2):
+ config = OperatorConfig(
+ name=f"lg_buy_consolidation_{n}",
+ description=f"{n}日大单买入盘整期",
+ required_columns=['close', 'buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol',
+ 'sell_elg_vol', 'vol'],
+ output_columns=[f'lg_buy_consolidation_{n}'],
+ parameters={'n': n, 'vol_quantile': vol_quantile}
+ )
+ super().__init__(config)
+ self.n = n
+ self.vol_quantile = vol_quantile
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算大单买入盘整期"""
+ epsilon = 1e-8
+
+ # 计算收盘价滚动标准差
+ rolling_std = pl.col('close').rolling_std(window=self.n)
+
+ # 计算大单净流比率
+ net_lg_flow_ratio = (
+ (pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')) /
+ (pl.col('vol') + epsilon)
+ )
+
+ # 计算滚动均值
+ rolling_mean_ratio = net_lg_flow_ratio.rolling_mean(window=self.n)
+
+ # 计算低波动阈值
+ # 这里需要按日期分组计算分位数,比较复杂,简化处理
+ # 在实际使用时,可能需要DateWiseOperator来处理横截面分位数
+
+ return stock_df.with_columns(
+ rolling_mean_ratio.alias(f'lg_buy_consolidation_{self.n}')
+ )
+
+
+# 资金流因子集合
+MONEY_FLOW_OPERATORS = [
+ MoneyFlowIntensityOperator(),
+ FlowDivergenceRatioOperator(),
+ FlowStructureChangeOperator(),
+ FlowAccelerationOperator(),
+ LargeFlowMomentumCorrelationOperator(),
+ LargeBuyConsolidationOperator(),
+]
+
+
+def apply_money_flow_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """
+ 应用所有资金流因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ operators: 要应用的算子列表,如果为None则使用默认列表
+
+ Returns:
+ 添加了资金流因子的DataFrame
+ """
+ if operators is None:
+ operators = MONEY_FLOW_OPERATORS
+
+ result_df = df
+ for operator in operators:
+ result_df = operator(result_df)
+
+ return result_df
diff --git a/main/factor/polars_sentiment_factors.py b/main/factor/polars_sentiment_factors.py
new file mode 100644
index 0000000..b13d34a
--- /dev/null
+++ b/main/factor/polars_sentiment_factors.py
@@ -0,0 +1,365 @@
+"""
+情绪因子 - 使用Polars实现
+包含市场情绪、恐慌贪婪指数、反转因子等相关因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, OperatorConfig
+import talib
+
+
+class SentimentPanicGreedIndexOperator(StockWiseOperator):
+ """市场恐慌/贪婪指数算子"""
+
+ def __init__(self, window_atr: int = 14, window_smooth: int = 5):
+ config = OperatorConfig(
+ name=f"senti_panic_greed_{window_atr}_{window_smooth}",
+ description=f"{window_atr}日ATR{window_smooth}日平滑恐慌贪婪指数",
+ required_columns=['open', 'high', 'low', 'close', 'pct_chg', 'vol'],
+ output_columns=[f'senti_panic_greed_{window_atr}_{window_smooth}'],
+ parameters={'window_atr': window_atr, 'window_smooth': window_smooth}
+ )
+ super().__init__(config)
+ self.window_atr = window_atr
+ self.window_smooth = window_smooth
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算恐慌贪婪指数"""
+ # 计算前收盘价
+ prev_close = pl.col('close').shift(1)
+
+ # 计算真实波幅
+ tr = pl.max_horizontal(
+ pl.col('high') - pl.col('low'),
+ (pl.col('high') - prev_close).abs(),
+ (pl.col('low') - prev_close).abs()
+ )
+
+ # 计算ATR
+ atr = tr.rolling_mean(window=self.window_atr)
+
+ # 计算影线
+ upper_shadow = pl.col('high') - pl.max_horizontal(pl.col('open'), pl.col('close'))
+ lower_shadow = pl.min_horizontal(pl.col('open'), pl.col('close')) - pl.col('low')
+ body = (pl.col('close') - pl.col('open')).abs()
+
+ # 计算跳空
+ gap = (pl.col('open') / prev_close - 1).fill_null(0)
+
+ # 计算波动性意外
+ volatility_surprise = (tr / (atr + 1e-8) - 1) * pl.col('pct_chg').sign()
+
+ # 计算原始情绪指标
+ raw_senti = (tr / (atr + 1e-8)) * pl.col('pct_chg').sign() + gap * 2
+
+ # 平滑处理
+ sentiment = raw_senti.rolling_mean(window=self.window_smooth)
+
+ return stock_df.with_columns(
+ sentiment.alias(f'senti_panic_greed_{self.window_atr}_{self.window_smooth}')
+ )
+
+
+class SentimentMarketBreadthProxyOperator(StockWiseOperator):
+ """市场宽度情绪代理算子"""
+
+ def __init__(self, window_vol: int = 20, window_smooth: int = 3):
+ config = OperatorConfig(
+ name=f"senti_breadth_proxy_{window_vol}_{window_smooth}",
+ description=f"{window_vol}日成交量{window_smooth}日平滑市场宽度情绪代理",
+ required_columns=['pct_chg', 'vol'],
+ output_columns=[f'senti_breadth_proxy_{window_vol}_{window_smooth}'],
+ parameters={'window_vol': window_vol, 'window_smooth': window_smooth}
+ )
+ super().__init__(config)
+ self.window_vol = window_vol
+ self.window_smooth = window_smooth
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算市场宽度情绪代理"""
+ # 计算成交量滚动均值
+ rolling_avg_vol = pl.col('vol').rolling_mean(window=self.window_vol)
+
+ # 计算价量配合度
+ raw_breadth = pl.col('pct_chg') * (pl.col('vol') / (rolling_avg_vol + 1e-8))
+
+ # 平滑处理
+ breadth_proxy = raw_breadth.rolling_mean(window=self.window_smooth)
+
+ return stock_df.with_columns(
+ breadth_proxy.alias(f'senti_breadth_proxy_{self.window_vol}_{self.window_smooth}')
+ )
+
+
+class SentimentReversalIndicatorOperator(StockWiseOperator):
+ """短期情绪反转因子算子"""
+
+ def __init__(self, window_ret: int = 5, window_vol: int = 5):
+ config = OperatorConfig(
+ name=f"senti_reversal_{window_ret}_{window_vol}",
+ description=f"{window_ret}日收益{window_vol}日波动短期情绪反转因子",
+ required_columns=['close', 'pct_chg'],
+ output_columns=[f'senti_reversal_{window_ret}_{window_vol}'],
+ parameters={'window_ret': window_ret, 'window_vol': window_vol}
+ )
+ super().__init__(config)
+ self.window_ret = window_ret
+ self.window_vol = window_vol
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算短期情绪反转因子"""
+ # 计算累计收益率
+ return_m = pl.col('close').pct_change(self.window_ret)
+
+ # 计算波动率
+ volatility_m = pl.col('pct_chg').rolling_std(window=self.window_vol)
+
+ # 计算反转因子 (负号表示反转)
+ reversal_factor = -return_m * volatility_m
+
+ return stock_df.with_columns(
+ reversal_factor.alias(f'senti_reversal_{self.window_ret}_{self.window_vol}')
+ )
+
+
+class DailyMomentumBenchmarkOperator(StockWiseOperator):
+ """日级别动量基准算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="daily_momentum_benchmark",
+ description="日级别动量基准",
+ required_columns=['pct_chg'],
+ output_columns=['daily_positive_benchmark', 'daily_negative_benchmark'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算日级别动量基准"""
+ # 这个因子需要横截面计算,简化处理
+ # 在实际应用中,应该使用DateWiseOperator来计算全市场基准
+
+ # 返回0作为占位符
+ return stock_df.with_columns([
+ pl.lit(0).alias('daily_positive_benchmark'),
+ pl.lit(0).alias('daily_negative_benchmark')
+ ])
+
+
+class DailyDeviationOperator(StockWiseOperator):
+ """日级别偏离度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="daily_deviation",
+ description="日级别偏离度",
+ required_columns=['pct_chg', 'daily_positive_benchmark', 'daily_negative_benchmark'],
+ output_columns=['daily_deviation'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算日级别偏离度"""
+ # 根据条件计算偏离度
+ conditions = [
+ (pl.col('pct_chg') > 0) & (pl.col('daily_positive_benchmark') > 0),
+ (pl.col('pct_chg') < 0) & (pl.col('daily_negative_benchmark') < 0),
+ ]
+
+ choices = [
+ pl.col('pct_chg') - pl.col('daily_positive_benchmark'),
+ pl.col('pct_chg') - pl.col('daily_negative_benchmark'),
+ ]
+
+ deviation = pl.select(conditions=conditions, choices=choices, default=0)
+
+ return stock_df.with_columns(deviation.alias('daily_deviation'))
+
+
+class CatSentimentMomentumVolumeSpikeOperator(StockWiseOperator):
+ """情绪动量成交量激增分类算子"""
+
+ def __init__(self, return_period: int = 3, return_threshold: float = 0.05,
+ volume_ratio_threshold: float = 1.5, current_pct_chg_min: float = -0.01,
+ current_pct_chg_max: float = 0.03):
+ config = OperatorConfig(
+ name=f"cat_senti_mom_vol_spike_{return_period}",
+ description=f"{return_period}日情绪动量成交量激增分类",
+ required_columns=['close', 'pct_chg', 'volume_ratio'],
+ output_columns=[f'cat_senti_mom_vol_spike_{return_period}'],
+ parameters={'return_period': return_period, 'return_threshold': return_threshold,
+ 'volume_ratio_threshold': volume_ratio_threshold,
+ 'current_pct_chg_min': current_pct_chg_min,
+ 'current_pct_chg_max': current_pct_chg_max}
+ )
+ super().__init__(config)
+ self.return_period = return_period
+ self.return_threshold = return_threshold
+ self.volume_ratio_threshold = volume_ratio_threshold
+ self.current_pct_chg_min = current_pct_chg_min
+ self.current_pct_chg_max = current_pct_chg_max
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算情绪动量成交量激增分类"""
+ # 计算n日收益率
+ return_n = pl.col('close').pct_change(self.return_period)
+
+ # 定义条件
+ cond_momentum = return_n > self.return_threshold
+ cond_volume = pl.col('volume_ratio') > self.volume_ratio_threshold
+ cond_current_price = (pl.col('pct_chg') > self.current_pct_chg_min) & \
+ (pl.col('pct_chg') < self.current_pct_chg_max)
+
+ # 组合条件
+ result = (cond_momentum.cast(str) + cond_volume.cast(str) + cond_current_price.cast(str))
+
+ return stock_df.with_columns(result.alias(f'cat_senti_mom_vol_spike_{self.return_period}'))
+
+
+class CatSentimentPreBreakoutOperator(StockWiseOperator):
+ """情绪突破前盘整分类算子"""
+
+ def __init__(self, atr_short_n: int = 10, atr_long_m: int = 40,
+ vol_atrophy_n: int = 10, vol_atrophy_m: int = 40,
+ price_stab_n: int = 5, price_stab_threshold: float = 0.05,
+ current_pct_chg_min: float = 0.005, current_pct_chg_max: float = 0.07,
+ volume_ratio_threshold: float = 1.2):
+ config = OperatorConfig(
+ name=f"cat_senti_pre_breakout",
+ description="情绪突破前盘整分类",
+ required_columns=['high', 'low', 'close', 'vol', 'pct_chg', 'volume_ratio'],
+ output_columns=['cat_senti_pre_breakout'],
+ parameters={'atr_short_n': atr_short_n, 'atr_long_m': atr_long_m,
+ 'vol_atrophy_n': vol_atrophy_n, 'vol_atrophy_m': vol_atrophy_m,
+ 'price_stab_n': price_stab_n, 'price_stab_threshold': price_stab_threshold,
+ 'current_pct_chg_min': current_pct_chg_min, 'current_pct_chg_max': current_pct_chg_max,
+ 'volume_ratio_threshold': volume_ratio_threshold}
+ )
+ super().__init__(config)
+ self.atr_short_n = atr_short_n
+ self.atr_long_m = atr_long_m
+ self.vol_atrophy_n = vol_atrophy_n
+ self.vol_atrophy_m = vol_atrophy_m
+ self.price_stab_n = price_stab_n
+ self.price_stab_threshold = price_stab_threshold
+ self.current_pct_chg_min = current_pct_chg_min
+ self.current_pct_chg_max = current_pct_chg_max
+ self.volume_ratio_threshold = volume_ratio_threshold
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算情绪突破前盘整分类"""
+ # 1. 波动率收缩 (使用价格范围作为ATR代理)
+ price_range = pl.col('high') - pl.col('low')
+ atr_short = price_range.rolling_mean(window=self.atr_short_n)
+ atr_long = price_range.rolling_mean(window=self.atr_long_m)
+ cond_vol_contraction = atr_short < (0.7 * atr_long)
+
+ # 2. 成交量萎缩
+ vol_short = pl.col('vol').rolling_mean(window=self.vol_atrophy_n)
+ vol_long = pl.col('vol').rolling_mean(window=self.vol_atrophy_m)
+ cond_vol_atrophy = vol_short < (0.7 * vol_long)
+
+ # 3. 近期价格稳定
+ rolling_max_h = pl.col('high').rolling_max(window=self.price_stab_n)
+ rolling_min_l = pl.col('low').rolling_min(window=self.price_stab_n)
+ price_stability = (rolling_max_h - rolling_min_l) / pl.col('close')
+ cond_price_stability = price_stability < self.price_stab_threshold
+
+ # 4. 当日温和放量上涨信号
+ cond_price_signal = (pl.col('pct_chg') > self.current_pct_chg_min) & \
+ (pl.col('pct_chg') < self.current_pct_chg_max)
+ cond_vol_signal = pl.col('volume_ratio') > self.volume_ratio_threshold
+ cond_current_day_signal = cond_price_signal & cond_vol_signal
+
+ # 组合条件
+ result = (cond_vol_contraction.cast(str) + cond_vol_atrophy.cast(str) +
+ cond_price_stability.cast(str) + cond_current_day_signal.cast(str))
+
+ return stock_df.with_columns(result.alias('cat_senti_pre_breakout'))
+
+
+class StrongInflowSignalOperator(StockWiseOperator):
+ """强主力资金流入信号算子"""
+
+ def __init__(self, intensity_avg_n: int = 3, intensity_threshold: float = 0.01,
+ consecutive_buy_n: int = 2, accel_positive_m: int = 1):
+ config = OperatorConfig(
+ name="senti_strong_inflow",
+ description="强主力资金流入信号",
+ required_columns=['flow_lg_elg_intensity', 'flow_lg_elg_accel'],
+ output_columns=['senti_strong_inflow'],
+ parameters={'intensity_avg_n': intensity_avg_n, 'intensity_threshold': intensity_threshold,
+ 'consecutive_buy_n': consecutive_buy_n, 'accel_positive_m': accel_positive_m}
+ )
+ super().__init__(config)
+ self.intensity_avg_n = intensity_avg_n
+ self.intensity_threshold = intensity_threshold
+ self.consecutive_buy_n = consecutive_buy_n
+ self.accel_positive_m = accel_positive_m
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算强主力资金流入信号"""
+ # 检查必需列是否存在
+ required_cols = ['flow_lg_elg_intensity', 'flow_lg_elg_accel']
+ if not all(col in stock_df.columns for col in required_cols):
+ # 如果缺少列,返回0
+ return stock_df.with_columns(pl.lit(0).alias('senti_strong_inflow'))
+
+ # 1. 近N日主力资金强度均值
+ avg_intensity = pl.col('flow_lg_elg_intensity').rolling_mean(window=self.intensity_avg_n)
+ cond_avg_intensity = avg_intensity > self.intensity_threshold
+
+ # 2. 近N日连续主力净买入天数
+ is_net_buy = (pl.col('flow_lg_elg_intensity') > 0).cast(int)
+
+ # 计算连续买入信号 (简化版)
+ consecutive_buy = is_net_buy.rolling_sum(window=self.consecutive_buy_n) == self.consecutive_buy_n
+ cond_consecutive_buy = consecutive_buy
+
+ # 3. 近M日主力资金流加速度为正
+ is_accel_positive = (pl.col('flow_lg_elg_accel') > 0).cast(int)
+ accel_positive = is_accel_positive.rolling_sum(window=self.accel_positive_m) == self.accel_positive_m
+ cond_accel_positive = accel_positive
+
+ # 综合条件
+ strong_inflow = cond_avg_intensity & cond_consecutive_buy & cond_accel_positive
+
+ return stock_df.with_columns(strong_inflow.cast(int).alias('senti_strong_inflow'))
+
+
+# 情绪因子集合
+SENTIMENT_OPERATORS = [
+ SentimentPanicGreedIndexOperator(),
+ SentimentMarketBreadthProxyOperator(),
+ SentimentReversalIndicatorOperator(),
+ DailyMomentumBenchmarkOperator(),
+ DailyDeviationOperator(),
+ CatSentimentMomentumVolumeSpikeOperator(),
+ CatSentimentPreBreakoutOperator(),
+ StrongInflowSignalOperator(),
+]
+
+
+def apply_sentiment_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """
+ 应用所有情绪因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ operators: 要应用的算子列表,如果为None则使用默认列表
+
+ Returns:
+ 添加了情绪因子的DataFrame
+ """
+ if operators is None:
+ operators = SENTIMENT_OPERATORS
+
+ result_df = df
+ for operator in operators:
+ result_df = operator(result_df)
+
+ return result_df
diff --git a/main/factor/polars_technical_factors.py b/main/factor/polars_technical_factors.py
new file mode 100644
index 0000000..5e917de
--- /dev/null
+++ b/main/factor/polars_technical_factors.py
@@ -0,0 +1,488 @@
+"""
+技术指标因子 - 使用Polars实现
+包含ATR、OBV、RSI、EMA等技术指标相关因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, OperatorConfig
+import talib
+
+
+class ATROperator(StockWiseOperator):
+ """ATR算子"""
+
+ def __init__(self, period: int = 14):
+ config = OperatorConfig(
+ name=f"atr_{period}",
+ description=f"{period}日ATR",
+ required_columns=['high', 'low', 'close'],
+ output_columns=[f'atr_{period}'],
+ parameters={'period': period}
+ )
+ super().__init__(config)
+ self.period = period
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算ATR"""
+ # 使用TA-Lib计算ATR
+ atr_values = talib.ATR(
+ stock_df['high'].to_numpy(),
+ stock_df['low'].to_numpy(),
+ stock_df['close'].to_numpy(),
+ timeperiod=self.period
+ )
+
+ return stock_df.with_columns(pl.Series(atr_values).alias(f'atr_{self.period}'))
+
+
+class OBVOperator(StockWiseOperator):
+ """OBV算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="obv",
+ description="OBV能量潮",
+ required_columns=['close', 'vol'],
+ output_columns=['obv'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算OBV"""
+ # 使用TA-Lib计算OBV
+ obv_values = talib.OBV(
+ stock_df['close'].to_numpy(),
+ stock_df['vol'].to_numpy()
+ )
+
+ return stock_df.with_columns(pl.Series(obv_values).alias('obv'))
+
+
+class OBVMAOperator(StockWiseOperator):
+ """OBV均线算子"""
+
+ def __init__(self, period: int = 6):
+ config = OperatorConfig(
+ name=f"obv_ma_{period}",
+ description=f"{period}日OBV均线",
+ required_columns=['obv'],
+ output_columns=[f'maobv_{period}'],
+ parameters={'period': period}
+ )
+ super().__init__(config)
+ self.period = period
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算OBV均线"""
+ # 使用TA-Lib计算SMA
+ ma_values = talib.SMA(
+ stock_df['obv'].to_numpy(),
+ timeperiod=self.period
+ )
+
+ return stock_df.with_columns(pl.Series(ma_values).alias(f'maobv_{self.period}'))
+
+
+class RSIOperator(StockWiseOperator):
+ """RSI算子"""
+
+ def __init__(self, period: int = 3):
+ config = OperatorConfig(
+ name=f"rsi_{period}",
+ description=f"{period}日RSI",
+ required_columns=['close'],
+ output_columns=[f'rsi_{period}'],
+ parameters={'period': period}
+ )
+ super().__init__(config)
+ self.period = period
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算RSI"""
+ # 使用TA-Lib计算RSI
+ rsi_values = talib.RSI(
+ stock_df['close'].to_numpy(),
+ timeperiod=self.period
+ )
+
+ return stock_df.with_columns(pl.Series(rsi_values).alias(f'rsi_{self.period}'))
+
+
+class EMAOperator(StockWiseOperator):
+ """EMA算子"""
+
+ def __init__(self, period: int):
+ config = OperatorConfig(
+ name=f"ema_{period}",
+ description=f"{period}日EMA",
+ required_columns=['close'],
+ output_columns=[f'_ema_{period}'],
+ parameters={'period': period}
+ )
+ super().__init__(config)
+ self.period = period
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算EMA"""
+ # 使用TA-Lib计算EMA
+ ema_values = talib.EMA(
+ stock_df['close'].to_numpy(),
+ timeperiod=self.period
+ )
+
+ return stock_df.with_columns(pl.Series(ema_values).alias(f'_ema_{self.period}'))
+
+
+class ReturnOperator(StockWiseOperator):
+ """收益率算子"""
+
+ def __init__(self, period: int):
+ config = OperatorConfig(
+ name=f"return_{period}",
+ description=f"{period}日收益率",
+ required_columns=['close'],
+ output_columns=[f'return_{period}'],
+ parameters={'period': period}
+ )
+ super().__init__(config)
+ self.period = period
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算收益率"""
+ # 计算收益率
+ ret = pl.col('close').pct_change(self.period)
+
+ return stock_df.with_columns(ret.alias(f'return_{self.period}'))
+
+
+class ActivityFactorOperator(StockWiseOperator):
+ """活跃度因子算子"""
+
+ def __init__(self, period: int, scale: float):
+ config = OperatorConfig(
+ name=f"act_factor_{period}",
+ description=f"{period}日活跃度因子",
+ required_columns=[f'_ema_{period}'],
+ output_columns=[f'act_factor{period}'],
+ parameters={'period': period, 'scale': scale}
+ )
+ super().__init__(config)
+ self.period = period
+ self.scale = scale
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算活跃度因子"""
+ # 计算EMA变化率
+ ema_change = (pl.col(f'_ema_{self.period}') / pl.col(f'_ema_{self.period}').shift(1) - 1) * 100
+
+ # 计算活跃度因子
+ activity_factor = (ema_change * 57.3 / self.scale).arctan()
+
+ return stock_df.with_columns(activity_factor.alias(f'act_factor{self.period}'))
+
+
+class ActivityFactor5Operator(StockWiseOperator):
+ """活跃度因子5算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="act_factor_5",
+ description="5日活跃度因子",
+ required_columns=['_ema_5'],
+ output_columns=['act_factor1'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算5日活跃度因子"""
+ # 计算EMA变化率
+ ema_change = (pl.col('_ema_5') / pl.col('_ema_5').shift(1) - 1) * 100
+
+ # 计算活跃度因子
+ activity_factor = (ema_change * 57.3 / 50).arctan()
+
+ return stock_df.with_columns(activity_factor.alias('act_factor1'))
+
+
+class ActivityFactor13Operator(StockWiseOperator):
+ """活跃度因子13算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="act_factor_13",
+ description="13日活跃度因子",
+ required_columns=['_ema_13'],
+ output_columns=['act_factor2'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算13日活跃度因子"""
+ # 计算EMA变化率
+ ema_change = (pl.col('_ema_13') / pl.col('_ema_13').shift(1) - 1) * 100
+
+ # 计算活跃度因子
+ activity_factor = (ema_change * 57.3 / 40).arctan()
+
+ return stock_df.with_columns(activity_factor.alias('act_factor2'))
+
+
+class ActivityFactor20Operator(StockWiseOperator):
+ """活跃度因子20算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="act_factor_20",
+ description="20日活跃度因子",
+ required_columns=['_ema_20'],
+ output_columns=['act_factor3'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算20日活跃度因子"""
+ # 计算EMA变化率
+ ema_change = (pl.col('_ema_20') / pl.col('_ema_20').shift(1) - 1) * 100
+
+ # 计算活跃度因子
+ activity_factor = (ema_change * 57.3 / 21).arctan()
+
+ return stock_df.with_columns(activity_factor.alias('act_factor3'))
+
+
+class ActivityFactor60Operator(StockWiseOperator):
+ """活跃度因子60算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="act_factor_60",
+ description="60日活跃度因子",
+ required_columns=['_ema_60'],
+ output_columns=['act_factor4'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算60日活跃度因子"""
+ # 计算EMA变化率
+ ema_change = (pl.col('_ema_60') / pl.col('_ema_60').shift(1) - 1) * 100
+
+ # 计算活跃度因子
+ activity_factor = (ema_change * 57.3 / 10).arctan()
+
+ return stock_df.with_columns(activity_factor.alias('act_factor4'))
+
+
+class ActivityFactor5and6Operator(StockWiseOperator):
+ """活跃度因子5和6算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="act_factor_5_6",
+ description="活跃度因子5和6",
+ required_columns=['act_factor1', 'act_factor2'],
+ output_columns=['act_factor5', 'act_factor6'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算活跃度因子5和6"""
+ # 计算因子5
+ factor5 = pl.col('act_factor1') + pl.col('act_factor2') + pl.col('act_factor3') + pl.col('act_factor4')
+
+ # 计算因子6
+ numerator = pl.col('act_factor1') - pl.col('act_factor2')
+ denominator = (pl.col('act_factor1').pow(2) + pl.col('act_factor2').pow(2)).sqrt()
+ factor6 = numerator / (denominator + 1e-8) # 避免除零
+
+ return stock_df.with_columns([
+ factor5.alias('act_factor5'),
+ factor6.alias('act_factor6')
+ ])
+
+
+class Alpha003Operator(StockWiseOperator):
+ """Alpha003算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="alpha_003",
+ description="Alpha003因子",
+ required_columns=['open', 'close', 'high', 'low'],
+ output_columns=['alpha_003'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算Alpha003"""
+ # 计算因子
+ alpha_003 = pl.when(pl.col('high') != pl.col('low')) \
+ .then((pl.col('close') - pl.col('open')) / (pl.col('high') - pl.col('low'))) \
+ .otherwise(0)
+
+ return stock_df.with_columns(alpha_003.alias('alpha_003'))
+
+
+class Alpha007Operator(StockWiseOperator):
+ """Alpha007算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="alpha_007",
+ description="Alpha007因子",
+ required_columns=['close', 'vol'],
+ output_columns=['alpha_007'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算Alpha007"""
+ # 计算5日相关性
+ corr_5 = pl.col('close').rolling_corr(pl.col('vol'), window=5)
+
+ return stock_df.with_columns(corr_5.alias('alpha_007'))
+
+
+class Alpha013Operator(StockWiseOperator):
+ """Alpha013算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="alpha_013",
+ description="Alpha013因子",
+ required_columns=['close'],
+ output_columns=['alpha_013'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算Alpha013"""
+ # 计算5日和20日和
+ sum_5 = pl.col('close').rolling_sum(window=5)
+ sum_20 = pl.col('close').rolling_sum(window=20)
+
+ # 计算因子
+ alpha_013 = sum_5 - sum_20
+
+ return stock_df.with_columns(alpha_013.alias('alpha_013'))
+
+
+class Alpha022Operator(StockWiseOperator):
+ """Alpha022算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="alpha_022",
+ description="Alpha022改进因子",
+ required_columns=['high', 'low', 'close', 'vol'],
+ output_columns=['alpha_22_improved'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算Alpha022改进因子"""
+ # 计算滚动协方差
+ cov_5 = pl.col('high').rolling_cov(pl.col('vol'), window=5)
+
+ # 计算协方差差分
+ delta_cov = cov_5.diff(5)
+
+ # 计算收盘价标准差
+ std_close = pl.col('close').rolling_std(window=20)
+
+ # 计算标准差排名 (简化版)
+ rank_std = std_close
+
+ # 计算最终因子
+ alpha_22 = -1 * delta_cov * rank_std
+
+ return stock_df.with_columns(alpha_22.alias('alpha_22_improved'))
+
+
+class BBIRatioOperator(StockWiseOperator):
+ """BBI比率算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="bbi_ratio",
+ description="BBI比率因子",
+ required_columns=['close'],
+ output_columns=['bbi_ratio_factor'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算BBI比率"""
+ # 计算不同周期的SMA
+ sma3 = pl.col('close').rolling_mean(window=3)
+ sma6 = pl.col('close').rolling_mean(window=6)
+ sma12 = pl.col('close').rolling_mean(window=12)
+ sma24 = pl.col('close').rolling_mean(window=24)
+
+ # 计算BBI
+ bbi = (sma3 + sma6 + sma12 + sma24) / 4
+
+ # 计算比率
+ bbi_ratio = bbi / pl.col('close')
+
+ return stock_df.with_columns(bbi_ratio.alias('bbi_ratio_factor'))
+
+
+# 技术指标因子集合
+TECHNICAL_OPERATORS = [
+ ATROperator(14),
+ ATROperator(6),
+ OBVOperator(),
+ OBVMAOperator(6),
+ RSIOperator(3),
+ EMAOperator(5),
+ EMAOperator(13),
+ EMAOperator(20),
+ EMAOperator(60),
+ ReturnOperator(5),
+ ReturnOperator(20),
+ ActivityFactor5Operator(),
+ ActivityFactor13Operator(),
+ ActivityFactor20Operator(),
+ ActivityFactor60Operator(),
+ ActivityFactor5and6Operator(),
+ Alpha003Operator(),
+ Alpha007Operator(),
+ Alpha013Operator(),
+ Alpha022Operator(),
+ BBIRatioOperator(),
+]
+
+
+def apply_technical_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """
+ 应用所有技术指标因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ operators: 要应用的算子列表,如果为None则使用默认列表
+
+ Returns:
+ 添加了技术指标因子的DataFrame
+ """
+ if operators is None:
+ operators = TECHNICAL_OPERATORS
+
+ result_df = df
+ for operator in operators:
+ result_df = operator(result_df)
+
+ return result_df
diff --git a/main/factor/polars_volatility_factors.py b/main/factor/polars_volatility_factors.py
new file mode 100644
index 0000000..bab7ecd
--- /dev/null
+++ b/main/factor/polars_volatility_factors.py
@@ -0,0 +1,419 @@
+"""
+波动率因子 - 使用Polars实现
+包含上行波动率、下行波动率、波动率比率等相关因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, OperatorConfig
+
+
+class UpsideVolatilityOperator(StockWiseOperator):
+ """上行波动率算子"""
+
+ def __init__(self, window: int = 20):
+ config = OperatorConfig(
+ name=f"upside_volatility_{window}",
+ description=f"{window}日上行波动率",
+ required_columns=['pct_chg'],
+ output_columns=[f'upside_volatility_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算上行波动率"""
+ # 分离正收益率
+ pos_returns = pl.when(pl.col('pct_chg') > 0).then(pl.col('pct_chg')).otherwise(0)
+
+ # 计算正收益率的平方
+ pos_returns_sq = pos_returns.pow(2)
+
+ # 计算滚动和
+ rolling_pos_count = (pl.col('pct_chg') > 0).rolling_sum(window=self.window)
+ rolling_pos_sum = pos_returns.rolling_sum(window=self.window)
+ rolling_pos_sum_sq = pos_returns_sq.rolling_sum(window=self.window)
+
+ # 计算方差和标准差
+ pos_mean_sq = rolling_pos_sum_sq / rolling_pos_count
+ pos_mean = rolling_pos_sum / rolling_pos_count
+ pos_var = pos_mean_sq - pos_mean.pow(2)
+
+ # 处理样本数不足的情况
+ pos_var = pl.when(rolling_pos_count >= 2).then(pos_var).otherwise(None)
+ pos_var = pos_var.clip(lower=0)
+
+ upside_vol = pos_var.sqrt()
+
+ return stock_df.with_columns(upside_vol.alias(f'upside_volatility_{self.window}'))
+
+
+class DownsideVolatilityOperator(StockWiseOperator):
+ """下行波动率算子"""
+
+ def __init__(self, window: int = 20):
+ config = OperatorConfig(
+ name=f"downside_volatility_{window}",
+ description=f"{window}日下行波动率",
+ required_columns=['pct_chg'],
+ output_columns=[f'downside_volatility_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算下行波动率"""
+ # 分离负收益率
+ neg_returns = pl.when(pl.col('pct_chg') < 0).then(pl.col('pct_chg')).otherwise(0)
+
+ # 计算负收益率的平方
+ neg_returns_sq = neg_returns.pow(2)
+
+ # 计算滚动和
+ rolling_neg_count = (pl.col('pct_chg') < 0).rolling_sum(window=self.window)
+ rolling_neg_sum = neg_returns.rolling_sum(window=self.window)
+ rolling_neg_sum_sq = neg_returns_sq.rolling_sum(window=self.window)
+
+ # 计算方差和标准差
+ neg_mean_sq = rolling_neg_sum_sq / rolling_neg_count
+ neg_mean = rolling_neg_sum / rolling_neg_count
+ neg_var = neg_mean_sq - neg_mean.pow(2)
+
+ # 处理样本数不足的情况
+ neg_var = pl.when(rolling_neg_count >= 2).then(neg_var).otherwise(None)
+ neg_var = neg_var.clip(lower=0)
+
+ downside_vol = neg_var.sqrt()
+
+ return stock_df.with_columns(downside_vol.alias(f'downside_volatility_{self.window}'))
+
+
+class VolatilityRatioOperator(StockWiseOperator):
+ """波动率比率算子"""
+
+ def __init__(self, window: int = 20):
+ config = OperatorConfig(
+ name=f"volatility_ratio_{window}",
+ description=f"{window}日波动率比率",
+ required_columns=['pct_chg'],
+ output_columns=[f'volatility_ratio_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算波动率比率"""
+ # 计算上行和下行波动率
+ pos_returns = pl.when(pl.col('pct_chg') > 0).then(pl.col('pct_chg')).otherwise(0)
+ neg_returns = pl.when(pl.col('pct_chg') < 0).then(pl.col('pct_chg')).otherwise(0)
+
+ # 计算滚动标准差
+ upside_vol = pos_returns.rolling_std(window=self.window)
+ downside_vol = neg_returns.rolling_std(window=self.window)
+
+ # 计算比率
+ vol_ratio = upside_vol / downside_vol
+
+ # 处理无穷大和NaN值
+ vol_ratio = vol_ratio.replace([np.inf, -np.inf], None).fill_null(0)
+
+ return stock_df.with_columns(vol_ratio.alias(f'volatility_ratio_{self.window}'))
+
+
+class ReturnSkewnessOperator(StockWiseOperator):
+ """收益率偏度算子"""
+
+ def __init__(self, window: int = 5):
+ config = OperatorConfig(
+ name=f"return_skewness_{window}",
+ description=f"{window}日收益率偏度",
+ required_columns=['pct_chg'],
+ output_columns=[f'return_skewness_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算收益率偏度"""
+ skewness = pl.col('pct_chg').rolling_skew(window=self.window)
+
+ return stock_df.with_columns(skewness.alias(f'return_skewness_{self.window}'))
+
+
+class ReturnKurtosisOperator(StockWiseOperator):
+ """收益率峰度算子"""
+
+ def __init__(self, window: int = 5):
+ config = OperatorConfig(
+ name=f"return_kurtosis_{window}",
+ description=f"{window}日收益率峰度",
+ required_columns=['pct_chg'],
+ output_columns=[f'return_kurtosis_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算收益率峰度"""
+ kurtosis = pl.col('pct_chg').rolling_kurt(window=self.window)
+
+ return stock_df.with_columns(kurtosis.alias(f'return_kurtosis_{self.window}'))
+
+
+class VolatilityAmplificationOperator(StockWiseOperator):
+ """亏损状态波动率放大算子"""
+
+ def __init__(self, n: int = 20):
+ config = OperatorConfig(
+ name=f"vol_amp_loss_{n}",
+ description=f"{n}日亏损状态波动率放大",
+ required_columns=['pct_chg', 'weight_avg', 'close'],
+ output_columns=[f'vol_amp_loss_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算亏损状态波动率放大"""
+ # 计算n日波动率
+ vol_n = pl.col('pct_chg').rolling_std(window=self.n)
+
+ # 计算亏损程度
+ loss_degree = pl.max_horizontal(0, pl.col('weight_avg') - pl.col('close')) / pl.col('close')
+
+ # 计算放大因子
+ vol_amp = vol_n * loss_degree
+
+ return stock_df.with_columns(vol_amp.alias(f'vol_amp_loss_{self.n}'))
+
+
+class HighVolDropWhenProfitableOperator(StockWiseOperator):
+ """高成交量下跌当获利状态算子"""
+
+ def __init__(self, n: int = 20, m: int = 5, profit_thresh: float = 0.1,
+ drop_thresh: float = -0.03, vol_multiple: float = 2.0):
+ config = OperatorConfig(
+ name=f"vol_drop_profit_cnt_{m}",
+ description=f"{m}日高成交量下跌当获利状态计数",
+ required_columns=['close', 'pct_chg', 'vol', 'weight_avg'],
+ output_columns=[f'vol_drop_profit_cnt_{m}'],
+ parameters={'n': n, 'm': m, 'profit_thresh': profit_thresh,
+ 'drop_thresh': drop_thresh, 'vol_multiple': vol_multiple}
+ )
+ super().__init__(config)
+ self.n = n
+ self.m = m
+ self.profit_thresh = profit_thresh
+ self.drop_thresh = drop_thresh
+ self.vol_multiple = vol_multiple
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算高成交量下跌当获利状态计数"""
+ # 判断是否获利
+ is_profitable = pl.col('close') > pl.col('weight_avg') * (1 + self.profit_thresh)
+
+ # 判断是否下跌
+ is_dropping = pl.col('pct_chg') < self.drop_thresh
+
+ # 计算滚动均值和标准差
+ rolling_mean_vol = pl.col('vol').rolling_mean(window=self.n)
+ rolling_std_vol = pl.col('vol').rolling_std(window=self.n).fill_null(0)
+
+ # 判断是否高成交量
+ is_high_vol = pl.col('vol') > (rolling_mean_vol + self.vol_multiple * rolling_std_vol)
+
+ # 计算事件
+ event = is_profitable & is_dropping & is_high_vol
+
+ # 计算m日累计
+ event_cnt = event.cast(int).rolling_sum(window=self.m)
+
+ return stock_df.with_columns(event_cnt.alias(f'vol_drop_profit_cnt_{self.m}'))
+
+
+class LargeFlowVolatilityInteractionOperator(StockWiseOperator):
+ """大单资金流驱动波动率交互算子"""
+
+ def __init__(self, n: int = 20):
+ config = OperatorConfig(
+ name=f"lg_flow_vol_interact_{n}",
+ description=f"{n}日大单资金流驱动波动率交互",
+ required_columns=['pct_chg', 'buy_lg_vol', 'buy_elg_vol', 'sell_lg_vol',
+ 'sell_elg_vol', 'vol', 'close'],
+ output_columns=[f'lg_flow_vol_interact_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算大单资金流驱动波动率交互"""
+ epsilon = 1e-8
+
+ # 计算n日波动率
+ vol_n = pl.col('pct_chg').rolling_std(window=self.n)
+
+ # 计算大单净额
+ net_lg_flow_val = (
+ (pl.col('buy_lg_vol') + pl.col('buy_elg_vol') -
+ pl.col('sell_lg_vol') - pl.col('sell_elg_vol')) * pl.col('close')
+ )
+
+ # 计算总成交额
+ total_val = pl.col('vol') * pl.col('close')
+
+ # 计算大单净流入比率绝对值
+ abs_net_lg_flow_ratio = net_lg_flow_val.abs() / (total_val + epsilon)
+
+ # 计算n日均值
+ abs_ratio_n = abs_net_lg_flow_ratio.rolling_mean(window=self.n)
+
+ # 计算交互项
+ interaction = vol_n * abs_ratio_n
+
+ return stock_df.with_columns(interaction.alias(f'lg_flow_vol_interact_{self.n}'))
+
+
+class VolatilityAdjustedROCPOperator(StockWiseOperator):
+ """波动率调整收益率算子"""
+
+ def __init__(self, n: int = 20):
+ config = OperatorConfig(
+ name=f"vol_adj_roc_{n}",
+ description=f"{n}日波动率调整收益率",
+ required_columns=['close', 'pct_chg'],
+ output_columns=[f'vol_adj_roc_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算波动率调整收益率"""
+ # 计算n日收益率
+ roc_n = pl.col('close').pct_change(self.n)
+
+ # 计算n日波动率
+ vol_n = pl.col('pct_chg').rolling_std(window=self.n).fill_null(0)
+
+ # 计算波动率调整收益率
+ vol_adj_roc = roc_n / (vol_n + 1e-10) # 避免除零
+
+ return stock_df.with_columns(vol_adj_roc.alias(f'vol_adj_roc_{self.n}'))
+
+
+class StandardDeviation5Operator(StockWiseOperator):
+ """5日收益率标准差算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="std_return_5",
+ description="5日收益率标准差",
+ required_columns=['close'],
+ output_columns=['std_return_5'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算5日收益率标准差"""
+ # 计算收益率
+ returns = pl.col('close').pct_change()
+
+ # 计算5日标准差
+ std_5 = returns.rolling_std(window=5)
+
+ return stock_df.with_columns(std_5.alias('std_return_5'))
+
+
+class StandardDeviation90Operator(StockWiseOperator):
+ """90日收益率标准差算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="std_return_90",
+ description="90日收益率标准差",
+ required_columns=['close'],
+ output_columns=['std_return_90'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算90日收益率标准差"""
+ # 计算收益率
+ returns = pl.col('close').pct_change()
+
+ # 计算90日标准差
+ std_90 = returns.rolling_std(window=90)
+
+ return stock_df.with_columns(std_90.alias('std_return_90'))
+
+
+class StandardDeviation90ShiftedOperator(StockWiseOperator):
+ """90日收益率标准差(移位)算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="std_return_90_2",
+ description="90日收益率标准差(移位10日)",
+ required_columns=['close'],
+ output_columns=['std_return_90_2'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算90日收益率标准差(移位10日)"""
+ # 计算收益率(移位10日)
+ returns = pl.col('close').shift(10).pct_change()
+
+ # 计算90日标准差
+ std_90_2 = returns.rolling_std(window=90)
+
+ return stock_df.with_columns(std_90_2.alias('std_return_90_2'))
+
+
+# 波动率因子集合
+VOLATILITY_OPERATORS = [
+ UpsideVolatilityOperator(),
+ DownsideVolatilityOperator(),
+ VolatilityRatioOperator(),
+ ReturnSkewnessOperator(),
+ ReturnKurtosisOperator(),
+ VolatilityAmplificationOperator(),
+ HighVolDropWhenProfitableOperator(),
+ LargeFlowVolatilityInteractionOperator(),
+ VolatilityAdjustedROCPOperator(),
+ StandardDeviation5Operator(),
+ StandardDeviation90Operator(),
+ StandardDeviation90ShiftedOperator(),
+]
+
+
+def apply_volatility_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """
+ 应用所有波动率因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ operators: 要应用的算子列表,如果为None则使用默认列表
+
+ Returns:
+ 添加了波动率因子的DataFrame
+ """
+ if operators is None:
+ operators = VOLATILITY_OPERATORS
+
+ result_df = df
+ for operator in operators:
+ result_df = operator(result_df)
+
+ return result_df
diff --git a/main/factor/polars_volume_factors.py b/main/factor/polars_volume_factors.py
new file mode 100644
index 0000000..eddca0b
--- /dev/null
+++ b/main/factor/polars_volume_factors.py
@@ -0,0 +1,480 @@
+"""
+成交量因子 - 使用Polars实现
+包含成交量变化率、突破信号、换手率等相关因子计算
+"""
+
+import polars as pl
+import numpy as np
+from typing import Dict, List, Optional, Any
+from operator_framework import StockWiseOperator, OperatorConfig
+
+
+class VolumeChangeRateOperator(StockWiseOperator):
+ """成交量变化率算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="volume_change_rate",
+ description="短期成交量变化率",
+ required_columns=['vol'],
+ output_columns=['volume_change_rate'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量变化率"""
+ # 计算2日均量
+ vol_mean_2 = pl.col('vol').rolling_mean(window=2)
+
+ # 计算10日均量
+ vol_mean_10 = pl.col('vol').rolling_mean(window=10)
+
+ # 计算变化率
+ change_rate = (vol_mean_2 / vol_mean_10) - 1
+
+ return stock_df.with_columns(change_rate.alias('volume_change_rate'))
+
+
+class VolumeBreakoutOperator(StockWiseOperator):
+ """成交量突破算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="volume_breakout",
+ description="成交量突破信号",
+ required_columns=['vol'],
+ output_columns=['cat_volume_breakout'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量突破信号"""
+ # 计算5日最大成交量
+ max_vol_5 = pl.col('vol').rolling_max(window=5)
+
+ # 判断是否突破
+ breakout = pl.col('vol') > max_vol_5
+
+ return stock_df.with_columns(breakout.alias('cat_volume_breakout'))
+
+
+class TurnoverDeviationOperator(StockWiseOperator):
+ """换手率偏离度算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="turnover_deviation",
+ description="换手率均线偏离度",
+ required_columns=['turnover_rate'],
+ output_columns=['turnover_deviation'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算换手率均线偏离度"""
+ # 计算3日均值
+ mean_turnover = pl.col('turnover_rate').rolling_mean(window=3)
+
+ # 计算3日标准差
+ std_turnover = pl.col('turnover_rate').rolling_std(window=3)
+
+ # 计算偏离度
+ deviation = (pl.col('turnover_rate') - mean_turnover) / std_turnover
+
+ return stock_df.with_columns(deviation.alias('turnover_deviation'))
+
+
+class TurnoverSpikeOperator(StockWiseOperator):
+ """换手率激增算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="turnover_spike",
+ description="换手率激增信号",
+ required_columns=['turnover_rate'],
+ output_columns=['cat_turnover_spike'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算换手率激增信号"""
+ # 计算3日均值
+ mean_turnover = pl.col('turnover_rate').rolling_mean(window=3)
+
+ # 计算3日标准差
+ std_turnover = pl.col('turnover_rate').rolling_std(window=3)
+
+ # 判断是否激增 (超过均值+2倍标准差)
+ spike = pl.col('turnover_rate') > (mean_turnover + 2 * std_turnover)
+
+ return stock_df.with_columns(spike.alias('cat_turnover_spike'))
+
+
+class VolumeRatioAverageOperator(StockWiseOperator):
+ """量比均值算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="volume_ratio_average",
+ description="量比均值",
+ required_columns=['volume_ratio'],
+ output_columns=['avg_volume_ratio'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算量比均值"""
+ # 计算3日均值
+ avg_ratio = pl.col('volume_ratio').rolling_mean(window=3)
+
+ return stock_df.with_columns(avg_ratio.alias('avg_volume_ratio'))
+
+
+class VolumeRatioBreakoutOperator(StockWiseOperator):
+ """量比突破算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="volume_ratio_breakout",
+ description="量比突破信号",
+ required_columns=['volume_ratio'],
+ output_columns=['cat_volume_ratio_breakout'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算量比突破信号"""
+ # 计算5日最大量比
+ max_ratio_5 = pl.col('volume_ratio').rolling_max(window=5)
+
+ # 判断是否突破
+ breakout = pl.col('volume_ratio') > max_ratio_5
+
+ return stock_df.with_columns(breakout.alias('cat_volume_ratio_breakout'))
+
+
+class VolumeSpikeOperator(StockWiseOperator):
+ """成交量激增算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="volume_spike",
+ description="成交量激增",
+ required_columns=['vol'],
+ output_columns=['vol_spike'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量激增"""
+ # 计算20日均量
+ vol_mean_20 = pl.col('vol').rolling_mean(window=20)
+
+ return stock_df.with_columns(vol_mean_20.alias('vol_spike'))
+
+
+class VolumeStd5Operator(StockWiseOperator):
+ """5日成交量标准差算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="volume_std_5",
+ description="5日成交量标准差",
+ required_columns=['vol'],
+ output_columns=['vol_std_5'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算5日成交量标准差"""
+ # 计算成交量变化率
+ vol_pct_change = pl.col('vol').pct_change()
+
+ # 计算5日标准差
+ std_5 = vol_pct_change.rolling_std(window=5)
+
+ return stock_df.with_columns(std_5.alias('vol_std_5'))
+
+
+class TurnoverRateMeanOperator(StockWiseOperator):
+ """换手率均值算子"""
+
+ def __init__(self, n: int):
+ config = OperatorConfig(
+ name=f"turnover_rate_mean_{n}",
+ description=f"{n}日换手率均值",
+ required_columns=['turnover_rate'],
+ output_columns=[f'turnover_rate_mean_{n}'],
+ parameters={'n': n}
+ )
+ super().__init__(config)
+ self.n = n
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算n日换手率均值"""
+ # 计算n日均值
+ mean_rate = pl.col('turnover_rate').rolling_mean(window=self.n)
+
+ return stock_df.with_columns(mean_rate.alias(f'turnover_rate_mean_{self.n}'))
+
+
+class VolumeSpikeCategoryOperator(StockWiseOperator):
+ """成交量激增分类算子"""
+
+ def __init__(self):
+ config = OperatorConfig(
+ name="volume_spike_category",
+ description="成交量激增分类",
+ required_columns=['vol', 'vol_spike'],
+ output_columns=['cat_vol_spike'],
+ parameters={}
+ )
+ super().__init__(config)
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量激增分类"""
+ # 判断是否激增 (超过2倍均值)
+ spike = pl.col('vol') > (2 * pl.col('vol_spike'))
+
+ return stock_df.with_columns(spike.alias('cat_vol_spike'))
+
+
+class TurnoverVolatilityOperator(StockWiseOperator):
+ """换手率波动率算子"""
+
+ def __init__(self, window: int = 20):
+ config = OperatorConfig(
+ name=f"turnover_volatility_{window}",
+ description=f"{window}日换手率波动率",
+ required_columns=['turnover_rate'],
+ output_columns=[f'turnover_std_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算换手率波动率"""
+ # 计算滚动标准差
+ turnover_std = pl.col('turnover_rate').rolling_std(window=self.window)
+
+ return stock_df.with_columns(turnover_std.alias(f'turnover_std_{self.window}'))
+
+
+class VolumeCovarianceOperator(StockWiseOperator):
+ """成交量协方差算子"""
+
+ def __init__(self, window: int = 5):
+ config = OperatorConfig(
+ name=f"volume_covariance_{window}",
+ description=f"{window}日成交量协方差",
+ required_columns=['high', 'vol'],
+ output_columns=[f'cov_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量协方差"""
+ # 计算滚动协方差
+ def calculate_cov(group_df):
+ return group_df.select(
+ pl.col('high').rolling_cov(pl.col('vol'), window=self.window)
+ )
+
+ cov_result = calculate_cov(stock_df)
+
+ return stock_df.with_columns(cov_result[f'cov_{self.window}'].alias(f'cov_{self.window}'))
+
+
+class VolumeCovarianceDeltaOperator(StockWiseOperator):
+ """成交量协方差变化算子"""
+
+ def __init__(self, period: int = 5):
+ config = OperatorConfig(
+ name=f"volume_covariance_delta_{period}",
+ description=f"{period}日成交量协方差变化",
+ required_columns=['cov_5'],
+ output_columns=[f'delta_cov_{period}'],
+ parameters={'period': period}
+ )
+ super().__init__(config)
+ self.period = period
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量协方差变化"""
+ # 计算差分
+ delta = pl.col('cov_5').diff(self.period)
+
+ return stock_df.with_columns(delta.alias(f'delta_cov_{self.period}'))
+
+
+class TurnoverRateAccelerationOperator(StockWiseOperator):
+ """换手率加速度算子"""
+
+ def __init__(self, short_window: int = 5, long_window: int = 20):
+ config = OperatorConfig(
+ name=f"turnover_acceleration_{short_window}_{long_window}",
+ description=f"{short_window}日对{long_window}日换手率加速度",
+ required_columns=['turnover_rate'],
+ output_columns=[f'turnover_rate_acceleration_{short_window}_{long_window}'],
+ parameters={'short_window': short_window, 'long_window': long_window}
+ )
+ super().__init__(config)
+ self.short_window = short_window
+ self.long_window = long_window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算换手率加速度"""
+ # 计算短期均值
+ short_avg = pl.col('turnover_rate').rolling_mean(window=self.short_window)
+
+ # 计算长期均值
+ long_avg = pl.col('turnover_rate').rolling_mean(window=self.long_window)
+
+ # 计算加速度
+ acceleration = short_avg - long_avg
+
+ return stock_df.with_columns(
+ acceleration.alias(f'turnover_rate_acceleration_{self.short_window}_{self.long_window}')
+ )
+
+
+class VolumeSustainabilityOperator(StockWiseOperator):
+ """成交量持续性算子"""
+
+ def __init__(self, short_window: int = 10, long_window: int = 30):
+ config = OperatorConfig(
+ name=f"volume_sustain_{short_window}_{long_window}",
+ description=f"{short_window}日成交量大于{long_window}日均值占比",
+ required_columns=['vol'],
+ output_columns=[f'vol_sustain_{short_window}_{long_window}'],
+ parameters={'short_window': short_window, 'long_window': long_window}
+ )
+ super().__init__(config)
+ self.short_window = short_window
+ self.long_window = long_window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交量持续性"""
+ # 计算长期均值
+ long_avg = pl.col('vol').rolling_mean(window=self.long_window)
+
+ # 判断是否大于长期均值
+ above_avg = pl.col('vol') > long_avg
+
+ # 计算短期占比
+ sustain_ratio = above_avg.cast(int).rolling_mean(window=self.short_window)
+
+ return stock_df.with_columns(
+ sustain_ratio.alias(f'vol_sustain_{self.short_window}_{self.long_window}')
+ )
+
+
+class TurnoverRelativeStrengthOperator(StockWiseOperator):
+ """换手率相对强度算子"""
+
+ def __init__(self, window: int = 20):
+ config = OperatorConfig(
+ name=f"turnover_relative_strength_{window}",
+ description=f"{window}日换手率相对强度",
+ required_columns=['turnover_rate'],
+ output_columns=[f'turnover_relative_strength_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算换手率相对强度"""
+ # 计算长期均值
+ long_avg = pl.col('turnover_rate').rolling_mean(window=self.window)
+
+ # 计算相对强度
+ relative_strength = pl.col('turnover_rate') / long_avg
+
+ return stock_df.with_columns(
+ relative_strength.alias(f'turnover_relative_strength_{self.window}')
+ )
+
+
+class AmountOutlierOperator(StockWiseOperator):
+ """成交额异常值算子"""
+
+ def __init__(self, window: int = 10):
+ config = OperatorConfig(
+ name=f"amount_outlier_{window}",
+ description=f"{window}日成交额异常值",
+ required_columns=['amount'],
+ output_columns=[f'amount_outlier_{window}'],
+ parameters={'window': window}
+ )
+ super().__init__(config)
+ self.window = window
+
+ def apply_stock(self, stock_df: pl.DataFrame, **kwargs) -> pl.DataFrame:
+ """计算成交额异常值"""
+ # 计算均值
+ avg_amount = pl.col('amount').rolling_mean(window=self.window)
+
+ # 计算差值
+ amount_diff = pl.col('amount') - avg_amount
+
+ # 计算Z-score (简化版,实际使用时可能需要横截面标准化)
+ mean_diff = amount_diff.rolling_mean(window=self.window)
+ std_diff = amount_diff.rolling_std(window=self.window)
+
+ # 计算异常值分数
+ outlier_score = (amount_diff - mean_diff) / (std_diff + 1e-8)
+
+ return stock_df.with_columns(outlier_score.alias(f'amount_outlier_{self.window}'))
+
+
+# 成交量因子集合
+VOLUME_OPERATORS = [
+ VolumeChangeRateOperator(),
+ VolumeBreakoutOperator(),
+ TurnoverDeviationOperator(),
+ TurnoverSpikeOperator(),
+ VolumeRatioAverageOperator(),
+ VolumeRatioBreakoutOperator(),
+ VolumeSpikeOperator(),
+ VolumeStd5Operator(),
+ TurnoverRateMeanOperator(20),
+ VolumeSpikeCategoryOperator(),
+ TurnoverVolatilityOperator(),
+ TurnoverRateAccelerationOperator(),
+ VolumeSustainabilityOperator(),
+ TurnoverRelativeStrengthOperator(),
+ AmountOutlierOperator(),
+]
+
+
+def apply_volume_factors(df: pl.DataFrame, operators: List = None) -> pl.DataFrame:
+ """
+ 应用所有成交量因子
+
+ Args:
+ df: 输入的Polars DataFrame
+ operators: 要应用的算子列表,如果为None则使用默认列表
+
+ Returns:
+ 添加了成交量因子的DataFrame
+ """
+ if operators is None:
+ operators = VOLUME_OPERATORS
+
+ result_df = df
+ for operator in operators:
+ result_df = operator(result_df)
+
+ return result_df
diff --git a/main/train/Classify/Classify2.ipynb b/main/train/Classify/Classify2.ipynb
new file mode 100644
index 0000000..3ff5215
--- /dev/null
+++ b/main/train/Classify/Classify2.ipynb
@@ -0,0 +1,2860 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "79a7758178bafdd3",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:46:06.987506Z",
+ "start_time": "2025-04-03T12:46:06.259551Z"
+ },
+ "jupyter": {
+ "source_hidden": true
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "/mnt/d/PyProject/NewStock/main/train/Classify\n"
+ ]
+ },
+ {
+ "ename": "ModuleNotFoundError",
+ "evalue": "No module named 'main.factor'; 'main' is not a package",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
+ "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)",
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 15\u001b[39m\n\u001b[32m 13\u001b[39m \u001b[38;5;28mprint\u001b[39m(os.getcwd())\n\u001b[32m 14\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpandas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpd\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m15\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mmain\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mfactor\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mfactor\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m get_rolling_factor, get_simple_factor\n\u001b[32m 16\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mmain\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mfactor\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m read_industry_data\n\u001b[32m 17\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mmain\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mfactor_processor\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m calculate_score\n",
+ "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'main.factor'; 'main' is not a package"
+ ]
+ }
+ ],
+ "source": [
+ "%load_ext autoreload\n",
+ "%autoreload 2\n",
+ "# %load_ext cudf.pandas\n",
+ "\n",
+ "import gc\n",
+ "import os\n",
+ "import sys\n",
+ "sys.path.append('/mnt/d/PyProject/NewStock/')\n",
+ "import sys\n",
+ "sys.path.append('/mnt/d/PyProject/NewStock/main/')\n",
+ "import sys\n",
+ "sys.path.append('/mnt/d/PyProject/NewStock/train')\n",
+ "print(os.getcwd())\n",
+ "import pandas as pd\n",
+ "from main.factor.factor import get_rolling_factor, get_simple_factor\n",
+ "from main.utils.factor import read_industry_data\n",
+ "from main.utils.factor_processor import calculate_score\n",
+ "from main.utils.utils import read_and_merge_h5_data, merge_with_industry_data\n",
+ "\n",
+ "import warnings\n",
+ "\n",
+ "warnings.filterwarnings(\"ignore\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4a481c60",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "# 设置使用核心\n",
+ "import os\n",
+ "os.environ[\"MODIN_CPUS\"] = \"4\"\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a79cafb06a7e0e43",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:47:00.212859Z",
+ "start_time": "2025-04-03T12:46:06.998047Z"
+ },
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "daily data\n",
+ "daily basic\n",
+ "inner merge on ['ts_code', 'trade_date']\n",
+ "stk limit\n",
+ "left merge on ['ts_code', 'trade_date']\n",
+ "money flow\n",
+ "left merge on ['ts_code', 'trade_date']\n",
+ "cyq perf\n",
+ "left merge on ['ts_code', 'trade_date']\n",
+ "\n",
+ "RangeIndex: 8820754 entries, 0 to 8820753\n",
+ "Data columns (total 33 columns):\n",
+ " # Column Dtype \n",
+ "--- ------ ----- \n",
+ " 0 ts_code object \n",
+ " 1 trade_date datetime64[ns]\n",
+ " 2 open float64 \n",
+ " 3 close float64 \n",
+ " 4 high float64 \n",
+ " 5 low float64 \n",
+ " 6 vol float64 \n",
+ " 7 amount float64 \n",
+ " 8 pct_chg float64 \n",
+ " 9 turnover_rate float64 \n",
+ " 10 pe_ttm float64 \n",
+ " 11 circ_mv float64 \n",
+ " 12 total_mv float64 \n",
+ " 13 volume_ratio float64 \n",
+ " 14 is_st bool \n",
+ " 15 up_limit float64 \n",
+ " 16 down_limit float64 \n",
+ " 17 buy_sm_vol float64 \n",
+ " 18 sell_sm_vol float64 \n",
+ " 19 buy_lg_vol float64 \n",
+ " 20 sell_lg_vol float64 \n",
+ " 21 buy_elg_vol float64 \n",
+ " 22 sell_elg_vol float64 \n",
+ " 23 net_mf_vol float64 \n",
+ " 24 his_low float64 \n",
+ " 25 his_high float64 \n",
+ " 26 cost_5pct float64 \n",
+ " 27 cost_15pct float64 \n",
+ " 28 cost_50pct float64 \n",
+ " 29 cost_85pct float64 \n",
+ " 30 cost_95pct float64 \n",
+ " 31 weight_avg float64 \n",
+ " 32 winner_rate float64 \n",
+ "dtypes: bool(1), datetime64[ns](1), float64(30), object(1)\n",
+ "memory usage: 2.1+ GB\n",
+ "None\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "from main.utils.utils import read_and_merge_h5_data\n",
+ "\n",
+ "print('daily data')\n",
+ "df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/daily_data.h5', key='daily_data',\n",
+ " columns=['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'amount', 'pct_chg'],\n",
+ " df=None)\n",
+ "\n",
+ "print('daily basic')\n",
+ "df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/daily_basic.h5', key='daily_basic',\n",
+ " columns=['ts_code', 'trade_date', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv', 'volume_ratio',\n",
+ " 'is_st'], df=df, join='inner')\n",
+ "\n",
+ "print('stk limit')\n",
+ "df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/stk_limit.h5', key='stk_limit',\n",
+ " columns=['ts_code', 'trade_date', 'pre_close', 'up_limit', 'down_limit'],\n",
+ " df=df)\n",
+ "print('money flow')\n",
+ "df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/money_flow.h5', key='money_flow',\n",
+ " columns=['ts_code', 'trade_date', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol',\n",
+ " 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol'],\n",
+ " df=df)\n",
+ "print('cyq perf')\n",
+ "df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/cyq_perf.h5', key='cyq_perf',\n",
+ " columns=['ts_code', 'trade_date', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct',\n",
+ " 'cost_50pct',\n",
+ " 'cost_85pct', 'cost_95pct', 'weight_avg', 'winner_rate'],\n",
+ " df=df)\n",
+ "print(df.info())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cac01788dac10678",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:47:10.527104Z",
+ "start_time": "2025-04-03T12:47:00.488715Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "industry\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "print('industry')\n",
+ "industry_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/industry_data.h5', key='industry_data',\n",
+ " columns=['ts_code', 'l2_code', 'in_date'],\n",
+ " df=None, on=['ts_code'], join='left')\n",
+ "\n",
+ "\n",
+ "def merge_with_industry_data(df, industry_df):\n",
+ " # 确保日期字段是 datetime 类型\n",
+ " df['trade_date'] = pd.to_datetime(df['trade_date'])\n",
+ " industry_df['in_date'] = pd.to_datetime(industry_df['in_date'])\n",
+ "\n",
+ " # 对 industry_df 按 ts_code 和 in_date 排序\n",
+ " industry_df_sorted = industry_df.sort_values(['in_date', 'ts_code'])\n",
+ "\n",
+ " # 对原始 df 按 ts_code 和 trade_date 排序\n",
+ " df_sorted = df.sort_values(['trade_date', 'ts_code'])\n",
+ "\n",
+ " # 使用 merge_asof 进行向后合并\n",
+ " merged = pd.merge_asof(\n",
+ " df_sorted,\n",
+ " industry_df_sorted,\n",
+ " by='ts_code', # 按 ts_code 分组\n",
+ " left_on='trade_date',\n",
+ " right_on='in_date',\n",
+ " direction='backward'\n",
+ " )\n",
+ "\n",
+ " # 获取每个 ts_code 的最早 in_date 记录\n",
+ " min_in_date_per_ts = (industry_df_sorted\n",
+ " .groupby('ts_code')\n",
+ " .first()\n",
+ " .reset_index()[['ts_code', 'l2_code']])\n",
+ "\n",
+ " # 填充未匹配到的记录(trade_date 早于所有 in_date 的情况)\n",
+ " merged['l2_code'] = merged['l2_code'].fillna(\n",
+ " merged['ts_code'].map(min_in_date_per_ts.set_index('ts_code')['l2_code'])\n",
+ " )\n",
+ "\n",
+ " # 保留需要的列并重置索引\n",
+ " result = merged.reset_index(drop=True)\n",
+ " return result\n",
+ "\n",
+ "\n",
+ "# 使用示例\n",
+ "df = merge_with_industry_data(df, industry_df)\n",
+ "# print(mdf[mdf['ts_code'] == '600751.SH'][['ts_code', 'trade_date', 'l2_code']])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c4e9e1d31da6dba6",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:47:10.719252Z",
+ "start_time": "2025-04-03T12:47:10.541247Z"
+ },
+ "jupyter": {
+ "source_hidden": true
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "from main.factor.factor import *\n",
+ "\n",
+ "def calculate_indicators(df):\n",
+ " \"\"\"\n",
+ " 计算四个指标:当日涨跌幅、5日移动平均、RSI、MACD。\n",
+ " \"\"\"\n",
+ " df = df.sort_values('trade_date')\n",
+ " df['daily_return'] = (df['close'] - df['pre_close']) / df['pre_close'] * 100\n",
+ " # df['5_day_ma'] = df['close'].rolling(window=5).mean()\n",
+ " delta = df['close'].diff()\n",
+ " gain = delta.where(delta > 0, 0)\n",
+ " loss = -delta.where(delta < 0, 0)\n",
+ " avg_gain = gain.rolling(window=14).mean()\n",
+ " avg_loss = loss.rolling(window=14).mean()\n",
+ " rs = avg_gain / avg_loss\n",
+ " df['RSI'] = 100 - (100 / (1 + rs))\n",
+ "\n",
+ " # 计算MACD\n",
+ " ema12 = df['close'].ewm(span=12, adjust=False).mean()\n",
+ " ema26 = df['close'].ewm(span=26, adjust=False).mean()\n",
+ " df['MACD'] = ema12 - ema26\n",
+ " df['Signal_line'] = df['MACD'].ewm(span=9, adjust=False).mean()\n",
+ " df['MACD_hist'] = df['MACD'] - df['Signal_line']\n",
+ "\n",
+ " # 4. 情绪因子1:市场上涨比例(Up Ratio)\n",
+ " df['up_ratio'] = df['daily_return'].apply(lambda x: 1 if x > 0 else 0)\n",
+ " df['up_ratio_20d'] = df['up_ratio'].rolling(window=20).mean() # 过去20天上涨比例\n",
+ "\n",
+ " # 5. 情绪因子2:成交量变化率(Volume Change Rate)\n",
+ " df['volume_mean'] = df['vol'].rolling(window=20).mean() # 过去20天的平均成交量\n",
+ " df['volume_change_rate'] = (df['vol'] - df['volume_mean']) / df['volume_mean'] * 100 # 成交量变化率\n",
+ "\n",
+ " # 6. 情绪因子3:波动率(Volatility)\n",
+ " df['volatility'] = df['daily_return'].rolling(window=20).std() # 过去20天的日收益率标准差\n",
+ "\n",
+ " # 7. 情绪因子4:成交额变化率(Amount Change Rate)\n",
+ " df['amount_mean'] = df['amount'].rolling(window=20).mean() # 过去20天的平均成交额\n",
+ " df['amount_change_rate'] = (df['amount'] - df['amount_mean']) / df['amount_mean'] * 100 # 成交额变化率\n",
+ "\n",
+ " # df = sentiment_panic_greed_index(df)\n",
+ " # df = sentiment_market_breadth_proxy(df)\n",
+ " # df = sentiment_reversal_indicator(df)\n",
+ "\n",
+ " return df\n",
+ "\n",
+ "\n",
+ "def generate_index_indicators(h5_filename):\n",
+ " df = pd.read_hdf(h5_filename, key='index_data')\n",
+ " df['trade_date'] = pd.to_datetime(df['trade_date'], format='%Y%m%d')\n",
+ " df = df.sort_values('trade_date')\n",
+ "\n",
+ " # 计算每个ts_code的相关指标\n",
+ " df_indicators = []\n",
+ " for ts_code in df['ts_code'].unique():\n",
+ " df_index = df[df['ts_code'] == ts_code].copy()\n",
+ " df_index = calculate_indicators(df_index)\n",
+ " df_indicators.append(df_index)\n",
+ "\n",
+ " # 合并所有指数的结果\n",
+ " df_all_indicators = pd.concat(df_indicators, ignore_index=True)\n",
+ "\n",
+ " # 保留trade_date列,并将同一天的数据按ts_code合并成一行\n",
+ " df_final = df_all_indicators.pivot_table(\n",
+ " index='trade_date',\n",
+ " columns='ts_code',\n",
+ " values=['daily_return', \n",
+ " 'RSI', 'MACD', 'Signal_line', 'MACD_hist', \n",
+ " # 'sentiment_panic_greed_index',\n",
+ " 'up_ratio_20d', 'volume_change_rate', 'volatility',\n",
+ " 'amount_change_rate', 'amount_mean'],\n",
+ " aggfunc='last'\n",
+ " )\n",
+ "\n",
+ " df_final.columns = [f\"{col[1]}_{col[0]}\" for col in df_final.columns]\n",
+ " df_final = df_final.reset_index()\n",
+ "\n",
+ " return df_final\n",
+ "\n",
+ "\n",
+ "# 使用函数\n",
+ "h5_filename = '/mnt/d/PyProject/NewStock/data/index_data.h5'\n",
+ "index_data = generate_index_indicators(h5_filename)\n",
+ "index_data = index_data.dropna()\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a735bc02ceb4d872",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:47:10.821169Z",
+ "start_time": "2025-04-03T12:47:10.751831Z"
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "import talib\n",
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "53f86ddc0677a6d7",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:47:15.944254Z",
+ "start_time": "2025-04-03T12:47:10.826179Z"
+ },
+ "jupyter": {
+ "source_hidden": true
+ },
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "from main.utils.factor import get_act_factor\n",
+ "\n",
+ "\n",
+ "def read_industry_data(h5_filename):\n",
+ " # 读取 H5 文件中所有的行业数据\n",
+ " industry_data = pd.read_hdf(h5_filename, key='sw_daily', columns=[\n",
+ " 'ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'pe', 'pb', 'vol'\n",
+ " ]) # 假设 H5 文件的键是 'industry_data'\n",
+ " industry_data = industry_data.sort_values(by=['ts_code', 'trade_date'])\n",
+ " industry_data = industry_data.reindex()\n",
+ " industry_data['trade_date'] = pd.to_datetime(industry_data['trade_date'], format='%Y%m%d')\n",
+ "\n",
+ " grouped = industry_data.groupby('ts_code', group_keys=False)\n",
+ " industry_data['obv'] = grouped.apply(\n",
+ " lambda x: pd.Series(talib.OBV(x['close'].values, x['vol'].values), index=x.index)\n",
+ " )\n",
+ " industry_data['return_5'] = grouped['close'].apply(lambda x: x / x.shift(5) - 1)\n",
+ " industry_data['return_20'] = grouped['close'].apply(lambda x: x / x.shift(20) - 1)\n",
+ "\n",
+ " industry_data = get_act_factor(industry_data, cat=False)\n",
+ " industry_data = industry_data.sort_values(by=['trade_date', 'ts_code'])\n",
+ "\n",
+ " # # 计算每天每个 ts_code 的因子和当天所有 ts_code 的中位数的偏差\n",
+ " # factor_columns = ['obv', 'return_5', 'return_20', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4'] # 因子列\n",
+ " # \n",
+ " # for factor in factor_columns:\n",
+ " # if factor in industry_data.columns:\n",
+ " # # 计算每天每个 ts_code 的因子值与当天所有 ts_code 的中位数的偏差\n",
+ " # industry_data[f'{factor}_deviation'] = industry_data.groupby('trade_date')[factor].transform(\n",
+ " # lambda x: x - x.mean())\n",
+ "\n",
+ " industry_data['return_5_percentile'] = industry_data.groupby('trade_date')['return_5'].transform(\n",
+ " lambda x: x.rank(pct=True))\n",
+ " industry_data['return_20_percentile'] = industry_data.groupby('trade_date')['return_20'].transform(\n",
+ " lambda x: x.rank(pct=True))\n",
+ "\n",
+ " # cs_rank_intraday_range(industry_data)\n",
+ " # cs_rank_close_pos_in_range(industry_data)\n",
+ "\n",
+ " industry_data = industry_data.drop(columns=['open', 'close', 'high', 'low', 'pe', 'pb', 'vol'])\n",
+ "\n",
+ " industry_data = industry_data.rename(\n",
+ " columns={col: f'industry_{col}' for col in industry_data.columns if col not in ['ts_code', 'trade_date']})\n",
+ "\n",
+ " industry_data = industry_data.rename(columns={'ts_code': 'cat_l2_code'})\n",
+ " return industry_data\n",
+ "\n",
+ "\n",
+ "industry_df = read_industry_data('/mnt/d/PyProject/NewStock/data/sw_daily.h5')\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "dbe2fd8021b9417f",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:47:15.969344Z",
+ "start_time": "2025-04-03T12:47:15.963327Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "['ts_code', 'open', 'close', 'high', 'low', 'amount', 'circ_mv', 'total_mv', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg', 'in_date']\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "origin_columns = df.columns.tolist()\n",
+ "origin_columns = [col for col in origin_columns if\n",
+ " col not in ['turnover_rate', 'pe_ttm', 'volume_ratio', 'vol', 'pct_chg', 'l2_code', 'winner_rate']]\n",
+ "origin_columns = [col for col in origin_columns if col not in index_data.columns]\n",
+ "origin_columns = [col for col in origin_columns if 'cyq' not in col]\n",
+ "print(origin_columns)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "85c3e3d0235ffffa",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T12:47:16.089879Z",
+ "start_time": "2025-04-03T12:47:15.990101Z"
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "fina_indicator_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/fina_indicator.h5', key='fina_indicator',\n",
+ " columns=['ts_code', 'ann_date', 'undist_profit_ps', 'ocfps', 'bps', 'roa', 'roe'],\n",
+ " df=None)\n",
+ "cashflow_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/cashflow.h5', key='cashflow',\n",
+ " columns=['ts_code', 'ann_date', 'n_cashflow_act'],\n",
+ " df=None)\n",
+ "balancesheet_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/balancesheet.h5', key='balancesheet',\n",
+ " columns=['ts_code', 'ann_date', 'money_cap', 'total_liab'],\n",
+ " df=None)\n",
+ "top_list_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/top_list.h5', key='top_list',\n",
+ " columns=['ts_code', 'trade_date', 'reason'],\n",
+ " df=None)\n",
+ "\n",
+ "top_list_df = top_list_df.sort_values(by='trade_date', ascending=False).drop_duplicates(subset=['ts_code', 'trade_date'], keep='first').sort_values(by='trade_date')\n",
+ "\n",
+ "stk_holdertrade_df = read_and_merge_h5_data('/mnt/d/PyProject/NewStock/data/stk_holdertrade.h5', key='stk_holdertrade',\n",
+ " columns=['ts_code', 'ann_date', 'in_de', 'change_ratio', 'after_ratio'],\n",
+ " df=None)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "92d84ce15a562ec6",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T13:08:01.612695Z",
+ "start_time": "2025-04-03T12:47:16.121802Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n",
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n",
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n",
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n",
+ "开始计算因子: AR, BR (原地修改)...\n",
+ "因子 AR, BR 计算成功。\n",
+ "因子 AR, BR 计算流程结束。\n",
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "使用 'ann_date' 作为财务数据生效日期。\n",
+ "警告: 从 financial_data_subset 中移除了 366 行,因为其 'ts_code' 或 'ann_date' 列存在空值。\n",
+ "计算 BBI...\n",
+ "--- 计算日级别偏离度 (使用 pct_chg) ---\n",
+ "--- 计算日级别动量基准 (使用 pct_chg) ---\n",
+ "日级别动量基准计算完成 (使用 pct_chg)。\n",
+ "日级别偏离度计算完成 (使用 pct_chg)。\n",
+ "--- 计算日级别行业偏离度 (使用 pct_chg 和行业基准) ---\n",
+ "--- 计算日级别行业动量基准 (使用 pct_chg 和 cat_l2_code) ---\n",
+ "错误: 计算日级别行业动量基准需要以下列: ['pct_chg', 'cat_l2_code', 'trade_date', 'ts_code']。\n",
+ "错误: 计算日级别行业偏离度需要以下列: ['pct_chg', 'daily_industry_positive_benchmark', 'daily_industry_negative_benchmark']。请先运行 daily_industry_momentum_benchmark(df)。\n",
+ "Index(['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol',\n",
+ " 'amount', 'pct_chg', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv',\n",
+ " 'volume_ratio', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol',\n",
+ " 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol',\n",
+ " 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct',\n",
+ " 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg',\n",
+ " 'winner_rate', 'l2_code', 'undist_profit_ps', 'ocfps', 'roa', 'roe',\n",
+ " 'AR', 'BR', 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor',\n",
+ " 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20',\n",
+ " 'bbi_ratio_factor', 'daily_deviation', 'lg_elg_net_buy_vol',\n",
+ " 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'flow_divergence_diff',\n",
+ " 'flow_divergence_ratio', 'total_buy_vol', 'lg_elg_buy_prop',\n",
+ " 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change',\n",
+ " 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness',\n",
+ " 'floating_chip_proxy', 'cost_support_15pct_change',\n",
+ " 'cat_winner_price_zone', 'flow_chip_consistency',\n",
+ " 'profit_taking_vs_absorb', '_is_positive', '_is_negative',\n",
+ " 'cat_is_positive', '_pos_returns', '_neg_returns', '_pos_returns_sq',\n",
+ " '_neg_returns_sq', 'upside_vol', 'downside_vol', 'vol_ratio',\n",
+ " 'return_skew', 'return_kurtosis', 'volume_change_rate',\n",
+ " 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike',\n",
+ " 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike',\n",
+ " 'vol_std_5', 'atr_14', 'atr_6', 'obv'],\n",
+ " dtype='object')\n",
+ "Calculating lg_flow_mom_corr_20_60...\n",
+ "Finished lg_flow_mom_corr_20_60.\n",
+ "Calculating lg_flow_accel...\n",
+ "Finished lg_flow_accel.\n",
+ "Calculating profit_pressure...\n",
+ "Finished profit_pressure.\n",
+ "Calculating underwater_resistance...\n",
+ "Finished underwater_resistance.\n",
+ "Calculating cost_conc_std_20...\n",
+ "Finished cost_conc_std_20.\n",
+ "Calculating profit_decay_20...\n",
+ "Finished profit_decay_20.\n",
+ "Calculating vol_amp_loss_20...\n",
+ "Finished vol_amp_loss_20.\n",
+ "Calculating vol_drop_profit_cnt_5...\n",
+ "Finished vol_drop_profit_cnt_5.\n",
+ "Calculating lg_flow_vol_interact_20...\n",
+ "Finished lg_flow_vol_interact_20.\n",
+ "Calculating cost_break_confirm_cnt_5...\n",
+ "Finished cost_break_confirm_cnt_5.\n",
+ "Calculating atr_norm_channel_pos_14...\n",
+ "Finished atr_norm_channel_pos_14.\n",
+ "Calculating turnover_diff_skew_20...\n",
+ "Finished turnover_diff_skew_20.\n",
+ "Calculating lg_sm_flow_diverge_20...\n",
+ "Finished lg_sm_flow_diverge_20.\n",
+ "Calculating pullback_strong_20_20...\n",
+ "Finished pullback_strong_20_20.\n",
+ "Calculating vol_wgt_hist_pos_20...\n",
+ "Finished vol_wgt_hist_pos_20.\n",
+ "Calculating vol_adj_roc_20...\n",
+ "Finished vol_adj_roc_20.\n",
+ "Calculating cs_rank_net_lg_flow_val...\n",
+ "Finished cs_rank_net_lg_flow_val.\n",
+ "Calculating cs_rank_flow_divergence...\n",
+ "Finished cs_rank_flow_divergence.\n",
+ "Calculating cs_rank_ind_adj_lg_flow...\n",
+ "Finished cs_rank_ind_adj_lg_flow.\n",
+ "Calculating cs_rank_elg_buy_ratio...\n",
+ "Finished cs_rank_elg_buy_ratio.\n",
+ "Calculating cs_rank_rel_profit_margin...\n",
+ "Finished cs_rank_rel_profit_margin.\n",
+ "Calculating cs_rank_cost_breadth...\n",
+ "Finished cs_rank_cost_breadth.\n",
+ "Calculating cs_rank_dist_to_upper_cost...\n",
+ "Finished cs_rank_dist_to_upper_cost.\n",
+ "Calculating cs_rank_winner_rate...\n",
+ "Finished cs_rank_winner_rate.\n",
+ "Calculating cs_rank_intraday_range...\n",
+ "Finished cs_rank_intraday_range.\n",
+ "Calculating cs_rank_close_pos_in_range...\n",
+ "Finished cs_rank_close_pos_in_range.\n",
+ "Calculating cs_rank_opening_gap...\n",
+ "Error calculating cs_rank_opening_gap: Missing 'pre_close' column. Assigning NaN.\n",
+ "Calculating cs_rank_pos_in_hist_range...\n",
+ "Finished cs_rank_pos_in_hist_range.\n",
+ "Calculating cs_rank_vol_x_profit_margin...\n",
+ "Finished cs_rank_vol_x_profit_margin.\n",
+ "Calculating cs_rank_lg_flow_price_concordance...\n",
+ "Finished cs_rank_lg_flow_price_concordance.\n",
+ "Calculating cs_rank_turnover_per_winner...\n",
+ "Finished cs_rank_turnover_per_winner.\n",
+ "Calculating cs_rank_ind_cap_neutral_pe (Placeholder - requires statsmodels)...\n",
+ "Finished cs_rank_ind_cap_neutral_pe (Placeholder).\n",
+ "Calculating cs_rank_volume_ratio...\n",
+ "Finished cs_rank_volume_ratio.\n",
+ "Calculating cs_rank_elg_buy_sell_sm_ratio...\n",
+ "Finished cs_rank_elg_buy_sell_sm_ratio.\n",
+ "Calculating cs_rank_cost_dist_vol_ratio...\n",
+ "Finished cs_rank_cost_dist_vol_ratio.\n",
+ "Calculating cs_rank_size...\n",
+ "Finished cs_rank_size.\n",
+ "\n",
+ "RangeIndex: 4554725 entries, 0 to 4554724\n",
+ "Columns: 181 entries, ts_code to cs_rank_size\n",
+ "dtypes: bool(10), datetime64[ns](1), float64(165), int64(3), object(2)\n",
+ "memory usage: 5.8+ GB\n",
+ "None\n",
+ "['ts_code', 'trade_date', 'open', 'close', 'high', 'low', 'vol', 'amount', 'pct_chg', 'turnover_rate', 'pe_ttm', 'circ_mv', 'total_mv', 'volume_ratio', 'is_st', 'up_limit', 'down_limit', 'buy_sm_vol', 'sell_sm_vol', 'buy_lg_vol', 'sell_lg_vol', 'buy_elg_vol', 'sell_elg_vol', 'net_mf_vol', 'his_low', 'his_high', 'cost_5pct', 'cost_15pct', 'cost_50pct', 'cost_85pct', 'cost_95pct', 'weight_avg', 'winner_rate', 'cat_l2_code', 'undist_profit_ps', 'ocfps', 'roa', 'roe', 'AR', 'BR', 'AR_BR', 'log_circ_mv', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'daily_deviation', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'flow_divergence_diff', 'flow_divergence_ratio', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'price_cost_divergence', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', 'cs_rank_net_lg_flow_val', 'cs_rank_flow_divergence', 'cs_rank_ind_adj_lg_flow', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_opening_gap', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_ind_cap_neutral_pe', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size']\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "\n",
+ "import numpy as np\n",
+ "from main.factor.factor import *\n",
+ "from main.factor.money_factor import *\n",
+ "\n",
+ "\n",
+ "def filter_data(df):\n",
+ " # df = df.groupby('trade_date').apply(lambda x: x.nlargest(1000, 'act_factor1'))\n",
+ " df = df[df['trade_date'] <= '2025-06-01']\n",
+ " df = df[~df['is_st']]\n",
+ " df = df[~df['ts_code'].str.endswith('BJ')]\n",
+ " df = df[~df['ts_code'].str.startswith('30')]\n",
+ " df = df[~df['ts_code'].str.startswith('68')]\n",
+ " df = df[~df['ts_code'].str.startswith('8')]\n",
+ " df = df[df['trade_date'] >= '2019-01-01']\n",
+ " if 'in_date' in df.columns:\n",
+ " df = df.drop(columns=['in_date'])\n",
+ " df = df.reset_index(drop=True)\n",
+ " return df\n",
+ "\n",
+ "gc.collect()\n",
+ "\n",
+ "df = filter_data(df)\n",
+ "df = df.sort_values(by=['ts_code', 'trade_date'])\n",
+ "\n",
+ "# df = price_minus_deduction_price(df, n=120)\n",
+ "# df = price_deduction_price_diff_ratio_to_sma(df, n=120)\n",
+ "# df = cat_price_vs_sma_vs_deduction_price(df, n=120)\n",
+ "# df = cat_reason(df, top_list_df)\n",
+ "# df = cat_is_on_top_list(df, top_list_df)\n",
+ "\n",
+ "# df = ts_turnover_rate_acceleration_5_20(df)\n",
+ "# df = ts_vol_sustain_10_30(df)\n",
+ "# df = cs_turnover_rate_relative_strength_20(df)\n",
+ "# df = cs_amount_outlier_10(df)\n",
+ "# df = holder_trade_factors(stk_holdertrade_df, df)\n",
+ "\n",
+ "df = add_financial_factor(df, fina_indicator_df, factor_value_col='undist_profit_ps')\n",
+ "df = add_financial_factor(df, fina_indicator_df, factor_value_col='ocfps')\n",
+ "df = add_financial_factor(df, fina_indicator_df, factor_value_col='roa')\n",
+ "df = add_financial_factor(df, fina_indicator_df, factor_value_col='roe')\n",
+ "\n",
+ "calculate_arbr(df, N=26)\n",
+ "df['log_circ_mv'] = np.log(df['circ_mv'])\n",
+ "df = calculate_cashflow_to_ev_factor(df, cashflow_df, balancesheet_df)\n",
+ "df = caculate_book_to_price_ratio(df, fina_indicator_df)\n",
+ "\n",
+ "df = turnover_rate_n(df, n=5)\n",
+ "df = variance_n(df, n=20)\n",
+ "df = bbi_ratio_factor(df)\n",
+ "df = daily_deviation(df)\n",
+ "df = daily_industry_deviation(df)\n",
+ "df, _ = get_rolling_factor(df)\n",
+ "df, _ = get_simple_factor(df)\n",
+ "\n",
+ "df = df.rename(columns={'l1_code': 'cat_l1_code'})\n",
+ "df = df.rename(columns={'l2_code': 'cat_l2_code'})\n",
+ "\n",
+ "lg_flow_mom_corr(df, N=20, M=60)\n",
+ "lg_flow_accel(df)\n",
+ "profit_pressure(df)\n",
+ "underwater_resistance(df)\n",
+ "cost_conc_std(df, N=20)\n",
+ "profit_decay(df, N=20)\n",
+ "vol_amp_loss(df, N=20)\n",
+ "vol_drop_profit_cnt(df, N=20, M=5)\n",
+ "lg_flow_vol_interact(df, N=20)\n",
+ "cost_break_confirm_cnt(df, M=5)\n",
+ "atr_norm_channel_pos(df, N=14)\n",
+ "turnover_diff_skew(df, N=20)\n",
+ "lg_sm_flow_diverge(df, N=20)\n",
+ "pullback_strong(df, N=20, M=20)\n",
+ "vol_wgt_hist_pos(df, N=20)\n",
+ "vol_adj_roc(df, N=20)\n",
+ "\n",
+ "cs_rank_net_lg_flow_val(df)\n",
+ "cs_rank_flow_divergence(df)\n",
+ "cs_rank_industry_adj_lg_flow(df) # Needs cat_l2_code\n",
+ "cs_rank_elg_buy_ratio(df)\n",
+ "cs_rank_rel_profit_margin(df)\n",
+ "cs_rank_cost_breadth(df)\n",
+ "cs_rank_dist_to_upper_cost(df)\n",
+ "cs_rank_winner_rate(df)\n",
+ "cs_rank_intraday_range(df)\n",
+ "cs_rank_close_pos_in_range(df)\n",
+ "cs_rank_opening_gap(df) # Needs pre_close\n",
+ "cs_rank_pos_in_hist_range(df) # Needs his_low, his_high\n",
+ "cs_rank_vol_x_profit_margin(df)\n",
+ "cs_rank_lg_flow_price_concordance(df)\n",
+ "cs_rank_turnover_per_winner(df)\n",
+ "cs_rank_ind_cap_neutral_pe(df) # Placeholder - needs external libraries\n",
+ "cs_rank_volume_ratio(df) # Needs volume_ratio\n",
+ "cs_rank_elg_buy_sell_sm_ratio(df)\n",
+ "cs_rank_cost_dist_vol_ratio(df) # Needs volume_ratio\n",
+ "cs_rank_size(df) # Needs circ_mv\n",
+ "\n",
+ "\n",
+ "# df = df.merge(index_data, on='trade_date', how='left')\n",
+ "\n",
+ "print(df.info())\n",
+ "print(df.columns.tolist())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b87b938028afa206",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T13:08:03.658725Z",
+ "start_time": "2025-04-03T13:08:02.469611Z"
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "from scipy.stats import ks_2samp, wasserstein_distance\n",
+ "\n",
+ "\n",
+ "def remove_shifted_features(train_data, test_data, feature_columns, ks_threshold=0.05, wasserstein_threshold=0.1,\n",
+ " importance_threshold=0.05):\n",
+ " dropped_features = []\n",
+ "\n",
+ " # **统计数据漂移**\n",
+ " numeric_columns = train_data.select_dtypes(include=['float64', 'int64']).columns\n",
+ " numeric_columns = [col for col in numeric_columns if col in feature_columns]\n",
+ " for feature in numeric_columns:\n",
+ " ks_stat, p_value = ks_2samp(train_data[feature], test_data[feature])\n",
+ " wasserstein_dist = wasserstein_distance(train_data[feature], test_data[feature])\n",
+ "\n",
+ " if p_value < ks_threshold or wasserstein_dist > wasserstein_threshold:\n",
+ " dropped_features.append(feature)\n",
+ "\n",
+ " print(f\"检测到 {len(dropped_features)} 个可能漂移的特征: {dropped_features}\")\n",
+ "\n",
+ " # **应用阈值进行最终筛选**\n",
+ " filtered_features = [f for f in feature_columns if f not in dropped_features]\n",
+ "\n",
+ " return filtered_features, dropped_features\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f4f16d63ad18d1bc",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T13:08:03.670700Z",
+ "start_time": "2025-04-03T13:08:03.665739Z"
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "import statsmodels.api as sm # 用于中性化回归\n",
+ "from tqdm import tqdm # 可选,用于显示进度条\n",
+ "\n",
+ "# --- 常量 ---\n",
+ "epsilon = 1e-10 # 防止除零\n",
+ "\n",
+ "# --- 1. 中位数去极值 (MAD) ---\n",
+ "\n",
+ "def cs_mad_filter(df: pd.DataFrame,\n",
+ " features: list,\n",
+ " k: float = 3.0,\n",
+ " scale_factor: float = 1.4826):\n",
+ " \"\"\"\n",
+ " 对指定特征列进行截面 MAD 去极值处理 (原地修改)。\n",
+ "\n",
+ " 方法: 对每日截面数据,计算 median 和 MAD,\n",
+ " 将超出 [median - k * scale * MAD, median + k * scale * MAD] 范围的值\n",
+ " 替换为边界值 (Winsorization)。\n",
+ " scale_factor=1.4826 使得 MAD 约等于正态分布的标准差。\n",
+ "\n",
+ " Args:\n",
+ " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。\n",
+ " features (list): 需要处理的特征列名列表。\n",
+ " k (float): MAD 的倍数,用于确定边界。默认为 3.0。\n",
+ " scale_factor (float): MAD 的缩放因子。默认为 1.4826。\n",
+ "\n",
+ " WARNING: 此函数会原地修改输入的 DataFrame 'df'。\n",
+ " \"\"\"\n",
+ " print(f\"开始截面 MAD 去极值处理 (k={k})...\")\n",
+ " if not all(col in df.columns for col in features):\n",
+ " missing = [col for col in features if col not in df.columns]\n",
+ " print(f\"错误: DataFrame 中缺少以下特征列: {missing}。跳过去极值处理。\")\n",
+ " return\n",
+ "\n",
+ " grouped = df.groupby('trade_date')\n",
+ "\n",
+ " for col in tqdm(features, desc=\"MAD Filtering\"):\n",
+ " try:\n",
+ " # 计算截面中位数\n",
+ " median = grouped[col].transform('median')\n",
+ " # 计算截面 MAD (Median Absolute Deviation from Median)\n",
+ " mad = (df[col] - median).abs().groupby(df['trade_date']).transform('median')\n",
+ "\n",
+ " # 计算上下边界\n",
+ " lower_bound = median - k * scale_factor * mad\n",
+ " upper_bound = median + k * scale_factor * mad\n",
+ "\n",
+ " # 原地应用 clip\n",
+ " df[col] = np.clip(df[col], lower_bound, upper_bound)\n",
+ "\n",
+ " except KeyError:\n",
+ " print(f\"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的 MAD 处理。\")\n",
+ " except Exception as e:\n",
+ " print(f\"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的 MAD 处理。\")\n",
+ "\n",
+ " print(\"截面 MAD 去极值处理完成。\")\n",
+ "\n",
+ "\n",
+ "# --- 2. 行业市值中性化 ---\n",
+ "\n",
+ "from tqdm import tqdm\n",
+ "\n",
+ "def cs_neutralize_market_cap_numpy(df: pd.DataFrame,\n",
+ " features: list,\n",
+ " market_cap_col: str = 'circ_mv'):\n",
+ " \"\"\"\n",
+ " 对 DataFrame 中的指定特征进行截面市值中性化 (NumPy 优化)。\n",
+ "\n",
+ " Args:\n",
+ " df (pd.DataFrame): 包含数据的 DataFrame,需要有 'trade_date' 和 market_cap_col 列。\n",
+ " features (list): 需要进行市值中性化的特征列名列表。\n",
+ " market_cap_col (str): 包含市值数据的列名,默认为 'circ_mv'。\n",
+ " \"\"\"\n",
+ " print(\"开始截面市值中性化 (NumPy 优化)...\")\n",
+ " required_cols = features + ['trade_date', market_cap_col]\n",
+ " if not all(col in df.columns for col in required_cols):\n",
+ " missing = [col for col in required_cols if col not in df.columns]\n",
+ " print(f\"错误: DataFrame 中缺少必需列: {missing}。无法进行中性化。\")\n",
+ " return\n",
+ "\n",
+ " df_copy = df\n",
+ " log_cap_col = '_log_market_cap'\n",
+ " df_copy[log_cap_col] = np.log1p(df_copy[market_cap_col])\n",
+ "\n",
+ " # 创建一个 DataFrame 来存储所有日期的残差结果\n",
+ " residuals_container = pd.DataFrame(index=df_copy.index, columns=features, dtype=float)\n",
+ "\n",
+ " for date, group_df in tqdm(df_copy.groupby('trade_date'), desc=\"Neutralizing by Date (NumPy)\"):\n",
+ " # 准备 X 矩阵 (自变量):常数项和对数市值\n",
+ " X_daily = np.concatenate([np.ones((len(group_df), 1)), group_df[[log_cap_col]].values], axis=1)\n",
+ "\n",
+ " for feature_col in features:\n",
+ " Y_daily = group_df[feature_col].values\n",
+ "\n",
+ " # 处理 NaN:只对有效数据对进行回归\n",
+ " valid_mask_y = ~np.isnan(Y_daily)\n",
+ " valid_mask_x = ~np.isnan(X_daily).any(axis=1)\n",
+ " valid_mask = valid_mask_y & valid_mask_x\n",
+ "\n",
+ " current_feature_indices = group_df.index[valid_mask]\n",
+ "\n",
+ " if np.sum(valid_mask) < X_daily.shape[1] + 1:\n",
+ " # 有效数据不足,此特征在此日期保持 NaN\n",
+ " continue\n",
+ "\n",
+ " Y_valid = Y_daily[valid_mask]\n",
+ " X_valid = X_daily[valid_mask, :]\n",
+ "\n",
+ " try:\n",
+ " # 使用 np.linalg.lstsq 进行 OLS 计算\n",
+ " beta, sum_sq_resid, rank, s = np.linalg.lstsq(X_valid, Y_valid, rcond=None)\n",
+ "\n",
+ " # 计算预测值 Y_hat = X_valid @ beta\n",
+ " Y_hat_valid = X_valid @ beta\n",
+ "\n",
+ " # 计算残差 residuals = Y_valid - Y_hat_valid\n",
+ " residuals_valid = Y_valid - Y_hat_valid\n",
+ "\n",
+ " # 将计算得到的残差放回 residuals_container\n",
+ " residuals_container.loc[current_feature_indices, feature_col] = residuals_valid\n",
+ "\n",
+ " except np.linalg.LinAlgError:\n",
+ " pass\n",
+ " except Exception as e:\n",
+ " pass\n",
+ "\n",
+ " # 将所有计算得到的残差更新回原始的 df (原地修改)\n",
+ " for feature_col in features:\n",
+ " df[feature_col] = residuals_container[feature_col]\n",
+ "\n",
+ " # 清理临时列\n",
+ " df.drop(columns=[log_cap_col], inplace=True, errors='ignore')\n",
+ " print(\"截面市值中性化完成 (NumPy 优化)。\")\n",
+ "\n",
+ "# --- 3. Z-Score 标准化 ---\n",
+ "\n",
+ "def cs_zscore_standardize(df: pd.DataFrame, features: list, epsilon: float = 1e-10):\n",
+ " \"\"\"\n",
+ " 对指定特征列进行截面 Z-Score 标准化 (原地修改)。\n",
+ " 方法: Z = (value - cross_sectional_mean) / (cross_sectional_std + epsilon)\n",
+ "\n",
+ " Args:\n",
+ " df (pd.DataFrame): 输入 DataFrame,需包含 'trade_date' 和 features 列。\n",
+ " features (list): 需要处理的特征列名列表。\n",
+ " epsilon (float): 防止除以零的小常数。\n",
+ "\n",
+ " WARNING: 此函数会原地修改输入的 DataFrame 'df'。\n",
+ " \"\"\"\n",
+ " print(\"开始截面 Z-Score 标准化...\")\n",
+ " if not all(col in df.columns for col in features):\n",
+ " missing = [col for col in features if col not in df.columns]\n",
+ " print(f\"错误: DataFrame 中缺少以下特征列: {missing}。跳过标准化处理。\")\n",
+ " return\n",
+ "\n",
+ " grouped = df.groupby('trade_date')\n",
+ "\n",
+ " for col in tqdm(features, desc=\"Standardizing\"):\n",
+ " try:\n",
+ " # 使用 transform 计算截面均值和标准差\n",
+ " mean = grouped[col].transform('mean')\n",
+ " std = grouped[col].transform('std')\n",
+ "\n",
+ " # 计算 Z-Score 并原地赋值\n",
+ " df[col] = (df[col] - mean) / (std + epsilon)\n",
+ "\n",
+ " except KeyError:\n",
+ " print(f\"警告: 列 '{col}' 可能不存在或在分组中出错,跳过此列的标准化处理。\")\n",
+ " except Exception as e:\n",
+ " print(f\"警告: 处理列 '{col}' 时发生错误: {e},跳过此列的标准化处理。\")\n",
+ "\n",
+ " print(\"截面 Z-Score 标准化完成。\")\n",
+ "\n",
+ "def fill_nan_with_daily_median(df: pd.DataFrame, feature_columns: list[str]) -> pd.DataFrame:\n",
+ " \"\"\"\n",
+ " 对指定特征列进行每日截面中位数填充缺失值 (NaN)。\n",
+ "\n",
+ " 参数:\n",
+ " df (pd.DataFrame): 包含多日数据的DataFrame,需要包含 'trade_date' 和 feature_columns 中的列。\n",
+ " feature_columns (list[str]): 需要进行缺失值填充的特征列名称列表。\n",
+ "\n",
+ " 返回:\n",
+ " pd.DataFrame: 包含缺失值填充后特征列的DataFrame。在输入DataFrame的副本上操作。\n",
+ " \"\"\"\n",
+ " processed_df = df.copy() # 在副本上操作,保留原始数据\n",
+ "\n",
+ " # 确保 trade_date 是 datetime 类型以便正确分组\n",
+ " processed_df['trade_date'] = pd.to_datetime(processed_df['trade_date'])\n",
+ "\n",
+ " def _fill_daily_nan(group):\n",
+ " # group 是某一个交易日的 DataFrame\n",
+ "\n",
+ " # 遍历指定的特征列\n",
+ " for feature_col in feature_columns:\n",
+ " # 检查列是否存在于当前分组中\n",
+ " if feature_col in group.columns:\n",
+ " # 计算当日该特征的中位数\n",
+ " median_val = group[feature_col].median()\n",
+ "\n",
+ " # 使用当日中位数填充该特征列的 NaN 值\n",
+ " # inplace=True 会直接修改 group DataFrame\n",
+ " group[feature_col].fillna(median_val, inplace=True)\n",
+ " # else:\n",
+ " # print(f\"Warning: Feature column '{feature_col}' not found in daily group for {group['trade_date'].iloc[0]}. Skipping.\")\n",
+ "\n",
+ " return group\n",
+ "\n",
+ " # 按交易日期分组,并应用每日填充函数\n",
+ " # group_keys=False 避免将分组键添加到结果索引中\n",
+ " filled_df = processed_df.groupby('trade_date', group_keys=False).apply(_fill_daily_nan)\n",
+ "\n",
+ " return filled_df"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "40e6b68a91b30c79",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T13:08:04.694262Z",
+ "start_time": "2025-04-03T13:08:03.694904Z"
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "def remove_outliers_label_percentile(label: pd.Series, lower_percentile: float = 0.01, upper_percentile: float = 0.99,\n",
+ " log=True):\n",
+ " if not (0 <= lower_percentile < upper_percentile <= 1):\n",
+ " raise ValueError(\"Percentile values must satisfy 0 <= lower_percentile < upper_percentile <= 1.\")\n",
+ "\n",
+ " # Calculate lower and upper bounds based on percentiles\n",
+ " lower_bound = label.quantile(lower_percentile)\n",
+ " upper_bound = label.quantile(upper_percentile)\n",
+ "\n",
+ " # Filter out values outside the bounds\n",
+ " filtered_label = label[(label >= lower_bound) & (label <= upper_bound)]\n",
+ "\n",
+ " # Print the number of removed outliers\n",
+ " if log:\n",
+ " print(f\"Removed {len(label) - len(filtered_label)} outliers.\")\n",
+ " return filtered_label\n",
+ "\n",
+ "\n",
+ "def calculate_risk_adjusted_target(df, days=5):\n",
+ " df = df.sort_values(by=['ts_code', 'trade_date'])\n",
+ "\n",
+ " df['future_close'] = df.groupby('ts_code')['close'].shift(-days)\n",
+ " df['future_open'] = df.groupby('ts_code')['open'].shift(-1)\n",
+ " df['future_return'] = (df['future_close'] - df['future_open']) / df['future_open']\n",
+ "\n",
+ " df['future_volatility'] = df.groupby('ts_code')['future_return'].rolling(days, min_periods=1).std().reset_index(\n",
+ " level=0, drop=True)\n",
+ " sharpe_ratio = df['future_return'] * df['future_volatility']\n",
+ " sharpe_ratio.replace([np.inf, -np.inf], np.nan, inplace=True)\n",
+ "\n",
+ " return sharpe_ratio\n",
+ "\n",
+ "\n",
+ "def calculate_score(df, days=5, lambda_param=1.0):\n",
+ " def calculate_max_drawdown(prices):\n",
+ " peak = prices.iloc[0] # 初始化峰值\n",
+ " max_drawdown = 0 # 初始化最大回撤\n",
+ "\n",
+ " for price in prices:\n",
+ " if price > peak:\n",
+ " peak = price # 更新峰值\n",
+ " else:\n",
+ " drawdown = (peak - price) / peak # 计算当前回撤\n",
+ " max_drawdown = max(max_drawdown, drawdown) # 更新最大回撤\n",
+ "\n",
+ " return max_drawdown\n",
+ "\n",
+ " def compute_stock_score(stock_df):\n",
+ " stock_df = stock_df.sort_values(by=['trade_date'])\n",
+ " future_return = stock_df['future_return']\n",
+ " # 使用已有的 pct_chg 字段计算波动率\n",
+ " volatility = stock_df['pct_chg'].rolling(days).std().shift(-days)\n",
+ " max_drawdown = stock_df['close'].rolling(days).apply(calculate_max_drawdown, raw=False).shift(-days)\n",
+ " score = future_return - lambda_param * max_drawdown\n",
+ " return score\n",
+ "\n",
+ " # # 确保 DataFrame 按照股票代码和交易日期排序\n",
+ " # df = df.sort_values(by=['ts_code', 'trade_date'])\n",
+ "\n",
+ " # 对每个股票分别计算 score\n",
+ " df['score'] = df.groupby('ts_code').apply(compute_stock_score).reset_index(level=0, drop=True)\n",
+ "\n",
+ " return df['score']\n",
+ "\n",
+ "\n",
+ "def remove_highly_correlated_features(df, feature_columns, threshold=0.9):\n",
+ " numeric_features = df[feature_columns].select_dtypes(include=[np.number]).columns.tolist()\n",
+ " if not numeric_features:\n",
+ " raise ValueError(\"No numeric features found in the provided data.\")\n",
+ "\n",
+ " corr_matrix = df[numeric_features].corr().abs()\n",
+ " upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))\n",
+ " to_drop = [column for column in upper.columns if any(upper[column] > threshold)]\n",
+ " remaining_features = [col for col in feature_columns if col not in to_drop\n",
+ " or 'act' in col or 'af' in col]\n",
+ " return remaining_features\n",
+ "\n",
+ "\n",
+ "def cross_sectional_standardization(df, features):\n",
+ " df_sorted = df.sort_values(by='trade_date') # 按时间排序\n",
+ " df_standardized = df_sorted.copy()\n",
+ "\n",
+ " for date in df_sorted['trade_date'].unique():\n",
+ " # 获取当前时间点的数据\n",
+ " current_data = df_standardized[df_standardized['trade_date'] == date]\n",
+ "\n",
+ " # 只对指定特征进行标准化\n",
+ " scaler = StandardScaler()\n",
+ " standardized_values = scaler.fit_transform(current_data[features])\n",
+ "\n",
+ " # 将标准化结果重新赋值回去\n",
+ " df_standardized.loc[df_standardized['trade_date'] == date, features] = standardized_values\n",
+ "\n",
+ " return df_standardized\n",
+ "\n",
+ "\n",
+ "def neutralize_manual_revised(df: pd.DataFrame, features: list, industry_col: str, mkt_cap_col: str) -> pd.DataFrame:\n",
+ " \"\"\"\n",
+ " 手动实现简单回归以提升速度,通过构建 Series 确保索引对齐。\n",
+ " 对特征在行业内部进行市值中性化。\n",
+ "\n",
+ " Args:\n",
+ " df: 输入的 DataFrame,包含特征、行业分类和市值列。\n",
+ " features: 需要进行中性化的特征列名列表。\n",
+ " industry_col: 行业分类列的列名。\n",
+ " mkt_cap_col: 市值列的列名。\n",
+ "\n",
+ " Returns:\n",
+ " 中性化后的 DataFrame。\n",
+ " \"\"\"\n",
+ "\n",
+ " df[mkt_cap_col] = pd.to_numeric(df[mkt_cap_col], errors='coerce')\n",
+ " df_cleaned = df.dropna(subset=[mkt_cap_col]).copy()\n",
+ " df_cleaned = df_cleaned[df_cleaned[mkt_cap_col] > 0].copy()\n",
+ "\n",
+ " if df_cleaned.empty:\n",
+ " print(\"警告: 清理市值异常值后 DataFrame 为空。\")\n",
+ " return df # 返回原始或空df,取决于清理前的状态\n",
+ "\n",
+ " processed_df = df\n",
+ "\n",
+ " for col in features:\n",
+ " if col not in df_cleaned.columns:\n",
+ " print(f\"警告: 特征列 '{col}' 不存在于清理后的 DataFrame 中,已跳过。\")\n",
+ " # 对于原始 df 中该列不存在的,在结果 df 中也保持原样(可能全是NaN)\n",
+ " processed_df[col] = df[col] if col in df.columns else np.nan\n",
+ " continue\n",
+ "\n",
+ " # 跳过对控制变量本身进行中性化\n",
+ " if col == mkt_cap_col or col == industry_col:\n",
+ " print(f\"警告: 特征列 '{col}' 是控制变量或内部使用的列,跳过中性化。\")\n",
+ " # 在结果 df 中也保持原样\n",
+ " processed_df[col] = df[col] if col in df.columns else np.nan\n",
+ " continue\n",
+ "\n",
+ " residual_series = pd.Series(index=df_cleaned.index, dtype=float)\n",
+ "\n",
+ " # 在分组前处理特征列的 NaN,只对有因子值的行进行回归计算\n",
+ " df_subset_factor = df_cleaned.dropna(subset=[col]).copy()\n",
+ "\n",
+ " if not df_subset_factor.empty:\n",
+ " for industry, group in df_subset_factor.groupby(industry_col):\n",
+ " x = group[mkt_cap_col] # 市值对数\n",
+ " y = group[col] # 因子值\n",
+ "\n",
+ " # 确保有足够的数据点 (>1) 且市值对数有方差 (>0) 进行回归计算\n",
+ " # 检查 np.var > 一个很小的正数,避免浮点数误差导致的零方差判断问题\n",
+ " if len(group) > 1 and np.var(x) > 1e-9:\n",
+ " try:\n",
+ " beta = np.cov(y, x)[0, 1] / np.var(x)\n",
+ " alpha = np.mean(y) - beta * np.mean(x)\n",
+ "\n",
+ " # 计算残差\n",
+ " resid = y - (alpha + beta * x)\n",
+ "\n",
+ " # 将计算出的残差存储到 residual_series 中,通过索引自动对齐\n",
+ " residual_series.loc[resid.index] = resid\n",
+ "\n",
+ " except Exception as e:\n",
+ " # 捕获可能的计算异常,例如np.cov或np.var因为极端数据报错\n",
+ " print(f\"警告: 在行业 {industry} 计算回归时发生错误: {e}。该组残差将设为原始值或 NaN。\")\n",
+ " # 此时该组的残差会保持 residual_series 初始化时的 NaN 或后续处理\n",
+ " # 也可以选择保留原始值:residual_series.loc[group.index] = group[col]\n",
+ "\n",
+ " else:\n",
+ " residual_series.loc[group.index] = group[col] # 保留原始因子值\n",
+ " processed_df.loc[residual_series.index, col] = residual_series\n",
+ "\n",
+ "\n",
+ " else:\n",
+ " processed_df[col] = np.nan # 或 df[col] if col in df.columns else np.nan\n",
+ "\n",
+ " return processed_df\n",
+ "\n",
+ "\n",
+ "import gc\n",
+ "\n",
+ "gc.collect()\n",
+ "\n",
+ "\n",
+ "def mad_filter(df, features, n=3):\n",
+ " for col in features:\n",
+ " median = df[col].median()\n",
+ " mad = np.median(np.abs(df[col] - median))\n",
+ " upper = median + n * mad\n",
+ " lower = median - n * mad\n",
+ " df[col] = np.clip(df[col], lower, upper) # 截断极值\n",
+ " return df\n",
+ "\n",
+ "\n",
+ "def percentile_filter(df, features, lower_percentile=0.01, upper_percentile=0.99):\n",
+ " for col in features:\n",
+ " # 按日期分组计算上下百分位数\n",
+ " lower_bound = df.groupby('trade_date')[col].transform(\n",
+ " lambda x: x.quantile(lower_percentile)\n",
+ " )\n",
+ " upper_bound = df.groupby('trade_date')[col].transform(\n",
+ " lambda x: x.quantile(upper_percentile)\n",
+ " )\n",
+ " # 截断超出范围的值\n",
+ " df[col] = np.clip(df[col], lower_bound, upper_bound)\n",
+ " return df\n",
+ "\n",
+ "\n",
+ "from scipy.stats import iqr\n",
+ "\n",
+ "\n",
+ "def iqr_filter(df, features):\n",
+ " for col in features:\n",
+ " df[col] = df.groupby('trade_date')[col].transform(\n",
+ " lambda x: (x - x.median()) / iqr(x) if iqr(x) != 0 else x\n",
+ " )\n",
+ " return df\n",
+ "\n",
+ "\n",
+ "def quantile_filter(df, features, lower_quantile=0.01, upper_quantile=0.99, window=60):\n",
+ " df = df.copy()\n",
+ " for col in features:\n",
+ " # 计算 rolling 统计量,需要按日期进行 groupby\n",
+ " rolling_lower = df.groupby('trade_date')[col].transform(lambda x: x.rolling(window=min(len(x), window)).quantile(lower_quantile))\n",
+ " rolling_upper = df.groupby('trade_date')[col].transform(lambda x: x.rolling(window=min(len(x), window)).quantile(upper_quantile))\n",
+ "\n",
+ " # 对数据进行裁剪\n",
+ " df[col] = np.clip(df[col], rolling_lower, rolling_upper)\n",
+ " \n",
+ " return df\n",
+ "\n",
+ "def select_top_features_by_rankic(df: pd.DataFrame, feature_columns: list, n: int, target_column: str = 'future_return') -> list:\n",
+ " \"\"\"\n",
+ " 计算给定特征与目标列的 RankIC,并返回 RankIC 绝对值最高的 n 个特征。\n",
+ "\n",
+ " Args:\n",
+ " df: 包含特征列和目标列的 Pandas DataFrame。\n",
+ " feature_columns: 包含所有待评估特征列名的列表。\n",
+ " n: 希望选取的 RankIC 绝对值最高的特征数量。\n",
+ " target_column: 目标列的名称,用于计算 RankIC。默认为 'future_return'。\n",
+ "\n",
+ " Returns:\n",
+ " 包含 RankIC 绝对值最高的 n 个特征列名的列表。\n",
+ " \"\"\"\n",
+ " numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns\n",
+ " numeric_columns = [col for col in numeric_columns if col in feature_columns]\n",
+ " if target_column not in df.columns:\n",
+ " raise ValueError(f\"目标列 '{target_column}' 不存在于 DataFrame 中。\")\n",
+ "\n",
+ " rankic_scores = {}\n",
+ " for feature in numeric_columns:\n",
+ " if feature not in df.columns:\n",
+ " print(f\"警告: 特征列 '{feature}' 不存在于 DataFrame 中,已跳过。\")\n",
+ " continue\n",
+ "\n",
+ " # 计算特征与目标列的 RankIC (斯皮尔曼相关系数)\n",
+ " # dropna() 是为了处理缺失值,确保相关性计算不失败\n",
+ " valid_data = df[[feature, target_column]].dropna()\n",
+ " if len(valid_data) > 1: # 确保有足够的数据点进行相关性计算\n",
+ " # 计算斯皮尔曼相关性\n",
+ " correlation = valid_data[feature].corr(valid_data[target_column], method='spearman')\n",
+ " rankic_scores[feature] = abs(correlation) # 使用绝对值来衡量相关性强度\n",
+ " else:\n",
+ " rankic_scores[feature] = 0 # 数据不足,RankIC设为0或跳过\n",
+ "\n",
+ " # 将 RankIC 分数转换为 Series 便于排序\n",
+ " rankic_series = pd.Series(rankic_scores)\n",
+ "\n",
+ " # 按 RankIC 绝对值降序排序,选取前 n 个特征\n",
+ " # handle case where n might be larger than available features\n",
+ " n_actual = min(n, len(rankic_series))\n",
+ " top_features = rankic_series.sort_values(ascending=False).head(n_actual).index.tolist()\n",
+ " top_features = [col for col in feature_columns if col in top_features or col not in numeric_columns]\n",
+ " return top_features\n",
+ "\n",
+ "def create_deviation_within_dates(df, feature_columns):\n",
+ " groupby_col = 'cat_l2_code' # 使用 trade_date 进行分组\n",
+ " new_columns = {}\n",
+ " ret_feature_columns = feature_columns[:]\n",
+ "\n",
+ " # 自动选择所有数值型特征\n",
+ " num_features = [col for col in feature_columns if 'cat' not in col and 'index' not in col]\n",
+ "\n",
+ " # num_features = ['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'cat_vol_spike', 'obv', 'maobv_6', 'return_5', 'return_10', 'return_20', 'std_return_5', 'std_return_15', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'act_factor5', 'act_factor6', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'alpha_022', 'alpha_003', 'alpha_007', 'alpha_013']\n",
+ " num_features = [col for col in num_features if 'cat' not in col and 'industry' not in col]\n",
+ " num_features = [col for col in num_features if 'limit' not in col]\n",
+ " num_features = [col for col in num_features if 'cyq' not in col]\n",
+ "\n",
+ " # 遍历所有数值型特征\n",
+ " for feature in num_features:\n",
+ " if feature == 'trade_date': # 不需要对 'trade_date' 计算偏差\n",
+ " continue\n",
+ "\n",
+ " # grouped_mean = df.groupby(['trade_date'])[feature].transform('mean')\n",
+ " # deviation_col_name = f'deviation_mean_{feature}'\n",
+ " # new_columns[deviation_col_name] = df[feature] - grouped_mean\n",
+ " # ret_feature_columns.append(deviation_col_name)\n",
+ "\n",
+ " grouped_mean = df.groupby(['trade_date', groupby_col])[feature].transform('mean')\n",
+ " deviation_col_name = f'deviation_mean_{feature}'\n",
+ " new_columns[deviation_col_name] = df[feature] - grouped_mean\n",
+ " ret_feature_columns.append(deviation_col_name)\n",
+ "\n",
+ " # 将新计算的偏差特征与原始 DataFrame 合并\n",
+ " df = pd.concat([df, pd.DataFrame(new_columns)], axis=1)\n",
+ "\n",
+ " # for feature in ['obv', 'return_20', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4']:\n",
+ " # df[f'deviation_industry_{feature}'] = df[feature] - df[f'industry_{feature}']\n",
+ "\n",
+ " return df, ret_feature_columns\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "47c12bb34062ae7a",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T14:57:50.841165Z",
+ "start_time": "2025-04-03T14:49:25.889057Z"
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "days = 5\n",
+ "validation_days = 120\n",
+ "\n",
+ "import gc\n",
+ "\n",
+ "gc.collect()\n",
+ "\n",
+ "df = df.sort_values(by=['ts_code', 'trade_date'])\n",
+ "df['future_return'] = df.groupby('ts_code', group_keys=False)['close'].apply(lambda x: x.shift(-days) / x - 1)\n",
+ "# df['future_return'] = (df.groupby('ts_code')['close'].shift(-days) - df.groupby('ts_code')['open'].shift(-1)) / \\\n",
+ "# df.groupby('ts_code')['open'].shift(-1)\n",
+ "\n",
+ "df['cat_up_limit'] = df['pct_chg'] > 5\n",
+ "df['label'] = df.groupby('ts_code')['cat_up_limit'].rolling(window=5, min_periods=1).max().groupby('ts_code').shift(-5).fillna(0).astype(int).reset_index(level=0, drop=True)\n",
+ "\n",
+ "filter_index = df['future_return'].between(df['future_return'].quantile(0.01), df['future_return'].quantile(0.99))\n",
+ "\n",
+ "# for col in [col for col in df.columns]:\n",
+ "# train_data[col] = train_data[col].astype('str')\n",
+ "# test_data[col] = test_data[col].astype('str')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29221dde",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "191\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "feature_columns = [col for col in df.head(10).merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left').merge(index_data, on='trade_date', how='left').columns]\n",
+ "feature_columns = [col for col in feature_columns if col not in ['trade_date',\n",
+ " 'ts_code',\n",
+ " 'label']]\n",
+ "feature_columns = [col for col in feature_columns if 'future' not in col]\n",
+ "feature_columns = [col for col in feature_columns if 'label' not in col]\n",
+ "feature_columns = [col for col in feature_columns if 'score' not in col]\n",
+ "feature_columns = [col for col in feature_columns if 'gen' not in col]\n",
+ "feature_columns = [col for col in feature_columns if 'is_st' not in col]\n",
+ "feature_columns = [col for col in feature_columns if 'pe_ttm' not in col]\n",
+ "# feature_columns = [col for col in feature_columns if 'volatility' not in col]\n",
+ "feature_columns = [col for col in feature_columns if 'circ_mv' not in col]\n",
+ "feature_columns = [col for col in feature_columns if 'code' not in col]\n",
+ "feature_columns = [col for col in feature_columns if col not in origin_columns]\n",
+ "feature_columns = [col for col in feature_columns if not col.startswith('_')]\n",
+ "# feature_columns = [col for col in feature_columns if col not in ['ts_code', 'trade_date', 'vol_std_5', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_007', 'consecutive_up_limit', 'mv_volatility', 'volume_growth', 'mv_growth', 'arbr']]\n",
+ "feature_columns = [col for col in feature_columns if col not in ['intraday_lg_flow_corr_20', \n",
+ " 'cap_neutral_cost_metric', \n",
+ " 'hurst_net_mf_vol_60', \n",
+ " 'complex_factor_deap_1', \n",
+ " 'lg_buy_consolidation_20',\n",
+ " 'cs_rank_ind_cap_neutral_pe',\n",
+ " 'cs_rank_opening_gap',\n",
+ " 'cs_rank_ind_adj_lg_flow']]\n",
+ "feature_columns = [col for col in feature_columns if col not in ['roa', 'roe']]\n",
+ "print(len(feature_columns))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "03ee5daf",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "# df = fill_nan_with_daily_median(df, feature_columns)\n",
+ "for feature_col in [col for col in feature_columns if col in df.columns]:\n",
+ " # median_val = df[feature_col].median()\n",
+ " df[feature_col].fillna(0, inplace=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b76ea08a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " ts_code trade_date log_circ_mv\n",
+ "0 000001.SZ 2019-01-02 16.574219\n",
+ "1 000001.SZ 2019-01-03 16.583965\n",
+ "2 000001.SZ 2019-01-04 16.633371\n",
+ "['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'daily_deviation', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', 'cs_rank_net_lg_flow_val', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size', 'cat_up_limit', 'industry_obv', 'industry_return_5', 'industry_return_20', 'industry__ema_5', 'industry__ema_13', 'industry__ema_20', 'industry__ema_60', 'industry_act_factor1', 'industry_act_factor2', 'industry_act_factor3', 'industry_act_factor4', 'industry_act_factor5', 'industry_act_factor6', 'industry_rank_act_factor1', 'industry_rank_act_factor2', 'industry_rank_act_factor3', 'industry_return_5_percentile', 'industry_return_20_percentile', '000852.SH_MACD', '000905.SH_MACD', '399006.SZ_MACD', '000852.SH_MACD_hist', '000905.SH_MACD_hist', '399006.SZ_MACD_hist', '000852.SH_RSI', '000905.SH_RSI', '399006.SZ_RSI', '000852.SH_Signal_line', '000905.SH_Signal_line', '399006.SZ_Signal_line', '000852.SH_amount_change_rate', '000905.SH_amount_change_rate', '399006.SZ_amount_change_rate', '000852.SH_amount_mean', '000905.SH_amount_mean', '399006.SZ_amount_mean', '000852.SH_daily_return', '000905.SH_daily_return', '399006.SZ_daily_return', '000852.SH_up_ratio_20d', '000905.SH_up_ratio_20d', '399006.SZ_up_ratio_20d', '000852.SH_volatility', '000905.SH_volatility', '399006.SZ_volatility', '000852.SH_volume_change_rate', '000905.SH_volume_change_rate', '399006.SZ_volume_change_rate']\n",
+ "去除极值\n",
+ "开始截面 MAD 去极值处理 (k=3.0)...\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "MAD Filtering: 100%|██████████| 131/131 [00:15<00:00, 8.53it/s]\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "截面 MAD 去极值处理完成。\n",
+ "开始截面 MAD 去极值处理 (k=3.0)...\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "MAD Filtering: 100%|██████████| 131/131 [00:11<00:00, 11.23it/s]\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "截面 MAD 去极值处理完成。\n",
+ "开始截面 MAD 去极值处理 (k=3.0)...\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "MAD Filtering: 0it [00:00, ?it/s]\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "截面 MAD 去极值处理完成。\n",
+ "开始截面 MAD 去极值处理 (k=3.0)...\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "MAD Filtering: 0it [00:00, ?it/s]\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "截面 MAD 去极值处理完成。\n",
+ "feature_columns: ['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'daily_deviation', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', 'cs_rank_net_lg_flow_val', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size', 'cat_up_limit', 'industry_obv', 'industry_return_5', 'industry_return_20', 'industry__ema_5', 'industry__ema_13', 'industry__ema_20', 'industry__ema_60', 'industry_act_factor1', 'industry_act_factor2', 'industry_act_factor3', 'industry_act_factor4', 'industry_act_factor5', 'industry_act_factor6', 'industry_rank_act_factor1', 'industry_rank_act_factor2', 'industry_rank_act_factor3', 'industry_return_5_percentile', 'industry_return_20_percentile', '000852.SH_MACD', '000905.SH_MACD', '399006.SZ_MACD', '000852.SH_MACD_hist', '000905.SH_MACD_hist', '399006.SZ_MACD_hist', '000852.SH_RSI', '000905.SH_RSI', '399006.SZ_RSI', '000852.SH_Signal_line', '000905.SH_Signal_line', '399006.SZ_Signal_line', '000852.SH_amount_change_rate', '000905.SH_amount_change_rate', '399006.SZ_amount_change_rate', '000852.SH_amount_mean', '000905.SH_amount_mean', '399006.SZ_amount_mean', '000852.SH_daily_return', '000905.SH_daily_return', '399006.SZ_daily_return', '000852.SH_up_ratio_20d', '000905.SH_up_ratio_20d', '399006.SZ_up_ratio_20d', '000852.SH_volatility', '000905.SH_volatility', '399006.SZ_volatility', '000852.SH_volume_change_rate', '000905.SH_volume_change_rate', '399006.SZ_volume_change_rate']\n",
+ "df最小日期: 2019-01-02\n",
+ "df最大日期: 2025-05-30\n",
+ "2057465\n",
+ "train_data最小日期: 2020-01-02\n",
+ "train_data最大日期: 2022-12-30\n",
+ "1781706\n",
+ "test_data最小日期: 2023-01-03\n",
+ "test_data最大日期: 2025-05-30\n",
+ " ts_code trade_date log_circ_mv\n",
+ "0 000001.SZ 2019-01-02 16.574219\n",
+ "1 000001.SZ 2019-01-03 16.583965\n",
+ "2 000001.SZ 2019-01-04 16.633371\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "split_date = '2023-01-01'\n",
+ "train_data = df[filter_index & (df['trade_date'] <= split_date) & (df['trade_date'] >= '2020-01-01')]\n",
+ "test_data = df[(df['trade_date'] >= split_date)]\n",
+ "\n",
+ "print(df[['ts_code', 'trade_date', 'log_circ_mv']].head(3))\n",
+ "\n",
+ "industry_df = industry_df.sort_values(by=['trade_date'])\n",
+ "index_data = index_data.sort_values(by=['trade_date'])\n",
+ "\n",
+ "# train_data = train_data.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n",
+ "# train_data = train_data.merge(index_data, on='trade_date', how='left')\n",
+ "# test_data = test_data.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n",
+ "# test_data = test_data.merge(index_data, on='trade_date', how='left')\n",
+ "\n",
+ "train_data, test_data = train_data.replace([np.inf, -np.inf], np.nan), test_data.replace([np.inf, -np.inf], np.nan)\n",
+ "\n",
+ "# feature_columns_new = feature_columns[:]\n",
+ "# train_data, _ = create_deviation_within_dates(train_data, [col for col in feature_columns if col in train_data.columns])\n",
+ "# test_data, _ = create_deviation_within_dates(test_data, [col for col in feature_columns if col in train_data.columns])\n",
+ "\n",
+ "# feature_columns = [\n",
+ "# 'undist_profit_ps', \n",
+ "# 'AR_BR',\n",
+ "# 'pe_ttm',\n",
+ "# 'alpha_22_improved', \n",
+ "# 'alpha_003', \n",
+ "# 'alpha_007', \n",
+ "# 'alpha_013', \n",
+ "# 'cat_up_limit', \n",
+ "# 'cat_down_limit', \n",
+ "# 'up_limit_count_10d', \n",
+ "# 'down_limit_count_10d', \n",
+ "# 'consecutive_up_limit', \n",
+ "# 'vol_break', \n",
+ "# 'weight_roc5', \n",
+ "# 'price_cost_divergence', \n",
+ "# 'smallcap_concentration', \n",
+ "# 'cost_stability', \n",
+ "# 'high_cost_break_days', \n",
+ "# 'liquidity_risk', \n",
+ "# 'turnover_std', \n",
+ "# 'mv_volatility', \n",
+ "# 'volume_growth', \n",
+ "# 'mv_growth', \n",
+ "# 'lg_flow_mom_corr_20_60', \n",
+ "# 'lg_flow_accel', \n",
+ "# 'profit_pressure', \n",
+ "# 'underwater_resistance', \n",
+ "# 'cost_conc_std_20', \n",
+ "# 'profit_decay_20', \n",
+ "# 'vol_amp_loss_20', \n",
+ "# 'vol_drop_profit_cnt_5', \n",
+ "# 'lg_flow_vol_interact_20', \n",
+ "# 'cost_break_confirm_cnt_5', \n",
+ "# 'atr_norm_channel_pos_14', \n",
+ "# 'turnover_diff_skew_20', \n",
+ "# 'lg_sm_flow_diverge_20', \n",
+ "# 'pullback_strong_20_20', \n",
+ "# 'vol_wgt_hist_pos_20', \n",
+ "# 'vol_adj_roc_20',\n",
+ "# 'cashflow_to_ev_factor',\n",
+ "# 'ocfps',\n",
+ "# 'book_to_price_ratio',\n",
+ "# 'turnover_rate_mean_5',\n",
+ "# 'variance_20',\n",
+ "# 'bbi_ratio_factor'\n",
+ "# ]\n",
+ "# feature_columns = [col for col in feature_columns if col in train_data.columns]\n",
+ "# feature_columns = [col for col in feature_columns if not col.startswith('_')]\n",
+ "\n",
+ "numeric_columns = df.select_dtypes(include=['float64', 'int64']).columns\n",
+ "numeric_columns = [col for col in numeric_columns if col in feature_columns]\n",
+ "# feature_columns = select_top_features_by_rankic(df, numeric_columns, n=10)\n",
+ "print(feature_columns)\n",
+ "\n",
+ "# train_data = fill_nan_with_daily_median(train_data, feature_columns)\n",
+ "# test_data = fill_nan_with_daily_median(test_data, feature_columns)\n",
+ "\n",
+ "train_data = train_data.dropna(subset=[col for col in feature_columns if col in train_data.columns])\n",
+ "train_data = train_data.dropna(subset=['label'])\n",
+ "train_data = train_data.reset_index(drop=True)\n",
+ "# print(test_data.tail())\n",
+ "test_data = test_data.dropna(subset=[col for col in feature_columns if col in train_data.columns])\n",
+ "# test_data = test_data.dropna(subset=['label'])\n",
+ "test_data = test_data.reset_index(drop=True)\n",
+ "\n",
+ "transform_feature_columns = feature_columns\n",
+ "transform_feature_columns = [col for col in transform_feature_columns if col in feature_columns and not col.startswith('cat') and col in train_data.columns]\n",
+ "# transform_feature_columns.remove('undist_profit_ps')\n",
+ "print('去除极值')\n",
+ "cs_mad_filter(train_data, transform_feature_columns)\n",
+ "# print('中性化')\n",
+ "# cs_neutralize_market_cap_numpy(train_data, transform_feature_columns)\n",
+ "# print('标准化')\n",
+ "# cs_zscore_standardize(train_data, transform_feature_columns)\n",
+ "\n",
+ "cs_mad_filter(test_data, transform_feature_columns)\n",
+ "# cs_neutralize_market_cap_numpy(test_data, transform_feature_columns)\n",
+ "# cs_zscore_standardize(test_data, transform_feature_columns)\n",
+ "\n",
+ "mad_filter_feature_columns = [col for col in feature_columns if col not in transform_feature_columns and not col.startswith('cat') and col in train_data.columns]\n",
+ "cs_mad_filter(train_data, mad_filter_feature_columns)\n",
+ "cs_mad_filter(test_data, mad_filter_feature_columns)\n",
+ "\n",
+ "\n",
+ "print(f'feature_columns: {feature_columns}')\n",
+ "\n",
+ "\n",
+ "print(f\"df最小日期: {df['trade_date'].min().strftime('%Y-%m-%d')}\")\n",
+ "print(f\"df最大日期: {df['trade_date'].max().strftime('%Y-%m-%d')}\")\n",
+ "print(len(train_data))\n",
+ "print(f\"train_data最小日期: {train_data['trade_date'].min().strftime('%Y-%m-%d')}\")\n",
+ "print(f\"train_data最大日期: {train_data['trade_date'].max().strftime('%Y-%m-%d')}\")\n",
+ "print(len(test_data))\n",
+ "print(f\"test_data最小日期: {test_data['trade_date'].min().strftime('%Y-%m-%d')}\")\n",
+ "print(f\"test_data最大日期: {test_data['trade_date'].max().strftime('%Y-%m-%d')}\")\n",
+ "\n",
+ "cat_columns = [col for col in feature_columns if col.startswith('cat')]\n",
+ "for col in cat_columns:\n",
+ " train_data[col] = train_data[col].astype('category')\n",
+ " test_data[col] = test_data[col].astype('category')\n",
+ "\n",
+ "print(df[['ts_code', 'trade_date', 'log_circ_mv']].head(3))\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3ff2d1c5",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "from sklearn.preprocessing import StandardScaler\n",
+ "from sklearn.linear_model import LogisticRegression\n",
+ "import matplotlib.pyplot as plt # 保持 matplotlib 导入,尽管LightGBM的绘图功能已移除\n",
+ "from sklearn.decomposition import PCA\n",
+ "import datetime # 用于日期计算\n",
+ "from catboost import CatBoostClassifier\n",
+ "from catboost import Pool\n",
+ "import lightgbm as lgb\n",
+ "\n",
+ "def train_model(train_data_df, feature_columns,\n",
+ " print_info=True, # 调整参数名,更通用\n",
+ " validation_days=180, use_pca=False, split_date=None,\n",
+ " target_column='label', type='light'): # 增加目标列参数\n",
+ "\n",
+ " print('train data size: ', len(train_data_df))\n",
+ " print(train_data_df[['ts_code', 'trade_date', 'log_circ_mv']])\n",
+ " # 确保数据按时间排序\n",
+ " train_data_df = train_data_df.sort_values(by='trade_date')\n",
+ "\n",
+ " # 识别数值型特征列\n",
+ " numeric_feature_columns = train_data_df[feature_columns].select_dtypes(include=['float64', 'int64']).columns.tolist()\n",
+ "\n",
+ " # 去除标签为空的样本\n",
+ " initial_len = len(train_data_df)\n",
+ " train_data_df = train_data_df.dropna(subset=[target_column])\n",
+ "\n",
+ " if print_info:\n",
+ " print(f'原始样本数: {initial_len}, 去除标签为空后样本数: {len(train_data_df)}')\n",
+ "\n",
+ " # 提取特征和标签,只取数值型特征用于线性回归\n",
+ " \n",
+ " if split_date is None:\n",
+ " all_dates = train_data_df['trade_date'].unique() # 获取所有唯一的 trade_date\n",
+ " split_date = all_dates[-validation_days] # 划分点为倒数第 validation_days 天\n",
+ " train_data_split = train_data_df[train_data_df['trade_date'] < split_date] # 训练集\n",
+ " val_data_split = train_data_df[train_data_df['trade_date'] >= split_date] # 验证集\n",
+ " \n",
+ " X_train = train_data_split[feature_columns]\n",
+ " y_train = train_data_split[target_column]\n",
+ " \n",
+ " X_val = val_data_split[feature_columns]\n",
+ " y_val = val_data_split['label']\n",
+ "\n",
+ "\n",
+ " # # 标准化数值特征 (使用 StandardScaler 对训练集fit并transform, 对验证集只transform)\n",
+ " scaler = StandardScaler()\n",
+ " # X_train = scaler.fit_transform(X_train)\n",
+ "\n",
+ " # 训练线性回归模型\n",
+ " # model = LogisticRegression(random_state=42)\n",
+ " \n",
+ " # # 使用处理后的特征和样本权重进行训练\n",
+ " # model.fit(X_train, y_train)\n",
+ "\n",
+ "\n",
+ " if type == 'cat':\n",
+ " params = {\n",
+ " 'loss_function': 'Logloss', # 适用于二分类\n",
+ " 'eval_metric': 'Logloss', # 评估指标\n",
+ " 'iterations': 1500,\n",
+ " 'learning_rate': 0.01,\n",
+ " 'depth': 10, # 控制模型复杂度\n",
+ " 'l2_leaf_reg': 50, # L2 正则化\n",
+ " 'verbose': 5000,\n",
+ " 'early_stopping_rounds': 300,\n",
+ " # 'od_type': 'Iter', # Overfitting detector type\n",
+ " # 'od_wait': 300, # Number of iterations to wait after the bes\n",
+ " 'one_hot_max_size': 50,\n",
+ " 'class_weights': [0.6, 1.2],\n",
+ " 'task_type': 'GPU',\n",
+ " 'has_time': True,\n",
+ " 'random_seed': 7\n",
+ " }\n",
+ " cat_features = [i for i, col in enumerate(feature_columns) if col.startswith('cat')]\n",
+ " train_pool = Pool(data=X_train, label=y_train, cat_features=cat_features)\n",
+ " val_pool = Pool(data=X_val, label=y_val, cat_features=cat_features)\n",
+ "\n",
+ "\n",
+ " model = CatBoostClassifier(**params)\n",
+ " model.fit(train_pool,\n",
+ " eval_set=val_pool, \n",
+ " plot=True, \n",
+ " use_best_model=True\n",
+ " )\n",
+ " elif type == 'light':\n",
+ " params = {\n",
+ " 'objective': 'binary',\n",
+ " 'metric': 'average_precision',\n",
+ " 'learning_rate': 0.01,\n",
+ " 'is_unbalance': True,\n",
+ " 'num_leaves': 2048,\n",
+ " 'min_data_in_leaf': 1024,\n",
+ " 'max_depth': 32,\n",
+ " 'max_bin': 1024,\n",
+ " 'feature_fraction': 0.5,\n",
+ " 'bagging_fraction': 0.5,\n",
+ " 'bagging_freq': 1,\n",
+ " 'lambda_l1': 50,\n",
+ " 'lambda_l2': 50,\n",
+ " 'verbosity': -1,\n",
+ " 'num_threads' : 8\n",
+ " }\n",
+ " categorical_feature = [col for col in feature_columns if 'cat' in col]\n",
+ " train_dataset = lgb.Dataset(\n",
+ " X_train, label=y_train,\n",
+ " categorical_feature=categorical_feature\n",
+ " )\n",
+ " val_dataset = lgb.Dataset(\n",
+ " X_val, label=y_val,\n",
+ " categorical_feature=categorical_feature\n",
+ " )\n",
+ "\n",
+ " evals = {}\n",
+ " callbacks = [lgb.log_evaluation(period=1000),\n",
+ " lgb.callback.record_evaluation(evals),\n",
+ " lgb.early_stopping(100, first_metric_only=True)\n",
+ " ]\n",
+ " # 训练模型\n",
+ " model = lgb.train(\n",
+ " params, train_dataset, num_boost_round=1000,\n",
+ " valid_sets=[train_dataset, val_dataset], valid_names=['train', 'valid'],\n",
+ " callbacks=callbacks\n",
+ " )\n",
+ "\n",
+ " # 打印特征重要性(如果需要)\n",
+ " if True:\n",
+ " lgb.plot_metric(evals)\n",
+ " lgb.plot_importance(model, importance_type='split', max_num_features=20)\n",
+ " plt.show()\n",
+ "\n",
+ "\n",
+ " return model, scaler, None # 返回训练好的模型、scaler 和 pca 对象"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c6eb5cd4-e714-420a-ac48-39af3e11ee81",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T15:03:18.426481Z",
+ "start_time": "2025-04-03T15:02:19.926352Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "train data size: 36400\n",
+ " ts_code trade_date log_circ_mv\n",
+ "0 600306.SH 2020-01-02 11.552040\n",
+ "1 603269.SH 2020-01-02 11.324801\n",
+ "2 002633.SZ 2020-01-02 11.759023\n",
+ "3 603991.SH 2020-01-02 11.181150\n",
+ "4 000691.SZ 2020-01-02 11.677910\n",
+ "... ... ... ...\n",
+ "36395 600615.SH 2022-12-30 12.027909\n",
+ "36396 603829.SH 2022-12-30 12.034572\n",
+ "36397 603037.SH 2022-12-30 12.035767\n",
+ "36398 002767.SZ 2022-12-30 11.896427\n",
+ "36399 600561.SH 2022-12-30 11.858571\n",
+ "\n",
+ "[36400 rows x 3 columns]\n",
+ "原始样本数: 36400, 去除标签为空后样本数: 36400\n"
+ ]
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "9de2338da1fc42ec952054f233070da7",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "MetricVisualizer(layout=Layout(align_self='stretch', height='500px'))"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0:\tlearn: 0.6890148\ttest: 0.6905107\tbest: 0.6905107 (0)\ttotal: 92.5ms\tremaining: 2m 18s\n",
+ "bestTest = 0.5221693203\n",
+ "bestIteration = 874\n",
+ "Shrink model to first 875 iterations.\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "\n",
+ "gc.collect()\n",
+ "\n",
+ "use_pca = False\n",
+ "type = 'cat'\n",
+ "# feature_contri = [2 if feat.startswith('act_factor') or 'buy' in feat or 'sell' in feat else 1 for feat in feature_columns]\n",
+ "# light_params['feature_contri'] = feature_contri\n",
+ "# print(f'feature_contri: {feature_contri}')\n",
+ "model, scaler, pca = train_model(train_data\n",
+ " .dropna(subset=['label']).groupby('trade_date', group_keys=False)\n",
+ " .apply(lambda x: x.nsmallest(50, 'total_mv'))\n",
+ " .merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n",
+ " .merge(index_data, on='trade_date', how='left'), feature_columns, type=type)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5d1522a7538db91b",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-04-03T15:04:39.656944Z",
+ "start_time": "2025-04-03T15:04:39.298483Z"
+ }
+ },
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "score_df = test_data.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(300, 'total_mv'))\n",
+ "# score_df = fill_nan_with_daily_median(score_df, ['pe_ttm'])\n",
+ "# score_df = score_df[score_df['pe_ttm'] > 0]\n",
+ "score_df = score_df.merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n",
+ "score_df = score_df.merge(index_data, on='trade_date', how='left')\n",
+ "# score_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(50, 'total_mv')).reset_index()\n",
+ "numeric_columns = score_df.select_dtypes(include=['float64', 'int64']).columns\n",
+ "numeric_columns = [col for col in feature_columns if col in numeric_columns]\n",
+ "# score_df.loc[:, numeric_columns] = scaler.transform(score_df[numeric_columns])\n",
+ "# score_df = cross_sectional_standardization(score_df, numeric_columns)\n",
+ "\n",
+ "if type == 'cat':\n",
+ " score_df['score'] = model.predict_proba(score_df[feature_columns])[:, 1]\n",
+ "elif type == 'light':\n",
+ " score_df['score'] = model.predict(score_df[feature_columns])\n",
+ "score_df['score_ranks'] = score_df.groupby('trade_date')['score'].rank(ascending=True)\n",
+ "\n",
+ "score_df = score_df.groupby('trade_date', group_keys=False).apply(\n",
+ " lambda x: x[x['score'] >= x['score'].quantile(0.90)] # 计算90%分位数作为阈值,筛选分数>=阈值的行\n",
+ ").reset_index(drop=True) # drop=True 避免添加旧索引列\n",
+ "# save_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nlargest(1, 'score')).reset_index()\n",
+ "save_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(2, 'total_mv')).reset_index()\n",
+ "save_df = save_df.sort_values(['trade_date', 'score'])\n",
+ "save_df[['trade_date', 'score', 'ts_code']].to_csv('predictions_test.tsv', index=False)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c1c40917",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "模型已保存到: /mnt/d/PyProject/NewStock/main/train/catboost_model/catboost_model_2025-06-01.cbm\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "current_date = datetime.datetime.now()\n",
+ "\n",
+ "# 2. 格式化日期为字符串,例如 '2025-07-06'\n",
+ "# 你可以根据需要调整日期格式,例如 '%Y%m%d' 会得到 '20250706'\n",
+ "date_str = current_date.strftime('%Y-%m-%d')\n",
+ "\n",
+ "# 3. 构建包含日期的模型文件名\n",
+ "model_filename = f'/mnt/d/PyProject/NewStock/main/train/catboost_model/catboost_model_2025-06-01.cbm'\n",
+ "\n",
+ "model.save_model(model_filename)\n",
+ "print(f\"模型已保存到: {model_filename}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "09b1799e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "191\n",
+ "['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'undist_profit_ps', 'ocfps', 'AR', 'BR', 'AR_BR', 'cashflow_to_ev_factor', 'book_to_price_ratio', 'turnover_rate_mean_5', 'variance_20', 'bbi_ratio_factor', 'daily_deviation', 'lg_elg_net_buy_vol', 'flow_lg_elg_intensity', 'sm_net_buy_vol', 'total_buy_vol', 'lg_elg_buy_prop', 'flow_struct_buy_change', 'lg_elg_net_buy_vol_change', 'flow_lg_elg_accel', 'chip_concentration_range', 'chip_skewness', 'floating_chip_proxy', 'cost_support_15pct_change', 'cat_winner_price_zone', 'flow_chip_consistency', 'profit_taking_vs_absorb', 'cat_is_positive', 'upside_vol', 'downside_vol', 'vol_ratio', 'return_skew', 'return_kurtosis', 'volume_change_rate', 'cat_volume_breakout', 'turnover_deviation', 'cat_turnover_spike', 'avg_volume_ratio', 'cat_volume_ratio_breakout', 'vol_spike', 'vol_std_5', 'atr_14', 'atr_6', 'obv', 'maobv_6', 'rsi_3', 'return_5', 'return_20', 'std_return_5', 'std_return_90', 'std_return_90_2', 'act_factor1', 'act_factor2', 'act_factor3', 'act_factor4', 'rank_act_factor1', 'rank_act_factor2', 'rank_act_factor3', 'cov', 'delta_cov', 'alpha_22_improved', 'alpha_003', 'alpha_007', 'alpha_013', 'vol_break', 'weight_roc5', 'smallcap_concentration', 'cost_stability', 'high_cost_break_days', 'liquidity_risk', 'turnover_std', 'mv_volatility', 'volume_growth', 'mv_growth', 'momentum_factor', 'resonance_factor', 'log_close', 'cat_vol_spike', 'up', 'down', 'obv_maobv_6', 'std_return_5_over_std_return_90', 'std_return_90_minus_std_return_90_2', 'cat_af2', 'cat_af3', 'cat_af4', 'act_factor5', 'act_factor6', 'active_buy_volume_large', 'active_buy_volume_big', 'active_buy_volume_small', 'buy_lg_vol_minus_sell_lg_vol', 'buy_elg_vol_minus_sell_elg_vol', 'ctrl_strength', 'low_cost_dev', 'asymmetry', 'lock_factor', 'cat_vol_break', 'cost_atr_adj', 'cat_golden_resonance', 'mv_turnover_ratio', 'mv_adjusted_volume', 'mv_weighted_turnover', 'nonlinear_mv_volume', 'mv_volume_ratio', 'mv_momentum', 'lg_flow_mom_corr_20_60', 'lg_flow_accel', 'profit_pressure', 'underwater_resistance', 'cost_conc_std_20', 'profit_decay_20', 'vol_amp_loss_20', 'vol_drop_profit_cnt_5', 'lg_flow_vol_interact_20', 'cost_break_confirm_cnt_5', 'atr_norm_channel_pos_14', 'turnover_diff_skew_20', 'lg_sm_flow_diverge_20', 'pullback_strong_20_20', 'vol_wgt_hist_pos_20', 'vol_adj_roc_20', 'cs_rank_net_lg_flow_val', 'cs_rank_elg_buy_ratio', 'cs_rank_rel_profit_margin', 'cs_rank_cost_breadth', 'cs_rank_dist_to_upper_cost', 'cs_rank_winner_rate', 'cs_rank_intraday_range', 'cs_rank_close_pos_in_range', 'cs_rank_pos_in_hist_range', 'cs_rank_vol_x_profit_margin', 'cs_rank_lg_flow_price_concordance', 'cs_rank_turnover_per_winner', 'cs_rank_volume_ratio', 'cs_rank_elg_buy_sell_sm_ratio', 'cs_rank_cost_dist_vol_ratio', 'cs_rank_size', 'cat_up_limit', 'industry_obv', 'industry_return_5', 'industry_return_20', 'industry__ema_5', 'industry__ema_13', 'industry__ema_20', 'industry__ema_60', 'industry_act_factor1', 'industry_act_factor2', 'industry_act_factor3', 'industry_act_factor4', 'industry_act_factor5', 'industry_act_factor6', 'industry_rank_act_factor1', 'industry_rank_act_factor2', 'industry_rank_act_factor3', 'industry_return_5_percentile', 'industry_return_20_percentile', '000852.SH_MACD', '000905.SH_MACD', '399006.SZ_MACD', '000852.SH_MACD_hist', '000905.SH_MACD_hist', '399006.SZ_MACD_hist', '000852.SH_RSI', '000905.SH_RSI', '399006.SZ_RSI', '000852.SH_Signal_line', '000905.SH_Signal_line', '399006.SZ_Signal_line', '000852.SH_amount_change_rate', '000905.SH_amount_change_rate', '399006.SZ_amount_change_rate', '000852.SH_amount_mean', '000905.SH_amount_mean', '399006.SZ_amount_mean', '000852.SH_daily_return', '000905.SH_daily_return', '399006.SZ_daily_return', '000852.SH_up_ratio_20d', '000905.SH_up_ratio_20d', '399006.SZ_up_ratio_20d', '000852.SH_volatility', '000905.SH_volatility', '399006.SZ_volatility', '000852.SH_volume_change_rate', '000905.SH_volume_change_rate', '399006.SZ_volume_change_rate']\n",
+ "[]\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "print(len(feature_columns))\n",
+ "print(feature_columns)\n",
+ "print([col for col in feature_columns if 'total_mv' in col])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e53b209a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "5595 2057465\n",
+ " ts_code trade_date turnover_rate\n",
+ "0 000001.SZ 2023-01-03 1.1307\n",
+ "1 000001.SZ 2023-01-04 1.1284\n",
+ "2 000001.SZ 2023-01-05 0.8582\n",
+ "3 000001.SZ 2023-01-06 0.6162\n",
+ "4 000001.SZ 2023-01-09 0.5450\n",
+ "... ... ... ...\n",
+ "1781701 605599.SH 2025-05-26 0.6188\n",
+ "1781702 605599.SH 2025-05-27 1.2576\n",
+ "1781703 605599.SH 2025-05-28 2.0432\n",
+ "1781704 605599.SH 2025-05-29 2.0954\n",
+ "1781705 605599.SH 2025-05-30 1.4392\n",
+ "\n",
+ "[1781706 rows x 3 columns]\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "print(len(train_data[train_data['pct_chg'] > 7]), len(train_data))\n",
+ "print(test_data[['ts_code', 'trade_date', 'turnover_rate']])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "364e821a",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, roc_curve\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "def calculate_binary_classification_metrics(df: pd.DataFrame, score_col: str, label_col: str, future_return_col: str = None, total_mv_col: str = None, n_mv_bins: int = 10, threshold: float = 0.5):\n",
+ " \"\"\"\n",
+ " 计算二分类模型的评估指标,可选择计算 score 和 future_return 的相关性,\n",
+ " 并可选择计算 score 在按总市值 (total_mv) 分为 n 份后的每个分组上的预测性能(ROC AUC)。\n",
+ "\n",
+ " Args:\n",
+ " df: 包含 score (预测概率或置信度), label (真实二分类标签), 可选的 future_return 和 total_mv 的 Pandas DataFrame。\n",
+ " score_col: 包含模型预测 score 的列名。\n",
+ " label_col: 包含真实二分类标签 (0 或 1) 的列名。\n",
+ " future_return_col: (可选) 包含未来收益率的列名,用于计算相关性。\n",
+ " total_mv_col: (可选) 包含总市值的列名,用于按市值分 n 份分析预测性能。\n",
+ " n_mv_bins: (可选) 将总市值分为多少份,默认为 5。\n",
+ " threshold: 将 score 转换为预测类别的阈值,默认为 0.5。\n",
+ "\n",
+ " Returns:\n",
+ " 一个包含以下评估指标的字典:\n",
+ " - accuracy: 准确率\n",
+ " - precision: 精确率\n",
+ " - recall: 召回率\n",
+ " - f1: F1 分数\n",
+ " - roc_auc: ROC AUC 值\n",
+ " - fpr: ROC 曲线的假正率 (False Positive Rate)\n",
+ " - tpr: ROC 曲线的真正率 (True Positive Rate)\n",
+ " - thresholds: ROC 曲线的阈值\n",
+ " - score_return_correlation: (如果 future_return_col 提供) score 和 future_return 的皮尔逊相关系数\n",
+ " - mv_roc_auc: (如果 total_mv_col 提供) 一个字典,包含按总市值分为 n 份后的每个市值分组对应的 ROC AUC 值\n",
+ " \"\"\"\n",
+ " y_true = df[label_col].values\n",
+ " y_score = df[score_col].values\n",
+ " y_pred = (y_score >= threshold).astype(int)\n",
+ "\n",
+ " metrics = {}\n",
+ " metrics['accuracy'] = accuracy_score(y_true, y_pred)\n",
+ " metrics['precision'] = precision_score(y_true, y_pred)\n",
+ " metrics['recall'] = recall_score(y_true, y_pred)\n",
+ " metrics['f1'] = f1_score(y_true, y_pred)\n",
+ " metrics['roc_auc'] = roc_auc_score(y_true, y_score)\n",
+ " metrics['fpr'], metrics['tpr'], metrics['thresholds'] = roc_curve(y_true, y_score)\n",
+ "\n",
+ " if future_return_col in df.columns:\n",
+ " metrics['score_return_correlation'] = df[score_col].corr(df[future_return_col])\n",
+ "\n",
+ " if total_mv_col in df.columns and n_mv_bins > 1:\n",
+ " metrics['mv_roc_auc'] = {}\n",
+ " df['mv_quantile'] = pd.cut(df[total_mv_col], bins=n_mv_bins, labels=False, duplicates='drop')\n",
+ " for i in range(df['mv_quantile'].nunique()):\n",
+ " mv_group = df[df['mv_quantile'] == i]\n",
+ " if len(mv_group) > 0 and len(np.unique(mv_group[label_col])) > 1 and len(np.unique(mv_group[score_col])) > 1:\n",
+ " roc_auc_mv = roc_auc_score(mv_group[label_col], mv_group[score_col])\n",
+ " lower_bound = df[total_mv_col][df['mv_quantile'] == i].min()\n",
+ " upper_bound = df[total_mv_col][df['mv_quantile'] == i].max()\n",
+ " metrics['mv_roc_auc'][f'{lower_bound:.0e}-{upper_bound:.0e}'] = roc_auc_mv\n",
+ " else:\n",
+ " lower_bound = df[total_mv_col][df['mv_quantile'] == i].min()\n",
+ " upper_bound = df[total_mv_col][df['mv_quantile'] == i].max()\n",
+ " metrics['mv_roc_auc'][f'{lower_bound:.0e}-{upper_bound:.0e}'] = np.nan\n",
+ " print(f'{lower_bound:.0e}-{upper_bound:.0e}')\n",
+ " df.drop(columns=['mv_quantile'], inplace=True)\n",
+ "\n",
+ " return metrics\n",
+ "\n",
+ "def plot_roc_curve(metrics: dict):\n",
+ " plt.figure(figsize=(8, 6))\n",
+ " plt.plot(metrics['fpr'], metrics['tpr'], color='darkorange', lw=2, label=f'ROC curve (AUC = {metrics[\"roc_auc\"]:.2f})')\n",
+ " plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n",
+ " plt.xlabel('False Positive Rate')\n",
+ " plt.ylabel('True Positive Rate')\n",
+ " plt.title('Receiver Operating Characteristic (ROC)')\n",
+ " plt.legend(loc=\"lower right\")\n",
+ " plt.grid(True)\n",
+ " plt.show()\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1f6e6336",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "6e+04-9e+04\n",
+ "9e+04-1e+05\n",
+ "1e+05-1e+05\n",
+ "1e+05-1e+05\n",
+ "1e+05-2e+05\n",
+ "2e+05-2e+05\n",
+ "2e+05-2e+05\n",
+ "2e+05-2e+05\n",
+ "2e+05-3e+05\n",
+ "3e+05-3e+05\n",
+ "二分类评估指标:\n",
+ "accuracy: 0.6449\n",
+ "precision: 0.4384\n",
+ "recall: 0.2532\n",
+ "f1: 0.3210\n",
+ "roc_auc: 0.6147\n",
+ "fpr: (array of length 7456)\n",
+ "tpr: (array of length 7456)\n",
+ "thresholds: (array of length 7456)\n",
+ "score_return_correlation: -0.0356\n",
+ "mv_roc_auc: {'6e+04-9e+04': np.float64(0.5291280148423005), '9e+04-1e+05': np.float64(0.5695028952947505), '1e+05-1e+05': np.float64(0.5623844792554237), '1e+05-2e+05': np.float64(0.5622699726201068), '2e+05-2e+05': np.float64(0.6035659704533877), '2e+05-3e+05': np.float64(0.6119956359669062), '3e+05-3e+05': np.float64(0.5959528412973004)}\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "\n",
+ "for sd in [\n",
+ " # save_df1, \n",
+ " # save_df2, \n",
+ " score_df\n",
+ " ]:\n",
+ " # 计算二分类指标\n",
+ " evaluation_metrics = calculate_binary_classification_metrics(sd, score_col='score', label_col='label', threshold=0.6,\n",
+ " future_return_col='future_return', total_mv_col='total_mv')\n",
+ "\n",
+ " # 打印指标\n",
+ " print(\"二分类评估指标:\")\n",
+ " for metric, value in evaluation_metrics.items():\n",
+ " if isinstance(value, (float, int)):\n",
+ " print(f\"{metric}: {value:.4f}\")\n",
+ " elif isinstance(value, (list, tuple, np.ndarray)):\n",
+ " print(f\"{metric}: (array of length {len(value)})\")\n",
+ " else:\n",
+ " print(f\"{metric}: {value}\")\n",
+ "\n",
+ " # 绘制 ROC 曲线\n",
+ " plot_roc_curve(evaluation_metrics)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7e9023cc",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "def analyze_factors(\n",
+ " df: pd.DataFrame,\n",
+ " feature_columns: list[str],\n",
+ " target_column: str = 'target', # 假设目标列默认为 'target'\n",
+ " trade_date_col: str = 'trade_date', # 假设日期列默认为 'trade_date'\n",
+ " mcap_col: str = 'total_mv', # 新增: 市值列名称\n",
+ " mcap_bins: int = 5 # 新增: 市值分位数的数量 (例如 5 表示五分位数)\n",
+ ") -> pd.DataFrame:\n",
+ " \"\"\"\n",
+ " 分析DataFrame中指定特征列的各种指标,包括基本统计、相关性、日间IC、ICIR以及在不同市值分位数上的IC。\n",
+ "\n",
+ " Args:\n",
+ " df (pd.DataFrame): 包含日期、目标列、特征列和市值列的DataFrame。\n",
+ " 需要包含 trade_date_col, target_column, feature_columns 和 mcap_col 中的所有列。\n",
+ " feature_columns (list[str]): 需要分析的特征列名称列表。\n",
+ " target_column (str): 目标变量列的名称。\n",
+ " trade_date_col (str): 交易日期列的名称。\n",
+ " mcap_col (str): 市值列的名称。\n",
+ " mcap_bins (int): 市值分位数的数量 (例如 5 表示五分位数)。\n",
+ "\n",
+ " Returns:\n",
+ " pd.DataFrame: 包含各个因子分析指标的汇总DataFrame。\n",
+ " 同时打印因子在不同市值分位数上的平均IC表格。\n",
+ " 如果输入数据或列有问题,可能返回空或包含NaN的DataFrame。\n",
+ " \"\"\"\n",
+ "\n",
+ " # --- 数据校验 ---\n",
+ " required_cols = [trade_date_col, target_column, mcap_col] + feature_columns\n",
+ " if not all(col in df.columns for col in required_cols):\n",
+ " missing = [col for col in required_cols if col not in df.columns]\n",
+ " print(f\"错误: 输入DataFrame缺少必需的列: {missing}\")\n",
+ " return pd.DataFrame() # 返回空DataFrame\n",
+ "\n",
+ " # 确保日期列是 datetime 类型\n",
+ " df = df.copy() # 在副本上操作\n",
+ " df[trade_date_col] = pd.to_datetime(df[trade_date_col], errors='coerce')\n",
+ " df.dropna(subset=[trade_date_col], inplace=True) # 移除日期转换失败的行\n",
+ "\n",
+ " # 过滤掉那些在 feature_columns, target_column, mcap_col 上有 NaN 的行,以确保后续计算是在完整数据上\n",
+ " # 直接在 df 副本上进行清洗\n",
+ " initial_rows_before_clean = len(df)\n",
+ " df.dropna(subset=feature_columns + [target_column, mcap_col], inplace=True)\n",
+ " rows_dropped_clean = initial_rows_before_clean - len(df)\n",
+ " if rows_dropped_clean > 0:\n",
+ " print(f\"警告: 移除了 {rows_dropped_clean} 行,因为其特征、目标或市值列存在空值。\")\n",
+ "\n",
+ " if df.empty:\n",
+ " print(\"错误: 清理缺失值后数据为空,无法进行因子分析。\")\n",
+ " return pd.DataFrame() # 返回空DataFrame\n",
+ "\n",
+ "\n",
+ " print(f\"开始分析 {len(feature_columns)} 个因子指标...\")\n",
+ "\n",
+ " # --- 1. 基本因子统计量 ---\n",
+ " basic_stats = df[feature_columns].describe().T\n",
+ "\n",
+ " print(\"\\n--- 基本因子统计量 ---\")\n",
+ " print(basic_stats)\n",
+ "\n",
+ " # --- 2. 因子与目标变量的整体相关性 ---\n",
+ " overall_correlation = {}\n",
+ " for feature in feature_columns:\n",
+ " # 在清理后的 df 上计算相关性\n",
+ " if df[[feature, target_column]].dropna().shape[0] > 1: # 确保至少有两个有效数据点\n",
+ " overall_correlation[feature] = {\n",
+ " 'Pearson_Correlation_with_Target': df[feature].corr(df[target_column], method='pearson'),\n",
+ " 'Spearman_Correlation_with_Target': df[feature].corr(df[target_column], method='spearman')\n",
+ " }\n",
+ " else:\n",
+ " overall_correlation[feature] = {\n",
+ " 'Pearson_Correlation_with_Target': np.nan,\n",
+ " 'Spearman_Correlation_with_Target': np.nan\n",
+ " }\n",
+ " overall_corr_df = pd.DataFrame.from_dict(overall_correlation, orient='index')\n",
+ "\n",
+ " print(\"\\n--- 因子与目标变量的整体相关性 ---\")\n",
+ " print(overall_corr_df)\n",
+ "\n",
+ " # --- 3. 因子之间的相关性矩阵 ---\n",
+ " # 在清理后的 df 上计算相关性\n",
+ " factor_correlation_matrix = df[feature_columns].corr(method='spearman') # 改回 Spearman\n",
+ "\n",
+ " print(\"\\n--- 因子之间的相关性矩阵 (Spearman) ---\") # 修正打印信息\n",
+ " print(factor_correlation_matrix)\n",
+ "\n",
+ " # --- 4. 日间 IC 和 ICIR ---\n",
+ " print(\"\\n--- 计算日间 IC (Spearman 相关性) 和 ICIR ---\")\n",
+ "\n",
+ " # 直接在清理后的 df 上计算每日 IC\n",
+ " if df.empty: # 理论上上面已经检查过,这里再检查一次更安全\n",
+ " daily_ic_series = pd.Series(dtype=float) # 空 Series\n",
+ " ic_stats = pd.DataFrame({\n",
+ " 'Mean_IC (Spearman)': np.nan, 'Std_Dev_IC': np.nan, 'ICIR': np.nan\n",
+ " }, index=feature_columns)\n",
+ " else:\n",
+ " daily_ic_series = df.groupby(trade_date_col).apply(\n",
+ " lambda day_group: {\n",
+ " feature: day_group[feature].corr(day_group[target_column], method='spearman')\n",
+ " for feature in feature_columns if day_group.shape[0] > 1 # 确保每日数据点多于1才能计算相关性\n",
+ " }\n",
+ " ).apply(pd.Series) # 将字典结果转换为 DataFrame\n",
+ "\n",
+ " # 计算 IC 的统计量\n",
+ " if not daily_ic_series.empty:\n",
+ " ic_mean = daily_ic_series.mean()\n",
+ " ic_std = daily_ic_series.std()\n",
+ " # 避免除以零\n",
+ " ic_ir = ic_mean / ic_std.replace(0, np.nan) # 使用 replace 0 为 NaN\n",
+ "\n",
+ " ic_stats = pd.DataFrame({\n",
+ " 'Mean_IC (Spearman)': ic_mean,\n",
+ " 'Std_Dev_IC': ic_std,\n",
+ " 'ICIR': ic_ir\n",
+ " })\n",
+ " print(\"\\n--- 日间 IC 和 ICIR (Spearman) ---\")\n",
+ " print(ic_stats)\n",
+ " else:\n",
+ " ic_stats = pd.DataFrame({\n",
+ " 'Mean_IC (Spearman)': np.nan, 'Std_Dev_IC': np.nan, 'ICIR': np.nan\n",
+ " }, index=feature_columns)\n",
+ "\n",
+ "\n",
+ " # --- 5. 因子在不同市值分位数上的平均 IC ---\n",
+ " print(f\"\\n--- 计算因子在 {mcap_bins} 个市值分位数上的平均 IC (Spearman) ---\")\n",
+ "\n",
+ " # 在清理后的 df 上计算每日市值分位数,直接添加到 df 中\n",
+ " # 使用 transform() 和 qcut() 在每个日期分组内计算分位数\n",
+ " # labels=False 返回整数 0 to mcap_bins-1\n",
+ " # duplicates='drop' 处理在某些日期股票数量少于 bins 导致分位数边缘重复的情况,会返回 NaN\n",
+ " # 添加一个临时列来存储分位数\n",
+ " mcap_bin_col_name = f'_mcap_bin_{mcap_bins}'\n",
+ " df[mcap_bin_col_name] = df.groupby(trade_date_col)[mcap_col].transform(\n",
+ " lambda x: pd.qcut(x, q=mcap_bins, labels=False, duplicates='drop') if len(x) >= mcap_bins else np.nan # 确保股票数量足够进行分位数划分\n",
+ " )\n",
+ "\n",
+ " # 过滤掉无法划分分位数 (NaN) 的行,进行分位数 IC 计算\n",
+ " # 创建一个临时 DataFrame df_binned_analysis\n",
+ " df_binned_analysis = df.dropna(subset=[mcap_bin_col_name]).copy()\n",
+ "\n",
+ " if df_binned_analysis.empty:\n",
+ " print(\"错误: 划分市值分位数后数据为空,无法计算分位数上的 IC。\")\n",
+ " avg_ic_by_bin = pd.DataFrame(index=range(mcap_bins), columns=feature_columns) # Placeholder\n",
+ " else:\n",
+ " # 按日期和市值分位数分组,计算每个分组内的因子与目标变量的截面相关性 (分位数IC)\n",
+ " binned_ic_by_day = df_binned_analysis.groupby([trade_date_col, mcap_bin_col_name]).apply(\n",
+ " lambda group: {\n",
+ " feature: group[feature].corr(group[target_column], method='spearman')\n",
+ " for feature in feature_columns if group.shape[0] > 1 # 确保分位数组内数据点多于1\n",
+ " }\n",
+ " ).apply(pd.Series) # 将嵌套结果转为 DataFrame\n",
+ "\n",
+ " # 对每个分位数组的每日 IC 求平均\n",
+ " # unstack(level=mcap_bin_col_name) 将 mcap_bin 作为列\n",
+ " # mean(axis=0) 对日期索引求平均\n",
+ " avg_ic_by_bin = binned_ic_by_day.unstack(level=mcap_bin_col_name).mean(axis=0).unstack()\n",
+ "\n",
+ " # 重命名索引和列,使表格更清晰\n",
+ " if not avg_ic_by_bin.empty:\n",
+ " # Index name will be the original column name used for grouping ('_mcap_bin_X')\n",
+ " # Rename the index name explicitly\n",
+ " avg_ic_by_bin.index.name = 'MarketCap_Bin'\n",
+ " avg_ic_by_bin.columns.name = 'Feature'\n",
+ " # 可以根据需要对分位数 bin 索引进行排序 (虽然 pd.qcut labels=False usually sorts)\n",
+ " avg_ic_by_bin = avg_ic_by_bin.sort_index()\n",
+ "\n",
+ " print(avg_ic_by_bin)\n",
+ "\n",
+ "\n",
+ " # --- 6. 汇总所有指标 ---\n",
+ " # 将基本统计、整体相关性、IC/ICIR 合并到一个 DataFrame\n",
+ " # 注意:合并时需要根据索引进行对齐 (因子名称)\n",
+ " summary_df = basic_stats\n",
+ " summary_df = summary_df.merge(overall_corr_df, left_index=True, right_index=True, how='left')\n",
+ " summary_df = summary_df.merge(ic_stats, left_index=True, right_index=True, how='left')\n",
+ "\n",
+ " # print(\"\\n--- 因子分析汇总报告 ---\")\n",
+ " # print(summary_df)\n",
+ "\n",
+ " # --- 清理临时列 'mcap_bin' ---\n",
+ " # 修正:在函数结束时从我们一直在操作的 df 副本中删除临时列\n",
+ " if mcap_bin_col_name in df.columns:\n",
+ " df.drop(columns=[mcap_bin_col_name], inplace=True)\n",
+ "\n",
+ "\n",
+ " return summary_df # 主要返回汇总报告,分位数IC单独打印\n",
+ "\n",
+ "# # 运行分析函数\n",
+ "# factor_analysis_report = analyze_factors(test_data.copy(), feature_columns, 'future_return')\n",
+ "\n",
+ "# print(\"\\n--- 最终汇总报告 DataFrame ---\")\n",
+ "# print(factor_analysis_report)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a0000d75",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "开始分析 'score' 在 'circ_mv' 和 'future_return' 下的表现...\n",
+ "准备数据,处理 NaN 值...\n",
+ "原始数据 17430 行,移除 NaN 后剩余 17119 行用于分析。\n",
+ "对 'circ_mv' 和 'future_return' 进行 100 分位数分箱...\n",
+ "按二维分箱分组计算 Spearman Rank IC...\n",
+ "整理结果用于绘图...\n",
+ "circ_mv_bin 0 1 2 3 4 5 6 7 8 9 ... 90 91 92 \\\n",
+ "future_return_bin ... \n",
+ "0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "1 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "2 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "3 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "4 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "... .. .. .. .. .. .. .. .. .. .. ... .. .. .. \n",
+ "95 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "96 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "97 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "98 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "99 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN \n",
+ "\n",
+ "circ_mv_bin 93 94 95 96 97 98 99 \n",
+ "future_return_bin \n",
+ "0 NaN NaN NaN NaN NaN NaN NaN \n",
+ "1 NaN NaN NaN NaN NaN NaN NaN \n",
+ "2 NaN NaN NaN NaN NaN NaN NaN \n",
+ "3 NaN NaN NaN NaN NaN NaN NaN \n",
+ "4 NaN NaN NaN NaN NaN NaN NaN \n",
+ "... .. .. .. .. .. .. .. \n",
+ "95 NaN NaN NaN NaN NaN NaN NaN \n",
+ "96 NaN NaN NaN NaN NaN NaN NaN \n",
+ "97 NaN NaN NaN NaN NaN NaN NaN \n",
+ "98 NaN NaN NaN NaN NaN NaN NaN \n",
+ "99 NaN NaN NaN NaN NaN NaN NaN \n",
+ "\n",
+ "[100 rows x 100 columns]\n",
+ "生成热力图...\n",
+ "分析完成。\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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VuncuXN6+eWXs5T39/dX9+7d9d1332nHjh05XJ3nmDJliiSpT58+9gV0SXruuec0YcIE/fjjjyyie7j27dtr4sSJWrhwoV566SWzywEAAIDJWEQHAAAeJzIyUn/++aeCg4PT7UR3dJyLYRhKTk5WcnKyS2qw2WwqVqxYlrLz5s3T1KlTVbRoUY0aNSrd2eU3CgsL05QpU9S3b1+tXbtWzz77rF544QV1797dvuh6//33S5IuXbqk2NhYSdd+QZB6PZW/v7/279+vL7/8MsO5rl69qitXrqh///5ZupfMrFmzRg899FC2duYWKVJEknTlypVbnt8bJCYmasOGDfL391f9+vXTvJYnTx6VKFFC//zzjy5cuJDmmCB4lsDAQEVFRWn16tUsogMAAIBFdAAAPMG///6rCRMmaOPGjTpz5oyKFi2qBg0aqHfv3vZFyFQ2m00zZ87UggULdOTIERUpUkTVqlVTnz59VLFixXTvvXTpUs2YMUMHDhxQWFiYHnjgAb366quqUKFCmtyND2VctmyZ5syZo3379mnGjBnpzrPes2ePJk2apC1btighIUHly5dXx44d1bZtW6e/DwcPHlRwcLACAwMdLkTbbDbZbDb9+++/Dl8/efKkkpKS7DXv2bNHc+bMUXBwsCwWixITE/XBBx8oKSlJderUUVRUVLr3SUlJydIi+rx58/T2228rKChIbdu21bvvvquQkBAFBATcdDG9atWqqlSpkqZMmaJp06YpJCREffv2TZNZvHixEhMT7f/+559/VKZMGft7G4ahgwcPasaMGfbMhQsXZLPZlD9/fiUmJiopKckli+gWi+Wmu+xvtGfPHklK9xDVhIQETZs2TT/++KNOnDihggULqkaNGurXr5/Kli1rzy1cuFCDBg3SyJEjVa1aNX388cfatm2bAgMD9eCDD+qtt97K9K8FUlJSNGjQIC1evFjdu3dP9/11tYMHDyo5OVklS5a0P3T1ekOGDNH58+ez/fBXm82mhx9+WH5+flq3bl26ZwU0atRIVqtVv/32m/0vFK5cuaJp06bphx9+0PHjx5UnTx7dc8896tOnjyIiIpy/yWzau3evJk2apB07dig+Pl4lS5ZUq1at1Llz5wy/D+vXr9fo0aP1119/qUiRImrdurV69OiRLpucnKyvv/5aS5cu1ZEjRxQeHq6HHnpI/fv3V6lSpW657lq1amnhwoUyDOOm/y0DAAAgd2MRHQAAk8XHx6tDhw46duyYGjVqpHLlyuno0aOaO3eudu3apQULFtgXzGw2m3r27Km1a9eqXLlyat++vc6ePauVK1fql19+0cyZM9McCzJixAjNmDFDxYsX15NPPqmzZ89qxYoVWrdunaZOnaqaNWtmWNOQIUM0f/58lS5dWmXLllVoaGia19etW6devXopPDxcTZs2VWhoqH755RcNGTJE//77r3r37u3U96Jjx472h2jezMMPP3zTzL59+yRdOx5m4cKFCgoKUnJysq5evaqFCxcqKSlJefLkyXAR/ezZs6pevXqm72+1WjVnzhz5+fnpo48+0oULF/T5559nqX5JeuSRRzR58mTdd999mjdvnl599dU0rxuGoZkzZ9r//dNPP+l///uf3n33XbVt21Y2m02GYahly5Z677337Ll27drpzJkzGT589VacPHkyw4eJZiQpKUk7d+7UkCFD7DWlunr1qrp166bNmzerVq1aatiwoc6cOaOffvpJO3bs0Pfff5/mGBRJOnDggD744ANVrFhRTz31lH7//Xf98MMPSkhIcHjWumEYGjZsmNsW0CXpxIkTkpTul1+psvJzm5GAgAC1aNFC06ZN09atW1W7dm37a3/++aeOHTum559/Ps0RP6+//rp+/vln3XvvvXr22WcVHx+v5cuXq2PHjlq8eLFLFppvZs+ePerQoYNSUlL02GOPqVChQtq5c6fGjBmjU6dOaejQoWnycXFx6tq1q+677z61bdtWGzZs0Oeff669e/fq888/ty9mJycn6+WXX9bGjRtVq1Yt1a9fX0ePHtVPP/2kTZs2adGiRVn+SxJHSpQoIavVqvPnz3MuOgAAgI9jER0AAJNt2rRJR48e1dNPP60PPvjAfv2jjz7S8uXLdfToUd1xxx2Srj3scu3atXr44Yc1ceJE+87MH374Qa+99pomTpxoP4953bp1mjFjhqpUqaKZM2faj4749ddf9corr+j111/XihUr0u3uXLJkieLj4zV16tR0x1FI0uXLlzVw4EDlyZNHixYt0u233y5J6tu3r5544gl9+eWXio6OVsGCBbP9vXjttdfk7++vvHnzKjw83H7darVq6NChOnv2rF5//XWVK1dOSUlJGS5s2Ww2JSYm6ty5c/ZrTZo0kcVikSQNHDhQq1at0tatW9ONS3Xx4kVdvHgx3e5p6drO5tRfaoSHh2v27NnaunWrHnroISUmJqpFixYKDQ3Vli1b1KlTJ3Xu3Dnd+dcxMTF6+umn7QvSjRo1UqNGjdLN9f333+vvv/9WoUKFdO7cOdWtW1eLFi3SJ598ombNmunq1auSrh0P4w6JiYk3PYKkSpUqaf7t5+en559/Xh06dLBf++OPP7R582b7LxFSVa9eXcOHD9e6devUokWLNO/z9ddf6/nnn9dbb70lPz8/Wa1WPfroo1q3bp2uXLmS7hc9kjR8+HDNnz/fbQvo0rWfVUkKCQlx+Xu3atXKvnv/+kX0H3/80f56qosXL+rnn39WuXLlNGfOHPvPbIMGDfThhx9q27ZtbllE/+qrr2S1WjVu3Dg1b97cfv2pp57S/Pnz9dZbb6XZVX/27FlFR0fbf/litVr1/PPPa+3atVq9erWaNGkiSfrmm2+0ceNGvfzyy3r99dft46dPn66RI0dq2rRpevPNN2+p9tSf9ev/EgQAAAC+iUV0AABMlrpb8tChQ4qPj1e+fPkkSW+88YbeeOONNNnFixdLurYQfP3id9OmTTVmzJg0x0d89913kqT+/funWfhs0KCBmjZtqpUrV2rDhg165JFH0swRFxen6dOn64EHHsiw3vXr1+vs2bOKiIjQnDlz0ryWL18+nThxQtu3b89wUfhmMnog6OXLl9WzZ0+dPXtWvXv3VocOHdSwYUNFRUXp7bfftucuXbqkDz74QAMGDMjyw0Cvd/1Z6qlnkN945I10bYf19Qu2oaGheuihhyRdWzhNXTw9evSoJKlcuXLp3uP06dOSlOmu7qSkJI0bN05BQUFq166dJk+erEKFCumFF17QpEmTNGnSJD399NOSpKJFi2bnVp0WGhqq//77L9NMt27dJF07P33//v0aNWpUmsVdSXrooYfsfyWQ6siRI9q1a5eka0fW3KhkyZJ644037DuRw8PDVbNmTS1fvlz//fdfugXhESNGaPbs2apatarbFtAl2eu7/pcyrlK1alVVqVJFK1eu1LBhwxQQEKCUlBQtX75cFSpUULVq1ezZPHnyKDw8XOfOndORI0dUvnx5SdKjjz6qRx991OW1OfLJJ5/ok08+sf87KSlJ27Zt04ULF5SYmKiTJ0/afxEnScHBwWn+IiM8PFydO3fW66+/rjVr1tgX0ZcuXSrp2pE1Y8eOtefj4+MlXfvl5K06e/asJPf9kgoAAACei0V0AABMdu+996ply5ZaunSpHnzwQVWpUkURERGqU6eOmjZtmmax/NChQwoJCbEviKUKCQnR448/nubaX3/9JUkZnn189913a+XKlfrrr7/SLaI3atTI4QK6JB0+fFjStYXm1MXmG508edLxDWdDXFycevbsqX379qlevXrq1auXpGtnFS9atEjdunVTiRIlZLPZ9Nprr+mXX37R3r17NXv27GwvfN1zzz32Hf+//PKLpGu7WmfMmKG5c+fq/vvvl81mU0pKSpbeLyYmRlL6ndlS1hbRJ0yYoGPHjqlDhw5pFhk7duyov//+W23btrX34rbbbstSTbeqVKlSN+1tv379JEk1a9bUyy+/rEWLFqVbRJeufQ/mzp2rLVu2aO/evTp//rx9R3JG3+NmzZql+6uJ1B4bhpHm+vz583Xy5ElVrFhRe/fu1S+//JLu5zynpP4SLHUx90bt27fXnj17tGbNGqeOG2ndurVGjRqljRs36qGHHtLWrVt16tSpNDv9pWvHv7z55psaPny4oqKiVL58eUVERKhGjRqKiopy6hdNztq0aZOWLVumHTt26NChQ0pOTk5zRNX1SpYsme4on0qVKkn6v19MSdLff/8tSWmOO7qeKz6DTp48qXz58vEAWAAAAMj/5hEAAJDTPv74Yy1YsEC9evVS2bJl9fvvv6t///5q2bKlLly4kKX3uHLlihISEtItKGb3gXh33XVXpq+nvv/gwYO1b9++DL+effbZbM2Zke+++05PPfWUDhw4IElpjofp2bOnrly5omHDhikxMVF9+vTRL7/8oqZNm+qbb75xaudov3799M4778hms2nFihWqVq2aAgICdOzYMUnXFnGHDRuW5pgZR1JSUvT7778rLCxMkZGR6V5PXUQvXrx4huPXrVunKVOmqGDBgunOly9UqJDGjh2rihUr2h/ambrImNPuuecebdiwIUvZBg0a6O6779bGjRu1Y8eONK/FxMSoWbNm+vLLL1WoUCG9/PLLmjJlisOzzaWMd/Q7cvLkSb3yyiuaN2+eihUrpvfff99tR3Kk/vXCsWPHMtyNfvr0aSUnJ6dbKM6qFi1aKCAgwH6Ey48//ig/Pz+1bNkyXfaZZ57Rzz//rCFDhuj+++/X4cOH9d5776lJkybauXOn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7LHHVKxYMc2ePVvPPfecYmJi9MYbb6h27dqKiopSr1697At0jz32mOrWrauPP/5YGzZs0IQJE/T000/bF9D//fdfdenSRb/++qsGDBig2rVr2+e/fpfrnj17NGfOHPtDW6935MgRPfvss9q0aZPq1auX4SKeYRjavHmz9u3bl2a39M0kJydL+r8Hbm7btk1z5sxReHi4/diZ69lstkyPRrkVjRs3Vrt27dS/f3/t3bv3pvmWLVuqVKlSWrt2rf0Yob59+6p79+5KSkrSrFmztHv3bj3//POaOXOm/P39tWbNGpeeYX7//ferefPm+u2337Ry5UqXva8j/v7+GjdunIYMGaLg4GB9++23OnLkiJ5//nnNnj37lhdmw8LC7AvKTZo0yfD9goODNWvWLPXq1cv+ENelS5eqRIkSeu+999SjR49bqiErgoODNW3aNDVp0kRbt27VnDlzFBAQoLFjx9r/SmPZsmVpxpQrV06ffvqpveZLly6pS5cu9p3h17/3V199pf79+8tms2nu3Ln6448/1LRpUy1atEj33nuvUzUfP35c3bp1U926dfXcc885f/MAAADIVfyMnPr/YQEAALjAqlWr1LNnT0nSnXfeqWHDhqlWrVrpcgcPHtS3336r77//XhcvXtSTTz6pDz/8UNK1xeeMzjY/e/asZs6cqenTpys5OVnDhg1Tu3bt7K937txZGzdu1NChQxUSEqKPP/5YSUlJ+vXXX+1nr584cUIzZszQt99+q6SkJDVr1izDXf7R0dHatm2bfVH+jTfe0EsvvZSl78HUqVP18ccfa9q0aSpVqpSeffZZnT17Vj169FCfPn3S5Vu0aKFz587p999/z9L7Z1dSUpK6dOmiv/76S8uXL3f6IZmAJ7l69apatmypgIAAzZ49O8vPVwAAAEDux3EuAADAozVo0ED33nuvatWqpT59+jg8gqZixYoaNmyYBg8erD///DPNUSmOHg76yy+/aOrUqapSpYref//9NEeoSFKHDh104MABDR8+XNK1vxro169fmsW1lJQU/frrrwoMDNSbb76p559/PsO5OnTooMOHDysyMlItW7ZUVFRUlr8HqeffJyYmqmzZsho+fLi+/PJL+4M7M8pfvnw5y++fXcHBwfriiy+0YcMGFtCRawQGBuqtt95S1apVWUAHAABAGuxEBwAAHi8pKSnHHp55+PBhlStXzuFDSbPi6NGjCg4OVvHixV1Y2f85e/aszpw5o5IlS9qP7jAM45ZqBgAAAABkDYvoAAAAAAAAAAA4wINFAQAAAAAAAABwgEV0AAAAAAAAAAAcYBEdAAAAALxMYmLiTTOc3AkAAOAaLKIDAHKVn376Sd99953D1xcvXqwffvghS++VkJCgpKQkpaSkZHn+q1evKiEhQefPn8/yGHiu5ORkXblyxewyAOQCP/zwgy5cuGD/95UrVzR8+HC99dZb2X6v8+fP68EHH1Tnzp0VFxeXYcZms+nxxx9Xt27d9N9//zldNwAAAKRAswsAAMCVPv/8c8XFxempp57K8PWRI0eqQIECeuKJJ276XkOGDNGyZcucqqNo0aJav369U2NhjpSUFJ0+fVqHDh3Svn37tHPnTv3666/q06ePOnbsaHZ5ALzYyZMnNXDgQBUpUkSrVq1SUFCQQkNDtX//fm3fvl0vvviiKlasmOX3mzhxouLj42Wz2ZSUlKSDBw9KuvaLv+DgYFWoUEE//vijDh48qHvvvVdFihSxjzUMQ5cvX1ZoaKj8/dlTBQAAkBUsogMAvNq5c+f0999/KyQkRCEhIQoICFBQUJB9QeFGgYGB9tcNw1BiYqIKFiyoUqVKpcvWrl1bhQoVUmBgoAICAtK89vPPPysuLk7t2rVT3rx57devXr2qpKQkhYaGuvZGkSNOnz6t7t276+zZszp16pSSk5Ptr4WGhqpUqVI6cOCAiRUCyA2mTJmi5ORkde7cWUFBQfbrffv2VYcOHTR8+HDNmDEjS++1detWzZ49W5K0adMmRUVFpXm9WbNmGj16tMaNGyfp2l9gLVmyxP66zWaTJK1evVqlS5e+ldsCAADwGX4GB+UBALzYqlWr1LdvX/sCekJCgmw2mwoUKJBhPj4+Xn5+fsqbN68Mw1BSUpI6d+6sPn36ZGverl276pdffvHIRYjo6Ght3rxZ+/btM7sUr/Dqq6/KZrPp9ttvV0hIiKZOnao33nhDL774ovz8/Oy57du3a9euXRm+R0BAgKKjo2+5luTkZH355ZdavHixTpw4oTx58qhu3br69NNPb/m9gRsdPHhQzzzzjAYNGqQ2bdpkmImPj9ekSZO0YsUKnTlzRvfcc48GDRqkyMjIdNmkpCR99dVX9p/fihUr6vXXX9eDDz7ospqHDRum33//XWvWrHGY2bBhgyZOnKi9e/cqPDxcTz31lHr37q3AwPT7hywWi8aNG6ddu3bJz89PzZs314ABA5QnTx6X1fz333/riSeeULFixbRixQoFBweneb1Pnz5avny5Bg0apE6dOmX6XkePHlXbtm118eJFTZkyRdu3b9eECRP0ySefKCIiQklJSQoPD9f8+fM1ZcoUde3aVTVq1NC0adO0e/duffTRR0pOTlZiYqKaNm3q0vsEAADIzdiJDgDwak2aNJHFYrH/Ozo6WocOHXJ4lMoTTzyh8PBwzZs3z10lwsONHz/e/n8fPHhQU6dOVXh4eJoFdOnawtyECRMyfI/w8HCXLKKPGzdOU6dO1QMPPKBHH31U586d059//nnL7wvc6OzZs+rWrZsuXrzoMJOQkKDOnTsrJiZG1apVU7NmzfTrr78qOjpaCxYsSHP8iM1m06uvvqq1a9eqYsWK6tChg7Zt26aXX35Z06ZNU926dW+55unTp2vu3LkZ/uVQqh9++EEDBgxQaGionnjiCSUlJWnq1Kk6ffq0RowYkSa7efNmdenSRTabTVFRUQoLC9PixYv1999/6+uvv073GeCMlJQUDR48WMnJyerfv3+6BXTp2tFhGzdu1OjRo1WpUiU99NBDGb5XTEyMevbsqbNnz6pPnz6qV6+eQkJCNGHCBB08eNB+TNn69es1bdo03X333erbt6/8/f31zz//aPPmzSpQoIBq1ap1y/cFAADga1hEBwDkOmfOnFGVKlUcvn7vvfe6sRr3GzVqlC5fvmx2GblO6hE9P//8s0qWLGm/3qlTJ/31118umeP777/XHXfckWYBLzsPts3I0aNH1bhxYz355JP68MMPXVGmT9i0aZM6duyoXr16qXfv3maX41IHDhxQjx499M8//2SamzRpkmJiYvT4449r9OjR8vf3V+/evdW6dWu99dZbmjNnjj07d+5crV27Vvfff7++/vprBQcHy2azKTo6WoMHD9aKFSvSHGOSHSkpKRo3bpwmT56cae7MmTMaNmyYgoKCNGvWLN19992SpBo1aujtt99WVFSUfYE6MTFRAwcOVFJSkr744gs98sgjkqSmTZuqS5cumjdvntq3b+9UvdebNm2atm3bpho1ajh8FkexYsX04YcfqkePHurdu7fGjx+v+vXrp8kYhqHPP/9cJ0+eVOvWrdW9e3dJ0l133aWAgABt3bpV0rXvVeov+9599137med169aVn5+fLBYLi+gAAABO4EkyAIBcJ2/evBo5cmSGXyVKlDC7vBxXsmTJbD2gzpddunRJycnJcnS6XVJSki5duiSr1Wo/CsLf31+BgYH2Lz8/v3Rn5jvr5MmTuu2229LsgOXBf3ClQ4cOqV27dpKuHUvlyNWrVzV37lwFBQVpyJAh9p/D8PBwvfTSS9qxY4cOHTpkz3/77beSpKFDh9p3WwcEBKhHjx46duyYNm7c6HTNb7/9tiZPnqzu3btnugt98eLFSkhI0LPPPmtfQJekp59+Wrfffru+++47+7U1a9bo2LFjatKkiX0BXZLq16+vGjVqpMk6a8+ePfr0008VHh6ukSNHZrqzvVGjRho6dKisVqu6du2qKVOmpPkFmp+fnyZMmKB3333X/l6tW7fWwIEDNXXqVH311VeSJKvVquHDh+vdd99VeHi4Dh48qIMHDyowMFCfffaZGjRokKZvAAAAyBp2ogMAcp2goCCHu83DwsJuOj4hIUFBQUEKCgpy+s/5DcOQzWZTYmKiQkJCMjyLF+Z78sknM9yN+8477+idd96x//vll19W0aJF3VgZkDPOnDljX7DN7Fzx2NhYxcfHq169eipcuHCa11LPOF+/fr0qVKigs2fP6q+//lLZsmVVtWrVNNnatWsrKChIGzZsUIMGDZyqOSEhQZ999pmaNGmipUuXOsxt2bJFkvToo4+muR4YGKi6detq7dq1N82m3t9nn32mS5cupXlwdHYcP35cXbt2VVJSkoYPH67y5cvfdEyHDh0UFhamoUOHavTo0Vq9erWGDBliP38+ICBAzzzzjD1/9OhR5cmTR/Xq1bNfW7dunfr375/pPOHh4dqxY4dT9wUAAOCr+P/RAwBynXPnzikqKsrh6zc7zqV9+/Y6cOBAluZq3LjxTTPffPON6tSpk6X3u9H27ds1adIk7dixQ/7+/ipfvryio6P1xBNPOFzgz8qDRRs1aiTp2m7MQ4cO6auvvtKGDRv02GOP6c0330yTPX/+vD777DP9/PPPOnv2rEqWLKkGDRqoR48eKliwYLbvaeHChRo0aJAGDx6smJgYrVq1SuXLl9f48eM1ffp0LVy4UEWLFtXIkSN1//33q2/fvvrpp5/0008/qUKFCmnea+DAgVq0aJEWLVqku+66K9u19OzZU8nJyfad5IsWLdLmzZvVunVr1alTRzabTcnJybrzzjvTnL3vSjcePbR58+Y0167/+Um934weaHt9T1OPcLle6vcp1fXvkdqTkSNHpnvAZEZz3njUybJlyzRnzhzt27dPM2bMSLeQumfPHk2aNElbtmxRQkKCypcvr44dO6pt27bZ/n45uueb/RxfunRJX375pZYvX67jx4+rYMGCatSokfr166dChQqlua/rTZw4URMnTrT/+/r/riZMmKCJEydm+N94Rv8dXv99bt26tebNm6dFixbp4MGDWrt2rfLly5emjl69eqldu3YaNWqU1q9fr5SUFNWoUUNDhw5VmTJlnPqe1axZU7Vr175p7tSpU5KU4X9XpUuXVlBQkP0XUJllg4ODVbJkyTS/rJo4caImTJigzp07a+DAgfbrY8eO1RdffKFu3bqpX79+9usff/xxlv7a49SpU/Lz88uwjrJly+r8+fOKj49Xvnz57DVfv2P9+qxhGIqLi1NERMRN571RXFycXnjhBZ06dUrt27fXypUr9dtvvykwMDBLf1kyYsQIffjhh9qxY4dee+01LVmyxH6c1PUCAwPTfV9Sf1E8ZcqUDH9pER0dneX/9w0AAAD/h0V0AECuU7RoUYcPFk1ddMtMkyZNVLt27QwXKFL9/PPPiouLU7t27TLcqZiSkiKbzabLly87vYN56dKlGjhwoIKCgtSsWTPlz59fq1ev1uuvvy6LxaJBgwY59b7X++233/Tqq69KkipXrpzuuJtTp07pueeeU1xcnO6//341a9bMvlC6Zs0aLVy4UPnz53dq7nHjxum+++5T9erVtXHjRj399NMqVqyYHnvsMS1cuFBjx47Vt99+q1atWumnn37SsmXL1KtXL/v4pKQkrVq1SnfeeadTC+iS1Lp1a/v/ffXqVX366aeSpOrVq6tZs2YKCgqyH02RU4vo3bp1s//fX3zxhUqWLKmWLVvar11//npW5c+f3/6+ly5d0qxZs1SlShU1bNgwTcYVhgwZovnz56t06dIqW7ZsusW+devWqVevXgoPD1fTpk0VGhqqX375RUOGDNG///7rkvPGb/ZzHB8fr+eee04HDhxQ/fr11bhxY+3fv19z587Vtm3btGDBAoWFhalkyZL279vx48e1dOlS3X///br//vtvucbrXb16Va+88op+++03VahQQRUqVMhwcfX06dP2z5jWrVtr+/btWrdunU6ePKlFixY5ddRPVo8eunTpkiQ5/PzKly+fTp48maVs/vz57Vnp2s/8mjVrNHPmTLVp00Z33nmnDh8+rK+++kp33nmnevbs6XTN4eHhGf7FUerP+7///qt8+fLZay5SpEi6bIECBSRdO17JmUX0n376yX5UzJtvvqn77rsvW+PfeOMN1a1bV4MGDdIbb7xh/29qyZIl+vjjjxUcHCx/f39dvHhRO3fuVMOGDZWUlKTo6Gj7Z+HFixd1+vTpdO99/S8NAQAAkHUsogMAvNY///yjCxcu2I9ekaTLly/LZrPp4MGDGY5JTk5WYmJimteTk5Nls9nsOxL79u1707kPHTqkuLg4de3aNd2OYFc4deqUhg4dqpCQEC1YsMB+xvmrr76q5s2b65tvvlGXLl1UrFgxp+eIj4/Xa6+9pg4dOqh79+7KkydPusy7776ruLg49enTRz169LBfHzZsmObOnau5c+fq5Zdfdmr+atWq6auvvtLevXvVqlUrSdLs2bPtvyw4ceKEpGtnFBcuXDjdIvqvv/6q+Pj4NIvQt2LBggX6999/JV078uKZZ57R3XffneMP47x+x+0XX3yh0qVLp7nmjPz589vf4+jRo5o1a5buuuuuW37fGy1ZskTx8fGaOnVqugchStf+exw4cKDy5MmjRYsW6fbbb5d07b+xJ554Ql9++aWio6Od+ouGVFn5OR47dqz279+vd955R88++6z9+gcffKBvvvlGCxcuVIcOHVSmTBn792jTpk1aunSp6tat6/IHi3755ZcyDEPz589XtWrVHObmzZunpk2basyYMQoKCpLNZtNTTz2l2NhY/f333+n+MsOVUhdaM9oBLV3bYX7lypUsZy9cuGD/d2BgoD788EO1adNGw4cP18yZM/Xee+/JMAyNGjXK/our7PL393d4ZFfqe6Y+dDm15ozyN2az65VXXlHVqlVVt25dBQcHa/PmzQoLC1NgYKAaN24sq9WqTZs2pRv3xBNP6MiRI/bjc6ZNm5bm9Vq1aumDDz5QcHCwfvzxR82fP18REREaPHiwrl69qhIlSth3/L/22msO6+NoKgAAgOxjER0A4LUmT56sBQsWZPhaZse5nDp1Kt3rme1eN8Py5ct15coVvfLKK2keEpo/f36NHTtWp0+ftv/iwFkXL15Uq1at9Prrr2f4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5+zZoKAgffTRR2mORvn333/17rvvqmjRoho8eLAk6c0339Svv/6q999/X7Vq1VLp0qWdqvnDDz9Up06dNHHiRH355Zey2Wyy2Wxq3LixnnvuuTTZgQMH6tChQ5o3b54WLVokwzB09epVVatWTX369HFq/ozMnTtXM2fOVIkSJRy+b+rxP44W0Q3D0JgxYxQUFKT3339ffn5+Gf6iJiu/vLl+5z0AAACyhkV0AIDXSl0AvV7qYnqqoKAgBQQE2B98l7q7NFWePHn0yiuvZGm+/fv3288iT915mpUjH5zVunVrlS5dWp9//rlWrVqlq1evqlKlSurRo4eefvrpHJv3eiVKlNCCBQs0adIkrVy5Ujt27FDRokX1/PPPq1evXm4/S75169b6448/VKJECdWtW/eW3itPnjwaN25cmgftZbYAlXr+u7+/f5oxfn5+SkxMvKVasuree+/V6NGjNWnSJPXu3VuFCxdW8+bN1bt3bz3xxBMOxw0dOlQFCxbUihUrtHbtWuXPnz9NPm/evPryyy/1wQcfaOTIkQoODtYDDzygBQsW6LPPPnN6EV2SGjZsqDlz5mjSpElav369kpKSVL58eQ0fPtxtP8f58+fX3Llz9cUXX2j58uWaPXu2ihcvrrZt26pnz54Oz6EeO3asxowZo3Xr1um///5TkSJFFB0dbX+9VKlSmjRpkkaPHq1BgwYpb968atiwoebNm+fSRVgzvfDCC6pZs6a++eYbxcXFqVKlSurWrVu6z1JJat68ue666y599dVX+uuvv1SyZEn72dypDMPQwIEDdeHCBY0fP95+vEq+fPn07rvvqlu3bnrjjTc0a9Yspz5fixYtqoULF2rmzJn69ddfFRgYqKioKLVp08a+SztVWFiYZsyYoQULFmjFihW6evWqGjZsqOeff94lD+BMSkrSp59+qqlTpypfvnyaPHlyhg8rPXfunL7//ntJcvjLAz8/P40bN067du1S5cqVJcn+C4Lrpf77+uNebpT6PQcAAEDW+Rm54W9NAQA+KykpSRMnTlRISIh99/n1Zs+erYSEhHQLCikpKbLZbEpKStLTTz+tMmXK3HSulStXqnfv3vZ/BwcHa8uWLQoNDXXNzcB0FotFTz31lIYOHarnn38+zWv//POPjh49qpo1ayokJMR+/fz587p69arDneAAfEtSUpK+//57ffHF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cZwLAAAAAAAAAHipxo0bZ/r66tWrM7weHx+vO+64I821AgUKKDY2NtP3i4+PV0BAgMqUKWO/lj9/fp08eTLD/KRJk1SyZEk1bNhQ0rVF+AYNGujFF19UcnKy3nvvPfXr108LFizIdN6s2LVrl5YvX659+/bp1KlTCggIULFixRQZGanHHntMVapUcep9WUQHAAAAAAAA4BM4E/3/BAYGKiQkJM210NBQWa3Wm44LDg5Ocy0sLCzDcRs3btTMmTP19ddfKzDw2lJ06rEvqd577z01adJEhw4dsu9Wz659+/bpvffe0/nz5/XYY4+pS5cuKlasmGw2m06fPq3NmzerZ8+eqly5st566y2VLl06W+/PIjoAAAAAAAAAeClHO81vplChQjpz5kyaa5cuXUq3QJ7RuMuXL+vSpUvKmzevpGu7028cFxcXp/79+6t///6qVauWw/crXry4JOno0aNOLaLPnz9f48aNU79+/fT000+ne71KlSp66KGH1KdPH3399dd65plnNGLECDVo0CDLc3AmOgAAAAAAAAD4mOrVq+vAgQO6ePGi/VpMTIxuv/32TMeVKVNGRYsW1bZt2xyOO3v2rLp27aqGDRvqxRdftF//77//1Lx58zS71lPfp2TJktm+hwMHDujrr7/W7NmzM1xAv15AQIC6dOmiiRMn6oMPPkhz3zfDIjoAAAAAAAAA+JjKlSurQoUKGjNmjFJSUrR7926tXLlSjRo1UkpKii5evCibzZZunL+/v6KiojR+/HhdunRJ586d0/Tp09WoUSNJUmJiorp06aLChQtr4MCBSkhIUEJCgpKSklSkSBHlzZtXb731liwWi9auXashQ4aoXr16aR5wmp17+OGHH1S2bNksj6levbp++ukn5c+fP8tjWEQHAAAAAAAA4BOMXPh1K0aOHKnVq1erXr16ateunVq2bKkGDRro+PHjqlWrlvbv35/huFdffVVBQUFq0KCBGjVqpNDQUHXr1k2S9Pvvv2v37t3asmWLatWqpfvuu0/33Xefhv0/9u49zsZ6////c81gWBhGTskg0bZYI5JDSQoVaqSDDk7ZqcghKeWQQyEiKaWDdnaF7N2mHZ3syuHbrvZWk1MulkNJiSK7jRlrGoz1+6Nf89ljrDHXmmvW+1qzHvfbbW41az3f7/fr6tXt+uPt8r4mTpQkPfPMMzpy5Ih69+6tCRMm6Oqrr9bcuXMjvoaEBPtb3HbHeEKhUHH/W8cUy7JMlwAAAAAAAAA4wu/3my4hpvzh75NNl+C47TdMLNb4YDCojIwMpaSkqHnz5kUed/LkSa1bt07Hjh1TmzZtVLZs2WLV4WZx+WLRU28uwWBQgUBAPp9PXq+3QN6yLFs3JPJm8+HGFNZnt10D+cjG0GP35p1agx5HL2+qJnoced6NNdFj82vQY7N5N9ZEj82vEQv5WOqxG2uKhbzJHkdjjXjLA8Xl9XrVsWNH2+MSEhIKfWFoaRKXm+gAAAAAAAAAgNKhU6dO8ng8Rc6vWrXK1vxsogMAAAAAAACIC6FQ0TdaETuGDx9eovOziQ4AAAAAAAAAiFnXX399ic5v/9WlAAAAAAAAAADECTbRAQAAAAAAAAAII6LjXA4ePKgPPvhA27dv14EDB5SYmKgaNWrI7/fryiuvVJUqVZyuEwAAAAAAAACKJ2S6AETT8ePHtW/fPp199tk6cuSIqlevHtE8tp5EP3jwoB566CGlp6dry5YtSktL02233aYbb7xRPp9P//73v3XVVVfpscceU1ZWVkQFAQAAAAAAAAAQqezsbI0ePVoXXnihunXrpl27dmnGjBnq2bOnDhw4YHu+Im+if/rpp+rRo4fOOeccrVmzRtOmTVOvXr3UsWNHderUSbfccouefPJJffTRRzpx4oTS09O1bds22wUBAAAAAAAAABCpxx9/XP/+97/10EMP6eTJk5Kku+66SwkJCZoxY4bt+Yq0ib5z506NGTNGzz33nEaMGKHy5cuHzSYnJ2vSpEkaN26c7r77bh05csR2UQAAAAAAAAAAROKjjz7ShAkT1K9fv7zPzj//fI0cOVKffvqp7fk8oVCoSCcBZWVlqVKlSrYmj2RMSbMsy3QJAAAAAAAAgCP8fr/pEmJK4yVTTZfguJ29xpsuwXXatGmjWbNm6bLLLlOTJk20bNkyNWnSRCtXrtS4ceP0xRdf2JqvyC8WTUxM1Nq1a9WuXbsiT+62DfTfnXpzCQaDCgQC8vl88nq9BfKWZdm6IZE3mw83prA+u+0ayEc2hh67N+/UGvQ4enlTNdHjyPNurIkem1+DHpvNu7Ememx+jVjIx1KP3VhTLORN9jgaa8RbHkBBV1xxhZ544gnVrFkz77MdO3Zozpw5uuKKK2zPV+RN9H379mnQoEHatGlTge/atm2rChUqKDEx8f8mLlNGPXv21D333GO7KAAAAAAAAAAAIjFu3DgNGzZMPXv2lCTddNNNys3N1UUXXaSxY8fanq/Im+hJSUkqU+b08cOHD2vSpEn5Plu7dq3mz5/PJjoAAAAAAAAAIGqqVKmihQsX6osvvtCOHTskSY0bN1bbtm0jmq/Im+gejyfvSfOnnnpKSUlJOn78uO69915JUvfu3fPl69atq2PHjunYsWMqV65cRMUBAAAAAAAAgFOK9nZIlBZt2rRRmzZtij1PkTfR/9e8efPUuXNnffbZZxo2bFje52+88YbKli2rnJwc3XbbbWrevHmxCwQAAAAAAAAAwI5PP/1Ur732mr777jvl5uaqfv366tu3rzp16mR7roRICvB4PHruuedUpUqVvN8l6fHHH9frr7+uxx57LJJpAQAAAAAAAAAolsWLF+vOO+9UVlaWrrjiCl111VU6fvy4hg4dqr/97W+254voSfTf/b55/rvatWvrzTffVOvWrYszLQAAAAAAAAAAEZk3b57uvPNOjRo1Kt/nTz31lF5++WXdfPPNtuYr0ib6G2+8UaQd+lM31QEAAAAAAADALUIh9i/jQWZmptq3b1/g83bt2mnBggW25yvScS6bN2/W8ePHbU8OAAAAAAAAAEA03XDDDXrllVd09OjRvM+ys7O1aNEidevWzfZ8nlDozO+kzc3N1Y8//qgbbrhBX3zxhXw+nwKBgC6//HKtWrVKfr9fgUBA3bt31/vvv682bdroiy++sF2MJC1fvlxz5szR6tWrJUlvvfWWnn32WR09elQ333yzRo4cqYSEiI5ylyRZlhXxWAAAAAAAAMBN/H6/6RJiynlvlL53OX5zy8OmSzBu8ODB+X4PhUL67LPPVL58eTVp0kQej0c7duxQVlaWLr74Yr388su25i/ScS6JiYlFOqrlu+++k9/vV25urq0ifvfTTz9p6tSpqly5siTpn//8px5++GE9+uijat26tcaMGaNFixapf//+Ec3/u1NvLsFgUIFAQD6fT16vt0DesixbNyTyZvPhxhTWZ7ddA/nIxtBj9+adWoMeRy9vqiZ6HHnejTXRY/Nr0GOzeTfWRI/NrxEL+VjqsRtrioW8yR5HY414ywOQUlJSCnyWnp6e7/e6detGPH9ELxYNhUIaNmyYDh06lPe7JP3jH/9QcnKyDh8+HNGcY8eOVe3atfMes3/llVd01VVXqVevXpKkMWPG6KGHHir2JjoAAAAAAAAAoHSYPn16ic4f0Sb6LbfcogoVKuimm25SQkJC3lPqqampkqQqVarYnnPRokXat2+fxo4dq8mTJ0uStm7dqoceeigvk5aWpn379umXX35RtWrVIikdAAAAAAAAQLzixaJxIxgM6ptvvtGvv/6a99mJEye0bt06DRs2zNZcRd5ED4VCOnnypCTp0UcfLfDdhAkTTjtuypQpZ5z722+/1dNPP61XXnlF2dnZeZ9nZmaqfv36eb8nJiaqYsWKOnDgQLE20YPBYL7ff1/zf9c+0xi7a5CPbv50Y87UZ7ddA3n7Y+ixu/NOrEGPo5uPxhr02Nl8NNagx87mo7EGPTabj8Ya9NjZfDTWcHs+1nocjTVKW950j6OxRjzlT3c0MRDv/v3vf+vee+9VVlaWpP87ScXj8SglJcX2JnqRXiwqSd98842uu+66076Y87777lO5cuXynkoPhULKzc3V8ePH9fTTTxc6b25urm677TZ17NhRQ4cO1eeff66xY8dq9erVSktL0+uvv67mzZvn5Tt27KjZs2erVatWti70d5ZlKScnx9aYpKQkW2PIm827sSbyzubdWBN582uQdzbvxprIm1+DvLN5N9ZE3tm8G2sib34N8s7m3VgTefNrxFs+0j2yeHXeX6eZLsFx39w6znQJrtOzZ09deOGF6tmzp3r37q1//vOf+u9//6s//vGPGjVqlHr06GFrviI/iV6vXj2tWLHitN+daaO8MC+++KISEhIKvEFV+u1A+IMHD+b7LCsrS+XKlYt4PUny+Xz5fs/Oztbu3bvVoEEDVahQoUB+165dBcYUhrzZfLgxhfXZbddAPrIx9Ni9eafWoMfRy5uqiR5HnndjTfTY/Br02GzejTXRY/NrxEI+lnrsxppiIW+yx9FYI97yAAravXu3Jk6cqObNm6t+/fpav369unTpomHDhumFF14ouU30smXL5p157qQ333xT//nPf9S2bVtJvz2Znp2drYsuukg+n0/r1q1Tp06dJP12E8nKytLZZ59drDXD/TWXChUqhP3O7l+NIW82X9iYcH122zWQj3wMPXZn3sk16HF08tFYgx47m4/GGvTY2Xw01qDHZvPRWIMeO5uPxhqxko+VHkdjjdKaN9XjaKwRb3kU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bdmyRf/617+UlJSkiy66SMePH1e1atV02WWX6e6779YVV1yhihUrSpIOHDig++67T3/5y19sFQQAAAAAAAAAQKSef/55DRo0SCNGjMj77LrrrlPt2rU1d+7cktlEr1ixovx+v9q1a6cLL7xQ27Ztk9fr1d///ndlZGSoRo0auvbaa/PyOTk5GjVqlHr16qUlS5aoV69etooCAAAAAAAAAMeFTBeAaPjxxx/Vpk2bAp+3adNG8+fPtz2fJxQKnfF/nWAwqMsvv1zdu3dXlSpV9PXXX+u5557TY489pnPOOUcpKSl6+eWX1b17d/Xp00d///vf9cknn6hz585q0KCBLrnkEtuFlRTLskyXAAAAAAAAADjC7/ebLiGmNHhxlukSHLd78CjTJbjOrbfeqlq1amn27NlKTEyU9Ns7PUeOHKn9+/frr3/9q635irSJLknfffedXn31VZUvX1579+7V2WefrYyMDL3wwguSpM6dO6tly5baunWrkpKSNH78eHXv3t3m5ZU8y7IK3FyCwaACgYB8Pp+8Xm+Rxthdg3z08uHGFNZnt10D+cjG0GP35p1agx5HL2+qJnoced6NNdFj82vQY7N5N9ZEj82vEQv5WOqxG2uKhbzJHkdjjXjLwx420ePDpk2bNGDAANWoUUOtWrWSJK1fv14HDhzQa6+9pubNm9uaL6EooW3btmnt2rVKS0vTf//7X/34448qX768evTooe+//16SVK5cOS1cuFCPPPKIfvnlF23evNnmpQEAAAAAAAAAUDwXXHCBlixZopYtW2rz5s366quv1LJlSy1dutT2BrpUxDPRLcvSa6+9pjJlyigzM1M//fSTfv75Z6WkpGjbtm367rvv8rK5ublq27atVqxYoXbt2qljx462iwIAAAAAAAAAx3Emetxo1KiRZsyY4chcRdpEv+mmm3TTTTcpIyND33zzjZ566in9+uuvmjp1qrxer5566ilt3LhRN998s5KSktSjRw9Vq1ZN06dP12WXXSaPx+NIsQAAAAAAAAAAnMnhw4f1448/qkmTJtq7d6/+3//7f7r66qtVvXp123MV6TiXkydPasKECbr//vv1n//8RxdeeKFGjRqlu+++W+XLl9eDDz6Yt3n+3XffqX379urcubM8Ho9WrlxpuygAAAAAAAAAACKxZcsWde3aNe99nr/88otmzJiha6+9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r18/x+dNiHTgyZMnde+99+q+++5zsBwAAAAAAAAAANwj4k30F154QVu2bNFVV13lZD0AAAAAAAAAALhGRJvoK1as0HPPPadevXrJ4/Fo48aN+umnn3Ty5Emn6wMAAAAAAAAAR3hCpe+nOA4ePKghQ4aoZcuWuuGGG7Rt27YijcvNzdWMGTPUtm1bXXHFFXr//ffzfb97927169dPLVu2VP/+/bVv375837/11lvq1KmT2rZtqyeffNL1+8q2z0RfuHChHn/8cV133XUaMGCArrnmGnk8HklSQkKCatSoodq1a6tFixYaNmyYKlWq5HjRAAAAAAAAAIDIhUIhDRs2TJK0dOlSffXVVxoyZIjeeecdVaxYsdCxTz/9tJYvX65nn31WZcuW1fDhw1W/fn01a9ZMOTk5GjhwoJo0aaLly5drxYoVGjZsmJYuXaqEhAT985//1MMPP6xHH31UrVu31pgxY7Ro0SL1798/GpcdkSI/ib57924NHDhQ06dP15133qnp06fnbZ6vWbNGf/nLX/TUU09p4MCBatq0qd544w3Nnj27xAoHAAAAAAAAAERm/fr12rBhg6ZOnarzzjtP119/vc4991ytXLmy0HHHjh3TokWLNGzYMLVp0ybvafPFixdLkj744AMdPHhQU6dOVb169TRo0CBlZ2dr/fr1kqRXXnlFV111lXr16qUGDRrkbaK7WZGeRP/73/+u8ePHy+/3669//auaN2+e953H41Ht2rVVu3btfGMSEhK0evVqTZw40dmKAQAAAAAAAACSpM6dOxf6/apVq077+datW1WnTh01atQo77OWLVtq06ZNuu6668LO9+233yoYDKpDhw75xr399tt586alpSklJSXv+xYtWmjTpk266KKLtHXrVj300EN536WlpWnfvn365ZdfVK1atcIv1hBPKBQ648k5e/bs0TfffKPLL7883+e7du3SNddco0AgUGDMggULFAwGNXjw4DMWsXDhQk2dOjXfZ2PHjtWAAQP01Vdf6dFHH9WuXbvUvn17TZ06VVWrVj3jnOFYlhXxWAAAAAAAAMBN/H6/6RJiSsM5pe/kjHPffq/Q78Ntoj///PP6/PPP9dprr+V9tnDhQq1du1bPPfdc2Pm+/PJL9e/fX1u3bs37bPv27erbt68yMjL08MMPKxQKadq0aXnfP/744zp58qTGjRun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NeIt36pVqyJnITUbUwrPRH+cM9FP9Z///EcPPvig0tPTdeWVV+q6667Tvn37JEkjRozQ4MGDbc1XrE300/nqq680btw4jRw5Up07dy7SmHfeeUdjxozRV199pcTEREnS/v37ddlllykhIUHjxo3Lt2Gfnp6uoUOHqmvXrrbrsyxLDRs2zPdZdna2du/erQYNGqhChQoFxuzatavAmMKQN5sPN6awPrvtGshHNoYeuzfv1Br0OHp5UzXR48jzbqyJHptfgx6bzbuxJnpsfo1YyMdSj91YUyzkTfY4GmvEW54n0e1pNroUbqLPYBP9TI4ePaq1a9cqJSVFF154oe3xxTrO5XSaN2+uZ555Rvfee2+RN9Hr1KmjkydP6tdff817nP6HH36QJPXs2VPr1q3L20Q/evSovv32W9WpUyfiGsPdXCpUqBD2O7s3JPJm84WNCddnt10D+cjH0GN35p1cgx5HJx+NNeixs/lorEGPnc1HYw16bDYfjTXosbP5aKwRK/lY6XE01iiteVM9jsYa8ZYHULiKFSsWea/6dIr1YtFwzj77bP34449Fzl9wwQU699xzNXHiRO3Zs0dbtmzRY489pksuuUT9+vXTypUrlZGRIem3c9hTUlLk9/tLonQAAAAAAAAAQIx788031a9fP3Xs2FFff/21pkyZomnTpunEiRO25yrWk+jLli0r8Fl2drbeffddnX/++UUvokwZzZ8/XzNmzNBNN92kY8eO6eKLL9aUKVN01llnafjw4RowYICqVq2qYDCoOXPmKCGhRPb/AQAAAAAAAAAx7LXXXtPMmTPVqVMnffnllzpx4oRatWqlyZMnq3z58rr//vttzVesTfRnnnmm4IRlyqhJkyZ68MEHbc119tln6+mnnz7td4MGDVL37t21fft2paWlqVatWpGUCwAAAAAAACCOeRx9OyTcauHChRo7dqz69u2rJk2aSJK6d++ukydP6oknnrC9ie74i0XdzrIs0yUAAAAAAAAAjuDIY3v8D5W+F4taM3mx6KlatGihl19+WRdddJGaNGmiZcuWqUmTJvr3v/+te+65Rxs3brQ1n60n0T/55BMlJyfrggsusLWI25x6cwkGgwoEAvL5fKd9cYNlWbZuSOTN5sONKazPbrsG8pGNocfuzTu1Bj2OXt5UTfQ48rwba6LH5tegx2bzbqyJHptfIxby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OXKkli5dqszMTCUmJio1NTVvbHJyct5YkzyhUKgETs5xL8uyTJcAAAAAAAAAOMLv95suIaZcesMs0yU4LunwikK/P90m+n/+8x9lZ2efNn/HHXdo5MiR6tatW95nvXv31q233qoePXoUutbEiROVmJioSZMm5X329NNP5x3xcqo9e/aoS5cuWrFihY4cOaIBAwZo48aNed9///336tatm/Fz0ePySfRTby7BYFCBQEA+n09er7dA3rIsWzck8mbz4cYU1me3XQP5yMbQY/fmnVqDHkcvb6omehx53o010WPza9Bjs3k31kSPza8RC/lY6rEba4qFvMkeR2ONeMsDp9skP5Ozzjqr0O8OHjyY77PMzEyVK1fujPOmpKRo165d+T7LysoKO7ZmzZqSpB9++EH169dXdna2srKyVKlSJVvrljTORAcAAAAAAAAASPrtDPN169bl/X706FF9++23qlOnzhnHtmzZUhs2bFBubm7eZ5s3b9bZZ5+t//znP+rWrZuCwWDed7+vU6dOHaWmpqp69er51v59rGlsogMAAAAAAAAAJEnp6elauXKlMjIyJElz585VSkpK3t+QyMnJUVZW1mnHtm/fXidOnND8+fMlSWvWrNFXX32lTp066ayzzlKlSpX08MMPy7IsrVmzRuPHj9cll1yiRo0aKSEhQd27d9czzzyjrKws/fe//9Wrr76qTp06RefCCxGXx7kAAAAAAAAAiD+euHo7ZGSaNm2q4cOHa8CAAapataqCwaDmzJmjhITfnsd+6aWXtHLlSi1fvrzA2LJly2rWrFkaOXKkXn31VR06dEhDhw7VeeedJ0l65plnNH78ePXu3VvJyclKT0/XsGHD8sbfe++9uuuuu3TZZZcpFAqpfv36Gjx4cHQuvBBsogMAAAAAAAAA8gwaNEjdu3fX9u3blZaWplq1auV9N3z4cA0fPjzs2EsvvVQfffSR1q1bp3r16qlx48Z535199tl5T6mfTuXKlbV48WKtW7dOx44dU5s2bVS2bFlnLqoY2EQHAAAAAAAAAOSTmpqq1NTUiMZWrVpVnTt3jmhsQkKCWrduHdHYksKZ6AAAAAAAAAAAhMGT6AAAAAAAAADiQ4hD0WEfT6IDAAAAAAAAABCGJxSKrz9+sSzLdAkAAAAAAACAI/x+v+kSYkqHnk+YLsFxnyx70HQJpV5cHudy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Ffvv45M6duyou+66S2vWrLGveX8N92rXEhkZqQEDBuj555/XpEmTFBAQoIiICPXp00ePPPKIFi5cyOsuF5eQkKDBgwfriSee0Jdffqlx48ZpyZIlCgsLU6dOnfTJJ59o48aNOd74pMauJSkpSWPHjlWvXr20ePFiffDBBypdurRq1qypWrVqKTw8XBs3buTnGACcyG0fLHpNYGCgRowYoWXLlumVV17RN998Iw8PD+3bt0/Dhg2Tj4+P0Snib5hMJnl7eys2NlZHjhxRiRIl1Lt3bwUGBmrGjBl68cUXNW/ePPXo0UMhISGSrr6oSExMZKkeF5GZmSlfX1/17t1bktS5c2ft3r1bn332mSIiIpSWlqavv/5aO3bs0MMPP6zU1FRFR0drwIABBmeOvLBarapevbrat28v6WpD5pNPPtG2bdvUtm1bHTt2TJs2bdKOHTvUsmVLWSwW6uxisrKyZDKZVK5cOXXv3l0XLlxQWFiYFi9erIoVK+qNN95QSEiImjRpovPnz+v06dNq1KiR0WnjFpnNZtWrV0/9+vWT1WrVwIEDlZKSosTERLVr105paWk6c+aMJkyYoJCQEGVkZGjXrl2aOHEinwpzQbVr11ZERIQaNmyo7du3a8eOHbLZbIqPj1dCQoJ27NihFi1aKC0tjXu1Czl37pzCwsIUGhqqESNGSJJatmypxYsXS7r6KaH333+f110u7syZMwoKCtLo0aMlXX1WxbJly7R79249+OCDOnTokH766Sdec7mwc+fOqUyZMgoNDZWPj4/atGmj5ORkvf3221q9erVWr16tFStW8HMMAE7k9k10SfYnzbdp00Y//vijkpOT9c4776hFixZGp4ZbYLVa5eHhoWrVqikzM1Pbt2+XyWRS7969VbVqVc2aNUvvvvuuli5dqrlz5yooKEgbNmzQiy++yJskLiIzM1OJiYm6dOmSfVuzZs3syzCNGTNGW7Zs0d69ezV//nyZTCaNHDlSrVu3Nipl5FN6erpOnjypWrVqaenSpfr999+VkpIiSapcubKqVasmPz8/hYeHy9PTkzq7mGuzVu+//34lJSXp+eef19SpUzVkyBANGjRINptNp0+f1urVq+Xh4aGhQ4eqWbNmBmeNW9WsWTOtXbtWixcv1h9//KHSpUvr3Xffldls1t69e2WxWHTvvfcqKChIq1atkslk0ssv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8X/9mw7/+c9/5OnpecufWgAAAABwczTRAQCA26hbt64k6bfffrNv+/PPP/XUU0/p8OHDLjPGNV27dtXWrVu1detWtWjRQv7+/vk+1+XLlzVv3jw9+uij9tnp1yQkJOSriX7+/HnFxcXp999/V82aNRUQEHDD2GXLlunChQsym8369ddfNWvWLA0cOFDe3t6SpAEDBujw4cOKjIz827Xb/86jjz6q1atX59q2c+dORUVFSbpar82bN992w/56p06d0tNPP63Q0FCFhYXl+fjrm+ilS5fW008/re+//97+NXny5L9dHmbNmjV69NFHc50PAAAAQP6wnAsAAHAb9957rzp37qy3335bVqtVd911l+bNm6fq1avroYcecpkxruncubNmzJihtLQ0Pf744/k+z8WLFzV06FClpKRo7NixOfYlJSXp4sWLt7R8iNVqVVZWlooXL66GDRvqxx9/VPXq1bV79+4cs9uzs7Nls9nszd7+/furdu3a2rVrl1544QVZLBaVLl1ahw8fVoMGDXT58mV5e3vLZrMpOTnZYTP+VvTp00ddu3bVoUOH1LBhQ0lSu3bt1KxZMz3zzDMKCQnRzp07Va5cOfXt2zff41wvOjpaw4YN04QJE/L9iYHr13NPTk7WV199pcWLF9u3ZWdnq2TJkg7P8ddff+nLL7/UypUr85UDAAAAgNxoogMAALcyffp0zZw5U9OnT1d2draaNm2qqVOn2mc9u8oY0tW1vBs1aqR9+/Zp7ty5eT7earVq48aNev/993XhwgWFh4fbZ5zHxMTou+++0969e1WsWDHVr1/f4XmysrIkXZ1p3alTpxvG7Nu3T1999ZX9+2nTptkb/8WLF1ft2rW1du1alSlTRu+//75+/vln7du3T+fOndPEiRPVs2dPxcbGKjQ0VAsWLLhpPjdTunRpTZgwQePGjdOyZcsUEBAgk8mk+fPn66OPPtL27dvVvHlzjR07NteM/Nvx4Ycf6q233lKLFi3yfQ6r1Zrj+xkzZjiMPXfunI4ePaqzZ8/al3NJT0/Xyy+/rOeeey7fD2cFAAAAkJvJdv2UFwAAAOSb1WrN1Qz9Xx4eHre81vrt2rt3r5599lk99NBDmjx5sqpVq2bfl56ervbt26t69er65z//qZCQEIfneffdd3Xo0CEtXLhQx48fV4kSJW66VEhmZqYqVKigcuXKadu2bfr222+1bds2de/eXS+//LLKli2rU6dO6a233tKuXbs0duxYhYaGKjU1VUOGDNGBAwc0bdo09ejRI9/XPnPmTJ09e1Zvv/12vs9R0I4fP65u3brd0oNF//jjD3Xt2lU1a9bU3LlzVbVqVc2ePVtxcXGaMWMGS7kAAAAATkQTHQAAwInGjx+vb7/91uH+0NBQvfbaawWWzx9//KG77777ts7x+uuv6+jRo1q+fHmej42JidG6dev05JNPqmrVqvbtmZmZmjNnjp544glVr17dvj07O1vLly9X37595eXlle+cbTabLl26JD8/v3yfo7BLS0tTqVKlcnzv6emp4sWLG5gVAAAA4H5oogMAADjR6dOndfHiRYf7AwICFBgYWIAZAQAAAABuB010AAAAAAAAAAAcKJgFOQEAAAAAAAAAcEE00QEAAAAAAAAAcIAmOgAAAAAAAAAADtBEBwAAAAAAAADAAZroAAAAAAAAAAA4QBMdAAAAAAAAAAAHaKIDAAAAAAAAAOAATXQAAAAAAAAAABz4f+vX7CVnnsudAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "import seaborn as sns\n",
+ "from scipy.stats import spearmanr\n",
+ "from tqdm import tqdm # 用于显示进度条 (可选)\n",
+ "\n",
+ "# 设置 Matplotlib/Seaborn 样式 (可选)\n",
+ "sns.set_theme(style=\"whitegrid\")\n",
+ "plt.rcParams['font.sans-serif'] = ['SimHei'] # 或者其他支持中文的字体\n",
+ "plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题\n",
+ "\n",
+ "def analyze_score_performance_2d(score_df: pd.DataFrame,\n",
+ " score_col: str = 'score',\n",
+ " label_col: str = 'label',\n",
+ " condition1_col: str = 'circ_mv',\n",
+ " condition2_col: str = 'future_return',\n",
+ " n_bins: int = 100,\n",
+ " min_samples_per_bin: int = 30): # 每个格子最少样本数\n",
+ " \"\"\"\n",
+ " 分析 score 在两个条件下 (如市值、未来收益) 的二维分箱表现。\n",
+ "\n",
+ " Args:\n",
+ " score_df (pd.DataFrame): 包含分数、标签和条件列的 DataFrame。\n",
+ " score_col (str): 预测分数所在的列名。\n",
+ " label_col (str): 目标标签所在的列名 (应为数值或可排序类别)。\n",
+ " condition1_col (str): 第一个条件列名 (例如 'circ_mv')。\n",
+ " condition2_col (str): 第二个条件列名 (例如 'future_return')。\n",
+ " n_bins (int): 每个条件划分的箱数 (分位数数量)。\n",
+ " min_samples_per_bin (int): 计算指标所需的最小样本数,小于此数目的格子结果将被屏蔽。\n",
+ "\n",
+ " Returns:\n",
+ " tuple: 包含 (performance_pivot, count_pivot, fig)\n",
+ " performance_pivot: 以二维分箱为索引/列的 Spearman 相关系数矩阵。\n",
+ " count_pivot: 每个二维分箱的样本数量矩阵。\n",
+ " fig: 生成的热力图 Matplotlib Figure 对象。\n",
+ " \"\"\"\n",
+ " print(f\"开始分析 '{score_col}' 在 '{condition1_col}' 和 '{condition2_col}' 下的表现...\")\n",
+ "\n",
+ " required_cols = [score_col, label_col, condition1_col, condition2_col]\n",
+ " if not all(col in score_df.columns for col in required_cols):\n",
+ " missing = [col for col in required_cols if col not in score_df.columns]\n",
+ " raise ValueError(f\"输入 DataFrame 缺少必需列: {missing}\")\n",
+ "\n",
+ " # --- 1. 数据准备和清洗 ---\n",
+ " print(\"准备数据,处理 NaN 值...\")\n",
+ " # 只保留需要的列,并移除包含 NaN 的行,避免影响分箱和计算\n",
+ " analysis_df = score_df[required_cols].dropna().copy()\n",
+ " n_original = len(score_df)\n",
+ " n_after_drop = len(analysis_df)\n",
+ " print(f\"原始数据 {n_original} 行,移除 NaN 后剩余 {n_after_drop} 行用于分析。\")\n",
+ "\n",
+ " if n_after_drop < min_samples_per_bin * n_bins: # 检查数据量是否过少\n",
+ " print(f\"警告: 清理 NaN 后数据量 ({n_after_drop}) 可能不足以支持 {n_bins}x{n_bins} 的精细分箱分析。\")\n",
+ " if n_after_drop < min_samples_per_bin:\n",
+ " print(\"错误: 有效数据过少,无法进行分析。\")\n",
+ " return None, None, None\n",
+ "\n",
+ " # --- 2. 二维分箱 ---\n",
+ " print(f\"对 '{condition1_col}' 和 '{condition2_col}' 进行 {n_bins} 分位数分箱...\")\n",
+ " bin1_col = f'{condition1_col}_bin'\n",
+ " bin2_col = f'{condition2_col}_bin'\n",
+ "\n",
+ " try:\n",
+ " # 使用 qcut 进行分位数分箱,labels=False 返回 0 到 n_bins-1 的整数标签\n",
+ " # duplicates='drop' 会丢弃导致边界不唯一的重复值所在的箱子,可能导致某些箱号缺失\n",
+ " # 对于可视化,这通常可以接受,但如果需要严格的等分,需先 rank\n",
+ " analysis_df[bin1_col] = pd.qcut(analysis_df[condition1_col], q=n_bins, labels=False, duplicates='drop')\n",
+ " analysis_df[bin2_col] = pd.qcut(analysis_df[condition2_col], q=n_bins, labels=False, duplicates='drop')\n",
+ " except Exception as e:\n",
+ " print(f\"错误: 分箱失败,请检查数据分布或减少 n_bins。错误信息: {e}\")\n",
+ " # 可以尝试先 rank 再 qcut\n",
+ " # analysis_df[bin1_col] = pd.qcut(analysis_df[condition1_col].rank(method='first'), q=n_bins, labels=False, duplicates='raise')\n",
+ " # analysis_df[bin2_col] = pd.qcut(analysis_df[condition2_col].rank(method='first'), q=n_bins, labels=False, duplicates='raise')\n",
+ " return None, None, None\n",
+ "\n",
+ " # --- 3. 分组计算表现指标 (Spearman Rank IC) ---\n",
+ " print(\"按二维分箱分组计算 Spearman Rank IC...\")\n",
+ "\n",
+ " def safe_spearmanr(x, y):\n",
+ " \"\"\"安全计算 Spearman 相关性,处理数据量过少的情况\"\"\"\n",
+ " if len(x) < max(2, min_samples_per_bin): # 要求至少有 min_samples_per_bin 个点才计算\n",
+ " return np.nan\n",
+ " corr, p_value = spearmanr(x, y)\n",
+ " return corr if not np.isnan(corr) else np.nan # 确保返回 NaN 而不是 None 或其他\n",
+ "\n",
+ " # 按两个分箱列分组\n",
+ " grouped = analysis_df.groupby([bin1_col, bin2_col])\n",
+ "\n",
+ " # 计算每个格子的 Spearman 相关系数\n",
+ " # apply 可能较慢,但计算相关性通常需要 apply\n",
+ " performance_series = grouped.apply(lambda sub: safe_spearmanr(sub[score_col], sub[label_col]))\n",
+ "\n",
+ " # 计算每个格子的样本数量\n",
+ " count_series = grouped.size()\n",
+ "\n",
+ " # --- 4. 结果整理成 Pivot Table (用于绘图) ---\n",
+ " print(\"整理结果用于绘图...\")\n",
+ " try:\n",
+ " # 将 performance_series 转换成二维矩阵\n",
+ " # index 为 condition1_bin, columns 为 condition2_bin\n",
+ " performance_pivot = performance_series.unstack(level=0) # level=0 对应第一个 groupby key (bin1_col)\n",
+ " count_pivot = count_series.unstack(level=0)\n",
+ "\n",
+ " # 可选:按列和索引排序,确保顺序正确\n",
+ " performance_pivot = performance_pivot.sort_index(axis=0).sort_index(axis=1)\n",
+ " count_pivot = count_pivot.sort_index(axis=0).sort_index(axis=1)\n",
+ " \n",
+ " print(performance_pivot)\n",
+ "\n",
+ " except Exception as e:\n",
+ " print(f\"错误: 无法将结果转换为二维矩阵,可能因为分箱不均匀或数据问题: {e}\")\n",
+ " return None, None, None\n",
+ "\n",
+ " # --- 5. 可视化:绘制热力图 ---\n",
+ " print(\"生成热力图...\")\n",
+ " fig, ax = plt.subplots(figsize=(16, 12)) # 调整图像大小\n",
+ "\n",
+ " # 使用 count_pivot 创建一个 mask,屏蔽掉样本量过小的格子\n",
+ " mask = count_pivot < min_samples_per_bin\n",
+ "\n",
+ " # 绘制热力图\n",
+ " sns.heatmap(performance_pivot,\n",
+ " annot=False, # 100x100 个格子加注释会太密集\n",
+ " fmt=\".2f\",\n",
+ " cmap=\"viridis\", # 选择颜色映射, 'viridis', 'coolwarm', 'RdYlGn' 等都不错\n",
+ " linewidths=.5,\n",
+ " linecolor='lightgray',\n",
+ " # mask=mask, # 应用 mask\n",
+ " ax=ax,\n",
+ " cbar_kws={'label': f'Spearman Rank IC ({score_col} vs {label_col})'}) # 颜色条标签\n",
+ "\n",
+ " # 设置标题和轴标签\n",
+ " ax.set_title(f'{score_col} 表现分析 (Rank IC vs {label_col})\\n基于 {condition1_col} 和 {condition2_col} {n_bins}x{n_bins} 分箱', fontsize=16)\n",
+ " ax.set_xlabel(f'{condition1_col} 分位数 (0 -> 高)', fontsize=12)\n",
+ " ax.set_ylabel(f'{condition2_col} 分位数 (0 -> 高)', fontsize=12)\n",
+ "\n",
+ " # 可选:调整刻度标签,避免显示所有 100 个刻度\n",
+ " if n_bins > 20:\n",
+ " tick_interval = n_bins // 10 # 大约显示 10 个刻度\n",
+ " ax.set_xticks(np.arange(0, n_bins, tick_interval) + 0.5)\n",
+ " ax.set_yticks(np.arange(0, n_bins, tick_interval) + 0.5)\n",
+ " ax.set_xticklabels(np.arange(0, n_bins, tick_interval))\n",
+ " ax.set_yticklabels(np.arange(0, n_bins, tick_interval))\n",
+ "\n",
+ " plt.xticks(rotation=45, ha='right')\n",
+ " plt.yticks(rotation=0)\n",
+ " plt.tight_layout() # 调整布局\n",
+ "\n",
+ " print(\"分析完成。\")\n",
+ " return performance_pivot, count_pivot, fig\n",
+ "\n",
+ "# --- 如何使用 ---\n",
+ "# 假设你的包含预测结果和所需列的 DataFrame 是 final_predictions_df\n",
+ "# 确保它包含 'score', 'label', 'circ_mv', 'future_return'\n",
+ "\n",
+ "# # 示例调用 (你需要有实际的 score_df)\n",
+ "try:\n",
+ " # 确保数据类型正确\n",
+ " cols_to_numeric = ['score', 'label', 'circ_mv', 'future_return']\n",
+ " for col in cols_to_numeric:\n",
+ " if col in score_df.columns:\n",
+ " score_df[col] = pd.to_numeric(score_df[col], errors='coerce')\n",
+ "\n",
+ " # 调用分析函数\n",
+ " performance_matrix, count_matrix, heatmap_figure = analyze_score_performance_2d(\n",
+ " score_df,\n",
+ " n_bins=100, # 你要求的100分箱\n",
+ " min_samples_per_bin=50 # 每个格子至少需要50个样本才显示IC,可以调整\n",
+ " )\n",
+ "\n",
+ " # 显示图像\n",
+ " if heatmap_figure:\n",
+ " plt.show()\n",
+ "\n",
+ " # 可以查看具体的 performance_matrix 和 count_matrix\n",
+ " # print(\"\\nPerformance Matrix (Spearman IC):\")\n",
+ " # print(performance_matrix)\n",
+ " # print(\"\\nCount Matrix:\")\n",
+ " # print(count_matrix)\n",
+ "\n",
+ "except ValueError as ve:\n",
+ " print(f\"数据错误: {ve}\")\n",
+ "except Exception as e:\n",
+ " print(f\"发生未知错误: {e}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a436dba4",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Empty DataFrame\n",
+ "Columns: [ts_code, trade_date, is_st]\n",
+ "Index: []\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mnotebook controller is DISPOSED. \n",
+ "\u001b[1;31m有关更多详细信息,请查看 Jupyter log。"
+ ]
+ }
+ ],
+ "source": [
+ "print(df[(df['ts_code'] == '600242.SH') & (df['trade_date'] >= '2023-06-01')][['ts_code', 'trade_date', 'is_st']])"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "stock",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.13.2"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}