KNN股市预测实战:从原理到滚动回测的完整指南
简介面向Python机器学习入门者与金融数据爱好者这是一个使用KNN算法完成股市预测的小型源码项目。项目将K近邻分类思路引入股价走势判断场景覆盖从数据读取、特征构造到模型训练与结果预测的完整流程适合用作业余实战练习或课程设计参考。压缩包共2个文件包含1个Python脚本和1个Markdown说明文档整体大小仅3KB脚本实现算法核心逻辑文档对项目结构、运行方式与关键思路作补充说明便于快速上手。已有291人次浏览学习适合希望结合真实金融场景理解KNN原理的读者。通过该源码可看到KNN用于股市预测时的数据组织方式与Python实现细节也能借助配套说明快速定位并修改特征与参数为进一步扩展回测或改进模型提供起点。1. KNN 算法的股市预测这个 zip 里到底装了什么做量化的朋友聊到 KNN 算法的股市预测第一反应往往是又是个玄学模型。把源码真正拆开看你会发现它其实是入门机器学习加金融数据最顺的一条路不用神经网络的黑匣子也不用调一堆超参数。一个 zip 包里两个核心文件——share_foresee_end.py 和 README.md前者是可完整运行的预测脚本后者交代运行方式和数据格式。这个资源解决的具体问题是用过去 N 天的行情特征找出历史上最相似的 K 个交易日投票预测下一交易日涨跌。它适合两类人刚学完 sklearn 想找真实场景练手的 Python 新手以及想搭一个基线模型去对比复杂策略的量化分析者。这个包不承诺赚钱但能让你把 KNN 从理论一路跑到可回测的完整流程顺带把数据泄漏、滚动窗口这些量化里的基本功练扎实。2. KNN 选型与原理为什么相似日能预测下一根 K 线2.1 相似日假设与懒惰学习KNN 的数学直觉KNN 不是传统意义上训练出一个模型它把历史样本原封不动缓存下来预测时才现算距离。sklearn 管这个叫懒惰学习lazy learningfit 方法只把 X 和 y 存下来所有计算都发生在 predict 阶段。这个特性在股市场景里有两面性优点是实现简单、没有参数过拟合的风险缺点是预测时间复杂度是 O(N×D)N 是样本量、D 是特征维度样本过万之后每预测一次就要算上万次距离这也是它在高频场景里天然受限的原因。把 KNN 搬到股市相似日假设可以概括成三个字历史会重演。今天这根 K 线的形态——缩量回踩 20 日均线、量比突然放大、价格贴着 5 日均线走——如果历史上出现过高度相似的形态那这些相似形态之后的涨跌分布就是今天最可靠的参考。KNN 做的就是把高度相似量化为距离把之后涨跌量化为投票。举个具体的数字例子。某股票今天 ret0.01、vol_ratio1.2、ma_gap-0.005历史样本 A 是0.008, 1.15, -0.004B 是0.05, 1.8, 0.02。按欧氏距离A 和今天的距离是 sqrt(0.002² 0.05² 0.001²) ≈ 0.0501B 和今天的距离大约 0.63。显然 A 更接近今天如果 K5、A 的次日是涨它就贡献一票看涨。这就是 KNN 的全部直觉剩下的都是工程细节。距离度量最常用的三种选型逻辑看下表度量方式计算思路股市场景里的特点欧氏距离 euclidean各维度差值平方和再开根号sklearn 默认项特征归一化后最稳曼哈顿距离 manhattan各维度差值绝对值求和对离群值不敏感特征噪声大时更抗造余弦相似度 cosine看向量方向夹角不看长度特征整体缩放不一致时用比如量价混合参数说明KNeighborsClassifier 默认 metriceuclidean、weightsuniform。欧氏距离的前提是特征先归一化否则成交量这种数值大的特征会在距离计算里一票独大收益率这种数值小但信息量大的特征直接被淹没。我一般优先试欧氏距离 MinMax 归一化数据噪声明显时再切曼哈顿。2.2 K 值与投票权重偏差、方差和置信度K 是 KNN 唯一的超参数但它同时控制偏差和方差。K1 时模型只认最近一个邻居方差极大历史上某一天的偶然极端形态就能左右预测K50 时投票被大量远房邻居稀释模型趋近于直接输出多数类偏差变大。日线数据我习惯从 K5 起步配合滚动回测在 5~20 之间找稳定区间具体网格搜法在第六章展开。另外 K 为偶数时存在平票可能KNeighborsClassifier 会按类别索引顺序硬决出胜负我一般直接用奇数 K 把这个不确定性回避掉。weights 参数值得单独说。uniform 是等权投票10 个邻居里 6 个看涨就输出涨distance 模式下越近的邻居权重越高相当于给更相似的形态更大的发言权。股市里的相似度本身是连续的用 distance 通常比 uniform 稳但代价是容易被个别极近的邻居带偏。所以别拍脑袋两个都跑一遍回测再定。投票结果还有一层数学含义K 个邻居里看涨的比例 P上涨数/K本质是条件概率估计。这个 P 不只是分类输出还能当置信度用——K10 时 8 个邻居看涨、P0.8说明当前形态的历史一致性很高P0.52 的预测基本等于抛硬币。第六章会讲怎么利用这个概率做信号过滤那才是 KNN 预测真正能落地的地方。2.3 KNN 在量化场景的边界能做什么、不能做什么先说能做的三件事。第一短周期涨跌方向二分类预测下一交易日收盘相对当天涨跌这是 share_foresee_end.py 的主任务。第二相似 K 线检索把历史上和当前形态最接近的若干交易日找出来人工复核后续走势分布复盘上一次出现这种形态之后发生了什么很好用。第三当基线模型和 LSTM、XGBoost 做对比——很多策略里 KNN 的胜率并不比复杂模型差多少但它能提供简单模型的下限参照让你判断复杂模型多出来的复杂度是否真的换来了收益。不能做的也要说透。KNN 回归虽然能输出数值点位但在非平稳价格序列上预测收盘价基本是随缘方向二分类才是它的舒适区分钟级高频也不现实惰性模型的计算开销扛不住每秒多次预测sklearn 的 algorithm 参数可以设 kd_tree 加速但特征维度一高收益就有限最要命的是分布漂移KNN 假设历史分布稳定而市场风格半年就可能切换一次所以必须滚动训练、只保留最近窗口的样本这个坑在第五章单独展开。边界清楚了再看这个包的主文件会发现它的设计很克制只做方向预测、特征全是派生指标、保留了滚动窗口入口。这正是 KNN 用在股市的正确姿势——先承认局限再在局限里把模型用满。3. 项目结构与数据准备share_foresee_end.py 的运行链路3.1 压缩包结构与环境依赖zip 解压之后核心文件就两个结构很清爽文件作用share_foresee_end.py主流程加载数据、派生特征、建样本、训练 KNN、滚动回测README.md运行说明、数据字段约定、环境要求数据文件一般不在包里需要按 README 约定的字段自己准备日线 CSV。这个项目的定位很明确单脚本可运行不搞工程化目录适合直接读代码学流程也适合改造成自己的第一个量化策略骨架。运行环境非常常规Python 3.7 以上四个依赖装齐就能跑pip install pandas numpy scikit-learn matplotlib # 把数据文件放到约定路径后直接执行主脚本 python share_foresee_end.py逻辑说明第一行装齐四个核心依赖第二行按 README 把数据放到位之后直接执行。如果 README 里要求指定数据路径把 CSV 放到脚本同目录并确保字段名对齐即可。参数说明pandas 负责读表和滚动窗口计算numpy 处理数组运算scikit-learn 提供 KNN 模型和归一化器matplotlib 画回测净值曲线。顺带提一句装 sklearn 用 pip install scikit-learn 就够了网上那些让你单独装 sklearn 包的教程多半是过时内容。提示跑之前先打开 README 确认数据字段名不要想当然用 open/high/low/close 直接读字段对不上时 read_csv 不会报错只会给你一堆 NaN。3.2 数据字段与加载细节KNN 股市预测的数据格式没有行业标准但这类包里最常见的约定是一张日线 CSV字段至少覆盖 trade_date、open、high、low、close、volume。数据来源用 tushare、baostock、akshare 或者券商导出的都行关键是字段名对齐否则读进来就是一堆空值KNN 距离计算直接变成 NaN 传播。import pandas as pd def load_kline(path: str) - pd.DataFrame: df pd.read_csv(path, parse_dates[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) # 派生特征收益率、量比、均线偏离度 df[ret] df[close].pct_change() df[vol_ratio] df[volume] / df[volume].rolling(5).mean() df[ma_gap] df[close] / df[close].rolling(5).mean() - 1 return df.dropna().reset_index(dropTrue)逻辑说明读入 CSV 后先按日期升序排序并重置索引这一步是后面所有逻辑的地基——KNN 样本的顺序就是时间顺序乱序等于把未来数据混进历史。派生特征用收益率而不是收盘价是为了消掉价格序列的趋势项KNN 距离计算最怕非平稳数据vol_ratio 用当日成交量除以 5 日均量反映量能异动ma_gap 表示收盘价相对 5 日均线的偏离度用来捕捉超买超卖位置。参数说明rolling(5) 是 5 日均值窗口波动大的标的可以换成 10窗口太短噪声大、太长反应慢这个和后面样本窗口 WINDOW 是两回事。需要注意 pct_change() 产生的第一行是 NaNrolling 均值的前面几行同样是 NaN所以最后统一 dropna 丢掉无特征段。dropna 的同时也顺带清理了停牌复牌产生的重复收盘价行但前提是加载时已经把停牌日正确标记出来否则会把有效样本误删这个坑第五章详细讲。3.3 滑动窗口把时间序列变成特征矩阵单日三个特征信息量不够KNN 需要把过去 W 天的特征拼成一个向量一个向量就是一个样本。这也是用历史相似形态预测未来在代码里的直接体现。WINDOW 10 # 每个样本由过去 10 个交易日的特征组成 FEATURES [ret, vol_ratio, ma_gap] def build_samples(df: pd.DataFrame, window: int WINDOW): X, y, dates [], [], [] for i in range(window, len(df) - 1): vec df[FEATURES].iloc[i - window:i].values.flatten() X.append(vec) # 标签下一交易日收盘价是否高于当天 y.append(1 if df[close].iloc[i 1] df[close].iloc[i] else 0) dates.append(df[trade_date].iloc[i]) return np.array(X), np.array(y), dates逻辑说明外层循环从第 window 行走到倒数第二行每个位置 i 取它之前 10 天的三维特征展平成 30 维向量10 天 × 3 个特征标签取第 i1 天的实际涨跌。dates 数组同步记录每个样本对应的交易日后面画时间序列图、按日期核对预测结果都靠它。参数说明WINDOW10 是经验值窗口越短越贴近近期走势、越容易被单日噪声带偏窗口越长包含的趋势信息越完整、但样本数越少每多一天窗口就少一个样本。如果觉得 30 维信息不够可以加到 15 或 20 天但特征维度超过 50 之后 KNN 预测速度会明显下降需要配合 PCA这里先不展开。到这里数据管线就齐了load_kline 读原始 CSV 并派生特征build_samples 把时间序列切成样本矩阵、标签和日期索引。下一步是归一化、切分、训练和滚动回测那是第四章的内容。3.4 跑模型前的数据体检三行代码排掉明显问题数据问题在 KNN 里会被距离计算无限放大因为一个脏样本就会成为某个预测的最近邻居。我在每次建模前都会做三道体检空值比例、停牌重复样本、日期断档。def inspect(df: pd.DataFrame): print(空值比例:\n, df[[ret, vol_ratio, ma_gap]].isna().mean()) dup df[close].duplicated(keepFalse).sum() print(疑似停牌重复收盘价条数:, dup) gap df[trade_date].diff().dt.days.gt(3).sum() print(相隔超过 3 天的日期断档数:, gap)逻辑说明第一行输出各特征的空值占比超过 2% 就要回头查数据源第二行统计重复收盘价条数连续多天收盘价相同一般是停牌或数据商填充这样的重复样本会成为距离计算里的克隆邻居把投票带偏第三行统计日期断档长假期隔 3 天属正常非节假日断档说明数据缺段。参数说明diff().dt.days 计算的是相邻日期间隔天数周末加法定假日导致 3 天间隔是正常的所以用 gt(3) 只统计异常断档。这三项体检通过后再进 build_samples能省掉后面大量排查时间。4. 核心预测流程归一化、KNN 训练与滚动回测4.1 先切分再归一化时序数据泄漏的第一道防线随机切分的 train_test_split 在时序数据上不能用——训练集里会出现比测试集更晚的日期等于拿未来预测过去回测胜率虚高到 80% 都不奇怪。正确做法是按时间顺序切分前面的是训练集、后面的是测试集并且归一化器只在训练集上 fit。from sklearn.preprocessing import MinMaxScaler def split_and_scale(X, y, test_ratio0.2): cut int(len(X) * (1 - test_ratio)) X_train, X_test X[:cut], X[cut:] y_train, y_test y[:cut], y[cut:] scaler MinMaxScaler() X_train scaler.fit_transform(X_train) # 只在训练集上拟合 X_test scaler.transform(X_test) # 测试集只做变换 return X_train, X_test, y_train, y_test逻辑说明cut 是训练集和测试集的时间分界点前 80% 训练、后 20% 测试。scaler.fit_transform 和 transform 分开是硬性要求——如果先用全量数据 fit 再切分测试集的均值和极值已经混进归一化参数测试集就不再是没见过的数据回测成绩会系统性虚高。参数说明MinMaxScaler 把特征映射到 [0,1] 区间和欧氏距离天然匹配如果特征里有明显长尾分布换成 StandardScaler 更稳它按均值方差标准化对量纲差异的处理思路不同但目标一致。4.2 训练 KNN 并输出概率from sklearn.neighbors import KNeighborsClassifier knn KNeighborsClassifier(n_neighbors7, weightsdistance, metriceuclidean) knn.fit(X_train, y_train) pred knn.predict(X_test) # 0/1 方向 prob knn.predict_proba(X_test)[:, 1] # 上涨概率逻辑说明n_neighbors7 表示取最近 7 个邻居投票weightsdistance 让更近的邻居拥有更高投票权重metriceuclidean 配合前面 MinMax 归一化使用。predict 输出 0/1 方向predict_proba 输出上涨概率——后面做信号过滤时用的是概率而不是方向这个区别很重要。参数说明这三个参数只是起点值最终应该在滚动回测里用网格搜一遍第六章给出具体搜法这里先记住概率比方向值钱。4.3 滚动回测模拟每周重训一次的真实节奏固定训练集回测还有一个隐蔽问题市场风格会变半年前的样本对今天的预测价值很低。合格的做法是滚动回测——每隔一段交易日用最近 N 天的样本重新训练一次再去预测接下来的 M 天。def walk_forward(df, window60, step5, k7): X, y, dates build_samples(df) preds, probs, y_true [], [], [] for start in range(0, len(X) - window - step, step): train_slice slice(start, start window) test_slice slice(start window, start window step) # 每个窗口内部独立归一化杜绝跨窗口泄漏 scaler MinMaxScaler() X_train scaler.fit_transform(X[train_slice]) X_test scaler.transform(X[test_slice]) # 每个窗口重新训练一个 KNN model KNeighborsClassifier(n_neighborsk, weightsdistance) model.fit(X_train, y[train_slice]) preds.extend(model.predict(X_test)) probs.extend(model.predict_proba(X_test)[:, 1]) y_true.extend(y[test_slice]) return np.array(preds), np.array(probs), np.array(y_true)逻辑说明外层循环从样本头部开始每次取最近 60 个交易日当训练集预测随后 5 天然后整个窗口前移 5 天再重复。注意每个窗口内部都重新创建 scaler 和 model这是刻意为之——模拟的是每周五收盘后重新训练一次的真实操作节奏训练数据永远是最近一段模型永远面向下一周。y_true 数组同步收集测试段真实标签后面算指标直接用。参数说明window60 是训练窗口长度太短样本不够、太长模型反应慢step5 是重训练频率日线数据一周调一次是折中选择k 是邻居数先传 7 跑通全流程再调。回测跑完别急着看胜率先把预测结果和 dates 对回真实交易日算一条累计收益曲线——每笔预测按次日真实涨跌计入盈亏再扣掉双边手续费和滑点。手续费按万二点五、滑点按一跳来估就行目的是确认胜率优势没有被交易成本吃掉。KNN 的高胜率往往集中在少数高置信度信号上小仓位多频次交易反而会把成本吃光这一点很多人会忽略。4.4 评估指标别只看准确率KNN 二分类的准确率在股市里有个陷阱如果测试期恰好是上涨行情预测全涨的准确率也能有 60%但没有任何交易价值。所以要同时看三个指标整体准确率、上涨日召回率、以及信号过滤后的条件胜率。acc (pred y_test_all).mean() up_recall ((pred 1) (y_test_all 1)).sum() / (y_test_all 1).sum() high_conf prob 0.6 cond_hit (high_conf (y_test_all 1)).sum() / high_conf.sum()逻辑说明acc 是全局准确率只能当参考up_recall 看模型对上涨日的敏感度避免模型变成永远看跌的懒鬼cond_hit 是只统计高置信度信号里的命中率这个值才是策略真实能用的胜率。y_test_all 就是上一节 walk_forward 返回的 y_true。参数说明0.6 是置信度阈值可以替换成 0.55~0.7 做敏感性测试。这三个指标一起看才能判断模型是真的学到了形态还是恰好撞上了行情方向。5. 避坑记录五个让预测结果失真的细节下面这五条是我照着这类源码复现时踩过的真坑按从最常见到最阴间的顺序排每一条都按现象 → 原因 → 解决讲清楚。5.1 回测胜率虚高到 80%实盘却完全失灵现象跑完完整流程回测准确率漂亮得吓人一上模拟盘就原形毕露。原因八成是数据泄漏。最常见的是归一化器在包含测试集的全量数据上 fit测试集的分布信息提前进了模型其次是 build_samples 之前日期没排序样本里混入了未来数据。解决切分在前、归一化在后scaler 只 fit 训练集跑完回测后抽出预测结果对应的 dates 列表打印前三条人工核对日期顺序和切分边界是否对得上。这条检查我每次必做因为它不会报错只会安静地骗你。5.2 换个标的之后模型彻底失效现象同一套代码、同一组参数换一只股票回测胜率从 58% 掉到 51%跟随机猜差不多。原因K 值、WINDOW、特征组合都是跟着原标的数据特性调的。不同标的的波动率、换手率、趋势持续性差异很大固定参数必然顾此失彼。解决每个标的单独跑一遍参数网格别指望一套参数通吃。把 K 和 WINDOW 的搜索范围写进配置换标的只改配置文件、不动主逻辑。这也是我把 walk_forward 写成独立函数的原因参数化之后换标的的成本就是改一行调用。5.3 预测结果清一色看涨现象pred 数组里几乎全是 1上涨日准确率很高下跌日几乎全错。原因样本不平衡。慢牛行情的数据里上涨样本占比天然超过 60%KNN 投票向多数类倾斜把下跌日也判成上涨。解决不要只看整体准确率单独算下跌日召回率必要时候用 class_weightbalanced 加权或者对多数类降采样。更要认清一个事实模型学到的可能是市场长期趋势而不是形态与次日涨跌的稳定关系后者才是 KNN 该学的东西。5.4 加了更多指标效果反而变差现象RSI、MACD、布林带全部加进 FEATURES准确率不升反降预测还变慢了。原因维度灾难。KNN 在高维空间里距离趋同最近邻居和第十近的邻居差不了多少投票趋于随机计算量却线性上涨。解决特征控制在 5 个以内先用收益率、量比、均线偏离这类强相关特征真想加指标先跑一遍特征相关性分析去掉冗余项必要时用 PCA 压到 10 维以内再喂给 KNN。记住一个原则KNN 是懒模型特征质量决定它的上限数量只会稀释它的下限。5.5 解压乱码、CSV 读出来全是空值现象zip 解压后 README 文件名是乱码CSV 读进来字段对不上程序直接报 KeyError 或者全是 NaN。原因zip 包在 Windows 下用默认编码压缩Linux/macOS 解压时中文文件名和中文表头出现编码错位另外很多 CSV 是 GBK 编码而不是 UTF-8。解决Linux 下用 unzip -O GBK 包名.zip 解压pandas 读 CSV 时指定 encodinggbk保险的做法是先把数据列全部重命名为英文代码里只认英文列名跟编码彻底解耦。还有一个常见坑是所谓的zip 伪加密——下载页明确说没密码unzip 却提示要密码这是 zip 标志位被置位导致的假象用 Python 的 zipfile 把文件头偏移 6 处的加密标志位清零就能正常解压不用找什么密码工具。6. 让结果可信用概率阈值和滚动参数网格收尾把概率当置信度用是 KNN 股市预测里性价比最高的一步。K10 时 8 个邻居看涨上涨概率 0.8这种信号的历史一致性远高于 P0.52 的擦边球。实战中只对高置信度信号下注是 KNN 从模型成绩变成可执行策略的关键一步。signals prob 0.6 # 只保留上涨概率超过 60% 的预测参数说明0.6 是保守阈值降到 0.55 信号变多但胜率被稀释升到 0.7 信号变少但单笔更稳。阈值怎么定取决于你对信号密度和单笔胜率的取舍没有绝对正确答案。然后是参数网格。不要在固定训练集上选 K要在滚动回测里选best_k, best_hit 0, 0 for k in range(3, 21, 2): preds, probs, y_true walk_forward(df, window60, step5, kk) hit (preds y_true).mean() print(fK{k}, 准确率{hit:.3f}) if hit best_hit: best_k, best_hit k, hit参数说明range(3, 21, 2) 只扫奇数 K直接回避平票问题window 也可以放进外层循环一起扫找到组合最优再跑一次完整回测确认。记录每个 K 下的信号数量和条件胜率K 太大时信号会扎堆在看涨一侧这时候优先选信号分布更均衡的 K而不是准确率最高的 K。说句实在话KNN 日线方向预测的胜率天花板就在 55%~60% 之间超过这个数字先怀疑数据泄漏别急着怀疑自己。从那以后我每次跑这类模型都强制走一遍三件事先切分再缩放、按时间滚动重训、用概率阈值过滤信号哪怕只是个练手项目这三步也能保证回测结果拿去跟人讨论时站得住脚。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →