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
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
"""Log Message Module - Logging utilities for backtrader.
|
|
2
|
+
|
|
3
|
+
This module is the **single logging entry point** for the whole framework.
|
|
4
|
+
It builds on Python's standard ``logging`` under the hood (no third-party
|
|
5
|
+
``spdlog`` dependency) but framework and user code should go through the
|
|
6
|
+
helpers exposed here rather than importing ``logging`` directly:
|
|
7
|
+
|
|
8
|
+
- :func:`get_logger` — get a logger under the ``backtrader`` namespace.
|
|
9
|
+
- :func:`configure_logging` — opt-in handler/level setup (stderr + optional
|
|
10
|
+
rotating file). Until this is called, backtrader emits nothing (a
|
|
11
|
+
``NullHandler`` is installed on the root ``backtrader`` logger).
|
|
12
|
+
- :func:`set_level` / :func:`reset_logging` — runtime level control / test
|
|
13
|
+
reset.
|
|
14
|
+
- :class:`SpdLogManager` — legacy per-file logger factory (daily rotation),
|
|
15
|
+
kept for backward compatibility and reused internally by the strategy
|
|
16
|
+
TradeLogger path.
|
|
17
|
+
|
|
18
|
+
See ``docs/LOGGING_GUIDELINES.md`` for level-usage conventions.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> from backtrader.utils.log_message import get_logger, configure_logging
|
|
22
|
+
>>> configure_logging(level="INFO", log_file="run.log")
|
|
23
|
+
>>> logger = get_logger(__name__)
|
|
24
|
+
>>> logger.info("Strategy started")
|
|
25
|
+
|
|
26
|
+
# Legacy factory (still supported):
|
|
27
|
+
>>> log_manager = SpdLogManager(file_name="mylog.log")
|
|
28
|
+
>>> logger = log_manager.create_logger()
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import atexit
|
|
32
|
+
import functools
|
|
33
|
+
import logging
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import sys
|
|
37
|
+
import threading
|
|
38
|
+
import time
|
|
39
|
+
from datetime import date, timedelta
|
|
40
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
41
|
+
from typing import List
|
|
42
|
+
|
|
43
|
+
# Root namespace for every backtrader logger. ``get_logger(__name__)`` from
|
|
44
|
+
# inside the package already yields names like "backtrader.xxx"; a single
|
|
45
|
+
# ``configure_logging`` call on this root therefore controls them all.
|
|
46
|
+
ROOT_LOGGER_NAME = "backtrader"
|
|
47
|
+
|
|
48
|
+
DEFAULT_FORMAT = "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s"
|
|
49
|
+
DEFAULT_DATEFMT = "%Y-%m-%d %H:%M:%S"
|
|
50
|
+
|
|
51
|
+
# Marker so configure_logging can recognize and replace only the handlers it
|
|
52
|
+
# installed, never the ones a host application may have attached.
|
|
53
|
+
_BT_HANDLER_FLAG = "_backtrader_managed"
|
|
54
|
+
_CONFIG_LOCK = threading.RLock()
|
|
55
|
+
_THROTTLE_LOCK = threading.RLock()
|
|
56
|
+
_PROCESS_PID = os.getpid()
|
|
57
|
+
_logging_config = None
|
|
58
|
+
_warned_failures = set()
|
|
59
|
+
|
|
60
|
+
# Standard library pattern for libraries: install a NullHandler at import so
|
|
61
|
+
# backtrader stays silent (and warning-free) until the user opts in.
|
|
62
|
+
_root_logger = logging.getLogger(ROOT_LOGGER_NAME)
|
|
63
|
+
_root_logger.propagate = False
|
|
64
|
+
if not any(isinstance(h, logging.NullHandler) for h in _root_logger.handlers):
|
|
65
|
+
_root_logger.addHandler(logging.NullHandler())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _serialized_configuration(function):
|
|
69
|
+
@functools.wraps(function)
|
|
70
|
+
def wrapped(*args, **kwargs):
|
|
71
|
+
with _CONFIG_LOCK:
|
|
72
|
+
return function(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
return wrapped
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _warn_once(key, message):
|
|
78
|
+
"""Report a logging failure without exposing record or exception payloads."""
|
|
79
|
+
if key in _warned_failures:
|
|
80
|
+
return
|
|
81
|
+
_warned_failures.add(key)
|
|
82
|
+
try:
|
|
83
|
+
print("backtrader: " + message, file=sys.stderr)
|
|
84
|
+
except Exception: # nosec B110
|
|
85
|
+
# No remaining safe diagnostic sink.
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_SECRET_VALUE = re.compile(
|
|
90
|
+
r"(?i)(\b(?:password|passwd|passphrase|api[_-]?key|api[_-]?secret|secret|"
|
|
91
|
+
r"access[_-]?token|refresh[_-]?token|authorization)\b[\"']?\s*[:=]\s*)"
|
|
92
|
+
r"(?:\"[^\"]*\"|'[^']*'|[^\s,;}\]]+)"
|
|
93
|
+
)
|
|
94
|
+
_BEARER_VALUE = re.compile(r"(?i)\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+")
|
|
95
|
+
_URL_PASSWORD = re.compile(r"(://[^\s/:@]+:)[^\s/@]+(@)")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _RedactingFormatter(logging.Formatter):
|
|
99
|
+
"""Redact common credential fields in messages and rendered tracebacks."""
|
|
100
|
+
|
|
101
|
+
def format(self, record):
|
|
102
|
+
rendered = super().format(record)
|
|
103
|
+
rendered = _BEARER_VALUE.sub(r"\1 [REDACTED]", rendered)
|
|
104
|
+
rendered = _SECRET_VALUE.sub(r"\1[REDACTED]", rendered)
|
|
105
|
+
return _URL_PASSWORD.sub(r"\1[REDACTED]\2", rendered)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _NonFatalHandlerMixin:
|
|
109
|
+
"""Shared failure isolation for handlers owned by this module."""
|
|
110
|
+
|
|
111
|
+
# ``logging.Handler._closed`` is a private implementation detail and is
|
|
112
|
+
# absent on Python 3.8/3.9. Keep the lifecycle state we need under our
|
|
113
|
+
# own stable name instead.
|
|
114
|
+
_bt_closed: bool
|
|
115
|
+
|
|
116
|
+
def handleError(self, record):
|
|
117
|
+
_warn_once("write", "log write failed; further logging failures are suppressed")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _SafeStreamHandler(_NonFatalHandlerMixin, logging.StreamHandler):
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _get_logging_config_snapshot():
|
|
125
|
+
"""Return only opt-in split-file configuration, never live handlers."""
|
|
126
|
+
with _CONFIG_LOCK:
|
|
127
|
+
return None if _logging_config is None else dict(_logging_config)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _restore_logging_config(snapshot):
|
|
131
|
+
"""Restore configured split logging in optimization workers; None is inert."""
|
|
132
|
+
if snapshot is None:
|
|
133
|
+
return
|
|
134
|
+
with _CONFIG_LOCK:
|
|
135
|
+
managed = [
|
|
136
|
+
handler
|
|
137
|
+
for handler in _root_logger.handlers
|
|
138
|
+
if getattr(handler, _BT_HANDLER_FLAG, False)
|
|
139
|
+
]
|
|
140
|
+
if (
|
|
141
|
+
snapshot == _logging_config
|
|
142
|
+
and managed
|
|
143
|
+
and all(
|
|
144
|
+
getattr(handler, "_owner_pid", os.getpid()) == os.getpid() for handler in managed
|
|
145
|
+
)
|
|
146
|
+
):
|
|
147
|
+
return
|
|
148
|
+
configure_logging(**snapshot)
|
|
149
|
+
# A scoped DEBUG sink alone cannot enable a fresh worker's child
|
|
150
|
+
# logger: its default NOTSET level would inherit the root INFO level.
|
|
151
|
+
# Restore the explicit levels that created these scopes with the same
|
|
152
|
+
# config snapshot used for the split handlers.
|
|
153
|
+
for scope in snapshot.get("_debug_scopes", ()):
|
|
154
|
+
get_logger(scope).setLevel(logging.DEBUG)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_logger(name=None):
|
|
158
|
+
"""Return a logger under the ``backtrader`` namespace.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
name: Usually ``__name__`` of the calling module. If it already starts
|
|
162
|
+
with ``"backtrader"`` it is used as-is; otherwise it is nested
|
|
163
|
+
under the ``backtrader`` root (e.g. ``"mystuff"`` ->
|
|
164
|
+
``"backtrader.mystuff"``). ``None`` returns the root logger.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
logging.Logger: A logger in the backtrader hierarchy.
|
|
168
|
+
"""
|
|
169
|
+
if not name:
|
|
170
|
+
return logging.getLogger(ROOT_LOGGER_NAME)
|
|
171
|
+
if name == ROOT_LOGGER_NAME or name.startswith(ROOT_LOGGER_NAME + "."):
|
|
172
|
+
return logging.getLogger(name)
|
|
173
|
+
return logging.getLogger(f"{ROOT_LOGGER_NAME}.{name}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _is_output_enabled_for(level, logger=None):
|
|
177
|
+
"""Return whether an opt-in output can receive ``level`` from ``logger``.
|
|
178
|
+
|
|
179
|
+
A library logger at ``NOTSET`` inherits the host root's effective level
|
|
180
|
+
even when backtrader has only its default :class:`logging.NullHandler`.
|
|
181
|
+
Lifecycle diagnostics must not mistake that host setting for an explicit
|
|
182
|
+
backtrader logging opt-in because evaluating a diagnostic may call a live
|
|
183
|
+
broker or feed accessor. A non-null handler on the backtrader root is
|
|
184
|
+
either installed by :func:`configure_logging` or deliberately attached by
|
|
185
|
+
the host, so it is the required output boundary.
|
|
186
|
+
"""
|
|
187
|
+
level_int = _level_to_int(level)
|
|
188
|
+
with _CONFIG_LOCK:
|
|
189
|
+
current = logger or logging.getLogger(ROOT_LOGGER_NAME)
|
|
190
|
+
if not current.isEnabledFor(level_int):
|
|
191
|
+
return False
|
|
192
|
+
while current is not None:
|
|
193
|
+
if any(not isinstance(handler, logging.NullHandler) for handler in current.handlers):
|
|
194
|
+
return True
|
|
195
|
+
if not current.propagate:
|
|
196
|
+
return False
|
|
197
|
+
current = current.parent
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _level_to_int(level):
|
|
202
|
+
"""Coerce a level given as int or name into the logging int constant."""
|
|
203
|
+
if isinstance(level, int):
|
|
204
|
+
return level
|
|
205
|
+
if isinstance(level, str):
|
|
206
|
+
resolved = logging.getLevelName(level.upper())
|
|
207
|
+
if isinstance(resolved, int):
|
|
208
|
+
return resolved
|
|
209
|
+
raise ValueError(f"invalid logging level: {level!r}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _normalize_debug_scopes(scopes):
|
|
213
|
+
"""Normalize internal split-file DEBUG logger prefixes."""
|
|
214
|
+
if scopes is None:
|
|
215
|
+
return ()
|
|
216
|
+
if isinstance(scopes, str):
|
|
217
|
+
raise ValueError("_debug_scopes must be an iterable of logger names")
|
|
218
|
+
try:
|
|
219
|
+
candidates = tuple(scopes)
|
|
220
|
+
except TypeError as exc:
|
|
221
|
+
raise ValueError("_debug_scopes must be an iterable of logger names") from exc
|
|
222
|
+
|
|
223
|
+
normalized = []
|
|
224
|
+
for scope in candidates:
|
|
225
|
+
if not isinstance(scope, str) or not scope:
|
|
226
|
+
raise ValueError("_debug_scopes entries must be non-empty logger names")
|
|
227
|
+
normalized.append(get_logger(scope).name)
|
|
228
|
+
return tuple(sorted(set(normalized)))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _remove_managed_handlers(logger):
|
|
232
|
+
"""Remove only handlers previously installed by configure_logging()."""
|
|
233
|
+
for handler in list(logger.handlers):
|
|
234
|
+
if getattr(handler, _BT_HANDLER_FLAG, False):
|
|
235
|
+
logger.removeHandler(handler)
|
|
236
|
+
try:
|
|
237
|
+
handler.close()
|
|
238
|
+
except Exception: # nosec B110
|
|
239
|
+
# Handler may already be closed; closing is best-effort cleanup
|
|
240
|
+
# during logging reconfiguration. Logging here could recurse
|
|
241
|
+
# into the handler being torn down, so stay silent.
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _close_handler_safely(handler):
|
|
246
|
+
"""Keep cleanup failures from replacing configuration errors or state."""
|
|
247
|
+
try:
|
|
248
|
+
handler.close()
|
|
249
|
+
except Exception:
|
|
250
|
+
_warn_once(
|
|
251
|
+
"handler_close",
|
|
252
|
+
"log handler close failed; further close failures are suppressed",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@_serialized_configuration
|
|
257
|
+
def configure_logging(
|
|
258
|
+
level="INFO",
|
|
259
|
+
log_file=None,
|
|
260
|
+
fmt=None,
|
|
261
|
+
datefmt=None,
|
|
262
|
+
console=True,
|
|
263
|
+
max_bytes=10 * 1024 * 1024,
|
|
264
|
+
backup_count=5,
|
|
265
|
+
propagate=False,
|
|
266
|
+
*,
|
|
267
|
+
log_dir=None,
|
|
268
|
+
script_name=None,
|
|
269
|
+
backend="auto",
|
|
270
|
+
retention_days=30,
|
|
271
|
+
_debug_scopes=(),
|
|
272
|
+
):
|
|
273
|
+
"""Configure the ``backtrader`` logger hierarchy (opt-in).
|
|
274
|
+
|
|
275
|
+
Backtrader emits nothing until you call this. It configures only the
|
|
276
|
+
``backtrader`` logger (never the root logger), so it will not clobber a
|
|
277
|
+
host application's logging. Calling it again replaces backtrader-managed
|
|
278
|
+
handlers (idempotent) while leaving host-added handlers untouched.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
level: Level as int (``logging.INFO``) or name (``"INFO"``).
|
|
282
|
+
log_file: If given, also write to this single file via a
|
|
283
|
+
:class:`~logging.handlers.RotatingFileHandler`. Mutually exclusive
|
|
284
|
+
with ``log_dir``.
|
|
285
|
+
log_dir: If given, enable the iteration-29 split-file layout
|
|
286
|
+
``log_dir/<script>/<YYYY_MM_DD>/{error,warning,info}.log``
|
|
287
|
+
(plus ``debug.log`` when ``level=DEBUG``). ``<script>`` comes from
|
|
288
|
+
``script_name`` or auto-detection of ``sys.argv[0]``.
|
|
289
|
+
script_name: Directory name under ``log_dir``. ``None`` auto-detects
|
|
290
|
+
from ``sys.argv[0]`` (``xxx/run.py`` -> ``xxx_run``).
|
|
291
|
+
backend: ``"auto"`` (default; prefer the optional ``spdlog`` package,
|
|
292
|
+
fall back to stdlib with one diagnostic), ``"spdlog"`` (raise ``ImportError``
|
|
293
|
+
if unavailable) or ``"stdlib"``.
|
|
294
|
+
retention_days: Date directories older than this many days under the
|
|
295
|
+
script's own directory are removed at configure/rollover time.
|
|
296
|
+
fmt: Message format. Defaults to :data:`DEFAULT_FORMAT`.
|
|
297
|
+
datefmt: Date format. Defaults to :data:`DEFAULT_DATEFMT`.
|
|
298
|
+
console: Whether to add a stderr ``StreamHandler``.
|
|
299
|
+
max_bytes: Rotating file handler size before rollover (``log_file``).
|
|
300
|
+
backup_count: Number of rotated backups to keep (``log_file``).
|
|
301
|
+
propagate: Whether the backtrader logger propagates to the root
|
|
302
|
+
logger (default ``False``).
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
logging.Logger: The configured ``backtrader`` root logger.
|
|
306
|
+
"""
|
|
307
|
+
from logging.handlers import RotatingFileHandler
|
|
308
|
+
|
|
309
|
+
global _logging_config
|
|
310
|
+
if log_dir is not None and log_file is not None:
|
|
311
|
+
raise ValueError("log_dir and log_file are mutually exclusive; pass only one")
|
|
312
|
+
if backend not in {"auto", "stdlib", "spdlog"}:
|
|
313
|
+
raise ValueError(f"invalid backend: {backend!r}")
|
|
314
|
+
if retention_days is not None and (
|
|
315
|
+
isinstance(retention_days, bool)
|
|
316
|
+
or not isinstance(retention_days, int)
|
|
317
|
+
or retention_days < 0
|
|
318
|
+
):
|
|
319
|
+
raise ValueError("retention_days must be a non-negative integer or None")
|
|
320
|
+
logger = logging.getLogger(ROOT_LOGGER_NAME)
|
|
321
|
+
level_int = _level_to_int(level)
|
|
322
|
+
debug_scopes = _normalize_debug_scopes(_debug_scopes)
|
|
323
|
+
formatter = _RedactingFormatter(fmt or DEFAULT_FORMAT, datefmt or DEFAULT_DATEFMT)
|
|
324
|
+
spdlog_mod = None
|
|
325
|
+
snapshot = None
|
|
326
|
+
if log_dir is not None:
|
|
327
|
+
log_dir = os.path.abspath(os.fspath(log_dir))
|
|
328
|
+
script = _sanitize_script_name(script_name) if script_name else _detect_script_name()
|
|
329
|
+
script_dir = os.path.join(log_dir, script)
|
|
330
|
+
if os.path.islink(script_dir):
|
|
331
|
+
raise ValueError("script log directory must not be a symbolic link")
|
|
332
|
+
if backend != "stdlib":
|
|
333
|
+
spdlog_mod = _detect_spdlog()
|
|
334
|
+
if spdlog_mod is None and backend == "spdlog":
|
|
335
|
+
raise ImportError("backend='spdlog' requested but the spdlog package is not usable")
|
|
336
|
+
if spdlog_mod is None:
|
|
337
|
+
_warn_once("backend", "spdlog unavailable; using stdlib logging")
|
|
338
|
+
snapshot = {
|
|
339
|
+
"level": level_int,
|
|
340
|
+
"log_dir": log_dir,
|
|
341
|
+
"script_name": script,
|
|
342
|
+
# Persist the requested policy, not this process's selected
|
|
343
|
+
# implementation. ``auto`` must re-probe in a spawned worker
|
|
344
|
+
# where spdlog may be unavailable.
|
|
345
|
+
"backend": backend,
|
|
346
|
+
"retention_days": retention_days,
|
|
347
|
+
"fmt": fmt,
|
|
348
|
+
"datefmt": datefmt,
|
|
349
|
+
"console": console,
|
|
350
|
+
"propagate": propagate,
|
|
351
|
+
}
|
|
352
|
+
if debug_scopes:
|
|
353
|
+
snapshot["_debug_scopes"] = debug_scopes
|
|
354
|
+
|
|
355
|
+
# Build first: invalid configuration must not destroy a working logger.
|
|
356
|
+
pending: List[logging.Handler] = []
|
|
357
|
+
handler: logging.Handler
|
|
358
|
+
try:
|
|
359
|
+
if console:
|
|
360
|
+
pending.append(_SafeStreamHandler())
|
|
361
|
+
if log_file is not None:
|
|
362
|
+
os.makedirs(os.path.dirname(os.path.abspath(log_file)), exist_ok=True)
|
|
363
|
+
pending.append(
|
|
364
|
+
RotatingFileHandler(
|
|
365
|
+
log_file, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8"
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
if log_dir is not None:
|
|
369
|
+
debug_scope_filter = (
|
|
370
|
+
_NamePrefixFilter(debug_scopes)
|
|
371
|
+
if debug_scopes and level_int > logging.DEBUG
|
|
372
|
+
else None
|
|
373
|
+
)
|
|
374
|
+
matrix = (
|
|
375
|
+
("error", logging.ERROR, None),
|
|
376
|
+
("warning", logging.WARNING, _ExactLevelFilter(logging.WARNING)),
|
|
377
|
+
("info", logging.INFO, _ExactLevelFilter(logging.INFO)),
|
|
378
|
+
("debug", logging.DEBUG, _ExactLevelFilter(logging.DEBUG)),
|
|
379
|
+
)
|
|
380
|
+
for level_name, handler_level, exact in matrix:
|
|
381
|
+
if level_name == "debug" and level_int > logging.DEBUG and not debug_scopes:
|
|
382
|
+
continue
|
|
383
|
+
if spdlog_mod is not None:
|
|
384
|
+
handler = SpdlogHandler(
|
|
385
|
+
spdlog_mod, script_dir, level_name, handler_level, exact, retention_days
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
handler = _DailyLevelFileHandler(
|
|
389
|
+
script_dir, level_name, handler_level, exact, retention_days
|
|
390
|
+
)
|
|
391
|
+
if level_name == "debug" and debug_scope_filter is not None:
|
|
392
|
+
handler.addFilter(debug_scope_filter)
|
|
393
|
+
pending.append(handler)
|
|
394
|
+
for handler in pending:
|
|
395
|
+
handler.setFormatter(formatter)
|
|
396
|
+
setattr(handler, _BT_HANDLER_FLAG, True)
|
|
397
|
+
except Exception:
|
|
398
|
+
for handler in pending:
|
|
399
|
+
_close_handler_safely(handler)
|
|
400
|
+
raise
|
|
401
|
+
|
|
402
|
+
previous = [h for h in logger.handlers if getattr(h, _BT_HANDLER_FLAG, False)]
|
|
403
|
+
logger.handlers = [h for h in logger.handlers if h not in previous] + pending
|
|
404
|
+
for handler in previous:
|
|
405
|
+
_close_handler_safely(handler)
|
|
406
|
+
logger.setLevel(level_int)
|
|
407
|
+
logger.propagate = propagate
|
|
408
|
+
_logging_config = snapshot
|
|
409
|
+
if log_dir is not None:
|
|
410
|
+
_cleanup_retention(script_dir, retention_days)
|
|
411
|
+
return logger
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@_serialized_configuration
|
|
415
|
+
def set_level(level, name=None):
|
|
416
|
+
"""Set the level of a backtrader logger at runtime.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
level: Level as int or name.
|
|
420
|
+
name: Sub-logger name (``__name__``-style). ``None`` targets the root.
|
|
421
|
+
A root-level update with active split-file logging atomically
|
|
422
|
+
rebuilds its handler matrix, so a DEBUG transition creates
|
|
423
|
+
``debug.log`` and is inherited by optimization workers. A named
|
|
424
|
+
DEBUG update creates a DEBUG sink restricted to that logger's
|
|
425
|
+
hierarchy, leaving unrelated loggers at the configured root level.
|
|
426
|
+
"""
|
|
427
|
+
level_int = _level_to_int(level)
|
|
428
|
+
if name is None and _logging_config is not None:
|
|
429
|
+
snapshot = dict(_logging_config)
|
|
430
|
+
snapshot["level"] = level_int
|
|
431
|
+
configure_logging(**snapshot)
|
|
432
|
+
return
|
|
433
|
+
target = get_logger(name)
|
|
434
|
+
if name is not None and _logging_config is not None:
|
|
435
|
+
old_scopes = set(_logging_config.get("_debug_scopes", ()))
|
|
436
|
+
scopes = set(old_scopes)
|
|
437
|
+
if level_int == logging.DEBUG:
|
|
438
|
+
scopes.add(target.name)
|
|
439
|
+
else:
|
|
440
|
+
scopes.discard(target.name)
|
|
441
|
+
if scopes != old_scopes:
|
|
442
|
+
# Split-file routing has no DEBUG sink above INFO. Register a
|
|
443
|
+
# prefix-filtered sink for this child instead of widening the
|
|
444
|
+
# entire backtrader hierarchy to DEBUG.
|
|
445
|
+
snapshot = dict(_logging_config)
|
|
446
|
+
if scopes:
|
|
447
|
+
snapshot["_debug_scopes"] = tuple(sorted(scopes))
|
|
448
|
+
else:
|
|
449
|
+
snapshot.pop("_debug_scopes", None)
|
|
450
|
+
configure_logging(**snapshot)
|
|
451
|
+
target.setLevel(level_int)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@_serialized_configuration
|
|
455
|
+
def reset_logging():
|
|
456
|
+
"""Remove backtrader-managed handlers and restore the default NullHandler.
|
|
457
|
+
|
|
458
|
+
Mainly useful in tests to return to the pristine, no-output state. Also
|
|
459
|
+
flushes and releases any spdlog-side resources held by managed handlers.
|
|
460
|
+
"""
|
|
461
|
+
global _logging_config
|
|
462
|
+
logger = logging.getLogger(ROOT_LOGGER_NAME)
|
|
463
|
+
with _THROTTLE_LOCK:
|
|
464
|
+
_throttle_state.clear()
|
|
465
|
+
_remove_managed_handlers(logger)
|
|
466
|
+
logger.setLevel(logging.NOTSET)
|
|
467
|
+
logger.propagate = False
|
|
468
|
+
_logging_config = None
|
|
469
|
+
if not any(isinstance(h, logging.NullHandler) for h in logger.handlers):
|
|
470
|
+
logger.addHandler(logging.NullHandler())
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class SpdLogManager:
|
|
474
|
+
"""Logger factory using the Python standard ``logging`` module.
|
|
475
|
+
|
|
476
|
+
Creates loggers with daily file rotation and optional console output.
|
|
477
|
+
API is kept compatible with the previous spdlog-based implementation.
|
|
478
|
+
|
|
479
|
+
Attributes:
|
|
480
|
+
file_name: Name of the log file.
|
|
481
|
+
logger_name: Name for the logger.
|
|
482
|
+
rotation_hour: Hour of day for log rotation (0-23).
|
|
483
|
+
rotation_minute: Minute of hour for log rotation (0-59).
|
|
484
|
+
print_info: Whether to also print to console.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
_LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
488
|
+
|
|
489
|
+
def __init__(
|
|
490
|
+
self,
|
|
491
|
+
file_name="log_strategy_info.log",
|
|
492
|
+
logger_name="hello",
|
|
493
|
+
rotation_hour=0,
|
|
494
|
+
rotation_minute=0,
|
|
495
|
+
print_info=False,
|
|
496
|
+
):
|
|
497
|
+
"""Initialize the SpdLogManager.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
file_name: Name of the output log file.
|
|
501
|
+
logger_name: Name for the logger.
|
|
502
|
+
rotation_hour: Hour (0-23) to rotate log files daily.
|
|
503
|
+
rotation_minute: Minute (0-59) to rotate log files.
|
|
504
|
+
print_info: Whether to also output to console.
|
|
505
|
+
"""
|
|
506
|
+
self.file_name = file_name
|
|
507
|
+
self.logger_name = logger_name
|
|
508
|
+
self.rotation_hour = rotation_hour
|
|
509
|
+
self.rotation_minute = rotation_minute
|
|
510
|
+
self.print_info = print_info
|
|
511
|
+
|
|
512
|
+
def create_logger(self):
|
|
513
|
+
"""Create and return a configured ``logging.Logger`` instance.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
logging.Logger: Logger with file handler (daily rotation)
|
|
517
|
+
and optionally a console handler.
|
|
518
|
+
"""
|
|
519
|
+
logger = logging.getLogger(f"backtrader.{self.logger_name}")
|
|
520
|
+
|
|
521
|
+
# Avoid adding duplicate handlers on repeated calls
|
|
522
|
+
if logger.handlers:
|
|
523
|
+
return logger
|
|
524
|
+
|
|
525
|
+
logger.setLevel(logging.DEBUG)
|
|
526
|
+
formatter = logging.Formatter(self._LOG_FORMAT)
|
|
527
|
+
|
|
528
|
+
# File handler with daily rotation
|
|
529
|
+
if self.file_name:
|
|
530
|
+
# Ensure the log directory exists
|
|
531
|
+
log_dir = os.path.dirname(self.file_name)
|
|
532
|
+
if log_dir and not os.path.exists(log_dir):
|
|
533
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
534
|
+
|
|
535
|
+
at_time = None
|
|
536
|
+
if self.rotation_hour or self.rotation_minute:
|
|
537
|
+
from datetime import time
|
|
538
|
+
|
|
539
|
+
at_time = time(self.rotation_hour, self.rotation_minute)
|
|
540
|
+
|
|
541
|
+
fh = TimedRotatingFileHandler(
|
|
542
|
+
self.file_name,
|
|
543
|
+
when="midnight",
|
|
544
|
+
interval=1,
|
|
545
|
+
backupCount=30,
|
|
546
|
+
encoding="utf-8",
|
|
547
|
+
atTime=at_time,
|
|
548
|
+
)
|
|
549
|
+
fh.setLevel(logging.DEBUG)
|
|
550
|
+
fh.setFormatter(formatter)
|
|
551
|
+
logger.addHandler(fh)
|
|
552
|
+
|
|
553
|
+
# Console handler
|
|
554
|
+
if self.print_info:
|
|
555
|
+
ch = logging.StreamHandler()
|
|
556
|
+
ch.setLevel(logging.INFO)
|
|
557
|
+
ch.setFormatter(formatter)
|
|
558
|
+
logger.addHandler(ch)
|
|
559
|
+
|
|
560
|
+
return logger
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ===========================================================================
|
|
564
|
+
# Iteration 29: split-file logging (log_dir/<script>/<YYYY_MM_DD>/<level>.log)
|
|
565
|
+
# with level-exact routing, daily rollover, retention cleanup, optional
|
|
566
|
+
# spdlog write backend (stdlib fallback) and throttled storm suppression.
|
|
567
|
+
# See docs/_internal/opts/requirements/迭代29-日志体系完善/.
|
|
568
|
+
# ===========================================================================
|
|
569
|
+
|
|
570
|
+
_DATE_DIR_RE = re.compile(r"^\d{4}_\d{2}_\d{2}$")
|
|
571
|
+
|
|
572
|
+
# Test hook: override to freeze the "current day" (rollover tests monkeypatch
|
|
573
|
+
# this instead of relying on the real clock crossing midnight).
|
|
574
|
+
_today = date.today
|
|
575
|
+
|
|
576
|
+
# Test hook for the multiprocess pid; None means "detect from environment".
|
|
577
|
+
_MP_PID = None
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _mp_log_suffix():
|
|
581
|
+
"""File suffix isolating child-process logs: ``.p<pid>`` or ``""``.
|
|
582
|
+
|
|
583
|
+
Under cerebro optimize (maxcpus>1) each spawned worker opens its own
|
|
584
|
+
level files; the main process keeps the unsuffixed names.
|
|
585
|
+
"""
|
|
586
|
+
global _MP_PID
|
|
587
|
+
if _MP_PID is not None:
|
|
588
|
+
return f".p{_MP_PID}"
|
|
589
|
+
if os.getpid() != _PROCESS_PID:
|
|
590
|
+
return f".p{os.getpid()}"
|
|
591
|
+
try:
|
|
592
|
+
import multiprocessing
|
|
593
|
+
|
|
594
|
+
if multiprocessing.parent_process() is not None:
|
|
595
|
+
return f".p{os.getpid()}"
|
|
596
|
+
except Exception: # nosec B110
|
|
597
|
+
# Process detection must not break logging teardown.
|
|
598
|
+
pass
|
|
599
|
+
return ""
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _detect_script_name(default="backtrader"):
|
|
603
|
+
"""Derive the log directory name from ``sys.argv[0]``.
|
|
604
|
+
|
|
605
|
+
``xxx/run.py`` -> ``xxx_run`` (path separators become underscores so the
|
|
606
|
+
relative-path structure survives as a flat directory name). Interactive
|
|
607
|
+
interpreters, ``-c`` and ``<stdin>`` fall back to ``default``.
|
|
608
|
+
"""
|
|
609
|
+
argv0 = sys.argv[0] if sys.argv and sys.argv[0] else ""
|
|
610
|
+
if not argv0 or argv0 in ("<stdin>", "-c") or argv0.startswith("<"):
|
|
611
|
+
return default
|
|
612
|
+
return _sanitize_script_name(os.path.splitext(argv0.replace("\\", "/"))[0], default)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _sanitize_script_name(name, default="backtrader"):
|
|
616
|
+
"""Keep both inferred and explicit names in one safe path component."""
|
|
617
|
+
name = str(name).replace("\\", "/").strip("./")
|
|
618
|
+
name = re.sub(r"[^A-Za-z0-9_.\-]", "_", name)
|
|
619
|
+
return name or default
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _day_directory(script_dir, day):
|
|
623
|
+
day_dir = os.path.join(script_dir, day.strftime("%Y_%m_%d"))
|
|
624
|
+
if os.path.islink(script_dir) or os.path.islink(day_dir):
|
|
625
|
+
raise OSError("symbolic links are not allowed in split log directories")
|
|
626
|
+
os.makedirs(day_dir, exist_ok=True)
|
|
627
|
+
return day_dir
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _cleanup_retention(script_dir, retention_days):
|
|
631
|
+
"""Delete this script's date dirs older than ``retention_days`` days.
|
|
632
|
+
|
|
633
|
+
Scoped to ``script_dir`` only: other scripts' directories and non-date
|
|
634
|
+
entries are never touched. Failures warn once on stderr and never raise
|
|
635
|
+
(logging teardown must not break a running backtest).
|
|
636
|
+
"""
|
|
637
|
+
if retention_days is None or os.path.islink(script_dir) or not os.path.isdir(script_dir):
|
|
638
|
+
return
|
|
639
|
+
try:
|
|
640
|
+
cutoff = _today() - timedelta(days=int(retention_days))
|
|
641
|
+
for entry in os.listdir(script_dir):
|
|
642
|
+
if not _DATE_DIR_RE.match(entry):
|
|
643
|
+
continue
|
|
644
|
+
try:
|
|
645
|
+
day = date(int(entry[0:4]), int(entry[5:7]), int(entry[8:10]))
|
|
646
|
+
except ValueError:
|
|
647
|
+
continue
|
|
648
|
+
if day < cutoff:
|
|
649
|
+
import shutil
|
|
650
|
+
|
|
651
|
+
path = os.path.join(script_dir, entry)
|
|
652
|
+
if os.path.islink(path) or not os.path.isdir(path):
|
|
653
|
+
continue
|
|
654
|
+
shutil.rmtree(path)
|
|
655
|
+
except Exception: # cleanup must never affect trading
|
|
656
|
+
_warn_once("retention", "log retention cleanup failed")
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class _ExactLevelFilter(logging.Filter):
|
|
660
|
+
"""Pass only records whose level *equals* the configured level."""
|
|
661
|
+
|
|
662
|
+
def __init__(self, level):
|
|
663
|
+
super().__init__()
|
|
664
|
+
self.level = level
|
|
665
|
+
|
|
666
|
+
def filter(self, record):
|
|
667
|
+
return record.levelno == self.level
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class _NamePrefixFilter(logging.Filter):
|
|
671
|
+
"""Pass records from explicitly enabled logger subtrees."""
|
|
672
|
+
|
|
673
|
+
def __init__(self, prefixes):
|
|
674
|
+
super().__init__()
|
|
675
|
+
self.prefixes = tuple(prefixes)
|
|
676
|
+
|
|
677
|
+
def filter(self, record):
|
|
678
|
+
return any(
|
|
679
|
+
record.name == prefix or record.name.startswith(prefix + ".")
|
|
680
|
+
for prefix in self.prefixes
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class _DailyLevelFileHandler(_NonFatalHandlerMixin, logging.FileHandler):
|
|
685
|
+
"""Stdlib-backend file handler writing ``<script_dir>/<date>/<level>.log``.
|
|
686
|
+
|
|
687
|
+
Lazily rolls to a new date directory on the first record emitted after
|
|
688
|
+
midnight; runs retention cleanup on each rollover. ``os.path.dirname``
|
|
689
|
+
is empty only for relative single-component paths, which never happens
|
|
690
|
+
here because the constructor always joins ``script_dir``.
|
|
691
|
+
"""
|
|
692
|
+
|
|
693
|
+
def __init__(self, script_dir, level_name, handler_level, exact_filter, retention_days):
|
|
694
|
+
self._script_dir = script_dir
|
|
695
|
+
self._level_name = level_name
|
|
696
|
+
self._retention_days = retention_days
|
|
697
|
+
self._owner_pid = os.getpid()
|
|
698
|
+
self._cur_date = _today()
|
|
699
|
+
path = self._path_for(self._cur_date)
|
|
700
|
+
super().__init__(path, mode="a", encoding="utf-8", delay=False)
|
|
701
|
+
self._bt_closed = False
|
|
702
|
+
self.setLevel(handler_level)
|
|
703
|
+
if exact_filter is not None:
|
|
704
|
+
self.addFilter(exact_filter)
|
|
705
|
+
|
|
706
|
+
def _path_for(self, day):
|
|
707
|
+
day_dir = _day_directory(self._script_dir, day)
|
|
708
|
+
path = os.path.join(day_dir, f"{self._level_name}{_mp_log_suffix()}.log")
|
|
709
|
+
if os.path.islink(path):
|
|
710
|
+
raise OSError("symbolic links are not allowed for split log files")
|
|
711
|
+
return path
|
|
712
|
+
|
|
713
|
+
def emit(self, record):
|
|
714
|
+
if self._bt_closed:
|
|
715
|
+
return
|
|
716
|
+
try:
|
|
717
|
+
today = _today()
|
|
718
|
+
if today != self._cur_date or self._owner_pid != os.getpid():
|
|
719
|
+
self._rotate(today)
|
|
720
|
+
super().emit(record)
|
|
721
|
+
except Exception:
|
|
722
|
+
self.handleError(record)
|
|
723
|
+
|
|
724
|
+
def _rotate(self, today):
|
|
725
|
+
path = os.path.abspath(self._path_for(today))
|
|
726
|
+
new_stream = open(path, "a", encoding="utf-8")
|
|
727
|
+
old_stream = self.stream
|
|
728
|
+
self.stream = new_stream
|
|
729
|
+
self.baseFilename = path
|
|
730
|
+
self._cur_date = today
|
|
731
|
+
self._owner_pid = os.getpid()
|
|
732
|
+
if old_stream is not None:
|
|
733
|
+
old_stream.close()
|
|
734
|
+
_cleanup_retention(self._script_dir, self._retention_days)
|
|
735
|
+
|
|
736
|
+
def close(self):
|
|
737
|
+
"""Close once and keep a closed FileHandler from reopening on emit."""
|
|
738
|
+
self._bt_closed = True
|
|
739
|
+
super().close()
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _detect_spdlog():
|
|
743
|
+
"""Return the ``spdlog`` module if usable, else ``None``.
|
|
744
|
+
|
|
745
|
+
Probe contract (frozen in D29-13): import succeeds, required symbols
|
|
746
|
+
exist, and a smoke write to a temp file works. Never raises.
|
|
747
|
+
"""
|
|
748
|
+
try:
|
|
749
|
+
import spdlog
|
|
750
|
+
|
|
751
|
+
if not all(hasattr(spdlog, name) for name in ("FileLogger", "LogLevel", "drop")):
|
|
752
|
+
return None
|
|
753
|
+
import tempfile
|
|
754
|
+
|
|
755
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
756
|
+
probe_name = f"bt_backend_probe.{os.getpid()}.{time.monotonic_ns()}"
|
|
757
|
+
try:
|
|
758
|
+
probe = spdlog.FileLogger(probe_name, os.path.join(tmp, "probe.log"), truncate=True)
|
|
759
|
+
probe.set_pattern("%v")
|
|
760
|
+
probe.set_level(spdlog.LogLevel.TRACE)
|
|
761
|
+
probe.flush_on(spdlog.LogLevel.WARN)
|
|
762
|
+
probe.info("probe")
|
|
763
|
+
probe.flush()
|
|
764
|
+
with open(os.path.join(tmp, "probe.log"), encoding="utf-8") as stream:
|
|
765
|
+
if "probe" not in stream.read():
|
|
766
|
+
return None
|
|
767
|
+
finally:
|
|
768
|
+
spdlog.drop(probe_name)
|
|
769
|
+
return spdlog
|
|
770
|
+
except Exception:
|
|
771
|
+
return None
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
class SpdlogHandler(_NonFatalHandlerMixin, logging.Handler):
|
|
775
|
+
"""spdlog-backend handler mirroring :class:`_DailyLevelFileHandler`.
|
|
776
|
+
|
|
777
|
+
The framework layer owns the layout: the Python formatter renders the
|
|
778
|
+
full line and spdlog writes it with pattern ``%v`` (message only), so
|
|
779
|
+
line format matches the stdlib backend. Date rollover drops and reopens
|
|
780
|
+
the underlying spdlog logger on the new path.
|
|
781
|
+
"""
|
|
782
|
+
|
|
783
|
+
_METHODS = (
|
|
784
|
+
(logging.CRITICAL, "critical"),
|
|
785
|
+
(logging.ERROR, "error"),
|
|
786
|
+
(logging.WARNING, "warn"),
|
|
787
|
+
(logging.INFO, "info"),
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
def __init__(
|
|
791
|
+
self, spdlog_mod, script_dir, level_name, handler_level, exact_filter, retention_days
|
|
792
|
+
):
|
|
793
|
+
super().__init__(level=handler_level)
|
|
794
|
+
self._bt_closed = False
|
|
795
|
+
self._mod = spdlog_mod
|
|
796
|
+
self._script_dir = script_dir
|
|
797
|
+
self._level_name = level_name
|
|
798
|
+
self._retention_days = retention_days
|
|
799
|
+
self._owner_pid = os.getpid()
|
|
800
|
+
self._cur_date = _today()
|
|
801
|
+
self._unique = f"bt.{level_name}.{os.getpid()}.{id(self):x}"
|
|
802
|
+
if exact_filter is not None:
|
|
803
|
+
self.addFilter(exact_filter)
|
|
804
|
+
self._path = self._path_for(self._cur_date)
|
|
805
|
+
self._logger = self._open_logger(self._path)
|
|
806
|
+
|
|
807
|
+
def _path_for(self, day):
|
|
808
|
+
day_dir = _day_directory(self._script_dir, day)
|
|
809
|
+
path = os.path.join(day_dir, f"{self._level_name}{_mp_log_suffix()}.log")
|
|
810
|
+
if os.path.islink(path):
|
|
811
|
+
raise OSError("symbolic links are not allowed for split log files")
|
|
812
|
+
return path
|
|
813
|
+
|
|
814
|
+
def _open_logger(self, path):
|
|
815
|
+
open(path, "a", encoding="utf-8").close() # config-time file creation
|
|
816
|
+
lg = self._mod.FileLogger(self._unique, path, truncate=False)
|
|
817
|
+
lg.set_pattern("%v") # the Python formatter already rendered the line
|
|
818
|
+
lg.set_level(self._mod.LogLevel.TRACE) # filtering stays on our side
|
|
819
|
+
lg.flush_on(self._mod.LogLevel.WARN)
|
|
820
|
+
return lg
|
|
821
|
+
|
|
822
|
+
def _method_for(self, levelno):
|
|
823
|
+
for lvl, name in self._METHODS:
|
|
824
|
+
if levelno >= lvl:
|
|
825
|
+
return name
|
|
826
|
+
return "debug"
|
|
827
|
+
|
|
828
|
+
def emit(self, record):
|
|
829
|
+
if self._bt_closed:
|
|
830
|
+
return
|
|
831
|
+
try:
|
|
832
|
+
today = _today()
|
|
833
|
+
if today != self._cur_date or self._owner_pid != os.getpid():
|
|
834
|
+
self._rotate(today)
|
|
835
|
+
line = self.format(record)
|
|
836
|
+
getattr(self._logger, self._method_for(record.levelno))(line)
|
|
837
|
+
except Exception:
|
|
838
|
+
self.handleError(record)
|
|
839
|
+
|
|
840
|
+
def _rotate(self, today):
|
|
841
|
+
path = self._path_for(today)
|
|
842
|
+
if self._owner_pid == os.getpid():
|
|
843
|
+
self._logger.flush()
|
|
844
|
+
self._mod.drop(self._unique)
|
|
845
|
+
self._unique = f"bt.{self._level_name}.{os.getpid()}.{id(self):x}"
|
|
846
|
+
self._logger = self._open_logger(path)
|
|
847
|
+
self._path = path
|
|
848
|
+
self._cur_date = today
|
|
849
|
+
self._owner_pid = os.getpid()
|
|
850
|
+
_cleanup_retention(self._script_dir, self._retention_days)
|
|
851
|
+
|
|
852
|
+
def flush(self):
|
|
853
|
+
self.acquire()
|
|
854
|
+
try:
|
|
855
|
+
if getattr(self, "_owner_pid", None) != os.getpid() or self._bt_closed:
|
|
856
|
+
return
|
|
857
|
+
try:
|
|
858
|
+
self._logger.flush()
|
|
859
|
+
except Exception: # nosec B110
|
|
860
|
+
# Best-effort cleanup only.
|
|
861
|
+
pass
|
|
862
|
+
finally:
|
|
863
|
+
self.release()
|
|
864
|
+
|
|
865
|
+
def close(self):
|
|
866
|
+
self.acquire()
|
|
867
|
+
try:
|
|
868
|
+
if getattr(self, "_owner_pid", None) == os.getpid() and not self._bt_closed:
|
|
869
|
+
try:
|
|
870
|
+
self._logger.flush()
|
|
871
|
+
self._mod.drop(self._unique)
|
|
872
|
+
except Exception: # nosec B110
|
|
873
|
+
# Best-effort cleanup only.
|
|
874
|
+
pass
|
|
875
|
+
self._bt_closed = True
|
|
876
|
+
super().close()
|
|
877
|
+
finally:
|
|
878
|
+
self.release()
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def flush_all():
|
|
882
|
+
"""Flush every managed handler (use before reading log files in tests)."""
|
|
883
|
+
for handler in tuple(logging.getLogger(ROOT_LOGGER_NAME).handlers):
|
|
884
|
+
if not getattr(handler, _BT_HANDLER_FLAG, False):
|
|
885
|
+
continue
|
|
886
|
+
try:
|
|
887
|
+
handler.flush()
|
|
888
|
+
except Exception: # nosec B110
|
|
889
|
+
# Best-effort cleanup only.
|
|
890
|
+
pass
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
# ---------------------------------------------------------------------------
|
|
894
|
+
# Throttled storm suppression (FR29-05): first occurrence logs in full, then
|
|
895
|
+
# repeats are counted and only summarized every ``every`` events or ``window``
|
|
896
|
+
# seconds; a final summary is emitted at interpreter exit.
|
|
897
|
+
# ---------------------------------------------------------------------------
|
|
898
|
+
|
|
899
|
+
_throttle_state: dict = {}
|
|
900
|
+
_throttle_enabled = True
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def set_throttle(enabled):
|
|
904
|
+
"""Enable/disable storm suppression (diagnostic mode: disable)."""
|
|
905
|
+
global _throttle_enabled
|
|
906
|
+
_throttle_enabled = enabled
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _throttled(func, logger, key, msg, *args, every=100, window=60.0, exc_info=True):
|
|
910
|
+
level = logging.WARNING if func is logging.Logger.warning else logging.ERROR
|
|
911
|
+
if not _is_output_enabled_for(level, logger=logger):
|
|
912
|
+
return
|
|
913
|
+
if not _throttle_enabled:
|
|
914
|
+
func(logger, msg, *args, exc_info=exc_info)
|
|
915
|
+
return
|
|
916
|
+
exception = exc_info if isinstance(exc_info, tuple) else sys.exc_info() if exc_info else None
|
|
917
|
+
exception_type = exception[0] if exception else None
|
|
918
|
+
state_key = (logger.name, key, level, exception_type)
|
|
919
|
+
now = time.monotonic()
|
|
920
|
+
first = False
|
|
921
|
+
summary = None
|
|
922
|
+
with _THROTTLE_LOCK:
|
|
923
|
+
state = _throttle_state.get(state_key)
|
|
924
|
+
if state is None:
|
|
925
|
+
_throttle_state[state_key] = {"count": 0, "total": 1, "last": now}
|
|
926
|
+
first = True
|
|
927
|
+
else:
|
|
928
|
+
state["count"] += 1
|
|
929
|
+
state["total"] += 1
|
|
930
|
+
if state["count"] >= every or (now - state["last"]) >= window:
|
|
931
|
+
summary = (state["count"], state["total"])
|
|
932
|
+
state["count"] = 0
|
|
933
|
+
state["last"] = now
|
|
934
|
+
if first:
|
|
935
|
+
func(logger, msg, *args, exc_info=exc_info)
|
|
936
|
+
elif summary is not None:
|
|
937
|
+
func(
|
|
938
|
+
logger,
|
|
939
|
+
"previous %s repeated %d more times (key=%s, total=%d)",
|
|
940
|
+
msg.splitlines()[0] if msg else msg,
|
|
941
|
+
summary[0],
|
|
942
|
+
key,
|
|
943
|
+
summary[1],
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def throttled_error(logger, key, msg, *args, every=100, window=60.0, exc_info=True):
|
|
948
|
+
"""logger.error with storm suppression: first full, repeats summarized."""
|
|
949
|
+
_throttled(
|
|
950
|
+
logging.Logger.error, logger, key, msg, *args, every=every, window=window, exc_info=exc_info
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def throttled_warning(logger, key, msg, *args, every=100, window=60.0, exc_info=True):
|
|
955
|
+
"""logger.warning with storm suppression (same contract as error)."""
|
|
956
|
+
_throttled(
|
|
957
|
+
logging.Logger.warning,
|
|
958
|
+
logger,
|
|
959
|
+
key,
|
|
960
|
+
msg,
|
|
961
|
+
*args,
|
|
962
|
+
every=every,
|
|
963
|
+
window=window,
|
|
964
|
+
exc_info=exc_info,
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def flush_throttle_summary():
|
|
969
|
+
"""Emit one summary line per key with unreported repeats (atexit hook)."""
|
|
970
|
+
with _THROTTLE_LOCK:
|
|
971
|
+
pending = list(_throttle_state.items())
|
|
972
|
+
_throttle_state.clear()
|
|
973
|
+
for (logger_name, key, level, _exception_type), state in pending:
|
|
974
|
+
if state["count"] > 0:
|
|
975
|
+
logging.getLogger(logger_name).log(
|
|
976
|
+
level,
|
|
977
|
+
"suppressed %d repeat occurrence(s) of key=%s (total=%d)",
|
|
978
|
+
state["count"],
|
|
979
|
+
key,
|
|
980
|
+
state["total"],
|
|
981
|
+
)
|
|
982
|
+
flush_all()
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _after_fork():
|
|
986
|
+
"""Do not inherit another thread's configuration/throttle locks or counts."""
|
|
987
|
+
global _CONFIG_LOCK, _THROTTLE_LOCK
|
|
988
|
+
_CONFIG_LOCK = threading.RLock()
|
|
989
|
+
_THROTTLE_LOCK = threading.RLock()
|
|
990
|
+
_throttle_state.clear()
|
|
991
|
+
_warned_failures.clear()
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
if hasattr(os, "register_at_fork"):
|
|
995
|
+
os.register_at_fork(after_in_child=_after_fork)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
atexit.register(flush_throttle_summary)
|