Como utilizar os dados COT para filtrar as entradas de negociação FX banner image

Implementation

How-To Guides

Como utilizar os dados COT para filtrar as entradas de negociação FX

Guia passo a passo para extrair os dados de posicionamento dos Compromissos da CFTC dos Traders da API FXMacroData, calcular métricas de posicionar líquido e construir um filtro direcional que alinhe as entradas de negociação com o fluxo institucional.

Também disponível em English

Os dados de posicionamento COT dizem o que os maiores participantes especulativos nos mercados de futuros de moeda estão fazendo com dinheiro real todas as semanas, sem interpretação.

Este guia percorre o processo completo: extrair dados COT da API FXMacroData, calcular as métricas derivadas da chave, construir um filtro de posicionamento e aplicá-lo ao seu fluxo de trabalho de entrada.

O que você vai construir

  • Uma função Python que recupera dados COT semanais para qualquer uma das oito moedas suportadas
  • Uma métrica de normalização do posicionamento líquido (neto em % do interesse aberto)
  • Filtro de posicionamento multicondição com limiares configuráveis
  • Uma porta de entrada de comércio que retorna um sinal direcional: long- Não . shortOu ... neutral
  • Um passo prático usando o EUR/USD como exemplo de trabalho

Requisitos

  • Chave da API do FXMacroData disponível em / subscreverO ponto final COT está incluído em todos os planos pagos.
  • Python 3.9+ com o ... requests biblioteca instalada (pip install requests)).
  • Familiarização básica com a terminologia dos relatórios COT (longs, shorts, open interest não comerciais). Guia de relatórios COT para operadores de câmbio Abrange os fundamentos.
  • Opcionalmente, pandas para as etapas de manipulação de dados (pip install pandas)).

Passo 1 Obter dados COT da API

O endpoint FXMacroData COT retorna posicionamento semanal não comercial e comercial para futuros de moeda. As moedas suportadas são AUD, CAD, CHF, EUR, GBP, JPY, NZD e USD. Cada registro contém a contagem de contratos longos, curtos e líquidos para participantes não comerciais e comerciais, mais o interesse total aberto.

curl "https://fxmacrodata.com/api/v1/cot/eur?api_key=YOUR_API_KEY&start=2023-01-01"

A resposta JSON tem esta estrutura:

{
  "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
    }
  ]
}

Em Python, envolva esta chamada em um auxiliar que retorna a lista de dados ordenada cronologicamente:

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 que 12 meses de história?

Os limiares de filtro no Passo 3 são expressos como ranques percentil durante o ano final. Um ano é longo o suficiente para capturar um ciclo de posicionamento completo para a maioria dos principais pares sem incluir mudanças de regime que são muito antigas para serem relevantes. Você pode ampliar a janela para 23 anos para moedas com ciclos de posicionar mais lentos como JPY ou CHF.

Passo 2 Calcular métricas de posicionamento derivadas

A duração líquida de 80.000 contratos significa algo muito diferente nos futuros EUR (mercado grande e líquido) versus CHF (interesse aberto menor).

2a. Posição líquida em percentagem de participação aberta

Dividindo a posição líquida não comercial pelo total dos juros abertos, obtém-se um rácio normalizado entre -1 e +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. Posicionamento do Percentil de Classe

Para saber se o posicionamento atual é extremo, é preciso um contexto histórico. net_pct O valor de um percentil acima de 75 indica um longo lotado; abaixo de 25 indica um curto lotado.

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")

Interpretação de ranques percentais

  • 75o100o percentil: Os não comerciais são lotados de longo, favorece entradas longas enquanto a tendência se mantém, acrescenta risco de reversão se os fundamentos mudarem.
  • 25o75o percentil: Zona neutra, sem vento de contra-volta ou vento de posição forte outros sinais devem conduzir.
  • 0° 25° percentil: Os não comerciais estão lotados de curto, favorecendo entradas curtas enquanto a tendência se mantém, acrescentando risco de compressão em qualquer surpresa de alta.

2c. Impulso de posicionamento

A direção da tendência é tão importante quanto o nível atual. Um longo líquido que está crescendo é um sinal diferente de um longo líquidos que está estagnado ou começou a encolher. Calcule a mudança de 4 semanas no net_pct para capturar o 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}")

Passo 3 Construir o filtro de entrada

Com as três métricas em mãos, você pode construir uma função de filtro que retorna um sinal direcional para qualquer moeda.

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)

Produto da amostra quando EUR não comerciais estão a executar um longo lotado e adicionar a ele:

{
  "currency"  : "EUR",
  "signal"    : "long",
  "net_pct"   : 0.231,
  "percentile": 82.4,
  "momentum"  : 0.018,
  "date"      : "2025-03-25"
}

Passo 4 Aplicar o filtro às entradas de transacção

A função de filtro retorna um dos três sinais long- Não . shortOu ... neutralO uso pretendido é como um portão à frente do seu sinal de entrada primário: só tomar configurações longas quando o filtro COT diz long (ou neutral Se for mais agressivo), e só faça configurações curtas quando o filtro COT disser short- Não .

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 execução: O sinal COT é semanal

Os dados COT são liberados todas as sextas-feiras para posições a partir da terça-feira anterior. Isso torna um sinal de baixa frequência adequado para filtrar bias semanal ou diário, não entradas intradiárias. Reexecutar o filtro uma vez por semana após a liberação das 15h30 ET de sexta-feita, armazenar o resultado em cache e usá-lo como um portão de bias estático para todas as entradas na semana seguinte. Documentação do ponto final do COT para verificar o calendário de liberação.

Passo 5 Extender para um painel de instrumentos de várias moedas

A execução do filtro em todas as oito moedas suportadas ao mesmo tempo dá-lhe um painel de posicionamento semanal.

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}")

Produto semanal da amostra:

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

A posição de um trader que considera longs EUR/JPY seria confirmada pelo fluxo de especuladores. Um trader que pensa em longs USD/CAD enfrentaria um vento contrário COT no USD e um pano de fundo neutro no CAD uma configuração mais fraca do ponto de vista do posicionamento.

Passo 6 Combinar COT com uma camada de confirmação macro

A Comissão Europeia, em nome da Comissão Europeia e do Parlamento Europeu, aprovou, em 15 de Dezembro de 1999, um regulamento que estabelece as regras de execução do Regulamento (CE) n.o 1083/2006 do Parlamento europeu e do Conselho relativo à aplicação dos regimes de coesão monetária e de supervisão (JO L 347 de 20.12.2006, p. 1).

Usa o ... Ponto final da taxa de juro Para obter a taxa mais recente para cada moeda e calcular o 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"])

Quando a COT e a macro discordam

Quando o posicionamento está extremamente lotado em um lado, mas o fundamental macro está mudando contra ele (por exemplo, grandes futuros curtos do JPY, mas a Banco do Japão está começando a apertar), o regime pode estar em transição. Estas são as configurações que produzem os movimentos mais rápidos e maiores muitas vezes na direção que força a cobertura curta ou a liquidação longa. Histórico de taxas de juro de política do banco central A partir de agora, a Comissão deverá proceder a uma análise dos resultados do trabalho realizado.

Resumo

Agora você tem um filtro de entrada baseado em COT completo construído em dados da API FXMacroData ao vivo.

  1. Obter registos semanais de COT para a moeda ou moedas que está a negociar.
  2. Calcular a posição líquida em percentagem do juro aberto para normalizar em todas as moedas.
  3. Classificar o posicionamento atual no ano anterior para identificar condições extremas ou neutras.
  4. Calcule o momento de 4 semanas para confirmar se o posicionamento está a evoluir a seu favor.
  5. A porta de entrada das suas entradas de negociação contra o sinal do filtro só deve ser colocada quando o alinhamento COT corresponder à sua direção.
  6. Combinar opcionalmente com uma verificação de diferencial de taxa para uma confirmação de dois fatores.

O painel de instrumentos multi-moeda completo dá-lhe um instantâneo semanal de onde o dinheiro dos especuladores está posicionado, para que você entre em negociações com o fluxo institucional em vez de contra ele. inflação Ou ... emprego Os resultados do estudo foram apresentados em conjunto com os resultados de um estudo de avaliação de risco e de um relatório de avaliação do impacto do mercado.

Blogroll