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,192 @@
|
|
|
1
|
+
"""Author-supplied order descriptions and public argument validation without trading side effects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Literal, cast, overload
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from openbacktesting.broker.models import finite, freeze_tag
|
|
13
|
+
from openbacktesting.broker.sizing import UNSET, _SizeIntent, select_size
|
|
14
|
+
|
|
15
|
+
type _JsonValue = str | int | float | bool | None | list["_JsonValue"] | dict[str, "_JsonValue"]
|
|
16
|
+
type _Expiry = datetime | pd.Timestamp | str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_conditions(
|
|
20
|
+
*,
|
|
21
|
+
limit=None,
|
|
22
|
+
stop=None,
|
|
23
|
+
sl=None,
|
|
24
|
+
tp=None,
|
|
25
|
+
tag=None,
|
|
26
|
+
reduce_only=False,
|
|
27
|
+
expires_at=None,
|
|
28
|
+
valid_bars=None,
|
|
29
|
+
) -> dict:
|
|
30
|
+
"""Normalize conditions requiring no account or instrument; do not modify caller data."""
|
|
31
|
+
if type(reduce_only) is not bool:
|
|
32
|
+
raise TypeError("reduce_only must be bool")
|
|
33
|
+
prices = {
|
|
34
|
+
name: None if value is None else finite(value, name, positive=True)
|
|
35
|
+
for name, value in (("limit", limit), ("stop", stop), ("sl", sl), ("tp", tp))
|
|
36
|
+
}
|
|
37
|
+
if expires_at is not None and valid_bars is not None:
|
|
38
|
+
raise ValueError("expires_at and valid_bars are mutually exclusive")
|
|
39
|
+
if valid_bars is not None and (
|
|
40
|
+
isinstance(valid_bars, bool) or not isinstance(valid_bars, int) or valid_bars <= 0
|
|
41
|
+
):
|
|
42
|
+
raise ValueError("valid_bars must be a positive integer")
|
|
43
|
+
if expires_at is not None:
|
|
44
|
+
if not isinstance(expires_at, (datetime, pd.Timestamp, str)):
|
|
45
|
+
raise TypeError("expires_at must be a timezone-aware timestamp or string")
|
|
46
|
+
expires_at = pd.Timestamp(expires_at)
|
|
47
|
+
if pd.isna(expires_at):
|
|
48
|
+
raise ValueError("expires_at must not be NaT")
|
|
49
|
+
if expires_at.tzinfo is None:
|
|
50
|
+
raise ValueError("Absolute expiration must be timezone-aware")
|
|
51
|
+
return prices | {
|
|
52
|
+
"reduce_only": reduce_only,
|
|
53
|
+
"expires_at": expires_at,
|
|
54
|
+
"valid_bars": valid_bars,
|
|
55
|
+
"tag_json": freeze_tag(tag),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, init=False)
|
|
60
|
+
class OrderRequest:
|
|
61
|
+
"""Frozen entry request awaiting submission; construction does not register an order and is allowed during initialization.
|
|
62
|
+
|
|
63
|
+
All arguments must be supplied by name. Instrument precision, market-data timezone, and protection direction are validated at submission.
|
|
64
|
+
``tag`` supports plain JSON values; it is frozen at construction, and reads return independent copies.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
side: Buy (buy) or sell (sell).
|
|
68
|
+
qty: Finite positive asset quantity; choose exactly one of qty/amount/percent, without using a default budget.
|
|
69
|
+
amount: Finite positive notional amount, converted using Close at submission without reserving fees.
|
|
70
|
+
percent: Equity percentage in (0,100]; 20 means 20%, calculated at submission.
|
|
71
|
+
limit: Entry limit price; None means no limit condition.
|
|
72
|
+
stop: Entry trigger price, distinct from the post-entry sl.
|
|
73
|
+
sl: Absolute post-entry stop-loss price.
|
|
74
|
+
tp: Absolute post-entry take-profit price.
|
|
75
|
+
tag: Optional user tag, excluded from matching conditions.
|
|
76
|
+
expires_at: Timezone-aware absolute expiration, mutually exclusive with valid_bars.
|
|
77
|
+
valid_bars: Number of bars eligible for execution after submission; must be a positive integer.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
TypeError: Invalid argument types, names, or tag types.
|
|
81
|
+
ValueError: Invalid quantity, price, direction, or expiration values.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
side: Literal["buy", "sell"]
|
|
85
|
+
_size: _SizeIntent
|
|
86
|
+
limit: float | None = None
|
|
87
|
+
stop: float | None = None
|
|
88
|
+
sl: float | None = None
|
|
89
|
+
tp: float | None = None
|
|
90
|
+
expires_at: pd.Timestamp | None = None
|
|
91
|
+
valid_bars: int | None = None
|
|
92
|
+
_tag_json: str = field(default="null", repr=False)
|
|
93
|
+
|
|
94
|
+
@overload
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
side: Literal["buy", "sell"],
|
|
99
|
+
qty: float,
|
|
100
|
+
limit: float | None = None,
|
|
101
|
+
stop: float | None = None,
|
|
102
|
+
sl: float | None = None,
|
|
103
|
+
tp: float | None = None,
|
|
104
|
+
tag: _JsonValue = None,
|
|
105
|
+
expires_at: _Expiry | None = None,
|
|
106
|
+
valid_bars: int | None = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Explicit asset-quantity intent."""
|
|
109
|
+
|
|
110
|
+
@overload
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
side: Literal["buy", "sell"],
|
|
115
|
+
amount: float,
|
|
116
|
+
limit: float | None = None,
|
|
117
|
+
stop: float | None = None,
|
|
118
|
+
sl: float | None = None,
|
|
119
|
+
tp: float | None = None,
|
|
120
|
+
tag: _JsonValue = None,
|
|
121
|
+
expires_at: _Expiry | None = None,
|
|
122
|
+
valid_bars: int | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Explicit notional-amount intent, converted at submission."""
|
|
125
|
+
|
|
126
|
+
@overload
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
side: Literal["buy", "sell"],
|
|
131
|
+
percent: float,
|
|
132
|
+
limit: float | None = None,
|
|
133
|
+
stop: float | None = None,
|
|
134
|
+
sl: float | None = None,
|
|
135
|
+
tp: float | None = None,
|
|
136
|
+
tag: _JsonValue = None,
|
|
137
|
+
expires_at: _Expiry | None = None,
|
|
138
|
+
valid_bars: int | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Explicit equity-percentage intent, converted at submission."""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
side: Literal["buy", "sell"],
|
|
146
|
+
qty: float = cast(float, UNSET),
|
|
147
|
+
amount: float = cast(float, UNSET),
|
|
148
|
+
percent: float = cast(float, UNSET),
|
|
149
|
+
limit: float | None = None,
|
|
150
|
+
stop: float | None = None,
|
|
151
|
+
sl: float | None = None,
|
|
152
|
+
tp: float | None = None,
|
|
153
|
+
tag: _JsonValue = None,
|
|
154
|
+
expires_at: _Expiry | None = None,
|
|
155
|
+
valid_bars: int | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
if side not in ("buy", "sell"):
|
|
158
|
+
raise ValueError("side must be buy/sell")
|
|
159
|
+
size = select_size(qty=qty, amount=amount, percent=percent)
|
|
160
|
+
values = _normalize_conditions(
|
|
161
|
+
limit=limit,
|
|
162
|
+
stop=stop,
|
|
163
|
+
sl=sl,
|
|
164
|
+
tp=tp,
|
|
165
|
+
tag=tag,
|
|
166
|
+
expires_at=expires_at,
|
|
167
|
+
valid_bars=valid_bars,
|
|
168
|
+
)
|
|
169
|
+
values.pop("reduce_only")
|
|
170
|
+
values["_tag_json"] = values.pop("tag_json")
|
|
171
|
+
for name, value in (values | {"side": side, "_size": size}).items():
|
|
172
|
+
object.__setattr__(self, name, value)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def qty(self) -> float | None:
|
|
176
|
+
"""Explicit asset quantity; None in other modes."""
|
|
177
|
+
return self._size.value if self._size.mode == "qty" else None
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def amount(self) -> float | None:
|
|
181
|
+
"""Explicit notional amount; converted at submission, None in other modes."""
|
|
182
|
+
return self._size.value if self._size.mode == "amount" else None
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def percent(self) -> float | None:
|
|
186
|
+
"""Explicit equity percentage (20 means 20%); None in other modes."""
|
|
187
|
+
return self._size.value if self._size.mode == "percent" else None
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def tag(self) -> _JsonValue:
|
|
191
|
+
"""Independent JSON tag copy; modifying it does not change the request."""
|
|
192
|
+
return json.loads(self._tag_json)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Pure order-sizing resolution: intents are not bound to a run, and budgets use only explicit submission context."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from openbacktesting.broker.instrument import InstrumentSpec
|
|
8
|
+
from openbacktesting.broker.models import AccountSnapshot, OrderSizingSnapshot, finite
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _Unset(Enum):
|
|
12
|
+
VALUE = "omitted"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
UNSET = _Unset.VALUE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class _SizeIntent:
|
|
20
|
+
mode: Literal["qty", "amount", "percent"]
|
|
21
|
+
value: float
|
|
22
|
+
source: Literal["explicit", "default"] = "explicit"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def select_size(*, qty=UNSET, amount=UNSET, percent=UNSET, default=None, reduce_only=False):
|
|
26
|
+
"""Validate sizing selection; None is explicitly invalid, and defaults apply only when all choices are omitted."""
|
|
27
|
+
if type(reduce_only) is not bool:
|
|
28
|
+
raise TypeError("reduce_only must be bool")
|
|
29
|
+
supplied = [
|
|
30
|
+
(mode, value)
|
|
31
|
+
for mode, value in (("qty", qty), ("amount", amount), ("percent", percent))
|
|
32
|
+
if value is not UNSET
|
|
33
|
+
]
|
|
34
|
+
if len(supplied) > 1:
|
|
35
|
+
raise TypeError("qty, amount, and percent are mutually exclusive")
|
|
36
|
+
if reduce_only and (not supplied or supplied[0][0] != "qty"):
|
|
37
|
+
raise TypeError("Reduce-only orders require an explicit qty")
|
|
38
|
+
if not supplied:
|
|
39
|
+
if default is None:
|
|
40
|
+
raise TypeError("Supply one of qty, amount, or percent, or configure a default quantity")
|
|
41
|
+
return default
|
|
42
|
+
mode, value = supplied[0]
|
|
43
|
+
value = finite(value, mode, positive=True)
|
|
44
|
+
if mode == "percent" and value > 100:
|
|
45
|
+
raise ValueError("percent must be in (0,100]; 20 means 20%")
|
|
46
|
+
return _SizeIntent(mode, value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_size(
|
|
50
|
+
intent: _SizeIntent,
|
|
51
|
+
*,
|
|
52
|
+
account: AccountSnapshot,
|
|
53
|
+
reference_price: float,
|
|
54
|
+
instrument: InstrumentSpec,
|
|
55
|
+
reduce_only: bool = False,
|
|
56
|
+
) -> OrderSizingSnapshot:
|
|
57
|
+
"""Convert using the requested Close; validate explicit quantity strictly and round budgets toward zero without reserving fees."""
|
|
58
|
+
price = finite(reference_price, "reference_price", positive=True)
|
|
59
|
+
equity = finite(account.equity, "equity", positive=intent.mode == "percent")
|
|
60
|
+
budget = None
|
|
61
|
+
raw_qty = intent.value
|
|
62
|
+
if intent.mode != "qty":
|
|
63
|
+
budget = finite(
|
|
64
|
+
equity * (intent.value / 100) if intent.mode == "percent" else intent.value,
|
|
65
|
+
"budget",
|
|
66
|
+
positive=True,
|
|
67
|
+
)
|
|
68
|
+
raw_qty = finite(budget / price, "Budgeted quantity", positive=True)
|
|
69
|
+
floor = intent.mode != "qty" or intent.source == "default"
|
|
70
|
+
qty = instrument.quantity(raw_qty, floor=floor, opening=not reduce_only)
|
|
71
|
+
if floor:
|
|
72
|
+
if qty == 0:
|
|
73
|
+
raise ValueError("Quantity budget failed: below_min_quantity")
|
|
74
|
+
if finite(qty * price, "Budgeted notional amount") < instrument.min_notional:
|
|
75
|
+
raise ValueError("Quantity budget failed: below_min_notional")
|
|
76
|
+
return OrderSizingSnapshot(intent.mode, intent.source, intent.value, equity, price, budget, qty)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Immutable RGBA color values and fixed presets independent of renderers and runtime context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re as _re
|
|
6
|
+
from dataclasses import dataclass as _dataclass
|
|
7
|
+
from math import floor as _floor
|
|
8
|
+
from math import isfinite as _isfinite
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _alpha(transparency):
|
|
12
|
+
if type(transparency) not in (int, float):
|
|
13
|
+
raise TypeError("Transparency must be a Python int or float; bool is not accepted")
|
|
14
|
+
if not 0 <= transparency <= 100:
|
|
15
|
+
raise ValueError("Transparency must be a finite number from 0 to 100")
|
|
16
|
+
return _floor(255 * (100 - transparency) / 100 + 0.5)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@_dataclass(frozen=True, slots=True)
|
|
20
|
+
class Color:
|
|
21
|
+
"""Immutable color compared by all four channels; alpha means opacity, with no implicit equality to strings.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
red: Native Python int red component from 0 to 255.
|
|
25
|
+
green: Native Python int green component from 0 to 255.
|
|
26
|
+
blue: Native Python int blue component from 0 to 255.
|
|
27
|
+
alpha: Native Python int from 0 to 255; 0 is fully transparent, 255 is opaque.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
TypeError: A channel is not a native int, including bool.
|
|
31
|
+
ValueError: A channel is outside its range.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
red: int
|
|
35
|
+
green: int
|
|
36
|
+
blue: int
|
|
37
|
+
alpha: int = 255
|
|
38
|
+
|
|
39
|
+
def __post_init__(self):
|
|
40
|
+
for value in (self.red, self.green, self.blue, self.alpha):
|
|
41
|
+
if type(value) is not int:
|
|
42
|
+
raise TypeError("Color channels must be Python int values; bool is not accepted")
|
|
43
|
+
if not 0 <= value <= 255:
|
|
44
|
+
raise ValueError("Color channels must be in 0–255")
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_hex(cls, value: str) -> Color:
|
|
48
|
+
"""Parse six- or eight-digit hexadecimal colors; append FF for six digits.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
value: #RRGGBB or #RRGGBBAA without whitespace, in either case.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The corresponding color value.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
TypeError: Input is not a native string.
|
|
58
|
+
ValueError: Invalid string format; CSS names and shorthand are not accepted.
|
|
59
|
+
"""
|
|
60
|
+
if type(value) is not str:
|
|
61
|
+
raise TypeError("Color must be a hexadecimal string")
|
|
62
|
+
if _re.fullmatch(r"#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?", value) is None:
|
|
63
|
+
raise ValueError("Color must be #RRGGBB or #RRGGBBAA")
|
|
64
|
+
rgba = value[1:] + ("FF" if len(value) == 7 else "")
|
|
65
|
+
return cls(*(int(rgba[i : i + 2], 16) for i in (0, 2, 4, 6)))
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def rgb(cls, red: int, green: int, blue: int, *, transparency: float = 0) -> Color:
|
|
69
|
+
"""Construct from integer RGB and 0–100 transparency; 0 is opaque, 100 is fully transparent.
|
|
70
|
+
|
|
71
|
+
Transparency accepts only native finite int/float values; invalid types raise TypeError, out-of-range values raise ValueError.
|
|
72
|
+
Quantize alpha to the nearest byte, rounding half values upward without first rounding the percentage.
|
|
73
|
+
"""
|
|
74
|
+
return cls(red, green, blue, _alpha(transparency))
|
|
75
|
+
|
|
76
|
+
def with_transparency(self, transparency: float) -> Color:
|
|
77
|
+
"""Replace alpha using 0–100 transparency and return a new value; do not modify or multiply the original color.
|
|
78
|
+
|
|
79
|
+
Transparency uses the same range, quantization, and exception contract as rgb.
|
|
80
|
+
"""
|
|
81
|
+
return Color(self.red, self.green, self.blue, _alpha(transparency))
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_gradient(
|
|
85
|
+
cls,
|
|
86
|
+
value: float,
|
|
87
|
+
*,
|
|
88
|
+
lower: float,
|
|
89
|
+
upper: float,
|
|
90
|
+
lower_color: Color,
|
|
91
|
+
upper_color: Color,
|
|
92
|
+
) -> Color:
|
|
93
|
+
"""Linearly interpolate a scalar between unpremultiplied RGBA bytes at both endpoints; clamp outside the interval.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
value: Finite native int/float; bool is not accepted.
|
|
97
|
+
lower: Finite lower bound, strictly below upper.
|
|
98
|
+
upper: Finite upper bound.
|
|
99
|
+
lower_color: Lower-bound Color; None and strings are not accepted.
|
|
100
|
+
upper_color: Upper-bound Color.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Immutable Color, with each channel rounding half-byte values upward; neither endpoint is modified.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TypeError: Invalid numeric or color types.
|
|
107
|
+
ValueError: Nonfinite values or invalid bound ordering.
|
|
108
|
+
"""
|
|
109
|
+
for number in (value, lower, upper):
|
|
110
|
+
if type(number) not in (int, float):
|
|
111
|
+
raise TypeError("Gradient values and bounds must be Python int or float values")
|
|
112
|
+
try:
|
|
113
|
+
valid = _isfinite(number)
|
|
114
|
+
except OverflowError:
|
|
115
|
+
valid = False
|
|
116
|
+
if not valid:
|
|
117
|
+
raise ValueError("Gradient values and bounds must be finite numbers")
|
|
118
|
+
if lower >= upper:
|
|
119
|
+
raise ValueError("Gradient lower must be less than upper")
|
|
120
|
+
if not isinstance(lower_color, Color) or not isinstance(upper_color, Color):
|
|
121
|
+
raise TypeError("Gradient endpoints must be Color instances")
|
|
122
|
+
if value <= lower:
|
|
123
|
+
return lower_color
|
|
124
|
+
if value >= upper:
|
|
125
|
+
return upper_color
|
|
126
|
+
# upper-lower can overflow for extreme opposite signs; use the original difference for equal signs to avoid losing precision between nearby values.
|
|
127
|
+
if lower < 0 < upper:
|
|
128
|
+
scale = max(-lower, upper)
|
|
129
|
+
ratio = (value / scale - lower / scale) / (upper / scale - lower / scale)
|
|
130
|
+
else:
|
|
131
|
+
ratio = (value - lower) / (upper - lower)
|
|
132
|
+
return cls(
|
|
133
|
+
*(
|
|
134
|
+
_floor(a + (b - a) * ratio + 0.5)
|
|
135
|
+
for a, b in zip(
|
|
136
|
+
(lower_color.red, lower_color.green, lower_color.blue, lower_color.alpha),
|
|
137
|
+
(upper_color.red, upper_color.green, upper_color.blue, upper_color.alpha),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def transparency(self) -> float:
|
|
144
|
+
"""0–100 transparency derived from quantized alpha; it may differ from the original percentage."""
|
|
145
|
+
return 100 * (255 - self.alpha) / 255
|
|
146
|
+
|
|
147
|
+
def to_hex(self) -> str:
|
|
148
|
+
"""Return uppercase #RRGGBBAA; fully transparent colors retain RGB and do not become None."""
|
|
149
|
+
return f"#{self.red:02X}{self.green:02X}{self.blue:02X}{self.alpha:02X}"
|
|
150
|
+
|
|
151
|
+
def __str__(self) -> str:
|
|
152
|
+
return self.to_hex()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _as_color(value):
|
|
156
|
+
return value if isinstance(value, Color) else Color.from_hex(value)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
AQUA = Color.from_hex("#00BCD4")
|
|
160
|
+
BLACK = Color.from_hex("#363A45")
|
|
161
|
+
BLUE = Color.from_hex("#2196F3")
|
|
162
|
+
FUCHSIA = Color.from_hex("#E040FB")
|
|
163
|
+
GRAY = Color.from_hex("#787B86")
|
|
164
|
+
GREEN = Color.from_hex("#4CAF50")
|
|
165
|
+
LIME = Color.from_hex("#00E676")
|
|
166
|
+
MAROON = Color.from_hex("#880E4F")
|
|
167
|
+
NAVY = Color.from_hex("#311B92")
|
|
168
|
+
OLIVE = Color.from_hex("#808000")
|
|
169
|
+
ORANGE = Color.from_hex("#FF9800")
|
|
170
|
+
PURPLE = Color.from_hex("#9C27B0")
|
|
171
|
+
RED = Color.from_hex("#F23645")
|
|
172
|
+
SILVER = Color.from_hex("#B2B5BE")
|
|
173
|
+
TEAL = Color.from_hex("#089981")
|
|
174
|
+
WHITE = Color.from_hex("#FFFFFF")
|
|
175
|
+
YELLOW = Color.from_hex("#FDD835")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Compatibility exports for the old data path; implemented in data.core."""
|
|
2
|
+
|
|
3
|
+
from openbacktesting.data.core import (
|
|
4
|
+
_Array as _Array,
|
|
5
|
+
)
|
|
6
|
+
from openbacktesting.data.core import (
|
|
7
|
+
_Data as _Data,
|
|
8
|
+
)
|
|
9
|
+
from openbacktesting.data.core import (
|
|
10
|
+
_data_period as _data_period,
|
|
11
|
+
)
|
|
12
|
+
from openbacktesting.data.core import (
|
|
13
|
+
_Indicator as _Indicator,
|
|
14
|
+
)
|
|
15
|
+
from openbacktesting.data.core import (
|
|
16
|
+
_indicator_warmup_nbars as _indicator_warmup_nbars,
|
|
17
|
+
)
|
|
18
|
+
from openbacktesting.data.core import (
|
|
19
|
+
_strategy_indicators as _strategy_indicators,
|
|
20
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Native resampling rules for market data and trades."""
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
|
|
5
|
+
OHLCV_AGG = OrderedDict(
|
|
6
|
+
(
|
|
7
|
+
("Open", "first"),
|
|
8
|
+
("High", "max"),
|
|
9
|
+
("Low", "min"),
|
|
10
|
+
("Close", "last"),
|
|
11
|
+
("Volume", "sum"),
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
"""Dictionary of rules for aggregating resampled OHLCV data frames,
|
|
15
|
+
e.g.
|
|
16
|
+
|
|
17
|
+
df.resample('4H', label='right').agg(OHLCV_AGG).dropna()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
TRADES_AGG = OrderedDict(
|
|
21
|
+
(
|
|
22
|
+
("Size", "sum"),
|
|
23
|
+
("EntryBar", "first"),
|
|
24
|
+
("ExitBar", "last"),
|
|
25
|
+
("EntryPrice", "mean"),
|
|
26
|
+
("ExitPrice", "mean"),
|
|
27
|
+
("PnL", "sum"),
|
|
28
|
+
("ReturnPct", "mean"),
|
|
29
|
+
("EntryTime", "first"),
|
|
30
|
+
("ExitTime", "last"),
|
|
31
|
+
("Duration", "sum"),
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
"""Dictionary of rules for aggregating resampled trades data,
|
|
35
|
+
e.g.
|
|
36
|
+
|
|
37
|
+
stats['_trades'].resample('1D', on='ExitTime',
|
|
38
|
+
label='right').agg(TRADES_AGG)
|
|
39
|
+
"""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Host-provided chart context; instrument specifications are bound separately."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
|
|
6
|
+
from openbacktesting.data.timeframe import Timeframe
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class DataContext:
|
|
11
|
+
"""Describe the existing market-data timeframe and shared timezone for charts, strategy calendars, and statistics.
|
|
12
|
+
|
|
13
|
+
Timezone-aware market data is converted to this timezone before strategy construction; absolute instants and original data remain unchanged.
|
|
14
|
+
Statistics group by local day boundaries; changing timezone requires a new run, and annualized metrics may differ.
|
|
15
|
+
Provides no downloading, resampling, or exchange-calendar capability.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
timeframe: Timeframe
|
|
19
|
+
timezone: str
|
|
20
|
+
|
|
21
|
+
def __post_init__(self):
|
|
22
|
+
if not isinstance(self.timeframe, Timeframe):
|
|
23
|
+
raise TypeError("timeframe must be a Timeframe")
|
|
24
|
+
if not isinstance(self.timezone, str) or not self.timezone:
|
|
25
|
+
raise TypeError("timezone must be a nonempty IANA timezone string")
|
|
26
|
+
ZoneInfo(self.timezone)
|
|
27
|
+
|
|
28
|
+
def snapshot(self) -> dict:
|
|
29
|
+
"""Return independent plain values without reusing mutable request objects."""
|
|
30
|
+
return {"timeframe": self.timeframe.snapshot(), "timezone": self.timezone}
|