Блог им. Riskplayer

Пересечение SMA

    В этой статье рассматривается очень простая система. Если быстрая SMA пересекает медленную SMA снизу вверх, то покупаем, если пересекает сверху вниз, то ликвидируем позицию. Только лонг, никаких стопов, т.к. ими пользуется только трусы. Для упрощения комиссия не учитывается. Для тестирования берется портфель ликвидных бумаг: SBER, GAZP, GMKN, LKOH, ROSN, MOEX, NVTK, PLZL, AFLT, MTLR, NLMK, MAGN, TRNFP, MGNT. Для этой системы нужно найти два параметра: периоды быстрой и медленной SMA.  Для подбора этих параметров берётся диапазон с 01.01.2015г. по 01.01.2023г. А диапазон с 01.01.2023г. по 07.03.2025г. используется для проверки.
   Для всех бумаг используется система с одинаковыми параметрами. Чтобы найти эти параметры, сначала для каждой бумаги строится свой профиль доходности в зависимости от периодов (т.е. двух параметров) на испытательном диапазоне. Затем все эти профили доходности суммируются и усредняются, в итоге мы имеем усредненный профиль доходности портфеля. Находится максимальная доходность на этом профиле и соответствующие ему параметры, т.е. два периода (быстрой и медленной SMA), и далее молимся надеемся, чтобы найденные параметры достаточно робастные.
  Итак, с 01.01.20215г. по 01.01.2023г. лучшие параметры:
Best params (4, 8)
Т.е. система следующая: если SMA(4) пересекает снизу вверх SMA(8), то покупаем, если пересекает сверху вниз, то ликвидируем позицию.
Некоторые характеристики системы для каждой бумаги за весь суммарный период (average profit — средний профит на сделку)
:
SBER average profit = 1.654%  pf = 2.3  Max win = 31.144%  max loss -18.70%
GAZP average profit = 0.883%  pf = 1.7  Max win = 37.161%  max loss -7.26%
GMKN average profit = 0.728%  pf = 1.6  Max win = 19.071%  max loss -14.99%
LKOH average profit = 0.365%  pf = 1.2  Max win = 19.565%  max loss -9.32%
ROSN average profit = 0.920%  pf = 1.8  Max win = 20.785%  max loss -13.21%
MOEX average profit = 0.989%  pf = 1.8  Max win = 29.939%  max loss -8.98%
NVTK average profit = 0.591%  pf = 1.4  Max win = 25.483%  max loss -11.18%
PLZL average profit = 2.084%  pf = 2.4  Max win = 64.463%  max loss -20.14%
AFLT average profit = 1.125%  pf = 1.7  Max win = 37.095%  max loss -9.32%
MTLR average profit = 3.068%  pf = 2.8  Max win = 160.692%  max loss -25.22%
NLMK average profit = 1.057%  pf = 1.7  Max win = 34.744%  max loss -13.75%
MAGN average profit = 1.342%  pf = 1.9  Max win = 33.568%  max loss -16.13%
TRNFP average profit = 0.955%  pf = 1.7  Max win = 33.902%  max loss -11.36%
MGNT average profit = 0.318%  pf = 1.2  Max win = 21.875%  max loss -18.84%
График накопленной доходности без реинвестирования (включая испытательный и контрольный диапазон):
Пересечение SMA
На диапазоне 01.01.2015-01.01.2023 показывает доходность 125%. Цифра не ошеломительная за 8 лет, зато на контрольном отрезке около 70% за чуть больше, чем два года.
Теперь рассмотрим, что будет, если текущая прибыль не будет пропиваться выводиться, а реинвестироваться, т.е. получится график сложного процента портфеля.
Пересечение SMA
За срок 01.01.2015-01.01.2023 доходность приблизительно 230%, зато на контрольном отрезке более чем в два раза капитал увеличился. Но просадки увеличились. Использование плеча может увеличить доходность, но и просадки станут больше.

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


Код на питоне:
import pandas as pd
import numpy as np
from datetime import datetime
import itertools

import talib


def get_daily_data(ticker):
    
    filename = "..\\moex.h5"

    with pd.HDFStore(filename) as store:
        data_1min = store[ticker]
    
    data_1min = data_1min[['Close']]
    #df = data_1min.resample('D').agg({'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last'})
    df = data_1min.resample('D').agg({'Close': 'last'})

    df = df.dropna()
    
    return df    


def get_arr_of_trades(df, first_timeperiod = 10, second_timeperiod = 30):

    df['fast_sma'] = talib.SMA(df['Close'], first_timeperiod)
    df['slow_sma'] = talib.SMA(df['Close'], second_timeperiod)

    df['signal_buy'] = (df['fast_sma'] > df['slow_sma']) & (df['fast_sma'].shift() < df['slow_sma'].shift())
    
    df['signal_sell'] = (df['fast_sma'] < df['slow_sma']) & (df['fast_sma'].shift() > df['slow_sma'].shift())
    
    df['pos'] = np.NaN
    df['pos'] = df['pos'].mask(df['signal_buy'], 1)
    df['pos'] = df['pos'].mask(df['signal_sell'], 0)
    df.loc[df.index[0], 'pos'] = 0
    df['pos'] = df['pos'].ffill()
    
    df['diff'] = df['Close'].diff()
    df['denominator'] = np.NaN
    df['denominator'] = df['denominator'].mask((df['pos']==1) & (df['pos'].shift() == 0), df['Close'])
    df['denominator'] = df['denominator'].ffill()
    df['ret'] = 0
    df['ret'] = df['diff']*df['pos'].shift()/df['denominator']
    df['cumret'] = df['ret'].cumsum()
    
    df['isStartNewTrade'] = 0
    df['isStartNewTrade'] = df['isStartNewTrade'].mask((df['pos']==1) & (df['pos'].shift() == 0), 1)
    df['count'] = df['isStartNewTrade'].cumsum()
    df['count'] = df['count'].mask(df['pos']== 0, 0)
    df['count'] = df['count'].shift()
    df['isFinishTrade'] = False
    df['isFinishTrade'] = df['isFinishTrade'].mask((df['pos'] == 0) & (df['pos'].shift() == 1), True)
    df['isFinishTrade'] = df['isFinishTrade'].mask((df['pos'] == 1) & (df.index == df.index[-1]) & (df['pos'].shift() == 1), True)
    
    return df


list_tickers = ['sber', 'gazp', 'gmkn', 'lkoh', 'rosn', 'moex', 'nvtk', 'plzl', 'aflt',
                'mtlr', 'nlmk', 'magn', 'trnfp', 'mgnt']
list_tickers = [x.upper() for x in list_tickers]


start_date = datetime(2015, 1, 1)
finish_date = datetime(2023, 1, 1)

profil = np.zeros((len(list_tickers), 51, 51))

for idx, ticker in enumerate(list_tickers):
    print(ticker)

    df = get_daily_data(ticker)
    df = df[(df.index > start_date) & (df.index < finish_date)]

    max_period = profil.shape[1]
    iterator = itertools.combinations(range(2, max_period), 2)

    for item in iterator:
        i = item[0]
        j = item[1]
        df = get_arr_of_trades(df, i, j)
        profil[idx, i, j] = df.iloc[-1]['cumret']

sum_profil = profil.mean(axis = 0)

result = np.unravel_index(np.argmax(sum_profil), sum_profil.shape)

first_timeperiod = result[0]
second_timeperiod = result[1]

print('Best params', result)


rng = pd.date_range(start = start_date.date(), end= datetime.today().date())
rng = rng[rng.weekday < 5]

# df_simple для учета простых процентов
# df_compound для учета сложного процента

df_simple = pd.DataFrame(index = rng)
df_compound = pd.DataFrame(index = rng)

for idx, ticker in enumerate(list_tickers):

    df = get_daily_data(ticker)
    df = df[df.index > start_date]

    df = get_arr_of_trades(df, first_timeperiod, second_timeperiod)
    df_simple[ticker] = df['cumret']
    trades = df.groupby(['count'])['ret'].sum()
    trades = trades[trades.index > 0]
    df_t = pd.DataFrame(data = trades.values, index = df[df['isFinishTrade']].index, columns = [ticker])
    df_compound = df_compound.join(df_t)
    av_profit = trades.mean()
    pf = - trades[trades > 0].sum()/trades[trades < 0].sum()
    max_win = trades.max()
    max_loss = trades.min()
    print(f'{ticker} average profit = {av_profit:.3%}  pf = {pf:.2}  Max win = {max_win:.3%}  max loss {max_loss:.2%}')

df_simple.iloc[0] = df_simple.iloc[0].fillna(0)
df_simple = df_simple.ffill()
df_simple['average_simple_ret'] = df_simple[list_tickers].mean(axis = 1)
#df_simple['average_simple_ret'].plot(grid = True, title = 'Simple percent')

df_compound = df_compound.fillna(0) + 1
df_compound = df_compound.cumprod() - 1
df_compound['average_compound_ret'] = df_compound[list_tickers].mean(axis = 1)
#df_compound['average_compound_ret'].plot(grid = True, title = 'Compound percent')

pd.concat([df_simple['average_simple_ret'], df_compound['average_compound_ret']], axis=1).plot(grid = True)
11 комментариев
нуу как бы этож без комиссий  и надо тестить не на 1ом лоте а на постоянной сумме... 
avatar
ves2010, так здесь тест по сути на постоянной сумме. А по поводу комиссии, здесь скорее набросок, а не серьезное исследование, чтобы понять, есть ли смысл дальше копать.
avatar
Riskplayer, тогда все равно не сможешь реально торговать… т.к  3 года боковик 17 18 19гг… это нереально… даже год боковика очень тяжело выдержать… а уж 3... 
avatar
ves2010, С этим не поспоришь.
avatar
ves2010, если не хватит терпелки 2-3 года потерпеть, то и начинать не стоит в этом деле. Не хфт же.
Sergey Pavlov, у меня не хватает я даю год если нет профита то перенастраиваю если снова нет пол года профита то выкидываю
avatar
ves2010, ну вот и получилось, что надо хотя бы полтора года терпения иметь.
Отличная статья! Интересно было бы посмотреть, как стратегия покажет себя на EMA вместо SMA — они быстрее реагируют на изменения цены и могут уменьшить запаздывание сигналов. По идее, изменения в коде будут минимальные.
avatar
IgorK, исправил в коде вместо SMA на EMA. Получилось на 01/01/2015-01/01/2023 лучшие параметры: быстрый EMA(2) и медленный EMA(5). Вот такой график на 01/01/2015-07/03/2025г., т.е. на суммарном диапазоне:

Работает чуть лучше, чем обычные SMA.

avatar
Riskplayer, спасибо. Интересно, что из такой классики, как пересечение SMA, можно ещё что-то выжать. А что там на крипте?..
avatar
Детский лепет про 2 средние. Но у А. Элдера для детей биржи есть индекс силы где приращение цены над средней умножено на объем (!!! ) .
(C — mov(C,S,13)) * V. Под силой Элдер подразумевает объем .
У него сила мишек (L-mov(C,S,13)) * V  и сила бычков. Вот с этого (расхождение цены и индекса силы ) и надо начинать. Я начал с этого в 1999г. Закончить ТА можно средней из синусов. Индикаторы ( кроме объема ) есть производное от цены. Их минус = статичный период. Период надо считать через силу тренда. Сила тренда = функция от перекрытия фракталов  L-ref(H,-2). Тут фрактал из 1 свечи, те частный случай. Чем тренд сильнее, тем период меньше те 3.Чем тренд слабее, то до 34.
avatar

теги блога Riskplayer

....все тэги



UPDONW
Новый дизайн