diff --git a/src/experiment/common.py b/src/experiment/common.py new file mode 100644 index 0000000..621a16f --- /dev/null +++ b/src/experiment/common.py @@ -0,0 +1,278 @@ +"""实验脚本的共用配置和辅助函数。 + +此模块包含 regression.py 和 learn_to_rank.py 共用的代码, +避免重复维护两份相同的配置和函数。 +""" + +from datetime import datetime +from typing import List + +import polars as pl + +from src.factors import FactorEngine + + +# ============================================================================= +# 日期范围配置(正确的 train/val/test 三分法) +# ============================================================================= +TRAIN_START = "20200101" +TRAIN_END = "20231231" +VAL_START = "20240101" +VAL_END = "20241231" +TEST_START = "20250101" +TEST_END = "20261231" + + +# ============================================================================= +# 因子配置 +# ============================================================================= +# 当前选择的因子列表(从 FACTOR_DEFINITIONS 中选择要使用的因子) +SELECTED_FACTORS = [ + # ================= 1. 价格、趋势与路径依赖 ================= + "ma_5", + "ma_20", + "ma_ratio_5_20", + "bias_10", + "high_low_ratio", + "bbi_ratio", + "return_5", + "return_20", + "kaufman_ER_20", + "mom_acceleration_10_20", + "drawdown_from_high_60", + "up_days_ratio_20", + # ================= 2. 波动率、风险调整与高阶矩 ================= + "volatility_5", + "volatility_20", + "volatility_ratio", + "std_return_20", + "sharpe_ratio_20", + "min_ret_20", + "volatility_squeeze_5_60", + # ================= 3. 日内微观结构与异象 ================= + "overnight_intraday_diff", + "upper_shadow_ratio", + "capital_retention_20", + "max_ret_20", + # ================= 4. 量能、流动性与量价背离 ================= + "volume_ratio_5_20", + "turnover_rate_mean_5", + "turnover_deviation", + "amihud_illiq_20", + "turnover_cv_20", + "pv_corr_20", + "close_vwap_deviation", + # ================= 5. 基本面财务特征 ================= + "roe", + "roa", + "profit_margin", + "debt_to_equity", + "current_ratio", + "net_profit_yoy", + "revenue_yoy", + "healthy_expansion_velocity", + # ================= 6. 基本面估值与截面动量共振 ================= + "EP", + "BP", + "CP", + "market_cap_rank", + "turnover_rank", + "return_5_rank", + "EP_rank", + "pe_expansion_trend", + "value_price_divergence", + "active_market_cap", + "ebit_rank", +] + +# 因子定义字典(完整因子库,用于存放尚未注册到metadata的因子) +FACTOR_DEFINITIONS = {} + + +def get_label_factor(label_name: str) -> dict: + """获取Label因子定义字典。 + + Args: + label_name: label因子名称 + + Returns: + Label因子定义字典 + """ + return { + label_name: "(ts_delay(close, -5) / ts_delay(open, -1)) - 1", + } + + +# ============================================================================= +# 辅助函数 +# ============================================================================= +def register_factors( + engine: FactorEngine, + selected_factors: List[str], + factor_definitions: dict, + label_factor: dict, +) -> List[str]: + """注册因子。 + + selected_factors 从 metadata 查询,factor_definitions 用 DSL 表达式注册。 + + Args: + engine: FactorEngine实例 + selected_factors: 从metadata中选择的因子名称列表 + factor_definitions: 通过表达式定义的因子字典 + label_factor: label因子定义字典 + + Returns: + 特征列名称列表 + """ + print("=" * 80) + print("注册因子") + print("=" * 80) + + # 注册 SELECTED_FACTORS 中的因子(已在 metadata 中) + print("\n注册特征因子(从 metadata):") + for name in selected_factors: + engine.add_factor(name) + print(f" - {name}") + + # 注册 FACTOR_DEFINITIONS 中的因子(通过表达式,尚未在 metadata 中) + print("\n注册特征因子(表达式):") + for name, expr in factor_definitions.items(): + engine.add_factor(name, expr) + print(f" - {name}: {expr}") + + # 注册 label 因子(通过表达式) + print("\n注册 Label 因子(表达式):") + for name, expr in label_factor.items(): + engine.add_factor(name, expr) + print(f" - {name}: {expr}") + + # 特征列 = SELECTED_FACTORS + FACTOR_DEFINITIONS 的 keys + feature_cols = selected_factors + list(factor_definitions.keys()) + + print(f"\n特征因子数: {len(feature_cols)}") + print(f" - 来自 metadata: {len(selected_factors)}") + print(f" - 来自表达式: {len(factor_definitions)}") + print(f"Label: {list(label_factor.keys())[0]}") + print(f"已注册因子总数: {len(engine.list_registered())}") + + return feature_cols + + +def prepare_data( + engine: FactorEngine, + feature_cols: List[str], + start_date: str, + end_date: str, + label_name: str, +) -> pl.DataFrame: + """准备数据。 + + 计算因子并返回包含特征和label的数据框。 + + Args: + engine: FactorEngine实例 + feature_cols: 特征列名称列表 + start_date: 开始日期 (YYYYMMDD) + end_date: 结束日期 (YYYYMMDD) + label_name: label列名称 + + Returns: + 包含因子计算结果的数据框 + """ + print("\n" + "=" * 80) + print("准备数据") + print("=" * 80) + + # 计算因子(全市场数据) + print(f"\n计算因子: {start_date} - {end_date}") + factor_names = feature_cols + [label_name] # 包含 label + + data = engine.compute( + factor_names=factor_names, + start_date=start_date, + end_date=end_date, + ) + + print(f"数据形状: {data.shape}") + print(f"数据列: {data.columns}") + print(f"\n前5行预览:") + print(data.head()) + + return data + + +# ============================================================================= +# 股票池筛选配置 +# ============================================================================= +def stock_pool_filter(df: pl.DataFrame) -> pl.Series: + """股票池筛选函数(单日数据)。 + + 筛选条件: + 1. 排除创业板(代码以 300 开头) + 2. 排除科创板(代码以 688 开头) + 3. 排除北交所(代码以 8、9 或 4 开头) + 4. 选取当日市值最小的500只股票 + + Args: + df: 单日数据框 + + Returns: + 布尔Series,表示哪些股票被选中 + """ + # 代码筛选(排除创业板、科创板、北交所) + code_filter = ( + ~df["ts_code"].str.starts_with("30") # 排除创业板 + & ~df["ts_code"].str.starts_with("68") # 排除科创板 + & ~df["ts_code"].str.starts_with("8") # 排除北交所 + & ~df["ts_code"].str.starts_with("9") # 排除北交所 + & ~df["ts_code"].str.starts_with("4") # 排除北交所 + ) + + # 在已筛选的股票中,选取市值最小的500只 + valid_df = df.filter(code_filter) + n = min(500, len(valid_df)) + small_cap_codes = valid_df.sort("total_mv").head(n)["ts_code"] + + # 返回布尔 Series:是否在被选中的股票中 + return df["ts_code"].is_in(small_cap_codes) + + +# 定义筛选所需的基础列 +STOCK_FILTER_REQUIRED_COLUMNS = ["total_mv"] + + +# ============================================================================= +# 输出配置 +# ============================================================================= +OUTPUT_DIR = "output" +SAVE_PREDICTIONS = True +PERSIST_MODEL = False + +# Top N 配置:每日推荐股票数量 +TOP_N = 5 # 可调整为 10, 20 等 + + +def get_output_path(model_type: str, test_start: str, test_end: str) -> str: + """生成输出文件路径。 + + Args: + model_type: 模型类型("regression" 或 "rank") + test_start: 测试开始日期 + test_end: 测试结束日期 + + Returns: + 输出文件路径 + """ + import os + + # 确保输出目录存在 + os.makedirs(OUTPUT_DIR, exist_ok=True) + + # 生成文件名 + start_dt = datetime.strptime(test_start, "%Y%m%d") + end_dt = datetime.strptime(test_end, "%Y%m%d") + date_str = f"{start_dt.strftime('%Y%m%d')}_{end_dt.strftime('%Y%m%d')}" + + filename = f"{model_type}_output.csv" + return os.path.join(OUTPUT_DIR, filename) diff --git a/src/experiment/learn_to_rank.ipynb b/src/experiment/learn_to_rank.ipynb index a7c7616..ccb2375 100644 --- a/src/experiment/learn_to_rank.ipynb +++ b/src/experiment/learn_to_rank.ipynb @@ -22,7 +22,12 @@ "source": "## 1. 导入依赖" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:08.941539Z", + "start_time": "2026-03-14T15:09:08.938469Z" + } + }, "cell_type": "code", "source": [ "import os\n", @@ -48,88 +53,47 @@ ")\n", "from src.training.components.models import LightGBMLambdaRankModel\n", "from src.training.config import TrainingConfig\n", + "\n", + "# 从 common 模块导入共用配置和函数\n", + "from src.experiment.common import (\n", + " SELECTED_FACTORS,\n", + " FACTOR_DEFINITIONS,\n", + " get_label_factor,\n", + " register_factors,\n", + " prepare_data,\n", + " TRAIN_START,\n", + " TRAIN_END,\n", + " VAL_START,\n", + " VAL_END,\n", + " TEST_START,\n", + " TEST_END,\n", + " stock_pool_filter,\n", + " STOCK_FILTER_REQUIRED_COLUMNS,\n", + " OUTPUT_DIR,\n", + " SAVE_PREDICTIONS,\n", + " PERSIST_MODEL,\n", + " TOP_N,\n", + ")\n", "\n" ], "outputs": [], - "execution_count": null + "execution_count": 13 }, { "metadata": {}, "cell_type": "markdown", - "source": "## 2. 辅助函数" + "source": "## 2. 本地辅助函数" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:08.987446Z", + "start_time": "2026-03-14T15:09:08.981368Z" + } + }, "cell_type": "code", "source": [ - "def register_factors(\n", - " engine: FactorEngine,\n", - " selected_factors: List[str],\n", - " factor_definitions: dict,\n", - " label_factor: dict,\n", - ") -> List[str]:\n", - " \"\"\"注册因子(selected_factors 从 metadata 查询,factor_definitions 用 DSL 表达式注册)\"\"\"\n", - " print(\"=\" * 80)\n", - " print(\"注册因子\")\n", - " print(\"=\" * 80)\n", - "\n", - " # 注册 SELECTED_FACTORS 中的因子(已在 metadata 中)\n", - " print(\"\\n注册特征因子(从 metadata):\")\n", - " for name in selected_factors:\n", - " engine.add_factor(name)\n", - " print(f\" - {name}\")\n", - "\n", - " # 注册 FACTOR_DEFINITIONS 中的因子(通过表达式,尚未在 metadata 中)\n", - " print(\"\\n注册特征因子(表达式):\")\n", - " for name, expr in factor_definitions.items():\n", - " engine.add_factor(name, expr)\n", - " print(f\" - {name}: {expr}\")\n", - "\n", - " # 注册 label 因子(通过表达式)\n", - " print(\"\\n注册 Label 因子(表达式):\")\n", - " for name, expr in label_factor.items():\n", - " engine.add_factor(name, expr)\n", - " print(f\" - {name}: {expr}\")\n", - "\n", - " # 特征列 = SELECTED_FACTORS + FACTOR_DEFINITIONS 的 keys\n", - " feature_cols = selected_factors + list(factor_definitions.keys())\n", - "\n", - " print(f\"\\n特征因子数: {len(feature_cols)}\")\n", - " print(f\" - 来自 metadata: {len(selected_factors)}\")\n", - " print(f\" - 来自表达式: {len(factor_definitions)}\")\n", - " print(f\"Label: {list(label_factor.keys())[0]}\")\n", - " print(f\"已注册因子总数: {len(engine.list_registered())}\")\n", - "\n", - " return feature_cols\n", - "\n", - "\n", - "def prepare_data(\n", - " engine: FactorEngine,\n", - " feature_cols: List[str],\n", - " start_date: str,\n", - " end_date: str,\n", - ") -> pl.DataFrame:\n", - " \"\"\"准备数据\"\"\"\n", - " print(\"\\n\" + \"=\" * 80)\n", - " print(\"准备数据\")\n", - " print(\"=\" * 80)\n", - "\n", - " # 计算因子(全市场数据)\n", - " print(f\"\\n计算因子: {start_date} - {end_date}\")\n", - " factor_names = feature_cols + [LABEL_NAME] # 包含 label\n", - "\n", - " data = engine.compute(\n", - " factor_names=factor_names,\n", - " start_date=start_date,\n", - " end_date=end_date,\n", - " )\n", - "\n", - " print(f\"数据形状: {data.shape}\")\n", - " print(f\"数据列: {data.columns}\")\n", - " print(f\"\\n前5行预览:\")\n", - " print(data.head())\n", - "\n", - " return data\n", + "# 注意:register_factors 和 prepare_data 已从 common 模块导入\n", "\n", "\n", "def prepare_ranking_data(\n", @@ -263,7 +227,7 @@ "\n" ], "outputs": [], - "execution_count": null + "execution_count": 14 }, { "metadata": {}, @@ -271,104 +235,29 @@ "source": [ "## 3. 配置参数\n", "#\n", - "### 3.1 因子定义" + "### 3.1 因子与日期配置" ] }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:08.996882Z", + "start_time": "2026-03-14T15:09:08.993154Z" + } + }, "cell_type": "code", "source": [ - "# 特征因子定义字典(复用 regression.ipynb 的因子定义)\n", - "LABEL_NAME = \"future_return_5_rank\"\n", + "# 注意:SELECTED_FACTORS, FACTOR_DEFINITIONS, 日期配置等已从 common 模块导入\n", + "# 本脚本特有的配置:\n", "\n", - "# 当前选择的因子列表(从 FACTOR_DEFINITIONS 中选择要使用的因子)\n", - "SELECTED_FACTORS = [\n", - " # ================= 1. 价格、趋势与路径依赖 =================\n", - " \"ma_5\",\n", - " \"ma_20\",\n", - " \"ma_ratio_5_20\",\n", - " \"bias_10\",\n", - " \"high_low_ratio\",\n", - " \"bbi_ratio\",\n", - " \"return_5\",\n", - " \"return_20\",\n", - " \"kaufman_ER_20\",\n", - " \"mom_acceleration_10_20\",\n", - " \"drawdown_from_high_60\",\n", - " \"up_days_ratio_20\",\n", - " # ================= 2. 波动率、风险调整与高阶矩 =================\n", - " \"volatility_5\",\n", - " \"volatility_20\",\n", - " \"volatility_ratio\",\n", - " \"std_return_20\",\n", - " \"sharpe_ratio_20\",\n", - " \"min_ret_20\",\n", - " \"volatility_squeeze_5_60\",\n", - " # ================= 3. 日内微观结构与异象 =================\n", - " \"overnight_intraday_diff\",\n", - " \"upper_shadow_ratio\",\n", - " \"capital_retention_20\",\n", - " \"max_ret_20\",\n", - " # ================= 4. 量能、流动性与量价背离 =================\n", - " \"volume_ratio_5_20\",\n", - " \"turnover_rate_mean_5\",\n", - " \"turnover_deviation\",\n", - " \"amihud_illiq_20\",\n", - " \"turnover_cv_20\",\n", - " \"pv_corr_20\",\n", - " \"close_vwap_deviation\",\n", - " # ================= 5. 基本面财务特征 =================\n", - " \"roe\",\n", - " \"roa\",\n", - " \"profit_margin\",\n", - " \"debt_to_equity\",\n", - " \"current_ratio\",\n", - " \"net_profit_yoy\",\n", - " \"revenue_yoy\",\n", - " \"healthy_expansion_velocity\",\n", - " \"ebit_rank\",\n", - " # ================= 6. 基本面估值与截面动量共振 =================\n", - " \"EP\",\n", - " \"BP\",\n", - " \"CP\",\n", - " \"market_cap_rank\",\n", - " \"turnover_rank\",\n", - " \"return_5_rank\",\n", - " \"EP_rank\",\n", - " \"pe_expansion_trend\",\n", - " \"value_price_divergence\",\n", - " \"active_market_cap\",\n", - "]\n", + "# Label 名称(排序学习使用原始收益率,会后续转换为分位数标签)\n", + "LABEL_NAME = \"future_return_5\"\n", "\n", - "# 因子定义字典(完整因子库)\n", - "FACTOR_DEFINITIONS = {\n", - " # \"turnover_rate_volatility\": \"ts_std(log(turnover_rate), 20)\"\n", - "}\n", + "# 获取 Label 因子定义\n", + "LABEL_FACTOR = get_label_factor(LABEL_NAME)\n", "\n", - "# Label 因子定义(不参与训练,用于计算目标)\n", - "LABEL_FACTOR = {\n", - " LABEL_NAME: \"(ts_delay(close, -5) / ts_delay(open, -1)) - 1\",\n", - "}" - ], - "outputs": [], - "execution_count": null - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": "### 3.2 训练参数配置" - }, - { - "metadata": {}, - "cell_type": "code", - "source": [ - "# 日期范围配置(正确的 train/val/test 三分法)\n", - "TRAIN_START = \"20200101\"\n", - "TRAIN_END = \"20231231\"\n", - "VAL_START = \"20240101\"\n", - "VAL_END = \"20241231\"\n", - "TEST_START = \"20250101\"\n", - "TEST_END = \"20251231\"\n", + "# 分位数配置\n", + "N_QUANTILES = 20 # 将 label 分为 20 组\n", "\n", "\n", "# 分位数配置\n", @@ -378,8 +267,8 @@ "MODEL_PARAMS = {\n", " \"objective\": \"lambdarank\",\n", " \"metric\": \"ndcg\",\n", - " \"ndcg_at\": 10, # 评估 NDCG@k\n", - " \"learning_rate\": 0.01,\n", + " \"ndcg_at\": 5, # 评估 NDCG@k\n", + " \"learning_rate\": 0.05,\n", " \"num_leaves\": 31,\n", " \"max_depth\": 4,\n", " \"min_data_in_leaf\": 20,\n", @@ -391,48 +280,15 @@ " \"reg_lambda\": 1.0,\n", " \"verbose\": -1,\n", " \"random_state\": 42,\n", - " \"lambdarank_truncation_level\": 10,\n", + " \"lambdarank_truncation_level\": 5,\n", " \"label_gain\": [i for i in range(1, N_QUANTILES + 1)],\n", "}\n", "\n", - "\n", - "# 股票池筛选函数\n", - "def stock_pool_filter(df: pl.DataFrame) -> pl.Series:\n", - " \"\"\"股票池筛选函数(单日数据)\n", - "\n", - " 筛选条件:\n", - " 1. 排除创业板(代码以 300 开头)\n", - " 2. 排除科创板(代码以 688 开头)\n", - " 3. 排除北交所(代码以 8、9 或 4 开头)\n", - " 4. 选取当日市值最小的500只股票\n", - " \"\"\"\n", - " code_filter = (\n", - " ~df[\"ts_code\"].str.starts_with(\"30\")\n", - " & ~df[\"ts_code\"].str.starts_with(\"68\")\n", - " & ~df[\"ts_code\"].str.starts_with(\"8\")\n", - " & ~df[\"ts_code\"].str.starts_with(\"9\")\n", - " & ~df[\"ts_code\"].str.starts_with(\"4\")\n", - " )\n", - "\n", - " valid_df = df.filter(code_filter)\n", - " n = min(500, len(valid_df))\n", - " small_cap_codes = valid_df.sort(\"total_mv\").head(n)[\"ts_code\"]\n", - "\n", - " return df[\"ts_code\"].is_in(small_cap_codes)\n", - "\n", - "\n", - "STOCK_FILTER_REQUIRED_COLUMNS = [\"total_mv\"]\n", - "\n", - "# 输出配置\n", - "OUTPUT_DIR = \"output\"\n", - "SAVE_PREDICTIONS = True\n", - "PERSIST_MODEL = False\n", - "\n", - "# Top N 配置:每日推荐股票数量\n", - "TOP_N = 5 # 可调整为 10, 20 等" + "# 注意:stock_pool_filter, STOCK_FILTER_REQUIRED_COLUMNS, OUTPUT_DIR 等配置\n", + "# 已从 common 模块导入" ], "outputs": [], - "execution_count": null + "execution_count": 15 }, { "metadata": {}, @@ -440,7 +296,12 @@ "source": "## 4. 训练流程" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:21.575611Z", + "start_time": "2026-03-14T15:09:09.003108Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -464,6 +325,7 @@ " feature_cols=feature_cols,\n", " start_date=TRAIN_START,\n", " end_date=TEST_END,\n", + " label_name=LABEL_NAME,\n", ")\n", "\n", "# 4. 转换为排序学习格式(分位数标签)\n", @@ -523,8 +385,223 @@ " persist_model=PERSIST_MODEL,\n", ")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "LightGBM LambdaRank 排序学习训练\n", + "================================================================================\n", + "\n", + "[1] 创建 FactorEngine\n", + "\n", + "[2] 定义因子(从 metadata 注册)\n", + "================================================================================\n", + "注册因子\n", + "================================================================================\n", + "\n", + "注册特征因子(从 metadata):\n", + " - ma_5\n", + " - ma_20\n", + " - ma_ratio_5_20\n", + " - bias_10\n", + " - high_low_ratio\n", + " - bbi_ratio\n", + " - return_5\n", + " - return_20\n", + " - kaufman_ER_20\n", + " - mom_acceleration_10_20\n", + " - drawdown_from_high_60\n", + " - up_days_ratio_20\n", + " - volatility_5\n", + " - volatility_20\n", + " - volatility_ratio\n", + " - std_return_20\n", + " - sharpe_ratio_20\n", + " - min_ret_20\n", + " - volatility_squeeze_5_60\n", + " - overnight_intraday_diff\n", + " - upper_shadow_ratio\n", + " - capital_retention_20\n", + " - max_ret_20\n", + " - volume_ratio_5_20\n", + " - turnover_rate_mean_5\n", + " - turnover_deviation\n", + " - amihud_illiq_20\n", + " - turnover_cv_20\n", + " - pv_corr_20\n", + " - close_vwap_deviation\n", + " - roe\n", + " - roa\n", + " - profit_margin\n", + " - debt_to_equity\n", + " - current_ratio\n", + " - net_profit_yoy\n", + " - revenue_yoy\n", + " - healthy_expansion_velocity\n", + " - EP\n", + " - BP\n", + " - CP\n", + " - market_cap_rank\n", + " - turnover_rank\n", + " - return_5_rank\n", + " - EP_rank\n", + " - pe_expansion_trend\n", + " - value_price_divergence\n", + " - active_market_cap\n", + " - ebit_rank\n", + "\n", + "注册特征因子(表达式):\n", + "\n", + "注册 Label 因子(表达式):\n", + " - future_return_5: (ts_delay(close, -5) / ts_delay(open, -1)) - 1\n", + "\n", + "特征因子数: 49\n", + " - 来自 metadata: 49\n", + " - 来自表达式: 0\n", + "Label: future_return_5\n", + "已注册因子总数: 63\n", + "\n", + "[3] 准备数据\n", + "\n", + "================================================================================\n", + "准备数据\n", + "================================================================================\n", + "\n", + "计算因子: 20200101 - 20261231\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "D:\\PyProject\\ProStock\\src\\data\\financial_loader.py:148: UserWarning: Sortedness of columns cannot be checked when 'by' groups provided\n", + " merged = df_price.join_asof(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "数据形状: (7255513, 70)\n", + "数据列: ['ts_code', 'trade_date', 'amount', 'low', 'turnover_rate', 'vol', 'open', 'close', 'high', 'total_assets', 'total_mv', 'f_ann_date', 'total_hldr_eqy_exc_min_int', 'total_liab', 'total_cur_liab', 'total_cur_assets', 'ebit', 'n_income', 'revenue', 'n_cashflow_act', 'ma_5', 'ma_20', 'ma_ratio_5_20', 'bias_10', 'high_low_ratio', 'bbi_ratio', 'return_5', 'return_20', 'kaufman_ER_20', 'mom_acceleration_10_20', 'drawdown_from_high_60', 'up_days_ratio_20', 'volatility_5', 'volatility_20', 'volatility_ratio', 'std_return_20', 'sharpe_ratio_20', 'min_ret_20', 'volatility_squeeze_5_60', 'overnight_intraday_diff', 'upper_shadow_ratio', 'capital_retention_20', 'max_ret_20', 'volume_ratio_5_20', 'turnover_rate_mean_5', 'turnover_deviation', 'amihud_illiq_20', 'turnover_cv_20', 'pv_corr_20', 'close_vwap_deviation', 'roe', 'roa', 'profit_margin', 'debt_to_equity', 'current_ratio', 'net_profit_yoy', 'revenue_yoy', 'healthy_expansion_velocity', 'EP', 'BP', 'CP', 'market_cap_rank', 'turnover_rank', 'return_5_rank', 'EP_rank', 'pe_expansion_trend', 'value_price_divergence', 'active_market_cap', 'ebit_rank', 'future_return_5']\n", + "\n", + "前5行预览:\n", + "shape: (5, 70)\n", + "┌───────────┬────────────┬──────────┬─────────┬───┬────────────┬───────────┬───────────┬───────────┐\n", + "│ ts_code ┆ trade_date ┆ amount ┆ low ┆ … ┆ value_pric ┆ active_ma ┆ ebit_rank ┆ future_re │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ e_divergen ┆ rket_cap ┆ --- ┆ turn_5 │\n", + "│ str ┆ str ┆ f64 ┆ f64 ┆ ┆ ce ┆ --- ┆ f64 ┆ --- │\n", + "│ ┆ ┆ ┆ ┆ ┆ --- ┆ f64 ┆ ┆ f64 │\n", + "│ ┆ ┆ ┆ ┆ ┆ f64 ┆ ┆ ┆ │\n", + "╞═══════════╪════════════╪══════════╪═════════╪═══╪════════════╪═══════════╪═══════════╪═══════════╡\n", + "│ 000001.SZ ┆ 20200102 ┆ 2.5712e6 ┆ 1806.75 ┆ … ┆ null ┆ null ┆ null ┆ -0.008857 │\n", + "│ 000001.SZ ┆ 20200103 ┆ 1.9145e6 ┆ 1847.15 ┆ … ┆ null ┆ null ┆ null ┆ -0.01881 │\n", + "│ 000001.SZ ┆ 20200106 ┆ 1.4779e6 ┆ 1846.05 ┆ … ┆ null ┆ null ┆ null ┆ -0.008171 │\n", + "│ 000001.SZ ┆ 20200107 ┆ 1.2470e6 ┆ 1850.42 ┆ … ┆ null ┆ null ┆ null ┆ -0.014117 │\n", + "│ 000001.SZ ┆ 20200108 ┆ 1.4236e6 ┆ 1815.49 ┆ … ┆ null ┆ null ┆ null ┆ -0.017252 │\n", + "└───────────┴────────────┴──────────┴─────────┴───┴────────────┴───────────┴───────────┴───────────┘\n", + "\n", + "[4] 转换为排序学习格式\n", + "\n", + "================================================================================\n", + "准备排序学习数据(将 future_return_5 转换为 20 分位数标签)\n", + "================================================================================\n", + "\n", + "原始 future_return_5 统计:\n", + "shape: (9, 2)\n", + "┌────────────┬────────────┐\n", + "│ statistic ┆ value │\n", + "│ --- ┆ --- │\n", + "│ str ┆ f64 │\n", + "╞════════════╪════════════╡\n", + "│ count ┆ 7.227054e6 │\n", + "│ null_count ┆ 28459.0 │\n", + "│ mean ┆ 0.003978 │\n", + "│ std ┆ 0.073204 │\n", + "│ min ┆ -0.969459 │\n", + "│ 25% ┆ -0.032998 │\n", + "│ 50% ┆ -0.001278 │\n", + "│ 75% ┆ 0.032666 │\n", + "│ max ┆ 10.361925 │\n", + "└────────────┴────────────┘\n", + "\n", + "转换后 future_return_5_rank 统计:\n", + "shape: (9, 2)\n", + "┌────────────┬────────────┐\n", + "│ statistic ┆ value │\n", + "│ --- ┆ --- │\n", + "│ str ┆ f64 │\n", + "╞════════════╪════════════╡\n", + "│ count ┆ 7.227054e6 │\n", + "│ null_count ┆ 28459.0 │\n", + "│ mean ┆ 9.493551 │\n", + "│ std ┆ 5.765628 │\n", + "│ min ┆ 0.0 │\n", + "│ 25% ┆ 4.0 │\n", + "│ 50% ┆ 9.0 │\n", + "│ 75% ┆ 14.0 │\n", + "│ max ┆ 19.0 │\n", + "└────────────┴────────────┘\n", + "\n", + "每日样本数统计:\n", + "shape: (9, 2)\n", + "┌────────────┬─────────────┐\n", + "│ statistic ┆ value │\n", + "│ --- ┆ --- │\n", + "│ str ┆ f64 │\n", + "╞════════════╪═════════════╡\n", + "│ count ┆ 1494.0 │\n", + "│ null_count ┆ 0.0 │\n", + "│ mean ┆ 4856.434404 │\n", + "│ std ┆ 564.521537 │\n", + "│ min ┆ 2885.0 │\n", + "│ 25% ┆ 4382.0 │\n", + "│ 50% ┆ 5069.0 │\n", + "│ 75% ┆ 5347.0 │\n", + "│ max ┆ 5476.0 │\n", + "└────────────┴─────────────┘\n", + "\n", + "分位数标签分布:\n", + "shape: (21, 2)\n", + "┌──────────────────────┬────────┐\n", + "│ future_return_5_rank ┆ count │\n", + "│ --- ┆ --- │\n", + "│ i64 ┆ u32 │\n", + "╞══════════════════════╪════════╡\n", + "│ null ┆ 28459 │\n", + "│ 0 ┆ 362270 │\n", + "│ 1 ┆ 361546 │\n", + "│ 2 ┆ 361599 │\n", + "│ 3 ┆ 361755 │\n", + "│ … ┆ … │\n", + "│ 15 ┆ 361289 │\n", + "│ 16 ┆ 361218 │\n", + "│ 17 ┆ 361227 │\n", + "│ 18 ┆ 361252 │\n", + "│ 19 ┆ 359483 │\n", + "└──────────────────────┴────────┘\n", + "\n", + "[配置] 训练期: 20200101 - 20231231\n", + "[配置] 验证期: 20240101 - 20241231\n", + "[配置] 测试期: 20250101 - 20261231\n", + "[配置] 特征数: 49\n", + "[配置] 目标变量: future_return_5_rank(20分位数)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\liaozhaorun\\AppData\\Local\\Temp\\ipykernel_28476\\562285170.py:58: DeprecationWarning: `pl.count()` is deprecated. Please use `pl.len()` instead.\n", + "(Deprecated in version 0.20.5)\n", + " daily_counts = df_ranked.group_by(date_col).agg(pl.count().alias(\"count\"))\n" + ] + } + ], + "execution_count": 16 }, { "metadata": {}, @@ -532,7 +609,12 @@ "source": "### 4.1 股票池筛选" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:45.297379Z", + "start_time": "2026-03-14T15:09:21.587998Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -557,8 +639,29 @@ " filtered_data = data\n", " print(\" 未配置股票池管理器,跳过筛选\")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "股票池筛选\n", + "================================================================================\n", + "\n", + "[过滤] 应用 ST 过滤器...\n", + " ST 过滤后数据规模: (7027678, 71)\n", + "\n", + "执行每日独立筛选股票池...\n", + " 筛选前数据规模: (7027678, 71)\n", + " 筛选后数据规模: (747000, 71)\n", + " 筛选前股票数: 5694\n", + " 筛选后股票数: 1439\n", + " 删除记录数: 6280678\n" + ] + } + ], + "execution_count": 17 }, { "metadata": {}, @@ -566,7 +669,12 @@ "source": "### 4.2 数据划分" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:45.376100Z", + "start_time": "2026-03-14T15:09:45.307622Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -593,8 +701,39 @@ "else:\n", " raise ValueError(\"必须配置数据划分器\")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "数据划分\n", + "================================================================================\n", + "\n", + "训练集数据规模: (485000, 71)\n", + "验证集数据规模: (121000, 71)\n", + "测试集数据规模: (141000, 71)\n", + "\n", + "训练集 group 数量: 970\n", + "验证集 group 数量: 242\n", + "测试集 group 数量: 282\n", + "训练集日均样本数: 500.0\n", + "验证集日均样本数: 500.0\n", + "测试集日均样本数: 500.0\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\liaozhaorun\\AppData\\Local\\Temp\\ipykernel_28476\\562285170.py:82: DeprecationWarning: `pl.count()` is deprecated. Please use `pl.len()` instead.\n", + "(Deprecated in version 0.20.5)\n", + " pl.count().alias(\"count\")\n" + ] + } + ], + "execution_count": 18 }, { "metadata": {}, @@ -602,7 +741,12 @@ "source": "### 4.3 数据质量检查" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:47.637521Z", + "start_time": "2026-03-14T15:09:45.382468Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -620,8 +764,52 @@ "\n", "print(\"[成功] 数据质量检查通过,未发现异常\")\n" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "数据质量检查(必须在预处理之前)\n", + "================================================================================\n", + "\n", + "检查训练集...\n", + "\n", + "================================================================================\n", + "数据质量检查报告\n", + "================================================================================\n", + "\n", + "[严重] 发现 1638 个全空因子:\n", + " (某天的某个因子所有值都是 null,可能是数据缺失或计算错误)\n", + " - 日期 20200824: roa (样本数: 500)\n", + " - 日期 20200824: net_profit_yoy (样本数: 500)\n", + " - 日期 20200824: revenue_yoy (样本数: 500)\n", + " - 日期 20200824: healthy_expansion_velocity (样本数: 500)\n", + " - 日期 20200824: value_price_divergence (样本数: 500)\n", + " - 日期 20200115: ma_20 (样本数: 500)\n", + " - 日期 20200115: ma_ratio_5_20 (样本数: 500)\n", + " - 日期 20200115: high_low_ratio (样本数: 500)\n", + " - 日期 20200115: bbi_ratio (样本数: 500)\n", + " - 日期 20200115: return_20 (样本数: 500)\n", + " ... 还有 1628 个\n", + "\n", + "--------------------------------------------------------------------------------\n", + "建议处理方式:\n", + " 1. 检查因子定义和数据源,确认计算逻辑是否正确\n", + " 2. 如果是预期内的缺失(如新股无历史数据),考虑调整因子计算窗口\n", + " 3. 如果是数据同步问题,重新同步相关数据\n", + " 4. 可以使用 filter 排除问题日期或因子\n", + "================================================================================\n", + "\n", + "检查验证集...\n", + "\n", + "检查测试集...\n", + "[成功] 数据质量检查通过,未发现异常\n" + ] + } + ], + "execution_count": 19 }, { "metadata": {}, @@ -629,7 +817,12 @@ "source": "### 4.4 数据预处理" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:47.934178Z", + "start_time": "2026-03-14T15:09:47.643592Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -656,8 +849,32 @@ "print(f\"处理后验证集形状: {val_data.shape}\")\n", "print(f\"处理后测试集形状: {test_data.shape}\")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "数据预处理\n", + "================================================================================\n", + "\n", + "训练集处理...\n", + " [1/3] NullFiller\n", + " [2/3] Winsorizer\n", + " [3/3] StandardScaler\n", + "\n", + "验证集处理...\n", + "\n", + "测试集处理...\n", + "\n", + "处理后训练集形状: (485000, 71)\n", + "处理后验证集形状: (121000, 71)\n", + "处理后测试集形状: (141000, 71)\n" + ] + } + ], + "execution_count": 20 }, { "metadata": {}, @@ -665,7 +882,12 @@ "source": "### 4.4 训练 LambdaRank 模型" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:48.844310Z", + "start_time": "2026-03-14T15:09:47.939155Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -696,8 +918,48 @@ ")\n", "print(\"训练完成!\")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "训练 LambdaRank 模型\n", + "================================================================================\n", + "\n", + "训练样本数: 485000\n", + "验证样本数: 121000\n", + "特征数: 49\n", + "目标变量: future_return_5_rank\n", + "\n", + "目标变量统计(训练集):\n", + "shape: (9, 2)\n", + "┌────────────┬──────────┐\n", + "│ statistic ┆ value │\n", + "│ --- ┆ --- │\n", + "│ str ┆ f64 │\n", + "╞════════════╪══════════╡\n", + "│ count ┆ 484665.0 │\n", + "│ null_count ┆ 335.0 │\n", + "│ mean ┆ 9.988943 │\n", + "│ std ┆ 5.224762 │\n", + "│ min ┆ 0.0 │\n", + "│ 25% ┆ 6.0 │\n", + "│ 50% ┆ 10.0 │\n", + "│ 75% ┆ 14.0 │\n", + "│ max ┆ 19.0 │\n", + "└────────────┴──────────┘\n", + "\n", + "开始训练...\n", + "Training until validation scores don't improve for 100 rounds\n", + "Early stopping, best iteration is:\n", + "[5]\ttrain's ndcg@5: 0.610972\tval's ndcg@5: 0.558883\n", + "训练完成!\n" + ] + } + ], + "execution_count": 21 }, { "metadata": {}, @@ -705,7 +967,12 @@ "source": "### 4.5 训练指标曲线" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:48.923721Z", + "start_time": "2026-03-14T15:09:48.850317Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -758,8 +1025,57 @@ " print(f\" {metric}: {best_val:.4f} (迭代 {best_iter_metric + 1})\")\n", " print(f\"\\n[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!\")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "训练指标曲线\n", + "================================================================================\n", + "[成功] 已从模型获取训练评估结果\n", + "\n", + "评估的 NDCG 指标: ['ndcg@5']\n", + "\n", + "[早停信息]\n", + " 配置的最大轮数: 2000\n", + " 实际训练轮数: 105\n", + " 早停状态: 已触发(最佳迭代: 5)\n", + "\n", + "最终 NDCG 指标:\n", + " ndcg@5: 训练集=0.7120, 验证集=0.5334\n", + "\n", + "[绘图] 使用 LightGBM 原生接口绘制训练曲线...\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ], + "image/png": "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kqLatWvr/PPPV3p6uiobQts9vPHGG7r66qt16623aubMmerWrZuGDRum1NTU6LxDAEps0qRJXiD7448/6ssvv1RGRoaGDh2qbdu25dzmb3/7mz788EO99dZb3u1XrVqlUaNG8aoDFcz06dP11FNPqWvXrvkuZxsHKrZNmzbp0EMPVWJioj799FP99ttv+te//qU6derk3Ob+++/XI488oieffFJTp05V9erVvb/fbacNAH+777779O9//1uPPfaY5s2b5523bfrRRx/NuQ3bOFCx2Pdty9KsILIgRdmmLbCdO3eu9z3+o48+8oLgCy+8UJVOEPn06dMneOmll+acz8rKCjZt2jQ4btw4XimggktNTbVd9sFJkyZ55zdv3hxMTEwMvvXWWzm3mTdvnnebKVOmRHFNARRHWlpasG3btsEvv/wyOGjQoOCVV17pXc42DlR8f//734MDBw4s9Prs7Oxg48aNgw888EDOZbbtJycnB1977bVyWksAJXXssccGzzvvvHyXjRo1KnjWWWd5p9nGgYrNvlu/++67OeeLsk3/9ttv3v2mT5+ec5tPP/00GAgEgitXrgxWJlTa5rF7927NmDHDK80Oi4uL885PmTIlGpk6gAjasmWLt6xbt663tO3dqm/zbvPt27fXAQccwDYPVCBWUX/sscfm25YN2zhQ8X3wwQfq3bu3TjnlFK/VUY8ePfTMM8/kXL9kyRKtWbMm3/Zfq1Ytr8UZf78D/jdgwABNmDBBCxYs8M7//PPP+v777zV8+HDvPNs4EFuKsk3b0loi9O7dO+c2dnvL56wytzJJiPYK+Mn69eu9njqNGjXKd7md//3336O2XgBKLzs72+tzaYdYdu7c2bvMflkkJSV5vxD23ObtOgD+9/rrr3vtjKw9wp7YxoGKb/Hixd6h09a+7MYbb/S29SuuuML7/T169Oic39cF/f3O73LA/66//npt3brVK5yIj4/3vo/ffffd3qHRhm0ciC1F2aZtaTtq80pISPCKryrb73ZCWwCVphLv119/9fbcA4gNy5cv15VXXun1urLJQwHE5k5Xq7S55557vPNWaWu/z60PnoW2ACq2N998U6+88opeffVVderUSbNnz/YKLWwCI7ZxAJUd7RHyqF+/vrd3b8+Z4+1848aNy/u9ARAhl112mde8fOLEiWrevHnO5bZdW1uUzZs357s92zxQMVj7A5sotGfPnt7edxs2oaBNbGCnbY892zhQsdnM0h07dsx3WYcOHbRs2TLvdPhvdP5+Byqma6+91qu2Pf3009WlSxedffbZ3iSi48aN865nGwdiS1G2aVva3/h5ZWZmauPGjZUumyO0zcMOs+rVq5fXUyfv3n07379//2i8PwBKwfqeW2D77rvv6uuvv1br1q3zXW/bu81GnXebnz9/vvdFkG0e8L/Bgwfrl19+8apywsMq8uyQyvBptnGgYrO2Rva7OS/rfdmyZUvvtP1uty9weX+X26HW1vOO3+WA/23fvt3rU5mXFVLZ93DDNg7ElqJs07a0wqoZM2bk3Ma+z9v/C9b7tjKhPcIerF+WHYZhX/T69Omj8ePHa9u2bRozZkx03iEAJWYtEexQq/fff181a9bM6X9jjc6rVq3qLc8//3xvu7f+OCkpKbr88su9XxL9+vXjlQd8zrbrcI/qsOrVq6tevXo5l7ONAxWbVdzZREXWHuHUU0/VtGnT9PTTT3vDBAIB71Dqu+66S23btvW+DN58883eodUjR46M9uoD2I/jjz/e62FrEwFbe4RZs2bpwQcf1HnnneddzzYOVDzp6elatGhRvsnHrKDCvnPbtr6/39t2RM0xxxyjsWPHeu2QbPJwK8ayiny7XaUSxF4effTR4AEHHBBMSkoK9unTJ/jjjz/yKgEVkP0XV9B4/vnnc26zY8eO4CWXXBKsU6dOsFq1asG//OUvwdWrV0d1vQGU3KBBg4JXXnllznm2caDi+/DDD4OdO3cOJicnB9u3bx98+umn812fnZ0dvPnmm4ONGjXybjN48ODg/Pnzo7a+AIpu69at3u9t+/5dpUqVYJs2bYL/+Mc/grt27cq5Dds4ULFMnDixwO/ho0ePLvI2vWHDhuAZZ5wRrFGjRjAlJSU4ZsyYYFpaWrCyCdg/0Q6OAQAAAAAAAAAOPW0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAAAAAAMBHCG0BAAAAAAAAwEcIbQEAAAAAAADARwhtAQAAgDLQqlUrjR8/ntcWAAAAxUZoCwAAgArv3HPP1ciRI73TRxxxhK666qpye+4XXnhBtWvX3uvy6dOn68ILLyy39QAAAEDsSIj2CgAAAAB+tHv3biUlJZX4/g0aNIjo+gAAAKDyoNIWAAAAMVVxO2nSJD388MMKBALe+PPPP73rfv31Vw0fPlw1atRQo0aNdPbZZ2v9+vU597UK3csuu8yr0q1fv76GDRvmXf7ggw+qS5cuql69ulq0aKFLLrlE6enp3nXffPONxowZoy1btuQ832233VZge4Rly5bpxBNP9J4/JSVFp556qtauXZtzvd2ve/fuevnll7371qpVS6effrrS0tJybvP2229761K1alXVq1dPQ4YM0bZt28rhlQUAAEB5IrQFAABAzLCwtn///ho7dqxWr17tDQtaN2/erKOOOko9evTQTz/9pM8++8wLTC04zevFF1/0qmt/+OEHPfnkk95lcXFxeuSRRzR37lzv+q+//lrXXXedd92AAQO8YNZC2PDzXXPNNXutV3Z2thfYbty40QuVv/zySy1evFinnXZavtv98ccfeu+99/TRRx95w2577733etfZY59xxhk677zzNG/ePC8wHjVqlILBYBm+ogAAAIgG2iMAAAAgZlh1qoWu1apVU+PGjXMuf+yxx7zA9p577sm57LnnnvMC3QULFqhdu3beZW3bttX999+f7zHz9se1Cti77rpLF198sZ544gnvuew5rcI27/PtacKECfrll1+0ZMkS7znNSy+9pE6dOnm9bw855JCccNd65NasWdM7b9XAdt+7777bC20zMzO9oLZly5be9VZ1CwAAgNhDpS0AAABi3s8//6yJEyd6rQnCo3379jnVrWG9evXa675fffWVBg8erGbNmnlhqgWpGzZs0Pbt24v8/FYZa2FtOLA1HTt29CYws+vyhsLhwNY0adJEqamp3ulu3bp562FB7SmnnKJnnnlGmzZtKsGrAQAAAL8jtAUAAEDMsx60xx9/vGbPnp1vLFy4UIcffnjO7axvbV7WD/e4445T165d9b///U8zZszQ448/njNRWaQlJibmO28VvFZ9a+Lj4722Cp9++qkX+D766KM6+OCDvepdAAAAxBZCWwAAAMQUa1mQlZWV77KePXt6PWmtkvWggw7KN/YMavOykNZC03/961/q16+f10Zh1apV+32+PXXo0EHLly/3Rthvv/3m9dq1ALaoLMQ99NBDdfvtt2vWrFnec7/77rtFvj8AAAAqBkJbAAAAxBQLZqdOnepVya5fv94LXS+99FJvEjCbyMt6yFpLhM8//1xjxozZZ+BqoW5GRoZX1WoTh7388ss5E5TlfT6r5LXes/Z8BbVNGDJkiNfW4KyzztLMmTM1bdo0nXPOORo0aJB69+5dpJ/LfibryWsTqS1btkzvvPOO1q1b5wXCAAAAiC2EtgAAAIgp11xzjddKwCpYGzRo4AWcTZs21Q8//OAFtEOHDvUCVJtgzHrKxsUV/iex9ZF98MEHdd9996lz58565ZVXNG7cuHy3GTBggDcx2WmnneY9354TmYUrZN9//33VqVPHa8dgIW6bNm30xhtvFPnnSklJ0bfffqsRI0Z4Fb833XSTVwE8fPjwYr5CAAAA8LtAMBgMRnslAAAAAAAAAAAOlbYAAAAAAAAA4COEtgAAAAAAAADgI4S2AAAAAAAAAOAjhLYAAAAAAAAA4COEtgAAAAAAAADgI4S2AAAAAAAAAOAjhLYAAAAAAAAA4COEtgAAAAAAAADgI4S2AAAAAAAAAOAjhLYAAAAAAAAA4COEtgAAAAAAAAAg//j/rE8k3ESUF+oAAAAASUVORK5CYII=" + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "[指标分析]\n", + " 各NDCG指标在验证集上的最佳值:\n", + " ndcg@5: 0.5589 (迭代 5)\n", + "\n", + "[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!\n" + ] + } + ], + "execution_count": 22 }, { "metadata": {}, @@ -767,7 +1083,12 @@ "source": "### 4.6 模型评估" }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:49.191132Z", + "start_time": "2026-03-14T15:09:48.936532Z" + } + }, "cell_type": "code", "source": [ "print(\"\\n\" + \"=\" * 80)\n", @@ -808,11 +1129,61 @@ " for i, (feature, score) in enumerate(top_features.items(), 1):\n", " print(f\" {i:2d}. {feature:30s} {score:10.2f}\")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "================================================================================\n", + "模型评估\n", + "================================================================================\n", + "\n", + "生成预测...\n", + "\n", + "计算 NDCG 指标...\n", + "\n", + "NDCG 评估结果:\n", + "----------------------------------------\n", + " ndcg@1: 0.5390\n", + " ndcg@5: 0.5280\n", + " ndcg@10: 0.5261\n", + " ndcg@20: 0.5281\n", + "\n", + "特征重要性(Top 20):\n", + "----------------------------------------\n", + " 1. max_ret_20 178.55\n", + " 2. ma_ratio_5_20 175.30\n", + " 3. ma_5 161.37\n", + " 4. market_cap_rank 144.83\n", + " 5. CP 130.89\n", + " 6. roa 109.85\n", + " 7. healthy_expansion_velocity 108.79\n", + " 8. roe 107.79\n", + " 9. ebit_rank 103.48\n", + " 10. close_vwap_deviation 96.07\n", + " 11. std_return_20 94.57\n", + " 12. revenue_yoy 93.39\n", + " 13. ma_20 90.67\n", + " 14. turnover_rank 78.58\n", + " 15. pv_corr_20 75.06\n", + " 16. amihud_illiq_20 71.67\n", + " 17. EP_rank 60.11\n", + " 18. return_20 46.65\n", + " 19. min_ret_20 46.34\n", + " 20. volume_ratio_5_20 45.85\n" + ] + } + ], + "execution_count": 23 }, { - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-14T15:09:49.453399Z", + "start_time": "2026-03-14T15:09:49.213145Z" + } + }, "cell_type": "code", "source": [ "# 确保输出目录存在\n", @@ -861,8 +1232,80 @@ "\n", "print(\"\\n训练流程完成!\")" ], - "outputs": [], - "execution_count": null + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "[1/1] 保存每日 Top 5 股票...\n", + " 保存路径: output\\rank_output.csv\n", + " 保存行数: 1410(282个交易日 x 每日top5)\n", + "\n", + " 预览(前15行):\n", + "shape: (15, 3)\n", + "┌────────────┬──────────┬───────────┐\n", + "│ trade_date ┆ score ┆ ts_code │\n", + "│ --- ┆ --- ┆ --- │\n", + "│ str ┆ f64 ┆ str │\n", + "╞════════════╪══════════╪═══════════╡\n", + "│ 2025-01-02 ┆ 0.092165 ┆ 002816.SZ │\n", + "│ 2025-01-02 ┆ 0.067764 ┆ 002634.SZ │\n", + "│ 2025-01-02 ┆ 0.066779 ┆ 002836.SZ │\n", + "│ 2025-01-02 ┆ 0.054118 ┆ 000004.SZ │\n", + "│ 2025-01-02 ┆ 0.046321 ┆ 000691.SZ │\n", + "│ … ┆ … ┆ … │\n", + "│ 2025-01-06 ┆ 0.092165 ┆ 002816.SZ │\n", + "│ 2025-01-06 ┆ 0.066779 ┆ 002836.SZ │\n", + "│ 2025-01-06 ┆ 0.05733 ┆ 002634.SZ │\n", + "│ 2025-01-06 ┆ 0.054118 ┆ 000004.SZ │\n", + "│ 2025-01-06 ┆ 0.052639 ┆ 600857.SH │\n", + "└────────────┴──────────┴───────────┘\n", + "\n", + "训练流程完成!\n" + ] + } + ], + "execution_count": 24 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## 5. 总结\n", + "#\n", + "本 Notebook 实现了完整的 Learn-to-Rank 训练流程:\n", + "#\n", + "### 核心步骤\n", + "#\n", + "1. **数据准备**: 计算 49 个特征因子,将 `future_return_5` 转换为 20 分位数标签\n", + "2. **模型训练**: 使用 LightGBM LambdaRank 学习每日股票排序\n", + "3. **模型评估**: 使用 NDCG@1/5/10/20 评估排序质量\n", + "4. **策略分析**: 基于排序分数构建 Top-k 选股策略\n", + "#\n", + "### 关键参数\n", + "#\n", + "- **Objective**: lambdarank\n", + "- **Metric**: ndcg\n", + "- **Learning Rate**: 0.05\n", + "- **Num Leaves**: 31\n", + "- **N Quantiles**: 20\n", + "#\n", + "### 输出结果\n", + "#\n", + "- rank_output.csv: 每日Top-N推荐股票(格式:date, score, ts_code)\n", + "- 特征重要性排名\n", + "- Top-k 策略统计和图表\n", + "- NDCG训练指标曲线\n", + "#\n", + "### 后续优化方向\n", + "#\n", + "1. **特征工程**: 尝试更多因子组合\n", + "2. **超参数调优**: 使用网格搜索优化 LambdaRank 参数\n", + "3. **模型集成**: 结合多个排序模型的预测\n", + "4. **更复杂的分组**: 考虑按行业分组排序\n", + "#\n" + ] }, { "metadata": {}, diff --git a/src/experiment/learn_to_rank.py b/src/experiment/learn_to_rank.py index bf85985..4205141 100644 --- a/src/experiment/learn_to_rank.py +++ b/src/experiment/learn_to_rank.py @@ -1,4 +1,4 @@ -#%% md +# %% md # # Learn-to-Rank 排序学习训练流程 # # # 本 Notebook 实现基于 LightGBM LambdaRank 的排序学习训练,用于股票排序任务。 @@ -9,9 +9,9 @@ # 2. **排序学习**: 使用 LambdaRank 目标函数,学习每日股票排序 # 3. **NDCG 评估**: 使用 NDCG@1/5/10/20 评估排序质量 # 4. **策略回测**: 基于排序分数构建 Top-k 选股策略 -#%% md +# %% md # ## 1. 导入依赖 -#%% +# %% import os from datetime import datetime from typing import List, Tuple, Optional @@ -36,78 +36,32 @@ from src.training import ( from src.training.components.models import LightGBMLambdaRankModel from src.training.config import TrainingConfig - -#%% md -# ## 2. 辅助函数 -#%% -def register_factors( - engine: FactorEngine, - selected_factors: List[str], - factor_definitions: dict, - label_factor: dict, -) -> List[str]: - """注册因子(selected_factors 从 metadata 查询,factor_definitions 用 DSL 表达式注册)""" - print("=" * 80) - print("注册因子") - print("=" * 80) - - # 注册 SELECTED_FACTORS 中的因子(已在 metadata 中) - print("\n注册特征因子(从 metadata):") - for name in selected_factors: - engine.add_factor(name) - print(f" - {name}") - - # 注册 FACTOR_DEFINITIONS 中的因子(通过表达式,尚未在 metadata 中) - print("\n注册特征因子(表达式):") - for name, expr in factor_definitions.items(): - engine.add_factor(name, expr) - print(f" - {name}: {expr}") - - # 注册 label 因子(通过表达式) - print("\n注册 Label 因子(表达式):") - for name, expr in label_factor.items(): - engine.add_factor(name, expr) - print(f" - {name}: {expr}") - - # 特征列 = SELECTED_FACTORS + FACTOR_DEFINITIONS 的 keys - feature_cols = selected_factors + list(factor_definitions.keys()) - - print(f"\n特征因子数: {len(feature_cols)}") - print(f" - 来自 metadata: {len(selected_factors)}") - print(f" - 来自表达式: {len(factor_definitions)}") - print(f"Label: {list(label_factor.keys())[0]}") - print(f"已注册因子总数: {len(engine.list_registered())}") - - return feature_cols +# 从 common 模块导入共用配置和函数 +from src.experiment.common import ( + SELECTED_FACTORS, + FACTOR_DEFINITIONS, + get_label_factor, + register_factors, + prepare_data, + TRAIN_START, + TRAIN_END, + VAL_START, + VAL_END, + TEST_START, + TEST_END, + stock_pool_filter, + STOCK_FILTER_REQUIRED_COLUMNS, + OUTPUT_DIR, + SAVE_PREDICTIONS, + PERSIST_MODEL, + TOP_N, +) -def prepare_data( - engine: FactorEngine, - feature_cols: List[str], - start_date: str, - end_date: str, -) -> pl.DataFrame: - """准备数据""" - print("\n" + "=" * 80) - print("准备数据") - print("=" * 80) - - # 计算因子(全市场数据) - print(f"\n计算因子: {start_date} - {end_date}") - factor_names = feature_cols + [LABEL_NAME] # 包含 label - - data = engine.compute( - factor_names=factor_names, - start_date=start_date, - end_date=end_date, - ) - - print(f"数据形状: {data.shape}") - print(f"数据列: {data.columns}") - print(f"\n前5行预览:") - print(data.head()) - - return data +# %% md +# ## 2. 本地辅助函数 +# %% +# 注意:register_factors 和 prepare_data 已从 common 模块导入 def prepare_ranking_data( @@ -240,92 +194,22 @@ def evaluate_ndcg_at_k( return results -#%% md +# %% md # ## 3. 配置参数 # # -# ### 3.1 因子定义 -#%% -# 特征因子定义字典(复用 regression.ipynb 的因子定义) -LABEL_NAME = "future_return_5_rank" +# ### 3.1 因子与日期配置 +# %% +# 注意:SELECTED_FACTORS, FACTOR_DEFINITIONS, 日期配置等已从 common 模块导入 +# 本脚本特有的配置: -# 当前选择的因子列表(从 FACTOR_DEFINITIONS 中选择要使用的因子) -SELECTED_FACTORS = [ - # ================= 1. 价格、趋势与路径依赖 ================= - "ma_5", - "ma_20", - "ma_ratio_5_20", - "bias_10", - "high_low_ratio", - "bbi_ratio", - "return_5", - "return_20", - "kaufman_ER_20", - "mom_acceleration_10_20", - "drawdown_from_high_60", - "up_days_ratio_20", - # ================= 2. 波动率、风险调整与高阶矩 ================= - "volatility_5", - "volatility_20", - "volatility_ratio", - "std_return_20", - "sharpe_ratio_20", - "min_ret_20", - "volatility_squeeze_5_60", - # ================= 3. 日内微观结构与异象 ================= - "overnight_intraday_diff", - "upper_shadow_ratio", - "capital_retention_20", - "max_ret_20", - # ================= 4. 量能、流动性与量价背离 ================= - "volume_ratio_5_20", - "turnover_rate_mean_5", - "turnover_deviation", - "amihud_illiq_20", - "turnover_cv_20", - "pv_corr_20", - "close_vwap_deviation", - # ================= 5. 基本面财务特征 ================= - "roe", - "roa", - "profit_margin", - "debt_to_equity", - "current_ratio", - "net_profit_yoy", - "revenue_yoy", - "healthy_expansion_velocity", - "ebit_rank", - # ================= 6. 基本面估值与截面动量共振 ================= - "EP", - "BP", - "CP", - "market_cap_rank", - "turnover_rank", - "return_5_rank", - "EP_rank", - "pe_expansion_trend", - "value_price_divergence", - "active_market_cap", -] +# Label 名称(排序学习使用原始收益率,会后续转换为分位数标签) +LABEL_NAME = "future_return_5" -# 因子定义字典(完整因子库) -FACTOR_DEFINITIONS = { - # "turnover_rate_volatility": "ts_std(log(turnover_rate), 20)" -} +# 获取 Label 因子定义 +LABEL_FACTOR = get_label_factor(LABEL_NAME) -# Label 因子定义(不参与训练,用于计算目标) -LABEL_FACTOR = { - LABEL_NAME: "(ts_delay(close, -5) / ts_delay(open, -1)) - 1", -} -#%% md -# ### 3.2 训练参数配置 -#%% -# 日期范围配置(正确的 train/val/test 三分法) -TRAIN_START = "20200101" -TRAIN_END = "20231231" -VAL_START = "20240101" -VAL_END = "20241231" -TEST_START = "20250101" -TEST_END = "20251231" +# 分位数配置 +N_QUANTILES = 20 # 将 label 分为 20 组 # 分位数配置 @@ -352,44 +236,11 @@ MODEL_PARAMS = { "label_gain": [i for i in range(1, N_QUANTILES + 1)], } - -# 股票池筛选函数 -def stock_pool_filter(df: pl.DataFrame) -> pl.Series: - """股票池筛选函数(单日数据) - - 筛选条件: - 1. 排除创业板(代码以 300 开头) - 2. 排除科创板(代码以 688 开头) - 3. 排除北交所(代码以 8、9 或 4 开头) - 4. 选取当日市值最小的500只股票 - """ - code_filter = ( - ~df["ts_code"].str.starts_with("30") - & ~df["ts_code"].str.starts_with("68") - & ~df["ts_code"].str.starts_with("8") - & ~df["ts_code"].str.starts_with("9") - & ~df["ts_code"].str.starts_with("4") - ) - - valid_df = df.filter(code_filter) - n = min(500, len(valid_df)) - small_cap_codes = valid_df.sort("total_mv").head(n)["ts_code"] - - return df["ts_code"].is_in(small_cap_codes) - - -STOCK_FILTER_REQUIRED_COLUMNS = ["total_mv"] - -# 输出配置 -OUTPUT_DIR = "output" -SAVE_PREDICTIONS = True -PERSIST_MODEL = False - -# Top N 配置:每日推荐股票数量 -TOP_N = 5 # 可调整为 10, 20 等 -#%% md +# 注意:stock_pool_filter, STOCK_FILTER_REQUIRED_COLUMNS, OUTPUT_DIR 等配置 +# 已从 common 模块导入 +# %% md # ## 4. 训练流程 -#%% +# %% print("\n" + "=" * 80) print("LightGBM LambdaRank 排序学习训练") print("=" * 80) @@ -411,6 +262,7 @@ data = prepare_data( feature_cols=feature_cols, start_date=TRAIN_START, end_date=TEST_END, + label_name=LABEL_NAME, ) # 4. 转换为排序学习格式(分位数标签) @@ -469,9 +321,9 @@ trainer = Trainer( feature_cols=feature_cols, persist_model=PERSIST_MODEL, ) -#%% md +# %% md # ### 4.1 股票池筛选 -#%% +# %% print("\n" + "=" * 80) print("股票池筛选") print("=" * 80) @@ -493,9 +345,9 @@ if pool_manager: else: filtered_data = data print(" 未配置股票池管理器,跳过筛选") -#%% md +# %% md # ### 4.2 数据划分 -#%% +# %% print("\n" + "=" * 80) print("数据划分") print("=" * 80) @@ -519,9 +371,9 @@ if splitter: print(f"测试集日均样本数: {np.mean(test_group):.1f}") else: raise ValueError("必须配置数据划分器") -#%% md +# %% md # ### 4.3 数据质量检查 -#%% +# %% print("\n" + "=" * 80) print("数据质量检查(必须在预处理之前)") print("=" * 80) @@ -537,9 +389,9 @@ check_data_quality(test_data, feature_cols, raise_on_error=True) print("[成功] 数据质量检查通过,未发现异常") -#%% md +# %% md # ### 4.4 数据预处理 -#%% +# %% print("\n" + "=" * 80) print("数据预处理") print("=" * 80) @@ -563,9 +415,9 @@ if processors: print(f"\n处理后训练集形状: {train_data.shape}") print(f"处理后验证集形状: {val_data.shape}") print(f"处理后测试集形状: {test_data.shape}") -#%% md +# %% md # ### 4.4 训练 LambdaRank 模型 -#%% +# %% print("\n" + "=" * 80) print("训练 LambdaRank 模型") print("=" * 80) @@ -593,9 +445,9 @@ model.fit( eval_set=(X_val, y_val, val_group), ) print("训练完成!") -#%% md +# %% md # ### 4.5 训练指标曲线 -#%% +# %% print("\n" + "=" * 80) print("训练指标曲线") print("=" * 80) @@ -645,9 +497,9 @@ else: best_val = max(val_metric_list) print(f" {metric}: {best_val:.4f} (迭代 {best_iter_metric + 1})") print(f"\n[重要提醒] 验证集仅用于早停/调参,测试集完全独立于训练过程!") -#%% md +# %% md # ### 4.6 模型评估 -#%% +# %% print("\n" + "=" * 80) print("模型评估") print("=" * 80) @@ -685,7 +537,7 @@ if importance is not None: top_features = importance.sort_values(ascending=False).head(20) for i, (feature, score) in enumerate(top_features.items(), 1): print(f" {i:2d}. {feature:30s} {score:10.2f}") -#%% +# %% # 确保输出目录存在 os.makedirs(OUTPUT_DIR, exist_ok=True) @@ -731,7 +583,7 @@ print(f"\n 预览(前15行):") print(topn_to_save.head(15)) print("\n训练流程完成!") -#%% md +# %% md # ## 5. 总结 # # # 本 Notebook 实现了完整的 Learn-to-Rank 训练流程: @@ -764,4 +616,4 @@ print("\n训练流程完成!") # 2. **超参数调优**: 使用网格搜索优化 LambdaRank 参数 # 3. **模型集成**: 结合多个排序模型的预测 # 4. **更复杂的分组**: 考虑按行业分组排序 -# \ No newline at end of file +# diff --git a/src/experiment/regression.ipynb b/src/experiment/regression.ipynb index 64393cf..6ead512 100644 --- a/src/experiment/regression.ipynb +++ b/src/experiment/regression.ipynb @@ -15,7 +15,6 @@ "source": [ "import os\n", "from datetime import datetime\n", - "from typing import List\n", "\n", "import polars as pl\n", "\n", @@ -25,7 +24,6 @@ " LightGBMModel,\n", " STFilter,\n", " StandardScaler,\n", - " # StockFilterConfig, # 已删除,使用 StockPoolManager + filter_func 替代\n", " StockPoolManager,\n", " Trainer,\n", " Winsorizer,\n", @@ -33,87 +31,27 @@ " check_data_quality,\n", ")\n", "from src.training.config import TrainingConfig\n", - "\n" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": "## 2. 定义辅助函数" - }, - { - "metadata": {}, - "cell_type": "code", - "outputs": [], - "execution_count": null, - "source": [ - "def register_factors(\n", - " engine: FactorEngine,\n", - " selected_factors: List[str],\n", - " factor_definitions: dict,\n", - " label_factor: dict,\n", - ") -> List[str]:\n", - " \"\"\"注册因子(selected_factors 从 metadata 查询,factor_definitions 用 DSL 表达式注册)\"\"\"\n", - " print(\"=\" * 80)\n", - " print(\"注册因子\")\n", - " print(\"=\" * 80)\n", "\n", - " # 注册 SELECTED_FACTORS 中的因子(已在 metadata 中)\n", - " print(\"\\n注册特征因子(从 metadata):\")\n", - " for name in selected_factors:\n", - " engine.add_factor(name)\n", - " print(f\" - {name}\")\n", - "\n", - " # 注册 FACTOR_DEFINITIONS 中的因子(通过表达式,尚未在 metadata 中)\n", - " print(\"\\n注册特征因子(表达式):\")\n", - " for name, expr in factor_definitions.items():\n", - " engine.add_factor(name, expr)\n", - " print(f\" - {name}: {expr}\")\n", - "\n", - " # 注册 label 因子(通过表达式)\n", - " print(\"\\n注册 Label 因子(表达式):\")\n", - " for name, expr in label_factor.items():\n", - " engine.add_factor(name, expr)\n", - " print(f\" - {name}: {expr}\")\n", - "\n", - " # 特征列 = SELECTED_FACTORS + FACTOR_DEFINITIONS 的 keys\n", - " feature_cols = selected_factors + list(factor_definitions.keys())\n", - "\n", - " print(f\"\\n特征因子数: {len(feature_cols)}\")\n", - " print(f\" - 来自 metadata: {len(selected_factors)}\")\n", - " print(f\" - 来自表达式: {len(factor_definitions)}\")\n", - " print(f\"Label: {list(label_factor.keys())[0]}\")\n", - " print(f\"已注册因子总数: {len(engine.list_registered())}\")\n", - "\n", - " return feature_cols\n", - "\n", - "\n", - "def prepare_data(\n", - " engine: FactorEngine,\n", - " feature_cols: List[str],\n", - " start_date: str,\n", - " end_date: str,\n", - ") -> pl.DataFrame:\n", - " print(\"\\n\" + \"=\" * 80)\n", - " print(\"准备数据\")\n", - " print(\"=\" * 80)\n", - "\n", - " # 计算因子(全市场数据)\n", - " print(f\"\\n计算因子: {start_date} - {end_date}\")\n", - " factor_names = feature_cols + [LABEL_NAME] # 包含 label\n", - "\n", - " data = engine.compute(\n", - " factor_names=factor_names,\n", - " start_date=start_date,\n", - " end_date=end_date,\n", - " )\n", - "\n", - " print(f\"数据形状: {data.shape}\")\n", - " print(f\"数据列: {data.columns}\")\n", - " print(f\"\\n前5行预览:\")\n", - " print(data.head())\n", - "\n", - " return data\n", + "# 从 common 模块导入共用配置和函数\n", + "from src.experiment.common import (\n", + " SELECTED_FACTORS,\n", + " FACTOR_DEFINITIONS,\n", + " get_label_factor,\n", + " register_factors,\n", + " prepare_data,\n", + " TRAIN_START,\n", + " TRAIN_END,\n", + " VAL_START,\n", + " VAL_END,\n", + " TEST_START,\n", + " TEST_END,\n", + " stock_pool_filter,\n", + " STOCK_FILTER_REQUIRED_COLUMNS,\n", + " OUTPUT_DIR,\n", + " SAVE_PREDICTIONS,\n", + " PERSIST_MODEL,\n", + " TOP_N,\n", + ")\n", "\n" ] }, @@ -121,9 +59,9 @@ "metadata": {}, "cell_type": "markdown", "source": [ - "## 3. 配置参数\n", + "## 2. 配置参数\n", "#\n", - "### 3.1 因子定义" + "### 2.1 标签定义" ] }, { @@ -132,177 +70,11 @@ "outputs": [], "execution_count": null, "source": [ - "# 特征因子定义字典:新增因子只需在此处添加一行\n", + "# Label 名称(回归任务使用连续收益率)\n", "LABEL_NAME = \"future_return_5\"\n", "\n", - "# 当前选择的因子列表(从 FACTOR_DEFINITIONS 中选择要使用的因子)\n", - "SELECTED_FACTORS = [\n", - " # ================= 1. 价格、趋势与路径依赖 =================\n", - " \"ma_5\",\n", - " \"ma_20\",\n", - " \"ma_ratio_5_20\",\n", - " \"bias_10\",\n", - " \"high_low_ratio\",\n", - " \"bbi_ratio\",\n", - " \"return_5\",\n", - " \"return_20\",\n", - " \"kaufman_ER_20\",\n", - " \"mom_acceleration_10_20\",\n", - " \"drawdown_from_high_60\",\n", - " \"up_days_ratio_20\",\n", - " # ================= 2. 波动率、风险调整与高阶矩 =================\n", - " \"volatility_5\",\n", - " \"volatility_20\",\n", - " \"volatility_ratio\",\n", - " \"std_return_20\",\n", - " \"sharpe_ratio_20\",\n", - " \"min_ret_20\",\n", - " \"volatility_squeeze_5_60\",\n", - " # ================= 3. 日内微观结构与异象 =================\n", - " \"overnight_intraday_diff\",\n", - " \"upper_shadow_ratio\",\n", - " \"capital_retention_20\",\n", - " \"max_ret_20\",\n", - " # ================= 4. 量能、流动性与量价背离 =================\n", - " \"volume_ratio_5_20\",\n", - " \"turnover_rate_mean_5\",\n", - " \"turnover_deviation\",\n", - " \"amihud_illiq_20\",\n", - " \"turnover_cv_20\",\n", - " \"pv_corr_20\",\n", - " \"close_vwap_deviation\",\n", - " # ================= 5. 基本面财务特征 =================\n", - " \"roe\",\n", - " \"roa\",\n", - " \"profit_margin\",\n", - " \"debt_to_equity\",\n", - " \"current_ratio\",\n", - " \"net_profit_yoy\",\n", - " \"revenue_yoy\",\n", - " \"healthy_expansion_velocity\",\n", - " # ================= 6. 基本面估值与截面动量共振 =================\n", - " \"EP\",\n", - " \"BP\",\n", - " \"CP\",\n", - " \"market_cap_rank\",\n", - " \"turnover_rank\",\n", - " \"return_5_rank\",\n", - " \"EP_rank\",\n", - " \"pe_expansion_trend\",\n", - " \"value_price_divergence\",\n", - " \"active_market_cap\",\n", - " \"ebit_rank\",\n", - "]\n", - "\n", - "# 因子定义字典(完整因子库)\n", - "FACTOR_DEFINITIONS = {\n", - " # ================= 1. 价格、趋势与路径依赖 (Trend, Momentum & Path Dependency) =================\n", - " \"ma_5\": \"ts_mean(close, 5)\",\n", - " \"ma_20\": \"ts_mean(close, 20)\",\n", - " \"ma_ratio_5_20\": \"ts_mean(close, 5) / (ts_mean(close, 20) + 1e-8) - 1\", # 均线发散度\n", - " \"bias_10\": \"close / (ts_mean(close, 10) + 1e-8) - 1\", # 10日乖离率\n", - " \"high_low_ratio\": \"(close - ts_min(low, 20)) / (ts_max(high, 20) - ts_min(low, 20) + 1e-8)\", # 威廉指标变形\n", - " \"bbi_ratio\": \"(ts_mean(close, 3) + ts_mean(close, 6) + ts_mean(close, 12) + ts_mean(close, 24)) / (4 * close + 1e-8)\", # 多空指标比率\n", - " \"return_5\": \"(close / (ts_delay(close, 5) + 1e-8)) - 1\", # 5日动量\n", - " \"return_20\": \"(close / (ts_delay(close, 20) + 1e-8)) - 1\", # 20日动量\n", - " # [高阶] Kaufman 趋势效率 (极高价值) - 衡量趋势流畅度,剔除无序震荡\n", - " \"kaufman_ER_20\": \"abs(close - ts_delay(close, 20)) / (ts_sum(abs(close - ts_delay(close, 1)), 20) + 1e-8)\",\n", - " # [高阶] 动量加速度 - 寻找二阶导数大于0,正在加速爆发的股票\n", - " \"mom_acceleration_10_20\": \"(close / (ts_delay(close, 10) + 1e-8) - 1) - (ts_delay(close, 10) / (ts_delay(close, 20) + 1e-8) - 1)\",\n", - " # [高阶] 高点距离衰减 - 衡量套牢盘压力\n", - " \"drawdown_from_high_60\": \"close / (ts_max(high, 60) + 1e-8) - 1\",\n", - " # [高阶] 趋势一致性 - 过去20天内收红的天数比例\n", - " \"up_days_ratio_20\": \"ts_sum(close > ts_delay(close, 1), 20) / 20\",\n", - " # ================= 2. 波动率、风险调整与高阶矩 (Volatility & Risk-Adjusted Returns) =================\n", - " \"volatility_5\": \"ts_std(close, 5)\",\n", - " \"volatility_20\": \"ts_std(close, 20)\",\n", - " \"volatility_ratio\": \"ts_std(close, 5) / (ts_std(close, 20) + 1e-8)\", # 波动率期限结构\n", - " \"std_return_20\": \"ts_std((close / (ts_delay(close, 1) + 1e-8)) - 1, 20)\", # 真实收益率波动率\n", - " # [高阶] 夏普趋势比率 - 惩罚暴涨暴跌,奖励稳健爬坡\n", - " \"sharpe_ratio_20\": \"ts_mean(close / (ts_delay(close, 1) + 1e-8) - 1, 20) / (ts_std(close / (ts_delay(close, 1) + 1e-8) - 1, 20) + 1e-8)\",\n", - " # [高阶] 尾部崩盘风险 - 过去一个月最大单日跌幅\n", - " \"min_ret_20\": \"ts_min(close / (ts_delay(close, 1) + 1e-8) - 1, 20)\",\n", - " # [高阶] 波动率挤压比 - 寻找盘整到极致面临变盘的股票 (布林带收口)\n", - " \"volatility_squeeze_5_60\": \"ts_std(close, 5) / (ts_std(close, 60) + 1e-8)\",\n", - " # ================= 3. 日内微观结构与异象 (Intraday Microstructure & Anomalies) =================\n", - " # [高阶] 隔夜与日内背离 - 差值越小说明主力越喜欢在盘中吸筹\n", - " \"overnight_intraday_diff\": \"(open / (ts_delay(close, 1) + 1e-8) - 1) - (close / (open + 1e-8) - 1)\",\n", - " # [高阶] 上影线抛压极值 - 冲高回落被套牢的概率\n", - " \"upper_shadow_ratio\": \"(high - ((open + close + abs(open - close)) / 2)) / (high - low + 1e-8)\",\n", - " # [高阶] 资金沉淀率 - 衡量主力日内高抛低吸洗盘的剧烈程度\n", - " \"capital_retention_20\": \"ts_sum(abs(close - open), 20) / (ts_sum(high - low, 20) + 1e-8)\",\n", - " # [高阶] MAX 彩票效应 - 反转因子,剔除近期有过妖股连板特征的标的\n", - " \"max_ret_20\": \"ts_max(close / (ts_delay(close, 1) + 1e-8) - 1, 20)\",\n", - " # ================= 4. 量能、流动性与量价背离 (Volume, Liquidity & Divergence) =================\n", - " \"volume_ratio_5_20\": \"ts_mean(vol, 5) / (ts_mean(vol, 20) + 1e-8)\", # 相对放量比\n", - " \"turnover_rate_mean_5\": \"ts_mean(turnover_rate, 5)\", # 活跃度\n", - " \"turnover_deviation\": \"(turnover_rate - ts_mean(turnover_rate, 10)) / (ts_std(turnover_rate, 10) + 1e-8)\", # 换手率偏离度\n", - " # [高阶] Amihud 非流动性异象 (绝对核心) - 衡量砸盘/拉升的摩擦成本\n", - " \"amihud_illiq_20\": \"ts_mean(abs(close / (ts_delay(close, 1) + 1e-8) - 1) / (amount + 1e-8), 20)\",\n", - " # [高阶] 换手率惩罚因子 - 换手率忽高忽低说明游资接力,行情极不稳定\n", - " \"turnover_cv_20\": \"ts_std(turnover_rate, 20) / (ts_mean(turnover_rate, 20) + 1e-8)\",\n", - " # [高阶] 纯粹量价相关性 - 检验是否是\"放量上涨,缩量下跌\"的良性多头\n", - " \"pv_corr_20\": \"ts_corr(close / (ts_delay(close, 1) + 1e-8) - 1, vol, 20)\",\n", - " # [高阶] 收盘价与均价背离 - 专门抓尾盘突袭拉升骗线的股票\n", - " \"close_vwap_deviation\": \"close / (amount / (vol * 100 + 1e-8) + 1e-8) - 1\",\n", - " # ================= 5. 基本面财务特征 (Fundamental Quality & Structure) =================\n", - " \"roe\": \"n_income / (total_hldr_eqy_exc_min_int + 1e-8)\", # 净资产收益率\n", - " \"roa\": \"n_income / (total_assets + 1e-8)\", # 总资产收益率\n", - " \"profit_margin\": \"n_income / (revenue + 1e-8)\", # 销售净利率\n", - " \"debt_to_equity\": \"total_liab / (total_hldr_eqy_exc_min_int + 1e-8)\", # 杠杆率\n", - " \"current_ratio\": \"total_cur_assets / (total_cur_liab + 1e-8)\", # 短期偿债安全垫\n", - " # [高阶] 利润同比增速 (日频延后252天等于去年同期)\n", - " \"net_profit_yoy\": \"(n_income / (ts_delay(n_income, 252) + 1e-8)) - 1\",\n", - " # [高阶] 营收同比增速\n", - " \"revenue_yoy\": \"(revenue / (ts_delay(revenue, 252) + 1e-8)) - 1\",\n", - " # [高阶] 资产负债表扩张斜率 - 剔除单纯靠举债扩张的公司\n", - " \"healthy_expansion_velocity\": \"(total_assets / (ts_delay(total_assets, 252) + 1e-8) - 1) - (total_liab / (ts_delay(total_liab, 252) + 1e-8) - 1)\",\n", - " # ================= 6. 基本面估值与截面动量共振 (Valuation & Cross-Sectional Ranking) =================\n", - " # 估值水平绝对值 (Tushare 市值单位需要 * 10000 转换为元)\n", - " \"EP\": \"n_income / (total_mv * 10000 + 1e-8)\", # 盈利收益率 (1/PE)\n", - " \"BP\": \"total_hldr_eqy_exc_min_int / (total_mv * 10000 + 1e-8)\", # 账面市值比 (1/PB)\n", - " \"CP\": \"n_cashflow_act / (total_mv * 10000 + 1e-8)\", # 经营现金流收益率 (1/PCF)\n", - " # 全市场截面排名因子\n", - " \"market_cap_rank\": \"cs_rank(total_mv)\", # 规模因子 (Size)\n", - " \"turnover_rank\": \"cs_rank(turnover_rate)\",\n", - " \"return_5_rank\": \"cs_rank((close / (ts_delay(close, 5) + 1e-8)) - 1)\",\n", - " \"EP_rank\": \"cs_rank(n_income / (total_mv + 1e-8))\", # 谁最便宜\n", - " # [高阶] 戴维斯双击动量 - 估值相对上一年是否在扩张\n", - " \"pe_expansion_trend\": \"(total_mv / (n_income + 1e-8)) / (ts_delay(total_mv, 60) / (ts_delay(n_income, 60) + 1e-8) + 1e-8) - 1\",\n", - " # [高阶] 业绩与价格背离度 - 截面做差:利润排名全市场第一,但近20日价格排名倒数第一,捕捉被错杀的潜伏股\n", - " \"value_price_divergence\": \"cs_rank((n_income - ts_delay(n_income, 252)) / (abs(ts_delay(n_income, 252)) + 1e-8)) - cs_rank(close / (ts_delay(close, 20) + 1e-8))\",\n", - " # [高阶] 流动性溢价调整后市值 - 识别僵尸大盘股和极度活跃的小微盘\n", - " \"active_market_cap\": \"total_mv * ts_mean(turnover_rate, 20)\",\n", - " \"ebit_rank\": \"cs_rank(ebit)\",\n", - "}\n", - "\n", - "# Label 因子定义(不参与训练,用于计算目标)\n", - "LABEL_FACTOR = {\n", - " LABEL_NAME: \"(ts_delay(close, -5) / ts_delay(open, -1)) - 1\", # 未来5日收益率\n", - "}" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": "### 3.2 训练参数配置" - }, - { - "metadata": {}, - "cell_type": "code", - "outputs": [], - "execution_count": null, - "source": [ - "# 日期范围配置(正确的 train/val/test 三分法)\n", - "# Train: 用于训练模型参数\n", - "# Val: 用于验证/早停/调参(位于 train 之后,test 之前)\n", - "# Test: 仅用于最终评估,完全独立于训练过程\n", - "TRAIN_START = \"20200101\"\n", - "TRAIN_END = \"20231231\"\n", - "VAL_START = \"20240101\"\n", - "VAL_END = \"20241231\"\n", - "TEST_START = \"20250101\"\n", - "TEST_END = \"20261231\"\n", + "# 获取 Label 因子定义\n", + "LABEL_FACTOR = get_label_factor(LABEL_NAME)\n", "\n", "# 模型参数配置\n", "MODEL_PARAMS = {\n", @@ -326,60 +98,7 @@ " # 数值稳定性\n", " \"verbose\": -1,\n", " \"random_state\": 42,\n", - "}\n", - "\n", - "\n", - "# 股票池筛选函数\n", - "# 使用新的 StockPoolManager API:传入自定义筛选函数和所需列/因子\n", - "# 筛选函数接收单日 DataFrame,返回布尔 Series\n", - "#\n", - "# 筛选逻辑(针对单日数据):\n", - "# 1. 先排除创业板、科创板、北交所(ST过滤由STFilter组件处理)\n", - "# 2. 然后选取市值最小的500只股票\n", - "def stock_pool_filter(df: pl.DataFrame) -> pl.Series:\n", - " \"\"\"股票池筛选函数(单日数据)\n", - "\n", - " 筛选条件:\n", - " 1. 排除创业板(代码以 300 开头)\n", - " 2. 排除科创板(代码以 688 开头)\n", - " 3. 排除北交所(代码以 8、9 或 4 开头)\n", - " 4. 选取当日市值最小的500只股票\n", - " \"\"\"\n", - " # 代码筛选(排除创业板、科创板、北交所)\n", - " code_filter = (\n", - " ~df[\"ts_code\"].str.starts_with(\"30\") # 排除创业板\n", - " & ~df[\"ts_code\"].str.starts_with(\"68\") # 排除科创板\n", - " & ~df[\"ts_code\"].str.starts_with(\"8\") # 排除北交所\n", - " & ~df[\"ts_code\"].str.starts_with(\"9\") # 排除北交所\n", - " & ~df[\"ts_code\"].str.starts_with(\"4\") # 排除北交所\n", - " )\n", - "\n", - " # 在已筛选的股票中,选取市值最小的500只\n", - " # 按市值升序排序,取前500\n", - " valid_df = df.filter(code_filter)\n", - " n = min(1000, len(valid_df))\n", - " small_cap_codes = valid_df.sort(\"total_mv\").head(n)[\"ts_code\"]\n", - "\n", - " # 返回布尔 Series:是否在被选中的股票中\n", - " return df[\"ts_code\"].is_in(small_cap_codes)\n", - "\n", - "\n", - "# 定义筛选所需的基础列\n", - "STOCK_FILTER_REQUIRED_COLUMNS = [\"total_mv\"] # ST过滤由STFilter组件处理\n", - "\n", - "# 可选:定义筛选所需的因子(如果需要用因子进行筛选)\n", - "# STOCK_FILTER_REQUIRED_FACTORS = {\n", - "# \"market_cap_rank\": \"cs_rank(total_mv)\",\n", - "# }\n", - "\n", - "\n", - "# 输出配置(相对于本文件所在目录)\n", - "OUTPUT_DIR = \"output\"\n", - "SAVE_PREDICTIONS = True\n", - "PERSIST_MODEL = False\n", - "\n", - "# Top N 配置:每日推荐股票数量\n", - "TOP_N = 5 # 可调整为 10, 20 等" + "}" ] }, { @@ -420,6 +139,7 @@ " feature_cols=feature_cols,\n", " start_date=TRAIN_START,\n", " end_date=TEST_END,\n", + " label_name=LABEL_NAME,\n", ")\n", "\n", "# 4. 打印配置信息\n", @@ -515,8 +235,6 @@ { "metadata": {}, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "# 步骤 2: 划分训练/验证/测试集(正确的三分法)\n", "print(\"\\n[步骤 2/6] 划分训练集、验证集和测试集\")\n", @@ -550,7 +268,9 @@ " train_data = filtered_data\n", " test_data = filtered_data\n", " print(\" 未配置划分器,全部作为训练集\")" - ] + ], + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -579,8 +299,6 @@ { "metadata": {}, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "# 步骤 4: 训练集数据处理\n", "print(\"\\n[步骤 4/7] 训练集数据处理\")\n", @@ -608,7 +326,9 @@ " null_count = train_data[col].null_count()\n", " if null_count > 0:\n", " print(f\" {col}: {null_count} ({null_count / len(train_data) * 100:.2f}%)\")" - ] + ], + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -828,8 +548,6 @@ { "metadata": {}, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ "print(\"\\n\" + \"=\" * 80)\n", "print(\"训练结果\")\n", @@ -855,7 +573,9 @@ "sample_data = results.filter(results[\"trade_date\"] == sample_date).head(10)\n", "print(f\"\\n示例日期 {sample_date} 的前10条预测:\")\n", "print(sample_data.select([\"ts_code\", \"trade_date\", target_col, \"prediction\"]))" - ] + ], + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -978,6 +698,61 @@ "- 可以帮助理解哪些因子最有效" ] }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "print(\"绘制特征重要性...\")\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 8))\n", + "lgb.plot_importance(\n", + " booster,\n", + " max_num_features=20,\n", + " importance_type=\"gain\",\n", + " title=\"Feature Importance (Gain)\",\n", + " ax=ax,\n", + ")\n", + "ax.set_xlabel(\"Importance (Gain)\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# 打印重要性排名\n", + "importance_gain = pd.Series(\n", + " booster.feature_importance(importance_type=\"gain\"), index=feature_cols\n", + ").sort_values(ascending=False)\n", + "\n", + "print(\"\\n[特征重要性排名 - Gain]\")\n", + "print(importance_gain)\n", + "\n", + "# 识别低重要性特征\n", + "zero_importance = importance_gain[importance_gain == 0].index.tolist()\n", + "if zero_importance:\n", + " print(f\"\\n[低重要性特征] 以下{len(zero_importance)}个特征重要性为0,可考虑删除:\")\n", + " for feat in zero_importance:\n", + " print(f\" - {feat}\")\n", + "else:\n", + " print(\"\\n所有特征都有一定重要性\")\n" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "# 导入可视化库\n", + "import matplotlib.pyplot as plt\n", + "import lightgbm as lgb\n", + "import pandas as pd\n", + "\n", + "# 从封装的model中取出底层Booster\n", + "booster = model.model\n", + "print(f\"模型类型: {type(booster)}\")\n", + "print(f\"特征数量: {len(feature_cols)}\")" + ] + }, { "metadata": {}, "cell_type": "code", diff --git a/src/experiment/regression.py b/src/experiment/regression.py index 60a20db..d593cd6 100644 --- a/src/experiment/regression.py +++ b/src/experiment/regression.py @@ -3,7 +3,6 @@ # %% import os from datetime import datetime -from typing import List import polars as pl @@ -13,7 +12,6 @@ from src.training import ( LightGBMModel, STFilter, StandardScaler, - # StockFilterConfig, # 已删除,使用 StockPoolManager + filter_func 替代 StockPoolManager, Trainer, Winsorizer, @@ -22,167 +20,38 @@ from src.training import ( ) from src.training.config import TrainingConfig - -# %% md -# ## 2. 定义辅助函数 -# %% -def register_factors( - engine: FactorEngine, - selected_factors: List[str], - factor_definitions: dict, - label_factor: dict, -) -> List[str]: - """注册因子(selected_factors 从 metadata 查询,factor_definitions 用 DSL 表达式注册)""" - print("=" * 80) - print("注册因子") - print("=" * 80) - - # 注册 SELECTED_FACTORS 中的因子(已在 metadata 中) - print("\n注册特征因子(从 metadata):") - for name in selected_factors: - engine.add_factor(name) - print(f" - {name}") - - # 注册 FACTOR_DEFINITIONS 中的因子(通过表达式,尚未在 metadata 中) - print("\n注册特征因子(表达式):") - for name, expr in factor_definitions.items(): - engine.add_factor(name, expr) - print(f" - {name}: {expr}") - - # 注册 label 因子(通过表达式) - print("\n注册 Label 因子(表达式):") - for name, expr in label_factor.items(): - engine.add_factor(name, expr) - print(f" - {name}: {expr}") - - # 特征列 = SELECTED_FACTORS + FACTOR_DEFINITIONS 的 keys - feature_cols = selected_factors + list(factor_definitions.keys()) - - print(f"\n特征因子数: {len(feature_cols)}") - print(f" - 来自 metadata: {len(selected_factors)}") - print(f" - 来自表达式: {len(factor_definitions)}") - print(f"Label: {list(label_factor.keys())[0]}") - print(f"已注册因子总数: {len(engine.list_registered())}") - - return feature_cols - - -def prepare_data( - engine: FactorEngine, - feature_cols: List[str], - start_date: str, - end_date: str, -) -> pl.DataFrame: - print("\n" + "=" * 80) - print("准备数据") - print("=" * 80) - - # 计算因子(全市场数据) - print(f"\n计算因子: {start_date} - {end_date}") - factor_names = feature_cols + [LABEL_NAME] # 包含 label - - data = engine.compute( - factor_names=factor_names, - start_date=start_date, - end_date=end_date, - ) - - print(f"数据形状: {data.shape}") - print(f"数据列: {data.columns}") - print(f"\n前5行预览:") - print(data.head()) - - return data +# 从 common 模块导入共用配置和函数 +from src.experiment.common import ( + SELECTED_FACTORS, + FACTOR_DEFINITIONS, + get_label_factor, + register_factors, + prepare_data, + TRAIN_START, + TRAIN_END, + VAL_START, + VAL_END, + TEST_START, + TEST_END, + stock_pool_filter, + STOCK_FILTER_REQUIRED_COLUMNS, + OUTPUT_DIR, + SAVE_PREDICTIONS, + PERSIST_MODEL, + TOP_N, +) # %% md -# ## 3. 配置参数 +# ## 2. 配置参数 # -# ### 3.1 因子定义 +# ### 2.1 标签定义 # %% -# 特征因子定义字典:新增因子只需在此处添加一行 +# Label 名称(回归任务使用连续收益率) LABEL_NAME = "future_return_5" -# 当前选择的因子列表(从 FACTOR_DEFINITIONS 中选择要使用的因子) -SELECTED_FACTORS = [ - # ================= 1. 价格、趋势与路径依赖 ================= - "ma_5", - "ma_20", - "ma_ratio_5_20", - "bias_10", - "high_low_ratio", - "bbi_ratio", - "return_5", - "return_20", - "kaufman_ER_20", - "mom_acceleration_10_20", - "drawdown_from_high_60", - "up_days_ratio_20", - # ================= 2. 波动率、风险调整与高阶矩 ================= - "volatility_5", - "volatility_20", - "volatility_ratio", - "std_return_20", - "sharpe_ratio_20", - "min_ret_20", - "volatility_squeeze_5_60", - # ================= 3. 日内微观结构与异象 ================= - "overnight_intraday_diff", - "upper_shadow_ratio", - "capital_retention_20", - "max_ret_20", - # ================= 4. 量能、流动性与量价背离 ================= - "volume_ratio_5_20", - "turnover_rate_mean_5", - "turnover_deviation", - "amihud_illiq_20", - "turnover_cv_20", - "pv_corr_20", - "close_vwap_deviation", - # ================= 5. 基本面财务特征 ================= - "roe", - "roa", - "profit_margin", - "debt_to_equity", - "current_ratio", - "net_profit_yoy", - "revenue_yoy", - "healthy_expansion_velocity", - # ================= 6. 基本面估值与截面动量共振 ================= - "EP", - "BP", - "CP", - "market_cap_rank", - "turnover_rank", - "return_5_rank", - "EP_rank", - "pe_expansion_trend", - "value_price_divergence", - "active_market_cap", - "ebit_rank", -] - -# 因子定义字典(完整因子库) -FACTOR_DEFINITIONS = { -} - -# Label 因子定义(不参与训练,用于计算目标) -LABEL_FACTOR = { - LABEL_NAME: "(ts_delay(close, -5) / ts_delay(open, -1)) - 1", # 未来5日收益率 -} -# %% md -# ### 3.2 训练参数配置 -# %% -# 日期范围配置(正确的 train/val/test 三分法) -# Train: 用于训练模型参数 -# Val: 用于验证/早停/调参(位于 train 之后,test 之前) -# Test: 仅用于最终评估,完全独立于训练过程 -TRAIN_START = "20200101" -TRAIN_END = "20231231" -VAL_START = "20240101" -VAL_END = "20241231" -TEST_START = "20250101" -TEST_END = "20261231" +# 获取 Label 因子定义 +LABEL_FACTOR = get_label_factor(LABEL_NAME) # 模型参数配置 MODEL_PARAMS = { @@ -207,59 +76,6 @@ MODEL_PARAMS = { "verbose": -1, "random_state": 42, } - - -# 股票池筛选函数 -# 使用新的 StockPoolManager API:传入自定义筛选函数和所需列/因子 -# 筛选函数接收单日 DataFrame,返回布尔 Series -# -# 筛选逻辑(针对单日数据): -# 1. 先排除创业板、科创板、北交所(ST过滤由STFilter组件处理) -# 2. 然后选取市值最小的500只股票 -def stock_pool_filter(df: pl.DataFrame) -> pl.Series: - """股票池筛选函数(单日数据) - - 筛选条件: - 1. 排除创业板(代码以 300 开头) - 2. 排除科创板(代码以 688 开头) - 3. 排除北交所(代码以 8、9 或 4 开头) - 4. 选取当日市值最小的500只股票 - """ - # 代码筛选(排除创业板、科创板、北交所) - code_filter = ( - ~df["ts_code"].str.starts_with("30") # 排除创业板 - & ~df["ts_code"].str.starts_with("68") # 排除科创板 - & ~df["ts_code"].str.starts_with("8") # 排除北交所 - & ~df["ts_code"].str.starts_with("9") # 排除北交所 - & ~df["ts_code"].str.starts_with("4") # 排除北交所 - ) - - # 在已筛选的股票中,选取市值最小的500只 - # 按市值升序排序,取前500 - valid_df = df.filter(code_filter) - n = min(1000, len(valid_df)) - small_cap_codes = valid_df.sort("total_mv").head(n)["ts_code"] - - # 返回布尔 Series:是否在被选中的股票中 - return df["ts_code"].is_in(small_cap_codes) - - -# 定义筛选所需的基础列 -STOCK_FILTER_REQUIRED_COLUMNS = ["total_mv"] # ST过滤由STFilter组件处理 - -# 可选:定义筛选所需的因子(如果需要用因子进行筛选) -# STOCK_FILTER_REQUIRED_FACTORS = { -# "market_cap_rank": "cs_rank(total_mv)", -# } - - -# 输出配置(相对于本文件所在目录) -OUTPUT_DIR = "output" -SAVE_PREDICTIONS = True -PERSIST_MODEL = False - -# Top N 配置:每日推荐股票数量 -TOP_N = 5 # 可调整为 10, 20 等 # %% md # ## 4. 训练流程 # @@ -288,6 +104,7 @@ data = prepare_data( feature_cols=feature_cols, start_date=TRAIN_START, end_date=TEST_END, + label_name=LABEL_NAME, ) # 4. 打印配置信息