knitr::opts_chunk$set(warning=FALSE, echo=FALSE, message=FALSE) library(data.table) library(roll) library(ggplot2) library(future.apply) library(kableExtra) library(readxl) library(PerformanceAnalytics) library(xts) library(DT) source('C:/Users/Mislav/Documents/GitHub/alphar/R/import_data.R')
Strategija se temelji na hipotezi pozitivnog odnosa ekstremnih vrijednosti prinosa i sentimenta. Ako dionica SPY-a naraste 0.1% u jednom satu, sentiment je nepromijenjen. Ne očekuju se nagli šokovi u cijeni. Međutim ako SPY padne 2% u jednom satu, očito je da se nešto značajno događa. Ovo je na tragu teorije ekstremnih vrijednosti koja nastoji objasniti dinamiku cijena n repovima distribucije (vrlo velikih padova ili rasta cijena).
Budući da financijska serija SPY-a predstavlja samo jednu realizaciju, umjesto same cijene SPY-a, koristit ćemo cijene dionica sastavnica SPY-a. Dakle oko 500 dionica razdoblju ili oko 700 dionica ukupno (ovisno kako su dionice ulazile u indeks ili izlazile iz indeksa). Jednostavno rečeno, ako cijene dionica, koje su sastavnice indeksa počinju naglo padati, prodajemo SPY. U suprotnom držimo SPY.
Algoritam se sastoji od sljedećih koraka
U nastavku prikazujemo rezultate koje smo dobili u istraživačkom dijelu, a kasnije ću prikazati rezultate unutar Quantconnect backtesting okruženja.
Prvi korak je učitavanje podataka. Učitavamo cijene dionice sastavnica SP500 indeksa i računamo prinose. Ostavljamo samo varijable koje su nam potrebne za analizu (close, returns), a ostale brišemo. Primijenili smo i jednostavnu metodu eliminiranja ekstremnih vrijednosti: pobrisali smo prinose iznad 50% i ispod -50% (pretpostavili smo da se radi o greškama u podacima koji su po našem iskustvu mogući). Prikaz podataka:
# import data sp500_symbols <- import_sp500() sp500_stocks <- import_intraday("D:/market_data/equity/usa/hour/trades_adjusted", "csv") sp500_stocks <- sp500_stocks[symbol %in% c("SPY", sp500_symbols)] # calculate returns sp500_stocks[, returns := (close / shift(close)) - 1, by = .(symbol)] spy <- sp500_stocks[symbol == "SPY"] sp500_stocks <- sp500_stocks[symbol != "SPY"] sp500_stocks <- sp500_stocks[, .(symbol, datetime, close, returns)] head(sp500_stocks) # remove outliers sp500_stocks <- sp500_stocks[abs(returns) < 0.48]
Prateći opis algoritma, u sljedećem koraku smo izračunali vrijednost gornjeg i donjeg percentila (točka 2 u opisu algoritma) za sve dionice. Također provjeravamo jesu li prinosi niski ili visoki za svako razdoblje (točka 3). Nakon toga sumiramo prinose (0, ekstremno visoke, ekstremno niske) za svaki datum.
# calculate sum of extra negative and positive returns sp500_stocks[, p_999 := roll_quantile(returns, 255*8*2, p = 0.999), by = .(symbol)] sp500_stocks[, p_001 := roll_quantile(returns, 255*8*2, p = 0.001), by = .(symbol)] sp500_stocks[, above := ifelse(returns > p_999, returns - p_999, 0), by = .(symbol)] sp500_stocks[, below := ifelse(returns < p_001, abs(returns - p_001), 0), by = .(symbol)] sp500_stocks[, above_dummy := ifelse(returns > p_999, 1, 0), by = .(symbol)] sp500_stocks[, below_dummy := ifelse(returns < p_001, 1, 0), by = .(symbol)]
# get tail risk mesures sp500_indicators <- sp500_stocks[, .(below_sum = sum(below, na.rm = TRUE), above_sum = sum(above, na.rm = TRUE) ), by = .(datetime)] sp500_indicators <- sp500_indicators[order(datetime)] sp500_indicators[, `:=`(excess = above_sum - below_sum)] sp500_indicators[, excess_sma := SMA(excess, 8 * 9)] sp500_indicators[, excess_sma_short := SMA(excess, 8 * 1)] sp500_indicators <- spy[, .(datetime, close, returns)][sp500_indicators, on = c('datetime')] sp500_indicators <- na.omit(sp500_indicators)
Kako bi bolje prezentirali ponašanje indikatora i odnos SPY-a i indikatora u nastavku prikazujemo nekoliko grafova s dinamikom obje serije. Prvo prikazujemo dinamiku excess indikatora i dvije SMA verzije indikatora:
# plot excess sma tail returns ggplot(sp500_indicators, aes(x = datetime)) + geom_line(aes(y = excess), color = 'blue') + geom_line(aes(y = excess_sma), color = 'red') + geom_line(aes(y = excess_sma_short), color = 'green')
Plava linija prikazuje raw indikator dok zelena i crvena linija prikazuju pomične prosjeke (crvene dugi, zelena kratki). Može se primijetiti nekoliko obilježja: 1) serija nema trend 2) postoje klasteri volatilnosti (slično kao za volatilnost prinosa) 3) volatilnost je znatno veća oko financijskih kriza.
Kako bi dobili jasniju sliku kretanja ključnog indikatora pokazujemo kretanje SMA excess serije oko financijskih kriza:
# plot excess sma tail returns ggplot(sp500_indicators[datetime %between% c('2008-01-01', '2009-01-01')], aes(x = datetime)) + geom_line(aes(y = excess_sma), color = 'red') + ggtitle("2008-2009")
# plot excess sma tail returns ggplot(sp500_indicators[datetime %between% c('2020-01-01', '2021-01-01')], aes(x = datetime)) + geom_line(aes(y = excess_sma), color = 'red') + ggtitle("2020-2021")
# plot excess sma tail returns ggplot(sp500_indicators[datetime %between% c('2018-01-01', '2019-01-01')], aes(x = datetime)) + geom_line(aes(y = excess_sma), color = 'red') + ggtitle("2018-2019")
Vidljivo je da kod korekcija tržišta indeks poprima snažne negativne vrijednosti. Algoritam prodaje SPY kada vrijednost crvene linije padne ispod -0.005 ili neke druge slične razine.
Gornji grafovi ne otkrivaju dovoljno jasno odnos cijene SPY-a i sumiranih prinosa (prinosi iz točke 3). Stoga na sljedećem grafu prikazujem kretanje cijene SPY-a i razdoblja u kojima je suma ekstremno niskih prinosa vrlo visoka ili vrlo niska (iznad 0.7):
vetical_lines_sell <- sp500_indicators[below_sum > 0.6, datetime] vetical_lines_buy <- sp500_indicators[above_sum > 0.6, datetime] ggplot(sp500_indicators, aes(x = datetime, y = close)) + geom_line() + geom_vline(xintercept = vetical_lines_sell, color = 'red') + geom_vline(xintercept = vetical_lines_buy, color = 'green')
ggplot(sp500_indicators[datetime %between% c('2020-01-01', '2021-01-01')], aes(x = datetime, y = close)) + geom_line() + geom_vline(xintercept = vetical_lines_sell, color = 'red') + geom_vline(xintercept = vetical_lines_buy, color = 'green')
ggplot(sp500_indicators[datetime %between% c('2018-01-01', '2019-01-01')], aes(x = datetime, y = close)) + geom_line() + geom_vline(xintercept = vetical_lines_sell, color = 'red') + geom_vline(xintercept = vetical_lines_buy, color = 'green')
ggplot(sp500_indicators[datetime %between% c('2015-01-01', '2016-01-01')], aes(x = datetime, y = close)) + geom_line() + geom_vline(xintercept = vetical_lines_sell, color = 'red') + geom_vline(xintercept = vetical_lines_buy, color = 'green')
ggplot(sp500_indicators[datetime %between% c('2008-01-01', '2009-01-01')], aes(x = datetime, y = close)) + geom_line() + geom_vline(xintercept = vetical_lines_sell, color = 'red') + geom_vline(xintercept = vetical_lines_buy, color = 'green')
Na grafovima se vidi da se ekstremno niski prinosi pojavljuju za vrijeme ili malo nakon pojave krize. Čak i ako se ne koristi unutar algoritamskog trgovanja crvene linije se pokazuju kao prilično dobar signal za prodaju dionica.
MinMAx strategija se temelji na jednom indikatoru - excess. Računanje indikatora zahtijeva izbor određenih parametara. Iako načelno nije preporučljivo optimizirati strategiju na backtestu, ovdje ćemo prekršiti ovo pravilo, s namjerom da ispitamo osjetljivost profitabilnosti strategiji je na izbor parametara.
Optimizirat će se dva parametra: 1) duljina SMA serije 2)granica ispod koje prodajemo dionicu. Za SMA pomake ćemo koristiti vrijednosti od 3 do 120 (od dva sata do dva tjedna) uz pomak 3, dok ćemo za granicu koristiti vrijednosti od -0.0001 do -0.015. Ukupno će se na ovaj način testirat 7.500 strategija. Strategije s najboljim rezultatima su:
# params sma_width <- seq(3, 160, 3) threshold <- seq(-0.0001, -0.015, by = -0.0001) paramset <- expand.grid(sma_width, threshold) colnames(paramset) <- c('sma_width', 'threshold')
# perfromance plan(multiprocess(workers = availableCores() - 16)) # backtset optimize_strategy <- function(sma_width, threshold) { excess_sma <- SMA(sp500_indicators$excess, sma_width) side <- ifelse(shift(excess_sma) < threshold, 0, 1) returns_strategy <- side * sp500_indicators$returns beckteset_data <- cbind(sp500_indicators[, .(returns)], returns_strategy) beckteset_data <- na.omit(beckteset_data) cum_return <- PerformanceAnalytics::Return.cumulative(beckteset_data)[, 2] } cum_returns <- future_vapply(1:nrow(paramset), function(x) optimize_strategy(paramset[x, 1], paramset[x, 2]), numeric(1)) results <- as.data.table(cbind(paramset, cum_returns)) x <- results[order(cum_returns, decreasing = TRUE)] %>% head(20) datatable(x, rownames = FALSE, escape = FALSE, extensions = 'Buttons', options = list(dom = 'Blfrtip', buttons = c('copy', 'csv', 'excel', 'pdf', 'print'), lengthMenu = list(c(10,25,50,-1), c(10,25,50,"All")))) %>% DT::formatStyle(columns = c(1, 2, 3, 4, 5, 6), fontSize = '11pt')
Neoprezni analitičar bi mogao zaključiti da je moguće ostvarivati kumulativne prinose od 17 000% (17 puta), ako slijedimo prvu strategiju. Takav zaključak je pogrešan! To je samo jedna od 6000 strategija i ona predstavlja snažan overfit. Dvadeseta najbolja strategija pokazuje prinos od 16x. Umjesto prikaza najboljih strategija mnogo je korisnije pogledati histogram svih prinosa:
ggplot(results, aes(cum_returns)) + geom_histogram(aes(y=..density..)) + geom_density(fill="#FF6666", alpha = 0.2) + geom_vline(xintercept = 3.22, color = 'red')
Crvena linija pokazuje prinos "Kupi i drži strategije". Cilj optimizacije nije identificirati jednu najbolju strategiju nego dobiti dojam o općoj profitabilnosti strategije.
Osim ukupnih prinosa, važna spoznaja se dobiva iz međuodnosa korištenih parametara i kumulativnih prinosa. To pokazuje sljedeći heatmap grafikon svih prinosa:
# heatmap and returns ggplot(results, aes(x = sma_width, y = threshold, fill = cum_returns)) + geom_tile()
Svjetliji dijelovi na grafikonu označavaju veće prinose. Grafikon otkriva da su najprimjerenije vrijednosti parametara: 1) oko 45 i 120 za SMA pomak 2) oko -0.005 za granicu. Nema potrebe tražiti jednu najbolju strategiju.
Ako želimo točno utvrditi najbolji SMA pomak, možemo jednostavno izračunati prosječne prinose za sve pomake. Prikazujemo 50 najboljih pomaka:
x <- results[, mean(cum_returns, na.rm = TRUE), by = sma_width][order(V1, decreasing = TRUE)] %>% head(50) datatable(x, rownames = FALSE, escape = FALSE, extensions = 'Buttons', options = list(dom = 'Blfrtip', buttons = c('copy', 'csv', 'excel', 'pdf', 'print'), lengthMenu = list(c(10,25,50,-1), c(10,25,50,"All")))) %>% DT::formatStyle(columns = c(1, 2, 3, 4, 5, 6), fontSize = '11pt')
Na sličan način možemo izračunati i 50 najboljih granica:
x <- results[, mean(cum_returns, na.rm = TRUE), by = threshold][order(V1, decreasing = TRUE)] %>% head(50) datatable(x, rownames = FALSE, escape = FALSE, extensions = 'Buttons', options = list(dom = 'Blfrtip', buttons = c('copy', 'csv', 'excel', 'pdf', 'print'), lengthMenu = list(c(10,25,50,-1), c(10,25,50,"All")))) %>% DT::formatStyle(columns = c(1, 2, 3, 4, 5, 6), fontSize = '11pt')
Zaključno, vrijedilo bi detaljnije ispitati profitabilnost MinMAx strategije sa SMA pomacima 48 (6 dana), 72 (9 dana) i 120 (15 dana) i granicama -0.05, -0.04 i -0.03.
U prvo koraku ćemo provesti jednostavni, vekrorizirani backtest pomoću R paketa Performance analytics. Sljedeći grafikon pokazuje equity krivulje za gornje izavbrane pomake SMA pomake i granice:
sma_steps <- c(48, 72, 120) thresholds <- c(-0.05, -0.04, -0.03) params <- expand.grid(sma_steps, thresholds) # backtest function backtest <- function(sma_step, threshold) { sma_value <- sp500_indicators[, SMA(excess, sma_step)] side <- ifelse(shift(sma_value) < threshold, 0, 1) returns_strategy <- as.data.frame(side * sp500_indicators$returns) colnames(returns_strategy) <- paste('cumulative_returns', sma_step, threshold * -100, sep = '_') return(returns_strategy) } # single backtests backtests <- lapply(1:nrow(params), function(i) { x <- backtest(params[i, 1], params[i, 2]) x }) backtests <- do.call(cbind, backtests) backtests <- cbind(datetime = sp500_indicators$datetime, returns = sp500_indicators$returns, backtests) backtests <- as.xts.data.table(as.data.table(backtests)) charts.PerformanceSummary(backtests, plot.engine = 'ggplot2')
Dosadašnja analiza je samo prvi korak u provjeri isplativosti strategije. Backtestovi koje smo proveli su samo okvir. Ne kontroliraju za mnoge skrivene zamke (potencijalni lookahead bias, market impact, slips i td.). Mnogo realniju sliku pruža backtest podataka unutar Quantconnect okruženja. Quantconnect je trenutno, prema našem subjektivnom stavu, najbolje backtesting okruženje. Međutim, nedostatak je što je mnogo sporiji od lokalnog razvoja. Ovdje prikazujem backtesting rezultate za MinMAx strategiju:
backtests <- read_excel("C:/Users/Mislav/Documents/GitHub/alphar/backtests/MinMax.xlsx") datatable(backtests, rownames = FALSE, escape = FALSE, extensions = 'Buttons', options = list(dom = 'Blfrtip', buttons = c('copy', 'csv', 'excel', 'pdf', 'print'), lengthMenu = list(c(10,25,50,-1), c(10,25,50,"All")))) %>% DT::formatStyle(columns = c(1, 2, 3, 4, 5, 6), fontSize = '11pt')
Iz tablice se vidi da strategija koja koristi SMA s pomakom 72 (9 dana) i granicom -0.005 pobjeđuje SPY. Zanimljiv je i obrazac. Strategije ostvaruje niže prinose u godinama kada SPY raste, ali znatno bolje rezultate u razdobljima kada SPY pada. Intuitivno, gubimo na false positive okladama, ali zarađujemo mnogo više na true positive okladama.
Moguće je testirati nekoliko varijacija modela i testirati mogućnost povećanja profitabilnosti strategije. Nekoliko primjera (neki od ovih su testirani):
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.