tickbloom 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tickbloom
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,168 @@
1
+ Metadata-Version: 2.5
2
+ Name: tickbloom
3
+ Version: 0.1.0
4
+ Summary: Market-data integrity scoring and look-ahead detection for quantitative Python.
5
+ Project-URL: Homepage, https://tickbloom.com
6
+ Project-URL: Documentation, https://tickbloom.com
7
+ Project-URL: Source, https://github.com/tickbloom/tickbloom
8
+ Project-URL: Issues, https://github.com/tickbloom/tickbloom/issues
9
+ Author: Tickbloom
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: audit,backtesting,data-quality,look-ahead,market-data,quant,quantitative-finance,survivorship-bias,trading
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Office/Business :: Financial :: Investment
23
+ Classifier: Topic :: Software Development :: Quality Assurance
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: pandas>=2.0
27
+ Requires-Dist: pyarrow>=14.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: build; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # tickbloom
35
+
36
+ Data integrity you can put in a document.
37
+
38
+ Runs entirely in your own process. Your data, your strategy code, and your fills never leave the machine.
39
+
40
+ ## Quickstart
41
+
42
+ ```python
43
+ import pandas as pd
44
+ import tickbloom as tb
45
+
46
+ df = pd.read_parquet("es_2024q3.parquet") # ts_event, symbol, price, size
47
+
48
+ rep = tb.audit(df, source="databento:ES.c.0")
49
+ print(rep) # <AuditReport score=92.6 verdict=review fail=0 flag=2>
50
+ print(rep.breakdown.table()) # where every deducted point went
51
+ rep.to_json("audit.json") # reproducible, byte-stable
52
+ ```
53
+
54
+ ```
55
+ check weight rate tol -pts
56
+ ------------------------------------------------
57
+ gaps 25 0.5000% 2.00% 5.53
58
+ duplicates 10 0.1000% 0.50% 1.81
59
+ order 15 0.0000% 0.10% 0.00
60
+ lookahead 25structural — 0.00
61
+ survivorship 10structural — 0.00
62
+ session 10 0.0000% 0.50% 0.00
63
+ zero_volume 5 0.0000% 1.00% 0.00
64
+ ------------------------------------------------
65
+ SCORE 92.6
66
+ ```
67
+
68
+ ## What the score promises
69
+
70
+ Three properties, each with a test in `tests/test_scoring.py`:
71
+
72
+ 1. **Deterministic** — same frame, same config, same float. No sampling, no wall-clock, no dependence on column or dict ordering.
73
+ 2. **Decomposable** — `score = 100 - sum(penalties)`. Every point is attributable to one check. When an allocator asks where 7.4 points went, you print the table.
74
+ 3. **Explainable in one sentence per check** — if a weight can't be justified to a trader in one sentence, the weight is wrong.
75
+
76
+ Weights are declared in `scoring.WEIGHTS`, recorded in every manifest, and overridable. That's deliberate: a fund should be able to say *"we used these thresholds"*, not *"the vendor decided."*
77
+
78
+ ## Weights and why
79
+
80
+ | Check | Weight | One-sentence rationale |
81
+ |---|---|---|
82
+ | `lookahead` | 25 | Directly fabricates returns; a single leaked bar can turn a losing strategy into a plausible winner. |
83
+ | `gaps` | 25 | Missing sessions silently change the sample — a gap across a crash removes exactly the periods that set your tail risk. |
84
+ | `order` | 15 | Out-of-order timestamps break the causality assumption every feature is built on. |
85
+ | `survivorship` | 10 | Delisted names removed from the universe inflate equity returns 1–4% annually. |
86
+ | `duplicates` | 10 | Double-counted prints bias volume-weighted features and can double-fill in replay. |
87
+ | `session` | 10 | Overnight prints leaking into a regular-hours frame contaminate open/close logic. |
88
+ | `zero_volume` | 5 | Phantom prints move indicators without being tradeable, but rarely dominate a result. |
89
+
90
+ ## The penalty curve
91
+
92
+ ```
93
+ penalty = weight * (1 - exp(-defect_rate / tolerance))
94
+ ```
95
+
96
+ Not `weight * min(1, rate/tolerance)`. The linear clip was the first implementation and it was wrong: in a 1,000-row sample a **single** out-of-order tick is a 0.1% rate, which equals the tolerance exactly and deducted the entire 15-point weight for one bad tick. Small samples were being destroyed by single defects.
97
+
98
+ Published reference points, asserted in tests:
99
+
100
+ | Defect rate | Share of weight deducted |
101
+ |---|---|
102
+ | 0.25 × tolerance | 22% |
103
+ | 1.0 × tolerance | 63% |
104
+ | 3.0 × tolerance | 95% |
105
+ | 5.0 × tolerance | 99% (treated as full) |
106
+
107
+ Structural checks (`lookahead`, `survivorship`) stay binary. A "small" look-ahead leak is not a thing.
108
+
109
+ ## Look-ahead scanning
110
+
111
+ ```bash
112
+ python -m tickbloom scan strategies/ # exit 1 if a certain leak exists
113
+ python -m tickbloom audit data.csv --code strategies/ -o report.html
114
+ ```
115
+
116
+ A static AST pass over your source. Eight rules, each declaring a **confidence**, because a static analyser dies from false positives rather than missed detections:
117
+
118
+ | Confidence | Severity | Meaning |
119
+ |---|---|---|
120
+ | `certain` | `fail` | A leak by definition. `close.shift(-1)` is tomorrow's close; there is no other reading. |
121
+ | `likely` | `flag` | Usually a leak, but has legitimate uses — label construction, offline research. |
122
+
123
+ Only `certain` rules can fail an audit. A false `fail` costs you the user; a false `flag` costs them four seconds.
124
+
125
+ Forward-looking code is *correct* when building labels, so suppress per line:
126
+
127
+ ```python
128
+ df["target_1d"] = close.shift(-1) # tickbloom: allow
129
+ df["target_1d"] = close.shift(-1) # tickbloom: allow TB-101
130
+ ```
131
+
132
+ Without this, every supervised-learning codebase reports dozens of findings on its target construction and the tool gets uninstalled on day one.
133
+
134
+ **Limits, stated plainly.** This is a syntactic pass. It cannot see through a variable holding a shift amount, a leak inside a library you call, or one assembled at runtime. It catches the common written-down forms. TB-108 does one-pass tracking of names bound to `.shift(n>0)` so it stays quiet on code that lagged correctly upstream — but it is reported as `likely`, not `certain`, precisely because that analysis is shallow.
135
+
136
+ ## Sending a report
137
+
138
+ ```bash
139
+ python -m tickbloom audit their-file.csv -o report.html --source "ES · 2024-H1"
140
+ ```
141
+
142
+ Produces a self-contained HTML file — no external assets, no scripts, no network — that opens from an email attachment on a locked-down laptop. The column mapper handles `Datetime`/`Px`/`Qty`/`Ticker` and the usual variants, so nobody has to rename anything first.
143
+
144
+ ```python
145
+ rep = tb.audit(df)
146
+ tb.export(rep, "report.html") # sendable document
147
+ tb.export(rep, "report.json", fmt="json") # machine-readable
148
+ ```
149
+
150
+ Reports are **byte-reproducible**: same input, same bytes out. No timestamp is stamped unless you pass `generated=` explicitly. A document whose bytes change when nothing changed is not evidence, and `test_html_is_byte_reproducible` fails the build if that regresses.
151
+
152
+ `--fail-under 95` exits 1 below a threshold, so the CLI composes into CI today.
153
+
154
+ ## Status
155
+
156
+ Implemented: the five integrity checks, the scoring model, the static look-ahead scanner, the report object, the reproducibility manifest, HTML/JSON export, and the CLI.
157
+
158
+ Not yet: loaders (`load()`), the runtime look-ahead guard, PDF export, the robustness agent, and the GitHub Action.
159
+
160
+ ## Tests
161
+
162
+ ```
163
+ python -m pytest tests/ -q # 69 passed
164
+ ```
165
+
166
+ ## A note on scope
167
+
168
+ This does not replace QuantConnect or Lean. It sits upstream of whatever backtester you already use, and it does not assert compliance with, certification by, or approval from any trading firm. It produces evidence. A human draws the conclusion.
@@ -0,0 +1,135 @@
1
+ # tickbloom
2
+
3
+ Data integrity you can put in a document.
4
+
5
+ Runs entirely in your own process. Your data, your strategy code, and your fills never leave the machine.
6
+
7
+ ## Quickstart
8
+
9
+ ```python
10
+ import pandas as pd
11
+ import tickbloom as tb
12
+
13
+ df = pd.read_parquet("es_2024q3.parquet") # ts_event, symbol, price, size
14
+
15
+ rep = tb.audit(df, source="databento:ES.c.0")
16
+ print(rep) # <AuditReport score=92.6 verdict=review fail=0 flag=2>
17
+ print(rep.breakdown.table()) # where every deducted point went
18
+ rep.to_json("audit.json") # reproducible, byte-stable
19
+ ```
20
+
21
+ ```
22
+ check weight rate tol -pts
23
+ ------------------------------------------------
24
+ gaps 25 0.5000% 2.00% 5.53
25
+ duplicates 10 0.1000% 0.50% 1.81
26
+ order 15 0.0000% 0.10% 0.00
27
+ lookahead 25structural — 0.00
28
+ survivorship 10structural — 0.00
29
+ session 10 0.0000% 0.50% 0.00
30
+ zero_volume 5 0.0000% 1.00% 0.00
31
+ ------------------------------------------------
32
+ SCORE 92.6
33
+ ```
34
+
35
+ ## What the score promises
36
+
37
+ Three properties, each with a test in `tests/test_scoring.py`:
38
+
39
+ 1. **Deterministic** — same frame, same config, same float. No sampling, no wall-clock, no dependence on column or dict ordering.
40
+ 2. **Decomposable** — `score = 100 - sum(penalties)`. Every point is attributable to one check. When an allocator asks where 7.4 points went, you print the table.
41
+ 3. **Explainable in one sentence per check** — if a weight can't be justified to a trader in one sentence, the weight is wrong.
42
+
43
+ Weights are declared in `scoring.WEIGHTS`, recorded in every manifest, and overridable. That's deliberate: a fund should be able to say *"we used these thresholds"*, not *"the vendor decided."*
44
+
45
+ ## Weights and why
46
+
47
+ | Check | Weight | One-sentence rationale |
48
+ |---|---|---|
49
+ | `lookahead` | 25 | Directly fabricates returns; a single leaked bar can turn a losing strategy into a plausible winner. |
50
+ | `gaps` | 25 | Missing sessions silently change the sample — a gap across a crash removes exactly the periods that set your tail risk. |
51
+ | `order` | 15 | Out-of-order timestamps break the causality assumption every feature is built on. |
52
+ | `survivorship` | 10 | Delisted names removed from the universe inflate equity returns 1–4% annually. |
53
+ | `duplicates` | 10 | Double-counted prints bias volume-weighted features and can double-fill in replay. |
54
+ | `session` | 10 | Overnight prints leaking into a regular-hours frame contaminate open/close logic. |
55
+ | `zero_volume` | 5 | Phantom prints move indicators without being tradeable, but rarely dominate a result. |
56
+
57
+ ## The penalty curve
58
+
59
+ ```
60
+ penalty = weight * (1 - exp(-defect_rate / tolerance))
61
+ ```
62
+
63
+ Not `weight * min(1, rate/tolerance)`. The linear clip was the first implementation and it was wrong: in a 1,000-row sample a **single** out-of-order tick is a 0.1% rate, which equals the tolerance exactly and deducted the entire 15-point weight for one bad tick. Small samples were being destroyed by single defects.
64
+
65
+ Published reference points, asserted in tests:
66
+
67
+ | Defect rate | Share of weight deducted |
68
+ |---|---|
69
+ | 0.25 × tolerance | 22% |
70
+ | 1.0 × tolerance | 63% |
71
+ | 3.0 × tolerance | 95% |
72
+ | 5.0 × tolerance | 99% (treated as full) |
73
+
74
+ Structural checks (`lookahead`, `survivorship`) stay binary. A "small" look-ahead leak is not a thing.
75
+
76
+ ## Look-ahead scanning
77
+
78
+ ```bash
79
+ python -m tickbloom scan strategies/ # exit 1 if a certain leak exists
80
+ python -m tickbloom audit data.csv --code strategies/ -o report.html
81
+ ```
82
+
83
+ A static AST pass over your source. Eight rules, each declaring a **confidence**, because a static analyser dies from false positives rather than missed detections:
84
+
85
+ | Confidence | Severity | Meaning |
86
+ |---|---|---|
87
+ | `certain` | `fail` | A leak by definition. `close.shift(-1)` is tomorrow's close; there is no other reading. |
88
+ | `likely` | `flag` | Usually a leak, but has legitimate uses — label construction, offline research. |
89
+
90
+ Only `certain` rules can fail an audit. A false `fail` costs you the user; a false `flag` costs them four seconds.
91
+
92
+ Forward-looking code is *correct* when building labels, so suppress per line:
93
+
94
+ ```python
95
+ df["target_1d"] = close.shift(-1) # tickbloom: allow
96
+ df["target_1d"] = close.shift(-1) # tickbloom: allow TB-101
97
+ ```
98
+
99
+ Without this, every supervised-learning codebase reports dozens of findings on its target construction and the tool gets uninstalled on day one.
100
+
101
+ **Limits, stated plainly.** This is a syntactic pass. It cannot see through a variable holding a shift amount, a leak inside a library you call, or one assembled at runtime. It catches the common written-down forms. TB-108 does one-pass tracking of names bound to `.shift(n>0)` so it stays quiet on code that lagged correctly upstream — but it is reported as `likely`, not `certain`, precisely because that analysis is shallow.
102
+
103
+ ## Sending a report
104
+
105
+ ```bash
106
+ python -m tickbloom audit their-file.csv -o report.html --source "ES · 2024-H1"
107
+ ```
108
+
109
+ Produces a self-contained HTML file — no external assets, no scripts, no network — that opens from an email attachment on a locked-down laptop. The column mapper handles `Datetime`/`Px`/`Qty`/`Ticker` and the usual variants, so nobody has to rename anything first.
110
+
111
+ ```python
112
+ rep = tb.audit(df)
113
+ tb.export(rep, "report.html") # sendable document
114
+ tb.export(rep, "report.json", fmt="json") # machine-readable
115
+ ```
116
+
117
+ Reports are **byte-reproducible**: same input, same bytes out. No timestamp is stamped unless you pass `generated=` explicitly. A document whose bytes change when nothing changed is not evidence, and `test_html_is_byte_reproducible` fails the build if that regresses.
118
+
119
+ `--fail-under 95` exits 1 below a threshold, so the CLI composes into CI today.
120
+
121
+ ## Status
122
+
123
+ Implemented: the five integrity checks, the scoring model, the static look-ahead scanner, the report object, the reproducibility manifest, HTML/JSON export, and the CLI.
124
+
125
+ Not yet: loaders (`load()`), the runtime look-ahead guard, PDF export, the robustness agent, and the GitHub Action.
126
+
127
+ ## Tests
128
+
129
+ ```
130
+ python -m pytest tests/ -q # 69 passed
131
+ ```
132
+
133
+ ## A note on scope
134
+
135
+ This does not replace QuantConnect or Lean. It sits upstream of whatever backtester you already use, and it does not assert compliance with, certification by, or approval from any trading firm. It produces evidence. A human draws the conclusion.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tickbloom"
7
+ version = "0.1.0"
8
+ description = "Market-data integrity scoring and look-ahead detection for quantitative Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Tickbloom" }]
14
+ keywords = [
15
+ "quant", "quantitative-finance", "backtesting", "market-data",
16
+ "data-quality", "look-ahead", "survivorship-bias", "trading", "audit",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Financial and Insurance Industry",
21
+ "Intended Audience :: Developers",
22
+ "Topic :: Office/Business :: Financial :: Investment",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Operating System :: OS Independent",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = [
33
+ "pandas>=2.0",
34
+ "pyarrow>=14.0",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ dev = ["pytest>=8.0", "build", "twine"]
39
+
40
+ [project.scripts]
41
+ tickbloom = "tickbloom.cli:main"
42
+
43
+ [project.urls]
44
+ Homepage = "https://tickbloom.com"
45
+ Documentation = "https://tickbloom.com"
46
+ Source = "https://github.com/tickbloom/tickbloom"
47
+ Issues = "https://github.com/tickbloom/tickbloom/issues"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["tickbloom"]
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ include = ["tickbloom", "tests", "README.md", "LICENSE", "pyproject.toml"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
File without changes
@@ -0,0 +1,143 @@
1
+ """Tests for the sendable report.
2
+
3
+ The load-bearing test here is `test_html_is_byte_reproducible`. An audit
4
+ document whose bytes change when nothing changed is not evidence — if that
5
+ property breaks, the whole "you can defend this" pitch breaks with it.
6
+ """
7
+
8
+ import re
9
+
10
+ import pandas as pd
11
+ import pytest
12
+
13
+ import tickbloom as tb
14
+ from tests.test_scoring import clean_frame
15
+
16
+
17
+ def dirty_frame():
18
+ """Known defects: 4 duplicates, one dropped session, one bad timestamp."""
19
+ df = clean_frame()
20
+ df = pd.concat([df, df.iloc[[5, 6, 7, 8]]], ignore_index=True)
21
+ df = df[pd.to_datetime(df["ts_event"]).dt.date != pd.Timestamp("2024-07-10").date()]
22
+ return df.reset_index(drop=True)
23
+
24
+
25
+ # ---------- reproducibility ----------
26
+
27
+ def test_html_is_byte_reproducible():
28
+ rep = tb.audit(dirty_frame())
29
+ assert rep.to_html() == rep.to_html()
30
+
31
+
32
+ def test_html_has_no_implicit_timestamp():
33
+ """No wall-clock leaks in unless the caller asks for one."""
34
+ html = tb.audit(clean_frame()).to_html()
35
+ assert not re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}", html), \
36
+ "an implicit timestamp would break byte reproducibility"
37
+
38
+
39
+ def test_generated_timestamp_is_opt_in():
40
+ html = tb.audit(clean_frame()).to_html(generated="2026-08-25T09:00:00Z")
41
+ assert "2026-08-25T09:00:00Z" in html
42
+
43
+
44
+ def test_two_identical_frames_produce_identical_documents():
45
+ a = tb.audit(dirty_frame(), source="x").to_html()
46
+ b = tb.audit(dirty_frame(), source="x").to_html()
47
+ assert a == b
48
+
49
+
50
+ # ---------- content ----------
51
+
52
+ def test_report_contains_score_and_verdict():
53
+ rep = tb.audit(clean_frame())
54
+ html = rep.to_html()
55
+ assert "100.0" in html
56
+ assert ">Pass<" in html
57
+
58
+
59
+ def test_failing_audit_renders_fail_verdict():
60
+ rep = tb.audit(clean_frame(), lookahead_failed=True)
61
+ html = rep.to_html()
62
+ assert ">Fail<" in html
63
+ assert "Look-ahead" in html
64
+
65
+
66
+ def test_every_finding_appears():
67
+ rep = tb.audit(dirty_frame())
68
+ html = rep.to_html()
69
+ for f in rep.findings:
70
+ assert f.id in html, f"{f.id} missing from the rendered report"
71
+
72
+
73
+ def test_penalty_table_shows_arithmetic():
74
+ rep = tb.audit(dirty_frame())
75
+ html = rep.to_html()
76
+ assert "Where the points went" in html
77
+ for p in rep.breakdown.penalties:
78
+ assert p.check in html
79
+
80
+
81
+ def test_manifest_is_rendered():
82
+ html = tb.audit(clean_frame(), source="databento:ES").to_html()
83
+ assert "input_sha256" in html
84
+ assert "databento:ES" in html
85
+
86
+
87
+ def test_disclaimer_present():
88
+ """The one sentence that must never be dropped."""
89
+ html = tb.audit(clean_frame()).to_html()
90
+ assert "not a certification" in html
91
+ assert "any trading firm or regulator" in html
92
+
93
+
94
+ # ---------- safety ----------
95
+
96
+ def test_self_contained_no_network():
97
+ html = tb.audit(clean_frame()).to_html()
98
+ for bad in ["http://", "https://fonts", "<script", "src="]:
99
+ assert bad not in html, f"report must be self-contained; found {bad!r}"
100
+
101
+
102
+ def test_source_label_is_escaped():
103
+ """A crafted source label must not inject markup."""
104
+ rep = tb.audit(clean_frame(), source="<img src=x onerror=alert(1)>")
105
+ html = rep.to_html()
106
+ assert "<img src=x" not in html
107
+ assert "&lt;img" in html
108
+
109
+
110
+ # ---------- export ----------
111
+
112
+ def test_export_html_writes_file(tmp_path):
113
+ rep = tb.audit(dirty_frame())
114
+ out = tb.export(rep, tmp_path / "r.html")
115
+ assert out.exists() and out.stat().st_size > 2000
116
+ assert out.read_text().startswith("<!DOCTYPE html>")
117
+
118
+
119
+ def test_export_json_writes_file(tmp_path):
120
+ rep = tb.audit(clean_frame())
121
+ out = tb.export(rep, tmp_path / "r.json", fmt="json")
122
+ assert out.exists() and '"score"' in out.read_text()
123
+
124
+
125
+ def test_export_rejects_unknown_format(tmp_path):
126
+ with pytest.raises(ValueError, match="unsupported format"):
127
+ tb.export(tb.audit(clean_frame()), tmp_path / "r.xyz", fmt="xyz")
128
+
129
+
130
+ # ---------- cli column mapping ----------
131
+
132
+ def test_cli_maps_common_aliases():
133
+ from tickbloom.cli import normalize_columns
134
+ df = pd.DataFrame({"Datetime": ["2024-07-01T14:00:00Z"], "Px": [5500.0], "Qty": [2]})
135
+ out = normalize_columns(df, verbose=False)
136
+ assert {"ts_event", "price", "size", "symbol"} <= set(out.columns)
137
+ assert out["symbol"].iloc[0] == "UNKNOWN"
138
+
139
+
140
+ def test_cli_exits_on_unmappable_columns():
141
+ from tickbloom.cli import normalize_columns
142
+ with pytest.raises(SystemExit):
143
+ normalize_columns(pd.DataFrame({"foo": [1], "bar": [2]}), verbose=False)