Метод подбора параметров функции за константное время. Готовый торговый симулятор + улучшение Вашей стратегии

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Уважаемые участники сообщества Хабр, добрый день!
Представлюсь, я Алексей Волков, руководитель нескольких IT проектов. И сейчас в данной коротенькой статье будет вам представлена проблематика тестирования торговых стратегий с приближением к реальности и мои пути решения. Также поделюсь личным опытом в выявлении перспективности какой-либо торговой стратегии. (это не машинное обучение, не нейросети, не гадание и не астрология).

Главным нашим инструментом будет Python и Jupiter Notebook. Этого достаточно. Все по простому.

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

Итак, начнем.

Когда приходится программировать в три руки
Когда приходится программировать в три руки
Описываю проблематику

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

Как это происходит: смотрите обзор общих стратегий, модифицируете ее, проводите риск-менеджмент, выходите на рынок с новым алгоритмом и .... сливаете депо. Затем алгоритм "пожертвований" повторяется.

Почему делается идет упор на важность изучения ваших торговых стратегий? А все потому что из-за изменчивости конъюктуры рынка ваша стратегия может просто перестать работать как прежде. Поэтому с изменением рынка, нужно подстраиваться подстраиваться под него = менять свое поведение на нем.

Составим некий перечень ошибок, возможно что не учитывается, по крайней мере с чем столкнулся сам. (не исключаю, что возможно для вас очевидно, или вы сталкивались с чем-то большим и проблематичным)

Перечисляю ошибки с которыми столкнулся автор

0. Быть полностью уверенным что ваша стратегия точно работает.
Излишний оптимизм не дает холоднокровному разуму
применить хотя бы методы статистического анализа.

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

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

3. Отход от модели предполагаемой перспективной стратегии.
Как правило в момент уже тестирования появляются мысли как можно "улучшить" стратегию, добавить что-то новое, поэксперементировать с дополнительными паттернами, желая забрать все движения с рынка.

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

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

Соберем базовый симулятор, с заранее обозначенными условиями.

Учтем следующие условия:

1. Возьмем за гипотезу, что рынок это случайность (где все игроки с равноценными кошельками)
2. Используем минутный таймфрейм, чтобы его преобразовать в другой тайм для торговой стратегии, а симмулятор будет двигаться с шагом в 1 минуту.
3. Введем понятие максимального и минимального наблюдаемого профита. Пригодится для измерения потенциала стратегии. (оценка глубины роста и просадки)
4. Добавим значение стоплосса (либо процент либо конкретная цена)
5. Вводим фактор проскальзывания цены в 0.1%
6. Учитываем комиссию на бирже с умножительным коэффициентом (я использую запас 100% комиссии)
7. Нам понадобится также записывать все сделанные сделки симмулятором в отдельный журнал. Очень пригодится, для того чтобы отдельные сделки можно было построить и вручную проверить работу симулятора, а также изучить статистические величины.
8. Введем генератор стратегий на основе регулируемого количества полос боллинджера с генерацией случайных параметров.
9. Добавим случайный перебор take profit и stop loss

Затем построим симулятор с генератором случайной стратегии на основе линий боллинджера. Кстати, вы можете использовать свою стратегию, или/и добавить другие составляющие индикаторов, добавить дополнительные иные аспекты поведения алгоритма на рынке (час торгов, звезды, число упоминаний в новостях какого то слова, и т.д.). Но суть хочу показать, что можно легко строить очень сложные математические модели поведения и тут же тестировать их с перебором огромного количества параметров. Абсурдность в этом вся в том, сложность системы требует больше времени и исторического контента.

Итак, начнем. Мы сделали простой симулятор, и прогнали на нем множество итераций. Каждая итерация генерировала случайные значения параметров.

Первые две серии итераций будут по коротким дистанциям. По одной неделе исторических данных. Затем эти интервалы будут увеличены с ростом фиксированных параметров.

Код симулятора с генератором стратегий
# Импорты
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import os
import numpy as np
import matplotlib.pyplot as plt
import random
# Загружаем датасет предварительно подгруженный с биржи
symbols_data_close = {}

path = 'history4/'
for symbol in os.listdir(path)[0:1]:
    data = pd.read_csv(f'{path}{symbol}')
    symbols_data_close.update({symbol: data['4'].values})
# Берем фрагмет датасета и выведем что он представляет из себя
symbols = list(symbols_data_close.keys())
closdata = symbols_data_close[symbols[0]][:10000]
plt.figure(figsize=(20,10))
plt.plot(closdata)
Визуализация графика датасета SNXUSDT, промежуток 1 неделя
Визуализация графика датасета SNXUSDT, промежуток 1 неделя
# Функция генерации слчайных параметров
def get_generate_params(count=None):

    params = {'ma_window': [], 'std_window': [], 
              'std_rate_up': [], 'std_rate_dw': [], 
              'take_profit': [], 'stop_profit': []}
    
    params['ma_window'] = np.asarray(np.random.random(count) * 1000 + 1, 'int32')
    params['std_window'] = np.asarray(np.random.random(count) * 1000 + 1, 'int32')
    params['std_rate_up'] = np.random.random(count) * 8 - 4
    params['std_rate_dw'] = np.random.random(count) * 8 - 4
    
    params['take_profit'] = np.random.random(count) * 10
    params['stop_profit'] = -np.random.random(count) * 10
    
    return params
# Функция случайной стратегии по сгенерированным случайным параметрам
def get_my_strategy(p=None, cdata=None, count=None):
    # p -> params
    
    bb_up = []
    bb_dw = []
    
    for i in np.arange(count):
        ma = pd.Series(cdata).rolling(window=p['ma_window'][i]).mean().values
        std = pd.Series(cdata).rolling(window=p['std_window'][i]).std().values
        
        bb_up.append((ma + std * p['std_rate_up'][i])[1000:])
        bb_dw.append((ma - std * p['std_rate_dw'][i])[1000:])
    
    return {'bb_up': bb_up, 'bb_dw': bb_dw, 'closdata': cdata[1000:]}
# Функция поведения симулятора
def get_rules_result(p=None, strategy=None, operation=None,
                     profit=None, max_profit=None, min_profit=None, 
                     y_index=None, y_index_last=None, x_index=None):
    
    signal = {'in_long': False, 'end_long': False}
    
    if operation is None:
        for y_line in strategy['bb_up']:
            if y_index_last < y_line[x_index] < y_index:
                signal['in_long'] = True
                return signal
    
    
    if operation == 'long':
        
        for ind, (y_line_up, y_line_dw) in enumerate(zip(strategy['bb_up'], strategy['bb_dw'])):
            
            rule_1 = y_index > y_line_up[x_index] and profit > p['take_profit'][ind] and profit > 0
            rule_2 = y_index > y_line_up[x_index] and max_profit - profit > p['take_profit'][ind] and profit > 0
            rule_3 = y_index > y_line_up[x_index] and max_profit < -p['take_profit'][ind] and profit > 0
            
            rule_4 = y_index < y_line_dw[x_index] and profit < p['stop_profit'][ind]
            
            if rule_1 or rule_2 or rule_3 or rule_4:
                signal['end_long'] = True
                return signal 
            
    return signal

Выведем как выглядит наша одна случайная стратегия (график цены и линии боллинджера)

countData = 15 # количество линий боллинджера, которые к учиту в стратегии

my_strategy = get_my_strategy(
    p=get_generate_params(count=countData), 
    cdata=closdata, 
    count=countData)

plt.figure(figsize=(20,10))
plt.plot(my_strategy['closdata'], color='black')

for n in np.arange(countData):
    plt.plot(my_strategy['bb_up'][n])
    plt.plot(my_strategy['bb_dw'][n])
Случайно сгенерированные линии боллинджера на ценовом графике
Случайно сгенерированные линии боллинджера на ценовом графике

Основной движок симулятора. Здесь используем только сделки в лонг.

countData = 10 

leverage = 10 # плечо
params_history = []

for cnt in np.arange(9999999):

    # генерируем случайные параметры
    generate_params = get_generate_params(count=countData)

    # генерируем стратерию
    my_strategy = get_my_strategy(p=generate_params, cdata=closdata, count=countData)

    depo = [1000]

    operation=None
    profit=0
    max_profit=0
    min_profit=0

    # по шагам идем по ценовому графику
    for x, (closedata_last, closedata) in enumerate(zip(my_strategy['closdata'][:-1], my_strategy['closdata'][1:])):
        x_index = x + 1

        # вход в сделку лонг
        if rules_result['in_long']:
            rules_result['in_long'] = False

            operation = 'long'

            size_buy = depo[-1]
            price_buy = closedata

        # оценка профита, максимальной просадки и роста
        if operation is not None:

            profit = (closedata / price_buy - 1) * 100 - 0.3

            if profit > max_profit: max_profit = profit
            if profit < min_profit: min_profit = profit

        # выясняем наше поведение на каждом шаге
        rules_result = get_rules_result(p=generate_params, strategy=my_strategy, operation=operation,
                         profit=profit, max_profit=max_profit, min_profit=min_profit, 
                         y_index=closedata, y_index_last=closedata_last, x_index=x_index)

        depo.append(depo[-1])

        # выход из сделки по сигналу
        if rules_result['end_long']:
            rules_result['end_long'] = False

            depo[-1] = depo[-1] + size_buy * profit / 100 * leverage

            operation=None
            profit=0
            max_profit=0
            min_profit=0
            
    depo[-1] = depo[-1] + size_buy * profit / 100 * leverage

    # сохраняем результаты на каждой итерации случайной генерации параметров
    params_history.append({'generate_params': generate_params, 'profit': depo[-1]})
    if depo[-1] > 1000: print(round(depo[-1], 1), end=' ')
1276.3 1681.9 1072.2 1185.5 1013.5 1419.1 1206.4 1842.5 1260.9 1222.8
1016.5 1084.9 1478.2 1384.9 1022.9 1016.2 1490.1 2235.1 1100.1 1164.0
1076.1 1079.4 1070.4 1245.3 1007.7 1291.8 1101.4 1003.6 1002.8 1125.8
1010.9 1115.4 1197.5 1353.4 1037.5 1179.6 1204.8 1070.2 1334.8 1138.2
1301.0 1490.7 1036.8 1030.5 1400.4 1150.7 1146.6 1218.0 1362.6 1098.4
1695.6 1006.0 1149.5 1126.4 1048.8 1086.0 1137.5 1577.9 1266.4 1246.5
1005.0 1015.9 1031.1 1087.9 1311.0 1389.6 1019.0 1049.1 1210.9 1146.3
1127.8 1366.6 1360.8 1465.9 1101.6 1076.6 1060.4 1044.1 1281.3 1073.3
1047.7 1041.7 1322.0 1012.2 1301.1 1568.1 1056.8 1024.1 1485.3 1106.6
1008.8 1186.8 1007.3 1007.9 1100.6 1834.9 1142.1 1806.9 1352.7 1052.4
1284.5 1045.4 1017.0 1083.4 1229.2 1214.1 1012.2 1398.4 1000.3 1152.4
1003.1 2073.1 1301.3 2235.9 1223.4 1091.3 1011.6 1163.9 1006.3 1084.5
1514.4 1125.3 1334.6 1038.8 1375.3 1136.9 1211.9 1194.3 1746.2 1131.6
1029.2 1132.5 1311.5 1018.0 1355.9 1264.4 1069.1 .............

Генерируем как можно больше результатов. Оставляем генерацию на 96 часов.

# Наконец, выводим результаты
result = pd.DataFrame(params_history).sort_values(by='profit')
print('количество итераций', result.shape[0])
result = result[result['profit'] > 2500]
result

количество итераций 4326132

generate_params

profit

1355114

{'ma_window': [934, 829, 385, 291, 901, 489, 8...

2509.731530

2748242

{'ma_window': [583, 786, 272, 374, 993, 322, 4...

2515.246686

1168352

{'ma_window': [444, 256, 466, 165, 640, 298, 3...

2528.583217

1056488

{'ma_window': [173, 174, 148, 888, 122, 361, 7...

2570.601539

4090771

{'ma_window': [690, 930, 603, 720, 432, 411, 7...

2600.638982

3174694

{'ma_window': [885, 970, 293, 296, 358, 586, 8...

2604.829127

326805

{'ma_window': [873, 6, 478, 957, 263, 717, 682...

2605.651078

301489

{'ma_window': [372, 474, 59, 65, 545, 820, 969...

2665.182816

3733749

{'ma_window': [176, 31, 183, 29, 92, 9, 552, 5...

2688.979172

863146

{'ma_window': [634, 463, 554, 237, 36, 637, 74...

2707.697915

4280661

{'ma_window': [764, 908, 824, 165, 911, 682, 4...

2749.313573

817925

{'ma_window': [13, 252, 12, 571, 959, 376, 12,...

3259.250233

989229

{'ma_window': [401, 259, 980, 609, 254, 927, 7...

3725.547182

Самый максимальный профит при случайном подборе параметров получился
(3725 - 1000) / 1000 * 100% = 272,5%

Итак. С помощью метода перебора параметров, можно найти интересные результаты, экспериментируя применяя возможность перебора параметров модели.

В принципе на этом можно остановиться. Но нужно продолжить, чтобы поставить стратегию в неловкое положение. Сделаем следующее: для фиксации извлечем попарные параметры, найдем самые "популярные" параметры, и построим график изменчивости.

Найдем устойчивые параметры. Устойчивые - означает в данном контексте, что интервал изменения параметра не большой в рамках всей таблицы параметров.
Для начала выведем корреляционную зависимость между параметрами, усилив значения.

raw_counts = pd.Series(raw_col[0]).value_counts()
raw_popul = raw_counts[raw_counts > 5].index.values

col_counts = pd.Series(raw_col[1]).value_counts()
col_popul = raw_counts[raw_counts > 5].index.values

popul_params = np.unique(np.concatenate([raw_popul, col_popul]))
popul_params

array([14, 41, 42, 44, 45, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59])

col_params = table_populParameters.columns

fig, axs = plt.subplots(nrows=4, ncols=4, figsize=(16,12))

for n, ax in enumerate(axs.flatten()):
    ax.plot(table_populParameters[col_params[n]].rolling(window=5000).mean())

    ax.set_title(f'# param: {col_params[n]}')
    ax.set_yticks(())
    ax.set_xticks(())


plt.suptitle('Зависимость роста профита (ось оу) от значения параметра (ось ох)', y = 0.95)
plt.subplots_adjust(wspace=0.05)
plt.show()
Изменение значений выбранных параметров с ростом профита
Изменение значений выбранных параметров с ростом профита

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

dict_selectParams = {}
for _param in col_params:
    dict_selectParams.update(
        {_param: table_populParameters[_param].rolling(window=5000).mean().values[-10000:]})
df_selectParams = pd.DataFrame(dict_selectParams)

df_selectParams = pd.DataFrame(dict_selectParams)
df_selectParams.loc['mean'] = df_selectParams.mean()

df_selectParams
Таблица выбранных параметров
Таблица выбранных параметров

Затем добавим функцию с фиксированными нашими параметрами. Сделаем второй прогон на 96 часов.

# Функция генерации слчайных параметров с фиксацией параметров
def get_generate_params_withFix(count=None):

    params = {'ma_window': [], 'std_window': [], 
              'std_rate_up': [], 'std_rate_dw': [], 
              'take_profit': [], 'stop_profit': []}
    
    #0-9
    params['ma_window'] = np.asarray(np.random.random(count) * 1000 + 1, 'int32')
    
    #10-19
    params['std_window'] = np.asarray(np.random.random(count) * 1000 + 1, 'int32')
    params['std_window'][4] = 526
    
    #20-29
    params['std_rate_up'] = np.random.random(count) * 8 - 4
    
    #30-39
    params['std_rate_dw'] = np.random.random(count) * 8 - 4
    
    #40-49
    params['take_profit'] = np.random.random(count) * 10
    params['take_profit'][[1, 2, 4, 5, 9]] = 5.6
    
    #50-59
    params['stop_profit'] = -np.random.random(count) * 10
    params['stop_profit'][[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] = -5.9
    
    return params

#................................................
  
countData = 10

leverage = 10
params_history = []

for cnt in np.arange(9999999):
#     generate_params = get_generate_params(count=countData)
    generate_params = get_generate_params_withFix(count=countData)

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

Визуализация графика датасета BNBUSDT, промежуток 1 неделя
Визуализация графика датасета BNBUSDT, промежуток 1 неделя

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

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

Источник: https://habr.com/ru/articles/762342/


Интересные статьи

Интересные статьи

Всем привет, меня зовут Константин Терехов! Совместно с ProductStar, хочу поделиться в вами опытом начала пути во Frontend-разработке и трудностями, с которыми я столкнулся, прежду чем найти работу. Н...
Простое объяснение принципов передачи параметров в Java.Многие программисты часто путают, какие параметры в Java передаются по значению, а какие по ссылке. Давайте визуал...
Привет Хабр! По основной профессии я инженер по разработке нефтяных и газовых месторождений. Я только погружаюсь в Data Sciense и это мой первый пост, в котором хотел бы ...
Training Within Industry (TWI) позволяет снизить количество управленческих ошибок, наладить взаимопонимание и повысить уровень компетенций специалистов без отрыва от прои...
Добрый день, друзья. Перевод статьи подготовлен специально для студентов курса "Разработчик Java". Введение В этой статье я собираюсь показать вам, как работают методы persist, merge из J...