Размышления о стратегии высокочастотного трейдинга (2)

Автор:Трава, Создано: 2023-08-04 16:14:27, Обновлено: 2023-09-19 09:08:17

img

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

Моделирование накопленного трафика

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

img

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

from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
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']

Если соединить транзакции по счетам на каждые интервалы 1s в объем сделок, удалить часть, в которой не было сделок, а также привести их в соответствие с распределением транзакций по счетам выше, то результат будет лучше. Если рассматривать транзакции в пределах 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]
buy_trades
agg_trade_id цены количество first_trade_id last_trade_id это_покупатель_производитель дата время транзакции интервал Дифференциация
2023-01-27 00:00:00.161 1138369 2.901 54.3 3806199 3806201 Неправда 2023-01-27 00:00:00.161 1674777600161 НаН 0.001
2023-01-27 00:00:04.140 1138370 2.901 291.3 3806202 3806203 Неправда 2023-01-27 00:00:04.140 1674777604140 3979.0 0.000
2023-01-27 00:00:04.339 1138373 2.902 55.1 3806205 3806207 Неправда 2023-01-27 00:00:04.339 1674777604339 199.0 0.001
2023-01-27 00:00:04.772 1138374 2.902 1032.7 3806208 3806223 Неправда 2023-01-27 00:00:04.772 1674777604772 433.0 0.000
2023-01-27 00:00:05.562 1138375 2.901 3.5 3806224 3806224 Неправда 2023-01-27 00:00:05.562 1674777605562 790.0 0.000
2023-01-27 23:59:57.739 1544370 3.572 394.8 5074645 5074651 Неправда 2023-01-27 23:59:57.739 1674863997739 1224.0 0.002
2023-01-27 23:59:57.902 1544372 3.573 177.6 5074652 5074655 Неправда 2023-01-27 23:59:57.902 1674863997902 163.0 0.001
2023-01-27 23:59:58.107 1544373 3.573 139.8 5074656 5074656 Неправда 2023-01-27 23:59:58.107 1674863998107 205.0 0.000
2023-01-27 23:59:58.302 1544374 3.573 60.5 5074657 5074657 Неправда 2023-01-27 23:59:58.302 1674863998302 195.0 0.000
2023-01-27 23:59:59.894 1544376 3.571 12.1 5074662 5074664 Неправда 2023-01-27 23:59:59.894 1674863999894 1592.0 0.000
#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)

png

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]) # 无修正

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)

png

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

img

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

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

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)

png

Взрыв цены на единую купюру

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

Результаты показывают, что доля не вызвавших удар достигает 77%, доля одного тика - 16,5%, 2 тика - 3,7%, 3 тика - 1,2% и более 4 тиков - менее 1%.

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

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)
diff_counts = buy_trades['diff'].value_counts()
diff_counts[diff_counts>10]/diff_counts.sum()
0.000    0.769965
0.001    0.165527
0.002    0.037826
0.003    0.012546
0.004    0.005986
0.005    0.003173
0.006    0.001964
0.007    0.001036
0.008    0.000795
0.009    0.000474
0.010    0.000227
0.011    0.000187
0.012    0.000087
0.013    0.000080
Name: diff, dtype: float64
diff_group = buy_trades.groupby('diff').agg({
    'quantity': 'mean',
    'diff': 'last',
})
diff_group['quantity'][diff_group['diff']>0][diff_group['diff']<0.01].plot(figsize=(10,5),grid=True);

png

Фиксированные ценовые шоки

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

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]
result_df['price_diff'][abs(result_df['price_diff'])<0.016].value_counts().sort_index().plot.bar(figsize=(10,5));

png

result_df['price_diff'].value_counts()[result_df['price_diff'].value_counts()>30]
 0.001    7176
-0.001    3665
 0.002    3069
-0.002    1536
 0.003    1260
 0.004     692
-0.003     608
 0.005     391
-0.004     322
 0.006     259
-0.005     192
 0.007     146
-0.006     112
 0.008      82
 0.009      75
-0.007      75
-0.008      65
 0.010      51
 0.011      41
-0.010      31
Name: price_diff, dtype: int64
diff_group = result_df.groupby('price_diff').agg({ 'quantity_sum': 'mean'})
diff_group[(diff_group.index>0) & (diff_group.index<0.015)].plot(figsize=(10,5),grid=True);

png

Ценовой шок на объемы сделок

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

Здесь данные проанализированы по 1s, на 100 штук в длину одного шага, и в этом диапазоне оценивается изменение цены. Приведены некоторые более ценные выводы:

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

img

Среди них, C обозначает изменение цены, Q - объем сделки.

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]
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()
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)

png

grouped_df.head(10)
количество_группа цена_разница количество_группа_центр
0 0-199 -0.000302 99.5
1 100-299 -0.000124 199.5
2 200-399 -0.000068 299.5
3 300-499 -0.000017 399.5
4 400-599 -0.000048 499.5
5 500-699 0.000098 599.5
6 600-799 0.000006 699.5
7 700-899 0.000261 799.5
8 800-999 0.000186 899.5
9 900-1099 0.000299 999.5

Первоначальное первоочередное место

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

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

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

img

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

Подведение итогов

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

#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)

png


Больше

Ок количественный 🐂🍺