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,528 @@
|
|
|
1
|
+
"""Native backtest configuration, lifecycle, and per-bar execution coordination."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from copy import copy
|
|
9
|
+
from functools import partial
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
from numbers import Number
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from openbacktesting.broker.commission import Commission, normalize_commission
|
|
17
|
+
from openbacktesting.broker.core import _Broker
|
|
18
|
+
from openbacktesting.broker.instrument import InstrumentSpec
|
|
19
|
+
from openbacktesting.broker.models import Fill, RunSummary
|
|
20
|
+
from openbacktesting.broker.observations import Observations
|
|
21
|
+
from openbacktesting.data.context import DataContext
|
|
22
|
+
from openbacktesting.data.core import _Data
|
|
23
|
+
from openbacktesting.helpers import _set_legacy_module
|
|
24
|
+
from openbacktesting.indicators.runtime import (
|
|
25
|
+
DEFAULT_CACHE_BYTES,
|
|
26
|
+
IndicatorCache,
|
|
27
|
+
optimization_cache,
|
|
28
|
+
)
|
|
29
|
+
from openbacktesting.logs import StrategyLog, _Collector
|
|
30
|
+
from openbacktesting.plotting.collector import Plotting
|
|
31
|
+
from openbacktesting.plotting.types import PlottingError, PlottingLimits
|
|
32
|
+
from openbacktesting.portfolio.rates import validate_risk_free_rate
|
|
33
|
+
from openbacktesting.portfolio.snapshot import capture_snapshot
|
|
34
|
+
from openbacktesting.portfolio.stats import compute_stats
|
|
35
|
+
from openbacktesting.portfolio.trading import trading_results
|
|
36
|
+
from openbacktesting.simulation.collection import DEFAULT_COLLECTION, CollectionPolicy
|
|
37
|
+
from openbacktesting.simulation.config import DefaultOrderSize, validate_order_size
|
|
38
|
+
from openbacktesting.simulation.optimization import _optimize
|
|
39
|
+
from openbacktesting.simulation.parallel import SharedMemoryManager
|
|
40
|
+
from openbacktesting.strategy.base import Strategy
|
|
41
|
+
from openbacktesting.strategy.inputs import _definitions, resolve_native, resolve_request
|
|
42
|
+
from openbacktesting.util import _tqdm
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Backtest:
|
|
46
|
+
"""Store single-asset historical market data, strategy, and linear margin-account configuration.
|
|
47
|
+
|
|
48
|
+
data OHLC must contain finite positive prices, with an ordered unique index. instrument specifies quantity steps and an optional price grid.
|
|
49
|
+
cash is the initial balance; margin is the notional margin fraction; hedging controls retention of two-sided lots.
|
|
50
|
+
commission may be a rate, (fixed fee, rate), Commission, or pure fee function, charged once per actual execution.
|
|
51
|
+
spread and slippage apply to every buy/sell execution; slippage never permits orders to cross their limits.
|
|
52
|
+
execution_timing supports next_open or same_close; end_policy supports mark_to_market or close_at_last_close.
|
|
53
|
+
allow_short controls new short positions; max_gross_exposure caps gross notional/equity; valid reductions are always allowed.
|
|
54
|
+
exclusive_orders cancels previous entries and submits exits for existing lots when a new ordinary order is submitted.
|
|
55
|
+
context is optional; when provided, it requires a timezone-aware index and converts display timezone on a copy; strategies can read self.timeframe.
|
|
56
|
+
risk_free_rate is an annualized fraction affecting statistics only; default_order_size supplies the request budget when qty is omitted.
|
|
57
|
+
Low-level construction does not automatically merge @backtest settings; runtime handles declaration adoption.
|
|
58
|
+
No legacy size-fraction inference, price scaling, cross-currency trading, volume-based partial fills, or live execution.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
data: pd.DataFrame,
|
|
64
|
+
strategy: type[Strategy],
|
|
65
|
+
*,
|
|
66
|
+
cash: float = 10_000,
|
|
67
|
+
spread: float = 0.0,
|
|
68
|
+
commission: float
|
|
69
|
+
| tuple[float, float]
|
|
70
|
+
| Commission
|
|
71
|
+
| Callable[[float, float], float] = 0.0,
|
|
72
|
+
risk_free_rate: float = 0.0,
|
|
73
|
+
default_order_size: DefaultOrderSize | None = None,
|
|
74
|
+
margin: float = 1.0,
|
|
75
|
+
execution_timing="next_open",
|
|
76
|
+
instrument=None,
|
|
77
|
+
slippage=None,
|
|
78
|
+
allow_short=True,
|
|
79
|
+
max_gross_exposure=None,
|
|
80
|
+
hedging=False,
|
|
81
|
+
exclusive_orders=False,
|
|
82
|
+
end_policy="mark_to_market",
|
|
83
|
+
indicator_cache_bytes=DEFAULT_CACHE_BYTES,
|
|
84
|
+
context: DataContext | None = None,
|
|
85
|
+
plotting: bool = True,
|
|
86
|
+
plotting_limits: PlottingLimits | None = None,
|
|
87
|
+
_collection: CollectionPolicy = DEFAULT_COLLECTION,
|
|
88
|
+
):
|
|
89
|
+
if type(plotting) is not bool:
|
|
90
|
+
raise PlottingError("invalid-option", "plotting must be bool")
|
|
91
|
+
if plotting_limits is not None and not isinstance(plotting_limits, PlottingLimits):
|
|
92
|
+
raise PlottingError("invalid-limit", "plotting_limits must be PlottingLimits")
|
|
93
|
+
self._plotting_enabled = plotting and _collection.plotting
|
|
94
|
+
self._portfolio_enabled = _collection.portfolio
|
|
95
|
+
self._plotting_limits = plotting_limits
|
|
96
|
+
self._indicator_cache_bytes = IndicatorCache(indicator_cache_bytes).budget
|
|
97
|
+
if not (isinstance(strategy, type) and issubclass(strategy, Strategy)):
|
|
98
|
+
raise TypeError("`strategy` must be a Strategy sub-type")
|
|
99
|
+
if not isinstance(data, pd.DataFrame):
|
|
100
|
+
raise TypeError("`data` must be a pandas.DataFrame with columns")
|
|
101
|
+
if not isinstance(spread, Number):
|
|
102
|
+
raise TypeError("`spread` must be a float value, percent of entry order price")
|
|
103
|
+
self._risk_free_rate = validate_risk_free_rate(risk_free_rate)
|
|
104
|
+
self._default_order_size = validate_order_size(default_order_size)
|
|
105
|
+
if not callable(commission):
|
|
106
|
+
normalize_commission(commission)
|
|
107
|
+
|
|
108
|
+
data = data.copy(deep=False)
|
|
109
|
+
if context is not None:
|
|
110
|
+
if not isinstance(context, DataContext):
|
|
111
|
+
raise TypeError("context must be DataContext or None")
|
|
112
|
+
if not isinstance(data.index, pd.DatetimeIndex) or data.index.tz is None:
|
|
113
|
+
raise ValueError("Market data must use a timezone-aware DatetimeIndex when context is provided")
|
|
114
|
+
data.index = data.index.tz_convert(context.timezone)
|
|
115
|
+
self._context = context
|
|
116
|
+
|
|
117
|
+
# Convert index to datetime index
|
|
118
|
+
if (
|
|
119
|
+
not isinstance(data.index, pd.DatetimeIndex)
|
|
120
|
+
and not isinstance(data.index, pd.RangeIndex)
|
|
121
|
+
and
|
|
122
|
+
# Numeric index with most large numbers
|
|
123
|
+
(
|
|
124
|
+
data.index.is_numeric()
|
|
125
|
+
and (data.index > pd.Timestamp("1975").timestamp()).mean() > 0.8
|
|
126
|
+
)
|
|
127
|
+
):
|
|
128
|
+
try:
|
|
129
|
+
data.index = pd.to_datetime(data.index, infer_datetime_format=True)
|
|
130
|
+
except ValueError:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
if "Volume" not in data:
|
|
134
|
+
data["Volume"] = np.nan
|
|
135
|
+
|
|
136
|
+
if len(data) == 0:
|
|
137
|
+
raise ValueError("OHLC `data` is empty")
|
|
138
|
+
if len(data.columns.intersection({"Open", "High", "Low", "Close", "Volume"})) != 5:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
"`data` must be a pandas.DataFrame with columns "
|
|
141
|
+
"'Open', 'High', 'Low', 'Close', and (optionally) 'Volume'"
|
|
142
|
+
)
|
|
143
|
+
if data[["Open", "High", "Low", "Close"]].isnull().values.any():
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"Some OHLC values are missing (NaN). "
|
|
146
|
+
"Please strip those lines with `df.dropna()` or "
|
|
147
|
+
"fill them in with `df.interpolate()` or whatever."
|
|
148
|
+
)
|
|
149
|
+
instrument = instrument or InstrumentSpec()
|
|
150
|
+
instrument.validate_data(data)
|
|
151
|
+
if execution_timing not in ("next_open", "same_close"):
|
|
152
|
+
raise ValueError("execution_timing must be next_open or same_close")
|
|
153
|
+
if end_policy not in ("mark_to_market", "close_at_last_close"):
|
|
154
|
+
raise ValueError("end_policy must be mark_to_market or close_at_last_close")
|
|
155
|
+
if not isinstance(data.index, pd.DatetimeIndex):
|
|
156
|
+
warnings.warn(
|
|
157
|
+
"Data index is not datetime. Assuming simple periods, "
|
|
158
|
+
"but `pd.DateTimeIndex` is advised.",
|
|
159
|
+
stacklevel=2,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._data: pd.DataFrame = data
|
|
163
|
+
self._broker = partial(
|
|
164
|
+
_Broker,
|
|
165
|
+
cash=cash,
|
|
166
|
+
spread=spread,
|
|
167
|
+
commission=commission,
|
|
168
|
+
margin=margin,
|
|
169
|
+
execution_timing=execution_timing,
|
|
170
|
+
instrument=instrument,
|
|
171
|
+
slippage=slippage,
|
|
172
|
+
allow_short=allow_short,
|
|
173
|
+
max_gross_exposure=max_gross_exposure,
|
|
174
|
+
hedging=hedging,
|
|
175
|
+
exclusive_orders=exclusive_orders,
|
|
176
|
+
index=data.index,
|
|
177
|
+
)
|
|
178
|
+
self._strategy = strategy
|
|
179
|
+
self._results: pd.Series | None = None
|
|
180
|
+
self._end_policy = end_policy
|
|
181
|
+
self._log_collector = None
|
|
182
|
+
self._logs_enabled = _collection.logs
|
|
183
|
+
self._log_running = False
|
|
184
|
+
self._log_delegate = None
|
|
185
|
+
|
|
186
|
+
def run_inputs(self, request) -> pd.Series:
|
|
187
|
+
"""Submit an input request with schemaVersion/schemaHash/values keyed by unique title."""
|
|
188
|
+
with self._log_scope() as logs:
|
|
189
|
+
resolved = resolve_request(self._strategy, request)
|
|
190
|
+
self._log_delegate = logs
|
|
191
|
+
return self.run(**resolved.overrides())
|
|
192
|
+
|
|
193
|
+
def get_logs(self) -> list[dict[str, str | int | None]]:
|
|
194
|
+
"""Return an independent copy of all strategy logs for this run; empty before any run.
|
|
195
|
+
|
|
196
|
+
Failures preserve written records; new run/run_inputs/optimize calls clear old logs.
|
|
197
|
+
Logs contain time, bar_index, source file, and line; retained in full by default without silent truncation.
|
|
198
|
+
"""
|
|
199
|
+
logs = getattr(self, "_log_collector", None)
|
|
200
|
+
return [] if logs is None else logs.snapshot()
|
|
201
|
+
|
|
202
|
+
@contextmanager
|
|
203
|
+
def _log_scope(self):
|
|
204
|
+
if getattr(self, "_log_running", False):
|
|
205
|
+
raise RuntimeError("Backtest cannot run reentrantly")
|
|
206
|
+
self._log_collector = logs = _Collector(enabled=getattr(self, "_logs_enabled", True))
|
|
207
|
+
self._log_running = True
|
|
208
|
+
try:
|
|
209
|
+
yield logs
|
|
210
|
+
finally:
|
|
211
|
+
logs.close()
|
|
212
|
+
self._log_running = False
|
|
213
|
+
self._log_delegate = None
|
|
214
|
+
|
|
215
|
+
def run(self, **kwargs) -> pd.Series:
|
|
216
|
+
"""Backtest strategy parameters and return a statistics Series, plain-value ledger tables, and an optional diagnostic strategy.
|
|
217
|
+
|
|
218
|
+
Public statistics keys:
|
|
219
|
+
Start, End, Duration, Exposure Time [%]
|
|
220
|
+
Equity Final [$], Equity Peak [$], Return [%], Buy & Hold Return [%]
|
|
221
|
+
Return (Ann.) [%], Volatility (Ann.) [%], CAGR [%], Sharpe Ratio
|
|
222
|
+
Sortino Ratio, Calmar Ratio, Alpha [%], Beta
|
|
223
|
+
Max. Drawdown [%], Avg. Drawdown [%], Max. Drawdown Duration, Avg. Drawdown Duration
|
|
224
|
+
# Trades, Win Rate [%], Best Trade [%], Worst Trade [%]
|
|
225
|
+
Avg. Trade [%], Max. Trade Duration, Avg. Trade Duration, Profit Factor
|
|
226
|
+
Expectancy [%], SQN, Kelly Criterion, Commissions [$]
|
|
227
|
+
# Closed Lots, _strategy, _equity_curve, _trades
|
|
228
|
+
_orders, _order_sizing, _order_events, _fills, _trade_allocations
|
|
229
|
+
_positions, _account_curve, _final_account, _open_trades
|
|
230
|
+
|
|
231
|
+
Order and fill events are not gated by indicator NaN; next runs on every bar starting with the first.
|
|
232
|
+
Retain account and gross exposure on each normal bar; record terminal handling in a separate terminal snapshot.
|
|
233
|
+
Result attrs contain trading-policy version, configuration, and input snapshots; _strategy is not required to reconstruct trading facts.
|
|
234
|
+
"""
|
|
235
|
+
delegated = getattr(self, "_log_delegate", None)
|
|
236
|
+
if delegated is not None:
|
|
237
|
+
self._log_delegate = None
|
|
238
|
+
return self._run(kwargs, delegated)
|
|
239
|
+
with self._log_scope() as logs:
|
|
240
|
+
return self._run(kwargs, logs)
|
|
241
|
+
|
|
242
|
+
def _run(self, kwargs, logs):
|
|
243
|
+
if not hasattr(self, "_end_policy"):
|
|
244
|
+
raise TypeError(
|
|
245
|
+
"Backtest state from 0.1 cannot resume execution; reconstruct Backtest using execution_timing/end_policy"
|
|
246
|
+
)
|
|
247
|
+
resolved = resolve_native(self._strategy, kwargs)
|
|
248
|
+
data = _Data(self._data.copy(deep=True))
|
|
249
|
+
data._context = self._context
|
|
250
|
+
data._default_order_size = self._default_order_size
|
|
251
|
+
data._indicator_cache = getattr(self, "_indicator_cache", None) or IndicatorCache(
|
|
252
|
+
getattr(self, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)
|
|
253
|
+
)
|
|
254
|
+
broker: _Broker = self._broker(data=data)
|
|
255
|
+
if getattr(self, "_portfolio_enabled", True):
|
|
256
|
+
broker.observations = Observations()
|
|
257
|
+
strategy: Strategy = self._strategy(
|
|
258
|
+
broker, data, resolved if resolved is not None else kwargs
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
strategy._log = StrategyLog(logs)
|
|
262
|
+
|
|
263
|
+
def invoke(phase, callback, *args):
|
|
264
|
+
bar_index = None if strategy._bar_context is None else strategy._bar_context[0]
|
|
265
|
+
bar_time, time_kind = None, "none"
|
|
266
|
+
if bar_index is not None:
|
|
267
|
+
if isinstance(self._data.index, pd.DatetimeIndex):
|
|
268
|
+
label = self._data.index[bar_index]
|
|
269
|
+
bar_time = label.isoformat()
|
|
270
|
+
time_kind = "aware-datetime" if label.tzinfo is not None else "naive-datetime"
|
|
271
|
+
else:
|
|
272
|
+
time_kind = "ordinal"
|
|
273
|
+
with logs.phase(phase, bar_index, bar_time, time_kind):
|
|
274
|
+
return callback(*args)
|
|
275
|
+
|
|
276
|
+
collector = Plotting(
|
|
277
|
+
self._data.index,
|
|
278
|
+
strategy._indicator_cursor,
|
|
279
|
+
enabled=self._plotting_enabled,
|
|
280
|
+
limits=self._plotting_limits,
|
|
281
|
+
)
|
|
282
|
+
strategy._plotting = collector
|
|
283
|
+
finished = False
|
|
284
|
+
|
|
285
|
+
def notify(*, terminal=False):
|
|
286
|
+
# Closed notification batches: notifications produced by callbacks wait for the next notification point and cannot be drained recursively.
|
|
287
|
+
batch, broker._notifications = broker._notifications, []
|
|
288
|
+
broker.phase = "terminal" if terminal else "notify"
|
|
289
|
+
for event in batch:
|
|
290
|
+
phase = "on_fill" if isinstance(event, Fill) else "on_order"
|
|
291
|
+
collector._enter(phase.replace("on_", "terminal_") if terminal else phase)
|
|
292
|
+
invoke(
|
|
293
|
+
phase.replace("on_", "terminal_") if terminal else phase,
|
|
294
|
+
strategy.on_fill if isinstance(event, Fill) else strategy.on_order,
|
|
295
|
+
event,
|
|
296
|
+
)
|
|
297
|
+
collector._enter("idle")
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
collector._enter("init")
|
|
301
|
+
invoke("init", strategy.init)
|
|
302
|
+
collector._enter("idle")
|
|
303
|
+
strategy._strategy_data.validate()
|
|
304
|
+
data._update()
|
|
305
|
+
broker.instrument.validate_data(data.df)
|
|
306
|
+
strategy._indicator_cursor.phase = "run"
|
|
307
|
+
data._set_length(1)
|
|
308
|
+
strategy._indicator_cursor.length = 1
|
|
309
|
+
broker.phase = "start"
|
|
310
|
+
collector._enter("start")
|
|
311
|
+
strategy._enter_bar(0, len(self._data))
|
|
312
|
+
invoke("on_start", strategy.on_start)
|
|
313
|
+
collector._enter("idle")
|
|
314
|
+
reason = "end_of_data"
|
|
315
|
+
with np.errstate(invalid="ignore"):
|
|
316
|
+
for i in _tqdm(
|
|
317
|
+
range(len(self._data)),
|
|
318
|
+
desc=self.run.__qualname__,
|
|
319
|
+
unit="bar",
|
|
320
|
+
mininterval=2,
|
|
321
|
+
miniters=100,
|
|
322
|
+
):
|
|
323
|
+
data._set_length(i + 1)
|
|
324
|
+
strategy._indicator_cursor.length = i + 1
|
|
325
|
+
strategy._enter_bar(i, len(self._data))
|
|
326
|
+
broker._i = i
|
|
327
|
+
collector._enter("idle", i)
|
|
328
|
+
broker.process()
|
|
329
|
+
if broker.equity <= 0:
|
|
330
|
+
broker.capture()
|
|
331
|
+
reason = "insolvent"
|
|
332
|
+
broker.finish(liquidate=True, reason=reason)
|
|
333
|
+
broker._equity[i:] = broker.equity
|
|
334
|
+
notify(terminal=True)
|
|
335
|
+
break
|
|
336
|
+
notify()
|
|
337
|
+
broker.phase = "next"
|
|
338
|
+
collector._enter("next", i)
|
|
339
|
+
invoke("next", strategy.next)
|
|
340
|
+
collector._enter("idle")
|
|
341
|
+
if broker.execution_timing == "same_close":
|
|
342
|
+
broker.process(close=True)
|
|
343
|
+
notify()
|
|
344
|
+
broker.update_trails()
|
|
345
|
+
broker.capture()
|
|
346
|
+
else:
|
|
347
|
+
broker.finish(liquidate=self._end_policy == "close_at_last_close")
|
|
348
|
+
notify(terminal=True)
|
|
349
|
+
strategy._bar_context = None
|
|
350
|
+
broker.phase = "finish"
|
|
351
|
+
finished = True
|
|
352
|
+
collector._enter("finish")
|
|
353
|
+
invoke("on_finish", strategy.on_finish, RunSummary(reason, broker.account))
|
|
354
|
+
data._set_length(len(self._data))
|
|
355
|
+
strategy._indicator_cursor.length = len(self._data)
|
|
356
|
+
strategy._indicator_cursor.phase = "complete"
|
|
357
|
+
self._results = compute_stats(
|
|
358
|
+
trades=broker.closed_trades,
|
|
359
|
+
equity=broker._equity,
|
|
360
|
+
ohlc_data=self._data,
|
|
361
|
+
risk_free_rate=self._risk_free_rate,
|
|
362
|
+
strategy_instance=strategy,
|
|
363
|
+
exposure=broker._exposure,
|
|
364
|
+
initial_equity=broker.ledger.initial_balance,
|
|
365
|
+
)
|
|
366
|
+
self._results = trading_results(self._results, broker, self._end_policy)
|
|
367
|
+
self._results.attrs["statistics_risk_free_rate"] = self._risk_free_rate
|
|
368
|
+
self._results.attrs["statistics_initial_equity"] = broker.ledger.initial_balance
|
|
369
|
+
if resolved is not None:
|
|
370
|
+
self._results.attrs["input_snapshot"] = resolved.snapshot() | {
|
|
371
|
+
"engineVersion": version("openbacktesting")
|
|
372
|
+
}
|
|
373
|
+
self._results.attrs["indicator_snapshot"] = {
|
|
374
|
+
"engineVersion": version("openbacktesting"),
|
|
375
|
+
"indicators": [indicator.metadata for indicator in strategy._indicators],
|
|
376
|
+
}
|
|
377
|
+
snapshot = collector._finish(self._results, self._data, reason)
|
|
378
|
+
if snapshot is not None:
|
|
379
|
+
self._results.attrs["plotting_result"] = snapshot
|
|
380
|
+
self._results.attrs["strategy_logs"] = logs.snapshot()
|
|
381
|
+
if broker.observations is not None:
|
|
382
|
+
self._results.attrs["portfolio_snapshot"] = capture_snapshot(
|
|
383
|
+
self._results,
|
|
384
|
+
self._data,
|
|
385
|
+
broker.observations,
|
|
386
|
+
reason,
|
|
387
|
+
self._context.snapshot() if self._context else None,
|
|
388
|
+
)
|
|
389
|
+
return self._results
|
|
390
|
+
except BaseException as error:
|
|
391
|
+
if not finished:
|
|
392
|
+
strategy._bar_context = None
|
|
393
|
+
broker.phase = "finish"
|
|
394
|
+
finished = True
|
|
395
|
+
try:
|
|
396
|
+
collector._enter("finish")
|
|
397
|
+
invoke(
|
|
398
|
+
"on_finish",
|
|
399
|
+
strategy.on_finish,
|
|
400
|
+
RunSummary("error", broker.account, repr(error)),
|
|
401
|
+
)
|
|
402
|
+
except BaseException as finish_error: # noqa: BLE001 — preserve the primary exception and attach finish-hook failures
|
|
403
|
+
error.add_note(f"on_finish also failed: {finish_error!r}")
|
|
404
|
+
raise
|
|
405
|
+
finally:
|
|
406
|
+
strategy._bar_context = None
|
|
407
|
+
collector._close()
|
|
408
|
+
data._set_length(len(self._data))
|
|
409
|
+
strategy._indicator_cursor.length = len(self._data)
|
|
410
|
+
strategy._indicator_cursor.phase = "complete"
|
|
411
|
+
data._indicator_cache = None
|
|
412
|
+
broker.phase = "finished"
|
|
413
|
+
|
|
414
|
+
def optimize(
|
|
415
|
+
self,
|
|
416
|
+
*,
|
|
417
|
+
maximize: str | Callable[[pd.Series], float] = "SQN",
|
|
418
|
+
method: str = "grid",
|
|
419
|
+
max_tries: float | None = None,
|
|
420
|
+
constraint: Callable[[dict], bool] | None = None,
|
|
421
|
+
return_heatmap: bool = False,
|
|
422
|
+
return_optimization: bool = False,
|
|
423
|
+
random_state: int | None = None,
|
|
424
|
+
**kwargs,
|
|
425
|
+
) -> pd.Series | tuple[pd.Series, pd.Series] | tuple[pd.Series, pd.Series, dict]:
|
|
426
|
+
"""Search strategy-parameter combinations and select results by a specified statistic or scoring function.
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
Optimize strategy parameters to an optimal combination.
|
|
430
|
+
Returns result `pd.Series` of the best run.
|
|
431
|
+
|
|
432
|
+
`maximize` is a string key from the
|
|
433
|
+
`openbacktesting.engine.Backtest.run`-returned results series,
|
|
434
|
+
or a function that accepts this series object and returns a number;
|
|
435
|
+
the higher the better. By default, the method maximizes
|
|
436
|
+
Van Tharp's [System Quality Number](https://altpower.app/?q=System+Quality+Number).
|
|
437
|
+
|
|
438
|
+
`method` is the optimization method. Currently two methods are supported:
|
|
439
|
+
|
|
440
|
+
* `"grid"` which does an exhaustive (or randomized) search over the
|
|
441
|
+
cartesian product of parameter combinations, and
|
|
442
|
+
* `"sambo"` which finds close-to-optimal strategy parameters using
|
|
443
|
+
[model-based optimization], making at most `max_tries` evaluations.
|
|
444
|
+
|
|
445
|
+
[model-based optimization]: https://sambo-optimization.github.io
|
|
446
|
+
|
|
447
|
+
`max_tries` is the maximal number of strategy runs to perform.
|
|
448
|
+
If `method="grid"`, this results in randomized grid search.
|
|
449
|
+
If `max_tries` is a floating value between (0, 1], this sets the
|
|
450
|
+
number of runs to approximately that fraction of full grid space.
|
|
451
|
+
Alternatively, if integer, it denotes the absolute maximum number
|
|
452
|
+
of evaluations. If unspecified (default), grid search is exhaustive,
|
|
453
|
+
whereas for `method="sambo"`, `max_tries` is set to 200.
|
|
454
|
+
|
|
455
|
+
`constraint` is a function that accepts a dict-like object of
|
|
456
|
+
parameters (with values) and returns `True` when the combination
|
|
457
|
+
is admissible to test with. By default, any parameters combination
|
|
458
|
+
is considered admissible.
|
|
459
|
+
|
|
460
|
+
If `return_heatmap` is `True`, besides returning the result
|
|
461
|
+
series, an additional `pd.Series` is returned with a multiindex
|
|
462
|
+
of all admissible parameter combinations, which can be further
|
|
463
|
+
inspected as a table of parameter combinations and objective values.
|
|
464
|
+
|
|
465
|
+
If `return_optimization` is True and `method = 'sambo'`,
|
|
466
|
+
in addition to result series (and maybe heatmap), return raw
|
|
467
|
+
[`scipy.optimize.OptimizeResult`][OptimizeResult] for further
|
|
468
|
+
inspection by the caller.
|
|
469
|
+
|
|
470
|
+
[OptimizeResult]: https://sambo-optimization.github.io/doc/sambo/#sambo.OptimizeResult
|
|
471
|
+
[SAMBO]: https://sambo-optimization.github.io
|
|
472
|
+
|
|
473
|
+
If you want reproducible optimization results, set `random_state`
|
|
474
|
+
to a fixed integer random seed.
|
|
475
|
+
|
|
476
|
+
Additional keyword arguments represent strategy arguments with
|
|
477
|
+
list-like collections of possible values. For example, the following
|
|
478
|
+
code finds and returns the "best" of the 7 admissible (of the
|
|
479
|
+
9 possible) parameter combinations:
|
|
480
|
+
|
|
481
|
+
best_stats = backtest.optimize(sma1=[5, 10, 15], sma2=[10, 20, 40],
|
|
482
|
+
constraint=lambda p: p.sma1 < p.sma2)
|
|
483
|
+
"""
|
|
484
|
+
if getattr(self, "_log_running", False):
|
|
485
|
+
raise RuntimeError("Backtest cannot optimize reentrantly")
|
|
486
|
+
self._log_collector = None
|
|
487
|
+
return _optimize(
|
|
488
|
+
self,
|
|
489
|
+
Backtest._mp_task,
|
|
490
|
+
maximize=maximize,
|
|
491
|
+
method=method,
|
|
492
|
+
max_tries=max_tries,
|
|
493
|
+
constraint=constraint,
|
|
494
|
+
return_heatmap=return_heatmap,
|
|
495
|
+
return_optimization=return_optimization,
|
|
496
|
+
random_state=random_state,
|
|
497
|
+
**kwargs,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
@staticmethod
|
|
501
|
+
def _mp_task(arg):
|
|
502
|
+
bt, data_shm, params_batch = arg
|
|
503
|
+
# Thread pools receive the same template object; batches must isolate mutable run state such as _data/_results.
|
|
504
|
+
bt = copy(bt)
|
|
505
|
+
bt._plotting_enabled = False
|
|
506
|
+
bt._logs_enabled = False
|
|
507
|
+
bt._log_collector = None
|
|
508
|
+
bt._data, shm = SharedMemoryManager.shm2df(data_shm)
|
|
509
|
+
try:
|
|
510
|
+
token = getattr(bt, "_indicator_cache_token", None)
|
|
511
|
+
if token is not None:
|
|
512
|
+
bt._indicator_cache = optimization_cache(
|
|
513
|
+
token, getattr(bt, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)
|
|
514
|
+
)
|
|
515
|
+
declared = bool(_definitions(bt._strategy))
|
|
516
|
+
return [
|
|
517
|
+
stats.filter(regex="^[^_]") if stats["# Trades"] else None
|
|
518
|
+
for stats in (
|
|
519
|
+
bt.run_inputs(params) if declared else bt.run(**params)
|
|
520
|
+
for params in params_batch
|
|
521
|
+
)
|
|
522
|
+
]
|
|
523
|
+
finally:
|
|
524
|
+
for shmem in shm:
|
|
525
|
+
shmem.close()
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
_set_legacy_module(Backtest, "openbacktesting.engine")
|