В данном исследовании рассматривается разработка стратегии машинного обучения для торговли на финансовых рынках. Мы используем данные технического анализа и данные AlgoPack для обучения модели случайного леса и оценки ее эффективности на реальных и фейковых данных.
Исследование использует следующие библиотеки R: rusquant, caret, nnet, e1071, caretEnsemble, PerformanceAnalytics. Для анализа данных используется как пример акции Газпрома ‘(GAZP’). Технические и свечные данные загружаются с использованием функции getSymbols.Gigapack из пакета rusquant.
library(rusquant)
## Loading required package: quantmod
## Loading required package: xts
## Loading required package: zoo
##
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
##
## as.Date, as.Date.numeric
## Loading required package: TTR
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
## Loading required package: data.table
##
## Attaching package: 'data.table'
## The following objects are masked from 'package:xts':
##
## first, last
## Loading required package: jsonlite
## Loading required package: httr
##
## Attaching package: 'rusquant'
## The following objects are masked from 'package:quantmod':
##
## getDividends, getSplits
library(caret)
## Loading required package: ggplot2
## Loading required package: lattice
##
## Attaching package: 'caret'
## The following object is masked from 'package:httr':
##
## progress
library(nnet)
library(e1071)
library(caretEnsemble)
##
## Attaching package: 'caretEnsemble'
## The following object is masked from 'package:ggplot2':
##
## autoplot
library(PerformanceAnalytics)
##
## Attaching package: 'PerformanceAnalytics'
## The following objects are masked from 'package:e1071':
##
## kurtosis, skewness
## The following object is masked from 'package:graphics':
##
## legend
tech_fields = getSymbolList('gigapack',type = 'tech')
candles_fields = getSymbolList('gigapack',type = 'candles')
universe = c('ROSN','SBER','GAZP','LKOH','AFLT')
Для конкретного символа из выбранного набора мы загружаем реальные технические и микроструктурные данные. Делим всю выборку на train (до января 2023), test (после января 2023)
symbol = universe[3]
###### get real data ##########
technical_features = getSymbols.Gigapack(symbol, type = 'tech') #technical indicators
all_features = technical_features[,c('symbol','date','close','EMA_EXP_RSI_28','chaikin_mfi_10','atr_28')]
all_features = getSymbols.Gigapack(symbol,field = paste(candles_fields[c(41,134,145,209)],collapse = ','),type = 'candles')
all_features[,next_ret:=shift(close/shift(close,1)-1,-1),by='symbol']
dt_train = all_features[date<'2023-01-01',]
dt_test = all_features[date>'2023-01-01',]
Затем создается фейковый набор данных с аналогичными статистическими характеристиками, но случайно сгенерированный. Обучающие и тестовые наборы формируются для реальных данных, а также для каждого из 10 фейковых наборов.
# technical data
all_features_fake = getSymbols.Gigapack(symbol, type = 'tech',field = 'EMA_EXP_RSI_28,chaikin_mfi_10,atr_28',fake = T,reps = 10)
# add algopack data
for(i in c(41,134,145,209))
{
algopack_data_fake = getSymbols.Gigapack(symbol,field = candles_fields[i],type = 'candles',fake = T,reps = 10)
all_features_fake = merge(all_features_fake,algopack_data_fake,by=c('date','symbol','sim'))
}
all_features_fake[,next_ret:=shift(close/shift(close,1)-1,-1),by='symbol,sim']
dt_train_fake = all_features_fake[date<'2023-01-01',]
Модель случайного леса обучается на реальных данных с использованием функции train из пакета caret. Обученная модель затем используется для прогнозирования величины следующего дохода (next_ret). Стратегия торговли (перевертыш) определяется на основе прогнозов модели (если положительная доходность то покупка, иначе продажа): покупка (1), или продажа (-1).
### fit train ##
fitControl <- trainControl(method="repeatedcv", number=2, repeats=2)
rf_model <- train(next_ret ~ .,
data = dt_train[,-c('date','symbol','close')],
method = "rf",
trControl=fitControl)
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
### train ##
rf_forecast <- predict(rf_model, dt_train)
dt_train$predict = rf_forecast
dt_train$position = 0
dt_train[predict>0,]$position=1
dt_train[predict<0,]$position=-1
На реальных данных рассчитывается портфель стратегии, включая расчет прибыли и учет комиссий. Проводится анализ производительности стратегии с использованием метрик, таких как Sharpe Ratio и Calmar Ratio. Естественно, что этот кусок данных мы безбожно зафитили - поэтому получим очень высокий Шарп.
#add commisions
dt_train$pnl = dt_train$position * dt_train$next_ret
dt_train[,I_trade:= c(NA,diff(position))]
dt_train$pnl = dt_train$pnl - (0.1/100)*abs(dt_train$I_trade)
train_pnl = xts(dt_train$pnl,order.by = as.Date(dt_train$date))
charts.PerformanceSummary(train_pnl['2020/'],geometric = T)
SharpeRatio.annualized(train_pnl['2020/'])
## [,1]
## Annualized Sharpe Ratio (Rf=0%) 96.131
CalmarRatio(train_pnl['2020/'])
## [,1]
## Calmar Ratio 1809.947
#names(portfolio_opt) = 'alpha_portfolio'
Для каждого из 10 фейковых наборов данных применяется обученная модель, и анализируется прибыльность каждой стратегии. Плотность распределения прибыли по всем фейковым стратегиям сравнивается с реальной стратегией.
### fake calc strategy #
dt_train_fake$position = 0
dt_train_fake$predict = 0
dt_train_fake$pnl = 0
dt_train_fake$I_trade = 0
for(j in 1:10)
{
rf_forecast <- predict(rf_model, dt_train_fake[sim==j])
dt_train_fake[sim==j]$predict = rf_forecast
dt_train_fake[sim==j & predict>0,]$position=1
dt_train_fake[sim==j & predict<0,]$position=-1
dt_train_fake$pnl = dt_train_fake$position * dt_train_fake$next_ret
dt_train_fake[,I_trade:= c(NA,diff(position)),by='symbol,sim']
}
dt_train_fake$pnl = dt_train_fake$pnl - (0.1/100)*abs(dt_train_fake$I_trade)
dt_test = na.omit(dt_test)
mean(dt_train_fake[,sum(pnl,na.rm = T),by = sim]$V1)
## [1] 8.927844
fake_strats = dt_train_fake[,sum(pnl,na.rm = T),by = sim]
Для каждого из 10 фейковых наборов данных применяется обученная модель, и анализируется прибыльность каждой стратегии. Плотность распределения прибыли по всем фейковым стратегиям сравнивается с реальной стратегией. На альтернативных данных показатели стратегии значительно отличаются от реальной доходности, что означает что мы зафитили слишком сильно данные и наша стратегия не проходит фильтр альтернативной истории AlterGiga - значит она не будет работать и на новых данных.
library(ggplot2)
ggplot(fake_strats, aes(V1*100,fill='blue',alpha=.2)) + geom_density() + xlim(0, 1500)+
geom_vline(xintercept = 100*sum(dt_train$pnl,na.rm = T))+ labs(x = "Sum of PnL, %, 2020-01-01 - 2023-01-01")
Наконец, обученная модель применяется к новым данным для тестирования устойчивости стратегии к изменениям на рынке. Проводится анализ эффективности и сравнение с результатами на реальных данных и естественно мы получаем отрицательную доходность за счет оверфитинга случайного леса на обучающих данных.
### test ##
rf_forecast <- predict(rf_model, dt_test)
dt_test$predict = rf_forecast
dt_test$position = 0
dt_test[predict>0,]$position=1
dt_test[predict<0,]$position=-1
dt_test$pnl = dt_test$position * dt_test$next_ret
dt_test[,I_trade:= c(NA,diff(position))]
dt_test$pnl = dt_test$pnl - (0.1/100)*abs(dt_test$I_trade)
test_pnl = xts(dt_test$pnl,order.by = as.Date(dt_test$date))
charts.PerformanceSummary(test_pnl['2020/'],geometric = T)
SharpeRatio.annualized(test_pnl['2020/'])
## [,1]
## Annualized Sharpe Ratio (Rf=0%) -0.8557906
CalmarRatio(test_pnl['2020/'])
## [,1]
## Calmar Ratio -0.6631862
Мы показали пример того, как легко переобучаются ML алгоритмы и какие существуют инструменты для того, чтобы улавливать такое переубечение. Читателю этого блокнота предлагается самостоятельно попробовать построить ML алгоритм на других инструментах.