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 ...
requestsbiblioteca 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,
pandaspara 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.
- Obter registos semanais de COT para a moeda ou moedas que está a negociar.
- Calcular a posição líquida em percentagem do juro aberto para normalizar em todas as moedas.
- Classificar o posicionamento atual no ano anterior para identificar condições extremas ou neutras.
- Calcule o momento de 4 semanas para confirmar se o posicionamento está a evoluir a seu favor.
- 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.
- 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.