使用CatBoost算法在外汇市场中查找季节性模式

2021年2月15日,14:40
马克西姆·德米特里耶夫斯基(Maxim Dmitrievsky)
1
5 334

介绍

已经发表了另外两篇有关季节性模式搜索的文章(1, 2)。我想知道机器学习算法如何应对模式搜索任务。上述文章中的交易系统是在统计分析的基础上构建的。现在,只需指示模型在一周中特定日期的特定时间进行交易,就可以消除人为因素。模式搜索可以通过单独的算法提供。


时间过滤功能

通过添加过滤器功能,可以轻松扩展该库。

def time_filter(data, count):
    # filter by hour
    hours=[15]
    if data.index[count].hour not in hours:
        return False

    # filter by day of week
    days = [1]
    if data.index[count].dayofweek not in days:
        return False

    return True

该功能检查其中指定的条件。可以实现其他附加条件(不仅是时间过滤器)。但是由于本文专门针对季节性模式,因此我将仅使用时间过滤器。如果满足所有条件,则该函数返回True,并将适当的样本添加到训练集中。例如,在这种情况下,我们指示模型仅在星期二15:00开仓交易。 “小时”和“天”列表可以包括其他小时和天。通过注释掉所有条件,可以使算法在没有条件的情况下工作,就像上一篇文章中的工作方式一样。 

现在,add_labels函数接收此条件作为输入。在Python中,函数是第一级对象,因此您可以安全地将它们作为参数传递给其他函数。

def add_labels(dataset, min, max, filter=time_filter):
    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 filter(dataset, i):
            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)
        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)

    return dataset

过滤器一旦传递给函数,便可以用于标记“买入或卖出”交易。过滤器接收原始数据集和当前条形的索引。数据集中的索引表示为包含时间的“日期时间索引”。过滤器通过第i个数字在数据框的“ datetime index”中搜索小时和日期,如果未找到则返回False。如果满足条件,则将交易标记为1或0,否则标记为2。最后,将所有2都从训练数据集中删除, 因此,仅保留由过滤器确定的特定日期和时间的示例。

还应该向定制测试仪添加一个过滤器,以在特定时间(或根据该过滤器设置的任何其他条件)启用交易。

def tester(dataset, markup=0.0, plot=False, filter=time_filter):
    last_deal = int(2)
    last_price = 0.0
    report = [0.0]
    for i in range(dataset.shape[0]):
        pred = dataset['labels'][i]
        ind = dataset.index[i].hour
        if last_deal == 2 and filter(dataset, i):
            last_price = dataset['close'][i]
            last_deal = 0 if pred <= 0.5 else 1
            continue
        if last_deal == 0 and pred > 0.5:
            last_deal = 2
            report.append(report[-1] - markup +
                          (dataset['close'][i] - last_price))
            continue
        if last_deal == 1 and pred < 0.5:
            last_deal = 2
            report.append(report[-1] - markup +
                          (last_price - dataset['close'][i]))

    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.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

这实现如下。当没有未平仓头寸时使用数字2:last_deal =2。在测试开始之前没有未平仓头寸,因此设置为2。 遍历整个数据集,并检查是否满足过滤条件。如果满足条件,请打开一个买卖交易。过滤条件不适用于交易关闭,因为它们可以在另一个小时或一周中的一天关闭。这些更改足以进行进一步正确的培训和测试。  


每个交易时段的探索性分析

在每种情况下(以及数小时或数天的组合)手动测试模型不是很方便。为此已编写了一个特殊功能,该功能允许分别快速获取每个条件的摘要统计信息。该功能可能需要一些时间才能完成,但是会输出模型显示更好性能的时间范围。

def exploratory_analysis():
    h = [x for x in range(24)]
    result = pd.DataFrame()
    for _h in h:
        global hours 
        hours = [_h]
        pr = get_prices(START_DATE, STOP_DATE)
        pr = add_labels(pr, min=15, max=15, filter=time_filter)
        gmm = mixture.GaussianMixture(
            n_components=n_compnents, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]])

        # iterative learning
        res = []
        iterations = 10
        for i in range(iterations):
            res.append(brute_force(10000, gmm))
            print('Iteration: ', i, 'R^2: ', res[-1][0], ' hour= ', _h)
        
        r = pd.DataFrame(np.array(res)[:, 0], np.full(iterations,_h))
        result = result.append(r)

    plt.scatter(result.index, result, c = result.index)
    plt.show()
    return result

您可以在功能中设置要检查的小时数列表。在我的示例中,所有24小时均已设置。为了保证实验的纯度,我通过将“最小”和“最大”(未平仓位的最小和最大水平线)设置为15来禁用采样。“ iterations”变量负责每小时的再训练次数。通过增加此参数可以获得更可靠的统计信息。完成操作后,该功能将显示以下图形:


X轴显示小时的序数。 Y轴代表每次迭代的R ^ 2分数(使用了10次迭代,这意味着每小时进行模型再训练)。如您所见,第4、5和6个小时的通过时间更加紧密,这使人们对找到的图案的质量更有信心。选择原理很简单-点的位置和密度越高,模型越好。例如,在9-15的时间间隔中,图形显示了点的较大分散,模型的平均质量下降到0.6。您可以进一步选择所需的小时数,重新训练模型并在自定义测试仪中查看其结果。


测试选定的模型

对GBPUSD货币对进行了探索性分析,具有以下参数:

SYMBOL = 'GBPUSD'
MARKUP = 0.00010
TIMEFRAME = mt5.TIMEFRAME_H1
START_DATE = datetime(2017, 1, 1)
TSTART_DATE = datetime(2015, 1, 1)
FULL_DATE = datetime(2015, 1, 1)
STOP_DATE = datetime(2021, 1, 1)

相同的参数将用于测试。为了获得更大的信心,您可以更改FULL_DATE值以查看模型在早期历史数据中的表现。

我们可以从视觉上区分小时3、4、5和6的群集。可以假设相邻的小时具有相似的模式,因此可以在这整个小时内训练模型。

hours = [3,4,5,6]
# make dataset
pr = get_prices(START_DATE, STOP_DATE)
pr = add_labels(pr, min=15, max=15, filter=time_filter)
tester(pr, MARKUP, plot=True, filter=time_filter)

# perform GMM clasterizatin over dataset
# gmm = mixture.BayesianGaussianMixture(n_components=n_compnents, covariance_type='full').fit(X)
gmm = mixture.GaussianMixture(
    n_components=n_compnents, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]])

# iterative learning
res = []

for i in range(10):
    res.append(brute_force(10000, gmm))
    print('Iteration: ', i, 'R^2: ', res[-1][0])

# test best model
res.sort()
test_model(res[-1])

其余代码不需要其他解释,如先前文章中详细解释的那样。唯一的例外是,可以使用带注释的贝叶斯模型代替简单的GMM,尽管这只是一个实验性想法。 

交易采样后的理想模型如下所示:

经过训练的模型(包括测试数据)显示出以下性能:

可以对单独的模型进行高密度小时的训练。以下是第5和20小时已受训练的模型的余额图:

现在,为了进行比较,您可以查看方差小时内训练的模型。例如,查看9和11小时。

此处的余额图显示的内容远远超过任何评论。显然,在训练模型时,应特别注意时间安排。 


每个交易日的探索性分析

对于其他时间间隔(例如一周中的几天),可以轻松修改过滤器。您只需要用星期几代替小时。

def time_filter(data, count):
    # filter by day of week
    global hours
    if data.index[count].dayofweek not in hours:
        return False
    return True

在这种情况下,应在0到5(不包括第5个序数,即星期六)的范围内执行迭代。

def exploratory_analysis():
    h = [x for x in range(5)]

现在,对GBPUSD货币对执行探索性分析。交易的频率或范围是相同的(15条)。

pr = add_labels(pr, min=15, max=15, filter=time_filter)

培训过程显示在控制台中,您可以在其中立即查看当前期间的R ^ 2分数。现在,“小时”变量不包含小时数,而是包含星期几的序数。

Iteration:  0 R^2:  0.5297625368835237  hour=  0
Iteration:  1 R^2:  0.8166096906047893  hour=  0
Iteration:  2 R^2:  0.9357674260125702  hour=  0
Iteration:  3 R^2:  0.8913802241811986  hour=  0
Iteration:  4 R^2:  0.8079720208707672  hour=  0
Iteration:  5 R^2:  0.8505663844866759  hour=  0
Iteration:  6 R^2:  0.2736870273207084  hour=  0
Iteration:  7 R^2:  0.9282442121644887  hour=  0
Iteration:  8 R^2:  0.8769775718602929  hour=  0
Iteration:  9 R^2:  0.7046666925774866  hour=  0
Iteration:  0 R^2:  0.7492883761480897  hour=  1
Iteration:  1 R^2:  0.6101962958733655  hour=  1
Iteration:  2 R^2:  0.6877652983219245  hour=  1
Iteration:  3 R^2:  0.8579669286548137  hour=  1
Iteration:  4 R^2:  0.3822441930760343  hour=  1
Iteration:  5 R^2:  0.5207801806491617  hour=  1
Iteration:  6 R^2:  0.6893157850263495  hour=  1
Iteration:  7 R^2:  0.5799059801202937  hour=  1
Iteration:  8 R^2:  0.8228326786957887  hour=  1
Iteration:  9 R^2:  0.8742262956151615  hour=  1
Iteration:  0 R^2:  0.9257707800422799  hour=  2
Iteration:  1 R^2:  0.9413981795880517  hour=  2
Iteration:  2 R^2:  0.9354221623113591  hour=  2
Iteration:  3 R^2:  0.8370429185837882  hour=  2
Iteration:  4 R^2:  0.9142875737195697  hour=  2
Iteration:  5 R^2:  0.9586871067966855  hour=  2
Iteration:  6 R^2:  0.8209392060391961  hour=  2
Iteration:  7 R^2:  0.9457287035542066  hour=  2
Iteration:  8 R^2:  0.9587372191281025  hour=  2
Iteration:  9 R^2:  0.9269140213952402  hour=  2
Iteration:  0 R^2:  0.9001009579436263  hour=  3
Iteration:  1 R^2:  0.8735623527502183  hour=  3
Iteration:  2 R^2:  0.9460714774572146  hour=  3
Iteration:  3 R^2:  0.7221720163838841  hour=  3
Iteration:  4 R^2:  0.9063579778744433  hour=  3
Iteration:  5 R^2:  0.9695391076372475  hour=  3
Iteration:  6 R^2:  0.9297881558889788  hour=  3
Iteration:  7 R^2:  0.9271590681844957  hour=  3
Iteration:  8 R^2:  0.8817985496711311  hour=  3
Iteration:  9 R^2:  0.915205007218742   hour=  3
Iteration:  0 R^2:  0.9378516360378022  hour=  4
Iteration:  1 R^2:  0.9210968481902528  hour=  4
Iteration:  2 R^2:  0.9072205941748894  hour=  4
Iteration:  3 R^2:  0.9408826184927528  hour=  4
Iteration:  4 R^2:  0.9671981453714584  hour=  4
Iteration:  5 R^2:  0.9625144032389237  hour=  4
Iteration:  6 R^2:  0.9759244293257822  hour=  4
Iteration:  7 R^2:  0.9461473783201281  hour=  4
Iteration:  8 R^2:  0.9190627222826241  hour=  4
Iteration:  9 R^2:  0.9130350931314233  hour=  4

请注意,自2017年初以来,所有模型都使用数据进行了训练,而R ^ 2分数还包括测试期(从2015年开始的其他数据)。每天高估的一致性提供了更大的信心。让我们查看最终结果。

探索性分析显示,周三和周五是交易最有利的日子,尤其是周五。交易最糟糕的一天是星期二,因为它的误差大且平均值低。让我们训练模型仅在星期五交易并查看结果。

同样,我们可以在星期二获得模型交易。

固定期限的交易并不总是适合的,因此让我们尝试扩大搜索范围并将探索性分析迭代次数增加到20。

pr = add_labels(pr, min=5, max=25, filter=time_filter)
        gmm = mixture.GaussianMixture(
            n_components=n_compnents, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]])

        # iterative learning
        res = []
        iterations = 20

价值的范围已经变大,而交易的最佳日期是星期四和星期五。

现在让我们训练星期四的控制模型以查看结果。这就是学习周期的样子(对于那些尚未阅读之前的文章的人)。

hours = [3]
# make dataset
pr = get_prices(START_DATE, STOP_DATE)
pr = add_labels(pr, min=5, max=25, filter=time_filter)
tester(pr, MARKUP, plot=True, filter=time_filter)

# perform GMM clasterizatin over dataset
# gmm = mixture.BayesianGaussianMixture(n_components=n_compnents, covariance_type='full').fit(X)
gmm = mixture.GaussianMixture(
    n_components=n_compnents, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]])


# iterative learning
res = []
for i in range(10):
    res.append(brute_force(10000, gmm))
    print('Iteration: ', i, 'R^2: ', res[-1][0])

# test best model
res.sort()
test_model(res[-1])

结果要比固定期限的交易稍差。 

显然,特定时段的频率(水平)参数很重要。接下来,让我们遍历这些值并检查它们如何影响结果。


评估交易生命周期对模型质量的影响

类似于选定标准(过滤器)的探索性分析功能,我们可以创建一个辅助函数,该函数将根据交易寿命评估模型性能。假设我们可以在1到50条(或任何其他时间段)之间设置固定的交易生命周期,那么该函数将如下所示。

def deals_frequency_analyzer():
    freq = [x for x in range(1, 50)]
    result = pd.DataFrame()
    for _h in freq:
        pr = get_prices(START_DATE, STOP_DATE)
        pr = add_labels(pr, min=_h, max=_h, filter=time_filter)
        gmm = mixture.GaussianMixture(
            n_components=n_compnents, covariance_type='full', n_init=1).fit(pr[pr.columns[1:]])

        # iterative learning
        res = []
        iterations = 5
        for i in range(iterations):
            res.append(brute_force(10000, gmm))
            print('Iteration: ', i, 'R^2: ', res[-1][0], ' deal lifetime = ', _h)
        
        r = pd.DataFrame(np.array(res)[:, 0], np.full(iterations,_h))
        result = result.append(r)

    plt.scatter(result.index, result, c = result.index)
    plt.xticks(np.arange(0, len(freq)+1, 1))
    plt.title("Performance by deals lifetime")
    plt.xlabel("deals frequency")
    plt.ylabel("R^2 estimation")
    plt.show()
    return result

“频率”列表包含要迭代的交易生存期的值。我在GBPUSD对的第5小时执行了此迭代。这是结果。


X轴显示交易频率,或更确切地说,其生命周期以巴为单位。 Y轴代表每个遍的R ^ 2得分。如您所见,0-5条的太短交易对模型性能有负面影响,而15-23的寿命是最佳的。较长的交易(超过30条)会使结果恶化。有一个小集群,其交易寿命为6-9条,得分最高。让我们尝试使用这些寿命值训练模型,并将结果与​​其他集群进行比较。

我选择了8条的寿命,自2013年以来就对其进行了测试。但是,平衡曲线并不像我希望的那样均匀。

自2015年以来,在密度最高的集群的整个生命周期中,该图看起来都非常好,但是,该模型在更早的历史间隔内的表现不佳。

最后,我选择了一系列最佳的15-23集群,并对模型进行了几次重新训练(因为交易寿命的抽样是随机的)。 

pr = add_labels(pr, min=15, max=23, filter=time_filter)

基于这种模式的模型在2015年之前的数据中并未显示出生存能力。市场结构可能发生了一些根本性的变化。需要单独进行一项大型研究来分析这种情况。选择模型并在一定时间间隔内证明其稳定性之后,可以在整个时间范围内进行训练,包括测试样品。然后可以将此模型发送到生产中。


在更长的历史上进行测试

如果我们根据较长的历史记录检查模型怎么办?该模型从2000年开始就接受数据训练,并从1990年以来开始使用数据进行测试。在如此长的历史时期内很难捕获模式,这可以从余额曲线中看出,但结果仍然是肯定的。



结论

本文介绍了用于查找季节性模式和创建交易系统的强大工具。您可以针对不同的工具(除FOREX),不同的时间范围和不同的过滤器(不仅是时间过滤器)进行分析。这种方法的应用范围非常广泛。为了充分展示其功能,我们需要使用不同的过滤器进行多次测试。进行分析后,您可以使用先前文章中描述的模型导出功能构建交易机器人。

由MetaQuotes Software Corp.从俄语翻译而来。
来源文章: //www.tbxfkj.com/ru/articles/8863

附加的文件 |
CatBoost_conditional.py (8.88 KB)
Last comments | 去讨论 (1)
马克斯·布朗
马克斯·布朗 | 2021年2月20日在19:22
太棒了!  An amazing idea. 我向你摘下帽子,你是如此原始和富有创意!!!
市场及其全球格局的物理学 市场及其全球格局的物理学

在本文中,我将尝试检验以下假设:任何对市场了解甚少的系统都可以在全球范围内运行。我不会发明任何理论或模式,而只会使用已知的事实,并将这些事实逐步转化为数学分析的语言。

神经网络变得简单(第9部分):记录工作 神经网络变得简单(第9部分):记录工作

我们已经走了很长一段路,并且库中的代码越来越大。这使得难以跟踪所有连接和依赖性。因此,我建议为先前创建的代码创建文档,并使其随每个新步骤更新。正确准备的文档将帮助我们看到工作的完整性。

蛮力模式搜索(第三部分):新视野 蛮力模式搜索(第三部分):新视野

本文提供了蛮力话题的延续,并将市场分析的新机会引入了程序算法,从而加快了分析速度并提高了结果质量。新增加的功能可在此方法中以最高质量查看全局模式。

开发自适应算法(第二部分):提高效率 开发自适应算法(第二部分):提高效率

在本文中,我将通过提高先前创建的算法的灵活性来继续该主题的开发。随着分析窗口中蜡烛数量的增加或下降或增长蜡烛的超重阈值百分比的增加,该算法变得更加稳定。我必须做出让步,并设置更大的样本量进行分析,或者占当前蜡烛过量的较大百分比。