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,92 @@
1
+ """Immutable plotting-declaration identities, budgets, and stable errors without rendering capabilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, fields
6
+ from typing import Any
7
+
8
+
9
+ class PlottingError(ValueError):
10
+ """Plotting contract error; code is a stable category for caller dispatch."""
11
+
12
+ def __init__(self, code: str, detail: str):
13
+ self.code = code
14
+ super().__init__(f"{code}: {detail}")
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class PlottingLimits:
19
+ """Collection budgets; all limits are positive integers, and bytes is checked during explicit export.
20
+
21
+ points counts unique dense sources, including five market-data columns and two execution masks.
22
+ panes includes the main chart; plots includes fills; events includes every color call, marker, and trade fact.
23
+ text is the cumulative UTF-8 byte count of author identities, titles, units, and marker text.
24
+ """
25
+
26
+ panes: int = 32
27
+ plots: int = 256
28
+ points: int = 10_000_000
29
+ events: int = 1_000_000
30
+ text: int = 1_048_576
31
+ bytes: int = 268_435_456
32
+
33
+ def __post_init__(self):
34
+ for field in fields(self):
35
+ value = getattr(self, field.name)
36
+ if type(value) is not int or value <= 0:
37
+ raise PlottingError("invalid-limit", f"{field.name} must be a positive integer")
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class _Ref:
42
+ id: str
43
+ title: str
44
+ _owner: Any
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class PaneRef(_Ref):
49
+ """Separate-pane identity for the current run; pass this reference only to pane arguments."""
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class PlotRef(_Ref):
54
+ """Line, histogram, or horizontal-line identity usable as a fill endpoint."""
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class FillRef(_Ref):
59
+ """Fill identity between two compatible layers."""
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class MarkerRef(_Ref):
64
+ """Identity of one runtime marker."""
65
+
66
+
67
+ class _Snapshot(tuple):
68
+ """Immutable plain-value tuple; the entire result.attrs remains JSON-encodable."""
69
+
70
+ __slots__ = ()
71
+
72
+ def __new__(cls, version: str, payload: str, byte_limit: int):
73
+ return super().__new__(cls, (version, payload, byte_limit, len(payload.encode())))
74
+
75
+ def __getnewargs__(self):
76
+ return self[:3]
77
+
78
+ @property
79
+ def version(self):
80
+ return self[0]
81
+
82
+ @property
83
+ def payload(self):
84
+ return self[1]
85
+
86
+ @property
87
+ def byte_limit(self):
88
+ return self[2]
89
+
90
+ @property
91
+ def byte_length(self):
92
+ return self[3]
@@ -0,0 +1,15 @@
1
+ """Frozen account, trading, and report data for a single backtest."""
2
+
3
+ from .snapshot import PortfolioSnapshot as PortfolioSnapshot
4
+ from .snapshot import export_result as export_result
5
+ from .snapshot import get_snapshot as get_snapshot
6
+
7
+ __all__ = ["PortfolioSnapshot", "export_result", "get_snapshot", "report_catalog"]
8
+
9
+
10
+ def report_catalog() -> dict:
11
+ """Return an independent copy of the report-field catalog for hosts to check coverage and method identity."""
12
+ import json
13
+ from importlib.resources import files
14
+
15
+ return json.loads(files(__package__).joinpath("catalog.json").read_text(encoding="utf-8"))
@@ -0,0 +1,245 @@
1
+ """Deterministic trade distributions, streaks, time patterns, and equity intervals."""
2
+
3
+ from decimal import ROUND_FLOOR, Decimal
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from openbacktesting.portfolio.metrics import mean
9
+ from openbacktesting.portfolio.models import metric, ratio
10
+
11
+
12
+ def distribution(trades):
13
+ bins = {}
14
+ for t in trades:
15
+ key = int(
16
+ (Decimal(str(t["returnFraction"])) / Decimal("0.01")).to_integral_value(
17
+ rounding=ROUND_FLOOR
18
+ )
19
+ )
20
+ bins.setdefault(key, []).append(t["fragmentId"])
21
+ # Sparse buckets avoid huge numbers of empty buckets from extreme returns; each bucket has explicit boundaries.
22
+ histogram = [
23
+ {
24
+ "lower": float(Decimal(k) * Decimal("0.01")),
25
+ "upper": float(Decimal(k + 1) * Decimal("0.01")),
26
+ "count": len(ids),
27
+ "fragmentIds": ids,
28
+ }
29
+ for k, ids in sorted(bins.items())
30
+ ]
31
+ values = [t["returnFraction"] for t in trades]
32
+ q1, q3 = np.quantile(values, [0.25, 0.75]) if len(values) >= 4 else (None, None)
33
+ low, high = (q1 - 1.5 * (q3 - q1), q3 + 1.5 * (q3 - q1)) if q1 is not None else (None, None)
34
+ outliers = [
35
+ t
36
+ for t in trades
37
+ if low is not None and (t["returnFraction"] < low or t["returnFraction"] > high)
38
+ ]
39
+ return {
40
+ "histogram": histogram,
41
+ "interval": "left_closed_right_open",
42
+ "winners": sum(t["pnlNet"] > 0 for t in trades),
43
+ "losers": sum(t["pnlNet"] < 0 for t in trades),
44
+ "evens": sum(t["pnlNet"] == 0 for t in trades),
45
+ "averageProfitReturn": mean([t["returnFraction"] for t in trades if t["pnlNet"] > 0]),
46
+ "averageLossReturn": mean([-t["returnFraction"] for t in trades if t["pnlNet"] < 0]),
47
+ "outliers": {
48
+ "methodId": "tukey_return_iqr_v1",
49
+ "lower": low,
50
+ "upper": high,
51
+ "fragmentIds": [t["fragmentId"] for t in outliers],
52
+ "count": len(outliers),
53
+ "pnl": metric(
54
+ sum(t["pnlNet"] for t in outliers) if low is not None else None,
55
+ "money",
56
+ status="ok" if low is not None else "insufficient_data",
57
+ reason="fewer_than_four_samples",
58
+ ),
59
+ },
60
+ }
61
+
62
+
63
+ def streaks(trades):
64
+ rows = []
65
+ active = None
66
+ for t in trades:
67
+ sign = int(t["pnlNet"] > 0) - int(t["pnlNet"] < 0)
68
+ if not sign:
69
+ active = None
70
+ continue
71
+ if active is None or active["sign"] != sign:
72
+ active = {
73
+ "id": f"streak-{len(rows) + 1}",
74
+ "sign": sign,
75
+ "count": 0,
76
+ "amount": 0.0,
77
+ "startFragmentId": t["fragmentId"],
78
+ "endFragmentId": t["fragmentId"],
79
+ "current": False,
80
+ }
81
+ rows.append(active)
82
+ active["count"] += 1
83
+ active["amount"] += t["pnlNet"]
84
+ active["endFragmentId"] = t["fragmentId"]
85
+ if active is not None:
86
+ active["current"] = True
87
+ summary = {}
88
+ for label, sign in (("winning", 1), ("losing", -1)):
89
+ lengths = [r["count"] for r in rows if r["sign"] == sign]
90
+ amounts = [abs(r["amount"]) for r in rows if r["sign"] == sign]
91
+ summary[label] = {
92
+ "longest": max(lengths, default=0),
93
+ "average": mean(lengths),
94
+ "maximumAmount": max(amounts, default=0.0),
95
+ "averageAmount": mean(amounts),
96
+ }
97
+ return {"rows": rows, "summary": summary, "methodId": "zero_breaks_streak_v1"}
98
+
99
+
100
+ def time_patterns(trades, index):
101
+ if not isinstance(index, pd.DatetimeIndex):
102
+ return {"status": "not_applicable", "reason": "ordinal_index", "groups": {}}
103
+ result = {}
104
+ for name, attr, keys in (
105
+ ("hours", "hour", range(24)),
106
+ ("days", "dayofweek", range(7)),
107
+ ("months", "month", range(1, 13)),
108
+ ):
109
+ groups = {key: [] for key in keys}
110
+ for t in trades:
111
+ groups[getattr(index[t["entry"]["barIndex"]], attr)].append(t)
112
+ rows = []
113
+ for key, group in groups.items():
114
+ wins = sum(t["pnlNet"] > 0 for t in group)
115
+ rows.append(
116
+ {
117
+ "key": key,
118
+ "count": len(group),
119
+ "wins": wins,
120
+ "losses": sum(t["pnlNet"] < 0 for t in group),
121
+ "evens": sum(t["pnlNet"] == 0 for t in group),
122
+ "winRate": ratio(wins, len(group)),
123
+ "pnl": sum(t["pnlNet"] for t in group),
124
+ "averageBars": mean([t["barsHeld"] for t in group]),
125
+ }
126
+ )
127
+ best = max((r["winRate"] for r in rows if r["winRate"] is not None), default=None)
128
+ result[name] = {
129
+ "rows": rows,
130
+ "bestKeys": [r["key"] for r in rows if best is not None and r["winRate"] == best],
131
+ }
132
+ return {"status": "ok", "reason": None, "groups": result}
133
+
134
+
135
+ def episodes(rows, initial):
136
+ if not rows:
137
+ return {"drawdowns": [], "alternating": []}
138
+
139
+ def duration(start, end):
140
+ a, b = rows[start]["timestamp"], rows[end]["timestamp"]
141
+ return str((b - a).value) if isinstance(a, pd.Timestamp) else None
142
+
143
+ def item(start, end, kind, amount, fraction, current, extreme):
144
+ return {
145
+ "kind": kind,
146
+ "start": rows[start]["observationId"],
147
+ "end": rows[end]["observationId"],
148
+ "extreme": rows[extreme]["observationId"],
149
+ "amount": amount,
150
+ "fraction": fraction,
151
+ "durationNs": duration(start, end),
152
+ "current": current,
153
+ "recovered": not current,
154
+ }
155
+
156
+ drawdowns = []
157
+ peak, peak_i, active, lowest, low_i = initial, 0, False, initial, 0
158
+ for i, row in enumerate(rows):
159
+ value = row["equity"]
160
+ if value >= peak:
161
+ if active:
162
+ drawdowns.append(
163
+ item(
164
+ peak_i,
165
+ i,
166
+ "drawdown",
167
+ peak - lowest,
168
+ ratio(peak - lowest, peak),
169
+ False,
170
+ low_i,
171
+ )
172
+ )
173
+ peak, peak_i, active, lowest = value, i, False, value
174
+ else:
175
+ active = True
176
+ if value < lowest:
177
+ lowest, low_i = value, i
178
+ if active:
179
+ drawdowns.append(
180
+ item(
181
+ peak_i,
182
+ len(rows) - 1,
183
+ "drawdown",
184
+ peak - lowest,
185
+ ratio(peak - lowest, peak),
186
+ True,
187
+ low_i,
188
+ )
189
+ )
190
+ alternating = []
191
+ start, sign = 0, 0
192
+ for i in range(1, len(rows)):
193
+ delta = rows[i]["equity"] - rows[i - 1]["equity"]
194
+ new = int(delta > 0) - int(delta < 0)
195
+ if not new:
196
+ continue
197
+ if sign and new != sign:
198
+ a, b = rows[start]["equity"], rows[i - 1]["equity"]
199
+ alternating.append(
200
+ item(
201
+ start,
202
+ i - 1,
203
+ "runup" if sign > 0 else "decline",
204
+ abs(b - a),
205
+ abs(b - a) / a if a > 0 else None,
206
+ False,
207
+ i - 1,
208
+ )
209
+ )
210
+ start = i - 1
211
+ elif not sign:
212
+ start = i - 1
213
+ sign = new
214
+ if sign:
215
+ a, b = rows[start]["equity"], rows[-1]["equity"]
216
+ alternating.append(
217
+ item(
218
+ start,
219
+ len(rows) - 1,
220
+ "runup" if sign > 0 else "decline",
221
+ abs(b - a),
222
+ abs(b - a) / a if a > 0 else None,
223
+ True,
224
+ len(rows) - 1,
225
+ )
226
+ )
227
+ summary = {}
228
+ for name, group in (
229
+ ("drawdown", drawdowns),
230
+ ("growth", [r for r in alternating if r["kind"] == "runup"]),
231
+ ("decline", [r for r in alternating if r["kind"] == "decline"]),
232
+ ):
233
+ summary[name] = {
234
+ "maximum": max((r["amount"] for r in group), default=0.0),
235
+ "average": mean([r["amount"] for r in group]),
236
+ "maximumFraction": max(
237
+ (r["fraction"] for r in group if r["fraction"] is not None), default=None
238
+ ),
239
+ "averageFraction": mean([r["fraction"] for r in group if r["fraction"] is not None]),
240
+ "averageDurationNs": str(sum(int(r["durationNs"]) for r in group) // len(group))
241
+ if group and all(r["durationNs"] is not None for r in group)
242
+ else None,
243
+ "current": next((r for r in group if r["current"]), None),
244
+ }
245
+ return {"drawdowns": drawdowns, "alternating": alternating, "summary": summary}