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/metabase.py
ADDED
|
@@ -0,0 +1,1804 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Base classes and mixins for the Backtrader framework.
|
|
3
|
+
|
|
4
|
+
This module provides the foundational infrastructure that replaces the original
|
|
5
|
+
metaclass-based design. It includes parameter management, object factories,
|
|
6
|
+
and various mixin classes used throughout the framework.
|
|
7
|
+
|
|
8
|
+
Key Components:
|
|
9
|
+
- **ObjectFactory**: Factory class for creating objects with lifecycle hooks
|
|
10
|
+
- **BaseMixin**: Base mixin providing factory-based object creation
|
|
11
|
+
- **ParamsMixin**: Mixin for parameter management without metaclasses
|
|
12
|
+
- **AutoInfoClass**: Dynamic class for parameter/info storage
|
|
13
|
+
- **ParameterManager**: Static utility for handling parameter operations
|
|
14
|
+
- **ItemCollection**: Collection class with index and name-based access
|
|
15
|
+
|
|
16
|
+
Utility Functions:
|
|
17
|
+
- findbases: Recursively find base classes of a given type
|
|
18
|
+
- findowner: Search call stack for owner objects
|
|
19
|
+
- is_class_type: Cached type checking via MRO inspection
|
|
20
|
+
- patch_strategy_clk_update: Runtime patch for Strategy clock updates
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
Creating a class with parameters::
|
|
24
|
+
|
|
25
|
+
class MyIndicator(ParamsMixin):
|
|
26
|
+
params = (('period', 20), ('multiplier', 2.0))
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
print(f"Period: {self.p.period}")
|
|
30
|
+
|
|
31
|
+
Note:
|
|
32
|
+
This module was created during the metaclass removal refactoring to provide
|
|
33
|
+
equivalent functionality using explicit initialization patterns.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import math
|
|
37
|
+
import sys
|
|
38
|
+
import threading
|
|
39
|
+
from collections import OrderedDict
|
|
40
|
+
from contextlib import contextmanager
|
|
41
|
+
|
|
42
|
+
from .parameters import LegacyParamsSchema, make_legacy_parameter_accessor
|
|
43
|
+
from .utils.log_message import get_logger
|
|
44
|
+
from .utils.py3 import string_types, zip
|
|
45
|
+
|
|
46
|
+
logger = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
# PERFORMANCE OPTIMIZATION: Cache for MRO type checks
|
|
49
|
+
# This avoids repeatedly traversing __mro__ for the same classes
|
|
50
|
+
_type_check_cache: dict = {}
|
|
51
|
+
|
|
52
|
+
# PERFORMANCE OPTIMIZATION: One-time guard for indicator alias initialization
|
|
53
|
+
_INDICATOR_ALIASES_INITIALIZED = False
|
|
54
|
+
|
|
55
|
+
# Thread-local storage for owner context
|
|
56
|
+
# This replaces sys._getframe() based owner lookup with explicit context management
|
|
57
|
+
_owner_context = threading.local()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OwnerContext:
|
|
61
|
+
"""Context manager for tracking owner objects during indicator creation.
|
|
62
|
+
|
|
63
|
+
This class provides an alternative to sys._getframe() based owner lookup
|
|
64
|
+
by maintaining an explicit owner stack in thread-local storage.
|
|
65
|
+
|
|
66
|
+
Usage:
|
|
67
|
+
with OwnerContext.set_owner(strategy):
|
|
68
|
+
# All indicators created here will have strategy as their owner
|
|
69
|
+
sma = SMA(data, period=20)
|
|
70
|
+
|
|
71
|
+
The owner stack allows nested contexts, so indicators creating sub-indicators
|
|
72
|
+
will correctly assign ownership.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def get_current_owner(cls_filter=None):
|
|
77
|
+
"""Get the current owner from the context stack.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
cls_filter: Optional class type to filter owners. If provided,
|
|
81
|
+
only returns an owner that is an instance of this class.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The current owner object, or None if no owner is set or
|
|
85
|
+
no owner matches the filter.
|
|
86
|
+
"""
|
|
87
|
+
stack = getattr(_owner_context, "owner_stack", None)
|
|
88
|
+
if not stack:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Return the topmost owner matching the filter
|
|
92
|
+
for owner in reversed(stack):
|
|
93
|
+
if cls_filter is None or isinstance(owner, cls_filter):
|
|
94
|
+
return owner
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
@contextmanager
|
|
99
|
+
def set_owner(owner):
|
|
100
|
+
"""Set the current owner for indicator creation.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
owner: The owner object (typically a Strategy or Indicator).
|
|
104
|
+
|
|
105
|
+
Yields:
|
|
106
|
+
None. The owner is available via get_current_owner() within the context.
|
|
107
|
+
"""
|
|
108
|
+
if not hasattr(_owner_context, "owner_stack"):
|
|
109
|
+
_owner_context.owner_stack = []
|
|
110
|
+
|
|
111
|
+
_owner_context.owner_stack.append(owner)
|
|
112
|
+
try:
|
|
113
|
+
yield
|
|
114
|
+
finally:
|
|
115
|
+
_owner_context.owner_stack.pop()
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def push_owner(owner):
|
|
119
|
+
"""Push an owner onto the stack (non-context-manager version).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
owner: The owner object to push.
|
|
123
|
+
"""
|
|
124
|
+
if not hasattr(_owner_context, "owner_stack"):
|
|
125
|
+
_owner_context.owner_stack = []
|
|
126
|
+
_owner_context.owner_stack.append(owner)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def pop_owner():
|
|
130
|
+
"""Pop the current owner from the stack.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The popped owner, or None if the stack was empty.
|
|
134
|
+
"""
|
|
135
|
+
stack = getattr(_owner_context, "owner_stack", None)
|
|
136
|
+
if stack:
|
|
137
|
+
return stack.pop()
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def clear():
|
|
142
|
+
"""Clear the owner stack (useful for testing)."""
|
|
143
|
+
if hasattr(_owner_context, "owner_stack"):
|
|
144
|
+
_owner_context.owner_stack.clear()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_class_type(cls, type_name):
|
|
148
|
+
"""
|
|
149
|
+
OPTIMIZED: Check if a class is of a certain type by checking __mro__.
|
|
150
|
+
Results are cached for better performance.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
cls: The class to check
|
|
154
|
+
type_name: The type name to look for (e.g., 'Strategy', 'Indicator')
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
bool: True if the class has the type in its MRO
|
|
158
|
+
"""
|
|
159
|
+
cache_key = (id(cls), type_name)
|
|
160
|
+
if cache_key in _type_check_cache:
|
|
161
|
+
return _type_check_cache[cache_key]
|
|
162
|
+
|
|
163
|
+
# Check the class name and all base classes
|
|
164
|
+
result = type_name in cls.__name__ or any(type_name in base.__name__ for base in cls.__mro__)
|
|
165
|
+
_type_check_cache[cache_key] = result
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def patch_strategy_clk_update():
|
|
170
|
+
"""
|
|
171
|
+
CRITICAL FIX: Patch the Strategy class's _clk_update method to prevent
|
|
172
|
+
the "max() iterable argument is empty" error that occurs when no data
|
|
173
|
+
sources have any length yet.
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
from .strategy import Strategy
|
|
177
|
+
|
|
178
|
+
def safe_clk_update(self):
|
|
179
|
+
"""CRITICAL FIX: Safe _clk_update method that handles empty data sources"""
|
|
180
|
+
|
|
181
|
+
# CRITICAL FIX: Ensure data is available before clock operations
|
|
182
|
+
if getattr(self, "_data_assignment_pending", True) and (
|
|
183
|
+
not hasattr(self, "datas") or not self.datas
|
|
184
|
+
):
|
|
185
|
+
# Try to get data assignment from cerebro if not already done
|
|
186
|
+
if hasattr(self, "_ensure_data_available"):
|
|
187
|
+
self._ensure_data_available()
|
|
188
|
+
|
|
189
|
+
# CRITICAL FIX: Handle the old sync method safely
|
|
190
|
+
if hasattr(self, "_oldsync") and self._oldsync:
|
|
191
|
+
# Call parent class _clk_update if available
|
|
192
|
+
try:
|
|
193
|
+
# Use the parent class method from StrategyBase if available
|
|
194
|
+
from .lineiterator import StrategyBase
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
hasattr(StrategyBase, "_clk_update")
|
|
198
|
+
and StrategyBase._clk_update != safe_clk_update
|
|
199
|
+
):
|
|
200
|
+
clk_len = StrategyBase._clk_update(self)
|
|
201
|
+
else:
|
|
202
|
+
clk_len = 1
|
|
203
|
+
except Exception:
|
|
204
|
+
logger.debug("metabase:203 fallback on Exception")
|
|
205
|
+
clk_len = 1
|
|
206
|
+
|
|
207
|
+
# CRITICAL FIX: Only set datetime if we have valid data sources with length
|
|
208
|
+
if hasattr(self, "datas") and self.datas:
|
|
209
|
+
valid_data_times = []
|
|
210
|
+
for d in self.datas:
|
|
211
|
+
try:
|
|
212
|
+
if (
|
|
213
|
+
len(d) > 0
|
|
214
|
+
and hasattr(d, "datetime")
|
|
215
|
+
and hasattr(d.datetime, "__getitem__")
|
|
216
|
+
):
|
|
217
|
+
dt_val = d.datetime[0]
|
|
218
|
+
# Only add valid datetime values (not None or NaN)
|
|
219
|
+
if dt_val is not None and not (
|
|
220
|
+
isinstance(dt_val, float) and math.isnan(dt_val)
|
|
221
|
+
):
|
|
222
|
+
valid_data_times.append(dt_val)
|
|
223
|
+
except (IndexError, AttributeError, TypeError):
|
|
224
|
+
logger.debug("metabase:223 ignored IndexError,AttributeError,TypeError")
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
valid_data_times
|
|
229
|
+
and hasattr(self, "lines")
|
|
230
|
+
and hasattr(self.lines, "datetime")
|
|
231
|
+
):
|
|
232
|
+
try:
|
|
233
|
+
self.lines.datetime[0] = max(valid_data_times)
|
|
234
|
+
except (ValueError, IndexError, AttributeError):
|
|
235
|
+
# If setting datetime fails, use a default valid ordinal (1 = Jan 1, Year 1)
|
|
236
|
+
self.lines.datetime[0] = 1.0
|
|
237
|
+
elif hasattr(self, "lines") and hasattr(self.lines, "datetime"):
|
|
238
|
+
# No valid times, use default valid ordinal (1 = Jan 1, Year 1)
|
|
239
|
+
self.lines.datetime[0] = 1.0
|
|
240
|
+
|
|
241
|
+
return clk_len
|
|
242
|
+
|
|
243
|
+
# CRITICAL FIX: Handle the normal (non-oldsync) path
|
|
244
|
+
# Initialize _dlens if not present
|
|
245
|
+
if not hasattr(self, "_dlens"):
|
|
246
|
+
self._dlens = [
|
|
247
|
+
len(d) if hasattr(d, "__len__") else 0
|
|
248
|
+
for d in (self.datas if hasattr(self, "datas") else [])
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
# Get current data lengths safely
|
|
252
|
+
if hasattr(self, "datas") and self.datas:
|
|
253
|
+
newdlens = []
|
|
254
|
+
for d in self.datas:
|
|
255
|
+
try:
|
|
256
|
+
newdlens.append(len(d) if hasattr(d, "__len__") else 0)
|
|
257
|
+
except Exception:
|
|
258
|
+
logger.debug("metabase:256 fallback on Exception")
|
|
259
|
+
newdlens.append(0)
|
|
260
|
+
else:
|
|
261
|
+
newdlens = []
|
|
262
|
+
|
|
263
|
+
# Forward if any data source has grown
|
|
264
|
+
if (
|
|
265
|
+
newdlens
|
|
266
|
+
and hasattr(self, "_dlens")
|
|
267
|
+
and any(
|
|
268
|
+
nl > old_len
|
|
269
|
+
for old_len, nl in zip(self._dlens, newdlens)
|
|
270
|
+
if old_len is not None and nl is not None
|
|
271
|
+
)
|
|
272
|
+
):
|
|
273
|
+
try:
|
|
274
|
+
if hasattr(self, "forward"):
|
|
275
|
+
self.forward()
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.debug("Failed to forward in _clk_update: %s", e)
|
|
278
|
+
|
|
279
|
+
# Update _dlens
|
|
280
|
+
self._dlens = newdlens
|
|
281
|
+
|
|
282
|
+
# CRITICAL FIX: Set datetime safely - only use data sources that have valid data
|
|
283
|
+
if (
|
|
284
|
+
hasattr(self, "datas")
|
|
285
|
+
and self.datas
|
|
286
|
+
and hasattr(self, "lines")
|
|
287
|
+
and hasattr(self.lines, "datetime")
|
|
288
|
+
):
|
|
289
|
+
valid_data_times = []
|
|
290
|
+
for d in self.datas:
|
|
291
|
+
try:
|
|
292
|
+
if (
|
|
293
|
+
len(d) > 0
|
|
294
|
+
and hasattr(d, "datetime")
|
|
295
|
+
and hasattr(d.datetime, "__getitem__")
|
|
296
|
+
):
|
|
297
|
+
dt_val = d.datetime[0]
|
|
298
|
+
# Only add valid datetime values (not None or NaN)
|
|
299
|
+
if dt_val is not None and not (
|
|
300
|
+
isinstance(dt_val, float) and math.isnan(dt_val)
|
|
301
|
+
):
|
|
302
|
+
valid_data_times.append(dt_val)
|
|
303
|
+
except (IndexError, AttributeError, TypeError):
|
|
304
|
+
logger.debug("metabase:301 ignored IndexError,AttributeError,TypeError")
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
if valid_data_times:
|
|
308
|
+
try:
|
|
309
|
+
self.lines.datetime[0] = max(valid_data_times)
|
|
310
|
+
except (ValueError, IndexError, AttributeError):
|
|
311
|
+
# If setting datetime fails, use a default valid ordinal (1 = Jan 1, Year 1)
|
|
312
|
+
self.lines.datetime[0] = 1.0
|
|
313
|
+
else:
|
|
314
|
+
# CRITICAL FIX: Use valid ordinal instead of 0.0
|
|
315
|
+
# 1.0 corresponds to January 1, year 1 in the proleptic Gregorian calendar
|
|
316
|
+
self.lines.datetime[0] = 1.0
|
|
317
|
+
|
|
318
|
+
# Return the length of this strategy (number of processed bars)
|
|
319
|
+
try:
|
|
320
|
+
return len(self)
|
|
321
|
+
except Exception:
|
|
322
|
+
logger.debug("metabase:319 fallback on Exception")
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
# Monkey patch the Strategy class
|
|
326
|
+
Strategy._clk_update = safe_clk_update
|
|
327
|
+
|
|
328
|
+
except ImportError:
|
|
329
|
+
# Silently ignore - Strategy already has _clk_update method
|
|
330
|
+
logger.warning("metabase:325 suppressed ImportError")
|
|
331
|
+
except Exception: # nosec B110
|
|
332
|
+
# Silently ignore - Strategy already has _clk_update method
|
|
333
|
+
logger.warning("metabase:328 suppressed Exception")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def findbases(kls, topclass):
|
|
337
|
+
"""Recursively find all base classes that inherit from topclass.
|
|
338
|
+
|
|
339
|
+
This function traverses the class hierarchy using __bases__ and recursively
|
|
340
|
+
collects all base classes that are subclasses of the specified topclass.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
kls: The class to search bases for.
|
|
344
|
+
topclass: The top-level class to filter by (only bases that are
|
|
345
|
+
subclasses of this class are included).
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
list: A list of base classes in order from most ancestral to most
|
|
349
|
+
immediate parent.
|
|
350
|
+
|
|
351
|
+
Note:
|
|
352
|
+
This function uses recursion, but the depth is limited by Python's
|
|
353
|
+
recursion limit. In practice, class hierarchies rarely exceed this.
|
|
354
|
+
"""
|
|
355
|
+
retval = []
|
|
356
|
+
for base in kls.__bases__:
|
|
357
|
+
if issubclass(base, topclass):
|
|
358
|
+
retval.extend(findbases(base, topclass))
|
|
359
|
+
retval.append(base)
|
|
360
|
+
return retval
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def findowner(owned, cls, startlevel=2, skip=None):
|
|
364
|
+
"""Find the owner object in the call stack or context.
|
|
365
|
+
|
|
366
|
+
This function first checks the OwnerContext for an explicitly set owner,
|
|
367
|
+
then falls back to traversing the call stack to find an object that:
|
|
368
|
+
1. Is an instance of the specified class (cls)
|
|
369
|
+
2. Is not the owned object itself
|
|
370
|
+
3. Is not the skip object (if provided)
|
|
371
|
+
|
|
372
|
+
This is commonly used to find parent containers (e.g., Strategy finding
|
|
373
|
+
its Cerebro, or Indicator finding its Strategy).
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
owned: The object looking for its owner.
|
|
377
|
+
cls: The class type the owner must be an instance of.
|
|
378
|
+
startlevel: Stack frame level to start searching from (default: 2,
|
|
379
|
+
skips this function and the caller).
|
|
380
|
+
skip: Optional object to skip during the search.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
The owner object if found, None otherwise.
|
|
384
|
+
|
|
385
|
+
Note:
|
|
386
|
+
Uses OwnerContext for explicit owner management. The legacy sys._getframe()
|
|
387
|
+
based lookup has been removed for better portability and performance.
|
|
388
|
+
"""
|
|
389
|
+
# Check OwnerContext for explicit owner management
|
|
390
|
+
# This is the only method now - no stack frame inspection
|
|
391
|
+
context_owner = OwnerContext.get_current_owner(cls)
|
|
392
|
+
if context_owner is not None:
|
|
393
|
+
if context_owner is not owned and context_owner is not skip:
|
|
394
|
+
return context_owner
|
|
395
|
+
|
|
396
|
+
# No owner found in context
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class ObjectFactory:
|
|
401
|
+
"""Factory class to replace MetaBase functionality.
|
|
402
|
+
|
|
403
|
+
This class provides a static method for creating objects with lifecycle
|
|
404
|
+
hooks similar to the original metaclass implementation.
|
|
405
|
+
|
|
406
|
+
The creation process follows these steps:
|
|
407
|
+
1. doprenew: Pre-new processing (class modification)
|
|
408
|
+
2. donew: Object creation
|
|
409
|
+
3. dopreinit: Pre-initialization processing
|
|
410
|
+
4. doinit: Main initialization
|
|
411
|
+
5. dopostinit: Post-initialization processing
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
@staticmethod
|
|
415
|
+
def create(cls, *args, **kwargs):
|
|
416
|
+
"""Create an object with lifecycle hooks.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
cls: The class to instantiate.
|
|
420
|
+
*args: Positional arguments for initialization.
|
|
421
|
+
**kwargs: Keyword arguments for initialization.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
The created and initialized object.
|
|
425
|
+
"""
|
|
426
|
+
# Pre-new processing
|
|
427
|
+
if hasattr(cls, "doprenew"):
|
|
428
|
+
cls, args, kwargs = cls.doprenew(*args, **kwargs)
|
|
429
|
+
|
|
430
|
+
# Object creation
|
|
431
|
+
if hasattr(cls, "donew"):
|
|
432
|
+
_obj, args, kwargs = cls.donew(*args, **kwargs)
|
|
433
|
+
else:
|
|
434
|
+
_obj = cls.__new__(cls)
|
|
435
|
+
|
|
436
|
+
# Pre-init processing
|
|
437
|
+
if hasattr(cls, "dopreinit"):
|
|
438
|
+
_obj, args, kwargs = cls.dopreinit(_obj, *args, **kwargs)
|
|
439
|
+
|
|
440
|
+
# Main initialization
|
|
441
|
+
if hasattr(cls, "doinit"):
|
|
442
|
+
_obj, args, kwargs = cls.doinit(_obj, *args, **kwargs)
|
|
443
|
+
else:
|
|
444
|
+
_obj.__init__(*args, **kwargs)
|
|
445
|
+
|
|
446
|
+
# Post-init processing
|
|
447
|
+
if hasattr(cls, "dopostinit"):
|
|
448
|
+
_obj, args, kwargs = cls.dopostinit(_obj, *args, **kwargs)
|
|
449
|
+
|
|
450
|
+
return _obj
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class BaseMixin:
|
|
454
|
+
"""Mixin providing factory-based object creation without metaclass.
|
|
455
|
+
|
|
456
|
+
This mixin provides default implementations for the lifecycle hooks
|
|
457
|
+
used by ObjectFactory. Subclasses can override these methods to
|
|
458
|
+
customize object creation and initialization.
|
|
459
|
+
|
|
460
|
+
Methods:
|
|
461
|
+
doprenew: Called before object creation (class-level).
|
|
462
|
+
donew: Creates the object instance.
|
|
463
|
+
dopreinit: Called before __init__.
|
|
464
|
+
doinit: Calls __init__ on the object.
|
|
465
|
+
dopostinit: Called after __init__.
|
|
466
|
+
create: Factory method for instance creation.
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
@classmethod
|
|
470
|
+
def doprenew(cls, *args, **kwargs):
|
|
471
|
+
"""Called before object creation.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
*args: Positional arguments for object creation.
|
|
475
|
+
**kwargs: Keyword arguments for object creation.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
tuple: (cls, args, kwargs) - Class and arguments to use.
|
|
479
|
+
"""
|
|
480
|
+
return cls, args, kwargs
|
|
481
|
+
|
|
482
|
+
@classmethod
|
|
483
|
+
def donew(cls, *args, **kwargs):
|
|
484
|
+
"""Create a new object instance.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
*args: Positional arguments for object creation.
|
|
488
|
+
**kwargs: Keyword arguments for object creation.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
tuple: (_obj, args, kwargs) - New instance and remaining arguments.
|
|
492
|
+
"""
|
|
493
|
+
_obj = cls.__new__(cls)
|
|
494
|
+
return _obj, args, kwargs
|
|
495
|
+
|
|
496
|
+
@classmethod
|
|
497
|
+
def dopreinit(cls, _obj, *args, **kwargs):
|
|
498
|
+
"""Called before __init__ to modify arguments.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
_obj: The object instance.
|
|
502
|
+
*args: Positional arguments for __init__.
|
|
503
|
+
**kwargs: Keyword arguments for __init__.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
tuple: (_obj, args, kwargs) - Object and arguments for __init__.
|
|
507
|
+
"""
|
|
508
|
+
return _obj, args, kwargs
|
|
509
|
+
|
|
510
|
+
@classmethod
|
|
511
|
+
def doinit(cls, _obj, *args, **kwargs):
|
|
512
|
+
"""Call __init__ on the object.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
_obj: The object instance.
|
|
516
|
+
*args: Positional arguments for __init__.
|
|
517
|
+
**kwargs: Keyword arguments for __init__.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
tuple: (_obj, args, kwargs) - Object and remaining arguments.
|
|
521
|
+
"""
|
|
522
|
+
_obj.__init__(*args, **kwargs)
|
|
523
|
+
return _obj, args, kwargs
|
|
524
|
+
|
|
525
|
+
@classmethod
|
|
526
|
+
def dopostinit(cls, _obj, *args, **kwargs):
|
|
527
|
+
"""Called after __init__ for post-processing.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
_obj: The object instance.
|
|
531
|
+
*args: Remaining positional arguments.
|
|
532
|
+
**kwargs: Remaining keyword arguments.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
tuple: (_obj, args, kwargs) - Object and remaining arguments.
|
|
536
|
+
"""
|
|
537
|
+
return _obj, args, kwargs
|
|
538
|
+
|
|
539
|
+
@classmethod
|
|
540
|
+
def create(cls, *args, **kwargs):
|
|
541
|
+
"""Factory method to create instances"""
|
|
542
|
+
return ObjectFactory.create(cls, *args, **kwargs)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class AutoInfoClass:
|
|
546
|
+
"""Dynamic class for storing parameter and info key-value pairs.
|
|
547
|
+
|
|
548
|
+
This class provides a flexible mechanism for storing and retrieving
|
|
549
|
+
configuration data (parameters, plot info, etc.) with support for
|
|
550
|
+
inheritance and derivation.
|
|
551
|
+
|
|
552
|
+
Class Methods:
|
|
553
|
+
_getpairsbase: Get base class pairs as OrderedDict.
|
|
554
|
+
_getpairs: Get all pairs (including inherited) as OrderedDict.
|
|
555
|
+
_getrecurse: Check if recursive derivation is enabled.
|
|
556
|
+
_derive: Create a derived class with additional parameters.
|
|
557
|
+
_getkeys: Get all parameter keys.
|
|
558
|
+
_getdefaults: Get all default values.
|
|
559
|
+
_getitems: Get all key-value pairs.
|
|
560
|
+
_gettuple: Get pairs as tuple of tuples.
|
|
561
|
+
|
|
562
|
+
Instance Methods:
|
|
563
|
+
isdefault: Check if a parameter has its default value.
|
|
564
|
+
notdefault: Check if a parameter differs from default.
|
|
565
|
+
get/_get: Get a parameter value with optional default.
|
|
566
|
+
"""
|
|
567
|
+
|
|
568
|
+
# Class methods returning empty defaults - equivalent to:
|
|
569
|
+
# @classmethod
|
|
570
|
+
# def _getpairsbase(cls): return OrderedDict()
|
|
571
|
+
_getpairsbase: classmethod = classmethod(lambda cls: OrderedDict())
|
|
572
|
+
_getpairs: classmethod = classmethod(lambda cls: OrderedDict())
|
|
573
|
+
_getrecurse = classmethod(lambda cls: False)
|
|
574
|
+
|
|
575
|
+
@classmethod
|
|
576
|
+
def _derive(cls, name, info, otherbases, recurse=False):
|
|
577
|
+
"""Create a derived class with merged parameters.
|
|
578
|
+
|
|
579
|
+
This method creates a new class that inherits from the current class
|
|
580
|
+
and includes parameters from both the base class and additional sources.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
cls: The base class to derive from.
|
|
584
|
+
name: Name suffix for the new class.
|
|
585
|
+
info: New parameters to add (dict or tuple of tuples).
|
|
586
|
+
otherbases: Additional base classes or parameter dicts to merge.
|
|
587
|
+
recurse: If True, recursively derive nested parameter classes.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
A new class with merged parameters.
|
|
591
|
+
|
|
592
|
+
Example:
|
|
593
|
+
DerivedParams = BaseParams._derive('MyStrategy', newparams, morebasesparams)
|
|
594
|
+
"""
|
|
595
|
+
# Collect the 3 sets of info: base class, other bases, and new info
|
|
596
|
+
baseinfo = cls._getpairs().copy() # Shallow copy to preserve base class params
|
|
597
|
+
obasesinfo: dict = OrderedDict() # Parameters from other base classes
|
|
598
|
+
for obase in otherbases:
|
|
599
|
+
# If otherbases contains dicts/tuples, update directly
|
|
600
|
+
# Otherwise, get params from class instances via _getpairs()
|
|
601
|
+
if isinstance(obase, (tuple, dict)):
|
|
602
|
+
obasesinfo.update(obase)
|
|
603
|
+
else:
|
|
604
|
+
obasesinfo.update(obase._getpairs())
|
|
605
|
+
|
|
606
|
+
# Update base info with parameters from other bases
|
|
607
|
+
baseinfo.update(obasesinfo)
|
|
608
|
+
|
|
609
|
+
# Create final class info: base + otherbases + new params
|
|
610
|
+
clsinfo = baseinfo.copy()
|
|
611
|
+
clsinfo.update(info)
|
|
612
|
+
|
|
613
|
+
# Items to add: otherbases + new params (excluding base class params)
|
|
614
|
+
info2add = obasesinfo.copy()
|
|
615
|
+
info2add.update(info)
|
|
616
|
+
|
|
617
|
+
# Create new derived class and register in module
|
|
618
|
+
clsmodule = sys.modules[cls.__module__]
|
|
619
|
+
newclsname = str(cls.__name__ + "_" + name) # str - Python 2/3 compat
|
|
620
|
+
|
|
621
|
+
# This loop makes sure that if the name has already been defined, a new
|
|
622
|
+
# unique name is found. A collision example is in the plotlines names
|
|
623
|
+
# definitions of bt.indicators.MACD and bt.talib.MACD. Both end up
|
|
624
|
+
# definining a MACD_pl_macd and this makes it impossible for the pickle
|
|
625
|
+
# module to send results over a multiprocessing channel
|
|
626
|
+
namecounter = 1
|
|
627
|
+
while hasattr(clsmodule, newclsname):
|
|
628
|
+
newclsname += str(namecounter)
|
|
629
|
+
namecounter += 1
|
|
630
|
+
|
|
631
|
+
newcls = type(newclsname, (cls,), {})
|
|
632
|
+
setattr(clsmodule, newclsname, newcls)
|
|
633
|
+
# Set up class methods to return baseinfo, clsinfo, and recurse values
|
|
634
|
+
setattr(newcls, "_getpairsbase", classmethod(lambda cls: baseinfo.copy()))
|
|
635
|
+
setattr(newcls, "_getpairs", classmethod(lambda cls: clsinfo.copy()))
|
|
636
|
+
setattr(newcls, "_getrecurse", classmethod(lambda cls: recurse))
|
|
637
|
+
|
|
638
|
+
for infoname, infoval in info2add.items():
|
|
639
|
+
# If recurse is True, recursively derive nested info classes
|
|
640
|
+
# This is rarely used in practice
|
|
641
|
+
if recurse:
|
|
642
|
+
recursecls = getattr(newcls, infoname, AutoInfoClass)
|
|
643
|
+
infoval = recursecls._derive(name + "_" + infoname, infoval, [])
|
|
644
|
+
# Set the info attribute on the new class
|
|
645
|
+
setattr(newcls, infoname, infoval)
|
|
646
|
+
|
|
647
|
+
return newcls
|
|
648
|
+
|
|
649
|
+
def isdefault(self, pname):
|
|
650
|
+
"""Check if a parameter has its default value."""
|
|
651
|
+
return self._get(pname) == self._getkwargsdefault()[pname]
|
|
652
|
+
|
|
653
|
+
def notdefault(self, pname):
|
|
654
|
+
"""Check if a parameter differs from its default value."""
|
|
655
|
+
return self._get(pname) != self._getkwargsdefault()[pname]
|
|
656
|
+
|
|
657
|
+
def _get(self, name, default=None):
|
|
658
|
+
"""Get attribute value by name with optional default."""
|
|
659
|
+
return getattr(self, name, default)
|
|
660
|
+
|
|
661
|
+
def get(self, name, default=None):
|
|
662
|
+
"""Get a parameter value by name with optional default.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
name: Name of the parameter to get.
|
|
666
|
+
default: Default value if parameter is not found.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
The parameter value or default if not found.
|
|
670
|
+
"""
|
|
671
|
+
return self._get(name, default)
|
|
672
|
+
|
|
673
|
+
@classmethod
|
|
674
|
+
def _getkwargsdefault(cls):
|
|
675
|
+
"""Get default parameter values as OrderedDict."""
|
|
676
|
+
return cls._getpairs()
|
|
677
|
+
|
|
678
|
+
@classmethod
|
|
679
|
+
def _getkeys(cls):
|
|
680
|
+
"""Get all parameter keys."""
|
|
681
|
+
return cls._getpairs().keys()
|
|
682
|
+
|
|
683
|
+
@classmethod
|
|
684
|
+
def _getdefaults(cls):
|
|
685
|
+
"""Get all default parameter values as list."""
|
|
686
|
+
return list(cls._getpairs().values())
|
|
687
|
+
|
|
688
|
+
@classmethod
|
|
689
|
+
def _getitems(cls):
|
|
690
|
+
"""Get all key-value pairs as items view."""
|
|
691
|
+
return cls._getpairs().items()
|
|
692
|
+
|
|
693
|
+
@classmethod
|
|
694
|
+
def _gettuple(cls):
|
|
695
|
+
"""Get all key-value pairs as tuple of tuples."""
|
|
696
|
+
return tuple(cls._getpairs().items())
|
|
697
|
+
|
|
698
|
+
def _getkwargs(self, skip_=False):
|
|
699
|
+
"""Get current parameter values as OrderedDict."""
|
|
700
|
+
pairs = [
|
|
701
|
+
(x, getattr(self, x)) for x in self._getkeys() if not skip_ or not x.startswith("_")
|
|
702
|
+
]
|
|
703
|
+
return OrderedDict(pairs)
|
|
704
|
+
|
|
705
|
+
def _getvalues(self):
|
|
706
|
+
"""Get all current parameter values as list."""
|
|
707
|
+
return [getattr(self, x) for x in self._getkeys()]
|
|
708
|
+
|
|
709
|
+
def __new__(cls, *args, **kwargs):
|
|
710
|
+
"""Create a new instance with recursive parameter initialization."""
|
|
711
|
+
obj = super().__new__(cls, *args, **kwargs)
|
|
712
|
+
|
|
713
|
+
if cls._getrecurse():
|
|
714
|
+
for infoname in obj._getkeys():
|
|
715
|
+
recursecls = getattr(cls, infoname)
|
|
716
|
+
setattr(obj, infoname, recursecls())
|
|
717
|
+
|
|
718
|
+
return obj
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _merge_class_params_into(all_params, params):
|
|
722
|
+
"""Merge a class's ``params`` declaration into the ``all_params`` dict.
|
|
723
|
+
|
|
724
|
+
``params`` may be a dict, a tuple/list of (name, value) pairs (or bare
|
|
725
|
+
string names), a dict-like with ``items()``, an object with ``__dict__``,
|
|
726
|
+
or an AutoInfoClass-style object exposing ``_getpairs``/``_gettuple``.
|
|
727
|
+
Extracted from ParameterManager._derive_params for readability; behavior
|
|
728
|
+
is unchanged.
|
|
729
|
+
"""
|
|
730
|
+
if isinstance(params, dict):
|
|
731
|
+
# Direct dictionary
|
|
732
|
+
all_params.update(params)
|
|
733
|
+
elif isinstance(params, (tuple, list)):
|
|
734
|
+
# Convert tuple/list to dict
|
|
735
|
+
for item in params:
|
|
736
|
+
if isinstance(item, (tuple, list)) and len(item) >= 2:
|
|
737
|
+
key, value = item[0], item[1]
|
|
738
|
+
all_params[key] = value
|
|
739
|
+
elif isinstance(item, string_types):
|
|
740
|
+
# Just a key with None value
|
|
741
|
+
all_params[item] = None
|
|
742
|
+
elif hasattr(item, "__iter__") and not isinstance(item, string_types):
|
|
743
|
+
# Try to treat as key-value pair
|
|
744
|
+
item_list = list(item)
|
|
745
|
+
if len(item_list) >= 2:
|
|
746
|
+
all_params[item_list[0]] = item_list[1]
|
|
747
|
+
elif hasattr(params, "items"):
|
|
748
|
+
# Dict-like object
|
|
749
|
+
all_params.update(params)
|
|
750
|
+
elif hasattr(params, "_getpairs"):
|
|
751
|
+
all_params.update(params._getpairs())
|
|
752
|
+
elif hasattr(params, "_gettuple"):
|
|
753
|
+
all_params.update(dict(params._gettuple()))
|
|
754
|
+
elif hasattr(params, "__dict__"):
|
|
755
|
+
# OPTIMIZED: Object with attributes, using __dict__ for performance
|
|
756
|
+
for attr_name, attr_value in params.__dict__.items():
|
|
757
|
+
if not attr_name.startswith("_") and not callable(attr_value):
|
|
758
|
+
all_params[attr_name] = attr_value
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class ParameterManager:
|
|
762
|
+
"""Manager for handling parameter operations without metaclass.
|
|
763
|
+
|
|
764
|
+
This class provides static methods for setting up and deriving parameter
|
|
765
|
+
classes, handling package imports, and managing parameter inheritance.
|
|
766
|
+
|
|
767
|
+
Methods:
|
|
768
|
+
setup_class_params: Set up parameters for a class.
|
|
769
|
+
_derive_params: Create a derived parameter class.
|
|
770
|
+
_handle_packages: Handle package and module imports.
|
|
771
|
+
"""
|
|
772
|
+
|
|
773
|
+
@staticmethod
|
|
774
|
+
def setup_class_params(cls, params=(), packages=(), frompackages=()):
|
|
775
|
+
"""Set up parameters for a class"""
|
|
776
|
+
# Handle packages and frompackages
|
|
777
|
+
ParameterManager._handle_packages(cls, packages, frompackages)
|
|
778
|
+
|
|
779
|
+
# Get params from base classes
|
|
780
|
+
bases = tuple(cls.__mro__[1:]) # Skip self
|
|
781
|
+
|
|
782
|
+
# Create derived params
|
|
783
|
+
cls._params = ParameterManager._derive_params(cls.__name__, params, bases)
|
|
784
|
+
|
|
785
|
+
# Set params property on the class
|
|
786
|
+
setattr(cls, "params", cls._params)
|
|
787
|
+
|
|
788
|
+
return cls._params
|
|
789
|
+
|
|
790
|
+
@staticmethod
|
|
791
|
+
def _derive_params(name, params, otherbases):
|
|
792
|
+
"""Derive parameter class"""
|
|
793
|
+
# Create a simple parameter class
|
|
794
|
+
class_name = f"Params_{name}"
|
|
795
|
+
|
|
796
|
+
# Collect all parameters from base classes first
|
|
797
|
+
all_params = OrderedDict()
|
|
798
|
+
|
|
799
|
+
# Process base classes in reverse order for proper inheritance
|
|
800
|
+
for base in reversed(otherbases):
|
|
801
|
+
if hasattr(base, "_params") and base._params is not None:
|
|
802
|
+
if hasattr(base._params, "_getpairs"):
|
|
803
|
+
base_params = base._params._getpairs()
|
|
804
|
+
all_params.update(base_params)
|
|
805
|
+
elif hasattr(base._params, "_gettuple"):
|
|
806
|
+
base_params = dict(base._params._gettuple())
|
|
807
|
+
all_params.update(base_params)
|
|
808
|
+
elif hasattr(base._params, "__dict__"):
|
|
809
|
+
# OPTIMIZED: Get attributes from parameter instance using __dict__
|
|
810
|
+
for attr_name, attr_value in base._params.__dict__.items():
|
|
811
|
+
if not attr_name.startswith("_") and not callable(attr_value):
|
|
812
|
+
all_params[attr_name] = attr_value
|
|
813
|
+
|
|
814
|
+
# Handle current class params - could be tuple, dict, or dict-like
|
|
815
|
+
_merge_class_params_into(all_params, params)
|
|
816
|
+
|
|
817
|
+
# CRITICAL FIX: Ensure common parameter names are always available
|
|
818
|
+
# Many indicators expect these standard parameters
|
|
819
|
+
common_defaults = {
|
|
820
|
+
"period": 14,
|
|
821
|
+
"movav": None,
|
|
822
|
+
"_movav": None,
|
|
823
|
+
"lookback": 1,
|
|
824
|
+
"upperband": 70.0,
|
|
825
|
+
"lowerband": 30.0,
|
|
826
|
+
"safediv": False,
|
|
827
|
+
"safepct": False,
|
|
828
|
+
"fast": 5, # For oscillators
|
|
829
|
+
"slow": 34, # For oscillators
|
|
830
|
+
"signal": 9, # For MACD-style indicators
|
|
831
|
+
"mult": 2.0, # For bands
|
|
832
|
+
"matype": 0, # Moving average type
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
# Add common defaults if not already present
|
|
836
|
+
for key, default_value in common_defaults.items():
|
|
837
|
+
if key not in all_params:
|
|
838
|
+
all_params[key] = default_value
|
|
839
|
+
|
|
840
|
+
# CRITICAL FIX: Handle _movav parameter specially - it should default to SMA
|
|
841
|
+
if "_movav" not in all_params or all_params["_movav"] is None:
|
|
842
|
+
# CRITICAL FIX: Don't import MovAv during class creation to avoid circular imports
|
|
843
|
+
# We'll handle this lazily in the parameter getter instead
|
|
844
|
+
all_params["_movav"] = None
|
|
845
|
+
|
|
846
|
+
return LegacyParamsSchema(class_name, all_params, module=__name__)
|
|
847
|
+
|
|
848
|
+
@staticmethod
|
|
849
|
+
def _handle_packages(cls, packages, frompackages):
|
|
850
|
+
"""Handle package imports"""
|
|
851
|
+
cls.packages = packages
|
|
852
|
+
cls.frompackages = frompackages
|
|
853
|
+
|
|
854
|
+
clsmod = sys.modules[cls.__module__]
|
|
855
|
+
|
|
856
|
+
for package in packages:
|
|
857
|
+
if isinstance(package, (tuple, list)):
|
|
858
|
+
package, alias = package
|
|
859
|
+
else:
|
|
860
|
+
alias = package
|
|
861
|
+
|
|
862
|
+
try:
|
|
863
|
+
pmod = __import__(package)
|
|
864
|
+
for part in package.split(".")[1:]:
|
|
865
|
+
pmod = getattr(pmod, part)
|
|
866
|
+
setattr(clsmod, alias, pmod)
|
|
867
|
+
except ImportError:
|
|
868
|
+
# Optional linked package not installed; skip aliasing it.
|
|
869
|
+
logger.warning("metabase:864 suppressed ImportError")
|
|
870
|
+
|
|
871
|
+
for packageitems in frompackages:
|
|
872
|
+
if len(packageitems) != 2:
|
|
873
|
+
continue
|
|
874
|
+
package, frompackage = packageitems
|
|
875
|
+
|
|
876
|
+
if isinstance(frompackage, string_types):
|
|
877
|
+
frompackage = (frompackage,)
|
|
878
|
+
|
|
879
|
+
for fromitem in frompackage:
|
|
880
|
+
if isinstance(fromitem, (tuple, list)):
|
|
881
|
+
fromitem, alias = fromitem
|
|
882
|
+
else:
|
|
883
|
+
alias = fromitem
|
|
884
|
+
|
|
885
|
+
try:
|
|
886
|
+
pmod = __import__(package, fromlist=[fromitem])
|
|
887
|
+
pattr = getattr(pmod, fromitem)
|
|
888
|
+
setattr(clsmod, alias, pattr)
|
|
889
|
+
except (ImportError, AttributeError):
|
|
890
|
+
# Optional linked symbol unavailable; skip aliasing it.
|
|
891
|
+
logger.warning("metabase:886 suppressed ImportError,AttributeError")
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
class ParamsMixin(BaseMixin):
|
|
895
|
+
"""Mixin class that provides parameter management capabilities"""
|
|
896
|
+
|
|
897
|
+
def __init_subclass__(cls, **kwargs):
|
|
898
|
+
"""Set up parameters when a subclass is created"""
|
|
899
|
+
super().__init_subclass__(**kwargs)
|
|
900
|
+
|
|
901
|
+
# CRITICAL FIX: Call _initialize_indicator_aliases whenever an indicator class is created
|
|
902
|
+
# OPTIMIZED: Use cached type check
|
|
903
|
+
if is_class_type(cls, "Indicator"):
|
|
904
|
+
try:
|
|
905
|
+
_initialize_indicator_aliases()
|
|
906
|
+
except Exception as e:
|
|
907
|
+
logger.debug("Failed to initialize indicator aliases: %s", e)
|
|
908
|
+
|
|
909
|
+
# Set up params, packages, frompackages if they exist
|
|
910
|
+
params = getattr(cls, "params", ())
|
|
911
|
+
packages = getattr(cls, "packages", ())
|
|
912
|
+
frompackages = getattr(cls, "frompackages", ())
|
|
913
|
+
|
|
914
|
+
ParameterManager.setup_class_params(cls, params, packages, frompackages)
|
|
915
|
+
|
|
916
|
+
# CRITICAL FIX: Auto-patch __init__ methods of indicators to ensure proper parameter handling
|
|
917
|
+
if hasattr(cls, "__init__") and "__init__" in cls.__dict__:
|
|
918
|
+
original_init = cls.__init__
|
|
919
|
+
|
|
920
|
+
# CRITICAL FIX: Store the original __init__ so Strategy can call it directly
|
|
921
|
+
# This prevents infinite recursion when Strategy.user_init tries to call cls.__init__
|
|
922
|
+
cls._original_init = original_init
|
|
923
|
+
|
|
924
|
+
def patched_init(self, *args, **kwargs):
|
|
925
|
+
# CRITICAL FIX: For indicators, set up data0/data1 BEFORE anything else
|
|
926
|
+
# This ensures indicators can access self.data0, self.data1 during initialization
|
|
927
|
+
if "Indicator" in self.__class__.__name__ or any(
|
|
928
|
+
"Indicator" in base.__name__ for base in self.__class__.__mro__
|
|
929
|
+
):
|
|
930
|
+
if hasattr(self, "datas") and self.datas:
|
|
931
|
+
# Set data0, data1, etc. immediately from existing datas
|
|
932
|
+
for d, data in enumerate(self.datas):
|
|
933
|
+
setattr(self, f"data{d}", data)
|
|
934
|
+
elif args:
|
|
935
|
+
# If we don't have datas set yet, try to extract from args
|
|
936
|
+
temp_datas = []
|
|
937
|
+
for i, arg in enumerate(args):
|
|
938
|
+
# Check if this is a data-like object
|
|
939
|
+
if (
|
|
940
|
+
hasattr(arg, "lines")
|
|
941
|
+
or hasattr(arg, "_name")
|
|
942
|
+
or hasattr(arg, "__class__")
|
|
943
|
+
and "Data" in str(arg.__class__.__name__)
|
|
944
|
+
or hasattr(arg, "__class__")
|
|
945
|
+
and any(
|
|
946
|
+
"LineSeries" in base.__name__ for base in arg.__class__.__mro__
|
|
947
|
+
)
|
|
948
|
+
):
|
|
949
|
+
temp_datas.append(arg)
|
|
950
|
+
setattr(self, f"data{i}", arg)
|
|
951
|
+
else:
|
|
952
|
+
# Non-data argument, stop processing
|
|
953
|
+
break
|
|
954
|
+
|
|
955
|
+
# Set up datas if we found any
|
|
956
|
+
if temp_datas:
|
|
957
|
+
if not hasattr(self, "datas") or not self.datas:
|
|
958
|
+
self.datas = temp_datas
|
|
959
|
+
self.data = temp_datas[0]
|
|
960
|
+
else:
|
|
961
|
+
# If indicator created with no data, use OwnerContext to find data owner
|
|
962
|
+
# This handles cases like AwesomeOscillator() inside AccDecOscillator.__init__
|
|
963
|
+
if not hasattr(self, "datas") or not self.datas:
|
|
964
|
+
# Walk the OwnerContext stack for any owner with data
|
|
965
|
+
stack = getattr(_owner_context, "owner_stack", None)
|
|
966
|
+
if stack:
|
|
967
|
+
for potential_owner in reversed(stack):
|
|
968
|
+
if potential_owner is self:
|
|
969
|
+
continue
|
|
970
|
+
if hasattr(potential_owner, "datas") and potential_owner.datas:
|
|
971
|
+
self.datas = potential_owner.datas
|
|
972
|
+
self.data = potential_owner.datas[0]
|
|
973
|
+
for d, data in enumerate(potential_owner.datas):
|
|
974
|
+
setattr(self, f"data{d}", data)
|
|
975
|
+
break
|
|
976
|
+
if (
|
|
977
|
+
hasattr(potential_owner, "data")
|
|
978
|
+
and potential_owner.data is not None
|
|
979
|
+
):
|
|
980
|
+
self.datas = [potential_owner.data]
|
|
981
|
+
self.data = potential_owner.data
|
|
982
|
+
self.data0 = potential_owner.data
|
|
983
|
+
break
|
|
984
|
+
|
|
985
|
+
# CRITICAL FIX: Restore kwargs from __new__ if they were lost
|
|
986
|
+
if hasattr(self, "_init_kwargs") and not kwargs:
|
|
987
|
+
kwargs = self._init_kwargs
|
|
988
|
+
if hasattr(self, "_init_args") and not args:
|
|
989
|
+
args = self._init_args
|
|
990
|
+
|
|
991
|
+
# CRITICAL FIX: Extract parameter kwargs before creating parameter instance
|
|
992
|
+
# Separate parameter kwargs from other kwargs
|
|
993
|
+
param_kwargs = {}
|
|
994
|
+
other_kwargs = {}
|
|
995
|
+
|
|
996
|
+
# Get list of valid parameter names from class
|
|
997
|
+
# CRITICAL FIX: Use self.__class__ instead of cls to get the actual runtime class
|
|
998
|
+
actual_cls = self.__class__
|
|
999
|
+
valid_param_names = set()
|
|
1000
|
+
if hasattr(actual_cls, "_params") and actual_cls._params is not None:
|
|
1001
|
+
try:
|
|
1002
|
+
if hasattr(actual_cls._params, "_getkeys"):
|
|
1003
|
+
valid_param_names = set(actual_cls._params._getkeys())
|
|
1004
|
+
elif hasattr(actual_cls._params, "_getpairs"):
|
|
1005
|
+
valid_param_names = set(actual_cls._params._getpairs().keys())
|
|
1006
|
+
except Exception as e:
|
|
1007
|
+
logger.debug("Failed to get valid param names: %s", e)
|
|
1008
|
+
|
|
1009
|
+
# Separate kwargs into param_kwargs and other_kwargs
|
|
1010
|
+
# Filter out test-specific and non-constructor kwargs
|
|
1011
|
+
test_kwargs = {
|
|
1012
|
+
"main",
|
|
1013
|
+
"plot",
|
|
1014
|
+
"writer",
|
|
1015
|
+
"analyzer",
|
|
1016
|
+
"chkind",
|
|
1017
|
+
"chkmin",
|
|
1018
|
+
"chkargs",
|
|
1019
|
+
"chkvals",
|
|
1020
|
+
"chknext",
|
|
1021
|
+
"chksamebars",
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for key, value in kwargs.items():
|
|
1025
|
+
if key in valid_param_names:
|
|
1026
|
+
# This is a parameter - add to param_kwargs but NOT other_kwargs
|
|
1027
|
+
param_kwargs[key] = value
|
|
1028
|
+
elif key not in test_kwargs:
|
|
1029
|
+
# This is not a parameter and not a test kwarg - pass to parent init
|
|
1030
|
+
other_kwargs[key] = value
|
|
1031
|
+
|
|
1032
|
+
# CRITICAL FIX: Always update parameter values from param_kwargs
|
|
1033
|
+
# Don't skip if self.p exists - we need to update it with new values
|
|
1034
|
+
if not hasattr(self, "p") or self.p is None:
|
|
1035
|
+
# Create parameter instance with param_kwargs
|
|
1036
|
+
if hasattr(cls, "_params") and cls._params is not None:
|
|
1037
|
+
try:
|
|
1038
|
+
self.p = cls._params(**param_kwargs)
|
|
1039
|
+
except Exception:
|
|
1040
|
+
logger.debug("metabase:1036 fallback on Exception")
|
|
1041
|
+
from .utils import DotDict
|
|
1042
|
+
|
|
1043
|
+
self.p = DotDict(param_kwargs)
|
|
1044
|
+
else:
|
|
1045
|
+
from .utils import DotDict
|
|
1046
|
+
|
|
1047
|
+
self.p = DotDict(param_kwargs)
|
|
1048
|
+
else:
|
|
1049
|
+
# self.p already exists - update it with param_kwargs
|
|
1050
|
+
for key, value in param_kwargs.items():
|
|
1051
|
+
setattr(self.p, key, value)
|
|
1052
|
+
|
|
1053
|
+
# Also set self.params for backwards compatibility
|
|
1054
|
+
self.params = self.p
|
|
1055
|
+
|
|
1056
|
+
# CRITICAL FIX: Ensure indicator has _plotinit method before user init
|
|
1057
|
+
if "Indicator" in cls.__name__ or is_class_type(cls, "Indicator"):
|
|
1058
|
+
if not hasattr(self, "_plotinit"):
|
|
1059
|
+
# Add _plotinit method
|
|
1060
|
+
def default_plotinit():
|
|
1061
|
+
plotinfo_defaults = {
|
|
1062
|
+
"plot": True,
|
|
1063
|
+
"subplot": True,
|
|
1064
|
+
"plotname": "",
|
|
1065
|
+
"plotskip": False,
|
|
1066
|
+
"plotabove": False,
|
|
1067
|
+
"plotlinelabels": False,
|
|
1068
|
+
"plotlinevalues": True,
|
|
1069
|
+
"plotvaluetags": True,
|
|
1070
|
+
"plotymargin": 0.0,
|
|
1071
|
+
"plotyhlines": [],
|
|
1072
|
+
"plotyticks": [],
|
|
1073
|
+
"plothlines": [],
|
|
1074
|
+
"plotforce": False,
|
|
1075
|
+
"plotmaster": None,
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if not hasattr(self, "plotinfo"):
|
|
1079
|
+
# Create plotinfo object with _get method and legendloc
|
|
1080
|
+
class PlotInfoObj:
|
|
1081
|
+
"""Plot information object for indicators.
|
|
1082
|
+
|
|
1083
|
+
A simple plotinfo object with minimal attributes
|
|
1084
|
+
for plotting configuration.
|
|
1085
|
+
|
|
1086
|
+
Attributes:
|
|
1087
|
+
legendloc: Location for the plot legend.
|
|
1088
|
+
"""
|
|
1089
|
+
|
|
1090
|
+
def __init__(self):
|
|
1091
|
+
"""Initialize PlotInfoObj with default attributes."""
|
|
1092
|
+
self.legendloc = None # CRITICAL: Add legendloc attribute
|
|
1093
|
+
|
|
1094
|
+
def _get(self, key, default=None):
|
|
1095
|
+
"""Get a plotinfo attribute value.
|
|
1096
|
+
|
|
1097
|
+
Args:
|
|
1098
|
+
key: Name of the attribute.
|
|
1099
|
+
default: Default value if not found.
|
|
1100
|
+
|
|
1101
|
+
Returns:
|
|
1102
|
+
The attribute value or default.
|
|
1103
|
+
"""
|
|
1104
|
+
return getattr(self, key, default)
|
|
1105
|
+
|
|
1106
|
+
def get(self, key, default=None):
|
|
1107
|
+
"""Get a plotinfo attribute value.
|
|
1108
|
+
|
|
1109
|
+
Args:
|
|
1110
|
+
key: Name of the attribute.
|
|
1111
|
+
default: Default value if not found.
|
|
1112
|
+
|
|
1113
|
+
Returns:
|
|
1114
|
+
The attribute value or default.
|
|
1115
|
+
"""
|
|
1116
|
+
return getattr(self, key, default)
|
|
1117
|
+
|
|
1118
|
+
def __contains__(self, key):
|
|
1119
|
+
return hasattr(self, key)
|
|
1120
|
+
|
|
1121
|
+
self.plotinfo = PlotInfoObj()
|
|
1122
|
+
|
|
1123
|
+
for attr, default_val in plotinfo_defaults.items():
|
|
1124
|
+
if not hasattr(self.plotinfo, attr):
|
|
1125
|
+
setattr(self.plotinfo, attr, default_val)
|
|
1126
|
+
|
|
1127
|
+
return True
|
|
1128
|
+
|
|
1129
|
+
self._plotinit = default_plotinit
|
|
1130
|
+
|
|
1131
|
+
# CRITICAL FIX: Try calling original_init with different argument strategies
|
|
1132
|
+
# Some classes (like most indicators) don't accept args
|
|
1133
|
+
# Others (like _LineDelay, LinesOperation) need args
|
|
1134
|
+
# Parameter kwargs are already set via self.p, so don't pass them
|
|
1135
|
+
|
|
1136
|
+
# Check if original_init accepts *args or **kwargs
|
|
1137
|
+
import inspect
|
|
1138
|
+
|
|
1139
|
+
try:
|
|
1140
|
+
sig = inspect.signature(original_init)
|
|
1141
|
+
has_var_positional = any(
|
|
1142
|
+
p.kind == inspect.Parameter.VAR_POSITIONAL for p in sig.parameters.values()
|
|
1143
|
+
)
|
|
1144
|
+
has_var_keyword = any(
|
|
1145
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
1146
|
+
)
|
|
1147
|
+
except (ValueError, TypeError):
|
|
1148
|
+
has_var_positional = False
|
|
1149
|
+
has_var_keyword = False
|
|
1150
|
+
|
|
1151
|
+
# If __init__ accepts *args or **kwargs, pass everything
|
|
1152
|
+
if has_var_positional or has_var_keyword:
|
|
1153
|
+
return original_init(self, *args, **other_kwargs)
|
|
1154
|
+
|
|
1155
|
+
# Otherwise, try without args first (most common case)
|
|
1156
|
+
try:
|
|
1157
|
+
# First, try without args - most common case for indicators/strategies
|
|
1158
|
+
return original_init(self, **other_kwargs)
|
|
1159
|
+
except TypeError as e:
|
|
1160
|
+
# Check if the error is about THIS class's __init__, not an internal call
|
|
1161
|
+
# If the error mentions a different class name, it's from an internal call - re-raise it
|
|
1162
|
+
error_str = str(e)
|
|
1163
|
+
class_name = self.__class__.__name__
|
|
1164
|
+
|
|
1165
|
+
# If error mentions a different class, it's from internal code - re-raise
|
|
1166
|
+
if ".__init__()" in error_str:
|
|
1167
|
+
# Extract the class name from error message like "SomeClass.__init__() ..."
|
|
1168
|
+
import re
|
|
1169
|
+
|
|
1170
|
+
match = re.search(r"(\w+)\.__init__\(\)", error_str)
|
|
1171
|
+
if match and match.group(1) != class_name:
|
|
1172
|
+
# Error is about a different class (internal call) - re-raise
|
|
1173
|
+
raise
|
|
1174
|
+
|
|
1175
|
+
# If that failed, check if it's because THIS class needs positional arguments
|
|
1176
|
+
if "missing" in error_str and "required positional argument" in error_str:
|
|
1177
|
+
# This class needs positional args (like _LineDelay, LinesOperation)
|
|
1178
|
+
# Pass all args - they're needed
|
|
1179
|
+
return original_init(self, *args, **other_kwargs)
|
|
1180
|
+
# Different error - re-raise it
|
|
1181
|
+
raise
|
|
1182
|
+
|
|
1183
|
+
cls.__init__ = patched_init
|
|
1184
|
+
|
|
1185
|
+
# Handle plotinfo and other info attributes (like the old metaclass system)
|
|
1186
|
+
info_attributes = ["plotinfo", "plotlines", "plotinfoargs"]
|
|
1187
|
+
for info_attr in info_attributes:
|
|
1188
|
+
if info_attr in cls.__dict__:
|
|
1189
|
+
info_dict = cls.__dict__[info_attr]
|
|
1190
|
+
if isinstance(info_dict, dict):
|
|
1191
|
+
# CRITICAL FIX: Ensure plotinfo objects have all required attributes
|
|
1192
|
+
if info_attr == "plotinfo":
|
|
1193
|
+
# Set default plotinfo attributes if missing
|
|
1194
|
+
default_plotinfo = {
|
|
1195
|
+
"plot": True,
|
|
1196
|
+
"subplot": True,
|
|
1197
|
+
"plotname": "",
|
|
1198
|
+
"plotskip": False,
|
|
1199
|
+
"plotabove": False,
|
|
1200
|
+
"plotlinelabels": False,
|
|
1201
|
+
"plotlinevalues": True,
|
|
1202
|
+
"plotvaluetags": True,
|
|
1203
|
+
"plotymargin": 0.0,
|
|
1204
|
+
"plotyhlines": [],
|
|
1205
|
+
"plotyticks": [],
|
|
1206
|
+
"plothlines": [],
|
|
1207
|
+
"plotforce": False,
|
|
1208
|
+
"plotmaster": None,
|
|
1209
|
+
}
|
|
1210
|
+
# Merge provided plotinfo with defaults
|
|
1211
|
+
for key, default_value in default_plotinfo.items():
|
|
1212
|
+
if key not in info_dict:
|
|
1213
|
+
info_dict[key] = default_value
|
|
1214
|
+
|
|
1215
|
+
# Convert dictionary to attribute-accessible object
|
|
1216
|
+
info_obj = type(f"{info_attr}_obj", (), info_dict)()
|
|
1217
|
+
|
|
1218
|
+
# CRITICAL FIX: Ensure the object can be used like a dict too
|
|
1219
|
+
# Some code might expect dict-like access
|
|
1220
|
+
def info_getitem(self, key):
|
|
1221
|
+
# CRITICAL FIX: Ensure key is a string before using hasattr()
|
|
1222
|
+
if isinstance(key, str) and hasattr(self, key):
|
|
1223
|
+
return getattr(self, key)
|
|
1224
|
+
return None
|
|
1225
|
+
|
|
1226
|
+
def info_setitem(self, key, value):
|
|
1227
|
+
# Only set if key is a string
|
|
1228
|
+
if isinstance(key, str):
|
|
1229
|
+
setattr(self, key, value)
|
|
1230
|
+
|
|
1231
|
+
def info_contains(self, key):
|
|
1232
|
+
# CRITICAL FIX: Only check if key is a string
|
|
1233
|
+
return isinstance(key, str) and hasattr(self, key)
|
|
1234
|
+
|
|
1235
|
+
def info_get(self, key, default=None):
|
|
1236
|
+
# CRITICAL FIX: Ensure key is a string before using hasattr()
|
|
1237
|
+
if isinstance(key, str) and hasattr(self, key):
|
|
1238
|
+
return getattr(self, key)
|
|
1239
|
+
return default
|
|
1240
|
+
|
|
1241
|
+
def info_get_method(self, key, default=None):
|
|
1242
|
+
"""CRITICAL: _get method expected by plotting system"""
|
|
1243
|
+
# CRITICAL FIX: Ensure key is a string before using hasattr()
|
|
1244
|
+
if isinstance(key, str) and hasattr(self, key):
|
|
1245
|
+
return getattr(self, key)
|
|
1246
|
+
return default
|
|
1247
|
+
|
|
1248
|
+
def info_keys(self):
|
|
1249
|
+
# OPTIMIZED: Use __dict__ instead of dir() for better performance
|
|
1250
|
+
return [
|
|
1251
|
+
attr
|
|
1252
|
+
for attr, val in self.__dict__.items()
|
|
1253
|
+
if not attr.startswith("_") and not callable(val)
|
|
1254
|
+
]
|
|
1255
|
+
|
|
1256
|
+
def info_values(self):
|
|
1257
|
+
return [getattr(self, attr) for attr in self.keys()]
|
|
1258
|
+
|
|
1259
|
+
def info_items(self):
|
|
1260
|
+
return [(attr, getattr(self, attr)) for attr in self.keys()]
|
|
1261
|
+
|
|
1262
|
+
info_obj.__getitem__ = info_getitem
|
|
1263
|
+
info_obj.__setitem__ = info_setitem
|
|
1264
|
+
info_obj.__contains__ = info_contains
|
|
1265
|
+
info_obj.get = info_get
|
|
1266
|
+
info_obj._get = (
|
|
1267
|
+
info_get_method # CRITICAL: Add _get method for plotting compatibility
|
|
1268
|
+
)
|
|
1269
|
+
info_obj.keys = info_keys
|
|
1270
|
+
info_obj.values = info_values
|
|
1271
|
+
info_obj.items = info_items
|
|
1272
|
+
|
|
1273
|
+
setattr(cls, info_attr, info_obj)
|
|
1274
|
+
|
|
1275
|
+
# Ensure the class has a params attribute that can handle _gettuple calls
|
|
1276
|
+
if hasattr(cls, "_params"):
|
|
1277
|
+
# If _params is not a proper parameter class, make it one
|
|
1278
|
+
if isinstance(cls._params, (tuple, list)) or not hasattr(cls._params, "_gettuple"):
|
|
1279
|
+
cls._params = LegacyParamsSchema(f"Params_{cls.__name__}", cls._params)
|
|
1280
|
+
|
|
1281
|
+
# Set class-level params attribute for compatibility
|
|
1282
|
+
cls.params = cls._params
|
|
1283
|
+
|
|
1284
|
+
def __new__(cls, *args, **kwargs):
|
|
1285
|
+
"""Create instance and set up parameters before __init__ is called"""
|
|
1286
|
+
# Create the instance first
|
|
1287
|
+
instance = super().__new__(cls)
|
|
1288
|
+
|
|
1289
|
+
# Set up parameters for this instance
|
|
1290
|
+
if hasattr(cls, "_params") and cls._params is not None:
|
|
1291
|
+
params_cls = cls._params
|
|
1292
|
+
param_names = set()
|
|
1293
|
+
|
|
1294
|
+
# Get all parameter names from the class
|
|
1295
|
+
if hasattr(params_cls, "_getpairs"):
|
|
1296
|
+
param_names.update(params_cls._getpairs().keys())
|
|
1297
|
+
elif hasattr(params_cls, "_gettuple"):
|
|
1298
|
+
param_names.update(key for key, value in params_cls._gettuple())
|
|
1299
|
+
|
|
1300
|
+
# Separate parameter and non-parameter kwargs
|
|
1301
|
+
param_kwargs = {}
|
|
1302
|
+
non_param_kwargs = {}
|
|
1303
|
+
for key, value in kwargs.items():
|
|
1304
|
+
if key in param_names:
|
|
1305
|
+
param_kwargs[key] = value
|
|
1306
|
+
else:
|
|
1307
|
+
non_param_kwargs[key] = value
|
|
1308
|
+
|
|
1309
|
+
# Store non-param kwargs for later use
|
|
1310
|
+
instance._non_param_kwargs = non_param_kwargs
|
|
1311
|
+
|
|
1312
|
+
# Create parameter instance
|
|
1313
|
+
try:
|
|
1314
|
+
instance._params_instance = params_cls()
|
|
1315
|
+
except Exception:
|
|
1316
|
+
logger.debug("metabase:1311 fallback on Exception")
|
|
1317
|
+
instance._params_instance = make_legacy_parameter_accessor(name="ParamsInstance")
|
|
1318
|
+
|
|
1319
|
+
# Set all parameter values - first defaults, then custom values
|
|
1320
|
+
if hasattr(params_cls, "_getpairs"):
|
|
1321
|
+
for key, value in params_cls._getpairs().items():
|
|
1322
|
+
# Use custom value if provided, otherwise use default
|
|
1323
|
+
final_value = param_kwargs.get(key, value)
|
|
1324
|
+
setattr(instance._params_instance, key, final_value)
|
|
1325
|
+
elif hasattr(params_cls, "_gettuple"):
|
|
1326
|
+
for key, value in params_cls._gettuple():
|
|
1327
|
+
# Use custom value if provided, otherwise use default
|
|
1328
|
+
final_value = param_kwargs.get(key, value)
|
|
1329
|
+
setattr(instance._params_instance, key, final_value)
|
|
1330
|
+
|
|
1331
|
+
# Also set any extra parameters that were passed but not in the params definition
|
|
1332
|
+
for key, value in param_kwargs.items():
|
|
1333
|
+
if not hasattr(instance._params_instance, key):
|
|
1334
|
+
setattr(instance._params_instance, key, value)
|
|
1335
|
+
|
|
1336
|
+
else:
|
|
1337
|
+
instance._params_instance = make_legacy_parameter_accessor(
|
|
1338
|
+
values=kwargs, name="ParamsInstance"
|
|
1339
|
+
)
|
|
1340
|
+
instance._non_param_kwargs = {}
|
|
1341
|
+
|
|
1342
|
+
return instance
|
|
1343
|
+
|
|
1344
|
+
def __init__(self, *args, **kwargs):
|
|
1345
|
+
"""Initialize with only non-parameter kwargs"""
|
|
1346
|
+
# Use pre-filtered non-parameter kwargs if available
|
|
1347
|
+
if hasattr(self, "_non_param_kwargs"):
|
|
1348
|
+
filtered_kwargs = self._non_param_kwargs
|
|
1349
|
+
else:
|
|
1350
|
+
# Filter out parameter kwargs before calling super().__init__
|
|
1351
|
+
if hasattr(self.__class__, "_params") and self.__class__._params is not None:
|
|
1352
|
+
params_cls = self.__class__._params
|
|
1353
|
+
param_names = set()
|
|
1354
|
+
|
|
1355
|
+
# Get all parameter names from the class
|
|
1356
|
+
if hasattr(params_cls, "_getpairs"):
|
|
1357
|
+
param_names.update(params_cls._getpairs().keys())
|
|
1358
|
+
elif hasattr(params_cls, "_gettuple"):
|
|
1359
|
+
param_names.update(key for key, value in params_cls._gettuple())
|
|
1360
|
+
|
|
1361
|
+
# Filter kwargs to remove parameter kwargs
|
|
1362
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in param_names}
|
|
1363
|
+
else:
|
|
1364
|
+
# No parameters, but still avoid passing args to object.__init__
|
|
1365
|
+
filtered_kwargs = {}
|
|
1366
|
+
|
|
1367
|
+
# Call super().__init__ without args to avoid object.__init__() error
|
|
1368
|
+
# Only pass kwargs if this is not the base object to prevent "object.__init__() takes exactly one argument" error
|
|
1369
|
+
try:
|
|
1370
|
+
if filtered_kwargs:
|
|
1371
|
+
super().__init__(**filtered_kwargs)
|
|
1372
|
+
else:
|
|
1373
|
+
super().__init__()
|
|
1374
|
+
except TypeError as e:
|
|
1375
|
+
# If we reach object.__init__ and it complains about arguments, call it without kwargs
|
|
1376
|
+
if "object.__init__() takes" in str(e):
|
|
1377
|
+
super().__init__()
|
|
1378
|
+
else:
|
|
1379
|
+
raise
|
|
1380
|
+
|
|
1381
|
+
@property
|
|
1382
|
+
def params(self):
|
|
1383
|
+
"""Instance-level params property for backward compatibility"""
|
|
1384
|
+
return getattr(self, "_params_instance", None)
|
|
1385
|
+
|
|
1386
|
+
@params.setter
|
|
1387
|
+
def params(self, value):
|
|
1388
|
+
"""Allow setting params instance"""
|
|
1389
|
+
self._params_instance = value
|
|
1390
|
+
# CRITICAL FIX: Ensure p also points to the same instance
|
|
1391
|
+
object.__setattr__(self, "p", value)
|
|
1392
|
+
|
|
1393
|
+
@property
|
|
1394
|
+
def p(self):
|
|
1395
|
+
"""Provide p property for backward compatibility"""
|
|
1396
|
+
# PERFORMANCE OPTIMIZATION: Use __dict__.get() instead of getattr()
|
|
1397
|
+
# Called 6M+ times, direct dict access is faster
|
|
1398
|
+
return self.__dict__.get("_params_instance")
|
|
1399
|
+
|
|
1400
|
+
@p.setter
|
|
1401
|
+
def p(self, value):
|
|
1402
|
+
"""Allow setting p instance"""
|
|
1403
|
+
self._params_instance = value
|
|
1404
|
+
# CRITICAL FIX: Ensure params also points to the same instance
|
|
1405
|
+
object.__setattr__(self, "params", value)
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
# For backward compatibility, keep the old class names as aliases
|
|
1409
|
+
ParamsBase = ParamsMixin
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
class ItemCollection:
|
|
1413
|
+
"""Collection that allows access by both index and name.
|
|
1414
|
+
|
|
1415
|
+
This class holds a list of items that can be accessed either by their
|
|
1416
|
+
numeric index or by a string name. Names are set as attributes on the
|
|
1417
|
+
collection instance.
|
|
1418
|
+
|
|
1419
|
+
Attributes:
|
|
1420
|
+
items (list): The underlying list of items.
|
|
1421
|
+
|
|
1422
|
+
Example:
|
|
1423
|
+
collection = ItemCollection()
|
|
1424
|
+
collection.append(my_strategy, name='mystrat')
|
|
1425
|
+
collection[0] # Access by index
|
|
1426
|
+
collection.mystrat # Access by name
|
|
1427
|
+
"""
|
|
1428
|
+
|
|
1429
|
+
def __init__(self):
|
|
1430
|
+
"""Initialize the collection with an empty items list."""
|
|
1431
|
+
self.items = []
|
|
1432
|
+
|
|
1433
|
+
def __len__(self):
|
|
1434
|
+
"""Return the number of items in the collection."""
|
|
1435
|
+
return len(self.items)
|
|
1436
|
+
|
|
1437
|
+
def append(self, item, name=None):
|
|
1438
|
+
"""Add an item to the collection with an optional name."""
|
|
1439
|
+
setattr(self, name or item.__name__, item)
|
|
1440
|
+
self.items.append(item)
|
|
1441
|
+
|
|
1442
|
+
def __getitem__(self, key):
|
|
1443
|
+
"""Get item by index."""
|
|
1444
|
+
return self.items[key]
|
|
1445
|
+
|
|
1446
|
+
def getnames(self):
|
|
1447
|
+
"""Get list of all item names."""
|
|
1448
|
+
return [x.__name__ for x in self.items]
|
|
1449
|
+
|
|
1450
|
+
def getitems(self):
|
|
1451
|
+
"""Return list of (name, item) tuples for unpacking."""
|
|
1452
|
+
result = []
|
|
1453
|
+
for item in self.items:
|
|
1454
|
+
# Get item name from _name or __name__ attribute
|
|
1455
|
+
name = getattr(item, "_name", None) or getattr(item, "__name__", None)
|
|
1456
|
+
if name is None:
|
|
1457
|
+
# Fall back to lowercase class name
|
|
1458
|
+
name = item.__class__.__name__.lower()
|
|
1459
|
+
result.append((name, item))
|
|
1460
|
+
return result
|
|
1461
|
+
|
|
1462
|
+
def getbyname(self, name):
|
|
1463
|
+
"""Get item by name."""
|
|
1464
|
+
return getattr(self, name)
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _convert_plotlines_dict_to_object(cls):
|
|
1468
|
+
"""Convert plotlines from dict to object with _get method"""
|
|
1469
|
+
if not hasattr(cls, "plotlines") or not isinstance(cls.plotlines, dict):
|
|
1470
|
+
return
|
|
1471
|
+
|
|
1472
|
+
plotlines_dict = cls.plotlines
|
|
1473
|
+
|
|
1474
|
+
class PlotLinesObj:
|
|
1475
|
+
"""Object wrapper for plotlines dictionary.
|
|
1476
|
+
|
|
1477
|
+
Converts a plotlines dictionary into an object that supports
|
|
1478
|
+
attribute access and the _get method expected by the plotting system.
|
|
1479
|
+
|
|
1480
|
+
Attributes:
|
|
1481
|
+
_data: Original dictionary data.
|
|
1482
|
+
"""
|
|
1483
|
+
|
|
1484
|
+
def __init__(self, data_dict):
|
|
1485
|
+
"""Initialize the PlotLinesObj with dictionary data.
|
|
1486
|
+
|
|
1487
|
+
Args:
|
|
1488
|
+
data_dict: Dictionary of plot line configurations.
|
|
1489
|
+
"""
|
|
1490
|
+
self._data = data_dict.copy()
|
|
1491
|
+
# Set attributes for direct access
|
|
1492
|
+
for key, value in data_dict.items():
|
|
1493
|
+
if isinstance(value, dict):
|
|
1494
|
+
# Convert nested dicts to objects too
|
|
1495
|
+
nested_obj = PlotLineAttrObj(value)
|
|
1496
|
+
setattr(self, key, nested_obj)
|
|
1497
|
+
else:
|
|
1498
|
+
setattr(self, key, value)
|
|
1499
|
+
|
|
1500
|
+
def _get(self, key, default=None):
|
|
1501
|
+
"""CRITICAL: _get method expected by plotting system"""
|
|
1502
|
+
if hasattr(self, key):
|
|
1503
|
+
return getattr(self, key)
|
|
1504
|
+
return self._data.get(key, default)
|
|
1505
|
+
|
|
1506
|
+
def get(self, key, default=None):
|
|
1507
|
+
"""Standard get method for compatibility"""
|
|
1508
|
+
if hasattr(self, key):
|
|
1509
|
+
return getattr(self, key)
|
|
1510
|
+
return self._data.get(key, default)
|
|
1511
|
+
|
|
1512
|
+
def __contains__(self, key):
|
|
1513
|
+
return hasattr(self, key) or key in self._data
|
|
1514
|
+
|
|
1515
|
+
def __getattr__(self, name):
|
|
1516
|
+
if name.startswith("_"):
|
|
1517
|
+
raise AttributeError(
|
|
1518
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
1519
|
+
)
|
|
1520
|
+
# Return empty plot line object for missing attributes
|
|
1521
|
+
# Check if this might be a numeric index lookup first
|
|
1522
|
+
if name.isdigit() or name.startswith("_") and name[1:].isdigit():
|
|
1523
|
+
return PlotLineAttrObj({})
|
|
1524
|
+
return PlotLineAttrObj({})
|
|
1525
|
+
|
|
1526
|
+
class PlotLineAttrObj:
|
|
1527
|
+
"""Object wrapper for plot line attributes.
|
|
1528
|
+
|
|
1529
|
+
Converts nested dictionaries in plotlines into objects that
|
|
1530
|
+
support attribute access.
|
|
1531
|
+
|
|
1532
|
+
Attributes:
|
|
1533
|
+
_data: Original dictionary data.
|
|
1534
|
+
"""
|
|
1535
|
+
|
|
1536
|
+
def __init__(self, data_dict):
|
|
1537
|
+
"""Initialize the PlotLineAttrObj with dictionary data.
|
|
1538
|
+
|
|
1539
|
+
Args:
|
|
1540
|
+
data_dict: Dictionary of plot line attributes.
|
|
1541
|
+
"""
|
|
1542
|
+
self._data = data_dict.copy()
|
|
1543
|
+
# Set attributes for direct access
|
|
1544
|
+
for key, value in data_dict.items():
|
|
1545
|
+
setattr(self, key, value)
|
|
1546
|
+
|
|
1547
|
+
def _get(self, key, default=None):
|
|
1548
|
+
"""CRITICAL: _get method expected by plotting system"""
|
|
1549
|
+
if hasattr(self, key):
|
|
1550
|
+
return getattr(self, key)
|
|
1551
|
+
return self._data.get(key, default)
|
|
1552
|
+
|
|
1553
|
+
def get(self, key, default=None):
|
|
1554
|
+
"""Standard get method for compatibility"""
|
|
1555
|
+
if hasattr(self, key):
|
|
1556
|
+
return getattr(self, key)
|
|
1557
|
+
return self._data.get(key, default)
|
|
1558
|
+
|
|
1559
|
+
def __contains__(self, key):
|
|
1560
|
+
return hasattr(self, key) or key in self._data
|
|
1561
|
+
|
|
1562
|
+
# Replace the dict with the object
|
|
1563
|
+
cls.plotlines = PlotLinesObj(plotlines_dict)
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def _initialize_indicator_aliases():
|
|
1567
|
+
"""
|
|
1568
|
+
CRITICAL FIX: Initialize all indicator aliases and ensure _plotinit method exists
|
|
1569
|
+
This function must be called after all indicator modules are loaded
|
|
1570
|
+
"""
|
|
1571
|
+
try:
|
|
1572
|
+
global _INDICATOR_ALIASES_INITIALIZED
|
|
1573
|
+
if _INDICATOR_ALIASES_INITIALIZED:
|
|
1574
|
+
return True
|
|
1575
|
+
# Mark as initialized early to prevent re-entrancy from imports
|
|
1576
|
+
_INDICATOR_ALIASES_INITIALIZED = True
|
|
1577
|
+
import sys
|
|
1578
|
+
|
|
1579
|
+
# CRITICAL FIX: Add a universal _plotinit method to all indicator classes
|
|
1580
|
+
def universal_plotinit(self):
|
|
1581
|
+
"""Universal _plotinit method for all indicators"""
|
|
1582
|
+
# Set up default plotinfo if missing
|
|
1583
|
+
if not hasattr(self, "plotinfo"):
|
|
1584
|
+
# Create a plotinfo object that behaves like the expected plotinfo with _get method
|
|
1585
|
+
class PlotInfo:
|
|
1586
|
+
"""Plot configuration information object.
|
|
1587
|
+
|
|
1588
|
+
Stores plotting configuration for indicators and strategies.
|
|
1589
|
+
Provides both attribute and dictionary-style access with defaults.
|
|
1590
|
+
|
|
1591
|
+
Attributes:
|
|
1592
|
+
_data: Dictionary storing plot configuration values.
|
|
1593
|
+
plot: Whether to plot this item.
|
|
1594
|
+
subplot: Whether to plot in a separate subplot.
|
|
1595
|
+
plotname: Name for the plot.
|
|
1596
|
+
plotskip: Whether to skip plotting.
|
|
1597
|
+
plotabove: Whether to plot above the data.
|
|
1598
|
+
plotlinelabels: Whether to show line labels.
|
|
1599
|
+
plotlinevalues: Whether to show line values.
|
|
1600
|
+
plotvaluetags: Whether to show value tags.
|
|
1601
|
+
plotymargin: Vertical margin for the plot.
|
|
1602
|
+
plotyhlines: Horizontal lines at y values.
|
|
1603
|
+
plotyticks: Y-axis tick positions.
|
|
1604
|
+
plothlines: Horizontal lines.
|
|
1605
|
+
plotforce: Force plotting even if disabled.
|
|
1606
|
+
plotmaster: Master plot for this item.
|
|
1607
|
+
"""
|
|
1608
|
+
|
|
1609
|
+
def __init__(self):
|
|
1610
|
+
"""Initialize PlotInfo with default plotting configuration."""
|
|
1611
|
+
self._data = {}
|
|
1612
|
+
# Set default plot attributes
|
|
1613
|
+
defaults = {
|
|
1614
|
+
"plot": True,
|
|
1615
|
+
"subplot": True,
|
|
1616
|
+
"plotname": "",
|
|
1617
|
+
"plotskip": False,
|
|
1618
|
+
"plotabove": False,
|
|
1619
|
+
"plotlinelabels": False,
|
|
1620
|
+
"plotlinevalues": True,
|
|
1621
|
+
"plotvaluetags": True,
|
|
1622
|
+
"plotymargin": 0.0,
|
|
1623
|
+
"plotyhlines": [],
|
|
1624
|
+
"plotyticks": [],
|
|
1625
|
+
"plothlines": [],
|
|
1626
|
+
"plotforce": False,
|
|
1627
|
+
"plotmaster": None,
|
|
1628
|
+
}
|
|
1629
|
+
self._data.update(defaults)
|
|
1630
|
+
# CRITICAL FIX: Set attributes directly on the object for compatibility
|
|
1631
|
+
for key, value in defaults.items():
|
|
1632
|
+
setattr(self, key, value)
|
|
1633
|
+
|
|
1634
|
+
def _get(self, key, default=None):
|
|
1635
|
+
"""Get plot info attribute with default - CRITICAL METHOD"""
|
|
1636
|
+
# CRITICAL FIX: Ensure key is a string before using hasattr()
|
|
1637
|
+
if isinstance(key, str) and hasattr(self, key):
|
|
1638
|
+
return getattr(self, key)
|
|
1639
|
+
# Then try the _data dict
|
|
1640
|
+
if hasattr(self, "_data") and key in self._data:
|
|
1641
|
+
return self._data[key]
|
|
1642
|
+
return default
|
|
1643
|
+
|
|
1644
|
+
def get(self, key, default=None):
|
|
1645
|
+
"""Standard get method for dict-like access"""
|
|
1646
|
+
# CRITICAL FIX: Ensure key is a string before using hasattr()
|
|
1647
|
+
if isinstance(key, str) and hasattr(self, key):
|
|
1648
|
+
return getattr(self, key)
|
|
1649
|
+
# Then try the _data dict
|
|
1650
|
+
if hasattr(self, "_data") and key in self._data:
|
|
1651
|
+
return self._data[key]
|
|
1652
|
+
return default
|
|
1653
|
+
|
|
1654
|
+
def __getattr__(self, name):
|
|
1655
|
+
if name.startswith("_") and name != "_data":
|
|
1656
|
+
raise AttributeError(
|
|
1657
|
+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
1658
|
+
)
|
|
1659
|
+
# Try _data dict first
|
|
1660
|
+
if hasattr(self, "_data") and name in self._data:
|
|
1661
|
+
return self._data[name]
|
|
1662
|
+
# Return None for missing attributes to prevent errors
|
|
1663
|
+
return None
|
|
1664
|
+
|
|
1665
|
+
def __setattr__(self, name, value):
|
|
1666
|
+
if name.startswith("_") and name != "_data":
|
|
1667
|
+
super().__setattr__(name, value)
|
|
1668
|
+
else:
|
|
1669
|
+
if not hasattr(self, "_data"):
|
|
1670
|
+
super().__setattr__("_data", {})
|
|
1671
|
+
self._data[name] = value
|
|
1672
|
+
# CRITICAL FIX: Also set as direct attribute for compatibility
|
|
1673
|
+
super().__setattr__(name, value)
|
|
1674
|
+
|
|
1675
|
+
def __contains__(self, key):
|
|
1676
|
+
"""Support 'in' operator"""
|
|
1677
|
+
# CRITICAL FIX: Ensure key is a string before using hasattr()
|
|
1678
|
+
string_check = isinstance(key, str) and hasattr(self, key)
|
|
1679
|
+
dict_check = key in getattr(self, "_data", {})
|
|
1680
|
+
return string_check or dict_check
|
|
1681
|
+
|
|
1682
|
+
def keys(self):
|
|
1683
|
+
"""Return all keys"""
|
|
1684
|
+
keys = set(getattr(self, "_data", {}).keys())
|
|
1685
|
+
# OPTIMIZED: Use __dict__ instead of dir() for better performance
|
|
1686
|
+
keys.update(
|
|
1687
|
+
attr
|
|
1688
|
+
for attr, val in self.__dict__.items()
|
|
1689
|
+
if not attr.startswith("_") and not callable(val)
|
|
1690
|
+
)
|
|
1691
|
+
return list(keys)
|
|
1692
|
+
|
|
1693
|
+
def values(self):
|
|
1694
|
+
"""Return all values"""
|
|
1695
|
+
return [self._get(key) for key in self.keys()]
|
|
1696
|
+
|
|
1697
|
+
def items(self):
|
|
1698
|
+
"""Return all items"""
|
|
1699
|
+
return [(key, self._get(key)) for key in self.keys()]
|
|
1700
|
+
|
|
1701
|
+
self.plotinfo = PlotInfo()
|
|
1702
|
+
else:
|
|
1703
|
+
# If plotinfo exists but doesn't have _get method, add it
|
|
1704
|
+
if not hasattr(self.plotinfo, "_get"):
|
|
1705
|
+
|
|
1706
|
+
def _get_method(key, default=None):
|
|
1707
|
+
if hasattr(self.plotinfo, key):
|
|
1708
|
+
return getattr(self.plotinfo, key)
|
|
1709
|
+
if hasattr(self.plotinfo, "_data") and key in self.plotinfo._data:
|
|
1710
|
+
return self.plotinfo._data[key]
|
|
1711
|
+
return default
|
|
1712
|
+
|
|
1713
|
+
self.plotinfo._get = _get_method
|
|
1714
|
+
|
|
1715
|
+
# Also ensure get method exists
|
|
1716
|
+
if not hasattr(self.plotinfo, "get"):
|
|
1717
|
+
|
|
1718
|
+
def get_method(key, default=None):
|
|
1719
|
+
if hasattr(self.plotinfo, key):
|
|
1720
|
+
return getattr(self.plotinfo, key)
|
|
1721
|
+
if hasattr(self.plotinfo, "_data") and key in self.plotinfo._data:
|
|
1722
|
+
return self.plotinfo._data[key]
|
|
1723
|
+
return default
|
|
1724
|
+
|
|
1725
|
+
self.plotinfo.get = get_method
|
|
1726
|
+
|
|
1727
|
+
return True
|
|
1728
|
+
|
|
1729
|
+
# CRITICAL FIX: Apply _plotinit to indicator classes without complex patching
|
|
1730
|
+
indicators_module = sys.modules.get("backtrader.indicators")
|
|
1731
|
+
if indicators_module:
|
|
1732
|
+
for attr_name in dir(indicators_module):
|
|
1733
|
+
try:
|
|
1734
|
+
attr = getattr(indicators_module, attr_name)
|
|
1735
|
+
if (
|
|
1736
|
+
isinstance(attr, type)
|
|
1737
|
+
and hasattr(attr, "__module__")
|
|
1738
|
+
and "indicator" in attr.__module__.lower()
|
|
1739
|
+
and hasattr(attr, "lines")
|
|
1740
|
+
):
|
|
1741
|
+
# Add _plotinit method if missing
|
|
1742
|
+
if not hasattr(attr, "_plotinit"):
|
|
1743
|
+
attr._plotinit = universal_plotinit
|
|
1744
|
+
|
|
1745
|
+
# CRITICAL FIX: Convert plotlines dict to object with _get method
|
|
1746
|
+
if hasattr(attr, "plotlines") and isinstance(attr.plotlines, dict):
|
|
1747
|
+
_convert_plotlines_dict_to_object(attr)
|
|
1748
|
+
|
|
1749
|
+
except Exception: # nosec B112
|
|
1750
|
+
logger.warning("metabase:1743 suppressed Exception")
|
|
1751
|
+
continue
|
|
1752
|
+
|
|
1753
|
+
# CRITICAL FIX: Patch specific indicator classes that are known to be problematic
|
|
1754
|
+
try:
|
|
1755
|
+
from .indicators.sma import MovingAverageSimple
|
|
1756
|
+
|
|
1757
|
+
if not hasattr(MovingAverageSimple, "_plotinit"):
|
|
1758
|
+
MovingAverageSimple._plotinit = universal_plotinit
|
|
1759
|
+
except ImportError:
|
|
1760
|
+
# SMA module not importable in this context; nothing to patch.
|
|
1761
|
+
logger.warning("metabase:1753 suppressed ImportError")
|
|
1762
|
+
|
|
1763
|
+
# CRITICAL FIX: Search for any loaded indicator classes and ensure they have _plotinit
|
|
1764
|
+
for module_name, module in sys.modules.items():
|
|
1765
|
+
if "indicator" in module_name.lower() and hasattr(module, "__dict__"):
|
|
1766
|
+
for attr_name, attr_value in module.__dict__.items():
|
|
1767
|
+
try:
|
|
1768
|
+
if (
|
|
1769
|
+
isinstance(attr_value, type)
|
|
1770
|
+
and hasattr(attr_value, "lines")
|
|
1771
|
+
and "Indicator" in str(attr_value.__mro__)
|
|
1772
|
+
):
|
|
1773
|
+
# Ensure the class has _plotinit
|
|
1774
|
+
if not hasattr(attr_value, "_plotinit"):
|
|
1775
|
+
attr_value._plotinit = universal_plotinit
|
|
1776
|
+
|
|
1777
|
+
# CRITICAL FIX: Convert plotlines dict to object with _get method
|
|
1778
|
+
if hasattr(attr_value, "plotlines") and isinstance(
|
|
1779
|
+
attr_value.plotlines, dict
|
|
1780
|
+
):
|
|
1781
|
+
_convert_plotlines_dict_to_object(attr_value)
|
|
1782
|
+
|
|
1783
|
+
# CRITICAL FIX: Also handle Mixin classes that have plotlines
|
|
1784
|
+
elif (
|
|
1785
|
+
isinstance(attr_value, type)
|
|
1786
|
+
and hasattr(attr_value, "plotlines")
|
|
1787
|
+
and isinstance(attr_value.plotlines, dict)
|
|
1788
|
+
):
|
|
1789
|
+
_convert_plotlines_dict_to_object(attr_value)
|
|
1790
|
+
|
|
1791
|
+
except Exception: # nosec B112
|
|
1792
|
+
logger.warning("metabase:1784 suppressed Exception")
|
|
1793
|
+
continue
|
|
1794
|
+
|
|
1795
|
+
except Exception as e:
|
|
1796
|
+
logger.debug("Failed in _initialize_indicator_aliases: %s", e)
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
# CRITICAL FIX: Call initialization functions when module loads
|
|
1800
|
+
try:
|
|
1801
|
+
_initialize_indicator_aliases()
|
|
1802
|
+
patch_strategy_clk_update()
|
|
1803
|
+
except Exception as e:
|
|
1804
|
+
logger.debug("Failed to initialize at module load: %s", e)
|