Réflexion sur la stratégie de trading à haute fréquence (2)

Auteur:Le foin, Créé à partir de: 2023-08-04 16:14:27, Mis à jour à partir de: 2023-09-19 09:08:17

img

L'article traite principalement des stratégies de négociation à haute fréquence, en mettant l'accent sur la modélisation cumulative des transactions et des chocs de prix. Il propose un modèle de position de vente au détail optimal, basé sur une compréhension des transactions et des chocs de prix, pour tenter de trouver la position de vente optimale.

Modélisation cumulée des transactions

L'article précédent a donné l'expression de la probabilité d'une transaction unique supérieure à une certaine valeur:

img

Nous nous intéressons également à la répartition des transactions sur une période de temps, qui devrait être intuitivement liée à la fréquence des transactions et des commandes par transaction.

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']

Si l'on combine les transactions individuelles à chaque intervalle de 1s en quantité de transactions, en supprimant les parties qui n'ont pas été traitées, et en les ajustant à la distribution des transactions individuelles ci-dessus, on obtient un meilleur résultat. Si l'on considère toutes les transactions dans 1s comme des transactions individuelles, le problème devient un problème résolu. Mais lorsque le cycle est prolongé (par rapport à la fréquence des transactions), l'erreur augmente.

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
Nom de l'entreprise prix quantité première_échange_id dernière_échange_id est_acheteur_fabricant date de sortie Temps de transaction l'intervalle différent
2023-01-27 00:00:00.161 1138369 2.901 54.3 3806199 3806201 Faux 2023-01-27 00:00:00.161 1674777600161 N' est pas 0.001
2023-01-27 00:00:04.140 1138370 2.901 291.3 3806202 3806203 Faux 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 Faux 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 Faux 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 Faux 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 Faux 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 Faux 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 Faux 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 Faux 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 Faux 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

Maintenant, une formule générale est résumée pour les distributions de transactions cumulées en différentes périodes et est adaptée à la distribution des transactions individuelles, sans utiliser de statistiques séparées à chaque fois.

img

où avg_interval indique l'intervalle moyen des transactions individuelles, et avg_interval_T indique l'intervalle moyen des intervalles à estimer, ce qui est un peu détourné. Si nous voulons estimer l'intervalle de 1s, nous devons estimer l'intervalle moyen des événements qui sont inclus dans les 1s. Si la probabilité d'arrivée d'une commande est conforme à la distribution de Parsons, il devrait être possible de l'estimer directement, mais l'écart réel est très important, ce qui ne sera pas expliqué ici.

Notez que la probabilité d'une transaction à un intervalle de temps supérieur à une valeur spécifique et la probabilité d'une transaction réelle à cette position dans la profondeur devraient être très différentes, car plus le temps d'attente est long, plus il est probable que le carnet d'ordres change et que les transactions entraînent également des changements de profondeur, de sorte que la probabilité d'une transaction à la même profondeur varie en temps réel avec la mise à jour des données.

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

Les prix des transactions monétaires sont en baisse

Les données de transaction sont un trésor, et il y a beaucoup de données à extraire. Nous devrions être très attentifs à l'impact des commandes sur le prix, ce qui affecte la position des listes de la stratégie. De même, en fonction des données agrégées de transact_time, calculer la différence entre le dernier prix et le premier prix, si il n'y a qu'une seule commande, la différence est de 0.

Les résultats montrent que le taux d'absence de choc est de 77%, le taux de 1 tick est de 16,5%, le taux de 2 ticks est de 3,7%, le taux de 3 ticks est de 1,2%, et le taux de 4 ticks ou plus est inférieur à 1%.

Les volumes de transactions causant la différence de prix correspondante ont été statistiquement calculés, en éliminant les distorsions de choc trop importantes, et correspondent essentiellement à une relation linéaire, avec une fluctuation de prix d'environ 1 tick pour 1000 volumes. On peut également comprendre que la quantité moyenne de ticks près du prix par plateau est d'environ 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

Les chocs de prix à intervalles fixes

La différence ici est qu'il y aura des impacts négatifs, bien sûr, car il n'y a que des paiements statistiques ici, la position symétrique sera plus grande d'un tick. Continuer à observer la relation entre le volume de transactions et l'impact, ne résulte que des impacts statistiques supérieurs à 0, la conclusion est que les ordres individuels sont similaires, mais aussi une relation linéaire approximative, chaque tick nécessite environ 2000 volumes.

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

Le choc des prix des transactions

Le premier demande le volume de transaction requis pour un changement de tick, mais n'est pas précis car il est basé sur l'hypothèse que le choc a déjà eu lieu.

Ici, les données sont échantillonnées par 1s, soit une étape par 100 quantités, et les variations de prix dans cette fourchette sont statistiquement calculées.

  1. Lorsque le volume des commandes est inférieur à 500, la variation de prix attendue est la baisse, ce qui est conforme aux attentes, après tout, il y a aussi des commandes vendues qui affectent le prix.
  2. La relation est linéaire lorsque le volume de transactions est faible, c'est-à-dire que plus le volume de transactions est important, plus les prix augmentent.
  3. Les plus grandes transactions de paiement, les plus grandes variations de prix, ce qui représente souvent une rupture de prix, après la rupture, les prix peuvent revenir, ainsi que l'échantillonnage à intervalles fixes, ce qui provoque une instabilité des données.
  4. L'attention devrait être portée sur la partie supérieure du graphique à puces, c'est-à-dire la partie où le volume des transactions correspond à la hausse des prix.
  5. Pour cette seule paire de transactions, donnez une version grossière de la relation entre le volume des transactions et les variations de prix:

img

Dans ce cas, le point C représente la variation du prix et le point Q représente le volume de transactions.

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)
quantité_groupe prix_diff quantité_group_center
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

La première position la plus élevée

Avec une modélisation grossière du volume des transactions et du volume des transactions correspondant à l'impact des prix, il semble être possible de calculer la position optimale de l'annonce.

  1. Supposons que le prix retourne à sa valeur initiale après le choc (ce qui est bien sûr peu probable et nécessite une réanalyse des changements de prix après le choc)
  2. Supposons que la répartition des volumes et de la fréquence des commandes pendant cette période soit conforme aux prévisions (ce qui n'est pas non plus exact, car les estimations sont faites en une journée et les transactions sont nettement regroupées).
  3. Supposons qu'il n'y ait qu'une seule vente dans le temps d'analyses, puis une liquidation.
  4. Supposons que l'ordre soit passé et que d'autres paiements continuent à augmenter les prix, en particulier lorsque la quantité est faible, ce qui est évident, mais cet effet est ignoré ici, en supposant simplement qu'il y aura un retour.

Commencez par écrire un simple rendement attendu, c'est-à-dire la probabilité d'un paiement cumulé supérieur à Q en 1s, multiplié par le rendement attendu (c'est-à-dire le prix du choc):

img

Selon l'image, le rendement attendu est maximal autour de 2500 et est environ 2,5 fois le volume moyen des transactions. C'est-à-dire que les ordres de vente devraient être suspendus à la position 2500. Il est nécessaire de souligner à nouveau que le volume des transactions dans l'axe horizontal représente 1s, ne peut pas être simplement égal à la position de profondeur.

Résumé

Nous avons découvert que la distribution des transactions à différents intervalles de temps est une simple mise à l'échelle de la distribution des transactions individuelles. Nous avons également fait un modèle de rendement attendu simple basé sur les chocs de prix et la probabilité de transaction. Le résultat de ce modèle est conforme à nos attentes.

#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


Plus de

Quantitatif de l'oc 🐂🍺