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,620 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Performance metrics calculator.
|
|
4
|
+
|
|
5
|
+
Extracts and calculates all performance metrics from strategies and analyzers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
from ..utils.log_message import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PerformanceCalculator:
|
|
16
|
+
"""Unified performance metrics calculator.
|
|
17
|
+
|
|
18
|
+
Extracts and calculates all performance metrics from strategies and analyzers, including:
|
|
19
|
+
- PnL metrics: total return, annual return, cumulative return
|
|
20
|
+
- Risk metrics: max drawdown, Sharpe ratio, SQN, Calmar ratio
|
|
21
|
+
- Trade statistics: win rate, profit/loss ratio, average profit/loss
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
strategy: Strategy instance
|
|
25
|
+
|
|
26
|
+
Usage example:
|
|
27
|
+
calc = PerformanceCalculator(strategy)
|
|
28
|
+
metrics = calc.get_all_metrics()
|
|
29
|
+
print(f"Sharpe ratio: {metrics['sharpe_ratio']}")
|
|
30
|
+
print(f"SQN rating: {metrics['sqn_human']}")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, strategy):
|
|
34
|
+
"""Initialize the performance calculator.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
strategy: backtrader strategy instance (result from run())
|
|
38
|
+
"""
|
|
39
|
+
self.strategy = strategy
|
|
40
|
+
self._analyzers = getattr(strategy, "analyzers", None)
|
|
41
|
+
self._broker = getattr(strategy, "broker", None)
|
|
42
|
+
|
|
43
|
+
def get_all_metrics(self):
|
|
44
|
+
"""Return dictionary of all performance metrics.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
dict: Dictionary containing all performance metrics
|
|
48
|
+
"""
|
|
49
|
+
metrics = {}
|
|
50
|
+
pnl_metrics = self.get_pnl_metrics()
|
|
51
|
+
metrics.update(pnl_metrics)
|
|
52
|
+
metrics.update(self.get_risk_metrics(pnl_metrics=pnl_metrics))
|
|
53
|
+
metrics.update(self.get_trade_metrics())
|
|
54
|
+
metrics.update(self.get_kpi_metrics())
|
|
55
|
+
return metrics
|
|
56
|
+
|
|
57
|
+
def get_pnl_metrics(self):
|
|
58
|
+
"""Get profit and loss related metrics.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
dict: PnL metrics dictionary
|
|
62
|
+
"""
|
|
63
|
+
metrics = {
|
|
64
|
+
"start_cash": self._get_start_cash(),
|
|
65
|
+
"end_value": self._get_end_value(),
|
|
66
|
+
"rpl": None, # Realized profit/loss
|
|
67
|
+
"total_return": None, # Total return %
|
|
68
|
+
"annual_return": None, # Annual return %
|
|
69
|
+
"result_won_trades": None,
|
|
70
|
+
"result_lost_trades": None,
|
|
71
|
+
"profit_factor": None,
|
|
72
|
+
"rpl_per_trade": None,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Calculate basic returns
|
|
76
|
+
start_cash = metrics["start_cash"]
|
|
77
|
+
end_value = metrics["end_value"]
|
|
78
|
+
|
|
79
|
+
if start_cash is not None and end_value is not None:
|
|
80
|
+
if math.isfinite(start_cash) and math.isfinite(end_value):
|
|
81
|
+
metrics["rpl"] = end_value - start_cash
|
|
82
|
+
if start_cash != 0:
|
|
83
|
+
metrics["total_return"] = 100 * (end_value / start_cash - 1)
|
|
84
|
+
else:
|
|
85
|
+
logger.debug(
|
|
86
|
+
"Skipping basic PnL metric calculation for invalid broker values: start_cash=%s, end_value=%s",
|
|
87
|
+
start_cash,
|
|
88
|
+
end_value,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Get trade statistics from TradeAnalyzer
|
|
92
|
+
trade_analysis = self._get_analyzer_result("tradeanalyzer")
|
|
93
|
+
if trade_analysis:
|
|
94
|
+
self._apply_trade_metrics(metrics, trade_analysis)
|
|
95
|
+
|
|
96
|
+
# Calculate annual return
|
|
97
|
+
bt_period_days = self._get_backtest_days()
|
|
98
|
+
if bt_period_days and bt_period_days > 0 and metrics["total_return"] is not None:
|
|
99
|
+
total_return_decimal = metrics["total_return"] / 100
|
|
100
|
+
compound_ratio = 1 + total_return_decimal
|
|
101
|
+
if math.isfinite(compound_ratio) and compound_ratio > 0:
|
|
102
|
+
metrics["annual_return"] = 100 * (compound_ratio ** (365.25 / bt_period_days) - 1)
|
|
103
|
+
else:
|
|
104
|
+
logger.debug(
|
|
105
|
+
"Skipping annual_return calculation for invalid compound ratio: %s",
|
|
106
|
+
compound_ratio,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return metrics
|
|
110
|
+
|
|
111
|
+
def _apply_trade_metrics(self, metrics, trade_analysis):
|
|
112
|
+
"""Fill rpl / won-lost / profit_factor / rpl_per_trade into ``metrics``
|
|
113
|
+
from a TradeAnalyzer result dict. Extracted from get_pnl_metrics."""
|
|
114
|
+
pnl = trade_analysis.get("pnl", {})
|
|
115
|
+
net = pnl.get("net", {})
|
|
116
|
+
|
|
117
|
+
if "total" in net:
|
|
118
|
+
metrics["rpl"] = net["total"]
|
|
119
|
+
|
|
120
|
+
won = trade_analysis.get("won", {})
|
|
121
|
+
lost = trade_analysis.get("lost", {})
|
|
122
|
+
|
|
123
|
+
won_pnl = won.get("pnl", {})
|
|
124
|
+
lost_pnl = lost.get("pnl", {})
|
|
125
|
+
|
|
126
|
+
metrics["result_won_trades"] = won_pnl.get("total")
|
|
127
|
+
metrics["result_lost_trades"] = lost_pnl.get("total")
|
|
128
|
+
|
|
129
|
+
# Calculate profit factor
|
|
130
|
+
result_won_trades = metrics["result_won_trades"]
|
|
131
|
+
result_lost_trades = metrics["result_lost_trades"]
|
|
132
|
+
if all(
|
|
133
|
+
isinstance(value, (int, float)) and math.isfinite(value)
|
|
134
|
+
for value in (result_won_trades, result_lost_trades)
|
|
135
|
+
):
|
|
136
|
+
if result_lost_trades != 0:
|
|
137
|
+
metrics["profit_factor"] = abs(result_won_trades / result_lost_trades)
|
|
138
|
+
elif result_won_trades is not None or result_lost_trades is not None:
|
|
139
|
+
logger.debug(
|
|
140
|
+
"Skipping profit_factor calculation for invalid trade PnL totals: won=%s, lost=%s",
|
|
141
|
+
result_won_trades,
|
|
142
|
+
result_lost_trades,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Average profit/loss per trade
|
|
146
|
+
total = trade_analysis.get("total", {})
|
|
147
|
+
closed = total.get("closed", 0)
|
|
148
|
+
if (
|
|
149
|
+
isinstance(closed, (int, float))
|
|
150
|
+
and math.isfinite(closed)
|
|
151
|
+
and closed > 0
|
|
152
|
+
and isinstance(metrics["rpl"], (int, float))
|
|
153
|
+
and math.isfinite(metrics["rpl"])
|
|
154
|
+
):
|
|
155
|
+
metrics["rpl_per_trade"] = metrics["rpl"] / closed
|
|
156
|
+
elif closed not in (0, None) or metrics["rpl"] is not None:
|
|
157
|
+
logger.debug(
|
|
158
|
+
"Skipping rpl_per_trade calculation for invalid inputs: closed=%s, rpl=%s",
|
|
159
|
+
closed,
|
|
160
|
+
metrics["rpl"],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def get_risk_metrics(self, pnl_metrics=None):
|
|
164
|
+
"""Get risk-related metrics.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
pnl_metrics: Pre-computed PnL metrics dict (avoids recomputation)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
dict: Risk metrics dictionary
|
|
171
|
+
"""
|
|
172
|
+
metrics = {
|
|
173
|
+
"max_money_drawdown": None,
|
|
174
|
+
"max_pct_drawdown": None,
|
|
175
|
+
"calmar_ratio": None,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Get from DrawDown analyzer
|
|
179
|
+
drawdown = self._get_analyzer_result("drawdown")
|
|
180
|
+
if drawdown:
|
|
181
|
+
max_dd = drawdown.get("max", {})
|
|
182
|
+
metrics["max_money_drawdown"] = max_dd.get("moneydown")
|
|
183
|
+
metrics["max_pct_drawdown"] = max_dd.get("drawdown")
|
|
184
|
+
|
|
185
|
+
# Calculate Calmar ratio
|
|
186
|
+
if pnl_metrics is None:
|
|
187
|
+
pnl_metrics = self.get_pnl_metrics()
|
|
188
|
+
annual_return = pnl_metrics.get("annual_return")
|
|
189
|
+
max_pct_drawdown = metrics.get("max_pct_drawdown")
|
|
190
|
+
if annual_return is not None and max_pct_drawdown is not None:
|
|
191
|
+
if (
|
|
192
|
+
math.isfinite(annual_return)
|
|
193
|
+
and math.isfinite(max_pct_drawdown)
|
|
194
|
+
and max_pct_drawdown > 0
|
|
195
|
+
):
|
|
196
|
+
metrics["calmar_ratio"] = abs(annual_return / max_pct_drawdown)
|
|
197
|
+
else:
|
|
198
|
+
logger.debug(
|
|
199
|
+
"Skipping calmar_ratio calculation for invalid inputs: annual_return=%s, max_pct_drawdown=%s",
|
|
200
|
+
annual_return,
|
|
201
|
+
max_pct_drawdown,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return metrics
|
|
205
|
+
|
|
206
|
+
def get_trade_metrics(self):
|
|
207
|
+
"""Get trade statistics metrics.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
dict: Trade statistics dictionary
|
|
211
|
+
"""
|
|
212
|
+
metrics = {
|
|
213
|
+
"total_number_trades": 0,
|
|
214
|
+
"trades_closed": 0,
|
|
215
|
+
"trades_won": 0,
|
|
216
|
+
"trades_lost": 0,
|
|
217
|
+
"pct_winning": None,
|
|
218
|
+
"pct_losing": None,
|
|
219
|
+
"avg_money_winning": None,
|
|
220
|
+
"avg_money_losing": None,
|
|
221
|
+
"best_winning_trade": None,
|
|
222
|
+
"worst_losing_trade": None,
|
|
223
|
+
"avg_trade_duration": None,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
trade_analysis = self._get_analyzer_result("tradeanalyzer")
|
|
227
|
+
if trade_analysis:
|
|
228
|
+
total = trade_analysis.get("total", {})
|
|
229
|
+
metrics["total_number_trades"] = total.get("total", 0)
|
|
230
|
+
metrics["trades_closed"] = total.get("closed", 0)
|
|
231
|
+
|
|
232
|
+
won = trade_analysis.get("won", {})
|
|
233
|
+
lost = trade_analysis.get("lost", {})
|
|
234
|
+
|
|
235
|
+
metrics["trades_won"] = won.get("total", 0)
|
|
236
|
+
metrics["trades_lost"] = lost.get("total", 0)
|
|
237
|
+
|
|
238
|
+
# Win rate
|
|
239
|
+
trades_closed = metrics["trades_closed"]
|
|
240
|
+
trades_won = metrics["trades_won"]
|
|
241
|
+
trades_lost = metrics["trades_lost"]
|
|
242
|
+
if all(
|
|
243
|
+
isinstance(value, (int, float)) and math.isfinite(value)
|
|
244
|
+
for value in (trades_closed, trades_won, trades_lost)
|
|
245
|
+
):
|
|
246
|
+
if trades_closed > 0:
|
|
247
|
+
metrics["pct_winning"] = 100 * trades_won / trades_closed
|
|
248
|
+
metrics["pct_losing"] = 100 * trades_lost / trades_closed
|
|
249
|
+
else:
|
|
250
|
+
logger.debug(
|
|
251
|
+
"Skipping trade win/loss percentage calculation for invalid counts: closed=%s, won=%s, lost=%s",
|
|
252
|
+
trades_closed,
|
|
253
|
+
trades_won,
|
|
254
|
+
trades_lost,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Average profit/loss
|
|
258
|
+
won_pnl = won.get("pnl", {})
|
|
259
|
+
lost_pnl = lost.get("pnl", {})
|
|
260
|
+
|
|
261
|
+
metrics["avg_money_winning"] = won_pnl.get("average")
|
|
262
|
+
metrics["avg_money_losing"] = lost_pnl.get("average")
|
|
263
|
+
metrics["best_winning_trade"] = won_pnl.get("max")
|
|
264
|
+
metrics["worst_losing_trade"] = lost_pnl.get("max")
|
|
265
|
+
|
|
266
|
+
# Average trade duration
|
|
267
|
+
len_info = trade_analysis.get("len", {})
|
|
268
|
+
if isinstance(len_info, dict):
|
|
269
|
+
total_len = len_info.get("total", {})
|
|
270
|
+
if isinstance(total_len, dict):
|
|
271
|
+
metrics["avg_trade_duration"] = total_len.get("average")
|
|
272
|
+
|
|
273
|
+
return metrics
|
|
274
|
+
|
|
275
|
+
def get_kpi_metrics(self):
|
|
276
|
+
"""Get key performance indicators.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
dict: KPI metrics dictionary
|
|
280
|
+
"""
|
|
281
|
+
metrics = {
|
|
282
|
+
"sharpe_ratio": None,
|
|
283
|
+
"sqn_score": None,
|
|
284
|
+
"sqn_human": None,
|
|
285
|
+
"sortino_ratio": None,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Sharpe ratio
|
|
289
|
+
sharpe = self._get_analyzer_result("sharperatio")
|
|
290
|
+
if sharpe:
|
|
291
|
+
metrics["sharpe_ratio"] = sharpe.get("sharperatio")
|
|
292
|
+
|
|
293
|
+
# SQN
|
|
294
|
+
sqn = self._get_analyzer_result("sqn")
|
|
295
|
+
if sqn:
|
|
296
|
+
sqn_score = sqn.get("sqn")
|
|
297
|
+
metrics["sqn_score"] = sqn_score
|
|
298
|
+
metrics["sqn_human"] = self.sqn_to_rating(sqn_score)
|
|
299
|
+
|
|
300
|
+
# Sortino ratio
|
|
301
|
+
sortino = self._get_analyzer_result("sortinoratio")
|
|
302
|
+
if sortino:
|
|
303
|
+
metrics["sortino_ratio"] = sortino.get("sortinoratio")
|
|
304
|
+
|
|
305
|
+
return metrics
|
|
306
|
+
|
|
307
|
+
def get_equity_curve(self):
|
|
308
|
+
"""Get equity curve data.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
tuple: (dates, values) Lists of dates and equity values
|
|
312
|
+
"""
|
|
313
|
+
dates = []
|
|
314
|
+
values = []
|
|
315
|
+
|
|
316
|
+
# Try to get from Broker observer
|
|
317
|
+
if hasattr(self.strategy, "observers"):
|
|
318
|
+
for obs in self.strategy.observers:
|
|
319
|
+
if obs.__class__.__name__ == "Broker":
|
|
320
|
+
if hasattr(obs.lines, "value"):
|
|
321
|
+
value_line = obs.lines.value
|
|
322
|
+
length = len(value_line)
|
|
323
|
+
|
|
324
|
+
# Get dates
|
|
325
|
+
if hasattr(self.strategy, "data"):
|
|
326
|
+
data = self.strategy.data
|
|
327
|
+
from ..utils.date import num2date
|
|
328
|
+
|
|
329
|
+
# Correct indexing: from 1-length to 0
|
|
330
|
+
for i in range(length):
|
|
331
|
+
idx = 1 - length + i
|
|
332
|
+
try:
|
|
333
|
+
dt_num = data.datetime[idx]
|
|
334
|
+
dates.append(num2date(dt_num))
|
|
335
|
+
values.append(value_line[idx])
|
|
336
|
+
except (AttributeError, IndexError, TypeError, ValueError) as e:
|
|
337
|
+
logger.warning(
|
|
338
|
+
"Failed to get equity data at idx %d: %s", idx, e
|
|
339
|
+
)
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
if not values:
|
|
343
|
+
# Try to get from TimeReturn analyzer and calculate cumulative equity
|
|
344
|
+
time_return = self._get_analyzer_result("timereturn")
|
|
345
|
+
if time_return:
|
|
346
|
+
start_cash = self._resolve_start_cash(self._get_start_cash())
|
|
347
|
+
cumulative_value = start_cash
|
|
348
|
+
for dt, ret in sorted(time_return.items()):
|
|
349
|
+
if not isinstance(ret, (int, float)) or not math.isfinite(ret):
|
|
350
|
+
logger.debug(
|
|
351
|
+
"Skipping invalid timereturn value in equity curve: %s at %s",
|
|
352
|
+
ret,
|
|
353
|
+
dt,
|
|
354
|
+
)
|
|
355
|
+
dates.append(dt)
|
|
356
|
+
values.append(cumulative_value)
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
cumulative_value = cumulative_value * (1 + ret)
|
|
360
|
+
dates.append(dt)
|
|
361
|
+
values.append(cumulative_value)
|
|
362
|
+
|
|
363
|
+
if not values:
|
|
364
|
+
# If still no data, calculate buy-and-hold equity curve from data source as fallback
|
|
365
|
+
benchmark_dates, benchmark_values = self.get_buynhold_curve()
|
|
366
|
+
if benchmark_dates and benchmark_values:
|
|
367
|
+
start_cash = self._resolve_start_cash(self._get_start_cash())
|
|
368
|
+
dates = benchmark_dates
|
|
369
|
+
# Convert normalized values to actual equity values
|
|
370
|
+
values = [start_cash * v / 100 for v in benchmark_values]
|
|
371
|
+
|
|
372
|
+
return dates, values
|
|
373
|
+
|
|
374
|
+
def get_buynhold_curve(self):
|
|
375
|
+
"""Get buy-and-hold comparison curve.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
tuple: (dates, values) Lists of dates and buy-and-hold values
|
|
379
|
+
"""
|
|
380
|
+
if not hasattr(self.strategy, "data"):
|
|
381
|
+
return None, None
|
|
382
|
+
|
|
383
|
+
data = self.strategy.data
|
|
384
|
+
dates = []
|
|
385
|
+
values: list = []
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
length = len(data)
|
|
389
|
+
if length == 0:
|
|
390
|
+
return None, None
|
|
391
|
+
|
|
392
|
+
# Get open price as buy-and-hold benchmark
|
|
393
|
+
first_price = None
|
|
394
|
+
|
|
395
|
+
from ..utils.date import num2date
|
|
396
|
+
|
|
397
|
+
# Correct indexing: from 1-length to 0
|
|
398
|
+
for i in range(length):
|
|
399
|
+
idx = 1 - length + i
|
|
400
|
+
try:
|
|
401
|
+
dt_num = data.datetime[idx]
|
|
402
|
+
dates.append(num2date(dt_num))
|
|
403
|
+
|
|
404
|
+
price = data.open[idx]
|
|
405
|
+
if not isinstance(price, (int, float)) or not math.isfinite(price):
|
|
406
|
+
logger.debug(
|
|
407
|
+
"Skipping invalid buy-and-hold price at idx %d: %s", idx, price
|
|
408
|
+
)
|
|
409
|
+
values.append(values[-1] if values else 100)
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
if first_price is None and price > 0:
|
|
413
|
+
first_price = price
|
|
414
|
+
|
|
415
|
+
# Normalize to 100
|
|
416
|
+
values.append(100 * price / first_price if first_price else 100)
|
|
417
|
+
except (AttributeError, IndexError, TypeError, ValueError, ZeroDivisionError) as e:
|
|
418
|
+
logger.warning("Failed to get benchmark data at idx %d: %s", idx, e)
|
|
419
|
+
except (AttributeError, IndexError, TypeError, ValueError) as e:
|
|
420
|
+
logger.warning("Failed to calculate benchmark curve: %s", e)
|
|
421
|
+
|
|
422
|
+
return dates, values
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _resolve_start_cash(value, default=100000):
|
|
426
|
+
if not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
427
|
+
return default
|
|
428
|
+
return value
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _normalize_analyzer_name(value):
|
|
432
|
+
return value.lower() if isinstance(value, str) else ""
|
|
433
|
+
|
|
434
|
+
@staticmethod
|
|
435
|
+
def sqn_to_rating(sqn_score):
|
|
436
|
+
"""Convert SQN score to human-readable rating.
|
|
437
|
+
|
|
438
|
+
Reference: http://www.vantharp.com/tharp-concepts/sqn.asp
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
sqn_score: SQN score
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
str: Human-readable rating
|
|
445
|
+
"""
|
|
446
|
+
if sqn_score is None or not isinstance(sqn_score, (int, float)):
|
|
447
|
+
return "N/A"
|
|
448
|
+
|
|
449
|
+
if math.isnan(sqn_score):
|
|
450
|
+
return "N/A"
|
|
451
|
+
|
|
452
|
+
if sqn_score < 1.6:
|
|
453
|
+
return "Poor"
|
|
454
|
+
if sqn_score < 1.9:
|
|
455
|
+
return "Below Average"
|
|
456
|
+
if sqn_score < 2.4:
|
|
457
|
+
return "Average"
|
|
458
|
+
if sqn_score < 2.9:
|
|
459
|
+
return "Good"
|
|
460
|
+
if sqn_score < 5.0:
|
|
461
|
+
return "Excellent"
|
|
462
|
+
if sqn_score < 6.9:
|
|
463
|
+
return "Superb"
|
|
464
|
+
return "Holy Grail"
|
|
465
|
+
|
|
466
|
+
def _get_start_cash(self):
|
|
467
|
+
"""Get starting cash."""
|
|
468
|
+
if self._broker is None:
|
|
469
|
+
return None
|
|
470
|
+
try:
|
|
471
|
+
return getattr(self._broker, "startingcash", None)
|
|
472
|
+
except Exception as e:
|
|
473
|
+
# Broker is a pluggable object that may raise any error type;
|
|
474
|
+
# report generation must degrade gracefully (see edge-case tests).
|
|
475
|
+
logger.warning("Failed to get starting cash: %s", e)
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
def _get_end_value(self):
|
|
479
|
+
"""Get final portfolio value."""
|
|
480
|
+
if self._broker is None:
|
|
481
|
+
return None
|
|
482
|
+
try:
|
|
483
|
+
return self._broker.getvalue()
|
|
484
|
+
except Exception as e:
|
|
485
|
+
# Broker is a pluggable object that may raise any error type;
|
|
486
|
+
# report generation must degrade gracefully (see edge-case tests).
|
|
487
|
+
logger.warning("Failed to get end value: %s", e)
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
def _get_backtest_days(self):
|
|
491
|
+
"""Get number of backtest days."""
|
|
492
|
+
if not hasattr(self.strategy, "data"):
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
data = self.strategy.data
|
|
496
|
+
try:
|
|
497
|
+
from ..utils.date import num2date
|
|
498
|
+
|
|
499
|
+
length = len(data)
|
|
500
|
+
if length < 2:
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
# Correct indexing: 0 is current (last) bar, 1-length is first bar
|
|
504
|
+
start_dt = num2date(data.datetime[1 - length])
|
|
505
|
+
end_dt = num2date(data.datetime[0])
|
|
506
|
+
|
|
507
|
+
delta = end_dt - start_dt
|
|
508
|
+
return delta.days
|
|
509
|
+
except (AttributeError, IndexError, TypeError, ValueError) as e:
|
|
510
|
+
logger.warning("Failed to calculate backtest days: %s", e)
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
def _get_analyzer_result(self, name):
|
|
514
|
+
"""Get analyzer result.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
name: Analyzer name (case-insensitive)
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
dict: Analyzer result, or None if not found
|
|
521
|
+
"""
|
|
522
|
+
if self._analyzers is None:
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
# Try to get directly by name
|
|
526
|
+
name_lower = name.lower()
|
|
527
|
+
|
|
528
|
+
# First pass: exact match on class name or _name attribute
|
|
529
|
+
for analyzer in self._analyzers:
|
|
530
|
+
analyzer_name = analyzer.__class__.__name__.lower()
|
|
531
|
+
custom_name = self._normalize_analyzer_name(getattr(analyzer, "_name", ""))
|
|
532
|
+
|
|
533
|
+
if analyzer_name == name_lower or custom_name == name_lower:
|
|
534
|
+
try:
|
|
535
|
+
return analyzer.get_analysis()
|
|
536
|
+
except (AttributeError, KeyError, TypeError, ValueError, IndexError) as e:
|
|
537
|
+
logger.warning("Failed to get analysis from %s: %s", analyzer_name, e)
|
|
538
|
+
|
|
539
|
+
# Second pass: substring match (less precise, used as fallback)
|
|
540
|
+
for analyzer in self._analyzers:
|
|
541
|
+
analyzer_name = analyzer.__class__.__name__.lower()
|
|
542
|
+
custom_name = self._normalize_analyzer_name(getattr(analyzer, "_name", ""))
|
|
543
|
+
|
|
544
|
+
if name_lower in analyzer_name or name_lower in custom_name:
|
|
545
|
+
try:
|
|
546
|
+
return analyzer.get_analysis()
|
|
547
|
+
except (AttributeError, KeyError, TypeError, ValueError, IndexError) as e:
|
|
548
|
+
logger.warning("Failed to get analysis from %s: %s", analyzer_name, e)
|
|
549
|
+
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
def get_strategy_info(self):
|
|
553
|
+
"""Get strategy information.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
dict: Strategy information dictionary
|
|
557
|
+
"""
|
|
558
|
+
info = {
|
|
559
|
+
"strategy_name": self.strategy.__class__.__name__,
|
|
560
|
+
"params": {},
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
# Get strategy parameters
|
|
564
|
+
if hasattr(self.strategy, "params"):
|
|
565
|
+
params = self.strategy.params
|
|
566
|
+
try:
|
|
567
|
+
param_items = params.items()
|
|
568
|
+
except (AttributeError, TypeError) as e:
|
|
569
|
+
logger.warning("Failed to enumerate strategy parameters: %s", e)
|
|
570
|
+
param_items = None
|
|
571
|
+
|
|
572
|
+
if param_items is not None:
|
|
573
|
+
info["params"] = {name: value for name, value in param_items if not callable(value)}
|
|
574
|
+
else:
|
|
575
|
+
for name in dir(params):
|
|
576
|
+
if not name.startswith("_"):
|
|
577
|
+
try:
|
|
578
|
+
value = getattr(params, name)
|
|
579
|
+
if not callable(value):
|
|
580
|
+
info["params"][name] = value
|
|
581
|
+
except (AttributeError, TypeError) as e:
|
|
582
|
+
logger.warning("Failed to get param '%s': %s", name, e)
|
|
583
|
+
|
|
584
|
+
return info
|
|
585
|
+
|
|
586
|
+
def get_data_info(self):
|
|
587
|
+
"""Get data information.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
dict: Data information dictionary
|
|
591
|
+
"""
|
|
592
|
+
info = {
|
|
593
|
+
"data_name": None,
|
|
594
|
+
"start_date": None,
|
|
595
|
+
"end_date": None,
|
|
596
|
+
"bars": 0,
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if not hasattr(self.strategy, "data"):
|
|
600
|
+
return info
|
|
601
|
+
|
|
602
|
+
data = self.strategy.data
|
|
603
|
+
|
|
604
|
+
# Data name
|
|
605
|
+
info["data_name"] = getattr(data, "_name", None) or "Data"
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
from ..utils.date import num2date
|
|
609
|
+
|
|
610
|
+
length = len(data)
|
|
611
|
+
info["bars"] = length
|
|
612
|
+
|
|
613
|
+
if length > 0:
|
|
614
|
+
# Correct indexing: 0 is current (last) bar, 1-length is first bar
|
|
615
|
+
info["start_date"] = num2date(data.datetime[1 - length])
|
|
616
|
+
info["end_date"] = num2date(data.datetime[0])
|
|
617
|
+
except (AttributeError, IndexError, TypeError, ValueError) as e:
|
|
618
|
+
logger.warning("Failed to get data info: %s", e)
|
|
619
|
+
|
|
620
|
+
return info
|