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,92 @@
|
|
|
1
|
+
"""Immutable plotting-declaration identities, budgets, and stable errors without rendering capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, fields
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlottingError(ValueError):
|
|
10
|
+
"""Plotting contract error; code is a stable category for caller dispatch."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, code: str, detail: str):
|
|
13
|
+
self.code = code
|
|
14
|
+
super().__init__(f"{code}: {detail}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class PlottingLimits:
|
|
19
|
+
"""Collection budgets; all limits are positive integers, and bytes is checked during explicit export.
|
|
20
|
+
|
|
21
|
+
points counts unique dense sources, including five market-data columns and two execution masks.
|
|
22
|
+
panes includes the main chart; plots includes fills; events includes every color call, marker, and trade fact.
|
|
23
|
+
text is the cumulative UTF-8 byte count of author identities, titles, units, and marker text.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
panes: int = 32
|
|
27
|
+
plots: int = 256
|
|
28
|
+
points: int = 10_000_000
|
|
29
|
+
events: int = 1_000_000
|
|
30
|
+
text: int = 1_048_576
|
|
31
|
+
bytes: int = 268_435_456
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
for field in fields(self):
|
|
35
|
+
value = getattr(self, field.name)
|
|
36
|
+
if type(value) is not int or value <= 0:
|
|
37
|
+
raise PlottingError("invalid-limit", f"{field.name} must be a positive integer")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class _Ref:
|
|
42
|
+
id: str
|
|
43
|
+
title: str
|
|
44
|
+
_owner: Any
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class PaneRef(_Ref):
|
|
49
|
+
"""Separate-pane identity for the current run; pass this reference only to pane arguments."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class PlotRef(_Ref):
|
|
54
|
+
"""Line, histogram, or horizontal-line identity usable as a fill endpoint."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class FillRef(_Ref):
|
|
59
|
+
"""Fill identity between two compatible layers."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class MarkerRef(_Ref):
|
|
64
|
+
"""Identity of one runtime marker."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _Snapshot(tuple):
|
|
68
|
+
"""Immutable plain-value tuple; the entire result.attrs remains JSON-encodable."""
|
|
69
|
+
|
|
70
|
+
__slots__ = ()
|
|
71
|
+
|
|
72
|
+
def __new__(cls, version: str, payload: str, byte_limit: int):
|
|
73
|
+
return super().__new__(cls, (version, payload, byte_limit, len(payload.encode())))
|
|
74
|
+
|
|
75
|
+
def __getnewargs__(self):
|
|
76
|
+
return self[:3]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def version(self):
|
|
80
|
+
return self[0]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def payload(self):
|
|
84
|
+
return self[1]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def byte_limit(self):
|
|
88
|
+
return self[2]
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def byte_length(self):
|
|
92
|
+
return self[3]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Frozen account, trading, and report data for a single backtest."""
|
|
2
|
+
|
|
3
|
+
from .snapshot import PortfolioSnapshot as PortfolioSnapshot
|
|
4
|
+
from .snapshot import export_result as export_result
|
|
5
|
+
from .snapshot import get_snapshot as get_snapshot
|
|
6
|
+
|
|
7
|
+
__all__ = ["PortfolioSnapshot", "export_result", "get_snapshot", "report_catalog"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def report_catalog() -> dict:
|
|
11
|
+
"""Return an independent copy of the report-field catalog for hosts to check coverage and method identity."""
|
|
12
|
+
import json
|
|
13
|
+
from importlib.resources import files
|
|
14
|
+
|
|
15
|
+
return json.loads(files(__package__).joinpath("catalog.json").read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Deterministic trade distributions, streaks, time patterns, and equity intervals."""
|
|
2
|
+
|
|
3
|
+
from decimal import ROUND_FLOOR, Decimal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from openbacktesting.portfolio.metrics import mean
|
|
9
|
+
from openbacktesting.portfolio.models import metric, ratio
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def distribution(trades):
|
|
13
|
+
bins = {}
|
|
14
|
+
for t in trades:
|
|
15
|
+
key = int(
|
|
16
|
+
(Decimal(str(t["returnFraction"])) / Decimal("0.01")).to_integral_value(
|
|
17
|
+
rounding=ROUND_FLOOR
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
bins.setdefault(key, []).append(t["fragmentId"])
|
|
21
|
+
# Sparse buckets avoid huge numbers of empty buckets from extreme returns; each bucket has explicit boundaries.
|
|
22
|
+
histogram = [
|
|
23
|
+
{
|
|
24
|
+
"lower": float(Decimal(k) * Decimal("0.01")),
|
|
25
|
+
"upper": float(Decimal(k + 1) * Decimal("0.01")),
|
|
26
|
+
"count": len(ids),
|
|
27
|
+
"fragmentIds": ids,
|
|
28
|
+
}
|
|
29
|
+
for k, ids in sorted(bins.items())
|
|
30
|
+
]
|
|
31
|
+
values = [t["returnFraction"] for t in trades]
|
|
32
|
+
q1, q3 = np.quantile(values, [0.25, 0.75]) if len(values) >= 4 else (None, None)
|
|
33
|
+
low, high = (q1 - 1.5 * (q3 - q1), q3 + 1.5 * (q3 - q1)) if q1 is not None else (None, None)
|
|
34
|
+
outliers = [
|
|
35
|
+
t
|
|
36
|
+
for t in trades
|
|
37
|
+
if low is not None and (t["returnFraction"] < low or t["returnFraction"] > high)
|
|
38
|
+
]
|
|
39
|
+
return {
|
|
40
|
+
"histogram": histogram,
|
|
41
|
+
"interval": "left_closed_right_open",
|
|
42
|
+
"winners": sum(t["pnlNet"] > 0 for t in trades),
|
|
43
|
+
"losers": sum(t["pnlNet"] < 0 for t in trades),
|
|
44
|
+
"evens": sum(t["pnlNet"] == 0 for t in trades),
|
|
45
|
+
"averageProfitReturn": mean([t["returnFraction"] for t in trades if t["pnlNet"] > 0]),
|
|
46
|
+
"averageLossReturn": mean([-t["returnFraction"] for t in trades if t["pnlNet"] < 0]),
|
|
47
|
+
"outliers": {
|
|
48
|
+
"methodId": "tukey_return_iqr_v1",
|
|
49
|
+
"lower": low,
|
|
50
|
+
"upper": high,
|
|
51
|
+
"fragmentIds": [t["fragmentId"] for t in outliers],
|
|
52
|
+
"count": len(outliers),
|
|
53
|
+
"pnl": metric(
|
|
54
|
+
sum(t["pnlNet"] for t in outliers) if low is not None else None,
|
|
55
|
+
"money",
|
|
56
|
+
status="ok" if low is not None else "insufficient_data",
|
|
57
|
+
reason="fewer_than_four_samples",
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def streaks(trades):
|
|
64
|
+
rows = []
|
|
65
|
+
active = None
|
|
66
|
+
for t in trades:
|
|
67
|
+
sign = int(t["pnlNet"] > 0) - int(t["pnlNet"] < 0)
|
|
68
|
+
if not sign:
|
|
69
|
+
active = None
|
|
70
|
+
continue
|
|
71
|
+
if active is None or active["sign"] != sign:
|
|
72
|
+
active = {
|
|
73
|
+
"id": f"streak-{len(rows) + 1}",
|
|
74
|
+
"sign": sign,
|
|
75
|
+
"count": 0,
|
|
76
|
+
"amount": 0.0,
|
|
77
|
+
"startFragmentId": t["fragmentId"],
|
|
78
|
+
"endFragmentId": t["fragmentId"],
|
|
79
|
+
"current": False,
|
|
80
|
+
}
|
|
81
|
+
rows.append(active)
|
|
82
|
+
active["count"] += 1
|
|
83
|
+
active["amount"] += t["pnlNet"]
|
|
84
|
+
active["endFragmentId"] = t["fragmentId"]
|
|
85
|
+
if active is not None:
|
|
86
|
+
active["current"] = True
|
|
87
|
+
summary = {}
|
|
88
|
+
for label, sign in (("winning", 1), ("losing", -1)):
|
|
89
|
+
lengths = [r["count"] for r in rows if r["sign"] == sign]
|
|
90
|
+
amounts = [abs(r["amount"]) for r in rows if r["sign"] == sign]
|
|
91
|
+
summary[label] = {
|
|
92
|
+
"longest": max(lengths, default=0),
|
|
93
|
+
"average": mean(lengths),
|
|
94
|
+
"maximumAmount": max(amounts, default=0.0),
|
|
95
|
+
"averageAmount": mean(amounts),
|
|
96
|
+
}
|
|
97
|
+
return {"rows": rows, "summary": summary, "methodId": "zero_breaks_streak_v1"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def time_patterns(trades, index):
|
|
101
|
+
if not isinstance(index, pd.DatetimeIndex):
|
|
102
|
+
return {"status": "not_applicable", "reason": "ordinal_index", "groups": {}}
|
|
103
|
+
result = {}
|
|
104
|
+
for name, attr, keys in (
|
|
105
|
+
("hours", "hour", range(24)),
|
|
106
|
+
("days", "dayofweek", range(7)),
|
|
107
|
+
("months", "month", range(1, 13)),
|
|
108
|
+
):
|
|
109
|
+
groups = {key: [] for key in keys}
|
|
110
|
+
for t in trades:
|
|
111
|
+
groups[getattr(index[t["entry"]["barIndex"]], attr)].append(t)
|
|
112
|
+
rows = []
|
|
113
|
+
for key, group in groups.items():
|
|
114
|
+
wins = sum(t["pnlNet"] > 0 for t in group)
|
|
115
|
+
rows.append(
|
|
116
|
+
{
|
|
117
|
+
"key": key,
|
|
118
|
+
"count": len(group),
|
|
119
|
+
"wins": wins,
|
|
120
|
+
"losses": sum(t["pnlNet"] < 0 for t in group),
|
|
121
|
+
"evens": sum(t["pnlNet"] == 0 for t in group),
|
|
122
|
+
"winRate": ratio(wins, len(group)),
|
|
123
|
+
"pnl": sum(t["pnlNet"] for t in group),
|
|
124
|
+
"averageBars": mean([t["barsHeld"] for t in group]),
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
best = max((r["winRate"] for r in rows if r["winRate"] is not None), default=None)
|
|
128
|
+
result[name] = {
|
|
129
|
+
"rows": rows,
|
|
130
|
+
"bestKeys": [r["key"] for r in rows if best is not None and r["winRate"] == best],
|
|
131
|
+
}
|
|
132
|
+
return {"status": "ok", "reason": None, "groups": result}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def episodes(rows, initial):
|
|
136
|
+
if not rows:
|
|
137
|
+
return {"drawdowns": [], "alternating": []}
|
|
138
|
+
|
|
139
|
+
def duration(start, end):
|
|
140
|
+
a, b = rows[start]["timestamp"], rows[end]["timestamp"]
|
|
141
|
+
return str((b - a).value) if isinstance(a, pd.Timestamp) else None
|
|
142
|
+
|
|
143
|
+
def item(start, end, kind, amount, fraction, current, extreme):
|
|
144
|
+
return {
|
|
145
|
+
"kind": kind,
|
|
146
|
+
"start": rows[start]["observationId"],
|
|
147
|
+
"end": rows[end]["observationId"],
|
|
148
|
+
"extreme": rows[extreme]["observationId"],
|
|
149
|
+
"amount": amount,
|
|
150
|
+
"fraction": fraction,
|
|
151
|
+
"durationNs": duration(start, end),
|
|
152
|
+
"current": current,
|
|
153
|
+
"recovered": not current,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
drawdowns = []
|
|
157
|
+
peak, peak_i, active, lowest, low_i = initial, 0, False, initial, 0
|
|
158
|
+
for i, row in enumerate(rows):
|
|
159
|
+
value = row["equity"]
|
|
160
|
+
if value >= peak:
|
|
161
|
+
if active:
|
|
162
|
+
drawdowns.append(
|
|
163
|
+
item(
|
|
164
|
+
peak_i,
|
|
165
|
+
i,
|
|
166
|
+
"drawdown",
|
|
167
|
+
peak - lowest,
|
|
168
|
+
ratio(peak - lowest, peak),
|
|
169
|
+
False,
|
|
170
|
+
low_i,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
peak, peak_i, active, lowest = value, i, False, value
|
|
174
|
+
else:
|
|
175
|
+
active = True
|
|
176
|
+
if value < lowest:
|
|
177
|
+
lowest, low_i = value, i
|
|
178
|
+
if active:
|
|
179
|
+
drawdowns.append(
|
|
180
|
+
item(
|
|
181
|
+
peak_i,
|
|
182
|
+
len(rows) - 1,
|
|
183
|
+
"drawdown",
|
|
184
|
+
peak - lowest,
|
|
185
|
+
ratio(peak - lowest, peak),
|
|
186
|
+
True,
|
|
187
|
+
low_i,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
alternating = []
|
|
191
|
+
start, sign = 0, 0
|
|
192
|
+
for i in range(1, len(rows)):
|
|
193
|
+
delta = rows[i]["equity"] - rows[i - 1]["equity"]
|
|
194
|
+
new = int(delta > 0) - int(delta < 0)
|
|
195
|
+
if not new:
|
|
196
|
+
continue
|
|
197
|
+
if sign and new != sign:
|
|
198
|
+
a, b = rows[start]["equity"], rows[i - 1]["equity"]
|
|
199
|
+
alternating.append(
|
|
200
|
+
item(
|
|
201
|
+
start,
|
|
202
|
+
i - 1,
|
|
203
|
+
"runup" if sign > 0 else "decline",
|
|
204
|
+
abs(b - a),
|
|
205
|
+
abs(b - a) / a if a > 0 else None,
|
|
206
|
+
False,
|
|
207
|
+
i - 1,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
start = i - 1
|
|
211
|
+
elif not sign:
|
|
212
|
+
start = i - 1
|
|
213
|
+
sign = new
|
|
214
|
+
if sign:
|
|
215
|
+
a, b = rows[start]["equity"], rows[-1]["equity"]
|
|
216
|
+
alternating.append(
|
|
217
|
+
item(
|
|
218
|
+
start,
|
|
219
|
+
len(rows) - 1,
|
|
220
|
+
"runup" if sign > 0 else "decline",
|
|
221
|
+
abs(b - a),
|
|
222
|
+
abs(b - a) / a if a > 0 else None,
|
|
223
|
+
True,
|
|
224
|
+
len(rows) - 1,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
summary = {}
|
|
228
|
+
for name, group in (
|
|
229
|
+
("drawdown", drawdowns),
|
|
230
|
+
("growth", [r for r in alternating if r["kind"] == "runup"]),
|
|
231
|
+
("decline", [r for r in alternating if r["kind"] == "decline"]),
|
|
232
|
+
):
|
|
233
|
+
summary[name] = {
|
|
234
|
+
"maximum": max((r["amount"] for r in group), default=0.0),
|
|
235
|
+
"average": mean([r["amount"] for r in group]),
|
|
236
|
+
"maximumFraction": max(
|
|
237
|
+
(r["fraction"] for r in group if r["fraction"] is not None), default=None
|
|
238
|
+
),
|
|
239
|
+
"averageFraction": mean([r["fraction"] for r in group if r["fraction"] is not None]),
|
|
240
|
+
"averageDurationNs": str(sum(int(r["durationNs"]) for r in group) // len(group))
|
|
241
|
+
if group and all(r["durationNs"] is not None for r in group)
|
|
242
|
+
else None,
|
|
243
|
+
"current": next((r for r in group if r["current"]), None),
|
|
244
|
+
}
|
|
245
|
+
return {"drawdowns": drawdowns, "alternating": alternating, "summary": summary}
|