No final deste guia, você terá um backtest Python que usa apenas FXMacroData sem fornecedores de preços externos necessários. backtesting.py A Comissão Europeia e o Conselho de Ministros da União Europeia (CE)
Requisitos prévios
- Python 3.10 ou mais recente
- Uma chave de API FXMacroData (inscreva-se em / subscrever; os pontos finais em USD são gratuitos)
- Familiarização básica com pandas DataFrames
pipacesso à instalaçãobacktesting- Não .requests- Não .pandasE ...numpy
A abordagem: backtesting do índice de carregamento
backtesting.py é uma biblioteca de simulação leve orientada a eventos que produz gráficos interativos de Bokeh, métricas de desempenho chave (ratio Sharpe, taxa de retirada máxima, taxa da vitória) e otimização de parâmetros a partir de uma única chamada de método.
Em vez de depender de um fornecimento externo de preços, este guia constrói um índice de spread de transferência sintético A ideia é uma representação fiel de um carry trade: o diferencial de taxa GBP/USD (taxa da BoE menos taxa da Fed) acumula retornos de juros nocionais diários. Usamos o valor acumulado desse acúmulo como nossa série de "preço" de backtest, em seguida, sinais de entrada de fogo sempre que o Banco da Inglaterra muda sua taxa de política.
É assim que as estratégias de carregamento institucionais são realmente modeladas o preço que você está simulado é o P&L teórico de manter o diferencial de rendimento, não uma cotação de câmbio spot.
Passo 1 Instalar dependências
pip install backtesting requests pandas numpy
Passo 2 Obter históricos de taxas de juro da FXMacroData
Ambos os ... Ponto de partida da taxa de juro da GBP E o ... Ponto de partida da taxa de juro de política monetária em USD Retorna todas as decisões do banco central desde o início da série, cada uma com um número preciso announcement_datetime Data de USD está disponível sem uma chave 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
Passo 3 Construir um índice de spread de carregamento diário
Entre as datas de anúncio, cada taxa de política é constante, então podemos preencher ambas as séries para produzir uma taxa diária para cada dia civil. O spread é a taxa GBP menos a taxa USD. O índice de carregamento compõe que acumulam diariamente a partir de uma base de 100, replicando exatamente o P&L econômico de uma posição de carrega longa em 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
Quando os rendimentos do GBP excedem os rendimento do USD, o índice se desloca para cima; quando o Fed se aperta mais rápido que o BoE, ele desce.
Passo 4 Construir a coluna de sinal de entrada
Anexe um Signal coluna para o preço DataFrame: +1 no bar quando o BoE subir, −1 num corte, 0 em espera ou sem dados.
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
Passo 5 Escrever a estratégia de backtesting.py
backtesting.py requer que você subclasse Strategy e implementar init() E ... next()- O ... Signal A coluna é registada como um Indicator então aparece como seu próprio painel na saída 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
Passo 6 Faça o 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
Gráfico da curva de participação
A curva de ações sobe de forma constante ao longo da janela de 20 anos. A região sombreada em torno de 20192022 marca o período máximo de redução (−1,70%) quando os cortes do BoE durante o COVID coincidiram com um Fed que cortou ainda mais rápido reduzindo a vantagem de transporte esperada do GBP.
Passo 7 Gerencie o gráfico interativo Bokeh
O backtesting.py tem um sistema integrado . .plot() método que rende um relatório HTML interativo. open_browser=False se estiver a correr num caderno ou num ambiente sem cabeça.
# 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")
O relatório gerado contém quatro painéis: o gráfico de preços do índice de carregamento com marcadores de entrada e saída, a curva de ações, o rastreamento de retirada e o indicador de sinal de taxa do BoE. Clicar em qualquer barra de negociação destaca que a negociação em todos os painéis simultaneamente.
Gráfico de distribuição do comércio
Passo 8 Optimização dos parâmetros
- O que é? bt.optimize() A função de busca de rede em combinações 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
Um suporte de 7 bares dá ao carregamento mais tempo para se compor após cada anúncio, melhorando o Sharpe para 0,68 enquanto também reduz o drawdown máximo ligeiramente.
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)
Passo 9 Extender com sinais FXMacroData adicionais
As taxas de política são apenas um dos muitos sinais macro que você pode extrair do FXMacroData. fetch_… ligação:
Uma impressão do IPC acima da previsão reforça frequentemente um viés de aperto. Ponto de inflação GBP com o ... Ponto de inflação em USD Adicionar um sinal de confirmação antes de tomar posições de carregamento.
Os bancos centrais reagem aos dados sobre o emprego. Desemprego em GBP A taxa de juro é a taxa de câmbio mais elevada do que a taxa nominal.
Reequilibrar quando os diferenciais de taxa mudarem no momento do anúncio utilizando apenas FXMacroData announcement_datetime - Marcas de tempo.
# 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
O script executável completo
Colocando tudo em um único arquivo você pode executar diretamente nenhum fornecedor de preços externo necessário:
"""
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()
Resumo
Você construiu agora um backtest de transferência de FX completo baseado em macro usando apenas FXMacroData e o framework backtesting.py sem fornecedores de preços externos necessários.
- Obter históricos das taxas de juro de GBP e USD do GBP E ... USD Pontos finais de anúncio.
- Preencher as taxas diárias a prazo e compor o spread GBPUSD num índice de carregamento sintético.
- Derivar sinais diários de entrada de eventos de variação de taxas do BoE (
announcement_datetimemarcas de data e hora). - Implementar um
backtesting.pyclasse de estratégia que entra e sai com base nesses sinais. - Execute o backtest, inspecione métricas-chave e gere o gráfico interativo de Bokeh.
- Optimize o parâmetro de período de retenção com
bt.optimize()- Não .
O próximo artigo desta série estende esta abordagem a um Cesto de transporte de várias moedas classificação do GBP, EUR, AUD e CAD em relação ao USD por diferencial de taxa e reequilíbrio dinâmico em cada evento de anúncio utilizando apenas dados FXMacroData.
Consulte o catálogo completo dos indicadores para cada moeda no site da Índice de documentação FXMacroData- E verifique o ... Docs da taxa de juro de referência do GBP E ... Docs sobre a taxa de juro de política do dólar para as definições de campos e datas históricas de cobertura.