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,9 @@
|
|
|
1
|
+
"""Compatibility exports for the old parallel path; implemented in backtest.parallel."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.simulation.parallel import (
|
|
4
|
+
SharedMemory,
|
|
5
|
+
SharedMemoryManager,
|
|
6
|
+
_batch, # noqa: F401 — re-export the legacy entry point
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = ["SharedMemory", "SharedMemoryManager"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Standalone plotting-data interface without rendering backends, file output, or browser operations."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.plotting.collector import Plotting
|
|
4
|
+
from openbacktesting.plotting.encoding import export_result
|
|
5
|
+
from openbacktesting.plotting.types import (
|
|
6
|
+
FillRef,
|
|
7
|
+
MarkerRef,
|
|
8
|
+
PaneRef,
|
|
9
|
+
PlotRef,
|
|
10
|
+
PlottingError,
|
|
11
|
+
PlottingLimits,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FillRef",
|
|
16
|
+
"MarkerRef",
|
|
17
|
+
"PaneRef",
|
|
18
|
+
"PlotRef",
|
|
19
|
+
"Plotting",
|
|
20
|
+
"PlottingError",
|
|
21
|
+
"PlottingLimits",
|
|
22
|
+
"export_result",
|
|
23
|
+
]
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""Independent static declaration registry and runtime markers per run, decoupled from trade execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from numbers import Real
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from openbacktesting.colors import Color, _as_color
|
|
14
|
+
from openbacktesting.indicators.types import IndicatorCursor, IndicatorHandle
|
|
15
|
+
from openbacktesting.plotting.encoding import axis, facts, freeze, numeric
|
|
16
|
+
from openbacktesting.plotting.types import (
|
|
17
|
+
FillRef,
|
|
18
|
+
MarkerRef,
|
|
19
|
+
PaneRef,
|
|
20
|
+
PlotRef,
|
|
21
|
+
PlottingError,
|
|
22
|
+
PlottingLimits,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
Gap = Literal["break", "connect"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _require(condition, code, detail):
|
|
29
|
+
if not condition:
|
|
30
|
+
raise PlottingError(code, detail)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _number(value):
|
|
34
|
+
_require(
|
|
35
|
+
isinstance(value, Real) and not isinstance(value, (bool, np.bool_)),
|
|
36
|
+
"invalid-value",
|
|
37
|
+
"Numeric values must be real; convert booleans explicitly",
|
|
38
|
+
)
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _rgba(value):
|
|
43
|
+
if value is None:
|
|
44
|
+
return None
|
|
45
|
+
try:
|
|
46
|
+
return _as_color(value).to_hex()
|
|
47
|
+
except (TypeError, ValueError) as error:
|
|
48
|
+
raise PlottingError(
|
|
49
|
+
"invalid-color", "Color must be Color, #RRGGBB, #RRGGBBAA, or None"
|
|
50
|
+
) from error
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Plotting:
|
|
54
|
+
"""Access through Strategy.plotting; init declares market-data/indicator layers, and next/on_order/on_fill add markers.
|
|
55
|
+
|
|
56
|
+
Declaration and marker id/title are optional; title defaults to the final id, and pane defaults to the main chart.
|
|
57
|
+
bg_color/bar_color write current-bar colors only in next, without creating identities or inheriting across bars.
|
|
58
|
+
References belong only to this run. Consumers autoscale the vertical axis using visible valid data.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
index: pd.Index,
|
|
64
|
+
cursor: IndicatorCursor,
|
|
65
|
+
*,
|
|
66
|
+
enabled: bool = True,
|
|
67
|
+
limits: PlottingLimits | None = None,
|
|
68
|
+
):
|
|
69
|
+
_require(type(enabled) is bool, "invalid-option", "plotting must be bool")
|
|
70
|
+
_require(
|
|
71
|
+
limits is None or isinstance(limits, PlottingLimits),
|
|
72
|
+
"invalid-limit",
|
|
73
|
+
"plotting_limits must be PlottingLimits",
|
|
74
|
+
)
|
|
75
|
+
self._enabled = enabled
|
|
76
|
+
self._limits = limits or PlottingLimits()
|
|
77
|
+
self._index = index.copy(deep=True)
|
|
78
|
+
self._cursor = cursor
|
|
79
|
+
self._n = len(index)
|
|
80
|
+
self._phase = "closed"
|
|
81
|
+
self._bar = -1
|
|
82
|
+
self._counts = Counter()
|
|
83
|
+
self._ids = {"main"}
|
|
84
|
+
self._refs = {}
|
|
85
|
+
self._sources = {}
|
|
86
|
+
self._dedupe = {}
|
|
87
|
+
self._panes = [{"id": "main", "title": "main", "axis": "bars", "scale": "main"}]
|
|
88
|
+
self._plots = {}
|
|
89
|
+
self._fills = []
|
|
90
|
+
self._markers = []
|
|
91
|
+
self._color_events = []
|
|
92
|
+
self._charge("panes", 1)
|
|
93
|
+
self._charge("points", self._n * 7)
|
|
94
|
+
self._processed = np.zeros(self._n, bool) if enabled else None
|
|
95
|
+
self._next = np.zeros(self._n, bool) if enabled else None
|
|
96
|
+
|
|
97
|
+
def _charge(self, kind, count):
|
|
98
|
+
total = self._counts[kind] + count
|
|
99
|
+
_require(total <= getattr(self._limits, kind), "PlottingLimitExceeded", f"{kind} exceeds the budget")
|
|
100
|
+
self._counts[kind] = total
|
|
101
|
+
|
|
102
|
+
def _stage(self, *allowed):
|
|
103
|
+
_require(self._phase in allowed, "invalid-phase", f"Current phase {self._phase} does not allow this operation")
|
|
104
|
+
|
|
105
|
+
def _identity(self, cls, id, title):
|
|
106
|
+
counter = cls.__name__
|
|
107
|
+
if id is None:
|
|
108
|
+
id = f"auto:{counter}:{self._counts[counter] + 1}"
|
|
109
|
+
else:
|
|
110
|
+
_require(
|
|
111
|
+
isinstance(id, str) and bool(id) and not id.startswith("auto:") and id != "main",
|
|
112
|
+
"invalid-id",
|
|
113
|
+
"id must not be empty or use reserved main/auto: identities",
|
|
114
|
+
)
|
|
115
|
+
_require(id not in self._ids, "duplicate-id", id)
|
|
116
|
+
title = id if title is None else title
|
|
117
|
+
_require(isinstance(title, str), "invalid-title", "title must be text")
|
|
118
|
+
self._charge("text", len((id + title).encode()))
|
|
119
|
+
self._counts[counter] += 1
|
|
120
|
+
self._ids.add(id)
|
|
121
|
+
ref = cls(id, title, self)
|
|
122
|
+
self._refs[id] = ref
|
|
123
|
+
return ref
|
|
124
|
+
|
|
125
|
+
def _ref(self, ref, cls):
|
|
126
|
+
_require(
|
|
127
|
+
isinstance(ref, cls) and ref._owner is self and self._refs.get(ref.id) is ref,
|
|
128
|
+
"invalid-reference",
|
|
129
|
+
"Reference type or owning run does not match",
|
|
130
|
+
)
|
|
131
|
+
return ref.id
|
|
132
|
+
|
|
133
|
+
def _pane(self, pane):
|
|
134
|
+
return "main" if pane is None else self._ref(pane, PaneRef)
|
|
135
|
+
|
|
136
|
+
def pane(self, id: str | None = None, title: str | None = None) -> PaneRef:
|
|
137
|
+
"""Create a separate pane in init; return a reference for subsequent layers to share an automatically scaled vertical axis."""
|
|
138
|
+
self._stage("init")
|
|
139
|
+
self._charge("panes", 1)
|
|
140
|
+
ref = self._identity(PaneRef, id, title)
|
|
141
|
+
self._panes.append({"id": ref.id, "title": ref.title, "axis": "bars", "scale": ref.id})
|
|
142
|
+
return ref
|
|
143
|
+
|
|
144
|
+
def _aligned(self, values, index):
|
|
145
|
+
_require(
|
|
146
|
+
isinstance(index, pd.Index) and index.equals(self._index),
|
|
147
|
+
"misaligned-source",
|
|
148
|
+
"index must match the complete market-data axis",
|
|
149
|
+
)
|
|
150
|
+
array = np.asarray(values)
|
|
151
|
+
_require(array.shape == (self._n,), "invalid-source", "A complete single-channel sequence is required")
|
|
152
|
+
_require(
|
|
153
|
+
array.dtype.kind in "iuf", "invalid-source", "A real-valued array is required; object/complex/bool are not accepted"
|
|
154
|
+
)
|
|
155
|
+
return array
|
|
156
|
+
|
|
157
|
+
def _source(self, values, valid, ready, unit, provenance):
|
|
158
|
+
_require(unit is None or isinstance(unit, str), "invalid-unit", "unit must be text or None")
|
|
159
|
+
self._charge("points", self._n)
|
|
160
|
+
self._charge("text", len((unit or "").encode()))
|
|
161
|
+
id = f"auto:source:{len(self._sources) + 1}"
|
|
162
|
+
self._charge("text", len(id.encode()) * 2)
|
|
163
|
+
row = {
|
|
164
|
+
"id": id,
|
|
165
|
+
"title": id,
|
|
166
|
+
"axis": "bars",
|
|
167
|
+
"unit": unit,
|
|
168
|
+
"samplingMode": "offline",
|
|
169
|
+
"kind": "numeric",
|
|
170
|
+
"provenance": provenance or {},
|
|
171
|
+
}
|
|
172
|
+
if self._enabled:
|
|
173
|
+
row.update(values=values.copy(), valid=valid.copy(), ready=ready.copy())
|
|
174
|
+
self._sources[id] = row
|
|
175
|
+
return id
|
|
176
|
+
|
|
177
|
+
def _adapt(self, source):
|
|
178
|
+
_require(
|
|
179
|
+
isinstance(source, IndicatorHandle) and source._cursor is self._cursor,
|
|
180
|
+
"invalid-source",
|
|
181
|
+
"A single-channel indicator/market-data handle from this run is required",
|
|
182
|
+
)
|
|
183
|
+
values = self._aligned(np.asarray(source), source._index())
|
|
184
|
+
# Complete views of the same buffer and channel share a source; full slices do not duplicate payloads.
|
|
185
|
+
key = (id(source._values), repr(source._selectors))
|
|
186
|
+
if key not in self._dedupe:
|
|
187
|
+
meta = source.metadata
|
|
188
|
+
provenance = {
|
|
189
|
+
k: v
|
|
190
|
+
for k, v in meta.items()
|
|
191
|
+
if k in {"id", "version", "backend", "unit", "kind", "field"} and isinstance(v, str)
|
|
192
|
+
}
|
|
193
|
+
if isinstance(source.name, str):
|
|
194
|
+
provenance["channel"] = source.name
|
|
195
|
+
source_id = self._source(
|
|
196
|
+
values,
|
|
197
|
+
source.valid,
|
|
198
|
+
source.ready,
|
|
199
|
+
meta.get("unit"),
|
|
200
|
+
provenance,
|
|
201
|
+
)
|
|
202
|
+
self._dedupe[key] = source_id
|
|
203
|
+
return self._dedupe[key]
|
|
204
|
+
|
|
205
|
+
def _plot(self, kind, source, id, title, pane, color, gap_policy, offset_bars, **extra):
|
|
206
|
+
self._stage("init")
|
|
207
|
+
_require(
|
|
208
|
+
gap_policy in ("break", "connect"), "invalid-style", "gap_policy must be break/connect"
|
|
209
|
+
)
|
|
210
|
+
_require(
|
|
211
|
+
type(offset_bars) is int and abs(offset_bars) <= 2**53 - 1,
|
|
212
|
+
"invalid-offset",
|
|
213
|
+
"offset_bars must be a JavaScript safe integer",
|
|
214
|
+
)
|
|
215
|
+
target = self._pane(pane)
|
|
216
|
+
paint = _rgba(color)
|
|
217
|
+
source_id = None if kind == "level" else self._adapt(source)
|
|
218
|
+
self._charge("plots", 1)
|
|
219
|
+
ref = self._identity(PlotRef, id, title)
|
|
220
|
+
self._plots[ref.id] = {
|
|
221
|
+
"id": ref.id,
|
|
222
|
+
"title": ref.title,
|
|
223
|
+
"kind": kind,
|
|
224
|
+
"pane": target,
|
|
225
|
+
"axis": "bars",
|
|
226
|
+
"scale": target,
|
|
227
|
+
"source": source_id,
|
|
228
|
+
"color": paint,
|
|
229
|
+
"gapPolicy": gap_policy,
|
|
230
|
+
"offsetBars": offset_bars,
|
|
231
|
+
**extra,
|
|
232
|
+
}
|
|
233
|
+
return ref
|
|
234
|
+
|
|
235
|
+
def line(
|
|
236
|
+
self,
|
|
237
|
+
source: IndicatorHandle,
|
|
238
|
+
id: str | None = None,
|
|
239
|
+
title: str | None = None,
|
|
240
|
+
pane: PaneRef | None = None,
|
|
241
|
+
color: Color | str | None = None,
|
|
242
|
+
style: Literal["line", "step"] = "line",
|
|
243
|
+
gap_policy: Gap = "break",
|
|
244
|
+
offset_bars: int = 0,
|
|
245
|
+
) -> PlotRef:
|
|
246
|
+
"""Declare a line or step line, defaulting to the main chart with broken gaps; offsets affect display only."""
|
|
247
|
+
_require(style in ("line", "step"), "invalid-style", "style must be line/step")
|
|
248
|
+
return self._plot(style, source, id, title, pane, color, gap_policy, offset_bars)
|
|
249
|
+
|
|
250
|
+
def histogram(
|
|
251
|
+
self,
|
|
252
|
+
source: IndicatorHandle,
|
|
253
|
+
id: str | None = None,
|
|
254
|
+
title: str | None = None,
|
|
255
|
+
pane: PaneRef | None = None,
|
|
256
|
+
color: Color | str | None = None,
|
|
257
|
+
baseline: float = 0,
|
|
258
|
+
gap_policy: Gap = "break",
|
|
259
|
+
offset_bars: int = 0,
|
|
260
|
+
) -> PlotRef:
|
|
261
|
+
"""Declare a histogram; a finite baseline contributes to consumer vertical-axis autoscaling."""
|
|
262
|
+
_require(np.isfinite(_number(baseline)), "invalid-value", "baseline must be finite")
|
|
263
|
+
return self._plot(
|
|
264
|
+
"histogram",
|
|
265
|
+
source,
|
|
266
|
+
id,
|
|
267
|
+
title,
|
|
268
|
+
pane,
|
|
269
|
+
color,
|
|
270
|
+
gap_policy,
|
|
271
|
+
offset_bars,
|
|
272
|
+
baseline=float(baseline),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def level(
|
|
276
|
+
self,
|
|
277
|
+
value: float,
|
|
278
|
+
id: str | None = None,
|
|
279
|
+
title: str | None = None,
|
|
280
|
+
pane: PaneRef | None = None,
|
|
281
|
+
color: Color | str | None = None,
|
|
282
|
+
) -> PlotRef:
|
|
283
|
+
"""Declare a finite horizontal line, storing one constant rather than duplicating a full array."""
|
|
284
|
+
_require(np.isfinite(_number(value)), "invalid-value", "level must be finite")
|
|
285
|
+
return self._plot("level", None, id, title, pane, color, "break", 0, value=float(value))
|
|
286
|
+
|
|
287
|
+
def fill(
|
|
288
|
+
self,
|
|
289
|
+
lower: PlotRef,
|
|
290
|
+
upper: PlotRef,
|
|
291
|
+
id: str | None = None,
|
|
292
|
+
title: str | None = None,
|
|
293
|
+
color: Color | str | None = None,
|
|
294
|
+
gap_policy: Gap = "break",
|
|
295
|
+
) -> FillRef:
|
|
296
|
+
"""Fill between two layers; endpoints must share pane, axis, offset, and compatible units."""
|
|
297
|
+
self._stage("init")
|
|
298
|
+
lo, hi = (self._plots[self._ref(r, PlotRef)] for r in (lower, upper))
|
|
299
|
+
_require(
|
|
300
|
+
all(lo[k] == hi[k] for k in ("pane", "axis", "scale", "offsetBars")),
|
|
301
|
+
"incompatible-fill",
|
|
302
|
+
"Endpoint panes/axes/offsets must match",
|
|
303
|
+
)
|
|
304
|
+
units = [self._sources[p["source"]]["unit"] for p in (lo, hi) if p["source"] is not None]
|
|
305
|
+
_require(len(set(units)) <= 1, "incompatible-fill", "Endpoint units are incompatible")
|
|
306
|
+
_require(gap_policy in ("break", "connect"), "invalid-style", "Unknown gap policy")
|
|
307
|
+
paint = _rgba(color)
|
|
308
|
+
self._charge("plots", 1)
|
|
309
|
+
ref = self._identity(FillRef, id, title)
|
|
310
|
+
self._fills.append(
|
|
311
|
+
{
|
|
312
|
+
"id": ref.id,
|
|
313
|
+
"title": ref.title,
|
|
314
|
+
"lower": lo["id"],
|
|
315
|
+
"upper": hi["id"],
|
|
316
|
+
"pane": lo["pane"],
|
|
317
|
+
"axis": "bars",
|
|
318
|
+
"scale": lo["scale"],
|
|
319
|
+
"color": paint,
|
|
320
|
+
"gapPolicy": gap_policy,
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
return ref
|
|
324
|
+
|
|
325
|
+
def _color(self, kind, color, pane):
|
|
326
|
+
self._stage("next")
|
|
327
|
+
target, paint = self._pane(pane), _rgba(color)
|
|
328
|
+
sequence = self._counts["color"] + 1
|
|
329
|
+
_require(sequence <= 2**53 - 1, "PlottingLimitExceeded", "Color index exceeds the safe-integer range")
|
|
330
|
+
self._charge("events", 1)
|
|
331
|
+
self._counts["color"] = sequence
|
|
332
|
+
if self._enabled:
|
|
333
|
+
self._color_events.append(
|
|
334
|
+
{
|
|
335
|
+
"kind": kind,
|
|
336
|
+
"pane": target,
|
|
337
|
+
"axis": "bars",
|
|
338
|
+
"anchorBar": self._bar,
|
|
339
|
+
"knownAtBar": self._bar,
|
|
340
|
+
"phase": "next",
|
|
341
|
+
"sequence": sequence,
|
|
342
|
+
"color": paint,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def bg_color(self, color: Color | str | None, *, pane: PaneRef | None = None) -> None:
|
|
347
|
+
"""Write the current bar's pane background only in next; the last write to the same target wins.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
color: Required Color, #RRGGBB, #RRGGBBAA, or None; None clears the current-bar override, while transparent colors are preserved.
|
|
351
|
+
pane: Main chart by default, or a separate-pane reference created in this run.
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
PlottingError: Invalid phase, color, or pane ownership, or shared event budget exceeded.
|
|
355
|
+
|
|
356
|
+
No cross-bar inheritance; disabled collection still validates and counts without retaining history.
|
|
357
|
+
"""
|
|
358
|
+
self._color("background", color, pane)
|
|
359
|
+
|
|
360
|
+
def bar_color(self, color: Color | str | None) -> None:
|
|
361
|
+
"""Color the current main-chart candle only in next, applying one override to body, wick, and border.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
color: Required Color, #RRGGBB, #RRGGBBAA, or None; None clears the current-bar override, while transparent colors are preserved.
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
PlottingError: Invalid phase or color, or shared event budget exceeded.
|
|
368
|
+
|
|
369
|
+
The last write on a bar wins, without cross-bar inheritance; market data and automatic vertical axes remain unchanged.
|
|
370
|
+
"""
|
|
371
|
+
self._color("bar", color, None)
|
|
372
|
+
|
|
373
|
+
def marker(
|
|
374
|
+
self,
|
|
375
|
+
id: str | None = None,
|
|
376
|
+
title: str | None = None,
|
|
377
|
+
pane: PaneRef | None = None,
|
|
378
|
+
*,
|
|
379
|
+
position: Literal["above_bar", "below_bar", "price"] = "above_bar",
|
|
380
|
+
price: float | None = None,
|
|
381
|
+
shape: Literal["circle", "square", "triangle_up", "triangle_down"] = "circle",
|
|
382
|
+
text: str = "",
|
|
383
|
+
color: Color | str | None = None,
|
|
384
|
+
anchor_bar: int | None = None,
|
|
385
|
+
) -> MarkerRef:
|
|
386
|
+
"""Record a runtime marker; anchor_bar may refer to historical bars, while knownAtBar is always the current bar."""
|
|
387
|
+
self._stage("next", "on_order", "on_fill", "terminal_order", "terminal_fill")
|
|
388
|
+
_require(
|
|
389
|
+
position in ("above_bar", "below_bar", "price")
|
|
390
|
+
and shape in ("circle", "square", "triangle_up", "triangle_down"),
|
|
391
|
+
"invalid-style",
|
|
392
|
+
"Unknown marker location or shape",
|
|
393
|
+
)
|
|
394
|
+
if position == "price":
|
|
395
|
+
_require(np.isfinite(_number(price)), "invalid-value", "price location requires a finite price")
|
|
396
|
+
price = float(price)
|
|
397
|
+
else:
|
|
398
|
+
_require(price is None, "invalid-value", "Only price location allows a price")
|
|
399
|
+
anchor = self._bar if anchor_bar is None else anchor_bar
|
|
400
|
+
_require(
|
|
401
|
+
type(anchor) is int and 0 <= anchor <= self._bar,
|
|
402
|
+
"invalid-anchor",
|
|
403
|
+
"anchor_bar may refer only to the current or a past bar",
|
|
404
|
+
)
|
|
405
|
+
_require(isinstance(text, str), "invalid-text", "text must be plain text")
|
|
406
|
+
target, paint = self._pane(pane), _rgba(color)
|
|
407
|
+
self._charge("events", 1)
|
|
408
|
+
self._charge("text", len(text.encode()))
|
|
409
|
+
ref = self._identity(MarkerRef, id, title)
|
|
410
|
+
if self._enabled:
|
|
411
|
+
self._markers.append(
|
|
412
|
+
{
|
|
413
|
+
"id": ref.id,
|
|
414
|
+
"title": ref.title,
|
|
415
|
+
"pane": target,
|
|
416
|
+
"axis": "bars",
|
|
417
|
+
"scale": target,
|
|
418
|
+
"anchorBar": anchor,
|
|
419
|
+
"knownAtBar": self._bar,
|
|
420
|
+
"phase": self._phase,
|
|
421
|
+
"sequence": self._counts["MarkerRef"],
|
|
422
|
+
"position": position,
|
|
423
|
+
"price": price,
|
|
424
|
+
"shape": shape,
|
|
425
|
+
"text": text,
|
|
426
|
+
"color": paint,
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
return ref
|
|
430
|
+
|
|
431
|
+
def _enter(self, phase, bar=None):
|
|
432
|
+
self._phase = phase
|
|
433
|
+
if bar is not None:
|
|
434
|
+
self._bar = bar
|
|
435
|
+
if self._enabled:
|
|
436
|
+
self._processed[bar] = True
|
|
437
|
+
if phase == "next":
|
|
438
|
+
self._next[bar] = True
|
|
439
|
+
|
|
440
|
+
def _finish(self, result, market, reason):
|
|
441
|
+
self._phase = "closed"
|
|
442
|
+
self._charge("events", len(result["_fills"]) + len(result["_account_curve"]))
|
|
443
|
+
if not self._enabled:
|
|
444
|
+
return None
|
|
445
|
+
sources = []
|
|
446
|
+
for row in self._sources.values():
|
|
447
|
+
item = {k: v for k, v in row.items() if k not in {"values", "valid", "ready"}}
|
|
448
|
+
item.update(numeric(row["values"], row["valid"], row["ready"]))
|
|
449
|
+
sources.append(item)
|
|
450
|
+
columns = [
|
|
451
|
+
"fill_id",
|
|
452
|
+
"order_id",
|
|
453
|
+
"bar_index",
|
|
454
|
+
"phase",
|
|
455
|
+
"sequence",
|
|
456
|
+
"side",
|
|
457
|
+
"qty",
|
|
458
|
+
"price",
|
|
459
|
+
"fee",
|
|
460
|
+
]
|
|
461
|
+
account_columns = [c for c in result["_account_curve"].columns if c != "timestamp"]
|
|
462
|
+
payload = {
|
|
463
|
+
"schemaVersion": "1.1.0",
|
|
464
|
+
"engineVersion": version("openbacktesting"),
|
|
465
|
+
"axes": [axis(self._index)],
|
|
466
|
+
"panes": self._panes,
|
|
467
|
+
"scales": [
|
|
468
|
+
{"id": p["scale"], "pane": p["id"], "mode": "auto", "rangePolicy": "visible-valid"}
|
|
469
|
+
for p in self._panes
|
|
470
|
+
],
|
|
471
|
+
"sources": sources,
|
|
472
|
+
"plots": list(self._plots.values()),
|
|
473
|
+
"fills": self._fills,
|
|
474
|
+
"markers": self._markers,
|
|
475
|
+
"colorEvents": self._color_events,
|
|
476
|
+
"market": {
|
|
477
|
+
"axis": "bars",
|
|
478
|
+
**{
|
|
479
|
+
c.lower(): numeric(
|
|
480
|
+
market[c].to_numpy(),
|
|
481
|
+
np.isfinite(market[c].to_numpy()),
|
|
482
|
+
np.ones(self._n, bool),
|
|
483
|
+
)
|
|
484
|
+
for c in ("Open", "High", "Low", "Close", "Volume")
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
"tradingFacts": {
|
|
488
|
+
"fills": facts(result["_fills"], columns),
|
|
489
|
+
"account": facts(result["_account_curve"], account_columns),
|
|
490
|
+
},
|
|
491
|
+
"execution": {
|
|
492
|
+
"processed": self._processed.tolist(),
|
|
493
|
+
"strategyNext": self._next.tolist(),
|
|
494
|
+
"endReason": reason,
|
|
495
|
+
"lastProcessedBar": self._bar,
|
|
496
|
+
},
|
|
497
|
+
"requiredFeatures": ["core-v1"] + (["bar-coloring-v1"] if self._color_events else []),
|
|
498
|
+
"optionalFeatures": [],
|
|
499
|
+
"diagnostics": [],
|
|
500
|
+
}
|
|
501
|
+
return freeze(payload, self._limits.bytes)
|
|
502
|
+
|
|
503
|
+
def _close(self):
|
|
504
|
+
self._phase = "closed"
|
|
505
|
+
self._sources.clear()
|
|
506
|
+
self._dedupe.clear()
|
|
507
|
+
self._markers.clear()
|
|
508
|
+
self._color_events.clear()
|
|
509
|
+
self._processed = self._next = None
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Freezing and strict JSON encoding; accept only completed plain-value facts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from numbers import Integral
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from openbacktesting.plotting.types import PlottingError, _Snapshot
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def scalar(value):
|
|
16
|
+
if value is None or value is pd.NA or value is pd.NaT:
|
|
17
|
+
return None
|
|
18
|
+
if isinstance(value, (bool, np.bool_)):
|
|
19
|
+
return bool(value)
|
|
20
|
+
if isinstance(value, Integral):
|
|
21
|
+
value = int(value)
|
|
22
|
+
return value if abs(value) <= 2**53 - 1 else str(value)
|
|
23
|
+
if isinstance(value, (float, np.floating)):
|
|
24
|
+
return float(value) if math.isfinite(value) else None
|
|
25
|
+
if isinstance(value, pd.Timestamp):
|
|
26
|
+
return value.isoformat()
|
|
27
|
+
if isinstance(value, str):
|
|
28
|
+
return value
|
|
29
|
+
raise PlottingError("invalid-value", f"Unsupported plain-value type {type(value).__name__}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def axis(index):
|
|
33
|
+
if isinstance(index, pd.DatetimeIndex):
|
|
34
|
+
if index.tz is not None:
|
|
35
|
+
return {
|
|
36
|
+
"id": "bars",
|
|
37
|
+
"kind": "utc-ns",
|
|
38
|
+
"timezone": str(index.tz),
|
|
39
|
+
"labels": [str(v.value) for v in index],
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
"id": "bars",
|
|
43
|
+
"kind": "naive-datetime",
|
|
44
|
+
"timezone": None,
|
|
45
|
+
"labels": [v.isoformat() for v in index],
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
"id": "bars",
|
|
49
|
+
"kind": "ordinal",
|
|
50
|
+
"timezone": None,
|
|
51
|
+
"labels": [str(scalar(v)) for v in index],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def numeric(values, valid, ready):
|
|
56
|
+
finite = np.isfinite(values)
|
|
57
|
+
return {
|
|
58
|
+
"values": [scalar(v) for v in values],
|
|
59
|
+
"valid": (valid & finite).tolist(),
|
|
60
|
+
"ready": ready.tolist(),
|
|
61
|
+
"missing": [None if ok else "non-finite" for ok in finite],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def facts(table, columns):
|
|
66
|
+
return [
|
|
67
|
+
{key: scalar(value) for key, value in zip(columns, row)}
|
|
68
|
+
for row in table.loc[:, columns].itertuples(index=False, name=None)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def freeze(payload, byte_limit):
|
|
73
|
+
return _Snapshot(
|
|
74
|
+
"1.1.0",
|
|
75
|
+
json.dumps(payload, ensure_ascii=False, allow_nan=False, separators=(",", ":")),
|
|
76
|
+
byte_limit,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def export_result(result: pd.Series) -> dict:
|
|
81
|
+
"""Return an independent PlottingResult dictionary from a successful backtest's frozen snapshot without reading the strategy instance.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
result: Backtest.run or runtime.run result with collection enabled.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Independent dictionary encodable with json.dumps(..., allow_nan=False).
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
PlottingError: Missing snapshot, unsupported type or version, or export-byte budget exceeded.
|
|
91
|
+
"""
|
|
92
|
+
snapshot = getattr(result, "attrs", {}).get("plotting_result")
|
|
93
|
+
if not isinstance(snapshot, _Snapshot):
|
|
94
|
+
raise PlottingError("missing-snapshot", "A successfully completed backtest result with collection enabled is required")
|
|
95
|
+
if snapshot.version not in ("1.0.0", "1.1.0"):
|
|
96
|
+
raise PlottingError("unsupported-version", snapshot.version)
|
|
97
|
+
if snapshot.byte_length > snapshot.byte_limit:
|
|
98
|
+
raise PlottingError("PlottingLimitExceeded", "bytes exceeds the export budget")
|
|
99
|
+
payload = json.loads(snapshot.payload)
|
|
100
|
+
if payload.get("schemaVersion") != snapshot.version:
|
|
101
|
+
raise PlottingError("unsupported-version", "Snapshot and payload versions do not match")
|
|
102
|
+
return payload
|