topstep-backtest 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.
- topstep_backtest-0.1.0/.gitignore +43 -0
- topstep_backtest-0.1.0/AGENTS.md +216 -0
- topstep_backtest-0.1.0/CHANGELOG.md +49 -0
- topstep_backtest-0.1.0/LICENSE +21 -0
- topstep_backtest-0.1.0/PKG-INFO +250 -0
- topstep_backtest-0.1.0/README.md +215 -0
- topstep_backtest-0.1.0/docs/DESIGN.md +806 -0
- topstep_backtest-0.1.0/docs/GUIDE.md +631 -0
- topstep_backtest-0.1.0/docs/ROADMAP.md +43 -0
- topstep_backtest-0.1.0/docs/STRATEGY_API.md +615 -0
- topstep_backtest-0.1.0/docs/TUTORIAL_EMA_CROSSOVER.md +1096 -0
- topstep_backtest-0.1.0/docs/topstep-rules.md +230 -0
- topstep_backtest-0.1.0/examples/ema_cross.py +118 -0
- topstep_backtest-0.1.0/examples/hand_wired.py +104 -0
- topstep_backtest-0.1.0/examples/run_combine.py +46 -0
- topstep_backtest-0.1.0/examples/run_real_data.py +226 -0
- topstep_backtest-0.1.0/examples/sma_cross.py +121 -0
- topstep_backtest-0.1.0/examples/talib_macd.py +165 -0
- topstep_backtest-0.1.0/pyproject.toml +112 -0
- topstep_backtest-0.1.0/src/topstep_backtest/__init__.py +43 -0
- topstep_backtest-0.1.0/src/topstep_backtest/clock/__init__.py +1 -0
- topstep_backtest-0.1.0/src/topstep_backtest/clock/live_clock.py +82 -0
- topstep_backtest-0.1.0/src/topstep_backtest/clock/test_clock.py +133 -0
- topstep_backtest-0.1.0/src/topstep_backtest/core/__init__.py +1 -0
- topstep_backtest-0.1.0/src/topstep_backtest/core/ids.py +23 -0
- topstep_backtest-0.1.0/src/topstep_backtest/core/instruments.py +167 -0
- topstep_backtest-0.1.0/src/topstep_backtest/core/money.py +160 -0
- topstep_backtest-0.1.0/src/topstep_backtest/core/time.py +125 -0
- topstep_backtest-0.1.0/src/topstep_backtest/data/__init__.py +1 -0
- topstep_backtest-0.1.0/src/topstep_backtest/data/clean.py +86 -0
- topstep_backtest-0.1.0/src/topstep_backtest/data/feed.py +56 -0
- topstep_backtest-0.1.0/src/topstep_backtest/data/synthetic.py +137 -0
- topstep_backtest-0.1.0/src/topstep_backtest/data/validator.py +215 -0
- topstep_backtest-0.1.0/src/topstep_backtest/data/wrangler.py +306 -0
- topstep_backtest-0.1.0/src/topstep_backtest/engine/__init__.py +1 -0
- topstep_backtest-0.1.0/src/topstep_backtest/engine/backtest.py +209 -0
- topstep_backtest-0.1.0/src/topstep_backtest/execution/__init__.py +1 -0
- topstep_backtest-0.1.0/src/topstep_backtest/execution/rejections.py +53 -0
- topstep_backtest-0.1.0/src/topstep_backtest/execution/sim_broker.py +1436 -0
- topstep_backtest-0.1.0/src/topstep_backtest/fills/__init__.py +1 -0
- topstep_backtest-0.1.0/src/topstep_backtest/fills/bar_fill.py +268 -0
- topstep_backtest-0.1.0/src/topstep_backtest/fills/fees.py +120 -0
- topstep_backtest-0.1.0/src/topstep_backtest/fills/path.py +59 -0
- topstep_backtest-0.1.0/src/topstep_backtest/harness.py +446 -0
- topstep_backtest-0.1.0/src/topstep_backtest/indicators/__init__.py +46 -0
- topstep_backtest-0.1.0/src/topstep_backtest/indicators/base.py +57 -0
- topstep_backtest-0.1.0/src/topstep_backtest/indicators/library.py +303 -0
- topstep_backtest-0.1.0/src/topstep_backtest/indicators/talib_adapter.py +657 -0
- topstep_backtest-0.1.0/src/topstep_backtest/metrics/__init__.py +5 -0
- topstep_backtest-0.1.0/src/topstep_backtest/metrics/stats.py +153 -0
- topstep_backtest-0.1.0/src/topstep_backtest/protocols.py +473 -0
- topstep_backtest-0.1.0/src/topstep_backtest/py.typed +0 -0
- topstep_backtest-0.1.0/src/topstep_backtest/rules/__init__.py +1 -0
- topstep_backtest-0.1.0/src/topstep_backtest/rules/kernel.py +281 -0
- topstep_backtest-0.1.0/src/topstep_backtest/rules/params.py +74 -0
- topstep_backtest-0.1.0/src/topstep_backtest/strategy/__init__.py +20 -0
- topstep_backtest-0.1.0/src/topstep_backtest/strategy/base.py +118 -0
- topstep_backtest-0.1.0/src/topstep_backtest/strategy/symbol.py +344 -0
- topstep_backtest-0.1.0/src/topstep_backtest/strategy/tracker.py +151 -0
- topstep_backtest-0.1.0/tests/__init__.py +0 -0
- topstep_backtest-0.1.0/tests/conftest.py +59 -0
- topstep_backtest-0.1.0/tests/golden/__init__.py +0 -0
- topstep_backtest-0.1.0/tests/golden/artifacts/verdict_failed_mll_s50k.json +1 -0
- topstep_backtest-0.1.0/tests/golden/artifacts/verdict_passed_s50k.json +1 -0
- topstep_backtest-0.1.0/tests/golden/test_combine_kernel.py +346 -0
- topstep_backtest-0.1.0/tests/golden/test_facade_equivalence.py +167 -0
- topstep_backtest-0.1.0/tests/golden/test_sugar_equivalence.py +266 -0
- topstep_backtest-0.1.0/tests/golden/test_verdict_goldens.py +303 -0
- topstep_backtest-0.1.0/tests/parity/__init__.py +0 -0
- topstep_backtest-0.1.0/tests/parity/test_broker_conformance.py +74 -0
- topstep_backtest-0.1.0/tests/property/__init__.py +0 -0
- topstep_backtest-0.1.0/tests/property/test_indicator_props.py +842 -0
- topstep_backtest-0.1.0/tests/property/test_kernel_props.py +174 -0
- topstep_backtest-0.1.0/tests/property/test_money_props.py +246 -0
- topstep_backtest-0.1.0/tests/unit/__init__.py +0 -0
- topstep_backtest-0.1.0/tests/unit/test_bar_fill.py +638 -0
- topstep_backtest-0.1.0/tests/unit/test_clean.py +68 -0
- topstep_backtest-0.1.0/tests/unit/test_clock.py +351 -0
- topstep_backtest-0.1.0/tests/unit/test_data_feed.py +85 -0
- topstep_backtest-0.1.0/tests/unit/test_engine.py +211 -0
- topstep_backtest-0.1.0/tests/unit/test_fees.py +162 -0
- topstep_backtest-0.1.0/tests/unit/test_harness.py +360 -0
- topstep_backtest-0.1.0/tests/unit/test_indicators.py +1075 -0
- topstep_backtest-0.1.0/tests/unit/test_instruments.py +195 -0
- topstep_backtest-0.1.0/tests/unit/test_path.py +135 -0
- topstep_backtest-0.1.0/tests/unit/test_sim_broker.py +258 -0
- topstep_backtest-0.1.0/tests/unit/test_stats.py +212 -0
- topstep_backtest-0.1.0/tests/unit/test_symbol_strategy.py +750 -0
- topstep_backtest-0.1.0/tests/unit/test_synthetic.py +151 -0
- topstep_backtest-0.1.0/tests/unit/test_talib_adapter_hardening.py +353 -0
- topstep_backtest-0.1.0/tests/unit/test_time.py +264 -0
- topstep_backtest-0.1.0/tests/unit/test_tracker.py +310 -0
- topstep_backtest-0.1.0/tests/unit/test_validator.py +196 -0
- topstep_backtest-0.1.0/tests/unit/test_wrangler.py +376 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
|
|
15
|
+
# uv
|
|
16
|
+
.uv/
|
|
17
|
+
uv.lock
|
|
18
|
+
|
|
19
|
+
# Testing / coverage
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
coverage.xml
|
|
23
|
+
htmlcov/
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
.mypy_cache/
|
|
26
|
+
.pyright/
|
|
27
|
+
|
|
28
|
+
# Docs
|
|
29
|
+
site/
|
|
30
|
+
|
|
31
|
+
# IDE
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/
|
|
34
|
+
*.swp
|
|
35
|
+
.DS_Store
|
|
36
|
+
|
|
37
|
+
# Secrets / local config
|
|
38
|
+
.env
|
|
39
|
+
.env.*
|
|
40
|
+
!.env.example
|
|
41
|
+
|
|
42
|
+
# Hypothesis
|
|
43
|
+
.hypothesis/
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# topstep-backtest — agent guide
|
|
2
|
+
|
|
3
|
+
Dense reference for working on this codebase. Read this before touching code.
|
|
4
|
+
Companion docs: `docs/DESIGN.md` (architecture contract), `docs/topstep-rules.md`
|
|
5
|
+
(rulebook with sources + verify-checklist), `docs/ROADMAP.md` (phase gates).
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
An event-driven backtester answering ONE question: *can a strategy profitably pass
|
|
10
|
+
the Topstep Trading Combine?* Two load-bearing properties:
|
|
11
|
+
|
|
12
|
+
1. **Backtest/live parity** with the sibling `topstep-sdk` (`../topstep-sdk`, an
|
|
13
|
+
editable uv path dep): strategies are written once against structural protocols
|
|
14
|
+
that BOTH `SimBroker` and `AsyncTopstepClient` satisfy. The SDK's msgspec models
|
|
15
|
+
(`OrderModel`, `HalfTradeModel`, `PositionModel`, enums, `APIError`) are imported
|
|
16
|
+
verbatim — never redefined.
|
|
17
|
+
2. **The prop-firm rule engine is real-time**, not a post-hoc scorecard: the
|
|
18
|
+
two-state trailing MLL, optional DLL, consistency target, position caps, and the
|
|
19
|
+
16:10 ET flatten all mutate the trade sequence (forced liquidation with adverse
|
|
20
|
+
slippage + Topstep's $10/contract auto-liquidation fee).
|
|
21
|
+
|
|
22
|
+
**Scope:** Combine only. Funded/XFA is parked (docs/topstep-rules.md §6). Tier-0
|
|
23
|
+
(OHLCV bars) fills only, so far.
|
|
24
|
+
|
|
25
|
+
## Binding conventions (violations are bugs)
|
|
26
|
+
|
|
27
|
+
- **Money:** `Decimal` on the tick grid, always. All grid math via `core/money.py`.
|
|
28
|
+
Position accounting is FIFO lots with an exact cost basis — unrealized P&L goes
|
|
29
|
+
through `position_unrealized` (division-free); never average-then-divide.
|
|
30
|
+
*Indicator values are the deliberate exception:* TA-Lib computes in float64 and the
|
|
31
|
+
`Decimal` handed back at the boundary is **not** tick-snapped — an indicator level is
|
|
32
|
+
not a tradeable price, and quantizing it to the grid would be a lie. Never route an
|
|
33
|
+
indicator value into grid math without an explicit `round_to_tick`.
|
|
34
|
+
- **Time:** int-ns UTC in the hot path; canonical timezone **ET** at boundaries
|
|
35
|
+
(flatten 16:10, session close 17:00, day reset 18:00 ET). All time flows through
|
|
36
|
+
the `Clock` protocol — never `datetime.now()`. `dt_to_ns`/`ns_to_dt` are exact
|
|
37
|
+
integer math (no float round-trips).
|
|
38
|
+
- **No look-ahead, structurally:** bars stamp `ts_init` at CLOSE; an order
|
|
39
|
+
participates in a bar only if `accepted_ts <= bar.ts_event` (its OPEN); trailing
|
|
40
|
+
stops ratchet only from bars the order lived through; `SimHistoryApi` serves only
|
|
41
|
+
already-seen bars; the engine asserts feed time-order.
|
|
42
|
+
- **Intrabar determinism:** fills and rule breaches are ordered along ONE
|
|
43
|
+
pessimistic price path (`fills/path.py`; adverse extreme first) by TRIGGER level
|
|
44
|
+
(never slippage-adjusted prices); breach ties beat fills; equity is re-checked AT
|
|
45
|
+
each fill after it applies.
|
|
46
|
+
- **Rejections:** always the SDK's `APIError` with the gateway's numeric
|
|
47
|
+
`error_code` (see `execution/rejections.py` + SDK `PLACE_ORDER_ERRORS` et al.).
|
|
48
|
+
- `protocols.py` is THE canonical interface module. Change signatures there first
|
|
49
|
+
or not at all; `tests/parity/` has a pyright-strict conformance test plus a
|
|
50
|
+
signature-diff of `SimOrderApi.place` against the SDK's `OrderResource.place`
|
|
51
|
+
that catches drift. **There is no CI** — drift surfaces only when someone runs
|
|
52
|
+
`uv run pytest`. Both tests prove STRUCTURAL conformance; the BEHAVIOURAL gate
|
|
53
|
+
(an intent-sequence test asserting an identical Submit/Modify/Cancel sequence
|
|
54
|
+
sim vs live, docs/ROADMAP.md Phase 4) is not built, so behavioural parity is
|
|
55
|
+
argued, not proven.
|
|
56
|
+
- Toolchain: `uv`, ruff (line 100, E/F/I/UP/B/SIM/ASYNC/RUF), pyright **strict**,
|
|
57
|
+
pytest (+ hypothesis, asyncio_mode=auto). Everything must stay green:
|
|
58
|
+
`uv run pytest && uv run ruff check . && uv run pyright`. Runtime deps are
|
|
59
|
+
`topstep-sdk`, `msgspec`, and `ta-lib` — the last is core, not an extra.
|
|
60
|
+
|
|
61
|
+
## Map
|
|
62
|
+
|
|
63
|
+
| Module | Owns |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `protocols.py` | Clock/Bar/DataFeed/Broker/OrderApi/PositionApi/HistoryApi/FillModel/Fill/WorkingOrder |
|
|
66
|
+
| `core/` | money (grid/P&L), instruments (15-product spec table), time (ET sessions), ids |
|
|
67
|
+
| `rules/` | `CombineKernel` — THE canonical rulebook state machine (golden-fixtured); params per size |
|
|
68
|
+
| `clock/` | deterministic `TestClock` (`__test__=False`!), wall-clock `LiveClock` |
|
|
69
|
+
| `fills/` | pessimistic Tier-0 `BarFillModel`, shared `build_path`, `TopstepFees` (per-side stack) |
|
|
70
|
+
| `data/` | wrangler (explicit `stamp="open"|"close"` — never guess), validator, synthetic generator, `ListBarFeed` |
|
|
71
|
+
| `execution/` | `SimBroker` (lifecycle, OCO brackets, netting, breach walk, forced liq), rejections |
|
|
72
|
+
| `engine/` | `BacktestEngine` loop + `BacktestResult` (msgspec-serializable; carries `rejections`) |
|
|
73
|
+
| `strategy/` | `Strategy` base + `StrategyContext` (the write-once seam); `SymbolStrategy` (`use()` registration, ready-gate); `tracker.py` — `NetPosition`/`PositionTracker`/`OrderTracker`, pure event folds over SDK models ONLY (that is the parity argument) |
|
|
74
|
+
| `metrics/` | `SummaryStats` (frozen msgspec) + `compute_summary` — drawdown, win rate, expectancy, profit factor, distance-to-floor, consistency headroom |
|
|
75
|
+
| `harness.py` | `Backtest` facade (`run()`/`arun()`/`from_dataframe`, ONE shared clock, instruments derived from the feed, strict `validate_bars`, `account=`, `fee_model=`) + `Report` (verdict/day-trail/summary rendering, provenance line); refuses backtesting.py's economics knobs by name |
|
|
76
|
+
| `indicators/` | TA-Lib adapter — `TalibIndicator` drives 152 of TA-Lib's 161 functions bar-by-bar (nine refused, see below); typed named wrappers (`Sma`/`Ema`/`Rsi`/`Atr`/`Macd`/`BBands`/…); pure-Python `Cross`; `Indicator`/`ValueSource` protocols in `base.py` |
|
|
77
|
+
|
|
78
|
+
## Semantics that trip people up
|
|
79
|
+
|
|
80
|
+
- **MLL is two-state:** floor ratchets ONLY on end-of-day closed balance (locks
|
|
81
|
+
permanently at the starting balance once EOD ≥ start + buffer); breach checks run
|
|
82
|
+
every tick on realized+unrealized. Intraday-trailing = Apex, not Topstep.
|
|
83
|
+
- A session close at/below the floor is itself a breach (flatten fees can do it).
|
|
84
|
+
- DLL (optional, off by default) is NOT a fail: flatten + day-lock, cleared at
|
|
85
|
+
18:00 ET; no re-emission while locked; does not preclude passing.
|
|
86
|
+
- Consistency: `best_day <= 0.5 * total_profit` — the formula is canonical; the
|
|
87
|
+
dollar table is canonical (no cross-size ratio holds).
|
|
88
|
+
- **There is NO exchange holiday calendar, by decision.** `core/calendar.py` was
|
|
89
|
+
deleted: its computed table disagreed with CME on about five dates a year (it
|
|
90
|
+
marked MLK / Presidents' Day / Memorial Day / Juneteenth / Labor Day as full
|
|
91
|
+
closures when CME equity index trades a half session), and the cleaner built on
|
|
92
|
+
it ran by DEFAULT, so it silently deleted tradable sessions. A wrong answer you
|
|
93
|
+
cannot see beats a missing feature, so the feature went. Consequences to hold in
|
|
94
|
+
your head: a holiday bar is indistinguishable from any other weekday bar; the
|
|
95
|
+
validator has no `holiday_bar`/`early_close_bar` code and never had a
|
|
96
|
+
replacement; `SimBroker` rejects only weekends (`error_code` 5, "market closed
|
|
97
|
+
(weekend)"); and `data/synthetic.py` skips weekends ONLY — its `days=` counts
|
|
98
|
+
WEEKDAYS, so a synthetic tape spanning Thanksgiving or Good Friday (those two
|
|
99
|
+
ARE genuine CME full closures) emits a session real data would not contain.
|
|
100
|
+
**Filter exchange holidays upstream, in the data you feed in.**
|
|
101
|
+
- ALL market orders — including `positions.close`/`partial_close` — rest and fill
|
|
102
|
+
at the NEXT bar's open (Tier-0). `wait_for_fill` raises in sim: use
|
|
103
|
+
`on_order`/`on_fill` callbacks (parity-safe in both worlds).
|
|
104
|
+
- Bracket children are created at the entry fill (offsets from the ACTUAL fill
|
|
105
|
+
price), active the next bar; only the TP carries `linked_order_id` (gateway
|
|
106
|
+
shape); OCO pairing lives in the broker's internal map. Reduce-only orders clamp
|
|
107
|
+
to the live position and can never flip exposure.
|
|
108
|
+
- `OrderModel.trail_price` is emitted as the trail DISTANCE as a price offset
|
|
109
|
+
(matching the SDK field docs), not the absolute stop.
|
|
110
|
+
- Limit fills require trade-THROUGH by default (exact touch — including at the
|
|
111
|
+
open — does not fill unless `fill_limit_on_touch=True`).
|
|
112
|
+
- **Refused placements are tallied, not silent.** Every order path funnels through
|
|
113
|
+
one choke point that counts rejections by gateway `error_code`:
|
|
114
|
+
`SimBroker.rejections` (a dict) and `BacktestResult.rejections` (sorted
|
|
115
|
+
`(error_code, count)` pairs), and `Report` prints a REJECTED line when it is
|
|
116
|
+
non-empty. Before this, a strategy whose every order was refused — cap 4,
|
|
117
|
+
outside-hours 5 — rendered as a clean zero-trade report. A zero-trade result
|
|
118
|
+
with a REJECTED line is a wiring bug, not a flat strategy.
|
|
119
|
+
- **TA-Lib is the single source of every indicator value.** Nothing in this repo
|
|
120
|
+
re-implements an indicator formula, so there is no second implementation to drift.
|
|
121
|
+
Values are float64 → `Decimal` at the boundary and NOT tick-snapped (see Money above);
|
|
122
|
+
float arithmetic is deterministic, so reruns stay byte-equal, but values are
|
|
123
|
+
float-precise, not Decimal-exact.
|
|
124
|
+
- **Bounded history is a PARITY decision, not an optimisation.** The buffer keeps the last
|
|
125
|
+
`history_bars` bars, so a value is a pure function of that window. Unbounded history would
|
|
126
|
+
make every value depend on where the series happened to start, and a live session warming
|
|
127
|
+
up from a finite history fetch could never reproduce a backtest that began earlier.
|
|
128
|
+
**Preload `history_bars` bars live** for bit-exact parity.
|
|
129
|
+
- **`ready` is NOT `warm`.** `ready` = a value EXISTS (at `lookback`). `warm` = the buffer is
|
|
130
|
+
FULL (at `history_bars`), i.e. the value no longer depends on where this run started. Bars
|
|
131
|
+
in `[lookback, history_bars)` are cold-start dependent. **Parity begins at `warm`**; the
|
|
132
|
+
`use()` ready-gate is a warmup gate, not a parity gate. `TalibLine` exposes `warm` too.
|
|
133
|
+
Never write "parity holds once ready".
|
|
134
|
+
- **`history_bars` is NOT always `max(512, 64 × lookback)`.** Functions whose memory is a
|
|
135
|
+
RATE get a derived window: SAR/SAREXT from `acceleration` (`SAR()` → 2000,
|
|
136
|
+
`SAR(acceleration=0.001)` → 40000; its lookback is 2 either way), MAMA from `slowlimit`,
|
|
137
|
+
KAMA a flat 9000-bar floor. Read `history_bars` off the instance, never recompute it.
|
|
138
|
+
- **"Windowed == unbounded" is NOT universal.** It holds (and is asserted) for the
|
|
139
|
+
exponentially-decaying family — EMA, Wilder RSI/ATR/ADX, MACD, DEMA/TEMA, T3, MAMA,
|
|
140
|
+
HT_TRENDLINE. It does NOT hold for accumulators (OBV, AD — weight 1.0 forever, so the LEVEL
|
|
141
|
+
is window-relative: use slope/divergence, never the level), running-sum functions (SMA and
|
|
142
|
+
what is built on it — STOCH slowd, CCI, MFI, ACCBANDS, BETA, ADOSC — within an ulp, since
|
|
143
|
+
float rounding depends on where the sum began), or adaptive smoothers (KAMA, HT_DCPERIOD).
|
|
144
|
+
**Backtest/live parity is unaffected either way** — both sides run the same window over the
|
|
145
|
+
same bars; only the "also equals an unbounded run" bonus varies.
|
|
146
|
+
- Streaming == batch, **bit for bit**: TA-Lib recomputes from index 0 every call and every
|
|
147
|
+
function is causal, so `FUNC(bars[:t+1])[-1] == FUNC(bars)[t]` exactly. `update(bar)`
|
|
148
|
+
appends to a buffer of already-closed bars and reads the LAST output element — the
|
|
149
|
+
no-look-ahead invariant is structural, not a convention.
|
|
150
|
+
- **Nine of the 161 functions are REFUSED at construction** (152 wrap fine): `EXP`/`COSH`/
|
|
151
|
+
`SINH`/`ACOS`/`ASIN` overflow or leave their domain at futures prices, so they could never
|
|
152
|
+
become ready — `SymbolStrategy` would swallow every bar and the strategy would silently
|
|
153
|
+
never trade (the constructor probes on a PRICE-REALISTIC series to catch this); `MAVP`
|
|
154
|
+
needs a non-bar `periods` input; `MAXINDEX`/`MININDEX`/`MINMAXINDEX` return an OFFSET into
|
|
155
|
+
the array passed in, whose basis silently changes once the bounded window fills.
|
|
156
|
+
- **Lossy parameters are refused.** `Sma(14.7)` (would have become `SMA(14)`) and
|
|
157
|
+
`matype=2.9` (would have become WMA when DEMA was meant) raise `ValueError`; so do bools
|
|
158
|
+
and non-integer `history=`. `Sma(14.0)` — an integral float — is fine.
|
|
159
|
+
- **`_compute` clears its dirty flag only AFTER the TA-Lib call succeeds.** Order matters and
|
|
160
|
+
is not stylistic: clearing it first MEMOISES THE FAILURE — the exception surfaces once, then
|
|
161
|
+
every later read that bar reports a benign "not ready", which `SymbolStrategy._gated()`
|
|
162
|
+
silently swallows, so a broken indicator becomes a strategy that just never trades. Never
|
|
163
|
+
reorder those two lines when refactoring `_compute`. Pinned by
|
|
164
|
+
`tests/unit/test_talib_adapter_hardening.py::test_a_raising_compute_is_not_cached_as_not_ready`.
|
|
165
|
+
- **Thread-safety: build anywhere, drive anywhere; one instance per thread.** `talib`'s
|
|
166
|
+
`Function` keeps its parameters in a `threading.local`, so a Function configured on one
|
|
167
|
+
thread silently reverts to TA-Lib's DEFAULTS on another (an `Ema(50)` quietly computing
|
|
168
|
+
`Ema(30)`). The adapter therefore treats it as STATELESS — parameters re-passed on every
|
|
169
|
+
call, `price=` applied by choosing which bar field fills the input slot instead of mutating
|
|
170
|
+
`input_names`. Keep it that way: never stash config on the Function. A single instance is
|
|
171
|
+
still not safe to drive from two threads at once (per-bar state) — parameter sweeps get one
|
|
172
|
+
instance per worker.
|
|
173
|
+
- **`STDDEV`/`VAR`/`BBANDS` use TA-Lib's naive `E[x²] − E[x]²`**, which cancels at index price
|
|
174
|
+
levels: measured relative error 1.9e-9 @5k, 2.4e-8 @20k, 6.7e-7 @100k. Fine for a threshold
|
|
175
|
+
read; a `Cross` on a Bollinger edge can flip on that noise. A band touch is not exact.
|
|
176
|
+
- **`Rsi` on flat closes reads 0, not 100.** TA-Lib returns 0.0 when all closes are equal
|
|
177
|
+
(all-gains still reads 100, all-losses 0). The old native "zero avg loss reads 100" rule
|
|
178
|
+
is gone; TA-Lib is the authority.
|
|
179
|
+
- **Minimum periods are per-function**, not global: TA-Lib rejects `RSI(1)`, `STDDEV(1)`,
|
|
180
|
+
`MAX(1)`, `MIN(1)`, `ADX(1)` (need ≥2) while `SMA(1)`, `EMA(1)`, `ATR(1)`, `WMA(1)` are
|
|
181
|
+
fine. The adapter PROBES TA-Lib at construction rather than hard-coding a table that
|
|
182
|
+
would drift → `ValueError("TA-Lib rejected …")` at `Rsi(1)`, not 500 bars into a run.
|
|
183
|
+
`period < 1` still raises `ValueError("period must be >= 1, got N")`. Lookbacks are
|
|
184
|
+
TA-Lib's: `Sma(3)`/`Ema(3)`/`StdDev(3)`/`Highest(3)` need 3 bars, `Rsi(3)`/`Atr(3)` need 4.
|
|
185
|
+
- **`TalibLine` is not `use()`-registerable.** A line (`macd.line("macdsignal")`) has no
|
|
186
|
+
`update` on purpose, so it cannot satisfy `Indicator`; `SymbolStrategy.use()` raises
|
|
187
|
+
`ValueError` if handed one — it would never advance and would gate the strategy forever.
|
|
188
|
+
Register the OWNER; `use()` resolves a `Cross` input through `TalibLine.owner`, so
|
|
189
|
+
`use(Cross(macd.line("macd"), macd.line("macdsignal")))` works once `macd` itself is
|
|
190
|
+
`use()`d.
|
|
191
|
+
- **Three more `use()` guards, each pinning a silent failure.** A `Cross` over another
|
|
192
|
+
`Cross` is refused (`ValueError`): it type-checks but has no `.value`, so it used to die
|
|
193
|
+
with `AttributeError` on the first bar both inner inputs were ready, hours in. A
|
|
194
|
+
non-`Indicator` is refused (`TypeError`) at registration, not on the first bar. And a
|
|
195
|
+
`Cross` that was never `use()`-registered now RAISES from `up`/`down` once both inputs are
|
|
196
|
+
ready — it never updates, so it used to read False forever and take zero trades in silence.
|
|
197
|
+
|
|
198
|
+
## Testing idioms
|
|
199
|
+
|
|
200
|
+
- Golden fixtures for every rulebook worked example (`tests/golden/`); hypothesis
|
|
201
|
+
property suites for money/kernel/fills invariants (`tests/property/`, seeded/
|
|
202
|
+
derandomized); parity conformance in `tests/parity/`; determinism =
|
|
203
|
+
`msgspec.json.encode(result)` equality across reruns.
|
|
204
|
+
- Build test bars with `tests/conftest.py::minute_bar` (ET wall-clock in, exact ns
|
|
205
|
+
out). A bar "at 9:30" OPENS 9:30 and CLOSES 9:31.
|
|
206
|
+
|
|
207
|
+
## Known deferred items (documented, not bugs)
|
|
208
|
+
|
|
209
|
+
STOP_LIMIT + JOIN_BID/JOIN_ASK rejected at Tier-0; no MessageBus yet; no
|
|
210
|
+
multi-timeframe resampling (history refuses non-native bar specs); DAY-unit
|
|
211
|
+
wrangling rejected (23h Globex day needs a session-aware resampling layer); no
|
|
212
|
+
exchange holiday calendar at all (removed 2026-07-28 — holidays and half sessions
|
|
213
|
+
are the caller's problem, upstream, see above); no behavioural intent-sequence
|
|
214
|
+
parity test (docs/ROADMAP.md Phase 4); fee/rule numbers are cited config pending
|
|
215
|
+
calibration against a real account (docs/topstep-rules.md §9 checklist, correct
|
|
216
|
+
fee rates with `fee_model=`).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. This project adheres to
|
|
4
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the version is
|
|
5
|
+
below 1.0, minor releases may contain breaking changes.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-07-28
|
|
8
|
+
|
|
9
|
+
First public release. Pre-alpha: the engine core is well tested, but the Topstep
|
|
10
|
+
rule and fee constants are **not yet calibrated against a live account**, so a
|
|
11
|
+
`PASSED`/`FAILED` verdict is a diagnostic, not an authoritative answer.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Event-driven backtest engine with backtest/live parity against
|
|
16
|
+
[`topstep-sdk`](https://pypi.org/project/topstep-sdk/): strategies are written
|
|
17
|
+
once against structural protocols that both `SimBroker` and `AsyncTopstepClient`
|
|
18
|
+
satisfy. SDK msgspec models (`OrderModel`, `PositionModel`, `HalfTradeModel`,
|
|
19
|
+
enums, `APIError`) are imported verbatim, never redefined.
|
|
20
|
+
- `CombineKernel` — the canonical Topstep Combine rulebook as a pure state
|
|
21
|
+
machine: two-state trailing MLL (floor ratchets only on end-of-day closed
|
|
22
|
+
balance, breach checked every tick on realized + unrealized equity), optional
|
|
23
|
+
daily loss limit, consistency target, position caps, 16:10 ET flatten.
|
|
24
|
+
- `Backtest(bars, strategy).run()` facade plus `Report`, and a
|
|
25
|
+
backtesting.py-flavoured strategy dialect (`SymbolStrategy` with `use()`
|
|
26
|
+
indicator registration and `buy`/`sell`/`close` sugar).
|
|
27
|
+
- Tier-0 fill model over OHLCV bars: market orders rest to the next bar's open,
|
|
28
|
+
limit orders require trade-through, one pessimistic intrabar price path shared
|
|
29
|
+
by fills and rule-breach detection.
|
|
30
|
+
- Indicators via TA-Lib — `TalibIndicator` drives 152 of TA-Lib's 161 functions
|
|
31
|
+
bar-by-bar, with typed wrappers (`Sma`, `Ema`, `Rsi`, `Atr`, `Macd`, `BBands`, …).
|
|
32
|
+
Nothing in this package re-implements an indicator formula.
|
|
33
|
+
- Exact-Decimal money on the instrument tick grid, FIFO lot cost basis, int-ns
|
|
34
|
+
UTC hot path with ET session boundaries, and a 15-product instrument table.
|
|
35
|
+
- Data wrangling from OHLCV records or a pandas DataFrame with an explicit
|
|
36
|
+
`stamp="open"|"close"`, a strict validator, and a seeded synthetic generator.
|
|
37
|
+
|
|
38
|
+
### Known limitations
|
|
39
|
+
|
|
40
|
+
- **No exchange holiday calendar.** Bars on market holidays and past early-close
|
|
41
|
+
halts are not detected or filtered anywhere. Filter them upstream.
|
|
42
|
+
- **No continuous-contract stitching.** One contract per run, inside a single
|
|
43
|
+
front-month window. A multi-month export spanning a roll is silently merged.
|
|
44
|
+
- **Rule and fee constants are uncalibrated** — see `docs/topstep-rules.md` §9.
|
|
45
|
+
- Multi-symbol runs, `dll_enabled=True`, and non-quarter-tick products (CL, GC)
|
|
46
|
+
are not exercised end to end.
|
|
47
|
+
- Tier-0 bar fills only. No quote, depth or MBO tiers.
|
|
48
|
+
|
|
49
|
+
[0.1.0]: https://pypi.org/project/topstep-backtest/0.1.0/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tarric Sookdeo
|
|
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,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: topstep-backtest
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Event-driven backtesting framework for Topstep Trading Combine strategies, with backtest/live parity against topstep-sdk.
|
|
5
|
+
Author-email: Tarric Sookdeo <tarricsookdeo@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: backtesting,combine,futures,prop-firm,topstep,trading
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: msgspec<1.0,>=0.18
|
|
21
|
+
Requires-Dist: ta-lib<0.8,>=0.7.1
|
|
22
|
+
Requires-Dist: topstep-sdk<0.2,>=0.1.2
|
|
23
|
+
Requires-Dist: tzdata>=2024.1; sys_platform == 'win32'
|
|
24
|
+
Provides-Extra: data
|
|
25
|
+
Requires-Dist: pandas>=2.2; extra == 'data'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
28
|
+
Requires-Dist: pandas>=2.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: pyright>=1.1.380; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# topstep-backtest
|
|
37
|
+
|
|
38
|
+
An event-driven backtesting framework for developing futures strategies that can
|
|
39
|
+
**profitably pass the Topstep Trading Combine**.
|
|
40
|
+
|
|
41
|
+
Its defining property is **backtest/live parity** with the companion
|
|
42
|
+
`topstep-sdk`: a strategy is written **once** against structural protocols that
|
|
43
|
+
both the deterministic `SimBroker` *and* the live `AsyncTopstepClient` satisfy.
|
|
44
|
+
Its core differentiator is a first-class **prop-firm rule engine**: the two-state
|
|
45
|
+
trailing Maximum Loss Limit, optional Daily Loss Limit, consistency target,
|
|
46
|
+
position caps, and session flatten are enforced *in real time* (including
|
|
47
|
+
intrabar forced liquidation with adverse slippage), not scored after the fact.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
class SmaCross(SymbolStrategy):
|
|
51
|
+
def __init__(self, contract_id: str) -> None:
|
|
52
|
+
super().__init__(contract_id)
|
|
53
|
+
self.fast = self.use(Sma(20)) # TA-Lib, driven bar by bar, causal by construction
|
|
54
|
+
self.slow = self.use(Sma(50))
|
|
55
|
+
self.cross = self.use(Cross(self.fast, self.slow))
|
|
56
|
+
|
|
57
|
+
async def on_bar(self, bar: Bar) -> None: # gated until every use()d indicator is ready
|
|
58
|
+
if self.cross.up and self.position.flat:
|
|
59
|
+
await self.buy( # sugar over ctx.orders — the topstep-sdk surface
|
|
60
|
+
2,
|
|
61
|
+
stop_loss_ticks=40,
|
|
62
|
+
take_profit_ticks=80, # signed-tick OCO bracket
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install topstep-backtest
|
|
70
|
+
pip install "topstep-backtest[data]" # adds pandas, for DataFrame input
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Requires Python 3.12+. TA-Lib is a core dependency and ships wheels for common
|
|
74
|
+
platforms; on others you will need the TA-Lib C library first.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from datetime import date
|
|
78
|
+
from decimal import Decimal
|
|
79
|
+
|
|
80
|
+
from topstep_backtest import AccountSize, Backtest, SymbolStrategy
|
|
81
|
+
from topstep_backtest.core.instruments import spec_for_symbol
|
|
82
|
+
from topstep_backtest.data.synthetic import synthetic_bars
|
|
83
|
+
from topstep_backtest.indicators import Cross, Sma
|
|
84
|
+
|
|
85
|
+
MNQ = "CON.F.US.MNQ.U26"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SmaCross(SymbolStrategy):
|
|
89
|
+
def __init__(self, contract_id: str) -> None:
|
|
90
|
+
super().__init__(contract_id)
|
|
91
|
+
self.fast = self.use(Sma(10))
|
|
92
|
+
self.slow = self.use(Sma(30))
|
|
93
|
+
self.cross = self.use(Cross(self.fast, self.slow))
|
|
94
|
+
|
|
95
|
+
async def on_bar(self, bar) -> None:
|
|
96
|
+
if self.cross.up and self.position.flat:
|
|
97
|
+
await self.buy(1, stop_loss_ticks=40, take_profit_ticks=80)
|
|
98
|
+
elif self.cross.down and self.position.is_long:
|
|
99
|
+
await self.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
bars = synthetic_bars(
|
|
103
|
+
contract_id=MNQ,
|
|
104
|
+
spec=spec_for_symbol("MNQ"),
|
|
105
|
+
start_day=date(2026, 5, 4),
|
|
106
|
+
days=5,
|
|
107
|
+
seed=7,
|
|
108
|
+
start_price=Decimal("23000.00"),
|
|
109
|
+
bars_per_day=120,
|
|
110
|
+
vol_ticks=12,
|
|
111
|
+
)
|
|
112
|
+
print(Backtest(bars, SmaCross(MNQ), account=AccountSize.S50K).run())
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Feeding your own data (e.g. normalized Databento candles):
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from topstep_backtest.data.wrangler import bars_from_dataframe
|
|
119
|
+
|
|
120
|
+
bars = bars_from_dataframe(
|
|
121
|
+
df,
|
|
122
|
+
contract_id="CON.F.US.MNQ.U26",
|
|
123
|
+
spec=spec,
|
|
124
|
+
unit=AggregateBarUnit.MINUTE,
|
|
125
|
+
unit_number=1,
|
|
126
|
+
stamp="open",
|
|
127
|
+
) # declare what your timestamps mean!
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`bars_from_records` is the same loader without the pandas dependency.
|
|
131
|
+
|
|
132
|
+
## Status and limitations
|
|
133
|
+
|
|
134
|
+
**Pre-alpha (0.1.0).** The engine core is well covered — exact-Decimal money on
|
|
135
|
+
the tick grid, FIFO lot accounting, a structurally enforced no-look-ahead
|
|
136
|
+
firewall, and byte-identical reruns — but read these before trusting a number:
|
|
137
|
+
|
|
138
|
+
- **The rule and fee constants are NOT calibrated against a live account.** They
|
|
139
|
+
are researched, source-cited config (docs/topstep-rules.md §9 has 8 unchecked
|
|
140
|
+
boxes). Treat a `PASSED`/`FAILED` verdict as a diagnostic, not an answer, and
|
|
141
|
+
distrust any result landing within a tick or a fee of a limit.
|
|
142
|
+
- **No exchange holiday calendar ships with this package.** Bars on market
|
|
143
|
+
holidays and past early-close halts are not detected, flagged or filtered
|
|
144
|
+
anywhere — filter them upstream. (A built-in calendar was removed in 0.1.0: it
|
|
145
|
+
disagreed with CME on several dates a year and silently discarded tradable
|
|
146
|
+
sessions, which is worse than not having one.)
|
|
147
|
+
- **One contract per run, inside a single front-month window.** There is no
|
|
148
|
+
continuous-contract stitching, and no roll detector. A multi-month export
|
|
149
|
+
spanning a quarterly roll is silently merged into one price series.
|
|
150
|
+
- **Tier-0 bar fills only.** Market orders fill at the next bar's open and
|
|
151
|
+
default to zero slippage, so a strategy whose edge is thinner than roughly
|
|
152
|
+
8–10 ticks per round turn is inside the model's error bars. Event windows
|
|
153
|
+
(08:30 ET releases and the like) are not honestly modelled at bar resolution.
|
|
154
|
+
- **Untested end to end:** multi-symbol runs, `dll_enabled=True`, and
|
|
155
|
+
non-quarter-tick products (CL, GC).
|
|
156
|
+
|
|
157
|
+
What is proven is *structural* parity — pyright-strict conformance plus a
|
|
158
|
+
signature-diff test against the live SDK. The behavioural gate (an intent-sequence
|
|
159
|
+
test against a recording live broker) is **not built yet**; see docs/ROADMAP.md.
|
|
160
|
+
|
|
161
|
+
## What works today
|
|
162
|
+
|
|
163
|
+
- **Rule kernel** (`rules/`) — the canonical Topstep Combine rulebook: EOD-ratcheting
|
|
164
|
+
trailing MLL with permanent lock at the starting balance, real-time breach on
|
|
165
|
+
realized+unrealized equity, optional DLL (flatten-and-lock, not a fail),
|
|
166
|
+
consistency (`best_day ≤ 50% × total`), fixed 5/10/15-mini position caps —
|
|
167
|
+
golden-fixtured against the documented worked examples.
|
|
168
|
+
- **Deterministic engine** (`engine/`, `clock/`) — single time-ordered loop,
|
|
169
|
+
`TestClock`, strict `ts_init` ordering assertions, bit-for-bit reproducible runs.
|
|
170
|
+
- **SimBroker** (`execution/`) — full order lifecycle (market/limit/stop/trailing,
|
|
171
|
+
signed-tick OCO brackets), netting + exact-Decimal P&L, gateway-parity `APIError`
|
|
172
|
+
rejections tallied on the result, intrabar fill-vs-breach resolution along one
|
|
173
|
+
pessimistic price path, forced liquidation whose loss can exceed the floor.
|
|
174
|
+
- **Tier-0 fills** (`fills/`) — pessimistic bar fills: adverse-extreme-first path,
|
|
175
|
+
next-bar-open for close signals, gap-through at the open, ≥1-tick stop slippage,
|
|
176
|
+
trade-through (not touch) limit fills; per-side/per-instrument Topstep fee stack,
|
|
177
|
+
correctable via `Backtest(..., fee_model=TopstepFees(overrides={...}))`.
|
|
178
|
+
- **Data layer** (`data/`) — pandas/records → validated `Bar` streams (explicit
|
|
179
|
+
open/close stamping kills the off-by-one-bar look-ahead), tick-grid validation,
|
|
180
|
+
session/halt/weekend checks, deterministic synthetic generator.
|
|
181
|
+
- **Indicators** (`indicators/`) — every indicator is TA-Lib: `TalibIndicator` drives 152 of
|
|
182
|
+
its 161 functions bar-by-bar (typed `Sma`/`Ema`/`Rsi`/`Atr`/`Macd`/`BBands`/… wrappers,
|
|
183
|
+
plus a pure-Python `Cross`), so no formula is re-implemented here to drift; the nine
|
|
184
|
+
refused are the ones that could only fail silently. Streaming values are bit-identical to a
|
|
185
|
+
batch run, and the bounded history window is a *parity* decision — preload `history_bars`
|
|
186
|
+
bars live and the live number equals the sim number. Parity begins at `warm` (the window is
|
|
187
|
+
full), not at `ready` (a value exists).
|
|
188
|
+
- **Parity seam** (`protocols.py`) — pyright-strict conformance tests prove
|
|
189
|
+
`AsyncTopstepClient` and `SimBroker` satisfy the same `Broker` protocol, plus a
|
|
190
|
+
signature-diff test against the live SDK that fails the suite on drift.
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
From a checkout (the sibling `topstep-sdk` repo is expected at `../topstep-sdk`;
|
|
195
|
+
set `UV_NO_SOURCES=1` to resolve it from PyPI instead):
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv sync --extra dev
|
|
199
|
+
uv run pytest && uv run ruff check . && uv run ruff format --check . && uv run pyright
|
|
200
|
+
cd examples && uv run python run_combine.py # end-to-end combine verdict
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Documentation
|
|
204
|
+
|
|
205
|
+
The source repository is private, so there is no public issue tracker or docs
|
|
206
|
+
site. Everything below ships **inside the sdist** — `pip download --no-binary
|
|
207
|
+
:all: topstep-backtest` and unpack it, or read the copies in your environment.
|
|
208
|
+
|
|
209
|
+
- **`docs/GUIDE.md` — start here**: the user guide (data in, writing strategies,
|
|
210
|
+
wiring runs, reading results, verification checklist).
|
|
211
|
+
- `docs/TUTORIAL_EMA_CROSSOVER.md` — an in-depth walkthrough building one
|
|
212
|
+
strategy end to end.
|
|
213
|
+
- `docs/STRATEGY_API.md` — the strategy dialect reference, including the
|
|
214
|
+
indicator surface.
|
|
215
|
+
- `docs/topstep-rules.md` — the rulebook being enforced, with sources,
|
|
216
|
+
confidence levels, and a verify-before-trusting checklist.
|
|
217
|
+
- `docs/DESIGN.md` — architecture contract (parity seam, no-look-ahead
|
|
218
|
+
invariants, fill tiers). Its §4 layout is the *target* architecture, not the
|
|
219
|
+
current tree.
|
|
220
|
+
- `AGENTS.md` — dense maintainer/agent reference and the real module map.
|
|
221
|
+
- `examples/` — runnable: `run_real_data.py` (your CSV/Parquet → verdict),
|
|
222
|
+
`run_combine.py` (synthetic end-to-end), `ema_cross.py`, `sma_cross.py`,
|
|
223
|
+
`talib_macd.py`.
|
|
224
|
+
|
|
225
|
+
## Roadmap
|
|
226
|
+
|
|
227
|
+
Analytics + Monte-Carlo pass-probability → live adapter + calibration → L1/L2/MBO
|
|
228
|
+
fill tiers. Funded-account (XFA) modeling is deliberately parked. See
|
|
229
|
+
`docs/ROADMAP.md` in the sdist.
|
|
230
|
+
|
|
231
|
+
## Stack
|
|
232
|
+
|
|
233
|
+
Python 3.12+ · `topstep-sdk` · `msgspec` · `TA-Lib` · `uv` · `ruff` · `pyright`
|
|
234
|
+
(strict) · `pytest` + `hypothesis`. Canonical timezone: **ET** (`America/New_York`);
|
|
235
|
+
internal hot path is int-ns UTC; all money is exact `Decimal` on the tick grid —
|
|
236
|
+
indicator values cross from float64 into `Decimal` and are deliberately *not*
|
|
237
|
+
tick-snapped (an indicator level is not a tradeable price).
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT — see LICENSE.
|
|
242
|
+
|
|
243
|
+
> **Unofficial.** Not affiliated with, endorsed by, or sponsored by Topstep, LLC
|
|
244
|
+
> or ProjectX Trading, LLC. "Topstep" is a trademark of its respective owner and
|
|
245
|
+
> is used here only to identify the evaluation program this tool models. Rule
|
|
246
|
+
> numbers researched July 2026 — re-verify against Topstep's help center before
|
|
247
|
+
> trusting a pass verdict (see the checklist in docs/topstep-rules.md §9).
|
|
248
|
+
>
|
|
249
|
+
> **Not financial advice.** This software simulates a trading evaluation and can
|
|
250
|
+
> be wrong. You are solely responsible for any capital you risk.
|