рекомендации

суббота, 2 марта 2019 г.

Стратегия парного трейдинга и тестирование торговой стратегии с помощью Quantstrat


Эта статья является финальным проектом, представленным автором в рамках его курсовой работы в Executive Program in Algorithmic Trading (EPAT) в QuantInsti. 

Введение

Одним из моих любимых классов во время EPAT был статистический арбитраж, поэтому стратегия парного трейдинга показалась мне хорошей идеей. Моя стратегия выставляет новые ордеры, когда соотношение цен на пару акций отклоняется от среднего. Но прежде чем работать, мы сначала должны проверить, что пара коинтегрирована. Если отношение пары коинтегрировано, то отношение возвращается к среднему, и чем больше отклонение от его среднего значения, тем выше вероятность разворота, что делает торговлю более привлекательной. Я выбрал следующую пару акций:

Bank of America (BAC)
Citigroup (C)

Идея заключается в следующем: если мы найдем две акции, которые коррелированы между собой (они принадлежат одному и тому же сектору индустрии), а соотношение цен пары отклоняется от определенного порогового значения, мы открываем короткую позицию по акции, которая дорожает, и длинную по той, которая дешевеет. Как только они сходятся к среднему значению, мы закрываем позиции и получаем прибыль от разворота.

Логика торговой стратегии

Логика проста. Алгоритм вычисляет дневное Z-значение для каждой пары акций. Z-значение - это количество стандартных отклонений, на которое соотношение цен пары отклонилось от среднего:

    Z = (R – μ) / σ 

Где R - отношение цен акций, μ - среднее значение отношения, а σ - стандартное отклонение отношения цен.

Когда Z-значение находится за пределами определенного порога, выполняется первое условие, необходимое для отправки ордера.

Но алгоритм должен также соответствовать второму условию: он выполняет развернутый расширенный тест Дики Фуллера для пары акций. Более конкретно, он получает значение p из теста. Затем он сравнивает его с определенным уровнем значимости (альфа), и если значение p меньше альфа, это означает, что серия ценовых соотношений является стационарной и выполняется второе условие. Если оба условия выполнены, то алгоритм покупает упавшую акцию и продает выросшую. Правила выхода применяются при определенном пороге Z-значения. Для оптимизации стратегии я использовал следующие переменные:

1. Пороговые Z-значения для входа.
2. Пороговые Z-значения для выхода.
3. Второе условие (коинтеграция) - True или False.

Код и тестирование стратегии:

Период выборочной проверки для бэк-тестинга: с 01-01-2009 по 31-12-2012. Z-значение рассчитывалась с использованием следующих параметров:

     Скользящее среднее отношения цен: 20 дней
     Стандартное отклонение отношения цен: 20 дней
     Окно теста ADF: 60 дней
     Первоначальный капитал = 100000 долларов США
     Спрэд купли/продажи = 3000

При сужении спреда мы продаем «C» и покупаем «BAC», при его росте мы поступаем наоборот. Я использовал библиотеку Quantstrat для проверки стратегии. Давайте погрузимся в код:

# Загрузка библиотек
library(quantstrat)
library(tseries)
library(IKTrading)
library(PerformanceAnalytics)

# .blotter содержит портфолио и объект учетной записи, а .strategy содержит книгу ордеров и объект стратегии

.blotter <- font="" new.env="">
.strategy <- font="" new.env="">

# получаем рыночные данные и строим спрэд.
symb1 <- font="">
symb2 <- font="">
getSymbols(symb1, from=startDate, to=endDate, adjust=TRUE)

getSymbols(symb2, from=startDate, to=endDate, adjust=TRUE)

spread <- font="" ohlc="">
colnames(spread)<-c close="" font="" high="" low="" open="">

symbols <- c="" font="" spread="">
stock(symbols, currency = 'USD', multiplier = 1)

chart_Series(spread)

add_TA(EMA(Cl(spread), n=20), on=1, col="blue", lwd=1.5)
legend(x=5, y=50, legend=c("EMA 20"),
       fill=c("blue"), bty="n")



Как упоминалось ранее, я буду использовать библиотеку quantsrat для оптимизации моей стратегии. Чтобы использовать Quantstrat, нам сначала нужно определить и инициализировать инструменты, стратегию, портфолио, учетную запись и ордеры:

#Инициализация стратегии, портфеля, счета и ордеров

qs.strategy<- div="" pairstrat="">
initPortf(qs.strategy,symbols=symbols,initDate=initDate)

initAcct(qs.strategy,portfolios=qs.strategy,initDate=initDate,initEq=initEq)

initOrders(qs.strategy,initDate=initDate)

# Сохраняем стратегию
strategy(qs.strategy,store=TRUE)
# rm.strat(pairStrat) # только при новом тесте
ls(.blotter) # содержит портфолио и объект учетной записи
ls(.strategy)# содержит книгу ордеров и объект стратегии

Затем мы вычисляем и добавляем наши два показателя стратегии:

– Z-Score
– ADF Test (True или False)

# a) Z-Score
PairRatio <- 2="" close="" font="" for="" function="" of="" prices="" ratio="" returns="" symbols="" the="" x="">
  x1 <- font="" get="" x="">
  x2 <- font="" get="" x="">
  rat <- cl="" font="" l="" log10="" x1="" x2="">
  colnames(rat) <- font="" rice.ratio="">
  rat
}

Price.Ratio <- c="" font="" pairratio="" symb1="" symb2="">

MaRatio <- font="" function="" x="">

  Mavg <- font="" mean="" n="" rollapply="" x="">
  colnames(Mavg) <- font="" rice.ratio.ma="">
  Mavg
}

Price.Ratio.MA <- font="" maratio="" rice.ratio="">

Sd <- font="" function="" x="">

  Stand.dev <- font="" n="" rollapply="" sd="" x="">
  colnames(Stand.dev) <- font="" rice.ratio.sd="">
  Stand.dev
}

Price.Ratio.SD <- font="" rice.ratio="" sd="">

ZScore <- font="" function="" x="">

  a1 <- font="" rice.ratio="" x="">
  b1 <- font="" rice.ratio.ma="" x="">
  c1 <- font="" rice.ratio.sd="" x="">

  z <- a1-b1="" c1="" font="">

  colnames(z)<- core="" font="">
  z

}

# Расширенный тест Дики Фуллера

ft2<-function font="" x="">
  adf.test(x)$p.value
}

Pval <- font="" function="" x="">

  Augmented.df <- font="" ft2="" rollapply="" width="N.ADF," x="">
  colnames(Augmented.df) <- alue="" font="">
  Augmented.df
}

P.Value <- font="" pval="" rice.ratio="">

add.indicator(strategy = qs.strategy, name = "ZScore", arguments =
                list(x=merge(Price.Ratio,Price.Ratio.MA,Price.Ratio.SD)))

add.indicator(strategy = qs.strategy, name = "Pval", arguments =
                list(x=quote(Price.Ratio)))
На следующей диаграмме мы можем видеть эволюцию Z-значения в течение периода и возможные значения порога, где отношение возвращается к среднему и экстремальному значениям. Я установил для нескольких строк пороговое Z-значение +/- 2 , где, похоже, происходит возврат отношения. Это значение означает, что отношение цен равно +/- стандартных отклонений от его среднего значения.

Z.Score <- x="merge(Price.Ratio,Price.Ratio.MA,Price.Ratio.SD))</font" zscore="">
plot(main = "Z-Score Time Series", xlab = "Date" , ylab = "Z-Score",Z.Score, type = "l" )
abline(h = 2, col = 2, lwd = 3 ,lty = 2)
abline(h = -2, col = 3, lwd = 3 ,lty = 2)



Теперь мы задаем наши переменные оптимизации:

alpha = 1 
#Мы устанавливаем его в 0,1, если хотим уровень значимости 10%. Если мы хотим #отключить тест ADF (второе условие), мы просто изменим его на «1», в этом случае #значение p всегда будет ниже уровня значимости, и стратегия не требует #коинтеграции пары.

# входные и выходные пороговые Z-значения:

buyThresh = -2
sellThresh = -buyThresh
exitlong = 1
exitshort = 1

Перед тем, как запустить тестирование, мы должны добавить сигналы, лимиты позиций и правила нашей стратегии:

add.signal(qs.strategy, name="sigThreshold",arguments=list(column="Z.Score", threshold=buyThresh,relationship="lt", cross=FALSE),label="longEntryZ")

add.signal(qs.strategy, name="sigThreshold",arguments=list(column="P.Value", threshold= alpha,relationship="lt", cross=FALSE),label="PEntry")

add.signal(qs.strategy, name="sigAND", arguments=list(columns=c("longEntryZ", "PEntry"), cross=FALSE),label="longEntry")

add.signal(qs.strategy, name="sigThreshold",arguments=list(column="Z.Score", threshold= exitlong,relationship="gt", cross=FALSE),label="longExit")

add.signal(qs.strategy, name="sigThreshold",arguments=list(column="Z.Score", threshold=sellThresh,relationship="gt", cross=FALSE),label="shortEntryZ")

add.signal(qs.strategy, name="sigAND", arguments=list(columns=c("shortEntryZ", "PEntry"), cross=FALSE),label="shortEntry")

add.signal(qs.strategy, name="sigThreshold",arguments=list(column="Z.Score", threshold= exitshort,relationship="lt", cross=FALSE),label="shortExit")

addPosLimit( portfolio = qs.strategy, добавляем лимиты позиций
             symbol = 'spread',
             timestamp = initDate,
             maxpos = 3000,
             longlevels = 1,
             minpos = -3000)

add.rule(qs.strategy, name='ruleSignal',arguments = list (sigcol = "longEntry", sigval=TRUE, orderqty=3000,  osFUN = osMaxPos, replace = FALSE, ordertype='market', orderside='long', prefer = "open"), type='enter' )

add.rule(qs.strategy, name='ruleSignal', arguments = list(sigcol="shortEntry",
sigval=TRUE, orderqty=-3000,  osFUN = osMaxPos, replace = FALSE,ordertype='market',
orderside='short', prefer = "open"), type='enter')

add.rule(qs.strategy, name='ruleSignal', arguments = list(sigcol="longExit",
sigval=TRUE, orderqty= 'all', ordertype='market', orderside='short', prefer = "open"), type='exit')

add.rule(qs.strategy, name='ruleSignal', arguments = list(sigcol="shortExit",
sigval=TRUE, orderqty= 'all' , ordertype='market', orderside='long', prefer = "open"), type='exit')

summary(get.strategy(qs.strategy))


Как видно из нашего резюме, в нашей стратегии определены 2 индикатора, 7 сигналов и 3 правила. Теперь мы можем запустить тестирование, проверить транзакции и эффективность нашей стратегии.

applyStrategy(strategy = qs.strategy, portfolios = qs.strategy, mktdata = spread)

tns <-gettxns ortfolio="qs.strategy," symbol="symbols)</font">

# Обновляем портфель, аккаунт и счет
updatePortf(qs.strategy)

updateAcct(qs.strategy)

updateEndEq(qs.strategy)

Оптимизация была выполнена со следующими значениями для переменных:


Тестирование стратегии показало следующие результаты:


Из этой таблицы мы можем получить значения переменных, которые оптимизируют стратегию. На первый взгляд кажется, что есть 3 кандидата (случай 4, случай 6 и случай 8). Если мы сопоставим между собой случаи 6 и 8, то придем к выводу, что случай 8 является лучшим, поскольку он имеет большее годовое отношение Шарпа и  отношение прибыли к максимальной просадке, более высокий процент успешных сделок, более высокий конечный капитал с таким же числом трейдов. Итак, теперь у нас осталось только 2 кандидата: 4 и 8. Если мы будем проверять только тот, у кого наибольшее годовое соотношение Шарпа, мы предпочли бы случай 4. Случай 8 также не учитывает, что ряды должны быть коинтегрированы, в отличие от случая 4, так что это будет еще одним плюсом для случая 4. Но если учесть количество транзакций, отношение прибыли к максимальной просадке, конечный капитал, процент успешных сделок и тот факт, что разница в коэффициент Шарпа не является большой, мы бы определенно выбрали случай 8 как наш лучший кандидат.

Тестирование:

Теперь, когда мы оптимизировали стратегию и получили оптимальные значения для параметров, мы можем запустить тестирование и посмотреть, как работает стратегия. Тестовый период берем от 01-01-2013 до 31-12-2015, а оптимизированные значения для пороговых значений и правил были следующими:

Пороговое Z-значение для покупки = -2
Пороговое Z-значение для продажи = 2
Пороговое Z-значение для выхода из длинной позиции = -1
Пороговое Z-значение для выхода из короткой позиции = 1
ADF Test = False

Следующая диаграмма показывает нам различные транзакции, окончательный капитал и просадки для нашей стратегии:

chart.P2 = function (Portfolio, Symbol, Dates = NULL, ..., TA = NULL)
{
  pname <- font="" portfolio="">
  Portfolio <- font="" getportfolio="" pname="">
  if (missing(Symbol))
    Symbol <- font="" ls="" ortfolio="" symbols="">
  else Symbol <- font="" symbol="">
  Prices = get(Symbol)
  if (!is.OHLC(Prices)) {
     if (hasArg(prefer))
       prefer = eval(match.call(expand.dots = TRUE)$prefer)
     else prefer = NULL
  Prices = getPrice(Prices, prefer = prefer)
}
freq = periodicity(Prices)
switch(freq$scale, seconds = {
  mult = 1
}, minute = {
  mult = 60
}, hourly = {
  mult = 3600
}, daily = {
  mult = 86400
}, {
  mult = 86400
})
if (!isTRUE(freq$frequency * mult == round(freq$frequency,
0) * mult)) {
  n = round((freq$frequency/mult), 0) * mult
}
else {
  n = mult
}
tzero = xts(0, order.by = index(Prices[1, ]))
if (is.null(Dates))
  Dates <- first="" font="" index="" last="" paste="" rices="">
                 sep = "::")
Portfolio$symbols[[Symbol]]$txn <- ates="" font="" portfolio="" symbols="" txn="" ymbol="">
Portfolio$symbols[[Symbol]]$posPL <- ates="" font="" portfolio="" pospl="" symbols="" ymbol="">
Trades = Portfolio$symbols[[Symbol]]$txn$Txn.Qty
Buys = Portfolio$symbols[[Symbol]]$txn$Txn.Price[which(Trades >
0)]
Sells = Portfolio$symbols[[Symbol]]$txn$Txn.Price[which(Trades <
0)]
Position = Portfolio$symbols[[Symbol]]$txn$Pos.Qty
if (nrow(Position) < 1)
  stop("no transactions/positions to chart")
if (as.POSIXct(first(index(Prices))) < as.POSIXct(first(index(Position))))
  Position <- -="" font="" order.by="first(index(Prices)" rbind="" xts="">
1)), Position)
Positionfill = na.locf(merge(Position, index(Prices)))
CumPL = cumsum(Portfolio$symbols[[Symbol]]$posPL$Net.Trading.PL)
if (length(CumPL) > 1)
  CumPL = na.omit(na.locf(merge(CumPL, index(Prices))))
else CumPL = NULL
if (!is.null(CumPL)) {
  CumMax <- cummax="" font="" umpl="">
  Drawdown <- -="" cumpl="" font="" ummax="">
  Drawdown <- -="" font="" max="" order.by="first(index(Drawdown)" rbind="" umpl="" xts="">
1)), Drawdown)
}
else {
  Drawdown <- font="" null="">
}
if (!is.null(Dates))
  Prices = Prices[Dates]
chart_Series(Prices, name = Symbol, TA = TA, ...)
if (!is.null(nrow(Buys)) && nrow(Buys) >= 1)
  (add_TA(Buys, pch = 2, type = "p", col = "green", on = 1))
if (!is.null(nrow(Sells)) && nrow(Sells) >= 1)
  (add_TA(Sells, pch = 6, type = "p", col = "red", on = 1))
if (nrow(Position) >= 1) {
  (add_TA(Positionfill, type = "h", col = "blue", lwd = 2))
  (add_TA(Position, type = "p", col = "orange", lwd = 2,
on = 2))
}
if (!is.null(CumPL))
  (add_TA(CumPL, col = "darkgreen", lwd = 2))
if (!is.null(Drawdown))
  (add_TA(Drawdown, col = "darkred", lwd = 2, yaxis = c(0,
-max(CumMax))))
plot(current.chob())
}

chart.P2(qs.strategy, "spread", prefer = "close")


Из приведенной ниже таблицы видно, что результаты тестирования на другом периоде не так хороши, как те, которые мы получили из нашего теста.


Годовой коэффициент Шарпа по-прежнему положителен, но меньше, чем 3,52, который мы получили раньше. отношение прибыли к максимальной просадке намного хуже, чем 4.23, но максимальная просадка снизилась с 16327 до 8641. Наша стратегия обеспечивает совокупный доход в размере 16,04% и годовую прибыль в размере 5,08% за три года, на которых производился тест.
returns <- font="" portfreturns="" qs.strategy="">
charts.PerformanceSummary(returns, geometric=FALSE, wealth.index=TRUE, main = "Pair Strategy Returns")

Комментариев нет:

Отправить комментарий