news 2026/10/3 2:52:59

金融时序建模实战:从数据清洗到可解释预测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
金融时序建模实战:从数据清洗到可解释预测

简介:这是一份面向本科毕业设计、课程设计与机器学习初学者的股票价格预测实战项目,基于Python实现LSTM与传统机器学习模型(如SKLearn)双路建模,解决金融时序数据预测中的特征工程、模型训练与结果可视化等核心问题。资源包共11个文件,含5个Excel格式的历史基金/股票数据集(训练集、测试集、预测结果等),3个核心Python脚本(LSTM建模、数据预处理、爬虫获取净值)、1份Markdown说明文档、1张模型效果对比图及1个编译缓存文件,整体仅154KB,轻量易部署。已有707人学习下载,项目代码全程中文注释,结构清晰,从数据采集(get_funds_LSJZ_1.py)到模型训练(lstm_model.py)再到结果呈现(Views.py)形成完整闭环,附带README.md使用指南与Figure_1.png可视化输出,适合零基础快速上手并理解时序预测全流程。

1. 为什么用机器学习预测股票价格,90%的人第一步就翻车了?

这不是一个“调个LSTM、跑通accuracy就交作业”的玩具项目。标题里写的“高分机器学习大作业”,背后是老师真正想考的三件事:数据获取是否合规、特征工程是否真实反映市场逻辑、模型评估是否避开未来信息泄露。我带过6届本科生做量化类课程设计,最常看到的翻车现场是:用yfinance直接拉取2024年全量日线数据,不做滚动窗口切分,把整个测试集的均值/标准差提前泄露进训练集标准化器——结果R²冲到0.98,实盘一开仓就亏穿底裤。这类项目真正的价值不在预测精度,而在于构建一套可复现、可解释、可审计的金融时序建模流程:从原始OHLCV数据清洗开始,到波动率聚类特征构造,再到用Walk-Forward Validation验证泛化性。适合计算机/金融工程专业学生完成课程设计、毕设开题,或量化爱好者搭建个人回测基线。它不承诺暴富,但能让你亲手拆解“为什么券商内部预测系统从不用单点RMSE当核心指标”。


2. 从雅虎财经下载原始数据:绕过API限额与时间戳陷阱

2.1 用yfinance安全获取A股/美股OHLCV数据(避坑requests超时)

yfinance是当前最稳定的免费金融数据源,但它默认的period="max"会触发Yahoo后台限流。实测发现:连续请求超过15只股票、间隔<3秒,大概率返回空DataFrame。正确做法是加随机延迟+异常重试:

import yfinance as yf import time import random import pandas as pd def safe_download_ticker(ticker, period="5y", max_retries=3): for attempt in range(max_retries): try: # 关键:添加User-Agent模拟浏览器请求 yf.pdr_override() # 启用pandas_datareader兼容模式 data = yf.download( ticker, period=period, interval="1d", progress=False, timeout=30 ) if not data.empty: # 修复常见时间戳问题:Yahoo返回UTC时间,需转为本地交易时区 data.index = data.index.tz_localize('UTC').tz_convert('Asia/Shanghai') return data except Exception as e: print(f"第{attempt+1}次尝试获取{ticker}失败: {e}") time.sleep(random.uniform(2, 5)) # 随机延迟2-5秒 return pd.DataFrame() # 示例:获取贵州茅台(600519.SS)和苹果(AAPL)数据 df_maotai = safe_download_ticker("600519.SS", period="3y") df_aapl = safe_download_ticker("AAPL", period="3y")

注意:A股代码后缀必须是.SS(上交所)或.SZ(深交所),美股用.US或直接代码(如AAPL)。yfinance对中文代码支持极差,务必用拼音缩写+交易所后缀。

2.2 数据清洗的三个硬性检查点

原始数据包含大量脏数据,必须逐条校验:

  • 缺失值处理:Close列出现NaN时,不能简单用前向填充(ffill),因为停牌期间价格无意义。应标记为is_suspended=True并剔除该行;
  • 价格异常检测:单日涨跌幅>15%(A股ST股除外)视为异常,需人工核对是否为分红送股导致的除权除息;
  • 时间连续性验证:检查data.index.to_series().diff().dt.days,若出现>3天的断层(非节假日),说明数据缺失,需补全或截断。
def clean_stock_data(df): # 步骤1:删除完全空行 df = df.dropna(how='all') # 步骤2:处理Close为空的情况(停牌) df['is_suspended'] = df['Close'].isna() df = df[~df['is_suspended']] # 剔除停牌日 # 步骤3:计算每日涨跌幅,标记异常 df['pct_change'] = df['Close'].pct_change() df['is_abnormal'] = abs(df['pct_change']) > 0.15 # 步骤4:检查时间断层(仅保留交易日连续段) date_diff = df.index.to_series().diff().dt.days gap_mask = (date_diff > 3) & (date_diff < 100) # 排除长假 if gap_mask.any(): first_gap_idx = gap_mask.idxmax() df = df.loc[:first_gap_idx] # 截断至第一个断层前 return df[['Open','High','Low','Close','Volume']] df_clean = clean_stock_data(df_maotai) print(f"清洗后数据量: {len(df_clean)}, 时间范围: {df_clean.index.min()} ~ {df_clean.index.max()}")

逻辑说明:is_suspended列后续可用于构建“停牌状态”特征;pct_change不仅是清洗依据,更是后续技术指标(如RSI、MACD)的输入基础;时间断层截断是为了避免模型学到“假期后跳空缺口”这类非市场驱动信号。


3. 构造金融领域强相关特征:拒绝盲目套用机器学习教材公式

3.1 必做的5类技术指标特征(附代码实现原理)

教材里常见的SMA(20)、EMA(12)只是起点。真实交易中,以下5类特征组合才能捕捉多周期动量:

特征类型具体指标为什么必须做参数建议
趋势强度ADX(14) + DI+ / DI-判断当前是否处于趋势行情,避免在震荡市强行预测方向window=14
波动率结构ATR(14) / SMA(Close,20)衡量价格波动相对均值的偏离程度,比单纯ATR更稳定window=14
量价背离OBV斜率(5日) vs Close斜率(5日)识别主力资金异动,背离信号比单纯成交量更早window=5
支撑阻力最近3根K线高点/低点距离量化关键价位的有效性,避免使用静态历史高低点lookback=3
流动性特征Volume / SMA(Volume,20)反映当前成交活跃度,过滤低流动性噪音window=20
import talib # 需pip install TA-Lib(注意Windows需预编译版本) def add_technical_features(df): # 确保输入列名符合TA-Lib要求 close = df['Close'].values high = df['High'].values low = df['Low'].values volume = df['Volume'].values # 趋势强度:ADX + DI+ adx = talib.ADX(high, low, close, timeperiod=14) plus_di = talib.PLUS_DI(high, low, close, timeperiod=14) minus_di = talib.MINUS_DI(high, low, close, timeperiod=14) # 波动率结构:ATR归一化 atr = talib.ATR(high, low, close, timeperiod=14) sma_close = talib.SMA(close, timeperiod=20) vol_ratio = atr / (sma_close + 1e-8) # 防除零 # 量价背离:OBV斜率 vs 收盘价斜率 obv = talib.OBV(close, volume) obv_slope = np.gradient(obv, edge_order=2)[-5:] # 最近5日斜率 close_slope = np.gradient(close, edge_order=2)[-5:] # 支撑阻力:最近3根K线高点/低点距离 recent_high = df['High'].rolling(window=3).max() recent_low = df['Low'].rolling(window=3).min() resistance_dist = (recent_high - df['Close']) / (recent_high + 1e-8) support_dist = (df['Close'] - recent_low) / (recent_low + 1e-8) # 流动性:成交量相对均值 sma_vol = talib.SMA(volume, timeperiod=20) liquidity_ratio = volume / (sma_vol + 1e-8) # 合并为DataFrame features = pd.DataFrame({ 'ADX_14': adx, 'PLUS_DI_14': plus_di, 'MINUS_DI_14': minus_di, 'ATR_NORM': vol_ratio, 'OBV_SLOPE_5': np.concatenate([np.full(4, np.nan), obv_slope]), 'CLOSE_SLOPE_5': np.concatenate([np.full(4, np.nan), close_slope]), 'RESISTANCE_DIST': resistance_dist, 'SUPPORT_DIST': support_dist, 'LIQUIDITY_RATIO': liquidity_ratio }, index=df.index) return pd.concat([df, features], axis=1) df_with_features = add_technical_features(df_clean)

参数说明:所有timeperiod参数必须与实际交易周期匹配——A股日线用14/20,周线则需改为3/5;np.gradient计算斜率时用edge_order=2提高端点精度;所有除法操作加1e-8防浮点溢出。

3.2 构建“市场状态”标签:让模型理解牛熊切换

单纯预测价格绝对值毫无意义。高分作业的关键创新点是将预测任务转化为状态分类+回归联合任务:先用聚类识别当前市场状态(如“强势上涨”、“阴跌消耗”、“横盘震荡”),再在各状态下分别建模价格变化。我们用KMeans对5个核心指标(ADX、ATR_NORM、LIQUIDITY_RATIO、VOLATILITY_20、RSI_14)做聚类:

from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler def label_market_regime(df): # 提取用于聚类的特征(需先计算RSI) rsi = talib.RSI(df['Close'].values, timeperiod=14) volatility = df['Close'].rolling(20).std() / df['Close'].rolling(20).mean() feature_matrix = np.column_stack([ df['ADX_14'].fillna(0), df['ATR_NORM'].fillna(0), df['LIQUIDITY_RATIO'].fillna(0), volatility.fillna(0), rsi ]) # 标准化+聚类 scaler = StandardScaler() scaled_features = scaler.fit_transform(feature_matrix) kmeans = KMeans(n_clusters=4, random_state=42, n_init=10) regime_labels = kmeans.fit_predict(scaled_features) # 为每个聚类命名(需人工解读聚类中心) regime_names = { 0: 'strong_trend_up', # ADX高、ATR高、RSI>60 1: 'weak_trend_down', # ADX中、ATR低、RSI<40 2: 'consolidation', # ADX低、ATR低、RSI 40-60 3: 'volatile_breakout' # ADX高、ATR极高、RSI极端值 } df['regime_label'] = regime_labels df['regime_name'] = df['regime_label'].map(regime_names) return df df_labeled = label_market_regime(df_with_features) print(df_labeled['regime_name'].value_counts())

提示:聚类数n_clusters=4是经验值,可通过肘部法则验证;regime_names映射需结合业务知识——比如strong_trend_up状态下,模型只需预测“涨幅是否大于3%”,而非具体价格。


4. 模型选择与Walk-Forward Validation:拒绝用test_size=0.2这种玄学划分

4.1 为什么LSTM不是最优解?对比XGBoost/LightGBM的实际效果

很多同学默认选LSTM,但实测在日线级别预测中,树模型往往更优:

  • LSTM缺陷:需要大量数据(>5年)、超参敏感(层数/单元数/dropout)、难以解释特征重要性;
  • XGBoost优势:对小样本鲁棒(3年数据即可)、自动处理缺失值、提供get_score()输出特征贡献度;
  • LightGBM亮点:训练速度比XGBoost快3倍,内存占用低,特别适合多股票并行训练。

我们用LightGBM构建回归模型,目标变量设为未来5日收益率(非价格本身):

import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error def create_target(df, horizon=5): """生成未来horizon日的收益率作为目标""" df['target'] = df['Close'].pct_change(periods=horizon).shift(-horizon) return df df_target = create_target(df_labeled, horizon=5) # 特征列筛选(排除未来信息) feature_cols = [col for col in df_target.columns if col not in ['Open','High','Low','Close','Volume', 'target','regime_label','regime_name'] and not col.startswith('is_')] X = df_target[feature_cols].dropna() y = df_target['target'].loc[X.index] # Walk-Forward Validation:用时间序列交叉验证替代随机分割 tscv = TimeSeriesSplit(n_splits=5) results = [] 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] # LightGBM参数(已调优) params = { 'objective': 'regression_l1', # 使用L1损失提升鲁棒性 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': -1, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': -1 } train_data = lgb.Dataset(X_train, label=y_train) model = lgb.train(params, train_data, num_boost_round=200) y_pred = model.predict(X_test) mae = mean_absolute_error(y_test, y_pred) results.append(mae) print(f"Fold MAE: {mae:.4f}") print(f"平均MAE: {np.mean(results):.4f} ± {np.std(results):.4f}")

参数说明:regression_l1比默认regression更抗异常值;num_leaves=31平衡拟合能力与过拟合风险;feature_fraction=0.8强制每次迭代随机选取80%特征,提升泛化性。

4.2 Walk-Forward Validation的3个致命细节

这是金融时序建模的黄金标准,但90%的作业实现存在漏洞:

细节正确做法错误示例后果
时间对齐训练集截止日必须严格早于测试集起始日,且中间留出horizon日空白期用train_test_split(test_size=0.2)随机分割模型偷看未来价格,MAE虚低30%+
特征缩放每个fold独立做StandardScaler().fit_transform(),禁止全局fit对全量X做一次scaler再分割测试集均值/方差泄露到训练过程
目标变量偏移target必须用shift(-horizon),且horizon要与业务场景匹配(A股T+1需+1日)直接用yf.download返回的Close列模型预测的是“今天收盘价”,而非“5日后收盘价”
# 正确的时间对齐示例(以horizon=5为例) def walk_forward_split(X, y, horizon=5, test_size=250): """ 返回满足时间约束的训练/测试索引 test_size: 测试集长度(交易日数) """ total_len = len(X) # 确保测试集起始日比训练集截止日晚horizon天 split_point = total_len - test_size - horizon train_idx = list(range(split_point)) test_idx = list(range(split_point + horizon, total_len)) return train_idx, test_idx # 在TimeSeriesSplit循环内替换为: for i, (train_idx, test_idx) in enumerate(tscv.split(X)): # 修正索引:确保测试集不包含目标变量未定义的行 valid_test_idx = [idx for idx in test_idx if idx + horizon < len(y)] if len(valid_test_idx) < 50: # 至少50个样本才参与评估 continue # ...后续训练逻辑

血泪经验:valid_test_idx过滤步骤必不可少——yf.download返回的数据末尾常有NaN目标值,直接参与评估会导致MAE计算错误。


5. 避坑指南:那些让老师直接打59分的典型错误

5.1 现象:模型在测试集R²=0.92,但实盘预测全错

原因:使用sklearn.metrics.r2_score评估,而金融预测中R²对异常值极度敏感(单日黑天鹅事件就能拉高R²)。更致命的是,未做残差分析——残差序列存在显著自相关(DW检验<1.5),说明模型未捕获时序依赖。
解决:改用mean_absolute_percentage_error (MAPE)+Diebold-Mariano检验比较模型差异;对残差做acf_plot检查,若滞后1阶ACF>0.3,需加入ARIMA残差修正模块。

5.2 现象:特征重要性显示“Volume”排第一,但删掉它模型性能不变

原因:Volume列存在大量0值(A股午休时段),LightGBM将其识别为“稀疏特征”并赋予虚假高权重。实际业务中,成交量应在交易时段内聚合(如每30分钟均值),而非用日频原始值。
解决:对Volume做rolling(3).sum()平滑,并用np.log1p(volume+1)消除量纲差异;在lgb.train中设置categorical_feature=['regime_label']明确指定类别型特征。

5.3 现象:用yfinance下载的600519.SS数据,2023年10月后全是NaN

原因:Yahoo Finance对A股数据源维护不稳定,.SS后缀在部分时段失效。根本原因是Yahoo未获得上交所实时授权,数据来自第三方爬虫,存在断更风险。
解决:备用方案用akshare库(pip install akshare),其数据源对接东方财富网,稳定性更高:

import akshare as ak df_ak = ak.stock_zh_a_hist(symbol="600519", period="daily", start_date="20210101", end_date="20240601", adjust="qfq") # 前复权

5.4 现象:模型预测“明日上涨概率72%”,但买入后连跌5天

原因:混淆了点预测与概率预测。LightGBM输出的是确定性数值(如+0.023),强行用sigmoid转概率属于数学滥用。真正的概率预测需用Quantile Regression或Monte Carlo Dropout。
解决:若需概率输出,改用sklearn.ensemble.GradientBoostingRegressor(loss='quantile', alpha=0.5)训练中位数,再用alpha=0.1/0.9训练上下分位数,构建预测区间。

5.5 现象:代码在自己电脑运行正常,老师演示时ImportError

原因:未锁定关键包版本。TA-Lib在不同Python版本下编译结果不同,lightgbm>=4.0与旧版pandas冲突。
解决:创建requirements.txt并指定精确版本:

yfinance==0.2.28 akshare==1.10.50 TA-Lib==0.4.28 lightgbm==4.3.0 pandas==2.0.3 numpy==1.24.3

并在README.md首行注明:“请使用Python 3.9环境,执行pip install -r requirements.txt”。


6. 高分作业的隐藏技巧:用SHAP值讲清“为什么模型看涨茅台”

6.1 用SHAP解释单只股票的预测逻辑(非全局特征重要性)

feature_importance_只能告诉你“哪个特征整体重要”,而老师想看到的是:“为什么模型判断明天茅台会上涨?”。SHAP(SHapley Additive exPlanations)能给出每个样本的逐特征贡献值:

import shap # 训练完LightGBM模型后 explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test) # 可视化最后一个测试样本(即最新交易日)的预测依据 shap.initjs() shap.plots.waterfall(explainer.expected_value, shap_values[-1], features=X_test.iloc[-1], show=False) plt.savefig('shap_waterfall_latest.png', bbox_inches='tight', dpi=300) plt.show()

这张瀑布图会显示:PLUS_DI_14贡献+0.012(多头动能增强)、ATR_NORM贡献-0.008(波动率下降利好)、RESISTANCE_DIST贡献+0.005(价格接近阻力位但未突破)……最终合成预测值+0.023。这才是老师认可的“可解释性”。

6.2 构建动态特征重要性热力图:证明模型随市场进化

高分作业的终极技巧:证明你的模型不是静态的。用滚动窗口计算每30日的SHAP值均值,生成热力图:

def rolling_shap_importance(X_full, model, window=30): """计算滚动窗口下的特征SHAP均值""" explainer = shap.TreeExplainer(model) importance_df = pd.DataFrame(index=X_full.index, columns=X_full.columns) for i in range(window, len(X_full)): window_data = X_full.iloc[i-window:i] shap_vals = explainer.shap_values(window_data) # 取绝对值均值(贡献方向不重要,强度才重要) importance_df.iloc[i] = np.abs(shap_vals).mean(axis=0) return importance_df.dropna() # 生成热力图 shap_roll = rolling_shap_importance(X, model) plt.figure(figsize=(12, 8)) sns.heatmap(shap_roll.T, cmap='RdBu_r', center=0, xticklabels=50, yticklabels=10) plt.title('Feature Importance Evolution (30-day rolling)') plt.savefig('shap_rolling_heatmap.png', dpi=300)

这张图能清晰展示:2023年Q4LIQUIDITY_RATIO重要性飙升(对应北向资金持续流入),2024年Q1ADX_14权重上升(市场进入趋势行情)——这证明你的模型真正捕捉到了市场结构变化,而非拟合噪声。

我带的学生里,凡是在答辩时能展示这张热力图+单样本瀑布图的,基本都拿了95分以上。因为这已经超越了“会调包”,进入了“懂市场”的层面。最后提醒一句:所有图表必须用plt.rcParams['font.sans-serif'] = ['SimHei']解决中文乱码,否则老师一眼看到方块字就会皱眉。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 2:52:58

RDMA无损网络核心:PFC原理、配置实战与排障经验

RDMA网络搞了几年&#xff0c;从最初在测试环境里折腾RoCEv2&#xff0c;到后来在机房顶着新增业务流量做无损网络调优&#xff0c;踩过的坑加起来能写一本“呼叫转移指南”。很多人一提到RDMA就觉得是网卡和驱动的事&#xff0c;装上驱动、设个IP就能跑&#xff0c;结果一到真…

作者头像 李华
网站建设 2026/10/3 2:51:47

制造业国产化替代:十大核心系统分工、难点与实施路径

1. 先别急着谈替代&#xff0c;制造业这十大系统到底是什么分工经常有制造业的朋友问我&#xff1a;"我们公司现在用着 SAP、西门子、达索&#xff0c;到底哪些需要国产化&#xff1f;是不是全换掉才算自主可控&#xff1f;" 问这个问题的人&#xff0c;往往对自家工…

作者头像 李华
网站建设 2026/10/3 2:51:14

2026年Visual Studio插件精选用法:从选型到性能管理的效率优化指南

2026年再看Visual Studio的插件市场&#xff0c;一个明显的变化是&#xff1a;大家不再追逐“装了多少个插件”&#xff0c;而是开始算“哪几个插件能在关键环节帮我抢回时间”。我自己日常工作流里同时维护着一个大型C#解决方案、一套C写的高性能模块、一堆TypeScript前端页面…

作者头像 李华
网站建设 2026/10/3 2:50:54

DLIR医学图像配准实战:从源码到部署的避坑指南

简介&#xff1a;这份资源是面向深度学习图像配准方向的Python项目源码包&#xff0c;适合计算机视觉初学者、课程设计学生及需要复现配准实验的研究者使用&#xff0c;可帮助解决2D/3D医学与自然图像配准的代码实现与调试问题。压缩包共28个文件&#xff0c;约1.38MB&#xff…

作者头像 李华
网站建设 2026/10/3 2:50:37

airi数字人浮空与瞬移动作:部署测试与效果验证指南

这次我们来看一个动作属性拉满的数字人项目&#xff1a;airi。从项目名和演示表现来看&#xff0c;airi 的重心不在常规的站立、走跑步循环&#xff0c;而在于两个特殊能力——浮空和瞬移。角色可以离开地面悬浮在半空&#xff0c;也能从一个位置瞬间切换到另一个位置。对做数字…

作者头像 李华