back-trader-python 1.4.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 (465) hide show
  1. back_trader_python-1.4.0.dist-info/METADATA +1491 -0
  2. back_trader_python-1.4.0.dist-info/RECORD +465 -0
  3. back_trader_python-1.4.0.dist-info/WHEEL +5 -0
  4. back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
  5. back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
  6. backtrader/__init__.py +148 -0
  7. backtrader/_cerebro/__init__.py +5 -0
  8. backtrader/_cerebro/channel.py +382 -0
  9. backtrader/_cerebro/execution.py +377 -0
  10. backtrader/_cerebro/lifecycle.py +143 -0
  11. backtrader/_cerebro/notifications.py +150 -0
  12. backtrader/_cerebro/presentation.py +230 -0
  13. backtrader/_cerebro/registry.py +593 -0
  14. backtrader/_cerebro/runnext.py +551 -0
  15. backtrader/_cerebro/runonce.py +142 -0
  16. backtrader/analyzer.py +594 -0
  17. backtrader/analyzers/__init__.py +50 -0
  18. backtrader/analyzers/annualreturn.py +226 -0
  19. backtrader/analyzers/calmar.py +165 -0
  20. backtrader/analyzers/drawdown.py +287 -0
  21. backtrader/analyzers/leverage.py +112 -0
  22. backtrader/analyzers/logreturnsrolling.py +190 -0
  23. backtrader/analyzers/periodstats.py +153 -0
  24. backtrader/analyzers/positions.py +119 -0
  25. backtrader/analyzers/pyfolio.py +470 -0
  26. backtrader/analyzers/returns.py +192 -0
  27. backtrader/analyzers/sharpe.py +307 -0
  28. backtrader/analyzers/sharpe_ratio_stats.py +534 -0
  29. backtrader/analyzers/sqn.py +112 -0
  30. backtrader/analyzers/timereturn.py +192 -0
  31. backtrader/analyzers/total_value.py +75 -0
  32. backtrader/analyzers/tradeanalyzer.py +278 -0
  33. backtrader/analyzers/transactions.py +141 -0
  34. backtrader/analyzers/vwr.py +245 -0
  35. backtrader/bokeh/__init__.py +155 -0
  36. backtrader/bokeh/analyzers/__init__.py +13 -0
  37. backtrader/bokeh/analyzers/plot.py +192 -0
  38. backtrader/bokeh/analyzers/recorder.py +181 -0
  39. backtrader/bokeh/app.py +1094 -0
  40. backtrader/bokeh/live/__init__.py +11 -0
  41. backtrader/bokeh/live/client.py +352 -0
  42. backtrader/bokeh/live/datahandler.py +346 -0
  43. backtrader/bokeh/plot_adapter.py +200 -0
  44. backtrader/bokeh/schemes/__init__.py +14 -0
  45. backtrader/bokeh/schemes/blackly.py +76 -0
  46. backtrader/bokeh/schemes/scheme.py +150 -0
  47. backtrader/bokeh/schemes/tradimo.py +82 -0
  48. backtrader/bokeh/tab.py +125 -0
  49. backtrader/bokeh/tabs/__init__.py +30 -0
  50. backtrader/bokeh/tabs/analyzer.py +120 -0
  51. backtrader/bokeh/tabs/config.py +154 -0
  52. backtrader/bokeh/tabs/live.py +109 -0
  53. backtrader/bokeh/tabs/log.py +185 -0
  54. backtrader/bokeh/tabs/metadata.py +182 -0
  55. backtrader/bokeh/tabs/performance.py +359 -0
  56. backtrader/bokeh/tabs/source.py +70 -0
  57. backtrader/bokeh/utils/__init__.py +8 -0
  58. backtrader/bokeh/utils/helpers.py +167 -0
  59. backtrader/bokeh/webapp.py +164 -0
  60. backtrader/broker.py +478 -0
  61. backtrader/brokers/__init__.py +36 -0
  62. backtrader/brokers/bbroker.py +2576 -0
  63. backtrader/brokers/btapibroker.py +8227 -0
  64. backtrader/brokers/hft/__init__.py +89 -0
  65. backtrader/brokers/hft/binance_bbo.py +625 -0
  66. backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
  67. backtrader/brokers/hft/examples.py +1228 -0
  68. backtrader/brokers/hft/exchange.py +380 -0
  69. backtrader/brokers/hft/latency.py +309 -0
  70. backtrader/brokers/hft/matching_core.py +572 -0
  71. backtrader/brokers/hft/queue.py +238 -0
  72. backtrader/brokers/hft/recorder.py +88 -0
  73. backtrader/brokers/hft/state.py +138 -0
  74. backtrader/brokers/impact_models.py +118 -0
  75. backtrader/brokers/mixbroker.py +895 -0
  76. backtrader/brokers/tickbroker.py +1991 -0
  77. backtrader/btrun/__init__.py +12 -0
  78. backtrader/btrun/btrun.py +1218 -0
  79. backtrader/cerebro.py +828 -0
  80. backtrader/channel.py +682 -0
  81. backtrader/channels/__init__.py +23 -0
  82. backtrader/channels/bridge.py +186 -0
  83. backtrader/channels/funding.py +248 -0
  84. backtrader/channels/live_queue.py +216 -0
  85. backtrader/channels/live_validator.py +294 -0
  86. backtrader/channels/orderbook.py +257 -0
  87. backtrader/channels/tick.py +202 -0
  88. backtrader/comminfo.py +665 -0
  89. backtrader/commissions/__init__.py +106 -0
  90. backtrader/commissions/ctpoption.py +993 -0
  91. backtrader/configs/account_config_example.yaml +8 -0
  92. backtrader/dataseries.py +379 -0
  93. backtrader/errors.py +106 -0
  94. backtrader/events.py +980 -0
  95. backtrader/feed.py +1523 -0
  96. backtrader/feeds/__init__.py +75 -0
  97. backtrader/feeds/barrier.py +2006 -0
  98. backtrader/feeds/blaze.py +118 -0
  99. backtrader/feeds/btapifeed.py +1538 -0
  100. backtrader/feeds/btcsv.py +203 -0
  101. backtrader/feeds/chainer.py +114 -0
  102. backtrader/feeds/cryptohftdata.py +164 -0
  103. backtrader/feeds/csvgeneric.py +1205 -0
  104. backtrader/feeds/ctpcohort.py +1051 -0
  105. backtrader/feeds/influxfeed.py +158 -0
  106. backtrader/feeds/livefeed.py +71 -0
  107. backtrader/feeds/mixed_channel.py +108 -0
  108. backtrader/feeds/mt4csv.py +42 -0
  109. backtrader/feeds/pandafeed.py +381 -0
  110. backtrader/feeds/quandl.py +256 -0
  111. backtrader/feeds/rollover.py +229 -0
  112. backtrader/feeds/sierrachart.py +30 -0
  113. backtrader/feeds/vchart.py +162 -0
  114. backtrader/feeds/vchartcsv.py +84 -0
  115. backtrader/feeds/vchartfile.py +153 -0
  116. backtrader/feeds/yahoo.py +399 -0
  117. backtrader/fillers.py +148 -0
  118. backtrader/filters/__init__.py +34 -0
  119. backtrader/filters/bsplitter.py +127 -0
  120. backtrader/filters/calendardays.py +121 -0
  121. backtrader/filters/datafiller.py +192 -0
  122. backtrader/filters/datafilter.py +74 -0
  123. backtrader/filters/daysteps.py +96 -0
  124. backtrader/filters/heikinashi.py +63 -0
  125. backtrader/filters/renko.py +164 -0
  126. backtrader/filters/session.py +289 -0
  127. backtrader/flt.py +80 -0
  128. backtrader/functions.py +960 -0
  129. backtrader/indicator.py +449 -0
  130. backtrader/indicators/__init__.py +148 -0
  131. backtrader/indicators/accdecoscillator.py +110 -0
  132. backtrader/indicators/aroon.py +300 -0
  133. backtrader/indicators/atr.py +315 -0
  134. backtrader/indicators/awesomeoscillator.py +122 -0
  135. backtrader/indicators/basicops.py +834 -0
  136. backtrader/indicators/bollinger.py +223 -0
  137. backtrader/indicators/cci.py +89 -0
  138. backtrader/indicators/channels_ext.py +83 -0
  139. backtrader/indicators/contrib/__init__.py +228 -0
  140. backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
  141. backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
  142. backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
  143. backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
  144. backtrader/indicators/contrib/adxdmi.py +34 -0
  145. backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
  146. backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
  147. backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
  148. backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
  149. backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
  150. backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
  151. backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
  152. backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
  153. backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
  154. backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
  155. backtrader/indicators/contrib/average_change_candle.py +165 -0
  156. backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
  157. backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
  158. backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
  159. backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
  160. backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
  161. backtrader/indicators/contrib/blau_csi.py +76 -0
  162. backtrader/indicators/contrib/blau_ergodic.py +53 -0
  163. backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
  164. backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
  165. backtrader/indicators/contrib/blau_tvi.py +55 -0
  166. backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
  167. backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
  168. backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
  169. backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
  170. backtrader/indicators/contrib/bsi_indicator.py +87 -0
  171. backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
  172. backtrader/indicators/contrib/bulls_power.py +56 -0
  173. backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
  174. backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
  175. backtrader/indicators/contrib/candle_stop_color.py +46 -0
  176. backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
  177. backtrader/indicators/contrib/candlesticks_bw.py +45 -0
  178. backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
  179. backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
  180. backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
  181. backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
  182. backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
  183. backtrader/indicators/contrib/cg_oscillator.py +40 -0
  184. backtrader/indicators/contrib/close_line_cci.py +38 -0
  185. backtrader/indicators/contrib/close_price_fractals.py +47 -0
  186. backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
  187. backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
  188. backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
  189. backtrader/indicators/contrib/color_hma.py +71 -0
  190. backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
  191. backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
  192. backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
  193. backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
  194. backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
  195. backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
  196. backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
  197. backtrader/indicators/contrib/color_x2_ma.py +49 -0
  198. backtrader/indicators/contrib/color_x_derivative.py +63 -0
  199. backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
  200. backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
  201. backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
  202. backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
  203. backtrader/indicators/contrib/derivative_indicator.py +95 -0
  204. backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
  205. backtrader/indicators/contrib/digital_macd.py +200 -0
  206. backtrader/indicators/contrib/donchian_channels_system.py +45 -0
  207. backtrader/indicators/contrib/dots_indicator.py +93 -0
  208. backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
  209. backtrader/indicators/contrib/ema_rsi_va.py +80 -0
  210. backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
  211. backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
  212. backtrader/indicators/contrib/fatl_filter.py +179 -0
  213. backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
  214. backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
  215. backtrader/indicators/contrib/fisher_org_v1.py +102 -0
  216. backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
  217. backtrader/indicators/contrib/force_index_ema.py +96 -0
  218. backtrader/indicators/contrib/force_index_ema_2.py +27 -0
  219. backtrader/indicators/contrib/forecast_oscilator.py +145 -0
  220. backtrader/indicators/contrib/fractal_amambk.py +81 -0
  221. backtrader/indicators/contrib/frama_series.py +84 -0
  222. backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
  223. backtrader/indicators/contrib/go_indicator.py +93 -0
  224. backtrader/indicators/contrib/hlr_indicator.py +95 -0
  225. backtrader/indicators/contrib/hma.py +50 -0
  226. backtrader/indicators/contrib/i4_drfv2.py +34 -0
  227. backtrader/indicators/contrib/i4_drfv3.py +38 -0
  228. backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
  229. backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
  230. backtrader/indicators/contrib/i_gap_indicator.py +45 -0
  231. backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
  232. backtrader/indicators/contrib/i_trend_indicator.py +125 -0
  233. backtrader/indicators/contrib/iamma_indicator.py +39 -0
  234. backtrader/indicators/contrib/indexed_moving_average.py +33 -0
  235. backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
  236. backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
  237. backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
  238. backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
  239. backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
  240. backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
  241. backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
  242. backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
  243. backtrader/indicators/contrib/kalman_filter_line.py +127 -0
  244. backtrader/indicators/contrib/kama_indicator.py +150 -0
  245. backtrader/indicators/contrib/karacatica_indicator.py +99 -0
  246. backtrader/indicators/contrib/kdj_indicator.py +59 -0
  247. backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
  248. backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
  249. backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
  250. backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
  251. backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
  252. backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
  253. backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
  254. backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
  255. backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
  256. backtrader/indicators/contrib/loco_indicator.py +88 -0
  257. backtrader/indicators/contrib/lrma_indicator.py +185 -0
  258. backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
  259. backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
  260. backtrader/indicators/contrib/macd2_indicator.py +61 -0
  261. backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
  262. backtrader/indicators/contrib/malr_indicator.py +77 -0
  263. backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
  264. backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
  265. backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
  266. backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
  267. backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
  268. backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
  269. backtrader/indicators/contrib/nrtr_indicator.py +95 -0
  270. backtrader/indicators/contrib/p_channel_system.py +40 -0
  271. backtrader/indicators/contrib/percent_envelope.py +37 -0
  272. backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
  273. backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
  274. backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
  275. backtrader/indicators/contrib/price_extreme_channel.py +35 -0
  276. backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
  277. backtrader/indicators/contrib/ravi_indicator.py +40 -0
  278. backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
  279. backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
  280. backtrader/indicators/contrib/renko_level.py +85 -0
  281. backtrader/indicators/contrib/renko_line_break.py +91 -0
  282. backtrader/indicators/contrib/rftl_indicator.py +41 -0
  283. backtrader/indicators/contrib/rkd_indicator.py +53 -0
  284. backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
  285. backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
  286. backtrader/indicators/contrib/rsi_slowdown.py +57 -0
  287. backtrader/indicators/contrib/rsioma_v2.py +41 -0
  288. backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
  289. backtrader/indicators/contrib/safe_adx.py +89 -0
  290. backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
  291. backtrader/indicators/contrib/sidus_indicator.py +105 -0
  292. backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
  293. backtrader/indicators/contrib/sliding_range_color.py +56 -0
  294. backtrader/indicators/contrib/slow_stoch.py +42 -0
  295. backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
  296. backtrader/indicators/contrib/smoothed_rsi.py +31 -0
  297. backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
  298. backtrader/indicators/contrib/stalin_indicator.py +152 -0
  299. backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
  300. backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
  301. backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
  302. backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
  303. backtrader/indicators/contrib/t3_average.py +76 -0
  304. backtrader/indicators/contrib/t3_indicator.py +40 -0
  305. backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
  306. backtrader/indicators/contrib/three_candles_indicator.py +70 -0
  307. backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
  308. backtrader/indicators/contrib/time_line.py +57 -0
  309. backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
  310. backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
  311. backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
  312. backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
  313. backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
  314. backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
  315. backtrader/indicators/contrib/trigger_line.py +66 -0
  316. backtrader/indicators/contrib/triple_ema_rate.py +34 -0
  317. backtrader/indicators/contrib/trvi_indicator.py +194 -0
  318. backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
  319. backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
  320. backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
  321. backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
  322. backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
  323. backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
  324. backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
  325. backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
  326. backtrader/indicators/contrib/vwma_candle.py +57 -0
  327. backtrader/indicators/contrib/vwma_digit_system.py +70 -0
  328. backtrader/indicators/contrib/wami.py +43 -0
  329. backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
  330. backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
  331. backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
  332. backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
  333. backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
  334. backtrader/indicators/contrib/xma_ichimoku.py +163 -0
  335. backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
  336. backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
  337. backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
  338. backtrader/indicators/contrib/xmacd_indicator.py +70 -0
  339. backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
  340. backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
  341. backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
  342. backtrader/indicators/contrib/xrvi_indicator.py +130 -0
  343. backtrader/indicators/contrib/zero_lag_macd.py +36 -0
  344. backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
  345. backtrader/indicators/contrib/zpf_indicator.py +115 -0
  346. backtrader/indicators/crossover.py +337 -0
  347. backtrader/indicators/dema.py +175 -0
  348. backtrader/indicators/demarker.py +270 -0
  349. backtrader/indicators/deviation.py +284 -0
  350. backtrader/indicators/directionalmove.py +1071 -0
  351. backtrader/indicators/dma.py +112 -0
  352. backtrader/indicators/dpo.py +96 -0
  353. backtrader/indicators/dv2.py +56 -0
  354. backtrader/indicators/ema.py +145 -0
  355. backtrader/indicators/envelope.py +475 -0
  356. backtrader/indicators/hadelta.py +198 -0
  357. backtrader/indicators/heikinashi.py +153 -0
  358. backtrader/indicators/hma.py +153 -0
  359. backtrader/indicators/hurst.py +151 -0
  360. backtrader/indicators/ichimoku.py +267 -0
  361. backtrader/indicators/kama.py +181 -0
  362. backtrader/indicators/kst.py +159 -0
  363. backtrader/indicators/lrsi.py +125 -0
  364. backtrader/indicators/mabase.py +147 -0
  365. backtrader/indicators/macd.py +322 -0
  366. backtrader/indicators/momentum.py +267 -0
  367. backtrader/indicators/moneyflow.py +237 -0
  368. backtrader/indicators/mt5atr.py +124 -0
  369. backtrader/indicators/myind.py +179 -0
  370. backtrader/indicators/obv.py +94 -0
  371. backtrader/indicators/ols.py +265 -0
  372. backtrader/indicators/oscillator.py +161 -0
  373. backtrader/indicators/percentchange.py +83 -0
  374. backtrader/indicators/percentrank.py +46 -0
  375. backtrader/indicators/pivotpoint.py +469 -0
  376. backtrader/indicators/prettygoodoscillator.py +113 -0
  377. backtrader/indicators/priceops_ext.py +123 -0
  378. backtrader/indicators/priceoscillator.py +262 -0
  379. backtrader/indicators/psar.py +212 -0
  380. backtrader/indicators/rmi.py +69 -0
  381. backtrader/indicators/rsi.py +440 -0
  382. backtrader/indicators/sma.py +141 -0
  383. backtrader/indicators/smma.py +116 -0
  384. backtrader/indicators/spread.py +54 -0
  385. backtrader/indicators/stochastic.py +263 -0
  386. backtrader/indicators/supertrend.py +436 -0
  387. backtrader/indicators/trend_ext.py +105 -0
  388. backtrader/indicators/trix.py +202 -0
  389. backtrader/indicators/tsi.py +155 -0
  390. backtrader/indicators/ultimateoscillator.py +158 -0
  391. backtrader/indicators/vortex.py +62 -0
  392. backtrader/indicators/williams.py +194 -0
  393. backtrader/indicators/wma.py +103 -0
  394. backtrader/indicators/zlema.py +135 -0
  395. backtrader/indicators/zlind.py +104 -0
  396. backtrader/linebuffer.py +3155 -0
  397. backtrader/lineiterator.py +2911 -0
  398. backtrader/lineroot.py +1106 -0
  399. backtrader/lineseries.py +2559 -0
  400. backtrader/live_trading/__init__.py +31 -0
  401. backtrader/live_trading/interface.py +404 -0
  402. backtrader/mathsupport.py +94 -0
  403. backtrader/metabase.py +1804 -0
  404. backtrader/mixins/__init__.py +21 -0
  405. backtrader/mixins/singleton.py +118 -0
  406. backtrader/observer.py +106 -0
  407. backtrader/observers/__init__.py +45 -0
  408. backtrader/observers/benchmark.py +126 -0
  409. backtrader/observers/broker.py +184 -0
  410. backtrader/observers/buysell.py +144 -0
  411. backtrader/observers/drawdown.py +161 -0
  412. backtrader/observers/logreturns.py +113 -0
  413. backtrader/observers/timereturn.py +86 -0
  414. backtrader/observers/trade_logger.py +2972 -0
  415. backtrader/observers/tradelogger.py +6 -0
  416. backtrader/observers/trades.py +258 -0
  417. backtrader/order.py +1114 -0
  418. backtrader/parameters.py +2345 -0
  419. backtrader/plot/__init__.py +54 -0
  420. backtrader/plot/finance.py +1022 -0
  421. backtrader/plot/formatters.py +200 -0
  422. backtrader/plot/locator.py +353 -0
  423. backtrader/plot/multicursor.py +495 -0
  424. backtrader/plot/plot.py +2500 -0
  425. backtrader/plot/plot_plotly.py +1351 -0
  426. backtrader/plot/scheme.py +253 -0
  427. backtrader/plot/utils.py +104 -0
  428. backtrader/position.py +290 -0
  429. backtrader/position_modes.py +132 -0
  430. backtrader/profiles.py +254 -0
  431. backtrader/reports/__init__.py +39 -0
  432. backtrader/reports/charts.py +371 -0
  433. backtrader/reports/performance.py +620 -0
  434. backtrader/reports/reporter.py +660 -0
  435. backtrader/resamplerfilter.py +1001 -0
  436. backtrader/signal.py +118 -0
  437. backtrader/signals/__init__.py +17 -0
  438. backtrader/sizer.py +114 -0
  439. backtrader/sizers/__init__.py +26 -0
  440. backtrader/sizers/fixedsize.py +161 -0
  441. backtrader/sizers/percents_sizer.py +119 -0
  442. backtrader/store.py +221 -0
  443. backtrader/stores/__init__.py +33 -0
  444. backtrader/stores/btapistore.py +15506 -0
  445. backtrader/stores/livestore.py +137 -0
  446. backtrader/stores/vchartfile.py +96 -0
  447. backtrader/strategy.py +3655 -0
  448. backtrader/talib.py +280 -0
  449. backtrader/test_helpers.py +96 -0
  450. backtrader/timer.py +358 -0
  451. backtrader/trade.py +442 -0
  452. backtrader/tradingcal.py +361 -0
  453. backtrader/utils/__init__.py +68 -0
  454. backtrader/utils/autodict.py +251 -0
  455. backtrader/utils/date.py +71 -0
  456. backtrader/utils/dateintern.py +509 -0
  457. backtrader/utils/flushfile.py +94 -0
  458. backtrader/utils/fractal.py +101 -0
  459. backtrader/utils/get_metrics.py +101 -0
  460. backtrader/utils/load_data.py +209 -0
  461. backtrader/utils/log_message.py +998 -0
  462. backtrader/utils/ordereddefaultdict.py +75 -0
  463. backtrader/utils/py3.py +296 -0
  464. backtrader/version.py +21 -0
  465. backtrader/writer.py +372 -0
@@ -0,0 +1,1398 @@
1
+ """Cross-check backtrader's HFT engine against hftbacktest on Binance BBO data.
2
+
3
+ Diagnostic/validation script that runs the same Binance BBO/depth dataset
4
+ through both the backtrader tick broker and the reference ``hftbacktest``
5
+ engine and compares fills/snapshots, used to verify matching-engine fidelity.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import bisect
11
+ import json
12
+ from collections import Counter
13
+ from dataclasses import asdict, dataclass
14
+ from pathlib import Path
15
+ from types import SimpleNamespace
16
+ from typing import Iterator, Optional
17
+
18
+ import numpy as np
19
+
20
+ from backtrader.brokers.hft.examples import build_quote_builder, get_hftbacktest_example_spec
21
+ from backtrader.brokers.hft.exchange import FillRole, OrderResult, QueueExchangeModel
22
+ from backtrader.brokers.hft.queue import ProbQueueModel
23
+ from backtrader.brokers.tickbroker import TickBroker
24
+ from backtrader.channels.orderbook import OrderBookChannel
25
+ from backtrader.channels.tick import TickChannel
26
+ from backtrader.order import Order
27
+
28
+ from ...utils.log_message import get_logger
29
+
30
+ logger = get_logger(__name__)
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ComparisonFill:
35
+ """Represents a fill (trade) for comparison purposes.
36
+
37
+ Attributes:
38
+ side: Order side ('buy' or 'sell').
39
+ price: Fill price.
40
+ size: Fill quantity.
41
+ timestamp_ns: Timestamp in nanoseconds.
42
+ local_timestamp_ns: Local timestamp in nanoseconds.
43
+ exch_timestamp_ns: Exchange timestamp in nanoseconds.
44
+ order_ref: Order reference ID.
45
+ """
46
+
47
+ side: str
48
+ price: float
49
+ size: float
50
+ timestamp_ns: Optional[int] = None
51
+ local_timestamp_ns: Optional[int] = None
52
+ exch_timestamp_ns: Optional[int] = None
53
+ order_ref: Optional[int] = None
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class EngineResult:
58
+ """Result from a backtesting engine.
59
+
60
+ Attributes:
61
+ balance: Final account balance.
62
+ position: Final position quantity.
63
+ num_trades: Total number of trades.
64
+ fills: List of all comparison fills.
65
+ """
66
+
67
+ balance: float
68
+ position: float
69
+ num_trades: int
70
+ fills: list[ComparisonFill]
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class StrategyComparisonResult:
75
+ """Comparison result between backtrader and hftbacktest engines.
76
+
77
+ Attributes:
78
+ strategy: Strategy name.
79
+ decision_interval_ns: Decision interval in nanoseconds.
80
+ backtrader: Engine result from backtrader.
81
+ hftbacktest: Engine result from hftbacktest.
82
+ matches: Dictionary of match status by field.
83
+ deltas: Dictionary of delta values by field.
84
+ """
85
+
86
+ strategy: str
87
+ decision_interval_ns: int
88
+ backtrader: EngineResult
89
+ hftbacktest: EngineResult
90
+ matches: dict[str, bool]
91
+ deltas: dict[str, float]
92
+
93
+
94
+ class _DataRef:
95
+ """Reference data object for market data symbols."""
96
+
97
+ def __init__(self, symbol: str):
98
+ """Initialize a data reference.
99
+
100
+ Args:
101
+ symbol: Trading symbol.
102
+ """
103
+ self._name = symbol
104
+ self.name = symbol
105
+ self.symbol = symbol
106
+
107
+
108
+ _BACKTRADER_COMPARISON_INITIAL_CASH = 1_000_000_000.0
109
+ _DEPTH_EVENT = 1
110
+ _TRADE_EVENT = 2
111
+ _BUY_EVENT = 1 << 29
112
+ _SELL_EVENT = 1 << 28
113
+ _EXCH_EVENT = 1 << 31
114
+
115
+
116
+ def _build_runtime_builder(spec, market_data_path):
117
+ builder = build_quote_builder(spec)
118
+ requirement = next(
119
+ (item for item in spec.input_requirements if item.name == "precompute_data"), None
120
+ )
121
+ if requirement is None:
122
+ return builder
123
+ precompute_path = _resolve_required_input_path(market_data_path, requirement.patterns)
124
+ if precompute_path is None:
125
+ raise FileNotFoundError(
126
+ f"Missing required precompute_data for strategy '{spec.name}'. Expected one of: {', '.join(requirement.patterns)}"
127
+ )
128
+ precompute = _load_precompute_data(precompute_path)
129
+ if hasattr(builder, "precompute_data"):
130
+ builder.precompute_data = precompute
131
+ return builder
132
+
133
+
134
+ def _resolve_required_input_path(market_data_path, patterns):
135
+ market_data_path = Path(market_data_path)
136
+ for pattern in patterns:
137
+ for base in (market_data_path.parent, *market_data_path.parents):
138
+ candidate = base / pattern
139
+ if candidate.exists():
140
+ return candidate
141
+ return None
142
+
143
+
144
+ def _load_precompute_data(precompute_path):
145
+ with np.load(str(precompute_path)) as payload:
146
+ if "data" in payload:
147
+ return payload["data"]
148
+ keys = list(payload.keys())
149
+ if keys:
150
+ return payload[keys[0]]
151
+ raise ValueError(f"Unable to load precompute data from {precompute_path}")
152
+
153
+
154
+ def _builder_order_qty(builder) -> float:
155
+ return float(getattr(builder, "current_order_qty", getattr(builder, "order_qty", 1.0)))
156
+
157
+
158
+ class _NoPartialQueueExchangeModel(QueueExchangeModel):
159
+ """Exchange model that simulates no partial fills.
160
+
161
+ Used for comparing backtrader against hftbacktest which uses
162
+ no-partial-fill exchange semantics.
163
+
164
+ Attributes:
165
+ queue_model_power: Power parameter for probability queue model.
166
+ lot_size: Minimum order quantity.
167
+ tick_size: Minimum price increment.
168
+ """
169
+
170
+ def __init__(
171
+ self, queue_model_power: float = 2.0, lot_size: float = 1.0, tick_size: float = None
172
+ ):
173
+ """Initialize the no-partial-fill exchange model.
174
+
175
+ Args:
176
+ queue_model_power: Queue model power parameter (default: 2.0).
177
+ lot_size: Minimum lot size (default: 1.0).
178
+ tick_size: Minimum price increment (default: None).
179
+ """
180
+ super().__init__(
181
+ queue_model=ProbQueueModel(power=queue_model_power, lot_size=lot_size),
182
+ tick_size=tick_size,
183
+ )
184
+
185
+ def on_new_order(self, order, ob_snapshot):
186
+ """Handle a new order event.
187
+
188
+ Args:
189
+ order: The incoming order.
190
+ ob_snapshot: Current order book snapshot.
191
+
192
+ Returns:
193
+ OrderResult indicating the order action.
194
+ """
195
+ if getattr(order, "_fill_role", None) == FillRole.MAKER:
196
+ return OrderResult(action="PENDING")
197
+ result = super().on_new_order(order, ob_snapshot)
198
+ if result.action == "PENDING" and getattr(order, "_fill_role", None) == FillRole.MAKER:
199
+ levels = (
200
+ getattr(ob_snapshot, "bids", None)
201
+ if order.isbuy()
202
+ else getattr(ob_snapshot, "asks", None)
203
+ )
204
+ level_qty = self._level_qty(levels, float(order.price))
205
+ order._queue_wait_for_first_visible_level = (
206
+ order.isbuy()
207
+ and level_qty <= 1e-12
208
+ and float(getattr(order, "_queue_initial_ahead", 0.0)) <= 1e-12
209
+ )
210
+ return result
211
+
212
+ def _same_price_tick(self, price_a: float, price_b: float) -> bool:
213
+ if self._tick_size is not None and self._tick_size > 0:
214
+ return round(price_a / self._tick_size) == round(price_b / self._tick_size)
215
+ return price_a == price_b
216
+
217
+ def _level_qty(self, levels, price: float) -> float:
218
+ if not levels:
219
+ return 0.0
220
+ for level_price, level_qty in levels:
221
+ if self._same_price_tick(float(level_price), float(price)):
222
+ return float(level_qty)
223
+ return 0.0
224
+
225
+ def _trade_reaches_order(
226
+ self, order_price: float, trade_price: float, is_buy_order: bool
227
+ ) -> bool:
228
+ if self._tick_size is not None and self._tick_size > 0:
229
+ order_tick = round(order_price / self._tick_size)
230
+ trade_tick = round(trade_price / self._tick_size)
231
+ return trade_tick <= order_tick if is_buy_order else trade_tick >= order_tick
232
+ return trade_price <= order_price if is_buy_order else trade_price >= order_price
233
+
234
+ def on_trade(self, trade_event, pending_orders):
235
+ """Handle a trade event and determine fills.
236
+
237
+ Args:
238
+ trade_event: Trade event with price and direction.
239
+ pending_orders: List of pending maker orders.
240
+
241
+ Returns:
242
+ List of (order, price, remaining_size, role) tuples for fills.
243
+ """
244
+ fills: list = []
245
+ trade_price = getattr(trade_event, "price", None)
246
+ direction = str(getattr(trade_event, "direction", "")).lower()
247
+ if trade_price is None or direction not in {"buy", "sell"}:
248
+ return fills
249
+
250
+ for order in pending_orders:
251
+ if getattr(order, "_fill_role", None) != FillRole.MAKER:
252
+ continue
253
+ if direction == "buy":
254
+ if order.isbuy():
255
+ continue
256
+ if not self._trade_reaches_order(
257
+ float(order.price), float(trade_price), is_buy_order=False
258
+ ):
259
+ continue
260
+ else:
261
+ if not order.isbuy():
262
+ continue
263
+ if not self._trade_reaches_order(
264
+ float(order.price), float(trade_price), is_buy_order=True
265
+ ):
266
+ continue
267
+
268
+ remaining = getattr(getattr(order, "executed", None), "remsize", None)
269
+ if remaining is None:
270
+ remaining = getattr(order, "size", 0.0)
271
+ remaining = abs(float(remaining))
272
+ if remaining <= 0.0:
273
+ continue
274
+
275
+ if self._same_price_tick(float(order.price), float(trade_price)):
276
+ if getattr(order, "_queue_wait_for_first_visible_level", False):
277
+ continue
278
+ fillable = self._queue_model.update_on_trade(order, trade_event)
279
+ if fillable <= 1e-12 and float(getattr(order, "_queue_ahead", 0.0)) >= 0.0:
280
+ continue
281
+ fills.append((order, float(order.price), remaining, FillRole.MAKER))
282
+ return fills
283
+
284
+ def on_depth_update(self, ob_event, pending_orders):
285
+ """Handle an order book depth update event.
286
+
287
+ Args:
288
+ ob_event: Order book event with bids/asks.
289
+ pending_orders: List of pending maker orders.
290
+
291
+ Returns:
292
+ List of (order, price, remaining_size, role) tuples for fills.
293
+ """
294
+ fills: list = []
295
+ prev_bids = getattr(ob_event, "previous_bids", None) or []
296
+ prev_asks = getattr(ob_event, "previous_asks", None) or []
297
+ curr_bids = getattr(ob_event, "bids", None) or []
298
+ curr_asks = getattr(ob_event, "asks", None) or []
299
+
300
+ for order in pending_orders:
301
+ if getattr(order, "_fill_role", None) != FillRole.MAKER:
302
+ continue
303
+ price = getattr(order, "price", None)
304
+ if price is None:
305
+ continue
306
+ prev_qty = self._level_qty(prev_bids if order.isbuy() else prev_asks, float(price))
307
+ new_qty = self._level_qty(curr_bids if order.isbuy() else curr_asks, float(price))
308
+ if (
309
+ getattr(order, "_queue_wait_for_first_visible_level", False)
310
+ and prev_qty <= 1e-12
311
+ and new_qty > 1e-12
312
+ ):
313
+ order._queue_ahead = float(new_qty)
314
+ order._queue_initial_ahead = max(
315
+ float(getattr(order, "_queue_initial_ahead", 0.0)), float(new_qty)
316
+ )
317
+ order._queue_trade_qty = 0.0
318
+ order._queue_fillable = 0.0
319
+ order._queue_wait_for_first_visible_level = False
320
+ continue
321
+ if abs(prev_qty - new_qty) <= 1e-12:
322
+ continue
323
+ self._queue_model.update_on_depth(order, prev_qty, new_qty)
324
+ return fills
325
+
326
+
327
+ def _fill_counter(fills: list[ComparisonFill]) -> Counter:
328
+ return Counter(
329
+ (item.side, round(float(item.price), 8), round(float(item.size), 8)) for item in fills
330
+ )
331
+
332
+
333
+ def _ordered_fill_sequence(fills: list[ComparisonFill]) -> list[tuple[object, ...]]:
334
+ return [(item.side, round(float(item.price), 8), round(float(item.size), 8)) for item in fills]
335
+
336
+
337
+ def _normalized_fill_sequence(fills: list[ComparisonFill]) -> list[tuple[object, ...]]:
338
+ return sorted(
339
+ (
340
+ int(item.timestamp_ns or item.exch_timestamp_ns or item.local_timestamp_ns or 0),
341
+ item.side,
342
+ round(float(item.price), 8),
343
+ round(float(item.size), 8),
344
+ int(item.exch_timestamp_ns or 0),
345
+ )
346
+ for item in fills
347
+ )
348
+
349
+
350
+ def compare_binance_bbo_strategy(
351
+ strategy_name: str,
352
+ orderbook_path,
353
+ tick_path,
354
+ market_data_path,
355
+ tick_size: float,
356
+ lot_size: float,
357
+ symbol: str = "ETH/USDT",
358
+ decision_interval_ns: Optional[int] = None,
359
+ maker_commission: Optional[float] = None,
360
+ taker_commission: Optional[float] = None,
361
+ queue_model_power: Optional[float] = None,
362
+ max_decisions: Optional[int] = None,
363
+ ) -> StrategyComparisonResult:
364
+ """Compare backtrader HFT engine against hftbacktest on Binance BBO data.
365
+
366
+ Args:
367
+ strategy_name: Name of the HFT strategy to compare.
368
+ orderbook_path: Path to orderbook depth data file.
369
+ tick_path: Path to tick trade data file.
370
+ market_data_path: Base path for market data.
371
+ tick_size: Minimum price increment.
372
+ lot_size: Minimum order quantity.
373
+ symbol: Trading symbol (default: "ETH/USDT").
374
+ decision_interval_ns: Decision interval in nanoseconds.
375
+ maker_commission: Maker commission rate (overrides spec).
376
+ taker_commission: Taker commission rate (overrides spec).
377
+ queue_model_power: Queue model power (overrides spec).
378
+ max_decisions: Maximum number of decisions to process.
379
+
380
+ Returns:
381
+ StrategyComparisonResult with backtrader and hftbacktest results.
382
+ """
383
+ spec = get_hftbacktest_example_spec(strategy_name)
384
+ interval_ns = int(decision_interval_ns or spec.strategy.interval_ns)
385
+ maker_fee = float(
386
+ maker_commission
387
+ if maker_commission is not None
388
+ else spec.asset_parameters.get("maker_commission", 0.0)
389
+ )
390
+ taker_fee = float(
391
+ taker_commission
392
+ if taker_commission is not None
393
+ else spec.asset_parameters.get("taker_commission", 0.0)
394
+ )
395
+ queue_power = float(
396
+ queue_model_power
397
+ if queue_model_power is not None
398
+ else spec.asset_parameters.get("queue_model_power", 2.0)
399
+ )
400
+
401
+ backtrader_builder = _build_runtime_builder(spec, market_data_path)
402
+ hftbacktest_builder = _build_runtime_builder(spec, market_data_path)
403
+ decision_anchor_ns = _market_data_exchange_anchor_ns(market_data_path)
404
+ exchange_book = _market_data_exchange_book(market_data_path)
405
+
406
+ backtrader_result = _run_backtrader_strategy(
407
+ orderbook_path=orderbook_path,
408
+ tick_path=tick_path,
409
+ market_data_path=market_data_path,
410
+ symbol=symbol,
411
+ builder=backtrader_builder,
412
+ tick_size=tick_size,
413
+ lot_size=lot_size,
414
+ decision_anchor_ns=decision_anchor_ns,
415
+ exchange_book=exchange_book,
416
+ interval_ns=interval_ns,
417
+ maker_commission=maker_fee,
418
+ taker_commission=taker_fee,
419
+ queue_model_power=queue_power,
420
+ max_decisions=max_decisions,
421
+ )
422
+ hftbacktest_result = _run_hftbacktest_strategy(
423
+ market_data_path=market_data_path,
424
+ builder=hftbacktest_builder,
425
+ tick_size=tick_size,
426
+ lot_size=lot_size,
427
+ interval_ns=interval_ns,
428
+ maker_commission=maker_fee,
429
+ taker_commission=taker_fee,
430
+ queue_model_power=queue_power,
431
+ max_decisions=max_decisions,
432
+ )
433
+ bt_fills = _fill_counter(backtrader_result.fills)
434
+ hft_fills = _fill_counter(hftbacktest_result.fills)
435
+ bt_in_order = _ordered_fill_sequence(backtrader_result.fills)
436
+ hft_in_order = _ordered_fill_sequence(hftbacktest_result.fills)
437
+ bt_normalized = _normalized_fill_sequence(backtrader_result.fills)
438
+ hft_normalized = _normalized_fill_sequence(hftbacktest_result.fills)
439
+
440
+ return StrategyComparisonResult(
441
+ strategy=strategy_name,
442
+ decision_interval_ns=interval_ns,
443
+ backtrader=backtrader_result,
444
+ hftbacktest=hftbacktest_result,
445
+ matches={
446
+ "balance": abs(backtrader_result.balance - hftbacktest_result.balance) < 1e-5,
447
+ "position": abs(backtrader_result.position - hftbacktest_result.position) < 1e-9,
448
+ "num_trades": backtrader_result.num_trades == hftbacktest_result.num_trades,
449
+ "fills": bt_fills == hft_fills,
450
+ "fills_in_order": bt_in_order == hft_in_order,
451
+ "fills_normalized_order": bt_normalized == hft_normalized,
452
+ },
453
+ deltas={
454
+ "balance": backtrader_result.balance - hftbacktest_result.balance,
455
+ "position": backtrader_result.position - hftbacktest_result.position,
456
+ "num_trades": float(backtrader_result.num_trades - hftbacktest_result.num_trades),
457
+ },
458
+ )
459
+
460
+
461
+ def comparison_to_json(result: StrategyComparisonResult) -> str:
462
+ """Convert a strategy comparison result to JSON string.
463
+
464
+ Args:
465
+ result: StrategyComparisonResult to serialize.
466
+
467
+ Returns:
468
+ JSON-formatted string representation.
469
+ """
470
+ payload = asdict(result)
471
+ return json.dumps(payload, indent=2)
472
+
473
+
474
+ def engine_result_to_json(result: EngineResult) -> str:
475
+ """Convert an engine result to JSON string.
476
+
477
+ Args:
478
+ result: EngineResult to serialize.
479
+
480
+ Returns:
481
+ JSON-formatted string representation.
482
+ """
483
+ payload = asdict(result)
484
+ return json.dumps(payload, indent=2)
485
+
486
+
487
+ def run_binance_bbo_backtrader_strategy(
488
+ strategy_name: str,
489
+ orderbook_path,
490
+ tick_path,
491
+ market_data_path,
492
+ tick_size: float,
493
+ lot_size: float,
494
+ symbol: str = "ETH/USDT",
495
+ decision_interval_ns: Optional[int] = None,
496
+ maker_commission: Optional[float] = None,
497
+ taker_commission: Optional[float] = None,
498
+ queue_model_power: Optional[float] = None,
499
+ max_decisions: Optional[int] = None,
500
+ ) -> EngineResult:
501
+ """Run backtrader HFT strategy on Binance BBO data.
502
+
503
+ Args:
504
+ strategy_name: Name of the HFT strategy to run.
505
+ orderbook_path: Path to orderbook depth data file.
506
+ tick_path: Path to tick trade data file.
507
+ market_data_path: Base path for market data.
508
+ tick_size: Minimum price increment.
509
+ lot_size: Minimum order quantity.
510
+ symbol: Trading symbol (default: "ETH/USDT").
511
+ decision_interval_ns: Decision interval in nanoseconds.
512
+ maker_commission: Maker commission rate (overrides spec).
513
+ taker_commission: Taker commission rate (overrides spec).
514
+ queue_model_power: Queue model power (overrides spec).
515
+ max_decisions: Maximum number of decisions to process.
516
+
517
+ Returns:
518
+ EngineResult with backtrader backtest results.
519
+ """
520
+ spec = get_hftbacktest_example_spec(strategy_name)
521
+ interval_ns = int(decision_interval_ns or spec.strategy.interval_ns)
522
+ maker_fee = float(
523
+ maker_commission
524
+ if maker_commission is not None
525
+ else spec.asset_parameters.get("maker_commission", 0.0)
526
+ )
527
+ taker_fee = float(
528
+ taker_commission
529
+ if taker_commission is not None
530
+ else spec.asset_parameters.get("taker_commission", 0.0)
531
+ )
532
+ queue_power = float(
533
+ queue_model_power
534
+ if queue_model_power is not None
535
+ else spec.asset_parameters.get("queue_model_power", 2.0)
536
+ )
537
+ builder = _build_runtime_builder(spec, market_data_path)
538
+ decision_anchor_ns = _market_data_exchange_anchor_ns(market_data_path)
539
+ exchange_book = _market_data_exchange_book(market_data_path)
540
+ return _run_backtrader_strategy(
541
+ orderbook_path=orderbook_path,
542
+ tick_path=tick_path,
543
+ market_data_path=market_data_path,
544
+ symbol=symbol,
545
+ builder=builder,
546
+ tick_size=tick_size,
547
+ lot_size=lot_size,
548
+ decision_anchor_ns=decision_anchor_ns,
549
+ exchange_book=exchange_book,
550
+ interval_ns=interval_ns,
551
+ maker_commission=maker_fee,
552
+ taker_commission=taker_fee,
553
+ queue_model_power=queue_power,
554
+ max_decisions=max_decisions,
555
+ )
556
+
557
+
558
+ def run_binance_bbo_hftbacktest_strategy(
559
+ strategy_name: str,
560
+ orderbook_path,
561
+ tick_path,
562
+ market_data_path,
563
+ tick_size: float,
564
+ lot_size: float,
565
+ symbol: str = "ETH/USDT",
566
+ decision_interval_ns: Optional[int] = None,
567
+ maker_commission: Optional[float] = None,
568
+ taker_commission: Optional[float] = None,
569
+ queue_model_power: Optional[float] = None,
570
+ max_decisions: Optional[int] = None,
571
+ ) -> EngineResult:
572
+ """Run hftbacktest strategy on Binance BBO data.
573
+
574
+ Args:
575
+ strategy_name: Name of the HFT strategy to run.
576
+ orderbook_path: Path to orderbook depth data file (unused, for API compat).
577
+ tick_path: Path to tick trade data file (unused, for API compat).
578
+ market_data_path: Base path for market data.
579
+ tick_size: Minimum price increment.
580
+ lot_size: Minimum order quantity.
581
+ symbol: Trading symbol (default: "ETH/USDT").
582
+ decision_interval_ns: Decision interval in nanoseconds.
583
+ maker_commission: Maker commission rate (overrides spec).
584
+ taker_commission: Taker commission rate (overrides spec).
585
+ queue_model_power: Queue model power (overrides spec).
586
+ max_decisions: Maximum number of decisions to process.
587
+
588
+ Returns:
589
+ EngineResult with hftbacktest results.
590
+ """
591
+ _ = (orderbook_path, tick_path, symbol)
592
+ spec = get_hftbacktest_example_spec(strategy_name)
593
+ interval_ns = int(decision_interval_ns or spec.strategy.interval_ns)
594
+ maker_fee = float(
595
+ maker_commission
596
+ if maker_commission is not None
597
+ else spec.asset_parameters.get("maker_commission", 0.0)
598
+ )
599
+ taker_fee = float(
600
+ taker_commission
601
+ if taker_commission is not None
602
+ else spec.asset_parameters.get("taker_commission", 0.0)
603
+ )
604
+ queue_power = float(
605
+ queue_model_power
606
+ if queue_model_power is not None
607
+ else spec.asset_parameters.get("queue_model_power", 2.0)
608
+ )
609
+ builder = _build_runtime_builder(spec, market_data_path)
610
+ return _run_hftbacktest_strategy(
611
+ market_data_path=market_data_path,
612
+ builder=builder,
613
+ tick_size=tick_size,
614
+ lot_size=lot_size,
615
+ interval_ns=interval_ns,
616
+ maker_commission=maker_fee,
617
+ taker_commission=taker_fee,
618
+ queue_model_power=queue_power,
619
+ max_decisions=max_decisions,
620
+ )
621
+
622
+
623
+ def _run_backtrader_strategy(
624
+ orderbook_path,
625
+ tick_path,
626
+ market_data_path,
627
+ symbol: str,
628
+ builder,
629
+ tick_size: float,
630
+ lot_size: float,
631
+ decision_anchor_ns: Optional[int],
632
+ exchange_book,
633
+ interval_ns: int,
634
+ maker_commission: float,
635
+ taker_commission: float,
636
+ queue_model_power: float,
637
+ max_decisions: Optional[int],
638
+ ) -> EngineResult:
639
+ data = _DataRef(symbol)
640
+ broker = TickBroker(
641
+ cash=_BACKTRADER_COMPARISON_INITIAL_CASH,
642
+ checksubmit=False,
643
+ allow_partial=False,
644
+ exchange_model=_NoPartialQueueExchangeModel(
645
+ queue_model_power=queue_model_power, lot_size=lot_size, tick_size=tick_size
646
+ ),
647
+ )
648
+ broker.setcommission(
649
+ commission=0.0,
650
+ maker_commission=maker_commission,
651
+ taker_commission=taker_commission,
652
+ name=data.name,
653
+ )
654
+ local_orderbooks = iter(
655
+ OrderBookChannel(symbol=symbol, dataname=str(orderbook_path), depth=1).load()
656
+ )
657
+ next_local_orderbook = next(local_orderbooks, None)
658
+ exchange_events = _iter_exchange_market_events(market_data_path, symbol)
659
+ depth_probe = _create_depth_probe(market_data_path, tick_size=tick_size, lot_size=lot_size)
660
+ depth_probe_timestamp_ns = (
661
+ int(getattr(depth_probe, "current_timestamp", 0) or 0) if depth_probe is not None else None
662
+ )
663
+ working_orders: dict = {}
664
+ latest_snapshot = None
665
+ latest_exchange_snapshot = None
666
+ next_decision_ns = None
667
+ decisions = 0
668
+ decision_trades: list = []
669
+
670
+ for channel_type, event in exchange_events:
671
+ event_ns = _event_timestamp_ns(event)
672
+ reached_limit = False
673
+ if next_decision_ns is None:
674
+ next_decision_ns = (
675
+ int(decision_anchor_ns + interval_ns)
676
+ if decision_anchor_ns is not None
677
+ else int(event_ns + interval_ns)
678
+ )
679
+ while next_decision_ns is not None and event_ns > next_decision_ns:
680
+ decision_ts = float(next_decision_ns / 1_000_000_000.0)
681
+ latest_snapshot, next_local_orderbook = _advance_local_orderbook_snapshot(
682
+ latest_snapshot,
683
+ next_local_orderbook,
684
+ local_orderbooks,
685
+ decision_ts,
686
+ )
687
+ if latest_snapshot is None:
688
+ next_decision_ns += interval_ns
689
+ continue
690
+ builder_context = {
691
+ "timestamp_ns": int(next_decision_ns),
692
+ "last_trades": tuple(decision_trades),
693
+ }
694
+ quotes = builder(broker.getposition(data).size, latest_snapshot, builder_context)
695
+ decision_trades = []
696
+ depth_probe_timestamp_ns, decision_depth = _advance_depth_probe(
697
+ depth_probe, depth_probe_timestamp_ns, next_decision_ns
698
+ )
699
+ finalized_exchange_snapshot: Optional[object] = (
700
+ latest_exchange_snapshot
701
+ if int(getattr(latest_exchange_snapshot, "timestamp_ns", 0) or 0)
702
+ == int(next_decision_ns)
703
+ else None
704
+ )
705
+ if finalized_exchange_snapshot is not None:
706
+ base_submission_snapshot = finalized_exchange_snapshot
707
+ elif decision_depth is not None and _is_finite_book(
708
+ float(decision_depth.best_bid), float(decision_depth.best_ask)
709
+ ):
710
+ base_submission_snapshot = SimpleNamespace(
711
+ bids=[(float(decision_depth.best_bid), float(decision_depth.best_bid_qty))],
712
+ asks=[(float(decision_depth.best_ask), float(decision_depth.best_ask_qty))],
713
+ )
714
+ else:
715
+ base_submission_snapshot = (
716
+ latest_exchange_snapshot
717
+ or _lookup_exchange_snapshot(exchange_book, next_decision_ns)
718
+ or latest_snapshot
719
+ )
720
+ submission_snapshot = base_submission_snapshot
721
+ if submission_snapshot is not None:
722
+ submission_fallback_snapshot = (
723
+ None if finalized_exchange_snapshot is not None else latest_snapshot
724
+ )
725
+ submission_snapshot = _augment_submission_snapshot(
726
+ base_submission_snapshot,
727
+ decision_depth,
728
+ quotes,
729
+ tick_size=float(tick_size),
730
+ fallback_snapshot=submission_fallback_snapshot,
731
+ )
732
+ working_orders = _submit_or_replace_quotes(
733
+ broker,
734
+ data,
735
+ working_orders,
736
+ quotes,
737
+ order_qty=_builder_order_qty(builder),
738
+ tick_size=float(tick_size),
739
+ snapshot=submission_snapshot,
740
+ activation_timestamp_ns=int(next_decision_ns),
741
+ )
742
+ decisions += 1
743
+ next_decision_ns += interval_ns
744
+ if max_decisions is not None and decisions >= max_decisions:
745
+ reached_limit = True
746
+ break
747
+ if channel_type == "orderbook":
748
+ depth_probe_timestamp_ns, event_depth = _advance_depth_probe(
749
+ depth_probe, depth_probe_timestamp_ns, event_ns
750
+ )
751
+ orderbook_event = _augment_orderbook_snapshot_for_orders(
752
+ event,
753
+ event_depth,
754
+ list(broker._orders_by_symbol.get(data.name, [])),
755
+ tick_size=float(tick_size),
756
+ )
757
+ latest_exchange_snapshot = orderbook_event
758
+ broker.process_orderbook(orderbook_event)
759
+ else:
760
+ decision_trades.append(event)
761
+ broker.process_tick(event)
762
+ if reached_limit:
763
+ break
764
+
765
+ fills = [
766
+ ComparisonFill(
767
+ side=item["side"],
768
+ price=float(item["price"]),
769
+ size=float(item["size"]),
770
+ timestamp_ns=int(item.get("timestamp_ns", 0)) or None,
771
+ local_timestamp_ns=int(item.get("timestamp_ns", 0)) or None,
772
+ exch_timestamp_ns=int(item.get("timestamp_ns", 0)) or None,
773
+ order_ref=int(item.get("order_ref")) if item.get("order_ref") is not None else None,
774
+ )
775
+ for item in broker.order_history
776
+ if item.get("status") in ("Partial", "Completed") and float(item.get("size", 0.0)) > 0.0
777
+ ]
778
+ state = broker.state_values(data)
779
+ return EngineResult(
780
+ balance=float(state["balance"] - _BACKTRADER_COMPARISON_INITIAL_CASH + state["fee"]),
781
+ position=float(broker.getposition(data).size),
782
+ num_trades=len(fills),
783
+ fills=fills,
784
+ )
785
+
786
+
787
+ def _run_hftbacktest_strategy(
788
+ market_data_path,
789
+ builder,
790
+ tick_size: float,
791
+ lot_size: float,
792
+ interval_ns: int,
793
+ maker_commission: float,
794
+ taker_commission: float,
795
+ queue_model_power: float,
796
+ max_decisions: Optional[int],
797
+ ) -> EngineResult:
798
+ try:
799
+ from hftbacktest import BacktestAsset, HashMapMarketDepthBacktest
800
+ from hftbacktest.order import BUY, GTX, LIMIT, PARTIALLY_FILLED, SELL
801
+ except Exception as exc:
802
+ logger.error("binance_bbo_compare:800 re-raising Exception", exc_info=True)
803
+ raise RuntimeError("hftbacktest is required to run this comparison") from exc
804
+
805
+ asset = (
806
+ BacktestAsset()
807
+ .data([str(Path(market_data_path))])
808
+ .linear_asset(1.0)
809
+ .constant_order_latency(0, 0)
810
+ .power_prob_queue_model(float(queue_model_power))
811
+ .no_partial_fill_exchange()
812
+ .trading_value_fee_model(float(maker_commission), float(taker_commission))
813
+ .tick_size(float(tick_size))
814
+ .lot_size(float(lot_size))
815
+ )
816
+ hbt = HashMapMarketDepthBacktest([asset])
817
+ fills: list = []
818
+ seen_exec_qty: dict = {}
819
+ decisions = 0
820
+
821
+ while hbt.elapse(interval_ns) == 0:
822
+ depth = hbt.depth(0)
823
+ if not _is_finite_book(depth.best_bid, depth.best_ask):
824
+ continue
825
+
826
+ _collect_hft_fills(hbt.orders(0), seen_exec_qty, fills)
827
+ hbt.clear_inactive_orders(0)
828
+ last_trades = list(hbt.last_trades(0))
829
+
830
+ snapshot = SimpleNamespace(
831
+ bids=[(float(depth.best_bid), float(depth.best_bid_qty))],
832
+ asks=[(float(depth.best_ask), float(depth.best_ask_qty))],
833
+ )
834
+ quotes = builder(
835
+ float(hbt.position(0)),
836
+ snapshot,
837
+ {
838
+ "timestamp_ns": int(getattr(hbt, "current_timestamp", 0)),
839
+ "last_trades": tuple(last_trades),
840
+ },
841
+ )
842
+ if last_trades and hasattr(hbt, "clear_last_trades"):
843
+ hbt.clear_last_trades(0)
844
+ _replace_hft_orders(
845
+ hbt=hbt,
846
+ quotes=quotes,
847
+ tick_size=float(tick_size),
848
+ order_qty=_builder_order_qty(builder),
849
+ buy_flag=BUY,
850
+ sell_flag=SELL,
851
+ gtx_flag=GTX,
852
+ limit_flag=LIMIT,
853
+ partial_filled_flag=PARTIALLY_FILLED,
854
+ )
855
+ decisions += 1
856
+ if max_decisions is not None and decisions >= max_decisions:
857
+ break
858
+
859
+ _collect_hft_fills(hbt.orders(0), seen_exec_qty, fills)
860
+ state = hbt.state_values(0)
861
+ return EngineResult(
862
+ balance=float(state.balance),
863
+ position=float(state.position),
864
+ num_trades=int(state.num_trades),
865
+ fills=fills,
866
+ )
867
+
868
+
869
+ def _iter_market_events(orderbook_path, tick_path, symbol: str) -> Iterator[tuple[str, object]]:
870
+ orderbooks = iter(OrderBookChannel(symbol=symbol, dataname=str(orderbook_path), depth=1).load())
871
+ ticks = iter(TickChannel(symbol=symbol, dataname=str(tick_path)).load())
872
+ next_orderbook = next(orderbooks, None)
873
+ next_tick = next(ticks, None)
874
+ while next_orderbook is not None or next_tick is not None:
875
+ if next_tick is None:
876
+ yield "orderbook", next_orderbook
877
+ next_orderbook = next(orderbooks, None)
878
+ continue
879
+ if next_orderbook is None:
880
+ yield "tick", next_tick
881
+ next_tick = next(ticks, None)
882
+ continue
883
+ if next_orderbook.timestamp <= next_tick.timestamp:
884
+ yield "orderbook", next_orderbook
885
+ next_orderbook = next(orderbooks, None)
886
+ else:
887
+ yield "tick", next_tick
888
+ next_tick = next(ticks, None)
889
+
890
+
891
+ def _iter_exchange_market_events(market_data_path, symbol: str) -> Iterator[tuple[str, object]]:
892
+ with np.load(str(Path(market_data_path))) as payload:
893
+ data = payload["data"]
894
+ bid_price = None
895
+ ask_price = None
896
+ bid_qty = 0.0
897
+ ask_qty = 0.0
898
+ previous_bid_price = None
899
+ previous_ask_price = None
900
+ previous_bid_qty = 0.0
901
+ previous_ask_qty = 0.0
902
+ event_seq = 0
903
+ for row in data:
904
+ ev = int(row["ev"])
905
+ if not (ev & _EXCH_EVENT):
906
+ continue
907
+ timestamp = float(int(row["exch_ts"]) / 1_000_000_000.0)
908
+ if ev & _TRADE_EVENT:
909
+ event_seq += 1
910
+ yield (
911
+ "tick",
912
+ SimpleNamespace(
913
+ timestamp=timestamp,
914
+ timestamp_ns=int(row["exch_ts"]),
915
+ event_seq=event_seq,
916
+ symbol=symbol,
917
+ price=float(row["px"]),
918
+ volume=float(row["qty"]),
919
+ direction="buy" if (ev & _BUY_EVENT) else "sell",
920
+ bid_price=bid_price,
921
+ ask_price=ask_price,
922
+ bid_volume=bid_qty,
923
+ ask_volume=ask_qty,
924
+ ),
925
+ )
926
+ if ev & _DEPTH_EVENT:
927
+ previous_bid_price = bid_price
928
+ previous_ask_price = ask_price
929
+ previous_bid_qty = bid_qty
930
+ previous_ask_qty = ask_qty
931
+ if ev & _BUY_EVENT:
932
+ bid_price = float(row["px"])
933
+ bid_qty = float(row["qty"])
934
+ elif ev & _SELL_EVENT:
935
+ ask_price = float(row["px"])
936
+ ask_qty = float(row["qty"])
937
+ if bid_price is None or ask_price is None:
938
+ continue
939
+ event_seq += 1
940
+ yield (
941
+ "orderbook",
942
+ SimpleNamespace(
943
+ timestamp=timestamp,
944
+ timestamp_ns=int(row["exch_ts"]),
945
+ event_seq=event_seq,
946
+ symbol=symbol,
947
+ previous_bids=(
948
+ [(previous_bid_price, previous_bid_qty)]
949
+ if previous_bid_price is not None
950
+ else []
951
+ ),
952
+ previous_asks=(
953
+ [(previous_ask_price, previous_ask_qty)]
954
+ if previous_ask_price is not None
955
+ else []
956
+ ),
957
+ bids=[(bid_price, bid_qty)],
958
+ asks=[(ask_price, ask_qty)],
959
+ ),
960
+ )
961
+
962
+
963
+ def _advance_local_orderbook_snapshot(
964
+ latest_snapshot, next_orderbook, orderbooks, target_ts: float
965
+ ):
966
+ while next_orderbook is not None and float(next_orderbook.timestamp) <= float(target_ts):
967
+ latest_snapshot = next_orderbook
968
+ next_orderbook = next(orderbooks, None)
969
+ return latest_snapshot, next_orderbook
970
+
971
+
972
+ def _create_depth_probe(market_data_path, tick_size: float, lot_size: float):
973
+ try:
974
+ from hftbacktest import BacktestAsset, HashMapMarketDepthBacktest
975
+ except Exception:
976
+ logger.warning("binance_bbo_compare:974 fallback on Exception")
977
+ return None
978
+
979
+ asset = (
980
+ BacktestAsset()
981
+ .data([str(Path(market_data_path))])
982
+ .linear_asset(1.0)
983
+ .constant_order_latency(0, 0)
984
+ .power_prob_queue_model(2.0)
985
+ .no_partial_fill_exchange()
986
+ .trading_value_fee_model(0.0, 0.0)
987
+ .tick_size(float(tick_size))
988
+ .lot_size(float(lot_size))
989
+ )
990
+ return HashMapMarketDepthBacktest([asset])
991
+
992
+
993
+ def _augment_submission_snapshot(
994
+ base_snapshot, depth, quotes, tick_size: float, fallback_snapshot=None
995
+ ):
996
+ if base_snapshot is None:
997
+ return None
998
+ if not getattr(base_snapshot, "bids", None) or not getattr(base_snapshot, "asks", None):
999
+ return base_snapshot
1000
+ base_best_bid = float(base_snapshot.bids[0][0])
1001
+ base_best_ask = float(base_snapshot.asks[0][0])
1002
+ depth_usable = False
1003
+ if depth is not None:
1004
+ probe_best_bid = float(depth.best_bid)
1005
+ probe_best_ask = float(depth.best_ask)
1006
+ if _is_finite_book(probe_best_bid, probe_best_ask):
1007
+ depth_usable = (
1008
+ abs(base_best_bid - probe_best_bid) <= 1e-12
1009
+ and abs(base_best_ask - probe_best_ask) <= 1e-12
1010
+ )
1011
+
1012
+ bid_levels = [(float(price), float(qty)) for price, qty in base_snapshot.bids]
1013
+ ask_levels = [(float(price), float(qty)) for price, qty in base_snapshot.asks]
1014
+ seen_bid_ticks = {_price_tick(price, tick_size) for price, _ in bid_levels}
1015
+ seen_ask_ticks = {_price_tick(price, tick_size) for price, _ in ask_levels}
1016
+
1017
+ def _snapshot_level_qty(levels, target_tick: int) -> float:
1018
+ if not levels:
1019
+ return 0.0
1020
+ for level_price, level_qty in levels:
1021
+ if _price_tick(level_price, tick_size) == target_tick:
1022
+ return float(level_qty)
1023
+ return 0.0
1024
+
1025
+ for side, price_tick, price in _normalize_quotes(quotes, tick_size=tick_size):
1026
+ if side == "buy":
1027
+ if price_tick in seen_bid_ticks:
1028
+ continue
1029
+ qty = float(depth.bid_qty_at_tick(price_tick)) if depth_usable else 0.0
1030
+ if qty <= 0.0 and fallback_snapshot is not None:
1031
+ qty = _snapshot_level_qty(getattr(fallback_snapshot, "bids", None), price_tick)
1032
+ if qty > 0.0:
1033
+ bid_levels.append((price, qty))
1034
+ seen_bid_ticks.add(price_tick)
1035
+ continue
1036
+ if price_tick in seen_ask_ticks:
1037
+ continue
1038
+ qty = float(depth.ask_qty_at_tick(price_tick)) if depth_usable else 0.0
1039
+ if qty <= 0.0 and fallback_snapshot is not None:
1040
+ qty = _snapshot_level_qty(getattr(fallback_snapshot, "asks", None), price_tick)
1041
+ if qty > 0.0:
1042
+ ask_levels.append((price, qty))
1043
+ seen_ask_ticks.add(price_tick)
1044
+
1045
+ bid_levels.sort(key=lambda item: item[0], reverse=True)
1046
+ ask_levels.sort(key=lambda item: item[0])
1047
+ payload = dict(getattr(base_snapshot, "__dict__", {}))
1048
+ payload["bids"] = bid_levels
1049
+ payload["asks"] = ask_levels
1050
+ return SimpleNamespace(**payload)
1051
+
1052
+
1053
+ def _advance_depth_probe(
1054
+ depth_probe, current_timestamp_ns: Optional[int], target_timestamp_ns: int
1055
+ ):
1056
+ if depth_probe is None:
1057
+ return current_timestamp_ns, None
1058
+ if current_timestamp_ns is None:
1059
+ current_timestamp_ns = int(getattr(depth_probe, "current_timestamp", 0) or 0)
1060
+ target_timestamp_ns = int(target_timestamp_ns)
1061
+ if target_timestamp_ns > current_timestamp_ns:
1062
+ status = depth_probe.elapse(target_timestamp_ns - current_timestamp_ns)
1063
+ current_timestamp_ns = target_timestamp_ns
1064
+ if status != 0:
1065
+ return current_timestamp_ns, None
1066
+ return current_timestamp_ns, depth_probe.depth(0)
1067
+
1068
+
1069
+ def _augment_orderbook_snapshot_for_orders(base_snapshot, depth, pending_orders, tick_size: float):
1070
+ if base_snapshot is None:
1071
+ return None
1072
+ if depth is None:
1073
+ return base_snapshot
1074
+ if not getattr(base_snapshot, "bids", None) or not getattr(base_snapshot, "asks", None):
1075
+ return base_snapshot
1076
+ base_best_bid = float(base_snapshot.bids[0][0])
1077
+ base_best_ask = float(base_snapshot.asks[0][0])
1078
+ probe_best_bid = float(depth.best_bid)
1079
+ probe_best_ask = float(depth.best_ask)
1080
+ if not _is_finite_book(probe_best_bid, probe_best_ask):
1081
+ return base_snapshot
1082
+ if abs(base_best_bid - probe_best_bid) > 1e-12 or abs(base_best_ask - probe_best_ask) > 1e-12:
1083
+ return base_snapshot
1084
+
1085
+ bid_levels = [(float(price), float(qty)) for price, qty in base_snapshot.bids]
1086
+ ask_levels = [(float(price), float(qty)) for price, qty in base_snapshot.asks]
1087
+ seen_bid_ticks = {_price_tick(price, tick_size) for price, _ in bid_levels}
1088
+ seen_ask_ticks = {_price_tick(price, tick_size) for price, _ in ask_levels}
1089
+
1090
+ for order in pending_orders:
1091
+ if getattr(order, "_fill_role", None) != FillRole.MAKER:
1092
+ continue
1093
+ price = getattr(order, "price", None)
1094
+ if price is None:
1095
+ continue
1096
+ price = float(price)
1097
+ price_tick = _price_tick(price, tick_size)
1098
+ if order.isbuy():
1099
+ if price_tick in seen_bid_ticks:
1100
+ continue
1101
+ qty = float(depth.bid_qty_at_tick(price_tick))
1102
+ if qty > 0.0:
1103
+ bid_levels.append((price, qty))
1104
+ seen_bid_ticks.add(price_tick)
1105
+ continue
1106
+ if price_tick in seen_ask_ticks:
1107
+ continue
1108
+ qty = float(depth.ask_qty_at_tick(price_tick))
1109
+ if qty > 0.0:
1110
+ ask_levels.append((price, qty))
1111
+ seen_ask_ticks.add(price_tick)
1112
+
1113
+ bid_levels.sort(key=lambda item: item[0], reverse=True)
1114
+ ask_levels.sort(key=lambda item: item[0])
1115
+ payload = dict(getattr(base_snapshot, "__dict__", {}))
1116
+ payload["bids"] = bid_levels
1117
+ payload["asks"] = ask_levels
1118
+ return SimpleNamespace(**payload)
1119
+
1120
+
1121
+ def _submit_or_replace_quotes(
1122
+ broker,
1123
+ data,
1124
+ working_orders,
1125
+ quotes,
1126
+ order_qty: float,
1127
+ tick_size: float,
1128
+ snapshot,
1129
+ activation_timestamp_ns: int | None = None,
1130
+ activation_event_seq: int | None = None,
1131
+ ):
1132
+ working_orders = {
1133
+ key: order
1134
+ for key, order in working_orders.items()
1135
+ if order.alive() and order.status not in (Order.Canceled, Order.Rejected)
1136
+ }
1137
+ normalized = _normalize_quotes(quotes, tick_size=tick_size)
1138
+ target_keys = {(side, price_tick) for side, price_tick, _ in normalized}
1139
+
1140
+ for key in list(working_orders):
1141
+ if key in target_keys:
1142
+ continue
1143
+ broker.cancel(working_orders[key])
1144
+ working_orders.pop(key, None)
1145
+
1146
+ for side, price_tick, price in normalized:
1147
+ key = (side, price_tick)
1148
+ if key in working_orders:
1149
+ continue
1150
+ if side == "buy":
1151
+ order = broker.buy(
1152
+ owner=None, data=data, size=order_qty, price=price, exectype=Order.Limit
1153
+ )
1154
+ else:
1155
+ order = broker.sell(
1156
+ owner=None, data=data, size=order_qty, price=price, exectype=Order.Limit
1157
+ )
1158
+ order.time_in_force = "GTX"
1159
+ if activation_timestamp_ns is not None:
1160
+ order._active_after_timestamp_ns = int(activation_timestamp_ns)
1161
+ if activation_event_seq is not None:
1162
+ order._active_after_event_seq = int(activation_event_seq)
1163
+ if not _prime_backtrader_order(broker, order, snapshot):
1164
+ continue
1165
+ working_orders[key] = order
1166
+ return {
1167
+ key: order
1168
+ for key, order in working_orders.items()
1169
+ if order.alive() and order.status not in (Order.Canceled, Order.Rejected)
1170
+ }
1171
+
1172
+
1173
+ def _prime_backtrader_order(broker, order, snapshot) -> bool:
1174
+ if snapshot is None or broker._exchange_model is None:
1175
+ return True
1176
+ if order.exectype not in (Order.Market, Order.Limit):
1177
+ return True
1178
+ exchange_result = broker._exchange_model.on_new_order(order, snapshot)
1179
+ if exchange_result.action == "REJECT":
1180
+ broker._remove_pending_order(order)
1181
+ order.addinfo(reject_reason=exchange_result.reject_reason)
1182
+ order.reject(broker)
1183
+ broker.notify(order)
1184
+ return False
1185
+ if exchange_result.action == "FILL":
1186
+ fill_price, fill_size = broker._aggregate_exchange_fills(exchange_result.fills)
1187
+ if fill_size > 0:
1188
+ broker._execute(order, fill_price, fill_size, snapshot, source="orderbook_depth")
1189
+ broker._remove_pending_order(order)
1190
+ return False
1191
+ return True
1192
+
1193
+
1194
+ def _process_backtrader_depth_crosses(broker, data, ob_event) -> None:
1195
+ best_bid = ob_event.bids[0][0] if ob_event.bids else None
1196
+ best_ask = ob_event.asks[0][0] if ob_event.asks else None
1197
+ matched = []
1198
+ for order in list(broker._orders_by_symbol.get(data.name, [])):
1199
+ if getattr(order, "_fill_role", None) != FillRole.MAKER:
1200
+ continue
1201
+ if order.status in (
1202
+ Order.Canceled,
1203
+ Order.Rejected,
1204
+ Order.Completed,
1205
+ Order.Expired,
1206
+ Order.Margin,
1207
+ ):
1208
+ continue
1209
+ if order.isbuy():
1210
+ if best_ask is None or float(order.price) <= float(best_ask):
1211
+ continue
1212
+ else:
1213
+ if best_bid is None or float(order.price) >= float(best_bid):
1214
+ continue
1215
+ fill_size = broker._get_remaining_size(order)
1216
+ if fill_size <= 0:
1217
+ continue
1218
+ broker._execute(order, float(order.price), float(fill_size), ob_event, source="maker")
1219
+ matched.append(order)
1220
+
1221
+ for order in matched:
1222
+ broker._remove_pending_order(order)
1223
+
1224
+
1225
+ def _replace_hft_orders(
1226
+ hbt,
1227
+ quotes,
1228
+ tick_size: float,
1229
+ order_qty: float,
1230
+ buy_flag,
1231
+ sell_flag,
1232
+ gtx_flag,
1233
+ limit_flag,
1234
+ partial_filled_flag,
1235
+ ):
1236
+ target_keys = {
1237
+ (side, price_tick) for side, price_tick, _ in _normalize_quotes(quotes, tick_size=tick_size)
1238
+ }
1239
+ active_orders = {}
1240
+ values = hbt.orders(0).values()
1241
+ while True:
1242
+ order = values.next()
1243
+ if order is None:
1244
+ break
1245
+ side = "buy" if order.side == buy_flag else "sell"
1246
+ price_tick = int(round(float(order.price) / tick_size))
1247
+ active_orders[(side, price_tick)] = order
1248
+ if (side, price_tick) not in target_keys and order.cancellable:
1249
+ hbt.cancel(0, int(order.order_id), True)
1250
+
1251
+ for side, price_tick, price in _normalize_quotes(quotes, tick_size=tick_size):
1252
+ if (side, price_tick) in active_orders:
1253
+ continue
1254
+ order_id = _order_id(side, price_tick)
1255
+ if side == "buy":
1256
+ hbt.submit_buy_order(0, order_id, price, order_qty, gtx_flag, limit_flag, True)
1257
+ else:
1258
+ hbt.submit_sell_order(0, order_id, price, order_qty, gtx_flag, limit_flag, True)
1259
+
1260
+
1261
+ def _hft_order_key(order):
1262
+ return (
1263
+ int(order.order_id),
1264
+ int(getattr(order, "local_timestamp", 0)),
1265
+ int(getattr(order, "exch_timestamp", 0)),
1266
+ int(getattr(order, "side", 0)),
1267
+ )
1268
+
1269
+
1270
+ def _collect_hft_fills(order_dict, seen_exec_qty, fills):
1271
+ values = order_dict.values()
1272
+ while True:
1273
+ order = values.next()
1274
+ if order is None:
1275
+ break
1276
+ key = _hft_order_key(order)
1277
+ exec_qty = float(order.exec_qty)
1278
+ previous = seen_exec_qty.get(key, 0.0)
1279
+ if exec_qty <= previous + 1e-12:
1280
+ continue
1281
+ fills.append(
1282
+ ComparisonFill(
1283
+ side="buy" if int(order.side) > 0 else "sell",
1284
+ price=float(order.exec_price),
1285
+ size=exec_qty - previous,
1286
+ timestamp_ns=int(getattr(order, "exch_timestamp", 0)) or None,
1287
+ local_timestamp_ns=int(getattr(order, "local_timestamp", 0)) or None,
1288
+ exch_timestamp_ns=int(getattr(order, "exch_timestamp", 0)) or None,
1289
+ order_ref=int(order.order_id),
1290
+ )
1291
+ )
1292
+ seen_exec_qty[key] = exec_qty
1293
+
1294
+
1295
+ def _normalize_quotes(quotes, tick_size: float = 0.01):
1296
+ normalized = []
1297
+ seen = set()
1298
+ for side, value in quotes.items():
1299
+ prices = value if isinstance(value, (list, tuple)) else [value]
1300
+ for price in prices:
1301
+ price_tick = _price_tick(float(price), tick_size)
1302
+ key = (side, price_tick)
1303
+ if key in seen:
1304
+ continue
1305
+ seen.add(key)
1306
+ normalized_price = round(price_tick * tick_size, 12)
1307
+ normalized.append((side, price_tick, normalized_price))
1308
+ return normalized
1309
+
1310
+
1311
+ def _price_tick(price: float, tick_size: float) -> int:
1312
+ return int(round(price / tick_size))
1313
+
1314
+
1315
+ def _order_id(side: str, price_tick: int) -> int:
1316
+ return price_tick if side == "buy" else 1_000_000_000 + price_tick
1317
+
1318
+
1319
+ def _is_finite_book(best_bid: float, best_ask: float) -> bool:
1320
+ return float(best_bid) == float(best_bid) and float(best_ask) == float(best_ask)
1321
+
1322
+
1323
+ def _event_timestamp_ns(event) -> int:
1324
+ timestamp_ns = getattr(event, "timestamp_ns", None)
1325
+ if timestamp_ns is not None:
1326
+ return int(timestamp_ns)
1327
+ timestamp = getattr(event, "local_time", None) or getattr(event, "timestamp", 0.0)
1328
+ return int(round(float(timestamp) * 1_000_000_000.0))
1329
+
1330
+
1331
+ def _market_data_exchange_anchor(market_data_path) -> Optional[float]:
1332
+ anchor_ns = _market_data_exchange_anchor_ns(market_data_path)
1333
+ if anchor_ns is None:
1334
+ return None
1335
+ return float(anchor_ns / 1_000_000_000.0)
1336
+
1337
+
1338
+ def _market_data_exchange_anchor_ns(market_data_path) -> Optional[int]:
1339
+ try:
1340
+ with np.load(str(Path(market_data_path))) as payload:
1341
+ data = payload["data"]
1342
+ if len(data) == 0:
1343
+ return None
1344
+ return int(np.min(data["exch_ts"]))
1345
+ except Exception:
1346
+ logger.warning("binance_bbo_compare:1343 fallback on Exception")
1347
+ return None
1348
+
1349
+
1350
+ def _market_data_exchange_book(market_data_path):
1351
+ try:
1352
+ with np.load(str(Path(market_data_path))) as payload:
1353
+ data = payload["data"]
1354
+ if len(data) == 0:
1355
+ return None
1356
+ mask = (data["ev"] & np.uint64(_EXCH_EVENT) != 0) & (
1357
+ data["ev"] & np.uint64(_DEPTH_EVENT) != 0
1358
+ )
1359
+ rows = data[mask]
1360
+ if len(rows) == 0:
1361
+ return None
1362
+ order = np.argsort(rows["exch_ts"], kind="mergesort")
1363
+ rows = rows[order]
1364
+ exch_ts = []
1365
+ best_bids = []
1366
+ best_asks = []
1367
+ best_bid = None
1368
+ best_ask = None
1369
+ for row in rows:
1370
+ ev = int(row["ev"])
1371
+ if ev & _BUY_EVENT:
1372
+ best_bid = float(row["px"])
1373
+ elif ev & _SELL_EVENT:
1374
+ best_ask = float(row["px"])
1375
+ if best_bid is None or best_ask is None:
1376
+ continue
1377
+ exch_ts.append(int(row["exch_ts"]))
1378
+ best_bids.append(best_bid)
1379
+ best_asks.append(best_ask)
1380
+ if not exch_ts:
1381
+ return None
1382
+ return (exch_ts, best_bids, best_asks)
1383
+ except Exception:
1384
+ logger.warning("binance_bbo_compare:1380 fallback on Exception")
1385
+ return None
1386
+
1387
+
1388
+ def _lookup_exchange_snapshot(exchange_book, timestamp_ns: int):
1389
+ if exchange_book is None:
1390
+ return None
1391
+ exch_ts, best_bids, best_asks = exchange_book
1392
+ index = bisect.bisect_right(exch_ts, int(timestamp_ns)) - 1
1393
+ if index < 0:
1394
+ return None
1395
+ return SimpleNamespace(
1396
+ bids=[(float(best_bids[index]), 0.0)],
1397
+ asks=[(float(best_asks[index]), 0.0)],
1398
+ )