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.
- back_trader_python-1.4.0.dist-info/METADATA +1491 -0
- back_trader_python-1.4.0.dist-info/RECORD +465 -0
- back_trader_python-1.4.0.dist-info/WHEEL +5 -0
- back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
- back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
- backtrader/__init__.py +148 -0
- backtrader/_cerebro/__init__.py +5 -0
- backtrader/_cerebro/channel.py +382 -0
- backtrader/_cerebro/execution.py +377 -0
- backtrader/_cerebro/lifecycle.py +143 -0
- backtrader/_cerebro/notifications.py +150 -0
- backtrader/_cerebro/presentation.py +230 -0
- backtrader/_cerebro/registry.py +593 -0
- backtrader/_cerebro/runnext.py +551 -0
- backtrader/_cerebro/runonce.py +142 -0
- backtrader/analyzer.py +594 -0
- backtrader/analyzers/__init__.py +50 -0
- backtrader/analyzers/annualreturn.py +226 -0
- backtrader/analyzers/calmar.py +165 -0
- backtrader/analyzers/drawdown.py +287 -0
- backtrader/analyzers/leverage.py +112 -0
- backtrader/analyzers/logreturnsrolling.py +190 -0
- backtrader/analyzers/periodstats.py +153 -0
- backtrader/analyzers/positions.py +119 -0
- backtrader/analyzers/pyfolio.py +470 -0
- backtrader/analyzers/returns.py +192 -0
- backtrader/analyzers/sharpe.py +307 -0
- backtrader/analyzers/sharpe_ratio_stats.py +534 -0
- backtrader/analyzers/sqn.py +112 -0
- backtrader/analyzers/timereturn.py +192 -0
- backtrader/analyzers/total_value.py +75 -0
- backtrader/analyzers/tradeanalyzer.py +278 -0
- backtrader/analyzers/transactions.py +141 -0
- backtrader/analyzers/vwr.py +245 -0
- backtrader/bokeh/__init__.py +155 -0
- backtrader/bokeh/analyzers/__init__.py +13 -0
- backtrader/bokeh/analyzers/plot.py +192 -0
- backtrader/bokeh/analyzers/recorder.py +181 -0
- backtrader/bokeh/app.py +1094 -0
- backtrader/bokeh/live/__init__.py +11 -0
- backtrader/bokeh/live/client.py +352 -0
- backtrader/bokeh/live/datahandler.py +346 -0
- backtrader/bokeh/plot_adapter.py +200 -0
- backtrader/bokeh/schemes/__init__.py +14 -0
- backtrader/bokeh/schemes/blackly.py +76 -0
- backtrader/bokeh/schemes/scheme.py +150 -0
- backtrader/bokeh/schemes/tradimo.py +82 -0
- backtrader/bokeh/tab.py +125 -0
- backtrader/bokeh/tabs/__init__.py +30 -0
- backtrader/bokeh/tabs/analyzer.py +120 -0
- backtrader/bokeh/tabs/config.py +154 -0
- backtrader/bokeh/tabs/live.py +109 -0
- backtrader/bokeh/tabs/log.py +185 -0
- backtrader/bokeh/tabs/metadata.py +182 -0
- backtrader/bokeh/tabs/performance.py +359 -0
- backtrader/bokeh/tabs/source.py +70 -0
- backtrader/bokeh/utils/__init__.py +8 -0
- backtrader/bokeh/utils/helpers.py +167 -0
- backtrader/bokeh/webapp.py +164 -0
- backtrader/broker.py +478 -0
- backtrader/brokers/__init__.py +36 -0
- backtrader/brokers/bbroker.py +2576 -0
- backtrader/brokers/btapibroker.py +8227 -0
- backtrader/brokers/hft/__init__.py +89 -0
- backtrader/brokers/hft/binance_bbo.py +625 -0
- backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
- backtrader/brokers/hft/examples.py +1228 -0
- backtrader/brokers/hft/exchange.py +380 -0
- backtrader/brokers/hft/latency.py +309 -0
- backtrader/brokers/hft/matching_core.py +572 -0
- backtrader/brokers/hft/queue.py +238 -0
- backtrader/brokers/hft/recorder.py +88 -0
- backtrader/brokers/hft/state.py +138 -0
- backtrader/brokers/impact_models.py +118 -0
- backtrader/brokers/mixbroker.py +895 -0
- backtrader/brokers/tickbroker.py +1991 -0
- backtrader/btrun/__init__.py +12 -0
- backtrader/btrun/btrun.py +1218 -0
- backtrader/cerebro.py +828 -0
- backtrader/channel.py +682 -0
- backtrader/channels/__init__.py +23 -0
- backtrader/channels/bridge.py +186 -0
- backtrader/channels/funding.py +248 -0
- backtrader/channels/live_queue.py +216 -0
- backtrader/channels/live_validator.py +294 -0
- backtrader/channels/orderbook.py +257 -0
- backtrader/channels/tick.py +202 -0
- backtrader/comminfo.py +665 -0
- backtrader/commissions/__init__.py +106 -0
- backtrader/commissions/ctpoption.py +993 -0
- backtrader/configs/account_config_example.yaml +8 -0
- backtrader/dataseries.py +379 -0
- backtrader/errors.py +106 -0
- backtrader/events.py +980 -0
- backtrader/feed.py +1523 -0
- backtrader/feeds/__init__.py +75 -0
- backtrader/feeds/barrier.py +2006 -0
- backtrader/feeds/blaze.py +118 -0
- backtrader/feeds/btapifeed.py +1538 -0
- backtrader/feeds/btcsv.py +203 -0
- backtrader/feeds/chainer.py +114 -0
- backtrader/feeds/cryptohftdata.py +164 -0
- backtrader/feeds/csvgeneric.py +1205 -0
- backtrader/feeds/ctpcohort.py +1051 -0
- backtrader/feeds/influxfeed.py +158 -0
- backtrader/feeds/livefeed.py +71 -0
- backtrader/feeds/mixed_channel.py +108 -0
- backtrader/feeds/mt4csv.py +42 -0
- backtrader/feeds/pandafeed.py +381 -0
- backtrader/feeds/quandl.py +256 -0
- backtrader/feeds/rollover.py +229 -0
- backtrader/feeds/sierrachart.py +30 -0
- backtrader/feeds/vchart.py +162 -0
- backtrader/feeds/vchartcsv.py +84 -0
- backtrader/feeds/vchartfile.py +153 -0
- backtrader/feeds/yahoo.py +399 -0
- backtrader/fillers.py +148 -0
- backtrader/filters/__init__.py +34 -0
- backtrader/filters/bsplitter.py +127 -0
- backtrader/filters/calendardays.py +121 -0
- backtrader/filters/datafiller.py +192 -0
- backtrader/filters/datafilter.py +74 -0
- backtrader/filters/daysteps.py +96 -0
- backtrader/filters/heikinashi.py +63 -0
- backtrader/filters/renko.py +164 -0
- backtrader/filters/session.py +289 -0
- backtrader/flt.py +80 -0
- backtrader/functions.py +960 -0
- backtrader/indicator.py +449 -0
- backtrader/indicators/__init__.py +148 -0
- backtrader/indicators/accdecoscillator.py +110 -0
- backtrader/indicators/aroon.py +300 -0
- backtrader/indicators/atr.py +315 -0
- backtrader/indicators/awesomeoscillator.py +122 -0
- backtrader/indicators/basicops.py +834 -0
- backtrader/indicators/bollinger.py +223 -0
- backtrader/indicators/cci.py +89 -0
- backtrader/indicators/channels_ext.py +83 -0
- backtrader/indicators/contrib/__init__.py +228 -0
- backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
- backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
- backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
- backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
- backtrader/indicators/contrib/adxdmi.py +34 -0
- backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
- backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
- backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
- backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
- backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
- backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
- backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
- backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
- backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
- backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
- backtrader/indicators/contrib/average_change_candle.py +165 -0
- backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
- backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
- backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
- backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
- backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
- backtrader/indicators/contrib/blau_csi.py +76 -0
- backtrader/indicators/contrib/blau_ergodic.py +53 -0
- backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
- backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
- backtrader/indicators/contrib/blau_tvi.py +55 -0
- backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
- backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
- backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
- backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
- backtrader/indicators/contrib/bsi_indicator.py +87 -0
- backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
- backtrader/indicators/contrib/bulls_power.py +56 -0
- backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
- backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
- backtrader/indicators/contrib/candle_stop_color.py +46 -0
- backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
- backtrader/indicators/contrib/candlesticks_bw.py +45 -0
- backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
- backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
- backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
- backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
- backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
- backtrader/indicators/contrib/cg_oscillator.py +40 -0
- backtrader/indicators/contrib/close_line_cci.py +38 -0
- backtrader/indicators/contrib/close_price_fractals.py +47 -0
- backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
- backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
- backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
- backtrader/indicators/contrib/color_hma.py +71 -0
- backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
- backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
- backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
- backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
- backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
- backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
- backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
- backtrader/indicators/contrib/color_x2_ma.py +49 -0
- backtrader/indicators/contrib/color_x_derivative.py +63 -0
- backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
- backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
- backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
- backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
- backtrader/indicators/contrib/derivative_indicator.py +95 -0
- backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
- backtrader/indicators/contrib/digital_macd.py +200 -0
- backtrader/indicators/contrib/donchian_channels_system.py +45 -0
- backtrader/indicators/contrib/dots_indicator.py +93 -0
- backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
- backtrader/indicators/contrib/ema_rsi_va.py +80 -0
- backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
- backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
- backtrader/indicators/contrib/fatl_filter.py +179 -0
- backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
- backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
- backtrader/indicators/contrib/fisher_org_v1.py +102 -0
- backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
- backtrader/indicators/contrib/force_index_ema.py +96 -0
- backtrader/indicators/contrib/force_index_ema_2.py +27 -0
- backtrader/indicators/contrib/forecast_oscilator.py +145 -0
- backtrader/indicators/contrib/fractal_amambk.py +81 -0
- backtrader/indicators/contrib/frama_series.py +84 -0
- backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
- backtrader/indicators/contrib/go_indicator.py +93 -0
- backtrader/indicators/contrib/hlr_indicator.py +95 -0
- backtrader/indicators/contrib/hma.py +50 -0
- backtrader/indicators/contrib/i4_drfv2.py +34 -0
- backtrader/indicators/contrib/i4_drfv3.py +38 -0
- backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
- backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
- backtrader/indicators/contrib/i_gap_indicator.py +45 -0
- backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
- backtrader/indicators/contrib/i_trend_indicator.py +125 -0
- backtrader/indicators/contrib/iamma_indicator.py +39 -0
- backtrader/indicators/contrib/indexed_moving_average.py +33 -0
- backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
- backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
- backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
- backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
- backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
- backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
- backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
- backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
- backtrader/indicators/contrib/kalman_filter_line.py +127 -0
- backtrader/indicators/contrib/kama_indicator.py +150 -0
- backtrader/indicators/contrib/karacatica_indicator.py +99 -0
- backtrader/indicators/contrib/kdj_indicator.py +59 -0
- backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
- backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
- backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
- backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
- backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
- backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
- backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
- backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
- backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
- backtrader/indicators/contrib/loco_indicator.py +88 -0
- backtrader/indicators/contrib/lrma_indicator.py +185 -0
- backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
- backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
- backtrader/indicators/contrib/macd2_indicator.py +61 -0
- backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
- backtrader/indicators/contrib/malr_indicator.py +77 -0
- backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
- backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
- backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
- backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
- backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
- backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
- backtrader/indicators/contrib/nrtr_indicator.py +95 -0
- backtrader/indicators/contrib/p_channel_system.py +40 -0
- backtrader/indicators/contrib/percent_envelope.py +37 -0
- backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
- backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
- backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
- backtrader/indicators/contrib/price_extreme_channel.py +35 -0
- backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
- backtrader/indicators/contrib/ravi_indicator.py +40 -0
- backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
- backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
- backtrader/indicators/contrib/renko_level.py +85 -0
- backtrader/indicators/contrib/renko_line_break.py +91 -0
- backtrader/indicators/contrib/rftl_indicator.py +41 -0
- backtrader/indicators/contrib/rkd_indicator.py +53 -0
- backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
- backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
- backtrader/indicators/contrib/rsi_slowdown.py +57 -0
- backtrader/indicators/contrib/rsioma_v2.py +41 -0
- backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
- backtrader/indicators/contrib/safe_adx.py +89 -0
- backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
- backtrader/indicators/contrib/sidus_indicator.py +105 -0
- backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
- backtrader/indicators/contrib/sliding_range_color.py +56 -0
- backtrader/indicators/contrib/slow_stoch.py +42 -0
- backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
- backtrader/indicators/contrib/smoothed_rsi.py +31 -0
- backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
- backtrader/indicators/contrib/stalin_indicator.py +152 -0
- backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
- backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
- backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
- backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
- backtrader/indicators/contrib/t3_average.py +76 -0
- backtrader/indicators/contrib/t3_indicator.py +40 -0
- backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
- backtrader/indicators/contrib/three_candles_indicator.py +70 -0
- backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
- backtrader/indicators/contrib/time_line.py +57 -0
- backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
- backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
- backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
- backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
- backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
- backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
- backtrader/indicators/contrib/trigger_line.py +66 -0
- backtrader/indicators/contrib/triple_ema_rate.py +34 -0
- backtrader/indicators/contrib/trvi_indicator.py +194 -0
- backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
- backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
- backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
- backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
- backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
- backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
- backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
- backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
- backtrader/indicators/contrib/vwma_candle.py +57 -0
- backtrader/indicators/contrib/vwma_digit_system.py +70 -0
- backtrader/indicators/contrib/wami.py +43 -0
- backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
- backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
- backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
- backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
- backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
- backtrader/indicators/contrib/xma_ichimoku.py +163 -0
- backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
- backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
- backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
- backtrader/indicators/contrib/xmacd_indicator.py +70 -0
- backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
- backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
- backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
- backtrader/indicators/contrib/xrvi_indicator.py +130 -0
- backtrader/indicators/contrib/zero_lag_macd.py +36 -0
- backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
- backtrader/indicators/contrib/zpf_indicator.py +115 -0
- backtrader/indicators/crossover.py +337 -0
- backtrader/indicators/dema.py +175 -0
- backtrader/indicators/demarker.py +270 -0
- backtrader/indicators/deviation.py +284 -0
- backtrader/indicators/directionalmove.py +1071 -0
- backtrader/indicators/dma.py +112 -0
- backtrader/indicators/dpo.py +96 -0
- backtrader/indicators/dv2.py +56 -0
- backtrader/indicators/ema.py +145 -0
- backtrader/indicators/envelope.py +475 -0
- backtrader/indicators/hadelta.py +198 -0
- backtrader/indicators/heikinashi.py +153 -0
- backtrader/indicators/hma.py +153 -0
- backtrader/indicators/hurst.py +151 -0
- backtrader/indicators/ichimoku.py +267 -0
- backtrader/indicators/kama.py +181 -0
- backtrader/indicators/kst.py +159 -0
- backtrader/indicators/lrsi.py +125 -0
- backtrader/indicators/mabase.py +147 -0
- backtrader/indicators/macd.py +322 -0
- backtrader/indicators/momentum.py +267 -0
- backtrader/indicators/moneyflow.py +237 -0
- backtrader/indicators/mt5atr.py +124 -0
- backtrader/indicators/myind.py +179 -0
- backtrader/indicators/obv.py +94 -0
- backtrader/indicators/ols.py +265 -0
- backtrader/indicators/oscillator.py +161 -0
- backtrader/indicators/percentchange.py +83 -0
- backtrader/indicators/percentrank.py +46 -0
- backtrader/indicators/pivotpoint.py +469 -0
- backtrader/indicators/prettygoodoscillator.py +113 -0
- backtrader/indicators/priceops_ext.py +123 -0
- backtrader/indicators/priceoscillator.py +262 -0
- backtrader/indicators/psar.py +212 -0
- backtrader/indicators/rmi.py +69 -0
- backtrader/indicators/rsi.py +440 -0
- backtrader/indicators/sma.py +141 -0
- backtrader/indicators/smma.py +116 -0
- backtrader/indicators/spread.py +54 -0
- backtrader/indicators/stochastic.py +263 -0
- backtrader/indicators/supertrend.py +436 -0
- backtrader/indicators/trend_ext.py +105 -0
- backtrader/indicators/trix.py +202 -0
- backtrader/indicators/tsi.py +155 -0
- backtrader/indicators/ultimateoscillator.py +158 -0
- backtrader/indicators/vortex.py +62 -0
- backtrader/indicators/williams.py +194 -0
- backtrader/indicators/wma.py +103 -0
- backtrader/indicators/zlema.py +135 -0
- backtrader/indicators/zlind.py +104 -0
- backtrader/linebuffer.py +3155 -0
- backtrader/lineiterator.py +2911 -0
- backtrader/lineroot.py +1106 -0
- backtrader/lineseries.py +2559 -0
- backtrader/live_trading/__init__.py +31 -0
- backtrader/live_trading/interface.py +404 -0
- backtrader/mathsupport.py +94 -0
- backtrader/metabase.py +1804 -0
- backtrader/mixins/__init__.py +21 -0
- backtrader/mixins/singleton.py +118 -0
- backtrader/observer.py +106 -0
- backtrader/observers/__init__.py +45 -0
- backtrader/observers/benchmark.py +126 -0
- backtrader/observers/broker.py +184 -0
- backtrader/observers/buysell.py +144 -0
- backtrader/observers/drawdown.py +161 -0
- backtrader/observers/logreturns.py +113 -0
- backtrader/observers/timereturn.py +86 -0
- backtrader/observers/trade_logger.py +2972 -0
- backtrader/observers/tradelogger.py +6 -0
- backtrader/observers/trades.py +258 -0
- backtrader/order.py +1114 -0
- backtrader/parameters.py +2345 -0
- backtrader/plot/__init__.py +54 -0
- backtrader/plot/finance.py +1022 -0
- backtrader/plot/formatters.py +200 -0
- backtrader/plot/locator.py +353 -0
- backtrader/plot/multicursor.py +495 -0
- backtrader/plot/plot.py +2500 -0
- backtrader/plot/plot_plotly.py +1351 -0
- backtrader/plot/scheme.py +253 -0
- backtrader/plot/utils.py +104 -0
- backtrader/position.py +290 -0
- backtrader/position_modes.py +132 -0
- backtrader/profiles.py +254 -0
- backtrader/reports/__init__.py +39 -0
- backtrader/reports/charts.py +371 -0
- backtrader/reports/performance.py +620 -0
- backtrader/reports/reporter.py +660 -0
- backtrader/resamplerfilter.py +1001 -0
- backtrader/signal.py +118 -0
- backtrader/signals/__init__.py +17 -0
- backtrader/sizer.py +114 -0
- backtrader/sizers/__init__.py +26 -0
- backtrader/sizers/fixedsize.py +161 -0
- backtrader/sizers/percents_sizer.py +119 -0
- backtrader/store.py +221 -0
- backtrader/stores/__init__.py +33 -0
- backtrader/stores/btapistore.py +15506 -0
- backtrader/stores/livestore.py +137 -0
- backtrader/stores/vchartfile.py +96 -0
- backtrader/strategy.py +3655 -0
- backtrader/talib.py +280 -0
- backtrader/test_helpers.py +96 -0
- backtrader/timer.py +358 -0
- backtrader/trade.py +442 -0
- backtrader/tradingcal.py +361 -0
- backtrader/utils/__init__.py +68 -0
- backtrader/utils/autodict.py +251 -0
- backtrader/utils/date.py +71 -0
- backtrader/utils/dateintern.py +509 -0
- backtrader/utils/flushfile.py +94 -0
- backtrader/utils/fractal.py +101 -0
- backtrader/utils/get_metrics.py +101 -0
- backtrader/utils/load_data.py +209 -0
- backtrader/utils/log_message.py +998 -0
- backtrader/utils/ordereddefaultdict.py +75 -0
- backtrader/utils/py3.py +296 -0
- backtrader/version.py +21 -0
- backtrader/writer.py +372 -0
backtrader/linebuffer.py
ADDED
|
@@ -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
|