在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓
交易者有可能正确预测市场的未来价格变化,但却以亏损平仓。在交易圈中,这通常被称为“被止损出局”。这个问题源于价格水平并非以直线和可预测的方式变化。
在图 1 中,我们为您提供了一张 EURUSD 货币对小时价格变化的快照。白色虚线垂直线标记了交易日的开始和结束。一位确信当天价格水平会下跌的交易者,本应正确预测了未来的价格变化。不幸的是,在价格下跌之前,它曾大幅上涨,这意味着如果我们交易者的止损位位于图 1 中高亮的红色区域内,那么他在正确预测了未来价格变化之后,仍会以亏损平仓。

图 1:可视化交易者通常被止损出局的情形
多年来,交易者们提出了各种解决方案来解决这个问题,但正如我们将在讨论中看到的,其中大多数都不是有效的解决方案。最常被引用的解决方案是简单地“放宽你的止损”。
这意味着,交易者应在波动性特别大的交易日放宽他们的止损,以防止被止损出局。但这是糟糕的建议,因为它鼓励交易者形成在类似交易上承担不同风险的习惯,而没有任何既定且明确定义的规则来指导他们的决策。
另一个常被引用的解决方案是“在进入交易前等待确认”。同样,考虑到我们所关注问题的性质,这也是一个糟糕的建议。人们可能会发现,等待确认只是推迟了被止损出局的过程,并不能完全解决问题。
简而言之,被止损出局的问题使交易者难以遵循稳健的风险管理原则,同时降低了交易时段的盈利能力,并给交易者带来了其他担忧。
因此,我们的目标是,为您(读者)提供更合理且明确定义的规则,以最大限度地减少您因盈利交易被止损而出局的频率。
我们提出的解决方案将与常见的解决方案背道而驰,并将鼓励您建立良好的交易习惯,例如保持止损规模固定,而不是像常被引用的解决方案那样“放宽你的止损”。
交易策略概览
我们的交易策略将是一个均值回归策略,由支撑和阻力水平与技术分析相结合组成。我们将首先使用前一天的高点和低点来标记我们感兴趣的价格水平。然后,我们将等待观察前一天的价格水平是否会在当天被突破。例如,如果前一天的高价在当天被新的高价突破,我们将寻找在市场中做空的机会,押注价格水平将回归其平均值。当我们观察到价格水平在成功收于前一日高点之上后,又收于移动平均线指标之上时,我们进入空头交易的信号就会变得明确。

图 2:可视化我们交易策略的实际运作
回测周期概述
为了分析我们对交易策略所提修改的有效性,我们必须首先有一个固定的时间段,以便在此期间比较我们对系统所做的更改。在本次讨论中,我们的测试将从 2022 年 1 月 1 日开始,直到 2025 年 1 月 1 日。所讨论的时段已在图 1 中高亮显示,请注意,在该图中我们正在月线时间框架上观察 EURUSD。 
图 3:我们将执行回测的时间段
我们实际的测试将在 M30(30分钟)时间框架上进行。在图 2 中,我们高亮显示了我们将用于测试的预期市场,以及我们之前讨论的时间段。这些设置将在本文的其余部分保持不变,因此有必要在此处进行讨论。对于所有后续的测试,我们将保持这些设置不变。此外,如果您想与我们同步操作,请确保选择 EURUSD,或者您偏好的任何交易品种。

图 4:我们的回测周期
此外,选择“基于真实 tick 的每个 tick”,以重现我们能创建的最准确的历史市场事件模拟。请注意,此设置将从您的经纪商那里获取相关数据,这可能需要相当长的时间,具体取决于您的网络状况。

图 5:我们将用于回测的账户设置
在 MQL5 中开始
既然我们已经熟悉了今天测试的回测周期,那么让我们首先建立一个将要超越的基础检测标准。我们将首先构建一个交易应用程序,以实现一个旨在交易突破的支撑阻力交易策略。首先,我们将导入交易库。
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
定义系统常量。这些常量帮助我们确保在所有测试中,我们对应用程序的行为都有明确的控制。
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average #define TF_1 PERIOD_D1 //--- Our time frame for technical analysis #define TF_2 PERIOD_M30 //--- Our time frame for managing positions #define VOL 0.1 //--- Our trading volume #define SL_SIZE 1e3 * _Point //--- The size of our stop loss
我们还需要一些全局变量来帮助我们跟踪前一天感兴趣的价格水平。
//+------------------------------------------------------------------+ //| Our global variables | //+------------------------------------------------------------------+ int ma_handler,system_state; double ma[]; double bid,ask,yesterday_high,yesterday_low; const string last_high = "LAST_HIGH"; const string last_low = "LAST_LOW";
当我们的应用程序首次加载时,设置我们所有的技术指标。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); }
如果我们的应用程序不再使用,就释放我们不再使用的技术指标。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- release(); }
每当收到更新的价格时,存储它们并重新计算我们的指标值。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); }
我们现在将定义在事件循环中调用的每个函数。首先,我们的更新函数将在两个不同的时间框架上工作。我们有必须每天执行一次的规则和程序,而其他的则必须在更短的时间间隔内执行。这种关注点分离由我们定义的两个系统常量 TF_1(日线时间框架)和 TF_2(M30 时间框架)为我们处理。诸如获取前一天的高点和低点之类的任务,每天只需要执行一次。另一方面,诸如搜索持仓之类的任务,只需要在每个新的 30 分钟K线形成时执行一次。
//+------------------------------------------------------------------+ //| Perform our update routines | //+------------------------------------------------------------------+ void update() { //--- Daily procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_1,0); if(time_stamp != current_time) { yesterday_high = iHigh(Symbol(),TF_1,1); yesterday_low = iLow(Symbol(),TF_1,1); //--- Mark yesterday's levels ObjectDelete(0,last_high); ObjectDelete(0,last_low); ObjectCreate(0,last_high,OBJ_HLINE,0,0,yesterday_high); ObjectCreate(0,last_low,OBJ_HLINE,0,0,yesterday_low); } } //--- M30 procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_2,0); if(time_stamp != current_time) { time_stamp = current_time; //--- Get updated prices bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); //--- Update our technical indicators CopyBuffer(ma_handler,0,0,1,ma); //--- Check for a setup if(PositionsTotal()==0) find_setup(); } } }
这个特定的应用程序仅依赖 1 个技术指标。因此,我们定义设置函数的过程很简单。
//+------------------------------------------------------------------+ //| Custom functions | //+------------------------------------------------------------------+ void setup(void) { ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,0,MA_TYPE,MA_PRICE); };
如果我们检测到当前的极端价格超过了前一天观察到的相反极端价格,那么我们开仓的条件就会满足。除了突破前一天设定的水平外,我们还希望根据价格与其移动平均线的关系来获得额外的确认。
//+------------------------------------------------------------------+ //| Check if we have any trading setups | //+------------------------------------------------------------------+ void find_setup(void) { if(iHigh(Symbol(),TF_2,1) < yesterday_low) { if(iClose(Symbol(),TF_2,1) < ma[0]) { Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } } if(iLow(Symbol(),TF_2,1) > yesterday_high) { if(iClose(Symbol(),TF_2,1) > ma[0]) { Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } } }
如果我们不再使用我们的EA,我们应该释放不再需要的系统资源。
//+------------------------------------------------------------------+ //| Free resources we are no longer using up | //+------------------------------------------------------------------+ void release(void) { IndicatorRelease(ma_handler); }
最后,在我们程序执行周期的末尾,我们将删除之前定义的系统常量。
//+------------------------------------------------------------------+ //| Undefine the system constants we created | //+------------------------------------------------------------------+ #undef TF_1 #undef TF_2 #undef VOL #undef SL_SIZE #undef MA_PERIOD #undef MA_PRICE #undef MA_TYPE
我们当前交易策略产生的资金曲线并不稳定。我们当前系统产生的余额呈现出随时间持续下降的趋势。我们希望一个策略是偶尔下跌,但总体上有随时间增长的趋势。因此,我们将保持用于开仓的规则不变,并尝试过滤掉那些我们认为会触及止损的交易。这项练习无疑将是具有挑战性的。然而,采取任何解决方案都比不采取要好。

图 6:我们当前版本交易策略产生的资金曲线
当我们分析交易策略的详细结果时,我们可以观察到我们的算法在 3 年的回测期间亏损超过了 1000 美元。这远非令人鼓舞的信息。此外,我们的平均亏损和最大亏损都超过了我们的平均盈利和最大盈利。这让我们对该策略未来的表现产生了负面的预期。因此,我们不希望以其当前形式使用该策略来交易真实资金的账户。

图 7:分析我们交易策略产生的详细结果
改进基本策略
我们防止爆仓策略的基础,在于之前系列文的讨论中提出的一个事实。希望回顾之前讨论的读者可以在这里找到它。总而言之,我们观察到,在我们的 MetaTrader 5 终端上超过 200 个不同的交易品种中,移动平均线技术指标似乎始终比直接预测价格更容易。
我们可以通过预测移动平均线的未来值是否会超过我们的止损水平,来很好地利用我们的观察。如果我们的计算机预期会出现这种情况,那么只要它预期移动平均线会触及我们的止损,就不应该进行任何交易,否则我们的应用程序将被允许进行交易。
这就是我们解决方案的精髓。它从头到尾都定义明确,并且基于稳健的原则和客观的推理。请注意,我们甚至可以更具体地要求,除了不预期止损会被触发外,我们的计算机还应该预期移动平均线会超过我们的止盈水平,然后才能进行任何交易。否则,如果他们没有理由相信自己的止盈订单会被成交,为什么还要进行交易呢?
作为开始,我们首先需要使用 MQL5 脚本从我们的 MetaTrader 5 终端获取相关的市场数据。
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- Define our moving average indicator #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average //--- Our handlers for our indicators int ma_handle; //--- Data structures to store the readings from our indicators double ma_reading[]; //--- File name string file_name = Symbol() + " Stop Out Prevention Market Data.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_M30,MA_PERIOD,0,MA_TYPE,MA_PRICE); //---Set the values as series CopyBuffer(ma_handle,0,0,size,ma_reading); ArraySetAsSeries(ma_reading,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","Open","High","Low","Close","MA 14"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), ma_reading[i]); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
在 Python 中分析我们的数据
在您将脚本应用到您选择的市场后,我们可以开始使用 Python 库来分析我们的金融数据。我们的目标是构建一个神经网络,它可以帮助我们预测移动平均线指标的未来值,并可能让我们避免亏损的交易。
import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
现在读取我们从终端提取的数据。
data = pd.read_csv("EURUSD Stop Out Prevention Market Data.csv")
data 对数据进行标记。
LOOK_AHEAD = 48 data['Target'] = data['MA 14'].shift(-LOOK_AHEAD) data.dropna(inplace=True) data.reset_index(drop=True,inplace=True)
舍弃与我们回测重叠的时间段。
#Let's entirely drop off the last 2 years of data data.iloc[-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)):,:]
用不包含我们回测周期内观测值的新数据覆盖原始市场数据。
#Let's entirely drop off the last 2 years of data _ = data.iloc[-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)):,:] data = data.iloc[:-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)),:] data
现在加载我们的机器学习库。
from sklearn.neural_network import MLPRegressor from sklearn.model_selection import train_test_split,TimeSeriesSplit,cross_val_score创建一个时间序列分割对象,以便我们能快速交叉验证我们的模型。
tscv = TimeSeriesSplit(n_splits=5,gap=LOOK_AHEAD) 指定输入和目标。
X = data.columns[1:-1] y = data.columns[-1:]将数据分成两半,用于训练和测试新模型。
train , test = train_test_split(data,test_size=0.5,shuffle=False)
准备训练和测试分区以进行归一化和缩放。
train_X = train.loc[:,X] train_y = train.loc[:,y] test_X = test.loc[:,X] test_y = test.loc[:,y]
计算我们 z-scores 的参数。
mean_scores = train_X.mean() std_scores = train_X.std()归一化模型的输入数据。
train_X = ((train_X - mean_scores) / std_scores) test_X = ((test_X - mean_scores) / std_scores)
我们希望对深度神经网络的最佳训练迭代次数执行线性搜索。我们将以 2 的幂递增遍历。从 2 的 0 次方开始,直到 2 的 14 次方。
MAX_POWER = 15 results = pd.DataFrame(index=["Train","Test"],columns=[np.arange(0,MAX_POWER)])
定义一个 for 循环,它将帮助我们估算将深度神经网络模型拟合到我们数据上所需的最优训练迭代次数。
#Classical Inputs for i in np.arange(0,MAX_POWER): print(i) model = MLPRegressor(hidden_layer_sizes=(5,10,4,2),solver="adam",activation="relu",max_iter=(2**i),early_stopping=False) results.iloc[0,i] = np.mean(np.abs(cross_val_score(model,train_X.loc[:,:],train_y.values.ravel(),cv=tscv))) results.iloc[1,i] = np.mean(np.abs(cross_val_score(model,test_X.loc[:,:],test_y.values.ravel(),cv=tscv))) results
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 训练 | 19675.492496 | 19765.297106 | 19609.7644 | 19511.588484 | 19859.734807 | 19942.30371 | 18831.617167 | 10703.554068 | 4930.771654 | 1639.952482 | 1389.615052 | 2938.371438 | 1.536765 | 2.193895 | 30.553918 |
| 测试 | 13171.519137 | 14113.252994 | 14428.159203 | 13649.157525 | 13655.643066 | 12919.773346 | 11472.770729 | 5878.964564 | 11293.444345 | 3788.388634 | 2545.368419 | 3599.364028 | 2240.598518 | 1041.641869 | 882.696622 |
可视化地绘制数据表明,我们需要采用最大迭代次数才能从我们的模型中获得最优输出。然而,读者也应该对一种可能性持开放态度,即我们的搜索程序可能过早地终止了。这意味着,如果我们使用大于 14 的 2 的幂,我们本可能获得更好的结果。但是,由于训练这些模型的计算成本,我们的搜索没有超过 2 的 14 次方。
plt.title("Neural Network RMSE Forecasting 14 Period MA") plt.ylabel("5 CV RMSE") plt.xlabel("Training Iterations As Powers of 2") plt.grid() sns.lineplot(np.array(results.iloc[1,:]).transpose()) plt.axhline(results.min(1)[1],linestyle='--',color='red') plt.axvline(14,linestyle='--',color='red')

图 8:为我们的深度神经网络模型搜索最优训练迭代次数的结果
既然我们的模型已经训练完成,我们现在可以准备将模型导出为 ONNX 格式。
import onnx import skl2onnx from skl2onnx.common.data_types import FloatTensorType
准备使用我们估算出的最优训练迭代次数来拟合模型。
model = MLPRegressor(hidden_layer_sizes=(5,10,4,2),solver="adam",activation="relu",max_iter=(2**14),early_stopping=False)
加载整个数据集的 z-scores。
mean_scores = data.loc[:,X].mean() std_scores = data.loc[:,X].std() mean_scores.to_csv("EURUSD StopOut Mean.csv") std_scores.to_csv("EURUSD StopOut Std.csv")
转换整个数据集。
data[X] = ((data.loc[:,X] - mean_scores) / std_scores)
在我们拥有的所有数据上拟合模型,不包括测试日期。
model.fit(data.loc[:,X],data.loc[:,'Target'].values.ravel())
指定我们模型的输入形状。
initial_types = [("float_input",FloatTensorType([1,5]))]
准备将模型转换为 ONNX 格式。
model_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=12) 将模型保存为 ONNX 文件。
onnx.save(model_proto,"EURUSD StopOut Prevention Model.onnx") 构建我们策略的改进版本
让我们开始构建我们交易策略的新改进版本。首先,加载我们刚刚创建好的 ONNX 模型。
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD StopOut Prevention Model.onnx" as uchar onnx_model_buffer[];
我们将为这个版本的应用程序创建一些额外的系统常量。
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average #define TF_1 PERIOD_D1 //--- Our time frame for technical analysis #define TF_2 PERIOD_M30 //--- Our time frame for managing positions #define VOL 0.1 //--- Our trading volume #define SL_SIZE 1e3 * _Point //--- The size of our stop loss #define SL_ADJUSTMENT 1e-5 * _Point //--- The step size for our trailing stop #define ONNX_MODEL_INPUTS 5 //---- Total model inputs for our ONNX model
此外,我们的全局 z-scores 必须被加载到数组中。
//+------------------------------------------------------------------+ //| Our global variables | //+------------------------------------------------------------------+ int ma_handler,system_state; double ma[]; double mean_values[ONNX_MODEL_INPUTS] = {1.157641086508574,1.1581085911361018,1.1571729541088953,1.1576420747040126,1.157640521193191}; double std_values[ONNX_MODEL_INPUTS] = {0.04070388112283021,0.040730761156963606,0.04067819202368064,0.040703752648947544,0.040684857239172416}; double bid,ask,yesterday_high,yesterday_low; const string last_high = "LAST_HIGH"; const string last_low = "LAST_LOW"; long onnx_model; vectorf model_forecast = vectorf::Zeros(1);
在我们能够使用 ONNX 模型之前,我们必须首先相应地设置这些模型,并检查它们是否已正确配置。
//+------------------------------------------------------------------+ //| Prepare the resources our EA requires | //+------------------------------------------------------------------+ bool setup(void) { onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT); if(onnx_model == INVALID_HANDLE) { Comment("Failed to create ONNX model: ",GetLastError()); return(false); } ulong input_shape[] = {1,ONNX_MODEL_INPUTS}; ulong output_shape[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,input_shape)) { Comment("Failed to set ONNX model input shape: ",GetLastError()); return(false); } if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { Comment("Failed to set ONNX model output shape: ",GetLastError()); return(false); } ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,0,MA_TYPE,MA_PRICE); if(ma_handler == INVALID_HANDLE) { Comment("Failed to load technical indicator: ",GetLastError()); return(false); } return(true); };
我们寻找交易设置的过程将发生一些轻微的变化。首先,我们将从我们的模型中获取一个预测。之后,我们开仓和平仓的条件保持不变。然而,除了满足这些条件外,我们还将检查新的条件是否得到满足。
//+------------------------------------------------------------------+ //| Check if we have any trading setups | //+------------------------------------------------------------------+ void find_setup(void) { if(!model_predict()) { Comment("Failed to get a forecast from our model"); return; } if((iHigh(Symbol(),TF_2,1) < yesterday_low) && (iHigh(Symbol(),TF_2,2) < yesterday_low)) { if(iClose(Symbol(),TF_2,1) > ma[0]) { check_buy(); } } if((iLow(Symbol(),TF_2,1) > yesterday_high) && (iLow(Symbol(),TF_2,2) > yesterday_high)) { if(iClose(Symbol(),TF_2,1) < ma[0]) { check_sell(); } } }
我们需要指定的新条件将同时适用于我们的多头和空头头寸。首先,我们将检查我们对移动平均线的预测是否大于我们当前可用的移动平均线指标的读数。此外,我们还将检查移动平均线指标的未来预期值是否大于当前提供的价格读数。
这意味着我们的计算机怀疑趋势很可能继续朝一个方向发展。最后,我们将检查我们的计算机是否预期移动平均线将保持在止损位之下。如果我们所有的条件都得到满足,那么我们将立即在市场上开仓。
//+------------------------------------------------------------------+ //| Check if we have a valid buy setup | //+------------------------------------------------------------------+ void check_buy(void) { if((model_forecast[0] > ma[0]) && (model_forecast[0] > iClose(Symbol(),TF_2,0))) { if(model_forecast[0] > (bid - (SL_SIZE))) Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } }
开空单的条件和我们开多单的一样,只是他们开仓方向相反。
//+------------------------------------------------------------------+ //| Check if we have a valid sell setup | //+------------------------------------------------------------------+ void check_sell(void) { if((model_forecast[0] < ma[0]) && (model_forecast[0] < iClose(Symbol(),TF_2,0))) { if(model_forecast[0] < (ask + (SL_SIZE))) Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } }
一旦我们开仓,就必须继续监控它。我们的更新止损函数将根据其调用方式服务于两个目的。它接受一个标志参数,该参数会修改其行为。如果标志设置为 0,我们只是在寻找机会将我们的止损水平推向更有利可图的价格。否则,如果标志设置为 1,我们希望首先从模型中获取一个新的预测,并检查移动平均线的未来值是否会超过我们当前的止损水平。
如果预期移动平均线会超过我们的止损位,但仍能形成有利可图的变动,那么我们将把我们的止损调整到我们预期移动平均线在达到峰值时会触及的水平。否则,如果预期交易会跌破其开仓价格,那么我们希望指示我们的计算机在那些盈利潜力很小的交易上承担更小的风险。
//+------------------------------------------------------------------+ //| Update our stop loss | //+------------------------------------------------------------------+ void update_sl(int flag) { //--- First find our open position if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); double open_price = PositionGetDouble(POSITION_PRICE_OPEN); //--- Flag 0 means we just want to push the stop loss and take profit forward if its possible if(flag == 0) { //--- Buy Setup if(current_tp > current_sl) { if((bid - SL_SIZE) > current_sl) Trade.PositionModify(Symbol(),(bid - SL_SIZE),(bid + SL_SIZE)); } //--- Sell setup if(current_tp < current_sl) { if((ask + SL_SIZE) < current_sl) Trade.PositionModify(Symbol(),(ask + SL_SIZE),(ask - SL_SIZE)); } } //--- Flag 1 means we want to check if the stop loss may be hit soon, and act accordingly if(flag == 1) { model_predict(); //--- Buy setup if(current_tp > current_sl) { if(model_forecast[0] < current_sl) { if((model_forecast[0] > ma[0]) && (model_forecast[0] > yesterday_low)) Trade.PositionModify(Symbol(),model_forecast[0],current_tp); } if(model_forecast[0] < open_price) Trade.PositionModify(Symbol(),model_forecast[0] * 1.5,current_tp); } //--- Sell setup if(current_tp < current_sl) { if(model_forecast[0] > current_sl) { if((model_forecast[0] < ma[0]) && (model_forecast[0] < yesterday_high)) Trade.PositionModify(Symbol(),model_forecast[0],current_tp); } if(model_forecast[0] > open_price) Trade.PositionModify(Symbol(),model_forecast[0] * 0.5,current_tp); } } } }
我们的更新程序稍作修改,以调用更新止损函数。
//+------------------------------------------------------------------+ //| Perform our update routines | //+------------------------------------------------------------------+ void update() { //--- Daily procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_1,0); if(time_stamp != current_time) { yesterday_high = iHigh(Symbol(),TF_1,1); yesterday_low = iLow(Symbol(),TF_1,1); //--- Mark yesterday's levels ObjectDelete(0,last_high); ObjectDelete(0,last_low); ObjectCreate(0,last_high,OBJ_HLINE,0,0,yesterday_high); ObjectCreate(0,last_low,OBJ_HLINE,0,0,yesterday_low); } } //--- M30 procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_2,0); if(time_stamp != current_time) { time_stamp = current_time; //--- Get updated prices bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); //--- Update our technical indicators CopyBuffer(ma_handler,0,0,1,ma); //--- Check for a setup if(PositionsTotal()==0) find_setup(); //--- Check for a setup if(PositionsTotal() > 0) update_sl(1); } } //--- Per tick procedures { //--- These function calls can become expensive and may slow down the speed of your back tests //--- Be thoughtful when placing any function calls in this scope update_sl(0); } }
我们还需要一个专门的函数,负责从我们的神经网络模型中获取预测。我们将首先把输入准备成一个浮点向量类型,然后对输入进行标准化,这样我们就可以从模型中获取预测。
//+------------------------------------------------------------------+ //| Get a forecast from our deep neural network | //+------------------------------------------------------------------+ bool model_predict(void) { double ma_input[] = {0}; CopyBuffer(ma_handler,0,1,1,ma_input); vectorf model_inputs = { (float) iOpen(Symbol(),TF_2,1), (float) iHigh(Symbol(),TF_2,1), (float) iLow(Symbol(),TF_2,1), (float) iClose(Symbol(),TF_2,1), (float) ma_input[0] }; for(int i = 0; i < ONNX_MODEL_INPUTS;i++) { model_inputs[i] = (float)((model_inputs[i] - mean_values[i]) / std_values[i]); } if(!OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast)) { Comment("Failed to obtain forecast: ",GetLastError()); return(false); } Comment(StringFormat("Expected MA Value: %f",model_forecast[0])); return(true); }
最后,当我们的应用程序不再使用时,我们将释放该指标和 ONNX 模型。
//+------------------------------------------------------------------+ //| Free resources we are no longer using up | //+------------------------------------------------------------------+ void release(void) { OnnxRelease(onnx_model); IndicatorRelease(ma_handler); } //+------------------------------------------------------------------+
当我们分析由我们交易算法的新改进版本产生的权益曲线时,我们可以快速观察到,在策略的第一个实现中观察到的特征性负斜率已被修正,我们的策略现在呈现出积极的趋势,并伴有偶有回落。这比我们策略的初始状态更理想。

图 9:可视化我们新的改进版止损预防算法产生的利润曲线
经过仔细检查,我们发现我们的新策略现在是盈利的。我们策略的初始版本亏损了大约 1000 美元,而我们当前的版本则赚取了略多于 1000 美元的利润。这是一个重大的改进。我们最初的夏普比率是 -0.39,而我们新的夏普比率是 0.79。读者还会注意到,我们的平均盈利交易从 98 美元增长到 130 美元,而平均亏损交易则从 102 美元下降到 63 美元。这表明我们的平均利润增长速度明显快于我们的平均亏损。如果我们考虑使用这个版本的交易策略,这些指标给了我们积极的预期。
尽管我们取得了重大进展,但被止损出局的问题显然仍然是一个难以解决的问题。这一点从我们开立的头寸中约有 60% 是亏损交易这一事实中可以明显看出。试图完全过滤掉所有会导致交易者被止损的交易是具有挑战性的,因为今天我们成功地过滤掉了大部分大型且不盈利的交易。

图 10:我们使用新的止损预防算法所获结果的详细分析
结论
在本文中,我们引导读者了解了一个针对长期存在的“盈利交易被止损出局”问题的潜在解决方案。这个问题是成功交易的核心,并且可能永远无法完全解决。每个新的解决方案都会给我们的策略带来其自身的一系列漏洞。在阅读本文后,读者将获得一个用于管理其止损水平的更量化的框架。识别并过滤掉那些会不必要地侵蚀您账户资金的交易,是任何交易策略的关键组成部分。
| 文件名 | 文件描述 |
|---|---|
| Baseline Model.mq5 | 我们旨在超越的原始交易策略。 |
| Stop Out Prevention Model.mq5 | 我们由深度神经网络驱动的交易策略的改进版本。 |
| EURUSD Stop Out Moving Average Model.ipynb | 我们用来分析从 MetaTrader 5 终端提取的金融数据的 Jupyter Notebook。 |
| EURUSD Stop Out Prevention Model.onnx | 我们的深度神经网络。 |
| Fetch Data MA.mq5 | 我们用来获取所需市场数据的 MQL5 脚本。 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/17213
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
MQL5交易策略自动化(第八部分):构建基于蝴蝶谐波形态的智能交易系统(EA)
算法交易中的神经符号化系统:结合符号化规则和神经网络
市场模拟(第一部分):跨期订单(一)
价格行为分析工具包开发(第十四部分):抛物线转向与反转工具