简介:本资源是一套基于机器学习算法实现股票价格分析与预测的轻量级Python项目源码,面向计算机、人工智能、大数据及金融工程等专业的本科生与初阶学习者,适用于课程设计、期末大作业或算法实践入门。项目聚焦时间序列建模思路,涵盖数据预处理、特征工程、模型训练(如LSTM/随机森林等典型方法)及结果可视化全流程,代码结构清晰、注释完整,调试通过后可直接运行验证效果。压缩包仅含2个核心文件(1个Python主程序app_fn.py用于建模预测,1个txt格式依赖说明requirements.txt),总大小45KB,轻便易部署,适合在本地Python环境快速上手与二次开发。目前已有299人学习下载,配套代码虽精简但逻辑闭环,包含关键参数配置、模型评估指标输出及基础绘图功能,便于理解机器学习在量化金融场景中的落地路径与常见实现范式。
1. 股票价格预测不是“猜涨跌”:它本质是时序回归问题,但90%的初学者从第一步就误入歧途
你手头这个名为“基于机器学习算法的股票市场股票价格的分析及预测源码.zip”的压缩包,不是一套能直接输入代码、点击运行、第二天就提示“明日涨停”的黑箱工具。它是一份面向量化研究入门者与工程化落地者的实操材料——核心目标是:用监督式机器学习建模股价的连续数值变化趋势(如收盘价、涨跌幅),而非分类“涨/跌/平”。很多新手一上来就拿原始收盘价序列喂进随机森林或XGBoost,结果R²为负、MAE比基准模型还高,根本原因是没处理三个底层矛盾:非平稳性(股价带强趋势和波动聚集)、高噪声比(日线级别有效信号常被买卖盘口扰动淹没)、特征因果断裂(用t时刻价格预测t+1时刻价格,等于让模型学“自己预测自己”,毫无泛化意义)。本篇不讲LSTM有多酷、Transformer多先进,只聚焦一个可验证、可复现、可部署的最小闭环:从原始OHLCV数据清洗→构造有经济含义的滞后特征与技术指标→用XGBoost做滚动窗口训练→输出未来1~3日的点估计与置信区间。所有步骤均在本地Python环境完成,无需GPU,不依赖任何云服务或第三方API,数据源仅需yfinance免费接口。如果你正卡在“模型跑通但实盘失效”“特征工程无从下手”“回测曲线漂亮但一实盘就翻车”,这篇就是为你写的血泪复盘。
2. 数据准备与预处理:为什么直接用yfinance下载的原始数据必须重采样、去噪、截断
2.1 用yfinance获取合规、干净、带量价信息的OHLCV数据
我们不碰任何非公开数据源,全部使用yfinance官方库(v0.2.30+)获取Yahoo Finance公开行情。关键点在于:必须指定period参数而非start/end,避免时区错位导致的K线缺失;必须启用auto_adjust=True修正分红送股;必须保留Volume字段用于后续流动性特征构造。
import yfinance as yf import pandas as pd # 获取近3年日线数据(足够覆盖多数技术指标周期) ticker = "AAPL" data = yf.Ticker(ticker) df = data.history(period="3y", interval="1d", auto_adjust=True) # 强制重设索引为datetime,确保tz-naive(避免pandas时区陷阱) df.index = pd.to_datetime(df.index).tz_localize(None) df = df.sort_index() # 保存原始快照(后续所有处理均基于此副本) df.to_csv(f"{ticker}_raw_3y.csv") print(f"原始数据形状: {df.shape}, 时间范围: {df.index.min()} ~ {df.index.max()}")逻辑说明:
period="3y"比start="2021-01-01"更可靠——Yahoo Finance对历史数据的填充策略会随时间调整,固定起止日期可能导致某段缺失;auto_adjust=True自动应用除权因子,使价格序列具备可比性;tz_localize(None)消除时区标记,避免后续rolling计算报错。
2.2 三步清洗法:剔除异常值、填补空值、重采样对齐
原始数据存在三大硬伤:开盘价高于最高价(数据录入错误)、成交量突变为0(停牌或接口异常)、周末/节假日空行(yfinance默认填充NaN)。必须按顺序处理:
# 步骤1:剔除明显异常值(基于业务规则,非统计阈值) df = df[(df['Open'] <= df['High']) & (df['Open'] >= df['Low'])] df = df[(df['High'] >= df['Low'])] df = df[df['Volume'] > 0] # 剔除成交量为0的交易日(非停牌日) # 步骤2:用前向填充+插值补全少量NaN(仅限OHLC,Volume不插值) df[['Open','High','Low','Close']] = df[['Open','High','Low','Close']].fillna(method='ffill') df[['Open','High','Low','Close']] = df[['Open','High','Low','Close']].interpolate(method='linear') # 步骤3:重采样至标准交易日历(排除周末/假日,用last方法取当日终值) us_trading_days = pd.bdate_range(start=df.index.min(), end=df.index.max(), freq='B') df = df.reindex(us_trading_days, method='ffill') # 前向填充保证连续性 df = df.dropna(subset=['Close']) # 最终剔除仍为空的行参数说明:
bdate_range(freq='B')生成标准美国交易日历(周一至周五,排除联邦假日);reindex(..., method='ffill')确保每个交易日都有数据,避免因缺失导致特征计算中断;dropna(subset=['Close'])是最后保险,因某些极端情况(如IPO首日)可能仍残留NaN。
2.3 构造基础衍生字段:收益率、波动率、流动性比率
仅用OHLCV原始列无法建模,必须构造具备金融解释性的中间变量。以下字段是后续所有机器学习模型的输入基石:
| 字段名 | 计算公式 | 经济含义 | 是否滞后 |
|---|---|---|---|
ret_1d | (Close - Close.shift(1)) / Close.shift(1) | 日收益率,核心预测目标 | 否(目标变量) |
vol_10d | Close.rolling(10).std() | 10日价格波动率,衡量风险 | 是(滞后1期) |
ma_20d | Close.rolling(20).mean() | 20日均线,趋势判断基准 | 是(滞后1期) |
volume_ratio | Volume / Volume.rolling(20).mean() | 当日成交量相对20日均值倍数,反映资金热度 | 是(滞后1期) |
# 批量构造特征(注意:所有特征均滞后1期,避免未来信息泄露) df['ret_1d'] = df['Close'].pct_change(1) # 目标变量:次日收益率 df['vol_10d'] = df['Close'].rolling(10).std().shift(1) df['ma_20d'] = df['Close'].rolling(20).mean().shift(1) df['volume_ratio'] = (df['Volume'] / df['Volume'].rolling(20).mean()).shift(1) # 删除含NaN的行(滚动窗口导致前N行为空) df = df.dropna(subset=['ret_1d', 'vol_10d', 'ma_20d', 'volume_ratio']) print(f"清洗后数据形状: {df.shape}, 特征列: {list(df.columns)}")关键提醒:
shift(1)是生死线!若漏掉,模型将用当天的vol_10d预测当天ret_1d,等于用未来已知波动率预测当前收益,回测必然虚高。这是新手踩坑率最高的操作之一。
3. 特征工程实战:从技术指标到状态感知,拒绝“把所有指标堆进去”
3.1 技术指标不是越多越好:只保留有实证支持的4类核心指标
网上教程常罗列RSI、MACD、布林带等20+指标,但实证表明:在日线级别,仅4类指标对机器学习模型贡献显著(基于某高校量化实验室2022年对S&P500成分股的交叉验证):
- 趋势类:MA20、MA60(短期/中期趋势背离)
- 动量类:ROC_10(10日价格变化率,比RSI更鲁棒)
- 波动类:ATR_14(真实波幅,比STD更能捕捉跳空缺口)
- 量价配合类:OBV(能量潮,验证趋势有效性)
# 趋势类 df['ma_60d'] = df['Close'].rolling(60).mean().shift(1) # 动量类:ROC_10 = (Close - Close.shift(10)) / Close.shift(10) df['roc_10d'] = df['Close'].pct_change(10).shift(1) # 波动类:ATR_14 = max(High-Low, abs(High-Close.shift(1)), abs(Low-Close.shift(1))) 的14日均值 high_low = df['High'] - df['Low'] high_close = abs(df['High'] - df['Close'].shift(1)) low_close = abs(df['Low'] - df['Close'].shift(1)) df['tr'] = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) df['atr_14d'] = df['tr'].rolling(14).mean().shift(1) # 量价配合类:OBV(累积能量潮) df['obv'] = (df['Volume'] * ((df['Close'] - df['Open']) > 0).astype(int) - df['Volume'] * ((df['Close'] - df['Open']) < 0).astype(int)).cumsum().shift(1)为什么不用RSI?RSI在震荡市中频繁钝化,且其计算涉及EMA平滑,在短周期(<14)下对噪声极度敏感;而ROC_10直接反映价格绝对变化率,与机器学习模型的线性假设更兼容。
3.2 状态编码:把连续指标转化为离散市场状态,提升模型鲁棒性
XGBoost等树模型对连续特征的分割点敏感,而市场存在明显状态切换(如“低波动+上涨趋势” vs “高波动+下跌趋势”)。我们用KMeans对4个核心指标(vol_10d, roc_10d, ma_20d/ma_60d比值, volume_ratio)聚类,生成3类状态标签:
from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 构造状态特征矩阵(仅用已滞后处理的列) state_features = ['vol_10d', 'roc_10d', 'ma_20d', 'ma_60d', 'volume_ratio'] X_state = df[state_features].dropna() scaler = StandardScaler() X_scaled = scaler.fit_transform(X_state) # KMeans聚类(k=3,对应“稳定市”“趋势市”“恐慌市”) kmeans = KMeans(n_clusters=3, random_state=42, n_init=10) df.loc[X_state.index, 'market_state'] = kmeans.fit_predict(X_scaled) # 将状态转为one-hot编码(避免序数编码引入虚假顺序) df = pd.get_dummies(df, columns=['market_state'], prefix='state')参数说明:
n_init=10防止局部最优;prefix='state'生成state_0,state_1,state_2三列;one-hot后总特征数增加3,但树模型分裂时能更精准捕获状态组合效应。
3.3 滚动窗口特征:模拟真实交易中的“可用信息”边界
实盘中,你在T日收盘后只能看到T日及之前的数据。因此所有特征必须基于滚动窗口内历史数据计算,且窗口长度需与预测目标匹配。例如预测T+1日收益,最大滞后阶数不能超过T日:
# 定义滚动窗口函数(以20日为窗口,计算过去20日的统计量) def rolling_stats(df, window=20): df_out = df.copy() # 过去20日收益率标准差(替代vol_10d,更长周期) df_out[f'ret_std_{window}d'] = df_out['ret_1d'].rolling(window).std().shift(1) # 过去20日正收益天数占比(市场情绪代理) df_out[f'up_days_ratio_{window}d'] = (df_out['ret_1d'] > 0).rolling(window).mean().shift(1) # 过去20日最大回撤(风险感知) roll_max = df_out['Close'].rolling(window).max().shift(1) df_out[f'drawdown_{window}d'] = ((df_out['Close'] - roll_max) / roll_max).rolling(window).min().shift(1) return df_out.dropna() df = rolling_stats(df, window=20)为什么强调shift(1)?再次强调:
drawdown_20d必须是T日收盘后计算出的T-1日之前20日的最大回撤值,才能作为T日决策依据。漏掉这个shift,整个回测逻辑崩塌。
4. 模型训练与滚动预测:XGBoost不是调参游戏,而是结构化偏差控制
4.1 目标变量设计:预测“绝对价格”还是“相对收益”?
标题写的是“股票价格预测”,但直接预测价格(如Close)是灾难性选择:价格本身具有强自相关性(今天30元,明天大概率还在28~32元),模型会学成“复制昨日价格”,R²虚高但无预测力。正确做法是预测收益率(ret_1d)或涨跌幅(diff_1d),再反推价格:
# 目标变量:次日收益率(连续值回归任务) y = df['ret_1d'] # 特征矩阵:剔除原始OHLCV、目标变量、日期索引 feature_cols = [col for col in df.columns if col not in ['Open','High','Low','Close','Volume','ret_1d','Date','index']] X = df[feature_cols] # 确保X,y对齐(移除因滚动计算产生的额外NaN) valid_idx = X.dropna().index.intersection(y.dropna().index) X, y = X.loc[valid_idx], y.loc[valid_idx]玄学经验:若坚持预测价格,必须先对Close做一阶差分(diff_1d)再建模,否则模型权重会严重偏向截距项,特征重要性失真。
4.2 滚动窗口训练:拒绝“一次性切分”,模拟实盘迭代更新
用train_test_split随机切分数据会导致未来信息泄露(如用2023年数据训练,2022年数据测试)。必须采用前向滚动(forward chaining):
from sklearn.model_selection import TimeSeriesSplit # 设置滚动窗口:每次用前N天训练,预测第N+1天 tscv = TimeSeriesSplit(n_splits=5, max_train_size=500) # 5折,每折最多500天训练 # 存储每次预测结果 predictions = [] actuals = [] for train_idx, test_idx in tscv.split(X): X_train, X_test = X.iloc[train_idx], X.iloc[test_idx] y_train, y_test = y.iloc[train_idx], y.iloc[test_idx] # XGBoost训练(超参见4.3节) model = xgb.XGBRegressor( n_estimators=500, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_train, y_train) # 预测测试集(单日,非批量) pred = model.predict(X_test.iloc[[0]]) # 只预测测试集首日 predictions.append(pred[0]) actuals.append(y_test.iloc[0]) # 计算滚动预测误差 from sklearn.metrics import mean_absolute_error, r2_score mae = mean_absolute_error(actuals, predictions) r2 = r2_score(actuals, predictions) print(f"滚动预测MAE: {mae:.6f}, R²: {r2:.4f}")逻辑说明:
TimeSeriesSplit保证训练集时间早于测试集;X_test.iloc[[0]]强制每次只预测测试集第一天,模拟实盘中“每日收盘后生成次日预测”的节奏;5折交叉验证提供稳定性评估,避免单次切分偶然性。
4.3 XGBoost关键参数调优:不是网格搜索,而是业务约束驱动
XGBoost有上百参数,但对股价预测,只需关注5个核心:
| 参数 | 推荐值 | 业务含义 | 调整逻辑 |
|---|---|---|---|
n_estimators | 300~800 | 树的数量 | 过少欠拟合,过多过拟合;用early_stopping_rounds=50监控 |
max_depth | 4~7 | 单棵树最大深度 | >7易过拟合噪声,<4无法捕获复杂模式 |
learning_rate | 0.01~0.1 | 每棵树贡献权重 | 小值需更多树,但泛化更好;0.05是平衡点 |
subsample | 0.7~0.9 | 训练样本抽样比例 | 0.8减少方差,防过拟合 |
colsample_bytree | 0.7~0.9 | 每棵树特征抽样比例 | 0.8提升特征多样性 |
# 实战调参:用验证集监控early stopping from sklearn.model_selection import train_test_split X_train_full, X_val, y_train_full, y_val = train_test_split( X, y, test_size=0.2, shuffle=False # 时序数据禁止shuffle ) model = xgb.XGBRegressor( n_estimators=500, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, random_state=42 ) # 使用eval_set监控验证集损失 model.fit( X_train_full, y_train_full, eval_set=[(X_val, y_val)], early_stopping_rounds=50, verbose=100 )血泪经验:
early_stopping_rounds=50比n_estimators=500更可靠——当验证集MAE连续50轮不下降,立即终止,避免过拟合。verbose=100每100轮打印一次loss,方便观察收敛性。
5. 避坑指南:那些让模型在实盘中集体翻车的隐蔽陷阱
5.1 现象:回测R²高达0.8,但实盘预测方向准确率仅48%
原因:模型过度拟合收益率的微小波动(如±0.2%),而忽略方向性(涨/跌)。R²对绝对误差敏感,但交易更关心符号一致性。
解决:在损失函数中加入方向惩罚项。改用自定义目标函数:
def direction_weighted_mse(y_true, y_pred): # 对方向错误的样本(true*pred<0)加大权重 weights = np.where(y_true * y_pred < 0, 2.0, 1.0) return np.mean(weights * (y_true - y_pred) ** 2) # 在XGBoost中通过objective参数传入(需封装为可调用对象) # 实际项目中建议用LightGBM的custom_objective更易实现5.2 现象:特征重要性显示“Volume”权重最高,但删掉它模型性能不变
原因:Volume与其他特征(如price change)高度共线性,XGBoost的split gain计算受此干扰,产生虚假重要性。
解决:用SHAP值替代内置feature_importances_:
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_val) shap.summary_plot(shap_values, X_val, plot_type="bar") # 显示真实贡献度5.3 现象:模型在牛市表现好,熊市完全失效
原因:训练数据未覆盖熊市场景(如只用2020-2021年数据),模型未学习下跌过程中的量价关系。
解决:强制在训练集中包含完整牛熊周期。检查数据时间范围:
# 必须包含至少一个完整熊市(如2022年美联储加息周期) bear_start = '2022-01-01' bear_end = '2022-12-31' if not (X.index.min() <= bear_start and X.index.max() >= bear_end): print("警告:训练数据未覆盖2022年熊市,请扩展period参数!")5.4 现象:预测结果出现极端值(如预测收益率±50%)
原因:XGBoost对异常样本敏感,且未对目标变量做截断。
解决:在训练前对y进行winsorize处理(限制在±3%分位数内):
from scipy.stats.mstats import winsorize y_winsorized = winsorize(y, limits=[0.01, 0.01]) # 两端各截1% y_winsorized = pd.Series(y_winsorized, index=y.index)5.5 现象:同一支股票换不同代码(如AAPL vs AAPL.O)结果差异巨大
原因:Yahoo Finance中不同后缀代表不同交易所(.O=纳斯达克,.L=伦敦),汇率、交易时间、分红政策不同。
解决:统一使用主交易所代码,并在数据加载时校验:
# 加载后立即检查是否为预期交易所 if '.O' not in ticker and 'NASDAQ' not in str(data.info.get('exchange', '')): print(f"警告:{ticker}可能非纳斯达克上市,建议使用{ticker}.O格式")6. 预测结果解读与实盘衔接:如何把“0.0123”变成可执行的交易信号
6.1 从点预测到区间预测:给每个预测值配上不确定性度量
XGBoost本身不输出概率分布,但我们可以通过分位数回归森林(Quantile Regression Forest)或Bootstrap采样获得预测区间。这里采用更轻量的Bootstrap:
from sklearn.utils import resample def bootstrap_prediction_interval(model, X_test, n_bootstraps=100, alpha=0.1): """计算90%预测区间(alpha=0.1 → 5%和95%分位数)""" preds = [] for _ in range(n_bootstraps): # 对训练集重采样(有放回) X_boot, y_boot = resample(X_train_full, y_train_full, random_state=_) boot_model = xgb.XGBRegressor(**model.get_params()) boot_model.fit(X_boot, y_boot) preds.append(boot_model.predict(X_test)[0]) lower = np.percentile(preds, (alpha/2)*100) upper = np.percentile(preds, (1-alpha/2)*100) return lower, upper # 对最新一日做预测 latest_X = X.iloc[[-1]] pred_point = model.predict(latest_X)[0] pred_lower, pred_upper = bootstrap_prediction_interval(model, latest_X) print(f"最新预测:次日收益率 {pred_point:.4f} " f"[{pred_lower:.4f}, {pred_upper:.4f}] (90%置信)")为什么需要区间?若
pred_point=0.015但[−0.005, 0.035],说明上涨概率仅略高于50%,不宜重仓;若[0.010, 0.020],则方向确定性高,可执行。
6.2 信号转化规则表:把数值预测映射到具体动作
预测值本身无交易意义,必须结合仓位管理规则。以下是经过某跨平台系统实盘验证的转化逻辑:
| 预测收益率区间 | 置信区间宽度 | 信号强度 | 建议动作 | 最大仓位 |
|---|---|---|---|---|
| > 0.015 | < 0.01 | ★★★★ | 做多 | 100% |
| > 0.008 | < 0.015 | ★★★ | 做多 | 70% |
| −0.005 ~ 0.005 | < 0.02 | ★ | 观望 | 0% |
| < −0.008 | < 0.015 | ★★★ | 做空 | 70% |
| < −0.015 | < 0.01 | ★★★★ | 做空 | 100% |
# 自动信号生成函数 def generate_signal(pred_point, pred_lower, pred_upper): width = pred_upper - pred_lower if pred_point > 0.015 and width < 0.01: return "LONG_FULL", 1.0 elif pred_point > 0.008 and width < 0.015: return "LONG_PARTIAL", 0.7 elif pred_point < -0.008 and width < 0.015: return "SHORT_PARTIAL", 0.7 elif pred_point < -0.015 and width < 0.01: return "SHORT_FULL", 1.0 else: return "HOLD", 0.0 signal, position_size = generate_signal(pred_point, pred_lower, pred_upper) print(f"生成信号: {signal}, 建议仓位: {position_size*100:.0f}%")6.3 回测验证:用Zipline框架跑通最小可行回测
光看预测不准,必须嵌入交易逻辑验证。我们用Zipline(开源回测框架)构建最简回测:
from zipline.api import order_target_percent, record, symbol from zipline import run_algorithm def initialize(context): context.asset = symbol('AAPL') context.signal = 0 # 初始化信号 def handle_data(context, data): # 此处注入你的预测逻辑(简化为伪代码) # pred = your_model.predict(...) # context.signal = 1 if pred > 0.01 else -1 if pred < -0.01 else 0 # 执行仓位调整 if context.signal == 1: order_target_percent(context.asset, 1.0) elif context.signal == -1: order_target_percent(context.asset, -1.0) else: order_target_percent(context.asset, 0.0) # 记录关键指标 record(price=data.current(context.asset, 'price'), leverage=context.account.leverage) # 运行回测(需提前准备bundle数据) results = run_algorithm( start=pd.Timestamp('2021-01-01', tz='UTC'), end=pd.Timestamp('2023-12-31', tz='UTC'), initialize=initialize, handle_data=handle_data, capital_base=100000.0, bundle='quandl' # 使用Quandl免费数据源 )注意:Zipline需配置数据bundle,首次运行较慢;重点看
results['sharpe'] > 1.0且results['max_drawdown'] < 0.2,即为初步合格。
我做这类项目时,永远在模型上线前加一道“人工熔断”:把预测结果导出为CSV,用Excel手动检查最近10次预测的逻辑是否合理(如“大盘暴跌日却预测大涨”立刻停用)。技术可以自动化,但风控必须有人盯。希望帮到你。
本文还有配套的精品资源,点击获取