无需 Python 或 R 语言知识的 Yandex CatBoost 机器学习算法·进阶篇
给机器学习喂什么数据
样本就是带预测因子和目标变量的数据数组:列是因子枚举,行是某个度量时刻的快照。常见落地方式是 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 策略测试器用自有数据验证。
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 打出来就能核对自己的表宽。外汇与贵金属杠杆高,这类信号仅作概率参考,实盘须控仓。
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 根以上。直接抄档位不回测,特征大概率过拟合。
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 线就能在归档里看到方向标签落库。
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 用策略测试器跑一轮,就能验证你本地模型对近期行情的触发频率。
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