简介:Python 基于 kNN 算法实现股市预测的完整工程,适合有一定 Python 基础的金融数据分析爱好者、量化入门者及对股票预测感兴趣的开发者参考。压缩包体积仅 2KB,共包含 2 个文件,分别是核心 Python 脚本和 Markdown 使用说明,结构精简,便于快速定位代码与说明内容。代码借助 pandas、tushare、numpy、scipy、fastdtw 等库完成历史行情数据的获取、清洗与时间序列距离计算,用户在交互式命令行中调用 main 函数,传入股票代码与 k 参数即可运行,还可设置 ktype='W' 实施以周为单位的预测。资源特别指出 k 值变化会显著影响准确率,建议多次调整对比后确定最高准确率下的结果,帮助读者理解 kNN 参数调优、数据预处理与预测结果评估的完整思路。目前已有 121 人学习下载,适合用作理解经典机器学习算法在金融时间序列预测中实际落地的小型示例项目。
1. KNN 做股市预测:先别急着跑代码,你得知道它在赌什么
拿到「Python基于knn算法实现的股市预测源代码+使用说明」,大多数人第一反应是跑 main.py,看到"准确率 78%"就以为找到印钞机。我劝你先冷静。KNN 做股市预测的逻辑很朴素:把今天的行情状态(涨跌幅、量比、波动率)跟历史每天比对,找出最像的 K 天,投票决定之后是涨是跌——它不推理,只匹配。这类包的使用说明通常把流程分成环境准备、数据清洗、特征构造、模型训练、回测五步。它适合刚入门量化、想用 Python 把 K 线数据到预测信号串成闭环的人。整份代码最值钱的不是模型,是数据和特征,这两步错了,后面全是白干。
2. 从行情数据到 KNN 特征:把 K 线转成监督学习样本
2.1 数据源选择与清洗:免费接口也有坑
KNN 是监督学习模型,第一步是拿到历史行情并清洗成"一行样本一个交易日"的表。常见做法是用 akshare 或 tushare 两个免费 Python 库拉数据。akshare 零门槛,pip 装完就能用,不需要注册 token;tushare 要注册拿 token,但数据字段更规整。我一般先用 akshare 跑通流程,确认特征有效后再切换到 tushare 验证一遍,避免单数据源的脏数据带偏结论。注意代码环境里要先pip install akshare pandas scikit-learn,VSCode 里把解释器指到对应的虚拟环境,这是使用说明里第一步要确认的事。
import akshare as ak import pandas as pd # 拉取平安银行最近 5 年日线,前复权(qfq) df = ak.stock_zh_a_hist( symbol="000001", period="daily", start_date="20190101", end_date="20241231", adjust="qfq" # 前复权,避免除权除息跳空干扰距离计算 ) # akshare 返回的列名是中文,统一改成英文方便后面处理 df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }) df = df[["date", "open", "close", "high", "low", "volume"]] df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # 严格按时间升序 print(df.head()) print(df.tail())这段代码有三个隐蔽的坑。第一,adjust="qfq"不能省,否则除权除息当天 K 线会跳空,KNN 算距离时会把复权缺口当成真实价格波动,生成一堆"假相似"的样本。第二,akshare 接口偶尔会因为上游数据源改版抛异常,生产环境里建议对请求包一层 try/except 加重试。第三,返回后必须按日期升序排序,后续构造时间窗口特征依赖行顺序,顺序错乱等于特征全错,这是最容易被忽略的一步。
提示:akshare 的股票接口字段偶尔会调整,跑之前先 print(df.head()) 确认列名,不要盲信文档。
2.2 特征窗口与标签构造:预测涨跌而不是预测价格
清洗完行情,就该构造 KNN 真正吃的"样本"了。很多新手直接用当天收盘价当特征、明天收盘价当标签,这是最典型的翻车姿势。价格是非平稳序列,十年前 5 块的票和现在 50 块的票,绝对价格差了 10 倍,KNN 的欧氏距离里它们永远不会是"近邻",模型等于瞎猜。正确做法是把价格转成收益率、波动率、量比这类平稳特征,再把预测目标定义成"方向"而不是"数值"。
import numpy as np def make_features_and_label(df, window=10, horizon=1): df = df.copy() df["ret"] = df["close"].pct_change() # 当日收益率 df["ret_ma"] = df["ret"].rolling(window).mean() # 窗口内平均收益 df["volatility"] = df["ret"].rolling(window).std() # 窗口内波动率 df["volume_ratio"] = df["volume"] / df["volume"].rolling(window).mean() # 量比 df["close_ma_ratio"] = df["close"] / df["close"].rolling(window).mean() - 1 # 价格偏离均线程度 # 标签:horizon 天后涨跌方向,涨为 1 跌为 0 df["future_ret"] = df["close"].shift(-horizon) / df["close"] - 1 df["label"] = (df["future_ret"] > 0).astype(int) feat_cols = ["ret", "ret_ma", "volatility", "volume_ratio", "close_ma_ratio"] df = df.dropna() # 去掉因 rolling 和 shift 产生的空行,同时也自然删掉了最后 horizon 行 return df[feat_cols].reset_index(drop=True), df["label"].reset_index(drop=True), df["date"].reset_index(drop=True) X, y, dates = make_features_and_label(df, window=10, horizon=1) print("样本数:", len(X), "上涨样本占比: {:.2f}".format(y.mean()))这里最关键的设计是"标签取方向、不取价格"。原因有二:其一,KNN 的投票机制天然是分类器,K 个近邻中涨的多就说涨,硬拿它做回归会输出一个"历史均价",毫无交易意义;其二,方向预测对噪声的容忍度高,价格预测差 0.5% 就算错,方向预测只要没踏空就算对。window=10表示看过去 10 个交易日的状态,horizon=1表示预测下一个交易日。y.mean()这行一定要看,如果上涨样本占比明显偏离 0.5,后面第 5 章的类别不平衡坑就要提前防。
2.3 训练集与测试集切分:时间序列不能随机打乱
特征造好之后,紧接着就是切分。这一步翻车的人比想象中多。sklearn 的 train_test_split 默认 shuffle=True,会把 2020 年和 2024 年的样本随机混进训练集和测试集。对 KNN 这种"延迟计算"的模型,它预测某个交易日时会就近去找训练集里的样本,如果"未来样本"混进了训练集,预测 2023 年时 2024 年的样本就成了它的近邻——这等于直接偷看答案。所以时间序列必须按顺序切,不能打乱。
# 按日期切分:训练集是 2023 年之前的历史,测试集是 2023 年开始的"未来" train_mask = dates < "2023-01-01" X_train, y_train = X[train_mask], y[train_mask] X_test, y_test = X[~train_mask], y[~train_mask] print("训练样本:", X_train.shape, "测试样本:", X_test.shape)按时间切分后,训练集永远是测试集之前的真实历史,模型的"近邻"不可能来自未来。量化圈管这个叫前推验证,实盘里你每天做的事本质就是"拿截至昨天的数据预测明天",所以回测必须复现这个过程。这里按日期硬切只算起点,第 4 章会用 TimeSeriesSplit 做更严格的滚动验证。
3. 实现 KNN 预测器:sklearn 最小可运行代码与三个必调参数
3.1 最小可运行代码:训练、预测、回测一条链
环境和依赖按这个顺序装:先装 Python 官方安装包,再在终端里pip install pandas scikit-learn akshare,最后用 VSCode 打开项目目录并选中虚拟环境解释器。版本不用刻意追新,scikit-learn 的稳定版就够。装好之后,接上一节的 X_train、y_train、X_test、y_test,KNN 的最小训练代码就是下面这一段:
from sklearn.preprocessing import StandardScaler from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score # 标准化:KNN 距离计算对量纲敏感,必须先把特征压到同一尺度 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) knn = KNeighborsClassifier( n_neighbors=10, # 看最近的 10 个历史交易日 weights="distance", # 近邻投票时按距离加权 p=2, # 欧氏距离 metric="minkowski" # minkowski 是 sklearn 里 p 参数的统一入口 ) knn.fit(X_train_scaled, y_train) y_pred = knn.predict(X_test_scaled) print("测试集准确率: {:.2f}".format(accuracy_score(y_test, y_pred)))这段代码里最容易被忽略的是 scaler 的 fit/transform 顺序:在训练集上fit_transform,在测试集上只能transform。如果对全量数据先标准化再切分,scaler 的均值和方差里就混进了测试集的信息,回测分数会虚高,属于教科书级的温和数据泄漏。weights="distance"表示近邻投票按距离加权,越近的历史状态话语权越大;默认的 uniform 是等权投票,在高噪声的股票数据里,等权容易让"远房亲戚"和"今天最像的几天"平起平坐,所以我默认就用 distance。K 值先给 10,是经验起跳值,后面网格搜索会取代它。
注意:股票数据里有涨跌停当天成交量冻结这类极端样本,StandardScaler 会被极端值拉偏。如果回测结果对单个样本特别敏感,把 scaler 换成 RobustScaler,它用中位数和四分位距,抗异常值能力更强。
3.2 n_neighbors、weights、p 三个必调参数
KNN 在股市预测场景,值得花时间调的参数就三个,其他保持默认。下面这个表是我按经验总结的速查:
| 参数 | 常见取值范围 | 作用 | 翻车信号 |
|---|---|---|---|
| n_neighbors | 5~50,建议从 10 起跳 | 参与投票的历史状态数量 | 训练集 90%、测试集 50%,基本是 K 太小 |
| weights | uniform / distance | 是否按距离加权投票 | 高噪声数据上 uniform 的假阳性偏多 |
| p 与 metric | p=1 曼哈顿,p=2 欧氏 | 决定"相似"到底怎么算 | 特征里有少量极端值时 p=2 过于敏感 |
n_neighbors 是最关键的旋钮。K=1 时模型会"记住"训练集里每一个样本,训练准确率逼近 100%,但测试集立刻被打回原形,这是 KNN 独有的过拟合形态——K 太小记噪声。K 拉到 50 甚至 100,投票被大量平庸历史淹没,预测分布会向涨跌先验比例收敛,模型退化成"永远猜涨"。A 股日线上涨天数本来就略多于下跌天数,所以 K 过大时准确率会稳定在一个虚高的基线附近,看起来不错,但没有信息量。
p 参数决定距离度量方式。p=2 是欧氏距离,对离群点敏感;p=1 曼哈顿距离对各维度的异常值更钝感。特征维度在个位数时两者差别不大,如果数据里有偶发异常成交量,p=1 往往更稳。我的习惯是先用 p=2 跑基线,再换 p=1 对比,哪一个在测试集上更稳就留哪一个。
3.3 用 predict_proba 拿置信度:把 0/1 输出变成可过滤的信号
predict 只给你 0 或 1,浪费了 KNN 投票过程中最有价值的信息——投票的悬殊程度。K=10 时,10 票全涨和 6:4 险胜都输出"预测涨",但前者值得重仓,后者应该观望。用 predict_proba 可以拿到这个信息,这是使用说明里通常不会细讲、但实战价值很高的一步:
proba = knn.predict_proba(X_test_scaled) up_prob = proba[:, 1] # 上涨概率(正类列) # 只在上涨概率 >= 0.7 时产生交易信号 signal = up_prob >= 0.7 print("高置信度信号占比: {:.2f}".format(signal.mean())) # 对比高低置信度两组的命中率 high_hit = accuracy_score(y_test[signal], y_pred[signal]) if signal.sum() > 0 else 0 low_hit = accuracy_score(y_test[~signal], y_pred[~signal]) if (~signal).sum() > 0 else 0 print("高置信度命中率: {:.2f},低置信度命中率: {:.2f}".format(high_hit, low_hit))这个置信度阈值是把 KNN 从"能跑"变成"能用"的关键:不做预测时收益是 0,做错时是负收益,如果高置信度区间的命中率显著高于低置信度区间,加阈值过滤就是免费的超额收益。注意 KNN 的 predict_proba 本质是 K 个近邻里正类的占比,不是严格校准过的概率,但拿它当排序信号完全够用。第 6 章的滚动训练里,up_prob 还会继续充当策略的输入。
4. 回测评估与参数搜索:准确率超过 55% 才值得继续
4.1 用混淆矩阵看预测偏差:别被准确率骗了
单看准确率容易被骗,原因很简单:A 股日线级别"明天涨"的随机概率大约在 52%~55%,一个什么都不学的模型只要永远输出"涨"就能拿到这个准确率。如果 KNN 报出来 70%,先别高兴,把混淆矩阵打出来看看是不是"预测全是涨"。测试集准确率如果连 55% 都不到,说明特征里确实没有可预测的信息,先回头改数据,别急着调 K——这个决心越早下,浪费的时间越少。
from sklearn.metrics import confusion_matrix, classification_report cm = confusion_matrix(y_test, y_pred) print("混淆矩阵:\n", cm) # classification_report 一行给出精确率、召回率、F1 print(classification_report(y_test, y_pred, target_names=["跌", "涨"]))混淆矩阵四象限在股票场景下含义不同:真正例是盈利来源,假正例是亏损来源,假负例是踏空,真负例是躲过的下跌。对做多策略来说,假正例的代价是一笔真实亏损,假负例的代价只是少赚,所以好的模型应该优先压低假正例率,宁可错过也不做错——这正是 3.3 置信度阈值存在的意义。同样重要的还有predict结果里"涨"的占比,如果超过 60%,模型大概率在贴先验而不是在学特征。
4.2 网格搜索找参数:时间序列上别用默认交叉验证
手动试 k=10、15、20 效率太低,正确做法是网格搜索。但注意,sklearn 的 GridSearchCV 默认用 KFold 做交叉验证,这在股票数据上是错的:KFold 会把第 1 折的数据拿去训练、第 5 折的数据拿去验证,验证集在时间上早于训练集,模型等于先看未来再猜过去。正确姿势是 TimeSeriesSplit,它保证验证集永远在训练集之后。另外我把 StandardScaler 和 KNN 一起塞进 pipeline,这样每一折内部都会重新算 scaler,从根上杜绝标准化的数据泄漏。
from sklearn.model_selection import GridSearchCV, TimeSeriesSplit from sklearn.pipeline import make_pipeline # TimeSeriesSplit 每一折:训练集逐渐扩窗,验证集是紧邻其后的时间点 tscv = TimeSeriesSplit(n_splits=5) param_grid = { "kneighborsclassifier__n_neighbors": [5, 10, 15, 20, 30], "kneighborsclassifier__weights": ["uniform", "distance"], "kneighborsclassifier__p": [1, 2], } pipe = make_pipeline(StandardScaler(), KNeighborsClassifier()) search = GridSearchCV(pipe, param_grid, cv=tscv, scoring="accuracy", n_jobs=-1) search.fit(X, y) print("最优参数:", search.best_params_) print("最优得分: {:.2f}".format(search.best_score_))GridSearchCV 在 pipeline 上的参数名是"组件名__参数名",所以要用kneighborsclassifier__n_neighbors这种写法,直接写 n_neighbors 会报参数错误。n_jobs=-1让全部 CPU 核并行,这个网格 5x2x2 共 20 组、每组 5 折,也就是 100 次模型拟合,单机一两分钟就能跑完。网格搜索的意义不止于找最优参数,它还能告诉你一个残酷事实:如果所有参数组合的得分都在 52%~56% 之间徘徊,说明这组特征在这个股票上没有预测力,换标的或者换特征才是正路。
4.3 评估指标的选择:准确率、F1 与方向命中率的取舍
如果要选一个指标当网格搜索的目标,我推荐 F1 而不是准确率,原因就是 4.1 里的先验概率问题。F1 是精确率和召回率的调和平均,它会同时惩罚"预测了很多涨但命中率低"和"漏掉了所有上涨"的模型。实战里我还会额外算"方向命中率"——把测试集的预测按 up_prob 从高到低排序,取前 30% 看命中率。这个数字直接对应"如果只交易最有把握的几天,胜率是多少",比全局准确率更贴近实盘决策。
| 指标 | 计算方式 | 用在哪 |
|---|---|---|
| 准确率 | (TP+TN)/(P+N) | 一眼看基线,不作决策依据 |
| 精确率 | TP/(TP+FP) | 控制做多策略的亏损笔数 |
| 召回率 | TP/(TP+FN) | 衡量会不会漏掉大行情 |
| F1 | 2PR/(P+R) | 综合判断,推荐作为网格搜索目标 |
5. KNN 股市预测避坑指南:5 个翻车场景与解决方案
5.1 数据泄漏:回测漂亮,实盘拉胯
现象:训练集准确率 99%,测试集准确率也高达 90%,一上实盘亏得亲妈都不认识。 原因:特征里混入了未来数据,KNN 直接"抄答案"。常见来源有三个:一是复权数据没处理干净,前复权因子用了包含当天及之后除权信息的数据;二是 shift(-1) 的标签和特征没有正确对齐,某一行把"今天的特征"和"明天的收益"错位拼接;三是 scaler 或缺失值填充用了全量数据的统计量。 解决:检查 pipeline 顺序——scaler 必须在训练集上 fit,缺失值填充的均值/中位数也必须只从训练集算。有个笨办法可以验证:把测试集日期往后整体平移 10 天再预测,如果准确率大幅下降后又回升,说明模型在依赖时间邻近性而不是真实规律。
5.2 特征尺度不一致:距离被"大数"绑架
现象:加了一个新特征(比如成交量),回测结果骤变,有的特征加上去反而掉分。 原因:KNN 的距离是所有特征维度的加权和,量纲大的特征会吞掉量纲小的特征。比如 close_ma_ratio 是 0.01 量级,volume_ratio 是 1.0 量级,不做标准化的话,volume_ratio 对距离的贡献是前者的 100 倍,模型等于只看了量比这一个维度。 解决:对每个特征做 Z-score 标准化(StandardScaler),或者对有明显厚尾的特征做 RobustScaler。成交量这类长尾分布特征,建议先取 log 再标准化,不然几个异常放量日会直接拉偏整个特征维度的尺度。
5.3 类别不平衡:模型学会"永远看涨"
现象:混淆矩阵里预测几乎全是"涨",震荡市里频繁做多被反复打脸,准确率还维持在 55% 左右。 原因:A 股日线上涨天数天然多于下跌天数(大约 53:47),当 K 取较大值时,投票结果被先验概率主导,模型退化为"猜涨",KNN 的近邻搜索本身没问题,问题是决策边界被先验带偏了。 解决:两个方向。一是回调 K 值,小 K 让投票更依赖局部近邻,减弱先验影响;二是用class_weight="balanced"给少数类(下跌)加权重,KNeighborsClassifier 支持这个参数。我的经验是回测时同时打印"预测为涨的比例",如果这个比例超过 60%,就要警惕模型在贴先验而不是在学特征。
5.4 K 值选择的玄学:奇数不是金科玉律
现象:网上说"K 必须选奇数避免平票",于是只试 5、7、9、11,结果忽高忽低,换了一只股票结论全变。 原因:奇数规则本身没毛病,但股票预测的平票问题根本不是 K 值奇偶能解决的——你用了 weights="distance" 或 predict_proba,连续权重下不会出现严格平票。真正影响结果的是 K 的绝对大小和它跟波动率的关系:市场波动大的年份,K 大一点更稳;震荡市里 K 小一点更敏感。 解决:别手动选奇偶,交给 4.2 的网格搜索。如果一定要经验值:日线数据、5~8 个特征、样本量 1000 以上时,K 在 10~20 之间最常见最优,K<5 基本是过拟合重灾区。
5.5 复权与停牌:看不见的"未来函数"和脏样本
现象:某只股票历史回测年化 80%,细看收益集中在除权除息那几天,或者某次停牌复牌后信号全部异常。 原因:用了未复权数据,除权日 K 线跳空被 KNN 当成"暴跌然后暴涨"的相似模式,恰好这类模式在历史上确实跟着上涨,模型等于在赌"除权后会填权",这不是你想要的预测信号。另一个隐藏问题是停牌:长期停牌复牌后第一天,pct_change 会算出一个极端涨跌幅,因为停牌期间的累计变动被集中到一天释放,这种样本是特征空间的离群点。 解决:数据源统一用前复权;停牌样本直接过滤。akshare 拉日线时已做复权,但要过滤退市整理期和长时间停牌的数据。我一般会加df = df[df["volume"] > 0]过滤停牌日,再对 ret 做 1% 和 99% 分位的缩尾处理,压掉极端值对距离计算的干扰。
6. 让 KNN 预测更接近实盘:滚动训练与特征工程进阶
到这里链路已经能跑,但离"能实盘"还差两步:滚动训练和特征工程。KNN 是懒惰学习模型,它不更新权重,把所有训练样本存下来现场算距离,所以训练集越陈旧,匹配越失真——2015 年的暴涨暴跌和 2023 年的震荡市,特征形态完全不同。常见做法是滑动窗口:每次只用最近 500 个交易日样本,每 20 天重训一次。核心代码就是把第 3 章的训练逻辑包成循环:
def rolling_predict(X, y, window=500, step=20): preds, confs = [], [] for end in range(window, len(X), step): start = end - window sc = StandardScaler().fit(X.iloc[start:end]) knn = KNeighborsClassifier(10, weights="distance") knn.fit(sc.transform(X.iloc[start:end]), y.iloc[start:end]) proba = knn.predict_proba(sc.transform(X.iloc[end:end+step]))[:, 1] preds.extend(proba >= 0.5) confs.extend(proba) return preds, confs特征工程我给 KNN 补三样:MACD 的 DIF 值、RSI 14 日值这类技术指标,帮模型区分趋势形态;个股相对大盘的超额收益,避免大盘系统性上涨时模型无脑看多;星期几和月份这类时间特征,A 股确实有周内效应。这些特征加进 2.2 的 feat_cols 重新跑一遍即可,KNN 不需要重新训练,这也是懒惰模型好调试的一面。
收尾说一个我的习惯:每次改完特征或参数,我不只看整体准确率,而是把预测按季度切片逐段看胜率。如果某两个季度胜率差超过 15 个百分点,说明模型只在特定市场状态下有效,实盘就得给它加"状态开关"。KNN 做股市预测的天花板从来不是算法,是你对数据边界和市场状态的敬畏。希望帮到你,也祝你少交点学费。
本文还有配套的精品资源,点击获取