QuantJourney Backtester

QuantJourney Backtester

Share product feedback

Thank you.

Your note is now in the QuantJourney inbox.

How to Start · parallel preview

Run first. Understand every layer next.

Begin with deterministic sample data: no account, API key or external download required. You will produce a complete local report, inspect how signals become weights, and then switch the same strategy to real market data.

Verified on v0.11.0 First run needs no credentials Produces local HTML and PNG reports
Run locally

Choose your operating system

The commands below update when you change the selected system.

Choose your first path

There are two data paths, but only one strategy interface. Start offline so installation and engine behavior are isolated from authentication and network issues.

Recommended first run

Deterministic sample data

Bundled, reproducible data intended for installation checks, report generation and reading the engine flow.

  • No account or API key
  • No network data dependency
  • Same result on repeated runs
After the sample works

Real market data

Current real-data workflows, including Yahoo Finance, use the authenticated QJ API path and require approved Backtester beta access plus QJ credentials.

  • Real dates and instruments
  • Authenticated cloud API path
  • Normal caching and run persistence

1. Install the repository

Clone the repository when you want the full strategy catalog and native strategy.bat or strategy.sh launcher. Installing only the wheel gives you the Python library, but not all repository examples.

terminalmacOS / Linux
git clone https://github.com/QuantJourneyOrg/quantjourney-bt
cd quantjourney-bt

python3 -m venv .venv
source .venv/bin/activate

python -m pip install -U pip wheel
python -m pip install -e ".[data,reports]"
Supported Python

The current package declares Python 3.11, 3.12, 3.13 and 3.14. Always use a virtual environment. The Windows commands activate .venv with the standard activate.bat; WSL and Git Bash are not required.

2. Verify the installation

Confirm that the package imports and inspect the real strategy catalog before executing anything.

terminalmacOS / Linux
python - <<'PY'
from backtester import Backtester
from backtester.version import __version__

print("QuantJourney Backtester:", __version__)
print("Import OK:", Backtester.__name__)
PY

./strategy.sh --list

The repository currently includes 50 examples: 25 weight strategies, 20 order strategies and 5 walk-forward or optimization workflows. The filename prefix tells you which engine contract the example teaches.

example_weights_*Signals and target allocations
example_orders_*Explicit orders and fill state
example_wf_*Validation and optimization
--checkImport without running a backtest

3. Run the verified sample

This is the shortest reliable path from a fresh clone to a generated research packet.

terminalmacOS / Linux
./strategy.sh example_weights_01_sma_daily --sample-data --quiet --output ./reports

The deterministic fixture currently produces the following high-level result:

Running strategy: example_weights_01_sma_daily Backtester: v0.11.0 Data: deterministic sample data Strategy: ExampleWeights01_DailySMATrend Initial NAV: $100,000.00 Final NAV: $255,491.91 Total Return: 155.49% Max Drawdown: -7.17% Reports saved under: reports/ExampleWeights01_DailySMATrend/
This is an installation fixture, not an investment result

The sample dataset is intentionally deterministic and small. Its Sharpe or return should never be used to judge the strategy’s economic quality. The point is to verify that data, accounting and reporting complete successfully.

4. Inspect the evidence packet

A successful run is not only one terminal number. Open the report directory and follow the result from summary to path, drawdown, exposure and diversification.

reports/ExampleWeights01_DailySMATrend/ ├── dashboard.html # local report index ├── performance_report.txt # readable metric summary ├── run_metadata.json # run context ├── summary.json # machine-readable headline result ├── metrics.csv # metric table ├── equity_curve.csv # portfolio path ├── weights.csv # allocation history └── plots/ ├── cumulative_returns.png ├── portfolio_drawdown.png ├── percentage_weights.png ├── monthly_returns_heatmap.png └── ...
See the published evidence packet These plots come from the published SMA example. Your local sample run produces the same report structure and plot types.
First

Check the path

Open cumulative returns and portfolio drawdown. A final NAV alone can hide an unacceptable path.

Then

Check exposure

Read weights, cash and holdings. Confirm the strategy held what you intended.

Finally

Check assumptions

Confirm dates, source, rebalance, costs and execution mode in metadata.

5. Understand the strategy

The first example uses weight mode. It answers two separate questions: when is an asset eligible, and how much of the portfolio should it receive?

Signals describe the idea

SMA(50) above SMA(200) produces a long signal. Warmup dates are masked so missing indicators never become accidental trades.

signals.pypython
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)

Weights describe portfolio intent

Active names share capital equally, subject to a 25% cap. If fewer than four names are active, residual exposure stays in cash.

weights.pypython
    def _compute_weights(self) -> pd.DataFrame:
        active = self.signals == 1.0
        counts = active.sum(axis=1)

        # Equal weight active assets, capped at 25% each.
        # Any unallocated exposure remains cash.
        return active.div(counts, axis=0).fillna(0.0).clip(upper=0.25)

Configuration makes assumptions explicit

configuration.pypython
strategy = DailySMATrend(
    strategy_name="ExampleWeights01_DailySMATrend",
    initial_capital=100_000,
    instruments=["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
    backtest_period={"start": "2015-01-01", "end": "2025-01-01"},
    source="sample" if sample_mode else "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,
)

After the hook returns, the engine shifts weight decisions by one bar, applies optional risk controls, evaluates the rebalance policy, lets non-rebalanced weights drift and then calculates portfolio returns.

6. Create your first variation

Begin from a tested example rather than a blank file. Make one conceptual change at a time so you can attribute differences in the result.

terminalmacOS / Linux
cp strategies/example_weights_01_sma_daily.py strategies/my_first_strategy.py

python -m py_compile strategies/my_first_strategy.py

./strategy.sh my_first_strategy --sample-data --quiet

Rename the strategy

Change the class and strategy_name so reports do not overwrite or mix with the example.

Change one hypothesis

For example, change SMA periods from 50/200 to 20/100 in both indicator configuration and feature names.

Run with the same data

Keep universe, dates and costs fixed. Compare the new path, turnover and drawdown against the baseline.

Inspect causality

Check warmup, signal timing and rebalance dates before interpreting better performance.

7. Move to real market data

Once the deterministic run works, request Backtester beta access. After approval, create a Backtester API key for the authenticated real-data path. Put the key in the local .env file—never paste it into a strategy or commit it. Both launchers load the same file safely.

terminalmacOS / Linux
# Add QJ_API_KEY=QJ_live_... to the local .env file.

# The same strategy, now without the deterministic sample-data switch.
./strategy.sh example_weights_01_sma_daily --quiet
Local

Strategy and compute

Your Python strategy, signals, weights, orders, accounting and public report generation run locally.

Hosted

Platform services

Authenticated market-data APIs, orchestration, hosted sharing, PDF factsheets and extended report packs belong to the hosted platform.

8. Move from weights to orders

Do this only if fill state matters. For a ranked monthly allocation, weight mode is the natural contract. For a limit entry with a protective bracket, order mode is the natural contract.

If the strategy says…Start withRead this example
“Hold the top five assets equally.”Weightsexample_weights_15_cross_sectional_momentum.py
“Rebalance only when allocation drifts.”Weightsexample_weights_02_monthly_drift_etf.py
“Enter only if my limit price trades.”Ordersexample_orders_03_limit_rsi_dip.py
“Enter with linked take-profit and stop-loss.”Ordersexample_orders_12_bracket_trend.py
“Refit and evaluate on rolling OOS windows.”Walk-forwardexample_wf_01_rolling_walkforward.py

9. Check or run the catalog

The launcher can import-check every strategy or run the entire catalog sequentially. Batch output includes a summary and per-strategy logs.

terminalmacOS / Linux
# Check that every example imports, without running backtests.
./strategy.sh --all --check

# Run all 50 examples sequentially on deterministic sample data.
./strategy.sh --all --sample-data --output ./reports

10. Read a rejected configuration

The web workspace validates POST /bt/run before creating a run. The installed Python package also preserves field errors returned later by qj-api from POST /bt/prepare. Both paths identify the affected field and supply a correction hint plus a request reference.

HTTP status422 · configuration rejected
Stable codeBT_RUN_CONFIG_INVALID / ERR_VAL_003
DetailsEvery failing field in field_errors / errors
ReferenceX-Request-ID and request_id
invalid request · POST /bt/runJSON
{  "instruments": "AAPL",  "execution_mode": "market",  "fill_at": "next_open",  "max_volume_participation": 0}
real response from the current validatorHTTP 422
HTTP/1.1 422 Unprocessable EntityX-Request-ID: 6b2f2d9a-99a1-4db7-9db4-b7e197622e2c {  "detail": "Backtest configuration is invalid.",  "message": "Instruments must be a list.",  "error_code": "BT_RUN_CONFIG_INVALID",  "request_id": "6b2f2d9a-99a1-4db7-9db4-b7e197622e2c",  "field_errors": [    {      "field": "instruments",      "code": "list_type",      "message": "Instruments must be a list.",      "hint": "Select one or more symbols and send them as a list, for example ["AAPL", "MSFT"]."    },    {      "field": "execution_mode",      "code": "literal_error",      "message": "Execution mode contains an unsupported option.",      "hint": "Choose template, weights or orders."    },    {      "field": "fill_at",      "code": "literal_error",      "message": "Fill timing contains an unsupported option.",      "hint": "Choose open or close."    },    {      "field": "max_volume_participation",      "code": "greater_than",      "message": "Volume participation must be greater than 0.0.",      "hint": "Use a decimal greater than 0 and at most 1; 0.10 means 10%."    }  ]}

One rejected request can return several entries. Fix every item in field_errors, then submit the configuration again.

When the Python package reaches /bt/prepare

Data preparation has its own server-side checks for fields such as source, granularity, date range, instruments and request size. qj-api returns errors; since version 0.12.3 the package renders the same entries as a field-level correction panel instead of a generic HTTP 422. In a real terminal the title, border and affected field are yellow. Normal output contains no raw response or traceback.

real qj-api response · POST /bt/prepareHTTP 422
HTTP/1.1 422 Unprocessable EntityX-Request-ID: 9ee3e7ae-15d7-415e-a853-13ea4fb56f13 {  "type": "https://api.quantjourney.cloud/errors/validation/invalid-field",  "title": "Invalid Field Value",  "status": 422,  "detail": "The 1-minute interval supports at most 7 calendar days per request.",  "error_code": "ERR_VAL_003",  "errors": [    {      "field": "trading_range",      "code": "range_too_long",      "message": "The selected date range is too long for 1-minute data.",      "hint": "Use a range of 7 calendar days or less. For a longer period, choose 5m, 15m, 1h or 1d."    }  ],  "instance": "/bt/prepare",  "request_id": "9ee3e7ae-15d7-415e-a853-13ea4fb56f13"}
real terminal output · yellow panelquantjourney-bt 0.12.4
╭──────────── Configuration needs attention ────────────╮│ The strategy stopped before market data was prepared. ││ Fix the configuration below and run it again.         ││                                                       ││ 1. Date range                                         ││    The selected date range is too long for            ││    1-minute data.                                     ││    Suggested fix: Use a range of 7 calendar days      ││    or less. For a longer period, choose 5m, 15m,      ││    1h or 1d.                                          ││                                                       ││ No trades were executed and no report was created.    ││ Request 9ee3e7ae-15d7-415e-a853-13ea4fb56f13          ││ Error ERR_VAL_003                                     │╰───────────────────────────────────────────────────────╯

Common preparation errors and exact corrections

These are the six production cases exercised against POST /bt/prepare. The rejected value is not repeated in the response.

Configuration problemFieldMessageSuggested fix
No instruments selected instruments No instruments were selected. Add at least one symbol, for example AAPL or MSFT.
More than 50 instruments instruments A single backtest can use at most 50 instruments. Remove some symbols and run the strategy again.
Unsupported data interval provider.granularity The selected data interval is not supported. Choose 1d, 1h, 90m, 30m, 15m, 5m, 2m or 1m.
Too many days for 1-minute data trading_range The selected date range is too long for 1-minute data. Use 7 calendar days or less, or choose 5m, 15m, 1h or 1d.
Backtest result would be too large configuration This backtest would return too much market data in one run. Use fewer instruments or shorter dates; for the same dates choose 5m, 15m, 1h or 1d.
Invalid start date backtest_period.start The start date is not a valid calendar date. Use YYYY-MM-DD, for example 2026-06-01.

More invalid configurations

These examples cover malformed JSON, wrong JSON types, unsupported options, range violations, nested indicators and structured strategy sections. Open a case to compare the rejected request with the exact field_errors returned for it.

Malformed JSON bodyThe endpoint cannot parse the submitted body as a JSON object. 1 field error
rejected requestJSON
{ "instruments": ["AAPL"]
response excerptfield_errors
{  "field_errors": [    {      "field": "request",      "code": "json_invalid",      "message": "The request body must be a valid JSON object.",      "hint": "Send a JSON object containing the backtest configuration."    }  ]}
Wrong shapes and value typesObjects, numbers and booleans were sent using incompatible JSON types. 5 field errors
rejected requestJSON
{  "backtest_period": [],  "initial_capital": {},  "target_volatility": "high",  "max_position_size": [],  "generate_plots": "sometimes"}
response excerptfield_errors
{  "field_errors": [    {      "field": "backtest_period",      "code": "dict_type",      "message": "Backtest period must be an object.",      "hint": "Use an object with ISO dates, for example {\"start\":\"2020-01-01\",\"end\":\"2025-01-01\"}."    },    {      "field": "initial_capital",      "code": "float_type",      "message": "Initial capital must be a valid number.",      "hint": "Enter a numeric amount greater than zero, for example 100000."    },    {      "field": "target_volatility",      "code": "float_parsing",      "message": "Target volatility must be a valid number.",      "hint": "Use a decimal value, for example 0.15 for 15%."    },    {      "field": "max_position_size",      "code": "float_type",      "message": "Maximum position size must be a valid number.",      "hint": "Use a decimal value, for example 0.20 for 20%."    },    {      "field": "generate_plots",      "code": "bool_parsing",      "message": "Generate plots must be true or false.",      "hint": "Use true or false."    }  ]}
Unsupported engine optionsEach option must be one of the values supported by the run contract. 4 field errors
rejected requestJSON
{  "execution_mode": "market",  "weight_execution": "instant",  "rebalance_policy": "whenever",  "fill_at": "next_open"}
response excerptfield_errors
{  "field_errors": [    {      "field": "execution_mode",      "code": "literal_error",      "message": "Execution mode contains an unsupported option.",      "hint": "Choose template, weights or orders."    },    {      "field": "weight_execution",      "code": "literal_error",      "message": "Weight execution contains an unsupported option.",      "hint": "Choose fast or orders."    },    {      "field": "rebalance_policy",      "code": "literal_error",      "message": "Rebalance policy contains an unsupported option.",      "hint": "Choose daily, weekly, monthly, quarterly, yearly, monthly_drift, monthly_partial, risk_managed, signal_driven, tax_aware or institutional."    },    {      "field": "fill_at",      "code": "literal_error",      "message": "Fill timing contains an unsupported option.",      "hint": "Choose open or close."    }  ]}
Values outside allowed rangesCosts and execution limits are numeric, but their values still have boundaries. 4 field errors
rejected requestJSON
{  "slippage_bps": -1,  "commission_bps": 1001,  "max_volume_participation": 0,  "max_margin_utilization": 11}
response excerptfield_errors
{  "field_errors": [    {      "field": "slippage_bps",      "code": "greater_than_equal",      "message": "Slippage must be at least 0.0.",      "hint": "Use a number from 0 to 1000 basis points."    },    {      "field": "commission_bps",      "code": "less_than_equal",      "message": "Commission must be at most 1000.0.",      "hint": "Use a number from 0 to 1000 basis points."    },    {      "field": "max_volume_participation",      "code": "greater_than",      "message": "Volume participation must be greater than 0.0.",      "hint": "Use a decimal greater than 0 and at most 1; 0.10 means 10%."    },    {      "field": "max_margin_utilization",      "code": "less_than_equal",      "message": "Margin utilization must be at most 10.0.",      "hint": "Use a decimal greater than 0 and at most 10; 1.0 means 100%."    }  ]}
Malformed indicator configurationNested errors retain the indicator index and exact property path. 3 field errors
rejected requestJSON
{  "indicators_config": [    {      "function": 123,      "price_cols": "close",      "params": []    }  ]}
response excerptfield_errors
{  "field_errors": [    {      "field": "indicators_config.0.function",      "code": "string_type",      "message": "Indicators configuration (0.function) must be text.",      "hint": "Send a list of indicator objects with function, price_cols and params."    },    {      "field": "indicators_config.0.price_cols",      "code": "list_type",      "message": "Indicators configuration (0.price_cols) must be a list.",      "hint": "Send a list of indicator objects with function, price_cols and params."    },    {      "field": "indicators_config.0.params",      "code": "dict_type",      "message": "Indicators configuration (0.params) must be an object.",      "hint": "Send a list of indicator objects with function, price_cols and params."    }  ]}
Structured sections and custom sourceConfiguration sections must remain objects, while custom strategy source must remain text. 4 field errors
rejected requestJSON
{  "research_assumptions": [],  "advanced": "fast",  "walk_forward": false,  "custom_code": {}}
response excerptfield_errors
{  "field_errors": [    {      "field": "research_assumptions",      "code": "dict_type",      "message": "Research assumptions must be an object.",      "hint": "Send research assumptions as a JSON object."    },    {      "field": "advanced",      "code": "dict_type",      "message": "Advanced configuration must be an object.",      "hint": "Send advanced configuration as a JSON object."    },    {      "field": "walk_forward",      "code": "dict_type",      "message": "Walk-forward configuration must be an object.",      "hint": "Send walk-forward configuration as a JSON object."    },    {      "field": "custom_code",      "code": "string_type",      "message": "Custom strategy code must be text.",      "hint": "Send custom strategy code as plain text."    }  ]}

Complete validation checklist

This covers every part of the run configuration currently checked by the request contract, including grouped free-text fields that share the same rule.

Field or groupRejected exampleAccepted shape or value
request body Malformed JSON or a top-level list One JSON object: { ... }
id, strategy_name, strategy_type, strategy_template Object, list or boolean Text; optional fields may be omitted or null
instruments "AAPL" or mixed non-text items ["AAPL", "MSFT"]
backtest_period List, text or non-text date values {"start":"2020-01-01","end":"2025-01-01"}
initial_capital, target_volatility, max_position_size Object, list or non-numeric text JSON numbers such as 100000, 0.15 and 0.20
source, granularity, base_currency Object, list or boolean Text such as "yfinance", "1d" and "USD"
benchmark_symbol, benchmark_name, benchmark_enabled Non-text symbol/name or unparseable boolean "SPY", "S&P 500" and true/false
indicators_config Not a list, or malformed nested function/price_cols/params [{"function":"SMA","price_cols":["close"],"params":{}}]
execution_mode Any other option template, weights or orders
weight_execution Any other option fast or orders
rebalance_policy Any other option daily, weekly, monthly, quarterly, yearly and documented advanced policies
fill_at Any other option open or close
slippage_bps, commission_bps Below 0 or above 1000 A number from 0 through 1000
max_volume_participation 0, a negative value or above 1 null, or a number greater than 0 and at most 1
max_margin_utilization 0, a negative value or above 10 null, or a number greater than 0 and at most 10
plot_theme, generate_plots Non-text theme or unparseable boolean Theme text and true/false
research_assumptions, advanced, walk_forward List, text or boolean A JSON object for each section
custom_code, custom_code_filename Object, list or boolean Text, null or an omitted field
A corrected version of the same configuration

The web workspace highlights these fields inline. After any field changes, the stale warning is cleared; the next run receives a new request reference.

corrected requestJSON
{  "instruments": ["AAPL"],  "execution_mode": "orders",  "fill_at": "open",  "max_volume_participation": 0.10}

Troubleshooting

SymptomLikely causeCheck
externally-managed-environmentUsing system/Homebrew PythonCreate .venv and install through its Python executable.
Launcher cannot import dependenciesPackage installed outside the selected environmentReinstall the editable package inside .venv.
'strategy.bat' is not recognizedCommand Prompt is outside the cloned repositoryOpen the cloned quantjourney-bt folder in Command Prompt, then retry.
Missing API keyRunning a real-data path without credentialsUse --sample-data or add QJ_API_KEY to .env.
Empty early signalsIndicator warmupInspect feature NaNs and increase the pre-signal history.
Unexpected NAVTiming, rebalance or cash exposure differsInspect weights, rebalance flags and run metadata.

Next steps