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.
Files changed (44) hide show
  1. topstep_backtest/__init__.py +43 -0
  2. topstep_backtest/clock/__init__.py +1 -0
  3. topstep_backtest/clock/live_clock.py +82 -0
  4. topstep_backtest/clock/test_clock.py +133 -0
  5. topstep_backtest/core/__init__.py +1 -0
  6. topstep_backtest/core/ids.py +23 -0
  7. topstep_backtest/core/instruments.py +167 -0
  8. topstep_backtest/core/money.py +160 -0
  9. topstep_backtest/core/time.py +125 -0
  10. topstep_backtest/data/__init__.py +1 -0
  11. topstep_backtest/data/clean.py +86 -0
  12. topstep_backtest/data/feed.py +56 -0
  13. topstep_backtest/data/synthetic.py +137 -0
  14. topstep_backtest/data/validator.py +215 -0
  15. topstep_backtest/data/wrangler.py +306 -0
  16. topstep_backtest/engine/__init__.py +1 -0
  17. topstep_backtest/engine/backtest.py +209 -0
  18. topstep_backtest/execution/__init__.py +1 -0
  19. topstep_backtest/execution/rejections.py +53 -0
  20. topstep_backtest/execution/sim_broker.py +1436 -0
  21. topstep_backtest/fills/__init__.py +1 -0
  22. topstep_backtest/fills/bar_fill.py +268 -0
  23. topstep_backtest/fills/fees.py +120 -0
  24. topstep_backtest/fills/path.py +59 -0
  25. topstep_backtest/harness.py +446 -0
  26. topstep_backtest/indicators/__init__.py +46 -0
  27. topstep_backtest/indicators/base.py +57 -0
  28. topstep_backtest/indicators/library.py +303 -0
  29. topstep_backtest/indicators/talib_adapter.py +657 -0
  30. topstep_backtest/metrics/__init__.py +5 -0
  31. topstep_backtest/metrics/stats.py +153 -0
  32. topstep_backtest/protocols.py +473 -0
  33. topstep_backtest/py.typed +0 -0
  34. topstep_backtest/rules/__init__.py +1 -0
  35. topstep_backtest/rules/kernel.py +281 -0
  36. topstep_backtest/rules/params.py +74 -0
  37. topstep_backtest/strategy/__init__.py +20 -0
  38. topstep_backtest/strategy/base.py +118 -0
  39. topstep_backtest/strategy/symbol.py +344 -0
  40. topstep_backtest/strategy/tracker.py +151 -0
  41. topstep_backtest-0.1.0.dist-info/METADATA +250 -0
  42. topstep_backtest-0.1.0.dist-info/RECORD +44 -0
  43. topstep_backtest-0.1.0.dist-info/WHEEL +4 -0
  44. 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