1313from concurrent .futures import ProcessPoolExecutor , as_completed
1414from copy import copy
1515from functools import lru_cache , partial
16- from itertools import chain , compress , product , repeat
16+ from itertools import chain , product , repeat
1717from math import copysign
1818from numbers import Number
1919from typing import Callable , Dict , List , Optional , Sequence , Tuple , Type , Union
@@ -1278,19 +1278,18 @@ def optimize(self, *,
12781278
12791279 * `"grid"` which does an exhaustive (or randomized) search over the
12801280 cartesian product of parameter combinations, and
1281- * `"skopt "` which finds close-to-optimal strategy parameters using
1281+ * `"sambo "` which finds close-to-optimal strategy parameters using
12821282 [model-based optimization], making at most `max_tries` evaluations.
12831283
1284- [model-based optimization]: \
1285- https://scikit-optimize.github.io/stable/auto_examples/bayesian-optimization.html
1284+ [model-based optimization]: https://sambo-optimization.github.io
12861285
12871286 `max_tries` is the maximal number of strategy runs to perform.
12881287 If `method="grid"`, this results in randomized grid search.
12891288 If `max_tries` is a floating value between (0, 1], this sets the
12901289 number of runs to approximately that fraction of full grid space.
12911290 Alternatively, if integer, it denotes the absolute maximum number
12921291 of evaluations. If unspecified (default), grid search is exhaustive,
1293- whereas for `method="skopt "`, `max_tries` is set to 200.
1292+ whereas for `method="sambo "`, `max_tries` is set to 200.
12941293
12951294 `constraint` is a function that accepts a dict-like object of
12961295 parameters (with values) and returns `True` when the combination
@@ -1303,16 +1302,14 @@ def optimize(self, *,
13031302 inspected or projected onto 2D to plot a heatmap
13041303 (see `backtesting.lib.plot_heatmaps()`).
13051304
1306- If `return_optimization` is True and `method = 'skopt '`,
1305+ If `return_optimization` is True and `method = 'sambo '`,
13071306 in addition to result series (and maybe heatmap), return raw
13081307 [`scipy.optimize.OptimizeResult`][OptimizeResult] for further
1309- inspection, e.g. with [scikit-optimize]\
1310- [plotting tools].
1308+ inspection, e.g. with [SAMBO]'s [plotting tools].
13111309
1312- [OptimizeResult]: \
1313- https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.OptimizeResult.html
1314- [scikit-optimize]: https://scikit-optimize.github.io
1315- [plotting tools]: https://scikit-optimize.github.io/stable/modules/plots.html
1310+ [OptimizeResult]: https://sambo-optimization.github.io/doc/sambo/#sambo.OptimizeResult
1311+ [SAMBO]: https://sambo-optimization.github.io
1312+ [plotting tools]: https://sambo-optimization.github.io/doc/sambo/plot.html
13161313
13171314 If you want reproducible optimization results, set `random_state`
13181315 to a fixed integer random seed.
@@ -1360,8 +1357,12 @@ def constraint(_):
13601357 "the combination of parameters is admissible or not" )
13611358 assert callable (constraint ), constraint
13621359
1363- if return_optimization and method != 'skopt' :
1364- raise ValueError ("return_optimization=True only valid if method='skopt'" )
1360+ if method == 'skopt' :
1361+ method = 'sambo'
1362+ warnings .warn ('`Backtest.optimize(method="skopt")` is deprecated. Use `method="sambo"`.' ,
1363+ DeprecationWarning , stacklevel = 2 )
1364+ if return_optimization and method != 'sambo' :
1365+ raise ValueError ("return_optimization=True only valid if method='sambo'" )
13651366
13661367 def _tuple (x ):
13671368 return x if isinstance (x , Sequence ) and not isinstance (x , str ) else (x ,)
@@ -1456,18 +1457,13 @@ def _batch(seq):
14561457 return stats , heatmap
14571458 return stats
14581459
1459- def _optimize_skopt () -> Union [pd .Series ,
1460+ def _optimize_sambo () -> Union [pd .Series ,
14601461 Tuple [pd .Series , pd .Series ],
14611462 Tuple [pd .Series , pd .Series , dict ]]:
14621463 try :
1463- from skopt import forest_minimize
1464- from skopt .callbacks import DeltaXStopper
1465- from skopt .learning import ExtraTreesRegressor
1466- from skopt .space import Categorical , Integer , Real
1467- from skopt .utils import use_named_args
1464+ import sambo
14681465 except ImportError :
1469- raise ImportError ("Need package 'scikit-optimize' for method='skopt'. "
1470- "pip install scikit-optimize" ) from None
1466+ raise ImportError ("Need package 'sambo' for method='sambo'. pip install sambo" ) from None
14711467
14721468 nonlocal max_tries
14731469 max_tries = (200 if max_tries is None else
@@ -1478,80 +1474,62 @@ def _optimize_skopt() -> Union[pd.Series,
14781474 for key , values in kwargs .items ():
14791475 values = np .asarray (values )
14801476 if values .dtype .kind in 'mM' : # timedelta, datetime64
1481- # these dtypes are unsupported in skopt , so convert to raw int
1477+ # these dtypes are unsupported in SAMBO , so convert to raw int
14821478 # TODO: save dtype and convert back later
14831479 values = values .astype (int )
14841480
14851481 if values .dtype .kind in 'iumM' :
1486- dimensions .append (Integer ( low = values .min (), high = values .max (), name = key ))
1482+ dimensions .append (( values .min (), values .max () + 1 ))
14871483 elif values .dtype .kind == 'f' :
1488- dimensions .append (Real ( low = values .min (), high = values .max (), name = key ))
1484+ dimensions .append (( values .min (), values .max ()))
14891485 else :
1490- dimensions .append (Categorical ( values .tolist (), name = key , transform = 'onehot' ))
1486+ dimensions .append (values .tolist ())
14911487
14921488 # Avoid recomputing re-evaluations:
1493- # "The objective has been evaluated at this point before."
1494- # https://github.com/scikit- optimize/scikit-optimize/issues/302
1495- memoized_run = lru_cache ()( lambda tup : self . run ( ** dict ( tup ) ))
1489+ memoized_run = lru_cache ()( lambda tup : self . run ( ** dict ( tup ))) # XXX: Reeval if this needed?
1490+ progress = iter ( _tqdm ( repeat ( None ), total = max_tries , leave = False , desc = 'Backtest. optimize' ))
1491+ _names = tuple ( kwargs . keys ( ))
14961492
1497- # np.inf/np.nan breaks sklearn, np.finfo(float).max breaks skopt.plots.plot_objective
1498- INVALID = 1e300
1499- progress = iter (_tqdm (repeat (None ), total = max_tries , desc = 'Backtest.optimize' ))
1500-
1501- @use_named_args (dimensions = dimensions )
1502- def objective_function (** params ):
1493+ def objective_function (x ):
1494+ nonlocal progress , memoized_run , constraint , _names
15031495 next (progress )
1504- # Check constraints
1505- # TODO: Adjust after https://github.com/scikit-optimize/scikit-optimize/pull/971
1506- if not constraint (AttrDict (params )):
1507- return INVALID
1508- res = memoized_run (tuple (params .items ()))
1496+ res = memoized_run (tuple (zip (_names , x )))
15091497 value = - maximize (res )
1510- if np .isnan (value ):
1511- return INVALID
1512- return value
1513-
1514- with warnings .catch_warnings ():
1515- warnings .filterwarnings (
1516- 'ignore' , 'The objective has been evaluated at this point before.' )
1517-
1518- res = forest_minimize (
1519- func = objective_function ,
1520- dimensions = dimensions ,
1521- n_calls = max_tries ,
1522- base_estimator = ExtraTreesRegressor (n_estimators = 20 , min_samples_leaf = 2 ),
1523- acq_func = 'LCB' ,
1524- kappa = 3 ,
1525- n_initial_points = min (max_tries , 20 + 3 * len (kwargs )),
1526- initial_point_generator = 'lhs' , # 'sobel' requires n_initial_points ~ 2**N
1527- callback = DeltaXStopper (9e-7 ),
1528- random_state = random_state )
1498+ return 0 if np .isnan (value ) else value
1499+
1500+ def cons (x ):
1501+ nonlocal constraint , _names
1502+ return constraint (AttrDict (zip (_names , x )))
1503+
1504+ res = sambo .minimize (
1505+ fun = objective_function ,
1506+ bounds = dimensions ,
1507+ constraints = cons ,
1508+ max_iter = max_tries ,
1509+ method = 'sceua' ,
1510+ rng = random_state )
15291511
15301512 stats = self .run (** dict (zip (kwargs .keys (), res .x )))
15311513 output = [stats ]
15321514
15331515 if return_heatmap :
1534- heatmap = pd .Series (dict (zip (map (tuple , res .x_iters ), - res .func_vals )),
1516+ heatmap = pd .Series (dict (zip (map (tuple , res .xv ), - res .funv )),
15351517 name = maximize_key )
15361518 heatmap .index .names = kwargs .keys ()
1537- heatmap = heatmap [heatmap != - INVALID ]
15381519 heatmap .sort_index (inplace = True )
15391520 output .append (heatmap )
15401521
15411522 if return_optimization :
1542- valid = res .func_vals != INVALID
1543- res .x_iters = list (compress (res .x_iters , valid ))
1544- res .func_vals = res .func_vals [valid ]
15451523 output .append (res )
15461524
15471525 return stats if len (output ) == 1 else tuple (output )
15481526
15491527 if method == 'grid' :
15501528 output = _optimize_grid ()
1551- elif method == ' skopt' :
1552- output = _optimize_skopt ()
1529+ elif method in ( 'sambo' , ' skopt') :
1530+ output = _optimize_sambo ()
15531531 else :
1554- raise ValueError (f"Method should be 'grid' or 'skopt ', not { method !r} " )
1532+ raise ValueError (f"Method should be 'grid' or 'sambo ', not { method !r} " )
15551533 return output
15561534
15571535 @staticmethod
0 commit comments