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,272 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, cast
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from openbacktesting.broker.models import finite
|
|
9
|
+
from openbacktesting.data.core import _data_period
|
|
10
|
+
from openbacktesting.indicators.types import IndicatorHandle
|
|
11
|
+
from openbacktesting.portfolio.rates import validate_risk_free_rate
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from openbacktesting.broker.core import Trade
|
|
15
|
+
from openbacktesting.strategy.base import Strategy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_drawdown_duration_peaks(dd: pd.Series):
|
|
19
|
+
iloc = np.unique(
|
|
20
|
+
np.r_[[-1] if dd.iloc[0] > 0 else [], (dd == 0).values.nonzero()[0], len(dd) - 1]
|
|
21
|
+
)
|
|
22
|
+
iloc = pd.Series(iloc.astype(np.intp))
|
|
23
|
+
df = iloc.to_frame("iloc").assign(prev=iloc.shift())
|
|
24
|
+
df = df[(df["iloc"] > df["prev"] + 1) | (df["prev"] == -1)].astype(np.int64)
|
|
25
|
+
|
|
26
|
+
# If no drawdown since no trade, avoid below for pandas sake and return nan series
|
|
27
|
+
if not len(df):
|
|
28
|
+
return (dd.replace(0, np.nan),) * 2
|
|
29
|
+
|
|
30
|
+
df["duration"] = df["iloc"].map(dd.index.__getitem__) - df["prev"].clip(lower=0).map(
|
|
31
|
+
dd.index.__getitem__
|
|
32
|
+
)
|
|
33
|
+
df["peak_dd"] = df.apply(
|
|
34
|
+
lambda row: dd.iloc[max(0, row["prev"]) : row["iloc"] + 1].max(), axis=1
|
|
35
|
+
)
|
|
36
|
+
df.index = dd.index[df["iloc"]]
|
|
37
|
+
df = df.reindex(dd.index)
|
|
38
|
+
return df["duration"], df["peak_dd"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def geometric_mean(returns: pd.Series) -> float:
|
|
42
|
+
returns = returns.fillna(0) + 1
|
|
43
|
+
if np.any(returns <= 0):
|
|
44
|
+
return 0
|
|
45
|
+
return np.exp(np.log(returns).sum() / (len(returns) or np.nan)) - 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compute_stats(
|
|
49
|
+
trades: list[Trade] | pd.DataFrame,
|
|
50
|
+
equity: np.ndarray,
|
|
51
|
+
ohlc_data: pd.DataFrame,
|
|
52
|
+
strategy_instance: Strategy | None,
|
|
53
|
+
risk_free_rate: float = 0,
|
|
54
|
+
exposure: np.ndarray | None = None,
|
|
55
|
+
initial_equity: float = cast(float, ...),
|
|
56
|
+
) -> pd.Series:
|
|
57
|
+
risk_free_rate = validate_risk_free_rate(risk_free_rate)
|
|
58
|
+
initial_equity = finite(
|
|
59
|
+
equity[0] if initial_equity is Ellipsis else initial_equity,
|
|
60
|
+
"initial_equity",
|
|
61
|
+
positive=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
index = ohlc_data.index
|
|
65
|
+
dd = 1 - equity / np.maximum(initial_equity, np.maximum.accumulate(equity))
|
|
66
|
+
dd_dur, dd_peaks = compute_drawdown_duration_peaks(pd.Series(dd, index=index))
|
|
67
|
+
|
|
68
|
+
equity_df = pd.DataFrame(
|
|
69
|
+
{"Equity": equity, "DrawdownPct": dd, "DrawdownDuration": dd_dur}, index=index
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if isinstance(trades, pd.DataFrame):
|
|
73
|
+
trades_df: pd.DataFrame = trades
|
|
74
|
+
commissions = None # Not shown
|
|
75
|
+
else:
|
|
76
|
+
# Came straight from Backtest.run()
|
|
77
|
+
trades_df = pd.DataFrame(
|
|
78
|
+
{
|
|
79
|
+
"Size": [t.size for t in trades],
|
|
80
|
+
"EntryBar": [t.entry_bar for t in trades],
|
|
81
|
+
"ExitBar": [t.exit_bar for t in trades],
|
|
82
|
+
"EntryPrice": [t.entry_price for t in trades],
|
|
83
|
+
"ExitPrice": [t.exit_price for t in trades],
|
|
84
|
+
"SL": [t.sl for t in trades],
|
|
85
|
+
"TP": [t.tp for t in trades],
|
|
86
|
+
"PnL": [t.pl for t in trades],
|
|
87
|
+
"Commission": [t._commissions for t in trades],
|
|
88
|
+
"ReturnPct": [t.return_fraction for t in trades],
|
|
89
|
+
"EntryTime": [t.entry_time for t in trades],
|
|
90
|
+
"ExitTime": [t.exit_time for t in trades],
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
trades_df["Duration"] = trades_df["ExitTime"] - trades_df["EntryTime"]
|
|
94
|
+
trades_df["Tag"] = [t.tag for t in trades]
|
|
95
|
+
|
|
96
|
+
# Add indicator values
|
|
97
|
+
if len(trades_df) and strategy_instance:
|
|
98
|
+
for ind in strategy_instance._indicators:
|
|
99
|
+
name = ind.name
|
|
100
|
+
ind = np.atleast_2d(ind._view() if isinstance(ind, IndicatorHandle) else ind)
|
|
101
|
+
for i, values in enumerate(ind): # multi-d indicators
|
|
102
|
+
suffix = f"_{i}" if len(ind) > 1 else ""
|
|
103
|
+
trades_df[f"Entry_{name}{suffix}"] = values[trades_df["EntryBar"].values]
|
|
104
|
+
trades_df[f"Exit_{name}{suffix}"] = values[trades_df["ExitBar"].values]
|
|
105
|
+
|
|
106
|
+
commissions = sum(t._commissions for t in trades)
|
|
107
|
+
del trades
|
|
108
|
+
|
|
109
|
+
pl = trades_df["PnL"]
|
|
110
|
+
returns = trades_df["ReturnPct"]
|
|
111
|
+
durations = trades_df["Duration"]
|
|
112
|
+
|
|
113
|
+
def _round_timedelta(value, _period=_data_period(index)): # noqa: B008 — freeze defaults at initialization, preserving native behavior
|
|
114
|
+
if not isinstance(value, pd.Timedelta) or pd.isna(_period):
|
|
115
|
+
return value
|
|
116
|
+
resolution = getattr(_period, "resolution_string", None) or _period.resolution
|
|
117
|
+
return value.ceil(resolution)
|
|
118
|
+
|
|
119
|
+
s = {}
|
|
120
|
+
s["Start"] = index[0]
|
|
121
|
+
s["End"] = index[-1]
|
|
122
|
+
s["Duration"] = s["End"] - s["Start"]
|
|
123
|
+
|
|
124
|
+
s["Exposure Time [%]"] = np.nan if exposure is None else exposure.mean() * 100
|
|
125
|
+
s["Equity Final [$]"] = equity[-1]
|
|
126
|
+
s["Equity Peak [$]"] = max(initial_equity, equity.max())
|
|
127
|
+
if commissions:
|
|
128
|
+
s["Commissions [$]"] = commissions
|
|
129
|
+
s["Return [%]"] = (equity[-1] - initial_equity) / initial_equity * 100
|
|
130
|
+
c = ohlc_data.Close.values
|
|
131
|
+
s["Buy & Hold Return [%]"] = (c[-1] - c[0]) / c[0] * 100
|
|
132
|
+
|
|
133
|
+
gmean_day_return: float = 0
|
|
134
|
+
day_returns = np.array(np.nan)
|
|
135
|
+
annual_trading_days = np.nan
|
|
136
|
+
is_datetime_index = isinstance(index, pd.DatetimeIndex)
|
|
137
|
+
if is_datetime_index:
|
|
138
|
+
freq_days = cast(pd.Timedelta, _data_period(index)).days
|
|
139
|
+
have_weekends = index.dayofweek.to_series().between(5, 6).mean() > 2 / 7 * 0.6
|
|
140
|
+
annual_trading_days = (
|
|
141
|
+
52
|
|
142
|
+
if freq_days == 7
|
|
143
|
+
else 12
|
|
144
|
+
if freq_days == 31
|
|
145
|
+
else 1
|
|
146
|
+
if freq_days == 365
|
|
147
|
+
else (365 if have_weekends else 252)
|
|
148
|
+
)
|
|
149
|
+
freq = {7: "W", 31: "ME", 365: "YE"}.get(freq_days, "D")
|
|
150
|
+
period_equity = equity_df["Equity"].resample(freq).last().dropna()
|
|
151
|
+
day_returns = period_equity.pct_change()
|
|
152
|
+
day_returns.iloc[0] = period_equity.iloc[0] / initial_equity - 1
|
|
153
|
+
gmean_day_return = geometric_mean(day_returns)
|
|
154
|
+
|
|
155
|
+
# Annualized return and risk metrics are computed based on the (mostly correct)
|
|
156
|
+
# assumption that the returns are compounded. See: https://dx.doi.org/10.2139/ssrn.3054517
|
|
157
|
+
# Our annualized return matches `empyrical.annual_return(day_returns)` whereas
|
|
158
|
+
# our risk doesn't; they use the simpler approach below.
|
|
159
|
+
annualized_return = (1 + gmean_day_return) ** annual_trading_days - 1
|
|
160
|
+
s["Return (Ann.) [%]"] = annualized_return * 100
|
|
161
|
+
s["Volatility (Ann.) [%]"] = (
|
|
162
|
+
np.sqrt(
|
|
163
|
+
(day_returns.var(ddof=int(bool(day_returns.shape))) + (1 + gmean_day_return) ** 2)
|
|
164
|
+
** annual_trading_days
|
|
165
|
+
- (1 + gmean_day_return) ** (2 * annual_trading_days)
|
|
166
|
+
)
|
|
167
|
+
* 100
|
|
168
|
+
)
|
|
169
|
+
# d['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
|
|
170
|
+
# d['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
|
|
171
|
+
if is_datetime_index:
|
|
172
|
+
time_in_years = (s["Duration"].days + s["Duration"].seconds / 86400) / 365.25
|
|
173
|
+
s["CAGR [%]"] = (
|
|
174
|
+
((s["Equity Final [$]"] / initial_equity) ** (1 / time_in_years) - 1) * 100
|
|
175
|
+
if time_in_years and equity[-1] > 0
|
|
176
|
+
else np.nan
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
|
|
180
|
+
# and simple standard deviation
|
|
181
|
+
s["Sharpe Ratio"] = (s["Return (Ann.) [%]"] - risk_free_rate * 100) / (
|
|
182
|
+
s["Volatility (Ann.) [%]"] or np.nan
|
|
183
|
+
)
|
|
184
|
+
# Our Sortino mismatches `empyrical.sortino_ratio()` because they use arithmetic mean return
|
|
185
|
+
with np.errstate(divide="ignore"):
|
|
186
|
+
s["Sortino Ratio"] = (annualized_return - risk_free_rate) / (
|
|
187
|
+
np.sqrt(np.mean(day_returns.clip(-np.inf, 0) ** 2)) * np.sqrt(annual_trading_days)
|
|
188
|
+
)
|
|
189
|
+
max_dd = -np.nan_to_num(dd.max())
|
|
190
|
+
s["Calmar Ratio"] = annualized_return / (-max_dd or np.nan)
|
|
191
|
+
with np.errstate(divide="ignore", invalid="ignore"): # Preserve deficits instead of forcing them to zero
|
|
192
|
+
equity_log_returns = np.log(equity[1:] / equity[:-1])
|
|
193
|
+
market_log_returns = np.log(c[1:] / c[:-1])
|
|
194
|
+
beta = np.nan
|
|
195
|
+
if len(equity_log_returns) > 1 and len(market_log_returns) > 1:
|
|
196
|
+
# len == 0 on dummy call `stats_keys = compute_stats(...)` pre optimization
|
|
197
|
+
cov_matrix = np.cov(equity_log_returns, market_log_returns)
|
|
198
|
+
beta = cov_matrix[0, 1] / cov_matrix[1, 1]
|
|
199
|
+
# Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large
|
|
200
|
+
s["Alpha [%]"] = (
|
|
201
|
+
s["Return [%]"]
|
|
202
|
+
- risk_free_rate * 100
|
|
203
|
+
- beta * (s["Buy & Hold Return [%]"] - risk_free_rate * 100)
|
|
204
|
+
)
|
|
205
|
+
s["Beta"] = beta
|
|
206
|
+
s["Max. Drawdown [%]"] = max_dd * 100
|
|
207
|
+
s["Avg. Drawdown [%]"] = -dd_peaks.mean() * 100
|
|
208
|
+
s["Max. Drawdown Duration"] = _round_timedelta(dd_dur.max())
|
|
209
|
+
s["Avg. Drawdown Duration"] = _round_timedelta(dd_dur.mean())
|
|
210
|
+
s["# Trades"] = n_trades = len(trades_df)
|
|
211
|
+
win_rate = np.nan if not n_trades else (pl > 0).mean()
|
|
212
|
+
s["Win Rate [%]"] = win_rate * 100
|
|
213
|
+
s["Best Trade [%]"] = returns.max() * 100
|
|
214
|
+
s["Worst Trade [%]"] = returns.min() * 100
|
|
215
|
+
mean_return = geometric_mean(returns)
|
|
216
|
+
s["Avg. Trade [%]"] = mean_return * 100
|
|
217
|
+
s["Max. Trade Duration"] = _round_timedelta(durations.max())
|
|
218
|
+
s["Avg. Trade Duration"] = _round_timedelta(durations.mean())
|
|
219
|
+
s["Profit Factor"] = returns[returns > 0].sum() / (abs(returns[returns < 0].sum()) or np.nan)
|
|
220
|
+
s["Expectancy [%]"] = returns.mean() * 100
|
|
221
|
+
s["SQN"] = np.sqrt(n_trades) * pl.mean() / (pl.std() or np.nan)
|
|
222
|
+
s["Kelly Criterion"] = win_rate - (1 - win_rate) / (pl[pl > 0].mean() / -pl[pl < 0].mean())
|
|
223
|
+
|
|
224
|
+
s["_strategy"] = strategy_instance
|
|
225
|
+
s["_equity_curve"] = equity_df
|
|
226
|
+
s["_trades"] = trades_df
|
|
227
|
+
|
|
228
|
+
s = _Stats(s, dtype=object)
|
|
229
|
+
s.attrs["statistics_initial_equity"] = initial_equity
|
|
230
|
+
return s
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class _Stats(pd.Series):
|
|
234
|
+
def __repr__(self):
|
|
235
|
+
with pd.option_context(
|
|
236
|
+
"display.max_colwidth",
|
|
237
|
+
20, # Prevent expansion due to _equity and _trades dfs
|
|
238
|
+
"display.max_rows",
|
|
239
|
+
len(self), # Reveal self whole
|
|
240
|
+
"display.precision",
|
|
241
|
+
5, # Enough for my eyes at least
|
|
242
|
+
):
|
|
243
|
+
# XXX: .fillna(nan) to replace None which aren't na_rep'd otherwise (pandas 3.0).
|
|
244
|
+
# Replace with proper na_rep option when that becomes available.
|
|
245
|
+
# https://github.com/pandas-dev/pandas/issues/66407
|
|
246
|
+
return super().fillna(np.nan).to_string(na_rep="--", dtype=True)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def dummy_stats():
|
|
250
|
+
from openbacktesting.broker.core import Trade, _Broker
|
|
251
|
+
|
|
252
|
+
index = pd.DatetimeIndex(["2025"])
|
|
253
|
+
data = pd.DataFrame({col: [np.nan] for col in ("Close",)}, index=index)
|
|
254
|
+
trade = Trade(
|
|
255
|
+
_Broker(
|
|
256
|
+
data=data,
|
|
257
|
+
cash=10000,
|
|
258
|
+
spread=0.01,
|
|
259
|
+
commission=0.01,
|
|
260
|
+
margin=0.1,
|
|
261
|
+
hedging=True,
|
|
262
|
+
exclusive_orders=False,
|
|
263
|
+
index=index,
|
|
264
|
+
),
|
|
265
|
+
1,
|
|
266
|
+
1,
|
|
267
|
+
0,
|
|
268
|
+
None,
|
|
269
|
+
)
|
|
270
|
+
trade.exit_price, trade.exit_bar = 1, 0
|
|
271
|
+
trade._exit_commission = np.nan
|
|
272
|
+
return compute_stats([trade], np.r_[[1.0]], data, None, 0, initial_equity=1.0)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Report projections of orders, allocations, and lots without live objects."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from openbacktesting.portfolio.models import plain, time_ref
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def signal(tag):
|
|
11
|
+
if isinstance(tag, str) and tag:
|
|
12
|
+
return tag, None
|
|
13
|
+
if isinstance(tag, dict):
|
|
14
|
+
name = tag.get("signal")
|
|
15
|
+
comment = tag.get("comment")
|
|
16
|
+
return (
|
|
17
|
+
name if isinstance(name, str) and name else None,
|
|
18
|
+
comment if isinstance(comment, str) else None,
|
|
19
|
+
)
|
|
20
|
+
return None, None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def excursion(state, quantity, entry_price):
|
|
24
|
+
return {
|
|
25
|
+
"mfeMoney": state["mfe"] * quantity,
|
|
26
|
+
"maeMoney": state["mae"] * quantity,
|
|
27
|
+
"mfeFraction": state["mfe"] / entry_price,
|
|
28
|
+
"maeFraction": state["mae"] / entry_price,
|
|
29
|
+
"mfeBar": state["mfeBar"],
|
|
30
|
+
"maeBar": state["maeBar"],
|
|
31
|
+
"coverage": "partial" if state["partial"] else "complete_under_bar_model",
|
|
32
|
+
"methodId": "sampled_bounds_v1",
|
|
33
|
+
"reason": "ambiguous_intrabar_order" if state["partial"] else None,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def project_trades(stats, observations):
|
|
38
|
+
facts = {
|
|
39
|
+
key: stats[field].astype(object).where(stats[field].notna(), None).to_dict("records")
|
|
40
|
+
for key, field in (
|
|
41
|
+
("orders", "_orders"),
|
|
42
|
+
("orderSizing", "_order_sizing"),
|
|
43
|
+
("orderEvents", "_order_events"),
|
|
44
|
+
("fills", "_fills"),
|
|
45
|
+
("allocations", "_trade_allocations"),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
orders = {r["order_id"]: r for r in facts["orders"]}
|
|
49
|
+
fills = {r["fill_id"]: r for r in facts["fills"]}
|
|
50
|
+
|
|
51
|
+
def leg(fill):
|
|
52
|
+
tag = json.loads(orders[fill["order_id"]]["tag_json"])
|
|
53
|
+
name, comment = signal(tag)
|
|
54
|
+
return {
|
|
55
|
+
"fillId": fill["fill_id"],
|
|
56
|
+
"orderId": fill["order_id"],
|
|
57
|
+
"barIndex": fill["bar_index"],
|
|
58
|
+
"time": time_ref(fill["timestamp"]),
|
|
59
|
+
"phase": fill["phase"],
|
|
60
|
+
"sequence": fill["sequence"],
|
|
61
|
+
"price": fill["price"],
|
|
62
|
+
"signal": name,
|
|
63
|
+
"comment": comment,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
extras = {r["allocation_id"]: r for r in stats["_trades"].to_dict("records")}
|
|
67
|
+
closed = []
|
|
68
|
+
cumulative = 0.0
|
|
69
|
+
for allocation in facts["allocations"]:
|
|
70
|
+
if allocation["kind"] != "close":
|
|
71
|
+
continue
|
|
72
|
+
entry, exit_ = fills[allocation["entry_fill_id"]], fills[allocation["fill_id"]]
|
|
73
|
+
qty = allocation["qty"]
|
|
74
|
+
cost = qty * entry["price"]
|
|
75
|
+
pnl = allocation["realized_pnl_gross"] - allocation["entry_fee"] - allocation["fee"]
|
|
76
|
+
cumulative += pnl
|
|
77
|
+
elapsed = exit_["bar_index"] - entry["bar_index"]
|
|
78
|
+
duration = (
|
|
79
|
+
(exit_["timestamp"] - entry["timestamp"]).value
|
|
80
|
+
if isinstance(entry["timestamp"], pd.Timestamp)
|
|
81
|
+
else None
|
|
82
|
+
)
|
|
83
|
+
closed.append(
|
|
84
|
+
{
|
|
85
|
+
"fragmentId": allocation["allocation_id"],
|
|
86
|
+
"lotId": allocation["trade_id"],
|
|
87
|
+
"displayOrdinal": len(closed) + 1,
|
|
88
|
+
"direction": "long" if entry["side"] == "buy" else "short",
|
|
89
|
+
"status": "closed",
|
|
90
|
+
"entry": leg(entry),
|
|
91
|
+
"exit": leg(exit_),
|
|
92
|
+
"quantity": qty,
|
|
93
|
+
"signedQuantity": qty if entry["side"] == "buy" else -qty,
|
|
94
|
+
"entryNotional": cost,
|
|
95
|
+
"exitNotional": qty * exit_["price"],
|
|
96
|
+
"pnlGross": allocation["realized_pnl_gross"],
|
|
97
|
+
"pnlNet": pnl,
|
|
98
|
+
"entryFeeAllocated": allocation["entry_fee"],
|
|
99
|
+
"exitFee": allocation["fee"],
|
|
100
|
+
"feeTotal": allocation["entry_fee"] + allocation["fee"],
|
|
101
|
+
"returnFraction": pnl / cost,
|
|
102
|
+
"barsElapsed": elapsed,
|
|
103
|
+
"barsHeld": elapsed + 1,
|
|
104
|
+
"durationNs": str(duration) if duration is not None else None,
|
|
105
|
+
"cumulativeClosedPnl": cumulative,
|
|
106
|
+
"fractionOfInitialCapital": cumulative / stats.attrs["statistics_initial_equity"],
|
|
107
|
+
"excursion": excursion(
|
|
108
|
+
observations.closed[allocation["allocation_id"]], qty, entry["price"]
|
|
109
|
+
),
|
|
110
|
+
"extensions": plain(extras[allocation["allocation_id"]]),
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
opened = []
|
|
114
|
+
for t in stats["_open_trades"].to_dict("records"):
|
|
115
|
+
entry = fills[t["entry_fill_id"]]
|
|
116
|
+
opened.append(
|
|
117
|
+
{
|
|
118
|
+
"lotId": t["trade_id"],
|
|
119
|
+
"status": "open",
|
|
120
|
+
"direction": "long" if t["Size"] > 0 else "short",
|
|
121
|
+
"entry": leg(entry),
|
|
122
|
+
"exit": None,
|
|
123
|
+
"remainingQuantity": abs(t["Size"]),
|
|
124
|
+
"remainingEntryFee": t["EntryFeeRemaining"],
|
|
125
|
+
"openPnlGross": t["PnLGross"],
|
|
126
|
+
"openPnlNet": t["PnL"],
|
|
127
|
+
"returnFraction": t["ReturnPct"],
|
|
128
|
+
"markPrice": observations.rows[-1]["price"],
|
|
129
|
+
"asOf": observations.rows[-1]["observationId"],
|
|
130
|
+
"excursion": excursion(
|
|
131
|
+
observations.lots[t["trade_id"]], abs(t["Size"]), entry["price"]
|
|
132
|
+
),
|
|
133
|
+
"tag": plain(t["Tag"]),
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
return facts, closed, opened
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Generate plain-value result tables with fixed columns from trading facts."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from openbacktesting.broker.commission import Commission
|
|
10
|
+
from openbacktesting.broker.models import (
|
|
11
|
+
AccountSnapshot,
|
|
12
|
+
Fill,
|
|
13
|
+
NormalizedOrderRequest,
|
|
14
|
+
OrderSizingSnapshot,
|
|
15
|
+
OrderSnapshot,
|
|
16
|
+
PositionSnapshot,
|
|
17
|
+
TradeAllocation,
|
|
18
|
+
columns,
|
|
19
|
+
record,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_FLOAT_FIELDS = {
|
|
23
|
+
"value",
|
|
24
|
+
"reference_price",
|
|
25
|
+
"budget",
|
|
26
|
+
"qty",
|
|
27
|
+
"price",
|
|
28
|
+
"fee",
|
|
29
|
+
"filled_qty",
|
|
30
|
+
"remaining_qty",
|
|
31
|
+
"avg_fill_price",
|
|
32
|
+
"limit",
|
|
33
|
+
"stop",
|
|
34
|
+
"sl",
|
|
35
|
+
"tp",
|
|
36
|
+
"entry_fee",
|
|
37
|
+
"realized_pnl_gross",
|
|
38
|
+
"balance",
|
|
39
|
+
"equity",
|
|
40
|
+
"unrealized_pnl_gross",
|
|
41
|
+
"fees_paid",
|
|
42
|
+
"margin_used",
|
|
43
|
+
"margin_available",
|
|
44
|
+
"gross_notional",
|
|
45
|
+
"net_notional",
|
|
46
|
+
"net_qty",
|
|
47
|
+
"gross_qty",
|
|
48
|
+
"long_qty",
|
|
49
|
+
"short_qty",
|
|
50
|
+
"long_avg_price",
|
|
51
|
+
"short_avg_price",
|
|
52
|
+
"avg_price",
|
|
53
|
+
"unrealized_pnl_net",
|
|
54
|
+
"entry_fees_remaining",
|
|
55
|
+
"return_fraction",
|
|
56
|
+
"Size",
|
|
57
|
+
"EntryPrice",
|
|
58
|
+
"EntryFeeRemaining",
|
|
59
|
+
"PnLGross",
|
|
60
|
+
"PnL",
|
|
61
|
+
"ReturnPct",
|
|
62
|
+
}
|
|
63
|
+
_INT_FIELDS = {"bar_index", "created_bar", "sequence", "EntryBar"}
|
|
64
|
+
_BOOL_FIELDS = {"triggered", "reduce_only"}
|
|
65
|
+
_OBJECT_FIELDS = {"timestamp", "expires_at", "EntryTime", "Tag"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _table(rows, *, columns):
|
|
69
|
+
# Construct columns once with target dtypes; avoid inference followed by per-column conversion/replacement of entire DataFrame blocks.
|
|
70
|
+
data = {}
|
|
71
|
+
for name in columns:
|
|
72
|
+
values = [row.get(name) for row in rows]
|
|
73
|
+
if name in _FLOAT_FIELDS:
|
|
74
|
+
data[name] = np.asarray(values, dtype="float64")
|
|
75
|
+
elif name in _INT_FIELDS:
|
|
76
|
+
data[name] = np.asarray(values, dtype="int64")
|
|
77
|
+
elif name in _BOOL_FIELDS:
|
|
78
|
+
data[name] = np.asarray(values, dtype=bool)
|
|
79
|
+
elif name in _OBJECT_FIELDS:
|
|
80
|
+
# Series preserves object dtype; passing arrays directly makes DataFrame infer timestamps/nested tags again.
|
|
81
|
+
data[name] = pd.Series(values, dtype="object")
|
|
82
|
+
else:
|
|
83
|
+
data[name] = pd.array(values, dtype="Int64" if name == "valid_bars" else "string")
|
|
84
|
+
return pd.DataFrame(data, columns=columns)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def trading_results(stats, broker, end_policy):
|
|
88
|
+
"""Attach ledger tables and traceable configuration without reading trade records from _strategy."""
|
|
89
|
+
original = stats
|
|
90
|
+
stats = dict(stats)
|
|
91
|
+
order_columns = (
|
|
92
|
+
columns(OrderSnapshot)
|
|
93
|
+
+ columns(NormalizedOrderRequest)
|
|
94
|
+
+ ["group_id", "kind", "created_bar"]
|
|
95
|
+
)
|
|
96
|
+
stats["_orders"] = _table(
|
|
97
|
+
[
|
|
98
|
+
record(o.snapshot)
|
|
99
|
+
| record(o.request)
|
|
100
|
+
| {"group_id": o.group_id, "kind": o.kind, "created_bar": o.created_bar}
|
|
101
|
+
for o in broker.all_orders
|
|
102
|
+
],
|
|
103
|
+
columns=order_columns,
|
|
104
|
+
)
|
|
105
|
+
stats["_order_sizing"] = _table(
|
|
106
|
+
[
|
|
107
|
+
{"order_id": o.order_id} | record(o.sizing)
|
|
108
|
+
for o in broker.all_orders
|
|
109
|
+
if o.sizing is not None
|
|
110
|
+
],
|
|
111
|
+
columns=["order_id"] + columns(OrderSizingSnapshot),
|
|
112
|
+
)
|
|
113
|
+
event_columns = ["sequence", "bar_index", "timestamp", "phase"] + columns(OrderSnapshot)
|
|
114
|
+
stats["_order_events"] = _table(
|
|
115
|
+
[
|
|
116
|
+
{
|
|
117
|
+
"sequence": e.sequence,
|
|
118
|
+
"bar_index": e.bar_index,
|
|
119
|
+
"timestamp": e.timestamp,
|
|
120
|
+
"phase": e.phase,
|
|
121
|
+
}
|
|
122
|
+
| record(e.snapshot)
|
|
123
|
+
for e in broker.events
|
|
124
|
+
],
|
|
125
|
+
columns=event_columns,
|
|
126
|
+
)
|
|
127
|
+
for name, records, model in (
|
|
128
|
+
("_fills", broker.fills, Fill),
|
|
129
|
+
("_trade_allocations", broker.allocations, TradeAllocation),
|
|
130
|
+
):
|
|
131
|
+
stats[name] = _table([record(item) for item in records], columns=columns(model))
|
|
132
|
+
prefix = ["bar_index", "timestamp", "phase"]
|
|
133
|
+
stats["_positions"] = _table(broker.position_curve, columns=prefix + columns(PositionSnapshot))
|
|
134
|
+
stats["_account_curve"] = _table(
|
|
135
|
+
broker.account_curve, columns=prefix + columns(AccountSnapshot)
|
|
136
|
+
)
|
|
137
|
+
stats["_final_account"] = record(broker.account)
|
|
138
|
+
stats["_open_trades"] = _table(
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
"trade_id": t.trade_id,
|
|
142
|
+
"entry_fill_id": t.entry_fill_id,
|
|
143
|
+
"Size": t.size,
|
|
144
|
+
"EntryPrice": t.entry_price,
|
|
145
|
+
"EntryBar": t.entry_bar,
|
|
146
|
+
"EntryTime": t.entry_time,
|
|
147
|
+
"EntryFeeRemaining": t._entry_commission,
|
|
148
|
+
"PnLGross": t.pl_gross,
|
|
149
|
+
"PnL": t.pl,
|
|
150
|
+
"ReturnPct": t.return_fraction,
|
|
151
|
+
"Tag": t.tag,
|
|
152
|
+
}
|
|
153
|
+
for t in broker.trades
|
|
154
|
+
],
|
|
155
|
+
columns=[
|
|
156
|
+
"trade_id",
|
|
157
|
+
"entry_fill_id",
|
|
158
|
+
"Size",
|
|
159
|
+
"EntryPrice",
|
|
160
|
+
"EntryBar",
|
|
161
|
+
"EntryTime",
|
|
162
|
+
"EntryFeeRemaining",
|
|
163
|
+
"PnLGross",
|
|
164
|
+
"PnL",
|
|
165
|
+
"ReturnPct",
|
|
166
|
+
"Tag",
|
|
167
|
+
],
|
|
168
|
+
)
|
|
169
|
+
stats["_trades"]["trade_id"] = pd.Series(
|
|
170
|
+
[t.trade_id for t in broker.closed_trades], dtype="string"
|
|
171
|
+
)
|
|
172
|
+
stats["_trades"]["entry_fill_id"] = pd.Series(
|
|
173
|
+
[t.entry_fill_id for t in broker.closed_trades], dtype="string"
|
|
174
|
+
)
|
|
175
|
+
for name in ("allocation_id", "exit_fill_id", "exit_sequence"):
|
|
176
|
+
stats["_trades"][name] = pd.Series(
|
|
177
|
+
[getattr(t, name) for t in broker.closed_trades],
|
|
178
|
+
dtype="int64" if name == "exit_sequence" else "string",
|
|
179
|
+
)
|
|
180
|
+
stats["Commissions [$]"] = broker.ledger.fees_paid
|
|
181
|
+
stats["# Closed Lots"] = len(
|
|
182
|
+
{t.trade_id for t in broker.closed_trades} - {t.trade_id for t in broker.trades}
|
|
183
|
+
)
|
|
184
|
+
commission = broker._commission_model
|
|
185
|
+
if isinstance(commission, Commission):
|
|
186
|
+
commission = commission.snapshot()
|
|
187
|
+
elif callable(commission):
|
|
188
|
+
commission = {
|
|
189
|
+
"reconstructable": False,
|
|
190
|
+
"name": getattr(commission, "__qualname__", type(commission).__name__),
|
|
191
|
+
}
|
|
192
|
+
broker.diagnostics.append({"code": "commission_not_reconstructable"})
|
|
193
|
+
result = type(original)(stats, dtype=object)
|
|
194
|
+
result.attrs.update(original.attrs)
|
|
195
|
+
result.attrs.update(
|
|
196
|
+
{
|
|
197
|
+
"trading_schema_version": 4,
|
|
198
|
+
"execution_policy_version": "2",
|
|
199
|
+
"engine_version": version("openbacktesting"),
|
|
200
|
+
"instrument": asdict(broker.instrument),
|
|
201
|
+
"account_model": "linear_margin",
|
|
202
|
+
"execution_timing": broker.execution_timing,
|
|
203
|
+
"end_policy": end_policy,
|
|
204
|
+
"commission": commission,
|
|
205
|
+
"spread": broker._spread,
|
|
206
|
+
"slippage": asdict(broker.slippage),
|
|
207
|
+
"margin": broker.margin,
|
|
208
|
+
"allow_short": broker.allow_short,
|
|
209
|
+
"max_gross_exposure": broker.max_gross_exposure,
|
|
210
|
+
"hedging": broker._hedging,
|
|
211
|
+
"diagnostics": list(broker.diagnostics),
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return result
|
openbacktesting/py.typed
ADDED
|
File without changes
|