forexia-cot 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Forexia (forexiapro.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.5
2
+ Name: forexia-cot
3
+ Version: 1.0.0
4
+ Summary: CFTC Commitments of Traders (COT) data for FX futures and gold, legacy and TFF reports: positions, net and net as a share of open interest. Weekly, since 2010.
5
+ Project-URL: Homepage, https://github.com/ForexiaPro/cot-data
6
+ Project-URL: Source, https://github.com/ForexiaPro/cot-data/tree/main/python
7
+ Project-URL: COT report, https://forexiapro.com/cot-report/?via=cot-data
8
+ Author: Forexia
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cftc,commitments of traders,cot,cot report,forex,futures,gold,positioning
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Office/Business :: Financial
16
+ Classifier: Topic :: Office/Business :: Financial :: Investment
17
+ Requires-Python: >=3.9
18
+ Provides-Extra: pandas
19
+ Requires-Dist: pandas; extra == 'pandas'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # forexia-cot
23
+
24
+ The CFTC's weekly **Commitments of Traders (COT)** report for FX futures, the US dollar index and gold: **positions only** (long, short, net, net as a share of open interest), legacy and TFF reports, weekly since 2010. Reads the files of [github.com/ForexiaPro/cot-data](https://github.com/ForexiaPro/cot-data), updated every Friday after the CFTC's release.
25
+
26
+ Standard library only. pandas is optional.
27
+
28
+ ```sh
29
+ pip install forexia-cot
30
+ forexia-cot # this week's table: net non-commercial, weekly change, % of open interest
31
+ forexia-cot gbp # the pound's last eight weeks
32
+ ```
33
+
34
+ ```python
35
+ import forexia_cot as cot
36
+
37
+ cot.get_cot("EUR") # every legacy report since 2010, oldest first (a list of dicts)
38
+ cot.get_cot("USDJPY", report="tff") # the yen future's TFF groups
39
+ cot.get_cot("gold", since="2024-01-01")
40
+ cot.latest() # this week, every market and group
41
+
42
+ df = cot.cot_frame("EUR") # pip install "forexia-cot[pandas]"
43
+ df.noncommercial_net.plot()
44
+ ```
45
+
46
+ Markets: `EUR`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `MXN`, `USD` (the dollar index), `XAU` (gold; legacy only). `EURUSD`, `USDJPY`, `gold` and `DXY` work too.
47
+
48
+ Columns: `date` (the Tuesday the positions were counted), `open_interest`, then for each group `_long`, `_short`, `_net`, `_net_pct_oi` (net as a share of open interest, in per cent); non-commercial also has `_spreading`. Legacy groups: `noncommercial`, `commercial`, `nonreportable`. TFF groups: `dealer`, `asset_manager`, `leveraged_funds`, `other_reportable`, `nonreportable`. Positions are contracts in the future's terms: long `JPY` is long the yen, so short USDJPY.
49
+
50
+ For Forexia's COT index and weekly reading of these positions, see [forexiapro.com/cot-report](https://forexiapro.com/cot-report/?via=cot-data).
51
+
52
+ ## Licence
53
+
54
+ The code is MIT. The figures are the CFTC's Commitments of Traders (public domain), compiled by Forexia: [forexiapro.com/cot-report](https://forexiapro.com/cot-report/?via=cot-data).
55
+
56
+ ---
57
+
58
+ Part of the Forexia network: [InterestRateDecision.com](https://interestratedecision.com/?via=cot-data) · [Forexia Academy](https://forexiapro.com/?via=cot-data) · [TradingSignalBot](https://tradingsignalbot.com/?via=cot-data) · [PropSim](https://github.com/PropSim-Enterprise) · [OpenCharts](https://github.com/dylanpersonguy/OpenCharts)
@@ -0,0 +1,37 @@
1
+ # forexia-cot
2
+
3
+ The CFTC's weekly **Commitments of Traders (COT)** report for FX futures, the US dollar index and gold: **positions only** (long, short, net, net as a share of open interest), legacy and TFF reports, weekly since 2010. Reads the files of [github.com/ForexiaPro/cot-data](https://github.com/ForexiaPro/cot-data), updated every Friday after the CFTC's release.
4
+
5
+ Standard library only. pandas is optional.
6
+
7
+ ```sh
8
+ pip install forexia-cot
9
+ forexia-cot # this week's table: net non-commercial, weekly change, % of open interest
10
+ forexia-cot gbp # the pound's last eight weeks
11
+ ```
12
+
13
+ ```python
14
+ import forexia_cot as cot
15
+
16
+ cot.get_cot("EUR") # every legacy report since 2010, oldest first (a list of dicts)
17
+ cot.get_cot("USDJPY", report="tff") # the yen future's TFF groups
18
+ cot.get_cot("gold", since="2024-01-01")
19
+ cot.latest() # this week, every market and group
20
+
21
+ df = cot.cot_frame("EUR") # pip install "forexia-cot[pandas]"
22
+ df.noncommercial_net.plot()
23
+ ```
24
+
25
+ Markets: `EUR`, `GBP`, `JPY`, `CHF`, `CAD`, `AUD`, `NZD`, `MXN`, `USD` (the dollar index), `XAU` (gold; legacy only). `EURUSD`, `USDJPY`, `gold` and `DXY` work too.
26
+
27
+ Columns: `date` (the Tuesday the positions were counted), `open_interest`, then for each group `_long`, `_short`, `_net`, `_net_pct_oi` (net as a share of open interest, in per cent); non-commercial also has `_spreading`. Legacy groups: `noncommercial`, `commercial`, `nonreportable`. TFF groups: `dealer`, `asset_manager`, `leveraged_funds`, `other_reportable`, `nonreportable`. Positions are contracts in the future's terms: long `JPY` is long the yen, so short USDJPY.
28
+
29
+ For Forexia's COT index and weekly reading of these positions, see [forexiapro.com/cot-report](https://forexiapro.com/cot-report/?via=cot-data).
30
+
31
+ ## Licence
32
+
33
+ The code is MIT. The figures are the CFTC's Commitments of Traders (public domain), compiled by Forexia: [forexiapro.com/cot-report](https://forexiapro.com/cot-report/?via=cot-data).
34
+
35
+ ---
36
+
37
+ Part of the Forexia network: [InterestRateDecision.com](https://interestratedecision.com/?via=cot-data) · [Forexia Academy](https://forexiapro.com/?via=cot-data) · [TradingSignalBot](https://tradingsignalbot.com/?via=cot-data) · [PropSim](https://github.com/PropSim-Enterprise) · [OpenCharts](https://github.com/dylanpersonguy/OpenCharts)
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "forexia-cot"
7
+ version = "1.0.0"
8
+ description = "CFTC Commitments of Traders (COT) data for FX futures and gold, legacy and TFF reports: positions, net and net as a share of open interest. Weekly, since 2010."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Forexia" }]
14
+ keywords = ["cot", "cot report", "commitments of traders", "cftc", "forex", "positioning", "futures", "gold"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Office/Business :: Financial",
18
+ "Topic :: Office/Business :: Financial :: Investment",
19
+ "Intended Audience :: Financial and Insurance Industry",
20
+ "Intended Audience :: Science/Research",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.optional-dependencies]
25
+ pandas = ["pandas"]
26
+
27
+ [project.scripts]
28
+ forexia-cot = "forexia_cot.__main__:main"
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/ForexiaPro/cot-data"
32
+ Source = "https://github.com/ForexiaPro/cot-data/tree/main/python"
33
+ "COT report" = "https://forexiapro.com/cot-report/?via=cot-data"
@@ -0,0 +1,95 @@
1
+ """CFTC Commitments of Traders (COT) positions for FX futures and gold.
2
+
3
+ Weekly legacy and TFF reports since 2010 for EUR, GBP, JPY, CHF, CAD, AUD, NZD,
4
+ MXN, the US dollar index and gold, read from github.com/ForexiaPro/cot-data.
5
+ Standard library only; pandas is optional.
6
+
7
+ CFTC Commitments of Traders (public domain). Compiled by Forexia:
8
+ https://forexiapro.com/cot-report/
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import urllib.request
15
+ from typing import Any, Callable, Optional
16
+
17
+ __version__ = "1.0.0"
18
+ __all__ = ["BASE", "MARKETS", "REPORTS", "COT_REPORT", "market_of", "get_cot", "latest", "markets", "cot_frame"]
19
+
20
+ BASE = "https://raw.githubusercontent.com/ForexiaPro/cot-data/main/data"
21
+ MARKETS = ("EUR", "GBP", "JPY", "CHF", "CAD", "AUD", "NZD", "MXN", "USD", "XAU")
22
+ REPORTS = ("legacy", "tff")
23
+ COT_REPORT = "https://forexiapro.com/cot-report/?via=cot-data"
24
+
25
+ _ALIASES = {"GOLD": "XAU", "XAUUSD": "XAU", "DXY": "USD", "DX": "USD", "USDX": "USD", "EURO": "EUR"}
26
+
27
+ Fetch = Callable[[str], str]
28
+
29
+
30
+ def _fetch(url: str) -> str:
31
+ req = urllib.request.Request(url, headers={"User-Agent": f"forexia-cot-python/{__version__}"})
32
+ with urllib.request.urlopen(req, timeout=30) as res:
33
+ return res.read().decode("utf-8")
34
+
35
+
36
+ def _get(path: str, fetch: Optional[Fetch] = None, base: str = BASE) -> Any:
37
+ return json.loads((fetch or _fetch)(f"{base}{path}"))
38
+
39
+
40
+ def market_of(name: str) -> str:
41
+ """A market id from what people type: "eur", "EURUSD", "USDJPY", "gold", "DXY"."""
42
+ s = "".join(c for c in str(name).upper() if c.isalpha())
43
+ if s in MARKETS:
44
+ return s
45
+ if s in _ALIASES:
46
+ return _ALIASES[s]
47
+ if len(s) == 6:
48
+ a, b = s[:3], s[3:]
49
+ if a == "USD" and b in MARKETS:
50
+ return b
51
+ if b == "USD" and a in MARKETS:
52
+ return a
53
+ raise ValueError(f'unknown market "{name}" (one of {", ".join(MARKETS)})')
54
+
55
+
56
+ def _rows(file: dict) -> list[dict[str, Any]]:
57
+ cols = file["columns"]
58
+ return [dict(zip(cols, r)) for r in file["rows"]]
59
+
60
+
61
+ def get_cot(market: str, report: str = "legacy", since: Optional[str] = None, fetch: Optional[Fetch] = None, base: str = BASE) -> list[dict[str, Any]]:
62
+ """Every weekly report of a market since 2010, oldest first.
63
+
64
+ ``report`` is ``"legacy"`` (non-commercial, commercial, non-reportable) or
65
+ ``"tff"`` (dealer, asset manager, leveraged funds, other reportable,
66
+ non-reportable; not for gold). ``since`` keeps rows on or after a date
67
+ (YYYY-MM-DD). Positions are contracts in the future's terms: long EUR is
68
+ long the euro, long JPY is long the yen (short USDJPY).
69
+ """
70
+ mid = market_of(market)
71
+ if report not in REPORTS:
72
+ raise ValueError(f'unknown report "{report}" (legacy or tff)')
73
+ if report == "tff" and mid == "XAU":
74
+ raise ValueError("gold is not in the TFF report; use legacy")
75
+ rows = _rows(_get(f"/{report}/{mid}.json", fetch, base))
76
+ return [r for r in rows if since is None or r["date"] >= since]
77
+
78
+
79
+ def latest(fetch: Optional[Fetch] = None, base: str = BASE) -> dict[str, Any]:
80
+ """The latest report of every market: net, weekly change and share of open interest of each group."""
81
+ return _get("/latest.json", fetch, base)
82
+
83
+
84
+ def markets(fetch: Optional[Fetch] = None, base: str = BASE) -> list[dict[str, Any]]:
85
+ """The markets, their CFTC codes and the dates each report covers."""
86
+ return _get("/markets.json", fetch, base)["markets"]
87
+
88
+
89
+ def cot_frame(market: str, report: str = "legacy", since: Optional[str] = None, fetch: Optional[Fetch] = None):
90
+ """The reports as a pandas DataFrame indexed by date (needs pandas)."""
91
+ import pandas as pd # optional
92
+
93
+ df = pd.DataFrame(get_cot(market, report, since, fetch))
94
+ df["date"] = pd.to_datetime(df["date"])
95
+ return df.set_index("date")
@@ -0,0 +1,35 @@
1
+ """forexia-cot: this week's positioning, or one market's last weeks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from . import COT_REPORT, get_cot, latest
8
+
9
+
10
+ def _fmt(x) -> str:
11
+ return "" if x is None else f"{x:,}" if isinstance(x, int) else str(x)
12
+
13
+
14
+ def main(argv: list[str] | None = None) -> int:
15
+ args = sys.argv[1:] if argv is None else argv
16
+ if args:
17
+ rows = get_cot(args[0], args[1] if len(args) > 1 else "legacy")[-8:]
18
+ group = "noncommercial" if "noncommercial_net" in rows[-1] else "leveraged_funds"
19
+ label = "net " + group.replace("_", " ")
20
+ print(f"{'date':<12}{label:>22}{'% of OI':>10}")
21
+ for r in rows:
22
+ print(f"{r['date']:<12}{_fmt(r[group + '_net']):>22}{_fmt(r[group + '_net_pct_oi']):>10}")
23
+ else:
24
+ data = latest()
25
+ print(f"COT report of {data['report_date']} (non-commercial, legacy)")
26
+ print(f"{'market':<8}{'net':>12}{'1w':>10}{'% of OI':>10}")
27
+ for m in data["markets"]:
28
+ nc = m["legacy"]["noncommercial"]
29
+ print(f"{m['id']:<8}{_fmt(nc['net']):>12}{_fmt(nc['net_change_1w']):>10}{_fmt(nc['net_pct_oi']):>10}")
30
+ print(f"\nForexia's COT index and weekly reading: {COT_REPORT}")
31
+ return 0
32
+
33
+
34
+ if __name__ == "__main__":
35
+ raise SystemExit(main())
@@ -0,0 +1,500 @@
1
+ {
2
+ "report_date": "2026-10-27",
3
+ "released": "2026-10-30T19:30:00.000Z",
4
+ "source": "CFTC Commitments of Traders (public domain). Compiled by Forexia: https://forexiapro.com/cot-report/?via=cot-data",
5
+ "markets": [
6
+ {
7
+ "id": "EUR",
8
+ "name": "Euro FX",
9
+ "code": "099741",
10
+ "report_date": "2026-10-27",
11
+ "open_interest": 868188,
12
+ "legacy": {
13
+ "noncommercial": {
14
+ "net": -143894,
15
+ "net_change_1w": -9370,
16
+ "net_pct_oi": -16.57
17
+ },
18
+ "commercial": {
19
+ "net": 114484,
20
+ "net_change_1w": 9589,
21
+ "net_pct_oi": 13.19
22
+ },
23
+ "nonreportable": {
24
+ "net": 29410,
25
+ "net_change_1w": -219,
26
+ "net_pct_oi": 3.39
27
+ }
28
+ },
29
+ "tff": {
30
+ "dealer": {
31
+ "net": -138016,
32
+ "net_change_1w": 17702,
33
+ "net_pct_oi": -15.9
34
+ },
35
+ "asset_manager": {
36
+ "net": 173249,
37
+ "net_change_1w": -17172,
38
+ "net_pct_oi": 19.96
39
+ },
40
+ "leveraged_funds": {
41
+ "net": -71574,
42
+ "net_change_1w": -2734,
43
+ "net_pct_oi": -8.24
44
+ },
45
+ "other_reportable": {
46
+ "net": 6931,
47
+ "net_change_1w": 2423,
48
+ "net_pct_oi": 0.8
49
+ },
50
+ "nonreportable": {
51
+ "net": 29410,
52
+ "net_change_1w": -219,
53
+ "net_pct_oi": 3.39
54
+ }
55
+ },
56
+ "released": "2026-10-30T19:30:00.000Z"
57
+ },
58
+ {
59
+ "id": "GBP",
60
+ "name": "British pound",
61
+ "code": "096742",
62
+ "report_date": "2026-10-27",
63
+ "open_interest": 253937,
64
+ "legacy": {
65
+ "noncommercial": {
66
+ "net": -79414,
67
+ "net_change_1w": 1006,
68
+ "net_pct_oi": -31.27
69
+ },
70
+ "commercial": {
71
+ "net": 89898,
72
+ "net_change_1w": -5610,
73
+ "net_pct_oi": 35.4
74
+ },
75
+ "nonreportable": {
76
+ "net": -10484,
77
+ "net_change_1w": 4604,
78
+ "net_pct_oi": -4.13
79
+ }
80
+ },
81
+ "tff": {
82
+ "dealer": {
83
+ "net": 121697,
84
+ "net_change_1w": -3423,
85
+ "net_pct_oi": 47.92
86
+ },
87
+ "asset_manager": {
88
+ "net": -128893,
89
+ "net_change_1w": 5728,
90
+ "net_pct_oi": -50.76
91
+ },
92
+ "leveraged_funds": {
93
+ "net": 17195,
94
+ "net_change_1w": -6909,
95
+ "net_pct_oi": 6.77
96
+ },
97
+ "other_reportable": {
98
+ "net": 485,
99
+ "net_change_1w": 0,
100
+ "net_pct_oi": 0.19
101
+ },
102
+ "nonreportable": {
103
+ "net": -10484,
104
+ "net_change_1w": 4604,
105
+ "net_pct_oi": -4.13
106
+ }
107
+ },
108
+ "released": "2026-10-30T19:30:00.000Z"
109
+ },
110
+ {
111
+ "id": "JPY",
112
+ "name": "Japanese yen",
113
+ "code": "097741",
114
+ "report_date": "2026-10-27",
115
+ "open_interest": 374026,
116
+ "legacy": {
117
+ "noncommercial": {
118
+ "net": 101591,
119
+ "net_change_1w": 31713,
120
+ "net_pct_oi": 27.16
121
+ },
122
+ "commercial": {
123
+ "net": -105090,
124
+ "net_change_1w": -34003,
125
+ "net_pct_oi": -28.1
126
+ },
127
+ "nonreportable": {
128
+ "net": 3499,
129
+ "net_change_1w": 2290,
130
+ "net_pct_oi": 0.94
131
+ }
132
+ },
133
+ "tff": {
134
+ "dealer": {
135
+ "net": -130739,
136
+ "net_change_1w": -31449,
137
+ "net_pct_oi": -34.95
138
+ },
139
+ "asset_manager": {
140
+ "net": 91191,
141
+ "net_change_1w": 12088,
142
+ "net_pct_oi": 24.38
143
+ },
144
+ "leveraged_funds": {
145
+ "net": -14630,
146
+ "net_change_1w": 8260,
147
+ "net_pct_oi": -3.91
148
+ },
149
+ "other_reportable": {
150
+ "net": 50679,
151
+ "net_change_1w": 8811,
152
+ "net_pct_oi": 13.55
153
+ },
154
+ "nonreportable": {
155
+ "net": 3499,
156
+ "net_change_1w": 2290,
157
+ "net_pct_oi": 0.94
158
+ }
159
+ },
160
+ "released": "2026-10-30T19:30:00.000Z"
161
+ },
162
+ {
163
+ "id": "CHF",
164
+ "name": "Swiss franc",
165
+ "code": "092741",
166
+ "report_date": "2026-10-27",
167
+ "open_interest": 135489,
168
+ "legacy": {
169
+ "noncommercial": {
170
+ "net": -20049,
171
+ "net_change_1w": -1899,
172
+ "net_pct_oi": -14.8
173
+ },
174
+ "commercial": {
175
+ "net": 33633,
176
+ "net_change_1w": -384,
177
+ "net_pct_oi": 24.82
178
+ },
179
+ "nonreportable": {
180
+ "net": -13584,
181
+ "net_change_1w": 2283,
182
+ "net_pct_oi": -10.03
183
+ }
184
+ },
185
+ "tff": {
186
+ "dealer": {
187
+ "net": 56207,
188
+ "net_change_1w": 1564,
189
+ "net_pct_oi": 41.48
190
+ },
191
+ "asset_manager": {
192
+ "net": -25067,
193
+ "net_change_1w": 468,
194
+ "net_pct_oi": -18.5
195
+ },
196
+ "leveraged_funds": {
197
+ "net": -17502,
198
+ "net_change_1w": -3895,
199
+ "net_pct_oi": -12.92
200
+ },
201
+ "other_reportable": {
202
+ "net": -54,
203
+ "net_change_1w": -420,
204
+ "net_pct_oi": -0.04
205
+ },
206
+ "nonreportable": {
207
+ "net": -13584,
208
+ "net_change_1w": 2283,
209
+ "net_pct_oi": -10.03
210
+ }
211
+ },
212
+ "released": "2026-10-30T19:30:00.000Z"
213
+ },
214
+ {
215
+ "id": "CAD",
216
+ "name": "Canadian dollar",
217
+ "code": "090741",
218
+ "report_date": "2026-10-27",
219
+ "open_interest": 360941,
220
+ "legacy": {
221
+ "noncommercial": {
222
+ "net": -66424,
223
+ "net_change_1w": 8285,
224
+ "net_pct_oi": -18.4
225
+ },
226
+ "commercial": {
227
+ "net": 74260,
228
+ "net_change_1w": -12588,
229
+ "net_pct_oi": 20.57
230
+ },
231
+ "nonreportable": {
232
+ "net": -7836,
233
+ "net_change_1w": 4303,
234
+ "net_pct_oi": -2.17
235
+ }
236
+ },
237
+ "tff": {
238
+ "dealer": {
239
+ "net": 95809,
240
+ "net_change_1w": -4989,
241
+ "net_pct_oi": 26.54
242
+ },
243
+ "asset_manager": {
244
+ "net": -1325,
245
+ "net_change_1w": 4074,
246
+ "net_pct_oi": -0.37
247
+ },
248
+ "leveraged_funds": {
249
+ "net": -96747,
250
+ "net_change_1w": -5634,
251
+ "net_pct_oi": -26.8
252
+ },
253
+ "other_reportable": {
254
+ "net": 10099,
255
+ "net_change_1w": 2246,
256
+ "net_pct_oi": 2.8
257
+ },
258
+ "nonreportable": {
259
+ "net": -7836,
260
+ "net_change_1w": 4303,
261
+ "net_pct_oi": -2.17
262
+ }
263
+ },
264
+ "released": "2026-10-30T19:30:00.000Z"
265
+ },
266
+ {
267
+ "id": "AUD",
268
+ "name": "Australian dollar",
269
+ "code": "232741",
270
+ "report_date": "2026-10-27",
271
+ "open_interest": 325008,
272
+ "legacy": {
273
+ "noncommercial": {
274
+ "net": -128315,
275
+ "net_change_1w": -1023,
276
+ "net_pct_oi": -39.48
277
+ },
278
+ "commercial": {
279
+ "net": 111235,
280
+ "net_change_1w": -3690,
281
+ "net_pct_oi": 34.23
282
+ },
283
+ "nonreportable": {
284
+ "net": 17080,
285
+ "net_change_1w": 4713,
286
+ "net_pct_oi": 5.26
287
+ }
288
+ },
289
+ "tff": {
290
+ "dealer": {
291
+ "net": 55637,
292
+ "net_change_1w": -4637,
293
+ "net_pct_oi": 17.12
294
+ },
295
+ "asset_manager": {
296
+ "net": -125237,
297
+ "net_change_1w": 3502,
298
+ "net_pct_oi": -38.53
299
+ },
300
+ "leveraged_funds": {
301
+ "net": 48414,
302
+ "net_change_1w": -4083,
303
+ "net_pct_oi": 14.9
304
+ },
305
+ "other_reportable": {
306
+ "net": 4106,
307
+ "net_change_1w": 505,
308
+ "net_pct_oi": 1.26
309
+ },
310
+ "nonreportable": {
311
+ "net": 17080,
312
+ "net_change_1w": 4713,
313
+ "net_pct_oi": 5.26
314
+ }
315
+ },
316
+ "released": "2026-10-30T19:30:00.000Z"
317
+ },
318
+ {
319
+ "id": "NZD",
320
+ "name": "New Zealand dollar",
321
+ "code": "112741",
322
+ "report_date": "2026-10-27",
323
+ "open_interest": 129053,
324
+ "legacy": {
325
+ "noncommercial": {
326
+ "net": -57690,
327
+ "net_change_1w": 459,
328
+ "net_pct_oi": -44.7
329
+ },
330
+ "commercial": {
331
+ "net": 59546,
332
+ "net_change_1w": -876,
333
+ "net_pct_oi": 46.14
334
+ },
335
+ "nonreportable": {
336
+ "net": -1856,
337
+ "net_change_1w": 417,
338
+ "net_pct_oi": -1.44
339
+ }
340
+ },
341
+ "tff": {
342
+ "dealer": {
343
+ "net": 68000,
344
+ "net_change_1w": 718,
345
+ "net_pct_oi": 52.69
346
+ },
347
+ "asset_manager": {
348
+ "net": -46841,
349
+ "net_change_1w": 2472,
350
+ "net_pct_oi": -36.3
351
+ },
352
+ "leveraged_funds": {
353
+ "net": -22554,
354
+ "net_change_1w": -3274,
355
+ "net_pct_oi": -17.48
356
+ },
357
+ "other_reportable": {
358
+ "net": 3251,
359
+ "net_change_1w": -333,
360
+ "net_pct_oi": 2.52
361
+ },
362
+ "nonreportable": {
363
+ "net": -1856,
364
+ "net_change_1w": 417,
365
+ "net_pct_oi": -1.44
366
+ }
367
+ },
368
+ "released": "2026-10-30T19:30:00.000Z"
369
+ },
370
+ {
371
+ "id": "MXN",
372
+ "name": "Mexican peso",
373
+ "code": "095741",
374
+ "report_date": "2026-10-27",
375
+ "open_interest": 279359,
376
+ "legacy": {
377
+ "noncommercial": {
378
+ "net": 20843,
379
+ "net_change_1w": 3820,
380
+ "net_pct_oi": 7.46
381
+ },
382
+ "commercial": {
383
+ "net": -21166,
384
+ "net_change_1w": -3340,
385
+ "net_pct_oi": -7.58
386
+ },
387
+ "nonreportable": {
388
+ "net": 323,
389
+ "net_change_1w": -480,
390
+ "net_pct_oi": 0.12
391
+ }
392
+ },
393
+ "tff": {
394
+ "dealer": {
395
+ "net": 12757,
396
+ "net_change_1w": -2495,
397
+ "net_pct_oi": 4.57
398
+ },
399
+ "asset_manager": {
400
+ "net": -25201,
401
+ "net_change_1w": -1137,
402
+ "net_pct_oi": -9.02
403
+ },
404
+ "leveraged_funds": {
405
+ "net": 68335,
406
+ "net_change_1w": 4521,
407
+ "net_pct_oi": 24.46
408
+ },
409
+ "other_reportable": {
410
+ "net": -56214,
411
+ "net_change_1w": -409,
412
+ "net_pct_oi": -20.12
413
+ },
414
+ "nonreportable": {
415
+ "net": 323,
416
+ "net_change_1w": -480,
417
+ "net_pct_oi": 0.12
418
+ }
419
+ },
420
+ "released": "2026-10-30T19:30:00.000Z"
421
+ },
422
+ {
423
+ "id": "USD",
424
+ "name": "US dollar index",
425
+ "code": "098662",
426
+ "report_date": "2026-10-27",
427
+ "open_interest": 48263,
428
+ "legacy": {
429
+ "noncommercial": {
430
+ "net": 14121,
431
+ "net_change_1w": -923,
432
+ "net_pct_oi": 29.26
433
+ },
434
+ "commercial": {
435
+ "net": -16088,
436
+ "net_change_1w": 926,
437
+ "net_pct_oi": -33.33
438
+ },
439
+ "nonreportable": {
440
+ "net": 1967,
441
+ "net_change_1w": -3,
442
+ "net_pct_oi": 4.08
443
+ }
444
+ },
445
+ "tff": {
446
+ "dealer": {
447
+ "net": -19547,
448
+ "net_change_1w": 432,
449
+ "net_pct_oi": -40.5
450
+ },
451
+ "asset_manager": {
452
+ "net": 16713,
453
+ "net_change_1w": -307,
454
+ "net_pct_oi": 34.63
455
+ },
456
+ "leveraged_funds": {
457
+ "net": 3158,
458
+ "net_change_1w": -25,
459
+ "net_pct_oi": 6.54
460
+ },
461
+ "other_reportable": {
462
+ "net": -2291,
463
+ "net_change_1w": -97,
464
+ "net_pct_oi": -4.75
465
+ },
466
+ "nonreportable": {
467
+ "net": 1967,
468
+ "net_change_1w": -3,
469
+ "net_pct_oi": 4.08
470
+ }
471
+ },
472
+ "released": "2026-10-30T19:30:00.000Z"
473
+ },
474
+ {
475
+ "id": "XAU",
476
+ "name": "Gold",
477
+ "code": "088691",
478
+ "report_date": "2026-10-27",
479
+ "open_interest": 396810,
480
+ "legacy": {
481
+ "noncommercial": {
482
+ "net": 186451,
483
+ "net_change_1w": -13414,
484
+ "net_pct_oi": 46.99
485
+ },
486
+ "commercial": {
487
+ "net": -215624,
488
+ "net_change_1w": 19958,
489
+ "net_pct_oi": -54.34
490
+ },
491
+ "nonreportable": {
492
+ "net": 29173,
493
+ "net_change_1w": -6544,
494
+ "net_pct_oi": 7.35
495
+ }
496
+ },
497
+ "released": "2026-10-30T19:30:00.000Z"
498
+ }
499
+ ]
500
+ }
@@ -0,0 +1 @@
1
+ {"market":"EUR","name":"Euro FX","cftc_code":"099741","exchange":"CME","report":"legacy","source":"CFTC Commitments of Traders, Legacy - Futures Only (publicreporting.cftc.gov, dataset 6dca-aqww)","first":"2026-10-13","last":"2026-10-27","columns":["date","open_interest","noncommercial_long","noncommercial_short","noncommercial_spreading","noncommercial_net","noncommercial_net_pct_oi","commercial_long","commercial_short","commercial_net","commercial_net_pct_oi","nonreportable_long","nonreportable_short","nonreportable_net","nonreportable_net_pct_oi"],"rows":[["2026-10-13",874541,222929,360502,45685,-137573,-15.73,521361,410136,111225,12.72,84566,58218,26348,3.01],["2026-10-20",877940,226421,360945,43294,-134524,-15.32,522461,417566,104895,11.95,85764,56135,29629,3.37],["2026-10-27",868188,230646,374540,49306,-143894,-16.57,504851,390367,114484,13.19,83385,53975,29410,3.39]]}
@@ -0,0 +1,217 @@
1
+ {
2
+ "markets": [
3
+ {
4
+ "id": "EUR",
5
+ "name": "Euro FX",
6
+ "code": "099741",
7
+ "exchange": "CME",
8
+ "reports": {
9
+ "legacy": {
10
+ "first": "2010-01-05",
11
+ "last": "2026-10-27",
12
+ "weeks": 878,
13
+ "csv": "data/legacy/EUR.csv",
14
+ "json": "data/legacy/EUR.json"
15
+ },
16
+ "tff": {
17
+ "first": "2010-01-05",
18
+ "last": "2026-10-27",
19
+ "weeks": 878,
20
+ "csv": "data/tff/EUR.csv",
21
+ "json": "data/tff/EUR.json"
22
+ }
23
+ }
24
+ },
25
+ {
26
+ "id": "GBP",
27
+ "name": "British pound",
28
+ "code": "096742",
29
+ "exchange": "CME",
30
+ "reports": {
31
+ "legacy": {
32
+ "first": "2010-01-05",
33
+ "last": "2026-10-27",
34
+ "weeks": 878,
35
+ "csv": "data/legacy/GBP.csv",
36
+ "json": "data/legacy/GBP.json"
37
+ },
38
+ "tff": {
39
+ "first": "2010-01-05",
40
+ "last": "2026-10-27",
41
+ "weeks": 878,
42
+ "csv": "data/tff/GBP.csv",
43
+ "json": "data/tff/GBP.json"
44
+ }
45
+ }
46
+ },
47
+ {
48
+ "id": "JPY",
49
+ "name": "Japanese yen",
50
+ "code": "097741",
51
+ "exchange": "CME",
52
+ "reports": {
53
+ "legacy": {
54
+ "first": "2010-01-05",
55
+ "last": "2026-10-27",
56
+ "weeks": 878,
57
+ "csv": "data/legacy/JPY.csv",
58
+ "json": "data/legacy/JPY.json"
59
+ },
60
+ "tff": {
61
+ "first": "2010-01-05",
62
+ "last": "2026-10-27",
63
+ "weeks": 878,
64
+ "csv": "data/tff/JPY.csv",
65
+ "json": "data/tff/JPY.json"
66
+ }
67
+ }
68
+ },
69
+ {
70
+ "id": "CHF",
71
+ "name": "Swiss franc",
72
+ "code": "092741",
73
+ "exchange": "CME",
74
+ "reports": {
75
+ "legacy": {
76
+ "first": "2010-01-05",
77
+ "last": "2026-10-27",
78
+ "weeks": 878,
79
+ "csv": "data/legacy/CHF.csv",
80
+ "json": "data/legacy/CHF.json"
81
+ },
82
+ "tff": {
83
+ "first": "2010-01-05",
84
+ "last": "2026-10-27",
85
+ "weeks": 878,
86
+ "csv": "data/tff/CHF.csv",
87
+ "json": "data/tff/CHF.json"
88
+ }
89
+ }
90
+ },
91
+ {
92
+ "id": "CAD",
93
+ "name": "Canadian dollar",
94
+ "code": "090741",
95
+ "exchange": "CME",
96
+ "reports": {
97
+ "legacy": {
98
+ "first": "2010-01-05",
99
+ "last": "2026-10-27",
100
+ "weeks": 878,
101
+ "csv": "data/legacy/CAD.csv",
102
+ "json": "data/legacy/CAD.json"
103
+ },
104
+ "tff": {
105
+ "first": "2010-01-05",
106
+ "last": "2026-10-27",
107
+ "weeks": 878,
108
+ "csv": "data/tff/CAD.csv",
109
+ "json": "data/tff/CAD.json"
110
+ }
111
+ }
112
+ },
113
+ {
114
+ "id": "AUD",
115
+ "name": "Australian dollar",
116
+ "code": "232741",
117
+ "exchange": "CME",
118
+ "reports": {
119
+ "legacy": {
120
+ "first": "2010-01-05",
121
+ "last": "2026-10-27",
122
+ "weeks": 878,
123
+ "csv": "data/legacy/AUD.csv",
124
+ "json": "data/legacy/AUD.json"
125
+ },
126
+ "tff": {
127
+ "first": "2010-01-05",
128
+ "last": "2026-10-27",
129
+ "weeks": 878,
130
+ "csv": "data/tff/AUD.csv",
131
+ "json": "data/tff/AUD.json"
132
+ }
133
+ }
134
+ },
135
+ {
136
+ "id": "NZD",
137
+ "name": "New Zealand dollar",
138
+ "code": "112741",
139
+ "exchange": "CME",
140
+ "reports": {
141
+ "legacy": {
142
+ "first": "2010-01-05",
143
+ "last": "2026-10-27",
144
+ "weeks": 878,
145
+ "csv": "data/legacy/NZD.csv",
146
+ "json": "data/legacy/NZD.json"
147
+ },
148
+ "tff": {
149
+ "first": "2010-01-05",
150
+ "last": "2026-10-27",
151
+ "weeks": 878,
152
+ "csv": "data/tff/NZD.csv",
153
+ "json": "data/tff/NZD.json"
154
+ }
155
+ }
156
+ },
157
+ {
158
+ "id": "MXN",
159
+ "name": "Mexican peso",
160
+ "code": "095741",
161
+ "exchange": "CME",
162
+ "reports": {
163
+ "legacy": {
164
+ "first": "2010-01-05",
165
+ "last": "2026-10-27",
166
+ "weeks": 878,
167
+ "csv": "data/legacy/MXN.csv",
168
+ "json": "data/legacy/MXN.json"
169
+ },
170
+ "tff": {
171
+ "first": "2010-01-05",
172
+ "last": "2026-10-27",
173
+ "weeks": 878,
174
+ "csv": "data/tff/MXN.csv",
175
+ "json": "data/tff/MXN.json"
176
+ }
177
+ }
178
+ },
179
+ {
180
+ "id": "USD",
181
+ "name": "US dollar index",
182
+ "code": "098662",
183
+ "exchange": "ICE Futures U.S.",
184
+ "reports": {
185
+ "legacy": {
186
+ "first": "2010-01-05",
187
+ "last": "2026-10-27",
188
+ "weeks": 878,
189
+ "csv": "data/legacy/USD.csv",
190
+ "json": "data/legacy/USD.json"
191
+ },
192
+ "tff": {
193
+ "first": "2010-01-05",
194
+ "last": "2026-10-27",
195
+ "weeks": 878,
196
+ "csv": "data/tff/USD.csv",
197
+ "json": "data/tff/USD.json"
198
+ }
199
+ }
200
+ },
201
+ {
202
+ "id": "XAU",
203
+ "name": "Gold",
204
+ "code": "088691",
205
+ "exchange": "COMEX",
206
+ "reports": {
207
+ "legacy": {
208
+ "first": "2010-01-05",
209
+ "last": "2026-10-27",
210
+ "weeks": 878,
211
+ "csv": "data/legacy/XAU.csv",
212
+ "json": "data/legacy/XAU.json"
213
+ }
214
+ }
215
+ }
216
+ ]
217
+ }
@@ -0,0 +1,70 @@
1
+ import io
2
+ import json
3
+ import pathlib
4
+ import unittest
5
+ from contextlib import redirect_stdout
6
+
7
+ import forexia_cot as cot
8
+ from forexia_cot.__main__ import main
9
+
10
+ FIXTURES = pathlib.Path(__file__).parent / "fixtures"
11
+ FILES = {
12
+ "/legacy/EUR.json": (FIXTURES / "legacy-EUR.json").read_text(encoding="utf-8"),
13
+ "/latest.json": (FIXTURES / "latest.json").read_text(encoding="utf-8"),
14
+ "/markets.json": (FIXTURES / "markets.json").read_text(encoding="utf-8"),
15
+ }
16
+
17
+
18
+ def fetch(url: str) -> str:
19
+ path = url[len(cot.BASE):]
20
+ if path not in FILES:
21
+ raise FileNotFoundError(path)
22
+ return FILES[path]
23
+
24
+
25
+ class Markets(unittest.TestCase):
26
+ def test_names(self):
27
+ self.assertEqual(cot.market_of("eur"), "EUR")
28
+ self.assertEqual(cot.market_of("EUR/USD"), "EUR")
29
+ self.assertEqual(cot.market_of("USDJPY"), "JPY")
30
+ self.assertEqual(cot.market_of("gold"), "XAU")
31
+ self.assertEqual(cot.market_of("DXY"), "USD")
32
+ with self.assertRaises(ValueError):
33
+ cot.market_of("BTC")
34
+
35
+
36
+ class Client(unittest.TestCase):
37
+ def test_get_cot(self):
38
+ rows = cot.get_cot("EUR", fetch=fetch)
39
+ self.assertEqual(len(rows), 3)
40
+ self.assertEqual(rows[-1]["date"], "2026-10-27")
41
+ self.assertEqual(rows[-1]["noncommercial_net"], -143894)
42
+ self.assertEqual(rows[-1]["noncommercial_net_pct_oi"], -16.57)
43
+ self.assertNotIn("noncommercial_extreme", rows[-1])
44
+ self.assertEqual(len(cot.get_cot("EURUSD", since="2026-10-20", fetch=fetch)), 2)
45
+ with self.assertRaises(ValueError):
46
+ cot.get_cot("XAU", report="tff", fetch=fetch)
47
+
48
+ def test_latest_and_markets(self):
49
+ eur = next(m for m in cot.latest(fetch)["markets"] if m["id"] == "EUR")
50
+ self.assertEqual(eur["legacy"]["noncommercial"], {"net": -143894, "net_change_1w": -9370, "net_pct_oi": -16.57})
51
+ self.assertNotIn("extremes", cot.latest(fetch))
52
+ self.assertEqual(len(cot.markets(fetch)), 10)
53
+
54
+ def test_cli(self):
55
+ original = cot.latest
56
+ try:
57
+ import forexia_cot.__main__ as m
58
+
59
+ m.latest = lambda: json.loads(FILES["/latest.json"])
60
+ out = io.StringIO()
61
+ with redirect_stdout(out):
62
+ main([])
63
+ self.assertIn("EUR", out.getvalue())
64
+ self.assertIn("via=cot-data", out.getvalue())
65
+ finally:
66
+ m.latest = original
67
+
68
+
69
+ if __name__ == "__main__":
70
+ unittest.main()