Стратегия высокочастотной торговли

  • Часть 1
  • Часть 2
  • Часть 3
  • Часть 4
  • Часть 5
  • Анализ стратегии высокочастотной торговли Penny Jump
  • Стратегия высокочастотной торговли товарными фьючерсами
  • Высокочастотные торговые сигналы на Python

Часть 1

Я написал две статьи о высокочастотной торговле цифровыми валютами, а именно «Подробное введение в высокочастотную стратегию цифровой валюты» и «Заработайте 80 раз за 5 дней, сила высокочастотной стратегии». Однако эти статьи можно рассматривать только как обмен опытом и общий обзор. На этот раз я планирую написать серию статей, чтобы познакомить с мыслительным процессом, лежащим в основе высокочастотной торговли с нуля. Я надеюсь, что он будет кратким и ясным, но из-за моего ограниченного опыта мое понимание высокочастотной торговли может быть не очень глубоким. Эту статью следует рассматривать как отправную точку для обсуждения, и я приветствую исправления и рекомендации экспертов.

Источник высокочастотной прибыли

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

Проблемы, подлежащие решению

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

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

Обязательные данные

Binance предоставляет загружаемые данные для отдельных сделок и лучших ордеров на покупку и продажу. Данные о глубине могут быть загружены через их API, занесенные в белый список, или они могут быть собраны вручную. Для целей тестирования на истории достаточно агрегированных торговых данных. В этой статье мы будем использовать пример данных HOOKUSDT-aggTrades-2023–01–27.

В [1]:from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

Индивидуальные торговые данные включают следующее:

  1. agg_trade_id: Идентификатор агрегированной сделки.
  2. цена: цена, по которой была совершена сделка.
  3. количество: Количество сделки.
  4. first_trade_id: В случаях, когда агрегируется несколько сделок, это представляет собой идентификатор первой сделки.
  5. last_trade_id: Идентификатор последней сделки в агрегации.
  6. transact_time: Временная метка исполнения сделки.
  7. is_buyer_maker: Указывает направление сделки. «True» представляет собой ордер на покупку, выполненный как мейкер, в то время как ордер на продажу исполняется как тейкер.

Видно, что в этот день было совершено 660 000 сделок, что указывает на высокую активность рынка. CSV-файл будет прикреплен в разделе комментариев.

В [4]:trades = pd.read_csv(‘COMPUSDT-aggTrades-2023-07-02.csv’)
trades

Вышел[4]:
, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,

664475 строк × 7 столбцов

Моделирование индивидуальной суммы сделки

Во-первых, данные обрабатываются путем разделения исходных сделок на две группы: ордера на покупку, исполненные как мейкеры, и ордера на продажу, исполненные как тейкеры. Кроме того, исходные агрегированные торговые данные объединяют сделки, совершенные в одно и то же время, по одной и той же цене и в одном направлении, в единую точку данных. Например, если есть один ордер на покупку объемом 100, он может быть разделен на две сделки объемом 60 и 40 соответственно, если цены разные. Это может повлиять на оценку объемов ордеров на покупку. Поэтому необходимо снова агрегировать данные на основе transact_time. После этого второго агрегирования объем данных уменьшается на 140 000 записей.

В работе [6]:trades[‘date’] = pd.to_datetime(trades[‘transact_time’], unit=’ms’)
trades.index = trades[‘date’]
buy_trades = trades[trades[‘is_buyer_maker’]==False].copy()
sell_trades = trades[trades[‘is_buyer_maker’]==True].copy()
buy_trades = buy_trades.groupby(‘transact_time’).agg({
‘agg_trade_id’: ‘last’,
‘price’: ‘last’,
‘quantity’: ‘sum’,
‘first_trade_id’: ‘first’,
‘last_trade_id’: ‘last’,
‘is_buyer_maker’: ‘last’,
‘date’: ‘last’,
‘transact_time’:’last’
})
sell_trades = sell_trades.groupby(‘transact_time’).agg({
‘agg_trade_id’: ‘last’,
‘price’: ‘last’,
‘quantity’: ‘sum’,
‘first_trade_id’: ‘first’,
‘last_trade_id’: ‘last’,
‘is_buyer_maker’: ‘last’,
‘date’: ‘last’,
‘transact_time’:’last’
})
buy_trades[‘interval’]=buy_trades[‘transact_time’] — buy_trades[‘transact_time’].shift()
sell_trades[‘interval’]=sell_trades[‘transact_time’] — sell_trades[‘transact_time’].shift()

В работе [10]:print(trades.shape[0] — (buy_trades.shape[0]+sell_trades.shape[0]))

Выход [10]:
146181

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

В работе [36]:buy_trades[‘quantity’].plot.hist(bins=200,figsize=(10, 5));

Выход [36]:

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

В работе [37]:buy_trades[‘quantity’][buy_trades[‘quantity’]<200].plot.hist(bins=200,figsize=(10, 5));

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

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

Здесь N является параметром нормализации. Мы выберем среднюю сумму сделки, M, и установим альфа на -2,06. Конкретная оценка альфа может быть получена путем вычисления P-значения при D=N. В частности, альфа = log(P(d>M))/log(2). Выбор разных точек может привести к небольшим различиям в значении альфы.

В работе [55]:depths = range(0, 250, 2)
probabilities = np.array([np.mean(buy_trades[‘quantity’] > depth) for depth in depths])
alpha = np.log(np.mean(buy_trades[‘quantity’] > mean_quantity))/np.log(2)
mean_quantity = buy_trades[‘quantity’].mean()
probabilities_s = np.array([(1+depth/mean_quantity)**alpha for depth in depths])

plt.figure(figsize=(10, 5))
plt.plot(depths, probabilities)
plt.plot(depths, probabilities_s)
plt.xlabel(‘Depth’)
plt.ylabel(‘Probability of execution’)
plt.title(‘Execution probability at different depths’)
plt.grid(True)

Выход[55]:

В [56]:plt.figure(figsize=(10, 5))
plt.grid(True)
plt.title(‘Diff’)
plt.plot(depths, probabilities_s-probabilities);

Вышел[56]:

Однако эта оценка является лишь приблизительной, как показано на графике, где мы строим разницу между смоделированными и фактическими значениями. Когда сумма сделки небольшая, отклонение значительное, даже приближающееся к 10%. Хотя выбор различных точек во время оценки параметра может повысить точность вероятности этой конкретной точки, он не решает проблему отклонения в целом. Это несоответствие возникает из-за разницы между степенным распределением и фактическим распределением. Для получения более точных результатов необходимо модифицировать уравнение степенного распределения. Конкретный процесс здесь не рассматривается, но вкратце, после минутного озарения, обнаруживается, что фактическое уравнение должно быть следующим:

Для упрощения давайте использовать r = q/M для представления нормализованной суммы сделки. Мы можем оценить параметры тем же методом, что и раньше. На следующем графике видно, что после модификации максимальное отклонение составляет не более 2%. Теоретически могут быть внесены дополнительные коррективы, но такого уровня точности уже достаточно.

В [52]:depths = range(0, 250, 2)
probabilities = np.array([np.mean(buy_trades[‘quantity’] > depth) for depth in depths])
mean = buy_trades[‘quantity’].mean()
alpha = np.log(np.mean(buy_trades[‘quantity’] > mean))/np.log(2.05)
probabilities_s = np.array([(((1+20**(-depth/mean))*depth+mean)/mean)**alpha for depth in depths])

plt.figure(figsize=(10, 5))
plt.plot(depths, probabilities)
plt.plot(depths, probabilities_s)
plt.xlabel(‘Depth’)
plt.ylabel(‘Probability of execution’)
plt.title(‘Execution probability at different depths’)
plt.grid(True)

В работе [53]:plt.figure(figsize=(10, 5))
plt.grid(True)
plt.title(‘Diff’)
plt.plot(depths, probabilities_s-probabilities);

Вышел[53]:

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

Часть 2

Моделирование накопленной торговой суммы

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

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

В [1]:from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

В [2]:trades = pd.read_csv(‘HOOKUSDT-aggTrades-2023-01-27.csv’)
trades[‘date’] = pd.to_datetime(trades[‘transact_time’], unit=’ms’)
trades.index = trades[‘date’]
buy_trades = trades[trades[‘is_buyer_maker’]==False].copy()
buy_trades = buy_trades.groupby(‘transact_time’).agg({
‘agg_trade_id’: ‘last’,
‘price’: ‘last’,
‘quantity’: ‘sum’,
‘first_trade_id’: ‘first’,
‘last_trade_id’: ‘last’,
‘is_buyer_maker’: ‘last’,
‘date’: ‘last’,
‘transact_time’:’last’
})
buy_trades[‘interval’]=buy_trades[‘transact_time’] — buy_trades[‘transact_time’].shift()
buy_trades.index = buy_trades[‘date’]

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

В [3]:df_resampled = buy_trades[‘quantity’].resample(‘1S’).sum()
df_resampled = df_resampled.to_frame(name=’quantity’)
df_resampled = df_resampled[df_resampled[‘quantity’]>0]

В [4]:# Cumulative distribution in 1s
depths = np.array(range(0, 3000, 5))
probabilities = np.array([np.mean(df_resampled[‘quantity’] > depth) for depth in depths])
mean = df_resampled[‘quantity’].mean()
alpha = np.log(np.mean(df_resampled[‘quantity’] > mean))/np.log(2.05)
probabilities_s = np.array([((1+20**(-depth/mean))*depth/mean+1)**(alpha) for depth in depths])

plt.figure(figsize=(10, 5))
plt.plot(depths, probabilities)
plt.plot(depths, probabilities_s)
plt.xlabel(‘Depth’)
plt.ylabel(‘Probability of execution’)
plt.title(‘Execution probability at different depths’)
plt.grid(True)

Вышел[4]:

В [5]:df_resampled = buy_trades[‘quantity’].resample(’30S’).sum()
df_resampled = df_resampled.to_frame(name=’quantity’)
df_resampled = df_resampled[df_resampled[‘quantity’]>0]
depths = np.array(range(0, 12000, 20))
probabilities = np.array([np.mean(df_resampled[‘quantity’] > depth) for depth in depths])
mean = df_resampled[‘quantity’].mean()
alpha = np.log(np.mean(df_resampled[‘quantity’] > mean))/np.log(2.05)
probabilities_s = np.array([((1+20**(-depth/mean))*depth/mean+1)**(alpha) for depth in depths])
alpha = np.log(np.mean(df_resampled[‘quantity’] > mean))/np.log(2)
probabilities_s_2 = np.array([(depth/mean+1)**alpha for depth in depths]) # No amendment

plt.figure(figsize=(10, 5))
plt.plot(depths, probabilities,label=’Probabilities (True)’)
plt.plot(depths, probabilities_s, label=’Probabilities (Simulation 1)’)
plt.plot(depths, probabilities_s_2, label=’Probabilities (Simulation 2)’)
plt.xlabel(‘Depth’)
plt.ylabel(‘Probability of execution’)
plt.title(‘Execution probability at different depths’)
plt.legend()
plt.grid(True)

Вышел[5]:

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

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

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

В работе [6]:df_resampled = buy_trades[‘quantity’].resample(‘2S’).sum()
df_resampled = df_resampled.to_frame(name=’quantity’)
df_resampled = df_resampled[df_resampled[‘quantity’]>0]
depths = np.array(range(0, 6500, 10))
probabilities = np.array([np.mean(df_resampled[‘quantity’] > depth) for depth in depths])
mean = buy_trades[‘quantity’].mean()
adjust = buy_trades[‘interval’].mean() / 2620
alpha = np.log(np.mean(buy_trades[‘quantity’] > mean))/0.7178397931503168
probabilities_s = np.array([((1+20**(-depth*adjust/mean))*depth*adjust/mean+1)**(alpha) for depth in depths])

plt.figure(figsize=(10, 5))
plt.plot(depths, probabilities)
plt.plot(depths, probabilities_s)
plt.xlabel(‘Depth’)
plt.ylabel(‘Probability of execution’)
plt.title(‘Execution probability at different depths’)
plt.grid(True)

Вышел[6]:

Влияние цены на одну сделку

Торговые данные ценны, и есть еще много данных, которые можно добыть. Следует обратить пристальное внимание на влияние ордеров на цены, так как это влияет на позиционирование стратегий. Точно так же, агрегируя данные на основе transact_time, мы вычисляем разницу между последней ценой и первой ценой. Если ордер один, разница в цене равна 0. Интересно, что есть несколько отрицательных результатов данных, что может быть связано с упорядочиванием данных, но мы не будем углубляться в это здесь.

Результаты показывают, что доля сделок, которые не оказали никакого влияния, достигает 77%, в то время как доля сделок, вызывающих движение цены на 1 тик, составляет 16,5%, 2 тика — 3,7%, 3 тика — 1,2%, а более 4 тиков — менее 1%. Это в основном следует характеристикам экспоненциальной функции, но подгонка не является точной.

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

В работе [7]:diff_df = trades[trades[‘is_buyer_maker’]==False].groupby(‘transact_time’)[‘price’].agg(lambda x: abs(round(x.iloc[-1] — x.iloc[0],3)) if len(x) > 1 else 0)
buy_trades[‘diff’] = buy_trades[‘transact_time’].map(diff_df)

В работе [8]:diff_counts = buy_trades[‘diff’].value_counts()
diff_counts[diff_counts>10]/diff_counts.sum()

Вышел[8]:

В [9]:diff_group = buy_trades.groupby(‘diff’).agg({
‘quantity’: ‘mean’,
‘diff’: ‘last’,
})

В работе [10]:diff_group[‘quantity’][diff_group[‘diff’]>0][diff_group[‘diff’]<0.01].plot(figsize=(10,5),grid=True);

Вышел[10]:

Влияние на цену с фиксированным интервалом

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

В работе [11]:df_resampled = buy_trades.resample(‘2S’).agg({
‘price’: [‘first’, ‘last’, ‘count’],
‘quantity’: ‘sum’
})
df_resampled[‘price_diff’] = round(df_resampled[(‘price’, ‘last’)] — df_resampled[(‘price’, ‘first’)],3)
df_resampled[‘price_diff’] = df_resampled[‘price_diff’].fillna(0)
result_df_raw = pd.DataFrame({
‘price_diff’: df_resampled[‘price_diff’],
‘quantity_sum’: df_resampled[(‘quantity’, ‘sum’)],
‘data_count’: df_resampled[(‘price’, ‘count’)]
})
result_df = result_df_raw[result_df_raw[‘price_diff’] != 0]

В работе [12]:result_df[‘price_diff’][abs(result_df[‘price_diff’])<0.016].value_counts().sort_index().plot.bar(figsize=(10,5));

Вышел[12]:

В [23]:result_df[‘price_diff’].value_counts()[result_df[‘price_diff’].value_counts()>30]

Вышел[23]:

В [14]:diff_group = result_df.groupby(‘price_diff’).agg({ ‘quantity_sum’: ‘mean’})

В работе [15]:diff_group[(diff_group.index>0) & (diff_group.index<0.015)].plot(figsize=(10,5),grid=True);

Вышел[15]:

Влияние суммы сделки на цену

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

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

  1. Когда сумма ордера на покупку ниже 500, ожидаемое изменение цены является снижением, что и ожидалось, поскольку есть также ордера на продажу, влияющие на цену.
  2. При меньших объемах сделок существует линейная зависимость, означающая, что чем больше сумма сделки, тем больше рост цены.
  3. По мере увеличения суммы ордера на покупку изменение цены становится более значительным. Это часто указывает на ценовой прорыв, который впоследствии может регрессировать. Кроме того, выборка с фиксированным интервалом увеличивает нестабильность данных.
  4. Важно обратить внимание на верхнюю часть точечной диаграммы, которая соответствует росту цены с суммой сделки.
  5. Для этой конкретной торговой пары мы предоставляем приблизительную версию взаимосвязи между суммой сделки и изменением цены.

Где «C» представляет собой изменение цены, а «Q» представляет собой количество ордеров на покупку.

В работе [16]:df_resampled = buy_trades.resample(‘1S’).agg({
‘price’: [‘first’, ‘last’, ‘count’],
‘quantity’: ‘sum’
})
df_resampled[‘price_diff’] = round(df_resampled[(‘price’, ‘last’)] — df_resampled[(‘price’, ‘first’)],3)
df_resampled[‘price_diff’] = df_resampled[‘price_diff’].fillna(0)
result_df_raw = pd.DataFrame({
‘price_diff’: df_resampled[‘price_diff’],
‘quantity_sum’: df_resampled[(‘quantity’, ‘sum’)],
‘data_count’: df_resampled[(‘price’, ‘count’)]
})
result_df = result_df_raw[result_df_raw[‘price_diff’] != 0]

В работе [24]:df = result_df.copy()
bins = np.arange(0, 30000, 100) #
labels = [f'{i}-{i+100-1}’ for i in bins[:-1]]
df.loc[:, ‘quantity_group’] = pd.cut(df[‘quantity_sum’], bins=bins, labels=labels)
grouped = df.groupby(‘quantity_group’)[‘price_diff’].mean()

В работе [25]:grouped_df = pd.DataFrame(grouped).reset_index()
grouped_df[‘quantity_group_center’] = grouped_df[‘quantity_group’].apply(lambda x: (float(x.split(‘-‘)[0]) + float(x.split(‘-‘)[1])) / 2)

plt.figure(figsize=(10,5))
plt.scatter(grouped_df[‘quantity_group_center’], grouped_df[‘price_diff’],s=10)
plt.plot(grouped_df[‘quantity_group_center’], np.array(grouped_df[‘quantity_group_center’].values)/2e6-0.000352,color=’red’)
plt.xlabel(‘quantity_group_center’)
plt.ylabel(‘average price_diff’)
plt.title(‘Scatter plot of average price_diff by quantity_group’)
plt.grid(True)

Вышел[25]:

В работе [19]:grouped_df.head(10)

Вышел[19]:
, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,

Предварительное оптимальное размещение заказа

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

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

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

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

Сводка

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

В работе [20]:# Cumulative distribution in 1s
df_resampled = buy_trades[‘quantity’].resample(‘1S’).sum()
df_resampled = df_resampled.to_frame(name=’quantity’)
df_resampled = df_resampled[df_resampled[‘quantity’]>0]

depths = np.array(range(0, 15000, 10))
mean = df_resampled[‘quantity’].mean()
alpha = np.log(np.mean(df_resampled[‘quantity’] > mean))/np.log(2.05)
probabilities_s = np.array([((1+20**(-depth/mean))*depth/mean+1)**(alpha) for depth in depths])
profit_s = np.array([depth/2e6-0.000352 for depth in depths])
plt.figure(figsize=(10, 5))
plt.plot(depths, probabilities_s*profit_s)
plt.xlabel(‘Q’)
plt.ylabel(‘Excpet profit’)
plt.grid(True)

Вышел[20]:

Часть 3

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

Временные интервалы заказа

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

Я скачал данные aggTrades за 5 августа, которые состоят из 1 931 193 сделок, что довольно значительно. Во-первых, давайте посмотрим на распределение ордеров на покупку. Мы можем видеть негладкий локальный пик около 100 мс и 500 мс, что, вероятно, вызвано айсберговыми ордерами, размещаемыми торговыми ботами через равные промежутки времени. Это также может быть одной из причин необычных рыночных условий в этот день.

Массовая функция вероятности (PMF) распределения Пуассона определяется следующей формулой:

Где:

  • κ – количество интересующих нас событий.
  • λ — средняя частота событий, происходящих в единицу времени (или единицу пространства).
  • представляет собой вероятность того, что произойдет ровно κ событий, учитывая среднюю скорость λ.

В пуассоновском процессе временные интервалы между событиями следуют экспоненциальному распределению. Функция плотности вероятности (PDF) экспоненциального распределения определяется следующей формулой:

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

В [1]:from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

В [2]:trades = pd.read_csv(‘YGGUSDT-aggTrades-2023-08-05.csv’)
trades[‘date’] = pd.to_datetime(trades[‘transact_time’], unit=’ms’)
trades.index = trades[‘date’]
buy_trades = trades[trades[‘is_buyer_maker’]==False].copy()
buy_trades = buy_trades.groupby(‘transact_time’).agg({
‘agg_trade_id’: ‘last’,
‘price’: ‘last’,
‘quantity’: ‘sum’,
‘first_trade_id’: ‘first’,
‘last_trade_id’: ‘last’,
‘is_buyer_maker’: ‘last’,
‘date’: ‘last’,
‘transact_time’:’last’
})
buy_trades[‘interval’]=buy_trades[‘transact_time’] — buy_trades[‘transact_time’].shift()
buy_trades.index = buy_trades[‘date’]

В [10]buy_trades[‘interval’][buy_trades[‘interval’]<1000].plot.hist(bins=200,figsize=(10, 5));

Вышел[10]:

В работе [20]:
Intervals = np.array(range(0, 1000, 5))
mean_intervals = buy_trades[‘interval’].mean()
buy_rates = 1000/mean_intervals
probabilities = np.array([np.mean(buy_trades[‘interval’] > interval) for interval in Intervals])
probabilities_s = np.array([np.e**(-buy_rates*interval/1000) for interval in Intervals])

plt.figure(figsize=(10, 5))
plt.plot(Intervals, probabilities)
plt.plot(Intervals, probabilities_s)
plt.xlabel(‘Intervals’)
plt.ylabel(‘Probability’)
plt.grid(True)

Вышел[20]:

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

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

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

В [190]:result_df = buy_trades.resample(‘1S’).agg({
‘price’: ‘count’,
‘quantity’: ‘sum’
}).rename(columns={‘price’: ‘order_count’, ‘quantity’: ‘quantity_sum’})

В [219]:count_df = result_df[‘order_count’].value_counts().sort_index()[result_df[‘order_count’].value_counts()>20]
(count_df/count_df.sum()).plot(figsize=(10,5),grid=True,label=’sample pmf’);

from scipy.stats import poisson
prob_values = poisson.pmf(count_df.index, 1000/mean_intervals)

plt.plot(count_df.index, prob_values,label=’poisson pmf’);
plt.legend() ;

Вышел[219]:

Обновление параметров в режиме реального времени

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

Из графиков мы также можем понять, почему частота порядка так сильно отклоняется от распределения Пуассона. Хотя среднее количество ордеров в секунду составляет всего 8,5, крайние случаи значительно отклоняются от этого значения.

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

В [221]:result_df[‘order_count’].rolling(1000).mean().plot(figsize=(10,5),grid=True);

Вышел[221]:

В [193]:result_df[‘quantity_sum’].rolling(1000).mean().plot(figsize=(10,5),grid=True);

Вышел[193]:

В [195]:(result_df[‘order_count’] — result_df[‘mean_count’].mean()).abs().mean()

Вышел[195]:

6.985628185332997

В [205]:result_df[‘mean_count’] = result_df[‘order_count’].rolling(2).mean()
(result_df[‘order_count’] — result_df[‘mean_count’].shift()).abs().mean()

Вышел[205]:

3.091737586730269

Сводка

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

Часть 4

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

Данные о глубине

Binance предоставляет загрузку исторических данных для best_bid_price (самая высокая цена покупки), best_bid_quantity (количество по лучшей цене предложения), best_ask_price (самая низкая цена продажи), best_ask_quantity (количество по лучшей цене предложения) и transaction_time. Эти данные не включают второй или более глубокие уровни книги заказов. Анализ в этой статье основан на рынке YGG 7 августа, который испытал значительную волатильность с более чем 9 миллионами точек данных.

Во-первых, давайте посмотрим на рыночные условия в этот день. Были большие колебания, и объем книги заказов значительно изменился вместе с волатильностью рынка. Спред, в частности, указывал на степень рыночных колебаний, которая представляет собой разницу между лучшими ценами спроса и предложения. В статистике рынка YGG в этот день спред был больше одного тика в течение 20% времени. В нынешнюю эпоху различных торговых ботов, конкурирующих в стакане, такие ситуации становятся все более редкими.

В [1]:from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

В [2]:books = pd.read_csv(‘YGGUSDT-bookTicker-2023-08-07.csv’)

В [3]:tick_size = 0.0001

В [4]:books[‘date’] = pd.to_datetime(books[‘transaction_time’], unit=’ms’)
books.index = books[‘date’]

В [5]:books[‘spread’] = round(books[‘best_ask_price’] — books[‘best_bid_price’],4)

В работе [6]:books[‘best_bid_price’][::10].plot(figsize=(10,5),grid=True);

Вышел[6]:

В работе [7]:books[‘best_bid_qty’][::10].rolling(10000).mean().plot(figsize=(10,5),grid=True);
books[‘best_ask_qty’][::10].rolling(10000).mean().plot(figsize=(10,5),grid=True);

Вышел[7]:

В работе [8]:(books[‘spread’][::10]/tick_size).rolling(10000).mean().plot(figsize=(10,5),grid=True);

Вышел[8]:

В [9]:books[‘spread’].value_counts()[books[‘spread’].value_counts()>500]/books[‘spread’].value_counts().sum()

Вышел[9]:

Несбалансированные котировки

Несбалансированные котировки наблюдаются из-за значительной разницы в объемах книги заказов между заявками на покупку и продажу в большинстве случаев. Эта разница оказывает сильное прогностическое влияние на краткосрочные рыночные тенденции, аналогично упомянутой ранее причине, что снижение объема ордеров на покупку часто приводит к снижению. Если одна сторона книги заказов значительно меньше другой, предполагая, что активные ордера на покупку и продажу одинаковы по объему, существует большая вероятность того, что меньшая сторона будет использована, что приведет к изменениям цены. Несбалансированные кавычки обозначаются буквой «I».

Где Q_b представляет собой количество отложенных ордеров на покупку (best_bid_qty), а Q_a представляет собой количество отложенных ордеров на продажу (best_ask_qty).

Определите среднюю цену:

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

В работе [10]:books[‘I’] = books[‘best_bid_qty’] / (books[‘best_bid_qty’] + books[‘best_ask_qty’])

В работе [11]:books[‘mid_price’] = (books[‘best_ask_price’] + books[‘best_bid_price’])/2

В работе [12]:bins = np.linspace(0, 1, 51)
books[‘I_bins’] = pd.cut(books[‘I’], bins, labels=bins[1:])
books[‘price_change’] = (books[‘mid_price’].pct_change()/tick_size).shift(-1)
avg_change = books.groupby(‘I_bins’)[‘price_change’].mean()
plt.figure(figsize=(8,5))
plt.plot(avg_change)
plt.xlabel(‘I Value Range’)
plt.ylabel(‘Average Mid Price Change Rate’);
plt.grid(True)

Вышел[12]:

В работе [13]:books[‘weighted_mid_price’] = books[‘mid_price’] + books[‘spread’]*books[‘I’]/2
bins = np.linspace(-1, 1, 51)
books[‘I_bins’] = pd.cut(books[‘I’], bins, labels=bins[1:])
books[‘weighted_price_change’] = (books[‘weighted_mid_price’].pct_change()/tick_size).shift(-1)
avg_change = books.groupby(‘I_bins’)[‘weighted_price_change’].mean()
plt.figure(figsize=(8,5))
plt.plot(avg_change)
plt.xlabel(‘I Value Range’)
plt.ylabel(‘Weighted Average Mid Price Change Rate’);
plt.grid(True)

Вышел[13]:

Отрегулируйте взвешенную среднюю цену:

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

Чтобы обеспечить более интуитивное представление, вот переопределение I:

Пересмотренное определение I:

На этом этапе:

При наблюдении можно заметить, что взвешенная средняя цена представляет собой коррекцию к средней средней цене, где срок коррекции умножается на спред. Поправочный член является функцией I, а взвешенная средняя цена предполагает простое соотношение I/2. В этом случае становится очевидным преимущество скорректированного распределения I (-1, 1), так как I симметрично относительно начала координат, что позволяет удобно находить подходящее соотношение для функции. Изучив график, выясняется, что эта функция должна удовлетворять нечетным степеням I, поскольку она согласуется с быстрым ростом с обеих сторон и симметрией вокруг начала координат. Кроме того, можно наблюдать, что значения, близкие к началу координат, близки к линейным. Кроме того, когда I равен 0, результат функции равен 0, а когда I равен 1, результат функции равен 0,5. Поэтому предполагается, что функция имеет вид:

Здесь N — положительное четное число, после фактического тестирования лучше, когда N равно 8. До сих пор в этой статье представлена модифицированная взвешенная средняя цена:

На данный момент прогноз средних ценовых изменений уже не имеет существенной связи с I. Хотя этот результат немного лучше, чем простая взвешенная средняя цена, он все же неприменим в реальных торговых сценариях. Это всего лишь предлагаемый подход. В статье С. Стойкова 2017 года понятие Micro-Price вводится с использованием подхода цепи Маркова, и приводится соответствующий код. Исследователи могут изучить этот подход дальше.

В [14]:books[‘I’] = (books[‘best_bid_qty’] — books[‘best_ask_qty’]) / (books[‘best_bid_qty’] + books[‘best_ask_qty’])

В работе [15]:books[‘weighted_mid_price’] = books[‘mid_price’] + books[‘spread’]*books[‘I’]/2
bins = np.linspace(-1, 1, 51)
books[‘I_bins’] = pd.cut(books[‘I’], bins, labels=bins[1:])
books[‘weighted_price_change’] = (books[‘weighted_mid_price’].pct_change()/tick_size).shift(-1)
avg_change = books.groupby(‘I_bins’)[‘weighted_price_change’].mean()
plt.figure(figsize=(8,5))
plt.plot(avg_change)
plt.xlabel(‘I Value Range’)
plt.ylabel(‘Weighted Average Mid Price Change Rate’);
plt.grid(True)

Вышел[15]:

В работе [16]:books[‘adjust_mid_price’] = books[‘mid_price’] + books[‘spread’]*books[‘I’]*(books[‘I’]**8+1)/4
bins = np.linspace(-1, 1, 51)
books[‘I_bins’] = pd.cut(books[‘I’], bins, labels=bins[1:])
books[‘adjust_mid_price’] = (books[‘adjust_mid_price’].pct_change()/tick_size).shift(-1)
avg_change = books.groupby(‘I_bins’)[‘adjust_mid_price’].mean()
plt.figure(figsize=(8,5))
plt.plot(avg_change)
plt.xlabel(‘I Value Range’)
plt.ylabel(‘Weighted Average Mid Price Change Rate’);
plt.grid(True)

Вышел[16]:

Сводка

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

Часть5

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

Необходимые данные

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

В [1]:from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import ast
%matplotlib inline

В [2]:tick_size = 0.0001

В [3]:trades = pd.read_csv(‘YGGUSDT_aggTrade.csv’,names=[‘type’,’event_time’, ‘agg_trade_id’,’symbol’, ‘price’, ‘quantity’, ‘first_trade_id’, ‘last_trade_id’,
‘transact_time’, ‘is_buyer_maker’])

В [4]:trades = trades.groupby([‘transact_time’,’is_buyer_maker’]).agg({
‘transact_time’:’last’,
‘agg_trade_id’: ‘last’,
‘price’: ‘first’,
‘quantity’: ‘sum’,
‘first_trade_id’: ‘first’,
‘last_trade_id’: ‘last’,
‘is_buyer_maker’: ‘last’,
})

В [5]:trades.index = pd.to_datetime(trades[‘transact_time’], unit=’ms’)
trades.index.rename(‘time’, inplace=True)
trades[‘interval’] = trades[‘transact_time’] — trades[‘transact_time’].shift()

В работе [6]:depths = pd.read_csv(‘YGGUSDT_depth.csv’,names=[‘type’,’event_time’, ‘transact_time’,’symbol’, ‘u1’, ‘u2’, ‘u3’, ‘bids’,’asks’])

В работе [7]:depths = depths.iloc[:100000]

В работе [8]:depths[‘bids’] = depths[‘bids’].apply(ast.literal_eval).copy()
depths[‘asks’] = depths[‘asks’].apply(ast.literal_eval).copy()

В [9]:def expand_bid(bid_data):
expanded = {}
for j, (price, quantity) in enumerate(bid_data):
expanded[f’bid_{j}_price’] = float(price)
expanded[f’bid_{j}_quantity’] = float(quantity)
return pd.Series(expanded)
def expand_ask(ask_data):
expanded = {}
for j, (price, quantity) in enumerate(ask_data):
expanded[f’ask_{j}_price’] = float(price)
expanded[f’ask_{j}_quantity’] = float(quantity)
return pd.Series(expanded)
# Apply to each line to get a new df
expanded_df_bid = depths[‘bids’].apply(expand_bid)
expanded_df_ask = depths[‘asks’].apply(expand_ask)
# Expansion on the original df
depths = pd.concat([depths, expanded_df_bid, expanded_df_ask], axis=1)

В работе [10]:depths.index = pd.to_datetime(depths[‘transact_time’], unit=’ms’)
depths.index.rename(‘time’, inplace=True);

В работе [11]:trades = trades[trades[‘transact_time’] < depths[‘transact_time’].iloc[-1]]

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

В [14]:bid_mean_list = []
ask_mean_list = []
for i in range(20):
bid_mean_list.append(round(depths[f’bid_{i}_quantity’].mean(),0))
ask_mean_list.append(round(depths[f’ask_{i}_quantity’].mean(),0))
plt.figure(figsize=(10, 5))
plt.plot(bid_mean_list);
plt.plot(ask_mean_list);
plt.grid(True)

Вышел[14]:

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

Из результатов наибольшая погрешность для среднего значения цен покупки и продажи (mid_price). Однако при переходе на взвешенный mid_price погрешность сразу уменьшается в разы. Дальнейшее улучшение наблюдается при использовании скорректированного взвешенного mid_price. После получения отзывов об использовании только I³/2 была проведена проверка и обнаружено, что результаты были лучше. Поразмыслив, это, вероятно, связано с разной частотой событий. Когда I близок к -1 и 1, он представляет события с низкой вероятностью. Чтобы исправить эти события с низкой вероятностью, точность прогнозирования высокочастотных событий ставится под угрозу. Поэтому для приоритизации высокочастотных событий были внесены некоторые коррективы (эти параметры были чисто методом проб и ошибок и имеют ограниченное практическое значение в реальной торговле).

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

В работе [15]:df = pd.merge_asof(trades, depths, on=’transact_time’, direction=’backward’)

В работе [17]:df[‘spread’] = round(df[‘ask_0_price’] — df[‘bid_0_price’],4)
df[‘mid_price’] = (df[‘bid_0_price’]+ df[‘ask_0_price’]) / 2
df[‘I’] = (df[‘bid_0_quantity’] — df[‘ask_0_quantity’]) / (df[‘bid_0_quantity’] + df[‘ask_0_quantity’])
df[‘weight_mid_price’] = df[‘mid_price’] + df[‘spread’]*df[‘I’]/2
df[‘adjust_mid_price’] = df[‘mid_price’] + df[‘spread’]*(df[‘I’])*(df[‘I’]**8+1)/4
df[‘adjust_mid_price_2’] = df[‘mid_price’] + df[‘spread’]*df[‘I’]*(df[‘I’]**2+1)/4
df[‘adjust_mid_price_3’] = df[‘mid_price’] + df[‘spread’]*df[‘I’]**3/2
df[‘adjust_mid_price_4’] = df[‘mid_price’] + df[‘spread’]*(df[‘I’]+0.3)*(df[‘I’]**4+0.7)/3.8

В работе [18]:print(‘Mean value Error in mid_price:’, ((df[‘price’]-df[‘mid_price’])**2).sum())
print(‘Error of pending order volume weighted mid_price:’, ((df[‘price’]-df[‘weight_mid_price’])**2).sum())
print(‘The error of the adjusted mid_price:’, ((df[‘price’]-df[‘adjust_mid_price’])**2).sum())
print(‘The error of the adjusted mid_price_2:’, ((df[‘price’]-df[‘adjust_mid_price_2’])**2).sum())
print(‘The error of the adjusted mid_price_3:’, ((df[‘price’]-df[‘adjust_mid_price_3’])**2).sum())
print(‘The error of the adjusted mid_price_4:’, ((df[‘price’]-df[‘adjust_mid_price_4’])**2).sum())

Вышел[18]:

Среднее значение Погрешность в mid_price: 0.00487519249999999845
Погрешность взвешенного объема отложенного ордера mid_price: 0.0048373440193987035
Погрешность скорректированного mid_price: 0,004803654771638586
Погрешность скорректированного mid_price_2: 0,004808216498329721
Погрешность скорректированного mid_price_3: 0,004794984755260528
Погрешность скорректированного mid_price_4: 0,0047909595497071375

Рассмотрим второй уровень глубины

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

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

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

В работе [19]:bins = np.linspace(-1, 1, 50)
df[‘change’] = (df[‘price’].pct_change().shift(-1))/tick_size
df[‘I_bins’] = pd.cut(df[‘I’], bins, labels=bins[1:])
df[‘I_2’] = (df[‘bid_1_quantity’] — df[‘ask_1_quantity’]) / (df[‘bid_1_quantity’] + df[‘ask_1_quantity’])
df[‘I_2_bins’] = pd.cut(df[‘I_2’], bins, labels=bins[1:])
df[‘I_3’] = (df[‘bid_2_quantity’] — df[‘ask_2_quantity’]) / (df[‘bid_2_quantity’] + df[‘ask_2_quantity’])
df[‘I_3_bins’] = pd.cut(df[‘I_3’], bins, labels=bins[1:])
df[‘I_4’] = (df[‘bid_3_quantity’] — df[‘ask_3_quantity’]) / (df[‘bid_3_quantity’] + df[‘ask_3_quantity’])
df[‘I_4_bins’] = pd.cut(df[‘I_4’], bins, labels=bins[1:])
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8, 5))

axes[0][0].plot(df.groupby(‘I_bins’)[‘change’].mean())
axes[0][0].set_title(‘I’)
axes[0][0].grid(True)

axes[0][1].plot(df.groupby(‘I_2_bins’)[‘change’].mean())
axes[0][1].set_title(‘I 2’)
axes[0][1].grid(True)

axes[1][0].plot(df.groupby(‘I_3_bins’)[‘change’].mean())
axes[1][0].set_title(‘I 3’)
axes[1][0].grid(True)

axes[1][1].plot(df.groupby(‘I_4_bins’)[‘change’].mean())
axes[1][1].set_title(‘I 4’)
axes[1][1].grid(True)
plt.tight_layout();

Вышел[19]:

В работе [20]:df[‘adjust_mid_price_4’] = df[‘mid_price’] + df[‘spread’]*(df[‘I’]+0.3)*(df[‘I’]**4+0.7)/3.8
df[‘adjust_mid_price_5’] = df[‘mid_price’] + df[‘spread’]*(0.7*df[‘I’]+0.3*df[‘I_2’])/2
df[‘adjust_mid_price_6’] = df[‘mid_price’] + df[‘spread’]*(0.7*df[‘I’]+0.3*df[‘I_2’])**3/2
df[‘adjust_mid_price_7’] = df[‘mid_price’] + df[‘spread’]*(0.7*df[‘I’]+0.3*df[‘I_2’]+0.3)*((0.7*df[‘I’]+0.3*df[‘I_2’])**4+0.7)/3.8
df[‘adjust_mid_price_8’] = df[‘mid_price’] + df[‘spread’]*(0.7*df[‘I’]+0.2*df[‘I_2’]+0.1*df[‘I_3’]+0.3)*((0.7*df[‘I’]+0.3*df[‘I_2’]+0.1*df[‘I_3’])**4+0.7)/3.8

В [21]:print(‘The error of the adjusted mid_price_4:’, ((df[‘price’]-df[‘adjust_mid_price_4’])**2).sum())
print(‘The error of the adjusted mid_price_5:’, ((df[‘price’]-df[‘adjust_mid_price_5’])**2).sum())
print(‘The error of the adjusted mid_price_6:’, ((df[‘price’]-df[‘adjust_mid_price_6’])**2).sum())
print(‘The error of the adjusted mid_price_7:’, ((df[‘price’]-df[‘adjust_mid_price_7’])**2).sum())
print(‘The error of the adjusted mid_price_8:’, ((df[‘price’]-df[‘adjust_mid_price_8’])**2).sum())

Вышел[21]:

Погрешность скорректированного mid_price_4: 0,0047909595497071375
Погрешность скорректированного mid_price_5: 0,0047884350488318714
Погрешность скорректированного mid_price_6: 0,0047778319053133735
Погрешность скорректированного mid_price_7: 0,004773578540592192
Погрешность скорректированного mid_price_8: 0,004771415189297518

Учет данных о транзакциях

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

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

Результаты показывают, что количество поступлений за короткий промежуток времени оказывает наиболее существенное влияние на прогноз изменения цены. Когда VI находится между 0,1 и 0,9, он отрицательно коррелирует с ценой, в то время как за пределами этого диапазона он положительно коррелирует с ценой. Это говорит о том, что, когда рынок не является экстремальным и в основном колеблется, цена имеет тенденцию возвращаться к среднему значению. Однако в экстремальных рыночных условиях, например, когда есть большое количество ордеров на покупку, подавляющих ордера на продажу, возникает тенденция. Даже без учета этих сценариев с низкой вероятностью предположение об отрицательной линейной зависимости между трендом и VI значительно снижает ошибку предсказания mid_price. Коэффициент «a» представляет собой вес этого соотношения в уравнении.

В [22]:alpha=0.1

В [23]:df[‘avg_buy_interval’] = None
df[‘avg_sell_interval’] = None
df.loc[df[‘is_buyer_maker’] == True, ‘avg_buy_interval’] = df[df[‘is_buyer_maker’] == True][‘transact_time’].diff().ewm(alpha=alpha).mean()
df.loc[df[‘is_buyer_maker’] == False, ‘avg_sell_interval’] = df[df[‘is_buyer_maker’] == False][‘transact_time’].diff().ewm(alpha=alpha).mean()

В работе [24]:df[‘avg_buy_quantity’] = None
df[‘avg_sell_quantity’] = None
df.loc[df[‘is_buyer_maker’] == True, ‘avg_buy_quantity’] = df[df[‘is_buyer_maker’] == True][‘quantity’].ewm(alpha=alpha).mean()
df.loc[df[‘is_buyer_maker’] == False, ‘avg_sell_quantity’] = df[df[‘is_buyer_maker’] == False][‘quantity’].ewm(alpha=alpha).mean()

В работе [25]:df[‘avg_buy_quantity’] = df[‘avg_buy_quantity’].fillna(method=’ffill’)
df[‘avg_sell_quantity’] = df[‘avg_sell_quantity’].fillna(method=’ffill’)
df[‘avg_buy_interval’] = df[‘avg_buy_interval’].fillna(method=’ffill’)
df[‘avg_sell_interval’] = df[‘avg_sell_interval’].fillna(method=’ffill’)

df[‘avg_buy_rate’] = 1000 / df[‘avg_buy_interval’]
df[‘avg_sell_rate’] =1000 / df[‘avg_sell_interval’]

df[‘avg_buy_volume’] = df[‘avg_buy_rate’]*df[‘avg_buy_quantity’]
df[‘avg_sell_volume’] = df[‘avg_sell_rate’]*df[‘avg_sell_quantity’]

В работе [26]:df[‘I’] = (df[‘bid_0_quantity’]- df[‘ask_0_quantity’]) / (df[‘bid_0_quantity’] + df[‘ask_0_quantity’])
df[‘OI’] = (df[‘avg_buy_rate’]-df[‘avg_sell_rate’]) / (df[‘avg_buy_rate’] + df[‘avg_sell_rate’])
df[‘QI’] = (df[‘avg_buy_quantity’]-df[‘avg_sell_quantity’]) / (df[‘avg_buy_quantity’] + df[‘avg_sell_quantity’])
df[‘VI’] = (df[‘avg_buy_volume’]-df[‘avg_sell_volume’]) / (df[‘avg_buy_volume’] + df[‘avg_sell_volume’])

В работе [27]:bins = np.linspace(-1, 1, 50)
df[‘VI_bins’] = pd.cut(df[‘VI’], bins, labels=bins[1:])
plt.plot(df.groupby(‘VI_bins’)[‘change’].mean());
plt.grid(True)

Вышел[27]:

В работе [28]:df[‘adjust_mid_price’] = df[‘mid_price’] + df[‘spread’]*df[‘I’]/2
df[‘adjust_mid_price_9’] = df[‘mid_price’] + df[‘spread’]*(-df[‘OI’])*2
df[‘adjust_mid_price_10’] = df[‘mid_price’] + df[‘spread’]*(-df[‘VI’])*1.4

В [29]:print(‘The error of the adjusted mid_price:’, ((df[‘price’]-df[‘adjust_mid_price’])**2).sum())
print(‘The error of the adjusted mid_price_9:’, ((df[‘price’]-df[‘adjust_mid_price_9’])**2).sum())
print(‘The error of the adjusted mid_price_10:’, ((df[‘price’]-df[‘adjust_mid_price_10’])**2).sum())

Вышел[29]:

Погрешность скорректированного mid_price: 0,0048373440193987035
Погрешность скорректированного mid_price_9: 0,004629586542840461
Погрешность скорректированного mid_price_10: 0,004401790287167206

Комплексная средняя цена

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

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

В работе [30]:#Note that the VI needs to be delayed by one to use
df[‘CI’] = -1.5*df[‘VI’].shift()+0.7*(0.7*df[‘I’]+0.2*df[‘I_2’]+0.1*df[‘I_3’])**3

В [31]:df[‘adjust_mid_price_11’] = df[‘mid_price’] + df[‘spread’]*(df[‘CI’])
print(‘The error of the adjusted mid_price_11:’, ((df[‘price’]-df[‘adjust_mid_price_11’])**2).sum())

Вышел[31]:

Погрешность скорректированного mid_price_11: 0,0043001941412563575

Сводка

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

Анализ стратегии высокочастотной торговли Penny Jump

Высокочастотный трейдинг — это сложная и конкурентная область, которая зависит от быстрого исполнения сделок и тонкого понимания микроструктуры рынка. Одной из примечательных стратегий является Penny Jump, которая фокусируется на эксплуатации «слонов» на рынке для получения небольшой, но частой прибыли. В этой статье мы подробно объясним, как работает стратегия Penny Jump, углубившись в детали ее кода, чтобы новички могли понять, как она работает.

Понимание стратегии Penny Jump

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

Например, предположим, что первоначальная глубина фондового рынка была такой: 200 | $1,01 x $1,03 | 200. Затем входит «слон» и выставляет ордер на покупку 3000 акций по 1,01 доллара за штуку. В этот момент глубина рынка изменится на 3 200 | $1,01 x $1,03 | 200 . Это действие похоже на введение «слона», который становится центром внимания других участников рынка.

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

Основная идея стратегии Penny Jump

Основная идея стратегии Penny Jump заключается в том, что как только на рынке появляется «крупный игрок» и поддерживает определенную цену (например, 1,01 доллара), высокочастотные трейдеры быстро повышают свою ставку на один цент, например, до 1,02 доллара. Это связано с тем, что высокочастотные трейдеры понимают, что появление крупного игрока означает, что на этом ценовом уровне есть сильная поддержка покупателей, поэтому они стараются внимательно следить за ними в надежде на рост цены. Когда цена действительно вырастает до $1,03 x $1,05, высокочастотные трейдеры могут быстро продать и получить прибыль в размере $0,01.

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

Анализ кода стратегии Penny Jump

Исходный код стратегии: https://www.fmz.com/strategy/358

Приведенный выше код стратегии является примером, используемым для реализации стратегии Penny Jump. Ниже приведено подробное объяснение кода, позволяющее новичкам понять, как он работает:var Counter = {
i: 0,
w: 0,
f: 0
};

// Variables
var InitAccount = null;

function CancelAll() {
while (true) {
var orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
exchange.CancelOrder(orders[i].Id);
}
Sleep(Interval);
}
}

function updateStatus(msg) {
LogStatus(«Number of debugging sessions:», Counter.i, «succeeded:», Counter.w, «failed:», Counter.f, «\n»+msg+»#0000ff\n»+new Date());
}

function main() {
if (DisableLog) {
EnableLog(false);
}
CancelAll();
InitAccount = _C(exchange.GetAccount);
Log(InitAccount);
var i = 0;
var locks = 0;
while (true) {
Sleep(Interval);
var depth = _C(exchange.GetDepth);
if (depth.Asks.length === 0 || depth.Bids.length === 0) {
continue;
}
updateStatus(«Searching within the elephant… Buy one: » + depth.Bids[0].Price + «, Sell one:» + depth.Asks[0].Price + «, Lock times: » + locks);
var askPrice = 0;
for (i = 0; i < depth.Asks.length; i++) {
if (depth.Asks[i].Amount >= Lot) {
askPrice = depth.Asks[i].Price;
break;
}
}
if (askPrice === 0) {
continue;
}
var elephant = null;
// skip Bids[0]
for (i = 1; i < depth.Bids.length; i++) {
if ((askPrice — depth.Bids[i].Price) > ElephantSpace) {
break;
}
if (depth.Bids[i].Amount >= ElephantAmount) {
elephant = depth.Bids[i];
break;
}
}

if (!elephant) {
locks = 0;
continue;
}
locks++;
if (locks < LockCount) {
continue;
}
locks = 0;

updateStatus(«Debug the elephant… The elephant is in gear » + i + «, » + JSON.stringify(elephant));
exchange.Buy(elephant.Price + PennyTick, Lot, «Bids[» + i + «]», elephant);
var ts = new Date().getTime();
while (true) {
Sleep(CheckInterval);
var orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
if ((new Date().getTime() — ts) > WaitInterval) {
for (var i = 0; i < orders.length; i++) {
exchange.CancelOrder(orders[i].Id);
}
}
}
var account = _C(exchange.GetAccount);
var opAmount = _N(account.Stocks — InitAccount.Stocks);
if (opAmount < 0.001) {
Counter.f++;
Counter.i++;
continue;
}
updateStatus(«Successful payment: » + opAmount +», Start taking action…»);
exchange.Sell(elephant.Price + (PennyTick * ProfitTick), opAmount);
var success = true;
while (true) {
var depth = _C(exchange.GetDepth);
if (depth.Bids.length > 0 && depth.Bids[0].Price <= (elephant.Price-(STTick*PennyTick))) {
success = false;
updateStatus(«Didn’t get it, start to stop loss, currently buying one: » + depth.Bids[0].Price);
CancelAll();
account = _C(exchange.GetAccount);
var opAmount = _N(account.Stocks — InitAccount.Stocks);
if (opAmount < 0.001) {
break;
}
exchange.Sell(depth.Bids[0].Price, opAmount);
}
var orders = _C(exchange.GetOrders);
if (orders.length === 0) {
break;
}
Sleep(CheckInterval);
}
if (success) {
Counter.w++;
} else {
Counter.f++;
}
Counter.i++;
var account = _C(exchange.GetAccount);
LogProfit(account.Balance — InitAccount.Balance, account);
}
}

Я разберу предоставленный вами код стратегии строка за строкой, чтобы помочь вам детально разобраться в ее работе.var Counter = {
i: 0,
w: 0,
f: 0
};

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

  • i: Представляет общее количество транзакций.
  • w: Представляет количество успешных транзакций.
  • f: Представляет количество неудачных транзакций.

Эти атрибуты будут записываться и обновляться в процессе исполнения стратегии.var InitAccount = null;

Эта строка кода инициализирует переменную с именем InitAccount, которая будет хранить информацию об учетной записи, когда стратегия начнет выполняться.function CancelAll() {
while (true) {
var orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
for (var i = 0; i < orders.length; i++) {
exchange.CancelOrder(orders[i].Id);
}
Sleep(Interval);
}
}

Это функция с именем CancelAll() ее целью является отмена всех неисполненных ордеров на рынке. Объясним его функции шаг за шагом:

  • while (true): Это бесконечный цикл, он будет продолжаться до тех пор, пока не закончатся незавершенные ордера.
  • var orders = _C(exchange.GetOrders): В этой строке кода используется биржа. Функция GetOrders извлекает все отложенные ордера на текущем счете и сохраняет их в переменной orders.
  • if (orders.length == 0): Эта строка кода проверяет наличие незавершенных заказов. Если длина массива ордеров равна 0, то это означает, что незавершенных ордеров нет и цикл будет прерван (прерван).
  • for for (var i = 0; i < orders.length; i++): Это цикл for, который перебирает все незавершенные ордера.
  • exchange.CancelOrder(orders[i].Id): В этой строке кода используется обмен. CancelOrder() для отмены каждого ордера по его идентификатору.
  • Sleep(Interval) строка кода вводит период ожидания, приостанавливающийся на определенное время (в миллисекундах), чтобы гарантировать, что операция отмены ордеров не будет слишком частой.

Эта строка кода вводит период ожидания, приостанавливающийся на определенное время (в миллисекундах), чтобы гарантировать, что операция отмены ордеров не будет слишком частой.function updateStatus(msg) {
LogStatus(«Number of debugging sessions:», Counter.i, «succeeded:», Counter.w, «failed:», Counter.f, «\n» + msg + «#0000ff\n» + new Date());
}

Это функция updateStatus(msg) которая используется для обновления и записи информации о состоянии транзакции. Он принимает параметр msg, который обычно содержит информацию о текущем состоянии рынка. К специфическим операциям функции относятся:

Использование функции LogStatus() для записи информации, отображаемой в строке состояния во время исполнения стратегии. В нем отображается текст о количестве сделок, успешных и неудачных.
Добавляется параметр msg, который содержит информацию о текущем состоянии рынка.
Текущая метка времени (new Date() добавляется для отображения информации о времени.
Целью данной функции является запись и обновление информации о состоянии транзакций для мониторинга и анализа во время исполнения стратегии.function main() {
if (DisableLog) {
EnableLog(false);
}
CancelAll();
InitAccount = _C(exchange.GetAccount);
Log(InitAccount);
var i = 0;
var locks = 0;
while (true) {
Sleep(Interval);
var depth = _C(exchange.GetDepth);
if (depth.Asks.length === 0 || depth.Bids.length === 0) {
continue;
}
updateStatus(«Searching within the elephant… Buy one: » + depth.Bids[0].Price + «, Sell one:» + depth.Asks[0].Price + «, Lock times: » + locks);
var askPrice = 0;
for (i = 0; i < depth.Asks.length; i++) {
if (depth.Asks[i].Amount >= Lot) {
askPrice = depth.Asks[i].Price;
break;
}
}
if (askPrice === 0) {
continue;
}
var elephant = null;
// skip Bids[0]
for (i = 1; i < depth.Bids.length; i++) {
if ((askPrice — depth.Bids[i].Price) > ElephantSpace) {
break;
}
if (depth.Bids[i].Amount >= ElephantAmount) {
elephant = depth.Bids[i];
break;
}
}

if (!elephant) {
locks = 0;
continue;
}
locks++;
if (locks < LockCount) {
continue;
}
locks = 0;

updateStatus(«Debug the elephant… The elephant is in gear » + i + «, » + JSON.stringify(elephant));
exchange.Buy(elephant.Price + PennyTick, Lot, «Bids[» + i + «]», elephant);
var ts = new Date().getTime();
while (true) {
Sleep(CheckInterval);
var orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
if ((new Date().getTime() — ts) > WaitInterval) {
for (var i = 0; i < orders.length; i++) {
exchange.CancelOrder(orders[i].Id);
}
}
}
var account = _C(exchange.GetAccount);
var opAmount = _N(account.Stocks — InitAccount.Stocks);
if (opAmount < 0.001) {
Counter.f++;
Counter.i++;
continue;
}
updateStatus(«Successful payment: » + opAmount +», Start taking action…»);
exchange.Sell(elephant.Price + (PennyTick * ProfitTick), opAmount);
var success = true;
while (true) {
var depth = _C(exchange.GetDepth);
if (depth.Bids.length > 0 && depth.Bids[0].Price <= (elephant.Price-(STTick*PennyTick))) {
success = false;
updateStatus(«Didn’t get it, start to stop loss, currently buying one: » + depth.Bids[0].Price);
CancelAll();
account = _C(exchange.GetAccount);
var opAmount = _N(account.Stocks — InitAccount.Stocks);
if (opAmount < 0.001) {
break;
}
exchange.Sell(depth.Bids[0].Price, opAmount);
}
var orders = _C(exchange.GetOrders);
if (orders.length === 0) {
break;
}
Sleep(CheckInterval);
}
if (success) {
Counter.w++;
} else {
Counter.f++;
}
Counter.i++;
var account = _C(exchange.GetAccount);
LogProfit(account.Balance — InitAccount.Balance, account);
}
}

Это основная исполнительная функция main() стратегии, которая содержит основную логику стратегии. Объясним его работу построчно:

  • if (DisableLog): Эта строка кода проверяет, имеет ли переменная if (DisableLog) значение true, и если да, то отключает запись журнала. Это необходимо для того, чтобы стратегия не записывала ненужные журналы.
  • CancelAll(): Вызовите ранее описанную функцию CancelAll()), чтобы убедиться в отсутствии незавершенных ордеров.
  • InitAccount = _C(exchange.GetAccount): Эта строка кода извлекает текущую информацию об учетной записи и сохраняет ее в переменной InitAccount. Это будет использоваться для записи состояния счета, когда стратегия начнет выполняться.
  • var i = 0; and var locks var locks = 0;: Инициализирует две переменные, i и locks, которые будут использоваться в последующей логике стратегии.
  • while (true): Это бесконечный цикл, в основном используемый для непрерывного выполнения стратегий.

Далее мы объясним основную логику стратегии в цикле while (true) строка за строкой.while (true) {
Sleep(Interval);
var depth = _C(exchange.GetDepth);
if (depth.Asks.length === 0 || depth.Bids.length === 0) {
continue;
}
updateStatus(«Searching within the elephant… Buy one: » + depth.Bids[0].Price + «, Sell one:» + depth.Asks[0].Price + «, Lock times: » + locks);

  • Sleep(Interval) строка кода позволяет стратегии находиться в спящем режиме в течение определенного периода времени, чтобы контролировать частоту выполнения стратегии. Параметр Interval определяет интервал сна (в миллисекундах).
  • var depth = _C(exchange.GetDepth): Получить текущую информацию о глубине рынка, включая цены и количество ордеров на продажу и покупку. Эта информация будет храниться в переменной depth.
  • if (depth.Asks.length === 0 || depth.Bids.length === 0): Эта строка кода проверяет информацию о глубине рынка, гарантируя, что существуют как ордера на продажу, так и ордера на покупку. Если одного из них нет, это говорит о том, что на рынке может не хватить торговой информации, поэтому стратегия продолжит ждать.
  • updateStatus("Searching within the elephant... Buy one: " + depth.Bids[0].Price + ", Sell one:" + depth.Asks[0].Price + ", Lock times: " + locks) Эта строка кода вызывает функцию updateStatus для обновления информации о состоянии стратегии. Он записывает текущее состояние рынка, включая самую высокую цену предложения, самую низкую цену предложения и ранее заблокированное время (блокировки).

var askPrice = 0;
for (i = 0; i < depth.Asks.length; i++) {
if (depth.Asks[i].Amount >= Lot) {
askPrice = depth.Asks[i].Price;
break;
}
}
if (askPrice === 0) {
continue;
}
var elephant = null;

  • var askPrice = 0;: Инициализируем переменную askPrice, она будет использоваться для хранения цены ордеров на продажу, которые удовлетворяют условиям.
  • for (i = 0; i < depth.Asks.length; i++): Это цикл for, используемый для обхода информации о цене и количестве рыночных ордеров на продажу.
  • if (depth.Asks[i].Amount >= Lot): В цикле проверяется, больше ли количество каждого ордера на продажу или равно указанному лоту (количеству рук). Если это так, сохраните цену этого ордера на продажу в askPrice и завершите цикл.
  • if (askPrice if (askPrice === 0): Если ордера на продажу, удовлетворяющие условиям, не найдены (askPrice по-прежнему равен 0), стратегия продолжит ждать и пропускать последующие операции.
  • var elephant = null;: Инициализируйте переменную elephant, она будет использоваться для хранения информации об ордере на покупку, идентифицированной как «elephant».

for (i = 1; i < depth.Bids.length; i++) {
if ((askPrice — depth.Bids[i].Price) > ElephantSpace) {
break;
}
if (depth.Bids[i].Amount >= ElephantAmount) {
elephant = depth.Bids[i];
break;
}
}

if (!elephant) {
locks = 0;
continue;
}
locks++;
if (locks < LockCount) {
continue;
}
locks = 0;

Продолжайте просматривать информацию о цене и количестве рыночных ордеров на покупку, пропуская первый ордер на покупку (Bids[0]).

  • if ((askPrice - depth.Bids[i].Price) > ElephantSpace)Price) > ElephantSpace): Проверьте, не больше ли разрыв между текущей ценой bid и askPrice, чем у ElephantSpace. Если это так, то это говорит о том, что он находится достаточно далеко от «слона», и стратегия больше не будет продолжать поиски.
  • if (depth.Bids[i].Amount >= ElephantAmount) ElephantAmount): Проверьте, больше ли количество текущего ордера на покупку или равно ElephantAmount. Если это так, сохраните информацию об ордере на покупку в переменной elephant.
  • if (!elephant): Если «слон» не найден, сбросьте счетчик блокировок на 0 и продолжайте ожидание.
  • locks++: Если «слон» найден, увеличьте количество блокировок. Это необходимо для того, чтобы стратегия выполнялась только после подтверждения существования «слона» несколько раз в течение определенного периода времени.
  • if (locks < LockCount): проверяет, соответствует ли количество раз блокировки требованию if (locks < LockCount) Если это не так, продолжайте ждать.

updateStatus(«Debug the elephant… The elephant is in gear » + i + «, » + JSON.stringify(elephant));
exchange.Buy(elephant.Price + PennyTick, Lot, «Bids[» + i + «]», elephant);
var ts = new Date().getTime();
while (true) {
Sleep(CheckInterval);
var orders = _C(exchange.GetOrders);
if (orders.length == 0) {
break;
}
if ((new Date().getTime() — ts) > WaitInterval) {
for (var i = 0; i < orders.length; i++) {
exchange.CancelOrder(orders[i].Id);
}
}
}

  • updateStatus("Debug the elephant... The elephant is in gear " + i + ", " + JSON.stringify(elephant)): вызов функции updateStatus для записи текущего состояния стратегии, включая положение шестеренки найденного «слона» и связанную с ним информацию. Это будет отображаться в строке состояния стратегии.
  • exchange.Buy(elephant.Price + PennyTick, Lot, "Bids[" + i + "]", elephant): Использовать биржу. Функция покупки для покупки найденного «слона». Цена покупки — слон. Price + PennyTick, количество покупки равно Lot, и опишите операцию покупки как «Bids[» + i + «]».
  • var ts = new Date().getTime(): Получение временной метки текущего времени для последующего вычисления временных интервалов.
  • while (true): Вход в новый бесконечный цикл, используемый для ожидания исполнения ордеров на покупку «слона».
  • Sleep(CheckInterval) Стратегия переходит в спящий режим на некоторое время, чтобы контролировать частоту проверки статуса ордера.
  • var orders = _C(exchange.GetOrders): Получить всю информацию о заказе по текущему счету.
  • if (orders.length == 0): Проверяет, есть ли незавершенные ордера, если нет, то прерываем цикл.
  • (new Date().getTime() - ts) > WaitInterval: Вычислить временной интервал между текущим временем и моментом покупки «слона». Если он превышает WaitInterval, это означает, что время ожидания истекло.
  • for (var i = 0; i < orders.length; i++): Пройти по всем незавершенным ордерам.
  • exchange.CancelOrder(orders[i].Id): Используйте биржу. Функция CancelOrder для отмены каждого незавершенного ордера.

var account = _C(exchange.GetAccount);
var opAmount = _N(account.Stocks — InitAccount.Stocks);
if (opAmount < 0.001) {
Counter.f++;
Counter.i++;
continue;
}
updateStatus(«Successful payment: » + opAmount + «, Start taking action…»);
exchange.Sell(elephant.Price + (PennyTick * ProfitTick), opAmount);
var success = true;
while (true) {
var depth = _C(exchange.GetDepth);
if (depth.Bids.length > 0 && depth.Bids[0].Price <= (elephant.Price — (STTick * PennyTick))) {
success = false;
updateStatus(«Didn’t get it, start to stop loss, currently buying one: » + depth.Bids[0].Price);
CancelAll();
account = _C(exchange.GetAccount);
var opAmount = _N(account.Stocks — InitAccount.Stocks);
if (opAmount < 0.001) {
break;
}
exchange.Sell(depth.Bids[0].Price, opAmount);
}
var orders = _C(exchange.GetOrders);
if (orders.length === 0) {
break;
}
Sleep(CheckInterval);
}
if (success) {
Counter.w++;
} else {
Counter.f++;
}
Counter.i++;
var account = _C(exchange.GetAccount);
LogProfit(account.Balance — InitAccount.Balance, account);
}

  • var account = _C(exchange.GetAccount): Получение информации о текущем счете
  • var opAmount = _N(account.Stocks - InitAccount.Stocks): Рассчитать изменение активов счета после покупки «слона». Если изменение меньше 0,001, это указывает на то, что покупка не удалась, увеличьте количество сбоев и перейдите к следующему циклу.
  • updateStatus("Successful payment: " + opAmount + ", Start taking action...") Записать информацию об успешной покупке «слона», включая количество приобретенных товаров.
  • exchange.Sell(elephant.Price + (PennyTick * ProfitTick), opAmount): Использовать биржу. Функция Sell для продажи успешно купленного «слона» с целью получения прибыли. Цена продажи слона. Цена + (PennyTick * ProfitTick).

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

  • var depth = _C(exchange.GetDepth): Получить информацию о глубине рынка.
  • if (depth.Bids.length > 0 && depth.Bids[0].Price <= (elephant.Price - (STTick * PennyTick))): Проверьте информацию о стакане цен, если рыночная цена уже упала до уровня стоп-лосса, то выполните операцию стоп-лосс.
  • CancelAll(): вызов функции CancelAll() для отмены всех незавершенных ордеров, чтобы избежать риска позиции.
  • if (opAmount < 0.001) 0.001): Проверить количество покупки еще раз, если оно меньше 0.001, это означает, что покупка не удалась, выйти из цикла.
  • exchange.Sell(depth.Bids[0].Price, opAmount): Выполнить операцию стоп-лосс, продать оставшиеся активы по самой низкой цене на текущем рынке.

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

Это построчное объяснение всей стратегии. Основная идея этой стратегии заключается в том, чтобы найти «слонов» (крупные ордера на покупку) на рынке, покупать и продавать их, чтобы получить небольшую прибыль. Он включает в себя несколько важных параметров, таких как Lot, Error retry interval (Interval), ElephantAmount, ElephantSpace и т.д., для корректировки стратегии.

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

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

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

  1. Во-первых, стратегия проверит информацию о глубине рынка, чтобы понять текущую ситуацию с ордерами на продажу и покупку.
  2. Далее стратегия попытается найти ордера на продажу, которые соответствуют критериям, в частности, ордера на продажу с количеством, большим или равным лоту. Если будет найден соответствующий ордер на продажу, цена ордера на продажу будет записана как askPrice.
  3. Далее стратегия продолжит поиск «слонов» (большое количество ордеров на покупку). Он будет проходить по рыночным ордерам на покупку, пропуская первый (обычно самый дорогой ордер на покупку). Если он найдет «слона», который соответствует критериям, он запишет информацию о «слоне» и увеличит количество блокировок.
  4. Если последовательно будет найдено достаточное количество «слонов» (контролируется параметром LockCount), стратегия в дальнейшем выполнит следующие операции:
  • Вызовите функцию updateStatus, чтобы записать снаряжение и связанную с ним информацию о «слоне».
  • Воспользуйтесь биржей. Функция покупки для покупки «слона», с ценой покупки слона. Price + PennyTick и количество лота.
  • Запускаем новый бесконечный цикл ожидания исполнения ордера на покупку.
  • Проверьте статус заказа. Если она завершена, вырываем из петли.
  • Если время ожидания превышает установленный интервал (WaitInterval), отмените все незавершенные заказы.
  • Рассчитайте изменения активов счета после успешной покупки. Если изменение меньше 0,001, это означает, что покупка не удалась; Увеличьте количество сбоев и продолжите следующий цикл.
  • Записывайте информацию об успешных покупках «слонов», включая количество купленных товаров.

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

  • Получите информацию о глубине рынка, проверьте, достигла ли уже рыночная цена уровня стоп-лосса.
  • Если рыночная цена достигла или опустилась ниже уровня стоп-лосса, будет исполнена операция стоп-лосс, то есть оставшиеся активы будут проданы.
  • Вызовите функцию CancelAll, чтобы отменить все незавершенные ордера, снизив риск позиции.
  • Перепроверьте изменение активов на счете после успешной покупки. Если изменение меньше 0,001, это говорит о том, что покупка не удалась и выход из цикла.
  • Наконец, запишите, является ли транзакция успешной или нет, и обновите количество успешных и неудачных операций в зависимости от результатов транзакции.

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

Итог

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

Стратегия высокочастотной торговли товарными фьючерсами

Стратегия высокочастотной торговли товарными фьючерсами «Penny Jump», написанная на C++

Введение

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

Классификация высокочастотных стратегий

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

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

Что такое стратегия Penny Jump?

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

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

Принцип стратегии Penny Jump

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

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

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

Selling Price 400.3, Order volume 50; buying price 400.1, Order volume 10.

Внезапно этот громоздкий слон прыгнул на рынок, и цена предложения была отправлена по цене 400,1. В это время рынком становится:

Selling Price 400.3, Order volume 50; Buying price 400.1, Order volume 510.

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

Selling Price 400.3, Order volume 50; Buying price 400.2, Order volume 1,

Цена 400.1 становится ценой «Покупка 2» в глубине стакана. Тогда, если цена поднимется до 400,3, высокочастотный трейдер получит прибыль в размере 0,1.

Даже если цена не вырастет, в позиции «покупка 2» все равно есть «слон», удерживающий цену, и его можно быстро продать обратно слону по цене 400,1. Это общая идея стратегии Penny Jump. Его логика проста: отслеживая состояние рыночного ордера, спекулировать на намерениях оппонента, а затем взять на себя инициативу в построении выгодной позиции и, наконец, получить прибыль от небольшого спреда за короткий промежуток времени. Для этого «слона», поскольку он вывешивает на рынке огромное количество ордеров на покупку, он разоблачил свои торговые намерения, и естественно стал преследуемой мишенью высокочастотных трейдеров.

Реализация стратегии «Пенни Джамп»

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

В чем смысл поддержки механизма тестирования на истории «сначала цена, затем объем»? Понимать это можно так: вы отправляете отложенный ордер на покупку по цене 400,1, только когда цена продажи в глубине стакана составляет 400,1 или ниже, ваш отложенный ордер может быть закрыт (исполнен). Он вычисляет только ценовые данные отложенных ордеров и не вычисляет данные об объеме отложенных ордеров, которые соответствуют только ценовому приоритету (цена первая) в правилах сопоставления биржевых ордеров.

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

Кроме того, некоторые читатели могут обнаружить, что стратегия Penny Jump требует рыночных торговых возможностей, то есть рыночная потребность имеет как минимум два ценовых разрыва «прыжка». При нормальных обстоятельствах основной торговый контракт товарных фьючерсов относительно «занят». Разница между прыжком «Покупка 1» и «Продажа 1» заключается в том, что шансов на торговлю практически нет. Таким образом, мы вкладываем нашу энергию в субпервичный контракт, где торговля не слишком активна. Этот тип торгового контракта иногда имеет две или даже три возможности «прыжка». Например, в контракте MA («код метанола в китайских товарных фьючерсах») 1909 года имеет место следующая ситуация:

«Продажа 1» цена 2225 с объемом 551, «Покупка 1» цена 2223 с объемом 565, посмотрите вниз на несколько секунд. После того, как это произойдет, она исчезнет после нескольких тиков. В этом случае мы рассматриваем рынок как самокоррекцию. Что мы должны сделать, так это наверстать упущенное. До того, как рынок активно его скорректирует. Если бы мы сделали это вручную, это было бы невозможно, с помощью автоматической торговли мы можем сделать это возможным.

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

Далее мы наблюдаем разницу между предыдущими «Продажа 1» «Покупка 1» и «Покупка 1» «Продажа 1» сейчас. Для того, чтобы заполнить ценовой разрыв между рынком, если скорость достаточно высокая, его можно поставить во главу угла другие ордера. Кроме того, время удержания позиции очень короткое, с этой торговой логикой, после реализации стратегии, возьмите MA909 в качестве примера, реальный рыночный тест рекомендует Esunny вместо интерфейса CTP, механизм изменения позиции и ситуации с фондом для Esunny — это pushed-данные, очень подходящие для высокочастотной торговли.

Код стратегии

После очистки торговой логики мы можем использовать код для ее достижения. Так как на платформе FMZ Quant используют C++ примеров написания стратегии слишком мало, здесь мы используем C++ для написания этой стратегии, которая удобна для изучения каждому, а разновидностью являются товарные фьючерсы. Сначала откройте: fmz.com > Войти > панель управления > Библиотека стратегий > Новая стратегия > Нажмите на выпадающее меню в левом верхнем углу > Выберите C++ Чтобы приступить к написанию стратегии, обратите внимание на комментарии в коде ниже.

  • Шаг 1: Сначала постройте каркас стратегии, в котором определены класс HFT и основная функция. Первая строка в основной функции — очистить журнал. Это делается для того, чтобы очищать ранее запущенную информацию журнала при каждом перезапуске стратегии. Вторая строка предназначена для фильтрации некоторых сообщений об ошибках, которые не нужны, таких как задержка сети и появление некоторых подсказок, чтобы журнал записывал только важную информацию и выглядел более аккуратно; третья строка предназначена для вывода сообщения «Init OK», означающего, что программа начала запускаться. четвёртая строка — создать объект по классу HFT, а имя объекта — hft; пятая строка программы входит в цикл while, и всегда выполняет цикл в объекте hft Method, видно, что метод Loop является основной логикой этой программы. Строка 6 — еще одно печатное сообщение. При нормальных обстоятельствах программа не будет выполняться до строки 6. Если программа выполняется до строки 6, программа проверки завершена.

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

/ / Define the HFT class
Class HFT {
Public:
HFT() {
// Constructor
}

Int getTradingWeekDay() {
// Get the current day of the week to determine if it is a new K line
}

State getState() {
/ / Get order data
}

Void stop() {
// Print orders and positions
}

Bool Loop() {
// Strategy logic and placing orders
}
};

// main function
Void main() {
LogReset(); // clear the log
SetErrorFilter("ready|timeout"); // Filter error messages
Log("Init OK"); // Print the log
HFT hft; // Create HFT object
While (hft.Loop()); // enter loop
Log("Exit"); // Program exits, prints the log
}

Итак, давайте посмотрим, как реализован каждый из методов в этом классе HFT, и как работает самый основной метод Loop. Сверху донизу мы будем реализовывать конкретную реализацию каждого метода один за другим, и вы обнаружите, что оригинальная высокочастотная стратегия очень проста. Прежде чем говорить о классе HFT, мы сначала определили несколько глобальных переменных для хранения результатов вычисления hft-объекта. К ним относятся: хранение статуса ордера, статус позиции, удержание длинной позиции, удержание короткой позиции, цена покупки, количество покупки, цена продажи, количество продажи. Пожалуйста, ознакомьтесь с кодом ниже:

/ / Define the global enumeration type State
Enum State {
STATE_NA, // store order status
STATE_IDLE, // store position status
STATE_HOLD_LONG, // store long position directions
STATE_HOLD_SHORT, // store short position direction
};

/ / Define global floating point type variable
Typedef struct {
Double bidPrice; // store the buying price
Double bidAmount; // store the buying amount
Double askPrice; // store the selling price
Double askAmount; // store the selling amount
} Book;

С помощью приведенных выше глобальных переменных мы можем хранить результаты, вычисленные объектом hft, отдельно, что удобно для последующих вызовов программой. Далее мы поговорим о конкретной реализации каждого метода в классе HFT. Во-первых, первый HFT-метод является конструктором, который вызывает второй метод getTradingWeekDay и выводит результат в журнал. Второй метод getTradingWeekDay получает текущий день недели, чтобы определить, является ли он новой линией K. Это также очень просто реализовать, получить временную метку, рассчитать час и неделю и, наконец, вернуть количество недель; третий метод getState немного длинный, я просто опишу общую идею, для конкретного объяснения вы можете посмотреть комментарии в следующем блоке кодирования.

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

Public:
// Constructor
HFT() {
_tradingDay = getTradingWeekDay();
Log("current trading weekday", _tradingDay);
}

// Get the current day of the week to determine if it is a new K line
Int getTradingWeekDay() {
Int seconds = Unix() + 28800; // get the timestamp
Int hour = (seconds/3600)%24; // hour
Int weekDay = (seconds/(60*60*24))%7+4; // week
If (hour > 20) {
weekDay += 1;
}
Return weekDay;
}

/ / Get order data
State getState() {
Auto orders = exchange.GetOrders(); // Get all orders
If (!orders.Valid || orders.size() == 2) { // If there is no order or the length of the order data is equal to 2
Return STATE_NA;
}

Bool foundCover = false; // Temporary variable used to control the cancellation of all unexecuted orders
// Traverse the order array and cancel all unexecuted orders
For (auto &order : orders) {
If (order.Id == _coverId) {
If ((order.Type == ORDER_TYPE_BUY && order.Price < _book.bidPrice - _toleratePrice) ||
(order.Type == ORDER_TYPE_SELL && order.Price > _book.askPrice + _toleratePrice)) {
exchange.CancelOrder(order.Id, "Cancel Cover Order"); // Cancel order based on order ID
_countCancel++;
_countRetry++;
} else {
foundCover = true;
}
} else {
exchange.CancelOrder(order.Id); // Cancel order based on order ID
_countCancel++;
}
}
If (foundCover) {
Return STATE_NA;
}

// Get position data
Auto positions = exchange.GetPosition(); // Get position data
If (!positions.Valid) { // if the position data is empty
Return STATE_NA;
}

// Traverse the position array to get specific position information
For (auto &pos : positions) {
If (pos.ContractType == Symbol) {
_holdPrice = pos.Price;
_holdAmount = pos.Amount;
_holdType = pos.Type;
Return pos.Type == PD_LONG || pos.Type == PD_LONG_YD ? STATE_HOLD_LONG : STATE_HOLD_SHORT;
}
}
Return STATE_IDLE;
}

// Print orders and positions information
Void stop() {
Log(exchange.GetOrders()); // print order
Log(exchange.GetPosition()); // Print position
Log("Stop");
}

Наконец, мы сосредоточимся на том, как функция Loop управляет логикой стратегии и порядком. Если вы хотите увидеть более внимательно, вы можете обратиться к комментариям в коде. Сначала определите, связаны ли CTP-транзакция и рыночный сервер; затем получите доступный остаток на счете и получите количество недель; затем установите код разновидности для торговли, вызвав официальную функцию FMZ Quant SetContractType, и можете использовать эту функцию для возврата сведений о торговой разновидности; затем вызовите функцию GetDepth, чтобы получить данные о глубине текущего рынка. Данные глубины включают в себя: цену покупки, объем покупки, цену продажи, объем продажи и т. Д., И мы храним их с переменными, потому что они будут использоваться позже; Затем выведите эти данные порта в строку состояния, чтобы облегчить пользователю просмотр текущего состояния рынка; Код выглядит следующим образом:

// Strategy logic and placing order
Bool Loop() {
If (exchange.IO("status") == 0) { // If the CTP and the quote server are connected
LogStatus(_D(), "Server not connect ...."); // Print information to the status bar
Sleep(1000); // Sleep 1 second
Return true;
}

If (_initBalance == 0) {
_initBalance = _C(exchange.GetAccount).Balance; // Get account balance
}

Auto day = getTradingWeekDay(); // Get the number of weeks
If (day != _tradingDay) {
_tradingDay = day;
_countCancel = 0;
}

// Set the futures contract type and get the contract specific information
If (_ct.is_null()) {
Log(_D(), "subscribe", Symbol); // Print the log
_ct = exchange.SetContractType(Symbol); // Set futures contract type
If (!_ct.is_null()) {
Auto obj = _ct["Commodity"]["CommodityTickSize"];
Int volumeMultiple = 1;
If (obj.is_null()) { // CTP
Obj = _ct["PriceTick"];
volumeMultiple = _ct["VolumeMultiple"];
_exchangeId = _ct["ExchangeID"];
} else { // Esunny
volumeMultiple = _ct["Commodity"]["ContractSize"];
_exchangeId = _ct["Commodity"]["ExchangeNo"];
}
If (obj.is_null() || obj <= 0) {
Panic("PriceTick not found");
}
If (_priceTick < 1) {
exchange.SetPrecision(1, 0); // Set the decimal precision of the price and the quantity of the order.
}
_priceTick = double(obj);
_toleratePrice = _priceTick * TolerateTick;
_ins = _ct["InstrumentID"];
Log(_ins, _exchangeId, "PriceTick:", _priceTick, "VolumeMultiple:", volumeMultiple); // print the log
}
Sleep(1000); // Sleep 1 second
Return true;
}

// Check orders and positions to set status
Auto depth = exchange.GetDepth(); // Get depth data
If (!depth.Valid) { // if no depth data is obtained
LogStatus(_D(), "Market not ready"); // Print status information
Sleep(1000); // Sleep 1 second
Return true;
}
_countTick++;
_preBook = _book;
_book.bidPrice = depth.Bids[0].Price; // "Buying 1" price
_book.bidAmount = depth.Bids[0].Amount; // "Buying 1" amount
_book.askPrice = depth.Asks[0].Price; // "Selling 1" price
_book.askAmount = depth.Asks[0].Amount; // "Selling 1" amount
// Determine the state of the port data assignment
If (_preBook.bidAmount == 0) {
Return true;
}
Auto st = getState(); // get the order data

// Print the port data to the status bar
LogStatus(_D(), _ins, "State:", st,
"Ask:", depth.Asks[0].Price, depth.Asks[0].Amount,
"Bid:", depth.Bids[0].Price, depth.Bids[0].Amount,
"Cancel:", _countCancel,
"Tick:", _countTick);
}

После того, как мы сделали так много, мы, наконец, можем размещать заказы. Перед торговлей сначала мы оцениваем текущий статус удерживающей позиции программы (нет позиции удержания, ордера на длинную позицию, ордера на короткую позицию), здесь мы использовали if… В противном случае, если… в противном случае, если логическое управление. Они очень просты, если нет удерживающей позиции, позиция будет открыта в соответствии с логическим условием. Если есть удерживающая позиция, позиция будет закрыта в соответствии с логическим условием. Для того, чтобы облегчить понимание каждого, мы используем три абзаца, чтобы объяснить логику, Для открытия позиции часть:

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

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

Bool forceCover = _countRetry >= _retryMax; // Boolean value used to control the number of closings
If (st == STATE_IDLE) { // if there is no holding position
If (_holdAmount > 0) {
If (_countRetry > 0) {
_countLoss++; // failure count
} else {
_countWin++; // success count
}
Auto account = exchange.GetAccount(); // Get account information
If (account.Valid) { // If get account information
LogProfit(_N(account.Balance+account.FrozenBalance-_initBalance, 2), "Win:", _countWin, "Loss:", _countLoss); // Record profit value
}
}
_countRetry = 0;
_holdAmount = 0;

// Judging the status of withdrawal
If (_countCancel > _cancelMax) {
Log("Cancel Exceed", _countCancel); // Print the log
Return false;
}

Bool canDo = false; // temporary variable
If (abs(_book.bidPrice - _book.askPrice) > _priceTick * 1) { // If there is more than 2 hops between the current bid and ask price
canDo = true;
}
If (!canDo) {
Return true;
}

Auto bidPrice = depth.Bids[0].Price; // Buying 1 price
Auto askPrice = depth.Asks[0].Price; // Selling 1 price
Auto bidAmount = 1.0;
Auto askAmount = 1.0;

If (_preBook.bidPrice > _book.bidPrice && _book.askAmount < _book.bidAmount) { // If the previous buying price is greater than the current buying price and the current selling volume is less than the buying volume
bidPrice += _priceTick; // Set the opening long position price
bidAmount = 2; // set the opening long position volume
} else if (_preBook.askPrice < _book.askPrice && _book.bidAmount < _book.askAmount) { // If the previous selling price is less than the current selling price and the current buying volume is less than the selling volume
askPrice -= _priceTick; // set the opening short position volume
askAmount = 2; // set the opening short position volume
} else {
Return true;
}
Log(_book.bidPrice, _book.bidAmount, _book.askPrice, _book.askAmount); // Print current market data
exchange.SetDirection("buy"); // Set the order type to buying long
exchange.Buy(bidPrice, bidAmount); // buying long and open position
exchange.SetDirection("sell"); // Set the order type to selling short
exchange.Sell(askPrice, askAmount); // short sell and open position
}

Далее мы поговорим о том, как закрыть длинную позицию, сначала установить тип ордера в соответствии с текущим статусом позиции, а затем получить цену «Продажа 1». Если текущая цена «Продажа 1» больше, чем цена открытия покупки длинной позиции, установите цену закрытия длинной позиции. Если текущая цена «продажи 1» меньше цены открытия длинной позиции, сбросьте переменную количества закрытия на true, а затем закройте все длинные позиции. Часть кодирования, как показано ниже:

Else if (st == STATE_HOLD_LONG) { // if holding long position
exchange.SetDirection((_holdType == PD_LONG && _exchangeId == "SHFE") ? "closebuy_today" : "closebuy"); // Set the order type, and close position
Auto sellPrice = depth.Asks[0].Price; // Get "Selling 1" price
If (sellPrice > _holdPrice) { // If the current "selling 1" price is greater than the long position opening price
Log(_holdPrice, "Hit #ff0000"); // Print long position opening price
sellPrice = _holdPrice + ProfitTick; // Set closing long position price
} else if (sellPrice < _holdPrice) { // If the current "selling 1" price is less than the long position opening price
forceCover = true;
}
If (forceCover) {
Log("StopLoss");
}
_coverId = exchange.Sell(forceCover ? depth.Bids[0].Price : sellPrice, _holdAmount); // close long position
If (!_coverId.Valid) {
Return false;
}
}

Наконец, давайте посмотрим, как закрыть короткую позицию. Принцип противоположен вышеупомянутому закрытию длинной позиции. Во-первых, в соответствии с текущим статусом позиции, установите тип ордера, а затем получите цену «Покупка 1», если текущая цена «Покупка 1» меньше цены открытия короткой позиции, будет установлена цена закрытия короткой позиции. Если текущая цена «покупки 1» больше, чем цена открытия короткой позиции, сбросьте переменную количества закрытия на true, а затем закройте все короткие позиции.

Else if (st == STATE_HOLD_SHORT) { // if holding short position
exchange.SetDirection((_holdType == PD_SHORT && _exchangeId == "SHFE") ? "closesell_today" : "closesell"); // Set the order type, and close position
Auto buyPrice = depth.Bids[0].Price; // Get "buying 1" price
If (buyPrice < _holdPrice) { // If the current "buying 1" price is less than the opening short position price
Log(_holdPrice, "Hit #ff0000"); // Print the log
buyPrice = _holdPrice - ProfitTick; // Set the close short position price
} else if (buyPrice > _holdPrice) { // If the current "buying 1" price is greater than the opening short position price
forceCover = true;
}
If (forceCover) {
Log("StopLoss");
}
_coverId = exchange.Buy(forceCover ? depth.Asks[0].Price : buyPrice, _holdAmount); // close short position
If (!_coverId.Valid) {
Return false;
}
}

Выше приведен полный анализ этой стратегии. Нажмите здесь (https://www.fmz.com/strategy/163427), чтобы скопировать полный исходный код стратегии без настройки среды тестирования на FMZ Quant.

Результаты бэктеста

Торговая логика

Заявление о стратегии

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

Высокочастотные торговые сигналы на Python

Обзор

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

Извлечение данных с помощью Databento

Для этого примера мы будем использовать фьючерсный контракт E-mini S&P 500 (ES).

Пользоваться Databento очень просто. Вам просто нужно знать эти 4 вещи:

  • Идентификатор набора данных. ES торгуется на CME Globex. Мы получим его данные из основного канала CME, MDP 3.0, т.е. dataset='GLBX.MDP3'.
  • Требуемый формат и степень детализации (схема) данных. Нам нужно 10 уровней глубины рынка. Это можно указать schema='mbp-10'.
  • Система входных символов. Мы будем использовать контракт ведущего месяца, который может быть указан с помощью символов непрерывного контракта или stype_in='continuous'.
  • Символ. Это будет symbols=['ES.n.0'] Имейте в виду, что непрерывные символы привязаны к реальным символам и их исходным ценам без каких-либо корректировок. Если вы предпочитаете работать с необработанными символами, вы можете использовать stype_in='raw_symbol'‘ и symbols=['ESZ3'] чтобы получить идентичный результат, но это будет утомительно, если ваш анализ охватывает несколько ролловеров.

import databento as db

client = db.Historical(‘YOUR_API_KEY’)

# Get 10 levels of ES lead month
data = client.timeseries.get_range(
dataset=’GLBX.MDP3′,
schema=’mbp-10′,
start=»2023-12-06T14:30″,
end=»2023-12-06T20:30″,
symbols=[‘ES.n.0’],
stype_in=’continuous’,
)
df = data.to_df()

Построение целевого вектора

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

Точно так же мы прогнозируем доходность 500 сделок. Это упрощение, и вы можете попробовать другую цель. На практике сделки могут быть сгруппированы, так что у вас может не быть времени на действия в соответствии с прогнозом о 500 сделках, или ваша стратегия может быть построена таким образом, что не может монетизировать торговые события с нерегулярным временем поступления.# Filter out trades only
df = df[df.action == ‘T’]

# Get midprice returns with a forward markout of 500 trades
df[‘mid’] = (df[‘bid_px_00’] + df[‘ask_px_00’])/2
df[‘ret_500t’] = df[‘mid’].shift(-500) — df[‘mid’]

df = df.dropna()

Создание наших функций

Мы создадим две характеристики: асимметрию верхней части книги и дисбаланс порядка на верхних 10 уровнях книги.

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

Дисбаланс ордеров похож на асимметрию, за исключением того, что мы будем строить его из количества ордеров на каждом ценовом уровне, а не из совокупной глубины. Это поможет нам продемонстрировать предельную добавленную стоимость функций, основанных на данных стакана. Схема MBP-10 от Databento представляет собой гибрид между MBP с ограниченной глубиной (иногда называемой «L2») и порядковыми данными (иногда называемой «L3»), в том смысле, что она удобно включает количество ордеров на каждом ценовом уровне. Обозначается side_ct_xx, например, bid_ct_00 представляет собой количество ордеров по лучшей ставке.import numpy as np

# Depth imbalance on top level (‘skew’)
df[‘skew’] = np.log(df.bid_sz_00) — np.log(df.ask_sz_00)

# Order imbalance on top ten levels (‘imbalance’)
df[‘imbalance’] = np.log(df[list(df.filter(regex=’bid_ct_0[0-9]’))].sum(axis=1)) — \
np.log(df[list(df.filter(regex=’ask_ct_0[0-9]’))].sum(axis=1))

Разделение в выборке и вне выборки

Разделите данные на входящий и вневыборочный наборы.split = int(0.66 * len(df))
split -= split % 100
df_in = df.iloc[:split]
df_out = df.iloc[split:]

Построение нашего сигнала

На основе этих двух характеристик мы построим простой торговый сигнал.

Во-первых, мы проверим коллинеарность между нашими объектами, а также их корреляции с целью.corr = df_in[[‘skew’, ‘imbalance’, ‘ret_500t’]].corr()
print(corr.where(np.triu(np.ones(corr.shape)).astype(bool))) skew imbalance ret_500t
skew 1.0 0.474489 0.108694
imbalance NaN 1.000000 0.065495
ret_500t NaN NaN 1.000000

Как видно на рисунке, наша функция дисбаланса ордеров имеет лишь умеренную корреляцию с простым перекосом в верхней части книги. Оба имеют некоторую прогностическую ценность по сравнению с краткосрочными будущими доходами; В среде с низким соотношением сигнал/шум, такой как данные стакана, часто можно увидеть такие полезные функции с R² ≈ 0.01.

Далее мы обучим нашу модель на этих двух признаках. sklearn предоставляет различные модели регрессии с помощью простого API. Мы применим простую линейную регрессию к нашим данным в выборке, а затем сделаем прогноз на нашем наборе вне выборки.from sklearn.linear_model import LinearRegression

reg = LinearRegression(fit_intercept=False, positive=True)

reg.fit(df_in[[‘skew’]], df_in[‘ret_500t’])
pred_skew = reg.predict(df_out[[‘skew’]])

reg.fit(df_in[[‘imbalance’]], df_in[‘ret_500t’])
pred_imbalance = reg.predict(df_out[[‘imbalance’]])

reg.fit(df_in[[‘skew’, ‘imbalance’]], df_in[‘ret_500t’])
pred_combined = reg.predict(df_out[[‘skew’, ‘imbalance’]])

Вы можете легко переключаться на другие модели, что может быть более подходящим, особенно при наличии большего количества функций и нелинейных взаимодействий. Например, раскомментируйте следующие четыре строки, чтобы использовать деревья на основе гистограммы с усилением градиента, аналогичные LightGBM.# from sklearn.ensemble import HistGradientBoostingRegressor

# reg = HistGradientBoostingRegressor()
# reg.fit(df_in[[‘skew’, ‘imbalance’]], df_in[‘ret_500t’])
# pred_combined = reg.predict(df_out[[‘skew’, ‘imbalance’]])

Результаты

Мы можем сравнить распределение нашей цели с распределением каждого предиктора, суммируя цель с отсортированными значениями предиктора. Хороший сигнал должен иметь плавно растущую кривую.import pandas as pd
import plotly
import plotly.express as px
import plotly.io as pio

# pio.renderers.default = ‘notebook’
# pio.renderers.default = ‘iframe’

pct = np.arange(0, 100, step=100/len(df_out))

def get_cumulative_markout_pnl(pred):
df_pnl = pd.DataFrame({‘pred’: pred, ‘ret_500t’: df_out[‘ret_500t’].values})
df_pnl.loc[df_pnl[‘pred’] < 0, ‘ret_500t’] *= -1
df_pnl = df_pnl.sort_values(by=’pred’)
return df_pnl[‘ret_500t’].cumsum().values

results = pd.DataFrame({
‘pct’: pct,
‘skew’: get_cumulative_markout_pnl(pred_skew),
‘imbalance’: get_cumulative_markout_pnl(pred_imbalance),
‘combined’: get_cumulative_markout_pnl(pred_combined),
})

fig = px.line(
results, x=’pct’, y=[‘skew’, ‘imbalance’, ‘combined’],
title=’Forecasting with book skew vs. imbalance’,
labels={‘pct’: ‘Predictor value (percentile)’},
)

fig.update_yaxes(title_text=’Cumulative return’)

fig.update_layout(legend=dict(
orientation=»h»,
yanchor=»bottom»,
y=1.02,
xanchor=»right»,
x=1
))

fig.show()

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

Кумуляция разметки возвращает значения предиктора, отсортированные в порядке возрастания. Чем выше кривая, тем лучше.

Вы можете получить весь этот пример в виде одного файла на GitHub здесь.

Источник

Источник

Источник

Источник