
Dans cet article, nous allons écrire une stratégie de day trading. Il utilisera le concept de trading classique des « paires de trading à retour à la moyenne ». Dans cet exemple, nous utiliserons deux fonds négociés en bourse (ETF), SPY et IWM, qui se négocient à la Bourse de New York (NYSE) et tentent de représenter les indices boursiers américains, le S&P 500 et le Russell 2000. .
La stratégie crée un « carry » en étant long sur un ETF et short sur un autre. Le ratio long-short peut être défini de plusieurs manières, par exemple en utilisant des méthodes de séries chronologiques de cointégration statistique. Dans ce scénario, nous calculerons le ratio de couverture entre SPY et IWM via une régression linéaire continue. Cela nous permettra de créer un « spread » entre SPY et IWM qui est normalisé à un score z. Lorsque le z-score dépasse un certain seuil, un signal de trading est généré car nous pensons que ce « spread » reviendra à la moyenne.
La raison d’être de cette stratégie est que SPY et IWM représentent à peu près le même scénario de marché, à savoir la performance du cours des actions d’un groupe de grandes et petites entreprises américaines. Le principe est que si vous acceptez la théorie du « retour à la moyenne » des prix, alors elle reviendra toujours, car les « événements » peuvent affecter le S&P 500 et le Russell 2000 séparément dans une très courte période de temps, mais le « différentiel de taux d’intérêt » entre ils reviendront toujours à la moyenne normale, et les séries de prix à long terme des deux sont toujours cointégrées.
Stratégie
La stratégie est exécutée comme suit :
Données - Obtenez des graphiques en chandeliers d’une minute de SPY et IWM d’avril 2007 à février 2014.
Traitement - Alignez correctement les données et supprimez les barres qui manquent les unes aux autres. (Si un côté manque, les deux côtés seront supprimés)
Spread - Le ratio de couverture entre deux ETF est calculé à l’aide d’une régression linéaire continue. Défini comme le coefficient de régression bêta utilisant une fenêtre rétrospective qui est avancée d’une barre et le coefficient de régression est recalculé. Par conséquent, le ratio de couverture βi, bi K-line est utilisé pour tracer la K-line en calculant le point de croisement de bi-1-k à bi-1.
Z-Score - La valeur du spread standard est calculée de la manière habituelle. Cela signifie soustraire la moyenne de l’écart (échantillon) et diviser par l’écart type de l’écart (échantillon). La raison pour laquelle cela est fait est de rendre le paramètre de seuil plus facile à comprendre puisque le score Z est une quantité sans dimension. J’ai intentionnellement introduit un « biais d’anticipation » dans les calculs pour montrer à quel point il peut être subtil. Essayez-le !
Trading - Les signaux longs sont générés lorsque la valeur du score z négatif tombe en dessous d’un seuil prédéterminé (ou post-optimisé), tandis que les signaux courts sont générés dans l’autre sens. Lorsque la valeur absolue du score z descend en dessous d’un seuil supplémentaire, un signal de fermeture de la position est généré. Pour cette stratégie, j’ai (un peu arbitrairement) choisi |z| = 2 comme seuil d’entrée et |z| = 1 comme seuil de sortie. En supposant que le retour à la moyenne joue un rôle dans le spread, ce qui précède devrait, espérons-le, capturer cette relation d’arbitrage et fournir un joli profit.
La meilleure façon de comprendre en profondeur une stratégie est peut-être de la mettre en œuvre. La section suivante détaille le code Python complet (fichier unique) utilisé pour implémenter cette stratégie de retour à la moyenne. J’ai ajouté des commentaires de code détaillés pour vous aider à mieux comprendre.
Implémentation Python
Comme pour tous les tutoriels Python/pandas, votre environnement Python doit être configuré comme décrit dans ce tutoriel. Une fois la configuration terminée, la première tâche consiste à importer les bibliothèques Python nécessaires. Ceci est nécessaire pour utiliser matplotlib et pandas.
Les versions spécifiques de la bibliothèque que j’utilise sont les suivantes :
Python - 2.7.3 NumPy - 1.8.0 pandas - 0.12.0 matplotlib - 1.1.0
Allons-y et importons ces bibliothèques :
# mr_spy_iwm.py
import matplotlib.pyplot as plt
import numpy as np
import os, os.path
import pandas as pd
La fonction suivante create_pairs_dataframe importe deux fichiers CSV contenant les chandeliers intrajournaliers de deux symboles. Dans notre cas, ce serait SPY et IWM. Il crée ensuite une « paire de trames de données » distincte qui utilise les index des deux fichiers d’origine. Leurs horodatages peuvent varier en raison de transactions manquées et d’erreurs. C’est l’un des principaux avantages de l’utilisation d’une bibliothèque d’analyse de données comme Pandas. Nous traitons le code « boilerplate » de manière très efficace.
# mr_spy_iwm.py
def create_pairs_dataframe(datadir, symbols):
"""Creates a pandas DataFrame containing the closing price
of a pair of symbols based on CSV files containing a datetime
stamp and OHLCV data."""
# Open the individual CSV files and read into pandas DataFrames
print "Importing CSV data..."
sym1 = pd.io.parsers.read_csv(os.path.join(datadir, '%s.csv' % symbols[0]),
header=0, index_col=0,
names=['datetime','open','high','low','close','volume','na'])
sym2 = pd.io.parsers.read_csv(os.path.join(datadir, '%s.csv' % symbols[1]),
header=0, index_col=0,
names=['datetime','open','high','low','close','volume','na'])
# Create a pandas DataFrame with the close prices of each symbol
# correctly aligned and dropping missing entries
print "Constructing dual matrix for %s and %s..." % symbols
pairs = pd.DataFrame(index=sym1.index)
pairs['%s_close' % symbols[0].lower()] = sym1['close']
pairs['%s_close' % symbols[1].lower()] = sym2['close']
pairs = pairs.dropna()
return pairs
L’étape suivante consiste à effectuer une régression linéaire continue entre SPY et IWM. Dans ce scénario, IWM est le prédicteur (« x ») et SPY est la réponse (« y »). J’ai défini une fenêtre de rétrospection par défaut de 100 chandeliers. Comme mentionné ci-dessus, ce sont les paramètres de la stratégie. Pour qu’une stratégie soit considérée comme robuste, nous aimerions idéalement voir un rapport de rendement convexe sur la période rétrospective (ou une autre mesure de performance). Par conséquent, à un stade ultérieur du code, nous effectuerons une analyse de sensibilité en faisant varier la période de rétrospection dans le périmètre.
Après avoir calculé les coefficients bêta glissants dans le modèle de régression linéaire pour SPY-IWM, ajoutez-le à la paire DataFrame et supprimez les lignes vides. Cela construit le premier ensemble de chandeliers, qui est égal à la mesure tronquée de la longueur de rétrospection. Nous avons ensuite créé un spread entre les deux ETF, une unité de SPY et une unité de -βi de IWM. De toute évidence, ce n’est pas un scénario réaliste, car nous utilisons une petite quantité d’IWM, ce qui n’est pas possible dans une mise en œuvre pratique.
Enfin, nous créons le score z du spread, calculé en soustrayant la moyenne du spread et en normalisant par l’écart type du spread. Il est important de noter qu’il existe ici un « biais prospectif » assez subtil. Je l’ai laissé intentionnellement dans le code parce que je voulais souligner à quel point il est facile de faire des erreurs comme celle-ci dans la recherche. Calculer la moyenne et l’écart type de l’ensemble de la série temporelle étalée. Si ces informations sont censées refléter une véritable exactitude historique, elles ne peuvent pas être obtenues car elles utilisent implicitement des informations provenant du futur. Par conséquent, nous devrions utiliser la moyenne mobile et l’écart type pour calculer le score z.
# mr_spy_iwm.py
def calculate_spread_zscore(pairs, symbols, lookback=100):
"""Creates a hedge ratio between the two symbols by calculating
a rolling linear regression with a defined lookback period. This
is then used to create a z-score of the 'spread' between the two
symbols based on a linear combination of the two."""
# Use the pandas Ordinary Least Squares method to fit a rolling
# linear regression between the two closing price time series
print "Fitting the rolling Linear Regression..."
model = pd.ols(y=pairs['%s_close' % symbols[0].lower()],
x=pairs['%s_close' % symbols[1].lower()],
window=lookback)
# Construct the hedge ratio and eliminate the first
# lookback-length empty/NaN period
pairs['hedge_ratio'] = model.beta['x']
pairs = pairs.dropna()
# Create the spread and then a z-score of the spread
print "Creating the spread/zscore columns..."
pairs['spread'] = pairs['spy_close'] - pairs['hedge_ratio']*pairs['iwm_close']
pairs['zscore'] = (pairs['spread'] - np.mean(pairs['spread']))/np.std(pairs['spread'])
return pairs
Dans create_long_short_market_signals, créez des signaux de trading. Ils sont calculés en mesurant la valeur du score z dépassant un seuil. Lorsque la valeur absolue du z-score est inférieure ou égale à un autre seuil (plus petit), un signal de fermeture de la position est donné.
Pour y parvenir, il est nécessaire d’établir si la stratégie de trading est « d’ouverture » ou de « fermeture » pour chaque ligne K. Long_market et short_market sont deux variables définies pour suivre les positions longues et courtes. Malheureusement, cette méthode de calcul est lente car elle est beaucoup plus simple à programmer de manière itérative qu’une approche vectorisée. Même si un graphique en chandeliers d’une minute nécessite environ 700 000 points de données par fichier CSV, il est toujours relativement rapide à calculer sur mon ancien ordinateur de bureau !
Pour itérer sur un DataFrame pandas (une opération certes peu courante), il est nécessaire d’utiliser la méthode iterrows, qui fournit un générateur itérable :
# mr_spy_iwm.py
def create_long_short_market_signals(pairs, symbols,
z_entry_threshold=2.0,
z_exit_threshold=1.0):
"""Create the entry/exit signals based on the exceeding of
z_enter_threshold for entering a position and falling below
z_exit_threshold for exiting a position."""
# Calculate when to be long, short and when to exit
pairs['longs'] = (pairs['zscore'] <= -z_entry_threshold)*1.0
pairs['shorts'] = (pairs['zscore'] >= z_entry_threshold)*1.0
pairs['exits'] = (np.abs(pairs['zscore']) <= z_exit_threshold)*1.0
# These signals are needed because we need to propagate a
# position forward, i.e. we need to stay long if the zscore
# threshold is less than z_entry_threshold by still greater
# than z_exit_threshold, and vice versa for shorts.
pairs['long_market'] = 0.0
pairs['short_market'] = 0.0
# These variables track whether to be long or short while
# iterating through the bars
long_market = 0
short_market = 0
# Calculates when to actually be "in" the market, i.e. to have a
# long or short position, as well as when not to be.
# Since this is using iterrows to loop over a dataframe, it will
# be significantly less efficient than a vectorised operation,
# i.e. slow!
print "Calculating when to be in the market (long and short)..."
for i, b in enumerate(pairs.iterrows()):
# Calculate longs
if b[1]['longs'] == 1.0:
long_market = 1
# Calculate shorts
if b[1]['shorts'] == 1.0:
short_market = 1
# Calculate exists
if b[1]['exits'] == 1.0:
long_market = 0
short_market = 0
# This directly assigns a 1 or 0 to the long_market/short_market
# columns, such that the strategy knows when to actually stay in!
pairs.ix[i]['long_market'] = long_market
pairs.ix[i]['short_market'] = short_market
return pairs
À ce stade, nous mettons à jour les paires pour contenir les signaux longs et courts réels, ce qui nous permet de déterminer si nous devons ouvrir une position. Nous devons maintenant créer un portefeuille pour suivre la valeur marchande des positions. La première tâche consiste à créer une colonne de position qui combine des signaux longs et courts. Cela contiendra une liste d’éléments de (1,0,-1) où 1 représente une position longue, 0 ne représente aucune position (qui doit être fermée) et -1 représente une position courte. Les colonnes sym1 et sym2 représentent la valeur marchande des positions SPY et IWM à la fin de chaque chandelier.
Une fois les valeurs de marché des ETF créées, nous les additionnons pour produire la valeur de marché totale à la fin de chaque chandelier. Il est ensuite converti en valeur de retour via la méthode pct_change de cet objet. Les lignes de code suivantes nettoient les entrées erronées (éléments NaN et inf) et calculent enfin la courbe d’équité complète.
# mr_spy_iwm.py
def create_portfolio_returns(pairs, symbols):
"""Creates a portfolio pandas DataFrame which keeps track of
the account equity and ultimately generates an equity curve.
This can be used to generate drawdown and risk/reward ratios."""
# Convenience variables for symbols
sym1 = symbols[0].lower()
sym2 = symbols[1].lower()
# Construct the portfolio object with positions information
# Note that minuses to keep track of shorts!
print "Constructing a portfolio..."
portfolio = pd.DataFrame(index=pairs.index)
portfolio['positions'] = pairs['long_market'] - pairs['short_market']
portfolio[sym1] = -1.0 * pairs['%s_close' % sym1] * portfolio['positions']
portfolio[sym2] = pairs['%s_close' % sym2] * portfolio['positions']
portfolio['total'] = portfolio[sym1] + portfolio[sym2]
# Construct a percentage returns stream and eliminate all
# of the NaN and -inf/+inf cells
print "Constructing the equity curve..."
portfolio['returns'] = portfolio['total'].pct_change()
portfolio['returns'].fillna(0.0, inplace=True)
portfolio['returns'].replace([np.inf, -np.inf], 0.0, inplace=True)
portfolio['returns'].replace(-1.0, 0.0, inplace=True)
# Calculate the full equity curve
portfolio['returns'] = (portfolio['returns'] + 1.0).cumprod()
return portfolio
La fonction principale relie le tout. Les fichiers CSV intrajournaliers sont situés dans le chemin datadir. Assurez-vous de modifier le code suivant pour pointer vers votre répertoire spécifique.
Afin de déterminer la sensibilité de la stratégie à la période rétrospective, il est nécessaire de calculer une gamme de mesures de performance rétrospective. J’ai sélectionné le pourcentage de rendement total final du portefeuille comme mesure de performance et comme plage rétrospective.[[50 200] avec un incrément de 10. Vous pouvez voir dans le code ci-dessous que la fonction précédente est encapsulée dans une boucle for sur cette plage et les autres seuils restent les mêmes. La tâche finale consiste à créer un graphique linéaire des rétrospectives par rapport aux retours à l’aide de matplotlib :
# mr_spy_iwm.py
if __name__ == "__main__":
datadir = '/your/path/to/data/' # Change this to reflect your data path!
symbols = ('SPY', 'IWM')
lookbacks = range(50, 210, 10)
returns = []
# Adjust lookback period from 50 to 200 in increments
# of 10 in order to produce sensitivities
for lb in lookbacks:
print "Calculating lookback=%s..." % lb
pairs = create_pairs_dataframe(datadir, symbols)
pairs = calculate_spread_zscore(pairs, symbols, lookback=lb)
pairs = create_long_short_market_signals(pairs, symbols,
z_entry_threshold=2.0,
z_exit_threshold=1.0)
portfolio = create_portfolio_returns(pairs, symbols)
returns.append(portfolio.ix[-1]['returns'])
print "Plot the lookback-performance scatterchart..."
plt.plot(lookbacks, returns, '-o')
plt.show()

Vous pouvez maintenant voir un graphique des rétrospectives et des retours. Notez qu’il existe un maximum « global » pour les rétrospections, égal à 110 barres. Si nous voyons une situation où les rétrospections n’ont rien à voir avec les retours, c’est parce que :
Analyse de sensibilité de la période de rétrospection du ratio de couverture de régression linéaire SPY-IWM
Aucun article sur le backtesting ne serait complet sans une courbe de profit en pente ascendante ! Donc, si vous souhaitez tracer les rendements des bénéfices cumulés en fonction du temps, vous pouvez utiliser le code suivant. Il tracera le portefeuille final généré à partir de l’étude des paramètres rétrospectifs. Il est donc nécessaire de choisir le lookback en fonction du graphique que vous souhaitez visualiser. Ce graphique présente également les rendements de SPY sur la même période pour faciliter la comparaison :
# mr_spy_iwm.py
# This is still within the main function
print "Plotting the performance charts..."
fig = plt.figure()
fig.patch.set_facecolor('white')
ax1 = fig.add_subplot(211, ylabel='%s growth (%%)' % symbols[0])
(pairs['%s_close' % symbols[0].lower()].pct_change()+1.0).cumprod().plot(ax=ax1, color='r', lw=2.)
ax2 = fig.add_subplot(212, ylabel='Portfolio value growth (%%)')
portfolio['returns'].plot(ax=ax2, lw=2.)
fig.show()
Le graphique de la courbe des capitaux propres ci-dessous a une période rétrospective de 100 jours :

Analyse de sensibilité de la période de rétrospection du ratio de couverture de régression linéaire SPY-IWM
Il faut noter que la baisse du SPY a été assez importante en 2009 pendant la crise financière. La stratégie se trouve également dans une période turbulente durant cette phase. Notez également que les performances se sont détériorées au cours de l’année dernière en raison de la nature fortement orientée du SPY au cours de cette période, reflétant le S&P 500.
Notez que nous devons toujours tenir compte du « biais d’anticipation » lors du calcul de l’écart de score z. De plus, tous ces calculs sont effectués sans frais de transaction. Une fois ces facteurs pris en compte, cette stratégie est vouée à être peu performante. Les frais et les dérapages ne sont actuellement pas déterminés. De plus, la stratégie se négocie en unités fractionnaires de l’ETF, ce qui est également très irréaliste.
Dans un prochain article, nous créerons un backtester piloté par événements plus complexe qui prendra en compte tout ce qui précède, nous donnant ainsi plus de confiance dans notre courbe d’équité et nos indicateurs de performance.