Guides

Accessing CFTC Commitments of Traders Data with FinzData Python

The Commitments of Traders (COT) reports provide weekly positioning data for futures markets. FinzData's Python client allows direct access to all COT report families without scraping. This guide shows how to list available markets, pull historical data for a specific market, compute net speculative positioning, and assess its percentile rank over a custom lookback period.

Updated 2026-10-11. Code on this page was run against live FinzData data before publishing.

Understanding COT Report Families

FinzData provides access to all CFTC Commitments of Traders report types through the c.cot() method. The main report families are: legacy futures-only (legacy_fo), legacy futures-and-options (legacy_combined), disaggregated futures-only (disagg_fo), disaggregated futures-and-options (disagg_combined), traders in financial futures futures-only (tff_fo), traders in financial futures futures-and-options (tff_combined), and supplemental trader classifications (cit_supplemental).

Each report family categorizes traders differently. Legacy reports combine all non-reportable positions into a single group. Disaggregated reports separate non-reportable traders into small traders. Traders in Financial Futures (TFF) reports focus on asset managers, leveraged money, and other reportable categories specific to financial futures. The supplemental report adds details on index traders and other classifications.

To see available markets for a report family, use c.cot_markets() with the desired report type. For example, c.cot_markets(report='legacy_fo') returns all markets in the legacy futures-only report, including code and market name. Always prefer specifying code= over market= when pulling data, as market name matching is case-insensitive and can return unintended matches (e.g., 'GOLD' matches both Comex Gold and Goldman Sachs index).

Listing Available COT Markets

Before fetching COT data, identify the correct market code for your instrument of interest. Use c.cot_markets() to retrieve all markets for a given report family. This returns a DataFrame with code and market columns. For legacy futures-only reports, there are 1,352 markets as of the latest data.

Filter the results to find specific markets. For example, to find gold-related markets in the legacy futures-only report, query the returned DataFrame for rows where the market name contains 'GOLD'. However, due to case-insensitive substring matching, it is safer to first identify the exact code (e.g., '088691' for Comex Gold) and then use that code in subsequent calls.

Once you have the market code, you can fetch historical data. Note that free-tier access includes full COT history since 1986, so no look-back bundle is required for COT data retrieval.

import finzdata as yf
c = yf.Client()
markets = c.cot_markets(report='legacy_fo')
gold_markets = markets[markets['market'].str.contains('GOLD', case=False)]
print(gold_markets[['code', 'market']].head())

Fetching Historical COT Data

To retrieve historical COT data for a specific market, use c.cot() with the report type, market code, and optional date range. For example, to get legacy futures-only data for Comex Gold (code '088691') starting from January 1, 2020, call c.cot(report='legacy_fo', code='088691', start='2020-01-01'). This returns a DataFrame with 136 columns including date, open interest, and position breakdowns by trader category.

The returned data includes positions for non-commercial (speculative), commercial (hedging), and non-reportable traders. Key columns for analysis are noncomm_positions_long_all, noncomm_positions_short_all, comm_positions_long_all, and comm_positions_short_all. Each row represents a weekly report date, typically published every Friday.

Always verify the data covers your expected date range. COT reports are released with a lag of approximately one week from the Tuesday as-of date. The date column in the DataFrame reflects the report date, not the as-of date of the positions.

import finzdata as yf
c = yf.Client()
df = c.cot(report='legacy_fo', code='088691', start='2020-01-01')
print(df[['date', 'noncomm_positions_long_all', 'noncomm_positions_short_all']].head())

Calculating Net Speculative Positioning

Net speculative positioning is a key metric derived from COT data, calculated as non-commercial long positions minus non-commercial short positions. This measures the net bias of speculative traders (such as hedge funds and large speculators) in a market. A positive net speculative position indicates more longs than shorts among speculators, suggesting bullish sentiment, while negative indicates bearish sentiment.

To compute this, subtract the noncomm_positions_short_all column from the noncomm_positions_long_all column in the COT DataFrame. The result is a time series showing the evolution of speculative net positioning over time. This metric helps identify extremes in positioning that may precede market reversals.

For example, in the Comex Gold market, sustained high net speculative longs have historically coincided with market tops, while extreme shorts have preceded bottoms. However, positioning should always be interpreted in context with price action and other market factors.

import finzdata as yf
c = yf.Client()
df = c.cot(report='legacy_fo', code='088691', start='2020-01-01')
df['net_speculative'] = df['noncomm_positions_long_all'] - df['noncomm_positions_short_all']
print(df[['date', 'net_speculative']].tail())

Computing Percentile Rank Over Lookback

To assess whether current net speculative positioning is extreme relative to history, compute its percentile rank over a lookback period. The percentile rank indicates the percentage of historical values that are less than or equal to the current value. A high percentile (e.g., above 90) suggests positioning is relatively bullish compared to history, while a low percentile (e.g., below 10) suggests bearish extremes.

Use pandas' rolling() method with rank(pct=True) to calculate the rolling percentile rank. For example, to compute the percentile rank of net speculative positioning over a 52-week (approximately one-year) lookback, apply df['net_speculative'].rolling(260).rank(pct=True) * 100, assuming weekly data and approximately 52 weeks per year. Adjust the window based on your desired lookback in weeks.

This approach helps identify when positioning reaches historically extreme levels. For instance, if the current percentile rank is 95, it means the current net speculative positioning is higher than 95% of values over the lookback period, indicating relatively bullish speculative sentiment.

import finzdata as yf
import pandas as pd
c = yf.Client()
df = c.cot(report='legacy_fo', code='088691', start='2020-01-01')
df['net_speculative'] = df['noncomm_positions_long_all'] - df['noncomm_positions_short_all']
df['speculative_pct_rank'] = df['net_speculative'].rolling(260).rank(pct=True) * 100
print(df[['date', 'net_speculative', 'speculative_pct_rank']].tail(10))

Handling Different COT Report Types

Different COT report families require adjustments to the net positioning calculation based on their specific column names. In disaggregated reports (disagg_fo and disagg_combined), the non-commercial category is split into non-reportable and reportable components, but the net speculative positioning still uses the same core columns: noncomm_positions_long_all minus noncomm_positions_short_all.

For Traders in Financial Futures (TFF) reports (tff_fo and tff_combined), the speculative categories are more granular. Net speculative positioning can be calculated using asset_mgr_positions_long - asset_mgr_positions_short for asset managers, or lev_money_positions_long - lev_money_positions_short for leveraged money, depending on which trader group you wish to analyze. The TFF reports also include columns for other reportables and non-reportable positions.

Always consult the returned DataFrame's column names to confirm the correct fields for your calculation. The c.cot() method returns consistent column naming across report types, making it straightforward to adapt the analysis.

import finzdata as yf
c = yf.Client()
# Example: TFF report for S&P 500 Consolidated
df_tff = c.cot(report='tff_fo', market='S&P 500 Consolidated', start='2025-01-01')
df_tff['net_asset_mgr'] = df_tff['asset_mgr_positions_long'] - df_tff['asset_mgr_positions_short']
print(df_tff[['date', 'net_asset_mgr']].head())

Questions

Do I need a paid plan to access COT data through FinzData's Python client?

No, the full history of COT reports since 1986 is available on the free plan. No look-back bundle or institutional license is required to retrieve COT data via c.cot() or c.cot_markets().

How often is COT data updated in FinzData?

COT data is updated weekly, typically on Fridays, following the CFTC's release of the Commitments of Traders reports. The data reflects positions as of the previous Tuesday.

Can I access intraday or daily granularity COT data?

No, COT reports are released weekly only. FinzData provides the data at its native weekly frequency, with one row per report date. There is no intraday or daily version of the COT dataset.

What is the difference between using code= and market= in the c.cot() call?

The code= parameter uses the exact CFTC market code (e.g., '088691' for Comex Gold) and is unambiguous. The market= parameter performs a case-insensitive substring match on the market name, which can return unintended results (e.g., 'GOLD' matches both Comex Gold and Goldman Sachs index). Always prefer code= for precise data retrieval.