Optimize
The optimize() research function lets you run hyperparameter optimization from a Python script or Jupyter notebook — no dashboard required. It mirrors how monte_carlo_trades() and backtest() work: you prepare candle data, configure the run, call the function, and inspect a structured result.
Under the hood it uses Optuna for parameter search and Ray for parallel trial execution — exactly like the dashboard's optimization mode — but without any session, database, or WebSocket dependencies.
When to Use
- Scripted batch runs — automate optimization across multiple symbols, timeframes, or strategy variants from a single Python script.
- Jupyter workflows — explore parameter sensitivity interactively without leaving the notebook.
- CI pipelines — gate strategy deployments on minimum fitness or Sharpe thresholds by running optimization as part of an automated test suite.
- Chaining with Monte Carlo — after finding good parameters with
optimize(), validate robustness withmonte_carlo_trades()ormonte_carlo_candles()before committing to live trading.
Function Signature
from jesse.research import optimize
result = optimize(
config: dict,
routes: list,
data_routes: list,
training_candles: dict,
training_warmup_candles: dict,
testing_candles: dict,
testing_warmup_candles: dict,
optimal_total: int = 200,
fast_mode: bool = True,
cpu_cores: Optional[int] = None,
trials: int = 200,
objective_function: str = 'sharpe',
best_candidates_count: int = 20,
progress_bar: bool = True,
) -> dictParameters
config(dict): Strategy and exchange configuration. See Config Structure below for the exact shape — it differs from theconfigdict used bybacktest().routes(list): Trading routes in the same format asbacktest(). Each entry is a dict with'symbol','timeframe', and'strategy'keys. The'exchange'key is injected automatically fromconfig['exchange']['name'], so you do not need to set it manually.data_routes(list): Additional data routes (e.g. higher timeframes) that the strategy reads viaself.get_candles()but does not trade on. Same format asbacktest(). Pass an empty list if not needed.training_candles(dict): Candle data for the training period — the period the optimizer actively tunes parameters against. Keyed byjh.key(exchange, symbol).training_warmup_candles(dict): Warmup candle data corresponding to the training period. Same key format.testing_candles(dict): Candle data for the testing / validation period. This period is completely held out and is never seen during the parameter search — it is used only to measure how well the optimized parameters generalize.testing_warmup_candles(dict): Warmup candle data corresponding to the testing period.optimal_total(int, default200): Target number of trades used in fitness normalisation. Trials with fewer than 5 trades score near zero; trials whose trade count approachesoptimal_totalreceive full credit for the trade-count component. Set this to roughly the number of trades your strategy is expected to produce over the training period.fast_mode(bool, defaultTrue): Enables the fast backtest engine. Keep thisTruefor optimization — it is the same engine used by the dashboard and is orders of magnitude faster than the standard engine.cpu_cores(int, optional): Number of parallel Ray workers to use. Defaults to 80% of available CPU cores on the machine.trials(int, default200): Number of trials per hyperparameter. The actual total number of Optuna trials that will run istrials × number_of_hyperparameters. This matches the behaviour of the dashboard's optimization mode. For a strategy with 3 hyperparameters and the default of200, that is 600 trials total; lower this (e.g.trials=40) for a quick exploratory run.objective_function(str, default'sharpe'): The metric each trial is optimised to maximise. Accepted values:'sharpe','calmar','sortino','omega'.best_candidates_count(int, default20): How many top-scoring trials to keep and return inresult['best_trials'].progress_bar(bool, defaultTrue): Show atqdmprogress bar in the terminal while trials are running.
Config Structure
The config dict passed to optimize() has a different shape from the flat dict used by backtest(). It wraps exchange settings inside a nested 'exchange' key:
config = {
'exchange': {
'name': 'Binance Perpetual Futures', # exchange name constant
'balance': 10_000, # starting balance in USDT
'fee': 0.0007, # taker fee as a decimal
'type': 'futures', # 'futures' or 'spot'
'futures_leverage': 2,
'futures_leverage_mode': 'cross', # 'cross' or 'isolated'
},
'warm_up_candles': 210, # warmup bars before trading begins
}TIP
For 'type': 'spot' strategies you can omit futures_leverage and futures_leverage_mode — they are ignored for spot accounts.
Return Value
optimize() returns a typed dict (OptimizeReturn) with the following keys:
best_trials(list): The topbest_candidates_counttrials sorted by fitness score (highest first). Each entry is a dict containing:rank(int) — position in the ranking, 1-indexedtrial(int) — the Optuna trial numberparams(dict) — the hyperparameter values that produced this resultfitness(float) — the composite fitness score (0 to ~1); higher is betterdna(str) — base64-encoded representation of the params; can be pasted directly into the Jesse dashboard's DNA field to instantly apply those parameterstraining_metrics(dict) — full backtest metrics dict for the training periodtesting_metrics(dict) — full backtest metrics dict for the testing / validation period
total_trials(int): Total number of trials that were requested.completed_trials(int): Number of trials that actually finished (some may be pruned or fail).objective_function(str): The objective function that was used for this run.
Printing Results
Jesse ships a ready-made summary printer so you don't have to format the result dict manually:
from jesse.research import print_optimize_summary
# Show DNA strings in the last column (default)
print_optimize_summary(result)
# Show raw parameter dicts instead of DNA strings
print_optimize_summary(result, show_params=True)Parameters
result(dict): The dict returned byoptimize().show_params(bool, defaultFalse): WhenFalse(the default), the last column of the table shows the compact DNA string for each trial. WhenTrue, the last column shows the raw parameter dict instead.
Example output (default — DNA column)
Completed 30 / 30 trials | Objective: sharpe
Rank Trial Fitness Train Sharpe Ratio Test Sharpe Ratio DNA
---- -------- ------- ------------------ ----------------- ---
#1 Trial 17 0.1966 0.7962 0.8909 eyJlbnRyeV9hdHJfbXVsdGlwbGllciI6IDAuOTJ9
#2 Trial 6 0.1765 0.8148 1.5286 eyJlbnRyeV9hdHJfbXVsdGlwbGllciI6IDAuNTl9
...Example output (show_params=True)
Completed 30 / 30 trials | Objective: sharpe
Rank Trial Fitness Train Sharpe Ratio Test Sharpe Ratio Params
---- -------- ------- ------------------ ----------------- ------
#1 Trial 17 0.1966 0.7962 0.8909 {'entry_atr_multiplier': 0.92, 'stop_loss': 2.81, 'adx_threshold': 36}
#2 Trial 6 0.1765 0.8148 1.5286 {'entry_atr_multiplier': 0.59, 'stop_loss': 2.80, 'adx_threshold': 43}
...Hyperparameters
Your strategy must implement a hyperparameters() method that returns the search space. Refer to the hyperparameters documentation for the full specification. A minimal example:
def hyperparameters(self) -> list:
return [
{'name': 'slow_period', 'type': int, 'min': 50, 'max': 200, 'default': 100},
{'name': 'fast_period', 'type': int, 'min': 10, 'max': 50, 'default': 20},
{'name': 'threshold', 'type': float, 'min': 0.1, 'max': 2.0, 'default': 1.0},
]Inside the strategy, read the current trial's values via self.hp:
slow = self.hp['slow_period']
fast = self.hp['fast_period']Using the DNA
Every trial in best_trials includes a dna field — a compact base64 string that encodes the full parameter set. You can paste it directly into the DNA field of the Jesse dashboard's backtest or live form to instantly load those parameters without typing them manually.
You can also work with both params and dna programmatically:
best = result['best_trials'][0]
print('Best params:', best['params'])
print('DNA:', best['dna'])Complete Example
The following script is ready to copy and run. It loads candles for a training window and a held-out testing window, runs the optimization, prints the summary, and then inspects the top result directly.
import logging
import os
# suppress Ray startup noise
os.environ['RAY_DISABLE_IMPORT_WARNING'] = '1'
logging.getLogger('ray').setLevel(logging.ERROR)
from jesse.enums import exchanges
import jesse.helpers as jh
from jesse.research import get_candles, optimize, print_optimize_summary
# =============================================================================
# CONFIGURATION
# =============================================================================
EXCHANGE = exchanges.BINANCE_PERPETUAL_FUTURES
SYMBOL = 'BTC-USDT'
TIMEFRAME = '4h'
TRAINING_START = '2021-01-01'
TRAINING_FINISH = '2023-12-31'
TESTING_START = '2024-01-01'
TESTING_FINISH = '2024-12-31'
WARM_UP_CANDLES = 210
CONFIG = {
'exchange': {
'name': EXCHANGE,
'balance': 10_000,
'fee': 0.0007,
'type': 'futures',
'futures_leverage': 2,
'futures_leverage_mode': 'cross',
},
'warm_up_candles': WARM_UP_CANDLES,
}
ROUTES = [
# Replace 'MyStrategy' with your actual strategy class or string name
{'symbol': SYMBOL, 'timeframe': TIMEFRAME, 'strategy': 'MyStrategy'},
]
DATA_ROUTES = []
# =============================================================================
# HELPER — load and wrap candles for one date range
# =============================================================================
def load_candles_for_period(start: str, finish: str) -> tuple:
"""Return (trading_candles_dict, warmup_candles_dict) for the given period."""
warmup_arr, trading_arr = get_candles(
EXCHANGE,
SYMBOL,
TIMEFRAME,
jh.date_to_timestamp(start),
jh.date_to_timestamp(finish),
WARM_UP_CANDLES,
caching=True, # cache to disk — repeated runs skip the DB query
is_for_jesse=True, # required when passing arrays to optimize()
)
key = jh.key(EXCHANGE, SYMBOL)
trading_candles = {
key: {'exchange': EXCHANGE, 'symbol': SYMBOL, 'candles': trading_arr}
}
warmup_candles = {
key: {'exchange': EXCHANGE, 'symbol': SYMBOL, 'candles': warmup_arr}
}
return trading_candles, warmup_candles
# =============================================================================
# MAIN
# =============================================================================
if __name__ == '__main__':
print('📦 Loading candles...')
training_candles, training_warmup = load_candles_for_period(TRAINING_START, TRAINING_FINISH)
testing_candles, testing_warmup = load_candles_for_period(TESTING_START, TESTING_FINISH)
print(f' Training : {TRAINING_START} → {TRAINING_FINISH}')
print(f' Testing : {TESTING_START} → {TESTING_FINISH}')
print()
print('🚀 Starting optimization...')
result = optimize(
config=CONFIG,
routes=ROUTES,
data_routes=DATA_ROUTES,
training_candles=training_candles,
training_warmup_candles=training_warmup,
testing_candles=testing_candles,
testing_warmup_candles=testing_warmup,
optimal_total=200,
fast_mode=True,
objective_function='sharpe', # 'sharpe' | 'calmar' | 'sortino' | 'omega'
best_candidates_count=20,
progress_bar=True,
)
# Pretty-print the ranked table
print_optimize_summary(result)
# Access the top result directly
best = result['best_trials'][0]
print('Best params:', best['params'])
print('Training Sharpe:', best['training_metrics'].get('sharpe_ratio'))
print('Testing Sharpe:', best['testing_metrics'].get('sharpe_ratio'))
print('DNA:', best['dna'])Interpreting Results
Fitness Score
The fitness score is a composite value that combines the chosen objective metric (e.g. Sharpe ratio) with a trade-count component that rewards trials close to optimal_total trades and penalises trials with very few trades.
- Scores above ~0.0001 are generally "usable" — they indicate the trial completed with enough trades for the metric to be meaningful.
- The absolute value is not directly interpretable in human terms; use it only for ranking trials against each other.
- Always look at the underlying
training_metricsandtesting_metrics(e.g. Sharpe ratio, Calmar ratio, max drawdown) to judge the actual quality of a trial.
Training vs Testing
The function enforces a strict train / test split:
- The optimizer tunes parameters entirely on the training data. The Optuna objective function sees only training period metrics.
- The testing period is completely held out during the search. Testing metrics are computed once — after the best trial parameters are already fixed — and are used purely to measure generalisation.
- A good trial has both training and testing metrics in reasonable ranges.
- A trial whose training metrics look excellent but whose testing metrics collapse is a sign of overfitting to the training window.
Overfitting Warning
WARNING
Running more trials increases the chance of finding a parameter set that happened to work well historically by coincidence. More is not always better.
Tips
Start with fewer trials.
trialsdefaults to200per hyperparameter. For a strategy with 3 hyperparameters that is 600 total trials, which can take a long time. Passtrials=40for a quick first pass to check that everything is wired up correctly, then run the full count overnight.Set
optimal_totalto match your strategy. If your strategy typically produces 50 trades over a 3-year training period, passoptimal_total=50. Using the default of200when your strategy rarely trades will unfairly penalise otherwise good trials.Use
caching=Trueinget_candles(). Candle arrays are cached to disk the first time they are fetched. Subsequent calls for the same exchange / symbol / timeframe / date range load from cache and skip the database entirely, saving significant time on repeated runs.Chain with Monte Carlo for robustness.
optimize()finds parameter sets that score well on historical data. After you have promising candidates, run them throughmonte_carlo_trades()ormonte_carlo_candles()to verify that the performance holds across shuffled trade sequences and resampled market conditions before committing to live trading.
