Машинное обучение в торговых системах на сетке и мартингейле. Есть ли рыба?

23 февраля 2021, 08:07
Maxim Dmitrievsky
41
1 384

Введение

Мы уже изрядно потрудились и исследовали различные подходы применения машинного обучения для поиска закономерностей на валютном рынке. Вы уже имеете представление как обучать модели и внедрять их в продакшн. Но существует большое количество подходов к торговле, и практически каждый можно улучшить за счет современных алгоритмов машинного обучения. Одними из самых популярных является сетка и/или мартингейл. Перед написанием статьи я провел небольшой разведочный анализ на предмет присутствия в интернете информации на эту тему. К моему удивлению, такой подход по каким-то причинам совершенно не затронут в глобальной сети. Также на мои вопросы участникам комьюнити о перспективности такого решения большинство ответило, что они даже не представляют, как подойти к этой теме, но сама идея вызывает интерес. Хотя, казалось бы, ничего сложного в этом нет.

Давайте проведем ряд экспериментов для собственного успокоения чтобы, во-первых, доказать, что это не так сложно, как может показаться на первый взгляд. И во-вторых, чтобы выяснить, применим ли такой подход и является ли он эффективным. 


Разметка сделок

Основной задачей является правильная разметка сделок. Давайте вспомним, как это делалось для одиночных позиций в предыдущих статьях. Задавался случайный или детерминированный горизонт сделок, например, 15 баров. Если рынок вырос за эти 15 баров, то сделка размечалась на покупку, наоборот — на продажу. С сеткой ордеров логика будет аналогичной, но следует учесть совокупную прибыль\убыток по группе открытых позиций. Это можно проиллюстрировать на простом примере. Автор статьи рисовал как мог, поэтому просьба не пинать за это.

Предположим, что горизонт сделки равен 15 (пятнадцати) барам (отмечен вертикальным красным штрихом на условной шкале времени). Если используется одиночная позиция, то она будет размечена на покупку (наклонная зеленая штрих-пунктирная линия), поскольку от точки до точки рынок вырос. Рынок — это черная ломанная кривая, если кто-то не понял.

При такой разметке не будут учитываться промежуточные колебания рынка. Если применить сетку ордеров (красные и зеленые горизонтальные линии), то следует посчитать совокупную прибыль по всем сработавшим отложенным ордерам плюс ордер, открытый в самом начале (можно открыть одну позицию сразу и разместить сетку в том же направлении, но можно не открывать позицию и ограничиться только сеткой отложенных ордеров). Такая разметка будет продолжаться в скользящем окне на всю глубину истории обучения, а задачей МО (машинного обучения) является обобщение всего разнообразия ситуаций и эффективное предсказание на новых данных (если это возможно).

В таком случае может быть несколько вариантов выбора направления торговли и разметки данных, выбор одного из них является философской и экспериментальной задачей одновременно.

  • Выбор по максимальной совокупной прибыли. Если сетка на продажу дает больше прибыли, то размечается именно она.
  • Взвешенный выбор между количеством открытых ордеров и совокупной прибылью. Если средняя прибыль на каждый открытый ордер сетки выше, чем при противоположной стороне, то выбирается именно эта сторона.
  • Выбор по максимальному количеству сработавших ордеров. Поскольку мы хотим, чтобы робот торговал именно сетку, то резонно остановиться на таком подходе. Если количество сработавших ордеров максимально и совокупная позиция в прибыли, то выбирается эта сторона. Под стороной здесь подразумевается направление сетки (продажа или покупка).

Пожалуй, для начала достаточно этих трех критериев. Я бы хотел остановиться на первом, поскольку он наиболее простой и нацелен на максимальную прибыль.


Разметка сделок в коде

Давайте теперь вспомним, как происходила разметка сделок в предыдущих статьях.

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 до 3 для остальных ордеров сетки. Для того чтобы использовать сетку с фиксированным множителем для всех ордеров, следует вызвать np.full. 

Учет сработавших и несработавших ордеров может выглядеть определенным трюкачеством, поэтому следует создать какую-нибудь структуру данных. Я решил создать словарь для учета ордеров и позиций для каждого конкретного случая (семпла). Вместо этого можно было бы воспользоваться объектом Data Class или pandas Data Frame, либо структурированным 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, семплы помеченные этой меткой удаляются из обучающего датасета как неинформативные. Эти условия можно изменить впоследствии, в зависимости от желаемых вариантов построения сетки, описанных выше.


Апгрейдим тестер для работы с сеткой ордеров 

Для корректного расчета прибыли, полученной от торговли сеткой, следует модифицировать тестер стратегий. Я решил сделать его наиболее приближенным к тестеру MetaTrader 5 в том плане, что тестер последовательно проходит по истории котировок в цикле и открывает и закрывает сделки как будто это реальная торговля. В этом случае улучшается понимание кода и исключается подглядывание. Я остановлюсь на основных моментах кода для того, чтобы вы его тоже поняли. Старую версию тестера приводить не стал, но вы можете посмотреть на нее заглянув в листинги предыдущих статей. Предполагаю, что для большинства читающих приведенный ниже код является темным лесом, и они побыстрее хотели бы заполучить Грааль, не вдаваясь ни в какие подробности. Тем не менее, ключевые моменты следует пояснить.

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

Исторически сложилось, что сеточников интересует только кривая баланса, а кривая эквити игнорируется. Давайте будем придерживаться этой традиции и не станем переусложнять и без того сложный тестер, будем выводить только график баланса. А кривую эквити всегда можно посмотреть в терминале MetaTrader 5. 

В цикле пробегаем по всем ценам и добавляем их в массив 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])

В данном примере мы обучим десять моделей на 10000 сгенерированных семплах и выберем лучшую через оценку 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 на новых данных, что говорит о высокой стабильности модели. В итоге график баланса на обучающих данных и данных вне обучения получился такой.

Выглядит неплохо. Теперь мы можем экспортировать обученную модель в MetaTrader 5 и проверить её результативность в тестере терминала. Для этого нужно подготовить торгового эксперта и подключаемый include-файл. Для каждой обученной модели будет свой файл, поэтому их легко хранить и менять между собой.


Экспортируем 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 здесь опущена)

#include <Math\Stat\Math.mqh>
int MAs[14] = {5,25,55,75,100,125,150,200,250,300,350,400,450,500};
int grid_size = 10;
double grid_distances[10] = {0.003,0.0035555555555555557,0.004111111111111111,0.004666666666666666,0.005222222222222222,
			     0.0057777777777777775,0.006333333333333333,0.006888888888888889,0.0074444444444444445,0.008};
double grid_coefficients[10] = {1.0,1.4444444444444444,1.8888888888888888,2.333333333333333,
				2.7777777777777777,3.2222222222222223,3.6666666666666665,4.111111111111111,4.555555555555555,5.0};
void fill_arays( double &features[]) {
   double pr[], ret[];
   ArrayResize(ret, 1);
   for(int i=ArraySize(MAs)-1; i>=0; i--) {
       CopyClose(NULL,PERIOD_CURRENT,1,MAs[i],pr);
       double mean = MathMean(pr);
       ret[0] = pr[MAs[i]-1] - mean;
       ArrayInsert(features, ret, ArraySize(features), 0, WHOLE_ARRAY); }
   ArraySetAsSeries(features, true);
}

Как видно, все настройки сетки сохранены и модель готова к работе, достаточно подключить её к советнику.

#include <EURUSD_cat_model_martin.mqh>

Теперь следует пояснить логику обработки сигналов советником на примере всего того, что работает в OnTick() функции. В боте используется библиотека MT4Orders, которую необходимо скачать.

void OnTick() {
//---
   if(!isNewBar()) return;
   TimeToStruct(TimeCurrent(), hours);
   double features[];

   fill_arays(features);
   if(ArraySize(features) !=ArraySize(MAs)) {
      Print("No history availible, will try again on next signal!");
      return;
   }
   double sig = catboost_model(features);

// закрываем позиции по противоположному сигналу
   if(count_market_orders(0) || count_market_orders(1))
      for(int b = OrdersTotal() - 1; b >= 0; b--)
         if(OrderSelect(b, SELECT_BY_POS) == true) {
            if(OrderType() == 0 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic && sig > 0.5)
               if(OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 0, Red)) {
               }
            if(OrderType() == 1 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic && sig < 0.5)
               if(OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 0, Red)) {
               }
         }

// удаляем все отложки, если нет маркет ордеров
   if(!count_market_orders(0) && !count_market_orders(1)) {

      for(int b = OrdersTotal() - 1; b >= 0; b--)
         if(OrderSelect(b, SELECT_BY_POS) == true) {

            if(OrderType() == 2 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic )
               if(OrderDelete(OrderTicket())) {
               }

            if(OrderType() == 3 && OrderSymbol() == _Symbol && OrderMagicNumber() == OrderMagic )
               if(OrderDelete(OrderTicket())) {
               }
         }
   }

// открываем позиции и отложки по сигналам
   if(countOrders() == 0 && CheckMoneyForTrade(_Symbol,LotsOptimized(),ORDER_TYPE_BUY)) {
      double l = LotsOptimized();

      if(sig < 0.5) {
         OrderSend(Symbol(),OP_BUY,l, Ask, 0, Bid-stoploss*_Point, Ask+takeprofit*_Point, NULL, OrderMagic);
         double p = Ask;
         for(int i=0; i<grid_size; i++) {
            p = NormalizeDouble(p - grid_distances[i], _Digits);
            double gl = NormalizeDouble(l * grid_coefficients[i], 2);
            OrderSend(Symbol(),OP_BUYLIMIT,gl, p, 0, p-stoploss*_Point, p+takeprofit*_Point, NULL, OrderMagic);
         }
      }
      else {
         OrderSend(Symbol(),OP_SELL,l, Bid, 0, Ask+stoploss*_Point, Bid-takeprofit*_Point, NULL, OrderMagic);
         double p = Ask;
         for(int i=0; i<grid_size; i++) {
            p = NormalizeDouble(p + grid_distances[i], _Digits);
            double gl = NormalizeDouble(l * grid_coefficients[i], 2);
            OrderSend(Symbol(),OP_SELLLIMIT,gl, p, 0, p+stoploss*_Point, p-takeprofit*_Point, NULL, OrderMagic);
         }
      }
   }
}

Функция fill_arrays подготавливает признаки для модели CatBoost, заполняя ими массив features. Дальше этот массив передается в функцию catboost_model(), которая возвращает сигнал в диапазоне 0;1.

На примере ордеров на покупку видно, что используется переменная grid_size (количество отложенных ордеров), которые располагаются на расстоянии grid_distances друг от друга. Стандартный лот домножается на коэффициент из массива grid_coefficients, который соответствует порядковому номеру ордера.

После того, как бот скомпилирован, можно перейти к тестированию.


Проверка бота в MetaTrader 5 тестере

Тестировать необходимо на том таймфрейме, для которого бот обучался. В данном случае это H1. Можно тестировать по ценам открытия, поскольку бот с явным контролем открытия баров. Но т. к. используется сетка, для точности, можно выбрать M1 OHLC.

Данный конкретный бот обучался на периоде:

START_DATE = datetime(2020, 5, 1)
TSTART_DATE = datetime(2019, 1, 1)
FULL_DATE = datetime(2018, 1, 1)
END_DATE = datetime(2022, 1, 1)

  • С пятого месяца 20 года и по сей день — это период обучения, который разделен 50\50 на тренировочную и валидационную подвыборки. 
  • С 1 месяца 2019 года модель оценивалась по R^2 и выбиралась лучшая.
  • С 1 месяца 2018 года модель была протестирована в кастомном тестере.
  • Данные для обучения брались синтетические (сгенерированный моделью гауссовских смесей)
  • Модель CatBoost имеет сильную регуляризацию, благодаря которой не подгоняется под обучающую выборку.

Все эти факторы говорят о том (и кастомный тестер это подтвердил), что найдена определенная закономерность на интервале с 2018 года по сей день.

Давайте посмотрим как это выглядит в тестере MT5.


За исключением того, что теперь видны просадки по эквити, график баланса выглядит аналогичным образом, как это было в моем кастомном тестере. Это хорошая новость. Проверим, что бот торгует именно сетку, а не что-либо еще.


Я протестировал бота с начала 2015 года, и он показал следующий результат.

Из графика следует, что найденная закономерность работает с конца 2016 года по сей день, а затем ломается. Первоначальный лот в данном случае минимальный, поэтому бот не слился. Хорошо, мы знаем, что бот работает с начала 2017 года и можем поднять риск, чтобы повысить прибыльность. В данном случае он показывает внушительные 1600% за 3 года при просадке 40% и гипотетическом риске полного слива.



Робот также имеет стоплосс и тейкпрофит для каждой позиции. Их можно использовать, жертвуя производительность, но ограничивая риски. 

Следует заметить, что я использовал достаточно агрессивную сетку.

GRID_COEFFICIENTS = np.linspace(1, 5, num= GRID_SIZE)
array([1.        , 1.44444444, 1.88888889, 2.33333333, 2.77777778,
       3.22222222, 3.66666667, 4.11111111, 4.55555556, 5.        ])

Последний множитель равен пяти. Это означает, что лот последнего ордера в серии в пять раз выше чем первоначальный, что ведет к дополнительным рискам. Вы можете выбрать более щадящие режимы.

Почему бот перестал работать в 2016 году и ранее? У меня нет осмысленного ответа на данный вопрос. Похоже, что существуют длительные семилетние циклы на FOREX или более короткие, закономерности которых никак не связаны между собой. Это отдельная тема, требующая пристального рассмотрения.


Заключение

В данной статье я постарался описать технику, через которую можно обучать бустинг либо нейронную сеть торговать мартингейл. Предложено готовое решение, позволяющее создавать собственных ботов, готовых к торговле.


Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (41)
Evgeni Gavrilovi
Evgeni Gavrilovi | 19 мар 2021 в 19:31
Maxim Dmitrievsky:

Придется ведь учитывать те данные, которые никак не размечены вручную

Это необязательно, надо чтобы учитывались только те, что с моей разметкой

ROMAN KIVERIN
ROMAN KIVERIN | 15 апр 2021 в 11:17

Вы наверно рыбак, раз такое название пишите. Я вот давно не ловлю рыбу. Кушать жалко. Вполне можно что-то другое скушать. Дичь какая-то, поймать рыбку. Потом её и съесть. Кстати Китайцы давно придумали соевую рыбу. Там даже кости есть, но более богатые кальцием. Правда если ещё приправить немного, то вполне сносная еда. Моя жена(кстати у неё подруга была шеф поваром одного крупного ресторана), когда первый раз мне приготовила, я вообще не мог поверить что это можно приготовить. Представляете, вполне простые продукты, да я и сам не плохо готовлю. Но когда я попробовал у неё, я сначала вытаращил глаза, потом долго расспрашивал у неё рецепт. Я так и не поверил что так можно готовить. Но я ел это несколько лет. Кстати и не привык. Всегда вспоминаю и мне кажется что это какое-то было наваждение. Магия. Мне кажется она что-то делала совсем не так, или не что что мне рассказывала.

Самое интересное, что она рассказывала про кулинарию, она говорила что можно приготовить любой кусок мяса разными вкусами. Всегда по разному. И его не отличишь. Более того главное не в основных продуктах, а специях. Конечно, можно покупать супер дорогие и изысканные продукты, тратя куча времени и средств на это занятие. Но можно самое просто. Просто добавить специй. Может странно звучит. Но я так и сейчас не верю что это работает.

Удивительно могут ли дорогие продукты быть заменены дешёвыми продуктами и простыми приправами. Как не странно, не смотря на то что многие скажут более нет чем да. Это работает. Более того работает даже лучше. Зачем нужно тратить кучу сил и времени, бегая за синей птицей по магазинам, когда всегда всё и так есть под ругой.

Что же касается техники, оно вроде и хорошо с одной стороны, но уж как-то выглядит угловато. Естественно можно демонстрировать брутал, вкачивая в алгоритмы и в производительность. И может здесь я ошибаюсь, но всё хорошо в меру. Всегда нужно смотреть чтобы картина была комплексной. Мне кажется всего должно быть в меру.

Конечно, хорошо выцеживать показатели мощностями, а почему бы и нет, конечно будут результата, но если использовать более гибкие подходы, то здесь может быть и сети лишние, да и рыбу мучать не зачем. Палка всегда об двух концах. Если с одной стороны на кучку свечей терафлопсы, то может с другой стороны нужно хотя бы машку.

Иначе картина будет очень однобокая, если мы будем что-то не замечать. Это моё мнение. Можете его игнорировать. Но я думаю Вы согласны с тем что ударяясь во что-то одно, другое мы просто можем упускать из вида. 

Статья хорошая, правда немного заумно написана. Но и ладно. В целом норм.

Maxim Dmitrievsky
Maxim Dmitrievsky | 15 апр 2021 в 11:53
ROMAN KIVERIN:

Вы наверно рыбак, раз такое название пишите. Я вот давно не ловлю рыбу. Кушать жалко. Вполне можно что-то другое скушать. Дичь какая-то, поймать рыбку. Потом её и съесть. Кстати Китайцы давно придумали соевую рыбу. Там даже кости есть, но более богатые кальцием. Правда если ещё приправить немного, то вполне сносная еда. Моя жена(кстати у неё подруга была шеф поваром одного крупного ресторана), когда первый раз мне приготовила, я вообще не мог поверить что это можно приготовить. Представляете, вполне простые продукты, да я и сам не плохо готовлю. Но когда я попробовал у неё, я сначала вытаращил глаза, потом долго расспрашивал у неё рецепт. Я так и не поверил что так можно готовить. Но я ел это несколько лет. Кстати и не привык. Всегда вспоминаю и мне кажется что это какое-то было наваждение. Магия. Мне кажется она что-то делала совсем не так, или не что что мне рассказывала.

Самое интересное, что она рассказывала про кулинарию, она говорила что можно приготовить любой кусок мяса разными вкусами. Всегда по разному. И его не отличишь. Более того главное не в основных продуктах, а специях. Конечно, можно покупать супер дорогие и изысканные продукты, тратя куча времени и средств на это занятие. Но можно самое просто. Просто добавить специй. Может странно звучит. Но я так и сейчас не верю что это работает.

Удивительно могут ли дорогие продукты быть заменены дешёвыми продуктами и простыми приправами. Как не странно, не смотря на то что многие скажут более нет чем да. Это работает. Более того работает даже лучше. Зачем нужно тратить кучу сил и времени, бегая за синей птицей по магазинам, когда всегда всё и так есть под ругой.

Что же касается техники, оно вроде и хорошо с одной стороны, но уж как-то выглядит угловато. Естественно можно демонстрировать брутал, вкачивая в алгоритмы и в производительность. И может здесь я ошибаюсь, но всё хорошо в меру. Всегда нужно смотреть чтобы картина была комплексной. Мне кажется всего должно быть в меру.

Конечно, хорошо выцеживать показатели мощностями, а почему бы и нет, конечно будут результата, но если использовать более гибкие подходы, то здесь может быть и сети лишние, да и рыбу мучать не зачем. Палка всегда об двух концах. Если с одной стороны на кучку свечей терафлопсы, то может с другой стороны нужно хотя бы машку.

Иначе картина будет очень однобокая, если мы будем что-то не замечать. Это моё мнение. Можете его игнорировать. Но я думаю Вы согласны с тем что ударяясь во что-то одно, другое мы просто можем упускать из вида. 

Статья хорошая, правда немного заумно написана. Но и ладно. В целом норм.

Многим известно, что сетями можно поймать больше рыбы, но они запрещены. Если перегородить сетью реку, то она перекроет ход косяка рыбы, и многие рыбаки останутся без еды в этот вечер. Конечно, сейчас мало кто добывает себе еду таким образом, в основном это любители, которые не прочь выловить пару-тройку лещей и пожарить их на сковороде. Бывает, сидишь в лодке час или больше, а клева все нет. К тому же постоянно зацепляешь блесны об коряги. В такие моменты жалеешь что не поставил сеть и не занялся более насущными проблемами. Конечно, рыбалка сетью это совсем не то, что ловля на блесну. Упускаются многие, в том числе эстетические, моменты. Настоящий рыбак, видимо, никогда не пойдет на это. Он хочет чувствовать каждое свое движение, ощущать ветер в лицо и слушать жужжание комаров в прибрежных зонах, перемежающееся кваканьем лягушек в запрудах. Такая рыбалка постепенно уходит в глубокий вечер, когда солнце в закате освещает бездвижную как зеркало водную гладь, натянутую струну которой то там то здесь потревожат редкие всплески кормящейся щуки и судака. Определенно это особая философия, недоступная для урбанизированного потребителя, привыкшего брать готовое из печи.

ROMAN KIVERIN
ROMAN KIVERIN | 16 апр 2021 в 03:20
Maxim Dmitrievsky:

Многим известно, что сетями можно поймать больше рыбы, но они запрещены. Если перегородить сетью реку, то она перекроет ход косяка рыбы, и многие рыбаки останутся без еды в этот вечер. Конечно, сейчас мало кто добывает себе еду таким образом, в основном это любители, которые не прочь выловить пару-тройку лещей и пожарить их на сковороде. Бывает, сидишь в лодке час или больше, а клева все нет. К тому же постоянно зацепляешь блесны об коряги. В такие моменты жалеешь что не поставил сеть и не занялся более насущными проблемами. Конечно, рыбалка сетью это совсем не то, что ловля на блесну. Упускаются многие, в том числе эстетические, моменты. Настоящий рыбак, видимо, никогда не пойдет на это. Он хочет чувствовать каждое свое движение, ощущать ветер в лицо и слушать жужжание комаров в прибрежных зонах, перемежающееся кваканьем лягушек в запрудах. Такая рыбалка постепенно уходит в глубокий вечер, когда солнце в закате освещает бездвижную как зеркало водную гладь, натянутую струну которой то там то здесь потревожат редкие всплески кормящейся щуки и судака. Определенно это особая философия, недоступная для урбанизированного потребителя, привыкшего брать готовое из печи.

Так то да. Если у тебя есть стадо лучше доить пару десятков коров, чтобы потом собирать сливки, а лишнее сбывать и покупать новое сено. Многие хотят всё и сразу. Ваша статья гарантия для большинства. Если есть стадо нужно его двигать, иначе оно разбредётся. Вливание новой порции каких-то гарантий это всегда похвально. Здесь даже возражать трудно. Самое прибыльное всегда есть, будет, да и останется хорошее шоу. А лучше хорошего шоу, может быть только более лучшее шоу на запросы большинства. Это верно. Толпа рулит. Значит надо кидать всё больше и больше корма на потеху зрителей. Главное что все готовы платить за то что они получают. Готовы иметь стабильный доход? Получите распишитесь. Дальше в соответствие с купленными местами на корабле. Публика получает своё место в зале.

Зачёт, здесь даже разумная логика не нужна. Подходим берём. Чем больше покупаем тем больше берём. Статья как раз в кассу. Хотите сеткой работать, значит работаем сеткой. Кстати, у меня сейчас пару заказов на сетки. Ладно толпа рулит. :)

Maxim Dmitrievsky
Maxim Dmitrievsky | 16 апр 2021 в 10:01
ROMAN KIVERIN:

Так то да. Если у тебя есть стадо лучше доить пару десятков коров, чтобы потом собирать сливки, а лишнее сбывать и покупать новое сено. Многие хотят всё и сразу. Ваша статья гарантия для большинства. Если есть стадо нужно его двигать, иначе оно разбредётся. Вливание новой порции каких-то гарантий это всегда похвально. Здесь даже возражать трудно. Самое прибыльное всегда есть, будет, да и останется хорошее шоу. А лучше хорошего шоу, может быть только более лучшее шоу на запросы большинства. Это верно. Толпа рулит. Значит надо кидать всё больше и больше корма на потеху зрителей. Главное что все готовы платить за то что они получают. Готовы иметь стабильный доход? Получите распишитесь. Дальше в соответствие с купленными местами на корабле. Публика получает своё место в зале.

Зачёт, здесь даже разумная логика не нужна. Подходим берём. Чем больше покупаем тем больше берём. Статья как раз в кассу. Хотите сеткой работать, значит работаем сеткой. Кстати, у меня сейчас пару заказов на сетки. Ладно толпа рулит. :)

Совершенно верно. Есть некоторые "традиционные" вещи, которые некогда пользовались\пользуются популярностью\спросом. Почему бы не взять новые фантики в виде технологий машинного обучения и завернуть в них старые конфетки.

Есть спрос также на применение ИИ в парной торговле и торговле корзинами. Отчасти, спрос подогревается статьями. Но и новые продукты в Маркете от других авторов появляются на основе этих статей. Поскольку тут не вода, а готовые решения.

Работа с ценами и Сигналами в библиотеке DoEasy (Часть 65): Коллекция стаканов  и класс для работы с Сигналами MQL5.com Работа с ценами и Сигналами в библиотеке DoEasy (Часть 65): Коллекция стаканов и класс для работы с Сигналами MQL5.com
В статье создадим класс-коллекцию стаканов цен всех символов и начнём разработку функционала для работы с сервисом сигналов MQL5.com — создадим класс объекта-сигнала.
Полезные и экзотические приемы для автоматической торговли Полезные и экзотические приемы для автоматической торговли
В данной статье я покажу несколько очень интересных и полезных приемов для автоматической торговли. Часть из этих приемов возможно кому-то знакома, кому-то — нет, но я постараюсь привести самые интересные методы и объяснить почему стоит ими пользоваться. Самое главное, покажу на практике, что они могут. Напишем советники и проверим все описанные приемы на истории котировок.
Нейросети — это просто (Часть 12): Dropout Нейросети — это просто (Часть 12): Dropout
Продвигаясь дальше в изучении нейронных сетей, наверное, стоит немного уделить внимания методам повышения их сходимости при обучении. Существует несколько таких методов. В этой статье предлагаю рассмотреть один из них — Dropout.
Самоадаптирующийся алгоритм (Часть IV): Дополнительный функционал и тесты Самоадаптирующийся алгоритм (Часть IV): Дополнительный функционал и тесты
Продолжаю наполнять алгоритм минимально необходимым функционалом, проведу тесты того, что получилось. Доходность получилась невысокая, но в статьях показана модель, которая позволяет в полностью автоматическом режиме торговать в плюс по совершенно разным торговым инструментам, и не только разным, но и торгующимся на принципиально разных рынках.