В этой статье рассматривается очень простая система. Если быстрая 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%
График накопленной доходности без реинвестирования (включая испытательный и контрольный диапазон):

На диапазоне 01.01.2015-01.01.2023 показывает доходность 125%. Цифра не ошеломительная за 8 лет, зато на контрольном отрезке около 70% за чуть больше, чем два года.
Теперь рассмотрим, что будет, если текущая прибыль не будет
пропиваться выводиться, а реинвестироваться, т.е. получится график сложного процента портфеля.

За срок 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)
Работает чуть лучше, чем обычные SMA.