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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. back_trader_python-1.4.0.dist-info/METADATA +1491 -0
  2. back_trader_python-1.4.0.dist-info/RECORD +465 -0
  3. back_trader_python-1.4.0.dist-info/WHEEL +5 -0
  4. back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
  5. back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
  6. backtrader/__init__.py +148 -0
  7. backtrader/_cerebro/__init__.py +5 -0
  8. backtrader/_cerebro/channel.py +382 -0
  9. backtrader/_cerebro/execution.py +377 -0
  10. backtrader/_cerebro/lifecycle.py +143 -0
  11. backtrader/_cerebro/notifications.py +150 -0
  12. backtrader/_cerebro/presentation.py +230 -0
  13. backtrader/_cerebro/registry.py +593 -0
  14. backtrader/_cerebro/runnext.py +551 -0
  15. backtrader/_cerebro/runonce.py +142 -0
  16. backtrader/analyzer.py +594 -0
  17. backtrader/analyzers/__init__.py +50 -0
  18. backtrader/analyzers/annualreturn.py +226 -0
  19. backtrader/analyzers/calmar.py +165 -0
  20. backtrader/analyzers/drawdown.py +287 -0
  21. backtrader/analyzers/leverage.py +112 -0
  22. backtrader/analyzers/logreturnsrolling.py +190 -0
  23. backtrader/analyzers/periodstats.py +153 -0
  24. backtrader/analyzers/positions.py +119 -0
  25. backtrader/analyzers/pyfolio.py +470 -0
  26. backtrader/analyzers/returns.py +192 -0
  27. backtrader/analyzers/sharpe.py +307 -0
  28. backtrader/analyzers/sharpe_ratio_stats.py +534 -0
  29. backtrader/analyzers/sqn.py +112 -0
  30. backtrader/analyzers/timereturn.py +192 -0
  31. backtrader/analyzers/total_value.py +75 -0
  32. backtrader/analyzers/tradeanalyzer.py +278 -0
  33. backtrader/analyzers/transactions.py +141 -0
  34. backtrader/analyzers/vwr.py +245 -0
  35. backtrader/bokeh/__init__.py +155 -0
  36. backtrader/bokeh/analyzers/__init__.py +13 -0
  37. backtrader/bokeh/analyzers/plot.py +192 -0
  38. backtrader/bokeh/analyzers/recorder.py +181 -0
  39. backtrader/bokeh/app.py +1094 -0
  40. backtrader/bokeh/live/__init__.py +11 -0
  41. backtrader/bokeh/live/client.py +352 -0
  42. backtrader/bokeh/live/datahandler.py +346 -0
  43. backtrader/bokeh/plot_adapter.py +200 -0
  44. backtrader/bokeh/schemes/__init__.py +14 -0
  45. backtrader/bokeh/schemes/blackly.py +76 -0
  46. backtrader/bokeh/schemes/scheme.py +150 -0
  47. backtrader/bokeh/schemes/tradimo.py +82 -0
  48. backtrader/bokeh/tab.py +125 -0
  49. backtrader/bokeh/tabs/__init__.py +30 -0
  50. backtrader/bokeh/tabs/analyzer.py +120 -0
  51. backtrader/bokeh/tabs/config.py +154 -0
  52. backtrader/bokeh/tabs/live.py +109 -0
  53. backtrader/bokeh/tabs/log.py +185 -0
  54. backtrader/bokeh/tabs/metadata.py +182 -0
  55. backtrader/bokeh/tabs/performance.py +359 -0
  56. backtrader/bokeh/tabs/source.py +70 -0
  57. backtrader/bokeh/utils/__init__.py +8 -0
  58. backtrader/bokeh/utils/helpers.py +167 -0
  59. backtrader/bokeh/webapp.py +164 -0
  60. backtrader/broker.py +478 -0
  61. backtrader/brokers/__init__.py +36 -0
  62. backtrader/brokers/bbroker.py +2576 -0
  63. backtrader/brokers/btapibroker.py +8227 -0
  64. backtrader/brokers/hft/__init__.py +89 -0
  65. backtrader/brokers/hft/binance_bbo.py +625 -0
  66. backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
  67. backtrader/brokers/hft/examples.py +1228 -0
  68. backtrader/brokers/hft/exchange.py +380 -0
  69. backtrader/brokers/hft/latency.py +309 -0
  70. backtrader/brokers/hft/matching_core.py +572 -0
  71. backtrader/brokers/hft/queue.py +238 -0
  72. backtrader/brokers/hft/recorder.py +88 -0
  73. backtrader/brokers/hft/state.py +138 -0
  74. backtrader/brokers/impact_models.py +118 -0
  75. backtrader/brokers/mixbroker.py +895 -0
  76. backtrader/brokers/tickbroker.py +1991 -0
  77. backtrader/btrun/__init__.py +12 -0
  78. backtrader/btrun/btrun.py +1218 -0
  79. backtrader/cerebro.py +828 -0
  80. backtrader/channel.py +682 -0
  81. backtrader/channels/__init__.py +23 -0
  82. backtrader/channels/bridge.py +186 -0
  83. backtrader/channels/funding.py +248 -0
  84. backtrader/channels/live_queue.py +216 -0
  85. backtrader/channels/live_validator.py +294 -0
  86. backtrader/channels/orderbook.py +257 -0
  87. backtrader/channels/tick.py +202 -0
  88. backtrader/comminfo.py +665 -0
  89. backtrader/commissions/__init__.py +106 -0
  90. backtrader/commissions/ctpoption.py +993 -0
  91. backtrader/configs/account_config_example.yaml +8 -0
  92. backtrader/dataseries.py +379 -0
  93. backtrader/errors.py +106 -0
  94. backtrader/events.py +980 -0
  95. backtrader/feed.py +1523 -0
  96. backtrader/feeds/__init__.py +75 -0
  97. backtrader/feeds/barrier.py +2006 -0
  98. backtrader/feeds/blaze.py +118 -0
  99. backtrader/feeds/btapifeed.py +1538 -0
  100. backtrader/feeds/btcsv.py +203 -0
  101. backtrader/feeds/chainer.py +114 -0
  102. backtrader/feeds/cryptohftdata.py +164 -0
  103. backtrader/feeds/csvgeneric.py +1205 -0
  104. backtrader/feeds/ctpcohort.py +1051 -0
  105. backtrader/feeds/influxfeed.py +158 -0
  106. backtrader/feeds/livefeed.py +71 -0
  107. backtrader/feeds/mixed_channel.py +108 -0
  108. backtrader/feeds/mt4csv.py +42 -0
  109. backtrader/feeds/pandafeed.py +381 -0
  110. backtrader/feeds/quandl.py +256 -0
  111. backtrader/feeds/rollover.py +229 -0
  112. backtrader/feeds/sierrachart.py +30 -0
  113. backtrader/feeds/vchart.py +162 -0
  114. backtrader/feeds/vchartcsv.py +84 -0
  115. backtrader/feeds/vchartfile.py +153 -0
  116. backtrader/feeds/yahoo.py +399 -0
  117. backtrader/fillers.py +148 -0
  118. backtrader/filters/__init__.py +34 -0
  119. backtrader/filters/bsplitter.py +127 -0
  120. backtrader/filters/calendardays.py +121 -0
  121. backtrader/filters/datafiller.py +192 -0
  122. backtrader/filters/datafilter.py +74 -0
  123. backtrader/filters/daysteps.py +96 -0
  124. backtrader/filters/heikinashi.py +63 -0
  125. backtrader/filters/renko.py +164 -0
  126. backtrader/filters/session.py +289 -0
  127. backtrader/flt.py +80 -0
  128. backtrader/functions.py +960 -0
  129. backtrader/indicator.py +449 -0
  130. backtrader/indicators/__init__.py +148 -0
  131. backtrader/indicators/accdecoscillator.py +110 -0
  132. backtrader/indicators/aroon.py +300 -0
  133. backtrader/indicators/atr.py +315 -0
  134. backtrader/indicators/awesomeoscillator.py +122 -0
  135. backtrader/indicators/basicops.py +834 -0
  136. backtrader/indicators/bollinger.py +223 -0
  137. backtrader/indicators/cci.py +89 -0
  138. backtrader/indicators/channels_ext.py +83 -0
  139. backtrader/indicators/contrib/__init__.py +228 -0
  140. backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
  141. backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
  142. backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
  143. backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
  144. backtrader/indicators/contrib/adxdmi.py +34 -0
  145. backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
  146. backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
  147. backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
  148. backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
  149. backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
  150. backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
  151. backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
  152. backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
  153. backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
  154. backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
  155. backtrader/indicators/contrib/average_change_candle.py +165 -0
  156. backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
  157. backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
  158. backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
  159. backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
  160. backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
  161. backtrader/indicators/contrib/blau_csi.py +76 -0
  162. backtrader/indicators/contrib/blau_ergodic.py +53 -0
  163. backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
  164. backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
  165. backtrader/indicators/contrib/blau_tvi.py +55 -0
  166. backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
  167. backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
  168. backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
  169. backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
  170. backtrader/indicators/contrib/bsi_indicator.py +87 -0
  171. backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
  172. backtrader/indicators/contrib/bulls_power.py +56 -0
  173. backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
  174. backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
  175. backtrader/indicators/contrib/candle_stop_color.py +46 -0
  176. backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
  177. backtrader/indicators/contrib/candlesticks_bw.py +45 -0
  178. backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
  179. backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
  180. backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
  181. backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
  182. backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
  183. backtrader/indicators/contrib/cg_oscillator.py +40 -0
  184. backtrader/indicators/contrib/close_line_cci.py +38 -0
  185. backtrader/indicators/contrib/close_price_fractals.py +47 -0
  186. backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
  187. backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
  188. backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
  189. backtrader/indicators/contrib/color_hma.py +71 -0
  190. backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
  191. backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
  192. backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
  193. backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
  194. backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
  195. backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
  196. backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
  197. backtrader/indicators/contrib/color_x2_ma.py +49 -0
  198. backtrader/indicators/contrib/color_x_derivative.py +63 -0
  199. backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
  200. backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
  201. backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
  202. backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
  203. backtrader/indicators/contrib/derivative_indicator.py +95 -0
  204. backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
  205. backtrader/indicators/contrib/digital_macd.py +200 -0
  206. backtrader/indicators/contrib/donchian_channels_system.py +45 -0
  207. backtrader/indicators/contrib/dots_indicator.py +93 -0
  208. backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
  209. backtrader/indicators/contrib/ema_rsi_va.py +80 -0
  210. backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
  211. backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
  212. backtrader/indicators/contrib/fatl_filter.py +179 -0
  213. backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
  214. backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
  215. backtrader/indicators/contrib/fisher_org_v1.py +102 -0
  216. backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
  217. backtrader/indicators/contrib/force_index_ema.py +96 -0
  218. backtrader/indicators/contrib/force_index_ema_2.py +27 -0
  219. backtrader/indicators/contrib/forecast_oscilator.py +145 -0
  220. backtrader/indicators/contrib/fractal_amambk.py +81 -0
  221. backtrader/indicators/contrib/frama_series.py +84 -0
  222. backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
  223. backtrader/indicators/contrib/go_indicator.py +93 -0
  224. backtrader/indicators/contrib/hlr_indicator.py +95 -0
  225. backtrader/indicators/contrib/hma.py +50 -0
  226. backtrader/indicators/contrib/i4_drfv2.py +34 -0
  227. backtrader/indicators/contrib/i4_drfv3.py +38 -0
  228. backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
  229. backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
  230. backtrader/indicators/contrib/i_gap_indicator.py +45 -0
  231. backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
  232. backtrader/indicators/contrib/i_trend_indicator.py +125 -0
  233. backtrader/indicators/contrib/iamma_indicator.py +39 -0
  234. backtrader/indicators/contrib/indexed_moving_average.py +33 -0
  235. backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
  236. backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
  237. backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
  238. backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
  239. backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
  240. backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
  241. backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
  242. backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
  243. backtrader/indicators/contrib/kalman_filter_line.py +127 -0
  244. backtrader/indicators/contrib/kama_indicator.py +150 -0
  245. backtrader/indicators/contrib/karacatica_indicator.py +99 -0
  246. backtrader/indicators/contrib/kdj_indicator.py +59 -0
  247. backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
  248. backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
  249. backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
  250. backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
  251. backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
  252. backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
  253. backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
  254. backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
  255. backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
  256. backtrader/indicators/contrib/loco_indicator.py +88 -0
  257. backtrader/indicators/contrib/lrma_indicator.py +185 -0
  258. backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
  259. backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
  260. backtrader/indicators/contrib/macd2_indicator.py +61 -0
  261. backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
  262. backtrader/indicators/contrib/malr_indicator.py +77 -0
  263. backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
  264. backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
  265. backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
  266. backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
  267. backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
  268. backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
  269. backtrader/indicators/contrib/nrtr_indicator.py +95 -0
  270. backtrader/indicators/contrib/p_channel_system.py +40 -0
  271. backtrader/indicators/contrib/percent_envelope.py +37 -0
  272. backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
  273. backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
  274. backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
  275. backtrader/indicators/contrib/price_extreme_channel.py +35 -0
  276. backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
  277. backtrader/indicators/contrib/ravi_indicator.py +40 -0
  278. backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
  279. backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
  280. backtrader/indicators/contrib/renko_level.py +85 -0
  281. backtrader/indicators/contrib/renko_line_break.py +91 -0
  282. backtrader/indicators/contrib/rftl_indicator.py +41 -0
  283. backtrader/indicators/contrib/rkd_indicator.py +53 -0
  284. backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
  285. backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
  286. backtrader/indicators/contrib/rsi_slowdown.py +57 -0
  287. backtrader/indicators/contrib/rsioma_v2.py +41 -0
  288. backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
  289. backtrader/indicators/contrib/safe_adx.py +89 -0
  290. backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
  291. backtrader/indicators/contrib/sidus_indicator.py +105 -0
  292. backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
  293. backtrader/indicators/contrib/sliding_range_color.py +56 -0
  294. backtrader/indicators/contrib/slow_stoch.py +42 -0
  295. backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
  296. backtrader/indicators/contrib/smoothed_rsi.py +31 -0
  297. backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
  298. backtrader/indicators/contrib/stalin_indicator.py +152 -0
  299. backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
  300. backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
  301. backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
  302. backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
  303. backtrader/indicators/contrib/t3_average.py +76 -0
  304. backtrader/indicators/contrib/t3_indicator.py +40 -0
  305. backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
  306. backtrader/indicators/contrib/three_candles_indicator.py +70 -0
  307. backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
  308. backtrader/indicators/contrib/time_line.py +57 -0
  309. backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
  310. backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
  311. backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
  312. backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
  313. backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
  314. backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
  315. backtrader/indicators/contrib/trigger_line.py +66 -0
  316. backtrader/indicators/contrib/triple_ema_rate.py +34 -0
  317. backtrader/indicators/contrib/trvi_indicator.py +194 -0
  318. backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
  319. backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
  320. backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
  321. backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
  322. backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
  323. backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
  324. backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
  325. backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
  326. backtrader/indicators/contrib/vwma_candle.py +57 -0
  327. backtrader/indicators/contrib/vwma_digit_system.py +70 -0
  328. backtrader/indicators/contrib/wami.py +43 -0
  329. backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
  330. backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
  331. backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
  332. backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
  333. backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
  334. backtrader/indicators/contrib/xma_ichimoku.py +163 -0
  335. backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
  336. backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
  337. backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
  338. backtrader/indicators/contrib/xmacd_indicator.py +70 -0
  339. backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
  340. backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
  341. backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
  342. backtrader/indicators/contrib/xrvi_indicator.py +130 -0
  343. backtrader/indicators/contrib/zero_lag_macd.py +36 -0
  344. backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
  345. backtrader/indicators/contrib/zpf_indicator.py +115 -0
  346. backtrader/indicators/crossover.py +337 -0
  347. backtrader/indicators/dema.py +175 -0
  348. backtrader/indicators/demarker.py +270 -0
  349. backtrader/indicators/deviation.py +284 -0
  350. backtrader/indicators/directionalmove.py +1071 -0
  351. backtrader/indicators/dma.py +112 -0
  352. backtrader/indicators/dpo.py +96 -0
  353. backtrader/indicators/dv2.py +56 -0
  354. backtrader/indicators/ema.py +145 -0
  355. backtrader/indicators/envelope.py +475 -0
  356. backtrader/indicators/hadelta.py +198 -0
  357. backtrader/indicators/heikinashi.py +153 -0
  358. backtrader/indicators/hma.py +153 -0
  359. backtrader/indicators/hurst.py +151 -0
  360. backtrader/indicators/ichimoku.py +267 -0
  361. backtrader/indicators/kama.py +181 -0
  362. backtrader/indicators/kst.py +159 -0
  363. backtrader/indicators/lrsi.py +125 -0
  364. backtrader/indicators/mabase.py +147 -0
  365. backtrader/indicators/macd.py +322 -0
  366. backtrader/indicators/momentum.py +267 -0
  367. backtrader/indicators/moneyflow.py +237 -0
  368. backtrader/indicators/mt5atr.py +124 -0
  369. backtrader/indicators/myind.py +179 -0
  370. backtrader/indicators/obv.py +94 -0
  371. backtrader/indicators/ols.py +265 -0
  372. backtrader/indicators/oscillator.py +161 -0
  373. backtrader/indicators/percentchange.py +83 -0
  374. backtrader/indicators/percentrank.py +46 -0
  375. backtrader/indicators/pivotpoint.py +469 -0
  376. backtrader/indicators/prettygoodoscillator.py +113 -0
  377. backtrader/indicators/priceops_ext.py +123 -0
  378. backtrader/indicators/priceoscillator.py +262 -0
  379. backtrader/indicators/psar.py +212 -0
  380. backtrader/indicators/rmi.py +69 -0
  381. backtrader/indicators/rsi.py +440 -0
  382. backtrader/indicators/sma.py +141 -0
  383. backtrader/indicators/smma.py +116 -0
  384. backtrader/indicators/spread.py +54 -0
  385. backtrader/indicators/stochastic.py +263 -0
  386. backtrader/indicators/supertrend.py +436 -0
  387. backtrader/indicators/trend_ext.py +105 -0
  388. backtrader/indicators/trix.py +202 -0
  389. backtrader/indicators/tsi.py +155 -0
  390. backtrader/indicators/ultimateoscillator.py +158 -0
  391. backtrader/indicators/vortex.py +62 -0
  392. backtrader/indicators/williams.py +194 -0
  393. backtrader/indicators/wma.py +103 -0
  394. backtrader/indicators/zlema.py +135 -0
  395. backtrader/indicators/zlind.py +104 -0
  396. backtrader/linebuffer.py +3155 -0
  397. backtrader/lineiterator.py +2911 -0
  398. backtrader/lineroot.py +1106 -0
  399. backtrader/lineseries.py +2559 -0
  400. backtrader/live_trading/__init__.py +31 -0
  401. backtrader/live_trading/interface.py +404 -0
  402. backtrader/mathsupport.py +94 -0
  403. backtrader/metabase.py +1804 -0
  404. backtrader/mixins/__init__.py +21 -0
  405. backtrader/mixins/singleton.py +118 -0
  406. backtrader/observer.py +106 -0
  407. backtrader/observers/__init__.py +45 -0
  408. backtrader/observers/benchmark.py +126 -0
  409. backtrader/observers/broker.py +184 -0
  410. backtrader/observers/buysell.py +144 -0
  411. backtrader/observers/drawdown.py +161 -0
  412. backtrader/observers/logreturns.py +113 -0
  413. backtrader/observers/timereturn.py +86 -0
  414. backtrader/observers/trade_logger.py +2972 -0
  415. backtrader/observers/tradelogger.py +6 -0
  416. backtrader/observers/trades.py +258 -0
  417. backtrader/order.py +1114 -0
  418. backtrader/parameters.py +2345 -0
  419. backtrader/plot/__init__.py +54 -0
  420. backtrader/plot/finance.py +1022 -0
  421. backtrader/plot/formatters.py +200 -0
  422. backtrader/plot/locator.py +353 -0
  423. backtrader/plot/multicursor.py +495 -0
  424. backtrader/plot/plot.py +2500 -0
  425. backtrader/plot/plot_plotly.py +1351 -0
  426. backtrader/plot/scheme.py +253 -0
  427. backtrader/plot/utils.py +104 -0
  428. backtrader/position.py +290 -0
  429. backtrader/position_modes.py +132 -0
  430. backtrader/profiles.py +254 -0
  431. backtrader/reports/__init__.py +39 -0
  432. backtrader/reports/charts.py +371 -0
  433. backtrader/reports/performance.py +620 -0
  434. backtrader/reports/reporter.py +660 -0
  435. backtrader/resamplerfilter.py +1001 -0
  436. backtrader/signal.py +118 -0
  437. backtrader/signals/__init__.py +17 -0
  438. backtrader/sizer.py +114 -0
  439. backtrader/sizers/__init__.py +26 -0
  440. backtrader/sizers/fixedsize.py +161 -0
  441. backtrader/sizers/percents_sizer.py +119 -0
  442. backtrader/store.py +221 -0
  443. backtrader/stores/__init__.py +33 -0
  444. backtrader/stores/btapistore.py +15506 -0
  445. backtrader/stores/livestore.py +137 -0
  446. backtrader/stores/vchartfile.py +96 -0
  447. backtrader/strategy.py +3655 -0
  448. backtrader/talib.py +280 -0
  449. backtrader/test_helpers.py +96 -0
  450. backtrader/timer.py +358 -0
  451. backtrader/trade.py +442 -0
  452. backtrader/tradingcal.py +361 -0
  453. backtrader/utils/__init__.py +68 -0
  454. backtrader/utils/autodict.py +251 -0
  455. backtrader/utils/date.py +71 -0
  456. backtrader/utils/dateintern.py +509 -0
  457. backtrader/utils/flushfile.py +94 -0
  458. backtrader/utils/fractal.py +101 -0
  459. backtrader/utils/get_metrics.py +101 -0
  460. backtrader/utils/load_data.py +209 -0
  461. backtrader/utils/log_message.py +998 -0
  462. backtrader/utils/ordereddefaultdict.py +75 -0
  463. backtrader/utils/py3.py +296 -0
  464. backtrader/version.py +21 -0
  465. backtrader/writer.py +372 -0
@@ -0,0 +1,2345 @@
1
+ """
2
+ New Parameter System for Backtrader
3
+
4
+ This module implements a modern parameter system that replaces the metaclass-based
5
+ parameter handling with descriptor-based approach. This provides better type safety,
6
+ validation, and maintainability while maintaining backward compatibility.
7
+
8
+ Key Components:
9
+ - ParameterDescriptor: Core descriptor for parameter handling
10
+ - ParameterManager: Parameter storage and management
11
+ - ParameterizedBase: Base class for parameterized objects (without metaclass)
12
+ - Type checking and validation mechanisms
13
+ - Python 3.6+ __set_name__ support
14
+ """
15
+
16
+ import time as _time
17
+ from collections import OrderedDict
18
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union, cast
19
+
20
+ from .utils.log_message import get_logger
21
+ from .utils.py3 import string_types
22
+
23
+ logger = get_logger(__name__)
24
+
25
+ _PARAMETER_ACCESSOR_DIRECT_ATTRS = frozenset(
26
+ {
27
+ "get",
28
+ "isdefault",
29
+ "items",
30
+ "keys",
31
+ "notdefault",
32
+ "params",
33
+ "to_dict",
34
+ "values",
35
+ }
36
+ )
37
+
38
+
39
+ class ParameterDescriptor:
40
+ """
41
+ Advanced parameter descriptor with type checking and validation.
42
+
43
+ This descriptor replaces the metaclass-based parameter system with a more
44
+ modern and maintainable approach. It provides:
45
+
46
+ - Automatic type checking and conversion
47
+ - Value validation
48
+ - Default value handling
49
+ - Documentation support
50
+ - Python 3.6+ __set_name__ support
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ default: Any = None,
56
+ type_: Optional[Union[Type, Tuple[Type, ...]]] = None,
57
+ validator: Optional[Callable[[Any], bool]] = None,
58
+ doc: Optional[str] = None,
59
+ name: Optional[str] = None,
60
+ required: bool = False,
61
+ ):
62
+ """
63
+ Initialize parameter descriptor.
64
+
65
+ Args:
66
+ default: Default value for the parameter
67
+ type_: Expected type for the parameter (enables type checking)
68
+ validator: Function to validate parameter values
69
+ doc: Documentation string for the parameter
70
+ name: Parameter name (usually set by __set_name__)
71
+ required: Whether this parameter is required (no default allowed)
72
+ """
73
+ self.default = default
74
+ self.type_ = type_
75
+ self.validator = validator
76
+ self.doc = doc
77
+ self.name: Optional[str] = name
78
+ self.required = required
79
+
80
+ # Internal attribute name where the value is stored. Always set to a
81
+ # concrete string by __set_name__ before the descriptor is ever used.
82
+ self._attr_name: Optional[str] = None
83
+
84
+ def __set_name__(self, owner, name):
85
+ """
86
+ Called when the descriptor is assigned to a class attribute.
87
+ This is a Python 3.6+ feature that automatically sets the parameter name.
88
+ """
89
+ self.name = name
90
+ self._attr_name = f"_param_{name}"
91
+
92
+ # Don't register with owner._parameter_descriptors here since we use lazy loading
93
+ # The _compute_parameter_descriptors method will find this descriptor later
94
+
95
+ def __get__(self, obj, objtype=None):
96
+ """Get parameter value from object instance."""
97
+ if obj is None:
98
+ return self
99
+
100
+ # Get value from parameter manager
101
+ if hasattr(obj, "_param_manager"):
102
+ return obj._param_manager.get(self.name, self.default)
103
+
104
+ # Fallback: get from object attribute. _attr_name is a str post __set_name__.
105
+ return getattr(obj, self._attr_name, self.default)
106
+
107
+ def __set__(self, obj, value):
108
+ """Set parameter value on object instance with validation."""
109
+ # Type checking
110
+ if self.type_ is not None and value is not None:
111
+ if not isinstance(value, self.type_):
112
+ # Only a single concrete type can be used to coerce the value;
113
+ # a tuple of types (used for isinstance checks like
114
+ # (list, type(None))) is not callable, so skip conversion.
115
+ if isinstance(self.type_, tuple):
116
+ type_names = ", ".join(getattr(t, "__name__", str(t)) for t in self.type_)
117
+ raise TypeError(
118
+ f"Parameter '{self.name}' expects one of ({type_names}), "
119
+ f"got {type(value).__name__}."
120
+ )
121
+ try:
122
+ # Attempt type conversion
123
+ value = self.type_(value)
124
+ except (ValueError, TypeError) as e:
125
+ logger.error("parameters:125 re-raising ValueError,TypeError", exc_info=True)
126
+ raise TypeError(
127
+ f"Parameter '{self.name}' expects {self.type_.__name__}, "
128
+ f"got {type(value).__name__}. Conversion failed: {e}"
129
+ ) from e
130
+
131
+ # Value validation
132
+ if self.validator is not None:
133
+ if not self.validator(value):
134
+ raise ValueError(f"Invalid value for parameter '{self.name}': {value}")
135
+
136
+ # Set value through parameter manager
137
+ if hasattr(obj, "_param_manager"):
138
+ obj._param_manager.set(self.name, value)
139
+ else:
140
+ # Fallback: set as object attribute (_attr_name is str post __set_name__).
141
+ setattr(obj, self._attr_name, value)
142
+
143
+ def __delete__(self, obj):
144
+ """Delete parameter value, reverting to default."""
145
+ if hasattr(obj, "_param_manager"):
146
+ obj._param_manager.reset(self.name)
147
+ elif hasattr(obj, self._attr_name):
148
+ delattr(obj, self._attr_name)
149
+
150
+ def validate(self, value: Any) -> bool:
151
+ """
152
+ Validate a value for this parameter.
153
+
154
+ Args:
155
+ value: Value to validate
156
+
157
+ Returns:
158
+ True if value is valid, False otherwise
159
+ """
160
+ try:
161
+ type_ = self.type_
162
+ validator = self.validator
163
+
164
+ # Required check
165
+ if self.required and value is None:
166
+ return False
167
+
168
+ # Type check - be more flexible with numeric types
169
+ if type_ is not None and value is not None:
170
+ if type_ is float:
171
+ # For float, accept int, float, and convertible strings
172
+ if not isinstance(value, (int, float)):
173
+ try:
174
+ float(value) # Test conversion
175
+ except (ValueError, TypeError):
176
+ return False
177
+ elif type_ is int:
178
+ # For int, accept int and convertible values
179
+ if not isinstance(value, int):
180
+ try:
181
+ int(value) # Test conversion
182
+ except (ValueError, TypeError):
183
+ return False
184
+ elif type_ is bool:
185
+ # For bool, be flexible with boolean-like values
186
+ if not isinstance(value, bool) and value not in (
187
+ 0,
188
+ 1,
189
+ "True",
190
+ "False",
191
+ "true",
192
+ "false",
193
+ ):
194
+ return False
195
+ elif not isinstance(value, type_):
196
+ if isinstance(type_, tuple):
197
+ # Tuple of accepted types: isinstance already failed,
198
+ # and a tuple is not callable for a conversion test.
199
+ return False
200
+ try:
201
+ type_(value) # Test conversion for other types
202
+ except (ValueError, TypeError):
203
+ return False
204
+
205
+ # Custom validation
206
+ if validator is not None:
207
+ return validator(value)
208
+
209
+ return True
210
+ except (ValueError, TypeError):
211
+ return False
212
+
213
+ def get_type_info(self) -> Dict[str, Any]:
214
+ """Get type information for this parameter."""
215
+ return {
216
+ "name": self.name,
217
+ "type": self.type_,
218
+ "default": self.default,
219
+ "required": self.required,
220
+ "has_validator": self.validator is not None,
221
+ "doc": self.doc,
222
+ }
223
+
224
+
225
+ class ParameterManager:
226
+ """
227
+ Enhanced Parameter storage and management system.
228
+
229
+ This class manages parameter values for an object, replacing the functionality
230
+ of AutoInfoClass. It provides efficient storage, inheritance support, batch
231
+ operations, and advanced features like change tracking, callbacks, and
232
+ transactional updates.
233
+
234
+ New Features in Day 32-33:
235
+ - Parameter change history and tracking
236
+ - Change callbacks and notifications
237
+ - Parameter locking mechanism
238
+ - Parameter groups for organization
239
+ - Advanced inheritance with conflict resolution
240
+ - Lazy default value evaluation
241
+ - Transactional batch updates
242
+ """
243
+
244
+ def __init__(
245
+ self,
246
+ descriptors: Dict[str, ParameterDescriptor],
247
+ initial_values: Optional[Dict[str, Any]] = None,
248
+ enable_history: bool = True,
249
+ enable_callbacks: bool = True,
250
+ ):
251
+ """
252
+ Initialize parameter manager.
253
+
254
+ Args:
255
+ descriptors: Dictionary of parameter descriptors
256
+ initial_values: Initial parameter values
257
+ enable_history: Whether to track parameter change history
258
+ enable_callbacks: Whether to enable change callbacks
259
+ """
260
+ self._descriptors = descriptors.copy()
261
+ self._values: Dict[str, Any] = {}
262
+ self._defaults: Dict[str, Any] = {}
263
+ self._modified: Set[str] = set()
264
+
265
+ # Extract defaults from descriptors
266
+ for name, desc in descriptors.items():
267
+ self._defaults[name] = desc.default
268
+
269
+ # Advanced features
270
+ self._enable_history = enable_history
271
+ self._enable_callbacks = enable_callbacks
272
+
273
+ # Change tracking
274
+ self._change_history: Optional[Dict[str, Any]] = {} if enable_history else None
275
+ self._history_seq = 0 # Sequence counter for history ordering
276
+ self._change_callbacks: Optional[Dict[str, Any]] = {} if enable_callbacks else None
277
+ self._global_callbacks: Optional[List[Callable]] = [] if enable_callbacks else None
278
+
279
+ # Parameter locking
280
+ self._locked_params: Set[str] = set()
281
+
282
+ # Parameter groups
283
+ self._param_groups: Dict[str, List[str]] = {}
284
+ self._param_to_group: Dict[str, str] = {}
285
+
286
+ # Lazy defaults
287
+ self._lazy_defaults: Dict[str, Callable[[], Any]] = {}
288
+
289
+ # Dependencies
290
+ self._dependencies: Dict[str, List[str]] = {} # param -> list of dependents
291
+ self._dependents: Dict[str, List[str]] = {} # dependent -> list of params it depends on
292
+
293
+ # Transaction support
294
+ self._in_transaction = False
295
+ self._transaction_snapshot = None
296
+
297
+ # Value cache for lazy evaluation
298
+ self._value_cache: Dict[str, Any] = {}
299
+ self._cache_valid: Set[str] = set()
300
+
301
+ # Inheritance tracking
302
+ self._inheritance_sources: Dict[str, Any] = {} # param -> source ParameterManager
303
+
304
+ # Set initial values
305
+ if initial_values:
306
+ self.update(initial_values, validate_all=False)
307
+
308
+ def _invalidate_cache(self, name: str) -> None:
309
+ """Invalidate cache for a parameter."""
310
+ self._cache_valid.discard(name)
311
+ self._value_cache.pop(name, None)
312
+
313
+ def _clear_cache(self) -> None:
314
+ """Clear all cached values."""
315
+ self._value_cache.clear()
316
+ self._cache_valid.clear()
317
+
318
+ # Sentinel object for detecting missing keys (faster than 'in' check)
319
+ _MISSING = object()
320
+
321
+ def get(self, name: str, default: Any = None, _MISSING=_MISSING) -> Any:
322
+ """
323
+ Get parameter value with optimized caching and lazy evaluation support.
324
+
325
+ Args:
326
+ name: Parameter name
327
+ default: Default value if parameter not found
328
+
329
+ Returns:
330
+ Parameter value
331
+ """
332
+ # PERFORMANCE OPTIMIZATION: _MISSING as default arg avoids class attribute lookup
333
+ # Fast path: Check if we have a custom value (most common case)
334
+ val = self._values.get(name, _MISSING)
335
+ if val is not _MISSING:
336
+ return val
337
+
338
+ # Fast path: Check if we have a cached descriptor default
339
+ val = self._value_cache.get(name, _MISSING)
340
+ if val is not _MISSING:
341
+ return val
342
+
343
+ # Check lazy defaults
344
+ lazy_func = self._lazy_defaults.get(name, _MISSING)
345
+ if lazy_func is not _MISSING:
346
+ if name not in self._cache_valid:
347
+ try:
348
+ computed_value = lazy_func()
349
+ self._value_cache[name] = computed_value
350
+ self._cache_valid.add(name)
351
+ return computed_value
352
+ except Exception:
353
+ # If lazy evaluation fails, use descriptor default
354
+ logger.debug("parameters:352 fallback on Exception")
355
+ desc = self._descriptors.get(name, _MISSING)
356
+ if desc is not _MISSING:
357
+ default_val = desc.default
358
+ self._value_cache[name] = default_val
359
+ return default_val
360
+ return default
361
+ return self._value_cache[name]
362
+
363
+ # Cache descriptor default for faster subsequent access
364
+ desc = self._descriptors.get(name, _MISSING)
365
+ if desc is not _MISSING:
366
+ default_val = desc.default
367
+ self._value_cache[name] = default_val
368
+ return default_val
369
+
370
+ # Use provided default
371
+ return default
372
+
373
+ def set(
374
+ self,
375
+ name: str,
376
+ value: Any,
377
+ force: bool = False,
378
+ trigger_callbacks: bool = True,
379
+ skip_validation: bool = False,
380
+ ) -> None:
381
+ """
382
+ Set parameter value with validation and dependency updates.
383
+
384
+ Args:
385
+ name: Parameter name
386
+ value: Parameter value
387
+ force: Force setting even if parameter is locked
388
+ trigger_callbacks: Whether to trigger change callbacks
389
+ skip_validation: Skip validation (use with caution)
390
+ """
391
+ # Check if parameter is locked
392
+ if not force and name in self._locked_params:
393
+ raise ValueError(f"Parameter '{name}' is locked and cannot be modified")
394
+
395
+ # Get old value for callbacks and history
396
+ old_value = self.get(name)
397
+
398
+ # Validate if not skipping validation
399
+ descriptor = self._descriptors.get(name) if not skip_validation else None
400
+ if descriptor is not None:
401
+ if not descriptor.validate(value):
402
+ raise ValueError(f"Invalid value for parameter '{name}': {value}")
403
+
404
+ # Set the value
405
+ self._values[name] = value
406
+ self._modified.add(name)
407
+
408
+ # Invalidate cache
409
+ self._invalidate_cache(name)
410
+
411
+ # Record change in history
412
+ if self._enable_history and self._change_history is not None:
413
+ self._history_seq += 1
414
+ self._change_history.setdefault(name, []).append(
415
+ {
416
+ "timestamp": _time.time(),
417
+ "seq": self._history_seq,
418
+ "old_value": old_value,
419
+ "new_value": value,
420
+ "forced": force,
421
+ }
422
+ )
423
+
424
+ # Trigger callbacks only if not in transaction
425
+ has_callbacks = bool(self._global_callbacks) or (
426
+ self._change_callbacks is not None and name in self._change_callbacks
427
+ )
428
+ if (
429
+ trigger_callbacks
430
+ and self._enable_callbacks
431
+ and not self._in_transaction
432
+ and has_callbacks
433
+ ):
434
+ self._trigger_change_callbacks(name, old_value, value)
435
+
436
+ # Update dependent parameters
437
+ self._update_dependents(name, value)
438
+
439
+ def reset(self, name: str, force: bool = False) -> None:
440
+ """
441
+ Reset parameter to its default value.
442
+
443
+ Args:
444
+ name: Parameter name
445
+ force: Force reset even if parameter is locked
446
+ """
447
+ # Check if parameter is locked
448
+ if not force and name in self._locked_params:
449
+ raise ValueError(f"Parameter '{name}' is locked and cannot be reset")
450
+
451
+ # Get old value for callbacks
452
+ old_value = self.get(name)
453
+
454
+ # Remove from values (will revert to default)
455
+ if name in self._values:
456
+ del self._values[name]
457
+
458
+ # Remove from modified set
459
+ self._modified.discard(name)
460
+
461
+ # Invalidate cache
462
+ self._invalidate_cache(name)
463
+
464
+ # Get new value (should be default)
465
+ new_value = self.get(name)
466
+
467
+ # Record change in history
468
+ if self._enable_history and self._change_history is not None:
469
+ self._history_seq += 1
470
+ self._change_history.setdefault(name, []).append(
471
+ {
472
+ "timestamp": _time.time(),
473
+ "seq": self._history_seq,
474
+ "old_value": old_value,
475
+ "new_value": new_value,
476
+ "reset": True,
477
+ "forced": force,
478
+ }
479
+ )
480
+
481
+ # Trigger callbacks only if not in transaction
482
+ has_callbacks = bool(self._global_callbacks) or (
483
+ self._change_callbacks is not None and name in self._change_callbacks
484
+ )
485
+ if self._enable_callbacks and not self._in_transaction and has_callbacks:
486
+ self._trigger_change_callbacks(name, old_value, new_value)
487
+
488
+ # Update dependent parameters
489
+ self._update_dependents(name, new_value)
490
+
491
+ def update(
492
+ self,
493
+ values: Union[Dict[str, Any], "ParameterManager"],
494
+ force: bool = False,
495
+ validate_all: bool = True,
496
+ ) -> None:
497
+ """
498
+ Update multiple parameters at once.
499
+
500
+ Args:
501
+ values: Dictionary of parameter values or another ParameterManager
502
+ force: Force update even for locked parameters
503
+ validate_all: Validate all parameters before updating any
504
+ """
505
+ if isinstance(values, ParameterManager):
506
+ values = values.to_dict()
507
+
508
+ # Validate all parameters first if requested
509
+ if validate_all:
510
+ validation_errors = []
511
+
512
+ # Check for locked parameters
513
+ for name, value in values.items():
514
+ if not force and name in self._locked_params:
515
+ validation_errors.append(f"Parameter '{name}' is locked")
516
+
517
+ # Check parameter validation
518
+ for name, value in values.items():
519
+ if name in self._descriptors:
520
+ descriptor = self._descriptors[name]
521
+ if not descriptor.validate(value):
522
+ validation_errors.append(f"Invalid value for '{name}': {value}")
523
+
524
+ if validation_errors:
525
+ raise ValueError("Validation errors: " + "; ".join(validation_errors))
526
+
527
+ # Update parameters
528
+ for name, value in values.items():
529
+ self.set(name, value, force=force, skip_validation=not validate_all)
530
+
531
+ def to_dict(self) -> Dict[str, Any]:
532
+ """
533
+ Convert parameter manager to dictionary.
534
+
535
+ Returns:
536
+ Dictionary of current parameter values
537
+ """
538
+ result = {}
539
+ for name in self._descriptors:
540
+ result[name] = self.get(name)
541
+ return result
542
+
543
+ def keys(self):
544
+ """Get parameter names."""
545
+ return self._descriptors.keys()
546
+
547
+ def items(self):
548
+ """Get parameter name-value pairs."""
549
+ for name in self._descriptors:
550
+ yield name, self.get(name)
551
+
552
+ def values(self):
553
+ """Get parameter values."""
554
+ for name in self._descriptors:
555
+ yield self.get(name)
556
+
557
+ def __getitem__(self, name):
558
+ return self.get(name)
559
+
560
+ def __setitem__(self, name, value):
561
+ self.set(name, value)
562
+
563
+ def __contains__(self, name):
564
+ return name in self._descriptors
565
+
566
+ def __len__(self):
567
+ return len(self._descriptors)
568
+
569
+ def __iter__(self):
570
+ return iter(self._descriptors)
571
+
572
+ def copy(self) -> "ParameterManager":
573
+ """
574
+ Create a copy of this parameter manager.
575
+
576
+ Returns:
577
+ New ParameterManager instance with same values
578
+ """
579
+ new_manager = ParameterManager(
580
+ self._descriptors,
581
+ enable_history=self._enable_history,
582
+ enable_callbacks=self._enable_callbacks,
583
+ )
584
+
585
+ # Copy current values
586
+ new_manager._values = self._values.copy()
587
+ new_manager._modified = self._modified.copy()
588
+
589
+ # Copy advanced features
590
+ if self._enable_history and self._change_history:
591
+ new_manager._change_history = {k: v.copy() for k, v in self._change_history.items()}
592
+
593
+ new_manager._locked_params = self._locked_params.copy()
594
+ new_manager._param_groups = self._param_groups.copy()
595
+ new_manager._param_to_group = self._param_to_group.copy()
596
+ new_manager._lazy_defaults = self._lazy_defaults.copy()
597
+ new_manager._dependencies = {k: v.copy() for k, v in self._dependencies.items()}
598
+ new_manager._dependents = {k: v.copy() for k, v in self._dependents.items()}
599
+
600
+ return new_manager
601
+
602
+ def inherit_from(
603
+ self,
604
+ parent: "ParameterManager",
605
+ strategy: str = "merge",
606
+ conflict_resolution: str = "parent",
607
+ selective: Optional[List[str]] = None,
608
+ ) -> None:
609
+ """
610
+ Inherit parameters from another ParameterManager.
611
+
612
+ Args:
613
+ parent: Parent ParameterManager to inherit from
614
+ strategy: Inheritance strategy ('merge', 'replace', 'add_only', 'selective')
615
+ conflict_resolution: How to resolve conflicts ('parent', 'child', 'error', 'raise')
616
+ selective: Only inherit specific parameters (list of names)
617
+ """
618
+ if strategy == "replace":
619
+ self._inherit_replace(parent, selective)
620
+ elif strategy == "merge":
621
+ self._inherit_merge(parent, conflict_resolution, selective)
622
+ elif strategy == "add_only":
623
+ self._inherit_add_only(parent, selective)
624
+ elif strategy == "selective":
625
+ self._inherit_selective(parent, conflict_resolution, selective)
626
+ else:
627
+ raise ValueError(f"Unknown inheritance strategy: {strategy}")
628
+
629
+ def _copy_param_from(self, parent: "ParameterManager", name: str) -> None:
630
+ """Copy a single parameter's descriptor/default/current value from parent."""
631
+ self._descriptors[name] = parent._descriptors[name]
632
+ self._defaults[name] = parent._defaults[name]
633
+ self._values[name] = parent.get(name)
634
+ self._inheritance_sources[name] = parent
635
+
636
+ def _parent_has_set(self, parent: "ParameterManager", name: str) -> bool:
637
+ """Whether the parent has a non-default (explicitly set) value for name."""
638
+ return parent.get(name) != parent._defaults.get(name) or name in parent._values
639
+
640
+ def _resolve_conflict(self, parent: "ParameterManager", name: str, conflict_resolution: str):
641
+ """Apply conflict_resolution for a parameter present in both managers."""
642
+ if conflict_resolution == "parent":
643
+ self._copy_param_from(parent, name)
644
+ elif conflict_resolution == "child":
645
+ # Keep current values
646
+ pass
647
+ elif conflict_resolution in ("error", "raise"):
648
+ raise ValueError(f"Parameter '{name}' conflicts between parent and child")
649
+
650
+ def _inherit_replace(self, parent: "ParameterManager", selective: Optional[List[str]]) -> None:
651
+ """Replace strategy: overwrite (selected) params with the parent's."""
652
+ names = selective if selective else list(parent._descriptors)
653
+ for name in names:
654
+ if name in parent._descriptors:
655
+ self._copy_param_from(parent, name)
656
+
657
+ def _inherit_merge(
658
+ self,
659
+ parent: "ParameterManager",
660
+ conflict_resolution: str,
661
+ selective: Optional[List[str]],
662
+ ) -> None:
663
+ """Merge strategy: only touch params present in both, honoring conflicts."""
664
+ params_to_process = selective if selective else parent._descriptors.keys()
665
+ for name in params_to_process:
666
+ if name in parent._descriptors and name in self._descriptors:
667
+ parent_has_value = self._parent_has_set(parent, name)
668
+ child_has_value = self.get(name) != self._defaults.get(name) or name in self._values
669
+ if parent_has_value and child_has_value:
670
+ self._resolve_conflict(parent, name, conflict_resolution)
671
+ elif parent_has_value and not child_has_value:
672
+ # Parent has value, child has default - inherit from parent
673
+ self._copy_param_from(parent, name)
674
+ # If only child has value, keep child's value
675
+
676
+ def _inherit_add_only(self, parent: "ParameterManager", selective: Optional[List[str]]) -> None:
677
+ """Add-only strategy: copy params that don't already exist on the child."""
678
+ params_to_process = selective if selective else parent._descriptors.keys()
679
+ for name in params_to_process:
680
+ if name in parent._descriptors and name not in self._descriptors:
681
+ self._copy_param_from(parent, name)
682
+
683
+ def _inherit_selective(
684
+ self,
685
+ parent: "ParameterManager",
686
+ conflict_resolution: str,
687
+ selective: Optional[List[str]],
688
+ ) -> None:
689
+ """Selective strategy: like merge, but driven by an explicit name list."""
690
+ if not selective:
691
+ raise ValueError("Selective strategy requires a list of parameter names")
692
+ for name in selective:
693
+ if name in parent._descriptors:
694
+ if name in self._descriptors:
695
+ self._resolve_conflict(parent, name, conflict_resolution)
696
+ else:
697
+ # No conflict, add parameter
698
+ self._copy_param_from(parent, name)
699
+
700
+ def get_inheritance_info(self, name: str) -> Optional[Dict[str, Any]]:
701
+ """
702
+ Get inheritance information for a parameter.
703
+
704
+ Args:
705
+ name: Parameter name
706
+
707
+ Returns:
708
+ Dictionary with inheritance information, or None if not available
709
+ """
710
+ if name not in self._descriptors:
711
+ return None
712
+
713
+ descriptor = self._descriptors[name]
714
+
715
+ # Check if parameter is inherited (not in _modified set and has different value than default)
716
+ current_value = self.get(name)
717
+ is_inherited = (
718
+ name not in self._modified
719
+ and current_value != descriptor.default
720
+ and name in self._values
721
+ )
722
+
723
+ return {
724
+ "name": name,
725
+ "current_value": current_value,
726
+ "default_value": descriptor.default,
727
+ "is_modified": name in self._modified,
728
+ "type": descriptor.type_,
729
+ "has_validator": descriptor.validator is not None,
730
+ "doc": descriptor.doc,
731
+ "is_locked": name in self._locked_params,
732
+ "group": self._param_to_group.get(name),
733
+ "has_lazy_default": name in self._lazy_defaults,
734
+ "dependents": self._dependencies.get(name, []),
735
+ "depends_on": self._dependents.get(name, []),
736
+ "inherited": is_inherited,
737
+ "source": self._inheritance_sources.get(name),
738
+ }
739
+
740
+ # Parameter locking methods
741
+ def lock_parameter(self, name: str) -> None:
742
+ """Lock a parameter to prevent modification."""
743
+ if name in self._descriptors:
744
+ self._locked_params.add(name)
745
+ else:
746
+ raise ValueError(f"Parameter '{name}' does not exist")
747
+
748
+ def unlock_parameter(self, name: str) -> None:
749
+ """Unlock a parameter to allow modification."""
750
+ self._locked_params.discard(name)
751
+
752
+ def is_locked(self, name: str) -> bool:
753
+ """Check if a parameter is locked."""
754
+ return name in self._locked_params
755
+
756
+ def get_locked_parameters(self) -> List[str]:
757
+ """Get list of locked parameter names."""
758
+ return list(self._locked_params)
759
+
760
+ # Parameter grouping methods
761
+ def create_group(self, group_name: str, param_names: List[str]) -> None:
762
+ """
763
+ Create a parameter group.
764
+
765
+ Args:
766
+ group_name: Name of the group
767
+ param_names: List of parameter names to include in the group
768
+ """
769
+ # Validate that all parameters exist
770
+ invalid_params = [name for name in param_names if name not in self._descriptors]
771
+ if invalid_params:
772
+ raise AttributeError(f"Invalid parameters for group '{group_name}': {invalid_params}")
773
+
774
+ self._param_groups[group_name] = param_names.copy()
775
+
776
+ # Update reverse mapping
777
+ for param_name in param_names:
778
+ self._param_to_group[param_name] = group_name
779
+
780
+ def get_group(self, group_name: str) -> List[str]:
781
+ """Get parameter names in a group."""
782
+ return self._param_groups.get(group_name, []).copy()
783
+
784
+ def get_parameter_group(self, param_name: str) -> Optional[str]:
785
+ """Get the group name for a parameter."""
786
+ return self._param_to_group.get(param_name)
787
+
788
+ def set_group(self, group_name: str, values: Dict[str, Any]) -> None:
789
+ """Set values for all parameters in a group."""
790
+ if group_name not in self._param_groups:
791
+ raise ValueError(f"Group '{group_name}' does not exist")
792
+
793
+ group_params = self._param_groups[group_name]
794
+ filtered_values = {k: v for k, v in values.items() if k in group_params}
795
+ self.update(filtered_values)
796
+
797
+ def get_group_values(self, group_name: str) -> Dict[str, Any]:
798
+ """Get values for all parameters in a group."""
799
+ if group_name not in self._param_groups:
800
+ raise ValueError(f"Group '{group_name}' does not exist")
801
+
802
+ group_params = self._param_groups[group_name]
803
+ return {name: self.get(name) for name in group_params}
804
+
805
+ # Lazy defaults
806
+ def set_lazy_default(self, name: str, lazy_func: Callable[[], Any]) -> None:
807
+ """
808
+ Set a lazy default function for a parameter.
809
+
810
+ Args:
811
+ name: Parameter name
812
+ lazy_func: Function that returns the default value when called
813
+ """
814
+ if name not in self._descriptors:
815
+ raise ValueError(f"Parameter '{name}' does not exist")
816
+
817
+ self._lazy_defaults[name] = lazy_func
818
+ self._invalidate_cache(name)
819
+
820
+ def clear_lazy_default(self, name: str) -> None:
821
+ """Clear lazy default for a parameter."""
822
+ if name in self._lazy_defaults:
823
+ del self._lazy_defaults[name]
824
+ self._invalidate_cache(name)
825
+
826
+ # Change callbacks
827
+ def add_change_callback(
828
+ self, callback: Callable[[str, Any, Any], None], param_name: Optional[str] = None
829
+ ) -> None:
830
+ """
831
+ Add a callback function that will be called when parameters change.
832
+
833
+ Args:
834
+ callback: Function to call with (param_name, old_value, new_value)
835
+ param_name: Specific parameter to watch, or None for all parameters
836
+ """
837
+ if not self._enable_callbacks:
838
+ return
839
+
840
+ if param_name is None:
841
+ # Global callback
842
+ if self._global_callbacks is not None:
843
+ self._global_callbacks.append(callback)
844
+ else:
845
+ # Parameter-specific callback
846
+ if self._change_callbacks is not None:
847
+ if param_name not in self._change_callbacks:
848
+ self._change_callbacks[param_name] = []
849
+ self._change_callbacks[param_name].append(callback)
850
+
851
+ def remove_change_callback(
852
+ self, callback: Callable[[str, Any, Any], None], param_name: Optional[str] = None
853
+ ) -> None:
854
+ """Remove a change callback."""
855
+ if not self._enable_callbacks:
856
+ return
857
+
858
+ if param_name is None:
859
+ # Remove from global callbacks
860
+ if self._global_callbacks is not None and callback in self._global_callbacks:
861
+ self._global_callbacks.remove(callback)
862
+ else:
863
+ # Remove from parameter-specific callbacks
864
+ if (
865
+ self._change_callbacks is not None
866
+ and param_name in self._change_callbacks
867
+ and callback in self._change_callbacks[param_name]
868
+ ):
869
+ self._change_callbacks[param_name].remove(callback)
870
+
871
+ def _trigger_change_callbacks(self, name: str, old_value: Any, new_value: Any) -> None:
872
+ """Trigger change callbacks for a parameter."""
873
+ if not self._enable_callbacks:
874
+ return
875
+
876
+ # Trigger parameter-specific callbacks
877
+ if self._change_callbacks is not None and name in self._change_callbacks:
878
+ for callback in self._change_callbacks[name]:
879
+ try:
880
+ callback(name, old_value, new_value)
881
+ except Exception as e:
882
+ logger.debug("Parameter change callback failed for '%s': %s", name, e)
883
+
884
+ # Trigger global callbacks
885
+ if self._global_callbacks is not None:
886
+ for callback in self._global_callbacks:
887
+ try:
888
+ callback(name, old_value, new_value)
889
+ except Exception as e:
890
+ logger.debug("Global parameter callback failed for '%s': %s", name, e)
891
+
892
+ # History methods
893
+ def get_change_history(self, name: str, limit: Optional[int] = None) -> List[tuple]:
894
+ """
895
+ Get change history for a parameter.
896
+
897
+ Args:
898
+ name: Parameter name
899
+ limit: Maximum number of history entries to return
900
+
901
+ Returns:
902
+ List of history entries (newest first) in format (old_value, new_value, timestamp)
903
+ """
904
+ if not self._enable_history or self._change_history is None:
905
+ return []
906
+
907
+ history = self._change_history.get(name, [])
908
+
909
+ # Sort by sequence number (newest first) for reliable ordering
910
+ sorted_history = sorted(history, key=lambda x: x.get("seq", 0), reverse=True)
911
+
912
+ if limit is not None:
913
+ sorted_history = sorted_history[:limit]
914
+
915
+ # Convert to tuple format (old_value, new_value, timestamp)
916
+ return [
917
+ (entry["old_value"], entry["new_value"], entry["timestamp"]) for entry in sorted_history
918
+ ]
919
+
920
+ def clear_history(self, name: Optional[str] = None) -> None:
921
+ """
922
+ Clear change history.
923
+
924
+ Args:
925
+ name: Specific parameter name, or None to clear all history
926
+ """
927
+ if not self._enable_history or self._change_history is None:
928
+ return
929
+
930
+ if name is None:
931
+ self._change_history.clear()
932
+ elif name in self._change_history:
933
+ del self._change_history[name]
934
+
935
+ # Dependency management
936
+ def add_dependency(self, param_name: str, dependent_param: str) -> None:
937
+ """
938
+ Add a dependency relationship between parameters.
939
+
940
+ Args:
941
+ param_name: Parameter that others depend on
942
+ dependent_param: Parameter that depends on param_name
943
+ """
944
+ # Validate parameters exist
945
+ if param_name not in self._descriptors:
946
+ raise AttributeError(f"Parameter '{param_name}' does not exist")
947
+ if dependent_param not in self._descriptors:
948
+ raise AttributeError(f"Dependent parameter '{dependent_param}' does not exist")
949
+
950
+ # Add to dependencies
951
+ if param_name not in self._dependencies:
952
+ self._dependencies[param_name] = []
953
+ if dependent_param not in self._dependencies[param_name]:
954
+ self._dependencies[param_name].append(dependent_param)
955
+
956
+ # Add to dependents (reverse mapping)
957
+ if dependent_param not in self._dependents:
958
+ self._dependents[dependent_param] = []
959
+ if param_name not in self._dependents[dependent_param]:
960
+ self._dependents[dependent_param].append(param_name)
961
+
962
+ def remove_dependency(self, param_name: str, dependent_param: str) -> None:
963
+ """Remove a dependency relationship."""
964
+ if param_name in self._dependencies:
965
+ if dependent_param in self._dependencies[param_name]:
966
+ self._dependencies[param_name].remove(dependent_param)
967
+
968
+ if dependent_param in self._dependents:
969
+ if param_name in self._dependents[dependent_param]:
970
+ self._dependents[dependent_param].remove(param_name)
971
+
972
+ def get_dependencies(self, param_name: str) -> List[str]:
973
+ """Get list of parameters that depend on the given parameter."""
974
+ return self._dependencies.get(param_name, []).copy()
975
+
976
+ def get_dependents(self, param_name: str) -> List[str]:
977
+ """Get list of parameters that this parameter depends on."""
978
+ return self._dependents.get(param_name, []).copy()
979
+
980
+ def _update_dependents(self, param_name: str, new_value: Any) -> None:
981
+ """Update dependent parameters when a parameter changes."""
982
+ for dependent in self._dependencies.get(param_name, ()):
983
+ # This is a placeholder for custom dependency logic
984
+ # In a real implementation, you might have specific update rules
985
+ pass
986
+
987
+ # Transaction support
988
+ def begin_transaction(self) -> None:
989
+ """Begin a parameter transaction."""
990
+ if self._in_transaction:
991
+ raise RuntimeError("Already in a transaction")
992
+
993
+ self._in_transaction = True
994
+ self._transaction_snapshot = {
995
+ "values": self._values.copy(),
996
+ "modified": self._modified.copy(),
997
+ }
998
+
999
+ def commit_transaction(self) -> None:
1000
+ """Commit the current transaction."""
1001
+ if not self._in_transaction:
1002
+ raise RuntimeError("Not in a transaction")
1003
+
1004
+ # Collect changes made during transaction for callbacks
1005
+ if self._enable_callbacks and self._transaction_snapshot:
1006
+ old_values = self._transaction_snapshot["values"]
1007
+ affected_names = set(old_values) | set(self._values)
1008
+ for name in affected_names:
1009
+ old_value = old_values.get(name, self._defaults.get(name))
1010
+ new_value = self.get(name)
1011
+ if new_value != old_value:
1012
+ self._trigger_change_callbacks(name, old_value, new_value)
1013
+
1014
+ # Transaction is committed by keeping current state
1015
+ self._in_transaction = False
1016
+ self._transaction_snapshot = None
1017
+
1018
+ def rollback_transaction(self) -> None:
1019
+ """Rollback the current transaction."""
1020
+ if not self._in_transaction:
1021
+ raise RuntimeError("Not in a transaction")
1022
+
1023
+ # Restore snapshot
1024
+ if self._transaction_snapshot:
1025
+ self._values = self._transaction_snapshot["values"].copy()
1026
+ self._modified = self._transaction_snapshot["modified"].copy()
1027
+
1028
+ self._in_transaction = False
1029
+ self._transaction_snapshot = None
1030
+
1031
+ # Clear cache since values changed
1032
+ self._clear_cache()
1033
+
1034
+ def is_in_transaction(self) -> bool:
1035
+ """Check if currently in a transaction."""
1036
+ return self._in_transaction
1037
+
1038
+
1039
+ class ParameterAccessor:
1040
+ """
1041
+ Parameter accessor that provides dict-like and attribute-like access to parameters.
1042
+
1043
+ This class serves as a bridge between the new parameter system and the old
1044
+ MetaParams-style parameter access patterns. It provides backward compatibility
1045
+ by supporting both attribute access (obj.p.param_name) and dict-like access.
1046
+ """
1047
+
1048
+ def __init__(self, param_manager: ParameterManager):
1049
+ """
1050
+ Initialize with a parameter manager.
1051
+
1052
+ NOTE: Originally attempted to pre-create instance attributes for performance, but this causes parameter synchronization issues.
1053
+ When parameters are modified through other means (like broker.set_cash()), instance attributes won't update.
1054
+ Therefore maintain dynamic lookup to ensure latest values are always retrieved.
1055
+ """
1056
+ # Use object.__setattr__ to avoid our custom __setattr__
1057
+ object.__setattr__(self, "_param_manager", param_manager)
1058
+ object.__setattr__(self, "params", self)
1059
+
1060
+ # Create a dict-like interface for _getitems() compatibility
1061
+ object.__setattr__(self, "_items_cache", None)
1062
+
1063
+ def __getattribute__(self, name):
1064
+ if name.startswith("_") or name in _PARAMETER_ACCESSOR_DIRECT_ATTRS:
1065
+ return object.__getattribute__(self, name)
1066
+ param_manager = object.__getattribute__(self, "_param_manager")
1067
+ if (
1068
+ name in param_manager._descriptors
1069
+ or name in param_manager._values
1070
+ or name in param_manager._lazy_defaults
1071
+ ):
1072
+ return param_manager.get(name)
1073
+ try:
1074
+ return object.__getattribute__(self, name)
1075
+ except AttributeError:
1076
+ return param_manager.get(name)
1077
+
1078
+ def __getattr__(self, name):
1079
+ """
1080
+ Get parameter value via attribute access.
1081
+
1082
+ Always get latest value from param_manager to ensure consistency.
1083
+ All parameter accesses are dynamically looked up to guarantee latest values.
1084
+ """
1085
+ # Use object.__getattribute__ to avoid recursion during unpickling
1086
+ param_manager = object.__getattribute__(self, "_param_manager")
1087
+ return param_manager.get(name)
1088
+
1089
+ def __getstate__(self):
1090
+ """Return normal instance state for Python 3.8/3.9 pickle protocols."""
1091
+ return object.__getattribute__(self, "__dict__").copy()
1092
+
1093
+ def _ensure_dynamic_parameter(self, name):
1094
+ """Register an ad-hoc parameter name so legacy writes remain introspectable."""
1095
+ param_manager = object.__getattribute__(self, "_param_manager")
1096
+ if name not in param_manager._descriptors:
1097
+ param_manager._descriptors[name] = ParameterDescriptor(default=None, name=name)
1098
+ param_manager._defaults[name] = None
1099
+
1100
+ def __setattr__(self, name, value):
1101
+ """Set parameter value via attribute access."""
1102
+ if name.startswith("_"):
1103
+ object.__setattr__(self, name, value)
1104
+ else:
1105
+ self._ensure_dynamic_parameter(name)
1106
+ self._param_manager.set(name, value)
1107
+
1108
+ def __getitem__(self, name):
1109
+ """Get parameter value via dict-like access."""
1110
+ return self._param_manager.get(name)
1111
+
1112
+ def __setitem__(self, name, value):
1113
+ """Set parameter value via dict-like access."""
1114
+ self._ensure_dynamic_parameter(name)
1115
+ self._param_manager.set(name, value)
1116
+
1117
+ def __contains__(self, name):
1118
+ """Check if parameter exists."""
1119
+ return name in self._param_manager
1120
+
1121
+ def __iter__(self):
1122
+ """Iterate over parameter names."""
1123
+ return iter(self._param_manager)
1124
+
1125
+ def __len__(self):
1126
+ """Get number of parameters."""
1127
+ return len(self._param_manager)
1128
+
1129
+ def get(self, name, default=None):
1130
+ """Get a parameter value with a default fallback."""
1131
+ return self._param_manager.get(name, default)
1132
+
1133
+ def _get(self, name, default=None):
1134
+ """Backtrader-compatible alias for get()."""
1135
+ return self.get(name, default)
1136
+
1137
+ def _getitems(self):
1138
+ """Get parameter items as list of tuples (name, value) for MetaParams compatibility."""
1139
+ return list(self._param_manager.items())
1140
+
1141
+ def _getpairs(self):
1142
+ """Get current parameter values as an OrderedDict for MetaParams compatibility."""
1143
+ return OrderedDict(self._param_manager.items())
1144
+
1145
+ def _gettuple(self):
1146
+ """Get current parameter values as a tuple of pairs."""
1147
+ return tuple(self._param_manager.items())
1148
+
1149
+ def _getkeys(self):
1150
+ """Get parameter keys for MetaParams compatibility."""
1151
+ return list(self._param_manager.keys())
1152
+
1153
+ def _getvalues(self):
1154
+ """Get parameter values for MetaParams compatibility."""
1155
+ return list(self._param_manager.values())
1156
+
1157
+ def _getdefaults(self):
1158
+ """Get parameter default values in declaration order."""
1159
+ return [self._param_manager._defaults[name] for name in self._param_manager.keys()]
1160
+
1161
+ def _getkwargsdefault(self):
1162
+ """Get parameter defaults as an OrderedDict in declaration order."""
1163
+ return OrderedDict(
1164
+ (name, self._param_manager._defaults[name]) for name in self._param_manager.keys()
1165
+ )
1166
+
1167
+ def isdefault(self, pname):
1168
+ """Check whether a parameter currently has its default value."""
1169
+ defaults = self._getkwargsdefault()
1170
+ return self._get(pname) == defaults[pname]
1171
+
1172
+ def notdefault(self, pname):
1173
+ """Check whether a parameter currently differs from its default value."""
1174
+ return not self.isdefault(pname)
1175
+
1176
+ def _getkwargs(self, skip_=False):
1177
+ """
1178
+ Get parameters as keyword arguments for MetaParams compatibility.
1179
+
1180
+ Args:
1181
+ skip_: Whether to skip parameters starting with underscore
1182
+
1183
+ Returns:
1184
+ Dictionary of parameter names and values
1185
+ """
1186
+ kwargs = {}
1187
+ for name, value in self._param_manager.items():
1188
+ if skip_ and name.startswith("_"):
1189
+ continue
1190
+ kwargs[name] = value
1191
+ return kwargs
1192
+
1193
+ def keys(self):
1194
+ """Get parameter names."""
1195
+ return list(self._param_manager.keys())
1196
+
1197
+ def items(self):
1198
+ """Get parameter name-value pairs."""
1199
+ return list(self._param_manager.items())
1200
+
1201
+ def values(self):
1202
+ """Get parameter values."""
1203
+ return list(self._param_manager.values())
1204
+
1205
+ def to_dict(self):
1206
+ """Convert parameters to a plain dictionary."""
1207
+ return dict(self._param_manager.items())
1208
+
1209
+ def __repr__(self):
1210
+ """String representation showing parameter values."""
1211
+ items = list(self._param_manager.items())
1212
+ return f"ParameterAccessor({dict(items)})"
1213
+
1214
+
1215
+ def _normalize_legacy_params(params) -> "OrderedDict[str, Any]":
1216
+ """Normalize legacy params declarations into an ordered defaults mapping."""
1217
+ pairs: "OrderedDict[str, Any]" = OrderedDict()
1218
+
1219
+ if params is None:
1220
+ return pairs
1221
+ if isinstance(params, dict):
1222
+ pairs.update(params)
1223
+ return pairs
1224
+ if hasattr(params, "_getpairs"):
1225
+ pairs.update(params._getpairs())
1226
+ return pairs
1227
+ if hasattr(params, "_gettuple"):
1228
+ pairs.update(dict(params._gettuple()))
1229
+ return pairs
1230
+ if hasattr(params, "_getitems"):
1231
+ pairs.update(dict(params._getitems()))
1232
+ return pairs
1233
+ if isinstance(params, (tuple, list)):
1234
+ for item in params:
1235
+ if isinstance(item, (tuple, list)) and len(item) >= 2:
1236
+ pairs[item[0]] = item[1]
1237
+ elif isinstance(item, string_types):
1238
+ pairs[item] = None
1239
+ elif hasattr(item, "__iter__") and not isinstance(item, string_types):
1240
+ item_list = list(item)
1241
+ if len(item_list) >= 2:
1242
+ pairs[item_list[0]] = item_list[1]
1243
+ return pairs
1244
+ if hasattr(params, "items"):
1245
+ pairs.update(params.items())
1246
+ return pairs
1247
+ if hasattr(params, "__dict__"):
1248
+ for attr_name, attr_value in params.__dict__.items():
1249
+ if not attr_name.startswith("_") and not callable(attr_value):
1250
+ pairs[attr_name] = attr_value
1251
+
1252
+ return pairs
1253
+
1254
+
1255
+ class LegacyParamsSchema:
1256
+ """Callable legacy params schema backed by the modern parameter manager.
1257
+
1258
+ The object keeps the old class-level params protocol (`_gettuple`,
1259
+ `_getpairs`, `_getkeys`) while creating instance-level `ParameterAccessor`
1260
+ objects. This removes the dynamic per-instance empty parameter classes that
1261
+ survived the metaclass-removal migration.
1262
+ """
1263
+
1264
+ _ALIASES = {
1265
+ "period": ("periods", "window", "length"),
1266
+ "movav": ("_movav", "ma", "moving_average"),
1267
+ "_movav": ("movav", "ma", "moving_average"),
1268
+ "lookback": ("look_back", "lag"),
1269
+ "upperband": ("upper_band", "upper", "high_band"),
1270
+ "lowerband": ("lower_band", "lower", "low_band"),
1271
+ "fast": ("fast_period", "fastperiod"),
1272
+ "slow": ("slow_period", "slowperiod"),
1273
+ "signal": ("signal_period", "signalperiod"),
1274
+ "mult": ("multiplier",),
1275
+ "safediv": ("safe_div",),
1276
+ "safepct": ("safe_pct",),
1277
+ }
1278
+
1279
+ def __init__(self, name: str = "Params", params=(), module: Optional[str] = None):
1280
+ self.__name__ = str(name)
1281
+ self.__qualname__ = str(name)
1282
+ self.__module__ = module or __name__
1283
+ self._pairs = _normalize_legacy_params(params)
1284
+
1285
+ def __call__(self, **kwargs):
1286
+ all_pairs = self._pairs.copy()
1287
+ for name in kwargs:
1288
+ if name not in all_pairs:
1289
+ all_pairs[name] = None
1290
+ descriptors = {
1291
+ name: ParameterDescriptor(default=default, name=name)
1292
+ for name, default in all_pairs.items()
1293
+ }
1294
+ return ParameterAccessor(
1295
+ ParameterManager(
1296
+ descriptors,
1297
+ initial_values=kwargs,
1298
+ enable_history=False,
1299
+ enable_callbacks=False,
1300
+ )
1301
+ )
1302
+
1303
+ def _getpairs(self):
1304
+ return self._pairs.copy()
1305
+
1306
+ def _gettuple(self):
1307
+ return tuple(self._pairs.items())
1308
+
1309
+ def _getkeys(self):
1310
+ return list(self._pairs.keys())
1311
+
1312
+ def _getdefaults(self):
1313
+ return list(self._pairs.values())
1314
+
1315
+ def _getitems(self):
1316
+ return self._pairs.items()
1317
+
1318
+ def _getkwargsdefault(self):
1319
+ return self._getpairs()
1320
+
1321
+ def _get(self, name, default=None):
1322
+ return self.get(name, default)
1323
+
1324
+ def get(self, name, default=None):
1325
+ if name in self._pairs:
1326
+ return self._pairs[name]
1327
+ for canonical_name, aliases in self._ALIASES.items():
1328
+ if name in aliases and canonical_name in self._pairs:
1329
+ return self._pairs[canonical_name]
1330
+ return default
1331
+
1332
+ def __getattr__(self, name):
1333
+ value = self.get(name, None)
1334
+ if value is not None or name in self._pairs:
1335
+ return value
1336
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
1337
+
1338
+ def __contains__(self, name):
1339
+ return name in self._pairs
1340
+
1341
+ def __getitem__(self, name):
1342
+ return self._pairs[name]
1343
+
1344
+ def __iter__(self):
1345
+ return iter(self._pairs)
1346
+
1347
+ def __len__(self):
1348
+ return len(self._pairs)
1349
+
1350
+ def keys(self):
1351
+ return list(self._pairs.keys())
1352
+
1353
+ def items(self):
1354
+ return list(self._pairs.items())
1355
+
1356
+ def values(self):
1357
+ return list(self._pairs.values())
1358
+
1359
+ def __repr__(self):
1360
+ return f"LegacyParamsSchema({dict(self._pairs)!r})"
1361
+
1362
+
1363
+ def make_legacy_parameter_accessor(params=(), values=None, name: str = "Params"):
1364
+ """Create a legacy-compatible parameter accessor from old params declarations."""
1365
+ schema = LegacyParamsSchema(name=name, params=params)
1366
+ return schema(**(values or {}))
1367
+
1368
+
1369
+ class ParameterizedBase:
1370
+ """
1371
+ Enhanced base class for objects with parameters - without metaclass.
1372
+
1373
+ This class provides the modern parameter system interface while maintaining
1374
+ backward compatibility with the old MetaParams-based system. It uses
1375
+ regular class mechanisms instead of metaclass.
1376
+ """
1377
+
1378
+ def __init_subclass__(cls, **kwargs):
1379
+ """
1380
+ Called when a class is subclassed. Replaces metaclass functionality.
1381
+
1382
+ This method sets up the class for lazy parameter descriptor resolution
1383
+ to avoid inheritance contamination issues.
1384
+ """
1385
+ super().__init_subclass__(**kwargs)
1386
+
1387
+ # Don't compute _parameter_descriptors here - do it lazily
1388
+ # This prevents child class definitions from affecting parent classes
1389
+ cls._parameter_descriptors = None # Mark as not computed
1390
+ cls._parameter_descriptors_computed = False
1391
+
1392
+ # Check for MetaParams compatibility
1393
+ cls._has_metaparams_heritage = any(
1394
+ hasattr(base, "params") and hasattr(getattr(base, "params", None), "_getitems")
1395
+ for base in cls.__mro__[1:] # Skip self
1396
+ )
1397
+
1398
+ @classmethod
1399
+ def _compute_parameter_descriptors(cls) -> Dict[str, "ParameterDescriptor"]:
1400
+ """
1401
+ Compute parameter descriptors for this class on-demand.
1402
+
1403
+ This is called lazily to avoid inheritance contamination issues
1404
+ that occur when descriptors are computed during class definition.
1405
+ """
1406
+ if cls._parameter_descriptors_computed:
1407
+ return cast(Dict[str, "ParameterDescriptor"], cls._parameter_descriptors)
1408
+
1409
+ # Create a completely new _parameter_descriptors for this class
1410
+ # Each class must have its own independent dictionary
1411
+
1412
+ # STEP 1: Collect all parameters from the inheritance hierarchy
1413
+ all_params: Dict[str, "ParameterDescriptor"] = {}
1414
+ cls._collect_inherited_descriptors(all_params)
1415
+
1416
+ # STEP 2/3: Add descriptors / legacy params from the current class
1417
+ # (highest precedence): these override inherited parameters by name.
1418
+ cls._collect_own_descriptors(all_params)
1419
+
1420
+ # STEP 4: Set the final descriptors for this class and mark as computed
1421
+ cls._parameter_descriptors = all_params
1422
+ cls._parameter_descriptors_computed = True
1423
+
1424
+ return all_params
1425
+
1426
+ @classmethod
1427
+ def _collect_inherited_descriptors(cls, all_params: Dict[str, "ParameterDescriptor"]) -> None:
1428
+ """STEP 1: gather descriptors / legacy params from base classes.
1429
+
1430
+ Processes base classes from least specific to most specific (reverse
1431
+ MRO) so more specific classes override less specific ones. Mutates
1432
+ ``all_params`` in place. Extracted verbatim from
1433
+ ``_compute_parameter_descriptors``; behavior unchanged.
1434
+ """
1435
+ # Process base classes from least specific to most specific
1436
+ # This way, more specific classes override less specific ones
1437
+ for base_cls in reversed(cls.__mro__[1:-1]): # Skip cls and object, reverse order
1438
+ # FIRST: Look for parameter descriptors directly defined in this base class
1439
+ if hasattr(base_cls, "__dict__"):
1440
+ for attr_name, attr_value in base_cls.__dict__.items():
1441
+ if isinstance(attr_value, ParameterDescriptor):
1442
+ # More specific classes override less specific ones
1443
+ # Since we process from least to most specific, always update
1444
+ all_params[attr_name] = attr_value
1445
+
1446
+ # SECOND: Look for legacy params tuple in this base class
1447
+ if hasattr(base_cls, "__dict__") and "params" in base_cls.__dict__:
1448
+ base_params = base_cls.__dict__["params"]
1449
+ if isinstance(base_params, (tuple, list)):
1450
+ for param_def in base_params:
1451
+ if isinstance(param_def, (tuple, list)) and len(param_def) >= 2:
1452
+ param_name, param_default = param_def[0], param_def[1]
1453
+ # More specific classes override less specific ones
1454
+ # Since we process from least to most specific, always update
1455
+ all_params[param_name] = ParameterDescriptor(
1456
+ default=param_default, name=param_name
1457
+ )
1458
+
1459
+ @classmethod
1460
+ def _collect_own_descriptors(cls, all_params: Dict[str, "ParameterDescriptor"]) -> None:
1461
+ """STEP 2/3: gather descriptors / legacy params from the current class.
1462
+
1463
+ Current-class definitions have highest precedence and override any
1464
+ inherited parameter with the same name. Mutates ``all_params`` in
1465
+ place. Extracted verbatim from ``_compute_parameter_descriptors``;
1466
+ behavior unchanged.
1467
+ """
1468
+ # STEP 2: Add descriptors from the current class (highest precedence)
1469
+ # These override any inherited parameters with the same name
1470
+ if hasattr(cls, "__dict__"):
1471
+ for attr_name, attr_value in cls.__dict__.items():
1472
+ if isinstance(attr_value, ParameterDescriptor):
1473
+ all_params[attr_name] = attr_value
1474
+ # Ensure descriptor has proper name set
1475
+ if attr_value.name is None:
1476
+ attr_value.name = attr_name
1477
+ attr_value._attr_name = f"_param_{attr_name}"
1478
+
1479
+ # STEP 3: Handle legacy params definition in current class
1480
+ if hasattr(cls, "__dict__") and "params" in cls.__dict__:
1481
+ current_params = cls.__dict__["params"]
1482
+ if isinstance(current_params, (tuple, list)):
1483
+ for param_def in current_params:
1484
+ if isinstance(param_def, (tuple, list)) and len(param_def) >= 2:
1485
+ param_name, param_default = param_def[0], param_def[1]
1486
+ # Current class params override inherited ones
1487
+ all_params[param_name] = ParameterDescriptor(
1488
+ default=param_default, name=param_name
1489
+ )
1490
+
1491
+ def __init__(self, **kwargs):
1492
+ """Initialize the parameterized object."""
1493
+ # Initialize parent first
1494
+ super().__init__()
1495
+
1496
+ # Get parameter descriptors from the class hierarchy
1497
+ descriptors = self._compute_parameter_descriptors()
1498
+
1499
+ if descriptors:
1500
+ # Use the modern parameter system
1501
+ self._init_with_new_system(descriptors, kwargs)
1502
+ else:
1503
+ # Fall back to compatibility mode if needed
1504
+ legacy_params = getattr(self, "params", ())
1505
+ if legacy_params:
1506
+ # Convert legacy params to descriptors
1507
+ legacy_descriptors = ParamsBridge.convert_legacy_params_tuple(legacy_params)
1508
+ self._init_with_metaparams_compatibility(legacy_descriptors, kwargs)
1509
+ else:
1510
+ # No parameters at all
1511
+ self._param_manager = ParameterManager({})
1512
+
1513
+ # Set up parameter accessor as 'p' for backward compatibility
1514
+ if hasattr(self, "_param_manager"):
1515
+ self.p = ParameterAccessor(self._param_manager)
1516
+ # Also create 'params' accessor for full compatibility
1517
+ self.params = self.p
1518
+ else:
1519
+ self.p = None
1520
+ self.params = None
1521
+
1522
+ def _get_parameter_descriptors(self) -> Dict[str, ParameterDescriptor]:
1523
+ """
1524
+ Get parameter descriptors from the class hierarchy.
1525
+
1526
+ Returns:
1527
+ Dictionary of parameter descriptors
1528
+ """
1529
+ return self.__class__._compute_parameter_descriptors()
1530
+
1531
+ def _init_with_metaparams_compatibility(
1532
+ self, descriptors: Dict[str, ParameterDescriptor], kwargs: Dict[str, Any]
1533
+ ):
1534
+ """
1535
+ Initialize with MetaParams compatibility mode.
1536
+
1537
+ Args:
1538
+ descriptors: Parameter descriptors
1539
+ kwargs: Initialization keyword arguments
1540
+ """
1541
+ # Initialize the new parameter manager
1542
+ self._param_manager = ParameterManager(
1543
+ descriptors, enable_history=True, enable_callbacks=True
1544
+ )
1545
+
1546
+ # Handle inheritance from MetaParams-based classes
1547
+ if self._has_metaparams_heritage:
1548
+ for base in self.__class__.__mro__[1:]: # Skip self
1549
+ if hasattr(base, "params") and hasattr(getattr(base, "params", None), "_getitems"):
1550
+ # Extract parameters from MetaParams base
1551
+ try:
1552
+ for param_name, param_default in base.params._getitems():
1553
+ if param_name not in descriptors:
1554
+ # Create a descriptor for the MetaParams parameter
1555
+ descriptors[param_name] = ParameterDescriptor(
1556
+ default=param_default, name=param_name
1557
+ )
1558
+ self._param_manager._descriptors[param_name] = descriptors[
1559
+ param_name
1560
+ ]
1561
+ self._param_manager._defaults[param_name] = param_default
1562
+ self._inheritance_sources[param_name] = base
1563
+ except (AttributeError, TypeError):
1564
+ # Skip if _getitems() doesn't work as expected
1565
+ logger.debug("parameters:1559 ignored AttributeError,TypeError")
1566
+
1567
+ # Separate parameter kwargs from other kwargs
1568
+ param_kwargs = {}
1569
+ other_kwargs = {}
1570
+
1571
+ for key, value in kwargs.items():
1572
+ if key in descriptors or key in self._param_manager._descriptors:
1573
+ param_kwargs[key] = value
1574
+ else:
1575
+ other_kwargs[key] = value
1576
+
1577
+ # Set parameter values with enhanced validation
1578
+ if param_kwargs:
1579
+ try:
1580
+ self._param_manager.update(param_kwargs)
1581
+ except Exception as e:
1582
+ logger.debug("parameters:1576 fallback on Exception")
1583
+ self._handle_initialization_error(e)
1584
+
1585
+ # Return other kwargs for parent class initialization
1586
+ return other_kwargs
1587
+
1588
+ def _init_with_new_system(
1589
+ self, descriptors: Dict[str, ParameterDescriptor], kwargs: Dict[str, Any]
1590
+ ):
1591
+ """
1592
+ Initialize with the new parameter system only.
1593
+
1594
+ Args:
1595
+ descriptors: Parameter descriptors
1596
+ kwargs: Initialization keyword arguments
1597
+ """
1598
+ self._param_manager = ParameterManager(
1599
+ descriptors, enable_history=True, enable_callbacks=True
1600
+ )
1601
+
1602
+ # Separate parameter kwargs from other kwargs
1603
+ param_kwargs = {}
1604
+ other_kwargs = {}
1605
+
1606
+ for key, value in kwargs.items():
1607
+ if key in descriptors:
1608
+ param_kwargs[key] = value
1609
+ else:
1610
+ other_kwargs[key] = value
1611
+
1612
+ # Set parameter values
1613
+ if param_kwargs:
1614
+ try:
1615
+ self._param_manager.update(param_kwargs)
1616
+ except Exception as e:
1617
+ logger.debug("parameters:1610 fallback on Exception")
1618
+ self._handle_initialization_error(e)
1619
+
1620
+ # Return other kwargs for parent class initialization
1621
+ return other_kwargs
1622
+
1623
+ def _handle_initialization_error(self, error: Exception):
1624
+ """
1625
+ Handle initialization errors with enhanced error messages.
1626
+
1627
+ Args:
1628
+ error: The original exception
1629
+ """
1630
+ error_msg = f"Parameter initialization failed for {self.__class__.__name__}: {error}"
1631
+
1632
+ # Add helpful information about available parameters
1633
+ if hasattr(self, "_param_manager"):
1634
+ available_params = list(self._param_manager.keys())
1635
+ if available_params:
1636
+ error_msg += f". Available parameters: {available_params}"
1637
+
1638
+ # Re-raise with enhanced message
1639
+ if isinstance(error, (ValueError, TypeError)):
1640
+ raise type(error)(error_msg) from error
1641
+ raise ValueError(error_msg) from error
1642
+
1643
+ # Parameter access methods for backward compatibility and convenience
1644
+ def get_param(self, name: str, default: Any = None) -> Any:
1645
+ """
1646
+ Get parameter value with fallback.
1647
+
1648
+ Args:
1649
+ name: Parameter name
1650
+ default: Default value if parameter not found
1651
+
1652
+ Returns:
1653
+ Parameter value
1654
+ """
1655
+ try:
1656
+ return object.__getattribute__(self, "_param_manager").get(name, default)
1657
+ except AttributeError:
1658
+ return default
1659
+
1660
+ def set_param(self, name: str, value: Any, validate: bool = True) -> None:
1661
+ """
1662
+ Set parameter value with optional validation.
1663
+
1664
+ Args:
1665
+ name: Parameter name
1666
+ value: Parameter value
1667
+ validate: Whether to perform validation
1668
+
1669
+ Raises:
1670
+ AttributeError: If parameter manager not initialized
1671
+ ValueError: If validation fails
1672
+ """
1673
+ try:
1674
+ param_manager = object.__getattribute__(self, "_param_manager")
1675
+ except AttributeError:
1676
+ logger.debug("parameters: param manager not initialized, re-raising")
1677
+ raise AttributeError(
1678
+ f"Parameter manager not initialized for {self.__class__.__name__}"
1679
+ ) from None
1680
+
1681
+ try:
1682
+ param_manager.set(name, value, skip_validation=not validate)
1683
+ except Exception as e:
1684
+ logger.error("parameters:1675 re-raising Exception", exc_info=True)
1685
+ raise ValueError(f"Failed to set parameter '{name}' to {value}: {e}") from e
1686
+
1687
+ def get_param_info(self) -> Dict[str, Dict[str, Any]]:
1688
+ """
1689
+ Get comprehensive information about all parameters.
1690
+
1691
+ Returns:
1692
+ Dictionary with parameter information
1693
+ """
1694
+ try:
1695
+ param_manager = object.__getattribute__(self, "_param_manager")
1696
+ except AttributeError:
1697
+ return {}
1698
+
1699
+ info = {}
1700
+ for name in param_manager.keys():
1701
+ inheritance_info = param_manager.get_inheritance_info(name)
1702
+ if inheritance_info:
1703
+ info[name] = inheritance_info
1704
+ else:
1705
+ # Fallback for parameters without inheritance info
1706
+ info[name] = {
1707
+ "name": name,
1708
+ "current_value": param_manager.get(name),
1709
+ "type": "unknown",
1710
+ }
1711
+
1712
+ return info
1713
+
1714
+ def validate_params(self) -> List[str]:
1715
+ """
1716
+ Validate all parameters and return list of validation errors.
1717
+
1718
+ Returns:
1719
+ List of validation error messages (empty if all valid)
1720
+ """
1721
+ if not hasattr(self, "_param_manager"):
1722
+ return []
1723
+
1724
+ errors = []
1725
+ for name, descriptor in self._param_manager._descriptors.items():
1726
+ current_value = self._param_manager.get(name)
1727
+ if not descriptor.validate(current_value):
1728
+ errors.append(f"Parameter '{name}' has invalid value: {current_value}")
1729
+
1730
+ return errors
1731
+
1732
+ def reset_param(self, name: str) -> None:
1733
+ """
1734
+ Reset parameter to its default value.
1735
+
1736
+ Args:
1737
+ name: Parameter name
1738
+
1739
+ Raises:
1740
+ AttributeError: If parameter manager not initialized
1741
+ ValueError: If parameter doesn't exist or is locked
1742
+ """
1743
+ if not hasattr(self, "_param_manager"):
1744
+ raise AttributeError(f"Parameter manager not initialized for {self.__class__.__name__}")
1745
+
1746
+ try:
1747
+ self._param_manager.reset(name)
1748
+ except Exception as e:
1749
+ logger.error("parameters:1739 re-raising Exception", exc_info=True)
1750
+ raise ValueError(f"Failed to reset parameter '{name}': {e}") from e
1751
+
1752
+ def reset_all_params(self) -> None:
1753
+ """Reset all parameters to their default values."""
1754
+ if hasattr(self, "_param_manager"):
1755
+ for name in list(self._param_manager.keys()):
1756
+ try:
1757
+ self._param_manager.reset(name)
1758
+ except Exception as e:
1759
+ logger.debug("Failed to reset parameter '%s': %s", name, e)
1760
+
1761
+ def get_modified_params(self) -> Dict[str, Any]:
1762
+ """
1763
+ Get parameters that have been modified from their defaults.
1764
+
1765
+ Returns:
1766
+ Dictionary of modified parameter names and values
1767
+ """
1768
+ if not hasattr(self, "_param_manager"):
1769
+ return {}
1770
+
1771
+ modified = {}
1772
+ for name in self._param_manager._modified:
1773
+ modified[name] = self._param_manager.get(name)
1774
+
1775
+ return modified
1776
+
1777
+ def copy_params_from(
1778
+ self,
1779
+ other: "ParameterizedBase",
1780
+ param_names: Optional[List[str]] = None,
1781
+ exclude: Optional[List[str]] = None,
1782
+ ) -> None:
1783
+ """
1784
+ Copy parameters from another ParameterizedBase instance.
1785
+
1786
+ Args:
1787
+ other: Source object to copy parameters from
1788
+ param_names: Specific parameter names to copy (None for all)
1789
+ exclude: Parameter names to exclude from copying
1790
+ """
1791
+ if not hasattr(self, "_param_manager") or not hasattr(other, "_param_manager"):
1792
+ return
1793
+
1794
+ # Determine which parameters to copy
1795
+ if param_names is None:
1796
+ param_names = list(other._param_manager.keys())
1797
+
1798
+ if exclude:
1799
+ param_names = [name for name in param_names if name not in exclude]
1800
+
1801
+ # Copy parameters
1802
+ for name in param_names:
1803
+ if (
1804
+ name in self._param_manager._descriptors
1805
+ and name in other._param_manager._descriptors
1806
+ ):
1807
+ try:
1808
+ value = other._param_manager.get(name)
1809
+ self._param_manager.set(name, value)
1810
+ except Exception as e:
1811
+ logger.debug("Failed to copy parameter '%s': %s", name, e)
1812
+
1813
+ def __repr__(self) -> str:
1814
+ """Enhanced string representation with parameter information."""
1815
+ class_name = self.__class__.__name__
1816
+ if hasattr(self, "_param_manager") and self._param_manager:
1817
+ param_count = len(self._param_manager)
1818
+ return f"{class_name}(parameters={param_count})"
1819
+ return f"{class_name}(no_parameters)"
1820
+
1821
+
1822
+ # CRITICAL FIX: Picklable validator classes for multiprocessing support
1823
+ # Local functions returned by Int() and Float() cannot be pickled,
1824
+ # causing failures in strategy optimization (optstrategy).
1825
+
1826
+
1827
+ class _IntValidator:
1828
+ """
1829
+ Integer validator that can be pickled for multiprocessing.
1830
+
1831
+ CRITICAL FIX: Class-based validator instead of closure to support pickling.
1832
+ """
1833
+
1834
+ def __init__(self, min_val=None, max_val=None):
1835
+ """Initialize the integer validator.
1836
+
1837
+ Args:
1838
+ min_val: Minimum allowed value (inclusive). None means no minimum.
1839
+ max_val: Maximum allowed value (inclusive). None means no maximum.
1840
+ """
1841
+ self.min_val = min_val
1842
+ self.max_val = max_val
1843
+
1844
+ def __call__(self, value):
1845
+ """Validate that value is an integer within the specified range.
1846
+
1847
+ Args:
1848
+ value: Value to validate.
1849
+
1850
+ Returns:
1851
+ True if value is a valid integer within range, False otherwise.
1852
+ """
1853
+ if not isinstance(value, int) or isinstance(value, bool):
1854
+ return False
1855
+ if self.min_val is not None and value < self.min_val:
1856
+ return False
1857
+ if self.max_val is not None and value > self.max_val:
1858
+ return False
1859
+ return True
1860
+
1861
+ def __reduce__(self):
1862
+ """Support pickling for multiprocessing."""
1863
+ return (_IntValidator, (self.min_val, self.max_val))
1864
+
1865
+
1866
+ class _FloatValidator:
1867
+ """
1868
+ Float validator that can be pickled for multiprocessing.
1869
+
1870
+ CRITICAL FIX: Class-based validator instead of closure to support pickling.
1871
+ """
1872
+
1873
+ def __init__(self, min_val=None, max_val=None):
1874
+ """Initialize the float validator.
1875
+
1876
+ Args:
1877
+ min_val: Minimum allowed value (inclusive). None means no minimum.
1878
+ max_val: Maximum allowed value (inclusive). None means no maximum.
1879
+ """
1880
+ self.min_val = min_val
1881
+ self.max_val = max_val
1882
+
1883
+ def __call__(self, value):
1884
+ """Validate that value is a float within the specified range.
1885
+
1886
+ Args:
1887
+ value: Value to validate.
1888
+
1889
+ Returns:
1890
+ True if value is a valid numeric type within range, False otherwise.
1891
+ """
1892
+ # Only accept actual numeric types, not strings
1893
+ if not isinstance(value, (int, float)):
1894
+ return False
1895
+ if self.min_val is not None and value < self.min_val:
1896
+ return False
1897
+ if self.max_val is not None and value > self.max_val:
1898
+ return False
1899
+ return True
1900
+
1901
+ def __reduce__(self):
1902
+ """Support pickling for multiprocessing."""
1903
+ return (_FloatValidator, (self.min_val, self.max_val))
1904
+
1905
+
1906
+ class _BoolValidator:
1907
+ """
1908
+ Boolean validator that can be pickled for multiprocessing.
1909
+
1910
+ CRITICAL FIX: Class-based validator instead of closure to support pickling.
1911
+ """
1912
+
1913
+ def __call__(self, value):
1914
+ """Boolean validator that accepts various boolean representations."""
1915
+ if isinstance(value, bool):
1916
+ return True
1917
+ if value in (0, 1, "True", "False", "true", "false", "TRUE", "FALSE"):
1918
+ return True
1919
+ return False
1920
+
1921
+ def __reduce__(self):
1922
+ """Support pickling for multiprocessing."""
1923
+ return (_BoolValidator, ())
1924
+
1925
+
1926
+ class _StringValidator:
1927
+ """
1928
+ String validator that can be pickled for multiprocessing.
1929
+
1930
+ CRITICAL FIX: Class-based validator instead of closure to support pickling.
1931
+ """
1932
+
1933
+ def __init__(self, min_length=None, max_length=None):
1934
+ """Initialize the string validator.
1935
+
1936
+ Args:
1937
+ min_length: Minimum allowed string length. None means no minimum.
1938
+ max_length: Maximum allowed string length. None means no maximum.
1939
+ """
1940
+ self.min_length = min_length
1941
+ self.max_length = max_length
1942
+
1943
+ def __call__(self, value):
1944
+ """Validate that value is a string within the specified length range.
1945
+
1946
+ Args:
1947
+ value: Value to validate.
1948
+
1949
+ Returns:
1950
+ True if value is a string with valid length, False otherwise.
1951
+ """
1952
+ if not isinstance(value, string_types):
1953
+ return False
1954
+ if self.min_length is not None and len(value) < self.min_length:
1955
+ return False
1956
+ if self.max_length is not None and len(value) > self.max_length:
1957
+ return False
1958
+ return True
1959
+
1960
+ def __reduce__(self):
1961
+ """Support pickling for multiprocessing."""
1962
+ return (_StringValidator, (self.min_length, self.max_length))
1963
+
1964
+
1965
+ class _OneOfValidator:
1966
+ """
1967
+ OneOf validator that can be pickled for multiprocessing.
1968
+
1969
+ CRITICAL FIX: Class-based validator instead of closure to support pickling.
1970
+ """
1971
+
1972
+ def __init__(self, choices):
1973
+ """Initialize the OneOf validator.
1974
+
1975
+ Args:
1976
+ choices: Tuple or list of allowed values.
1977
+ """
1978
+ self.choices = choices
1979
+
1980
+ def __call__(self, value):
1981
+ """Validate that value is one of the allowed choices.
1982
+
1983
+ Args:
1984
+ value: Value to validate.
1985
+
1986
+ Returns:
1987
+ True if value is in the allowed choices, False otherwise.
1988
+ """
1989
+ return value in self.choices
1990
+
1991
+ def __reduce__(self):
1992
+ """Support pickling for multiprocessing."""
1993
+ return (_OneOfValidator, (self.choices,))
1994
+
1995
+
1996
+ # Convenience functions for creating parameter descriptors with validation
1997
+ def Int(min_val: Optional[int] = None, max_val: Optional[int] = None) -> Callable[[Any], bool]:
1998
+ """
1999
+ Create an integer validator function.
2000
+
2001
+ Args:
2002
+ min_val: Minimum allowed value
2003
+ max_val: Maximum allowed value
2004
+
2005
+ Returns:
2006
+ Validator function for integer parameters
2007
+ """
2008
+ return _IntValidator(min_val, max_val)
2009
+
2010
+
2011
+ def Float(
2012
+ min_val: Optional[float] = None, max_val: Optional[float] = None
2013
+ ) -> Callable[[Any], bool]:
2014
+ """
2015
+ Create a float validator function.
2016
+
2017
+ Args:
2018
+ min_val: Minimum allowed value
2019
+ max_val: Maximum allowed value
2020
+
2021
+ Returns:
2022
+ Validator function for float parameters
2023
+ """
2024
+ return _FloatValidator(min_val, max_val)
2025
+
2026
+
2027
+ # Convenience functions for creating typed parameter descriptors
2028
+ def FloatParam(
2029
+ default=None, min_val: Optional[float] = None, max_val: Optional[float] = None, doc: str = None
2030
+ ) -> ParameterDescriptor:
2031
+ """Create a float parameter descriptor with validation."""
2032
+ return ParameterDescriptor(
2033
+ default=default, type_=float, validator=Float(min_val, max_val), doc=doc
2034
+ )
2035
+
2036
+
2037
+ def BoolParam(default=None, doc: str = None) -> ParameterDescriptor:
2038
+ """Create a boolean parameter descriptor."""
2039
+ return ParameterDescriptor(default=default, type_=bool, validator=_BoolValidator(), doc=doc)
2040
+
2041
+
2042
+ def StringParam(
2043
+ default=None,
2044
+ min_length: Optional[int] = None,
2045
+ max_length: Optional[int] = None,
2046
+ doc: str = None,
2047
+ ) -> ParameterDescriptor:
2048
+ """Create a string parameter descriptor with length validation."""
2049
+ return ParameterDescriptor(
2050
+ default=default, type_=str, validator=String(min_length, max_length), doc=doc
2051
+ )
2052
+
2053
+
2054
+ def String(
2055
+ min_length: Optional[int] = None, max_length: Optional[int] = None
2056
+ ) -> Callable[[Any], bool]:
2057
+ """
2058
+ Create a string validator function.
2059
+
2060
+ Args:
2061
+ min_length: Minimum allowed string length
2062
+ max_length: Maximum allowed string length
2063
+
2064
+ Returns:
2065
+ Validator function for string parameters
2066
+ """
2067
+ return _StringValidator(min_length, max_length)
2068
+
2069
+
2070
+ def Bool() -> Callable[[Any], bool]:
2071
+ """
2072
+ Create a boolean validator function.
2073
+
2074
+ Returns:
2075
+ Validator function for boolean parameters
2076
+ """
2077
+ return _BoolValidator()
2078
+
2079
+
2080
+ def OneOf(*choices) -> Callable[[Any], bool]:
2081
+ """
2082
+ Create a validator that checks if value is one of the given choices.
2083
+
2084
+ Args:
2085
+ *choices: Allowed values
2086
+
2087
+ Returns:
2088
+ Validator function
2089
+ """
2090
+ return _OneOfValidator(choices)
2091
+
2092
+
2093
+ def create_param_descriptor(name: str, default: Any = None, doc: str = None) -> ParameterDescriptor:
2094
+ """
2095
+ Create a basic parameter descriptor.
2096
+
2097
+ Args:
2098
+ name: Parameter name
2099
+ default: Default value
2100
+ doc: Documentation string
2101
+
2102
+ Returns:
2103
+ ParameterDescriptor instance
2104
+ """
2105
+ return ParameterDescriptor(default=default, name=name, doc=doc)
2106
+
2107
+
2108
+ def derive_params(base_params, new_params, other_base_params=None):
2109
+ """
2110
+ Derive parameters by combining base parameters with new ones.
2111
+
2112
+ Args:
2113
+ base_params: Base parameter descriptors or tuples
2114
+ new_params: New parameter descriptors or tuples
2115
+ other_base_params: Additional base parameters
2116
+
2117
+ Returns:
2118
+ Dictionary of combined parameter descriptors
2119
+ """
2120
+ combined_params = {}
2121
+
2122
+ # Add base parameters
2123
+ if hasattr(base_params, "_parameter_descriptors"):
2124
+ combined_params.update(base_params._parameter_descriptors)
2125
+ elif hasattr(base_params, "__mro__"):
2126
+ # It's a class, collect from all bases
2127
+ for base in base_params.__mro__:
2128
+ if hasattr(base, "_parameter_descriptors"):
2129
+ combined_params.update(base._parameter_descriptors)
2130
+
2131
+ # Add new parameters
2132
+ if isinstance(new_params, (list, tuple)):
2133
+ for i, param in enumerate(new_params):
2134
+ if isinstance(param, (list, tuple)) and len(param) >= 2:
2135
+ name, default = param[0], param[1]
2136
+ combined_params[name] = ParameterDescriptor(default=default, name=name)
2137
+ else:
2138
+ # Handle other formats as needed
2139
+ pass
2140
+
2141
+ return combined_params
2142
+
2143
+
2144
+ class ParamsBridge:
2145
+ """
2146
+ Bridge class for transitioning from MetaParams to new parameter system.
2147
+
2148
+ This class provides utilities for converting between the old MetaParams
2149
+ system and the new descriptor-based system during the migration period.
2150
+ """
2151
+
2152
+ @staticmethod
2153
+ def extract_params_from_metaparams_class(cls) -> Dict[str, ParameterDescriptor]:
2154
+ """
2155
+ Extract parameter descriptors from a MetaParams-based class.
2156
+
2157
+ Args:
2158
+ cls: Class with MetaParams-based parameters
2159
+
2160
+ Returns:
2161
+ Dictionary of parameter descriptors
2162
+ """
2163
+ descriptors = {}
2164
+
2165
+ if hasattr(cls, "params") and hasattr(cls.params, "_getitems"):
2166
+ for param_name, param_default in cls.params._getitems():
2167
+ descriptors[param_name] = ParameterDescriptor(
2168
+ default=param_default,
2169
+ name=param_name,
2170
+ doc=f"Migrated from MetaParams class {cls.__name__}",
2171
+ )
2172
+
2173
+ return descriptors
2174
+
2175
+ @staticmethod
2176
+ def convert_legacy_params_tuple(params_tuple) -> Dict[str, ParameterDescriptor]:
2177
+ """
2178
+ Convert legacy params tuple to parameter descriptors.
2179
+
2180
+ Args:
2181
+ params_tuple: Legacy params tuple like (('param1', 10), ('param2', 'value'))
2182
+
2183
+ Returns:
2184
+ Dictionary of parameter descriptors
2185
+ """
2186
+ descriptors = {}
2187
+
2188
+ if isinstance(params_tuple, (tuple, list)):
2189
+ for param_def in params_tuple:
2190
+ if isinstance(param_def, (tuple, list)) and len(param_def) >= 2:
2191
+ param_name, param_default = param_def[0], param_def[1]
2192
+
2193
+ # Try to infer type from default value
2194
+ param_type = type(param_default) if param_default is not None else None
2195
+
2196
+ descriptors[param_name] = ParameterDescriptor(
2197
+ default=param_default,
2198
+ type_=param_type,
2199
+ name=param_name,
2200
+ doc="Converted from legacy params tuple",
2201
+ )
2202
+
2203
+ return descriptors
2204
+
2205
+ @staticmethod
2206
+ def create_compatibility_wrapper(metaparams_class):
2207
+ """
2208
+ Create a compatibility wrapper for MetaParams-based classes.
2209
+
2210
+ Args:
2211
+ metaparams_class: Original MetaParams-based class
2212
+
2213
+ Returns:
2214
+ New class that uses the modern parameter system
2215
+ """
2216
+ # Extract existing parameters
2217
+ descriptors = ParamsBridge.extract_params_from_metaparams_class(metaparams_class)
2218
+
2219
+ # Create new class with descriptor-based parameters
2220
+ class_name = f"Modern{metaparams_class.__name__}"
2221
+
2222
+ # Build class namespace
2223
+ namespace = {
2224
+ "__module__": metaparams_class.__module__,
2225
+ "__doc__": f"Modernized version of {metaparams_class.__name__} with descriptor-based parameters",
2226
+ }
2227
+
2228
+ # Add parameter descriptors to namespace
2229
+ for name, descriptor in descriptors.items():
2230
+ namespace[name] = descriptor
2231
+
2232
+ # Create the new class using regular class creation (no metaclass)
2233
+ new_class = type(class_name, (ParameterizedBase,), namespace)
2234
+
2235
+ return new_class
2236
+
2237
+
2238
+ class ParameterValidationError(ValueError):
2239
+ """Specific exception for parameter validation errors."""
2240
+
2241
+ def __init__(
2242
+ self,
2243
+ parameter_name: str,
2244
+ value: Any,
2245
+ expected_type: Optional[Type] = None,
2246
+ additional_info: str = "",
2247
+ ):
2248
+ """Initialize a parameter validation error.
2249
+
2250
+ Args:
2251
+ parameter_name: Name of the parameter that failed validation.
2252
+ value: The invalid value that was provided.
2253
+ expected_type: Expected type for the parameter (optional).
2254
+ additional_info: Additional error information (optional).
2255
+ """
2256
+ self.parameter_name = parameter_name
2257
+ self.value = value
2258
+ self.expected_type = expected_type
2259
+
2260
+ message = f"Validation failed for parameter '{parameter_name}' with value {value}"
2261
+ if expected_type:
2262
+ message += f" (expected type: {expected_type.__name__})"
2263
+ if additional_info:
2264
+ message += f". {additional_info}"
2265
+
2266
+ super().__init__(message)
2267
+
2268
+
2269
+ class ParameterAccessError(AttributeError):
2270
+ """Specific exception for parameter access errors."""
2271
+
2272
+ def __init__(self, parameter_name: str, class_name: str, available_params: List[str]):
2273
+ """Initialize a parameter access error.
2274
+
2275
+ Args:
2276
+ parameter_name: Name of the parameter that was not found.
2277
+ class_name: Name of the class where access was attempted.
2278
+ available_params: List of available parameter names.
2279
+ """
2280
+ self.parameter_name = parameter_name
2281
+ self.class_name = class_name
2282
+ self.available_params = available_params
2283
+
2284
+ message = f"Parameter '{parameter_name}' not found in {class_name}"
2285
+ if available_params:
2286
+ message += f". Available parameters: {available_params}"
2287
+ else:
2288
+ message += ". No parameters are available"
2289
+
2290
+ super().__init__(message)
2291
+
2292
+
2293
+ def validate_parameter_compatibility(old_class, new_class) -> Dict[str, Any]:
2294
+ """
2295
+ Validate compatibility between old MetaParams class and new descriptor-based class.
2296
+
2297
+ Args:
2298
+ old_class: Original MetaParams-based class
2299
+ new_class: New descriptor-based class
2300
+
2301
+ Returns:
2302
+ Dictionary with compatibility analysis results
2303
+ """
2304
+ results = {
2305
+ "compatible": True,
2306
+ "missing_params": [],
2307
+ "extra_params": [],
2308
+ "type_mismatches": [],
2309
+ "default_mismatches": [],
2310
+ }
2311
+
2312
+ # Get old parameters
2313
+ old_params = {}
2314
+ if hasattr(old_class, "params") and hasattr(old_class.params, "_getitems"):
2315
+ old_params = dict(old_class.params._getitems())
2316
+
2317
+ # Get new parameters
2318
+ new_params = {}
2319
+ if hasattr(new_class, "_parameter_descriptors"):
2320
+ new_params = {name: desc.default for name, desc in new_class._parameter_descriptors.items()}
2321
+
2322
+ # Check for missing parameters
2323
+ for name in old_params:
2324
+ if name not in new_params:
2325
+ results["missing_params"].append(name)
2326
+ results["compatible"] = False
2327
+
2328
+ # Check for extra parameters
2329
+ for name in new_params:
2330
+ if name not in old_params:
2331
+ results["extra_params"].append(name)
2332
+
2333
+ # Check for default value mismatches
2334
+ for name in old_params:
2335
+ if name in new_params:
2336
+ if old_params[name] != new_params[name]:
2337
+ results["default_mismatches"].append(
2338
+ {
2339
+ "param": name,
2340
+ "old_default": old_params[name],
2341
+ "new_default": new_params[name],
2342
+ }
2343
+ )
2344
+
2345
+ return results