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,460 @@
|
|
|
1
|
+
"""Typed strategy parameters: declaration, discovery, normalization, and title-based resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins as _b
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass, field, replace
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from functools import partial
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from openbacktesting.colors import Color, _as_color
|
|
18
|
+
|
|
19
|
+
SCHEMA_VERSION = "1.0.0"
|
|
20
|
+
_SAFE_INT = 2**53 - 1
|
|
21
|
+
_UNSET = object()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InputError(ValueError):
|
|
25
|
+
"""Parameter error that callers can display using code/title/expected/actual_type."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, code, title, expected, value=None):
|
|
28
|
+
self.code = code
|
|
29
|
+
self.title = title
|
|
30
|
+
self.expected = expected
|
|
31
|
+
self.actual_type = type(value).__name__
|
|
32
|
+
super().__init__(f"{code}: {title!r}: {expected} (got {self.actual_type})")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fail(code, title, expected, value=None):
|
|
36
|
+
raise InputError(code, title, expected, value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _scalar(value, title):
|
|
40
|
+
if isinstance(value, (np.bool_, np.integer, np.floating)):
|
|
41
|
+
native = value.item()
|
|
42
|
+
# longdouble.item() does not guarantee a Python float; lossy conversion is prohibited.
|
|
43
|
+
if type(native) not in (_b.bool, _b.int, _b.float):
|
|
44
|
+
_fail("InvalidInputValue", title, "Lossless Python scalar", value)
|
|
45
|
+
return native
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _session(value, title):
|
|
50
|
+
if value == "24x7":
|
|
51
|
+
return "0000-0000:1234567"
|
|
52
|
+
match = re.fullmatch(r"([0-9]{4}-[0-9]{4}(?:,[0-9]{4}-[0-9]{4})*)(?::([1-7]+))?", value)
|
|
53
|
+
if not match:
|
|
54
|
+
_fail("InvalidInputValue", title, "HHMM-HHMM[,HHMM-HHMM...][:days]", value)
|
|
55
|
+
periods, days = match.groups()
|
|
56
|
+
days = days or "1234567"
|
|
57
|
+
if len(set(days)) != len(days):
|
|
58
|
+
_fail("InvalidInputValue", title, "Weekdays must not repeat", value)
|
|
59
|
+
for clock in re.split("[-,]", periods):
|
|
60
|
+
if _b.int(clock[:2]) > 23 or _b.int(clock[2:]) > 59:
|
|
61
|
+
_fail("InvalidInputValue", title, "Time range 0000–2359", value)
|
|
62
|
+
return periods + ":" + "".join(sorted(days))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, eq=False)
|
|
66
|
+
class InputSpec:
|
|
67
|
+
"""Immutable strategy class-field declaration created by inputs factories; instance reads return this run's value."""
|
|
68
|
+
|
|
69
|
+
kind: str
|
|
70
|
+
title: str
|
|
71
|
+
default: object
|
|
72
|
+
options: tuple | None = None
|
|
73
|
+
bounds: tuple = ()
|
|
74
|
+
metadata: tuple = ()
|
|
75
|
+
enum_type: type[Enum] | None = None
|
|
76
|
+
bindings: tuple = field(default=(), repr=False)
|
|
77
|
+
|
|
78
|
+
def __set_name__(self, owner, name):
|
|
79
|
+
object.__setattr__(self, "bindings", (*self.bindings, name))
|
|
80
|
+
|
|
81
|
+
def __get__(self, instance, owner=None):
|
|
82
|
+
if instance is None:
|
|
83
|
+
return self
|
|
84
|
+
return dict(instance._resolved_inputs.runtime)[self.title]
|
|
85
|
+
|
|
86
|
+
def __set__(self, instance, value):
|
|
87
|
+
_fail("ImmutableInput", self.title, "input is read-only within this run", value)
|
|
88
|
+
|
|
89
|
+
def __delete__(self, instance):
|
|
90
|
+
_fail("ImmutableInput", self.title, "input cannot be deleted")
|
|
91
|
+
|
|
92
|
+
def normalize(self, value, *, wire=False):
|
|
93
|
+
"""Validate and normalize value; wire=True parses enums from their serialized form, and invalid values raise InputError."""
|
|
94
|
+
value = _scalar(value, self.title)
|
|
95
|
+
kind = self.kind
|
|
96
|
+
if kind == "color":
|
|
97
|
+
try:
|
|
98
|
+
return Color.from_hex(value) if wire else _as_color(value)
|
|
99
|
+
except (TypeError, ValueError) as error:
|
|
100
|
+
raise InputError(
|
|
101
|
+
"InvalidInputValue",
|
|
102
|
+
self.title,
|
|
103
|
+
"Color or #RRGGBB/#RRGGBBAA (wire accepts strings only)",
|
|
104
|
+
value,
|
|
105
|
+
) from error
|
|
106
|
+
if kind == "enum":
|
|
107
|
+
if wire:
|
|
108
|
+
if type(value) is not str or value not in self.enum_type.__members__:
|
|
109
|
+
_fail("InvalidInputValue", self.title, "Enum member name", value)
|
|
110
|
+
value = self.enum_type[value]
|
|
111
|
+
valid = type(value) is self.enum_type
|
|
112
|
+
elif kind == "bool":
|
|
113
|
+
valid = type(value) is _b.bool
|
|
114
|
+
elif kind in ("int", "time"):
|
|
115
|
+
valid = type(value) is _b.int and abs(value) <= _SAFE_INT
|
|
116
|
+
elif kind in ("float", "price"):
|
|
117
|
+
valid = type(value) in (_b.int, _b.float)
|
|
118
|
+
if valid:
|
|
119
|
+
try:
|
|
120
|
+
converted = _b.float(value)
|
|
121
|
+
valid = math.isfinite(converted) and converted == value
|
|
122
|
+
value = converted if converted else 0.0
|
|
123
|
+
except OverflowError:
|
|
124
|
+
valid = False
|
|
125
|
+
else:
|
|
126
|
+
valid = type(value) is str
|
|
127
|
+
if not valid:
|
|
128
|
+
_fail("InvalidInputValue", self.title, f"valid {kind} value", value)
|
|
129
|
+
if kind == "session":
|
|
130
|
+
value = _session(value, self.title)
|
|
131
|
+
if self.options is not None and value not in self.options:
|
|
132
|
+
_fail("InvalidInputValue", self.title, "Value must belong to options", value)
|
|
133
|
+
for key, boundary in self.bounds:
|
|
134
|
+
if (key == "minval" and value < boundary) or (key == "maxval" and value > boundary):
|
|
135
|
+
_fail("InvalidInputValue", self.title, f"satisfying {key}={boundary}", value)
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
def encode(self, value):
|
|
139
|
+
"""Encode normalized value into configuration form; enums use member names, and colors use uppercase eight-digit strings."""
|
|
140
|
+
if self.kind == "color":
|
|
141
|
+
return value.to_hex()
|
|
142
|
+
return value.name if self.kind == "enum" else value
|
|
143
|
+
|
|
144
|
+
def export(self, attribute):
|
|
145
|
+
"""Export the declaration dictionary; attribute is the Python field name, while configuration associations use unique title."""
|
|
146
|
+
result = {
|
|
147
|
+
"title": self.title,
|
|
148
|
+
"attribute": attribute,
|
|
149
|
+
"kind": self.kind,
|
|
150
|
+
"default": self.encode(self.default),
|
|
151
|
+
**dict(self.bounds),
|
|
152
|
+
**dict(self.metadata),
|
|
153
|
+
}
|
|
154
|
+
if type(result["active"]) is str:
|
|
155
|
+
result["active"] = {"title": result["active"]}
|
|
156
|
+
if self.options is not None:
|
|
157
|
+
result["options"] = [self.encode(v) for v in self.options]
|
|
158
|
+
if self.enum_type is not None:
|
|
159
|
+
result["enumTypeId"] = f"{self.enum_type.__module__}.{self.enum_type.__qualname__}"
|
|
160
|
+
result["members"] = [
|
|
161
|
+
{"name": member.name, "title": member.name} for member in self.enum_type
|
|
162
|
+
]
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _make(
|
|
167
|
+
kind,
|
|
168
|
+
defval,
|
|
169
|
+
*,
|
|
170
|
+
title,
|
|
171
|
+
tooltip="",
|
|
172
|
+
group="",
|
|
173
|
+
inline="",
|
|
174
|
+
display=None,
|
|
175
|
+
active=True,
|
|
176
|
+
confirm=False,
|
|
177
|
+
options=_UNSET,
|
|
178
|
+
minval=_UNSET,
|
|
179
|
+
maxval=_UNSET,
|
|
180
|
+
step=_UNSET,
|
|
181
|
+
):
|
|
182
|
+
"""Declare read-only strategy configuration fields; inputs.bool/int/float and other entry points bind the kind.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
defval: Default value matching the declared kind.
|
|
186
|
+
title: Required and unique within one strategy; also the title-based configuration key.
|
|
187
|
+
tooltip: Hint text.
|
|
188
|
+
group: Display group, without changing title uniqueness scope.
|
|
189
|
+
inline: Hint for display on the same row.
|
|
190
|
+
display: all/none/status_line/data_window; metadata only.
|
|
191
|
+
active: Boolean constant or mapping referencing a bool parameter title; does not bypass value validation.
|
|
192
|
+
confirm: Confirmation-hint metadata, without opening a UI.
|
|
193
|
+
options: Allowed-value set; numeric kinds cannot also specify bounds or step.
|
|
194
|
+
minval: Numeric lower bound for int/float only.
|
|
195
|
+
maxval: Numeric upper bound for int/float only.
|
|
196
|
+
step: Adjustment increment, not a divisibility constraint; int/float only.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
InputSpec: Class-field descriptor; instance reads return this run's resolved value.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
InputError: Invalid declaration, default, parameter domain, or display metadata.
|
|
203
|
+
"""
|
|
204
|
+
if type(title) is not str or not title.strip():
|
|
205
|
+
_fail("InvalidDeclaration", title, "title is required and must not be empty", title)
|
|
206
|
+
if any(type(v) is not str for v in (tooltip, group, inline)):
|
|
207
|
+
_fail("InvalidDeclaration", title, "tooltip/group/inline must be strings")
|
|
208
|
+
display = display if display is not None else ("none" if kind in ("bool", "color") else "all")
|
|
209
|
+
if display not in ("all", "none", "status_line", "data_window") or type(confirm) is not _b.bool:
|
|
210
|
+
_fail("InvalidDeclaration", title, "Valid display and boolean confirm")
|
|
211
|
+
if isinstance(active, Mapping) and set(active) == {"title"} and type(active["title"]) is str:
|
|
212
|
+
active = active["title"]
|
|
213
|
+
elif type(active) is not _b.bool:
|
|
214
|
+
_fail("InvalidDeclaration", title, "active must be bool or a title reference", active)
|
|
215
|
+
metadata = tuple(
|
|
216
|
+
{
|
|
217
|
+
"tooltip": tooltip,
|
|
218
|
+
"group": group,
|
|
219
|
+
"inline": inline,
|
|
220
|
+
"display": display,
|
|
221
|
+
"active": active,
|
|
222
|
+
"confirm": confirm,
|
|
223
|
+
}.items()
|
|
224
|
+
)
|
|
225
|
+
enum_type = None
|
|
226
|
+
if kind == "enum":
|
|
227
|
+
if not isinstance(defval, Enum):
|
|
228
|
+
_fail("InvalidDeclaration", title, "Default value must be an Enum member", defval)
|
|
229
|
+
enum_type = type(defval)
|
|
230
|
+
if len(enum_type.__members__) != len(enum_type):
|
|
231
|
+
_fail("InvalidDeclaration", title, "Enums must not contain aliases")
|
|
232
|
+
spec = InputSpec(kind, title, defval, metadata=metadata, enum_type=enum_type)
|
|
233
|
+
bounds = {
|
|
234
|
+
k: v for k, v in (("minval", minval), ("maxval", maxval), ("step", step)) if v is not _UNSET
|
|
235
|
+
}
|
|
236
|
+
if (bounds and kind not in ("int", "float")) or (bounds and options is not _UNSET):
|
|
237
|
+
_fail("InvalidDeclaration", title, "Numeric bounds apply only to int/float and are mutually exclusive with options")
|
|
238
|
+
try:
|
|
239
|
+
bounds = {key: spec.normalize(value) for key, value in bounds.items()}
|
|
240
|
+
if "minval" in bounds and "maxval" in bounds and bounds["minval"] > bounds["maxval"]:
|
|
241
|
+
_fail("InvalidDeclaration", title, "minval must not exceed maxval")
|
|
242
|
+
if "step" in bounds and bounds["step"] <= 0:
|
|
243
|
+
_fail("InvalidDeclaration", title, "step must be greater than zero")
|
|
244
|
+
normalized_options = None
|
|
245
|
+
if options is not _UNSET:
|
|
246
|
+
if (
|
|
247
|
+
kind not in ("int", "float", "string", "session", "enum")
|
|
248
|
+
or not isinstance(options, (list, tuple))
|
|
249
|
+
or not options
|
|
250
|
+
):
|
|
251
|
+
_fail("InvalidDeclaration", title, "This type requires a nonempty options list", options)
|
|
252
|
+
normalized_options = tuple(spec.normalize(v) for v in options)
|
|
253
|
+
if len(set(normalized_options)) != len(normalized_options):
|
|
254
|
+
_fail("InvalidDeclaration", title, "options must not repeat")
|
|
255
|
+
spec = replace(spec, bounds=tuple(bounds.items()), options=normalized_options)
|
|
256
|
+
return replace(spec, default=spec.normalize(defval))
|
|
257
|
+
except InputError as error:
|
|
258
|
+
raise InputError("InvalidDeclaration", title, error.expected, defval) from error
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
bool = partial(_make, "bool")
|
|
262
|
+
color = partial(_make, "color")
|
|
263
|
+
enum = partial(_make, "enum")
|
|
264
|
+
float = partial(_make, "float")
|
|
265
|
+
int = partial(_make, "int")
|
|
266
|
+
price = partial(_make, "price")
|
|
267
|
+
session = partial(_make, "session")
|
|
268
|
+
string = partial(_make, "string")
|
|
269
|
+
time = partial(_make, "time")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _collect(cls):
|
|
273
|
+
fields = {}
|
|
274
|
+
conflicts = set()
|
|
275
|
+
for base in cls.__bases__:
|
|
276
|
+
for name, spec in _collect(base).items():
|
|
277
|
+
if name in fields and fields[name] is not spec:
|
|
278
|
+
conflicts.add(name)
|
|
279
|
+
else:
|
|
280
|
+
fields[name] = spec
|
|
281
|
+
local = vars(cls)
|
|
282
|
+
for name, value in local.items():
|
|
283
|
+
if isinstance(value, InputSpec):
|
|
284
|
+
fields[name] = value
|
|
285
|
+
conflicts.discard(name)
|
|
286
|
+
elif name in fields:
|
|
287
|
+
_fail("InvalidDeclaration", fields[name].title, f"Ordinary attribute shadows input: {name}")
|
|
288
|
+
if conflicts:
|
|
289
|
+
_fail("InvalidDeclaration", None, f"Multiple-inheritance field conflict: {sorted(conflicts)}")
|
|
290
|
+
return fields
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _definitions(cls):
|
|
294
|
+
if not isinstance(cls, type):
|
|
295
|
+
_fail("InvalidDeclaration", None, "Loaded strategy class", cls)
|
|
296
|
+
bases = [base for base in cls.__mro__ if vars(base).get("_input_strategy_base") is True]
|
|
297
|
+
if not bases:
|
|
298
|
+
_fail("InvalidDeclaration", None, "Must inherit Strategy", cls)
|
|
299
|
+
reserved = set(vars(bases[0]))
|
|
300
|
+
data_names = vars(bases[0]).get("_reserved_data_names", ())
|
|
301
|
+
for base in cls.__mro__:
|
|
302
|
+
if base is not bases[0]:
|
|
303
|
+
conflicts = set(data_names).intersection(vars(base))
|
|
304
|
+
if conflicts:
|
|
305
|
+
name = min(conflicts)
|
|
306
|
+
_fail("InvalidDeclaration", name, f"Reserved field {name}; rename the field with the same name")
|
|
307
|
+
fields = _collect(cls)
|
|
308
|
+
by_title = {}
|
|
309
|
+
for name, spec in fields.items():
|
|
310
|
+
if name.startswith("_") or name in reserved or len(spec.bindings) != 1:
|
|
311
|
+
_fail("InvalidDeclaration", spec.title, f"Reserved field or reused declaration: {name}")
|
|
312
|
+
if spec.title in by_title:
|
|
313
|
+
_fail("InvalidDeclaration", spec.title, "title must be unique within one strategy")
|
|
314
|
+
by_title[spec.title] = spec
|
|
315
|
+
for title in by_title:
|
|
316
|
+
visited = set()
|
|
317
|
+
current = title
|
|
318
|
+
while type(current) is str:
|
|
319
|
+
if current in visited:
|
|
320
|
+
_fail("InvalidDeclaration", title, "active must not form dependency cycles")
|
|
321
|
+
visited.add(current)
|
|
322
|
+
reference = dict(by_title[current].metadata)["active"]
|
|
323
|
+
if type(reference) is str and (
|
|
324
|
+
reference not in by_title or by_title[reference].kind != "bool"
|
|
325
|
+
):
|
|
326
|
+
_fail("InvalidDeclaration", title, "active must reference an existing bool title")
|
|
327
|
+
current = reference
|
|
328
|
+
return fields
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _schema(fields):
|
|
332
|
+
schema = {
|
|
333
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
334
|
+
"inputs": [spec.export(name) for name, spec in fields.items()],
|
|
335
|
+
}
|
|
336
|
+
canonical = json.dumps(
|
|
337
|
+
schema, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
338
|
+
)
|
|
339
|
+
schema["schemaHash"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
340
|
+
return schema
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def describe(strategy_class):
|
|
344
|
+
"""Export an independent JSON definition of the loaded strategy class without running it."""
|
|
345
|
+
return _schema(_definitions(strategy_class))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@dataclass(frozen=True)
|
|
349
|
+
class ResolvedInputs:
|
|
350
|
+
"""Validated input-resolution result containing the declaration digest, runtime values, encoded values, and explicitly overridden titles."""
|
|
351
|
+
|
|
352
|
+
schema_hash: str
|
|
353
|
+
runtime: tuple
|
|
354
|
+
encoded: tuple
|
|
355
|
+
overridden: tuple
|
|
356
|
+
attributes: tuple
|
|
357
|
+
|
|
358
|
+
def snapshot(self):
|
|
359
|
+
"""Return a snapshot of version, declaration digest, complete values, and explicitly overridden titles for result attrs."""
|
|
360
|
+
return {
|
|
361
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
362
|
+
"schemaHash": self.schema_hash,
|
|
363
|
+
"values": dict(self.encoded),
|
|
364
|
+
"overriddenTitles": list(self.overridden),
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
def overrides(self):
|
|
368
|
+
"""Return explicitly overridden Python fields and values for injection into strategy instances."""
|
|
369
|
+
runtime = dict(self.runtime)
|
|
370
|
+
return {
|
|
371
|
+
attribute: runtime[title]
|
|
372
|
+
for title, attribute in self.attributes
|
|
373
|
+
if title in self.overridden
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _resolve(fields, values, *, wire):
|
|
378
|
+
if not isinstance(values, Mapping):
|
|
379
|
+
_fail("InvalidInputValue", None, "Mapping indexed by title", values)
|
|
380
|
+
known = {spec.title for spec in fields.values()}
|
|
381
|
+
for title in values:
|
|
382
|
+
if title not in known:
|
|
383
|
+
_fail("UnknownInput", title, "Titles in the current declaration")
|
|
384
|
+
runtime, encoded, overridden, attributes = [], [], [], []
|
|
385
|
+
for name, spec in fields.items():
|
|
386
|
+
value = spec.default
|
|
387
|
+
if spec.title in values:
|
|
388
|
+
value = spec.normalize(values[spec.title], wire=wire)
|
|
389
|
+
overridden.append(spec.title)
|
|
390
|
+
runtime.append((spec.title, value))
|
|
391
|
+
encoded.append((spec.title, spec.encode(value)))
|
|
392
|
+
attributes.append((spec.title, name))
|
|
393
|
+
return ResolvedInputs(
|
|
394
|
+
_schema(fields)["schemaHash"],
|
|
395
|
+
tuple(runtime),
|
|
396
|
+
tuple(encoded),
|
|
397
|
+
tuple(overridden),
|
|
398
|
+
tuple(attributes),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def resolve_native(strategy_class, kwargs):
|
|
403
|
+
"""Native field entry point; undeclared classes return None to preserve legacy behavior."""
|
|
404
|
+
fields = _definitions(strategy_class)
|
|
405
|
+
if not fields:
|
|
406
|
+
return None
|
|
407
|
+
for name in kwargs:
|
|
408
|
+
if name not in fields:
|
|
409
|
+
_fail("UnknownInput", name, "Declared Python fields")
|
|
410
|
+
return _resolve(
|
|
411
|
+
fields, {fields[name].title: value for name, value in kwargs.items()}, wire=False
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def resolve_request(strategy_class, request):
|
|
416
|
+
"""Validate request version, digest, and title values against strategy_class; failures raise InputError, and success returns ResolvedInputs."""
|
|
417
|
+
fields = _definitions(strategy_class)
|
|
418
|
+
if not fields:
|
|
419
|
+
_fail("InvalidDeclaration", None, "Title-based entry requires at least one input")
|
|
420
|
+
if not isinstance(request, Mapping) or set(request) != {
|
|
421
|
+
"schemaVersion",
|
|
422
|
+
"schemaHash",
|
|
423
|
+
"values",
|
|
424
|
+
}:
|
|
425
|
+
_fail("InvalidInputValue", None, "schemaVersion/schemaHash/values request", request)
|
|
426
|
+
if request["schemaVersion"] != SCHEMA_VERSION:
|
|
427
|
+
_fail("UnsupportedInputVersion", None, SCHEMA_VERSION, request["schemaVersion"])
|
|
428
|
+
if request["schemaHash"] != _schema(fields)["schemaHash"]:
|
|
429
|
+
_fail("SchemaMismatch", None, "Rediscover and verify the current declaration", request["schemaHash"])
|
|
430
|
+
return _resolve(fields, request["values"], wire=True)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def request_for(strategy_class, kwargs):
|
|
434
|
+
"""Convert Python field kwargs for a declared strategy_class into a title-based request; first confirm the class contains Input; invalid values raise InputError."""
|
|
435
|
+
resolved = resolve_native(strategy_class, kwargs)
|
|
436
|
+
snapshot = resolved.snapshot()
|
|
437
|
+
snapshot["values"] = {
|
|
438
|
+
title: value for title, value in resolved.encoded if title in resolved.overridden
|
|
439
|
+
}
|
|
440
|
+
del snapshot["overriddenTitles"]
|
|
441
|
+
return snapshot
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
from openbacktesting.helpers import _set_legacy_module
|
|
445
|
+
|
|
446
|
+
_set_legacy_module(InputError, "openbacktesting.inputs")
|
|
447
|
+
_set_legacy_module(_fail, "openbacktesting.inputs")
|
|
448
|
+
_set_legacy_module(_scalar, "openbacktesting.inputs")
|
|
449
|
+
_set_legacy_module(_session, "openbacktesting.inputs")
|
|
450
|
+
_set_legacy_module(InputSpec, "openbacktesting.inputs")
|
|
451
|
+
_set_legacy_module(_make, "openbacktesting.inputs")
|
|
452
|
+
_set_legacy_module(_collect, "openbacktesting.inputs")
|
|
453
|
+
_set_legacy_module(_definitions, "openbacktesting.inputs")
|
|
454
|
+
_set_legacy_module(_schema, "openbacktesting.inputs")
|
|
455
|
+
_set_legacy_module(describe, "openbacktesting.inputs")
|
|
456
|
+
_set_legacy_module(ResolvedInputs, "openbacktesting.inputs")
|
|
457
|
+
_set_legacy_module(_resolve, "openbacktesting.inputs")
|
|
458
|
+
_set_legacy_module(resolve_native, "openbacktesting.inputs")
|
|
459
|
+
_set_legacy_module(resolve_request, "openbacktesting.inputs")
|
|
460
|
+
_set_legacy_module(request_for, "openbacktesting.inputs")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Pure position budgets: quantity, net-notional fraction, and planned stop-loss risk."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from openbacktesting.broker.instrument import InstrumentSpec
|
|
6
|
+
from openbacktesting.broker.models import AccountSnapshot, finite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class SizingDecision:
|
|
11
|
+
"""Budget result; estimated_cost estimates costs for the given quantity without guaranteeing fill prices."""
|
|
12
|
+
|
|
13
|
+
qty: float
|
|
14
|
+
reference_price: float
|
|
15
|
+
estimated_cost: float
|
|
16
|
+
reason: str | None
|
|
17
|
+
budget: float | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _decision(qty, price, instrument, cost_per_unit, budget=None):
|
|
21
|
+
price = finite(price, "reference_price", positive=True)
|
|
22
|
+
cost = finite(cost_per_unit, "cost_per_unit")
|
|
23
|
+
if cost < 0:
|
|
24
|
+
raise ValueError("Cost budget must not be negative")
|
|
25
|
+
qty = instrument.quantity(qty, floor=True)
|
|
26
|
+
reason = None
|
|
27
|
+
if qty == 0:
|
|
28
|
+
reason = "below_min_quantity"
|
|
29
|
+
elif qty * price < instrument.min_notional:
|
|
30
|
+
qty, reason = 0.0, "below_min_notional"
|
|
31
|
+
return SizingDecision(qty, price, qty * cost, reason, budget)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fixed_quantity(qty: float, *, reference_price, instrument=None, cost_per_unit=0):
|
|
35
|
+
"""Round fixed asset quantities toward zero without inferring capital fractions."""
|
|
36
|
+
return _decision(qty, reference_price, instrument or InstrumentSpec(), cost_per_unit)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def equity_fraction(
|
|
40
|
+
fraction, *, account: AccountSnapshot, reference_price, instrument=None, cost_per_unit=0
|
|
41
|
+
):
|
|
42
|
+
"""Equity net-notional budget, with quantity costs charged to the same budget; margin fraction does not amplify it."""
|
|
43
|
+
equity = finite(account.equity, "equity", positive=True)
|
|
44
|
+
fraction = finite(fraction, "fraction", positive=True)
|
|
45
|
+
price = finite(reference_price, "reference_price", positive=True)
|
|
46
|
+
cost = finite(cost_per_unit, "cost_per_unit")
|
|
47
|
+
if cost < 0:
|
|
48
|
+
raise ValueError("Cost budget must not be negative")
|
|
49
|
+
budget = equity * fraction
|
|
50
|
+
return _decision(budget / (price + cost), price, instrument or InstrumentSpec(), cost, budget)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def stop_risk(
|
|
54
|
+
fraction,
|
|
55
|
+
*,
|
|
56
|
+
account: AccountSnapshot,
|
|
57
|
+
reference_price,
|
|
58
|
+
stop_price,
|
|
59
|
+
side="buy",
|
|
60
|
+
instrument=None,
|
|
61
|
+
cost_per_unit=0,
|
|
62
|
+
):
|
|
63
|
+
"""Stop-loss risk budgets include estimated round-trip costs per unit; gaps may exceed the planned loss."""
|
|
64
|
+
equity = finite(account.equity, "equity", positive=True)
|
|
65
|
+
fraction = finite(fraction, "fraction", positive=True)
|
|
66
|
+
price = finite(reference_price, "reference_price", positive=True)
|
|
67
|
+
stop = finite(stop_price, "stop_price", positive=True)
|
|
68
|
+
if (
|
|
69
|
+
side not in ("buy", "sell")
|
|
70
|
+
or (side == "buy" and stop >= price)
|
|
71
|
+
or (side == "sell" and stop <= price)
|
|
72
|
+
):
|
|
73
|
+
raise ValueError("Invalid stop-loss direction or distance")
|
|
74
|
+
cost = finite(cost_per_unit, "cost_per_unit")
|
|
75
|
+
if cost < 0:
|
|
76
|
+
raise ValueError("Cost budget must not be negative")
|
|
77
|
+
budget = equity * fraction
|
|
78
|
+
return _decision(
|
|
79
|
+
budget / (abs(price - stop) + cost), price, instrument or InstrumentSpec(), cost, budget
|
|
80
|
+
)
|