Convert backtest to optimiser
SkillCommerce & financeConvert a backtesting notebook into a parameter optimisation notebook using the bundled transformation mapping to choose searchable parameters and rewrite the notebook.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Convert backtest to optimiser skill
What this skill tells your AI
The instructions your AI receives, as published by tradingstrategy-ai/getting-started in .claude/skills/convert-to-optimiser/SKILL.md and read by ahel’s review.
Convert a single backtesting notebook into an optimiser notebook that optimises strategy parameters using scikit-optimize's Gaussian Process.
Input
- Path to a backtesting notebook (.ipynb)
Reference files
- Reference optimiser notebook:
getting-started/scratchpad/vault-of-vaults/32-waterfall-diversified-larger-universe-grid-search-4d-rebalance-profit.ipynb - Reference backtest notebook:
getting-started/scratchpad/bnb-ath-2/11-bnb-ath-2h-rerun.ipynb
Steps
-
Read the input backtest notebook and the cell-by-cell mapping section below.
-
Read the reference optimiser notebook (
getting-started/scratchpad/vault-of-vaults/32-waterfall-diversified-larger-universe-grid-search-4d-rebalance-profit.ipynb) — you will copy output cells from this. -
Identify tunable parameters: Look at the
Parametersclass in the backtest notebook. Identify strategy-specific parameters that could be searched (signal or indicator params, portfolio sizing, risk params). Exclude structural parameters likechain_id,exchanges,candle_time_bucket,cycle_duration,routing,backtest_start/end,initial_cash,min_volume,min_tvl, rolling window calculations, and yield settings. -
Ask the user which parameters to make searchable and what value ranges to use. Suggest ranges centred on the current fixed values. The original value must be included in every range.
-
Create the optimiser notebook with these cells in order:
a. Title cell (markdown): Change title to
# {Strategy name} parameter searchb. Setup cell (code): Copy as-is from backtest
c. Chain config cell (code): Copy as-is from backtest
d. Parameters cell (code): Transform from the backtest version:
- Add
from skopt.space import Categoricalto imports - Replace chosen tunable parameters with
Categorical([...])ranges - Set
use_managed_yield = False - Add at the end:
from tradeexecutor.strategy.parameters import display_parameters display_parameters(parameters)
e. Trading universe cell (code): Copy as-is from backtest (the
create_trading_universe()function)f. Token map cell (code): Copy as-is from backtest
g. Indicators cell (code): Copy from backtest but remove the
calculate_and_load_indicators_inline()call at the end of the cell (and its preceding comment). Keepdisplay_indicators(indicators)if present. The optimiser calculates indicators itself for each parameter combination — callingcalculate_and_load_indicators_inline()with Categorical parameter values will crash withAssertionError: Detected scikit-optimize Dimension as a parameter value.h. Backtest time range cell (markdown + code): Copy from backtest, but remove any reference to
indicator_datasince that variable no longer exists (the inline indicator calculation was removed in step g). Simplify to useParameters.backtest_startandParameters.backtest_enddirectly. Example:backtest_start = Parameters.backtest_start backtest_end = Parameters.backtest_end print(f"Time range is {backtest_start} - {backtest_end}")i. Strategy cell (code): Copy
decide_trades()(andcreate_yield_rules()if present) from the backtest. Important: Ifrun_backtest_inline()is in the same cell asdecide_trades(), remove therun_backtest_inline()call and everything after it. The strategy function must be in its own cell without the backtest execution.numpy.float64 fix: scikit-optimize's
Categoricalreturnsnumpy.float64instead of native Pythonfloat. Ifdecide_trades()passes any Categorical float parameter toalpha_model.normalise_weights()asmax_weight, the waterfall code path will crash withAssertionError: Got <class 'numpy.float64'> instead of float. Wrap such values withfloat(). For example, change:max_weight = parameters.max_concentrationto:
max_weight = float(parameters.max_concentration)Apply the same
float()cast to any other parameter value that ends up passed to a function expecting a native Pythonfloat(common withmax_weight,per_position_cap, etc.).j. Optimiser and output cells: Copy cells from the reference optimiser notebook (
32-waterfall-diversified-larger-universe-grid-search-4d-rebalance-profit.ipynb). These are heading + code cell pairs:Heading Cells # Optimisermarkdown + code # Resultsmarkdown + code ## Equity curvesmarkdown + code # Parameter analysismarkdown only ## Decision tree visualisationmarkdown + code ## Feature importance analysismarkdown + code ## Heatmaps for parameter pairsmarkdown + code ## Cluster analysismarkdown + code ## Parallel coordinates plotmarkdown + code # The best candidate equity curvemarkdown + code # Portfolio performance (best pick)markdown + code # Trade summary (best pick)markdown + code # Trading pair performance breakdownmarkdown + code # Best positionsmarkdown + code # Rolling sharpemarkdown + code # Data diagnosticsmarkdown + 3 code cells Equity curves cell: The
## Equity curvescode cell overlays all optimiser results on a single chart with buy-and-hold benchmarks. It must extractGridSearchResultinstances from the optimiser results and pass them tovisualise_grid_search_equity_curves. Usegroup_byandgroup_by_secondaryto colour and style curves by parameter values — pick two of the searchableCategoricalparameters from the strategy'sParametersclass:from tradeexecutor.visual.grid_search_basic import visualise_grid_search_equity_curves from tradeexecutor.analysis.multi_asset_benchmark import get_benchmark_data benchmark_indexes = get_benchmark_data( strategy_universe, cumulative_with_initial_cash=Parameters.initial_cash, ) grid_search_results = [r.result for r in optimiser_result.results if not r.filtered] fig = visualise_grid_search_equity_curves( grid_search_results, benchmark_indexes=benchmark_indexes, log_y=False, group_by="<primary_categorical_param>", # colour family per value group_by_secondary="<secondary_categorical_param>", # dash style per value ) fig.show()Do not copy any cells from the backtest that are not listed above (no chart registry, no pre-backtest visualisations, no single-backtest output cells like equity curves, weight charts, positions, trading metrics, vault performance).
- Add
-
Write the output notebook as
{original-notebook-name}-optimiser.ipynbin the same directory as the input notebook. -
Verify the notebook structure:
- Parameters class has
Categoricalvalues for the chosen searchable params create_trading_universe()anddecide_trades()are unchanged from the backtest- Optimiser cell uses
perform_optimisationwithprepare_optimiser_parameters(Parameters) - All output cells match the reference optimiser structure
- Test run with the observable runner:
poetry run jupyter-execute-agent {output-notebook}.ipynb(omit--timeoutfor long optimisers). See notebook-execution.md.
- Parameters class has
Cell-by-cell mapping
Reference notebooks (in getting-started/scratchpad/bnb-ath-2/):
- Backtest:
11-bnb-ath-2h-rerun.ipynb - Optimiser:
10-bnb-ath-optimise-sharpe.ipynb
What stays the same
These cells are copied verbatim from the backtest notebook:
- Setup cell:
Client.create_jupyter_client()+setup_charting_and_output() - Chain config cell: Exchange/pair/vault configuration
- Trading universe cell:
create_trading_universe()function - Token map cell:
token_map,benchmark_pair_ids,category_pair_ids - Indicators cell: All indicator functions (
local_high,full_history_ath,volatility,signal, etc.) — but see "Remove inline indicator calculation" below - Backtest time range cell: Start/end date calculation — but see "Fix indicator_data references" below
decide_trades()function: Core strategy logic (but separated fromrun_backtest_inlinecall) — but see "numpy.float64 cast" below
What changes
1. Title cell
- Backtest:
# BNB local high strategywith description - Optimiser:
# {Strategy name} parameter search
2. Parameters cell
Add import:
from skopt.space import Categorical
Wrap tunable strategy parameters in Categorical([...]). The original value must be included in the list. Example:
# Backtest (fixed)
local_high_delay_bars = 12
local_high_window_bars = 2160
# Optimiser (searchable)
local_high_delay_bars = Categorical([12, 60, 144, 200, 360, 500])
local_high_window_bars = Categorical([10, 25, 50, 100, 150, 360, 800, 720, 2160, 4320])
Other changes:
- Set
use_managed_yield = False - Add at end:
display_parameters(parameters)(with importfrom tradeexecutor.strategy.parameters import display_parameters)
Parameters that are not searchable (structural):
id,candle_time_bucket,cycle_duration,chain_id,exchangesrolling_volume_bars,rolling_volatility_bars,tvl_ewm_spanmin_volume,min_tvl,min_tvl_prefilter,min_token_sniffer_scorerouting,required_history_period,slippage_tolerancebacktest_start,backtest_end,initial_cashyield_flow_dust_threshold,directional_trade_yield_buffer_pctassummed_liquidity_when_data_missings
Parameters typically made searchable:
- Signal/indicator parameters (delay, window, threshold, span)
- Portfolio sizing parameters (max_assets, allocation, max_concentration)
- Risk parameters (stop_loss, per_position_cap)
- Filter thresholds (min_from_full_history_ath)
3. Remove inline indicator calculation
The backtest indicators cell typically ends with:
indicator_data = calculate_and_load_indicators_inline(
strategy_universe=strategy_universe,
create_indicators=indicators.create_indicators,
parameters=parameters,
)
Remove this call (and any preceding comment like # Calculate all indicators...). The optimiser calculates indicators for each parameter combination internally. Passing Categorical parameter values to calculate_and_load_indicators_inline() causes:
AssertionError: Detected scikit-optimize Dimension as a parameter value: rolling_returns_bars: Categorical(...)
Keep display_indicators(indicators) if present — it only describes the indicator definitions, it does not calculate them.
4. Fix indicator_data references in time range cell
The backtest time range cell may reference indicator_data (e.g. indicator_data.get_indicator_series(...)). Since indicator_data no longer exists after step 3, simplify the cell to use static Parameters values:
backtest_start = Parameters.backtest_start
backtest_end = Parameters.backtest_end
print(f"Time range is {backtest_start} - {backtest_end}")
5. Remove charts cell and pre-backtest visualisations
The backtest notebook has:
- Charts cell:
ChartRegistrysetup with 20+ chart registrations - Pre-backtest visualisation cells: available pairs, inclusion criteria, signals, volatility, price vs signal, local high
All of these are removed from the optimiser notebook.
6. Replace run_backtest_inline with perform_optimisation
The backtest runs a single backtest:
result = run_backtest_inline(
name=parameters.id,
engine_version="0.5",
decide_trades=decide_trades,
...
)
state = result.state
The optimiser replaces this with:
from tradeexecutor.backtest.optimiser import perform_optimisation, prepare_optimiser_parameters, MinTradeCountFilter
from tradeexecutor.backtest.optimiser_functions import optimise_sharpe
iterations = 4
search_func = optimise_sharpe
optimiser_result = perform_optimisation(
iterations=iterations,
search_func=search_func,
decide_trades=decide_trades,
strategy_universe=strategy_universe,
parameters=prepare_optimiser_parameters(Parameters),
create_indicators=indicators.create_indicators,
result_filter=MinTradeCountFilter(50),
timeout=70*60,
batch_size=5,
ignore_wallet_errors=True,
)
7. Cast numpy.float64 to float in decide_trades
scikit-optimize's Categorical returns numpy.float64 for float values, not native Python float. The alpha_model.normalise_weights() waterfall code path has a strict type check:
assert type(max_weight) == float, f"Got {type(max_weight)} instead of float"
If decide_trades() passes a Categorical float parameter (e.g. parameters.max_concentration) as max_weight, wrap it with float():
# Before (crashes with numpy.float64)
max_weight = parameters.max_concentration
# After
max_weight = float(parameters.max_concentration)
Apply the same float() cast to any parameter value passed to functions that assert native Python types. Common cases: max_weight, per_position_cap, allocation.
8. Replace backtest output cells with optimiser analysis cells
All backtest output cells (performance metrics, equity curves, weights, positions, trading metrics, vault performance) are removed.
Replaced with these heading + code cell pairs (copied from reference optimiser notebook cells 18-48):
| # | Heading | Description |
|---|---|---|
| 1 | # Optimiser | perform_optimisation() call |
| 2 | # Results | analyse_optimiser_result() + render_grid_search_result_table() |
| 3 | # Parameter analysis | Section heading only |
| 4 | ## Decision tree visualisation | sklearn DecisionTreeRegressor on parameter combinations |
| 5 | ## Feature importance analysis | sklearn RandomForestRegressor feature importances |
| 6 | ## Heatmaps for parameter pairs | plotly go.Heatmap for parameter pair interactions |
| 7 | ## Cluster analysis | sklearn KMeans + PCA 3D scatter |
| 8 | ## Parallel coordinates plot | plotly px.parallel_coordinates |
| 9 | # The best candidate equity curve | visualise_single_grid_search_result_benchmark() |
| 10 | # Portfolio performance (best pick) | compare_strategy_backtest_to_multiple_assets() |
| 11 | # Trade summary (best pick) | best_pick.summary.to_dataframe() |
| 12 | # Trading pair performance breakdown | analyse_multipair() |
| 13 | # Best positions | Top 5 positions sorted by profit |
| 14 | # Rolling sharpe | calculate_rolling_sharpe() with 180-day window |
| 15 | # Data diagnostics | Commented-out debug cells |
Signals
- GitHub stars
- 247
- Forks
- 39
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
convert-to-optimiser- Source
- github.com/tradingstrategy-ai/getting-started