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