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,381 @@
1
+ """Assemble Portfolio once; project trade facts only after simulation ends."""
2
+
3
+ import math
4
+ from types import SimpleNamespace
5
+
6
+ import pandas as pd
7
+
8
+ from openbacktesting.portfolio.analysis import distribution, episodes, streaks, time_patterns
9
+ from openbacktesting.portfolio.metrics import mean, monthly_risk, path_metrics, trade_metrics
10
+ from openbacktesting.portfolio.models import metric, ratio, time_ref
11
+ from openbacktesting.portfolio.periods import benchmarks, periodical
12
+
13
+
14
+ def contributions(rows, facts, initial):
15
+ fills = {f["fill_id"]: f for f in facts["fills"]}
16
+ grouped = {}
17
+ for a in facts["allocations"]:
18
+ f = fills[a["fill_id"]]
19
+ side = "long" if fills[a["entry_fill_id"]]["side"] == "buy" else "short"
20
+ grouped.setdefault(f["sequence"], []).append((side, a["realized_pnl_gross"] - a["fee"]))
21
+ pending = iter(sorted(grouped.items()))
22
+ current = next(pending, None)
23
+ balance = {"long": 0.0, "short": 0.0}
24
+ result = {"long": [], "short": []}
25
+ for row in rows:
26
+ while current is not None and current[0] <= row["fillSequence"]:
27
+ for side, value in current[1]:
28
+ balance[side] += value
29
+ current = next(pending, None)
30
+ for side, sign in (("long", 1), ("short", -1)):
31
+ qty = row[side + "_qty"]
32
+ avg = row[side + "_avg_price"]
33
+ unrealized = sign * qty * (row["price"] - avg) if avg is not None else 0.0
34
+ result[side].append(
35
+ {
36
+ "observationId": row["observationId"],
37
+ "barIndex": row["barIndex"],
38
+ "timestamp": row["timestamp"],
39
+ "phase": row["phase"],
40
+ "contribution": balance[side] + unrealized,
41
+ "equity": initial + balance[side] + unrealized,
42
+ }
43
+ )
44
+ return result
45
+
46
+
47
+ def legacy(stats):
48
+ return {
49
+ str(k): metric(v, "native", method="engine_legacy_v1")
50
+ for k, v in stats.items()
51
+ if not k.startswith("_")
52
+ }
53
+
54
+
55
+ def build_report(source):
56
+ stats = SimpleNamespace(attrs=source["attrs"])
57
+ kind, timezone = source["timeKind"], source["timezone"]
58
+ if kind == "ordinal":
59
+ index = pd.Index(source["index"])
60
+ else:
61
+ index = pd.to_datetime([int(t) for t in source["index"]], utc=kind == "aware")
62
+ if timezone:
63
+ index = index.tz_convert(timezone)
64
+ data = pd.DataFrame({"Close": source["close"]}, index=index)
65
+ initial = stats.attrs["statistics_initial_equity"]
66
+ facts, closed, opened = source["facts"], source["closed"], source["opened"]
67
+ rows = [dict(zip(source["columns"], row)) for row in source["rows"]]
68
+ expanded, id_map = [], {}
69
+ for row in rows:
70
+ row["timestamp"] = index[row["barIndex"]]
71
+ eligible = row.pop("rangeEligible")
72
+ if eligible:
73
+ for price in (source["high"][row["barIndex"]], source["low"][row["barIndex"]]):
74
+ extreme = row.copy()
75
+ gross = row["unrealized_pnl_gross"] + row["net_qty"] * (price - row["price"])
76
+ equity = row["balance"] + gross
77
+ used = row["gross_qty"] * price * stats.attrs["margin"]
78
+ cost = sum(
79
+ row[side + "_qty"] * (row[side + "_avg_price"] or 0)
80
+ for side in ("long", "short")
81
+ )
82
+ extreme.update(
83
+ price=price,
84
+ phase="bar_range",
85
+ ordered=False,
86
+ equity=equity,
87
+ unrealized_pnl_gross=gross,
88
+ unrealized_pnl_net=gross - row["entry_fees_remaining"],
89
+ margin_used=used,
90
+ margin_available=max(0.0, equity - used),
91
+ gross_notional=row["gross_qty"] * price,
92
+ net_notional=row["net_qty"] * price,
93
+ return_fraction=(gross - row["entry_fees_remaining"]) / cost
94
+ if cost and row["avg_price"] is not None
95
+ else None,
96
+ )
97
+ expanded.append(extreme)
98
+ old_id = row["observationId"]
99
+ expanded.append(row)
100
+ id_map[old_id] = f"observation-{len(expanded)}"
101
+ rows = expanded
102
+ for i, row in enumerate(rows, 1):
103
+ row.update(observationId=f"observation-{i}", observationSequence=i)
104
+ for lot in opened:
105
+ lot["asOf"] = id_map[lot["asOf"]]
106
+ for table in (facts["fills"], facts["orderEvents"]):
107
+ for row in table:
108
+ row["timestamp"] = index[row["bar_index"]]
109
+ reason, context = source["reason"], source["context"]
110
+ # Snapshot curves retain each bar's final ordinary close and a separate terminal snapshot; intermediate observations in the same close phase do not overwrite them.
111
+ bar_rows = {r["barIndex"]: r for r in rows if r["phase"] == "bar"}
112
+ close_rows = [bar_rows[i] for i in sorted(bar_rows)] + [
113
+ r for r in rows if r["phase"] == "terminal"
114
+ ]
115
+ end = rows[-1]["barIndex"]
116
+ benchmark = benchmarks(
117
+ data, rows, facts["fills"], facts["allocations"], stats.attrs["instrument"], initial
118
+ )
119
+ periods = periodical(
120
+ rows, closed, facts["fills"], benchmark["input_start_close"], initial, data.index
121
+ )
122
+ direction = contributions(rows, facts, initial)
123
+ summary = {}
124
+ directional_monthly = {}
125
+ close_ids = {v["observationId"] for v in close_rows}
126
+ for side in ("all", "long", "short"):
127
+ trades = [t for t in closed if side == "all" or t["direction"] == side]
128
+ lots = [t for t in opened if side == "all" or t["direction"] == side]
129
+ values = trade_metrics(trades, lots)
130
+ path = (
131
+ close_rows
132
+ if side == "all"
133
+ else [r for r in direction[side] if r["observationId"] in close_ids]
134
+ )
135
+ values.update(path_metrics(path, initial))
136
+ final = path[-1]["equity"]
137
+ for key, val, unit in (
138
+ ("initialCapital", initial, "money"),
139
+ ("equityFinal", final, "money"),
140
+ ("equityPeak", max([initial] + [r["equity"] for r in path]), "money"),
141
+ ("totalPnl", final - initial, "money"),
142
+ ("totalReturn", (final - initial) / initial, "ratio"),
143
+ ("openPnlGross", sum(t["openPnlGross"] for t in lots), "money"),
144
+ ("openPnlNet", sum(t["openPnlNet"] for t in lots), "money"),
145
+ ("remainingEntryFees", sum(t["remainingEntryFee"] for t in lots), "money"),
146
+ (
147
+ "feesPaid",
148
+ sum(t["feeTotal"] for t in trades) + sum(t["remainingEntryFee"] for t in lots),
149
+ "money",
150
+ ),
151
+ ("closedPnlGross", sum(t["pnlGross"] for t in trades), "money"),
152
+ (
153
+ "totalPnlGross",
154
+ sum(t["pnlGross"] for t in trades) + sum(t["openPnlGross"] for t in lots),
155
+ "money",
156
+ ),
157
+ ("closedReturn", sum(t["pnlNet"] for t in trades) / initial, "ratio"),
158
+ ):
159
+ values[key] = metric(
160
+ val,
161
+ unit,
162
+ method="shared_initial_capital_v1" if side != "all" else "openbacktesting_portfolio_v1",
163
+ )
164
+ if side != "all":
165
+ monthly = []
166
+ if isinstance(data.index, pd.DatetimeIndex):
167
+ endpoints = {}
168
+ for row in path:
169
+ endpoints[data.index[row["barIndex"]].strftime("%Y-%m")] = row["equity"]
170
+ previous = initial
171
+ for key in periods["buckets"].get("monthly", []):
172
+ stamp = pd.Timestamp(int(key["start"]["value"]), tz="UTC")
173
+ if data.index.tz:
174
+ stamp = stamp.tz_convert(data.index.tz)
175
+ value = endpoints.get(stamp.strftime("%Y-%m"), previous)
176
+ monthly.append({"equityReturn": value / previous - 1 if previous > 0 else None})
177
+ previous = value
178
+ values.update(monthly_risk(monthly, stats.attrs["statistics_risk_free_rate"]))
179
+ directional_monthly[side] = monthly
180
+ summary[side] = values
181
+ all_ = summary["all"]
182
+ last = rows[-1]
183
+ all_["balance"] = metric(last["balance"], "money")
184
+ all_["feesPaid"] = metric(last["fees_paid"], "money")
185
+ all_["commissionLoad"] = metric(
186
+ ratio(last["fees_paid"], sum(abs(f["qty"] * f["price"]) for f in facts["fills"])),
187
+ method="commission_load_gross_activity_v1",
188
+ reason="zero_denominator",
189
+ )
190
+ average_margin = mean([r["margin_used"] for r in bar_rows.values()])
191
+ all_["averageMarginUsed"] = metric(average_margin, "money", method="bar_close_mean_v1")
192
+ all_["maxMarginUsed"] = metric(
193
+ max(r["margin_used"] for r in rows), "money", method="execution_samples_v1"
194
+ )
195
+ for key, field in (
196
+ ("maxGrossQuantity", "gross_qty"),
197
+ ("maxLongQuantity", "long_qty"),
198
+ ("maxShortQuantity", "short_qty"),
199
+ ("maxNetQuantity", "net_qty"),
200
+ ):
201
+ all_[key] = metric(
202
+ max(abs(r[field]) for r in rows), "quantity", method="execution_samples_v1"
203
+ )
204
+ all_.update(path_metrics(rows, initial, prefix="intrabar.", approximate=True))
205
+ required = max(0.0, max(r["margin_used"] - (r["equity"] - initial) for r in rows))
206
+ all_["observedCapitalRequired"] = metric(
207
+ required, "money", method="observed_capital_requirement_v1"
208
+ )
209
+ net = all_["closedNetPnl"]["value"]
210
+ for key, denominator in (
211
+ ("returnOnInitialCapital", initial),
212
+ ("returnOnMaxDrawdown", all_["intrabar.maxDrawdown"]["value"]),
213
+ ("netPnlToLargestLoss", all_["largestLoss"]["value"]),
214
+ ("marginEfficiency", average_margin),
215
+ ("returnOnObservedCapital", required),
216
+ ):
217
+ all_[key] = metric(
218
+ ratio(net, denominator),
219
+ method="observed_capital_efficiency_v1",
220
+ reason="zero_denominator",
221
+ )
222
+ for key in (
223
+ "marginCalls",
224
+ "totalMarginLiquidatedVolume",
225
+ "largestMarginLiquidatedVolume",
226
+ "tvAccountSizeRequired",
227
+ "tvReturnOnAccountSize",
228
+ "currencyConversion",
229
+ "contractMultiplier",
230
+ ):
231
+ all_[key] = metric(
232
+ None,
233
+ "count" if key == "marginCalls" else "money",
234
+ status="unsupported",
235
+ reason="unsupported_trading_model",
236
+ )
237
+ all_.update(
238
+ monthly_risk(
239
+ periods["buckets"].get("monthly", []), stats.attrs["statistics_risk_free_rate"]
240
+ )
241
+ )
242
+ years = (
243
+ (data.index[end] - data.index[0]).total_seconds() / (365.25 * 86400)
244
+ if isinstance(data.index, pd.DatetimeIndex)
245
+ else 0.0
246
+ )
247
+ exponent = math.log(last["equity"] / initial) / years if years and last["equity"] > 0 else None
248
+ cagr = math.expm1(exponent) if exponent is not None and exponent < 709 else None
249
+ all_["cagr"] = metric(cagr, reason="nonpositive_equity_or_duration")
250
+ all_["buyHoldPnl"] = metric(benchmark["input_start_close"]["points"][-1]["pnlMoney"], "money")
251
+ all_["buyHoldReturn"] = metric(benchmark["input_start_close"]["points"][-1]["returnFraction"])
252
+ all_["outperformance"] = metric(benchmark["input_start_close"]["points"][-1]["outperformance"])
253
+ all_["correlation"] = periods["correlations"].get(
254
+ "daily", metric(None, status="not_applicable", reason="ordinal_index")
255
+ )
256
+ breakdown = {}
257
+ for grouping in ("signal", "direction"):
258
+ groups = {}
259
+ for t in closed:
260
+ key = t["entry"]["signal"] if grouping == "signal" else t["direction"]
261
+ groups.setdefault(key, []).append(t)
262
+ for t in opened:
263
+ key = t["entry"]["signal"] if grouping == "signal" else t["direction"]
264
+ groups.setdefault(key, [])
265
+ breakdown[grouping] = [
266
+ {
267
+ "key": key,
268
+ "metrics": trade_metrics(
269
+ group,
270
+ [
271
+ t
272
+ for t in opened
273
+ if (t["entry"]["signal"] if grouping == "signal" else t["direction"]) == key
274
+ ],
275
+ ),
276
+ "openPnlNet": sum(
277
+ t["openPnlNet"]
278
+ for t in opened
279
+ if (t["entry"]["signal"] if grouping == "signal" else t["direction"]) == key
280
+ ),
281
+ }
282
+ for key, group in groups.items()
283
+ ]
284
+ terminal = {}
285
+ for why in ("insolvent", "end_liquidation"):
286
+ terminal[why] = {
287
+ "quantity": sum(f["qty"] for f in facts["fills"] if f["reason"] == why),
288
+ "notional": sum(f["qty"] * f["price"] for f in facts["fills"] if f["reason"] == why),
289
+ }
290
+ payload = {
291
+ "schemaVersion": "1.0.0",
292
+ "semantics": {
293
+ "metricsDefinitionVersion": "openbacktesting_portfolio_v1",
294
+ "executionPolicyVersion": stats.attrs["execution_policy_version"],
295
+ "tradingSchemaVersion": stats.attrs["trading_schema_version"],
296
+ "engineVersion": stats.attrs["engine_version"],
297
+ "tradingViewCompatibility": "not_verified",
298
+ "riskFreeRate": stats.attrs["statistics_risk_free_rate"],
299
+ },
300
+ "run": {
301
+ "runId": None,
302
+ "strategyRef": None,
303
+ "datasetRef": None,
304
+ "inputRange": [time_ref(data.index[0]), time_ref(data.index[-1])],
305
+ "executedRange": [time_ref(data.index[0]), time_ref(data.index[end])],
306
+ "processedThroughBar": end,
307
+ "terminationReason": reason,
308
+ "complete": reason == "end_of_data",
309
+ },
310
+ "context": {
311
+ "instrument": stats.attrs["instrument"],
312
+ "timezone": str(data.index.tz)
313
+ if isinstance(data.index, pd.DatetimeIndex) and data.index.tz
314
+ else None,
315
+ "timeKind": time_ref(data.index[0])["kind"],
316
+ "dataContext": context,
317
+ "settings": {
318
+ k: stats.attrs[k]
319
+ for k in (
320
+ "commission",
321
+ "spread",
322
+ "slippage",
323
+ "margin",
324
+ "end_policy",
325
+ "execution_timing",
326
+ "hedging",
327
+ "allow_short",
328
+ "max_gross_exposure",
329
+ )
330
+ },
331
+ "inputs": stats.attrs.get("input_snapshot"),
332
+ "backtest": stats.attrs.get("backtest_snapshot"),
333
+ },
334
+ "facts": facts,
335
+ "trades": {"closedFragments": closed, "openLots": opened},
336
+ "summary": summary,
337
+ "series": {
338
+ "account": rows,
339
+ "close": close_rows,
340
+ "directions": direction,
341
+ "benchmark": benchmark,
342
+ "closedCumulative": [
343
+ {
344
+ "fragmentId": t["fragmentId"],
345
+ "value": t["cumulativeClosedPnl"],
346
+ "fraction": t["fractionOfInitialCapital"],
347
+ }
348
+ for t in closed
349
+ ],
350
+ "margin": [
351
+ {
352
+ "observationId": r["observationId"],
353
+ "margin": r["margin_used"],
354
+ "utilization": r["margin_used"] / r["equity"] if r["equity"] > 0 else None,
355
+ }
356
+ for r in rows
357
+ ],
358
+ },
359
+ "analysis": {
360
+ "directionalMonthly": directional_monthly,
361
+ "breakdown": breakdown,
362
+ "periodical": periods,
363
+ "distribution": distribution(closed),
364
+ "streaks": streaks(closed),
365
+ "timePatterns": time_patterns(closed, data.index),
366
+ "growthAndDecline": episodes(close_rows, initial),
367
+ "liquidations": terminal,
368
+ },
369
+ "extensions": {"legacy": source["legacy"]},
370
+ "diagnostics": list(stats.attrs["diagnostics"]),
371
+ }
372
+ for table in facts.values():
373
+ for row in table:
374
+ if "timestamp" in row:
375
+ row["timestamp"] = time_ref(row["timestamp"])
376
+ for row in rows:
377
+ row["timestamp"] = time_ref(row["timestamp"])
378
+ for curve in direction.values():
379
+ for row in curve:
380
+ row["timestamp"] = time_ref(row["timestamp"])
381
+ return payload