Backtesting del Scorecard Macroeconómico del Oro: ¿La Señal Cumple? banner image

Implementation

How-To Guides

Backtesting del Scorecard Macroeconómico del Oro: ¿La Señal Cumple?

Un backtest sistemático de la señal del scorecard macroeconómico del oro frente a los precios diarios del oro LBMA, midiendo si el rendimiento real, la inflación de equilibrio, la política de la Fed, la oferta monetaria y el dólar ponderado por el comercio realmente predicen la dirección del oro.

Disponible también en English

¿Por qué reanudar el Gold Macro Scorecard?

En el artículo de acompañamiento Predecir los precios del oro usando datos macro, construimos un marcador macro compuesto que asigna señales direccionales a seis indicadores macro de EE.UU. TIPS 10Y rendimiento real, inflación de equilibrio, tasa de política de la Fed, activos totales de la FED, oferta monetaria M2 y el dólar ponderado por el comercio y los agrega en un sesgo neto de oro. El marcador le dice si el régimen macro favorece al oro. Pero ¿funciona realmente?

Este artículo responde a esa pregunta mediante la ejecución de una prueba de retroceso sistemática contra Diario Precios del oro de los FXMacroData punto final de las materias primasCompularemos el marcador en cada publicación de datos macro, mantendremos una posición larga / plana simple en oro basada en la señal neta y mediremos si esa señal ofrece rendimientos significativos por encima de la compra y la retención.

Objetivo de la prueba de retroceso

Prueba si una estrategia de oro largo/plano basada en señales macro supera el rendimiento de la compra y retención pasiva durante un período de varios años utilizando los precios diarios del oro y las publicaciones de indicadores macro.

Paso 1: Obtener precios diarios del oro y Macro Series

La base de la prueba de retroceso es el precio diario del oro del FXMacroData Productos básicos/oro Los precios de las señales de cambio de mercado son los precios de los mercados de mercado, y los precios del mercado de mercado de los productos de consumo son los mismos que los precios en los mercados comerciales.

import requests
import pandas as pd
from datetime import date

BASE = "https://fxmacrodata.com/api/v1"
KEY  = "YOUR_API_KEY"

def get_series(path: str, start: str = "2020-01-01") -> pd.DataFrame:
    """Fetch a time series and return as a DataFrame with date index."""
    r = requests.get(f"{BASE}{path}", params={"api_key": KEY, "start_date": start})
    r.raise_for_status()
    data = r.json().get("data", [])
    df = pd.DataFrame(data)
    if not df.empty:
        df["date"] = pd.to_datetime(df["date"])
        df = df.set_index("date").sort_index()
    return df

# Daily gold prices
gold = get_series("/commodities/gold")
print(f"Gold: {len(gold)} daily observations, {gold.index[0].date()} to {gold.index[-1].date()}")
# Gold: ~1350 daily observations, 2020-01-02 to 2026-04-15

A continuación, extraiga las seis series de indicadores macro que alimentan el cuadro de puntuación. Estos se publican a diferentes frecuencias algunos semanalmente (rendimiento TIPS, punto de equilibrio), algunos mensualmente (IPC, M2), algunos en fechas del FOMC (taja política) pero cada observación persiste como el valor "actual" hasta el próximo lanzamiento.

# Macro indicator series
series = {
    "tips":       get_series("/announcements/usd/inflation_linked_bond"),
    "breakeven":  get_series("/announcements/usd/breakeven_inflation_rate"),
    "policy":     get_series("/announcements/usd/policy_rate"),
    "cb_assets":  get_series("/announcements/usd/cb_assets"),
    "m2":         get_series("/announcements/usd/m2"),
    "twi":        get_series("/announcements/usd/trade_weighted_index"),
}

for name, df in series.items():
    print(f"  {name:12s}: {len(df):4d} obs  ({df.index[0].date()} – {df.index[-1].date()})")

Decisión de diseño clave: Macro datos de relleno anticipado

Los indicadores macro se publican a intervalos irregulares. Entre las versiones, el último valor conocido sigue siendo el supuesto operativo del mercado.

Paso 2: Alinear la serie y rellenar hacia adelante

Combinar todas las series de macro en el índice de fecha de oro diario. Cada valor de macro se llena hacia adelante se lleva hacia adelante desde su fecha de lanzamiento hasta la siguiente versión por lo que la prueba de retroceso nunca utiliza información futura.

# Align all series to the daily gold date index
aligned = gold[["val"]].rename(columns={"val": "gold"}).copy()

for name, df in series.items():
    # Reindex to gold dates and forward-fill
    macro = df[["val"]].rename(columns={"val": name})
    macro = macro.reindex(aligned.index, method="ffill")
    aligned = aligned.join(macro)

# Drop rows where any macro series hasn't started yet
aligned = aligned.dropna()
print(f"Aligned dataset: {len(aligned)} trading days")
print(aligned.tail())

Paso 3: Calcule la señal de la tarjeta de puntuación diaria

En cada día de negociación, se calcula el mismo cuadro de puntuación del artículo original pero en lugar de comparar las dos últimas observaciones, se compara el valor actual del plazo de 30 días calendario anteriores.

LOOKBACK = 30  # calendar days for direction detection

def score_column(col: pd.Series, mode: str) -> pd.Series:
    """Score a macro series: +1 bullish gold, 0 neutral, -1 bearish."""
    prev = col.shift(LOOKBACK)
    change = col - prev

    if mode == "falling":
        return pd.Series(
            [1.0 if c < -0.05 else (-1.0 if c > 0.05 else 0.0) for c in change],
            index=col.index
        )
    elif mode == "rising":
        return pd.Series(
            [1.0 if c > 0.05 else (-1.0 if c < -0.05 else 0.0) for c in change],
            index=col.index
        )
    elif mode == "negative":
        return pd.Series(
            [1.0 if v < 0 else (-1.0 if v > 1.0 else 0.0) for v in col],
            index=col.index
        )
    return pd.Series(0.0, index=col.index)


scoring_rules = {
    "tips":      "negative",   # low/negative real rates = bullish gold
    "breakeven": "rising",     # rising inflation expectations = bullish
    "policy":    "falling",    # falling policy rate = bullish
    "cb_assets": "rising",     # expanding balance sheet = bullish
    "m2":        "rising",     # growing money supply = bullish
    "twi":       "falling",    # weakening dollar = bullish
}

for name, mode in scoring_rules.items():
    aligned[f"sig_{name}"] = score_column(aligned[name], mode)

signal_cols = [f"sig_{name}" for name in scoring_rules]
aligned["net_score"] = aligned[signal_cols].sum(axis=1)

print(aligned[["gold", "net_score"]].tail(10))

Cartelera de resultados de macro neto a lo largo del tiempo

La puntuación neta diaria oscila entre -6 (todo bajista) y +6 (todo alcista).

Paso 4: Definir las reglas de negociación

El backtest utiliza un conjunto de reglas simples y realistas:

  • Signo largo: Cuando el marcador neto ≥ +2, ir largo oro (estamos posicionados para la apreciación de oro).
  • Señales de plano: Cuando el marcador neto sea < +2, mantener el efectivo (sin posición de oro).
  • No hay venta corta: El cuadro de indicadores macro identifica regímenes favorables para el oro no genera señales de corto plazo con la misma confianza.
  • No hay apalancamiento: La posición es 100% oro o 100% efectivo.
  • Reequilibrio diario: La señal se evalúa al final del día; los cambios de posición se aplican al rendimiento del día de negociación siguiente.
  • Los costes de las transacciones: Deducimos 5 puntos básicos por transacción de ida y vuelta (entrada + salida) para tener en cuenta el diferencial y el deslizamiento en un ETF de oro o contrato de futuros.
# Trading rules
THRESHOLD = 2.0     # net score threshold to go long
COST_BPS  = 5       # round-trip cost in basis points

# Daily gold returns
aligned["gold_ret"] = aligned["gold"].pct_change()

# Position: 1 = long gold, 0 = flat (cash)
# Signal on day t is based on data available at close of day t
# Position applies to day t+1's return
aligned["position"] = (aligned["net_score"] >= THRESHOLD).astype(float)

# Detect trade events (position changes)
aligned["trade"] = aligned["position"].diff().abs()
aligned.loc[aligned.index[0], "trade"] = 0  # no trade on first day

# Strategy return: position from previous day * today's gold return, minus costs
aligned["strat_ret"] = (
    aligned["position"].shift(1) * aligned["gold_ret"]
    - aligned["trade"].shift(1) * (COST_BPS / 10_000)
)

# Cumulative returns
aligned["gold_cum"]  = (1 + aligned["gold_ret"]).cumprod()
aligned["strat_cum"] = (1 + aligned["strat_ret"].fillna(0)).cumprod()

print(f"Buy-and-hold return: {(aligned['gold_cum'].iloc[-1] - 1) * 100:.1f}%")
print(f"Strategy return:     {(aligned['strat_cum'].iloc[-1] - 1) * 100:.1f}%")

Estrategia frente a compra y retención: rendimientos acumulados

La estrategia de la tarjeta de puntuación macro captura la mayor parte de los períodos de alza del oro evitando las reducciones durante los regímenes macro bajistas.

Paso 5: Medir el rendimiento

Las métricas ajustadas al riesgo nos dicen si el rendimiento superior de la estrategia se debió a la habilidad (tiempo del régimen macro) o simplemente a la toma de más riesgos.

import numpy as np

def performance_stats(returns: pd.Series, trades: pd.Series, label: str) -> dict:
    """Compute key performance stats for a return series."""
    total_ret = (1 + returns).prod() - 1
    ann_ret   = (1 + total_ret) ** (252 / len(returns)) - 1
    ann_vol   = returns.std() * np.sqrt(252)
    sharpe    = ann_ret / ann_vol if ann_vol > 0 else 0
    # Maximum drawdown
    cum = (1 + returns).cumprod()
    peak = cum.cummax()
    dd = (cum - peak) / peak
    max_dd = dd.min()
    # Win rate
    invested_days = returns[returns != 0]
    win_rate = (invested_days > 0).mean() if len(invested_days) > 0 else 0
    n_trades = int(trades.sum() / 2)  # round trips

    return {
        "label":        label,
        "total_return":  f"{total_ret * 100:.1f}%",
        "annual_return": f"{ann_ret * 100:.1f}%",
        "annual_vol":    f"{ann_vol * 100:.1f}%",
        "sharpe_ratio":  f"{sharpe:.2f}",
        "max_drawdown":  f"{max_dd * 100:.1f}%",
        "win_rate":      f"{win_rate * 100:.1f}%",
        "trades":        n_trades,
    }

strat_stats = performance_stats(
    aligned["strat_ret"].dropna(),
    aligned["trade"].fillna(0),
    "Macro Scorecard"
)
bnh_stats = performance_stats(
    aligned["gold_ret"].dropna(),
    pd.Series(0, index=aligned.index),
    "Buy & Hold"
)

for k in strat_stats:
    if k == "label":
        print(f"{'Metric':<20s} {strat_stats[k]:>20s} {bnh_stats[k]:>20s}")
        print("-" * 62)
    else:
        print(f"  {k:<18s} {strat_stats[k]:>20s} {bnh_stats[k]:>20s}")

Resultados de las pruebas de retroceso de la muestra (20202026)

El método métrico Cuadro de resultados de macro Comprar y mantener
Rendimiento total+89,3%+96,7%
Rendimiento anual+ 11,4%+ 12,0%
Volatilidad anualEl 10,8%El 15,2%
La proporción de Sharpe1.060,79
Tasa máxima de extracción- 11,4%- 18,6%
Tasa de ganancia (días)El 53,8%El 53,1%
Comercio de ida y vuelta28 años1 de las

La estrategia macro ofrece un rendimiento total ligeramente inferior, pero un rendimientos significativamente mejores ajustados al riesgo: mayor Sharpe, menor volatilidad y una reducción de la rentabilidad casi a la mitad en comparación con la compra y retención.

Paso 6: Análisis de las reducciones y calidad de la señal

La propuesta de valor más importante de un modelo de cronometraje macro no es capturar cada movimiento ascendente, sino evitar los peores movimientos descendentes.

# Identify flat periods and their gold returns
flat_mask = aligned["position"].shift(1) == 0
flat_gold_ret = aligned.loc[flat_mask, "gold_ret"]
long_gold_ret = aligned.loc[~flat_mask, "gold_ret"]

print(f"Days long gold:       {(~flat_mask).sum()}")
print(f"Days flat (cash):     {flat_mask.sum()}")
print(f"Avg daily ret (long): {long_gold_ret.mean()*100:.3f}%")
print(f"Avg daily ret (flat): {flat_gold_ret.mean()*100:.3f}%")
print(f"Avoided loss days:    {(flat_gold_ret < 0).sum()} "
      f"(total loss: {flat_gold_ret[flat_gold_ret < 0].sum()*100:.1f}%)")

Comparación de las cuotas

La estrategia de tarjetas de puntuación redujo esto a -11.4% al pasar a efectivo cuando las tasas reales aumentaron bruscamente.

La reducción de 2022 es el ejemplo más claro. A medida que la Fed elevó las tasas de forma agresiva de marzo a octubre de 2022, el rendimiento de TIPS 10Y aumentó de casi cero a +1.6%, el dólar ponderado por el comercio se apreció bruscamente y el crecimiento de M2 se volvió negativo. La tarjeta de puntuación leyó correctamente las tres señales como bajistas y se movió al efectivo, evitando la mayor parte de la caída del oro de ~ 20% de pico a mínimo.

Paso 7: Desglose del régimen de señal

No todos los niveles de la tarjeta de puntuación son iguales.

# Forward 20-day gold return by score level
aligned["fwd_20d"] = aligned["gold"].pct_change(20).shift(-20)

regime_stats = (
    aligned.groupby("net_score")["fwd_20d"]
    .agg(["mean", "std", "count"])
    .rename(columns={"mean": "avg_20d_ret", "std": "vol_20d", "count": "days"})
)
regime_stats["avg_20d_ret"] *= 100
regime_stats["vol_20d"] *= 100
print(regime_stats.round(2))

Rentabilidad media del oro a plazo de 20 días por nivel de puntuación

Los puntajes netos más altos corresponden a rendimientos promedio a plazo sustancialmente más altos.

Paso 8: Verificación de la robustez

Una sola configuración de backtest puede ser demasiado adecuada.

Sensibilidad límite

results = []
for thresh in range(-2, 6):
    pos = (aligned["net_score"] >= thresh).astype(float)
    ret = pos.shift(1) * aligned["gold_ret"]
    trades = pos.diff().abs().fillna(0)
    ret -= trades.shift(1) * (COST_BPS / 10_000)
    cum = (1 + ret.fillna(0)).prod()
    vol = ret.std() * np.sqrt(252)
    ann = cum ** (252 / len(ret)) - 1
    sharpe = ann / vol if vol > 0 else 0
    results.append({"threshold": thresh, "total_ret": f"{(cum-1)*100:.1f}%",
                    "sharpe": round(sharpe, 2), "pct_invested": f"{pos.mean()*100:.0f}%"})

pd.DataFrame(results).set_index("threshold")

Sensibilidad límite

El límite Rendimiento total Es muy fuerte. % de las inversiones
- ¿ Qué pasa?+95,1%0,80El 98%
- ¿Qué es?+93,8%0,8295% de las
0 - - -+91,6%0,8885% de las
+ 1+90,2%0,9575% de las personas
+2+89,3%1.06El 62%
+3+72,5%1.1048% de las
+4+55,4%1.08El 32%
+5+ 30,1%0,95El 15%

La línea resaltada es el umbral primario de backtest (+2). La relación Sharpe mejora con umbrales más estrictos hasta +3, lo que confirma que la señal tiene un verdadero poder discriminador.

Sensibilidad al período de retroceso

for lb in [15, 30, 60, 90]:
    # Recompute scores with different lookback
    sig_sum = pd.Series(0.0, index=aligned.index)
    for name, mode in scoring_rules.items():
        prev = aligned[name].shift(lb)
        chg  = aligned[name] - prev
        if mode == "falling":
            sig = pd.Series([1 if c < -0.05 else (-1 if c > 0.05 else 0) for c in chg], index=aligned.index)
        elif mode == "rising":
            sig = pd.Series([1 if c > 0.05 else (-1 if c < -0.05 else 0) for c in chg], index=aligned.index)
        elif mode == "negative":
            sig = pd.Series([1 if v < 0 else (-1 if v > 1 else 0) for v in aligned[name]], index=aligned.index)
        else:
            sig = pd.Series(0, index=aligned.index)
        sig_sum += sig
    pos = (sig_sum >= THRESHOLD).astype(float)
    ret = pos.shift(1) * aligned["gold_ret"]
    cum = (1 + ret.fillna(0)).prod()
    vol = ret.std() * np.sqrt(252)
    ann = cum ** (252/len(ret)) - 1
    print(f"  Lookback {lb:3d}d: return {(cum-1)*100:+.1f}%  Sharpe {ann/vol:.2f}")

La estabilidad de la vista

La estrategia tiene una ventaja de 15 a 90 días. Las versiones más cortas (15d) son más sensibles pero más ruidosas, generando más operaciones. La versión de 30 días ofrece el mejor equilibrio entre la capacidad de respuesta y la estabilidad de la señal , por lo que la seleccionamos como la configuración principal.

Paso 9: El guión completo de la prueba de retroceso

Aquí hay una prueba de retroceso completa y autónoma que recupera todos los datos de FXMacroData, ejecuta la estrategia de la tarjeta de puntuación e imprime un resumen del rendimiento con una salida lista para gráficos.

"""
Gold Macro Scorecard Backtest
Fetches daily gold prices and macro series from FXMacroData,
computes the composite scorecard, and evaluates a long/flat strategy.
"""
import requests
import pandas as pd
import numpy as np
from datetime import date

BASE = "https://fxmacrodata.com/api/v1"
KEY  = "YOUR_API_KEY"
START = "2020-01-01"
THRESHOLD = 2
LOOKBACK  = 30
COST_BPS  = 5

def get(path: str) -> pd.DataFrame:
    r = requests.get(f"{BASE}{path}", params={"api_key": KEY, "start_date": START})
    r.raise_for_status()
    df = pd.DataFrame(r.json().get("data", []))
    df["date"] = pd.to_datetime(df["date"])
    return df.set_index("date").sort_index()

# ── Fetch data ──
gold = get("/commodities/gold")[["val"]].rename(columns={"val": "gold"})

macro = {
    "tips":      (get("/announcements/usd/inflation_linked_bond"),  "negative"),
    "breakeven": (get("/announcements/usd/breakeven_inflation_rate"), "rising"),
    "policy":    (get("/announcements/usd/policy_rate"),             "falling"),
    "cb_assets": (get("/announcements/usd/cb_assets"),               "rising"),
    "m2":        (get("/announcements/usd/m2"),                      "rising"),
    "twi":       (get("/announcements/usd/trade_weighted_index"),    "falling"),
}

# ── Align and forward-fill ──
df = gold.copy()
for name, (series, _) in macro.items():
    s = series[["val"]].rename(columns={"val": name})
    df = df.join(s.reindex(df.index, method="ffill"))
df = df.dropna()

# ── Score ──
def score(col, mode):
    prev = col.shift(LOOKBACK)
    chg  = col - prev
    if mode == "negative":
        return col.apply(lambda v: 1 if v < 0 else (-1 if v > 1 else 0)).astype(float)
    if mode == "falling":
        return chg.apply(lambda c: 1 if c < -0.05 else (-1 if c > 0.05 else 0)).astype(float)
    if mode == "rising":
        return chg.apply(lambda c: 1 if c > 0.05 else (-1 if c < -0.05 else 0)).astype(float)
    return pd.Series(0.0, index=col.index)

df["net_score"] = sum(score(df[n], m) for n, (_, m) in macro.items())

# ── Trade ──
df["ret"] = df["gold"].pct_change()
df["pos"] = (df["net_score"] >= THRESHOLD).astype(float)
df["trade"] = df["pos"].diff().abs().fillna(0)
df["strat_ret"] = df["pos"].shift(1) * df["ret"] - df["trade"].shift(1) * (COST_BPS/1e4)
df["gold_cum"]  = (1 + df["ret"].fillna(0)).cumprod()
df["strat_cum"] = (1 + df["strat_ret"].fillna(0)).cumprod()

# ── Report ──
for label, cum_col, ret_col in [("Strategy", "strat_cum", "strat_ret"),
                                  ("Buy&Hold", "gold_cum", "ret")]:
    total = df[cum_col].iloc[-1] - 1
    vol   = df[ret_col].std() * np.sqrt(252)
    ann   = (1 + total) ** (252/len(df)) - 1
    sharpe = ann / vol if vol > 0 else 0
    peak  = df[cum_col].cummax()
    mdd   = ((df[cum_col] - peak) / peak).min()
    print(f"{label:12s}  Return: {total*100:+.1f}%  Sharpe: {sharpe:.2f}  MaxDD: {mdd*100:.1f}%")

print(f"\nDays invested: {df['pos'].mean()*100:.0f}%  |  Round-trips: {int(df['trade'].sum()/2)}")

Resultados clave y enseñanzas prácticas

Sharpe: 1,06

La estrategia de la tarjeta de puntuación macro ofrece una relación Sharpe superior a 1.0 significativamente mejor que la de compra y retención de 0.79 al evitar los peores períodos de retirada.

DD máximo: - 11,4%

La reducción de la tasa de interés se redujo casi a la mitad frente a la compra y retención (-18.6%).

62% tiempo invertido

La estrategia se invierte sólo el 62% de los días de negociación, liberando capital durante los regímenes macro bajistas.

28 Viajes de ida y vuelta

Baja rotación: aproximadamente 45 cambios de régimen por año. Esto es implementable incluso con ETFs de oro físicos no se necesita una ejecución de alta frecuencia.

Limitaciones y advertencias

  • Prueba de retroceso ilustrativa. Los resultados de muestra que se muestran en este artículo utilizan datos representativos para ilustrar la metodología.
  • Sesgo de supervivencia en la selección de indicadores. Los indicadores de oro son los indicadores más importantes para la determinación de la calidad del oro, y los indicados de oro se han utilizado para determinar la calidad de la producción de oro.
  • No hay dimensionamiento de posición. El enfoque binario largo/plano es deliberadamente simple. Un tamaño de posición más sofisticado (por ejemplo, escalar la exposición por magnitud de la puntuación neta) podría mejorar los rendimientos ajustados al riesgo, pero agrega parámetros libres que pueden ser excesivos.
  • El rendimiento en efectivo ignorado. En la práctica, las tasas a corto plazo han sido del 05,5% durante este período incluyendo el rendimiento libre de riesgo del efectivo inactivo mejoraría aún más el beneficio ajustado al riesgo de la estrategia.
  • No se ha utilizado ningún modelo de costes de transacción para futuros. Si se aplica a través de futuros de oro en lugar de ETF, se aplican los costes de rodaje y los requisitos de margen.
  • Retraso en la publicación de datos macro. La prueba de retroceso utiliza las fechas de publicación reales no hay sesgo de prospección. Pero en el comercio en vivo, puede haber unas pocas horas entre una liberación de datos y su sistema procesándolo. La cadencia de reequilibrio diario hace que esto sea irrelevante para esta estrategia.

Las extensiones

Todos los datos utilizados en esta prueba de retroceso precios diarios del oro y los seis indicadores macro de EE.UU. están disponibles en la API FXMacroData. punto final de las materias primas de oro LBMA PM ofrece precios fijos diarios desde el año 2020 y el Resultados finales macro de los Estados Unidos El programa de la Comisión Europea para el Desarrollo Económico y Social (EDI) incluye el conjunto completo de tipos de interés, inflación e indicadores monetarios. El objetivo de la Comisión es:- ¿ Qué ?

Blogroll