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,528 @@
1
+ """Native backtest configuration, lifecycle, and per-bar execution coordination."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Callable
7
+ from contextlib import contextmanager
8
+ from copy import copy
9
+ from functools import partial
10
+ from importlib.metadata import version
11
+ from numbers import Number
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ from openbacktesting.broker.commission import Commission, normalize_commission
17
+ from openbacktesting.broker.core import _Broker
18
+ from openbacktesting.broker.instrument import InstrumentSpec
19
+ from openbacktesting.broker.models import Fill, RunSummary
20
+ from openbacktesting.broker.observations import Observations
21
+ from openbacktesting.data.context import DataContext
22
+ from openbacktesting.data.core import _Data
23
+ from openbacktesting.helpers import _set_legacy_module
24
+ from openbacktesting.indicators.runtime import (
25
+ DEFAULT_CACHE_BYTES,
26
+ IndicatorCache,
27
+ optimization_cache,
28
+ )
29
+ from openbacktesting.logs import StrategyLog, _Collector
30
+ from openbacktesting.plotting.collector import Plotting
31
+ from openbacktesting.plotting.types import PlottingError, PlottingLimits
32
+ from openbacktesting.portfolio.rates import validate_risk_free_rate
33
+ from openbacktesting.portfolio.snapshot import capture_snapshot
34
+ from openbacktesting.portfolio.stats import compute_stats
35
+ from openbacktesting.portfolio.trading import trading_results
36
+ from openbacktesting.simulation.collection import DEFAULT_COLLECTION, CollectionPolicy
37
+ from openbacktesting.simulation.config import DefaultOrderSize, validate_order_size
38
+ from openbacktesting.simulation.optimization import _optimize
39
+ from openbacktesting.simulation.parallel import SharedMemoryManager
40
+ from openbacktesting.strategy.base import Strategy
41
+ from openbacktesting.strategy.inputs import _definitions, resolve_native, resolve_request
42
+ from openbacktesting.util import _tqdm
43
+
44
+
45
+ class Backtest:
46
+ """Store single-asset historical market data, strategy, and linear margin-account configuration.
47
+
48
+ data OHLC must contain finite positive prices, with an ordered unique index. instrument specifies quantity steps and an optional price grid.
49
+ cash is the initial balance; margin is the notional margin fraction; hedging controls retention of two-sided lots.
50
+ commission may be a rate, (fixed fee, rate), Commission, or pure fee function, charged once per actual execution.
51
+ spread and slippage apply to every buy/sell execution; slippage never permits orders to cross their limits.
52
+ execution_timing supports next_open or same_close; end_policy supports mark_to_market or close_at_last_close.
53
+ allow_short controls new short positions; max_gross_exposure caps gross notional/equity; valid reductions are always allowed.
54
+ exclusive_orders cancels previous entries and submits exits for existing lots when a new ordinary order is submitted.
55
+ context is optional; when provided, it requires a timezone-aware index and converts display timezone on a copy; strategies can read self.timeframe.
56
+ risk_free_rate is an annualized fraction affecting statistics only; default_order_size supplies the request budget when qty is omitted.
57
+ Low-level construction does not automatically merge @backtest settings; runtime handles declaration adoption.
58
+ No legacy size-fraction inference, price scaling, cross-currency trading, volume-based partial fills, or live execution.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ data: pd.DataFrame,
64
+ strategy: type[Strategy],
65
+ *,
66
+ cash: float = 10_000,
67
+ spread: float = 0.0,
68
+ commission: float
69
+ | tuple[float, float]
70
+ | Commission
71
+ | Callable[[float, float], float] = 0.0,
72
+ risk_free_rate: float = 0.0,
73
+ default_order_size: DefaultOrderSize | None = None,
74
+ margin: float = 1.0,
75
+ execution_timing="next_open",
76
+ instrument=None,
77
+ slippage=None,
78
+ allow_short=True,
79
+ max_gross_exposure=None,
80
+ hedging=False,
81
+ exclusive_orders=False,
82
+ end_policy="mark_to_market",
83
+ indicator_cache_bytes=DEFAULT_CACHE_BYTES,
84
+ context: DataContext | None = None,
85
+ plotting: bool = True,
86
+ plotting_limits: PlottingLimits | None = None,
87
+ _collection: CollectionPolicy = DEFAULT_COLLECTION,
88
+ ):
89
+ if type(plotting) is not bool:
90
+ raise PlottingError("invalid-option", "plotting must be bool")
91
+ if plotting_limits is not None and not isinstance(plotting_limits, PlottingLimits):
92
+ raise PlottingError("invalid-limit", "plotting_limits must be PlottingLimits")
93
+ self._plotting_enabled = plotting and _collection.plotting
94
+ self._portfolio_enabled = _collection.portfolio
95
+ self._plotting_limits = plotting_limits
96
+ self._indicator_cache_bytes = IndicatorCache(indicator_cache_bytes).budget
97
+ if not (isinstance(strategy, type) and issubclass(strategy, Strategy)):
98
+ raise TypeError("`strategy` must be a Strategy sub-type")
99
+ if not isinstance(data, pd.DataFrame):
100
+ raise TypeError("`data` must be a pandas.DataFrame with columns")
101
+ if not isinstance(spread, Number):
102
+ raise TypeError("`spread` must be a float value, percent of entry order price")
103
+ self._risk_free_rate = validate_risk_free_rate(risk_free_rate)
104
+ self._default_order_size = validate_order_size(default_order_size)
105
+ if not callable(commission):
106
+ normalize_commission(commission)
107
+
108
+ data = data.copy(deep=False)
109
+ if context is not None:
110
+ if not isinstance(context, DataContext):
111
+ raise TypeError("context must be DataContext or None")
112
+ if not isinstance(data.index, pd.DatetimeIndex) or data.index.tz is None:
113
+ raise ValueError("Market data must use a timezone-aware DatetimeIndex when context is provided")
114
+ data.index = data.index.tz_convert(context.timezone)
115
+ self._context = context
116
+
117
+ # Convert index to datetime index
118
+ if (
119
+ not isinstance(data.index, pd.DatetimeIndex)
120
+ and not isinstance(data.index, pd.RangeIndex)
121
+ and
122
+ # Numeric index with most large numbers
123
+ (
124
+ data.index.is_numeric()
125
+ and (data.index > pd.Timestamp("1975").timestamp()).mean() > 0.8
126
+ )
127
+ ):
128
+ try:
129
+ data.index = pd.to_datetime(data.index, infer_datetime_format=True)
130
+ except ValueError:
131
+ pass
132
+
133
+ if "Volume" not in data:
134
+ data["Volume"] = np.nan
135
+
136
+ if len(data) == 0:
137
+ raise ValueError("OHLC `data` is empty")
138
+ if len(data.columns.intersection({"Open", "High", "Low", "Close", "Volume"})) != 5:
139
+ raise ValueError(
140
+ "`data` must be a pandas.DataFrame with columns "
141
+ "'Open', 'High', 'Low', 'Close', and (optionally) 'Volume'"
142
+ )
143
+ if data[["Open", "High", "Low", "Close"]].isnull().values.any():
144
+ raise ValueError(
145
+ "Some OHLC values are missing (NaN). "
146
+ "Please strip those lines with `df.dropna()` or "
147
+ "fill them in with `df.interpolate()` or whatever."
148
+ )
149
+ instrument = instrument or InstrumentSpec()
150
+ instrument.validate_data(data)
151
+ if execution_timing not in ("next_open", "same_close"):
152
+ raise ValueError("execution_timing must be next_open or same_close")
153
+ if end_policy not in ("mark_to_market", "close_at_last_close"):
154
+ raise ValueError("end_policy must be mark_to_market or close_at_last_close")
155
+ if not isinstance(data.index, pd.DatetimeIndex):
156
+ warnings.warn(
157
+ "Data index is not datetime. Assuming simple periods, "
158
+ "but `pd.DateTimeIndex` is advised.",
159
+ stacklevel=2,
160
+ )
161
+
162
+ self._data: pd.DataFrame = data
163
+ self._broker = partial(
164
+ _Broker,
165
+ cash=cash,
166
+ spread=spread,
167
+ commission=commission,
168
+ margin=margin,
169
+ execution_timing=execution_timing,
170
+ instrument=instrument,
171
+ slippage=slippage,
172
+ allow_short=allow_short,
173
+ max_gross_exposure=max_gross_exposure,
174
+ hedging=hedging,
175
+ exclusive_orders=exclusive_orders,
176
+ index=data.index,
177
+ )
178
+ self._strategy = strategy
179
+ self._results: pd.Series | None = None
180
+ self._end_policy = end_policy
181
+ self._log_collector = None
182
+ self._logs_enabled = _collection.logs
183
+ self._log_running = False
184
+ self._log_delegate = None
185
+
186
+ def run_inputs(self, request) -> pd.Series:
187
+ """Submit an input request with schemaVersion/schemaHash/values keyed by unique title."""
188
+ with self._log_scope() as logs:
189
+ resolved = resolve_request(self._strategy, request)
190
+ self._log_delegate = logs
191
+ return self.run(**resolved.overrides())
192
+
193
+ def get_logs(self) -> list[dict[str, str | int | None]]:
194
+ """Return an independent copy of all strategy logs for this run; empty before any run.
195
+
196
+ Failures preserve written records; new run/run_inputs/optimize calls clear old logs.
197
+ Logs contain time, bar_index, source file, and line; retained in full by default without silent truncation.
198
+ """
199
+ logs = getattr(self, "_log_collector", None)
200
+ return [] if logs is None else logs.snapshot()
201
+
202
+ @contextmanager
203
+ def _log_scope(self):
204
+ if getattr(self, "_log_running", False):
205
+ raise RuntimeError("Backtest cannot run reentrantly")
206
+ self._log_collector = logs = _Collector(enabled=getattr(self, "_logs_enabled", True))
207
+ self._log_running = True
208
+ try:
209
+ yield logs
210
+ finally:
211
+ logs.close()
212
+ self._log_running = False
213
+ self._log_delegate = None
214
+
215
+ def run(self, **kwargs) -> pd.Series:
216
+ """Backtest strategy parameters and return a statistics Series, plain-value ledger tables, and an optional diagnostic strategy.
217
+
218
+ Public statistics keys:
219
+ Start, End, Duration, Exposure Time [%]
220
+ Equity Final [$], Equity Peak [$], Return [%], Buy & Hold Return [%]
221
+ Return (Ann.) [%], Volatility (Ann.) [%], CAGR [%], Sharpe Ratio
222
+ Sortino Ratio, Calmar Ratio, Alpha [%], Beta
223
+ Max. Drawdown [%], Avg. Drawdown [%], Max. Drawdown Duration, Avg. Drawdown Duration
224
+ # Trades, Win Rate [%], Best Trade [%], Worst Trade [%]
225
+ Avg. Trade [%], Max. Trade Duration, Avg. Trade Duration, Profit Factor
226
+ Expectancy [%], SQN, Kelly Criterion, Commissions [$]
227
+ # Closed Lots, _strategy, _equity_curve, _trades
228
+ _orders, _order_sizing, _order_events, _fills, _trade_allocations
229
+ _positions, _account_curve, _final_account, _open_trades
230
+
231
+ Order and fill events are not gated by indicator NaN; next runs on every bar starting with the first.
232
+ Retain account and gross exposure on each normal bar; record terminal handling in a separate terminal snapshot.
233
+ Result attrs contain trading-policy version, configuration, and input snapshots; _strategy is not required to reconstruct trading facts.
234
+ """
235
+ delegated = getattr(self, "_log_delegate", None)
236
+ if delegated is not None:
237
+ self._log_delegate = None
238
+ return self._run(kwargs, delegated)
239
+ with self._log_scope() as logs:
240
+ return self._run(kwargs, logs)
241
+
242
+ def _run(self, kwargs, logs):
243
+ if not hasattr(self, "_end_policy"):
244
+ raise TypeError(
245
+ "Backtest state from 0.1 cannot resume execution; reconstruct Backtest using execution_timing/end_policy"
246
+ )
247
+ resolved = resolve_native(self._strategy, kwargs)
248
+ data = _Data(self._data.copy(deep=True))
249
+ data._context = self._context
250
+ data._default_order_size = self._default_order_size
251
+ data._indicator_cache = getattr(self, "_indicator_cache", None) or IndicatorCache(
252
+ getattr(self, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)
253
+ )
254
+ broker: _Broker = self._broker(data=data)
255
+ if getattr(self, "_portfolio_enabled", True):
256
+ broker.observations = Observations()
257
+ strategy: Strategy = self._strategy(
258
+ broker, data, resolved if resolved is not None else kwargs
259
+ )
260
+
261
+ strategy._log = StrategyLog(logs)
262
+
263
+ def invoke(phase, callback, *args):
264
+ bar_index = None if strategy._bar_context is None else strategy._bar_context[0]
265
+ bar_time, time_kind = None, "none"
266
+ if bar_index is not None:
267
+ if isinstance(self._data.index, pd.DatetimeIndex):
268
+ label = self._data.index[bar_index]
269
+ bar_time = label.isoformat()
270
+ time_kind = "aware-datetime" if label.tzinfo is not None else "naive-datetime"
271
+ else:
272
+ time_kind = "ordinal"
273
+ with logs.phase(phase, bar_index, bar_time, time_kind):
274
+ return callback(*args)
275
+
276
+ collector = Plotting(
277
+ self._data.index,
278
+ strategy._indicator_cursor,
279
+ enabled=self._plotting_enabled,
280
+ limits=self._plotting_limits,
281
+ )
282
+ strategy._plotting = collector
283
+ finished = False
284
+
285
+ def notify(*, terminal=False):
286
+ # Closed notification batches: notifications produced by callbacks wait for the next notification point and cannot be drained recursively.
287
+ batch, broker._notifications = broker._notifications, []
288
+ broker.phase = "terminal" if terminal else "notify"
289
+ for event in batch:
290
+ phase = "on_fill" if isinstance(event, Fill) else "on_order"
291
+ collector._enter(phase.replace("on_", "terminal_") if terminal else phase)
292
+ invoke(
293
+ phase.replace("on_", "terminal_") if terminal else phase,
294
+ strategy.on_fill if isinstance(event, Fill) else strategy.on_order,
295
+ event,
296
+ )
297
+ collector._enter("idle")
298
+
299
+ try:
300
+ collector._enter("init")
301
+ invoke("init", strategy.init)
302
+ collector._enter("idle")
303
+ strategy._strategy_data.validate()
304
+ data._update()
305
+ broker.instrument.validate_data(data.df)
306
+ strategy._indicator_cursor.phase = "run"
307
+ data._set_length(1)
308
+ strategy._indicator_cursor.length = 1
309
+ broker.phase = "start"
310
+ collector._enter("start")
311
+ strategy._enter_bar(0, len(self._data))
312
+ invoke("on_start", strategy.on_start)
313
+ collector._enter("idle")
314
+ reason = "end_of_data"
315
+ with np.errstate(invalid="ignore"):
316
+ for i in _tqdm(
317
+ range(len(self._data)),
318
+ desc=self.run.__qualname__,
319
+ unit="bar",
320
+ mininterval=2,
321
+ miniters=100,
322
+ ):
323
+ data._set_length(i + 1)
324
+ strategy._indicator_cursor.length = i + 1
325
+ strategy._enter_bar(i, len(self._data))
326
+ broker._i = i
327
+ collector._enter("idle", i)
328
+ broker.process()
329
+ if broker.equity <= 0:
330
+ broker.capture()
331
+ reason = "insolvent"
332
+ broker.finish(liquidate=True, reason=reason)
333
+ broker._equity[i:] = broker.equity
334
+ notify(terminal=True)
335
+ break
336
+ notify()
337
+ broker.phase = "next"
338
+ collector._enter("next", i)
339
+ invoke("next", strategy.next)
340
+ collector._enter("idle")
341
+ if broker.execution_timing == "same_close":
342
+ broker.process(close=True)
343
+ notify()
344
+ broker.update_trails()
345
+ broker.capture()
346
+ else:
347
+ broker.finish(liquidate=self._end_policy == "close_at_last_close")
348
+ notify(terminal=True)
349
+ strategy._bar_context = None
350
+ broker.phase = "finish"
351
+ finished = True
352
+ collector._enter("finish")
353
+ invoke("on_finish", strategy.on_finish, RunSummary(reason, broker.account))
354
+ data._set_length(len(self._data))
355
+ strategy._indicator_cursor.length = len(self._data)
356
+ strategy._indicator_cursor.phase = "complete"
357
+ self._results = compute_stats(
358
+ trades=broker.closed_trades,
359
+ equity=broker._equity,
360
+ ohlc_data=self._data,
361
+ risk_free_rate=self._risk_free_rate,
362
+ strategy_instance=strategy,
363
+ exposure=broker._exposure,
364
+ initial_equity=broker.ledger.initial_balance,
365
+ )
366
+ self._results = trading_results(self._results, broker, self._end_policy)
367
+ self._results.attrs["statistics_risk_free_rate"] = self._risk_free_rate
368
+ self._results.attrs["statistics_initial_equity"] = broker.ledger.initial_balance
369
+ if resolved is not None:
370
+ self._results.attrs["input_snapshot"] = resolved.snapshot() | {
371
+ "engineVersion": version("openbacktesting")
372
+ }
373
+ self._results.attrs["indicator_snapshot"] = {
374
+ "engineVersion": version("openbacktesting"),
375
+ "indicators": [indicator.metadata for indicator in strategy._indicators],
376
+ }
377
+ snapshot = collector._finish(self._results, self._data, reason)
378
+ if snapshot is not None:
379
+ self._results.attrs["plotting_result"] = snapshot
380
+ self._results.attrs["strategy_logs"] = logs.snapshot()
381
+ if broker.observations is not None:
382
+ self._results.attrs["portfolio_snapshot"] = capture_snapshot(
383
+ self._results,
384
+ self._data,
385
+ broker.observations,
386
+ reason,
387
+ self._context.snapshot() if self._context else None,
388
+ )
389
+ return self._results
390
+ except BaseException as error:
391
+ if not finished:
392
+ strategy._bar_context = None
393
+ broker.phase = "finish"
394
+ finished = True
395
+ try:
396
+ collector._enter("finish")
397
+ invoke(
398
+ "on_finish",
399
+ strategy.on_finish,
400
+ RunSummary("error", broker.account, repr(error)),
401
+ )
402
+ except BaseException as finish_error: # noqa: BLE001 — preserve the primary exception and attach finish-hook failures
403
+ error.add_note(f"on_finish also failed: {finish_error!r}")
404
+ raise
405
+ finally:
406
+ strategy._bar_context = None
407
+ collector._close()
408
+ data._set_length(len(self._data))
409
+ strategy._indicator_cursor.length = len(self._data)
410
+ strategy._indicator_cursor.phase = "complete"
411
+ data._indicator_cache = None
412
+ broker.phase = "finished"
413
+
414
+ def optimize(
415
+ self,
416
+ *,
417
+ maximize: str | Callable[[pd.Series], float] = "SQN",
418
+ method: str = "grid",
419
+ max_tries: float | None = None,
420
+ constraint: Callable[[dict], bool] | None = None,
421
+ return_heatmap: bool = False,
422
+ return_optimization: bool = False,
423
+ random_state: int | None = None,
424
+ **kwargs,
425
+ ) -> pd.Series | tuple[pd.Series, pd.Series] | tuple[pd.Series, pd.Series, dict]:
426
+ """Search strategy-parameter combinations and select results by a specified statistic or scoring function.
427
+
428
+
429
+ Optimize strategy parameters to an optimal combination.
430
+ Returns result `pd.Series` of the best run.
431
+
432
+ `maximize` is a string key from the
433
+ `openbacktesting.engine.Backtest.run`-returned results series,
434
+ or a function that accepts this series object and returns a number;
435
+ the higher the better. By default, the method maximizes
436
+ Van Tharp's [System Quality Number](https://altpower.app/?q=System+Quality+Number).
437
+
438
+ `method` is the optimization method. Currently two methods are supported:
439
+
440
+ * `"grid"` which does an exhaustive (or randomized) search over the
441
+ cartesian product of parameter combinations, and
442
+ * `"sambo"` which finds close-to-optimal strategy parameters using
443
+ [model-based optimization], making at most `max_tries` evaluations.
444
+
445
+ [model-based optimization]: https://sambo-optimization.github.io
446
+
447
+ `max_tries` is the maximal number of strategy runs to perform.
448
+ If `method="grid"`, this results in randomized grid search.
449
+ If `max_tries` is a floating value between (0, 1], this sets the
450
+ number of runs to approximately that fraction of full grid space.
451
+ Alternatively, if integer, it denotes the absolute maximum number
452
+ of evaluations. If unspecified (default), grid search is exhaustive,
453
+ whereas for `method="sambo"`, `max_tries` is set to 200.
454
+
455
+ `constraint` is a function that accepts a dict-like object of
456
+ parameters (with values) and returns `True` when the combination
457
+ is admissible to test with. By default, any parameters combination
458
+ is considered admissible.
459
+
460
+ If `return_heatmap` is `True`, besides returning the result
461
+ series, an additional `pd.Series` is returned with a multiindex
462
+ of all admissible parameter combinations, which can be further
463
+ inspected as a table of parameter combinations and objective values.
464
+
465
+ If `return_optimization` is True and `method = 'sambo'`,
466
+ in addition to result series (and maybe heatmap), return raw
467
+ [`scipy.optimize.OptimizeResult`][OptimizeResult] for further
468
+ inspection by the caller.
469
+
470
+ [OptimizeResult]: https://sambo-optimization.github.io/doc/sambo/#sambo.OptimizeResult
471
+ [SAMBO]: https://sambo-optimization.github.io
472
+
473
+ If you want reproducible optimization results, set `random_state`
474
+ to a fixed integer random seed.
475
+
476
+ Additional keyword arguments represent strategy arguments with
477
+ list-like collections of possible values. For example, the following
478
+ code finds and returns the "best" of the 7 admissible (of the
479
+ 9 possible) parameter combinations:
480
+
481
+ best_stats = backtest.optimize(sma1=[5, 10, 15], sma2=[10, 20, 40],
482
+ constraint=lambda p: p.sma1 < p.sma2)
483
+ """
484
+ if getattr(self, "_log_running", False):
485
+ raise RuntimeError("Backtest cannot optimize reentrantly")
486
+ self._log_collector = None
487
+ return _optimize(
488
+ self,
489
+ Backtest._mp_task,
490
+ maximize=maximize,
491
+ method=method,
492
+ max_tries=max_tries,
493
+ constraint=constraint,
494
+ return_heatmap=return_heatmap,
495
+ return_optimization=return_optimization,
496
+ random_state=random_state,
497
+ **kwargs,
498
+ )
499
+
500
+ @staticmethod
501
+ def _mp_task(arg):
502
+ bt, data_shm, params_batch = arg
503
+ # Thread pools receive the same template object; batches must isolate mutable run state such as _data/_results.
504
+ bt = copy(bt)
505
+ bt._plotting_enabled = False
506
+ bt._logs_enabled = False
507
+ bt._log_collector = None
508
+ bt._data, shm = SharedMemoryManager.shm2df(data_shm)
509
+ try:
510
+ token = getattr(bt, "_indicator_cache_token", None)
511
+ if token is not None:
512
+ bt._indicator_cache = optimization_cache(
513
+ token, getattr(bt, "_indicator_cache_bytes", DEFAULT_CACHE_BYTES)
514
+ )
515
+ declared = bool(_definitions(bt._strategy))
516
+ return [
517
+ stats.filter(regex="^[^_]") if stats["# Trades"] else None
518
+ for stats in (
519
+ bt.run_inputs(params) if declared else bt.run(**params)
520
+ for params in params_batch
521
+ )
522
+ ]
523
+ finally:
524
+ for shmem in shm:
525
+ shmem.close()
526
+
527
+
528
+ _set_legacy_module(Backtest, "openbacktesting.engine")