Данная статья продолжает цикл анализа простых стратегий со стандартными индикаторами. Тестируем стратегии в Quantopian, а пишем на Python🐍. В этот раз мы сравним индикатор Rate-of-Change (ROC) и популярное пересечение скользящих средних SMA(50) и SMA(200).
Дополнительно рассмотрим подход быстрого получения доходности📈 и просадки📉 простых стратегий в блокноте Jupyter.
Индикатор ROC показывает процент изменения текущей цены относительно прошлого значения. Параметр индикатора — кол-во дней, между которыми сравниваем цены. Формула:
Значение индикатора находится вокруг нуля.
✊Купи и держи по ROC(200)
Проверять будем на NYSE:SPY, но помним, что все активы ведут себя по разному. Условия:
- Период: 2004-2018.
- Время сделки: за 1 час до закрытия рынка.
- Капитал: $100K.
- Покупка, когда ROC(200) >= 0.
- Продажа, когда ROC(200) < 0.
Код условия на Quantopian:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# получаем цены и рассчитываем ROC(200) price_hist = data.history(context.asset, 'close', 400, '1d') roc = talib.ROC(price_hist, timeperiod=200) # сигнал на значении выше нуля allow = roc[-1] >= 0 # ... if data.can_trade(context.asset): if allow: # покупаем актив на 100% портфеля, если разрешено order_target_percent(context.asset, 1.) else: # закрываем позицию при запрете order_target_percent(context.asset, 0.) |
Результаты ROC(200) и сравнение с пересечением SMA(50) и SMA(200):
С 2004 год SPY вырос на +230% имея просадку в 2008 году около -55%. Рядом с этим показателем только пересечение SMA(50) и SMA(200) с меньшей просадкой. ROC(200) оказался даже хуже пересечения цены и SMA(200), показав чуть меньшее количество сделок за весь период. Внизу показан результат сглаженного ROC по формуле из этой статьи, который показал наихудший результат.
🔬Как получить результаты без бэктеста?
Тесты дают более точную картину и это актуально при сложных стратегиях. Но для стратегий с редкими ребалансировками, где можно использовать цену закрытия, мы получим максимально приближенные результаты используя Jupyter, pandas и numpy.
Получение доходности
Получая итоговую доходность для серии pandas, мы проходим следующие шаги:
- Получаем ежедневную доходность chg=prices/prices.shift(1).
- Устанавливаем первое значение chg.iloc[0]=1.
- При необходимости применяем фильтр и смещаем условия на один день (сделка проходит после получения сигнала) sma200=chg[(df.close>sma200).shift(1)]. Это важно запомнить, чтобы не заглядывать в будущее.
- Считаем доходность перемножив все значения result=np.prod(sma200) .
Код есть в telegram: @quantiki.
Получение просадки
Получить максимальную просадку можно так:
- Получаем ежедневную доходность chg=prices/prices.shift(1).
- Получаем доходность на каждый день
series.rolling(total, min_periods=1).apply(np.prod). - Получаем абсолютный максимум на каждый день
series.rolling(total, min_periods=1).max(). - Получаем просадку на каждый день относительно последнего максимума series/rolling_max - 1.0.
- Получаем максимальную просадку на каждый день daily_drawdown.rolling(total, min_periods=1).min().
В итоге имеем следующий код для расчёта простых стратегий и их просадки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
def get_drawdown(series, is_prod=False): # расчёт просадки total = len(series.index) if is_prod: series = series.rolling(total, min_periods=1).apply(np.prod) rolling_max = series.rolling(total, min_periods=1).max() daily_drawdown = series/rolling_max - 1.0 daily_drawdown_max = daily_drawdown.rolling(total, min_periods=1).min() return daily_drawdown_max.min() def get_strategy_returns(df, symbol='-'): # фильтр периода fltr = df['dt'] >= '2004-01-01' # функция накопления доходности calc_returns = lambda x: (np.prod(x) - 1) * 100 # получаем дневное изменение цены и оставляем необходимый период chg = (df.close / df.close.shift(1))[fltr] chg.iloc[0] = 1 # начинаем всегда с единицы benchmark = chg sma200_chg = chg[(df.close.rolling(1).mean() - df.close.rolling(200).mean()).shift(1) >= 0] sma50x200_chg = chg[(df.close.rolling(50).mean() - df.close.rolling(200).mean()).shift(1) >= 0] # ... returns = { 'symbol': symbol, 'bench': calc_returns(benchmark), 'bench dd': get_drawdown(benchmark, is_prod=True) * 100, 's200': calc_returns(sma200_chg[fltr]), 's200 dd': get_drawdown(sma200_chg[fltr], is_prod=True) * 100, # ... } return returns |
Результаты выглядят следующим образом:
- bench — бенчмарк, результат удержания актива в течение всего времени.
- * dd — максимальная просадка при каждом условии.
- roc200 — удержание при ROC(200) больше нуля.
- roc200s — использование сглаженного ROC(200).
- s200 — пересечение цены и SMA(200).
- s50x200 — пересечение SMA(50) и SMA(200).
Сравнив результаты с тестами на Quantopian видна разница, которой можно принебречь для принятия первичных решений. В некоторых случаях разница в доли процентов. Высока вероятность, что расхождение связано со временем ребалансировки стратегий (за 1 час до закрытия рынка). Мы же делаем расчёт по цене закрытия.
🏁Вывод
Мы увидели, что использование ROC(200), как фильтра трендов, является невыгодным. Также научились быстро получать доходность и просадку для простых стратегий, достаточные для принятия решений, а работающие на порядок быстрее.
В следующий раз мы ещё немного помучаем ROC. Проверим доходность, используя силу тренда, пересечение разных периодов ROC и создадим индикатор Know Sure Thing (KST), созданный Мартином Прингом на основе ROC.
💬В комментариях пишите, ваши вопросы по тесту и коду. Запрашивайте полный код стратегии и блокнота.
Автор Quantrum.me
Telegram-канал📣: @quantiki
Подбор и тестирование портфелей. Подключение стратегий к IB.