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,326 @@
1
+ """Native parameter search and parallel optimization orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Callable, Sequence
7
+ from contextlib import contextmanager
8
+ from copy import copy
9
+ from functools import lru_cache
10
+ from itertools import product, repeat
11
+ from multiprocessing.pool import ThreadPool
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+ from numpy.random import default_rng
16
+
17
+ from openbacktesting.helpers import patch
18
+ from openbacktesting.indicators.runtime import (
19
+ DEFAULT_CACHE_BYTES,
20
+ IndicatorCache,
21
+ optimization_scope,
22
+ )
23
+ from openbacktesting.portfolio.stats import dummy_stats
24
+ from openbacktesting.simulation.parallel import SharedMemoryManager, _batch
25
+ from openbacktesting.strategy.inputs import InputError, _definitions, request_for, resolve_native
26
+ from openbacktesting.util import _tqdm
27
+
28
+
29
+ @contextmanager
30
+ def _joined_pool(pool):
31
+ """ThreadPool.__exit__ does not wait for in-flight threads; join them before releasing market data and caches."""
32
+ active = pool
33
+ try:
34
+ with pool as active:
35
+ yield active
36
+ finally:
37
+ if isinstance(active, ThreadPool):
38
+ active.join()
39
+
40
+
41
+ def _optimize(
42
+ self,
43
+ _mp_task,
44
+ *,
45
+ maximize: str | Callable[[pd.Series], float] = "SQN",
46
+ method: str = "grid",
47
+ max_tries: float | None = None,
48
+ constraint: Callable[[dict], bool] | None = None,
49
+ return_heatmap: bool = False,
50
+ return_optimization: bool = False,
51
+ random_state: int | None = None,
52
+ **kwargs,
53
+ ) -> pd.Series | tuple[pd.Series, pd.Series] | tuple[pd.Series, pd.Series, dict]:
54
+ if not kwargs:
55
+ raise ValueError("Need some strategy parameters to optimize")
56
+
57
+ declarations = _definitions(self._strategy)
58
+ if not declarations:
59
+ self._strategy._check_reserved_params(kwargs)
60
+ if declarations and method in ("sambo", "skopt"):
61
+ raise InputError("UnsupportedOptimizationInput", None, "Declared strategies initially support only grid")
62
+
63
+ maximize_key = None
64
+ if isinstance(maximize, str):
65
+ maximize_key = str(maximize)
66
+ if maximize not in dummy_stats().index:
67
+ raise ValueError(
68
+ "`maximize`, if str, must match a key in pd.Series result of backtest.run()"
69
+ )
70
+
71
+ def maximize(stats: pd.Series, _key=maximize):
72
+ return stats[_key]
73
+
74
+ elif not callable(maximize):
75
+ raise TypeError(
76
+ "`maximize` must be str (a field of backtest.run() result "
77
+ "Series) or a function that accepts result Series "
78
+ "and returns a number; the higher the better"
79
+ )
80
+ assert callable(maximize), maximize
81
+
82
+ have_constraint = bool(constraint)
83
+ if constraint is None:
84
+
85
+ def constraint(_):
86
+ return True
87
+
88
+ elif not callable(constraint):
89
+ raise TypeError(
90
+ "`constraint` must be a function that accepts a dict "
91
+ "of strategy parameters and returns a bool whether "
92
+ "the combination of parameters is admissible or not"
93
+ )
94
+ assert callable(constraint), constraint
95
+
96
+ if method == "skopt":
97
+ method = "sambo"
98
+ warnings.warn(
99
+ '`Backtest.optimize(method="skopt")` is deprecated. Use `method="sambo"`.',
100
+ DeprecationWarning,
101
+ stacklevel=2,
102
+ )
103
+ if return_optimization and method != "sambo":
104
+ raise ValueError("return_optimization=True only valid if method='sambo'")
105
+
106
+ def _tuple(x):
107
+ return x if isinstance(x, Sequence) and not isinstance(x, str) else (x,)
108
+
109
+ for k, v in kwargs.items():
110
+ if len(_tuple(v)) == 0:
111
+ raise ValueError(
112
+ f"Optimization variable '{k}' is passed no optimization values: {k}={v}"
113
+ )
114
+
115
+ if declarations:
116
+ # Validate all values before constraint evaluation and random sampling so invalid candidates cannot be hidden.
117
+ kwargs = {
118
+ name: [
119
+ resolve_native(self._strategy, {name: value}).overrides()[name]
120
+ for value in _tuple(values)
121
+ ]
122
+ for name, values in kwargs.items()
123
+ }
124
+
125
+ class AttrDict(dict):
126
+ def __getattr__(self, item):
127
+ return self[item]
128
+
129
+ def _grid_size():
130
+ size = int(np.prod([len(_tuple(v)) for v in kwargs.values()]))
131
+ if size < 10_000 and have_constraint:
132
+ size = sum(
133
+ 1
134
+ for p in product(*(zip(repeat(k), _tuple(v)) for k, v in kwargs.items()))
135
+ if constraint(AttrDict(p))
136
+ )
137
+ return size
138
+
139
+ def _optimize_grid() -> pd.Series | tuple[pd.Series, pd.Series]:
140
+ rand = default_rng(random_state).random
141
+ grid_frac = (
142
+ 1
143
+ if max_tries is None
144
+ else max_tries
145
+ if 0 < max_tries <= 1
146
+ else max_tries / _grid_size()
147
+ )
148
+ param_combos = [
149
+ dict(params) # back to dict so it pickles
150
+ for params in (
151
+ AttrDict(params)
152
+ for params in product(*(zip(repeat(k), _tuple(v)) for k, v in kwargs.items()))
153
+ )
154
+ if constraint(params) and rand() <= grid_frac
155
+ ]
156
+ if not param_combos:
157
+ raise ValueError("No admissible parameter combinations to test")
158
+
159
+ if len(param_combos) > 300:
160
+ warnings.warn(
161
+ f"Searching for best of {len(param_combos)} configurations.", stacklevel=2
162
+ )
163
+
164
+ heatmap = pd.Series(
165
+ np.nan,
166
+ name=maximize_key,
167
+ index=pd.MultiIndex.from_tuples(
168
+ [p.values() for p in param_combos], names=next(iter(param_combos)).keys()
169
+ ),
170
+ )
171
+
172
+ batches = list(_batch(param_combos))
173
+ if declarations:
174
+ batches = [
175
+ [request_for(self._strategy, params) for params in batch] for batch in batches
176
+ ]
177
+
178
+ from openbacktesting import Pool
179
+
180
+ with (
181
+ optimization_scope(
182
+ getattr(self, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)
183
+ ) as token,
184
+ SharedMemoryManager() as smm,
185
+ _joined_pool(Pool()) as pool,
186
+ ):
187
+ with patch(self, "_data", None):
188
+ bt = copy(self) # bt._data will be reassigned in _mp_task worker
189
+ bt._portfolio_enabled = maximize_key is None
190
+ bt._results = None
191
+ bt._log_collector = None
192
+ bt._indicator_cache_token = token
193
+ data_shm = smm.df2shm(self._data)
194
+ results = _tqdm(
195
+ pool.imap(
196
+ _mp_task,
197
+ ((bt, data_shm, params_batch) for params_batch in batches),
198
+ ),
199
+ total=len(param_combos),
200
+ desc="Backtest.optimize",
201
+ )
202
+ for param_batch, result in zip(_batch(param_combos), results):
203
+ for params, stats in zip(param_batch, result):
204
+ if stats is not None:
205
+ heatmap[tuple(params.values())] = maximize(stats)
206
+
207
+ if pd.isnull(heatmap).all():
208
+ # No trade was made in any of the runs. Just make a random
209
+ # run so we get some, if empty, results
210
+ stats = self.run(**param_combos[0])
211
+ else:
212
+ best_params = heatmap.idxmax(skipna=True)
213
+ stats = self.run(**dict(zip(heatmap.index.names, best_params)))
214
+
215
+ if return_heatmap:
216
+ return stats, heatmap
217
+ return stats
218
+
219
+ def _optimize_sambo() -> (
220
+ pd.Series | tuple[pd.Series, pd.Series] | tuple[pd.Series, pd.Series, dict]
221
+ ):
222
+ try:
223
+ import sambo
224
+ except ImportError:
225
+ raise ImportError(
226
+ "Need package 'sambo' for method='sambo'. pip install sambo"
227
+ ) from None
228
+
229
+ nonlocal max_tries
230
+ max_tries = (
231
+ 200
232
+ if max_tries is None
233
+ else max(1, int(max_tries * _grid_size()))
234
+ if 0 < max_tries <= 1
235
+ else max_tries
236
+ )
237
+
238
+ dimensions = []
239
+ for values in kwargs.values():
240
+ values = np.asarray(values)
241
+ if values.dtype.kind in "mM": # timedelta, datetime64
242
+ # these dtypes are unsupported in SAMBO, so convert to raw int
243
+ # TODO: save dtype and convert back later
244
+ values = values.astype(np.int64)
245
+
246
+ if values.dtype.kind in "iumM":
247
+ dimensions.append((values.min(), values.max() + 1))
248
+ elif values.dtype.kind == "f":
249
+ dimensions.append((values.min(), values.max()))
250
+ else:
251
+ dimensions.append(values.tolist())
252
+
253
+ # Avoid recomputing re-evaluations
254
+ @lru_cache
255
+ def memoized_run(tup):
256
+ nonlocal maximize, self
257
+ with (
258
+ patch(self, "_plotting_enabled", False),
259
+ patch(self, "_logs_enabled", False),
260
+ patch(self, "_portfolio_enabled", maximize_key is None),
261
+ ):
262
+ stats = self.run(**dict(tup))
263
+ return -maximize(stats)
264
+
265
+ progress = iter(
266
+ _tqdm(
267
+ repeat(None),
268
+ total=max_tries,
269
+ leave=False,
270
+ desc=self.optimize.__qualname__,
271
+ mininterval=2,
272
+ )
273
+ )
274
+ _names = tuple(kwargs.keys())
275
+
276
+ def objective_function(x):
277
+ nonlocal progress, memoized_run, constraint, _names
278
+ next(progress)
279
+ value = memoized_run(tuple(zip(_names, x)))
280
+ return 0 if np.isnan(value) else value
281
+
282
+ def cons(x):
283
+ nonlocal constraint, _names
284
+ return constraint(AttrDict(zip(_names, x)))
285
+
286
+ res = sambo.minimize(
287
+ fun=objective_function,
288
+ bounds=dimensions,
289
+ constraints=cons,
290
+ max_iter=max_tries,
291
+ method="sceua",
292
+ rng=random_state,
293
+ )
294
+
295
+ stats = self.run(**dict(zip(kwargs.keys(), res.x)))
296
+ output = [stats]
297
+
298
+ if return_heatmap:
299
+ heatmap = pd.Series(dict(zip(map(tuple, res.xv), -res.funv)), name=maximize_key)
300
+ heatmap.index.names = kwargs.keys()
301
+ heatmap.sort_index(inplace=True)
302
+ output.append(heatmap)
303
+
304
+ if return_optimization:
305
+ output.append(res)
306
+
307
+ return stats if len(output) == 1 else tuple(output)
308
+
309
+ if method == "grid":
310
+ output = _optimize_grid()
311
+ elif method in ("sambo", "skopt"):
312
+ with patch(
313
+ self,
314
+ "_indicator_cache",
315
+ IndicatorCache(getattr(self, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)),
316
+ ):
317
+ output = _optimize_sambo()
318
+ else:
319
+ raise ValueError(f"Method should be 'grid' or 'sambo', not {method!r}")
320
+ return output
321
+
322
+
323
+ from openbacktesting.helpers import _set_legacy_module
324
+
325
+ _set_legacy_module(_joined_pool, "openbacktesting.optimization")
326
+ _set_legacy_module(_optimize, "openbacktesting.optimization")
@@ -0,0 +1,145 @@
1
+ """Native parallel batches and shared-memory lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import warnings
8
+ from itertools import chain
9
+ from multiprocessing import resource_tracker as _mprt
10
+ from multiprocessing import shared_memory as _mpshm
11
+ from threading import Lock
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ from openbacktesting.helpers import _set_legacy_module, patch
17
+
18
+ if sys.version_info >= (3, 13):
19
+ SharedMemory = _mpshm.SharedMemory
20
+ else:
21
+
22
+ class SharedMemory(_mpshm.SharedMemory):
23
+ # From https://github.com/python/cpython/issues/82300#issuecomment-2169035092
24
+ __lock = Lock()
25
+
26
+ def __init__(self, *args, track: bool = True, **kwargs):
27
+ self._track = track
28
+ if track:
29
+ return super().__init__(*args, **kwargs) # noqa: PLE0101 — preserve the shared-memory initialization retry return
30
+ with self.__lock, patch(_mprt, "register", lambda *a, **kw: None):
31
+ super().__init__(*args, **kwargs)
32
+
33
+ def unlink(self):
34
+ if _mpshm._USE_POSIX and self._name:
35
+ _mpshm._posixshmem.shm_unlink(self._name)
36
+ if self._track:
37
+ _mprt.unregister(self._name, "shared_memory")
38
+
39
+
40
+ def _batch(seq):
41
+ # XXX: Replace with itertools.batched
42
+ n = np.clip(int(len(seq) // (os.cpu_count() or 1)), 1, 300)
43
+ for i in range(0, len(seq), n):
44
+ yield seq[i : i + n]
45
+
46
+
47
+ class SharedMemoryManager:
48
+ """
49
+ A simple shared memory contextmanager based on
50
+ https://docs.python.org/3/library/multiprocessing.shared_memory.html#multiprocessing.shared_memory.SharedMemory
51
+ """
52
+
53
+ def __init__(self, create=False) -> None:
54
+ self._shms: list[SharedMemory] = []
55
+ self.__create = create
56
+
57
+ def SharedMemory(self, *, name=None, create=False, size=0, track=True):
58
+ shm = SharedMemory(name=name, create=create, size=size, track=track)
59
+ shm._create = create
60
+ # Essential to keep refs on Windows
61
+ # https://stackoverflow.com/questions/74193377/filenotfounderror-when-passing-a-shared-memory-to-a-new-process#comment130999060_74194875
62
+ self._shms.append(shm)
63
+ return shm
64
+
65
+ def __enter__(self):
66
+ return self
67
+
68
+ def __exit__(self, *args, **kwargs):
69
+ failures = []
70
+ for shm in self._shms:
71
+ try:
72
+ shm.close()
73
+ if shm._create:
74
+ shm.unlink()
75
+ except Exception as error: # noqa: BLE001 — release each segment, then re-raise the first error
76
+ failures.append(error)
77
+ warnings.warn(
78
+ f"Failed to unlink shared memory {shm.name!r}",
79
+ category=ResourceWarning,
80
+ stacklevel=2,
81
+ )
82
+ if failures:
83
+ raise failures[0]
84
+
85
+ def arr2shm(self, vals):
86
+ """Array to shared memory. Returns (shm_name, shape, dtype) used for restore."""
87
+ assert vals.ndim == 1, (vals.ndim, vals.shape, vals)
88
+ # Not `vals.nbytes`; lazy indexes (e.g. RangeIndex) report their own,
89
+ # constant size rather than that of the array they materialize into
90
+ shm = self.SharedMemory(size=vals.size * vals.dtype.base.itemsize, create=True)
91
+ buf = np.ndarray(vals.shape, dtype=vals.dtype.base, buffer=shm.buf)
92
+ if isinstance(vals.dtype, pd.DatetimeTZDtype):
93
+ # Preserve absolute instants at their original precision in both index and columns; do not lose offsets for repeated DST hours.
94
+ buf.view("i8")[:] = vals.array.asi8
95
+ else:
96
+ buf[:] = vals
97
+ return shm.name, vals.shape, vals.dtype
98
+
99
+ def df2shm(self, df):
100
+ return tuple(
101
+ (
102
+ (column, *self.arr2shm(values))
103
+ for column, values in chain([(self._DF_INDEX_COL, df.index)], df.items())
104
+ )
105
+ )
106
+
107
+ @staticmethod
108
+ def shm2s(shm, shape, dtype) -> pd.Series:
109
+ arr = np.ndarray(shape, dtype=dtype.base, buffer=shm.buf)
110
+ arr.setflags(write=False)
111
+ if isinstance(dtype, pd.DatetimeTZDtype):
112
+ arr = pd.DatetimeIndex(arr).tz_localize("UTC").tz_convert(dtype.tz)
113
+ return pd.Series(arr, dtype=dtype, copy=False)
114
+
115
+ _DF_INDEX_COL = "__bt_index"
116
+
117
+ @staticmethod
118
+ def shm2df(data_shm):
119
+ shm = []
120
+ try:
121
+ for _, name, _, _ in data_shm:
122
+ shm.append(SharedMemory(name=name, create=False, track=False))
123
+ df = pd.DataFrame(
124
+ {
125
+ col: SharedMemoryManager.shm2s(handle, shape, dtype)
126
+ for handle, (col, _, shape, dtype) in zip(shm, data_shm)
127
+ },
128
+ copy=False,
129
+ )
130
+ df.set_index(SharedMemoryManager._DF_INDEX_COL, drop=True, inplace=True)
131
+ # Result statistics retain the market-data index; it must survive independently after the mapping closes.
132
+ df.index = df.index.copy(deep=True)
133
+ df.index.name = None
134
+ return df, shm
135
+ except BaseException:
136
+ for handle in shm:
137
+ handle.close()
138
+ raise
139
+
140
+
141
+ _set_legacy_module(_batch, "openbacktesting._util")
142
+ _set_legacy_module(SharedMemoryManager, "openbacktesting._util")
143
+
144
+ if sys.version_info < (3, 13):
145
+ _set_legacy_module(SharedMemory, "openbacktesting._util")
@@ -0,0 +1,107 @@
1
+ """Existing implementations moved from lib: FractionalBacktest, MultiBacktest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from itertools import chain, count
6
+
7
+ import pandas as pd
8
+
9
+ from openbacktesting.helpers import _set_legacy_module
10
+ from openbacktesting.simulation.core import Backtest
11
+ from openbacktesting.simulation.parallel import SharedMemoryManager, _batch
12
+ from openbacktesting.util import _tqdm
13
+
14
+
15
+ class FractionalBacktest(Backtest):
16
+ """Removed price-scaling entry point; use Backtest and InstrumentSpec."""
17
+
18
+ def __init__(self, *args, **kwargs):
19
+ raise TypeError(
20
+ "FractionalBacktest has been removed; use Backtest(instrument=InstrumentSpec(quantity_step=...)) and qty"
21
+ )
22
+
23
+
24
+ class MultiBacktest:
25
+ """Run one strategy on multiple datasets and aggregate statistics for each dataset.
26
+
27
+
28
+ Multi-dataset `openbacktesting.engine.Backtest` wrapper.
29
+
30
+ Run supplied `openbacktesting.engine.Strategy` on several instruments,
31
+ in parallel. Used for comparing strategy runs across many instruments
32
+ or classes of instruments. Example:
33
+
34
+ from openbacktesting.test import EURUSD, BTCUSD, SmaCross
35
+ btm = MultiBacktest([EURUSD, BTCUSD], SmaCross)
36
+ stats_per_ticker: pd.DataFrame = btm.run(fast=10, slow=20)
37
+ heatmap_per_ticker: pd.DataFrame = btm.optimize(...)
38
+ """
39
+
40
+ def __init__(self, df_list, strategy_cls, **kwargs):
41
+ self._dfs = df_list
42
+ self._strategy = strategy_cls
43
+ self._bt_kwargs = kwargs
44
+
45
+ def run(self, **kwargs):
46
+ """Backtest each dataset and return statistics in dataset columns.
47
+
48
+
49
+ Wraps `openbacktesting.engine.Backtest.run`. Returns `pd.DataFrame` with
50
+ currency indexes in columns.
51
+ """
52
+ from openbacktesting import Pool
53
+
54
+ with Pool() as pool, SharedMemoryManager() as smm:
55
+ shm = [smm.df2shm(df) for df in self._dfs]
56
+ results = _tqdm(
57
+ pool.imap(
58
+ self._mp_task_run,
59
+ (
60
+ (df_batch, self._strategy, self._bt_kwargs, kwargs)
61
+ for df_batch in _batch(shm)
62
+ ),
63
+ ),
64
+ total=len(shm),
65
+ desc=self.run.__qualname__,
66
+ mininterval=2,
67
+ )
68
+ df = pd.DataFrame(list(chain(*results))).transpose()
69
+ return df
70
+
71
+ @staticmethod
72
+ def _mp_task_run(args):
73
+ data_shm, strategy, bt_kwargs, run_kwargs = args
74
+ dfs, shms = zip(*(SharedMemoryManager.shm2df(i) for i in data_shm))
75
+ try:
76
+ return [
77
+ stats.filter(regex="^[^_]")
78
+ for stats in (Backtest(df, strategy, **bt_kwargs).run(**run_kwargs) for df in dfs)
79
+ ]
80
+ finally:
81
+ for shmem in chain(*shms):
82
+ shmem.close()
83
+
84
+ def optimize(self, **kwargs) -> pd.DataFrame:
85
+ """Optimize parameters separately for each dataset and aggregate parameter scores.
86
+
87
+
88
+ Wraps `openbacktesting.engine.Backtest.optimize`, but returns `pd.DataFrame` with
89
+ currency indexes in columns.
90
+
91
+ heatmap: pd.DataFrame = btm.optimize(...)
92
+ mean_scores = heatmap.mean(axis=1)
93
+ """
94
+ heatmaps = []
95
+ # Simple loop since bt.optimize already does its own multiprocessing
96
+ for df in _tqdm(self._dfs, desc=self.__class__.__name__, mininterval=2):
97
+ bt = Backtest(df, self._strategy, **self._bt_kwargs)
98
+ _best_stats, heatmap = bt.optimize( # type: ignore
99
+ return_heatmap=True, return_optimization=False, **kwargs
100
+ )
101
+ heatmaps.append(heatmap)
102
+ heatmap = pd.DataFrame(dict(zip(count(), heatmaps)))
103
+ return heatmap
104
+
105
+
106
+ _set_legacy_module(FractionalBacktest, "openbacktesting.lib")
107
+ _set_legacy_module(MultiBacktest, "openbacktesting.lib")
@@ -0,0 +1,19 @@
1
+ """Compatibility exports for the old strategy path; implemented in strategy.base."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from openbacktesting.strategy.bar_state import BarState as BarState
6
+
7
+ if TYPE_CHECKING:
8
+ from openbacktesting.strategy.base import Strategy as Strategy
9
+
10
+ __all__ = ["BarState", "Strategy"]
11
+
12
+
13
+ def __getattr__(name):
14
+ if name == "Strategy":
15
+ from openbacktesting.strategy.base import Strategy
16
+
17
+ globals()[name] = Strategy
18
+ return Strategy
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,14 @@
1
+ """Immutable Bar state for fixed historical input."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True, slots=True)
7
+ class BarState:
8
+ """First/last snapshot at the current execution position; saved values do not change with the run cursor.
9
+
10
+ is_first means input bar 0; is_last means the final input bar, not the end of the run.
11
+ """
12
+
13
+ is_first: bool
14
+ is_last: bool