market-data-normalizer 1.3.1__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 (41) hide show
  1. market_data_normalizer-1.3.1/.github/workflows/ci.yml +27 -0
  2. market_data_normalizer-1.3.1/.github/workflows/publish.yml +38 -0
  3. market_data_normalizer-1.3.1/.gitignore +28 -0
  4. market_data_normalizer-1.3.1/CHANGELOG.md +140 -0
  5. market_data_normalizer-1.3.1/LICENSE +21 -0
  6. market_data_normalizer-1.3.1/PKG-INFO +330 -0
  7. market_data_normalizer-1.3.1/README.md +281 -0
  8. market_data_normalizer-1.3.1/examples/demo.py +24 -0
  9. market_data_normalizer-1.3.1/pyproject.toml +47 -0
  10. market_data_normalizer-1.3.1/src/mdnorm/__init__.py +92 -0
  11. market_data_normalizer-1.3.1/src/mdnorm/__main__.py +5 -0
  12. market_data_normalizer-1.3.1/src/mdnorm/bars.py +248 -0
  13. market_data_normalizer-1.3.1/src/mdnorm/cli.py +223 -0
  14. market_data_normalizer-1.3.1/src/mdnorm/csvio.py +79 -0
  15. market_data_normalizer-1.3.1/src/mdnorm/fileio.py +21 -0
  16. market_data_normalizer-1.3.1/src/mdnorm/jsonl.py +101 -0
  17. market_data_normalizer-1.3.1/src/mdnorm/normalizers.py +197 -0
  18. market_data_normalizer-1.3.1/src/mdnorm/pipeline.py +145 -0
  19. market_data_normalizer-1.3.1/src/mdnorm/quality.py +89 -0
  20. market_data_normalizer-1.3.1/src/mdnorm/records.py +72 -0
  21. market_data_normalizer-1.3.1/src/mdnorm/schema.py +71 -0
  22. market_data_normalizer-1.3.1/src/mdnorm/sessions.py +187 -0
  23. market_data_normalizer-1.3.1/src/mdnorm/streams.py +40 -0
  24. market_data_normalizer-1.3.1/src/mdnorm/symbols.py +69 -0
  25. market_data_normalizer-1.3.1/src/mdnorm/timeutil.py +49 -0
  26. market_data_normalizer-1.3.1/tests/test_bars.py +66 -0
  27. market_data_normalizer-1.3.1/tests/test_cli.py +109 -0
  28. market_data_normalizer-1.3.1/tests/test_csvio.py +57 -0
  29. market_data_normalizer-1.3.1/tests/test_event_bars.py +107 -0
  30. market_data_normalizer-1.3.1/tests/test_fill_gaps.py +47 -0
  31. market_data_normalizer-1.3.1/tests/test_gzip_streaming.py +90 -0
  32. market_data_normalizer-1.3.1/tests/test_jsonl.py +94 -0
  33. market_data_normalizer-1.3.1/tests/test_normalizers.py +79 -0
  34. market_data_normalizer-1.3.1/tests/test_pipeline.py +73 -0
  35. market_data_normalizer-1.3.1/tests/test_quality.py +50 -0
  36. market_data_normalizer-1.3.1/tests/test_quotes.py +45 -0
  37. market_data_normalizer-1.3.1/tests/test_records.py +54 -0
  38. market_data_normalizer-1.3.1/tests/test_resample.py +54 -0
  39. market_data_normalizer-1.3.1/tests/test_sessions.py +164 -0
  40. market_data_normalizer-1.3.1/tests/test_streams.py +45 -0
  41. market_data_normalizer-1.3.1/tests/test_symbols.py +43 -0
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Set up Python ${{ matrix.python-version }}
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - name: Install
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install pytest
26
+ - name: Run tests
27
+ run: pytest -q
@@ -0,0 +1,38 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes a release to PyPI when a version tag is pushed.
4
+ # Authentication uses PyPI Trusted Publishing (OIDC) — no long-lived
5
+ # API token is stored anywhere.
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+ - run: pip install pytest
20
+ - run: pytest -q
21
+
22
+ publish:
23
+ needs: test
24
+ runs-on: ubuntu-latest
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write # required for Trusted Publishing
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.12"
33
+ - name: Build distributions
34
+ run: |
35
+ python -m pip install --upgrade build
36
+ python -m build
37
+ - name: Publish
38
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,28 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Tooling
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # OS / editor
20
+ .DS_Store
21
+ .idea/
22
+ .vscode/
23
+
24
+ # Never commit secrets
25
+ *.env
26
+ .env
27
+ *.token
28
+ secrets*
@@ -0,0 +1,140 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
+
6
+ ## [1.3.1] - 2026-08-10
7
+
8
+ ### Changed
9
+ - Packaging: the project is now published on PyPI as
10
+ `market-data-normalizer` (`pip install market-data-normalizer`; the
11
+ import name stays `mdnorm`). Added Python version classifiers, project
12
+ URLs for the changelog and issue tracker, and an install section in the
13
+ README. Releases are published from CI via PyPI Trusted Publishing, so
14
+ no long-lived API token exists. No library code changed in this release.
15
+
16
+ ## [1.3.0] - 2026-08-08
17
+
18
+ ### Added
19
+ - Trading sessions and calendar filtering (`mdnorm.sessions`): a `Session`
20
+ describes a recurring local-time window — `in_session`, `filter_session`,
21
+ `session_date` and `group_by_session_date` decide what belongs to it.
22
+ Handles intraday windows, overnight sessions that cross midnight, and
23
+ daylight-saving transitions via `zoneinfo`; ready-made `US_EQUITY_RTH`
24
+ and `US_FUTURES_OVERNIGHT` are included.
25
+ - Matching `Pipeline` step (`.session(...)`) and CLI flags `--session
26
+ HH:MM-HH:MM` and `--tz ZONE`, applied before aggregation.
27
+
28
+ ### Fixed
29
+ - `canonical_symbol` mangled single-listed instruments: tickers without a
30
+ quote leg were split by a blind 3-character rule, turning `AAPL` into
31
+ `A-APL`. Equities, ETFs and indices now keep their ticker (`AAPL`,
32
+ `SPY`, `BRK.B`), while traded pairs are unchanged.
33
+
34
+ ## [1.2.0] - 2026-08-07
35
+
36
+ ### Added
37
+ - Event-driven bars, the standard alternatives to time bars: ``count_bars``
38
+ (one bar per N trades), ``volume_bars`` (close at a cumulative base-unit
39
+ threshold) and ``dollar_bars`` (close at a traded-notional threshold).
40
+ For these bars ``start_ns`` is the first trade's timestamp and
41
+ ``interval_ns`` the realized span; the trailing partial bar is included.
42
+ - Matching ``Pipeline`` steps (``.count_bars()``, ``.volume_bars()``,
43
+ ``.dollar_bars()``) and CLI flags (``--every-trades``, ``--every-volume``,
44
+ ``--every-notional``) as alternatives to ``--interval``.
45
+
46
+ ## [1.1.0] - 2026-08-05
47
+
48
+ ### Added
49
+ - Transparent gzip support across all file I/O: any ``.gz`` path
50
+ (``.csv.gz``, ``.jsonl.gz``, ``.ndjson.gz``) is compressed/decompressed
51
+ automatically in ``read_csv_trades`` / ``write_records_csv`` /
52
+ ``write_jsonl`` / ``read_jsonl_events`` and the CLI. Standard library only.
53
+ - Streaming readers for large files: ``iter_csv_trades`` and
54
+ ``iter_jsonl_events`` yield normalized events one at a time instead of
55
+ loading the whole file into memory.
56
+
57
+ ## [1.0.0] - 2026-08-04
58
+
59
+ ### Added
60
+ - `Pipeline` — declarative, reusable processing chains: compose `dedupe`,
61
+ `clean`, `time_bars`, `resample`, `fill_gaps` (plus custom steps via
62
+ `apply`) and run the same pipeline across venues and files. Quality
63
+ reports from `clean` are exposed on `pipeline.last_issues`.
64
+ - NDJSON / JSON Lines I/O: `write_jsonl` (events and bars, one compact JSON
65
+ object per line) and `read_jsonl_events` / `event_from_dict` for lossless
66
+ round-trips. Standard library only.
67
+ - Command-line interface: `mdnorm bars`, `mdnorm quality` and
68
+ `mdnorm convert` (CSV <-> NDJSON), with human-friendly intervals
69
+ (`30s`, `1m`, `4h`, `1d`). Installed as the `mdnorm` console script;
70
+ also runnable as `python -m mdnorm`.
71
+
72
+ ### Changed
73
+ - Project status raised to stable (`Development Status :: 5`); the public
74
+ API of `0.x` is carried over unchanged.
75
+
76
+ ## [0.9.0] - 2026-08-03
77
+
78
+ ### Added
79
+ - File-level CSV I/O: `read_csv_trades(path, ...)` reads a CSV of trades into
80
+ normalized events, and `write_records_csv(items, path)` writes events/bars
81
+ to a CSV (union of fields). Standard library only.
82
+
83
+ ## [0.8.0] - 2026-08-02
84
+
85
+ ### Added
86
+ - Stream consolidation: `merge_streams(*streams)` merges multiple venue feeds
87
+ into one timestamp-ordered timeline (stable), and `dedupe(events)` drops exact
88
+ duplicate events from reconnects/replays, preserving first-seen order.
89
+
90
+ ## [0.7.0] - 2026-08-01
91
+
92
+ ### Added
93
+ - Serialization: `event_to_dict`, `bar_to_dict` and `to_records` flatten
94
+ events and bars into plain, JSON-serialisable dicts (Decimals as strings by
95
+ default, `as_float=True` for numeric output) — ready for pandas / CSV / JSON.
96
+
97
+ ## [0.6.0] - 2026-07-31
98
+
99
+ ### Added
100
+ - `fill_gaps(bars)` — return a gapless bar series, inserting flat zero-volume
101
+ bars (OHLC = previous close) for any missing interval. Pairs with
102
+ `time_bars` and `resample_bars` for a continuous grid.
103
+
104
+ ## [0.5.0] - 2026-07-30
105
+
106
+ ### Added
107
+ - `resample_bars(bars, interval_ns)` — downsample OHLCV bars to a coarser
108
+ interval (e.g. 1-minute to 5-minute) with correct OHLC aggregation and
109
+ volume-weighted VWAP.
110
+
111
+ ## [0.4.0] - 2026-07-30
112
+
113
+ ### Added
114
+ - Data-quality module: `find_issues` and `clean` detect and drop bad ticks
115
+ (price outliers), gaps, out-of-order records and non-positive price/size,
116
+ returning a structured `QualityIssue` report.
117
+
118
+ ## [0.3.0] - 2026-07-28
119
+
120
+ ### Added
121
+ - OHLCV time-bar aggregation: `time_bars(events, interval_ns)` and the `Bar`
122
+ type (open/high/low/close/volume/trades/vwap). Handles out-of-order input
123
+ and ignores non-trade events.
124
+
125
+ ## [0.2.0] - 2026-07-27
126
+
127
+ ### Added
128
+ - Quote (bid/ask) normalization: `from_ws_quote` (exchange book-ticker
129
+ messages) and `from_csv_quote` (CSV bid/ask rows).
130
+ - `MarketEvent.mid_price` and `MarketEvent.spread` convenience properties.
131
+ - Dedicated test suite for quote events (`tests/test_quotes.py`).
132
+
133
+ ## [0.1.0] - 2026-07-27
134
+
135
+ ### Added
136
+ - Initial release.
137
+ - Unified `MarketEvent` schema (Decimal prices, ns UTC timestamps,
138
+ canonical `BASE-QUOTE` symbols).
139
+ - Trade normalizers for CSV, exchange WebSocket JSON, and FIX.
140
+ - Cross-venue equivalence tests.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HarvestGroup360 (AMII LTD)
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,330 @@
1
+ Metadata-Version: 2.4
2
+ Name: market-data-normalizer
3
+ Version: 1.3.1
4
+ Summary: Normalize heterogeneous market-data feeds (CSV, WebSocket JSON, FIX) into one exchange-agnostic schema.
5
+ Project-URL: Homepage, https://harvestgroup360.com
6
+ Project-URL: Repository, https://github.com/Harvestgroup360/market-data-normalizer
7
+ Project-URL: Changelog, https://github.com/Harvestgroup360/market-data-normalizer/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/Harvestgroup360/market-data-normalizer/issues
9
+ Author-email: HarvestGroup360 <github@harvestgroup360.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 HarvestGroup360 (AMII LTD)
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: fix-protocol,market-data,normalization,quantitative-finance,tick-data,trading
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Intended Audience :: Financial and Insurance Industry
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Office/Business :: Financial :: Investment
43
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.10
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=7; extra == 'dev'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # market-data-normalizer (`mdnorm`)
51
+
52
+ [![CI](https://github.com/Harvestgroup360/market-data-normalizer/actions/workflows/ci.yml/badge.svg)](https://github.com/Harvestgroup360/market-data-normalizer/actions/workflows/ci.yml)
53
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
54
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
55
+ [![PyPI](https://img.shields.io/pypi/v/market-data-normalizer.svg)](https://pypi.org/project/market-data-normalizer/)
56
+
57
+ Normalize heterogeneous market-data feeds — CSV tick dumps, exchange
58
+ WebSocket JSON, and FIX — into a single, exchange-agnostic event schema, so
59
+ downstream research and execution code never has to care where a tick came
60
+ from.
61
+
62
+ Zero runtime dependencies. Pure Python (3.10+). `Decimal` prices, integer
63
+ nanosecond timestamps.
64
+
65
+ ## Why
66
+
67
+ Every venue spells the same thing differently: `BTCUSDT` vs `XBT/USD`,
68
+ millisecond epochs vs FIX `UTCTimestamp`, `is_buyer_maker` booleans vs side
69
+ codes. Research notebooks and backtesters end up littered with per-venue
70
+ parsing branches. `mdnorm` pushes that mess to the edge and hands the rest of
71
+ your stack one clean type.
72
+
73
+ ## Install
74
+
75
+ ```console
76
+ pip install market-data-normalizer
77
+ ```
78
+
79
+ The distribution is named `market-data-normalizer`; the import name is
80
+ `mdnorm`:
81
+
82
+ ```python
83
+ import mdnorm
84
+ ```
85
+
86
+ Pure Python, no runtime dependencies, Python 3.10+.
87
+
88
+ ## Quick start
89
+
90
+ ```python
91
+ from mdnorm import from_csv_row, from_ws_json, from_fix
92
+
93
+ # CSV row (ISO-8601 timestamp)
94
+ from_csv_row(
95
+ {"symbol": "btc/usd", "ts": "2026-01-02T00:00:00Z",
96
+ "price": "42000.5", "size": "0.25", "side": "buy"},
97
+ venue="coinbase",
98
+ )
99
+
100
+ # Exchange WebSocket trade message
101
+ from_ws_json({"s": "BTCUSDT", "p": "42000.5", "q": "0.25",
102
+ "T": 1767312000000, "m": False}, venue="binance")
103
+
104
+ # FIX execution report (SOH-delimited in the wild; "|" here for readability)
105
+ from_fix("55=BTC/USD|31=42000.5|32=0.25|54=1|60=20260102-00:00:00",
106
+ venue="lmax", sep="|")
107
+ ```
108
+
109
+ All three calls above produce the **same** `MarketEvent`.
110
+
111
+ ### Quotes (bid/ask)
112
+
113
+ ```python
114
+ from mdnorm import from_ws_quote
115
+
116
+ q = from_ws_quote(
117
+ {"s": "BTCUSDT", "b": "41999.5", "B": "1.2",
118
+ "a": "42000.5", "A": "0.8", "T": 1767312000000},
119
+ venue="binance",
120
+ )
121
+ q.mid_price # Decimal("42000.0")
122
+ q.spread # Decimal("1.0")
123
+ ```
124
+
125
+ `from_csv_quote` does the same for CSV rows with bid/ask columns.
126
+
127
+ ### OHLCV bars
128
+
129
+ ```python
130
+ from mdnorm import time_bars
131
+
132
+ bars = time_bars(events, interval_ns=60_000_000_000) # 1-minute bars
133
+ bars[0].open, bars[0].high, bars[0].low, bars[0].close, bars[0].volume, bars[0].vwap
134
+ ```
135
+
136
+ `time_bars` reduces a stream of trade events into fixed-interval OHLCV `Bar`s
137
+ (with VWAP and trade count), sorting out-of-order input and skipping quotes.
138
+
139
+ `resample_bars(bars, interval_ns)` downsamples bars to a coarser interval
140
+ (e.g. 1-minute → 5-minute) with correct OHLC aggregation and volume-weighted
141
+ VWAP.
142
+
143
+ `fill_gaps(bars)` returns a gapless series, inserting flat zero-volume bars
144
+ (OHLC = previous close) for any interval with no trades — a continuous grid for
145
+ backtests and feature pipelines.
146
+
147
+ ### Event-driven bars
148
+
149
+ Time bars are not the only clock. Sample by activity instead:
150
+
151
+ ```python
152
+ from decimal import Decimal
153
+ from mdnorm import count_bars, volume_bars, dollar_bars
154
+
155
+ count_bars(events, every=500) # tick bars
156
+ volume_bars(events, min_volume=Decimal("100")) # volume bars
157
+ dollar_bars(events, min_notional=Decimal("1e6")) # dollar bars
158
+ ```
159
+
160
+ ### Trading sessions
161
+
162
+ Filter a feed down to the hours that matter, with daylight saving handled
163
+ for you:
164
+
165
+ ```python
166
+ from mdnorm import US_EQUITY_RTH, filter_session, group_by_session_date
167
+
168
+ rth = filter_session(events, US_EQUITY_RTH) # 09:30-16:00 New York
169
+ by_day = group_by_session_date(events, US_EQUITY_RTH)
170
+ ```
171
+
172
+ Overnight windows (a session that opens at 18:00 and closes at 17:00 the
173
+ next day) are supported, and `session_date` keeps a whole night in one
174
+ bucket. From the command line:
175
+
176
+ ```console
177
+ $ mdnorm bars trades.csv --interval 5m --session 09:30-16:00 --tz America/New_York -o rth.csv
178
+ ```
179
+
180
+ ### Data quality
181
+
182
+ ```python
183
+ from mdnorm.quality import find_issues, clean
184
+
185
+ find_issues(events) # list of QualityIssue (outlier / gap / out_of_order / non_positive)
186
+ cleaned, issues = clean(events) # drop bad ticks & invalid rows, keep a report
187
+ ```
188
+
189
+ `clean` removes price outliers and non-positive price/size records and returns
190
+ the surviving events plus everything it flagged.
191
+
192
+ ### Serialization
193
+
194
+ ```python
195
+ from mdnorm import to_records
196
+
197
+ to_records(events) # list of flat dicts (Decimals as strings)
198
+ to_records(bars, as_float=True) # numeric output for DataFrames
199
+ ```
200
+
201
+ `to_records` (and `event_to_dict` / `bar_to_dict`) flatten events and bars into
202
+ plain, JSON-serialisable dicts — drop straight into `pandas.DataFrame`, a
203
+ `csv.DictWriter`, or `json.dumps`.
204
+
205
+ ### Consolidating streams
206
+
207
+ ```python
208
+ from mdnorm import merge_streams, dedupe
209
+
210
+ timeline = dedupe(merge_streams(binance_events, coinbase_events))
211
+ ```
212
+
213
+ `merge_streams` interleaves multiple venue feeds into one timestamp-ordered
214
+ timeline; `dedupe` drops exact duplicate events left behind by reconnects and
215
+ replays.
216
+
217
+ ### CSV files
218
+
219
+ ```python
220
+ from mdnorm import read_csv_trades, write_records_csv
221
+
222
+ events = read_csv_trades("trades.csv", venue="coinbase") # file -> events
223
+ write_records_csv(bars, "bars.csv", as_float=True) # events/bars -> file
224
+ ```
225
+
226
+ `read_csv_trades` parses a whole CSV of trades into normalized events;
227
+ `write_records_csv` writes events or bars back out. Standard library only.
228
+
229
+ ### NDJSON / JSON Lines
230
+
231
+ ```python
232
+ from mdnorm import write_jsonl, read_jsonl_events
233
+
234
+ write_jsonl(events, "events.jsonl") # one JSON object per line
235
+ events2 = read_jsonl_events("events.jsonl") # lossless round-trip
236
+
237
+ # large files: stream lazily, .gz handled transparently
238
+ for e in iter_jsonl_events("dump.jsonl.gz"):
239
+ ...
240
+ ```
241
+
242
+ ### Pipelines
243
+
244
+ Declare a processing chain once, reuse it everywhere:
245
+
246
+ ```python
247
+ from decimal import Decimal
248
+ from mdnorm import Pipeline
249
+
250
+ pipe = (
251
+ Pipeline()
252
+ .dedupe()
253
+ .clean(max_return=Decimal("0.1"))
254
+ .time_bars(60_000_000_000) # 1-minute bars
255
+ .fill_gaps()
256
+ )
257
+ bars = pipe.run(events)
258
+ print(pipe.last_issues) # quality report from clean()
259
+ ```
260
+
261
+ ### Command line
262
+
263
+ The common conversions ship as a zero-dependency CLI:
264
+
265
+ ```console
266
+ $ mdnorm bars trades.csv --venue binance --interval 1m -o bars.csv
267
+ $ mdnorm quality trades.csv --max-gap 5m
268
+ $ mdnorm convert trades.csv -o trades.jsonl
269
+ ```
270
+
271
+ Also available as `python -m mdnorm`.
272
+
273
+ ## The unified schema
274
+
275
+ ```python
276
+ @dataclass(frozen=True, slots=True)
277
+ class MarketEvent:
278
+ symbol: str # canonical "BASE-QUOTE", e.g. "BTC-USD"
279
+ venue: str # source venue
280
+ event_type: EventType # TRADE | QUOTE
281
+ ts_ns: int # nanoseconds since Unix epoch (UTC)
282
+ price: Decimal | None
283
+ size: Decimal | None
284
+ side: Side | None # BUY | SELL
285
+ # ... plus bid/ask fields for quotes
286
+ ```
287
+
288
+ ## Design notes
289
+
290
+ - **Money is `Decimal`.** Prices and sizes never touch binary floats, so
291
+ `42000.10` stays `42000.10`.
292
+ - **Time is integer nanoseconds, UTC.** One comparable integer regardless of
293
+ whether the source gave seconds, milliseconds, or a FIX timestamp string.
294
+ - **Symbols are canonicalized** to `BASE-QUOTE`, with venue aliases resolved
295
+ (`XBT` → `BTC`) and quote currencies detected longest-match-first so
296
+ `USDT` wins over `USD`.
297
+ - **Normalizers are pure functions** — one raw record in, one `MarketEvent`
298
+ out — which keeps them trivial to unit-test and compose into any streaming
299
+ or batch pipeline.
300
+
301
+ ## Architecture
302
+
303
+ ```
304
+ raw feed ──► normalizer ─────────────► MarketEvent ──► your pipeline
305
+ (CSV / (from_csv_row / (unified, (research,
306
+ WS JSON / from_ws_json / immutable) backtest,
307
+ FIX) from_fix) execution)
308
+
309
+ ├── symbols.canonical_symbol() BTCUSDT → BTC-USDT
310
+ └── timeutil.*_to_ns() any time → ns UTC
311
+ ```
312
+
313
+ ## Tests
314
+
315
+ ```bash
316
+ pip install pytest
317
+ pytest -q
318
+ ```
319
+
320
+ The suite includes a cross-venue equivalence test proving CSV, WebSocket and
321
+ FIX representations of one trade collapse to an identical event.
322
+
323
+ ## License
324
+
325
+ MIT © HarvestGroup360 (AMII LTD). See [LICENSE](LICENSE).
326
+
327
+ ---
328
+
329
+ Maintained by [HarvestGroup360](https://harvestgroup360.com) as part of our
330
+ open quantitative-infrastructure tooling.