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,330 @@
|
|
|
1
|
+
"""Consume successfully loaded strategy modules; discovery needs no data, and the host injects chart context for execution."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
from inspect import isabstract
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from openbacktesting.broker.instrument import InstrumentSpec
|
|
11
|
+
from openbacktesting.data.context import DataContext
|
|
12
|
+
from openbacktesting.indicators.runtime import DEFAULT_CACHE_BYTES
|
|
13
|
+
from openbacktesting.logs import RunLogs
|
|
14
|
+
from openbacktesting.optimizer.query import ExperimentResult
|
|
15
|
+
from openbacktesting.plotting.types import PlottingLimits
|
|
16
|
+
from openbacktesting.simulation.collection import DEFAULT_COLLECTION, CollectionPolicy
|
|
17
|
+
from openbacktesting.simulation.core import Backtest
|
|
18
|
+
from openbacktesting.strategy.base import Strategy
|
|
19
|
+
from openbacktesting.strategy.declaration import declaration
|
|
20
|
+
from openbacktesting.strategy.inputs import describe as describe_inputs
|
|
21
|
+
from openbacktesting.strategy.inputs import resolve_request
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def discover(namespace: Mapping) -> type[Strategy]:
|
|
25
|
+
"""Discover the single concrete strategy directly decorated in this module; deduplicate aliases and exclude imported and inherited entry points.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
namespace: Successfully loaded module namespace containing a nonempty string __name__.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
type[Strategy]: Original strategy class, without instance construction.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
TypeError: Invalid namespace or declaration.
|
|
35
|
+
ValueError: No entry point or multiple entry points.
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(namespace, Mapping):
|
|
38
|
+
raise TypeError("namespace must be a module mapping")
|
|
39
|
+
name = namespace.get("__name__")
|
|
40
|
+
if not isinstance(name, str) or not name:
|
|
41
|
+
raise TypeError("namespace must contain a nonempty __name__")
|
|
42
|
+
candidates = {}
|
|
43
|
+
for value in namespace.values():
|
|
44
|
+
if (
|
|
45
|
+
isinstance(value, type)
|
|
46
|
+
and issubclass(value, Strategy)
|
|
47
|
+
and value.__module__ == name
|
|
48
|
+
and not isabstract(value)
|
|
49
|
+
and declaration(value) is not None
|
|
50
|
+
):
|
|
51
|
+
candidates[id(value)] = value
|
|
52
|
+
if len(candidates) != 1:
|
|
53
|
+
names = sorted(f"{cls.__module__}:{cls.__qualname__}" for cls in candidates.values())
|
|
54
|
+
raise ValueError(f"The module must contain exactly one @backtest strategy; candidates: {names}")
|
|
55
|
+
return next(iter(candidates.values()))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def describe(namespace: Mapping) -> dict:
|
|
59
|
+
"""Export inputs and complete default backtest settings without market data or executing the strategy lifecycle.
|
|
60
|
+
|
|
61
|
+
The returned backtest description is independent of input schemaHash; modifying copies does not alter class declarations.
|
|
62
|
+
"""
|
|
63
|
+
cls = discover(namespace)
|
|
64
|
+
settings = declaration(cls)
|
|
65
|
+
return describe_inputs(cls) | {
|
|
66
|
+
"backtest": {"schemaVersion": 2, "settings": settings.snapshot()}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def run(
|
|
71
|
+
namespace: Mapping,
|
|
72
|
+
request: Mapping | None = None,
|
|
73
|
+
*,
|
|
74
|
+
data: pd.DataFrame,
|
|
75
|
+
context: DataContext,
|
|
76
|
+
instrument: InstrumentSpec,
|
|
77
|
+
settings: Mapping | None = None,
|
|
78
|
+
options: Mapping | None = None,
|
|
79
|
+
logs: RunLogs | None = None,
|
|
80
|
+
) -> pd.Series:
|
|
81
|
+
"""Run a new session for this chart and return existing results plus backtest_snapshot.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
namespace: Module successfully loaded by the host for this run; bt-only exports are no longer accepted.
|
|
85
|
+
request: Existing title-based input request; None uses defaults.
|
|
86
|
+
data: Current chart OHLC with a timezone-aware DatetimeIndex.
|
|
87
|
+
context: Actual timeframe and shared timezone for charts, strategy calendars, and statistics.
|
|
88
|
+
Convert the internal index before strategy construction, preserving inputs and absolute instants without resampling.
|
|
89
|
+
Changing timezone requires a new run; local-time conditions and statistics grouped by day may change.
|
|
90
|
+
instrument: Chart instrument specifications, without inference from market data.
|
|
91
|
+
settings: Sparse overrides of flat settings; enum fields accept corresponding members or exact strings.
|
|
92
|
+
Changing types requires supplying a value too; default_qty_value=None clears the default quantity.
|
|
93
|
+
options: cache budget, plotting, and plotting_limits are host runtime options.
|
|
94
|
+
logs: Optional host log session; get_logs works after success or exceptions, and sequential reuse clears old records.
|
|
95
|
+
|
|
96
|
+
No source loading, cached old instances, or declaration changes; exceptions propagate unchanged.
|
|
97
|
+
"""
|
|
98
|
+
if logs is not None and not isinstance(logs, RunLogs):
|
|
99
|
+
raise TypeError("logs must be RunLogs or None")
|
|
100
|
+
session = RunLogs() if logs is None else logs
|
|
101
|
+
with session._capture():
|
|
102
|
+
return _run(namespace, request, data, context, instrument, settings, options, session)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _run(
|
|
106
|
+
namespace,
|
|
107
|
+
request,
|
|
108
|
+
data,
|
|
109
|
+
context,
|
|
110
|
+
instrument,
|
|
111
|
+
settings,
|
|
112
|
+
options,
|
|
113
|
+
session,
|
|
114
|
+
collection=DEFAULT_COLLECTION,
|
|
115
|
+
):
|
|
116
|
+
cls = discover(namespace)
|
|
117
|
+
resolved = declaration(cls).resolve(settings)
|
|
118
|
+
if not isinstance(context, DataContext):
|
|
119
|
+
raise TypeError("context must be DataContext")
|
|
120
|
+
if not isinstance(instrument, InstrumentSpec):
|
|
121
|
+
raise TypeError("instrument must be InstrumentSpec")
|
|
122
|
+
if options is None:
|
|
123
|
+
options = {}
|
|
124
|
+
if not isinstance(options, Mapping):
|
|
125
|
+
raise TypeError("options must be a mapping or None")
|
|
126
|
+
defaults = {
|
|
127
|
+
"indicator_cache_bytes": DEFAULT_CACHE_BYTES,
|
|
128
|
+
"plotting": True,
|
|
129
|
+
"plotting_limits": PlottingLimits(),
|
|
130
|
+
}
|
|
131
|
+
unknown = set(options).difference(defaults)
|
|
132
|
+
if unknown:
|
|
133
|
+
raise ValueError(f"Unknown runtime option: {list(unknown)!r}")
|
|
134
|
+
effective_options = defaults | dict(options)
|
|
135
|
+
if effective_options["plotting_limits"] is None:
|
|
136
|
+
effective_options["plotting_limits"] = PlottingLimits()
|
|
137
|
+
# Digest and value errors must surface before constructing any strategy, preserving existing title-based entry semantics.
|
|
138
|
+
if request is not None:
|
|
139
|
+
resolve_request(cls, request)
|
|
140
|
+
bt = Backtest(
|
|
141
|
+
data,
|
|
142
|
+
cls,
|
|
143
|
+
context=context,
|
|
144
|
+
instrument=instrument,
|
|
145
|
+
_collection=collection,
|
|
146
|
+
**resolved.kwargs(),
|
|
147
|
+
**effective_options,
|
|
148
|
+
)
|
|
149
|
+
try:
|
|
150
|
+
result = bt.run() if request is None else bt.run_inputs(request)
|
|
151
|
+
finally:
|
|
152
|
+
session._collector = bt._log_collector
|
|
153
|
+
result.attrs["backtest_snapshot"] = {
|
|
154
|
+
"schemaVersion": 2,
|
|
155
|
+
"strategy": {"module": cls.__module__, "qualname": cls.__qualname__},
|
|
156
|
+
"settings": resolved.snapshot(),
|
|
157
|
+
"context": context.snapshot(),
|
|
158
|
+
"instrument": asdict(instrument),
|
|
159
|
+
"options": effective_options
|
|
160
|
+
| {"plotting_limits": asdict(effective_options["plotting_limits"])},
|
|
161
|
+
"engineVersion": version("openbacktesting"),
|
|
162
|
+
}
|
|
163
|
+
from openbacktesting.portfolio.snapshot import refresh_snapshot
|
|
164
|
+
|
|
165
|
+
refresh_snapshot(result)
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def optimize(
|
|
170
|
+
namespace: Mapping,
|
|
171
|
+
search: Mapping,
|
|
172
|
+
*,
|
|
173
|
+
path,
|
|
174
|
+
identity,
|
|
175
|
+
data: pd.DataFrame,
|
|
176
|
+
context: DataContext,
|
|
177
|
+
instrument: InstrumentSpec,
|
|
178
|
+
request=None,
|
|
179
|
+
settings=None,
|
|
180
|
+
constraint=None,
|
|
181
|
+
constraint_id=None,
|
|
182
|
+
cancel=None,
|
|
183
|
+
on_progress=None,
|
|
184
|
+
) -> ExperimentResult:
|
|
185
|
+
"""Exhaustively search selected inputs sequentially, committing each complete Portfolio to a new local result database.
|
|
186
|
+
|
|
187
|
+
Unselected values come from the current request; retain only Portfolio without collecting logs/plotting.
|
|
188
|
+
identity is a host-supplied ExecutionIdentity; the framework computes data/configuration identities.
|
|
189
|
+
constraint uses complete wire values and requires constraint_id; cancel is a no-argument callable.
|
|
190
|
+
Return a read-only ExperimentResult; record strategy-trial failures and continue, but stop on storage failures.
|
|
191
|
+
"""
|
|
192
|
+
return _optimize(
|
|
193
|
+
namespace,
|
|
194
|
+
search,
|
|
195
|
+
path,
|
|
196
|
+
identity,
|
|
197
|
+
data,
|
|
198
|
+
context,
|
|
199
|
+
instrument,
|
|
200
|
+
request,
|
|
201
|
+
settings,
|
|
202
|
+
constraint,
|
|
203
|
+
constraint_id,
|
|
204
|
+
cancel,
|
|
205
|
+
on_progress,
|
|
206
|
+
None,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def resume_optimization(
|
|
211
|
+
namespace: Mapping,
|
|
212
|
+
*,
|
|
213
|
+
path,
|
|
214
|
+
experiment_id: str,
|
|
215
|
+
identity,
|
|
216
|
+
data: pd.DataFrame,
|
|
217
|
+
context: DataContext,
|
|
218
|
+
instrument: InstrumentSpec,
|
|
219
|
+
request=None,
|
|
220
|
+
settings=None,
|
|
221
|
+
constraint=None,
|
|
222
|
+
constraint_id=None,
|
|
223
|
+
cancel=None,
|
|
224
|
+
on_progress=None,
|
|
225
|
+
) -> ExperimentResult:
|
|
226
|
+
"""Resume unfinished trials after checking current declarations, inputs, and execution identity; failed trials are not automatically retried."""
|
|
227
|
+
from openbacktesting.optimizer.models import Range, Values
|
|
228
|
+
from openbacktesting.optimizer.query import open_result
|
|
229
|
+
|
|
230
|
+
source = open_result(path, experiment_id).info()["plan"]
|
|
231
|
+
search = {}
|
|
232
|
+
for dimension in source["dimensions"]:
|
|
233
|
+
expression = dimension["expression"]
|
|
234
|
+
search[dimension["title"]] = (
|
|
235
|
+
Range(**expression["range"]) if "range" in expression else Values(expression["values"])
|
|
236
|
+
)
|
|
237
|
+
return _optimize(
|
|
238
|
+
namespace,
|
|
239
|
+
search,
|
|
240
|
+
path,
|
|
241
|
+
identity,
|
|
242
|
+
data,
|
|
243
|
+
context,
|
|
244
|
+
instrument,
|
|
245
|
+
request,
|
|
246
|
+
settings,
|
|
247
|
+
constraint,
|
|
248
|
+
constraint_id,
|
|
249
|
+
cancel,
|
|
250
|
+
on_progress,
|
|
251
|
+
experiment_id,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _optimize(
|
|
256
|
+
namespace,
|
|
257
|
+
search,
|
|
258
|
+
path,
|
|
259
|
+
identity,
|
|
260
|
+
data,
|
|
261
|
+
context,
|
|
262
|
+
instrument,
|
|
263
|
+
request,
|
|
264
|
+
settings,
|
|
265
|
+
constraint,
|
|
266
|
+
constraint_id,
|
|
267
|
+
cancel,
|
|
268
|
+
on_progress,
|
|
269
|
+
experiment_id,
|
|
270
|
+
):
|
|
271
|
+
from copy import deepcopy
|
|
272
|
+
|
|
273
|
+
from openbacktesting.optimizer.identity import execution_identity
|
|
274
|
+
from openbacktesting.optimizer.models import _ExportError
|
|
275
|
+
from openbacktesting.optimizer.plan import plan
|
|
276
|
+
from openbacktesting.optimizer.runner import execute
|
|
277
|
+
from openbacktesting.portfolio.snapshot import export_result
|
|
278
|
+
|
|
279
|
+
cls = discover(namespace)
|
|
280
|
+
resolved = declaration(cls).resolve(settings)
|
|
281
|
+
if not isinstance(context, DataContext) or not isinstance(instrument, InstrumentSpec):
|
|
282
|
+
raise TypeError("optimizer: DataContext and InstrumentSpec must be supplied")
|
|
283
|
+
frozen = data.copy(deep=True)
|
|
284
|
+
run_settings = resolved.snapshot()
|
|
285
|
+
backtest = {
|
|
286
|
+
"schemaVersion": 2,
|
|
287
|
+
"settings": run_settings,
|
|
288
|
+
"context": context.snapshot(),
|
|
289
|
+
"instrument": asdict(instrument),
|
|
290
|
+
"engineVersion": version("openbacktesting"),
|
|
291
|
+
}
|
|
292
|
+
execution = execution_identity(frozen, identity, backtest)
|
|
293
|
+
plan_value = plan(
|
|
294
|
+
cls, search, base_inputs=request, execution=execution, constraint_id=constraint_id
|
|
295
|
+
)
|
|
296
|
+
# Construction validation does not execute strategies; reject invalid market data before creating a database or running the first trial.
|
|
297
|
+
Backtest(
|
|
298
|
+
frozen, cls, context=context, instrument=instrument, **resolved.kwargs(), plotting=False
|
|
299
|
+
)
|
|
300
|
+
namespace = dict(namespace)
|
|
301
|
+
collection = CollectionPolicy(portfolio=True, logs=False, plotting=False)
|
|
302
|
+
|
|
303
|
+
def run_one(inputs_request):
|
|
304
|
+
session = RunLogs()
|
|
305
|
+
with session._capture():
|
|
306
|
+
result = _run(
|
|
307
|
+
namespace,
|
|
308
|
+
inputs_request,
|
|
309
|
+
frozen,
|
|
310
|
+
context,
|
|
311
|
+
instrument,
|
|
312
|
+
deepcopy(run_settings),
|
|
313
|
+
{"plotting": False},
|
|
314
|
+
session,
|
|
315
|
+
collection,
|
|
316
|
+
)
|
|
317
|
+
try:
|
|
318
|
+
return export_result(result)
|
|
319
|
+
except Exception as error:
|
|
320
|
+
raise _ExportError(str(error)) from error
|
|
321
|
+
|
|
322
|
+
return execute(
|
|
323
|
+
plan_value,
|
|
324
|
+
run_one,
|
|
325
|
+
path,
|
|
326
|
+
experiment_id=experiment_id,
|
|
327
|
+
constraint=constraint,
|
|
328
|
+
cancel=cancel,
|
|
329
|
+
on_progress=on_progress,
|
|
330
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Lazy class exports for the simulation package; implemented in simulation.core."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from openbacktesting.simulation.core import Backtest as Backtest
|
|
7
|
+
|
|
8
|
+
__all__ = ["Backtest"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name):
|
|
12
|
+
if name == "Backtest":
|
|
13
|
+
from openbacktesting.simulation.core import Backtest
|
|
14
|
+
|
|
15
|
+
globals()[name] = Backtest
|
|
16
|
+
return Backtest
|
|
17
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Internal run-collection policy; defaults match ordinary backtest behavior."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class CollectionPolicy:
|
|
8
|
+
portfolio: bool = True
|
|
9
|
+
logs: bool = True
|
|
10
|
+
plotting: bool = True
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_COLLECTION = CollectionPolicy()
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Exportable default backtest settings independent of strategies, hosts, and execution lifecycles."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, fields, replace
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from openbacktesting.broker.commission import Commission
|
|
8
|
+
from openbacktesting.broker.execution import Slippage
|
|
9
|
+
from openbacktesting.broker.models import finite
|
|
10
|
+
from openbacktesting.portfolio.rates import validate_risk_free_rate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CommissionType(Enum):
|
|
14
|
+
"""Author-facing commission type; PERCENT uses percentage points, and other members use account amounts."""
|
|
15
|
+
|
|
16
|
+
PERCENT = "percent"
|
|
17
|
+
CASH_PER_ORDER = "cash_per_order"
|
|
18
|
+
CASH_PER_CONTRACT = "cash_per_contract"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QuantityType(Enum):
|
|
22
|
+
"""Default order size: Fixed asset quantity or a percentage of equity at request time."""
|
|
23
|
+
|
|
24
|
+
FIXED = "fixed"
|
|
25
|
+
PERCENT_OF_EQUITY = "percent_of_equity"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SlippageType(Enum):
|
|
29
|
+
"""Slippage distance: Fixed price units or basis points of price."""
|
|
30
|
+
|
|
31
|
+
FIXED = "fixed"
|
|
32
|
+
BPS = "bps"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExecutionTiming(Enum):
|
|
36
|
+
"""Execution timing: Process no earlier than the next bar, or process new unconditional market orders at this bar's close."""
|
|
37
|
+
|
|
38
|
+
NEXT_OPEN = "next_open"
|
|
39
|
+
SAME_CLOSE = "same_close"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EndPolicy(Enum):
|
|
43
|
+
"""End policy: Retain marked positions or liquidate actual positions at the final close."""
|
|
44
|
+
|
|
45
|
+
MARK_TO_MARKET = "mark_to_market"
|
|
46
|
+
CLOSE_AT_LAST_CLOSE = "close_at_last_close"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_ENUM_FIELDS = {
|
|
50
|
+
"commission_type": CommissionType,
|
|
51
|
+
"default_qty_type": QuantityType,
|
|
52
|
+
"slippage_type": SlippageType,
|
|
53
|
+
"execution_timing": ExecutionTiming,
|
|
54
|
+
"end_policy": EndPolicy,
|
|
55
|
+
}
|
|
56
|
+
_PAIRED_FIELDS = (
|
|
57
|
+
("commission_type", "commission_value"),
|
|
58
|
+
("default_qty_type", "default_qty_value"),
|
|
59
|
+
("slippage_type", "slippage_value"),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _decode_enums(values: Mapping) -> dict:
|
|
64
|
+
"""Convert exact strings to corresponding enum members only at the host boundary."""
|
|
65
|
+
result = dict(values)
|
|
66
|
+
for name, kind in _ENUM_FIELDS.items():
|
|
67
|
+
if name not in result:
|
|
68
|
+
continue
|
|
69
|
+
value = result[name]
|
|
70
|
+
if type(value) is str:
|
|
71
|
+
try:
|
|
72
|
+
result[name] = kind(value)
|
|
73
|
+
except ValueError:
|
|
74
|
+
choices = ", ".join(member.value for member in kind)
|
|
75
|
+
raise ValueError(f"{name} must be one of: {choices}") from None
|
|
76
|
+
elif not isinstance(value, kind):
|
|
77
|
+
raise TypeError(f"{name} must be {kind.__name__} member or corresponding string")
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class DefaultOrderSize:
|
|
83
|
+
"""Request budget when ordinary orders omit qty; does not represent a target position or filled notional."""
|
|
84
|
+
|
|
85
|
+
mode: str
|
|
86
|
+
value: float
|
|
87
|
+
|
|
88
|
+
def __post_init__(self):
|
|
89
|
+
if self.mode not in ("fixed", "equity_fraction"):
|
|
90
|
+
raise ValueError("mode must be fixed or equity_fraction")
|
|
91
|
+
object.__setattr__(self, "value", finite(self.value, "value", positive=True))
|
|
92
|
+
if self.mode == "equity_fraction" and self.value > 1:
|
|
93
|
+
raise ValueError("equity_fraction must be in (0, 1]")
|
|
94
|
+
|
|
95
|
+
def snapshot(self) -> dict:
|
|
96
|
+
"""Export independent mode and value."""
|
|
97
|
+
return {"mode": self.mode, "value": self.value}
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_snapshot(cls, value: Mapping) -> "DefaultOrderSize":
|
|
101
|
+
"""Strictly restore complete configuration without filling missing fields."""
|
|
102
|
+
if not isinstance(value, Mapping):
|
|
103
|
+
raise TypeError("DefaultOrderSize snapshot must be a mapping")
|
|
104
|
+
if set(value) != {"mode", "value"}:
|
|
105
|
+
raise ValueError("DefaultOrderSize snapshot has missing or unknown fields")
|
|
106
|
+
return cls(value["mode"], value["value"])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def validate_order_size(value: DefaultOrderSize | None) -> DefaultOrderSize | None:
|
|
110
|
+
"""Accept only validated immutable models or explicit clearing."""
|
|
111
|
+
if value is not None and not isinstance(value, DefaultOrderSize):
|
|
112
|
+
raise TypeError("default_order_size must be DefaultOrderSize or None")
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class BacktestSettings:
|
|
118
|
+
"""Flat immutable author-facing settings; validate enums and numbers after decoding at the host boundary."""
|
|
119
|
+
|
|
120
|
+
cash: float = 10_000
|
|
121
|
+
spread: float = 0.0
|
|
122
|
+
commission_type: CommissionType = CommissionType.PERCENT
|
|
123
|
+
commission_value: float = 0.0
|
|
124
|
+
risk_free_rate: float = 0.0
|
|
125
|
+
default_qty_type: QuantityType = QuantityType.FIXED
|
|
126
|
+
default_qty_value: float | None = None
|
|
127
|
+
margin: float = 1.0
|
|
128
|
+
execution_timing: ExecutionTiming = ExecutionTiming.NEXT_OPEN
|
|
129
|
+
slippage_type: SlippageType = SlippageType.BPS
|
|
130
|
+
slippage_value: float = 0.0
|
|
131
|
+
allow_short: bool = True
|
|
132
|
+
max_gross_exposure: float | None = None
|
|
133
|
+
hedging: bool = False
|
|
134
|
+
exclusive_orders: bool = False
|
|
135
|
+
end_policy: EndPolicy = EndPolicy.MARK_TO_MARKET
|
|
136
|
+
|
|
137
|
+
def __post_init__(self):
|
|
138
|
+
for name, kind in _ENUM_FIELDS.items():
|
|
139
|
+
if not isinstance(getattr(self, name), kind):
|
|
140
|
+
raise TypeError(f"{name} must be {kind.__name__} member")
|
|
141
|
+
object.__setattr__(self, "risk_free_rate", validate_risk_free_rate(self.risk_free_rate))
|
|
142
|
+
for name in ("cash", "margin"):
|
|
143
|
+
object.__setattr__(self, name, finite(getattr(self, name), name, positive=True))
|
|
144
|
+
spread = finite(self.spread, "spread")
|
|
145
|
+
object.__setattr__(self, "spread", spread)
|
|
146
|
+
if self.margin > 1:
|
|
147
|
+
raise ValueError("margin must be in (0,1]")
|
|
148
|
+
if not 0 <= spread < 1:
|
|
149
|
+
raise ValueError("spread must be in [0,1)")
|
|
150
|
+
if self.max_gross_exposure is not None:
|
|
151
|
+
object.__setattr__(
|
|
152
|
+
self,
|
|
153
|
+
"max_gross_exposure",
|
|
154
|
+
finite(self.max_gross_exposure, "max_gross_exposure", positive=True),
|
|
155
|
+
)
|
|
156
|
+
for name in ("allow_short", "hedging", "exclusive_orders"):
|
|
157
|
+
if type(getattr(self, name)) is not bool:
|
|
158
|
+
raise TypeError(f"{name} must be bool")
|
|
159
|
+
value = finite(self.commission_value, "commission_value")
|
|
160
|
+
if self.commission_type is CommissionType.PERCENT:
|
|
161
|
+
if not -10 <= value < 10:
|
|
162
|
+
raise ValueError("commission_value percentage must be in [-10,10)")
|
|
163
|
+
elif value < 0:
|
|
164
|
+
raise ValueError("commission_value amount must not be negative")
|
|
165
|
+
object.__setattr__(self, "commission_value", value)
|
|
166
|
+
if self.default_qty_value is not None:
|
|
167
|
+
value = finite(self.default_qty_value, "default_qty_value", positive=True)
|
|
168
|
+
if self.default_qty_type is QuantityType.PERCENT_OF_EQUITY and value > 100:
|
|
169
|
+
raise ValueError("default_qty_value equity percentage must be in (0,100]")
|
|
170
|
+
object.__setattr__(self, "default_qty_value", value)
|
|
171
|
+
value = finite(self.slippage_value, "slippage_value")
|
|
172
|
+
if value < 0:
|
|
173
|
+
raise ValueError("slippage_value must not be negative")
|
|
174
|
+
object.__setattr__(self, "slippage_value", value)
|
|
175
|
+
|
|
176
|
+
def _values(self) -> dict:
|
|
177
|
+
return {field.name: getattr(self, field.name) for field in fields(self)}
|
|
178
|
+
|
|
179
|
+
def kwargs(self) -> dict:
|
|
180
|
+
"""Adapt to low-level Backtest arguments, separately from flat snapshots and the override allowlist."""
|
|
181
|
+
result = self._values()
|
|
182
|
+
for type_name, value_name in _PAIRED_FIELDS:
|
|
183
|
+
del result[type_name], result[value_name]
|
|
184
|
+
if self.commission_type is CommissionType.PERCENT:
|
|
185
|
+
commission = Commission(relative=self.commission_value / 100)
|
|
186
|
+
elif self.commission_type is CommissionType.CASH_PER_ORDER:
|
|
187
|
+
commission = Commission(fixed=self.commission_value)
|
|
188
|
+
else:
|
|
189
|
+
commission = Commission(per_unit=self.commission_value)
|
|
190
|
+
size = None
|
|
191
|
+
if self.default_qty_value is not None:
|
|
192
|
+
size = (
|
|
193
|
+
DefaultOrderSize("fixed", self.default_qty_value)
|
|
194
|
+
if self.default_qty_type is QuantityType.FIXED
|
|
195
|
+
else DefaultOrderSize("equity_fraction", self.default_qty_value / 100)
|
|
196
|
+
)
|
|
197
|
+
return result | {
|
|
198
|
+
"commission": commission,
|
|
199
|
+
"default_order_size": size,
|
|
200
|
+
"slippage": Slippage(self.slippage_type.value, self.slippage_value),
|
|
201
|
+
"execution_timing": self.execution_timing.value,
|
|
202
|
+
"end_policy": self.end_policy.value,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def resolve(self, overrides: Mapping | None) -> "BacktestSettings":
|
|
206
|
+
"""Decode sparse host overrides; a changed type must be supplied together with a new value."""
|
|
207
|
+
if overrides is None:
|
|
208
|
+
overrides = {}
|
|
209
|
+
if not isinstance(overrides, Mapping):
|
|
210
|
+
raise TypeError("settings must be a mapping or None")
|
|
211
|
+
unknown = set(overrides).difference(self._values())
|
|
212
|
+
if unknown:
|
|
213
|
+
raise ValueError(f"Unknown backtest setting: {list(unknown)!r}")
|
|
214
|
+
values = _decode_enums(overrides)
|
|
215
|
+
for type_name, value_name in _PAIRED_FIELDS:
|
|
216
|
+
if (
|
|
217
|
+
type_name in values
|
|
218
|
+
and values[type_name] is not getattr(self, type_name)
|
|
219
|
+
and value_name not in values
|
|
220
|
+
):
|
|
221
|
+
raise ValueError(f"Changing {type_name} requires also supplying {value_name}")
|
|
222
|
+
return replace(self, **values)
|
|
223
|
+
|
|
224
|
+
def snapshot(self) -> dict:
|
|
225
|
+
"""Export all flat fields; enums become strings and percentages retain author units."""
|
|
226
|
+
return {
|
|
227
|
+
name: value.value if isinstance(value, Enum) else value
|
|
228
|
+
for name, value in self._values().items()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def from_snapshot(cls, value: Mapping) -> "BacktestSettings":
|
|
233
|
+
"""Strictly restore complete JSON settings; sparse fields and Python enums are not accepted."""
|
|
234
|
+
if not isinstance(value, Mapping):
|
|
235
|
+
raise TypeError("Backtest settings snapshot must be a mapping")
|
|
236
|
+
if set(value) != {field.name for field in fields(cls)}:
|
|
237
|
+
raise ValueError("Backtest settings snapshot has missing or unknown fields")
|
|
238
|
+
for name, item in value.items():
|
|
239
|
+
if type(item) not in (str, int, float, bool, type(None)):
|
|
240
|
+
raise TypeError(f"{name} must be a JSON primitive")
|
|
241
|
+
return cls(**_decode_enums(value))
|