Am Ende dieses Leitfadens haben Sie einen funktionierenden Python-Backtest, der nur FXMacroData verwendet keine externen Preisanbieter erforderlich. Backtesting.py Die Kommission hat die Kommission aufgefordert, die Ergebnisse der Untersuchung zu ermitteln.
Voraussetzungen
- Python 3.10 oder neuer
- Ein FXMacroData API-Schlüssel (anmelden Sie sich unter /abonnieren; USD-Endpunkte sind kostenlos)
- Grundlegende Kenntnisse mit Pandas
pipZugriff auf Installationbacktesting- Ich weiß .requests- Ich weiß .pandasUnd ...numpy
Der Ansatz: Rückprüfung des Carry-Index
Backtesting.py ist eine leichte, ereignisgesteuerte Simulationsbibliothek, die interaktive Bokeh-Plots, wichtige Leistungsmetriken (Sharpe-Verhältnis, maximale Auslastung, Gewinnrate) und Parameteroptimierung aus einem einzigen Methodenanruf erzeugt.
Anstatt sich auf eine externe Preiszufuhr zu verlassen, erstellt dieser Leitfaden eine synthetischer Spread-Index Die Idee ist eine treue Darstellung eines Carry-Tradings: Die GBP/USD-Zinsdifferenz (BoE-Zinsen minus Fed-Zinserhöhungen) erwirtschaftet tägliche Zinsrenditen. Wir verwenden den kumulativen Wert dieser Erträge als unsere Backtest-Preisreihe und schalten dann Eingangssignale aus, wenn die Bank of England ihren Leitzins ändert.
So werden institutionelle Carry-Strategien tatsächlich modelliert der Preis, den Sie simulieren, ist der theoretische Gewinn und Verlust der Renditedifferenz, nicht eine Spot-Devisenquote.
Schritt 1 Installation von Abhängigkeiten
pip install backtesting requests pandas numpy
Schritt 2 Holen Sie die Zinsverlaufshistorien von FXMacroData ab
Beide . Endpunkt für den Leitzins des GBP Und die ... Endpunkt für den Kurs der USD-Policy Die Daten werden in der Liste der Zentralbanken auf der Grundlage der Datenbank der Zentrale Bank der Europäischen Union (Zentralbank) erfasst. announcement_datetime USD-Daten sind ohne API-Schlüssel verfügbar.
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
Schritt 3 Erstellen Sie einen täglichen Carry-Spread-Index
Zwischen den Ankündigungsdaten ist jeder Kurs konstant, so dass wir beide Reihen vorwärts füllen können, um einen Tageskurs für jeden Kalendertag zu erzeugen. Der Spread ist der GBP-Rate minus der USD-Rat. Der Carry-Index setzt sich zusammen, der täglich von einer Basis von 100 anläuft und genau den wirtschaftlichen Gewinn und Verlust einer langen GBP-Carry-Position wiedergibt.
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
Wenn die GBP-Erträge die USD-Ertragswerte übersteigen, zieht der Index nach oben; wenn die Fed schneller zusammengeht als die BoE, ziehet er nach unten. Dies ist genau der P&L-Pfad, den ein Carry-Trader auf dem GBP/USD-Paar erlebt.
Schritt 4 Erstellen der Eingangssignalkolonne
Ich füge ein Signal Spalte zum Preis DataFrame: +1 Auf der Bar, wenn die Bank von England steigt, −1 Auf einem Schnitt, 0 Wartezeit oder keine Daten.
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
Schritt 5 Schreiben Sie die Backtesting.py-Strategie
Backtesting.py erfordert eine Unterklasse Strategy und umsetzen . init() Und ... next()- Die ... Signal Die Spalte ist als Indicator Also erscheint es als sein eigenes Panel in der Bokeh-Ausgabe.
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
Schritt 6 Backtest durchführen
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
Aktienkurve
The equity curve climbs steadily across the 20-year window. The shaded region around 2019–2022 marks the maximum drawdown period (−1.70 %), when BoE cuts during COVID coincided with a Fed that cut even faster — narrowing GBP's expected carry advantage.
Schritt 7 Erstellen Sie das interaktive Bokeh-Grafiken
Backtesting.py hat eine eingebaute .plot() Die Methode, die einen interaktiven HTML-Bericht erstellt. open_browser=False wenn Sie in einem Notizbuch oder einer kopflosen Umgebung laufen.
# 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")
Der erstellte Bericht enthält vier Paneele: das Carry-Index-Preiskart mit Ein- und Ausstiegsmarkern, die Eigenkapitalkurve, die Drawdown-Trace und den BoE-Zinssignalindikator.
Handelsverteilungstabelle
Schritt 8 Optimierung der Parameter
Das ist Backtesting.py. bt.optimize() Durchsuchen Sie die Wartezeit, um die Konfiguration zu finden, die das Sharpe-Verhältnis maximiert:
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
Ein 7-Bar-Hold gibt dem Carry-Akkrual mehr Zeit, sich nach jeder Ankündigung zu vergrößern, wodurch Sharpe auf 0,68 verbessert wird und gleichzeitig der maximale Drawdown leicht reduziert wird.
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)
Schritt 9 Erweiterung mit zusätzlichen FXMacroData-Signalen
Die Kurspolitik ist nur eines von vielen Makrosignalen, die Sie von FXMacroData ziehen können. fetch_… Ruf an:
Ein höherer Preisindex als der vorhergesagte Druck verstärkt oft eine Verschärfung. Endpunkt der GBP-Inflation Mit dem ... Endpunkt der USD-Inflation ein Bestätigungssignal vor der Einnahme von Carry-Positionen hinzuzufügen.
Die Zentralbanken reagieren auf die Beschäftigungsdaten. GBP Arbeitslosigkeit Die Bank wird die Anlage als Regimefilter verwenden, um die Anlagen zu übernehmen.
Die Rate-Differenz wird bei der Ankündigung nur mit FXMacroData angepasst. announcement_datetime Zeitstempel.
# 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
Vollständig ausführbares Skript
Mit einer einzigen Datei können Sie direkt laufen kein externer Preisgeber erforderlich:
"""
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()
Zusammenfassung
Sie haben nun einen vollständigen makro-gesteuerten FX-Carry-Backtest mit nur FXMacroData und dem Backtesting.py-Framework ohne externe Preisanbieter erstellt.
- Holen Sie sich die Kurshistorien von GBP und USD aus dem GBP Und ... USD Ankündigungsendpunkte.
- Die Tageskurse für die Auszahlung von Zinszuschüssen werden in einem Synthetic Carry Index zusammengefasst.
- Ableiten Sie tägliche Einstiegssignale aus den Wechselkurseffekten der BoE (
announcement_datetimeZeitstempel). - Einführung
backtesting.pyStrategie-Klasse, die auf der Grundlage dieser Signale ein- und ausgeht. - Führen Sie den Backtest durch, überprüfen Sie die wichtigsten Kennzahlen und erstellen Sie das interaktive Bokeh-Grafiken.
- Optimieren Sie den Parameter "Haltungszeit" mit
bt.optimize()- Ich weiß .
Der nächste Artikel dieser Reihe erweitert diesen Ansatz auf eine Mehrwährungs-Behandelskorb Rangfolge von GBP, EUR, AUD und CAD gegenüber USD nach Kursdifferenz und dynamische Neuausgleichung bei jedem Ankündigungsereignis unter Verwendung nur von FXMacroData-Daten.
Sie können den vollständigen Indikatorkatalog für jede Währung im FXMacroData Dokumentationsindex- Und überprüfen Sie die ... Die Zinssätze für die Politik der GBP Und ... USD-Leistungszinsdokumente für Felddefinitionen und historische Berichtsdaten.