Продолжаем серию статей по количественным финансам! В первой части я описал стилизованные факты возврата активов. Теперь я хотел бы представить концепцию торговых стратегий для тестирования на истории и как это сделать, используя существующие фреймворки в Python.
Что такое тестирование на истории?
Давайте начнем с торговой стратегии. Ее можно определить как метод (основанный на заранее определенных правилах) покупки и/или продажи активов на рынках. Эти правила могут основываться, например, на техническом анализе или моделях машинного обучения.
Тестирование на истории - это, в основном, оценка эффективности торговой стратегии на исторических данных, то есть, если бы в прошлом мы использовали данную стратегию для торговли, насколько хорошо/плохо она бы работала. Конечно, нет никакой гарантии, что прошлые результаты свидетельствуют о будущем, но мы все же можем их исследовать!
Есть несколько доступных фреймворков для тестирования в Python, в этой статье я решил использовать zipline.
Почему zipline?
Некоторые из приятных функций, предлагаемых средой zipline:
- Простота использования - существует четкая структура того, как создать тест и какого результата мы можем ожидать, поэтому большую часть времени можно потратить на разработку современных торговых стратегий :).
- Реалистичность - включает в себя транзакционные издержки, проскальзывание, задержки ордеров и т. д.
- Основана на потоке - обрабатывает каждое событие индивидуально, таким образом избегая смещения вперед.
- Поставляется со многими легкодоступными статистическими показателями, такими как скользящее среднее, линейная регрессия и т. д. - нет необходимости кодировать их с нуля.
- Интеграция с экосистемой PyData - zipline использует Pandas DataFrames для хранения входных данных, а также показателей эффективности.
- Легко интегрировать другие библиотеки, такие как matplotlib, scipy, statsmodels и sklearn, в рабочий процесс создания и оценки стратегий.
- Разрабатывается и обновляется Quantopian, который предоставляет веб-интерфейс для zipline, исторических данных и даже возможности для реальной торговли.
Я считаю, что эти аргументы говорят сами за себя. Давайте начнем кодировать!
Настройка виртуальной среды с использованием conda
Самый удобный способ установить zipline - это использовать виртуальную среду. В этой статье я использую для этого conda. Я создаю новую среду с Python 3.5 (у меня возникли проблемы с использованием 3.6 или 3.7), а затем устанавливаю zipline. Вы также можете установить ее.
# создаем новую виртуальную среду
conda create -n env_zipline python=3.5
# активируем ее
conda activate env_zipline
# устанавливаем zipline
conda install -c Quantopian zipline
Для того, чтобы все работало правильно, вы также должны установить jupyter и другие пакеты, используемые в этой статье (см. распечатку watermark ниже).
Импорт библиотек
Во-первых, нам нужно загрузить расширения IPython, используя магию % load_ext.
%load_ext watermark
%load_ext zipline
Затем мы импортируем остальные библиотеки:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import zipline
from yahoofinancials import YahooFinancials
import warningsplt.style.use('seaborn')
plt.rcParams['figure.figsize'] = [16, 9]
plt.rcParams['figure.dpi'] = 200
warnings.simplefilter(action='ignore', category=FutureWarning)
Ниже вы можете увидеть список библиотек, используемых в этой статье, вместе с их версиями.
Импорт пользовательских данных
zipline поставляется с данными, загруженными из Quandl (база данных WIKI). Вы всегда можете проверить уже загруженные данные, выполнив команду:
!zipline bundles
Проблема с этим подходом состоит в том, что в середине 2018 года выдача данных была прекращена, и данные за последний год отсутствуют. Кроме того, набор данных Quandl охватывает только американские акции. В этой статье мы хотим построить простые стратегии, основанные на акциях ЕС. Вот почему мы должны вручную получить данные из другого источника. Для этого я использую yahoofinancialslibrary. Для загрузки в zipline данные должны быть в CSV-файле и в определенном формате. Для этой статьи я загружаю две ценные бумаги: цены на акции ABN AMRO (голландский банк) и AEX (индекс фондового рынка, составленный из голландских компаний, торгующих на Euronext Amsterdam). Мы используем последний в качестве бенчмарка.
Во-первых, мы определяем короткую функцию для загрузки данных с использованием yahoofinancials и подготовки DataFrame таким образом, чтобы он мог быть принят zipline. После подготовки данных мы сохраняем данные в виде файла CSV в папке с именем daily (или в другой папке по вашему выбору).
Можно передать несколько тикеров в yahoofinancials и загрузить их все сразу, однако мы выбрали этот способ для обеспечения простоты необходимых манипуляций.
def download_csv_data(ticker, start_date, end_date, freq, path):
yahoo_financials = YahooFinancials(ticker)
df = yahoo_financials.get_historical_price_data(start_date, end_date, freq)
df = pd.DataFrame(df[ticker]['prices']).drop(['date'], axis=1) \
.rename(columns={'formatted_date':'date'}) \
.loc[:, ['date','open','high','low','close','volume']] \
.set_index('date')
df.index = pd.to_datetime(df.index)
df['dividend'] = 0
df['split'] = 1
# save data to csv for later ingestion
df.to_csv(path, header=True, index=True)
# plot the time series
df.close.plot(title='{} prices --- {}:{}'.format(ticker, start_date, end_date));
Мы начнем с загрузки цен на акции ABN AMRO.
download_csv_data(ticker='ABN.AS',
start_date='2017-01-01',
end_date='2017-12-31',
freq='daily',
path='european/daily/abn.csv')
И продолжим с индексом AEX.
download_csv_data(ticker='^AEX',
start_date='2017-01-01',
end_date='2017-12-31',
freq='daily',
path='european/daily/aex.csv')
Давайте проверим загруженные пакеты данных:
!zipline bundles
Теперь мы добавим пользовательский пакет с именем eu_stocks. Для этого нам нужно изменить файл extension.py, расположенный в каталоге zipline. Нам нужно добавить следующее:
start_session = pd.Timestamp('2017-01-02', tz='utc')
end_session = pd.Timestamp('2017-12-29', tz='utc')
# register the bundle
register(
'eu_stocks', # name we select for the bundle
csvdir_equities(
# name of the directory as specified above (named after data frequency)
['daily'],
# path to directory containing the
'/.../medium_articles/Quantitative Finance/european',
),
calendar_name='XAMS', # Euronext Amsterdam
start_session=start_session,
end_session=end_session
)
В отличие от функции загрузки данных, нам нужно передать точный диапазон дат загружаемых данных. В этом примере мы начинаем с 2017–01–02, поскольку это первый день, за который у нас есть данные о ценах.
Наконец, мы запускаем следующую команду для загрузки пакета:
!zipline ingest --bundle eu_stocks
Стратегия "купи и держи"
Мы начнем с самой основной стратегии - Buy и Hold. Идея заключается в том, что мы покупаем определенный актив и ничего не делаем в течение всего срока инвестиционного горизонта. Эту простую стратегию также можно считать эталоном для более продвинутых - потому что нет смысла использовать очень сложную стратегию, которая генерирует меньше денег (например, из-за транзакционных издержек), чем покупать и ничего не делать.
В этом примере мы рассматриваем акции ABN AMRO и выбираем 2017 год в качестве периода тестирования. Мы начинаем с капитала 250 €. Я выбрал это число, так как мы покупаем только 10 акций, поэтому нет необходимости в начальном балансе в пару тысяч. Кроме того, zipline по умолчанию работает с долларами США, однако, когда все активы находятся в одной валюте, нет проблем с использованием акций и индексов, котируемых в евро. Как и в BUX Zero, мы не берем на себя никаких операционных издержек (0 евро за акцию, без минимальной цены за сделку).
Существует два подхода к использованию zipline - использование командной строки или Jupyter Notebook. Чтобы использовать последний вариант, мы должны написать алгоритм в ячейке Notebook и указать, что zipline должен его запускать. Это делается с помощью магической команды IPython %%zipline. Она принимает те же аргументы, что и команда, упомянутая выше.
Также важно отметить, что все импорты библиотек, необходимые для запуска алгоритма (такие как numpy, sklearn и т.д.), должны быть указаны в ячейке алгоритма, даже если они были ранее импортированы в другом месте.
%%zipline --start 2017-1-2 --end 2017-12-29 --capital-base 250 --bundle eu_stocks -o buy_and_hold.pkl --trading-calendar XAMS
# imports
from zipline.api import order, symbol, record, set_benchmark
# parameters
selected_stock = 'ABN'
n_stocks_to_buy = 10
def initialize(context):
set_benchmark(symbol('AEX'))
context.asset = symbol('ABN')
context.has_ordered = False
def handle_data(context, data):
# record price for further inspection
record(price=data.current(symbol(selected_stock), 'price'))
# trading logic
if not context.has_ordered:
# placing order, negative number for sale/short
order(symbol(selected_stock), n_stocks_to_buy)
# setting up a flag for holding a position
context.has_ordered = True
Поздравляем, мы написали наш первый тест. Так что же на самом деле произошло?
Каждый алгоритм zipline содержит (как минимум) две функции, которые мы должны определить:
- initialize(context)
- handle_data(context, data)
Перед запуском алгоритма вызывается функция initialize() и передается переменная context. context - это глобальная переменная, в которой мы можем хранить дополнительные переменные, которые нам нужны для доступа от одной итерации алгоритма к следующей. Здесь также необходимо изменить эталонный тест на AEX, поскольку эталонный эталон по умолчанию - SP500, торгуемый на NYSE.
После инициализации алгоритма функция handle_data() вызывается один раз для каждого события. При каждом вызове она передает одну и ту же переменную context и фрейм события, называемый data. Он содержит текущий торговый бар с ценами открытия, максимума, минимума и закрытия (OHLC) вместе с объемом.
Мы создаем заказ, используя order(asset, number_of_units), где мы указываем, что покупать и сколько акций. Положительное число указывает на покупку такого количества акций, 0 означает продажу всего, что у нас есть, а отрицательное число используется для коротких продаж. Другой полезный тип ордера - order_target, который заказывает столько акций, сколько необходимо для достижения желаемого числа в портфеле.
В нашей стратегии покупки и удержания мы проверяем, разместили ли мы уже заказ. Если нет, мы заказываем определенное количество акций, а затем ничего не делаем для остальной части тестирования.
Давайте проанализируем эффективность стратегии. Во-первых, нам нужно загрузить DataFrame performance из файла pickle.
# читаем итоговый фрейм данных об эффективности
buy_and_hold_results = pd.read_pickle('buy_and_hold.pkl')
И теперь мы можем построить некоторые из сохраненных метрик:
fig, ax = plt.subplots(3, 1, sharex=True, figsize=[16, 9])
# portfolio value
buy_and_hold_results.portfolio_value.plot(ax=ax[0])
ax[0].set_ylabel('portfolio value in €')
# asset
buy_and_hold_results.price.plot(ax=ax[1])
ax[1].set_ylabel('price in €')
# mark transactions
perf_trans = buy_and_hold_results.loc[[t != [] for t in buy_and_hold_results.transactions]]
buys = perf_trans.loc[[t[0]['amount'] > 0 for t in perf_trans.transactions]]
sells = perf_trans.loc[[t[0]['amount'] < 0 for t in perf_trans.transactions]]
ax[1].plot(buys.index, buy_and_hold_results.price.loc[buys.index], '^', markersize=10, color='g', label='buy')
ax[1].plot(sells.index, buy_and_hold_results.price.loc[sells.index], 'v', markersize=10, color='r', label='sell')
# daily returns
buy_and_hold_results.returns.plot(ax=ax[2])
ax[2].set_ylabel('daily returns')
fig.suptitle('Buy and Hold Strategy - ABN AMRO', fontsize=16)
plt.legend()
plt.show()
print('Final portfolio value (including cash): {}€'.format(np.round(buy_and_hold_results.portfolio_value[-1], 2)))
С первого взгляда мы видим, что портфель генерировал деньги за инвестиционный горизонт и очень близко следовал цене ABN AMRO (что имеет смысл, поскольку это единственный актив в портфеле).
Для просмотра транзакций нам нужно преобразовать столбец транзакций из DataFrame performance.
pd.DataFrame.from_records([x[0] for x in buy_and_hold_results.transactions.values if x != []])
Изучив столбцы эффективности `DataFrame`, мы можем увидеть все доступные метрики.
buy_and_hold_results.columns
Некоторые из заслуживающих внимания:
- starting/ending cash - проверка наличия денег в данный день;
- starting/ending value - проверка активов, их стоимость в данный день;
- orders - используется для проверки ордеров. Существуют разные события для создания ордера, когда торговая стратегия генерирует сигнал, и отдельное событие, когда оно фактически выполняется на следующий торговый день.
- pnl - ежедневные прибыли и убытки.
Простая стратегия скользящего среднего
Вторая стратегия, которую мы рассматриваем, основана на простой скользящей средней (SMA). «Механика» стратегии может быть обобщена следующим образом:
- когда цена пересекает 20-дневную SMA вверх - покупаем x акций;
- когда цена пересекает 20-дневную SMA вниз - продаем акции;
- мы можем иметь только максимум x акций в любой момент времени;
- в стратегии нет коротких продаж (хотя их легко реализовать).
Остальные компоненты бэк-теста, такие как рассматриваемый актив, горизонт инвестиций или стартовый капитал, такие же, как в примере Buy и Hold.
%%zipline --start 2017-1-2 --end 2017-12-29 --capital-base 250 --bundle eu_stocks -o sma_strategy.pkl --trading-calendar XAMS
# imports
from zipline.api import order_target, record, symbol, set_benchmark
from zipline.finance import commission
import matplotlib.pyplot as plt
import numpy as np
# parameters
ma_periods = 20
selected_stock = 'ABN'
n_stocks_to_buy = 10
def initialize(context):
context.time = 0
context.asset = symbol(selected_stock)
set_benchmark(symbol('AEX'))
# 1. manually setting the commission
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
def handle_data(context, data):
# 2. warm-up period
context.time += 1
if context.time < ma_periods:
return
# 3. access price history
price_history = data.history(context.asset, fields="price", bar_count=ma_periods, frequency="1d")
# 4. calculate moving averages
ma = price_history.mean()
# 5. trading logic
# cross up
if (price_history[-2] < ma) & (price_history[-1] > ma):
order_target(context.asset, n_stocks_to_buy)
# cross down
elif (price_history[-2] > ma) & (price_history[-1] < ma):
order_target(context.asset, 0)
# save values for later inspection
record(price=data.current(context.asset, 'price'),
moving_average=ma)
# 6. analyze block
def analyze(context, perf):
fig, ax = plt.subplots(3, 1, sharex=True, figsize=[16, 9])
# portfolio value
perf.portfolio_value.plot(ax=ax[0])
ax[0].set_ylabel('portfolio value in €')
# asset
perf[['price', 'moving_average']].plot(ax=ax[1])
ax[1].set_ylabel('price in €')
# mark transactions
perf_trans = perf.loc[[t != [] for t in perf.transactions]]
buys = perf_trans.loc[[t[0]['amount'] > 0 for t in perf_trans.transactions]]
sells = perf_trans.loc[[t[0]['amount'] < 0 for t in perf_trans.transactions]]
ax[1].plot(buys.index, perf.price.loc[buys.index], '^', markersize=10, color='g', label='buy')
ax[1].plot(sells.index, perf.price.loc[sells.index], 'v', markersize=10, color='r', label='sell')
ax[1].legend()
# daily returns
perf.returns.plot(ax=ax[2])
ax[2].set_ylabel('daily returns')
fig.suptitle('Simple Moving Average Strategy - ABN AMRO', fontsize=16)
plt.legend()
plt.show()
print('Final portfolio value (including cash): {}€'.format(np.round(perf.portfolio_value[-1], 2)))
Код для этого алгоритма немного сложнее, но мы рассмотрим все новые аспекты кода. Для простоты я обозначил контрольные точки в приведенном выше фрагменте кода (sma_strategy.py) и буду ссылаться на них по номерам ниже.
1. Я показываю, как вручную установить комиссию. В этом случае я использую значение по умолчанию для сравнения.
2. «warm-up period» - это уловка, используемая для того, чтобы у алгоритма было достаточно дней, чтобы вычислить скользящее среднее. Если мы используем несколько метрик с разной длиной окна, мы всегда должны брать самую длинную из них для warm-up.
3. Мы получаем доступ к историческим (и текущим) точкам данных, используя data.history. В этом примере мы получаем доступ за последние 20 дней. Данные (в случае одного актива) хранятся как pandas.Series, проиндексированные по времени.
4. SMA - это очень простая мера, поэтому для расчета я просто беру среднее из ранее полученных данных.
5. Я инкапсулирую логику торговой стратегии в операторе if. Для доступа к текущим и предыдущим точкам данных я использую price_history [-2] и price_history [-1], соответственно. Чтобы увидеть, не было ли пересечения, я сравниваю текущие и предыдущие цены с МА и определяю, в каком случае я имею сигнал (на покупку/продажу). В случае отсутствия сигнала алгоритм ничего не делает.
6. Вы можете использовать оператор analyze(context, perf), чтобы выполнить дополнительный анализ (например, построение графиков), когда тестирование завершено. Объект perf - это просто DataFrame эффективности, который мы также храним в файле pickle. Но когда он используется в алгоритме, мы должны ссылаться на него как на perf, и нет необходимости его загружать.
По сравнению со стратегией покупки и удержания вы могли заметить периоды, когда стоимость портфеля не изменилась. Это потому, что когда мы продаем актив (и до того, как купим снова), мы держим только наличные.
В нашем случае стратегия Buy и Hold превзошла простую скользящую среднюю. Конечная стоимость портфеля (включая денежные средства) для стратегии B & H составляет 299,37 евро, а в случае более сложной - 267,68 евро.
Заключение
В этой статье я показал, как использовать структуру zipline для тестирования торговых стратегий на истории. Когда вы ознакомитесь с библиотекой, вы легко сможете проверить различные стратегии.
В будущей статье я хотел бы рассказать об использовании более продвинутых торговых стратегий, основанных на техническом анализе.
Вы можете найти код, используемый для этой статьи, в моем GitHub.
Комментариев нет:
Отправить комментарий