数据科学与机器学习(第 09 部分):K-最近邻算法(KNN)·综合运用
📘

数据科学与机器学习(第 09 部分):K-最近邻算法(KNN)·综合运用

第 3/3 篇

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 日线属高风险品种,回测准确率不构成任何方向暗示,只说明模型在该参数下的拟合倾向。

MQL5 / C++
  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 调用会直接悬空。

MQL5 / C++
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,外汇与贵金属的高杠杆下等于空转。

MQL5 / C++
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 却从没把非删除元素正确填入,也没返回,等于什么都没删。 把这类细节修掉,才可能让分类器在您自己的品种上跑出像样结果。算法不是圣杯,留给您去拆。

MQL5 / C++
        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++;
      }
 }

常见问题

用邻数扫描从 3 到 30 逐档跑,观察交叉验证准确率曲线,选拐点附近(如 7~11)的 k,避免过大欠拟合或过小过拟合。
在每根 K 线收盘后调用模型预测,仅当信号连续 2 根同向再发单,减少毛刺假信号,下单前加风控手数限制。
小布可自动对品种做 k 值扫描标出准确率拐点,并把 KNN 信号接入盯盘页推送,你只需设定品种和参数范围。
只缓存最近 N 根特征向量做滑动窗口,老数据出窗,通常 500 根内占用极低,普通电脑可实时跑。
建议用标准化后的收益率、波动率、RSI 等衍生特征,裸价因量纲差异大易失真;贵金属与外汇属高风险,信号仅作概率参考。