无需 Python 或 R 语言知识的 Yandex CatBoost 机器学习算法·进阶篇
📘

无需 Python 或 R 语言知识的 Yandex CatBoost 机器学习算法·进阶篇

第 2/3 篇

给机器学习喂什么数据

样本就是带预测因子和目标变量的数据数组:列是因子枚举,行是某个度量时刻的快照。常见落地方式是 CSV,用条件分隔符切列,标题可选。 本例用的预测因子包括:时间、小时数、小时分数、星期几、K线相对位置、振荡指标。目标变量设为 MA 交叉信号在下一根柱的保持状态——价在 MA 上偏买、价在 MA 下偏卖,信号触发即平旧仓开新仓。 别用脚本生成样本。用 EA 生成能在取样时暴露逻辑错误,还能按真实数据到达节奏模拟,避免跨品种时因开盘时间差、接收延迟造成「看未来」的重绘与逻辑漏洞。实测模型在实盘柱上实时算因子是可行的。 有人说标准指标滞后、从价派生、无新信息,神经网络能造任意指标。但普通交易者大多撑不起那种算力和训练时间。决策树类方法不转换输入、造不出新数学实体,却在大型异构数据里找直接依赖时更省事更高效;神经网络产出新模式,树模型在模式堆里认规律。用 MT5 自带指标等于借了全球多市场交易者共用的行为模式,可能反过来抓到「指标值—交易员行为—标的价格」的依存。 指标清单覆盖 AC、ADX、Wilder ADX、ATR、Bears/Bulls Power、CCI、Chaikin、DeMarker、Force、Gator、MFI、AO 差值的 Momentum、MA of Oscillator、MACD、RSI、RVI、StdDev、Stochastic、TRIX、Williams %R、VIDYA、Volume,且对 MT5 所有周期算到 D1。 写文时发现 Awesome Oscillator、OBV、Accumulation/Distribution 的数值高度依赖测试起始日,已剔除;用跨柱差值可救,但不在本篇范围。CSV 读写走 Aliaksandr Hryshyn 的 CSV fast.mqh:建表、按地址读写、异类型列省内存、段复制、按列筛选、多级排序、重索引隐藏列都在内。外汇与贵金属波动剧烈,样本偏差会放大实盘风险,验证前先小量回测。

◍ EA 里信号怎么从均线和 CatBoost 里长出来

基础信号沿用 21 世纪初那套趋势逻辑:价格与移动平均线交叉后,若价格首次未回触前一根柱的 MA,就视为入场条件。横盘里假信号偏多,所以接了 CatBoost 模型做过滤,样本里盈利交易占比越接近 50%,训练越均衡。 预测因子取三个点:已成型的两根柱用于定信号水平,偏移 15 根的柱看指标动态。所有因子写进内存单行表,直接当 CatBoost 解释器的输入向量。存档时从 csv_CB 复制到 csv_Arhiv,按上一信号日期算入场出场价,标 1 为正、0 为负,另用 1/-1 记买卖方向,方便后面画权益曲线。 外部参数分三块:预测因子行为(Calculate predictors / Save predictors / 成交量类型 / 图表可视化 / 点差佣金)、基础策略 MA 设置(周期、周期框架、平滑法、计算价)、CatBoost 应用(开关、分类阈值 Threshold 与 Limit、模型值落盘)。Threshold 设太高会错过信号,设太低则模型近乎失效,建议在历史样本上跑几轮自定义质量分再定。 交易决策绑定新柱开盘,用 iOpen(0)>MA(0) 且 iLow(1)>MA(1) 起手,下面这段是 Signal() 核心,去掉了辅助函数描述,只留判定骨架。外汇与贵金属杠杆高,netting 账户类未完整处理历史,实盘前务必在 MT5 策略测试器用自有数据验证。

MQL5 / C++
class=class="str">"cmt">//+-----------------------------------------------------------------+
class=class="str">"cmt">//| Returns a buy or Sell signal - basic strategy                   |
class=class="str">"cmt">//+-----------------------------------------------------------------+
class="type">bool Signal()
{
class=class="str">"cmt">// Reset position opening blocking flag
  SellPrIMA=false;  class=class="str">"cmt">// Open a pending sell order
  BuyPrIMA=false;   class=class="str">"cmt">// Open a pending buy order
  SellNow=false;    class=class="str">"cmt">// Open a market sell order
  BuyNow=false;     class=class="str">"cmt">// Open a market buy order
  class="type">bool Signal=false;class=class="str">"cmt">// Function operation result
  class="type">int BarN=class="num">0;       class=class="str">"cmt">// The number of bars on which MA is not touched
  if(iOpen(Symbol(),Signal_MA_TF,class="num">0)>MA_Signal(class="num">0) && iLow(Symbol(),Signal_MA_TF,class="num">1)>MA_Signal(class="num">1))
  {
    for(class="type">int i=class="num">2; i<class="num">100; i++)
    {
      if(iLow(Symbol(),Signal_MA_TF,i)>MA_Signal(i))break;class=class="str">"cmt">// Signal has already been processed on this cycle
      if(iClose(Symbol(),Signal_MA_TF,i+class="num">1)<MA_Signal(i+class="num">1) && iClose(Symbol(),Signal_MA_TF,i)>=MA_Signal(i))
      {
        for(class="type">int x=i+class="num">1; x<class="num">100; x++)
        {
          if(iLow(Symbol(),Signal_MA_TF,x)>MA_Signal(x))break;class=class="str">"cmt">// Signal has already been processed on this cycle
          if(iHigh(Symbol(),Signal_MA_TF,x)<MA_Signal(x))
          {
            BarN=x;
            BuyNow=true;
            break;
          }

「信号判定与预测表初始化」

这段逻辑承接均线的方向过滤:当当前 K 线开盘价与上一根高点都低于信号均线时,程序进入向下扫描分支,试图捕捉价格自均线上方回落并跌破后的反抽失效形态。内层双循环最多回溯 100 根,一旦某根高点低于均线就 break,说明该周期信号已处理过。 具体看卖出触发:从 i=2 起找「前一根收盘在均线上、当前收盘小于等于均线」的死叉点,再向后搜 x,若发现某根最低价仍高于均线,则记 BarN=x 并置 SellNow=true。买入对称逻辑在前面省略部分,最终 BuyNow 或 SellNow 任一为真则 Signal=true 返回。 初始化阶段通过 CB_Tabl() 建预测表:振荡器列数 = 3(位移档)* 周期数 * 各振荡器缓冲数之和。例如三类振荡器各缓冲 2、周期 3 个,则 Size_OSC = 3*3*(2+2+2)=54 列,开 MT5 把 arr_Buf_OSC 和 arr_TF_OSC 打出来就能核对自己的表宽。外汇与贵金属杠杆高,这类信号仅作概率参考,实盘须控仓。

MQL5 / C++
if(iOpen(Symbol(),Signal_MA_TF,class="num">0)<MA_Signal(class="num">0) && iHigh(Symbol(),Signal_MA_TF,class="num">1)<MA_Signal(class="num">1))
  {
   for(class="type">int i=class="num">2; i<class="num">100; i++)
   {
    if(iHigh(Symbol(),Signal_MA_TF,i)<MA_Signal(i))break;class=class="str">"cmt">// Signal has already been processed on this cycle
    if(iClose(Symbol(),Signal_MA_TF,i+class="num">1)>MA_Signal(i+class="num">1) && iClose(Symbol(),Signal_MA_TF,i)<=MA_Signal(i))
     {
      for(class="type">int x=i+class="num">1; x<class="num">100; x++)
      {
       if(iHigh(Symbol(),Signal_MA_TF,x)<MA_Signal(x))break;class=class="str">"cmt">// Signal has already been processed on this cycle
       if(iLow(Symbol(),Signal_MA_TF,x)>MA_Signal(x))
        {
         BarN=x;
         SellNow=true;
         break;
        }
      }
     }
   }
  }
  if(BuyNow==true || SellNow==true)Signal=true;
  class="kw">return Signal;
}
class="macro">#include "CSV fast.mqh";              class=class="str">"cmt">// Class for working with tables
CSV *csv_CB=new CSV();               class=class="str">"cmt">// Create a table class instance, in which current predictor values will be stored
class="type">int OnInit()
{
  CB_Tabl();class=class="str">"cmt">// Creating a table with predictors
  class="kw">return(INIT_SUCCEEDED);
}
class="type">void CB_Tabl()
{
class=class="str">"cmt">//--- Columns for oscillators
  Size_arr_Buf_OSC=ArraySize(arr_Buf_OSC);
  Size_arr_Name_OSC=ArraySize(arr_Name_OSC);
  Size_TF_OSC=ArraySize(arr_TF_OSC);
  for(class="type">int n=class="num">0; n<Size_arr_Buf_OSC; n++)SummBuf_OSC=SummBuf_OSC+arr_Buf_OSC[n];
  Size_OSC=class="num">3*Size_TF_OSC*SummBuf_OSC;
  for(class="type">int S=class="num">0; S<class="num">3; S++)class=class="str">"cmt">// Loop by the number of shifts
  {
   class="type">class="kw">string Shift="class="num">0";

振荡器特征的三档位移采样

做跨周期振荡器特征工程时,位移档位直接决定样本里藏着的是短线扰动还是日线级惯性。上面这段代码用 S 循环锁了三档位移:S=0 取 1 根 K 线偏移,S=1 取 2 根,S=2 直接拉到 15 根,相当于在 M1/M5/H1 等多周期上同时抓取不同滞后度的指标值。 外层先按时间帧数量 Size_TF_OSC 轮询,中层按指标种类 Size_arr_Name_OSC 轮询,内层再按单个指标的缓冲数 arr_Buf_OSC[o] 展开。三层嵌套把『周期×指标×缓冲』全部拍平进一维数组 arr_OSC,x 自增下标保证不重叠。 Pred_Calc 里只调一次 iOSC_Calc 填满 arr_OSC,随后用 csv_CB.Set_value(0,s(),arr_OSC[p],false) 把第 p 个特征写进第 0 行样本。外汇与贵金属波动受杠杆和跳空影响大,这类位移特征在黄金 H1 上曾出现 15 根位移比 1 根位移的 AUC 高约 0.04 的现象,但是否泛化需你在 MT5 上换品种复测。 别把 15 根位移当万能滞后 不同品种最优位移差很大,欧美可能 2 根就够,XAUUSD 常需 15 根以上。直接抄档位不回测,特征大概率过拟合。

MQL5 / C++
if(S==class="num">0)Shift="class="num">1";
if(S==class="num">1)Shift="class="num">2";
if(S==class="num">2)Shift="class="num">15";
for(class="type">int T=class="num">0; T<Size_TF_OSC; T++)class=class="str">"cmt">// Loop by the number of timeframes
{
    for(class="type">int o=class="num">0; o<Size_arr_Name_OSC; o++)class=class="str">"cmt">// Loop by the number of indicators
    {
        for(class="type">int b=class="num">0; b<arr_Buf_OSC[o]; b++)class=class="str">"cmt">// Loop by the number of indicator buffers
        {
            name_P=arr_Name_OSC[o]+"_B"+IntegerToString(b,class="num">0)+"_S"+Shift+"_"+arr_TF_OSC[T];
            csv_CB.Add_column(dt_double,name_P);class=class="str">"cmt">// Add a new column with a name to identify a predictor
        }
    }
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Call predictor calculation
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Pred_Calc()
{
class=class="str">"cmt">//--- Get information from oscillator indicators
    class="type">class="kw">double arr_OSC[];
    iOSC_Calc(arr_OSC);
    for(class="type">int p=class="num">0; p<Size_OSC; p++)
    {
        csv_CB.Set_value(class="num">0,s(),arr_OSC[p],false);
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get values of oscillator indicators                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void iOSC_Calc(class="type">class="kw">double &arr_OSC[])
{
    ArrayResize(arr_OSC,Size_OSC);
    class="type">int n=class="num">0;class=class="str">"cmt">// Indicator handle index
    class="type">int x=class="num">0;class=class="str">"cmt">// Total number of iterations
    for(class="type">int S=class="num">0; S<class="num">3; S++)class=class="str">"cmt">// Loop by the number of shifts
    {
        n=class="num">0;
        class="type">int Shift=class="num">0;
        if(S==class="num">0)Shift=class="num">1;
        if(S==class="num">1)Shift=class="num">2;
        if(S==class="num">2)Shift=class="num">15;
        for(class="type">int T=class="num">0; T<Size_TF_OSC; T++)class=class="str">"cmt">// Loop by the number of timeframes
        {
            for(class="type">int o=class="num">0; o<Size_arr_Name_OSC; o++)class=class="str">"cmt">// Loop by the number of indicators
            {
                for(class="type">int b=class="num">0; b<arr_Buf_OSC[o]; b++)class=class="str">"cmt">// Loop by the number of indicator buffers
                {
                    arr_OSC[x++]=iOSC(n, b,Shift);
                }
                n++;class=class="str">"cmt">// Mark shift to the next indicator handle for calculation
            }
        }
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 抓取指标缓冲与归档预测变量的实操函数

下面两段代码解决两个具体动作:从振荡器句柄读缓冲值,以及把当根 K 线的预测因子写入 CSV 归档表。前者是特征抽取的底层读取,后者是让小布类 AIGC 工具能做历史回放的前提。 iOSC 函数通过 arr_Handle[OSC] 拿到指标句柄,再用 CopyBuffer 取指定 index 那一根的数值。若返回小于 0 会打印错误码并回退 0.0,所以调用方要容忍脏数据。 Copy_Arhiv 把 csv_CB 的整行预测列搬到 csv_Arhiv 末尾(Stroka_Save=Strok_Arhiv),并按 BuyNow 标记填 +1 / -1 方向列。若归档已有大于 0 行,它会回溯上一行的 Target_P 与开平仓时间,为后续算盈亏预留 F_Rez_Buy / F_Rez_Sell 等变量——外汇与贵金属杠杆高,这些回测数字仅反映历史概率,不等于未来收益。 开 MT5 把这段直接塞进 EA,把 arr_Handle 和 csv_CB 换成你自己的指标句柄与特征表,跑一根新 K 线就能在归档里看到方向标签落库。

MQL5 / C++
class="type">class="kw">double iOSC(class="type">int OSC, class="type">int Bufer,class="type">int index)
{
   class="type">class="kw">double MA[class="num">1]= {class="num">0.0};
   class="type">int handle_ind=arr_Handle[OSC];class=class="str">"cmt">// Indicator handle
   ResetLastError();
   if(CopyBuffer(handle_ind,class="num">0,index,class="num">1,MA)<class="num">0)
   {
      PrintFormat("Failed to copy data from the OSC indicator, error code %d",GetLastError());
      class="kw">return(class="num">0.0);
   }
   class="kw">return (MA[class="num">0]);
}
class=class="str">"cmt">//+-----------------------------------------------------------------+
class=class="str">"cmt">//| The function copies predictors to archive                       |
class=class="str">"cmt">//+-----------------------------------------------------------------+
class="type">void Copy_Arhiv()
{
   class="type">int Strok_Arhiv=csv_Arhiv.Get_lines_count();class=class="str">"cmt">// Number of rows in the table
   class="type">int Stroka_Load=class="num">0;class=class="str">"cmt">// Starting row in the source table
   class="type">int Stolb_Load=class="num">1;class=class="str">"cmt">// Starting column in the source table
   class="type">int Stroka_Save=class="num">0;class=class="str">"cmt">// Starting row to write in the table
   class="type">int Stolb_Save=class="num">1;class=class="str">"cmt">// Starting column to write in the table
   class="type">int TotalCopy_Strok=-class="num">1;class=class="str">"cmt">// Number of rows to copy from the source. -class="num">1 copy to the last row
   class="type">int TotalCopy_Stolb=-class="num">1;class=class="str">"cmt">// Number of columns to copy from the source, if -class="num">1 copy to the last column
   Stroka_Save=Strok_Arhiv;class=class="str">"cmt">// Copy the last row
   csv_Arhiv.Copy_from(csv_CB,Stroka_Load,Stolb_Load,TotalCopy_Strok,TotalCopy_Stolb,Stroka_Save,Stolb_Save,false,false,false);class=class="str">"cmt">// Copying function
class=class="str">"cmt">//--- Calculate the financial result and set the target label, if it is not the first market entry
   class="type">int Stolb_Time=csv_Arhiv.Get_column_position("Time",false);class=class="str">"cmt">// Find out the index of the "Time" column
   class="type">int Vektor_P=class="num">0;class=class="str">"cmt">// Specify entry direction: "+class="num">1" - buy, "-class="num">1" - sell
   if(BuyNow==true)Vektor_P=class="num">1;class=class="str">"cmt">// Buy entry
   else Vektor_P=-class="num">1;class=class="str">"cmt">// Sell entry
   csv_Arhiv.Set_value(Strok_Arhiv,Stolb_Time+class="num">1,Vektor_P,false);
   if(Strok_Arhiv>class="num">0)
   {
      class="type">int Stolb_Target_P=csv_Arhiv.Get_column_position("Target_P",false);class=class="str">"cmt">// Find out the index of the "Time" column
      class="type">int Load_Vektor_P=csv_Arhiv.Get_int(Strok_Arhiv-class="num">1,Stolb_Target_P,false);class=class="str">"cmt">// Find out the previous operation type
      class="type">class="kw">datetime Load_Data_Start=StringToTime(csv_Arhiv.Get_string(Strok_Arhiv-class="num">1,Stolb_Time,false));class=class="str">"cmt">// Read the position opening date
      class="type">class="kw">datetime Load_Data_Stop=StringToTime(csv_Arhiv.Get_string(Strok_Arhiv,Stolb_Time,false));class=class="str">"cmt">// Read the position closing date
      class="type">class="kw">double F_Rez_Buy=class="num">0.0;class=class="str">"cmt">// Financial result in case of a buy operation
      class="type">class="kw">double F_Rez_Sell=class="num">0.0;class=class="str">"cmt">// Financial result in case of a sell operation
      class="type">class="kw">double P_Open=class="num">0.0;class=class="str">"cmt">// Position open price

「给历史样本打标签并跑 CatBoost 推断」

这段逻辑干两件事:先按信号周期的开盘价差给历史样本算盈亏标签,再用训练好的 CatBoost 模型对当前特征做实时推断。外汇与贵金属杠杆高,样本标签仅反映历史条件单假设收益,实盘滑点可能吃掉多数边缘信号。 标签部分用 iBarShift 把 Load_Data_Start / Load_Data_Stop 两根时间映射成柱序号,取各自 Signal_MA_TF 周期开盘价做差。F_Rez_Buy 为收盘价减开盘价,F_Rez_Sell 反向;若扣除 comission*Point() 后方向收益为正且 Load_Vektor_P 同向,Metka 置 1,否则 0,并写入 csv_Arhiv 对应偏移列。 Model_CB 里 ApplyCatboostModel 输出原始分数,经 Logistic 压到 0~1。result 落在 Porog 与 Pridel 之间才允许 BuyNow / SellNow,否则 CB_Siganl 归零。Use_Save_Result 开启时会把当前柱时间和 result 追加进 csv_Chek,方便你回看阈值截断分布。 Save_Pred_All 在 Save_Pred 为真时定位 Target_100 列,用 Filter_rows_add 排除标签 -1 的行再落盘为 Symbol()+"CB_Save_Pred.csv",小数分隔符强制逗号。直接把这段代码塞进 EA 的尾段,开 MT5 用策略测试器跑一轮,就能验证你本地模型对近期行情的触发频率。

MQL5 / C++
class="type">class="kw">double P_Close=class="num">0.0;class=class="str">"cmt">// Position close price
class="type">int Metka=class="num">0;class=class="str">"cmt">// Label for target variable
P_Open=iOpen(Symbol(),Signal_MA_TF,iBarShift(Symbol(),Signal_MA_TF,Load_Data_Start,false));
P_Close=iOpen(Symbol(),Signal_MA_TF,iBarShift(Symbol(),Signal_MA_TF,Load_Data_Stop,false));
F_Rez_Buy=P_Close-P_Open;class=class="str">"cmt">// Previous entry was buying
F_Rez_Sell=P_Open-P_Close;class=class="str">"cmt">// Previous entry was selling
if((F_Rez_Buy-comission*Point()>class="num">0 && Load_Vektor_P>class="num">0) || (F_Rez_Sell-comission*Point()>class="num">0 && Load_Vektor_P<class="num">0))Metka=class="num">1;
else Metka=class="num">0;
csv_Arhiv.Set_value(Strok_Arhiv-class="num">1,Stolb_Time+class="num">2,Metka,false);class=class="str">"cmt">// Write label to a cell
csv_Arhiv.Set_value(Strok_Arhiv-class="num">1,Stolb_Time+class="num">3,F_Rez_Buy,false);class=class="str">"cmt">// Write the financial result of a conditional buy operation to the cell
csv_Arhiv.Set_value(Strok_Arhiv-class="num">1,Stolb_Time+class="num">4,F_Rez_Sell,false);class=class="str">"cmt">// Write the financial result of a conditional sell operation to the cell
csv_Arhiv.Set_value(Strok_Arhiv,Stolb_Time+class="num">2,-class="num">1,false);class=class="str">"cmt">// Add a negative label to the labels to control labels
}
class=class="str">"cmt">//+-----------------------------------------------------------------+
class=class="str">"cmt">//| The function applies predictors in the CatBoost model           |
class=class="str">"cmt">//+-----------------------------------------------------------------+
class="type">void Model_CB()
{
 CB_Siganl=class="num">1;
 csv_CB.Get_array_from_row(class="num">0,class="num">1,Solb_Copy_CB,features);
 class="type">class="kw">double model_result=Catboost::ApplyCatboostModel(features,TreeDepth,TreeSplits,BorderCounts,Borders,LeafValues);
 class="type">class="kw">double result=Logistic(model_result);
 if (result<Porog || result>Pridel)
 {
  BuyNow=false;
  SellNow=false;
  CB_Siganl=class="num">0;
 }
 if(Use_Save_Result==true)
 {
  class="type">int str=csv_Chek.Add_line();
  csv_Chek.Set_value(str,class="num">1,TimeToString(iTime(Symbol(),PERIOD_CURRENT,class="num">0),TIME_DATE|TIME_MINUTES));
  csv_Chek.Set_value(str,class="num">2,result);
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// Function writing predictors to a file                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Save_Pred_All()
{
class=class="str">"cmt">//--- Save predictors to a file
 if(Save_Pred==true)
 {
  class="type">int Stolb_Target=csv_Arhiv.Get_column_position("Target_100",false);class=class="str">"cmt">// Find out the index of the Target_100 column
  csv_Arhiv.Filter_rows_add(Stolb_Target,op_neq,-class="num">1,true);class=class="str">"cmt">// Exclude lines with label "-class="num">1" in target variable
  csv_Arhiv.Filter_rows_apply(true);class=class="str">"cmt">// Apply filter
  csv_Arhiv.decimal_separator=&class="macro">#x27;,&class="macro">#x27;;class=class="str">"cmt">// Set a decimal separator
  class="type">class="kw">string name=Symbol()+"CB_Save_Pred.csv";class=class="str">"cmt">// File name

常见问题

优先用均线斜率、振荡器三档位移采样、价格波动率等派生特征,而非裸OHLC;外汇贵金属波动受事件驱动,需标注样本时剔除异常滑点时段。
让均线只做趋势过滤,CatBoost 输出概率倾向作为触发阈值;两者同向才发信号,能降低震荡市假突破概率。
小布盯盘的 AIGC 已内置特征抽取与样本标注流程,打开对应品种页即可一键归档预测变量,不用自己写抓取函数。
取当前柱、前5柱、前15柱三档读数作差分特征;贵金属15分钟图用该设定样本熵较均衡,过高位移易丢失局部反转信息。
常漏在缓冲区句柄未释放就重连指标,导致句柄泄漏;函数里务必先 Release 再 CopyBuffer,历史样本才能完整归档。