我们曾努力研究各种运用机器学习的方式,旨在发现市场中的形态。 您已经知道如何训练模型并实现它们。 但还有很多交易方式,几乎每种方法都可以通过运用现代机器学习算法进行改进。 其中最流行的算法之一就是网格和/或马丁格尔。 在撰写本文之前,我做了一些探索性分析,在互联网上搜索相关信息。 令人惊讶的是,这种方法在全球网络中难觅踪迹。 我在社区成员中发起了一次有关此解决方案前景的调查,大多数人回答说他们甚至不知道如何入手该主题,但是这个想法听起来很有趣。 虽然,这个思路本身似乎很简单。赫兹股票期货量化软件
我们抱着两个目的来进行一系列实验。 首先,我们将尝试证明它并不像乍看起来那样困难。 其次,我们将尝试找出这种方式是否实用和有效。
成交贴标签
主要任务是正确地为成交贴标签。赫兹股票期货量化软件 我们还记得以前文章中如何处理单一仓位的。 我们设置了随机或确定性的成交边际,例如,15 根柱线。 如果行情在这 15 根柱线上都在上涨,则该成交被贴上标签“买入”,否则被贴上标签“卖出”。 类似的逻辑也运用在订单网格,但是此处必须考虑一组持仓的总盈利/亏损。 这可以用一个简单的例子来阐述。 作者会尽力描绘蓝图。
假设交易边际是15(十五)根柱线(在传统时间标尺上以垂直线标记的红色笔划)。 如果只有一笔持仓,由于行情已从一个点位上涨到另一个点位,故它将被贴上标签“买入”(绿色点划斜线)。 行情在此处显示为黑色折线。
有了这样的标签,过渡行情的波动即被忽略了赫兹股票期货量化软件。 如果我们运用订单网格(红色和绿色水平线),则必须计算所有已触发挂单的总利润,包括自最初开始的订单(您可在同一方向上开仓布局网格;或者选择放置挂单网格,无需即刻开仓)。 针对学习历史的整个深度,在滑动窗口中持续如此贴标签。赫兹股票期货量化软件 ML(机器学习)的任务是归纳各种状况,并基于新数据有效地预测(如果可能)。
在这种情况下,也许会有若干个选项用于选择交易方向,并为数据贴标签。 如何选择在此即是哲学也是实验任务。
- 依据最大总利润进行选择。 如果“卖出”网格产生更多的利润,则为该网格贴标签。
- 在持单数量和总利润之间进行加权选择。赫兹股票期货量化软件 如果网格中每笔持单的平均利润高于逆向的平均利润,则选择该边。
- 依据已触发订单的最大数量进行选择。赫兹股票期货量化软件 由于所期望的机器人应遵照网格交易,该选项看起来很合理。 如果已触发订单数量最大,且总仓位处于获利状态,则选择此侧。 这一侧代表网格的方向(卖出或买入)。赫兹股票期货量化软件
这三条准测对于开始似乎已经足够了。 赫兹股票期货量化软件我们来详细研究第一个,因为它是最简单的一个,旨在获得最大的利润。
在代码中为成交贴标签
现在我们回想一下以前文章中如何为成交贴标签。
def add_labels(dataset, min, max): labels = [] for i in range(dataset.shape[0]-max): rand = random.randint(min, max) curr_pr = dataset['close'][i] future_pr = dataset['close'][i + rand] if future_pr + MARKUP < curr_pr: labels.append(1.0) elif future_pr - MARKUP > curr_pr: labels.append(0.0) else: labels.append(2.0) dataset = dataset.iloc[:len(labels)].copy() dataset['labels'] = labels dataset = dataset.dropna() dataset = dataset.drop( dataset[dataset.labels == 2].index).reset_index(drop=True) return dataset
该代码需要针对常规网格和马丁格尔网格进行归纳。 另一个令人兴奋的功能是,您可以浏览含有不同数量订单的网格,订单之间的距离不同,甚至可以运用马丁格尔(手数递增)。赫兹股票期货量化软件
为此,我们添加全局变量,以后可用它来优化。赫兹股票期货量化软件
GRID_SIZE = 10 GRID_DISTANCES = np.full(GRID_SIZE, 0.00200) GRID_COEFFICIENTS = np.linspace(1, 3, num= GRID_SIZE)
GRID_SIZE 变量里包含双向订单的数量。
GRID_DISTANCES 设置订单之间的距离。 距离可以是固定的,亦或可变的(对于所有订单而言各有不同)。 这将有助于提升交易系统的灵活性。
GRID_COEFFICIENTS 变量包含每笔订单的手数乘数。 如果它们是常数,则系统将创建规则的网格。 如果手数不同,那么它会是马丁格尔或逆马丁格尔,或任何其他运用不同手数乘数的网格名称。
对于那些刚接触 numpy 函数库的人:
- np.full 用指定数量的相同值填充数组
- np.linspace 用指定数量的值填充数组,其值均匀地分布在两个实数之间。 在上面的示例中,GRID_COEFFICIENTS 将包含以下内容。
array([1. , 1.22222222, 1.44444444, 1.66666667, 1.88888889, 2.11111111, 2.33333333, 2.55555556, 2.77777778, 3. ])
相应地,第一个手数乘数将等于 1,因此该手数将等于交易系统参数中指定的基本手数。 在以后的网格里订单手数乘数连续从 1 到 3。 若在网格里的所有订单采用固定乘数,则需调用 np.full。 赫兹股票期货量化软件
统计已触发和未触发订单可能有些棘手,故此我们需要创建某种数据结构。 我决定创建一本字典来保存每个特定案例(样本)的订单和仓位记录。 取而代之,我们可以利用数据类对象,熊猫数据框架或 numpy 结构化数组。 最后的解决方案,很可能是最快的,但在此它并不要紧。赫兹股票期货量化软件
每次迭代将样本添加到训练集合当中,并创建有关的订单网格信息,保存在字典里。 这可能需要一些解释。 grid_stats 字典包含有关当前订单网格从打开到平仓的所有必需信息。 赫兹股票期货量化软件
def add_labels(dataset, min, max, distances, coefficients): labels = [] for i in range(dataset.shape[0]-max): rand = random.randint(min, max) all_pr = dataset['close'][i:i + rand + 1] grid_stats = {'up_range': all_pr[0] - all_pr.min(), 'dwn_range': all_pr.max() - all_pr[0], 'up_state': 0, 'dwn_state': 0, 'up_orders': 0, 'dwn_orders': 0, 'up_profit': all_pr[-1] - all_pr[0] - MARKUP, 'dwn_profit': all_pr[0] - all_pr[-1] - MARKUP } for i in np.nditer(distances): if grid_stats['up_state'] + i <= grid_stats['up_range']: grid_stats['up_state'] += i grid_stats['up_orders'] += 1 grid_stats['up_profit'] += (all_pr[-1] - all_pr[0] + grid_stats['up_state']) \ * coefficients[int(grid_stats['up_orders']-1)] grid_stats['up_profit'] -= MARKUP * coefficients[int(grid_stats['up_orders']-1)] if grid_stats['dwn_state'] + i <= grid_stats['dwn_range']: grid_stats['dwn_state'] += i grid_stats['dwn_orders'] += 1 grid_stats['dwn_profit'] += (all_pr[0] - all_pr[-1] + grid_stats['dwn_state']) \ * coefficients[int(grid_stats['dwn_orders']-1)] grid_stats['dwn_profit'] -= MARKUP * coefficients[int(grid_stats['dwn_orders']-1)] if grid_stats['up_profit'] > grid_stats['dwn_profit'] and grid_stats['up_profit'] > 0: labels.append(0.0) continue elif grid_stats['dwn_profit'] > 0: labels.append(1.0) continue labels.append(2.0) dataset = dataset.iloc[:len(labels)].copy() dataset['labels'] = labels dataset = dataset.dropna() dataset = dataset.drop( dataset[dataset.labels == 2].index).reset_index(drop=True) return dataset
all_pr 变量包含从当前到未来的价格。 需要计算网格本身。 为了构建网格,我们想知道从第一根到最后一根柱线的价格范围。 这些值包含在 “up_range” 和 “dwn_range” 字典条目当中。 变量 “up_profit” 和 “dwn_profit” 将包含当前历史片段中的 “买入” 或 “卖出” 网格所获得的最终利润。 这些数值的初始值,来自最初开仓的一笔成交中获得的利润。 然后,如果网格挂单被触发,所有开单成交均会汇总。
现在,我们需要遍历所有 GRID_DISTANCES,并检查是否触发了挂单。 如果订单处于 up_range 或 dwn_range 范围内,则该订单已被触发。 在这种情况下,我们增加相应的 up_state 和 dwn_state 计数器,这些计数器存储最后一次激活订单的级别。 在下一次迭代中,网格中距新订单的距离会被添加到该级别 - 如果该订单在价格范围内,则它也已被触发。
所有已触发的订单均要编写附加信息。 例如,挂单的利润被添加到总数值之中。 对于买入仓位,此利润采用以下公式计算。 此处,用仓位的最后价格(应该是该仓位的平仓价)减去开仓价格,加上与系列中所选挂单的距离,并将结果乘以网格中该订单的手数乘数。 卖出订单则是逆计算。 累积的标记则会另外计算。
grid_stats['up_profit'] += (all_pr[-1] - all_pr[0] + grid_stats['up_state']) \ * coefficients[int(grid_stats['up_orders']-1)] grid_stats['up_profit'] -= MARKUP * coefficients[int(grid_stats['up_orders']-1)]
下一个代码模块检查买入和卖出网格的利润。 参考累计的标记,若利润大于零,且是最大值,则将相应的样本添加到训练集合当中。 如果不满足任何条件,则添加 2.0 标记 - 带有该标记的样本将从训练数据集合中删除,因为这代表它们是无用的。 这些条件可以以后更改,取决于所期望网格的构建选项。
升级测试器以便能操控订单网格
为了正确地计算来自网格交易中获得的利润,我们需要修改策略测试器。 我决定令其类似于 赫兹股票期货量化软件 测试器,如此它即可顺序地遍历报价历史,并像真实交易一样开仓和平仓。 这样可以提升对代码的理解,并避免以后有所遗漏。 我将重点介绍代码的要点。 我不会在这里提供测试器旧版,但是您可以在我以前的文章中找到它。 我猜测有些读者可能不理解下面的代码,因为他们想快点拿到圣杯,不想啰嗦。 然而,关键点应予以澄清。赫兹股票期货量化软件
def tester(dataset, markup, distances, coefficients, plot=False): last_deal = int(2) all_pr = np.array([]) report = [0.0] for i in range(dataset.shape[0]): pred = dataset['labels'][i] all_pr = np.append(all_pr, dataset['close'][i]) if last_deal == 2: last_deal = 0 if pred <= 0.5 else 1 continue if last_deal == 0 and pred > 0.5: last_deal = 1 up_range = all_pr[0] - all_pr.min() up_state = 0 up_orders = 0 up_profit = (all_pr[-1] - all_pr[0]) - markup report.append(report[-1] + up_profit) up_profit = 0 for d in np.nditer(distances): if up_state + d <= up_range: up_state += d up_orders += 1 up_profit += (all_pr[-1] - all_pr[0] + up_state) \ * coefficients[int(up_orders-1)] up_profit -= markup * coefficients[int(up_orders-1)] report.append(report[-1] + up_profit) up_profit = 0 all_pr = np.array([dataset['close'][i]]) continue if last_deal == 1 and pred < 0.5: last_deal = 0 dwn_range = all_pr.max() - all_pr[0] dwn_state = 0 dwn_orders = 0 dwn_profit = (all_pr[0] - all_pr[-1]) - markup report.append(report[-1] + dwn_profit) dwn_profit = 0 for d in np.nditer(distances): if dwn_state + d <= dwn_range: dwn_state += d dwn_orders += 1 dwn_profit += (all_pr[0] + dwn_state - all_pr[-1]) \ * coefficients[int(dwn_orders-1)] dwn_profit -= markup * coefficients[int(dwn_orders-1)] report.append(report[-1] + dwn_profit) dwn_profit = 0 all_pr = np.array([dataset['close'][i]]) continue y = np.array(report).reshape(-1, 1) X = np.arange(len(report)).reshape(-1, 1) lr = LinearRegression() lr.fit(X, y) l = lr.coef_ if l >= 0: l = 1 else: l = -1 if(plot): plt.figure(figsize=(12,7)) plt.plot(report) plt.plot(lr.predict(X)) plt.title("Strategy performance") plt.xlabel("the number of trades") plt.ylabel("cumulative profit in pips") plt.show() return lr.score(X, y) * l
纵观历史,网格交易者仅对余额曲线感兴趣,而往往忽视净值曲线。 因此,我们将坚持这一传统,且不会令我们复杂的测试仪变得过于复杂。 我们将仅显示余额图形。 进而,净值曲线可始终在赫兹股票期货量化软件终端中查看。
我们循环遍历所有价格,并将它们添加到数组 all_pr。 此外,上面标记了三个选项。 由于以前的文章中已经讨论过该测试器,因此,我仅解释出现相反信号时网格平单的选项。 就像在为成交贴标签时一样,up_range 变量存储按平仓时间的价格范围。 接下来,计算第一笔仓位(按市价开仓)的利润。 然后,循环检查是否存在已触发挂单。 如果有,则将其结果添加到余额图形中。 卖出订单/仓位也需执行相同的操作。 因此,余额图形反映的是所有已平仓位,而不是一组的总利润。
测试操控订单网格的新方法
我们已经很熟悉如何为机器学习准备数据。 首先获取价格和一套功能,然后为数据贴标签(创建“买入”和“卖出”标签),然后在自定义测试器中检查标签。
# Get prices and labels and test it pr = get_prices(START_DATE, END_DATE) pr = add_labels(pr, 15, 15, GRID_DISTANCES, GRID_COEFFICIENTS) tester(pr, MARKUP, GRID_DISTANCES, GRID_COEFFICIENTS, plot=True)
现在,我们需要训练 CatBoost 模型,并依据新数据对其进行测试。 由于其效果良好,我决定再次依据高斯混合模型生成的合成数据进行训练。
# Learn and test CatBoost model gmm = mixture.GaussianMixture( n_components=N_COMPONENTS, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]]) res = [] for i in range(10): res.append(brute_force(10000)) print('Iteration: ', i, 'R^2: ', res[-1][0]) res.sort() test_model(res[-1])
在此示例中,我们将依据 10,000 个生成的样本上训练 10 个模型,并通过 R^2 分数选择最佳的一个模型。 学习过程如下。
Iteration: 0 R^2: 0.8719436661855786 Iteration: 1 R^2: 0.912006346274096 Iteration: 2 R^2: 0.9532278725035132 Iteration: 3 R^2: 0.900845571741786 Iteration: 4 R^2: 0.9651728908727953 Iteration: 5 R^2: 0.966531822300101 Iteration: 6 R^2: 0.9688263099200539 Iteration: 7 R^2: 0.8789927823514787 Iteration: 8 R^2: 0.6084261786804662 Iteration: 9 R^2: 0.884741078512629
大多数模型在新数据上的 R^2 分数都很高,这表明该模型具有很高的稳定性。 这是训练数据和训练外数据的余额图结果。
看起来不错。 现在,我们可以导出经过 赫兹股票期货量化软件 训练的模型,并在终端测试器中检查其结果。 在测试之前,有必要准备智能交易系统,并包含文件。 每个训练过的模型都有其自己的文件,因此可以轻松存储和更改它们。
把 CatBoost 模型导出到 MQL5
调用以下函数来导出模型。
export_model_to_MQL_code(res[-1][1])
该函数已稍作修改。 针对修改的解释如下。
def export_model_to_MQL_code(model): model.save_model('catmodel.h', format="cpp", export_parameters=None, pool=None) # add variables code = '#include <Math\Stat\Math.mqh>' code += '\n' code += 'int MAs[' + str(len(MA_PERIODS)) + \ '] = {' + ','.join(map(str, MA_PERIODS)) + '};' code += '\n' code += 'int grid_size = ' + str(GRID_SIZE) + ';' code += '\n' code += 'double grid_distances[' + str(len(GRID_DISTANCES)) + \ '] = {' + ','.join(map(str, GRID_DISTANCES)) + '};' code += '\n' code += 'double grid_coefficients[' + str(len(GRID_COEFFICIENTS)) + \ '] = {' + ','.join(map(str, GRID_COEFFICIENTS)) + '};' code += '\n' # get features code += 'void fill_arays( double &features[]) {\n' code += ' double pr[], ret[];\n' code += ' ArrayResize(ret, 1);\n' code += ' for(int i=ArraySize(MAs)-1; i>=0; i--) {\n' code += ' CopyClose(NULL,PERIOD_CURRENT,1,MAs[i],pr);\n' code += ' double mean = MathMean(pr);\n' code += ' ret[0] = pr[MAs[i]-1] - mean;\n' code += ' ArrayInsert(features, ret, ArraySize(features), 0, WHOLE_ARRAY); }\n' code += ' ArraySetAsSeries(features, true);\n' code += '}\n\n' # add CatBosst code += 'double catboost_model' + '(const double &features[]) { \n' code += ' ' with open('catmodel.h', 'r') as file: data = file.read() code += data[data.find("unsigned int TreeDepth") :data.find("double Scale = 1;")] code += '\n\n' code += 'return ' + \ 'ApplyCatboostModel(features, TreeDepth, TreeSplits , BorderCounts, Borders, LeafValues); } \n\n' code += 'double ApplyCatboostModel(const double &features[],uint &TreeDepth_[],uint &TreeSplits_[],uint &BorderCounts_[],float &Borders_[],double &LeafValues_[]) {\n\ uint FloatFeatureCount=ArrayRange(BorderCounts_,0);\n\ uint BinaryFeatureCount=ArrayRange(Borders_,0);\n\ uint TreeCount=ArrayRange(TreeDepth_,0);\n\ bool binaryFeatures[];\n\ ArrayResize(binaryFeatures,BinaryFeatureCount);\n\ uint binFeatureIndex=0;\n\ for(uint i=0; i<FloatFeatureCount; i++) {\n\ for(uint j=0; j<BorderCounts_[i]; j++) {\n\ binaryFeatures[binFeatureIndex]=features[i]>Borders_[binFeatureIndex];\n\ binFeatureIndex++;\n\ }\n\ }\n\ double result=0.0;\n\ uint treeSplitsPtr=0;\n\ uint leafValuesForCurrentTreePtr=0;\n\ for(uint treeId=0; treeId<TreeCount; treeId++) {\n\ uint currentTreeDepth=TreeDepth_[treeId];\n\ uint index=0;\n\ for(uint depth=0; depth<currentTreeDepth; depth++) {\n\ index|=(binaryFeatures[TreeSplits_[treeSplitsPtr+depth]]<<depth);\n\ }\n\ result+=LeafValues_[leafValuesForCurrentTreePtr+index];\n\ treeSplitsPtr+=currentTreeDepth;\n\ leafValuesForCurrentTreePtr+=(1<<currentTreeDepth);\n\ }\n\ return 1.0/(1.0+MathPow(M_E,-result));\n\ }' file = open('C:/Users/dmitrievsky/AppData/Roaming/MetaQuotes/Terminal/D0E8209F77C8CF37AD8BF550E51FF075/MQL5/Include/' + str(SYMBOL) + '_cat_model_martin' + '.mqh', "w") file.write(code) file.close() print('The file ' + 'cat_model' + '.mqh ' + 'has been written to disc')
现在可以保存训练中所用的网格设置。 它们将会在交易时用到。
来自标准终端发布包的移动平均线和指标缓冲区,已不再使用。 取而代之,所有功能都在函数主体中计算。 添加原始功能时,也应在导出的函数里添加这些功能。
绿色高亮示意终端的 “Include” 文件夹的路径。 它允许保存 .mqh 文件,并将其连接到智能交易系统。
我们来查看 .mqh 文件本身(此处省略了 CatBoost 模型)