ml4t-data 0.1.0__py3-none-any.whl

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 (171) hide show
  1. ml4t/data/AGENTS.md +46 -0
  2. ml4t/data/__init__.py +54 -0
  3. ml4t/data/_version.py +24 -0
  4. ml4t/data/adjustments/__init__.py +23 -0
  5. ml4t/data/adjustments/core.py +252 -0
  6. ml4t/data/anomaly/AGENTS.md +16 -0
  7. ml4t/data/anomaly/__init__.py +23 -0
  8. ml4t/data/anomaly/base.py +142 -0
  9. ml4t/data/anomaly/config.py +117 -0
  10. ml4t/data/anomaly/detectors.py +496 -0
  11. ml4t/data/anomaly/manager.py +306 -0
  12. ml4t/data/assets/AGENTS.md +16 -0
  13. ml4t/data/assets/__init__.py +26 -0
  14. ml4t/data/assets/asset_class.py +252 -0
  15. ml4t/data/assets/contracts.py +422 -0
  16. ml4t/data/assets/data/__init__.py +0 -0
  17. ml4t/data/assets/data/crypto_top100.json +102 -0
  18. ml4t/data/assets/data/forex_majors.json +30 -0
  19. ml4t/data/assets/data/nasdaq100.json +102 -0
  20. ml4t/data/assets/data/sp500.json +505 -0
  21. ml4t/data/assets/schemas.py +322 -0
  22. ml4t/data/assets/validation.py +296 -0
  23. ml4t/data/calendar/__init__.py +0 -0
  24. ml4t/data/calendar/crypto.py +366 -0
  25. ml4t/data/cli/__init__.py +69 -0
  26. ml4t/data/cli/batch.py +206 -0
  27. ml4t/data/cli/config.py +83 -0
  28. ml4t/data/cli/core.py +805 -0
  29. ml4t/data/cli/cot.py +278 -0
  30. ml4t/data/cli/futures.py +350 -0
  31. ml4t/data/cli/utils.py +169 -0
  32. ml4t/data/cli_interface.py +59 -0
  33. ml4t/data/config/__init__.py +38 -0
  34. ml4t/data/config/_serialization.py +46 -0
  35. ml4t/data/config/loader.py +331 -0
  36. ml4t/data/config/models.py +607 -0
  37. ml4t/data/config/validator.py +247 -0
  38. ml4t/data/core/AGENTS.md +16 -0
  39. ml4t/data/core/__init__.py +6 -0
  40. ml4t/data/core/config.py +123 -0
  41. ml4t/data/core/exceptions.py +315 -0
  42. ml4t/data/core/models.py +179 -0
  43. ml4t/data/core/schemas.py +467 -0
  44. ml4t/data/cot/AGENTS.md +14 -0
  45. ml4t/data/cot/__init__.py +46 -0
  46. ml4t/data/cot/fetcher.py +695 -0
  47. ml4t/data/cot/workflow.py +519 -0
  48. ml4t/data/crypto/__init__.py +15 -0
  49. ml4t/data/crypto/downloader.py +500 -0
  50. ml4t/data/data_manager.py +584 -0
  51. ml4t/data/etfs/__init__.py +16 -0
  52. ml4t/data/etfs/downloader.py +589 -0
  53. ml4t/data/export/__init__.py +5 -0
  54. ml4t/data/export/formats/__init__.py +14 -0
  55. ml4t/data/export/formats/base.py +145 -0
  56. ml4t/data/export/formats/csv.py +138 -0
  57. ml4t/data/export/formats/excel.py +347 -0
  58. ml4t/data/export/formats/json.py +280 -0
  59. ml4t/data/export/manager.py +364 -0
  60. ml4t/data/futures/AGENTS.md +116 -0
  61. ml4t/data/futures/__init__.py +188 -0
  62. ml4t/data/futures/adjustment.py +115 -0
  63. ml4t/data/futures/book_downloader.py +760 -0
  64. ml4t/data/futures/config.py +230 -0
  65. ml4t/data/futures/continuous.py +227 -0
  66. ml4t/data/futures/continuous_downloader.py +568 -0
  67. ml4t/data/futures/databento_parser.py +607 -0
  68. ml4t/data/futures/definitions.py +334 -0
  69. ml4t/data/futures/downloader.py +857 -0
  70. ml4t/data/futures/individual_downloader.py +501 -0
  71. ml4t/data/futures/parser.py +328 -0
  72. ml4t/data/futures/roll.py +415 -0
  73. ml4t/data/futures/schema.py +433 -0
  74. ml4t/data/macro/__init__.py +15 -0
  75. ml4t/data/macro/downloader.py +465 -0
  76. ml4t/data/managers/__init__.py +46 -0
  77. ml4t/data/managers/async_batch.py +281 -0
  78. ml4t/data/managers/batch_manager.py +409 -0
  79. ml4t/data/managers/bulk_manager.py +326 -0
  80. ml4t/data/managers/config_manager.py +262 -0
  81. ml4t/data/managers/fetch_manager.py +279 -0
  82. ml4t/data/managers/metadata_manager.py +296 -0
  83. ml4t/data/managers/provider_manager.py +378 -0
  84. ml4t/data/managers/storage_manager.py +631 -0
  85. ml4t/data/provider_updater.py +466 -0
  86. ml4t/data/providers/AGENTS.md +63 -0
  87. ml4t/data/providers/__init__.py +216 -0
  88. ml4t/data/providers/alpaca.py +1039 -0
  89. ml4t/data/providers/aqr.py +1185 -0
  90. ml4t/data/providers/async_base.py +264 -0
  91. ml4t/data/providers/base.py +360 -0
  92. ml4t/data/providers/binance.py +415 -0
  93. ml4t/data/providers/binance_public.py +1923 -0
  94. ml4t/data/providers/coingecko.py +766 -0
  95. ml4t/data/providers/cryptocompare.py +576 -0
  96. ml4t/data/providers/databento.py +727 -0
  97. ml4t/data/providers/eodhd.py +624 -0
  98. ml4t/data/providers/fama_french.py +1003 -0
  99. ml4t/data/providers/finnhub.py +444 -0
  100. ml4t/data/providers/fred.py +586 -0
  101. ml4t/data/providers/fundamentals.py +406 -0
  102. ml4t/data/providers/fxmacrodata.py +749 -0
  103. ml4t/data/providers/kalshi.py +801 -0
  104. ml4t/data/providers/learned_synthetic.py +529 -0
  105. ml4t/data/providers/mixins/__init__.py +45 -0
  106. ml4t/data/providers/mixins/async_session.py +143 -0
  107. ml4t/data/providers/mixins/circuit_breaker.py +362 -0
  108. ml4t/data/providers/mixins/rate_limit.py +91 -0
  109. ml4t/data/providers/mixins/retry.py +116 -0
  110. ml4t/data/providers/mixins/session.py +127 -0
  111. ml4t/data/providers/mixins/validation.py +366 -0
  112. ml4t/data/providers/mock.py +355 -0
  113. ml4t/data/providers/nasdaq_itch.py +638 -0
  114. ml4t/data/providers/oanda.py +328 -0
  115. ml4t/data/providers/okx.py +602 -0
  116. ml4t/data/providers/polygon.py +574 -0
  117. ml4t/data/providers/polymarket.py +976 -0
  118. ml4t/data/providers/protocols.py +249 -0
  119. ml4t/data/providers/registry.py +348 -0
  120. ml4t/data/providers/synthetic.py +543 -0
  121. ml4t/data/providers/tiingo.py +238 -0
  122. ml4t/data/providers/twelve_data.py +266 -0
  123. ml4t/data/providers/wiki_prices.py +807 -0
  124. ml4t/data/providers/yahoo.py +658 -0
  125. ml4t/data/py.typed +0 -0
  126. ml4t/data/security/__init__.py +5 -0
  127. ml4t/data/security/path_validator.py +255 -0
  128. ml4t/data/sessions/__init__.py +10 -0
  129. ml4t/data/sessions/assigner.py +287 -0
  130. ml4t/data/sessions/completer.py +359 -0
  131. ml4t/data/storage/AGENTS.md +45 -0
  132. ml4t/data/storage/__init__.py +92 -0
  133. ml4t/data/storage/async_base.py +102 -0
  134. ml4t/data/storage/backend.py +448 -0
  135. ml4t/data/storage/chunked.py +703 -0
  136. ml4t/data/storage/config.py +162 -0
  137. ml4t/data/storage/data_profile.py +325 -0
  138. ml4t/data/storage/flat.py +188 -0
  139. ml4t/data/storage/hive.py +640 -0
  140. ml4t/data/storage/keys.py +111 -0
  141. ml4t/data/storage/legacy_migration.py +454 -0
  142. ml4t/data/storage/metadata_tracker.py +399 -0
  143. ml4t/data/storage/migration.py +598 -0
  144. ml4t/data/storage/protocols.py +129 -0
  145. ml4t/data/synthetic/__init__.py +51 -0
  146. ml4t/data/synthetic/ohlcv_utils.py +404 -0
  147. ml4t/data/synthetic/registry.py +409 -0
  148. ml4t/data/universe.py +217 -0
  149. ml4t/data/update_manager.py +827 -0
  150. ml4t/data/utils/AGENTS.md +20 -0
  151. ml4t/data/utils/__init__.py +8 -0
  152. ml4t/data/utils/async_rate_limit.py +302 -0
  153. ml4t/data/utils/conversion.py +138 -0
  154. ml4t/data/utils/format.py +342 -0
  155. ml4t/data/utils/gap_optimizer.py +148 -0
  156. ml4t/data/utils/gaps.py +329 -0
  157. ml4t/data/utils/global_rate_limit.py +118 -0
  158. ml4t/data/utils/locking.py +193 -0
  159. ml4t/data/utils/rate_limit.py +87 -0
  160. ml4t/data/utils/retry.py +54 -0
  161. ml4t/data/validation/__init__.py +13 -0
  162. ml4t/data/validation/base.py +86 -0
  163. ml4t/data/validation/cross_validation.py +286 -0
  164. ml4t/data/validation/ohlcv.py +409 -0
  165. ml4t/data/validation/report.py +223 -0
  166. ml4t/data/validation/rules.py +300 -0
  167. ml4t_data-0.1.0.dist-info/METADATA +457 -0
  168. ml4t_data-0.1.0.dist-info/RECORD +171 -0
  169. ml4t_data-0.1.0.dist-info/WHEEL +4 -0
  170. ml4t_data-0.1.0.dist-info/entry_points.txt +2 -0
  171. ml4t_data-0.1.0.dist-info/licenses/LICENSE +21 -0
ml4t/data/AGENTS.md ADDED
@@ -0,0 +1,46 @@
1
+ # ml4t.data - Package Index
2
+
3
+ ## Core Modules
4
+
5
+ | File | Lines | Purpose |
6
+ |------|-------|---------|
7
+ | data_manager.py | 564 | Main orchestration API |
8
+ | update_manager.py | 829 | Incremental update system |
9
+ | universe.py | 987 | Asset universe management |
10
+ | provider_updater.py | 449 | Provider coordination |
11
+
12
+ ## Subpackages
13
+
14
+ | Directory | Lines | Purpose |
15
+ |-----------|-------|---------|
16
+ | providers/ | 14k | 20 live provider adapters + synthetic/testing providers |
17
+ | storage/ | 4.6k | Hive-partitioned backends + profiling |
18
+ | futures/ | 4.6k | Databento futures downloader |
19
+ | etfs/ | 600 | ETFDataManager (Yahoo Finance) |
20
+ | crypto/ | 420 | CryptoDataManager (Binance Public) |
21
+ | core/ | 1k | Models, schemas, config |
22
+ | utils/ | 1.6k | Rate limiting, gaps, retry |
23
+ | assets/ | 1.3k | Asset class definitions |
24
+ | anomaly/ | 1k | Data quality detection |
25
+ | cot/ | 1k | CFTC COT data |
26
+ | validation/ | 1.2k | OHLC validation |
27
+ | sessions/ | 462 | Session assignment |
28
+ | macro/ | 455 | Macro data downloader |
29
+ | calendar/ | 359 | Trading calendars |
30
+ | export/ | 231 | CSV/JSON/Excel export |
31
+
32
+ ## Book Data Managers
33
+
34
+ Simplified managers for ML4T book readers with built-in profiling:
35
+
36
+ | Manager | Asset | Source | Profiling |
37
+ |---------|-------|--------|-----------|
38
+ | `ETFDataManager` | ETFs | Yahoo Finance | `generate_profile()` |
39
+ | `CryptoDataManager` | Crypto | Binance Public | `generate_profile()` |
40
+ | `FuturesDataManager` | Futures | Databento | `generate_profile(product)` |
41
+
42
+ All managers inherit from `ProfileMixin` for on-demand column statistics.
43
+
44
+ ## Key
45
+
46
+ `DataManager`, `UpdateManager`, `HiveStorage`, `ProfileMixin`, `get_provider()`
ml4t/data/__init__.py ADDED
@@ -0,0 +1,54 @@
1
+ """ML4T Data - Modern financial data management library with unified provider interface."""
2
+
3
+ try:
4
+ from ml4t.data._version import __version__
5
+ except ImportError:
6
+ __version__ = "0.0.0.dev0"
7
+ __author__ = "ML4T Team"
8
+ __email__ = "info@ml4trading.io"
9
+
10
+ # Contract specifications (always available - no external dependencies)
11
+ # Asset classes
12
+ from ml4t.data.assets.asset_class import AssetClass
13
+ from ml4t.data.assets.contracts import (
14
+ FUTURES_REGISTRY,
15
+ ContractSpec,
16
+ get_contract_spec,
17
+ load_contract_specs,
18
+ register_contract_spec,
19
+ )
20
+ from ml4t.data.core.exceptions import ProviderRoutingError
21
+
22
+ __all__ = [
23
+ # Contract specifications
24
+ "ContractSpec",
25
+ "FUTURES_REGISTRY",
26
+ "get_contract_spec",
27
+ "load_contract_specs",
28
+ "register_contract_spec",
29
+ "AssetClass",
30
+ "ProviderRoutingError",
31
+ ]
32
+
33
+ # Core imports (may have additional dependencies)
34
+ try:
35
+ from ml4t.data.core.config import Config # noqa: F401
36
+ from ml4t.data.data_manager import DataManager # noqa: F401
37
+ from ml4t.data.providers.base import BaseProvider # noqa: F401
38
+
39
+ __all__.extend(["BaseProvider", "Config", "DataManager"])
40
+ except ImportError:
41
+ # Try to import what's available
42
+ try:
43
+ from ml4t.data.data_manager import DataManager # noqa: F401
44
+
45
+ __all__.append("DataManager")
46
+ except ImportError:
47
+ pass
48
+
49
+ try:
50
+ from ml4t.data.providers.base import BaseProvider # noqa: F401
51
+
52
+ __all__.append("BaseProvider")
53
+ except ImportError:
54
+ pass
ml4t/data/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,23 @@
1
+ """Corporate actions and price adjustment utilities.
2
+
3
+ The canonical convention treats a split ratio as new shares per old share on
4
+ the event date. Raw values are retained, and adjusted values use the share
5
+ basis of the latest observation.
6
+
7
+ Example:
8
+ >>> from ml4t.data.adjustments import apply_corporate_actions
9
+ >>> adjusted_prices = apply_corporate_actions(unadjusted_df)
10
+ >>> adjusted_prices.select("close", "adj_close", "price_adjustment_factor")
11
+ """
12
+
13
+ from .core import (
14
+ apply_corporate_actions,
15
+ apply_dividends,
16
+ apply_splits,
17
+ )
18
+
19
+ __all__ = [
20
+ "apply_splits",
21
+ "apply_dividends",
22
+ "apply_corporate_actions",
23
+ ]
@@ -0,0 +1,252 @@
1
+ """Corporate-action adjustment functions."""
2
+
3
+ import polars as pl
4
+
5
+
6
+ def _validate_adjustment_inputs(
7
+ df: pl.DataFrame,
8
+ *,
9
+ split_col: str,
10
+ dividend_col: str | None,
11
+ price_cols: list[str],
12
+ volume_col: str | None,
13
+ ) -> None:
14
+ required = {"date", split_col, *price_cols}
15
+ if dividend_col is not None:
16
+ required.update(("close", dividend_col))
17
+ missing = sorted(required.difference(df.columns))
18
+ if missing:
19
+ raise ValueError(f"Missing required adjustment columns: {', '.join(missing)}")
20
+ if df.get_column("date").null_count():
21
+ raise ValueError("Corporate-action dates cannot contain null values")
22
+ if df.get_column("date").n_unique() != df.height:
23
+ raise ValueError("Corporate-action input must contain exactly one row per date")
24
+
25
+ finite_columns = [split_col, *price_cols]
26
+ if dividend_col is not None:
27
+ finite_columns.extend(("close", dividend_col))
28
+ if volume_col is not None and volume_col in df.columns:
29
+ finite_columns.append(volume_col)
30
+ for column in dict.fromkeys(finite_columns):
31
+ invalid = df.filter(pl.col(column).is_null() | ~pl.col(column).is_finite())
32
+ if not invalid.is_empty():
33
+ raise ValueError(f"Adjustment column '{column}' must contain finite values")
34
+
35
+ if not df.filter(pl.col(split_col) <= 0).is_empty():
36
+ raise ValueError(f"Split ratios in '{split_col}' must be positive")
37
+ if dividend_col is not None and not df.filter(pl.col("close") <= 0).is_empty():
38
+ raise ValueError("Close prices must be positive")
39
+ if dividend_col is not None and not df.filter(pl.col(dividend_col) < 0).is_empty():
40
+ raise ValueError(f"Dividends in '{dividend_col}' cannot be negative")
41
+ if volume_col is not None and volume_col in df.columns:
42
+ if not df.filter(pl.col(volume_col) < 0).is_empty():
43
+ raise ValueError(f"Volume in '{volume_col}' cannot be negative")
44
+
45
+
46
+ def _future_cumulative_product(column: str) -> pl.Expr:
47
+ return pl.col(column).shift(-1, fill_value=1.0).reverse().cum_prod().reverse()
48
+
49
+
50
+ def _apply_canonical_adjustments(
51
+ prices: pl.DataFrame,
52
+ *,
53
+ split_col: str,
54
+ dividend_col: str | None,
55
+ price_cols: list[str],
56
+ volume_col: str | None,
57
+ ) -> pl.DataFrame:
58
+ _validate_adjustment_inputs(
59
+ prices,
60
+ split_col=split_col,
61
+ dividend_col=dividend_col,
62
+ price_cols=price_cols,
63
+ volume_col=volume_col,
64
+ )
65
+ df = prices.sort("date").clone()
66
+
67
+ price_event_factor = (
68
+ pl.col("close")
69
+ / (pl.col(split_col).cast(pl.Float64) * (pl.col("close") + pl.col(dividend_col)))
70
+ if dividend_col is not None
71
+ else 1.0 / pl.col(split_col).cast(pl.Float64)
72
+ )
73
+ df = df.with_columns(
74
+ price_event_factor.alias("_price_event_factor"),
75
+ pl.col(split_col).cast(pl.Float64).alias("_volume_event_factor"),
76
+ ).with_columns(
77
+ _future_cumulative_product("_price_event_factor").alias("price_adjustment_factor"),
78
+ _future_cumulative_product("_volume_event_factor").alias("volume_adjustment_factor"),
79
+ )
80
+
81
+ adjustments = [
82
+ (pl.col(column) * pl.col("price_adjustment_factor")).alias(f"adj_{column}")
83
+ for column in price_cols
84
+ ]
85
+ if volume_col is not None and volume_col in df.columns:
86
+ adjustments.append(
87
+ (pl.col(volume_col) * pl.col("volume_adjustment_factor")).alias(f"adj_{volume_col}")
88
+ )
89
+ return df.with_columns(adjustments).drop("_price_event_factor", "_volume_event_factor")
90
+
91
+
92
+ def apply_corporate_actions(
93
+ prices: pl.DataFrame,
94
+ split_col: str = "split_ratio",
95
+ dividend_col: str = "ex-dividend",
96
+ price_cols: list[str] | None = None,
97
+ volume_col: str | None = "volume",
98
+ ) -> pl.DataFrame:
99
+ """Adjust prices and volume to the share basis of the latest observation.
100
+
101
+ A split ratio on date ``t`` is new shares per old share and applies between
102
+ the preceding observation and ``t``. The row on ``t`` is already on the new
103
+ share basis. Earlier prices are divided by subsequent split ratios and
104
+ earlier volume is multiplied by them. A dividend is cash per post-event
105
+ share on its ex-date. Adjusted close-to-close returns include that cash.
106
+ The dividend factor uses the ex-date close. This differs from data vendors
107
+ that discount dividends using the prior close.
108
+
109
+ Args:
110
+ prices: DataFrame with date-sorted prices and corporate action data
111
+ split_col: Column with split ratios (default: 'split_ratio')
112
+ dividend_col: Column with dividend amounts (default: 'ex-dividend')
113
+ price_cols: Price columns to adjust (default: ['open', 'high', 'low', 'close'])
114
+ volume_col: Volume column to adjust (default: 'volume')
115
+
116
+ Returns:
117
+ DataFrame retaining raw columns and adding adjusted columns plus explicit
118
+ price and volume adjustment factors
119
+ """
120
+ if price_cols is None:
121
+ price_cols = [
122
+ column for column in ("open", "high", "low", "close") if column in prices.columns
123
+ ]
124
+ return _apply_canonical_adjustments(
125
+ prices,
126
+ split_col=split_col,
127
+ dividend_col=dividend_col,
128
+ price_cols=price_cols,
129
+ volume_col=volume_col,
130
+ )
131
+
132
+
133
+ def apply_splits(
134
+ prices: pl.DataFrame,
135
+ split_col: str = "split_ratio",
136
+ price_cols: list[str] | None = None,
137
+ volume_col: str | None = "volume",
138
+ ) -> pl.DataFrame:
139
+ """Apply the canonical split-only adjustment convention.
140
+
141
+ Args:
142
+ prices: DataFrame with date-sorted prices and split_ratio column
143
+ split_col: Name of column containing split ratios (default: 'split_ratio')
144
+ price_cols: List of price columns to adjust (default: ['open', 'high', 'low', 'close'])
145
+ volume_col: Volume column name to adjust
146
+
147
+ Returns:
148
+ DataFrame retaining raw values and adding adjusted values and factors
149
+ """
150
+ if price_cols is None:
151
+ price_cols = [
152
+ column for column in ("open", "high", "low", "close") if column in prices.columns
153
+ ]
154
+ return _apply_canonical_adjustments(
155
+ prices,
156
+ split_col=split_col,
157
+ dividend_col=None,
158
+ price_cols=price_cols,
159
+ volume_col=volume_col,
160
+ )
161
+
162
+
163
+ def apply_dividends(
164
+ prices: pl.DataFrame,
165
+ dividend_col: str = "ex-dividend",
166
+ price_cols: list[str] | None = None,
167
+ close_col: str = "adj_close",
168
+ ) -> pl.DataFrame:
169
+ """Apply dividend-only adjustments that preserve total close-to-close returns.
170
+
171
+ A dividend on date ``t`` is cash per share paid between the preceding
172
+ observation and ``t``. The event row remains unchanged. This adapter is for
173
+ data whose split adjustments have already been handled consistently. When
174
+ split factors are present, dividends are converted to the latest share basis
175
+ and the dividend factor is composed with the existing price factor.
176
+
177
+ Args:
178
+ prices: DataFrame with date-sorted prices and ex-dividend column
179
+ dividend_col: Name of column containing dividend amounts
180
+ price_cols: List of price columns to adjust
181
+ close_col: Column to use for dividend factor calculation (default: 'adj_close')
182
+
183
+ Returns:
184
+ DataFrame with adjusted price columns and an explicit adjustment factor
185
+ """
186
+ if price_cols is None:
187
+ price_cols = [
188
+ column
189
+ for column in ("adj_open", "adj_high", "adj_low", "adj_close")
190
+ if column in prices.columns
191
+ ]
192
+
193
+ required = {"date", dividend_col, close_col, *price_cols}
194
+ missing = sorted(required.difference(prices.columns))
195
+ if missing:
196
+ raise ValueError(f"Missing required dividend columns: {', '.join(missing)}")
197
+ df = prices.sort("date").clone()
198
+ if df.get_column("date").null_count():
199
+ raise ValueError("Dividend dates cannot contain null values")
200
+ if df.get_column("date").n_unique() != df.height:
201
+ raise ValueError("Dividend input must contain exactly one row per date")
202
+ for column in (dividend_col, close_col, *price_cols):
203
+ if not df.filter(pl.col(column).is_null() | ~pl.col(column).is_finite()).is_empty():
204
+ raise ValueError(f"Dividend adjustment column '{column}' must contain finite values")
205
+ if not df.filter(pl.col(dividend_col) < 0).is_empty():
206
+ raise ValueError(f"Dividends in '{dividend_col}' cannot be negative")
207
+ if not df.filter(pl.col(close_col) <= 0).is_empty():
208
+ raise ValueError(f"Close prices in '{close_col}' must be positive")
209
+
210
+ for factor_column in ("price_adjustment_factor", "volume_adjustment_factor"):
211
+ if (
212
+ factor_column in df.columns
213
+ and not df.filter(
214
+ pl.col(factor_column).is_null()
215
+ | ~pl.col(factor_column).is_finite()
216
+ | (pl.col(factor_column) <= 0)
217
+ ).is_empty()
218
+ ):
219
+ raise ValueError(f"Adjustment factor '{factor_column}' must be finite and positive")
220
+
221
+ rebased_dividend = pl.col(dividend_col)
222
+ if "volume_adjustment_factor" in df.columns:
223
+ rebased_dividend = rebased_dividend / pl.col("volume_adjustment_factor")
224
+ existing_factor = (
225
+ pl.col("price_adjustment_factor")
226
+ if "price_adjustment_factor" in df.columns
227
+ else pl.lit(1.0)
228
+ )
229
+
230
+ return (
231
+ df.with_columns(rebased_dividend.alias("_rebased_dividend"))
232
+ .with_columns(
233
+ (pl.col(close_col) / (pl.col(close_col) + pl.col("_rebased_dividend"))).alias(
234
+ "_dividend_event_factor"
235
+ )
236
+ )
237
+ .with_columns(
238
+ _future_cumulative_product("_dividend_event_factor").alias(
239
+ "_dividend_adjustment_factor"
240
+ )
241
+ )
242
+ .with_columns(
243
+ *[
244
+ (pl.col(column) * pl.col("_dividend_adjustment_factor")).alias(column)
245
+ for column in price_cols
246
+ ],
247
+ (existing_factor * pl.col("_dividend_adjustment_factor")).alias(
248
+ "price_adjustment_factor"
249
+ ),
250
+ )
251
+ .drop("_rebased_dividend", "_dividend_event_factor", "_dividend_adjustment_factor")
252
+ )
@@ -0,0 +1,16 @@
1
+ # anomaly/ - 1k Lines
2
+
3
+ Data quality and anomaly detection.
4
+
5
+ ## Functions
6
+
7
+ | Function | Purpose |
8
+ |----------|---------|
9
+ | detect_anomalies | Find data anomalies |
10
+ | validate_ohlc | OHLC consistency |
11
+ | check_gaps | Missing data detection |
12
+ | flag_outliers | Statistical outliers |
13
+
14
+ ## Key
15
+
16
+ `AnomalyDetector`, `detect_anomalies()`, `DataQualityReport`
@@ -0,0 +1,23 @@
1
+ """Anomaly detection for financial data."""
2
+
3
+ from ml4t.data.anomaly.base import Anomaly, AnomalyDetector, AnomalyReport, AnomalySeverity
4
+ from ml4t.data.anomaly.config import AnomalyConfig, DetectorConfig
5
+ from ml4t.data.anomaly.detectors import (
6
+ PriceStalenessDetector,
7
+ ReturnOutlierDetector,
8
+ VolumeSpikeDetector,
9
+ )
10
+ from ml4t.data.anomaly.manager import AnomalyManager
11
+
12
+ __all__ = [
13
+ "Anomaly",
14
+ "AnomalyConfig",
15
+ "AnomalyDetector",
16
+ "AnomalyManager",
17
+ "AnomalyReport",
18
+ "AnomalySeverity",
19
+ "DetectorConfig",
20
+ "PriceStalenessDetector",
21
+ "ReturnOutlierDetector",
22
+ "VolumeSpikeDetector",
23
+ ]
@@ -0,0 +1,142 @@
1
+ """Base classes for anomaly detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from datetime import datetime
7
+ from enum import StrEnum
8
+ from typing import Any
9
+
10
+ import polars as pl
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class AnomalySeverity(StrEnum):
15
+ """Anomaly severity levels."""
16
+
17
+ INFO = "info"
18
+ WARNING = "warning"
19
+ ERROR = "error"
20
+ CRITICAL = "critical"
21
+
22
+
23
+ class AnomalyType(StrEnum):
24
+ """Types of anomalies."""
25
+
26
+ RETURN_OUTLIER = "return_outlier"
27
+ VOLUME_SPIKE = "volume_spike"
28
+ PRICE_STALE = "price_stale"
29
+ DATA_GAP = "data_gap"
30
+ PRICE_SPIKE = "price_spike"
31
+ ZERO_VOLUME = "zero_volume"
32
+ NEGATIVE_PRICE = "negative_price"
33
+
34
+
35
+ class Anomaly(BaseModel):
36
+ """Represents a detected anomaly."""
37
+
38
+ timestamp: datetime = Field(description="When the anomaly occurred")
39
+ symbol: str = Field(description="Symbol with the anomaly")
40
+ type: AnomalyType = Field(description="Type of anomaly")
41
+ severity: AnomalySeverity = Field(description="Severity level")
42
+ value: float = Field(description="The anomalous value")
43
+ expected_range: tuple[float, float] | None = Field(
44
+ default=None, description="Expected value range"
45
+ )
46
+ threshold: float | None = Field(default=None, description="Detection threshold used")
47
+ message: str = Field(description="Human-readable description")
48
+ metadata: dict[str, Any] = Field(default_factory=dict, description="Additional context")
49
+
50
+ def __str__(self) -> str:
51
+ """String representation."""
52
+ return f"[{self.severity.value.upper()}] {self.symbol} @ {self.timestamp}: {self.message}"
53
+
54
+
55
+ class AnomalyDetector(ABC):
56
+ """Abstract base class for anomaly detectors."""
57
+
58
+ def __init__(self, enabled: bool = True):
59
+ """
60
+ Initialize detector.
61
+
62
+ Args:
63
+ enabled: Whether this detector is enabled
64
+ """
65
+ self.enabled = enabled
66
+
67
+ @abstractmethod
68
+ def detect(self, df: pl.DataFrame, symbol: str) -> list[Anomaly]:
69
+ """
70
+ Detect anomalies in the data.
71
+
72
+ Args:
73
+ df: DataFrame with OHLCV data
74
+ symbol: Symbol being analyzed
75
+
76
+ Returns:
77
+ List of detected anomalies
78
+ """
79
+
80
+ @property
81
+ @abstractmethod
82
+ def name(self) -> str:
83
+ """Return detector name."""
84
+
85
+ def is_enabled(self) -> bool:
86
+ """Check if detector is enabled."""
87
+ return self.enabled
88
+
89
+
90
+ class AnomalyReport(BaseModel):
91
+ """Anomaly detection report."""
92
+
93
+ symbol: str = Field(description="Symbol analyzed")
94
+ start_date: datetime = Field(description="Start of analysis period")
95
+ end_date: datetime = Field(description="End of analysis period")
96
+ total_rows: int = Field(description="Total data points analyzed")
97
+ anomalies: list[Anomaly] = Field(default_factory=list, description="Detected anomalies")
98
+ summary: dict[str, int] = Field(default_factory=dict, description="Summary statistics")
99
+ detectors_used: list[str] = Field(default_factory=list, description="Detectors applied")
100
+
101
+ def add_anomaly(self, anomaly: Anomaly) -> None:
102
+ """Add an anomaly to the report."""
103
+ self.anomalies.append(anomaly)
104
+
105
+ # Update summary
106
+ severity_key = f"{anomaly.severity.value}_count"
107
+ self.summary[severity_key] = self.summary.get(severity_key, 0) + 1
108
+
109
+ type_key = f"{anomaly.type.value}_count"
110
+ self.summary[type_key] = self.summary.get(type_key, 0) + 1
111
+
112
+ def get_critical_anomalies(self) -> list[Anomaly]:
113
+ """Get only critical anomalies."""
114
+ return [a for a in self.anomalies if a.severity == AnomalySeverity.CRITICAL]
115
+
116
+ def get_by_type(self, anomaly_type: AnomalyType) -> list[Anomaly]:
117
+ """Get anomalies of specific type."""
118
+ return [a for a in self.anomalies if a.type == anomaly_type]
119
+
120
+ def has_critical_issues(self) -> bool:
121
+ """Check if there are any critical issues."""
122
+ return any(a.severity == AnomalySeverity.CRITICAL for a in self.anomalies)
123
+
124
+ def to_dataframe(self) -> pl.DataFrame:
125
+ """Convert anomalies to DataFrame for analysis."""
126
+ if not self.anomalies:
127
+ return pl.DataFrame()
128
+
129
+ records = []
130
+ for anomaly in self.anomalies:
131
+ records.append(
132
+ {
133
+ "timestamp": anomaly.timestamp,
134
+ "symbol": anomaly.symbol,
135
+ "type": anomaly.type.value,
136
+ "severity": anomaly.severity.value,
137
+ "value": anomaly.value,
138
+ "message": anomaly.message,
139
+ }
140
+ )
141
+
142
+ return pl.DataFrame(records)