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.
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.
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
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.
Open Command Prompt in the folder where you want to keep the project. It normally starts under
C:\Users\your-name, so the clone will be created as C:\Users\your-name\quantjourney-bt.
git clone https://github.com/QuantJourneyOrg/quantjourney-bt
cd quantjourney-bt
py -3.11 -m venv .venv
.venv\Scripts\activate.bat
python -m pip install --upgrade pip wheel
python -m pip install -e ".[data,reports]" 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]" 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.
python -c "from backtester import Backtester; print('Import OK:', Backtester.__name__)"
strategy.bat --list 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.
3. Run the verified sample
This is the shortest reliable path from a fresh clone to a generated research packet.
strategy.bat example_weights_01_sma_daily --sample-data --quiet --output reports ./strategy.sh example_weights_01_sma_daily --sample-data --quiet --output ./reports The deterministic fixture currently produces the following high-level 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.
Check the path
Open cumulative returns and portfolio drawdown. A final NAV alone can hide an unacceptable path.
Check exposure
Read weights, cash and holdings. Confirm the strategy held what you intended.
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.
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.
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
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.
copy /Y strategies\example_weights_01_sma_daily.py strategies\my_first_strategy.py
python -m py_compile strategies\my_first_strategy.py
strategy.bat my_first_strategy --sample-data --quiet 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.
REM Add QJ_API_KEY=QJ_live_... to the local .env file.
REM The same strategy, now without the deterministic sample-data switch.
strategy.bat example_weights_01_sma_daily --quiet # 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 Strategy and compute
Your Python strategy, signals, weights, orders, accounting and public report generation run locally.
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 with | Read this example |
|---|---|---|
| “Hold the top five assets equally.” | Weights | example_weights_15_cross_sectional_momentum.py |
| “Rebalance only when allocation drifts.” | Weights | example_weights_02_monthly_drift_etf.py |
| “Enter only if my limit price trades.” | Orders | example_orders_03_limit_rsi_dip.py |
| “Enter with linked take-profit and stop-loss.” | Orders | example_orders_12_bracket_trend.py |
| “Refit and evaluate on rolling OOS windows.” | Walk-forward | example_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.
REM Check that every example imports, without running backtests.
strategy.bat --all --check
REM Run all 50 examples sequentially on deterministic sample data.
strategy.bat --all --sample-data --output reports # 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.
{ "instruments": "AAPL", "execution_mode": "market", "fill_at": "next_open", "max_volume_participation": 0} 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.
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"} ╭──────────── 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 problem | Field | Message | Suggested 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
{ "instruments": ["AAPL"] { "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
{ "backtest_period": [], "initial_capital": {}, "target_volatility": "high", "max_position_size": [], "generate_plots": "sometimes"} { "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
{ "execution_mode": "market", "weight_execution": "instant", "rebalance_policy": "whenever", "fill_at": "next_open"} { "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
{ "slippage_bps": -1, "commission_bps": 1001, "max_volume_participation": 0, "max_margin_utilization": 11} { "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
{ "indicators_config": [ { "function": 123, "price_cols": "close", "params": [] } ]} { "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
{ "research_assumptions": [], "advanced": "fast", "walk_forward": false, "custom_code": {}} { "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 group | Rejected example | Accepted 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 |
The web workspace highlights these fields inline. After any field changes, the stale warning is cleared; the next run receives a new request reference.
{ "instruments": ["AAPL"], "execution_mode": "orders", "fill_at": "open", "max_volume_participation": 0.10} Troubleshooting
| Symptom | Likely cause | Check |
|---|---|---|
externally-managed-environment | Using system/Homebrew Python | Create .venv and install through its Python executable. |
| Launcher cannot import dependencies | Package installed outside the selected environment | Reinstall the editable package inside .venv. |
'strategy.bat' is not recognized | Command Prompt is outside the cloned repository | Open the cloned quantjourney-bt folder in Command Prompt, then retry. |
| Missing API key | Running a real-data path without credentials | Use --sample-data or add QJ_API_KEY to .env. |
| Empty early signals | Indicator warmup | Inspect feature NaNs and increase the pre-signal history. |
| Unexpected NAV | Timing, rebalance or cash exposure differs | Inspect weights, rebalance flags and run metadata. |