最近在量化投资圈子里,阿里达摩院开源的一些AI工具引起了不小的关注。作为一个技术出身的开发者,我本能地对“AI选股”这个标签产生了好奇——它到底是营销噱头,还是真有技术含量?抱着学习和验证的心态,我花了一周时间,深入研究了达摩院相关开源项目,并基于其核心思想,用Python搭建了一套简易但完整的AI选股回测系统。
整个过程下来,我的感受是:真香!当然,这里的“香”不是指它能让你一夜暴富,而是其技术路径清晰、代码质量高,为我们提供了一个绝佳的、低成本学习AI在金融领域应用的实战案例。对于想入门量化、学习机器学习应用,或者单纯想了解AI如何分析市场的开发者来说,这无疑是一份宝贵的学习资料。
本文将带你从零开始,复现这套“AI选股”工具的核心流程。我们将涵盖从环境搭建、数据获取、特征工程、模型训练到回测评估的全过程,并提供完整的、可运行的Python源码。无论你是Python新手,还是有一定基础的开发者,都能跟着步骤一步步实现。
1. 背景与核心概念:什么是AI选股?
在深入代码之前,我们有必要厘清几个基本概念,避免陷入“玄学”的误区。
1.1 传统选股 vs. AI选股
- 传统量化选股:通常基于固定的财务指标(如市盈率PE、市净率PB)或技术指标(如MACD、RSI),通过设定阈值或规则来筛选股票。逻辑清晰,但难以处理高维、非线性的市场关系。
- AI选股:利用机器学习(ML)或深度学习(DL)模型,从海量历史数据(价格、成交量、财务数据、另类数据等)中自动学习和挖掘潜在的、复杂的规律与模式,从而预测股票未来的表现(如涨跌、收益率)。其核心优势在于模式识别能力和处理高维数据的能力。
1.2 达摩院AI选股工具的核心思想阿里达摩院开源的相关项目(例如一些时序预测、图神经网络项目),其核心贡献在于提供了先进的模型架构和特征提取方法。它们并非一个开箱即用的“摇钱树”程序,而是一套技术框架。我们“白嫖”的正是这些前沿的算法思想与高质量的代码实现。 在本文的实践中,我们将借鉴其思路,使用更经典、更易于理解的机器学习模型(如LightGBM)来演示整个流程,其方法论是相通的:将选股问题转化为一个监督学习问题。
1.3 关键问题澄清
- 能保证赚钱吗?绝对不能。任何基于历史数据的模型都存在过拟合风险,且金融市场瞬息万变,存在“黑天鹅”事件。本文内容仅供技术学习与交流,不构成任何投资建议。
- 我们需要做什么?我们的目标是构建一个回测系统。即,假设在过去的某个时间段,按照我们模型的信号进行交易,结果会如何?回测是验证策略思想的重要手段,但过去的表现不代表未来。
2. 环境准备与项目搭建
工欲善其事,必先利其器。我们先来配置开发环境并创建项目结构。
2.1 环境要求
- 操作系统:Windows 10/11, macOS, 或 Linux (如Ubuntu)均可。本文演示在Windows 11下进行。
- Python版本:>= 3.8。推荐使用3.8或3.9,稳定性兼容性最好。
- 包管理工具:
pip或conda。本文使用pip。
2.2 创建虚拟环境(强烈推荐)为了避免包版本冲突,建议为项目创建独立的虚拟环境。
# 在项目根目录下,使用 venv 创建虚拟环境 python -m venv venv_ai_stock # 激活虚拟环境 # Windows (CMD/PowerShell) venv_ai_stock\Scripts\activate # macOS/Linux source venv_ai_stock/bin/activate激活后,命令行提示符前会出现(venv_ai_stock)字样。
2.3 安装核心依赖库我们将使用以下库,请通过pip安装:
pip install pandas numpy scikit-learn lightgbm matplotlib seaborn tushare backtraderpandas,numpy: 数据处理基石。scikit-learn: 机器学习工具库,用于数据预处理、划分数据集等。lightgbm: 微软开源的高效梯度提升框架,训练速度快,精度高,非常适合金融数据。matplotlib,seaborn: 数据可视化。tushare: 免费、强大的国内财经数据接口(需要注册获取token)。backtrader: 功能强大的Python回测框架。
2.4 项目目录结构创建如下目录和文件,使项目结构清晰:
ai_stock_selection/ │ ├── data/ # 存放原始和预处理后的数据 │ ├── raw/ # 原始数据 │ └── processed/ # 处理后的特征数据 │ ├── models/ # 存放训练好的模型文件 │ ├── src/ # 源代码 │ ├── data_fetcher.py # 数据获取模块 │ ├── feature_engineer.py # 特征工程模块 │ ├── model_trainer.py # 模型训练模块 │ └── backtest.py # 回测模块 │ ├── config.py # 配置文件(如token、股票池、参数) ├── main.py # 主程序入口 ├── requirements.txt # 依赖列表 └── README.md在项目根目录下创建requirements.txt,内容即上述pip install的库列表。
3. 核心流程拆解:从数据到信号
一个完整的AI选股回测流程,可以分解为以下几个核心步骤,它们环环相扣。
3.1 数据获取与预处理数据是AI的“燃料”。我们需要获取两类主要数据:
- 行情数据:股票每日的开盘价、收盘价、最高价、最低价、成交量。
- 标签数据:我们需要定义什么是“好股票”。通常,我们用未来N日(例如5日)的收益率作为标签。例如,如果一只股票明天买入,5天后卖出收益率超过阈值(如3%),则标记为“好”(1),否则为“差”(0)。这就把选股问题转化为了一个二分类问题。
3.2 特征工程这是最关键的一步,决定了模型能学到什么。特征是从原始数据中提炼出的、可供模型使用的指标。常见的特征包括:
- 技术指标:移动平均线(MA)、相对强弱指数(RSI)、布林带(Bollinger Bands)、MACD等。可以使用
ta-lib库方便计算,但本文为简化使用基础公式演示。 - 价格衍生特征:过去N日的收益率、波动率、价格与均线的偏离度等。
- 成交量特征:成交量的变化率、量价关系等。
- 横截面特征:股票特征在全市场中的排名、分位数等(需要全市场数据)。
3.3 模型训练与预测我们使用LightGBM分类器。流程如下:
- 将数据按时间划分为训练集和测试集(严禁打乱时间顺序,避免未来信息泄露)。
- 用训练集数据训练LightGBM模型。
- 用训练好的模型对测试集进行预测,得到每只股票每天属于“好股票”的概率。
3.4 回测验证将模型的预测概率转化为交易信号(例如,每天买入概率最高的前10只股票),并使用backtrader框架模拟真实交易,计算策略的收益率、夏普比率、最大回撤等关键绩效指标。
4. 完整实战:构建你的AI选股回测系统
下面,我们按照项目结构,一步步填充代码。
4.1 配置文件 (config.py)集中管理配置项,便于修改。
# config.py import os # 1. Tushare Pro Token (需要到 tushare.pro 官网注册获取) TUSHARE_TOKEN = '你的tushare_token_here' # 请替换为你的真实token # 2. 数据参数 START_DATE = '20180101' # 数据开始日期 END_DATE = '20231231' # 数据结束日期 # 初始股票池:沪深300成分股(示例,实际需要动态获取或指定) SAMPLE_STOCK_POOL = ['000001.SZ', '000002.SZ', '600000.SH'] # 平安银行、万科A、浦发银行,仅作示例 # 3. 特征与标签参数 LOOKBACK_WINDOW = 20 # 特征回看窗口(例如计算20日均线) FORWARD_WINDOW = 5 # 标签展望窗口(预测未来5日收益) LABEL_THRESHOLD = 0.03 # 收益率阈值,未来5日收益超过3%则为正样本(1) # 4. 模型参数 MODEL_PARAMS = { 'objective': 'binary', 'metric': 'auc', 'boosting_type': 'gbdt', 'num_leaves': 31, 'learning_rate': 0.05, 'feature_fraction': 0.9, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': -1, 'seed': 42 } TRAIN_TEST_SPLIT_DATE = '20210101' # 此日期之前为训练集,之后为测试集 # 5. 回测参数 BACKTEST_START = '20210101' BACKTEST_END = '20211231' INITIAL_CASH = 1000000 # 初始资金100万 TOP_K = 10 # 每日持仓股票数量 COMMISSION_RATE = 0.0003 # 佣金费率万三4.2 数据获取模块 (src/data_fetcher.py)负责从Tushare获取原始数据。
# src/data_fetcher.py import tushare as ts import pandas as pd import os import time from datetime import datetime, timedelta from config import TUSHARE_TOKEN, START_DATE, END_DATE, SAMPLE_STOCK_POOL class DataFetcher: def __init__(self): # 初始化tushare pro接口 ts.set_token(TUSHARE_TOKEN) self.pro = ts.pro_api() # 确保数据目录存在 os.makedirs('./data/raw', exist_ok=True) def fetch_daily_data(self, ts_code, start_date, end_date): """获取单只股票的日线行情数据""" try: df = self.pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date) # 按日期排序 df = df.sort_values('trade_date').reset_index(drop=True) # 将字符串日期转换为datetime格式 df['trade_date'] = pd.to_datetime(df['trade_date']) # 计算日收益率 (次日收盘价/当日收盘价 - 1) df['daily_return'] = df['close'].pct_change().shift(-1) # 注意shift方向 return df except Exception as e: print(f"获取 {ts_code} 数据失败: {e}") return pd.DataFrame() def fetch_bulk_daily_data(self, stock_list, start_date, end_date): """批量获取多只股票数据,并保存到本地""" all_data = {} for ts_code in stock_list: print(f"正在获取 {ts_code} 的数据...") df = self.fetch_daily_data(ts_code, start_date, end_date) if not df.empty: all_data[ts_code] = df # 可选:保存到CSV df.to_csv(f'./data/raw/{ts_code}.csv', index=False) time.sleep(0.2) # 礼貌性延时,避免请求过快 print(f"数据获取完成,共获取 {len(all_data)} 只股票的数据。") return all_data if __name__ == '__main__': # 示例:获取示例股票池数据 fetcher = DataFetcher() data_dict = fetcher.fetch_bulk_daily_data(SAMPLE_STOCK_POOL, START_DATE, END_DATE) # 可以在这里查看数据 if '000001.SZ' in data_dict: print(data_dict['000001.SZ'].head())4.3 特征工程模块 (src/feature_engineer.py)这是AI选股的“灵魂”,我们创建一些基础特征。
# src/feature_engineer.py import pandas as pd import numpy as np from config import LOOKBACK_WINDOW, FORWARD_WINDOW, LABEL_THRESHOLD class FeatureEngineer: def __init__(self, lookback_window=LOOKBACK_WINDOW): self.lookback = lookback_window def create_features(self, price_series, volume_series): """为单只股票创建特征""" df = pd.DataFrame({ 'close': price_series.values, 'volume': volume_series.values }, index=price_series.index) # 1. 价格动量特征 df['returns_1d'] = df['close'].pct_change(1) df['returns_5d'] = df['close'].pct_change(5) df['returns_10d'] = df['close'].pct_change(10) df['returns_20d'] = df['close'].pct_change(self.lookback) # 2. 移动平均线与价格关系 df['ma_5'] = df['close'].rolling(window=5).mean() df['ma_10'] = df['close'].rolling(window=10).mean() df['ma_20'] = df['close'].rolling(window=self.lookback).mean() df['close_to_ma5'] = df['close'] / df['ma_5'] - 1 df['close_to_ma20'] = df['close'] / df['ma_20'] - 1 # 3. 波动率特征 df['volatility_5d'] = df['returns_1d'].rolling(window=5).std() df['volatility_20d'] = df['returns_1d'].rolling(window=self.lookback).std() # 4. 成交量特征 df['volume_ma5'] = df['volume'].rolling(window=5).mean() df['volume_ratio'] = df['volume'] / df['volume_ma5'] # 5. 简单技术指标 (RSI近似) delta = df['close'].diff() gain = (delta.where(delta > 0, 0)).rolling(window=14).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean() rs = gain / loss df['rsi'] = 100 - (100 / (1 + rs)) # 删除因滚动计算产生的NaN值 df = df.dropna() return df def create_label(self, price_series, forward_window=FORWARD_WINDOW, threshold=LABEL_THRESHOLD): """创建标签:未来forward_window日的收益率是否超过threshold""" # 计算未来N日收益率 future_return = price_series.shift(-forward_window) / price_series - 1 # 二分类标签:1表示未来收益超过阈值,0表示未超过 label = (future_return > threshold).astype(int) # 由于使用了未来数据,需要将最后forward_window天的标签设为NaN(无法用于训练) label.iloc[-forward_window:] = np.nan return label def prepare_dataset(self, data_dict): """整合所有股票的数据,创建特征和标签""" features_list = [] labels_list = [] stock_ids = [] for ts_code, df in data_dict.items(): if df.empty or len(df) < 50: # 数据太少则跳过 continue # 创建特征 feat_df = self.create_features(df['close'], df['volume']) # 创建标签 label_s = self.create_label(df['close']) # 对齐索引(日期) aligned_index = feat_df.index.intersection(label_s.dropna().index) if len(aligned_index) == 0: continue feat_df = feat_df.loc[aligned_index] label_s = label_s.loc[aligned_index] # 添加股票代码和日期作为多级索引的一部分 for date in feat_df.index: features_list.append(feat_df.loc[date].values) labels_list.append(label_s.loc[date]) stock_ids.append((ts_code, date)) # 转换为DataFrame feature_columns = feat_df.columns.tolist() features_df = pd.DataFrame(features_list, columns=feature_columns) labels_df = pd.Series(labels_list, name='label') # 创建多级索引 (stock_id, date) index = pd.MultiIndex.from_tuples(stock_ids, names=['ts_code', 'trade_date']) features_df.index = index labels_df.index = index # 合并并删除任何包含NaN的行 full_df = pd.concat([features_df, labels_df], axis=1).dropna() return full_df4.4 模型训练模块 (src/model_trainer.py)负责训练模型并保存。
# src/model_trainer.py import pandas as pd import numpy as np import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, roc_auc_score, classification_report import joblib import os from config import MODEL_PARAMS, TRAIN_TEST_SPLIT_DATE class ModelTrainer: def __init__(self, model_params=MODEL_PARAMS): self.model_params = model_params self.model = None os.makedirs('./models', exist_ok=True) def prepare_train_test_data(self, full_dataset_df): """按时间划分训练集和测试集""" # 确保索引是MultiIndex if not isinstance(full_dataset_df.index, pd.MultiIndex): raise ValueError("数据集索引必须是 (ts_code, trade_date) 的MultiIndex") # 提取日期部分 dates = full_dataset_df.index.get_level_values('trade_date') # 按日期划分 train_mask = dates < pd.Timestamp(TRAIN_TEST_SPLIT_DATE) test_mask = dates >= pd.Timestamp(TRAIN_TEST_SPLIT_DATE) X_train = full_dataset_df[train_mask].drop('label', axis=1) y_train = full_dataset_df[train_mask]['label'] X_test = full_dataset_df[test_mask].drop('label', axis=1) y_test = full_dataset_df[test_mask]['label'] print(f"训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)}") print(f"训练集正样本比例: {y_train.mean():.4f}, 测试集正样本比例: {y_test.mean():.4f}") return X_train, X_test, y_train, y_test def train(self, X_train, y_train, X_val=None, y_val=None): """训练LightGBM模型""" print("开始训练LightGBM模型...") # 如果没有验证集,则从训练集划分 if X_val is None or y_val is None: X_train_split, X_val_split, y_train_split, y_val_split = train_test_split( X_train, y_train, test_size=0.2, random_state=42, stratify=y_train ) else: X_train_split, y_train_split = X_train, y_train X_val_split, y_val_split = X_val, y_val # 创建LightGBM数据集 lgb_train = lgb.Dataset(X_train_split, y_train_split) lgb_eval = lgb.Dataset(X_val_split, y_val_split, reference=lgb_train) # 训练模型 self.model = lgb.train( self.model_params, lgb_train, valid_sets=[lgb_train, lgb_eval], num_boost_round=1000, callbacks=[ lgb.early_stopping(stopping_rounds=50, verbose=True), lgb.log_evaluation(period=100) ] ) print("模型训练完成。") return self.model def evaluate(self, X_test, y_test): """在测试集上评估模型""" if self.model is None: raise ValueError("请先训练模型或加载已有模型。") y_pred_prob = self.model.predict(X_test, num_iteration=self.model.best_iteration) y_pred = (y_pred_prob > 0.5).astype(int) accuracy = accuracy_score(y_test, y_pred) auc = roc_auc_score(y_test, y_pred_prob) print("="*50) print("模型在测试集上的表现:") print(f"准确率 (Accuracy): {accuracy:.4f}") print(f"AUC 分数: {auc:.4f}") print("\n分类报告:") print(classification_report(y_test, y_pred, target_names=['负样本', '正样本'])) print("="*50) # 特征重要性 feature_importance = pd.DataFrame({ 'feature': X_test.columns, 'importance': self.model.feature_importance(importance_type='gain') }).sort_values('importance', ascending=False) print("\n特征重要性 Top 10:") print(feature_importance.head(10)) return accuracy, auc, feature_importance def save_model(self, filepath='./models/lgbm_ai_stock.pkl'): """保存模型到文件""" if self.model: joblib.dump(self.model, filepath) print(f"模型已保存至 {filepath}") else: print("没有可保存的模型。") def load_model(self, filepath='./models/lgbm_ai_stock.pkl'): """从文件加载模型""" self.model = joblib.load(filepath) print(f"模型已从 {filepath} 加载。") return self.model4.5 回测模块 (src/backtest.py)使用Backtrader进行策略回测。
# src/backtest.py import backtrader as bt import pandas as pd import numpy as np from datetime import datetime from config import INITIAL_CASH, COMMISSION_RATE, TOP_K class AIStockStrategy(bt.Strategy): params = ( ('top_k', TOP_K), # 每日持有股票数量 ) def __init__(self): # 记录交易日期 self.date = self.datas[0].datetime.date # 为每个数据(股票)添加一个订单引用字典 self.orders = {data._name: None for data in self.datas} def next(self): # 回测框架在每个bar(交易日)调用此函数 current_date = self.datas[0].datetime.date(0) # 假设我们有一个外部信号源,这里用随机信号模拟 # 在实际应用中,这里应该读取模型预测的概率 signals = {} for data in self.datas: # 模拟信号:随机生成一个“买入概率” # 替换为:signal = your_model.predict_proba_for_stock(data._name, current_date)[1] signal = np.random.rand() # 模拟信号,需替换 signals[data._name] = signal # 按信号强度排序 sorted_stocks = sorted(signals.items(), key=lambda x: x[1], reverse=True) buy_candidates = [s[0] for s in sorted_stocks[:self.params.top_k]] # 调整仓位:买入候选股,卖出非候选股 for data in self.datas: stock_name = data._name pos = self.getposition(data).size # 如果股票在买入列表且当前未持有,则买入 if stock_name in buy_candidates and pos == 0: # 计算买入金额:均分可用资金 cash_per_stock = self.broker.getcash() / (self.params.top_k - len([d for d in self.datas if self.getposition(d).size > 0])) size = int(cash_per_stock / data.close[0] / 100) * 100 # A股按手(100股)交易 if size > 0: self.orders[stock_name] = self.buy(data=data, size=size) # 如果股票不在买入列表但当前持有,则卖出 elif stock_name not in buy_candidates and pos > 0: self.orders[stock_name] = self.close(data=data) def run_backtest(stock_data_dict, start_date, end_date, initial_cash=INITIAL_CASH): """运行回测""" cerebro = bt.Cerebro() cerebro.broker.setcash(initial_cash) # 设置佣金 cerebro.broker.setcommission(commission=COMMISSION_RATE) # 添加数据 for ts_code, df in stock_data_dict.items(): # 确保数据格式符合backtrader要求 df_bt = df.copy() df_bt['datetime'] = pd.to_datetime(df_bt['trade_date']) df_bt.set_index('datetime', inplace=True) df_bt['openinterest'] = 0 # 期货数据,股票设为0 df_bt = df_bt[['open', 'high', 'low', 'close', 'volume', 'openinterest']] # 过滤回测期间数据 df_bt = df_bt.loc[start_date:end_date] if len(df_bt) > 0: data = bt.feeds.PandasData(dataname=df_bt, name=ts_code) cerebro.adddata(data) if len(cerebro.datas) == 0: print("在指定回测期间内没有可用的股票数据。") return None # 添加策略 cerebro.addstrategy(AIStockStrategy) # 添加分析器 cerebro.addanalyzer(bt.analyzers.Returns, _name='returns') cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.0) cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown') cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades') print(f'初始资金: {cerebro.broker.getvalue():.2f}') # 运行回测 results = cerebro.run() strat = results[0] print(f'期末资金: {cerebro.broker.getvalue():.2f}') print(f'净收益: {cerebro.broker.getvalue() - initial_cash:.2f}') # 打印分析结果 print("\n========== 回测分析报告 ==========") ret_analysis = strat.analyzers.returns.get_analysis() sharpe_analysis = strat.analyzers.sharpe.get_analysis() dd_analysis = strat.analyzers.drawdown.get_analysis() trade_analysis = strat.analyzers.trades.get_analysis() if 'rnorm100' in ret_analysis: print(f"年化收益率: {ret_analysis['rnorm100']:.2f}%") print(f"夏普比率: {sharpe_analysis['sharperatio']:.3f}") if 'max' in dd_analysis: print(f"最大回撤: {dd_analysis['max']['drawdown']:.2f}%") print(f"最长回撤周期: {dd_analysis['max']['len']} 天") # 绘制图表 cerebro.plot(style='candlestick', volume=False) return strat4.6 主程序入口 (main.py)串联整个流程。
# main.py import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__))) from src.data_fetcher import DataFetcher from src.feature_engineer import FeatureEngineer from src.model_trainer import ModelTrainer from src.backtest import run_backtest from config import * def main(): print("="*60) print("开始运行AI选股回测系统") print("="*60) # 步骤1: 获取数据 print("\n[步骤1/4] 获取股票数据...") fetcher = DataFetcher() # 注意:这里使用示例股票池,实际应用应扩大范围(如沪深300) stock_data_dict = fetcher.fetch_bulk_daily_data(SAMPLE_STOCK_POOL, START_DATE, END_DATE) if not stock_data_dict: print("数据获取失败,请检查网络或Token。") return # 步骤2: 特征工程与数据集构建 print("\n[步骤2/4] 进行特征工程...") engineer = FeatureEngineer() full_dataset_df = engineer.prepare_dataset(stock_data_dict) print(f"特征数据集形状: {full_dataset_df.shape}") # 步骤3: 模型训练与评估 print("\n[步骤3/4] 训练与评估模型...") trainer = ModelTrainer() X_train, X_test, y_train, y_test = trainer.prepare_train_test_data(full_dataset_df) model = trainer.train(X_train, y_train) trainer.evaluate(X_test, y_test) trainer.save_model() # 步骤4: 回测 (注意:此处回测信号是模拟的,需要将模型预测集成进去) print("\n[步骤4/4] 运行回测...") # 重要:实际回测需要将模型预测的概率作为信号,传递给策略。 # 此处为演示,使用随机信号。你需要修改 backtest.py 中的 AIStockStrategy, # 使其能读取一个预先计算好的、包含每日每只股票预测概率的DataFrame。 print("提示:当前回测使用随机信号进行演示。") print("要使用真实模型信号,请修改 backtest.py 中的 AIStockStrategy.next() 方法,") print("使其从文件或变量中读取模型对当前日期、当前股票的预测概率。") # 运行回测(基于随机信号) # run_backtest(stock_data_dict, BACKTEST_START, BACKTEST_END) print("\n" + "="*60) print("流程执行完毕!") print("下一步:") print("1. 扩大股票池(如获取全A股数据),重新进行特征工程和训练。") print("2. 设计更复杂的特征(如财务指标、另类数据)。") print("3. 将模型预测概率集成到回测策略中(修改 backtest.py)。") print("4. 尝试不同的模型(如XGBoost、神经网络)或调整超参数。") print("="*60) if __name__ == '__main__': main()5. 常见问题与排查思路
在运行上述代码时,你可能会遇到以下问题:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
ModuleNotFoundError: No module named 'tushare' | 依赖库未安装或虚拟环境未激活。 | 1. 确认虚拟环境已激活。 2. 在激活的环境中运行 pip install -r requirements.txt。 |
ts.pro_api() 返回 ‘抱歉,您没有权限访问该数据’ | Tushare Token 无效或未设置。 | 1. 前往 tushare.pro 官网注册并获取Token。 2. 将 config.py中的TUSHARE_TOKEN替换为你的真实Token。 |
| 数据获取非常慢或中断 | 网络问题或Tushare API调用频率限制。 | 1. 检查网络连接。 2. 在 data_fetcher.py的fetch_bulk_daily_data方法中适当增加time.sleep的间隔。 |
| 特征工程后数据量骤减 | 计算滚动特征(如20日均线)和未来标签导致大量NaN值被删除。 | 1. 这是正常现象,确保初始数据量足够大。 2. 可适当减小 LOOKBACK_WINDOW和FORWARD_WINDOW进行测试。 |
| 模型AUC值很低(接近0.5) | 特征与标签关系弱,或存在未来函数/数据泄露。 | 1.仔细检查特征工程:确保所有特征只使用到当前时刻或过去的信息。 2.检查标签创建: create_label函数中的shift(-forward_window)方向是否正确,是否已将未来数据置为NaN。3. 尝试更复杂的特征或模型。 |
| 回测策略不交易或交易异常 | 回测数据日期范围不对,或策略逻辑条件不满足。 | 1. 检查BACKTEST_START和BACKTEST_END是否在数据范围内。2. 在 AIStockStrategy.next()方法中添加print语句,调试信号和买卖逻辑。3. 确保股票数据格式(OHLCV)符合Backtrader要求。 |
LightGBM训练警告或错误 | 数据包含NaN或inf,参数设置不当。 | 1. 在训练前检查X_train.isnull().sum().sum()确保没有NaN。2. 确保 y_train只包含0和1。3. 调整 MODEL_PARAMS,如减小learning_rate,增加num_leaves。 |
6. 最佳实践与工程建议
将AI应用于选股是一个系统工程,除了跑通流程,以下几点能帮助你构建更稳健、更实用的系统:
6.1 数据质量与预处理
- 数据源:Tushare是很好的起点,但对于生产级研究,需要考虑数据的完整性、准确性和清洗(如复权处理)。专业机构会使用Wind、聚宽等更全面的数据源。
- 幸存者偏差:只使用当前存在的股票回测,会忽略已退市股票,导致结果过于乐观。应使用历史成分股列表。
- 未来函数:这是量化研究的大忌。务必确保特征计算绝对不使用未来数据。在特征工程代码中,所有
.rolling().mean()、.shift()等操作都要反复检查方向。
6.2 特征工程进阶
- 标准化/归一化:在将数据输入模型前,应对特征进行标准化(如Z-score)或归一化,特别是对于梯度提升树模型,虽然LightGBM对尺度不敏感,但处理后可加速收敛。
- 横截面特征:单个股票的特征往往不如它在全市场中的相对位置有效。例如,计算每只股票的“市盈率”在全市场中的分位数排名。
- 避免过度拟合:特征不是越多越好。使用特征重要性分析(LightGBM已提供)进行筛选,或使用递归特征消除(RFE)。
6.3 模型训练与验证
- 时间序列交叉验证:对于金融时间序列数据,不能使用随机划分。应使用滚动窗口或扩展窗口的交叉验证方法,更贴近实战。
- 样本不平衡:股票市场大部分时间波动不大,正样本(大涨)可能很少。可以使用
lightgbm的is_unbalance参数或scale_pos_weight参数,或对训练集进行过采样/欠采样。 - 模型集成:不要只依赖一个模型。可以训练多个不同参数或不同特征的LightGBM模型,进行投票或平均,以提升稳定性。
6.4 回测的陷阱
- 交易成本:本文包含了佣金,但实际还有印花税和滑点(实际成交价与预期价的偏差)。这些会显著侵蚀利润。
- 流动性假设:策略假设可以瞬间以收盘价买入/卖出任意数量的股票,这对于小盘股不现实。需考虑成交量限制。
- 前视偏差:确保回测中,策略在
t日交易时,只能使用t日及之前的信息。模型预测也必须是在t日收盘后,基于t日及之前的数据做出的。
6.5 工程化与部署
- 模块化:本文的代码结构是一个好的开始。在实际项目中,可以将数据更新、特征计算、模型预测、信号生成、风险控制等模块进一步解耦,便于维护和迭代。
- 自动化流水线:使用
Airflow或Prefect等工具构建自动化流水线,定时执行数据更新、模型重训和信号生成。 - 日志与监控:为关键步骤添加详细的日志记录,并监控模型预测性能的衰减(例如,每周计算测试集的AUC),当性能下降到阈值以下时触发模型重训。
通过这个项目,我们不仅“白嫖”了达摩院开源项目的思路,更重要的是亲手搭建了一个完整的AI选股研究框架。这个框架的价值不在于提供一个立即赚钱的策略,而在于为你提供了一个可扩展、可实验的沙盒。你可以在此基础上,尝试不同的特征、不同的模型、不同的回测参数,去验证自己的想法,这才是量化研究和AI应用的真正乐趣所在。记住,在金融市场中,对市场的敬畏心和持续学习的能力,比任何一个单一的模型都更重要。