openbacktesting 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openbacktesting/__init__.py +85 -0
- openbacktesting/aggregation.py +8 -0
- openbacktesting/broker/__init__.py +54 -0
- openbacktesting/broker/commission.py +57 -0
- openbacktesting/broker/core.py +1122 -0
- openbacktesting/broker/execution.py +62 -0
- openbacktesting/broker/instrument.py +106 -0
- openbacktesting/broker/ledger.py +60 -0
- openbacktesting/broker/models.py +236 -0
- openbacktesting/broker/observations.py +99 -0
- openbacktesting/broker/requests.py +192 -0
- openbacktesting/broker/sizing.py +76 -0
- openbacktesting/colors.py +175 -0
- openbacktesting/data/__init__.py +20 -0
- openbacktesting/data/aggregation.py +39 -0
- openbacktesting/data/context.py +30 -0
- openbacktesting/data/core.py +218 -0
- openbacktesting/data/generators.py +54 -0
- openbacktesting/data/strategy_data.py +103 -0
- openbacktesting/data/timeframe.py +69 -0
- openbacktesting/engine.py +34 -0
- openbacktesting/helpers.py +77 -0
- openbacktesting/indicator_api.py +10 -0
- openbacktesting/indicator_methods.py +123 -0
- openbacktesting/indicator_types.py +19 -0
- openbacktesting/indicators/__init__.py +7 -0
- openbacktesting/indicators/api.py +97 -0
- openbacktesting/indicators/calculus.py +75 -0
- openbacktesting/indicators/candles/__init__.py +0 -0
- openbacktesting/indicators/candles/cdl_doji.py +17 -0
- openbacktesting/indicators/candles/cdl_engulfing.py +14 -0
- openbacktesting/indicators/candles/cdl_eveningstar.py +15 -0
- openbacktesting/indicators/candles/cdl_hammer.py +14 -0
- openbacktesting/indicators/candles/cdl_harami.py +14 -0
- openbacktesting/indicators/candles/cdl_inside.py +14 -0
- openbacktesting/indicators/candles/cdl_marubozu.py +14 -0
- openbacktesting/indicators/candles/cdl_morningstar.py +15 -0
- openbacktesting/indicators/candles/cdl_shootingstar.py +14 -0
- openbacktesting/indicators/candles/cdl_z.py +28 -0
- openbacktesting/indicators/candles/ha.py +19 -0
- openbacktesting/indicators/catalog.json +8348 -0
- openbacktesting/indicators/common.py +94 -0
- openbacktesting/indicators/cycles/__init__.py +0 -0
- openbacktesting/indicators/cycles/dsp.py +10 -0
- openbacktesting/indicators/cycles/ebsw.py +10 -0
- openbacktesting/indicators/cycles/ht_dcperiod.py +10 -0
- openbacktesting/indicators/cycles/ht_dcphase.py +10 -0
- openbacktesting/indicators/cycles/ht_phasor.py +17 -0
- openbacktesting/indicators/cycles/ht_sine.py +17 -0
- openbacktesting/indicators/cycles/ht_trendmode.py +10 -0
- openbacktesting/indicators/cycles/msw.py +19 -0
- openbacktesting/indicators/methods.py +5762 -0
- openbacktesting/indicators/momentum/__init__.py +0 -0
- openbacktesting/indicators/momentum/ac.py +9 -0
- openbacktesting/indicators/momentum/ao.py +10 -0
- openbacktesting/indicators/momentum/apo.py +10 -0
- openbacktesting/indicators/momentum/bias.py +10 -0
- openbacktesting/indicators/momentum/bop.py +10 -0
- openbacktesting/indicators/momentum/brar.py +26 -0
- openbacktesting/indicators/momentum/cci.py +10 -0
- openbacktesting/indicators/momentum/cfo.py +10 -0
- openbacktesting/indicators/momentum/cg.py +10 -0
- openbacktesting/indicators/momentum/cmo.py +10 -0
- openbacktesting/indicators/momentum/coppock.py +10 -0
- openbacktesting/indicators/momentum/crsi.py +28 -0
- openbacktesting/indicators/momentum/cti.py +10 -0
- openbacktesting/indicators/momentum/dm.py +17 -0
- openbacktesting/indicators/momentum/er.py +10 -0
- openbacktesting/indicators/momentum/eri.py +17 -0
- openbacktesting/indicators/momentum/fisher.py +17 -0
- openbacktesting/indicators/momentum/fosc.py +10 -0
- openbacktesting/indicators/momentum/gator.py +13 -0
- openbacktesting/indicators/momentum/ibs.py +7 -0
- openbacktesting/indicators/momentum/inertia.py +22 -0
- openbacktesting/indicators/momentum/kdj.py +18 -0
- openbacktesting/indicators/momentum/kst.py +30 -0
- openbacktesting/indicators/momentum/lrsi.py +10 -0
- openbacktesting/indicators/momentum/macd.py +27 -0
- openbacktesting/indicators/momentum/macdext.py +27 -0
- openbacktesting/indicators/momentum/macdfix.py +18 -0
- openbacktesting/indicators/momentum/mom.py +10 -0
- openbacktesting/indicators/momentum/pgo.py +10 -0
- openbacktesting/indicators/momentum/pmo.py +13 -0
- openbacktesting/indicators/momentum/po.py +10 -0
- openbacktesting/indicators/momentum/ppo.py +20 -0
- openbacktesting/indicators/momentum/psl.py +12 -0
- openbacktesting/indicators/momentum/pvo.py +20 -0
- openbacktesting/indicators/momentum/qqe.py +31 -0
- openbacktesting/indicators/momentum/rci.py +20 -0
- openbacktesting/indicators/momentum/rmi.py +7 -0
- openbacktesting/indicators/momentum/roc.py +10 -0
- openbacktesting/indicators/momentum/rocp.py +10 -0
- openbacktesting/indicators/momentum/rocr.py +10 -0
- openbacktesting/indicators/momentum/rocr100.py +10 -0
- openbacktesting/indicators/momentum/rsi.py +10 -0
- openbacktesting/indicators/momentum/rsx.py +10 -0
- openbacktesting/indicators/momentum/rvgi.py +26 -0
- openbacktesting/indicators/momentum/slope.py +17 -0
- openbacktesting/indicators/momentum/smc_sweep.py +18 -0
- openbacktesting/indicators/momentum/smi.py +21 -0
- openbacktesting/indicators/momentum/smi_stochastic.py +15 -0
- openbacktesting/indicators/momentum/squeeze.py +47 -0
- openbacktesting/indicators/momentum/squeeze_pro.py +53 -0
- openbacktesting/indicators/momentum/stc.py +20 -0
- openbacktesting/indicators/momentum/stoch.py +19 -0
- openbacktesting/indicators/momentum/stochf.py +19 -0
- openbacktesting/indicators/momentum/stochrsi.py +19 -0
- openbacktesting/indicators/momentum/td_seq.py +17 -0
- openbacktesting/indicators/momentum/trix.py +19 -0
- openbacktesting/indicators/momentum/trixh.py +20 -0
- openbacktesting/indicators/momentum/tsi.py +26 -0
- openbacktesting/indicators/momentum/uo.py +22 -0
- openbacktesting/indicators/momentum/vwmacd.py +20 -0
- openbacktesting/indicators/momentum/wavetrend.py +15 -0
- openbacktesting/indicators/momentum/willr.py +10 -0
- openbacktesting/indicators/overlap/__init__.py +0 -0
- openbacktesting/indicators/overlap/alligator.py +13 -0
- openbacktesting/indicators/overlap/alma.py +33 -0
- openbacktesting/indicators/overlap/anchored_vwap.py +50 -0
- openbacktesting/indicators/overlap/anchored_vwap_bands.py +13 -0
- openbacktesting/indicators/overlap/avgprice.py +10 -0
- openbacktesting/indicators/overlap/dema.py +10 -0
- openbacktesting/indicators/overlap/ema.py +12 -0
- openbacktesting/indicators/overlap/frama.py +29 -0
- openbacktesting/indicators/overlap/fwma.py +10 -0
- openbacktesting/indicators/overlap/hilo.py +26 -0
- openbacktesting/indicators/overlap/hl2.py +10 -0
- openbacktesting/indicators/overlap/hlc3.py +10 -0
- openbacktesting/indicators/overlap/hma.py +10 -0
- openbacktesting/indicators/overlap/ht_trendline.py +10 -0
- openbacktesting/indicators/overlap/hwma.py +10 -0
- openbacktesting/indicators/overlap/ichimoku.py +44 -0
- openbacktesting/indicators/overlap/jma.py +10 -0
- openbacktesting/indicators/overlap/kama.py +10 -0
- openbacktesting/indicators/overlap/linreg.py +15 -0
- openbacktesting/indicators/overlap/linregangle.py +10 -0
- openbacktesting/indicators/overlap/linregintercept.py +10 -0
- openbacktesting/indicators/overlap/linregslope.py +10 -0
- openbacktesting/indicators/overlap/ma.py +10 -0
- openbacktesting/indicators/overlap/ma_envelopes.py +16 -0
- openbacktesting/indicators/overlap/mama.py +17 -0
- openbacktesting/indicators/overlap/mavp.py +17 -0
- openbacktesting/indicators/overlap/mcgd.py +10 -0
- openbacktesting/indicators/overlap/medprice.py +10 -0
- openbacktesting/indicators/overlap/midpoint.py +10 -0
- openbacktesting/indicators/overlap/midprice.py +10 -0
- openbacktesting/indicators/overlap/mmar.py +21 -0
- openbacktesting/indicators/overlap/ohlc4.py +10 -0
- openbacktesting/indicators/overlap/pwma.py +10 -0
- openbacktesting/indicators/overlap/rainbow.py +25 -0
- openbacktesting/indicators/overlap/rma.py +10 -0
- openbacktesting/indicators/overlap/sinwma.py +10 -0
- openbacktesting/indicators/overlap/sma.py +14 -0
- openbacktesting/indicators/overlap/ssf.py +10 -0
- openbacktesting/indicators/overlap/supertrend.py +21 -0
- openbacktesting/indicators/overlap/swma.py +10 -0
- openbacktesting/indicators/overlap/t3.py +10 -0
- openbacktesting/indicators/overlap/tema.py +10 -0
- openbacktesting/indicators/overlap/trima.py +10 -0
- openbacktesting/indicators/overlap/tsf.py +10 -0
- openbacktesting/indicators/overlap/typprice.py +10 -0
- openbacktesting/indicators/overlap/vidya.py +10 -0
- openbacktesting/indicators/overlap/vwap.py +10 -0
- openbacktesting/indicators/overlap/vwma.py +10 -0
- openbacktesting/indicators/overlap/wcp.py +10 -0
- openbacktesting/indicators/overlap/wma.py +10 -0
- openbacktesting/indicators/overlap/zlma.py +10 -0
- openbacktesting/indicators/performance/__init__.py +0 -0
- openbacktesting/indicators/performance/drawdown.py +18 -0
- openbacktesting/indicators/performance/log_return.py +10 -0
- openbacktesting/indicators/performance/percent_return.py +10 -0
- openbacktesting/indicators/predicates.py +88 -0
- openbacktesting/indicators/registry.py +571 -0
- openbacktesting/indicators/runtime.py +290 -0
- openbacktesting/indicators/statistics/__init__.py +0 -0
- openbacktesting/indicators/statistics/beta.py +10 -0
- openbacktesting/indicators/statistics/correl.py +10 -0
- openbacktesting/indicators/statistics/entropy.py +10 -0
- openbacktesting/indicators/statistics/kurtosis.py +10 -0
- openbacktesting/indicators/statistics/mad.py +10 -0
- openbacktesting/indicators/statistics/md.py +10 -0
- openbacktesting/indicators/statistics/median.py +10 -0
- openbacktesting/indicators/statistics/mode.py +48 -0
- openbacktesting/indicators/statistics/quantile.py +10 -0
- openbacktesting/indicators/statistics/skew.py +10 -0
- openbacktesting/indicators/statistics/stderr.py +10 -0
- openbacktesting/indicators/statistics/stdev.py +10 -0
- openbacktesting/indicators/statistics/variance.py +10 -0
- openbacktesting/indicators/statistics/zscore.py +10 -0
- openbacktesting/indicators/trend/__init__.py +0 -0
- openbacktesting/indicators/trend/adx.py +28 -0
- openbacktesting/indicators/trend/adxr.py +28 -0
- openbacktesting/indicators/trend/amat.py +19 -0
- openbacktesting/indicators/trend/aroon.py +18 -0
- openbacktesting/indicators/trend/atr_stop.py +33 -0
- openbacktesting/indicators/trend/chop.py +20 -0
- openbacktesting/indicators/trend/cksp.py +17 -0
- openbacktesting/indicators/trend/cpr.py +57 -0
- openbacktesting/indicators/trend/decay.py +10 -0
- openbacktesting/indicators/trend/decreasing.py +18 -0
- openbacktesting/indicators/trend/dpo.py +12 -0
- openbacktesting/indicators/trend/dx.py +19 -0
- openbacktesting/indicators/trend/edecay.py +10 -0
- openbacktesting/indicators/trend/increasing.py +18 -0
- openbacktesting/indicators/trend/long_run.py +10 -0
- openbacktesting/indicators/trend/minus_dm.py +10 -0
- openbacktesting/indicators/trend/pivot_points.py +89 -0
- openbacktesting/indicators/trend/pivothigh.py +23 -0
- openbacktesting/indicators/trend/pivotlow.py +23 -0
- openbacktesting/indicators/trend/plus_dm.py +10 -0
- openbacktesting/indicators/trend/pmax.py +18 -0
- openbacktesting/indicators/trend/psar.py +19 -0
- openbacktesting/indicators/trend/qstick.py +10 -0
- openbacktesting/indicators/trend/sarext.py +35 -0
- openbacktesting/indicators/trend/short_run.py +10 -0
- openbacktesting/indicators/trend/tsignals.py +26 -0
- openbacktesting/indicators/trend/ttm_trend.py +16 -0
- openbacktesting/indicators/trend/vhf.py +10 -0
- openbacktesting/indicators/trend/volatility_stop.py +33 -0
- openbacktesting/indicators/trend/vortex.py +17 -0
- openbacktesting/indicators/trend/xsignals.py +29 -0
- openbacktesting/indicators/types.py +344 -0
- openbacktesting/indicators/utility/__init__.py +0 -0
- openbacktesting/indicators/utility/barssince.py +21 -0
- openbacktesting/indicators/utility/change.py +7 -0
- openbacktesting/indicators/utility/cross.py +12 -0
- openbacktesting/indicators/utility/crossover.py +11 -0
- openbacktesting/indicators/utility/crossunder.py +11 -0
- openbacktesting/indicators/utility/cum.py +7 -0
- openbacktesting/indicators/utility/falling.py +10 -0
- openbacktesting/indicators/utility/highest.py +7 -0
- openbacktesting/indicators/utility/highestbars.py +29 -0
- openbacktesting/indicators/utility/lowest.py +7 -0
- openbacktesting/indicators/utility/lowestbars.py +29 -0
- openbacktesting/indicators/utility/percentile_linear_interpolation.py +12 -0
- openbacktesting/indicators/utility/percentile_nearest_rank.py +16 -0
- openbacktesting/indicators/utility/percentrank.py +7 -0
- openbacktesting/indicators/utility/range.py +8 -0
- openbacktesting/indicators/utility/rising.py +10 -0
- openbacktesting/indicators/utility/rolling_count.py +12 -0
- openbacktesting/indicators/utility/rolling_sum.py +7 -0
- openbacktesting/indicators/utility/running_max.py +7 -0
- openbacktesting/indicators/utility/running_min.py +7 -0
- openbacktesting/indicators/utility/shift.py +7 -0
- openbacktesting/indicators/utility/valuewhen.py +25 -0
- openbacktesting/indicators/volatility/__init__.py +0 -0
- openbacktesting/indicators/volatility/aberration.py +21 -0
- openbacktesting/indicators/volatility/accbands.py +20 -0
- openbacktesting/indicators/volatility/adr.py +7 -0
- openbacktesting/indicators/volatility/atr.py +12 -0
- openbacktesting/indicators/volatility/avolume.py +10 -0
- openbacktesting/indicators/volatility/bbands.py +20 -0
- openbacktesting/indicators/volatility/ce.py +25 -0
- openbacktesting/indicators/volatility/cvi.py +10 -0
- openbacktesting/indicators/volatility/donchian.py +20 -0
- openbacktesting/indicators/volatility/hvol.py +10 -0
- openbacktesting/indicators/volatility/hwc.py +27 -0
- openbacktesting/indicators/volatility/kc.py +27 -0
- openbacktesting/indicators/volatility/massi.py +10 -0
- openbacktesting/indicators/volatility/natr.py +19 -0
- openbacktesting/indicators/volatility/pdist.py +10 -0
- openbacktesting/indicators/volatility/rvi.py +21 -0
- openbacktesting/indicators/volatility/starc.py +11 -0
- openbacktesting/indicators/volatility/thermo.py +28 -0
- openbacktesting/indicators/volatility/true_range.py +31 -0
- openbacktesting/indicators/volatility/ui.py +10 -0
- openbacktesting/indicators/volume/__init__.py +0 -0
- openbacktesting/indicators/volume/ad.py +10 -0
- openbacktesting/indicators/volume/adosc.py +19 -0
- openbacktesting/indicators/volume/aobv.py +31 -0
- openbacktesting/indicators/volume/cmf.py +12 -0
- openbacktesting/indicators/volume/efi.py +12 -0
- openbacktesting/indicators/volume/emv.py +10 -0
- openbacktesting/indicators/volume/eom.py +19 -0
- openbacktesting/indicators/volume/iii.py +9 -0
- openbacktesting/indicators/volume/kvo.py +28 -0
- openbacktesting/indicators/volume/marketfi.py +10 -0
- openbacktesting/indicators/volume/mfi.py +12 -0
- openbacktesting/indicators/volume/nvi.py +26 -0
- openbacktesting/indicators/volume/obv.py +10 -0
- openbacktesting/indicators/volume/pvi.py +26 -0
- openbacktesting/indicators/volume/pvol.py +10 -0
- openbacktesting/indicators/volume/pvr.py +10 -0
- openbacktesting/indicators/volume/pvt.py +10 -0
- openbacktesting/indicators/volume/rvol.py +8 -0
- openbacktesting/indicators/volume/vfi.py +18 -0
- openbacktesting/indicators/volume/vosc.py +10 -0
- openbacktesting/indicators/volume/wad.py +10 -0
- openbacktesting/indicators/volume/wvad.py +9 -0
- openbacktesting/inputs.py +51 -0
- openbacktesting/lib.py +31 -0
- openbacktesting/logs.py +129 -0
- openbacktesting/optimization.py +8 -0
- openbacktesting/optimizer/__init__.py +25 -0
- openbacktesting/optimizer/_semantics.py +155 -0
- openbacktesting/optimizer/identity.py +45 -0
- openbacktesting/optimizer/models.py +92 -0
- openbacktesting/optimizer/plan.py +129 -0
- openbacktesting/optimizer/portfolio-result.schema.json +4984 -0
- openbacktesting/optimizer/query.py +257 -0
- openbacktesting/optimizer/runner.py +98 -0
- openbacktesting/optimizer/storage.py +261 -0
- openbacktesting/optimizer/validation.py +64 -0
- openbacktesting/parallel.py +9 -0
- openbacktesting/plotting/__init__.py +23 -0
- openbacktesting/plotting/collector.py +509 -0
- openbacktesting/plotting/encoding.py +102 -0
- openbacktesting/plotting/types.py +92 -0
- openbacktesting/portfolio/__init__.py +15 -0
- openbacktesting/portfolio/analysis.py +245 -0
- openbacktesting/portfolio/builder.py +381 -0
- openbacktesting/portfolio/catalog.json +1404 -0
- openbacktesting/portfolio/helpers.py +92 -0
- openbacktesting/portfolio/metrics.py +186 -0
- openbacktesting/portfolio/models.py +63 -0
- openbacktesting/portfolio/periods.py +164 -0
- openbacktesting/portfolio/rates.py +14 -0
- openbacktesting/portfolio/snapshot.py +111 -0
- openbacktesting/portfolio/stats.py +272 -0
- openbacktesting/portfolio/trades.py +136 -0
- openbacktesting/portfolio/trading.py +215 -0
- openbacktesting/py.typed +0 -0
- openbacktesting/runtime.py +330 -0
- openbacktesting/simulation/__init__.py +17 -0
- openbacktesting/simulation/collection.py +13 -0
- openbacktesting/simulation/config.py +241 -0
- openbacktesting/simulation/core.py +528 -0
- openbacktesting/simulation/optimization.py +326 -0
- openbacktesting/simulation/parallel.py +145 -0
- openbacktesting/simulation/variants.py +107 -0
- openbacktesting/strategy/__init__.py +19 -0
- openbacktesting/strategy/bar_state.py +14 -0
- openbacktesting/strategy/base.py +810 -0
- openbacktesting/strategy/declaration.py +90 -0
- openbacktesting/strategy/helpers.py +198 -0
- openbacktesting/strategy/inputs.py +460 -0
- openbacktesting/strategy/sizing.py +80 -0
- openbacktesting/strategy/templates.py +160 -0
- openbacktesting/strategy_data.py +10 -0
- openbacktesting/test/BTCUSD.csv +157 -0
- openbacktesting/test/EURUSD.csv +5001 -0
- openbacktesting/test/GOOG.csv +2149 -0
- openbacktesting/test/__init__.py +29 -0
- openbacktesting/test/__main__.py +11 -0
- openbacktesting/test/_test.py +1312 -0
- openbacktesting/util.py +31 -0
- openbacktesting-0.2.0.dist-info/METADATA +151 -0
- openbacktesting-0.2.0.dist-info/RECORD +350 -0
- openbacktesting-0.2.0.dist-info/WHEEL +4 -0
- openbacktesting-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Native parameter search and parallel optimization orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Callable, Sequence
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from copy import copy
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from itertools import product, repeat
|
|
11
|
+
from multiprocessing.pool import ThreadPool
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
from numpy.random import default_rng
|
|
16
|
+
|
|
17
|
+
from openbacktesting.helpers import patch
|
|
18
|
+
from openbacktesting.indicators.runtime import (
|
|
19
|
+
DEFAULT_CACHE_BYTES,
|
|
20
|
+
IndicatorCache,
|
|
21
|
+
optimization_scope,
|
|
22
|
+
)
|
|
23
|
+
from openbacktesting.portfolio.stats import dummy_stats
|
|
24
|
+
from openbacktesting.simulation.parallel import SharedMemoryManager, _batch
|
|
25
|
+
from openbacktesting.strategy.inputs import InputError, _definitions, request_for, resolve_native
|
|
26
|
+
from openbacktesting.util import _tqdm
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextmanager
|
|
30
|
+
def _joined_pool(pool):
|
|
31
|
+
"""ThreadPool.__exit__ does not wait for in-flight threads; join them before releasing market data and caches."""
|
|
32
|
+
active = pool
|
|
33
|
+
try:
|
|
34
|
+
with pool as active:
|
|
35
|
+
yield active
|
|
36
|
+
finally:
|
|
37
|
+
if isinstance(active, ThreadPool):
|
|
38
|
+
active.join()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _optimize(
|
|
42
|
+
self,
|
|
43
|
+
_mp_task,
|
|
44
|
+
*,
|
|
45
|
+
maximize: str | Callable[[pd.Series], float] = "SQN",
|
|
46
|
+
method: str = "grid",
|
|
47
|
+
max_tries: float | None = None,
|
|
48
|
+
constraint: Callable[[dict], bool] | None = None,
|
|
49
|
+
return_heatmap: bool = False,
|
|
50
|
+
return_optimization: bool = False,
|
|
51
|
+
random_state: int | None = None,
|
|
52
|
+
**kwargs,
|
|
53
|
+
) -> pd.Series | tuple[pd.Series, pd.Series] | tuple[pd.Series, pd.Series, dict]:
|
|
54
|
+
if not kwargs:
|
|
55
|
+
raise ValueError("Need some strategy parameters to optimize")
|
|
56
|
+
|
|
57
|
+
declarations = _definitions(self._strategy)
|
|
58
|
+
if not declarations:
|
|
59
|
+
self._strategy._check_reserved_params(kwargs)
|
|
60
|
+
if declarations and method in ("sambo", "skopt"):
|
|
61
|
+
raise InputError("UnsupportedOptimizationInput", None, "Declared strategies initially support only grid")
|
|
62
|
+
|
|
63
|
+
maximize_key = None
|
|
64
|
+
if isinstance(maximize, str):
|
|
65
|
+
maximize_key = str(maximize)
|
|
66
|
+
if maximize not in dummy_stats().index:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"`maximize`, if str, must match a key in pd.Series result of backtest.run()"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def maximize(stats: pd.Series, _key=maximize):
|
|
72
|
+
return stats[_key]
|
|
73
|
+
|
|
74
|
+
elif not callable(maximize):
|
|
75
|
+
raise TypeError(
|
|
76
|
+
"`maximize` must be str (a field of backtest.run() result "
|
|
77
|
+
"Series) or a function that accepts result Series "
|
|
78
|
+
"and returns a number; the higher the better"
|
|
79
|
+
)
|
|
80
|
+
assert callable(maximize), maximize
|
|
81
|
+
|
|
82
|
+
have_constraint = bool(constraint)
|
|
83
|
+
if constraint is None:
|
|
84
|
+
|
|
85
|
+
def constraint(_):
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
elif not callable(constraint):
|
|
89
|
+
raise TypeError(
|
|
90
|
+
"`constraint` must be a function that accepts a dict "
|
|
91
|
+
"of strategy parameters and returns a bool whether "
|
|
92
|
+
"the combination of parameters is admissible or not"
|
|
93
|
+
)
|
|
94
|
+
assert callable(constraint), constraint
|
|
95
|
+
|
|
96
|
+
if method == "skopt":
|
|
97
|
+
method = "sambo"
|
|
98
|
+
warnings.warn(
|
|
99
|
+
'`Backtest.optimize(method="skopt")` is deprecated. Use `method="sambo"`.',
|
|
100
|
+
DeprecationWarning,
|
|
101
|
+
stacklevel=2,
|
|
102
|
+
)
|
|
103
|
+
if return_optimization and method != "sambo":
|
|
104
|
+
raise ValueError("return_optimization=True only valid if method='sambo'")
|
|
105
|
+
|
|
106
|
+
def _tuple(x):
|
|
107
|
+
return x if isinstance(x, Sequence) and not isinstance(x, str) else (x,)
|
|
108
|
+
|
|
109
|
+
for k, v in kwargs.items():
|
|
110
|
+
if len(_tuple(v)) == 0:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Optimization variable '{k}' is passed no optimization values: {k}={v}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if declarations:
|
|
116
|
+
# Validate all values before constraint evaluation and random sampling so invalid candidates cannot be hidden.
|
|
117
|
+
kwargs = {
|
|
118
|
+
name: [
|
|
119
|
+
resolve_native(self._strategy, {name: value}).overrides()[name]
|
|
120
|
+
for value in _tuple(values)
|
|
121
|
+
]
|
|
122
|
+
for name, values in kwargs.items()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class AttrDict(dict):
|
|
126
|
+
def __getattr__(self, item):
|
|
127
|
+
return self[item]
|
|
128
|
+
|
|
129
|
+
def _grid_size():
|
|
130
|
+
size = int(np.prod([len(_tuple(v)) for v in kwargs.values()]))
|
|
131
|
+
if size < 10_000 and have_constraint:
|
|
132
|
+
size = sum(
|
|
133
|
+
1
|
|
134
|
+
for p in product(*(zip(repeat(k), _tuple(v)) for k, v in kwargs.items()))
|
|
135
|
+
if constraint(AttrDict(p))
|
|
136
|
+
)
|
|
137
|
+
return size
|
|
138
|
+
|
|
139
|
+
def _optimize_grid() -> pd.Series | tuple[pd.Series, pd.Series]:
|
|
140
|
+
rand = default_rng(random_state).random
|
|
141
|
+
grid_frac = (
|
|
142
|
+
1
|
|
143
|
+
if max_tries is None
|
|
144
|
+
else max_tries
|
|
145
|
+
if 0 < max_tries <= 1
|
|
146
|
+
else max_tries / _grid_size()
|
|
147
|
+
)
|
|
148
|
+
param_combos = [
|
|
149
|
+
dict(params) # back to dict so it pickles
|
|
150
|
+
for params in (
|
|
151
|
+
AttrDict(params)
|
|
152
|
+
for params in product(*(zip(repeat(k), _tuple(v)) for k, v in kwargs.items()))
|
|
153
|
+
)
|
|
154
|
+
if constraint(params) and rand() <= grid_frac
|
|
155
|
+
]
|
|
156
|
+
if not param_combos:
|
|
157
|
+
raise ValueError("No admissible parameter combinations to test")
|
|
158
|
+
|
|
159
|
+
if len(param_combos) > 300:
|
|
160
|
+
warnings.warn(
|
|
161
|
+
f"Searching for best of {len(param_combos)} configurations.", stacklevel=2
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
heatmap = pd.Series(
|
|
165
|
+
np.nan,
|
|
166
|
+
name=maximize_key,
|
|
167
|
+
index=pd.MultiIndex.from_tuples(
|
|
168
|
+
[p.values() for p in param_combos], names=next(iter(param_combos)).keys()
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
batches = list(_batch(param_combos))
|
|
173
|
+
if declarations:
|
|
174
|
+
batches = [
|
|
175
|
+
[request_for(self._strategy, params) for params in batch] for batch in batches
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
from openbacktesting import Pool
|
|
179
|
+
|
|
180
|
+
with (
|
|
181
|
+
optimization_scope(
|
|
182
|
+
getattr(self, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)
|
|
183
|
+
) as token,
|
|
184
|
+
SharedMemoryManager() as smm,
|
|
185
|
+
_joined_pool(Pool()) as pool,
|
|
186
|
+
):
|
|
187
|
+
with patch(self, "_data", None):
|
|
188
|
+
bt = copy(self) # bt._data will be reassigned in _mp_task worker
|
|
189
|
+
bt._portfolio_enabled = maximize_key is None
|
|
190
|
+
bt._results = None
|
|
191
|
+
bt._log_collector = None
|
|
192
|
+
bt._indicator_cache_token = token
|
|
193
|
+
data_shm = smm.df2shm(self._data)
|
|
194
|
+
results = _tqdm(
|
|
195
|
+
pool.imap(
|
|
196
|
+
_mp_task,
|
|
197
|
+
((bt, data_shm, params_batch) for params_batch in batches),
|
|
198
|
+
),
|
|
199
|
+
total=len(param_combos),
|
|
200
|
+
desc="Backtest.optimize",
|
|
201
|
+
)
|
|
202
|
+
for param_batch, result in zip(_batch(param_combos), results):
|
|
203
|
+
for params, stats in zip(param_batch, result):
|
|
204
|
+
if stats is not None:
|
|
205
|
+
heatmap[tuple(params.values())] = maximize(stats)
|
|
206
|
+
|
|
207
|
+
if pd.isnull(heatmap).all():
|
|
208
|
+
# No trade was made in any of the runs. Just make a random
|
|
209
|
+
# run so we get some, if empty, results
|
|
210
|
+
stats = self.run(**param_combos[0])
|
|
211
|
+
else:
|
|
212
|
+
best_params = heatmap.idxmax(skipna=True)
|
|
213
|
+
stats = self.run(**dict(zip(heatmap.index.names, best_params)))
|
|
214
|
+
|
|
215
|
+
if return_heatmap:
|
|
216
|
+
return stats, heatmap
|
|
217
|
+
return stats
|
|
218
|
+
|
|
219
|
+
def _optimize_sambo() -> (
|
|
220
|
+
pd.Series | tuple[pd.Series, pd.Series] | tuple[pd.Series, pd.Series, dict]
|
|
221
|
+
):
|
|
222
|
+
try:
|
|
223
|
+
import sambo
|
|
224
|
+
except ImportError:
|
|
225
|
+
raise ImportError(
|
|
226
|
+
"Need package 'sambo' for method='sambo'. pip install sambo"
|
|
227
|
+
) from None
|
|
228
|
+
|
|
229
|
+
nonlocal max_tries
|
|
230
|
+
max_tries = (
|
|
231
|
+
200
|
|
232
|
+
if max_tries is None
|
|
233
|
+
else max(1, int(max_tries * _grid_size()))
|
|
234
|
+
if 0 < max_tries <= 1
|
|
235
|
+
else max_tries
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
dimensions = []
|
|
239
|
+
for values in kwargs.values():
|
|
240
|
+
values = np.asarray(values)
|
|
241
|
+
if values.dtype.kind in "mM": # timedelta, datetime64
|
|
242
|
+
# these dtypes are unsupported in SAMBO, so convert to raw int
|
|
243
|
+
# TODO: save dtype and convert back later
|
|
244
|
+
values = values.astype(np.int64)
|
|
245
|
+
|
|
246
|
+
if values.dtype.kind in "iumM":
|
|
247
|
+
dimensions.append((values.min(), values.max() + 1))
|
|
248
|
+
elif values.dtype.kind == "f":
|
|
249
|
+
dimensions.append((values.min(), values.max()))
|
|
250
|
+
else:
|
|
251
|
+
dimensions.append(values.tolist())
|
|
252
|
+
|
|
253
|
+
# Avoid recomputing re-evaluations
|
|
254
|
+
@lru_cache
|
|
255
|
+
def memoized_run(tup):
|
|
256
|
+
nonlocal maximize, self
|
|
257
|
+
with (
|
|
258
|
+
patch(self, "_plotting_enabled", False),
|
|
259
|
+
patch(self, "_logs_enabled", False),
|
|
260
|
+
patch(self, "_portfolio_enabled", maximize_key is None),
|
|
261
|
+
):
|
|
262
|
+
stats = self.run(**dict(tup))
|
|
263
|
+
return -maximize(stats)
|
|
264
|
+
|
|
265
|
+
progress = iter(
|
|
266
|
+
_tqdm(
|
|
267
|
+
repeat(None),
|
|
268
|
+
total=max_tries,
|
|
269
|
+
leave=False,
|
|
270
|
+
desc=self.optimize.__qualname__,
|
|
271
|
+
mininterval=2,
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
_names = tuple(kwargs.keys())
|
|
275
|
+
|
|
276
|
+
def objective_function(x):
|
|
277
|
+
nonlocal progress, memoized_run, constraint, _names
|
|
278
|
+
next(progress)
|
|
279
|
+
value = memoized_run(tuple(zip(_names, x)))
|
|
280
|
+
return 0 if np.isnan(value) else value
|
|
281
|
+
|
|
282
|
+
def cons(x):
|
|
283
|
+
nonlocal constraint, _names
|
|
284
|
+
return constraint(AttrDict(zip(_names, x)))
|
|
285
|
+
|
|
286
|
+
res = sambo.minimize(
|
|
287
|
+
fun=objective_function,
|
|
288
|
+
bounds=dimensions,
|
|
289
|
+
constraints=cons,
|
|
290
|
+
max_iter=max_tries,
|
|
291
|
+
method="sceua",
|
|
292
|
+
rng=random_state,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
stats = self.run(**dict(zip(kwargs.keys(), res.x)))
|
|
296
|
+
output = [stats]
|
|
297
|
+
|
|
298
|
+
if return_heatmap:
|
|
299
|
+
heatmap = pd.Series(dict(zip(map(tuple, res.xv), -res.funv)), name=maximize_key)
|
|
300
|
+
heatmap.index.names = kwargs.keys()
|
|
301
|
+
heatmap.sort_index(inplace=True)
|
|
302
|
+
output.append(heatmap)
|
|
303
|
+
|
|
304
|
+
if return_optimization:
|
|
305
|
+
output.append(res)
|
|
306
|
+
|
|
307
|
+
return stats if len(output) == 1 else tuple(output)
|
|
308
|
+
|
|
309
|
+
if method == "grid":
|
|
310
|
+
output = _optimize_grid()
|
|
311
|
+
elif method in ("sambo", "skopt"):
|
|
312
|
+
with patch(
|
|
313
|
+
self,
|
|
314
|
+
"_indicator_cache",
|
|
315
|
+
IndicatorCache(getattr(self, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)),
|
|
316
|
+
):
|
|
317
|
+
output = _optimize_sambo()
|
|
318
|
+
else:
|
|
319
|
+
raise ValueError(f"Method should be 'grid' or 'sambo', not {method!r}")
|
|
320
|
+
return output
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
from openbacktesting.helpers import _set_legacy_module
|
|
324
|
+
|
|
325
|
+
_set_legacy_module(_joined_pool, "openbacktesting.optimization")
|
|
326
|
+
_set_legacy_module(_optimize, "openbacktesting.optimization")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Native parallel batches and shared-memory lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from multiprocessing import resource_tracker as _mprt
|
|
10
|
+
from multiprocessing import shared_memory as _mpshm
|
|
11
|
+
from threading import Lock
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from openbacktesting.helpers import _set_legacy_module, patch
|
|
17
|
+
|
|
18
|
+
if sys.version_info >= (3, 13):
|
|
19
|
+
SharedMemory = _mpshm.SharedMemory
|
|
20
|
+
else:
|
|
21
|
+
|
|
22
|
+
class SharedMemory(_mpshm.SharedMemory):
|
|
23
|
+
# From https://github.com/python/cpython/issues/82300#issuecomment-2169035092
|
|
24
|
+
__lock = Lock()
|
|
25
|
+
|
|
26
|
+
def __init__(self, *args, track: bool = True, **kwargs):
|
|
27
|
+
self._track = track
|
|
28
|
+
if track:
|
|
29
|
+
return super().__init__(*args, **kwargs) # noqa: PLE0101 — preserve the shared-memory initialization retry return
|
|
30
|
+
with self.__lock, patch(_mprt, "register", lambda *a, **kw: None):
|
|
31
|
+
super().__init__(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
def unlink(self):
|
|
34
|
+
if _mpshm._USE_POSIX and self._name:
|
|
35
|
+
_mpshm._posixshmem.shm_unlink(self._name)
|
|
36
|
+
if self._track:
|
|
37
|
+
_mprt.unregister(self._name, "shared_memory")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _batch(seq):
|
|
41
|
+
# XXX: Replace with itertools.batched
|
|
42
|
+
n = np.clip(int(len(seq) // (os.cpu_count() or 1)), 1, 300)
|
|
43
|
+
for i in range(0, len(seq), n):
|
|
44
|
+
yield seq[i : i + n]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SharedMemoryManager:
|
|
48
|
+
"""
|
|
49
|
+
A simple shared memory contextmanager based on
|
|
50
|
+
https://docs.python.org/3/library/multiprocessing.shared_memory.html#multiprocessing.shared_memory.SharedMemory
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, create=False) -> None:
|
|
54
|
+
self._shms: list[SharedMemory] = []
|
|
55
|
+
self.__create = create
|
|
56
|
+
|
|
57
|
+
def SharedMemory(self, *, name=None, create=False, size=0, track=True):
|
|
58
|
+
shm = SharedMemory(name=name, create=create, size=size, track=track)
|
|
59
|
+
shm._create = create
|
|
60
|
+
# Essential to keep refs on Windows
|
|
61
|
+
# https://stackoverflow.com/questions/74193377/filenotfounderror-when-passing-a-shared-memory-to-a-new-process#comment130999060_74194875
|
|
62
|
+
self._shms.append(shm)
|
|
63
|
+
return shm
|
|
64
|
+
|
|
65
|
+
def __enter__(self):
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(self, *args, **kwargs):
|
|
69
|
+
failures = []
|
|
70
|
+
for shm in self._shms:
|
|
71
|
+
try:
|
|
72
|
+
shm.close()
|
|
73
|
+
if shm._create:
|
|
74
|
+
shm.unlink()
|
|
75
|
+
except Exception as error: # noqa: BLE001 — release each segment, then re-raise the first error
|
|
76
|
+
failures.append(error)
|
|
77
|
+
warnings.warn(
|
|
78
|
+
f"Failed to unlink shared memory {shm.name!r}",
|
|
79
|
+
category=ResourceWarning,
|
|
80
|
+
stacklevel=2,
|
|
81
|
+
)
|
|
82
|
+
if failures:
|
|
83
|
+
raise failures[0]
|
|
84
|
+
|
|
85
|
+
def arr2shm(self, vals):
|
|
86
|
+
"""Array to shared memory. Returns (shm_name, shape, dtype) used for restore."""
|
|
87
|
+
assert vals.ndim == 1, (vals.ndim, vals.shape, vals)
|
|
88
|
+
# Not `vals.nbytes`; lazy indexes (e.g. RangeIndex) report their own,
|
|
89
|
+
# constant size rather than that of the array they materialize into
|
|
90
|
+
shm = self.SharedMemory(size=vals.size * vals.dtype.base.itemsize, create=True)
|
|
91
|
+
buf = np.ndarray(vals.shape, dtype=vals.dtype.base, buffer=shm.buf)
|
|
92
|
+
if isinstance(vals.dtype, pd.DatetimeTZDtype):
|
|
93
|
+
# Preserve absolute instants at their original precision in both index and columns; do not lose offsets for repeated DST hours.
|
|
94
|
+
buf.view("i8")[:] = vals.array.asi8
|
|
95
|
+
else:
|
|
96
|
+
buf[:] = vals
|
|
97
|
+
return shm.name, vals.shape, vals.dtype
|
|
98
|
+
|
|
99
|
+
def df2shm(self, df):
|
|
100
|
+
return tuple(
|
|
101
|
+
(
|
|
102
|
+
(column, *self.arr2shm(values))
|
|
103
|
+
for column, values in chain([(self._DF_INDEX_COL, df.index)], df.items())
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def shm2s(shm, shape, dtype) -> pd.Series:
|
|
109
|
+
arr = np.ndarray(shape, dtype=dtype.base, buffer=shm.buf)
|
|
110
|
+
arr.setflags(write=False)
|
|
111
|
+
if isinstance(dtype, pd.DatetimeTZDtype):
|
|
112
|
+
arr = pd.DatetimeIndex(arr).tz_localize("UTC").tz_convert(dtype.tz)
|
|
113
|
+
return pd.Series(arr, dtype=dtype, copy=False)
|
|
114
|
+
|
|
115
|
+
_DF_INDEX_COL = "__bt_index"
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def shm2df(data_shm):
|
|
119
|
+
shm = []
|
|
120
|
+
try:
|
|
121
|
+
for _, name, _, _ in data_shm:
|
|
122
|
+
shm.append(SharedMemory(name=name, create=False, track=False))
|
|
123
|
+
df = pd.DataFrame(
|
|
124
|
+
{
|
|
125
|
+
col: SharedMemoryManager.shm2s(handle, shape, dtype)
|
|
126
|
+
for handle, (col, _, shape, dtype) in zip(shm, data_shm)
|
|
127
|
+
},
|
|
128
|
+
copy=False,
|
|
129
|
+
)
|
|
130
|
+
df.set_index(SharedMemoryManager._DF_INDEX_COL, drop=True, inplace=True)
|
|
131
|
+
# Result statistics retain the market-data index; it must survive independently after the mapping closes.
|
|
132
|
+
df.index = df.index.copy(deep=True)
|
|
133
|
+
df.index.name = None
|
|
134
|
+
return df, shm
|
|
135
|
+
except BaseException:
|
|
136
|
+
for handle in shm:
|
|
137
|
+
handle.close()
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_set_legacy_module(_batch, "openbacktesting._util")
|
|
142
|
+
_set_legacy_module(SharedMemoryManager, "openbacktesting._util")
|
|
143
|
+
|
|
144
|
+
if sys.version_info < (3, 13):
|
|
145
|
+
_set_legacy_module(SharedMemory, "openbacktesting._util")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Existing implementations moved from lib: FractionalBacktest, MultiBacktest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from itertools import chain, count
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from openbacktesting.helpers import _set_legacy_module
|
|
10
|
+
from openbacktesting.simulation.core import Backtest
|
|
11
|
+
from openbacktesting.simulation.parallel import SharedMemoryManager, _batch
|
|
12
|
+
from openbacktesting.util import _tqdm
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FractionalBacktest(Backtest):
|
|
16
|
+
"""Removed price-scaling entry point; use Backtest and InstrumentSpec."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
raise TypeError(
|
|
20
|
+
"FractionalBacktest has been removed; use Backtest(instrument=InstrumentSpec(quantity_step=...)) and qty"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MultiBacktest:
|
|
25
|
+
"""Run one strategy on multiple datasets and aggregate statistics for each dataset.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Multi-dataset `openbacktesting.engine.Backtest` wrapper.
|
|
29
|
+
|
|
30
|
+
Run supplied `openbacktesting.engine.Strategy` on several instruments,
|
|
31
|
+
in parallel. Used for comparing strategy runs across many instruments
|
|
32
|
+
or classes of instruments. Example:
|
|
33
|
+
|
|
34
|
+
from openbacktesting.test import EURUSD, BTCUSD, SmaCross
|
|
35
|
+
btm = MultiBacktest([EURUSD, BTCUSD], SmaCross)
|
|
36
|
+
stats_per_ticker: pd.DataFrame = btm.run(fast=10, slow=20)
|
|
37
|
+
heatmap_per_ticker: pd.DataFrame = btm.optimize(...)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, df_list, strategy_cls, **kwargs):
|
|
41
|
+
self._dfs = df_list
|
|
42
|
+
self._strategy = strategy_cls
|
|
43
|
+
self._bt_kwargs = kwargs
|
|
44
|
+
|
|
45
|
+
def run(self, **kwargs):
|
|
46
|
+
"""Backtest each dataset and return statistics in dataset columns.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
Wraps `openbacktesting.engine.Backtest.run`. Returns `pd.DataFrame` with
|
|
50
|
+
currency indexes in columns.
|
|
51
|
+
"""
|
|
52
|
+
from openbacktesting import Pool
|
|
53
|
+
|
|
54
|
+
with Pool() as pool, SharedMemoryManager() as smm:
|
|
55
|
+
shm = [smm.df2shm(df) for df in self._dfs]
|
|
56
|
+
results = _tqdm(
|
|
57
|
+
pool.imap(
|
|
58
|
+
self._mp_task_run,
|
|
59
|
+
(
|
|
60
|
+
(df_batch, self._strategy, self._bt_kwargs, kwargs)
|
|
61
|
+
for df_batch in _batch(shm)
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
total=len(shm),
|
|
65
|
+
desc=self.run.__qualname__,
|
|
66
|
+
mininterval=2,
|
|
67
|
+
)
|
|
68
|
+
df = pd.DataFrame(list(chain(*results))).transpose()
|
|
69
|
+
return df
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _mp_task_run(args):
|
|
73
|
+
data_shm, strategy, bt_kwargs, run_kwargs = args
|
|
74
|
+
dfs, shms = zip(*(SharedMemoryManager.shm2df(i) for i in data_shm))
|
|
75
|
+
try:
|
|
76
|
+
return [
|
|
77
|
+
stats.filter(regex="^[^_]")
|
|
78
|
+
for stats in (Backtest(df, strategy, **bt_kwargs).run(**run_kwargs) for df in dfs)
|
|
79
|
+
]
|
|
80
|
+
finally:
|
|
81
|
+
for shmem in chain(*shms):
|
|
82
|
+
shmem.close()
|
|
83
|
+
|
|
84
|
+
def optimize(self, **kwargs) -> pd.DataFrame:
|
|
85
|
+
"""Optimize parameters separately for each dataset and aggregate parameter scores.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
Wraps `openbacktesting.engine.Backtest.optimize`, but returns `pd.DataFrame` with
|
|
89
|
+
currency indexes in columns.
|
|
90
|
+
|
|
91
|
+
heatmap: pd.DataFrame = btm.optimize(...)
|
|
92
|
+
mean_scores = heatmap.mean(axis=1)
|
|
93
|
+
"""
|
|
94
|
+
heatmaps = []
|
|
95
|
+
# Simple loop since bt.optimize already does its own multiprocessing
|
|
96
|
+
for df in _tqdm(self._dfs, desc=self.__class__.__name__, mininterval=2):
|
|
97
|
+
bt = Backtest(df, self._strategy, **self._bt_kwargs)
|
|
98
|
+
_best_stats, heatmap = bt.optimize( # type: ignore
|
|
99
|
+
return_heatmap=True, return_optimization=False, **kwargs
|
|
100
|
+
)
|
|
101
|
+
heatmaps.append(heatmap)
|
|
102
|
+
heatmap = pd.DataFrame(dict(zip(count(), heatmaps)))
|
|
103
|
+
return heatmap
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_set_legacy_module(FractionalBacktest, "openbacktesting.lib")
|
|
107
|
+
_set_legacy_module(MultiBacktest, "openbacktesting.lib")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Compatibility exports for the old strategy path; implemented in strategy.base."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from openbacktesting.strategy.bar_state import BarState as BarState
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from openbacktesting.strategy.base import Strategy as Strategy
|
|
9
|
+
|
|
10
|
+
__all__ = ["BarState", "Strategy"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __getattr__(name):
|
|
14
|
+
if name == "Strategy":
|
|
15
|
+
from openbacktesting.strategy.base import Strategy
|
|
16
|
+
|
|
17
|
+
globals()[name] = Strategy
|
|
18
|
+
return Strategy
|
|
19
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Immutable Bar state for fixed historical input."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class BarState:
|
|
8
|
+
"""First/last snapshot at the current execution position; saved values do not change with the run cursor.
|
|
9
|
+
|
|
10
|
+
is_first means input bar 0; is_last means the final input bar, not the end of the run.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
is_first: bool
|
|
14
|
+
is_last: bool
|