This package contains concrete trading strategies and the registry that exposes them to the rest of the system.
Each production strategy should be self-contained in its own file under
src/strategies/. Shared code belongs in src/strategies/filters/ only when
it is a pure indicator, mask builder, or other stateless helper. Avoid
cross-strategy execution helpers or "family" loaders that hide runtime logic.
Treat registry.py as the only canonical list of available strategies.
Practical workflow:
- Create or update a strategy module under
src/strategies/. - Register or unregister it in
registry.py. - Run the relevant tests.
This README intentionally does not list concrete strategy IDs. The strategy set changes frequently, and duplicating that inventory in docs creates stale maintenance overhead with no operational value.
All strategies:
- inherit from
BaseStrategy - receive an engine instance in
__init__ - precompute expensive indicators up front
- keep
on_bar()lightweight
Do not manually shift signals to "fix" timing.
The engine contract is already:
- strategy evaluates
bar[t] - returned orders execute at
open[t+1]
Adding extra shift(1) logic usually creates a delayed strategy, not a safer one.
Non-market orders still follow the same no-lookahead rule:
- a
LIMIT/STOP/STOP_LIMITcreated onbar[t]first becomes eligible onbar[t+1] DAYexpires on the next calendar dayIOCis attempted on the first eligible bar and then cancelled if unfilled
Single-asset strategies can return:
MARKETLIMITSTOPSTOP_LIMIT
Use the convenience factories in BaseStrategy rather than instantiating Order directly when possible.
Important single-engine bracket rule:
- multiple same-bar
reduce_only=Truenon-market orders are auto-grouped as one protective OCO bracket - if both stop and target are reachable on the same coarse bar, the single engine first tries lower-timeframe replay when
intrabar_conflict_resolution=lower_timeframe - if replay data is missing, incomplete, or anomalous, the single engine falls back to the pessimistic policy and lets the stop win
- this rule exists because the legacy strategy contract returns only
List[Order], not an explicit bracket object
Practical implication:
- native stop/target brackets are now safe in the single engine only when emitted as same-bar
reduce_onlynon-market siblings - if you emit unrelated resting orders on the same bar, do not mark them all
reduce_only=Trueunless you actually want OCO behavior
The portfolio engine is still target-driven, but it now preserves enough raw intent for resting entries to work.
Current bridge behavior:
- the live portfolio position is the source of truth for bridge state, but raw non-
reduce_onlyorder intent is still preserved - if a strategy is flat and emits a non-
reduce_onlyLIMIT/STOP/STOP_LIMITentry, the bridge infers provisional direction from that raw order side - if a strategy is already invested and emits an opposite-side non-
reduce_onlyorder, the bridge maps it to explicitCLOSEorREVERSEintent instead of blindly reusing the live position sign reduce_only=Trueorders are excluded from entry-direction fallback so protective exits do not request fresh exposure- same-bar entry plus protective bracket metadata is preserved and forwarded into the portfolio OMS as parent/child intent
Practical implication:
- flat pending-entry strategies such as
three_bar_mrandchannel_breakoutcan trade in portfolio mode without pre-setting_invested=True - explicit exit/reversal strategies can now close or reverse live portfolio positions without local bridge workarounds
- Create a new module in
src/strategies/. - Keep the strategy's config and execution state in that same file.
- Use
src/strategies/filters/only for stateless helpers. - Precompute indicators in
__init__. - Implement
on_bar()using O(1) lookups. - Implement
get_search_space()in the same module if WFO support is needed. - Register the strategy in
registry.py.
get_search_space() is for walk-forward optimization and should reflect robust ranges, not ultra-fine parameter mining.
Guidelines:
- prefer coarse steps over tiny increments
- optimize the 4-6 parameters that most affect regime, entry quality, and risk placement
- avoid stuffing the search space with every boolean or cosmetic knob
- keep the number of optimized parameters within the global WFO budget in
BacktestSettings.wfo_max_parameters - if a strategy uses
trade_direction, it can be a categorical search dimension when directionality is materially part of the thesis
Good candidates:
- lookback lengths
- ATR windows / ATR multiples
- regime filters
- entry offsets for
LIMIT/STOPlogic
Usually weak candidates:
- logging toggles
- redundant mirrored thresholds
- overly precise decimal steps that will not generalize OOS
registry.py is the source of truth for:
- CLI strategy IDs
- accepted aliases
- class loading for portfolio YAML configs
If the strategy is not registered, it is not part of the platform.
- keep data slicing out of
on_bar() - keep comments and docstrings in English
- put reusable execution settings in
src/backtest_engine/settings.py - add tests for unusual entry, exit, or stateful behavior
- if a strategy emits native protective exits, add at least one regression test covering bracket cancel behavior