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.
- tickbloom-0.1.0/LICENSE +21 -0
- tickbloom-0.1.0/PKG-INFO +168 -0
- tickbloom-0.1.0/README.md +135 -0
- tickbloom-0.1.0/pyproject.toml +56 -0
- tickbloom-0.1.0/tests/__init__.py +0 -0
- tickbloom-0.1.0/tests/test_report.py +143 -0
- tickbloom-0.1.0/tests/test_scan.py +239 -0
- tickbloom-0.1.0/tests/test_scoring.py +174 -0
- tickbloom-0.1.0/tickbloom/__init__.py +158 -0
- tickbloom-0.1.0/tickbloom/__main__.py +3 -0
- tickbloom-0.1.0/tickbloom/checks.py +163 -0
- tickbloom-0.1.0/tickbloom/cli.py +183 -0
- tickbloom-0.1.0/tickbloom/report.py +242 -0
- tickbloom-0.1.0/tickbloom/scan.py +374 -0
- tickbloom-0.1.0/tickbloom/scoring.py +185 -0
tickbloom-0.1.0/LICENSE
ADDED
|
@@ -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.
|
tickbloom-0.1.0/PKG-INFO
ADDED
|
@@ -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 "<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)
|