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