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,9 @@
1
+ """Compatibility exports for the old parallel path; implemented in backtest.parallel."""
2
+
3
+ from openbacktesting.simulation.parallel import (
4
+ SharedMemory,
5
+ SharedMemoryManager,
6
+ _batch, # noqa: F401 — re-export the legacy entry point
7
+ )
8
+
9
+ __all__ = ["SharedMemory", "SharedMemoryManager"]
@@ -0,0 +1,23 @@
1
+ """Standalone plotting-data interface without rendering backends, file output, or browser operations."""
2
+
3
+ from openbacktesting.plotting.collector import Plotting
4
+ from openbacktesting.plotting.encoding import export_result
5
+ from openbacktesting.plotting.types import (
6
+ FillRef,
7
+ MarkerRef,
8
+ PaneRef,
9
+ PlotRef,
10
+ PlottingError,
11
+ PlottingLimits,
12
+ )
13
+
14
+ __all__ = [
15
+ "FillRef",
16
+ "MarkerRef",
17
+ "PaneRef",
18
+ "PlotRef",
19
+ "Plotting",
20
+ "PlottingError",
21
+ "PlottingLimits",
22
+ "export_result",
23
+ ]
@@ -0,0 +1,509 @@
1
+ """Independent static declaration registry and runtime markers per run, decoupled from trade execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from importlib.metadata import version
7
+ from numbers import Real
8
+ from typing import Literal
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+ from openbacktesting.colors import Color, _as_color
14
+ from openbacktesting.indicators.types import IndicatorCursor, IndicatorHandle
15
+ from openbacktesting.plotting.encoding import axis, facts, freeze, numeric
16
+ from openbacktesting.plotting.types import (
17
+ FillRef,
18
+ MarkerRef,
19
+ PaneRef,
20
+ PlotRef,
21
+ PlottingError,
22
+ PlottingLimits,
23
+ )
24
+
25
+ Gap = Literal["break", "connect"]
26
+
27
+
28
+ def _require(condition, code, detail):
29
+ if not condition:
30
+ raise PlottingError(code, detail)
31
+
32
+
33
+ def _number(value):
34
+ _require(
35
+ isinstance(value, Real) and not isinstance(value, (bool, np.bool_)),
36
+ "invalid-value",
37
+ "Numeric values must be real; convert booleans explicitly",
38
+ )
39
+ return value
40
+
41
+
42
+ def _rgba(value):
43
+ if value is None:
44
+ return None
45
+ try:
46
+ return _as_color(value).to_hex()
47
+ except (TypeError, ValueError) as error:
48
+ raise PlottingError(
49
+ "invalid-color", "Color must be Color, #RRGGBB, #RRGGBBAA, or None"
50
+ ) from error
51
+
52
+
53
+ class Plotting:
54
+ """Access through Strategy.plotting; init declares market-data/indicator layers, and next/on_order/on_fill add markers.
55
+
56
+ Declaration and marker id/title are optional; title defaults to the final id, and pane defaults to the main chart.
57
+ bg_color/bar_color write current-bar colors only in next, without creating identities or inheriting across bars.
58
+ References belong only to this run. Consumers autoscale the vertical axis using visible valid data.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ index: pd.Index,
64
+ cursor: IndicatorCursor,
65
+ *,
66
+ enabled: bool = True,
67
+ limits: PlottingLimits | None = None,
68
+ ):
69
+ _require(type(enabled) is bool, "invalid-option", "plotting must be bool")
70
+ _require(
71
+ limits is None or isinstance(limits, PlottingLimits),
72
+ "invalid-limit",
73
+ "plotting_limits must be PlottingLimits",
74
+ )
75
+ self._enabled = enabled
76
+ self._limits = limits or PlottingLimits()
77
+ self._index = index.copy(deep=True)
78
+ self._cursor = cursor
79
+ self._n = len(index)
80
+ self._phase = "closed"
81
+ self._bar = -1
82
+ self._counts = Counter()
83
+ self._ids = {"main"}
84
+ self._refs = {}
85
+ self._sources = {}
86
+ self._dedupe = {}
87
+ self._panes = [{"id": "main", "title": "main", "axis": "bars", "scale": "main"}]
88
+ self._plots = {}
89
+ self._fills = []
90
+ self._markers = []
91
+ self._color_events = []
92
+ self._charge("panes", 1)
93
+ self._charge("points", self._n * 7)
94
+ self._processed = np.zeros(self._n, bool) if enabled else None
95
+ self._next = np.zeros(self._n, bool) if enabled else None
96
+
97
+ def _charge(self, kind, count):
98
+ total = self._counts[kind] + count
99
+ _require(total <= getattr(self._limits, kind), "PlottingLimitExceeded", f"{kind} exceeds the budget")
100
+ self._counts[kind] = total
101
+
102
+ def _stage(self, *allowed):
103
+ _require(self._phase in allowed, "invalid-phase", f"Current phase {self._phase} does not allow this operation")
104
+
105
+ def _identity(self, cls, id, title):
106
+ counter = cls.__name__
107
+ if id is None:
108
+ id = f"auto:{counter}:{self._counts[counter] + 1}"
109
+ else:
110
+ _require(
111
+ isinstance(id, str) and bool(id) and not id.startswith("auto:") and id != "main",
112
+ "invalid-id",
113
+ "id must not be empty or use reserved main/auto: identities",
114
+ )
115
+ _require(id not in self._ids, "duplicate-id", id)
116
+ title = id if title is None else title
117
+ _require(isinstance(title, str), "invalid-title", "title must be text")
118
+ self._charge("text", len((id + title).encode()))
119
+ self._counts[counter] += 1
120
+ self._ids.add(id)
121
+ ref = cls(id, title, self)
122
+ self._refs[id] = ref
123
+ return ref
124
+
125
+ def _ref(self, ref, cls):
126
+ _require(
127
+ isinstance(ref, cls) and ref._owner is self and self._refs.get(ref.id) is ref,
128
+ "invalid-reference",
129
+ "Reference type or owning run does not match",
130
+ )
131
+ return ref.id
132
+
133
+ def _pane(self, pane):
134
+ return "main" if pane is None else self._ref(pane, PaneRef)
135
+
136
+ def pane(self, id: str | None = None, title: str | None = None) -> PaneRef:
137
+ """Create a separate pane in init; return a reference for subsequent layers to share an automatically scaled vertical axis."""
138
+ self._stage("init")
139
+ self._charge("panes", 1)
140
+ ref = self._identity(PaneRef, id, title)
141
+ self._panes.append({"id": ref.id, "title": ref.title, "axis": "bars", "scale": ref.id})
142
+ return ref
143
+
144
+ def _aligned(self, values, index):
145
+ _require(
146
+ isinstance(index, pd.Index) and index.equals(self._index),
147
+ "misaligned-source",
148
+ "index must match the complete market-data axis",
149
+ )
150
+ array = np.asarray(values)
151
+ _require(array.shape == (self._n,), "invalid-source", "A complete single-channel sequence is required")
152
+ _require(
153
+ array.dtype.kind in "iuf", "invalid-source", "A real-valued array is required; object/complex/bool are not accepted"
154
+ )
155
+ return array
156
+
157
+ def _source(self, values, valid, ready, unit, provenance):
158
+ _require(unit is None or isinstance(unit, str), "invalid-unit", "unit must be text or None")
159
+ self._charge("points", self._n)
160
+ self._charge("text", len((unit or "").encode()))
161
+ id = f"auto:source:{len(self._sources) + 1}"
162
+ self._charge("text", len(id.encode()) * 2)
163
+ row = {
164
+ "id": id,
165
+ "title": id,
166
+ "axis": "bars",
167
+ "unit": unit,
168
+ "samplingMode": "offline",
169
+ "kind": "numeric",
170
+ "provenance": provenance or {},
171
+ }
172
+ if self._enabled:
173
+ row.update(values=values.copy(), valid=valid.copy(), ready=ready.copy())
174
+ self._sources[id] = row
175
+ return id
176
+
177
+ def _adapt(self, source):
178
+ _require(
179
+ isinstance(source, IndicatorHandle) and source._cursor is self._cursor,
180
+ "invalid-source",
181
+ "A single-channel indicator/market-data handle from this run is required",
182
+ )
183
+ values = self._aligned(np.asarray(source), source._index())
184
+ # Complete views of the same buffer and channel share a source; full slices do not duplicate payloads.
185
+ key = (id(source._values), repr(source._selectors))
186
+ if key not in self._dedupe:
187
+ meta = source.metadata
188
+ provenance = {
189
+ k: v
190
+ for k, v in meta.items()
191
+ if k in {"id", "version", "backend", "unit", "kind", "field"} and isinstance(v, str)
192
+ }
193
+ if isinstance(source.name, str):
194
+ provenance["channel"] = source.name
195
+ source_id = self._source(
196
+ values,
197
+ source.valid,
198
+ source.ready,
199
+ meta.get("unit"),
200
+ provenance,
201
+ )
202
+ self._dedupe[key] = source_id
203
+ return self._dedupe[key]
204
+
205
+ def _plot(self, kind, source, id, title, pane, color, gap_policy, offset_bars, **extra):
206
+ self._stage("init")
207
+ _require(
208
+ gap_policy in ("break", "connect"), "invalid-style", "gap_policy must be break/connect"
209
+ )
210
+ _require(
211
+ type(offset_bars) is int and abs(offset_bars) <= 2**53 - 1,
212
+ "invalid-offset",
213
+ "offset_bars must be a JavaScript safe integer",
214
+ )
215
+ target = self._pane(pane)
216
+ paint = _rgba(color)
217
+ source_id = None if kind == "level" else self._adapt(source)
218
+ self._charge("plots", 1)
219
+ ref = self._identity(PlotRef, id, title)
220
+ self._plots[ref.id] = {
221
+ "id": ref.id,
222
+ "title": ref.title,
223
+ "kind": kind,
224
+ "pane": target,
225
+ "axis": "bars",
226
+ "scale": target,
227
+ "source": source_id,
228
+ "color": paint,
229
+ "gapPolicy": gap_policy,
230
+ "offsetBars": offset_bars,
231
+ **extra,
232
+ }
233
+ return ref
234
+
235
+ def line(
236
+ self,
237
+ source: IndicatorHandle,
238
+ id: str | None = None,
239
+ title: str | None = None,
240
+ pane: PaneRef | None = None,
241
+ color: Color | str | None = None,
242
+ style: Literal["line", "step"] = "line",
243
+ gap_policy: Gap = "break",
244
+ offset_bars: int = 0,
245
+ ) -> PlotRef:
246
+ """Declare a line or step line, defaulting to the main chart with broken gaps; offsets affect display only."""
247
+ _require(style in ("line", "step"), "invalid-style", "style must be line/step")
248
+ return self._plot(style, source, id, title, pane, color, gap_policy, offset_bars)
249
+
250
+ def histogram(
251
+ self,
252
+ source: IndicatorHandle,
253
+ id: str | None = None,
254
+ title: str | None = None,
255
+ pane: PaneRef | None = None,
256
+ color: Color | str | None = None,
257
+ baseline: float = 0,
258
+ gap_policy: Gap = "break",
259
+ offset_bars: int = 0,
260
+ ) -> PlotRef:
261
+ """Declare a histogram; a finite baseline contributes to consumer vertical-axis autoscaling."""
262
+ _require(np.isfinite(_number(baseline)), "invalid-value", "baseline must be finite")
263
+ return self._plot(
264
+ "histogram",
265
+ source,
266
+ id,
267
+ title,
268
+ pane,
269
+ color,
270
+ gap_policy,
271
+ offset_bars,
272
+ baseline=float(baseline),
273
+ )
274
+
275
+ def level(
276
+ self,
277
+ value: float,
278
+ id: str | None = None,
279
+ title: str | None = None,
280
+ pane: PaneRef | None = None,
281
+ color: Color | str | None = None,
282
+ ) -> PlotRef:
283
+ """Declare a finite horizontal line, storing one constant rather than duplicating a full array."""
284
+ _require(np.isfinite(_number(value)), "invalid-value", "level must be finite")
285
+ return self._plot("level", None, id, title, pane, color, "break", 0, value=float(value))
286
+
287
+ def fill(
288
+ self,
289
+ lower: PlotRef,
290
+ upper: PlotRef,
291
+ id: str | None = None,
292
+ title: str | None = None,
293
+ color: Color | str | None = None,
294
+ gap_policy: Gap = "break",
295
+ ) -> FillRef:
296
+ """Fill between two layers; endpoints must share pane, axis, offset, and compatible units."""
297
+ self._stage("init")
298
+ lo, hi = (self._plots[self._ref(r, PlotRef)] for r in (lower, upper))
299
+ _require(
300
+ all(lo[k] == hi[k] for k in ("pane", "axis", "scale", "offsetBars")),
301
+ "incompatible-fill",
302
+ "Endpoint panes/axes/offsets must match",
303
+ )
304
+ units = [self._sources[p["source"]]["unit"] for p in (lo, hi) if p["source"] is not None]
305
+ _require(len(set(units)) <= 1, "incompatible-fill", "Endpoint units are incompatible")
306
+ _require(gap_policy in ("break", "connect"), "invalid-style", "Unknown gap policy")
307
+ paint = _rgba(color)
308
+ self._charge("plots", 1)
309
+ ref = self._identity(FillRef, id, title)
310
+ self._fills.append(
311
+ {
312
+ "id": ref.id,
313
+ "title": ref.title,
314
+ "lower": lo["id"],
315
+ "upper": hi["id"],
316
+ "pane": lo["pane"],
317
+ "axis": "bars",
318
+ "scale": lo["scale"],
319
+ "color": paint,
320
+ "gapPolicy": gap_policy,
321
+ }
322
+ )
323
+ return ref
324
+
325
+ def _color(self, kind, color, pane):
326
+ self._stage("next")
327
+ target, paint = self._pane(pane), _rgba(color)
328
+ sequence = self._counts["color"] + 1
329
+ _require(sequence <= 2**53 - 1, "PlottingLimitExceeded", "Color index exceeds the safe-integer range")
330
+ self._charge("events", 1)
331
+ self._counts["color"] = sequence
332
+ if self._enabled:
333
+ self._color_events.append(
334
+ {
335
+ "kind": kind,
336
+ "pane": target,
337
+ "axis": "bars",
338
+ "anchorBar": self._bar,
339
+ "knownAtBar": self._bar,
340
+ "phase": "next",
341
+ "sequence": sequence,
342
+ "color": paint,
343
+ }
344
+ )
345
+
346
+ def bg_color(self, color: Color | str | None, *, pane: PaneRef | None = None) -> None:
347
+ """Write the current bar's pane background only in next; the last write to the same target wins.
348
+
349
+ Args:
350
+ color: Required Color, #RRGGBB, #RRGGBBAA, or None; None clears the current-bar override, while transparent colors are preserved.
351
+ pane: Main chart by default, or a separate-pane reference created in this run.
352
+
353
+ Raises:
354
+ PlottingError: Invalid phase, color, or pane ownership, or shared event budget exceeded.
355
+
356
+ No cross-bar inheritance; disabled collection still validates and counts without retaining history.
357
+ """
358
+ self._color("background", color, pane)
359
+
360
+ def bar_color(self, color: Color | str | None) -> None:
361
+ """Color the current main-chart candle only in next, applying one override to body, wick, and border.
362
+
363
+ Args:
364
+ color: Required Color, #RRGGBB, #RRGGBBAA, or None; None clears the current-bar override, while transparent colors are preserved.
365
+
366
+ Raises:
367
+ PlottingError: Invalid phase or color, or shared event budget exceeded.
368
+
369
+ The last write on a bar wins, without cross-bar inheritance; market data and automatic vertical axes remain unchanged.
370
+ """
371
+ self._color("bar", color, None)
372
+
373
+ def marker(
374
+ self,
375
+ id: str | None = None,
376
+ title: str | None = None,
377
+ pane: PaneRef | None = None,
378
+ *,
379
+ position: Literal["above_bar", "below_bar", "price"] = "above_bar",
380
+ price: float | None = None,
381
+ shape: Literal["circle", "square", "triangle_up", "triangle_down"] = "circle",
382
+ text: str = "",
383
+ color: Color | str | None = None,
384
+ anchor_bar: int | None = None,
385
+ ) -> MarkerRef:
386
+ """Record a runtime marker; anchor_bar may refer to historical bars, while knownAtBar is always the current bar."""
387
+ self._stage("next", "on_order", "on_fill", "terminal_order", "terminal_fill")
388
+ _require(
389
+ position in ("above_bar", "below_bar", "price")
390
+ and shape in ("circle", "square", "triangle_up", "triangle_down"),
391
+ "invalid-style",
392
+ "Unknown marker location or shape",
393
+ )
394
+ if position == "price":
395
+ _require(np.isfinite(_number(price)), "invalid-value", "price location requires a finite price")
396
+ price = float(price)
397
+ else:
398
+ _require(price is None, "invalid-value", "Only price location allows a price")
399
+ anchor = self._bar if anchor_bar is None else anchor_bar
400
+ _require(
401
+ type(anchor) is int and 0 <= anchor <= self._bar,
402
+ "invalid-anchor",
403
+ "anchor_bar may refer only to the current or a past bar",
404
+ )
405
+ _require(isinstance(text, str), "invalid-text", "text must be plain text")
406
+ target, paint = self._pane(pane), _rgba(color)
407
+ self._charge("events", 1)
408
+ self._charge("text", len(text.encode()))
409
+ ref = self._identity(MarkerRef, id, title)
410
+ if self._enabled:
411
+ self._markers.append(
412
+ {
413
+ "id": ref.id,
414
+ "title": ref.title,
415
+ "pane": target,
416
+ "axis": "bars",
417
+ "scale": target,
418
+ "anchorBar": anchor,
419
+ "knownAtBar": self._bar,
420
+ "phase": self._phase,
421
+ "sequence": self._counts["MarkerRef"],
422
+ "position": position,
423
+ "price": price,
424
+ "shape": shape,
425
+ "text": text,
426
+ "color": paint,
427
+ }
428
+ )
429
+ return ref
430
+
431
+ def _enter(self, phase, bar=None):
432
+ self._phase = phase
433
+ if bar is not None:
434
+ self._bar = bar
435
+ if self._enabled:
436
+ self._processed[bar] = True
437
+ if phase == "next":
438
+ self._next[bar] = True
439
+
440
+ def _finish(self, result, market, reason):
441
+ self._phase = "closed"
442
+ self._charge("events", len(result["_fills"]) + len(result["_account_curve"]))
443
+ if not self._enabled:
444
+ return None
445
+ sources = []
446
+ for row in self._sources.values():
447
+ item = {k: v for k, v in row.items() if k not in {"values", "valid", "ready"}}
448
+ item.update(numeric(row["values"], row["valid"], row["ready"]))
449
+ sources.append(item)
450
+ columns = [
451
+ "fill_id",
452
+ "order_id",
453
+ "bar_index",
454
+ "phase",
455
+ "sequence",
456
+ "side",
457
+ "qty",
458
+ "price",
459
+ "fee",
460
+ ]
461
+ account_columns = [c for c in result["_account_curve"].columns if c != "timestamp"]
462
+ payload = {
463
+ "schemaVersion": "1.1.0",
464
+ "engineVersion": version("openbacktesting"),
465
+ "axes": [axis(self._index)],
466
+ "panes": self._panes,
467
+ "scales": [
468
+ {"id": p["scale"], "pane": p["id"], "mode": "auto", "rangePolicy": "visible-valid"}
469
+ for p in self._panes
470
+ ],
471
+ "sources": sources,
472
+ "plots": list(self._plots.values()),
473
+ "fills": self._fills,
474
+ "markers": self._markers,
475
+ "colorEvents": self._color_events,
476
+ "market": {
477
+ "axis": "bars",
478
+ **{
479
+ c.lower(): numeric(
480
+ market[c].to_numpy(),
481
+ np.isfinite(market[c].to_numpy()),
482
+ np.ones(self._n, bool),
483
+ )
484
+ for c in ("Open", "High", "Low", "Close", "Volume")
485
+ },
486
+ },
487
+ "tradingFacts": {
488
+ "fills": facts(result["_fills"], columns),
489
+ "account": facts(result["_account_curve"], account_columns),
490
+ },
491
+ "execution": {
492
+ "processed": self._processed.tolist(),
493
+ "strategyNext": self._next.tolist(),
494
+ "endReason": reason,
495
+ "lastProcessedBar": self._bar,
496
+ },
497
+ "requiredFeatures": ["core-v1"] + (["bar-coloring-v1"] if self._color_events else []),
498
+ "optionalFeatures": [],
499
+ "diagnostics": [],
500
+ }
501
+ return freeze(payload, self._limits.bytes)
502
+
503
+ def _close(self):
504
+ self._phase = "closed"
505
+ self._sources.clear()
506
+ self._dedupe.clear()
507
+ self._markers.clear()
508
+ self._color_events.clear()
509
+ self._processed = self._next = None
@@ -0,0 +1,102 @@
1
+ """Freezing and strict JSON encoding; accept only completed plain-value facts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from numbers import Integral
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from openbacktesting.plotting.types import PlottingError, _Snapshot
13
+
14
+
15
+ def scalar(value):
16
+ if value is None or value is pd.NA or value is pd.NaT:
17
+ return None
18
+ if isinstance(value, (bool, np.bool_)):
19
+ return bool(value)
20
+ if isinstance(value, Integral):
21
+ value = int(value)
22
+ return value if abs(value) <= 2**53 - 1 else str(value)
23
+ if isinstance(value, (float, np.floating)):
24
+ return float(value) if math.isfinite(value) else None
25
+ if isinstance(value, pd.Timestamp):
26
+ return value.isoformat()
27
+ if isinstance(value, str):
28
+ return value
29
+ raise PlottingError("invalid-value", f"Unsupported plain-value type {type(value).__name__}")
30
+
31
+
32
+ def axis(index):
33
+ if isinstance(index, pd.DatetimeIndex):
34
+ if index.tz is not None:
35
+ return {
36
+ "id": "bars",
37
+ "kind": "utc-ns",
38
+ "timezone": str(index.tz),
39
+ "labels": [str(v.value) for v in index],
40
+ }
41
+ return {
42
+ "id": "bars",
43
+ "kind": "naive-datetime",
44
+ "timezone": None,
45
+ "labels": [v.isoformat() for v in index],
46
+ }
47
+ return {
48
+ "id": "bars",
49
+ "kind": "ordinal",
50
+ "timezone": None,
51
+ "labels": [str(scalar(v)) for v in index],
52
+ }
53
+
54
+
55
+ def numeric(values, valid, ready):
56
+ finite = np.isfinite(values)
57
+ return {
58
+ "values": [scalar(v) for v in values],
59
+ "valid": (valid & finite).tolist(),
60
+ "ready": ready.tolist(),
61
+ "missing": [None if ok else "non-finite" for ok in finite],
62
+ }
63
+
64
+
65
+ def facts(table, columns):
66
+ return [
67
+ {key: scalar(value) for key, value in zip(columns, row)}
68
+ for row in table.loc[:, columns].itertuples(index=False, name=None)
69
+ ]
70
+
71
+
72
+ def freeze(payload, byte_limit):
73
+ return _Snapshot(
74
+ "1.1.0",
75
+ json.dumps(payload, ensure_ascii=False, allow_nan=False, separators=(",", ":")),
76
+ byte_limit,
77
+ )
78
+
79
+
80
+ def export_result(result: pd.Series) -> dict:
81
+ """Return an independent PlottingResult dictionary from a successful backtest's frozen snapshot without reading the strategy instance.
82
+
83
+ Args:
84
+ result: Backtest.run or runtime.run result with collection enabled.
85
+
86
+ Returns:
87
+ Independent dictionary encodable with json.dumps(..., allow_nan=False).
88
+
89
+ Raises:
90
+ PlottingError: Missing snapshot, unsupported type or version, or export-byte budget exceeded.
91
+ """
92
+ snapshot = getattr(result, "attrs", {}).get("plotting_result")
93
+ if not isinstance(snapshot, _Snapshot):
94
+ raise PlottingError("missing-snapshot", "A successfully completed backtest result with collection enabled is required")
95
+ if snapshot.version not in ("1.0.0", "1.1.0"):
96
+ raise PlottingError("unsupported-version", snapshot.version)
97
+ if snapshot.byte_length > snapshot.byte_limit:
98
+ raise PlottingError("PlottingLimitExceeded", "bytes exceeds the export budget")
99
+ payload = json.loads(snapshot.payload)
100
+ if payload.get("schemaVersion") != snapshot.version:
101
+ raise PlottingError("unsupported-version", "Snapshot and payload versions do not match")
102
+ return payload