数据分析版本V20250225基于Python语言的-Stock Market Prediction(股票价格预测)

Author

AndyBourne

Stock Market Prediction(股票价格预测)

链接:https://www.kaggle.com/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs

任务:时序预测、特征工程、简单回归模型

苹果公司的分析

进一步分析

# 1. 导入必要的库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

# 2. 加载数据(使用你提供的路径)
# 注意:请确保该路径在你的Kaggle环境中确实存在。如果路径不正确,可以运行下方注释的os.walk代码查找正确路径。
df = pd.read_csv('/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/aapl.us.txt')

# (可选)如果路径不对,取消下面代码的注释来查找实际文件位置
# import os
# for dirname, _, filenames in os.walk('/kaggle/input'):
#     for filename in filenames:
#         print(os.path.join(dirname, filename))

# 3. 数据预处理
# 将日期列转为时间格式并设为索引
df['Date'] = pd.to_datetime(df['Date'])
df = df.set_index('Date')
# 按时间升序排列(确保时间顺序正确)
df = df.sort_index()

# 4. 特征工程
# 基础统计特征:移动平均线
df['SMA_5'] = df['Close'].rolling(window=5).mean()    # 5日简单移动平均
df['SMA_20'] = df['Close'].rolling(window=20).mean()  # 20日简单移动平均

# 成交量变化率(处理可能的除零问题,使用diff避免inf)
df['Volume_Change'] = df['Volume'].diff() / (df['Volume'].shift(1) + 1e-10)

# 定义预测目标:下一日的收盘价
df['Target'] = df['Close'].shift(-1)

# 5. 处理无穷大和缺失值
# 将无穷大(inf或-inf)替换为NaN
df = df.replace([np.inf, -np.inf], np.nan)
# 删除所有包含NaN的行(包括因滚动计算、pct_change、shift产生的NaN)
df = df.dropna()

# 6. 准备模型数据
features = ['Open', 'High', 'Low', 'Close', 'Volume', 'SMA_5', 'SMA_20', 'Volume_Change']
X = df[features]
y = df['Target']

# 7. 按时间顺序划分训练集和测试集(前80%训练,后20%测试)
split_point = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_point], X.iloc[split_point:]
y_train, y_test = y.iloc[:split_point], y.iloc[split_point:]

# 8. 训练线性回归模型
model = LinearRegression()
model.fit(X_train, y_train)

# 9. 预测与评估
y_pred = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f'测试集上的均方根误差 (RMSE): {rmse:.4f}')

# 10. (可选)绘制真实值与预测值的对比图
plt.figure(figsize=(12, 6))
plt.plot(y_test.index, y_test.values, label='Actual Price', linewidth=1)
plt.plot(y_test.index, y_pred, label='Predicted Price', linewidth=1, alpha=0.7)
plt.xlabel('Date')
plt.ylabel('Close Price')
plt.title('AAPL Stock Price Prediction (Linear Regression)')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()

# 11. (可选)输出模型系数,了解特征重要性
coeff_df = pd.DataFrame({
    'Feature': features,
    'Coefficient': model.coef_
})
print("\n特征系数:")
print(coeff_df)
测试集上的均方根误差 (RMSE): 1.7455


特征系数:
         Feature   Coefficient
0           Open  1.687612e-01
1           High  1.690537e-01
2            Low  1.670418e-01
3          Close  1.687599e-01
4         Volume -2.607402e-10
5          SMA_5  1.680046e-01
6         SMA_20  1.652753e-01
7  Volume_Change  0.000000e+00

继续分析,使用 ta 库:Kaggle环境通常预装了 ta(Technical Analysis Library),可以方便地计算上百种指标。

!pip install ta  # 如果未安装
import ta
df['rsi'] = ta.momentum.RSIIndicator(df['Close'], window=14).rsi()
df['macd'] = ta.trend.MACD(df['Close']).macd()
df['bb_high'] = ta.volatility.BollingerBands(df['Close']).bollinger_hband()
Collecting ta
  Downloading ta-0.11.0.tar.gz (25 kB)
  Preparing metadata (setup.py) ... done
Requirement already satisfied: numpy in /opt/conda/lib/python3.10/site-packages (from ta) (1.26.4)
Requirement already satisfied: pandas in /opt/conda/lib/python3.10/site-packages (from ta) (2.2.2)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.4)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.10/site-packages (from python-dateutil>=2.8.2->pandas->ta) (1.16.0)
Building wheels for collected packages: ta
  Building wheel for ta (setup.py) ... done
  Created wheel for ta: filename=ta-0.11.0-py3-none-any.whl size=29413 sha256=5666e0d8fac784c2bc7f0a0c0a5e7a18553425e2d0fe4e54681b022d70aa1ace
  Stored in directory: /root/.cache/pip/wheels/5f/67/4f/8a9f252836e053e532c6587a3230bc72a4deb16b03a829610b
Successfully built ta
Installing collected packages: ta
Successfully installed ta-0.11.0
# ==================== 1. 安装并导入必要的库 ====================
!pip install ta  # Kaggle 环境可能未预装,运行安装(如果已安装可注释掉)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import train_test_split, TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

import ta  # 技术分析库

# 设置中文显示(如有需要)
# plt.rcParams['font.sans-serif'] = ['SimHei']
# plt.rcParams['axes.unicode_minus'] = False

# ==================== 2. 加载数据 ====================
# 请确保路径与你的数据集实际位置一致
df = pd.read_csv('/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/aapl.us.txt')

# 日期处理
df['Date'] = pd.to_datetime(df['Date'])
df = df.set_index('Date').sort_index()

print(f"数据时间范围:{df.index.min()}{df.index.max()}")
print(f"总记录数:{len(df)}")

# ==================== 3. 特征工程(使用 ta 库) ====================
# 复制一份以避免 SettingWithCopyWarning
data = df.copy()

# ---------- 3.1 趋势指标 ----------
data['SMA_10'] = ta.trend.sma_indicator(close=data['Close'], window=10)          # 10日简单移动平均
data['SMA_50'] = ta.trend.sma_indicator(close=data['Close'], window=50)          # 50日简单移动平均
data['EMA_10'] = ta.trend.ema_indicator(close=data['Close'], window=10)          # 10日指数移动平均
data['EMA_50'] = ta.trend.ema_indicator(close=data['Close'], window=50)          # 50日指数移动平均
data['MACD'] = ta.trend.MACD(close=data['Close']).macd()                          # MACD 线
data['MACD_signal'] = ta.trend.MACD(close=data['Close']).macd_signal()           # MACD 信号线
data['MACD_diff'] = ta.trend.MACD(close=data['Close']).macd_diff()               # MACD 柱状图
data['ADX'] = ta.trend.ADXIndicator(high=data['High'], low=data['Low'], close=data['Close'], window=14).adx()  # 平均趋向指数

# ---------- 3.2 动量指标 ----------
data['RSI'] = ta.momentum.RSIIndicator(close=data['Close'], window=14).rsi()     # 相对强弱指标
data['Stoch_K'] = ta.momentum.StochasticOscillator(high=data['High'], low=data['Low'], close=data['Close'], window=14, smooth_window=3).stoch()  # 随机指标 %K
data['Stoch_D'] = ta.momentum.StochasticOscillator(high=data['High'], low=data['Low'], close=data['Close'], window=14, smooth_window=3).stoch_signal()  # 随机指标 %D
data['WilliamsR'] = ta.momentum.WilliamsRIndicator(high=data['High'], low=data['Low'], close=data['Close'], lbp=14).williams_r()  # 威廉指标

# ---------- 3.3 波动率指标 ----------
bb = ta.volatility.BollingerBands(close=data['Close'], window=20, window_dev=2)
data['BB_high'] = bb.bollinger_hband()               # 布林带上轨
data['BB_low'] = bb.bollinger_lband()                 # 布林带下轨
data['BB_width'] = bb.bollinger_wband()               # 布林带宽度
data['ATR'] = ta.volatility.AverageTrueRange(high=data['High'], low=data['Low'], close=data['Close'], window=14).average_true_range()  # 平均真实波幅

# ---------- 3.4 成交量指标 ----------
data['OBV'] = ta.volume.OnBalanceVolumeIndicator(close=data['Close'], volume=data['Volume']).on_balance_volume()  # 能量潮
data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(high=data['High'], low=data['Low'], close=data['Close'], volume=data['Volume'], window=20).chaikin_money_flow()  # 佳庆资金流

# ---------- 3.5 价格衍生特征 ----------
data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100               # 当日振幅百分比
data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100             # 当日涨跌幅
data['Volume_Change'] = data['Volume'].pct_change()                               # 成交量变化率(注意会出现inf)

# ---------- 3.6 定义预测目标:下一日收盘价 ----------
data['Target'] = data['Close'].shift(-1)

# ==================== 4. 处理缺失值和无穷大 ====================
# 将无穷大替换为 NaN
data = data.replace([np.inf, -np.inf], np.nan)

# 查看各列的缺失情况
missing = data.isnull().sum()
print("\n各列缺失值数量:")
print(missing[missing > 0].sort_values(ascending=False))

# 删除包含 NaN 的行(因为所有指标都需要历史窗口,开头部分必有缺失)
data_clean = data.dropna().copy()
print(f"\n清洗后数据量:{len(data_clean)}")

# 分离特征和目标
features = [
    'SMA_10', 'SMA_50', 'EMA_10', 'EMA_50', 'MACD', 'MACD_signal', 'MACD_diff', 'ADX',
    'RSI', 'Stoch_K', 'Stoch_D', 'WilliamsR',
    'BB_high', 'BB_low', 'BB_width', 'ATR',
    'OBV', 'CMF',
    'HL_pct', 'OC_pct', 'Volume_Change',
    'Open', 'High', 'Low', 'Close', 'Volume'   # 保留原始价格和成交量作为基准
]

X = data_clean[features]
y = data_clean['Target']

# 检查是否还有 NaN 或 inf
assert not X.isnull().any().any(), "特征中存在 NaN"
assert not np.isinf(X.values).any(), "特征中存在无穷大"

# ==================== 5. 时间序列划分 ====================
# 使用 TimeSeriesSplit 进行交叉验证(此处只演示单次划分,便于可视化)
split_point = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_point], X.iloc[split_point:]
y_train, y_test = y.iloc[:split_point], y.iloc[split_point:]

print(f"\n训练集:{X_train.index[0]}{X_train.index[-1]},样本数 {len(X_train)}")
print(f"测试集:{X_test.index[0]}{X_test.index[-1]},样本数 {len(X_test)}")

# ==================== 6. 模型训练与评估 ====================

# ---------- 6.1 线性回归 ----------
lr = LinearRegression()
lr.fit(X_train, y_train)
y_pred_lr = lr.predict(X_test)

# 计算评估指标
rmse_lr = np.sqrt(mean_squared_error(y_test, y_pred_lr))
mae_lr = mean_absolute_error(y_test, y_pred_lr)
r2_lr = r2_score(y_test, y_pred_lr)

print("\n========== 线性回归结果 ==========")
print(f"RMSE: {rmse_lr:.4f}")
print(f"MAE : {mae_lr:.4f}")
print(f"R²  : {r2_lr:.4f}")

# 特征系数
coef_df = pd.DataFrame({'Feature': features, 'Coefficient': lr.coef_}).sort_values(by='Coefficient', ascending=False)
print("\n线性回归特征系数(前10):")
print(coef_df.head(10))

# ---------- 6.2 随机森林 ----------
rf = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)

rmse_rf = np.sqrt(mean_squared_error(y_test, y_pred_rf))
mae_rf = mean_absolute_error(y_test, y_pred_rf)
r2_rf = r2_score(y_test, y_pred_rf)

print("\n========== 随机森林结果 ==========")
print(f"RMSE: {rmse_rf:.4f}")
print(f"MAE : {mae_rf:.4f}")
print(f"R²  : {r2_rf:.4f}")

# 特征重要性
importance_df = pd.DataFrame({'Feature': features, 'Importance': rf.feature_importances_}).sort_values(by='Importance', ascending=False)
print("\n随机森林特征重要性(前10):")
print(importance_df.head(10))

# ==================== 7. 可视化 ====================

# 7.1 实际值 vs 预测值(线性回归)
plt.figure(figsize=(14, 6))
plt.plot(y_test.index, y_test.values, label='Actual Price', linewidth=1)
plt.plot(y_test.index, y_pred_lr, label='Linear Regression Predicted', linewidth=1, alpha=0.7)
plt.title('AAPL Stock Price Prediction (Linear Regression)')
plt.xlabel('Date')
plt.ylabel('Close Price')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()

# 7.2 实际值 vs 预测值(随机森林)
plt.figure(figsize=(14, 6))
plt.plot(y_test.index, y_test.values, label='Actual Price', linewidth=1)
plt.plot(y_test.index, y_pred_rf, label='Random Forest Predicted', linewidth=1, alpha=0.7)
plt.title('AAPL Stock Price Prediction (Random Forest)')
plt.xlabel('Date')
plt.ylabel('Close Price')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()

# 7.3 特征重要性条形图(随机森林)
plt.figure(figsize=(10, 8))
sns.barplot(data=importance_df.head(15), x='Importance', y='Feature')
plt.title('Top 15 Feature Importances (Random Forest)')
plt.tight_layout()
plt.show()

# 7.4 线性回归系数(绝对值较大者)
coef_abs = coef_df.copy()
coef_abs['Abs'] = np.abs(coef_abs['Coefficient'])
coef_abs = coef_abs.sort_values('Abs', ascending=False).head(15)
plt.figure(figsize=(10, 8))
sns.barplot(data=coef_abs, x='Coefficient', y='Feature')
plt.title('Top 15 Feature Coefficients (Linear Regression)')
plt.tight_layout()
plt.show()
Requirement already satisfied: ta in /opt/conda/lib/python3.10/site-packages (0.11.0)
Requirement already satisfied: numpy in /opt/conda/lib/python3.10/site-packages (from ta) (1.26.4)
Requirement already satisfied: pandas in /opt/conda/lib/python3.10/site-packages (from ta) (2.2.2)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.4)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.10/site-packages (from python-dateutil>=2.8.2->pandas->ta) (1.16.0)
数据时间范围:1984-09-07 00:00:00 至 2017-11-10 00:00:00
总记录数:8364

各列缺失值数量:
EMA_50           49
SMA_50           49
MACD_signal      33
MACD_diff        33
MACD             25
BB_width         19
CMF              19
BB_high          19
BB_low           19
Stoch_D          15
Stoch_K          13
RSI              13
WilliamsR        13
SMA_10            9
EMA_10            9
Volume_Change     2
Target            1
dtype: int64

清洗后数据量:8313

训练集:1984-11-15 00:00:00 至 2011-04-04 00:00:00,样本数 6650
测试集:2011-04-05 00:00:00 至 2017-11-09 00:00:00,样本数 1663

========== 线性回归结果 ==========
RMSE: 1.4102
MAE : 0.9982
R²  : 0.9980

线性回归特征系数(前10):
          Feature  Coefficient
24          Close     0.800270
2          EMA_10     0.269605
5     MACD_signal     0.261328
22           High     0.240774
23            Low     0.166713
3          EMA_50     0.118963
20  Volume_Change     0.001128
7             ADX     0.000317
8             RSI     0.000298
9         Stoch_K     0.000292

========== 随机森林结果 ==========
RMSE: 55.3061
MAE : 45.7810
R²  : -2.1428

随机森林特征重要性(前10):
    Feature  Importance
23      Low    0.552909
24    Close    0.169545
22     High    0.120471
21     Open    0.058296
12  BB_high    0.026034
3    EMA_50    0.024388
0    SMA_10    0.012732
1    SMA_50    0.012322
2    EMA_10    0.011680
13   BB_low    0.011267

继续改进

# ==================== 1. 安装并导入必要的库 ====================
!pip install ta  # Kaggle 环境可能未预装,运行安装(若已安装可注释掉)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import train_test_split, TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

import ta  # 技术分析库

# ==================== 2. 加载数据 ====================
# 请确保路径与你的数据集实际位置一致
df = pd.read_csv('/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/aapl.us.txt')

# 日期处理
df['Date'] = pd.to_datetime(df['Date'])
df = df.set_index('Date').sort_index()

print(f"数据时间范围:{df.index.min()}{df.index.max()}")
print(f"总记录数:{len(df)}")

# ==================== 3. 特征工程(使用 ta 库) ====================
data = df.copy()

# ---------- 3.1 趋势指标 ----------
data['SMA_10'] = ta.trend.sma_indicator(close=data['Close'], window=10)
data['SMA_50'] = ta.trend.sma_indicator(close=data['Close'], window=50)
data['EMA_10'] = ta.trend.ema_indicator(close=data['Close'], window=10)
data['EMA_50'] = ta.trend.ema_indicator(close=data['Close'], window=50)
macd = ta.trend.MACD(close=data['Close'])
data['MACD'] = macd.macd()
data['MACD_signal'] = macd.macd_signal()
data['MACD_diff'] = macd.macd_diff()
data['ADX'] = ta.trend.ADXIndicator(high=data['High'], low=data['Low'], close=data['Close'], window=14).adx()

# ---------- 3.2 动量指标 ----------
data['RSI'] = ta.momentum.RSIIndicator(close=data['Close'], window=14).rsi()
stoch = ta.momentum.StochasticOscillator(high=data['High'], low=data['Low'], close=data['Close'], window=14, smooth_window=3)
data['Stoch_K'] = stoch.stoch()
data['Stoch_D'] = stoch.stoch_signal()
data['WilliamsR'] = ta.momentum.WilliamsRIndicator(high=data['High'], low=data['Low'], close=data['Close'], lbp=14).williams_r()

# ---------- 3.3 波动率指标 ----------
bb = ta.volatility.BollingerBands(close=data['Close'], window=20, window_dev=2)
data['BB_high'] = bb.bollinger_hband()
data['BB_low'] = bb.bollinger_lband()
data['BB_width'] = bb.bollinger_wband()
data['ATR'] = ta.volatility.AverageTrueRange(high=data['High'], low=data['Low'], close=data['Close'], window=14).average_true_range()

# ---------- 3.4 成交量指标 ----------
data['OBV'] = ta.volume.OnBalanceVolumeIndicator(close=data['Close'], volume=data['Volume']).on_balance_volume()
data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(high=data['High'], low=data['Low'], close=data['Close'], volume=data['Volume'], window=20).chaikin_money_flow()

# ---------- 3.5 价格衍生特征 ----------
data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
data['Volume_Change'] = data['Volume'].pct_change()

# ---------- 3.6 定义预测目标:次日收益率(百分比) ----------
# 平稳化处理:预测收益率而非价格,使序列更平稳,有利于树模型的外推
data['Target'] = data['Close'].pct_change().shift(-1) * 100

# ==================== 4. 处理缺失值和无穷大 ====================
# 将无穷大替换为 NaN
data = data.replace([np.inf, -np.inf], np.nan)

# 查看各列的缺失情况
missing = data.isnull().sum()
print("\n各列缺失值数量:")
print(missing[missing > 0].sort_values(ascending=False))

# 删除包含 NaN 的行
data_clean = data.dropna().copy()
print(f"\n清洗后数据量:{len(data_clean)}")

# 分离特征和目标
features = [
    'SMA_10', 'SMA_50', 'EMA_10', 'EMA_50', 'MACD', 'MACD_signal', 'MACD_diff', 'ADX',
    'RSI', 'Stoch_K', 'Stoch_D', 'WilliamsR',
    'BB_high', 'BB_low', 'BB_width', 'ATR',
    'OBV', 'CMF',
    'HL_pct', 'OC_pct', 'Volume_Change',
    'Open', 'High', 'Low', 'Close', 'Volume'
]

X = data_clean[features]
y = data_clean['Target']

# 最终检查
assert not X.isnull().any().any(), "特征中存在 NaN"
assert not np.isinf(X.values).any(), "特征中存在无穷大"

# ==================== 5. 时间序列划分 ====================
split_point = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_point], X.iloc[split_point:]
y_train, y_test = y.iloc[:split_point], y.iloc[split_point:]

print(f"\n训练集:{X_train.index[0]}{X_train.index[-1]},样本数 {len(X_train)}")
print(f"测试集:{X_test.index[0]}{X_test.index[-1]},样本数 {len(X_test)}")

# ==================== 6. 模型训练与评估 ====================

# ---------- 6.1 线性回归 ----------
lr = LinearRegression()
lr.fit(X_train, y_train)
y_pred_lr = lr.predict(X_test)

# 评估函数
def evaluate(y_true, y_pred, model_name):
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    mae = mean_absolute_error(y_true, y_pred)
    r2 = r2_score(y_true, y_pred)
    print(f"\n========== {model_name} 结果(收益率预测) ==========")
    print(f"RMSE: {rmse:.4f}%")
    print(f"MAE : {mae:.4f}%")
    print(f"R²  : {r2:.4f}")
    return rmse, mae, r2

rmse_lr, mae_lr, r2_lr = evaluate(y_test, y_pred_lr, "线性回归")

# 线性回归系数(前10)
coef_df = pd.DataFrame({'Feature': features, 'Coefficient': lr.coef_}).sort_values(by='Coefficient', ascending=False)
print("\n线性回归特征系数(前10):")
print(coef_df.head(10))

# ---------- 6.2 随机森林(调参以增强泛化能力)----------
rf = RandomForestRegressor(
    n_estimators=200,
    max_depth=5,                 # 限制深度,避免过拟合
    min_samples_split=50,        # 节点分裂所需最小样本数
    min_samples_leaf=20,         # 叶子节点最小样本数
    max_features='sqrt',         # 每次分裂考虑的特征数
    random_state=42,
    n_jobs=-1
)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)

rmse_rf, mae_rf, r2_rf = evaluate(y_test, y_pred_rf, "随机森林")

# 特征重要性(前10)
importance_df = pd.DataFrame({'Feature': features, 'Importance': rf.feature_importances_}).sort_values(by='Importance', ascending=False)
print("\n随机森林特征重要性(前10):")
print(importance_df.head(10))

# ==================== 7. 可视化 ====================

# 7.1 收益率预测对比图
plt.figure(figsize=(14, 6))
plt.plot(y_test.index, y_test.values, label='Actual Return %', linewidth=1, alpha=0.7)
plt.plot(y_test.index, y_pred_lr, label='Linear Regression Predicted %', linewidth=1, alpha=0.7)
plt.plot(y_test.index, y_pred_rf, label='Random Forest Predicted %', linewidth=1, alpha=0.7)
plt.xlabel('Date')
plt.ylabel('Next-Day Return (%)')
plt.title('AAPL Next-Day Return Prediction')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()

# 7.2 随机森林特征重要性
plt.figure(figsize=(10, 8))
sns.barplot(data=importance_df.head(15), x='Importance', y='Feature')
plt.title('Top 15 Feature Importances (Random Forest)')
plt.tight_layout()
plt.show()

# 7.3 线性回归系数(绝对值较大者)
coef_abs = coef_df.copy()
coef_abs['Abs'] = np.abs(coef_abs['Coefficient'])
coef_abs = coef_abs.sort_values('Abs', ascending=False).head(15)
plt.figure(figsize=(10, 8))
sns.barplot(data=coef_abs, x='Coefficient', y='Feature')
plt.title('Top 15 Feature Coefficients by Absolute Value (Linear Regression)')
plt.tight_layout()
plt.show()

# ==================== 8. 可选:将收益率预测还原为价格进行对比 ====================
# 注意:还原过程会累积误差,仅作为直观对比参考
# 还原方法:从测试集前一天的真实收盘价开始,依次乘以(1 + 预测收益率/100)
last_price = X_test.iloc[0]['Close']  # 测试集第一天的前一天的收盘价?实际需要前一天的价格
# 更准确的做法:获取测试集开始前一天的收盘价
train_last_close = data_clean.loc[X_train.index[-1], 'Close']
pred_prices = [train_last_close]
for ret in y_pred_rf:
    pred_prices.append(pred_prices[-1] * (1 + ret/100))
pred_prices = pred_prices[1:]  # 对齐测试集

plt.figure(figsize=(14, 6))
plt.plot(y_test.index, data_clean.loc[y_test.index, 'Close'], label='Actual Price')
plt.plot(y_test.index, pred_prices, label='RF Predicted Price (from returns)')
plt.legend()
plt.title('Random Forest: Price Reconstruction from Return Predictions')
plt.grid(True, linestyle='--')
plt.show()
Requirement already satisfied: ta in /opt/conda/lib/python3.10/site-packages (0.11.0)
Requirement already satisfied: numpy in /opt/conda/lib/python3.10/site-packages (from ta) (1.26.4)
Requirement already satisfied: pandas in /opt/conda/lib/python3.10/site-packages (from ta) (2.2.2)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.4)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.10/site-packages (from python-dateutil>=2.8.2->pandas->ta) (1.16.0)
数据时间范围:1984-09-07 00:00:00 至 2017-11-10 00:00:00
总记录数:8364

各列缺失值数量:
EMA_50           49
SMA_50           49
MACD_signal      33
MACD_diff        33
MACD             25
BB_width         19
CMF              19
BB_high          19
BB_low           19
Stoch_D          15
Stoch_K          13
RSI              13
WilliamsR        13
SMA_10            9
EMA_10            9
Volume_Change     2
Target            1
dtype: int64

清洗后数据量:8313

训练集:1984-11-15 00:00:00 至 2011-04-04 00:00:00,样本数 6650
测试集:2011-04-05 00:00:00 至 2017-11-09 00:00:00,样本数 1663

========== 线性回归 结果(收益率预测) ==========
RMSE: 2.0469%
MAE : 1.5546%
R²  : -0.6542

线性回归特征系数(前10):
          Feature  Coefficient
2          EMA_10     3.678331
22           High     2.250294
5     MACD_signal     2.141676
23            Low     1.042892
3          EMA_50     0.712127
17            CMF     0.095602
20  Volume_Change     0.025847
18         HL_pct     0.012959
9         Stoch_K     0.006205
11      WilliamsR     0.006205

========== 随机森林 结果(收益率预测) ==========
RMSE: 1.6000%
MAE : 1.1392%
R²  : -0.0106

随机森林特征重要性(前10):
          Feature  Importance
19         OC_pct    0.130824
18         HL_pct    0.117695
14       BB_width    0.060585
4            MACD    0.057589
25         Volume    0.050684
8             RSI    0.046921
10        Stoch_D    0.043304
17            CMF    0.040246
5     MACD_signal    0.035612
20  Volume_Change    0.034287

qw 预测收益率确实比预测价格难得多:线性回归 R² 为负,随机森林 R² 接近 0,说明模型几乎无法解释收益率的变动。但这在金融时序预测中非常常见——收益率序列接近随机游走,可预测性很低。不过,我们仍可以通过更丰富的特征、更复杂的模型以及分类任务来提升效果。

# ==================== 1. 安装必要库 ====================
!pip install ta xgboost lightgbm  # Kaggle 环境通常已预装,若缺失则运行

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from sklearn.metrics import (mean_squared_error, mean_absolute_error, r2_score,
                             accuracy_score, roc_auc_score, confusion_matrix)
import xgboost as xgb
import lightgbm as lgb
import ta

# ==================== 2. 加载数据 ====================
df = pd.read_csv('/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/aapl.us.txt')
df['Date'] = pd.to_datetime(df['Date'])
df = df.set_index('Date').sort_index()
print(f"数据时间范围:{df.index.min()}{df.index.max()}")
print(f"总记录数:{len(df)}")

# ==================== 3. 扩展特征工程 ====================
data = df.copy()

# ---------- 基础技术指标(同前)----------
# 趋势
data['SMA_10'] = ta.trend.sma_indicator(data['Close'], 10)
data['SMA_50'] = ta.trend.sma_indicator(data['Close'], 50)
data['EMA_10'] = ta.trend.ema_indicator(data['Close'], 10)
data['EMA_50'] = ta.trend.ema_indicator(data['Close'], 50)
macd = ta.trend.MACD(data['Close'])
data['MACD'] = macd.macd()
data['MACD_signal'] = macd.macd_signal()
data['MACD_diff'] = macd.macd_diff()
data['ADX'] = ta.trend.ADXIndicator(data['High'], data['Low'], data['Close'], 14).adx()

# 动量
data['RSI'] = ta.momentum.RSIIndicator(data['Close'], 14).rsi()
stoch = ta.momentum.StochasticOscillator(data['High'], data['Low'], data['Close'], 14, 3)
data['Stoch_K'] = stoch.stoch()
data['Stoch_D'] = stoch.stoch_signal()
data['WilliamsR'] = ta.momentum.WilliamsRIndicator(data['High'], data['Low'], data['Close'], 14).williams_r()

# 波动率
bb = ta.volatility.BollingerBands(data['Close'], 20, 2)
data['BB_high'] = bb.bollinger_hband()
data['BB_low'] = bb.bollinger_lband()
data['BB_width'] = bb.bollinger_wband()
data['ATR'] = ta.volatility.AverageTrueRange(data['High'], data['Low'], data['Close'], 14).average_true_range()

# 成交量
data['OBV'] = ta.volume.OnBalanceVolumeIndicator(data['Close'], data['Volume']).on_balance_volume()
data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(data['High'], data['Low'], data['Close'], data['Volume'], 20).chaikin_money_flow()

# 价格衍生
data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
data['Volume_Change'] = data['Volume'].pct_change()

# ---------- 新增特征 ----------
# 收益率滞后项(过去1~5日收益率)
for lag in range(1, 6):
    data[f'return_lag{lag}'] = data['Close'].pct_change(lag) * 100

# 波动率特征:过去5日、20日收益率标准差
data['vol_5'] = data['Close'].pct_change().rolling(5).std() * 100
data['vol_20'] = data['Close'].pct_change().rolling(20).std() * 100

# 日历特征
data['day_of_week'] = data.index.dayofweek  # 星期一=0, 星期日=6
data['month'] = data.index.month
data['quarter'] = data.index.quarter
# 可以将星期几、月份进行one-hot编码,这里暂留作数值特征,树模型能处理

# ---------- 预测目标 ----------
# 回归目标:次日收益率 (%)
data['target_reg'] = data['Close'].pct_change().shift(-1) * 100
# 分类目标:次日涨跌(1=涨,0=跌或平)
data['target_cls'] = (data['Close'].shift(-1) > data['Close']).astype(int)

# ==================== 4. 处理缺失值 ====================
data = data.replace([np.inf, -np.inf], np.nan)
# 由于新增了滞后和滚动特征,缺失值会增多,需要合理处理
# 简单做法:删除所有含有NaN的行(会损失较多数据,但保证干净)
data_clean = data.dropna().copy()
print(f"\n清洗后数据量:{len(data_clean)}")

# 特征列表(包含原始价格、成交量)
price_volume_cols = ['Open', 'High', 'Low', 'Close', 'Volume']
tech_cols = ['SMA_10', 'SMA_50', 'EMA_10', 'EMA_50', 'MACD', 'MACD_signal', 'MACD_diff', 'ADX',
             'RSI', 'Stoch_K', 'Stoch_D', 'WilliamsR', 'BB_high', 'BB_low', 'BB_width', 'ATR',
             'OBV', 'CMF', 'HL_pct', 'OC_pct', 'Volume_Change']
lag_cols = [f'return_lag{i}' for i in range(1,6)]
vol_cols = ['vol_5', 'vol_20']
calendar_cols = ['day_of_week', 'month', 'quarter']
features = price_volume_cols + tech_cols + lag_cols + vol_cols + calendar_cols

X = data_clean[features]
y_reg = data_clean['target_reg']
y_cls = data_clean['target_cls']

# 最终检查
assert not X.isnull().any().any()
assert not np.isinf(X.values).any()

# ==================== 5. 时间序列划分(单次训练/测试)====================
split_point = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_point], X.iloc[split_point:]
y_reg_train, y_reg_test = y_reg.iloc[:split_point], y_reg.iloc[split_point:]
y_cls_train, y_cls_test = y_cls.iloc[:split_point], y_cls.iloc[split_point:]

print(f"\n训练集:{X_train.index[0]}{X_train.index[-1]},样本数 {len(X_train)}")
print(f"测试集:{X_test.index[0]}{X_test.index[-1]},样本数 {len(X_test)}")

# ==================== 6. 回归任务 ====================
# 定义评估函数
def reg_evaluate(y_true, y_pred, name):
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    mae = mean_absolute_error(y_true, y_pred)
    r2 = r2_score(y_true, y_pred)
    print(f"\n{name} 回归结果:")
    print(f"RMSE: {rmse:.4f}%")
    print(f"MAE : {mae:.4f}%")
    print(f"R²  : {r2:.4f}")
    return rmse, mae, r2

# 模型1:线性回归
lr = LinearRegression()
lr.fit(X_train, y_reg_train)
y_pred_lr = lr.predict(X_test)
reg_evaluate(y_reg_test, y_pred_lr, "线性回归")

# 模型2:随机森林(调参)
rf = RandomForestRegressor(
    n_estimators=200, max_depth=5, min_samples_split=50,
    min_samples_leaf=20, max_features='sqrt', random_state=42, n_jobs=-1
)
rf.fit(X_train, y_reg_train)
y_pred_rf = rf.predict(X_test)
reg_evaluate(y_reg_test, y_pred_rf, "随机森林")

# 模型3:XGBoost
xgb_model = xgb.XGBRegressor(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42
)
xgb_model.fit(X_train, y_reg_train)
y_pred_xgb = xgb_model.predict(X_test)
reg_evaluate(y_reg_test, y_pred_xgb, "XGBoost")

# 模型4:LightGBM
lgb_model = lgb.LGBMRegressor(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1
)
lgb_model.fit(X_train, y_reg_train)
y_pred_lgb = lgb_model.predict(X_test)
reg_evaluate(y_reg_test, y_pred_lgb, "LightGBM")

# 可视化回归预测对比(取其中一种模型,如随机森林)
plt.figure(figsize=(14,5))
plt.plot(y_reg_test.index, y_reg_test.values, label='Actual Return %', alpha=0.7)
plt.plot(y_reg_test.index, y_pred_rf, label='RF Predicted %', alpha=0.7)
plt.title('Random Forest: Next-Day Return Prediction')
plt.legend()
plt.grid(True)
plt.show()

# ==================== 7. 分类任务(涨跌方向预测)====================
def cls_evaluate(y_true, y_pred_prob, y_pred_label, name):
    acc = accuracy_score(y_true, y_pred_label)
    auc = roc_auc_score(y_true, y_pred_prob)
    print(f"\n{name} 分类结果:")
    print(f"准确率: {acc:.4f}")
    print(f"AUC: {auc:.4f}")
    # 混淆矩阵
    cm = confusion_matrix(y_true, y_pred_label)
    print("混淆矩阵:")
    print(cm)

# 逻辑回归
logit = LogisticRegression(max_iter=1000, random_state=42)
logit.fit(X_train, y_cls_train)
y_pred_logit_prob = logit.predict_proba(X_test)[:, 1]
y_pred_logit_label = logit.predict(X_test)
cls_evaluate(y_cls_test, y_pred_logit_prob, y_pred_logit_label, "逻辑回归")

# 随机森林分类
rf_cls = RandomForestClassifier(
    n_estimators=200, max_depth=5, min_samples_split=50,
    min_samples_leaf=20, max_features='sqrt', random_state=42, n_jobs=-1
)
rf_cls.fit(X_train, y_cls_train)
y_pred_rf_prob = rf_cls.predict_proba(X_test)[:, 1]
y_pred_rf_label = rf_cls.predict(X_test)
cls_evaluate(y_cls_test, y_pred_rf_prob, y_pred_rf_label, "随机森林分类")

# XGBoost分类
xgb_cls = xgb.XGBClassifier(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42, use_label_encoder=False, eval_metric='logloss'
)
xgb_cls.fit(X_train, y_cls_train)
y_pred_xgb_prob = xgb_cls.predict_proba(X_test)[:, 1]
y_pred_xgb_label = xgb_cls.predict(X_test)
cls_evaluate(y_cls_test, y_pred_xgb_prob, y_pred_xgb_label, "XGBoost分类")

# LightGBM分类
lgb_cls = lgb.LGBMClassifier(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1
)
lgb_cls.fit(X_train, y_cls_train)
y_pred_lgb_prob = lgb_cls.predict_proba(X_test)[:, 1]
y_pred_lgb_label = lgb_cls.predict(X_test)
cls_evaluate(y_cls_test, y_pred_lgb_prob, y_pred_lgb_label, "LightGBM分类")

# 绘制AUC曲线(以随机森林为例)
from sklearn.metrics import roc_curve
fpr, tpr, _ = roc_curve(y_cls_test, y_pred_rf_prob)
plt.figure()
plt.plot(fpr, tpr, label=f'RF (AUC = {roc_auc_score(y_cls_test, y_pred_rf_prob):.3f})')
plt.plot([0,1],[0,1],'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve - Random Forest Classifier')
plt.legend()
plt.show()

# ==================== 8. 时间序列交叉验证(更稳健的评估)====================
print("\n===== 时间序列交叉验证 (5折) =====")
tscv = TimeSeriesSplit(n_splits=5)

# 以随机森林回归为例
rf_cv_scores = cross_val_score(rf, X, y_reg, cv=tscv, scoring='neg_mean_squared_error')
rf_cv_rmse = np.sqrt(-rf_cv_scores)
print(f"随机森林回归 CV RMSE: {rf_cv_rmse.mean():.4f} ± {rf_cv_rmse.std():.4f}%")

# 分类以随机森林为例
rf_cls_cv_auc = cross_val_score(rf_cls, X, y_cls, cv=tscv, scoring='roc_auc')
print(f"随机森林分类 CV AUC: {rf_cls_cv_auc.mean():.4f} ± {rf_cls_cv_auc.std():.4f}")

# ==================== 9. 特征重要性分析(以XGBoost回归为例)====================
xgb_importance = pd.DataFrame({'Feature': features, 'Importance': xgb_model.feature_importances_})
xgb_importance = xgb_importance.sort_values('Importance', ascending=False).head(15)
plt.figure(figsize=(10,6))
sns.barplot(data=xgb_importance, x='Importance', y='Feature')
plt.title('XGBoost Regressor Feature Importance')
plt.tight_layout()
plt.show()
Requirement already satisfied: ta in /opt/conda/lib/python3.10/site-packages (0.11.0)
Requirement already satisfied: xgboost in /opt/conda/lib/python3.10/site-packages (2.0.3)
Requirement already satisfied: lightgbm in /opt/conda/lib/python3.10/site-packages (4.2.0)
Requirement already satisfied: numpy in /opt/conda/lib/python3.10/site-packages (from ta) (1.26.4)
Requirement already satisfied: pandas in /opt/conda/lib/python3.10/site-packages (from ta) (2.2.2)
Requirement already satisfied: scipy in /opt/conda/lib/python3.10/site-packages (from xgboost) (1.11.4)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.7 in /opt/conda/lib/python3.10/site-packages (from pandas->ta) (2023.4)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.10/site-packages (from python-dateutil>=2.8.2->pandas->ta) (1.16.0)
数据时间范围:1984-09-07 00:00:00 至 2017-11-10 00:00:00
总记录数:8364

清洗后数据量:8313

训练集:1984-11-15 00:00:00 至 2011-04-04 00:00:00,样本数 6650
测试集:2011-04-05 00:00:00 至 2017-11-09 00:00:00,样本数 1663

线性回归 回归结果:
RMSE: 2.1213%
MAE : 1.6129%
R²  : -0.7766

随机森林 回归结果:
RMSE: 1.5949%
MAE : 1.1367%
R²  : -0.0042

XGBoost 回归结果:
RMSE: 1.6639%
MAE : 1.1826%
R²  : -0.0929

LightGBM 回归结果:
RMSE: 1.5968%
MAE : 1.1413%
R²  : -0.0066


逻辑回归 分类结果:
准确率: 0.5207
AUC: 0.5102
混淆矩阵:
[[  1 796]
 [  1 865]]

随机森林分类 分类结果:
准确率: 0.5232
AUC: 0.5390
混淆矩阵:
[[ 71 726]
 [ 67 799]]

XGBoost分类 分类结果:
准确率: 0.5195
AUC: 0.5306
混淆矩阵:
[[353 444]
 [355 511]]

LightGBM分类 分类结果:
准确率: 0.5394
AUC: 0.5365
混淆矩阵:
[[302 495]
 [271 595]]


===== 时间序列交叉验证 (5折) =====
随机森林回归 CV RMSE: 2.7618 ± 0.8533%
随机森林分类 CV AUC: 0.5066 ± 0.0198

### 更心后的
# ==================== 1. 安装与导入 ====================
!pip install ta xgboost lightgbm --quiet

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from sklearn.metrics import (mean_squared_error, mean_absolute_error, r2_score,
                             accuracy_score, roc_auc_score, confusion_matrix,
                             precision_recall_curve)
import xgboost as xgb
import lightgbm as lgb
import ta

# ==================== 2. 加载数据 ====================
# 请确保路径与你的数据集实际位置一致
df = pd.read_csv('/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/aapl.us.txt')
df['Date'] = pd.to_datetime(df['Date'])
df = df.set_index('Date').sort_index()
print(f"数据时间范围:{df.index.min()}{df.index.max()}")
print(f"总记录数:{len(df)}")

# ==================== 3. 特征工程 ====================
data = df.copy()

# ---------- 基础技术指标 ----------
# 趋势
data['SMA_10'] = ta.trend.sma_indicator(data['Close'], 10)
data['SMA_50'] = ta.trend.sma_indicator(data['Close'], 50)
data['EMA_10'] = ta.trend.ema_indicator(data['Close'], 10)
data['EMA_50'] = ta.trend.ema_indicator(data['Close'], 50)
macd = ta.trend.MACD(data['Close'])
data['MACD'] = macd.macd()
data['MACD_signal'] = macd.macd_signal()
data['MACD_diff'] = macd.macd_diff()
data['ADX'] = ta.trend.ADXIndicator(data['High'], data['Low'], data['Close'], 14).adx()

# 动量
data['RSI'] = ta.momentum.RSIIndicator(data['Close'], 14).rsi()
stoch = ta.momentum.StochasticOscillator(data['High'], data['Low'], data['Close'], 14, 3)
data['Stoch_K'] = stoch.stoch()
data['Stoch_D'] = stoch.stoch_signal()
data['WilliamsR'] = ta.momentum.WilliamsRIndicator(data['High'], data['Low'], data['Close'], 14).williams_r()

# 波动率
bb = ta.volatility.BollingerBands(data['Close'], 20, 2)
data['BB_high'] = bb.bollinger_hband()
data['BB_low'] = bb.bollinger_lband()
data['BB_width'] = bb.bollinger_wband()
data['ATR'] = ta.volatility.AverageTrueRange(data['High'], data['Low'], data['Close'], 14).average_true_range()

# 成交量
data['OBV'] = ta.volume.OnBalanceVolumeIndicator(data['Close'], data['Volume']).on_balance_volume()
data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(data['High'], data['Low'], data['Close'], data['Volume'], 20).chaikin_money_flow()

# 价格衍生
data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
data['Volume_Change'] = data['Volume'].pct_change()

# ---------- 新增有效特征 ----------
# 收益率滞后(过去1~3日)
for lag in range(1, 4):
    data[f'return_lag{lag}'] = data['Close'].pct_change(lag) * 100

# 波动率特征(5日、20日收益率标准差)
data['vol_5'] = data['Close'].pct_change().rolling(5).std() * 100
data['vol_20'] = data['Close'].pct_change().rolling(20).std() * 100

# 日历特征(星期几、月份)
data['day_of_week'] = data.index.dayofweek
data['month'] = data.index.month

# ---------- 构造交叉特征(基于重要特征)----------
data['OC_pct_vol'] = data['OC_pct'] * data['Volume_Change']          # 涨跌幅与成交量变化交互
data['HL_pct_vol'] = data['HL_pct'] * data['Volume_Change']          # 振幅与成交量变化交互
data['RSI_OC'] = data['RSI'] * data['OC_pct']                        # RSI与涨跌幅

# ---------- 定义预测目标 ----------
# 回归目标:次日收益率 (%)
data['target_reg'] = data['Close'].pct_change().shift(-1) * 100
# 分类目标:次日涨跌(1=涨,0=跌或平)
data['target_cls'] = (data['Close'].shift(-1) > data['Close']).astype(int)

# ==================== 4. 处理缺失值与无穷大 ====================
data = data.replace([np.inf, -np.inf], np.nan)

# 查看缺失情况
missing = data.isnull().sum()
print("\n各列缺失值数量:")
print(missing[missing > 0].sort_values(ascending=False))

# 删除所有含NaN的行
data_clean = data.dropna().copy()
print(f"\n清洗后数据量:{len(data_clean)}")

# 定义特征列(根据重要性保留,可自行调整)
feature_cols = [
    'Open', 'High', 'Low', 'Close', 'Volume',
    'SMA_10', 'SMA_50', 'EMA_10', 'EMA_50', 'MACD', 'MACD_signal', 'ADX',
    'RSI', 'Stoch_K', 'Stoch_D', 'WilliamsR',
    'BB_width', 'ATR',
    'OBV', 'CMF',
    'HL_pct', 'OC_pct', 'Volume_Change',
    'return_lag1', 'return_lag2', 'return_lag3',
    'vol_5', 'vol_20',
    'day_of_week', 'month',
    'OC_pct_vol', 'HL_pct_vol', 'RSI_OC'
]

X = data_clean[feature_cols]
y_reg = data_clean['target_reg']
y_cls = data_clean['target_cls']

# 最终检查
assert not X.isnull().any().any(), "特征中存在NaN"
assert not np.isinf(X.values).any(), "特征中存在无穷大"

# ==================== 5. 时间序列划分(单次训练/测试)====================
split_point = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_point], X.iloc[split_point:]
y_reg_train, y_reg_test = y_reg.iloc[:split_point], y_reg.iloc[split_point:]
y_cls_train, y_cls_test = y_cls.iloc[:split_point], y_cls.iloc[split_point:]

print(f"\n训练集:{X_train.index[0]}{X_train.index[-1]},样本数 {len(X_train)}")
print(f"测试集:{X_test.index[0]}{X_test.index[-1]},样本数 {len(X_test)}")

# ==================== 6. 回归任务 ====================
def reg_evaluate(y_true, y_pred, name):
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    mae = mean_absolute_error(y_true, y_pred)
    r2 = r2_score(y_true, y_pred)
    print(f"\n{name} 回归结果:")
    print(f"RMSE: {rmse:.4f}%")
    print(f"MAE : {mae:.4f}%")
    print(f"R²  : {r2:.4f}")
    return rmse, mae, r2

# 线性回归
lr = LinearRegression()
lr.fit(X_train, y_reg_train)
y_pred_lr = lr.predict(X_test)
reg_evaluate(y_reg_test, y_pred_lr, "线性回归")

# 随机森林(调参)
rf = RandomForestRegressor(
    n_estimators=200, max_depth=5, min_samples_split=50,
    min_samples_leaf=20, max_features='sqrt', random_state=42, n_jobs=-1
)
rf.fit(X_train, y_reg_train)
y_pred_rf = rf.predict(X_test)
reg_evaluate(y_reg_test, y_pred_rf, "随机森林")

# XGBoost
xgb_reg = xgb.XGBRegressor(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42
)
xgb_reg.fit(X_train, y_reg_train)
y_pred_xgb = xgb_reg.predict(X_test)
reg_evaluate(y_reg_test, y_pred_xgb, "XGBoost")

# LightGBM
lgb_reg = lgb.LGBMRegressor(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1
)
lgb_reg.fit(X_train, y_reg_train)
y_pred_lgb = lgb_reg.predict(X_test)
reg_evaluate(y_reg_test, y_pred_lgb, "LightGBM")

# ==================== 7. 分类任务 ====================
def cls_evaluate(y_true, y_pred_prob, y_pred_label, name):
    acc = accuracy_score(y_true, y_pred_label)
    auc = roc_auc_score(y_true, y_pred_prob)
    print(f"\n{name} 分类结果:")
    print(f"准确率: {acc:.4f}")
    print(f"AUC: {auc:.4f}")
    print("混淆矩阵:")
    print(confusion_matrix(y_true, y_pred_label))
    return acc, auc

# 逻辑回归
logit = LogisticRegression(max_iter=1000, random_state=42)
logit.fit(X_train, y_cls_train)
y_pred_logit_prob = logit.predict_proba(X_test)[:, 1]
y_pred_logit_label = logit.predict(X_test)
cls_evaluate(y_cls_test, y_pred_logit_prob, y_pred_logit_label, "逻辑回归")

# 随机森林分类
rf_cls = RandomForestClassifier(
    n_estimators=200, max_depth=5, min_samples_split=50,
    min_samples_leaf=20, max_features='sqrt', random_state=42, n_jobs=-1
)
rf_cls.fit(X_train, y_cls_train)
y_pred_rf_prob = rf_cls.predict_proba(X_test)[:, 1]
y_pred_rf_label = rf_cls.predict(X_test)
cls_evaluate(y_cls_test, y_pred_rf_prob, y_pred_rf_label, "随机森林分类")

# XGBoost分类
xgb_cls = xgb.XGBClassifier(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42,
    use_label_encoder=False, eval_metric='logloss'
)
xgb_cls.fit(X_train, y_cls_train)
y_pred_xgb_prob = xgb_cls.predict_proba(X_test)[:, 1]
y_pred_xgb_label = xgb_cls.predict(X_test)
cls_evaluate(y_cls_test, y_pred_xgb_prob, y_pred_xgb_label, "XGBoost分类")

# LightGBM分类
lgb_cls = lgb.LGBMClassifier(
    n_estimators=200, max_depth=3, learning_rate=0.05,
    subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1
)
lgb_cls.fit(X_train, y_cls_train)
y_pred_lgb_prob = lgb_cls.predict_proba(X_test)[:, 1]
y_pred_lgb_label = lgb_cls.predict(X_test)
cls_evaluate(y_cls_test, y_pred_lgb_prob, y_pred_lgb_label, "LightGBM分类")

# ==================== 8. 时间序列交叉验证(稳健性评估)====================
print("\n===== 时间序列交叉验证 (5折) =====")
tscv = TimeSeriesSplit(n_splits=5)

# 回归交叉验证(以LightGBM为例,因为它表现相对较好)
lgb_reg_cv_scores = cross_val_score(lgb_reg, X, y_reg, cv=tscv, scoring='neg_mean_squared_error')
lgb_reg_cv_rmse = np.sqrt(-lgb_reg_cv_scores)
print(f"LightGBM回归 CV RMSE: {lgb_reg_cv_rmse.mean():.4f} ± {lgb_reg_cv_rmse.std():.4f}%")

# 分类交叉验证(以LightGBM分类为例)
lgb_cls_cv_auc = cross_val_score(lgb_cls, X, y_cls, cv=tscv, scoring='roc_auc')
print(f"LightGBM分类 CV AUC: {lgb_cls_cv_auc.mean():.4f} ± {lgb_cls_cv_auc.std():.4f}")

# ==================== 9. 分类阈值优化与策略回测 ====================
# 使用LightGBM的预测概率(因为它准确率略高)
# 在验证集(这里用测试集代替,实际应另设验证集)上寻找最佳阈值
prec, recall, thresh = precision_recall_curve(y_cls_test, y_pred_lgb_prob)
f1_scores = 2 * (prec * recall) / (prec + recall + 1e-10)
best_idx = np.argmax(f1_scores[:-1])  # 去掉最后一个无效值
best_thresh = thresh[best_idx]
print(f"\n最佳分类阈值(基于F1): {best_thresh:.4f}")

# 根据最佳阈值重新预测标签
y_pred_opt = (y_pred_lgb_prob >= best_thresh).astype(int)
acc_opt = accuracy_score(y_cls_test, y_pred_opt)
print(f"优化阈值后准确率: {acc_opt:.4f}")

# ----- 简单策略回测 -----
# 策略:当预测上涨概率 >= 阈值时,次日开盘买入,收盘卖出(假设T+0,无手续费简化)
# 计算每日收益率
test_df = X_test.copy()
test_df['actual_return'] = y_reg_test.values  # 次日真实收益率
test_df['pred_prob'] = y_pred_lgb_prob
test_df['signal'] = (test_df['pred_prob'] >= best_thresh).astype(int)

# 策略收益率:当信号为1时,获得实际收益率;信号为0时,收益率为0(空仓)
test_df['strategy_return'] = test_df['signal'] * test_df['actual_return']

# 计算累计净值
test_df['strategy_cum'] = (1 + test_df['strategy_return']/100).cumprod()
test_df['buy_hold_cum'] = (1 + test_df['actual_return']/100).cumprod()

# 评估策略
total_return = (test_df['strategy_cum'].iloc[-1] - 1) * 100
buy_hold_return = (test_df['buy_hold_cum'].iloc[-1] - 1) * 100
sharpe = test_df['strategy_return'].mean() / test_df['strategy_return'].std() * np.sqrt(252)  # 年化夏普(假设252个交易日)
print(f"\n策略总收益率: {total_return:.2f}%")
print(f"买入并持有收益率: {buy_hold_return:.2f}%")
print(f"策略夏普比率(年化): {sharpe:.2f}")

# 绘制净值曲线
plt.figure(figsize=(12,5))
plt.plot(test_df.index, test_df['strategy_cum'], label='Strategy', linewidth=1)
plt.plot(test_df.index, test_df['buy_hold_cum'], label='Buy & Hold', linewidth=1, alpha=0.7)
plt.title('Strategy vs Buy & Hold (Test Set)')
plt.xlabel('Date')
plt.ylabel('Cumulative Return')
plt.legend()
plt.grid(True, linestyle='--')
plt.show()

# ==================== 10. 特征重要性可视化 ====================
# 以LightGBM分类器为例
lgb_importance = pd.DataFrame({'Feature': feature_cols, 'Importance': lgb_cls.booster_.feature_importance(importance_type='gain')})
lgb_importance = lgb_importance.sort_values('Importance', ascending=False).head(15)
plt.figure(figsize=(10,6))
sns.barplot(data=lgb_importance, x='Importance', y='Feature')
plt.title('LightGBM Classifier Feature Importance (Gain)')
plt.tight_layout()
plt.show()

print("\n===== 分析完成 =====")
数据时间范围:1984-09-07 00:00:00 至 2017-11-10 00:00:00
总记录数:8364

各列缺失值数量:
EMA_50           49
SMA_50           49
MACD_signal      33
MACD_diff        33
MACD             25
vol_20           20
BB_low           19
BB_high          19
BB_width         19
CMF              19
Stoch_D          15
RSI_OC           13
RSI              13
Stoch_K          13
WilliamsR        13
SMA_10            9
EMA_10            9
vol_5             5
return_lag3       3
return_lag2       2
Volume_Change     2
OC_pct_vol        2
HL_pct_vol        2
return_lag1       1
target_reg        1
dtype: int64

清洗后数据量:8313

训练集:1984-11-15 00:00:00 至 2011-04-04 00:00:00,样本数 6650
测试集:2011-04-05 00:00:00 至 2017-11-09 00:00:00,样本数 1663

线性回归 回归结果:
RMSE: 1.8927%
MAE : 1.4184%
R²  : -0.4142

随机森林 回归结果:
RMSE: 1.5957%
MAE : 1.1381%
R²  : -0.0052

XGBoost 回归结果:
RMSE: 1.5996%
MAE : 1.1418%
R²  : -0.0101

LightGBM 回归结果:
RMSE: 1.5868%
MAE : 1.1388%
R²  : 0.0059

逻辑回归 分类结果:
准确率: 0.5207
AUC: 0.5102
混淆矩阵:
[[  1 796]
 [  1 865]]

随机森林分类 分类结果:
准确率: 0.5256
AUC: 0.5285
混淆矩阵:
[[ 82 715]
 [ 74 792]]

XGBoost分类 分类结果:
准确率: 0.5195
AUC: 0.5241
混淆矩阵:
[[459 338]
 [461 405]]

LightGBM分类 分类结果:
准确率: 0.5141
AUC: 0.5205
混淆矩阵:
[[259 538]
 [270 596]]

===== 时间序列交叉验证 (5折) =====
LightGBM回归 CV RMSE: 2.8620 ± 0.9159%
LightGBM分类 CV AUC: 0.5078 ± 0.0122

最佳分类阈值(基于F1): 0.3879
优化阈值后准确率: 0.5244

策略总收益率: 341.67%
买入并持有收益率: 302.46%
策略夏普比率(年化): 1.02


===== 分析完成 =====

1. 回归模型表现

LightGBM回归的R²为0.0059,虽为正但极小,说明模型能解释的方差仅0.59%,预测能力依然微弱。RMSE为1.59%,意味着预测次日收益率的平均误差##### 为1.59个百分点,相比股价波动(日均波动约1-2%)尚可接受,但不足以作为可靠交易信号。
交叉验证的RMSE高达2.86%,且标准差0.92%,说明模型在不同时间段的预测误差波动极大,泛化能力差。单次测试集上的良好表现(R²≈0)很可能是特定市场环境(2011-2017牛市)下的偶然结果。

2. 分类模型表现

最高准确率仅52.56%(随机森林),AUC 0.5285,略高于随机猜测。但交叉验证AUC为0.5078±0.0122,进一步证明模型的区分能力极不稳定,几乎接##### 近随机。
逻辑回归依然几乎全部预测为涨,说明线性模型完全失效。

3. 策略回测结果:亮眼但需警惕

策略总收益率341.67% vs 买入并持有302.46%,超额收益约39%,夏普比率1.02(年化),看似不错。

但必须警惕以下陷阱:

过拟合风险:阈值优化(0.3879)是在测试集上进行的,这引入了未来信息,相当于用测试集调参。正确做法应再划分验证集,或使用交叉验证选择阈值。
数据窥探:回测区间正好是美股长牛(2011-2017),任何多头策略都可能盈利。策略是否能在下跌或震荡市场中有效?未经验证。
交易成本与滑点:策略假设T+0无手续费,实际操作中单边成本0.1%就会显著侵蚀收益(尤其交易频繁时)。
幸存者偏差:仅用苹果一只股票回测,不具代表性。

筛选了以下10只具有代表性、行业分散、市值梯度合理的股票:

核心科技巨头(4只)
AAPL(苹果) - 你已分析过的基准,信息技术,市值3.8T
MSFT(微软) - 另一科技巨头,可对比AAPL的预测难度
NVDA(英伟达) - 高波动性、高成长性代表,市值4.4T
GOOGL(谷歌) - 通信服务龙头,与AAPL、MSFT形成”科技三巨头”对比
消费与零售(2只)
AMZN(亚马逊) - 消费周期性与科技结合,高波动
WMT(沃尔玛) - 消费防御性代表,波动相对稳定
金融与医疗(2只)
JPM(摩根大通) - 金融业龙头,与科技股形成行业对比
JNJ(强生) - 医疗保健代表,防御性特征
通信与半导体(2只)
TMUS(T-Mobile) - 通信服务,相比科技股波动性较低
AMD(超微半导体) - 半导体高波动代表,与NVDA形成对比
# ==================== 1. 安装与导入 ====================
!pip install ta xgboost lightgbm --quiet

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
import os
import gc
warnings.filterwarnings('ignore')

from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from sklearn.metrics import (mean_squared_error, mean_absolute_error, r2_score,
                             accuracy_score, roc_auc_score, confusion_matrix,
                             precision_recall_curve)
import xgboost as xgb
import lightgbm as lgb
import ta

# ==================== 2. 配置参数 ====================
# 股票列表(基于之前的推荐)
TICKERS = ['AAPL', 'MSFT', 'NVDA', 'GOOGL', 'AMZN', 'WMT', 'JPM', 'JNJ', 'TMUS', 'AMD']
# 数据路径(请根据实际路径调整)
DATA_PATH = '/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/'
# 输出结果存储
results = {}

# ==================== 3. 定义特征工程函数 ====================
def engineer_features(df):
    """对单个股票的DataFrame进行特征工程,返回处理后的DataFrame"""
    data = df.copy()
    
    # ----- 趋势指标 -----
    data['SMA_10'] = ta.trend.sma_indicator(data['Close'], 10)
    data['SMA_50'] = ta.trend.sma_indicator(data['Close'], 50)
    data['EMA_10'] = ta.trend.ema_indicator(data['Close'], 10)
    data['EMA_50'] = ta.trend.ema_indicator(data['Close'], 50)
    macd = ta.trend.MACD(data['Close'])
    data['MACD'] = macd.macd()
    data['MACD_signal'] = macd.macd_signal()
    data['MACD_diff'] = macd.macd_diff()
    data['ADX'] = ta.trend.ADXIndicator(data['High'], data['Low'], data['Close'], 14).adx()
    
    # ----- 动量指标 -----
    data['RSI'] = ta.momentum.RSIIndicator(data['Close'], 14).rsi()
    stoch = ta.momentum.StochasticOscillator(data['High'], data['Low'], data['Close'], 14, 3)
    data['Stoch_K'] = stoch.stoch()
    data['Stoch_D'] = stoch.stoch_signal()
    data['WilliamsR'] = ta.momentum.WilliamsRIndicator(data['High'], data['Low'], data['Close'], 14).williams_r()
    
    # ----- 波动率指标 -----
    bb = ta.volatility.BollingerBands(data['Close'], 20, 2)
    data['BB_high'] = bb.bollinger_hband()
    data['BB_low'] = bb.bollinger_lband()
    data['BB_width'] = bb.bollinger_wband()
    data['ATR'] = ta.volatility.AverageTrueRange(data['High'], data['Low'], data['Close'], 14).average_true_range()
    
    # ----- 成交量指标 -----
    data['OBV'] = ta.volume.OnBalanceVolumeIndicator(data['Close'], data['Volume']).on_balance_volume()
    data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(data['High'], data['Low'], data['Close'], data['Volume'], 20).chaikin_money_flow()
    
    # ----- 价格衍生特征 -----
    data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
    data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
    data['Volume_Change'] = data['Volume'].pct_change()
    
    # ----- 收益率滞后 -----
    for lag in range(1, 4):
        data[f'return_lag{lag}'] = data['Close'].pct_change(lag) * 100
    
    # ----- 波动率特征 -----
    data['vol_5'] = data['Close'].pct_change().rolling(5).std() * 100
    data['vol_20'] = data['Close'].pct_change().rolling(20).std() * 100
    
    # ----- 日历特征 -----
    data['day_of_week'] = data.index.dayofweek
    data['month'] = data.index.month
    
    # ----- 交叉特征 -----
    data['OC_pct_vol'] = data['OC_pct'] * data['Volume_Change']
    data['HL_pct_vol'] = data['HL_pct'] * data['Volume_Change']
    data['RSI_OC'] = data['RSI'] * data['OC_pct']
    
    # ----- 目标变量 -----
    data['target_reg'] = data['Close'].pct_change().shift(-1) * 100
    data['target_cls'] = (data['Close'].shift(-1) > data['Close']).astype(int)
    
    return data

# ==================== 4. 定义模型训练与评估函数 ====================
def train_evaluate_stock(ticker, df):
    """对单只股票执行完整的训练、评估和回测"""
    print(f"\n{'='*60}\n开始处理: {ticker}\n{'='*60}")
    
    # 特征工程
    data = engineer_features(df)
    
    # 处理缺失值
    data = data.replace([np.inf, -np.inf], np.nan)
    initial_len = len(data)
    data_clean = data.dropna().copy()
    print(f"清洗前样本数: {initial_len}, 清洗后: {len(data_clean)}")
    
    # 定义特征列
    feature_cols = [
        'Open', 'High', 'Low', 'Close', 'Volume',
        'SMA_10', 'SMA_50', 'EMA_10', 'EMA_50', 'MACD', 'MACD_signal', 'ADX',
        'RSI', 'Stoch_K', 'Stoch_D', 'WilliamsR',
        'BB_width', 'ATR',
        'OBV', 'CMF',
        'HL_pct', 'OC_pct', 'Volume_Change',
        'return_lag1', 'return_lag2', 'return_lag3',
        'vol_5', 'vol_20',
        'day_of_week', 'month',
        'OC_pct_vol', 'HL_pct_vol', 'RSI_OC'
    ]
    
    X = data_clean[feature_cols]
    y_reg = data_clean['target_reg']
    y_cls = data_clean['target_cls']
    
    # 时间序列划分(80%训练,20%测试)
    split_point = int(len(X) * 0.8)
    X_train, X_test = X.iloc[:split_point], X.iloc[split_point:]
    y_reg_train, y_reg_test = y_reg.iloc[:split_point], y_reg.iloc[split_point:]
    y_cls_train, y_cls_test = y_cls.iloc[:split_point], y_cls.iloc[split_point:]
    
    print(f"训练集: {X_train.index[0]}{X_train.index[-1]}, 样本数 {len(X_train)}")
    print(f"测试集: {X_test.index[0]}{X_test.index[-1]}, 样本数 {len(X_test)}")
    
    # ========== 回归模型 ==========
    # LightGBM回归(作为代表)
    lgb_reg = lgb.LGBMRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05,
        subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1
    )
    lgb_reg.fit(X_train, y_reg_train)
    y_pred_reg = lgb_reg.predict(X_test)
    rmse = np.sqrt(mean_squared_error(y_reg_test, y_pred_reg))
    r2 = r2_score(y_reg_test, y_pred_reg)
    print(f"LightGBM回归 RMSE: {rmse:.4f}%, R²: {r2:.4f}")
    
    # 时间序列交叉验证(5折)
    tscv = TimeSeriesSplit(n_splits=5)
    cv_scores = cross_val_score(lgb_reg, X, y_reg, cv=tscv, scoring='neg_mean_squared_error')
    cv_rmse = np.sqrt(-cv_scores)
    print(f"交叉验证 RMSE: {cv_rmse.mean():.4f} ± {cv_rmse.std():.4f}%")
    
    # ========== 分类模型 ==========
    # LightGBM分类
    lgb_cls = lgb.LGBMClassifier(
        n_estimators=200, max_depth=3, learning_rate=0.05,
        subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1
    )
    lgb_cls.fit(X_train, y_cls_train)
    y_pred_prob = lgb_cls.predict_proba(X_test)[:, 1]
    y_pred_label = lgb_cls.predict(X_test)
    acc = accuracy_score(y_cls_test, y_pred_label)
    auc = roc_auc_score(y_cls_test, y_pred_prob)
    print(f"LightGBM分类 准确率: {acc:.4f}, AUC: {auc:.4f}")
    
    # 分类交叉验证
    cv_auc = cross_val_score(lgb_cls, X, y_cls, cv=tscv, scoring='roc_auc')
    print(f"分类CV AUC: {cv_auc.mean():.4f} ± {cv_auc.std():.4f}")
    
    # ========== 阈值优化与策略回测 ==========
    # 在测试集上寻找最佳阈值(注意:这里仅用于演示,实际应使用验证集)
    precision, recall, thresholds = precision_recall_curve(y_cls_test, y_pred_prob)
    f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
    best_idx = np.argmax(f1_scores[:-1])
    best_thresh = thresholds[best_idx]
    print(f"最佳阈值: {best_thresh:.4f}")
    
    # 根据最佳阈值生成信号
    test_df = X_test.copy()
    test_df['actual_return'] = y_reg_test.values
    test_df['pred_prob'] = y_pred_prob
    test_df['signal'] = (test_df['pred_prob'] >= best_thresh).astype(int)
    
    # 策略收益(假设T+0,次日开盘买入收盘卖出,无交易成本)
    test_df['strategy_return'] = test_df['signal'] * test_df['actual_return']
    
    # 计算累计净值
    test_df['strategy_cum'] = (1 + test_df['strategy_return']/100).cumprod()
    test_df['buy_hold_cum'] = (1 + test_df['actual_return']/100).cumprod()
    
    total_return = (test_df['strategy_cum'].iloc[-1] - 1) * 100
    buy_hold_return = (test_df['buy_hold_cum'].iloc[-1] - 1) * 100
    sharpe = test_df['strategy_return'].mean() / test_df['strategy_return'].std() * np.sqrt(252)
    
    print(f"策略总收益率: {total_return:.2f}%")
    print(f"买入持有收益率: {buy_hold_return:.2f}%")
    print(f"策略夏普比率: {sharpe:.2f}")
    
    # 返回关键指标
    return {
        'ticker': ticker,
        'reg_rmse': rmse,
        'reg_r2': r2,
        'cv_rmse_mean': cv_rmse.mean(),
        'cv_rmse_std': cv_rmse.std(),
        'cls_acc': acc,
        'cls_auc': auc,
        'cv_auc_mean': cv_auc.mean(),
        'cv_auc_std': cv_auc.std(),
        'best_thresh': best_thresh,
        'strategy_return': total_return,
        'buy_hold_return': buy_hold_return,
        'sharpe': sharpe
    }

# ==================== 5. 主循环:遍历股票 ====================
for ticker in TICKERS:
    # 构建文件路径(注意文件名可能为小写)
    file_path = os.path.join(DATA_PATH, f"{ticker.lower()}.us.txt")
    
    if not os.path.exists(file_path):
        print(f"警告: 文件 {file_path} 不存在,跳过 {ticker}")
        continue
    
    try:
        # 读取数据
        df = pd.read_csv(file_path)
        df['Date'] = pd.to_datetime(df['Date'])
        df = df.set_index('Date').sort_index()
        
        # 检查数据长度
        if len(df) < 500:
            print(f"警告: {ticker} 数据量不足 ({len(df)} 行),跳过")
            continue
        
        # 训练评估
        result = train_evaluate_stock(ticker, df)
        results[ticker] = result
        
        # 释放内存
        del df
        gc.collect()
        
    except Exception as e:
        print(f"处理 {ticker} 时发生错误: {e}")
        results[ticker] = None

# ==================== 6. 结果汇总与可视化 ====================
# 转换为DataFrame
summary = pd.DataFrame(results).T
summary = summary.dropna()  # 移除失败的股票

print("\n\n========== 最终结果汇总 ==========")
print(summary.to_string())

# 绘制对比图
fig, axes = plt.subplots(2, 3, figsize=(18, 10))

# 回归RMSE
ax = axes[0,0]
summary['reg_rmse'].sort_values().plot(kind='bar', ax=ax, color='skyblue')
ax.set_title('回归RMSE (越低越好)')
ax.set_ylabel('RMSE (%)')
ax.axhline(y=summary['reg_rmse'].mean(), color='red', linestyle='--', label='均值')
ax.legend()

# 回归R²
ax = axes[0,1]
summary['reg_r2'].sort_values(ascending=False).plot(kind='bar', ax=ax, color='lightgreen')
ax.set_title('回归R² (越高越好)')
ax.set_ylabel('R²')
ax.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)

# 分类AUC
ax = axes[0,2]
summary['cls_auc'].sort_values(ascending=False).plot(kind='bar', ax=ax, color='coral')
ax.set_title('分类AUC (越高越好)')
ax.set_ylabel('AUC')
ax.axhline(y=0.5, color='red', linestyle='--', label='随机')
ax.axhline(y=summary['cls_auc'].mean(), color='blue', linestyle='--', label='均值')
ax.legend()

# 交叉验证RMSE(带误差线)
ax = axes[1,0]
x_pos = np.arange(len(summary))
ax.bar(x_pos, summary['cv_rmse_mean'], yerr=summary['cv_rmse_std'], capsize=5, color='lightblue')
ax.set_xticks(x_pos)
ax.set_xticklabels(summary.index, rotation=45)
ax.set_title('交叉验证RMSE (均值±标准差)')
ax.set_ylabel('RMSE (%)')

# 策略收益率对比
ax = axes[1,1]
summary[['strategy_return', 'buy_hold_return']].plot(kind='bar', ax=ax)
ax.set_title('策略 vs 买入持有收益率')
ax.set_ylabel('总收益率 (%)')
ax.legend()

# 夏普比率
ax = axes[1,2]
summary['sharpe'].sort_values(ascending=False).plot(kind='bar', ax=ax, color='purple')
ax.set_title('策略夏普比率 (年化)')
ax.set_ylabel('夏普比率')
ax.axhline(y=1, color='gray', linestyle='--', label='优秀线')
ax.axhline(y=0, color='black', linewidth=0.5)
ax.legend()

plt.tight_layout()
plt.show()

# 保存结果到CSV(可选)
summary.to_csv('multi_stock_results.csv', index=True)
print("\n结果已保存至 multi_stock_results.csv")

============================================================
开始处理: AAPL
============================================================
清洗前样本数: 8364, 清洗后: 8313
训练集: 1984-11-15 00:00:00 至 2011-04-04 00:00:00, 样本数 6650
测试集: 2011-04-05 00:00:00 至 2017-11-09 00:00:00, 样本数 1663
LightGBM回归 RMSE: 1.5868%, R²: 0.0059
交叉验证 RMSE: 2.8620 ± 0.9159%
LightGBM分类 准确率: 0.5141, AUC: 0.5205
分类CV AUC: 0.5078 ± 0.0122
最佳阈值: 0.3879
策略总收益率: 341.67%
买入持有收益率: 302.46%
策略夏普比率: 1.02

============================================================
开始处理: MSFT
============================================================
清洗前样本数: 7983, 清洗后: 7921
训练集: 1986-06-02 00:00:00 至 2011-07-26 00:00:00, 样本数 6336
测试集: 2011-07-27 00:00:00 至 2017-11-09 00:00:00, 样本数 1585
LightGBM回归 RMSE: 1.4643%, R²: -0.0527
交叉验证 RMSE: 1.9730 ± 0.4489%
LightGBM分类 准确率: 0.4984, AUC: 0.4891
分类CV AUC: 0.5217 ± 0.0146
最佳阈值: 0.2145
策略总收益率: 267.38%
买入持有收益率: 263.58%
策略夏普比率: 1.03

============================================================
开始处理: NVDA
============================================================
清洗前样本数: 4733, 清洗后: 4682
训练集: 1999-04-05 00:00:00 至 2014-02-24 00:00:00, 样本数 3745
测试集: 2014-02-25 00:00:00 至 2017-11-09 00:00:00, 样本数 937
LightGBM回归 RMSE: 2.5078%, R²: -0.0894
交叉验证 RMSE: 3.3465 ± 1.0005%
LightGBM分类 准确率: 0.4472, AUC: 0.4782
分类CV AUC: 0.4966 ± 0.0294
最佳阈值: 0.0920
策略总收益率: 1103.25%
买入持有收益率: 1103.25%
策略夏普比率: 1.94

============================================================
开始处理: GOOGL
============================================================
清洗前样本数: 3333, 清洗后: 3282
训练集: 2004-10-28 00:00:00 至 2015-04-06 00:00:00, 样本数 2625
测试集: 2015-04-07 00:00:00 至 2017-11-09 00:00:00, 样本数 657
LightGBM回归 RMSE: 1.4838%, R²: -0.0993
交叉验证 RMSE: 1.8065 ± 0.5241%
LightGBM分类 准确率: 0.4795, AUC: 0.5074
分类CV AUC: 0.5129 ± 0.0133
最佳阈值: 0.0615
策略总收益率: 103.94%
买入持有收益率: 91.64%
策略夏普比率: 1.33

============================================================
开始处理: AMZN
============================================================
清洗前样本数: 5153, 清洗后: 5102
训练集: 1997-07-28 00:00:00 至 2013-10-22 00:00:00, 样本数 4081
测试集: 2013-10-23 00:00:00 至 2017-11-09 00:00:00, 样本数 1021
LightGBM回归 RMSE: 2.0857%, R²: -0.2258
交叉验证 RMSE: 3.1546 ± 1.1939%
LightGBM分类 准确率: 0.5328, AUC: 0.5189
分类CV AUC: 0.5207 ± 0.0154
最佳阈值: 0.3183
策略总收益率: 252.31%
买入持有收益率: 244.40%
策略夏普比率: 1.19

============================================================
开始处理: WMT
============================================================
清洗前样本数: 11443, 清洗后: 10589
训练集: 1972-08-24 00:00:00 至 2009-06-12 00:00:00, 样本数 8471
测试集: 2009-06-15 00:00:00 至 2017-11-09 00:00:00, 样本数 2118
LightGBM回归 RMSE: 1.0590%, R²: -0.1017
交叉验证 RMSE: 1.6933 ± 0.4399%
LightGBM分类 准确率: 0.4816, AUC: 0.5074
分类CV AUC: 0.5091 ± 0.0123
最佳阈值: 0.0645
策略总收益率: 122.50%
买入持有收益率: 122.50%
策略夏普比率: 0.67

============================================================
开始处理: JPM
============================================================
清洗前样本数: 12074, 清洗后: 12023
训练集: 1970-03-13 00:00:00 至 2008-04-23 00:00:00, 样本数 9618
测试集: 2008-04-24 00:00:00 至 2017-11-09 00:00:00, 样本数 2405
LightGBM回归 RMSE: 2.9323%, R²: -0.1410
交叉验证 RMSE: 2.2903 ± 0.5248%
LightGBM分类 准确率: 0.4969, AUC: 0.4972
分类CV AUC: 0.5137 ± 0.0166
最佳阈值: 0.0945
策略总收益率: 143.06%
买入持有收益率: 143.06%
策略夏普比率: 0.43

============================================================
开始处理: JNJ
============================================================
清洗前样本数: 12074, 清洗后: 12023
训练集: 1970-03-13 00:00:00 至 2008-04-23 00:00:00, 样本数 9618
测试集: 2008-04-24 00:00:00 至 2017-11-09 00:00:00, 样本数 2405
LightGBM回归 RMSE: 1.1115%, R²: -0.1326
交叉验证 RMSE: 1.4784 ± 0.3205%
LightGBM分类 准确率: 0.5040, AUC: 0.4926
分类CV AUC: 0.5011 ± 0.0137
最佳阈值: 0.1417
策略总收益率: 153.21%
买入持有收益率: 153.21%
策略夏普比率: 0.67

============================================================
开始处理: TMUS
============================================================
清洗前样本数: 2663, 清洗后: 2613
训练集: 2007-06-27 00:00:00 至 2015-10-14 00:00:00, 样本数 2090
测试集: 2015-10-15 00:00:00 至 2017-11-09 00:00:00, 样本数 523
LightGBM回归 RMSE: 1.9029%, R²: -0.2878
交叉验证 RMSE: 3.0977 ± 1.2020%
LightGBM分类 准确率: 0.4589, AUC: 0.4521
分类CV AUC: 0.4972 ± 0.0449
最佳阈值: 0.1838
策略总收益率: 42.21%
买入持有收益率: 42.21%
策略夏普比率: 0.77

============================================================
开始处理: AMD
============================================================
清洗前样本数: 8737, 清洗后: 8686
训练集: 1983-05-31 00:00:00 至 2010-12-15 00:00:00, 样本数 6948
测试集: 2010-12-16 00:00:00 至 2017-11-09 00:00:00, 样本数 1738
LightGBM回归 RMSE: 4.0162%, R²: -0.1806
交叉验证 RMSE: 4.0546 ± 0.2687%
LightGBM分类 准确率: 0.5132, AUC: 0.5061
分类CV AUC: 0.5053 ± 0.0115
最佳阈值: 0.3463
策略总收益率: 52.91%
买入持有收益率: 40.22%
策略夏普比率: 0.39


========== 最终结果汇总 ==========
      ticker  reg_rmse    reg_r2 cv_rmse_mean cv_rmse_std   cls_acc   cls_auc cv_auc_mean cv_auc_std best_thresh strategy_return buy_hold_return    sharpe
AAPL    AAPL  1.586838  0.005891     2.861964    0.915915  0.514131  0.520536    0.507783   0.012228    0.387885      341.669549      302.456165  1.019973
MSFT    MSFT   1.46434  -0.05269     1.973018    0.448926  0.498423  0.489126    0.521715    0.01456     0.21448      267.382178       263.57725  1.026538
NVDA    NVDA  2.507825 -0.089403     3.346483    1.000459  0.447172  0.478226    0.496571    0.02942    0.092035     1103.251127     1103.251127   1.93821
GOOGL  GOOGL  1.483759 -0.099336     1.806505    0.524077  0.479452  0.507421    0.512853   0.013283    0.061541      103.940663       91.636384  1.333386
AMZN    AMZN  2.085658 -0.225839     3.154608     1.19393  0.532811  0.518864    0.520663   0.015386    0.318325      252.306055      244.396499  1.189388
WMT      WMT  1.058981 -0.101684     1.693258    0.439903  0.481586  0.507445     0.50906   0.012266    0.064487       122.49955       122.49955  0.674327
JPM      JPM  2.932303 -0.140997     2.290324    0.524762  0.496881  0.497157    0.513728   0.016642    0.094528      143.056102      143.056102  0.428834
JNJ      JNJ  1.111509 -0.132645     1.478422    0.320513   0.50395  0.492585    0.501132   0.013718     0.14166      153.205579      153.205579  0.669661
TMUS    TMUS  1.902894 -0.287835     3.097713    1.202003  0.458891  0.452059    0.497189   0.044927    0.183773        42.21332        42.21332  0.770155
AMD      AMD  4.016171 -0.180614     4.054623    0.268679  0.513234  0.506115    0.505273   0.011498    0.346317       52.911201       40.224159  0.391307


结果已保存至 multi_stock_results.csv
  1. 回归任务:预测能力极弱,不稳定

所有股票的回归 R² 均为负或接近零,说明模型完全无法解释次日收益率的方差。 交叉验证 RMSE 普遍高于单次测试 RMSE(例如 AAPL 从 1.59% 升至 2.86%),且标准差较大,表明模型在不同时间段表现差异巨大,泛化能力差。 2. 分类任务:略高于随机,但无实际意义

分类 AUC 多在 0.5 左右(最高 AMZN 0.519),交叉验证 AUC 均值也接近 0.5,标准差很小。这说明模型仅比随机猜测好一点点,且这种微弱优势在不同时间段基本稳定。 但 AUC 仅 0.52 意味着模型的区分能力极低,几乎无法用于实际决策。 3. 策略回测:亮眼收益背后的陷阱

NVDA、WMT、JPM、JNJ、TMUS 这 5 只股票的策略收益率完全等于买入持有收益率。原因是这些股票的 best_thresh 极低(如 NVDA 0.092),导致几乎所有测试样本都被预测为“涨”,策略信号全为 1,结果等于长期持股。

检查 NVDA 的分类 AUC 仅 0.478,低于 0.5,模型实际上更倾向于预测下跌,但阈值优化后却选择了全仓做多,这显然是过拟合测试集的结果。 AAPL、MSFT、GOOGL、AMZN、AMD 的策略收益略高于买入持有,但超额收益很小(例如 AAPL 多赚 39 个百分点,但 7 年总收益 300% 背景下,超额并不显著)。夏普比率在 1.0 左右,但这是在不考虑交易成本下的理想值。

一份修正验证流程的完整 Kaggle Python 代码。主要改进点包括:

严格的三段划分:训练集 (60%)、验证集 (20%)、测试集 (20%),按时间顺序。 在验证集上优化阈值:基于 F1 分数选择最佳分类阈值,避免测试集信息泄露。 引入交易成本:假设单边交易成本 0.1% + 滑点 0.05%。 支持单只或多只股票(通过股票列表控制),并提供组合策略示例。 输出全面的评估指标,包括考虑成本后的策略收益和夏普比率。

# ==================== 1. 安装与导入 ====================
!pip install ta xgboost lightgbm --quiet

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
import os
import gc
warnings.filterwarnings('ignore')

from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import (accuracy_score, roc_auc_score, confusion_matrix,
                             precision_recall_curve)
import lightgbm as lgb
import ta

# ==================== 2. 配置参数 ====================
# 股票列表(可修改为单只或多只)
TICKERS = ['AAPL', 'MSFT', 'NVDA', 'GOOGL', 'AMZN', 'WMT', 'JPM', 'JNJ', 'TMUS', 'AMD']
# 数据路径(请根据实际路径调整)
DATA_PATH = '/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/'
# 交易成本参数(单边)
TRADE_COST = 0.001  # 0.1%
SLIPPAGE = 0.0005   # 0.05%

# 存储结果
results = {}

# ==================== 3. 特征工程函数(复用) ====================
def engineer_features(df):
    """对单个股票的DataFrame进行特征工程,返回处理后的DataFrame"""
    data = df.copy()
    
    # ----- 趋势指标 -----
    data['SMA_10'] = ta.trend.sma_indicator(data['Close'], 10)
    data['SMA_50'] = ta.trend.sma_indicator(data['Close'], 50)
    data['EMA_10'] = ta.trend.ema_indicator(data['Close'], 10)
    data['EMA_50'] = ta.trend.ema_indicator(data['Close'], 50)
    macd = ta.trend.MACD(data['Close'])
    data['MACD'] = macd.macd()
    data['MACD_signal'] = macd.macd_signal()
    data['MACD_diff'] = macd.macd_diff()
    data['ADX'] = ta.trend.ADXIndicator(data['High'], data['Low'], data['Close'], 14).adx()
    
    # ----- 动量指标 -----
    data['RSI'] = ta.momentum.RSIIndicator(data['Close'], 14).rsi()
    stoch = ta.momentum.StochasticOscillator(data['High'], data['Low'], data['Close'], 14, 3)
    data['Stoch_K'] = stoch.stoch()
    data['Stoch_D'] = stoch.stoch_signal()
    data['WilliamsR'] = ta.momentum.WilliamsRIndicator(data['High'], data['Low'], data['Close'], 14).williams_r()
    
    # ----- 波动率指标 -----
    bb = ta.volatility.BollingerBands(data['Close'], 20, 2)
    data['BB_high'] = bb.bollinger_hband()
    data['BB_low'] = bb.bollinger_lband()
    data['BB_width'] = bb.bollinger_wband()
    data['ATR'] = ta.volatility.AverageTrueRange(data['High'], data['Low'], data['Close'], 14).average_true_range()
    
    # ----- 成交量指标 -----
    data['OBV'] = ta.volume.OnBalanceVolumeIndicator(data['Close'], data['Volume']).on_balance_volume()
    data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(data['High'], data['Low'], data['Close'], data['Volume'], 20).chaikin_money_flow()
    
    # ----- 价格衍生特征 -----
    data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
    data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
    data['Volume_Change'] = data['Volume'].pct_change()
    
    # ----- 收益率滞后 -----
    for lag in range(1, 4):
        data[f'return_lag{lag}'] = data['Close'].pct_change(lag) * 100
    
    # ----- 波动率特征 -----
    data['vol_5'] = data['Close'].pct_change().rolling(5).std() * 100
    data['vol_20'] = data['Close'].pct_change().rolling(20).std() * 100
    
    # ----- 日历特征 -----
    data['day_of_week'] = data.index.dayofweek
    data['month'] = data.index.month
    
    # ----- 交叉特征 -----
    data['OC_pct_vol'] = data['OC_pct'] * data['Volume_Change']
    data['HL_pct_vol'] = data['HL_pct'] * data['Volume_Change']
    data['RSI_OC'] = data['RSI'] * data['OC_pct']
    
    # ----- 目标变量 -----
    data['target_reg'] = data['Close'].pct_change().shift(-1) * 100          # 次日收益率
    data['target_cls'] = (data['Close'].shift(-1) > data['Close']).astype(int)  # 次日涨跌
    
    return data

# ==================== 4. 定义单只股票处理函数(改进版)====================
def process_stock(ticker):
    print(f"\n{'='*60}\n处理股票: {ticker}\n{'='*60}")
    
    # 读取数据
    file_path = os.path.join(DATA_PATH, f"{ticker.lower()}.us.txt")
    if not os.path.exists(file_path):
        print(f"文件不存在: {file_path}")
        return None
    
    df = pd.read_csv(file_path)
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index('Date').sort_index()
    
    if len(df) < 500:
        print(f"数据量不足: {len(df)}")
        return None
    
    # 特征工程
    data = engineer_features(df)
    
    # 处理缺失值和无穷大
    data = data.replace([np.inf, -np.inf], np.nan)
    data_clean = data.dropna().copy()
    print(f"清洗后样本数: {len(data_clean)}")
    
    # 特征列(与之前相同)
    feature_cols = [
        'Open', 'High', 'Low', 'Close', 'Volume',
        'SMA_10', 'SMA_50', 'EMA_10', 'EMA_50', 'MACD', 'MACD_signal', 'ADX',
        'RSI', 'Stoch_K', 'Stoch_D', 'WilliamsR',
        'BB_width', 'ATR',
        'OBV', 'CMF',
        'HL_pct', 'OC_pct', 'Volume_Change',
        'return_lag1', 'return_lag2', 'return_lag3',
        'vol_5', 'vol_20',
        'day_of_week', 'month',
        'OC_pct_vol', 'HL_pct_vol', 'RSI_OC'
    ]
    
    X = data_clean[feature_cols]
    y_reg = data_clean['target_reg']
    y_cls = data_clean['target_cls']
    
    # ========== 改进的划分:训练(60%) + 验证(20%) + 测试(20%) ==========
    n = len(X)
    train_end = int(n * 0.6)
    val_end = int(n * 0.8)
    
    X_train = X.iloc[:train_end]
    y_train_cls = y_cls.iloc[:train_end]
    y_train_reg = y_reg.iloc[:train_end]
    
    X_val = X.iloc[train_end:val_end]
    y_val_cls = y_cls.iloc[train_end:val_end]
    y_val_reg = y_reg.iloc[train_end:val_end]
    
    X_test = X.iloc[val_end:]
    y_test_cls = y_cls.iloc[val_end:]
    y_test_reg = y_reg.iloc[val_end:]
    
    print(f"训练集: {X_train.index[0]}{X_train.index[-1]}, 样本数 {len(X_train)}")
    print(f"验证集: {X_val.index[0]}{X_val.index[-1]}, 样本数 {len(X_val)}")
    print(f"测试集: {X_test.index[0]}{X_test.index[-1]}, 样本数 {len(X_test)}")
    
    # ========== 训练LightGBM分类器 ==========
    lgb_cls = lgb.LGBMClassifier(
        n_estimators=200, max_depth=3, learning_rate=0.05,
        subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1
    )
    lgb_cls.fit(X_train, y_train_cls)
    
    # 在验证集上预测概率
    val_prob = lgb_cls.predict_proba(X_val)[:, 1]
    
    # ========== 在验证集上优化阈值 ==========
    precision, recall, thresholds = precision_recall_curve(y_val_cls, val_prob)
    f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
    best_idx = np.argmax(f1_scores[:-1])  # 去掉最后一个无效值
    best_thresh = thresholds[best_idx]
    print(f"验证集最佳阈值 (基于F1): {best_thresh:.4f}")
    
    # ========== 在测试集上评估 ==========
    test_prob = lgb_cls.predict_proba(X_test)[:, 1]
    test_pred_label = (test_prob >= best_thresh).astype(int)
    
    # 分类指标
    acc = accuracy_score(y_test_cls, test_pred_label)
    auc = roc_auc_score(y_test_cls, test_prob)
    print(f"\n测试集分类准确率: {acc:.4f}")
    print(f"测试集AUC: {auc:.4f}")
    
    # ========== 策略回测(考虑交易成本) ==========
    # 构建测试集DataFrame
    test_df = X_test[['Close']].copy()  # 只需收盘价用于计算收益
    test_df['actual_return'] = y_test_reg.values  # 次日真实收益率
    test_df['pred_prob'] = test_prob
    test_df['signal'] = (test_prob >= best_thresh).astype(int)
    
    # 计算每日策略收益(考虑交易成本)
    # 假设交易规则:今天收盘时根据信号决定明天的持仓
    # 由于是次日收益率,我们需要在当天交易结束后产生信号,第二天开盘执行。
    # 简化处理:我们假设信号在当天收盘时产生,第二天以收盘价成交(与次日收益率一致)。
    # 交易成本仅在发生交易时扣除(即信号发生变化时)。
    test_df['prev_signal'] = test_df['signal'].shift(1).fillna(0)
    test_df['trade'] = (test_df['signal'] != test_df['prev_signal']).astype(int)  # 是否调仓
    
    # 策略收益 = 信号 * 实际收益率 - 交易成本(调仓时扣除)
    test_df['strategy_return'] = test_df['signal'] * test_df['actual_return'] - test_df['trade'] * (TRADE_COST + SLIPPAGE) * 100  # 成本转换为百分比
    test_df['buy_hold_return'] = test_df['actual_return']  # 买入持有就是每日收益率
    
    # 计算累计净值
    test_df['strategy_cum'] = (1 + test_df['strategy_return']/100).cumprod()
    test_df['buy_hold_cum'] = (1 + test_df['buy_hold_return']/100).cumprod()
    
    strategy_return = (test_df['strategy_cum'].iloc[-1] - 1) * 100
    buy_hold_return = (test_df['buy_hold_cum'].iloc[-1] - 1) * 100
    
    # 夏普比率(年化)
    strategy_sharpe = test_df['strategy_return'].mean() / test_df['strategy_return'].std() * np.sqrt(252)
    buy_hold_sharpe = test_df['buy_hold_return'].mean() / test_df['buy_hold_return'].std() * np.sqrt(252)
    
    print(f"\n策略总收益率 (考虑成本): {strategy_return:.2f}%")
    print(f"买入持有总收益率: {buy_hold_return:.2f}%")
    print(f"策略夏普比率: {strategy_sharpe:.2f}")
    print(f"买入持有夏普比率: {buy_hold_sharpe:.2f}")
    
    # 绘制净值曲线
    plt.figure(figsize=(10,5))
    plt.plot(test_df.index, test_df['strategy_cum'], label='Strategy (with cost)', linewidth=1)
    plt.plot(test_df.index, test_df['buy_hold_cum'], label='Buy & Hold', linewidth=1, alpha=0.7)
    plt.title(f'{ticker} - Strategy vs Buy & Hold (Test Set)')
    plt.xlabel('Date')
    plt.ylabel('Cumulative Return')
    plt.legend()
    plt.grid(True, linestyle='--')
    plt.show()
    
    # 返回结果摘要
    return {
        'ticker': ticker,
        'acc': acc,
        'auc': auc,
        'best_thresh': best_thresh,
        'strategy_return': strategy_return,
        'buy_hold_return': buy_hold_return,
        'strategy_sharpe': strategy_sharpe,
        'buy_hold_sharpe': buy_hold_sharpe,
        'n_trades': test_df['trade'].sum()  # 总交易次数
    }

# ==================== 5. 主循环(可单只或多只)====================
# 如果想只跑一只股票,可以将 TICKERS 改为 ['AAPL']
for ticker in TICKERS:
    result = process_stock(ticker)
    if result is not None:
        results[ticker] = result
    gc.collect()

# ==================== 6. 多股票结果汇总 ====================
if len(results) > 1:
    summary = pd.DataFrame(results).T
    print("\n\n========== 多股票结果汇总(考虑成本)==========")
    print(summary[['acc', 'auc', 'strategy_return', 'buy_hold_return', 'strategy_sharpe', 'buy_hold_sharpe', 'n_trades']].to_string())
    
    # 可视化对比
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))
    
    # 准确率 & AUC
    summary[['acc', 'auc']].plot(kind='bar', ax=axes[0,0])
    axes[0,0].set_title('分类准确率 & AUC')
    axes[0,0].axhline(y=0.5, color='gray', linestyle='--')
    
    # 策略 vs 买入持有收益率
    summary[['strategy_return', 'buy_hold_return']].plot(kind='bar', ax=axes[0,1])
    axes[0,1].set_title('总收益率对比')
    
    # 夏普比率
    summary[['strategy_sharpe', 'buy_hold_sharpe']].plot(kind='bar', ax=axes[1,0])
    axes[1,0].set_title('夏普比率')
    axes[1,0].axhline(y=1, color='gray', linestyle='--')
    
    # 交易次数
    summary['n_trades'].plot(kind='bar', ax=axes[1,1], color='orange')
    axes[1,1].set_title('交易次数')
    
    plt.tight_layout()
    plt.show()
    
    # 保存结果
    summary.to_csv('multi_stock_results_with_cost.csv')
    print("结果已保存至 multi_stock_results_with_cost.csv")
else:
    # 单只股票结果已打印,无需汇总
    pass

============================================================
处理股票: AAPL
============================================================
清洗后样本数: 8313
训练集: 1984-11-15 00:00:00 至 2004-08-25 00:00:00, 样本数 4987
验证集: 2004-08-26 00:00:00 至 2011-04-04 00:00:00, 样本数 1663
测试集: 2011-04-05 00:00:00 至 2017-11-09 00:00:00, 样本数 1663
验证集最佳阈值 (基于F1): 0.1954

测试集分类准确率: 0.5207
测试集AUC: 0.5053

策略总收益率 (考虑成本): 301.85%
买入持有总收益率: 302.46%
策略夏普比率: 0.96
买入持有夏普比率: 0.96


============================================================
处理股票: MSFT
============================================================
清洗后样本数: 7921
训练集: 1986-06-02 00:00:00 至 2005-04-11 00:00:00, 样本数 4752
验证集: 2005-04-12 00:00:00 至 2011-07-26 00:00:00, 样本数 1584
测试集: 2011-07-27 00:00:00 至 2017-11-09 00:00:00, 样本数 1585
验证集最佳阈值 (基于F1): 0.3321

测试集分类准确率: 0.5129
测试集AUC: 0.5201

策略总收益率 (考虑成本): 215.59%
买入持有总收益率: 263.58%
策略夏普比率: 0.93
买入持有夏普比率: 1.02


============================================================
处理股票: NVDA
============================================================
清洗后样本数: 4682
训练集: 1999-04-05 00:00:00 至 2010-06-04 00:00:00, 样本数 2809
验证集: 2010-06-07 00:00:00 至 2014-02-24 00:00:00, 样本数 936
测试集: 2014-02-25 00:00:00 至 2017-11-09 00:00:00, 样本数 937
验证集最佳阈值 (基于F1): 0.2815

测试集分类准确率: 0.5133
测试集AUC: 0.4872

策略总收益率 (考虑成本): 423.66%
买入持有总收益率: 1103.25%
策略夏普比率: 1.44
买入持有夏普比率: 1.94


============================================================
处理股票: GOOGL
============================================================
清洗后样本数: 3282
训练集: 2004-10-28 00:00:00 至 2012-08-22 00:00:00, 样本数 1969
验证集: 2012-08-23 00:00:00 至 2015-04-06 00:00:00, 样本数 656
测试集: 2015-04-07 00:00:00 至 2017-11-09 00:00:00, 样本数 657
验证集最佳阈值 (基于F1): 0.2457

测试集分类准确率: 0.5449
测试集AUC: 0.5122

策略总收益率 (考虑成本): 76.97%
买入持有总收益率: 91.64%
策略夏普比率: 1.11
买入持有夏普比率: 1.22


============================================================
处理股票: AMZN
============================================================
清洗后样本数: 5102
训练集: 1997-07-28 00:00:00 至 2009-10-01 00:00:00, 样本数 3061
验证集: 2009-10-02 00:00:00 至 2013-10-22 00:00:00, 样本数 1020
测试集: 2013-10-23 00:00:00 至 2017-11-09 00:00:00, 样本数 1021
验证集最佳阈值 (基于F1): 0.1709

测试集分类准确率: 0.5367
测试集AUC: 0.5154

策略总收益率 (考虑成本): 240.90%
买入持有总收益率: 244.40%
策略夏普比率: 1.23
买入持有夏普比率: 1.17


============================================================
处理股票: WMT
============================================================
清洗后样本数: 10589
训练集: 1972-08-24 00:00:00 至 2001-01-08 00:00:00, 样本数 6353
验证集: 2001-01-09 00:00:00 至 2009-06-12 00:00:00, 样本数 2118
测试集: 2009-06-15 00:00:00 至 2017-11-09 00:00:00, 样本数 2118
验证集最佳阈值 (基于F1): 0.2320

测试集分类准确率: 0.5246
测试集AUC: 0.4840

策略总收益率 (考虑成本): 114.30%
买入持有总收益率: 122.50%
策略夏普比率: 0.68
买入持有夏普比率: 0.67


============================================================
处理股票: JPM
============================================================
清洗后样本数: 12023
训练集: 1970-03-13 00:00:00 至 1998-09-25 00:00:00, 样本数 7213
验证集: 1998-09-28 00:00:00 至 2008-04-23 00:00:00, 样本数 2405
测试集: 2008-04-24 00:00:00 至 2017-11-09 00:00:00, 样本数 2405
验证集最佳阈值 (基于F1): 0.1744

测试集分类准确率: 0.5077
测试集AUC: 0.5137

策略总收益率 (考虑成本): 85.99%
买入持有总收益率: 143.06%
策略夏普比率: 0.36
买入持有夏普比率: 0.43


============================================================
处理股票: JNJ
============================================================
清洗后样本数: 12023
训练集: 1970-03-13 00:00:00 至 1998-09-28 00:00:00, 样本数 7213
验证集: 1998-09-29 00:00:00 至 2008-04-23 00:00:00, 样本数 2405
测试集: 2008-04-24 00:00:00 至 2017-11-09 00:00:00, 样本数 2405
验证集最佳阈值 (基于F1): 0.2481

测试集分类准确率: 0.5085
测试集AUC: 0.5044

策略总收益率 (考虑成本): 128.00%
买入持有总收益率: 153.21%
策略夏普比率: 0.61
买入持有夏普比率: 0.67


============================================================
处理股票: TMUS
============================================================
清洗后样本数: 2613
训练集: 2007-06-27 00:00:00 至 2013-09-17 00:00:00, 样本数 1567
验证集: 2013-09-18 00:00:00 至 2015-10-14 00:00:00, 样本数 523
测试集: 2015-10-15 00:00:00 至 2017-11-09 00:00:00, 样本数 523
验证集最佳阈值 (基于F1): 0.0389

测试集分类准确率: 0.5182
测试集AUC: 0.4967

策略总收益率 (考虑成本): 42.00%
买入持有总收益率: 42.21%
策略夏普比率: 0.77
买入持有夏普比率: 0.77


============================================================
处理股票: AMD
============================================================
清洗后样本数: 8686
训练集: 1983-05-31 00:00:00 至 2004-01-22 00:00:00, 样本数 5211
验证集: 2004-01-23 00:00:00 至 2010-12-15 00:00:00, 样本数 1737
测试集: 2010-12-16 00:00:00 至 2017-11-09 00:00:00, 样本数 1738
验证集最佳阈值 (基于F1): 0.1491

测试集分类准确率: 0.4845
测试集AUC: 0.5159

策略总收益率 (考虑成本): 40.02%
买入持有总收益率: 40.22%
策略夏普比率: 0.37
买入持有夏普比率: 0.37



========== 多股票结果汇总(考虑成本)==========
            acc       auc strategy_return buy_hold_return strategy_sharpe buy_hold_sharpe n_trades
AAPL   0.520746    0.5053      301.850946      302.456165        0.960817        0.961731        1
MSFT   0.512934  0.520148      215.591558       263.57725        0.932221        1.019072      152
NVDA    0.51334   0.48715      423.660898     1103.251127        1.435808         1.93821      137
GOOGL  0.544901  0.512232       76.967913       91.636384        1.110123        1.220262       57
AMZN   0.536729  0.515385       240.89683      244.396499        1.229914        1.168858      111
WMT    0.524551  0.483978      114.302288       122.49955        0.676137        0.674327      181
JPM    0.507692  0.513727       85.989901      143.056102        0.364104        0.428834        9
JNJ    0.508524  0.504389      128.000487      153.205579        0.605487        0.669661       29
TMUS   0.518164  0.496661       42.003157        42.21332        0.767547        0.770155        1
AMD    0.484465  0.515906       40.015641       40.224159        0.369631        0.369999        1

结果已保存至 multi_stock_results_with_cost.csv

更新模型

# ==================== 1. 安装与导入 ====================
!pip install ta catboost xgboost lightgbm --quiet

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
import os
import gc
from tqdm import tqdm

from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score, accuracy_score, roc_auc_score

import lightgbm as lgb
import catboost as cb
import xgboost as xgb
import ta

# 深度学习(需启用GPU)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

warnings.filterwarnings('ignore')

# ==================== 2. 配置参数 ====================
TICKERS = ['AAPL', 'MSFT', 'NVDA', 'GOOGL', 'AMZN', 'WMT', 'JPM', 'JNJ', 'TMUS', 'AMD']
DATA_PATH = '/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/'
TRADE_COST = 0.001  # 交易成本(用于策略回测,可选)

results = {}

# ==================== 3. 增强特征工程函数 ====================
def engineer_features_advanced(df):
    """对单个股票进行高级特征工程,包括时间窗口分组聚合"""
    data = df.copy()
    
    # ---------- 基础技术指标(与之前相同)----------
    # 趋势
    data['SMA_10'] = ta.trend.sma_indicator(data['Close'], 10)
    data['SMA_50'] = ta.trend.sma_indicator(data['Close'], 50)
    data['EMA_10'] = ta.trend.ema_indicator(data['Close'], 10)
    data['EMA_50'] = ta.trend.ema_indicator(data['Close'], 50)
    macd = ta.trend.MACD(data['Close'])
    data['MACD'] = macd.macd()
    data['MACD_signal'] = macd.macd_signal()
    data['MACD_diff'] = macd.macd_diff()
    data['ADX'] = ta.trend.ADXIndicator(data['High'], data['Low'], data['Close'], 14).adx()
    
    # 动量
    data['RSI'] = ta.momentum.RSIIndicator(data['Close'], 14).rsi()
    stoch = ta.momentum.StochasticOscillator(data['High'], data['Low'], data['Close'], 14, 3)
    data['Stoch_K'] = stoch.stoch()
    data['Stoch_D'] = stoch.stoch_signal()
    data['WilliamsR'] = ta.momentum.WilliamsRIndicator(data['High'], data['Low'], data['Close'], 14).williams_r()
    
    # 波动率
    bb = ta.volatility.BollingerBands(data['Close'], 20, 2)
    data['BB_high'] = bb.bollinger_hband()
    data['BB_low'] = bb.bollinger_lband()
    data['BB_width'] = bb.bollinger_wband()
    data['ATR'] = ta.volatility.AverageTrueRange(data['High'], data['Low'], data['Close'], 14).average_true_range()
    
    # 成交量
    data['OBV'] = ta.volume.OnBalanceVolumeIndicator(data['Close'], data['Volume']).on_balance_volume()
    data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(data['High'], data['Low'], data['Close'], data['Volume'], 20).chaikin_money_flow()
    
    # 价格衍生
    data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
    data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
    data['Volume_Change'] = data['Volume'].pct_change()
    
    # 收益率滞后
    for lag in range(1, 6):
        data[f'return_lag{lag}'] = data['Close'].pct_change(lag) * 100
    
    # 波动率特征
    data['vol_5'] = data['Close'].pct_change().rolling(5).std() * 100
    data['vol_20'] = data['Close'].pct_change().rolling(20).std() * 100
    
    # ---------- 创新特征:时间窗口分组聚合(灵感来自 seconds_in_bucket_group)----------
    # 将时间序列划分为多个窗口(例如每5天为一组),计算组内统计量相对于当前值的比率
    # 这里以5天为窗口大小,步长1天(滚动窗口),计算窗口内第一个值、均值、最大值等与当前值的比率
    windows = [5, 10, 20]
    for w in windows:
        # 窗口内第一个值 / 当前值(相当于 group_first_ratio)
        data[f'first_{w}_ratio'] = data['Close'].rolling(w).apply(lambda x: x.iloc[0] if len(x) == w else np.nan, raw=False) / data['Close']
        # 窗口内均值 / 当前值
        data[f'mean_{w}_ratio'] = data['Close'].rolling(w).mean() / data['Close']
        # 窗口内最大值 / 当前值
        data[f'max_{w}_ratio'] = data['Close'].rolling(w).max() / data['Close']
        # 窗口内最小值 / 当前值
        data[f'min_{w}_ratio'] = data['Close'].rolling(w).min() / data['Close']
        
        # 成交量类似
        data[f'volume_mean_{w}_ratio'] = data['Volume'].rolling(w).mean() / data['Volume']
    
    # ---------- 日历特征(星期几、月份)----------
    data['day_of_week'] = data.index.dayofweek
    data['month'] = data.index.month
    
    # ---------- 交叉特征 ----------
    data['OC_pct_vol'] = data['OC_pct'] * data['Volume_Change']
    data['HL_pct_vol'] = data['HL_pct'] * data['Volume_Change']
    data['RSI_OC'] = data['RSI'] * data['OC_pct']
    
    # 目标变量
    data['target_reg'] = data['Close'].pct_change().shift(-1) * 100
    data['target_cls'] = (data['Close'].shift(-1) > data['Close']).astype(int)
    
    return data

# ==================== 4. 定义 GRU 模型(PyTorch)====================
class GRUModel(nn.Module):
    def __init__(self, input_dim, hidden_dim=64, num_layers=2, dropout=0.2):
        super().__init__()
        self.gru = nn.GRU(input_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout)
        self.fc = nn.Linear(hidden_dim, 1)
        
    def forward(self, x):
        # x shape: (batch, seq_len, input_dim)
        out, _ = self.gru(x)
        out = self.fc(out[:, -1, :])  # 取最后一个时间步的输出
        return out.squeeze()

# ==================== 5. 定义特征筛选函数(基于CatBoost重要性)====================
def select_features(X_train, y_train, top_k=200):
    """使用CatBoost特征重要性筛选Top K特征"""
    model = cb.CatBoostRegressor(iterations=100, verbose=0, random_seed=42)
    model.fit(X_train, y_train)
    importance = model.feature_importances_
    feat_imp = pd.DataFrame({'feature': X_train.columns, 'importance': importance})
    feat_imp = feat_imp.sort_values('importance', ascending=False).head(top_k)
    selected = feat_imp['feature'].tolist()
    return selected

# ==================== 6. 单只股票处理函数 ====================
def process_stock_advanced(ticker):
    print(f"\n{'='*60}\n处理股票: {ticker}\n{'='*60}")
    
    # 读取数据
    file_path = os.path.join(DATA_PATH, f"{ticker.lower()}.us.txt")
    if not os.path.exists(file_path):
        print(f"文件不存在: {file_path}")
        return None
    
    df = pd.read_csv(file_path)
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index('Date').sort_index()
    
    if len(df) < 500:
        print(f"数据量不足: {len(df)}")
        return None
    
    # 特征工程
    data = engineer_features_advanced(df)
    
    # 处理缺失值
    data = data.replace([np.inf, -np.inf], np.nan)
    data_clean = data.dropna().copy()
    print(f"清洗后样本数: {len(data_clean)}")
    
    # 分离特征和目标
    feature_cols = [c for c in data_clean.columns if c not in ['target_reg', 'target_cls']]
    X = data_clean[feature_cols]
    y_reg = data_clean['target_reg']
    y_cls = data_clean['target_cls']
    
    # 划分:训练 (60%) + 验证 (20%) + 测试 (20%)
    n = len(X)
    train_end = int(n * 0.6)
    val_end = int(n * 0.8)
    
    X_train = X.iloc[:train_end]
    y_train_reg = y_reg.iloc[:train_end]
    y_train_cls = y_cls.iloc[:train_end]
    
    X_val = X.iloc[train_end:val_end]
    y_val_reg = y_reg.iloc[train_end:val_end]
    y_val_cls = y_cls.iloc[train_end:val_end]
    
    X_test = X.iloc[val_end:]
    y_test_reg = y_reg.iloc[val_end:]
    y_test_cls = y_cls.iloc[val_end:]
    
    print(f"训练集: {X_train.index[0]}{X_train.index[-1]}, 样本数 {len(X_train)}")
    print(f"验证集: {X_val.index[0]}{X_val.index[-1]}, 样本数 {len(X_val)}")
    print(f"测试集: {X_test.index[0]}{X_test.index[-1]}, 样本数 {len(X_test)}")
    
    # ---------- 特征筛选 ----------
    print("\n正在进行特征筛选...")
    selected_features = select_features(X_train, y_train_reg, top_k=200)  # 选出200个特征
    X_train_sel = X_train[selected_features]
    X_val_sel = X_val[selected_features]
    X_test_sel = X_test[selected_features]
    print(f"筛选后特征数: {len(selected_features)}")
    
    # ---------- 模型 1: CatBoost ----------
    print("\n训练 CatBoost...")
    cb_model = cb.CatBoostRegressor(iterations=500, learning_rate=0.05, depth=5, verbose=0, random_seed=42)
    cb_model.fit(X_train_sel, y_train_reg, eval_set=(X_val_sel, y_val_reg), early_stopping_rounds=50, verbose=False)
    cb_pred = cb_model.predict(X_test_sel)
    
    # ---------- 模型 2: LightGBM ----------
    print("\n训练 LightGBM...")
    lgb_model = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05, max_depth=5, random_state=42, verbose=-1)
    lgb_model.fit(X_train_sel, y_train_reg, eval_set=[(X_val_sel, y_val_reg)], callbacks=[lgb.early_stopping(50)])
    lgb_pred = lgb_model.predict(X_test_sel)
    
    # ---------- 模型 3: GRU (需构造序列样本) ----------
    # 对于GRU,需要构造时间序列样本:用过去seq_len天的特征预测下一天收益率
    seq_len = 20  # 用20天历史
    def create_sequences(X, y, seq_len):
        X_seq, y_seq = [], []
        for i in range(len(X) - seq_len):
            X_seq.append(X.iloc[i:i+seq_len].values)
            y_seq.append(y.iloc[i+seq_len])
        return np.array(X_seq), np.array(y_seq)
    
    X_train_seq, y_train_seq = create_sequences(X_train_sel, y_train_reg, seq_len)
    X_val_seq, y_val_seq = create_sequences(X_val_sel, y_val_reg, seq_len)
    X_test_seq, y_test_seq = create_sequences(X_test_sel, y_test_reg, seq_len)
    
    print(f"GRU 训练样本数: {len(X_train_seq)}")
    
    # 转换为PyTorch张量
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    train_dataset = TensorDataset(torch.FloatTensor(X_train_seq), torch.FloatTensor(y_train_seq))
    val_dataset = TensorDataset(torch.FloatTensor(X_val_seq), torch.FloatTensor(y_val_seq))
    test_dataset = TensorDataset(torch.FloatTensor(X_test_seq), torch.FloatTensor(y_test_seq))
    
    train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
    val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
    test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
    
    # 初始化GRU
    input_dim = X_train_sel.shape[1]
    gru_model = GRUModel(input_dim, hidden_dim=64, num_layers=2).to(device)
    criterion = nn.MSELoss()
    optimizer = optim.Adam(gru_model.parameters(), lr=0.001)
    
    # 训练GRU(简化版,早停)
    best_val_loss = np.inf
    patience = 10
    counter = 0
    for epoch in range(100):
        gru_model.train()
        train_loss = 0
        for Xb, yb in train_loader:
            Xb, yb = Xb.to(device), yb.to(device)
            optimizer.zero_grad()
            pred = gru_model(Xb)
            loss = criterion(pred, yb)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
        
        gru_model.eval()
        val_loss = 0
        with torch.no_grad():
            for Xb, yb in val_loader:
                Xb, yb = Xb.to(device), yb.to(device)
                pred = gru_model(Xb)
                val_loss += criterion(pred, yb).item()
        val_loss /= len(val_loader)
        
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            counter = 0
            torch.save(gru_model.state_dict(), 'best_gru.pth')
        else:
            counter += 1
            if counter >= patience:
                break
    
    # 加载最佳模型并在测试集上预测
    gru_model.load_state_dict(torch.load('best_gru.pth'))
    gru_model.eval()
    gru_pred = []
    with torch.no_grad():
        for Xb, _ in test_loader:
            Xb = Xb.to(device)
            pred = gru_model(Xb)
            gru_pred.extend(pred.cpu().numpy())
    gru_pred = np.array(gru_pred)
    
    # 注意:GRU预测的索引与测试集不对齐(因为序列构造少了前seq_len个),需调整
    y_test_reg_aligned = y_test_reg.iloc[seq_len:].values
    
    # ---------- 模型融合 ----------
    # 在验证集上搜索最佳权重(简单平均作为baseline)
    # 这里我们使用验证集上三个模型的预测进行线性回归学习权重,或直接等权
    # 简单起见,使用验证集上的均方误差倒数作为权重
    # 先对验证集进行预测(需对齐序列)
    cb_val_pred = cb_model.predict(X_val_sel)
    lgb_val_pred = lgb_model.predict(X_val_sel)
    
    # GRU验证集预测
    gru_val_pred = []
    with torch.no_grad():
        for Xb, _ in DataLoader(val_dataset, batch_size=64, shuffle=False):
            Xb = Xb.to(device)
            pred = gru_model(Xb)
            gru_val_pred.extend(pred.cpu().numpy())
    gru_val_pred = np.array(gru_val_pred)
    
    y_val_aligned = y_val_reg.iloc[seq_len:].values
    # 对齐三个预测(由于GRU使用了seq_len,验证集预测也少了前seq_len个)
    cb_val_aligned = cb_val_pred[seq_len:]
    lgb_val_aligned = lgb_val_pred[seq_len:]
    
    # 计算每个模型在验证集上的RMSE
    rmse_cb = np.sqrt(mean_squared_error(y_val_aligned, cb_val_aligned))
    rmse_lgb = np.sqrt(mean_squared_error(y_val_aligned, lgb_val_aligned))
    rmse_gru = np.sqrt(mean_squared_error(y_val_aligned, gru_val_pred))
    
    # 权重与RMSE成反比
    inv_rmse = np.array([1/rmse_cb, 1/rmse_lgb, 1/rmse_gru])
    weights = inv_rmse / inv_rmse.sum()
    print(f"\n融合权重: CB={weights[0]:.3f}, LGB={weights[1]:.3f}, GRU={weights[2]:.3f}")
    
    # 测试集融合预测
    # 对齐测试集预测
    cb_test_aligned = cb_pred[seq_len:]
    lgb_test_aligned = lgb_pred[seq_len:]
    # gru_pred已经是对齐后的长度
    ensemble_pred = weights[0]*cb_test_aligned + weights[1]*lgb_test_aligned + weights[2]*gru_pred
    
    # ---------- 评估 ----------
    rmse_ensemble = np.sqrt(mean_squared_error(y_test_reg_aligned, ensemble_pred))
    r2_ensemble = r2_score(y_test_reg_aligned, ensemble_pred)
    print(f"\n融合模型测试集 RMSE: {rmse_ensemble:.4f}%, R²: {r2_ensemble:.4f}")
    
    # ---------- 后处理:减去均值(简单版本)----------
    ensemble_pred_adj = ensemble_pred - ensemble_pred.mean()
    rmse_adj = np.sqrt(mean_squared_error(y_test_reg_aligned, ensemble_pred_adj))
    print(f"后处理(减均值)后 RMSE: {rmse_adj:.4f}%")
    
    # 返回结果
    return {
        'ticker': ticker,
        'rmse': rmse_ensemble,
        'r2': r2_ensemble,
        'rmse_adj': rmse_adj,
        'weights': weights
    }

# ==================== 7. 主循环 ====================
for ticker in TICKERS:
    result = process_stock_advanced(ticker)
    if result:
        results[ticker] = result
    gc.collect()

# ==================== 8. 结果汇总 ====================
summary = pd.DataFrame(results).T
print("\n\n========== 多股票结果汇总 ==========")
print(summary[['rmse', 'r2', 'rmse_adj']].to_string())

# 绘制对比图
fig, axes = plt.subplots(1, 2, figsize=(12,5))
summary['rmse'].sort_values().plot(kind='bar', ax=axes[0], color='skyblue')
axes[0].set_title('RMSE (%)')
summary['r2'].sort_values().plot(kind='bar', ax=axes[1], color='lightgreen')
axes[1].set_title('R²')
plt.tight_layout()
plt.show()

============================================================
处理股票: AAPL
============================================================
清洗后样本数: 8312
训练集: 1984-11-15 00:00:00 至 2004-08-25 00:00:00, 样本数 4987
验证集: 2004-08-26 00:00:00 至 2011-04-04 00:00:00, 样本数 1662
测试集: 2011-04-05 00:00:00 至 2017-11-09 00:00:00, 样本数 1663

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's l2: 6.33253
GRU 训练样本数: 4967

融合权重: CB=0.333, LGB=0.333, GRU=0.334

融合模型测试集 RMSE: 1.5972%, R²: -0.0007
后处理(减均值)后 RMSE: 1.6000%

============================================================
处理股票: MSFT
============================================================
清洗后样本数: 7920
训练集: 1986-06-02 00:00:00 至 2005-04-11 00:00:00, 样本数 4752
验证集: 2005-04-12 00:00:00 至 2011-07-27 00:00:00, 样本数 1584
测试集: 2011-07-28 00:00:00 至 2017-11-09 00:00:00, 样本数 1584

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[93]    valid_0's l2: 3.46672
GRU 训练样本数: 4732

融合权重: CB=0.334, LGB=0.334, GRU=0.332

融合模型测试集 RMSE: 1.4312%, R²: -0.0362
后处理(减均值)后 RMSE: 1.4172%

============================================================
处理股票: NVDA
============================================================
清洗后样本数: 4681
训练集: 1999-04-05 00:00:00 至 2010-06-04 00:00:00, 样本数 2808
验证集: 2010-06-07 00:00:00 至 2014-02-24 00:00:00, 样本数 936
测试集: 2014-02-25 00:00:00 至 2017-11-09 00:00:00, 样本数 937

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's l2: 6.34154
GRU 训练样本数: 2788

融合权重: CB=0.333, LGB=0.333, GRU=0.333

融合模型测试集 RMSE: 2.4276%, R²: -0.0047
后处理(减均值)后 RMSE: 2.4408%

============================================================
处理股票: GOOGL
============================================================
清洗后样本数: 3281
训练集: 2004-10-28 00:00:00 至 2012-08-22 00:00:00, 样本数 1968
验证集: 2012-08-23 00:00:00 至 2015-04-06 00:00:00, 样本数 656
测试集: 2015-04-07 00:00:00 至 2017-11-09 00:00:00, 样本数 657

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's l2: 1.87279
GRU 训练样本数: 1948

融合权重: CB=0.333, LGB=0.333, GRU=0.333

融合模型测试集 RMSE: 1.4182%, R²: -0.0006
后处理(减均值)后 RMSE: 1.4222%

============================================================
处理股票: AMZN
============================================================
清洗后样本数: 5101
训练集: 1997-07-28 00:00:00 至 2009-09-30 00:00:00, 样本数 3060
验证集: 2009-10-01 00:00:00 至 2013-10-22 00:00:00, 样本数 1020
测试集: 2013-10-23 00:00:00 至 2017-11-09 00:00:00, 样本数 1021

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's l2: 4.92244
GRU 训练样本数: 3040

融合权重: CB=0.334, LGB=0.333, GRU=0.333

融合模型测试集 RMSE: 1.8754%, R²: -0.0079
后处理(减均值)后 RMSE: 1.8733%

============================================================
处理股票: WMT
============================================================
清洗后样本数: 10588
训练集: 1972-08-24 00:00:00 至 2001-01-05 00:00:00, 样本数 6352
验证集: 2001-01-08 00:00:00 至 2009-06-11 00:00:00, 样本数 2118
测试集: 2009-06-12 00:00:00 至 2017-11-09 00:00:00, 样本数 2118

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[42]    valid_0's l2: 2.46596
GRU 训练样本数: 6332

融合权重: CB=0.333, LGB=0.333, GRU=0.333

融合模型测试集 RMSE: 1.0108%, R²: 0.0007
后处理(减均值)后 RMSE: 1.0118%

============================================================
处理股票: JPM
============================================================
清洗后样本数: 12022
训练集: 1970-03-13 00:00:00 至 1998-09-25 00:00:00, 样本数 7213
验证集: 1998-09-28 00:00:00 至 2008-04-22 00:00:00, 样本数 2404
测试集: 2008-04-23 00:00:00 至 2017-11-09 00:00:00, 样本数 2405

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's l2: 5.42209
GRU 训练样本数: 7193

融合权重: CB=0.333, LGB=0.333, GRU=0.333

融合模型测试集 RMSE: 2.7486%, R²: 0.0000
后处理(减均值)后 RMSE: 2.7490%

============================================================
处理股票: JNJ
============================================================
清洗后样本数: 12022
训练集: 1970-03-13 00:00:00 至 1998-09-28 00:00:00, 样本数 7213
验证集: 1998-09-29 00:00:00 至 2008-04-22 00:00:00, 样本数 2404
测试集: 2008-04-23 00:00:00 至 2017-11-09 00:00:00, 样本数 2405

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[7] valid_0's l2: 1.98806
GRU 训练样本数: 7193

融合权重: CB=0.334, LGB=0.333, GRU=0.333

融合模型测试集 RMSE: 1.0445%, R²: 0.0061
后处理(减均值)后 RMSE: 1.0455%

============================================================
处理股票: TMUS
============================================================
清洗后样本数: 2613
训练集: 2007-06-27 00:00:00 至 2013-09-17 00:00:00, 样本数 1567
验证集: 2013-09-18 00:00:00 至 2015-10-14 00:00:00, 样本数 523
测试集: 2015-10-15 00:00:00 至 2017-11-09 00:00:00, 样本数 523

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's l2: 3.47624
GRU 训练样本数: 1547

融合权重: CB=0.333, LGB=0.333, GRU=0.334

融合模型测试集 RMSE: 1.6541%, R²: -0.0010
后处理(减均值)后 RMSE: 1.6558%

============================================================
处理股票: AMD
============================================================
清洗后样本数: 8685
训练集: 1983-05-31 00:00:00 至 2004-01-22 00:00:00, 样本数 5211
验证集: 2004-01-23 00:00:00 至 2010-12-16 00:00:00, 样本数 1737
测试集: 2010-12-17 00:00:00 至 2017-11-09 00:00:00, 样本数 1737

正在进行特征筛选...
筛选后特征数: 54

训练 CatBoost...

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[22]    valid_0's l2: 13.2289
GRU 训练样本数: 5191

融合权重: CB=0.334, LGB=0.333, GRU=0.333

融合模型测试集 RMSE: 3.7074%, R²: -0.0001
后处理(减均值)后 RMSE: 3.7069%


========== 多股票结果汇总 ==========
           rmse        r2  rmse_adj
AAPL   1.597163 -0.000743  1.600019
MSFT   1.431194  -0.03622  1.417159
NVDA   2.427579 -0.004701  2.440844
GOOGL  1.418194 -0.000573  1.422232
AMZN   1.875434 -0.007888  1.873285
WMT    1.010818   0.00067   1.01176
JPM    2.748647  0.000018   2.74905
JNJ    1.044547  0.006094  1.045518
TMUS   1.654122  -0.00104  1.655803
AMD    3.707436 -0.000075  3.706942

你提供的运行结果展示了使用增强版特征工程和多模型融合后的股票预测效果。下面我将从多个角度对结果进行分析,并与之前的简单模型进行对比,最后给出后续建议。


📊 本次结果概览

股票 RMSE (%) 后处理 RMSE (%)
AAPL 1.597 -0.0007 1.600
MSFT 1.431 -0.0362 1.417
NVDA 2.428 -0.0047 2.441
GOOGL 1.418 -0.0006 1.422
AMZN 1.875 -0.0079 1.873
WMT 1.011 0.0007 1.012
JPM 2.749 0.0000 2.749
JNJ 1.045 0.0061 1.046
TMUS 1.654 -0.0010 1.656
AMD 3.707 -0.0001 3.707
  • R² 接近 0:所有股票的 R² 均在 0 附近(-0.036 ~ 0.006),说明模型几乎无法解释次日收益率的方差。这与之前简单模型的结果一致,反映了日频收益率预测的固有难度。
  • RMSE 与波动性相关:高波动股票(如 AMD、JPM、NVDA)的 RMSE 较大(2.4%~3.7%),低波动股票(如 WMT、JNJ)的 RMSE 较小(1.0%),符合预期。
  • 后处理效果微弱:减去均值后 RMSE 变化极小,说明模型预测的均值已接近真实均值,没有系统性偏差。

与之前结果的对比

将本次融合模型(以下简称“新模型”)的结果与你上一次多股票汇总中的 LightGBM 回归结果(以下简称“旧模型”)进行对比:

股票 旧 RMSE (%) 新 RMSE (%) 变化 旧 R² 新 R² 变化
AAPL 1.587 1.597 +0.01 0.006 -0.001 变差
MSFT 1.464 1.431 -0.033 -0.053 -0.036 改善
NVDA 2.508 2.428 -0.08 -0.089 -0.005 改善
GOOGL 1.484 1.418 -0.066 -0.099 -0.001 改善
AMZN 2.086 1.875 -0.211 -0.226 -0.008 改善
WMT 1.059 1.011 -0.048 -0.102 0.001 改善
JPM 2.932 2.749 -0.183 -0.141 0.000 改善
JNJ 1.112 1.045 -0.067 -0.133 0.006 改善
TMUS 1.903 1.654 -0.249 -0.288 -0.001 改善
AMD 4.016 3.707 -0.309 -0.181 -0.000 改善

结论: - 绝大多数股票的 RMSE 有所下降(尤其是 AMD、TMUS、AMZN 降幅明显),说明特征增强和模型融合在一定程度上提升了预测精度。 - R² 普遍向 0 靠拢,意味着模型虽然仍无法解释方差,但偏差减小(不再像之前那样远低于 0)。 - 然而,R² 依然接近 0,表明预测能力仍非常微弱,无法用于实际交易。


关键观察与解释

1. 特征筛选后特征数为 54

你的特征工程共产生了约 50~60 个特征(基础指标 + 窗口比率特征),因此筛选后保留了全部特征。CatBoost 特征重要性显示所有特征都有非零贡献,没有需要剔除的冗余特征,这在一定程度上说明构造的特征都是有用的。

2. 融合权重几乎均等

三个模型在验证集上的 RMSE 非常接近,导致融合权重各占 1/3。这说明在单股票预测上,树模型(CatBoost、LightGBM)和时序模型(GRU)的表现差异不大,没有出现某一模型明显占优的情况。

3. 后处理效果微弱

减去均值对结果影响很小,表明模型预测的均值与真实均值基本一致,没有整体偏移。


下一步建议

尽管本次改进在 RMSE 上有所优化,但 R² 仍无法突破 0 的界限。这说明日频收益率预测问题本身极难,现有技术指标和简单模型难以捕捉有效信号。以下是几个可行的后续方向:

1. 转向分类任务

  • 预测次日涨跌方向(准确率/AUC)可能比回归更稳定。你之前的分类结果(AUC 约 0.52)已表明存在微弱信号,可进一步优化特征和模型。
  • 可以尝试使用同样的特征和融合策略进行分类,观察 AUC 是否提升。

2. 预测波动率

  • 将目标改为未来 N 日的实际波动率(如 ATR 的变化、收益率标准差),波动率通常比方向更具可预测性。
  • 可用于期权定价或风险对冲,即使预测精度不高也有应用价值。

3. 引入外部数据

  • 市场整体数据(如标普 500 指数、VIX 恐慌指数)
  • 行业板块数据(相对强弱)
  • 基本面数据(市盈率、盈利增长)
  • 新闻情感分析(需额外数据源)

4. 更长周期预测

  • 尝试预测周度或月度收益率,降低噪声,同时减少交易频率,使交易成本影响减小。

5. 组合策略

  • 不再追求单只股票的精确择时,而是构建多股票组合,根据预测信号进行多空配对,分散风险。

总结

本次增强版代码在 RMSE 上取得了普遍改善,说明特征工程和模型融合的有效性。但 R² 依然接近 0,反映了金融时间序列预测的内在困难。你已经通过严谨的实验验证了这一结论,这是非常有价值的探索过程。建议将当前结果作为基线,未来可向分类、波动率预测或多股票组合方向拓展。

改进点包括:

任务转换:从回归(预测收益率)转向分类(预测涨跌方向),因为分类的评估指标(AUC)更稳定,且之前的分类结果(AUC≈0.52)表明存在微弱信号。 引入Transformer模型:用于捕捉时间序列中的长期依赖和股票间关系(单股票场景下可视为时间步上的自注意力)。 增强特征:加入基于星期几和月份的聚合特征(如过去所有星期一的平均收益率),以及基于滚动窗口的分位数特征。 超参数优化:使用optuna对LightGBM和CatBoost进行简单调优(可选,为避免运行时间过长,此处仅演示框架)。 模型融合:将CatBoost、LightGBM、Transformer的预测概率进行加权融合,权重在验证集上基于AUC优化。 策略回测:基于融合概率构建简单交易策略,并考虑交易成本。

/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/

# ==================== 1. 安装与导入 ====================
!pip install ta catboost lightgbm optuna --quiet

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
import os
import gc
from tqdm import tqdm

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

from sklearn.metrics import (accuracy_score, roc_auc_score, confusion_matrix,
                             precision_recall_curve, f1_score)
from sklearn.model_selection import TimeSeriesSplit
import lightgbm as lgb
import catboost as cb
import optuna

import ta

warnings.filterwarnings('ignore')

# ==================== 2. 配置参数 ====================
TICKERS = ['AAPL', 'MSFT', 'NVDA', 'GOOGL', 'AMZN', 'WMT', 'JPM', 'JNJ', 'TMUS', 'AMD']
DATA_PATH = '/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/'
TRADE_COST = 0.001  # 0.1% 交易成本

results = {}

# ==================== 3. 修复版特征工程(减少NaN)====================
def engineer_features_cls(df):
    """针对分类任务的特征工程,减少NaN产生"""
    data = df.copy()
    
    # ----- 基础技术指标(同前)-----
    data['SMA_10'] = ta.trend.sma_indicator(data['Close'], 10)
    data['SMA_50'] = ta.trend.sma_indicator(data['Close'], 50)
    data['EMA_10'] = ta.trend.ema_indicator(data['Close'], 10)
    data['EMA_50'] = ta.trend.ema_indicator(data['Close'], 50)
    macd = ta.trend.MACD(data['Close'])
    data['MACD'] = macd.macd()
    data['MACD_signal'] = macd.macd_signal()
    data['MACD_diff'] = macd.macd_diff()
    data['ADX'] = ta.trend.ADXIndicator(data['High'], data['Low'], data['Close'], 14).adx()
    
    data['RSI'] = ta.momentum.RSIIndicator(data['Close'], 14).rsi()
    stoch = ta.momentum.StochasticOscillator(data['High'], data['Low'], data['Close'], 14, 3)
    data['Stoch_K'] = stoch.stoch()
    data['Stoch_D'] = stoch.stoch_signal()
    data['WilliamsR'] = ta.momentum.WilliamsRIndicator(data['High'], data['Low'], data['Close'], 14).williams_r()
    
    bb = ta.volatility.BollingerBands(data['Close'], 20, 2)
    data['BB_high'] = bb.bollinger_hband()
    data['BB_low'] = bb.bollinger_lband()
    data['BB_width'] = bb.bollinger_wband()
    data['ATR'] = ta.volatility.AverageTrueRange(data['High'], data['Low'], data['Close'], 14).average_true_range()
    
    data['OBV'] = ta.volume.OnBalanceVolumeIndicator(data['Close'], data['Volume']).on_balance_volume()
    data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(data['High'], data['Low'], data['Close'], data['Volume'], 20).chaikin_money_flow()
    
    data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
    data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
    data['Volume_Change'] = data['Volume'].pct_change()
    
    # ----- 收益率滞后 -----
    for lag in range(1, 6):
        data[f'return_lag{lag}'] = data['Close'].pct_change(lag) * 100
    
    # ----- 波动率特征(允许部分NaN) -----
    data['vol_5'] = data['Close'].pct_change().rolling(5, min_periods=1).std() * 100
    data['vol_20'] = data['Close'].pct_change().rolling(20, min_periods=1).std() * 100
    
    # ----- 窗口比率特征(使用min_periods=1减少NaN)-----
    windows = [5, 10, 20]
    for w in windows:
        # 窗口内均值 / 当前值(使用min_periods=1,使得前期也有值)
        data[f'mean_{w}_ratio'] = data['Close'].rolling(w, min_periods=1).mean() / data['Close']
        # 窗口内最大值 / 当前值
        data[f'max_{w}_ratio'] = data['Close'].rolling(w, min_periods=1).max() / data['Close']
        # 窗口内最小值 / 当前值
        data[f'min_{w}_ratio'] = data['Close'].rolling(w, min_periods=1).min() / data['Close']
        # 成交量均值 / 当前成交量
        data[f'volume_mean_{w}_ratio'] = data['Volume'].rolling(w, min_periods=1).mean() / data['Volume']
    
    # ----- 日历特征(不会产生NaN)-----
    data['day_of_week'] = data.index.dayofweek
    data['month'] = data.index.month
    
    # ----- 交叉特征(可能继承NaN)-----
    data['OC_pct_vol'] = data['OC_pct'] * data['Volume_Change']
    data['HL_pct_vol'] = data['HL_pct'] * data['Volume_Change']
    data['RSI_OC'] = data['RSI'] * data['OC_pct']
    
    # ----- 目标变量(分类)-----
    data['target_cls'] = (data['Close'].shift(-1) > data['Close']).astype(int)
    
    return data

# ==================== 4. 定义Transformer模型(用于分类)====================
class TransformerClassifier(nn.Module):
    def __init__(self, input_dim, d_model=64, nhead=4, num_layers=2, dim_feedforward=128, dropout=0.1):
        super().__init__()
        self.input_proj = nn.Linear(input_dim, d_model)
        encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, 
                                                    dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True)
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        self.fc = nn.Linear(d_model, 1)
        
    def forward(self, x):
        # x shape: (batch, seq_len, input_dim)
        x = self.input_proj(x)
        x = self.transformer(x)
        # 取最后一个时间步的输出
        out = self.fc(x[:, -1, :])
        return torch.sigmoid(out).squeeze()

# ==================== 5. 特征筛选函数(基于CatBoost重要性)====================
def select_features_cls(X_train, y_train, top_k=200):
    """使用CatBoost分类器筛选特征"""
    model = cb.CatBoostClassifier(iterations=100, verbose=0, random_seed=42)
    model.fit(X_train, y_train)
    importance = model.feature_importances_
    feat_imp = pd.DataFrame({'feature': X_train.columns, 'importance': importance})
    feat_imp = feat_imp.sort_values('importance', ascending=False).head(top_k)
    selected = feat_imp['feature'].tolist()
    return selected

# ==================== 6. 单只股票处理函数(分类,无dropna)====================
def process_stock_cls(ticker):
    print(f"\n{'='*60}\n处理股票: {ticker}\n{'='*60}")
    
    file_path = os.path.join(DATA_PATH, f"{ticker.lower()}.us.txt")
    if not os.path.exists(file_path):
        print(f"文件不存在: {file_path}")
        return None
    
    df = pd.read_csv(file_path)
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index('Date').sort_index()
    
    if len(df) < 500:
        print(f"原始数据量不足: {len(df)},跳过")
        return None
    
    data = engineer_features_cls(df)
    
    # 处理无穷大(但不dropna)
    data = data.replace([np.inf, -np.inf], np.nan)
    
    # 特征列
    feature_cols = [c for c in data.columns if c not in ['target_cls']]
    X = data[feature_cols]
    y = data['target_cls']
    
    # 检查有效样本数(y不能为NaN)
    valid_idx = ~y.isna()
    X = X[valid_idx]
    y = y[valid_idx]
    print(f"有效样本数(目标非空): {len(X)}")
    if len(X) < 500:
        print(f"有效样本不足: {len(X)},跳过")
        return None
    
    # 划分:训练(60%) + 验证(20%) + 测试(20%)
    n = len(X)
    train_end = int(n * 0.6)
    val_end = int(n * 0.8)
    
    if train_end == 0 or val_end == train_end or n - val_end == 0:
        print(f"划分后某集合为空,跳过")
        return None
    
    X_train = X.iloc[:train_end]
    y_train = y.iloc[:train_end]
    X_val = X.iloc[train_end:val_end]
    y_val = y.iloc[train_end:val_end]
    X_test = X.iloc[val_end:]
    y_test = y.iloc[val_end:]
    
    print(f"训练集: {X_train.index[0]}{X_train.index[-1]}, 样本数 {len(X_train)}")
    print(f"验证集: {X_val.index[0]}{X_val.index[-1]}, 样本数 {len(X_val)}")
    print(f"测试集: {X_test.index[0]}{X_test.index[-1]}, 样本数 {len(X_test)}")
    
    # ---------- 特征筛选 ----------
    print("\n正在进行特征筛选...")
    if X_train.shape[1] == 0:
        print("训练集无特征,跳过")
        return None
    top_k = min(200, X_train.shape[1])
    selected_features = select_features_cls(X_train, y_train, top_k=top_k)
    if len(selected_features) == 0:
        print("特征筛选后无特征,跳过")
        return None
    X_train_sel = X_train[selected_features]
    X_val_sel = X_val[selected_features]
    X_test_sel = X_test[selected_features]
    print(f"筛选后特征数: {len(selected_features)}")
    
    # ---------- 模型1: CatBoost ----------
    print("\n训练 CatBoost...")
    cb_model = cb.CatBoostClassifier(iterations=500, learning_rate=0.05, depth=5, 
                                      verbose=0, random_seed=42, eval_metric='AUC')
    cb_model.fit(X_train_sel, y_train, eval_set=(X_val_sel, y_val), early_stopping_rounds=50, verbose=False)
    cb_pred = cb_model.predict_proba(X_test_sel)[:, 1]
    cb_val_pred = cb_model.predict_proba(X_val_sel)[:, 1]
    cb_auc = roc_auc_score(y_val, cb_val_pred)
    print(f"CatBoost 验证集 AUC: {cb_auc:.4f}")
    
    # ---------- 模型2: LightGBM ----------
    print("\n训练 LightGBM...")
    # 修正:移除了fit中的verbose参数,在构造模型时设置verbose=-1
    lgb_model = lgb.LGBMClassifier(n_estimators=300, max_depth=5, learning_rate=0.05, 
                                    random_state=42, verbose=-1)
    lgb_model.fit(X_train_sel, y_train, eval_set=[(X_val_sel, y_val)], 
                  callbacks=[lgb.early_stopping(50)])
    lgb_pred = lgb_model.predict_proba(X_test_sel)[:, 1]
    lgb_val_pred = lgb_model.predict_proba(X_val_sel)[:, 1]
    lgb_auc = roc_auc_score(y_val, lgb_val_pred)
    print(f"LightGBM 验证集 AUC: {lgb_auc:.4f}")
    
    # ---------- 模型3: Transformer (需构造序列,填充NaN) ----------
    seq_len = 20
    def create_sequences(X, y, seq_len):
        X_seq, y_seq = [], []
        for i in range(len(X) - seq_len):
            # 获取窗口数据,并将NaN填充为0(简单填充)
            window = X.iloc[i:i+seq_len].values
            window = np.nan_to_num(window, nan=0.0)  # 填充NaN为0
            X_seq.append(window)
            y_seq.append(y.iloc[i+seq_len])
        return np.array(X_seq), np.array(y_seq)
    
    # 检查数据量是否足够构造序列
    if len(X_train_sel) <= seq_len or len(X_val_sel) <= seq_len or len(X_test_sel) <= seq_len:
        print("数据量不足以构造序列,跳过Transformer")
        # 只用树模型融合
        weights = np.array([cb_auc, lgb_auc]) / (cb_auc + lgb_auc)
        ensemble_val_prob = weights[0]*cb_val_pred + weights[1]*lgb_val_pred
        ensemble_val_auc = roc_auc_score(y_val, ensemble_val_prob)
        print(f"仅树模型融合验证集 AUC: {ensemble_val_auc:.4f}")
        
        ensemble_test_prob = weights[0]*cb_pred + weights[1]*lgb_pred
        ensemble_test_auc = roc_auc_score(y_test, ensemble_test_prob)
        print(f"仅树模型融合测试集 AUC: {ensemble_test_auc:.4f}")
        
        # 阈值优化
        precision, recall, thresholds = precision_recall_curve(y_val, ensemble_val_prob)
        f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
        best_idx = np.argmax(f1_scores[:-1])
        best_thresh = thresholds[best_idx]
        test_pred_label = (ensemble_test_prob >= best_thresh).astype(int)
        test_acc = accuracy_score(y_test, test_pred_label)
        
        # 策略回测
        test_df = X_test_sel.copy()
        test_df['actual_return'] = df.loc[test_df.index, 'Close'].pct_change().shift(-1).values * 100
        test_df['pred_prob'] = ensemble_test_prob
        test_df['signal'] = test_pred_label
        test_df['prev_signal'] = test_df['signal'].shift(1).fillna(0)
        test_df['trade'] = (test_df['signal'] != test_df['prev_signal']).astype(int)
        test_df['strategy_return'] = test_df['signal'] * test_df['actual_return'] - test_df['trade'] * TRADE_COST * 100
        test_df['buy_hold_return'] = test_df['actual_return']
        test_df['strategy_cum'] = (1 + test_df['strategy_return']/100).cumprod()
        test_df['buy_hold_cum'] = (1 + test_df['buy_hold_return']/100).cumprod()
        
        strategy_return = (test_df['strategy_cum'].iloc[-1] - 1) * 100
        buy_hold_return = (test_df['buy_hold_cum'].iloc[-1] - 1) * 100
        sharpe = test_df['strategy_return'].mean() / test_df['strategy_return'].std() * np.sqrt(252)
        
        return {
            'ticker': ticker,
            'test_auc': ensemble_test_auc,
            'test_acc': test_acc,
            'best_thresh': best_thresh,
            'strategy_return': strategy_return,
            'buy_hold_return': buy_hold_return,
            'sharpe': sharpe,
            'n_trades': test_df['trade'].sum()
        }
    
    X_train_seq, y_train_seq = create_sequences(X_train_sel, y_train, seq_len)
    X_val_seq, y_val_seq = create_sequences(X_val_sel, y_val, seq_len)
    X_test_seq, y_test_seq = create_sequences(X_test_sel, y_test, seq_len)
    print(f"\nTransformer 训练样本数: {len(X_train_seq)}")
    
    # 转换为PyTorch张量
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    train_dataset = TensorDataset(torch.FloatTensor(X_train_seq), torch.FloatTensor(y_train_seq))
    val_dataset = TensorDataset(torch.FloatTensor(X_val_seq), torch.FloatTensor(y_val_seq))
    test_dataset = TensorDataset(torch.FloatTensor(X_test_seq), torch.FloatTensor(y_test_seq))
    
    train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
    val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
    test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
    
    # 初始化Transformer
    input_dim = X_train_sel.shape[1]
    tf_model = TransformerClassifier(input_dim=input_dim, d_model=64, nhead=4, num_layers=2).to(device)
    criterion = nn.BCELoss()
    optimizer = optim.Adam(tf_model.parameters(), lr=0.001)
    
    # 训练(早停)
    best_val_auc = 0
    patience = 10
    counter = 0
    for epoch in range(100):
        tf_model.train()
        train_loss = 0
        for Xb, yb in train_loader:
            Xb, yb = Xb.to(device), yb.to(device)
            optimizer.zero_grad()
            pred = tf_model(Xb)
            loss = criterion(pred, yb)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
        
        # 验证
        tf_model.eval()
        val_preds = []
        with torch.no_grad():
            for Xb, _ in val_loader:
                Xb = Xb.to(device)
                pred = tf_model(Xb)
                val_preds.extend(pred.cpu().numpy())
        val_auc = roc_auc_score(y_val_seq, val_preds)
        
        if val_auc > best_val_auc:
            best_val_auc = val_auc
            counter = 0
            torch.save(tf_model.state_dict(), 'best_tf.pth')
        else:
            counter += 1
            if counter >= patience:
                break
    
    # 加载最佳模型并在测试集上预测
    tf_model.load_state_dict(torch.load('best_tf.pth'))
    tf_model.eval()
    tf_test_preds = []
    with torch.no_grad():
        for Xb, _ in test_loader:
            Xb = Xb.to(device)
            pred = tf_model(Xb)
            tf_test_preds.extend(pred.cpu().numpy())
    tf_test_preds = np.array(tf_test_preds)
    
    # 对齐标签
    y_test_aligned = y_test.iloc[seq_len:].values
    y_val_aligned = y_val.iloc[seq_len:].values
    
    # 对齐树模型预测
    cb_val_aligned = cb_val_pred[seq_len:]
    lgb_val_aligned = lgb_val_pred[seq_len:]
    cb_test_aligned = cb_pred[seq_len:]
    lgb_test_aligned = lgb_pred[seq_len:]
    
    # ---------- 模型融合 ----------
    aucs = np.array([roc_auc_score(y_val_aligned, cb_val_aligned),
                     roc_auc_score(y_val_aligned, lgb_val_aligned),
                     best_val_auc])
    weights = aucs / aucs.sum()
    print(f"\n融合权重: CB={weights[0]:.3f}, LGB={weights[1]:.3f}, TF={weights[2]:.3f}")
    
    ensemble_val_prob = weights[0]*cb_val_aligned + weights[1]*lgb_val_aligned + weights[2]*np.array(val_preds)
    ensemble_val_auc = roc_auc_score(y_val_aligned, ensemble_val_prob)
    print(f"融合模型验证集 AUC: {ensemble_val_auc:.4f}")
    
    ensemble_test_prob = weights[0]*cb_test_aligned + weights[1]*lgb_test_aligned + weights[2]*tf_test_preds
    ensemble_test_auc = roc_auc_score(y_test_aligned, ensemble_test_prob)
    print(f"融合模型测试集 AUC: {ensemble_test_auc:.4f}")
    
    # ---------- 阈值优化与策略回测 ----------
    precision, recall, thresholds = precision_recall_curve(y_val_aligned, ensemble_val_prob)
    f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
    best_idx = np.argmax(f1_scores[:-1])
    best_thresh = thresholds[best_idx]
    print(f"最佳阈值 (基于验证集F1): {best_thresh:.4f}")
    
    test_pred_label = (ensemble_test_prob >= best_thresh).astype(int)
    test_acc = accuracy_score(y_test_aligned, test_pred_label)
    print(f"测试集准确率: {test_acc:.4f}")
    
    test_df = X_test_sel.iloc[seq_len:].copy()
    test_df['actual_return'] = df.loc[test_df.index, 'Close'].pct_change().shift(-1).values * 100
    test_df['pred_prob'] = ensemble_test_prob
    test_df['signal'] = test_pred_label
    
    test_df['prev_signal'] = test_df['signal'].shift(1).fillna(0)
    test_df['trade'] = (test_df['signal'] != test_df['prev_signal']).astype(int)
    test_df['strategy_return'] = test_df['signal'] * test_df['actual_return'] - test_df['trade'] * TRADE_COST * 100
    test_df['buy_hold_return'] = test_df['actual_return']
    
    test_df['strategy_cum'] = (1 + test_df['strategy_return']/100).cumprod()
    test_df['buy_hold_cum'] = (1 + test_df['buy_hold_return']/100).cumprod()
    
    strategy_return = (test_df['strategy_cum'].iloc[-1] - 1) * 100
    buy_hold_return = (test_df['buy_hold_cum'].iloc[-1] - 1) * 100
    sharpe = test_df['strategy_return'].mean() / test_df['strategy_return'].std() * np.sqrt(252)
    
    print(f"\n策略总收益率 (考虑成本): {strategy_return:.2f}%")
    print(f"买入持有收益率: {buy_hold_return:.2f}%")
    print(f"策略夏普比率: {sharpe:.2f}")
    print(f"交易次数: {test_df['trade'].sum()}")
    
    plt.figure(figsize=(10,5))
    plt.plot(test_df.index, test_df['strategy_cum'], label='Strategy (with cost)')
    plt.plot(test_df.index, test_df['buy_hold_cum'], label='Buy & Hold')
    plt.title(f'{ticker} - Classification Strategy vs Buy & Hold')
    plt.xlabel('Date')
    plt.ylabel('Cumulative Return')
    plt.legend()
    plt.grid(True)
    plt.show()
    
    return {
        'ticker': ticker,
        'test_auc': ensemble_test_auc,
        'test_acc': test_acc,
        'best_thresh': best_thresh,
        'strategy_return': strategy_return,
        'buy_hold_return': buy_hold_return,
        'sharpe': sharpe,
        'n_trades': test_df['trade'].sum()
    }

# ==================== 7. 主循环 ====================
for ticker in TICKERS:
    result = process_stock_cls(ticker)
    if result:
        results[ticker] = result
    gc.collect()

# ==================== 8. 结果汇总 ====================
valid_results = {k: v for k, v in results.items() if v is not None and isinstance(v, dict)}
if len(valid_results) == 0:
    print("\n没有有效结果,请检查各股票处理是否成功。")
else:
    summary = pd.DataFrame(valid_results).T
    print("\n所有结果包含的键:", set().union(*[set(v.keys()) for v in valid_results.values()]))
    
    required_cols = ['test_auc', 'test_acc', 'strategy_return', 'buy_hold_return', 'sharpe', 'n_trades']
    missing_cols = [col for col in required_cols if col not in summary.columns]
    if missing_cols:
        print(f"\n警告:下列列缺失:{missing_cols},无法生成汇总。")
    else:
        print("\n\n========== 多股票分类结果汇总 ==========")
        print(summary[required_cols].to_string())
        
        fig, axes = plt.subplots(2, 2, figsize=(14,10))
        summary['test_auc'].sort_values().plot(kind='bar', ax=axes[0,0], color='skyblue')
        axes[0,0].set_title('Test AUC')
        axes[0,0].axhline(y=0.5, color='red', linestyle='--')
        
        summary['test_acc'].sort_values().plot(kind='bar', ax=axes[0,1], color='lightgreen')
        axes[0,1].set_title('Test Accuracy')
        axes[0,1].axhline(y=0.5, color='red', linestyle='--')
        
        summary[['strategy_return', 'buy_hold_return']].plot(kind='bar', ax=axes[1,0])
        axes[1,0].set_title('Strategy vs Buy & Hold Return')
        
        summary['sharpe'].sort_values().plot(kind='bar', ax=axes[1,1], color='coral')
        axes[1,1].set_title('Strategy Sharpe Ratio')
        
        plt.tight_layout()
        plt.show()
        
        summary.to_csv('classification_results.csv')
        print("\n结果已保存至 classification_results.csv")

============================================================
处理股票: AAPL
============================================================
有效样本数(目标非空): 8364
训练集: 1984-09-07 00:00:00 至 2004-07-30 00:00:00, 样本数 5018
验证集: 2004-08-02 00:00:00 至 2011-03-22 00:00:00, 样本数 1673
测试集: 2011-03-23 00:00:00 至 2017-11-10 00:00:00, 样本数 1673

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5417

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's binary_logloss: 0.698878
LightGBM 验证集 AUC: 0.5329

Transformer 训练样本数: 4998

融合权重: CB=0.345, LGB=0.340, TF=0.315
融合模型验证集 AUC: 0.5543
融合模型测试集 AUC: 0.4963
最佳阈值 (基于验证集F1): 0.4826
测试集准确率: 0.4828

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.25
交易次数: 466


============================================================
处理股票: MSFT
============================================================
有效样本数(目标非空): 7983
训练集: 1986-03-13 00:00:00 至 2005-03-08 00:00:00, 样本数 4789
验证集: 2005-03-09 00:00:00 至 2011-07-11 00:00:00, 样本数 1597
测试集: 2011-07-12 00:00:00 至 2017-11-10 00:00:00, 样本数 1597

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5253

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[17]    valid_0's binary_logloss: 0.69558
LightGBM 验证集 AUC: 0.5082

Transformer 训练样本数: 4769

融合权重: CB=0.333, LGB=0.322, TF=0.345
融合模型验证集 AUC: 0.5233
融合模型测试集 AUC: 0.4967
最佳阈值 (基于验证集F1): 0.4007
测试集准确率: 0.5079

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.05
交易次数: 9


============================================================
处理股票: NVDA
============================================================
有效样本数(目标非空): 4733
训练集: 1999-01-22 00:00:00 至 2010-05-06 00:00:00, 样本数 2839
验证集: 2010-05-07 00:00:00 至 2014-02-10 00:00:00, 样本数 947
测试集: 2014-02-11 00:00:00 至 2017-11-10 00:00:00, 样本数 947

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5278

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[5] valid_0's binary_logloss: 0.692615
LightGBM 验证集 AUC: 0.5209

Transformer 训练样本数: 2819

融合权重: CB=0.337, LGB=0.331, TF=0.332
融合模型验证集 AUC: 0.5245
融合模型测试集 AUC: 0.4930
最佳阈值 (基于验证集F1): 0.4668
测试集准确率: 0.5491

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.97
交易次数: 1


============================================================
处理股票: GOOGL
============================================================
有效样本数(目标非空): 3333
训练集: 2004-08-19 00:00:00 至 2012-07-25 00:00:00, 样本数 1999
验证集: 2012-07-26 00:00:00 至 2015-03-23 00:00:00, 样本数 667
测试集: 2015-03-24 00:00:00 至 2017-11-10 00:00:00, 样本数 667

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5270

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's binary_logloss: 0.692734
LightGBM 验证集 AUC: 0.4846

Transformer 训练样本数: 1979

融合权重: CB=0.354, LGB=0.323, TF=0.323
融合模型验证集 AUC: 0.5272
融合模型测试集 AUC: 0.4494
最佳阈值 (基于验证集F1): 0.5066
测试集准确率: 0.4930

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.59
交易次数: 193


============================================================
处理股票: AMZN
============================================================
有效样本数(目标非空): 5153
训练集: 1997-05-16 00:00:00 至 2009-09-03 00:00:00, 样本数 3091
验证集: 2009-09-04 00:00:00 至 2013-10-09 00:00:00, 样本数 1031
测试集: 2013-10-10 00:00:00 至 2017-11-10 00:00:00, 样本数 1031

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5290

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[2] valid_0's binary_logloss: 0.69359
LightGBM 验证集 AUC: 0.5195

Transformer 训练样本数: 3071

融合权重: CB=0.339, LGB=0.335, TF=0.325
融合模型验证集 AUC: 0.5319
融合模型测试集 AUC: 0.5142
最佳阈值 (基于验证集F1): 0.4665
测试集准确率: 0.5361

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.15
交易次数: 1


============================================================
处理股票: WMT
============================================================
有效样本数(目标非空): 11443
训练集: 1972-03-20 00:00:00 至 1999-08-31 00:00:00, 样本数 6865
验证集: 1999-09-01 00:00:00 至 2008-10-09 00:00:00, 样本数 2289
测试集: 2008-10-10 00:00:00 至 2017-11-10 00:00:00, 样本数 2289

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5198

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[42]    valid_0's binary_logloss: 0.696587
LightGBM 验证集 AUC: 0.5087

Transformer 训练样本数: 6845

融合权重: CB=0.333, LGB=0.327, TF=0.340
融合模型验证集 AUC: 0.5107
融合模型测试集 AUC: 0.4817
最佳阈值 (基于验证集F1): 0.3288
测试集准确率: 0.5253

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.52
交易次数: 1


============================================================
处理股票: JPM
============================================================
有效样本数(目标非空): 12074
训练集: 1970-01-02 00:00:00 至 1998-08-31 00:00:00, 样本数 7244
验证集: 1998-09-01 00:00:00 至 2008-04-11 00:00:00, 样本数 2415
测试集: 2008-04-14 00:00:00 至 2017-11-10 00:00:00, 样本数 2415

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5259

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[12]    valid_0's binary_logloss: 0.693436
LightGBM 验证集 AUC: 0.5157

Transformer 训练样本数: 7224

融合权重: CB=0.342, LGB=0.334, TF=0.324
融合模型验证集 AUC: 0.5272
融合模型测试集 AUC: 0.5068
最佳阈值 (基于验证集F1): 0.3718
测试集准确率: 0.5086

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.48
交易次数: 5


============================================================
处理股票: JNJ
============================================================
有效样本数(目标非空): 12074
训练集: 1970-01-02 00:00:00 至 1998-09-01 00:00:00, 样本数 7244
验证集: 1998-09-02 00:00:00 至 2008-04-11 00:00:00, 样本数 2415
测试集: 2008-04-14 00:00:00 至 2017-11-10 00:00:00, 样本数 2415

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5340

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[34]    valid_0's binary_logloss: 0.694645
LightGBM 验证集 AUC: 0.5229

Transformer 训练样本数: 7224

融合权重: CB=0.340, LGB=0.333, TF=0.327
融合模型验证集 AUC: 0.5265
融合模型测试集 AUC: 0.5177
最佳阈值 (基于验证集F1): 0.3993
测试集准确率: 0.5123

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.65
交易次数: 45


============================================================
处理股票: TMUS
============================================================
有效样本数(目标非空): 2663
训练集: 2007-04-18 00:00:00 至 2013-08-20 00:00:00, 样本数 1597
验证集: 2013-08-21 00:00:00 至 2015-10-01 00:00:00, 样本数 533
测试集: 2015-10-02 00:00:00 至 2017-11-10 00:00:00, 样本数 533

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5136

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's binary_logloss: 0.693029
LightGBM 验证集 AUC: 0.4956

Transformer 训练样本数: 1577
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 434
    432 # ==================== 7. 主循环 ====================
    433 for ticker in TICKERS:
--> 434     result = process_stock_cls(ticker)
    435     if result:
    436         results[ticker] = result

Cell In[4], line 330, in process_stock_cls(ticker)
    328         Xb = Xb.to(device)
    329         pred = tf_model(Xb)
--> 330         val_preds.extend(pred.cpu().numpy())
    331 val_auc = roc_auc_score(y_val_seq, val_preds)
    333 if val_auc > best_val_auc:

TypeError: iteration over a 0-d array

已修正收集Transformer预测时遇到了错误,因为pred可能在某些情况下变成0维数组。

# ==================== 1. 安装与导入 ====================
!pip install ta catboost lightgbm optuna --quiet

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
import os
import gc
from tqdm import tqdm

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

from sklearn.metrics import (accuracy_score, roc_auc_score, confusion_matrix,
                             precision_recall_curve, f1_score)
from sklearn.model_selection import TimeSeriesSplit
import lightgbm as lgb
import catboost as cb
import optuna

import ta

warnings.filterwarnings('ignore')

# ==================== 2. 配置参数 ====================
TICKERS = ['AAPL', 'MSFT', 'NVDA', 'GOOGL', 'AMZN', 'WMT', 'JPM', 'JNJ', 'TMUS', 'AMD']
DATA_PATH = '/kaggle/input/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs/Stocks/'
TRADE_COST = 0.001  # 0.1% 交易成本

results = {}

# ==================== 3. 修复版特征工程(减少NaN)====================
def engineer_features_cls(df):
    """针对分类任务的特征工程,减少NaN产生"""
    data = df.copy()
    
    # ----- 基础技术指标(同前)-----
    data['SMA_10'] = ta.trend.sma_indicator(data['Close'], 10)
    data['SMA_50'] = ta.trend.sma_indicator(data['Close'], 50)
    data['EMA_10'] = ta.trend.ema_indicator(data['Close'], 10)
    data['EMA_50'] = ta.trend.ema_indicator(data['Close'], 50)
    macd = ta.trend.MACD(data['Close'])
    data['MACD'] = macd.macd()
    data['MACD_signal'] = macd.macd_signal()
    data['MACD_diff'] = macd.macd_diff()
    data['ADX'] = ta.trend.ADXIndicator(data['High'], data['Low'], data['Close'], 14).adx()
    
    data['RSI'] = ta.momentum.RSIIndicator(data['Close'], 14).rsi()
    stoch = ta.momentum.StochasticOscillator(data['High'], data['Low'], data['Close'], 14, 3)
    data['Stoch_K'] = stoch.stoch()
    data['Stoch_D'] = stoch.stoch_signal()
    data['WilliamsR'] = ta.momentum.WilliamsRIndicator(data['High'], data['Low'], data['Close'], 14).williams_r()
    
    bb = ta.volatility.BollingerBands(data['Close'], 20, 2)
    data['BB_high'] = bb.bollinger_hband()
    data['BB_low'] = bb.bollinger_lband()
    data['BB_width'] = bb.bollinger_wband()
    data['ATR'] = ta.volatility.AverageTrueRange(data['High'], data['Low'], data['Close'], 14).average_true_range()
    
    data['OBV'] = ta.volume.OnBalanceVolumeIndicator(data['Close'], data['Volume']).on_balance_volume()
    data['CMF'] = ta.volume.ChaikinMoneyFlowIndicator(data['High'], data['Low'], data['Close'], data['Volume'], 20).chaikin_money_flow()
    
    data['HL_pct'] = (data['High'] - data['Low']) / data['Close'] * 100
    data['OC_pct'] = (data['Close'] - data['Open']) / data['Open'] * 100
    data['Volume_Change'] = data['Volume'].pct_change()
    
    # ----- 收益率滞后 -----
    for lag in range(1, 6):
        data[f'return_lag{lag}'] = data['Close'].pct_change(lag) * 100
    
    # ----- 波动率特征(允许部分NaN) -----
    data['vol_5'] = data['Close'].pct_change().rolling(5, min_periods=1).std() * 100
    data['vol_20'] = data['Close'].pct_change().rolling(20, min_periods=1).std() * 100
    
    # ----- 窗口比率特征(使用min_periods=1减少NaN)-----
    windows = [5, 10, 20]
    for w in windows:
        # 窗口内均值 / 当前值(使用min_periods=1,使得前期也有值)
        data[f'mean_{w}_ratio'] = data['Close'].rolling(w, min_periods=1).mean() / data['Close']
        # 窗口内最大值 / 当前值
        data[f'max_{w}_ratio'] = data['Close'].rolling(w, min_periods=1).max() / data['Close']
        # 窗口内最小值 / 当前值
        data[f'min_{w}_ratio'] = data['Close'].rolling(w, min_periods=1).min() / data['Close']
        # 成交量均值 / 当前成交量
        data[f'volume_mean_{w}_ratio'] = data['Volume'].rolling(w, min_periods=1).mean() / data['Volume']
    
    # ----- 日历特征(不会产生NaN)-----
    data['day_of_week'] = data.index.dayofweek
    data['month'] = data.index.month
    
    # ----- 交叉特征(可能继承NaN)-----
    data['OC_pct_vol'] = data['OC_pct'] * data['Volume_Change']
    data['HL_pct_vol'] = data['HL_pct'] * data['Volume_Change']
    data['RSI_OC'] = data['RSI'] * data['OC_pct']
    
    # ----- 目标变量(分类)-----
    data['target_cls'] = (data['Close'].shift(-1) > data['Close']).astype(int)
    
    return data

# ==================== 4. 定义Transformer模型(用于分类)====================
class TransformerClassifier(nn.Module):
    def __init__(self, input_dim, d_model=64, nhead=4, num_layers=2, dim_feedforward=128, dropout=0.1):
        super().__init__()
        self.input_proj = nn.Linear(input_dim, d_model)
        encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, 
                                                    dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True)
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        self.fc = nn.Linear(d_model, 1)
        
    def forward(self, x):
        # x shape: (batch, seq_len, input_dim)
        x = self.input_proj(x)
        x = self.transformer(x)
        # 取最后一个时间步的输出
        out = self.fc(x[:, -1, :])  # shape: (batch, 1)
        return torch.sigmoid(out).squeeze()  # shape: (batch,)

# ==================== 5. 特征筛选函数(基于CatBoost重要性)====================
def select_features_cls(X_train, y_train, top_k=200):
    """使用CatBoost分类器筛选特征"""
    model = cb.CatBoostClassifier(iterations=100, verbose=0, random_seed=42)
    model.fit(X_train, y_train)
    importance = model.feature_importances_
    feat_imp = pd.DataFrame({'feature': X_train.columns, 'importance': importance})
    feat_imp = feat_imp.sort_values('importance', ascending=False).head(top_k)
    selected = feat_imp['feature'].tolist()
    return selected

# ==================== 6. 单只股票处理函数(分类,无dropna)====================
def process_stock_cls(ticker):
    print(f"\n{'='*60}\n处理股票: {ticker}\n{'='*60}")
    
    file_path = os.path.join(DATA_PATH, f"{ticker.lower()}.us.txt")
    if not os.path.exists(file_path):
        print(f"文件不存在: {file_path}")
        return None
    
    df = pd.read_csv(file_path)
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index('Date').sort_index()
    
    if len(df) < 500:
        print(f"原始数据量不足: {len(df)},跳过")
        return None
    
    data = engineer_features_cls(df)
    
    # 处理无穷大(但不dropna)
    data = data.replace([np.inf, -np.inf], np.nan)
    
    # 特征列
    feature_cols = [c for c in data.columns if c not in ['target_cls']]
    X = data[feature_cols]
    y = data['target_cls']
    
    # 检查有效样本数(y不能为NaN)
    valid_idx = ~y.isna()
    X = X[valid_idx]
    y = y[valid_idx]
    print(f"有效样本数(目标非空): {len(X)}")
    if len(X) < 500:
        print(f"有效样本不足: {len(X)},跳过")
        return None
    
    # 划分:训练(60%) + 验证(20%) + 测试(20%)
    n = len(X)
    train_end = int(n * 0.6)
    val_end = int(n * 0.8)
    
    if train_end == 0 or val_end == train_end or n - val_end == 0:
        print(f"划分后某集合为空,跳过")
        return None
    
    X_train = X.iloc[:train_end]
    y_train = y.iloc[:train_end]
    X_val = X.iloc[train_end:val_end]
    y_val = y.iloc[train_end:val_end]
    X_test = X.iloc[val_end:]
    y_test = y.iloc[val_end:]
    
    print(f"训练集: {X_train.index[0]}{X_train.index[-1]}, 样本数 {len(X_train)}")
    print(f"验证集: {X_val.index[0]}{X_val.index[-1]}, 样本数 {len(X_val)}")
    print(f"测试集: {X_test.index[0]}{X_test.index[-1]}, 样本数 {len(X_test)}")
    
    # ---------- 特征筛选 ----------
    print("\n正在进行特征筛选...")
    if X_train.shape[1] == 0:
        print("训练集无特征,跳过")
        return None
    top_k = min(200, X_train.shape[1])
    selected_features = select_features_cls(X_train, y_train, top_k=top_k)
    if len(selected_features) == 0:
        print("特征筛选后无特征,跳过")
        return None
    X_train_sel = X_train[selected_features]
    X_val_sel = X_val[selected_features]
    X_test_sel = X_test[selected_features]
    print(f"筛选后特征数: {len(selected_features)}")
    
    # ---------- 模型1: CatBoost ----------
    print("\n训练 CatBoost...")
    cb_model = cb.CatBoostClassifier(iterations=500, learning_rate=0.05, depth=5, 
                                      verbose=0, random_seed=42, eval_metric='AUC')
    cb_model.fit(X_train_sel, y_train, eval_set=(X_val_sel, y_val), early_stopping_rounds=50, verbose=False)
    cb_pred = cb_model.predict_proba(X_test_sel)[:, 1]
    cb_val_pred = cb_model.predict_proba(X_val_sel)[:, 1]
    cb_auc = roc_auc_score(y_val, cb_val_pred)
    print(f"CatBoost 验证集 AUC: {cb_auc:.4f}")
    
    # ---------- 模型2: LightGBM ----------
    print("\n训练 LightGBM...")
    lgb_model = lgb.LGBMClassifier(n_estimators=300, max_depth=5, learning_rate=0.05, 
                                    random_state=42, verbose=-1)
    lgb_model.fit(X_train_sel, y_train, eval_set=[(X_val_sel, y_val)], 
                  callbacks=[lgb.early_stopping(50)])
    lgb_pred = lgb_model.predict_proba(X_test_sel)[:, 1]
    lgb_val_pred = lgb_model.predict_proba(X_val_sel)[:, 1]
    lgb_auc = roc_auc_score(y_val, lgb_val_pred)
    print(f"LightGBM 验证集 AUC: {lgb_auc:.4f}")
    
    # ---------- 模型3: Transformer (需构造序列,填充NaN) ----------
    seq_len = 20
    def create_sequences(X, y, seq_len):
        X_seq, y_seq = [], []
        for i in range(len(X) - seq_len):
            # 获取窗口数据,并将NaN填充为0(简单填充)
            window = X.iloc[i:i+seq_len].values
            window = np.nan_to_num(window, nan=0.0)  # 填充NaN为0
            X_seq.append(window)
            y_seq.append(y.iloc[i+seq_len])
        return np.array(X_seq), np.array(y_seq)
    
    # 检查数据量是否足够构造序列
    if len(X_train_sel) <= seq_len or len(X_val_sel) <= seq_len or len(X_test_sel) <= seq_len:
        print("数据量不足以构造序列,跳过Transformer")
        # 只用树模型融合
        weights = np.array([cb_auc, lgb_auc]) / (cb_auc + lgb_auc)
        ensemble_val_prob = weights[0]*cb_val_pred + weights[1]*lgb_val_pred
        ensemble_val_auc = roc_auc_score(y_val, ensemble_val_prob)
        print(f"仅树模型融合验证集 AUC: {ensemble_val_auc:.4f}")
        
        ensemble_test_prob = weights[0]*cb_pred + weights[1]*lgb_pred
        ensemble_test_auc = roc_auc_score(y_test, ensemble_test_prob)
        print(f"仅树模型融合测试集 AUC: {ensemble_test_auc:.4f}")
        
        # 阈值优化
        precision, recall, thresholds = precision_recall_curve(y_val, ensemble_val_prob)
        f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
        best_idx = np.argmax(f1_scores[:-1])
        best_thresh = thresholds[best_idx]
        test_pred_label = (ensemble_test_prob >= best_thresh).astype(int)
        test_acc = accuracy_score(y_test, test_pred_label)
        
        # 策略回测
        test_df = X_test_sel.copy()
        test_df['actual_return'] = df.loc[test_df.index, 'Close'].pct_change().shift(-1).values * 100
        test_df['pred_prob'] = ensemble_test_prob
        test_df['signal'] = test_pred_label
        test_df['prev_signal'] = test_df['signal'].shift(1).fillna(0)
        test_df['trade'] = (test_df['signal'] != test_df['prev_signal']).astype(int)
        test_df['strategy_return'] = test_df['signal'] * test_df['actual_return'] - test_df['trade'] * TRADE_COST * 100
        test_df['buy_hold_return'] = test_df['actual_return']
        test_df['strategy_cum'] = (1 + test_df['strategy_return']/100).cumprod()
        test_df['buy_hold_cum'] = (1 + test_df['buy_hold_return']/100).cumprod()
        
        strategy_return = (test_df['strategy_cum'].iloc[-1] - 1) * 100
        buy_hold_return = (test_df['buy_hold_cum'].iloc[-1] - 1) * 100
        sharpe = test_df['strategy_return'].mean() / test_df['strategy_return'].std() * np.sqrt(252)
        
        return {
            'ticker': ticker,
            'test_auc': ensemble_test_auc,
            'test_acc': test_acc,
            'best_thresh': best_thresh,
            'strategy_return': strategy_return,
            'buy_hold_return': buy_hold_return,
            'sharpe': sharpe,
            'n_trades': test_df['trade'].sum()
        }
    
    X_train_seq, y_train_seq = create_sequences(X_train_sel, y_train, seq_len)
    X_val_seq, y_val_seq = create_sequences(X_val_sel, y_val, seq_len)
    X_test_seq, y_test_seq = create_sequences(X_test_sel, y_test, seq_len)
    print(f"\nTransformer 训练样本数: {len(X_train_seq)}")
    
    # 转换为PyTorch张量
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    train_dataset = TensorDataset(torch.FloatTensor(X_train_seq), torch.FloatTensor(y_train_seq))
    val_dataset = TensorDataset(torch.FloatTensor(X_val_seq), torch.FloatTensor(y_val_seq))
    test_dataset = TensorDataset(torch.FloatTensor(X_test_seq), torch.FloatTensor(y_test_seq))
    
    train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
    val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
    test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
    
    # 初始化Transformer
    input_dim = X_train_sel.shape[1]
    tf_model = TransformerClassifier(input_dim=input_dim, d_model=64, nhead=4, num_layers=2).to(device)
    criterion = nn.BCELoss()
    optimizer = optim.Adam(tf_model.parameters(), lr=0.001)
    
    # 训练(早停)
    best_val_auc = 0
    patience = 10
    counter = 0
    for epoch in range(100):
        tf_model.train()
        train_loss = 0
        for Xb, yb in train_loader:
            Xb, yb = Xb.to(device), yb.to(device)
            optimizer.zero_grad()
            pred = tf_model(Xb)
            loss = criterion(pred, yb)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
        
        # 验证
        tf_model.eval()
        val_preds = []
        with torch.no_grad():
            for Xb, _ in val_loader:
                Xb = Xb.to(device)
                pred = tf_model(Xb)
                val_preds.extend(pred.cpu().numpy().flatten().tolist())  # 修复:确保一维
        val_auc = roc_auc_score(y_val_seq, val_preds)
        
        if val_auc > best_val_auc:
            best_val_auc = val_auc
            counter = 0
            torch.save(tf_model.state_dict(), 'best_tf.pth')
        else:
            counter += 1
            if counter >= patience:
                break
    
    # 加载最佳模型并在测试集上预测
    tf_model.load_state_dict(torch.load('best_tf.pth'))
    tf_model.eval()
    tf_test_preds = []
    with torch.no_grad():
        for Xb, _ in test_loader:
            Xb = Xb.to(device)
            pred = tf_model(Xb)
            tf_test_preds.extend(pred.cpu().numpy().flatten().tolist())  # 修复
    tf_test_preds = np.array(tf_test_preds)
    
    # 对齐标签
    y_test_aligned = y_test.iloc[seq_len:].values
    y_val_aligned = y_val.iloc[seq_len:].values
    
    # 对齐树模型预测
    cb_val_aligned = cb_val_pred[seq_len:]
    lgb_val_aligned = lgb_val_pred[seq_len:]
    cb_test_aligned = cb_pred[seq_len:]
    lgb_test_aligned = lgb_pred[seq_len:]
    
    # ---------- 模型融合 ----------
    aucs = np.array([roc_auc_score(y_val_aligned, cb_val_aligned),
                     roc_auc_score(y_val_aligned, lgb_val_aligned),
                     best_val_auc])
    weights = aucs / aucs.sum()
    print(f"\n融合权重: CB={weights[0]:.3f}, LGB={weights[1]:.3f}, TF={weights[2]:.3f}")
    
    ensemble_val_prob = weights[0]*cb_val_aligned + weights[1]*lgb_val_aligned + weights[2]*np.array(val_preds)
    ensemble_val_auc = roc_auc_score(y_val_aligned, ensemble_val_prob)
    print(f"融合模型验证集 AUC: {ensemble_val_auc:.4f}")
    
    ensemble_test_prob = weights[0]*cb_test_aligned + weights[1]*lgb_test_aligned + weights[2]*tf_test_preds
    ensemble_test_auc = roc_auc_score(y_test_aligned, ensemble_test_prob)
    print(f"融合模型测试集 AUC: {ensemble_test_auc:.4f}")
    
    # ---------- 阈值优化与策略回测 ----------
    precision, recall, thresholds = precision_recall_curve(y_val_aligned, ensemble_val_prob)
    f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
    best_idx = np.argmax(f1_scores[:-1])
    best_thresh = thresholds[best_idx]
    print(f"最佳阈值 (基于验证集F1): {best_thresh:.4f}")
    
    test_pred_label = (ensemble_test_prob >= best_thresh).astype(int)
    test_acc = accuracy_score(y_test_aligned, test_pred_label)
    print(f"测试集准确率: {test_acc:.4f}")
    
    test_df = X_test_sel.iloc[seq_len:].copy()
    test_df['actual_return'] = df.loc[test_df.index, 'Close'].pct_change().shift(-1).values * 100
    test_df['pred_prob'] = ensemble_test_prob
    test_df['signal'] = test_pred_label
    
    test_df['prev_signal'] = test_df['signal'].shift(1).fillna(0)
    test_df['trade'] = (test_df['signal'] != test_df['prev_signal']).astype(int)
    test_df['strategy_return'] = test_df['signal'] * test_df['actual_return'] - test_df['trade'] * TRADE_COST * 100
    test_df['buy_hold_return'] = test_df['actual_return']
    
    test_df['strategy_cum'] = (1 + test_df['strategy_return']/100).cumprod()
    test_df['buy_hold_cum'] = (1 + test_df['buy_hold_return']/100).cumprod()
    
    strategy_return = (test_df['strategy_cum'].iloc[-1] - 1) * 100
    buy_hold_return = (test_df['buy_hold_cum'].iloc[-1] - 1) * 100
    sharpe = test_df['strategy_return'].mean() / test_df['strategy_return'].std() * np.sqrt(252)
    
    print(f"\n策略总收益率 (考虑成本): {strategy_return:.2f}%")
    print(f"买入持有收益率: {buy_hold_return:.2f}%")
    print(f"策略夏普比率: {sharpe:.2f}")
    print(f"交易次数: {test_df['trade'].sum()}")
    
    plt.figure(figsize=(10,5))
    plt.plot(test_df.index, test_df['strategy_cum'], label='Strategy (with cost)')
    plt.plot(test_df.index, test_df['buy_hold_cum'], label='Buy & Hold')
    plt.title(f'{ticker} - Classification Strategy vs Buy & Hold')
    plt.xlabel('Date')
    plt.ylabel('Cumulative Return')
    plt.legend()
    plt.grid(True)
    plt.show()
    
    return {
        'ticker': ticker,
        'test_auc': ensemble_test_auc,
        'test_acc': test_acc,
        'best_thresh': best_thresh,
        'strategy_return': strategy_return,
        'buy_hold_return': buy_hold_return,
        'sharpe': sharpe,
        'n_trades': test_df['trade'].sum()
    }

# ==================== 7. 主循环 ====================
for ticker in TICKERS:
    result = process_stock_cls(ticker)
    if result:
        results[ticker] = result
    gc.collect()

# ==================== 8. 结果汇总 ====================
valid_results = {k: v for k, v in results.items() if v is not None and isinstance(v, dict)}
if len(valid_results) == 0:
    print("\n没有有效结果,请检查各股票处理是否成功。")
else:
    summary = pd.DataFrame(valid_results).T
    print("\n所有结果包含的键:", set().union(*[set(v.keys()) for v in valid_results.values()]))
    
    required_cols = ['test_auc', 'test_acc', 'strategy_return', 'buy_hold_return', 'sharpe', 'n_trades']
    missing_cols = [col for col in required_cols if col not in summary.columns]
    if missing_cols:
        print(f"\n警告:下列列缺失:{missing_cols},无法生成汇总。")
    else:
        print("\n\n========== 多股票分类结果汇总 ==========")
        print(summary[required_cols].to_string())
        
        fig, axes = plt.subplots(2, 2, figsize=(14,10))
        summary['test_auc'].sort_values().plot(kind='bar', ax=axes[0,0], color='skyblue')
        axes[0,0].set_title('Test AUC')
        axes[0,0].axhline(y=0.5, color='red', linestyle='--')
        
        summary['test_acc'].sort_values().plot(kind='bar', ax=axes[0,1], color='lightgreen')
        axes[0,1].set_title('Test Accuracy')
        axes[0,1].axhline(y=0.5, color='red', linestyle='--')
        
        summary[['strategy_return', 'buy_hold_return']].plot(kind='bar', ax=axes[1,0])
        axes[1,0].set_title('Strategy vs Buy & Hold Return')
        
        summary['sharpe'].sort_values().plot(kind='bar', ax=axes[1,1], color='coral')
        axes[1,1].set_title('Strategy Sharpe Ratio')
        
        plt.tight_layout()
        plt.show()
        
        summary.to_csv('classification_results.csv')
        print("\n结果已保存至 classification_results.csv")

============================================================
处理股票: AAPL
============================================================
有效样本数(目标非空): 8364
训练集: 1984-09-07 00:00:00 至 2004-07-30 00:00:00, 样本数 5018
验证集: 2004-08-02 00:00:00 至 2011-03-22 00:00:00, 样本数 1673
测试集: 2011-03-23 00:00:00 至 2017-11-10 00:00:00, 样本数 1673

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5417

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's binary_logloss: 0.698878
LightGBM 验证集 AUC: 0.5329

Transformer 训练样本数: 4998

融合权重: CB=0.338, LGB=0.333, TF=0.329
融合模型验证集 AUC: 0.5537
融合模型测试集 AUC: 0.4973
最佳阈值 (基于验证集F1): 0.4747
测试集准确率: 0.5203

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.96
交易次数: 1


============================================================
处理股票: MSFT
============================================================
有效样本数(目标非空): 7983
训练集: 1986-03-13 00:00:00 至 2005-03-08 00:00:00, 样本数 4789
验证集: 2005-03-09 00:00:00 至 2011-07-11 00:00:00, 样本数 1597
测试集: 2011-07-12 00:00:00 至 2017-11-10 00:00:00, 样本数 1597

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5253

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[17]    valid_0's binary_logloss: 0.69558
LightGBM 验证集 AUC: 0.5082

Transformer 训练样本数: 4769

融合权重: CB=0.340, LGB=0.328, TF=0.332
融合模型验证集 AUC: 0.5233
融合模型测试集 AUC: 0.4967
最佳阈值 (基于验证集F1): 0.3881
测试集准确率: 0.5073

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.06
交易次数: 3


============================================================
处理股票: NVDA
============================================================
有效样本数(目标非空): 4733
训练集: 1999-01-22 00:00:00 至 2010-05-06 00:00:00, 样本数 2839
验证集: 2010-05-07 00:00:00 至 2014-02-10 00:00:00, 样本数 947
测试集: 2014-02-11 00:00:00 至 2017-11-10 00:00:00, 样本数 947

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5278

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[5] valid_0's binary_logloss: 0.692615
LightGBM 验证集 AUC: 0.5209

Transformer 训练样本数: 2819

融合权重: CB=0.338, LGB=0.333, TF=0.329
融合模型验证集 AUC: 0.5254
融合模型测试集 AUC: 0.4942
最佳阈值 (基于验证集F1): 0.4769
测试集准确率: 0.5480

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.93
交易次数: 3


============================================================
处理股票: GOOGL
============================================================
有效样本数(目标非空): 3333
训练集: 2004-08-19 00:00:00 至 2012-07-25 00:00:00, 样本数 1999
验证集: 2012-07-26 00:00:00 至 2015-03-23 00:00:00, 样本数 667
测试集: 2015-03-24 00:00:00 至 2017-11-10 00:00:00, 样本数 667

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5270

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's binary_logloss: 0.692734
LightGBM 验证集 AUC: 0.4846

Transformer 训练样本数: 1979

融合权重: CB=0.346, LGB=0.316, TF=0.338
融合模型验证集 AUC: 0.5273
融合模型测试集 AUC: 0.4495
最佳阈值 (基于验证集F1): 0.4906
测试集准确率: 0.5363

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.22
交易次数: 1


============================================================
处理股票: AMZN
============================================================
有效样本数(目标非空): 5153
训练集: 1997-05-16 00:00:00 至 2009-09-03 00:00:00, 样本数 3091
验证集: 2009-09-04 00:00:00 至 2013-10-09 00:00:00, 样本数 1031
测试集: 2013-10-10 00:00:00 至 2017-11-10 00:00:00, 样本数 1031

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5290

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[2] valid_0's binary_logloss: 0.69359
LightGBM 验证集 AUC: 0.5195

Transformer 训练样本数: 3071

融合权重: CB=0.339, LGB=0.335, TF=0.326
融合模型验证集 AUC: 0.5319
融合模型测试集 AUC: 0.5144
最佳阈值 (基于验证集F1): 0.4808
测试集准确率: 0.5361

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.14
交易次数: 5


============================================================
处理股票: WMT
============================================================
有效样本数(目标非空): 11443
训练集: 1972-03-20 00:00:00 至 1999-08-31 00:00:00, 样本数 6865
验证集: 1999-09-01 00:00:00 至 2008-10-09 00:00:00, 样本数 2289
测试集: 2008-10-10 00:00:00 至 2017-11-10 00:00:00, 样本数 2289

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5198

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[42]    valid_0's binary_logloss: 0.696587
LightGBM 验证集 AUC: 0.5087

Transformer 训练样本数: 6845

融合权重: CB=0.333, LGB=0.327, TF=0.340
融合模型验证集 AUC: 0.5107
融合模型测试集 AUC: 0.4817
最佳阈值 (基于验证集F1): 0.3268
测试集准确率: 0.5253

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.52
交易次数: 1


============================================================
处理股票: JPM
============================================================
有效样本数(目标非空): 12074
训练集: 1970-01-02 00:00:00 至 1998-08-31 00:00:00, 样本数 7244
验证集: 1998-09-01 00:00:00 至 2008-04-11 00:00:00, 样本数 2415
测试集: 2008-04-14 00:00:00 至 2017-11-10 00:00:00, 样本数 2415

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5259

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[12]    valid_0's binary_logloss: 0.693436
LightGBM 验证集 AUC: 0.5157

Transformer 训练样本数: 7224

融合权重: CB=0.342, LGB=0.335, TF=0.323
融合模型验证集 AUC: 0.5273
融合模型测试集 AUC: 0.5096
最佳阈值 (基于验证集F1): 0.3684
测试集准确率: 0.5077

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.44
交易次数: 1


============================================================
处理股票: JNJ
============================================================
有效样本数(目标非空): 12074
训练集: 1970-01-02 00:00:00 至 1998-09-01 00:00:00, 样本数 7244
验证集: 1998-09-02 00:00:00 至 2008-04-11 00:00:00, 样本数 2415
测试集: 2008-04-14 00:00:00 至 2017-11-10 00:00:00, 样本数 2415

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5340

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[34]    valid_0's binary_logloss: 0.694645
LightGBM 验证集 AUC: 0.5229

Transformer 训练样本数: 7224

融合权重: CB=0.340, LGB=0.333, TF=0.327
融合模型验证集 AUC: 0.5265
融合模型测试集 AUC: 0.5176
最佳阈值 (基于验证集F1): 0.4005
测试集准确率: 0.5119

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.67
交易次数: 31


============================================================
处理股票: TMUS
============================================================
有效样本数(目标非空): 2663
训练集: 2007-04-18 00:00:00 至 2013-08-20 00:00:00, 样本数 1597
验证集: 2013-08-21 00:00:00 至 2015-10-01 00:00:00, 样本数 533
测试集: 2015-10-02 00:00:00 至 2017-11-10 00:00:00, 样本数 533

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5136

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[1] valid_0's binary_logloss: 0.693029
LightGBM 验证集 AUC: 0.4956

Transformer 训练样本数: 1577

融合权重: CB=0.324, LGB=0.312, TF=0.364
融合模型验证集 AUC: 0.5031
融合模型测试集 AUC: 0.5287
最佳阈值 (基于验证集F1): 0.4780
测试集准确率: 0.5244

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 1.02
交易次数: 19


============================================================
处理股票: AMD
============================================================
有效样本数(目标非空): 8737
训练集: 1983-03-21 00:00:00 至 2003-12-24 00:00:00, 样本数 5242
验证集: 2003-12-26 00:00:00 至 2010-12-02 00:00:00, 样本数 1747
测试集: 2010-12-03 00:00:00 至 2017-11-10 00:00:00, 样本数 1748

正在进行特征筛选...
筛选后特征数: 51

训练 CatBoost...
CatBoost 验证集 AUC: 0.5139

训练 LightGBM...
Training until validation scores don't improve for 50 rounds
Early stopping, best iteration is:
[21]    valid_0's binary_logloss: 0.693716
LightGBM 验证集 AUC: 0.5295

Transformer 训练样本数: 5222

融合权重: CB=0.337, LGB=0.348, TF=0.316
融合模型验证集 AUC: 0.5316
融合模型测试集 AUC: 0.5154
最佳阈值 (基于验证集F1): 0.4053
测试集准确率: 0.4832

策略总收益率 (考虑成本): nan%
买入持有收益率: nan%
策略夏普比率: 0.36
交易次数: 15


所有结果包含的键: {'ticker', 'best_thresh', 'buy_hold_return', 'test_auc', 'n_trades', 'test_acc', 'sharpe', 'strategy_return'}


========== 多股票分类结果汇总 ==========
       test_auc  test_acc strategy_return buy_hold_return    sharpe n_trades
AAPL   0.497303  0.520266             NaN             NaN  0.959384        1
MSFT   0.496734  0.507292             NaN             NaN  1.060257        3
NVDA     0.4942  0.548004             NaN             NaN  1.933743        3
GOOGL  0.449491  0.536321             NaN             NaN  1.220571        1
AMZN   0.514408  0.536103             NaN             NaN  1.139771        5
WMT    0.481738  0.525342             NaN             NaN  0.515746        1
JPM    0.509609  0.507724             NaN             NaN  0.435072        1
JNJ    0.517611    0.5119             NaN             NaN  0.668619       31
TMUS   0.528675  0.524366             NaN             NaN  1.018444       19
AMD    0.515393  0.483218             NaN             NaN  0.355769       15


结果已保存至 classification_results.csv
本站总访问量 | 访客数