topstep-backtest 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.
- topstep_backtest/__init__.py +43 -0
- topstep_backtest/clock/__init__.py +1 -0
- topstep_backtest/clock/live_clock.py +82 -0
- topstep_backtest/clock/test_clock.py +133 -0
- topstep_backtest/core/__init__.py +1 -0
- topstep_backtest/core/ids.py +23 -0
- topstep_backtest/core/instruments.py +167 -0
- topstep_backtest/core/money.py +160 -0
- topstep_backtest/core/time.py +125 -0
- topstep_backtest/data/__init__.py +1 -0
- topstep_backtest/data/clean.py +86 -0
- topstep_backtest/data/feed.py +56 -0
- topstep_backtest/data/synthetic.py +137 -0
- topstep_backtest/data/validator.py +215 -0
- topstep_backtest/data/wrangler.py +306 -0
- topstep_backtest/engine/__init__.py +1 -0
- topstep_backtest/engine/backtest.py +209 -0
- topstep_backtest/execution/__init__.py +1 -0
- topstep_backtest/execution/rejections.py +53 -0
- topstep_backtest/execution/sim_broker.py +1436 -0
- topstep_backtest/fills/__init__.py +1 -0
- topstep_backtest/fills/bar_fill.py +268 -0
- topstep_backtest/fills/fees.py +120 -0
- topstep_backtest/fills/path.py +59 -0
- topstep_backtest/harness.py +446 -0
- topstep_backtest/indicators/__init__.py +46 -0
- topstep_backtest/indicators/base.py +57 -0
- topstep_backtest/indicators/library.py +303 -0
- topstep_backtest/indicators/talib_adapter.py +657 -0
- topstep_backtest/metrics/__init__.py +5 -0
- topstep_backtest/metrics/stats.py +153 -0
- topstep_backtest/protocols.py +473 -0
- topstep_backtest/py.typed +0 -0
- topstep_backtest/rules/__init__.py +1 -0
- topstep_backtest/rules/kernel.py +281 -0
- topstep_backtest/rules/params.py +74 -0
- topstep_backtest/strategy/__init__.py +20 -0
- topstep_backtest/strategy/base.py +118 -0
- topstep_backtest/strategy/symbol.py +344 -0
- topstep_backtest/strategy/tracker.py +151 -0
- topstep_backtest-0.1.0.dist-info/METADATA +250 -0
- topstep_backtest-0.1.0.dist-info/RECORD +44 -0
- topstep_backtest-0.1.0.dist-info/WHEEL +4 -0
- topstep_backtest-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Named indicators — typed spellings of the TA-Lib functions strategies reach
|
|
2
|
+
for most, plus ``Cross``.
|
|
3
|
+
|
|
4
|
+
Each class is a thin ``TalibIndicator`` subclass: it fixes the TA-Lib function
|
|
5
|
+
and gives the parameters real names and types, so ``Ema(20)`` is autocompletable
|
|
6
|
+
and pyright-checked while still being exactly ``TalibIndicator("EMA",
|
|
7
|
+
timeperiod=20)``. Anything not named here is one line away —
|
|
8
|
+
``TalibIndicator("CDLENGULFING")``, ``TalibIndicator("ULTOSC")`` — 152 of
|
|
9
|
+
TA-Lib's 161 functions are reachable through the generic form (the other nine
|
|
10
|
+
are refused at construction; see ``talib_adapter``).
|
|
11
|
+
|
|
12
|
+
``lookback`` is TA-Lib's, so the warmup a strategy declares comes from the same
|
|
13
|
+
authority that computes the values: ``Sma(3)`` and ``Ema(3)`` need 3 bars,
|
|
14
|
+
``Rsi(3)`` and ``Atr(3)`` need 4 (their first delta/true-range consumes a bar).
|
|
15
|
+
``lookback`` is where a value EXISTS; ``history_bars`` (``warm``) is where it
|
|
16
|
+
stops depending on where the run started, which is where sim/live parity begins
|
|
17
|
+
— see ``talib_adapter``'s module docstring.
|
|
18
|
+
|
|
19
|
+
``Cross`` is not a TA-Lib function — it is a comparator over two value series
|
|
20
|
+
and stays pure Python.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from .talib_adapter import TalibIndicator
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from ..protocols import Bar
|
|
31
|
+
from .base import ValueSource
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Adx",
|
|
35
|
+
"Atr",
|
|
36
|
+
"BBands",
|
|
37
|
+
"Cross",
|
|
38
|
+
"Ema",
|
|
39
|
+
"Highest",
|
|
40
|
+
"Lowest",
|
|
41
|
+
"Macd",
|
|
42
|
+
"Obv",
|
|
43
|
+
"Rsi",
|
|
44
|
+
"Sma",
|
|
45
|
+
"StdDev",
|
|
46
|
+
"Stoch",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _check_period(period: int) -> None:
|
|
51
|
+
if period < 1:
|
|
52
|
+
raise ValueError(f"period must be >= 1, got {period}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --------------------------------------------------------------------------- #
|
|
56
|
+
# Single-output
|
|
57
|
+
# --------------------------------------------------------------------------- #
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Sma(TalibIndicator):
|
|
61
|
+
"""Simple moving average of closes (TA-Lib ``SMA``)."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, period: int, *, history: int | None = None) -> None:
|
|
64
|
+
_check_period(period)
|
|
65
|
+
super().__init__("SMA", history=history, timeperiod=period)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Ema(TalibIndicator):
|
|
69
|
+
"""Exponential moving average of closes (TA-Lib ``EMA``).
|
|
70
|
+
|
|
71
|
+
Seeded with the SMA of the first ``period`` closes, then ``k = 2/(period+1)``
|
|
72
|
+
recursion — TA-Lib's convention.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, period: int, *, history: int | None = None) -> None:
|
|
76
|
+
_check_period(period)
|
|
77
|
+
super().__init__("EMA", history=history, timeperiod=period)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Rsi(TalibIndicator):
|
|
81
|
+
"""Wilder's relative strength index (TA-Lib ``RSI``); ``lookback == period + 1``."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, period: int, *, history: int | None = None) -> None:
|
|
84
|
+
_check_period(period)
|
|
85
|
+
super().__init__("RSI", history=history, timeperiod=period)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Atr(TalibIndicator):
|
|
89
|
+
"""Wilder's average true range (TA-Lib ``ATR``); ``lookback == period + 1``."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, period: int, *, history: int | None = None) -> None:
|
|
92
|
+
_check_period(period)
|
|
93
|
+
super().__init__("ATR", history=history, timeperiod=period)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class StdDev(TalibIndicator):
|
|
97
|
+
"""POPULATION standard deviation of closes (TA-Lib ``STDDEV``, ``nbdev=1``).
|
|
98
|
+
|
|
99
|
+
TA-Lib evaluates the NAIVE ``E[x^2] - E[x]^2`` form, which cancels badly
|
|
100
|
+
once the mean is large relative to the spread — at futures price levels the
|
|
101
|
+
relative error reaches ~2e-9 at 5,000 and ~7e-7 at 100,000. Harmless for a
|
|
102
|
+
threshold read; do not treat a value derived from it as exact.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, period: int, *, history: int | None = None) -> None:
|
|
106
|
+
_check_period(period)
|
|
107
|
+
super().__init__("STDDEV", history=history, timeperiod=period, nbdev=1.0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Highest(TalibIndicator):
|
|
111
|
+
"""Rolling maximum of ``bar.high`` (TA-Lib ``MAX`` redirected onto highs)."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, period: int, *, history: int | None = None) -> None:
|
|
114
|
+
_check_period(period)
|
|
115
|
+
super().__init__("MAX", price="high", history=history, timeperiod=period)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Lowest(TalibIndicator):
|
|
119
|
+
"""Rolling minimum of ``bar.low`` (TA-Lib ``MIN`` redirected onto lows)."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, period: int, *, history: int | None = None) -> None:
|
|
122
|
+
_check_period(period)
|
|
123
|
+
super().__init__("MIN", price="low", history=history, timeperiod=period)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Adx(TalibIndicator):
|
|
127
|
+
"""Average directional index (TA-Lib ``ADX``)."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, period: int = 14, *, history: int | None = None) -> None:
|
|
130
|
+
_check_period(period)
|
|
131
|
+
super().__init__("ADX", history=history, timeperiod=period)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Obv(TalibIndicator):
|
|
135
|
+
"""On-balance volume (TA-Lib ``OBV``).
|
|
136
|
+
|
|
137
|
+
OBV accumulates without decay, so — unlike every other indicator here — its
|
|
138
|
+
LEVEL depends on where accumulation started and is therefore windowed to
|
|
139
|
+
``history_bars`` like everything else. Use its slope or divergence, never
|
|
140
|
+
the absolute level; an inception-anchored level could not be reproduced
|
|
141
|
+
live in any case.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, *, history: int | None = None) -> None:
|
|
145
|
+
super().__init__("OBV", history=history)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# --------------------------------------------------------------------------- #
|
|
149
|
+
# Multi-output — read individual lines with .get(...) / .line(...)
|
|
150
|
+
# --------------------------------------------------------------------------- #
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Macd(TalibIndicator):
|
|
154
|
+
"""MACD (TA-Lib ``MACD``): outputs ``macd``, ``macdsignal``, ``macdhist``.
|
|
155
|
+
|
|
156
|
+
``value`` is the MACD line; cross the lines with
|
|
157
|
+
``Cross(macd.line("macd"), macd.line("macdsignal"))``.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
fast: int = 12,
|
|
163
|
+
slow: int = 26,
|
|
164
|
+
signal: int = 9,
|
|
165
|
+
*,
|
|
166
|
+
history: int | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
for period in (fast, slow, signal):
|
|
169
|
+
_check_period(period)
|
|
170
|
+
super().__init__(
|
|
171
|
+
"MACD", history=history, fastperiod=fast, slowperiod=slow, signalperiod=signal
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class BBands(TalibIndicator):
|
|
176
|
+
"""Bollinger bands (TA-Lib ``BBANDS``): ``upperband``/``middleband``/``lowerband``.
|
|
177
|
+
|
|
178
|
+
The bands inherit ``StdDev``'s naive-variance error (~7e-7 relative at
|
|
179
|
+
100,000), so a band EDGE is not an exact number: a ``Cross`` against one can
|
|
180
|
+
flip on that noise, and a "touch" of a band is never exact. Use it as a
|
|
181
|
+
threshold, not as a price.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
period: int = 20,
|
|
187
|
+
*,
|
|
188
|
+
deviations: float = 2.0,
|
|
189
|
+
history: int | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
_check_period(period)
|
|
192
|
+
super().__init__(
|
|
193
|
+
"BBANDS",
|
|
194
|
+
history=history,
|
|
195
|
+
timeperiod=period,
|
|
196
|
+
nbdevup=deviations,
|
|
197
|
+
nbdevdn=deviations,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Stoch(TalibIndicator):
|
|
202
|
+
"""Slow stochastic (TA-Lib ``STOCH``): outputs ``slowk`` and ``slowd``."""
|
|
203
|
+
|
|
204
|
+
def __init__(
|
|
205
|
+
self,
|
|
206
|
+
fastk: int = 5,
|
|
207
|
+
slowk: int = 3,
|
|
208
|
+
slowd: int = 3,
|
|
209
|
+
*,
|
|
210
|
+
history: int | None = None,
|
|
211
|
+
) -> None:
|
|
212
|
+
for period in (fastk, slowk, slowd):
|
|
213
|
+
_check_period(period)
|
|
214
|
+
super().__init__(
|
|
215
|
+
"STOCH",
|
|
216
|
+
history=history,
|
|
217
|
+
fastk_period=fastk,
|
|
218
|
+
slowk_period=slowk,
|
|
219
|
+
slowd_period=slowd,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# --------------------------------------------------------------------------- #
|
|
224
|
+
# Cross
|
|
225
|
+
# --------------------------------------------------------------------------- #
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Cross:
|
|
229
|
+
"""Crossover of two value series: ``up``/``down`` fire only on the crossing bar.
|
|
230
|
+
|
|
231
|
+
Tracks the sign of ``a.value - b.value`` per update: ``up`` when the sign
|
|
232
|
+
goes from <= 0 to > 0, ``down`` when it goes from >= 0 to < 0 — an exact
|
|
233
|
+
``a == b`` touch therefore fires on the bar that resolves it, not on the
|
|
234
|
+
touch itself. Both are ``False`` until the detector holds a previous AND
|
|
235
|
+
a current comparison with both inputs ready, so
|
|
236
|
+
``lookback = max(a.lookback, b.lookback) + 1``.
|
|
237
|
+
|
|
238
|
+
Note that TA-Lib values are float64, so an exact ``a == b`` touch is
|
|
239
|
+
vanishingly rare in practice — the zero-sign branch is a correctness
|
|
240
|
+
guarantee, not a common path.
|
|
241
|
+
|
|
242
|
+
``update(bar)`` ignores the bar and reads ``a.value`` / ``b.value`` — it
|
|
243
|
+
REQUIRES both inputs to have been updated for the same bar first. Under
|
|
244
|
+
``SymbolStrategy``'s registration-order update rule that means ``use()``
|
|
245
|
+
the inputs BEFORE the ``Cross`` that reads them (``use()`` enforces it via
|
|
246
|
+
``inputs``, resolving a ``TalibLine`` to the indicator that owns it).
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
def __init__(self, a: ValueSource, b: ValueSource) -> None:
|
|
250
|
+
self._a = a
|
|
251
|
+
self._b = b
|
|
252
|
+
self._prev_sign: int | None = None
|
|
253
|
+
self._curr_sign: int | None = None
|
|
254
|
+
self._updates = 0
|
|
255
|
+
self._registered = False
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def inputs(self) -> tuple[ValueSource, ValueSource]:
|
|
259
|
+
"""The ``(a, b)`` sources read on update, for registration-order checks."""
|
|
260
|
+
return (self._a, self._b)
|
|
261
|
+
|
|
262
|
+
def mark_registered(self) -> None:
|
|
263
|
+
"""Called by ``use()``. A Cross nobody registered never updates, so its
|
|
264
|
+
``up``/``down`` would read False forever and the strategy would take zero
|
|
265
|
+
trades in silence — ``up``/``down`` raise instead once that is provable."""
|
|
266
|
+
self._registered = True
|
|
267
|
+
|
|
268
|
+
def _check_driven(self) -> None:
|
|
269
|
+
if self._registered or self._updates:
|
|
270
|
+
return
|
|
271
|
+
if self._a.ready and self._b.ready:
|
|
272
|
+
raise RuntimeError(
|
|
273
|
+
"this Cross has never been updated although both inputs are "
|
|
274
|
+
"ready — it was never use()-registered, so it can never fire"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def lookback(self) -> int:
|
|
279
|
+
return max(self._a.lookback, self._b.lookback) + 1
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def ready(self) -> bool:
|
|
283
|
+
return self._prev_sign is not None
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def up(self) -> bool:
|
|
287
|
+
self._check_driven()
|
|
288
|
+
prev, curr = self._prev_sign, self._curr_sign
|
|
289
|
+
return prev is not None and curr is not None and prev <= 0 and curr > 0
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def down(self) -> bool:
|
|
293
|
+
self._check_driven()
|
|
294
|
+
prev, curr = self._prev_sign, self._curr_sign
|
|
295
|
+
return prev is not None and curr is not None and prev >= 0 and curr < 0
|
|
296
|
+
|
|
297
|
+
def update(self, bar: Bar) -> None:
|
|
298
|
+
self._updates += 1
|
|
299
|
+
if not (self._a.ready and self._b.ready):
|
|
300
|
+
return
|
|
301
|
+
diff = self._a.value - self._b.value
|
|
302
|
+
self._prev_sign = self._curr_sign
|
|
303
|
+
self._curr_sign = 1 if diff > 0 else -1 if diff < 0 else 0
|