
Le trading par paires est un excellent exemple de développement d’une stratégie de trading basée sur l’analyse mathématique. Dans cet article, nous allons vous montrer comment exploiter les données pour créer et automatiser une stratégie de trading par paires.
Supposons que vous ayez une paire d’investissements X et Y qui ont une certaine corrélation sous-jacente, comme les deux sociétés produisant le même produit, comme Pepsi et Coca-Cola. Vous souhaitez que le rapport de prix ou la base (également appelé spread) entre les deux reste constant au fil du temps. Cependant, l’écart entre les deux paires peut diverger de temps à autre en raison de changements temporaires de l’offre et de la demande, tels que des ordres d’achat/vente importants pour une cible d’investissement, une réaction à des nouvelles importantes concernant l’une des sociétés, etc. Dans ce cas, un investissement évolue vers le haut et l’autre vers le bas l’un par rapport à l’autre. Si vous vous attendez à ce que cette divergence se normalise au fil du temps, vous pouvez repérer une opportunité de trading (ou opportunité d’arbitrage). De telles opportunités d’arbitrage sont partout sur le marché des devises numériques ou sur le marché des contrats à terme sur matières premières nationales, comme la relation entre le BTC et les actifs refuges ; la relation entre la farine de soja, l’huile de soja et les variétés de soja dans les contrats à terme.
Lorsqu’il y a une différence de prix temporaire, la transaction vendra l’investissement le plus performant (l’investissement qui a augmenté) et achètera l’investissement le moins performant (l’investissement qui a baissé). Vous pouvez être sûr qu’il y a une différence entre les deux investissements. L’écart se traduira éventuellement par une baisse de l’investissement le plus performant ou par une remontée de l’investissement le moins performant, ou par les deux. Votre transaction vous rapportera de l’argent dans tous ces scénarios. Si les investissements augmentent ou diminuent simultanément sans modifier la différence entre eux, vous ne gagnerez ni ne perdrez d’argent.
Par conséquent, le trading par paires est une stratégie de trading neutre sur le marché qui permet aux traders de profiter de presque toutes les conditions de marché : tendance à la hausse, tendance à la baisse ou latérale.
Tout d’abord, pour que le travail soit fluide, nous devons créer notre environnement de recherche. Dans cet article, nous utilisons la plateforme quantitative Inventor (FMZ.COM) pour créer l’environnement de recherche, principalement pour pouvoir utiliser l’API pratique et rapide interface et encapsulation de cette plateforme ultérieurement. Système Docker complet.
Dans le nom officiel de la plateforme quantitative Inventor, ce système Docker est appelé système hôte.
Pour plus d’informations sur la manière de déployer des hôtes et des robots, veuillez vous référer à mon article précédent : https://www.fmz.com/bbs-topic/4140
Les lecteurs qui souhaitent acheter leur propre hébergeur de déploiement de serveur de cloud computing peuvent se référer à cet article : https://www.fmz.com/bbs-topic/2848
Après avoir déployé avec succès le service de cloud computing et le système hôte, nous installerons l’outil Python le plus puissant : Anaconda
Afin d’obtenir tous les environnements de programme pertinents requis pour cet article (bibliothèques dépendantes, gestion des versions, etc.), le moyen le plus simple est d’utiliser Anaconda. Il s’agit d’un écosystème de science des données Python packagé et d’un gestionnaire de dépendances.
Pour la méthode d’installation d’Anaconda, veuillez vous référer au guide officiel d’Anaconda : https://www.anaconda.com/distribution/
Cet article utilisera également numpy et pandas, deux bibliothèques très populaires et importantes dans le calcul scientifique Python.
Pour le travail de base ci-dessus, vous pouvez également vous référer à mon article précédent, qui présente comment configurer l’environnement Anaconda et les deux bibliothèques numpy et pandas. Pour plus de détails, veuillez consulter : https://www.fmz.com/digest- sujet/4169
Ensuite, utilisons le code pour implémenter « deux cibles d’investissement hypothétiques »
import numpy as np
import pandas as pd
import statsmodels
from statsmodels.tsa.stattools import coint
# just set the seed for the random number generator
np.random.seed(107)
import matplotlib.pyplot as plt
Oui, nous utiliserons également matplotlib, une bibliothèque de graphiques très célèbre en Python.
Générons un actif d’investissement hypothétique X et simulons le tracé de ses rendements quotidiens à l’aide d’une distribution normale. Nous effectuons ensuite une somme cumulative pour obtenir la valeur X quotidienne.
# Generate daily returns
Xreturns = np.random.normal(0, 1, 100)
# sum them and shift all the prices up
X = pd.Series(np.cumsum(
Xreturns), name='X')
+ 50
X.plot(figsize=(15,7))
plt.show()

Cible d’investissement X, simulez et dessinez son rendement quotidien via une distribution normale
Nous générons maintenant Y qui est fortement corrélé à X, donc le prix de Y devrait évoluer de manière très similaire aux variations de X. Nous modélisons cela en prenant X, en le décalant vers le haut et en ajoutant du bruit aléatoire tiré d’une distribution normale.
noise = np.random.normal(0, 1, 100)
Y = X + 5 + noise
Y.name = 'Y'
pd.concat([X, Y], axis=1).plot(figsize=(15,7))
plt.show()

Cibles d’investissement cointégrantes X et Y
La cointégration est très similaire à la corrélation, ce qui signifie que le rapport entre deux séries de données varie autour de la moyenne. Les deux séries Y et X suivent les principes suivants :
Y = ⍺ X + e
où ⍺ est un rapport constant et e est le bruit.
Pour une paire de négociation entre deux séries temporelles, la valeur attendue du ratio au fil du temps doit converger vers la moyenne, c’est-à-dire qu’elles doivent être cointégrées. Les séries temporelles que nous avons construites ci-dessus sont cointégrées. Nous allons maintenant dessiner l’échelle entre les deux afin que nous puissions voir à quoi cela ressemblera.
(Y/X).plot(figsize=(15,7))
plt.axhline((Y/X).mean(), color='red', linestyle='--')
plt.xlabel('Time')
plt.legend(['Price Ratio', 'Mean'])
plt.show()

Le ratio et la moyenne des prix de deux investissements cointégrés
Un moyen pratique de tester cela est d’utiliser statsmodels.tsa.stattools. Nous devrions voir une valeur p très faible car nous avons créé artificiellement deux séries de données aussi cointégrées que possible.
# compute the p-value of the cointegration test
# will inform us as to whether the ratio between the 2 timeseries is stationary
# around its mean
score, pvalue, _ = coint(X,Y)
print pvalue
Le résultat est : 1,81864477307e-17
Bien que la corrélation et la cointégration soient similaires en théorie, elles ne sont pas identiques. Voyons des exemples de séries de données corrélées mais non cointégrées, et vice versa. Vérifions d’abord la corrélation de la série que nous venons de générer.
X.corr(Y)
Le résultat est: 0,951
Comme prévu, ce chiffre est très élevé. Mais qu’en est-il de deux séries corrélées mais non cointégrées ? Un exemple simple est celui de deux séries de données qui divergent.
ret1 = np.random.normal(1, 1, 100)
ret2 = np.random.normal(2, 1, 100)
s1 = pd.Series( np.cumsum(ret1), name='X')
s2 = pd.Series( np.cumsum(ret2), name='Y')
pd.concat([s1, s2], axis=1 ).plot(figsize=(15,7))
plt.show()
print 'Correlation: ' + str(X_diverging.corr(Y_diverging))
score, pvalue, _ = coint(X_diverging,Y_diverging)
print 'Cointegration test p-value: ' + str(pvalue)

Deux séries liées (non co-intégrées)
Coefficient de corrélation : 0,998 Valeur p du test de cointégration : 0,258
Des exemples simples de cointégration sans corrélation sont une série distribuée normalement et une onde carrée.
Y2 = pd.Series(np.random.normal(0, 1, 800), name='Y2') + 20
Y3 = Y2.copy()
Y3[0:100] = 30
Y3[100:200] = 10
Y3[200:300] = 30
Y3[300:400] = 10
Y3[400:500] = 30
Y3[500:600] = 10
Y3[600:700] = 30
Y3[700:800] = 10
Y2.plot(figsize=(15,7))
Y3.plot()
plt.ylim([0, 40])
plt.show()
# correlation is nearly zero
print 'Correlation: ' + str(Y2.corr(Y3))
score, pvalue, _ = coint(Y2,Y3)
print 'Cointegration test p-value: ' + str(pvalue)

Corrélation : 0,007546 Test de cointégration p-value : 0,0
La corrélation est très faible, mais la valeur p montre une cointégration parfaite !
Étant donné que deux séries temporelles cointégrées (telles que X et Y ci-dessus) se rapprochent et s’éloignent l’une de l’autre, il y a des moments où il y a une base haute et une base basse. Nous effectuons du trading de paires en achetant un investissement et en vendant un autre. De cette façon, si les deux objectifs d’investissement baissent ou augmentent ensemble, nous ne gagnons ni ne perdons d’argent, c’est-à-dire que nous sommes neutres sur le marché.
Pour revenir à X et Y dans Y = ⍺ X + e ci-dessus, nous gagnons de l’argent en faisant bouger le ratio (Y/X) autour de sa moyenne ⍺. Pour ce faire, nous notons que lorsque X Lorsque la valeur de ⍺ est trop élevé ou trop bas, la valeur de ⍺ est trop élevée ou trop basse :
Ratio long : c’est lorsque le ratio ⍺ est petit et que nous nous attendons à ce qu’il augmente. Dans l’exemple ci-dessus, nous ouvrons une position en allant long sur Y et short sur X.
Ratio court : c’est lorsque le ratio ⍺ est grand et que nous nous attendons à ce qu’il diminue. Dans l’exemple ci-dessus, nous ouvrons une position en vendant Y à découvert et en achetant X à long terme.
Notez que nous avons toujours une « position couverte » : si le long sous-jacent perd de la valeur, la position short gagne de l’argent, et vice versa, nous sommes donc immunisés contre les mouvements globaux du marché.
Lorsque les actifs X et Y évoluent l’un par rapport à l’autre, nous gagnons ou perdons de l’argent.
La meilleure façon de procéder est de commencer par les transactions que vous soupçonnez d’être cointégrées et d’effectuer des tests statistiques. Si vous effectuez un test statistique sur toutes les paires de trading, vous serezBiais de comparaisons multiplesvictime de.
Biais de comparaisons multiplesfait référence à la situation dans laquelle le risque de générer à tort une valeur p significative augmente lors de l’exécution de nombreux tests, car nous devons exécuter un grand nombre de tests. Si nous exécutons ce test 100 fois sur des données aléatoires, nous devrions voir 5 valeurs p inférieures à 0,05. Si vous comparez n instruments pour la cointégration, vous effectuerez n(n-1)/2 comparaisons et vous verrez de nombreuses valeurs p incorrectes, qui augmenteront à mesure que la taille de votre échantillon de test augmentera. Et augmentera. Pour éviter cela, sélectionnez quelques paires de trading dont vous avez des raisons de croire qu’elles sont susceptibles d’être cointégrées, puis testez-les individuellement. Cela réduira considérablementBiais de comparaisons multiples。
Essayons donc de trouver des instruments qui présentent une cointégration. Prenons un panier d’actions technologiques américaines à grande capitalisation du S&P 500. Ces instruments opèrent dans des segments de marché similaires et présentent une cointégration. Nous analysons la liste des instruments de trading et testons la cointégration entre toutes les paires.
La matrice de score du test de cointégration renvoyée, la matrice de valeur p et toutes les correspondances par paires avec une valeur p inférieure à 0,05 sont incluses.Cette méthode est sujette à un biais de comparaison multiple, c’est pourquoi, en pratique, il faut effectuer une deuxième validation. Dans cet article, pour la commodité de notre explication, nous choisissons d’ignorer cela dans les exemples.
def find_cointegrated_pairs(data):
n = data.shape[1]
score_matrix = np.zeros((n, n))
pvalue_matrix = np.ones((n, n))
keys = data.keys()
pairs = []
for i in range(n):
for j in range(i+1, n):
S1 = data[keys[i]]
S2 = data[keys[j]]
result = coint(S1, S2)
score = result[0]
pvalue = result[1]
score_matrix[i, j] = score
pvalue_matrix[i, j] = pvalue
if pvalue < 0.02:
pairs.append((keys[i], keys[j]))
return score_matrix, pvalue_matrix, pairs
Remarque : nous avons inclus l’indice de référence du marché (SPX) dans nos données. Le marché détermine le flux de nombreux instruments et vous pouvez souvent trouver deux instruments qui semblent être cointégrés ; mais en fait, ils ne sont pas cointégrés l’un avec l’autre, mais cointégrés avec le marché. C’est ce qu’on appelle une variable de confusion. Il est important d’examiner la participation du marché dans toute relation que vous trouvez.
from backtester.dataSource.yahoo_data_source import YahooStockDataSource
from datetime import datetime
startDateStr = '2007/12/01'
endDateStr = '2017/12/01'
cachedFolderName = 'yahooData/'
dataSetId = 'testPairsTrading'
instrumentIds = ['SPY','AAPL','ADBE','SYMC','EBAY','MSFT','QCOM',
'HPQ','JNPR','AMD','IBM']
ds = YahooStockDataSource(cachedFolderName=cachedFolderName,
dataSetId=dataSetId,
instrumentIds=instrumentIds,
startDateStr=startDateStr,
endDateStr=endDateStr,
event='history')
data = ds.getBookDataByFeature()['Adj Close']
data.head(3)

Essayons maintenant de trouver des paires de trading cointégrées en utilisant notre méthode.
# Heatmap to show the p-values of the cointegration test
# between each pair of stocks
scores, pvalues, pairs = find_cointegrated_pairs(data)
import seaborn
m = [0,0.2,0.4,0.6,0.8,1]
seaborn.heatmap(pvalues, xticklabels=instrumentIds,
yticklabels=instrumentIds, cmap=’RdYlGn_r’,
mask = (pvalues >= 0.98))
plt.show()
print pairs
[('ADBE', 'MSFT')]

Il semble que « ADBE » et « MSFT » soient cointégrés. Jetons un œil au prix pour nous assurer qu’il a réellement du sens.
S1 = data['ADBE']
S2 = data['MSFT']
score, pvalue, _ = coint(S1, S2)
print(pvalue)
ratios = S1 / S2
ratios.plot()
plt.axhline(ratios.mean())
plt.legend([' Ratio'])
plt.show()

Graphique du rapport de prix entre MSFT et ADBE de 2008 à 2017
Ce ratio ressemble à une moyenne stable. Les ratios absolus ne sont pas très utiles statistiquement. Il est plus utile de normaliser notre signal en le considérant comme un score z. Le score Z est défini comme :
Z Score (Value) = (Value — Mean) / Standard Deviation
En pratique, nous essayons généralement d’appliquer une certaine expansion aux données, mais seulement si les données sont distribuées normalement. Cependant, de nombreuses données financières ne sont pas distribuées normalement. Nous devons donc être très prudents et ne pas simplement supposer la normalité ou une distribution particulière lors de la génération de statistiques. La véritable distribution des ratios peut avoir des queues épaisses, et les données qui tendent vers les extrêmes peuvent perturber notre modèle et entraîner d’énormes pertes.
def zscore(series):
return (series - series.mean()) / np.std(series)
zscore(ratios).plot()
plt.axhline(zscore(ratios).mean())
plt.axhline(1.0, color=’red’)
plt.axhline(-1.0, color=’green’)
plt.show()

Ratio Z-Price entre MSFT et ADBE de 2008 à 2017
Il est désormais plus facile de voir comment le ratio évolue autour de la moyenne, mais il a parfois tendance à présenter de grands écarts par rapport à la moyenne, ce que nous pouvons exploiter.
Maintenant que nous avons discuté des bases d’une stratégie de trading de paires et identifié les cibles de co-intégration en fonction de l’historique des prix, essayons de développer un signal de trading. Commençons par passer en revue les étapes de développement de signaux de trading à l’aide de techniques de données :
Collecter des données fiables et nettoyer les données
Créer des fonctions à partir de données pour identifier les signaux/logiques de trading
Les fonctionnalités peuvent être des moyennes mobiles ou des données de prix, des corrélations ou des ratios de signaux plus complexes - combinez-les pour créer de nouvelles fonctionnalités
Utilisez ces fonctionnalités pour générer des signaux de trading, c’est-à-dire quels signaux sont des positions d’achat, de vente ou de vente
Heureusement, nous disposons de la plateforme quantitative Inventor (fmz.com) pour compléter les quatre aspects ci-dessus à notre place. C’est une grande bénédiction pour les développeurs de stratégies. Nous pouvons consacrer notre énergie et notre temps à la logique stratégique, à la conception et à l’expansion fonctionnelle.
Sur la plateforme quantitative Inventor, il existe des interfaces packagées de divers échanges grand public. Il suffit d’appeler ces interfaces API. Le reste de la logique d’implémentation sous-jacente a été peaufiné par une équipe de professionnels.
Dans un souci d’exhaustivité logique et d’explication des principes, nous présenterons ces logiques sous-jacentes de manière détaillée, mais en fonctionnement réel, les lecteurs peuvent appeler directement l’interface API d’Inventor Quant pour compléter les quatre aspects ci-dessus.
Commençons :
Ici, nous essayons de créer un signal qui nous indique si le ratio sera un achat ou une vente au moment suivant, ce qui est notre variable prédictive Y :
Y = Ratio is buy (1) or sell (-1)
Y(t)= Sign(Ratio(t+1) — Ratio(t))
Notez que nous n’avons pas besoin de prédire le prix réel de l’actif sous-jacent, ni même la valeur réelle du ratio (bien que nous le puissions), nous devons simplement prédire la direction du ratio ensuite.
Inventor Quant est votre ami ! Il vous suffit de spécifier les instruments que vous souhaitez négocier et la source de données que vous souhaitez utiliser, et il extraira les données requises et les nettoiera pour les divisions de dividendes et d’instruments. Nos données ici sont donc déjà très propres.
Nous avons utilisé les données suivantes de Yahoo Finance pour les jours de négociation au cours des 10 dernières années (environ 2 500 points de données) : ouverture, clôture, plus haut, plus bas et volume
N’oubliez pas cette étape très importante qui consiste à tester la précision de votre modèle. Nous utilisons la répartition suivante des données entre train/validation/test
Training 7 years ~ 70%
Test ~ 3 years 30%
ratios = data['ADBE'] / data['MSFT']
print(len(ratios))
train = ratios[:1762]
test = ratios[1762:]
Idéalement, nous créerions également un ensemble de validation, mais nous ne le ferons pas pour l’instant.
Quelles pourraient être les fonctions concernées ? Nous voulons prédire la direction du changement de ratio. Nous avons vu que nos deux instruments sont cointégrés, donc ce ratio aura tendance à se déplacer et à revenir à la moyenne. Il semble que notre fonctionnalité devrait être une mesure de la moyenne du ratio, et la différence entre la valeur actuelle et la moyenne peut générer notre signal de trading.
Nous utilisons les fonctions suivantes :
Ratio de moyenne mobile sur 60 jours : une mesure de la moyenne mobile
Ratio de la moyenne mobile sur 5 jours : une mesure de la valeur actuelle de la moyenne
Écart type sur 60 jours
score z : (MA 5 jours - MA 60 jours) / ET 60 jours
ratios_mavg5 = train.rolling(window=5,
center=False).mean()
ratios_mavg60 = train.rolling(window=60,
center=False).mean()
std_60 = train.rolling(window=60,
center=False).std()
zscore_60_5 = (ratios_mavg5 - ratios_mavg60)/std_60
plt.figure(figsize=(15,7))
plt.plot(train.index, train.values)
plt.plot(ratios_mavg5.index, ratios_mavg5.values)
plt.plot(ratios_mavg60.index, ratios_mavg60.values)
plt.legend(['Ratio','5d Ratio MA', '60d Ratio MA'])
plt.ylabel('Ratio')
plt.show()

Rapport de prix de 60j et 5j MA
plt.figure(figsize=(15,7))
zscore_60_5.plot()
plt.axhline(0, color='black')
plt.axhline(1.0, color='red', linestyle='--')
plt.axhline(-1.0, color='green', linestyle='--')
plt.legend(['Rolling Ratio z-Score', 'Mean', '+1', '-1'])
plt.show()

Rapport de prix Z-score 60-5
Le score Z de la moyenne mobile fait vraiment ressortir la nature de retour à la moyenne du ratio !
Commençons par un modèle très simple. En regardant le graphique du score z, nous pouvons voir que chaque fois que le score z est trop élevé ou trop bas, il régresse. Utilisons +1/-1 comme seuils pour définir les valeurs trop élevées et trop basses, puis nous pouvons utiliser le modèle suivant pour générer des signaux de trading :
Lorsque z est inférieur à -1,0, le ratio est d’achat (1) car nous nous attendons à ce que z revienne à 0, donc le ratio augmente
Lorsque z est supérieur à 1,0, le ratio est de vente (-1) car nous nous attendons à ce que z revienne à 0, diminuant ainsi le ratio
Enfin, voyons l’impact réel de notre modèle sur des données réelles ? Voyons comment ce signal se comporte dans des ratios réels
# Plot the ratios and buy and sell signals from z score
plt.figure(figsize=(15,7))
train[60:].plot()
buy = train.copy()
sell = train.copy()
buy[zscore_60_5>-1] = 0
sell[zscore_60_5<1] = 0
buy[60:].plot(color=’g’, linestyle=’None’, marker=’^’)
sell[60:].plot(color=’r’, linestyle=’None’, marker=’^’)
x1,x2,y1,y2 = plt.axis()
plt.axis((x1,x2,ratios.min(),ratios.max()))
plt.legend([‘Ratio’, ‘Buy Signal’, ‘Sell Signal’])
plt.show()

Signaux de ratio de prix d’achat et de vente
Ce signal semble raisonnable, nous semblons vendre le ratio lorsqu’il est élevé ou en hausse (points rouges) et le racheter lorsqu’il est bas (points verts) et en baisse. Qu’est-ce que cela signifie pour l’objet même de nos transactions ? Voyons
# Plot the prices and buy and sell signals from z score
plt.figure(figsize=(18,9))
S1 = data['ADBE'].iloc[:1762]
S2 = data['MSFT'].iloc[:1762]
S1[60:].plot(color='b')
S2[60:].plot(color='c')
buyR = 0*S1.copy()
sellR = 0*S1.copy()
# When buying the ratio, buy S1 and sell S2
buyR[buy!=0] = S1[buy!=0]
sellR[buy!=0] = S2[buy!=0]
# When selling the ratio, sell S1 and buy S2
buyR[sell!=0] = S2[sell!=0]
sellR[sell!=0] = S1[sell!=0]
buyR[60:].plot(color='g', linestyle='None', marker='^')
sellR[60:].plot(color='r', linestyle='None', marker='^')
x1,x2,y1,y2 = plt.axis()
plt.axis((x1,x2,min(S1.min(),S2.min()),max(S1.max(),S2.max())))
plt.legend(['ADBE','MSFT', 'Buy Signal', 'Sell Signal'])
plt.show()

Signaux d’achat et de vente des actions MSFT et ADBE
Remarquez comment nous gagnons parfois de l’argent sur le « segment court », parfois sur le « segment long », et parfois sur les deux.
Nous sommes satisfaits du signal des données de formation. Voyons quel type de profit ce signal peut générer. Nous pouvons créer un backtester simple qui achète 1 ratio (achète 1 action ADBE et vend ratio x action MSFT) lorsque le ratio est faible et vend 1 ratio (vend 1 action ADBE et appelle ratio x action MSFT) et calcule les transactions PnL pour ces derniers rapports.
# Trade using a simple strategy
def trade(S1, S2, window1, window2):
# If window length is 0, algorithm doesn't make sense, so exit
if (window1 == 0) or (window2 == 0):
return 0
# Compute rolling mean and rolling standard deviation
ratios = S1/S2
ma1 = ratios.rolling(window=window1,
center=False).mean()
ma2 = ratios.rolling(window=window2,
center=False).mean()
std = ratios.rolling(window=window2,
center=False).std()
zscore = (ma1 - ma2)/std
# Simulate trading
# Start with no money and no positions
money = 0
countS1 = 0
countS2 = 0
for i in range(len(ratios)):
# Sell short if the z-score is > 1
if zscore[i] > 1:
money += S1[i] - S2[i] * ratios[i]
countS1 -= 1
countS2 += ratios[i]
print('Selling Ratio %s %s %s %s'%(money, ratios[i], countS1,countS2))
# Buy long if the z-score is < 1
elif zscore[i] < -1:
money -= S1[i] - S2[i] * ratios[i]
countS1 += 1
countS2 -= ratios[i]
print('Buying Ratio %s %s %s %s'%(money,ratios[i], countS1,countS2))
# Clear positions if the z-score between -.5 and .5
elif abs(zscore[i]) < 0.75:
money += S1[i] * countS1 + S2[i] * countS2
countS1 = 0
countS2 = 0
print('Exit pos %s %s %s %s'%(money,ratios[i], countS1,countS2))
return money
trade(data['ADBE'].iloc[:1763], data['MSFT'].iloc[:1763], 60, 5)
Le résultat est: 1783.375
Cette stratégie semble donc rentable ! Maintenant, nous pouvons optimiser davantage en modifiant la fenêtre de temps moyenne mobile, en modifiant les seuils d’achat/vente et de clôture des positions, etc. et vérifier les améliorations de performances sur les données de validation.
Nous pouvons également essayer des modèles plus complexes comme la régression logistique, SVM, etc. pour des prédictions 1/-1.
Maintenant, avançons ce modèle, ce qui nous amène à
Je voudrais ici mentionner la plateforme quantitative Inventor. Elle utilise un moteur de backtesting QPS/TPS haute performance pour reproduire fidèlement l’environnement historique, éliminer les pièges courants du backtesting quantitatif et découvrir rapidement les lacunes de la stratégie, afin de mieux fournir des résultats réels. -investissement en temps. Proposer de l’aide.
Afin d’expliquer le principe, cet article choisit de montrer la logique sous-jacente. Dans l’application pratique, il est recommandé aux lecteurs d’utiliser la plateforme quantitative Inventor. En plus de gagner du temps, l’important est d’améliorer le taux de tolérance aux pannes.
Le backtesting est simple. Nous pouvons utiliser la fonction ci-dessus pour afficher le PnL des données de test.
trade(data[‘ADBE’].iloc[1762:], data[‘MSFT’].iloc[1762:], 60, 5)
Le résultat est: 5262.868
Ce modèle est très bien réalisé ! C’est devenu notre premier modèle simple de trading de paires.
Avant de conclure, je veux parler spécifiquement du surajustement. Le surajustement est le piège le plus dangereux des stratégies de trading. Un algorithme de surajustement peut être extrêmement performant lors de backtests, mais échouer sur de nouvelles données invisibles, ce qui signifie qu’il ne révèle pas vraiment de tendances dans les données et n’a pas de réel pouvoir prédictif. Prenons un exemple simple.
Dans notre modèle, nous utilisons des estimations de paramètres glissants et espérons optimiser la longueur de la fenêtre temporelle. Nous pourrions décider de simplement itérer sur toutes les possibilités, sur des durées de fenêtre temporelle raisonnables, et de choisir la durée en fonction de laquelle notre modèle fonctionne le mieux. Ci-dessous, nous écrivons une boucle simple pour évaluer les longueurs de fenêtre temporelle en fonction du PNL des données d’entraînement et trouver la meilleure boucle.
# Find the window length 0-254
# that gives the highest returns using this strategy
length_scores = [trade(data['ADBE'].iloc[:1762],
data['MSFT'].iloc[:1762], l, 5)
for l in range(255)]
best_length = np.argmax(length_scores)
print ('Best window length:', best_length)
('Best window length:', 40)
Nous vérifions maintenant les performances du modèle sur les données de test, et nous voyons que cette longueur de fenêtre temporelle est loin d’être optimale ! C’est parce que notre choix initial est clairement sur-adapté aux données de l’échantillon.
# Find the returns for test data
# using what we think is the best window length
length_scores2 = [trade(data['ADBE'].iloc[1762:],
data['MSFT'].iloc[1762:],l,5)
for l in range(255)]
print (best_length, 'day window:', length_scores2[best_length])
# Find the best window length based on this dataset,
# and the returns using this window length
best_length2 = np.argmax(length_scores2)
print (best_length2, 'day window:', length_scores2[best_length2])
(40, 'day window:', 1252233.1395)
(15, 'day window:', 1449116.4522)
Il est évident que ce qui fonctionne bien pour nos données d’échantillon ne produit pas toujours de bons résultats à l’avenir. Juste pour tester, traçons les scores de longueur calculés à partir des deux ensembles de données
plt.figure(figsize=(15,7))
plt.plot(length_scores)
plt.plot(length_scores2)
plt.xlabel('Window length')
plt.ylabel('Score')
plt.legend(['Training', 'Test'])
plt.show()

Nous pouvons voir que tout ce qui se situe entre 20 et 50 est un bon choix pour la fenêtre temporelle.
Pour éviter le surajustement, nous pouvons utiliser le raisonnement économique ou les propriétés de l’algorithme pour choisir la longueur de la fenêtre temporelle. Nous pouvons également utiliser un filtre de Kalman, qui ne nécessite pas de spécifier une longueur ; cette méthode sera abordée plus tard dans un autre article.
Dans cet article, nous présentons quelques méthodes d’introduction simples pour démontrer le processus de développement d’une stratégie de trading. En pratique, des statistiques plus sophistiquées devraient être utilisées et vous pouvez envisager les options suivantes :
Exposant de Hurst
La demi-vie de retour à la moyenne déduite du processus d’Ornstein-Uhlenbeck
Filtre de Kalman