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,3155 @@
1
+ #!/usr/bin/env python
2
+ """LineBuffer Module - Circular buffer storage for time-series data.
3
+
4
+ This module provides the LineBuffer class which implements a circular
5
+ buffer for storing time-series data. The buffer allows efficient
6
+ operations like appending, forwarding, rewinding, and resetting.
7
+
8
+ Key Features:
9
+ - Index 0 always points to the current active value
10
+ - Positive indices fetch past values (left-hand side)
11
+ - Negative indices fetch future values (right-hand side)
12
+ - Automatic memory management with qbuffer
13
+ - Line bindings for automatic value propagation
14
+
15
+ Classes:
16
+ LineBuffer: Core circular buffer implementation.
17
+ LineActions: Base class for line objects with multiple lines.
18
+ LineActionsMixin: Mixin providing line operations.
19
+ LineActionsCache: Cache system for performance optimization.
20
+ PseudoArray: Wrapper for non-array iterables.
21
+ LinesOperation: Operations on multiple lines.
22
+ LineOwnOperation: Operations on owned lines.
23
+
24
+ Example:
25
+ Basic buffer usage:
26
+ >>> buf = LineBuffer()
27
+ >>> buf.home() # Reset to beginning
28
+ >>> buf.forward() # Move to next position
29
+ >>> buf[0] = 100.0 # Set current value
30
+ >>> print(buf[0]) # Get current value
31
+ 100.0
32
+ >>> print(buf[-1]) # Get previous value
33
+ """
34
+
35
+ import array
36
+ import collections
37
+ import datetime
38
+ import itertools
39
+ import math
40
+ import operator
41
+ from itertools import islice, repeat
42
+
43
+ from . import metabase
44
+ from .lineroot import LineRoot, LineRootMixin, LineSingle
45
+ from .utils import num2date
46
+ from .utils.log_message import get_logger, throttled_error, throttled_warning
47
+ from .utils.py3 import range, string_types
48
+
49
+ logger = get_logger(__name__)
50
+
51
+ NAN = float("NaN")
52
+ INF = float("inf")
53
+ NEG_INF = float("-inf")
54
+
55
+ # PERFORMANCE OPTIMIZATION: Pre-create default datetime for error recovery
56
+ # Avoids repeated datetime object creation in hot path
57
+ _DEFAULT_DATETIME = datetime.datetime(2000, 1, 1, 0, 0, 0)
58
+
59
+
60
+ # PERFORMANCE OPTIMIZATION: Helper function to check for NaN/None values
61
+ # Using value != value is much faster than isinstance + math.isnan
62
+ def _is_nan_or_none(value):
63
+ """Fast check for NaN or None values.
64
+ NaN is the only value that's not equal to itself (value != value).
65
+ This is much faster than isinstance(value, float) and math.isnan(value).
66
+ """
67
+ return value is None or value != value
68
+
69
+
70
+ class LineBuffer(LineSingle, LineRootMixin):
71
+ """
72
+ LineBuffer defines an interface to an "array.array" (or list) in which
73
+ index 0 points to the item which is active for input and output.
74
+
75
+ Positive indices fetch values from the past (left-hand side)
76
+ Negative indices fetch values from the future (if the array has been
77
+ extended on the right-hand side)
78
+
79
+ With this behavior, no index has to be passed around to entities which have
80
+ to work with the current value produced by other entities: the value is
81
+ always reachable at "0".
82
+
83
+ Likewise, storing the current value produced by "self" is done at 0.
84
+
85
+ Additional operations to move the pointer (home, forward, extend, rewind,
86
+ advance getzero) are provided
87
+
88
+ The class can also hold "bindings" to other LineBuffers. When a value
89
+ is set in this class,
90
+ it will also be set in the binding.
91
+ """
92
+
93
+ # Define LineBuffer mode attributes: UnBounded (0) and QBuffer (1)
94
+ UnBounded, QBuffer = (0, 1)
95
+
96
+ # Initialization
97
+ def __init__(self):
98
+ """Initialize the LineBuffer instance.
99
+
100
+ Sets up all internal attributes including the array storage,
101
+ index pointer, buffer mode, and performance optimization flags.
102
+ """
103
+ # ===== Optimization A: Pre-initialize all attributes to eliminate runtime hasattr checks =====
104
+ # Core attributes - must be initialized first
105
+ self._minperiod = 1 # Minimum period
106
+ self._array = array.array("d") # Internal array storage
107
+ self._idx = -1 # Current index
108
+ self._size = 0 # Current array size
109
+
110
+ # Buffer-related attributes - set to reasonable defaults
111
+ self.maxlen = 0 # Maximum length (used in QBuffer mode)
112
+ self.extension = 0 # Extension size
113
+ self.lencount = 0 # Length counter
114
+ self.useislice = False # Whether to use islice
115
+ self.extrasize = 0 # Extra size
116
+ self.lenmark = 0 # Length mark
117
+
118
+ # Array - initialize as empty array (will be reset based on mode in reset())
119
+ self.array = array.array("d")
120
+
121
+ # Lines-related - ensure lines exists
122
+ if not hasattr(self, "lines"):
123
+ self.lines = [self] # lines is a list containing itself
124
+
125
+ # Mode and bindings
126
+ self.mode = self.UnBounded # Default unbounded mode
127
+ self.bindings = [] # Binding list
128
+
129
+ # Other attributes
130
+ self._tz = None # Timezone setting
131
+ self._owner = None # Owner object
132
+ self._clock = None # Clock object
133
+ self._ltype = None # Line type
134
+ # Pre-calculate whether this is an indicator line to avoid repeated checks in hot paths
135
+ try:
136
+ self._is_indicator = (self._ltype == 0) or ("Indicator" in str(self.__class__.__name__))
137
+ except Exception:
138
+ throttled_warning(
139
+ logger,
140
+ "linebuffer.init.indicator_classification_recovery",
141
+ "LineBuffer indicator classification failed; using non-indicator defaults",
142
+ exc_info=False,
143
+ )
144
+ self._is_indicator = False
145
+
146
+ # Performance optimization: pre-calculate whether this is a datetime line
147
+ # to avoid repeated checks in __setitem__. Check once at init and cache the result.
148
+ self._is_datetime_line = False
149
+ try:
150
+ if hasattr(self, "_name"):
151
+ name_str = str(self._name).lower()
152
+ self._is_datetime_line = "datetime" in name_str
153
+ elif hasattr(self, "__class__"):
154
+ class_str = str(self.__class__.__name__).lower()
155
+ self._is_datetime_line = "datetime" in class_str
156
+ except Exception:
157
+ throttled_warning(
158
+ logger,
159
+ "linebuffer.init.datetime_classification_recovery",
160
+ "LineBuffer datetime classification failed; using non-datetime defaults",
161
+ exc_info=False,
162
+ )
163
+ self._is_datetime_line = False
164
+
165
+ # Pre-calculate default value to avoid repeated checks in __setitem__
166
+ if self._is_datetime_line:
167
+ self._default_value = 1.0 # datetime lines use 1.0 (valid ordinal value)
168
+ elif self._is_indicator:
169
+ self._default_value = float("nan") # indicators use NaN
170
+ else:
171
+ self._default_value = 0.0 # others use 0.0
172
+
173
+ # Recursion guard (for __len__)
174
+ self._in_len = False # Instance attribute guard replacing global set
175
+
176
+ self._dt_cache_idx = None
177
+ self._dt_cache_value = None
178
+ self._dt_cache_tz = None
179
+ self._dt_cache_dt = None
180
+
181
+ # Call reset to complete initialization
182
+ self.reset() # Reset, call own reset method
183
+
184
+ # Get the value of _idx
185
+ def get_idx(self):
186
+ """Get the current index position.
187
+
188
+ Returns:
189
+ int: The current index in the buffer.
190
+ """
191
+ # Optimization A: Removed hasattr check, __init__ ensures _idx exists
192
+ return self._idx
193
+
194
+ def _refresh_cached_line_flags(self, owner=None, ltype=None):
195
+ """Refresh cached owner/type-derived flags after a line is attached."""
196
+ if owner is not None:
197
+ self._owner = owner
198
+ if ltype is not None:
199
+ self._ltype = ltype
200
+
201
+ effective_ltype = getattr(self, "_ltype", None)
202
+ owner_obj = getattr(self, "_owner", None)
203
+ owner_ref = getattr(owner_obj, "_owner_ref", None)
204
+
205
+ if effective_ltype is None and owner_ref is not None:
206
+ effective_ltype = getattr(owner_ref, "_ltype", None)
207
+ if effective_ltype is None and owner_obj is not None:
208
+ effective_ltype = getattr(owner_obj, "_ltype", None)
209
+
210
+ try:
211
+ self._is_indicator = (effective_ltype == LineRoot.IndType) or (
212
+ "Indicator" in str(self.__class__.__name__)
213
+ )
214
+ except Exception:
215
+ throttled_warning(
216
+ logger,
217
+ "linebuffer.refresh.indicator_classification_recovery",
218
+ "LineBuffer cached indicator classification failed; using non-indicator defaults",
219
+ exc_info=False,
220
+ )
221
+ self._is_indicator = False
222
+
223
+ try:
224
+ if hasattr(self, "_name"):
225
+ name_str = str(self._name).lower()
226
+ self._is_datetime_line = "datetime" in name_str
227
+ else:
228
+ class_str = str(self.__class__.__name__).lower()
229
+ self._is_datetime_line = "datetime" in class_str
230
+ except Exception:
231
+ throttled_warning(
232
+ logger,
233
+ "linebuffer.refresh.datetime_classification_recovery",
234
+ "LineBuffer cached datetime classification failed; using non-datetime defaults",
235
+ exc_info=False,
236
+ )
237
+ self._is_datetime_line = False
238
+
239
+ if self._is_datetime_line:
240
+ self._default_value = 1.0
241
+ elif self._is_indicator:
242
+ self._default_value = float("nan")
243
+ else:
244
+ self._default_value = 0.0
245
+
246
+ # Set the value of _idx
247
+ def set_idx(self, idx, force=False):
248
+ """Set the index position.
249
+
250
+ Args:
251
+ idx: The new index value.
252
+ force: If True, force set even in QBuffer mode at lenmark.
253
+
254
+ Note:
255
+ In QBuffer mode, when at lenmark, the index stays at 0
256
+ unless force is True. This allows resampling operations.
257
+ """
258
+ # If QBuffer and the last position of the buffer were reached, keep
259
+ # it (unless force) as index 0. This allows resampling
260
+ # - forward adds a position. However, the 1st one is discarded, the 0 is
261
+ # invariant
262
+ # force supports replaying, which needs the extra bar to float
263
+ # forward/backwards, because the last input is read, and after a
264
+ # "backwards" is used to update the previous data. Unless position
265
+ # 0 was moved to the previous index, it would fail
266
+ # Optimization A: Removed all hasattr checks, __init__ ensures all attributes exist
267
+ if self.mode == self.QBuffer:
268
+ if force or self._idx < self.lenmark:
269
+ self._idx = idx
270
+ else: # default: UnBounded
271
+ self._idx = idx
272
+
273
+ # Property usage: can be used to get and set idx
274
+ idx = property(get_idx, set_idx)
275
+
276
+ # Reset
277
+ def reset(self):
278
+ """Resets the internal buffer structure and the indices"""
279
+ # CRITICAL FIX: In runonce mode, if array is already populated (from _once()),
280
+ # preserve the array and lencount, only reset idx
281
+ # Check if we're in runonce mode and array is populated
282
+ preserve_array = False
283
+ try:
284
+ # Check if this is an indicator line that was processed in runonce mode
285
+ # Line's _owner might be a Lines object, which has _owner pointing to the indicator
286
+ if hasattr(self, "_owner") and self._owner is not None:
287
+ owner = self._owner
288
+ # Check if owner is a Lines object (which wraps lines for indicators)
289
+ # Lines objects have _owner pointing to the actual indicator
290
+ if hasattr(owner, "_owner") and owner._owner is not None:
291
+ indicator = owner._owner
292
+ # Check if indicator was processed in runonce mode
293
+ if hasattr(indicator, "_once_called") and indicator._once_called:
294
+ # Check if array has data
295
+ if hasattr(self, "array") and self.array is not None:
296
+ array_len = len(self.array)
297
+ if array_len > 0:
298
+ preserve_array = True
299
+ # Also check if owner itself is an indicator
300
+ elif hasattr(owner, "_once_called") and owner._once_called:
301
+ # Check if array has data
302
+ if hasattr(self, "array") and self.array is not None:
303
+ array_len = len(self.array)
304
+ if array_len > 0:
305
+ preserve_array = True
306
+ except Exception:
307
+ throttled_warning(
308
+ logger,
309
+ "linebuffer.reset.runonce_preservation_recovery",
310
+ "LineBuffer runonce array preservation check failed; resetting normally",
311
+ exc_info=False,
312
+ )
313
+
314
+ if preserve_array:
315
+ # In runonce mode with populated arrays, preserve the precomputed
316
+ # values but restart logical length so Cerebro's event replay can
317
+ # advance indicators according to their own clocks.
318
+ self.idx = -1
319
+ if hasattr(self, "lencount"):
320
+ self.lencount = 0
321
+ self.extension = 0
322
+ else:
323
+ # Normal reset: clear array and reset all counters
324
+ # Optimization A: Removed hasattr checks, all attributes initialized in __init__
325
+ # If in cache mode (QBuffer), use deque to store data with fixed size
326
+ if self.mode == self.QBuffer:
327
+ # Add extrasize to ensure resample/replay work
328
+ deque_maxlen = max(1, self.maxlen + self.extrasize)
329
+ self.array = collections.deque(maxlen=deque_maxlen)
330
+ self.useislice = True
331
+ else:
332
+ # Non-cache mode, use array.array
333
+ self.array = array.array("d")
334
+ self.useislice = False
335
+
336
+ # CRITICAL FIX: Do NOT pre-fill array - this causes buflen() to be incorrect
337
+ # buflen() = len(array) - extension, so pre-filling increases buflen incorrectly
338
+ # Instead, let forward() handle array growth naturally
339
+
340
+ # Reset counters and indices
341
+ self.lencount = 0
342
+ self.idx = -1
343
+ self.extension = 0
344
+
345
+ # Set cache-related variables
346
+ def qbuffer(self, savemem=0, extrasize=0):
347
+ """Enable queued buffer mode for memory-efficient storage.
348
+
349
+ Args:
350
+ savemem: Memory saving mode (0=normal, >0=enable cache mode).
351
+ extrasize: Extra buffer size for resampling/replay operations.
352
+
353
+ Note:
354
+ In QBuffer mode, only the last maxlen values are kept,
355
+ reducing memory usage for long backtests.
356
+ """
357
+ self.mode = self.QBuffer # Set specific mode
358
+ self.maxlen = max(1, self._minperiod) # Set maximum length, ensure at least 1
359
+ self.extrasize = max(0, extrasize) # Set extra size, ensure non-negative
360
+ self.lenmark = self.maxlen - (not self.extrasize) # Max length minus 1 if extrasize=0
361
+ self.reset() # Reset
362
+
363
+ # Get indicator values
364
+ def getindicators(self):
365
+ """Get list of indicators using this line buffer.
366
+
367
+ Returns:
368
+ list: Empty list for base LineBuffer (override in subclasses).
369
+ """
370
+ return []
371
+
372
+ # Minimum buffer
373
+ def minbuffer(self, size):
374
+ """The linebuffer must guarantee the minimum requested size to be
375
+ available.
376
+
377
+ In non-dqbuffer mode, this is always true (of course, until data is
378
+ filled at the beginning, there are fewer values, but minperiod in the
379
+ framework should account for this.
380
+
381
+ In dqbuffer mode, the buffer has to be adjusted for this if currently
382
+ less than requested
383
+ """
384
+ # If not in cache mode or max length is already >= size, return None
385
+ if self.mode != self.QBuffer or self.maxlen >= size:
386
+ return
387
+ # In cache mode, set maxlen equal to size
388
+ self.maxlen = size
389
+ # Max length minus 1 if self.extrasize=0
390
+ self.lenmark = self.maxlen - (not self.extrasize)
391
+ # Reset
392
+ self.reset()
393
+
394
+ # Return actual length
395
+ def __len__(self):
396
+ """
397
+ Return the linebuffer's length counter.
398
+
399
+ Performance optimization: Restore master branch's simple implementation
400
+ - Directly return self.lencount (pre-calculated length value)
401
+ - Remove all recursion checks, hasattr calls and complex logic
402
+ - Performance improvement: from 0.611s to ~0.05s (92% improvement)
403
+ """
404
+ return self.lencount
405
+
406
+ # Return the length of data in the line cache
407
+ def buflen(self):
408
+ """Real data that can be currently held in the internal buffer
409
+
410
+ The internal buffer can be longer than the actual stored data to
411
+ allow for "lookahead" operations. The real amount of data that is
412
+ held/can be held in the buffer
413
+ is returned
414
+ """
415
+ return len(self.array) - self.extension
416
+
417
+ def __getitem__(self, ago):
418
+ """
419
+ Get the value at a specified offset - optimized for hot path.
420
+
421
+ Args:
422
+ ago (int): Relative offset from current index (0=current, -1=previous, 1=next)
423
+
424
+ Returns:
425
+ Value at the specified position
426
+ """
427
+ if ago == 0:
428
+ try:
429
+ current_idx = self._idx
430
+ if current_idx == self.lencount - 1:
431
+ value = self.array[current_idx]
432
+ if value in (INF, NEG_INF):
433
+ return 0.0
434
+ return value
435
+ if self.lencount > 0 and current_idx >= self.lencount:
436
+ current_idx = self.lencount - 1
437
+ value = self.array[current_idx]
438
+ if value in (INF, NEG_INF):
439
+ return 0.0
440
+ return value
441
+ except IndexError:
442
+ # An unpopulated buffer is an expected EAFP probe. Preserve the
443
+ # historical slow-path fallback without adding hot-path noise.
444
+ pass
445
+
446
+ # PERFORMANCE OPTIMIZATION: Fast path for common case (ago <= 0)
447
+ # Avoid __dict__ access for majority of calls
448
+ try:
449
+ current_idx = self._idx
450
+ lencount = self.lencount
451
+ if lencount > 0 and current_idx >= lencount:
452
+ current_idx = lencount - 1
453
+ value = self.array[current_idx + ago]
454
+ if value in (INF, NEG_INF):
455
+ return 0.0
456
+ return value
457
+ except IndexError:
458
+ # Index out of buffer range is an expected protocol probe; fall
459
+ # through to the existing slow-path handling without logging.
460
+ pass
461
+
462
+ # Slow path: handle special cases
463
+ # CRITICAL FIX: For data feed lines accessing FUTURE data
464
+ is_data_feed_line = getattr(self, "_is_data_feed_line", False)
465
+ if is_data_feed_line and ago > 0:
466
+ target_idx = self._idx + ago
467
+ if target_idx >= len(self.array) or self.array[target_idx] == 0.0:
468
+ raise IndexError("array index out of range")
469
+
470
+ # Check the simple flag for data feed line
471
+ if is_data_feed_line:
472
+ raise IndexError("array index out of range")
473
+
474
+ # For indicators and other cases, return appropriate default
475
+ if getattr(self, "_is_indicator", False):
476
+ return float("nan")
477
+ return 0.0
478
+
479
+ # Get data values, widely used in strategies
480
+ def get(self, ago=0, size=1):
481
+ """Returns a slice of the array relative to *ago*
482
+
483
+ Keyword Args:
484
+ ago (int): Point of the array to which size will be added
485
+ to return the slice size(int): size of the slice to return,
486
+ can be positive or negative
487
+
488
+ If size is positive *ago* will mark the end of the iterable and vice
489
+ versa if size is negative
490
+
491
+ Returns:
492
+ A slice of the underlying buffer
493
+ """
494
+ # Whether to use islice, use following syntax if true
495
+ start = self._idx + ago - size + 1
496
+ end = self._idx + ago + 1
497
+ if self.useislice:
498
+ values = list(islice(self.array, start, end))
499
+ else:
500
+ # If not using islice, directly slice the array
501
+ values = self.array[start:end]
502
+ if getattr(values, "typecode", None) == "d":
503
+ for idx, value in enumerate(values):
504
+ if value in (INF, NEG_INF):
505
+ values[idx] = 0.0
506
+ return values
507
+
508
+ return array.array(
509
+ "d",
510
+ ((0.0 if value in (INF, NEG_INF) else value) for value in values),
511
+ )
512
+
513
+ # Return the value at the actual index 0 of the array
514
+ def getzeroval(self, idx=0):
515
+ """Returns a single value of the array relative to the real zero
516
+ of the buffer
517
+
518
+ Keyword Args:
519
+ idx (int): Where to start relative to the real start of the buffer
520
+ size(int): size of the slice to return
521
+
522
+ Returns:
523
+ A slice of the underlying buffer
524
+ """
525
+ value = self.array[idx]
526
+ if isinstance(value, float) and (value in (INF, NEG_INF)):
527
+ return 0.0
528
+ return value
529
+
530
+ # Return data of size starting from idx in the array
531
+ def getzero(self, idx=0, size=1):
532
+ """Returns a slice of the array relative to the real zero of the buffer
533
+
534
+ Keyword Args:
535
+ idx (int): Where to start relative to the real start of the buffer
536
+ size(int): size of the slice to return
537
+
538
+ Returns:
539
+ A slice of the underlying buffer
540
+ """
541
+ if self.useislice:
542
+ values = list(islice(self.array, idx, idx + size))
543
+ else:
544
+ values = list(self.array[idx : idx + size])
545
+
546
+ return array.array(
547
+ "d",
548
+ (
549
+ (0.0 if isinstance(value, float) and (value in (INF, NEG_INF)) else value)
550
+ for value in values
551
+ ),
552
+ )
553
+
554
+ # Set values to the array
555
+ def __setitem__(self, ago, value):
556
+ """Sets a value at position "ago" and executes any associated bindings
557
+
558
+ Keyword Args:
559
+ ago (int): Point of the array to which size will be added to return
560
+ the slice
561
+ value (variable): value to be set
562
+
563
+ Performance optimization: Use pre-calculated flags to avoid repeated
564
+ hasattr and string operations
565
+ """
566
+ # Performance optimization: Use try-except instead of hasattr to check array existence
567
+ # array is already initialized in __init__, this is just a defensive check
568
+ try:
569
+ array = self.array
570
+ except AttributeError:
571
+ import array as array_module
572
+
573
+ array = array_module.array("d")
574
+ self.array = array
575
+
576
+ # Performance optimization: Use pre-calculated flags and default values
577
+ # Handle None/NaN values - use fast path for checking
578
+ if value is None:
579
+ value = self._default_value
580
+ # PERFORMANCE OPTIMIZATION: Use value != value for NaN check.
581
+ # Preserve explicit NaN writes for non-datetime lines. Data feeds often
582
+ # use NaN as a sparse signal sentinel; converting it to 0.0 turns
583
+ # "no signal" into a finite tradable value.
584
+ elif value != value: # NaN detection without isinstance + isnan
585
+ value = self._default_value if self._is_datetime_line else float("nan")
586
+ elif isinstance(value, float) and (value in (INF, NEG_INF)):
587
+ value = self._default_value
588
+ # datetime line value validation
589
+ elif self._is_datetime_line and value < 1.0:
590
+ value = 1.0
591
+ elif self._is_datetime_line:
592
+ # For non-numeric datetime line values, convert to 1.0
593
+ try:
594
+ float_value = float(value)
595
+ value = 1.0 if float_value < 1.0 else float_value
596
+ except (TypeError, ValueError):
597
+ value = 1.0
598
+
599
+ # Calculate the required index
600
+ required_index = self._idx + ago
601
+
602
+ # Handle index out of bounds - fast path
603
+ array_len = len(array)
604
+ if required_index >= array_len:
605
+ # Performance optimization: Use pre-calculated default value as fill value
606
+ fill_value = self._default_value
607
+ extend_size = required_index - array_len + 1
608
+
609
+ # Batch extend the array
610
+ for _ in range(extend_size):
611
+ array.append(fill_value)
612
+ elif required_index < 0:
613
+ # Skip setting values for negative indices
614
+ return
615
+
616
+ # Set the value at the required index
617
+ array[required_index] = value
618
+
619
+ # Update any bindings - only execute if bindings exist
620
+ # Performance optimization: bindings are empty in most cases, check before processing
621
+ if self.bindings:
622
+ for binding in self.bindings:
623
+ # Performance optimization: Use try-except to get binding's datetime flag
624
+ # Most bindings are not datetime lines, fast path
625
+ try:
626
+ binding_is_datetime = binding._is_datetime_line
627
+ except AttributeError:
628
+ # Binding doesn't have pre-calculated flag, fall back to simple check
629
+ binding_is_datetime = False
630
+
631
+ binding_value = value
632
+ if binding_is_datetime and (
633
+ not isinstance(binding_value, (int, float)) or binding_value < 1.0
634
+ ):
635
+ binding_value = 1.0
636
+
637
+ binding[ago] = binding_value
638
+
639
+ # Set specific value to array
640
+ def set(self, value, ago=0):
641
+ """Sets a value at position "ago" and executes any associated bindings
642
+
643
+ Keyword Args:
644
+ value (variable): value to be set
645
+ ago (int): Point of the array to which size will be added to return
646
+ the slice
647
+
648
+ PERF: Uses pre-calculated _is_datetime_line and _default_value flags
649
+ instead of hasattr/isinstance checks on every call.
650
+ """
651
+ # PERF: Use pre-calculated flag instead of hasattr + string ops
652
+ is_dt = self._is_datetime_line
653
+
654
+ # Handle None/NaN values using fast detection
655
+ if value is NAN or (
656
+ value is None
657
+ or value != value
658
+ or isinstance(value, float)
659
+ and (value in (INF, NEG_INF))
660
+ ):
661
+ value = self._default_value
662
+ elif is_dt and (not isinstance(value, (int, float)) or value < 1.0):
663
+ value = 1.0
664
+
665
+ # Array is always initialized in __init__, use direct access
666
+ arr = self.array
667
+ required_index = self._idx + ago
668
+ arr_len = len(arr)
669
+ if required_index >= arr_len:
670
+ fill_value = self._default_value
671
+ for _ in range(required_index - arr_len + 1):
672
+ arr.append(fill_value)
673
+ elif required_index < 0:
674
+ return
675
+
676
+ arr[required_index] = value
677
+ if self.bindings:
678
+ for binding in self.bindings:
679
+ try:
680
+ b_is_dt = binding._is_datetime_line
681
+ except AttributeError:
682
+ b_is_dt = False
683
+
684
+ b_val = value
685
+ if b_is_dt and (not isinstance(b_val, (int, float)) or b_val < 1.0):
686
+ b_val = 1.0
687
+ binding[ago] = b_val
688
+
689
+ # Return to the beginning
690
+ def home(self):
691
+ """Rewinds the logical index to the beginning
692
+
693
+ The underlying buffer remains untouched and the actual len can be found
694
+ out with buflen
695
+ """
696
+ self.idx = -1
697
+ self.lencount = 0
698
+
699
+ # Move forward one step
700
+ def forward(self, value=NAN, size=1):
701
+ """Moves the logical index forward and enlarges the buffer as much as needed
702
+
703
+ Keyword Args:
704
+ value (variable): value to be set in new positions
705
+ size (int): How many extra positions to enlarge the buffer
706
+ """
707
+ if value is NAN and size == 1:
708
+ if not self._is_indicator:
709
+ clock = self._clock
710
+ if clock is not None:
711
+ try:
712
+ if self.lencount >= len(clock):
713
+ return
714
+ except Exception:
715
+ # A broken optional clock must not block the line from
716
+ # advancing. Keep the established recovery semantics
717
+ # without rendering arbitrary exception data.
718
+ throttled_warning(
719
+ logger,
720
+ "linebuffer.forward.clock_length_recovery",
721
+ "LineBuffer clock length lookup failed; continuing forward",
722
+ exc_info=False,
723
+ )
724
+
725
+ if self.mode == self.QBuffer:
726
+ self.idx = self._idx + 1
727
+ else:
728
+ self._idx += 1
729
+ self.lencount += 1
730
+ self.array.append(self._default_value)
731
+ return
732
+
733
+ # PERFORMANCE OPTIMIZATION: Direct attribute access (faster than __dict__.get)
734
+ # Attributes are guaranteed to exist after __init__
735
+ is_indicator = self._is_indicator
736
+
737
+ # PERFORMANCE OPTIMIZATION: Use value != value for NaN check
738
+ # NaN is the only value that's not equal to itself
739
+ if value is NAN or (
740
+ value is None
741
+ or value != value
742
+ or isinstance(value, float)
743
+ and (value in (INF, NEG_INF))
744
+ ):
745
+ value = self._default_value
746
+
747
+ # For non-indicators, follow clock synchronization
748
+ if not is_indicator:
749
+ clock = self._clock
750
+ if clock is not None:
751
+ try:
752
+ clock_len = len(clock)
753
+ current_len = self.lencount
754
+ if current_len >= clock_len:
755
+ return
756
+ max_advance = clock_len - current_len
757
+ if size > max_advance:
758
+ size = max_advance
759
+ if size <= 0:
760
+ return
761
+ except Exception:
762
+ # Keep the requested size when a clock cannot be measured.
763
+ throttled_warning(
764
+ logger,
765
+ "linebuffer.forward.clock_length_recovery",
766
+ "LineBuffer clock length lookup failed; continuing forward",
767
+ exc_info=False,
768
+ )
769
+
770
+ # CRITICAL FIX: Ensure we have a valid size
771
+ if size <= 0:
772
+ return
773
+
774
+ if self.mode == self.QBuffer:
775
+ self.idx = self._idx + size
776
+ else:
777
+ self._idx += size
778
+ self.lencount += size
779
+
780
+ append_val = value
781
+ array = self.array
782
+ if size == 1:
783
+ array.append(append_val)
784
+ else:
785
+ # Batch extend for multiple positions
786
+ array.extend([append_val] * size)
787
+
788
+ # Move backward one step
789
+ def backwards(self, size=1, force=False):
790
+ """Moves the logical index backwards and reduces the buffer as much as needed
791
+
792
+ Keyword Args:
793
+ size (int): How many extra positions to rewind the buffer
794
+ force (bool): Whether to force the reduction of the logical buffer
795
+ regardless of the minperiod
796
+ """
797
+ # CRITICAL FIX: Match master behavior - use set_idx for force support and pop array elements
798
+ new_idx = self._idx - size
799
+ if self.mode == self.QBuffer:
800
+ self.set_idx(new_idx, force=force)
801
+ else:
802
+ self._idx = new_idx
803
+ self.lencount -= size
804
+ # PERFORMANCE OPTIMIZATION: Use slice deletion instead of loop pop
805
+ # Called 3.4M+ times, batch deletion is faster than loop
806
+ arr = self.array
807
+ arr_len = len(arr)
808
+ if arr_len > 0:
809
+ remove_count = min(size, arr_len)
810
+ try:
811
+ del arr[arr_len - remove_count :]
812
+ except TypeError:
813
+ # qbuffer/exactbars uses deque, which does not support slice
814
+ # deletion. Remove newest values explicitly in that mode.
815
+ for _ in range(remove_count):
816
+ arr.pop()
817
+
818
+ # Move backward one step (original backwards was overridden)
819
+ def safe_backwards(self, size=1):
820
+ """Safely move the index backwards without raising errors.
821
+
822
+ Args:
823
+ size: Number of positions to move backwards.
824
+
825
+ Returns:
826
+ bool: True if index is still >= 0 after moving, False otherwise.
827
+ """
828
+ # PERF: _idx is always initialized in __init__, skip hasattr
829
+ idx = self._idx
830
+ if idx is None:
831
+ self._idx = -1
832
+ return False
833
+ self._idx = idx - size
834
+ return self._idx >= 0
835
+
836
+ # Decrease idx and lencount by size
837
+ def rewind(self, size=1):
838
+ """Rewind the buffer by decreasing idx and lencount.
839
+
840
+ Args:
841
+ size: Number of positions to rewind.
842
+ """
843
+ # PERF: idx and lencount are always initialized in __init__
844
+ if self.mode == self.QBuffer:
845
+ self.idx = self._idx - size
846
+ else:
847
+ self._idx -= size
848
+ self.lencount -= size
849
+
850
+ # Increase idx and lencount by size
851
+ def advance(self, size=1):
852
+ """Advances the logical index without touching the underlying buffer"""
853
+ # CRITICAL FIX: Remove hasattr checks - attributes are always initialized in __init__
854
+ # The hasattr checks were preventing proper advancement
855
+ if self.mode == self.QBuffer:
856
+ self.idx = self._idx + size
857
+ else:
858
+ self._idx += size
859
+ self.lencount += size
860
+
861
+ # Extend forward
862
+ def extend(self, value=float("nan"), size=0):
863
+ """Extends the underlying array with positions that the index will not reach
864
+
865
+ Keyword Args:
866
+ value (variable): value to be set in new positins
867
+ size (int): How many extra positions to enlarge the buffer
868
+
869
+ The purpose is to allow for lookahead operations or to be able to
870
+ set values in the buffer "future"
871
+ """
872
+ if (
873
+ value is None
874
+ or value != value
875
+ or isinstance(value, float)
876
+ and (value in (INF, NEG_INF))
877
+ ):
878
+ value = self._default_value
879
+
880
+ self.extension += size
881
+ for i in range(size):
882
+ self.array.append(value)
883
+
884
+ # Add another LineBuffer
885
+ def addbinding(self, binding):
886
+ """Adds another line binding
887
+
888
+ Keyword Args:
889
+ binding (LineBuffer): another line that must be set when this line
890
+ becomes a value
891
+ """
892
+ self.bindings.append(binding)
893
+ # record in the binding when the period is starting (never sooner
894
+ # than self)
895
+ binding.updateminperiod(self._minperiod)
896
+
897
+ # Get all data starting from idx
898
+ def plot(self, idx=0, size=None):
899
+ """Returns a slice of the array relative to the real zero of the buffer
900
+
901
+ Keyword Args:
902
+ idx (int): Where to start relative to the real start of the buffer
903
+ size(int): size of the slice to return
904
+
905
+ This is a variant of getzero that unless told otherwise returns the
906
+ entire buffer, which is usually the idea behind plottint (all must
907
+ be plotted)
908
+
909
+ Returns:
910
+ A slice of the underlying buffer
911
+ """
912
+ return self.getzero(idx, size or len(self))
913
+
914
+ # Get partial data from array
915
+ def plotrange(self, start, end):
916
+ """Get a slice of data from the array.
917
+
918
+ Args:
919
+ start: Start index of the slice.
920
+ end: End index of the slice.
921
+
922
+ Returns:
923
+ list or array: Slice of data from start to end.
924
+ """
925
+ if self.useislice:
926
+ values = list(islice(self.array, start, end))
927
+ else:
928
+ values = list(self.array[start:end])
929
+
930
+ return [
931
+ (0.0 if isinstance(value, float) and (value in (INF, NEG_INF)) else value)
932
+ for value in values
933
+ ]
934
+
935
+ # Set array values for each binding when running in once mode
936
+ def oncebinding(self):
937
+ """
938
+ Executes the bindings when running in "once" mode
939
+ """
940
+ larray = self.array
941
+ blen = self.buflen()
942
+
943
+ for binding in self.bindings:
944
+ binding.array[0:blen] = larray[0:blen]
945
+
946
+ # Convert binding to line
947
+ def bind2lines(self, binding=0):
948
+ """
949
+ Stores a binding to another line. "Binding" can be an index or a name
950
+ """
951
+ if isinstance(binding, string_types):
952
+ line = getattr(self._owner.lines, binding)
953
+ else:
954
+ line = self._owner.lines[binding]
955
+
956
+ self.addbinding(line)
957
+
958
+ return self
959
+
960
+ bind2line = bind2lines
961
+
962
+ def __call__(self, ago=None):
963
+ """Returns either the current value (ago=None) or a delayed LineBuffer
964
+ that fetches the value which is "ago" periods before. Useful to have
965
+ the closing price 5 bars before: close(-5)
966
+ """
967
+ if ago is None:
968
+ return self[0]
969
+ return LineDelay(self, ago)
970
+
971
+ def _makeoperation(self, other, operation, r=False, _ownerskip=None, original_other=None):
972
+ # Only set parent_a/parent_b to LineActions instances (LinesOperation, _LineDelay, etc.).
973
+ # Full indicators (ATR, SMA, SuperTrend, etc.) are processed separately by _lineiterators
974
+ # ordering in _once(), so they must never be called via _parent_a.once() which would
975
+ # trigger premature once_via_next() calls that corrupt the data feed index state.
976
+ parent_a = None
977
+ if hasattr(self, "_owner") and self._owner is not None:
978
+ owner = self._owner
979
+ if hasattr(owner, "_owner_ref") and owner._owner_ref is not None:
980
+ ref = owner._owner_ref
981
+ if isinstance(ref, LineActions):
982
+ parent_a = ref
983
+ elif isinstance(owner, LineActions):
984
+ parent_a = owner
985
+ parent_b_candidate = original_other if original_other is not None else other
986
+ parent_b = parent_b_candidate if isinstance(parent_b_candidate, LineActions) else None
987
+ return LinesOperation(self, other, operation, r=r, parent_a=parent_a, parent_b=parent_b)
988
+
989
+ def _makeoperationown(self, operation, _ownerskip=None):
990
+ parent_a = None
991
+ if hasattr(self, "_owner") and self._owner is not None:
992
+ owner = self._owner
993
+ if hasattr(owner, "_owner_ref") and owner._owner_ref is not None:
994
+ ref = owner._owner_ref
995
+ if isinstance(ref, LineActions):
996
+ parent_a = ref
997
+ elif isinstance(owner, LineActions):
998
+ parent_a = owner
999
+ return LineOwnOperation(self, operation, parent_a=parent_a)
1000
+
1001
+ def _settz(self, tz):
1002
+ self._tz = tz
1003
+
1004
+ def datetime(self, ago=0, tz=None, naive=True):
1005
+ """Get the datetime value at the specified offset.
1006
+
1007
+ Args:
1008
+ ago: Number of periods to look back (0=current, -1=previous).
1009
+ tz: Timezone to apply. If None, uses self._tz.
1010
+ naive: If True, return naive datetime without timezone info.
1011
+
1012
+ Returns:
1013
+ datetime: Datetime object representing the timestamp.
1014
+
1015
+ Raises:
1016
+ IndexError: If the requested position is out of bounds for data feeds.
1017
+ """
1018
+ # PERFORMANCE OPTIMIZATION: Simplified datetime() method
1019
+ # - Use module-level _DEFAULT_DATETIME constant
1020
+ # - Remove redundant import statements
1021
+ # - Reduce nested try-except blocks
1022
+
1023
+ # Get value, may raise IndexError for data feeds
1024
+ value = self[ago]
1025
+
1026
+ # Fast path: Check for NaN/None values
1027
+ if _is_nan_or_none(value):
1028
+ return _DEFAULT_DATETIME if naive else _DEFAULT_DATETIME.replace(tzinfo=tz or self._tz)
1029
+
1030
+ # Fast path: Common case (ago=0, tz=None, naive=True) with caching
1031
+ if ago == 0 and tz is None and naive:
1032
+ current_idx = self._idx
1033
+ if self.lencount > 0 and current_idx >= self.lencount:
1034
+ current_idx = self.lencount - 1
1035
+
1036
+ # Check cache
1037
+ if (
1038
+ getattr(self, "_dt_cache_idx", None) == current_idx
1039
+ and getattr(self, "_dt_cache_tz", None) is self._tz
1040
+ and getattr(self, "_dt_cache_value", None) == value
1041
+ ):
1042
+ return self._dt_cache_dt
1043
+
1044
+ # Convert and cache
1045
+ try:
1046
+ dt = num2date(value, self._tz, True)
1047
+ self._dt_cache_idx = current_idx
1048
+ self._dt_cache_tz = self._tz
1049
+ self._dt_cache_value = value
1050
+ self._dt_cache_dt = dt
1051
+ return dt
1052
+ except (ValueError, OverflowError):
1053
+ return _DEFAULT_DATETIME
1054
+
1055
+ # Slow path: non-default parameters
1056
+ try:
1057
+ return num2date(value, tz or self._tz, naive)
1058
+ except (ValueError, OverflowError):
1059
+ return _DEFAULT_DATETIME if naive else _DEFAULT_DATETIME.replace(tzinfo=tz or self._tz)
1060
+
1061
+ def date(self, ago=0, tz=None, naive=True):
1062
+ """Get the date component of the datetime value at the specified offset.
1063
+
1064
+ Args:
1065
+ ago: Number of periods to look back (0=current, -1=previous).
1066
+ tz: Timezone to apply. If None, uses self._tz.
1067
+ naive: If True, return naive date without timezone info.
1068
+
1069
+ Returns:
1070
+ date: Date object representing the date portion of the timestamp.
1071
+
1072
+ Raises:
1073
+ IndexError: If the requested position is out of bounds for data feeds.
1074
+ """
1075
+ # CRITICAL FIX: date() calls datetime(), which should raise IndexError if out of range
1076
+ # This allows strategy to detect end of data for next_month calculation
1077
+ try:
1078
+ dt = self.datetime(ago, tz, naive)
1079
+ except IndexError:
1080
+ # This is the normal end-of-data signal. Preserve the exception
1081
+ # exactly and keep repeated protocol probes silent.
1082
+ raise
1083
+ if dt is None:
1084
+ return None
1085
+ try:
1086
+ return dt.date()
1087
+ except (AttributeError, ValueError):
1088
+ return None
1089
+
1090
+ def time(self, ago=0, tz=None, naive=True):
1091
+ """Get the time component of the datetime value at the specified offset.
1092
+
1093
+ Args:
1094
+ ago: Number of periods to look back (0=current, -1=previous).
1095
+ tz: Timezone to apply. If None, uses self._tz.
1096
+ naive: If True, return naive time without timezone info.
1097
+
1098
+ Returns:
1099
+ time: Time object representing the time portion of the timestamp.
1100
+ """
1101
+ dt = self.datetime(ago, tz, naive)
1102
+ if dt is None:
1103
+ return None
1104
+ try:
1105
+ return dt.time()
1106
+ except (AttributeError, ValueError):
1107
+ return None
1108
+
1109
+ def dt(self, ago=0):
1110
+ """Alias to avoid the extra chars in "datetime" for this field"""
1111
+ return self.datetime(ago)
1112
+
1113
+ def tm_raw(self, ago=0):
1114
+ """
1115
+ Returns a localtime/gmtime like time.struct_time object which is
1116
+ compatible with strftime formatting.
1117
+
1118
+ The time zone of the struct_time is naive
1119
+ """
1120
+ return self.datetime(ago, naive=False).timetuple()
1121
+
1122
+ def tm(self, ago=0):
1123
+ """
1124
+ Returns a localtime/gmtime like time.struct_time object which is
1125
+ compatible with strftime formatting.
1126
+
1127
+ The time zone of the struct_time is naive
1128
+ """
1129
+ return self.datetime(ago, naive=True).timetuple()
1130
+
1131
+ def tm_lt(self, other, ago=0):
1132
+ """
1133
+ Returns True if the time carried by this line's index "ago" is
1134
+ lower than the time carried by the "other" line
1135
+ """
1136
+ return self[ago] < other[0]
1137
+
1138
+ def tm_le(self, other, ago=0):
1139
+ """
1140
+ Returns True if the time carried by this line's index "ago" is
1141
+ lower than or equal to the time carried by the "other" line
1142
+ """
1143
+ return self[ago] <= other[0]
1144
+
1145
+ def tm_eq(self, other, ago=0):
1146
+ """
1147
+ Returns True if the time carried by this line's index "ago" is
1148
+ equal to the time carried by the "other" line
1149
+ """
1150
+ return self[ago] == other[0]
1151
+
1152
+ def tm_gt(self, other, ago=0):
1153
+ """
1154
+ Returns True if the time carried by this line's index "ago" is
1155
+ greater than the time carried by the "other" line
1156
+ """
1157
+ return self[ago] > other[0]
1158
+
1159
+ def tm_ge(self, other, ago=0):
1160
+ """
1161
+ Returns True if the time carried by this line's index "ago" is
1162
+ greater than or equal to the time carried by the "other" line
1163
+ """
1164
+ return self[ago] >= other[0]
1165
+
1166
+ def tm2dtime(self, tm, ago=0):
1167
+ """
1168
+ Returns the passed tm (time.struct_time) in a datetime using the
1169
+ timezone (if any) of the line
1170
+ """
1171
+ return datetime.datetime(*tm[:6])
1172
+
1173
+ def tm2datetime(self, tm, ago=0):
1174
+ """
1175
+ Returns the passed tm (time.struct_time) in a datetime using the
1176
+ timezone (if any) of the line
1177
+ """
1178
+ return datetime.datetime(*tm[:6])
1179
+
1180
+
1181
+ # LineActions cache for performance
1182
+ class LineActionsCache:
1183
+ """Cache system for LineActions to avoid repetitive calculations"""
1184
+
1185
+ _cache: dict = {}
1186
+ _cache_enabled = False
1187
+
1188
+ @classmethod
1189
+ def enable_cache(cls, enable=True):
1190
+ """Enable or disable the cache.
1191
+
1192
+ Args:
1193
+ enable: True to enable caching, False to disable.
1194
+ """
1195
+ cls._cache_enabled = enable
1196
+
1197
+ @classmethod
1198
+ def clear_cache(cls):
1199
+ """Clear all cached values."""
1200
+ cls._cache.clear()
1201
+
1202
+ @classmethod
1203
+ def get_cache_key(cls, *args):
1204
+ """Generate cache key from arguments"""
1205
+ return hash(tuple(id(arg) if hasattr(arg, "__hash__") else str(arg) for arg in args))
1206
+
1207
+
1208
+ class LineActionsMixin:
1209
+ """Mixin to provide LineActions functionality without metaclass"""
1210
+
1211
+ @classmethod
1212
+ def dopreinit(cls, _obj, *args, **kwargs):
1213
+ """Pre-initialization processing for LineActions"""
1214
+ # CRITICAL FIX: Set lines._owner BEFORE any user __init__ code runs
1215
+ # This is needed for line bindings like: self.lines.crossover = upcross - downcross
1216
+ if hasattr(_obj, "lines") and _obj.lines is not None:
1217
+ if not hasattr(_obj.lines, "_owner") or _obj.lines._owner is None:
1218
+ _obj.lines._owner = _obj
1219
+
1220
+ # Set up clock from explicit line arguments first, matching the
1221
+ # original LineActions metaclass semantics. This is critical for
1222
+ # operations built on secondary data feeds: the operation must follow
1223
+ # the line it was created from, not the strategy's primary data clock.
1224
+ _obj._clock = None
1225
+
1226
+ _obj._datas = [arg for arg in args if isinstance(arg, LineRoot)]
1227
+ if _obj._datas:
1228
+ data_clock = getattr(_obj._datas[0], "_clock", None)
1229
+ if data_clock is not None and data_clock.__class__.__name__ != "MinimalClock":
1230
+ _obj._clock = data_clock
1231
+ else:
1232
+ _obj._clock = _obj._datas[0]
1233
+
1234
+ if _obj._clock is None and hasattr(_obj, "_owner") and _obj._owner is not None:
1235
+ # Try to get clock from owner first
1236
+ if hasattr(_obj._owner, "_clock") and _obj._owner._clock is not None:
1237
+ _obj._clock = _obj._owner._clock
1238
+ # If owner has datas, use the first data as clock
1239
+ elif hasattr(_obj._owner, "datas") and _obj._owner.datas:
1240
+ _obj._clock = _obj._owner.datas[0]
1241
+ # If owner has data attribute, use it as clock
1242
+ elif hasattr(_obj._owner, "data") and _obj._owner.data is not None:
1243
+ _obj._clock = _obj._owner.data
1244
+ # Try the owner itself as clock if it has __len__
1245
+ elif hasattr(_obj._owner, "__len__"):
1246
+ _obj._clock = _obj._owner
1247
+
1248
+ # If still no clock found and we have datas, use the first data
1249
+ if _obj._clock is None and hasattr(_obj, "datas") and _obj.datas:
1250
+ _obj._clock = _obj.datas[0]
1251
+
1252
+ # CRITICAL FIX: Only initialize minperiod to 1 if not already set from data sources
1253
+ # The _minperiod might have been set in __new__ from data sources for nested indicators
1254
+ # (e.g., EMA applied to another indicator's output)
1255
+ if not hasattr(_obj, "_minperiod") or _obj._minperiod is None:
1256
+ _obj._minperiod = 1
1257
+
1258
+ # CRITICAL FIX: Calculate minperiod from args (like original metaclass did)
1259
+ # This ensures that indicators applied to other indicators inherit their minperiod
1260
+ from .lineroot import LineMultiple, LineSingle
1261
+
1262
+ _minperiods = []
1263
+ # Collect minperiods from LineSingle args
1264
+ for arg in args:
1265
+ if isinstance(arg, LineSingle):
1266
+ _minperiods.append(getattr(arg, "_minperiod", 1))
1267
+
1268
+ # Collect minperiods from LineMultiple args (get their first line)
1269
+ for arg in args:
1270
+ if isinstance(arg, LineMultiple) and hasattr(arg, "lines") and arg.lines:
1271
+ try:
1272
+ first_line = arg.lines[0]
1273
+ _minperiods.append(getattr(first_line, "_minperiod", 1))
1274
+ except (IndexError, TypeError):
1275
+ # Empty/non-indexable lines container; skip this arg.
1276
+ pass
1277
+
1278
+ # Update minperiod with max from args
1279
+ if _minperiods:
1280
+ _minperiod = max(_minperiods)
1281
+ _obj.updateminperiod(_minperiod)
1282
+
1283
+ return _obj, args, kwargs
1284
+
1285
+ @classmethod
1286
+ def dopostinit(cls, _obj, *args, **kwargs):
1287
+ """Post-initialization processing for LineActions"""
1288
+ # NOTE: Indicator registration is now handled in lineiterator.py dopostinit
1289
+ # with proper duplicate checking. No registration needed here.
1290
+
1291
+
1292
+ class PseudoArray:
1293
+ """Wrapper for non-array iterables to provide array-like access.
1294
+
1295
+ This class wraps iterables (including itertools.repeat) and provides
1296
+ array-like indexing access. It handles cases where the wrapped object
1297
+ doesn't support direct indexing.
1298
+
1299
+ Attributes:
1300
+ wrapped: The wrapped iterable object.
1301
+ _minperiod: Minimum period inherited from the wrapped object.
1302
+
1303
+ Example:
1304
+ >>> from itertools import repeat
1305
+ >>> pseudo = PseudoArray(repeat(1.0))
1306
+ >>> print(pseudo[0])
1307
+ 1.0
1308
+ """
1309
+
1310
+ def __init__(self, wrapped):
1311
+ """Initialize PseudoArray with a wrapped iterable.
1312
+
1313
+ Args:
1314
+ wrapped: The iterable object to wrap.
1315
+ """
1316
+ self.wrapped = wrapped
1317
+ # CRITICAL FIX: Ensure PseudoArray has _minperiod attribute
1318
+ self._minperiod = getattr(wrapped, "_minperiod", 1)
1319
+
1320
+ def __getitem__(self, key):
1321
+ try:
1322
+ # Try normal indexing first
1323
+ return self.wrapped[key]
1324
+ except (TypeError, IndexError, AttributeError):
1325
+ # Handle itertools.repeat objects and other iterables that don't support indexing
1326
+ if hasattr(self.wrapped, "__iter__"):
1327
+ # For repeat objects, all values are the same, so just get the first one
1328
+ try:
1329
+ # Convert to list if it's a repeat object
1330
+ if str(type(self.wrapped)) == "<class 'itertools.repeat'>":
1331
+ # For repeat, all values are the same
1332
+ return next(iter(self.wrapped))
1333
+ # Convert iterable to list and index
1334
+ wrapped_list = list(self.wrapped)
1335
+ return wrapped_list[key]
1336
+ except (StopIteration, IndexError):
1337
+ return float("nan")
1338
+ else:
1339
+ # If not iterable, return the wrapped object itself for index 0
1340
+ if key == 0:
1341
+ return self.wrapped
1342
+ return float("nan")
1343
+
1344
+ @property
1345
+ def array(self):
1346
+ """Get the array representation of the wrapped object.
1347
+
1348
+ Returns:
1349
+ list or array: Array representation of the wrapped object.
1350
+ """
1351
+ # Handle repeat objects specially
1352
+ if str(type(self.wrapped)) == "<class 'itertools.repeat'>":
1353
+ # For repeat objects, return a list with one element repeated
1354
+ return [next(iter(self.wrapped))]
1355
+ if hasattr(self.wrapped, "array"):
1356
+ return self.wrapped.array
1357
+ if not hasattr(self.wrapped, "__iter__"):
1358
+ return []
1359
+ return self.wrapped
1360
+
1361
+
1362
+ class LineActions(LineBuffer, LineActionsMixin, metabase.ParamsMixin):
1363
+ """
1364
+ Base class for *Line Clases* with different lines, derived from a
1365
+ LineBuffer
1366
+ """
1367
+
1368
+ _ltype = LineRoot.IndType
1369
+
1370
+ # Add plotlines attribute for plotting support
1371
+ plotlines = object()
1372
+
1373
+ def __new__(cls, *args, **kwargs):
1374
+ """Handle data processing for indicators and other LineActions objects"""
1375
+
1376
+ # Create the instance using the normal Python object creation
1377
+ instance = super().__new__(cls)
1378
+
1379
+ # Initialize basic attributes
1380
+ import collections
1381
+
1382
+ instance._lineiterators = collections.defaultdict(list)
1383
+ instance._lineaction_init_args = args
1384
+
1385
+ # CRITICAL FIX: Define mindatas before using it
1386
+ mindatas = getattr(cls, "_mindatas", getattr(cls, "mindatas", 1))
1387
+
1388
+ # Set up parameters for this instance (needed for self.p.period etc.)
1389
+ if hasattr(cls, "_params") and cls._params is not None:
1390
+ params_cls = cls._params
1391
+ # Create parameter instance for this object
1392
+ instance.p = params_cls()
1393
+ # Update with kwargs
1394
+ for key, value in kwargs.items():
1395
+ if hasattr(instance.p, key):
1396
+ setattr(instance.p, key, value)
1397
+ else:
1398
+ # Fallback to empty parameter object
1399
+ from .utils import DotDict
1400
+
1401
+ instance.p = DotDict(**kwargs)
1402
+
1403
+ # Create and set up Lines instance
1404
+ lines_cls = getattr(cls, "lines", None)
1405
+ if lines_cls is not None:
1406
+ instance.lines = lines_cls()
1407
+ # CRITICAL FIX: Set lines._owner immediately after creating lines instance
1408
+ # Use object.__setattr__ to directly set _owner_ref (bypasses Lines.__setattr__)
1409
+ object.__setattr__(instance.lines, "_owner_ref", instance)
1410
+ # Ensure lines are properly initialized with their own buffers
1411
+ if hasattr(instance.lines, "_obj"):
1412
+ instance.lines._obj = instance
1413
+
1414
+ # CRITICAL FIX: Ensure lines instance has the essential methods
1415
+ # If the lines instance doesn't have advance method, add it
1416
+ if not hasattr(instance.lines, "advance"):
1417
+
1418
+ def advance_method(size=1):
1419
+ """Forward all lines in the collection"""
1420
+ for line in getattr(instance.lines, "lines", []):
1421
+ if hasattr(line, "advance"):
1422
+ line.advance(size=size)
1423
+
1424
+ instance.lines.advance = advance_method
1425
+
1426
+ # CRITICAL FIX: Set up line references for indicators
1427
+ # Each line should be a separate LineBuffer with its own array
1428
+ if hasattr(instance.lines, "lines") and instance.lines.lines:
1429
+ # Ensure each line is a LineBuffer with its own array
1430
+ for i, line_obj in enumerate(instance.lines.lines):
1431
+ if not isinstance(line_obj, LineBuffer):
1432
+ # Create a new LineBuffer for this line - no import needed, we're in linebuffer.py
1433
+ new_line = LineBuffer()
1434
+ # Copy any existing attributes
1435
+ if hasattr(line_obj, "__dict__"):
1436
+ new_line.__dict__.update(line_obj.__dict__)
1437
+ instance.lines.lines[i] = new_line
1438
+ line_obj = new_line
1439
+
1440
+ # Ensure the line has its own array
1441
+ if not hasattr(line_obj, "array") or not line_obj.array:
1442
+ import array
1443
+
1444
+ line_obj.array = array.array("d")
1445
+ line_obj._idx = -1
1446
+ line_obj.lencount = 0
1447
+
1448
+ line_obj._refresh_cached_line_flags(
1449
+ owner=instance.lines,
1450
+ ltype=getattr(instance, "_ltype", cls._ltype),
1451
+ )
1452
+
1453
+ # Set up convenience references - first line as .line
1454
+ instance.line = instance.lines.lines[0] if instance.lines.lines else instance
1455
+ instance.l = instance.lines # Common shorthand
1456
+ else:
1457
+ # No individual lines, use the instance itself
1458
+ instance.line = instance
1459
+ instance.l = instance.lines
1460
+ else:
1461
+ # Create default lines using the proper Lines class
1462
+ from .lineseries import Lines
1463
+
1464
+ instance.lines = Lines()
1465
+ # Add the advance method if it doesn't exist
1466
+ if not hasattr(instance.lines, "advance"):
1467
+
1468
+ def advance_method(size=1):
1469
+ """Forward all lines in the collection"""
1470
+ for line in getattr(instance.lines, "lines", []):
1471
+ if hasattr(line, "advance"):
1472
+ line.advance(size=size)
1473
+
1474
+ instance.lines.advance = advance_method
1475
+ instance.line = instance
1476
+ instance.l = instance.lines
1477
+
1478
+ # CRITICAL FIX: Auto-assign data from owner if no data provided and mindatas > 0
1479
+ if mindatas > 0:
1480
+ # Try to get owner and auto-assign data using multiple strategies
1481
+ from . import metabase
1482
+
1483
+ owner = None
1484
+
1485
+ # Strategy 1: Use nearest LineIterator owner. Falling back directly
1486
+ # to Strategy can skip an enclosing indicator and bind expression
1487
+ # clocks to the primary strategy data.
1488
+ try:
1489
+ from .lineiterator import LineIterator
1490
+ except ImportError:
1491
+ LineIterator = None
1492
+
1493
+ if LineIterator is not None:
1494
+ owner = metabase.findowner(instance, LineIterator)
1495
+
1496
+ # Strategy 2: Use findowner for Strategy
1497
+ try:
1498
+ from .strategy import Strategy
1499
+ except ImportError:
1500
+ Strategy = None
1501
+
1502
+ if owner is None and Strategy is not None:
1503
+ owner = metabase.findowner(instance, Strategy)
1504
+
1505
+ # If we found an owner with data, auto-assign it
1506
+ if owner is not None and hasattr(owner, "data") and owner.data is not None:
1507
+ # Check if we already have data in args
1508
+ data_count = 0
1509
+ for arg in args:
1510
+ if (
1511
+ isinstance(arg, LineRoot)
1512
+ or hasattr(arg, "lines")
1513
+ or hasattr(arg, "_name")
1514
+ or str(type(arg).__name__).endswith("Data")
1515
+ ):
1516
+ data_count += 1
1517
+
1518
+ # If we need more data sources than we have, auto-assign from owner
1519
+ if data_count < mindatas:
1520
+ # Add owner's data as needed
1521
+ missing_data_count = mindatas - data_count
1522
+ for _ in range(missing_data_count):
1523
+ args = (owner.data,) + args
1524
+
1525
+ # Process arguments to identify data sources
1526
+ data_count = 0
1527
+ processed_datas = []
1528
+
1529
+ for i, arg in enumerate(args):
1530
+ if (
1531
+ isinstance(arg, LineRoot)
1532
+ or hasattr(arg, "lines")
1533
+ or hasattr(arg, "_name")
1534
+ or str(type(arg).__name__).endswith("Data")
1535
+ ):
1536
+ processed_datas.append(arg)
1537
+ data_count += 1
1538
+ if data_count >= mindatas:
1539
+ break
1540
+
1541
+ instance.datas = processed_datas
1542
+
1543
+ if processed_datas:
1544
+ instance.data = processed_datas[0]
1545
+ else:
1546
+ instance.data = None
1547
+
1548
+ # Set up dnames if available
1549
+ try:
1550
+ from .utils import DotDict
1551
+
1552
+ instance.dnames = DotDict(
1553
+ [(d._name, d) for d in instance.datas if getattr(d, "_name", "")]
1554
+ )
1555
+ except Exception:
1556
+ throttled_warning(
1557
+ logger,
1558
+ "linebuffer.lineactions.dnames_recovery",
1559
+ "LineActions data-name setup failed; using empty names",
1560
+ exc_info=False,
1561
+ )
1562
+ instance.dnames = {}
1563
+
1564
+ return instance
1565
+
1566
+ def __init__(self, *args, **kwargs):
1567
+ """Initialize LineActions instance.
1568
+
1569
+ Sets up lines, owner references, data sources, and clock.
1570
+ This is a complex initialization that handles multiple scenarios
1571
+ including indicators, strategies, and data feeds.
1572
+
1573
+ Args:
1574
+ *args: Positional arguments including data feeds.
1575
+ **kwargs: Keyword arguments for parameters.
1576
+ """
1577
+ # CRITICAL FIX: Set lines._owner FIRST, before any other initialization
1578
+ # This ensures line bindings in user's __init__ can find the owner
1579
+ if hasattr(self, "lines") and self.lines is not None:
1580
+ # If lines is still a class, create an instance first
1581
+ if isinstance(self.lines, type):
1582
+ self.lines = self.lines()
1583
+ # Now set owner using object.__setattr__ to directly set _owner_ref
1584
+ if self.lines is not None:
1585
+ object.__setattr__(self.lines, "_owner_ref", self)
1586
+
1587
+ # Set up _owner from call stack BEFORE calling dopreinit
1588
+ from . import metabase
1589
+
1590
+ # Try to find any LineIterator-like owner
1591
+ # Try findowner first with different classes
1592
+ self._owner = None
1593
+
1594
+ # First try to find the nearest LineIterator. For LineActions created
1595
+ # inside an indicator this keeps ownership/clock fallback local to that
1596
+ # indicator instead of jumping to the enclosing strategy.
1597
+ try:
1598
+ from .lineiterator import LineIterator
1599
+
1600
+ self._owner = metabase.findowner(self, LineIterator)
1601
+ except Exception:
1602
+ throttled_warning(
1603
+ logger,
1604
+ "linebuffer.lineactions.lineiterator_owner_recovery",
1605
+ "LineActions LineIterator owner lookup failed; continuing owner resolution",
1606
+ exc_info=False,
1607
+ )
1608
+
1609
+ # If no LineIterator found, try Strategy specifically
1610
+ try:
1611
+ from .strategy import Strategy
1612
+
1613
+ if self._owner is None:
1614
+ self._owner = metabase.findowner(self, Strategy)
1615
+ except Exception:
1616
+ throttled_warning(
1617
+ logger,
1618
+ "linebuffer.lineactions.strategy_owner_recovery",
1619
+ "LineActions Strategy owner lookup failed; continuing owner resolution",
1620
+ exc_info=False,
1621
+ )
1622
+
1623
+ # If still no owner, try a broader search
1624
+ # findowner() uses OwnerContext for owner lookup
1625
+ if self._owner is None:
1626
+ self._owner = metabase.findowner(self, None)
1627
+
1628
+ init_args = args or getattr(self, "_lineaction_init_args", ())
1629
+
1630
+ # Call pre-init
1631
+ self.__class__.dopreinit(self, *init_args, **kwargs)
1632
+
1633
+ # Call parent init
1634
+ super().__init__()
1635
+
1636
+ # LineBuffer.__init__ initializes low-level buffer fields and resets
1637
+ # _clock/_owner metadata. Re-apply the LineActions pre-init metadata
1638
+ # afterwards so explicit line operands keep their own data clock.
1639
+ self.__class__.dopreinit(self, *init_args, **kwargs)
1640
+
1641
+ self._refresh_cached_line_flags(
1642
+ owner=getattr(self, "_owner", None),
1643
+ ltype=getattr(self.__class__, "_ltype", LineRoot.IndType),
1644
+ )
1645
+
1646
+ if hasattr(self, "lines") and hasattr(self.lines, "lines"):
1647
+ for line_obj in self.lines.lines:
1648
+ if hasattr(line_obj, "_refresh_cached_line_flags"):
1649
+ line_obj._refresh_cached_line_flags(
1650
+ owner=self.lines,
1651
+ ltype=getattr(self, "_ltype", LineRoot.IndType),
1652
+ )
1653
+
1654
+ # Call post-init
1655
+ self.__class__.dopostinit(self, *args, **kwargs)
1656
+
1657
+ def getindicators(self):
1658
+ """Get list of indicators using this line actions object.
1659
+
1660
+ Returns:
1661
+ list: Empty list for base LineActions (override in subclasses).
1662
+ """
1663
+ return []
1664
+
1665
+ def qbuffer(self, savemem=0):
1666
+ """Enable queued buffer mode for memory-efficient storage.
1667
+
1668
+ Args:
1669
+ savemem: Memory saving mode (0=normal, >0=enable cache mode).
1670
+ """
1671
+ super().qbuffer(savemem=1)
1672
+
1673
+ def plotlabel(self):
1674
+ """Return the plot label for this line object"""
1675
+ # Try to get plot label from _plotlabel method
1676
+ if hasattr(self, "_plotlabel"):
1677
+ label_dict = self._plotlabel()
1678
+ # Convert dict to string format
1679
+ if isinstance(label_dict, dict):
1680
+ # Format as 'ClassName(param1=value1, param2=value2)'
1681
+ params_str = ", ".join(f"{k}={v}" for k, v in label_dict.items())
1682
+ if params_str:
1683
+ return f"{self.__class__.__name__}({params_str})"
1684
+ return self.__class__.__name__
1685
+ return str(label_dict)
1686
+ # Fallback: return class name
1687
+ return self.__class__.__name__
1688
+
1689
+ def _plotlabel(self):
1690
+ """Default implementation of plot label"""
1691
+ # Try to get params if available
1692
+ if hasattr(self, "params") and hasattr(self.params, "_getkwargs"):
1693
+ return self.params._getkwargs()
1694
+ # Otherwise return empty dict
1695
+ return {}
1696
+
1697
+ @staticmethod
1698
+ def arrayize(obj):
1699
+ """Convert an object to an array-compatible object.
1700
+
1701
+ Args:
1702
+ obj: Object to convert. Can be a value, iterable, or array-like.
1703
+
1704
+ Returns:
1705
+ The original object if it has an array attribute,
1706
+ otherwise a LineNum or PseudoArray wrapper.
1707
+ """
1708
+ if not hasattr(obj, "array"):
1709
+ if not hasattr(obj, "__getitem__"):
1710
+ # CRITICAL FIX: Create a LineNum that properly handles _minperiod
1711
+ line_num = LineNum(obj)
1712
+ # Ensure the LineNum has the _minperiod attribute
1713
+ if not hasattr(line_num, "_minperiod"):
1714
+ line_num._minperiod = 1
1715
+ return line_num # make it a LineNum
1716
+ if not hasattr(obj, "__len__"):
1717
+ pseudo_array = PseudoArray(obj)
1718
+ # CRITICAL FIX: Ensure PseudoArray objects have _minperiod for compatibility
1719
+ if not hasattr(pseudo_array, "_minperiod"):
1720
+ pseudo_array._minperiod = 1
1721
+ return pseudo_array # Can iterate (for once)
1722
+
1723
+ return obj
1724
+
1725
+ def _next_old(self):
1726
+ """DEPRECATED: This method is no longer used. LineIterator._next() is used instead."""
1727
+ # CRITICAL FIX: Prevent double processing if _once was already called
1728
+ if hasattr(self, "_once_called") and self._once_called:
1729
+ return # Already processed in once mode, don't process again
1730
+
1731
+ # CRITICAL FIX: Ensure data synchronization without over-advancing
1732
+ if hasattr(self, "_clock") and self._clock is not None:
1733
+ try:
1734
+ clock_len = len(self._clock)
1735
+ self_len = len(self)
1736
+
1737
+ # Only advance if we're behind the clock and not already at or ahead
1738
+ if self_len < clock_len and (clock_len - self_len) <= 1:
1739
+ # Forward one step to match the clock
1740
+ self.forward()
1741
+ except Exception:
1742
+ # Clock access changes the recovery path: keep advancing once,
1743
+ # but make recurring broken clocks visible without a log storm.
1744
+ throttled_warning(
1745
+ logger,
1746
+ "linebuffer.lineactions.next_old.clock_failure",
1747
+ "LineActions clock access failed; forcing one forward step",
1748
+ exc_info=False,
1749
+ )
1750
+ self.forward()
1751
+ else:
1752
+ # No clock, just forward once
1753
+ self.forward()
1754
+
1755
+ # Call prenext or nextstart/next depending on minperiod
1756
+ if len(self) < self._minperiod:
1757
+ self.prenext()
1758
+ elif len(self) == self._minperiod:
1759
+ self.nextstart() # called once for the 1st value over minperiod
1760
+ else:
1761
+ self.next() # called for each value over minperiod
1762
+
1763
+ def _once(self, start, end):
1764
+ # Mark that once was called to prevent double processing in _next
1765
+ self._once_called = True
1766
+
1767
+ # CRITICAL FIX: Ensure array exists but don't pre-fill it
1768
+ # Pre-filling causes incorrect buflen() calculations
1769
+ if not hasattr(self, "array") or self.array is None:
1770
+ import array as array_module
1771
+
1772
+ self.array = array_module.array("d")
1773
+
1774
+ # CRITICAL FIX: Ensure proper range for once processing
1775
+ if start < 0:
1776
+ start = 0
1777
+ if end < start:
1778
+ end = start
1779
+
1780
+ # CRITICAL FIX: Get the actual buffer length if available
1781
+ # Skip this check if _clock is MinimalClock (always returns 0)
1782
+ if hasattr(self, "_clock") and self._clock and hasattr(self._clock, "buflen"):
1783
+ clock_class_name = getattr(self._clock, "__class__", type(None)).__name__
1784
+ if "MinimalClock" not in clock_class_name:
1785
+ try:
1786
+ max_len = self._clock.buflen()
1787
+ except Exception:
1788
+ throttled_error(
1789
+ logger,
1790
+ "linebuffer.lineactions.once.clock_preflight_failure",
1791
+ "LineActions clock buffer length lookup failed; propagating exception",
1792
+ exc_info=False,
1793
+ )
1794
+ raise
1795
+ if max_len > 0 and end > max_len:
1796
+ end = max_len
1797
+
1798
+ # CRITICAL FIX: Call _once() on all child line iterators first
1799
+ # This ensures dependencies are calculated before this indicator
1800
+ if hasattr(self, "_lineiterators"):
1801
+ from .lineiterator import LineIterator
1802
+
1803
+ for indicator in self._lineiterators.get(LineIterator.IndType, []):
1804
+ try:
1805
+ if hasattr(indicator, "_once"):
1806
+ indicator._once(start, end)
1807
+ except Exception:
1808
+ # A child owns data consumed by this action's batch hook.
1809
+ # Continuing would fabricate a partial result with no
1810
+ # defined recovery value, so make the dependency failure
1811
+ # visible and preserve the original exception.
1812
+ throttled_error(
1813
+ logger,
1814
+ "linebuffer.lineactions.once.child_failure",
1815
+ "LineActions child batch computation failed; propagating exception",
1816
+ exc_info=False,
1817
+ )
1818
+ raise
1819
+
1820
+ # CRITICAL FIX: Call preonce before main processing
1821
+ try:
1822
+ if hasattr(self, "preonce"):
1823
+ self.preonce(start, end)
1824
+ except Exception:
1825
+ # A custom preonce hook can alter indicator state. Continuing
1826
+ # would fabricate an incomplete runonce result, so surface it.
1827
+ throttled_error(
1828
+ logger,
1829
+ "linebuffer.lineactions.once.preonce_failure",
1830
+ "LineActions preonce hook failed; propagating exception",
1831
+ exc_info=False,
1832
+ )
1833
+ raise
1834
+
1835
+ # CRITICAL FIX: Ensure operand arrays are computed before once()
1836
+ # For Logic subclasses (bt.If, bt.And, etc.), operands (args, cond) need
1837
+ # their arrays populated before once() reads from them.
1838
+ if hasattr(self, "args"):
1839
+ for arg in self.args:
1840
+ if hasattr(arg, "once") and hasattr(arg, "array") and len(arg.array) < end:
1841
+ try:
1842
+ arg.once(0, end)
1843
+ except Exception: # nosec B110
1844
+ # This operand is shorter than the requested batch and
1845
+ # the parent once() may read it. There is no safe
1846
+ # generic fallback, so do not continue with stale data.
1847
+ throttled_error(
1848
+ logger,
1849
+ "linebuffer.lineactions.once.argument_failure",
1850
+ "LineActions operand batch computation failed; propagating exception",
1851
+ exc_info=False,
1852
+ )
1853
+ raise
1854
+ if hasattr(self, "cond"):
1855
+ cond = self.cond
1856
+ if hasattr(cond, "once") and hasattr(cond, "array") and len(cond.array) < end:
1857
+ try:
1858
+ cond.once(0, end)
1859
+ except Exception: # nosec B110
1860
+ # A Logic condition controls which operand is read. A
1861
+ # failed batch update leaves it stale, so preserving the
1862
+ # exception is safer than selecting with invalid state.
1863
+ throttled_error(
1864
+ logger,
1865
+ "linebuffer.lineactions.once.condition_failure",
1866
+ "LineActions condition batch computation failed; propagating exception",
1867
+ exc_info=False,
1868
+ )
1869
+ raise
1870
+
1871
+ # CRITICAL FIX: Process the main once calculation
1872
+ # Try to call once method if it exists
1873
+ try:
1874
+ if hasattr(self, "once") and callable(self.once):
1875
+ self.once(start, end)
1876
+ except Exception:
1877
+ # A custom once hook owns the batch result. Do not silently accept
1878
+ # an incomplete array when it fails.
1879
+ throttled_error(
1880
+ logger,
1881
+ "linebuffer.lineactions.once.main_failure",
1882
+ "LineActions once hook failed; propagating exception",
1883
+ exc_info=False,
1884
+ )
1885
+ raise
1886
+
1887
+ # CRITICAL FIX: Update lencount after once processing to match the data length
1888
+ # In runonce mode, lencount should equal the number of data points processed
1889
+ # Get the actual data length from the clock or data source
1890
+ actual_data_len = end
1891
+ try:
1892
+ # Try to get the actual data length from clock or data sources
1893
+ if hasattr(self, "_clock") and self._clock:
1894
+ try:
1895
+ actual_data_len = self._clock.buflen()
1896
+ except Exception:
1897
+ throttled_warning(
1898
+ logger,
1899
+ "linebuffer.lineactions.once.clock_length_recovery",
1900
+ "LineActions clock length lookup failed; using fallback length",
1901
+ exc_info=False,
1902
+ )
1903
+ try:
1904
+ actual_data_len = len(self._clock)
1905
+ except Exception:
1906
+ throttled_warning(
1907
+ logger,
1908
+ "linebuffer.lineactions.once.clock_length_recovery",
1909
+ "LineActions clock length lookup failed; using fallback length",
1910
+ exc_info=False,
1911
+ )
1912
+ elif hasattr(self, "datas") and self.datas and len(self.datas) > 0:
1913
+ try:
1914
+ actual_data_len = self.datas[0].buflen()
1915
+ except Exception:
1916
+ throttled_warning(
1917
+ logger,
1918
+ "linebuffer.lineactions.once.data_length_recovery",
1919
+ "LineActions data length lookup failed; using fallback length",
1920
+ exc_info=False,
1921
+ )
1922
+ try:
1923
+ actual_data_len = len(self.datas[0])
1924
+ except Exception:
1925
+ throttled_warning(
1926
+ logger,
1927
+ "linebuffer.lineactions.once.data_length_recovery",
1928
+ "LineActions data length lookup failed; using fallback length",
1929
+ exc_info=False,
1930
+ )
1931
+ # Use the maximum of end and actual_data_len to ensure we don't truncate
1932
+ final_len = max(end, actual_data_len) if actual_data_len > 0 else end
1933
+ except Exception:
1934
+ throttled_warning(
1935
+ logger,
1936
+ "linebuffer.lineactions.once.length_recovery",
1937
+ "LineActions batch length recovery failed; using requested range",
1938
+ exc_info=False,
1939
+ )
1940
+ final_len = end
1941
+
1942
+ if hasattr(self, "lines") and hasattr(self.lines, "lines") and self.lines.lines:
1943
+ # Update lencount for all lines to match the data length
1944
+ for line in self.lines.lines:
1945
+ if hasattr(line, "lencount"):
1946
+ # CRITICAL FIX: Set lencount to final_len (actual data length)
1947
+ # This ensures len(indicator) == len(strategy) in runonce mode
1948
+ line.lencount = final_len
1949
+ if hasattr(line, "_idx"):
1950
+ # Set _idx to the last processed position
1951
+ line._idx = final_len - 1 if final_len > 0 else -1
1952
+
1953
+ # CRITICAL FIX: Call oncebinding to propagate computed values to bound lines
1954
+ # This is needed for bt.If, Logic subclasses etc. that compute values into
1955
+ # their own array and need to copy them to the bound output line.
1956
+ self.oncebinding()
1957
+
1958
+ @classmethod
1959
+ def cleancache(cls):
1960
+ """Clean the cache - called by cerebro"""
1961
+ LineActionsCache.clear_cache()
1962
+
1963
+ @classmethod
1964
+ def usecache(cls, enable=True):
1965
+ """Enable or disable the cache"""
1966
+ LineActionsCache.enable_cache(enable)
1967
+
1968
+
1969
+ def LineDelay(a, ago=0, **kwargs):
1970
+ """Create a delayed line object.
1971
+
1972
+ Args:
1973
+ a: Source line object.
1974
+ ago: Number of periods to delay. Negative for lookback.
1975
+ **kwargs: Additional keyword arguments.
1976
+
1977
+ Returns:
1978
+ _LineDelay or _LineForward: A delayed line object.
1979
+ """
1980
+ if ago <= 0:
1981
+ return _LineDelay(a, ago, **kwargs)
1982
+
1983
+ return _LineForward(a, ago)
1984
+
1985
+
1986
+ def LineNum(num):
1987
+ """Create a constant line from a number.
1988
+
1989
+ Args:
1990
+ num: The constant value.
1991
+
1992
+ Returns:
1993
+ _LineDelay: A line object that always returns the constant value.
1994
+ """
1995
+ return _LineDelay(PseudoArray(repeat(num)), 0)
1996
+
1997
+
1998
+ class _LineDelay(LineActions):
1999
+ """Delayed line object for negative ago values (lookback).
2000
+
2001
+ This class represents a line that accesses historical values
2002
+ from another line. For example, data(-1) returns the
2003
+ previous bar's value.
2004
+
2005
+ Attributes:
2006
+ a: The source line object.
2007
+ ago: Number of periods to look back (negative value).
2008
+ """
2009
+
2010
+ def __init__(self, a, ago):
2011
+ """Initialize the delayed line.
2012
+
2013
+ Args:
2014
+ a: Source line object.
2015
+ ago: Number of periods to look back (negative value).
2016
+ """
2017
+ super().__init__()
2018
+ self.a = self.arrayize(a)
2019
+ self.ago = ago
2020
+
2021
+ # CRITICAL FIX: Inherit minperiod from source's owner (indicator) if available
2022
+ # When called as nzd(-1), 'a' is nzd.lines[0] which has minperiod=1,
2023
+ # but the indicator nzd has minperiod=20. We need to use the indicator's minperiod.
2024
+ source_minperiod = getattr(a, "_minperiod", 1)
2025
+
2026
+ # Check if source has an owner with a higher minperiod
2027
+ if hasattr(a, "_owner") and a._owner is not None:
2028
+ owner = a._owner
2029
+ # Check for _owner_ref (Lines object pointing to indicator)
2030
+ if hasattr(owner, "_owner_ref") and owner._owner_ref is not None:
2031
+ owner_minperiod = getattr(owner._owner_ref, "_minperiod", 1)
2032
+ source_minperiod = max(source_minperiod, owner_minperiod)
2033
+ else:
2034
+ owner_minperiod = getattr(owner, "_minperiod", 1)
2035
+ source_minperiod = max(source_minperiod, owner_minperiod)
2036
+
2037
+ # Update our minperiod with the source's minperiod
2038
+ if source_minperiod > 1:
2039
+ self.updateminperiod(source_minperiod)
2040
+
2041
+ # Need to add the delay to the period. "ago" is 0 based and therefore
2042
+ # we need to pass an extra 1 which is the minimum defined period for
2043
+ # any data (which will be subtracted inside addminperiod)
2044
+ # CRITICAL FIX: Must add abs(ago) + 1, NOT just abs(ago)
2045
+ self.addminperiod(abs(ago) + 1)
2046
+
2047
+ def __getitem__(self, idx):
2048
+ """CRITICAL FIX: Override __getitem__ to compute delayed value dynamically.
2049
+
2050
+ This handles constants wrapped in PseudoArray correctly.
2051
+ For ago=-10 (lookback), accessing [0] should return self.a[-10] (10 bars back).
2052
+ Formula: self.a[idx + ago] where ago is negative for lookback.
2053
+ """
2054
+ try:
2055
+ # For delay operations, get value from source with delay applied
2056
+ # ago is negative for lookback, so idx + ago gives historical index
2057
+ value = self.a[idx + self.ago]
2058
+ if value is None:
2059
+ return 0.0
2060
+ if isinstance(value, float) and (value in (INF, NEG_INF) or value != value):
2061
+ return 0.0
2062
+ return value
2063
+ except (IndexError, TypeError):
2064
+ return 0.0
2065
+
2066
+ def next(self):
2067
+ """Calculate and set the delayed value for the current bar.
2068
+
2069
+ Gets the value from the source line at the delayed position
2070
+ and stores it at position 0.
2071
+ """
2072
+ # CRITICAL FIX: Proper delay operation
2073
+ # ago is negative for lookback (e.g., ago=-10 means 10 bars back)
2074
+ # We need self.a[ago] to get the historical value
2075
+ try:
2076
+ # Get the delayed value - ago is already negative for lookback
2077
+ delayed_val = self.a[self.ago]
2078
+
2079
+ # Ensure value is never None or NaN
2080
+ if (
2081
+ delayed_val is None
2082
+ or isinstance(delayed_val, float)
2083
+ and (delayed_val in (INF, NEG_INF) or delayed_val != delayed_val)
2084
+ ):
2085
+ delayed_val = 0.0
2086
+
2087
+ self[0] = delayed_val
2088
+ except (IndexError, AttributeError):
2089
+ # If we can't get the delayed value, use 0.0
2090
+ self[0] = 0.0
2091
+
2092
+ def once(self, start, end):
2093
+ """Calculate delayed values in batch mode (runonce).
2094
+
2095
+ Args:
2096
+ start: Starting index.
2097
+ end: Ending index.
2098
+ """
2099
+ # cache python dictionary lookups
2100
+ dst = self.array
2101
+ ago = self.ago
2102
+
2103
+ # Ensure destination array is properly sized. Missing delayed values must
2104
+ # remain NaN; using 0.0 would turn unavailable bars into real signals.
2105
+ while len(dst) < end:
2106
+ dst.append(float("nan"))
2107
+
2108
+ # CRITICAL FIX: Ensure source has computed its values before we access them
2109
+ # This is necessary for LinesOperation sources that haven't run once() yet
2110
+ if hasattr(self.a, "once") and hasattr(self.a, "array") and len(self.a.array) < end:
2111
+ self.a.once(start, end)
2112
+
2113
+ # CRITICAL FIX: Check if source is a constant value (PseudoArray with repeat)
2114
+ # We need to check the wrapped object, not just the array, because
2115
+ # PseudoArray.array returns a new list each time
2116
+ is_constant = False
2117
+ constant_value = None
2118
+
2119
+ # Check if self.a is a PseudoArray wrapping a repeat object
2120
+ # OR if self.a is a _LineDelay that wraps a PseudoArray with repeat
2121
+ source_obj = self.a
2122
+ if hasattr(self.a, "a"):
2123
+ # self.a is a _LineDelay, check its source
2124
+ source_obj = self.a.a
2125
+
2126
+ if hasattr(source_obj, "wrapped"):
2127
+ wrapped = source_obj.wrapped
2128
+ # Check if it's a repeat object
2129
+ if (
2130
+ isinstance(wrapped, itertools.repeat)
2131
+ or str(type(wrapped)) == "<class 'itertools.repeat'>"
2132
+ ):
2133
+ is_constant = True
2134
+ try:
2135
+ # Get the constant value from the repeat object
2136
+ # Create a new iterator to avoid consuming it
2137
+ constant_value = next(iter(wrapped))
2138
+ if constant_value is None:
2139
+ constant_value = float("nan")
2140
+ except (StopIteration, TypeError):
2141
+ constant_value = float("nan")
2142
+
2143
+ # If not a constant, get the source array
2144
+ if not is_constant:
2145
+ src = self.a.array
2146
+
2147
+ if is_constant:
2148
+ for i in range(start, end):
2149
+ dst[i] = constant_value
2150
+ return
2151
+
2152
+ src_len = len(src)
2153
+ valid_start = max(start, -ago)
2154
+ valid_end = min(end, src_len - ago)
2155
+ nan = NAN
2156
+
2157
+ for i in range(start, valid_start):
2158
+ dst[i] = nan
2159
+
2160
+ if valid_start < valid_end:
2161
+ src_start = valid_start + ago
2162
+ src_end = valid_end + ago
2163
+ if getattr(dst, "typecode", None) == "d" and getattr(src, "typecode", None) == "d":
2164
+ dst[valid_start:valid_end] = src[src_start:src_end]
2165
+ else:
2166
+ for i in range(valid_start, valid_end):
2167
+ val = src[i + ago]
2168
+ dst[i] = nan if val is None else val
2169
+
2170
+ for i in range(valid_end, end):
2171
+ dst[i] = nan
2172
+
2173
+
2174
+ class _LineForward(LineActions):
2175
+ """Forward a line by a positive offset (lookahead).
2176
+
2177
+ ``a(ago)`` is a time shift, not a unary or binary operation. A source
2178
+ value observed at position ``i`` belongs at position ``i - ago`` in the
2179
+ result, so the final ``ago`` positions remain unavailable.
2180
+ """
2181
+
2182
+ def __init__(self, a, ago):
2183
+ super().__init__()
2184
+ self.a = self.arrayize(a)
2185
+ self.ago = ago
2186
+
2187
+ # Keep the original lookahead-period rule: the source's own
2188
+ # warm-up already covers a smaller offset, while a larger offset must
2189
+ # extend the destination just enough to make that source position
2190
+ # available. Sources without a period retain the baseline minimum 1.
2191
+ source_minperiod = getattr(self.a, "_minperiod", 1)
2192
+ if ago > source_minperiod:
2193
+ self.addminperiod(ago - source_minperiod + 1)
2194
+
2195
+ def next(self):
2196
+ """Write today's source value into the earlier shifted output slot."""
2197
+ self[-self.ago] = self.a[0]
2198
+
2199
+ def _next(self):
2200
+ """Run the normal LineActions lifecycle when scheduled by an owner."""
2201
+ self._next_old()
2202
+
2203
+ def once(self, start, end):
2204
+ """Populate the same offset mapping used by :meth:`next`.
2205
+
2206
+ A direct ``once(0, end)`` call must not use Python's negative indexing
2207
+ to wrap the first source sample onto the tail of the output array.
2208
+ Actual Cerebro scheduling starts at the lookahead minperiod, but the
2209
+ guard also makes standalone callers deterministic.
2210
+ """
2211
+ dst = self.array
2212
+ src = self.a.array
2213
+ ago = self.ago
2214
+
2215
+ while len(dst) < end:
2216
+ dst.append(NAN)
2217
+
2218
+ valid_start = max(start, ago)
2219
+ valid_end = min(end, len(src))
2220
+ for i in range(valid_start, valid_end):
2221
+ dst[i - ago] = src[i]
2222
+
2223
+
2224
+ class LinesOperation(LineActions):
2225
+ """Operation between two line objects (binary operations).
2226
+
2227
+ This class represents binary operations (addition, subtraction, etc.)
2228
+ between two line objects. The result is a new line that contains the
2229
+ element-wise operation result.
2230
+
2231
+ Attributes:
2232
+ operation: The binary function to apply (e.g., operator.add).
2233
+ a: First operand (left-hand side).
2234
+ b: Second operand (right-hand side).
2235
+ r: If True, reverse operation order.
2236
+ _parent_a: Parent indicator for operand a.
2237
+ _parent_b: Parent indicator for operand b.
2238
+
2239
+ Example:
2240
+ >>> result = LinesOperation(indicator1, indicator2, operator.sub)
2241
+ >>> # result[0] = indicator1[0] - indicator2[0]
2242
+ """
2243
+
2244
+ def __init__(self, a, b, operation, r=False, parent_a=None, parent_b=None):
2245
+ """Initialize a binary operation between two line objects.
2246
+
2247
+ Args:
2248
+ a: First operand (left-hand side).
2249
+ b: Second operand (right-hand side).
2250
+ operation: The binary function to apply (e.g., operator.add).
2251
+ r: If True, reverse operation order (b op a instead of a op b).
2252
+ parent_a: Parent indicator for operand a.
2253
+ parent_b: Parent indicator for operand b.
2254
+ """
2255
+ super().__init__()
2256
+
2257
+ self.operation = operation
2258
+ self.a = a # always a linebuffer-like object
2259
+ self.b = self.arrayize(b)
2260
+ self.r = r
2261
+ self._datas = [operand for operand in (self.a, self.b) if isinstance(operand, LineRoot)]
2262
+ if self._datas:
2263
+ data_clock = getattr(self._datas[0], "_clock", None)
2264
+ if data_clock is not None and data_clock.__class__.__name__ != "MinimalClock":
2265
+ self._clock = data_clock
2266
+ else:
2267
+ self._clock = self._datas[0]
2268
+
2269
+ # CRITICAL FIX: Store references to parent indicators for _once processing
2270
+ # Use passed parent references if available, otherwise try to find them
2271
+ self._parent_a = parent_a if parent_a is not None else self._find_parent_indicator(a)
2272
+ self._parent_b = parent_b if parent_b is not None else self._find_parent_indicator(b)
2273
+
2274
+ # ensure a is added if it's a lineiterator-like object
2275
+ # self.addminperiod(1) already done by the base class
2276
+ # CRITICAL FIX: Handle _minperiod attribute access more safely
2277
+ a_minperiod = getattr(a, "_minperiod", 1) if hasattr(a, "_minperiod") else 1
2278
+ b_minperiod = getattr(b, "_minperiod", 1) if hasattr(b, "_minperiod") else 1
2279
+
2280
+ # Use updateminperiod to take max of operand minperiods
2281
+ # For me1 - me2, minperiod = max(me1._minperiod, me2._minperiod)
2282
+ max_minperiod = max(a_minperiod, b_minperiod)
2283
+ self.updateminperiod(max_minperiod)
2284
+
2285
+ self._a_minperiod = a_minperiod
2286
+ self._b_minperiod = b_minperiod
2287
+ self._a_guard_minperiod = self._needs_minperiod_guard(self.a)
2288
+ self._b_guard_minperiod = self._needs_minperiod_guard(self.b)
2289
+ self._next_operands = tuple(
2290
+ operand
2291
+ for operand in (self.a, self.b)
2292
+ if operand is not self
2293
+ and isinstance(operand, LineActions)
2294
+ and hasattr(operand, "_next")
2295
+ )
2296
+
2297
+ @staticmethod
2298
+ def _is_constant_operand(operand):
2299
+ """Return True for constants wrapped as LineDelay(PseudoArray)."""
2300
+ return (
2301
+ operand.__class__.__name__ == "_LineDelay"
2302
+ and getattr(operand, "a", None).__class__.__name__ == "PseudoArray"
2303
+ )
2304
+
2305
+ @staticmethod
2306
+ def _is_line_delay_operand(operand):
2307
+ return operand.__class__.__name__ == "_LineDelay"
2308
+
2309
+ @classmethod
2310
+ def _needs_minperiod_guard(cls, operand):
2311
+ return (
2312
+ not cls._is_constant_operand(operand)
2313
+ and not cls._is_line_delay_operand(operand)
2314
+ and not isinstance(operand, LineActions)
2315
+ and hasattr(operand, "__len__")
2316
+ and hasattr(operand, "_minperiod")
2317
+ )
2318
+
2319
+ @staticmethod
2320
+ def _is_missing(value):
2321
+ return value is None or (isinstance(value, float) and value != value)
2322
+
2323
+ def _operand_value(self, operand, ago=0, guard_minperiod=False, minperiod=1):
2324
+ """Read an operand while preserving indicator warmup NaN semantics."""
2325
+ if guard_minperiod:
2326
+ try:
2327
+ target_len = len(operand) + ago
2328
+ if target_len < minperiod:
2329
+ return float("nan")
2330
+ except (AttributeError, TypeError):
2331
+ # Operand without a usable length; skip the warmup guard.
2332
+ # This optional protocol probe runs per sample, so stay quiet.
2333
+ pass
2334
+ except Exception: # nosec B110
2335
+ throttled_warning(
2336
+ logger,
2337
+ "linebuffer.lines_operation.operand_minperiod_probe_recovery",
2338
+ "LinesOperation operand length probe failed; skipping warmup guard",
2339
+ exc_info=False,
2340
+ )
2341
+
2342
+ if hasattr(operand, "__getitem__"):
2343
+ return operand[ago]
2344
+ return operand
2345
+
2346
+ def _normalize_operand(self, value):
2347
+ if self._is_missing(value):
2348
+ return float("nan")
2349
+ if isinstance(value, float) and (value in (INF, NEG_INF)):
2350
+ return 0.0
2351
+ if isinstance(value, (int, float)):
2352
+ return value
2353
+ try:
2354
+ return float(value)
2355
+ except (ValueError, TypeError):
2356
+ return float("nan")
2357
+
2358
+ def _next_operand_if_due(self, operand):
2359
+ clock = getattr(operand, "_clock", None)
2360
+ if clock is not None:
2361
+ try:
2362
+ if len(clock) <= len(operand):
2363
+ return
2364
+ except (AttributeError, TypeError):
2365
+ # Clock without a comparable length; advance the operand anyway.
2366
+ # This optional protocol probe runs per bar, so stay quiet.
2367
+ pass
2368
+ except Exception: # nosec B110
2369
+ throttled_warning(
2370
+ logger,
2371
+ "linebuffer.lines_operation.operand_clock_probe_recovery",
2372
+ "LinesOperation operand clock length probe failed; advancing operand",
2373
+ exc_info=False,
2374
+ )
2375
+
2376
+ operand._next()
2377
+
2378
+ def _find_parent_indicator(self, operand):
2379
+ """Find the parent indicator that owns this operand.
2380
+
2381
+ Only returns LineActions objects. Full Indicator/LineIterator objects are
2382
+ never returned because they are processed separately via the _lineiterators
2383
+ ordering in _once(). Returning a full Indicator here would cause premature
2384
+ once_via_next() calls with incorrect data state.
2385
+ """
2386
+ # If operand is already a LineActions (arithmetic expression chain), return it
2387
+ if isinstance(operand, LineActions):
2388
+ return operand
2389
+ # For plain LineBuffer: check if owner is a LineActions (not a full Indicator)
2390
+ if hasattr(operand, "_owner") and operand._owner is not None:
2391
+ owner = operand._owner
2392
+ if hasattr(owner, "_owner_ref") and owner._owner_ref is not None:
2393
+ ref = owner._owner_ref
2394
+ if isinstance(ref, LineActions):
2395
+ return ref
2396
+ if isinstance(owner, LineActions):
2397
+ return owner
2398
+ return None
2399
+
2400
+ def __getitem__(self, ago):
2401
+ """Get value at the specified offset.
2402
+
2403
+ In runonce mode, the array is pre-computed by once(), so use it directly.
2404
+ Falls back to dynamic computation only if the array is not populated.
2405
+ """
2406
+ try:
2407
+ # Use pre-computed array if available (runonce mode)
2408
+ current_idx = self._idx
2409
+ if current_idx >= 0 and len(self.array) > 0:
2410
+ target_idx = current_idx + ago
2411
+ if 0 <= target_idx < len(self.array):
2412
+ value = self.array[target_idx]
2413
+ if value is not None:
2414
+ if isinstance(value, float):
2415
+ if value == value:
2416
+ if value in (INF, NEG_INF):
2417
+ return 0.0
2418
+ return value
2419
+ else:
2420
+ return value
2421
+
2422
+ # Fallback: compute value dynamically from source operands.
2423
+ a_val = self._normalize_operand(
2424
+ self._operand_value(self.a, ago, self._a_guard_minperiod, self._a_minperiod)
2425
+ )
2426
+ b_val = self._normalize_operand(
2427
+ self._operand_value(self.b, ago, self._b_guard_minperiod, self._b_minperiod)
2428
+ )
2429
+
2430
+ if self._is_missing(a_val):
2431
+ return float("nan")
2432
+ if self._is_missing(b_val):
2433
+ return float("nan")
2434
+
2435
+ # Compute and return the operation result
2436
+ if self.r:
2437
+ result = self.operation(b_val, a_val)
2438
+ else:
2439
+ result = self.operation(a_val, b_val)
2440
+ if self._is_missing(result):
2441
+ return float("nan")
2442
+ if isinstance(result, float) and not math.isfinite(result):
2443
+ return 0.0
2444
+ return result
2445
+ except (IndexError, TypeError):
2446
+ return float("nan")
2447
+
2448
+ def _next(self):
2449
+ """CRITICAL FIX: _next() method for compatibility with LineIterator processing loop.
2450
+ This method is called by LineIterator._next() for items in _lineiterators[IndType].
2451
+ """
2452
+ # Clock guard: skip if already advanced to the current clock position.
2453
+ # This prevents double-advancing when a LinesOperation is both registered
2454
+ # directly in _lineiterators AND driven via a parent's _next_operands chain.
2455
+ clock = getattr(self, "_clock", None)
2456
+ if clock is not None and clock.__class__.__name__ != "MinimalClock":
2457
+ try:
2458
+ if len(clock) <= len(self):
2459
+ return
2460
+ except (AttributeError, TypeError):
2461
+ # Clock without a comparable length; proceed to advance operands.
2462
+ # This optional protocol probe runs per bar, so stay quiet.
2463
+ pass
2464
+ except Exception: # nosec B110
2465
+ throttled_warning(
2466
+ logger,
2467
+ "linebuffer.lines_operation.clock_probe_recovery",
2468
+ "LinesOperation clock length probe failed; advancing operation",
2469
+ exc_info=False,
2470
+ )
2471
+
2472
+ for operand in self._next_operands:
2473
+ self._next_operand_if_due(operand)
2474
+
2475
+ # Advance the line buffer
2476
+ self.advance()
2477
+ # Call next() to compute the value
2478
+ self.next()
2479
+ # Update bindings so bound lines get the computed value
2480
+ for binding in self.bindings:
2481
+ binding[0] = self[0]
2482
+
2483
+ def next(self):
2484
+ """Calculate and set the operation result for the current bar.
2485
+
2486
+ Performs the binary operation on the current values of both
2487
+ operands and stores the result at position 0.
2488
+ """
2489
+ # operation(float, other) ... expecting other to be a float
2490
+ # CRITICAL FIX: Ensure we get valid numeric values for indicator calculations
2491
+ try:
2492
+ a_val = self._normalize_operand(
2493
+ self._operand_value(self.a, 0, self._a_guard_minperiod, self._a_minperiod)
2494
+ )
2495
+ b_val = self._normalize_operand(
2496
+ self._operand_value(self.b, 0, self._b_guard_minperiod, self._b_minperiod)
2497
+ )
2498
+
2499
+ if self._is_missing(a_val) or self._is_missing(b_val):
2500
+ self[0] = float("nan")
2501
+ return
2502
+
2503
+ # CRITICAL FIX: Actually perform the operation and store the result
2504
+ # Handle both normal and reverse operations
2505
+ if hasattr(self, "operation") and self.operation:
2506
+ # CRITICAL FIX: Handle reverse operations properly
2507
+ if getattr(self, "r", False):
2508
+ result = self.operation(b_val, a_val) # Reverse: b op a
2509
+ else:
2510
+ result = self.operation(a_val, b_val) # Normal: a op b
2511
+
2512
+ # Ensure result is a valid number
2513
+ if result is None:
2514
+ result = float("nan")
2515
+ elif isinstance(result, float) and not math.isfinite(result):
2516
+ if result != result:
2517
+ result = float("nan")
2518
+ else:
2519
+ result = 0.0
2520
+ elif not isinstance(result, (int, float)):
2521
+ try:
2522
+ result = float(result)
2523
+ except (ValueError, TypeError):
2524
+ result = float("nan")
2525
+
2526
+ # Store the result in the current position
2527
+ self[0] = result
2528
+ else:
2529
+ # Fallback: store a_val if no operation is defined
2530
+ self[0] = a_val
2531
+
2532
+ except Exception:
2533
+ throttled_warning(
2534
+ logger,
2535
+ "linebuffer.lines_operation.next.nan_recovery",
2536
+ "LinesOperation.next failed; storing NaN recovery value",
2537
+ exc_info=False,
2538
+ )
2539
+ self[0] = float("nan")
2540
+
2541
+ def once(self, start, end):
2542
+ """Calculate operation results in batch mode (runonce).
2543
+
2544
+ Args:
2545
+ start: Starting index.
2546
+ end: Ending index.
2547
+ """
2548
+ # CRITICAL FIX: Always use start=0 for nested operations
2549
+ # This ensures historical values are available for indicators like SMA
2550
+ nested_start = 0
2551
+
2552
+ # CRITICAL FIX: Call parent indicators' once() methods to populate their arrays
2553
+ # This is needed for cases like dif = ema_1 - ema_2 where ema_1/ema_2 must be computed first
2554
+ if self._parent_a is not None and hasattr(self._parent_a, "once"):
2555
+ try:
2556
+ self._parent_a.once(nested_start, end)
2557
+ except Exception:
2558
+ throttled_warning(
2559
+ logger,
2560
+ "linebuffer.lines_operation.once.parent_recovery",
2561
+ "LinesOperation parent batch computation failed; continuing",
2562
+ exc_info=False,
2563
+ )
2564
+
2565
+ if self._parent_b is not None and hasattr(self._parent_b, "once"):
2566
+ try:
2567
+ self._parent_b.once(nested_start, end)
2568
+ except Exception:
2569
+ throttled_warning(
2570
+ logger,
2571
+ "linebuffer.lines_operation.once.parent_recovery",
2572
+ "LinesOperation parent batch computation failed; continuing",
2573
+ exc_info=False,
2574
+ )
2575
+
2576
+ # CRITICAL FIX: Call once() on operands that have it, but ONLY for LineActions
2577
+ # instances (like _LineDelay, LinesOperation). Never call once() on full Indicators
2578
+ # (ATR, SuperTrend, etc.) because those are managed by _lineiterators in _once().
2579
+ # Calling once() on a full Indicator here would trigger premature once_via_next calls
2580
+ # before the indicator's data state is properly set up.
2581
+ if isinstance(self.a, LineActions) and hasattr(self.a, "once"):
2582
+ try:
2583
+ self.a.once(nested_start, end)
2584
+ except Exception:
2585
+ throttled_warning(
2586
+ logger,
2587
+ "linebuffer.lines_operation.once.operand_recovery",
2588
+ "LinesOperation operand batch computation failed; continuing",
2589
+ exc_info=False,
2590
+ )
2591
+
2592
+ if isinstance(self.b, LineActions) and hasattr(self.b, "once"):
2593
+ try:
2594
+ self.b.once(nested_start, end)
2595
+ except Exception:
2596
+ throttled_warning(
2597
+ logger,
2598
+ "linebuffer.lines_operation.once.operand_recovery",
2599
+ "LinesOperation operand batch computation failed; continuing",
2600
+ exc_info=False,
2601
+ )
2602
+
2603
+ # CRITICAL FIX: Always process from 0 to populate historical values
2604
+ if hasattr(self.b, "array") and type(self.b).__name__ != "PseudoArray":
2605
+ self._once_op(nested_start, end)
2606
+ else:
2607
+ if isinstance(self.b, float):
2608
+ (
2609
+ self._once_val_op_r(nested_start, end)
2610
+ if self.r
2611
+ else self._once_val_op(nested_start, end)
2612
+ )
2613
+ else:
2614
+ self._once_time_op(nested_start, end)
2615
+
2616
+ # CRITICAL FIX: Call oncebinding to copy computed values to bound lines
2617
+ # This is needed in runonce mode where once() computes all values at once
2618
+ self.oncebinding()
2619
+
2620
+ def _once_op(self, start, end):
2621
+ # Only call once() on LineActions instances (e.g., _LineDelay), not full Indicators
2622
+ if isinstance(self.b, LineActions) and hasattr(self.b, "once") and len(self.b.array) < end:
2623
+ try:
2624
+ self.b.once(start, end)
2625
+ except Exception:
2626
+ throttled_warning(
2627
+ logger,
2628
+ "linebuffer.lines_operation.once.operand_recovery",
2629
+ "LinesOperation operand batch computation failed; continuing",
2630
+ exc_info=False,
2631
+ )
2632
+
2633
+ # cache python dictionary lookups
2634
+ dst = self.array
2635
+ srca = self.a.array
2636
+ srcb = self.b.array
2637
+ op = self.operation
2638
+
2639
+ # Ensure destination array is sized for direct index assignment
2640
+ while len(dst) < end:
2641
+ dst.append(float("nan"))
2642
+
2643
+ # Clip processing range to available source data
2644
+ # CRITICAL FIX: Check if b is a _LineDelay wrapping a constant (PseudoArray)
2645
+ # In this case, srcb will be empty but b[i] will return the constant
2646
+ is_constant_b = (
2647
+ len(srcb) == 0 and hasattr(self.b, "a") and type(self.b.a).__name__ == "PseudoArray"
2648
+ )
2649
+
2650
+ if is_constant_b:
2651
+ # b is a _LineDelay wrapping a constant - use srca length only
2652
+ end = min(end, len(srca))
2653
+ else:
2654
+ end = min(end, len(srca), len(srcb))
2655
+
2656
+ # Use dynamic access for constant values wrapped in _LineDelay
2657
+ use_dynamic_b = is_constant_b
2658
+
2659
+ # CRITICAL FIX: Always process from 0 to ensure historical values are available
2660
+ # This is needed for indicators like SMA that need historical values for their calculations
2661
+ actual_start = 0
2662
+
2663
+ if (
2664
+ not use_dynamic_b
2665
+ and getattr(srca, "typecode", None) == "d"
2666
+ and getattr(srcb, "typecode", None) == "d"
2667
+ ):
2668
+ nan = NAN
2669
+ inf = float("inf")
2670
+ neg_inf = float("-inf")
2671
+ try:
2672
+ if op is operator.__mul__:
2673
+ for i in range(actual_start, end):
2674
+ a_val = srca[i]
2675
+ b_val = srcb[i]
2676
+ if a_val != a_val or b_val != b_val:
2677
+ dst[i] = nan
2678
+ continue
2679
+ if a_val in (inf, neg_inf):
2680
+ a_val = 0.0
2681
+ if b_val in (inf, neg_inf):
2682
+ b_val = 0.0
2683
+ result = a_val * b_val
2684
+ if result != result:
2685
+ dst[i] = nan
2686
+ elif result in (inf, neg_inf):
2687
+ dst[i] = 0.0
2688
+ else:
2689
+ dst[i] = result
2690
+ elif op is operator.__add__:
2691
+ for i in range(actual_start, end):
2692
+ a_val = srca[i]
2693
+ b_val = srcb[i]
2694
+ if a_val != a_val or b_val != b_val:
2695
+ dst[i] = nan
2696
+ continue
2697
+ if a_val in (inf, neg_inf):
2698
+ a_val = 0.0
2699
+ if b_val in (inf, neg_inf):
2700
+ b_val = 0.0
2701
+ result = a_val + b_val
2702
+ if result != result:
2703
+ dst[i] = nan
2704
+ elif result in (inf, neg_inf):
2705
+ dst[i] = 0.0
2706
+ else:
2707
+ dst[i] = result
2708
+ elif op is operator.__sub__:
2709
+ if self.r:
2710
+ for i in range(actual_start, end):
2711
+ a_val = srca[i]
2712
+ b_val = srcb[i]
2713
+ if a_val != a_val or b_val != b_val:
2714
+ dst[i] = nan
2715
+ continue
2716
+ if a_val in (inf, neg_inf):
2717
+ a_val = 0.0
2718
+ if b_val in (inf, neg_inf):
2719
+ b_val = 0.0
2720
+ result = b_val - a_val
2721
+ if result != result:
2722
+ dst[i] = nan
2723
+ elif result in (inf, neg_inf):
2724
+ dst[i] = 0.0
2725
+ else:
2726
+ dst[i] = result
2727
+ else:
2728
+ for i in range(actual_start, end):
2729
+ a_val = srca[i]
2730
+ b_val = srcb[i]
2731
+ if a_val != a_val or b_val != b_val:
2732
+ dst[i] = nan
2733
+ continue
2734
+ if a_val in (inf, neg_inf):
2735
+ a_val = 0.0
2736
+ if b_val in (inf, neg_inf):
2737
+ b_val = 0.0
2738
+ result = a_val - b_val
2739
+ if result != result:
2740
+ dst[i] = nan
2741
+ elif result in (inf, neg_inf):
2742
+ dst[i] = 0.0
2743
+ else:
2744
+ dst[i] = result
2745
+ elif self.r:
2746
+ for i in range(actual_start, end):
2747
+ a_val = srca[i]
2748
+ b_val = srcb[i]
2749
+ if a_val != a_val or b_val != b_val:
2750
+ dst[i] = nan
2751
+ continue
2752
+ if a_val in (inf, neg_inf):
2753
+ a_val = 0.0
2754
+ if b_val in (inf, neg_inf):
2755
+ b_val = 0.0
2756
+ result = op(b_val, a_val)
2757
+ if result != result:
2758
+ dst[i] = nan
2759
+ elif result in (inf, neg_inf):
2760
+ dst[i] = 0.0
2761
+ else:
2762
+ dst[i] = result
2763
+ else:
2764
+ for i in range(actual_start, end):
2765
+ a_val = srca[i]
2766
+ b_val = srcb[i]
2767
+ if a_val != a_val or b_val != b_val:
2768
+ dst[i] = nan
2769
+ continue
2770
+ if a_val in (inf, neg_inf):
2771
+ a_val = 0.0
2772
+ if b_val in (inf, neg_inf):
2773
+ b_val = 0.0
2774
+ result = op(a_val, b_val)
2775
+ if result != result:
2776
+ dst[i] = nan
2777
+ elif result in (inf, neg_inf):
2778
+ dst[i] = 0.0
2779
+ else:
2780
+ dst[i] = result
2781
+ return
2782
+ except Exception:
2783
+ # The generic and per-element paths below preserve the existing
2784
+ # NaN recovery. Only the recovered values are diagnosed so a
2785
+ # failed fast-path selection cannot duplicate log records.
2786
+ pass
2787
+
2788
+ # Fast path under a single try; the per-element try/except below is only
2789
+ # entered on error, preserving NaN-on-failure semantics while removing
2790
+ # per-element exception-handler setup in the common case (R2-S4: PERF203).
2791
+ try:
2792
+ for i in range(actual_start, end):
2793
+ a_val = srca[i]
2794
+ b_val = self.b[i] if use_dynamic_b else srcb[i]
2795
+ if a_val is None or a_val != a_val or b_val is None or b_val != b_val:
2796
+ dst[i] = float("nan")
2797
+ continue
2798
+ if isinstance(a_val, float) and not math.isfinite(a_val):
2799
+ a_val = 0.0
2800
+ if isinstance(b_val, float) and not math.isfinite(b_val):
2801
+ b_val = 0.0
2802
+ result = op(b_val, a_val) if self.r else op(a_val, b_val)
2803
+ if result is None or result != result:
2804
+ result = float("nan")
2805
+ elif isinstance(result, float) and not math.isfinite(result):
2806
+ result = 0.0
2807
+ dst[i] = result
2808
+ return
2809
+ except Exception:
2810
+ # Per-element recovery below emits the bounded diagnostic.
2811
+ pass
2812
+
2813
+ for i in range(actual_start, end):
2814
+ try:
2815
+ a_val = srca[i]
2816
+ if use_dynamic_b:
2817
+ b_val = self.b[i] # Use __getitem__ for constants
2818
+ else:
2819
+ b_val = srcb[i]
2820
+
2821
+ # Preserve NaN semantics for indicators: if any operand is None/NaN -> NaN
2822
+ if a_val is None or a_val != a_val or b_val is None or b_val != b_val:
2823
+ dst[i] = float("nan")
2824
+ continue
2825
+ if isinstance(a_val, float) and not math.isfinite(a_val):
2826
+ a_val = 0.0
2827
+ if isinstance(b_val, float) and not math.isfinite(b_val):
2828
+ b_val = 0.0
2829
+
2830
+ if self.r:
2831
+ result = op(b_val, a_val)
2832
+ else:
2833
+ result = op(a_val, b_val)
2834
+
2835
+ # Preserve NaN semantics
2836
+ if result is None or result != result:
2837
+ result = float("nan")
2838
+ elif isinstance(result, float) and not math.isfinite(result):
2839
+ result = 0.0
2840
+
2841
+ dst[i] = result
2842
+ except Exception:
2843
+ # If operation fails, store NaN for indicator semantics.
2844
+ throttled_warning(
2845
+ logger,
2846
+ "linebuffer.lines_operation.once.nan_recovery",
2847
+ "LinesOperation batch computation failed; storing NaN recovery value",
2848
+ exc_info=False,
2849
+ )
2850
+ dst[i] = float("nan")
2851
+
2852
+ def _once_time_op(self, start, end):
2853
+ # cache python dictionary lookups
2854
+ dst = self.array
2855
+ srca = self.a.array
2856
+ srcb = self.b[0]
2857
+ op = self.operation
2858
+
2859
+ # Ensure destination array is sized for direct index assignment
2860
+ while len(dst) < end:
2861
+ dst.append(float("nan"))
2862
+
2863
+ # Clip processing range to available source data
2864
+ end = min(end, len(srca))
2865
+
2866
+ for i in range(start, end):
2867
+ try:
2868
+ a_val = srca[i]
2869
+
2870
+ # Preserve NaN semantics
2871
+ if a_val is None or a_val != a_val or srcb is None or srcb != srcb:
2872
+ dst[i] = float("nan")
2873
+ continue
2874
+ if isinstance(a_val, float) and not math.isfinite(a_val):
2875
+ a_val = 0.0
2876
+ if isinstance(srcb, float) and not math.isfinite(srcb):
2877
+ srcb = 0.0
2878
+
2879
+ if self.r:
2880
+ result = op(srcb, a_val)
2881
+ else:
2882
+ result = op(a_val, srcb)
2883
+
2884
+ if result is None or result != result:
2885
+ result = float("nan")
2886
+ elif isinstance(result, float) and not math.isfinite(result):
2887
+ result = 0.0
2888
+
2889
+ dst[i] = result
2890
+ except Exception:
2891
+ throttled_warning(
2892
+ logger,
2893
+ "linebuffer.lines_operation.once.nan_recovery",
2894
+ "LinesOperation batch computation failed; storing NaN recovery value",
2895
+ exc_info=False,
2896
+ )
2897
+ dst[i] = float("nan")
2898
+
2899
+ def _once_val_op(self, start, end):
2900
+ # cache python dictionary lookups
2901
+ dst = self.array
2902
+ srca = self.a.array
2903
+ srcb = self.b[0] if hasattr(self.b, "__getitem__") else self.b
2904
+ op = self.operation
2905
+
2906
+ # Ensure destination array is sized for direct index assignment
2907
+ while len(dst) < end:
2908
+ dst.append(float("nan"))
2909
+
2910
+ # Clip processing range to available source data
2911
+ end = min(end, len(srca))
2912
+
2913
+ for i in range(start, end):
2914
+ try:
2915
+ a_val = srca[i]
2916
+
2917
+ if a_val is None or a_val != a_val or srcb is None or srcb != srcb:
2918
+ dst[i] = float("nan")
2919
+ continue
2920
+ if isinstance(a_val, float) and not math.isfinite(a_val):
2921
+ a_val = 0.0
2922
+ if isinstance(srcb, float) and not math.isfinite(srcb):
2923
+ srcb = 0.0
2924
+
2925
+ result = op(a_val, srcb)
2926
+
2927
+ if result is None or result != result:
2928
+ result = float("nan")
2929
+ elif isinstance(result, float) and not math.isfinite(result):
2930
+ result = 0.0
2931
+
2932
+ dst[i] = result
2933
+ except Exception:
2934
+ throttled_warning(
2935
+ logger,
2936
+ "linebuffer.lines_operation.once.nan_recovery",
2937
+ "LinesOperation batch computation failed; storing NaN recovery value",
2938
+ exc_info=False,
2939
+ )
2940
+ dst[i] = float("nan")
2941
+
2942
+ def _once_val_op_r(self, start, end):
2943
+ # cache python dictionary lookups
2944
+ dst = self.array
2945
+ srca = self.a.array
2946
+ srcb = self.b[0] if hasattr(self.b, "__getitem__") else self.b
2947
+ op = self.operation
2948
+
2949
+ # Ensure destination array is sized for direct index assignment
2950
+ while len(dst) < end:
2951
+ dst.append(float("nan"))
2952
+
2953
+ # Clip processing range to available source data
2954
+ end = min(end, len(srca))
2955
+
2956
+ for i in range(start, end):
2957
+ try:
2958
+ a_val = srca[i]
2959
+
2960
+ if a_val is None or a_val != a_val or srcb is None or srcb != srcb:
2961
+ dst[i] = float("nan")
2962
+ continue
2963
+ if isinstance(a_val, float) and not math.isfinite(a_val):
2964
+ a_val = 0.0
2965
+ if isinstance(srcb, float) and not math.isfinite(srcb):
2966
+ srcb = 0.0
2967
+
2968
+ result = op(srcb, a_val)
2969
+
2970
+ if result is None or result != result:
2971
+ result = float("nan")
2972
+ elif isinstance(result, float) and not math.isfinite(result):
2973
+ result = 0.0
2974
+
2975
+ dst[i] = result
2976
+ except Exception:
2977
+ throttled_warning(
2978
+ logger,
2979
+ "linebuffer.lines_operation.once.nan_recovery",
2980
+ "LinesOperation batch computation failed; storing NaN recovery value",
2981
+ exc_info=False,
2982
+ )
2983
+ dst[i] = float("nan")
2984
+
2985
+
2986
+ class LineOwnOperation(LineActions):
2987
+ """Operation on a single line object (unary operations).
2988
+
2989
+ This class represents unary operations (negation, absolute value, etc.)
2990
+ on a single line object. The result is a new line that contains the
2991
+ element-wise operation result.
2992
+
2993
+ Attributes:
2994
+ operation: The unary function to apply (e.g., operator.neg).
2995
+ a: The operand (line object).
2996
+ _parent_a: Parent indicator for the operand.
2997
+
2998
+ Example:
2999
+ >>> result = LineOwnOperation(indicator, operator.neg)
3000
+ >>> # result[0] = -indicator[0]
3001
+ """
3002
+
3003
+ def __init__(self, a, operation, parent_a=None):
3004
+ """Initialize a unary operation on a line object.
3005
+
3006
+ Args:
3007
+ a: The operand (line object).
3008
+ operation: The unary function to apply (e.g., operator.neg).
3009
+ parent_a: Parent indicator for the operand.
3010
+ """
3011
+ super().__init__()
3012
+
3013
+ self.operation = operation
3014
+ self.a = a
3015
+
3016
+ # CRITICAL FIX: Store reference to parent indicator for _once processing
3017
+ self._parent_a = parent_a if parent_a is not None else self._find_parent_indicator(a)
3018
+
3019
+ a_minperiod = getattr(a, "_minperiod", 1) if hasattr(a, "_minperiod") else 1
3020
+ self.updateminperiod(a_minperiod)
3021
+
3022
+ def _find_parent_indicator(self, operand):
3023
+ """Find the parent indicator that owns this operand.
3024
+
3025
+ Only returns LineActions objects. Full Indicators are never returned to
3026
+ prevent premature once_via_next() calls (see LinesOperation._find_parent_indicator).
3027
+ """
3028
+ if isinstance(operand, LineActions):
3029
+ return operand
3030
+ if hasattr(operand, "_owner") and operand._owner is not None:
3031
+ owner = operand._owner
3032
+ if hasattr(owner, "_owner_ref") and owner._owner_ref is not None:
3033
+ ref = owner._owner_ref
3034
+ if isinstance(ref, LineActions):
3035
+ return ref
3036
+ if isinstance(owner, LineActions):
3037
+ return owner
3038
+ return None
3039
+
3040
+ def __getitem__(self, ago):
3041
+ """CRITICAL FIX: Override __getitem__ to compute value dynamically from source operand."""
3042
+ try:
3043
+ a_val = self.a[ago] if hasattr(self.a, "__getitem__") else self.a
3044
+ if a_val is None or (isinstance(a_val, float) and a_val != a_val):
3045
+ return float("nan")
3046
+ if isinstance(a_val, float) and not math.isfinite(a_val):
3047
+ a_val = 0.0
3048
+ result = self.operation(a_val)
3049
+ if isinstance(result, float) and not math.isfinite(result):
3050
+ return 0.0
3051
+ return result
3052
+ except (IndexError, TypeError):
3053
+ return float("nan")
3054
+
3055
+ def next(self):
3056
+ """Calculate and set the unary operation result for the current bar.
3057
+
3058
+ Performs the unary operation on the current value of the operand
3059
+ and stores the result at position 0.
3060
+ """
3061
+ a_val = self.a[0]
3062
+ if a_val is None or (isinstance(a_val, float) and not math.isfinite(a_val)):
3063
+ a_val = 0.0
3064
+
3065
+ result = self.operation(a_val)
3066
+ if result is None or (isinstance(result, float) and not math.isfinite(result)):
3067
+ result = 0.0
3068
+
3069
+ self[0] = result
3070
+
3071
+ def once(self, start, end):
3072
+ """Calculate unary operation results in batch mode (runonce).
3073
+
3074
+ Args:
3075
+ start: Starting index.
3076
+ end: Ending index.
3077
+ """
3078
+ # CRITICAL FIX: Ensure source operand is processed first
3079
+ if self._parent_a is not None and hasattr(self._parent_a, "_once"):
3080
+ try:
3081
+ self._parent_a._once(start, end)
3082
+ except Exception:
3083
+ throttled_warning(
3084
+ logger,
3085
+ "linebuffer.line_own_operation.once.parent_recovery",
3086
+ "LineOwnOperation parent batch computation failed; continuing",
3087
+ exc_info=False,
3088
+ )
3089
+
3090
+ # cache python dictionary lookups
3091
+ dst = self.array
3092
+ srca = self.a.array
3093
+ op = self.operation
3094
+
3095
+ # CRITICAL FIX: Ensure destination array is properly sized
3096
+ while len(dst) < end:
3097
+ dst.append(float("nan"))
3098
+
3099
+ # CRITICAL FIX: Ensure source array has required data
3100
+ if len(srca) < end:
3101
+ # If source array is shorter than required range, only process available data
3102
+ end = min(end, len(srca))
3103
+
3104
+ # Fast path under a single try; per-element fallback only on error
3105
+ # (preserves 0.0-on-failure semantics, removes per-element handler setup; R2-S4).
3106
+ try:
3107
+ for i in range(start, end):
3108
+ a_val = srca[i] if i < len(srca) else 0.0
3109
+ if a_val is None or (isinstance(a_val, float) and not math.isfinite(a_val)):
3110
+ a_val = 0.0
3111
+ result = op(a_val)
3112
+ if result is None or (isinstance(result, float) and not math.isfinite(result)):
3113
+ result = 0.0
3114
+ dst[i] = result
3115
+ return
3116
+ except Exception:
3117
+ # The per-element loop below records the actual recovered values.
3118
+ # Do not emit a duplicate diagnostic for this implementation
3119
+ # transition, and never attach its exception traceback.
3120
+ pass
3121
+
3122
+ for i in range(start, end):
3123
+ try:
3124
+ # CRITICAL FIX: Bounds checking for source array
3125
+ a_val = srca[i] if i < len(srca) else 0.0
3126
+
3127
+ # Ensure value is numeric
3128
+ if a_val is None or (isinstance(a_val, float) and not math.isfinite(a_val)):
3129
+ a_val = 0.0
3130
+
3131
+ result = op(a_val)
3132
+
3133
+ # Ensure result is valid
3134
+ if result is None or (isinstance(result, float) and not math.isfinite(result)):
3135
+ result = 0.0
3136
+
3137
+ dst[i] = result
3138
+ except Exception:
3139
+ # One stable throttled key bounds a broken operation across all
3140
+ # elements of this batch while retaining the 0.0 recovery value.
3141
+ throttled_warning(
3142
+ logger,
3143
+ "linebuffer.line_own_operation.once.zero_recovery",
3144
+ "LineOwnOperation.once element failed; storing 0.0 recovery value",
3145
+ exc_info=False,
3146
+ )
3147
+ dst[i] = 0.0
3148
+
3149
+ def size(self):
3150
+ """Return the number of lines in this LineActions object"""
3151
+ if hasattr(self, "lines") and hasattr(self.lines, "size"):
3152
+ return self.lines.size()
3153
+ if hasattr(self, "lines") and hasattr(self.lines, "__len__"):
3154
+ return len(self.lines)
3155
+ return 1 # Default to 1 line if no lines object available