Files
NewStock/main/train/Rank2.ipynb
2025-06-04 13:50:02 +08:00

2245 lines
247 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "code",
"execution_count": 9,
"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": [
"The autoreload extension is already loaded. To reload it, use:\n",
" %reload_ext autoreload\n",
"/mnt/d/PyProject/NewStock\n"
]
}
],
"source": [
"%load_ext autoreload\n",
"%autoreload 2\n",
"\n",
"import gc\n",
"import os\n",
"import sys\n",
"sys.path.append('/mnt/d/PyProject/NewStock/')\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\")"
]
},
{
"cell_type": "code",
"execution_count": 10,
"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",
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 8692146 entries, 0 to 8692145\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 pct_chg float64 \n",
" 8 amount 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"
]
}
],
"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', 'pct_chg', 'amount'],\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": 11,
"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"
]
}
],
"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": 12,
"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": [],
"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": 13,
"id": "a735bc02ceb4d872",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T12:47:10.821169Z",
"start_time": "2025-04-03T12:47:10.751831Z"
}
},
"outputs": [],
"source": [
"import talib\n",
"import numpy as np"
]
},
{
"cell_type": "code",
"execution_count": 14,
"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": [],
"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": 15,
"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"
]
}
],
"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": 16,
"id": "85c3e3d0235ffffa",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T12:47:16.089879Z",
"start_time": "2025-04-03T12:47:15.990101Z"
}
},
"outputs": [],
"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'],\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'],\n",
" df=None)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"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": [
"正在计算股东增减持因子(优化版)...\n"
]
},
{
"ename": "KeyboardInterrupt",
"evalue": "",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mKeyboardInterrupt\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[18]\u001b[39m\u001b[32m, line 30\u001b[39m\n\u001b[32m 23\u001b[39m df = df.sort_values(by=[\u001b[33m\"\u001b[39m\u001b[33mts_code\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mtrade_date\u001b[39m\u001b[33m\"\u001b[39m])\n\u001b[32m 25\u001b[39m \u001b[38;5;66;03m# df = price_minus_deduction_price(df, n=120)\u001b[39;00m\n\u001b[32m 26\u001b[39m \u001b[38;5;66;03m# df = price_deduction_price_diff_ratio_to_sma(df, n=120)\u001b[39;00m\n\u001b[32m 27\u001b[39m \u001b[38;5;66;03m# df = cat_price_vs_sma_vs_deduction_price(df, n=120)\u001b[39;00m\n\u001b[32m 28\u001b[39m \u001b[38;5;66;03m# df = cat_reason(df, top_list_df)\u001b[39;00m\n\u001b[32m 29\u001b[39m \u001b[38;5;66;03m# df = cat_is_on_top_list(df, top_list_df)\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m30\u001b[39m df = \u001b[43mholder_trade_factors\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstk_holdertrade_df\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 32\u001b[39m df = cat_senti_mom_vol_spike(\n\u001b[32m 33\u001b[39m df,\n\u001b[32m 34\u001b[39m return_period=\u001b[32m3\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 38\u001b[39m current_pct_chg_max=\u001b[32m0.05\u001b[39m,\n\u001b[32m 39\u001b[39m ) \u001b[38;5;66;03m# 当日涨幅不宜过大\u001b[39;00m\n\u001b[32m 41\u001b[39m df = cat_senti_pre_breakout(\n\u001b[32m 42\u001b[39m df,\n\u001b[32m 43\u001b[39m atr_short_N=\u001b[32m10\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 51\u001b[39m volume_ratio_signal_threshold=\u001b[32m1.1\u001b[39m,\n\u001b[32m 52\u001b[39m )\n",
"\u001b[36mFile \u001b[39m\u001b[32m/mnt/d/PyProject/NewStock/main/factor/money_factor.py:50\u001b[39m, in \u001b[36mholder_trade_factors\u001b[39m\u001b[34m(all_data_df, stk_holdertrade_df)\u001b[39m\n\u001b[32m 43\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m date_in_window \u001b[38;5;129;01min\u001b[39;00m future_dates:\n\u001b[32m 44\u001b[39m \u001b[38;5;66;03m# 只有当日期是实际交易日时才添加\u001b[39;00m\n\u001b[32m 45\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m date_in_window \u001b[38;5;129;01min\u001b[39;00m all_trade_dates_set:\n\u001b[32m 46\u001b[39m expanded_holder_events.append({\n\u001b[32m 47\u001b[39m \u001b[33m'\u001b[39m\u001b[33mts_code\u001b[39m\u001b[33m'\u001b[39m: ts_code,\n\u001b[32m 48\u001b[39m \u001b[33m'\u001b[39m\u001b[33mtrade_date\u001b[39m\u001b[33m'\u001b[39m: date_in_window,\n\u001b[32m 49\u001b[39m \u001b[33m'\u001b[39m\u001b[33min_de_numeric\u001b[39m\u001b[33m'\u001b[39m: row[\u001b[33m'\u001b[39m\u001b[33min_de_numeric\u001b[39m\u001b[33m'\u001b[39m],\n\u001b[32m---> \u001b[39m\u001b[32m50\u001b[39m \u001b[33m'\u001b[39m\u001b[33mchange_ratio_total_agg\u001b[39m\u001b[33m'\u001b[39m: \u001b[43mrow\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mchange_ratio_total_agg\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m,\n\u001b[32m 51\u001b[39m \u001b[33m'\u001b[39m\u001b[33mchange_ratio_in_agg\u001b[39m\u001b[33m'\u001b[39m: row[\u001b[33m'\u001b[39m\u001b[33mchange_ratio_in_agg\u001b[39m\u001b[33m'\u001b[39m],\n\u001b[32m 52\u001b[39m \u001b[33m'\u001b[39m\u001b[33mchange_ratio_de_agg\u001b[39m\u001b[33m'\u001b[39m: row[\u001b[33m'\u001b[39m\u001b[33mchange_ratio_de_agg\u001b[39m\u001b[33m'\u001b[39m]\n\u001b[32m 53\u001b[39m })\n\u001b[32m 55\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m expanded_holder_events: \u001b[38;5;66;03m# 如果没有事件,直接返回原始 df\u001b[39;00m\n\u001b[32m 56\u001b[39m \u001b[38;5;66;03m# 确保返回的DataFrame与原始df具有相同的列和顺序\u001b[39;00m\n\u001b[32m 57\u001b[39m \u001b[38;5;66;03m# 并填充为默认值\u001b[39;00m\n\u001b[32m 58\u001b[39m default_factors = pd.DataFrame({\n\u001b[32m 59\u001b[39m \u001b[33m'\u001b[39m\u001b[33mholder_trade_type_10d\u001b[39m\u001b[33m'\u001b[39m: \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 60\u001b[39m \u001b[33m'\u001b[39m\u001b[33mholder_change_ratio_sum_10d\u001b[39m\u001b[33m'\u001b[39m: \u001b[32m0.0\u001b[39m,\n\u001b[32m 61\u001b[39m \u001b[33m'\u001b[39m\u001b[33mholder_in_change_ratio_sum_10d\u001b[39m\u001b[33m'\u001b[39m: \u001b[32m0.0\u001b[39m,\n\u001b[32m 62\u001b[39m \u001b[33m'\u001b[39m\u001b[33mholder_de_change_ratio_sum_10d\u001b[39m\u001b[33m'\u001b[39m: \u001b[32m0.0\u001b[39m\n\u001b[32m 63\u001b[39m }, index=all_data_df.index)\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/stock/lib/python3.13/site-packages/pandas/core/series.py:1121\u001b[39m, in \u001b[36mSeries.__getitem__\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 1118\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._values[key]\n\u001b[32m 1120\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m key_is_scalar:\n\u001b[32m-> \u001b[39m\u001b[32m1121\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_get_value\u001b[49m\u001b[43m(\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1123\u001b[39m \u001b[38;5;66;03m# Convert generator to list before going through hashable part\u001b[39;00m\n\u001b[32m 1124\u001b[39m \u001b[38;5;66;03m# (We will iterate through the generator there to check for slices)\u001b[39;00m\n\u001b[32m 1125\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m is_iterator(key):\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/stock/lib/python3.13/site-packages/pandas/core/series.py:1239\u001b[39m, in \u001b[36mSeries._get_value\u001b[39m\u001b[34m(self, label, takeable)\u001b[39m\n\u001b[32m 1236\u001b[39m \u001b[38;5;66;03m# Similar to Index.get_value, but we do not fall back to positional\u001b[39;00m\n\u001b[32m 1237\u001b[39m loc = \u001b[38;5;28mself\u001b[39m.index.get_loc(label)\n\u001b[32m-> \u001b[39m\u001b[32m1239\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[43mis_integer\u001b[49m\u001b[43m(\u001b[49m\u001b[43mloc\u001b[49m\u001b[43m)\u001b[49m:\n\u001b[32m 1240\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._values[loc]\n\u001b[32m 1242\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m.index, MultiIndex):\n",
"\u001b[31mKeyboardInterrupt\u001b[39m: "
]
}
],
"source": [
"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[\"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",
"\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",
"df = holder_trade_factors(df, stk_holdertrade_df)\n",
"\n",
"df = cat_senti_mom_vol_spike(\n",
" df,\n",
" return_period=3,\n",
" return_threshold=0.03, # 近3日涨幅超3%\n",
" volume_ratio_threshold=1.3,\n",
" current_pct_chg_min=0.0, # 当日必须收红\n",
" current_pct_chg_max=0.05,\n",
") # 当日涨幅不宜过大\n",
"\n",
"df = cat_senti_pre_breakout(\n",
" df,\n",
" atr_short_N=10,\n",
" atr_long_M=40,\n",
" vol_atrophy_N=10,\n",
" vol_atrophy_M=40,\n",
" price_stab_N=5,\n",
" price_stab_threshold=0.06,\n",
" current_pct_chg_min_signal=0.002,\n",
" current_pct_chg_max_signal=0.05,\n",
" volume_ratio_signal_threshold=1.1,\n",
")\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 = ts_ff_to_total_turnover_ratio(df)\n",
"df = ts_price_volume_trend_coherence_5_20(df)\n",
"# df = ts_turnover_rate_trend_strength_5(df)\n",
"df = ts_ff_turnover_rate_surge_10(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",
"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",
"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 = calculate_strong_inflow_signal(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": 10,
"id": "b87b938028afa206",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T13:08:03.658725Z",
"start_time": "2025-04-03T13:08:02.469611Z"
}
},
"outputs": [],
"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": 11,
"id": "f4f16d63ad18d1bc",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T13:08:03.670700Z",
"start_time": "2025-04-03T13:08:03.665739Z"
}
},
"outputs": [],
"source": [
"import pandas as pd\n",
"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",
"def cs_neutralize_industry_cap(df: pd.DataFrame,\n",
" features: list,\n",
" industry_col: str = 'cat_l2_code',\n",
" market_cap_col: str = 'circ_mv'):\n",
" \"\"\"\n",
" 对指定特征列进行截面行业和对数市值中性化 (原地修改)。\n",
" 使用 OLS 回归: feature ~ 1 + log(market_cap) + C(industry)\n",
" 将回归残差写回原特征列。\n",
"\n",
" Args:\n",
" df (pd.DataFrame): 输入 DataFrame需包含 'trade_date', features 列,\n",
" industry_col, market_cap_col。\n",
" features (list): 需要处理的特征列名列表。\n",
" industry_col (str): 行业分类列名。\n",
" market_cap_col (str): 流通市值列名。\n",
"\n",
" WARNING: 此函数会原地修改输入的 DataFrame 'df' 的 features 列。\n",
" 计算量较大,可能耗时较长。\n",
" 需要安装 statsmodels 库 (pip install statsmodels)。\n",
" \"\"\"\n",
" print(\"开始截面行业市值中性化...\")\n",
" required_cols = features + ['trade_date', industry_col, 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",
" # 预处理:计算 log 市值,处理 industry code 可能的 NaN\n",
" log_cap_col = '_log_market_cap'\n",
" df[log_cap_col] = np.log1p(df[market_cap_col]) # log1p 处理 0 值\n",
" # df[industry_col] = df[industry_col].cat.add_categories('UnknownIndustry')\n",
" # df[industry_col] = df[industry_col].fillna('UnknownIndustry') # 填充行业 NaN\n",
" # df[industry_col] = df[industry_col].astype('category') # 转为类别ols 会自动处理\n",
"\n",
" dates = df['trade_date'].unique()\n",
" all_residuals = [] # 用于收集所有日期的残差\n",
"\n",
" for date in tqdm(dates, desc=\"Neutralizing\"):\n",
" daily_data = df.loc[df['trade_date'] == date, features + [log_cap_col, industry_col]].copy() # 使用 .loc 获取副本\n",
"\n",
" # 准备自变量 X (常数项 + log市值 + 行业哑变量)\n",
" X = daily_data[[log_cap_col]]\n",
" X = sm.add_constant(X, prepend=True) # 添加常数项\n",
" # 创建行业哑变量 (drop_first=True 避免共线性)\n",
" industry_dummies = pd.get_dummies(daily_data[industry_col], prefix=industry_col, drop_first=True)\n",
" industry_dummies = industry_dummies.astype(int)\n",
" X = pd.concat([X, industry_dummies], axis=1)\n",
"\n",
" daily_residuals = daily_data[[col for col in features]].copy() # 创建用于存储残差的df\n",
"\n",
" for col in features:\n",
" Y = daily_data[col]\n",
"\n",
" # 处理 NaN 值,确保 X 和 Y 在相同位置有有效值\n",
" valid_mask = Y.notna() & X.notna().all(axis=1)\n",
" if valid_mask.sum() < (X.shape[1] + 1): # 数据点不足以估计模型\n",
" print(f\"警告: 日期 {date}, 特征 {col} 有效数据不足 ({valid_mask.sum()}个),无法中性化,填充 NaN。\")\n",
" daily_residuals[col] = np.nan\n",
" continue\n",
"\n",
" Y_valid = Y[valid_mask]\n",
" X_valid = X[valid_mask]\n",
"\n",
" # 执行 OLS 回归\n",
" try:\n",
" model = sm.OLS(Y_valid.to_numpy(), X_valid.to_numpy())\n",
" results = model.fit()\n",
" # 将残差填回对应位置\n",
" daily_residuals.loc[valid_mask, col] = results.resid\n",
" daily_residuals.loc[~valid_mask, col] = np.nan # 原本无效的位置填充 NaN\n",
" except Exception as e:\n",
" print(f\"警告: 日期 {date}, 特征 {col} 回归失败: {e},填充 NaN。\")\n",
" daily_residuals[col] = np.nan\n",
" break\n",
"\n",
" all_residuals.append(daily_residuals)\n",
"\n",
" # 合并所有日期的残差结果\n",
" if all_residuals:\n",
" residuals_df = pd.concat(all_residuals)\n",
" # 将残差结果更新回原始 df (原地修改)\n",
" # 使用 update 比 merge 更适合基于索引的原地更新\n",
" # 确保 residuals_df 的索引与 df 中对应部分一致\n",
" df.update(residuals_df)\n",
" else:\n",
" print(\"没有有效的残差结果可以合并。\")\n",
"\n",
"\n",
" # 清理临时列\n",
" df.drop(columns=[log_cap_col], inplace=True)\n",
" print(\"截面行业市值中性化完成。\")\n",
"\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": 12,
"id": "40e6b68a91b30c79",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T13:08:04.694262Z",
"start_time": "2025-04-03T13:08:03.694904Z"
}
},
"outputs": [],
"source": [
"import pandas as pd\n",
"\n",
"\n",
"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",
"import numpy as np\n",
"import pandas as pd\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": 13,
"id": "47c12bb34062ae7a",
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-03T14:57:50.841165Z",
"start_time": "2025-04-03T14:49:25.889057Z"
}
},
"outputs": [],
"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', group_keys=False)['close'].apply(lambda x: x.shift(-days) / x - 1) + df.groupby('ts_code', group_keys=False)['close'].apply(lambda x: x.shift(-2 * days) / x - 1)\n",
"\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']\n",
"# .rolling(window=5, min_periods=1).sum()\n",
"# .groupby('ts_code') # 再次按 ts_code 分组\n",
"# .shift(-5)\n",
"# .fillna(0) # 填充每个股票组最后的 NaN\n",
"# .astype(int)\n",
"# .reset_index(level=0, drop=True))\n",
"# df['label'] = df.groupby('trade_date', group_keys=False)['future_return'].transform(\n",
"# lambda x: pd.qcut(x, q=50, labels=False, duplicates='drop')\n",
"# )\n",
"# filter_index = df['future_return'].between(df['future_return'].quantile(0.01), df['future_return'].quantile(0.99))\n",
"filter_index = df['future_return'].between(df['future_return'].quantile(0.001), 0.6)\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": 14,
"id": "29221dde",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"200\n"
]
}
],
"source": [
"feature_columns = [col for col in df.head(10)\n",
" .merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n",
" .merge(index_data, on='trade_date', how='left')\n",
" .columns\n",
" ]\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 ['cat_reason', 'cat_is_on_top_list']]\n",
"print(len(feature_columns))"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "03ee5daf",
"metadata": {},
"outputs": [],
"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": 16,
"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', 'cat_senti_mom_vol_spike', 'cat_senti_pre_breakout', 'ts_turnover_rate_acceleration_5_20', 'ts_vol_sustain_10_30', 'cs_amount_outlier_10', 'ts_ff_to_total_turnover_ratio', 'ts_price_volume_trend_coherence_5_20', 'ts_ff_turnover_rate_surge_10', 'undist_profit_ps', 'ocfps', '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', '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', 'senti_strong_inflow', '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', '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%|██████████| 139/139 [00:06<00:00, 21.25it/s]\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"截面 MAD 去极值处理完成。\n",
"开始截面 MAD 去极值处理 (k=3.0)...\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"MAD Filtering: 100%|██████████| 139/139 [00:05<00:00, 26.68it/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', 'cat_senti_mom_vol_spike', 'cat_senti_pre_breakout', 'ts_turnover_rate_acceleration_5_20', 'ts_vol_sustain_10_30', 'cs_amount_outlier_10', 'ts_ff_to_total_turnover_ratio', 'ts_price_volume_trend_coherence_5_20', 'ts_ff_turnover_rate_surge_10', 'undist_profit_ps', 'ocfps', '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', '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', 'senti_strong_inflow', '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', '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",
"1091062\n",
"train_data最小日期: 2020-01-02\n",
"train_data最大日期: 2022-12-30\n",
"869968\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"
]
}
],
"source": [
"split_date = '2023-01-01'\n",
"train_data = df[filter_index & (df['trade_date'] <= split_date) & (df['trade_date'] >= '2020-01-01')].groupby(\n",
" 'trade_date', group_keys=False).apply(lambda x: x.nsmallest(1500, 'total_mv'))\n",
"test_data = df[(df['trade_date'] >= split_date)].groupby(\n",
" 'trade_date', group_keys=False).apply(lambda x: x.nsmallest(1500, 'total_mv'))\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",
"train_data['label'] = train_data.groupby('trade_date', group_keys=False)['future_return'].transform(\n",
" lambda x: pd.qcut(x, q=100, labels=False, duplicates='drop')\n",
")\n",
"test_data['label'] = test_data.groupby('trade_date', group_keys=False)['future_return'].transform(\n",
" lambda x: pd.qcut(x, q=100, labels=False, duplicates='drop')\n",
")\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_industry_cap(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_industry_cap(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": 106,
"id": "3ff2d1c5",
"metadata": {},
"outputs": [],
"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 pandas as pd\n",
"import numpy as np\n",
"import datetime # 用于日期计算\n",
"from catboost import CatBoostClassifier, CatBoostRanker, CatBoostRegressor\n",
"from catboost import Pool\n",
"import lightgbm as lgb\n",
"from lightgbm import LGBMRanker, LGBMRegressor\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",
" train_data_split = train_data_split.sort_values('trade_date')\n",
" val_data_split = val_data_split.sort_values('trade_date')\n",
"\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[target_column]\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': 'QueryRMSE', # 适用于二分类\n",
" 'eval_metric': 'NDCG', # 评估指标\n",
" 'iterations': 1500,\n",
" 'learning_rate': 0.03,\n",
" 'depth': 8, # 控制模型复杂度\n",
" 'l2_leaf_reg': 1, # L2 正则化\n",
" 'verbose': 5000,\n",
" 'early_stopping_rounds': 300,\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",
" group_train = train_data_split['trade_date'].factorize()[0]\n",
" group_val = val_data_split['trade_date'].factorize()[0]\n",
" train_pool = Pool(\n",
" data=X_train,\n",
" label=y_train,\n",
" group_id=group_train,\n",
" cat_features=cat_features\n",
" )\n",
" val_pool = Pool(\n",
" data=X_val,\n",
" label=y_val,\n",
" group_id=group_val,\n",
" cat_features=cat_features\n",
" )\n",
"\n",
"\n",
" model = CatBoostRanker(**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",
" label_gain = list(range(len(train_data_split['label'].unique())))\n",
" \n",
" params = {\n",
" 'label_gain': [gain * gain for gain in label_gain],\n",
" 'objective': 'lambdarank',\n",
" 'metric': 'ndcg',\n",
" 'learning_rate': 0.05,\n",
" 'num_leaves': 1024,\n",
" 'min_data_in_leaf': 256,\n",
" # 'max_depth': 10,\n",
" 'max_bin': 1024,\n",
" 'feature_fraction': 0.5,\n",
" 'bagging_fraction': 0.5,\n",
" 'bagging_freq': 5,\n",
" 'lambda_l1': 5,\n",
" 'lambda_l2': 50,\n",
" 'boosting': 'gbdt',\n",
" 'verbosity': -1,\n",
" 'extra_trees': True,\n",
" # 'max_position': 5,\n",
" 'ndcg_at': '5',\n",
" 'quant_train_renew_leaf': True,\n",
" 'lambdarank_truncation_level': 10,\n",
" 'lambdarank_position_bias_regularization': 1,\n",
" 'seed': 7\n",
" }\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",
" params['feature_contri'] = feature_contri\n",
"\n",
" train_groups = train_data_split.groupby('trade_date').size().tolist()\n",
" val_groups = val_data_split.groupby('trade_date').size().tolist()\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",
" group=train_groups,\n",
" categorical_feature=categorical_feature\n",
" )\n",
" val_dataset = lgb.Dataset(\n",
" X_val, label=y_val, \n",
" group=val_groups,\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(300, first_metric_only=False)\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",
" # from flaml import AutoML\n",
" # from sklearn.datasets import fetch_california_housing\n",
"\n",
" # # Initialize an AutoML instance\n",
" # model = AutoML()\n",
" # # Specify automl goal and constraint\n",
" # automl_settings = {\n",
" # \"time_budget\": 600, # in seconds\n",
" # \"metric\": \"ndcg@1\",\n",
" # \"task\": \"rank\",\n",
" # \"estimator_list\": [\n",
" # \"catboost\",\n",
" # \"lgbm\",\n",
" # \"xgboost\"\n",
" # ], \n",
" # \"ensemble\": {\n",
" # \"final_estimator\": LGBMRanker(),\n",
" # \"passthrough\": False,\n",
" # },\n",
" # }\n",
" # model.fit(X_train=X_train, y_train=y_train, groups=train_groups,\n",
" # X_val=X_val, y_val=y_val,groups_val=val_groups,\n",
" # mlflow_logging=False, **automl_settings)\n",
"\n",
"\n",
" return model, scaler, None # 返回训练好的模型、scaler 和 pca 对象"
]
},
{
"cell_type": "code",
"execution_count": 107,
"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: 1091062\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",
"1091057 603533.SH 2022-12-30 13.362893\n",
"1091058 603416.SH 2022-12-30 13.364553\n",
"1091059 002277.SZ 2022-12-30 13.364740\n",
"1091060 002140.SZ 2022-12-30 13.086924\n",
"1091061 002374.SZ 2022-12-30 13.347147\n",
"\n",
"[1091062 rows x 3 columns]\n",
"原始样本数: 1091062, 去除标签为空后样本数: 1091062\n",
"[1000]\ttrain's ndcg@5: 0.667175\tvalid's ndcg@5: 0.357008\n"
]
},
{
"data": {
"image/png": "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",
"text/plain": [
"<Figure size 640x480 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAvAAAAHHCAYAAADZMWzyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAA+ktJREFUeJzs3XlcTun/+PHX3b5TYSoiS3YS2bcQKjKWGesgYxlLRozEGFRj3xnLzBhkD2OEkSXGMmPPYOyGkRgZy2dIMmm5f3/063zdWlRK7tv7+XjcD53rXOc67/dd6rqvc53rqNRqtRohhBBCCCGEVtAr7ACEEEIIIYQQOScdeCGEEEIIIbSIdOCFEEIIIYTQItKBF0IIIYQQQotIB14IIYQQQggtIh14IYQQQgghtIh04IUQQgghhNAi0oEXQgghhBBCi0gHXgghhBBCCC0iHXghhBCiEIWGhqJSqYiOji7sUIQQWkI68EIIId6q9A5rZq+xY8cWyDmPHj1KUFAQjx8/LpD232cJCQkEBQVx8ODBwg5FiPeGQWEHIIQQ4v0UEhJC2bJlNcqqV69eIOc6evQowcHB+Pr6UrRo0QI5R1717t2b7t27Y2xsXNih5ElCQgLBwcEAuLu7F24wQrwnpAMvhBCiUHh5eeHm5lbYYbyRZ8+eYW5u/kZt6Ovro6+vn08RvT2pqam8ePGisMMQ4r0kU2iEEEK8k3bt2kXTpk0xNzfH0tKSdu3acfHiRY06f/zxB76+vpQrVw4TExPs7Oz49NNPefTokVInKCiIgIAAAMqWLatM14mOjiY6OhqVSkVoaGiG86tUKoKCgjTaUalUXLp0iZ49e2JtbU2TJk2U/WvXrqVOnTqYmppiY2ND9+7duX379mvzzGwOvJOTE+3bt+fgwYO4ublhampKjRo1lGkqP/30EzVq1MDExIQ6depw5swZjTZ9fX2xsLDgr7/+om3btpibm+Pg4EBISAhqtVqj7rNnz/jiiy9wdHTE2NiYSpUqMXv27Az1VCoVfn5+rFu3jmrVqmFsbMy3335L8eLFAQgODlbe2/T3LSffn5ff2+vXrytXSYoUKUK/fv1ISEjI8J6tXbuWevXqYWZmhrW1Nc2aNWPv3r0adXLy8yOEtpIReCGEEIXiyZMnPHz4UKOsWLFiAKxZs4a+ffvStm1bZsyYQUJCAkuXLqVJkyacOXMGJycnACIjI/nrr7/o168fdnZ2XLx4ke+//56LFy9y/PhxVCoVnTt35tq1a2zYsIF58+Yp5yhevDgPHjzIddwff/wxzs7OTJ06VenkTpkyhQkTJtC1a1cGDBjAgwcP+Oabb2jWrBlnzpzJ07Sd69ev07NnTz777DM++eQTZs+ejY+PD99++y1ffvklQ4cOBWDatGl07dqVq1evoqf3f+NyKSkpeHp60qBBA2bOnMnu3buZNGkSycnJhISEAKBWq+nQoQMHDhygf//+1KpViz179hAQEMDff//NvHnzNGL65Zdf2LRpE35+fhQrVgwXFxeWLl3KkCFD6NSpE507dwagZs2aQM6+Py/r2rUrZcuWZdq0afz+++/88MMPlChRghkzZih1goODCQoKolGjRoSEhGBkZMSJEyf45ZdfaNOmDZDznx8htJZaCCGEeItWrlypBjJ9qdVq9dOnT9VFixZVDxw4UOO4e/fuqYsUKaJRnpCQkKH9DRs2qAH14cOHlbJZs2apAfXNmzc16t68eVMNqFeuXJmhHUA9adIkZXvSpElqQN2jRw+NetHR0Wp9fX31lClTNMrPnz+vNjAwyFCe1fvxcmxlypRRA+qjR48qZXv27FEDalNTU/WtW7eU8u+++04NqA8cOKCU9e3bVw2ohw8frpSlpqaq27VrpzYyMlI/ePBArVar1eHh4WpAPXnyZI2YPvroI7VKpVJfv35d4/3Q09NTX7x4UaPugwcPMrxX6XL6/Ul/bz/99FONup06dVLb2toq23/++adaT09P3alTJ3VKSopG3dTUVLVanbufHyG0lUyhEUIIUSgWL15MZGSkxgvSRm0fP35Mjx49ePjwofLS19enfv36HDhwQGnD1NRU+fq///7j4cOHNGjQAIDff/+9QOIePHiwxvZPP/1EamoqXbt21YjXzs4OZ2dnjXhzo2rVqjRs2FDZrl+/PgAtW7akdOnSGcr/+uuvDG34+fkpX6dPgXnx4gX79u0DICIiAn19fT7//HON47744gvUajW7du3SKG/evDlVq1bNcQ65/f68+t42bdqUR48eERcXB0B4eDipqalMnDhR42pDen6Qu58fIbSVTKERQghRKOrVq5fpTax//vknkNZRzYyVlZXy9f/+9z+Cg4MJCwvj/v37GvWePHmSj9H+n1dXzvnzzz9Rq9U4OztnWt/Q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",
"text/plain": [
"<Figure size 640x480 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"\n",
"gc.collect()\n",
"\n",
"use_pca = False\n",
"type = 'light'\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(3000, 'total_mv'))\n",
" .merge(industry_df, on=['cat_l2_code', 'trade_date'], how='left')\n",
" .merge(index_data, on='trade_date', how='left'), \n",
" feature_columns, type=type, target_column='label')\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": [],
"source": [
"score_df = test_data.groupby('trade_date', group_keys=False).apply(lambda x: x.nsmallest(3000, '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",
"\n",
"if type == 'cat':\n",
" score_df['score'] = model.predict(score_df[feature_columns])\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: \n",
" x[\n",
" # (x['score'] <= x['score'].quantile(0.99)) & \n",
" (x['score'] >= x['score'].quantile(0.90))\n",
" ] # 计算90%分位数作为阈值,筛选分数>=阈值的行\n",
").reset_index(drop=True) # drop=True 避免添加旧索引列\n",
"# df_to_drop = score_df.loc[score_df.groupby('trade_date')['score'].idxmax()]\n",
"# score_df = score_df.drop(df_to_drop.index)\n",
"save_df = score_df.groupby('trade_date', group_keys=False).apply(lambda x: x.nlargest(5, '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": "1f3c1331",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"成功连接到 Redis 服务器: 140.143.91.66:6389数据库 0\n",
"DataFrame 已使用 Pickle 序列化并写入 Redis键为 'save_df'\n",
"从 Redis 读取到的 Pickle 序列化数据 (前 20 字节):\n",
"b'\\x80\\x04\\x95\\xbf\\x04\\x01\\x00\\x00\\x00\\x00\\x00\\x8c\\x11pandas.'\n",
"\n",
"从 Redis 加载的 DataFrame (使用 Pickle):\n",
" index ts_code trade_date open close high low vol \\\n",
"4 25 002802.SZ 2023-01-03 21.53 22.14 22.35 21.31 15974.43 \n",
"3 20 600235.SH 2023-01-03 12.23 12.41 12.50 12.23 35791.00 \n",
"2 13 002691.SZ 2023-01-03 9.13 9.32 9.37 9.13 35566.80 \n",
"1 35 603779.SH 2023-01-03 9.74 9.88 10.16 9.69 78641.45 \n",
"0 131 600228.SH 2023-01-03 15.68 15.75 15.79 15.60 17076.00 \n",
"... ... ... ... ... ... ... ... ... \n",
"2904 87098 000931.SZ 2025-05-30 8.02 7.96 8.13 7.92 170963.38 \n",
"2903 87001 605122.SH 2025-05-30 16.86 16.73 17.11 16.68 20146.40 \n",
"2902 87041 002942.SZ 2025-05-30 24.10 23.95 24.34 23.84 10821.00 \n",
"2901 87122 603170.SH 2025-05-30 14.69 14.34 14.83 14.32 26536.80 \n",
"2900 86987 603177.SH 2025-05-30 8.72 8.55 8.77 8.49 25875.00 \n",
"\n",
" pct_chg amount ... 000905.SH_up_ratio_20d 399006.SZ_up_ratio_20d \\\n",
"4 3.12 23729.134 ... 0.3 0.40 \n",
"3 0.73 20011.747 ... 0.3 0.40 \n",
"2 2.08 19174.296 ... 0.3 0.40 \n",
"1 1.65 53043.254 ... 0.3 0.40 \n",
"0 1.42 12366.664 ... 0.3 0.40 \n",
"... ... ... ... ... ... \n",
"2904 -0.38 89186.130 ... 0.6 0.45 \n",
"2903 -1.18 23723.010 ... 0.6 0.45 \n",
"2902 -1.07 18204.020 ... 0.6 0.45 \n",
"2901 -2.45 36665.356 ... 0.6 0.45 \n",
"2900 -1.38 21916.553 ... 0.6 0.45 \n",
"\n",
" 000852.SH_volatility 000905.SH_volatility 399006.SZ_volatility \\\n",
"4 1.036997 0.828596 0.935322 \n",
"3 1.036997 0.828596 0.935322 \n",
"2 1.036997 0.828596 0.935322 \n",
"1 1.036997 0.828596 0.935322 \n",
"0 1.036997 0.828596 0.935322 \n",
"... ... ... ... \n",
"2904 1.089861 0.850444 1.195355 \n",
"2903 1.089861 0.850444 1.195355 \n",
"2902 1.089861 0.850444 1.195355 \n",
"2901 1.089861 0.850444 1.195355 \n",
"2900 1.089861 0.850444 1.195355 \n",
"\n",
" 000852.SH_volume_change_rate 000905.SH_volume_change_rate \\\n",
"4 5.203088 -0.750721 \n",
"3 5.203088 -0.750721 \n",
"2 5.203088 -0.750721 \n",
"1 5.203088 -0.750721 \n",
"0 5.203088 -0.750721 \n",
"... ... ... \n",
"2904 -2.039466 -12.002493 \n",
"2903 -2.039466 -12.002493 \n",
"2902 -2.039466 -12.002493 \n",
"2901 -2.039466 -12.002493 \n",
"2900 -2.039466 -12.002493 \n",
"\n",
" 399006.SZ_volume_change_rate score score_ranks \n",
"4 8.827360 0.391351 1496.0 \n",
"3 8.827360 0.410662 1497.0 \n",
"2 8.827360 0.470817 1498.0 \n",
"1 8.827360 0.567032 1499.0 \n",
"0 8.827360 0.596280 1500.0 \n",
"... ... ... ... \n",
"2904 5.078672 0.707379 1490.0 \n",
"2903 5.078672 0.769014 1491.0 \n",
"2902 5.078672 0.874366 1492.0 \n",
"2901 5.078672 0.975826 1493.0 \n",
"2900 5.078672 1.072646 1494.0 \n",
"\n",
"[2905 rows x 241 columns]\n",
"\n",
"验证成功:原始 DataFrame 和从 Redis 加载的 DataFrame 一致。\n",
"\n",
"清理了 Redis 中键 'save_df' 的数据。\n"
]
}
],
"source": [
"import redis\n",
"import pickle\n",
"\n",
"redis_host = '140.143.91.66'\n",
"redis_port = 6389\n",
"redis_db = 0\n",
"redis_key = 'save_df'\n",
"\n",
"try:\n",
" # 1. 连接到 Redis 服务器\n",
" r = redis.Redis(host=redis_host, port=redis_port, db=redis_db, password='Redis520102')\n",
" r.ping()\n",
" print(f\"\\n成功连接到 Redis 服务器: {redis_host}:{redis_port},数据库 {redis_db}\")\n",
"\n",
" # 2. 将 DataFrame 写入 Redis (使用 Pickle 序列化)\n",
" df_serialized = pickle.dumps(save_df)\n",
" r.set(redis_key, df_serialized)\n",
" print(f\"DataFrame 已使用 Pickle 序列化并写入 Redis键为 '{redis_key}'\")\n",
"\n",
" # 3. 从 Redis 读取数据 (获取 Pickle 序列化的字节流)\n",
" retrieved_serialized = r.get(redis_key)\n",
"\n",
" if retrieved_serialized:\n",
" print(f\"从 Redis 读取到的 Pickle 序列化数据 (前 20 字节):\")\n",
" print(retrieved_serialized[:20])\n",
"\n",
" # 4. 使用 Pickle 反序列化回 Pandas DataFrame\n",
" loaded_df = pickle.loads(retrieved_serialized)\n",
" print(\"\\n从 Redis 加载的 DataFrame (使用 Pickle):\")\n",
" print(loaded_df)\n",
"\n",
" # 5. 验证原始 DataFrame 和加载的 DataFrame 是否一致\n",
" if save_df.equals(loaded_df):\n",
" print(\"\\n验证成功原始 DataFrame 和从 Redis 加载的 DataFrame 一致。\")\n",
" else:\n",
" print(\"\\n验证失败原始 DataFrame 和从 Redis 加载的 DataFrame 不一致!\")\n",
"\n",
" else:\n",
" print(f\"错误:无法从 Redis 获取键 '{redis_key}' 的值。\")\n",
"\n",
" # 6. 清理测试数据 (可选)\n",
" r.delete(redis_key)\n",
" print(f\"\\n清理了 Redis 中键 '{redis_key}' 的数据。\")\n",
"\n",
"except redis.exceptions.ConnectionError as e:\n",
" print(f\"无法连接到 Redis 服务器: {e}\")\n",
" print(\"请确保您的 Redis 服务器已启动并且主机和端口配置正确。\")\n",
"except redis.exceptions.TimeoutError as e:\n",
" print(f\"连接 Redis 服务器超时: {e}\")\n",
" print(\"请检查您的网络连接和 Redis 服务器状态。\")\n",
"except Exception as e:\n",
" print(f\"测试 Redis 时发生未知错误: {e}\")\n",
" print(f\"测试 Redis 时发生未知错误: {e}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "09b1799e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"200\n",
"['vol', 'pct_chg', 'turnover_rate', 'volume_ratio', 'winner_rate', 'cat_senti_mom_vol_spike', 'cat_senti_pre_breakout', 'ts_turnover_rate_acceleration_5_20', 'ts_vol_sustain_10_30', 'cs_amount_outlier_10', 'ts_ff_to_total_turnover_ratio', 'ts_price_volume_trend_coherence_5_20', 'ts_ff_turnover_rate_surge_10', 'undist_profit_ps', 'ocfps', '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', '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', 'senti_strong_inflow', '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', '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"
]
}
],
"source": [
"print(len(feature_columns))\n",
"print(feature_columns)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bceabd1f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"警告: DataFrame 中没有 'group_id' 列。假设整个 DataFrame 是一个需要排序的组。\n"
]
},
{
"ename": "AttributeError",
"evalue": "`np.asfarray` was removed in the NumPy 2.0 release. Use `np.asarray` with a proper dtype instead.",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mAttributeError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[105]\u001b[39m\u001b[32m, line 52\u001b[39m\n\u001b[32m 48\u001b[39m avg_ndcg = {k: np.mean(v) \u001b[38;5;28;01mif\u001b[39;00m v \u001b[38;5;28;01melse\u001b[39;00m np.nan \u001b[38;5;28;01mfor\u001b[39;00m k, v \u001b[38;5;129;01min\u001b[39;00m ndcg_scores.items()}\n\u001b[32m 49\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m avg_ndcg\n\u001b[32m---> \u001b[39m\u001b[32m52\u001b[39m ndcg_results_single_group = \u001b[43mcalculate_ndcg\u001b[49m\u001b[43m(\u001b[49m\u001b[43mscore_df\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mscore_col\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mscore\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlabel_col\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mlabel\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mk_values\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m3\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m5\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mgroup_id\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 53\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33mNDCG 结果\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 54\u001b[39m \u001b[38;5;28mprint\u001b[39m(ndcg_results_single_group)\n",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[105]\u001b[39m\u001b[32m, line 40\u001b[39m, in \u001b[36mcalculate_ndcg\u001b[39m\u001b[34m(df, score_col, label_col, group_id, k_values)\u001b[39m\n\u001b[32m 38\u001b[39m relevant_labels = group_df[label_col].values\n\u001b[32m 39\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m k \u001b[38;5;129;01min\u001b[39;00m k_values:\n\u001b[32m---> \u001b[39m\u001b[32m40\u001b[39m ndcg_scores[\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33mndcg@\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mk\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m].append(\u001b[43mndcg_at_k\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrelevant_labels\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mk\u001b[49m\u001b[43m)\u001b[49m)\n\u001b[32m 41\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 42\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m _, group_df \u001b[38;5;129;01min\u001b[39;00m df.groupby(group_id):\n",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[105]\u001b[39m\u001b[32m, line 27\u001b[39m, in \u001b[36mcalculate_ndcg.<locals>.ndcg_at_k\u001b[39m\u001b[34m(r, k)\u001b[39m\n\u001b[32m 26\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mndcg_at_k\u001b[39m(r, k):\n\u001b[32m---> \u001b[39m\u001b[32m27\u001b[39m dcg_max = \u001b[43mdcg_at_k\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43msorted\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mr\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mreverse\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mk\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m dcg_max:\n\u001b[32m 29\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[32m0.\u001b[39m\n",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[105]\u001b[39m\u001b[32m, line 23\u001b[39m, in \u001b[36mcalculate_ndcg.<locals>.dcg_at_k\u001b[39m\u001b[34m(r, k)\u001b[39m\n\u001b[32m 22\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mdcg_at_k\u001b[39m(r, k):\n\u001b[32m---> \u001b[39m\u001b[32m23\u001b[39m r = \u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43masfarray\u001b[49m(r)[:k] \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(r) > \u001b[32m0\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m np.zeros(k)\n\u001b[32m 24\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m np.sum(r / np.log2(np.arange(\u001b[32m2\u001b[39m, r.size + \u001b[32m2\u001b[39m)))\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/miniconda3/envs/stock/lib/python3.13/site-packages/numpy/__init__.py:400\u001b[39m, in \u001b[36m__getattr__\u001b[39m\u001b[34m(attr)\u001b[39m\n\u001b[32m 397\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(__former_attrs__[attr], name=\u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 399\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m attr \u001b[38;5;129;01min\u001b[39;00m __expired_attributes__:\n\u001b[32m--> \u001b[39m\u001b[32m400\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mAttributeError\u001b[39;00m(\n\u001b[32m 401\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m`np.\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mattr\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m` was removed in the NumPy 2.0 release. \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 402\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m__expired_attributes__[attr]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m,\n\u001b[32m 403\u001b[39m name=\u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m 404\u001b[39m )\n\u001b[32m 406\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m attr == \u001b[33m\"\u001b[39m\u001b[33mchararray\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m 407\u001b[39m warnings.warn(\n\u001b[32m 408\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m`np.chararray` is deprecated and will be removed from \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 409\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mthe main namespace in the future. Use an array with a string \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 410\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mor bytes dtype instead.\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;167;01mDeprecationWarning\u001b[39;00m, stacklevel=\u001b[32m2\u001b[39m)\n",
"\u001b[31mAttributeError\u001b[39m: `np.asfarray` was removed in the NumPy 2.0 release. Use `np.asarray` with a proper dtype instead."
]
}
],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"def calculate_ndcg(df: pd.DataFrame, score_col: str, label_col: str, group_id: str = 'trade_date', k_values: list = [1, 3, 5, 10]):\n",
" \"\"\"\n",
" 计算 DataFrame 中 score 列和 label 列的 NDCG 值。\n",
"\n",
" Args:\n",
" df (pd.DataFrame): 包含 score (排序学习预测分数) 和 label (相关性标签) 的 DataFrame。\n",
" 假设每个需要排序的组(例如,每天的股票)在 DataFrame 中是连续的。\n",
" score_col (str): 包含模型预测分数的列名。\n",
" label_col (str): 包含相关性标签的列名。标签值越高表示相关性越高。\n",
" k_values (list): 一个整数列表,表示计算 NDCG 的 top-k 值。\n",
" 例如,[1, 3, 5] 将计算 NDCG@1, NDCG@3 和 NDCG@5。\n",
"\n",
" Returns:\n",
" dict: 一个字典,包含每个 k 值对应的平均 NDCG 值。\n",
" 例如: {'ndcg@1': 0.85, 'ndcg@3': 0.78, 'ndcg@5': 0.72, 'ndcg@10': 0.65}\n",
" \"\"\"\n",
" ndcg_scores = {f'ndcg@{k}': [] for k in k_values}\n",
"\n",
" def dcg_at_k(r, k):\n",
" r = np.asfarray(r)[:k] if len(r) > 0 else np.zeros(k)\n",
" return np.sum(r / np.log2(np.arange(2, r.size + 2)))\n",
"\n",
" def ndcg_at_k(r, k):\n",
" dcg_max = dcg_at_k(sorted(r, reverse=True), k)\n",
" if not dcg_max:\n",
" return 0.\n",
" return dcg_at_k(r, k) / dcg_max\n",
"\n",
" # 假设 DataFrame 已经按照需要排序的组(例如,'trade_date')进行了分组,\n",
" # 并且每个组内的顺序不重要,我们只需要计算每个组的 NDCG。\n",
" # 如果需要按特定组计算 NDCG请先对 DataFrame 进行分组。\n",
" if group_id not in df.columns:\n",
" print(\"警告: DataFrame 中没有 'group_id' 列。假设整个 DataFrame 是一个需要排序的组。\")\n",
" group_df = df.sort_values(by=score_col, ascending=False)\n",
" relevant_labels = group_df[label_col].values\n",
" for k in k_values:\n",
" ndcg_scores[f'ndcg@{k}'].append(ndcg_at_k(relevant_labels, k))\n",
" else:\n",
" for _, group_df in df.groupby(group_id):\n",
" group_df_sorted = group_df.sort_values(by=score_col, ascending=False)\n",
" relevant_labels = group_df_sorted[label_col].values\n",
" for k in k_values:\n",
" ndcg_scores[f'ndcg@{k}'].append(ndcg_at_k(relevant_labels, k))\n",
"\n",
" avg_ndcg = {k: np.mean(v) if v else np.nan for k, v in ndcg_scores.items()}\n",
" return avg_ndcg\n",
"\n",
"\n",
"ndcg_results_single_group = calculate_ndcg(score_df, score_col='score', label_col='label', k_values=[1, 3, 5], group_id=None)\n",
"print(\"\\nNDCG 结果\")\n",
"print(ndcg_results_single_group)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "44f64679",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" ts_code trade_date open close high low vol pct_chg \\\n",
"1626778 002652.SZ 2019-01-02 19.59 19.64 19.89 19.28 20196.79 1.03 \n",
"1626779 002652.SZ 2019-01-03 19.74 19.44 19.84 19.33 15731.99 -1.02 \n",
"1626780 002652.SZ 2019-01-04 19.33 19.94 19.99 19.08 21099.93 2.57 \n",
"1626781 002652.SZ 2019-01-07 20.04 21.95 21.95 20.04 83534.19 10.08 \n",
"1626782 002652.SZ 2019-01-08 23.21 21.65 23.87 21.65 149377.97 -1.37 \n",
"... ... ... ... ... ... ... ... ... \n",
"1628321 002652.SZ 2025-05-19 15.05 15.05 15.21 14.80 142648.00 1.69 \n",
"1628322 002652.SZ 2025-05-20 15.11 15.31 15.36 14.85 131075.23 1.73 \n",
"1628323 002652.SZ 2025-05-21 15.51 15.26 15.56 15.11 147109.00 -0.33 \n",
"1628324 002652.SZ 2025-05-22 15.11 14.95 15.46 14.75 129187.20 -2.03 \n",
"1628325 002652.SZ 2025-05-23 14.95 14.70 15.11 14.70 139145.40 -1.67 \n",
"\n",
" amount turnover_rate ... cs_rank_vol_x_profit_margin \\\n",
"1626778 7867.047 0.3964 ... 0.608839 \n",
"1626779 6121.460 0.3088 ... 0.586710 \n",
"1626780 8245.083 0.4141 ... 0.682847 \n",
"1626781 35514.117 1.6394 ... 0.987591 \n",
"1626782 67160.354 2.9317 ... 0.765693 \n",
"... ... ... ... ... \n",
"1628321 42651.655 2.7857 ... 0.758644 \n",
"1628322 39438.290 2.5597 ... 0.834661 \n",
"1628323 44703.816 2.8729 ... 0.365327 \n",
"1628324 38679.608 2.5229 ... 0.810362 \n",
"1628325 41151.946 2.7173 ... 0.738293 \n",
"\n",
" cs_rank_lg_flow_price_concordance cs_rank_turnover_per_winner \\\n",
"1626778 0.203142 0.864865 \n",
"1626779 0.156684 0.763417 \n",
"1626780 0.184009 0.660949 \n",
"1626781 0.734940 0.700000 \n",
"1626782 0.874042 0.914234 \n",
"... ... ... \n",
"1628321 0.106051 0.544548 \n",
"1628322 0.202523 0.478420 \n",
"1628323 0.580870 0.520757 \n",
"1628324 0.808369 0.476918 \n",
"1628325 0.617735 0.404517 \n",
"\n",
" cs_rank_ind_cap_neutral_pe cs_rank_volume_ratio \\\n",
"1626778 NaN 0.646930 \n",
"1626779 NaN 0.251279 \n",
"1626780 NaN 0.311724 \n",
"1626781 NaN 0.988313 \n",
"1626782 NaN 0.990142 \n",
"... ... ... \n",
"1628321 NaN 0.695645 \n",
"1628322 NaN 0.542497 \n",
"1628323 NaN 0.678180 \n",
"1628324 NaN 0.524743 \n",
"1628325 NaN 0.585852 \n",
"\n",
" cs_rank_elg_buy_sell_sm_ratio cs_rank_cost_dist_vol_ratio \\\n",
"1626778 0.341855 0.678941 \n",
"1626779 0.318912 0.402916 \n",
"1626780 0.260036 0.460713 \n",
"1626781 0.796350 0.988501 \n",
"1626782 0.598905 0.991571 \n",
"... ... ... \n",
"1628321 0.287899 0.788896 \n",
"1628322 0.116534 0.705843 \n",
"1628323 0.492860 0.783793 \n",
"1628324 0.130521 0.696446 \n",
"1628325 0.134175 0.735636 \n",
"\n",
" cs_rank_size future_return label \n",
"1626778 0.258948 0.092159 45.0 \n",
"1626779 0.258123 0.075103 41.0 \n",
"1626780 0.257664 0.058175 41.0 \n",
"1626781 0.290146 -0.034169 4.0 \n",
"1626782 0.282482 -0.023095 4.0 \n",
"... ... ... ... \n",
"1628321 0.032912 NaN NaN \n",
"1628322 0.034861 NaN NaN \n",
"1628323 0.035204 NaN NaN \n",
"1628324 0.034208 NaN NaN \n",
"1628325 0.032547 NaN NaN \n",
"\n",
"[1548 rows x 190 columns]\n"
]
}
],
"source": [
"print(df[df['ts_code'] == '002652.SZ'])"
]
}
],
"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
}