From a strategy idea to an auditable portfolio result.
QuantJourney Backtester is a Python-native research engine with two explicit paths: target-weight portfolio simulation and stateful order simulation. This guide explains where every decision enters the pipeline, what the engine calculates and what the result can—and cannot—prove.
The 60-second mental model
A backtest is a chain of contracts. Market data becomes features; the strategy produces a decision; accounting turns that decision into positions and NAV; reporting exposes the assumptions and result. QJ keeps the two most common strategy contracts separate instead of pretending weights and orders are the same thing.
OHLCV, dates, universe and benchmark.
SMA, RSI, ATR and strategy-ready frames.
Desired exposure or explicit instructions.
Rebalance, fills, costs, cash and positions.
NAV, metrics, plots and run metadata.
Use execution_mode="weights" when the strategy decides portfolio allocation.
Use execution_mode="orders" when pending orders, trigger prices, gaps, brackets or order state are part of the thesis.
Choose the strategy contract
Start with the simplest model that can answer the research question. Extra execution state is useful only when it changes the decision you are testing.
“What should the portfolio hold?”
Return a dates × instruments matrix of target exposure. The engine handles timing, risk overlays, rebalancing, cash, turnover and portfolio returns.
- Asset allocation, ranking and rotation
- Factors and cross-sectional signals
- Risk parity and volatility targeting
- Daily, weekly, monthly or conditional rebalancing
“Which instruction should be submitted now?”
Submit orders bar by bar. The fill engine keeps pending state, tests OHLC triggers, applies costs and mutates cash and positions after fills.
- Market, limit and stop execution
- Bracket, trailing and OCO behavior
- Gap-aware protective exits
- Partial fills, expiry and order lifecycle
| Question | Weight mode | Order mode |
|---|---|---|
| Strategy output | Target-weight DataFrame | Submitted Order objects |
| State | Targets, realized weights, drift | Pending orders, fills, cash, positions |
| Costs | Turnover-based weight cost | Per-fill commission and slippage |
| Timing | Shifted allocation, then rebalance | Bar-by-bar fill rules |
| Best first use | Portfolio research | Execution-sensitive validation |
Data and indicators
The constructor defines the universe, date range, source and granularity. Data is normalized into instrument frames; declarative indicator configuration adds named features that both strategy modes can consume.
import pandas as pd
from backtester import Backtester
class DailySMATrend(Backtester):
def _compute_signals(self) -> pd.DataFrame:
fast = self.instruments_data.get_feature("SMA_50_close")
slow = self.instruments_data.get_feature("SMA_200_close")
valid = fast.notna() & slow.notna()
return (fast > slow).astype(float).where(valid, 0.0)
def _compute_weights(self) -> pd.DataFrame:
active = self.signals == 1.0
counts = active.sum(axis=1)
return active.div(counts, axis=0).fillna(0.0).clip(upper=0.25) The strategy does not download data inside its signal hook and does not recompute hidden indicators on every bar. It reads named, aligned features and handles the warmup region explicitly.
How weight mode becomes NAV
Target weights are intent, not final positions. Before they earn returns, QJ moves them through a causal timing boundary and portfolio controls.
Exposure requested by the strategy.
Avoid same-bar return capture.
Limits, vol target or risk parity.
Targets become realized weights.
Cash, turnover and drift are recorded.
strategy = DailySMATrend(
strategy_name="DailySMATrend",
initial_capital=100_000,
instruments=["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
backtest_period={"start": "2015-01-01", "end": "2025-01-01"},
source="yfinance",
execution_mode="weights",
max_position_size=0.25,
rebalance_policy=RebalancePolicy(frequency="D"),
indicators_config=[
{"function": "SMA", "price_cols": ["close"],
"params": {"periods": [50, 200]}},
],
save_text_reports=True,
save_portfolio_plots=True,
)
await strategy.run_strategy() A signal calculated from today’s close cannot also earn today’s close-to-close return. QJ shifts weight decisions before applying them. Changing the signal timestamp, execution price or rebalance anchor can materially change NAV.
How order mode works
Order mode receives the current date, bars, positions and NAV. Strategy code submits instructions; FillEngine owns the pending book and decides whether each order fills, remains active, expires, is rejected or is cancelled.
from backtester.execution.order_types import (
BracketSpec, Order, OrderSide, OrderType,
)
class BracketTrend(Backtester):
def _compute_orders(self, date, bars, current_positions, nav):
fast = self.instruments_data.get_feature("SMA_20_close")
slow = self.instruments_data.get_feature("SMA_50_close")
for instrument in self.instruments:
if fast.loc[date, instrument] <= slow.loc[date, instrument]:
continue
if current_positions.get(instrument, 0.0) != 0:
continue
close = bars[instrument].close
quantity = int(nav * 0.15 / close)
bracket = BracketSpec(
take_profit_price=round(close * 1.06, 2),
stop_loss_price=round(close * 0.97, 2),
)
self.fill_engine.submit(
Order(instrument, OrderSide.BUY, quantity,
OrderType.BRACKET, bracket=bracket)
) | Order | Engine behavior | Typical use |
|---|---|---|
| Market | Execute at the configured next-bar open or close. | Simple entry, exit and rebalance orders. |
| Limit | Fill only when the bar reaches the limit; open gaps can improve price. | Passive entry and take-profit. |
| Stop | Activate after the stop level is crossed, including gap-through behavior. | Breakout entry and protective exit. |
| Stop-limit | Stop activation followed by an at-or-better limit condition. | Price-controlled breakout or protection. |
| Trailing stop | Move the stop with favorable price movement. | Dynamic loss protection. |
| Trailing stop-limit | Trailing activation with an explicit limit constraint. | Controlled trailing execution. |
| Bracket | Entry plus linked take-profit and stop-loss children. | Defined reward/risk trade. |
| OCO | A fill in one leg cancels its sibling. | Competing exits or entries. |
The engine also supports DAY/GTD/GTC time-in-force, expiration by date or number of bars, volume-participation caps and order/fill histories. These are bar-based execution mechanics—not a queue-position or tick-level exchange simulator.
Rebalancing and drift
Rebalancing decides when target weights are allowed to replace realized, drifted weights. Calendar triggers are only the first layer; policies can also react to drift, tracking error, signal changes, drawdown and turnover budget.
from backtester.portfolio.rebalance import RebalancePolicy
policy = RebalancePolicy(
frequency="BME", # business month-end
drift_threshold=0.05, # or when a weight drifts by 5%
max_annual_turnover=4.0, # rolling turnover budget
partial_rebalance=True, # touch only positions outside the band
)
strategy = MyStrategy(..., rebalance_policy=policy) Scheduled
Daily, weekly, month-end, quarter-end, year-end or every N trading days.
Conditional
Drift, tracking-error and signal-change triggers can force a rebalance.
Gated
Turnover budgets, partial rebalance and drawdown breakers constrain activity.
Month-start and month-end are different strategies, not formatting aliases. Exchange holidays are snapped within the intended calendar period, and calendar conventions should be sensitivity-tested.
Risk, slippage and commissions
Risk adjustment sits between raw target weights and rebalancing. In order mode, slippage changes the fill price and commissions reduce cash on every fill.
from backtester.risk import (
PositionLimitModel,
RiskModelChain,
VolTargetModel,
)
risk = RiskModelChain([
VolTargetModel(target_vol=0.15, lookback=63, max_leverage=1.5),
PositionLimitModel(max_weight=0.25),
])
strategy = MyStrategy(..., risk_model=risk) from backtester.execution.commission import PerShareCommission
from backtester.execution.slippage import FixedBpsSlippage
strategy = MyOrderStrategy(
...,
execution_mode="orders",
slippage_model=FixedBpsSlippage(bps=5.0),
commission_scheme=PerShareCommission(
cost_per_share=0.005,
min_per_order=1.0,
),
) Contract-aware instruments
A futures contract, FX lot and equity share do not turn the same price move into the same P&L. ContractSpec carries multiplier, tick, margin, lot and currency semantics into execution-aware accounting.
from backtester.execution.contract_spec import AssetClass, ContractSpec
specs = {
"ES=F": ContractSpec(
symbol="ES=F",
asset_class=AssetClass.FUTURE,
multiplier=50.0,
tick_size=0.25,
margin=12_000.0,
),
}
strategy = MyStrategy(..., contract_specs=specs) ContractSpec describes accounting and execution mechanics. Data availability, continuous-contract construction, corporate actions and currency conversion still need to be correct for the instrument being tested.
Walk-forward and optimization
A strong in-sample result is only a hypothesis. QJ provides rolling, expanding and anchored folds, explicit pre-OOS purge controls, grid or Optuna search and diagnostics such as Sharpe decay, deflated Sharpe and rolling rank stability.
from backtester.walkforward import WalkForwardConfig, WalkForwardEngine
config = WalkForwardConfig(
scheme="rolling",
train_months=24,
test_months=6,
step_months=6,
purge_days=5,
extra_pre_oos_purge_pct=0.01,
)
engine = WalkForwardEngine(
config=config,
initial_capital=100_000,
backtester_factory=build_strategy_for_fold,
)
result = await engine.run_async(strategy.portfolio_data) Slicing a single full-period NAV is useful diagnostics, but it is not a refitted OOS test. Pass a backtester factory so each fold trains and evaluates inside its own information boundary. CPCV is not advertised here because the current public implementation remains planned.
What one run produces
The public package creates a local evidence packet that can be inspected without a hosted dashboard. The exact files depend on report flags and strategy mode.
Metrics and text
Terminal summary, performance_report.txt, metrics.csv, summary.json and summary.txt.
Portfolio artifacts
Equity curve, target or realized weights, holdings and mode-specific state.
PNG and HTML
Static plot pack plus a local dashboard.html for reviewing the run.
What ships where
The website should never blur a public engine capability with a hosted service. These are the current product boundaries for the v0.10.0 public repository.
| Capability | Status | Boundary |
|---|---|---|
| Weight and order backtesting | Open source | Local Python engine. |
| 50 runnable examples | Open source | 25 weights, 20 orders and 5 walk-forward examples. |
| Text, CSV, PNG and static HTML reports | Open source | Generated locally. |
| Cloud market-data warehouse and authenticated APIs | Hosted | QuantJourney platform service. |
| Interactive sharing, PDF factsheets and extended diagnostics | Hosted | Not part of the public package. |
| VWAP weight execution | Reserved | The enum exists; execution-aware mode rejects it. |
| CPCV fold scheme | Planned | Current public implementation is a placeholder. |
| Tick/queue simulation and live broker OMS | Non-goal | Outside the current engine lane. |
Module map
Users do not need to know every source file, but advanced researchers should be able to identify which layer owns an assumption.
| Area | Public repository | Responsibility |
|---|---|---|
| Strategy lifecycle | backtester/core.py | Data preparation, hook dispatch, mode selection and portfolio result. |
| Instrument data | backtester/portfolio/instr_data.py | Prices, returns and generated indicator features. |
| Weight accounting | backtester/portfolio/rebalance.py | Target weights, realized weights, drift and rebalance flags. |
| Orders and fills | backtester/execution/ | Order state, OHLC triggers, fills, commissions and slippage. |
| Risk overlays | backtester/risk/ | Position limits, volatility target, inverse volatility and risk parity. |
| Walk-forward | backtester/walkforward/ | Folds, refit, OOS aggregation, optimization and overfit diagnostics. |
| Reports | backtester/engines/ + backtester/plots/ | Text, CSV, PNG and static HTML outputs. |
| Examples | strategies/ | 50 runnable examples: weights, orders and walk-forward. |