Введение

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

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

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

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

В этом анализе мы:

  1. собирать бизнес-профили по акциям из AlphaWave Data Stock Analysis API с помощью конечной точки Описание компании.
  2. использовать scikit-learn функции обработки естественного языка CountVectorizer и TfidfTransformer, чтобы «читать» эти описания и извлекать важные и новые концептуальные особенности во всех компаниях.
  3. объедините акции с DBSCAN, чтобы найти акции с похожими профилями.
  4. визуализируйте особенности нескольких акций с помощью WordCloud, чтобы получить некоторую интуицию в отношении того, что изучает модель машинного обучения.
  5. изучите временные ряды обнаруженных кластеров, чтобы увидеть, связаны ли этот процесс, не имеющий вообще входных данных о ценах на акции.
  6. протестируйте пару на истории, чтобы определить, насколько хорошо наши идентифицированные пары торгуют алгоритмически.

Блокноты Jupyter доступны в Google Colab и Github.

В этом проекте мы используем несколько технологий научных вычислений на основе Python, перечисленных ниже.

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import requests
import time
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.cluster import DBSCAN
from wordcloud import WordCloud
import pymc3 as pm
import theano as th
import seaborn as sns

1. Определите стандартную вселенную.

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

# Scrape the S&P 500 tickers from Wikipedia
def get_tickers():
    wiki_page = requests.get('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies').text
    sp_data = pd.read_html(wiki_page)
    ticker_df = sp_data[0]
    ticker_options = ticker_df['Symbol']
    return ticker_options
# Run the ticker scrape function
# Let's convert the get_tickers() output to a list and 
# replace tickers that have '.' with '-' so we can use AlphaWave Data APIs
stock_tickers = get_tickers()
stock_tickers = stock_tickers.to_list()
for ticker in range(len(stock_tickers)):
    stock_tickers[ticker] = stock_tickers[ticker].upper().replace(".", "-")
print (len(stock_tickers))
# stock_tickers

2. Соберите бизнес-профили

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

Чтобы вызвать этот API с помощью Python, вы можете выбрать один из поддерживаемых фрагментов кода Python, представленных в консоли API. Ниже приведен пример вызова API с помощью запросов Python. Вам нужно будет вставить свои собственные x-rapidapi-host и x-rapidapi-key в блок кода ниже.

# Fetch company profile descriptions
url = "https://stock-analysis.p.rapidapi.com/api/v1/resources/profile"
headers = {
    'x-rapidapi-key': "YOUR_X-RAPIDAPI-KEY_WILL_COPY_DIRECTLY_FROM_RAPIDAPI_PYTHON_CODE_SNIPPETS",
    'x-rapidapi-host': "YOUR_X-RAPIDAPI-HOST_WILL_COPY_DIRECTLY_FROM_RAPIDAPI_PYTHON_CODE_SNIPPETS"
    }
description_data = []
for ticker in stock_tickers:
    
    querystring = {"ticker":ticker}
        
    try:
        key_stats_response = requests.request("GET", url, headers=headers, params=querystring)
        df = pd.DataFrame({'profile': key_stats_response.text},
                          index=[ticker])
        description_data.append(df)
    except:
        pass
profiles_df = pd.concat(description_data, ignore_index=False)
profiles_df

# Remove profiles with missing text
missing_text = '""'
profiles_df = profiles_df.loc[~(profiles_df['profile'] == missing_text)]
print ("We have %d stocks in the universe with profiles." % len(profiles_df))

3. Создайте конвейер машинного обучения для чтения профилей.

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

Мы будем использовать scikit-learn классы: CountVectorizer и TfidfTransformer для чтения профилей. Это так называемый подход мешок слов:

  • Обозначьте текст: разбейте текст на счетные сегменты, называемые нграммами.
  • Подсчитайте все вхождения жетонов.
  • Нормализовать счет.

3.a Векторизатор подсчета

Мы передаем CountVectorizer блок текста и получаем матрицу подсчета слов с отдельными словами по оси столбца и каждым документом по оси строки. На самом деле он не считает слова. Вместо этого он считает граммов. ngram - это фрагмент текста. Для этого анализа мы используем полные слова, а также биграммы и триграммы (которые представляют собой последовательности из двух и трех слов соответственно).

Давайте посмотрим, что CountVectorizer делает с двумя «документами» для извлечения слов и биграмм:

text = [
    "AlphaWave Data creates APIs that supply financial information which lowers the barriers for talented people everywhere to write investment algorithms.",
    "Some algorithms will be highlighted in blog posts."
]
vectorizer = CountVectorizer(
    analyzer='word',    # a single ngram is a "word": characters seperated by spaces
    ngram_range=(1,2)   # we care about ngrams with min length 1 and max length 2
)
# Transform the text
count_mat = vectorizer.fit_transform(text)
# Let's see the counts and all the ngrams across each document
list(zip(count_mat.toarray()[0], count_mat.toarray()[1], vectorizer.get_feature_names()))

Обычно нас не интересуют «общие» слова, такие как «the», «a», «and», «on» и т. Д. Эти слова не будут указывать на что-либо новое в корпусе. Термин для этих слов в НЛП - стоп-слова. scikit-learn содержит набор встроенных стоп-слов, которые мы можем использовать.

vectorizer = CountVectorizer(
    analyzer='word',    # a single ngram is a "word": characters separated by spaces
    ngram_range=(1,2),  # we care about ngrams with min lentgh 1 and max length 2
    stop_words='english'
)
# Transform the text
count_mat = vectorizer.fit_transform(text)
# Let's see the counts and all the ngrams across each document
list(zip(count_mat.toarray()[0], count_mat.toarray()[1], vectorizer.get_feature_names()))

Так выглядит лучше. Теперь мы извлекли значимые в корпусе слова и биграммы. Единственный токен, который встречается в этом корпусе более одного раза, - это «алгоритмы». Алгоритмы должны быть важны для данных AlphaWave.

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

# extract stop words so we can append our own
vect = CountVectorizer(stop_words='english')
stop_words = list(vect.get_stop_words())
# I am adding stop words which I do not expect to uniquely help determine stock similarity
# there are probably more to add
stop_words.extend(['founded', 'firm', 'company', 'llc', 'inc', 'incorporated'])
vect = CountVectorizer(
    analyzer='word',
    ngram_range=(1,3),
    strip_accents='unicode',
    max_features=3000,       # we limit the generation of tokens to the top 3000
    stop_words=stop_words
)
X = vect.fit_transform(profiles_df['profile'])
X

# let's see some features extracted
vect.get_feature_names()[1000:1030]

3.b Нормализация с TF-IDF

Сама по себе частота подсчета вряд ли будет достаточной для поиска пар. Почему? При классификации документов слова, которые часто встречаются во всех документах, вероятно, не имеют смысла. Например, в нашем случае слово «головной офис», вероятно, встречается часто. Посмотрим.

test_word = 'headquartered'
occurences = sum(X[:, vect.get_feature_names().index(test_word)].toarray().flatten()>0)
total = X.shape[0]
print ('%d of %d profiles contain the token "%s"!' % (occurences, total, test_word))

plt.figure(figsize=(16, 8))
plt.hist(X[:, vect.get_feature_names().index(test_word)].todense())

Слово «головной офис» встречается во многих профилях. Ясно, что это не лучшее слово для противостояния. Введите TF-IDF, что означает «Срок-частота, умноженная на обратную частоту документа».

Используя TfidTransformer, частота подсчета отдельных токенов на документ ("Частота термина") умножается на весовой коэффициент.

где 𝑛 - общее количество документов, а 𝑑𝑓 (𝑡) - количество документов, содержащих термин 𝑡.

np.log(1+total)/(1+occurences)+1

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

plt.figure(figsize=(16, 8))
plt.plot((np.log(1+total)/(1+np.linspace(1,50))) + 1)
plt.xlabel("The number of documents containing term t")
plt.ylabel("Each term t in each document gets multipled by this")

Это не следует путать с масштабированием на основе количества в документах. Эта функция контролируется флагом sublinear_tf. Если этот флаг равен true, то каждый счетчик в документе масштабируется на 1 + 𝑙𝑜𝑔 (𝑐), где 𝑐c - счетчик в документе. Например, если в одном профиле сказано: «… Энергетическая компания… энергетические рынки… спрос на энергию», количество «энергии» в этом профиле будет увеличено. В данном случае мы не используем это масштабирование.

Можно спросить, если это слово бессмысленно, почему бы просто не добавить его в стоп-слова? Мы не хотим этого делать, потому что, хотя слово «штаб-квартира» само по себе не имеет смысла, возможно фраза «штаб-квартира в Нью-Йорке» имеет смысл.

test_word = 'headquartered new york'  # "in" is a stop word, so we ignore it
occurences = sum(X[:, vect.get_feature_names().index(test_word)].toarray().ravel()>0)
total = X.shape[0]
print ('%d of %d profiles contain the token "%s"!' % (occurences, total, test_word))

# transform the count matrix
tfidf = TfidfTransformer()
X_idf = tfidf.fit_transform(X)
X_idf

4. Кластерные запасы на основе матрицы TF-IDF

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

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

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

clf = DBSCAN(eps=1.05, min_samples=2)
labels = clf.fit_predict(X_idf)
n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)
print ("\nTotal custers discovered: %d" % n_clusters_)
clustered = labels

clustered_series = pd.Series(index=profiles_df.index, data=clustered.flatten())
clustered_series = clustered_series[clustered_series != -1]
plt.figure(figsize=(16, 8))
plt.barh(
    range(len(clustered_series.value_counts())),
    clustered_series.value_counts(),
    alpha=0.625
)
plt.title('Cluster Member Counts')
plt.xlabel('Stocks in Cluster');

pair_clusters = clustered_series.value_counts()[clustered_series.value_counts()<3].index.values
print (pair_clusters)
print ("\nTotal pair clusters discovered: %d" % len(pair_clusters))

Давайте посмотрим на описания профиля пары, которую мы обнаружили.

cluster = clustered_series['CDNS']
profiles_df.iloc[clustered==cluster]

print (profiles_df.iloc[clustered==cluster].iloc[0,0])

print (profiles_df.iloc[clustered==cluster].iloc[1,0])

5. Визуализируйте, чему научилась модель машинного обучения.

Мы можем использовать конечную точку Исторические дневные цены за 2 года из API цен на акции AlphaWave, чтобы получить исторические цены за два года.

Чтобы вызвать этот API с помощью Python, вы можете выбрать один из поддерживаемых фрагментов кода Python, представленных в консоли API. Ниже приведен пример вызова API с помощью запросов Python. Вам нужно будет вставить свои собственные x-rapidapi-host и x-rapidapi-key в блок кода ниже.

#fetch 2 year daily return data
url = "https://stock-prices2.p.rapidapi.com/api/v1/resources/stock-prices/2y"
headers = {
    'x-rapidapi-host': "YOUR_X-RAPIDAPI-HOST_WILL_COPY_DIRECTLY_FROM_RAPIDAPI_PYTHON_CODE_SNIPPETS",
    'x-rapidapi-key': "YOUR_X-RAPIDAPI-KEY_WILL_COPY_DIRECTLY_FROM_RAPIDAPI_PYTHON_CODE_SNIPPETS"
    }
stock_frames = []
for ticker in stock_tickers:
    querystring = {"ticker":ticker}
    stock_daily_price_response = requests.request("GET", url, headers=headers, params=querystring)
# Create Stock Prices DataFrame
    stock_daily_price_df = pd.DataFrame.from_dict(stock_daily_price_response.json())
    stock_daily_price_df = stock_daily_price_df.transpose()
    stock_daily_price_df = stock_daily_price_df.rename(columns={'Close':ticker})
    stock_daily_price_df = stock_daily_price_df[{ticker}]
    stock_frames.append(stock_daily_price_df)
combined_stock_price_df = pd.concat(stock_frames, axis=1, sort=True)
combined_stock_price_df = combined_stock_price_df.dropna(how='all')
combined_stock_price_df = combined_stock_price_df.fillna("")
combined_stock_price_df

symbols=list(profiles_df[clustered==cluster].index)
combined_stock_price_df[symbols]

def plot_cluster(which_cluster, plot_mean=False):
    
    symbols=list(profiles_df[clustered==which_cluster].index)
    
    pricing = combined_stock_price_df[symbols]
    
    means = np.log(pricing).mean()
    data = np.log(pricing).sub(means)
    
    if plot_mean:
        means = data.mean(axis=1).rolling(window=21).mean().shift(1)
        data.sub(means, axis=0).plot()
        plt.axhline(0, lw=3, ls='--', label='mean', color='k')
        plt.legend(loc=0)
    else:
        data.plot()
        
    fig = plt.figure(1)
    fig.set_figheight(8)
    fig.set_figwidth(16)
plot_cluster(cluster)

Это просто потрясающе! Просто посмотрев текстовое описание компаний:

  1. модель ML смогла найти связанные компании.
  2. у связанных компаний действительно есть связанные ценовые ряды.

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

def visualize_cluster(target_cluster):
    
    num_stocks = len(list(profiles_df[clustered==cluster].index)[:])
    num_cols = 2
    num_rows = num_stocks // num_cols 
    num_rows += num_stocks % num_cols
    position = range(1,num_stocks + 1)
fig = plt.figure(1)
    fig.set_figheight(10)
    fig.set_figwidth(16)
for index in range(len(list(profiles_df[clustered==cluster].index)[:])):
        
        wordcloud_ticker = list(profiles_df[clustered==cluster].index)[index]
wordcloud_profile = [profiles_df.iloc[clustered==cluster]['profile'][index]]
        wordcloud_X = vect.fit_transform(wordcloud_profile)
        wordcloud_word_features = list(zip(wordcloud_X.toarray()[0], vect.get_feature_names()))
wordcloud_frequencies = []
        for word in wordcloud_word_features[:]:
            if word[0] > 0:
                new_word = (word[1],word[0])
                wordcloud_frequencies.append(new_word)
wordcloud = WordCloud(background_color="white",max_font_size=48).generate_from_frequencies(dict(wordcloud_frequencies))
ax = fig.add_subplot(num_rows, num_cols, position[index])
plt.tight_layout(h_pad=5, w_pad=5)
        plt.imshow(wordcloud, interpolation="bilinear")
        plt.title(wordcloud_ticker)
        plt.axis("off")
plt.show()
visualize_cluster(cluster)

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

5.a Четыре других примера

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

# Let's look at the clusters generated
clustered_series_df = clustered_series.to_frame()
clustered_series_df.columns =['cluster']
clustered_series_df['labels'] = clustered_series_df.groupby(by=['cluster']).cumcount()
clustered_series_df.reset_index(inplace=True)
clustered_series_df.groupby('cluster')['index'].apply(list)

cluster = clustered_series['EXR']
plot_cluster(cluster)

visualize_cluster(cluster)

cluster = clustered_series['APD']
plot_cluster(cluster)

visualize_cluster(cluster)

cluster = clustered_series['HCA']
plot_cluster(cluster)

visualize_cluster(cluster)

cluster = clustered_series['AOS']
plot_cluster(cluster)

visualize_cluster(cluster)

5.b Большие кластеры

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

def plot_cluster_relative_to_mean(which_cluster):
    
    symbols=list(profiles_df[clustered==which_cluster].index)
    
    pricing = combined_stock_price_df[symbols]
    
    means = np.log(pricing).mean()
    data = np.log(pricing).sub(means)
    
    means = data.mean(axis=1).rolling(window=21).mean().shift(1) # shift to avoid look-ahead bias
    data.sub(means, axis=0).plot()
    plt.axhline(0, lw=3, ls='--', label='mean', color='k')
    plt.ylabel('Price divergence from group mean')
    plt.legend(loc=0)
    
    fig = plt.figure(1)
    fig.set_figheight(8)
    fig.set_figwidth(16)
cluster = clustered_series['CCL']
plot_cluster_relative_to_mean(cluster)

visualize_cluster(cluster)

cluster = clustered_series['DHI']
plot_cluster_relative_to_mean(cluster)

visualize_cluster(cluster)

cluster = clustered_series['BKR']
plot_cluster_relative_to_mean(cluster)

visualize_cluster(cluster)

6. Анализ торговли парами

6.a Исторические цены на акции

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

stock_data = combined_stock_price_df
stock_data

Поскольку AOS (A.O. Smith Corporation) и PNR (Pentair) были определены как хорошие кандидаты для парной торговли, мы определяем их как symbol_one и symbol_two в нашем торговом алгоритме ниже:

symbol_one = 'AOS'
symbol_two = 'PNR'
stock_data = stock_data[[symbol_one,symbol_two]]
stock_data.index.name = 'Date'
stock_data

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

stock1_name, stock2_name = symbol_one,symbol_two
orig_data = stock_data.loc['2020-01-01':,]
data = orig_data.diff().cumsum()
data1 = data[stock1_name].ffill().fillna(0).values
data2 = data[stock2_name].ffill().fillna(0).values

Давайте теперь изобразим исторические цены акций для AOS и PNR.

plt.figure(figsize = (18,8))
ax = plt.gca()
plt.title("Potentially Cointegrated Stocks")
orig_data[stock1_name].plot(ax=ax,color=sns.color_palette()[1],linewidth=2)
orig_data[stock2_name].plot(ax=ax,color=sns.color_palette()[2],linewidth=2)
plt.ylabel("Price (USD)")
plt.legend()
plt.show()

У этих компаний действительно есть связанные ценовые серии.

6.b Байесовское моделирование

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

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

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

with pm.Model() as model:
    
    # inject external stock data
    stock1 = th.shared(data1)
    stock2 = th.shared(data2)
    
    # define our cointegration variables
    beta_sigma = pm.Exponential('beta_sigma', 50.)
    beta = pm.GaussianRandomWalk('beta', sd=beta_sigma,
                                 shape=data1.shape[0])
    
    # with our assumptions, cointegration can be reframed as a regression problem
    stock2_regression = beta * stock1
# Assume prices are Normally distributed, the mean comes from the regression.
    sd = pm.HalfNormal('sd', sd=.1)
    likelihood = pm.Normal('y',
                           mu=stock2_regression,
                           sd=sd,
                           observed=stock2)
with model:
    stock1.set_value(data1)
    stock2.set_value(data2)
    trace = pm.sample(2000,tune=1000,cores=4)

Построим график распределения 𝛽 модели с течением времени.

rolling_beta = trace[beta].T.mean(axis=1)
plt.figure(figsize = (18,8))
ax = plt.gca()
plt.title("Beta Distribution over Time")
pd.Series(rolling_beta,index=orig_data.index).plot(ax=ax,color='r',zorder=1e6,linewidth=2)
for orbit in trace[beta][:500]:
    pd.Series(orbit,index=orig_data.index).plot(ax=ax,color=sns.color_palette()[0],alpha=0.05)
plt.legend(['Beta Mean','Beta Orbit'])
plt.show()

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

6.c Торговая стратегия

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

Определите «сигнал», который должен означать возврат к нулю, если 𝛽 остается относительно неподвижным.

Определите «сглаженный сигнал», 15-дневную скользящую среднюю «сигнала».

Если мы не торгуем ...

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

Если мы торгуем в длинную позицию…

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

Если мы торгуем в шорт…

  • Если сглаженный сигнал превышает начальное значение, закройте сделку; мы можем отклоняться от среднего.
  • Если сглаженный сигнал падает за нулевую линию, мы достигли среднего значения. Закройте сделку.
def getStrategyPortfolioWeights(rolling_beta,stock_name1,stock_name2,data,smoothing_window=15):
data1 = data[stock_name1].ffill().fillna(0).values
    data2 = data[stock_name2].ffill().fillna(0).values
# initial signal rebalance
    fixed_beta = rolling_beta[smoothing_window]
    signal = fixed_beta*data1 - data2
    smoothed_signal = pd.Series(signal).rolling(smoothing_window).mean()
    d_smoothed_signal = smoothed_signal.diff()
    trading = "not"
    trading_start = 0
leverage = 0*data.copy()
    for i in range(smoothing_window,data1.shape[0]):
        leverage.iloc[i,:] = leverage.iloc[i-1,:]
if trading=="not":
# dynamically rebalance the signal when not trading
            fixed_beta = rolling_beta[i]
            signal = fixed_beta*data1 - data2
            smoothed_signal = pd.Series(signal).rolling(smoothing_window).mean()
            d_smoothed_signal = smoothed_signal.diff()
if smoothed_signal[i]>0 and d_smoothed_signal[i]<0:
leverage.iloc[i,0] = -fixed_beta / (abs(fixed_beta)+1)
                leverage.iloc[i,1] = 1 / (abs(fixed_beta)+1)
trading = "short"
                trading_start = smoothed_signal[i]
elif smoothed_signal[i]<0 and d_smoothed_signal[i]>0:
fixed_beta = rolling_beta[i]
                leverage.iloc[i,0] = fixed_beta / (abs(fixed_beta)+1)
                leverage.iloc[i,1] = -1 / (abs(fixed_beta)+1)
trading = "long"
                trading_start = smoothed_signal[i]
else:
                leverage.iloc[i,0] = 0
                leverage.iloc[i,1] = 0
elif trading=="long":
# a failed trade
            if smoothed_signal[i] < trading_start:
                leverage.iloc[i,0] = 0
                leverage.iloc[i,1] = 0
                trading = "not"
# a successful trade
            if smoothed_signal[i]>0:
                leverage.iloc[i,0] = 0
                leverage.iloc[i,1] = 0
                trading = "not"
elif trading=="short":
# a failed trade
            if smoothed_signal[i] > trading_start:
                leverage.iloc[i,0] = 0
                leverage.iloc[i,1] = 0
                trading = "not"
# a successful trade
            if smoothed_signal[i]<0:
                leverage.iloc[i,0] = 0
                leverage.iloc[i,1] = 0
                trading = "not"
                
    return leverage

6.d Тестирование на истории и производительность при падении рынка

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

portfolioWeights = getStrategyPortfolioWeights(rolling_beta,stock1_name, stock2_name,data).fillna(0)
def backtest(pricingDF,leverageDF,start_cash):
    """Backtests pricing based on some given set of leverage. Leverage works such that it happens "overnight",
    so leverage for "today" is applied to yesterday's close price. This algo can handle NaNs in pricing data
    before a stock exists, but ffill() should be used for NaNs that occur after the stock has existed, even
    if that stock ceases to exist later."""
    
    pricing = pricingDF.values
    leverage = leverageDF.values
    
    shares = np.zeros_like(pricing)
    cash = np.zeros(pricing.shape[0])
    cash[0] = start_cash
    curr_price = np.zeros(pricing.shape[1])
    curr_price_div = np.zeros(pricing.shape[1])
    
    for t in range(1,pricing.shape[0]):
        
        if np.any(leverage[t]!=leverage[t-1]):
# handle non-existent stock values
            curr_price[:] = pricing[t-1]     # you can multiply with this one
            curr_price[np.isnan(curr_price)] = 0
            trading_allowed = (curr_price!=0)
            curr_price_div[:] = curr_price    # you can divide with this one
            curr_price_div[~trading_allowed] = 1
            
            # determine new positions (warning: leverage to non-trading_allowed stocks is just lost)
            portfolio_value = (shares[t-1]*curr_price).sum()+cash[t-1]
            target_shares = trading_allowed * (portfolio_value*leverage[t]) // curr_price_div
            
            # rebalance
            shares[t] = target_shares
            cash[t] = cash[t-1] - ((shares[t]-shares[t-1])*curr_price).sum()
            
        else:
            
            # maintain positions
            shares[t] = shares[t-1]
            cash[t] = cash[t-1]
    
    returns = (shares*np.nan_to_num(pricing)).sum(axis=1)+cash
    pct_returns = (returns-start_cash)/start_cash
    return (
        pd.DataFrame( shares, index=pricingDF.index, columns=pricingDF.columns ),
        pd.Series( cash, index=pricingDF.index ),
        pd.Series( pct_returns, index=pricingDF.index)
    )
shares, cash, returns = backtest( orig_data, portfolioWeights, 1e6 )
plt.figure(figsize = (18,8))
ax = plt.gca()
plt.title("Return Profile of Algorithm")
plt.ylabel("Percent Returns")
returns.plot(ax=ax,linewidth=3)
vals = ax.get_yticks()
ax.set_yticklabels(['{:,.0%}'.format(x) for x in vals])
plt.show()

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

7. Выводы и возможные направления на будущее

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

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

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