Это вторая статья о тестировании торговых стратегий в Python. В предыдущей описывалось, как создавать простые тесты на истории с использованием пользовательских данных - в данном случае - акций ЕС.
На этот раз цель статьи - показать, как создавать торговые стратегии на основе технического анализа (TA). Ссылаясь на Википедию, технический анализ - это «методология прогнозирования направления цен посредством изучения прошлых рыночных данных, прежде всего цены и объема».
В этой статье я покажу, как использовать популярную библиотеку Python для расчета показателей TA - TA-Lib, вместе с платформой тестирования zipline. Я создам 5 стратегий и посмотрю, какая из них наиболее эффективна на одном и том же инвестиционном горизонте.
К концу статьи вы увидите, как легко тестировать торговые стратегии на основе технического анализа. Зная, что вы можете придумать собственные стратегии и легко реализовать их на практике.
Настройки
Для этой статьи я использую следующие библиотеки:
pyfolio 0.9.2
numpy 1.14.6
matplotlib 3.0.0
pandas 0.22.0
json 2.0.9
empyrical 0.5.0
zipline 1.3.0
Стратегии:
В этой статье мы используем следующую постановку задачи:
- инвестор имеет капитал 1000 €;
- инвестиционный горизонт охватывает 2018 год;
- инвестор может инвестировать только в Unilever;
- мы не предполагаем никаких операционных издержек - торговля с нулевой комиссией :);
- нет коротких продаж (инвестор может продать только то, что ему принадлежит);
- когда инвестор открывает позицию (покупает акции), он идет ва-банк - распределяет все доступные ресурсы для покупки.
Стратегия Buy And Hold
Мы начнем с самой основной стратегии - Buy и Hold. Идея заключается в том, что мы покупаем определенный актив и ничего не делаем в течение всего срока инвестиционного горизонта. Таким образом, при первой же возможности мы покупаем как можно больше акций за счет нашего капитала и ничего не делаем далее.
Эту простую стратегию также можно считать эталоном для более продвинутых - потому что нет смысла использовать очень сложную стратегию, которая генерирует меньше денег (в целом или из-за транзакционных издержек), чем покупка один раз.
%%zipline --start 2018-1-1 --end 2018-12-31 --capital-base 1000.0 -o buy_and_hold.pkl --bundle eu_stocks --trading-calendar XAMS
# imports
from zipline.api import order_percent, symbol, record
from zipline.finance import commission
# parameters
SELECTED_STOCK = 'UNA'
def initialize(context):
context.asset = symbol(SELECTED_STOCK)
context.has_ordered = False
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
def handle_data(context, data):
# trading logic
if not context.has_ordered:
order_percent(context.asset, 1)
context.has_ordered = True
record(price=data.current(context.asset, 'price'))
Загружаем performance DataFrame:
buy_and_hold_results = pd.read_pickle('buy_and_hold.pkl')
Здесь может произойти (этого не произошло, но хорошо осознавать такую возможность) внезапное появление отрицательного значения ending_cash. Причиной этого может быть тот факт, что количество акций, которые мы хотим купить, рассчитывается в конце дня с использованием цены (закрытия) этого дня. Однако ордер выполняется на следующий день, и цена может существенно измениться. В zipline заказ не отклоняется из-за недостатка средств, но мы можем получить отрицательный баланс. Это может происходить в основном со стратегиями, которые покупают «на все». Мы могли бы придумать несколько способов избежать этого - например, вручную подсчитать количество акций, которые мы можем купить на следующий день, а также включить некоторую наценку, чтобы предотвратить возникновение такой ситуации, однако для простоты мы допускаем, что это может произойти.
Мы используем вспомогательную функцию для визуализации некоторых деталей стратегии: эволюции стоимости портфеля, транзакций на вершине ценового ряда и ежедневных доходностей.
qf.visualize_results(buy_and_hold_results, 'Buy and Hold Strategy - UNA', '€')
Мы также создаем сводку эффективности (используя другую вспомогательную функцию), которая будет использоваться в последнем разделе:
buy_and_hold_perf = qf.get_performance_summary(buy_and_hold_results.returns)
Для краткости мы не будем показывать все эти шаги (такие как загрузка performance DataFrame или получение сводки эффективности) для каждой стратегии, потому что они выполняются одинаково каждый раз.
Кроме того, мы можем использовать библиотеку под названием pyfolio, чтобы быстро рассчитать наиболее важные показатели эффективности рядов доходностей.
pf.create_simple_tear_sheet(buy_and_hold_results.returns)
Стратегия простого скользящего среднего
Вторая стратегия, которую мы рассматриваем, основана на простой скользящей средней (SMA). Логика стратегии может быть обобщена следующим образом:
- когда цена пересекает 20-дневную SMA вверх - покупайте акции;
- когда цена пересекает 20-дневную SMA вниз - продайте все акции;
- скользящее среднее использует 19 предыдущих дней, а текущий день - торговое решение на следующий день
%%zipline --start 2018-1-1 --end 2018-12-31 --capital-base 1000.0 -o simple_moving_average.pkl --bundle eu_stocks --trading-calendar XAMS
# imports
from zipline.api import order_percent, record, symbol, order_target
from zipline.finance import commission
# parameters
MA_PERIODS = 20
SELECTED_STOCK = 'UNA'
def initialize(context):
context.asset = symbol(SELECTED_STOCK)
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
context.has_position = False
def handle_data(context, data):
price_history = data.history(context.asset, fields="price", bar_count=MA_PERIODS, frequency="1d")
ma = price_history.mean()
# cross up
if (price_history[-2] < ma) & (price_history[-1] > ma) & (not context.has_position):
order_percent(context.asset, 1.0)
context.has_position = True
# cross down
elif (price_history[-2] > ma) & (price_history[-1] < ma) & (context.has_position):
order_target(context.asset, 0)
context.has_position = False
record(price=data.current(context.asset, 'price'),
moving_average=ma)
Ниже мы иллюстрируем стратегию:
График ниже показывает ценовой ряд вместе с 20-дневной скользящей средней. Мы дополнительно отметили ордера, которые исполняются на следующий торговый день после генерации сигнала.
Пересечение скользящих средних
Эту стратегию можно считать продолжением предыдущей - вместо одного скользящего среднего мы используем два с разными размерами окна. 100-дневная скользящая средняя - это та, которая требует больше времени, чтобы приспособиться к внезапным изменениям цены, а 20-дневная - намного быстрее, чтобы учесть внезапные изменения.
Логика стратегии заключается в следующем:
- когда быстрая МА пересекает медленную вверх, мы покупаем актив;
- когда медленная МА пересекает быструю вверх, мы продаем актив.
Имейте в виду, что для этой стратегии можно рассмотреть множество различных комбинаций длин окон, определяющих быструю и медленную МА.
%zipline --start 2018-1-1 --end 2018-12-31 --capital-base 1000.0 -o moving_average_crossover.pkl --bundle eu_stocks --trading-calendar XAMS
# imports
from zipline.api import order_percent, record, symbol, order_target
from zipline.finance import commission
# parameters
SELECTED_STOCK = 'UNA'
SLOW_MA_PERIODS = 100
FAST_MA_PERIODS = 20
def initialize(context):
context.asset = symbol(SELECTED_STOCK)
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
context.has_position = False
def handle_data(context, data):
fast_ma = data.history(context.asset, 'price', bar_count=FAST_MA_PERIODS, frequency="1d").mean()
slow_ma = data.history(context.asset, 'price', bar_count=SLOW_MA_PERIODS, frequency="1d").mean()
# Trading logic
if (fast_ma > slow_ma) & (not context.has_position):
order_percent(context.asset, 1.0)
context.has_position = True
elif (fast_ma < slow_ma) & (context.has_position):
order_target(context.asset, 0)
context.has_position = False
record(price=data.current(context.asset, 'price'),
fast_ma=fast_ma,
slow_ma=slow_ma)
Ниже мы нанесли две скользящие средние поверх ценового ряда. Мы видим, что стратегия генерировала гораздо меньше сигналов, чем основанная на SMA.
MACD
MACD расшифровывается как Moving Average Convergence/Divergence (схождение/расхождение скользящих средних) и является индикатором/осциллятором, используемым в техническом анализе котировок акций.
MACD представляет собой набор из трех временных рядов, рассчитанных с использованием исторических цен закрытия:
MACD - разница между быстрой (с более коротким периодом) и медленной (с более длинным периодом) экспоненциальными скользящими средними;
signal - EMA на серии MACD;
divergence - разница между MACD и signal.
MACD задается количеством дней, использованных для расчета трех скользящих средних - MACD (a, b, c). Параметр a соответствует быстрой EMA, b - медленной EMA, c - signal. Наиболее распространенной настройкой, также используемой в этой статье, является MACD (12,26,9). Исторически эти цифры соответствовали 2 неделям, 1 месяцу и 1,5 неделям на основе 6-дневной рабочей недели.
Следует помнить, что MACD является запаздывающим индикатором, поскольку он основан на скользящих средних. Вот почему MACD менее полезен для акций, которые не показывают трендового движения.
Стратегия, которую мы используем в этой статье, может быть описана следующим образом:
- покупать акции, когда MACD пересекает сигнальную линию вверх;
- продавать акции, когда MACD пересекает сигнальную линию вниз.
%%zipline --start 2018-1-1 --end 2018-12-31 --capital-base 1000.0 -o macd.pkl --bundle eu_stocks --trading-calendar XAMS
# imports ----
from zipline.api import order_target, record, symbol, set_commission, order_percent
import matplotlib.pyplot as plt
import talib as ta
from zipline.finance import commission
# parameters ----
SELECTED_STOCK = 'UNA'
#initialize the strategy
def initialize(context):
context.asset = symbol(SELECTED_STOCK)
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
context.has_position = False
def handle_data(context, data):
price_history = data.history(context.asset, fields="price", bar_count=34, frequency="1d")
macd, macdsignal, macdhist = ta.MACD(price_history, 12, 26, 9)
if (macdsignal[-1] < macd[-1]) and (not context.has_position):
order_percent(context.asset, 1.0)
context.has_position = True
if (macdsignal[-1] > macd[-1]) and (context.has_position):
order_target(context.asset, 0)
context.has_position = False
record(macd = macd[-1], macdsignal = macdsignal[-1], macdhist = macdhist[-1], price=price_history[-1])
Ниже мы строим MACD и сигнальные линии, где пересечения указывают сигналы на покупку/продажу. Кроме того, можно изобразить divergence MACD в форме гистограммы (ее обычно называют гистограммой MACD).
RSI
RSI обозначает Relative Strength Index (индекс относительной силы), и он является еще одним техническим индикатором, который мы можем использовать для создания торговых стратегий. RSI классифицируется как осциллятор моментума и измеряет скорость и величину направленных ценовых движений. Моментум описывает скорость, с которой цена актива растет или падает.
Не вдаваясь в слишком много технических деталей, RSI измеряет моментум как отношение более высоких закрытий к более низким закрытиям. Активы с более сильными положительными изменениями имеют более высокий RSI, чем активы с более сильными отрицательными изменениями.
Вывод RSI представляет собой число по шкале от 0 до 100 и обычно рассчитывается на 14-дневной основе. Чтобы генерировать торговые сигналы, обычно задается низкий и высокий уровни RSI на 30 и 70 соответственно. Интерпретация пороговых значений заключается в том, что нижний показатель указывает на перепроданность актива, а верхний - на перекупленность актива.
Иногда также указывается средний уровень (на полпути между минимумом и максимумом), например, в случае стратегий, которые также допускают короткие продажи. Мы также можем выбрать более экстремальные пороги, такие как 20 и 80, которые бы указывали на более сильный моментум. Тем не менее, это должно делаться с использованием знаний предметной области или путем проведения тестирования на истории.
Стратегия, которую мы рассматриваем, может быть описана следующим образом:
- когда RSI пересекает нижний порог (30) - покупайте актив;
- когда RSI пересекает верхний порог (70) - продавайте актив;
Ниже мы наносим на график RSI вместе с верхним и нижним порогом.
Оценка эффективности
Последний шаг включает в себя объединение всех метрик производительности в один DataFrame и проверку результатов. Мы видим, что в случае нашего тестирования на истории стратегия, основанная на RSI, показала наилучшие результаты с точки зрения генерируемой прибыли. У нее также был самый высокий коэффициент Шарпа - самая высокая избыточная доходность (в данном случае доходность, поскольку мы не рассматриваем безрисковый актив) на единицу риска. Второй лучшей стратегией оказалась простая покупка и удержание. Также важно отметить, что две стратегии - на основе MACD и SMA, на самом деле приносили убытки.
perf_df = pd.DataFrame({'Buy and Hold': buy_and_hold_perf,
'Simple Moving Average': sma_perf,
'Moving Average Crossover': mac_perf,
'MACD': macd_perf,
'RSI': rsi_perf})
perf_df.transpose()
Заключение
В этой короткой статье я показал, как объединить zipline с talib, чтобы протестировать торговые стратегии на основе популярных технических индикаторов, таких как скользящие средние, MACD, RSI и т. д. Но это было только начало, так как можно было создать гораздо более сложные стратегии.
Некоторые из возможных будущих направлений:
- включить несколько активов в стратегии;
- разрешить короткие продажи;
- комбинировать показатели.
Проведите тестирование различных параметров для каждой стратегии, чтобы найти наиболее эффективные Мы также должны помнить, что тот факт, что стратегия работала хорошо в прошлом, не является гарантией того, что это повторится в будущем.
Вы можете найти код, используемый для этой статьи, в моем GitHub.
Комментариев нет:
Отправить комментарий