data.continuous¶
Stitching quarterly futures contracts into one continuous series without inventing P&L at the roll.
continuous
¶
Continuous-contract stitching: many expiries -> one back-adjusted series.
A two-year MNQ backtest spans about eight quarterly rolls. Concatenating the
expiries naively leaves a price jump at every seam, and while that jump does NOT
corrupt P&L (see below), it badly corrupts INDICATORS — a 50-point roll spread
is a spurious Cross, an Atr spike that inflates for a whole lookback, and a
false Highest/Lowest breakout. The trades that follow have perfectly correct
P&L; they simply should never have been taken.
Why additive back-adjustment is exactly P&L-neutral here. This engine force-flattens every position at 16:10 ET and cancels every working order, with a backstop at the session roll — so no position and no resting order ever survives a trading-day boundary, and a roll seam IS a day boundary. Entry and exit therefore always share one day, one contract, and one adjustment offset:
(exit + delta) - (entry + delta) == exit - entry
The offsets cancel identically. Realized P&L, unrealized marks, the balance and the trailing MLL floor are all unchanged by adjustment. That is a much stronger guarantee than a general-purpose backtester can make, and it is the reason this module adjusts prices rather than trying to model rolls as tradable events.
Additive only, never ratio. A multiplicative adjustment does not cancel in
the difference (so P&L WOULD move) and pushes prices off the tick grid, which
SimBroker asserts on every fill. The spread between two expiries of the same
product is an integer number of ticks — both trade the same grid — so the
additive offset preserves grid alignment exactly.
What adjustment does change, and it is on you to avoid:
- Absolute price levels. Logic keyed to round numbers or a fixed price target
("exit at 24,000") is distorted. Tick-relative logic —
stop_loss_ticks,take_profit_ticks, everything in this project's strategy sugar — is not. - The reported prices no longer match live
history.retrieve_bars, which returns raw single-expiry quotes. Signals are relative so they carry over; the printed numbers do not.
Rolls are forced onto trading-day boundaries. A mid-session roll would break the cancellation argument above, so it is refused rather than approximated.
RollEvent
¶
Bases: Struct
One seam: the day the front month changed, and what it cost to align.
to_price
instance-attribute
¶
The two expiries' closes at the SAME instant — the last timestamp before the roll where both traded. A spread taken from different instants would fold an overnight move into the adjustment.
offset_ticks
instance-attribute
¶
to_price - from_price in ticks, the amount added to everything BEFORE
this seam (cumulatively, walking backwards). Integer by construction: both
expiries trade the same tick grid.
ContinuousSeries
¶
Bases: Struct
A stitched, back-adjusted series ready to hand to Backtest.
symbol
instance-attribute
¶
The contract id the output bars carry — the bare product ticker (e.g.
"MNQ"), which spec_for_symbol already resolves. The whole series is
one instrument as far as the engine is concerned, so positions, orders and
the rule kernel see an unbroken account.
rolls
instance-attribute
¶
rolls: tuple[RollEvent, ...]
Every seam, in order. Empty when a single expiry covered the range.
adjusted
instance-attribute
¶
False when nothing needed shifting (one expiry, or every offset was
zero). Distinguishes "no adjustment applied" from "adjustment applied and
happened to be small".
roll_days
property
¶
The seam days — worth excluding when attributing a bad day, since an
indicator's lookback is still crossing the seam for lookback bars.
stitch_continuous
¶
stitch_continuous(series: Mapping[str, Sequence[Bar]], *, spec: InstrumentSpec, symbol: str | None = None, roll_days: Mapping[str, date] | None = None) -> ContinuousSeries
Back-adjust several expiries into one continuous series.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
Mapping[str, Sequence[Bar]]
|
|
required |
spec
|
InstrumentSpec
|
The product's |
required |
symbol
|
str | None
|
Contract id for the output bars. Defaults to |
None
|
roll_days
|
Mapping[str, date] | None
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
ContinuousSeries
|
class: |
ContinuousSeries
|
labelled with one contract id. |
Raises:
| Type | Description |
|---|---|
ValueError
|
on an empty input, a contract whose bars carry a foreign contract id, a roll day with no shared timestamp to price the spread from, or an offset that is not a whole number of ticks (which would mean the two expiries are not on the same grid). |
Source code in src/topstep_backtest/data/continuous.py
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | |