Los datos de posicionamiento de COT le dicen lo que los participantes especulativos más grandes en los mercados de futuros de divisas están haciendo con dinero real cada semana, sin interpretación.
Esta guía recorre el proceso completo: extraer datos de COT de la API FXMacroData, calcular las métricas derivadas de la clave, construir un filtro de posicionamiento y aplicarlo a su flujo de trabajo de entrada.
Lo que construirás
- Una función de Python que obtiene datos semanales de COT para cualquiera de las ocho monedas soportadas
- Una métrica de normalización de la posición neta (neto en % de interés abierto)
- Un filtro de posicionamiento de múltiples condiciones con umbrales configurables
- Una puerta de entrada de comercio que devuelve una señal direccional:
long¿ Qué ?short, oneutral - Un recorrido práctico con el EUR/USD como ejemplo de trabajo
Los requisitos previos
- La clave de la API de FXMacroData disponible en / suscribirseEl punto final de COT está incluido en todos los planes de pago.
- Python 3.9+ con el
requestslibrería instalada (pip install requests) y de la Comisión). - Conocimiento básico de la terminología de los informes de COT (longs, shorts, open interest no comerciales). Guía del informe de la COT para operadores de divisas cubre los fundamentos.
- Opcionalmente,
pandaspara las etapas de manipulación de datos (pip install pandas) y de la Comisión).
Paso 1 Obtener datos COT de la API
El punto final FXMacroData COT devuelve posicionamiento semanal no comercial y comercial para futuros de divisas. Las monedas soportadas son AUD, CAD, CHF, EUR, GBP, JPY, NZD y USD. Cada registro contiene el recuento de contratos largos, cortos y netos para participantes no comerciales y comerciales, más el interés total abierto.
curl "https://fxmacrodata.com/api/v1/cot/eur?api_key=YOUR_API_KEY&start=2023-01-01"
La respuesta JSON tiene esta estructura:
{
"currency": "eur",
"data": [
{
"date": "2025-03-25",
"noncommercial_long": 198432,
"noncommercial_short": 61840,
"noncommercial_net": 136592,
"commercial_long": 68230,
"commercial_short": 201860,
"open_interest": 591400
},
{
"date": "2025-03-18",
"noncommercial_long": 185710,
"noncommercial_short": 66320,
"noncommercial_net": 119390,
"commercial_long": 72140,
"commercial_short": 189430,
"open_interest": 578200
}
]
}
En Python, envuelve esta llamada en un ayudante que devuelve la lista de datos ordenada cronológicamente:
import requests
from datetime import date, timedelta
BASE_URL = "https://fxmacrodata.com/api/v1"
API_KEY = "YOUR_API_KEY"
def fetch_cot(currency: str, lookback_days: int = 365) -> list[dict]:
"""Return COT weekly records for *currency* over the last *lookback_days* days."""
start = (date.today() - timedelta(days=lookback_days)).isoformat()
resp = requests.get(
f"{BASE_URL}/cot/{currency.lower()}",
params={"api_key": API_KEY, "start": start},
timeout=15,
)
resp.raise_for_status()
payload = resp.json()
return sorted(payload["data"], key=lambda r: r["date"])
records = fetch_cot("eur")
print(f"Loaded {len(records)} COT records for EUR")
print("Latest:", records[-1])
¿Por qué 12 meses de historia?
Los umbrales de filtro en el Paso 3 se expresan como rangos percentiles durante el último año. Un año es suficiente para capturar un ciclo de posicionamiento completo para la mayoría de los pares principales sin incluir cambios de régimen que son demasiado viejos para ser relevantes. Puede ampliar la ventana a 23 años para monedas con ciclos de posicionar más lentos como JPY o CHF.
Paso 2 Calcular las métricas de posicionamiento derivadas
El número de contratos en bruto es difícil de comparar entre monedas y en el tiempo. Un largo neto de 80.000 contratos significa algo muy diferente en futuros de EUR (mercado grande y líquido) frente a CHF (interés abierto más pequeño).
2a. Posición neta en porcentaje de intereses abiertos
La división de la posición neta no comercial por el interés total abierto produce un ratio normalizado entre -1 y +1.
def net_pct_oi(records: list[dict]) -> list[dict]:
"""Add 'net_pct' field = noncommercial_net / open_interest to each record."""
enriched = []
for r in records:
oi = r.get("open_interest") or 1 # guard against zero
enriched.append({**r, "net_pct": r["noncommercial_net"] / oi})
return enriched
records = net_pct_oi(records)
latest = records[-1]
print(f"EUR net % OI: {latest['net_pct']:.3f} ({latest['date']})")
2b. Posicionamiento del rango percentil
Para saber si el posicionamiento actual es extremo, se necesita un contexto histórico. net_pct En el caso de los valores de los precios de los mercados de divisas, el valor de las divisas de divisación se calcula en función de la variación de los tipos de cambio de las posiciones de divisanzas.
def percentile_rank(series: list[float], value: float) -> float:
"""Return the percentile rank of *value* within *series* (0–100)."""
below = sum(1 for x in series if x < value)
return below / len(series) * 100
net_series = [r["net_pct"] for r in records]
current_net = records[-1]["net_pct"]
pct_rank = percentile_rank(net_series, current_net)
print(f"EUR positioning percentile: {pct_rank:.1f}th")
Interpretación de los rangos percentiles
- El 75° 100° percentil: Las operaciones no comerciales son largas, favorecen entradas largas mientras la tendencia se mantiene, añade riesgo de reversión si los fundamentos cambian.
- 25° 75° percentil: Zona neutral, no debe haber viento de cola o de contra si hay otras señales.
- 0° 25° percentil: Los no comerciales están llenos de corto, favorecen entradas cortas mientras la tendencia se mantiene, añade riesgo de compresión en cualquier sorpresa alcista.
2c. Momento de posicionamiento
La dirección de la tendencia es tan importante como el nivel actual. Un largo neto que está creciendo es una señal diferente de un largo neto de que se ha estabilizado o comenzó a reducir. Calcule el cambio de 4 semanas en net_pct para capturar el impulso:
def positioning_momentum(records: list[dict], periods: int = 4) -> float:
"""Return the change in net_pct over the last *periods* weeks."""
if len(records) < periods + 1:
return 0.0
return records[-1]["net_pct"] - records[-(periods + 1)]["net_pct"]
momentum = positioning_momentum(records)
print(f"EUR 4-week positioning change: {momentum:+.3f}")
Paso 3 Construye el filtro de entrada
Con las tres métricas en la mano, se puede construir una función de filtro que devuelve una señal direccional para cualquier moneda.
def cot_entry_filter(
currency: str,
lookback_days: int = 365,
long_pct_threshold: float = 55.0,
short_pct_threshold: float = 45.0,
momentum_min: float = 0.005,
) -> dict:
"""
Return a COT positioning signal for *currency*.
Parameters
----------
currency : ISO currency code (AUD, CAD, CHF, EUR, GBP, JPY, NZD, USD)
lookback_days : history window for percentile calculation
long_pct_threshold : minimum percentile to confirm a long bias
short_pct_threshold : maximum percentile to confirm a short bias
momentum_min : minimum absolute 4-week change to confirm momentum
Returns
-------
dict with keys: currency, signal, net_pct, percentile, momentum, date
"""
records = fetch_cot(currency, lookback_days)
records = net_pct_oi(records)
latest = records[-1]
net_series = [r["net_pct"] for r in records]
pct_rank = percentile_rank(net_series, latest["net_pct"])
momentum = positioning_momentum(records)
if pct_rank >= long_pct_threshold and momentum >= momentum_min:
signal = "long"
elif pct_rank <= short_pct_threshold and momentum <= -momentum_min:
signal = "short"
else:
signal = "neutral"
return {
"currency" : currency.upper(),
"signal" : signal,
"net_pct" : round(latest["net_pct"], 4),
"percentile" : round(pct_rank, 1),
"momentum" : round(momentum, 4),
"date" : latest["date"],
}
result = cot_entry_filter("eur")
print(result)
Producción de la muestra cuando EUR no comerciales están ejecutando un largo lleno y añadiendo a él:
{
"currency" : "EUR",
"signal" : "long",
"net_pct" : 0.231,
"percentile": 82.4,
"momentum" : 0.018,
"date" : "2025-03-25"
}
Paso 4 Aplicar el filtro a las entradas de transacción
La función de filtro devuelve una de las tres señales long¿ Qué ? short, o neutralEl uso previsto es como una puerta de entrada delante de su señal de entrada primaria: sólo tomar configuraciones largas cuando el filtro COT dice long (o neutral Si usted es más agresivo), y sólo tomar configuraciones cortas cuando el filtro COT dice short- ¿ Qué ?
def should_enter_trade(
currency: str,
proposed_direction: str,
allow_neutral: bool = False,
) -> bool:
"""
Return True if COT positioning supports *proposed_direction* for *currency*.
Parameters
----------
currency : ISO currency code
proposed_direction : 'long' or 'short'
allow_neutral : if True, a 'neutral' COT signal does not block entry
"""
cot = cot_entry_filter(currency)
if cot["signal"] == proposed_direction:
return True
if allow_neutral and cot["signal"] == "neutral":
return True
return False
# Example: checking whether to enter a EUR/USD long
currency = "eur" # base currency of the pair
direction = "long"
if should_enter_trade(currency, direction):
print(f"COT confirms {direction} bias for {currency.upper()} — proceed to entry check")
else:
print(f"COT filter blocked {direction} entry for {currency.upper()}")
Nota de aplicación: COT es una señal semanal
Los datos de COT se publican todos los viernes para posiciones a partir del martes anterior. Eso lo convierte en una señal de baja frecuencia adecuada para filtrar sesgos semanales o diarios, no entradas intradiarias. Vuelva a ejecutar el filtro una vez por semana después de la liberación del viernes a las 3:30 pm ET, almacenar en caché el resultado y usarlo como una puerta de sesgo estático para todas las entradas en la semana siguiente. Documentación de los puntos finales de la COT para verificar el momento de liberación.
Paso 5 Extender a un panel de control de varias monedas
Si ejecuta el filtro en las ocho monedas compatibles a la vez, obtendrá un panel de posicionamiento semanal. Esto es útil para identificar qué pares de divisas tienen los vientos favorables más claros impulsados por los especuladores y cuáles evitar porque el posicionamento funciona en contra de su dirección.
CURRENCIES = ["aud", "cad", "chf", "eur", "gbp", "jpy", "nzd", "usd"]
def cot_dashboard() -> list[dict]:
"""Return COT positioning signals for all supported currencies."""
results = []
for ccy in CURRENCIES:
try:
result = cot_entry_filter(ccy)
results.append(result)
except Exception as exc:
print(f"Warning: could not load {ccy.upper()} COT data — {exc}")
return results
dashboard = cot_dashboard()
for row in dashboard:
bar = "▲" if row["signal"] == "long" else ("▼" if row["signal"] == "short" else "–")
print(f"{row['currency']:4s} {bar} {row['signal']:8s} pct={row['percentile']:5.1f} mom={row['momentum']:+.3f}")
Producción semanal de la muestra:
AUD ▲ long pct= 71.2 mom=+0.012
CAD – neutral pct= 53.8 mom=-0.004
CHF ▲ long pct= 68.4 mom=+0.008
EUR ▲ long pct= 82.4 mom=+0.018
GBP – neutral pct= 48.1 mom=-0.002
JPY ▼ short pct= 19.6 mom=-0.022
NZD ▲ long pct= 63.0 mom=+0.007
USD ▼ short pct= 22.1 mom=-0.015
Leyendo este instantáneo: los futuros no comerciales están posicionados en EUR, AUD, CHF y NZD; en JPY y USD cortos; y neutrales en CAD y GBP. Un operador que considere los largos de EUR/JPY encontraría que ambas piernas están confirmadas por el flujo de especuladores. Un comerciante que consideren los largas de USD/CAD se enfrentaría a un viento en contra de COT en USD y un telón de fondo neutral en CAD una configuración más débil desde una perspectiva de posicionamiento.
Paso 6 Combinar el COT con una capa de confirmación macro
Los sistemas más robustos combinan el posicionamiento de la COT con al menos un fundamental macro que apoya la misma tesis direccional.
Usa el punto final de la tasa de interés para extraer el tipo de cambio más reciente para cada moneda y calcular el diferencial:
def fetch_latest_rate(currency: str) -> float | None:
"""Return the most recent policy rate for *currency* as a float."""
resp = requests.get(
f"{BASE_URL}/announcements/{currency.lower()}/policy_rate",
params={"api_key": API_KEY, "limit": 1},
timeout=15,
)
if resp.status_code != 200:
return None
data = resp.json().get("data", [])
return data[0]["val"] if data else None
def rate_differential(base_ccy: str, quote_ccy: str) -> float | None:
"""Return base_rate − quote_rate, or None if either rate is unavailable."""
base_rate = fetch_latest_rate(base_ccy)
quote_rate = fetch_latest_rate(quote_ccy)
if base_rate is None or quote_rate is None:
return None
return base_rate - quote_rate
def combined_filter(base_ccy: str, quote_ccy: str, direction: str) -> dict:
"""
Return a combined COT + rate-differential signal for a currency pair.
direction : 'long' (buy base) or 'short' (sell base)
"""
cot_base = cot_entry_filter(base_ccy)
cot_quote = cot_entry_filter(quote_ccy)
diff = rate_differential(base_ccy, quote_ccy)
# For a long (buy base): we want long bias on base AND short/neutral on quote
if direction == "long":
cot_ok = cot_base["signal"] == "long" and cot_quote["signal"] != "long"
macro_ok = diff is not None and diff > 0
else:
cot_ok = cot_base["signal"] == "short" and cot_quote["signal"] != "short"
macro_ok = diff is not None and diff < 0
return {
"pair" : f"{base_ccy.upper()}/{quote_ccy.upper()}",
"direction" : direction,
"cot_ok" : cot_ok,
"macro_ok" : macro_ok,
"confirmed" : cot_ok and macro_ok,
"cot_base" : cot_base,
"cot_quote" : cot_quote,
"rate_diff" : round(diff, 4) if diff is not None else None,
}
signal = combined_filter("eur", "jpy", "long")
print("Confirmed:", signal["confirmed"])
print("COT base :", signal["cot_base"]["signal"], "| COT quote:", signal["cot_quote"]["signal"])
print("Rate diff :", signal["rate_diff"])
Cuando el COT y la macro no están de acuerdo
Cuando el posicionamiento está extremadamente lleno en un lado, pero el fundamental macro se está moviendo en su contra (por ejemplo, grandes futuros cortos de JPY pero el Banco de Japón está comenzando a endurecer), el régimen puede estar en transición. Estas son las configuraciones que producen los movimientos más rápidos y más grandes a menudo en la dirección que obliga a cubrir corto o liquidar largo. historial de tipos de interés de política de los bancos centrales Esté atento a cualquier cambio que pueda desencadenar un despliegue de posicionamiento.
Resumen de las actividades
Ahora tiene un filtro de entrada completo basado en COT construido en datos de FXMacroData API en vivo.
- Obtenga los registros semanales de COT de la moneda o monedas que está negociando.
- Calcular la posición neta como porcentaje de los intereses abiertos para normalizar entre las monedas.
- Clasifique el posicionamiento actual dentro del año posterior para identificar condiciones extremas o neutrales.
- Calcule el impulso de 4 semanas para confirmar si el posicionamiento está en su favor.
- Enlace a las entradas de comercio con la señal del filtro sólo proceda cuando la alineación COT coincida con su dirección.
- Opcionalmente, combinar con una comprobación de diferencia de tasa para una confirmación de dos factores.
El panel completo de monedas múltiples le da una instantánea semanal de dónde se coloca el dinero del especulador, por lo que entra en operaciones con el flujo institucional en lugar de contra él. la inflación ¿ Qué ? empleo los puntos finales para construir un modelo de macro puntuación que evalúe las tres señales posicionamiento, diferenciales de tasas y impulso de crecimiento/inflación para obtener una imagen más completa del régimen.