flowbars 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.
Files changed (85) hide show
  1. flowbars-0.1.0/LICENSE +21 -0
  2. flowbars-0.1.0/MANIFEST.in +1 -0
  3. flowbars-0.1.0/PKG-INFO +650 -0
  4. flowbars-0.1.0/README.md +605 -0
  5. flowbars-0.1.0/flowbars/__init__.py +163 -0
  6. flowbars-0.1.0/flowbars/_version.py +8 -0
  7. flowbars-0.1.0/flowbars/adapters/__init__.py +29 -0
  8. flowbars-0.1.0/flowbars/adapters/polars.py +1115 -0
  9. flowbars-0.1.0/flowbars/bars/__init__.py +3 -0
  10. flowbars-0.1.0/flowbars/bars/accumulators.py +731 -0
  11. flowbars-0.1.0/flowbars/bars/constructor.py +1254 -0
  12. flowbars-0.1.0/flowbars/bars/information/__init__.py +41 -0
  13. flowbars-0.1.0/flowbars/bars/information/imbalance_dollar_bars.py +200 -0
  14. flowbars-0.1.0/flowbars/bars/information/imbalance_tick_bars.py +206 -0
  15. flowbars-0.1.0/flowbars/bars/information/imbalance_volume_bars.py +200 -0
  16. flowbars-0.1.0/flowbars/bars/information/run_dollar_bars.py +225 -0
  17. flowbars-0.1.0/flowbars/bars/information/run_tick_bars.py +231 -0
  18. flowbars-0.1.0/flowbars/bars/information/run_volume_bars.py +225 -0
  19. flowbars-0.1.0/flowbars/bars/minute/__init__.py +99 -0
  20. flowbars-0.1.0/flowbars/bars/minute/accumulators.py +303 -0
  21. flowbars-0.1.0/flowbars/bars/minute/engine.py +244 -0
  22. flowbars-0.1.0/flowbars/bars/minute/information.py +622 -0
  23. flowbars-0.1.0/flowbars/bars/minute/numba.py +1043 -0
  24. flowbars-0.1.0/flowbars/bars/minute/registry.py +22 -0
  25. flowbars-0.1.0/flowbars/bars/minute/standard.py +261 -0
  26. flowbars-0.1.0/flowbars/bars/minute/structural.py +474 -0
  27. flowbars-0.1.0/flowbars/bars/numba_backend.py +1227 -0
  28. flowbars-0.1.0/flowbars/bars/output.py +106 -0
  29. flowbars-0.1.0/flowbars/bars/registry.py +47 -0
  30. flowbars-0.1.0/flowbars/bars/spec.py +367 -0
  31. flowbars-0.1.0/flowbars/bars/standard/__init__.py +17 -0
  32. flowbars-0.1.0/flowbars/bars/standard/dollar_bars.py +168 -0
  33. flowbars-0.1.0/flowbars/bars/standard/tick_bars.py +166 -0
  34. flowbars-0.1.0/flowbars/bars/standard/time_bars.py +192 -0
  35. flowbars-0.1.0/flowbars/bars/standard/volume_bars.py +168 -0
  36. flowbars-0.1.0/flowbars/benchmarks.py +420 -0
  37. flowbars-0.1.0/flowbars/calendars.py +368 -0
  38. flowbars-0.1.0/flowbars/core.py +109 -0
  39. flowbars-0.1.0/flowbars/py.typed +0 -0
  40. flowbars-0.1.0/flowbars/quality.py +393 -0
  41. flowbars-0.1.0/flowbars/sample_data/__init__.py +28 -0
  42. flowbars-0.1.0/flowbars/sample_data/tick_data.csv +501 -0
  43. flowbars-0.1.0/flowbars/schema.py +476 -0
  44. flowbars-0.1.0/flowbars/thresholds.py +1167 -0
  45. flowbars-0.1.0/flowbars/tick_rule.py +89 -0
  46. flowbars-0.1.0/flowbars.egg-info/PKG-INFO +650 -0
  47. flowbars-0.1.0/flowbars.egg-info/SOURCES.txt +83 -0
  48. flowbars-0.1.0/flowbars.egg-info/dependency_links.txt +1 -0
  49. flowbars-0.1.0/flowbars.egg-info/requires.txt +24 -0
  50. flowbars-0.1.0/flowbars.egg-info/top_level.txt +1 -0
  51. flowbars-0.1.0/pyproject.toml +110 -0
  52. flowbars-0.1.0/setup.cfg +4 -0
  53. flowbars-0.1.0/tests/test_accumulators.py +870 -0
  54. flowbars-0.1.0/tests/test_bar_type_spec.py +165 -0
  55. flowbars-0.1.0/tests/test_calendars.py +357 -0
  56. flowbars-0.1.0/tests/test_calendars_extra.py +99 -0
  57. flowbars-0.1.0/tests/test_constructor.py +948 -0
  58. flowbars-0.1.0/tests/test_contract_metadata.py +316 -0
  59. flowbars-0.1.0/tests/test_information_bars.py +960 -0
  60. flowbars-0.1.0/tests/test_minute_bars.py +341 -0
  61. flowbars-0.1.0/tests/test_minute_calibration.py +283 -0
  62. flowbars-0.1.0/tests/test_minute_engine.py +437 -0
  63. flowbars-0.1.0/tests/test_minute_new_types.py +431 -0
  64. flowbars-0.1.0/tests/test_minute_numba.py +303 -0
  65. flowbars-0.1.0/tests/test_minute_session_policies.py +405 -0
  66. flowbars-0.1.0/tests/test_minute_state.py +278 -0
  67. flowbars-0.1.0/tests/test_numba_backend.py +1150 -0
  68. flowbars-0.1.0/tests/test_output_contract.py +175 -0
  69. flowbars-0.1.0/tests/test_polars_adapter.py +534 -0
  70. flowbars-0.1.0/tests/test_public_api.py +558 -0
  71. flowbars-0.1.0/tests/test_quality.py +495 -0
  72. flowbars-0.1.0/tests/test_sample_data.py +120 -0
  73. flowbars-0.1.0/tests/test_schema.py +363 -0
  74. flowbars-0.1.0/tests/test_session_gaps.py +350 -0
  75. flowbars-0.1.0/tests/test_smoke.py +74 -0
  76. flowbars-0.1.0/tests/test_standard_bars.py +554 -0
  77. flowbars-0.1.0/tests/test_state_identity.py +394 -0
  78. flowbars-0.1.0/tests/test_state_v2.py +283 -0
  79. flowbars-0.1.0/tests/test_strict_ordering.py +106 -0
  80. flowbars-0.1.0/tests/test_thresholds.py +637 -0
  81. flowbars-0.1.0/tests/test_tick_rule.py +173 -0
  82. flowbars-0.1.0/tests/test_update_many.py +253 -0
  83. flowbars-0.1.0/tests/test_volume_semantics.py +540 -0
  84. flowbars-0.1.0/tests/test_warmup_contract.py +297 -0
  85. flowbars-0.1.0/tests/test_watermark.py +210 -0
flowbars-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Syed Qaisar Jalil
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 @@
1
+ include flowbars/py.typed
@@ -0,0 +1,650 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowbars
3
+ Version: 0.1.0
4
+ Summary: Production-grade bar construction for financial ML — standard and information-driven bars, correct per AFML.
5
+ Author: Syed Qaisar Jalil
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/syedqaisarjalil/flowbars
8
+ Project-URL: Repository, https://github.com/syedqaisarjalil/flowbars
9
+ Project-URL: Issues, https://github.com/syedqaisarjalil/flowbars/issues
10
+ Keywords: financial-data,trading,ohlcv,market-microstructure,machine-learning,afml,bar-construction
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Typing :: Typed
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Office/Business :: Financial
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy>=1.24
27
+ Requires-Dist: pandas>=1.5
28
+ Provides-Extra: numba
29
+ Requires-Dist: numba>=0.57; extra == "numba"
30
+ Provides-Extra: polars
31
+ Requires-Dist: polars>=0.19; extra == "polars"
32
+ Requires-Dist: pyarrow>=12; extra == "polars"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7; extra == "dev"
35
+ Requires-Dist: pytest-cov; extra == "dev"
36
+ Requires-Dist: mypy>=1; extra == "dev"
37
+ Requires-Dist: ruff; extra == "dev"
38
+ Provides-Extra: quality
39
+ Requires-Dist: scipy>=1.9; extra == "quality"
40
+ Provides-Extra: calendars
41
+ Requires-Dist: exchange_calendars>=4.0; extra == "calendars"
42
+ Provides-Extra: all
43
+ Requires-Dist: flowbars[calendars,numba,polars,quality]; extra == "all"
44
+ Dynamic: license-file
45
+
46
+ # flowbars
47
+
48
+ Production-grade bar construction for financial ML — standard and
49
+ information-driven bars, correct per *Advances in Financial Machine Learning*
50
+ (López de Prado, 2018).
51
+
52
+ ```python
53
+ from flowbars import load_sample_data, compute_tick_bars, bar_quality_report
54
+ from flowbars.schema import SchemaMapping
55
+
56
+ df = load_sample_data()
57
+ schema = SchemaMapping({"timestamp": "timestamp", "price": "price", "volume": "volume"})
58
+ bars = compute_tick_bars(df, threshold=100, schema=schema)
59
+ report = bar_quality_report(bars)
60
+ print(f"Ljung-Box p-value: {report['ljung_box']['p_value']:.4f}")
61
+ ```
62
+
63
+ ## What makes this different
64
+
65
+ ### Correct AFML-spec thresholds
66
+
67
+ The two-component EWMA formula from AFML §2.4 is implemented exactly:
68
+
69
+ | Bar family | Threshold formula |
70
+ |---|---|
71
+ | Imbalance | *Tₙ = E₀[T]ₙ × \|E₀[θ]ₙ\|* |
72
+ | Run | *Tₙ = E₀[T]ₙ × max(E₀[P⁺]ₙ, 1 − E₀[P⁺]ₙ)* |
73
+
74
+ Both terms are updated **at bar-close time** using the bar-level statistics
75
+ (tick count or volume, signed-proportion or buy-fraction). This is not a
76
+ single-EWMA approximation — it is the specification.
77
+
78
+ Other libraries (mlfinlab, mlfinpy) use a single EWMA on the raw cumulative
79
+ metric, which produces different bar counts and different information content.
80
+ If you are reproducing research, this matters.
81
+
82
+ ### Streaming + resumable
83
+
84
+ Every bar constructor supports `update(tick)` for real-time streaming and
85
+ `get_state()` / `load_state()` / `from_state()` for crash-safe resume:
86
+
87
+ ```python
88
+ # Save mid-stream state
89
+ state = ctor.get_state() # plain dict, JSON-serializable
90
+
91
+ # Resume later — identical output as uninterrupted
92
+ ctor2 = TickBarConstructor.from_state(state)
93
+ ```
94
+
95
+ Resume is **idempotent** via a configurable watermark: re-feeding ticks that
96
+ were already processed is silently discarded. Defaults to the tick timestamp;
97
+ pass `watermark="seq"` (or any column name) for feeds that emit multiple ticks
98
+ per millisecond, and `watermark=None` to disable.
99
+
100
+ ```python
101
+ ctor = TickBarConstructor(threshold=100, schema=schema, watermark="timestamp")
102
+ state = ctor.get_state()
103
+
104
+ # Re-feeding an already-processed tick is a no-op
105
+ ctor2 = TickBarConstructor.from_state(state, schema=schema)
106
+ ctor2.update(tick_already_processed) # -> None (deduplicated)
107
+ ```
108
+
109
+ State is versioned (`schema_version`/`engine_version`) and carries the full
110
+ params plus a SHA-256 params hash, so resuming with a mismatched threshold,
111
+ multiplier, volume type, or session policy raises `StateValidationError`
112
+ instead of silently changing bar semantics. The state also records the
113
+ canonical registry key (`registry_name`), the calendar identity
114
+ (`calendar` + `calendar_fingerprint`), the input column mapping
115
+ (`schema_mapping` + `schema_fingerprint`), and the active session
116
+ (`session_id`, `last_session_end`, `in_session`). Resuming with a different
117
+ calendar or schema mapping therefore fails loudly — pass
118
+ `verify_identity=False` to `load_state()` / `from_state()` to opt out.
119
+ Tick and minute state can never be cross-loaded (`input_kind` is validated).
120
+ `update_many(df)` streams a whole DataFrame through the same `update()` path.
121
+
122
+ ### Warm-up contract
123
+
124
+ ```python
125
+ ctor = TickBarConstructor(
126
+ threshold=100,
127
+ schema=schema,
128
+ warmup_minutes=30, # discard bars closing in the first 30 minutes
129
+ )
130
+ # or warmup_bars=N — the two are mutually exclusive
131
+ ...
132
+ assert ctor.warmup_done # True once the warm-up window has elapsed
133
+ ```
134
+
135
+ Warm-up bars keep their original `bar_id`, still fire `on_bar`, and still feed
136
+ the adaptive estimator; only their return is suppressed.
137
+
138
+ ### Session policies
139
+
140
+ On each session-boundary transition (weekends, holidays, early closes, DST —
141
+ whatever the calendar reports) the in-progress bar is handled per
142
+ `session_policy`:
143
+
144
+ | Policy | Behavior |
145
+ |---|---|
146
+ | `"close"` (default) | Force-close as a `partial` bar; no bar spans two sessions |
147
+ | `"discard"` | Drop the in-progress bar silently (no callback, no overflow carried) |
148
+ | `"carry"` | Keep the bar open across the boundary (opt-in); the gap adds no return |
149
+
150
+ Session-only feeds (equity/FX data that never prints outside the trading day)
151
+ are handled too: the engine compares each in-session input against the
152
+ calendar's next session open, so the first input of a later session
153
+ force-closes the hanging bar at the **prior session's close** (`partial=True`,
154
+ `close_ts` = last session end) instead of letting one bar span the weekend or a
155
+ holiday. Minute volatility/hybrid bars exclude the gap return, and the minute
156
+ tick rule derives no sign across the gap.
157
+
158
+ When a stream simply ends mid-session (no later input to reveal the gap), call
159
+ `flush()` to emit the open bar as a `partial` bar:
160
+
161
+ ```python
162
+ bar = ctor.flush() # close_ts = calendar session end of the last input
163
+ bar = ctor.flush(now_ms) # or an explicit close instant
164
+ ```
165
+
166
+ `session_estimator="persist"` (default) keeps EWMA learning across sessions;
167
+ `"reset"` restores the seeds at each boundary. Policies are per constructor,
168
+ so different series can choose independently.
169
+
170
+ ### Discover bar types
171
+
172
+ ```python
173
+ from flowbars.bars.registry import BarRegistry
174
+
175
+ BarRegistry.list() # ['dollar', 'imbalance_tick', ...]
176
+ spec = BarRegistry.get_spec("volume")
177
+ spec.params_schema # JSON Schema for constructor params
178
+ spec.output_columns # core_columns + extras_columns
179
+ spec.core_columns # the 13 contract columns
180
+ spec.extras_columns # typed bar-specific extras ({} for built-ins)
181
+ spec.session_policies # ('close',)
182
+ spec.state_schema_version # 2
183
+ spec.spec_version # output-contract version (pin with params_hash)
184
+ spec.warmup_minutes # recommended warmup; 0 = none required
185
+ ```
186
+
187
+ `MinuteBarRegistry` mirrors this for the `minute_*` types. The registry key
188
+ is the canonical identity (`minute_volume`), while each row's `bar_type`
189
+ label is the shorter family name (`volume`); `frame.attrs["registry_name"]`
190
+ and `state["registry_name"]` carry the unambiguous key.
191
+
192
+ Rebuild any registered type from saved state without knowing its concrete
193
+ constructor:
194
+
195
+ ```python
196
+ resumed = BarRegistry.constructor_from_state("volume", state, schema=schema)
197
+ resumed_minute = MinuteBarRegistry.constructor_from_state(
198
+ "minute_renko", state, schema=minute_schema
199
+ )
200
+ ```
201
+
202
+ Pin a downstream contract with `params_hash(params)` (canonical JSON: sorted
203
+ keys, no whitespace, NaN/Infinity rejected) — frame attrs expose
204
+ `(registry_name, spec_version, params_hash)` for exactly this.
205
+
206
+ ### Dual backend
207
+
208
+ | Backend | Dependency | Speed vs. pure-Python |
209
+ |---|---|---|
210
+ | `"python"` | numpy, pandas | 1× (baseline) |
211
+ | `"numba"` | + numba | 85× – 237× |
212
+
213
+ The numba backend JIT-compiles the tick-ingestion loop. Compilation happens
214
+ on first call; subsequent calls run at native speed. If numba is not installed
215
+ or compilation fails, the Python path is used automatically with a warning.
216
+
217
+ ### Honest benchmarks
218
+
219
+ Measured, not assumed. Compilation cost reported separately. Benchmark script
220
+ in the repo — run it yourself:
221
+
222
+ ```bash
223
+ python -m flowbars.benchmarks
224
+ ```
225
+
226
+ Results on 250,000 synthetic ticks (Intel/AMD x86-64, numba 0.57+):
227
+
228
+ | Bar type | Python (s) | numba (ms) | Speedup |
229
+ |---|---|---|---|
230
+ | tick | 0.56 | 6.6 | **86×** |
231
+ | volume | 0.56 | 5.1 | **109×** |
232
+ | dollar | 0.68 | 4.7 | **145×** |
233
+ | time (5-min) | 0.57 | 3.2 | **181×** |
234
+ | imbalance_tick | 0.61 | 3.5 | **177×** |
235
+ | imbalance_volume | 0.64 | 3.3 | **194×** |
236
+ | imbalance_dollar | 0.75 | 6.1 | **123×** |
237
+ | run_tick | 1.04 | 5.0 | **210×** |
238
+ | run_volume | 1.00 | 4.2 | **237×** |
239
+ | run_dollar | 1.24 | 6.7 | **185×** |
240
+ | imbalance_tick (EWMA) | 0.59 | 5.3 | **111×** |
241
+ | run_tick (EWMA) | 2.40 | 18.8 | **127×** |
242
+
243
+ ### Pluggable calendars
244
+
245
+ Four calendars cover every asset class, with full holiday/DST support where it
246
+ matters:
247
+
248
+ | Asset class | Calendar |
249
+ |---|---|
250
+ | Crypto (spot/perp, 24/7) | `ContinuousCalendar` |
251
+ | FX majors/minors (24/5) | `WeekdayCalendar` |
252
+ | US equities | `ExchangeCalendar("XNYS")` |
253
+ | US futures / commodities | `ExchangeCalendar("CME")` |
254
+ | LSE / EU equities | `ExchangeCalendar("LSE")` / `("XETR")` |
255
+ | Custom fixed UTC session | `SessionCalendar(...)` |
256
+
257
+ - `ContinuousCalendar` — always open; never a boundary.
258
+ - `WeekdayCalendar` — open Mon–Fri; boundaries only at weekends.
259
+ - `SessionCalendar` — fixed daily UTC hours (no holidays/DST — use only for a
260
+ constant UTC window).
261
+ - `ExchangeCalendar` — wraps `exchange_calendars` for per-exchange holidays,
262
+ half-days, and DST-aware sessions (`pip install flowbars[calendars]`).
263
+
264
+ Session boundaries trigger automatic bar close and accumulator reset.
265
+
266
+ ### No silent guesses
267
+
268
+ Explicit schema mapping — you tell the library your column names. No
269
+ auto-detection, no magic. If a required column is missing, you get a clear
270
+ `SchemaError`.
271
+
272
+ ### Bar quality diagnostics
273
+
274
+ `bar_quality_report()` provides honest, standard statistical tests. `scipy`
275
+ is optional — without it the Ljung-Box / Jarque-Bera statistics are still
276
+ computed (in numpy) but their p-values and reject flags are `None`.
277
+
278
+ | Diagnostic | Test | What it flags |
279
+ |---|---|---|
280
+ | Autocorrelation | Ljung-Box | Serially-correlated returns |
281
+ | Normality | Jarque-Bera | Non-normal return distribution |
282
+ | Stability | CV of per-day bar counts | Uneven sampling rate |
283
+ | Fragmentation | Fraction of run-bars ≤ 2 ticks/minutes | Over-fragmented run bars |
284
+ | Return moments | mean, std, skewness, kurtosis | Distribution shape |
285
+ | Return ACF | lag-1, lag-5 (raw and `\|r\|`) | Serial dependence, volatility clustering |
286
+ | Variance ratio | lag-2, lag-5 | Random-walk departure |
287
+ | Entropy | Histogram Shannon entropy | Information content (normalized) |
288
+ | Bar-size CV | CV of volume (or `num_ticks`) | Uneven bar sizes |
289
+ | Duration CV | CV of `close_ts − open_ts` | Uneven bar durations |
290
+ | Effective sample size | `n·(1−ρ₁)/(1+ρ₁)` | Overstated sample size from autocorrelation |
291
+ | OHLC integrity | Violations of `low ≤ open, close ≤ high` | Malformed bars |
292
+ | Composite score | Mean of available sub-scores | One-number quality summary |
293
+
294
+ All metrics are deterministic (fixed lag set, fixed histogram rule, no RNG).
295
+ For minute bars pass `num_ticks_unit="minutes"`; the run-fragmentation
296
+ threshold is configurable with `fragmentation_threshold=`. Run fragmentation
297
+ is masked to `run_*` bar types, so mixed frames report correctly.
298
+
299
+ ## Bar types
300
+
301
+ ### Standard (activity-based)
302
+
303
+ | Bar type | Closes when… | Constructor |
304
+ |---|---|---|
305
+ | tick | `num_ticks ≥ threshold` | `TickBarConstructor` |
306
+ | volume | `cumulative_volume ≥ threshold` | `VolumeBarConstructor` |
307
+ | dollar | `cumulative_dollar ≥ threshold` | `DollarBarConstructor` |
308
+ | time | tick crosses interval boundary | `TimeBarConstructor` |
309
+
310
+ ### Information-driven (adaptive threshold)
311
+
312
+ | Bar type | Closes when… | Constructor |
313
+ |---|---|---|
314
+ | imbalance_tick | \|Σ signed ticks\| ≥ adaptive T | `ImbalanceTickBarConstructor` |
315
+ | imbalance_volume | \|Σ signed volume\| ≥ adaptive T | `ImbalanceVolumeBarConstructor` |
316
+ | imbalance_dollar | \|Σ signed dollar\| ≥ adaptive T | `ImbalanceDollarBarConstructor` |
317
+ | run_tick | same-sign tick run ≥ adaptive T | `RunTickBarConstructor` |
318
+ | run_volume | same-sign volume run ≥ adaptive T | `RunVolumeBarConstructor` |
319
+ | run_dollar | same-sign dollar run ≥ adaptive T | `RunDollarBarConstructor` |
320
+
321
+ All ten are available through convenience functions:
322
+
323
+ ```python
324
+ from flowbars import (
325
+ compute_tick_bars,
326
+ compute_volume_bars,
327
+ compute_dollar_bars,
328
+ compute_time_bars,
329
+ compute_imbalance_tick_bars,
330
+ compute_imbalance_volume_bars,
331
+ compute_imbalance_dollar_bars,
332
+ compute_run_tick_bars,
333
+ compute_run_volume_bars,
334
+ compute_run_dollar_bars,
335
+ )
336
+ ```
337
+
338
+ Every tick constructor and `compute_*_bars` function accepts `warmup_bars` /
339
+ `warmup_minutes`, `backend`, `stream_id`, `multiplier`, `volume_type`,
340
+ `session_policy`, `session_estimator`, and the `on_bar` /
341
+ `on_threshold_update` callbacks, matching the minute path's signature surface.
342
+
343
+ ## Minute-OHLCV path
344
+
345
+ Non-time bars can also be built from **minute OHLCV candles** instead of raw
346
+ ticks. The minute path has full mode parity with the tick path — streaming
347
+ `update()`/`update_many()`, versioned state save/resume, callbacks,
348
+ `warmup_bars`/`warmup_minutes`, watermark dedup, `strict_ordering`, calendars
349
+ with the same session policies, the numba batch backend, and the polars
350
+ adapter — and produces the identical 13-column output schema. `num_ticks`
351
+ counts **minutes**, and `dollar_value` / `vwap` use `close × volume` as the
352
+ notional proxy (so dollar-family minute bars are flagged
353
+ `approximate=True` in the registry spec and frame metadata).
354
+
355
+ | Minute type | Closes when… | Function |
356
+ |---|---|---|
357
+ | volume | `Σ volume ≥ threshold` | `compute_volume_bars_from_minutes` |
358
+ | dollar | `Σ close×volume ≥ threshold` | `compute_dollar_bars_from_minutes` |
359
+ | imbalance_volume | \|Σ sign×volume\| ≥ EWMA T | `compute_imbalance_volume_bars_from_minutes` |
360
+ | imbalance_dollar | \|Σ sign×close×volume\| ≥ EWMA T | `compute_imbalance_dollar_bars_from_minutes` |
361
+ | run_volume | same-sign volume run ≥ EWMA T | `compute_run_volume_bars_from_minutes` |
362
+ | run_dollar | same-sign dollar run ≥ EWMA T | `compute_run_dollar_bars_from_minutes` |
363
+ | volatility | `Σ \|closeₜ−closeₜ₋₁\|/closeₜ₋₁ ≥ threshold` | `compute_volatility_bars_from_minutes` |
364
+ | range | `Σ (high−low)/close ≥ threshold` | `compute_range_bars_from_minutes` |
365
+ | renko | `\|close−anchor\|/anchor ≥ threshold` (anchor = first open) | `compute_renko_bars_from_minutes` |
366
+ | hybrid | both `Σ close×volume` and volatility targets met | `compute_hybrid_bars_from_minutes` |
367
+
368
+ The four structural types (volatility, range, renko, hybrid) are minute-only
369
+ and use **static** thresholds. Renko displacement is not additive, so its
370
+ anchor resets on every close with no overflow; the other three roll their
371
+ scalar excess into the next bar. Volatility and hybrid reset `prev_close` at
372
+ session boundaries so an overnight gap contributes no fake return.
373
+
374
+ ### Calibrate thresholds from minute data
375
+
376
+ `MinuteCalibrationHelper` is deterministic (no RNG, fixed bisection
377
+ iterations): repeated calls on the same input return identical values. Targets
378
+ are bars **per UTC day**, scaled by the number of observed UTC days.
379
+
380
+ ```python
381
+ from flowbars import MinuteCalibrationHelper
382
+
383
+ vol_threshold = MinuteCalibrationHelper.estimate_volume_threshold(minutes, 50.0)
384
+ renko_threshold = MinuteCalibrationHelper.estimate_renko_threshold(minutes, 50.0)
385
+ dollar_t, variance_t = MinuteCalibrationHelper.estimate_hybrid_thresholds(
386
+ minutes, 50.0, volatility_threshold=0.01
387
+ )
388
+ seeds = MinuteCalibrationHelper.estimate_ewma_seeds(
389
+ minutes, "run", metric="dollar", span=20.0, target_bars=50
390
+ )
391
+ ```
392
+
393
+ Minute bars work with the polars adapter too:
394
+ `from flowbars.adapters.polars import compute_volume_bars_from_minutes`.
395
+
396
+ ## Install
397
+
398
+ ```bash
399
+ pip install flowbars # pure-Python backend
400
+ pip install flowbars[numba] # + numba JIT backend
401
+ pip install flowbars[polars] # + polars DataFrame adapter
402
+ pip install flowbars[calendars] # + exchange_calendars (holidays/DST)
403
+ pip install flowbars[all] # everything
404
+ ```
405
+
406
+ ## Quickstart
407
+
408
+ ### Build tick bars from the bundled sample data
409
+
410
+ ```python
411
+ from flowbars import load_sample_data, compute_tick_bars
412
+ from flowbars.schema import SchemaMapping
413
+
414
+ df = load_sample_data() # 500 ticks with timestamp, price, volume
415
+ schema = SchemaMapping({"timestamp": "timestamp", "price": "price", "volume": "volume"})
416
+
417
+ bars = compute_tick_bars(df, threshold=50, schema=schema)
418
+ print(bars.head())
419
+ ```
420
+
421
+ ### Build dollar bars from your own data
422
+
423
+ ```python
424
+ import pandas as pd
425
+ from flowbars import compute_dollar_bars
426
+ from flowbars.schema import SchemaMapping
427
+
428
+ ticks = pd.read_csv("my_ticks.csv") # must have ts, px, qty columns
429
+ schema = SchemaMapping({"timestamp": "ts", "price": "px", "volume": "qty"})
430
+
431
+ bars = compute_dollar_bars(ticks, threshold=1_000_000, schema=schema)
432
+ ```
433
+
434
+ ### Build imbalance bars with adaptive threshold
435
+
436
+ ```python
437
+ from flowbars import compute_imbalance_tick_bars
438
+ from flowbars.tick_rule import resolve_tick_signs
439
+
440
+ df["side"] = resolve_tick_signs(df["price"].values, None)
441
+ schema = SchemaMapping(
442
+ {
443
+ "timestamp": "ts",
444
+ "price": "px",
445
+ "volume": "qty",
446
+ "side": "side",
447
+ }
448
+ )
449
+
450
+ bars = compute_imbalance_tick_bars(
451
+ df,
452
+ span=20.0, # EWMA span
453
+ warmup_bars=5, # discard first 5 bars while EWMA converges
454
+ schema=schema,
455
+ )
456
+ ```
457
+
458
+ ### Use the numba backend
459
+
460
+ ```python
461
+ from flowbars import VolumeBarConstructor
462
+
463
+ ctor = VolumeBarConstructor(
464
+ 5_000.0,
465
+ schema=schema,
466
+ backend="numba", # flip this one switch
467
+ )
468
+ bars = ctor.batch(ticks_df)
469
+ ```
470
+
471
+ All ten tick constructors and all ten `compute_*_bars` functions accept
472
+ `backend="numba"`; minute constructors take the same argument. The Python
473
+ backend remains the default, and `update()` always runs in Python.
474
+
475
+ ### Polars adapter
476
+
477
+ ```python
478
+ import polars as pl
479
+ from flowbars.adapters.polars import compute_volume_bars
480
+
481
+ pl_ticks = pl.read_csv("ticks.csv")
482
+ pl_bars = compute_volume_bars(pl_ticks, threshold=5000)
483
+ ```
484
+
485
+ ### Save and resume mid-stream
486
+
487
+ ```python
488
+ # Process first batch
489
+ ctor = TickBarConstructor(threshold=100, schema=schema)
490
+ batch1 = ctor.batch(ticks_df.iloc[:1000])
491
+
492
+ # Save state
493
+ state = ctor.get_state()
494
+
495
+ # ... days later, resume with new data ...
496
+ ctor2 = TickBarConstructor.from_state(state, schema=schema)
497
+ batch2 = ctor2.batch(ticks_df.iloc[1000:])
498
+ ```
499
+
500
+ ### Check bar quality
501
+
502
+ ```python
503
+ from flowbars import bar_quality_report
504
+
505
+ report = bar_quality_report(bars, ljung_box_lags=10)
506
+ print(f"Autocorrelated returns: {report['ljung_box']['reject_autocorrelation']}")
507
+ print(f"Non-normal returns: {report['jarque_bera']['reject_normality']}")
508
+ print(f"Bar-count CV: {report['bar_count_stability']['cv']:.3f}")
509
+ ```
510
+
511
+ ## Reproducing benchmarks
512
+
513
+ ### Data sourcing
514
+
515
+ The benchmarks use synthetic data generated in-process (seeded random walk
516
+ prices + exponential volumes). No external data files are needed.
517
+
518
+ To benchmark against the same data used in the README table:
519
+
520
+ ```bash
521
+ python -m flowbars.benchmarks
522
+ ```
523
+
524
+ ### Comparing with mlfinlab / mlfinpy
525
+
526
+ The comparison notebook `notebooks/01_compare_mlfinpy.ipynb` compares flowbars
527
+ against [mlfinpy](https://pypi.org/project/mlfinpy/) — the free, open-source
528
+ reimplementation of the paid MlFinLab — on synthetic tick data (no external
529
+ data files needed). It requires Python ≥ 3.11:
530
+
531
+ ```bash
532
+ conda create -n flowbars311 python=3.11 -y
533
+ conda activate flowbars311
534
+ pip install mlfinpy scikit-learn
535
+ pip install . # from the flowbars repo root
536
+ jupyter nbconvert --to notebook --execute notebooks/01_compare_mlfinpy.ipynb
537
+ ```
538
+
539
+ A direct comparison against the paid MlFinLab is deferred; mlfinpy serves as
540
+ the free, drop-in proxy.
541
+
542
+ ## Assumptions
543
+
544
+ | Assumption | Detail |
545
+ |---|---|
546
+ | **UTC only** | No timezone conversion or detection. All timestamps are treated as Unix milliseconds in UTC. |
547
+ | **Ticks ordered** | Ticks must be non-decreasing in timestamp. The library does not sort for you — it processes in the order given. Set `strict_ordering=True` to raise on out-of-order input. |
548
+ | **Explicit schema** | You tell the library your column names via `SchemaMapping`. There is no auto-detection. |
549
+ | **Float64 internally** | All prices, volumes, and computed values use `float64`. Float32 input is upcast. |
550
+ | **NaN side = unknown** | A tick with `side=NaN` is treated as undetermined (first tick of a stream). It is included in OHLCV but excluded from signed-imbalance sums. Run-bar logic treats NaN as a wildcard that matches any direction. |
551
+ | **Thread safety** | Bar constructors are **not** thread-safe. Feed ticks from a single thread. |
552
+ | **Batch = streaming** | `ctor.batch(df)` and `ctor.update(tick)` in a loop produce identical output for the same data. |
553
+
554
+ ## API reference
555
+
556
+ ### Top-level functions
557
+
558
+ | Function | Returns | Description |
559
+ |---|---|---|
560
+ | `compute_tick_bars(df, threshold, ...)` | DataFrame | Tick bars |
561
+ | `compute_volume_bars(df, threshold, ...)` | DataFrame | Volume bars |
562
+ | `compute_dollar_bars(df, threshold, ...)` | DataFrame | Dollar bars |
563
+ | `compute_time_bars(df, interval_ms, ...)` | DataFrame | Time bars |
564
+ | `compute_imbalance_tick_bars(df, ...)` | DataFrame | Tick-imbalance bars |
565
+ | `compute_imbalance_volume_bars(df, ...)` | DataFrame | Volume-imbalance bars |
566
+ | `compute_imbalance_dollar_bars(df, ...)` | DataFrame | Dollar-imbalance bars |
567
+ | `compute_run_tick_bars(df, ...)` | DataFrame | Tick-run bars |
568
+ | `compute_run_volume_bars(df, ...)` | DataFrame | Volume-run bars |
569
+ | `compute_run_dollar_bars(df, ...)` | DataFrame | Dollar-run bars |
570
+ | `load_sample_data()` | DataFrame | Bundled 500-tick sample |
571
+ | `bar_quality_report(bars)` | dict | Statistical diagnostics |
572
+ | `params_hash(params)` | str | Canonical SHA-256 of a params dict |
573
+
574
+ All `compute_*_bars` functions also accept keyword-only `warmup_bars` /
575
+ `warmup_minutes`, `backend`, `stream_id`, `multiplier`, `volume_type`,
576
+ `session_policy`, `session_estimator`, `on_bar`, and `on_threshold_update`.
577
+ The minute equivalents are `compute_*_bars_from_minutes`
578
+ (`compute_volume_bars_from_minutes`, …, `compute_hybrid_bars_from_minutes`)
579
+ with the same keyword surface.
580
+
581
+ ### Output contract
582
+
583
+ Every bar — tick or minute — has the same 13 columns: `bar_id`, `open`,
584
+ `high`, `low`, `close`, `volume`, `dollar_value`, `vwap`, `num_ticks`,
585
+ `open_ts`, `close_ts`, `bar_type`, `partial`. All are **core** columns
586
+ (`spec.core_columns`); registered extras, if any, are appended and typed
587
+ (`spec.extras_columns`). `partial` is `True` only for session-boundary
588
+ force-closes. Returned DataFrames carry series metadata in `DataFrame.attrs`
589
+ (`bar_type`, `registry_name`, `params_hash`, `spec_version`, input kind,
590
+ engine/state version, multiplier, volume type, `approximate`). `multiplier`
591
+ scales `dollar_value` (and the dollar/imbalance/run metrics) while `vwap`
592
+ stays a price; `volume_type` (`base`/`quote`/`shares`/`contracts`/`tick`) is
593
+ metadata.
594
+
595
+ Timestamp conventions:
596
+
597
+ * `open_ts` is the timestamp of the bar's first input — the **stable bar
598
+ identity**. It is unique within a series (`validate_bar_frame` enforces
599
+ this). `bar_id` is run-local and resets when you rebuild from source, so
600
+ key downstream rows on `open_ts`, not `bar_id`.
601
+ * `close_ts` is the last input that completed a threshold/time bar, or the
602
+ crossing minute's open timestamp on the minute path.
603
+ * For `partial=True` bars, `close_ts` is the **session boundary** (session
604
+ close or next open), i.e. the instant the bar's information became
605
+ available.
606
+
607
+ Input timestamps must be integer Unix epoch **milliseconds** (integral
608
+ floats are accepted and cast; `datetime64`/tz-aware columns raise
609
+ `SchemaError` with a conversion hint). Pass `validate=True` to
610
+ `batch()` / `update_many()` to run `validate_bar_frame` on the returned frame.
611
+
612
+ ### Core classes
613
+
614
+ | Class | Purpose |
615
+ |---|---|
616
+ | `BaseBarConstructor` | Configurable bar construction engine |
617
+ | `SchemaMapping` / `MinuteSchemaMapping` | Explicit column-name mapping |
618
+ | `BarRegistry` / `MinuteBarRegistry` | Discover registered bar types |
619
+ | `BarTypeSpec` | Machine-readable spec (params schema, output columns, capabilities) |
620
+ | `EWMAThresholdEstimator` | Two-component adaptive threshold |
621
+ | `StaticThresholdEstimator` | Fixed threshold |
622
+ | `StaticCalibrationHelper` | Estimate good seeds from data |
623
+ | `ContinuousCalendar` | No session boundaries (24/7) |
624
+ | `WeekdayCalendar` | Mon–Fri, 24/5 |
625
+ | `SessionCalendar` | Fixed daily UTC session hours |
626
+ | `ExchangeCalendar` | Holiday/DST-aware (via `exchange_calendars`) |
627
+
628
+ ### Tick rule
629
+
630
+ | Function | Description |
631
+ |---|---|
632
+ | `derive_tick_sign(prices)` | Tick rule on raw prices → side array |
633
+ | `resolve_tick_signs(prices, sides)` | Use supplied sides or derive |
634
+
635
+ ## Comparison to other libraries
636
+
637
+ | | flowbars | mlfinlab | mlfinpy |
638
+ |---|---|---|---|
639
+ | **EWMA formula** | Two-component (AFML spec) | Single EWMA | Single EWMA |
640
+ | **Streaming** | `update(tick)` + state save/resume | Batch only | Batch only |
641
+ | **numba backend** | Yes (85×–237×) | Yes | No |
642
+ | **Polars** | First-class adapter | No | No |
643
+ | **Calendars** | Pluggable (continuous, weekday, sessions, exchange-backed) | Hardcoded NYSE | Hardcoded |
644
+ | **Bar quality** | Built-in diagnostics | None | None |
645
+ | **Schema** | Explicit mapping | Auto-inference | Auto-inference |
646
+ | **Benchmarks** | Reproducible, in-repo | None | None |
647
+
648
+ ## License
649
+
650
+ MIT — see [LICENSE](https://github.com/syedqaisarjalil/flowbars/blob/main/LICENSE).