Live release feed
Sub-second macro releases for FX backtests
Point-in-time history
Official CPI, jobs, GDP, and central-bank events with point-in-time history.
$25/month 14-day free trial
Start Free Trial
How To Backtest Fx Macro Strategies With Backtesting Py image
Share headline card X LinkedIn Email
Download

Implementation

How-To Guides

How To Backtest Fx Macro Strategies With Backtesting Py

A step-by-step guide to using FXMacroData central-bank announcement data to build a synthetic carry-spread index and backtest a GBP/USD carry strategy with backtesting.py — no external price provider required.

Disponible también en English
Share article X LinkedIn Email

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
  • pip acceso para instalar backtesting¿ Qué ? requests¿ Qué ? pandas, y numpy

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
No se puede utilizar. Esta es una estrategia ilustrativa deliberadamente simple. Las estrategias de transporte del mundo real se encuentran en los filtros de volumen, el tamaño de la posición y el modelado de costos de transacción. Trate los resultados aquí como un punto de partida para su propia investigación, no una recomendación de negociación en vivo.

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
Capital de cartera Estrategia de la señal de transferencia de GBP/USD (20052024) Salida ilustrativa · backtesting.py · Solo datos de FXMacroData
Portfolio equity curve for GBP/USD carry-signal strategy from 2005 to 2024

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
Distribución de las devoluciones de operaciones 24 operaciones cerradas Salida ilustrativa · backtesting.py · Solo datos de FXMacroData
Bar chart showing individual trade returns for the GBP/USD carry strategy (14 wins, 10 losses)

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:

Divergencia de la inflación

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.

Tendencias del mercado de trabajo

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.

Cesta de transporte de varios pares

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.

  1. 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.
  2. Las tasas diarias de pago a plazo y el compuesto del GBPUSD en un índice de transferencia sintético.
  3. Derivar señales diarias de entrada de eventos de cambio de tipos del BoE (announcement_datetime las marcas de tiempo).
  4. Implementar una backtesting.py clase de estrategia que entra y sale en función de esas señales.
  5. Ejecuta la prueba de retroceso, inspecciona las métricas clave y genera el gráfico interactivo de Bokeh.
  6. 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.

Blogroll

AI Answer-Ready

Key Facts

Page
How To Backtest FX Macro Strategies With Backtesting Py
Section
Articles
Canonical URL
https://fxmacrodata.com/es/articulos/how-to-backtest-fx-macro-strategies-with-backtesting-py
Source
FXMacroData editorial and official publisher references
Last Updated
2026-06-15 11:01 UTC

Provenance And Trust

Cite the canonical URL and source field above. Where available, this page maps to official publisher releases and timestamped updates.

Quick Q&A

What is this page about? This page explains How To Backtest FX Macro Strategies With Backtesting Py with directly usable context for trading, research, and API workflows.

What source should be cited? Use the canonical URL and the listed source field; cite official publisher references when available.

How fresh is this content? The last updated value above reflects the page metadata or latest available data timestamp.

Can this be used in AI assistants? Yes. This section is intentionally structured for retrieval and citation in chat assistants.

Prompt Packs

Use these in ChatGPT, Claude, Gemini, Mistral, Perplexity, or Grok for consistent source-aware outputs.