Riskplayer
Riskplayer личный блог
11 марта 2025, 17:45

Пересечение 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 Комментариев
  • ves2010
    11 марта 2025, 18:24
    нуу как бы этож без комиссий  и надо тестить не на 1ом лоте а на постоянной сумме... 
  • IgorK
    11 марта 2025, 19:11
    Отличная статья! Интересно было бы посмотреть, как стратегия покажет себя на EMA вместо SMA — они быстрее реагируют на изменения цены и могут уменьшить запаздывание сигналов. По идее, изменения в коде будут минимальные.

Активные форумы
Что сейчас обсуждают

Старый дизайн
Старый
дизайн