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.
Files changed (350) hide show
  1. openbacktesting/__init__.py +85 -0
  2. openbacktesting/aggregation.py +8 -0
  3. openbacktesting/broker/__init__.py +54 -0
  4. openbacktesting/broker/commission.py +57 -0
  5. openbacktesting/broker/core.py +1122 -0
  6. openbacktesting/broker/execution.py +62 -0
  7. openbacktesting/broker/instrument.py +106 -0
  8. openbacktesting/broker/ledger.py +60 -0
  9. openbacktesting/broker/models.py +236 -0
  10. openbacktesting/broker/observations.py +99 -0
  11. openbacktesting/broker/requests.py +192 -0
  12. openbacktesting/broker/sizing.py +76 -0
  13. openbacktesting/colors.py +175 -0
  14. openbacktesting/data/__init__.py +20 -0
  15. openbacktesting/data/aggregation.py +39 -0
  16. openbacktesting/data/context.py +30 -0
  17. openbacktesting/data/core.py +218 -0
  18. openbacktesting/data/generators.py +54 -0
  19. openbacktesting/data/strategy_data.py +103 -0
  20. openbacktesting/data/timeframe.py +69 -0
  21. openbacktesting/engine.py +34 -0
  22. openbacktesting/helpers.py +77 -0
  23. openbacktesting/indicator_api.py +10 -0
  24. openbacktesting/indicator_methods.py +123 -0
  25. openbacktesting/indicator_types.py +19 -0
  26. openbacktesting/indicators/__init__.py +7 -0
  27. openbacktesting/indicators/api.py +97 -0
  28. openbacktesting/indicators/calculus.py +75 -0
  29. openbacktesting/indicators/candles/__init__.py +0 -0
  30. openbacktesting/indicators/candles/cdl_doji.py +17 -0
  31. openbacktesting/indicators/candles/cdl_engulfing.py +14 -0
  32. openbacktesting/indicators/candles/cdl_eveningstar.py +15 -0
  33. openbacktesting/indicators/candles/cdl_hammer.py +14 -0
  34. openbacktesting/indicators/candles/cdl_harami.py +14 -0
  35. openbacktesting/indicators/candles/cdl_inside.py +14 -0
  36. openbacktesting/indicators/candles/cdl_marubozu.py +14 -0
  37. openbacktesting/indicators/candles/cdl_morningstar.py +15 -0
  38. openbacktesting/indicators/candles/cdl_shootingstar.py +14 -0
  39. openbacktesting/indicators/candles/cdl_z.py +28 -0
  40. openbacktesting/indicators/candles/ha.py +19 -0
  41. openbacktesting/indicators/catalog.json +8348 -0
  42. openbacktesting/indicators/common.py +94 -0
  43. openbacktesting/indicators/cycles/__init__.py +0 -0
  44. openbacktesting/indicators/cycles/dsp.py +10 -0
  45. openbacktesting/indicators/cycles/ebsw.py +10 -0
  46. openbacktesting/indicators/cycles/ht_dcperiod.py +10 -0
  47. openbacktesting/indicators/cycles/ht_dcphase.py +10 -0
  48. openbacktesting/indicators/cycles/ht_phasor.py +17 -0
  49. openbacktesting/indicators/cycles/ht_sine.py +17 -0
  50. openbacktesting/indicators/cycles/ht_trendmode.py +10 -0
  51. openbacktesting/indicators/cycles/msw.py +19 -0
  52. openbacktesting/indicators/methods.py +5762 -0
  53. openbacktesting/indicators/momentum/__init__.py +0 -0
  54. openbacktesting/indicators/momentum/ac.py +9 -0
  55. openbacktesting/indicators/momentum/ao.py +10 -0
  56. openbacktesting/indicators/momentum/apo.py +10 -0
  57. openbacktesting/indicators/momentum/bias.py +10 -0
  58. openbacktesting/indicators/momentum/bop.py +10 -0
  59. openbacktesting/indicators/momentum/brar.py +26 -0
  60. openbacktesting/indicators/momentum/cci.py +10 -0
  61. openbacktesting/indicators/momentum/cfo.py +10 -0
  62. openbacktesting/indicators/momentum/cg.py +10 -0
  63. openbacktesting/indicators/momentum/cmo.py +10 -0
  64. openbacktesting/indicators/momentum/coppock.py +10 -0
  65. openbacktesting/indicators/momentum/crsi.py +28 -0
  66. openbacktesting/indicators/momentum/cti.py +10 -0
  67. openbacktesting/indicators/momentum/dm.py +17 -0
  68. openbacktesting/indicators/momentum/er.py +10 -0
  69. openbacktesting/indicators/momentum/eri.py +17 -0
  70. openbacktesting/indicators/momentum/fisher.py +17 -0
  71. openbacktesting/indicators/momentum/fosc.py +10 -0
  72. openbacktesting/indicators/momentum/gator.py +13 -0
  73. openbacktesting/indicators/momentum/ibs.py +7 -0
  74. openbacktesting/indicators/momentum/inertia.py +22 -0
  75. openbacktesting/indicators/momentum/kdj.py +18 -0
  76. openbacktesting/indicators/momentum/kst.py +30 -0
  77. openbacktesting/indicators/momentum/lrsi.py +10 -0
  78. openbacktesting/indicators/momentum/macd.py +27 -0
  79. openbacktesting/indicators/momentum/macdext.py +27 -0
  80. openbacktesting/indicators/momentum/macdfix.py +18 -0
  81. openbacktesting/indicators/momentum/mom.py +10 -0
  82. openbacktesting/indicators/momentum/pgo.py +10 -0
  83. openbacktesting/indicators/momentum/pmo.py +13 -0
  84. openbacktesting/indicators/momentum/po.py +10 -0
  85. openbacktesting/indicators/momentum/ppo.py +20 -0
  86. openbacktesting/indicators/momentum/psl.py +12 -0
  87. openbacktesting/indicators/momentum/pvo.py +20 -0
  88. openbacktesting/indicators/momentum/qqe.py +31 -0
  89. openbacktesting/indicators/momentum/rci.py +20 -0
  90. openbacktesting/indicators/momentum/rmi.py +7 -0
  91. openbacktesting/indicators/momentum/roc.py +10 -0
  92. openbacktesting/indicators/momentum/rocp.py +10 -0
  93. openbacktesting/indicators/momentum/rocr.py +10 -0
  94. openbacktesting/indicators/momentum/rocr100.py +10 -0
  95. openbacktesting/indicators/momentum/rsi.py +10 -0
  96. openbacktesting/indicators/momentum/rsx.py +10 -0
  97. openbacktesting/indicators/momentum/rvgi.py +26 -0
  98. openbacktesting/indicators/momentum/slope.py +17 -0
  99. openbacktesting/indicators/momentum/smc_sweep.py +18 -0
  100. openbacktesting/indicators/momentum/smi.py +21 -0
  101. openbacktesting/indicators/momentum/smi_stochastic.py +15 -0
  102. openbacktesting/indicators/momentum/squeeze.py +47 -0
  103. openbacktesting/indicators/momentum/squeeze_pro.py +53 -0
  104. openbacktesting/indicators/momentum/stc.py +20 -0
  105. openbacktesting/indicators/momentum/stoch.py +19 -0
  106. openbacktesting/indicators/momentum/stochf.py +19 -0
  107. openbacktesting/indicators/momentum/stochrsi.py +19 -0
  108. openbacktesting/indicators/momentum/td_seq.py +17 -0
  109. openbacktesting/indicators/momentum/trix.py +19 -0
  110. openbacktesting/indicators/momentum/trixh.py +20 -0
  111. openbacktesting/indicators/momentum/tsi.py +26 -0
  112. openbacktesting/indicators/momentum/uo.py +22 -0
  113. openbacktesting/indicators/momentum/vwmacd.py +20 -0
  114. openbacktesting/indicators/momentum/wavetrend.py +15 -0
  115. openbacktesting/indicators/momentum/willr.py +10 -0
  116. openbacktesting/indicators/overlap/__init__.py +0 -0
  117. openbacktesting/indicators/overlap/alligator.py +13 -0
  118. openbacktesting/indicators/overlap/alma.py +33 -0
  119. openbacktesting/indicators/overlap/anchored_vwap.py +50 -0
  120. openbacktesting/indicators/overlap/anchored_vwap_bands.py +13 -0
  121. openbacktesting/indicators/overlap/avgprice.py +10 -0
  122. openbacktesting/indicators/overlap/dema.py +10 -0
  123. openbacktesting/indicators/overlap/ema.py +12 -0
  124. openbacktesting/indicators/overlap/frama.py +29 -0
  125. openbacktesting/indicators/overlap/fwma.py +10 -0
  126. openbacktesting/indicators/overlap/hilo.py +26 -0
  127. openbacktesting/indicators/overlap/hl2.py +10 -0
  128. openbacktesting/indicators/overlap/hlc3.py +10 -0
  129. openbacktesting/indicators/overlap/hma.py +10 -0
  130. openbacktesting/indicators/overlap/ht_trendline.py +10 -0
  131. openbacktesting/indicators/overlap/hwma.py +10 -0
  132. openbacktesting/indicators/overlap/ichimoku.py +44 -0
  133. openbacktesting/indicators/overlap/jma.py +10 -0
  134. openbacktesting/indicators/overlap/kama.py +10 -0
  135. openbacktesting/indicators/overlap/linreg.py +15 -0
  136. openbacktesting/indicators/overlap/linregangle.py +10 -0
  137. openbacktesting/indicators/overlap/linregintercept.py +10 -0
  138. openbacktesting/indicators/overlap/linregslope.py +10 -0
  139. openbacktesting/indicators/overlap/ma.py +10 -0
  140. openbacktesting/indicators/overlap/ma_envelopes.py +16 -0
  141. openbacktesting/indicators/overlap/mama.py +17 -0
  142. openbacktesting/indicators/overlap/mavp.py +17 -0
  143. openbacktesting/indicators/overlap/mcgd.py +10 -0
  144. openbacktesting/indicators/overlap/medprice.py +10 -0
  145. openbacktesting/indicators/overlap/midpoint.py +10 -0
  146. openbacktesting/indicators/overlap/midprice.py +10 -0
  147. openbacktesting/indicators/overlap/mmar.py +21 -0
  148. openbacktesting/indicators/overlap/ohlc4.py +10 -0
  149. openbacktesting/indicators/overlap/pwma.py +10 -0
  150. openbacktesting/indicators/overlap/rainbow.py +25 -0
  151. openbacktesting/indicators/overlap/rma.py +10 -0
  152. openbacktesting/indicators/overlap/sinwma.py +10 -0
  153. openbacktesting/indicators/overlap/sma.py +14 -0
  154. openbacktesting/indicators/overlap/ssf.py +10 -0
  155. openbacktesting/indicators/overlap/supertrend.py +21 -0
  156. openbacktesting/indicators/overlap/swma.py +10 -0
  157. openbacktesting/indicators/overlap/t3.py +10 -0
  158. openbacktesting/indicators/overlap/tema.py +10 -0
  159. openbacktesting/indicators/overlap/trima.py +10 -0
  160. openbacktesting/indicators/overlap/tsf.py +10 -0
  161. openbacktesting/indicators/overlap/typprice.py +10 -0
  162. openbacktesting/indicators/overlap/vidya.py +10 -0
  163. openbacktesting/indicators/overlap/vwap.py +10 -0
  164. openbacktesting/indicators/overlap/vwma.py +10 -0
  165. openbacktesting/indicators/overlap/wcp.py +10 -0
  166. openbacktesting/indicators/overlap/wma.py +10 -0
  167. openbacktesting/indicators/overlap/zlma.py +10 -0
  168. openbacktesting/indicators/performance/__init__.py +0 -0
  169. openbacktesting/indicators/performance/drawdown.py +18 -0
  170. openbacktesting/indicators/performance/log_return.py +10 -0
  171. openbacktesting/indicators/performance/percent_return.py +10 -0
  172. openbacktesting/indicators/predicates.py +88 -0
  173. openbacktesting/indicators/registry.py +571 -0
  174. openbacktesting/indicators/runtime.py +290 -0
  175. openbacktesting/indicators/statistics/__init__.py +0 -0
  176. openbacktesting/indicators/statistics/beta.py +10 -0
  177. openbacktesting/indicators/statistics/correl.py +10 -0
  178. openbacktesting/indicators/statistics/entropy.py +10 -0
  179. openbacktesting/indicators/statistics/kurtosis.py +10 -0
  180. openbacktesting/indicators/statistics/mad.py +10 -0
  181. openbacktesting/indicators/statistics/md.py +10 -0
  182. openbacktesting/indicators/statistics/median.py +10 -0
  183. openbacktesting/indicators/statistics/mode.py +48 -0
  184. openbacktesting/indicators/statistics/quantile.py +10 -0
  185. openbacktesting/indicators/statistics/skew.py +10 -0
  186. openbacktesting/indicators/statistics/stderr.py +10 -0
  187. openbacktesting/indicators/statistics/stdev.py +10 -0
  188. openbacktesting/indicators/statistics/variance.py +10 -0
  189. openbacktesting/indicators/statistics/zscore.py +10 -0
  190. openbacktesting/indicators/trend/__init__.py +0 -0
  191. openbacktesting/indicators/trend/adx.py +28 -0
  192. openbacktesting/indicators/trend/adxr.py +28 -0
  193. openbacktesting/indicators/trend/amat.py +19 -0
  194. openbacktesting/indicators/trend/aroon.py +18 -0
  195. openbacktesting/indicators/trend/atr_stop.py +33 -0
  196. openbacktesting/indicators/trend/chop.py +20 -0
  197. openbacktesting/indicators/trend/cksp.py +17 -0
  198. openbacktesting/indicators/trend/cpr.py +57 -0
  199. openbacktesting/indicators/trend/decay.py +10 -0
  200. openbacktesting/indicators/trend/decreasing.py +18 -0
  201. openbacktesting/indicators/trend/dpo.py +12 -0
  202. openbacktesting/indicators/trend/dx.py +19 -0
  203. openbacktesting/indicators/trend/edecay.py +10 -0
  204. openbacktesting/indicators/trend/increasing.py +18 -0
  205. openbacktesting/indicators/trend/long_run.py +10 -0
  206. openbacktesting/indicators/trend/minus_dm.py +10 -0
  207. openbacktesting/indicators/trend/pivot_points.py +89 -0
  208. openbacktesting/indicators/trend/pivothigh.py +23 -0
  209. openbacktesting/indicators/trend/pivotlow.py +23 -0
  210. openbacktesting/indicators/trend/plus_dm.py +10 -0
  211. openbacktesting/indicators/trend/pmax.py +18 -0
  212. openbacktesting/indicators/trend/psar.py +19 -0
  213. openbacktesting/indicators/trend/qstick.py +10 -0
  214. openbacktesting/indicators/trend/sarext.py +35 -0
  215. openbacktesting/indicators/trend/short_run.py +10 -0
  216. openbacktesting/indicators/trend/tsignals.py +26 -0
  217. openbacktesting/indicators/trend/ttm_trend.py +16 -0
  218. openbacktesting/indicators/trend/vhf.py +10 -0
  219. openbacktesting/indicators/trend/volatility_stop.py +33 -0
  220. openbacktesting/indicators/trend/vortex.py +17 -0
  221. openbacktesting/indicators/trend/xsignals.py +29 -0
  222. openbacktesting/indicators/types.py +344 -0
  223. openbacktesting/indicators/utility/__init__.py +0 -0
  224. openbacktesting/indicators/utility/barssince.py +21 -0
  225. openbacktesting/indicators/utility/change.py +7 -0
  226. openbacktesting/indicators/utility/cross.py +12 -0
  227. openbacktesting/indicators/utility/crossover.py +11 -0
  228. openbacktesting/indicators/utility/crossunder.py +11 -0
  229. openbacktesting/indicators/utility/cum.py +7 -0
  230. openbacktesting/indicators/utility/falling.py +10 -0
  231. openbacktesting/indicators/utility/highest.py +7 -0
  232. openbacktesting/indicators/utility/highestbars.py +29 -0
  233. openbacktesting/indicators/utility/lowest.py +7 -0
  234. openbacktesting/indicators/utility/lowestbars.py +29 -0
  235. openbacktesting/indicators/utility/percentile_linear_interpolation.py +12 -0
  236. openbacktesting/indicators/utility/percentile_nearest_rank.py +16 -0
  237. openbacktesting/indicators/utility/percentrank.py +7 -0
  238. openbacktesting/indicators/utility/range.py +8 -0
  239. openbacktesting/indicators/utility/rising.py +10 -0
  240. openbacktesting/indicators/utility/rolling_count.py +12 -0
  241. openbacktesting/indicators/utility/rolling_sum.py +7 -0
  242. openbacktesting/indicators/utility/running_max.py +7 -0
  243. openbacktesting/indicators/utility/running_min.py +7 -0
  244. openbacktesting/indicators/utility/shift.py +7 -0
  245. openbacktesting/indicators/utility/valuewhen.py +25 -0
  246. openbacktesting/indicators/volatility/__init__.py +0 -0
  247. openbacktesting/indicators/volatility/aberration.py +21 -0
  248. openbacktesting/indicators/volatility/accbands.py +20 -0
  249. openbacktesting/indicators/volatility/adr.py +7 -0
  250. openbacktesting/indicators/volatility/atr.py +12 -0
  251. openbacktesting/indicators/volatility/avolume.py +10 -0
  252. openbacktesting/indicators/volatility/bbands.py +20 -0
  253. openbacktesting/indicators/volatility/ce.py +25 -0
  254. openbacktesting/indicators/volatility/cvi.py +10 -0
  255. openbacktesting/indicators/volatility/donchian.py +20 -0
  256. openbacktesting/indicators/volatility/hvol.py +10 -0
  257. openbacktesting/indicators/volatility/hwc.py +27 -0
  258. openbacktesting/indicators/volatility/kc.py +27 -0
  259. openbacktesting/indicators/volatility/massi.py +10 -0
  260. openbacktesting/indicators/volatility/natr.py +19 -0
  261. openbacktesting/indicators/volatility/pdist.py +10 -0
  262. openbacktesting/indicators/volatility/rvi.py +21 -0
  263. openbacktesting/indicators/volatility/starc.py +11 -0
  264. openbacktesting/indicators/volatility/thermo.py +28 -0
  265. openbacktesting/indicators/volatility/true_range.py +31 -0
  266. openbacktesting/indicators/volatility/ui.py +10 -0
  267. openbacktesting/indicators/volume/__init__.py +0 -0
  268. openbacktesting/indicators/volume/ad.py +10 -0
  269. openbacktesting/indicators/volume/adosc.py +19 -0
  270. openbacktesting/indicators/volume/aobv.py +31 -0
  271. openbacktesting/indicators/volume/cmf.py +12 -0
  272. openbacktesting/indicators/volume/efi.py +12 -0
  273. openbacktesting/indicators/volume/emv.py +10 -0
  274. openbacktesting/indicators/volume/eom.py +19 -0
  275. openbacktesting/indicators/volume/iii.py +9 -0
  276. openbacktesting/indicators/volume/kvo.py +28 -0
  277. openbacktesting/indicators/volume/marketfi.py +10 -0
  278. openbacktesting/indicators/volume/mfi.py +12 -0
  279. openbacktesting/indicators/volume/nvi.py +26 -0
  280. openbacktesting/indicators/volume/obv.py +10 -0
  281. openbacktesting/indicators/volume/pvi.py +26 -0
  282. openbacktesting/indicators/volume/pvol.py +10 -0
  283. openbacktesting/indicators/volume/pvr.py +10 -0
  284. openbacktesting/indicators/volume/pvt.py +10 -0
  285. openbacktesting/indicators/volume/rvol.py +8 -0
  286. openbacktesting/indicators/volume/vfi.py +18 -0
  287. openbacktesting/indicators/volume/vosc.py +10 -0
  288. openbacktesting/indicators/volume/wad.py +10 -0
  289. openbacktesting/indicators/volume/wvad.py +9 -0
  290. openbacktesting/inputs.py +51 -0
  291. openbacktesting/lib.py +31 -0
  292. openbacktesting/logs.py +129 -0
  293. openbacktesting/optimization.py +8 -0
  294. openbacktesting/optimizer/__init__.py +25 -0
  295. openbacktesting/optimizer/_semantics.py +155 -0
  296. openbacktesting/optimizer/identity.py +45 -0
  297. openbacktesting/optimizer/models.py +92 -0
  298. openbacktesting/optimizer/plan.py +129 -0
  299. openbacktesting/optimizer/portfolio-result.schema.json +4984 -0
  300. openbacktesting/optimizer/query.py +257 -0
  301. openbacktesting/optimizer/runner.py +98 -0
  302. openbacktesting/optimizer/storage.py +261 -0
  303. openbacktesting/optimizer/validation.py +64 -0
  304. openbacktesting/parallel.py +9 -0
  305. openbacktesting/plotting/__init__.py +23 -0
  306. openbacktesting/plotting/collector.py +509 -0
  307. openbacktesting/plotting/encoding.py +102 -0
  308. openbacktesting/plotting/types.py +92 -0
  309. openbacktesting/portfolio/__init__.py +15 -0
  310. openbacktesting/portfolio/analysis.py +245 -0
  311. openbacktesting/portfolio/builder.py +381 -0
  312. openbacktesting/portfolio/catalog.json +1404 -0
  313. openbacktesting/portfolio/helpers.py +92 -0
  314. openbacktesting/portfolio/metrics.py +186 -0
  315. openbacktesting/portfolio/models.py +63 -0
  316. openbacktesting/portfolio/periods.py +164 -0
  317. openbacktesting/portfolio/rates.py +14 -0
  318. openbacktesting/portfolio/snapshot.py +111 -0
  319. openbacktesting/portfolio/stats.py +272 -0
  320. openbacktesting/portfolio/trades.py +136 -0
  321. openbacktesting/portfolio/trading.py +215 -0
  322. openbacktesting/py.typed +0 -0
  323. openbacktesting/runtime.py +330 -0
  324. openbacktesting/simulation/__init__.py +17 -0
  325. openbacktesting/simulation/collection.py +13 -0
  326. openbacktesting/simulation/config.py +241 -0
  327. openbacktesting/simulation/core.py +528 -0
  328. openbacktesting/simulation/optimization.py +326 -0
  329. openbacktesting/simulation/parallel.py +145 -0
  330. openbacktesting/simulation/variants.py +107 -0
  331. openbacktesting/strategy/__init__.py +19 -0
  332. openbacktesting/strategy/bar_state.py +14 -0
  333. openbacktesting/strategy/base.py +810 -0
  334. openbacktesting/strategy/declaration.py +90 -0
  335. openbacktesting/strategy/helpers.py +198 -0
  336. openbacktesting/strategy/inputs.py +460 -0
  337. openbacktesting/strategy/sizing.py +80 -0
  338. openbacktesting/strategy/templates.py +160 -0
  339. openbacktesting/strategy_data.py +10 -0
  340. openbacktesting/test/BTCUSD.csv +157 -0
  341. openbacktesting/test/EURUSD.csv +5001 -0
  342. openbacktesting/test/GOOG.csv +2149 -0
  343. openbacktesting/test/__init__.py +29 -0
  344. openbacktesting/test/__main__.py +11 -0
  345. openbacktesting/test/_test.py +1312 -0
  346. openbacktesting/util.py +31 -0
  347. openbacktesting-0.2.0.dist-info/METADATA +151 -0
  348. openbacktesting-0.2.0.dist-info/RECORD +350 -0
  349. openbacktesting-0.2.0.dist-info/WHEEL +4 -0
  350. 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
+ )