market-data-normalizer (mdnorm)
Normalize heterogeneous market-data feeds — CSV tick dumps, exchange WebSocket JSON, and FIX — into a single, exchange-agnostic event schema, so downstream research and execution code never has to care where a tick came from.
Zero runtime dependencies. Pure Python (3.10+). Decimal prices, integer
nanosecond timestamps.
Why
Every venue spells the same thing differently: BTCUSDT vs XBT/USD,
millisecond epochs vs FIX UTCTimestamp, is_buyer_maker booleans vs side
codes. Research notebooks and backtesters end up littered with per-venue
parsing branches. mdnorm pushes that mess to the edge and hands the rest of
your stack one clean type.
Install
pip install market-data-normalizer
The distribution is named market-data-normalizer; the import name is
mdnorm:
import mdnorm
Pure Python, no runtime dependencies, Python 3.10+.
Quick start
from mdnorm import from_csv_row, from_ws_json, from_fix
# CSV row (ISO-8601 timestamp)
from_csv_row(
{"symbol": "btc/usd", "ts": "2026-01-02T00:00:00Z",
"price": "42000.5", "size": "0.25", "side": "buy"},
venue="coinbase",
)
# Exchange WebSocket trade message
from_ws_json({"s": "BTCUSDT", "p": "42000.5", "q": "0.25",
"T": 1767312000000, "m": False}, venue="binance")
# FIX execution report (SOH-delimited in the wild; "|" here for readability)
from_fix("55=BTC/USD|31=42000.5|32=0.25|54=1|60=20260102-00:00:00",
venue="lmax", sep="|")
All three calls above produce the same MarketEvent.
Quotes (bid/ask)
from mdnorm import from_ws_quote
q = from_ws_quote(
{"s": "BTCUSDT", "b": "41999.5", "B": "1.2",
"a": "42000.5", "A": "0.8", "T": 1767312000000},
venue="binance",
)
q.mid_price # Decimal("42000.0")
q.spread # Decimal("1.0")
from_csv_quote does the same for CSV rows with bid/ask columns.
OHLCV bars
from mdnorm import time_bars
bars = time_bars(events, interval_ns=60_000_000_000) # 1-minute bars
bars[0].open, bars[0].high, bars[0].low, bars[0].close, bars[0].volume, bars[0].vwap
time_bars reduces a stream of trade events into fixed-interval OHLCV Bars
(with VWAP and trade count), sorting out-of-order input and skipping quotes.
resample_bars(bars, interval_ns) downsamples bars to a coarser interval
(e.g. 1-minute → 5-minute) with correct OHLC aggregation and volume-weighted
VWAP.
fill_gaps(bars) returns a gapless series, inserting flat zero-volume bars
(OHLC = previous close) for any interval with no trades — a continuous grid for
backtests and feature pipelines.
Event-driven bars
Time bars are not the only clock. Sample by activity instead:
from decimal import Decimal
from mdnorm import count_bars, volume_bars, dollar_bars
count_bars(events, every=500) # tick bars
volume_bars(events, min_volume=Decimal("100")) # volume bars
dollar_bars(events, min_notional=Decimal("1e6")) # dollar bars
Trading sessions
Filter a feed down to the hours that matter, with daylight saving handled for you:
from mdnorm import US_EQUITY_RTH, filter_session, group_by_session_date
rth = filter_session(events, US_EQUITY_RTH) # 09:30-16:00 New York
by_day = group_by_session_date(events, US_EQUITY_RTH)
Overnight windows (a session that opens at 18:00 and closes at 17:00 the
next day) are supported, and session_date keeps a whole night in one
bucket. From the command line:
$ mdnorm bars trades.csv --interval 5m --session 09:30-16:00 --tz America/New_York -o rth.csv
Data quality
from mdnorm.quality import find_issues, clean
find_issues(events) # list of QualityIssue (outlier / gap / out_of_order / non_positive)
cleaned, issues = clean(events) # drop bad ticks & invalid rows, keep a report
clean removes price outliers and non-positive price/size records and returns
the surviving events plus everything it flagged.
Serialization
from mdnorm import to_records
to_records(events) # list of flat dicts (Decimals as strings)
to_records(bars, as_float=True) # numeric output for DataFrames
to_records (and event_to_dict / bar_to_dict) flatten events and bars into
plain, JSON-serialisable dicts — drop straight into pandas.DataFrame, a
csv.DictWriter, or json.dumps.
Consolidating streams
from mdnorm import merge_streams, dedupe
timeline = dedupe(merge_streams(binance_events, coinbase_events))
merge_streams interleaves multiple venue feeds into one timestamp-ordered
timeline; dedupe drops exact duplicate events left behind by reconnects and
replays.
CSV files
from mdnorm import read_csv_trades, write_records_csv
events = read_csv_trades("trades.csv", venue="coinbase") # file -> events
write_records_csv(bars, "bars.csv", as_float=True) # events/bars -> file
read_csv_trades parses a whole CSV of trades into normalized events;
write_records_csv writes events or bars back out. Standard library only.
NDJSON / JSON Lines
from mdnorm import write_jsonl, read_jsonl_events
write_jsonl(events, "events.jsonl") # one JSON object per line
events2 = read_jsonl_events("events.jsonl") # lossless round-trip
# large files: stream lazily, .gz handled transparently
for e in iter_jsonl_events("dump.jsonl.gz"):
...
Pipelines
Declare a processing chain once, reuse it everywhere:
from decimal import Decimal
from mdnorm import Pipeline
pipe = (
Pipeline()
.dedupe()
.clean(max_return=Decimal("0.1"))
.time_bars(60_000_000_000) # 1-minute bars
.fill_gaps()
)
bars = pipe.run(events)
print(pipe.last_issues) # quality report from clean()
Command line
The common conversions ship as a zero-dependency CLI:
$ mdnorm bars trades.csv --venue binance --interval 1m -o bars.csv
$ mdnorm quality trades.csv --max-gap 5m
$ mdnorm convert trades.csv -o trades.jsonl
Also available as python -m mdnorm.
The unified schema
@dataclass(frozen=True, slots=True)
class MarketEvent:
symbol: str # canonical "BASE-QUOTE", e.g. "BTC-USD"
venue: str # source venue
event_type: EventType # TRADE | QUOTE
ts_ns: int # nanoseconds since Unix epoch (UTC)
price: Decimal | None
size: Decimal | None
side: Side | None # BUY | SELL
# ... plus bid/ask fields for quotes
Design notes
- Money is
Decimal. Prices and sizes never touch binary floats, so42000.10stays42000.10. - Time is integer nanoseconds, UTC. One comparable integer regardless of whether the source gave seconds, milliseconds, or a FIX timestamp string.
- Symbols are canonicalized to
BASE-QUOTE, with venue aliases resolved (XBT→BTC) and quote currencies detected longest-match-first soUSDTwins overUSD. - Normalizers are pure functions — one raw record in, one
MarketEventout — which keeps them trivial to unit-test and compose into any streaming or batch pipeline.
Architecture
raw feed ──► normalizer ─────────────► MarketEvent ──► your pipeline
(CSV / (from_csv_row / (unified, (research,
WS JSON / from_ws_json / immutable) backtest,
FIX) from_fix) execution)
│
├── symbols.canonical_symbol() BTCUSDT → BTC-USDT
└── timeutil.*_to_ns() any time → ns UTC
Tests
pip install pytest
pytest -q
The suite includes a cross-venue equivalence test proving CSV, WebSocket and FIX representations of one trade collapse to an identical event.
License
MIT © HarvestGroup360 (AMII LTD). See LICENSE.
Maintained by HarvestGroup360 as part of our open quantitative-infrastructure tooling.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file market_data_normalizer-1.3.1.tar.gz.
File metadata
- Download URL: market_data_normalizer-1.3.1.tar.gz
- Upload date:
- Size: 34.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
479d582260036edea2b47121ac6bc690fc24d2b2329289fcc5018811f4ce3cf2
|
|
| MD5 |
7f226d107ae0053078b301f842980f4b
|
|
| BLAKE2b-256 |
f70881e16890c15a82d39e23d8afb0f62affc3b6abf7c673bbfa3045787e497b
|
Provenance
The following attestation bundles were made for market_data_normalizer-1.3.1.tar.gz:
Publisher:
publish.yml on Harvestgroup360/market-data-normalizer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
market_data_normalizer-1.3.1.tar.gz -
Subject digest:
479d582260036edea2b47121ac6bc690fc24d2b2329289fcc5018811f4ce3cf2 - Sigstore transparency entry: 2407441090
- Sigstore integration time:
-
Permalink:
Harvestgroup360/market-data-normalizer@0c56aa39fd68861829e7cb73017b44b6735efc11 -
Branch / Tag:
refs/tags/v1.3.1 - Owner: https://github.com/Harvestgroup360
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0c56aa39fd68861829e7cb73017b44b6735efc11 -
Trigger Event:
push
-
Statement type:
File details
Details for the file market_data_normalizer-1.3.1-py3-none-any.whl.
File metadata
- Download URL: market_data_normalizer-1.3.1-py3-none-any.whl
- Upload date:
- Size: 29.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76c44eec8ab029266f5bde91464a8b3581a5fa39096976df18d562582a254990
|
|
| MD5 |
ef80aa2b88fd07e1bc99ca9ae3e1dfb0
|
|
| BLAKE2b-256 |
a7360dd1806ea738c6e261ea0e42d4e83b876779e42bf5546d848e5e2e0cf3f3
|
Provenance
The following attestation bundles were made for market_data_normalizer-1.3.1-py3-none-any.whl:
Publisher:
publish.yml on Harvestgroup360/market-data-normalizer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
market_data_normalizer-1.3.1-py3-none-any.whl -
Subject digest:
76c44eec8ab029266f5bde91464a8b3581a5fa39096976df18d562582a254990 - Sigstore transparency entry: 2407441149
- Sigstore integration time:
-
Permalink:
Harvestgroup360/market-data-normalizer@0c56aa39fd68861829e7cb73017b44b6735efc11 -
Branch / Tag:
refs/tags/v1.3.1 - Owner: https://github.com/Harvestgroup360
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0c56aa39fd68861829e7cb73017b44b6735efc11 -
Trigger Event:
push
-
Statement type: