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,272 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, cast
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from openbacktesting.broker.models import finite
9
+ from openbacktesting.data.core import _data_period
10
+ from openbacktesting.indicators.types import IndicatorHandle
11
+ from openbacktesting.portfolio.rates import validate_risk_free_rate
12
+
13
+ if TYPE_CHECKING:
14
+ from openbacktesting.broker.core import Trade
15
+ from openbacktesting.strategy.base import Strategy
16
+
17
+
18
+ def compute_drawdown_duration_peaks(dd: pd.Series):
19
+ iloc = np.unique(
20
+ np.r_[[-1] if dd.iloc[0] > 0 else [], (dd == 0).values.nonzero()[0], len(dd) - 1]
21
+ )
22
+ iloc = pd.Series(iloc.astype(np.intp))
23
+ df = iloc.to_frame("iloc").assign(prev=iloc.shift())
24
+ df = df[(df["iloc"] > df["prev"] + 1) | (df["prev"] == -1)].astype(np.int64)
25
+
26
+ # If no drawdown since no trade, avoid below for pandas sake and return nan series
27
+ if not len(df):
28
+ return (dd.replace(0, np.nan),) * 2
29
+
30
+ df["duration"] = df["iloc"].map(dd.index.__getitem__) - df["prev"].clip(lower=0).map(
31
+ dd.index.__getitem__
32
+ )
33
+ df["peak_dd"] = df.apply(
34
+ lambda row: dd.iloc[max(0, row["prev"]) : row["iloc"] + 1].max(), axis=1
35
+ )
36
+ df.index = dd.index[df["iloc"]]
37
+ df = df.reindex(dd.index)
38
+ return df["duration"], df["peak_dd"]
39
+
40
+
41
+ def geometric_mean(returns: pd.Series) -> float:
42
+ returns = returns.fillna(0) + 1
43
+ if np.any(returns <= 0):
44
+ return 0
45
+ return np.exp(np.log(returns).sum() / (len(returns) or np.nan)) - 1
46
+
47
+
48
+ def compute_stats(
49
+ trades: list[Trade] | pd.DataFrame,
50
+ equity: np.ndarray,
51
+ ohlc_data: pd.DataFrame,
52
+ strategy_instance: Strategy | None,
53
+ risk_free_rate: float = 0,
54
+ exposure: np.ndarray | None = None,
55
+ initial_equity: float = cast(float, ...),
56
+ ) -> pd.Series:
57
+ risk_free_rate = validate_risk_free_rate(risk_free_rate)
58
+ initial_equity = finite(
59
+ equity[0] if initial_equity is Ellipsis else initial_equity,
60
+ "initial_equity",
61
+ positive=True,
62
+ )
63
+
64
+ index = ohlc_data.index
65
+ dd = 1 - equity / np.maximum(initial_equity, np.maximum.accumulate(equity))
66
+ dd_dur, dd_peaks = compute_drawdown_duration_peaks(pd.Series(dd, index=index))
67
+
68
+ equity_df = pd.DataFrame(
69
+ {"Equity": equity, "DrawdownPct": dd, "DrawdownDuration": dd_dur}, index=index
70
+ )
71
+
72
+ if isinstance(trades, pd.DataFrame):
73
+ trades_df: pd.DataFrame = trades
74
+ commissions = None # Not shown
75
+ else:
76
+ # Came straight from Backtest.run()
77
+ trades_df = pd.DataFrame(
78
+ {
79
+ "Size": [t.size for t in trades],
80
+ "EntryBar": [t.entry_bar for t in trades],
81
+ "ExitBar": [t.exit_bar for t in trades],
82
+ "EntryPrice": [t.entry_price for t in trades],
83
+ "ExitPrice": [t.exit_price for t in trades],
84
+ "SL": [t.sl for t in trades],
85
+ "TP": [t.tp for t in trades],
86
+ "PnL": [t.pl for t in trades],
87
+ "Commission": [t._commissions for t in trades],
88
+ "ReturnPct": [t.return_fraction for t in trades],
89
+ "EntryTime": [t.entry_time for t in trades],
90
+ "ExitTime": [t.exit_time for t in trades],
91
+ }
92
+ )
93
+ trades_df["Duration"] = trades_df["ExitTime"] - trades_df["EntryTime"]
94
+ trades_df["Tag"] = [t.tag for t in trades]
95
+
96
+ # Add indicator values
97
+ if len(trades_df) and strategy_instance:
98
+ for ind in strategy_instance._indicators:
99
+ name = ind.name
100
+ ind = np.atleast_2d(ind._view() if isinstance(ind, IndicatorHandle) else ind)
101
+ for i, values in enumerate(ind): # multi-d indicators
102
+ suffix = f"_{i}" if len(ind) > 1 else ""
103
+ trades_df[f"Entry_{name}{suffix}"] = values[trades_df["EntryBar"].values]
104
+ trades_df[f"Exit_{name}{suffix}"] = values[trades_df["ExitBar"].values]
105
+
106
+ commissions = sum(t._commissions for t in trades)
107
+ del trades
108
+
109
+ pl = trades_df["PnL"]
110
+ returns = trades_df["ReturnPct"]
111
+ durations = trades_df["Duration"]
112
+
113
+ def _round_timedelta(value, _period=_data_period(index)): # noqa: B008 — freeze defaults at initialization, preserving native behavior
114
+ if not isinstance(value, pd.Timedelta) or pd.isna(_period):
115
+ return value
116
+ resolution = getattr(_period, "resolution_string", None) or _period.resolution
117
+ return value.ceil(resolution)
118
+
119
+ s = {}
120
+ s["Start"] = index[0]
121
+ s["End"] = index[-1]
122
+ s["Duration"] = s["End"] - s["Start"]
123
+
124
+ s["Exposure Time [%]"] = np.nan if exposure is None else exposure.mean() * 100
125
+ s["Equity Final [$]"] = equity[-1]
126
+ s["Equity Peak [$]"] = max(initial_equity, equity.max())
127
+ if commissions:
128
+ s["Commissions [$]"] = commissions
129
+ s["Return [%]"] = (equity[-1] - initial_equity) / initial_equity * 100
130
+ c = ohlc_data.Close.values
131
+ s["Buy & Hold Return [%]"] = (c[-1] - c[0]) / c[0] * 100
132
+
133
+ gmean_day_return: float = 0
134
+ day_returns = np.array(np.nan)
135
+ annual_trading_days = np.nan
136
+ is_datetime_index = isinstance(index, pd.DatetimeIndex)
137
+ if is_datetime_index:
138
+ freq_days = cast(pd.Timedelta, _data_period(index)).days
139
+ have_weekends = index.dayofweek.to_series().between(5, 6).mean() > 2 / 7 * 0.6
140
+ annual_trading_days = (
141
+ 52
142
+ if freq_days == 7
143
+ else 12
144
+ if freq_days == 31
145
+ else 1
146
+ if freq_days == 365
147
+ else (365 if have_weekends else 252)
148
+ )
149
+ freq = {7: "W", 31: "ME", 365: "YE"}.get(freq_days, "D")
150
+ period_equity = equity_df["Equity"].resample(freq).last().dropna()
151
+ day_returns = period_equity.pct_change()
152
+ day_returns.iloc[0] = period_equity.iloc[0] / initial_equity - 1
153
+ gmean_day_return = geometric_mean(day_returns)
154
+
155
+ # Annualized return and risk metrics are computed based on the (mostly correct)
156
+ # assumption that the returns are compounded. See: https://dx.doi.org/10.2139/ssrn.3054517
157
+ # Our annualized return matches `empyrical.annual_return(day_returns)` whereas
158
+ # our risk doesn't; they use the simpler approach below.
159
+ annualized_return = (1 + gmean_day_return) ** annual_trading_days - 1
160
+ s["Return (Ann.) [%]"] = annualized_return * 100
161
+ s["Volatility (Ann.) [%]"] = (
162
+ np.sqrt(
163
+ (day_returns.var(ddof=int(bool(day_returns.shape))) + (1 + gmean_day_return) ** 2)
164
+ ** annual_trading_days
165
+ - (1 + gmean_day_return) ** (2 * annual_trading_days)
166
+ )
167
+ * 100
168
+ )
169
+ # d['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
170
+ # d['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
171
+ if is_datetime_index:
172
+ time_in_years = (s["Duration"].days + s["Duration"].seconds / 86400) / 365.25
173
+ s["CAGR [%]"] = (
174
+ ((s["Equity Final [$]"] / initial_equity) ** (1 / time_in_years) - 1) * 100
175
+ if time_in_years and equity[-1] > 0
176
+ else np.nan
177
+ )
178
+
179
+ # Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
180
+ # and simple standard deviation
181
+ s["Sharpe Ratio"] = (s["Return (Ann.) [%]"] - risk_free_rate * 100) / (
182
+ s["Volatility (Ann.) [%]"] or np.nan
183
+ )
184
+ # Our Sortino mismatches `empyrical.sortino_ratio()` because they use arithmetic mean return
185
+ with np.errstate(divide="ignore"):
186
+ s["Sortino Ratio"] = (annualized_return - risk_free_rate) / (
187
+ np.sqrt(np.mean(day_returns.clip(-np.inf, 0) ** 2)) * np.sqrt(annual_trading_days)
188
+ )
189
+ max_dd = -np.nan_to_num(dd.max())
190
+ s["Calmar Ratio"] = annualized_return / (-max_dd or np.nan)
191
+ with np.errstate(divide="ignore", invalid="ignore"): # Preserve deficits instead of forcing them to zero
192
+ equity_log_returns = np.log(equity[1:] / equity[:-1])
193
+ market_log_returns = np.log(c[1:] / c[:-1])
194
+ beta = np.nan
195
+ if len(equity_log_returns) > 1 and len(market_log_returns) > 1:
196
+ # len == 0 on dummy call `stats_keys = compute_stats(...)` pre optimization
197
+ cov_matrix = np.cov(equity_log_returns, market_log_returns)
198
+ beta = cov_matrix[0, 1] / cov_matrix[1, 1]
199
+ # Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large
200
+ s["Alpha [%]"] = (
201
+ s["Return [%]"]
202
+ - risk_free_rate * 100
203
+ - beta * (s["Buy & Hold Return [%]"] - risk_free_rate * 100)
204
+ )
205
+ s["Beta"] = beta
206
+ s["Max. Drawdown [%]"] = max_dd * 100
207
+ s["Avg. Drawdown [%]"] = -dd_peaks.mean() * 100
208
+ s["Max. Drawdown Duration"] = _round_timedelta(dd_dur.max())
209
+ s["Avg. Drawdown Duration"] = _round_timedelta(dd_dur.mean())
210
+ s["# Trades"] = n_trades = len(trades_df)
211
+ win_rate = np.nan if not n_trades else (pl > 0).mean()
212
+ s["Win Rate [%]"] = win_rate * 100
213
+ s["Best Trade [%]"] = returns.max() * 100
214
+ s["Worst Trade [%]"] = returns.min() * 100
215
+ mean_return = geometric_mean(returns)
216
+ s["Avg. Trade [%]"] = mean_return * 100
217
+ s["Max. Trade Duration"] = _round_timedelta(durations.max())
218
+ s["Avg. Trade Duration"] = _round_timedelta(durations.mean())
219
+ s["Profit Factor"] = returns[returns > 0].sum() / (abs(returns[returns < 0].sum()) or np.nan)
220
+ s["Expectancy [%]"] = returns.mean() * 100
221
+ s["SQN"] = np.sqrt(n_trades) * pl.mean() / (pl.std() or np.nan)
222
+ s["Kelly Criterion"] = win_rate - (1 - win_rate) / (pl[pl > 0].mean() / -pl[pl < 0].mean())
223
+
224
+ s["_strategy"] = strategy_instance
225
+ s["_equity_curve"] = equity_df
226
+ s["_trades"] = trades_df
227
+
228
+ s = _Stats(s, dtype=object)
229
+ s.attrs["statistics_initial_equity"] = initial_equity
230
+ return s
231
+
232
+
233
+ class _Stats(pd.Series):
234
+ def __repr__(self):
235
+ with pd.option_context(
236
+ "display.max_colwidth",
237
+ 20, # Prevent expansion due to _equity and _trades dfs
238
+ "display.max_rows",
239
+ len(self), # Reveal self whole
240
+ "display.precision",
241
+ 5, # Enough for my eyes at least
242
+ ):
243
+ # XXX: .fillna(nan) to replace None which aren't na_rep'd otherwise (pandas 3.0).
244
+ # Replace with proper na_rep option when that becomes available.
245
+ # https://github.com/pandas-dev/pandas/issues/66407
246
+ return super().fillna(np.nan).to_string(na_rep="--", dtype=True)
247
+
248
+
249
+ def dummy_stats():
250
+ from openbacktesting.broker.core import Trade, _Broker
251
+
252
+ index = pd.DatetimeIndex(["2025"])
253
+ data = pd.DataFrame({col: [np.nan] for col in ("Close",)}, index=index)
254
+ trade = Trade(
255
+ _Broker(
256
+ data=data,
257
+ cash=10000,
258
+ spread=0.01,
259
+ commission=0.01,
260
+ margin=0.1,
261
+ hedging=True,
262
+ exclusive_orders=False,
263
+ index=index,
264
+ ),
265
+ 1,
266
+ 1,
267
+ 0,
268
+ None,
269
+ )
270
+ trade.exit_price, trade.exit_bar = 1, 0
271
+ trade._exit_commission = np.nan
272
+ return compute_stats([trade], np.r_[[1.0]], data, None, 0, initial_equity=1.0)
@@ -0,0 +1,136 @@
1
+ """Report projections of orders, allocations, and lots without live objects."""
2
+
3
+ import json
4
+
5
+ import pandas as pd
6
+
7
+ from openbacktesting.portfolio.models import plain, time_ref
8
+
9
+
10
+ def signal(tag):
11
+ if isinstance(tag, str) and tag:
12
+ return tag, None
13
+ if isinstance(tag, dict):
14
+ name = tag.get("signal")
15
+ comment = tag.get("comment")
16
+ return (
17
+ name if isinstance(name, str) and name else None,
18
+ comment if isinstance(comment, str) else None,
19
+ )
20
+ return None, None
21
+
22
+
23
+ def excursion(state, quantity, entry_price):
24
+ return {
25
+ "mfeMoney": state["mfe"] * quantity,
26
+ "maeMoney": state["mae"] * quantity,
27
+ "mfeFraction": state["mfe"] / entry_price,
28
+ "maeFraction": state["mae"] / entry_price,
29
+ "mfeBar": state["mfeBar"],
30
+ "maeBar": state["maeBar"],
31
+ "coverage": "partial" if state["partial"] else "complete_under_bar_model",
32
+ "methodId": "sampled_bounds_v1",
33
+ "reason": "ambiguous_intrabar_order" if state["partial"] else None,
34
+ }
35
+
36
+
37
+ def project_trades(stats, observations):
38
+ facts = {
39
+ key: stats[field].astype(object).where(stats[field].notna(), None).to_dict("records")
40
+ for key, field in (
41
+ ("orders", "_orders"),
42
+ ("orderSizing", "_order_sizing"),
43
+ ("orderEvents", "_order_events"),
44
+ ("fills", "_fills"),
45
+ ("allocations", "_trade_allocations"),
46
+ )
47
+ }
48
+ orders = {r["order_id"]: r for r in facts["orders"]}
49
+ fills = {r["fill_id"]: r for r in facts["fills"]}
50
+
51
+ def leg(fill):
52
+ tag = json.loads(orders[fill["order_id"]]["tag_json"])
53
+ name, comment = signal(tag)
54
+ return {
55
+ "fillId": fill["fill_id"],
56
+ "orderId": fill["order_id"],
57
+ "barIndex": fill["bar_index"],
58
+ "time": time_ref(fill["timestamp"]),
59
+ "phase": fill["phase"],
60
+ "sequence": fill["sequence"],
61
+ "price": fill["price"],
62
+ "signal": name,
63
+ "comment": comment,
64
+ }
65
+
66
+ extras = {r["allocation_id"]: r for r in stats["_trades"].to_dict("records")}
67
+ closed = []
68
+ cumulative = 0.0
69
+ for allocation in facts["allocations"]:
70
+ if allocation["kind"] != "close":
71
+ continue
72
+ entry, exit_ = fills[allocation["entry_fill_id"]], fills[allocation["fill_id"]]
73
+ qty = allocation["qty"]
74
+ cost = qty * entry["price"]
75
+ pnl = allocation["realized_pnl_gross"] - allocation["entry_fee"] - allocation["fee"]
76
+ cumulative += pnl
77
+ elapsed = exit_["bar_index"] - entry["bar_index"]
78
+ duration = (
79
+ (exit_["timestamp"] - entry["timestamp"]).value
80
+ if isinstance(entry["timestamp"], pd.Timestamp)
81
+ else None
82
+ )
83
+ closed.append(
84
+ {
85
+ "fragmentId": allocation["allocation_id"],
86
+ "lotId": allocation["trade_id"],
87
+ "displayOrdinal": len(closed) + 1,
88
+ "direction": "long" if entry["side"] == "buy" else "short",
89
+ "status": "closed",
90
+ "entry": leg(entry),
91
+ "exit": leg(exit_),
92
+ "quantity": qty,
93
+ "signedQuantity": qty if entry["side"] == "buy" else -qty,
94
+ "entryNotional": cost,
95
+ "exitNotional": qty * exit_["price"],
96
+ "pnlGross": allocation["realized_pnl_gross"],
97
+ "pnlNet": pnl,
98
+ "entryFeeAllocated": allocation["entry_fee"],
99
+ "exitFee": allocation["fee"],
100
+ "feeTotal": allocation["entry_fee"] + allocation["fee"],
101
+ "returnFraction": pnl / cost,
102
+ "barsElapsed": elapsed,
103
+ "barsHeld": elapsed + 1,
104
+ "durationNs": str(duration) if duration is not None else None,
105
+ "cumulativeClosedPnl": cumulative,
106
+ "fractionOfInitialCapital": cumulative / stats.attrs["statistics_initial_equity"],
107
+ "excursion": excursion(
108
+ observations.closed[allocation["allocation_id"]], qty, entry["price"]
109
+ ),
110
+ "extensions": plain(extras[allocation["allocation_id"]]),
111
+ }
112
+ )
113
+ opened = []
114
+ for t in stats["_open_trades"].to_dict("records"):
115
+ entry = fills[t["entry_fill_id"]]
116
+ opened.append(
117
+ {
118
+ "lotId": t["trade_id"],
119
+ "status": "open",
120
+ "direction": "long" if t["Size"] > 0 else "short",
121
+ "entry": leg(entry),
122
+ "exit": None,
123
+ "remainingQuantity": abs(t["Size"]),
124
+ "remainingEntryFee": t["EntryFeeRemaining"],
125
+ "openPnlGross": t["PnLGross"],
126
+ "openPnlNet": t["PnL"],
127
+ "returnFraction": t["ReturnPct"],
128
+ "markPrice": observations.rows[-1]["price"],
129
+ "asOf": observations.rows[-1]["observationId"],
130
+ "excursion": excursion(
131
+ observations.lots[t["trade_id"]], abs(t["Size"]), entry["price"]
132
+ ),
133
+ "tag": plain(t["Tag"]),
134
+ }
135
+ )
136
+ return facts, closed, opened
@@ -0,0 +1,215 @@
1
+ """Generate plain-value result tables with fixed columns from trading facts."""
2
+
3
+ from dataclasses import asdict
4
+ from importlib.metadata import version
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from openbacktesting.broker.commission import Commission
10
+ from openbacktesting.broker.models import (
11
+ AccountSnapshot,
12
+ Fill,
13
+ NormalizedOrderRequest,
14
+ OrderSizingSnapshot,
15
+ OrderSnapshot,
16
+ PositionSnapshot,
17
+ TradeAllocation,
18
+ columns,
19
+ record,
20
+ )
21
+
22
+ _FLOAT_FIELDS = {
23
+ "value",
24
+ "reference_price",
25
+ "budget",
26
+ "qty",
27
+ "price",
28
+ "fee",
29
+ "filled_qty",
30
+ "remaining_qty",
31
+ "avg_fill_price",
32
+ "limit",
33
+ "stop",
34
+ "sl",
35
+ "tp",
36
+ "entry_fee",
37
+ "realized_pnl_gross",
38
+ "balance",
39
+ "equity",
40
+ "unrealized_pnl_gross",
41
+ "fees_paid",
42
+ "margin_used",
43
+ "margin_available",
44
+ "gross_notional",
45
+ "net_notional",
46
+ "net_qty",
47
+ "gross_qty",
48
+ "long_qty",
49
+ "short_qty",
50
+ "long_avg_price",
51
+ "short_avg_price",
52
+ "avg_price",
53
+ "unrealized_pnl_net",
54
+ "entry_fees_remaining",
55
+ "return_fraction",
56
+ "Size",
57
+ "EntryPrice",
58
+ "EntryFeeRemaining",
59
+ "PnLGross",
60
+ "PnL",
61
+ "ReturnPct",
62
+ }
63
+ _INT_FIELDS = {"bar_index", "created_bar", "sequence", "EntryBar"}
64
+ _BOOL_FIELDS = {"triggered", "reduce_only"}
65
+ _OBJECT_FIELDS = {"timestamp", "expires_at", "EntryTime", "Tag"}
66
+
67
+
68
+ def _table(rows, *, columns):
69
+ # Construct columns once with target dtypes; avoid inference followed by per-column conversion/replacement of entire DataFrame blocks.
70
+ data = {}
71
+ for name in columns:
72
+ values = [row.get(name) for row in rows]
73
+ if name in _FLOAT_FIELDS:
74
+ data[name] = np.asarray(values, dtype="float64")
75
+ elif name in _INT_FIELDS:
76
+ data[name] = np.asarray(values, dtype="int64")
77
+ elif name in _BOOL_FIELDS:
78
+ data[name] = np.asarray(values, dtype=bool)
79
+ elif name in _OBJECT_FIELDS:
80
+ # Series preserves object dtype; passing arrays directly makes DataFrame infer timestamps/nested tags again.
81
+ data[name] = pd.Series(values, dtype="object")
82
+ else:
83
+ data[name] = pd.array(values, dtype="Int64" if name == "valid_bars" else "string")
84
+ return pd.DataFrame(data, columns=columns)
85
+
86
+
87
+ def trading_results(stats, broker, end_policy):
88
+ """Attach ledger tables and traceable configuration without reading trade records from _strategy."""
89
+ original = stats
90
+ stats = dict(stats)
91
+ order_columns = (
92
+ columns(OrderSnapshot)
93
+ + columns(NormalizedOrderRequest)
94
+ + ["group_id", "kind", "created_bar"]
95
+ )
96
+ stats["_orders"] = _table(
97
+ [
98
+ record(o.snapshot)
99
+ | record(o.request)
100
+ | {"group_id": o.group_id, "kind": o.kind, "created_bar": o.created_bar}
101
+ for o in broker.all_orders
102
+ ],
103
+ columns=order_columns,
104
+ )
105
+ stats["_order_sizing"] = _table(
106
+ [
107
+ {"order_id": o.order_id} | record(o.sizing)
108
+ for o in broker.all_orders
109
+ if o.sizing is not None
110
+ ],
111
+ columns=["order_id"] + columns(OrderSizingSnapshot),
112
+ )
113
+ event_columns = ["sequence", "bar_index", "timestamp", "phase"] + columns(OrderSnapshot)
114
+ stats["_order_events"] = _table(
115
+ [
116
+ {
117
+ "sequence": e.sequence,
118
+ "bar_index": e.bar_index,
119
+ "timestamp": e.timestamp,
120
+ "phase": e.phase,
121
+ }
122
+ | record(e.snapshot)
123
+ for e in broker.events
124
+ ],
125
+ columns=event_columns,
126
+ )
127
+ for name, records, model in (
128
+ ("_fills", broker.fills, Fill),
129
+ ("_trade_allocations", broker.allocations, TradeAllocation),
130
+ ):
131
+ stats[name] = _table([record(item) for item in records], columns=columns(model))
132
+ prefix = ["bar_index", "timestamp", "phase"]
133
+ stats["_positions"] = _table(broker.position_curve, columns=prefix + columns(PositionSnapshot))
134
+ stats["_account_curve"] = _table(
135
+ broker.account_curve, columns=prefix + columns(AccountSnapshot)
136
+ )
137
+ stats["_final_account"] = record(broker.account)
138
+ stats["_open_trades"] = _table(
139
+ [
140
+ {
141
+ "trade_id": t.trade_id,
142
+ "entry_fill_id": t.entry_fill_id,
143
+ "Size": t.size,
144
+ "EntryPrice": t.entry_price,
145
+ "EntryBar": t.entry_bar,
146
+ "EntryTime": t.entry_time,
147
+ "EntryFeeRemaining": t._entry_commission,
148
+ "PnLGross": t.pl_gross,
149
+ "PnL": t.pl,
150
+ "ReturnPct": t.return_fraction,
151
+ "Tag": t.tag,
152
+ }
153
+ for t in broker.trades
154
+ ],
155
+ columns=[
156
+ "trade_id",
157
+ "entry_fill_id",
158
+ "Size",
159
+ "EntryPrice",
160
+ "EntryBar",
161
+ "EntryTime",
162
+ "EntryFeeRemaining",
163
+ "PnLGross",
164
+ "PnL",
165
+ "ReturnPct",
166
+ "Tag",
167
+ ],
168
+ )
169
+ stats["_trades"]["trade_id"] = pd.Series(
170
+ [t.trade_id for t in broker.closed_trades], dtype="string"
171
+ )
172
+ stats["_trades"]["entry_fill_id"] = pd.Series(
173
+ [t.entry_fill_id for t in broker.closed_trades], dtype="string"
174
+ )
175
+ for name in ("allocation_id", "exit_fill_id", "exit_sequence"):
176
+ stats["_trades"][name] = pd.Series(
177
+ [getattr(t, name) for t in broker.closed_trades],
178
+ dtype="int64" if name == "exit_sequence" else "string",
179
+ )
180
+ stats["Commissions [$]"] = broker.ledger.fees_paid
181
+ stats["# Closed Lots"] = len(
182
+ {t.trade_id for t in broker.closed_trades} - {t.trade_id for t in broker.trades}
183
+ )
184
+ commission = broker._commission_model
185
+ if isinstance(commission, Commission):
186
+ commission = commission.snapshot()
187
+ elif callable(commission):
188
+ commission = {
189
+ "reconstructable": False,
190
+ "name": getattr(commission, "__qualname__", type(commission).__name__),
191
+ }
192
+ broker.diagnostics.append({"code": "commission_not_reconstructable"})
193
+ result = type(original)(stats, dtype=object)
194
+ result.attrs.update(original.attrs)
195
+ result.attrs.update(
196
+ {
197
+ "trading_schema_version": 4,
198
+ "execution_policy_version": "2",
199
+ "engine_version": version("openbacktesting"),
200
+ "instrument": asdict(broker.instrument),
201
+ "account_model": "linear_margin",
202
+ "execution_timing": broker.execution_timing,
203
+ "end_policy": end_policy,
204
+ "commission": commission,
205
+ "spread": broker._spread,
206
+ "slippage": asdict(broker.slippage),
207
+ "margin": broker.margin,
208
+ "allow_short": broker.allow_short,
209
+ "max_gross_exposure": broker.max_gross_exposure,
210
+ "hedging": broker._hedging,
211
+ "diagnostics": list(broker.diagnostics),
212
+ }
213
+ )
214
+
215
+ return result
File without changes