QuantJourney Backtester

QuantJourney Backtester

Share product feedback

Thank you.

Your note is now in the QuantJourney inbox.

Overview · complete engine guide

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.

Apache-2.0 engine Python 3.11–3.14 Weights and orders Sample-data mode without an account

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.

1 · inputMarket data

OHLCV, dates, universe and benchmark.

2 · featuresIndicators

SMA, RSI, ATR and strategy-ready frames.

3 · decisionWeights or orders

Desired exposure or explicit instructions.

4 · engineAccounting

Rebalance, fills, costs, cash and positions.

5 · evidenceResult packet

NAV, metrics, plots and run metadata.

The core design choice

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.

Weight mode

“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
Order mode

“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
QuestionWeight modeOrder mode
Strategy outputTarget-weight DataFrameSubmitted Order objects
StateTargets, realized weights, driftPending orders, fills, cash, positions
CostsTurnover-based weight costPer-fill commission and slippage
TimingShifted allocation, then rebalanceBar-by-bar fill rules
Best first usePortfolio researchExecution-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.

SourceSample, yfinance or QuantJourney API path
ShapeDates × instruments
Feature accessinstruments_data.get_feature(...)
ImportantWarmup values can be NaN
strategy_weights.pypython
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.

strategyRaw weights

Exposure requested by the strategy.

timingShift one bar

Avoid same-bar return capture.

controlsRisk model

Limits, vol target or risk parity.

calendarRebalance

Targets become realized weights.

accountingReturns and NAV

Cash, turnover and drift are recorded.

configure_and_run.pypython
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()
Timing is part of the 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.

strategy_orders.pypython
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)
            )
OrderEngine behaviorTypical use
MarketExecute at the configured next-bar open or close.Simple entry, exit and rebalance orders.
LimitFill only when the bar reaches the limit; open gaps can improve price.Passive entry and take-profit.
StopActivate after the stop level is crossed, including gap-through behavior.Breakout entry and protective exit.
Stop-limitStop activation followed by an at-or-better limit condition.Price-controlled breakout or protection.
Trailing stopMove the stop with favorable price movement.Dynamic loss protection.
Trailing stop-limitTrailing activation with an explicit limit constraint.Controlled trailing execution.
BracketEntry plus linked take-profit and stop-loss children.Defined reward/risk trade.
OCOA 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.

rebalance_policy.pypython
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)
Calendar

Scheduled

Daily, weekly, month-end, quarter-end, year-end or every N trading days.

State

Conditional

Drift, tracking-error and signal-change triggers can force a rebalance.

Guardrail

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.

risk.pypython
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)
execution_costs.pypython
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,
    ),
)
Risk modelsPosition limit, vol target, inverse vol, risk parity, chains
SlippageNone, fixed bps, volatility and market impact
CommissionZero, per-share, fixed bps and tiered
AuditCompare gross and net paths

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.

contract_specs.pypython
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)
Asset-class label is not a promise of market-data coverage

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.

walk_forward.pypython
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)
Use per-fold refit for honest out-of-sample evidence

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.

Summary

Metrics and text

Terminal summary, performance_report.txt, metrics.csv, summary.json and summary.txt.

Path

Portfolio artifacts

Equity curve, target or realized weights, holdings and mode-specific state.

Visual

PNG and HTML

Static plot pack plus a local dashboard.html for reviewing the run.

reports/ExampleWeights01_DailySMATrend/ ├── dashboard.html ├── performance_report.txt ├── run_metadata.json ├── summary.json ├── metrics.csv ├── equity_curve.csv ├── weights.csv └── plots/ ├── cumulative_returns.png ├── portfolio_drawdown.png ├── percentage_weights.png └── ...

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.

CapabilityStatusBoundary
Weight and order backtestingOpen sourceLocal Python engine.
50 runnable examplesOpen source25 weights, 20 orders and 5 walk-forward examples.
Text, CSV, PNG and static HTML reportsOpen sourceGenerated locally.
Cloud market-data warehouse and authenticated APIsHostedQuantJourney platform service.
Interactive sharing, PDF factsheets and extended diagnosticsHostedNot part of the public package.
VWAP weight executionReservedThe enum exists; execution-aware mode rejects it.
CPCV fold schemePlannedCurrent public implementation is a placeholder.
Tick/queue simulation and live broker OMSNon-goalOutside 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.

AreaPublic repositoryResponsibility
Strategy lifecyclebacktester/core.pyData preparation, hook dispatch, mode selection and portfolio result.
Instrument databacktester/portfolio/instr_data.pyPrices, returns and generated indicator features.
Weight accountingbacktester/portfolio/rebalance.pyTarget weights, realized weights, drift and rebalance flags.
Orders and fillsbacktester/execution/Order state, OHLC triggers, fills, commissions and slippage.
Risk overlaysbacktester/risk/Position limits, volatility target, inverse volatility and risk parity.
Walk-forwardbacktester/walkforward/Folds, refit, OOS aggregation, optimization and overfit diagnostics.
Reportsbacktester/engines/ + backtester/plots/Text, CSV, PNG and static HTML outputs.
Examplesstrategies/50 runnable examples: weights, orders and walk-forward.

Where to go next