克服机器学习的局限性(第二部分):缺乏可重复性
我常收到读者的积极反馈,但私信和评论中反复出现的一个问题是,一些读者在尝试复现我们文章中展示的结果时遇到了困难。起初,这让我感到困惑,但是经过一番思考,我找到了一个有可能的解释。
全球金融市场是一个庞大且分散的网络。世界上有许多经纪商,且每天都有新的经纪商注册,然而并没有一个单一的国际权威机构来监管这些经纪商或协调他们的价格数据。每个经纪商都可以自由地从他们偏好的专有信息源或数据服务(例如路透社)获取价格。
因此,如果您对比两家经纪商(不妨称他们为经纪商A和经纪商B)的欧元兑美元(EURUSD)表现,您可能会发现同一时刻同一货币对的走势相反。例如,经纪商A可能报告欧元兑美元在一天内升值0.12%,而经纪商B则记录同一货币对在同一天贬值-0.65%。
问题的核心:经纪商之间的数据差异
为了便于讨论,我随机选取了用于独立交易的两家经纪商。根据我们的社区准则,禁止推广经纪商,因此将他们的名字隐去,以“经纪商A”和“经纪商B”取而代之。
我使用MetaTrader 5的Python库,从这两家经纪商处获取了四年的欧元兑美元每日历史数据。复核后,我注意到时间戳并不一致:一家经纪商的数据可追溯到2019年9月,而另一家仅到2020年8月。尽管如此,两家经纪商都返回了1460行每日数据,精准地满足了我们的要求。
鉴于经纪商的分散性,他们的运营时区可能不同,这在意料之中。然而,但容易被忽视的是,夏令时转换、公共假期安排以及其他细微差异,都会导致时间戳无法完全对齐。
然后,我们计算了两家经纪商的10日欧元兑美元收益率,同一交易品种的数据特征存在显著差异。经纪商A的10日欧元兑美元平均收益率为0.000267,而经纪商B的平均10日收益率为-0.000352。这就意味着,对于同一标的资产,两者的预期收益率竟然相差了2.3倍之多。
更糟糕的是,经纪商A的预期收益率似乎比经纪商B的预期收益率风险高出21%。从两家经纪商之间的收益率方差也增长了相同的比例(即21%),可以看出这一点。
初学者须知:读者应理解收益率的方差被视为金融风险。对于可能事先不了解这一原则的读者,可以参考任何金融投资组合理论的入门教材。
在统计学中,我们可以通过测量两个变量之间的相关性水平,来探究它们是同向变动还是独立变动。标准化的相关度量范围从1到-1。得分为1意味着变量完美同向变动,而得分为-1则意味着变量完美反向变动。当我们比较两家经纪商的皮尔逊相关系数时,我原本期望相关系数接近1。然而,数据显示的相关性水平仅为0.41。
这就表明,任何关于不同经纪商的欧元兑美元价格水平会同步变动的想法,从数学角度似乎都是没有根据的。相反,我们的测试结果表明,超过一半的时间里,不同经纪商欧元兑美元的市场走势是不同的。
通过比较两家经纪商报价的其他重要数值特性,进一步凸显了本文要向读者揭示的问题的严重性。在之前关于人工智能(AI)的局限性讨论中,我们向读者展示了构建回归模型时常用的一些指标(如均方根误差RMSE)所存在的陷阱。读者可点击此处,查找相关文章的链接。
简而言之,我们建议读者不要将均方根误差RMSE作为一个独立指标来解读,而应谨慎对待这一指标,通过将您打算使用的模型(残差平方和RSS)性能与一个总是预测平均市场收益率的简单模型(总平方和TSS)所产生的误差进行比较来解读。关键在于,读者可能会惊讶地发现,要超越这个简单模型是多么具有挑战性。残差平方和RSS除以总平方和TSS的比率,能告诉我们我们超越简单模型的效率如何。
对于同一标的,人们会认为,即便在不同的经纪商那里,这个比率也应基本保持不变。然而,仅仅通过更换经纪商,我们超越预测平均市场收益率模型的性能就提高了7%。这就意味着,与经纪商A相比,使用经纪商B直接预测10日欧元兑美元收益率要容易约7%!
统计学家经常将分布的中心与标准差进行比较,以进一步了解给定分布尾部的特征。当我们将这一操作重新解读并应用于10日欧元兑美元收益率时,我们就能找到一种数值方法,来比较哪家经纪商更倾向于产生超常收益率。按照这一思路,经纪商B的10日欧元兑美元收益率似乎被夸大了147%。
至此,我们面临的问题应该已经很清楚了:同一标的的重要数值特性并不能保证在不同经纪商之间保持一致。因此,任何给定交易策略的盈利能力,在不同经纪商之间并不总能直接被复制。
采用通过ONNX API构建的AI模型,甚至直接在MQL5中构建的人工智能模型的交易策略,除非广泛采用额外花时间针对目标经纪商独特定制人工智能的做法,否则可能始终无法满足投资者的期望。虽然这是一项耗时的工作,但显然也是一项至关重要的工作。
在您阅读本文时,我们将逐步重现大多数MQL5开发者可能遵循的生产周期。我们的目的是说明,当开发者使用自己的私人经纪商(在我们的例子中是经纪商B)构建并优化其应用程序,但客户却使用不同的经纪商(经纪商A)部署该应用程序时,那么开发者和客户很快就会遇到麻烦。对于遵循这种方式的开发者,其产品很可能会收到褒贬不一的评价。
为了避免出现这种不尽如人意的表现水平,希望提供可靠服务的MQL5开发者需要认识到,为了保障我们在市场上想要服务的客户的安全,最好提供针对特定经纪商定制的策略和应用程序。
开始
首先,我们需要导入常用的数据分析库。
#Load our libraries import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import MetaTrader5 as mt5
设定我们要关注的时间周期、交易品种以及所需的数据长度。
#Let us define certain constants TF = mt5.TIMEFRAME_D1 DATA = (365 * 4) START = 1 PAIR = "EURUSD"
启动您的终端。
#Log in to the terminal if mt5.initialize(): print('Logged in successfully') else: print('Failed To Log In')
登录成功
让我们来分析一下使用经纪商A预测欧元兑美元汇率的难度。
EURUSD_BROKER_A = pd.DataFrame(mt5.copy_rates_from_pos(PAIR,TF,START,DATA)) #Store the data we retrieved from broker A EURUSD_BROKER_A.to_csv("EURUSD BROKER A.csv")
现在,我们将对经纪商B重复同样的流程。
#I have manually changed brokers using the MT5 terminal, you should also do the same on your side EURUSD_BROKER_B = pd.DataFrame(mt5.copy_rates_from_pos(PAIR,TF,START,DATA)) #Store the data we retrieved from broker B EURUSD_BROKER_B.to_csv("EURUSD BROKER B.csv")
太棒了。现在,我们已经从两家经纪商处收集了欧元兑美元的历史数据,接下来,让我们开始研究这些数据集的实证特性,看看不同经纪商的欧元兑美元标的是否一致。我们需要确定预测的目标时间跨度。
#Our forecasting horizon HORIZON = 10
读取两个数据集。
EURUSD_BROKER_A = pd.read_csv("EURUSD BROKER A.csv") EURUSD_BROKER_B = pd.read_csv("EURUSD BROKER B.csv")
当前数据集的时间戳是以秒为单位,我们更希望采用人们习惯的日期-月份-年份格式。让我们创建一个方法来实现这一点。
def format_data(f_data): #First make a copy of the data, so we always preserve the original data f_data_copy = f_data.copy() #Format the time correctly, form seconds to human readable formats f_data_copy['time'] = pd.to_datetime(f_data_copy['time'],unit='s') return(f_data_copy)
格式化我们的数据集。
A = format_data(EURUSD_BROKER_A) B = format_data(EURUSD_BROKER_B)
将所有列名相应重命名,以便每列的名称都加上提供数据经纪商的字母后缀。来自经纪商A或B的所有列将分别以A或B结尾。现在,让我们仔细查看从两家经纪商处获得的欧元兑美元历史数据。请注意,这两组数据都包含1460行日数据,这意味着每家经纪商都恰好正确地返回了4年的日数据。作为读者还能观察到其他什么差异吗?您查看过交易量了吗?
# Rename all columns (except the join key) B = B.rename(columns=lambda col: col + ' B' if col != 'id' else col) A = A.rename(columns=lambda col: col + ' A' if col != 'id' else col)

图1:我们从经纪商A处获取的欧元兑美元历史日线数据,时间戳最早可追溯至2019年9月

图2:我们从经纪商B处获取的欧元兑美元历史日线数据,其时间戳与图1中的并不一致,但两者均恰好涵盖4年数据
现在,让我们合并两个数据集。
combined = pd.concat([A,B],axis=1) 创建一个全为0的列。
combined['Null'] = 0
定义输入。
inputs = ['open A','high A','low A','close A','tick_volume A','spread A','open B','high B','low B','close B','tick_volume B','spread B']
计算欧元兑美元的10天回报率。
#Label the data combined['A Target'] = combined['close A'].shift(-HORIZON) - combined['close A'] combined['B Target'] = combined['close B'].shift(-HORIZON) - combined['close B'] #Drop the last HORIZON rows of data combined = combined.iloc[:-HORIZON,:]
成交量反映了特定时间段内价格变动的频次。交易活跃的时期会通过高交易量显现出来,这表明市场活动频繁;相反,低交易量则意味着市场相对平静,活动较少。经纪商A的交易量数据呈现出长期上升趋势,这表明投资者持有的未平仓头寸似乎在随时间增长。图中偶尔会出现大幅峰值,这可能对应于交易特别繁忙的时期,此时欧元兑美元的未平仓头寸接近最大值。
plt.title('Broker A Daily EURUSD Tick Volume')
plt.plot(combined['tick_volume A'],color='black')
plt.ylabel('Tick Volume')
plt.xlabel('Historical Day')
plt.grid() 
图3:我们从经纪商A处获取的欧元兑美元交易量数据
当我们对比图4中经纪商B提供的交易量数据与图3中经纪商A的数据时,可以明显看出两者在报告的活动水平上存在巨大差异。与经纪商A相比,经纪商B的交易量数据几乎没有任何趋势。图4中数据密集,且存在随机出现的峰值,这些峰值并不像图3中观察到的峰值那样具有周期性。
plt.title('Broker B Daily EURUSD Tick Volume')
plt.plot(combined['tick_volume B'],color='black')
plt.ylabel('Tick Volume')
plt.xlabel('Historical Day')
plt.grid() 
图4:我们从经纪商B处获取的欧元兑美元日交易量数据
当我们考虑投资者持有欧元兑美元,从每家经纪商处可能获得的平均收益时,我们发现,这两家经纪商提供的虽然是同一交易品种,但具体版本却存在差异。否则,如果这两家经纪商提供的是完全相同的交易品种版本,那么我们获得的预期收益水平难道不应该是一致的吗?
#What's the average 10-Day EURUSD return from both brokers delta_return = str(((combined.iloc[:,-2:].mean()[0]-combined.iloc[:,-2:].mean()[1]) / combined.iloc[:,-2:].mean()[0]) * 100) t = 'The Expected 10-Day EURUSD Return Differes by ' + delta_return[:5] + '% Between Our Brokers' sns.barplot(combined.iloc[:,-2:].mean(),color='black') plt.axhline(0,color='grey',linestyle='--') plt.title(t) plt.ylabel('Return')

图5:两家经纪商的平均市场收益率分处零值两侧
让我们将两个市场产生的收益率曲线叠加绘制在一起。我将对收益率数据进行标准化处理,使两条曲线均以零为中心,偏离零值的距离代表我们相对于平均市场收益率的偏离标准差数。我们可以立即观察到,存在许多时刻两条曲线分处零值线两侧,但也有其他时候两条曲线走势相互跟随。回想一下,我们通常倾向于认为这两条曲线总是相互跟随的,但图6却表明,这种情形仅在部分时间内成立。
plt.plot(((combined.iloc[:,-1]-combined.iloc[:,-1].mean())/combined.iloc[:,-1].std()),color='red') plt.plot(((combined.iloc[:,-2]-combined.iloc[:,-2].mean())/combined.iloc[:,-2].std()),color='black') plt.grid() plt.axhline(0,color='black',linestyle='--') plt.ylabel('Std. Deviations From Expected 10-Day EURUSD Return') plt.xlabel('Historical Days') plt.title('EURUSD Returns from Different Brokers May Not Always Allign') plt.legend(['Broker A','Broker B'])

图6:两家不同经纪商生成的欧元兑美元10日收益率可视化对比
通过比较两家经纪商提供的收益率波动程度,我们能够评估哪家经纪商的风险更高,以及哪家经纪商提供的收益率更为稳定可靠。根据这一衡量标准,与经纪商B相比,经纪商A提供的欧元兑美元收益率伴随着更高的风险。
#The variance of returns is not the same across both brokers, broker A is riskier delta_var = str(((combined.iloc[:,-2:].var()[0]-combined.iloc[:,-2:].var()[1]) / combined.iloc[:,-2:].var()[0]) * 100) t = 'Broker A EURUSD Returns Appear to Carry '+ delta_var[:5]+'% Additional Risk.' sns.barplot(combined.iloc[:,-2:].var(),color='black') plt.axhline(np.min(combined.iloc[:,-2:].var()),color='red',linestyle=':') plt.title(t) plt.ylabel('Vriance of Returns')

图7:经纪商A的收益率风险比经纪商B高出21%,此时,您是否仍认为这两个交易品种是“相同的”?
当我们转而关注两家经纪商记录的最大回撤时,仍无法得出一致的观察结论。两家经纪商所呈现的市场最大回撤幅度相差约37%。所有迹象似乎表明,经纪商B提供的行情数据波动性更低,相当于给客户过滤”掉了部分欧元兑美元市场的剧烈波动。
#Broker A also demonstrated the largest drawdown ever in our 4 year sample window delta = (((combined.iloc[:,-2:].min()[0]-combined.iloc[:,-2:].min()[1]) / combined.iloc[:,-2:].min()[0]) *100) delta_s = str(delta) t = 'The Largest Negative 10-Day EURUSD Return Grew By: ' + delta_s[:5] + ' %' sns.barplot(combined.iloc[:,-2:].min(),color='black') plt.axhline(np.max(combined.iloc[:,-2:].min()),color='red',linestyle=':') plt.title(t) plt.ylabel('Return')

图8:经纪商A的收益率最大回撤达36.79%,远超经纪商B的最大回撤幅度
将两家经纪商生成的欧元兑美元10日收益率分布进行叠加对比后发现,两家经纪商提供的市场视角确实截然不同。正如我们在讨论引言中所述,各经纪商可以自由选择任意数据源获取价格信息。这种去中心化的定价机制意味着,不同经纪商对同一市场的呈现可能存在显著差异。
sns.histplot(((combined.iloc[:,-2]-combined.iloc[:,-2].mean())/combined.iloc[:,-2].std()),color='black') sns.histplot(((combined.iloc[:,-1]-combined.iloc[:,-1].mean())/combined.iloc[:,-1].std()),color='red') plt.xlabel('Std. Deviations From The Expected Return') plt.ylabel('Frequency') plt.title('Comparing The Distribution of 10-Day EURUSD Returns Between 2 Brokers') plt.grid() plt.legend(['Broker A','Broker B'])

图9:对比两个市场产生的收益率分布
此外,当我们分析两家经纪商之间的价格相关性时,发现市场价格之间的相关性很低。这就意味着,正如我们之前所述,在超过一半的时间里,这两家特定经纪商的价格水平可能朝着相反的方向变动。
sns.heatmap(combined.loc[:,inputs].corr(),annot=True)

图10:相关性水平可视化显示,两家经纪商的交易品种在大多数时候几乎独立波动
现在,让我们看一下预测能力在两家经纪商之间是否保持一致。
from sklearn.model_selection import train_test_split,TimeSeriesSplit,cross_val_score from sklearn.linear_model import Ridge
创建时间序列验证对象。
tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON) 编写一个方法,该方法将返回一个可供我们使用的新模型。
def get_model(): return(Ridge())分割数据,且确保不进行随机打乱。
train , test = train_test_split(combined,shuffle=False,test_size=0.5)
记录我们使用刻意填充为全0的列时的误差水平。这将迫使模型始终预测目标值的平均值。回想一下,当所有输入均为0时,线性模型将预测截距项。或者简单来说,这个模型告诉我们,如果我们总是预测市场平均收益率,在这个市场中的表现会如何。如果无法超越这个模型,就说明我们不具备预测能力。
这个基准被称为TSS(零策略标准)。我们在讨论引言中定义了TSS。我们的目标是在两家经纪商中分别测量TSS,然后比较在不同经纪商中超越该基准的能力。
broker_a_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,['Null']],train.loc[:,'A Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv))) broker_a_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[0:(len(inputs)//2)]],train.loc[:,'A Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv))) broker_b_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,['Null']],train.loc[:,'B Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv))) broker_b_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[(len(inputs)//2):]],train.loc[:,'B Target'],scoring='neg_mean_squared_error',n_jobs=-1,cv=tscv)))
令人惊讶的是,我们在经纪商B上超越TSS基准要比在经纪商A上容易得多!这就意味着当我们从一家经纪商切换到另一家时,未来10日欧元兑美元的收益率并不总是保持同等效率。
res = [(broker_a_rss/broker_a_tss),(broker_b_rss/broker_b_tss)] eff = str(((res[0] - res[1])/res[1]) * 100) t = 'The EURUSD Appears ' + eff[0:4] + '% Easier To Forecast With Broker B' sns.barplot(res,color='black') plt.axhline(np.min(res),color='red',linestyle=':') plt.ylabel('5-Fold Cross Valiated Ratio of RSS/TSS ') plt.title(t) plt.xticks([0,1],['Broker A','Broker B'])

图11:使用经纪商B的数据更易于预测未来10日欧元兑美元的收益率
既然已经确定了我们要聚焦的经纪商,接下来请选择从经纪商B处获取的输入数据。
b_inputs = inputs[len(inputs)//2:] 现在,让我们彻底构建一个全新的模型。
from sklearn.ensemble import GradientBoostingRegressor model = GradientBoostingRegressor()
使用我们从经纪商B处获取的所有数据来拟合该模型。
model.fit(train.loc[:,b_inputs[:-2]],train['B Target'])现在,让我们准备将模型导出为ONNX格式,以便能轻松地将AI模型集成到MQL5应用程序中。
import skl2onnx,onnx定义我们的ONNX模型可接受的输入数量。
initial_types = [('float_input',skl2onnx.common.data_types.FloatTensorType([1,4]))]将ONNX模型转换为ONNX原型。
onnx_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=12) 将ONNX原型保存至磁盘。
onnx.save(onnx_proto,"EURUSD GBR D1.onnx") MQL5入门指南
现在,我们已经准备好了ONNX模型,可以开始构建MQL5应用程序。第一步,加载我们需要的库。//+------------------------------------------------------------------+ //| EURUSD.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" //+------------------------------------------------------------------+ //| System Constants Definitions | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
我们还需要设置系统常量,以确保应用程序能够反映在前期讨论中定义的重要参数,例如10日收益周期。
//+------------------------------------------------------------------+ //| System Constants Definitions | //+------------------------------------------------------------------+ #define ONNX_INPUT_SHAPE 4 #define ONNX_OUTPUT_SHAPE 1 #define SYSTEM_TIME_FRAME PERIOD_D1 #define RETURN_PERIOD 10 #define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)将ONNX文件作为系统资源加载,以便在编译应用程序时将其一并打包。
//+------------------------------------------------------------------+ //| System Resources | //+------------------------------------------------------------------+ #resource "\\Files\\Broker Manipulation\\EURUSD GBR D1.onnx" as const uchar onnx_proto[];
要实现我们的交易策略,需要定义一些全局变量。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ long model; int position_timer; double bid,ask; double o,h,l,c; bool bullish; double sl_width;
当首次初始化系统时,我们将设置ONNX模型,然后重置交易策略所需的重要全局变量。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- model = OnnxCreateFromBuffer(onnx_proto,ONNX_DATA_TYPE_FLOAT); ulong input_shape[] = {1,ONNX_INPUT_SHAPE}; ulong output_shape[] = {1,ONNX_OUTPUT_SHAPE}; if(model == INVALID_HANDLE) { Comment("Failed To Load EURUSD Auto-Encoder-Decoder: ",GetLastError()); return(INIT_FAILED); } if(!OnnxSetInputShape(model,0,input_shape)) { Comment("Failed To Set EURUSD Auto-Encoder-Decoder Input Shape: ",GetLastError()); return(INIT_FAILED); } else if(!OnnxSetOutputShape(model,0,output_shape)) { Comment("Failed To Set EURUSD Auto-Encoder-Decoder Output Shape: ",GetLastError()); return(INIT_FAILED); } position_timer = 0; sl_width = 30; //--- return(INIT_SUCCEEDED); }
如果我们不再使用交易策略,需释放ONNX模型占用的资源。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- OnnxRelease(model); }
每当收到更新后的价格时,我们每日仅存储一次新的价格水平。如果当前无持仓,我们将从模型中获取预测结果,并据此进行交易。反之,如果已持仓,我们将在每笔交易的10日持仓期内,尽可能尝试跟踪止损。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- static datetime time_stamp; datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); if(time_stamp != current_time) { time_stamp = current_time; ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); o = iOpen(Symbol(),SYSTEM_TIME_FRAME,1); h = iHigh(Symbol(),SYSTEM_TIME_FRAME,1); l = iLow(Symbol(),SYSTEM_TIME_FRAME,1); c = iClose(Symbol(),SYSTEM_TIME_FRAME,1); bullish = (o < c) && (c > iClose(Symbol(),SYSTEM_TIME_FRAME,2)); if(PositionsTotal() == 0) { position_timer = 0; find_setup(); } else if(PositionsTotal() > 0) { if(PositionSelect(Symbol())) { long position_type = PositionGetInteger(POSITION_TYPE); double current_sl = PositionGetDouble(POSITION_SL); double new_sl; //--- Buy Trades if(position_type == POSITION_TYPE_BUY) { new_sl = bid - ((h-l)*sl_width); if(new_sl > current_sl) Trade.PositionModify(Symbol(),new_sl,0); } //--- Sell Trades else if(position_type == POSITION_TYPE_SELL) { new_sl = ask + ((h-l)*sl_width); if(new_sl < current_sl) Trade.PositionModify(Symbol(),new_sl,0); } } if(position_timer < RETURN_PERIOD) position_timer+=1; else Trade.PositionClose(Symbol()); } } }
最后,该函数将从我们的模型中获取预测结果,并检查是否存在有效的交易机会。
//+------------------------------------------------------------------+ //| Find A Trading Setup | //+------------------------------------------------------------------+ void find_setup(void) { vectorf model_inputs(ONNX_INPUT_SHAPE); model_inputs[0] = (float) iOpen(Symbol(),SYSTEM_TIME_FRAME,0); model_inputs[1] = (float) iHigh(Symbol(),SYSTEM_TIME_FRAME,0); model_inputs[2] = (float) iLow(Symbol(),SYSTEM_TIME_FRAME,0); model_inputs[3] = (float) iClose(Symbol(),SYSTEM_TIME_FRAME,0); vectorf model_output(ONNX_OUTPUT_SHAPE); if(!OnnxRun(model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output)) { Comment("Failed To Get A Prediction From Our Model: ",GetLastError()); return; } else { Comment("Prediction: ",model_output[0]); vector open,close; open.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_OPEN,1,2); close.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_CLOSE,1,2); if(open.Mean() < close.Mean()) { if((model_output[0] > 0) && (bullish)) Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid - ((h-l) * sl_width)),0); } else if(open.Mean() > close.Mean()) { if((model_output[0] < 0) && (!bullish)) Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask + ((h-l) * sl_width)),0); } } }
请不要忘记在应用程序中取消定义(释放)您所创建的任何系统常量。
//+------------------------------------------------------------------+ //| Undefine System Constants | //+------------------------------------------------------------------+ #undef ONNX_INPUT_SHAPE #undef ONNX_OUTPUT_SHAPE #undef SYSTEM_TIME_FRAME #undef TRADING_VOLUME #undef RETURN_PERIOD //+------------------------------------------------------------------+
我们用于回测的日期区间与模型训练期间所使用的数据是样本外的。这些日期在针对经纪商A和经纪商B的测试中均保持不变。需要注意的是,经纪商B代表MQL5开发者构建应用程序时使用的经纪商,而经纪商A则代表其客户最终部署该应用程序时可能使用的经纪商。

图12:选择我们测试期间的输入日期
上述图12和下述图13中指定的所有设置,在我们进行的两次测试中均保持不变。

图13:我们还将选择具有挑战性的建模设置,以便对策略的实际表现能力做出合理预期
如图14所示,当我们在经纪商B处测试该策略时,其表现颇具潜力。该策略能很好地处理样本外数据,这就促使我们投入更多的时间优化策略,以充分发挥其性能。然而,我们试图向读者传达的核心观点是:如果认为针对某一经纪商的优化,必然能在其他经纪商处产生同等效果,这种想法未免过于天真。

图14:将我们的策略应用于目标经纪商时,其资金曲线呈现正向趋势
然而,当我们在经纪商A处应用相同策略时,却无法观察到在经纪商B处所呈现的账户余额正向增长趋势。显然,如果仅更换经纪商而不调整底层模型,该策略几乎无法为我们创造任何价值。开发者们需要明白,这并不总是他们自己的过错。要让开发者为全球所有经纪商逐一定制模型,几乎是不可能完成的任务。
然而,这为我们提供了一种直观理解问题的方式。如果开发者与客户对经纪商的差异缺乏共识,双方对策略效果的理解可能完全南辕北辙。

图15:当我们在经纪商A处部署策略时,未能重现我们费尽心力才实现的账户余额上升趋势
我们还可以在图16中更详细地分析策略在经纪商B处的表现,并将其与图17中策略在经纪商A处的表现进行对比。

图16:聚焦目标经纪商时,交易表现的详细分析
显然,要构建一个能在多个经纪商间均表现良好的策略绝非易事。随着机器学习模型愈发复杂,其对输入数据的细微变化也愈发敏感。交易品种数值属性的这些差异,可能会对我们共享策略并有效复现其交易表现的能力造成严重破坏。

图17:在未经训练的经纪商处,策略运行情况的详细分析
结论
全球金融市场的分散性带来了现实限制,使得我们社区难以复现彼此的研究成果。各经纪商无法保证其报价一致,这意味着即便在同一交易品种上使用相同策略,您在某经纪商处利用的套利机会,在他处可能根本不存在。
根据您在我们社区中所偏好的角色,这些分析具有实际意义:
- 如果您喜欢使用MQL5网站上的“自由职业”板块,在发布需求时请指定经纪商,并要求开发者用您的经纪商开设模拟账户,以确保获得定制化解决方案。避免发布诸如“求购欧元兑美元交易程序”这类宽泛的需求,因为正如我们所见,需求描述越详细,对您越安全。
- 经常在市场购买应用程序的用户现在应该明白,为何针对特定经纪商的产品比宣称“通用”的产品更具价值。
- 交易信号订阅者可通过选择与自己使用相同经纪商的信号提供商,来最大化满意度,确保报告收益与实际收益始终一致。
- 最后,我的MQL5开发者同行们,现在更清楚地认识到,要为客户提供始终如一的产品和可靠服务,我们需要付出哪些努力。
通过正视这些挑战,我们可以致力于开发更易复现、针对特定经纪商的解决方案,让多元化且包容的社区中的每个人都能受益。本文旨在揭示在不同经纪商间共享单个ONNX模型的潜藏风险。作为MQL5开发者,我认为我们应秉持更高标准,避免让客户暴露于此类风险之中。
| 文件名 | 文件描述 |
|---|---|
| Requesting Broker Data.ipynb | 我们用于从两家经纪商处获取欧元兑美元历史日线数据的Jupyter Notebook。 |
| Analyzing Broker Data.ipynb | 我们用于检测两家经纪商所提供的欧元兑美元历史日线数据一致性的Jupyter Notebook。 |
| EURUSD.mq5 | 我们构建的智能交易系统(EA),用于评估在两家不同经纪商处基于同一模型操作的盈利能力。 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18133
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
使用MQL5经济日历进行交易(第十部分):可拖动仪表盘与交互式悬停效果,实现流畅的新闻导航
您应当知道的 MQL5 向导技术(第 59 部分):配以移动平均和随机振荡器形态的强化学习(DDPG)
市场模拟(第九部分):套接字(三)