数据科学与机器学习(第 09 部分):K-最近邻算法(KNN)·综合运用
KNN 回测里 k 值怎么挑
在 EURUSD 的 D1 周期上跑最近邻分类器,核心就是遍历不同的近邻数 k,看哪一组样本外准确率更高。日志里能看到程序从 k=24 一路打印到 k=32,每一档都给出 TrainTest 后的识别准确率。 具体数字有点反直觉:k=24 时准确率 46.67%,k=26 掉到 40.0%,k=28 又弹回 56.67%,直到 k=30 到 k=31 摸到 60.0%,k=32 又回落到 56.67%。外汇日线这种噪声多的品种,k 太小容易过拟合、太大容易 smoothing 掉结构,准确率随 k 波动十几点是常态。 下面这段是驱动遍历的骨架代码,每轮重建对象并释放,避免内存堆积。 遍历逻辑提醒一句:k 从 0 跑到 bars 会非常慢,实盘前建议先限定一个合理区间(比如 5~50)再打印,否则 MT5 日志会被刷爆。EURUSD 日线属高风险品种,回测准确率不构成任何方向暗示,只说明模型在该参数下的拟合倾向。
nearest_neigbors = new CKNNNearestNeighbors(Matrix,k); nearest_neigbors.TrainTest(); for(class="type">uint i=class="num">0; i<bars; i++) { printf("<<< k %d >>>",i); nearest_neigbors = new CKNNNearestNeighbors(Matrix,i); nearest_neigbors.TrainTest(); class="kw">delete(nearest_neigbors); }
◍ KNN 邻数扫描里的准确率拐点
在 EURUSD 的 D1 周期上跑最近邻模型时,引擎会把 k 值从 33 一路扫到 47,每换一个邻数就吐一行回测准确率。日志里 k=33 时准确率约 56.67%,k=34、35 直接掉到 50.0%,说明这一带模型基本在随机猜。 k 推到 42~45 出现一段抬升:k=42 命中 63.33%,k=44 到 66.67%,是整段扫描里最像样的区间。但 k=46 又回落到 56.67%,证明邻数不是越大越稳,EURUSD 日线在这个样本窗里对 k 值极敏感。 外汇和贵金属都是高杠杆高风险品种,这类历史命中率只反映过去样本,实盘可能明显偏离。开 MT5 把这段扫描接上自己的品种,重点看 k=42~45 附近是否复现,再决定要不要拿去当过滤条件。
「把 KNN 信号接进开平仓执行」
这段逻辑干的事很直接:每根新 K 线出现时,把当前 ATR 与成交量塞进特征向量,丢给 KNN 分类器拿信号,再决定多空与反手。特征向量 x_vars 长度为 2,分别对应 atr_val[0] 与 volume_val[0],也就是最新一根完成的 K 线上的波动与量能读数。 代码里用 isNewBar() 卡住执行节奏,避免同一根 K 线内重复触发。signal==1 时优先检查是否已有 BUY 持仓,没有就按 tick.ask 市价买入,并尝试平掉 SELL;否则对称地做 SELL 并平 BUY。手数取 SymbolInfoDouble 的 SYMBOL_VOLUME_MIN,属于最小单位的试探仓,外汇与贵金属杠杆高,实盘需自行放大或约束风控。 下面这段是 EA 主体里的关键片段,逐行拆一遍就能在 MT5 里照抄验证: vector x_vars(2); // 声明长度2的特征向量,存 atr 与 volume double atr_val[], volume_val[]; // 两个缓冲区数组 CopyBuffer(atr_handle,0,0,1,atr_val); // 从ATR指标句柄拷贝最新1根值 CopyBuffer(volume_handle,0,0,1,volume_val); // 从成交量指标句柄拷贝最新1根值 x_vars[0] = atr_val[0]; // 特征1:当前ATR x_vars[1] = volume_val[0]; // 特征2:当前成交量 int signal = 0; // 初始化信号 double volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); // 最小交易手数 MqlTick ticks; // 报价结构体 SymbolInfoTick(Symbol(),ticks); // 取最新 tick double ask = ticks.ask, bid = ticks.bid; // 记录买卖价 if (isNewBar() == true) // 仅新K线执行 { signal = nearest_neigbors.KNNAlgorithm(x_vars); // 调KNN拿分类信号 if (signal == 1) { if (!CheckPosionType(POSITION_TYPE_BUY)) { m_trade.Buy(volume,Symbol(),ask,0,0); // 市价买 if (ClosePosType(POSITION_TYPE_SELL)) printf("Failed to close %s Err = %d",EnumToString(POSITION_TYPE_SELL),GetLastError()); } } else { if (!CheckPosionType(POSITION_TYPE_SELL)) { m_trade.Sell(volume,Symbol(),bid,0,0); // 市价卖 if (ClosePosType(POSITION_TYPE_BUY)) printf("Failed to close %s Err = %d",EnumToString(POSITION_TYPE_BUY),GetLastError()); } } } 注意末尾的 #include "KNN_nearest_neighbors.mqh" 与 CKNNNearestNeighbors *nearest_neigbors 声明,KNN 算法本体在外部 mqh 里。没把训练矩阵灌进 Matrix 并初始化 nearest_neigbors 之前,KNNAlgorithm 调用会直接悬空。
vector x_vars(class="num">2); class=class="str">"cmt">//vector to store atr and volumes values class="type">class="kw">double atr_val[], volume_val[]; CopyBuffer(atr_handle,class="num">0,class="num">0,class="num">1,atr_val); CopyBuffer(volume_handle,class="num">0,class="num">0,class="num">1,volume_val); x_vars[class="num">0] = atr_val[class="num">0]; x_vars[class="num">1] = volume_val[class="num">0]; class=class="str">"cmt">//--- class="type">int signal = class="num">0; class="type">class="kw">double volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); class="type">MqlTick ticks; SymbolInfoTick(Symbol(),ticks); class="type">class="kw">double ask = ticks.ask, bid = ticks.bid; if (isNewBar() == true) class=class="str">"cmt">//we are on the new candle { signal = nearest_neigbors.KNNAlgorithm(x_vars); class=class="str">"cmt">//Calling the algorithm if (signal == class="num">1) { if (!CheckPosionType(POSITION_TYPE_BUY)) { m_trade.Buy(volume,Symbol(),ask,class="num">0,class="num">0); if (ClosePosType(POSITION_TYPE_SELL)) printf("Failed to close %s Err = %d",EnumToString(POSITION_TYPE_SELL),GetLastError()); } } else { if (!CheckPosionType(POSITION_TYPE_SELL)) { m_trade.Sell(volume,Symbol(),bid,class="num">0,class="num">0); if (ClosePosType(POSITION_TYPE_BUY)) printf("Failed to close %s Err = %d",EnumToString(POSITION_TYPE_BUY),GetLastError()); } } } } class="macro">#include "KNN_nearest_neighbors.mqh"; CKNNNearestNeighbors *nearest_neigbors; matrix Matrix;
KNN 模型的初始化与逐根 K 线触发
把 KNN 分类器塞进 EA 生命周期,重点在 OnInit 里用 new 实例化 CKNNNearestNeighbors,传入已构建好的 Matrix 与近邻数 _k;OnDeinit 里必须 delete 掉指针,否则 MT5 跑久了会漏内存。 行情触发放在 OnTick,但真正算信号只在新 K 线成立时:isNewBar() 返回 true 才调用 KNNAlgorithm(x_vars),x_vars 是长度 2 的 vector,原文用来装 ATR 与成交量。这样能把回测耗时压到每根一根,而不是每 tick 重算。 实盘前你要在被忽略的两处补代码:OnInit 里把历史样本填进 Matrix,OnTick 里把实时指标写进 x_vars。缺了任一步,nearest_neigbors 拿到的是空向量,signal 恒为 0,外汇与贵金属的高杠杆下等于空转。
class="type">int OnInit() { class=class="str">"cmt">// gathering data to Matrix has been ignored nearest_neigbors = new CKNNNearestNeighbors(Matrix,_k); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- class="kw">delete(nearest_neigbors); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { vector x_vars(class="num">2); class=class="str">"cmt">//vector to store atr and volumes values class=class="str">"cmt">//adding live indicator values from the market has been ignored class=class="str">"cmt">//--- class="type">int signal = class="num">0; if (isNewBar() == true) class=class="str">"cmt">//we are on the new candle { signal = nearest_neigbors.KNNAlgorithm(x_vars); class=class="str">"cmt">//trading actions } }
◍ KNN 该用在哪些行情数据上
KNN 不是万能锤,先看清数据形态再上。标记清晰、无噪声、样本量偏小(几百到几千根 bar 内)的数据集最合适,样本过大时每次预测都要重算全量距离,MT5 回测会明显变慢。 外汇与贵金属属高杠杆高风险品种,局部聚集明显的波段拐点附近,KNN 基于邻近点分类可能比全局拟合更灵敏;但报价含滑点噪声时直接喂进去,分类边界会被打乱。 优势也很直白:逻辑肉眼可懂,不用训模型权重;预测时拿全部训练样本做参照,新 K 线进来即时比对。代价是内存里得常驻全量数据,实盘 tick 刷新频繁时得权衡 K 值与窗口长度。
「别急着下结论」
KNN 分类器在外汇和贵金属这类噪声密集的市场里,表现往往不如在股票、指数上稳定,后者数据结构更规则,模型预测倾向更可靠。但外汇保证金交易自带高杠杆高风险,任何算法信号都只是概率倾斜,实盘前务必自己跑一遍。 在 MT5 策略测试器里跑这套逻辑时,即便只取 50 根柱线、且机器人仅在新柱开立时行动,测试器仍会在每根蜡烛停顿 20 到 30 秒等算法跑完,实时交易反而更快,这是测试环境的性能坑。 代码里有个明显短板:Init 函数提不到指标读数,只能在训练处一次性提取再用。下面这段 VectorRemoveIndex 更有问题,它建了 new_v 却从没把非删除元素正确填入,也没返回,等于什么都没删。 把这类细节修掉,才可能让分类器在您自己的品种上跑出像样结果。算法不是圣杯,留给您去拆。
if (isNewBar() == true) class=class="str">"cmt">//we are on the new candle { if (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION)) { gather_data(); nearest_neigbors = new CKNNNearestNeighbors(Matrix,_k); signal = nearest_neigbors.KNNAlgorithm(x_vars); class="kw">delete(nearest_neigbors); } class="type">void CKNNNearestNeighbors::VectorRemoveIndex(vector &v, class="type">class="kw">ulong index) { vector new_v(v.Size()-class="num">1); for (class="type">class="kw">ulong i=class="num">0, count = class="num">0; i<v.Size(); i++) if (i == index) { new_v[count] = new_v[i]; count++; } }