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,1538 @@
1
+ #!/usr/bin/env python
2
+ """Unified bt_api_py-backed live data feed."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import collections
7
+ import copy
8
+ import datetime as _dt
9
+ import math
10
+ import time as _time
11
+ from types import SimpleNamespace
12
+
13
+ from ..channel import Event, EventPriority
14
+ from ..dataseries import TimeFrame
15
+ from ..events import BarEvent
16
+ from ..feed import DataBase
17
+ from ..stores.btapistore import _normalize_bar, _redact_diagnostic
18
+ from ..utils import date2num
19
+ from ..utils.log_message import get_logger
20
+ from .barrier import BarEvidence
21
+ from .ctpcohort import CtpCohortNow
22
+ from .livefeed import LiveFeedBase
23
+
24
+ logger = get_logger(__name__)
25
+ _LOGGING_HEALTH: "collections.Counter[str]" = collections.Counter()
26
+
27
+
28
+ def _safe_log(level, message, *args):
29
+ """Keep a failing log sink outside feed control flow."""
30
+ try:
31
+ getattr(logger, level)(_redact_diagnostic(message), *map(_redact_diagnostic, args))
32
+ except Exception:
33
+ # The sink itself failed. Calling it again would escape this guard
34
+ # before its health counter is incremented and disrupt feed callbacks.
35
+ _LOGGING_HEALTH["logging_errors"] += 1
36
+
37
+
38
+ _UTC = _dt.timezone.utc
39
+ _CTP_INVALID_ABS = 1.0e50
40
+
41
+
42
+ def _set_tick_value(tick, name, value):
43
+ """Set one normalized field on mapping and object event shapes."""
44
+ if isinstance(tick, dict):
45
+ tick[name] = value
46
+ else:
47
+ setattr(tick, name, value)
48
+
49
+
50
+ def _finite_market_number(value):
51
+ """Return a finite market number, rejecting CTP's DBL_MAX-style sentinels."""
52
+ if value in (None, "") or isinstance(value, bool):
53
+ return None
54
+ try:
55
+ number = float(value)
56
+ except (TypeError, ValueError, OverflowError):
57
+ return None
58
+ if not math.isfinite(number) or abs(number) >= _CTP_INVALID_ABS:
59
+ return None
60
+ return number
61
+
62
+
63
+ def _as_utc_datetime(value):
64
+ """Parse an event-time field without silently replacing invalid source time."""
65
+ if isinstance(value, _dt.datetime):
66
+ if value.tzinfo is None or value.utcoffset() is None:
67
+ return value.replace(tzinfo=_UTC)
68
+ return value.astimezone(_UTC)
69
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
70
+ try:
71
+ return _dt.datetime.fromtimestamp(_coerce_epoch_seconds(value), _UTC)
72
+ except (OSError, OverflowError, TypeError, ValueError):
73
+ return None
74
+ if isinstance(value, str) and value.strip():
75
+ try:
76
+ parsed = _dt.datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
77
+ except ValueError:
78
+ return None
79
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
80
+ parsed = parsed.replace(tzinfo=_UTC)
81
+ return parsed.astimezone(_UTC)
82
+ return None
83
+
84
+
85
+ def _coerce_epoch_seconds(value):
86
+ ts = float(value)
87
+ if ts > 10_000_000_000:
88
+ ts /= 1000.0
89
+ return ts
90
+
91
+
92
+ def _datetime_to_utc_naive(value):
93
+ if value.tzinfo is not None and value.utcoffset() is not None:
94
+ return value.astimezone(_UTC).replace(tzinfo=None)
95
+ return value.replace(tzinfo=None)
96
+
97
+
98
+ def _datetime_to_timestamp(value):
99
+ return _datetime_to_utc_naive(value).replace(tzinfo=_UTC).timestamp()
100
+
101
+
102
+ def _tick_value(tick, *names, default=None):
103
+ if isinstance(tick, dict):
104
+ for name in names:
105
+ if name in tick and tick[name] is not None:
106
+ return tick[name]
107
+ return default
108
+
109
+ for name in names:
110
+ value = getattr(tick, name, None)
111
+ if value is not None:
112
+ return value
113
+ return default
114
+
115
+
116
+ def _tick_timestamp(tick):
117
+ event_time = _as_utc_datetime(_tick_value(tick, "event_time_utc", default=None))
118
+ if event_time is not None:
119
+ return event_time.timestamp()
120
+
121
+ value = _tick_value(tick, "timestamp", "Timestamp", default=None)
122
+ if value is not None:
123
+ return _coerce_epoch_seconds(value)
124
+
125
+ dt_value = _tick_value(tick, "datetime", "dt", default=None)
126
+ if isinstance(dt_value, _dt.datetime):
127
+ return _datetime_to_timestamp(dt_value)
128
+ if isinstance(dt_value, str) and dt_value:
129
+ try:
130
+ return _datetime_to_timestamp(
131
+ _dt.datetime.fromisoformat(dt_value.replace("Z", "+00:00"))
132
+ )
133
+ except ValueError:
134
+ _safe_log("debug", "btapifeed:132 ignored ValueError")
135
+
136
+ return _coerce_epoch_seconds(_tick_value(tick, "local_time", "LocalTime", default=0.0) or 0.0)
137
+
138
+
139
+ def _tick_datetime(tick):
140
+ event_time = _as_utc_datetime(_tick_value(tick, "event_time_utc", default=None))
141
+ if event_time is not None:
142
+ return event_time.replace(tzinfo=None)
143
+
144
+ timestamp_value = _tick_value(tick, "timestamp", "Timestamp", default=None)
145
+ if timestamp_value not in (None, ""):
146
+ try:
147
+ ts = _coerce_epoch_seconds(timestamp_value)
148
+ except (TypeError, ValueError):
149
+ _safe_log("debug", "btapifeed:147 ignored TypeError,ValueError")
150
+ else:
151
+ if ts > 0:
152
+ return _dt.datetime.fromtimestamp(ts, _UTC).replace(tzinfo=None)
153
+
154
+ value = _tick_value(tick, "datetime", "dt", default=None)
155
+ if isinstance(value, _dt.datetime):
156
+ return _datetime_to_utc_naive(value)
157
+ if isinstance(value, str) and value:
158
+ try:
159
+ return _datetime_to_utc_naive(_dt.datetime.fromisoformat(value.replace("Z", "+00:00")))
160
+ except ValueError:
161
+ _safe_log("debug", "btapifeed:159 ignored ValueError")
162
+ return _dt.datetime.fromtimestamp(_tick_timestamp(tick), _UTC).replace(tzinfo=None)
163
+
164
+
165
+ def _causal_event_kwargs(event):
166
+ """Copy standard timing and identity fields into derived events."""
167
+ return {
168
+ key: _tick_value(event, key, default=None)
169
+ for key in (
170
+ "exchange_time",
171
+ "received_wall_time",
172
+ "received_monotonic_ns",
173
+ "clock_domain_id",
174
+ "sequence",
175
+ "previous_sequence",
176
+ "snapshot_or_delta",
177
+ "continuity_status",
178
+ "stale",
179
+ "stale_reason",
180
+ "source",
181
+ "event_id",
182
+ "coalesced_count",
183
+ )
184
+ if _tick_value(event, key, default=None) is not None
185
+ }
186
+
187
+
188
+ class BtApiFeed(DataBase, LiveFeedBase):
189
+ """Data feed that backfills and streams bars through BtApiStore.
190
+
191
+ ``orderbook_as_ticks=True`` exposes each depth snapshot as a zero-volume
192
+ midpoint tick bar before calling ``notify_orderbook``. This gives native
193
+ broker orders a valid feed price and clock even without trade/bar streams.
194
+ It requires ``timeframe=TimeFrame.Ticks``.
195
+ """
196
+
197
+ params = (
198
+ ("store", None),
199
+ ("provider", "btapi"),
200
+ ("historical_bars", None),
201
+ ("live_bars", None),
202
+ ("backfill_start", True),
203
+ ("dispatch_ticks", True),
204
+ ("dispatch_orderbooks", True),
205
+ ("dispatch_bars", True),
206
+ ("orderbook_as_ticks", False),
207
+ ("bar_watermark_ms", 500),
208
+ ("event_time_max_age", 2.0),
209
+ ("receive_time_max_age", 2.0),
210
+ ("price_tick", None),
211
+ ("clock", None),
212
+ # A caller-owned, calibrated provider invoked at the synchronous
213
+ # strategy-dispatch boundary for strict ctp.quote.v2 ticks. It must
214
+ # return CtpCohortNow in the event's exact monotonic clock domain.
215
+ # There is deliberately no process-clock fallback here.
216
+ ("ctp_decision_now_provider", None),
217
+ # A caller-owned adapter from a Feed-owned, immutable closed BarEvent
218
+ # to the public BarEvidence hand-off. The Feed only attaches a
219
+ # successfully validated object; it never invents a clock mapping or
220
+ # candidate scope from process-local state.
221
+ ("closed_bar_evidence_provider", None),
222
+ )
223
+
224
+ def __init__(self, *args, **kwargs):
225
+ """Initialize the feed, normalize inputs, and prepare internal state.
226
+
227
+ The constructor performs three pieces of work:
228
+
229
+ 1. Resolves the :class:`BtApiStore` instance and the data provider
230
+ tag from the parsed parameters and stashes them on the instance
231
+ for quick access during :meth:`start` / :meth:`_load`.
232
+ 2. Normalizes the optional pre-supplied ``historical_bars`` and
233
+ ``live_bars`` parameters into :class:`collections.deque`
234
+ instances so that :meth:`_load` can ``popleft`` from them in O(1).
235
+ 3. Initializes the runtime flags that govern backfill behavior
236
+ (``_history_backfilled``) and bar aggregation
237
+ (``_bar_builder``).
238
+
239
+ Args:
240
+ *args: Positional arguments forwarded to the
241
+ :class:`backtrader.feed.DataBase` constructor. Typically
242
+ this is just the ``dataname`` (symbol/contract identifier).
243
+ **kwargs: Parameter overrides. Any key matching a name in
244
+ :attr:`params` overrides the corresponding default; unknown
245
+ keys are forwarded to the base class unchanged.
246
+ """
247
+ super().__init__(*args, **kwargs)
248
+ if self.p.closed_bar_evidence_provider is not None and not callable(
249
+ self.p.closed_bar_evidence_provider
250
+ ):
251
+ raise ValueError("closed_bar_evidence_provider must be callable")
252
+ self.store = self.p.store
253
+ self.provider = self.p.provider
254
+ self._history = collections.deque(
255
+ _normalize_bar(bar) for bar in (self.p.historical_bars or [])
256
+ )
257
+ self._live = collections.deque(_normalize_bar(bar) for bar in (self.p.live_bars or []))
258
+ self._live_notified = False
259
+ self._bar_builder = None
260
+ self._bar_builders = collections.OrderedDict()
261
+ self._bar_quality_overrides = collections.defaultdict(set)
262
+ self._max_event_timestamp = None
263
+ self._last_ingest_monotonic_ns = None
264
+ self._last_closed_bucket_end = None
265
+ self._last_connection_generation = None
266
+ self._last_ctp_scope = None
267
+ self._highest_ctp_scope = None
268
+ self._bar_sequence = 0
269
+ # Per-feed opaque marker proves that a strategy callback received the
270
+ # sealed event from this exact Feed instance, rather than a caller
271
+ # constructing a look-alike object around a BarEvidence value.
272
+ self._closed_bar_evidence_dispatch_token = object()
273
+ # This short-lived identity binding is populated immediately before
274
+ # synchronous strategy dispatch and cleared immediately afterward.
275
+ # It prevents a callback hook from retaining the event marker while
276
+ # replacing the immutable evidence object with a different one.
277
+ self._sealed_closed_bar_evidence_by_event_id = {}
278
+ self._tick_consumer_claimed = False
279
+ self._history_backfilled = bool(self._history)
280
+ self._continuity_degraded = False
281
+ self._session_active = False
282
+
283
+ def start(self):
284
+ """Start the feed, register it, and backfill if configured."""
285
+ new_session = not self._session_active
286
+ if new_session:
287
+ self._live_notified = False
288
+ self._continuity_degraded = False
289
+ claimed_this_start = False
290
+ try:
291
+ super().start()
292
+ if self.p.orderbook_as_ticks and self._timeframe != TimeFrame.Ticks:
293
+ raise ValueError("orderbook_as_ticks requires timeframe=TimeFrame.Ticks")
294
+
295
+ if self.store is None:
296
+ self.store = getattr(self, "_store", None)
297
+
298
+ if self.store is None:
299
+ self._session_active = True
300
+ return
301
+
302
+ self.store.start(data=self)
303
+ self.store.register(self)
304
+
305
+ if self.p.backfill_start and not self._history and not self._history_backfilled:
306
+ try:
307
+ bars = self.store.fetch_history(
308
+ self._dataname,
309
+ timeframe=self._timeframe,
310
+ compression=self._compression,
311
+ )
312
+ self._history.extend(bars)
313
+ self._history_backfilled = True
314
+ except Exception as e:
315
+ _safe_log("warning", "btapifeed:312 fallback on Exception")
316
+ _safe_log("debug", "Failed to backfill history: %s", e)
317
+
318
+ claim = getattr(self.store, "claim_tick_consumer", None)
319
+ if (
320
+ callable(claim)
321
+ and not self.p.orderbook_as_ticks
322
+ and not self._tick_consumer_claimed
323
+ ):
324
+ claim(self._dataname, self)
325
+ self._tick_consumer_claimed = True
326
+ claimed_this_start = True
327
+ self.store.subscribe(self._dataname)
328
+ self._session_active = True
329
+ except Exception:
330
+ _safe_log("error", "btapifeed:326 exception before re-raise (Exception)")
331
+ if claimed_this_start and self.store is not None:
332
+ release = getattr(self.store, "release_tick_consumer", None)
333
+ if callable(release):
334
+ release(self._dataname, self)
335
+ self._tick_consumer_claimed = False
336
+ if new_session:
337
+ self._session_active = False
338
+ raise
339
+
340
+ def stop(self):
341
+ """Stop the feed."""
342
+ try:
343
+ super().stop()
344
+ finally:
345
+ if self._tick_consumer_claimed and self.store is not None:
346
+ release = getattr(self.store, "release_tick_consumer", None)
347
+ if callable(release):
348
+ release(self._dataname, self)
349
+ # A live partial bucket is not a completed market bar. Clear it
350
+ # during teardown without dispatching a synthetic notify_bar after
351
+ # Cerebro has already stopped the strategy.
352
+ self._bar_builders.clear()
353
+ self._bar_builder = None
354
+ self._bar_quality_overrides.clear()
355
+ self._max_event_timestamp = None
356
+ self._last_ingest_monotonic_ns = None
357
+ self._last_closed_bucket_end = None
358
+ self._last_connection_generation = None
359
+ self._last_ctp_scope = None
360
+ self._highest_ctp_scope = None
361
+ self._tick_consumer_claimed = False
362
+ self._session_active = False
363
+
364
+ def islive(self) -> bool:
365
+ """Return whether this feed has a configured live data source."""
366
+ dataname = getattr(self, "_dataname", None)
367
+
368
+ if self._live:
369
+ return True
370
+
371
+ store = self.store or getattr(self, "_store", None)
372
+ if store is None:
373
+ return bool(self.p.live_bars)
374
+
375
+ # Cerebro queries islive before Store.start. A public BtApi event
376
+ # source is live without the legacy supports_live_* duck protocol.
377
+ if getattr(store, "_sdk_mode", False):
378
+ return True
379
+
380
+ live_cache = getattr(store, "_live_bars", {})
381
+ if dataname is not None and live_cache.get(dataname):
382
+ return True
383
+
384
+ api = getattr(store, "_api", None)
385
+ if api is not None and dataname is not None:
386
+ api_live = self._api_indicates_live(api, dataname)
387
+ if api_live is not None:
388
+ return api_live
389
+
390
+ if getattr(store, "_api_cls", None) is not None:
391
+ return True
392
+
393
+ if api is None:
394
+ return True
395
+
396
+ return False
397
+
398
+ @staticmethod
399
+ def _api_indicates_live(api, dataname):
400
+ """Whether the store API reports a live source for ``dataname``.
401
+
402
+ Returns True/False when the API gives a definitive answer, or None when
403
+ it has no opinion (caller falls through to other heuristics). Extracted
404
+ from islive() to flatten the repeated supports_live_* probes.
405
+ """
406
+ for capability in (
407
+ "supports_live_streaming",
408
+ "supports_live_ticks",
409
+ "supports_live_orderbook",
410
+ ):
411
+ if hasattr(api, capability):
412
+ try:
413
+ if bool(getattr(api, capability)(dataname)):
414
+ return True
415
+ except Exception as e:
416
+ _safe_log("warning", "btapifeed:411 fallback on Exception")
417
+ _safe_log("debug", "%s check failed: %s", capability, e)
418
+
419
+ live_ticks = getattr(api, "live_ticks", None)
420
+ if live_ticks is not None:
421
+ return dataname in live_ticks
422
+
423
+ live_orderbooks = getattr(api, "live_orderbooks", None)
424
+ if live_orderbooks is not None:
425
+ return dataname in live_orderbooks
426
+
427
+ live_bars = getattr(api, "live", None)
428
+ if live_bars is not None:
429
+ return dataname in live_bars
430
+
431
+ return None
432
+
433
+ def haslivedata(self) -> bool:
434
+ """Return whether a completed live bar is immediately available.
435
+
436
+ Pending raw ticks/orderbooks are realtime traffic, but they do not
437
+ advance the strategy clock until they aggregate into a completed bar.
438
+ Treating them as live data here makes Cerebro skip qcheck and spin while
439
+ repeatedly draining ticks that produce no bar.
440
+ """
441
+ if self._live:
442
+ return True
443
+
444
+ store = self.store or getattr(self, "_store", None)
445
+ if store is None:
446
+ return False
447
+
448
+ live_cache = getattr(store, "_live_bars", {})
449
+ return bool(live_cache.get(self._dataname))
450
+
451
+ def _load_history(self) -> bool:
452
+ """Load one historical bar if available."""
453
+ if not self._history:
454
+ return False
455
+
456
+ return self._load_bar(self._history.popleft())
457
+
458
+ def _load(self) -> bool:
459
+ """Load the next historical or live bar."""
460
+ if self._history:
461
+ return self._load_history()
462
+
463
+ # Preserve the causal pair between a completed bar callback and the
464
+ # matching data-line advance. Do not consume newer ticks while an
465
+ # already completed bar is waiting for Strategy.next().
466
+ if self._live:
467
+ self._mark_live()
468
+ return self._load_bar(self._live.popleft())
469
+
470
+ if self.p.orderbook_as_ticks:
471
+ if self._load_orderbook_tick():
472
+ return True
473
+ if self._qcheck > 0:
474
+ _time.sleep(self._qcheck)
475
+ return None
476
+
477
+ drained_ticks = self._drain_live_ticks()
478
+ drained_orderbooks = self._drain_live_orderbooks()
479
+ self._flush_ready_bars(reason="load")
480
+ # If this turn already produced a line bar, deliver it before an EOF
481
+ # watermark is allowed to close the following bucket.
482
+ source_exhausted = False if self._live else self._handle_source_exhaustion()
483
+
484
+ if self._live:
485
+ bar = self._live.popleft()
486
+ elif self.store is not None:
487
+ bar = self.store.poll_live(self._dataname)
488
+ else:
489
+ bar = None
490
+
491
+ if bar is None:
492
+ if source_exhausted and not self._bar_builders:
493
+ return False
494
+ if drained_ticks or drained_orderbooks:
495
+ self._mark_live()
496
+ if self._qcheck > 0:
497
+ _time.sleep(self._qcheck)
498
+ return None
499
+
500
+ self._mark_live()
501
+
502
+ return self._load_bar(bar)
503
+
504
+ def _check(self, forcedata=None):
505
+ """Drain live ticks while waiting for the next completed bar."""
506
+ super()._check(forcedata=forcedata)
507
+ if self.p.orderbook_as_ticks:
508
+ return # _load must establish the feed clock before the callback.
509
+ if self._live:
510
+ return # _load must pair the queued callback with its line bar.
511
+ drained_ticks = self._drain_live_ticks()
512
+ drained_orderbooks = self._drain_live_orderbooks()
513
+ self._flush_ready_bars(reason="idle")
514
+ self._handle_source_exhaustion()
515
+ if not self._history and (drained_ticks or drained_orderbooks):
516
+ self._mark_live()
517
+
518
+ def _load_orderbook_tick(self):
519
+ """Load one snapshot per turn so neither another venue nor the broker starves."""
520
+ if self.store is None:
521
+ return False
522
+ orderbook = self.store.poll_orderbook(self._dataname)
523
+ if orderbook is None:
524
+ return False
525
+ if self._handle_event_health(orderbook):
526
+ if self.p.dispatch_orderbooks:
527
+ self._dispatch_event("orderbook", EventPriority.ORDERBOOK, orderbook)
528
+ else:
529
+ self._mark_event_dropped(orderbook, "orderbook_dispatch_disabled")
530
+ return False
531
+ bids = _tick_value(orderbook, "bids", default=[]) or []
532
+ asks = _tick_value(orderbook, "asks", default=[]) or []
533
+ if not bids or not asks:
534
+ self._mark_event_dropped(orderbook, "orderbook_missing_top_of_book")
535
+ return False
536
+ bid, ask = float(bids[0][0]), float(asks[0][0])
537
+ if not math.isfinite(bid) or not math.isfinite(ask) or bid <= 0 or ask < bid:
538
+ self._mark_event_dropped(orderbook, "orderbook_invalid_top_of_book")
539
+ return False
540
+ midpoint = (bid + ask) / 2.0
541
+ stamp = _tick_timestamp(orderbook)
542
+ bar = BarEvent(
543
+ timestamp=stamp,
544
+ symbol=self._dataname,
545
+ exchange=_tick_value(orderbook, "exchange", default=""),
546
+ asset_type=_tick_value(orderbook, "asset_type", default="futures"),
547
+ local_time=_tick_value(orderbook, "local_time", default=stamp),
548
+ **_causal_event_kwargs(orderbook),
549
+ open=midpoint,
550
+ high=midpoint,
551
+ low=midpoint,
552
+ close=midpoint,
553
+ volume=0.0,
554
+ )
555
+ self._load_bar(
556
+ {
557
+ "datetime": _tick_datetime(orderbook),
558
+ "open": midpoint,
559
+ "high": midpoint,
560
+ "low": midpoint,
561
+ "close": midpoint,
562
+ "volume": 0.0,
563
+ "openinterest": 0.0,
564
+ }
565
+ )
566
+ self._mark_live()
567
+ if self.p.dispatch_orderbooks:
568
+ self._dispatch_event("orderbook", EventPriority.ORDERBOOK, orderbook)
569
+ else:
570
+ self._mark_event_dropped(orderbook, "orderbook_dispatch_disabled")
571
+ if self.p.dispatch_bars:
572
+ self._dispatch_event("bar", EventPriority.BAR, bar)
573
+ return True
574
+
575
+ def _load_bar(self, bar) -> bool:
576
+ """Write a normalized bar into line buffers."""
577
+ bar = _normalize_bar(bar)
578
+ self.lines.datetime[0] = date2num(bar["datetime"])
579
+ self.lines.open[0] = bar["open"]
580
+ self.lines.high[0] = bar["high"]
581
+ self.lines.low[0] = bar["low"]
582
+ self.lines.close[0] = bar["close"]
583
+ self.lines.volume[0] = bar["volume"]
584
+ self.lines.openinterest[0] = bar["openinterest"]
585
+ return True
586
+
587
+ def _drain_live_ticks(self):
588
+ """Consume ticks only until the next completed bar boundary.
589
+
590
+ A single ``_load`` turn may inspect many ticks inside one bucket, but
591
+ it must stop as soon as any bar event closes. Otherwise callbacks for
592
+ several future bars can run before the first matching data-line/next
593
+ turn, which makes the strategy observe the final callback repeatedly.
594
+ """
595
+ if self.store is None or not hasattr(self.store, "poll_tick"):
596
+ return False
597
+
598
+ drained = False
599
+
600
+ while True:
601
+ bar_sequence_before = self._bar_sequence
602
+ tick = self.store.poll_tick(self._dataname)
603
+ if tick is None:
604
+ break
605
+ drained = True
606
+
607
+ self._prepare_tick(tick)
608
+
609
+ if self._handle_event_health(tick):
610
+ if self.p.dispatch_ticks:
611
+ self._dispatch_event(
612
+ channel_type="tick",
613
+ priority=EventPriority.TICK,
614
+ event_data=tick,
615
+ )
616
+ else:
617
+ self._mark_event_dropped(tick, "tick_dispatch_disabled")
618
+ continue
619
+
620
+ if self.p.dispatch_ticks:
621
+ self._dispatch_event(
622
+ channel_type="tick",
623
+ priority=EventPriority.TICK,
624
+ event_data=tick,
625
+ )
626
+ else:
627
+ self._mark_event_dropped(tick, "tick_dispatch_disabled")
628
+ self._ingest_tick(tick)
629
+ self._flush_ready_bars(reason="tick")
630
+ if self._bar_sequence != bar_sequence_before:
631
+ break
632
+ return drained
633
+
634
+ def _handle_source_exhaustion(self):
635
+ """Finalize an explicitly finite source and report natural EOF.
636
+
637
+ Live transports do not expose this contract and therefore continue to
638
+ return ``None`` while idle. Deterministic replay sources may declare
639
+ both exhaustion and their final event-time watermark. A missing or
640
+ insufficient watermark invalidates any residual bucket rather than
641
+ promoting a partial bar to executable data.
642
+ """
643
+
644
+ store = self.store
645
+ exhausted = getattr(store, "is_source_exhausted", None) if store is not None else None
646
+ if not callable(exhausted) or not exhausted(self._dataname):
647
+ return False
648
+
649
+ watermark_reader = getattr(store, "get_source_event_time_watermark", None)
650
+ watermark = watermark_reader(self._dataname) if callable(watermark_reader) else None
651
+ watermark_dt = _as_utc_datetime(watermark)
652
+ if watermark_dt is not None:
653
+ watermark_ts = watermark_dt.timestamp()
654
+ if self._max_event_timestamp is None or watermark_ts > self._max_event_timestamp:
655
+ self._max_event_timestamp = watermark_ts
656
+ self._last_ingest_monotonic_ns = self._now_monotonic_ns()
657
+ self._flush_ready_bars(reason="source_exhausted")
658
+
659
+ if self._bar_builders:
660
+ self._flush_ready_bars(reason="source_exhausted_incomplete", force_invalid=True)
661
+ return True
662
+
663
+ def _drain_live_orderbooks(self):
664
+ if self.store is None or not hasattr(self.store, "poll_orderbook"):
665
+ return False
666
+
667
+ drained = False
668
+
669
+ while True:
670
+ orderbook = self.store.poll_orderbook(self._dataname)
671
+ if orderbook is None:
672
+ break
673
+ drained = True
674
+
675
+ self._handle_event_health(orderbook)
676
+
677
+ if self.p.dispatch_orderbooks:
678
+ self._dispatch_event(
679
+ channel_type="orderbook",
680
+ priority=EventPriority.ORDERBOOK,
681
+ event_data=orderbook,
682
+ )
683
+ else:
684
+ self._mark_event_dropped(orderbook, "orderbook_dispatch_disabled")
685
+ return drained
686
+
687
+ def _ingest_tick(self, tick):
688
+ """Update the current bar builder from a live tick."""
689
+ tick_dt = _tick_datetime(tick)
690
+ tick_ts = _tick_timestamp(tick)
691
+
692
+ price = _finite_market_number(
693
+ _tick_value(tick, "price", "last_price", "LastPrice", default=None)
694
+ )
695
+ if price is None or price <= 0 or not bool(_tick_value(tick, "bar_eligible", default=True)):
696
+ return
697
+
698
+ volume = _finite_market_number(
699
+ _tick_value(tick, "delta_volume", "volume", "Volume", default=0.0)
700
+ )
701
+ if volume is None or volume <= 0:
702
+ return
703
+ openinterest = _finite_market_number(
704
+ _tick_value(tick, "openinterest", "open_interest", "OpenInterest", default=0.0)
705
+ )
706
+ openinterest = max(openinterest or 0.0, 0.0)
707
+
708
+ if self._timeframe == TimeFrame.Ticks:
709
+ self._enqueue_bar_event(
710
+ BarEvent(
711
+ timestamp=tick_ts,
712
+ symbol=self._dataname,
713
+ exchange=_tick_value(tick, "exchange", "exchange_id", "ExchangeID", default=""),
714
+ asset_type=_tick_value(tick, "asset_type", "assetType", default="futures"),
715
+ local_time=_tick_value(tick, "local_time", "LocalTime", default=None),
716
+ **_causal_event_kwargs(tick),
717
+ open=price,
718
+ high=price,
719
+ low=price,
720
+ close=price,
721
+ volume=volume,
722
+ openinterest=openinterest,
723
+ ),
724
+ tick_dt,
725
+ )
726
+ return
727
+
728
+ bucket_start = self._get_bucket_start(tick_dt)
729
+ current = self._bar_builders.get(bucket_start)
730
+ if current is None:
731
+ current = self._new_bar_builder(bucket_start, tick, price, volume, openinterest)
732
+ self._bar_builders[bucket_start] = current
733
+ self._bar_builders.move_to_end(bucket_start)
734
+ self._bar_builder = current
735
+ return
736
+
737
+ if bucket_start == current["bucket_start"]:
738
+ current["high"] = max(current["high"], price)
739
+ current["low"] = min(current["low"], price)
740
+ current["close"] = price
741
+ current["volume"] += volume
742
+ current["openinterest"] = openinterest
743
+ current["last_timestamp"] = tick_ts
744
+ current["last_ingest_seq"] = _tick_value(
745
+ tick, "ingest_seq", "sequence", default=current["last_ingest_seq"]
746
+ )
747
+ current["trade_count"] += 1
748
+ for field, aliases, mismatch_flag in (
749
+ ("rules_hash", ("rules_hash",), "RULES_HASH_CHANGED"),
750
+ ("session_segment", ("session_segment",), "SESSION_SEGMENT_CHANGED"),
751
+ ("trading_day", ("trading_day", "TradingDay"), "TRADING_DAY_CHANGED"),
752
+ ):
753
+ value = _tick_value(tick, *aliases, default=current[field])
754
+ if value != current[field]:
755
+ current["quality_flags"].add(mismatch_flag)
756
+ current["quality_flags"].update(_tick_value(tick, "quality_flags", default=()) or ())
757
+ return
758
+
759
+ def _new_bar_builder(self, bucket_start, tick, price, volume, openinterest):
760
+ """Create the mutable state for an in-progress aggregated bar."""
761
+ ingest_seq = _tick_value(tick, "ingest_seq", "sequence", default=0)
762
+ return {
763
+ "bucket_start": bucket_start,
764
+ "bucket_end": self._get_bucket_end(bucket_start),
765
+ "open": price,
766
+ "high": price,
767
+ "low": price,
768
+ "close": price,
769
+ "volume": volume,
770
+ "openinterest": openinterest,
771
+ "last_timestamp": _tick_timestamp(tick),
772
+ "causal": _causal_event_kwargs(tick),
773
+ "exchange": _tick_value(tick, "exchange", "exchange_id", "ExchangeID", default=""),
774
+ "asset_type": _tick_value(tick, "asset_type", "assetType", default="futures"),
775
+ "trading_day": _tick_value(tick, "trading_day", "TradingDay", default=""),
776
+ "action_day": _tick_value(tick, "action_day", "ActionDay", default=""),
777
+ "rules_hash": _tick_value(tick, "rules_hash", default=None),
778
+ "session_segment": _tick_value(tick, "session_segment", default=None),
779
+ "connection_generation": _tick_value(
780
+ tick, "connection_generation", "stream_generation", default=None
781
+ ),
782
+ "first_ingest_seq": ingest_seq,
783
+ "last_ingest_seq": ingest_seq,
784
+ "trade_count": 1,
785
+ "volume_complete": bool(_tick_value(tick, "volume_complete", default=True)),
786
+ "quality_flags": set(_tick_value(tick, "quality_flags", default=()) or ()),
787
+ }
788
+
789
+ def _attach_closed_bar_evidence(self, bar_event):
790
+ """Attach only a scope-consistent caller-produced BarEvidence object.
791
+
792
+ The adapter receives a detached snapshot after the Feed has frozen
793
+ its closed-bar metadata but before the channel callback. Validation
794
+ remains against the Feed-owned event, so a provider cannot mutate its
795
+ input and make a forged result appear scope-consistent. The hand-off
796
+ stays narrow: unlike a strategy it cannot reconstruct evidence from
797
+ mutable line buffers, and unlike the Feed it cannot invent a clock
798
+ mapping or candidate identity.
799
+ """
800
+
801
+ provider = self.p.closed_bar_evidence_provider
802
+ if provider is None:
803
+ return
804
+ provider_input = SimpleNamespace(**copy.deepcopy(vars(bar_event)))
805
+ evidence = provider(provider_input)
806
+ if not isinstance(evidence, BarEvidence):
807
+ raise ValueError("closed_bar_evidence_provider must return BarEvidence")
808
+ if evidence.symbol != bar_event.symbol or evidence.exchange != bar_event.exchange:
809
+ raise ValueError("closed BarEvidence identity does not match BarEvent")
810
+ for name in (
811
+ "bucket_start",
812
+ "bucket_end",
813
+ "available_at",
814
+ "trading_day",
815
+ "connection_generation",
816
+ "rules_hash",
817
+ "session_segment",
818
+ "first_ingest_seq",
819
+ "last_ingest_seq",
820
+ "quote_cutoff_seq",
821
+ "bar_id",
822
+ "bar_sequence",
823
+ "complete",
824
+ ):
825
+ event_name = "generation" if name == "connection_generation" else name
826
+ if getattr(evidence, event_name) != getattr(bar_event, name):
827
+ raise ValueError(f"closed BarEvidence {event_name} does not match BarEvent")
828
+ for name in ("quality", "volume_complete", "closure_reason", "trade_count", "watermark"):
829
+ if getattr(evidence, name) != getattr(bar_event, name):
830
+ raise ValueError(f"closed BarEvidence {name} does not match BarEvent")
831
+ if evidence.max_event_time != getattr(bar_event, "max_event_time"):
832
+ raise ValueError("closed BarEvidence max_event_time does not match BarEvent")
833
+ for name in ("open", "high", "low", "close", "volume", "openinterest"):
834
+ if getattr(evidence, name) != float(getattr(bar_event, name)):
835
+ raise ValueError(f"closed BarEvidence {name} does not match BarEvent")
836
+ if evidence.clock_domain != getattr(bar_event, "clock_domain_id", None):
837
+ raise ValueError("closed BarEvidence clock domain does not match BarEvent")
838
+ setattr(bar_event, "closed_bar_evidence", evidence)
839
+
840
+ def _has_sealed_closed_bar_evidence(self, bar_event, evidence):
841
+ """Return whether this exact event/evidence pair is still in dispatch."""
842
+
843
+ return self._sealed_closed_bar_evidence_by_event_id.get(id(bar_event)) is evidence
844
+
845
+ def _enqueue_bar_event(self, bar_event, bar_datetime, *, deliver_lines=True):
846
+ """Queue a completed bar for both notify_bar and line delivery."""
847
+ bar_event.datetime = bar_datetime
848
+ if self.p.dispatch_bars:
849
+ self._dispatch_event(
850
+ channel_type="bar",
851
+ priority=EventPriority.BAR,
852
+ event_data=bar_event,
853
+ )
854
+ if deliver_lines:
855
+ self._live.append(
856
+ {
857
+ "datetime": bar_datetime,
858
+ "open": bar_event.open,
859
+ "high": bar_event.high,
860
+ "low": bar_event.low,
861
+ "close": bar_event.close,
862
+ "volume": bar_event.volume,
863
+ "openinterest": bar_event.openinterest,
864
+ }
865
+ )
866
+
867
+ def _now_monotonic_ns(self):
868
+ clock = self.p.clock
869
+ method = getattr(clock, "monotonic_ns", None) if clock is not None else None
870
+ if callable(method):
871
+ return int(method())
872
+ method = getattr(clock, "monotonic_now", None) if clock is not None else None
873
+ if callable(method):
874
+ return int(float(method()) * 1_000_000_000)
875
+ method = getattr(clock, "monotonic", None) if clock is not None else None
876
+ if callable(method):
877
+ return int(float(method()) * 1_000_000_000)
878
+ return _time.monotonic_ns()
879
+
880
+ def _event_time_watermark(self):
881
+ if self._max_event_timestamp is None:
882
+ return None
883
+ elapsed = 0.0
884
+ if self._last_ingest_monotonic_ns is not None:
885
+ elapsed = (
886
+ max(
887
+ self._now_monotonic_ns() - self._last_ingest_monotonic_ns,
888
+ 0,
889
+ )
890
+ / 1_000_000_000.0
891
+ )
892
+ return self._max_event_timestamp + elapsed
893
+
894
+ def _cached_price_tick(self):
895
+ configured = _finite_market_number(self.p.price_tick)
896
+ if configured is not None and configured > 0:
897
+ return configured
898
+ store = self.store
899
+ metadata = getattr(store, "contract_metadata", {}) if store is not None else {}
900
+ candidates = [self._dataname]
901
+ text = str(self._dataname or "")
902
+ for separator in (".", ":", "/"):
903
+ candidates.extend(part for part in text.split(separator) if part)
904
+ for key in candidates:
905
+ row = metadata.get(key) if isinstance(metadata, dict) else None
906
+ if not isinstance(row, dict):
907
+ continue
908
+ value = _finite_market_number(
909
+ row.get("price_tick") or row.get("tick_size") or row.get("min_price_tick")
910
+ )
911
+ if value is not None and value > 0:
912
+ return value
913
+ return None
914
+
915
+ @staticmethod
916
+ def _on_price_grid(value, price_tick):
917
+ if value is None or price_tick is None:
918
+ return True
919
+ scaled = value / price_tick
920
+ return math.isfinite(scaled) and abs(scaled - round(scaled)) <= 1e-8
921
+
922
+ def _add_bar_quality_override(self, bucket_start, *flags):
923
+ """Retain blocking evidence only while its minute can still be built."""
924
+ if bucket_start is None:
925
+ return
926
+ bucket_end = self._get_bucket_end(bucket_start)
927
+ if self._last_closed_bucket_end is not None and bucket_end <= self._last_closed_bucket_end:
928
+ return
929
+ self._bar_quality_overrides[bucket_start].update(flag for flag in flags if flag)
930
+
931
+ def _prune_bar_quality_overrides(self, watermark=None):
932
+ """Discard override-only buckets after their watermark can no longer admit data."""
933
+ if not self._bar_quality_overrides:
934
+ return
935
+ watermark = self._event_time_watermark() if watermark is None else watermark
936
+ watermark_delay = max(float(self.p.bar_watermark_ms or 0.0), 0.0) / 1000.0
937
+ for bucket_start in list(self._bar_quality_overrides):
938
+ if bucket_start in self._bar_builders:
939
+ continue
940
+ bucket_end = self._get_bucket_end(bucket_start)
941
+ already_closed = (
942
+ self._last_closed_bucket_end is not None
943
+ and bucket_end <= self._last_closed_bucket_end
944
+ )
945
+ deadline = bucket_end.replace(tzinfo=_UTC).timestamp() + watermark_delay
946
+ if already_closed or (watermark is not None and deadline <= watermark):
947
+ self._bar_quality_overrides.pop(bucket_start, None)
948
+
949
+ def _prepare_tick(self, tick):
950
+ """Normalize one tick's schema, quality, ordering and volume semantics."""
951
+ schema = str(_tick_value(tick, "schema_version", default="") or "").strip()
952
+ if not schema:
953
+ schema = "backtrader.tick.v1"
954
+ _set_tick_value(tick, "schema_version", schema)
955
+ semantics = "delta"
956
+ _set_tick_value(tick, "volume_semantics", semantics)
957
+ legacy = True
958
+ else:
959
+ semantics = str(_tick_value(tick, "volume_semantics", default="") or "").strip().lower()
960
+ legacy = False
961
+
962
+ strict_ctp_v2 = schema == "ctp.quote.v2"
963
+ raw_quality_flags = _tick_value(tick, "quality_flags", default=None)
964
+ valid_quality_container = isinstance(raw_quality_flags, (list, tuple, set, frozenset))
965
+ try:
966
+ quality_items = tuple(raw_quality_flags or ()) if valid_quality_container else ()
967
+ except TypeError:
968
+ # A custom collection is allowed by the broad runtime protocol,
969
+ # but a broken iterator must never turn into an uncaught dispatch
970
+ # failure or a clean quote.
971
+ quality_items = ()
972
+ valid_quality_container = False
973
+ valid_quality_items = all(
974
+ isinstance(flag, str) and bool(flag) and flag.strip() == flag for flag in quality_items
975
+ )
976
+ if strict_ctp_v2 and (not valid_quality_container or not valid_quality_items):
977
+ # A V2 producer must make both the evidence container and every
978
+ # flag explicit. Do not coerce malformed input into apparently
979
+ # clean evidence or let an unhashable/non-string item crash the
980
+ # strategy dispatch path.
981
+ flags = {"QUOTE_QUALITY_FLAGS_INVALID"}
982
+ elif not valid_quality_items:
983
+ flags = {"QUOTE_QUALITY_FLAGS_INVALID"}
984
+ else:
985
+ flags = set(quality_items)
986
+ if legacy:
987
+ flags.add("LEGACY_SCHEMA")
988
+
989
+ if semantics in {"delta", "incremental"}:
990
+ delta = _finite_market_number(
991
+ _tick_value(tick, "delta_volume", "volume", "Volume", default=None)
992
+ )
993
+ semantics = "delta"
994
+ elif semantics in {"cumulative", "cum", "total"}:
995
+ # Conversion is owned by the SDK/Store. Feed never differences a
996
+ # declared cumulative value because doing so can double-difference.
997
+ delta = _finite_market_number(_tick_value(tick, "delta_volume", default=None))
998
+ semantics = "cumulative"
999
+ if delta is None:
1000
+ flags.add("DELTA_VOLUME_MISSING")
1001
+ else:
1002
+ delta = None
1003
+ flags.add("VOLUME_SEMANTICS_UNKNOWN")
1004
+ if delta is None or delta < 0:
1005
+ flags.add("DELTA_VOLUME_INVALID")
1006
+ delta = 0.0
1007
+ _set_tick_value(tick, "volume_semantics", semantics)
1008
+ _set_tick_value(tick, "delta_volume", delta)
1009
+
1010
+ cumulative = _finite_market_number(
1011
+ _tick_value(tick, "cum_volume", "cumulative_volume", default=None)
1012
+ )
1013
+ if cumulative is not None:
1014
+ _set_tick_value(tick, "cum_volume", cumulative)
1015
+ _set_tick_value(tick, "cumulative_volume", cumulative)
1016
+
1017
+ price = _finite_market_number(
1018
+ _tick_value(tick, "price", "last_price", "LastPrice", default=None)
1019
+ )
1020
+ bid = _finite_market_number(_tick_value(tick, "bid_price", "BidPrice1", default=None))
1021
+ ask = _finite_market_number(_tick_value(tick, "ask_price", "AskPrice1", default=None))
1022
+ bid_size = _finite_market_number(
1023
+ _tick_value(tick, "bid_volume", "bid_size", "BidVolume1", default=None)
1024
+ )
1025
+ ask_size = _finite_market_number(
1026
+ _tick_value(tick, "ask_volume", "ask_size", "AskVolume1", default=None)
1027
+ )
1028
+ ctp_schema = schema.startswith("ctp.")
1029
+ if price is None or price <= 0:
1030
+ flags.add("LAST_PRICE_INVALID")
1031
+ if ctp_schema:
1032
+ if bid is None or bid <= 0:
1033
+ flags.add("BID_PRICE_INVALID")
1034
+ if ask is None or ask <= 0:
1035
+ flags.add("ASK_PRICE_INVALID")
1036
+ if bid_size is None or bid_size < 0:
1037
+ flags.add("BID_SIZE_INVALID")
1038
+ elif bid_size == 0:
1039
+ flags.add("BID_DEPTH_ZERO")
1040
+ if ask_size is None or ask_size < 0:
1041
+ flags.add("ASK_SIZE_INVALID")
1042
+ elif ask_size == 0:
1043
+ flags.add("ASK_DEPTH_ZERO")
1044
+ if bid is not None and ask is not None and bid > ask:
1045
+ flags.add("CROSSED_BOOK")
1046
+
1047
+ price_tick = self._cached_price_tick()
1048
+ if ctp_schema and price_tick is None:
1049
+ flags.add("PRICE_TICK_UNKNOWN")
1050
+ elif price_tick is not None:
1051
+ for name, value in (("LAST", price), ("BID", bid), ("ASK", ask)):
1052
+ if value is not None and value > 0 and not self._on_price_grid(value, price_tick):
1053
+ flags.add(f"{name}_PRICE_OFF_GRID")
1054
+
1055
+ upstream_execution_eligible = _tick_value(
1056
+ tick,
1057
+ "execution_eligible",
1058
+ default=None,
1059
+ )
1060
+ if strict_ctp_v2:
1061
+ # ``BtApiFeed`` is a consumer-side quality boundary, not an
1062
+ # authority that may promote a hand-built or incomplete V2 quote.
1063
+ # The SDK/Store must explicitly attest the upstream decision; this
1064
+ # Feed only keeps it false when any local gate also fails.
1065
+ if upstream_execution_eligible is not True:
1066
+ flags.add("UPSTREAM_EXECUTION_INELIGIBLE")
1067
+ if _tick_value(tick, "source_clock_quality", default="") != "verified":
1068
+ flags.add("SOURCE_CLOCK_UNVERIFIED")
1069
+ if _tick_value(tick, "receive_clock_quality", default="") != "verified":
1070
+ flags.add("RECEIVE_CLOCK_UNVERIFIED")
1071
+ if _tick_value(tick, "freshness_verified", default=False) is not True:
1072
+ flags.add("FRESHNESS_UNVERIFIED")
1073
+ if _tick_value(tick, "stale", default=None) is not False:
1074
+ flags.add("STREAM_UNREADY")
1075
+ if _tick_value(tick, "stale_reason", default=None) != "":
1076
+ flags.add("STREAM_UNREADY")
1077
+ raw_event_time = _tick_value(tick, "event_time_utc", default=None)
1078
+ if strict_ctp_v2 and raw_event_time in (None, ""):
1079
+ flags.add("EVENT_TIME_MISSING")
1080
+ event_dt = _as_utc_datetime(
1081
+ raw_event_time
1082
+ if raw_event_time not in (None, "")
1083
+ else _tick_value(tick, "timestamp", "datetime", default=None)
1084
+ )
1085
+ if event_dt is None:
1086
+ flags.add("EVENT_TIME_INVALID")
1087
+ raw_recv_time = _tick_value(tick, "recv_time_utc", default=None)
1088
+ if strict_ctp_v2 and raw_recv_time in (None, ""):
1089
+ flags.add("RECV_TIME_MISSING")
1090
+ received_wall = _as_utc_datetime(
1091
+ raw_recv_time
1092
+ if raw_recv_time not in (None, "")
1093
+ else _tick_value(tick, "received_wall_time", "local_time", default=None)
1094
+ )
1095
+ if strict_ctp_v2 and received_wall is None:
1096
+ flags.add("RECV_TIME_INVALID")
1097
+ if received_wall is not None and event_dt is not None:
1098
+ event_age = (received_wall - event_dt).total_seconds()
1099
+ _set_tick_value(tick, "event_age_seconds", event_age)
1100
+ maximum = max(float(self.p.event_time_max_age or 0.0), 0.0)
1101
+ if ctp_schema and (event_age < -0.5 or (maximum and event_age > maximum)):
1102
+ flags.add("EVENT_TIME_STALE")
1103
+
1104
+ raw_recv_mono = _tick_value(tick, "recv_monotonic_ns", default=None)
1105
+ if strict_ctp_v2 and raw_recv_mono in (None, ""):
1106
+ flags.add("RECV_MONOTONIC_MISSING")
1107
+ recv_mono = (
1108
+ raw_recv_mono
1109
+ if raw_recv_mono not in (None, "")
1110
+ else _tick_value(tick, "received_monotonic_ns", default=None)
1111
+ )
1112
+ if isinstance(recv_mono, int) and recv_mono > 0:
1113
+ recv_age = max(self._now_monotonic_ns() - recv_mono, 0) / 1_000_000_000.0
1114
+ _set_tick_value(tick, "recv_age_seconds", recv_age)
1115
+ maximum = max(float(self.p.receive_time_max_age or 0.0), 0.0)
1116
+ if ctp_schema and maximum and recv_age > maximum:
1117
+ flags.add("RECEIVE_TIME_STALE")
1118
+ elif strict_ctp_v2:
1119
+ flags.add("RECV_MONOTONIC_INVALID")
1120
+
1121
+ tick_ts = event_dt.timestamp() if event_dt is not None else None
1122
+ raw_timestamp = _finite_market_number(
1123
+ _tick_value(tick, "timestamp", "Timestamp", default=None)
1124
+ )
1125
+ if strict_ctp_v2 and event_dt is not None and raw_timestamp is not None:
1126
+ raw_timestamp = _coerce_epoch_seconds(raw_timestamp)
1127
+ if abs(raw_timestamp - tick_ts) > 1.0e-6:
1128
+ flags.add("EVENT_TIME_CONFLICT")
1129
+ prior_watermark = self._event_time_watermark()
1130
+ bucket_start = (
1131
+ self._get_bucket_start(event_dt.replace(tzinfo=None)) if event_dt is not None else None
1132
+ )
1133
+ bucket_end = self._get_bucket_end(bucket_start) if bucket_start is not None else None
1134
+ bucket_end_ts = (
1135
+ bucket_end.replace(tzinfo=_UTC).timestamp() if bucket_end is not None else None
1136
+ )
1137
+ watermark_delay = max(float(self.p.bar_watermark_ms or 0.0), 0.0) / 1000.0
1138
+ if (
1139
+ self._timeframe != TimeFrame.Ticks
1140
+ and prior_watermark is not None
1141
+ and bucket_end_ts is not None
1142
+ and bucket_end_ts + watermark_delay <= prior_watermark
1143
+ ):
1144
+ flags.add("LATE_AFTER_WATERMARK")
1145
+ elif (
1146
+ self._max_event_timestamp is not None
1147
+ and tick_ts is not None
1148
+ and tick_ts < self._max_event_timestamp
1149
+ ):
1150
+ flags.add("OUT_OF_ORDER_EVENT_TIME")
1151
+ if delta > 0:
1152
+ flags.add("ORDERING_VOLUME_GAP")
1153
+ if bucket_start is not None:
1154
+ self._add_bar_quality_override(bucket_start, "ORDERING_VOLUME_GAP")
1155
+ current_start = self._get_bucket_start(
1156
+ _dt.datetime.fromtimestamp(self._max_event_timestamp, _UTC).replace(tzinfo=None)
1157
+ )
1158
+ self._add_bar_quality_override(current_start, "ORDERING_VOLUME_GAP")
1159
+
1160
+ generation = _tick_value(tick, "connection_generation", "stream_generation", default=None)
1161
+ subscription_epoch = _tick_value(tick, "subscription_epoch", default=None)
1162
+ retired_ctp_scope = False
1163
+ if strict_ctp_v2:
1164
+ scope_is_valid = (
1165
+ type(generation) is int
1166
+ and generation > 0
1167
+ and type(subscription_epoch) is int
1168
+ and subscription_epoch > 0
1169
+ )
1170
+ if not scope_is_valid:
1171
+ flags.add("CTP_SCOPE_INVALID")
1172
+ else:
1173
+ scope = (generation, subscription_epoch)
1174
+ if self._highest_ctp_scope is not None and scope < self._highest_ctp_scope:
1175
+ # A delayed callback from an old connection/subscribe
1176
+ # scope must not reopen a retired stream after a newer
1177
+ # scope has been observed. In particular, `(8, 1)` is
1178
+ # newer than `(7, 99)` because generation dominates.
1179
+ flags.add("RETIRED_CONNECTION_SCOPE")
1180
+ retired_ctp_scope = True
1181
+ elif self._highest_ctp_scope is not None and scope != self._highest_ctp_scope:
1182
+ for builder in self._bar_builders.values():
1183
+ builder["quality_flags"].add(
1184
+ (
1185
+ "CONNECTION_GENERATION_CHANGED"
1186
+ if generation != self._highest_ctp_scope[0]
1187
+ else "SUBSCRIPTION_EPOCH_CHANGED"
1188
+ )
1189
+ )
1190
+ self._flush_ready_bars(
1191
+ reason=(
1192
+ "generation"
1193
+ if generation != self._highest_ctp_scope[0]
1194
+ else "subscription_epoch"
1195
+ ),
1196
+ force_invalid=True,
1197
+ )
1198
+ self._max_event_timestamp = None
1199
+ flags.add(
1200
+ (
1201
+ "CONNECTION_GENERATION_CHANGED"
1202
+ if generation != self._highest_ctp_scope[0]
1203
+ else "SUBSCRIPTION_EPOCH_CHANGED"
1204
+ )
1205
+ )
1206
+ self._add_bar_quality_override(
1207
+ bucket_start,
1208
+ (
1209
+ "CONNECTION_GENERATION_CHANGED"
1210
+ if generation != self._highest_ctp_scope[0]
1211
+ else "SUBSCRIPTION_EPOCH_CHANGED"
1212
+ ),
1213
+ )
1214
+ if not retired_ctp_scope:
1215
+ self._highest_ctp_scope = scope
1216
+ self._last_ctp_scope = scope
1217
+ self._last_connection_generation = generation
1218
+ elif generation not in (None, ""):
1219
+ if (
1220
+ self._last_connection_generation is not None
1221
+ and generation != self._last_connection_generation
1222
+ ):
1223
+ for builder in self._bar_builders.values():
1224
+ builder["quality_flags"].add("CONNECTION_GENERATION_CHANGED")
1225
+ self._flush_ready_bars(reason="generation", force_invalid=True)
1226
+ self._max_event_timestamp = None
1227
+ flags.add("CONNECTION_GENERATION_CHANGED")
1228
+ self._add_bar_quality_override(bucket_start, "CONNECTION_GENERATION_CHANGED")
1229
+ self._last_connection_generation = generation
1230
+
1231
+ if (
1232
+ self._timeframe != TimeFrame.Ticks
1233
+ and bucket_end is not None
1234
+ and self._last_closed_bucket_end is not None
1235
+ and bucket_end <= self._last_closed_bucket_end
1236
+ ):
1237
+ flags.add("BUCKET_ALREADY_CLOSED")
1238
+
1239
+ if tick_ts is not None and "EVENT_TIME_CONFLICT" not in flags and not retired_ctp_scope:
1240
+ if self._max_event_timestamp is None or tick_ts >= self._max_event_timestamp:
1241
+ self._max_event_timestamp = tick_ts
1242
+ self._last_ingest_monotonic_ns = self._now_monotonic_ns()
1243
+
1244
+ blocking = {
1245
+ flag
1246
+ for flag in flags
1247
+ if flag
1248
+ not in {
1249
+ "LEGACY_SCHEMA",
1250
+ "NO_TRADE",
1251
+ "VOLUME_BASELINE",
1252
+ }
1253
+ }
1254
+ volume_complete = bool(_tick_value(tick, "volume_complete", default=not ctp_schema))
1255
+ if ctp_schema and not volume_complete and delta > 0:
1256
+ blocking.add("VOLUME_INCOMPLETE")
1257
+ flags.add("VOLUME_INCOMPLETE")
1258
+ # A rejected snapshot can still prove that an already-open bucket is
1259
+ # incomplete. Preserve that evidence before _ingest_tick declines to
1260
+ # mutate OHLCV. Otherwise a later watermark could publish the earlier
1261
+ # trades as a deceptively complete bar after a volume/order/time gap.
1262
+ if bucket_start is not None and blocking:
1263
+ already_closed = (
1264
+ self._last_closed_bucket_end is not None
1265
+ and bucket_end is not None
1266
+ and bucket_end <= self._last_closed_bucket_end
1267
+ )
1268
+ if not already_closed:
1269
+ self._add_bar_quality_override(bucket_start, *blocking)
1270
+ execution_eligible = (
1271
+ (not strict_ctp_v2 or upstream_execution_eligible is True)
1272
+ and not blocking
1273
+ and all(value is not None and value > 0 for value in (bid, ask, bid_size, ask_size))
1274
+ )
1275
+ bar_eligible = not blocking and price is not None and price > 0 and delta > 0
1276
+ _set_tick_value(tick, "quality_flags", tuple(sorted(flags)))
1277
+ _set_tick_value(tick, "quality", "GOOD" if not blocking else "INVALID")
1278
+ _set_tick_value(tick, "execution_eligible", execution_eligible)
1279
+ _set_tick_value(tick, "bar_eligible", bar_eligible)
1280
+ self._prune_bar_quality_overrides()
1281
+
1282
+ def _flush_ready_bars(self, *, reason, force_invalid=False):
1283
+ """Close trade-backed buckets once the event-time watermark has passed."""
1284
+ watermark = self._event_time_watermark()
1285
+ if not self._bar_builders:
1286
+ self._prune_bar_quality_overrides(watermark)
1287
+ return 0
1288
+ watermark_delay = max(float(self.p.bar_watermark_ms or 0.0), 0.0) / 1000.0
1289
+ closed = 0
1290
+ for bucket_start in sorted(self._bar_builders):
1291
+ current = self._bar_builders[bucket_start]
1292
+ bucket_end = current["bucket_end"]
1293
+ deadline = bucket_end.replace(tzinfo=_UTC).timestamp() + watermark_delay
1294
+ if not force_invalid and (watermark is None or watermark < deadline):
1295
+ continue
1296
+ flags = set(current["quality_flags"])
1297
+ flags.update(self._bar_quality_overrides.pop(bucket_start, set()))
1298
+ if force_invalid:
1299
+ flags.add("FORCED_INVALIDATION")
1300
+ complete = bool(current["volume_complete"] and not flags.difference({"LEGACY_SCHEMA"}))
1301
+ available_ts = max(deadline, watermark or deadline)
1302
+ available_at = _dt.datetime.fromtimestamp(available_ts, _UTC)
1303
+ self._bar_sequence += 1
1304
+ first_seq = current["first_ingest_seq"]
1305
+ last_seq = current["last_ingest_seq"]
1306
+ generation = current["connection_generation"]
1307
+ bar_id = (
1308
+ f"{self._dataname}:{bucket_start.isoformat()}:{generation}:"
1309
+ f"{first_seq}-{last_seq}"
1310
+ )
1311
+ completed = BarEvent(
1312
+ timestamp=bucket_end.replace(tzinfo=_UTC).timestamp(),
1313
+ symbol=self._dataname,
1314
+ exchange=current["exchange"],
1315
+ asset_type=current["asset_type"],
1316
+ local_time=available_ts,
1317
+ **current["causal"],
1318
+ open=current["open"],
1319
+ high=current["high"],
1320
+ low=current["low"],
1321
+ close=current["close"],
1322
+ volume=current["volume"],
1323
+ openinterest=current["openinterest"],
1324
+ )
1325
+ extensions = {
1326
+ "bucket_start": bucket_start.replace(tzinfo=_UTC),
1327
+ "bucket_end": bucket_end.replace(tzinfo=_UTC),
1328
+ "closed_at": available_at,
1329
+ "available_at": available_at,
1330
+ "bar_available_at": available_at,
1331
+ "complete": complete,
1332
+ "quality": "GOOD" if complete else "INVALID",
1333
+ "quality_flags": tuple(sorted(flags)),
1334
+ "volume_complete": bool(current["volume_complete"]),
1335
+ "first_ingest_seq": first_seq,
1336
+ "last_ingest_seq": last_seq,
1337
+ "quote_cutoff_seq": last_seq,
1338
+ "trading_day": current["trading_day"],
1339
+ "action_day": current["action_day"],
1340
+ "rules_hash": current["rules_hash"],
1341
+ "session_segment": current["session_segment"],
1342
+ "connection_generation": generation,
1343
+ "bar_id": bar_id,
1344
+ "decision_version": bar_id,
1345
+ "closure_reason": reason,
1346
+ "bar_sequence": self._bar_sequence,
1347
+ "trade_count": current["trade_count"],
1348
+ "watermark": _dt.datetime.fromtimestamp(watermark or available_ts, _UTC),
1349
+ "max_event_time": _dt.datetime.fromtimestamp(current["last_timestamp"], _UTC),
1350
+ }
1351
+ for name, value in extensions.items():
1352
+ setattr(completed, name, value)
1353
+ self._attach_closed_bar_evidence(completed)
1354
+ if getattr(completed, "closed_bar_evidence", None) is not None:
1355
+ setattr(
1356
+ completed,
1357
+ "_closed_bar_evidence_dispatch_token",
1358
+ self._closed_bar_evidence_dispatch_token,
1359
+ )
1360
+ self._enqueue_bar_event(completed, bucket_start, deliver_lines=complete)
1361
+ del self._bar_builders[bucket_start]
1362
+ self._last_closed_bucket_end = bucket_end
1363
+ closed += 1
1364
+ self._bar_builder = next(reversed(self._bar_builders.values()), None)
1365
+ self._prune_bar_quality_overrides(watermark)
1366
+ return closed
1367
+
1368
+ def _dispatch_event(self, channel_type, priority, event_data):
1369
+ """Dispatch a tick/bar event into Cerebro's channel callback surface."""
1370
+ env = getattr(self, "_env", None)
1371
+ if env is None or not hasattr(env, "dispatch_channel_event"):
1372
+ self._mark_event_dropped(event_data, "strategy_dispatch_unavailable")
1373
+ return False
1374
+
1375
+ if channel_type == "tick":
1376
+ self._attach_ctp_decision_now(event_data)
1377
+
1378
+ event = Event(
1379
+ timestamp=_tick_timestamp(event_data),
1380
+ priority=priority,
1381
+ channel_type=channel_type,
1382
+ channel_name=self._dataname,
1383
+ data=event_data,
1384
+ )
1385
+ # Only feed-origin events carry this private reference. Channel queues
1386
+ # already drive the matching broker in their own event loop.
1387
+ event._source_feed = self
1388
+ sealed_evidence = (
1389
+ getattr(event_data, "closed_bar_evidence", None) if channel_type == "bar" else None
1390
+ )
1391
+ if sealed_evidence is not None:
1392
+ self._sealed_closed_bar_evidence_by_event_id[id(event_data)] = sealed_evidence
1393
+ try:
1394
+ env.dispatch_channel_event(event)
1395
+ except Exception:
1396
+ _safe_log("error", "btapifeed:1390 exception before re-raise (Exception)")
1397
+ self._mark_event_dropped(event_data, "strategy_dispatch_failed")
1398
+ raise
1399
+ finally:
1400
+ # Native callbacks are synchronous. Do not retain evidence
1401
+ # identity after their dispatch window has closed.
1402
+ self._sealed_closed_bar_evidence_by_event_id.pop(id(event_data), None)
1403
+ if self.store is not None and hasattr(self.store, "mark_strategy_delivered"):
1404
+ self.store.mark_strategy_delivered(event_data)
1405
+ return True
1406
+
1407
+ def _attach_ctp_decision_now(self, tick):
1408
+ """Attach caller-owned decision-boundary time to a strict CTP V2 tick.
1409
+
1410
+ Parent receipt time is useful evidence but cannot measure time spent
1411
+ in the Store/Feed path. A live caller must explicitly provide a
1412
+ calibrated same-domain provider; raw tick fields never supply this
1413
+ boundary. Replay code can provide its own deterministic evidence
1414
+ without involving this Feed.
1415
+ """
1416
+
1417
+ if _tick_value(tick, "schema_version", default=None) != "ctp.quote.v2":
1418
+ return
1419
+ decision_fields = (
1420
+ "cohort_decision_now_monotonic_ns",
1421
+ "cohort_decision_now_epoch",
1422
+ "cohort_decision_now_clock_domain_id",
1423
+ "cohort_decision_now_receive_clock_error_ms",
1424
+ "cohort_decision_now_receive_clock_quality",
1425
+ "cohort_decision_now_freshness_verified",
1426
+ )
1427
+ # These fields belong to this dispatch boundary. A raw transport
1428
+ # payload must never pre-populate them and masquerade as a later local
1429
+ # decision timestamp.
1430
+ for name in decision_fields:
1431
+ _set_tick_value(tick, name, None)
1432
+ provider = self.p.ctp_decision_now_provider
1433
+ if not callable(provider):
1434
+ return
1435
+ try:
1436
+ now = provider(tick)
1437
+ except Exception:
1438
+ _safe_log("warning", "btapifeed:1431 fallback on Exception")
1439
+ return
1440
+ if not isinstance(now, CtpCohortNow):
1441
+ return
1442
+ if now.clock_domain_id != _tick_value(tick, "clock_domain_id", default=None):
1443
+ return
1444
+ for name, value in (
1445
+ ("cohort_decision_now_monotonic_ns", now.now_monotonic_ns),
1446
+ ("cohort_decision_now_epoch", now.now_epoch),
1447
+ ("cohort_decision_now_clock_domain_id", now.clock_domain_id),
1448
+ ("cohort_decision_now_receive_clock_error_ms", now.receive_clock_error_ms),
1449
+ ("cohort_decision_now_receive_clock_quality", now.receive_clock_quality),
1450
+ ("cohort_decision_now_freshness_verified", now.freshness_verified),
1451
+ ):
1452
+ _set_tick_value(tick, name, value)
1453
+
1454
+ def _mark_event_dropped(self, event_data, reason):
1455
+ """Close Store conservation accounting for an undispatched feed event."""
1456
+ marker = getattr(self.store, "mark_feed_dropped", None)
1457
+ if callable(marker):
1458
+ marker(event_data, reason)
1459
+
1460
+ def _handle_event_health(self, event_data):
1461
+ """Emit feed status transitions and tell callers whether data is unsafe."""
1462
+ stale = bool(_tick_value(event_data, "stale", default=False))
1463
+ continuity = str(
1464
+ _tick_value(event_data, "continuity_status", "continuity", default="unknown")
1465
+ or "unknown"
1466
+ ).lower()
1467
+ unhealthy = stale or continuity in {
1468
+ "gap",
1469
+ "stale",
1470
+ "disconnected",
1471
+ "checksum_failed",
1472
+ "out_of_order",
1473
+ "invalid",
1474
+ }
1475
+ if unhealthy:
1476
+ if not self._continuity_degraded:
1477
+ self.put_notification(
1478
+ self.DELAYED,
1479
+ stale_reason=_tick_value(
1480
+ event_data, "stale_reason", default=continuity or "stale"
1481
+ ),
1482
+ event_id=_tick_value(event_data, "event_id", default=""),
1483
+ )
1484
+ # A later verified recovery is a fresh LIVE transition.
1485
+ self._live_notified = False
1486
+ self._continuity_degraded = True
1487
+ return True
1488
+ if self._continuity_degraded and continuity in {
1489
+ "ok",
1490
+ "continuous",
1491
+ "recovered",
1492
+ "snapshot",
1493
+ }:
1494
+ self._continuity_degraded = False
1495
+ self._mark_live()
1496
+ return False
1497
+
1498
+ def get_logging_health(self):
1499
+ """Return the number of feed log-sink failures observed in this process."""
1500
+ return dict(_LOGGING_HEALTH)
1501
+
1502
+ def _mark_live(self):
1503
+ """Emit the LIVE status exactly once when real-time traffic begins."""
1504
+ if self._continuity_degraded:
1505
+ return
1506
+ if not self._live_notified:
1507
+ self.put_notification(self.LIVE)
1508
+ self._live_notified = True
1509
+
1510
+ def _get_bucket_start(self, dt_value):
1511
+ """Round a tick timestamp down to the current feed timeframe bucket."""
1512
+ dt_value = dt_value.replace(microsecond=0)
1513
+
1514
+ if self._timeframe == TimeFrame.Seconds:
1515
+ second = (dt_value.second // self._compression) * self._compression
1516
+ return dt_value.replace(second=second)
1517
+
1518
+ if self._timeframe == TimeFrame.Minutes:
1519
+ minute = (dt_value.minute // self._compression) * self._compression
1520
+ return dt_value.replace(minute=minute, second=0)
1521
+
1522
+ if self._timeframe == TimeFrame.Days:
1523
+ return dt_value.replace(hour=0, minute=0, second=0)
1524
+
1525
+ # Fall back to minute-style bucketing for other sub-day frames.
1526
+ return dt_value.replace(second=0)
1527
+
1528
+ def _get_bucket_end(self, bucket_start):
1529
+ """Return the exclusive right edge for a feed bucket."""
1530
+ if self._timeframe == TimeFrame.Ticks:
1531
+ return bucket_start
1532
+ if self._timeframe == TimeFrame.Seconds:
1533
+ return bucket_start + _dt.timedelta(seconds=self._compression)
1534
+ if self._timeframe == TimeFrame.Minutes:
1535
+ return bucket_start + _dt.timedelta(minutes=self._compression)
1536
+ if self._timeframe == TimeFrame.Days:
1537
+ return bucket_start + _dt.timedelta(days=self._compression)
1538
+ return bucket_start + _dt.timedelta(minutes=self._compression)