Les données de positionnement COT vous indiquent ce que les plus grands participants spéculatifs aux marchés à terme de devises font avec de l'argent réel chaque semaine, sans interprétation.
Ce guide vous explique le processus complet: extraire les données COT de l'API FXMacroData, calculer les métriques dérivées des clés, créer un filtre de positionnement et l'appliquer à votre flux de travail d'entrée.
Ce que vous allez construire
- Une fonction Python qui récupère des données COT hebdomadaires pour l'une des huit devises prises en charge
- Une mesure de normalisation du positionnement net (en pourcentage net de l'intérêt ouvert)
- Filtre de positionnement à conditions multiples avec seuils configurables
- Une passerelle d'entrée de transaction qui renvoie un signal directionnel:
longJe suis désolé .short, ouneutral - Une marche pratique en utilisant l'EUR/USD comme exemple de travail
Pré-requis
- Clé de l'API FXMacroData disponible à / souscrivezLe point final de COT est inclus dans tous les plans payants.
- Python 3.9+ avec le
requestsbibliothèque installée (pip install requests) et de la - La connaissance de base de la terminologie des rapports de COT (longs, shorts, ouverts). Guide du rapport COT pour les opérateurs de change couvre les bases.
- Facultatif
pandaspour les étapes de manipulation des données (pip install pandas) et de la
Étape 1 Retirer les données COT de l'API
Le point de fin FXMacroData COT renvoie le positionnement hebdomadaire non commercial et commercial pour les contrats à terme sur devises. Les devises prises en charge sont AUD, CAD, CHF, EUR, GBP, JPY, NZD et USD. Chaque enregistrement contient le nombre de contrats longs, courts et nets pour les participants non commerciaux et commerciauts, plus l'intérêt total ouvert.
curl "https://fxmacrodata.com/api/v1/cot/eur?api_key=YOUR_API_KEY&start=2023-01-01"
La réponse JSON a cette structure:
{
"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, enveloppez cet appel dans un assistant qui renvoie la liste de données triée chronologiquement:
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])
Pourquoi 12 mois d'histoire ?
Les seuils de filtrage de l'étape 3 sont exprimés en rangs de percentiles sur l'année suivante. Un an est suffisant pour capturer un cycle de positionnement complet pour la plupart des paires majeures sans inclure les changements de régime qui sont trop anciens pour être pertinents. Vous pouvez élargir la fenêtre à 23 ans pour les devises avec des cycles de positionnement plus lents comme le JPY ou le CHF.
Étape 2 Calcul des mesures de positionnement dérivées
Les contrats bruts sont difficiles à comparer entre les devises et dans le temps. Une longueur nette de 80 000 contrats signifie quelque chose de très différent dans les contrats à terme EUR (grand marché liquide) par rapport au CHF (petit intérêt ouvert).
2a. Position nette en pourcentage des intérêts ouverts
Le ratio normalisé entre -1 et +1 est obtenu en divisant la position nette non commerciale par le total des intérêts ouverts.
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. Positionnement du rang de percentile
Pour savoir si le positionnement actuel est extrême, il faut un contexte historique. net_pct Dans le cas d'un taux de change de plus de 0,5% en moyenne, le taux de conversion est de 0,5%.
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")
Interprétation des rangs percentiles
- 75ème 100ème centile: Les non-commerciaux sont surpeuplés longs, favorisent les longues entrées pendant que la tendance se maintient, ajoutent un risque d'inversion si les fondamentaux changent.
- 25ème 75ème centile: Aucun vent de fond ou de contre-vent ne devrait conduire à d'autres signaux.
- 0° 25° percentile: Les non-commerciaux sont surpeuplés, favorisent les entrées à court terme pendant que la tendance se maintient, ajoutent un risque de contraction à toute surprise haussière.
2c. Momentum de positionnement
La direction de la tendance est aussi importante que le niveau actuel. Une longueur nette qui augmente est un signal différent d'une longueur nettement qui a atteint un plateau ou a commencé à rétrécir. Calculez le changement de 4 semaines de net_pct pour capturer l'élan:
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}")
Étape 3 Construire le filtre d'entrée
Avec les trois mesures à portée de main, vous pouvez construire une fonction de filtre qui renvoie un signal directionnel pour n'importe quelle devise.
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)
L'échantillon produit lorsque les EUR non commerciaux sont en cours d'exécution et en ajoutant:
{
"currency" : "EUR",
"signal" : "long",
"net_pct" : 0.231,
"percentile": 82.4,
"momentum" : 0.018,
"date" : "2025-03-25"
}
Étape 4 Appliquer le filtre aux entrées de transaction
La fonction filtre renvoie l'un des trois signaux longJe suis désolé . short, ou neutralL'utilisation prévue est comme une porte devant votre signal d'entrée principal: ne prenez que des réglages longs lorsque le filtre COT dit long (ou neutral Si vous êtes plus agressif, vous pouvez utiliser des réglages courts lorsque le filtre COT indique shortJe suis désolé .
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()}")
Note d'exécution: le COT est un signal hebdomadaire
Les données COT sont publiées tous les vendredis pour les positions à partir du mardi précédent. Cela en fait un signal à basse fréquence approprié pour filtrer les biais hebdomadaires ou quotidiens, pas les entrées intradiennes. Ré-exécutez le filtre une fois par semaine après la sortie de vendredi à 15h30 ET, en cache le résultat et utilisez-le comme porte de biais statique pour toutes les entréses de la semaine suivante. Documents relatifs aux points de terminaison du COT pour vérifier le moment de la libération.
Étape 5 Étendre à un tableau de bord multi-monnaies
L'exécution du filtre sur les huit devises prises en charge en même temps vous donne un tableau de bord de positionnement hebdomadaire.
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}")
Produit hebdomadaire de l'échantillon:
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
Lire cet instantané: les non-commerciaux sont positionnés longs EUR, AUD, CHF et NZD futures; courts JPY et USD; et neutres sur CAD et GBP. Un trader qui envisage des longs en EUR/JPY trouverait les deux jambes confirmées par le flux des spéculateurs. Un commerçant qui envisagerait des longues en USD/CAD se heurterait à un vent de contre-courant sur USD et à un contexte neutre sur CAD une configuration plus faible d'un point de vue de positionnement.
Étape 6 Combiner le COT avec une couche de confirmation macro
Les réglages les plus robustes combinent le positionnement des taux de change avec au moins un élément fondamental macro qui soutient la même thèse directionnelle.
Utilisez le point final du taux directeur pour obtenir le taux le plus récent pour chaque devise et calculer le différentiel:
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"])
Lorsque le COT et la macro ne sont pas d'accord
Lorsque le positionnement est extrêmement bondé d'un côté mais que le macro fondamental se déplace contre lui (par exemple, les gros contrats à terme courts JPY mais la Banque du Japon commence à se resserrer), le régime peut être en transition. Ce sont les configurations qui produisent les mouvements les plus rapides et les plus importants souvent dans la direction qui oblige à la couverture courte ou à la liquidation longue. Dans de tels cas, le filtre COT seul est insuffisant; surveillez le historique des taux de référence des banques centrales Il est nécessaire de surveiller de près tout déplacement qui pourrait déclencher un décalage de position.
Résumé
Vous disposez maintenant d'un filtre d'entrée complet basé sur COT construit sur des données FXMacroData API en direct.
- Retrouvez les enregistrements hebdomadaires de COT pour la ou les devises que vous négociez.
- Calculer la position nette en pourcentage des intérêts ouverts pour normaliser les devises.
- Classez le positionnement actuel dans l'année de suivi pour identifier les conditions extrêmes ou neutres.
- Calculez la dynamique de 4 semaines pour confirmer si le positionnement est en votre faveur.
- Enregistrez vos entrées commerciales par rapport au signal du filtre ne procédez que lorsque l'alignement COT correspond à votre direction.
- Combiner facultativement avec une vérification par différentiel de taux pour une confirmation à deux facteurs.
Le tableau de bord multi-values complet vous donne un instantané hebdomadaire de l'endroit où l'argent des spéculateurs est positionné, de sorte que vous entrez dans les transactions avec le flux institutionnel plutôt que contre elle. inflation ou ... emploi Les résultats de l'analyse de la croissance ont été évalués en fonction des critères de performance et des indicateurs de performance.