Парная торговля

  • Часть I
  • Часть 2
  • Часть 3
  • Парная торговля на основе данных
  • Тестирование стратегии парной торговли на истории
  • Разработка стратегии торговли парами
  • Анализ стратегий парного трейдинга
  • Стратегия Citadel

Часть I

«Когда вы начинаете идти, появляется путь».

Девизу я следую уже давно. Годы работы в банковской сфере в качестве инженера по автоматизации аварийного восстановления. Розничная торговля. Смена карьеры в сторону финансов. Двойная степень магистра финансов за 2 года. Что дальше? Не уверен в этом, что не мешает мне писать на Medium.

«Лучшее время для посадки дерева было 20 лет назад. Второе лучшее время сейчас». Китайская пословица

Реализация стратегии парной торговли на Python была бы хорошим началом. В этой части я кратко объясню;

  • Торговля парами
  • Подготовка данных по Brent и сырой нефти
  • Внедрение корреляционного анализа

1) ЧТО ТАКОЕ ПАРНАЯ ТОРГОВЛЯ?

LГоворят, что два очень похожих продукта, продукт А и продукт Б, обычно продаются по сопоставимой цене, хотя время от времени она колеблется из-за проблем с поставками. Если вы знаете, что в какой-то момент цены сойдутся друг с другом, что бы вы сделали, если бы нашли товар А со скидкой и дорогой товар Б? Предполагая, что вам безразлично качество этих двух, вы бы купили более дешевый, чтобы продать его дороже. Тем не менее, нет уверенности в том, что цена вырастет, но вы почему-то думаете, что она сойдется с ценой продукта B. Таким образом, покупка продукта А и продажа продукта Б, предположительно имеющегося, по их текущим ценам, может принести прибыль от спреда независимо от повышения стоимости или обесценивания продуктов.

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

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

  • Они представляют собой нестационарные временные ряды,
  • Доходность двух активов по отдельности стационарна,
  • Они взаимосвязаны.

Выполнение этих трех предварительных условий является лишь шлюзом для начала тестирования на истории на различных активах. Но как именно? Только в США насчитывается более 6000 публично торгуемых компаний, у нас есть нефтепродукты, цифровые активы (потрясающий способ сказать криптовалюты, не считаясь мошенническими), товары, многочисленные индикаторы для инкапсуляции спреда, и этот список можно продолжить. Допустим, мы загружаем данные, реализуем анализ, а те, которые мы выбрали, не интегрированы. Представьте, что вы делаете это для 500 разных пар. Невозможный.

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

2) Создание набора данных

Fили, проще говоря, я выбрал один из самых широко известных дуэтов, подходящих для торговли парами, а именно Brent и сырую нефть. Для тех, кто не знаком с этим, вы можете взглянуть на Investopedia, как всегда:

Эталонные масла: нефть марки Brent, WTI и Дубай

2.1. Загрузка данных с помощью yfinance (Yahoo Finance Python Library)

WЯ использую для загрузки простую функцию, использующую библиотеку yfinance. Следующий код загружает BZ=F (нефть марки Brent) и CL=F (нефть марки WTI); Однако данные можно расширить, добавив в список новые элементы.

import yfinance as yf #import yfinance
import pandas as pd #import pandas

#Function that downloads Daily OHLC data for ticker starting from 15-11-2020
#Please refer to https://pypi.org/project/yfinance/ for more details and options

def dataDownloader(ticker):
df = yf.download(ticker,start="2000-11-15",interval="1d",progress=False)["Close"]
df.index = pd.to_datetime(df.index, format = '%Y/%m/%d').strftime('%Y-%m-%d')
return df

#Enter tickers you want to be downloaded to the list
assets = ["BZ=F","CL=F"]

#Dataframe that will hold Daily OHLC Data
pairsData = dataDownloader(assets)

Функция загрузки в библиотеке yfinance заслуживает внимания со всеми подробностями. Вы можете импортировать не только данные OHLC, но и прибыль, балансы и т. Д. Интервалы также могут быть организованы ежечасно, еженедельно или ежемесячно. Хотя получение данных представляется достаточным, может существовать ряд проблем, которые необходимо решить. Начнем с построения графика данных и проверки того, существуют ли какие-либо нулевые данные.

2.2. Построение временных рядов и проверка нулевых данных

AПриведите следующий фрагмент приведенного выше кода, чтобы отобразить данные и быстро проверить, существует ли какой-либо пробел.

import matplotlib.pyplot as plt #import plot library

#Data Plot function
def pricePlot(dataframe,colname):
fig, ax = plt.subplots(figsize=(10,8))
dataframe.plot.line(y=colname,color='crimson', ax=ax)
plt.ylabel(colname)
plt.show()

pricePlot(pairsData,assets[0])
pricePlot(pairsData,assets[1])

Эта часть обнаружит только огромные пробелы в наборе данных, что может привести к значительному расхождению в процессе интерполяции. Хотя вероятность столкнуться с такой проблемой в Yahoo Finance меньше, в таком случае следует рассмотреть возможность поиска другого источника данных. Несортированные данные и выбросы, которые будут рассмотрены в 2.3, также можно легко наблюдать с помощью метода построения графиков в начале всех процессов. С нулевыми данными еще предстоит разобраться, и приведенный ниже код решит проблему. Но подождите, а что, если данные нужно отсортировать, а мы интерполируем с неправильными данными? Давайте также добавим функцию сортировки перед интерполяцией нуля.

def sortData(dataframe):
#Checks if index is monotonically decreasing
isSorted = dataframe.index.is_monotonic_decreasing
if not isSorted:
dataframe.sort_index(inplace=True, ascending=False)
#ascending=False for descending data
return dataframe

def detectNull(dataframe,colname): #detect if there are null values

isnull = dataframe[colname].isnull().values.any()
if isnull:
dataframe[colname].interpolate(method = 'linear', inplace = True)
return data

#inplace changes the dataframe completely, no need to assign again
sortData(pairsData)
detectNull(pairsData,assets[0])
detectNull(pairsData,assets[1])

Интерполяция данных временных рядов сама по себе требует еще одного полного поста; поэтому я буду придерживаться самого простого метода, «линейного», который добавляет среднее значение двух соседних точек данных. Приведенный ниже кадр данных показывает, что метод линейной интерполяции заполняет данные, относящиеся к 04–05–2021.

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

#Find the first index for both data where data is not nan or null
first_index1 = pairsData[assets[0]].first_valid_index()
first_index2 = pairsData[assets[1]].first_valid_index()

#If indices are not equal, delete every row that contains nan, or null value
if first_index1 != first_index2:
pairsData.dropna(inplace=True)

2.3. Выбросы:

OUtliers может быть большой проблемой, особенно в алгоритмах HFT. Представьте, что актив А имеет среднюю цену на уровне 100 долларов за определенный период, но у вас есть 900 долларов в наборе данных или знак минус. Об этих значениях следует позаботиться, чтобы сохранить целостность данных. Тем не менее, финансовые рынки склонны к событиям «черного лебедя», поэтому устранение только точных выбросов имеет решающее значение. Следующий фрагмент кода проверяет, содержат ли данные какие-либо выбросы, присваивая Z-оценку каждому значению и интерполируя те, из которых Z-оценка больше или меньше 3 стандартных отклонений от среднего, используя тот же метод, который применялся ранее.

import numpy as np

def detect_outliers_zscore(dataframe,colname):
thres = 3 #threshold which eliminates the outlier data
mean = np.mean(dataframe[colname]) #find average price
std = np.std(dataframe[colname]) #find standard deviation
for i in dataframe[colname]:
z_score = (i-mean)/std
if (np.abs(z_score) > thres):
dataframe[colname].interpolate(method = 'linear', inplace = True)

return data

Часть II

Переход к статистическому анализу с данными

«Есть ложь, проклятая ложь и статистика».

— Марк Твен

Let не разрывает связи и не погружается во вторую часть, которая будет включать;

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

Как следует из заголовка, будет масса статистических данных, чтобы проверить, является ли наша пара хорошей комбинацией для торговой системы. Для тех, кто еще не читал первую часть, это может вызвать эффект начала трилогии «Властелин колец» с двух башен; Фильм получше, но стоит посмотреть первый.

Описание расчета данных и возврата

Just, чтобы убедиться, что все, что мы выполнили в части 1, является точным, и для введения в статистические вычисления в Python я буду использовать фрагмент кода ниже. В дополнение к фундаментальным концепциям статистического распределения (среднее значениедисперсияасимметрия и эксцесс), он также предоставляет такие значения, как min, max, 25%-50%-75% квантили и т. д.

def describeData(data,colname):

#describe() is the built-in Python function
stat = data[colname].describe()
stat.loc['var'] = data[colname].var()
stat.loc['skew'] = data[colname].skew()
stat.loc['kurt'] = data[colname].kurtosis()

return stat

#Statistical Summary of Timeseries Data for a final check
statData = pd.DataFrame(describeData(pairsData,assets[0]))
statData.insert(1,assets[1],describeData(pairsData,assets[1]))

Как можно сделать вывод из количества, min и max, наша пара готова к дальнейшей проверке с тем же количеством точек данных (count) без значительных выбросов (min и max).

Со всеми теми шагами, которые были предприняты до этого, есть только одно: ВОЗВРАЩЕНИЕ. К счастью, Python нас не подводит и дает нам красивую функцию pct_change(), чтобы легко понять наш набор данных.

#Calculate Returns with pct_change() 
#Change column names and precision by playing with "_%Return" and round()
pairsData[assets[0]+"_%Return"] = round(pairsData[assets[0]].pct_change(),4)*100
pairsData[assets[1]+"_%Return"] = round(pairsData[assets[1]].pct_change(),4)*100

#Sort Dataframe according to the Date index in the descending format
sortData(pairsData)

Вы вспомнили нашего друга sortData из первой части? Наш фрейм pairsData теперь должен выглядеть следующим образом:

Корреляция и стационарность

Чтобы пара подходила для системы Pairs Trading, их доходность должна быть соотнесена друг с другом. Поскольку мы уже сгенерировали доходность для каждого актива, все, что нужно, — это запустить еще одну строчку на Python. С другой стороны, все же стоит упомянуть, что корреляционные расчеты основаны на предположении, что доходность индивидуально неподвижна, что может быть проверено с помощьюОткрытый тест Дики-Фуллера (ADF-тест, еще одна тема, заслуживающая поста). Если вас интересует статистика, следующий фрагмент кода позаботится обо всем (Боже, храни Python!).

#import library for ADF Test
from statsmodels.tsa.stattools import adfuller

def ADFTest(data,colname):

#Variable that holds statistical results of ADF
global adfStats
adfStats = adfuller(data[colname],maxlag=0)

# Test statistics for the given dataset
print('Augmented Dickey_fuller Statistic: %f' % adfStats[0])
# p-value
print('p-value: %f' % adfStats[1])

# printing the critical values at different alpha levels.
print('critical values at different levels:')
for k, v in adfStats[4].items():
print('\t%s: %.3f' % (k, v))
return adfStats[1]

print('ADF Test for ', assets[0])
#Assign p-value of Asset 1
pValue1 = ADFTest(pairsData, assets[0]+"_%Return")

print('ADF Test for ', assets[1])
#Asset p-value of Asset 2
pValue2 = ADFTest(pairsData, assets[1]+"_%Return")

#If returns are individually stationary
if round(pValue1,2) == 0.00 and round(pValue2,2) == 0.00:

#calculate Pearson, Spearman and Kendall correlation coefficients
print('Pearson\'s: %f' % pairsData[assets[0]+"_%Return"].corr(pairsData[assets[1]+"_%Return"],method='pearson'))
print('Spearman\'s: %f' % pairsData[assets[0]+"_%Return"].corr(pairsData[assets[1]+"_%Return"],method='spearman'))
print('Kendall\'s: %f' % pairsData[assets[0]+"_%Return"].corr(pairsData[assets[1]+"_%Return"],method='kendall'))

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

Метод Пирсона широко используется в качестве коэффициента корреляции. Несмотря на то, что значение значительно ниже требуемого значения для правильной пары, я продолжу с производными нефтяными инструментами, чтобы проверить, как система будет работать с умеренно коррелированными активами. Если вы хотите проверить предположение о сильно коррелированных активах для системы, либо работайте с более текущими данными (скажем, начиная с 2020 года), либо меняйте активы (KO & PEP или BTC = F, ETH = F). Как правило, ожидается, что корреляция будет выше 0,80 (хотя никого не волнует, зарабатываете ли вы деньги).

Дополнительная статистика* и расчет ожидаемой цены

TПоследний шаг, прежде чем мы заблудимся в джунглях дизайна торговой системы: обнаружение высокой корреляции может дать ощущение, что дискуссия идет о том, чтобы куда-то попасть, что действительно имеет значение, так это поведение спреда между двумя активами. Представьте себе две акции, которые, по вашему мнению, подойдут для парной торговли, и после шагов, которые я предоставил до этого момента, корреляция составляет 0,95. Значит ли это, что они сойдутся друг на друге? Действительно?

* Коинтеграция: метод анализа временных рядов, если они имеют устойчивые долгосрочные отношения. Тесты Энгла-Грейнджера, Филипс-Улариса и Йохансена являются одними из популярных тестов для проверки наличия коинтеграции между двумя наборами данных.

Я реализую тест Энгла-Грейнджера, который строит остатки на основе модели статической регрессии и проверяет, являются ли остатки стационарными во времени (ADF Test). Стационарность остатков подразумевает постоянное среднее значение и дисперсию спреда, и БИНГО! У нас есть коррелированные, обращенные к среднему два нестационарных временных ряда, которые сходятся друг с другом с течением времени, и наше ожидание найти торговую пару подтвердилось.

#Build Cointegration Model
import statsmodels.api as sm
model = sm.OLS(pairsData[assets[0]+"_%Return"],pairsData[assets[1]+"_%Return"])
model = model.fit()
#Good old Beta of simple regression equation
hedgeRatio = round(model.params[0],2)

#Populate DataFrame with Spread value
pairsData['Spread'] = pairsData[assets[1]] - model.params[0] * pairsData[assets[0]]

#Plot Spread
import matplotlib.pyplot as plt
pairsData.Spread.plot(figsize=(8,4))
plt.ylabel('Spread')
plt.show()

#Check if residual is stationary
print('ADF Test for Spread',end="\n\n")
pValueResidual = ADFTest(pairsData,"Spread")

if round(pValueResidual,2) == 0.00:
print(assets[0]," and", assets[1], " are suitable for Pairs Trading System")
else:
print("Check another pair, or change time interval")

Обычно получается неподходящая торговая пара или исторический интервал. Результаты теста АПД и графика распространения, начиная с 2000 года, являются следующими:

Чтобы преодолеть эту проблему, я выбрал отправную точку, которая дает результат, чтобы продолжить анализ того, что мы называем переобучением. Эти соображения будут обсуждаться в Части III, Торговля; Тем не менее, вы можете видеть, как красиво разворот является средним и неподвижным.

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

#Calculate the theoretical price and populate pairsData
pairsData[assets[0] + "_tPrice"] = pairsData[assets[0]] * hedgeRatio

#Plot market price and theoretical price of asset[0]
pairsData[assets[0]].plot(figsize=(30,15),label='Market Price '+assets[0])
pairsData[assets[0] + "_tPrice"].plot(figsize=(30,15),label='Theoretical Price ' + assets[0])
plt.legend(loc='upper right',prop={'size':30})
plt.ylabel('LogPrice')
plt.show()

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

И вот мы завершили Торговлю парами — Часть II. Мы на шаг дальше от разработки торговой системы после обсуждения статистической сводки, корреляции, стационарности и коинтеграции. Опять же, для тех, кто интересуется кодом, вот ссылка в сочетании с частью I:

https://gist.github.com/yalinyuksel/ef4a978ea32f0367a33b4520b2de939a

Часть III

Построение торговой стратегии на анализируемых данных в Части I и II

HAving проанализировал данные временных рядов о том, подходят ли они для торговли парами, — это только верхушка айсберга. Решение вопроса, который Гамлет задал много веков назад, приглашает нас в другую страну чудес: «Ну и что? Скажи мне, как я собираюсь монетизироваться». В этой заключительной части «Парной торговли» я собираюсь сосредоточиться на;

  • Построение торговой стратегии
  • Анализ результатов
  • Обсуждение проблем

Чтобы взглянуть на историю, стоящую за этим, вот Часть I и Часть II. Давайте погрузимся.

Стратегия — скользящая средняя с отклонениями (полосы Боллинджера)

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

def getTradeBands(prices, rate=50):
# Calculate the simple moving average (SMA) using the specified rate
sma = prices.rolling(rate).mean()

# Calculate the standard deviation (std) using the specified rate
std = prices.rolling(rate).std()

# Calculate the upper band by adding 1.5 times the std to the SMA
bandUp = sma + std * 1.5

# Calculate the lower band by subtracting 1.5 times the std from the SMA
bandDown = sma - std * 1.5

# Return the upper and lower bands
return bandUp, bandDown

#Sort Spread Data historically, and export spread
spreadPrices = pairsData['Spread'].sort_index(ascending=True)

bandUp, bandDown = getTradeBands(spreadPrices)

Стратегия заключается в открытии длинной позиции, когда спред касается нижней полосы (купить недооцененный актив, продать другой), удерживать ее до тех пор, пока спред не дойдет до верхней полосы, и открывать противоположную позицию сразу после закрытия первоначальной (продать переоцененный актив, купить другой). На следующей диаграмме показан пример использования полос Боллинджера для данных о спреде.

def pairsTradeStrategy(data, bandDown, bandUp):
buyPrice = [] # List to store buy prices
sellPrice = [] # List to store sell prices
spreadSignal = [] # List to store spread signals
signal = 0 # Variable to track current signal

for i in range(0, len(data)):
# Check for buy signal
if data[i-1] > bandDown[i-1] and data[i] < bandDown[i]:
if signal != 1:
buyPrice.append(data[i])
sellPrice.append(np.nan)
signal = 1
spreadSignal.append(signal)
else:
buyPrice.append(np.nan)
sellPrice.append(np.nan)
spreadSignal.append(0)
# Check for sell signal
elif data[i-1] < bandUp[i-1] and data[i] > bandUp[i]:
if signal != -1:
buyPrice.append(np.nan)
sellPrice.append(data[i])
signal = -1
spreadSignal.append(signal)
else:
buyPrice.append(np.nan)
sellPrice.append(np.nan)
spreadSignal.append(0)
else:
# No signal
buyPrice.append(np.nan)
sellPrice.append(np.nan)
spreadSignal.append(0)

return buyPrice, sellPrice, spreadSignal

# Call the pairsTradeStrategy function with the necessary inputs
buyPrice, sellPrice, spreadSignal = pairsTradeStrategy(spreadPrices, bandDown, bandUp)

# Create a copy of pairsData and sort it in ascending order
tradeFrame = pairsData[[assets[0], assets[1]]].copy().sort_index(ascending=True)

# Add the spreadSignal column to the tradeFrame DataFrame
tradeFrame['Signal'] = spreadSignal

Я знаю, что программная часть становится все больше и больше в части торговой стратегии, но, поверьте мне, тяжелая работа окупится. Здесь мы получаем кадр данных, который показывает торговые сигналы (1 — длинный портфель, -1 — короткий портфель) и стоимость выбранных активов для системы. В качестве примера на рисунке 1, 29 ноября 2019 года система генерирует короткую позицию для BZ=F и длинную позицию для CL=F, которые будут закрыты 3 декабря 2019 года и продолжатся с противоположными позициями (длинная BZ=F, короткая CL=F).

Единственное, что остается при реализации стратегии, — это фактически построить саму торговую стратегию, как описано выше. Соответствующий код можно найти по ссылке Git ниже вместе с полным проектом.

Результат — то, что мы имеем здесь

По моему скромному мнению, пост-анализ торговой системы — самая сложная, субъективная и недооцененная часть. Я сталкивался с трейдерами, которые выбрасывают систему в мусор с 60% выигрышем, и я читал истории о том, как трейдеры становились миллионерами с менее чем 40%, и они даже не публиковали сообщения в Instagram (отличное предупреждение о прочтении: «Волшебники неизвестного рынка» Джека Швагера). Поэтому я должен сказать, что это действительно зависит от того, какой продукт вы хотели бы иметь, от вашей эмоциональной устойчивости и от того, насколько вам может быть некомфортно.

На первый взгляд, я хотел бы видеть положительную кривую PnL, которая постоянно растет с низкой волатильностью. В то время как простой график показывает, как работает ваша кривая капитала, я всегда использую коэффициент Кальмара, чтобы определить, насколько быстро система восстанавливается после определенных периодов просадки, которые, как я ожидаю, будут больше, чем минимум 5 (робкие сказали бы 10). Следующие 3 примера объяснят, что я имел в виду:

  1. Фьючерсы на золото и серебро (01.01.2021– 19.05.2023, 1D)

Хотя 700% доходность кажется сочной, золото и серебро в этот период не подходят для парной торговли, поэтому даже не осмеливайтесь их использовать.

2. Фьючерсы на Brent vs WTI (01.01.2021–19.05.2023, 1D)

Определенно торгуемая пара с положительной доходностью. Однако плохое соотношение Кальмара. Среди тысяч активов следует найти лучший.

3. Компании, работающие в той же отрасли (01.01.2015–19.05.2023, 1D)

И бинго! Это точно суммирует результат, который кто-то был бы рад увидеть после всех этих усилий. Почти 2-кратная доходность с комфортной просадкой за 8 лет, что дает нам прекрасный коэффициент Кальмара 7,5

  • Имейте в виду, что все эти классы активов должны показывать определенные случаи.

Что может пойти не так?

Когда дело доходит до ответа на этот вопрос на финансовых рынках, есть только один, и только один: «Что угодно». Если вы не уверены, пожалуйста, обратитесь к спасению LTCM, хедж-фонда, возглавляемого экономистами Нобелевской премии.

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

2) Маржа: «Рынок может оставаться иррациональным дольше, чем вы можете оставаться платежеспособным». Нет никакой гарантии, что спред быстро превратится в среднее или нулевое значение для получения стабильной прибыли. Спасение LTCM в 1998 году является хорошим примером, демонстрирующим эффект систем с чрезмерным кредитным плечом, которые нацелены на получение безрисковой арбитражной прибыли.

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

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

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

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

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

Для того, чтобы поиграть с разными типами активов, проект можно найти по ссылке ниже:

https://gist.github.com/yalinyuksel/fceffc8bd93d8883f60afd4c96b29fb0

Единственное, что вам нужно сделать, это проверить тикер от Yahoo Finance и ввести его после запуска программы.

Парная торговля на основе данных

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

Основные принципы

Предположим, у вас есть пара инвестиционных целей X и Y, которые имеют некоторые потенциальные связи. Например, две компании производят одни и те же продукты, такие как Pepsi Cola и Coca Cola. Вы хотите, чтобы соотношение цен или базисные спреды (также известные как разница цен) между ними оставались неизменными с течением времени. Однако из-за временных изменений спроса и предложения, таких как большой ордер на покупку/продажу объекта инвестирования и реакция на важные новости одной из компаний, разница в цене между двумя парами может время от времени отличаться. В этом случае один объект инвестирования движется вверх, а другой — вниз относительно друг друга. Если вы хотите, чтобы это разногласие со временем нормализовалось, вы можете найти торговые возможности (или возможности арбитража). Такие арбитражные возможности можно найти повсюду на рынке цифровых валют или внутреннем рынке товарных фьючерсов, таких как отношения между BTC и активами-убежищами; Взаимосвязь соевого шрота, соевого масла и сортов сои в фьючерсах.

При временной разнице в цене вы продаете объект инвестирования с отличными показателями (растущий объект инвестирования) и покупаете объект инвестирования с плохими показателями (падающий объект инвестирования). Вы уверены, что процентная маржа между двумя инвестиционными объектами в конечном итоге упадет из-за падения инвестиционного объекта с отличными показателями или роста инвестиционного объекта с плохими показателями, или того и другого. Ваша транзакция будет приносить деньги во всех этих подобных ситуациях. Если объекты инвестирования движутся вверх или вниз вместе, не изменяя разницу в цене между ними, вы не заработаете и не потеряете деньги.

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

Объясните концепцию: две гипотетические цели инвестирования

  • Постройте нашу исследовательскую среду на платформе FMZ Quant

Прежде всего, для того, чтобы работать бесперебойно, нам нужно выстроить нашу исследовательскую среду. В этой статье мы используем платформу FMZ Quant (FMZ.COM) для построения нашей исследовательской среды, в основном для того, чтобы позже использовать удобный и быстрый интерфейс API и хорошо упакованную систему Docker этой платформы.

В официальном названии платформы FMZ Quant эта система Docker называется системой Docker.

Пожалуйста, обратитесь к моей предыдущей статье о том, как развернуть докер и робота: https://www.fmz.com/bbs-topic/9864.

Читатели, которые хотят приобрести собственный сервер облачных вычислений для развертывания докеров, могут обратиться к этой статье: https://www.fmz.com/digest-topic/5711.

После успешного развертывания сервера облачных вычислений и системы docker мы установим самый большой артефакт Python: Anaconda

Чтобы реализовать все соответствующие программные среды (библиотеки зависимостей, управление версиями и т. д.), требуемые в этой статье, самым простым способом является использование Anaconda. Это упакованная экосистема обработки и анализа данных Python и менеджер библиотеки зависимостей.

Чтобы узнать о способе установки Anaconda, обратитесь к официальному руководству Anaconda: https://www.anaconda.com/distribution/.

В этой статье также будут использоваться numpy и pandas, две популярные и важные библиотеки в научных вычислениях Python.

Приведенная выше основная работа также может относиться к моим предыдущим статьям, в которых рассказывается о том, как настроить среду Anaconda и библиотеки numpy и pandas. Для получения дополнительной информации, пожалуйста, обратитесь к: https://www.fmz.com/bbs-topic/9863.

Далее давайте воспользуемся кодом для реализации «двух гипотетических инвестиционных целей»:

import numpy as np
import pandas as pd

import statsmodels
from statsmodels.tsa.stattools import coint
# just set the seed for the random number generator
np.random.seed(107)

import matplotlib.pyplot as plt

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

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

# Generate daily returns
Xreturns = np.random.normal(0, 1, 100)
# sum them and shift all the prices up
X = pd.Series(np.cumsum(
Xreturns), name='X')
+ 50
X.plot(figsize=(15,7))
plt.show()

X объекта инвестирования моделируется для построения его ежедневной доходности через нормальное распределение

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

noise = np.random.normal(0, 1, 100)
Y = X + 5 + noise
Y.name = 'Y'
pd.concat([X, Y], axis=1).plot(figsize=(15,7))
plt.show()

X и Y объекта инвестирования в коинтеграцию

Коинтеграция

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

Y = ⍺ X + e

Где ⍺ — постоянное отношение, а e — шум.

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

(Y/X).plot(figsize=(15,7)) 
plt.axhline((Y/X).mean(), color='red', linestyle='--')
plt.xlabel('Time')
plt.legend(['Price Ratio', 'Mean'])
plt.show()

Соотношение и среднее значение между двумя совместно интегрированными инвестиционными целевыми ценами

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

Удобным методом тестирования является использование statsmodels.tsa.stattools. Мы увидим очень низкое значение p, потому что мы искусственно создали два ряда данных, которые максимально интегрированы.

# compute the p-value of the cointegration test
# will inform us as to whether the ratio between the 2 timeseries is stationary
# around its mean
score, pvalue, _ = coint(X,Y)
print pvalue

Результат : 1.81864477307e-17

Примечание: корреляция и коинтеграция

Корреляция и коинтеграция, хотя и похожи в теории, не одно и то же. Давайте рассмотрим примеры релевантных, но не совместно интегрированных рядов данных и наоборот. Во-первых, давайте проверим корреляцию рядов, которые мы только что сгенерировали.

X.corr(Y)

Результат : 0.951

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

ret1 = np.random.normal(1, 1, 100)
ret2 = np.random.normal(2, 1, 100)

s1 = pd.Series( np.cumsum(ret1), name='X')
s2 = pd.Series( np.cumsum(ret2), name='Y')

pd.concat([s1, s2], axis=1 ).plot(figsize=(15,7))
plt.show()
print 'Correlation: ' + str(X_diverging.corr(Y_diverging))
score, pvalue, _ = coint(X_diverging,Y_diverging)
print 'Cointegration test p-value: ' + str(pvalue)

Две связанные серии (не интегрированные вместе)

Коэффициент корреляции: 0,998
Значение P теста на коинтеграцию: 0,258

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

Y2 = pd.Series(np.random.normal(0, 1, 800), name='Y2') + 20
Y3 = Y2.copy()
Y3[0:100] = 30
Y3[100:200] = 10
Y3[200:300] = 30
Y3[300:400] = 10
Y3[400:500] = 30
Y3[500:600] = 10
Y3[600:700] = 30
Y3[700:800] = 10
Y2.plot(figsize=(15,7))
Y3.plot()
plt.ylim([0, 40])
plt.show()
# correlation is nearly zero
print 'Correlation: ' + str(Y2.corr(Y3))
score, pvalue, _ = coint(Y2,Y3)
print 'Cointegration test p-value: ' + str(pvalue)

Корреляция: 0,007546
Значение P теста на коинтеграцию: 0,0

Корреляция очень низкая, но значение p показывает идеальную коинтеграцию!

Как вести парную торговлю?

Поскольку два коинтегрированных временных ряда (например, X и Y выше) обращены друг к другу и отклоняются друг от друга, иногда базисные спреды бывают высокими или низкими. Мы ведем парную торговлю, покупая один объект инвестирования и продавая другой. Таким образом, если две инвестиционные цели падают или растут вместе, мы не будем ни зарабатывать, ни терять деньги, то есть мы нейтральны на рынке.

Возвращаясь к вышесказанному, X и Y в Y = ⍺ X + e, так что отношение (Y/X) движется вокруг своего среднего значения ⍺. Мы зарабатываем деньги за счет коэффициента возврата средней стоимости. Для этого обратим внимание на случай, когда X и Y находятся далеко друг от друга, то есть значение ⍺ слишком высокое или слишком низкое:

  • Коэффициент длинных позиций: Это когда коэффициент ⍺ очень мал, и мы ожидаем, что он увеличится. В приведенном выше примере мы открываем позицию, открывая длинную позицию Y и короткую позицию X.
  • Коэффициент коротких позиций: это когда коэффициент ⍺ очень велик, и мы ожидаем, что он уменьшится. В приведенном выше примере мы открываем позицию, открывая короткую позицию Y и открывая длинную позицию X.

Обратите внимание, что у нас всегда есть «позиция хеджирования»: если субъект торговли покупает убыточную стоимость, короткая позиция будет приносить деньги, и наоборот, поэтому мы невосприимчивы к общему рыночному тренду.

Если X и Y торгового объекта будут двигаться относительно друг друга, мы заработаем или потеряем.

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

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

Систематическая ошибка множественного сравнения относится к повышенной вероятности неправильной генерации важных значений p при выполнении большого количества тестов, потому что нам нужно выполнить большое количество тестов. Если мы проведем 100 тестов на случайных данных, мы должны увидеть 5 значений p ниже 0,05. Если вы хотите сравнить n торговых целей для совместной интеграции, вы выполните n (n-1)/2 сравнений, и вы увидите много неправильных значений p, которые будут увеличиваться с увеличением ваших тестовых выборок. Чтобы избежать такой ситуации, выберите несколько торговых пар и у вас есть основания определить, что они могут быть коинтеграцией, а затем протестируйте их по отдельности. Это значительно уменьшит множественную погрешность сравнения.

Поэтому давайте попробуем найти некоторые торговые цели, которые показывают коинтеграцию. Возьмем в качестве примера корзину крупных технологических акций США в индексе S&P 500. Эти торговые цели работают в аналогичных сегментах рынка и имеют цены на коинтеграцию. Мы сканируем список торговых объектов и тестируем коинтеграцию между всеми парами.

Возвращенная матрица баллов теста коинтеграции, матрица p-значений и все пары с p-значением меньше 0,05. Этот метод подвержен множественной предвзятости сравнения, поэтому на самом деле им необходимо провести повторную проверку. В этой статье, для удобства нашего объяснения, мы предпочитаем игнорировать этот момент в примере.

def find_cointegrated_pairs(data):
n = data.shape[1]
score_matrix = np.zeros((n, n))
pvalue_matrix = np.ones((n, n))
keys = data.keys()
pairs = []
for i in range(n):
for j in range(i+1, n):
S1 = data[keys[i]]
S2 = data[keys[j]]
result = coint(S1, S2)
score = result[0]
pvalue = result[1]
score_matrix[i, j] = score
pvalue_matrix[i, j] = pvalue
if pvalue < 0.02:
pairs.append((keys[i], keys[j]))
return score_matrix, pvalue_matrix, pairs

Примечание: Мы включили в данные рыночный бенчмарк (SPX) — рынок управлял потоком многих торговых объектов. Обычно вы можете найти два торговых объекта, которые кажутся взаимосвязанными; Но на самом деле они интегрируются не друг с другом, а с рынком. Это называется смешанной переменной. Важно проверить участие рынка в любых отношениях, которые вы найдете.

from backtester.dataSource.yahoo_data_source import YahooStockDataSource
from datetime import datetime
startDateStr = '2007/12/01'
endDateStr = '2017/12/01'
cachedFolderName = 'yahooData/'
dataSetId = 'testPairsTrading'
instrumentIds = ['SPY','AAPL','ADBE','SYMC','EBAY','MSFT','QCOM',
'HPQ','JNPR','AMD','IBM']
ds = YahooStockDataSource(cachedFolderName=cachedFolderName,
dataSetId=dataSetId,
instrumentIds=instrumentIds,
startDateStr=startDateStr,
endDateStr=endDateStr,
event='history')
data = ds.getBookDataByFeature()['Adj Close']
data.head(3)

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

# Heatmap to show the p-values of the cointegration test
# between each pair of stocks
scores, pvalues, pairs = find_cointegrated_pairs(data)
import seaborn
m = [0,0.2,0.4,0.6,0.8,1]
seaborn.heatmap(pvalues, xticklabels=instrumentIds,
yticklabels=instrumentIds, cmap=’RdYlGn_r’,
mask = (pvalues >= 0.98))
plt.show()
print pairs
[('ADBE', 'MSFT')]

Похоже, что «ADBE» и «MSFT» интегрированы. Давайте посмотрим на цену, чтобы убедиться, что она действительно имеет смысл.

S1 = data['ADBE']
S2 = data['MSFT']
score, pvalue, _ = coint(S1, S2)
print(pvalue)
ratios = S1 / S2
ratios.plot()
plt.axhline(ratios.mean())
plt.legend([' Ratio'])
plt.show()

График соотношения цен между MSFT и ADBE с 2008 по 2017 год

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

z-оценка (значение) = (значение — среднее) / стандартное отклонение

Предупреждение

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

def zscore(series):
return (series - series.mean()) / np.std(series)
zscore(ratios).plot()
plt.axhline(zscore(ratios).mean())
plt.axhline(1.0, color=’red’)
plt.axhline(-1.0, color=’green’)
plt.show()

Соотношение цен Z между MSFT и ADBE с 2008 по 2017 год

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

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

  • Сбор достоверных данных и очистка данных;
  • Создание функций из данных для идентификации торговых сигналов/логики;
  • Функциями могут быть скользящие средние или ценовые данные, корреляции или соотношения более сложных сигналов — комбинируйте их для создания новых функций;
  • Используйте эти функции для генерации торговых сигналов, то есть для того, какие сигналы следует отслеживать.

К счастью, у нас есть платформа FMZ Quant (fmz.com), которая завершила для нас вышеперечисленные четыре аспекта, что является большим благословением для разработчиков стратегий. Мы можем посвятить свою энергию и время разработке логики стратегии и расширению функций.

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

Чтобы дополнить логику и объяснить принцип в этой статье, мы подробно представим эту лежащую в основе логику. Однако в реальной работе считыватели могут напрямую вызывать интерфейс FMZ Quant API для выполнения вышеуказанных четырех аспектов.

Давайте начнем:

Шаг 1: Задайте свой вопрос

Здесь мы пытаемся создать сигнал, который скажет нам, будет ли соотношение покупать или продавать в следующий момент, то есть нашу прогнозную переменную Y:

Y = соотношение покупки (1) или продажи (-1)

Y(t)= Знак(Ratio(t+1) — Ratio(t))

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

Шаг 2: Соберите надежные и точные данные

FMZ Quant — ваш друг! Вам нужно только указать объект сделки, который будет торговаться, и источник данных, который будет использоваться, и он извлечет необходимые данные и очистит их для разделения дивидендов и объектов транзакции. Так что данные здесь очень чистые.

За последние 10 лет (около 2500 точек данных) мы получили следующие данные с помощью Yahoo Finance: цена открытия, цена закрытия, самая высокая цена, самая низкая цена и объем торгов.

Шаг 3: Разделите данные

Не забывайте об этом очень важном шаге в проверке точности модели. Мы используем следующие данные для разделения на обучение/проверку/тестирование.

  • Обучение 7 лет ~ 70%
  • Тест ~ 3 года 30%
ratios = data['ADBE'] / data['MSFT']
print(len(ratios))
train = ratios[:1762]
test = ratios[1762:]

В идеале мы также должны сделать проверочные наборы, но пока мы этого делать не будем.

Шаг 4: Проектирование функций

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

Мы используем следующие функции:

  • 60-дневный коэффициент скользящей средней: измерение скользящей средней;
  • 5-дневный коэффициент скользящей средней: измерение текущего значения среднего;
  • 60-дневное стандартное отклонение;
  • Оценка Z: (5d MA — 60d MA) / 60d SD.
ratios_mavg5 = train.rolling(window=5,
center=False).mean()
ratios_mavg60 = train.rolling(window=60,
center=False).mean()
std_60 = train.rolling(window=60,
center=False).std()
zscore_60_5 = (ratios_mavg5 - ratios_mavg60)/std_60
plt.figure(figsize=(15,7))
plt.plot(train.index, train.values)
plt.plot(ratios_mavg5.index, ratios_mavg5.values)
plt.plot(ratios_mavg60.index, ratios_mavg60.values)
plt.legend(['Ratio','5d Ratio MA', '60d Ratio MA'])
plt.ylabel('Ratio')
plt.show()

Соотношение цен между 60d и 5d MA

plt.figure(figsize=(15,7))
zscore_60_5.plot()
plt.axhline(0, color='black')
plt.axhline(1.0, color='red', linestyle='--')
plt.axhline(-1.0, color='green', linestyle='--')
plt.legend(['Rolling Ratio z-Score', 'Mean', '+1', '-1'])
plt.show()

Соотношение цен 60–5 Z Score

Z-оценка скользящего среднего значения выявляет свойство регрессии среднего значения коэффициента!

Шаг 5: Выбор модели

Начнем с очень простой модели. Глядя на диаграмму оценки z, мы видим, что если оценка z слишком высока или слишком низка, она вернется. Давайте воспользуемся +1/- 1 в качестве порога для определения слишком высокого и слишком низкого, а затем мы можем использовать следующую модель для генерации торговых сигналов:

  • Когда z ниже — 1,0, соотношение на покупку (1), потому что мы ожидаем, что z вернется к 0, поэтому коэффициент увеличивается;
  • Когда z выше 1,0, коэффициент продажи равен (- 1), потому что мы ожидаем, что z вернется к 0, поэтому коэффициент уменьшается.

Шаг 6: Обучение, проверка и оптимизация

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

# Plot the ratios and buy and sell signals from z score
plt.figure(figsize=(15,7))
train[60:].plot()
buy = train.copy()
sell = train.copy()
buy[zscore_60_5>-1] = 0
sell[zscore_60_5<1] = 0
buy[60:].plot(color=’g’, linestyle=’None’, marker=’^’)
sell[60:].plot(color=’r’, linestyle=’None’, marker=’^’)
x1,x2,y1,y2 = plt.axis()
plt.axis((x1,x2,ratios.min(),ratios.max()))
plt.legend([‘Ratio’, ‘Buy Signal’, ‘Sell Signal’])
plt.show()

Сигнал соотношения цен покупки и продажи

Сигнал кажется разумным. Кажется, что мы продаем, когда он высок или растет (красные точки), и покупаем его, когда он низкий (зеленые точки) и падает. Что это означает для фактического предмета нашей сделки? Давайте посмотрим:

# Plot the prices and buy and sell signals from z score
plt.figure(figsize=(18,9))
S1 = data['ADBE'].iloc[:1762]
S2 = data['MSFT'].iloc[:1762]
S1[60:].plot(color='b')
S2[60:].plot(color='c')
buyR = 0*S1.copy()
sellR = 0*S1.copy()
# When buying the ratio, buy S1 and sell S2
buyR[buy!=0] = S1[buy!=0]
sellR[buy!=0] = S2[buy!=0]
# When selling the ratio, sell S1 and buy S2
buyR[sell!=0] = S2[sell!=0]
sellR[sell!=0] = S1[sell!=0]
buyR[60:].plot(color='g', linestyle='None', marker='^')
sellR[60:].plot(color='r', linestyle='None', marker='^')
x1,x2,y1,y2 = plt.axis()
plt.axis((x1,x2,min(S1.min(),S2.min()),max(S1.max(),S2.max())))
plt.legend(['ADBE','MSFT', 'Buy Signal', 'Sell Signal'])
plt.show()

Сигналы на покупку и продажу акций MSFT и ADBE

Пожалуйста, обратите внимание на то, как мы иногда получаем прибыль на «коротких ногах», иногда на «длинных ногах», а иногда и на том, и на другом.

Мы удовлетворены сигналом тренировочных данных. Давайте посмотрим, какую прибыль может принести этот сигнал. Когда коэффициент низкий, мы можем сделать простой бэктестер, купить коэффициент (купить 1 акцию ADBE и продать акцию x акции MSFT) и продать коэффициент (продать 1 акцию ADBE и купить акцию MSFT с соотношением x), когда он высок, и рассчитать транзакции PnL этих коэффициентов.

# Trade using a simple strategy
def trade(S1, S2, window1, window2):

# If window length is 0, algorithm doesn't make sense, so exit
if (window1 == 0) or (window2 == 0):
return 0

# Compute rolling mean and rolling standard deviation
ratios = S1/S2
ma1 = ratios.rolling(window=window1,
center=False).mean()
ma2 = ratios.rolling(window=window2,
center=False).mean()
std = ratios.rolling(window=window2,
center=False).std()
zscore = (ma1 - ma2)/std

# Simulate trading
# Start with no money and no positions
money = 0
countS1 = 0
countS2 = 0
for i in range(len(ratios)):
# Sell short if the z-score is > 1
if zscore[i] > 1:
money += S1[i] - S2[i] * ratios[i]
countS1 -= 1
countS2 += ratios[i]
print('Selling Ratio %s %s %s %s'%(money, ratios[i], countS1,countS2))
# Buy long if the z-score is < 1
elif zscore[i] < -1:
money -= S1[i] - S2[i] * ratios[i]
countS1 += 1
countS2 -= ratios[i]
print('Buying Ratio %s %s %s %s'%(money,ratios[i], countS1,countS2))
# Clear positions if the z-score between -.5 and .5
elif abs(zscore[i]) < 0.75:
money += S1[i] * countS1 + S2[i] * countS2
countS1 = 0
countS2 = 0
print('Exit pos %s %s %s %s'%(money,ratios[i], countS1,countS2))


return money
trade(data['ADBE'].iloc[:1763], data['MSFT'].iloc[:1763], 60, 5)

Результат : 1783.375

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

Мы также можем попробовать более сложные модели, такие как логистическая регрессия и SVM, чтобы предсказать 1/- 1.

Теперь давайте продвинем эту модель, которая подводит нас к:

Шаг 7: Тестирование тестовых данных на истории

Опять же, платформа FMZ Quant использует высокопроизводительный механизм тестирования QPS / TPS, чтобы точно воспроизвести историческую среду, устранить распространенные количественные подводные камни тестирования и вовремя обнаружить недостатки стратегий, чтобы лучше помочь реальным инвестициям в ботов.

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

Тестирование на истории простое. Мы можем использовать приведенную выше функцию для просмотра PnL тестовых данных.

trade(data['ADBE'].iloc[1762:], data['MSFT'].iloc[1762:], 60, 5)

Результат : 5262.868

Модель отлично справилась со своей задачей! Это стало нашей первой простой парной торговой моделью.

Избегайте переобучения

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

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

# Find the window length 0-254 
# that gives the highest returns using this strategy
length_scores = [trade(data['ADBE'].iloc[:1762],
data['MSFT'].iloc[:1762], l, 5)
for l in range(255)]
best_length = np.argmax(length_scores)
print ('Best window length:', best_length)
('Best window length:', 40)

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

# Find the returns for test data
# using what we think is the best window length
length_scores2 = [trade(data['ADBE'].iloc[1762:],
data['MSFT'].iloc[1762:],l,5)
for l in range(255)]
print (best_length, 'day window:', length_scores2[best_length])
# Find the best window length based on this dataset,
# and the returns using this window length
best_length2 = np.argmax(length_scores2)
print (best_length2, 'day window:', length_scores2[best_length2])
(40, 'day window:', 1252233.1395)
(15, 'day window:', 1449116.4522)

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

plt.figure(figsize=(15,7))
plt.plot(length_scores)
plt.plot(length_scores2)
plt.xlabel('Window length')
plt.ylabel('Score')
plt.legend(['Training', 'Test'])
plt.show()

Мы видим, что все, что находится между 20 и 50, является хорошим выбором для временных окон.

Чтобы избежать переобучения, мы можем использовать экономические рассуждения или характер алгоритма для выбора длины временного окна. Мы также можем использовать фильтр Калмана, который не требует от нас указания длины; Этот подход будет описан далее в другой статье.

Тестирование стратегии парной торговли на истории

Это переработанная версия оригинального поста на Medium. Торговый период в этой версии сократился вдвое (с одного квартала или 63 дня до полуквартала или 31 дня). Результаты торговли парами значительно лучше.

В этом блокноте выполняется бэктест стратегии парной торговли, основанный на выборе пар с использованием корреляции и коинтеграции. Бэктест предназначен для того, чтобы напоминать реальную торговлю по стратегии. Код Python в этом блокноте будет служить моделью для кода Java, который будет использоваться для торговли стратегией (я бы никогда не развернул Python в приложении, требующем высокой надежности).

Парная торговля — это стратегия, которая восходит к 1980-м годам.

Парная торговля была впервые предложена Джерри Бамбергером, а затем возглавила количественная группа Нунцио Тарталья в Morgan Stanley в 1980-х годах.
Торговля парами в Википедии

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

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

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

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

Исходный код этой записной книжки можно найти на GitHub (https://github.com/IanLKaplan/pairs_trading/blob/master/pairs_trading_backtest.ipynb)

Этот блокнот является продолжением блокнота Исследовательская статистика парной торговли (https://github.com/IanLKaplan/pairs_trading/blob/master/pairs_trading.ipynb). В предыдущем блокноте рассматриваются алгоритмы подбора пар и статистика торговли парами. Это статистическое исследование обеспечивает основу для стратегии, которая тестируется в этой записной книжке. Обсуждение торговли парами, алгоритмов, используемых для выбора пар, и предыстории стратегии, протестированной в этой записной книжке, см. в предыдущей записной книжке.

Стратегия торговли парами

Шортинг акций в парной торговле

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

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

Когда акция «шортится», акция заимствуется, а затем сразу же продается, получая наличные деньги. Например, если 100 акций по текущей рыночной цене 10 будут шортить, на брокерский счет будет зачислено 1000 (100 х 10). В какой-то момент в будущем заемные акции должны быть возвращены путем покупки акций по текущей рыночной цене. Короткая позиция прибыльна, когда рыночная цена акции падает. Например, если рыночная цена акции шортится на уровне 10 и снижается до 6, прибыль составляет 4 на акцию (4 x 100 = 400).

Короткие позиции могут иметь неограниченный убыток, когда цена акций растет. Например, если рыночная цена 10 акций вырастет до 14 за акцию, при покупке 100 акций произойдет убыток в размере 400.Если цена акций удвоится до 20, будет убыток в размере 10 на акцию или 1000 для короткой позиции.

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

Когда акция шортится, она заимствуется у брокера. Это рассматривается как маржинальный кредит. Брокерская компания требует, чтобы клиент поддерживал баланс с ликвидными активами в размере 150 процентов от суммы займа. Это включает в себя выручку от короткой продажи плюс 50 процентов. Например, если 100 акций 10-долларовой акции будут шортить, на счет будет зачислено 1000. На счету также должен быть дополнительный баланс в размере 500. Маржинальное требование может быть удовлетворено наличными деньгами или высоко торгуемыми акциями «голубых фишек» (например, акциями S&P 500).

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

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

  1. Открываем короткую позицию. Это приведет к наличным деньгам от короткой продажи.
  2. Выручка от короткой продажи используется для оплаты длинной позиции. Для длинной позиции может потребоваться дополнительная сумма денежных средств.

Это кратко изложено в уравнениях ниже. Оператор «/» является целочисленным оператором деления:

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

Пример:

Если наш торговый капитал равен 100 000, мы можем открыть 200 000 длинных и 200 000 коротких позиций с 50-процентной маржой. В идеале маржинальные средства могут быть распределены на такой актив, как ETF облигаций, который выплачивает ежемесячные дивиденды.

Если мы торгуем 100 парами, то каждой паре выделяется 1 600 для длинных и коротких позиций.

Акция A (длинная позиция) составляет 63 за акцию, а акция B (короткая позиция) — 54 за акцию. Сначала мы открываем короткую позицию, чтобы получить деньги за длинную позицию. Операции деления являются целочисленными делениями.

Interactive Brokers взимает ежегодную комиссию за короткие позиции в размере 0,25% или 0,25/360% в день, когда позиция удерживается. Это достаточно мало, чтобы краткосрочные процентные ставки можно было игнорировать.

Стратегия торговли парами будет иметь портфель коротких и длинных позиций, которые открываются и закрываются по мере движения парного спреда. В любое время совокупная стоимость коротких и длинных позиций, а также маржинальные денежные средства должны быть в пределах маржинальных требований. Если есть дефицит ликвидности по отношению к марже, IB ликвидирует сумму дефицита, умноженную на 4 (ой!)

При открытии короткой позиции должна быть маржа не менее 50 процентов. Интерактивные брокеры выводят на рынок в режиме реального времени. Регламент SEC T требует, чтобы для открытых коротких позиций была маржа не менее 25%.

Справочник по марже Interactive Brokers

Проблемы с данными о ценах на акции

Бэктест в этом блокноте использует дневную цену закрытия акций. Если торгуется большое количество акций (т.е. 100 акций), торговое приложение Java будет использовать внутридневные цены. Внутридневные цены, как правило, не совпадают с ценой закрытия. Цель бэктеста в этом блокноте — дать представление о прибыльности и риске стратегии торговли парами, поэтому эта разница приемлема.

Периоды времени в выборке и вне выборки

Набор для торговли парами строится на основе взгляда на прошедший период выборки. Вневыборочный период — это торговый период.

  • Период выборки: шесть месяцев (126 торговых дней)
  • Вневыборочный (торговый) период: три месяца (31 торговый день). 31-дневный период должен быть достаточно длинным, чтобы зафиксировать возврат к среднему значению, сохраняя при этом статистические характеристики периода выборки. При использовании относительно короткого периода вне выборки снижается риск удержания пар, и статистика для отбора пар может быть рассчитана после периода вне выборки.

Отслеживание данных

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

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

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

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

Стратегия

Получите пары для каждого сектора промышленности S&P 500

Для каждого 126-дневного окна выборки (далее каждые 63 дня):

  1. Выберите пары с корреляцией ценового ряда закрытия больше или равной 0,75
  2. Выберите пары с высокой корреляцией, которые показывают коинтеграцию Грейнджера
  3. Отсортируйте временные ряды спреда пары по волатильности (от высокой до низкой волатильности). Пары с более высокой волатильностью (стандартным отклонением) с большей вероятностью будут прибыльными.
  4. Выберите первые N пар из списка отсортированных пар

Период торговли вне выборки

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

На дату начала теста общая доступная сумма денежных средств составляет N долларов (например, 100 000). При требуемой марже в 50 процентов это позволило бы нам иметь позицию 2N для длинных и коротких позиций (например, для 100 000 наличных денег, длинных и коротких позиций по 200 000 каждая).

Позиции открываются на целые акции.

В конце каждого невыборочного торгового периода все открытые позиции будут закрыты.

Для каждой пары (в наборе N пар) в период торговли вне выборки:

Фильтрующие пары

Из совокупности отраслевых пар акций S&P 500 пары сначала отбираются для высокой корреляции, а затем для коинтеграции с использованием теста Энгла-Грейнджера (линейная регрессия и тест ADF).

Распределение стандартного отклонения спреда пар показано ниже.

png

Нестабильная статистика

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

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

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

png
png
png
png

Полоса Боллинджера

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

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

Это полоса Боллинджера. В литературе по парной торговле в ряде статей полосы Боллинджера используются для парной торговли. Смотрите Торговля парами на практике Джонатан Кинлей, 18 февраля 2019 г.

Общий предел полосы Боллинджера равен двум стандартным отклонениям. Кажется, это хорошо работает для парной торговли. Я пробовал как один, так и два диапазона стандартного отклонения. Диапазон двух стандартных отклонений приводит к более высокой прибыли портфеля и меньшему количеству торговли.

На приведенных ниже графиках показаны спреды для пар в выборке и вне выборки, а также полосы Болинджера с использованием среднего значения и стандартного отклонения.

png
png

Количество пар

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

В этом бэктесте используется 100 пар. Пары выбираются из отраслей промышленности, как описано выше. Если бы акции в парах были уникальными, так что пары не делили одну и ту же акцию, было бы 200 уникальных акций. Это будет почти половина набора акций S&P 500. Стратегия парной торговли, использующая такой набор акций, рискует воспроизвести производительность S&P 500 (цель стратегии торговли парами состоит в том, чтобы превзойти S&P 500 либо по доходности, либо по риску, либо по тому и другому).

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

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

png

Открытие пар, позиций и маржи

Когда спред отклоняется от среднего значения на 2σ, открывается парная позиция с длинными и короткими позициями примерно равной стоимости в долларах (мы покупаем целые акции, поэтому может оказаться невозможным иметь точно эквивалентные позиции).

Пар 100, поэтому на пару выделяется 1/100 капитала. Если у нас есть 100 000 торгового капитала, на каждую пару выделяется 1 000. Это поддерживает 2 000 коротких позиций и 2 000 длинных. Длинная позиция используется для частичного удовлетворения маржинальных требований для короткой позиции. При открытии короткой позиции должно быть 50% ликвидных активов или денежных средств для маржи (этого требует Положение T Комиссии по ценным бумагам и биржам США). В этом случае у нас есть 1,000 для 50% маржи. Когда шорт открывается, акция берется в долг и сразу же продается. Выручка от короткой продажи используется для открытия эквивалентной позиции на 2 000 позиций. В результате получается относительно рыночно-нейтральная позиция с 2 000 короткими и 2 000 длинными позициями по фондовой паре.

Как только позиция открыта, цены на акции в паре движутся. По мере движения цен Положение T требует маржи не менее 25% для коротких позиций в дополнение к длинной позиции, которая уравновешивает 100% короткой позиции. В худшем случае, если короткая позиция растет, а длинная идет вниз, может потребоваться дополнительная маржа.

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

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

png

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

Распределения

Один из способов проверить достоверность результатов бэктеста — посмотреть статистику возврата.

Когда сделка по парам закрыта, будет возврат как для коротких, так и для длинных пар. Эти парные позиции составляют портфель из двух активов, где каждый актив (длинные и короткие позиции) составляет примерно 50 процентов портфеля. Несколько пар могут быть закрыты в один день. Это формирует портфель на день. Доходность для каждой пары может быть сложена в виде взвешенной суммы, где вес равен 1/num_pairs, где num_pairs — количество пар, закрытых в этот день.

Распределение возвратов показано ниже. Распределение доходности выглядит как распределение доходности, которое можно ожидать для активов фондового рынка.

png

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

png

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

png

Открытые позиции по парам

Длинная/короткая позиция по паре открывается, когда спред пары на 2σ выше или ниже среднего значения. Когда спред пересекает среднее значение, позиция пары закрывается. На графике ниже показано количество дней, в течение которых открыта позиция по паре.

png

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

png

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

total profit (loss): 236994.0 min val: -35870.0 max val: 12421.0
png
total percent positive trades: 58.83

Парный портфель против случайного портфеля

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

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

png

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

Торговля парами против SPY: пять лет и один год

На графиках ниже показана эффективность стратегии торговли парами за последние пять лет и последний год.

png
png

Торговый период по стратегии составляет половину квартала (31 торговый день). Перед началом торгового периода следующий набор пар выбирается с использованием корреляции и коинтеграции за ретроспективный период в 126 торговых дней (шесть месяцев). Сделки по парам открываются в начале квартала, так как спред пары достаточно расходится от среднего. Позиция пары закрывается, когда спред возвращается к среднему значению. Все открытые позиции по парам закрываются в конце квартала.

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

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

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

Обсуждение

Доходность для коинтегрированных и случайных пар

png

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

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

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

Есть много статей о стратегии торговли парами, и она описана в таких книгах, как «Алгоритмическая торговля» Э.. Чана и «Торговля парами» Ганапати Видьямурти. Несмотря на то, что в статьях и книгах рассматриваются расчеты в выборке для коинтеграции, очень немногие исследуют постоянство этой статистики. Очень немногие ссылки используют бэктесты, которые очень похожи на реальную торговлю стратегией.

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

Стратегия парной торговли является рыночно-нейтральной стратегией и имеет значительно меньшую максимальную просадку, чем S&P 500. Стратегия пар торгуется с более низкими просадками для производительности, которая отстает от S&P 500 в некоторые годы.

Разработка стратегии торговли парами

Часть 1

Отказ от ответственности: это не финансовый совет, торгуйте осторожно и держитесь подальше от азартных игр.

Почему парная торговля?

ВВ 1980-х годах команда количественной торговли наткнулась на открытие, которое произвело революцию в области количественных финансов. Это звучит не очень революционно, но вот оно: цены на активы, которые определяются одним и тем же рынком и макроэкономическими факторами, имеют тенденцию двигаться вместе. Что такого революционного в утверждении, что определенные сектора рынка движутся относительно определенного равновесия? Цены на помидоры и огурцы всегда росли и падали вместе, как и автомобили и кофейные чашки.

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

Этого можно достичь, просто имея длинную позицию по одному активу, получившему название «длинная нога», и одновременно имея короткую позицию по другому, «короткому участку».

В отличие от философии оптимизации портфеля со средней дисперсией, хитрость здесь заключается в создании портфеля с возвратом к среднему, на основе которого можно использовать предсказательную способность до конца краткосрочной прибыли. Портфель, возвращающийся к среднему, гораздо более предсказуем просто в силу своей стационарности (постоянства среднего значения и дисперсии во времени). В этом можно убедиться, посмотрев на график ACF и PACF:

Графики генерируются с помощью Statsmodels api

Простая линейная авторегрессионная модель может быть использована для моделирования портфеля возврата к среднему значению, называемого «спредом» между двумя активами. Такие стратегии называются статистическим арбитражем или StatArb, потому что они предоставляют возможности для статистически безрисковой прибыли. Существует много исследовательской литературы в этой области, в которой подробно рассказывается о том, как найти такие пары активов, стратегиях, подробно описывающих, как можно моделировать спред и торговать им, даже о стратегиях управления рисками, характерных для этого типа стратегии. Все это будет рассмотрено в этой серии статей о StatArb, как можно более подробно и подробно. Перво-наперво мы сосредоточимся в этой статье на подборе пар, который можно сделать, используя подход на минимальное расстояние.

Соедините их в пару

Мы импортируем данные об активах, разделенные на периоды обучения (2017/01–2019/04), валидации (2019/01–2021/04) и тестирования (2021/04–2023/07).

Цены на бревна, поставленные в равные условия путем вычитания исходного значения

Моё личное мнение, что подглядывать в любые нетренировочные данные — смертный грех, это холм, на котором я умру.

Подход к минимальному расстоянию, вкратце, выглядит следующим образом:

Мы вычисляем стандартизированные ценовые ряды для каждого актива

Средняя цена актива i
Стандартное отклонение цены актива i
Стандартизированная цена

Затем мы вычисляем квадратную сумму для всех пар в нашей вселенной активов

И отсортируйте пары (i,j) по квадратной сумме, чтобы найти пары-кандидаты. Те, у кого меньше всего S-S, с большей вероятностью, чем другие, будут соинтегрированы. Это можно визуализировать с помощью тепловой карты, которая поначалу выглядит довольно беспорядочно, но вы поняли идею.from scipy.spatial.distance import pdist, squareform
from scipy.cluster.hierarchy import dendrogram, linkage
# price dataframe ‘m’ is standardized
m -= m.mean()
m /= m.std()
# scipy has an implementation of dozens of distance measures
# there are many distance measures that can be used, however here we will keep it vanilla
dist_matrix_condensed = pdist(m.T, metric = ‘euclidean’)
# convert it from list to square form
dist_matrix_sqaure = squareform(dist_matrix_condensed, checks = True)

fig, axes = plt.subplots(1,1,figsize = (20,20), dpi = 400)
ax = axes
# make a heatmap out of the matrix
sns.heatmap(dist_matrix_sqaure, ax = ax)
# make it look chique
cbar = ax.collections[0].colorbar
cbar.ax.tick_params(labelsize=20)
ax.set_xticklabels(log_returns.columns, rotation = 90, fontsize = 15)
img = ax.set_yticklabels(log_returns.columns, rotation = 0, fontsize = 15)

ax.set_title(r’$SS_{i,j} Matrix$’, fontsize = 30)

fig.tight_layout()

Дендрограмма была бы более полезна при рассмотрении корзин активов, определении того, какие кандидаты могут быть совместно интегрированы (коинтеграция — это когда два временных ряда демонстрируют тенденции, но их линейная комбинация может быть построена таким образом, чтобы тенденция была устранена то есть то, что мы ищем):fig, axes = plt.subplots(1,1,figsize = (10,5), dpi = 400)
ax = axes
# this converts the distance measure list (list, NOT the matrix form)
# into a matrix of clusters
Z = linkage(dist_matrix_condensed)
# plots the matrix in the form of a dendrogram
dn = dendrogram(Z, ax = ax, labels = log_returns.columns)
ax.set_ylabel(r’$SS_{i,j}$’)

Различные цветовые коды для каждого набора ветвей указывают на то, что это корзина акций, которые «ближе» друг к другу, чем другие за пределами корзины. Например, посмотрите на оранжевую корзину (PHDC, BTFH, MNHD, ACGC, HELI):

Эта корзина, конечно, может быть просто аномалией. Рыночные режимы постоянно меняются, что приводит к формированию и распаду различных корзин активов. При этом при выборе пар важно понимать, ПОЧЕМУ они проявляют такое поведение. Это может быть связано с разными причинами, однако эти акции часто находятся в одном и том же секторе и, следовательно, обусловлены одними и теми же экономическими и рыночными факторами. Быстрый поиск по компонентам этой корзины показывает, что три из пяти тикеров (PHDC, MNHD, HELI) относятся к сектору недвижимости и строительства, что говорит о том, что они, скорее всего, останутся соинтегрированными вне выборки.

Святой Грааль

Однако, в соответствии с методом минимального расстояния, мы приступим к выбору глобальной минимальной пары EFID/PRMH. Эти пары не находятся в одном секторе, поэтому мы должны быть осторожны при торговле этими парами, так как они более склонны к постоянной потере равновесия.

При торговле парами часто предпочитается, чтобы стратегия была нейтральной по отношению к доллару. Это означает, что в среднем убыток, понесенный при использовании 1 доллара в одном активе, должен быть эквивалентен прибыли, полученной от продажи (б) суммы долларов в другом активе. Это можно сформулировать следующим образом:

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

Что составляет около 1.42 доллара на один доллар в длину. Тот же результат можно получить, регрессируя цены длинных ног на короткие, этот метод на самом деле является более распространенным. Я нахожу это более интуитивным, хотя они оба эквивалентны:from statsmodels.regression.linear_model import OLS
import statsmodels.api as sm
# train dataframe holds the training log price series
# chosen_pair[1] is PRMH and chosen_pair[0] is EFID
model = OLS(train[chosen_pair[1]], sm.add_constant(train[chosen_pair[0]]), hasconst=False).fit()

model.summary()

Statsmodels имеет лучшую реализацию регрессии OLS, сражайтесь со мной

Обе оценки хорошо согласуются с доверительными интервалами друг друга и с честью проходят t-критерии. Аллилуйя! Наконец, спред может быть выражен в виде портфеля со следующими распределениями:

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

Тестовая статистика (-2,968) находится между уровнями значимости 1% и 5% (-3,437, -2,864), что говорит о том, что спред почти не является стационарным. Для целей этой серии этот спред будет считаться торгуемым (не делайте этого в реальной жизни).

Часть 2

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

  1. Постройте математическую модель спреда
  2. Понять, как получать прибыль, торгуя спредом
  3. Постройте и протестируйте стратегию парной торговли

Краткое резюме

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

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

Вкусные процессы

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

Который моделируется с помощью процесса Вайнера:

Главным кандидатом на стохастический аналог синусоиды является процесс Орнштейна-Уленбека (O-U):

Который в основном можно описать как процесс Вайнера с дополнительным экспоненциальным членом распада — за исключением того, что исчисление никоим образом не такое базовое. Процесс Вайнера недифференцируемый, поэтому SDE (стохастические дифференциальные уравнения) не могут быть решены, как «ванильные» ODE, которые все принимают в старшей школе. Не стесняйтесь пропустить эту часть статьи, она не совсем важна для парной торговли, хотя понимание стохастического исчисления (которое построено на лемме Ито) имеет первостепенное значение в финансовой математике. Чтобы приспособить этот процесс к нашему распространению, мы должны решить SDE в непрерывном времени, затем дискретизировать его и превратить в регрессионную задачу, которую можно решить с помощью OLS. Мы можем начать с удаления тета-параметра с помощью подстановки (которая позже будет отменена):

Таким образом, чтобы:

Затем мы можем подставить фиктивную переменную и найти ее дифференциал с помощью правила произведения:

и заменить измененное SDE:

и интегрироваться с обеих сторон:

Затем мы можем отменить смещение

И получите это

Мы можем легко видеть, что среднее значение:

и дисперсия составляет

Мы можем дискретизировать эти уравнения для моделирования процесса O-U с помощью Эйлера-Маруямы:

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

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

тогда актив A будет GBM + процесс OU, а актив B будет GBM минус процесс OU:

Вверху: Коинтегрированные активы A и B, Внизу: Актив A минус Актив B

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

Найди себе подходящую форму

Используя дискретизированную версию SDE, мы можем подогнать спред с помощью регрессии OLS:

С параметрами:

Существуют и другие методы оценки параметров, такие как MLE, которые будут связаны в ссылках.

Было бы полезно создать класс python для процесса O-U, который позволяет нам использовать оба метода (MLE и OLS), а также моделировать процесс:from statsmodels.tsa.ar_model import AutoReg
import scipy
from stochastic.processes.diffusion import VasicekProcess

class Ornstein_Uhlenbeck():
def __init__(self):
pass

def fit(self, spread, delta_t=1, method = ‘MLE’):
if method == ‘MLE’:
X_x = spread.shift(1).fillna(0).sum()
X_y = spread.sum()
X_xx = (spread.shift(1).fillna(0)**2).sum()
X_yy = (spread**2).sum()
X_xy = (spread*spread.shift(1).fillna(0)).sum()
n = len(spread)
«»»
theta is the process mean
mu is the mean reversion strength
sigma is the residual volatility
«»»
self.theta = (X_y*X_xx — X_x*X_xy)/(n*(X_xx — X_xy) — X_x**2 + X_x*X_y)

self.mu = np.log(X_xy — self.theta*X_x — self.theta*X_y + n*self.theta**2)
self.mu -= np.log(X_xx — 2*self.theta*X_x + n*self.theta**2)
self.mu /= -delta_t

bracket_one = 2*self.mu/(n*(1-np.exp(-2*self.mu*delta_t)))
bracket_two = X_yy — 2*np.exp(-self.mu*delta_t)*X_xy + np.exp(-2*self.mu*delta_t)*X_xx — 2*self.theta*(1-np.exp(-self.mu*delta_t))*(X_y — np.exp(-self.mu*delta_t)*X_x) + n*(self.theta*(1-np.exp(-self.mu*delta_t)))**2

self.volatility = np.sqrt(bracket_one*bracket_two)
elif method == ‘Least-squares’:
model = AutoReg(spread,exog = np.ones(len(spread)),lags = 1, trend = ‘n’).fit()

beta, constant = model.params.values[0], model.params.values[1]

self.mu = — np.log(beta) / delta_t
self.theta = constant/(1-beta)

residuals = spread — beta*spread.shift(1).fillna(0) — constant
self.volatility = np.std(residuals)*np.sqrt(2*self.mu/(1-beta**2))

def log_likelihood(self, spread):

proxy_volatility = self.volatility * ((1 — np.exp(-2*self.mu))/(2*self.mu))**0.5
summation_term = (spread — spread.shift(1).fillna(0)*np.exp(-self.mu) — self.theta*(1-np.exp(-self.mu)))**2

return -0.5*np.log(2*3.1415) — np.log(proxy_volatility) — summation_term.sum()*0.5/(len(spread) * proxy_volatility**2)

def simulate(self, N):
diffuser = VasicekProcess(speed = self.mu,mean = self.theta, vol = self.volatility, t = N)

return diffuser.sample(N, initial = 0), diffuser.times(N)

Дельта Т здесь равна 0,5, потому что ряд цен на бревно включает в себя как цены открытия, так и цены закрытия за один и тот же день

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

Мы можем посмотреть на распределение периода полураспада, чтобы определить, как выглядит 95-й процентиль. Установка эксперимента следующая:

  1. Сгенерируйте O-U путь с использованием оценочных параметров
  2. Подгонка O-u к сгенерированному пути
  3. Возвращаемые параметры для mu, theta и волатильности

Затем нанесите эти параметры на гистограмму. Это называется параметрической начальной загрузкой:mus = []
for i in range(2000):
mean_reversion = Ornstein_Uhlenbeck()
mean_reversion.fit(residuals, delta_t = 0.5)
process = mean_reversion.simulate(len(train))[0]
mean_reversion = Ornstein_Uhlenbeck()
mean_reversion.fit(pd.Series(process))
mus.append(mean_reversion.mu)

fig, ax = plt.subplots(1, figsize = (7,7), dpi = 400)
time_constant = np.log(2)/mus
img = ax.hist(time_constant, bins = 100, density=True)
ax.axvline(np.percentile(time_constant,95), c = ‘red’, label = ‘5th and 95th CIs’)
ax.axvline(np.percentile(time_constant,5), c = ‘red’)
ax.legend()
ax.set_xlabel(r’$\tau$’, fontsize =20)
ax.set_title(r’$\tau$ Bootstrap distribution’, fontsize =20)

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

Где деньги?

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

Стратегия парной торговли, реализованная с использованием 4-дневной скользящей средней (опционально)

И вот почему: нет абсолютно никакой гарантии, что:

  1. Спред останется средним
  2. Параметры спреда не изменятся

Таким образом, нам необходимо разработать стратегию, которая будет устойчива к обеим этим вещам и будет способна справляться с этими ситуациями. Существует также вопрос оптимальных порогов: когда именно вы должны покупать? Вы можете покупать с удвоенным стандартным отклонением, но спред достигает только ~ 5% времени, но это позволяет получить большую прибыль на сделку. Другой подход может заключаться в половине стандартного отклонения, хотя это дает гораздо больше торговых возможностей, прибыль на сделку низкая и, скорее всего, будет съедена транзакционными издержками.

Если спред можно смоделировать как нормальное распределение, то его абсолютное значение можно смоделировать как сложенное. Это позволяет оптимальной пороговой задаче иметь аналитическое решение (заимствованное для A Signal Processing Perspective on Financial Engineering):

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

где Phi — кумулятивная функция распределения нормального распределения. Тогда средняя прибыль составит (с точки зрения сигмы спреда):

Максимизация прибыли — это простой вопрос решения:

Использование разделения переменных:

Интеграция и упрощение:

Это говорит нам о том, что оптимальным порогом является место, где z-оценка пересекается с нормальной функцией выживания. Поскольку функция выживания не имеет аналитической формулы, ее можно легко решить графически, с помощью благородного метода «присмотреться к ней»:

По-видимому, составляет около 0,75–0,8 стандартного отклонения. Учитывая, что наш порог будет равен 0,8 стандартного отклонения, мы хотим узнать наше среднее время удержания, которое мы можем рассчитать по формуле, приведенной в книге:

Где c — это:

А именно, стандартизированный порог входа. c = 0,8, среднее время удержания получается:

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

Часть 3

В предыдущих двух частях мы познакомили с понятием парной торговли, как формируется спред из корзины минимум из 2-х активов, которые должны быть интегрированы. Мы также смоделировали распространение, решив стохастическое дифференциальное уравнение для процесса Орнштейна-Уленбека, который изначально был предложен для моделирования частиц в газе. Изучив свойства спреда, определив теоретически оптимальный порог входа, среднее время выдержки и период полураспада, мы в некоторой степени готовы закодировать наш фреймворк тестирования на истории.

Численное решение процесса Орнштейна-Уленбека
Уравнение для среднего времени удержания, где c — стандартизированный порог входа

Что такое тестирование на истории?

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

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

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

  1. Векторизованное бэктестирование

Векторизованный подход к тестированию на истории использует преимущества, как вы уже догадались, векторизации. Векторизованный подход генерирует сигналы, применяя вашу стратегию в виде матрицы умножения. Нет никакого «направления» к причинно-следственной связи (строго говоря) моделируемого принятия решений. По сути, вы должны рассчитывать на свои аналитические навыки и навыки написания кода, чтобы избежать предвзятого взгляда вперед. Преимущество здесь в том, что код намного проще, работает быстрее и просто более читабельный. С другой стороны, он обычно игнорирует множество торговых формальностей, таких как рынки без дробных акций, ограничивающие трейдера квантованным капиталом, а также денежные ограничения. Хороший векторизованный бэктест может учитывать интересы, а также многое другое, однако, на мой взгляд, это слишком хлопотно. Векторизованное тестирование на истории должно быть быстрым и простым инструментом для проверки стратегий, и точка.

2. Событийно-ориентированное тестирование на истории

Событийно-ориентированное тестирование на истории — это реальная сделка, она предназначена для того, чтобы быть как можно ближе к симуляции к реальной вещи. Это явно основано на причинно-следственной связи, трейдер может торговать только на основе информации до смоделированного времени. Думайте об этом как о Simulink, но для финансовых рынков, и вы часто являетесь как разработчиком, так и пользователем программного обеспечения. Определенно существует программное обеспечение для тестирования на основе событий, однако оно обычно стоит денег и используется в основном учреждениями. Я рекомендую написать свой собственный событийно-ориентированный бэктестер, настроенный под каждую стратегию, а не общий. Ваш событийно-ориентированный бэктест (в идеале) должен быть максимально идентичен реальному (реализации).

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

Один тест вперед и два теста назад

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

Дельта капитала — это позиция, 1 — длинная, -1 — короткая, 0 — нейтральная, а спред в момент времени t равен z_t. Обратите внимание, что этот псевдокод не является «полной» реализацией, поскольку он написан в виде бесконечного цикла, что, очевидно, не относится к бэктесту. Реализация python выглядит следующим образом:def pairs_signals(spread, threshold_enter, threshold_exit):
positions = pd.Series(data = np.zeros(len(spread)), index = spread.index)
position = 0
print(‘Beginning Backtest’)
for i in range(len(spread)):
print(f’Day {i}’)
if position == 0:
print(‘Checking for available positions’)
if bool(spread.iloc[i] < threshold_enter and spread.iloc[i-1] > threshold_enter) is True:
position = -1
print(‘Shorting’)
elif bool(spread.iloc[i] > -threshold_enter and spread.iloc[i-1] < -threshold_enter) is True:
position = 1
print(‘Longing’)
else:
pass
elif position == -1:
print(‘Checking for cover opportunities’)
if bool(spread.iloc[i] < threshold_exit and spread.iloc[i-1] > threshold_exit) is True:
position = 0
print(‘Covering Short Position’)
else:
pass
elif position == 1:
print(‘Checking for sell opportunities’)
if bool(spread.iloc[i] > -threshold_exit and spread.iloc[i-1] < -threshold_exit) is True:
position = 0
print(‘Selling Long Position’)
else:
pass
else:
continue

positions.iloc[i] = position

return positions

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

За каждую сделку взимается фиксированная комиссия в размере 4 египетских фунтов, а также 0,8% от стоимости каждой транзакции. Затем мы можем реализовать следующую процедуру для расчета чистой прибыли:

R_eff — эффективная процентная доходность
Постоянные кумулятивные затраты
Совокупная стоимость портфеля

Реализовано на Python:def backtest( returns, signals, commision_fixed = 4, commision_percentage = 0.008,initial_capital = 1000):
fixed_costs = signals.diff().fillna(0)**2*commision_fixed
returns = returns — commision_percentage*signals.diff().fillna(0)**2
strategy_returns = returns.shift(-1).fillna(0)*signals
value = strategy_returns.cumsum().apply(np.exp)
value /= value.iloc[0]
value *= initial_capital
value -= fixed_costs.cumsum()
value /= initial_capital

return np.log(value)

Там, где включение начального капитала является всего лишь артефактом векторизованного бэктеста, на самом деле вы можете просто покрыть длинную позицию, используя средства из короткой позиции. Это единственная векторизованная часть нашего бэктеста, все остальное управляется evemt, но упрощено. StatArb — это стратегия без стартового капитала. Теперь мы можем протестировать на обучающем наборе:

Небольшой дашборд для нашей стратегии торговли парами

За период чуть более двух лет мы достигаем совокупной логарифмической доходности около 1,5, что соответствует примерно exp (1,5) -1 = 3,46 или около того реальной прибыли. Тем не менее, это без каких-либо мер по управлению рисками, без коротких позиций или ограничения капитала. Мы можем реализовать условие, при котором любая позиция, удерживаемая дольше определенного времени, автоматически закрывается, если спред отклоняется от среднего, скажем, в два раза превышающего порог входа. Мы можем принять это время за значение 95-го процентиля периода полураспада.

def pairs_signals(spread, threshold_enter, threshold_exit):
positions = pd.Series(data = np.zeros(len(spread)), index = spread.index)
position = 0
max_hold_time = 7
print(‘Beginning Backtest’)
for i in range(len(spread)):
print(f’Day {i}’)
if position == 0:
holding_time = 0
print(‘Checking for available positions’)
if bool(spread.iloc[i] < threshold_enter and spread.iloc[i-1] > threshold_enter) is True:
position = -1
print(‘Shorting’)
elif bool(spread.iloc[i] > -threshold_enter and spread.iloc[i-1] < -threshold_enter) is True:
position = 1
print(‘Longing’)
else:
pass
elif position == -1:
holding_time += 1
print(f’Checking for cover opportunities, hold time: {holding_time}, spread: {spread.iloc[i]}’)
if bool(spread.iloc[i] < threshold_exit and spread.iloc[i-1] > threshold_exit) is True:
position = 0
print(‘Covering Short Position’)
elif bool(holding_time > max_hold_time and np.abs(spread.iloc[i]) > 2*threshold_enter) is True:
position = 0
print(‘Closing for risk management’)
else:
pass
elif position == 1:
holding_time += 1
print(f’Checking for sell opportunities, hold time: {holding_time}, spread: {spread.iloc[i]}’)
if bool(spread.iloc[i] > -threshold_exit and spread.iloc[i-1] < -threshold_exit) is True:
position = 0
print(‘Selling Long Position’)
elif bool(holding_time > max_hold_time and np.abs(spread.iloc[i]) > 2*threshold_enter) is True:
position = 0
print(‘Closing for risk management’)
else:
pass
else:
continue

positions.iloc[i] = position

return positions

Попробуйте это на нашем обучающем наборе:

Показывает небольшую разницу. Однако это может быть просто потому, что условия, которые мы устанавливаем, слишком редки в выборке. Тем не менее, это будет удобно всякий раз, когда спред перестает быть средним или когда изменяются параметры спреда. Максимальное время удержания может быть позже оценено как время первого прохождения 95-го процентиля, которое отличается от предыдущей оценки среднего времени удержания. Время удержания также может быть лучше рассчитано как время, проведенное за пределами определенного порога, учитывая, что спред примерно нормально распределен, мы можем сравнить ожидаемую продолжительность времени выше определенного процентиля с фактическим затраченным временем. Это дает нам более обоснованную предпосылку для принятия решения о том, что спред больше не может вернуться к среднему значению.

Теперь мы можем приступить к тестированию на проверочном наборе:

Это прекрасный пример печально известной коричневой и вонючей слизи, поразившей вентилятор эпическим столкновением, достойным бумаги CFD. Это также пример того, почему важны правила управления рисками; Глядя на график сигналов, мы видим, что наша последняя позиция была короткой, если бы мы не закрыли эту позицию, мы бы опустились примерно до exp(-2)-1 нашего богатства — мы были бы в тускло освещенном подвале с некоторыми сборщиками долгов! В какой-то глубокой, печально известной, коричневой и вонючей слизи.

Но почему? Почему распространение остановило коинтеграцию? Причин несколько:

  1. Если вы внимательно посмотрите на даты на оси X, спред перестает быть стационарным в середине 2019 года, то есть примерно тогда, когда COVID начал появляться и влиять на мировую экономику, это вызывает сдвиг в силах, которые двигают рынок.
  2. Выбранная нами пара тикеров находилась не в одном секторе, вполне вероятно, что коинтеграция, наблюдаемая в обучающей выборке, была не более чем небольшим совпадением.
  3. Что касается причин 1 и 2, COVID может вызвать некоторое «жонглирование» в синтетических секторах вселенной активов, то есть, если мы выберем произвольный набор характеристик для тикеров и сгруппируем их на основе этих характеристик, они не обязательно будут кластеризоваться по секторам.

Часть 4

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

Это заставляет нас задавать такие вопросы, как:

  1. Почему наши активы перестают интегрироваться?
  2. Является ли это постоянным изменением? Или спред вернется к равновесию?
  3. Можно ли скорректировать свою стратегию с учетом таких изменений?
  4. Как выбрать пары, которые, скорее всего, останутся совместно интегрированными вне выборки?

На все эти вопросы будут даны ответы как можно лучше в пределах знаний автора (меня). Я был бы рад, если бы читатели ответили на эту историю своими мыслями по этой теме. Без лишних слов:

Почему? Только почему?

Ну что тут скажешь? случается.

Ответ на этот вопрос заключается в том, ПОЧЕМУ эти отношения коинтеграции появились с самого начала? Это одна из двух причин:

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

Если это случай 2, то это означает, что мы просто были обмануты случайной комбинацией значительных событий, которые заставили цены этих двух акций двигаться вместе, и что взаимосвязь, которую мы обнаружили, не является статистически значимой (помните, что тест стационарности дал нам p < 0,05, но p < 0,01, поэтому мы не совсем уверены в этом.

Если случай 1, то мы на правильном пути: активы действительно были соинтегрированы, но почему?

  1. Ими движут одни и те же рыночные силы, одни и те же макроэкономические факторы
  2. Так называемые киты или инвесторы, обладающие достаточным капиталом, чтобы заметно сдвинуть цену, торговали портфелем, состоящим из нескольких акций, включая эти две
  3. Много других причин, таких как аналогичные основы, столько, сколько вы можете придумать

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

Они видят, как я катюсь

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

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

Мы можем опробовать его с окном в 30 дней:from statsmodels.regression.rolling import RollingOLS

model_rolling = RollingOLS(spread[chosen_pair[1]], sm.add_constant(spread[chosen_pair[0]]), window = 60, expanding = True)
res = model_rolling.fit(method= ‘lstsq’)

series = res.params.fillna(0)

predictions = spread[chosen_pair[0]]*series[chosen_pair[0]] + series[‘const’]

fig, ax = plt.subplots(2,1, figsize = (15,7), dpi = 400)

ax[0].plot(spread[chosen_pair[1]] -predictions, label = ‘Spread’)
ax[0].legend()
ax[1].plot(series[chosen_pair[0]], label = ‘Hedge Ratio’)
ax[1].legend()

Это включает в себя весь период обучения-проверки-тестирования, когда скользящая регрессия не помнит ничего за последние 30 дней назад. Сразу заметно, что коэффициент хеджирования теперь представляет собой временной ряд, с довольно резкими взлетами и падениями. Это довольно проблематично, так как в один прекрасный день вы открываете короткую позицию по акции, а на следующий день вам, возможно, придется открывать длинную позицию, и наоборот. С другой стороны, спред неподвижен и еще более узкий с более высокой силой возврата к среднему. Мы можем попытаться установить баланс между силой возврата к среднему и изменением коэффициента хеджирования, сформулировав задачу как задачу оптимизации с коэффициентом регуляризации, ограничивающим дисперсию коэффициента хеджирования, чтобы максимизировать силу возврата к среднему:from bayes_opt import BayesianOptimization

def rolling_model(window, set = train, tradeoff = 0.1):
model = RollingOLS(set[chosen_pair[1]], sm.add_constant(set[chosen_pair[0]]), window = int(window), expanding = True, )

res = model.fit(method= ‘lstsq’)
series = res.params.fillna(0)
predictions = set[chosen_pair[0]]*series[chosen_pair[0]]+ series[‘const’]

process = Ornstein_Uhlenbeck()
spread = set[chosen_pair[1]] — predictions

process.fit(spread, method = ‘Least-squares’)
llikelihood = process.log_likelihood(spread)

return process.mu — tradeoff*series[chosen_pair[0]].std()

optimizer = BayesianOptimization(
f=rolling_model,
pbounds={‘window’: (3, 500)},
verbose=1,
random_state=1,allow_duplicate_points = True

)

optimizer.set_gp_params(alpha=1)
optimizer.maximize(
n_iter=20
)

35-дневное окно

Хотя это не спред мечты, коэффициент хеджирования немного более хорош, за исключением резкого падения в первом полугодии 2019 года (начало COVID). Мы можем увеличить параметр компромисса до 0,5, чтобы увеличить штраф за большие изменения коэффициента хеджирования:

Окно длиной в 1 год

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

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

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

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

Фильтр Калмана

Да, антагонист каждого инженерного студента (по крайней мере, там, где я учился в университете), отчасти потому, что все ненавидели, насколько это сложно. Фильтр Калмана можно использовать в качестве рекурсивной оценки коэффициента хеджирования с настраиваемыми матрицами Q и R, которые взвешивают оценку для получения оптимальной оценки коэффициента хеджирования. Математические детали фильтра Калмана можно найти в книге «Взгляд на обработку сигналов в финансовой инженерии», а также в любой книге по системам обработки сигналов и управления.

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

Кривая коэффициента хеджирования плавная, но спред не совсем стационарный.

После некоторой настройки матрицы нам удается просто переключать спред между двумя состояниями возврата к среднему, сохраняя при этом плавную и стабильную кривую коэффициента хеджирования. Теперь мы можем торговать, просто создав оценщик для долгосрочного среднего, например, скользящую среднюю с окном, большим, чем период полураспада. Затем мы можем торговать на основе отклонения спреда от этого среднего значения, такая стратегия основана на так называемых полосах Боллинджера, чтобы протестировать эту динамическую пороговую стратегию, нам нужна оценка долгосрочного среднего значения и дисперсии (для каждого режима). Оценщик не должен знать, какие средние возможны априори (это означает, что он не может знать, что спред возвращается к 0 и 1,5).

(Наверх) Спред с экспоненциальным средним значением в качестве оценки, (Средняя) Разница между спредом и долгосрочным оценочным средним, (Нижнее) Логарифмический график собственного капитала

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

Катастрофическая производительность

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

Анализ стратегий парного трейдинга

MidJourney Sea of Stonk Charts

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

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

Это продолжение статьи о возврате к среднему значению.

Подготовка среды

Подготовьте среду jupyter и pip install следующие библиотеки:

  • numpy
  • pandas
  • yfinance

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

Подготовка среды

Подготовьте среду jupyter и pip установите следующие библиотеки:

  • numpy
  • pandas
  • yfinance

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

Дайте мне больше данных!

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

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

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

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

Последнее будет непросто, так как криптобратья торгуют весь день, каждый день! Давайте сделаем так:crypto_forex_stocks = [‘BTC-USD’, ‘ETH-USD’, ‘BNB-USD’, ‘XRP-USD’, ‘ADA-USD’, ‘DOGE-USD’, ‘ETC-USD’, ‘XLM-USD’, ‘AAVE-USD’, ‘EOS-USD’, ‘XTZ-USD’, ‘ALGO-USD’, ‘XMR-USD’, ‘KCS-USD’,
‘MKR-USD’, ‘BSV-USD’, ‘RUNE-USD’, ‘DASH-USD’, ‘KAVA-USD’, ‘ICX-USD’, ‘LINA-USD’, ‘WAXP-USD’, ‘LSK-USD’, ‘EWT-USD’, ‘XCN-USD’, ‘HIVE-USD’, ‘FTX-USD’, ‘RVN-USD’, ‘SXP-USD’, ‘BTCB-USD’]
bank_stocks = [‘JPM’, ‘BAC’, ‘WFC’, ‘C’, ‘GS’, ‘MS’, ‘DB’, ‘UBS’, ‘BBVA’, ‘SAN’, ‘ING’, ‘ BNPQY’, ‘HSBC’, ‘SMFG’, ‘PNC’, ‘USB’, ‘BK’, ‘STT’, ‘KEY’, ‘RF’, ‘HBAN’, ‘FITB’, ‘CFG’,
‘BLK’, ‘ALLY’, ‘MTB’, ‘NBHC’, ‘ZION’, ‘FFIN’, ‘FHN’, ‘UBSI’, ‘WAL’, ‘PACW’, ‘SBCF’, ‘TCBI’, ‘BOKF’, ‘PFG’, ‘GBCI’, ‘TFC’, ‘CFR’, ‘UMBF’, ‘SPFI’, ‘FULT’, ‘ONB’, ‘INDB’, ‘IBOC’, ‘HOMB’]
global_indexes = [‘^DJI’, ‘^IXIC’, ‘^GSPC’, ‘^FTSE’, ‘^N225’, ‘^HSI’, ‘^AXJO’, ‘^KS11’, ‘^BFX’, ‘^N100’,
‘^RUT’, ‘^VIX’, ‘^TNX’]

START_DATE = ‘2021-01-01’
END_DATE = ‘2023-10-31’
universe_tickers = crypto_forex_stocks + bank_stocks + global_indexes
universe_tickers_ts_map = {ticker: load_ticker_ts_df(
ticker, START_DATE, END_DATE) for ticker in universe_tickers}

def sanitize_data(data_map):
TS_DAYS_LENGTH = (pd.to_datetime(END_DATE) —
pd.to_datetime(START_DATE)).days
data_sanitized = {}
date_range = pd.date_range(start=START_DATE, end=END_DATE, freq=’D’)
for ticker, data in data_map.items():
if data is None or len(data) < (TS_DAYS_LENGTH / 2):
# We cannot handle shorter TSs
continue
if len(data) > TS_DAYS_LENGTH:
# Normalize to have the same length (TS_DAYS_LENGTH)
data = data[-TS_DAYS_LENGTH:]
# Reindex the time series to match the date range and fill in any blanks (Not Numbers)
data = data.reindex(date_range)
data[‘Adj Close’].replace([np.inf, -np.inf], np.nan, inplace=True)
data[‘Adj Close’].interpolate(method=’linear’, inplace=True)
data[‘Adj Close’].fillna(method=’pad’, inplace=True)
data[‘Adj Close’].fillna(method=’bfill’, inplace=True)
assert not np.any(np.isnan(data[‘Adj Close’])) and not np.any(
np.isinf(data[‘Adj Close’]))
data_sanitized[ticker] = data
return data_sanitized

# Sample some
uts_sanitized = sanitize_data(universe_tickers_ts_map)
uts_sanitized[‘JPM’].shape, uts_sanitized[‘BTC-USD’].shape

Обратите внимание на date_range = pd.date_range(start=START_DATE, end=END_DATE, freq='D') который установит нужное нам временное окно данных. Затем все это interpolate и fillna, чтобы покрыть эти NaN или None. Мы делаем все возможное, чтобы интерполировать линейно, или, если это не удается, просто заполняем задним числом последнее разумное значение.

Проверяем, что все так, как ожидалось, с assert и проверяем формы 2-х случайных инструментов, которые должны иметь одинаковые размеры.

Кто кого перемещает?

В качестве примера в этом анализе Late’s берут последний криптоскандал с FTX. Означает ли это, что с крахом этой биржи рынок будет больше доверять банкам до тех пор, пока скандал не будет забыт?

Мы будем соотносить и коинтегрировать, чтобы найти закономерности.

Соотнесение и коинтеграция

Корреляция количественно определяет взаимосвязь между двумя переменными с помощью коэффициента корреляции Пирсона (r). Он колеблется от -1 до 1, где:

Латекс в блокноте

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

Это оценивается с помощью статистических тестов, таких как расширенный тест Дики-Фуллера (ADF), который проверяет, является ли спред между двумя активами постоянным. Если спред стационарный, это говорит о том, что активы коинтегрированы и имеют долгосрочные отношения.

Латекс в блокноте

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

Поиск пар

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

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

Он проверит так называемую нулевую гипотезу (H0), которая в статистике означает, что мы не можем предполагать никакого эффекта или взаимосвязи. В целом, если p_value ниже 0,02, Н0 отклоняется, и паре что-то движется.from statsmodels.tsa.stattools import coint
from itertools import combinations
from statsmodels.tsa.stattools import coint

def find_cointegrated_pairs(tickers_ts_map, p_value_threshold=0.2):
«»»
Find cointegrated pairs of stocks based on the Augmented Dickey-Fuller (ADF) test.
Parameters:
— tickers_ts_map (dict): A dictionary where keys are stock tickers and values are time series data.
— p_value_threshold (float): The significance level for cointegration testing.
Returns:
— pvalue_matrix (numpy.ndarray): A matrix of cointegration p-values between stock pairs.
— pairs (list): A list of tuples representing cointegrated stock pairs and their p-values.
«»»
tickers = list(tickers_ts_map.keys())
n = len(tickers)
# Extract ‘Adj Close’ prices into a matrix (each column is a time series)
adj_close_data = np.column_stack(
[tickers_ts_map[ticker][‘Adj Close’].values for ticker in tickers])
pvalue_matrix = np.ones((n, n))
# Calculate cointegration p-values for unique pair combinations
for i, j in combinations(range(n), 2):
result = coint(adj_close_data[:, i], adj_close_data[:, j])
pvalue_matrix[i, j] = result[1]
pairs = [(tickers[i], tickers[j], pvalue_matrix[i, j])
for i, j in zip(*np.where(pvalue_matrix < p_value_threshold))]
return pvalue_matrix, pairs

# This section can take up to 5mins
P_VALUE_THRESHOLD = 0.02
pvalues, pairs = find_cointegrated_pairs(
uts_sanitized, p_value_threshold=P_VALUE_THRESHOLD)

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

Тепловая карта ниже даст нам карту того, кто с кем работает в паре — на основе обнаруженного p-значения.import seaborn as sns

plt.figure(figsize=(26, 26))
heatmap = sns.heatmap(pvalues, xticklabels=uts_sanitized.keys(),
yticklabels=uts_sanitized.keys(), cmap=’RdYlGn_r’,
mask=(pvalues > (P_VALUE_THRESHOLD)),
linecolor=’gray’, linewidths=0.5)
heatmap.set_xticklabels(heatmap.get_xticklabels(), size=14)
heatmap.set_yticklabels(heatmap.get_yticklabels(), size=14)
plt.show()

Тепловая карта Corelation/Cointegration

Это довольно много — и все эти криптовалюты лежат в постели друг с другом!

Выберем 3 с наибольшим соотношением. Приведенная ниже гистограмма помогает нам идентифицировать эти пары и их силу, чем меньше значение p, тем сильнее:sorted_pairs = sorted(pairs, key=lambda x: x[2], reverse=False)
sorted_pairs = sorted_pairs[0:35]
sorted_pairs_labels, pairs_p_values = zip(
*[(f'{y1} <-> {y2}’, p*1000) for y1, y2, p in sorted_pairs])plt.figure(figsize=(12, 18))
plt.barh(sorted_pairs_labels,
pairs_p_values, color=’red’)
plt.xlabel(‘P-Values (1000)’, fontsize=8)
plt.ylabel(‘Pairs’, fontsize=6)
plt.title(‘Cointegration P-Values (in 1000s)’, fontsize=20)plt.grid(axis=’both’, linestyle=’—‘, alpha=0.7)
plt.show()

P-значения наиболее коинтегрированных пар

У нас есть несколько разумных кандидатов:

  • AAVE-USD с Citigroup Inc ©
  • XMR-UD с Citigroup Inc ©
  • FTX-USD (о боже!) с Ally Financial Inc (ALLY)

Давайте посмотрим на их таймсери с помощью кода ниже. Учитывая, насколько изменчивы и малы эти криптоинструменты, мы решили масштабировать цены, чтобы иметь возможность лучше сравнивать их с парными акциями. Мы будем использовать MinMax из sktlearn, для преобразования цен закрытия.

Есть некоторое сглаживание с помощью скользящего окна, чтобы мы могли лучше видеть стационарность между парами:from sklearn.preprocessing import MinMaxScaler

ticker_pairs = [(«AAVE-USD», «C»), («XMR-USD», «C»), («FTX-USD», «ALLY»)]
fig, axs = plt.subplots(3, 1, figsize=(18, 14))
scaler = MinMaxScaler()
for i, (ticker1, ticker2) in enumerate(ticker_pairs):
# Scale the price data for each pair using MIN MAX
scaled_data1 = scaler.fit_transform(
uts_sanitized[ticker1][‘Adj Close’].values.reshape(-1, 1))
scaled_data2 = scaler.fit_transform(
uts_sanitized[ticker2][‘Adj Close’].values.reshape(-1, 1))
axs[i].plot(scaled_data1, label=f'{ticker1}’, color=’lightgray’, alpha=0.7)
axs[i].plot(scaled_data2, label=f'{ticker2}’, color=’lightgray’, alpha=0.7)
# Apply rolling mean with a window of 15
scaled_data1_smooth = pd.Series(scaled_data1.flatten()).rolling(
window=15, min_periods=1).mean()
scaled_data2_smooth = pd.Series(scaled_data2.flatten()).rolling(
window=15, min_periods=1).mean()
axs[i].plot(scaled_data1_smooth, label=f'{ticker1} SMA’, color=’red’)
axs[i].plot(scaled_data2_smooth, label=f'{ticker2} SMA’, color=’blue’)
axs[i].set_ylabel(‘*Scaled* Price $’, fontsize=12)
axs[i].set_title(f'{ticker1} vs {ticker2}’, fontsize=18)
axs[i].legend()
axs[i].set_xticks([])
plt.tight_layout()
plt.show()

Визуализация коинтеграции

AAVE-USD и C выглядят хорошо для нашего эксперимента, так как за пределами дислокации в начале ряда цены кажутся стационарными по отношению друг к другу.

Мы будем создавать торговые сигналы, используя Z-score и среднее значение. Со скользящим окном, поэтому нам не нужно разбиваться на обучающий и тестовый наборы. Z-оценка обозначается как:

Латекс в блокноте
  1. X — это цена, которую мы хотим стандартизировать.
  2. 2. μ – среднее значение скользящего окна.
  3. 3. σ – стандартное отклонение скользящего окна.

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

И наоборот, если Z-оценка падает ниже -1, это говорит о том, что недооцененный актив стал переоцененным, что побуждает продавать первый актив и покупать второй. Эта стратегия использует принципы возврата к среднему значению, чтобы извлечь выгоду из временных дивергенций и ожидания возврата к среднему значению.TRAIN = int(len(uts_sanitized[«AAVE-USD»]) * 0.85)
TEST = len(uts_sanitized[«AAVE-USD»]) — TRAIN

AAVE_ts = uts_sanitized[«AAVE-USD»][«Adj Close»][:TRAIN]
C_ts = uts_sanitized[«C»][«Adj Close»][:TRAIN]
# Calculate price ratio (AAVE-USD price / C price)
ratios = C_ts/AAVE_ts
fig, ax = plt.subplots(figsize=(12, 8))
ratios_mean = np.mean(ratios)
ratios_std = np.std(ratios)
ratios_zscore = (ratios — ratios_mean) / ratios_std
ax.plot(ratios.index, ratios_zscore, label=»Z-Score», color=’blue’)
# Plot reference lines
ax.axhline(1.0, color=»green», linestyle=’—‘, label=»Upper Threshold (1.0)»)
ax.axhline(-1.0, color=»red», linestyle=’—‘, label=»Lower Threshold (-1.0)»)
ax.axhline(0, color=»black», linestyle=’—‘, label=»Mean»)
ax.set_title(‘AAVE-USD / C: Price Ratio and Z-Score’, fontsize=18)
ax.set_xlabel(‘Date’)
ax.set_ylabel(‘Price Ratio / Z-Score’)
ax.legend()
plt.tight_layout()
plt.show()

Зеленая горизонтальная линия здесь будет сигнализировать о покупке для Citi при пересечении и продаже для Aave, красная линия сделает обратное. Этот график предназначен только для визуализации статонарии, при запуске нашего сигнала пороги будут перемещаться с скользящим окном, чтобы отразить изменения на рынке.

Применим сигнал:def signals_zscore_evolution(ticker1_ts, ticker2_ts, window_size=15, first_ticker=True):
«»»
Generate trading signals based on z-score analysis of the ratio between two time series.
Parameters:
— ticker1_ts (pandas.Series): Time series data for the first security.
— ticker2_ts (pandas.Series): Time series data for the second security.
— window_size (int): The window size for calculating z-scores and ratios’ statistics.
— first_ticker (bool): Set to True to use the first ticker as the primary signal source, and False to use the second.Returns:
— signals_df (pandas.DataFrame): A DataFrame with ‘signal’ and ‘orders’ columns containing buy (1) and sell (-1) signals.
«»»
ratios = ticker1_ts / ticker2_ts
ratios_mean = ratios.rolling(
window=window_size, min_periods=1, center=False).mean()
ratios_std = ratios.rolling(
window=window_size, min_periods=1, center=False).std()
z_scores = (ratios — ratios_mean) / ratios_std
buy = ratios.copy()
sell = ratios.copy()
if first_ticker:
# These are empty zones, where there should be no signal
# the rest is signalled by the ratio.
buy[z_scores > -1] = 0
sell[z_scores < 1] = 0
else:
buy[z_scores < 1] = 0
sell[z_scores > -1] = 0
signals_df = pd.DataFrame(index=ticker1_ts.index)
signals_df[‘signal’] = np.where(buy > 0, 1, np.where(sell < 0, -1, 0))
signals_df[‘orders’] = signals_df[‘signal’].diff()
signals_df.loc[signals_df[‘orders’] == 0, ‘orders’] = None
return signals_df

AAVE_ts = uts_sanitized[«AAVE-USD»][«Adj Close»]
C_ts = uts_sanitized[«C»][«Adj Close»]
plt.figure(figsize=(26, 18))
signals_df1 = signals_zscore_evolution(AAVE_ts, C_ts)
profit_df1 = calculate_profit(signals_df1, AAVE_ts)
ax1, _ = plot_strategy(AAVE_ts, signals_df1, profit_df1)
signals_df2 = signals_zscore_evolution(AAVE_ts, C_ts, first_ticker=False)
profit_df2 = calculate_profit(signals_df2, C_ts)
ax2, _ = plot_strategy(C_ts, signals_df2, profit_df2)
ax1.legend(loc=’upper left’, fontsize=10)
ax1.set_title(f’Citigroup Paired with Aave’, fontsize=18)
ax2.legend(loc=’upper left’, fontsize=10)
ax2.set_title(f’Aave Paired with Citigroup’, fontsize=18)
plt.tight_layout()
plt.show()

В системе алготрейдинга они будут работать вместе, поэтому доходность лучше всего представить в виде суммы.plt.figure(figsize=(12, 6))
cumulative_profit_combined = profit_df1 + profit_df2
ax2_combined = cumulative_profit_combined.plot(
label=’Profit%’, color=’green’)
plt.legend(loc=’upper left’, fontsize=10)
plt.title(f’Aave & Citigroup Paired — Cumulative Profit’, fontsize=18)
plt.tight_layout()
plt.show()

Это на удивление хорошо, за вычетом 50% просадки, эта стратегия вернула бумажную доходность в размере 100% (против 10% у SnP500 за 2 года).

Опять же, это происходило с высокой дисперсией из-за того, с каким криптоинструментом мы связывали Citi, это были кванты, которые измеряли стратегию с помощью показателей эффективности с поправкой на риск, таких как коэффициент Сортино.

Заключение

Чтобы закончить эту статью, мы узнали о стратегии парного трейдинга, и увидели, что она обладает следующими атрибутами:

  • Рыночно-нейтральные: Стратегии парной торговли нацелены на то, чтобы быть рыночно-нейтральными, что означает, что они стремятся получить прибыль от относительного движения цен между двумя активами, а не от общего направления рынка.
  • Статистическая основа: Стратегия опирается на статистические показатели, такие как Z-оценка и коинтеграция, обеспечивая количественную основу для принятия решений.
  • Возврат к среднему значению: Он использует преимущества возврата к среднему значению, используя тенденцию цен на активы возвращаться к своим историческим средним значениям. Ознакомьтесь с предыдущей статьей.

Хотя на самом деле у него были бы следующие проблемы:

  • Транзакционные издержки: В нашей симуляции мы сигнализировали о большом количестве сделок, которые могут привести к серьезным комиссионным издержкам и издержкам.
  • Риск нестационарности: Если корреляция активов или коинтеграция нарушаются, стратегия будет неэффективной и создаст риск.

В то время как проведение количественного анализа и кодирование для финансовой инженерии является сложной задачей, создание курсов трейдинга на YouTube не является сложной задачей, всегда будьте скептически настроены!

Продать лопаты в золотую лихорадку MidJourney 2023.11.02

Ссылки

Github

Статья здесь также доступна на Github

Блокнот Kaggle доступен здесь

Стратегия Citadel

Парный трейдинг — это сложная стратегия, часто используемая количественными трейдерами для торговли портфелем, а не сосредоточения исключительно на отдельных инструментах. В этой статье мы углубимся в анализ этой рыночно-нейтральной стратегии, которая направлена на извлечение выгоды из относительных движений двух тесно связанных финансовых инструментов. Мы проведем этот анализ с помощью моделирования с использованием данных Yahoo Finance, находящихся в свободном доступе.

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

Подготовка среды

Чтобы установить необходимые библиотеки в среде Jupyter, можно использовать следующие команды:!pip install numpy
!pip install pandas
!pip install yfinance

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

Дайте мне больше данных!

Для сбора и очистки данных для стратегии парной торговли с участием криптовалют мы будем использовать Yahoo Finance в качестве источника данных. Однако из-за волатильного характера криптовалют могут быть случаи, когда инструменты были исключены из листинга или демонстрируют экстремальные выбросы, что затрудняет корреляцию цен с традиционными акциями или индексами. Чтобы обеспечить целостность данных, мы выполним следующие шаги:

1. Интерполяция отсутствующих значений: Заполните все пропущенные точки данных во временных рядах, чтобы обеспечить непрерывность.
2. Сглаживание выбросов: используйте методы для сглаживания экстремальных точек данных, которые могут исказить анализ.
3. Убедитесь, что все временные ряды имеют одинаковое количество точек данных: Стандартизируйте данные временных рядов, чтобы они имели постоянную длину, несмотря на непрерывную торговую активность на рынке криптовалют.

Реализация этих мер поможет сохранить достоверность данных для нашей стратегии парного трейдинга. Приступим к реализации.crypto_forex_stocks = [‘BTC-USD’, ‘ETH-USD’, ‘BNB-USD’, ‘XRP-USD’, ‘ADA-USD’, ‘DOGE-USD’, ‘ETC-USD’, ‘XLM-USD’, ‘AAVE-USD’, ‘EOS-USD’, ‘XTZ-USD’, ‘ALGO-USD’, ‘XMR-USD’, ‘KCS-USD’,
‘MKR-USD’, ‘BSV-USD’, ‘RUNE-USD’, ‘DASH-USD’, ‘KAVA-USD’, ‘ICX-USD’, ‘LINA-USD’, ‘WAXP-USD’, ‘LSK-USD’, ‘EWT-USD’, ‘XCN-USD’, ‘HIVE-USD’, ‘FTX-USD’, ‘RVN-USD’, ‘SXP-USD’, ‘BTCB-USD’]
bank_stocks = [‘JPM’, ‘BAC’, ‘WFC’, ‘C’, ‘GS’, ‘MS’, ‘DB’, ‘UBS’, ‘BBVA’, ‘SAN’, ‘ING’, ‘ BNPQY’, ‘HSBC’, ‘SMFG’, ‘PNC’, ‘USB’, ‘BK’, ‘STT’, ‘KEY’, ‘RF’, ‘HBAN’, ‘FITB’, ‘CFG’,
‘BLK’, ‘ALLY’, ‘MTB’, ‘NBHC’, ‘ZION’, ‘FFIN’, ‘FHN’, ‘UBSI’, ‘WAL’, ‘PACW’, ‘SBCF’, ‘TCBI’, ‘BOKF’, ‘PFG’, ‘GBCI’, ‘TFC’, ‘CFR’, ‘UMBF’, ‘SPFI’, ‘FULT’, ‘ONB’, ‘INDB’, ‘IBOC’, ‘HOMB’]
global_indexes = [‘^DJI’, ‘^IXIC’, ‘^GSPC’, ‘^FTSE’, ‘^N225’, ‘^HSI’, ‘^AXJO’, ‘^KS11’, ‘^BFX’, ‘^N100’,
‘^RUT’, ‘^VIX’, ‘^TNX’]

START_DATE = ‘2021-01-01’
END_DATE = ‘2023-10-31’
universe_tickers = crypto_forex_stocks + bank_stocks + global_indexes
universe_tickers_ts_map = {ticker: load_ticker_ts_df(
ticker, START_DATE, END_DATE) for ticker in universe_tickers}

def sanitize_data(data_map):
TS_DAYS_LENGTH = (pd.to_datetime(END_DATE) —
pd.to_datetime(START_DATE)).days
data_sanitized = {}
date_range = pd.date_range(start=START_DATE, end=END_DATE, freq=’D’)
for ticker, data in data_map.items():
if data is None or len(data) < (TS_DAYS_LENGTH / 2):
# We cannot handle shorter TSs
continue
if len(data) > TS_DAYS_LENGTH:
# Normalize to have the same length (TS_DAYS_LENGTH)
data = data[-TS_DAYS_LENGTH:]
# Reindex the time series to match the date range and fill in any blanks (Not Numbers)
data = data.reindex(date_range)
data[‘Adj Close’].replace([np.inf, -np.inf], np.nan, inplace=True)
data[‘Adj Close’].interpolate(method=’linear’, inplace=True)
data[‘Adj Close’].fillna(method=’pad’, inplace=True)
data[‘Adj Close’].fillna(method=’bfill’, inplace=True)
assert not np.any(np.isnan(data[‘Adj Close’])) and not np.any(
np.isinf(data[‘Adj Close’]))
data_sanitized[ticker] = data
return data_sanitized

# Sample some
uts_sanitized = sanitize_data(universe_tickers_ts_map)
uts_sanitized[‘JPM’].shape, uts_sanitized[‘BTC-USD’].shape

Переменная ‘date_range’ определяется с помощью ‘pd.date_range(start=START_DATE, end=END_DATE, freq=’D’)’, устанавливая желаемое временное окно для данных. Затем мы переходим к интерполяции и заполнению любых отсутствующих значений (NaN или None) с помощью линейной интерполяции или, если это не удалось, путем заполнения задним числом последним допустимым значением.

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

Погружение глубже

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

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

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

Корреляция и коинтеграция

Корреляция количественно определяет связь между двумя переменными с помощью коэффициента корреляции Пирсона (r), который находится в диапазоне от -1 до 1. Значение -1 указывает на идеальную отрицательную линейную зависимость, 0 — на отсутствие линейной зависимости, а 1 — на идеальную положительную линейную зависимость.

Коинтеграция, с другой стороны, оценивает, связаны ли два актива друг с другом с течением времени, подразумевая, что их ценовые спреды имеют тенденцию возвращаться к среднему значению. Это означает, что есть возможности для торговли, когда они временно отклоняются от своей исторической взаимосвязи. Коинтеграция оценивается с помощью статистических тестов, таких как расширенный тест Дики-Фуллера (ADF), который проверяет, является ли спред между двумя активами постоянным. Если спред стационарный, это говорит о том, что активы коинтегрированы и имеют долгосрочные отношения.

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

Поиск пар

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

Следующий код протестирует вселенную акций и других финансовых инструментов для выявления любых скрытых связей. Он проверит так называемую нулевую гипотезу (H0), которая не предполагает никакого влияния или связи между активами. Как правило, если p-значение, полученное в результате теста, ниже 0,02, нулевая гипотеза (H0) отклоняется, указывая на то, что пара демонстрирует некоторую степень коинтеграции или отношения, заслуживающие дальнейшего изучения.from statsmodels.tsa.stattools import coint
from itertools import combinations
from statsmodels.tsa.stattools import coint

def find_cointegrated_pairs(tickers_ts_map, p_value_threshold=0.2):
«»»
Find cointegrated pairs of stocks based on the Augmented Dickey-Fuller (ADF) test.
Parameters:
— tickers_ts_map (dict): A dictionary where keys are stock tickers and values are time series data.
— p_value_threshold (float): The significance level for cointegration testing.
Returns:
— pvalue_matrix (numpy.ndarray): A matrix of cointegration p-values between stock pairs.
— pairs (list): A list of tuples representing cointegrated stock pairs and their p-values.
«»»
tickers = list(tickers_ts_map.keys())
n = len(tickers)
# Extract ‘Adj Close’ prices into a matrix (each column is a time series)
adj_close_data = np.column_stack(
[tickers_ts_map[ticker][‘Adj Close’].values for ticker in tickers])
pvalue_matrix = np.ones((n, n))
# Calculate cointegration p-values for unique pair combinations
for i, j in combinations(range(n), 2):
result = coint(adj_close_data[:, i], adj_close_data[:, j])
pvalue_matrix[i, j] = result[1]
pairs = [(tickers[i], tickers[j], pvalue_matrix[i, j])
for i, j in zip(*np.where(pvalue_matrix < p_value_threshold))]
return pvalue_matrix, pairs

# This section can take up to 5mins
P_VALUE_THRESHOLD = 0.02
pvalues, pairs = find_cointegrated_pairs(
uts_sanitized, p_value_threshold=P_VALUE_THRESHOLD)

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

Приведенная ниже тепловая карта предоставит нам визуальную карту того, какие активы связаны друг с другом, на основе p-значений, обнаруженных в ходе тестирования коинтеграции. Эта тепловая карта поможет нам определить потенциальные пары со значительными взаимосвязями, что позволит провести дальнейший анализ и потенциальные торговые возможности.import seaborn as sns

plt.figure(figsize=(26, 26))
heatmap = sns.heatmap(pvalues, xticklabels=uts_sanitized.keys(),
yticklabels=uts_sanitized.keys(), cmap=’RdYlGn_r’,
mask=(pvalues > (P_VALUE_THRESHOLD)),
linecolor=’gray’, linewidths=0.5)
heatmap.set_xticklabels(heatmap.get_xticklabels(), size=14)
heatmap.set_yticklabels(heatmap.get_yticklabels(), size=14)
plt.show()

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

Чтобы упростить наш анализ, давайте выберем три пары с наибольшей прочностью отношений. Мы будем использовать линейчатую диаграмму для визуализации этих пар и соответствующих им p-значений. Более низкое p-значение указывает на более сильную взаимосвязь между парами, подчеркивая потенциальные торговые возможности.sorted_pairs = sorted(pairs, key=lambda x: x[2], reverse=False)
sorted_pairs = sorted_pairs[0:35]
sorted_pairs_labels, pairs_p_values = zip(
*[(f'{y1} <-> {y2}’, p*1000) for y1, y2, p in sorted_pairs])
plt.figure(figsize=(12, 18))
plt.barh(sorted_pairs_labels,
pairs_p_values, color=’red’)
plt.xlabel(‘P-Values (1000)’, fontsize=8)
plt.ylabel(‘Pairs’, fontsize=6)
plt.title(‘Cointegration P-Values (in 1000s)’, fontsize=20)plt.grid(axis=’both’, linestyle=’—‘, alpha=0.7)
plt.show()

Мы выделили несколько перспективных кандидатов для парного трейдинга:

1. AAVE-USD с Citigroup Inc ©
2. XMR-USD с Citigroup Inc ©
3. FTX-USD с Ally Financial Inc (ALLY)

Визуализируем данные временных рядов для этих пар. Поскольку криптовалюты, как правило, демонстрируют высокую волатильность и меньшие ценовые диапазоны по сравнению с акциями, мы будем масштабировать цены, чтобы облегчить сравнение с парными акциями. Мы будем использовать масштабирование MinMax из scikit-learn для преобразования цен закрытия. Кроме того, мы применим сглаживание с помощью скользящего окна, чтобы улучшить видимость стационарности между парами.from sklearn.preprocessing import MinMaxScaler

ticker_pairs = [(«AAVE-USD», «C»), («XMR-USD», «C»), («FTX-USD», «ALLY»)]
fig, axs = plt.subplots(3, 1, figsize=(18, 14))
scaler = MinMaxScaler()
for i, (ticker1, ticker2) in enumerate(ticker_pairs):
# Scale the price data for each pair using MIN MAX
scaled_data1 = scaler.fit_transform(
uts_sanitized[ticker1][‘Adj Close’].values.reshape(-1, 1))
scaled_data2 = scaler.fit_transform(
uts_sanitized[ticker2][‘Adj Close’].values.reshape(-1, 1))
axs[i].plot(scaled_data1, label=f'{ticker1}’, color=’lightgray’, alpha=0.7)
axs[i].plot(scaled_data2, label=f'{ticker2}’, color=’lightgray’, alpha=0.7)
# Apply rolling mean with a window of 15
scaled_data1_smooth = pd.Series(scaled_data1.flatten()).rolling(
window=15, min_periods=1).mean()
scaled_data2_smooth = pd.Series(scaled_data2.flatten()).rolling(
window=15, min_periods=1).mean()
axs[i].plot(scaled_data1_smooth, label=f'{ticker1} SMA’, color=’red’)
axs[i].plot(scaled_data2_smooth, label=f'{ticker2} SMA’, color=’blue’)
axs[i].set_ylabel(‘*Scaled* Price $’, fontsize=12)
axs[i].set_title(f'{ticker1} vs {ticker2}’, fontsize=18)
axs[i].legend()
axs[i].set_xticks([])
plt.tight_layout()
plt.show()

Похоже, что AAVE-USD и Citigroup Inc © представляют собой многообещающую возможность для нашего эксперимента, поскольку их цены кажутся стационарными по отношению друг к другу, несмотря на некоторую дислокацию в начале ряда.

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

Z-оценка измеряет стандартизацию ценового ряда относительно его исторического среднего значения, используя подход скользящего окна. Он рассчитывается следующим образом:

Где:
-X — цена, которую мы хотим стандартизировать.
-u — среднее значение скользящего окна.
-sigma — стандартное отклонение скользящего окна.

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

И наоборот, если Z-оценка падает ниже -1, это говорит о том, что недооцененный актив стал переоцененным, что побуждает продавать первый актив и покупать второй. Эта стратегия использует принципы возврата к среднему значению, чтобы извлечь выгоду из временных дивергенций и ожидания возврата к среднему значению.TRAIN = int(len(uts_sanitized[«AAVE-USD»]) * 0.85)
TEST = len(uts_sanitized[«AAVE-USD»]) — TRAIN

AAVE_ts = uts_sanitized[«AAVE-USD»][«Adj Close»][:TRAIN]
C_ts = uts_sanitized[«C»][«Adj Close»][:TRAIN]
# Calculate price ratio (AAVE-USD price / C price)
ratios = C_ts/AAVE_ts
fig, ax = plt.subplots(figsize=(12, 8))
ratios_mean = np.mean(ratios)
ratios_std = np.std(ratios)
ratios_zscore = (ratios — ratios_mean) / ratios_std
ax.plot(ratios.index, ratios_zscore, label=»Z-Score», color=’blue’)
# Plot reference lines
ax.axhline(1.0, color=»green», linestyle=’—‘, label=»Upper Threshold (1.0)»)
ax.axhline(-1.0, color=»red», linestyle=’—‘, label=»Lower Threshold (-1.0)»)
ax.axhline(0, color=»black», linestyle=’—‘, label=»Mean»)
ax.set_title(‘AAVE-USD / C: Price Ratio and Z-Score’, fontsize=18)
ax.set_xlabel(‘Date’)
ax.set_ylabel(‘Price Ratio / Z-Score’)
ax.legend()
plt.tight_layout()
plt.show()

На визуализации ниже зеленая горизонтальная линия представляет собой сигнал на покупку для Citigroup Inc © в случае пересечения и сигнал на продажу для Aave (AAVE), в то время как красная линия указывает на обратное. Однако обратите внимание, что эта диаграмма предназначена в первую очередь для визуализации стационарности. При применении нашего сигнала пороговые значения будут перемещаться с скользящим окном, отражая изменения в динамике рынка.

Приступим к применению торгового сигнала:def signals_zscore_evolution(ticker1_ts, ticker2_ts, window_size=15, first_ticker=True):
«»»
Generate trading signals based on z-score analysis of the ratio between two time series.
Parameters:
— ticker1_ts (pandas.Series): Time series data for the first security.
— ticker2_ts (pandas.Series): Time series data for the second security.
— window_size (int): The window size for calculating z-scores and ratios’ statistics.
— first_ticker (bool): Set to True to use the first ticker as the primary signal source, and False to use the second.Returns:
— signals_df (pandas.DataFrame): A DataFrame with ‘signal’ and ‘orders’ columns containing buy (1) and sell (-1) signals.
«»»
ratios = ticker1_ts / ticker2_ts
ratios_mean = ratios.rolling(
window=window_size, min_periods=1, center=False).mean()
ratios_std = ratios.rolling(
window=window_size, min_periods=1, center=False).std()
z_scores = (ratios — ratios_mean) / ratios_std
buy = ratios.copy()
sell = ratios.copy()
if first_ticker:
# These are empty zones, where there should be no signal
# the rest is signalled by the ratio.
buy[z_scores > -1] = 0
sell[z_scores < 1] = 0
else:
buy[z_scores < 1] = 0
sell[z_scores > -1] = 0
signals_df = pd.DataFrame(index=ticker1_ts.index)
signals_df[‘signal’] = np.where(buy > 0, 1, np.where(sell < 0, -1, 0))
signals_df[‘orders’] = signals_df[‘signal’].diff()
signals_df.loc[signals_df[‘orders’] == 0, ‘orders’] = None
return signals_df

AAVE_ts = uts_sanitized[«AAVE-USD»][«Adj Close»]
C_ts = uts_sanitized[«C»][«Adj Close»]
plt.figure(figsize=(26, 18))
signals_df1 = signals_zscore_evolution(AAVE_ts, C_ts)
profit_df1 = calculate_profit(signals_df1, AAVE_ts)
ax1, _ = plot_strategy(AAVE_ts, signals_df1, profit_df1)
signals_df2 = signals_zscore_evolution(AAVE_ts, C_ts, first_ticker=False)
profit_df2 = calculate_profit(signals_df2, C_ts)
ax2, _ = plot_strategy(C_ts, signals_df2, profit_df2)
ax1.legend(loc=’upper left’, fontsize=10)
ax1.set_title(f’Citigroup Paired with Aave’, fontsize=18)
ax2.legend(loc=’upper left’, fontsize=10)
ax2.set_title(f’Aave Paired with Citigroup’, fontsize=18)
plt.tight_layout()
plt.show()

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

Давайте рассчитаем доходность соответствующим образом.plt.figure(figsize=(12, 6))
cumulative_profit_combined = profit_df1 + profit_df2
ax2_combined = cumulative_profit_combined.plot(
label=’Profit%’, color=’green’)
plt.legend(loc=’upper left’, fontsize=10)
plt.title(f’Aave & Citigroup Paired — Cumulative Profit’, fontsize=18)
plt.tight_layout()
plt.show()

Эффективность этой стратегии торговли по парам на удивление хороша, достигая бумажной доходности в размере 100% за анализируемый период, несмотря на просадку в 50%. Это превзошло доходность S&P 500 за 2 года в 10%. Однако стоит отметить, что стратегия продемонстрировала высокую дисперсию, вероятно, из-за волатильного характера криптовалютных инструментов в паре с Citigroup Inc ©.

На практике кванты оценивают эффективность стратегии с помощью индикаторов эффективности с поправкой на риск, таких как коэффициент Сортино, который учитывает риск снижения.

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

Источник

Источник

Источник

Источник

Источник

Источник