Como construir uma estratégia de negociação de pares de spread de rendimento com FXMacroData banner image

Implementation

How-To Guides

Como construir uma estratégia de negociação de pares de spread de rendimento com FXMacroData

Um passo completo em Python para a construção de uma estratégia de câmbio baseada em regras impulsionada por diferenciais de rendimento de títulos do governo desde a obtenção de rendimentos a 10 anos através da API FXMacroData até o backtesting de sinais de spread no EUR/USD.

Também disponível em English

Troque a diferença entre os rendimentos, não os rendimento em si

Os diferenciais de rendimento de títulos do governo entre duas economias estão entre as forças estruturais mais confiáveis nos mercados de câmbio. Quando o rendimento dos EUA a 10 anos se move significativamente acima do equivalente ao Bund alemão, o capital tende a fluir para ativos USD e o EUR/USD enfrenta pressão de venda sustentada. Quando esse spread se comprime, o par se recupera. A relação não é mecânica, mas é persistente o suficiente para construir uma estratégia de negociação baseada em regras ao redor dela.

Este guia guia você através da construção de uma completa estratégia de par de negociação de spread de rendimento usando a FXMacroData API.

  • Traz séries temporais de rendimento de títulos do governo para duas moedas através do Pontos finais de rendimento das obrigações FXMacroData
  • Calcula o diferencial de rendimento e a sua média móvel e desvio padrão
  • Gera sinais de câmbio longos/cortos estatisticamente orientados utilizando a reversão da média da pontuação Z
  • Acompanha as posições abertas, aplica controlos de risco básicos e apresenta alertas de entrada/saída

Tese central

Os spreads de rendimento reverteram a média em torno de equilíbrios estruturalmente estáveis. Quando um spread se amplia acentuadamente além de sua média recente, o par de câmbio correspondente é estendido estatisticamente e provavelmente voltará.

Requisitos

Antes de começar, certifique-se de ter o seguinte pronto:

  • Python 3.9+ todos os trechos usam anotações de tipo padrão
  • Chave da API do FXMacroData Inscreva-se em / subscrever e copiar a sua chave do painel da conta
  • Pacotes Python- Não . requests- Não . pandas- Não . numpy
pip install requests pandas numpy

Armazenar sua chave de API como uma variável de ambiente nunca codificá-lo em arquivos de origem:

export FXMACRO_API_KEY="YOUR_API_KEY"

Passo 1: Obter dados sobre o rendimento das obrigações do governo

A base desta estratégia é a série temporal de rendimentos de títulos confiável. gov_bond_10y Cada observação leva consigo um date- Não . val (rendimento em %), e um announcement_datetime para o carimbo de lançamento de segundo nível.

Aqui, tiramos USD e EUR rendimentos de 10 anos, em seguida, montá-los em um único DataFrame alinhado em data:

import os
import requests
import pandas as pd
from datetime import datetime, timedelta

BASE_URL = "https://fxmacrodata.com/api/v1"
API_KEY = os.environ["FXMACRO_API_KEY"]


def fetch_yield(currency: str, tenor: str = "gov_bond_10y", start: str = "2022-01-01") -> pd.Series:
    """Fetch a bond yield series from FXMacroData and return as a dated Series."""
    resp = requests.get(
        f"{BASE_URL}/announcements/{currency}/{tenor}",
        params={"api_key": API_KEY, "start": start},
        timeout=15,
    )
    resp.raise_for_status()
    records = resp.json()["data"]
    if not records:
        raise ValueError(f"No data returned for {currency}/{tenor}")
    series = pd.Series(
        {r["date"]: r["val"] for r in records},
        name=f"{currency.upper()}_10Y",
        dtype=float,
    )
    series.index = pd.to_datetime(series.index)
    return series.sort_index()


# Fetch USD and EUR 10-year yields
usd_10y = fetch_yield("usd")
eur_10y = fetch_yield("eur")

# Align on a shared date index (inner join drops days missing in either series)
yields = pd.DataFrame({"USD_10Y": usd_10y, "EUR_10Y": eur_10y}).dropna()

print(yields.tail())
# Output:
#            USD_10Y  EUR_10Y
# 2025-04-08    4.41     2.71
# 2025-04-09    4.39     2.69
# 2025-04-10    4.49     2.70
# 2025-04-11    4.52     2.73
# 2025-04-14    4.47     2.70

Pode utilizar qualquer par de moedas suportado pelo Ponto final de 10 anos As combinações mais populares incluem os spreads USD/JPY, AUD/USD e GBP/USD.

Rendimento de 10 anos em US vs. EUR ilustrativo

Os rendimentos em USD aumentaram mais rapidamente do que os equivalentes em EUR até 20222024, criando um diferencial persistentemente elevado que pesou sobre o par EUR/USD durante todo o ciclo.

Passo 2: Calcule o spread de rendimento e a pontuação Z

O spread bruto (USD menos rendimento EUR) diz a você em que direção o capital está estruturalmente tendencioso.

Uma pontuação Z acima de +1,5 indica que o spread se alargou de forma anormal uma configuração potencial de curto EUR/USD (longo USD).

def compute_spread_signal(
    yields_df: pd.DataFrame,
    base_col: str,
    quote_col: str,
    lookback: int = 60,
    entry_z: float = 1.5,
    exit_z: float = 0.3,
) -> pd.DataFrame:
    """
    Compute yield spread, rolling Z-score, and directional signals.

    A positive spread means base currency yields are higher → base currency
    is structurally favoured → signal to be long base / short quote FX pair.

    Mean reversion: enter when Z-score is extreme, exit when it normalises.
    """
    df = yields_df.copy()
    df["spread"] = df[base_col] - df[quote_col]

    # Rolling statistics over the lookback window
    roll = df["spread"].rolling(lookback, min_periods=lookback // 2)
    df["spread_mean"] = roll.mean()
    df["spread_std"]  = roll.std()

    # Z-score: how many standard deviations from the rolling mean
    df["zscore"] = (df["spread"] - df["spread_mean"]) / df["spread_std"].replace(0, float("nan"))

    # Signal: +1 = long base/short quote, -1 = short base/long quote, 0 = flat
    df["signal"] = 0
    df.loc[df["zscore"] >  entry_z, "signal"] = -1   # spread too wide → expect compression → short base pair
    df.loc[df["zscore"] < -entry_z, "signal"] = +1   # spread too tight → expect widening → long base pair
    # Exit (override) when Z-score returns toward zero
    df.loc[df["zscore"].abs() < exit_z, "signal"] = 0

    return df.dropna(subset=["zscore"])


analysis = compute_spread_signal(yields, base_col="USD_10Y", quote_col="EUR_10Y")

print(analysis[["spread", "spread_mean", "zscore", "signal"]].tail(10))

USDEUR 10Y Spread e Z-Score ilustrativo

Z-scores para além de ±1,5 episódios historicamente marcados em que a propagação se movia demasiado, demasiado rapidamente e tendeu a reverter-se na média nas semanas seguintes.

Passo 3: Extensione para vários pares

Uma única estratégia de spread no EUR/USD é útil, mas o poder real surge quando você executa a mesma lógica em vários pares simultaneamente.

Também pode usar rendimentos de tenor mais curto o gov_bond_2y O ponto final é especialmente sensível às expectativas de taxas de juro, tornando-se um indicador líder em comparação com as séries de 10 anos:

PAIRS = [
    # (base_currency, quote_currency, fx_pair_label)
    ("usd", "eur", "EUR/USD"),
    ("usd", "jpy", "USD/JPY"),
    ("aud", "usd", "AUD/USD"),
    ("gbp", "usd", "GBP/USD"),
]

TENOR = "gov_bond_10y"   # swap for gov_bond_2y for rate-expectation signals

results = {}
for base_ccy, quote_ccy, fx_label in PAIRS:
    try:
        base_series  = fetch_yield(base_ccy, tenor=TENOR)
        quote_series = fetch_yield(quote_ccy, tenor=TENOR)
        df = pd.DataFrame({
            f"{base_ccy.upper()}_10Y":  base_series,
            f"{quote_ccy.upper()}_10Y": quote_series,
        }).dropna()
        signal_df = compute_spread_signal(
            df,
            base_col=f"{base_ccy.upper()}_10Y",
            quote_col=f"{quote_ccy.upper()}_10Y",
        )
        latest = signal_df.iloc[-1]
        results[fx_label] = {
            "spread_pct": round(latest["spread"], 3),
            "zscore":     round(latest["zscore"], 2),
            "signal":     int(latest["signal"]),
        }
        print(f"{fx_label}: spread={latest['spread']:.3f}%, z={latest['zscore']:+.2f}, signal={int(latest['signal']):+d}")
    except Exception as exc:
        print(f"{fx_label}: skipped — {exc}")

# Example output:
# EUR/USD: spread=1.770%, z=+1.21, signal=0
# USD/JPY: spread=4.050%, z=+2.18, signal=-1
# AUD/USD: spread=0.340%, z=-0.55, signal=0
# GBP/USD: spread=0.890%, z=-1.62, signal=+1

Referência do sinal

  • + 1: diferença demasiado comprimida espera alargamento longo percurso da moeda de base (por exemplo, longo GBP/USD)
  • - 1: spread demasiado alargado expect compressão curta perna da moeda de base (por exemplo, curto USD/JPY é longo JPY)
  • 0: espalhada dentro do intervalo normal sem borda direcional permanecer plana

Passo 4: Adicionar uma sobreposição de inclinação de 2 anos versus 10 anos

A forma da curva de rendimento adiciona uma segunda dimensão à estratégia. Uma curva em queda (rendimentos de longo prazo subindo mais rápido do que os de curto prazo) normalmente sinaliza melhorias nas expectativas de crescimento e apoia a moeda. Uma Curva invertida ou achatada muitas vezes precede desacelerações e pivots do banco central.

Puxa os dois . 2 anos E ... 10 anos A taxa de variação da curva de curva da moeda de origem é a taxa de fluxo de curvas de curvos de curvo da moeda nacional.

def fetch_curve_slope(currency: str, start: str = "2022-01-01") -> pd.Series:
    """Return the 10Y–2Y slope for a currency (positive = normal/steep, negative = inverted)."""
    y10 = fetch_yield(currency, tenor="gov_bond_10y", start=start)
    y2  = fetch_yield(currency, tenor="gov_bond_2y",  start=start)
    slope = (y10 - y2).dropna()
    slope.name = f"{currency.upper()}_slope"
    return slope


def apply_curve_filter(
    signal_df: pd.DataFrame,
    base_slope: pd.Series,
    quote_slope: pd.Series,
) -> pd.DataFrame:
    """
    Suppress signals that contradict the curve-slope regime.

    Long base / short quote (signal=+1) is only taken when:
      - base curve is steep (positive slope) AND
      - quote curve is flat or inverted (slope < base_slope)

    Short base / long quote (signal=-1) is only taken when:
      - quote curve is steep relative to base
    """
    df = signal_df.copy()
    df = df.join(base_slope.rename("base_slope"), how="left")
    df = df.join(quote_slope.rename("quote_slope"), how="left")
    df[["base_slope", "quote_slope"]] = df[["base_slope", "quote_slope"]].ffill()

    # Slope differential: positive → base is steeper → supportive for base
    df["slope_diff"] = df["base_slope"] - df["quote_slope"]

    # Filter: suppress longs when slope_diff is negative (quote steeper)
    df.loc[(df["signal"] == +1) & (df["slope_diff"] < 0), "signal"] = 0
    # Filter: suppress shorts when slope_diff is positive (base steeper)
    df.loc[(df["signal"] == -1) & (df["slope_diff"] > 0), "signal"] = 0

    return df


# Example for EUR/USD
usd_slope = fetch_curve_slope("usd")
eur_slope = fetch_curve_slope("eur")

analysis_filtered = apply_curve_filter(analysis, base_slope=usd_slope, quote_slope=eur_slope)
print(analysis_filtered[["zscore", "signal", "slope_diff"]].tail(8))

Inclinação da curva USD e EUR (10Y2Y) ilustrativo

Ambas as curvas se inverteram em 20222023; o diferencial de inclinação relativa ainda forneceu um sinal negociável mesmo quando as inclinações absolutas eram negativas.

Passo 5: Gerar alertas e construir um monitor em tempo real

O último passo reúne a lógica do sinal em um monitor que você pode executar em um cronograma (fechamento diário, horário, ou desencadeado imediatamente após um anúncio de rendimento de títulos através do Calendário de lançamento do FXMacroDataQuando um novo sinal dispara, o monitor imprime um alerta estruturado que você pode encaminhar para Slack, e-mail ou um webhook de negociação.

from dataclasses import dataclass
from typing import Literal

SignalType = Literal["LONG", "SHORT", "EXIT", "HOLD"]


@dataclass
class SpreadAlert:
    fx_pair:     str
    signal:      SignalType
    spread_pct:  float
    zscore:      float
    slope_diff:  float
    timestamp:   str


def latest_signal(
    base_ccy: str,
    quote_ccy: str,
    fx_pair:   str,
    tenor:     str = "gov_bond_10y",
    lookback:  int = 60,
) -> SpreadAlert:
    base_yields  = fetch_yield(base_ccy,  tenor=tenor)
    quote_yields = fetch_yield(quote_ccy, tenor=tenor)
    df = pd.DataFrame({
        "base":  base_yields,
        "quote": quote_yields,
    }).dropna()

    base_col, quote_col = "base", "quote"
    df = compute_spread_signal(
        df.rename(columns={"base": base_col, "quote": quote_col}),
        base_col=base_col,
        quote_col=quote_col,
        lookback=lookback,
    )

    # Curve filter
    base_slope  = fetch_curve_slope(base_ccy)
    quote_slope = fetch_curve_slope(quote_ccy)
    df = apply_curve_filter(df, base_slope, quote_slope)

    row = df.iloc[-1]
    sig_map = {1: "LONG", -1: "SHORT", 0: "HOLD"}

    return SpreadAlert(
        fx_pair=fx_pair,
        signal=sig_map[int(row["signal"])],
        spread_pct=round(float(row["spread"]), 3),
        zscore=round(float(row["zscore"]), 2),
        slope_diff=round(float(row.get("slope_diff", float("nan"))), 3),
        timestamp=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
    )


# Run for all pairs
for base_ccy, quote_ccy, fx_label in PAIRS:
    try:
        alert = latest_signal(base_ccy, quote_ccy, fx_label)
        print(
            f"[{alert.timestamp}] {alert.fx_pair:8s} | {alert.signal:5s} | "
            f"spread={alert.spread_pct:+.3f}%  z={alert.zscore:+.2f}  slope_diff={alert.slope_diff:+.3f}"
        )
    except Exception as exc:
        print(f"{fx_label}: error — {exc}")

# Example output:
# [2025-04-14T08:32:11Z] EUR/USD   | HOLD  | spread=+1.770%  z=+1.21  slope_diff=+0.210
# [2025-04-14T08:32:14Z] USD/JPY   | SHORT | spread=+4.050%  z=+2.18  slope_diff=+0.580
# [2025-04-14T08:32:17Z] AUD/USD   | HOLD  | spread=+0.340%  z=-0.55  slope_diff=-0.120
# [2025-04-14T08:32:20Z] GBP/USD   | LONG  | spread=+0.890%  z=-1.62  slope_diff=-0.340

Dica de agendamento

Os dados sobre o rendimento dos títulos são actualizados quando os governos publicam novos resultados de emissão e quando os bancos centrais publicam decisões de política monetária. Calendário de lançamento do FXMacroData para encontrar o próximo leilão de títulos programado ou anúncio de política, e acionar a atualização do sinal imediatamente após o evento.

Distribuição do sinal entre pares ilustrativo

Com um limiar de pontuação Z de ±1,5 numa janela de 60 dias, uma maioria significativa dos dias se situa na zona plana, concentrando o desdobramento de capital em configurações de alta convicção.

Resumo e passos seguintes

Agora você tem uma estrutura completa de negociação de pares de spread de rendimento.

  1. Obtenção de rendimento de títulos - Não . /announcements/{currency}/gov_bond_10y E ... /announcements/{currency}/gov_bond_2y fornecer a matéria-prima com carimbos de tempo de anúncio de segundo nível
  2. Spread e pontuação Z a reversão média em torno de uma janela de 60 dias gera níveis de entrada e saída objetivos sem ajustamento da curva num único limiar
  3. Filtro de inclinação da curva o diferencial 10Y2Y atua como um regime gate, suprimindo sinais que vão contra o viés estrutural da curva de rendimento de cada moeda
  4. Alertas em tempo real o estruturado SpreadAlert A saída é fácil de rotear para qualquer notificação, registro ou pipeline de execução

As extensões naturais incluem a combinação de diferenciais de rendimento com Diferenciais de taxas de juro E ... Diferenciais do IPC A avaliação de desempenho é feita através de um Taxa de inflação de equilíbrio A Comissão deve, por conseguinte, tomar as medidas necessárias para que os Estados-Membros possam assegurar a aplicação dos princípios da estabilidade financeira.

A documentação completa para todos os pontos finais de rendimento e taxa disponíveis encontra-se em /api-referênciaPara obter a sua chave API, visite / subscrever- Não .

Blogroll