-
Notifications
You must be signed in to change notification settings - Fork 0
COTAnalysis
The COTAnalysis class computes metrics on fetched COT data.
from cftc_cot import COTAnalysis
# df comes from client.execute()
analysis = COTAnalysis(df, classification="legacy")
# Compute
df = analysis.net_positions()
df = analysis.cot_index(window=156)Note: On construction, rows are sorted ascending by
report_date_as_yyyy_mm_ddwhen that column is present, so rolling and difference-based metrics are correct even though the client returns data newest-first. All methods are chainable in the sense that each enriches and returns the same internal DataFrame.
| Method | Description | Columns added |
|---|---|---|
.net_positions() |
Net (long − short) per category. | {cat}_net |
.z_scores(window=52) |
Rolling Z-scores of net positions. | {cat}_net_zscore |
.cot_index(window=156) |
Classic 0–100 COT Index over a rolling lookback. | {cat}_net_cot_index |
.cot_index_multi(windows=(26,52,156)) |
COT Index at several windows (term structure); skips windows > history. | {cat}_net_cot_index_w{N} |
.extremes(threshold=0.95, window=156, persistence=2) |
Flags "bullish"/"bearish"/NaN from the COT Index (ramp excluded, persistence-filtered). |
{cat}_net_extreme |
.masking() |
Gross-vs-net positioning the coarse report hides (disaggregated/tff). Returns a DataFrame. |
— |
.long_short_ratios() |
Long ÷ short per category. | {cat}_ls_ratio |
.percentile_rank(column) |
Percentile rank (0–1) of the most recent value. Returns a float. |
— |
.wow_change() |
Week-over-week change in net positions. | {cat}_net_wow |
Categories depend on the classification:
-
legacy:
noncomm,comm -
disaggregated:
prod_merc,swap,m_money,other -
tff:
dealer,asset_mgr,lev_money,other
The COT Index normalizes the current net position against its range over a rolling window:
100 * (net - rolling_min) / (rolling_max - rolling_min)
A reading near 100 is the most bullish positioning of the window; near 0, the
most bearish. The default window=156 is ~3 years of weekly data, the conventional
lookback. Rows without a populated range yet (e.g. the very first observation) are
NaN.
df = analysis.cot_index(window=156)
print(df[['report_date_as_yyyy_mm_dd', 'noncomm_net_cot_index']].tail())df = analysis.extremes(threshold=0.95, persistence=2)
# noncomm_net_extreme == "bullish" when COT index >= 95 for >= 2 consecutive weeks
# noncomm_net_extreme == "bearish" when COT index <= 5 for >= 2 consecutive weeksBecause the COT Index is a rolling min-max, trending series pin to 0/100 and a bare
threshold flags very often. Two guards keep the signal meaningful: the first window
rows (an incomplete, degenerate lookback) are never flagged, and persistence
requires the reading to hold for N consecutive weeks. Pass persistence=1 to disable
the persistence filter.
Changed in 0.5.0: default
threshold0.9 → 0.95; ramp excluded;persistence(default 2) added. Earlier versions flagged ~38% of weeks.
df = analysis.cot_index_multi(windows=(26, 52, 156))
# noncomm_net_cot_index_w26 / _w52 / _w156 — same position, different lookbacksThe same position can read as a short-term extreme but mid-range long-term, so the window choice is itself informative.
disaggregated and tff split legacy's coarse categories into finer ones. masking()
shows how much positioning the coarse net hides — for ~84% of commodities the legacy
"commercial" net is small while producers and swap dealers hold large, offsetting books.
from cftc_cot import COTClient, COTAnalysis
c = COTClient()
df = c.history("disaggregated", "AECO FIN BASIS - ICE FUTURES ENERGY DIV", weeks=120, exact=True)
COTAnalysis(df, "disaggregated").masking()
# group components net gross masking_ratio components_corr
# commercial [prod_merc_net, swap_net] -40647 1301267 53.8 -0.91A high masking_ratio with negative components_corr = the headline net is misleading.
Returns empty for legacy (no finer split).
Getting Started
API Reference
Field Reference
Guides
Reference