back-trader-python 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. back_trader_python-1.4.0.dist-info/METADATA +1491 -0
  2. back_trader_python-1.4.0.dist-info/RECORD +465 -0
  3. back_trader_python-1.4.0.dist-info/WHEEL +5 -0
  4. back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
  5. back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
  6. backtrader/__init__.py +148 -0
  7. backtrader/_cerebro/__init__.py +5 -0
  8. backtrader/_cerebro/channel.py +382 -0
  9. backtrader/_cerebro/execution.py +377 -0
  10. backtrader/_cerebro/lifecycle.py +143 -0
  11. backtrader/_cerebro/notifications.py +150 -0
  12. backtrader/_cerebro/presentation.py +230 -0
  13. backtrader/_cerebro/registry.py +593 -0
  14. backtrader/_cerebro/runnext.py +551 -0
  15. backtrader/_cerebro/runonce.py +142 -0
  16. backtrader/analyzer.py +594 -0
  17. backtrader/analyzers/__init__.py +50 -0
  18. backtrader/analyzers/annualreturn.py +226 -0
  19. backtrader/analyzers/calmar.py +165 -0
  20. backtrader/analyzers/drawdown.py +287 -0
  21. backtrader/analyzers/leverage.py +112 -0
  22. backtrader/analyzers/logreturnsrolling.py +190 -0
  23. backtrader/analyzers/periodstats.py +153 -0
  24. backtrader/analyzers/positions.py +119 -0
  25. backtrader/analyzers/pyfolio.py +470 -0
  26. backtrader/analyzers/returns.py +192 -0
  27. backtrader/analyzers/sharpe.py +307 -0
  28. backtrader/analyzers/sharpe_ratio_stats.py +534 -0
  29. backtrader/analyzers/sqn.py +112 -0
  30. backtrader/analyzers/timereturn.py +192 -0
  31. backtrader/analyzers/total_value.py +75 -0
  32. backtrader/analyzers/tradeanalyzer.py +278 -0
  33. backtrader/analyzers/transactions.py +141 -0
  34. backtrader/analyzers/vwr.py +245 -0
  35. backtrader/bokeh/__init__.py +155 -0
  36. backtrader/bokeh/analyzers/__init__.py +13 -0
  37. backtrader/bokeh/analyzers/plot.py +192 -0
  38. backtrader/bokeh/analyzers/recorder.py +181 -0
  39. backtrader/bokeh/app.py +1094 -0
  40. backtrader/bokeh/live/__init__.py +11 -0
  41. backtrader/bokeh/live/client.py +352 -0
  42. backtrader/bokeh/live/datahandler.py +346 -0
  43. backtrader/bokeh/plot_adapter.py +200 -0
  44. backtrader/bokeh/schemes/__init__.py +14 -0
  45. backtrader/bokeh/schemes/blackly.py +76 -0
  46. backtrader/bokeh/schemes/scheme.py +150 -0
  47. backtrader/bokeh/schemes/tradimo.py +82 -0
  48. backtrader/bokeh/tab.py +125 -0
  49. backtrader/bokeh/tabs/__init__.py +30 -0
  50. backtrader/bokeh/tabs/analyzer.py +120 -0
  51. backtrader/bokeh/tabs/config.py +154 -0
  52. backtrader/bokeh/tabs/live.py +109 -0
  53. backtrader/bokeh/tabs/log.py +185 -0
  54. backtrader/bokeh/tabs/metadata.py +182 -0
  55. backtrader/bokeh/tabs/performance.py +359 -0
  56. backtrader/bokeh/tabs/source.py +70 -0
  57. backtrader/bokeh/utils/__init__.py +8 -0
  58. backtrader/bokeh/utils/helpers.py +167 -0
  59. backtrader/bokeh/webapp.py +164 -0
  60. backtrader/broker.py +478 -0
  61. backtrader/brokers/__init__.py +36 -0
  62. backtrader/brokers/bbroker.py +2576 -0
  63. backtrader/brokers/btapibroker.py +8227 -0
  64. backtrader/brokers/hft/__init__.py +89 -0
  65. backtrader/brokers/hft/binance_bbo.py +625 -0
  66. backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
  67. backtrader/brokers/hft/examples.py +1228 -0
  68. backtrader/brokers/hft/exchange.py +380 -0
  69. backtrader/brokers/hft/latency.py +309 -0
  70. backtrader/brokers/hft/matching_core.py +572 -0
  71. backtrader/brokers/hft/queue.py +238 -0
  72. backtrader/brokers/hft/recorder.py +88 -0
  73. backtrader/brokers/hft/state.py +138 -0
  74. backtrader/brokers/impact_models.py +118 -0
  75. backtrader/brokers/mixbroker.py +895 -0
  76. backtrader/brokers/tickbroker.py +1991 -0
  77. backtrader/btrun/__init__.py +12 -0
  78. backtrader/btrun/btrun.py +1218 -0
  79. backtrader/cerebro.py +828 -0
  80. backtrader/channel.py +682 -0
  81. backtrader/channels/__init__.py +23 -0
  82. backtrader/channels/bridge.py +186 -0
  83. backtrader/channels/funding.py +248 -0
  84. backtrader/channels/live_queue.py +216 -0
  85. backtrader/channels/live_validator.py +294 -0
  86. backtrader/channels/orderbook.py +257 -0
  87. backtrader/channels/tick.py +202 -0
  88. backtrader/comminfo.py +665 -0
  89. backtrader/commissions/__init__.py +106 -0
  90. backtrader/commissions/ctpoption.py +993 -0
  91. backtrader/configs/account_config_example.yaml +8 -0
  92. backtrader/dataseries.py +379 -0
  93. backtrader/errors.py +106 -0
  94. backtrader/events.py +980 -0
  95. backtrader/feed.py +1523 -0
  96. backtrader/feeds/__init__.py +75 -0
  97. backtrader/feeds/barrier.py +2006 -0
  98. backtrader/feeds/blaze.py +118 -0
  99. backtrader/feeds/btapifeed.py +1538 -0
  100. backtrader/feeds/btcsv.py +203 -0
  101. backtrader/feeds/chainer.py +114 -0
  102. backtrader/feeds/cryptohftdata.py +164 -0
  103. backtrader/feeds/csvgeneric.py +1205 -0
  104. backtrader/feeds/ctpcohort.py +1051 -0
  105. backtrader/feeds/influxfeed.py +158 -0
  106. backtrader/feeds/livefeed.py +71 -0
  107. backtrader/feeds/mixed_channel.py +108 -0
  108. backtrader/feeds/mt4csv.py +42 -0
  109. backtrader/feeds/pandafeed.py +381 -0
  110. backtrader/feeds/quandl.py +256 -0
  111. backtrader/feeds/rollover.py +229 -0
  112. backtrader/feeds/sierrachart.py +30 -0
  113. backtrader/feeds/vchart.py +162 -0
  114. backtrader/feeds/vchartcsv.py +84 -0
  115. backtrader/feeds/vchartfile.py +153 -0
  116. backtrader/feeds/yahoo.py +399 -0
  117. backtrader/fillers.py +148 -0
  118. backtrader/filters/__init__.py +34 -0
  119. backtrader/filters/bsplitter.py +127 -0
  120. backtrader/filters/calendardays.py +121 -0
  121. backtrader/filters/datafiller.py +192 -0
  122. backtrader/filters/datafilter.py +74 -0
  123. backtrader/filters/daysteps.py +96 -0
  124. backtrader/filters/heikinashi.py +63 -0
  125. backtrader/filters/renko.py +164 -0
  126. backtrader/filters/session.py +289 -0
  127. backtrader/flt.py +80 -0
  128. backtrader/functions.py +960 -0
  129. backtrader/indicator.py +449 -0
  130. backtrader/indicators/__init__.py +148 -0
  131. backtrader/indicators/accdecoscillator.py +110 -0
  132. backtrader/indicators/aroon.py +300 -0
  133. backtrader/indicators/atr.py +315 -0
  134. backtrader/indicators/awesomeoscillator.py +122 -0
  135. backtrader/indicators/basicops.py +834 -0
  136. backtrader/indicators/bollinger.py +223 -0
  137. backtrader/indicators/cci.py +89 -0
  138. backtrader/indicators/channels_ext.py +83 -0
  139. backtrader/indicators/contrib/__init__.py +228 -0
  140. backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
  141. backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
  142. backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
  143. backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
  144. backtrader/indicators/contrib/adxdmi.py +34 -0
  145. backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
  146. backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
  147. backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
  148. backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
  149. backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
  150. backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
  151. backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
  152. backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
  153. backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
  154. backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
  155. backtrader/indicators/contrib/average_change_candle.py +165 -0
  156. backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
  157. backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
  158. backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
  159. backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
  160. backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
  161. backtrader/indicators/contrib/blau_csi.py +76 -0
  162. backtrader/indicators/contrib/blau_ergodic.py +53 -0
  163. backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
  164. backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
  165. backtrader/indicators/contrib/blau_tvi.py +55 -0
  166. backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
  167. backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
  168. backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
  169. backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
  170. backtrader/indicators/contrib/bsi_indicator.py +87 -0
  171. backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
  172. backtrader/indicators/contrib/bulls_power.py +56 -0
  173. backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
  174. backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
  175. backtrader/indicators/contrib/candle_stop_color.py +46 -0
  176. backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
  177. backtrader/indicators/contrib/candlesticks_bw.py +45 -0
  178. backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
  179. backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
  180. backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
  181. backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
  182. backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
  183. backtrader/indicators/contrib/cg_oscillator.py +40 -0
  184. backtrader/indicators/contrib/close_line_cci.py +38 -0
  185. backtrader/indicators/contrib/close_price_fractals.py +47 -0
  186. backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
  187. backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
  188. backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
  189. backtrader/indicators/contrib/color_hma.py +71 -0
  190. backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
  191. backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
  192. backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
  193. backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
  194. backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
  195. backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
  196. backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
  197. backtrader/indicators/contrib/color_x2_ma.py +49 -0
  198. backtrader/indicators/contrib/color_x_derivative.py +63 -0
  199. backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
  200. backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
  201. backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
  202. backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
  203. backtrader/indicators/contrib/derivative_indicator.py +95 -0
  204. backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
  205. backtrader/indicators/contrib/digital_macd.py +200 -0
  206. backtrader/indicators/contrib/donchian_channels_system.py +45 -0
  207. backtrader/indicators/contrib/dots_indicator.py +93 -0
  208. backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
  209. backtrader/indicators/contrib/ema_rsi_va.py +80 -0
  210. backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
  211. backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
  212. backtrader/indicators/contrib/fatl_filter.py +179 -0
  213. backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
  214. backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
  215. backtrader/indicators/contrib/fisher_org_v1.py +102 -0
  216. backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
  217. backtrader/indicators/contrib/force_index_ema.py +96 -0
  218. backtrader/indicators/contrib/force_index_ema_2.py +27 -0
  219. backtrader/indicators/contrib/forecast_oscilator.py +145 -0
  220. backtrader/indicators/contrib/fractal_amambk.py +81 -0
  221. backtrader/indicators/contrib/frama_series.py +84 -0
  222. backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
  223. backtrader/indicators/contrib/go_indicator.py +93 -0
  224. backtrader/indicators/contrib/hlr_indicator.py +95 -0
  225. backtrader/indicators/contrib/hma.py +50 -0
  226. backtrader/indicators/contrib/i4_drfv2.py +34 -0
  227. backtrader/indicators/contrib/i4_drfv3.py +38 -0
  228. backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
  229. backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
  230. backtrader/indicators/contrib/i_gap_indicator.py +45 -0
  231. backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
  232. backtrader/indicators/contrib/i_trend_indicator.py +125 -0
  233. backtrader/indicators/contrib/iamma_indicator.py +39 -0
  234. backtrader/indicators/contrib/indexed_moving_average.py +33 -0
  235. backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
  236. backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
  237. backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
  238. backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
  239. backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
  240. backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
  241. backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
  242. backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
  243. backtrader/indicators/contrib/kalman_filter_line.py +127 -0
  244. backtrader/indicators/contrib/kama_indicator.py +150 -0
  245. backtrader/indicators/contrib/karacatica_indicator.py +99 -0
  246. backtrader/indicators/contrib/kdj_indicator.py +59 -0
  247. backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
  248. backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
  249. backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
  250. backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
  251. backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
  252. backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
  253. backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
  254. backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
  255. backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
  256. backtrader/indicators/contrib/loco_indicator.py +88 -0
  257. backtrader/indicators/contrib/lrma_indicator.py +185 -0
  258. backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
  259. backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
  260. backtrader/indicators/contrib/macd2_indicator.py +61 -0
  261. backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
  262. backtrader/indicators/contrib/malr_indicator.py +77 -0
  263. backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
  264. backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
  265. backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
  266. backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
  267. backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
  268. backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
  269. backtrader/indicators/contrib/nrtr_indicator.py +95 -0
  270. backtrader/indicators/contrib/p_channel_system.py +40 -0
  271. backtrader/indicators/contrib/percent_envelope.py +37 -0
  272. backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
  273. backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
  274. backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
  275. backtrader/indicators/contrib/price_extreme_channel.py +35 -0
  276. backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
  277. backtrader/indicators/contrib/ravi_indicator.py +40 -0
  278. backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
  279. backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
  280. backtrader/indicators/contrib/renko_level.py +85 -0
  281. backtrader/indicators/contrib/renko_line_break.py +91 -0
  282. backtrader/indicators/contrib/rftl_indicator.py +41 -0
  283. backtrader/indicators/contrib/rkd_indicator.py +53 -0
  284. backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
  285. backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
  286. backtrader/indicators/contrib/rsi_slowdown.py +57 -0
  287. backtrader/indicators/contrib/rsioma_v2.py +41 -0
  288. backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
  289. backtrader/indicators/contrib/safe_adx.py +89 -0
  290. backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
  291. backtrader/indicators/contrib/sidus_indicator.py +105 -0
  292. backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
  293. backtrader/indicators/contrib/sliding_range_color.py +56 -0
  294. backtrader/indicators/contrib/slow_stoch.py +42 -0
  295. backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
  296. backtrader/indicators/contrib/smoothed_rsi.py +31 -0
  297. backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
  298. backtrader/indicators/contrib/stalin_indicator.py +152 -0
  299. backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
  300. backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
  301. backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
  302. backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
  303. backtrader/indicators/contrib/t3_average.py +76 -0
  304. backtrader/indicators/contrib/t3_indicator.py +40 -0
  305. backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
  306. backtrader/indicators/contrib/three_candles_indicator.py +70 -0
  307. backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
  308. backtrader/indicators/contrib/time_line.py +57 -0
  309. backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
  310. backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
  311. backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
  312. backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
  313. backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
  314. backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
  315. backtrader/indicators/contrib/trigger_line.py +66 -0
  316. backtrader/indicators/contrib/triple_ema_rate.py +34 -0
  317. backtrader/indicators/contrib/trvi_indicator.py +194 -0
  318. backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
  319. backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
  320. backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
  321. backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
  322. backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
  323. backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
  324. backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
  325. backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
  326. backtrader/indicators/contrib/vwma_candle.py +57 -0
  327. backtrader/indicators/contrib/vwma_digit_system.py +70 -0
  328. backtrader/indicators/contrib/wami.py +43 -0
  329. backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
  330. backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
  331. backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
  332. backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
  333. backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
  334. backtrader/indicators/contrib/xma_ichimoku.py +163 -0
  335. backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
  336. backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
  337. backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
  338. backtrader/indicators/contrib/xmacd_indicator.py +70 -0
  339. backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
  340. backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
  341. backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
  342. backtrader/indicators/contrib/xrvi_indicator.py +130 -0
  343. backtrader/indicators/contrib/zero_lag_macd.py +36 -0
  344. backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
  345. backtrader/indicators/contrib/zpf_indicator.py +115 -0
  346. backtrader/indicators/crossover.py +337 -0
  347. backtrader/indicators/dema.py +175 -0
  348. backtrader/indicators/demarker.py +270 -0
  349. backtrader/indicators/deviation.py +284 -0
  350. backtrader/indicators/directionalmove.py +1071 -0
  351. backtrader/indicators/dma.py +112 -0
  352. backtrader/indicators/dpo.py +96 -0
  353. backtrader/indicators/dv2.py +56 -0
  354. backtrader/indicators/ema.py +145 -0
  355. backtrader/indicators/envelope.py +475 -0
  356. backtrader/indicators/hadelta.py +198 -0
  357. backtrader/indicators/heikinashi.py +153 -0
  358. backtrader/indicators/hma.py +153 -0
  359. backtrader/indicators/hurst.py +151 -0
  360. backtrader/indicators/ichimoku.py +267 -0
  361. backtrader/indicators/kama.py +181 -0
  362. backtrader/indicators/kst.py +159 -0
  363. backtrader/indicators/lrsi.py +125 -0
  364. backtrader/indicators/mabase.py +147 -0
  365. backtrader/indicators/macd.py +322 -0
  366. backtrader/indicators/momentum.py +267 -0
  367. backtrader/indicators/moneyflow.py +237 -0
  368. backtrader/indicators/mt5atr.py +124 -0
  369. backtrader/indicators/myind.py +179 -0
  370. backtrader/indicators/obv.py +94 -0
  371. backtrader/indicators/ols.py +265 -0
  372. backtrader/indicators/oscillator.py +161 -0
  373. backtrader/indicators/percentchange.py +83 -0
  374. backtrader/indicators/percentrank.py +46 -0
  375. backtrader/indicators/pivotpoint.py +469 -0
  376. backtrader/indicators/prettygoodoscillator.py +113 -0
  377. backtrader/indicators/priceops_ext.py +123 -0
  378. backtrader/indicators/priceoscillator.py +262 -0
  379. backtrader/indicators/psar.py +212 -0
  380. backtrader/indicators/rmi.py +69 -0
  381. backtrader/indicators/rsi.py +440 -0
  382. backtrader/indicators/sma.py +141 -0
  383. backtrader/indicators/smma.py +116 -0
  384. backtrader/indicators/spread.py +54 -0
  385. backtrader/indicators/stochastic.py +263 -0
  386. backtrader/indicators/supertrend.py +436 -0
  387. backtrader/indicators/trend_ext.py +105 -0
  388. backtrader/indicators/trix.py +202 -0
  389. backtrader/indicators/tsi.py +155 -0
  390. backtrader/indicators/ultimateoscillator.py +158 -0
  391. backtrader/indicators/vortex.py +62 -0
  392. backtrader/indicators/williams.py +194 -0
  393. backtrader/indicators/wma.py +103 -0
  394. backtrader/indicators/zlema.py +135 -0
  395. backtrader/indicators/zlind.py +104 -0
  396. backtrader/linebuffer.py +3155 -0
  397. backtrader/lineiterator.py +2911 -0
  398. backtrader/lineroot.py +1106 -0
  399. backtrader/lineseries.py +2559 -0
  400. backtrader/live_trading/__init__.py +31 -0
  401. backtrader/live_trading/interface.py +404 -0
  402. backtrader/mathsupport.py +94 -0
  403. backtrader/metabase.py +1804 -0
  404. backtrader/mixins/__init__.py +21 -0
  405. backtrader/mixins/singleton.py +118 -0
  406. backtrader/observer.py +106 -0
  407. backtrader/observers/__init__.py +45 -0
  408. backtrader/observers/benchmark.py +126 -0
  409. backtrader/observers/broker.py +184 -0
  410. backtrader/observers/buysell.py +144 -0
  411. backtrader/observers/drawdown.py +161 -0
  412. backtrader/observers/logreturns.py +113 -0
  413. backtrader/observers/timereturn.py +86 -0
  414. backtrader/observers/trade_logger.py +2972 -0
  415. backtrader/observers/tradelogger.py +6 -0
  416. backtrader/observers/trades.py +258 -0
  417. backtrader/order.py +1114 -0
  418. backtrader/parameters.py +2345 -0
  419. backtrader/plot/__init__.py +54 -0
  420. backtrader/plot/finance.py +1022 -0
  421. backtrader/plot/formatters.py +200 -0
  422. backtrader/plot/locator.py +353 -0
  423. backtrader/plot/multicursor.py +495 -0
  424. backtrader/plot/plot.py +2500 -0
  425. backtrader/plot/plot_plotly.py +1351 -0
  426. backtrader/plot/scheme.py +253 -0
  427. backtrader/plot/utils.py +104 -0
  428. backtrader/position.py +290 -0
  429. backtrader/position_modes.py +132 -0
  430. backtrader/profiles.py +254 -0
  431. backtrader/reports/__init__.py +39 -0
  432. backtrader/reports/charts.py +371 -0
  433. backtrader/reports/performance.py +620 -0
  434. backtrader/reports/reporter.py +660 -0
  435. backtrader/resamplerfilter.py +1001 -0
  436. backtrader/signal.py +118 -0
  437. backtrader/signals/__init__.py +17 -0
  438. backtrader/sizer.py +114 -0
  439. backtrader/sizers/__init__.py +26 -0
  440. backtrader/sizers/fixedsize.py +161 -0
  441. backtrader/sizers/percents_sizer.py +119 -0
  442. backtrader/store.py +221 -0
  443. backtrader/stores/__init__.py +33 -0
  444. backtrader/stores/btapistore.py +15506 -0
  445. backtrader/stores/livestore.py +137 -0
  446. backtrader/stores/vchartfile.py +96 -0
  447. backtrader/strategy.py +3655 -0
  448. backtrader/talib.py +280 -0
  449. backtrader/test_helpers.py +96 -0
  450. backtrader/timer.py +358 -0
  451. backtrader/trade.py +442 -0
  452. backtrader/tradingcal.py +361 -0
  453. backtrader/utils/__init__.py +68 -0
  454. backtrader/utils/autodict.py +251 -0
  455. backtrader/utils/date.py +71 -0
  456. backtrader/utils/dateintern.py +509 -0
  457. backtrader/utils/flushfile.py +94 -0
  458. backtrader/utils/fractal.py +101 -0
  459. backtrader/utils/get_metrics.py +101 -0
  460. backtrader/utils/load_data.py +209 -0
  461. backtrader/utils/log_message.py +998 -0
  462. backtrader/utils/ordereddefaultdict.py +75 -0
  463. backtrader/utils/py3.py +296 -0
  464. backtrader/version.py +21 -0
  465. backtrader/writer.py +372 -0
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)