股票量化软件:网格和马丁格尔交易系统中的机器学习 您敢为其打赌吗?

我们曾努力研究各种运用机器学习的方式,旨在发现市场中的形态。 您已经知道如何训练模型并实现它们。 但还有很多交易方式,几乎每种方法都可以通过运用现代机器学习算法进行改进。 其中最流行的算法之一就是网格和/或马丁格尔。 在撰写本文之前,我做了一些探索性分析,在互联网上搜索相关信息。 令人惊讶的是,这种方法在全球网络中难觅踪迹。 我在社区成员中发起了一次有关此解决方案前景的调查,大多数人回答说他们甚至不知道如何入手该主题,但是这个想法听起来很有趣。 虽然,这个思路本身似乎很简单。赫兹股票期货量化软件

我们抱着两个目的来进行一系列实验。 首先,我们将尝试证明它并不像乍看起来那样困难。 其次,我们将尝试找出这种方式是否实用和有效。 

成交贴标签

主要任务是正确地为成交贴标签。赫兹股票期货量化软件 我们还记得以前文章中如何处理单一仓位的。 我们设置了随机或确定性的成交边际,例如,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 模型)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值