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,10 @@
|
|
|
1
|
+
"""ta.pvt: Computation wrapper for the pinned dependency with canonical outputs."""
|
|
2
|
+
|
|
3
|
+
from pandas_ta_classic.volume.pvt import pvt as _calculate
|
|
4
|
+
|
|
5
|
+
from ..common import named_series
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute(*, close, volume, drift, talib):
|
|
9
|
+
result = _calculate(close=close, volume=volume, drift=drift, talib=talib)
|
|
10
|
+
return named_series(result, "ta.pvt", "pvt")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""ta.vfi: Computation wrapper for the pinned dependency with canonical outputs."""
|
|
2
|
+
|
|
3
|
+
from pandas_ta_classic.volume.vfi import vfi as _calculate
|
|
4
|
+
|
|
5
|
+
from ..common import named_series
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute(*, close, volume, length, coef, vcoef, mamode, talib):
|
|
9
|
+
result = _calculate(
|
|
10
|
+
close=close,
|
|
11
|
+
volume=volume,
|
|
12
|
+
length=length,
|
|
13
|
+
coef=coef,
|
|
14
|
+
vcoef=vcoef,
|
|
15
|
+
mamode=mamode,
|
|
16
|
+
talib=talib,
|
|
17
|
+
)
|
|
18
|
+
return named_series(result, "ta.vfi", "vfi")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""ta.vosc: Computation wrapper for the pinned dependency with canonical outputs."""
|
|
2
|
+
|
|
3
|
+
from pandas_ta_classic.volume.vosc import vosc as _calculate
|
|
4
|
+
|
|
5
|
+
from ..common import named_series
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute(*, volume, fast, slow, talib):
|
|
9
|
+
result = _calculate(volume=volume, fast=fast, slow=slow, talib=talib)
|
|
10
|
+
return named_series(result, "ta.vosc", "vosc")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""ta.wad: Computation wrapper for the pinned dependency with canonical outputs."""
|
|
2
|
+
|
|
3
|
+
from pandas_ta_classic.volume.wad import wad as _calculate
|
|
4
|
+
|
|
5
|
+
from ..common import named_series
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute(*, high, low, close, talib):
|
|
9
|
+
result = _calculate(high=high, low=low, close=close, talib=talib)
|
|
10
|
+
return named_series(result, "ta.wad", "wad")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""ta.wvad: Independent formula with a fixed initialization contract."""
|
|
2
|
+
|
|
3
|
+
from ..calculus import clean, ratio
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute(*, open_, high, low, close, volume):
|
|
7
|
+
return ratio(clean(volume) * (clean(close) - clean(open_)), clean(high) - clean(low)).rename(
|
|
8
|
+
"wvad"
|
|
9
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Compatibility exports for the old inputs path; implemented in strategy.inputs."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.strategy.inputs import (
|
|
4
|
+
_SAFE_INT, # noqa: F401 — re-export the legacy entry point
|
|
5
|
+
_UNSET, # noqa: F401 — re-export the legacy entry point
|
|
6
|
+
SCHEMA_VERSION,
|
|
7
|
+
InputError,
|
|
8
|
+
InputSpec,
|
|
9
|
+
ResolvedInputs,
|
|
10
|
+
_collect, # noqa: F401 — re-export the legacy entry point
|
|
11
|
+
_definitions, # noqa: F401 — re-export the legacy entry point
|
|
12
|
+
_fail, # noqa: F401 — re-export the legacy entry point
|
|
13
|
+
_make, # noqa: F401 — re-export the legacy entry point
|
|
14
|
+
_resolve, # noqa: F401 — re-export the legacy entry point
|
|
15
|
+
_scalar, # noqa: F401 — re-export the legacy entry point
|
|
16
|
+
_schema, # noqa: F401 — re-export the legacy entry point
|
|
17
|
+
_session, # noqa: F401 — re-export the legacy entry point
|
|
18
|
+
bool,
|
|
19
|
+
color,
|
|
20
|
+
describe,
|
|
21
|
+
enum,
|
|
22
|
+
float,
|
|
23
|
+
int,
|
|
24
|
+
price,
|
|
25
|
+
request_for,
|
|
26
|
+
resolve_native,
|
|
27
|
+
resolve_request,
|
|
28
|
+
session,
|
|
29
|
+
string,
|
|
30
|
+
time,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"SCHEMA_VERSION",
|
|
35
|
+
"InputError",
|
|
36
|
+
"InputSpec",
|
|
37
|
+
"ResolvedInputs",
|
|
38
|
+
"bool",
|
|
39
|
+
"color",
|
|
40
|
+
"describe",
|
|
41
|
+
"enum",
|
|
42
|
+
"float",
|
|
43
|
+
"int",
|
|
44
|
+
"price",
|
|
45
|
+
"request_for",
|
|
46
|
+
"resolve_native",
|
|
47
|
+
"resolve_request",
|
|
48
|
+
"session",
|
|
49
|
+
"string",
|
|
50
|
+
"time",
|
|
51
|
+
]
|
openbacktesting/lib.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Legacy entry point for convenience APIs; each domain directly re-exports the same objects."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.data.aggregation import OHLCV_AGG, TRADES_AGG
|
|
4
|
+
from openbacktesting.data.generators import random_ohlc_data
|
|
5
|
+
from openbacktesting.portfolio.helpers import compute_stats
|
|
6
|
+
from openbacktesting.simulation.core import Backtest # noqa: F401
|
|
7
|
+
from openbacktesting.simulation.variants import FractionalBacktest, MultiBacktest
|
|
8
|
+
from openbacktesting.strategy.base import Strategy # noqa: F401
|
|
9
|
+
from openbacktesting.strategy.helpers import barssince, quantile, resample_apply
|
|
10
|
+
from openbacktesting.strategy.templates import SignalStrategy, TrailingStrategy
|
|
11
|
+
|
|
12
|
+
__pdoc__ = {
|
|
13
|
+
"Strategy.__init__": False,
|
|
14
|
+
"SignalStrategy.__init__": False,
|
|
15
|
+
"TrailingStrategy.__init__": False,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
__all__ = [ # noqa: RUF022 — preserve existing export order
|
|
19
|
+
"OHLCV_AGG",
|
|
20
|
+
"TRADES_AGG",
|
|
21
|
+
"barssince",
|
|
22
|
+
"quantile",
|
|
23
|
+
"compute_stats",
|
|
24
|
+
"resample_apply",
|
|
25
|
+
"random_ohlc_data",
|
|
26
|
+
"SignalStrategy",
|
|
27
|
+
"TrailingStrategy",
|
|
28
|
+
"FractionalBacktest",
|
|
29
|
+
"FractionalBacktest", # noqa: RUF068 — preserve existing duplicate exports
|
|
30
|
+
"MultiBacktest",
|
|
31
|
+
]
|
openbacktesting/logs.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Independent strategy logs for each historical run; no connection to global Python logging or output streams."""
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager as _contextmanager
|
|
4
|
+
from datetime import UTC
|
|
5
|
+
from datetime import datetime as _datetime
|
|
6
|
+
from inspect import currentframe as _currentframe
|
|
7
|
+
from threading import Lock as _Lock
|
|
8
|
+
from typing import NamedTuple as _NamedTuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _Record(_NamedTuple):
|
|
12
|
+
sequence: int
|
|
13
|
+
level: str
|
|
14
|
+
message: str
|
|
15
|
+
phase: str
|
|
16
|
+
bar_index: int | None
|
|
17
|
+
bar_time: str | None
|
|
18
|
+
time_kind: str
|
|
19
|
+
recorded_at: str
|
|
20
|
+
source_file: str | None
|
|
21
|
+
source_line: int | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _Collector:
|
|
25
|
+
def __init__(self, *, enabled=True):
|
|
26
|
+
self.enabled = enabled
|
|
27
|
+
self._records: list[_Record] = []
|
|
28
|
+
self._context = None
|
|
29
|
+
self._closed = False
|
|
30
|
+
|
|
31
|
+
@_contextmanager
|
|
32
|
+
def phase(self, phase, bar_index=None, bar_time=None, time_kind="none"):
|
|
33
|
+
if self._closed:
|
|
34
|
+
raise RuntimeError("Strategy log session has ended")
|
|
35
|
+
self._context = (phase, bar_index, bar_time, time_kind)
|
|
36
|
+
try:
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
self._context = None
|
|
40
|
+
|
|
41
|
+
def close(self):
|
|
42
|
+
self._closed = True
|
|
43
|
+
self._context = None
|
|
44
|
+
|
|
45
|
+
def snapshot(self) -> list[dict[str, str | int | None]]:
|
|
46
|
+
return [record._asdict() for record in self._records]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StrategyLog:
|
|
50
|
+
"""Read-only strategy log handle, accessed through self.log without author construction.
|
|
51
|
+
|
|
52
|
+
Record strings inside active author callbacks; error denotes only a level and does not stop the backtest.
|
|
53
|
+
Automatically record the actual calling file and line; calls after termination raise RuntimeError.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, collector: _Collector):
|
|
57
|
+
self._collector = collector
|
|
58
|
+
|
|
59
|
+
def info(self, message: str) -> None:
|
|
60
|
+
"""Record informational messages, preserving empty strings, Chinese text, and newlines."""
|
|
61
|
+
self._write("info", message)
|
|
62
|
+
|
|
63
|
+
def warning(self, message: str) -> None:
|
|
64
|
+
"""Record warning messages without changing strategy execution."""
|
|
65
|
+
self._write("warning", message)
|
|
66
|
+
|
|
67
|
+
def error(self, message: str) -> None:
|
|
68
|
+
"""Record error-level messages; authors must explicitly raise to stop the strategy."""
|
|
69
|
+
self._write("error", message)
|
|
70
|
+
|
|
71
|
+
def _write(self, level, message):
|
|
72
|
+
if not isinstance(message, str):
|
|
73
|
+
raise TypeError("Strategy log message must be a string")
|
|
74
|
+
collector = self._collector
|
|
75
|
+
if collector._context is None:
|
|
76
|
+
raise RuntimeError("Strategy logs can only be written inside active author callbacks")
|
|
77
|
+
if not collector.enabled:
|
|
78
|
+
return
|
|
79
|
+
source_file, source_line = None, None
|
|
80
|
+
frame = _currentframe()
|
|
81
|
+
caller = None
|
|
82
|
+
try:
|
|
83
|
+
# Skip this function and the three public level methods explicitly; do not scan author helper functions.
|
|
84
|
+
caller = frame.f_back if frame is not None else None
|
|
85
|
+
caller = caller.f_back if caller is not None else None
|
|
86
|
+
if caller is not None:
|
|
87
|
+
filename, lineno = caller.f_code.co_filename, caller.f_lineno
|
|
88
|
+
if filename and lineno > 0:
|
|
89
|
+
source_file, source_line = filename, lineno
|
|
90
|
+
finally:
|
|
91
|
+
del frame, caller
|
|
92
|
+
collector._records.append(
|
|
93
|
+
_Record(
|
|
94
|
+
len(collector._records) + 1,
|
|
95
|
+
level,
|
|
96
|
+
message,
|
|
97
|
+
*collector._context,
|
|
98
|
+
_datetime.now(UTC).isoformat(timespec="microseconds"),
|
|
99
|
+
source_file,
|
|
100
|
+
source_line,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class RunLogs:
|
|
106
|
+
"""Host-owned single-run log session, injected through runtime.run(..., logs=session).
|
|
107
|
+
|
|
108
|
+
Sequential reuse clears records before strategy discovery; a session cannot serve concurrent runs.
|
|
109
|
+
Queryable after success or exceptional exit; returns list and dictionary copies without holding author stack frames.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self):
|
|
113
|
+
self._collector = _Collector()
|
|
114
|
+
self._lock = _Lock()
|
|
115
|
+
|
|
116
|
+
def get_logs(self) -> list[dict[str, str | int | None]]:
|
|
117
|
+
"""Return an independent copy of all logs from this run; an empty list before the first run."""
|
|
118
|
+
return self._collector.snapshot()
|
|
119
|
+
|
|
120
|
+
@_contextmanager
|
|
121
|
+
def _capture(self):
|
|
122
|
+
if not self._lock.acquire(blocking=False):
|
|
123
|
+
raise RuntimeError("RunLogs is already occupied by another run")
|
|
124
|
+
try:
|
|
125
|
+
self._collector = _Collector()
|
|
126
|
+
yield
|
|
127
|
+
finally:
|
|
128
|
+
self._collector.close()
|
|
129
|
+
self._lock.release()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Compatibility exports for the old optimization path; implemented in backtest.optimization."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.simulation.optimization import (
|
|
4
|
+
_joined_pool, # noqa: F401 — re-export the legacy entry point
|
|
5
|
+
_optimize, # noqa: F401 — re-export the legacy entry point
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = []
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Exhaustive parameter search, local storage of individual Portfolios, and subsequent queries."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.optimizer import identity as identity
|
|
4
|
+
from openbacktesting.optimizer.models import (
|
|
5
|
+
ExecutionIdentity,
|
|
6
|
+
OptimizationError,
|
|
7
|
+
Plan,
|
|
8
|
+
Range,
|
|
9
|
+
Values,
|
|
10
|
+
)
|
|
11
|
+
from openbacktesting.optimizer.plan import plan
|
|
12
|
+
from openbacktesting.optimizer.query import ExperimentResult, open_result
|
|
13
|
+
from openbacktesting.optimizer.runner import execute
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ExecutionIdentity",
|
|
17
|
+
"ExperimentResult",
|
|
18
|
+
"OptimizationError",
|
|
19
|
+
"Plan",
|
|
20
|
+
"Range",
|
|
21
|
+
"Values",
|
|
22
|
+
"execute",
|
|
23
|
+
"open_result",
|
|
24
|
+
"plan",
|
|
25
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Generated by scripts/generate-contracts.mjs; do not edit manually.
|
|
2
|
+
"""Cross-table Portfolio semantics; Schema ensures structure first, then this layer rejects inconsistent references."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def check_portfolio(p):
|
|
8
|
+
def require(ok, reason):
|
|
9
|
+
if not ok:
|
|
10
|
+
raise ValueError("portfolio-result: " + reason)
|
|
11
|
+
|
|
12
|
+
def unique(rows, key):
|
|
13
|
+
values = [r[key] for r in rows]
|
|
14
|
+
require(len(values) == len(set(values)), "duplicate_" + key)
|
|
15
|
+
return {r[key]: r for r in rows}
|
|
16
|
+
|
|
17
|
+
def close(a, b):
|
|
18
|
+
return math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-8)
|
|
19
|
+
|
|
20
|
+
facts, trades, series = p["facts"], p["trades"], p["series"]
|
|
21
|
+
orders = unique(facts["orders"], "order_id")
|
|
22
|
+
fills = unique(facts["fills"], "fill_id")
|
|
23
|
+
allocations = unique(facts["allocations"], "allocation_id")
|
|
24
|
+
observations = unique(series["account"], "observationId")
|
|
25
|
+
fragments = unique(trades["closedFragments"], "fragmentId")
|
|
26
|
+
unique(trades["openLots"], "lotId")
|
|
27
|
+
require(bool(observations), "empty_observations")
|
|
28
|
+
require(
|
|
29
|
+
[r["observationSequence"] for r in observations.values()]
|
|
30
|
+
== list(range(1, len(observations) + 1)),
|
|
31
|
+
"observation_sequence",
|
|
32
|
+
)
|
|
33
|
+
require(
|
|
34
|
+
all(
|
|
35
|
+
a["barIndex"] <= b["barIndex"] for a, b in zip(series["account"], series["account"][1:])
|
|
36
|
+
),
|
|
37
|
+
"bar_order",
|
|
38
|
+
)
|
|
39
|
+
require(series["account"][-1]["barIndex"] == p["run"]["processedThroughBar"], "executed_range")
|
|
40
|
+
for r in series["account"]:
|
|
41
|
+
require(0 <= r["barIndex"] <= p["run"]["processedThroughBar"], "bar_range")
|
|
42
|
+
for rows in (facts["orderSizing"], facts["orderEvents"], facts["fills"]):
|
|
43
|
+
require(all(r["order_id"] in orders for r in rows), "order_reference")
|
|
44
|
+
for table in (facts["fills"], facts["orderEvents"]):
|
|
45
|
+
seq = [r["sequence"] for r in table]
|
|
46
|
+
require(seq == sorted(set(seq)), "event_sequence")
|
|
47
|
+
require(
|
|
48
|
+
all(
|
|
49
|
+
r["fillSequence"] in {0} | {f["sequence"] for f in fills.values()}
|
|
50
|
+
for r in series["account"]
|
|
51
|
+
),
|
|
52
|
+
"fill_sequence_reference",
|
|
53
|
+
)
|
|
54
|
+
for a in allocations.values():
|
|
55
|
+
require(a["fill_id"] in fills and a["entry_fill_id"] in fills, "fill_reference")
|
|
56
|
+
require(
|
|
57
|
+
set(fragments)
|
|
58
|
+
== {a["allocation_id"] for a in allocations.values() if a["kind"] == "close"},
|
|
59
|
+
"fragment_allocations",
|
|
60
|
+
)
|
|
61
|
+
require(
|
|
62
|
+
[t["displayOrdinal"] for t in fragments.values()] == list(range(1, len(fragments) + 1)),
|
|
63
|
+
"fragment_order",
|
|
64
|
+
)
|
|
65
|
+
for t in list(fragments.values()) + trades["openLots"]:
|
|
66
|
+
require(
|
|
67
|
+
t["entry"]["fillId"] in fills and t["entry"]["orderId"] in orders, "entry_reference"
|
|
68
|
+
)
|
|
69
|
+
for t in fragments.values():
|
|
70
|
+
require(t["exit"]["fillId"] in fills and t["exit"]["orderId"] in orders, "exit_reference")
|
|
71
|
+
require(allocations[t["fragmentId"]]["trade_id"] == t["lotId"], "lot_reference")
|
|
72
|
+
require(close(t["pnlNet"], t["pnlGross"] - t["feeTotal"]), "trade_accounting")
|
|
73
|
+
require(all(t["asOf"] in observations for t in trades["openLots"]), "open_asof")
|
|
74
|
+
for row in series["close"]:
|
|
75
|
+
require(row == observations.get(row["observationId"]), "close_reference")
|
|
76
|
+
require(len(series["margin"]) == len(observations), "margin_length")
|
|
77
|
+
for row, account in zip(series["margin"], series["account"]):
|
|
78
|
+
require(row["observationId"] == account["observationId"], "margin_axis")
|
|
79
|
+
for curve in series["directions"].values():
|
|
80
|
+
require(len(curve) == len(observations), "direction_length")
|
|
81
|
+
for row, account in zip(curve, series["account"]):
|
|
82
|
+
require(row["observationId"] == account["observationId"], "direction_axis")
|
|
83
|
+
initial = p["summary"]["all"]["initialCapital"]["value"]
|
|
84
|
+
for row, long, short in zip(
|
|
85
|
+
series["account"], series["directions"]["long"], series["directions"]["short"]
|
|
86
|
+
):
|
|
87
|
+
require(
|
|
88
|
+
close(row["equity"] - initial, long["contribution"] + short["contribution"]),
|
|
89
|
+
"direction_accounting",
|
|
90
|
+
)
|
|
91
|
+
require(
|
|
92
|
+
close(sum(f["fee"] for f in fills.values()), sum(a["fee"] for a in allocations.values())),
|
|
93
|
+
"fee_conservation",
|
|
94
|
+
)
|
|
95
|
+
require(p["summary"]["all"]["closedFragmentCount"]["value"] == len(fragments), "closed_count")
|
|
96
|
+
require(p["summary"]["all"]["openLotCount"]["value"] == len(trades["openLots"]), "open_count")
|
|
97
|
+
for row in series["closedCumulative"]:
|
|
98
|
+
require(row["fragmentId"] in fragments, "cumulative_reference")
|
|
99
|
+
for bucket in p["analysis"]["distribution"]["histogram"]:
|
|
100
|
+
require(bucket["count"] == len(bucket["fragmentIds"]), "histogram_count")
|
|
101
|
+
require(all(ref in fragments for ref in bucket["fragmentIds"]), "histogram_reference")
|
|
102
|
+
|
|
103
|
+
histogram_ids = [
|
|
104
|
+
ref
|
|
105
|
+
for bucket in p["analysis"]["distribution"]["histogram"]
|
|
106
|
+
for ref in bucket["fragmentIds"]
|
|
107
|
+
]
|
|
108
|
+
require(
|
|
109
|
+
len(histogram_ids) == len(fragments) and set(histogram_ids) == set(fragments),
|
|
110
|
+
"histogram_coverage",
|
|
111
|
+
)
|
|
112
|
+
for t in list(fragments.values()) + trades["openLots"]:
|
|
113
|
+
for leg in (t["entry"], t["exit"]):
|
|
114
|
+
if leg is not None:
|
|
115
|
+
fill = fills[leg["fillId"]]
|
|
116
|
+
require(
|
|
117
|
+
leg["orderId"] == fill["order_id"]
|
|
118
|
+
and leg["sequence"] == fill["sequence"]
|
|
119
|
+
and leg["barIndex"] == fill["bar_index"]
|
|
120
|
+
and leg["price"] == fill["price"],
|
|
121
|
+
"leg_fill_consistency",
|
|
122
|
+
)
|
|
123
|
+
total = sum(t["pnlNet"] for t in fragments.values()) + sum(
|
|
124
|
+
t["openPnlNet"] for t in trades["openLots"]
|
|
125
|
+
)
|
|
126
|
+
require(close(total, series["account"][-1]["equity"] - initial), "portfolio_accounting")
|
|
127
|
+
for name in ("drawdowns", "alternating"):
|
|
128
|
+
for episode in p["analysis"]["growthAndDecline"][name]:
|
|
129
|
+
require(
|
|
130
|
+
all(episode[k] in observations for k in ("start", "end", "extreme")),
|
|
131
|
+
"episode_reference",
|
|
132
|
+
)
|
|
133
|
+
for ref in p["analysis"]["distribution"]["outliers"]["fragmentIds"]:
|
|
134
|
+
require(ref in fragments, "outlier_reference")
|
|
135
|
+
for streak in p["analysis"]["streaks"]["rows"]:
|
|
136
|
+
require(
|
|
137
|
+
streak["startFragmentId"] in fragments and streak["endFragmentId"] in fragments,
|
|
138
|
+
"streak_reference",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def metrics(value):
|
|
142
|
+
if isinstance(value, dict):
|
|
143
|
+
if "methodId" in value and "unit" in value and "status" in value:
|
|
144
|
+
missing = value["value"] is None
|
|
145
|
+
require((value["status"] in ("ok", "approximate")) != missing, "metric_status")
|
|
146
|
+
require(not missing or bool(value["reason"]), "metric_reason")
|
|
147
|
+
for item in value.values():
|
|
148
|
+
metrics(item)
|
|
149
|
+
elif isinstance(value, list):
|
|
150
|
+
for item in value:
|
|
151
|
+
metrics(item)
|
|
152
|
+
|
|
153
|
+
metrics(p["summary"])
|
|
154
|
+
metrics(p["analysis"])
|
|
155
|
+
metrics(p["extensions"])
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Calculate experiment identity only for stably encodable market data and host declarations."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import platform
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from openbacktesting.optimizer.models import ExecutionIdentity, digest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execution_identity(data, identity, backtest):
|
|
13
|
+
if not isinstance(identity, ExecutionIdentity):
|
|
14
|
+
raise TypeError("optimizer: identity must be an ExecutionIdentity")
|
|
15
|
+
if not isinstance(data, pd.DataFrame) or not isinstance(data.index, pd.DatetimeIndex):
|
|
16
|
+
raise TypeError("optimizer: data must be a DataFrame with a datetime index")
|
|
17
|
+
if not data.columns.is_unique or any(type(c) is not str for c in data.columns):
|
|
18
|
+
raise ValueError("optimizer: market-data column names must be unique strings")
|
|
19
|
+
for dtype in data.dtypes:
|
|
20
|
+
if dtype.kind not in "biufMm" or str(dtype) == "object":
|
|
21
|
+
raise TypeError("optimizer: market data may contain only numeric, boolean, and datetime columns")
|
|
22
|
+
hashes = pd.util.hash_pandas_object(data, index=True).to_numpy().astype("<u8").tobytes()
|
|
23
|
+
data_hash = hashlib.sha256(hashes).hexdigest()
|
|
24
|
+
return {
|
|
25
|
+
"identityVersion": 1,
|
|
26
|
+
"source": identity.strategy_sha256,
|
|
27
|
+
"dependencies": identity.dependencies_sha256,
|
|
28
|
+
"data": digest(
|
|
29
|
+
{
|
|
30
|
+
"hash": data_hash,
|
|
31
|
+
"columns": list(data.columns),
|
|
32
|
+
"dtypes": list(map(str, data.dtypes)),
|
|
33
|
+
"index": str(data.index.dtype),
|
|
34
|
+
"indexName": data.index.name,
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
"environment": {
|
|
38
|
+
"python": platform.python_version(),
|
|
39
|
+
**{
|
|
40
|
+
name: version(name)
|
|
41
|
+
for name in ("openbacktesting", "numpy", "pandas", "pandas-ta-classic")
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
"backtest": backtest,
|
|
45
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Plain-value types and canonical encoding for parameter experiments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def encode(value) -> str:
|
|
12
|
+
"""Canonical JSON; reject nonfinite numbers and nonstring keys."""
|
|
13
|
+
|
|
14
|
+
def check(item):
|
|
15
|
+
if isinstance(item, dict):
|
|
16
|
+
if any(type(k) is not str for k in item):
|
|
17
|
+
raise ValueError("optimizer: JSON keys must be strings")
|
|
18
|
+
for child in item.values():
|
|
19
|
+
check(child)
|
|
20
|
+
elif isinstance(item, (list, tuple)):
|
|
21
|
+
for child in item:
|
|
22
|
+
check(child)
|
|
23
|
+
|
|
24
|
+
check(value)
|
|
25
|
+
return json.dumps(
|
|
26
|
+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def digest(value) -> str:
|
|
31
|
+
return hashlib.sha256(encode(value).encode()).hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Values:
|
|
36
|
+
"""Finite candidate values; plan validates against the corresponding input declaration."""
|
|
37
|
+
|
|
38
|
+
values: tuple | list
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Range:
|
|
43
|
+
"""Ascending grid range; int uses integers, float/price uses decimal strings or integers."""
|
|
44
|
+
|
|
45
|
+
start: int | str
|
|
46
|
+
stop: int | str
|
|
47
|
+
step: int | str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class ExecutionIdentity:
|
|
52
|
+
"""Host declaration of SHA256 digests for loaded source and local dependencies; no files are loaded or executed."""
|
|
53
|
+
|
|
54
|
+
strategy_sha256: str
|
|
55
|
+
dependencies_sha256: str
|
|
56
|
+
|
|
57
|
+
def __post_init__(self):
|
|
58
|
+
for value in (self.strategy_sha256, self.dependencies_sha256):
|
|
59
|
+
if not isinstance(value, str) or re.fullmatch("[0-9a-f]{64}", value) is None:
|
|
60
|
+
raise ValueError("optimizer: execution identity must be a lowercase SHA256 digest")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Plan:
|
|
65
|
+
"""Immutable canonical text; snapshot returns an independent copy, and combinations are generated incrementally at runtime."""
|
|
66
|
+
|
|
67
|
+
_json: str
|
|
68
|
+
|
|
69
|
+
def snapshot(self) -> dict:
|
|
70
|
+
"""Return an independent JSON copy of the complete plan."""
|
|
71
|
+
return json.loads(self._json)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def total(self) -> int:
|
|
75
|
+
"""Raw Cartesian-product count, without deducting constraints not yet checked."""
|
|
76
|
+
return self.snapshot()["total"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OptimizationError(RuntimeError):
|
|
80
|
+
"""Experiment infrastructure error preserving result location, identity, and last known committed count."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, message, path, experiment_id, committed):
|
|
83
|
+
self.path = str(path)
|
|
84
|
+
self.experiment_id = experiment_id
|
|
85
|
+
self.committed = committed
|
|
86
|
+
super().__init__(
|
|
87
|
+
f"{message}; path={path}; experiment={experiment_id}; committed={committed}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _ExportError(ValueError):
|
|
92
|
+
"""A backtest returned, but complete Portfolio export failed."""
|