Python

Супертренд – реализация, скрининг и тестирование на историй в Python

Сегодня я расскажу вам об очень простом но сильном индикаторе. А также покажу как охотиться за акциями которые только что вошли в супертренд.

Вот что мы будем делать:

  1. Знакомство с Супертрендом.
  2. Реализация на Python: расчет и визуализация.
  3. Тестирование на историй и оптимизация параметров.
  4. Скрининг.

Супертренд

Для тех, кто уже знаком с индикатором супертренд перейдите вперед и пропустите эту часть. Для тех кто не знает о нем, супертренд – это индикатор следования за трендом, который использует средний истинный диапазон (ATR) и простые Low Hight Average (давайте пока назовем это HL2, через минуту вы поймете почему ), они нужны чтобы сформировать нижнюю и верхнюю полосы.

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

На многих платформах супертренд представлен как на следующем рисунке. С верхней полосой в красном цвете и нижней полосой в зеленом цвете.

Изображение из TradingView

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

Реализация на Python

Супертренд рассчитывается довольно просто.

UPPERBAND = HL2 + MULT * ATR
LOWERBAND = HL2 - MULT * ATR

Где HL2 это просто (high + low)/2 , MULT это простая константа мультипликатора, и ATR это среднее значение истинного диапазона (TR), который является простым различием трех максимальных значений цен: max[(high-low), |high — previous close|, |previous close — low|] .

Однако, есть небольшой поворот, который нам нужно сделать, чтобы настроить полосы в зависимости от направления тренда. Установленные полосы обычно называют конечной нижней полосой (final lowerband) и конечной верхней полосой(final upperband). В основном, регулировка заключается в том, что в случае восходящего тренда окончательная верхняя полоса останется неизменной до тех пор, пока не появится более высокое значение верхней полосы и наоборот. Надеюсь следующий рисунок сделает понимание более ясным.

Перейдем к визуализаций в части тестирования.

Теперь давайте перейдем к коду. Как обычно импортируем все необходимые библиотеки. Данные цен акций мы будем получать используя библиотеку yfinance.

import pandas as pd
import numpy as np
from datetime import datetime
import yfinance as yf
import math
import matplotlib.pyplot as plt
symbol = 'AAPL'
df = yf.download(symbol, start='2020-01-01')

Затем мы можем приступить к расчетам. Давайте начнем с ATR.

high = df['High']
low = df['Low']
close = df['Close']
# расчет ATR
price_diffs = [high - low, 
               high - close.shift(), 
               close.shift() - low]
tr = pd.concat(price_diffs, axis=1)
tr = tr.abs().max(axis=1)
# по умолчанию ATR расчитанный in supertrend indicator использует ewm
atr = tr.ewm(alpha=1/atr_period,min_periods=atr_period).mean() 
# HL2 is simply the average of high and low prices
hl2 = (high + low) / 2
# расчет upperband и lowerband
# notice that final bands are set to be equal to the respective bands
final_upperband = upperband = hl2 + (multiplier * atr)
final_lowerband = lowerband = hl2 - (multiplier * atr)

Обратите внимание, что в приведенном выше расчете, нам нужны две входные переменные: atr_period и multiplier. Давайте для них будет использовать значения по умолчанию 10 и 3 для нашего первого расчета.

# input variables
atr_period = 10
multiplier = 3

Мы еще не закончили. Помните я упоминал некоторую корректировку в нижнюю полосу(lowerband) и верхнюю полосу(upperband), чтобы сформировать заключительную полосу(final_band)?

# initialize Supertrend column to True
supertrend = [True] * len(df)
for i in range(1, len(df.index)):
    curr, prev = i, i-1
    # if current close price crosses above upperband
    if close[curr] > final_upperband[prev]:
        supertrend[curr] = True
    # if current close price crosses below lowerband
    elif close[curr] < final_lowerband[prev]:
        supertrend[curr] = False
    # else, the trend continues
    else:
        supertrend[curr] = supertrend[prev]
        # adjustment to the final bands
        if supertrend[curr] == True and final_lowerband[curr] < final_lowerband[prev]:
            final_lowerband[curr] = final_lowerband[prev]
        if supertrend[curr] == False and final_upperband[curr] > final_upperband[prev]:
            final_upperband[curr] = final_upperband[prev]
    # remove bands depending on the trend direction for visualization
    if supertrend[curr] == True:
        final_upperband[curr] = np.nan
    else:
        final_lowerband[curr] = np.nan

На самом деле есть несколько способов вычислить окончательные нижнюю и верхние полосы(final lower and upper band).

Ниже представлена законченная функция Supertrend.

import pandas as pd
import numpy as np
from datetime import datetime
import yfinance as yf
import math
import matplotlib.pyplot as plt

def Supertrend(df, atr_period, multiplier):
    
    high = df['High']
    low = df['Low']
    close = df['Close']
    
    # calculate ATR
    price_diffs = [high - low, 
                   high - close.shift(), 
                   close.shift() - low]
    true_range = pd.concat(price_diffs, axis=1)
    true_range = true_range.abs().max(axis=1)
    # default ATR calculation in supertrend indicator
    atr = true_range.ewm(alpha=1/atr_period,min_periods=atr_period).mean() 
    # df['atr'] = df['tr'].rolling(atr_period).mean()
    
    # HL2 is simply the average of high and low prices
    hl2 = (high + low) / 2
    # upperband and lowerband calculation
    # notice that final bands are set to be equal to the respective bands
    final_upperband = upperband = hl2 + (multiplier * atr)
    final_lowerband = lowerband = hl2 - (multiplier * atr)
    
    # initialize Supertrend column to True
    supertrend = [True] * len(df)
    
    for i in range(1, len(df.index)):
        curr, prev = i, i-1
        
        # if current close price crosses above upperband
        if close[curr] > final_upperband[prev]:
            supertrend[curr] = True
        # if current close price crosses below lowerband
        elif close[curr] < final_lowerband[prev]:
            supertrend[curr] = False
        # else, the trend continues
        else:
            supertrend[curr] = supertrend[prev]
            
            # adjustment to the final bands
            if supertrend[curr] == True and final_lowerband[curr] < final_lowerband[prev]:
                final_lowerband[curr] = final_lowerband[prev]
            if supertrend[curr] == False and final_upperband[curr] > final_upperband[prev]:
                final_upperband[curr] = final_upperband[prev]

        # to remove bands according to the trend direction
        if supertrend[curr] == True:
            final_upperband[curr] = np.nan
        else:
            final_lowerband[curr] = np.nan
    
    return pd.DataFrame({
        'Supertrend': supertrend,
        'Final Lowerband': final_lowerband,
        'Final Upperband': final_upperband
    }, index=df.index)
    
    
atr_period = 10
atr_multiplier = 3.0

symbol = 'AAPL'
df = yf.download(symbol, start='2020-01-01')
supertrend = Supertrend(df, atr_period, atr_multiplier)
df = df.join(supertrend)

Запуская функцию выше, результат должен быть похожим на этот:

Результат выполнения функций Supertrend

Теперь давайте визуализируем наш результат. Поскольку мы в значительной степени отфильтровали наш результат в DataFrame как мы хотели, часть визуализаций будет для нас довольно проста.

# visualization
plt.plot(df['Close'], label='Close Price')
plt.plot(df['Final Lowerband'], 'g', label = 'Final Lowerband')
plt.plot(df['Final Upperband'], 'r', label = 'Final Upperband')
plt.show()
Обратите внимание, как только цена закрытия выходит за последнюю верхнюю полосу (красная), фокус переключается на нижнюю полосу (зеленую).

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

Давайте проведем простое тестирование индикатора Supertrend на следующей стратегий:

  • Входите когда цена находится в бычьем тренде и выходите когда цена меняется.
def backtest_supertrend(df, investment):
    is_uptrend = df['Supertrend']
    close = df['Close']
    
    # initial condition
    in_position = False
    equity = investment
    commission = 5
    share = 0
    entry = []
    exit = []
    
    for i in range(2, len(df)):
        # if not in position & price is on uptrend -> buy
        if not in_position and is_uptrend[i]:
            share = math.floor(equity / close[i] / 100) * 100
            equity -= share * close[i]
            entry.append((i, close[i]))
            in_position = True
            print(f'Buy {share} shares at {round(close[i],2)} on {df.index[i].strftime("%Y/%m/%d")}')
        # if in position & price is not on uptrend -> sell
        elif in_position and not is_uptrend[i]:
            equity += share * close[i] - commission
            exit.append((i, close[i]))
            in_position = False
            print(f'Sell at {round(close[i],2)} on {df.index[i].strftime("%Y/%m/%d")}')
    # if still in position -> sell all share 
    if in_position:
        equity += share * close[i] - commission
    
    earning = equity - investment
    roi = round(earning/investment*100,2)
    print(f'Earning from investing $100k is ${round(earning,2)} (ROI = {roi}%)')
    return entry, exit, equity
entry, exit, roi = backtest_supertrend(df, 100000)

Выполнение тестов на тикере AAPL от января 2020 до сентября 2021 дает следующий результат. Конечно есть способы улучшить торговую стратегию, например выставив стоп-лосс в торговую стратегию, но для наших целей подходит и эта простая стратегия.

Супертренд график c входящими и выходящими сигналами

Скрининг для Супертренда

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

Сначала мы просматриваем каждый символ в списке акций из S&P 500 и затем отфильтровываем те, которые только что вошли в Супертренд.

# get the full stock list of S&P 500
payload = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')
stock_list = payload[0]['Symbol'].values.tolist()
supertrend_stocks = []
# loop through each symbol
for symbol in stock_list:
    df = yf.download(symbol, start='2010-01-01', threads= False)
    if len(df) == 0: continue
    supertrend = Supertrend(df, atr_period, atr_multiplier)
    if not supertrend['Supertrend'][-2] and supertrend['Supertrend'][-1]:
        supertrend_stocks.append(symbol)

Результат скрининга на 24 сентября следующий.

Если мы взглянем на график, скажем AAL, мы увидим, что AAL просто входит в супертренд и это то что нам нужно!

TradingView

Бонус: Оптимизация параметров

Напомню, что в нашем скрипте мы использовали значения по умолчанию atr_period=10 и multiplier=3. Что происходит когда мы меняем параметры? Мы получаем лучший результат или худший? Что такое оптимальный набор параметров, если он существует? Давайте выясним.

def find_optimal_parameter(df):
    # predefine several parameter sets
    atr_period = [7, 8, 9, 10]
    atr_multiplier = [1.0, 1.5, 2.0, 2.5, 3.0]
    roi_list = []
    
    # for each period and multiplier, perform backtest
    for period, multiplier in [(x,y) for x in atr_period for y in atr_multiplier]:
        new_df = df
        supertrend = Supertrend(df, period, multiplier)
        new_df = df.join(supertrend)
        new_df = new_df[period:]
        entry, exit, roi = backtest_supertrend(new_df, 100000)
        roi_list.append((period, multiplier, roi))
    
    print(pd.DataFrame(roi_list, columns=['ATR_period','Multiplier','ROI']))
    
    # return the best parameter set
    return max(roi_list, key=lambda x:x[2])
df = yf.download('TSLA', start='2010-01-01')
optimal_param = find_optimal_parameter(df)
print(f'Best parameter set: ATR Period={optimal_param[0]}, Multiplier={optimal_param[1]}, ROI={optimal_param[2]}')

Запущенный тест на тикере TSLA, от январь 2020 по сентябрь 2021 покажет что лучшим установленным параметром для супертренда будет atr_period=7 и multiplier=2.5 .

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

Выводы

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

To top