cotmetrics 0.1.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.
Files changed (49) hide show
  1. cotmetrics-0.1.0/LICENSE +21 -0
  2. cotmetrics-0.1.0/PKG-INFO +75 -0
  3. cotmetrics-0.1.0/README.md +43 -0
  4. cotmetrics-0.1.0/pyproject.toml +52 -0
  5. cotmetrics-0.1.0/setup.cfg +4 -0
  6. cotmetrics-0.1.0/src/cotmetrics/CotDatabase.py +220 -0
  7. cotmetrics-0.1.0/src/cotmetrics/CotIndexer.py +1707 -0
  8. cotmetrics-0.1.0/src/cotmetrics/__init__.py +12 -0
  9. cotmetrics-0.1.0/src/cotmetrics/conditions.py +395 -0
  10. cotmetrics-0.1.0/src/cotmetrics/config.py +53 -0
  11. cotmetrics-0.1.0/src/cotmetrics/constants.py +527 -0
  12. cotmetrics-0.1.0/src/cotmetrics/database.py +9 -0
  13. cotmetrics-0.1.0/src/cotmetrics/etl.py +149 -0
  14. cotmetrics-0.1.0/src/cotmetrics/indexer.py +73 -0
  15. cotmetrics-0.1.0/src/cotmetrics/indicators.py +294 -0
  16. cotmetrics-0.1.0/src/cotmetrics/market_data.py +28 -0
  17. cotmetrics-0.1.0/src/cotmetrics/models.py +174 -0
  18. cotmetrics-0.1.0/src/cotmetrics/movers.py +287 -0
  19. cotmetrics-0.1.0/src/cotmetrics/options_data.py +376 -0
  20. cotmetrics-0.1.0/src/cotmetrics/params.yaml +41 -0
  21. cotmetrics-0.1.0/src/cotmetrics/pipelines/01_etl_downloader.py +56 -0
  22. cotmetrics-0.1.0/src/cotmetrics/pipelines/__init__.py +0 -0
  23. cotmetrics-0.1.0/src/cotmetrics/pipelines/etl_scheduler.py +235 -0
  24. cotmetrics-0.1.0/src/cotmetrics/report_styles.py +225 -0
  25. cotmetrics-0.1.0/src/cotmetrics/reports.py +289 -0
  26. cotmetrics-0.1.0/src/cotmetrics/signals.py +1091 -0
  27. cotmetrics-0.1.0/src/cotmetrics/symbol_code_map.py +613 -0
  28. cotmetrics-0.1.0/src/cotmetrics/synthesis.py +278 -0
  29. cotmetrics-0.1.0/src/cotmetrics/utils.py +260 -0
  30. cotmetrics-0.1.0/src/cotmetrics.egg-info/PKG-INFO +75 -0
  31. cotmetrics-0.1.0/src/cotmetrics.egg-info/SOURCES.txt +47 -0
  32. cotmetrics-0.1.0/src/cotmetrics.egg-info/dependency_links.txt +1 -0
  33. cotmetrics-0.1.0/src/cotmetrics.egg-info/requires.txt +21 -0
  34. cotmetrics-0.1.0/src/cotmetrics.egg-info/top_level.txt +1 -0
  35. cotmetrics-0.1.0/tests/test_basis.py +202 -0
  36. cotmetrics-0.1.0/tests/test_conditions.py +245 -0
  37. cotmetrics-0.1.0/tests/test_etl.py +124 -0
  38. cotmetrics-0.1.0/tests/test_etl_scheduler_rebuild.py +67 -0
  39. cotmetrics-0.1.0/tests/test_index_ramp.py +317 -0
  40. cotmetrics-0.1.0/tests/test_indexer_lazy.py +88 -0
  41. cotmetrics-0.1.0/tests/test_indicators.py +94 -0
  42. cotmetrics-0.1.0/tests/test_is_setup.py +69 -0
  43. cotmetrics-0.1.0/tests/test_models.py +307 -0
  44. cotmetrics-0.1.0/tests/test_movers.py +303 -0
  45. cotmetrics-0.1.0/tests/test_obos_concentration.py +220 -0
  46. cotmetrics-0.1.0/tests/test_options_cache.py +115 -0
  47. cotmetrics-0.1.0/tests/test_price_trend.py +78 -0
  48. cotmetrics-0.1.0/tests/test_roles.py +104 -0
  49. cotmetrics-0.1.0/tests/test_signals.py +175 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Spinola
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,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: cotmetrics
3
+ Version: 0.1.0
4
+ Summary: Turn raw COT data into positioning metrics and trading signals (positioning index, concentration/clustering/position-size, reversal signals).
5
+ Author-email: "Matt S." <cotanalyzer@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mspinola/cotmetrics
8
+ Project-URL: Repository, https://github.com/mspinola/cotmetrics
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: pandas>=1.5
16
+ Requires-Dist: numpy
17
+ Requires-Dist: pyarrow>=10
18
+ Requires-Dist: PyYAML>=6.0
19
+ Requires-Dist: pytz
20
+ Requires-Dist: requests>=2.28
21
+ Requires-Dist: xlrd>=2.0
22
+ Requires-Dist: cotdata>=0.1.0
23
+ Provides-Extra: options
24
+ Requires-Dist: yfinance; extra == "options"
25
+ Provides-Extra: scheduler
26
+ Requires-Dist: schedule; extra == "scheduler"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Requires-Dist: ruff==0.15.22; extra == "dev"
30
+ Requires-Dist: tomli; python_version < "3.11" and extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # cotmetrics
34
+
35
+ Turn raw COT (Commitments of Traders) data into positioning metrics and trading
36
+ signals — the positioning index, concentration / clustering / position-size,
37
+ reversal signals, and the `CotIndexer` that assembles per-instrument weekly panels.
38
+
39
+ Split out of `cot-analyzer` so the metrics layer installs without the Dash/Plotly
40
+ dashboard stack. Reads prices/COT from the shared [`cotdata`](../cotdata) store.
41
+
42
+ ## Install (workspace, editable)
43
+
44
+ ```bash
45
+ pip install -e ../cotdata -e .[options,scheduler,dev]
46
+ export COTDATA_STORE=~/code/cotdata_store # shared data store
47
+ export COTMETRICS_CACHE=~/.cache/cotmetrics # derived per-instrument parquet cache
48
+ ```
49
+
50
+ ## Use
51
+
52
+ ```python
53
+ import cotmetrics # flat metric fns
54
+ from cotmetrics.indexer import cotIndexer, boot_options_update
55
+ from cotmetrics.signals import append_trading_signals
56
+ cotmetrics.calculate_cot_index(...)
57
+ ```
58
+
59
+ `import cotmetrics` is side-effect-free. Constructing the indexer
60
+ (`from cotmetrics.indexer import cotIndexer`) loads the store; the daily options
61
+ fetch runs only when you call `boot_options_update()` explicitly.
62
+
63
+ ## Config / paths
64
+
65
+ | env | meaning | default |
66
+ |-----|---------|---------|
67
+ | `COTDATA_STORE` | shared price/COT store (from cotdata) | required |
68
+ | `COTMETRICS_CACHE` | derived per-instrument parquet cache | `~/.cache/cotmetrics` |
69
+ | `COTMETRICS_PARAMS` | instrument/params config | packaged **sample** `params.yaml` |
70
+ | `COTMETRICS_DATA` | legacy raw_cot_data.parquet + real_test_data exports | `<cache>/../cotmetrics_data` |
71
+
72
+ The packaged `params.yaml` is a small **generic sample** (a handful of well-known
73
+ symbols, untuned 52-week lookbacks) so cotmetrics runs out of the box. For a real
74
+ instrument universe and any tuned parameters, set `COTMETRICS_PARAMS` to your own
75
+ config file.
@@ -0,0 +1,43 @@
1
+ # cotmetrics
2
+
3
+ Turn raw COT (Commitments of Traders) data into positioning metrics and trading
4
+ signals — the positioning index, concentration / clustering / position-size,
5
+ reversal signals, and the `CotIndexer` that assembles per-instrument weekly panels.
6
+
7
+ Split out of `cot-analyzer` so the metrics layer installs without the Dash/Plotly
8
+ dashboard stack. Reads prices/COT from the shared [`cotdata`](../cotdata) store.
9
+
10
+ ## Install (workspace, editable)
11
+
12
+ ```bash
13
+ pip install -e ../cotdata -e .[options,scheduler,dev]
14
+ export COTDATA_STORE=~/code/cotdata_store # shared data store
15
+ export COTMETRICS_CACHE=~/.cache/cotmetrics # derived per-instrument parquet cache
16
+ ```
17
+
18
+ ## Use
19
+
20
+ ```python
21
+ import cotmetrics # flat metric fns
22
+ from cotmetrics.indexer import cotIndexer, boot_options_update
23
+ from cotmetrics.signals import append_trading_signals
24
+ cotmetrics.calculate_cot_index(...)
25
+ ```
26
+
27
+ `import cotmetrics` is side-effect-free. Constructing the indexer
28
+ (`from cotmetrics.indexer import cotIndexer`) loads the store; the daily options
29
+ fetch runs only when you call `boot_options_update()` explicitly.
30
+
31
+ ## Config / paths
32
+
33
+ | env | meaning | default |
34
+ |-----|---------|---------|
35
+ | `COTDATA_STORE` | shared price/COT store (from cotdata) | required |
36
+ | `COTMETRICS_CACHE` | derived per-instrument parquet cache | `~/.cache/cotmetrics` |
37
+ | `COTMETRICS_PARAMS` | instrument/params config | packaged **sample** `params.yaml` |
38
+ | `COTMETRICS_DATA` | legacy raw_cot_data.parquet + real_test_data exports | `<cache>/../cotmetrics_data` |
39
+
40
+ The packaged `params.yaml` is a small **generic sample** (a handful of well-known
41
+ symbols, untuned 52-week lookbacks) so cotmetrics runs out of the box. For a real
42
+ instrument universe and any tuned parameters, set `COTMETRICS_PARAMS` to your own
43
+ config file.
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cotmetrics"
7
+ version = "0.1.0"
8
+ description = "Turn raw COT data into positioning metrics and trading signals (positioning index, concentration/clustering/position-size, reversal signals)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ {name = "Matt S.", email = "cotanalyzer@gmail.com"}
13
+ ]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ requires-python = ">=3.9"
20
+ dependencies = [
21
+ "pandas>=1.5",
22
+ "numpy",
23
+ "pyarrow>=10",
24
+ "PyYAML>=6.0",
25
+ "pytz",
26
+ "requests>=2.28", # legacy CFTC ETL (cotmetrics.etl)
27
+ "xlrd>=2.0", # legacy CFTC dea_fut_xls are .xls
28
+ "cotdata>=0.1.0", # shared data layer; editable in the workspace: -e ../cotdata
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ options = ["yfinance"] # cotmetrics.options_data (max-pain snapshots)
33
+ scheduler = ["schedule"] # cotmetrics.pipelines.etl_scheduler
34
+ dev = ["pytest>=7", "ruff==0.15.22", 'tomli; python_version < "3.11"'] # tomli for scripts/check_dep_floors.py on <3.11
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/mspinola/cotmetrics"
38
+ Repository = "https://github.com/mspinola/cotmetrics"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.setuptools.package-data]
44
+ cotmetrics = ["*.yaml"]
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+ target-version = "py39"
49
+
50
+ [tool.ruff.lint]
51
+ select = ["E", "F", "W", "I"]
52
+ ignore = ["E501"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,220 @@
1
+ import os
2
+ import sqlite3
3
+ from datetime import datetime, timezone
4
+ from zoneinfo import ZoneInfo
5
+
6
+ import pandas as pd
7
+
8
+ import cotmetrics.utils as utils
9
+
10
+
11
+ class CotDatabase:
12
+ """Class to manage COT data"""
13
+ def __init__(self, db_name=None):
14
+ if db_name is None:
15
+ # Resolve absolutely to cot-analyzer/data/cot_data.db
16
+ base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
17
+ self.db_name = os.path.join(base_dir, 'data', 'cot_data.db')
18
+ else:
19
+ self.db_name = db_name
20
+
21
+ # Ensure directories exist
22
+ os.makedirs(os.path.dirname(self.db_name), exist_ok=True)
23
+
24
+ self.setup_database()
25
+
26
+ def setup_database(self):
27
+ """Create the database and the necessary table if it doesn't exist."""
28
+ conn = sqlite3.connect(self.db_name)
29
+ c = conn.cursor()
30
+ c.execute('''
31
+ CREATE TABLE IF NOT EXISTS zip_files (
32
+ year INTEGER PRIMARY KEY,
33
+ last_modified TEXT
34
+ )
35
+ ''')
36
+
37
+ # Add visitor logs table
38
+ c.execute('''
39
+ CREATE TABLE IF NOT EXISTS visitor_logs (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ timestamp TEXT,
42
+ ip_address TEXT,
43
+ path TEXT,
44
+ user_agent TEXT,
45
+ city TEXT,
46
+ country TEXT
47
+ )
48
+ ''')
49
+
50
+ # Add ml_predictions_v2 table
51
+ c.execute('''
52
+ CREATE TABLE IF NOT EXISTS ml_predictions_v2 (
53
+ symbol TEXT,
54
+ report_date TEXT,
55
+ prob_success REAL,
56
+ meta_side INTEGER,
57
+ expectancy REAL,
58
+ atr_mult_tp REAL,
59
+ atr_mult_sl REAL,
60
+ updated_at TEXT,
61
+ PRIMARY KEY (symbol, report_date)
62
+ )
63
+ ''')
64
+
65
+ # Safely add new columns to existing table
66
+ for col, col_type in [('expectancy', 'REAL'), ('atr_mult_tp', 'REAL'), ('atr_mult_sl', 'REAL')]:
67
+ try:
68
+ c.execute(f'ALTER TABLE ml_predictions_v2 ADD COLUMN {col} {col_type}')
69
+ except sqlite3.OperationalError:
70
+ pass
71
+ conn.commit()
72
+ conn.close()
73
+
74
+ def log_visit(self, ip, path, ua, city="Unknown", country="Unknown"):
75
+ """Records a new visitor event to the database."""
76
+ conn = sqlite3.connect(self.db_name)
77
+ c = conn.cursor()
78
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
79
+ c.execute('''
80
+ INSERT INTO visitor_logs (timestamp, ip_address, path, user_agent, city, country)
81
+ VALUES (?, ?, ?, ?, ?, ?)
82
+ ''', (now, ip, path, ua, city, country))
83
+ conn.commit()
84
+ conn.close()
85
+
86
+ def get_visitor_stats(self):
87
+ """Retrieves recent logs for the admin dashboard."""
88
+ conn = sqlite3.connect(self.db_name)
89
+ df = pd.read_sql_query("SELECT * FROM visitor_logs ORDER BY id DESC LIMIT 500", conn)
90
+ conn.close()
91
+ return df
92
+
93
+ def update_zip_file(self, year, last_modified):
94
+ """Update the last modified date of the zip file in the database."""
95
+ conn = sqlite3.connect(self.db_name)
96
+ c = conn.cursor()
97
+ c.execute('''
98
+ INSERT INTO zip_files (year, last_modified) VALUES (?, ?)
99
+ ON CONFLICT(year) DO UPDATE SET last_modified = ?
100
+ ''', (year, last_modified, last_modified))
101
+ conn.commit()
102
+ conn.close()
103
+ utils.get_cot_logger().warning(f"Updated latest zipfile time in DB {year} {last_modified}")
104
+
105
+ def get_zipfile_last_modified_time(self, year):
106
+ conn = sqlite3.connect(self.db_name)
107
+ c = conn.cursor()
108
+ c.execute('SELECT last_modified FROM zip_files WHERE year = ?', (year,))
109
+ row = c.fetchone()
110
+ conn.close()
111
+
112
+ result = None
113
+ if row:
114
+ try:
115
+ result = datetime.strptime(row[0], '%a, %d %b %Y %H:%M:%S %Z')
116
+ except Exception as e:
117
+ utils.get_cot_logger().debug(f"First attempt exception in date format {result}. {e}")
118
+ try:
119
+ utils.get_cot_logger().debug(f"2nd attempt on {row[0]}")
120
+ result = datetime.strptime(row[0], '%Y-%m-%d %H:%M:%S')
121
+ utils.get_cot_logger().debug(f"2nd time worked {result}")
122
+ except Exception as e:
123
+ utils.get_cot_logger().error(f"2nd time exception in date format {result}. {e}")
124
+ return None
125
+ return result
126
+
127
+ def latest_update_timestamp(self):
128
+ tz_aware = "Unknown"
129
+ year = str(datetime.now().year)
130
+ result = self.get_zipfile_last_modified_time(year)
131
+ if result is not None:
132
+ tz_aware = result.replace(tzinfo=timezone.utc)
133
+ tz_aware = tz_aware.astimezone(ZoneInfo("America/New_York"))
134
+ tz_aware = tz_aware.strftime("%Y-%m-%d %H:%M:%S %Z")
135
+ if tz_aware is None:
136
+ tz_aware = "Unknown"
137
+ return tz_aware
138
+
139
+ def save_predictions(self, symbol, report_date, prob_success, meta_side, expectancy=None, atr_mult_tp=None, atr_mult_sl=None):
140
+ conn = sqlite3.connect(self.db_name)
141
+ c = conn.cursor()
142
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
143
+ c.execute('''
144
+ INSERT INTO ml_predictions_v2 (
145
+ symbol, report_date, prob_success, meta_side, expectancy, atr_mult_tp, atr_mult_sl, updated_at
146
+ )
147
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
148
+ ON CONFLICT(symbol, report_date) DO UPDATE SET
149
+ prob_success = excluded.prob_success,
150
+ meta_side = excluded.meta_side,
151
+ expectancy = excluded.expectancy,
152
+ atr_mult_tp = excluded.atr_mult_tp,
153
+ atr_mult_sl = excluded.atr_mult_sl,
154
+ updated_at = excluded.updated_at
155
+ ''', (symbol, report_date, prob_success, meta_side, expectancy, atr_mult_tp, atr_mult_sl, now))
156
+ conn.commit()
157
+ conn.close()
158
+
159
+ def get_latest_prediction(self, symbol):
160
+ conn = sqlite3.connect(self.db_name)
161
+ c = conn.cursor()
162
+ c.execute('''
163
+ SELECT report_date, prob_success, meta_side, updated_at
164
+ FROM ml_predictions_v2
165
+ WHERE symbol = ?
166
+ ORDER BY report_date DESC LIMIT 1
167
+ ''', (symbol,))
168
+ row = c.fetchone()
169
+ conn.close()
170
+ if row:
171
+ return {
172
+ "report_date": row[0],
173
+ "prob_success": row[1],
174
+ "meta_side": row[2],
175
+ "updated_at": row[3]
176
+ }
177
+ return None
178
+
179
+ def get_prediction(self, symbol, report_date):
180
+ conn = sqlite3.connect(self.db_name)
181
+ c = conn.cursor()
182
+ c.execute('''
183
+ SELECT report_date, prob_success, meta_side, updated_at
184
+ FROM ml_predictions_v2
185
+ WHERE symbol = ? AND report_date = ?
186
+ ''', (symbol, report_date))
187
+ row = c.fetchone()
188
+ conn.close()
189
+ if row:
190
+ return {
191
+ "report_date": row[0],
192
+ "prob_success": row[1],
193
+ "meta_side": row[2],
194
+ "updated_at": row[3]
195
+ }
196
+ return None
197
+
198
+ def get_all_predictions(self, symbol):
199
+ conn = sqlite3.connect(self.db_name)
200
+ c = conn.cursor()
201
+ c.execute('''
202
+ SELECT report_date, prob_success, meta_side, updated_at
203
+ FROM ml_predictions_v2
204
+ WHERE symbol = ?
205
+ ORDER BY report_date ASC
206
+ ''', (symbol,))
207
+ rows = c.fetchall()
208
+ conn.close()
209
+
210
+ predictions = []
211
+ for row in rows:
212
+ predictions.append({
213
+ "report_date": row[0],
214
+ "prob_success": row[1],
215
+ "meta_side": row[2],
216
+ "updated_at": row[3]
217
+ })
218
+ return predictions
219
+
220
+