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,1312 @@
1
+ import inspect
2
+ import multiprocessing as mp
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ import unittest
8
+ from concurrent.futures.process import ProcessPoolExecutor
9
+ from contextlib import contextmanager
10
+ from functools import partial
11
+ from glob import glob
12
+ from pathlib import Path
13
+ from tempfile import gettempdir
14
+ from unittest import TestCase
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ from pandas.testing import assert_frame_equal
19
+
20
+ from openbacktesting import Backtest as _Backtest
21
+ from openbacktesting import Strategy
22
+ from openbacktesting._util import _Array, _as_str, _Indicator, patch, try_
23
+ from openbacktesting.lib import (
24
+ OHLCV_AGG,
25
+ FractionalBacktest,
26
+ MultiBacktest,
27
+ SignalStrategy,
28
+ TrailingStrategy,
29
+ barssince,
30
+ compute_stats,
31
+ quantile,
32
+ random_ohlc_data,
33
+ resample_apply,
34
+ )
35
+ from openbacktesting.portfolio.stats import compute_drawdown_duration_peaks
36
+ from openbacktesting.strategy.sizing import equity_fraction
37
+ from openbacktesting.test import BTCUSD, EURUSD, GOOG, SMA
38
+
39
+ SHORT_DATA = GOOG.iloc[:20] # Short data for fast tests with no indicator lag
40
+
41
+ # Avoid the 'Some trades remain open' warning in many tests
42
+ Backtest = partial(_Backtest, end_policy="close_at_last_close")
43
+
44
+
45
+ @contextmanager
46
+ def chdir(path):
47
+ cwd = os.getcwd()
48
+ os.chdir(path)
49
+ try:
50
+ yield
51
+ finally:
52
+ os.chdir(cwd)
53
+
54
+
55
+ class SmaCross(Strategy):
56
+ # NOTE: These values are also used on the website!
57
+ fast = 10
58
+ slow = 30
59
+
60
+ def init(self):
61
+ self.sma1 = self.ind(SMA, self.data.Close, self.fast)
62
+ self.sma2 = self.ind(SMA, self.data.Close, self.slow)
63
+
64
+ def next(self):
65
+ if self.ta.is_crossover(self.sma1, self.sma2):
66
+ self.position.close()
67
+ self.buy(
68
+ qty=equity_fraction(
69
+ 0.95,
70
+ account=self.account,
71
+ reference_price=self.close[-1],
72
+ instrument=self.instrument,
73
+ ).qty
74
+ )
75
+ elif self.ta.is_crossunder(self.sma1, self.sma2):
76
+ self.position.close()
77
+ self.sell(
78
+ qty=equity_fraction(
79
+ 0.95,
80
+ account=self.account,
81
+ reference_price=self.close[-1],
82
+ instrument=self.instrument,
83
+ ).qty
84
+ )
85
+
86
+
87
+ class _S(Strategy):
88
+ def init(self):
89
+ super().init()
90
+
91
+
92
+ class TestBacktest(TestCase):
93
+ def test_run(self):
94
+ bt = Backtest(EURUSD, SmaCross)
95
+ bt.run()
96
+
97
+ def test_run_invalid_param(self):
98
+ bt = Backtest(GOOG, SmaCross)
99
+ self.assertRaises(AttributeError, bt.run, foo=3)
100
+
101
+ def test_run_speed(self):
102
+ # Measure CPU performance thresholds with an uninstrumented interpreter; full regression coverage still captures functionality and branches.
103
+ process = subprocess.run(
104
+ [
105
+ sys.executable,
106
+ "-I",
107
+ "-c",
108
+ """
109
+ import sys
110
+ import time
111
+ from openbacktesting.test._test import Backtest, GOOG, SmaCross
112
+ assert "coverage" not in sys.modules
113
+ bt = Backtest(GOOG, SmaCross)
114
+ start = time.process_time()
115
+ bt.run()
116
+ print(time.process_time() - start)
117
+ """,
118
+ ],
119
+ env={
120
+ key: value for key, value in os.environ.items() if not key.startswith("COVERAGE_")
121
+ },
122
+ capture_output=True,
123
+ text=True,
124
+ check=True,
125
+ timeout=30,
126
+ )
127
+ self.assertLess(float(process.stdout), 0.3)
128
+
129
+ def test_data_missing_columns(self):
130
+ df = GOOG.copy(deep=False)
131
+ del df["Open"]
132
+ with self.assertRaises(ValueError):
133
+ Backtest(df, SmaCross).run()
134
+
135
+ def test_data_nan_columns(self):
136
+ df = GOOG.copy()
137
+ df["Open"] = np.nan
138
+ with self.assertRaises(ValueError):
139
+ Backtest(df, SmaCross).run()
140
+
141
+ def test_data_extra_columns(self):
142
+ df = GOOG.copy(deep=False)
143
+ df["P/E"] = np.arange(len(df))
144
+ df["MCap"] = np.arange(len(df))
145
+
146
+ class S(Strategy):
147
+ def init(self):
148
+ assert len(self.data.MCap) == len(self.data.Close)
149
+ assert len(self.data["P/E"]) == len(self.data.Close)
150
+
151
+ def next(self):
152
+ assert len(self.data.MCap) == len(self.data.Close)
153
+ assert len(self.data["P/E"]) == len(self.data.Close)
154
+
155
+ Backtest(df, S).run()
156
+
157
+ def test_data_invalid(self):
158
+ with self.assertRaises(TypeError):
159
+ Backtest(GOOG.index, SmaCross).run()
160
+ with self.assertRaises(ValueError):
161
+ Backtest(GOOG.iloc[:0], SmaCross).run()
162
+
163
+ def test_assertions(self):
164
+ class Assertive(Strategy):
165
+ def init(self):
166
+ self.sma = self.ind(SMA, self.data.Close, 10)
167
+ self.remains_indicator = self.ind(
168
+ lambda: np.r_[2] * np.cumsum(self.sma * 5 + 1) * np.r_[2]
169
+ )
170
+
171
+ self.transpose_invalid = self.ind(
172
+ lambda: np.column_stack((self.data.Open, self.data.Close))
173
+ )
174
+
175
+ resampled = resample_apply("W", SMA, self.data.Close, 3)
176
+ resampled_ind = resample_apply("W", SMA, self.sma, 3)
177
+ assert np.unique(resampled[-5:]).size == 1
178
+ assert np.unique(resampled[-6:]).size == 2
179
+ assert any(item is resampled for item in self._indicators), (
180
+ "Strategy.ind not called"
181
+ )
182
+ assert any(item is resampled_ind for item in self._indicators), (
183
+ "Strategy.ind not called"
184
+ )
185
+
186
+ assert 1 == try_(lambda: self.data.X, 1, AttributeError)
187
+ assert 1 == try_(lambda: self.data["X"], 1, KeyError)
188
+
189
+ assert self.data.pip == 0.01
190
+
191
+ assert float(self.data.Close) == self.data.Close[-1]
192
+
193
+ def next(self, _FEW_DAYS=pd.Timedelta("3 days")): # noqa: B008 — freeze defaults at initialization, preserving native behavior
194
+ assert self.equity >= 0
195
+
196
+ from openbacktesting.indicator_types import IndicatorHandle
197
+
198
+ assert isinstance(self.sma, IndicatorHandle)
199
+ assert isinstance(self.remains_indicator, IndicatorHandle)
200
+ assert self.remains_indicator.name
201
+ assert isinstance(self.remains_indicator._opts, dict)
202
+
203
+ assert not np.isnan(self.data.Open[-1])
204
+ assert not np.isnan(self.data.High[-1])
205
+ assert not np.isnan(self.data.Low[-1])
206
+ assert not np.isnan(self.data.Close[-1])
207
+ assert not np.isnan(self.data.Volume[-1])
208
+ assert bool(self.sma.valid[-1]) == bool(np.isfinite(self.sma[-1]))
209
+ assert self.data.index[-1]
210
+
211
+ _ = self.position
212
+ _ = self.position.size
213
+ _ = self.position.pl
214
+ _ = self.position.return_fraction
215
+ _ = self.position.is_long
216
+
217
+ if self.ta.is_crossover(self.sma, self.data.Close):
218
+ for order in self.orders:
219
+ if not order.is_contingent:
220
+ order.cancel()
221
+ price = self.data.Close[-1]
222
+ sl, tp = 1.05 * price, 0.9 * price
223
+
224
+ n_orders = len(self.orders)
225
+ self.sell(
226
+ qty=equity_fraction(
227
+ 0.21,
228
+ account=self.account,
229
+ reference_price=self.close[-1],
230
+ instrument=self.instrument,
231
+ ).qty,
232
+ limit=price,
233
+ stop=price,
234
+ sl=sl,
235
+ tp=tp,
236
+ )
237
+ assert len(self.orders) == n_orders + 1
238
+
239
+ order = self.orders[-1]
240
+ assert order.limit == price
241
+ assert order.stop == price
242
+ assert (
243
+ order.size
244
+ == -equity_fraction(0.21, account=self.account, reference_price=price).qty
245
+ )
246
+ assert order.sl == sl
247
+ assert order.tp == tp
248
+ assert not order.is_contingent
249
+
250
+ elif self.position:
251
+ assert not self.position.is_long
252
+ assert self.position.is_short
253
+ assert self.position.pl
254
+ assert self.position.return_fraction
255
+ assert self.position.size < 0
256
+
257
+ trade = self.trades[0]
258
+ if self.data.index[-1] - self.data.index[trade.entry_bar] > _FEW_DAYS:
259
+ assert not trade.is_long
260
+ assert trade.is_short
261
+ assert trade.size < 0
262
+ assert trade.entry_bar > 0
263
+ assert isinstance(trade.entry_time, pd.Timestamp)
264
+ assert trade.exit_bar is None
265
+ assert trade.exit_time is None
266
+ assert trade.entry_price > 0
267
+ assert trade.exit_price is None
268
+ assert trade.pl / 1
269
+ assert trade.return_fraction / 1
270
+ assert trade.value > 0
271
+ assert trade.sl
272
+ assert trade.tp
273
+ # Close multiple times
274
+ self.position.close(0.5 if abs(self.position.size) >= 2 else 1)
275
+ self.position.close(0.5 if abs(self.position.size) >= 2 else 1)
276
+ self.position.close(0.5 if abs(self.position.size) >= 2 else 1)
277
+ self.position.close()
278
+ self.position.close()
279
+
280
+ bt = Backtest(GOOG, Assertive)
281
+ stats = bt.run()
282
+ self.assertEqual(stats["# Trades"], 267) # Policy v2 baseline for explicit quantities and partial exits rounded toward zero
283
+
284
+ def test_broker_params(self):
285
+ bt = Backtest(
286
+ GOOG.iloc[:100],
287
+ SmaCross,
288
+ cash=1000,
289
+ spread=0.01,
290
+ margin=0.1,
291
+ execution_timing="same_close",
292
+ )
293
+ bt.run()
294
+
295
+ def test_absolute_size_order_warns_on_insufficient_margin(self):
296
+ class S(Strategy):
297
+ def init(self):
298
+ pass
299
+
300
+ def next(self):
301
+ self.buy(qty=10_000)
302
+
303
+ bt = Backtest(GOOG.iloc[:3], S, cash=1000)
304
+ stats = bt.run()
305
+ self.assertIn("insufficient_funds", stats._orders.reason.dropna().tolist())
306
+ self.assertEqual(stats["# Trades"], 0)
307
+
308
+ def test_spread_commission(self):
309
+ class S(Strategy):
310
+ def init(self):
311
+ self.done = False
312
+
313
+ def next(self):
314
+ if not self.position:
315
+ self.buy(
316
+ qty=equity_fraction(
317
+ 0.95,
318
+ account=self.account,
319
+ reference_price=self.close[-1],
320
+ instrument=self.instrument,
321
+ ).qty
322
+ )
323
+ else:
324
+ self.position.close()
325
+ self.next = lambda: None # Done
326
+
327
+ qty = int(10_000 * 0.95 / SHORT_DATA.Close.iloc[0])
328
+ for spread, commission in ((0.01, 0.01), (0, (100, 0.01)), (0, lambda q, p: q * p * 0.01)):
329
+ stats = Backtest(SHORT_DATA, S, spread=spread, commission=commission).run()
330
+ entry = min(SHORT_DATA.High.iloc[1], SHORT_DATA.Open.iloc[1] * (1 + spread))
331
+ exit_price = max(SHORT_DATA.Low.iloc[2], SHORT_DATA.Open.iloc[2] * (1 - spread))
332
+
333
+ def fee(q, price, commission=commission):
334
+ if callable(commission):
335
+ return commission(q, price)
336
+ fixed, relative = commission if isinstance(commission, tuple) else (0, commission)
337
+ return fixed + abs(q) * price * relative
338
+
339
+ expected = [
340
+ 10_000 - fee(qty, entry) + qty * (SHORT_DATA.Close.iloc[1] - entry),
341
+ 10_000 - fee(qty, entry) - fee(-qty, exit_price) + qty * (exit_price - entry),
342
+ ]
343
+ np.testing.assert_allclose(stats._equity_curve.Equity.iloc[1:3], expected)
344
+ self.assertEqual(stats._trades.EntryPrice.iloc[0], entry)
345
+ self.assertEqual(stats._trades.ExitPrice.iloc[0], exit_price)
346
+
347
+ def test_commissions(self):
348
+ class S(_S):
349
+ def next(self):
350
+ if len(self.data) == 2:
351
+ self.buy(qty=SIZE, tp=3)
352
+
353
+ FIXED_COMMISSION, COMMISSION = 10, 0.01
354
+ CASH, SIZE, PRICE_ENTRY, PRICE_EXIT = 5000, 100, 1, 4
355
+ arr = np.r_[1, PRICE_ENTRY, 1, 2, PRICE_EXIT, 1, 2]
356
+ df = pd.DataFrame({"Open": arr, "High": arr, "Low": arr, "Close": arr})
357
+ with self.assertWarnsRegex(UserWarning, "index is not datetime"):
358
+ stats = Backtest(df, S, cash=CASH, commission=(FIXED_COMMISSION, COMMISSION)).run()
359
+ EXPECTED_PAID_COMMISSION = (
360
+ FIXED_COMMISSION
361
+ + COMMISSION * SIZE * PRICE_ENTRY
362
+ + FIXED_COMMISSION
363
+ + COMMISSION * SIZE * PRICE_EXIT
364
+ )
365
+ self.assertEqual(stats["Commissions [$]"], EXPECTED_PAID_COMMISSION)
366
+ self.assertEqual(stats._trades["Commission"][0], EXPECTED_PAID_COMMISSION)
367
+ self.assertEqual(
368
+ stats["Equity Final [$]"],
369
+ CASH + (PRICE_EXIT - PRICE_ENTRY) * SIZE - EXPECTED_PAID_COMMISSION,
370
+ )
371
+
372
+ def test_dont_overwrite_data(self):
373
+ df = EURUSD.copy()
374
+ bt = Backtest(df, SmaCross)
375
+ bt.run()
376
+ bt.optimize(fast=4, slow=[6, 8])
377
+ self.assertTrue(df.equals(EURUSD))
378
+
379
+ def test_strategy_abstract(self):
380
+ class MyStrategy(Strategy):
381
+ pass
382
+
383
+ self.assertRaises(TypeError, MyStrategy, None, None)
384
+
385
+ def test_strategy_str(self):
386
+ bt = Backtest(GOOG.iloc[:100], SmaCross)
387
+ self.assertEqual(str(bt.run()._strategy), SmaCross.__name__)
388
+ self.assertEqual(str(bt.run(fast=11)._strategy), SmaCross.__name__ + "(fast=11)")
389
+
390
+ def test_compute_drawdown(self):
391
+ dd = pd.Series([0, 1, 7, 0, 4, 0, 0])
392
+ durations, peaks = compute_drawdown_duration_peaks(dd)
393
+ np.testing.assert_array_equal(durations, pd.Series([3, 2], index=[3, 5]).reindex(dd.index))
394
+ np.testing.assert_array_equal(peaks, pd.Series([7, 4], index=[3, 5]).reindex(dd.index))
395
+
396
+ def test_compute_stats(self):
397
+ stats = Backtest(GOOG, SmaCross, end_policy="close_at_last_close").run()
398
+ # Policy v2: Explicit 95% quantity budget and liquidation at the final Close; original historical baseline files are retained.
399
+ # Per-bar execution: Buy-and-hold starts at the first input bar; annualized risk includes the first zero-return daily bucket.
400
+ # Trades, equity, Beta, and drawdowns are unchanged; Alpha changes with the market benchmark.
401
+ expected = pd.Series(
402
+ {
403
+ "Start": pd.Timestamp("2004-08-19 00:00:00"),
404
+ "End": pd.Timestamp("2013-03-01 00:00:00"),
405
+ "Duration": pd.Timedelta("3116 days 00:00:00"),
406
+ "Exposure Time [%]": 96.74115456238361,
407
+ "Equity Final [$]": 49228.00999999996,
408
+ "Equity Peak [$]": 70110.02999999998,
409
+ "Return [%]": 392.28009999999955,
410
+ "Buy & Hold Return [%]": 703.4582419772772,
411
+ "Return (Ann.) [%]": 20.561671886086728,
412
+ "Volatility (Ann.) [%]": 34.57340358414538,
413
+ "CAGR [%]": 20.542294223341152,
414
+ "Sharpe Ratio": 0.5947251284081232,
415
+ "Sortino Ratio": 1.104726320298045,
416
+ "Calmar Ratio": 0.4462810975723509,
417
+ "Alpha [%]": 365.639613282582,
418
+ "Beta": 0.03787074360311223,
419
+ "Max. Drawdown [%]": -46.07336496646776,
420
+ "Avg. Drawdown [%]": -5.747302647122755,
421
+ "Max. Drawdown Duration": pd.Timedelta("584 days 00:00:00"),
422
+ "Avg. Drawdown Duration": pd.Timedelta("40 days 00:00:00"),
423
+ "# Trades": 66,
424
+ "Win Rate [%]": 46.96969696969697,
425
+ "Best Trade [%]": 53.59595229490424,
426
+ "Worst Trade [%]": -18.398873538354813,
427
+ "Avg. Trade [%]": 2.5479693282886906,
428
+ "Max. Trade Duration": pd.Timedelta("183 days 00:00:00"),
429
+ "Avg. Trade Duration": pd.Timedelta("46 days 00:00:00"),
430
+ "Profit Factor": 2.174469316409448,
431
+ "Expectancy [%]": 3.293098628562828,
432
+ "SQN": 1.1335447628114286,
433
+ "Kelly Criterion": 0.158222363299374,
434
+ "Commissions [$]": 0.0,
435
+ "# Closed Lots": 66,
436
+ }
437
+ )
438
+
439
+ def almost_equal(a, b):
440
+ try:
441
+ return np.isclose(a, b, rtol=1.0e-8)
442
+ except TypeError:
443
+ return a == b
444
+
445
+ diff = {
446
+ key: print(key) or value
447
+ for key, value in stats.filter(regex="^[^_]").items()
448
+ if not almost_equal(value, expected[key])
449
+ }
450
+ self.assertDictEqual(diff, {})
451
+
452
+ self.assertSequenceEqual(
453
+ sorted(stats["_equity_curve"].columns),
454
+ sorted(["Equity", "DrawdownPct", "DrawdownDuration"]),
455
+ )
456
+
457
+ self.assertEqual(len(stats["_trades"]), 66)
458
+
459
+ indicator_columns = [
460
+ f"{entry}_SMA(C,{n})"
461
+ for entry in ("Entry", "Exit")
462
+ for n in (SmaCross.fast, SmaCross.slow)
463
+ ]
464
+ self.assertSequenceEqual(
465
+ sorted(stats["_trades"].columns),
466
+ sorted(
467
+ [
468
+ "Size",
469
+ "EntryBar",
470
+ "ExitBar",
471
+ "EntryPrice",
472
+ "ExitPrice",
473
+ "SL",
474
+ "TP",
475
+ "PnL",
476
+ "ReturnPct",
477
+ "EntryTime",
478
+ "ExitTime",
479
+ "Duration",
480
+ "Tag",
481
+ "Commission",
482
+ "trade_id",
483
+ "entry_fill_id",
484
+ "allocation_id",
485
+ "exit_fill_id",
486
+ "exit_sequence",
487
+ *indicator_columns,
488
+ ]
489
+ ),
490
+ )
491
+
492
+ def test_compute_stats_bordercase(self):
493
+
494
+ class SingleTrade(Strategy):
495
+ def init(self):
496
+ self._done = False
497
+
498
+ def next(self):
499
+ if not self._done:
500
+ self.buy(
501
+ qty=equity_fraction(
502
+ 0.95,
503
+ account=self.account,
504
+ reference_price=self.close[-1],
505
+ instrument=self.instrument,
506
+ ).qty
507
+ )
508
+ self._done = True
509
+ if self.position:
510
+ self.position.close()
511
+
512
+ class SinglePosition(_S):
513
+ def next(self):
514
+ if not self.position:
515
+ self.buy(
516
+ qty=equity_fraction(
517
+ 0.95,
518
+ account=self.account,
519
+ reference_price=self.close[-1],
520
+ instrument=self.instrument,
521
+ ).qty
522
+ )
523
+
524
+ class NoTrade(_S):
525
+ def next(self):
526
+ pass
527
+
528
+ for strategy in (SmaCross, SingleTrade, SinglePosition, NoTrade):
529
+ with self.subTest(strategy=strategy.__name__):
530
+ stats = Backtest(GOOG.iloc[:100], strategy).run()
531
+
532
+ self.assertFalse(np.isnan(stats["Equity Final [$]"]))
533
+ self.assertFalse(stats["_equity_curve"]["Equity"].isnull().any())
534
+ self.assertEqual(stats["_strategy"].__class__, strategy)
535
+
536
+ def test_trade_enter_hit_sl_on_same_day(self):
537
+ the_day = pd.Timestamp("2012-10-17 00:00:00")
538
+
539
+ class S(_S):
540
+ def next(self):
541
+ if self.data.index[-1] == the_day:
542
+ self.buy(sl=720, qty=1)
543
+
544
+ self.assertEqual(Backtest(GOOG, S).run()._trades.iloc[0].ExitPrice, 720)
545
+
546
+ class S(_S):
547
+ def next(self):
548
+ if self.data.index[-1] == the_day:
549
+ self.buy(stop=758, sl=720, qty=1)
550
+
551
+ result = Backtest(GOOG, S).run()
552
+ self.assertEqual(result._trades.iloc[0].ExitPrice, 705.58)
553
+ self.assertEqual(result.attrs["diagnostics"][0]["code"], "protection_next_bar")
554
+
555
+ def test_stop_price_between_sl_tp(self):
556
+ class S(_S):
557
+ def next(self):
558
+ if self.data.index[-1] == pd.Timestamp("2004-09-09 00:00:00"):
559
+ self.buy(stop=104, sl=103, tp=110, qty=1)
560
+
561
+ self.assertEqual(Backtest(GOOG, S).run()._trades.iloc[0].EntryPrice, 104)
562
+
563
+ def test_position_close_portion(self):
564
+ class SmaCross(Strategy):
565
+ def init(self):
566
+ self.sma1 = self.ind(SMA, self.data.Close, 10)
567
+ self.sma2 = self.ind(SMA, self.data.Close, 20)
568
+
569
+ def next(self):
570
+ if not self.position and self.ta.is_crossover(self.sma1, self.sma2):
571
+ self.buy(qty=10)
572
+ if self.position and self.ta.is_crossunder(self.sma1, self.sma2):
573
+ self.position.close(portion=0.5 if abs(self.position.size) >= 2 else 1)
574
+
575
+ bt = Backtest(GOOG, SmaCross, spread=0.002)
576
+ bt.run()
577
+
578
+ def test_close_orders_from_last_strategy_iteration(self):
579
+ class S(_S):
580
+ def next(self):
581
+ if not self.position:
582
+ self.buy(
583
+ qty=equity_fraction(
584
+ 0.95,
585
+ account=self.account,
586
+ reference_price=self.close[-1],
587
+ instrument=self.instrument,
588
+ ).qty
589
+ )
590
+ elif len(self.data) == len(SHORT_DATA):
591
+ self.position.close()
592
+
593
+ result = Backtest(SHORT_DATA, S, end_policy="mark_to_market").run()
594
+ self.assertTrue(result._trades.empty)
595
+ self.assertEqual(result._orders.iloc[-1].reason, "end_of_data")
596
+ self.assertFalse(
597
+ Backtest(SHORT_DATA, S, end_policy="close_at_last_close").run()._trades.empty
598
+ )
599
+
600
+ def test_check_adjusted_price_when_placing_order(self):
601
+ class S(_S):
602
+ def next(self):
603
+ self.buy(tp=self.data.Close * 1.01, qty=1)
604
+
605
+ self.assertRaises(TypeError, Backtest(SHORT_DATA, S, spread=0.02).run)
606
+
607
+
608
+ class TestStrategy(TestCase):
609
+ @staticmethod
610
+ def _Backtest(strategy_coroutine, data=SHORT_DATA, **kwargs):
611
+ class S(Strategy):
612
+ def init(self):
613
+ self.step = strategy_coroutine(self)
614
+
615
+ def next(self):
616
+ try_(self.step.__next__, None, StopIteration)
617
+
618
+ return Backtest(data, S, **kwargs)
619
+
620
+ def test_position(self):
621
+ def coroutine(self):
622
+ yield self.buy(
623
+ qty=equity_fraction(
624
+ 0.95,
625
+ account=self.account,
626
+ reference_price=self.close[-1],
627
+ instrument=self.instrument,
628
+ ).qty
629
+ )
630
+
631
+ assert self.position
632
+ assert self.position.is_long
633
+ assert not self.position.is_short
634
+ assert self.position.size > 0
635
+ assert self.position.pl
636
+ assert self.position.return_fraction
637
+
638
+ yield self.position.close()
639
+
640
+ assert not self.position
641
+ assert not self.position.is_long
642
+ assert not self.position.is_short
643
+ assert not self.position.size
644
+ assert not self.position.pl
645
+ assert not self.position.return_fraction
646
+
647
+ self._Backtest(coroutine).run()
648
+
649
+ def test_broker_hedging(self):
650
+ def coroutine(self):
651
+ yield self.buy(qty=2)
652
+
653
+ assert len(self.trades) == 1
654
+ yield self.sell(qty=1)
655
+
656
+ assert len(self.trades) == 2
657
+
658
+ self._Backtest(coroutine, hedging=True).run()
659
+
660
+ def test_broker_exclusive_orders(self):
661
+ def coroutine(self):
662
+ yield self.buy(qty=2)
663
+
664
+ assert len(self.trades) == 1
665
+ yield self.sell(qty=3)
666
+
667
+ assert len(self.trades) == 1
668
+ assert self.trades[0].size == -3
669
+
670
+ self._Backtest(coroutine, exclusive_orders=True).run()
671
+
672
+ def test_trade_multiple_close(self):
673
+ def coroutine(self):
674
+ yield self.buy(
675
+ qty=equity_fraction(
676
+ 0.95,
677
+ account=self.account,
678
+ reference_price=self.close[-1],
679
+ instrument=self.instrument,
680
+ ).qty
681
+ )
682
+
683
+ assert self.trades
684
+ self.trades[-1].close(1)
685
+ self.trades[-1].close(0.1)
686
+ yield
687
+
688
+ self._Backtest(coroutine).run()
689
+
690
+ def test_close_trade_leaves_needsize_0(self):
691
+ def coroutine(self):
692
+ self.buy(qty=1)
693
+ self.buy(qty=1)
694
+ yield
695
+ if self.position:
696
+ self.sell(qty=1)
697
+
698
+ self._Backtest(coroutine).run()
699
+
700
+ def test_stop_limit_order_price_is_stop_price(self):
701
+ def coroutine(self):
702
+ yield # This price regression submits on the second bar, independently of the new first-bar callback.
703
+ self.buy(stop=112, limit=113, qty=1)
704
+ self.sell(stop=107, limit=105, qty=1)
705
+ yield
706
+
707
+ stats = self._Backtest(coroutine).run()
708
+ self.assertListEqual(stats._trades.filter(like="Price").stack().tolist(), [112, 107])
709
+
710
+ def test_autoclose_trades_on_finish(self):
711
+ def coroutine(self):
712
+ yield self.buy(
713
+ qty=equity_fraction(
714
+ 0.95,
715
+ account=self.account,
716
+ reference_price=self.close[-1],
717
+ instrument=self.instrument,
718
+ ).qty
719
+ )
720
+
721
+ stats = self._Backtest(coroutine, end_policy="close_at_last_close").run()
722
+ self.assertEqual(len(stats._trades), 1)
723
+
724
+ def test_order_tag(self):
725
+ def coroutine(self):
726
+ yield self.buy(qty=2, tag=1)
727
+ yield self.sell(qty=1, tag="s")
728
+ yield self.sell(qty=1)
729
+
730
+ yield self.buy(tag=2, qty=1)
731
+ yield self.position.close()
732
+
733
+ stats = self._Backtest(coroutine).run()
734
+ self.assertEqual(list(stats._trades.Tag), [1, 1, 2])
735
+
736
+
737
+ class TestOptimize(TestCase):
738
+ def test_optimize(self):
739
+ bt = Backtest(GOOG.iloc[:100], SmaCross)
740
+ OPT_PARAMS = {"fast": range(2, 5, 2), "slow": [2, 5, 7, 9]}
741
+
742
+ self.assertRaises(ValueError, bt.optimize)
743
+ self.assertRaises(ValueError, bt.optimize, maximize="missing key", **OPT_PARAMS)
744
+ self.assertRaises(ValueError, bt.optimize, maximize="missing key", **OPT_PARAMS)
745
+ self.assertRaises(TypeError, bt.optimize, maximize=15, **OPT_PARAMS)
746
+ self.assertRaises(TypeError, bt.optimize, constraint=15, **OPT_PARAMS)
747
+ self.assertRaises(ValueError, bt.optimize, constraint=lambda d: False, **OPT_PARAMS)
748
+ self.assertRaises(ValueError, bt.optimize, return_optimization=True, **OPT_PARAMS)
749
+
750
+ res = bt.optimize(**OPT_PARAMS)
751
+ self.assertIsInstance(res, pd.Series)
752
+
753
+ default_maximize = inspect.signature(bt.optimize).parameters["maximize"].default
754
+ res2 = bt.optimize(**OPT_PARAMS, maximize=lambda s: s[default_maximize])
755
+ self.assertDictEqual(
756
+ res.filter(regex="^[^_]").fillna(-1).to_dict(),
757
+ res2.filter(regex="^[^_]").fillna(-1).to_dict(),
758
+ )
759
+
760
+ _res3, heatmap = bt.optimize(
761
+ **OPT_PARAMS, return_heatmap=True, constraint=lambda d: d.slow > 2 * d.fast
762
+ )
763
+ self.assertIsInstance(heatmap, pd.Series)
764
+ self.assertEqual(len(heatmap), 4)
765
+ self.assertEqual(heatmap.name, default_maximize)
766
+
767
+ def test_method_sambo(self):
768
+ bt = Backtest(GOOG.iloc[:100], SmaCross, end_policy="close_at_last_close")
769
+ res, heatmap, sambo_results = bt.optimize(
770
+ fast=range(2, 20),
771
+ slow=np.arange(2, 20, dtype=object),
772
+ constraint=lambda p: p.fast < p.slow,
773
+ max_tries=30,
774
+ method="sambo",
775
+ return_optimization=True,
776
+ return_heatmap=True,
777
+ random_state=2,
778
+ )
779
+ self.assertIsInstance(res, pd.Series)
780
+ self.assertIsInstance(heatmap, pd.Series)
781
+ self.assertGreater(heatmap.max(), 1.1)
782
+ self.assertGreater(heatmap.min(), -2)
783
+ self.assertEqual(-sambo_results.fun, heatmap.max())
784
+ self.assertEqual(heatmap.index.tolist(), heatmap.dropna().index.unique().tolist())
785
+
786
+ def test_max_tries(self):
787
+ bt = Backtest(GOOG.iloc[:100], SmaCross)
788
+ OPT_PARAMS = {"fast": range(2, 10, 2), "slow": [2, 5, 7, 9]}
789
+ for method, max_tries, random_state in (
790
+ ("grid", 5, 0),
791
+ ("grid", 0.3, 0),
792
+ ("sambo", 6, 0),
793
+ ("sambo", 0.42, 0),
794
+ ):
795
+ with self.subTest(method=method, max_tries=max_tries, random_state=random_state):
796
+ _, heatmap = bt.optimize(
797
+ max_tries=max_tries,
798
+ method=method,
799
+ random_state=random_state,
800
+ return_heatmap=True,
801
+ **OPT_PARAMS,
802
+ )
803
+ if method == "grid":
804
+ self.assertEqual(len(heatmap), 6)
805
+ else:
806
+ # SAMBO may stop early when the objective converges; the budget is an upper bound.
807
+ self.assertTrue(0 < len(heatmap) <= 6)
808
+ self.assertTrue(heatmap.index.is_unique)
809
+
810
+ def test_optimize_invalid_param(self):
811
+ bt = Backtest(GOOG.iloc[:100], SmaCross)
812
+ self.assertRaises(AttributeError, bt.optimize, foo=range(3))
813
+ self.assertRaises(ValueError, bt.optimize, fast=[])
814
+
815
+ def test_optimize_no_trades(self):
816
+ bt = Backtest(GOOG, SmaCross)
817
+ stats = bt.optimize(fast=[3], slow=[3])
818
+ self.assertTrue(stats.isnull().any())
819
+
820
+ def test_optimize_speed(self):
821
+ bt = Backtest(GOOG.iloc[:100], SmaCross)
822
+ start = time.process_time()
823
+ bt.optimize(fast=range(2, 20, 2), slow=range(10, 40, 2))
824
+ end = time.process_time()
825
+ print(end - start)
826
+ self.assertLess(end - start, 15.0)
827
+
828
+
829
+ class TestCalculationRegressions(TestCase):
830
+ def test_wellknown(self):
831
+ class S(_S):
832
+ def next(self):
833
+ date = self.data.index[-1]
834
+ if date == pd.Timestamp("Thu 19 Oct 2006"):
835
+ self.buy(stop=484, limit=466, qty=100)
836
+ elif date == pd.Timestamp("Thu 30 Oct 2007"):
837
+ self.position.close()
838
+ elif date == pd.Timestamp("Tue 11 Nov 2008"):
839
+ self.sell(
840
+ stop=self.data.Low[-1],
841
+ limit=324.90, # High from 14 Nov
842
+ qty=200,
843
+ )
844
+
845
+ bt = Backtest(GOOG, S, margin=0.1)
846
+ stats = bt.run()
847
+ trades = stats["_trades"]
848
+
849
+ self.assertAlmostEqual(stats["Equity Peak [$]"], 46961)
850
+ self.assertEqual(stats["Equity Final [$]"], -951)
851
+ self.assertEqual(len(trades), 2)
852
+ assert trades[["EntryTime", "ExitTime"]].equals(
853
+ pd.DataFrame(
854
+ {
855
+ "EntryTime": pd.to_datetime(["2006-11-01", "2008-11-14"]),
856
+ "ExitTime": pd.to_datetime(["2007-10-31", "2009-09-21"]),
857
+ }
858
+ )
859
+ )
860
+ assert trades["PnL"].round().equals(pd.Series([23469.0, -34420.0]))
861
+
862
+ def test_indicator_name(self):
863
+ test_self = self
864
+
865
+ class S(Strategy):
866
+ def init(self):
867
+ def _SMA():
868
+ return SMA(self.data.Close, 5), SMA(self.data.Close, 10)
869
+
870
+ test_self.assertRaises(TypeError, self.ind, _SMA, name=42)
871
+ test_self.assertRaises(ValueError, self.ind, _SMA, name=("SMA One",))
872
+ test_self.assertRaises(
873
+ ValueError, self.ind, _SMA, name=("SMA One", "SMA Two", "SMA Three")
874
+ )
875
+
876
+ self.ind(SMA, self.data.Close, 5)
877
+ self.ind(SMA, self.data.Close, 5, name="My SMA")
878
+ self.ind(SMA, self.data.Close, 5, name=("My SMA",))
879
+ self.ind(_SMA)
880
+ self.ind(_SMA, name="My SMA")
881
+ self.ind(_SMA, name=("SMA One", "SMA Two"))
882
+
883
+ def next(self):
884
+ pass
885
+
886
+ bt = Backtest(GOOG, S)
887
+ bt.run()
888
+
889
+
890
+ class TestLib(TestCase):
891
+ def test_barssince(self):
892
+ self.assertEqual(barssince(np.r_[1, 0, 0]), 2)
893
+ self.assertEqual(barssince(np.r_[0, 0, 0]), np.inf)
894
+ self.assertEqual(barssince(np.r_[0, 0, 0], 0), 0)
895
+
896
+ def test_quantile(self):
897
+ self.assertEqual(quantile(np.r_[1, 3, 2], 0.5), 2)
898
+ self.assertEqual(quantile(np.r_[1, 3, 2]), 0.5)
899
+
900
+ def test_resample_apply(self):
901
+ res = resample_apply("D", SMA, EURUSD.Close, 10)
902
+ self.assertEqual(res.name, "C[D]")
903
+ self.assertEqual(res.count() / res.size, 0.9634)
904
+ np.testing.assert_almost_equal(
905
+ res.iloc[-48:].unique().tolist(), [1.242643, 1.242381, 1.242275], decimal=6
906
+ )
907
+
908
+ def resets_index(*args):
909
+ return pd.Series(SMA(*args).values)
910
+
911
+ res2 = resample_apply("D", resets_index, EURUSD.Close, 10)
912
+ self.assertTrue((res.dropna() == res2.dropna()).all())
913
+ self.assertTrue((res.index == res2.index).all())
914
+
915
+ res3 = resample_apply("D", None, EURUSD)
916
+ self.assertIn("Volume", res3)
917
+
918
+ res3 = resample_apply("D", lambda df: (df.Close, df.Close), EURUSD)
919
+ self.assertIsInstance(res3, pd.DataFrame)
920
+
921
+ def test_random_ohlc_data(self):
922
+ generator = random_ohlc_data(GOOG, frac=1)
923
+ new_data = next(generator)
924
+ self.assertEqual(list(new_data.index), list(GOOG.index))
925
+ self.assertEqual(new_data.shape, GOOG.shape)
926
+ self.assertEqual(list(new_data.columns), list(GOOG.columns))
927
+
928
+ def test_compute_stats(self):
929
+ stats = Backtest(GOOG, SmaCross).run()
930
+ only_long_trades = stats._trades[stats._trades.Size > 0]
931
+ long_stats = compute_stats(
932
+ stats=stats, trades=only_long_trades, data=GOOG, risk_free_rate=0.02
933
+ )
934
+ self.assertNotEqual(list(stats._equity_curve.Equity), list(long_stats._equity_curve.Equity))
935
+ self.assertNotEqual(stats["Sharpe Ratio"], long_stats["Sharpe Ratio"])
936
+ self.assertEqual(long_stats["# Trades"], len(only_long_trades))
937
+ self.assertEqual(stats._strategy, long_stats._strategy)
938
+ assert_frame_equal(long_stats._trades, only_long_trades)
939
+
940
+ def test_SignalStrategy(self):
941
+ class S(SignalStrategy):
942
+ def init(self):
943
+ sma = self.data.Close.s.rolling(10).mean()
944
+ self.set_signal(self.data.Close > sma, self.data.Close < sma)
945
+
946
+ stats = Backtest(GOOG, S).run()
947
+ self.assertIn("Rejected", stats._orders.status.tolist())
948
+ self.assertIn(stats["# Trades"], (1179, 1182)) # varies on different archs?
949
+
950
+ def test_TrailingStrategy(self):
951
+ class S(TrailingStrategy):
952
+ def init(self):
953
+ super().init()
954
+ self.set_atr_periods(40)
955
+ self.set_trailing_pct(0.1)
956
+ self.set_trailing_sl(3)
957
+ self.sma = self.ind(lambda: self.data.Close.s.rolling(10).mean())
958
+
959
+ def next(self):
960
+ super().next()
961
+ if not self.position and self.data.Close > self.sma:
962
+ self.buy(
963
+ qty=equity_fraction(
964
+ 0.95,
965
+ account=self.account,
966
+ reference_price=self.close[-1],
967
+ instrument=self.instrument,
968
+ ).qty
969
+ )
970
+
971
+ stats = Backtest(GOOG, S).run()
972
+ self.assertEqual(stats["# Trades"], 57)
973
+
974
+ def test_FractionalBacktest(self):
975
+ from openbacktesting.broker import InstrumentSpec
976
+
977
+ with self.assertRaisesRegex(TypeError, "InstrumentSpec"):
978
+ FractionalBacktest(BTCUSD, SmaCross)
979
+ ubtc_bt = Backtest(
980
+ BTCUSD["2015":],
981
+ SmaCross,
982
+ instrument=InstrumentSpec(quantity_step=1 / 1e6),
983
+ cash=100,
984
+ end_policy="close_at_last_close",
985
+ )
986
+ stats = ubtc_bt.run(fast=2, slow=3)
987
+ self.assertEqual(stats["# Trades"], 42)
988
+ trades = stats["_trades"]
989
+ self.assertEqual(len(trades), 42)
990
+ trade = trades.iloc[0]
991
+ self.assertAlmostEqual(trade["EntryPrice"], 236.69)
992
+ # Indicators retain internal computation units; explicit conversion still matches the original price baseline.
993
+ self.assertAlmostEqual(stats["_strategy"]._indicators[0][trade["EntryBar"]], 234.14)
994
+
995
+ def test_MultiBacktest(self):
996
+ import openbacktesting
997
+
998
+ assert callable(getattr(openbacktesting, "Pool", None)), openbacktesting.__dict__
999
+ for start_method in mp.get_all_start_methods():
1000
+ with (
1001
+ self.subTest(start_method=start_method),
1002
+ patch(openbacktesting, "Pool", mp.get_context(start_method).Pool),
1003
+ ):
1004
+ start_time = time.monotonic()
1005
+ btm = MultiBacktest(
1006
+ [GOOG, EURUSD, BTCUSD], SmaCross, cash=100_000, end_policy="close_at_last_close"
1007
+ )
1008
+ res = btm.run(fast=2)
1009
+ self.assertIsInstance(res, pd.DataFrame)
1010
+ self.assertEqual(res.columns.tolist(), [0, 1, 2])
1011
+ heatmap = btm.optimize(fast=[2, 4], slow=[10, 20])
1012
+ self.assertIsInstance(heatmap, pd.DataFrame)
1013
+ self.assertEqual(heatmap.columns.tolist(), [0, 1, 2])
1014
+ print(start_method, time.monotonic() - start_time)
1015
+
1016
+ class SometimesNoTrade(Strategy):
1017
+ def init(self):
1018
+ self._will_trade = len(self.data) == 20
1019
+
1020
+ def next(self):
1021
+ if not self._will_trade:
1022
+ return
1023
+ if self.position:
1024
+ self.position.close()
1025
+ elif not self.closed_trades:
1026
+ self.buy(
1027
+ qty=equity_fraction(
1028
+ 0.95,
1029
+ account=self.account,
1030
+ reference_price=self.close[-1],
1031
+ instrument=self.instrument,
1032
+ ).qty
1033
+ )
1034
+
1035
+ def test_MultiBacktest_handles_mixed_no_trade_results(self):
1036
+ btm = MultiBacktest(
1037
+ [GOOG.iloc[:20], GOOG.iloc[:21], GOOG.iloc[:22]], self.SometimesNoTrade, cash=100_000
1038
+ )
1039
+ res = btm.run()
1040
+ self.assertEqual(res.loc["# Trades"].tolist(), [1, 0, 0])
1041
+ self.assertNotIsInstance(res.iloc[0, 0], pd.Series)
1042
+
1043
+
1044
+ class TestUtil(TestCase):
1045
+ def test_as_str(self):
1046
+ def func():
1047
+ pass
1048
+
1049
+ class Class:
1050
+ def __call__(self):
1051
+ pass
1052
+
1053
+ self.assertEqual(_as_str("4"), "4")
1054
+ self.assertEqual(_as_str(4), "4")
1055
+ self.assertEqual(_as_str(_Indicator([1, 2], name="x")), "x")
1056
+ self.assertEqual(_as_str(func), "func")
1057
+ self.assertEqual(_as_str(Class), "Class")
1058
+ self.assertEqual(_as_str(Class()), "Class")
1059
+ self.assertEqual(_as_str(pd.Series([1, 2], name="x")), "x")
1060
+ self.assertEqual(_as_str(pd.DataFrame()), "df")
1061
+ self.assertEqual(_as_str(lambda x: x), "λ")
1062
+ for s in ("Open", "High", "Low", "Close", "Volume"):
1063
+ self.assertEqual(_as_str(_Array([1], name=s)), s[0])
1064
+
1065
+ def test_patch(self):
1066
+ class Object:
1067
+ pass
1068
+
1069
+ o = Object()
1070
+ o.attr = False
1071
+ with patch(o, "attr", True):
1072
+ self.assertTrue(o.attr)
1073
+ self.assertFalse(o.attr)
1074
+
1075
+ def test_pandas_accessors(self):
1076
+ class S(Strategy):
1077
+ def init(self):
1078
+ close, index = self.data.Close, self.data.index
1079
+ assert close.s.equals(pd.Series(close, index=index))
1080
+ assert self.data.df["Close"].equals(pd.Series(close, index=index))
1081
+ self.data.df["new_key"] = 2 * close
1082
+
1083
+ def next(self):
1084
+ close, index = self.data.Close, self.data.index
1085
+ assert close.s.equals(pd.Series(close, index=index))
1086
+ assert self.data.df["Close"].equals(pd.Series(close, index=index))
1087
+ assert self.data.df["new_key"].equals(pd.Series(self.data.new_key, index=index))
1088
+
1089
+ Backtest(GOOG.iloc[:20], S).run()
1090
+
1091
+ def test_indicators_picklable(self):
1092
+ bt = Backtest(SHORT_DATA, SmaCross)
1093
+ with ProcessPoolExecutor() as executor:
1094
+ stats = executor.submit(_Backtest.run, bt).result()
1095
+ assert stats._strategy._indicators[0]._opts, "._opts and .name were not unpickled"
1096
+
1097
+
1098
+ class TestDocs(TestCase):
1099
+ DOCS_DIR = str(Path(__file__).resolve().parents[3])
1100
+
1101
+ @unittest.skipUnless(os.path.isdir(DOCS_DIR), "docs dir doesn't exist")
1102
+ @unittest.skipUnless(
1103
+ sys.platform.startswith("linux"), "test_examples requires mp.start_method=fork"
1104
+ )
1105
+ def test_examples(self):
1106
+ from importlib.util import module_from_spec, spec_from_file_location
1107
+ from runpy import run_path
1108
+ from unittest.mock import patch as mock_patch
1109
+
1110
+ import openbacktesting
1111
+
1112
+ examples = glob(os.path.join(self.DOCS_DIR, "examples", "**", "*.py"), recursive=True)
1113
+ self.assertGreaterEqual(len(examples), 4)
1114
+ with (
1115
+ chdir(gettempdir()),
1116
+ patch(openbacktesting, "Pool", mp.get_context("fork").Pool),
1117
+ ):
1118
+ for file in examples:
1119
+ with self.subTest(example=os.path.basename(file)):
1120
+ # Preserve module identity until the run ends; fork task queues still need to pickle strategy classes.
1121
+ name = "_engine_example_" + Path(file).stem
1122
+ spec = spec_from_file_location(name, file)
1123
+ module = module_from_spec(spec)
1124
+ with mock_patch.dict(sys.modules, {name: module}):
1125
+ spec.loader.exec_module(module)
1126
+ if hasattr(module, "main"):
1127
+ result = module.main()
1128
+ else:
1129
+ host = run_path(
1130
+ str(Path(self.DOCS_DIR) / "examples/advanced/run_author.py")
1131
+ )
1132
+ result = openbacktesting.runtime.run(
1133
+ vars(module), **host["chart_for"](file)
1134
+ )
1135
+ self.assertIsNotNone(result)
1136
+
1137
+ def test_backtest_run_docstring_contains_stats_keys(self):
1138
+ stats = Backtest(SHORT_DATA, SmaCross).run()
1139
+ for key in stats.index:
1140
+ self.assertIn(key, _Backtest.run.__doc__)
1141
+
1142
+ def test_readme_backtest_example(self):
1143
+ import re
1144
+ from runpy import run_path
1145
+ from tempfile import TemporaryDirectory
1146
+
1147
+ readme = (Path(self.DOCS_DIR) / "README.md").read_text()
1148
+ examples = re.findall(r"```python\n(.*?)```", readme, re.DOTALL)
1149
+ self.assertTrue(examples, "README must include a runnable Python example")
1150
+ with TemporaryDirectory(prefix="readme-example-") as directory:
1151
+ script = Path(directory) / "quickstart.py"
1152
+ script.write_text("\n\n".join(examples))
1153
+ namespace = run_path(str(script))
1154
+ result = namespace["result"]
1155
+ self.assertIsInstance(result, pd.Series)
1156
+ self.assertGreater(result["# Trades"], 0)
1157
+ self.assertEqual(len(result["_equity_curve"]), len(GOOG))
1158
+
1159
+
1160
+ class TestRegressions(TestCase):
1161
+ def test_gh_521(self):
1162
+ class S(_S):
1163
+ def next(self):
1164
+ if self.data.Close[-1] == 100:
1165
+ self.buy(qty=1, sl=90)
1166
+
1167
+ arr = np.r_[100, 100, 100, 50, 50]
1168
+ df = pd.DataFrame({"Open": arr, "High": arr, "Low": arr, "Close": arr})
1169
+ with self.assertWarnsRegex(UserWarning, "index is not datetime"):
1170
+ bt = Backtest(df, S, cash=100, execution_timing="same_close")
1171
+ result = bt.run()
1172
+ self.assertEqual(result._trades["ExitPrice"][0], 50)
1173
+ self.assertIn("insufficient_funds", result._orders.reason.dropna().tolist())
1174
+
1175
+ def test_stats_annualized(self):
1176
+ stats = Backtest(GOOG.resample("W").agg(OHLCV_AGG), SmaCross).run()
1177
+ self.assertFalse(np.isnan(stats["Return (Ann.) [%]"]))
1178
+ self.assertEqual(round(stats["Return (Ann.) [%]"]), -3)
1179
+
1180
+ def test_cancel_orders(self):
1181
+ class S(_S):
1182
+ def next(self):
1183
+ self.buy(sl=1, tp=1e3, qty=1)
1184
+ if self.position:
1185
+ self.position.close()
1186
+ for order in self.orders:
1187
+ order.cancel()
1188
+
1189
+ Backtest(SHORT_DATA, S).run()
1190
+
1191
+ def test_trade_on_close_closes_trades_on_close(self):
1192
+ def coro(strat):
1193
+ yield strat.buy(qty=1, sl=90) and strat.buy(qty=1, sl=80)
1194
+ assert len(strat.trades) == 2
1195
+ yield strat.trades[0].close()
1196
+ yield
1197
+
1198
+ arr = np.r_[100, 101, 102, 50, 51]
1199
+ df = pd.DataFrame({"Open": arr - 10, "Close": arr, "High": arr, "Low": arr - 10})
1200
+ with self.assertWarnsRegex(UserWarning, "index is not datetime"):
1201
+ trades = (
1202
+ TestStrategy._Backtest(coro, df, cash=250, execution_timing="same_close")
1203
+ .run()
1204
+ ._trades
1205
+ )
1206
+ # trades = Backtest(df, S, cash=250, trade_on_close=True).run()._trades
1207
+ self.assertEqual(trades["EntryBar"][0], 0)
1208
+ self.assertEqual(trades["ExitBar"][0], 1)
1209
+ self.assertEqual(trades["EntryPrice"][0], 100)
1210
+ self.assertEqual(trades["ExitPrice"][0], 101)
1211
+ self.assertEqual(trades["EntryBar"][1], 0)
1212
+ self.assertEqual(trades["ExitBar"][1], 3)
1213
+ self.assertEqual(trades["EntryPrice"][1], 100)
1214
+ self.assertEqual(trades["ExitPrice"][1], 40)
1215
+
1216
+ with self.assertWarnsRegex(UserWarning, "index is not datetime"):
1217
+ trades = (
1218
+ TestStrategy._Backtest(coro, df, cash=250, execution_timing="next_open")
1219
+ .run()
1220
+ ._trades
1221
+ )
1222
+ # trades = Backtest(df, S, cash=250, trade_on_close=False).run()._trades
1223
+ self.assertEqual(trades["EntryBar"][0], 1)
1224
+ self.assertEqual(trades["ExitBar"][0], 2)
1225
+ self.assertEqual(trades["EntryPrice"][0], 91)
1226
+ self.assertEqual(trades["ExitPrice"][0], 92)
1227
+ self.assertEqual(trades["EntryBar"][1], 1)
1228
+ self.assertEqual(trades["ExitBar"][1], 3)
1229
+ self.assertEqual(trades["EntryPrice"][1], 91)
1230
+ self.assertEqual(trades["ExitPrice"][1], 40)
1231
+
1232
+ def test_trades_dates_match_prices(self):
1233
+ bt = Backtest(EURUSD, SmaCross, execution_timing="same_close")
1234
+ trades = bt.run()._trades
1235
+ self.assertEqual(EURUSD.Close[trades["ExitTime"]].tolist(), trades["ExitPrice"].tolist())
1236
+
1237
+ def test_sl_always_before_tp(self):
1238
+ class S(_S):
1239
+ def next(self):
1240
+ i = len(self.data.index)
1241
+ if i == 4:
1242
+ self.buy(
1243
+ qty=equity_fraction(
1244
+ 0.95,
1245
+ account=self.account,
1246
+ reference_price=self.close[-1],
1247
+ instrument=self.instrument,
1248
+ ).qty
1249
+ )
1250
+ if i == 5:
1251
+ t = self.trades[0]
1252
+ t.sl = 105
1253
+ t.tp = 107.9
1254
+
1255
+ trades = Backtest(SHORT_DATA, S).run()._trades
1256
+ self.assertEqual(trades["ExitPrice"].iloc[0], 104.95)
1257
+
1258
+ def test_stop_entry_and_tp_in_same_bar(self):
1259
+ class S(_S):
1260
+ def next(self):
1261
+ i = len(self.data.index)
1262
+ if i == 3:
1263
+ self.sell(stop=108, tp=105, sl=113, qty=1)
1264
+
1265
+ trades = Backtest(SHORT_DATA, S).run()._trades
1266
+ self.assertEqual(trades["ExitBar"].iloc[0], 4)
1267
+ self.assertEqual(trades["ExitPrice"].iloc[0], 104.96) # Next-open price improvement
1268
+
1269
+ def test_optimize_datetime_index_with_timezone(self):
1270
+ data: pd.DataFrame = GOOG.iloc[:100]
1271
+ data.index = data.index.tz_localize("Asia/Kolkata")
1272
+ res = Backtest(data, SmaCross).optimize(fast=range(2, 3), slow=range(4, 5))
1273
+ self.assertGreater(res["# Trades"], 0)
1274
+
1275
+ def test_optimize_range_index(self):
1276
+ # Data is passed to the workers through shared memory sized by
1277
+ # `.nbytes`, which a lazy RangeIndex under-reports, previously raising
1278
+ # "TypeError: buffer is too small for requested array". See GH issue #1237.
1279
+ data: pd.DataFrame = GOOG.iloc[:100].reset_index(drop=True)
1280
+ with self.assertWarnsRegex(UserWarning, "index is not datetime"):
1281
+ bt = Backtest(data, SmaCross)
1282
+ res = bt.optimize(fast=range(2, 3), slow=range(4, 5))
1283
+ self.assertGreater(res["# Trades"], 0)
1284
+
1285
+ def test_sl_tp_values_in_trades_df(self):
1286
+ class S(_S):
1287
+ def next(self):
1288
+ self.next = lambda: None
1289
+ self.buy(qty=1, tp=111)
1290
+ self.buy(qty=1, sl=99)
1291
+
1292
+ trades = Backtest(SHORT_DATA, S).run()._trades
1293
+ self.assertEqual(trades["SL"].fillna(0).tolist(), [0, 99])
1294
+ self.assertEqual(trades["TP"].fillna(0).tolist(), [111, 0])
1295
+
1296
+ def test_sl_value_in_trades_df_when_gapped_through(self):
1297
+ # An SL that is gapped through (the bar opens beyond the stop, so the
1298
+ # fill price is worse than the stop price) must still be recorded in
1299
+ # stats._trades["SL"]. See GH issue #1340.
1300
+ class S(_S):
1301
+ def next(self):
1302
+ if len(self.data.index) == 9:
1303
+ self.buy(qty=1, sl=99.5)
1304
+
1305
+ trades = Backtest(SHORT_DATA, S).run()._trades
1306
+ self.assertEqual(len(trades), 1)
1307
+ trade = trades.iloc[0]
1308
+ # The long SL (99.5) is gapped through on the next bar's open, so the
1309
+ # trade exits at that worse market price rather than at the stop price.
1310
+ self.assertEqual(trade["ExitPrice"], 99.19)
1311
+ # ... yet the SL value must be preserved in the trades data frame.
1312
+ self.assertEqual(trade["SL"], 99.5)