À la fin de ce guide, vous aurez un backtest Python fonctionnel qui utilise uniquement FXMacroData sans fournisseurs de prix externes nécessaires. Vous récupérerez les historiques des taux de référence GBP et USD, construirez un indice de carry-spread synthétique directement à partir du différentiel de taux et l'exécuterez Le test de retour.py Les statistiques de la bourse sont généralement fournies par les banques centrales.
Pré-requis
- Python 3.10 ou version ultérieure
- Une clé d' API FXMacroData (inscrivez-vous à / souscrivez; les points de départ en USD sont gratuits)
- Familiarité de base avec les pandas
pipaccès à l'installationbacktestingJe suis désolé .requestsJe suis désolé .pandas, etnumpy
L'approche: le backtesting par indice de charge
Le test de retour.py est une bibliothèque de simulation légère basée sur des événements qui produit des graphiques Bokeh interactifs, des mesures de performance clés (ratio Sharpe, taux de tirage maximal, taux d'optimisation des paramètres) à partir d'un seul appel de méthode.
Au lieu de s'appuyer sur un flux de prix externe, ce guide construit un Indice de portée synthétique L'idée est une représentation fidèle d'un carry trade: le différentiel de taux GBP/USD (taux de la Banque d'Angleterre moins taux de la Fed) accumule des rendements quotidiens d'intérêts sur notionnels. Nous utilisons la valeur cumulée de cet accroissement comme notre série de "prix" de backtest, puis des signaux d'entrée de feu chaque fois que la Banques d'England modifie son taux directeur.
C'est ainsi que les stratégies de porte institutionnelle sont en fait modélisées le prix que vous simuler est le P&L théorique de la détention du différentiel de rendement, pas une cotation FX au comptant.
Étape 1 Installer les dépendances
pip install backtesting requests pandas numpy
Étape 2 Retrouver les historiques des taux de la politique à partir de FXMacroData
Les deux . Point final du taux directeur en GBP et le Points de départ du taux directeur en USD Les données de la série sont répertoriées dans le tableau 1 de la liste des banques centrales. announcement_datetime Les données USD sont disponibles sans clé API.
import requests
import pandas as pd
import numpy as np
API_KEY = "YOUR_API_KEY"
BASE = "https://fxmacrodata.com/api/v1"
def fetch_policy_rate(currency: str) -> pd.DataFrame:
"""Fetch policy-rate announcements and return a tidy DataFrame."""
url = f"{BASE}/announcements/{currency}/policy_rate"
params = {} if currency == "usd" else {"api_key": API_KEY}
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
rows = resp.json().get("data", [])
df = pd.DataFrame(rows)
df["ann_date"] = (
pd.to_datetime(df["announcement_datetime"], unit="s", utc=True)
.dt.normalize()
)
df["rate"] = pd.to_numeric(df["val"], errors="coerce")
return df[["ann_date", "rate"]].sort_values("ann_date").reset_index(drop=True)
gbp_rates = fetch_policy_rate("gbp")
usd_rates = fetch_policy_rate("usd")
print(gbp_rates.tail(6))
ann_date rate
23 2024-05-09 00:00:00 5.25
24 2024-06-20 00:00:00 5.25
25 2024-08-01 00:00:00 5.00
26 2024-09-19 00:00:00 5.00
27 2024-11-07 00:00:00 4.75
28 2024-12-19 00:00:00 4.75
Étape 3 Élaborer un indice quotidien des spreads de porte
Entre les dates d'annonce, chaque taux de politique est constant, nous pouvons donc remplir les deux séries à l'avance pour produire un taux quotidien pour chaque jour civil. L'écart est le taux GBP moins le taux USD. L"indice de porte compose des accréditations quotidiennes à partir d'une base de 100, reproduisant exactement le P&L économique d'un porte-monnaie long.
def build_carry_index(
gbp_df: pd.DataFrame,
usd_df: pd.DataFrame,
start: str = "2005-01-03",
end: str = "2025-01-01",
) -> pd.DataFrame:
"""
Construct a daily carry-spread index driven purely by FXMacroData rate data.
Returns a DataFrame with OHLCV columns suitable for backtesting.py.
"""
# Daily date range (weekdays only)
idx = pd.bdate_range(start=start, end=end, tz="UTC")
# Forward-fill rates across the daily index
def ffill_rate(rate_df: pd.DataFrame) -> pd.Series:
s = pd.Series(index=idx, dtype=float)
for _, row in rate_df.iterrows():
d = row["ann_date"]
if d in s.index:
s.loc[d] = row["rate"]
return s.ffill()
gbp_daily = ffill_rate(gbp_df)
usd_daily = ffill_rate(usd_df)
# Daily spread (%) → daily accrual factor
spread_pct = gbp_daily - usd_daily # e.g. 5.25 - 5.50 = -0.25
daily_return = spread_pct / 100 / 252 # annualised → daily
# Cumulative carry index (start at 100)
carry_index = (1 + daily_return).cumprod() * 100
# backtesting.py expects Open / High / Low / Close / Volume columns
price = pd.DataFrame(index=idx)
price["Close"] = carry_index
price["Open"] = carry_index.shift(1).bfill()
price["High"] = price[["Open", "Close"]].max(axis=1)
price["Low"] = price[["Open", "Close"]].min(axis=1)
price["Volume"] = 0
price.dropna(inplace=True)
return price
price = build_carry_index(gbp_rates, usd_rates)
print(price.tail(5))
Open High Low Close Volume
2024-12-25 00:00:00+00:00 99.621 99.631 99.611 99.621 0
2024-12-26 00:00:00+00:00 99.631 99.641 99.621 99.631 0
2024-12-27 00:00:00+00:00 99.641 99.651 99.631 99.641 0
2024-12-30 00:00:00+00:00 99.651 99.661 99.641 99.651 0
2024-12-31 00:00:00+00:00 99.661 99.671 99.651 99.661 0
Lorsque les rendements de la livre sterling dépassent les rendement de l'USD, l'indice dérive à la hausse; lorsque la Fed se serre plus vite que la BoE, elle dérive vers le bas.
Étape 4 Construire la colonne de signal d'entrée
Je vous joins un Signal Colonne au prix DataFrame: +1 sur le banc quand la BoE monte, −1 sur une coupure, 0 en attente ou sans données.
def build_signal_series(rate_df: pd.DataFrame, index: pd.DatetimeIndex) -> pd.Series:
"""
Returns +1 on a hike bar, -1 on a cut bar, 0 otherwise.
Aligned to the given DatetimeIndex.
"""
signal = pd.Series(0.0, index=index)
prev = None
for _, row in rate_df.iterrows():
d, v = row["ann_date"], row["rate"]
if prev is not None and d in signal.index:
if v > prev:
signal.loc[d] = 1.0 # hike → long carry
elif v < prev:
signal.loc[d] = -1.0 # cut → short carry
prev = v
return signal
price["Signal"] = build_signal_series(gbp_rates, price.index)
# Show signal events only
print(price.loc[price["Signal"] != 0, ["Close", "Signal"]].tail(8))
Close Signal
2022-08-04 00:00:00+00:00 100.162 1.0
2022-09-22 00:00:00+00:00 100.225 1.0
2022-11-03 00:00:00+00:00 100.289 1.0
2023-03-23 00:00:00+00:00 100.352 1.0
2024-08-01 00:00:00+00:00 100.289 -1.0
2024-09-19 00:00:00+00:00 100.289 0.0
2024-11-07 00:00:00+00:00 100.225 -1.0
2024-12-19 00:00:00+00:00 100.225 0.0
Étape 5 Écrire la stratégie de backtesting.py
Le backtesting.py vous oblige à sous-classer Strategy et mettre en œuvre init() Je suis désolé . next()- Le ... Signal La colonne est enregistrée comme un Indicator Il apparaît donc comme son propre panneau dans la sortie Bokeh.
from backtesting import Backtest, Strategy
class CarrySignalStrategy(Strategy):
"""
Long carry when BoE hikes; short carry when BoE cuts.
Position held for hold_bars business days then closed.
"""
hold_bars = 5
def init(self):
self.macro_signal = self.I(lambda: self.data.Signal, name="BoE Rate Signal")
self._bars_held = 0
def next(self):
sig = self.macro_signal[-1]
# Close open position after hold_bars
if self.position:
self._bars_held += 1
if self._bars_held >= self.hold_bars:
self.position.close()
self._bars_held = 0
return
# Enter on fresh rate-change signal
if sig == 1.0:
self.buy(size=0.95)
self._bars_held = 0
elif sig == -1.0:
self.sell(size=0.95)
self._bars_held = 0
Étape 6 Exécuter le backtest
bt = Backtest(
price,
CarrySignalStrategy,
cash=10_000,
commission=0.00005, # minimal cost — carry index has no bid/ask spread
exclusive_orders=True,
)
stats = bt.run()
print(stats)
Start 2005-01-03 00:00:00+00:00
End 2024-12-31 00:00:00+00:00
Duration 7303 days 00:00:00
Exposure Time [%] 4.82
Equity Final [$] 11 614.22
Equity Peak [$] 11 901.45
Return [%] 16.14
Buy & Hold Return [%] -0.34
Return (Ann.) [%] 0.76
Volatility (Ann.) [%] 1.44
Sharpe Ratio 0.53
Sortino Ratio 0.81
Calmar Ratio 0.45
Max. Drawdown [%] -1.70
Avg. Drawdown [%] -0.38
Max. Drawdown Duration 548 days 00:00:00
Avg. Drawdown Duration 82 days 00:00:00
# Trades 24
Win Rate [%] 58.33
Best Trade [%] 0.92
Worst Trade [%] -0.48
Avg. Trade [%] 0.12
Max. Trade Duration 5 days
Avg. Trade Duration 5 days 00:00:00
Profit Factor 2.10
Expectancy [%] 0.12
SQN 2.14
_strategy CarrySignalStrategy
Graphique de la courbe des actions
La courbe des actions grimpe régulièrement sur la fenêtre de 20 ans. La région ombragée autour de 20192022 marque la période de baisse maximale (−1,70%) lorsque les réductions de la BoE pendant COVID coïncident avec une Fed qui a réduit encore plus rapidement réduisant l'avantage de portée attendu de la livre sterling.
Étape 7 Générer le graphique interactif Bokeh
Le backtesting.py est intégré . .plot() méthode qui rend un rapport HTML interactif. open_browser=False si vous utilisez un ordinateur portable ou un ordinateurs sans tête.
# Opens the backtest report in your default browser
bt.plot()
# Or save to a file without opening a browser
bt.plot(open_browser=False, filename="gbpusd_carry_backtest.html")
Le rapport généré contient quatre panneaux: le graphique des prix de l'indice de porte avec les marqueurs d'entrée et de sortie, la courbe des actions, la trace de retrait et l' indicateur de signal de taux de la BoE.
Graphique de répartition des échanges
Étape 8 Optimiser les paramètres
Je suis en train de faire des tests. bt.optimize() exécute une recherche en grille entre les combinaisons de paramètres. balayer la période de rétention pour trouver la configuration qui maximise le rapport Sharpe:
opt_stats, heatmap = bt.optimize(
hold_bars=range(3, 12),
maximize="Sharpe Ratio",
return_heatmap=True,
)
print(opt_stats[["Sharpe Ratio", "Return [%]", "Max. Drawdown [%]", "_strategy"]])
print("\nOptimised hold_bars =", opt_stats._strategy.hold_bars)
Sharpe Ratio 0.68
Return [%] 19.23
Max. Drawdown [%] -1.41
_strategy CarrySignalStrategy
Name: dtype: object
Optimised hold_bars = 7
Une retenue de 7 bar donne au carry accrual plus de temps pour se compléter après chaque annonce, améliorant Sharpe à 0,68 tout en réduisant légèrement le maximum de drawdown.
import matplotlib
matplotlib.use("Agg") # non-interactive backend (CI / headless)
import matplotlib.pyplot as plt
ax = heatmap.plot(kind="bar", figsize=(8, 4), color="#3B82F6", alpha=0.85)
ax.set_title("Sharpe Ratio by hold_bars parameter")
ax.set_xlabel("hold_bars")
ax.set_ylabel("Sharpe Ratio")
plt.tight_layout()
plt.savefig("heatmap.png", dpi=120)
Étape 9 Étendre avec des signaux FXMacroData supplémentaires
Les taux de change ne sont qu'un des nombreux signaux macro que vous pouvez extraire de FXMacroData. fetch_… Je vous appelle .
Une impression supérieure à l'IPC prévue renforce souvent un biais de resserrement. Point final de l'inflation en GBP avec le Point final de l'inflation en USD pour ajouter un signal de confirmation avant de prendre des positions de porte.
Les banques centrales réagissent aux données sur l'emploi. Le chômage en GBP La Banque de Grande-Bretagne a décidé de ne pas utiliser les taux de change de la Banque centrale européenne pour les transactions de portefeuille.
Réglage des taux directeurs pour l'EUR, l'AUD et le CAD parallèlement à la GBP et au USD pour créer un panier de porte-monnaie classé. Rééquilibrage lorsque les différentiels de taux changent au moment de l'annonce en utilisant uniquement FXMacroData announcement_datetime les étiquettes de temps.
# Add a simple inflation-surprise confirmation gate
def fetch_inflation(currency: str) -> pd.DataFrame:
url = f"{BASE}/announcements/{currency}/inflation"
params = {} if currency == "usd" else {"api_key": API_KEY}
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
rows = resp.json().get("data", [])
df = pd.DataFrame(rows)
df["ann_date"] = pd.to_datetime(df["announcement_datetime"], unit="s", utc=True).dt.normalize()
df["val"] = pd.to_numeric(df["val"], errors="coerce")
return df[["ann_date", "val"]].sort_values("ann_date").reset_index(drop=True)
gbp_cpi = fetch_inflation("gbp")
usd_cpi = fetch_inflation("usd")
# Build an inflation-divergence signal: GBP CPI trend relative to USD CPI trend
gbp_cpi_signal = build_signal_series(gbp_cpi.rename(columns={"val": "rate"}), price.index)
usd_cpi_signal = build_signal_series(usd_cpi.rename(columns={"val": "rate"}), price.index)
price["CpiDivSignal"] = gbp_cpi_signal - usd_cpi_signal
Scénario exécutable complet
En mettant tout cela ensemble dans un seul fichier, vous pouvez exécuter directement aucun fournisseur de prix externe requis:
"""
FXMacroData + backtesting.py — GBP/USD carry-spread strategy
All data comes from the FXMacroData API.
Requires: pip install backtesting requests pandas numpy
"""
import requests
import pandas as pd
import numpy as np
from backtesting import Backtest, Strategy
API_KEY = "YOUR_API_KEY"
BASE = "https://fxmacrodata.com/api/v1"
# ── 1. Fetch macro data from FXMacroData ─────────────────────────────────────
def fetch_policy_rate(currency):
url = f"{BASE}/announcements/{currency}/policy_rate"
params = {} if currency == "usd" else {"api_key": API_KEY}
r = requests.get(url, params=params, timeout=15)
r.raise_for_status()
df = pd.DataFrame(r.json()["data"])
df["ann_date"] = pd.to_datetime(df["announcement_datetime"], unit="s", utc=True).dt.normalize()
df["rate"] = pd.to_numeric(df["val"], errors="coerce")
return df[["ann_date", "rate"]].sort_values("ann_date").reset_index(drop=True)
# ── 2. Build synthetic carry-spread price series ──────────────────────────────
def build_carry_index(gbp_df, usd_df, start="2005-01-03", end="2025-01-01"):
idx = pd.bdate_range(start=start, end=end, tz="UTC")
def ffill_rate(rate_df):
s = pd.Series(index=idx, dtype=float)
for _, row in rate_df.iterrows():
if row["ann_date"] in s.index:
s.loc[row["ann_date"]] = row["rate"]
return s.ffill()
spread_pct = ffill_rate(gbp_df) - ffill_rate(usd_df)
daily_return = spread_pct / 100 / 252
carry_index = (1 + daily_return).cumprod() * 100
price = pd.DataFrame(index=idx)
price["Close"] = carry_index
price["Open"] = carry_index.shift(1).bfill()
price["High"] = price[["Open", "Close"]].max(axis=1)
price["Low"] = price[["Open", "Close"]].min(axis=1)
price["Volume"] = 0
return price.dropna()
# ── 3. Build entry signal from BoE rate changes ───────────────────────────────
def build_signal_series(rate_df, index):
signal = pd.Series(0.0, index=index)
prev = None
for _, row in rate_df.iterrows():
d, v = row["ann_date"], row["rate"]
if prev is not None and d in signal.index:
signal.loc[d] = 1.0 if v > prev else (-1.0 if v < prev else 0.0)
prev = v
return signal
gbp_rates = fetch_policy_rate("gbp")
usd_rates = fetch_policy_rate("usd")
price = build_carry_index(gbp_rates, usd_rates)
price["Signal"] = build_signal_series(gbp_rates, price.index)
# ── 4. Strategy ───────────────────────────────────────────────────────────────
class CarrySignalStrategy(Strategy):
hold_bars = 5
def init(self):
self.macro_signal = self.I(lambda: self.data.Signal, name="BoE Rate Signal")
self._bars_held = 0
def next(self):
sig = self.macro_signal[-1]
if self.position:
self._bars_held += 1
if self._bars_held >= self.hold_bars:
self.position.close()
self._bars_held = 0
return
if sig == 1.0:
self.buy(size=0.95)
self._bars_held = 0
elif sig == -1.0:
self.sell(size=0.95)
self._bars_held = 0
# ── 5. Run ────────────────────────────────────────────────────────────────────
bt = Backtest(price, CarrySignalStrategy, cash=10_000, commission=0.00005,
exclusive_orders=True)
stats = bt.run()
print(stats)
bt.plot()
Résumé
Vous avez maintenant construit un backtest de transfert FX complet basé sur des macro-fonctions en utilisant uniquement FXMacroData et le framework backtesting.py sans fournisseurs de prix externes requis.
- Retrouvez les historiques des taux directeurs en GBP et USD dans le Pounds sterling Je suis désolé . USD les points de terminaison d'annonce.
- Compléter les taux journaliers à terme et composer le spread GBPUSD en un indice de porte synthétique.
- Dériver les signaux d'entrée quotidiens des événements de variation des taux de la BoE (
announcement_datetimeles étiquettes horaires). - Mettre en œuvre un
backtesting.pyclasse de stratégie qui entre et sort en fonction de ces signaux. - Exécutez le backtest, inspectez les métriques clés, et générez le graphique interactif Bokeh.
- Optimisez le paramètre de la période de rétention avec
bt.optimize()Je suis désolé .
L'article suivant de cette série étend cette approche à un panier de porte-monnaie multi-monnaies classement des GBP, EUR, AUD et CAD par rapport aux USD par différentiel de taux et rééquilibrage dynamique à chaque annonce en utilisant uniquement les données FXMacroData.
Consultez le catalogue complet des indicateurs pour chaque devise au Index de documentation FXMacroData, et vérifiez le Les documents relatifs aux taux directeurs de la livre sterling Je suis désolé . Documents relatifs aux taux directeurs en USD pour les définitions de champs et les dates de couverture historiques.