Введение
Торговля парами, возможно, является самой ранней формой количественной торговли акциями по относительной стоимости. Используя некоторые современные инструменты машинного обучения в процессе инвестирования в парную торговлю, мы покажем, как создавать разумные пары без использования каких-либо ценовых данных.
У некоторых акций есть сильно связанные ценовые ряды, потому что они:
- работают в аналогичных бизнес-направлениях
- иметь аналогичные экономические риски
- иметь аналогичное нормативное бремя
- иметь совпадающий набор однородных инвесторов
- работают на одних и тех же географических рынках
Следовательно, если бы мы могли прочитать и понять бизнес каждой компании, а затем связать компании на основе этого понимания, у нас должен быть надежный набор потенциальных подходящих пар. Это идеальная задача для машинного обучения и, в частности, для подполя Обработка естественного языка.
В этом анализе мы:
- собирать бизнес-профили по акциям из AlphaWave Data Stock Analysis API с помощью конечной точки Описание компании.
- использовать
scikit-learn
функции обработки естественного языкаCountVectorizer
иTfidfTransformer
, чтобы «читать» эти описания и извлекать важные и новые концептуальные особенности во всех компаниях. - объедините акции с
DBSCAN
, чтобы найти акции с похожими профилями. - визуализируйте особенности нескольких акций с помощью
WordCloud
, чтобы получить некоторую интуицию в отношении того, что изучает модель машинного обучения. - изучите временные ряды обнаруженных кластеров, чтобы увидеть, связаны ли этот процесс, не имеющий вообще входных данных о ценах на акции.
- протестируйте пару на истории, чтобы определить, насколько хорошо наши идентифицированные пары торгуют алгоритмически.
Блокноты 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)
Это просто потрясающе! Просто посмотрев текстовое описание компаний:
- модель ML смогла найти связанные компании.
- у связанных компаний действительно есть связанные ценовые ряды.
Чтобы увидеть, что изучает модель машинного обучения, давайте сгенерируем несколько словесных облаков, используя приведенный ниже код.
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 не дает никаких гарантий относительно их точности или полноты. Вся информация может быть изменена и может быстро стать ненадежной по разным причинам, включая изменения рыночных условий или экономических обстоятельств.