Перевод. Оригинал статьи: 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="">-function>
csvData$spread<-0 font="">-0>
csvData$adfTest<-0 font="">-0>
csvData$mean<-0 font="">-0>
csvData$stdev<-0 font="">-0>
csvData$zScore<-0 font="">-0>
csvData$signal<-0 font="">-0>
csvData$BuyPrice<-0 font="">-0>
csvData$SellPrice<-0 font="">-0>
csvData$LongReturn<-0 font="">-0>
csvData$ShortReturn<-0 font="">-0>
csvData$Slippage<-0 font="">-0>
csvData$TotalReturn<-0 font="">-0>
csvData$TransactionRatio<-0 font="">-0>
csvData$TradeClose<-0 font="">-0>
return(csvData)
}
PrepareData
Функция PrepareData рассчитывает отношение цен активов и десятичные логарифмы цен пары активов. Внутри нее вызывается функция AddColumns.
PrepareData<-function csvdata="" font="">-function>
# Рассчитываем отношение цен активов
csvData$pairRatio<-csvdata csvdata="" font="">-csvdata>
# рассчитываем логарифмы цен активов
csvData$LogA<-log10 csvdata="" font="">-log10>
csvData$LogB<-log10 csvdata="" font="">-log10>
# Добавляем столбцы в DF
csvData<-addcolumns csvdata="" font="">-addcolumns>
# Убеждаемся, что столбец не будет прочитан как вектор или набор символов
csvData$Date<-as .date="" ate="" csvdata="" font="">-as>
return(csvData)
}
GenerateRowValue
Функция GenerateRowValue рассчитывает среднее значение, стандартное отклонение и z-оценку для заданной строки в data frame.
# расчет среднего стандартного отклонения и z-оценки для заданной строки Row[end]
GenerateRowValue<-function begin="" csvdata="" end="" font="">-function>
average<-mean begin:end="" csvdata="" font="" spread="">-mean>
stdev<-sd begin:end="" csvdata="" font="" spread="">-sd>
csvData$mean[end]<-average font="">-average>
csvData$stdev[end]<-stdev font="">-stdev>
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="">-function>
# Trigger и close представляют зоны входа и выхода (значение относится
# к значению z-оценки)
trigger<-1 font="">-1>
close<-0 .5="" font="">-0>
currentSignal<-csvdata counter="" font="" signal="">-csvdata>
prevSignal<-csvdata counter-1="" font="" signal="">-csvdata>
# Задаем торговый сигнал для данной строки [end]
if(csvData$adfTest[counter]==1)
{
# Если наблюдается изменение сигнала от длинного к короткому,
# вы должны сначала закрыть текущую позицию
if(currentSignal==-1&&prevSignal==1)
csvData$signal[counter]<-0 font="">-0>
elseif(currentSignal==1&&prevSignal==-1)
csvData$signal[counter]<-0 font="">-0>
# Создаем длинный/короткий сигнал, если текущая z-оценка
# больше/меньше, чем значение trigger
elseif(csvData$zScore[counter]>trigger)
csvData$signal[counter]<--1 font="">--1>
elseif(csvData$zScore[counter]<-trigger font="">-trigger>
csvData$signal[counter]<-1 font="">-1>
# закрываем позицию, если значение z-оценки между двумя значениями "close"
elseif(csvData$zScore[counter]-close)
csvData$signal[counter]<-0 font="">-0>
else
csvData$signal[counter]<-prevsignal font="">-prevsignal>
}
else
csvData$signal[counter]<-0 font="">-0>
return(csvData)
}
GenerateTransactions
Функция GenerateTransactions отвечает за установку цен входа и выхода для соответствующих длинной и короткой позиций, необходимых для создания пары.
Примечание. QuantInsti научил нас очень специфичному способу тестирования торговой стратегии. Они использовали excel для обучения стратегиям, и когда я кодировал эту стратегию, я использовал большую часть методологии excel.
Однако в будущем я бы рассмотрел другие способы хранения переменных. Одна из замечательных особенностей этого метода заключается в том, что вы можете вытащить весь data frame и проанализировать, почему был сделан трейд, и все детали, относящиеся к нему.
# транзакции на базе торговых сигналов
# следуем фреймворку, заданному QuantInsti (Примечание: этот код можно улучшить)
GenerateTransactions<-function csvdata="" currentsignal="" end="" font="" prevsignal="">-function>
# В парном трейдинге вам нужно открыть длинную позицию
# в одном активе и короткую в другом
#и затем обратные к ним, чтобы закрыть позицию.
## первая часть сдели (длинная позиция)
#если нет изменений в сигнале
if(currentSignal==0&&prevSignal==0)
{
csvData$BuyPrice[end]<-0 font="">-0>
csvData$TransactionRatio[end]<-0 font="">-0>
}
elseif(currentSignal==prevSignal)
{csvData$BuyPrice[end]<-csvdata end-1="" font="" uyprice="">-csvdata>
csvData$TransactionRatio[end]<-csvdata end-1="" font="" ransactionratio="">-csvdata>
}
#если сигнал указывает на новый трейд
#Короткая B и длинная A
elseif(currentSignal==1&¤tSignal!=prevSignal)
csvData$BuyPrice[end]<-csvdata end="" font="">-csvdata>
#Короткая A и длинная B
elseif(currentSignal==-1&¤tSignal!=prevSignal){
csvData$BuyPrice[end]<-csvdata csvdata="" end="" font="" pairratio="">-csvdata>
transactionPairRatio<<-csvdata end="" font="" pairratio="">-csvdata>
csvData$TransactionRatio[end]<-transactionpairratio font="">-transactionpairratio>
}
#Закрытие позиций
elseif(currentSignal==0&&prevSignal==1)
csvData$BuyPrice[end]<-csvdata end="" font="">-csvdata>
elseif(currentSignal==0&&prevSignal==-1)
{csvData$TransactionRatio[end]=csvData$TransactionRatio[end-1]
csvData$BuyPrice[end]<-csvdata csvdata="" end="" font="" ransactionratio="">-csvdata>
}
##вторая часть сделки (короткая позиция)
##задаем короткую цену, если нет изменений в сигнале
if(currentSignal==0&&prevSignal==0)
csvData$SellPrice[end]<-0 font="">-0>
elseif(currentSignal==prevSignal)
csvData$SellPrice[end]<-csvdata ellprice="" end-1="" font="">-csvdata>
#если сигнал указывает на новый трейд
elseif(currentSignal==1&¤tSignal!=prevSignal){
csvData$SellPrice[end]<-csvdata csvdata="" end="" font="" pairratio="">-csvdata>
transactionPairRatio<<-csvdata end="" font="" pairratio="">-csvdata>
csvData$TransactionRatio[end]<-transactionpairratio font="">-transactionpairratio>
}
elseif(currentSignal==-1&¤tSignal!=prevSignal)
csvData$SellPrice[end]<-csvdata end="" font="">-csvdata>
#Закрытие позиций
elseif(currentSignal==0&&prevSignal==1){
csvData$TransactionRatio[end]=csvData$TransactionRatio[end-1]
csvData$SellPrice[end]<-csvdata csvdata="" end="" font="" ransactionratio="">-csvdata>
}
elseif(currentSignal==0&&prevSignal==-1)
csvData$SellPrice[end]<-csvdata end="" font="">-csvdata>
return(csvData)
}
GetReturnsDaily
GetReturnsDaily рассчитывает дневные прибыли для каждой позиции, затем вычисляет общую доходность и добавляет проскальзывание.
#Расчет дневной прибыли
#Добавляем проскальзывание
GetReturnsDaily<-function csvdata="" end="" font="" slippage="">-function>
#Расчет прибыли на базе каждой части сделки (для длинной и короткой позиций)
#Длинная сделка
if(csvData$signal[end-1]>0){csvData$LongReturn[end]<-log csvdata="" end-1="" end="" font="">-log>
else
if(csvData$signal[end-1]<0 csvdata="" end-1="" end="" font="" log="" ongreturn="" ransactionratio="">0>
#Короткая сделка
if(csvData$signal[end-1]>0){csvData$ShortReturn[end]<--log csvdata="" end-1="" end="" font="" ransactionratio="">--log>
else
if(csvData$signal[end-1]<0 csvdata="" end-1="" end="" font="" hortreturn="" log="">0>
#Добавляем проскальзывание
if(csvData$signal[end]==0&&csvData$signal[end-1]!=0)
{
csvData$Slippage[end]<-slippage font="">-slippage>
csvData$TradeClose[end]<-1 font="">-1>
}
#Если сделка закрыта, рассчитываем общую доходность
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="">-function>
#Подбор дат
returns<-xts as.date="" ate="" font="" otalreturn="" pairdata="">-xts>
returns<-returns enddate="" font="" paste="" sep="::" startdate="">-returns>
#График
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="">-xts>
returns_sub<-returns_sub enddate="" font="" paste="" sep="::" startdate="">-returns_sub>
#var returns = xts object
totalTrades<-0 font="">-0>
positiveTrades<-0 font="">-0>
profitsVector<-c font="">-c>
lossesVector<-c font="">-c>
#loop through the data to find the + & - trades and total trades
for(iinreturns_sub){
if(i!=0){
totalTrades<-totaltrades font="">-totaltrades>
if(i>0){
positiveTrades<-positivetrades font="">-positivetrades>
profitsVector<-c font="" i="" profitsvector="">-c>
}
elseif(i<0 font="">0>
lossesVector<-c font="" i="" lossesvector="">-c>
}
}
}
#Вывод результатов в консоль
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">-function>
#Метрики
returns<-returns enddate="" font="" paste="" sep="::" startdate="">-returns>
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="">-function>
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="">-preparedata>
#Итерации по каждому дню во временном ряду
for(iin1:length(pairData[,2])){
#Для каждого дня после количества дней, необходимых для запуска теста ADF
if(i>130){
begin<-i-mean font="">-i-mean>
end<-i font="">-i>
#Расчет спреда
spread<-pairdata end="" font="" pairratio="">-pairdata>
pairData$spread[end]<-spread font="">-spread>
#Тест ADF
#120 - 90 - 60
if(adfTest==FALSE){
pairData$adfTest[end]<-1 font="">-1>
}
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="">-1>
}
}
}
}
#Вычислить оставшиеся переменные
if(i>=mean){
#Генерация значений Row
pairData<-generaterowvalue begin="" end="" font="" pairdata="">-generaterowvalue>
#Генерация сигналов
pairData<-generatesignal font="" i="" pairdata="">-generatesignal>
currentSignal<-pairdata font="" i="" signal="">-pairdata>
prevSignal<-pairdata font="" i-1="" signal="">-pairdata>
#Генерация транзакций
pairData<-generatetransactions currentsignal="" font="" i="" pairdata="" prevsignal="">-generatetransactions>
#Получить прибыль с добавлением проскальзывания
pairData<-getreturnsdaily font="" i="" pairdata="" slippage="">-getreturnsdaily>
}
}
}
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="">-function>
##Итерации по всем парам и тестирование каждой
##храним данные в списке численных векторов
returns.list<-list font="">-list>
counter<-f font="">-f>
ticker<-1 font="">-1>
for(name innames){
#Уведомление, чтобы знать, на каком этапе процесс
print(paste(ticker," of ",length(names)))
ticker<-ticker font="">-ticker>
#Запуск тестирования на паре
data<-read .csv="" font="" name="">-read>
BackTest.df<-backtestpair data="" generatereport="FALSE)</font" mean="">-backtestpair>
#Сохранение дат в отдельном векторе
if(counter==F){
dates<<-as .date="" acktest.df="" ate="" font="">-as>
counter<-t font="">-t>
}
#Добавляем в список
returns.list<-c acktest.df="" font="" list="" returns.list="">-c>
}
#Собираем доходности за каждый день и
#затем рассчитываем среднюю дневную доходность
total.returns<-c font="">-c>
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="">-total>
##Генерируем отчет для портфеля
returns<-xts dates="" font="" leverage="" total.returns="">-xts>
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="">-3>
data<-read .csv="" font="" nvestec.csv="">-read>
investec<-backtestpair data="" generatereport="F,adfTest=F)</font">-backtestpair>
#Format to an xts object and pass to GenerateReport.xts()
investec.returns<-xts ate="" font="" investec="" leverage="">-xts>
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="">-xts>
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">->
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="">->
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">->
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)
[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, поэтому я с нетерпением жду, если появятся какие-либо новые участники этого проекта.
Репозиторий Github: https://github.com/Jackal08/QuantInsti-Final-Project-Statistical-Arbitrage
Комментариев нет:
Отправить комментарий