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

среда, 27 марта 2019 г.

Стратегия статистического арбитража в R

Перевод. Оригинал статьи: Statistical Arbitrage Strategy In R – By Jacques Joubert 

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

Те из вас, кто следил за моими сообщениями в блоге за последние 6 месяцев, знают, что я принял участие в Executive Programme in Algorithmic Trading, предлагаемой QuantInsti.

Эта статья служит отчетом о моем финальном проекте, посвященном статистическому арбитражу, закодированному в R. Эта статья представляет собой комбинацию моих заметок и моего исходного кода.

Я загрузил все в GitHub, чтобы побудить читателей вносить улучшения, использовать код или работать над этим проектом. Он также станет частью моего проекта Open Hedge Fund в моем блоге QuantsPortal.

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

История статистического арбитража

Изначально он был разработан и использовался в середине 1980-х годов группой Nunzio Tartaglia в Morgan Stanly.

Что такое парный трейдинг?

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

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

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

Концепция

Шаг 1: поиск двух взаимосвязанных активов

Ищем две ценные бумаги в одной и той же отрасли, которые имеют сравнимые рыночные капитализации и средние объемы торгов.

Примером таких активов являются Anglo Gold и Harmony Gold.

Шаг 2: Расчет спреда

В приведенном ниже для определения спреда я использовал отношение цен активов. 

Шаг 3: Расчет среднего значения, стандартного отклонения и z-оценки отношения/спреда пары.

Шаг 4: Тест на коинтеграцию

В этом коде я использую расширенный тест Дики-Фуллера (тест ADF), чтобы проверить коинтеграцию. Я провел три теста, каждый с различным количеством наблюдений (120, 90, 60), все три теста должны отклонить нулевую гипотезу о том, что пара не коинтегрирована.

Шаг 5: Генерирование торговых сигналов

Торговые сигналы основаны на z-оценке, поскольку они прошли тест на коинтеграцию. В моем проекте я использовал z-оценку 1, поскольку я заметил, что другие алгоритмы, с которыми сравнивал свои результаты, использовали очень низкие значения параметров (я бы предпочел z-оценку 2, так как она лучше соответствует литературным данным, однако она менее прибыльная).

Шаг 6: Обработка транзакций на базе сигналов

Шаг 7: Отчетность

Код R для моего проекта

Импорт пакетов и задание рабочей директории

Сначала импортируем требуемые нам пакеты.

# импорт пакетов
require(tseries)
require(urca) # используется для теста ADF
require(PerformanceAnalytics)

Эта стратегия будет работать с акциями, котирующимся на Фондовой бирже Йоханнесбурга (JSE); из-за этого я не смогу использовать пакет quantmod для извлечения данных с yahoo finance, вместо этого я уже получил и очистил данные, которые я хранил в базе данных SQL, и экспортировал их в CSV-файлы.

Я добавил все пары, используемые в стратегии, в папку, которую я теперь установил в качестве рабочего каталога.

## В этой папке хранятся файлы csv с данными
setwd("~/R/QuantInsti-Final-Project-Statistical-Arbitrage/database/FullList")

Функции, которые вызываются из других функций

Далее: создаем нужные нам функции. Ниже описаны функции, которые вызываются из других функций, поэтому вам не нужно беспокоиться об их аргументах.

AddColumns

Функция AddColumns используется для добавления столбцов в дата-фрейм, что потребуется нам для хранения переменных. 

# Добавление столбцов в csvData
AddColumns<-function csvdata="" font="">
csvData$spread<-0 font="">
csvData$adfTest<-0 font="">
csvData$mean<-0 font="">
csvData$stdev<-0 font="">
csvData$zScore<-0 font="">
csvData$signal<-0 font="">
csvData$BuyPrice<-0 font="">
csvData$SellPrice<-0 font="">
csvData$LongReturn<-0 font="">
csvData$ShortReturn<-0 font="">
csvData$Slippage<-0 font="">
csvData$TotalReturn<-0 font="">
csvData$TransactionRatio<-0 font="">
csvData$TradeClose<-0 font="">
return(csvData)
}

PrepareData

Функция PrepareData рассчитывает отношение цен активов и десятичные логарифмы цен пары активов. Внутри нее вызывается функция AddColumns.
PrepareData<-function csvdata="" font="">
# Рассчитываем отношение цен активов
csvData$pairRatio<-csvdata csvdata="" font="">
# рассчитываем логарифмы цен активов
csvData$LogA<-log10 csvdata="" font="">
csvData$LogB<-log10 csvdata="" font="">
# Добавляем столбцы в DF
csvData<-addcolumns csvdata="" font="">
# Убеждаемся, что столбец не будет прочитан как вектор или набор символов
csvData$Date<-as .date="" ate="" csvdata="" font="">
return(csvData)
}

GenerateRowValue

Функция GenerateRowValue рассчитывает среднее значение, стандартное отклонение и z-оценку для заданной строки в data frame.
# расчет среднего стандартного отклонения и z-оценки для заданной строки Row[end]
GenerateRowValue<-function begin="" csvdata="" end="" font="">
average<-mean begin:end="" csvdata="" font="" spread="">
stdev<-sd begin:end="" csvdata="" font="" spread="">
csvData$mean[end]<-average font="">
csvData$stdev[end]<-stdev font="">
csvData$zScore[end]<- average="" csvdata="" end="" font="" spread="" stdev="">
return(csvData)
}

GenerateSignal

Функция GenerateSignal генерирует сигналы для открытия длинной, или короткой позиции, или закрытия позиции на базе z-оценки. Вы можете вручную выбрать значение z-оценки. Я задаю значения 1 и -1 для входа в позицию и любое значения между 0,5 и -0,5 для выхода из позиции.

GenerateSignal<-function counter="" csvdata="" font="">
# Trigger и close представляют зоны входа и выхода (значение относится 
# к значению z-оценки)
trigger<-1 font="">
close<-0 .5="" font="">
currentSignal<-csvdata counter="" font="" signal="">
prevSignal<-csvdata counter-1="" font="" signal="">
# Задаем торговый сигнал для данной строки [end] 
if(csvData$adfTest[counter]==1)
{
# Если наблюдается изменение сигнала от длинного к короткому, 
# вы должны сначала закрыть текущую позицию
if(currentSignal==-1&&prevSignal==1)
csvData$signal[counter]<-0 font="">
elseif(currentSignal==1&&prevSignal==-1)
csvData$signal[counter]<-0 font="">
# Создаем длинный/короткий сигнал, если текущая z-оценка
# больше/меньше, чем значение trigger
elseif(csvData$zScore[counter]>trigger)
csvData$signal[counter]<--1 font="">
elseif(csvData$zScore[counter]<-trigger font="">
csvData$signal[counter]<-1 font="">
# закрываем позицию, если значение z-оценки между двумя значениями "close"
elseif(csvData$zScore[counter]-close)
csvData$signal[counter]<-0 font="">
else
csvData$signal[counter]<-prevsignal font="">
}
else
csvData$signal[counter]<-0 font="">
return(csvData)
}

GenerateTransactions

Функция GenerateTransactions отвечает за установку цен входа и выхода для соответствующих длинной и короткой позиций, необходимых для создания пары.

Примечание. QuantInsti научил нас очень специфичному способу тестирования торговой стратегии. Они использовали excel для обучения стратегиям, и когда я кодировал эту стратегию, я использовал большую часть методологии excel.

Однако в будущем я бы рассмотрел другие способы хранения переменных. Одна из замечательных особенностей этого метода заключается в том, что вы можете вытащить весь data frame и проанализировать, почему был сделан трейд, и все детали, относящиеся к нему.

# транзакции на базе торговых сигналов
# следуем фреймворку, заданному QuantInsti (Примечание: этот код можно улучшить)
GenerateTransactions<-function csvdata="" currentsignal="" end="" font="" prevsignal="">
# В парном трейдинге вам нужно открыть длинную позицию 
# в одном активе и короткую в другом
#и затем обратные к ним, чтобы закрыть позицию.
## первая часть сдели (длинная позиция)
#если нет изменений в сигнале
if(currentSignal==0&&prevSignal==0)
{
csvData$BuyPrice[end]<-0 font="">
csvData$TransactionRatio[end]<-0 font="">
}
elseif(currentSignal==prevSignal)
{csvData$BuyPrice[end]<-csvdata end-1="" font="" uyprice="">
csvData$TransactionRatio[end]<-csvdata end-1="" font="" ransactionratio="">
}
#если сигнал указывает на новый трейд
#Короткая B и длинная A
elseif(currentSignal==1&¤tSignal!=prevSignal)
csvData$BuyPrice[end]<-csvdata end="" font="">
#Короткая A и длинная B
elseif(currentSignal==-1&¤tSignal!=prevSignal){
csvData$BuyPrice[end]<-csvdata csvdata="" end="" font="" pairratio="">
transactionPairRatio<<-csvdata end="" font="" pairratio="">
csvData$TransactionRatio[end]<-transactionpairratio font="">
}
#Закрытие позиций
elseif(currentSignal==0&&prevSignal==1)
csvData$BuyPrice[end]<-csvdata end="" font="">
elseif(currentSignal==0&&prevSignal==-1)
{csvData$TransactionRatio[end]=csvData$TransactionRatio[end-1]
csvData$BuyPrice[end]<-csvdata csvdata="" end="" font="" ransactionratio="">
}
##вторая часть сделки (короткая позиция)
##задаем короткую цену, если нет изменений в сигнале
if(currentSignal==0&&prevSignal==0)
csvData$SellPrice[end]<-0 font="">
elseif(currentSignal==prevSignal)
csvData$SellPrice[end]<-csvdata ellprice="" end-1="" font="">
#если сигнал указывает на новый трейд
elseif(currentSignal==1&¤tSignal!=prevSignal){
csvData$SellPrice[end]<-csvdata csvdata="" end="" font="" pairratio="">
transactionPairRatio<<-csvdata end="" font="" pairratio="">
csvData$TransactionRatio[end]<-transactionpairratio font="">
}
elseif(currentSignal==-1&¤tSignal!=prevSignal)
csvData$SellPrice[end]<-csvdata end="" font="">
#Закрытие позиций
elseif(currentSignal==0&&prevSignal==1){
csvData$TransactionRatio[end]=csvData$TransactionRatio[end-1]
csvData$SellPrice[end]<-csvdata csvdata="" end="" font="" ransactionratio="">
}
elseif(currentSignal==0&&prevSignal==-1)
csvData$SellPrice[end]<-csvdata end="" font="">
return(csvData)
}

GetReturnsDaily

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

#Расчет дневной прибыли
#Добавляем проскальзывание
GetReturnsDaily<-function csvdata="" end="" font="" slippage="">
#Расчет прибыли на базе каждой части сделки (для длинной и короткой позиций)
#Длинная сделка
if(csvData$signal[end-1]>0){csvData$LongReturn[end]<-log csvdata="" end-1="" end="" font="">
else
if(csvData$signal[end-1]<0 csvdata="" end-1="" end="" font="" log="" ongreturn="" ransactionratio="">
#Короткая сделка
if(csvData$signal[end-1]>0){csvData$ShortReturn[end]<--log csvdata="" end-1="" end="" font="" ransactionratio="">
else
if(csvData$signal[end-1]<0 csvdata="" end-1="" end="" font="" hortreturn="" log="">
#Добавляем проскальзывание
if(csvData$signal[end]==0&&csvData$signal[end-1]!=0)
{
csvData$Slippage[end]<-slippage font="">
csvData$TradeClose[end]<-1 font="">
}
#Если сделка закрыта, рассчитываем общую доходность
csvData$TotalReturn[end]<- csvdata="" end="" font="" hortreturn="" lippage="" ongreturn="">
return(csvData)
}

GenerateReports

Два следующих аргумента используются для генерации отчетов. Отчет включает:
Графики: 1. Кривая капитала. 2. Кривая просадки. 3. График дневных доходностей.
Статистика: 1. Годовая доходность 2. Годовой коэффициент Шарпа 3. Максимальная просадка
Таблица: 1. Наибольшие 5 просадок и их продолжительность.

Примечание. Если у вас есть время, вы можете дополнительно разбить эту функцию на более мелкие части, чтобы уменьшить количество строк кода и улучшить удобство использования. Меньше кода = меньше ошибок
#Возвращает кривую капитала, годовую доходность, 
#годовой коэффициент Шарпа и максимальную просадку
GenerateReport<-function enddate="" font="" pairdata="" startdate="">
#Подбор дат
returns<-xts as.date="" ate="" font="" otalreturn="" pairdata="">
returns<-returns enddate="" font="" paste="" sep="::" startdate="">
#График
charts.PerformanceSummary(returns)
#Метрики
print(paste("Annual Returns: ",Return.annualized(returns)))
print(paste("Annualized Sharpe: ",SharpeRatio.annualized(returns)))
print(paste("Max Drawdown: ",maxDrawdown(returns)))
pairDataSub=pairData[pairData$TradeClose==1,]
returns_sub<-xts as.date="" ate="" font="" otalreturn="" pairdatasub="">
returns_sub<-returns_sub enddate="" font="" paste="" sep="::" startdate="">
#var returns = xts object
totalTrades<-0 font="">
positiveTrades<-0 font="">
profitsVector<-c font="">
lossesVector<-c font="">
#loop through the data to find the + & - trades and total trades
for(iinreturns_sub){
if(i!=0){
totalTrades<-totaltrades font="">
if(i>0){
positiveTrades<-positivetrades font="">
profitsVector<-c font="" i="" profitsvector="">
}
elseif(i<0 font="">
lossesVector<-c font="" i="" lossesvector="">
}
}
}
#Вывод результатов в консоль
print(paste("Total Trades: ",totalTrades))
print(paste("Success Rate: ",positiveTrades/totalTrades))
print(paste("PnL Ratio: ",mean(profitsVector)/mean(lossesVector*-1)))
print(table.Drawdowns(returns))
}
GenerateReport.xts<-function enddate="2015-11-23" font="" returns="" startdate="2005-01-01">
#Метрики
returns<-returns enddate="" font="" paste="" sep="::" startdate="">
charts.PerformanceSummary(returns)
print(paste("Annual Returns: ",Return.annualized(returns)))
print(paste("Annualized Sharpe: ",SharpeRatio.annualized(returns)))
print(paste("Max Drawdown: ",maxDrawdown(returns)))
print(table.Drawdowns(returns))
}

Функции, которым параметры передает пользователь.

Следующие две функции единственные, с которыми должен работать пользователь.

BacktestPair

BacktestPair используется, когда вы хотите запустить тестирование на торговой паре (пара передается через CSV-файл).

Аргументы функции:

    pairData = CSV-файл с данными
    mean = количество наблюдений, используемое для расчета среднего значения спреда.
    slippage = проскальзывание в базовых пунктах.
    adfTest = логическое значение – должен ли производиться тест на коинтеграцию.
    criticalValue = критическое значение, используемое в ADF-тесте на коинтеграцию.
    generateReport = логическое значение –  должен ли генерироваться отчет.
#Функция, вызываемая пользователем для тестирования пары
BacktestPair<-function mean="35,slippage=-0.0025,adfTest=TRUE,criticalValue=-2.58,</font" pairdata="">
startDate='2005-01-01',endDate='2014-11-23',generateReport=TRUE){
# Для 150 точек данных
# Критическое значение для 1% : -3.46
# Критическое значение для 5% : -2.88
# Критическое значение для 10% : -2.57
#Подготовка изначального dataframe путем добавления 
# столбцов и предварительных расчетов
pairData<-preparedata font="" pairdata="">
#Итерации по каждому дню во временном ряду
for(iin1:length(pairData[,2])){
#Для каждого дня после количества дней, необходимых для запуска теста ADF
if(i>130){
begin<-i-mean font="">
end<-i font="">
#Расчет спреда
spread<-pairdata end="" font="" pairratio="">
pairData$spread[end]<-spread font="">
#Тест ADF
#120 - 90 - 60
if(adfTest==FALSE){
pairData$adfTest[end]<-1 font="">
}
else{
if(adf.test(pairData$spread[(i-120):end],k=1)[1]<=criticalValue){
if(adf.test(pairData$spread[(i-90):end],k=1)[1]<=criticalValue){
if(adf.test(pairData$spread[(i-60):end],k=1)[1]<=criticalValue){
#Если есть коинтеграция, задаем значение ADFTest  true/1
pairData$adfTest[end]<-1 font="">
}
}
}
}
#Вычислить оставшиеся переменные
if(i>=mean){
#Генерация значений Row
pairData<-generaterowvalue begin="" end="" font="" pairdata="">
#Генерация сигналов
pairData<-generatesignal font="" i="" pairdata="">
currentSignal<-pairdata font="" i="" signal="">
prevSignal<-pairdata font="" i-1="" signal="">
#Генерация транзакций
pairData<-generatetransactions currentsignal="" font="" i="" pairdata="" prevsignal="">
#Получить прибыль с добавлением проскальзывания
pairData<-getreturnsdaily font="" i="" pairdata="" slippage="">
}
}
}
if(generateReport==TRUE)
GenerateReport(pairData,startDate,endDate)
return(pairData)
}

BacktestPortfolio

BacktestPortfolio принимает вектор CSV-файлов, а затем генерирует одинаково взвешенный портфель.

Аргументы функции:

    names = вектор имен CSV-файлов, например c(‘DsyLib.csv’, ‘OldSanlam.csv’)
    mean = количество наблюдений, используемое для расчета среднего значения спреда.
    leverage = размер плеча, который вы хотите применить к портфелю.

#Одинако взвешенный портфель активов
BacktestPortfolio<-function enddate="2015-11-23" font="" mean="35,leverage=1,startDate=" names="">
##Итерации по всем парам и тестирование каждой
##храним данные в списке численных векторов
returns.list<-list font="">
counter<-f font="">
ticker<-1 font="">
for(name innames){
#Уведомление, чтобы знать, на каком этапе процесс
print(paste(ticker," of ",length(names)))
ticker<-ticker font="">
#Запуск тестирования на паре
data<-read .csv="" font="" name="">
BackTest.df<-backtestpair data="" generatereport="FALSE)</font" mean="">
#Сохранение дат в отдельном векторе
if(counter==F){
dates<<-as .date="" acktest.df="" ate="" font="">
counter<-t font="">
}
#Добавляем в список
returns.list<-c acktest.df="" font="" list="" returns.list="">
}
#Собираем доходности за каждый день и 
#затем рассчитываем среднюю дневную доходность
total.returns<-c font="">
for(iin1:length(returns.list)){
if(i==1)
total.returns=returns.list[[i]]
else
total.returns=total.returns+returns.list[[i]]
}
total.returns<-total .returns="" font="" length="" returns.list="">
##Генерируем отчет для портфеля
returns<-xts dates="" font="" leverage="" total.returns="">
GenerateReport.xts(returns,startDate,endDate)
return(returns)
}

Запуск тестирования

Теперь мы можем запустить тестирование стратегий с использованием нашего кода

Чистый арбитраж на JSE

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

Если оба листинга находятся на одной и той же бирже, это открывает дверь для стратегии чистого арбитража, так как есть два листинга, относящихся к одному и тому же активу. Поэтому вам не нужно тестировать коинтеграцию.

На JSE есть два очень очевидных примера.
Первый пример Investec:

Primary = Investec Ltd : Secondary = Investec PLC
Investec In-Sample Test (2005-01-01 – 2012-11-23)

Тестируем со следующими параметрами

    The Investec ltd / plc pair
    mean = 35
    Set adfTest = F (тест на коинтеграцию не выполняется)
    Leverage of x3
#Investec
leverage<-3 font="">
data<-read .csv="" font="" nvestec.csv="">
investec<-backtestpair data="" generatereport="F,adfTest=F)</font">
#Format to an xts object and pass to GenerateReport.xts()
investec.returns<-xts ate="" font="" investec="" leverage="">
GenerateReport.xts(investec.returns,startDate='2005-01-01',endDate='2012-11-23')


## [1] "Annual Returns: 0.619853087807437"
## [1] "Annualized Sharpe: 3.29778431709924"
## [1] "Max Drawdown: 0.105016628973292"
## From Trough To Depth Length To Trough Recovery
## 1 2009-03-19 2009-03-25 2009-05-04 -0.1050 28 5 23
## 2 2006-06-08 2006-07-13 2006-08-14 -0.0955 46 25 21
## 3 2008-10-03 2008-10-17 2008-10-24 -0.0887 16 11 5
## 4 2009-03-02 2009-03-02 2009-03-06 -0.0733 5 1 4
## 5 2008-10-27 2008-10-27 2008-11-05 -0.0697 8 1 7
Investec Out-of-Sample Test (2012-11-23 – 2015-11-23)

Примечание: если вы увеличите проскальзывание, вы очень быстро уменьшите прибыль до нуля.

GenerateReport.xts(investec.returns,startDate='2012-11-23',endDate='2015-11-23')


## [1] "Annual Returns: 0.1754103210963"
## [1] "Annualized Sharpe: 2.20385429706265"
## [1] "Max Drawdown: 0.0335642102186873"
## From Trough To Depth Length To Trough Recovery
## 1 2015-07-10 2015-11-13 -0.0336 96 89 NA
## 2 2013-06-18 2013-06-21 2013-07-01 -0.0267 10 4 6
## 3 2014-04-16 2014-08-13 2014-09-19 -0.0262 107 80 27
## 4 2015-01-20 2015-05-25 2015-06-01 -0.0258 91 86 5
## 5 2013-01-18 2013-01-24 2013-01-25 -0.0249 6 5 1

Второй пример Mondi:

Primary = Mondi Ltd : Secondary = Mondi PLC
Mondi In-Sample Test (2008-01-01 – 2012-11-23)

Тестируем со следующими параметрами

    The Mondi ltd / plc pair
    mean = 35
    Set adfTest = F (тест на коинтеграцию не выполняется)
    Leverage of x3

data <- 35="" adftest="F)</p" backtestpair="" data="" generatereport="F," mondi.csv="" mondi="" read.csv="">

mondi.returns<-xts ate="" leverage="" mondi="" p="">
GenerateReport.xts(mondi.returns,startDate='2008-01-01',endDate='2012-11-23')

## [1] "Annual Returns: 0.973552250431717"
## [1] "Annualized Sharpe: 2.88672185296756"
## [1] "Max Drawdown: 0.254688711989788"
## From Trough To Depth Length To Trough Recovery
## 1 2008-07-01 2008-08-01 2008-09-01 -0.2547 45 24 21
## 2 2009-03-11 2009-03-18 2009-04-08 -0.1906 21 6 15
## 3 2008-04-16 2008-06-03 2008-06-23 -0.1040 45 32 13
## 4 2008-09-02 2008-09-17 2008-09-18 -0.0926 13 12 1
## 5 2009-03-09 2009-03-09 2009-03-10 -0.0864 2 1 1
Mondi Out-of-Sample Test (2012-11-23 – 2015-11-23)

Примечание. Во время тестирования я обнаружил, что чем дальше по шкале времени были мои данные, тем сложнее было получать прибыль в конце дня. Я тестировал эту же стратегию для внутридневных данных и имел более высокий профиль доходности.

GenerateReport.xts(mondi.returns,startDate='2012-11-23',endDate='2015-11-23')


## [1] "Annual Returns: 0.0809094579019469"
## [1] "Annualized Sharpe: 1.25785312960412"
## [1] "Max Drawdown: 0.0385234269750542"
## From Trough To Depth Length To Trough Recovery
## 1 2013-12-19 2014-10-13 2015-01-26 -0.0385 273 202 71
## 2 2015-06-05 2015-08-14 -0.0313 120 49 NA
## 3 2015-01-27 2015-04-22 2015-04-28 -0.0245 63 60 3
## 4 2013-05-29 2013-05-30 2013-06-14 -0.0179 13 2 11
## 5 2013-11-08 2013-11-18 2013-12-18 -0.0175 28 7 21

Статистический арбитраж на JSE

Далее мы рассмотрим пару торговых стратегий.

Обычно пара состоит из двух акций, которые:

- находятся в одном секторе рынка;
- имеют аналогичную рыночную капитализацию;
- имеют похожие бизнес-модели и клиентов;
- коинтегрированы

Во всех портфелях ниже я использую плечо 3.

Формирование портфеля

In-sample test (2005-01-01 – 2012-11-01)

names  <- c="" font="" mravenge.csv="" mrppc.csv="" nbsp="" roupavenge.csv="" roupmr.csv="" roupppc.csv="" roupwhbo.csv="">
construction.return.series  <- backtestportfolio="" enddate="2015-11-23" leverage="4)</font" names="" startdate="2014-11-23">


[1]"Annual Returns: 0.0848959306632411"
## [1] "Annualized Sharpe: 0.733688101181479"
## [1] "Max Drawdown: 0.193914686702112"
## From Trough To Depth Length To Trough Recovery
## 1 2008-05-19 2008-07-08 2008-11-03 -0.1939 119 36 83
## 2 2008-11-04 2008-12-03 2009-06-29 -0.1345 160 22 138
## 3 2006-08-25 2007-12-19 2008-02-19 -0.1272 372 331 41
## 4 2009-08-04 2009-10-01 2009-11-10 -0.0701 69 41 28
## 5 2009-11-25 2010-03-10 2010-09-29 -0.0486 211 73 138
Out-of-sample test (2012-11-23 – 2015-11-23)

GenerateReport.xts(ReturnSeries,startDate='2012-11-23',endDate='2015-11-23')

## [1] "Annual Returns: 0.0159094762396512"
## [1] "Annualized Sharpe: 0.268766025866724"
## [1] "Max Drawdown: 0.0741426720423424"
## From Trough To Depth Length To Trough Recovery
## 1 2013-08-05 2013-09-06 2014-11-17 -0.0741 322 24 298
## 2 2014-11-20 2015-01-29 -0.0737 253 47 NA
## 3 2012-11-30 2013-04-23 2013-05-02 -0.0129 102 96 6
## 4 2013-06-10 2013-06-13 2013-06-24 -0.0100 10 4 6
## 5 2013-05-03 2013-05-03 2013-06-04 -0.0050 23 1 22

Страховочный портфель

names  <- anlam.csv="" c="" font="" ibmmi.csv="" isclib.csv="" iscmmi.csv="" iscsanlam.csv="" ld.csv="" ldsanlam.csv="" nbsp="">
insurance.return.series  <- backtestportfolio="" leverage="4)</font" names="">


## [1] "Annual Returns: 0.110600985165525"
## [1] "Annualized Sharpe: 0.791920916349154"
## [1] "Max Drawdown: 0.233251846760865"
## From Trough To Depth Length To Trough Recovery
## 1 2005-05-26 2005-10-14 2006-08-31 -0.2333 318 100 218
## 2 2008-10-15 2008-12-05 2009-04-30 -0.1513 134 38 96
## 3 2009-06-10 2009-12-10 2010-01-29 -0.1223 162 129 33
## 4 2011-10-04 2012-10-09 -0.0991 267 249 NA
## 5 2006-11-08 2007-12-11 2007-12-14 -0.0894 277 274 3

Out-of-sample test (2012-11-23 – 2015-11-23)
GenerateReport.xts(ReturnSeries,startDate='2012-11-23',endDate='2015-11-23')


## [1] "Annual Returns: -0.0265926093350092"
## [1] "Annualized Sharpe: -0.319582293135835"
## [1] "Max Drawdown: 0.128061204573991"
## From Trough To Depth Length To Trough Recovery
## 1 2014-08-08 2015-11-20 -0.1281 326 324 NA
## 2 2012-11-28 2013-05-13 2013-07-31 -0.0393 167 111 56
## 3 2014-06-10 2014-06-26 2014-07-23 -0.0284 31 12 19
## 4 2013-08-01 2013-08-30 2013-09-03 -0.0255 23 21 2
## 5 2013-09-11 2013-10-22 2013-12-04 -0.0209 60 29 31

General Retail Portfolio

In-sample test (2005-01-01 – 2012-11-01)
names  <- c="" csv="" font="" oolmr.csv="" ooltfg.csv="" ooltru.csv="" rutfg.csv="">
retail.return.series  <- backtestportfolio="" enddate="2015-11-23" font="" names="" startdate="2014-11-23">


## [1] "Annual Returns: 0.120956981644048"
## [1] "Annualized Sharpe: 1.4694780839876"
## [1] "Max Drawdown: 0.125406256082082"
## From Trough To Depth Length To Trough Recovery
## 1 2010-01-05 2012-01-17 -0.1254 705 504 NA
## 2 2008-09-29 2008-10-29 2009-02-20 -0.0690 101 23 78
## 3 2006-03-06 2006-05-15 2006-05-23 -0.0568 52 46 6
## 4 2005-07-18 2005-11-01 2005-12-06 -0.0538 101 76 25
## 5 2008-04-11 2008-04-29 2008-06-26 -0.0512 51 12 39

Out-of-sample test (2012-11-23 – 2015-11-23)

GenerateReport.xts(ReturnSeries,startDate='2012-11-23',endDate='2015-11-23')


[1]"Annual Returns: -0.0171898953593881"
## [1] "Annualized Sharpe: -0.336265418351652"
## [1] "Max Drawdown: 0.0884145115767888"
## From Trough To Depth Length To Trough Recovery
## 1 2013-10-15 2015-11-11 -0.0884 528 519 NA
## 2 2013-03-18 2013-06-24 2013-08-12 -0.0279 100 66 34
## 3 2013-09-05 2013-09-06 2013-09-20 -0.0088 12 2 10
## 4 2013-09-23 2013-10-02 2013-10-08 -0.0049 11 7 4
## 5 2013-02-20 2013-02-20 2013-03-15 -0.0037 18 1 17

Заключение:

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

Есть много вещей, которые, я думаю, могут быть добавлены для повышения эффективности. В будущем я исследую использование фильтров Калмана.

Подробнее о торговой стратегии чистого арбитража:

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

Подробнее о парной стратегии:

Число наблюдений, используемых в тестах ADF, сильно завышено. Проблема заключается в том, что для принятия решения на статистический арбитраж необходимо провести проверку на коинтеграцию, однако, используя 120, 90 и 60 наблюдений в качестве параметров для трех тестов, очень трудно найти пары, которые соответствуют критерию. (Здесь может быть полезна фильтрация Калмана).

Я не потратил много времени, меняя различные параметры, такие как количество наблюдений в вычислении среднего (Это требует дальнейших исследований).

Из вышеперечисленных отраслевых портфелей мы видим, что ранние годы дают хорошую прибыль, но чем дальше мы продвигаемся, тем ниже доходность. Я говорил с несколькими людьми в отрасли, а также с моими друзьями, делающими проекты stat arb в Университете Кейптауна. По их словам, в 2009 году Goldman купил пакет stat arb в отношении ценных бумаг, находящихся в листинге JSE.

То же самое наблюдается и с другими портфелями, которые я не включил в этот отчет, но они имеются в файле с кодом R.

Я считаю, что это связано с тем, что крупные инвесторы используют ту же стратегию хлеба и масла. Вы заметите (если вы потратите достаточно времени на тестирование всех стратегий), что в 2009 году, как представляется, происходит резкое изменение доходности.

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

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

Если вы дошли до конца этой статьи, я благодарю вас и надеюсь, что она имела для вас некоторую ценность. Это первый раз, когда я использую Github, поэтому я с нетерпением жду, если появятся какие-либо новые участники этого проекта.

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

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