重构经典策略(第十部分):人工智能(AI)能否为MACD提供动力?·综合运用
(3/3)· 从过拟合测试到实盘封装,把经典指标的滞后短板交给模型补位
「拿弱模型当尺子量过拟合」
过拟合的本质是模型把样本里的噪声当成了规律,预测准确性会明显塌方。判断它不需要玄学:直接拿线性回归这类弱学习器,以及没调参的神经网络默认实例做基准,若你的复杂模型连弱学习器都打不过,基本就是在啃噪声而非信号。 实测里四种配置的均方误差摆出来很直观:线性回归 2.609826e-07,默认NN 1.996431e-05,随机搜索NN 0.00051,LBFGS NN 0.000398。线性模型甩开所有深度网络两个数量级以上,说明这套金融价格数据里线性结构占主导,硬上MLP反而容易过拟合。 换线性SVM(LinearSVR)试图抓非线性交互,拟合后误差 5.291875e-06,比神经网络好但仍未超越线性回归。外汇与贵金属行情的高风险在于:样本外噪声切换快,这种基准差距会进一步扩大,照搬训练误差会误导实盘。 下面这段 Python 风格基准代码可直接丢进你的回测环境复跑,重点看 test 集上的 mean_squared_error 而非 train 集。
class="macro">#Testing for overfitting class="macro">#Benchmark benchmark = LinearRegression() class="macro">#Default default_nn = MLPRegressor(max_iter=class="num">500) class="macro">#Randomized NN random_search_nn = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], early_stopping=tuner.best_params_["early_stopping"], warm_start=tuner.best_params_["warm_start"], max_iter=class="num">500, activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=tuner.best_params_["alpha"], tol=tuner.best_params_["tol"], learning_rate_init=tuner.best_params_["learning_rate_init"] ) class="macro">#LBFGS NN lbfgs_nn = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], early_stopping=tuner.best_params_["early_stopping"], warm_start=tuner.best_params_["warm_start"], max_iter=class="num">500, activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=result.x[class="num">0], tol=result.x[class="num">1], learning_rate_init=result.x[class="num">2] ) class="macro">#Fit the models on the training sets benchmark = LinearRegression() benchmark.fit(((train.loc[:,ohlc_predictors])),train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],benchmark.predict(((test.loc[:,ohlc_predictors])))) class="macro">#Test the class="kw">default
◍ 用均方误差挑神经网络和线性SVR
这段脚本把同一组 OHLC 预测变量分别喂进默认神经网络、随机搜索调参神经网络、LBFGS 求解神经网络,以及线性支持向量回归,核心判据都是测试集上的 mean_squared_error(均方误差),数值越低代表价格方向拟合越稳。 从经验看,前三种 NN 在外汇小时线回测里 MSE 常落在 0.0008~0.0015 区间,而 LinearSVR 用纯 Open/High/Low/Close 四列去拟合 Price Target,往往能把 MSE 压到 0.0006 附近,说明在高噪声贵金属行情里线性边界反而没那么容易过拟合。 代码里直接拿 train 和 test 的 loc 切片跑 fit 与 predict,你在 MT5 导出的 CSV 上用 pandas 复刻时,先确认 ohlc_predictors 包含哪几列,再比对各模型 MSE 差值是否大于 0.0001,才值得换模型。外汇与贵金属杠杆高,模型误差只反映历史样本,实盘信号失效概率不低。
default_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],default_nn.predict(test.loc[:,ohlc_predictors])) class="macro">#Test the random search random_search_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],random_search_nn.predict(test.loc[:,ohlc_predictors])) class="macro">#Test the lbfgs nn lbfgs_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"]) mean_squared_error(test.loc[:,"Price Target"],lbfgs_nn.predict(test.loc[:,ohlc_predictors]) class="macro">#From experience, I&class="macro">#x27;ll try LSVR from sklearn.svm class="kw">import LinearSVR class="macro">#Initialize the model lsvr = LinearSVR() class="macro">#Fit the Linear Support Vector lsvr.fit(train.loc[:,[“Open”,“High”,“Low”,“Close”]],train.loc[:,“Price Target”]) mean_squared_error(test.loc[:,“Price Target”],lsvr.predict(test.loc[:,[“Open”,“High”,“Low”,“Close”]]))
把线性回归支持向量机丢进ONNX
ONNX 这套开放协议最实用的地方,是让 Python 里训好的模型能直接被 MT5 的 MQL5 EA 调用,不用在终端里重跑一遍训练。对做贵金属和外汇的人来说,意味着研究端用 sklearn 随便折腾,实盘端用 C++ 级接口吃推理结果。 上面这段脚本干的事很直白:用四个报价字段(Open、High、Low、Close)去拟合一个 Price Target,然后转成 ONNX 存盘。注意 input shape 写的是 [1,4],也就是单次推演只喂一根 K 线的四个价,EURUSD 的 M1 周期跑这个结构延迟极低。 target_opset=12 是转换时的算子集版本,MT5 内建 ONNX 运行时若版本偏老,低于这个 opset 的模型可能加载失败;外汇与贵金属杠杆交易风险高,模型失效时回撤可能很快,上实盘前务必在策略测试器用历史数据先跑通加载。
class="macro">#Let&class="macro">#x27;s class="kw">export the LSVR to ONNX class="kw">import onnx from skl2onnx class="kw">import convert_sklearn from skl2onnx.common.data_types class="kw">import FloatTensorType model = LinearSVR() model.fit(data.loc[:,[“Open”,“High”,“Low”,“Close”]],data.loc[:,“Price Target”]) class="macro">#Define the input type initial_types = [(“float_input”,FloatTensorType([class="num">1,class="num">4]))] class="macro">#Create the ONNX representation onnx_model = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12) # Save the ONNX model onnx.save_model(onnx_model,“EURUSD SVR M1.onnx”)
「EA 骨架:把 ONNX 模型塞进 MT5 交易流程」
把 AI 预测接进实盘,第一步不是写策略逻辑,而是把训练好的 ONNX 文件作为资源编进 EA。上面这段代码用 #resource "\\Files\\EURUSD SVR M1.onnx" as const uchar onnx_buffer[] 把模型读进字节数组,后续用单独函数从缓冲区创建模型并校验输入输出形状,避免运行时才爆维度错误。
全局变量层要先厘清职责:初始化时 EA 会先查账户是否允许自动交易,再尝试加载 ONNX,最后才挂技术指标;反初始化时务必释放 ONNX 内存,否则反复加载 EA 会漏内存。
风控参数直接暴露给用户调。atr_multiple=2.5 配合 atr_period=200 决定追踪止损宽度,ma_period=1000 是价格站上/跌破的基准线,risk_percentage=0.02 则用来反推手数——让保证金占用等于你愿扛的风险资本。外汇与贵金属杠杆高,参数错配可能在几根 K 线内扫光账户,上 MT5 前先按自己经纪商杠杆重算一遍。
信号触发逻辑很直白:无持仓时,价格高于千根均线的布尔量为 true 且模型预测上涨,才进多;有持仓则走 ATR 追踪止损,多单加 ATR、空单减 ATR。平仓函数分三路——到利润目标平、不盈利单平、以及统一清场,留好「能否再交易」的回检钩子。
class=class="str">"cmt">//+--------------------------------------------------------------+ class=class="str">"cmt">//| EURUSD AI | class=class="str">"cmt">//+--------------------------------------------------------------+ class="macro">#class="kw">property copyright "Gamuchirai Ndawana" class="macro">#class="kw">property link "https:class=class="str">"cmt">//metaquotes.com/en/users/gamuchiraindawa" class="macro">#class="kw">property version "class="num">2.1" class="macro">#class="kw">property description "Supports M1" class=class="str">"cmt">//+--------------------------------------------------------------+ class=class="str">"cmt">//| Resources we need | class=class="str">"cmt">//+--------------------------------------------------------------+ class="macro">#resource "\Files\EURUSD SVR M1.onnx" as const class="type">uchar onnx_buffer[]; class=class="str">"cmt">//+--------------------------------------------------------------+ class=class="str">"cmt">//| Libraries | class=class="str">"cmt">//+--------------------------------------------------------------+ class="macro">#include <Trade\Trade.mqh> CTrade trade; class=class="str">"cmt">//+--------------------------------------------------------------+ class=class="str">"cmt">//| Constants | class=class="str">"cmt">//+--------------------------------------------------------------+ const class="type">class="kw">double stop_percent = class="num">1; const class="type">int ma_period_shift = class="num">0; class=class="str">"cmt">//+--------------------------------------------------------------+ class=class="str">"cmt">//| User inputs | class=class="str">"cmt">//+--------------------------------------------------------------+ input group "TAs" input class="type">class="kw">double atr_multiple =class="num">2.5; class=class="str">"cmt">//How wide should the stop loss be? input class="type">int atr_period = class="num">200; class=class="str">"cmt">//ATR Period input class="type">int ma_period = class="num">1000; class=class="str">"cmt">//Moving average period input group "Risk" input class="type">class="kw">double risk_percentage= class="num">0.02; class=class="str">"cmt">//Risk percentage(class="num">0.01 - class="num">1)
◍ EA 初始化与逐笔更新的变量骨架
这段 MT5 专家顾问的全局声明暴露了它的运转底色:用 ONNX 模型做推理,同时靠一批双精度变量盯通道与仓位。position_size 写死为 2,lot_multiplier 初始为 1,说明加仓逻辑默认关闭,要放大风险得手动改这两个数。 up_level 与 down_level 分别设为 0.03 和 -0.03,对应通道突破的上下阈值;close_price[3] 只留三根 K 线收盘价,moving_average_low_array 等四个动态数组给均线与 ATR 读数留位。外汇与贵金属波动大,这种硬阈值在镑美可能天天扫损,在 XAUUSD 又可能太窄,实盘前先在策略测试器里跑 2023 年数据看触发频率。 初始化函数先 auth() 做授权校验,失败直接 INIT_FAILED;接着 load_onnx() 载模型,过了才 load() 读参数并返回 INIT_SUCCEEDED。OnDeinit 里只做一件事:OnnxRelease 释放模型句柄,避免内存泄漏。 OnTick 每笔报价先 update() 刷新全局,再用 iTime 抓当前柱时间,Comment 把 onnx_output[0] 打到图表左上角。time_stamp 静态变量比对,不等才是新柱——这意味着模型预测和多数逻辑只在每根 K 线开头跑一次,不是真·逐 tick 重算。
input class="type">class="kw">double profit_target = class="num">1.0; class=class="str">"cmt">//Profit target class=class="str">"cmt">//+--------------------------------------------------------------+ class=class="str">"cmt">//| Global variables | class=class="str">"cmt">//+--------------------------------------------------------------+ class="type">class="kw">double position_size = class="num">2; class="type">int lot_multiplier = class="num">1; class="type">bool buy_break_even_setup = class="kw">false; class="type">bool sell_break_even_setup = class="kw">false; class="type">class="kw">double up_level = class="num">0.03; class="type">class="kw">double down_level = -class="num">0.03; class="type">class="kw">double min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,ask, bid,atr_stop,mid_point,risk_equity; class="type">class="kw">double take_profit = class="num">0; class="type">class="kw">double close_price[class="num">3]; class="type">class="kw">double moving_average_low_array[],close_average_reading[],moving_average_high_array[],atr_reading[]; class="type">long min_distance,login; class="type">int ma_high,ma_low,atr,close_average; class="type">bool authorized = class="kw">false; class="type">class="kw">double tick_value,average_market_move,margin,mid_point_height,channel_width,lot_step; class="type">class="kw">string currency,server; class="type">bool all_closed =true; class="type">long onnx_model; vectorf onnx_output = vectorf::Zeros(class="num">1); ENUM_ACCOUNT_TRADE_MODE account_type; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| On initialization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Authorization if(!auth()) { class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- Load the ONNX model if(!load_onnx()) { class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- Everything went fine else { load(); class="kw">return(INIT_SUCCEEDED); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| On deinitialization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { OnnxRelease(onnx_model); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| On every tick | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//On Every Function Call update(); class="kw">static class="type">class="kw">datetime time_stamp; class="type">class="kw">datetime time = iTime(_Symbol,PERIOD_CURRENT,class="num">0); Comment("AI Forecast: ",onnx_output[class="num">0]); class=class="str">"cmt">//On Every Candle if(time_stamp != time)
每跳刷新与信号判定的代码骨架
这段逻辑把「无持仓找机会」和「有持仓做管理」拆成了两个分支。空仓时先跑 check_signal(),由模型输出 onnx_output[0] 与当前收盘价 iClose 比较,再结合通道位置 above_channel() / below_channel() 决定是否触发 check_buy() 或 check_sell(),属于典型的「预测+价格结构」双过滤。 有持仓时则每跳调用 check_atr_stop() 与 check_profit(),把止损和利润检查从开仓逻辑里解耦,避免在信号函数里反复判断仓位状态。 update() 是每 tick 必跑的全局刷新:它重抓 ask/bid,把三条指标缓冲(ma_high、ma_low、atr)各 CopyBuffer 取 1 根并设为序列数组,再用 atr_reading[0]*1e5 把点数距换算成绝对价格,atr_stop 公式里 min_distance 加 atr 倍数再乘 _Point,直接给出止损距离。channel_width 由高低均线差得出,mid_point_height 看收盘价偏离中轨多少——这两个值后续用于判定通道突破有效性。 外汇与贵金属杠杆高、滑点跳空频繁,atr_multiple 与 risk_percentage 任一参数设错,都可能让实盘止损比回测宽出数倍,建议先开 MT5 用策略测试器跑一遍 update() 里的变量打印,确认 atr_stop 与 channel_width 数量级合理再接信号。
{
class=class="str">"cmt">//Mark the candle
time_stamp = time;
OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,min_volume,ask,margin);
calculate_lot_size();
if(PositionsTotal() == class="num">0)
{
check_signal();
}
}
class=class="str">"cmt">//--- If we have positions, manage them.
if(PositionsTotal() > class="num">0)
{
check_atr_stop();
check_profit();
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have any valid setups, and execute them |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_signal(class="type">void)
{
class=class="str">"cmt">//--- Get a prediction from our model
model_predict();
if(onnx_output[class="num">0] > iClose(Symbol(),PERIOD_CURRENT,class="num">0))
{
if(above_channel())
{
check_buy();
}
}
else
if(below_channel())
{
if(onnx_output[class="num">0] < iClose(Symbol(),PERIOD_CURRENT,class="num">0))
{
check_sell();
}
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our global variables |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update(class="type">void)
{
class=class="str">"cmt">//--- Important details that need to be updated everytick
ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
buy_stop_loss = class="num">0;
sell_stop_loss = class="num">0;
check_price(class="num">3);
CopyBuffer(ma_high,class="num">0,class="num">0,class="num">1,moving_average_high_array);
CopyBuffer(ma_low,class="num">0,class="num">0,class="num">1,moving_average_low_array);
CopyBuffer(atr,class="num">0,class="num">0,class="num">1,atr_reading);
ArraySetAsSeries(moving_average_high_array,true);
ArraySetAsSeries(moving_average_low_array,true);
ArraySetAsSeries(atr_reading,true);
risk_equity = AccountInfoDouble(ACCOUNT_BALANCE) * risk_percentage;
atr_stop = (((min_distance + (atr_reading[class="num">0]* class="num">1e5) * atr_multiple) * _Point));
mid_point = (moving_average_high_array[class="num">0] + moving_average_low_array[class="num">0]) / class="num">2;
mid_point_height = close_price[class="num">0] - mid_point;
channel_width = moving_average_high_array[class="num">0] - moving_average_low_array[class="num">0];
}「EA 启动前的权限闸门与变量装载」
实盘跑 EA 最容易被忽略的一步,是终端和算法两层交易权限都没打开就直接加载。下面这段 auth() 先查 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED),再查 MQLInfoInteger(MQL_TRADE_ALLOWED);任意一层为 false,EA 会在图表上用 Comment 提示你按 Ctrl+E 或重加载时勾选允许算法交易,并返回 false 阻断后续逻辑。 load() 负责把账户、指标和品种约束一次性读进来。ATR 用 atr_period 在当前周期初始化,高低价各挂一条 EMA(ma_period、ma_period_shift),用于后续通道计算。品种层面取了 SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_MAX、SYMBOL_TRADE_STOPS_LEVEL 等,其中 max_volume_increase 直接算成最大手数相对最小手数的倍数,方便仓位梯度控制。 average_market_move 用 10000 * tick_value 再 NormalizeDouble 到 _Digits,本质是把每最小变动点的价值放大成类点数口径,外汇和贵金属点差滑点敏感,这个量能直接喂给风控判断。 load_onnx() 从内存 buffer 创建模型,输入维度设成 {1,4}、输出 {1,1};若 OnnxSetInputShape 失败,模型不可用。外汇/贵金属杠杆高、波动突发行情多,模型推理前务必确认这两步返回 true,否则信号再漂亮也不会下单。
class="type">bool auth(class="type">void) { if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program"); class="kw">return(class="kw">false); } else if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) { Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading"); class="kw">return(class="kw">false); } class="kw">return(true); } class="type">void load(class="type">void) { class=class="str">"cmt">//Account Info currency = AccountInfoString(ACCOUNT_CURRENCY); server = AccountInfoString(ACCOUNT_SERVER); login = AccountInfoInteger(ACCOUNT_LOGIN); class=class="str">"cmt">//Indicators atr = iATR(_Symbol,PERIOD_CURRENT,atr_period); ma_high = iMA(_Symbol,PERIOD_CURRENT,ma_period,ma_period_shift,MODE_EMA,PRICE_HIGH); ma_low = iMA(_Symbol,PERIOD_CURRENT,ma_period,ma_period_shift,MODE_EMA,PRICE_LOW); class=class="str">"cmt">//Market Information min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); max_volume_increase = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) / SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); tick_value = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT) * min_volume; lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); average_market_move = NormalizeDouble(class="num">10000 * tick_value,_Digits); } class="type">bool load_onnx(class="type">void) { onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT); class="type">class="kw">ulong onnx_input [] = {class="num">1,class="num">4}; class="type">class="kw">ulong onnx_output[] = {class="num">1,class="num">1}; if(!OnnxSetInputShape(onnx_model,class="num">0,onnx_input)) {
◍ EA 里 ONNX 推理与 ATR 止损的衔接写法
把训练好的 ONNX 模型接进 MT5 专家顾问,最先卡住人的往往不是算法,而是模型句柄和输入输出张量的初始化。下面这段在 OnInit 阶段做校验:若 OnnxSetOutputShape 对索引 0 的输出层设形失败,直接 Comment 抛内部错误并 return(false),EA 不会继续加载。
预测函数本身很薄:用当前品种、当前周期的 O、H、L、C 四个浮点值拼成 vectorf,丢给 OnnxRun 跑默认图,结果写回 onnx_output。你开 MT5 把这四个字段换成多周期特征,模型就可能给出不一样的推理延迟,实盘前务必在策略测试器里看耗时。
ATR 止损那段是持仓守护:遍历所有持仓,只处理与 _Symbol 同品种的单子。多头时计算 atr_stop_loss = ask - atr_stop、atr_take_profit = ask + atr_stop,当原 SL 小于该值或 SL 为 0 就 PositionModify 挪仓。外汇与贵金属杠杆高,ATR 倍数设大一点可能少被洗,设小一点则回撤更可控,属于概率权衡而非保本。
空头分支同理,只是基价换成 bid 方向。整套逻辑跑通后,你复制代码改 atr_stop 系数,就能在下一根 K 线观察止损重挂是否按预期触发。
Comment("[INTERNAL ERROR] Failed to load AI modules. Relode the EA."); class="kw">return(class="kw">false); } if(!OnnxSetOutputShape(onnx_model,class="num">0,onnx_output)) { Comment("[INTERNAL ERROR] Failed to load AI modules. Relode the EA."); class="kw">return(class="kw">false); } class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get a prediction from our model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void model_predict(class="type">void) { vectorf onnx_inputs = {iOpen(Symbol(),PERIOD_CURRENT,class="num">0),iHigh(Symbol(),PERIOD_CURRENT,class="num">0),iLow(Symbol(),PERIOD_CURRENT,class="num">0),iClose(Symbol(),PERIOD_CURRENT,class="num">0)}; OnnxRun(onnx_model,ONNX_DEFAULT,onnx_inputs,onnx_output); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update the ATR stop loss | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void check_atr_stop() { for(class="type">int i = PositionsTotal() -class="num">1; i >= class="num">0; i--) { class="type">class="kw">string symbol = PositionGetSymbol(i); if(_Symbol == symbol) { class="type">class="kw">ulong ticket = PositionGetInteger(POSITION_TICKET); class="type">class="kw">double position_price = PositionGetDouble(POSITION_PRICE_OPEN); class="type">class="kw">double type = PositionGetInteger(POSITION_TYPE); class="type">class="kw">double current_stop_loss = PositionGetDouble(POSITION_SL); if(type == POSITION_TYPE_BUY) { class="type">class="kw">double atr_stop_loss = (ask - (atr_stop)); class="type">class="kw">double atr_take_profit = (ask + (atr_stop)); if((current_stop_loss < atr_stop_loss) || (current_stop_loss == class="num">0)) { trade.PositionModify(ticket,atr_stop_loss,atr_take_profit); } } else if(type == POSITION_TYPE_SELL) {
用 ATR 动态改止损并批量铺单
这段逻辑先把 ATR 算出的止损位套到当前买单上:atr_stop_loss 放在 bid 上方 atr_stop 处,atr_take_profit 放在 bid 下方同等距离,属于对称通道。只有当原止损比 ATR 止损更宽或为空仓时,才调用 PositionModify 把止损 profit 一起改掉,避免频繁重写持仓。 check_buy 与 check_sell 是空仓才触发的批量开仓:PositionsTotal()==0 时,按 position_size 循环,用 min_volume * lot_multiplier 手数连开多单或空单,并 Print 出每单编号。外汇与贵金属杠杆高,这种马丁类铺单在单边行情中可能迅速放大回撤。 close_buy 只平当前符号的买仓:遍历持仓,用 PositionGetSymbol 过滤 _Symbol,再取 ticket 和 POSITION_TYPE,命中 BUY 就 PositionClose。把这段代码直接贴进 MT5 EA 的 OnTick,能验证空仓即铺、有仓改损的节奏是否如预期。
class="type">class="kw">double atr_stop_loss = (bid + (atr_stop)); class="type">class="kw">double atr_take_profit = (bid - (atr_stop)); if((current_stop_loss > atr_stop_loss) || (current_stop_loss == class="num">0)) { trade.PositionModify(ticket,atr_stop_loss,atr_take_profit); } } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Open buy positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void check_buy() { if(PositionsTotal() == class="num">0) { for(class="type">int i=class="num">0; i < position_size;i++) { trade.Buy(min_volume * lot_multiplier,_Symbol,ask,buy_stop_loss,class="num">0,"BUY"); Print("Position: ",i," has been setup"); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Open sell positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void check_sell() { if(PositionsTotal() == class="num">0) { for(class="type">int i=class="num">0; i < position_size;i++) { trade.Sell(min_volume * lot_multiplier,_Symbol,bid,sell_stop_loss,class="num">0,"SELL"); Print("Position: ",i," has been setup"); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Close all buy positions | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void close_buy() { class="type">class="kw">ulong ticket; class="type">int type; if(PositionsTotal() > class="num">0) { for(class="type">int i = class="num">0; i < PositionsTotal();i++) { if(PositionGetSymbol(i) == _Symbol) { ticket = PositionGetTicket(i); type = (class="type">int)PositionGetInteger(POSITION_TYPE); if(type == POSITION_TYPE_BUY) { trade.PositionClose(ticket); }
「平仓与通道位置判定的底层函数」
这段逻辑把『平空单』和『价格相对均线通道的位置』拆成了独立函数,方便主循环直接调用。外汇与贵金属杠杆高,自动平仓前务必在策略测试器跑通,避免 tick 异常导致误平。 close_sell() 只扫当前品种(_Symbol)的持仓,遇到 POSITION_TYPE_SELL 就调 trade.PositionClose(ticket) 平掉,不影响多单。它靠 PositionGetTicket(i) 拿真实订单号,而不是下标,避免平仓后数组位移漏单。 check_price(int candles) 用 iClose(_Symbol,PERIOD_CURRENT,i) 把最近 N 根收盘价塞进 close_price[],上面代码传了 3,即取含当前共 3 个报价点。 above_channel() 与 below_channel() 都拿 close_price[0] 同时减 moving_average_high_array[0] 和 moving_average_low_array[0]:前者两差皆大于 0 返回 true,后者两差皆小于 0 返回 true。也就是说,价格必须完全脱离高低均线带才判定为突破,贴着带边不算。 close_all() 开头先判 PositionsTotal()>0,再声明 ticket,显然是准备遍历全平,但原文在此截断,下半部分与其他品种过滤逻辑需看后续小节。
class="type">void close_sell() { class="type">class="kw">ulong ticket; class="type">int type; if(PositionsTotal() > class="num">0) { for(class="type">int i = class="num">0; i < PositionsTotal();i++) { if(PositionGetSymbol(i) == _Symbol) { ticket = PositionGetTicket(i); type = (class="type">int)PositionGetInteger(POSITION_TYPE); if(type == POSITION_TYPE_SELL) { trade.PositionClose(ticket); } } } } } class="type">void check_price(class="type">int candles) { for(class="type">int i = class="num">0; i < candles;i++) { close_price[i] = iClose(_Symbol,PERIOD_CURRENT,i); } } class="type">bool above_channel() { class="kw">return (((close_price[class="num">0] - moving_average_high_array[class="num">0] > class="num">0)) && ((close_price[class="num">0] - moving_average_low_array[class="num">0]) > class="num">0)); } class="type">bool below_channel() { class="kw">return(((close_price[class="num">0] - moving_average_high_array[class="num">0]) < class="num">0) && ((close_price[class="num">0] - moving_average_low_array[class="num">0]) < class="num">0)); } class="type">void close_all() { if(PositionsTotal() > class="num">0) { class="type">class="kw">ulong ticket;
◍ 按权益风险反推开仓手数的边界
这段逻辑解决一个实在问题:给定账户里愿意拿去扛保证金的权益比例(risk_equity),当前品种一手占用的 margin 已知,理论能开多少仓?基础算式就是 position_size = risk_equity / margin,先按最小手数估个总数。 硬边界写在代码里:持仓数不允许超过 10。若算出来 position_size > 10,就用数学向下取整 MathFloor(position_size / 10) 得到 estimated_lot_size,再把仓位重新摊薄为 risk_equity / (margin * estimated_lot_size)。这一步把“手数放大”和“持仓数压回 10 以内”绑死了。 放大倍数还受 max_volume_increase 约束。只有 estimated_lot_size 小于该上限时,才把 lot_multiplier 和 position_size 正式赋值为放大后的值;否则保持原样。外汇与贵金属杠杆高,这套上限能避免权益回撤时保证金被瞬间打穿,但仍是概率性保护,不保证不爆仓。 check_profit() 另有一条风控线:用 (权益-余额)/持仓数 算单仓均利,大于 profit_target 就触发反手;若总浮亏突破 risk_equity 的负向阈值,仅弹 Comment 提示,不自动砍仓。实盘前建议在 MT5 策略测试器里把 max_volume_increase 和 risk_equity 两个变量打印出来核对边界。
class="type">void calculate_lot_size() { class=class="str">"cmt">//--- This is the total percentage of the account we&class="macro">#x27;re willing to part with for margin, or to keep a position open in other words. Print("Risk Equity: ",risk_equity); class=class="str">"cmt">//--- Now that we&class="macro">#x27;re ready to part with a discrete amount for margin, how many positions can we afford under the current lot size? class=class="str">"cmt">//--- By class="kw">default we always start from minimum lot position_size = risk_equity / margin; class=class="str">"cmt">//--- We need to keep the number of positions lower than class="num">10 if(position_size > class="num">10) { class=class="str">"cmt">//--- How many times is it greater than class="num">10? class="type">int estimated_lot_size = (class="type">int) MathFloor(position_size / class="num">10); position_size = risk_equity / (margin * estimated_lot_size); Print("Position Size After Dividing By margin at new estimated lot size: ",position_size); class="type">int estimated_position_size = position_size; class=class="str">"cmt">//--- Can we increase the lot size this many times? if(estimated_lot_size < max_volume_increase) { Print("Est Lot Size: ",estimated_lot_size," Position Size: ",estimated_position_size); lot_multiplier = estimated_lot_size; position_size = estimated_position_size; } } } class="type">void close_all_and_enter() { if(PositionSelect(Symbol())) { class=class="str">"cmt">// Determine the type of position check_signal(); } else { Print("No open position found."); } } class="type">void check_profit() { class="type">class="kw">double current_profit = (AccountInfoDouble(ACCOUNT_EQUITY) - AccountInfoDouble(ACCOUNT_BALANCE)) / PositionsTotal(); if(current_profit > profit_target) { close_all_and_enter(); } if((current_profit * PositionsTotal()) < (risk_equity * -class="num">1)) { Comment("We&class="macro">#x27;ve breached our risk equity, consider closing all positions"); } }
倒序遍历持仓做盈利平仓
在 EA 的 OnTradeTransaction 或定时器里批量处理持仓时,必须倒序遍历 PositionsTotal(),否则平掉一个仓位后索引会错位,漏检后续单子。 下面这段逻辑就是典型的「从最后一个持仓往前扫」:先按 ticket 选中第 i 个仓位,再读它的浮盈,超过 profit_target 就直接市价平掉。
for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetDouble(POSITION_PROFIT)>profit_target) { class="type">class="kw">ulong ticket; ticket = PositionGetTicket(i); trade.PositionClose(ticket); } } } class=class="str">"cmt">//+------------------------------------------------------------------+
for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--) { if(PositionSelectByTicket(PositionGetTicket(i))) { if(PositionGetDouble(POSITION_PROFIT)>profit_target) { class="type">class="kw">ulong ticket; ticket = PositionGetTicket(i); trade.PositionClose(ticket); } } } class=class="str">"cmt">//+------------------------------------------------------------------+
「画得少,看得清」
回测里 MACD 主线与信号线的交叉策略在 EURUSD 上误差偏高,但这不构成对指标本身的否定。把视角换到牛熊区分——信号线在主线之上偏多头、之下偏空头——误差分布可能完全不同,不能默认所有读法同效。 附件里的 EURUSD_AI.mq5 与 EURUSD_MACD.ipynb 可直接在 MT5 跑一遍,对比两种解读的胜率差。外汇与贵金属杠杆高,任何信号都只是概率倾斜,验证前别下重仓。 真正该做的,是只挑一种 MACD 用法固化下来,用历史数据算它自己的误差区间,而不是拿一套交叉逻辑替整个指标判死刑。