涨跌预测模型实战:从特征工程到时间序列交叉验证的完整链路
发布时间:2026/10/1 9:02:44来源:尧图网络
简介一套聚焦金融时间序列涨跌预测的实战资源面向对机器学习与深度学习在量化分析中应用感兴趣的学习者。资源围绕LSTM长短时记忆网络与常见机器学习算法的建模流程展开覆盖数据读取与预处理、特征工程、模型训练、性能对比及结果分析等完整环节有助于理解从行情指标到趋势预测的全链路实现。压缩包共7个文件由4个Python脚本、2份Excel数据表和1个LSTM模型文件组成其中脚本分别承担数据读取、机器学习预测、LSTM预测和模型对比等功能Excel表则提供全指标汇总数据及预测结果整体大小约620KB。数据文件包含开盘价、收盘价、成交量、MACD、RSI等常用技术指标可直接作为模型输入。目前已有336人浏览学习。借助这套资源读者既能学习LSTM与传统机器学习方法的对比思路也能直接复现实验并观察特征数据对涨跌预测的影响适合希望快速上手金融预测建模的中级开发者。1. 涨跌预测不是玄学先搞清你在预测什么行业内有一种普遍错觉预测涨跌就等于预测未来价格。实际做下来你会发现一个靠谱的涨跌预测系统只回答一个问题——下一阶段价格向上的概率是否大于某个阈值。真正决定账户盈亏的不是模型输出的那个概率数字而是你在什么条件下采纳它、用什么仓位兑现它。《涨跌预测高质量精讲》这套资源把从数据清洗、特征构造、模型选型到回测验证的完整链路拆开讲并把链路里最容易隐性翻车的环节逐一暴露出来。它适合两类人已经会写基础Python代码、但模型效果始终不稳定的量化初学者以及想在现有规则策略上加入一层机器学习判别能力的从业者。读懂它你可以少走很多弯路。2. 数据准备与特征工程预测模型的底座不在模型在数据涨跌预测模型的性能不稳定十次有八次出在数据侧。频率怎么选、标签怎么打、特征怎么对齐这些环节决定了模型能看到什么而模型只是把特征里的信息榨干。2.1 数据频率的选择日线、小时线还是分钟线数据频率是很多人第一次翻车的地方。早期我见过不少新人直接上1分钟K线理由是样本多、训练充分。但分钟线相邻K线之间的价格变动高度相关这种自相关会被模型当成“记忆”而非“规律”——训练集上表现极好一换新数据就崩盘。我一般建议按持仓周期倒推频率做日内波段用15分钟线起步不要直接上1分钟线做2到5天的波段以日线为主、小时线作为入场过滤做更长的持仓则日线选方向、周线过滤。这套资源里的案例走的就是“日线选方向、小时线定入场”的双频框架先用日线信号判断多空倾向再用小时线特征确认具体执行节奏。数据读取和基础清洗是这样处理的import pandas as pd df pd.read_csv(kline_data.csv, parse_dates[datetime]) df df.set_index(datetime).sort_index() # 过滤成交量异常处理集合竞价和停牌造成的假K线 df df[df[volume] 0] # 去重同一时间戳出现多行时保留最后一行 df df[~df.index.duplicated(keeplast)] # 生成日线收盘序列并计算收益率 daily_close df[close].resample(1D).last().dropna() daily_ret daily_close.pct_change()这段代码做了三件事时间索引排序、剔除零成交量K线、按日线重采样生成收益率序列。pct_change() 生成的是简单收益率后续特征计算全部基于收益率而不是价格绝对值这样做的目的是规避价格本身的不平稳性带来的伪回归问题。有一个细节容易忽略跨时区数据做 resample(1D) 会出现日期错位建议先统一用无时区索引。2.2 标签构造涨跌预测问题中的“涨跌”到底怎么定义标签定义是所有环节里最容易被忽略、却又影响最大的一步。不少初学者直接把“下一根K线收盘价高于当前收盘价”定义为涨在趋势行情里这种做法会让标签严重偏向一方模型学到的只是惯性跟随不是转折识别。更稳妥的做法有两种。第一种是未来N根K线的累计收益率超过某阈值计为上涨低于对应负阈值计为下跌中间区域计为震荡。第二种是用未来N根K线的最高价与最低价构造区间突破标签。前者适合趋势策略后者更适合震荡策略。资源里的案例分析用了两种标签的对照实现import numpy as np def make_trend_label(close, horizon5, up_th0.02, down_th-0.02): 趋势型标签未来horizon根累计收益率 future_ret close.shift(-horizon) / close - 1.0 label np.zeros(len(close), dtypeint) label[future_ret up_th] 1 label[future_ret down_th] -1 return label def make_breakout_label(high, low, close, horizon5): 区间突破型标签未来是否存在突破性行情 future_high high.shift(-horizon).rolling(horizon).max() future_low low.shift(-horizon).rolling(horizon).min() label np.zeros(len(close), dtypeint) label[future_high close * 1.03] 1 label[future_low close * 0.97] -1 return label我把标签做成三分类1、0、-1而不是常见的二分类。原因很实际真实交易中“方向不明”是合法状态强制二分类会逼着模型在低置信区间做出错误判断。horizon和阈值需要根据品种波动率手动调整我做日线策略时horizon通常取5到10阈值取该品种近一年日均振幅的0.8倍波动大的品种可以把阈值放到3%到4%。2.3 特征工程三层特征与滞后对齐特征设计通常按三层展开。第一层是量价特征包括过去N期收益率、成交量变化率、最高价最低价在K线振幅中的位置。第二层是技术指标特征包括RSI、MACD柱值、布林带percentB。第三层是滚动统计特征包括滚动均值、滚动标准差、滚动偏度。前两层好理解第三层最容易踩坑。滚动统计量计算时天然包含当前数据比如滚动5日均值在当天收盘后计算没问题但如果你在盘中预测这个值实际上还不存在。解决方案是整体shift(1)让特征滞后一根K线def make_features(df, lookback5): feat pd.DataFrame(indexdf.index) # 滞后收益率 feat[ret_lag1] df[return].shift(1) feat[ret_lag2] df[return].shift(2) # 滚动统计量shift(1)避免用到当前K线 feat[ret_ma] df[return].rolling(lookback).mean().shift(1) feat[ret_std] df[return].rolling(lookback).std().shift(1) feat[ret_skew] df[return].rolling(lookback).skew().shift(1) # 量能变化 vol_ma df[volume].rolling(lookback).mean().shift(1) feat[vol_ratio] df[volume] / vol_ma # K线位置收盘价在当日振幅中所处的百分比 feat[pos_in_range] (df[close] - df[low]) / (df[high] - df[low]) # 简化版RSI delta df[return] gain delta.clip(lower0).rolling(lookback).mean().shift(1) loss (-delta).clip(lower0).rolling(lookback).mean().shift(1) feat[rsi_simple] gain / (gain loss) return feat参数说明lookback5是通用起始值不是定案。vol_ratio 用的是当前成交量除以过去5日均量这个特征天生就是当根信息用于预测次日方向时我建议额外做一次shift。pos_in_range 也是同步特征不能作为预测未来方向的输入实战中我会把它滞后化处理。特征构造完成后还有一个必要的检查把特征和标签按时间索引对齐避免因为索引错位导致最后一份样本里带NaN或者错位数据。3. 模型选型与技术实现从统计假设到机器学习的落差数据侧清理干净后进入模型阶段。这套资源在模型部分的核心结论是复杂的模型不一定更好三类模型的适用边界完全不同你要根据手上的数据和交易频率选主力模型。3.1 逻辑回归基线为什么先跑一个可解释的模型逻辑回归在涨跌预测场景里的价值被严重低估。很多人觉得它配不上“预测”这个词但它在实际流程里有两个不可替代的作用第一是输出天然的概率值可以直接对接资金管理模块第二是特征权重可解释能反向检查特征构造有没有问题。我的习惯是无论最终用什么模型都先跑一套逻辑回归全流程。如果某个特征权重方向与常识相反——比如成交量放大理应提高上涨概率系数却是显著负值——先不要怀疑市场逻辑先回头检查特征构造。大概率是滞后方向错了或者数据对齐出了问题。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) model_lr LogisticRegression(C0.1, max_iter1000, class_weightbalanced) model_lr.fit(X_train_s, y_train) # 检查特征权重确认方向与业务认知一致 coef_df pd.DataFrame({ feature: X_train.columns, coef: model_lr.coef_[0] }).sort_values(coef, ascendingFalse)参数说明C是正则化强度的倒数C越大正则化越弱在特征多但样本量少的场景里我一般取0.1到1之间的小值。class_weightbalanced 用于处理类别不平衡模型会自动把少数类样本权重调高避免整体偏向多数类。逻辑回归输出概率时用 predict_proba 而不是 predict这样后续仓位折算可以直接使用概率值。3.2 树模型主力LightGBM参数设置与特征筛选中低频涨跌预测场景里LightGBM是目前性价比最高的大将。它天然支持特征交互对量纲不敏感训练速度快调参空间大。相比随机森林它的优势是训练更快、Leaf-wise生长方式在高维稀疏数据上表现更好。但LightGBM的代价是容易在小样本上过拟合。我的优化方法是缩小num_leaves、开启行采样和列采样、增大L2正则。以下是一组比较保守的起点参数import lightgbm as lgb params { objective: multiclass, num_class: 3, learning_rate: 0.05, num_leaves: 31, max_depth: 6, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 1.0, min_data_in_leaf: 50, verbose: -1 } model_gbm lgb.train( params, lgb.Dataset(X_train, y_train), num_boost_round300, valid_sets[lgb.Dataset(X_val, y_val)], callbacks[lgb.early_stopping(50)] )参数逐个说清楚。learning_rate0.05配300轮迭代通常比0.1配100轮泛化更好因为学习率越小每棵树贡献的增量越小整体偏差更平滑。num_leaves31对应原生深度约5层太小欠拟合、太大过拟合31是保守起点。feature_fraction和bagging_fraction都是0.8让每棵树看到不同的特征子集和样本子集降低树间相关性。min_data_in_leaf50强制叶节点至少有50个样本有效阻止噪声被学进去。early_stopping(50)监控验证集的多分类logloss连续50轮不降就停机。特征筛选我会做两层先用feature_importance的gain排序去掉重要性接近零的特征再用相关系数矩阵剔除高度相关的冗余特征。corr X_train.corr().abs() upper corr.where(np.triu(np.ones(corr.shape), k1).astype(bool)) drop_cols [c for c in upper.columns if any(upper[c] 0.85)] X_train_filtered X_train.drop(columnsdrop_cols)相关系数阈值为0.85高于这个值的特征只保留gain重要性较高的那一列。需要留意的是相关性计算只能发生在训练集上不能在全部数据上计算后再划分训练测试集否则测试集的分布信息已经渗透进了特征选择过程。3.3 深度模型的边界LSTM在时序预测里的适用条件深度模型在涨跌预测里被高估得最严重。LSTM相对树模型的最大优势是能学习序列内部的长距离依赖比如连续缩量阴线后的放量阳线这类时间序列模式。但日线级别的样本量通常只有几千根LSTM参数量稍大就会过拟合。我的判断标准是分钟级或秒级数据可以尝试LSTM日线数据建议谨慎。LSTM训练前要把数据组织成滑窗序列窗口长度一般取16到64根import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_sequences(features, labels, seq_len16): X_seq, y_seq [], [] for i in range(seq_len, len(features)): X_seq.append(features[i - seq_len:i]) y_seq.append(labels[i]) return np.array(X_seq), np.array(y_seq) X_seq, y_seq build_sequences(X_train_scaled, y_train, seq_len16) model_lstm Sequential([ LSTM(32, return_sequencesTrue, input_shape(16, X_train.shape[1])), Dropout(0.2), LSTM(16), Dropout(0.2), Dense(3, activationsoftmax) ]) model_lstm.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy])seq_len16意味着每条样本由过去16根K线的特征序列组成。LSTM层数不要堆太深两层足够隐藏单元32和16已经能表达多数金融时序模式再大就是过拟合隐患。Dropout0.2是给每层加正则如果验证集logloss不降优先加大Dropout而不是增加层数。训练时用EarlyStopping监控验证集准确率patience设为10。最终复盘时我会拿LSTM结果与LightGBM对比收益和胜率没有显著提升就果断用LightGBM上线。4. 参数设置与回测验证训练和验证之间隔着一道时间涨跌预测模型的评估和普通机器学习问题有个本质区别时间顺序是数据最核心的结构破坏这个结构一切评估指标都会失真。这一章重点讲时间序列交叉验证、回测指标和训练测试集划分。4.1 时间序列交叉验证随机打乱就是数据泄漏处理时序数据时最常见的错误是在调参时直接套用默认的KFold交叉验证。KFold默认随机划分样本会把未来样本混进训练集模型在验证集上的成绩等于提前看到了答案。这套流程跑出来的成绩越漂亮实盘翻车概率越大。推荐的替代方案是TimeSeriesSplit或者手工实现滚动前推验证。两者的共同点是只用历史数据训练用最新一段数据验证每次验证都在模拟“预测未来”的状态from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X_train)): X_tr, X_va X_train.iloc[train_idx], X_train.iloc[val_idx] y_tr, y_va y_train[train_idx], y_train[val_idx] # 每一折内重新做标准化验证集只transform不fit sc StandardScaler() X_tr_scaled sc.fit_transform(X_tr) X_va_scaled sc.transform(X_va) # 训练并记录验证集logloss与accuracyn_splits5表示做5次滚动前推交叉验证第一次只用第1段训练验证第2段第二次用第1和第2段训练验证第3段以此类推。前几折的验证成绩参考意义有限因为训练数据少最后一折的成绩最关键此时训练数据量最接近未来实盘时的状态。关键细节是每一折内都要重新fit和transform标准化器。如果先对全部数据标准化再划分训练集和验证集两者的均值方差就已经互相污染验证成绩就不是真正的样本外表现。4.2 回测参数与评价指标胜率、盈亏比与最大回撤评价涨跌预测模型不能只看准确率。胜率70%但平均盈利30点、平均亏损50点长期执行照样亏钱反过来胜率45%但盈亏比3比1资金曲线可以非常漂亮。三个核心指标要一起看才能判断一个模型真实水平。回测参数设置建议照抄下表之后按品种特性调整参数建议初始值调整方向信号置信度阈值0.6高波动品种上调低波动品种下调止损比例2%日内策略压低到1%波段策略放宽到3%目标止盈比例6%与止损匹配确保盈亏比不低于2单笔最大仓位2%回撤超过15%时减半持仓时限5根K线超时离场避免尾仓拖累回测输出除了关注收益曲线更要关注回撤段的形态。如果最大回撤恰好集中在一段连续行情里说明模型对那类行情过拟合了需要回头看训练集里是否塞进了太多同质样本。最大回撤超过20%说明仓位失控这时候优先降仓位而不是继续改模型参数。4.3 训练集验证集测试集的划分比例与信息隔离资源包中给出的划分原则是按时间顺序70%训练、15%验证、15%测试。验证集用于模型选择测试集只允许评估一次评估完就不再回头调参。这种三分法在金融时序上的意义比随机划分大很多因为它等于模拟了“过去训练、近期验证、未来应用”的完整流程。这里有个现实中的坑市场上行和下行状态切换可能让固定比例失效。假如前70%数据刚好覆盖一波牛市模型学到的是牛市模式测试集正好处于熊市效果当然不好。所以划分时不能只看比例还要评估训练集和测试集各自覆盖的行情状态是否相近。如果只有两年数据前一年半都是牛市那这个划分怎么算都不够健康。训练与实盘衔接上还有一个容易忽略的细节——标准化器的保存和复用。正确做法是把训练时的scaler用joblib存盘实盘新数据到达时只做transform绝不对新数据重新fit。import joblib # 训练完毕后保存标准化器供实盘新数据使用 joblib.dump(scaler, scaler.pkl) # 实盘新数据接入时加载并transform sc_new joblib.load(scaler.pkl) X_new_scaled sc_new.transform(X_new)实盘新数据的均值和方差如果与训练时相差过大模型大概率已经失效。我会每跑完一周就对比一次新数据分布与训练分布的差异偏差超过20%就重新训练模型不再沿用旧权重。5. 涨跌预测高频问题排查五个真实翻车现场这部分内容全部来自实战复盘每个坑都对应资源中某一讲的排错方法。如果你照流程搭完模型但跑不出效果大概率撞上了其中一条。5.1 现象回测曲线完美实盘第一周就大幅回撤回测净值曲线陡峭向上年化收益看起来亮眼实盘第一周资金就开始缩水。原因大概率是未来函数特征计算时用了当根K线的收盘信息信号却在当天开盘时已经产生或者标签代码里把 shift(-5) 写在了 shift(1) 之前让样本混入了未来5日的收益数据。解决方法是跑一遍泄漏自检对每个特征计算它与未来收益的相关系数正常特征应近似为0相关性偏高的特征优先检查它的shift方向是漏滞后还是滞后方向写反。5.2 现象同一套代码换一个品种胜率从62%掉到39%模型在A品种上表现不错在B品种上直接失效。原因多半是两个品种的波动率水平和K线节奏差异过大模型学到的特征分布是A品种特有的形态。解决办法分两级先用收益率除以滚动标准差做波动率归一化让不同品种的特征分布尽量接近再做分桶训练资源里的示例是按波动率高、中、低分为三桶桶内训练、桶间不互相迁移预测。经过这两步处理跨品种的胜率差距能从20个百分点缩小到5个百分点以内。5.3 现象概率排序靠前的样本胜率不低账户却没赚钱模型预测概率从高到低排序取前20名买入盈利单的持仓时间很长亏损单很快就触发止损最终账户不赚钱。这大概率不是概率预测错了而是概率本身只代表方向置信度不代表最终盈亏比。举个例子两个品种都被预测为上涨一个波动率低、一个波动率高高波动品种的潜在亏损空间也大。解决方法是不要直接用概率做排序把概率和平均真实波幅相乘得到期望波动收益再用这个调整后的值排序。这套做法在资源第8讲有专门的实操对比。5.4 现象测试集准确率85%实盘表现却和抛硬币一样测试集准确率看着诱人实盘一跑只剩50%上下。常见的原因是类别不平衡加评估指标错配。上涨样本占75%时模型只用无脑输出“涨”就能有75%准确率加上少数类的样本还被特征区分了一部分85%的准确率就出现了。解决方法是先看混淆矩阵重点盯“下跌样本里预测正确的比例”只看准确率一定会被骗训练时配合 class_weightbalanced少数类样本上采样或多数类下采样。5.5 现象训练好的模型第二天继续跑预测结构突然全部乱套第一天预测正常第二天加载同一个模型输出全部偏离。原因基本可以锁定在特征标准化环节代码在每次运行时都对最新数据重新fit了scaler新数据里的最大值和最小值发生变化导致特征刻度整体偏移特征间的相对关系被破坏。解决方法是把scaler和模型一起存盘新数据只允许transform、不允许重新fit。如果实在要更新scaler就同时重训模型不要只更新标准化器而保留旧权重。6. 把涨跌预测概率转成仓位最后一步决定账户生存胜率再高的模型最终也得靠仓位管理来兑现利润。三个环节——信号采纳、仓位折算、止损执行——任何一个脱节概率优势都会归零。信号采纳方面模型输出的概率不是下单依据而是强弱分层依据。0.6到0.75之间视为弱信号开小仓0.75以上视为强信号加仓低于0.6不开仓。阈值不是拍脑袋定的要回过头看回测里不同概率段的真实准确率用概率校准曲线来拟合。仓位折算方面我常用凯利公式的保守变体先算盈亏比再算凯利系数最终取凯利值的四分之一作为实际仓位。全凯利在实战中回撤波动太大四分之一凯利才是能长期生存的版本。def calc_position(prob, avg_win, avg_loss, risk_ratio0.25): b avg_win / avg_loss q 1 - prob kelly (b * prob - q) / b kelly max(kelly, 0) return min(kelly * risk_ratio, 0.02) # 单笔风险不超资金2% pos_ratio calc_position(prob0.68, avg_win40, avg_loss20)参数说明avg_win40和avg_loss20来自回测时统计的平均盈利和平均亏损risk_ratio0.25是保守系数最后一行代码把实际仓位限制在2%以内无论模型置信度多高单笔风险暴露都不超过账户资金的2%。预测概率决定方向仓位决定生存这个顺序永远不能反。止损执行方面我的习惯是设三层止损初始止损进场就放好移动止损在盈利后逐根K线抬高时间止损在持仓超过计划周期且没有进展时触发。每层止损都提前写进纸面执行记录盘中不做主观判断。从那以后我每次上实盘都强制自己走一遍“信号确认、概率阈值、凯利折算、止损触发”四步纸面推演推演不过关就不动真钱。这套流程帮我躲过了不少看着漂亮但实际赚不到钱的模型希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网