Al final de esta guía, tendrá una prueba de retroceso de Python que solo utiliza FXMacroData sin necesidad de proveedores de precios externos. Obtendrá historias de tasas de política de GBP y USD, construirá un índice de carry-spread sintético directamente desde el diferencial de tasa y lo ejecutará El sistema de prueba de retroceso.py Para la elaboración de gráficos de curvas de renta variable y estadísticas por operación.
Los requisitos previos
- Python 3.10 o más reciente
- Una clave de la API de FXMacroData (regístrate en / suscribirse; los extremos en USD son gratuitos)
- Familiaridad básica con los pandas DataFrames
pipacceso para instalarbacktesting¿ Qué ?requests¿ Qué ?pandas, ynumpy
El enfoque: backtesting con índice de carga
El sistema de prueba de retroceso.py es una biblioteca de simulación ligera basada en eventos que produce gráficos interactivos de Bokeh, métricas clave de rendimiento (ratio Sharpe, extracción máxima, tasa de ganancia) y optimización de parámetros desde una sola llamada de método.
En lugar de basarse en un suministro externo de precios, esta guía construye un índice de propagación de la carga sintética La idea es una representación fiel de un carry trade: el diferencial de tasa GBP/USD (tasa del BoE menos tasa de la Fed) acumula rendimientos cotidianos de interés sobre rendimientos nocionales. Usamos el valor acumulado de ese acumulo como nuestra serie de "precio" de prueba posterior, luego disparamos señales de entrada cada vez que el Banco de Inglaterra cambia su tasa de política.
Así es como las estrategias de portabilidad institucional son realmente modelados el precio que está simulando es el P&L teórico de la celebración del diferencial de rendimiento, no un spot cotización de divisas.
Paso 1 Instalar dependencias
pip install backtesting requests pandas numpy
Paso 2 Obtener los historial de tasas de interés de la política de FXMacroData
Los dos . Punto final de la tasa de interés de la libra esterlina y el Punto final de la tasa de interés de política en USD Retorno de todas las decisiones del banco central desde que comenzó la serie, cada una con una precisión announcement_datetime Datos de USD están disponibles sin una clave 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
Paso 3 Construir un índice diario de diferenciales de cartera
Entre las fechas de anuncio cada tasa de política es constante, por lo que podemos completar ambas series para producir una tasa diaria para cada día calendario. El diferencial es la tasa de GBP menos la tasa USD. El índice de carga se compone de acumulaciones diarias a partir de una base de 100, replicando exactamente el P&L económico de una posición de carga larga en GBP.
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
Cuando los rendimientos de la GBP exceden los rendimiento del USD, el índice se desplaza hacia arriba; cuando la Fed se endurece más rápido que el BoE, se desplazará hacia abajo. Esta es exactamente la trayectoria de P&L que experimenta un operador de carry en el par GBP/USD.
Paso 4 Construir la columna de señal de entrada
Añadir un Signal Columna al precio DataFrame: +1 en el bar cuando el BoE sube, −1 en un corte, 0 en espera o sin datos.
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
Paso 5 Escriba la estrategia de backtesting.py
backtesting.py requiere que subclass Strategy y implementar init() ¿ Qué ? next()El ... Signal la columna está registrada como una Indicator Así que aparece como su propio panel en la salida 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
Paso 6 Ejecutar la prueba de retroceso
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
Gráfico de la curva de las acciones
La curva de renta variable sube constantemente a lo largo de la ventana de 20 años. La región sombreada alrededor de 20192022 marca el período máximo de reducción (−1.70%) cuando los recortes del BoE durante COVID coincidieron con una Fed que recortó aún más rápido reduciendo la ventaja de transporte esperada del GBP.
Paso 7 Generar el gráfico interactivo de Bokeh
backtesting.py tiene un sistema integrado .plot() El método que hace un informe HTML interactivo. open_browser=False si está corriendo en un cuaderno o en un entorno sin cabeza.
# 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")
El informe generado contiene cuatro paneles: el gráfico de precios del índice de cartera con marcadores de entrada y salida, la curva de renta variable, el rastro de descenso y el indicador de señal de tasa del BoE. Al hacer clic en cualquier barra de operaciones se destacan los intercambios en todos los paneles simultáneamente.
Gráfico de distribución del comercio
Paso 8 Optimización de los parámetros
El backtesting.py es bt.optimize() ejecuta una búsqueda en la cuadrícula de combinaciones de parámetros.
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
Un mantenimiento de 7 bares da al acumulado de carga más tiempo para compuesto después de cada anuncio, mejorando Sharpe a 0.68 mientras que también reduce ligeramente el descenso máximo.
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)
Paso 9 Extensión con señales adicionales de FXMacroData
Las tasas de política son sólo una de las muchas señales macro que puede extraer de FXMacroData. fetch_… Llamadero:
Una impresión del IPC superior a la prevista a menudo refuerza un sesgo de endurecimiento. Punto final de inflación en GBP con el Punto final de inflación en USD añadir una señal de confirmación antes de tomar posiciones de transporte.
Los bancos centrales reaccionan a los datos sobre el empleo. Desempleo en GBP El Banco de Inglaterra ha establecido un sistema de transferencia de activos y de transferencias de activos para el mercado de valores.
Reequilibrar cuando los diferenciales de tipos cambien en el momento del anuncio utilizando únicamente FXMacroData announcement_datetime las marcas de tiempo.
# 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
Escritura completa ejecutable
Poniéndolo todo en un solo archivo puede ejecutar directamente no se requiere un proveedor de precios externo:
"""
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()
Resumen de las actividades
Ahora ha construido un completo backtest de carga de FX impulsado por macro utilizando solo FXMacroData y el marco de backtesting.py sin proveedores de precios externos necesarios.
- Obtener los historial de tipos de interés de las políticas de GBP y USD de la El valor de las acciones ¿ Qué ? El precio de exportación los puntos finales de anuncio.
- Las tasas diarias de pago a plazo y el compuesto del GBPUSD en un índice de transferencia sintético.
- Derivar señales diarias de entrada de eventos de cambio de tipos del BoE (
announcement_datetimelas marcas de tiempo). - Implementar una
backtesting.pyclase de estrategia que entra y sale en función de esas señales. - Ejecuta la prueba de retroceso, inspecciona las métricas clave y genera el gráfico interactivo de Bokeh.
- Optimice el parámetro de período de retención con
bt.optimize()- ¿ Qué ?
El siguiente artículo de esta serie extiende este enfoque a un Cesta de transporte de varias monedas clasificar GBP, EUR, AUD y CAD frente al USD por diferencia de tipo de cambio y reequilibrar dinámicamente en cada evento de anuncio utilizando únicamente los datos de FXMacroData.
Explore el catálogo completo de indicadores para cada moneda en el Indice de documentación de FXMacroData, y comprueba el Documentación de las tasas de interés de la libra esterlina ¿ Qué ? Documentación sobre las tasas de interés de política monetaria en USD para las definiciones de campos y las fechas de cobertura históricas.