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
|
+
"""Existing implementation moved from lib: compute_stats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from openbacktesting.broker.models import finite
|
|
11
|
+
from openbacktesting.portfolio.rates import validate_risk_free_rate
|
|
12
|
+
from openbacktesting.portfolio.snapshot import refresh_snapshot
|
|
13
|
+
from openbacktesting.portfolio.stats import compute_stats as _compute_stats
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compute_stats(
|
|
17
|
+
*,
|
|
18
|
+
stats: pd.Series,
|
|
19
|
+
data: pd.DataFrame,
|
|
20
|
+
trades: pd.DataFrame = None,
|
|
21
|
+
risk_free_rate: float = cast(float, ...),
|
|
22
|
+
) -> pd.Series:
|
|
23
|
+
"""Recompute performance statistics from existing results, data, and an optional trade subset.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
(Re-)compute strategy performance metrics.
|
|
27
|
+
|
|
28
|
+
`stats` is the statistics series as returned by `openbacktesting.engine.Backtest.run()`.
|
|
29
|
+
`data` is OHLC data as passed to the `openbacktesting.engine.Backtest`
|
|
30
|
+
the `stats` were obtained in.
|
|
31
|
+
`trades` can be a dataframe subset of `stats._trades` (e.g. only long trades).
|
|
32
|
+
risk_free_rate is an annualized fraction used for Sharpe, Sortino, and Alpha; omission inherits the latest statistics value,
|
|
33
|
+
missing records fall back to zero, explicit zero overrides, and None is invalid. Recalculation does not rewrite the original run snapshot or actual fees.
|
|
34
|
+
Initial capital inherits statistics_initial_equity; historical records lacking it fall back to the first equity value.
|
|
35
|
+
Initial capital must be positive and finite; approximate equity for trade subsets starts from the same principal.
|
|
36
|
+
|
|
37
|
+
>>> stats = Backtest(GOOG, MyStrategy).run()
|
|
38
|
+
>>> only_long_trades = stats._trades[stats._trades.Size > 0]
|
|
39
|
+
>>> long_stats = compute_stats(stats=stats, trades=only_long_trades,
|
|
40
|
+
... data=GOOG, risk_free_rate=.02)
|
|
41
|
+
"""
|
|
42
|
+
if risk_free_rate is Ellipsis:
|
|
43
|
+
risk_free_rate = stats.attrs.get("statistics_risk_free_rate", 0.0)
|
|
44
|
+
risk_free_rate = validate_risk_free_rate(risk_free_rate)
|
|
45
|
+
equity = stats._equity_curve.Equity
|
|
46
|
+
initial_equity = finite(
|
|
47
|
+
stats.attrs.get("statistics_initial_equity", equity.iloc[0]),
|
|
48
|
+
"initial_equity",
|
|
49
|
+
positive=True,
|
|
50
|
+
)
|
|
51
|
+
exposure = None
|
|
52
|
+
full_account = trades is None
|
|
53
|
+
if full_account:
|
|
54
|
+
trades = stats._trades
|
|
55
|
+
if "_positions" in stats:
|
|
56
|
+
positions = stats["_positions"]
|
|
57
|
+
exposure = np.zeros(len(data), dtype=bool)
|
|
58
|
+
bars = positions[positions.phase == "bar"]
|
|
59
|
+
exposure[bars.bar_index] = bars.gross_qty > 0
|
|
60
|
+
else:
|
|
61
|
+
# XXX: Is this buggy?
|
|
62
|
+
equity = equity.copy()
|
|
63
|
+
equity[:] = initial_equity
|
|
64
|
+
for t in trades.itertuples(index=False):
|
|
65
|
+
equity.iloc[t.EntryBar :] += t.PnL
|
|
66
|
+
result = _compute_stats(
|
|
67
|
+
trades=trades,
|
|
68
|
+
equity=equity.values,
|
|
69
|
+
ohlc_data=data,
|
|
70
|
+
risk_free_rate=risk_free_rate,
|
|
71
|
+
strategy_instance=stats.get("_strategy"),
|
|
72
|
+
exposure=exposure,
|
|
73
|
+
initial_equity=initial_equity,
|
|
74
|
+
)
|
|
75
|
+
if full_account:
|
|
76
|
+
preserved = {
|
|
77
|
+
key: value
|
|
78
|
+
for key, value in stats.items()
|
|
79
|
+
if key.startswith("_")
|
|
80
|
+
and key not in ("_strategy", "_trades", "_equity_curve")
|
|
81
|
+
or key in ("Commissions [$]", "# Closed Lots")
|
|
82
|
+
}
|
|
83
|
+
result = type(result)(dict(result) | preserved, dtype=object)
|
|
84
|
+
result.attrs.update(stats.attrs)
|
|
85
|
+
result.attrs["statistics_risk_free_rate"] = risk_free_rate
|
|
86
|
+
result.attrs["statistics_initial_equity"] = initial_equity
|
|
87
|
+
if full_account:
|
|
88
|
+
refresh_snapshot(result)
|
|
89
|
+
else:
|
|
90
|
+
result.attrs.pop("portfolio_snapshot", None)
|
|
91
|
+
result.attrs["portfolio_diagnostic"] = "portfolio_unavailable_for_legacy_subset"
|
|
92
|
+
return result
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Versioned monetary, trade, and path metrics; legacy statistics are retained independently."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from itertools import groupby
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from openbacktesting.portfolio.models import metric, ratio
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def mean(values):
|
|
12
|
+
return sum(values) / len(values) if values else None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def trade_metrics(trades, opened):
|
|
16
|
+
pnl = [t["pnlNet"] for t in trades]
|
|
17
|
+
wins = [t for t in trades if t["pnlNet"] > 0]
|
|
18
|
+
losses = [t for t in trades if t["pnlNet"] < 0]
|
|
19
|
+
gp, gl = sum(t["pnlNet"] for t in wins), -sum(t["pnlNet"] for t in losses)
|
|
20
|
+
result = {}
|
|
21
|
+
|
|
22
|
+
def put(name, value, unit="ratio", **kwargs):
|
|
23
|
+
result[name] = metric(value, unit, sample=len(trades), **kwargs)
|
|
24
|
+
|
|
25
|
+
for name, value in (
|
|
26
|
+
("closedFragmentCount", len(trades)),
|
|
27
|
+
("openLotCount", len(opened)),
|
|
28
|
+
("closedLotCount", len({t["lotId"] for t in trades} - {t["lotId"] for t in opened})),
|
|
29
|
+
("openedLotCount", len({t["lotId"] for t in trades + opened})),
|
|
30
|
+
("winningCount", len(wins)),
|
|
31
|
+
("losingCount", len(losses)),
|
|
32
|
+
("evenCount", len(trades) - len(wins) - len(losses)),
|
|
33
|
+
):
|
|
34
|
+
put(name, value, "count")
|
|
35
|
+
for name, value in (
|
|
36
|
+
("closedNetPnl", sum(pnl)),
|
|
37
|
+
("grossProfit", gp),
|
|
38
|
+
("grossLoss", gl),
|
|
39
|
+
("expectedPayoff", mean(pnl)),
|
|
40
|
+
("averageProfit", mean([t["pnlNet"] for t in wins])),
|
|
41
|
+
("averageLoss", mean([-t["pnlNet"] for t in losses])),
|
|
42
|
+
("closedFees", sum(t["feeTotal"] for t in trades)),
|
|
43
|
+
):
|
|
44
|
+
put(name, value, "money", reason="no_closed_trades")
|
|
45
|
+
put(
|
|
46
|
+
"profitFactorMoney",
|
|
47
|
+
ratio(gp, gl),
|
|
48
|
+
reason="unbounded" if gp and not gl else "zero_denominator",
|
|
49
|
+
)
|
|
50
|
+
put("winRate", ratio(len(wins), len(trades)), reason="no_closed_trades")
|
|
51
|
+
put("meanTradeReturn", mean([t["returnFraction"] for t in trades]), reason="no_closed_trades")
|
|
52
|
+
for label, group in (("Trade", trades), ("Winning", wins), ("Losing", losses)):
|
|
53
|
+
put(
|
|
54
|
+
"average" + label + "Bars",
|
|
55
|
+
mean([t["barsHeld"] for t in group]),
|
|
56
|
+
"bars",
|
|
57
|
+
reason="no_samples",
|
|
58
|
+
)
|
|
59
|
+
for label, group, sign in (("Profit", wins, 1), ("Loss", losses, -1)):
|
|
60
|
+
put(
|
|
61
|
+
"average" + label + "Return",
|
|
62
|
+
mean([sign * t["returnFraction"] for t in group]),
|
|
63
|
+
reason="no_samples",
|
|
64
|
+
)
|
|
65
|
+
for suffix, key, unit in (("", "pnlNet", "money"), ("Return", "returnFraction", "ratio")):
|
|
66
|
+
item = max(group, key=lambda t: sign * t[key]) if group else None
|
|
67
|
+
put(
|
|
68
|
+
"largest" + label + suffix,
|
|
69
|
+
sign * item[key] if item else None,
|
|
70
|
+
unit,
|
|
71
|
+
reference=item["fragmentId"] if item else None,
|
|
72
|
+
reason="no_samples",
|
|
73
|
+
)
|
|
74
|
+
largest = max([sign * t["pnlNet"] for t in group], default=0.0)
|
|
75
|
+
put(
|
|
76
|
+
"largest" + label + "Share",
|
|
77
|
+
ratio(largest, gp if sign > 0 else gl),
|
|
78
|
+
reason="zero_denominator",
|
|
79
|
+
)
|
|
80
|
+
ap, al = mean([t["pnlNet"] for t in wins]), mean([-t["pnlNet"] for t in losses])
|
|
81
|
+
put("averageProfitLossRatio", ratio(ap, al) if ap is not None else None, reason="no_samples")
|
|
82
|
+
durations = [int(t["durationNs"]) for t in trades if t["durationNs"] is not None]
|
|
83
|
+
put(
|
|
84
|
+
"maxTradeDurationNs",
|
|
85
|
+
str(max(durations)) if durations else None,
|
|
86
|
+
"nanoseconds",
|
|
87
|
+
reason="no_duration",
|
|
88
|
+
)
|
|
89
|
+
put(
|
|
90
|
+
"averageTradeDurationNs",
|
|
91
|
+
str(sum(durations) // len(durations)) if durations else None,
|
|
92
|
+
"nanoseconds",
|
|
93
|
+
reason="no_duration",
|
|
94
|
+
)
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def path_metrics(rows, initial, *, prefix="", approximate=False):
|
|
99
|
+
high = low = initial
|
|
100
|
+
max_dd = max_ru = dd_fraction = ru_fraction = 0.0
|
|
101
|
+
dd_ref = ru_ref = ddf_ref = ruf_ref = None
|
|
102
|
+
invalid_runup_base = False
|
|
103
|
+
|
|
104
|
+
# Treat same-bar highs and lows with unknown ordering as a group; compare only with earlier peaks/troughs without inventing within-group order.
|
|
105
|
+
def group_key(row):
|
|
106
|
+
return row["barIndex"] if not row.get("ordered", True) else row["observationId"]
|
|
107
|
+
|
|
108
|
+
for _, group in groupby(rows, group_key):
|
|
109
|
+
points = list(group)
|
|
110
|
+
bottom = min(points, key=lambda r: r["equity"])
|
|
111
|
+
top = max(points, key=lambda r: r["equity"])
|
|
112
|
+
dd, ru = max(0.0, high - bottom["equity"]), max(0.0, top["equity"] - low)
|
|
113
|
+
if dd > max_dd:
|
|
114
|
+
max_dd, dd_ref = dd, bottom["observationId"]
|
|
115
|
+
if ru > max_ru:
|
|
116
|
+
max_ru, ru_ref = ru, top["observationId"]
|
|
117
|
+
if high > 0 and dd / high > dd_fraction:
|
|
118
|
+
dd_fraction, ddf_ref = dd / high, bottom["observationId"]
|
|
119
|
+
if ru > 0 and low <= 0:
|
|
120
|
+
invalid_runup_base = True
|
|
121
|
+
if low > 0 and ru / low > ru_fraction:
|
|
122
|
+
ru_fraction, ruf_ref = ru / low, top["observationId"]
|
|
123
|
+
high, low = max(high, top["equity"]), min(low, bottom["equity"])
|
|
124
|
+
result = {}
|
|
125
|
+
for key, value, unit, ref in (
|
|
126
|
+
("maxDrawdown", max_dd, "money", dd_ref),
|
|
127
|
+
("maxRunup", max_ru, "money", ru_ref),
|
|
128
|
+
("maxDrawdownFraction", dd_fraction, "ratio", ddf_ref),
|
|
129
|
+
("maxRunupFraction", None if invalid_runup_base else ru_fraction, "ratio", ruf_ref),
|
|
130
|
+
("maxDrawdownInitialFraction", max_dd / initial, "ratio", dd_ref),
|
|
131
|
+
("maxRunupInitialFraction", max_ru / initial, "ratio", ru_ref),
|
|
132
|
+
):
|
|
133
|
+
result[prefix + key] = metric(
|
|
134
|
+
value,
|
|
135
|
+
unit,
|
|
136
|
+
reference=ref,
|
|
137
|
+
method="sampled_bounds_v1" if approximate else "close_path_v1",
|
|
138
|
+
status="undefined" if value is None else ("approximate" if approximate else "ok"),
|
|
139
|
+
reason="nonpositive_runup_base"
|
|
140
|
+
if value is None
|
|
141
|
+
else ("unknown_intrabar_order" if approximate else None),
|
|
142
|
+
)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def monthly_risk(periods, rf):
|
|
147
|
+
returns = [p["equityReturn"] for p in periods]
|
|
148
|
+
if len(returns) < 2 or any(r is None for r in returns):
|
|
149
|
+
return {
|
|
150
|
+
k: metric(
|
|
151
|
+
None,
|
|
152
|
+
method="monthly_compounded_rf_v1",
|
|
153
|
+
status="insufficient_data",
|
|
154
|
+
reason="invalid_or_insufficient_periods",
|
|
155
|
+
sample=len(returns),
|
|
156
|
+
)
|
|
157
|
+
for k in ("sharpe", "sortino")
|
|
158
|
+
}
|
|
159
|
+
excess = np.asarray(returns) - ((1 + rf) ** (1 / 12) - 1)
|
|
160
|
+
denominators = (float(np.std(returns)), float(np.sqrt(np.mean(np.minimum(excess, 0) ** 2))))
|
|
161
|
+
return {
|
|
162
|
+
k: metric(
|
|
163
|
+
ratio(float(np.mean(excess)) * math.sqrt(12), d),
|
|
164
|
+
method="monthly_compounded_rf_v1",
|
|
165
|
+
reason="zero_denominator",
|
|
166
|
+
sample=len(returns),
|
|
167
|
+
)
|
|
168
|
+
for k, d in zip(("sharpe", "sortino"), denominators)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def correlation(x, y):
|
|
173
|
+
pairs = [(a, b) for a, b in zip(x, y) if a is not None and b is not None]
|
|
174
|
+
if len(pairs) < 2:
|
|
175
|
+
return metric(
|
|
176
|
+
None,
|
|
177
|
+
status="insufficient_data",
|
|
178
|
+
reason="insufficient_pairs",
|
|
179
|
+
sample=len(pairs),
|
|
180
|
+
method="paired_period_returns_v1",
|
|
181
|
+
)
|
|
182
|
+
a, b = np.array(pairs).T
|
|
183
|
+
value = float(np.corrcoef(a, b)[0, 1]) if np.std(a) and np.std(b) else None
|
|
184
|
+
return metric(
|
|
185
|
+
value, reason="zero_variance", sample=len(pairs), method="paired_period_returns_v1"
|
|
186
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Strict plain-value encoding, metrics, and immutable snapshots without runtime objects."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def time_ref(value):
|
|
10
|
+
if isinstance(value, pd.Timestamp):
|
|
11
|
+
return {
|
|
12
|
+
"kind": "aware" if value.tzinfo else "naive",
|
|
13
|
+
"value": str(value.as_unit("ns").value),
|
|
14
|
+
}
|
|
15
|
+
return {"kind": "ordinal", "value": str(value)}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def plain(value):
|
|
19
|
+
if value is None or value is pd.NA or value is pd.NaT:
|
|
20
|
+
return None
|
|
21
|
+
if isinstance(value, dict):
|
|
22
|
+
return {str(k): plain(v) for k, v in value.items()}
|
|
23
|
+
if isinstance(value, (list, tuple)):
|
|
24
|
+
return [plain(v) for v in value]
|
|
25
|
+
if isinstance(value, pd.Timestamp):
|
|
26
|
+
return time_ref(value)
|
|
27
|
+
if isinstance(value, pd.Timedelta):
|
|
28
|
+
return str(value.value)
|
|
29
|
+
if isinstance(value, np.generic):
|
|
30
|
+
return plain(value.item())
|
|
31
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
32
|
+
return None
|
|
33
|
+
if isinstance(value, int) and not isinstance(value, bool) and abs(value) > 2**53 - 1:
|
|
34
|
+
return str(value)
|
|
35
|
+
if isinstance(value, (str, bool, int, float)):
|
|
36
|
+
return value
|
|
37
|
+
raise TypeError(f"Portfolio does not support plain-value encoding: {type(value).__name__}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def metric(
|
|
41
|
+
value,
|
|
42
|
+
unit="ratio",
|
|
43
|
+
*,
|
|
44
|
+
method="openbacktesting_portfolio_v1",
|
|
45
|
+
reason=None,
|
|
46
|
+
status=None,
|
|
47
|
+
sample=None,
|
|
48
|
+
reference=None,
|
|
49
|
+
):
|
|
50
|
+
value = plain(value)
|
|
51
|
+
return {
|
|
52
|
+
"value": value,
|
|
53
|
+
"unit": unit,
|
|
54
|
+
"methodId": method,
|
|
55
|
+
"status": status or ("ok" if value is not None else "undefined"),
|
|
56
|
+
"reason": (reason or "undefined_value") if value is None else (reason if status else None),
|
|
57
|
+
"sampleCount": sample,
|
|
58
|
+
"reference": reference,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ratio(numerator, denominator):
|
|
63
|
+
return numerator / denominator if denominator else None
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Chart-calendar buckets, benchmarks, and period correlations."""
|
|
2
|
+
|
|
3
|
+
from decimal import ROUND_FLOOR, Decimal
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from openbacktesting.portfolio.metrics import correlation
|
|
8
|
+
from openbacktesting.portfolio.models import time_ref
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def benchmarks(data, rows, fills, allocations, instrument, initial):
|
|
12
|
+
close_prices = data.Close.to_numpy()
|
|
13
|
+
first = next((a for a in allocations if a["kind"] == "open"), None)
|
|
14
|
+
by_id = {f["fill_id"]: f for f in fills}
|
|
15
|
+
configs = [("input_start_close", 0, float(data.Close.iloc[0]), initial)]
|
|
16
|
+
if first:
|
|
17
|
+
fill = by_id[first["fill_id"]]
|
|
18
|
+
before = next(
|
|
19
|
+
r
|
|
20
|
+
for r in rows
|
|
21
|
+
if r["phase"].startswith("before_")
|
|
22
|
+
and r["fillSequence"] < fill["sequence"]
|
|
23
|
+
and r["barIndex"] == fill["bar_index"]
|
|
24
|
+
)
|
|
25
|
+
configs.append(("first_entry_price", fill["bar_index"], fill["price"], before["equity"]))
|
|
26
|
+
result = {}
|
|
27
|
+
for name, start, price, base in configs:
|
|
28
|
+
step = Decimal(str(instrument["quantity_step"]))
|
|
29
|
+
qty = float(
|
|
30
|
+
(Decimal(str(initial)) / Decimal(str(price)) / step).to_integral_value(
|
|
31
|
+
rounding=ROUND_FLOOR
|
|
32
|
+
)
|
|
33
|
+
* step
|
|
34
|
+
)
|
|
35
|
+
if qty < instrument["min_quantity"] or qty * price < instrument["min_notional"]:
|
|
36
|
+
qty = 0.0
|
|
37
|
+
cash = initial - qty * price
|
|
38
|
+
points = []
|
|
39
|
+
for r in rows:
|
|
40
|
+
if r["phase"] not in ("bar", "terminal") or r["barIndex"] < start:
|
|
41
|
+
continue
|
|
42
|
+
equity = cash + qty * float(close_prices[r["barIndex"]])
|
|
43
|
+
strategy = initial + r["equity"] - base
|
|
44
|
+
points.append(
|
|
45
|
+
{
|
|
46
|
+
"observationId": r["observationId"],
|
|
47
|
+
"barIndex": r["barIndex"],
|
|
48
|
+
"equity": equity,
|
|
49
|
+
"pnlMoney": equity - initial,
|
|
50
|
+
"returnFraction": (equity - initial) / initial,
|
|
51
|
+
"strategyReturn": (strategy - initial) / initial,
|
|
52
|
+
"outperformance": (strategy - equity) / initial,
|
|
53
|
+
"outperformanceMoney": strategy - equity,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
result[name] = {
|
|
57
|
+
"status": "ok",
|
|
58
|
+
"quantity": qty,
|
|
59
|
+
"cash": cash,
|
|
60
|
+
"entryPrice": price,
|
|
61
|
+
"startBar": start,
|
|
62
|
+
"costPolicy": "none",
|
|
63
|
+
"points": points,
|
|
64
|
+
"summary": {
|
|
65
|
+
key: {
|
|
66
|
+
"minimum": min(p[key] for p in points),
|
|
67
|
+
"maximum": max(p[key] for p in points),
|
|
68
|
+
"current": points[-1][key],
|
|
69
|
+
}
|
|
70
|
+
for key in (
|
|
71
|
+
"returnFraction",
|
|
72
|
+
"strategyReturn",
|
|
73
|
+
"outperformance",
|
|
74
|
+
"outperformanceMoney",
|
|
75
|
+
)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
if not first:
|
|
79
|
+
result["first_entry_price"] = {
|
|
80
|
+
"status": "not_applicable",
|
|
81
|
+
"reason": "no_entry",
|
|
82
|
+
"points": [],
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def periodical(rows, trades, fills, benchmark, initial, index):
|
|
88
|
+
if not isinstance(index, pd.DatetimeIndex):
|
|
89
|
+
return {
|
|
90
|
+
"status": "not_applicable",
|
|
91
|
+
"reason": "ordinal_index",
|
|
92
|
+
"buckets": {},
|
|
93
|
+
"correlations": {},
|
|
94
|
+
}
|
|
95
|
+
result, correlations = {}, {}
|
|
96
|
+
start, end = index[0], index[max(r["barIndex"] for r in rows)]
|
|
97
|
+
timezone = index.tz
|
|
98
|
+
naive = index.tz_localize(None) if timezone else index
|
|
99
|
+
for label, freq in (
|
|
100
|
+
("daily", "D"),
|
|
101
|
+
("weekly", "W-SUN"),
|
|
102
|
+
("monthly", "M"),
|
|
103
|
+
("quarterly", "Q"),
|
|
104
|
+
("yearly", "Y"),
|
|
105
|
+
):
|
|
106
|
+
keys = list(naive.to_period(freq))
|
|
107
|
+
covered = pd.period_range(keys[0], keys[max(r["barIndex"] for r in rows)], freq=freq)
|
|
108
|
+
groups = {key: [] for key in covered}
|
|
109
|
+
closed = {key: [] for key in covered}
|
|
110
|
+
paid = {key: 0.0 for key in covered}
|
|
111
|
+
for row in rows:
|
|
112
|
+
groups[keys[row["barIndex"]]].append(row)
|
|
113
|
+
for t in trades:
|
|
114
|
+
closed[keys[t["exit"]["barIndex"]]].append(t)
|
|
115
|
+
for f in fills:
|
|
116
|
+
paid[keys[f["bar_index"]]] += f["fee"]
|
|
117
|
+
previous, previous_bench = initial, initial
|
|
118
|
+
bench_by_bar = {p["barIndex"]: p["equity"] for p in benchmark["points"]}
|
|
119
|
+
buckets = []
|
|
120
|
+
for key, group in groups.items():
|
|
121
|
+
last = group[-1]["equity"] if group else previous
|
|
122
|
+
# Carry forward only unobserved buckets within the calendar range; never extend the execution range.
|
|
123
|
+
bvalue = bench_by_bar[group[-1]["barIndex"]] if group else previous_bench
|
|
124
|
+
left, right = key.start_time, (key + 1).start_time
|
|
125
|
+
if timezone:
|
|
126
|
+
left = left.tz_localize(timezone, ambiguous=True, nonexistent="shift_forward")
|
|
127
|
+
right = right.tz_localize(timezone, ambiguous=True, nonexistent="shift_forward")
|
|
128
|
+
ts = closed[key]
|
|
129
|
+
values = [r["equity"] for r in group]
|
|
130
|
+
buckets.append(
|
|
131
|
+
{
|
|
132
|
+
"start": time_ref(left),
|
|
133
|
+
"endExclusive": time_ref(right),
|
|
134
|
+
"partial": left < start or right > end,
|
|
135
|
+
"carriedForward": not group,
|
|
136
|
+
"count": len(group),
|
|
137
|
+
"firstBar": group[0]["barIndex"] if group else None,
|
|
138
|
+
"lastBar": group[-1]["barIndex"] if group else None,
|
|
139
|
+
"closedNetPnl": sum(t["pnlNet"] for t in ts),
|
|
140
|
+
"grossProfit": sum(max(0, t["pnlNet"]) for t in ts),
|
|
141
|
+
"grossLoss": sum(max(0, -t["pnlNet"]) for t in ts),
|
|
142
|
+
"closedFees": sum(t["feeTotal"] for t in ts),
|
|
143
|
+
"fees": paid[key],
|
|
144
|
+
"equityReturn": last / previous - 1 if previous > 0 else None,
|
|
145
|
+
"benchmarkReturn": bvalue / previous_bench - 1 if previous_bench > 0 else None,
|
|
146
|
+
"favorableExcursion": max([0.0] + [v - previous for v in values]),
|
|
147
|
+
"adverseExcursion": max([0.0] + [previous - v for v in values]),
|
|
148
|
+
"coverage": "partial"
|
|
149
|
+
if any(r["coverage"] == "partial" for r in group)
|
|
150
|
+
else "complete_under_bar_model",
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
previous, previous_bench = last, bvalue
|
|
154
|
+
for b in buckets:
|
|
155
|
+
b["outperformance"] = (
|
|
156
|
+
b["equityReturn"] - b["benchmarkReturn"]
|
|
157
|
+
if b["equityReturn"] is not None and b["benchmarkReturn"] is not None
|
|
158
|
+
else None
|
|
159
|
+
)
|
|
160
|
+
result[label] = buckets
|
|
161
|
+
correlations[label] = correlation(
|
|
162
|
+
[b["equityReturn"] for b in buckets], [b["benchmarkReturn"] for b in buckets]
|
|
163
|
+
)
|
|
164
|
+
return {"status": "ok", "reason": None, "buckets": result, "correlations": correlations}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Pure numeric validation of statistical rates without runtime or account dependencies."""
|
|
2
|
+
|
|
3
|
+
from math import isfinite
|
|
4
|
+
from numbers import Real
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_risk_free_rate(value: float) -> float:
|
|
8
|
+
"""Accept annualized fractions; reject booleans, nonfinite values, and out-of-range values."""
|
|
9
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
10
|
+
raise TypeError("risk_free_rate must be a finite number")
|
|
11
|
+
value = float(value)
|
|
12
|
+
if not isfinite(value) or not -1 < value < 1:
|
|
13
|
+
raise ValueError("risk_free_rate must be in (-1, 1)")
|
|
14
|
+
return value
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Freeze complete facts at runtime; derive reports on reads without executing user code."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from openbacktesting.portfolio.builder import build_report, legacy
|
|
8
|
+
from openbacktesting.portfolio.models import plain, time_ref
|
|
9
|
+
from openbacktesting.portfolio.trades import project_trades
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PortfolioSnapshot(str):
|
|
13
|
+
"""Immutable versioned fact text; read complete reports through to_dict.
|
|
14
|
+
|
|
15
|
+
Snapshots hold no strategy, pandas objects, or mutable inputs. Derived analyses read only frozen facts;
|
|
16
|
+
the text itself is an internal storage format; cross-language consumers use export_result reports.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ()
|
|
20
|
+
|
|
21
|
+
def __new__(cls, payload: str):
|
|
22
|
+
return super().__new__(cls, payload)
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict:
|
|
25
|
+
"""Compute and return an independent strict JSON report without modifying the snapshot or executing strategies."""
|
|
26
|
+
return build_report(json.loads(self))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def freeze(payload):
|
|
30
|
+
return PortfolioSnapshot(
|
|
31
|
+
json.dumps(
|
|
32
|
+
payload, default=plain, ensure_ascii=False, allow_nan=False, separators=(",", ":")
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def capture_snapshot(stats, data, observations, reason, context=None):
|
|
38
|
+
facts, closed, opened = project_trades(stats, observations)
|
|
39
|
+
columns = list(observations.rows[0])
|
|
40
|
+
keys = (
|
|
41
|
+
"statistics_initial_equity",
|
|
42
|
+
"statistics_risk_free_rate",
|
|
43
|
+
"instrument",
|
|
44
|
+
"execution_policy_version",
|
|
45
|
+
"trading_schema_version",
|
|
46
|
+
"engine_version",
|
|
47
|
+
"commission",
|
|
48
|
+
"spread",
|
|
49
|
+
"slippage",
|
|
50
|
+
"margin",
|
|
51
|
+
"end_policy",
|
|
52
|
+
"execution_timing",
|
|
53
|
+
"hedging",
|
|
54
|
+
"allow_short",
|
|
55
|
+
"max_gross_exposure",
|
|
56
|
+
"diagnostics",
|
|
57
|
+
)
|
|
58
|
+
attrs = {k: plain(stats.attrs[k]) for k in keys}
|
|
59
|
+
attrs.update({k: plain(stats.attrs.get(k)) for k in ("input_snapshot", "backtest_snapshot")})
|
|
60
|
+
return freeze(
|
|
61
|
+
{
|
|
62
|
+
"format": "openbacktesting_portfolio_facts_v1",
|
|
63
|
+
"facts": facts,
|
|
64
|
+
"closed": closed,
|
|
65
|
+
"opened": opened,
|
|
66
|
+
"columns": columns,
|
|
67
|
+
"rows": [list(r.values()) for r in observations.rows],
|
|
68
|
+
"index": data.index.as_unit("ns").asi8.astype(str).tolist()
|
|
69
|
+
if isinstance(data.index, pd.DatetimeIndex)
|
|
70
|
+
else data.index.tolist(),
|
|
71
|
+
"close": data.Close.tolist(),
|
|
72
|
+
"high": data.High.tolist(),
|
|
73
|
+
"low": data.Low.tolist(),
|
|
74
|
+
"timeKind": time_ref(data.index[0])["kind"],
|
|
75
|
+
"timezone": str(data.index.tz)
|
|
76
|
+
if isinstance(data.index, pd.DatetimeIndex) and data.index.tz
|
|
77
|
+
else None,
|
|
78
|
+
"reason": reason,
|
|
79
|
+
"context": plain(context),
|
|
80
|
+
"attrs": attrs,
|
|
81
|
+
"legacy": legacy(stats),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def refresh_snapshot(result):
|
|
87
|
+
snapshot = result.attrs.get("portfolio_snapshot")
|
|
88
|
+
if snapshot is None:
|
|
89
|
+
return
|
|
90
|
+
source = json.loads(snapshot)
|
|
91
|
+
source["attrs"]["statistics_risk_free_rate"] = result.attrs["statistics_risk_free_rate"]
|
|
92
|
+
source["attrs"]["backtest_snapshot"] = plain(result.attrs.get("backtest_snapshot"))
|
|
93
|
+
source["legacy"] = legacy(result)
|
|
94
|
+
result.attrs["portfolio_snapshot"] = freeze(source)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_snapshot(result: pd.Series) -> PortfolioSnapshot:
|
|
98
|
+
"""Read the frozen fact snapshot; explicitly reject old results or trade subsets lacking complete facts.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError: The result did not collect Portfolio data.
|
|
102
|
+
"""
|
|
103
|
+
snapshot = result.attrs.get("portfolio_snapshot")
|
|
104
|
+
if not isinstance(snapshot, PortfolioSnapshot):
|
|
105
|
+
raise ValueError("portfolio_snapshot_missing") # noqa: TRY004 — missing snapshot rather than incorrect argument type
|
|
106
|
+
return snapshot
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def export_result(result: pd.Series) -> dict:
|
|
110
|
+
"""Return an independent plain-value copy of the complete report without file, network, or plotting side effects."""
|
|
111
|
+
return get_snapshot(result).to_dict()
|