В этом опусе рассмотрим попытку использования алгоритм случайного леса для создания торгового модели
для слива денег на примере индекса IMOEX. Используется язык питон и библиотеки pandas и scikit-learn. Модель будет предсказывать сторону закрытие на следующий день, т.е. оно положительное или отрицательное, и на основании этого строится торговая система.
df["Tomorrow"] = df["Close"].shift(-1)
df["Target"] = (df["Tomorrow"] > df["Close"]).astype(int) # наша цель
Очень важно, какие данные будут использоваться для прогнозирования. Здесь используется: показатель силы закрытия бара (т.е. (Close-Low)/(High-Low)) за текущий и предыдущий день, процентные соотношения между ценой закрытия и средними за периоды 2,10,15,25,50 дней по индексам IMOEX, RVI, RGBITR, и плюс цены закрытия индексов RVI, RGBITR.
Для обучения модели используется период 2013-2022 гг., для проверки 2023-2024г.:
train = df.loc['2013':'2022']
test = df.loc['2023':]
Для создания модели используется <a href=«
scikit-learn. org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html»>RandomForestClassifier из библиотеки scikit-learn. Подгоняется только один параметр min_samples_split (минимальное число объектов, необходимое для того, чтобы узел дерева мог бы расщепиться), по умолчанию этот параметр равен 2, но для того, чтобы модель была не слишком переобученной, подгонка будет идти от 50 до 200 с шагом 10. Подгонка идет через кросс-валидацию
TimeSeriesSplit с помощью процедуры
GridSearchCV.
tscv = TimeSeriesSplit()
model = RandomForestClassifier(random_state = 0)
forest_params = {"min_samples_split": range(50,201,10)}
forest_grid = GridSearchCV(model, forest_params, n_jobs=-1, verbose = 1, cv = tscv)
forest_grid.fit(train[new_predictors], train['Target'])
По итогу кросс-валидации получилось, что лучший параметр и лучший оценка:
best_params_ {'min_samples_split': 70}
best_score_ = 0.4986149584487535
Теперь, проверяем на тестовом периоде полученную модель. Торговая система простая, если модель предсказывает, что закрытие завтра положительное, то встаем в лонг и на следующий день продаем, если наоборот, то в шорт, т.е. система всегда в рынке. Комиссия, проскальзывание и т.п. не учтены.
Вот такой «черный ящик» получился.
Полный код:
import pandas as pd
ticker = 'imoex'.upper()
filename = "..\\h5\\moex.h5" # здесь хранятся минутки
with pd.HDFStore(filename) as store:
store = pd.HDFStore(filename)
data_1min = store[ticker]
rgbitr_1min = store['RGBITR']
rvi_1min = store['RVI']
def convert_to_daily(intraday_data):
return intraday_data.between_time('10:00', '18:40').resample('D').agg({'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last'}).dropna()
df = convert_to_daily(data_1min)
rgbitr = rgbitr_1min.between_time('10:00', '18:40').resample('D').agg({'Close': 'last'}).dropna()
rgbitr = rgbitr.rename(columns = {'Close' : 'rgbitr'})
rvi = rvi_1min.between_time('10:00', '18:40').resample('D').agg({'Close': 'last'}).dropna()
rvi = rvi.rename(columns = {'Close' : 'rvi'})
df = pd.concat([df, rgbitr, rvi], axis = 1)
df["Tomorrow"] = df["Close"].shift(-1)
df["Target"] = (df["Tomorrow"] > df["Close"]).astype(int)
df['ibs'] = (df['Close'] - df['Low'])/(df['High'] - df['Low'])
df['ibs1'] = df['ibs'].shift()
df['pct_chg'] = df['Close'].pct_change()
df = df.dropna()
new_predictors = ['ibs','ibs1', 'rgbitr', 'rvi' ]
horizons = [2,10,15,25,50]
for col in ['Close', 'rgbitr', 'rvi']:
for horizon in horizons:
rolling_averages = df.rolling(horizon).mean()
ratio_column = f"{col}_Ratio_{horizon}"
df.loc[:, ratio_column] = df[col] / rolling_averages[col] - 1
new_predictors += [ratio_column]
df = df.dropna()
train = df.loc['2013':'2022']
test = df.loc['2023':]
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit()
model = RandomForestClassifier(random_state = 0)
forest_params = {"min_samples_split": range(50,201,10)}
forest_grid = GridSearchCV(model, forest_params, n_jobs=-1, verbose = 1, cv = tscv)
forest_grid.fit(train[new_predictors], train['Target'])
print(f"best_params_ {forest_grid.best_params_}")
print(f"best_score_ = {forest_grid.best_score_}")
test_preds = forest_grid.predict(test[new_predictors])
test = test.assign( preds = test_preds)
test['pos'] = test['preds']
test['pos'] = test['pos'].replace(0, -1)
test['ret'] = test['pos'].shift()*test['pct_chg']
import matplotlib.pyplot as plt
fig, axs = plt.subplots(1,2,figsize=(12, 4))
axs[0].axis([0, 10, 0, 10])
axs[0].text(1, 9, f"{ticker}")
axs[0].text(1, 7, f"min_samples_split = {forest_grid.best_params_['min_samples_split']}")
test['ret'].cumsum().plot(ax=axs[1])
axs[1].grid()
axs[1].set_title(f"test")
best_score — лучшая средняя оценка после кросс-валидации.
Хотя, как ни странно, на тестовом периоде оценка будет 0,617.
Не густо фичей).
А как выглядит график зависимости метрики качества от min_samples_split?
Выглядит он, скажем прямо, так себе.
Код, в основном, сам писал.