rules.kernel¶
The prop-firm rule engine — trailing MLL, daily loss limit, consistency — enforced in real time, not scored afterwards.
kernel
¶
The Topstep Combine rule kernel — the canonical combine-rulebook state machine.
A PURE state machine (docs/topstep-rules.md §§1-5): no I/O, no clock reads, no
randomness. Callers push int-ns UTC timestamps and exact Decimal balances
in; the kernel answers with breaches and a verdict.
The single biggest correctness item is the two-state trailing Maximum Loss Limit (§2):
- State A — floor ratchet, END OF DAY ONLY: the floor starts at
starting_balance - mll_bufferand ratchets up only on end-of-day CLOSED balance, never intraday, never down. Once the ratcheted floor would reach the starting balance it locks there permanently. - State B — breach check, REAL TIME: every tick the caller pushes live equity
(realized + unrealized) into :meth:
CombineKernel.check_equity; touching the floor (equity <= floor) fails the account immediately (terminal).
The optional Daily Loss Limit (§3) is a same-day lockout, not a violation:
tripping it returns a :class:Breach of kind DLL and sets day_locked
until the next session close. An MLL breach takes precedence when both trip
on the same tick. The stored breach property holds only the terminal MLL
breach; DLL breaches are returned to the caller but never stored.
Breach limit semantics: for MLL it is the floor; for DLL it is the equity
threshold day_start_balance - dll (the level at which the day locks).
Pass evaluation (§§1, 4) happens only in :meth:CombineKernel.on_session_close
and only while IN_PROGRESS: the closed balance must reach
starting_balance + profit_target, total profit must be positive, and the
best traded day must satisfy best_day <= consistency_pct * total_profit
(Topstep's target inflation: a too-big best day forces more total profit,
implying the ~2-day minimum). best_day only considers days on which
:meth:CombineKernel.on_trade_activity was called; losing days never reset it.
Verdict
¶
BreachKind
¶
Breach
¶
Bases: Struct
A limit breach at ts_ns. limit is the equity threshold breached:
the trailing floor for MLL, day_start_balance - dll for DLL.
DayRecord
¶
Bases: Struct
One closed trading day. floor_after is the MLL floor in effect after
this close's end-of-day ratchet; had_trade records whether trade
activity occurred during the day.
CombineKernel
¶
CombineKernel(params: CombineParams)
The single canonical implementation of the Topstep Combine rulebook.
Drive it with three calls: :meth:on_trade_activity when a fill happens,
:meth:check_equity on every tick with live equity (realized +
unrealized), and :meth:on_session_close at the 17:00 ET Globex close
with the day's closed balance. Once the verdict is terminal (PASSED or
FAILED) all mutating calls become no-ops.
Source code in src/topstep_backtest/rules/kernel.py
total_profit
property
¶
Last end-of-day closed balance minus the starting balance.
day_start_balance
property
¶
Balance at the current trading day's start (prior session's close).
day_locked
property
¶
Whether the DLL has locked out trading for the rest of the day.
on_trade_activity
¶
Mark that a trade happened; makes the current day a traded day.
check_equity
¶
check_equity(ts_ns: int, equity: Decimal) -> Breach | None
Real-time breach check on live equity (realized + unrealized).
Touching the MLL floor (equity <= floor) fails the account
terminally and returns (and stores) the MLL breach. Otherwise, with a
DLL configured, a day loss at/past the limit returns a DLL breach and
sets day_locked (not terminal; MLL takes precedence when both
trip). Once terminal, returns the stored breach without mutating.
Source code in src/topstep_backtest/rules/kernel.py
on_session_close
¶
Close the trading day at closed_balance (the 17:00 ET snapshot).
Closes the day's record first (day_pnl against the day's starting
balance), then applies the end-of-day floor ratchet, then evaluates
the pass condition, then rolls day state (DLL lockout clears; the next
day's starting balance becomes closed_balance). No-op once the
verdict is terminal.