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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. back_trader_python-1.4.0.dist-info/METADATA +1491 -0
  2. back_trader_python-1.4.0.dist-info/RECORD +465 -0
  3. back_trader_python-1.4.0.dist-info/WHEEL +5 -0
  4. back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
  5. back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
  6. backtrader/__init__.py +148 -0
  7. backtrader/_cerebro/__init__.py +5 -0
  8. backtrader/_cerebro/channel.py +382 -0
  9. backtrader/_cerebro/execution.py +377 -0
  10. backtrader/_cerebro/lifecycle.py +143 -0
  11. backtrader/_cerebro/notifications.py +150 -0
  12. backtrader/_cerebro/presentation.py +230 -0
  13. backtrader/_cerebro/registry.py +593 -0
  14. backtrader/_cerebro/runnext.py +551 -0
  15. backtrader/_cerebro/runonce.py +142 -0
  16. backtrader/analyzer.py +594 -0
  17. backtrader/analyzers/__init__.py +50 -0
  18. backtrader/analyzers/annualreturn.py +226 -0
  19. backtrader/analyzers/calmar.py +165 -0
  20. backtrader/analyzers/drawdown.py +287 -0
  21. backtrader/analyzers/leverage.py +112 -0
  22. backtrader/analyzers/logreturnsrolling.py +190 -0
  23. backtrader/analyzers/periodstats.py +153 -0
  24. backtrader/analyzers/positions.py +119 -0
  25. backtrader/analyzers/pyfolio.py +470 -0
  26. backtrader/analyzers/returns.py +192 -0
  27. backtrader/analyzers/sharpe.py +307 -0
  28. backtrader/analyzers/sharpe_ratio_stats.py +534 -0
  29. backtrader/analyzers/sqn.py +112 -0
  30. backtrader/analyzers/timereturn.py +192 -0
  31. backtrader/analyzers/total_value.py +75 -0
  32. backtrader/analyzers/tradeanalyzer.py +278 -0
  33. backtrader/analyzers/transactions.py +141 -0
  34. backtrader/analyzers/vwr.py +245 -0
  35. backtrader/bokeh/__init__.py +155 -0
  36. backtrader/bokeh/analyzers/__init__.py +13 -0
  37. backtrader/bokeh/analyzers/plot.py +192 -0
  38. backtrader/bokeh/analyzers/recorder.py +181 -0
  39. backtrader/bokeh/app.py +1094 -0
  40. backtrader/bokeh/live/__init__.py +11 -0
  41. backtrader/bokeh/live/client.py +352 -0
  42. backtrader/bokeh/live/datahandler.py +346 -0
  43. backtrader/bokeh/plot_adapter.py +200 -0
  44. backtrader/bokeh/schemes/__init__.py +14 -0
  45. backtrader/bokeh/schemes/blackly.py +76 -0
  46. backtrader/bokeh/schemes/scheme.py +150 -0
  47. backtrader/bokeh/schemes/tradimo.py +82 -0
  48. backtrader/bokeh/tab.py +125 -0
  49. backtrader/bokeh/tabs/__init__.py +30 -0
  50. backtrader/bokeh/tabs/analyzer.py +120 -0
  51. backtrader/bokeh/tabs/config.py +154 -0
  52. backtrader/bokeh/tabs/live.py +109 -0
  53. backtrader/bokeh/tabs/log.py +185 -0
  54. backtrader/bokeh/tabs/metadata.py +182 -0
  55. backtrader/bokeh/tabs/performance.py +359 -0
  56. backtrader/bokeh/tabs/source.py +70 -0
  57. backtrader/bokeh/utils/__init__.py +8 -0
  58. backtrader/bokeh/utils/helpers.py +167 -0
  59. backtrader/bokeh/webapp.py +164 -0
  60. backtrader/broker.py +478 -0
  61. backtrader/brokers/__init__.py +36 -0
  62. backtrader/brokers/bbroker.py +2576 -0
  63. backtrader/brokers/btapibroker.py +8227 -0
  64. backtrader/brokers/hft/__init__.py +89 -0
  65. backtrader/brokers/hft/binance_bbo.py +625 -0
  66. backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
  67. backtrader/brokers/hft/examples.py +1228 -0
  68. backtrader/brokers/hft/exchange.py +380 -0
  69. backtrader/brokers/hft/latency.py +309 -0
  70. backtrader/brokers/hft/matching_core.py +572 -0
  71. backtrader/brokers/hft/queue.py +238 -0
  72. backtrader/brokers/hft/recorder.py +88 -0
  73. backtrader/brokers/hft/state.py +138 -0
  74. backtrader/brokers/impact_models.py +118 -0
  75. backtrader/brokers/mixbroker.py +895 -0
  76. backtrader/brokers/tickbroker.py +1991 -0
  77. backtrader/btrun/__init__.py +12 -0
  78. backtrader/btrun/btrun.py +1218 -0
  79. backtrader/cerebro.py +828 -0
  80. backtrader/channel.py +682 -0
  81. backtrader/channels/__init__.py +23 -0
  82. backtrader/channels/bridge.py +186 -0
  83. backtrader/channels/funding.py +248 -0
  84. backtrader/channels/live_queue.py +216 -0
  85. backtrader/channels/live_validator.py +294 -0
  86. backtrader/channels/orderbook.py +257 -0
  87. backtrader/channels/tick.py +202 -0
  88. backtrader/comminfo.py +665 -0
  89. backtrader/commissions/__init__.py +106 -0
  90. backtrader/commissions/ctpoption.py +993 -0
  91. backtrader/configs/account_config_example.yaml +8 -0
  92. backtrader/dataseries.py +379 -0
  93. backtrader/errors.py +106 -0
  94. backtrader/events.py +980 -0
  95. backtrader/feed.py +1523 -0
  96. backtrader/feeds/__init__.py +75 -0
  97. backtrader/feeds/barrier.py +2006 -0
  98. backtrader/feeds/blaze.py +118 -0
  99. backtrader/feeds/btapifeed.py +1538 -0
  100. backtrader/feeds/btcsv.py +203 -0
  101. backtrader/feeds/chainer.py +114 -0
  102. backtrader/feeds/cryptohftdata.py +164 -0
  103. backtrader/feeds/csvgeneric.py +1205 -0
  104. backtrader/feeds/ctpcohort.py +1051 -0
  105. backtrader/feeds/influxfeed.py +158 -0
  106. backtrader/feeds/livefeed.py +71 -0
  107. backtrader/feeds/mixed_channel.py +108 -0
  108. backtrader/feeds/mt4csv.py +42 -0
  109. backtrader/feeds/pandafeed.py +381 -0
  110. backtrader/feeds/quandl.py +256 -0
  111. backtrader/feeds/rollover.py +229 -0
  112. backtrader/feeds/sierrachart.py +30 -0
  113. backtrader/feeds/vchart.py +162 -0
  114. backtrader/feeds/vchartcsv.py +84 -0
  115. backtrader/feeds/vchartfile.py +153 -0
  116. backtrader/feeds/yahoo.py +399 -0
  117. backtrader/fillers.py +148 -0
  118. backtrader/filters/__init__.py +34 -0
  119. backtrader/filters/bsplitter.py +127 -0
  120. backtrader/filters/calendardays.py +121 -0
  121. backtrader/filters/datafiller.py +192 -0
  122. backtrader/filters/datafilter.py +74 -0
  123. backtrader/filters/daysteps.py +96 -0
  124. backtrader/filters/heikinashi.py +63 -0
  125. backtrader/filters/renko.py +164 -0
  126. backtrader/filters/session.py +289 -0
  127. backtrader/flt.py +80 -0
  128. backtrader/functions.py +960 -0
  129. backtrader/indicator.py +449 -0
  130. backtrader/indicators/__init__.py +148 -0
  131. backtrader/indicators/accdecoscillator.py +110 -0
  132. backtrader/indicators/aroon.py +300 -0
  133. backtrader/indicators/atr.py +315 -0
  134. backtrader/indicators/awesomeoscillator.py +122 -0
  135. backtrader/indicators/basicops.py +834 -0
  136. backtrader/indicators/bollinger.py +223 -0
  137. backtrader/indicators/cci.py +89 -0
  138. backtrader/indicators/channels_ext.py +83 -0
  139. backtrader/indicators/contrib/__init__.py +228 -0
  140. backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
  141. backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
  142. backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
  143. backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
  144. backtrader/indicators/contrib/adxdmi.py +34 -0
  145. backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
  146. backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
  147. backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
  148. backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
  149. backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
  150. backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
  151. backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
  152. backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
  153. backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
  154. backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
  155. backtrader/indicators/contrib/average_change_candle.py +165 -0
  156. backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
  157. backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
  158. backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
  159. backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
  160. backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
  161. backtrader/indicators/contrib/blau_csi.py +76 -0
  162. backtrader/indicators/contrib/blau_ergodic.py +53 -0
  163. backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
  164. backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
  165. backtrader/indicators/contrib/blau_tvi.py +55 -0
  166. backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
  167. backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
  168. backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
  169. backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
  170. backtrader/indicators/contrib/bsi_indicator.py +87 -0
  171. backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
  172. backtrader/indicators/contrib/bulls_power.py +56 -0
  173. backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
  174. backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
  175. backtrader/indicators/contrib/candle_stop_color.py +46 -0
  176. backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
  177. backtrader/indicators/contrib/candlesticks_bw.py +45 -0
  178. backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
  179. backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
  180. backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
  181. backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
  182. backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
  183. backtrader/indicators/contrib/cg_oscillator.py +40 -0
  184. backtrader/indicators/contrib/close_line_cci.py +38 -0
  185. backtrader/indicators/contrib/close_price_fractals.py +47 -0
  186. backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
  187. backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
  188. backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
  189. backtrader/indicators/contrib/color_hma.py +71 -0
  190. backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
  191. backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
  192. backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
  193. backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
  194. backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
  195. backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
  196. backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
  197. backtrader/indicators/contrib/color_x2_ma.py +49 -0
  198. backtrader/indicators/contrib/color_x_derivative.py +63 -0
  199. backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
  200. backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
  201. backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
  202. backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
  203. backtrader/indicators/contrib/derivative_indicator.py +95 -0
  204. backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
  205. backtrader/indicators/contrib/digital_macd.py +200 -0
  206. backtrader/indicators/contrib/donchian_channels_system.py +45 -0
  207. backtrader/indicators/contrib/dots_indicator.py +93 -0
  208. backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
  209. backtrader/indicators/contrib/ema_rsi_va.py +80 -0
  210. backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
  211. backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
  212. backtrader/indicators/contrib/fatl_filter.py +179 -0
  213. backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
  214. backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
  215. backtrader/indicators/contrib/fisher_org_v1.py +102 -0
  216. backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
  217. backtrader/indicators/contrib/force_index_ema.py +96 -0
  218. backtrader/indicators/contrib/force_index_ema_2.py +27 -0
  219. backtrader/indicators/contrib/forecast_oscilator.py +145 -0
  220. backtrader/indicators/contrib/fractal_amambk.py +81 -0
  221. backtrader/indicators/contrib/frama_series.py +84 -0
  222. backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
  223. backtrader/indicators/contrib/go_indicator.py +93 -0
  224. backtrader/indicators/contrib/hlr_indicator.py +95 -0
  225. backtrader/indicators/contrib/hma.py +50 -0
  226. backtrader/indicators/contrib/i4_drfv2.py +34 -0
  227. backtrader/indicators/contrib/i4_drfv3.py +38 -0
  228. backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
  229. backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
  230. backtrader/indicators/contrib/i_gap_indicator.py +45 -0
  231. backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
  232. backtrader/indicators/contrib/i_trend_indicator.py +125 -0
  233. backtrader/indicators/contrib/iamma_indicator.py +39 -0
  234. backtrader/indicators/contrib/indexed_moving_average.py +33 -0
  235. backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
  236. backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
  237. backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
  238. backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
  239. backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
  240. backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
  241. backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
  242. backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
  243. backtrader/indicators/contrib/kalman_filter_line.py +127 -0
  244. backtrader/indicators/contrib/kama_indicator.py +150 -0
  245. backtrader/indicators/contrib/karacatica_indicator.py +99 -0
  246. backtrader/indicators/contrib/kdj_indicator.py +59 -0
  247. backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
  248. backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
  249. backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
  250. backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
  251. backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
  252. backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
  253. backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
  254. backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
  255. backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
  256. backtrader/indicators/contrib/loco_indicator.py +88 -0
  257. backtrader/indicators/contrib/lrma_indicator.py +185 -0
  258. backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
  259. backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
  260. backtrader/indicators/contrib/macd2_indicator.py +61 -0
  261. backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
  262. backtrader/indicators/contrib/malr_indicator.py +77 -0
  263. backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
  264. backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
  265. backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
  266. backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
  267. backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
  268. backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
  269. backtrader/indicators/contrib/nrtr_indicator.py +95 -0
  270. backtrader/indicators/contrib/p_channel_system.py +40 -0
  271. backtrader/indicators/contrib/percent_envelope.py +37 -0
  272. backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
  273. backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
  274. backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
  275. backtrader/indicators/contrib/price_extreme_channel.py +35 -0
  276. backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
  277. backtrader/indicators/contrib/ravi_indicator.py +40 -0
  278. backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
  279. backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
  280. backtrader/indicators/contrib/renko_level.py +85 -0
  281. backtrader/indicators/contrib/renko_line_break.py +91 -0
  282. backtrader/indicators/contrib/rftl_indicator.py +41 -0
  283. backtrader/indicators/contrib/rkd_indicator.py +53 -0
  284. backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
  285. backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
  286. backtrader/indicators/contrib/rsi_slowdown.py +57 -0
  287. backtrader/indicators/contrib/rsioma_v2.py +41 -0
  288. backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
  289. backtrader/indicators/contrib/safe_adx.py +89 -0
  290. backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
  291. backtrader/indicators/contrib/sidus_indicator.py +105 -0
  292. backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
  293. backtrader/indicators/contrib/sliding_range_color.py +56 -0
  294. backtrader/indicators/contrib/slow_stoch.py +42 -0
  295. backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
  296. backtrader/indicators/contrib/smoothed_rsi.py +31 -0
  297. backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
  298. backtrader/indicators/contrib/stalin_indicator.py +152 -0
  299. backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
  300. backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
  301. backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
  302. backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
  303. backtrader/indicators/contrib/t3_average.py +76 -0
  304. backtrader/indicators/contrib/t3_indicator.py +40 -0
  305. backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
  306. backtrader/indicators/contrib/three_candles_indicator.py +70 -0
  307. backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
  308. backtrader/indicators/contrib/time_line.py +57 -0
  309. backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
  310. backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
  311. backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
  312. backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
  313. backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
  314. backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
  315. backtrader/indicators/contrib/trigger_line.py +66 -0
  316. backtrader/indicators/contrib/triple_ema_rate.py +34 -0
  317. backtrader/indicators/contrib/trvi_indicator.py +194 -0
  318. backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
  319. backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
  320. backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
  321. backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
  322. backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
  323. backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
  324. backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
  325. backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
  326. backtrader/indicators/contrib/vwma_candle.py +57 -0
  327. backtrader/indicators/contrib/vwma_digit_system.py +70 -0
  328. backtrader/indicators/contrib/wami.py +43 -0
  329. backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
  330. backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
  331. backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
  332. backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
  333. backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
  334. backtrader/indicators/contrib/xma_ichimoku.py +163 -0
  335. backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
  336. backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
  337. backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
  338. backtrader/indicators/contrib/xmacd_indicator.py +70 -0
  339. backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
  340. backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
  341. backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
  342. backtrader/indicators/contrib/xrvi_indicator.py +130 -0
  343. backtrader/indicators/contrib/zero_lag_macd.py +36 -0
  344. backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
  345. backtrader/indicators/contrib/zpf_indicator.py +115 -0
  346. backtrader/indicators/crossover.py +337 -0
  347. backtrader/indicators/dema.py +175 -0
  348. backtrader/indicators/demarker.py +270 -0
  349. backtrader/indicators/deviation.py +284 -0
  350. backtrader/indicators/directionalmove.py +1071 -0
  351. backtrader/indicators/dma.py +112 -0
  352. backtrader/indicators/dpo.py +96 -0
  353. backtrader/indicators/dv2.py +56 -0
  354. backtrader/indicators/ema.py +145 -0
  355. backtrader/indicators/envelope.py +475 -0
  356. backtrader/indicators/hadelta.py +198 -0
  357. backtrader/indicators/heikinashi.py +153 -0
  358. backtrader/indicators/hma.py +153 -0
  359. backtrader/indicators/hurst.py +151 -0
  360. backtrader/indicators/ichimoku.py +267 -0
  361. backtrader/indicators/kama.py +181 -0
  362. backtrader/indicators/kst.py +159 -0
  363. backtrader/indicators/lrsi.py +125 -0
  364. backtrader/indicators/mabase.py +147 -0
  365. backtrader/indicators/macd.py +322 -0
  366. backtrader/indicators/momentum.py +267 -0
  367. backtrader/indicators/moneyflow.py +237 -0
  368. backtrader/indicators/mt5atr.py +124 -0
  369. backtrader/indicators/myind.py +179 -0
  370. backtrader/indicators/obv.py +94 -0
  371. backtrader/indicators/ols.py +265 -0
  372. backtrader/indicators/oscillator.py +161 -0
  373. backtrader/indicators/percentchange.py +83 -0
  374. backtrader/indicators/percentrank.py +46 -0
  375. backtrader/indicators/pivotpoint.py +469 -0
  376. backtrader/indicators/prettygoodoscillator.py +113 -0
  377. backtrader/indicators/priceops_ext.py +123 -0
  378. backtrader/indicators/priceoscillator.py +262 -0
  379. backtrader/indicators/psar.py +212 -0
  380. backtrader/indicators/rmi.py +69 -0
  381. backtrader/indicators/rsi.py +440 -0
  382. backtrader/indicators/sma.py +141 -0
  383. backtrader/indicators/smma.py +116 -0
  384. backtrader/indicators/spread.py +54 -0
  385. backtrader/indicators/stochastic.py +263 -0
  386. backtrader/indicators/supertrend.py +436 -0
  387. backtrader/indicators/trend_ext.py +105 -0
  388. backtrader/indicators/trix.py +202 -0
  389. backtrader/indicators/tsi.py +155 -0
  390. backtrader/indicators/ultimateoscillator.py +158 -0
  391. backtrader/indicators/vortex.py +62 -0
  392. backtrader/indicators/williams.py +194 -0
  393. backtrader/indicators/wma.py +103 -0
  394. backtrader/indicators/zlema.py +135 -0
  395. backtrader/indicators/zlind.py +104 -0
  396. backtrader/linebuffer.py +3155 -0
  397. backtrader/lineiterator.py +2911 -0
  398. backtrader/lineroot.py +1106 -0
  399. backtrader/lineseries.py +2559 -0
  400. backtrader/live_trading/__init__.py +31 -0
  401. backtrader/live_trading/interface.py +404 -0
  402. backtrader/mathsupport.py +94 -0
  403. backtrader/metabase.py +1804 -0
  404. backtrader/mixins/__init__.py +21 -0
  405. backtrader/mixins/singleton.py +118 -0
  406. backtrader/observer.py +106 -0
  407. backtrader/observers/__init__.py +45 -0
  408. backtrader/observers/benchmark.py +126 -0
  409. backtrader/observers/broker.py +184 -0
  410. backtrader/observers/buysell.py +144 -0
  411. backtrader/observers/drawdown.py +161 -0
  412. backtrader/observers/logreturns.py +113 -0
  413. backtrader/observers/timereturn.py +86 -0
  414. backtrader/observers/trade_logger.py +2972 -0
  415. backtrader/observers/tradelogger.py +6 -0
  416. backtrader/observers/trades.py +258 -0
  417. backtrader/order.py +1114 -0
  418. backtrader/parameters.py +2345 -0
  419. backtrader/plot/__init__.py +54 -0
  420. backtrader/plot/finance.py +1022 -0
  421. backtrader/plot/formatters.py +200 -0
  422. backtrader/plot/locator.py +353 -0
  423. backtrader/plot/multicursor.py +495 -0
  424. backtrader/plot/plot.py +2500 -0
  425. backtrader/plot/plot_plotly.py +1351 -0
  426. backtrader/plot/scheme.py +253 -0
  427. backtrader/plot/utils.py +104 -0
  428. backtrader/position.py +290 -0
  429. backtrader/position_modes.py +132 -0
  430. backtrader/profiles.py +254 -0
  431. backtrader/reports/__init__.py +39 -0
  432. backtrader/reports/charts.py +371 -0
  433. backtrader/reports/performance.py +620 -0
  434. backtrader/reports/reporter.py +660 -0
  435. backtrader/resamplerfilter.py +1001 -0
  436. backtrader/signal.py +118 -0
  437. backtrader/signals/__init__.py +17 -0
  438. backtrader/sizer.py +114 -0
  439. backtrader/sizers/__init__.py +26 -0
  440. backtrader/sizers/fixedsize.py +161 -0
  441. backtrader/sizers/percents_sizer.py +119 -0
  442. backtrader/store.py +221 -0
  443. backtrader/stores/__init__.py +33 -0
  444. backtrader/stores/btapistore.py +15506 -0
  445. backtrader/stores/livestore.py +137 -0
  446. backtrader/stores/vchartfile.py +96 -0
  447. backtrader/strategy.py +3655 -0
  448. backtrader/talib.py +280 -0
  449. backtrader/test_helpers.py +96 -0
  450. backtrader/timer.py +358 -0
  451. backtrader/trade.py +442 -0
  452. backtrader/tradingcal.py +361 -0
  453. backtrader/utils/__init__.py +68 -0
  454. backtrader/utils/autodict.py +251 -0
  455. backtrader/utils/date.py +71 -0
  456. backtrader/utils/dateintern.py +509 -0
  457. backtrader/utils/flushfile.py +94 -0
  458. backtrader/utils/fractal.py +101 -0
  459. backtrader/utils/get_metrics.py +101 -0
  460. backtrader/utils/load_data.py +209 -0
  461. backtrader/utils/log_message.py +998 -0
  462. backtrader/utils/ordereddefaultdict.py +75 -0
  463. backtrader/utils/py3.py +296 -0
  464. backtrader/version.py +21 -0
  465. backtrader/writer.py +372 -0
@@ -0,0 +1,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