Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Уважаемые участники сообщества Хабр, добрый день!
Представлюсь, я Алексей Волков, руководитель нескольких 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)
# Функция генерации слчайных параметров
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)
Теперь будут прогоны с данной модификацией по другому инструменту и с другими рыночными условиями. Помним, что в данной статье мы придерживаемся только "лонговых" сделок.
Естественно не нашлось случая, когда стратегия перебором параметров (из оставшейся степени свободы) вывела профит в плюс. Поскольку мы подбирали параметры только для лонговых сделок, то и работать будут только на лонговых и боковиковых ситуациях рынка. Однако, фиксируя параметры (снижая степень свободы модели), нужно брать более большие интервалы времени.
В заключении отмечу, что целью данной статьи является именно сама идея переборов параметров при выявлении подходящего поведения на рынке. Удачи.