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,62 @@
1
+ """Independent deterministic OHLC fill-price policy."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from openbacktesting.broker.models import finite
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Slippage:
10
+ """No slippage, fixed-price slippage, or basis-point slippage; apply once per fill."""
11
+
12
+ mode: str = "none"
13
+ value: float = 0
14
+
15
+ def __post_init__(self):
16
+ if self.mode not in ("none", "fixed", "bps"):
17
+ raise ValueError("slippage mode must be none/fixed/bps")
18
+ object.__setattr__(self, "value", finite(self.value, "slippage"))
19
+ if self.value < 0 or (self.mode == "none" and self.value != 0):
20
+ raise ValueError("Invalid slippage value")
21
+
22
+ def cost(self, price: float) -> float:
23
+ """Return a nonnegative slippage distance per unit."""
24
+ if self.mode == "fixed":
25
+ return self.value
26
+ return price * self.value / 10000 if self.mode == "bps" else 0.0
27
+
28
+
29
+ def match_price(request, *, triggered, open_, high, low):
30
+ """Return the candidate reference price and trigger state without modifying the request."""
31
+ buy = request.side == "buy"
32
+ stop = request.stop if not triggered else None
33
+ if stop is not None:
34
+ if not (high >= stop if buy else low <= stop):
35
+ return None, False
36
+ triggered = True
37
+ if request.limit is not None:
38
+ limit = request.limit
39
+ if not (low <= limit if buy else high >= limit):
40
+ return None, triggered
41
+ if stop is not None and (limit <= stop if buy else limit >= stop):
42
+ return None, triggered
43
+ return (min(stop or open_, limit) if buy else max(stop or open_, limit)), triggered
44
+ return ((max(open_, stop) if buy else min(open_, stop)) if stop else open_), triggered
45
+
46
+
47
+ def execution_price(base, side, *, spread, slippage, instrument, limit, bounds=None):
48
+ """Apply costs and the grid before checking the limit; ordinary execution is constrained to the OHLC range."""
49
+ sign = 1 if side == "buy" else -1
50
+ price = base * (1 + sign * spread) + sign * slippage.cost(base)
51
+ price = instrument.price(price, side=side)
52
+ if limit is not None and (price > limit if side == "buy" else price < limit):
53
+ return None
54
+ if bounds is not None:
55
+ low, high = bounds
56
+ price = min(max(price, low), high)
57
+ price = instrument.price(price, side=side)
58
+ if not low <= price <= high:
59
+ return None
60
+ if limit is not None and (price > limit if side == "buy" else price < limit):
61
+ return None
62
+ return price
@@ -0,0 +1,106 @@
1
+ """Explicit asset units; trading rules are not inferred from market-data decimal places."""
2
+
3
+ from dataclasses import dataclass
4
+ from decimal import ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_UP, Decimal, localcontext
5
+
6
+ import numpy as np
7
+
8
+ from openbacktesting.broker.models import finite
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class InstrumentSpec:
13
+ """Actual asset quantity and price grids; currency is only a label."""
14
+
15
+ symbol: str = "asset"
16
+ quantity_step: float = 1
17
+ min_quantity: float | None = None
18
+ price_tick: float | None = None
19
+ min_notional: float = 0
20
+ currency: str | None = None
21
+
22
+ def __post_init__(self):
23
+ if not isinstance(self.symbol, str) or not self.symbol:
24
+ raise ValueError("symbol must be a nonempty string")
25
+ if self.currency is not None and not isinstance(self.currency, str):
26
+ raise TypeError("currency must be a string or None")
27
+ step = finite(self.quantity_step, "quantity_step", positive=True)
28
+ minimum = (
29
+ step
30
+ if self.min_quantity is None
31
+ else finite(self.min_quantity, "min_quantity", positive=True)
32
+ )
33
+ object.__setattr__(self, "quantity_step", step)
34
+ object.__setattr__(self, "min_quantity", minimum)
35
+ if self.price_tick is not None:
36
+ object.__setattr__(
37
+ self, "price_tick", finite(self.price_tick, "price_tick", positive=True)
38
+ )
39
+ object.__setattr__(self, "min_notional", finite(self.min_notional, "min_notional"))
40
+ if self.min_notional < 0:
41
+ raise ValueError("min_notional must not be negative")
42
+
43
+ def quantity(self, value, *, floor=False, opening=True) -> float:
44
+ """Direct requests must lie on the grid; budgeted quantities may round toward zero."""
45
+ value = finite(value, "qty")
46
+ if value < 0:
47
+ raise ValueError("qty must not be negative")
48
+ lots = Decimal(str(value)) / Decimal(str(self.quantity_step))
49
+ whole = lots.to_integral_value(rounding=ROUND_DOWN)
50
+ if not floor and lots != whole:
51
+ raise ValueError("qty is not on the quantity_step grid")
52
+ result = float(whole * Decimal(str(self.quantity_step)))
53
+ if opening and result < self.min_quantity:
54
+ if floor:
55
+ return 0.0
56
+ raise ValueError("qty is below min_quantity")
57
+ return result
58
+
59
+ def price(self, value, *, side=None) -> float:
60
+ """Validate requested prices strictly; normalize execution prices in the adverse direction."""
61
+ value = finite(value, "price", positive=True)
62
+ if self.price_tick is None:
63
+ return value
64
+ ticks = Decimal(str(value)) / Decimal(str(self.price_tick))
65
+ rounded = ticks.to_integral_value(rounding=ROUND_CEILING if side == "buy" else ROUND_FLOOR)
66
+ if side is None and ticks != rounded:
67
+ raise ValueError("price is not on the price_tick grid")
68
+ return float(rounded * Decimal(str(self.price_tick)))
69
+
70
+ def validate_data(self, frame):
71
+ """Trading market data must be ordered, unique, and have valid prices; explicit ticks must not be silently corrected."""
72
+ if not frame.index.is_monotonic_increasing or not frame.index.is_unique:
73
+ raise ValueError("Market-data index must be increasing and unique")
74
+ values = frame[["Open", "High", "Low", "Close"]].to_numpy(dtype=float)
75
+ if not np.isfinite(values).all() or (values <= 0).any():
76
+ raise ValueError("OHLC must contain finite positive prices")
77
+ if self.price_tick is not None:
78
+ for price in np.unique(values):
79
+ self.price(float(price))
80
+
81
+ def round_price_to_tick(self, value: float) -> float:
82
+ """Round prices to the nearest explicit tick; half ticks round to the higher price.
83
+
84
+ Args:
85
+ value: Finite positive price; booleans are not accepted.
86
+
87
+ Returns:
88
+ Finite positive float passing this instrument's strict grid validation; does not change matching-rounding policy.
89
+
90
+ Raises:
91
+ TypeError: value is nonnumeric or a boolean.
92
+ ValueError: Missing tick, invalid input, zero result, or a valid grid value cannot be represented as a float.
93
+ """
94
+ value = finite(value, "price", positive=True)
95
+ if self.price_tick is None:
96
+ raise ValueError("round_price_to_tick requires an explicit price_tick")
97
+ # Decimal exponents of two finite floats can differ by more than 600; prevent default precision from losing half ticks.
98
+ with localcontext() as context:
99
+ context.prec = 700
100
+ tick = Decimal(str(self.price_tick))
101
+ ticks = (Decimal(str(value)) / tick).to_integral_value(rounding=ROUND_HALF_UP)
102
+ exact = ticks * tick
103
+ result = float(exact)
104
+ if Decimal(str(result)) != exact:
105
+ raise ValueError("Rounded price cannot represent the grid exactly as a float")
106
+ return self.price(result)
@@ -0,0 +1,60 @@
1
+ """Balance, actual fees, and position snapshots independent of strategies and result tables."""
2
+
3
+ from openbacktesting.broker.models import AccountSnapshot, PositionSnapshot
4
+
5
+
6
+ class TradingLedger:
7
+ """Record only actual executions; candidate calculations must not call apply."""
8
+
9
+ def __init__(self, balance):
10
+ self.initial_balance = float(balance)
11
+ self.balance = float(balance)
12
+ self.realized_pnl_gross = 0.0
13
+ self.fees_paid = 0.0
14
+
15
+ def apply(self, realized_pnl_gross, fee):
16
+ self.realized_pnl_gross += realized_pnl_gross
17
+ self.fees_paid += fee
18
+ self.balance = self.initial_balance + self.realized_pnl_gross - self.fees_paid
19
+
20
+ def position(self, trades, price):
21
+ long_qty = sum(t.size for t in trades if t.size > 0)
22
+ short_qty = sum(-t.size for t in trades if t.size < 0)
23
+ long_cost = sum(t.size * t.entry_price for t in trades if t.size > 0)
24
+ short_cost = sum(-t.size * t.entry_price for t in trades if t.size < 0)
25
+ long_avg = long_cost / long_qty if long_qty else None
26
+ short_avg = short_cost / short_qty if short_qty else None
27
+ average = None if long_qty and short_qty else long_avg or short_avg
28
+ gross_pl = sum(t.size * (price - t.entry_price) for t in trades)
29
+ fees = sum(t._entry_commission for t in trades)
30
+ cost = long_cost + short_cost
31
+ return PositionSnapshot(
32
+ long_qty - short_qty,
33
+ long_qty + short_qty,
34
+ long_qty,
35
+ short_qty,
36
+ long_avg,
37
+ short_avg,
38
+ average,
39
+ (long_qty + short_qty) * price,
40
+ (long_qty - short_qty) * price,
41
+ gross_pl,
42
+ gross_pl - fees,
43
+ fees,
44
+ (gross_pl - fees) / cost if cost and average is not None else None,
45
+ )
46
+
47
+ def account(self, position, margin):
48
+ equity = self.balance + position.unrealized_pnl_gross
49
+ used = position.gross_notional * margin
50
+ return AccountSnapshot(
51
+ self.balance,
52
+ equity,
53
+ self.realized_pnl_gross,
54
+ position.unrealized_pnl_gross,
55
+ self.fees_paid,
56
+ used,
57
+ max(0.0, equity - used),
58
+ position.gross_notional,
59
+ position.net_notional,
60
+ )
@@ -0,0 +1,236 @@
1
+ """Trade requests, events, and accounting snapshots independent of strategies and execution loops."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, fields
7
+ from enum import StrEnum
8
+ from functools import cache
9
+ from math import isfinite
10
+ from numbers import Real
11
+ from typing import Literal
12
+
13
+
14
+ def finite(value, name: str, *, positive=False) -> float:
15
+ """Reject booleans, nonnumeric values, and nonfinite values; return a normalized number."""
16
+ if isinstance(value, bool) or not isinstance(value, Real):
17
+ raise TypeError(f"{name} must be a finite number")
18
+ value = float(value)
19
+ if not isfinite(value) or (positive and value <= 0):
20
+ raise ValueError(f"{name} must be a {'positive ' if positive else ''}finite number")
21
+ return value
22
+
23
+
24
+ class OrderStatus(StrEnum):
25
+ """Observable order states; terminal states cannot reopen."""
26
+
27
+ SUBMITTED = "Submitted"
28
+ ACCEPTED = "Accepted"
29
+ PARTIALLY_FILLED = "PartiallyFilled"
30
+ FILLED = "Filled"
31
+ CANCELED = "Canceled"
32
+ EXPIRED = "Expired"
33
+ REJECTED = "Rejected"
34
+
35
+ @property
36
+ def terminal(self) -> bool:
37
+ """Whether the state is terminal."""
38
+ return self in (self.FILLED, self.CANCELED, self.EXPIRED, self.REJECTED)
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ClosedTradeStats:
43
+ """Cumulative closed portions as of the read; fees are allocated, and unrealized profit or loss is excluded.
44
+
45
+ Count closed portions rather than trade_id values; classify wins and losses by strictly positive or negative net profit or loss.
46
+ losing_amount is a nonnegative loss amount; average net profit or loss and the 0–1 win rate are None for empty records.
47
+ Values contain no runtime objects; saved snapshots are unaffected by later fills.
48
+ """
49
+
50
+ closed_count: int = 0
51
+ win_count: int = 0
52
+ loss_count: int = 0
53
+ even_count: int = 0
54
+ winning_profit: float = 0.0
55
+ losing_amount: float = 0.0
56
+ net_profit: float = 0.0
57
+ average_profit: float | None = None
58
+ win_rate: float | None = None
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class OrderSizingSnapshot:
63
+ """Sizing basis frozen at submission; budgets do not guarantee actual fill notional or fees.
64
+
65
+ mode is qty/amount/percent, source is explicit/default; percent value
66
+ uses percentage points (20 means 20%), and budget is None in qty mode.
67
+ """
68
+
69
+ mode: Literal["qty", "amount", "percent"]
70
+ source: Literal["explicit", "default"]
71
+ value: float
72
+ equity: float
73
+ reference_price: float
74
+ budget: float | None
75
+ qty: float
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class NormalizedOrderRequest:
80
+ """Immutable normalized request; tag is stored as JSON text, and reads return independent plain values."""
81
+
82
+ side: str
83
+ qty: float
84
+ limit: float | None = None
85
+ stop: float | None = None
86
+ sl: float | None = None
87
+ tp: float | None = None
88
+ reduce_only: bool = False
89
+ trade_id: str | None = None
90
+ expires_at: object = None
91
+ valid_bars: int | None = None
92
+ tag_json: str = "null"
93
+
94
+ @property
95
+ def tag(self):
96
+ """Return an independent tag copy; modifying it does not change the request."""
97
+ return json.loads(self.tag_json)
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class OrderSnapshot:
102
+ """Frozen order values at a state transition."""
103
+
104
+ order_id: str
105
+ status: OrderStatus
106
+ filled_qty: float
107
+ remaining_qty: float
108
+ avg_fill_price: float | None
109
+ reason: str | None
110
+ triggered: bool
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class OrderEvent:
115
+ """Immutable order event with time and phase."""
116
+
117
+ sequence: int
118
+ bar_index: int
119
+ timestamp: object
120
+ phase: str
121
+ snapshot: OrderSnapshot
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class Fill:
126
+ """An actual execution; fees are recorded exactly once here."""
127
+
128
+ fill_id: str
129
+ order_id: str
130
+ sequence: int
131
+ side: str
132
+ qty: float
133
+ price: float
134
+ fee: float
135
+ bar_index: int
136
+ timestamp: object
137
+ phase: str
138
+ reason: str
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class TradeAllocation:
143
+ """Allocation of an execution quantity to entry lots or closed portions."""
144
+
145
+ allocation_id: str
146
+ fill_id: str
147
+ trade_id: str
148
+ entry_fill_id: str
149
+ kind: str
150
+ qty: float
151
+ fee: float
152
+ entry_fee: float
153
+ realized_pnl_gross: float
154
+
155
+
156
+ @dataclass(frozen=True)
157
+ class AccountSnapshot:
158
+ """Single-currency linear margin account; balance is not securities settlement cash."""
159
+
160
+ balance: float
161
+ equity: float
162
+ realized_pnl_gross: float
163
+ unrealized_pnl_gross: float
164
+ fees_paid: float
165
+ margin_used: float
166
+ margin_available: float
167
+ gross_notional: float
168
+ net_notional: float
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class PositionSnapshot:
173
+ """Net position, two-sided gross exposure, and explicit gross and net profit or loss."""
174
+
175
+ net_qty: float
176
+ gross_qty: float
177
+ long_qty: float
178
+ short_qty: float
179
+ long_avg_price: float | None
180
+ short_avg_price: float | None
181
+ avg_price: float | None
182
+ gross_notional: float
183
+ net_notional: float
184
+ unrealized_pnl_gross: float
185
+ unrealized_pnl_net: float
186
+ entry_fees_remaining: float
187
+ return_fraction: float | None
188
+
189
+ @property
190
+ def has_exposure(self) -> bool:
191
+ """A two-sided net-zero position still has gross exposure."""
192
+ return self.gross_qty > 0
193
+
194
+
195
+ @dataclass(frozen=True)
196
+ class RunSummary:
197
+ """Frozen summary for the finish hook; error text does not replace the original exception."""
198
+
199
+ reason: str
200
+ account: AccountSnapshot
201
+ error: str | None = None
202
+
203
+
204
+ def record(value) -> dict:
205
+ """Produce a value-only dictionary without exposing live trading objects."""
206
+ names = _field_names(type(value))
207
+ return dict(zip(names, map(value.__dict__.__getitem__, names)))
208
+
209
+
210
+ def columns(model) -> list[str]:
211
+ """Empty result tables retain model fields."""
212
+ return list(_field_names(model))
213
+
214
+
215
+ @cache
216
+ def _field_names(model):
217
+ return tuple(item.name for item in fields(model))
218
+
219
+
220
+ def freeze_tag(value) -> str:
221
+ """Freeze JSON-safe tags as text; reject nonstring keys and nonfinite numbers."""
222
+
223
+ def validate(item):
224
+ if isinstance(item, dict):
225
+ if not all(isinstance(key, str) for key in item):
226
+ raise TypeError("tag mapping keys must be strings")
227
+ for child in item.values():
228
+ validate(child)
229
+ elif isinstance(item, list):
230
+ for child in item:
231
+ validate(child)
232
+ elif item is not None and not isinstance(item, (str, bool, int, float)):
233
+ raise TypeError("tag supports only plain JSON values")
234
+
235
+ validate(value)
236
+ return json.dumps(value, ensure_ascii=False, allow_nan=False, sort_keys=True)
@@ -0,0 +1,99 @@
1
+ """Read-only execution observations: independent sequence, effective price ranges, and lot extrema."""
2
+
3
+ from openbacktesting.broker.models import record
4
+
5
+
6
+ def _update_extrema(state, bar_index, favorable, adverse):
7
+ # Initialize at zero; strict comparisons exclude negative magnitudes and preserve the first position of tied extrema.
8
+ if favorable > state["mfe"]:
9
+ state["mfe"], state["mfeBar"] = favorable, bar_index
10
+ if adverse > state["mae"]:
11
+ state["mae"], state["maeBar"] = adverse, bar_index
12
+
13
+
14
+ class Observations:
15
+ def __init__(self):
16
+ self.rows = []
17
+ self.lots = {}
18
+ self.closed = {}
19
+ self.changed_bar = -1
20
+
21
+ def sample(
22
+ self,
23
+ broker,
24
+ price,
25
+ phase,
26
+ *,
27
+ ordered=True,
28
+ coverage="complete_under_bar_model",
29
+ facts=None,
30
+ range_eligible=False,
31
+ ):
32
+ if facts is None:
33
+ position = broker.ledger.position(broker.trades, price)
34
+ facts = record(broker.ledger.account(position, broker.margin)) | record(position)
35
+ row = {
36
+ "observationId": f"observation-{len(self.rows) + 1}",
37
+ "observationSequence": len(self.rows) + 1,
38
+ "barIndex": broker._i,
39
+ "phase": phase,
40
+ "price": price,
41
+ "fillSequence": broker.fills[-1].sequence if broker.fills else 0,
42
+ "ordered": ordered,
43
+ "coverage": coverage,
44
+ "rangeEligible": range_eligible,
45
+ **facts,
46
+ }
47
+ self.rows.append(row)
48
+ return row
49
+
50
+ def mark(self, broker, trade, price, *, partial=False):
51
+ state = self.lots[trade.trade_id]
52
+ value = (price - trade.entry_price) * (1 if trade.size > 0 else -1)
53
+ _update_extrema(state, broker._i, value, -value)
54
+ state["partial"] |= partial
55
+
56
+ def before(self, broker, price, phase, conditional):
57
+ self.sample(
58
+ broker,
59
+ price,
60
+ "before_" + phase,
61
+ coverage="partial" if conditional else "complete_under_bar_model",
62
+ )
63
+ for trade in broker.trades:
64
+ self.mark(broker, trade, price, partial=conditional)
65
+ if conditional or phase == "close":
66
+ self.changed_bar = broker._i
67
+
68
+ def opened(self, broker, trade, conditional, phase):
69
+ self.lots[trade.trade_id] = {
70
+ "mfe": 0.0,
71
+ "mae": 0.0,
72
+ "mfeBar": broker._i,
73
+ "maeBar": broker._i,
74
+ "partial": conditional,
75
+ "firstFullBar": broker._i + int(conditional or phase != "execute"),
76
+ }
77
+
78
+ def close(self, trade, allocation_id):
79
+ self.closed[allocation_id] = self.lots[trade.trade_id].copy()
80
+
81
+ def mark_bar(self, broker):
82
+ high, low = (broker._data._current_value(n) for n in ("High", "Low"))
83
+ if high < low:
84
+ high, low = low, high
85
+ for trade in broker.trades:
86
+ state = self.lots[trade.trade_id]
87
+ if state["firstFullBar"] <= broker._i:
88
+ entry = trade.entry_price
89
+ if trade.size > 0:
90
+ favorable, adverse = high - entry, entry - low
91
+ else:
92
+ favorable, adverse = entry - low, high - entry
93
+ _update_extrema(state, broker._i, favorable, adverse)
94
+ else:
95
+ self.mark(broker, trade, broker.last_price)
96
+
97
+ def bar(self, broker):
98
+ self.mark_bar(broker)
99
+ self.sample(broker, broker.last_price, "bar", range_eligible=self.changed_bar != broker._i)