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,1351 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Plotly-based plotting for backtrader.
|
|
4
|
+
|
|
5
|
+
This module provides high-performance interactive charts using Plotly,
|
|
6
|
+
which handles large datasets much better than matplotlib.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import bisect
|
|
10
|
+
import collections
|
|
11
|
+
import datetime
|
|
12
|
+
import math
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import plotly.graph_objects as go
|
|
16
|
+
from plotly.subplots import make_subplots
|
|
17
|
+
|
|
18
|
+
from ..parameters import ParameterDescriptor, ParameterizedBase
|
|
19
|
+
from ..utils.date import num2date
|
|
20
|
+
from ..utils.log_message import get_logger
|
|
21
|
+
from ..utils.py3 import range
|
|
22
|
+
from .scheme import PlotScheme
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
# Tableau color schemes
|
|
27
|
+
TABLEAU10 = [
|
|
28
|
+
"blue",
|
|
29
|
+
"darkorange",
|
|
30
|
+
"green",
|
|
31
|
+
"crimson",
|
|
32
|
+
"mediumpurple",
|
|
33
|
+
"saddlebrown",
|
|
34
|
+
"orchid",
|
|
35
|
+
"gray",
|
|
36
|
+
"olive",
|
|
37
|
+
"mediumturquoise",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
TABLEAU20 = [
|
|
41
|
+
"steelblue",
|
|
42
|
+
"lightsteelblue",
|
|
43
|
+
"darkorange",
|
|
44
|
+
"peachpuff",
|
|
45
|
+
"green",
|
|
46
|
+
"lightgreen",
|
|
47
|
+
"crimson",
|
|
48
|
+
"lightcoral",
|
|
49
|
+
"mediumpurple",
|
|
50
|
+
"thistle",
|
|
51
|
+
"saddlebrown",
|
|
52
|
+
"rosybrown",
|
|
53
|
+
"orchid",
|
|
54
|
+
"lightpink",
|
|
55
|
+
"gray",
|
|
56
|
+
"lightgray",
|
|
57
|
+
"olive",
|
|
58
|
+
"palegoldenrod",
|
|
59
|
+
"mediumturquoise",
|
|
60
|
+
"paleturquoise",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
TABLEAU10_LIGHT = [
|
|
64
|
+
"lightsteelblue",
|
|
65
|
+
"peachpuff",
|
|
66
|
+
"lightgreen",
|
|
67
|
+
"lightcoral",
|
|
68
|
+
"thistle",
|
|
69
|
+
"rosybrown",
|
|
70
|
+
"lightpink",
|
|
71
|
+
"lightgray",
|
|
72
|
+
"palegoldenrod",
|
|
73
|
+
"paleturquoise",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Color index mapping for optimized visual order
|
|
77
|
+
TAB10_INDEX = [3, 0, 2, 1, 2, 4, 5, 6, 7, 8, 9]
|
|
78
|
+
|
|
79
|
+
# Color mapper from matplotlib to plotly
|
|
80
|
+
COLOR_MAPPER = {
|
|
81
|
+
"b": "rgb(0, 0, 255)",
|
|
82
|
+
"blue": "rgb(0, 0, 255)",
|
|
83
|
+
"g": "rgb(0, 128, 0)",
|
|
84
|
+
"green": "rgb(0, 128, 0)",
|
|
85
|
+
"r": "rgb(255, 0, 0)",
|
|
86
|
+
"red": "rgb(255, 0, 0)",
|
|
87
|
+
"c": "rgb(0, 255, 255)",
|
|
88
|
+
"cyan": "rgb(0, 255, 255)",
|
|
89
|
+
"m": "rgb(255, 0, 255)",
|
|
90
|
+
"magenta": "rgb(255, 0, 255)",
|
|
91
|
+
"y": "rgb(255, 255, 0)",
|
|
92
|
+
"yellow": "rgb(255, 255, 0)",
|
|
93
|
+
"k": "rgb(0, 0, 0)",
|
|
94
|
+
"black": "rgb(0, 0, 0)",
|
|
95
|
+
"w": "rgb(255, 255, 255)",
|
|
96
|
+
"white": "rgb(255, 255, 255)",
|
|
97
|
+
"steelblue": "rgb(70, 130, 180)",
|
|
98
|
+
"darkorange": "rgb(255, 140, 0)",
|
|
99
|
+
"crimson": "rgb(220, 20, 60)",
|
|
100
|
+
"mediumpurple": "rgb(147, 112, 219)",
|
|
101
|
+
"saddlebrown": "rgb(139, 69, 19)",
|
|
102
|
+
"orchid": "rgb(218, 112, 214)",
|
|
103
|
+
"olive": "rgb(128, 128, 0)",
|
|
104
|
+
"mediumturquoise": "rgb(72, 209, 204)",
|
|
105
|
+
"lightsteelblue": "rgb(176, 196, 222)",
|
|
106
|
+
"peachpuff": "rgb(255, 218, 185)",
|
|
107
|
+
"lightgreen": "rgb(144, 238, 144)",
|
|
108
|
+
"lightcoral": "rgb(240, 128, 128)",
|
|
109
|
+
"thistle": "rgb(216, 191, 216)",
|
|
110
|
+
"rosybrown": "rgb(188, 143, 143)",
|
|
111
|
+
"lightpink": "rgb(255, 182, 193)",
|
|
112
|
+
"lightgray": "rgb(211, 211, 211)",
|
|
113
|
+
"palegoldenrod": "rgb(238, 232, 170)",
|
|
114
|
+
"paleturquoise": "rgb(175, 238, 238)",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_color_scheme(name="tableau10"):
|
|
119
|
+
"""Get color scheme by name.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
name: Color scheme name ('tableau10', 'tableau20', 'tableau10_light')
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
list: Color list
|
|
126
|
+
"""
|
|
127
|
+
schemes = {
|
|
128
|
+
"tableau10": TABLEAU10,
|
|
129
|
+
"tableau20": TABLEAU20,
|
|
130
|
+
"tableau10_light": TABLEAU10_LIGHT,
|
|
131
|
+
}
|
|
132
|
+
return schemes.get(name, TABLEAU10)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def wrap_legend_text(text, max_width=16):
|
|
136
|
+
"""Wrap legend text with automatic line breaks.
|
|
137
|
+
|
|
138
|
+
Reference: backtrader_plotly/plotter.py:695-702
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
text: Original text
|
|
142
|
+
max_width: Maximum character width per line
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
str: Processed text with <br> separators for long lines
|
|
146
|
+
"""
|
|
147
|
+
if text is None:
|
|
148
|
+
return ""
|
|
149
|
+
text = str(text)
|
|
150
|
+
|
|
151
|
+
# Remove existing newlines
|
|
152
|
+
text = text.replace("\n", "")
|
|
153
|
+
|
|
154
|
+
if len(text) <= max_width:
|
|
155
|
+
return text
|
|
156
|
+
|
|
157
|
+
# Split by max_width
|
|
158
|
+
return "<br>".join(text[i : i + max_width] for i in range(0, len(text), max_width))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class PlotlyScheme(PlotScheme):
|
|
162
|
+
"""Extended scheme for Plotly plotting with optimized colors.
|
|
163
|
+
|
|
164
|
+
Extends PlotScheme with Plotly-specific settings for interactive charts,
|
|
165
|
+
including theme selection, range slider configuration, and optimized
|
|
166
|
+
color schemes for better visual presentation in web-based plots.
|
|
167
|
+
|
|
168
|
+
Attributes:
|
|
169
|
+
plotly_theme (str): Plotly theme name (e.g., 'plotly_white').
|
|
170
|
+
rangeslider (bool): Whether to show range slider for navigation.
|
|
171
|
+
rangeslider_preview (bool): Whether to show preview in range slider.
|
|
172
|
+
height_ratios (list): Height ratios for subplots [price, volume, indicator].
|
|
173
|
+
barup (str): Color for bullish bars (default: red for Chinese market).
|
|
174
|
+
barupfill (bool): Whether bullish candles are filled.
|
|
175
|
+
buymarker_color (str): Color for buy markers.
|
|
176
|
+
buymarker_size (int): Size of buy markers.
|
|
177
|
+
sellmarker_color (str): Color for sell markers.
|
|
178
|
+
sellmarker_size (int): Size of sell markers.
|
|
179
|
+
equity_color (str): Color for equity curve.
|
|
180
|
+
decimal_places (int): Number of decimal places for price display.
|
|
181
|
+
max_legend_text_width (int): Maximum legend text width before wrapping.
|
|
182
|
+
color_scheme (str): Color scheme name ('tableau10', 'tableau20', 'tableau10_light').
|
|
183
|
+
fillalpha (float): Fill area transparency (0-1).
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, **kwargs):
|
|
187
|
+
"""Initialize PlotlyScheme with Plotly-specific defaults.
|
|
188
|
+
|
|
189
|
+
Sets up optimized color schemes and plotting configurations for
|
|
190
|
+
interactive Plotly charts, including Chinese market color conventions
|
|
191
|
+
(red for up, green for down).
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
**kwargs: Optional keyword arguments to override defaults.
|
|
195
|
+
- decimal_places (int): Price decimal places (default: 5)
|
|
196
|
+
- max_legend_text_width (int): Legend text width (default: 16)
|
|
197
|
+
- color_scheme (str): Color scheme name (default: 'tableau10')
|
|
198
|
+
- fillalpha (float): Fill transparency (default: 0.20)
|
|
199
|
+
"""
|
|
200
|
+
super().__init__()
|
|
201
|
+
# Plotly specific settings
|
|
202
|
+
self.plotly_theme = "plotly_white"
|
|
203
|
+
self.rangeslider = True
|
|
204
|
+
self.rangeslider_preview = False
|
|
205
|
+
self.height_ratios = [3, 1, 1] # price, volume, indicator
|
|
206
|
+
|
|
207
|
+
# Optimized color scheme (Chinese market: red up, green down)
|
|
208
|
+
self.barup = "#E74C3C" # Red for bullish
|
|
209
|
+
self.bardown = "#27AE60" # Green for bearish
|
|
210
|
+
self.barupfill = True
|
|
211
|
+
self.bardownfill = True
|
|
212
|
+
|
|
213
|
+
# Volume colors
|
|
214
|
+
self.volup = "rgba(231, 76, 60, 0.5)" # Red transparent
|
|
215
|
+
self.voldown = "rgba(39, 174, 96, 0.5)" # Green transparent
|
|
216
|
+
|
|
217
|
+
# Line colors for indicators (legacy, will use color_scheme)
|
|
218
|
+
self.linecolors = [
|
|
219
|
+
"#3498DB", # Blue
|
|
220
|
+
"#E67E22", # Orange
|
|
221
|
+
"#9B59B6", # Purple
|
|
222
|
+
"#1ABC9C", # Teal
|
|
223
|
+
"#F39C12", # Yellow
|
|
224
|
+
"#E91E63", # Pink
|
|
225
|
+
"#00BCD4", # Cyan
|
|
226
|
+
"#FF5722", # Deep Orange
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
# Buy/Sell marker colors
|
|
230
|
+
self.buymarker_color = "#E74C3C" # Red
|
|
231
|
+
self.sellmarker_color = "#27AE60" # Green
|
|
232
|
+
self.buymarker_size = 12
|
|
233
|
+
self.sellmarker_size = 12
|
|
234
|
+
|
|
235
|
+
# Equity curve
|
|
236
|
+
self.equity_color = "#3498DB" # Blue
|
|
237
|
+
|
|
238
|
+
# New parameters from backtrader_plotly
|
|
239
|
+
# Decimal places for price display
|
|
240
|
+
self.decimal_places = kwargs.get("decimal_places", 5)
|
|
241
|
+
|
|
242
|
+
# Maximum legend text width before wrapping
|
|
243
|
+
self.max_legend_text_width = kwargs.get("max_legend_text_width", 16)
|
|
244
|
+
|
|
245
|
+
# Color scheme selection
|
|
246
|
+
self.color_scheme = kwargs.get("color_scheme", "tableau10")
|
|
247
|
+
|
|
248
|
+
# Fill area transparency
|
|
249
|
+
self.fillalpha = kwargs.get("fillalpha", 0.20)
|
|
250
|
+
|
|
251
|
+
# Tableau color schemes
|
|
252
|
+
self.tableau10 = TABLEAU10
|
|
253
|
+
self.tableau20 = TABLEAU20
|
|
254
|
+
self.tableau10_light = TABLEAU10_LIGHT
|
|
255
|
+
|
|
256
|
+
# Color index mapping for optimized visual order
|
|
257
|
+
self.tab10_index = TAB10_INDEX
|
|
258
|
+
|
|
259
|
+
def get_colors(self):
|
|
260
|
+
"""Get current color scheme colors.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
list: Color list based on current color_scheme setting
|
|
264
|
+
"""
|
|
265
|
+
return getattr(self, self.color_scheme, self.tableau10)
|
|
266
|
+
|
|
267
|
+
def color(self, idx):
|
|
268
|
+
"""Get color for given index using tab10_index mapping.
|
|
269
|
+
|
|
270
|
+
Uses tab10_index mapping to optimize visual order of colors.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
idx: Color index
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
str: Color name or value
|
|
277
|
+
"""
|
|
278
|
+
colors = self.get_colors()
|
|
279
|
+
colidx = self.tab10_index[idx % len(self.tab10_index)]
|
|
280
|
+
return colors[colidx % len(colors)]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class PlotlyPlot(ParameterizedBase):
|
|
284
|
+
"""
|
|
285
|
+
Plotly-based plotter for backtrader strategies.
|
|
286
|
+
|
|
287
|
+
Provides interactive charts with:
|
|
288
|
+
- Candlestick/OHLC/Line charts
|
|
289
|
+
- Volume bars
|
|
290
|
+
- Indicator subplots
|
|
291
|
+
- Buy/Sell markers
|
|
292
|
+
- Range slider for navigation
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
scheme = ParameterDescriptor(default=PlotlyScheme(), doc="Plotting scheme")
|
|
296
|
+
|
|
297
|
+
def __init__(self, **kwargs):
|
|
298
|
+
"""Initialize PlotlyPlot with optional scheme overrides.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
**kwargs: Optional keyword arguments to override scheme parameters.
|
|
302
|
+
Any parameter name matching a PlotlyScheme attribute will
|
|
303
|
+
update that attribute in the scheme.
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
>>> plotter = PlotlyPlot(style='candle', volume=True)
|
|
307
|
+
"""
|
|
308
|
+
super().__init__()
|
|
309
|
+
for pname, pvalue in kwargs.items():
|
|
310
|
+
if hasattr(self.p.scheme, pname):
|
|
311
|
+
setattr(self.p.scheme, pname, pvalue)
|
|
312
|
+
|
|
313
|
+
self.figs = []
|
|
314
|
+
self.data_cache = {}
|
|
315
|
+
self.buysell_markers = [] # Store buy/sell signals
|
|
316
|
+
|
|
317
|
+
def _format_value(self, value):
|
|
318
|
+
"""Format numeric value with configured decimal places.
|
|
319
|
+
|
|
320
|
+
Uses scheme.decimal_places to control precision.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
value: Numeric value to format
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
str: Formatted value string
|
|
327
|
+
"""
|
|
328
|
+
decimal_places = getattr(self.p.scheme, "decimal_places", 5)
|
|
329
|
+
try:
|
|
330
|
+
return f"{float(value):.{decimal_places}f}"
|
|
331
|
+
except (ValueError, TypeError):
|
|
332
|
+
return str(value)
|
|
333
|
+
|
|
334
|
+
def _get_tick_format(self):
|
|
335
|
+
"""Get y-axis tick format string.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
str: Format string for axis ticks (e.g., '.5f')
|
|
339
|
+
"""
|
|
340
|
+
decimal_places = getattr(self.p.scheme, "decimal_places", 5)
|
|
341
|
+
return f".{decimal_places}f"
|
|
342
|
+
|
|
343
|
+
def _format_label(self, label):
|
|
344
|
+
"""Format legend label with automatic wrapping.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
label: Original label text
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
str: Wrapped label text
|
|
351
|
+
"""
|
|
352
|
+
max_width = getattr(self.p.scheme, "max_legend_text_width", 16)
|
|
353
|
+
return wrap_legend_text(label, max_width)
|
|
354
|
+
|
|
355
|
+
def fill_between(
|
|
356
|
+
self, fig, row, x, y1, y2, secondary_y=False, color=None, opacity=None, name="", where=None
|
|
357
|
+
):
|
|
358
|
+
"""Draw filled area between two lines.
|
|
359
|
+
|
|
360
|
+
Reference: backtrader_plotly/plotter.py:718-750
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
fig: Plotly figure object
|
|
364
|
+
row: Subplot row number
|
|
365
|
+
x: x-axis data
|
|
366
|
+
y1: Upper boundary data
|
|
367
|
+
y2: Lower boundary data
|
|
368
|
+
secondary_y: Whether to use right y-axis
|
|
369
|
+
color: Fill color
|
|
370
|
+
opacity: Fill opacity (default: scheme.fillalpha)
|
|
371
|
+
name: Legend name
|
|
372
|
+
where: Condition mask (optional)
|
|
373
|
+
"""
|
|
374
|
+
x = np.array(x)
|
|
375
|
+
y1 = np.array(y1)
|
|
376
|
+
y2 = np.array(y2)
|
|
377
|
+
|
|
378
|
+
# Apply condition filter
|
|
379
|
+
if where is not None:
|
|
380
|
+
y2 = np.where(where, y2, y1)
|
|
381
|
+
|
|
382
|
+
# Get opacity from scheme if not provided
|
|
383
|
+
if opacity is None:
|
|
384
|
+
opacity = getattr(self.p.scheme, "fillalpha", 0.20)
|
|
385
|
+
|
|
386
|
+
# Convert color to RGBA
|
|
387
|
+
if color is not None:
|
|
388
|
+
color = self._to_rgba_color(color, opacity)
|
|
389
|
+
else:
|
|
390
|
+
color = f"rgba(128, 128, 128, {opacity})"
|
|
391
|
+
|
|
392
|
+
legendgroup = f"fill_{name}_{row}"
|
|
393
|
+
|
|
394
|
+
# Add upper boundary line
|
|
395
|
+
fig.add_trace(
|
|
396
|
+
go.Scatter(
|
|
397
|
+
x=x,
|
|
398
|
+
y=y2,
|
|
399
|
+
name=name,
|
|
400
|
+
legendgroup=legendgroup,
|
|
401
|
+
showlegend=False,
|
|
402
|
+
line={"color": color, "width": 0},
|
|
403
|
+
),
|
|
404
|
+
row=row,
|
|
405
|
+
col=1,
|
|
406
|
+
secondary_y=secondary_y,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Add filled area
|
|
410
|
+
fig.add_trace(
|
|
411
|
+
go.Scatter(
|
|
412
|
+
x=x,
|
|
413
|
+
y=y1,
|
|
414
|
+
name=self._format_label(name) if name else "",
|
|
415
|
+
legendgroup=legendgroup,
|
|
416
|
+
fill="tonexty",
|
|
417
|
+
fillcolor=color,
|
|
418
|
+
line={"color": color, "width": 0},
|
|
419
|
+
),
|
|
420
|
+
row=row,
|
|
421
|
+
col=1,
|
|
422
|
+
secondary_y=secondary_y,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def _to_rgba_color(self, color, opacity):
|
|
426
|
+
"""Convert color to RGBA format.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
color: Color name or rgb string
|
|
430
|
+
opacity: Opacity value (0-1)
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
str: rgba(r, g, b, a) format string
|
|
434
|
+
"""
|
|
435
|
+
# Check if already rgba
|
|
436
|
+
if isinstance(color, str) and color.startswith("rgba"):
|
|
437
|
+
return color
|
|
438
|
+
|
|
439
|
+
# Check color mapper
|
|
440
|
+
if color in COLOR_MAPPER:
|
|
441
|
+
rgb = COLOR_MAPPER[color]
|
|
442
|
+
else:
|
|
443
|
+
rgb = self._to_plotly_color(color)
|
|
444
|
+
|
|
445
|
+
# Extract RGB values and add opacity
|
|
446
|
+
if rgb and rgb.startswith("rgb("):
|
|
447
|
+
return f"rgba{rgb[3:-1]}, {opacity})"
|
|
448
|
+
|
|
449
|
+
return f"rgba(128, 128, 128, {opacity})"
|
|
450
|
+
|
|
451
|
+
def plot(
|
|
452
|
+
self,
|
|
453
|
+
strategy,
|
|
454
|
+
figid=0,
|
|
455
|
+
numfigs=1,
|
|
456
|
+
iplot=True,
|
|
457
|
+
start=None,
|
|
458
|
+
end=None,
|
|
459
|
+
use=None,
|
|
460
|
+
**kwargs,
|
|
461
|
+
):
|
|
462
|
+
"""
|
|
463
|
+
Plot the strategy results using Plotly.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
strategy: The strategy to plot
|
|
467
|
+
figid: Figure ID for multiple figures
|
|
468
|
+
numfigs: Number of figures to split into
|
|
469
|
+
iplot: If True, display inline in notebook
|
|
470
|
+
start: Start index or datetime
|
|
471
|
+
end: End index or datetime
|
|
472
|
+
use: Ignored (matplotlib backend parameter)
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
List of Plotly figure objects
|
|
476
|
+
"""
|
|
477
|
+
if not strategy.datas:
|
|
478
|
+
return []
|
|
479
|
+
|
|
480
|
+
if not len(strategy):
|
|
481
|
+
return []
|
|
482
|
+
|
|
483
|
+
# Sort indicators and observers
|
|
484
|
+
self._sortdataindicators(strategy)
|
|
485
|
+
|
|
486
|
+
# Collect buy/sell signals
|
|
487
|
+
self._collect_buysell_signals(strategy)
|
|
488
|
+
|
|
489
|
+
# Get datetime range
|
|
490
|
+
st_dtime = strategy.lines.datetime.plot()
|
|
491
|
+
if start is None:
|
|
492
|
+
start = 0
|
|
493
|
+
if end is None:
|
|
494
|
+
end = len(st_dtime)
|
|
495
|
+
|
|
496
|
+
if isinstance(start, datetime.date):
|
|
497
|
+
start = bisect.bisect_left(st_dtime, self._date2num(start))
|
|
498
|
+
if isinstance(end, datetime.date):
|
|
499
|
+
end = bisect.bisect_right(st_dtime, self._date2num(end))
|
|
500
|
+
|
|
501
|
+
if end < 0:
|
|
502
|
+
end = len(st_dtime) + 1 + end
|
|
503
|
+
|
|
504
|
+
# Create figures
|
|
505
|
+
figs = []
|
|
506
|
+
for numfig in range(numfigs):
|
|
507
|
+
# Calculate range for this figure
|
|
508
|
+
slen = len(st_dtime[start:end])
|
|
509
|
+
d, m = divmod(slen, numfigs)
|
|
510
|
+
a = d * numfig + start
|
|
511
|
+
if numfig == (numfigs - 1):
|
|
512
|
+
d += m
|
|
513
|
+
b = a + d
|
|
514
|
+
|
|
515
|
+
fig = self._create_figure(strategy, a, b, st_dtime)
|
|
516
|
+
figs.append(fig)
|
|
517
|
+
self.figs.append(fig)
|
|
518
|
+
|
|
519
|
+
return figs
|
|
520
|
+
|
|
521
|
+
def _date2num(self, dt):
|
|
522
|
+
"""Convert datetime to matplotlib-style number."""
|
|
523
|
+
from .. import date2num
|
|
524
|
+
|
|
525
|
+
return date2num(dt)
|
|
526
|
+
|
|
527
|
+
def _num2date(self, num):
|
|
528
|
+
"""Convert matplotlib-style number to datetime."""
|
|
529
|
+
from .. import num2date
|
|
530
|
+
|
|
531
|
+
return num2date(num)
|
|
532
|
+
|
|
533
|
+
def _create_figure(self, strategy, pstart, pend, st_dtime):
|
|
534
|
+
"""Create a Plotly figure for the given range."""
|
|
535
|
+
# Count rows needed
|
|
536
|
+
n_rows, row_specs, row_heights = self._calc_rows(strategy)
|
|
537
|
+
|
|
538
|
+
# Create subplots
|
|
539
|
+
fig = make_subplots(
|
|
540
|
+
rows=n_rows,
|
|
541
|
+
cols=1,
|
|
542
|
+
shared_xaxes=True,
|
|
543
|
+
vertical_spacing=0.02,
|
|
544
|
+
row_heights=row_heights,
|
|
545
|
+
specs=row_specs,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Convert datetime
|
|
549
|
+
xdata = [self._num2date(x) for x in st_dtime[pstart:pend]]
|
|
550
|
+
current_row = 1
|
|
551
|
+
|
|
552
|
+
# Plot each data feed
|
|
553
|
+
for data in strategy.datas:
|
|
554
|
+
if not data.plotinfo.plot:
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
# Get OHLCV data
|
|
558
|
+
opens = list(data.open.plotrange(pstart, pend))
|
|
559
|
+
highs = list(data.high.plotrange(pstart, pend))
|
|
560
|
+
lows = list(data.low.plotrange(pstart, pend))
|
|
561
|
+
closes = list(data.close.plotrange(pstart, pend))
|
|
562
|
+
volumes = list(data.volume.plotrange(pstart, pend))
|
|
563
|
+
|
|
564
|
+
# Align x data if needed
|
|
565
|
+
data_xdata = xdata
|
|
566
|
+
dts = data.datetime.plot()
|
|
567
|
+
if len(dts) < len(st_dtime):
|
|
568
|
+
# This data has fewer bars, need to align
|
|
569
|
+
data_xdata = [self._num2date(x) for x in data.datetime.plotrange(pstart, pend)]
|
|
570
|
+
|
|
571
|
+
# Skip indicators above data (disabled for cleaner chart)
|
|
572
|
+
# for ind in self.dplotsup.get(data, []):
|
|
573
|
+
# current_row = self._plot_indicator(
|
|
574
|
+
# fig, ind, data_xdata, pstart, pend, current_row
|
|
575
|
+
# )
|
|
576
|
+
|
|
577
|
+
# Plot main price chart
|
|
578
|
+
current_row = self._plot_data(
|
|
579
|
+
fig, data, data_xdata, opens, highs, lows, closes, volumes, current_row
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Plot buy/sell signals with price offset
|
|
583
|
+
self._plot_buysell_markers(fig, data, data_xdata, lows, highs, current_row - 1)
|
|
584
|
+
|
|
585
|
+
# Skip indicators below data (user requested removal)
|
|
586
|
+
# for ind in self.dplotsdown.get(data, []):
|
|
587
|
+
# current_row = self._plot_indicator(
|
|
588
|
+
# fig, ind, data_xdata, pstart, pend, current_row
|
|
589
|
+
# )
|
|
590
|
+
|
|
591
|
+
# Plot equity curve with drawdown at bottom
|
|
592
|
+
current_row = self._plot_equity_curve(fig, strategy, xdata, pstart, pend, current_row)
|
|
593
|
+
|
|
594
|
+
# Update layout
|
|
595
|
+
self._update_layout(fig, strategy)
|
|
596
|
+
|
|
597
|
+
return fig
|
|
598
|
+
|
|
599
|
+
def _calc_rows(self, strategy):
|
|
600
|
+
"""Calculate number of rows and their specifications."""
|
|
601
|
+
n_rows = 0
|
|
602
|
+
row_heights = []
|
|
603
|
+
row_specs = []
|
|
604
|
+
|
|
605
|
+
# Data feeds and their indicators
|
|
606
|
+
for data in strategy.datas:
|
|
607
|
+
if not data.plotinfo.plot:
|
|
608
|
+
continue
|
|
609
|
+
|
|
610
|
+
# Indicators above - disabled for cleaner chart
|
|
611
|
+
# n_up = len(self.dplotsup.get(data, []))
|
|
612
|
+
# n_rows += n_up
|
|
613
|
+
# row_heights.extend([0.5] * n_up)
|
|
614
|
+
# row_specs.extend([[{"secondary_y": False}]] * n_up)
|
|
615
|
+
|
|
616
|
+
# Main data (with optional volume overlay)
|
|
617
|
+
n_rows += 1
|
|
618
|
+
row_heights.append(3)
|
|
619
|
+
row_specs.append([{"secondary_y": True}])
|
|
620
|
+
|
|
621
|
+
# Volume as separate row if not overlay (smaller height)
|
|
622
|
+
if self.p.scheme.volume and not self.p.scheme.voloverlay:
|
|
623
|
+
n_rows += 1
|
|
624
|
+
row_heights.append(0.6) # Smaller volume subplot
|
|
625
|
+
row_specs.append([{"secondary_y": False}])
|
|
626
|
+
|
|
627
|
+
# Overlaid indicators don't add rows
|
|
628
|
+
for ind in self.dplotsover.get(data, []):
|
|
629
|
+
pass # These are plotted on the same row as data
|
|
630
|
+
|
|
631
|
+
# Equity curve row at bottom (below K-line)
|
|
632
|
+
n_rows += 1
|
|
633
|
+
row_heights.append(1.5)
|
|
634
|
+
row_specs.append([{"secondary_y": False}])
|
|
635
|
+
|
|
636
|
+
if n_rows == 0:
|
|
637
|
+
n_rows = 1
|
|
638
|
+
row_heights = [1]
|
|
639
|
+
row_specs = [[{"secondary_y": False}]]
|
|
640
|
+
|
|
641
|
+
# Normalize heights
|
|
642
|
+
total = sum(row_heights)
|
|
643
|
+
row_heights = [h / total for h in row_heights]
|
|
644
|
+
|
|
645
|
+
return n_rows, row_specs, row_heights
|
|
646
|
+
|
|
647
|
+
def _plot_data(self, fig, data, xdata, opens, highs, lows, closes, volumes, row):
|
|
648
|
+
"""Plot OHLCV data."""
|
|
649
|
+
datalabel = getattr(data, "_name", "") or "Data"
|
|
650
|
+
|
|
651
|
+
# Choose chart style
|
|
652
|
+
style = self.p.scheme.style
|
|
653
|
+
if style.startswith("candle"):
|
|
654
|
+
fig.add_trace(
|
|
655
|
+
go.Candlestick(
|
|
656
|
+
x=xdata,
|
|
657
|
+
open=opens,
|
|
658
|
+
high=highs,
|
|
659
|
+
low=lows,
|
|
660
|
+
close=closes,
|
|
661
|
+
name=datalabel,
|
|
662
|
+
increasing_line_color=self._to_plotly_color(self.p.scheme.barup),
|
|
663
|
+
decreasing_line_color=self._to_plotly_color(self.p.scheme.bardown),
|
|
664
|
+
increasing_fillcolor=self._to_plotly_color(self.p.scheme.barup),
|
|
665
|
+
decreasing_fillcolor=self._to_plotly_color(self.p.scheme.bardown),
|
|
666
|
+
),
|
|
667
|
+
row=row,
|
|
668
|
+
col=1,
|
|
669
|
+
)
|
|
670
|
+
elif style.startswith("bar"):
|
|
671
|
+
fig.add_trace(
|
|
672
|
+
go.Ohlc(
|
|
673
|
+
x=xdata,
|
|
674
|
+
open=opens,
|
|
675
|
+
high=highs,
|
|
676
|
+
low=lows,
|
|
677
|
+
close=closes,
|
|
678
|
+
name=datalabel,
|
|
679
|
+
increasing_line_color=self._to_plotly_color(self.p.scheme.barup),
|
|
680
|
+
decreasing_line_color=self._to_plotly_color(self.p.scheme.bardown),
|
|
681
|
+
),
|
|
682
|
+
row=row,
|
|
683
|
+
col=1,
|
|
684
|
+
)
|
|
685
|
+
else: # line
|
|
686
|
+
fig.add_trace(
|
|
687
|
+
go.Scatter(
|
|
688
|
+
x=xdata,
|
|
689
|
+
y=closes,
|
|
690
|
+
mode="lines",
|
|
691
|
+
name=datalabel,
|
|
692
|
+
line={"color": self._to_plotly_color(self.p.scheme.loc)},
|
|
693
|
+
),
|
|
694
|
+
row=row,
|
|
695
|
+
col=1,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Plot volume
|
|
699
|
+
if self.p.scheme.volume and max(volumes) > 0:
|
|
700
|
+
colors = [
|
|
701
|
+
self.p.scheme.volup if c >= o else self.p.scheme.voldown
|
|
702
|
+
for o, c in zip(opens, closes)
|
|
703
|
+
]
|
|
704
|
+
colors = [self._to_plotly_color(c) for c in colors]
|
|
705
|
+
|
|
706
|
+
if self.p.scheme.voloverlay:
|
|
707
|
+
# Overlay on price chart - scale down volume to bottom 20% of chart
|
|
708
|
+
max_vol = max(volumes)
|
|
709
|
+
min_price = min(lows)
|
|
710
|
+
max_price = max(highs)
|
|
711
|
+
price_range = max_price - min_price
|
|
712
|
+
# Scale volume to 15% of price range, positioned at bottom
|
|
713
|
+
scale_factor = (price_range * 0.15) / max_vol if max_vol > 0 else 1
|
|
714
|
+
scaled_volumes = [v * scale_factor for v in volumes]
|
|
715
|
+
# Offset to position below price bars
|
|
716
|
+
vol_base = min_price - price_range * 0.02
|
|
717
|
+
|
|
718
|
+
fig.add_trace(
|
|
719
|
+
go.Bar(
|
|
720
|
+
x=xdata,
|
|
721
|
+
y=scaled_volumes,
|
|
722
|
+
base=[vol_base] * len(scaled_volumes),
|
|
723
|
+
name="Volume",
|
|
724
|
+
marker_color=colors,
|
|
725
|
+
opacity=0.6,
|
|
726
|
+
showlegend=True,
|
|
727
|
+
),
|
|
728
|
+
row=row,
|
|
729
|
+
col=1,
|
|
730
|
+
)
|
|
731
|
+
row_inc = 1
|
|
732
|
+
else:
|
|
733
|
+
# Separate volume subplot
|
|
734
|
+
fig.add_trace(
|
|
735
|
+
go.Bar(
|
|
736
|
+
x=xdata,
|
|
737
|
+
y=volumes,
|
|
738
|
+
name="Volume",
|
|
739
|
+
marker_color=colors,
|
|
740
|
+
opacity=0.7,
|
|
741
|
+
),
|
|
742
|
+
row=row + 1,
|
|
743
|
+
col=1,
|
|
744
|
+
)
|
|
745
|
+
row_inc = 2
|
|
746
|
+
else:
|
|
747
|
+
row_inc = 1
|
|
748
|
+
|
|
749
|
+
# Plot overlaid indicators
|
|
750
|
+
for ind in self.dplotsover.get(data, []):
|
|
751
|
+
self._plot_indicator_on_ax(fig, ind, xdata, row, is_overlay=True)
|
|
752
|
+
|
|
753
|
+
return row + row_inc
|
|
754
|
+
|
|
755
|
+
@staticmethod
|
|
756
|
+
def _trim_prewarmup_zeros(lplot, plot_xdata):
|
|
757
|
+
"""Trim leading pre-warmup values from an indicator line series.
|
|
758
|
+
|
|
759
|
+
Indicators emit ``0.0`` before they have enough data, then jump to
|
|
760
|
+
real values. This finds the first real (non-NaN, non-leading-zero)
|
|
761
|
+
sample and trims both the value list and its x-axis to match, then
|
|
762
|
+
replaces remaining NaNs with ``None`` so Plotly skips them.
|
|
763
|
+
|
|
764
|
+
Returns the ``(lplot, plot_xdata)`` pair, or ``(None, None)`` when the
|
|
765
|
+
trimmed series is empty (caller should skip the line). Extracted
|
|
766
|
+
verbatim from the duplicated blocks in ``_plot_indicator`` /
|
|
767
|
+
``_plot_indicator_on_ax``; behavior unchanged.
|
|
768
|
+
"""
|
|
769
|
+
# Find first valid (non-NaN and non-zero-before-real-data) value
|
|
770
|
+
# Indicators output 0.0 before they have enough data, then jump to real values
|
|
771
|
+
valid_start = 0
|
|
772
|
+
found_nonzero = False
|
|
773
|
+
for i, v in enumerate(lplot):
|
|
774
|
+
if math.isnan(v):
|
|
775
|
+
continue
|
|
776
|
+
# If we find a non-zero value, that's where real data starts
|
|
777
|
+
if v != 0.0:
|
|
778
|
+
valid_start = i
|
|
779
|
+
found_nonzero = True
|
|
780
|
+
break
|
|
781
|
+
# If all values are 0, we'll check if later values become non-zero
|
|
782
|
+
|
|
783
|
+
# If no non-zero found, check if there are any real values
|
|
784
|
+
if not found_nonzero:
|
|
785
|
+
# Find first transition from 0 to non-zero
|
|
786
|
+
for i in range(len(lplot) - 1):
|
|
787
|
+
if lplot[i] == 0.0 and lplot[i + 1] != 0.0 and not math.isnan(lplot[i + 1]):
|
|
788
|
+
valid_start = i + 1
|
|
789
|
+
found_nonzero = True
|
|
790
|
+
break
|
|
791
|
+
|
|
792
|
+
# Skip leading invalid portion (zeros before real data)
|
|
793
|
+
if valid_start > 0:
|
|
794
|
+
lplot = lplot[valid_start:]
|
|
795
|
+
plot_xdata = plot_xdata[valid_start:]
|
|
796
|
+
|
|
797
|
+
if not lplot:
|
|
798
|
+
return None, None
|
|
799
|
+
|
|
800
|
+
# Replace NaN with None for Plotly to skip
|
|
801
|
+
lplot = [None if math.isnan(v) else v for v in lplot]
|
|
802
|
+
return lplot, plot_xdata
|
|
803
|
+
|
|
804
|
+
def _plot_indicator(self, fig, ind, xdata, pstart, pend, row, is_observer=False):
|
|
805
|
+
"""Plot an indicator in its own subplot."""
|
|
806
|
+
indlabel = ind.plotlabel()
|
|
807
|
+
# Ensure indlabel is a string
|
|
808
|
+
if not isinstance(indlabel, str):
|
|
809
|
+
indlabel = str(ind.__class__.__name__)
|
|
810
|
+
|
|
811
|
+
for lineidx in range(ind.size()):
|
|
812
|
+
line = ind.lines[lineidx]
|
|
813
|
+
linealias = ind.lines._getlinealias(lineidx)
|
|
814
|
+
lplot = list(line.plotrange(pstart, pend))
|
|
815
|
+
|
|
816
|
+
if not lplot or len(lplot) == 0:
|
|
817
|
+
continue
|
|
818
|
+
|
|
819
|
+
# Align data length
|
|
820
|
+
plot_xdata = xdata
|
|
821
|
+
if len(lplot) != len(xdata):
|
|
822
|
+
plot_xdata = xdata[: len(lplot)]
|
|
823
|
+
|
|
824
|
+
lplot, plot_xdata = self._trim_prewarmup_zeros(lplot, plot_xdata)
|
|
825
|
+
if lplot is None:
|
|
826
|
+
continue
|
|
827
|
+
|
|
828
|
+
# Get line plot info
|
|
829
|
+
lineplotinfo = getattr(ind.plotlines, linealias, None)
|
|
830
|
+
if lineplotinfo is None:
|
|
831
|
+
lineplotinfo = getattr(ind.plotlines, "_%d" % lineidx, None)
|
|
832
|
+
|
|
833
|
+
# Get color
|
|
834
|
+
color = None
|
|
835
|
+
if lineplotinfo:
|
|
836
|
+
color = lineplotinfo._get("color", None)
|
|
837
|
+
if color is None:
|
|
838
|
+
color = self.p.scheme.color(lineidx)
|
|
839
|
+
|
|
840
|
+
# Get line style
|
|
841
|
+
linestyle = "solid"
|
|
842
|
+
if lineplotinfo:
|
|
843
|
+
ls = lineplotinfo._get("ls", None) or lineplotinfo._get("linestyle", None)
|
|
844
|
+
if ls == "--":
|
|
845
|
+
linestyle = "dash"
|
|
846
|
+
elif ls == ":":
|
|
847
|
+
linestyle = "dot"
|
|
848
|
+
elif ls == "-.":
|
|
849
|
+
linestyle = "dashdot"
|
|
850
|
+
|
|
851
|
+
label = f"{indlabel} - {linealias}" if ind.size() > 1 else indlabel
|
|
852
|
+
|
|
853
|
+
fig.add_trace(
|
|
854
|
+
go.Scatter(
|
|
855
|
+
x=plot_xdata,
|
|
856
|
+
y=lplot,
|
|
857
|
+
mode="lines",
|
|
858
|
+
name=label,
|
|
859
|
+
line={"color": self._to_plotly_color(color), "dash": linestyle},
|
|
860
|
+
),
|
|
861
|
+
row=row,
|
|
862
|
+
col=1,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# Plot horizontal lines
|
|
866
|
+
hlines = ind.plotinfo._get("plothlines", None) or []
|
|
867
|
+
if not hlines:
|
|
868
|
+
hlines = ind.plotinfo._get("plotyhlines", None) or []
|
|
869
|
+
for hline in hlines:
|
|
870
|
+
fig.add_hline(
|
|
871
|
+
y=hline,
|
|
872
|
+
line_dash="dash",
|
|
873
|
+
line_color=self._to_plotly_color(self.p.scheme.hlinescolor),
|
|
874
|
+
row=row,
|
|
875
|
+
col=1,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
return row + 1
|
|
879
|
+
|
|
880
|
+
def _plot_indicator_on_ax(self, fig, ind, xdata, row, is_overlay=False):
|
|
881
|
+
"""Plot an indicator overlaid on existing subplot."""
|
|
882
|
+
indlabel = ind.plotlabel()
|
|
883
|
+
# Ensure indlabel is a string
|
|
884
|
+
if not isinstance(indlabel, str):
|
|
885
|
+
indlabel = str(ind.__class__.__name__)
|
|
886
|
+
pstart = 0
|
|
887
|
+
pend = len(xdata)
|
|
888
|
+
|
|
889
|
+
for lineidx in range(ind.size()):
|
|
890
|
+
line = ind.lines[lineidx]
|
|
891
|
+
linealias = ind.lines._getlinealias(lineidx)
|
|
892
|
+
|
|
893
|
+
# Get plotinfo
|
|
894
|
+
lineplotinfo = getattr(ind.plotlines, linealias, None)
|
|
895
|
+
if lineplotinfo is None:
|
|
896
|
+
lineplotinfo = getattr(ind.plotlines, "_%d" % lineidx, None)
|
|
897
|
+
|
|
898
|
+
if lineplotinfo and lineplotinfo._get("_plotskip", False):
|
|
899
|
+
continue
|
|
900
|
+
|
|
901
|
+
lplot = list(line.plotrange(pstart, pend))
|
|
902
|
+
if not lplot:
|
|
903
|
+
continue
|
|
904
|
+
|
|
905
|
+
# Align data
|
|
906
|
+
plot_xdata = xdata
|
|
907
|
+
if len(lplot) != len(xdata):
|
|
908
|
+
plot_xdata = xdata[: len(lplot)]
|
|
909
|
+
|
|
910
|
+
lplot, plot_xdata = self._trim_prewarmup_zeros(lplot, plot_xdata)
|
|
911
|
+
if lplot is None:
|
|
912
|
+
continue
|
|
913
|
+
|
|
914
|
+
# Get color
|
|
915
|
+
color = None
|
|
916
|
+
if lineplotinfo:
|
|
917
|
+
color = lineplotinfo._get("color", None)
|
|
918
|
+
if color is None:
|
|
919
|
+
color = self.p.scheme.color(lineidx)
|
|
920
|
+
|
|
921
|
+
label = f"{indlabel} - {linealias}" if ind.size() > 1 else indlabel
|
|
922
|
+
|
|
923
|
+
# Determine plot method
|
|
924
|
+
pltmethod = "plot"
|
|
925
|
+
if lineplotinfo:
|
|
926
|
+
pltmethod = lineplotinfo._get("_method", "plot")
|
|
927
|
+
|
|
928
|
+
if pltmethod == "bar":
|
|
929
|
+
fig.add_trace(
|
|
930
|
+
go.Bar(x=plot_xdata, y=lplot, name=label, opacity=0.6),
|
|
931
|
+
row=row,
|
|
932
|
+
col=1,
|
|
933
|
+
)
|
|
934
|
+
else:
|
|
935
|
+
fig.add_trace(
|
|
936
|
+
go.Scatter(
|
|
937
|
+
x=plot_xdata,
|
|
938
|
+
y=lplot,
|
|
939
|
+
mode="lines",
|
|
940
|
+
name=label,
|
|
941
|
+
line={"color": self._to_plotly_color(color)},
|
|
942
|
+
),
|
|
943
|
+
row=row,
|
|
944
|
+
col=1,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
def _to_plotly_color(self, color):
|
|
948
|
+
"""Convert matplotlib color to plotly color."""
|
|
949
|
+
if color is None:
|
|
950
|
+
return None
|
|
951
|
+
if isinstance(color, str):
|
|
952
|
+
# Handle gray values like "0.75"
|
|
953
|
+
try:
|
|
954
|
+
gray = float(color)
|
|
955
|
+
gray_int = int(gray * 255)
|
|
956
|
+
return f"rgb({gray_int},{gray_int},{gray_int})"
|
|
957
|
+
except ValueError:
|
|
958
|
+
# Not a numeric gray string (e.g. a named color); return as-is.
|
|
959
|
+
logger.debug("plot_plotly:959 ignored ValueError")
|
|
960
|
+
return color
|
|
961
|
+
if isinstance(color, (tuple, list)):
|
|
962
|
+
if len(color) == 3:
|
|
963
|
+
r, g, b = color
|
|
964
|
+
if all(0 <= c <= 1 for c in color):
|
|
965
|
+
return f"rgb({int(r * 255)},{int(g * 255)},{int(b * 255)})"
|
|
966
|
+
return f"rgb({r},{g},{b})"
|
|
967
|
+
if len(color) == 4:
|
|
968
|
+
r, g, b, a = color
|
|
969
|
+
if all(0 <= c <= 1 for c in color):
|
|
970
|
+
return f"rgba({int(r * 255)},{int(g * 255)},{int(b * 255)},{a})"
|
|
971
|
+
return f"rgba({r},{g},{b},{a})"
|
|
972
|
+
return str(color)
|
|
973
|
+
|
|
974
|
+
def _update_layout(self, fig, strategy):
|
|
975
|
+
"""Update figure layout with styling."""
|
|
976
|
+
datalabel = ""
|
|
977
|
+
if strategy.datas:
|
|
978
|
+
data = strategy.datas[0]
|
|
979
|
+
if hasattr(data, "_name") and data._name:
|
|
980
|
+
datalabel = data._name
|
|
981
|
+
|
|
982
|
+
fig.update_layout(
|
|
983
|
+
title=f"Backtrader Chart - {datalabel}" if datalabel else "Backtrader Chart",
|
|
984
|
+
template=self.p.scheme.plotly_theme,
|
|
985
|
+
height=800,
|
|
986
|
+
showlegend=True,
|
|
987
|
+
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "right", "x": 1},
|
|
988
|
+
hovermode="x unified",
|
|
989
|
+
xaxis_rangeslider_visible=self.p.scheme.rangeslider,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
# Disable rangeslider to avoid duplicating the equity/drawdown subplot
|
|
993
|
+
fig.update_xaxes(rangeslider_visible=False)
|
|
994
|
+
|
|
995
|
+
if self.p.scheme.rangeslider:
|
|
996
|
+
rangeslider = {"visible": True}
|
|
997
|
+
if not self.p.scheme.rangeslider_preview:
|
|
998
|
+
rangeslider.update(
|
|
999
|
+
thickness=0.05,
|
|
1000
|
+
bgcolor="rgba(0,0,0,0)",
|
|
1001
|
+
borderwidth=0,
|
|
1002
|
+
yaxis={"rangemode": "fixed", "range": [1e12, 1e12 + 1]},
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
fig.update_xaxes(rangeslider=rangeslider, row=1, col=1)
|
|
1006
|
+
|
|
1007
|
+
# Best-practice: add range selector buttons (bottom axis only)
|
|
1008
|
+
try:
|
|
1009
|
+
bottom_row = fig._get_subplot_rows_columns()[0][-1]
|
|
1010
|
+
fig.update_xaxes(
|
|
1011
|
+
rangeselector={
|
|
1012
|
+
"buttons": [
|
|
1013
|
+
{"count": 1, "label": "1m", "step": "month", "stepmode": "backward"},
|
|
1014
|
+
{"count": 3, "label": "3m", "step": "month", "stepmode": "backward"},
|
|
1015
|
+
{"count": 6, "label": "6m", "step": "month", "stepmode": "backward"},
|
|
1016
|
+
{"count": 1, "label": "1y", "step": "year", "stepmode": "backward"},
|
|
1017
|
+
{"step": "all", "label": "All"},
|
|
1018
|
+
]
|
|
1019
|
+
},
|
|
1020
|
+
row=bottom_row,
|
|
1021
|
+
col=1,
|
|
1022
|
+
)
|
|
1023
|
+
except Exception as e:
|
|
1024
|
+
logger.debug("Failed to add range selector: %s", e)
|
|
1025
|
+
|
|
1026
|
+
# Crosshair spike lines
|
|
1027
|
+
fig.update_xaxes(showspikes=True, spikemode="across", spikesnap="cursor", spikethickness=1)
|
|
1028
|
+
fig.update_yaxes(showspikes=True, spikemode="across", spikesnap="cursor", spikethickness=1)
|
|
1029
|
+
|
|
1030
|
+
# Update y-axes with decimal places format
|
|
1031
|
+
tick_format = self._get_tick_format()
|
|
1032
|
+
fig.update_yaxes(side="right", tickformat=tick_format)
|
|
1033
|
+
|
|
1034
|
+
def _sortdataindicators(self, strategy):
|
|
1035
|
+
"""Sort indicators and observers into appropriate lists."""
|
|
1036
|
+
self.dplotstop = []
|
|
1037
|
+
self.dplotsup = collections.defaultdict(list)
|
|
1038
|
+
self.dplotsdown = collections.defaultdict(list)
|
|
1039
|
+
self.dplotsover = collections.defaultdict(list)
|
|
1040
|
+
|
|
1041
|
+
# Sort observers
|
|
1042
|
+
for x in strategy.getobservers():
|
|
1043
|
+
if not x.plotinfo.plot or x.plotinfo.plotskip:
|
|
1044
|
+
continue
|
|
1045
|
+
|
|
1046
|
+
if x.plotinfo.subplot:
|
|
1047
|
+
self.dplotstop.append(x)
|
|
1048
|
+
else:
|
|
1049
|
+
key = getattr(x._clock, "owner", x._clock)
|
|
1050
|
+
self.dplotsover[key].append(x)
|
|
1051
|
+
|
|
1052
|
+
# Sort indicators
|
|
1053
|
+
for x in strategy.getindicators():
|
|
1054
|
+
if not hasattr(x, "plotinfo"):
|
|
1055
|
+
continue
|
|
1056
|
+
|
|
1057
|
+
if not x.plotinfo.plot or x.plotinfo.plotskip:
|
|
1058
|
+
continue
|
|
1059
|
+
|
|
1060
|
+
x._plotinit()
|
|
1061
|
+
|
|
1062
|
+
key = getattr(x._clock, "owner", x._clock)
|
|
1063
|
+
if key is strategy:
|
|
1064
|
+
key = strategy.data
|
|
1065
|
+
|
|
1066
|
+
if getattr(x.plotinfo, "plotforce", False):
|
|
1067
|
+
if key not in strategy.datas:
|
|
1068
|
+
while key not in strategy.datas:
|
|
1069
|
+
key = key._clock
|
|
1070
|
+
|
|
1071
|
+
xpmaster = x.plotinfo.plotmaster
|
|
1072
|
+
if xpmaster is x:
|
|
1073
|
+
xpmaster = None
|
|
1074
|
+
if xpmaster is not None:
|
|
1075
|
+
key = xpmaster
|
|
1076
|
+
|
|
1077
|
+
if x.plotinfo.subplot and xpmaster is None:
|
|
1078
|
+
if x.plotinfo.plotabove:
|
|
1079
|
+
self.dplotsup[key].append(x)
|
|
1080
|
+
else:
|
|
1081
|
+
self.dplotsdown[key].append(x)
|
|
1082
|
+
else:
|
|
1083
|
+
self.dplotsover[key].append(x)
|
|
1084
|
+
|
|
1085
|
+
def show(self):
|
|
1086
|
+
"""Display all figures."""
|
|
1087
|
+
for fig in self.figs:
|
|
1088
|
+
fig.show()
|
|
1089
|
+
|
|
1090
|
+
def savefig(self, fig, filename, width=1600, height=900, scale=2):
|
|
1091
|
+
"""Save figure to file."""
|
|
1092
|
+
if filename.endswith(".html"):
|
|
1093
|
+
fig.write_html(filename)
|
|
1094
|
+
else:
|
|
1095
|
+
fig.write_image(filename, width=width, height=height, scale=scale)
|
|
1096
|
+
|
|
1097
|
+
def _collect_buysell_signals(self, strategy):
|
|
1098
|
+
"""Collect buy/sell signals from strategy automatically.
|
|
1099
|
+
|
|
1100
|
+
Tries four sources in priority order, stopping at the first that
|
|
1101
|
+
yields markers: Transactions analyzer, broker order history, a
|
|
1102
|
+
user-defined ``_buysell`` attribute, then the BuySell observer.
|
|
1103
|
+
"""
|
|
1104
|
+
self.buysell_markers = []
|
|
1105
|
+
|
|
1106
|
+
if self._buysell_from_transactions(strategy):
|
|
1107
|
+
return
|
|
1108
|
+
if self._buysell_from_broker_orders(strategy):
|
|
1109
|
+
return
|
|
1110
|
+
if self._buysell_from_strategy_attr(strategy):
|
|
1111
|
+
return
|
|
1112
|
+
self._buysell_from_observer(strategy)
|
|
1113
|
+
|
|
1114
|
+
def _buysell_from_transactions(self, strategy):
|
|
1115
|
+
"""Method 1: Transactions analyzer (most reliable). Returns True if found."""
|
|
1116
|
+
if hasattr(strategy, "analyzers"):
|
|
1117
|
+
for analyzer in strategy.analyzers:
|
|
1118
|
+
if analyzer.__class__.__name__ == "Transactions":
|
|
1119
|
+
txn = analyzer.get_analysis()
|
|
1120
|
+
for dt, trades in txn.items():
|
|
1121
|
+
for trade in trades:
|
|
1122
|
+
# trade format: [size, price, value, ...]
|
|
1123
|
+
size = trade[0]
|
|
1124
|
+
price = trade[1]
|
|
1125
|
+
self.buysell_markers.append(
|
|
1126
|
+
{
|
|
1127
|
+
"datetime": dt,
|
|
1128
|
+
"price": price,
|
|
1129
|
+
"type": "buy" if size > 0 else "sell",
|
|
1130
|
+
}
|
|
1131
|
+
)
|
|
1132
|
+
if self.buysell_markers:
|
|
1133
|
+
return True
|
|
1134
|
+
return False
|
|
1135
|
+
|
|
1136
|
+
def _buysell_from_broker_orders(self, strategy):
|
|
1137
|
+
"""Method 2: broker order history. Returns True if found."""
|
|
1138
|
+
if hasattr(strategy, "broker") and hasattr(strategy.broker, "orders"):
|
|
1139
|
+
for order in strategy.broker.orders:
|
|
1140
|
+
if order.status == order.Completed:
|
|
1141
|
+
# Get execution datetime and price
|
|
1142
|
+
exec_dt = num2date(order.executed.dt)
|
|
1143
|
+
self.buysell_markers.append(
|
|
1144
|
+
{
|
|
1145
|
+
"datetime": exec_dt,
|
|
1146
|
+
"price": order.executed.price,
|
|
1147
|
+
"type": "buy" if order.isbuy() else "sell",
|
|
1148
|
+
}
|
|
1149
|
+
)
|
|
1150
|
+
if self.buysell_markers:
|
|
1151
|
+
return True
|
|
1152
|
+
return False
|
|
1153
|
+
|
|
1154
|
+
def _buysell_from_strategy_attr(self, strategy):
|
|
1155
|
+
"""Method 3: user-defined ``_buysell`` attribute. Returns True if found."""
|
|
1156
|
+
if hasattr(strategy, "_buysell") and strategy._buysell:
|
|
1157
|
+
self.buysell_markers = strategy._buysell
|
|
1158
|
+
return True
|
|
1159
|
+
return False
|
|
1160
|
+
|
|
1161
|
+
def _buysell_from_observer(self, strategy):
|
|
1162
|
+
"""Method 4: BuySell observer buy/sell lines."""
|
|
1163
|
+
for obs in strategy.observers:
|
|
1164
|
+
if obs.__class__.__name__ == "BuySell":
|
|
1165
|
+
buy_line = obs.lines.buy
|
|
1166
|
+
sell_line = obs.lines.sell
|
|
1167
|
+
buy_vals = list(buy_line.plotrange(0, len(strategy)))
|
|
1168
|
+
sell_vals = list(sell_line.plotrange(0, len(strategy)))
|
|
1169
|
+
|
|
1170
|
+
st_dtime = strategy.lines.datetime.plot()
|
|
1171
|
+
for i, (bv, sv) in enumerate(zip(buy_vals, sell_vals)):
|
|
1172
|
+
if not math.isnan(bv):
|
|
1173
|
+
self.buysell_markers.append(
|
|
1174
|
+
{"datetime": self._num2date(st_dtime[i]), "price": bv, "type": "buy"}
|
|
1175
|
+
)
|
|
1176
|
+
if not math.isnan(sv):
|
|
1177
|
+
self.buysell_markers.append(
|
|
1178
|
+
{"datetime": self._num2date(st_dtime[i]), "price": sv, "type": "sell"}
|
|
1179
|
+
)
|
|
1180
|
+
break
|
|
1181
|
+
|
|
1182
|
+
def _plot_buysell_markers(self, fig, data, xdata, lows, highs, row):
|
|
1183
|
+
"""Plot buy/sell markers on the price chart with offset from price."""
|
|
1184
|
+
if not self.buysell_markers:
|
|
1185
|
+
return
|
|
1186
|
+
|
|
1187
|
+
# Calculate price range for offset
|
|
1188
|
+
price_range = max(highs) - min(lows) if highs and lows else 1
|
|
1189
|
+
offset = price_range * 0.03 # 3% offset from high/low
|
|
1190
|
+
|
|
1191
|
+
# Create datetime to index mapping for finding low/high values
|
|
1192
|
+
dt_to_idx = {dt: i for i, dt in enumerate(xdata)}
|
|
1193
|
+
|
|
1194
|
+
buy_x, buy_y, buy_prices = [], [], []
|
|
1195
|
+
sell_x, sell_y, sell_prices = [], [], []
|
|
1196
|
+
|
|
1197
|
+
for marker in self.buysell_markers:
|
|
1198
|
+
marker_dt = marker["datetime"]
|
|
1199
|
+
price = marker["price"]
|
|
1200
|
+
|
|
1201
|
+
# Find the closest datetime in xdata
|
|
1202
|
+
idx = dt_to_idx.get(marker_dt)
|
|
1203
|
+
if idx is None:
|
|
1204
|
+
# Try to find closest match
|
|
1205
|
+
for i, dt in enumerate(xdata):
|
|
1206
|
+
if hasattr(dt, "date") and hasattr(marker_dt, "date"):
|
|
1207
|
+
if dt.date() == marker_dt.date():
|
|
1208
|
+
idx = i
|
|
1209
|
+
break
|
|
1210
|
+
|
|
1211
|
+
if idx is not None and idx < len(lows) and idx < len(highs):
|
|
1212
|
+
if marker["type"] == "buy":
|
|
1213
|
+
buy_x.append(marker_dt)
|
|
1214
|
+
buy_y.append(lows[idx] - offset) # Below the low
|
|
1215
|
+
buy_prices.append(price)
|
|
1216
|
+
else:
|
|
1217
|
+
sell_x.append(marker_dt)
|
|
1218
|
+
sell_y.append(highs[idx] + offset) # Above the high
|
|
1219
|
+
sell_prices.append(price)
|
|
1220
|
+
|
|
1221
|
+
# Plot buy markers (triangle up) below lows
|
|
1222
|
+
if buy_x:
|
|
1223
|
+
fig.add_trace(
|
|
1224
|
+
go.Scatter(
|
|
1225
|
+
x=buy_x,
|
|
1226
|
+
y=buy_y,
|
|
1227
|
+
mode="markers",
|
|
1228
|
+
name="Buy",
|
|
1229
|
+
marker={
|
|
1230
|
+
"symbol": "triangle-up",
|
|
1231
|
+
"size": self.p.scheme.buymarker_size,
|
|
1232
|
+
"color": self.p.scheme.buymarker_color,
|
|
1233
|
+
"line": {"width": 1, "color": "white"},
|
|
1234
|
+
},
|
|
1235
|
+
customdata=buy_prices,
|
|
1236
|
+
hovertemplate="Buy @ %{customdata:.2f}<extra></extra>",
|
|
1237
|
+
),
|
|
1238
|
+
row=row,
|
|
1239
|
+
col=1,
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
# Plot sell markers (triangle down) above highs
|
|
1243
|
+
if sell_x:
|
|
1244
|
+
fig.add_trace(
|
|
1245
|
+
go.Scatter(
|
|
1246
|
+
x=sell_x,
|
|
1247
|
+
y=sell_y,
|
|
1248
|
+
mode="markers",
|
|
1249
|
+
name="Sell",
|
|
1250
|
+
marker={
|
|
1251
|
+
"symbol": "triangle-down",
|
|
1252
|
+
"size": self.p.scheme.sellmarker_size,
|
|
1253
|
+
"color": self.p.scheme.sellmarker_color,
|
|
1254
|
+
"line": {"width": 1, "color": "white"},
|
|
1255
|
+
},
|
|
1256
|
+
customdata=sell_prices,
|
|
1257
|
+
hovertemplate="Sell @ %{customdata:.2f}<extra></extra>",
|
|
1258
|
+
),
|
|
1259
|
+
row=row,
|
|
1260
|
+
col=1,
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
def _plot_equity_curve(self, fig, strategy, xdata, pstart, pend, row):
|
|
1264
|
+
"""Plot equity curve with drawdown area."""
|
|
1265
|
+
equity_values = None
|
|
1266
|
+
equity_dates = None
|
|
1267
|
+
|
|
1268
|
+
# Method 1: Try to get from TotalValue analyzer (recommended)
|
|
1269
|
+
if hasattr(strategy, "analyzers"):
|
|
1270
|
+
for analyzer in strategy.analyzers:
|
|
1271
|
+
if analyzer.__class__.__name__ == "TotalValue":
|
|
1272
|
+
total_value_data = analyzer.get_analysis()
|
|
1273
|
+
if total_value_data:
|
|
1274
|
+
equity_dates = list(total_value_data.keys())
|
|
1275
|
+
equity_values = list(total_value_data.values())
|
|
1276
|
+
break
|
|
1277
|
+
|
|
1278
|
+
# Method 2: Try to get from Broker observer
|
|
1279
|
+
if not equity_values:
|
|
1280
|
+
for obs in strategy.observers:
|
|
1281
|
+
if obs.__class__.__name__ == "Broker":
|
|
1282
|
+
if hasattr(obs.lines, "value"):
|
|
1283
|
+
equity_values = list(obs.lines.value.plotrange(pstart, pend))
|
|
1284
|
+
equity_dates = xdata
|
|
1285
|
+
break
|
|
1286
|
+
|
|
1287
|
+
if not equity_values or len(equity_values) == 0:
|
|
1288
|
+
return row
|
|
1289
|
+
|
|
1290
|
+
# Use equity_dates if available, otherwise use xdata
|
|
1291
|
+
plot_xdata = equity_dates if equity_dates else xdata
|
|
1292
|
+
plot_equity = equity_values
|
|
1293
|
+
|
|
1294
|
+
# Filter NaN values
|
|
1295
|
+
valid_data = [(x, v) for x, v in zip(plot_xdata, plot_equity) if not math.isnan(v)]
|
|
1296
|
+
if not valid_data:
|
|
1297
|
+
return row
|
|
1298
|
+
|
|
1299
|
+
plot_xdata, plot_equity = zip(*valid_data)
|
|
1300
|
+
plot_xdata = list(plot_xdata)
|
|
1301
|
+
plot_equity = list(plot_equity)
|
|
1302
|
+
|
|
1303
|
+
# Calculate percentage return from initial
|
|
1304
|
+
initial_value = plot_equity[0] if plot_equity[0] != 0 else 1
|
|
1305
|
+
pct_equity = [(v / initial_value - 1) * 100 for v in plot_equity]
|
|
1306
|
+
|
|
1307
|
+
# Calculate drawdown
|
|
1308
|
+
running_max = plot_equity[0]
|
|
1309
|
+
drawdowns = []
|
|
1310
|
+
for v in plot_equity:
|
|
1311
|
+
if v > running_max:
|
|
1312
|
+
running_max = v
|
|
1313
|
+
dd = ((v - running_max) / running_max) * 100 if running_max != 0 else 0
|
|
1314
|
+
drawdowns.append(dd)
|
|
1315
|
+
|
|
1316
|
+
max_dd = min(drawdowns) if drawdowns else 0
|
|
1317
|
+
|
|
1318
|
+
# Plot drawdown first (as filled area at bottom)
|
|
1319
|
+
fig.add_trace(
|
|
1320
|
+
go.Scatter(
|
|
1321
|
+
x=plot_xdata,
|
|
1322
|
+
y=drawdowns,
|
|
1323
|
+
mode="lines",
|
|
1324
|
+
name=f"Drawdown (Max: {max_dd:.2f}%)",
|
|
1325
|
+
line={"color": "#E74C3C", "width": 1},
|
|
1326
|
+
fill="tozeroy",
|
|
1327
|
+
fillcolor="rgba(231, 76, 60, 0.3)",
|
|
1328
|
+
hovertemplate="Drawdown: %{y:.2f}%<extra></extra>",
|
|
1329
|
+
),
|
|
1330
|
+
row=row,
|
|
1331
|
+
col=1,
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
# Plot equity curve on top
|
|
1335
|
+
fig.add_trace(
|
|
1336
|
+
go.Scatter(
|
|
1337
|
+
x=plot_xdata,
|
|
1338
|
+
y=pct_equity,
|
|
1339
|
+
mode="lines",
|
|
1340
|
+
name="Return %",
|
|
1341
|
+
line={"color": self.p.scheme.equity_color, "width": 2},
|
|
1342
|
+
hovertemplate="Return: %{y:.2f}%<extra></extra>",
|
|
1343
|
+
),
|
|
1344
|
+
row=row,
|
|
1345
|
+
col=1,
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
# Add zero line
|
|
1349
|
+
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5, row=row, col=1)
|
|
1350
|
+
|
|
1351
|
+
return row + 1
|