Тестирование стратегии с помощью Python

Часть 1

Что такое тестирование на истории

Слово backtesting довольно популярно в торговом сообществе. Это просто означает тестирование вашей стратегии или установки с историческими данными и расчет чистой прибыли и убытка. Простым примером может быть то, что каждый день мы покупаем акцию «А» в 13:00 и продаем ее в 15:00, результатом была бы одна и та же установка при повторении в течение последних 10 дней. Сколько убытка/прибыли я получаю.

Инструменты, необходимые для проведения тестирования на истории

Ниже приведены основные инструменты для проведения тестирования на истории:

  • Данные
  • Инструмент или язык программирования для проведения анализа

Также есть несколько готовых веб-сайтов, которые помогут нам с анализом. https://www.stockmock.in/#!/ является одним из примеров. Мы в Algotrading Page рекомендуем пользователям всегда проводить собственный анализ и не полагаться на какой-либо внешний веб-сайт.

Для целей этого блога мы собираемся придерживаться Python. Но можно выбрать любой инструмент или язык программирования.

Торговая терминология

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

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

Рассмотрим этот сценарий, мы покупаем акцию «X» по 100 рупий. Мы хотим завершить сделку с прибылью 5% или убытком 5%. Это означает, что если цены на акции достигли 105, мы продаем их и получаем прибыль в размере 5 рупий. Кроме того, мы не хотим в конечном итоге понести огромные убытки, и нам нужно управлять нашим риском, поэтому мы берем на себя риск в размере 5%, что означает, что когда цена опускается до 95, мы продаем наши акции.

  1. Цена входа: Это цена акции, по которой вошла в сделку. 100 рупий в нашем сценарии.
  2. Стоп-лосс: Максимальный убыток, который мы готовы понести. В нашем случае 95
  3. Цель: прибыль, на которую мы пытаемся рассчитывать, в нашем случае 105.
  4. Цена выхода: цена, по которой мы вышли из сделки. В зависимости от движения акции это может быть как стоп-лосс, так и цель. 95/105 в приведенном выше примере.

В зависимости от типа взгляда на рынки мы можем совершать два типа сделок: «короткие» и «длинные». Давайте разберемся в этом на примере:

Короткий: Мы видим, что среди пользователей Mercedes было недовольство, и продажи в последнее время падали. Мы считаем, что цена акций этой компании пострадает, и в ближайшем будущем она будет торговаться по более низкой цене. Итак, мы продаем акции сейчас, скажем, по цене «x», и я ожидаю, что цена снизится на 50, и она станет «x-50». В этот момент я бы купил акции и вышел из сделки.

Когда вы ожидаете, что цена снизится, вы сначала ПРОДАЕТЕ акцию.
После достижения цели или стоп-лосса вы покупаете.

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

Когда вы ожидаете, что цена вырастет, вы сначала покупаете акцию.
После достижения цели или стоп-лосса вы ПРОДАЕТЕ.

Стратегия

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

Акции — BANKNIFTY FUTURES (текущий месяц).

Шаг 1 — Проверьте дневной максимум и дневной минимум ровно в 11 часов утра.

Шаг 2 — После отметки максимума и минимума дня в 11 утра. Сейчас мы начинаем следить за ценами с. У нас есть три сценария.

  1. Акции торгуются в одном и том же диапазоне дневного максимума и минимума. В этом случае мы ничего не делаем. В этот день мы вообще не выходим на рынок.
  2. Если цена пересечет дневной максимум, наша позиция будет ДЛИННОЙ, т.е. мы будем ПОКУПАТЬ акцию.

Цена входа: Дневной максимум.

Цель:+100 баллов

Стоп-лосс: -175 пунктов

3. Если цена пересечет дневной минимум, наша позиция будет КОРОТКОЙ
т.е. мы сначала ПРОДАДИМ долю.

Цена входа: дневной минимум.

Цель:-100 баллов.

Стоп-лосс:+175 пунктов.

Шаг 3 — Теперь, когда мы определились с правилами входа в сделку, давайте обсудим точки выхода.

Как обсуждалось выше, мы выходим из сделки либо при достижении стоп-лосса, либо при достижении цели, которую он сработал.

Если позиция длинная, если цель пересечена, мы ПРОДАЕМ и фиксируем прибыль. Если стоп-лосс пересекается, мы ПРОДАЕМ, но в убыток.

Если позиция короткая, позиция длинная, Если цель пересечена, мы ПРОДАЕМ и фиксируем прибыль. Если стоп-лосс пересекается, мы ПРОДАЕМ, но в убыток.

Тестирование нашей стратегии на истории

Теперь, когда у нас есть стратегия, давайте проверим это, протестировав прошлые данные.

К счастью, нам удалось собрать чистые данные.

Это часть 1, здесь мы просто рассмотрим, как рассчитать результат всего за один день, в части 2 мы проведем все дни в календарном 2021 году.

Давайте изучим приведенные данные, прежде чем переходить к коду

Данные

У нас есть данные на уровне 1 минуты, и у нас есть начало, закрытие, максимум и минимум каждой минуты.

Объем и столбец открытого интереса — это то, что мы не будем их использовать.

1. Объявите все необходимые параметры

# Interval for which we calucalate our High and Low
interval_dict = {«11 AM» : 105}
interval_in_mins = interval_dict[’11 AM’]start_time = ’10:59′ #change it as per interval selection
end_time = ’15:00’#Target Points
target_param = 100# Stop Loss Points
loss_param = 175#File Path
data_path = ‘/Users/sreemantakesh/Desktop/The Algotrading Page/Datasets/FUTURES/BANK NIFTY FUT 2021.csv’

2. Загрузите данные

df = pd.read_csv(data_path)
df.head(5)

Вы можете видеть, что в столбце даты на самом деле есть и дата, и время. Мы должны были бы рассмотреть это, прежде чем двигаться вперед.

3. Создайте столбцы даты и времени.

Давайте создадим две колонки времени и дня. Такой, что

‘День’ = ДД-ММ-ГГГГ
‘Время’ = ЧЧ:ММ

Кроме того, преобразуйте столбец «date» в объект «datetime».df[‘time’]= df.date.str.slice(10,16)df[‘day’] = df.date.str.slice(0,10)df[«date»] = pd.to_datetime(df[«date»])df.head(5)

Мы создали нужные нам столбцы.

4. Рассчитайте дневной максимум и дневной минимум для каждого дня и сохраните их в виде столбцов с именами «interval_high» и «interval_low».

Это может быть достигнуто в три этапа

шаг 1 — Давайте сгруппируем каждый день и просто сохраним первые 105 записей. Почему 105? Это потому, что рынок открывается в 9:15 утра. С 9.15 до 10.59 у нас есть 105 минут.#doing a groupby and only keeping the first 105 entries.
temp = df.groupby(‘day’).head(interval_in_mins)

Хранение объекта groupby во временном кадре данных. Теперь у нас есть данные только с 9.15 до 10.59 за каждый день.

шаг 2 — Давайте снова сгруппируем данные и на этот раз просто агрегируйте максимум и минимум для каждого дня.interval_high_low = temp.groupby(‘day’, as_index=False).agg({«high»:»max», «low»:»min»})
interval_high_low.head(5)

шаг 3 — Объедините эти данные с исходными данными, чтобы получить interval_high и interval_low (за каждый день).day = ’04-01-2021’#renaming the columns
interval_high_low.columns = [‘day’,’interval_high’,’interval_low’]#Joining with the original dataframe ‘df’
df = pd.merge(df, interval_high_low, on = ‘day’)df.head(5)

Это был один из этапов предварительной обработки больших данных в нашем анализе.

5. Сохраняйте максимумы 11 утра и минимумы 11 утра в переменных «interval_high» и «interval_low».

day = ’04-01-2021’#filtering the data for one single day
d1 = df[df[«day»] == day]#Storing the 11 AM high and 11 AM low in variables ‘interval_high’ and ‘interval_low’
interval_high = d1[‘interval_high’].unique()[0]
interval_low = d1[‘interval_low’].unique()[0]

Мы ограничиваем наши данные только одним днем и, следовательно, фильтром

6. Храните максимум 11 утра и минимум 11 утра в переменных «interval_high_time» и «interval_low_time».

#Storing the 11 AM high and 11 AM low in variables ‘interval_high_time’ and ‘interval_low_time’
interval_high_time = d1[d1[‘high’]==interval_high].date.dt.time.values[0]
interval_low_time = d1[d1[‘low’]==interval_low].date.dt.time.values[0]

7. Отфильтруйте данные дальше, чтобы у нас были данные только с 11 до 15 часов за данный день.

#subsetting the data for post to interval time
d1 = d1.set_index(‘date’)
d1 = d1.between_time(start_time,end_time)
d1 = d1.reset_index()d1.head()

Есть короткий способ сделать это, все, что нам нужно сделать, это установить дату в качестве индекса. А затем используйте метод between_time, чтобы получить данные за нужный интервал времени.

8. Проверьте, запущена сделка или нет

Объявим пустую переменную ‘position’. Мы храним тип сделки в этой переменной «Короткая» или «Длинная».

Мы также создадим два логических столбца «interval_high_breach» и «interval_low_breach», чтобы позиция после высокого или низкого прорыва была помечена как 1.position = »d1[‘interval_high_breach’] = np.where(d1.high > interval_high , True, False)
d1[‘interval_low_breach’] = np.where(d1.low < interval_low , True, False)

Обратите внимание на столбец «interval_low_breach» во второй строке, он говорит об истине, так как сейчас цена акций находится ниже цены на day_low (рассчитано на #4).

Теперь вспомним условия входа из нашей стратегии, у нас было три условия:

  1. Акции торгуются в одном и том же диапазоне дневного максимума и минимума. В этом случае мы ничего не делаем. В этот день мы вообще не выходим на рынок.
  2. Если цена пересечет дневной максимум, наша позиция будет ДЛИННОЙ, т.е. мы будем ПОКУПАТЬ акцию.
  3. Если цена пересечет дневной минимум, наша позиция будет КОРОТКОЙ
    т.е. мы сначала ПРОДАДИМ долю.

Закодируем все эти три условия#CASE A — When after 11 AM both high breach and low breack has happend on that day
if len(d1[d1[‘interval_low_breach’] == True]) & len(d1[d1[‘interval_high_breach’] == True]):

long_entry_index = d1[d1[‘interval_high_breach’] == True].index[0]
short_entry_index = d1[d1[‘interval_low_breach’] == True].index[0]if long_entry_index < short_entry_index:position = ‘Long’

entry_price = interval_high
entry_index = d1[d1[‘interval_high_breach’] == True].index[0]
target = entry_price + target_param
stop_loss = entry_price — loss_param
entry_time = d1[‘date’].dt.time[entry_index]


else:position = ‘Short’

entry_price = interval_low
entry_index = d1[d1[‘interval_low_breach’] == True].index[0]
target = entry_price — target_param
stop_loss = entry_price + loss_param
entry_time = d1[‘date’].dt.time[entry_index]

#CASE 2 — When after 11 AM only low breach has happened
elif len(d1[d1[‘interval_low_breach’] == True]):position = ‘Short’
entry_price = interval_low
entry_index = d1[d1[‘interval_low_breach’] == True].index[0]
target = entry_price — target_param
stop_loss = entry_price + loss_param
entry_time = d1[‘date’].dt.time[entry_index]#CASE 3 — When after 11 AM only high breach has happened
elif len(d1[d1[‘interval_high_breach’] == True]):position = ‘Long’
entry_price = interval_high
entry_index = d1[d1[‘interval_high_breach’] == True].index[0]
target = entry_price + target_param
stop_loss = entry_price — loss_param
entry_time = d1[‘date’].dt.time[entry_index]
# This is the condtion 1 from out strategy where no trade is triggered
else:position = ‘NA’

entry_price = np.nanentry_index = np.nan
target = np.nan
stop_loss = np.nan
entry_time = np.nan

Мы фиксируем индекс кадра данных, в котором нарушается interval_low/interval_high. Этот индекс будет отмечаться как отправная точка, с которой мы будем оценивать наш результат сделки, поскольку это точка, с которой мы вошли в сделку.

Давайте распечатаем переменную, чтобы увидеть, была ли сделка запущена или нет на конкретную дату (’04–01–2021′).print(«Trade triggered {} position entered «.format(position))
print(«Trade entered at : «,format(entry_time))
print(«Entry Price : «,entry_price)
print(«Traget : «,target)
print(«Stop Loss : «,stop_loss)

Проверьте результат сделки

Объявите две переменные closing_price и exit_time.closing_price = »
exit_time = »

Чтобы проверить результат сделки, нам нужно перебрать каждую строку, начиная с entry_index, и отслеживать «максимум», «минимум», «закрытие» и «время». Мы продолжаем проверять до тех пор, пока «максимум» не будет равен «цели» или «Низкий» не будет равен стоп-лоссу (для позиции «Длинная»).if position!=»NA»:#filtering out the required data
post_signal_data = d1.iloc[entry_index:,:][[«high»,»low»,»close»,»time»]].reset_index(drop=True).copy()#iterate and check if target hits or stop loss hits
if position == «Long»:
for candle_high, candle_low, closing,time in zip(post_signal_data.high,
post_signal_data.low,
post_signal_data.close,
post_signal_data.time):
if candle_high > target:
result = ‘Profit’
#print(‘Profit’)
closing_price = target
exit_time = time
break
elif candle_low < stop_loss:
result = ‘Loss’
#print(‘Loss’)
closing_price = stop_loss
exit_time = time
break
else:
result = «Started but not finished»
closing_price = closing
exit_time = timeelse:
for candle_high, candle_low, closing, time in zip(post_signal_data.high,
post_signal_data.low,
post_signal_data.close,
post_signal_data.time):
if candle_low < target:
result = ‘Profit’
closing_price = target
exit_time = time
#print(‘Profit’)
break
elif candle_high > stop_loss:
result = ‘Loss’
closing_price = stop_loss
exit_time = time
#print(‘Loss’)
break
else:
result = «Started but not finished»
last_price = post_signal_data
closing_price = closing
exit_time = time
else:
result = «NA»
closing_price = ‘NA’

Давайте распечатаем параметры, чтобы увидеть результат.print(«Trade triggered {} position entered «.format(position))
print(«Trade entered at : «,format(entry_time))
print(«Entry Price : «,entry_price)
print(«Traget : «,target)
print(«Stop Loss : «,stop_loss)
print(«Trade Result : «,result)
print(«Trade Exited at : «,exit_time)

# Copy the data in a list
rpt = []rpt.append((day,
interval_high,
interval_high_time,
interval_low,
interval_low_time,
position,
entry_time,
exit_time,
result,
entry_price,
closing_price))#creating a DataFrame from the list
rpt_df = pd.DataFrame(rpt,columns= [‘day’,
‘interval_high’,
‘interval_high_time’,
‘interval_low’,
‘interval_low_time’,
‘position’,
‘entry_time’,
‘exit_time’,
‘result’,
‘entry_price’,
‘closing_price’])rpt_df

Поэтому, если бы мы применили нашу стратегию 01.04.2021, мы бы получили прибыль.

Если вы дошли до конца так далеко, теперь у вас есть основа для того, как проверить свою стратегию на любой день.

Заключение

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

Конечные примечания

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

Часть 2

Знакомство

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

Полные результаты календарного года

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

  1. Храните все дни в списке.

https://medium.com/media/919076ba20b741da9376438d9de073e1

2. Удалите данные выброса.

https://medium.com/media/51254597688623e4a58037ef1f92aa62

3. Прокрутите весь день и повторите код от 5 до 8 из последнего блога, где мы проходили процесс поиска результатов торговли за один день. На этом этапе мы делаем то же самое, что и сейчас, когда мы проводим анализ всех торговых дней 2021 года.

https://medium.com/media/aceb0ef1521c03b2dff6272512c647ec

Выпуск

Мы можем видеть логи за каждый торговый день.

Упаковка кода.

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

Мы разделим наш код на две 8 функций.

(Полный код доступен здесь: GitHub )

  1. prepare_backtesting_data : Создание кадра данных с дневным максимумом и дневным минимумом

https://medium.com/media/db7ad130b5d433d404f08d6fdf92875a

2. interval_check : Подмножествует данные в требуемые временные рамки, за которые будет отслеживаться сделка, и вычисляет время, когда были зафиксированы максимумы и минимумы.

https://medium.com/media/9e2597b86e153ef98640bcf367484e6f

3. trade_entry : Проверяет, была ли введена сделка или нет, при вводе рассчитывает стоп-лосс, цели и время входа

https://medium.com/media/9e2597b86e153ef98640bcf367484e6f

4. trade_check : Проверьте позицию сделки и активируйте функцию входа в сделку.

https://medium.com/media/621a896e85941230181ba57728af5bd2

5. trade_result : Проверяет результат сделки

https://medium.com/media/0b523d29d0349893dc8d8868ce4aef1b

6. generate_bactesting_report : Перебирает все торговые дни, выполняет все вышеперечисленные функции и генерирует отчет

https://medium.com/media/dfa8bab10a5eec35a32251be0c24c0b1

7. print_report : Печатает ключевые поля из окончательного отчета и добавляет цифры прибылей и убытков в окончательный отчет

https://medium.com/media/19bd5109b15b3570fda0142d84bd37d3

8. create_single_parameter_report : Запустите программу тестирования на истории.

https://medium.com/media/a57d8bcfe0666ab63e30bfa18ebf1407

Итоговый отчет

Аналитика и визуализация

Теперь, когда мы, наконец, подготовили наш отчет, давайте сделаем из него некоторые ключевые выводы.

  1. Наша стратегия принесла колоссальные 71482 рупии чистой прибыли, что составляет около 45% прибыли от инвестиций в 1 лот Bank Nifty Futures (цена 1 лота колебалась от 1,3 лакха до 1,6 лакха рупий, мы взяли приблизительное число 1,5 лакха рупий).
  2. Из отчета мы видим, что 66% (141/211) дней, когда мы оказались в прибыли, 14% сделок, начатых, но не завершенных.
  3. Вторник — самый прибыльный день, накопивший в общей сложности 36495 рупий, тогда как четверг, являющийся наименее прибыльным днем, может быть нестабильным, поскольку четверг приходится на неделю и истечение срока действия опционов.
  4. Сделки, которые заканчиваются прибылью, как правило, завершаются быстрее по сравнению с сделками, которые заканчиваются убытком.

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

Приборная панель сделана в Tableau

Заключение

В этом блоге мы протестировали простую стратегию BANKNIFTY FUTURES и рассчитали ее доходность за весь 2021 календарный год. Стратегия принесла 45% прибыли от первоначальных инвестиций. Тестирование стратегии на исторических данных дает нам больше уверенности и добавляет уверенности в нашей стратегии.

GitHub

Источник