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.
- market_data_normalizer-1.3.1/.github/workflows/ci.yml +27 -0
- market_data_normalizer-1.3.1/.github/workflows/publish.yml +38 -0
- market_data_normalizer-1.3.1/.gitignore +28 -0
- market_data_normalizer-1.3.1/CHANGELOG.md +140 -0
- market_data_normalizer-1.3.1/LICENSE +21 -0
- market_data_normalizer-1.3.1/PKG-INFO +330 -0
- market_data_normalizer-1.3.1/README.md +281 -0
- market_data_normalizer-1.3.1/examples/demo.py +24 -0
- market_data_normalizer-1.3.1/pyproject.toml +47 -0
- market_data_normalizer-1.3.1/src/mdnorm/__init__.py +92 -0
- market_data_normalizer-1.3.1/src/mdnorm/__main__.py +5 -0
- market_data_normalizer-1.3.1/src/mdnorm/bars.py +248 -0
- market_data_normalizer-1.3.1/src/mdnorm/cli.py +223 -0
- market_data_normalizer-1.3.1/src/mdnorm/csvio.py +79 -0
- market_data_normalizer-1.3.1/src/mdnorm/fileio.py +21 -0
- market_data_normalizer-1.3.1/src/mdnorm/jsonl.py +101 -0
- market_data_normalizer-1.3.1/src/mdnorm/normalizers.py +197 -0
- market_data_normalizer-1.3.1/src/mdnorm/pipeline.py +145 -0
- market_data_normalizer-1.3.1/src/mdnorm/quality.py +89 -0
- market_data_normalizer-1.3.1/src/mdnorm/records.py +72 -0
- market_data_normalizer-1.3.1/src/mdnorm/schema.py +71 -0
- market_data_normalizer-1.3.1/src/mdnorm/sessions.py +187 -0
- market_data_normalizer-1.3.1/src/mdnorm/streams.py +40 -0
- market_data_normalizer-1.3.1/src/mdnorm/symbols.py +69 -0
- market_data_normalizer-1.3.1/src/mdnorm/timeutil.py +49 -0
- market_data_normalizer-1.3.1/tests/test_bars.py +66 -0
- market_data_normalizer-1.3.1/tests/test_cli.py +109 -0
- market_data_normalizer-1.3.1/tests/test_csvio.py +57 -0
- market_data_normalizer-1.3.1/tests/test_event_bars.py +107 -0
- market_data_normalizer-1.3.1/tests/test_fill_gaps.py +47 -0
- market_data_normalizer-1.3.1/tests/test_gzip_streaming.py +90 -0
- market_data_normalizer-1.3.1/tests/test_jsonl.py +94 -0
- market_data_normalizer-1.3.1/tests/test_normalizers.py +79 -0
- market_data_normalizer-1.3.1/tests/test_pipeline.py +73 -0
- market_data_normalizer-1.3.1/tests/test_quality.py +50 -0
- market_data_normalizer-1.3.1/tests/test_quotes.py +45 -0
- market_data_normalizer-1.3.1/tests/test_records.py +54 -0
- market_data_normalizer-1.3.1/tests/test_resample.py +54 -0
- market_data_normalizer-1.3.1/tests/test_sessions.py +164 -0
- market_data_normalizer-1.3.1/tests/test_streams.py +45 -0
- 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
|
+
[](https://github.com/Harvestgroup360/market-data-normalizer/actions/workflows/ci.yml)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
[](pyproject.toml)
|
|
55
|
+
[](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.
|