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,381 @@
|
|
|
1
|
+
"""Assemble Portfolio once; project trade facts only after simulation ends."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from openbacktesting.portfolio.analysis import distribution, episodes, streaks, time_patterns
|
|
9
|
+
from openbacktesting.portfolio.metrics import mean, monthly_risk, path_metrics, trade_metrics
|
|
10
|
+
from openbacktesting.portfolio.models import metric, ratio, time_ref
|
|
11
|
+
from openbacktesting.portfolio.periods import benchmarks, periodical
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def contributions(rows, facts, initial):
|
|
15
|
+
fills = {f["fill_id"]: f for f in facts["fills"]}
|
|
16
|
+
grouped = {}
|
|
17
|
+
for a in facts["allocations"]:
|
|
18
|
+
f = fills[a["fill_id"]]
|
|
19
|
+
side = "long" if fills[a["entry_fill_id"]]["side"] == "buy" else "short"
|
|
20
|
+
grouped.setdefault(f["sequence"], []).append((side, a["realized_pnl_gross"] - a["fee"]))
|
|
21
|
+
pending = iter(sorted(grouped.items()))
|
|
22
|
+
current = next(pending, None)
|
|
23
|
+
balance = {"long": 0.0, "short": 0.0}
|
|
24
|
+
result = {"long": [], "short": []}
|
|
25
|
+
for row in rows:
|
|
26
|
+
while current is not None and current[0] <= row["fillSequence"]:
|
|
27
|
+
for side, value in current[1]:
|
|
28
|
+
balance[side] += value
|
|
29
|
+
current = next(pending, None)
|
|
30
|
+
for side, sign in (("long", 1), ("short", -1)):
|
|
31
|
+
qty = row[side + "_qty"]
|
|
32
|
+
avg = row[side + "_avg_price"]
|
|
33
|
+
unrealized = sign * qty * (row["price"] - avg) if avg is not None else 0.0
|
|
34
|
+
result[side].append(
|
|
35
|
+
{
|
|
36
|
+
"observationId": row["observationId"],
|
|
37
|
+
"barIndex": row["barIndex"],
|
|
38
|
+
"timestamp": row["timestamp"],
|
|
39
|
+
"phase": row["phase"],
|
|
40
|
+
"contribution": balance[side] + unrealized,
|
|
41
|
+
"equity": initial + balance[side] + unrealized,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def legacy(stats):
|
|
48
|
+
return {
|
|
49
|
+
str(k): metric(v, "native", method="engine_legacy_v1")
|
|
50
|
+
for k, v in stats.items()
|
|
51
|
+
if not k.startswith("_")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_report(source):
|
|
56
|
+
stats = SimpleNamespace(attrs=source["attrs"])
|
|
57
|
+
kind, timezone = source["timeKind"], source["timezone"]
|
|
58
|
+
if kind == "ordinal":
|
|
59
|
+
index = pd.Index(source["index"])
|
|
60
|
+
else:
|
|
61
|
+
index = pd.to_datetime([int(t) for t in source["index"]], utc=kind == "aware")
|
|
62
|
+
if timezone:
|
|
63
|
+
index = index.tz_convert(timezone)
|
|
64
|
+
data = pd.DataFrame({"Close": source["close"]}, index=index)
|
|
65
|
+
initial = stats.attrs["statistics_initial_equity"]
|
|
66
|
+
facts, closed, opened = source["facts"], source["closed"], source["opened"]
|
|
67
|
+
rows = [dict(zip(source["columns"], row)) for row in source["rows"]]
|
|
68
|
+
expanded, id_map = [], {}
|
|
69
|
+
for row in rows:
|
|
70
|
+
row["timestamp"] = index[row["barIndex"]]
|
|
71
|
+
eligible = row.pop("rangeEligible")
|
|
72
|
+
if eligible:
|
|
73
|
+
for price in (source["high"][row["barIndex"]], source["low"][row["barIndex"]]):
|
|
74
|
+
extreme = row.copy()
|
|
75
|
+
gross = row["unrealized_pnl_gross"] + row["net_qty"] * (price - row["price"])
|
|
76
|
+
equity = row["balance"] + gross
|
|
77
|
+
used = row["gross_qty"] * price * stats.attrs["margin"]
|
|
78
|
+
cost = sum(
|
|
79
|
+
row[side + "_qty"] * (row[side + "_avg_price"] or 0)
|
|
80
|
+
for side in ("long", "short")
|
|
81
|
+
)
|
|
82
|
+
extreme.update(
|
|
83
|
+
price=price,
|
|
84
|
+
phase="bar_range",
|
|
85
|
+
ordered=False,
|
|
86
|
+
equity=equity,
|
|
87
|
+
unrealized_pnl_gross=gross,
|
|
88
|
+
unrealized_pnl_net=gross - row["entry_fees_remaining"],
|
|
89
|
+
margin_used=used,
|
|
90
|
+
margin_available=max(0.0, equity - used),
|
|
91
|
+
gross_notional=row["gross_qty"] * price,
|
|
92
|
+
net_notional=row["net_qty"] * price,
|
|
93
|
+
return_fraction=(gross - row["entry_fees_remaining"]) / cost
|
|
94
|
+
if cost and row["avg_price"] is not None
|
|
95
|
+
else None,
|
|
96
|
+
)
|
|
97
|
+
expanded.append(extreme)
|
|
98
|
+
old_id = row["observationId"]
|
|
99
|
+
expanded.append(row)
|
|
100
|
+
id_map[old_id] = f"observation-{len(expanded)}"
|
|
101
|
+
rows = expanded
|
|
102
|
+
for i, row in enumerate(rows, 1):
|
|
103
|
+
row.update(observationId=f"observation-{i}", observationSequence=i)
|
|
104
|
+
for lot in opened:
|
|
105
|
+
lot["asOf"] = id_map[lot["asOf"]]
|
|
106
|
+
for table in (facts["fills"], facts["orderEvents"]):
|
|
107
|
+
for row in table:
|
|
108
|
+
row["timestamp"] = index[row["bar_index"]]
|
|
109
|
+
reason, context = source["reason"], source["context"]
|
|
110
|
+
# Snapshot curves retain each bar's final ordinary close and a separate terminal snapshot; intermediate observations in the same close phase do not overwrite them.
|
|
111
|
+
bar_rows = {r["barIndex"]: r for r in rows if r["phase"] == "bar"}
|
|
112
|
+
close_rows = [bar_rows[i] for i in sorted(bar_rows)] + [
|
|
113
|
+
r for r in rows if r["phase"] == "terminal"
|
|
114
|
+
]
|
|
115
|
+
end = rows[-1]["barIndex"]
|
|
116
|
+
benchmark = benchmarks(
|
|
117
|
+
data, rows, facts["fills"], facts["allocations"], stats.attrs["instrument"], initial
|
|
118
|
+
)
|
|
119
|
+
periods = periodical(
|
|
120
|
+
rows, closed, facts["fills"], benchmark["input_start_close"], initial, data.index
|
|
121
|
+
)
|
|
122
|
+
direction = contributions(rows, facts, initial)
|
|
123
|
+
summary = {}
|
|
124
|
+
directional_monthly = {}
|
|
125
|
+
close_ids = {v["observationId"] for v in close_rows}
|
|
126
|
+
for side in ("all", "long", "short"):
|
|
127
|
+
trades = [t for t in closed if side == "all" or t["direction"] == side]
|
|
128
|
+
lots = [t for t in opened if side == "all" or t["direction"] == side]
|
|
129
|
+
values = trade_metrics(trades, lots)
|
|
130
|
+
path = (
|
|
131
|
+
close_rows
|
|
132
|
+
if side == "all"
|
|
133
|
+
else [r for r in direction[side] if r["observationId"] in close_ids]
|
|
134
|
+
)
|
|
135
|
+
values.update(path_metrics(path, initial))
|
|
136
|
+
final = path[-1]["equity"]
|
|
137
|
+
for key, val, unit in (
|
|
138
|
+
("initialCapital", initial, "money"),
|
|
139
|
+
("equityFinal", final, "money"),
|
|
140
|
+
("equityPeak", max([initial] + [r["equity"] for r in path]), "money"),
|
|
141
|
+
("totalPnl", final - initial, "money"),
|
|
142
|
+
("totalReturn", (final - initial) / initial, "ratio"),
|
|
143
|
+
("openPnlGross", sum(t["openPnlGross"] for t in lots), "money"),
|
|
144
|
+
("openPnlNet", sum(t["openPnlNet"] for t in lots), "money"),
|
|
145
|
+
("remainingEntryFees", sum(t["remainingEntryFee"] for t in lots), "money"),
|
|
146
|
+
(
|
|
147
|
+
"feesPaid",
|
|
148
|
+
sum(t["feeTotal"] for t in trades) + sum(t["remainingEntryFee"] for t in lots),
|
|
149
|
+
"money",
|
|
150
|
+
),
|
|
151
|
+
("closedPnlGross", sum(t["pnlGross"] for t in trades), "money"),
|
|
152
|
+
(
|
|
153
|
+
"totalPnlGross",
|
|
154
|
+
sum(t["pnlGross"] for t in trades) + sum(t["openPnlGross"] for t in lots),
|
|
155
|
+
"money",
|
|
156
|
+
),
|
|
157
|
+
("closedReturn", sum(t["pnlNet"] for t in trades) / initial, "ratio"),
|
|
158
|
+
):
|
|
159
|
+
values[key] = metric(
|
|
160
|
+
val,
|
|
161
|
+
unit,
|
|
162
|
+
method="shared_initial_capital_v1" if side != "all" else "openbacktesting_portfolio_v1",
|
|
163
|
+
)
|
|
164
|
+
if side != "all":
|
|
165
|
+
monthly = []
|
|
166
|
+
if isinstance(data.index, pd.DatetimeIndex):
|
|
167
|
+
endpoints = {}
|
|
168
|
+
for row in path:
|
|
169
|
+
endpoints[data.index[row["barIndex"]].strftime("%Y-%m")] = row["equity"]
|
|
170
|
+
previous = initial
|
|
171
|
+
for key in periods["buckets"].get("monthly", []):
|
|
172
|
+
stamp = pd.Timestamp(int(key["start"]["value"]), tz="UTC")
|
|
173
|
+
if data.index.tz:
|
|
174
|
+
stamp = stamp.tz_convert(data.index.tz)
|
|
175
|
+
value = endpoints.get(stamp.strftime("%Y-%m"), previous)
|
|
176
|
+
monthly.append({"equityReturn": value / previous - 1 if previous > 0 else None})
|
|
177
|
+
previous = value
|
|
178
|
+
values.update(monthly_risk(monthly, stats.attrs["statistics_risk_free_rate"]))
|
|
179
|
+
directional_monthly[side] = monthly
|
|
180
|
+
summary[side] = values
|
|
181
|
+
all_ = summary["all"]
|
|
182
|
+
last = rows[-1]
|
|
183
|
+
all_["balance"] = metric(last["balance"], "money")
|
|
184
|
+
all_["feesPaid"] = metric(last["fees_paid"], "money")
|
|
185
|
+
all_["commissionLoad"] = metric(
|
|
186
|
+
ratio(last["fees_paid"], sum(abs(f["qty"] * f["price"]) for f in facts["fills"])),
|
|
187
|
+
method="commission_load_gross_activity_v1",
|
|
188
|
+
reason="zero_denominator",
|
|
189
|
+
)
|
|
190
|
+
average_margin = mean([r["margin_used"] for r in bar_rows.values()])
|
|
191
|
+
all_["averageMarginUsed"] = metric(average_margin, "money", method="bar_close_mean_v1")
|
|
192
|
+
all_["maxMarginUsed"] = metric(
|
|
193
|
+
max(r["margin_used"] for r in rows), "money", method="execution_samples_v1"
|
|
194
|
+
)
|
|
195
|
+
for key, field in (
|
|
196
|
+
("maxGrossQuantity", "gross_qty"),
|
|
197
|
+
("maxLongQuantity", "long_qty"),
|
|
198
|
+
("maxShortQuantity", "short_qty"),
|
|
199
|
+
("maxNetQuantity", "net_qty"),
|
|
200
|
+
):
|
|
201
|
+
all_[key] = metric(
|
|
202
|
+
max(abs(r[field]) for r in rows), "quantity", method="execution_samples_v1"
|
|
203
|
+
)
|
|
204
|
+
all_.update(path_metrics(rows, initial, prefix="intrabar.", approximate=True))
|
|
205
|
+
required = max(0.0, max(r["margin_used"] - (r["equity"] - initial) for r in rows))
|
|
206
|
+
all_["observedCapitalRequired"] = metric(
|
|
207
|
+
required, "money", method="observed_capital_requirement_v1"
|
|
208
|
+
)
|
|
209
|
+
net = all_["closedNetPnl"]["value"]
|
|
210
|
+
for key, denominator in (
|
|
211
|
+
("returnOnInitialCapital", initial),
|
|
212
|
+
("returnOnMaxDrawdown", all_["intrabar.maxDrawdown"]["value"]),
|
|
213
|
+
("netPnlToLargestLoss", all_["largestLoss"]["value"]),
|
|
214
|
+
("marginEfficiency", average_margin),
|
|
215
|
+
("returnOnObservedCapital", required),
|
|
216
|
+
):
|
|
217
|
+
all_[key] = metric(
|
|
218
|
+
ratio(net, denominator),
|
|
219
|
+
method="observed_capital_efficiency_v1",
|
|
220
|
+
reason="zero_denominator",
|
|
221
|
+
)
|
|
222
|
+
for key in (
|
|
223
|
+
"marginCalls",
|
|
224
|
+
"totalMarginLiquidatedVolume",
|
|
225
|
+
"largestMarginLiquidatedVolume",
|
|
226
|
+
"tvAccountSizeRequired",
|
|
227
|
+
"tvReturnOnAccountSize",
|
|
228
|
+
"currencyConversion",
|
|
229
|
+
"contractMultiplier",
|
|
230
|
+
):
|
|
231
|
+
all_[key] = metric(
|
|
232
|
+
None,
|
|
233
|
+
"count" if key == "marginCalls" else "money",
|
|
234
|
+
status="unsupported",
|
|
235
|
+
reason="unsupported_trading_model",
|
|
236
|
+
)
|
|
237
|
+
all_.update(
|
|
238
|
+
monthly_risk(
|
|
239
|
+
periods["buckets"].get("monthly", []), stats.attrs["statistics_risk_free_rate"]
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
years = (
|
|
243
|
+
(data.index[end] - data.index[0]).total_seconds() / (365.25 * 86400)
|
|
244
|
+
if isinstance(data.index, pd.DatetimeIndex)
|
|
245
|
+
else 0.0
|
|
246
|
+
)
|
|
247
|
+
exponent = math.log(last["equity"] / initial) / years if years and last["equity"] > 0 else None
|
|
248
|
+
cagr = math.expm1(exponent) if exponent is not None and exponent < 709 else None
|
|
249
|
+
all_["cagr"] = metric(cagr, reason="nonpositive_equity_or_duration")
|
|
250
|
+
all_["buyHoldPnl"] = metric(benchmark["input_start_close"]["points"][-1]["pnlMoney"], "money")
|
|
251
|
+
all_["buyHoldReturn"] = metric(benchmark["input_start_close"]["points"][-1]["returnFraction"])
|
|
252
|
+
all_["outperformance"] = metric(benchmark["input_start_close"]["points"][-1]["outperformance"])
|
|
253
|
+
all_["correlation"] = periods["correlations"].get(
|
|
254
|
+
"daily", metric(None, status="not_applicable", reason="ordinal_index")
|
|
255
|
+
)
|
|
256
|
+
breakdown = {}
|
|
257
|
+
for grouping in ("signal", "direction"):
|
|
258
|
+
groups = {}
|
|
259
|
+
for t in closed:
|
|
260
|
+
key = t["entry"]["signal"] if grouping == "signal" else t["direction"]
|
|
261
|
+
groups.setdefault(key, []).append(t)
|
|
262
|
+
for t in opened:
|
|
263
|
+
key = t["entry"]["signal"] if grouping == "signal" else t["direction"]
|
|
264
|
+
groups.setdefault(key, [])
|
|
265
|
+
breakdown[grouping] = [
|
|
266
|
+
{
|
|
267
|
+
"key": key,
|
|
268
|
+
"metrics": trade_metrics(
|
|
269
|
+
group,
|
|
270
|
+
[
|
|
271
|
+
t
|
|
272
|
+
for t in opened
|
|
273
|
+
if (t["entry"]["signal"] if grouping == "signal" else t["direction"]) == key
|
|
274
|
+
],
|
|
275
|
+
),
|
|
276
|
+
"openPnlNet": sum(
|
|
277
|
+
t["openPnlNet"]
|
|
278
|
+
for t in opened
|
|
279
|
+
if (t["entry"]["signal"] if grouping == "signal" else t["direction"]) == key
|
|
280
|
+
),
|
|
281
|
+
}
|
|
282
|
+
for key, group in groups.items()
|
|
283
|
+
]
|
|
284
|
+
terminal = {}
|
|
285
|
+
for why in ("insolvent", "end_liquidation"):
|
|
286
|
+
terminal[why] = {
|
|
287
|
+
"quantity": sum(f["qty"] for f in facts["fills"] if f["reason"] == why),
|
|
288
|
+
"notional": sum(f["qty"] * f["price"] for f in facts["fills"] if f["reason"] == why),
|
|
289
|
+
}
|
|
290
|
+
payload = {
|
|
291
|
+
"schemaVersion": "1.0.0",
|
|
292
|
+
"semantics": {
|
|
293
|
+
"metricsDefinitionVersion": "openbacktesting_portfolio_v1",
|
|
294
|
+
"executionPolicyVersion": stats.attrs["execution_policy_version"],
|
|
295
|
+
"tradingSchemaVersion": stats.attrs["trading_schema_version"],
|
|
296
|
+
"engineVersion": stats.attrs["engine_version"],
|
|
297
|
+
"tradingViewCompatibility": "not_verified",
|
|
298
|
+
"riskFreeRate": stats.attrs["statistics_risk_free_rate"],
|
|
299
|
+
},
|
|
300
|
+
"run": {
|
|
301
|
+
"runId": None,
|
|
302
|
+
"strategyRef": None,
|
|
303
|
+
"datasetRef": None,
|
|
304
|
+
"inputRange": [time_ref(data.index[0]), time_ref(data.index[-1])],
|
|
305
|
+
"executedRange": [time_ref(data.index[0]), time_ref(data.index[end])],
|
|
306
|
+
"processedThroughBar": end,
|
|
307
|
+
"terminationReason": reason,
|
|
308
|
+
"complete": reason == "end_of_data",
|
|
309
|
+
},
|
|
310
|
+
"context": {
|
|
311
|
+
"instrument": stats.attrs["instrument"],
|
|
312
|
+
"timezone": str(data.index.tz)
|
|
313
|
+
if isinstance(data.index, pd.DatetimeIndex) and data.index.tz
|
|
314
|
+
else None,
|
|
315
|
+
"timeKind": time_ref(data.index[0])["kind"],
|
|
316
|
+
"dataContext": context,
|
|
317
|
+
"settings": {
|
|
318
|
+
k: stats.attrs[k]
|
|
319
|
+
for k in (
|
|
320
|
+
"commission",
|
|
321
|
+
"spread",
|
|
322
|
+
"slippage",
|
|
323
|
+
"margin",
|
|
324
|
+
"end_policy",
|
|
325
|
+
"execution_timing",
|
|
326
|
+
"hedging",
|
|
327
|
+
"allow_short",
|
|
328
|
+
"max_gross_exposure",
|
|
329
|
+
)
|
|
330
|
+
},
|
|
331
|
+
"inputs": stats.attrs.get("input_snapshot"),
|
|
332
|
+
"backtest": stats.attrs.get("backtest_snapshot"),
|
|
333
|
+
},
|
|
334
|
+
"facts": facts,
|
|
335
|
+
"trades": {"closedFragments": closed, "openLots": opened},
|
|
336
|
+
"summary": summary,
|
|
337
|
+
"series": {
|
|
338
|
+
"account": rows,
|
|
339
|
+
"close": close_rows,
|
|
340
|
+
"directions": direction,
|
|
341
|
+
"benchmark": benchmark,
|
|
342
|
+
"closedCumulative": [
|
|
343
|
+
{
|
|
344
|
+
"fragmentId": t["fragmentId"],
|
|
345
|
+
"value": t["cumulativeClosedPnl"],
|
|
346
|
+
"fraction": t["fractionOfInitialCapital"],
|
|
347
|
+
}
|
|
348
|
+
for t in closed
|
|
349
|
+
],
|
|
350
|
+
"margin": [
|
|
351
|
+
{
|
|
352
|
+
"observationId": r["observationId"],
|
|
353
|
+
"margin": r["margin_used"],
|
|
354
|
+
"utilization": r["margin_used"] / r["equity"] if r["equity"] > 0 else None,
|
|
355
|
+
}
|
|
356
|
+
for r in rows
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
"analysis": {
|
|
360
|
+
"directionalMonthly": directional_monthly,
|
|
361
|
+
"breakdown": breakdown,
|
|
362
|
+
"periodical": periods,
|
|
363
|
+
"distribution": distribution(closed),
|
|
364
|
+
"streaks": streaks(closed),
|
|
365
|
+
"timePatterns": time_patterns(closed, data.index),
|
|
366
|
+
"growthAndDecline": episodes(close_rows, initial),
|
|
367
|
+
"liquidations": terminal,
|
|
368
|
+
},
|
|
369
|
+
"extensions": {"legacy": source["legacy"]},
|
|
370
|
+
"diagnostics": list(stats.attrs["diagnostics"]),
|
|
371
|
+
}
|
|
372
|
+
for table in facts.values():
|
|
373
|
+
for row in table:
|
|
374
|
+
if "timestamp" in row:
|
|
375
|
+
row["timestamp"] = time_ref(row["timestamp"])
|
|
376
|
+
for row in rows:
|
|
377
|
+
row["timestamp"] = time_ref(row["timestamp"])
|
|
378
|
+
for curve in direction.values():
|
|
379
|
+
for row in curve:
|
|
380
|
+
row["timestamp"] = time_ref(row["timestamp"])
|
|
381
|
+
return payload
|