flowbars 0.1.0__py3-none-any.whl
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.
- flowbars/__init__.py +163 -0
- flowbars/_version.py +8 -0
- flowbars/adapters/__init__.py +29 -0
- flowbars/adapters/polars.py +1115 -0
- flowbars/bars/__init__.py +3 -0
- flowbars/bars/accumulators.py +731 -0
- flowbars/bars/constructor.py +1254 -0
- flowbars/bars/information/__init__.py +41 -0
- flowbars/bars/information/imbalance_dollar_bars.py +200 -0
- flowbars/bars/information/imbalance_tick_bars.py +206 -0
- flowbars/bars/information/imbalance_volume_bars.py +200 -0
- flowbars/bars/information/run_dollar_bars.py +225 -0
- flowbars/bars/information/run_tick_bars.py +231 -0
- flowbars/bars/information/run_volume_bars.py +225 -0
- flowbars/bars/minute/__init__.py +99 -0
- flowbars/bars/minute/accumulators.py +303 -0
- flowbars/bars/minute/engine.py +244 -0
- flowbars/bars/minute/information.py +622 -0
- flowbars/bars/minute/numba.py +1043 -0
- flowbars/bars/minute/registry.py +22 -0
- flowbars/bars/minute/standard.py +261 -0
- flowbars/bars/minute/structural.py +474 -0
- flowbars/bars/numba_backend.py +1227 -0
- flowbars/bars/output.py +106 -0
- flowbars/bars/registry.py +47 -0
- flowbars/bars/spec.py +367 -0
- flowbars/bars/standard/__init__.py +17 -0
- flowbars/bars/standard/dollar_bars.py +168 -0
- flowbars/bars/standard/tick_bars.py +166 -0
- flowbars/bars/standard/time_bars.py +192 -0
- flowbars/bars/standard/volume_bars.py +168 -0
- flowbars/benchmarks.py +420 -0
- flowbars/calendars.py +368 -0
- flowbars/core.py +109 -0
- flowbars/py.typed +0 -0
- flowbars/quality.py +393 -0
- flowbars/sample_data/__init__.py +28 -0
- flowbars/sample_data/tick_data.csv +501 -0
- flowbars/schema.py +476 -0
- flowbars/thresholds.py +1167 -0
- flowbars/tick_rule.py +89 -0
- flowbars-0.1.0.dist-info/METADATA +650 -0
- flowbars-0.1.0.dist-info/RECORD +46 -0
- flowbars-0.1.0.dist-info/WHEEL +5 -0
- flowbars-0.1.0.dist-info/licenses/LICENSE +21 -0
- flowbars-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Run bars (dollar) — close when the cumulative same-sign dollar-run crosses an adaptive threshold.
|
|
2
|
+
|
|
3
|
+
Same two-component EWMA formula as tick-run bars, but runs are weighted
|
|
4
|
+
by notional value (price × volume) instead of tick count.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from flowbars.bars.accumulators import RunAccumulator
|
|
14
|
+
from flowbars.bars.constructor import BaseBarConstructor
|
|
15
|
+
from flowbars.bars.registry import BarRegistry
|
|
16
|
+
from flowbars.calendars import TradingCalendar
|
|
17
|
+
from flowbars.core import Bar
|
|
18
|
+
from flowbars.schema import SchemaMapping
|
|
19
|
+
from flowbars.thresholds import EWMAThresholdEstimator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RunDollarBarConstructor(BaseBarConstructor):
|
|
23
|
+
"""Bar constructor for dollar-run bars.
|
|
24
|
+
|
|
25
|
+
A dollar-run bar closes when the cumulative same-sign dollar-run total
|
|
26
|
+
reaches or exceeds an **adaptive** threshold estimated by an EWMA.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
span : float, default 20.0
|
|
31
|
+
EWMA span for the two-component threshold formula.
|
|
32
|
+
halflife : float or None, default None
|
|
33
|
+
EWMA halflife. Takes precedence over *span* when given.
|
|
34
|
+
initial_ewa_t : float, default 1.0
|
|
35
|
+
Initial seed for :math:`E[T]` (expected run dollar value per bar).
|
|
36
|
+
initial_ewa_proportion : float, default 0.5
|
|
37
|
+
Initial seed for :math:`E[P^+]` (expected buy-run proportion).
|
|
38
|
+
warmup_bars : int, default 0
|
|
39
|
+
Number of initial bars to discard while the EWMA converges.
|
|
40
|
+
min_run_length : int, default 0
|
|
41
|
+
Minimum number of ticks a run bar must contain to be returned.
|
|
42
|
+
Bars with fewer ticks are discarded (they still update the EWMA).
|
|
43
|
+
Default 0 — no filtering.
|
|
44
|
+
calendar : TradingCalendar, optional
|
|
45
|
+
schema : SchemaMapping, optional
|
|
46
|
+
watermark : str or None, default ``"timestamp"``
|
|
47
|
+
Dedup key for idempotent resume (``"timestamp"``, a column name, or
|
|
48
|
+
``None`` to disable).
|
|
49
|
+
strict_ordering : bool, default False
|
|
50
|
+
If True, raise ``TickDataError`` when a tick arrives with a timestamp
|
|
51
|
+
earlier than the previous tick's (out-of-order input).
|
|
52
|
+
backend : str, default ``"python"``
|
|
53
|
+
``"python"`` or ``"numba"``. The numba backend accelerates
|
|
54
|
+
:meth:`batch` only; ``update`` always uses Python.
|
|
55
|
+
stream_id : str, default ``"default"``
|
|
56
|
+
on_bar : callable or None
|
|
57
|
+
on_threshold_update : callable or None
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
span: float = 20.0,
|
|
63
|
+
halflife: float | None = None,
|
|
64
|
+
initial_ewa_t: float = 1.0,
|
|
65
|
+
initial_ewa_proportion: float = 0.5,
|
|
66
|
+
warmup_bars: int = 0,
|
|
67
|
+
warmup_minutes: int | None = None,
|
|
68
|
+
min_run_length: int = 0,
|
|
69
|
+
calendar: TradingCalendar | None = None,
|
|
70
|
+
schema: SchemaMapping | None = None,
|
|
71
|
+
watermark: str | None = "timestamp",
|
|
72
|
+
stream_id: str = "default",
|
|
73
|
+
strict_ordering: bool = False,
|
|
74
|
+
backend: Literal["python", "numba"] = "python",
|
|
75
|
+
on_bar: Any = None,
|
|
76
|
+
on_threshold_update: Any = None,
|
|
77
|
+
multiplier: float = 1.0,
|
|
78
|
+
volume_type: str = "base",
|
|
79
|
+
) -> None:
|
|
80
|
+
if min_run_length < 0:
|
|
81
|
+
raise ValueError(f"min_run_length must be non-negative, got {min_run_length}")
|
|
82
|
+
self._min_run_length = min_run_length
|
|
83
|
+
|
|
84
|
+
accumulator = RunAccumulator(bar_type="run_dollar", metric="dollar")
|
|
85
|
+
estimator = EWMAThresholdEstimator(
|
|
86
|
+
bar_family="run",
|
|
87
|
+
span=span,
|
|
88
|
+
halflife=halflife,
|
|
89
|
+
initial_ewa_t=initial_ewa_t,
|
|
90
|
+
initial_ewa_proportion=initial_ewa_proportion,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
super().__init__(
|
|
94
|
+
accumulator=accumulator,
|
|
95
|
+
threshold_estimator=estimator,
|
|
96
|
+
calendar=calendar,
|
|
97
|
+
schema=schema,
|
|
98
|
+
watermark=watermark,
|
|
99
|
+
stream_id=stream_id,
|
|
100
|
+
strict_ordering=strict_ordering,
|
|
101
|
+
warmup_bars=warmup_bars,
|
|
102
|
+
warmup_minutes=warmup_minutes,
|
|
103
|
+
backend=backend,
|
|
104
|
+
on_bar=on_bar,
|
|
105
|
+
on_threshold_update=on_threshold_update,
|
|
106
|
+
multiplier=multiplier,
|
|
107
|
+
volume_type=volume_type,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _should_return(self, bar: Bar) -> bool:
|
|
111
|
+
"""Filter bars below ``min_run_length`` ticks."""
|
|
112
|
+
return bar.num_ticks >= self._min_run_length
|
|
113
|
+
|
|
114
|
+
def _extra_params(self) -> dict[str, Any]:
|
|
115
|
+
return {"min_run_length": self._min_run_length}
|
|
116
|
+
|
|
117
|
+
def _extra_state(self) -> dict[str, Any]:
|
|
118
|
+
return {"min_run_length": self._min_run_length}
|
|
119
|
+
|
|
120
|
+
def _load_extra_state(self, state: dict[str, Any]) -> None:
|
|
121
|
+
self._min_run_length = int(state.get("min_run_length", 0))
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_state( # type: ignore[override]
|
|
125
|
+
cls,
|
|
126
|
+
state: dict[str, Any],
|
|
127
|
+
calendar: TradingCalendar | None = None,
|
|
128
|
+
schema: SchemaMapping | None = None,
|
|
129
|
+
*,
|
|
130
|
+
verify_identity: bool = True,
|
|
131
|
+
) -> RunDollarBarConstructor:
|
|
132
|
+
"""Reconstruct a ``RunDollarBarConstructor`` from a saved state dict."""
|
|
133
|
+
acc_state = state["accumulator"]
|
|
134
|
+
acc = RunAccumulator(
|
|
135
|
+
bar_type=state["bar_type"],
|
|
136
|
+
metric=acc_state["metric"],
|
|
137
|
+
)
|
|
138
|
+
est = EWMAThresholdEstimator.from_state(state["threshold_estimator"])
|
|
139
|
+
inst: RunDollarBarConstructor = super().from_state(
|
|
140
|
+
state, acc, est, calendar, schema, verify_identity=verify_identity
|
|
141
|
+
) # type: ignore[assignment]
|
|
142
|
+
return inst
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def compute_run_dollar_bars(
|
|
146
|
+
ticks_df: pd.DataFrame,
|
|
147
|
+
span: float = 20.0,
|
|
148
|
+
halflife: float | None = None,
|
|
149
|
+
initial_ewa_t: float = 1.0,
|
|
150
|
+
initial_ewa_proportion: float = 0.5,
|
|
151
|
+
warmup_bars: int = 0,
|
|
152
|
+
warmup_minutes: int | None = None,
|
|
153
|
+
min_run_length: int = 0,
|
|
154
|
+
schema: SchemaMapping | None = None,
|
|
155
|
+
watermark: str | None = "timestamp",
|
|
156
|
+
strict_ordering: bool = False,
|
|
157
|
+
calendar: TradingCalendar | None = None,
|
|
158
|
+
*,
|
|
159
|
+
backend: Literal["python", "numba"] = "python",
|
|
160
|
+
stream_id: str = "default",
|
|
161
|
+
on_bar: Any = None,
|
|
162
|
+
on_threshold_update: Any = None,
|
|
163
|
+
multiplier: float = 1.0,
|
|
164
|
+
volume_type: str = "base",
|
|
165
|
+
) -> pd.DataFrame:
|
|
166
|
+
"""Build dollar-run bars from a DataFrame of ticks.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
ticks_df : pd.DataFrame
|
|
171
|
+
Tick-level data with user-mapped columns.
|
|
172
|
+
span : float, default 20.0
|
|
173
|
+
EWMA span for the adaptive threshold.
|
|
174
|
+
halflife : float or None, default None
|
|
175
|
+
EWMA halflife (takes precedence over *span*).
|
|
176
|
+
initial_ewa_t : float, default 1.0
|
|
177
|
+
Initial seed for expected run dollar value per bar.
|
|
178
|
+
initial_ewa_proportion : float, default 0.5
|
|
179
|
+
Initial seed for expected buy-run proportion.
|
|
180
|
+
warmup_bars : int, default 0
|
|
181
|
+
Number of initial bars to discard.
|
|
182
|
+
min_run_length : int, default 0
|
|
183
|
+
Minimum ticks per bar to return.
|
|
184
|
+
schema : SchemaMapping, optional
|
|
185
|
+
watermark : str or None, default ``"timestamp"``
|
|
186
|
+
Dedup key for idempotent resume (``"timestamp"``, a column name, or
|
|
187
|
+
``None`` to disable).
|
|
188
|
+
strict_ordering : bool, default False
|
|
189
|
+
If True, raise ``TickDataError`` when a tick arrives with a timestamp
|
|
190
|
+
earlier than the previous tick's (out-of-order input).
|
|
191
|
+
calendar : TradingCalendar, optional
|
|
192
|
+
backend : str, default ``"python"``
|
|
193
|
+
``"python"`` or ``"numba"``.
|
|
194
|
+
stream_id : str, default ``"default"``
|
|
195
|
+
on_bar : callable or None
|
|
196
|
+
on_threshold_update : callable or None
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
pd.DataFrame
|
|
201
|
+
Completed bars as a DataFrame.
|
|
202
|
+
"""
|
|
203
|
+
ctor = RunDollarBarConstructor(
|
|
204
|
+
span=span,
|
|
205
|
+
halflife=halflife,
|
|
206
|
+
initial_ewa_t=initial_ewa_t,
|
|
207
|
+
initial_ewa_proportion=initial_ewa_proportion,
|
|
208
|
+
warmup_bars=warmup_bars,
|
|
209
|
+
warmup_minutes=warmup_minutes,
|
|
210
|
+
min_run_length=min_run_length,
|
|
211
|
+
calendar=calendar,
|
|
212
|
+
schema=schema,
|
|
213
|
+
watermark=watermark,
|
|
214
|
+
strict_ordering=strict_ordering,
|
|
215
|
+
backend=backend,
|
|
216
|
+
stream_id=stream_id,
|
|
217
|
+
on_bar=on_bar,
|
|
218
|
+
on_threshold_update=on_threshold_update,
|
|
219
|
+
multiplier=multiplier,
|
|
220
|
+
volume_type=volume_type,
|
|
221
|
+
)
|
|
222
|
+
return ctor.batch(ticks_df)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
BarRegistry.register("run_dollar", RunDollarBarConstructor, batch_fn=compute_run_dollar_bars)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Run bars (tick) — close when the cumulative same-sign tick-run crosses an adaptive threshold.
|
|
2
|
+
|
|
3
|
+
Uses the two-component EWMA formula from AFML §2.4:
|
|
4
|
+
|
|
5
|
+
.. math::
|
|
6
|
+
|
|
7
|
+
T_n = E[T]_n \\times \\max(E[P^+]_n,\\, 1 - E[P^+]_n)
|
|
8
|
+
|
|
9
|
+
where :math:`E[T]_n` is the expected run length per bar and
|
|
10
|
+
:math:`E[P^+]_n` is the expected buy-run proportion.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Literal
|
|
16
|
+
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from flowbars.bars.accumulators import RunAccumulator
|
|
20
|
+
from flowbars.bars.constructor import BaseBarConstructor
|
|
21
|
+
from flowbars.bars.registry import BarRegistry
|
|
22
|
+
from flowbars.calendars import TradingCalendar
|
|
23
|
+
from flowbars.core import Bar
|
|
24
|
+
from flowbars.schema import SchemaMapping
|
|
25
|
+
from flowbars.thresholds import EWMAThresholdEstimator
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RunTickBarConstructor(BaseBarConstructor):
|
|
29
|
+
"""Bar constructor for tick-run bars.
|
|
30
|
+
|
|
31
|
+
A tick-run bar closes when the cumulative same-sign tick-run total
|
|
32
|
+
reaches or exceeds an **adaptive** threshold estimated by an EWMA.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
span : float, default 20.0
|
|
37
|
+
EWMA span for the two-component threshold formula.
|
|
38
|
+
halflife : float or None, default None
|
|
39
|
+
EWMA halflife. Takes precedence over *span* when given.
|
|
40
|
+
initial_ewa_t : float, default 1.0
|
|
41
|
+
Initial seed for :math:`E[T]` (expected run length per bar).
|
|
42
|
+
initial_ewa_proportion : float, default 0.5
|
|
43
|
+
Initial seed for :math:`E[P^+]` (expected buy-run proportion).
|
|
44
|
+
warmup_bars : int, default 0
|
|
45
|
+
Number of initial bars to discard while the EWMA converges.
|
|
46
|
+
min_run_length : int, default 0
|
|
47
|
+
Minimum number of ticks a run bar must contain to be returned.
|
|
48
|
+
Bars with fewer ticks are discarded (they still update the EWMA).
|
|
49
|
+
Default 0 — no filtering.
|
|
50
|
+
calendar : TradingCalendar, optional
|
|
51
|
+
schema : SchemaMapping, optional
|
|
52
|
+
watermark : str or None, default ``"timestamp"``
|
|
53
|
+
Dedup key for idempotent resume (``"timestamp"``, a column name, or
|
|
54
|
+
``None`` to disable).
|
|
55
|
+
strict_ordering : bool, default False
|
|
56
|
+
If True, raise ``TickDataError`` when a tick arrives with a timestamp
|
|
57
|
+
earlier than the previous tick's (out-of-order input).
|
|
58
|
+
backend : str, default ``"python"``
|
|
59
|
+
``"python"`` or ``"numba"``. The numba backend accelerates
|
|
60
|
+
:meth:`batch` only; ``update`` always uses Python.
|
|
61
|
+
stream_id : str, default ``"default"``
|
|
62
|
+
on_bar : callable or None
|
|
63
|
+
on_threshold_update : callable or None
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
span: float = 20.0,
|
|
69
|
+
halflife: float | None = None,
|
|
70
|
+
initial_ewa_t: float = 1.0,
|
|
71
|
+
initial_ewa_proportion: float = 0.5,
|
|
72
|
+
warmup_bars: int = 0,
|
|
73
|
+
warmup_minutes: int | None = None,
|
|
74
|
+
min_run_length: int = 0,
|
|
75
|
+
calendar: TradingCalendar | None = None,
|
|
76
|
+
schema: SchemaMapping | None = None,
|
|
77
|
+
watermark: str | None = "timestamp",
|
|
78
|
+
stream_id: str = "default",
|
|
79
|
+
strict_ordering: bool = False,
|
|
80
|
+
backend: Literal["python", "numba"] = "python",
|
|
81
|
+
on_bar: Any = None,
|
|
82
|
+
on_threshold_update: Any = None,
|
|
83
|
+
multiplier: float = 1.0,
|
|
84
|
+
volume_type: str = "base",
|
|
85
|
+
) -> None:
|
|
86
|
+
if min_run_length < 0:
|
|
87
|
+
raise ValueError(f"min_run_length must be non-negative, got {min_run_length}")
|
|
88
|
+
self._min_run_length = min_run_length
|
|
89
|
+
|
|
90
|
+
accumulator = RunAccumulator(bar_type="run_tick", metric="tick")
|
|
91
|
+
estimator = EWMAThresholdEstimator(
|
|
92
|
+
bar_family="run",
|
|
93
|
+
span=span,
|
|
94
|
+
halflife=halflife,
|
|
95
|
+
initial_ewa_t=initial_ewa_t,
|
|
96
|
+
initial_ewa_proportion=initial_ewa_proportion,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
super().__init__(
|
|
100
|
+
accumulator=accumulator,
|
|
101
|
+
threshold_estimator=estimator,
|
|
102
|
+
calendar=calendar,
|
|
103
|
+
schema=schema,
|
|
104
|
+
watermark=watermark,
|
|
105
|
+
stream_id=stream_id,
|
|
106
|
+
strict_ordering=strict_ordering,
|
|
107
|
+
warmup_bars=warmup_bars,
|
|
108
|
+
warmup_minutes=warmup_minutes,
|
|
109
|
+
backend=backend,
|
|
110
|
+
on_bar=on_bar,
|
|
111
|
+
on_threshold_update=on_threshold_update,
|
|
112
|
+
multiplier=multiplier,
|
|
113
|
+
volume_type=volume_type,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _should_return(self, bar: Bar) -> bool:
|
|
117
|
+
"""Filter bars below ``min_run_length`` ticks."""
|
|
118
|
+
return bar.num_ticks >= self._min_run_length
|
|
119
|
+
|
|
120
|
+
def _extra_params(self) -> dict[str, Any]:
|
|
121
|
+
return {"min_run_length": self._min_run_length}
|
|
122
|
+
|
|
123
|
+
def _extra_state(self) -> dict[str, Any]:
|
|
124
|
+
return {"min_run_length": self._min_run_length}
|
|
125
|
+
|
|
126
|
+
def _load_extra_state(self, state: dict[str, Any]) -> None:
|
|
127
|
+
self._min_run_length = int(state.get("min_run_length", 0))
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_state( # type: ignore[override]
|
|
131
|
+
cls,
|
|
132
|
+
state: dict[str, Any],
|
|
133
|
+
calendar: TradingCalendar | None = None,
|
|
134
|
+
schema: SchemaMapping | None = None,
|
|
135
|
+
*,
|
|
136
|
+
verify_identity: bool = True,
|
|
137
|
+
) -> RunTickBarConstructor:
|
|
138
|
+
"""Reconstruct a ``RunTickBarConstructor`` from a saved state dict."""
|
|
139
|
+
acc_state = state["accumulator"]
|
|
140
|
+
acc = RunAccumulator(
|
|
141
|
+
bar_type=state["bar_type"],
|
|
142
|
+
metric=acc_state["metric"],
|
|
143
|
+
)
|
|
144
|
+
est = EWMAThresholdEstimator.from_state(state["threshold_estimator"])
|
|
145
|
+
inst: RunTickBarConstructor = super().from_state(
|
|
146
|
+
state, acc, est, calendar, schema, verify_identity=verify_identity
|
|
147
|
+
) # type: ignore[assignment]
|
|
148
|
+
return inst
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def compute_run_tick_bars(
|
|
152
|
+
ticks_df: pd.DataFrame,
|
|
153
|
+
span: float = 20.0,
|
|
154
|
+
halflife: float | None = None,
|
|
155
|
+
initial_ewa_t: float = 1.0,
|
|
156
|
+
initial_ewa_proportion: float = 0.5,
|
|
157
|
+
warmup_bars: int = 0,
|
|
158
|
+
warmup_minutes: int | None = None,
|
|
159
|
+
min_run_length: int = 0,
|
|
160
|
+
schema: SchemaMapping | None = None,
|
|
161
|
+
watermark: str | None = "timestamp",
|
|
162
|
+
strict_ordering: bool = False,
|
|
163
|
+
calendar: TradingCalendar | None = None,
|
|
164
|
+
*,
|
|
165
|
+
backend: Literal["python", "numba"] = "python",
|
|
166
|
+
stream_id: str = "default",
|
|
167
|
+
on_bar: Any = None,
|
|
168
|
+
on_threshold_update: Any = None,
|
|
169
|
+
multiplier: float = 1.0,
|
|
170
|
+
volume_type: str = "base",
|
|
171
|
+
) -> pd.DataFrame:
|
|
172
|
+
"""Build tick-run bars from a DataFrame of ticks.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
ticks_df : pd.DataFrame
|
|
177
|
+
Tick-level data with user-mapped columns.
|
|
178
|
+
span : float, default 20.0
|
|
179
|
+
EWMA span for the adaptive threshold.
|
|
180
|
+
halflife : float or None, default None
|
|
181
|
+
EWMA halflife (takes precedence over *span*).
|
|
182
|
+
initial_ewa_t : float, default 1.0
|
|
183
|
+
Initial seed for expected run length per bar.
|
|
184
|
+
initial_ewa_proportion : float, default 0.5
|
|
185
|
+
Initial seed for expected buy-run proportion.
|
|
186
|
+
warmup_bars : int, default 0
|
|
187
|
+
Number of initial bars to discard.
|
|
188
|
+
min_run_length : int, default 0
|
|
189
|
+
Minimum ticks per bar to return.
|
|
190
|
+
schema : SchemaMapping, optional
|
|
191
|
+
watermark : str or None, default ``"timestamp"``
|
|
192
|
+
Dedup key for idempotent resume (``"timestamp"``, a column name, or
|
|
193
|
+
``None`` to disable).
|
|
194
|
+
strict_ordering : bool, default False
|
|
195
|
+
If True, raise ``TickDataError`` when a tick arrives with a timestamp
|
|
196
|
+
earlier than the previous tick's (out-of-order input).
|
|
197
|
+
calendar : TradingCalendar, optional
|
|
198
|
+
backend : str, default ``"python"``
|
|
199
|
+
``"python"`` or ``"numba"``.
|
|
200
|
+
stream_id : str, default ``"default"``
|
|
201
|
+
on_bar : callable or None
|
|
202
|
+
on_threshold_update : callable or None
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
pd.DataFrame
|
|
207
|
+
Completed bars as a DataFrame.
|
|
208
|
+
"""
|
|
209
|
+
ctor = RunTickBarConstructor(
|
|
210
|
+
span=span,
|
|
211
|
+
halflife=halflife,
|
|
212
|
+
initial_ewa_t=initial_ewa_t,
|
|
213
|
+
initial_ewa_proportion=initial_ewa_proportion,
|
|
214
|
+
warmup_bars=warmup_bars,
|
|
215
|
+
warmup_minutes=warmup_minutes,
|
|
216
|
+
min_run_length=min_run_length,
|
|
217
|
+
calendar=calendar,
|
|
218
|
+
schema=schema,
|
|
219
|
+
watermark=watermark,
|
|
220
|
+
strict_ordering=strict_ordering,
|
|
221
|
+
backend=backend,
|
|
222
|
+
stream_id=stream_id,
|
|
223
|
+
on_bar=on_bar,
|
|
224
|
+
on_threshold_update=on_threshold_update,
|
|
225
|
+
multiplier=multiplier,
|
|
226
|
+
volume_type=volume_type,
|
|
227
|
+
)
|
|
228
|
+
return ctor.batch(ticks_df)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
BarRegistry.register("run_tick", RunTickBarConstructor, batch_fn=compute_run_tick_bars)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Run bars (volume) — close when the cumulative same-sign volume-run crosses an adaptive threshold.
|
|
2
|
+
|
|
3
|
+
Same two-component EWMA formula as tick-run bars, but runs are weighted
|
|
4
|
+
by trade volume instead of tick count.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from flowbars.bars.accumulators import RunAccumulator
|
|
14
|
+
from flowbars.bars.constructor import BaseBarConstructor
|
|
15
|
+
from flowbars.bars.registry import BarRegistry
|
|
16
|
+
from flowbars.calendars import TradingCalendar
|
|
17
|
+
from flowbars.core import Bar
|
|
18
|
+
from flowbars.schema import SchemaMapping
|
|
19
|
+
from flowbars.thresholds import EWMAThresholdEstimator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RunVolumeBarConstructor(BaseBarConstructor):
|
|
23
|
+
"""Bar constructor for volume-run bars.
|
|
24
|
+
|
|
25
|
+
A volume-run bar closes when the cumulative same-sign volume-run total
|
|
26
|
+
reaches or exceeds an **adaptive** threshold estimated by an EWMA.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
span : float, default 20.0
|
|
31
|
+
EWMA span for the two-component threshold formula.
|
|
32
|
+
halflife : float or None, default None
|
|
33
|
+
EWMA halflife. Takes precedence over *span* when given.
|
|
34
|
+
initial_ewa_t : float, default 1.0
|
|
35
|
+
Initial seed for :math:`E[T]` (expected run volume per bar).
|
|
36
|
+
initial_ewa_proportion : float, default 0.5
|
|
37
|
+
Initial seed for :math:`E[P^+]` (expected buy-run proportion).
|
|
38
|
+
warmup_bars : int, default 0
|
|
39
|
+
Number of initial bars to discard while the EWMA converges.
|
|
40
|
+
min_run_length : int, default 0
|
|
41
|
+
Minimum number of ticks a run bar must contain to be returned.
|
|
42
|
+
Bars with fewer ticks are discarded (they still update the EWMA).
|
|
43
|
+
Default 0 — no filtering.
|
|
44
|
+
calendar : TradingCalendar, optional
|
|
45
|
+
schema : SchemaMapping, optional
|
|
46
|
+
watermark : str or None, default ``"timestamp"``
|
|
47
|
+
Dedup key for idempotent resume (``"timestamp"``, a column name, or
|
|
48
|
+
``None`` to disable).
|
|
49
|
+
strict_ordering : bool, default False
|
|
50
|
+
If True, raise ``TickDataError`` when a tick arrives with a timestamp
|
|
51
|
+
earlier than the previous tick's (out-of-order input).
|
|
52
|
+
backend : str, default ``"python"``
|
|
53
|
+
``"python"`` or ``"numba"``. The numba backend accelerates
|
|
54
|
+
:meth:`batch` only; ``update`` always uses Python.
|
|
55
|
+
stream_id : str, default ``"default"``
|
|
56
|
+
on_bar : callable or None
|
|
57
|
+
on_threshold_update : callable or None
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
span: float = 20.0,
|
|
63
|
+
halflife: float | None = None,
|
|
64
|
+
initial_ewa_t: float = 1.0,
|
|
65
|
+
initial_ewa_proportion: float = 0.5,
|
|
66
|
+
warmup_bars: int = 0,
|
|
67
|
+
warmup_minutes: int | None = None,
|
|
68
|
+
min_run_length: int = 0,
|
|
69
|
+
calendar: TradingCalendar | None = None,
|
|
70
|
+
schema: SchemaMapping | None = None,
|
|
71
|
+
watermark: str | None = "timestamp",
|
|
72
|
+
stream_id: str = "default",
|
|
73
|
+
strict_ordering: bool = False,
|
|
74
|
+
backend: Literal["python", "numba"] = "python",
|
|
75
|
+
on_bar: Any = None,
|
|
76
|
+
on_threshold_update: Any = None,
|
|
77
|
+
multiplier: float = 1.0,
|
|
78
|
+
volume_type: str = "base",
|
|
79
|
+
) -> None:
|
|
80
|
+
if min_run_length < 0:
|
|
81
|
+
raise ValueError(f"min_run_length must be non-negative, got {min_run_length}")
|
|
82
|
+
self._min_run_length = min_run_length
|
|
83
|
+
|
|
84
|
+
accumulator = RunAccumulator(bar_type="run_volume", metric="volume")
|
|
85
|
+
estimator = EWMAThresholdEstimator(
|
|
86
|
+
bar_family="run",
|
|
87
|
+
span=span,
|
|
88
|
+
halflife=halflife,
|
|
89
|
+
initial_ewa_t=initial_ewa_t,
|
|
90
|
+
initial_ewa_proportion=initial_ewa_proportion,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
super().__init__(
|
|
94
|
+
accumulator=accumulator,
|
|
95
|
+
threshold_estimator=estimator,
|
|
96
|
+
calendar=calendar,
|
|
97
|
+
schema=schema,
|
|
98
|
+
watermark=watermark,
|
|
99
|
+
stream_id=stream_id,
|
|
100
|
+
strict_ordering=strict_ordering,
|
|
101
|
+
warmup_bars=warmup_bars,
|
|
102
|
+
warmup_minutes=warmup_minutes,
|
|
103
|
+
backend=backend,
|
|
104
|
+
on_bar=on_bar,
|
|
105
|
+
on_threshold_update=on_threshold_update,
|
|
106
|
+
multiplier=multiplier,
|
|
107
|
+
volume_type=volume_type,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _should_return(self, bar: Bar) -> bool:
|
|
111
|
+
"""Filter bars below ``min_run_length`` ticks."""
|
|
112
|
+
return bar.num_ticks >= self._min_run_length
|
|
113
|
+
|
|
114
|
+
def _extra_params(self) -> dict[str, Any]:
|
|
115
|
+
return {"min_run_length": self._min_run_length}
|
|
116
|
+
|
|
117
|
+
def _extra_state(self) -> dict[str, Any]:
|
|
118
|
+
return {"min_run_length": self._min_run_length}
|
|
119
|
+
|
|
120
|
+
def _load_extra_state(self, state: dict[str, Any]) -> None:
|
|
121
|
+
self._min_run_length = int(state.get("min_run_length", 0))
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_state( # type: ignore[override]
|
|
125
|
+
cls,
|
|
126
|
+
state: dict[str, Any],
|
|
127
|
+
calendar: TradingCalendar | None = None,
|
|
128
|
+
schema: SchemaMapping | None = None,
|
|
129
|
+
*,
|
|
130
|
+
verify_identity: bool = True,
|
|
131
|
+
) -> RunVolumeBarConstructor:
|
|
132
|
+
"""Reconstruct a ``RunVolumeBarConstructor`` from a saved state dict."""
|
|
133
|
+
acc_state = state["accumulator"]
|
|
134
|
+
acc = RunAccumulator(
|
|
135
|
+
bar_type=state["bar_type"],
|
|
136
|
+
metric=acc_state["metric"],
|
|
137
|
+
)
|
|
138
|
+
est = EWMAThresholdEstimator.from_state(state["threshold_estimator"])
|
|
139
|
+
inst: RunVolumeBarConstructor = super().from_state(
|
|
140
|
+
state, acc, est, calendar, schema, verify_identity=verify_identity
|
|
141
|
+
) # type: ignore[assignment]
|
|
142
|
+
return inst
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def compute_run_volume_bars(
|
|
146
|
+
ticks_df: pd.DataFrame,
|
|
147
|
+
span: float = 20.0,
|
|
148
|
+
halflife: float | None = None,
|
|
149
|
+
initial_ewa_t: float = 1.0,
|
|
150
|
+
initial_ewa_proportion: float = 0.5,
|
|
151
|
+
warmup_bars: int = 0,
|
|
152
|
+
warmup_minutes: int | None = None,
|
|
153
|
+
min_run_length: int = 0,
|
|
154
|
+
schema: SchemaMapping | None = None,
|
|
155
|
+
watermark: str | None = "timestamp",
|
|
156
|
+
strict_ordering: bool = False,
|
|
157
|
+
calendar: TradingCalendar | None = None,
|
|
158
|
+
*,
|
|
159
|
+
backend: Literal["python", "numba"] = "python",
|
|
160
|
+
stream_id: str = "default",
|
|
161
|
+
on_bar: Any = None,
|
|
162
|
+
on_threshold_update: Any = None,
|
|
163
|
+
multiplier: float = 1.0,
|
|
164
|
+
volume_type: str = "base",
|
|
165
|
+
) -> pd.DataFrame:
|
|
166
|
+
"""Build volume-run bars from a DataFrame of ticks.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
ticks_df : pd.DataFrame
|
|
171
|
+
Tick-level data with user-mapped columns.
|
|
172
|
+
span : float, default 20.0
|
|
173
|
+
EWMA span for the adaptive threshold.
|
|
174
|
+
halflife : float or None, default None
|
|
175
|
+
EWMA halflife (takes precedence over *span*).
|
|
176
|
+
initial_ewa_t : float, default 1.0
|
|
177
|
+
Initial seed for expected run volume per bar.
|
|
178
|
+
initial_ewa_proportion : float, default 0.5
|
|
179
|
+
Initial seed for expected buy-run proportion.
|
|
180
|
+
warmup_bars : int, default 0
|
|
181
|
+
Number of initial bars to discard.
|
|
182
|
+
min_run_length : int, default 0
|
|
183
|
+
Minimum ticks per bar to return.
|
|
184
|
+
schema : SchemaMapping, optional
|
|
185
|
+
watermark : str or None, default ``"timestamp"``
|
|
186
|
+
Dedup key for idempotent resume (``"timestamp"``, a column name, or
|
|
187
|
+
``None`` to disable).
|
|
188
|
+
strict_ordering : bool, default False
|
|
189
|
+
If True, raise ``TickDataError`` when a tick arrives with a timestamp
|
|
190
|
+
earlier than the previous tick's (out-of-order input).
|
|
191
|
+
calendar : TradingCalendar, optional
|
|
192
|
+
backend : str, default ``"python"``
|
|
193
|
+
``"python"`` or ``"numba"``.
|
|
194
|
+
stream_id : str, default ``"default"``
|
|
195
|
+
on_bar : callable or None
|
|
196
|
+
on_threshold_update : callable or None
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
pd.DataFrame
|
|
201
|
+
Completed bars as a DataFrame.
|
|
202
|
+
"""
|
|
203
|
+
ctor = RunVolumeBarConstructor(
|
|
204
|
+
span=span,
|
|
205
|
+
halflife=halflife,
|
|
206
|
+
initial_ewa_t=initial_ewa_t,
|
|
207
|
+
initial_ewa_proportion=initial_ewa_proportion,
|
|
208
|
+
warmup_bars=warmup_bars,
|
|
209
|
+
warmup_minutes=warmup_minutes,
|
|
210
|
+
min_run_length=min_run_length,
|
|
211
|
+
calendar=calendar,
|
|
212
|
+
schema=schema,
|
|
213
|
+
watermark=watermark,
|
|
214
|
+
strict_ordering=strict_ordering,
|
|
215
|
+
backend=backend,
|
|
216
|
+
stream_id=stream_id,
|
|
217
|
+
on_bar=on_bar,
|
|
218
|
+
on_threshold_update=on_threshold_update,
|
|
219
|
+
multiplier=multiplier,
|
|
220
|
+
volume_type=volume_type,
|
|
221
|
+
)
|
|
222
|
+
return ctor.batch(ticks_df)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
BarRegistry.register("run_volume", RunVolumeBarConstructor, batch_fn=compute_run_volume_bars)
|