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
backtrader/strategy.py ADDED
@@ -0,0 +1,3655 @@
1
+ #!/usr/bin/env python
2
+ """Strategy module - Base class for user-defined trading strategies.
3
+
4
+ This module provides the Strategy class which serves as the foundation for
5
+ all user-defined trading strategies in Backtrader. It handles order management,
6
+ position tracking, indicator integration, and the event-driven execution model.
7
+
8
+ Key Features:
9
+ - Order creation and management (buy, sell, close, cancel)
10
+ - Position tracking per data feed
11
+ - Integration with indicators and analyzers
12
+ - Event notifications (order, trade, data, timer)
13
+ - Support for multiple data feeds and timeframes
14
+ - Signal-based trading via SignalStrategy
15
+
16
+ Example:
17
+ Basic strategy implementation::
18
+
19
+ import backtrader as bt
20
+
21
+ class MyStrategy(bt.Strategy):
22
+ params = (('period', 20),)
23
+
24
+ def __init__(self):
25
+ self.sma = bt.indicators.SMA(period=self.p.period)
26
+
27
+ def next(self):
28
+ if self.data.close[0] > self.sma[0]:
29
+ self.buy()
30
+ elif self.data.close[0] < self.sma[0]:
31
+ self.sell()
32
+
33
+ Classes:
34
+ Strategy: Main base class for trading strategies.
35
+ SignalStrategy: Strategy subclass that responds to signal indicators.
36
+ """
37
+
38
+ from __future__ import absolute_import, division, print_function, unicode_literals
39
+
40
+ import collections
41
+ import copy
42
+ import datetime
43
+ import itertools
44
+ from typing import Optional
45
+
46
+ from .lineiterator import LineIterator, StrategyBase
47
+ from .lineroot import LineRoot, LineSingle
48
+ from .lineseries import LineSeriesStub
49
+ from .metabase import ItemCollection, OwnerContext, findowner
50
+ from .order import Order
51
+ from .parameters import make_legacy_parameter_accessor
52
+ from .position_modes import (
53
+ POSITION_MODE_DUAL_SIDE,
54
+ POSITION_OFFSET_CLOSE,
55
+ POSITION_SIDE_LONG,
56
+ POSITION_SIDE_SHORT,
57
+ normalize_position_mode,
58
+ normalize_position_side,
59
+ trade_key_from_order,
60
+ )
61
+ from .signal import (
62
+ SIGNAL_LONG,
63
+ SIGNAL_LONG_ANY,
64
+ SIGNAL_LONG_INV,
65
+ SIGNAL_LONGEXIT,
66
+ SIGNAL_LONGEXIT_ANY,
67
+ SIGNAL_LONGEXIT_INV,
68
+ SIGNAL_LONGSHORT,
69
+ SIGNAL_SHORT,
70
+ SIGNAL_SHORT_ANY,
71
+ SIGNAL_SHORT_INV,
72
+ SIGNAL_SHORTEXIT,
73
+ SIGNAL_SHORTEXIT_ANY,
74
+ SIGNAL_SHORTEXIT_INV,
75
+ )
76
+ from .sizers.fixedsize import FixedSize
77
+ from .trade import Trade
78
+ from .utils import AutoDictList, AutoOrderedDict
79
+ from .utils.log_message import SpdLogManager, get_logger
80
+ from .utils.py3 import MAXINT, filter, integer_types, iteritems, keys, map, string_types
81
+
82
+ logger = get_logger(__name__)
83
+ _INF = float("inf")
84
+
85
+
86
+ def _set_current_datetime(line, value):
87
+ """Set current datetime line slot directly when no bindings need propagation."""
88
+ try:
89
+ bindings = line.bindings
90
+ except AttributeError:
91
+ line[0] = value
92
+ return
93
+
94
+ if bindings:
95
+ line[0] = value
96
+ return
97
+
98
+ try:
99
+ idx = line._idx
100
+ array = line.array
101
+ except AttributeError:
102
+ line[0] = value
103
+ return
104
+
105
+ if idx < 0:
106
+ line[0] = value
107
+ return
108
+
109
+ try:
110
+ array[idx] = value if value >= 1.0 else 1.0
111
+ except IndexError:
112
+ line[0] = value
113
+
114
+
115
+ class Strategy(StrategyBase):
116
+ """Base class for user-defined trading strategies.
117
+
118
+ This class provides the core functionality for implementing trading
119
+ strategies including order management, position tracking, and event
120
+ handling. Users should subclass this to create custom strategies.
121
+
122
+ Attributes:
123
+ env: Reference to the Cerebro environment.
124
+ cerebro: Alias for env.
125
+ broker: Reference to the broker for order execution.
126
+ datas: List of data feeds available to the strategy.
127
+ data: Shortcut to the first data feed (datas[0]).
128
+ position: Current position for the main data feed.
129
+ stats: Collection of observer instances.
130
+ analyzers: Collection of analyzer instances.
131
+
132
+ Methods to Override:
133
+ __init__: Initialize indicators and strategy state.
134
+ start: Called when the strategy starts running.
135
+ prenext: Called before minimum period is reached.
136
+ nextstart: Called once when minimum period is first reached.
137
+ next: Main strategy logic, called on each bar.
138
+ stop: Called when the strategy stops running.
139
+ notify_order: Receive order status notifications.
140
+ notify_trade: Receive trade notifications.
141
+ notify_data: Receive data feed notifications.
142
+ notify_timer: Receive timer notifications.
143
+
144
+ Example:
145
+ class MyStrategy(bt.Strategy):
146
+ params = (('period', 20),)
147
+
148
+ def __init__(self):
149
+ self.sma = bt.indicators.SMA(period=self.p.period)
150
+
151
+ def next(self):
152
+ if not self.position:
153
+ if self.data.close[0] > self.sma[0]:
154
+ self.buy()
155
+ else:
156
+ if self.data.close[0] < self.sma[0]:
157
+ self.close()
158
+ """
159
+
160
+ # Class-level storage for strategies
161
+ _indcol: dict = {}
162
+
163
+ @classmethod
164
+ def _create_strategy_safely(cls, *args, **kwargs):
165
+ """Safely create a strategy instance with proper parameter filtering.
166
+
167
+ Separates __new__ (parameter setup) from __init__ (data/indicator setup)
168
+ to ensure parameters are fully processed before __init__ runs.
169
+ """
170
+ # __new__ processes all kwargs into _params_instance
171
+ instance = cls.__new__(cls, *args, **kwargs)
172
+
173
+ # __init__ handles data setup, clock, and user subclass init.
174
+ # Pass original kwargs so __init__ can filter out param kwargs itself.
175
+ if instance is not None:
176
+ Strategy.__init__(instance, *args, **kwargs)
177
+
178
+ return instance
179
+
180
+ def __new__(cls, *args, **kwargs):
181
+ """Override __new__ to handle method renaming that was done in MetaStrategy"""
182
+ # CRITICAL: First call StrategyBase.__new__ to properly set up data arguments and lines
183
+ # This ensures strategies get their data arguments processed correctly
184
+ instance = super().__new__(cls, *args, **kwargs)
185
+
186
+ # Store the original kwargs for parameter processing
187
+ instance._strategy_init_kwargs = kwargs
188
+
189
+ # CRITICAL FIX: Manually set up parameters here since Strategy inherits from ParamsMixin
190
+ # But we need to ensure the kwargs from cerebro.addstrategy are properly processed
191
+ if hasattr(cls, "_params") and cls._params is not None:
192
+ params_cls = cls._params
193
+ param_names = set()
194
+
195
+ # Get all parameter names from the class
196
+ if hasattr(params_cls, "_getpairs"):
197
+ param_names.update(params_cls._getpairs().keys())
198
+ elif hasattr(params_cls, "_gettuple"):
199
+ param_names.update(key for key, value in params_cls._gettuple())
200
+
201
+ # Filter parameter kwargs
202
+ param_kwargs = {k: v for k, v in kwargs.items() if k in param_names}
203
+
204
+ # Create parameter instance
205
+ try:
206
+ instance._params_instance = params_cls()
207
+ except Exception as exc:
208
+ logger.error("strategy:208 re-raising Exception", exc_info=True)
209
+ raise TypeError(
210
+ f"Failed to create params instance for {cls.__name__}: {exc}"
211
+ ) from exc
212
+
213
+ # Set all parameter values - first defaults, then custom values
214
+ if hasattr(params_cls, "_getpairs"):
215
+ for key, value in params_cls._getpairs().items():
216
+ # Use custom value if provided, otherwise use default
217
+ final_value = param_kwargs.get(key, value)
218
+ setattr(instance._params_instance, key, final_value)
219
+ elif hasattr(params_cls, "_gettuple"):
220
+ for key, value in params_cls._gettuple():
221
+ # Use custom value if provided, otherwise use default
222
+ final_value = param_kwargs.get(key, value)
223
+ setattr(instance._params_instance, key, final_value)
224
+
225
+ # Set any extra parameters that were passed but not in the params definition
226
+ for key, value in param_kwargs.items():
227
+ if not hasattr(instance._params_instance, key):
228
+ setattr(instance._params_instance, key, value)
229
+
230
+ else:
231
+ instance._params_instance = make_legacy_parameter_accessor(
232
+ values=kwargs, name="ParamsInstance"
233
+ )
234
+
235
+ # Create p property for parameter access
236
+ instance.p = instance._params_instance
237
+
238
+ # Handle method renaming like the old MetaStrategy.__new__ did
239
+ if hasattr(cls, "notify") and not hasattr(cls, "notify_order"):
240
+ cls.notify_order = cls.notify
241
+ delattr(cls, "notify")
242
+ if hasattr(cls, "notify_operation") and not hasattr(cls, "notify_trade"):
243
+ cls.notify_trade = cls.notify_operation
244
+ delattr(cls, "notify_operation")
245
+
246
+ # Register subclasses (from MetaStrategy.__init__)
247
+ if (
248
+ not getattr(cls, "aliased", False)
249
+ and cls.__name__ != "Strategy"
250
+ and not cls.__name__.startswith("_")
251
+ ):
252
+ cls._indcol[cls.__name__] = cls
253
+
254
+ # Initialize critical attributes early (from MetaStrategy.donew and dopreinit)
255
+ # These need to be available before __init__ completes since methods might be called
256
+ from .cerebro import Cerebro
257
+
258
+ instance.env = instance.cerebro = cerebro = findowner(instance, Cerebro)
259
+ instance._id = cerebro._next_stid()
260
+ instance.broker = instance.env.broker
261
+ from .sizers import FixedSize
262
+
263
+ instance._sizer = FixedSize()
264
+
265
+ instance.stats = instance.observers = ItemCollection()
266
+ instance.analyzers = ItemCollection()
267
+ instance._alnames = collections.defaultdict(itertools.count)
268
+ instance.writers = []
269
+ instance._slave_analyzers = []
270
+ instance._tradehistoryon = False
271
+ instance._orders = []
272
+ instance._orderspending = []
273
+ instance._trades = collections.defaultdict(AutoDictList)
274
+ instance._tradespending = []
275
+
276
+ return instance
277
+
278
+ def __reduce_ex__(self, protocol):
279
+ """Restore saved state without re-entering the run-time owner setup.
280
+
281
+ Optimization results are unpickled outside an active OwnerContext.
282
+ Calling Strategy.__new__ there would request a new strategy ID from
283
+ a nonexistent Cerebro and kill the multiprocessing result thread.
284
+ Keep the default state/slot handling and any user-defined reducer,
285
+ changing only the allocation of an otherwise ordinary instance.
286
+ ``object.__new__`` also resolves in older framework installations.
287
+ """
288
+ reduced = super().__reduce_ex__(protocol)
289
+ if type(self).__reduce__ is not object.__reduce__:
290
+ return reduced
291
+ return (object.__new__, (type(self),), *reduced[2:])
292
+
293
+ def __init__(self, *args, **kwargs):
294
+ """Initialize with functionality from MetaStrategy methods"""
295
+ # Critical attributes already initialized in __new__
296
+ # Handle the functionality that was in MetaStrategy.dopostinit
297
+ self._sizer.set(self, self.broker)
298
+
299
+ # OPTIMIZED: Simple and fast data extraction from args
300
+ # Cerebro passes datas at the beginning of args (cerebro.py:1433)
301
+ if not hasattr(self, "datas") or not self.datas:
302
+ self.datas = []
303
+
304
+ # Quick method: Extract datas directly from args
305
+ # Cerebro prepends all datas to args, so we just need to identify them
306
+ if args:
307
+ for arg in args:
308
+ # Fast check: data feeds have 'lines' and 'datetime' attributes
309
+ if hasattr(arg, "lines") and hasattr(arg, "datetime"):
310
+ self.datas.append(arg)
311
+ # No need for nested loops or complex checks
312
+
313
+ # Fallback: Try cerebro.datas directly (fast)
314
+ if not self.datas and hasattr(self, "cerebro") and self.cerebro is not None:
315
+ if hasattr(self.cerebro, "datas") and self.cerebro.datas:
316
+ self.datas = list(self.cerebro.datas)
317
+
318
+ # Set up primary data reference and data0/data1 aliases
319
+ if self.datas:
320
+ self.data = self.datas[0]
321
+ for d, data in enumerate(self.datas):
322
+ setattr(self, f"data{d}", data)
323
+ else:
324
+ self.data = None
325
+ self._data_assignment_pending = False
326
+
327
+ # Set up clock - this is critical for strategy execution
328
+ if not hasattr(self, "_clock") or self._clock is None:
329
+ if self.datas:
330
+ self._clock = self.datas[0]
331
+ else:
332
+ self._clock = None
333
+ # CRITICAL FIX: Don't create MinimalClock fallback
334
+ # It causes problems with indicator clock detection in _periodset()
335
+ # If no datas, leave _clock as None and let it be set later
336
+
337
+ # Call user subclass __init__ if this is a Strategy subclass
338
+ if self.__class__ != Strategy:
339
+ from backtrader.lineiterator import StrategyBase
340
+
341
+ # Guard against recursive calls when user's __init__ calls super().__init__()
342
+ if not getattr(self, "_user_init_called", False):
343
+ self._user_init_called = True
344
+
345
+ for cls in self.__class__.__mro__:
346
+ if (
347
+ cls not in (Strategy, StrategyBase)
348
+ and hasattr(cls, "__init__")
349
+ and "__init__" in cls.__dict__
350
+ ):
351
+ # Use _original_init if available to avoid calling patched_init
352
+ # (prevents infinite recursion when ParamsMixin patches __init__)
353
+ if hasattr(cls, "_original_init"):
354
+ user_init = cls._original_init
355
+ else:
356
+ user_init = cls.__dict__["__init__"]
357
+ # Use OwnerContext so indicators find this strategy as owner
358
+ with OwnerContext.set_owner(self):
359
+ user_init(self)
360
+ break
361
+
362
+ # Initialize tick/channel callback state (auto, no manual init needed)
363
+ if not hasattr(self, "_tick_count"):
364
+ self._tick_count = 0
365
+ self._event_count = 0
366
+ self._last_tick = {}
367
+ self._last_ob = {}
368
+ self._last_funding = {}
369
+ if not hasattr(self, "_hft_data_refs"):
370
+ self._hft_data_refs = {}
371
+
372
+ # Initialize critical attributes that are expected by strategy execution
373
+ # These should be available before any user code runs
374
+ if not hasattr(self, "_dlens"):
375
+ self._dlens = [len(data) for data in self.datas]
376
+
377
+ # CRITICAL FIX: DO NOT call super().__init__() here!
378
+ # StrategyBase.__init__ already calls super().__init__() which eventually
379
+ # calls Strategy.__init__. Calling super() again would create infinite recursion.
380
+ # The parent initialization is already done by StrategyBase.
381
+
382
+ # Clean up the temporary attribute
383
+ if hasattr(self, "_strategy_init_kwargs"):
384
+ delattr(self, "_strategy_init_kwargs")
385
+
386
+ # Line type is strategy type
387
+ _ltype = LineIterator.StratType
388
+ # CSV default is True
389
+ csv = True
390
+ # Old clock update methodology, default is False
391
+ _oldsync = False # update the clock using old methodology: data 0
392
+
393
+ # Keep the latest delivered data date in the line
394
+ lines = ("datetime",)
395
+
396
+ def log(self, txt, dt=None, level="info"):
397
+ """Log a message with optional datetime.
398
+
399
+ This method provides basic logging functionality. For comprehensive
400
+ logging including orders, trades, positions, indicators, and signals,
401
+ use the TradeLogger observer.
402
+
403
+ Args:
404
+ txt: The message text to log.
405
+ dt: Optional datetime. If None, uses current bar datetime.
406
+ level: Log level ('info', 'warning', 'error', 'debug').
407
+
408
+ Example:
409
+ >>> self.log(f'Close price: {self.data.close[0]:.2f}')
410
+ >>> self.log('Warning message', level='warning')
411
+ """
412
+ dt = dt or self.datetime.datetime()
413
+ print(f"[{dt}] {txt}")
414
+
415
+ def _notify_signal_to_observers(self, action, size, price, data=None, reason=None):
416
+ """Notify all TradeLogger observers about a trading signal.
417
+
418
+ This is called automatically by buy() and sell() methods to record
419
+ signals without requiring user intervention.
420
+
421
+ Args:
422
+ action: 'buy' or 'sell'
423
+ size: Order size
424
+ price: Signal price
425
+ data: Data feed (optional)
426
+ reason: Signal reason (optional)
427
+ """
428
+ # Notify all observers that have log_signal method (TradeLogger)
429
+ if hasattr(self, "stats") and self.stats:
430
+ for observer in self.stats:
431
+ if hasattr(observer, "log_signal"):
432
+ data_name = getattr(data, "_name", None) if data else None
433
+ observer.log_signal(action, size, price, data_name, reason)
434
+
435
+ def _notify_order_to_observers(self, order):
436
+ """Notify all observers about an order status change.
437
+
438
+ Args:
439
+ order: The order object with status change
440
+ """
441
+ if hasattr(self, "stats") and self.stats:
442
+ for observer in self.stats:
443
+ if hasattr(observer, "notify_order"):
444
+ observer.notify_order(order)
445
+
446
+ def _notify_trade_to_observers(self, trade):
447
+ """Notify all observers about a trade.
448
+
449
+ Args:
450
+ trade: The trade object
451
+ """
452
+ if hasattr(self, "stats") and self.stats:
453
+ for observer in self.stats:
454
+ if hasattr(observer, "notify_trade"):
455
+ observer.notify_trade(trade)
456
+
457
+ def _notify_store_to_observers(self, msg, *args, **kwargs):
458
+ """Forward store notifications to observers that support runtime events."""
459
+ if hasattr(self, "stats") and self.stats:
460
+ for observer in self.stats:
461
+ if hasattr(observer, "notify_store_event"):
462
+ observer.notify_store_event(msg, *args, **kwargs)
463
+
464
+ def _notify_data_to_observers(self, data, status, *args, **kwargs):
465
+ """Forward data-feed notifications to observers that support runtime events."""
466
+ if hasattr(self, "stats") and self.stats:
467
+ for observer in self.stats:
468
+ if hasattr(observer, "notify_data_event"):
469
+ observer.notify_data_event(data, status, *args, **kwargs)
470
+
471
+ def _notify_tick_to_observers(self, tick):
472
+ """Forward tick events to observers that support tick logging."""
473
+ if hasattr(self, "stats") and self.stats:
474
+ for observer in self.stats:
475
+ if hasattr(observer, "notify_tick_event"):
476
+ observer.notify_tick_event(tick)
477
+
478
+ def _notify_bar_to_observers(self, bar):
479
+ """Forward bar events to observers that support bar logging."""
480
+ if hasattr(self, "stats") and self.stats:
481
+ for observer in self.stats:
482
+ if hasattr(observer, "notify_bar_event"):
483
+ observer.notify_bar_event(bar)
484
+
485
+ def qbuffer(self, savemem=0, replaying=False):
486
+ """Enable the memory saving schemes. Possible values for ``savemem``:
487
+
488
+ 0: No savings. Each line object keeps in memory all values
489
+
490
+ 1: All lines objects save memory, using the strict minimum needed
491
+
492
+ Negative values are meant to be used when plotting is required:
493
+
494
+ -1: Indicators at Strategy Level and Observers do not enable memory
495
+ savings (but anything declared below it does)
496
+
497
+ -2: Same as -1 plus activation of memory saving for any indicators
498
+ which has declared *plotinfo.plot* as False (will not be plotted)
499
+ """
500
+ # If savemem < 0
501
+ if savemem < 0:
502
+ # Get any attribute that labels itself as Indicator
503
+ for ind in self._lineiterators[self.IndType]:
504
+ # Check if this ind is a single line
505
+ subsave = isinstance(ind, (LineSingle,))
506
+ # If not a single line and savemem == -2, check plotinfo.plot
507
+ if not subsave and savemem < -1:
508
+ subsave = not ind.plotinfo.plot
509
+ # Apply memory saving based on subsave flag
510
+ ind.qbuffer(savemem=subsave)
511
+ # If savemem > 0
512
+ elif savemem > 0:
513
+ # Apply memory saving to all data feeds
514
+ for data in self.datas:
515
+ data.qbuffer(replaying=replaying)
516
+ # Apply memory saving to all lines
517
+ for line in self.lines:
518
+ line.qbuffer(savemem=1)
519
+ # Apply memory saving to all lineiterators based on the strategy
520
+ for itcls in self._lineiterators:
521
+ for it in self._lineiterators[itcls]:
522
+ it.qbuffer(savemem=1)
523
+ # If savemem == 0, no action needed
524
+ else:
525
+ pass
526
+
527
+ def _iter_strategy_lineactions(self):
528
+ """Yield LineActions stored as strategy attributes."""
529
+ from .linebuffer import LineActions
530
+
531
+ try:
532
+ cache = object.__getattribute__(self, "_strategy_lineactions_cache")
533
+ except AttributeError:
534
+ cache = None
535
+
536
+ if cache is not None:
537
+ yield from cache
538
+ return
539
+
540
+ seen = set()
541
+ registered = set()
542
+
543
+ def mark_registered(lineiter):
544
+ lineiter_id = id(lineiter)
545
+ if lineiter_id in registered:
546
+ return
547
+ registered.add(lineiter_id)
548
+
549
+ try:
550
+ child_lists = lineiter._lineiterators.values()
551
+ except AttributeError:
552
+ return
553
+
554
+ for children in child_lists:
555
+ for child in children:
556
+ mark_registered(child)
557
+
558
+ try:
559
+ for children in self._lineiterators.values():
560
+ for child in children:
561
+ mark_registered(child)
562
+ except AttributeError:
563
+ # No sub-iterator registry yet; nothing to pre-mark as registered.
564
+ logger.debug("strategy:548 ignored AttributeError")
565
+
566
+ def visit(value):
567
+ value_id = id(value)
568
+ if value_id in seen:
569
+ return
570
+ seen.add(value_id)
571
+
572
+ if isinstance(value, LineActions):
573
+ for attr_name in ("_parent_a", "_parent_b", "a", "b", "cond"):
574
+ try:
575
+ dependency = getattr(value, attr_name)
576
+ except AttributeError:
577
+ logger.debug("strategy:561 ignored AttributeError")
578
+ continue
579
+ yield from visit(dependency)
580
+
581
+ try:
582
+ args = value.args
583
+ except AttributeError:
584
+ args = ()
585
+ for dependency in args:
586
+ yield from visit(dependency)
587
+
588
+ if value_id not in registered:
589
+ yield value
590
+
591
+ return
592
+
593
+ if isinstance(value, dict):
594
+ for item in value.values():
595
+ yield from visit(item)
596
+ elif isinstance(value, (list, tuple, set, frozenset)):
597
+ for item in value:
598
+ yield from visit(item)
599
+
600
+ try:
601
+ attrs = object.__getattribute__(self, "__dict__")
602
+ except AttributeError:
603
+ attrs = {}
604
+
605
+ result = []
606
+ for attr_name, attr_value in attrs.items():
607
+ if attr_name in {"_strategy_lineactions_cache", "_strategy_next_lineactions_cache"}:
608
+ continue
609
+ result.extend(visit(attr_value))
610
+
611
+ cache = tuple(result)
612
+ object.__setattr__(self, "_strategy_lineactions_cache", cache)
613
+ yield from cache
614
+
615
+ def _get_strategy_lineactions(self):
616
+ try:
617
+ return object.__getattribute__(self, "_strategy_lineactions_cache")
618
+ except AttributeError:
619
+ return tuple(self._iter_strategy_lineactions())
620
+
621
+ def _get_strategy_next_lineactions(self):
622
+ try:
623
+ return object.__getattribute__(self, "_strategy_next_lineactions_cache")
624
+ except AttributeError:
625
+ # Cache not built yet; compute and store it below.
626
+ logger.debug("strategy:609 ignored AttributeError")
627
+
628
+ cache = tuple(
629
+ (lineaction, getattr(lineaction, "_clock", None))
630
+ for lineaction in self._get_strategy_lineactions()
631
+ if hasattr(lineaction, "_next")
632
+ )
633
+ object.__setattr__(self, "_strategy_next_lineactions_cache", cache)
634
+ return cache
635
+
636
+ def _stage2(self):
637
+ super()._stage2()
638
+ for lineaction in self._get_strategy_lineactions():
639
+ try:
640
+ lineaction._stage2()
641
+ except Exception:
642
+ logger.debug("Failed to stage2 strategy LineActions", exc_info=True)
643
+
644
+ def _stage1(self):
645
+ super()._stage1()
646
+ for lineaction in self._get_strategy_lineactions():
647
+ try:
648
+ lineaction._stage1()
649
+ except Exception:
650
+ logger.debug("Failed to stage1 strategy LineActions", exc_info=True)
651
+
652
+ def _next_strategy_lineactions(self):
653
+ """Advance LineActions stored directly on the strategy.
654
+
655
+ LineActions created as strategy attributes, such as bt.Cmp/bt.If/bt.Max,
656
+ are not LineIterator indicators. They still need to be evaluated before
657
+ user next/prenext/nextstart reads them.
658
+ """
659
+ for lineaction, clock in self._get_strategy_next_lineactions():
660
+ if clock is not None:
661
+ try:
662
+ if len(clock) <= len(lineaction):
663
+ try:
664
+ current_value = lineaction[0]
665
+ except Exception: # nosec B112
666
+ # Per-bar hot path: the line buffer may not be
667
+ # populated yet for this index. Skipping is the
668
+ # intended behaviour; logging here would fire every
669
+ # bar. No control-flow change.
670
+ continue
671
+ if current_value == current_value:
672
+ continue
673
+ lineaction.next()
674
+ continue
675
+ except Exception: # nosec B110
676
+ # Clock/value comparison unavailable; fall back to plain
677
+ # _next(). Hot path, intentionally silent (no logging).
678
+ pass
679
+
680
+ lineaction._next()
681
+
682
+ def _periodset(self):
683
+ """Calculate and set the minimum period required for strategy execution.
684
+
685
+ This method determines the minimum number of bars needed before
686
+ the strategy's next() method can be called, based on the minimum
687
+ periods of all indicators and data feeds.
688
+ """
689
+
690
+ def iter_indicator_tree(indicators):
691
+ seen = set()
692
+ stack = list(indicators)
693
+ while stack:
694
+ lineiter = stack.pop(0)
695
+ lineiter_id = id(lineiter)
696
+ if lineiter_id in seen:
697
+ continue
698
+ seen.add(lineiter_id)
699
+ yield lineiter
700
+
701
+ try:
702
+ children = lineiter._lineiterators[LineIterator.IndType]
703
+ except (AttributeError, KeyError):
704
+ logger.debug("strategy:687 ignored AttributeError,KeyError")
705
+ continue
706
+ stack.extend(children)
707
+
708
+ # Data IDs
709
+ dataids = [id(data) for data in self.datas]
710
+ # Data minimum periods
711
+ _dminperiods = collections.defaultdict(list)
712
+ # Loop through all indicators
713
+ all_indicators = list(iter_indicator_tree(self._lineiterators[LineIterator.IndType]))
714
+
715
+ # CRITICAL FIX: bind secondary-feed indicator advance clocks now that
716
+ # the whole indicator tree is built. During construction, an indicator
717
+ # built on another indicator or a LinesOperation that follows a
718
+ # non-primary feed (e.g. SMA((h1.high + h1.low) / 2.0) or
719
+ # EMA(EMA(h4.close))) often has its clock defaulted to the strategy's
720
+ # primary feed because the parent clocks were not yet finalized. That
721
+ # makes the indicator warm up and emit values on the primary (fast)
722
+ # clock instead of the secondary (slow) feed in runonce mode. Here every
723
+ # clock is final, so we resolve each indicator's data dependency to the
724
+ # concrete feed it follows and, when that feed is a *secondary* feed,
725
+ # pin _resolved_secondary_clock to it (used by the runonce advance
726
+ # loop and Indicator.advance). See docs/DEV_REGRESSION_FAILURES.md.
727
+ from .lineiterator import _line_like_source_clock as _llsc
728
+
729
+ primary_feed = self.datas[0] if self.datas else None
730
+
731
+ # Map each feed's individual lines back to the owning feed so a clock
732
+ # that resolves to a feed *line* (e.g. data.high) can be attributed to
733
+ # its feed.
734
+ _line_to_feed = {}
735
+ for _data in self.datas:
736
+ _dlines = getattr(_data, "lines", None)
737
+ if _dlines is None:
738
+ continue
739
+ try:
740
+ for _ln in _dlines:
741
+ _line_to_feed[id(_ln)] = _data
742
+ except TypeError:
743
+ # Feed lines not iterable; skip mapping this feed's lines.
744
+ logger.debug("strategy:726 ignored TypeError")
745
+
746
+ def _feed_of(node, _seen=None):
747
+ """Resolve a data node to the concrete feed it ultimately follows."""
748
+ if _seen is None:
749
+ _seen = set()
750
+ if node is None or id(node) in _seen:
751
+ return None
752
+ _seen.add(id(node))
753
+ if id(node) in dataids:
754
+ return node
755
+ if id(node) in _line_to_feed:
756
+ return _line_to_feed[id(node)]
757
+ try:
758
+ src = _llsc(node)
759
+ except Exception:
760
+ logger.warning("strategy:745 fallback on Exception")
761
+ src = None
762
+ if src is not None and src is not node:
763
+ if id(src) in dataids:
764
+ return src
765
+ if id(src) in _line_to_feed:
766
+ return _line_to_feed[id(src)]
767
+ found = _feed_of(src, _seen)
768
+ if found is not None:
769
+ return found
770
+ nxt = getattr(node, "_clock", None)
771
+ if nxt is not None and nxt.__class__.__name__ != "MinimalClock":
772
+ found = _feed_of(nxt, _seen)
773
+ if found is not None:
774
+ return found
775
+ ndatas = getattr(node, "datas", None)
776
+ if ndatas:
777
+ found = _feed_of(ndatas[0], _seen)
778
+ if found is not None:
779
+ return found
780
+ # Walk owner references: a node may be an indicator output line
781
+ # (LineBuffer) whose owning indicator follows the target feed. The
782
+ # owner can be the indicator directly, or a Lines container whose
783
+ # _owner_ref points to it.
784
+ for owner_attr in ("_owner", "_owner_ref"):
785
+ owner = getattr(node, owner_attr, None)
786
+ if owner is not None and id(owner) not in _seen:
787
+ # A Lines container exposes _owner_ref to the real owner.
788
+ ref = getattr(owner, "_owner_ref", None)
789
+ if ref is not None and id(ref) not in _seen:
790
+ found = _feed_of(ref, _seen)
791
+ if found is not None:
792
+ return found
793
+ found = _feed_of(owner, _seen)
794
+ if found is not None:
795
+ return found
796
+ # Operands of a LinesOperation.
797
+ for op_attr in ("a", "b", "_parent_a", "_parent_b"):
798
+ operand = getattr(node, op_attr, None)
799
+ if operand is not None and id(operand) not in _seen:
800
+ found = _feed_of(operand, _seen)
801
+ if found is not None:
802
+ return found
803
+ return None
804
+
805
+ if primary_feed is not None and len(self.datas) > 1:
806
+ for lineiter in all_indicators:
807
+ idatas = getattr(lineiter, "datas", None)
808
+ if not idatas:
809
+ continue
810
+ feed = _feed_of(idatas[0])
811
+ if feed is not None and feed is not primary_feed and id(feed) in dataids:
812
+ # Pin only the advance clock used by the runonce post-phase
813
+ # loop and Indicator.advance(); deliberately do NOT change
814
+ # lineiter._clock here so the existing minperiod-to-feed
815
+ # attribution below (and thus the strategy warmup / bar_num)
816
+ # stays identical to the pre-fix behavior. Changing _clock
817
+ # perturbed multi-feed warmup for unrelated strategies.
818
+ lineiter._resolved_secondary_clock = feed
819
+
820
+ for lineiter in all_indicators:
821
+ # If multiple datas are used and multiple timeframes, the larger
822
+ # timeframe may place larger time constraints in calling next.
823
+ # Get the indicator's _clock attribute
824
+ clk = getattr(lineiter, "_clock", None)
825
+
826
+ # CRITICAL FIX: If clock is MinimalClock, use the indicator's actual data source
827
+ if (
828
+ clk is not None
829
+ and hasattr(clk, "__class__")
830
+ and "MinimalClock" in clk.__class__.__name__
831
+ ):
832
+ if self.datas:
833
+ # Find which data feed the indicator's data source belongs to
834
+ clock_set = False
835
+ if hasattr(lineiter, "datas") and lineiter.datas:
836
+ ind_data = lineiter.datas[0]
837
+ for data_feed in self.datas:
838
+ # Check if ind_data is the data feed itself
839
+ if ind_data is data_feed:
840
+ clk = data_feed
841
+ clock_set = True
842
+ break
843
+ # Check if ind_data is one of the lines of this data feed
844
+ if hasattr(data_feed, "lines") and ind_data in data_feed.lines:
845
+ clk = data_feed
846
+ clock_set = True
847
+ break
848
+ if not clock_set:
849
+ clk = self.datas[0]
850
+ lineiter._clock = clk # Update indicator's clock
851
+ else:
852
+ clk = None
853
+
854
+ # If the attribute value is None
855
+ if clk is None:
856
+ # Get the indicator's owner's _clock attribute value
857
+ clk = getattr(lineiter._owner, "_clock", None)
858
+ # CRITICAL FIX: If owner's clock is also MinimalClock, use data
859
+ if (
860
+ clk is not None
861
+ and hasattr(clk, "__class__")
862
+ and "MinimalClock" in clk.__class__.__name__
863
+ ):
864
+ if self.datas:
865
+ clk = self.datas[0]
866
+ else:
867
+ clk = None
868
+ if clk is None:
869
+ continue
870
+ # If clk is not None
871
+ while True:
872
+ # If clk is a data feed, break
873
+ if id(clk) in dataids:
874
+ break # already top-level clock (data feed)
875
+
876
+ # See if the current clock has higher level clocks
877
+ # Check if current clk has further _clock attribute
878
+ clk2 = getattr(clk, "_clock", None)
879
+ # If clk2 is None, get clk owner's _clock attribute value
880
+ if clk2 is None:
881
+ clk2 = getattr(clk._owner, "_clock", None)
882
+ if clk2 is None:
883
+ break # if no clock found, bail out
884
+ # If clk2 is not None, set clk to clk2
885
+ clk = clk2 # keep the ref and try to go up the hierarchy
886
+ # This check ensures clk is not None before proceeding
887
+ if clk is None:
888
+ continue # no clock found, go to next
889
+
890
+ # LineSeriesStub wraps a line and the clock is the wrapped line and
891
+ # not the wrapper itself.
892
+ # If clk is LineSeriesStub (multi-line object), get first line as clk
893
+ if isinstance(clk, LineSeriesStub):
894
+ clk = clk.lines[0]
895
+ # Save minimum period
896
+ _dminperiods[clk].append(lineiter._minperiod)
897
+
898
+ # Set minimum periods to empty list
899
+ self._minperiods = []
900
+ # Loop through all data feeds
901
+ for data in self.datas:
902
+ # Do not only consider the data as clock but also its lines, which
903
+ # may have been individually passed as clock references and
904
+ # discovered as clocks above
905
+
906
+ # Initialize with a data min period if any
907
+ # Minimum period needed for data to generate indicator lines
908
+ dlminperiods = _dminperiods[data]
909
+ # Loop through each line of data, add minperiods if line is in _dminperiods
910
+ for line in data.lines: # search each line for min periods
911
+ if line in _dminperiods:
912
+ dlminperiods += _dminperiods[line] # found, add it
913
+
914
+ # Keep the reference to the line if any was found
915
+ # If dlminperiods is not empty, calculate max value, else empty list
916
+ _dminperiods[data] = [max(dlminperiods)] if dlminperiods else []
917
+ # Data minimum period
918
+ dminperiod = max(_dminperiods[data] or [data._minperiod])
919
+ # Save minimum period to dminperiod
920
+ self._minperiods.append(dminperiod)
921
+
922
+ # Set the minperiod
923
+ # Indicator minimum periods
924
+ minperiods = [x._minperiod for x in all_indicators]
925
+
926
+ # Strategy-owned LineActions are not necessarily registered indicators.
927
+ # Original backtrader's metaclass machinery advanced them regardless of
928
+ # whether users kept them in public or private attributes. Reuse the
929
+ # recursive strategy-attribute scan here so private containers such as
930
+ # self._Type / self._OptionType participate in minperiod and execution.
931
+ strategy_lineactions = tuple(self._get_strategy_lineactions())
932
+ for attr in strategy_lineactions:
933
+ if (
934
+ hasattr(attr, "_minperiod")
935
+ and attr not in self._lineiterators[LineIterator.IndType]
936
+ ):
937
+ minperiods.append(attr._minperiod)
938
+
939
+ # Set strategy minimum period to max of indicator and data minperiods
940
+ self._minperiod = max(minperiods or [self._minperiod])
941
+
942
+ # Update _minperiods for strategy-owned LineActions, but only for their
943
+ # associated data. For multi-data strategies, LineActions minperiod
944
+ # should only affect the source data that clocks the expression.
945
+ if self._minperiods:
946
+ for attr in strategy_lineactions:
947
+ try:
948
+ if not hasattr(attr, "_minperiod"):
949
+ continue
950
+ # Try to determine which data this LineActions is associated
951
+ # with by checking its _clock or data sources.
952
+ data_idx = 0 # Default to data[0]
953
+ if hasattr(attr, "_clock") and attr._clock is not None:
954
+ for i, d in enumerate(self.datas):
955
+ if attr._clock is d or attr._clock in d.lines:
956
+ data_idx = i
957
+ break
958
+ # Only update minperiod for the specific data.
959
+ if data_idx < len(self._minperiods):
960
+ self._minperiods[data_idx] = max(
961
+ self._minperiods[data_idx], attr._minperiod
962
+ )
963
+ except (AttributeError, TypeError):
964
+ # Attribute access/typecheck failed; skip this attribute.
965
+ logger.debug("strategy:946 ignored AttributeError,TypeError")
966
+
967
+ def _addwriter(self, writer):
968
+ """Add a writer to the strategy.
969
+
970
+ Unlike the other _addxxx functions, this one receives an instance
971
+ because the writer works at cerebro level and is only passed to the
972
+ strategy to simplify the logic.
973
+ """
974
+ self.writers.append(writer)
975
+
976
+ def _addindicator(self, indcls, *indargs, **indkwargs):
977
+ """Add an indicator to the strategy.
978
+
979
+ Args:
980
+ indcls: Indicator class to instantiate
981
+ *indargs: Positional arguments for the indicator
982
+ **indkwargs: Keyword arguments for the indicator
983
+ """
984
+ indcls(*indargs, **indkwargs)
985
+
986
+ def _addanalyzer_slave(self, ancls, *anargs, **ankwargs):
987
+ """Add a slave analyzer for internal use.
988
+
989
+ Like _addanalyzer but meant for observers (or other entities) which
990
+ rely on the output of an analyzer for the data. These analyzers have
991
+ not been added by the user and are kept separate from the main
992
+ analyzers.
993
+
994
+ Args:
995
+ ancls: Analyzer class to instantiate
996
+ *anargs: Positional arguments for the analyzer
997
+ **ankwargs: Keyword arguments for the analyzer
998
+
999
+ Returns:
1000
+ The created analyzer instance
1001
+ """
1002
+ # Use OwnerContext so analyzer's findowner() can find this strategy
1003
+ with OwnerContext.set_owner(self):
1004
+ analyzer = ancls(*anargs, **ankwargs)
1005
+ self._slave_analyzers.append(analyzer)
1006
+ return analyzer
1007
+
1008
+ def _getanalyzer_slave(self, idx):
1009
+ """Get a slave analyzer by index.
1010
+
1011
+ Note: This appears to have a syntax bug - should use [] not append()
1012
+ """
1013
+ return self._slave_analyzers.append[idx]
1014
+
1015
+ def _addanalyzer(self, ancls, *anargs, **ankwargs):
1016
+ """Add an analyzer to the strategy.
1017
+
1018
+ Args:
1019
+ ancls: Analyzer class to instantiate
1020
+ *anargs: Positional arguments for the analyzer
1021
+ **ankwargs: Keyword arguments for the analyzer, may include _name
1022
+ """
1023
+ anname = ankwargs.pop("_name", "") or ancls.__name__.lower()
1024
+ nsuffix = next(self._alnames[anname])
1025
+ anname += str(nsuffix or "") # 0 (first instance) gets no suffix
1026
+ # Use OwnerContext so analyzer's findowner() can find this strategy
1027
+ with OwnerContext.set_owner(self):
1028
+ analyzer = ancls(*anargs, **ankwargs)
1029
+ # PERFORMANCE FIX: Explicitly set analyzer's owner to ensure it has access to strategy
1030
+ analyzer._parent = self
1031
+ analyzer._owner = self
1032
+ self.analyzers.append(analyzer, anname)
1033
+
1034
+ def _addobserver(self, multi, obscls, *obsargs, **obskwargs):
1035
+ """Add an observer to the strategy.
1036
+
1037
+ Args:
1038
+ multi: If True, create one observer per data feed; if False, create single observer
1039
+ obscls: Observer class to instantiate
1040
+ *obsargs: Positional arguments for the observer
1041
+ **obskwargs: Keyword arguments for the observer, may include obsname
1042
+ """
1043
+ obsname = obskwargs.pop("obsname", "")
1044
+ if not obsname:
1045
+ obsname = obscls.__name__.lower()
1046
+
1047
+ if not multi:
1048
+ newargs = list(itertools.chain(self.datas, obsargs))
1049
+ # Use OwnerContext so observer's findowner() can find this strategy
1050
+ with OwnerContext.set_owner(self):
1051
+ obs = obscls(*newargs, **obskwargs)
1052
+ # PERFORMANCE FIX: Explicitly set observer's owner to ensure it has access to strategy
1053
+ obs._parent = self
1054
+ obs._owner = self
1055
+ self._register_observer(obs)
1056
+ self.stats.append(obs, obsname)
1057
+ return
1058
+
1059
+ setattr(self.stats, obsname, [])
1060
+ obs_list = getattr(self.stats, obsname)
1061
+
1062
+ for data in self.datas:
1063
+ # Use OwnerContext so observer's findowner() can find this strategy
1064
+ with OwnerContext.set_owner(self):
1065
+ obs = obscls(data, *obsargs, **obskwargs)
1066
+ # PERFORMANCE FIX: Explicitly set observer's owner to ensure it has access to strategy
1067
+ obs._parent = self
1068
+ obs._owner = self
1069
+ self._register_observer(obs)
1070
+ obs_list.append(obs)
1071
+
1072
+ def _register_observer(self, obs):
1073
+ """Prepare an observer for execution.
1074
+
1075
+ Ensures the observer has _analyzers and has its clock set properly
1076
+ for strategy-wide observers. Does NOT register in _lineiterators
1077
+ to avoid double-processing (observers are processed bar-by-bar
1078
+ via _next_observers, not via _once batch processing).
1079
+
1080
+ Args:
1081
+ obs: Observer instance to register.
1082
+ """
1083
+ # Ensure _analyzers exists (some observers don't call super().__init__)
1084
+ if not hasattr(obs, "_analyzers"):
1085
+ obs._analyzers = []
1086
+ # Set clock for strategy-wide observers
1087
+ if getattr(obs, "_stclock", False):
1088
+ obs._clock = self
1089
+
1090
+ def _getminperstatus(self):
1091
+ """Check if minimum period requirements are satisfied.
1092
+
1093
+ Returns the maximum difference between required minimum periods
1094
+ and current data lengths.
1095
+
1096
+ Returns:
1097
+ int: Maximum value of (minperiod - current_length) across all data feeds.
1098
+ Negative values indicate all minimum periods are satisfied.
1099
+ """
1100
+ datas = self.datas
1101
+ minperiods = self._minperiods
1102
+ data_count = len(datas)
1103
+ if not data_count:
1104
+ raise ValueError("max() arg is an empty sequence") from None
1105
+
1106
+ minperstatus = minperiods[0] - len(datas[0])
1107
+ for index in range(1, data_count):
1108
+ status = minperiods[index] - len(datas[index])
1109
+ if status > minperstatus:
1110
+ minperstatus = status
1111
+
1112
+ self._minperstatus = minperstatus
1113
+ return minperstatus
1114
+
1115
+ def prenext_open(self):
1116
+ """Called before next() during prenext phase.
1117
+
1118
+ This is a hook for strategies to take action at the open of each bar
1119
+ before minimum period is reached.
1120
+ """
1121
+
1122
+ def nextstart_open(self):
1123
+ """Called at the open of the first bar where minimum period is satisfied.
1124
+
1125
+ This is called only once, transitioning from prenext to next phase.
1126
+ """
1127
+ self.next_open()
1128
+
1129
+ def next_open(self):
1130
+ """Called at the open of each bar during normal execution.
1131
+
1132
+ This is a hook for strategies to take action at the open of each bar.
1133
+ """
1134
+
1135
+ def _oncepost_open(self):
1136
+ """Prepare for _oncepost execution based on minimum period status.
1137
+
1138
+ Routes to appropriate method based on minperstatus:
1139
+ - minperstatus < 0: All data satisfied, call next_open()
1140
+ - minperstatus == 0: First bar with satisfied data, call nextstart_open()
1141
+ - minperstatus > 0: Data not ready, call prenext_open()
1142
+ """
1143
+ minperstatus = self._minperstatus
1144
+ if minperstatus < 0:
1145
+ self.next_open()
1146
+ elif minperstatus == 0:
1147
+ self.nextstart_open() # only called for the 1st value
1148
+ else:
1149
+ self.prenext_open()
1150
+
1151
+ def _oncepost(self, dt):
1152
+ """Execute oncepost processing for a single time step.
1153
+
1154
+ Args:
1155
+ dt: Current datetime
1156
+ """
1157
+ # CRITICAL FIX: Ensure _clock is set to actual data, not MinimalClock
1158
+ # During initialization, _clock might be set to MinimalClock if datas weren't available yet
1159
+ if hasattr(self, "_clock") and self._clock is not None:
1160
+ clock_type_name = type(self._clock).__name__
1161
+ if clock_type_name == "MinimalClock" and self.datas:
1162
+ # Replace MinimalClock with actual first data
1163
+ self._clock = self.datas[0]
1164
+ elif not hasattr(self, "_clock") or self._clock is None:
1165
+ # Set clock to first data if not set
1166
+ if self.datas:
1167
+ self._clock = self.datas[0]
1168
+
1169
+ # Loop through indicators, advance if indicator clock length exceeds indicator length
1170
+ for indicator in self._lineiterators[LineIterator.IndType]:
1171
+ # Honor a pinned secondary-feed clock (set in _periodset) so
1172
+ # indicators following a non-primary feed advance in sync with it.
1173
+ adv_clock = getattr(indicator, "_resolved_secondary_clock", None) or indicator._clock
1174
+ if len(adv_clock) > len(indicator):
1175
+ indicator.advance()
1176
+ # If using old data sync method, call advance; otherwise call forward
1177
+ if self._oldsync:
1178
+ # Strategy has not been reset, the line is there
1179
+ self.advance()
1180
+ else:
1181
+ # strategy has been reset to beginning. advance step by step
1182
+ self.forward()
1183
+ # Set datetime - and save it as the last valid datetime for use in stop()
1184
+ _set_current_datetime(self.lines.datetime, dt)
1185
+ if dt > 0:
1186
+ self._last_valid_datetime = dt
1187
+ # Notify
1188
+ self._notify()
1189
+
1190
+ try:
1191
+ has_strategy_next_lineactions = self._has_strategy_next_lineactions
1192
+ except AttributeError:
1193
+ has_strategy_next_lineactions = True
1194
+ if has_strategy_next_lineactions:
1195
+ self._next_strategy_lineactions()
1196
+
1197
+ # CRITICAL FIX: In runonce mode, ensure indicator lencount matches strategy length
1198
+ # This ensures len(indicator) == len(strategy) at the end of processing
1199
+ try:
1200
+ strategy_len = len(self)
1201
+ strategy_clock = getattr(self, "_clock", None)
1202
+ for indicator in self._lineiterators[LineIterator.IndType]:
1203
+ if getattr(indicator, "_clock", None) is not strategy_clock:
1204
+ continue
1205
+ # Only update if indicator was processed in runonce mode
1206
+ if hasattr(indicator, "_once_called") and indicator._once_called:
1207
+ # Update lencount for all lines in the indicator
1208
+ if hasattr(indicator, "lines") and hasattr(indicator.lines, "lines"):
1209
+ for line in indicator.lines.lines:
1210
+ if hasattr(line, "lencount"):
1211
+ # Set lencount to match strategy length (which equals data length)
1212
+ # Use the maximum of current lencount and strategy_len to ensure we don't decrease it
1213
+ line.lencount = max(line.lencount, strategy_len)
1214
+ except Exception:
1215
+ logger.debug("Failed to update indicator lencount in _oncepost_nextday", exc_info=True)
1216
+
1217
+ # Get current minimum period status and route to appropriate method
1218
+ # If all data satisfied, call next()
1219
+ # If first bar with all data satisfied, call nextstart()
1220
+ # If not all data satisfied, call prenext()
1221
+ try:
1222
+ minperstatus = self._single_minperiod - len(self._single_minperiod_data)
1223
+ self._minperstatus = minperstatus
1224
+ except AttributeError:
1225
+ minperstatus = self._getminperstatus()
1226
+ if minperstatus < 0:
1227
+ self.next()
1228
+ elif minperstatus == 0:
1229
+ self.nextstart() # only called for the 1st value
1230
+ else:
1231
+ self.prenext()
1232
+ # Update analyzers with minimum period status
1233
+ try:
1234
+ has_analyzers = self._has_analyzers
1235
+ except AttributeError:
1236
+ has_analyzers = bool(self.analyzers)
1237
+ if has_analyzers:
1238
+ self._next_analyzers(minperstatus, once=True)
1239
+ # Update observers with minimum period status
1240
+ try:
1241
+ has_observers = self._has_observers
1242
+ except AttributeError:
1243
+ has_observers = bool(self.stats.items)
1244
+ if has_observers:
1245
+ self._next_observers(minperstatus, once=True)
1246
+ # Clear pending orders and trades
1247
+ if self._orderspending or self._tradespending:
1248
+ self.clear()
1249
+
1250
+ def _clk_update(self):
1251
+ """Update the clock and advance strategy state if needed.
1252
+
1253
+ Returns:
1254
+ int: Current length of the strategy
1255
+ """
1256
+ # CRITICAL FIX: Ensure data is available before clock operations
1257
+ data_assignment_pending = self._data_assignment_pending
1258
+ clock = self._clock
1259
+
1260
+ if data_assignment_pending or clock is None:
1261
+ # Try to get data assignment from cerebro if not already done
1262
+ ensure_data_available = self.__dict__.get("_ensure_data_available")
1263
+ if ensure_data_available is not None:
1264
+ ensure_data_available()
1265
+
1266
+ # If using old data sync method
1267
+ if self._oldsync:
1268
+ # Call strategy's _clk_update() method
1269
+ clk_len = super()._clk_update()
1270
+ # Set datetime
1271
+ if self.datas:
1272
+ max_datetime = None
1273
+ for data in self.datas:
1274
+ if not len(data):
1275
+ continue
1276
+ dt_value = data.datetime[0]
1277
+ try:
1278
+ valid_dt = dt_value > 0 and dt_value < _INF
1279
+ except TypeError:
1280
+ valid_dt = False
1281
+ if valid_dt:
1282
+ if max_datetime is None or dt_value > max_datetime:
1283
+ max_datetime = dt_value
1284
+ if max_datetime is not None:
1285
+ _set_current_datetime(self.lines.datetime, max_datetime)
1286
+ # Return data length
1287
+ return clk_len
1288
+
1289
+ # CRITICAL FIX: Initialize _dlens if not present
1290
+ datas = self.datas
1291
+ try:
1292
+ olddlens = self._dlens
1293
+ except AttributeError:
1294
+ olddlens = [len(d) for d in datas]
1295
+ self._dlens = olddlens
1296
+ try:
1297
+ datetime_line = self._datetime_line
1298
+ except AttributeError:
1299
+ datetime_line = self.lines.datetime
1300
+ try:
1301
+ datetime_line_direct = self._datetime_line_direct
1302
+ except AttributeError:
1303
+ datetime_line_direct = False
1304
+
1305
+ try:
1306
+ data = self._single_clock_data
1307
+ except AttributeError:
1308
+ data = None
1309
+
1310
+ if data is not None:
1311
+ try:
1312
+ data_len = self._single_clock_len_line.lencount
1313
+ except AttributeError:
1314
+ data_len = len(data)
1315
+ if data_len > olddlens[0]:
1316
+ try:
1317
+ forward_line = self._single_line_forward_line
1318
+ except AttributeError:
1319
+ forward_line = None
1320
+ if forward_line is not None and forward_line.mode != forward_line.QBuffer:
1321
+ if forward_line.lencount < data_len:
1322
+ forward_line._idx += 1
1323
+ forward_line.lencount += 1
1324
+ forward_line.array.append(forward_line._default_value)
1325
+ else:
1326
+ self.forward()
1327
+ if data_len:
1328
+ try:
1329
+ data_datetime_line = self._single_clock_datetime_line
1330
+ data_datetime_idx = data_datetime_line._idx
1331
+ if data_datetime_idx >= 0:
1332
+ dt_value = data_datetime_line.array[data_datetime_idx]
1333
+ else:
1334
+ dt_value = data_datetime_line[0]
1335
+ except (AttributeError, IndexError):
1336
+ dt_value = data.datetime[0]
1337
+ try:
1338
+ valid_dt = dt_value > 0 and dt_value < _INF
1339
+ except TypeError:
1340
+ valid_dt = False
1341
+ if valid_dt:
1342
+ if datetime_line_direct:
1343
+ idx = datetime_line._idx
1344
+ if idx >= 0:
1345
+ try:
1346
+ datetime_line.array[idx] = dt_value if dt_value >= 1.0 else 1.0
1347
+ except IndexError:
1348
+ _set_current_datetime(datetime_line, dt_value)
1349
+ else:
1350
+ _set_current_datetime(datetime_line, dt_value)
1351
+ else:
1352
+ _set_current_datetime(datetime_line, dt_value)
1353
+ olddlens[0] = data_len
1354
+ try:
1355
+ return datetime_line.lencount
1356
+ except AttributeError:
1357
+ return len(self)
1358
+
1359
+ # Current new data lengths and valid datetimes in a single pass.
1360
+ newdlens = []
1361
+ max_datetime = None
1362
+ for data in datas:
1363
+ data_len = len(data)
1364
+ newdlens.append(data_len)
1365
+ if data_len:
1366
+ dt_value = data.datetime[0]
1367
+ try:
1368
+ valid_dt = dt_value > 0 and dt_value < _INF
1369
+ except TypeError:
1370
+ valid_dt = False
1371
+ if valid_dt:
1372
+ if max_datetime is None or dt_value > max_datetime:
1373
+ max_datetime = dt_value
1374
+
1375
+ # If new data length > old data length, forward
1376
+ if any(nl > old_len for old_len, nl in zip(olddlens, newdlens)):
1377
+ self.forward()
1378
+ # Set datetime to max of current datetimes - only update if we have valid datetimes
1379
+ if max_datetime is not None:
1380
+ _set_current_datetime(datetime_line, max_datetime)
1381
+ # Old data length equals new data length
1382
+ self._dlens = newdlens
1383
+
1384
+ try:
1385
+ return datetime_line.lencount
1386
+ except AttributeError:
1387
+ return len(self)
1388
+
1389
+ def _next_open(self):
1390
+ """Execute next_open phase based on minimum period status.
1391
+
1392
+ Same logic as _oncepost_open().
1393
+ """
1394
+ minperstatus = self._minperstatus
1395
+ if minperstatus < 0:
1396
+ self.next_open()
1397
+ elif minperstatus == 0:
1398
+ self.nextstart_open() # only called for the 1st value
1399
+ else:
1400
+ self.prenext_open()
1401
+
1402
+ def _next_fast_simple(self):
1403
+ """Fast _next implementation for simple single-strategy runs."""
1404
+ if self._orderspending or self._tradespending:
1405
+ Strategy._next(self)
1406
+ return
1407
+
1408
+ if self._fast_simple_clock_update:
1409
+ olddlens = self._dlens
1410
+ data_len = self._single_clock_len_line.lencount
1411
+ datetime_line = self._datetime_line
1412
+ forward_line = self._single_line_forward_line
1413
+ needs_forward = data_len > olddlens[0] and forward_line.lencount < data_len
1414
+ if data_len:
1415
+ data_datetime_line = self._single_clock_datetime_line
1416
+ data_datetime_idx = data_datetime_line._idx
1417
+ try:
1418
+ dt_value = data_datetime_line.array[data_datetime_idx]
1419
+ except IndexError:
1420
+ dt_value = data_datetime_line[0]
1421
+ try:
1422
+ valid_dt = dt_value > 0 and dt_value < _INF
1423
+ except TypeError:
1424
+ valid_dt = False
1425
+ if needs_forward:
1426
+ forward_line._idx += 1
1427
+ forward_line.lencount += 1
1428
+ forward_line.array.append(
1429
+ dt_value if valid_dt and dt_value >= 1.0 else forward_line._default_value
1430
+ )
1431
+ elif valid_dt:
1432
+ idx = datetime_line._idx
1433
+ if idx >= 0:
1434
+ try:
1435
+ datetime_line.array[idx] = dt_value if dt_value >= 1.0 else 1.0
1436
+ except IndexError:
1437
+ _set_current_datetime(datetime_line, dt_value)
1438
+ else:
1439
+ _set_current_datetime(datetime_line, dt_value)
1440
+ elif needs_forward:
1441
+ forward_line._idx += 1
1442
+ forward_line.lencount += 1
1443
+ forward_line.array.append(forward_line._default_value)
1444
+ olddlens[0] = data_len
1445
+ else:
1446
+ self._clk_update()
1447
+ minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
1448
+ object.__setattr__(self, "_minperstatus", minperstatus)
1449
+ if minperstatus < 0:
1450
+ self.next()
1451
+ elif minperstatus == 0:
1452
+ self.nextstart()
1453
+ else:
1454
+ self.prenext()
1455
+ if self._orderspending or self._tradespending:
1456
+ self.clear()
1457
+
1458
+ def _next_fast_simple_direct_clock(self):
1459
+ """Fast _next for trusted direct single-data clocks."""
1460
+ if self._orderspending or self._tradespending:
1461
+ Strategy._next(self)
1462
+ return
1463
+
1464
+ data_datetime_line = self._single_clock_datetime_line
1465
+ dt_value = data_datetime_line.array[data_datetime_line._idx]
1466
+ forward_line = self._single_line_forward_line
1467
+ forward_line._idx += 1
1468
+ forward_line.lencount += 1
1469
+ forward_line.array.append(dt_value)
1470
+ self._dlens[0] = data_datetime_line.lencount
1471
+
1472
+ minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
1473
+ object.__setattr__(self, "_minperstatus", minperstatus)
1474
+ if minperstatus < 0:
1475
+ self.next()
1476
+ elif minperstatus == 0:
1477
+ self.nextstart()
1478
+ else:
1479
+ self.prenext()
1480
+ if self._orderspending or self._tradespending:
1481
+ self.clear()
1482
+
1483
+ def _next(self):
1484
+ """Execute next() method and update analyzers and observers.
1485
+
1486
+ Gets minimum period status and passes it to analyzers and observers,
1487
+ then clears pending orders and trades.
1488
+ """
1489
+ try:
1490
+ fast_simple_next = self._fast_simple_next
1491
+ except AttributeError:
1492
+ fast_simple_next = False
1493
+ if fast_simple_next and not self._orderspending and not self._tradespending:
1494
+ if self._fast_simple_clock_update:
1495
+ olddlens = self._dlens
1496
+ data_len = self._single_clock_len_line.lencount
1497
+ datetime_line = self._datetime_line
1498
+ forward_line = self._single_line_forward_line
1499
+ needs_forward = data_len > olddlens[0] and forward_line.lencount < data_len
1500
+ if data_len:
1501
+ data_datetime_line = self._single_clock_datetime_line
1502
+ data_datetime_idx = data_datetime_line._idx
1503
+ try:
1504
+ dt_value = data_datetime_line.array[data_datetime_idx]
1505
+ except IndexError:
1506
+ dt_value = data_datetime_line[0]
1507
+ try:
1508
+ valid_dt = dt_value > 0 and dt_value < _INF
1509
+ except TypeError:
1510
+ valid_dt = False
1511
+ if needs_forward:
1512
+ forward_line._idx += 1
1513
+ forward_line.lencount += 1
1514
+ forward_line.array.append(
1515
+ dt_value
1516
+ if valid_dt and dt_value >= 1.0
1517
+ else forward_line._default_value
1518
+ )
1519
+ elif valid_dt:
1520
+ idx = datetime_line._idx
1521
+ if idx >= 0:
1522
+ try:
1523
+ datetime_line.array[idx] = dt_value if dt_value >= 1.0 else 1.0
1524
+ except IndexError:
1525
+ _set_current_datetime(datetime_line, dt_value)
1526
+ else:
1527
+ _set_current_datetime(datetime_line, dt_value)
1528
+ elif needs_forward:
1529
+ forward_line._idx += 1
1530
+ forward_line.lencount += 1
1531
+ forward_line.array.append(forward_line._default_value)
1532
+ olddlens[0] = data_len
1533
+ else:
1534
+ self._clk_update()
1535
+ minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
1536
+ object.__setattr__(self, "_minperstatus", minperstatus)
1537
+ if minperstatus < 0:
1538
+ self.next()
1539
+ elif minperstatus == 0:
1540
+ self.nextstart()
1541
+ else:
1542
+ self.prenext()
1543
+ if self._orderspending or self._tradespending:
1544
+ self.clear()
1545
+ return
1546
+
1547
+ super()._next()
1548
+
1549
+ try:
1550
+ minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
1551
+ object.__setattr__(self, "_minperstatus", minperstatus)
1552
+ except AttributeError:
1553
+ minperstatus = self._getminperstatus()
1554
+ try:
1555
+ has_analyzers = self._has_analyzers
1556
+ except AttributeError:
1557
+ has_analyzers = bool(self.analyzers)
1558
+ if has_analyzers:
1559
+ self._next_analyzers(minperstatus)
1560
+ try:
1561
+ has_observers = self._has_observers
1562
+ except AttributeError:
1563
+ has_observers = bool(self.stats.items)
1564
+ if has_observers:
1565
+ self._next_observers(minperstatus)
1566
+
1567
+ if self._orderspending or self._tradespending:
1568
+ self.clear()
1569
+
1570
+ def _get_all_observers(self):
1571
+ """Get all observer instances from self.stats.
1572
+
1573
+ Handles both single observers and multi-data observer lists.
1574
+ Ensures each observer has _analyzers attribute.
1575
+
1576
+ Returns:
1577
+ list: Flat list of all observer instances.
1578
+ """
1579
+ result = []
1580
+ for item in self.stats.items:
1581
+ if isinstance(item, list):
1582
+ for obs in item:
1583
+ if not hasattr(obs, "_analyzers"):
1584
+ obs._analyzers = []
1585
+ result.append(obs)
1586
+ else:
1587
+ if not hasattr(item, "_analyzers"):
1588
+ item._analyzers = []
1589
+ result.append(item)
1590
+ return result
1591
+
1592
+ def _next_observers(self, minperstatus, once=False):
1593
+ """Update observers based on minimum period status.
1594
+
1595
+ Iterates over self.stats.items (populated by _addobserver) instead of
1596
+ _lineiterators[ObsType] which is intentionally kept empty to avoid
1597
+ double-processing with _once() batch mode.
1598
+
1599
+ Note: Some pre-existing observers may have broken __init__ chains
1600
+ (due to ObserverBase.__init_subclass__ wrapped_init not calling
1601
+ super().__init__). Their next() calls are wrapped in try/except
1602
+ to prevent one broken observer from crashing the entire run.
1603
+
1604
+ Args:
1605
+ minperstatus: Current minimum period status
1606
+ once: If True, running in runonce mode; otherwise running in next() mode
1607
+ """
1608
+ # Collect all observers from _lineiterators
1609
+ observers_to_process = list(self._lineiterators[LineIterator.ObsType])
1610
+ # Also include TradeLogger observers from stats that are not in _lineiterators
1611
+ # (TradeLogger needs next() to be called for position/indicator logging)
1612
+ for obs in self.stats:
1613
+ if obs not in observers_to_process:
1614
+ # Only add TradeLogger type observers to avoid breaking other observers
1615
+ if obs.__class__.__name__ == "TradeLogger":
1616
+ observers_to_process.append(obs)
1617
+
1618
+ # Loop through observers
1619
+ for observer in observers_to_process:
1620
+ # For each analyzer in the observer (if observer has _analyzers)
1621
+ for analyzer in getattr(observer, "_analyzers", []):
1622
+ # Route to appropriate analyzer method based on minperstatus
1623
+ if minperstatus < 0:
1624
+ analyzer._next()
1625
+ elif minperstatus == 0:
1626
+ analyzer._nextstart() # only called for the 1st value
1627
+ else:
1628
+ analyzer._prenext()
1629
+ # If running in once mode
1630
+ if once:
1631
+ # If current data length > observer length
1632
+ if len(self) > len(observer):
1633
+ # If using old data sync method, call advance, else call forward
1634
+ if self._oldsync:
1635
+ observer.advance()
1636
+ else:
1637
+ observer.forward()
1638
+ # Route to appropriate observer method based on minperstatus
1639
+ if minperstatus < 0:
1640
+ observer.next()
1641
+ elif minperstatus == 0:
1642
+ observer.nextstart() # only called for the 1st value
1643
+ elif len(observer):
1644
+ observer.prenext()
1645
+ # If not in once mode, call _next()
1646
+ else:
1647
+ observer._next()
1648
+
1649
+ def _next_analyzers(self, minperstatus, once=False):
1650
+ """Update analyzers based on minimum period status.
1651
+
1652
+ Args:
1653
+ minperstatus: Current minimum period status
1654
+ once: If True, running in runonce mode (unused but kept for consistency)
1655
+ """
1656
+ for analyzer in self.analyzers:
1657
+ if minperstatus < 0:
1658
+ analyzer._next()
1659
+ elif minperstatus == 0:
1660
+ analyzer._nextstart() # only called for the 1st value
1661
+ else:
1662
+ analyzer._prenext()
1663
+
1664
+ def _settz(self, tz):
1665
+ """Set timezone for strategy's datetime line.
1666
+
1667
+ Args:
1668
+ tz: Timezone to set
1669
+ """
1670
+ self.lines.datetime._settz(tz)
1671
+
1672
+ def _start(self):
1673
+ """Initialize strategy and start execution.
1674
+
1675
+ Calculates minimum periods, starts analyzers and observers,
1676
+ and calls user's start() method.
1677
+ """
1678
+ # Calculate and set required minimum period
1679
+ self._periodset()
1680
+ # Start analyzers
1681
+ for analyzer in itertools.chain(self.analyzers, self._slave_analyzers):
1682
+ analyzer._start()
1683
+ # Start observers
1684
+ for obs in self.observers:
1685
+ if not isinstance(obs, list):
1686
+ obs = [obs] # support of multi-data observers
1687
+
1688
+ for o in obs:
1689
+ o._start()
1690
+
1691
+ # Change operators to stage 2
1692
+ self._stage2()
1693
+ # Current length of each data
1694
+ self._dlens = [len(data) for data in self.datas]
1695
+ self._datetime_line = self.lines.datetime
1696
+ try:
1697
+ self._datetime_line_direct = not self._datetime_line.bindings
1698
+ except AttributeError:
1699
+ self._datetime_line_direct = False
1700
+ try:
1701
+ strategy_lines = self.lines.lines
1702
+ if (
1703
+ len(strategy_lines) == 1
1704
+ and strategy_lines[0] is self._datetime_line
1705
+ and self._datetime_line_direct
1706
+ and self._datetime_line.mode != self._datetime_line.QBuffer
1707
+ ):
1708
+ self._single_line_forward_line = self._datetime_line
1709
+ else:
1710
+ self._single_line_forward_line = None
1711
+ except AttributeError:
1712
+ self._single_line_forward_line = None
1713
+ if len(self.datas) == 1:
1714
+ self._single_clock_data = self.datas[0]
1715
+ self._single_clock_datetime_line = self._single_clock_data.datetime
1716
+ self._single_clock_len_line = self._single_clock_datetime_line
1717
+ if len(self.datas) == 1 and len(self._minperiods) == 1:
1718
+ self._single_minperiod_data = self.datas[0]
1719
+ self._single_minperiod = self._minperiods[0]
1720
+ self._single_minperiod_len_line = self._single_minperiod_data.lines[0]
1721
+ from .linebuffer import LineActions
1722
+
1723
+ self._lineaction_datas = tuple(
1724
+ data for data in self.datas if isinstance(data, LineActions) and hasattr(data, "_next")
1725
+ )
1726
+ # Current minimum period status defaults to MAXINT (start in prenext)
1727
+ self._minperstatus = MAXINT
1728
+ # Call user's start()
1729
+ self.start()
1730
+ self._quicknotify = self.cerebro.p.quicknotify
1731
+ self._strategy_next_lineactions_cache = self._get_strategy_next_lineactions()
1732
+ self._has_strategy_next_lineactions = bool(self._strategy_next_lineactions_cache)
1733
+ self._all_analyzers_cache = list(self.analyzers) + list(self._slave_analyzers)
1734
+ self._has_analyzers = bool(self.analyzers)
1735
+ self._has_observers = bool(self.stats.items)
1736
+ self._notify_cashvalue_default = (
1737
+ "notify_cashvalue" not in self.__dict__
1738
+ and type(self).notify_cashvalue is Strategy.notify_cashvalue
1739
+ )
1740
+ self._notify_fund_default = (
1741
+ "notify_fund" not in self.__dict__ and type(self).notify_fund is Strategy.notify_fund
1742
+ )
1743
+ self._skip_empty_notify = (
1744
+ not self._quicknotify
1745
+ and not self._all_analyzers_cache
1746
+ and self._notify_cashvalue_default
1747
+ and self._notify_fund_default
1748
+ )
1749
+ self._fast_simple_next = (
1750
+ len(self.datas) == 1
1751
+ and len(self._minperiods) == 1
1752
+ and not self._lineiterators[LineIterator.IndType]
1753
+ and not self._lineaction_datas
1754
+ and not self._has_strategy_next_lineactions
1755
+ and self._skip_empty_notify
1756
+ and not self._has_analyzers
1757
+ and not self._has_observers
1758
+ )
1759
+ self._fast_simple_clock_update = (
1760
+ self._fast_simple_next
1761
+ and not self._oldsync
1762
+ and len(self.datas) == 1
1763
+ and self._single_line_forward_line is self._datetime_line
1764
+ and self._single_clock_len_line is self._single_clock_datetime_line
1765
+ and not self._data_assignment_pending
1766
+ and self._clock is not None
1767
+ )
1768
+ if self._fast_simple_next and type(self)._next is Strategy._next:
1769
+ object.__setattr__(self, "_next", self._next_fast_simple)
1770
+
1771
+ def start(self):
1772
+ """Called right before the backtesting is about to be started.
1773
+
1774
+ This is a hook for strategies to perform initialization before
1775
+ the backtesting loop begins.
1776
+ """
1777
+
1778
+ def getwriterheaders(self):
1779
+ """Get the CSV headers for writer output.
1780
+
1781
+ Returns:
1782
+ list: Headers including indicator/observer names and line aliases
1783
+ """
1784
+ # Filter indicators and observers for CSV output
1785
+ self.indobscsv = [self]
1786
+ # Filter indicators and observers, include only those with csv=True
1787
+ indobs = itertools.chain(self.getindicators_lines(), self.getobservers())
1788
+ self.indobscsv.extend(filter(lambda x: x.csv, indobs))
1789
+ # Initialize headers as empty list
1790
+ headers = []
1791
+
1792
+ # Prepare the indicators/observers data headers
1793
+ # Loop through indicators/observers marked for CSV output
1794
+ for iocsv in self.indobscsv:
1795
+ # Get indicator/observer name or class name
1796
+ name = iocsv.plotinfo.plotname or iocsv.__class__.__name__
1797
+ # Add name, length, and line aliases to headers
1798
+ headers.append(name)
1799
+ headers.append("len")
1800
+ headers.extend(iocsv.getlinealiases())
1801
+ # Return headers
1802
+ return headers
1803
+
1804
+ def getwritervalues(self):
1805
+ """Get current values for writer output.
1806
+
1807
+ Returns:
1808
+ list: Current values from indicators and observers
1809
+ """
1810
+ values = []
1811
+ # Loop through indicators/observers
1812
+ for iocsv in self.indobscsv:
1813
+ name = iocsv.plotinfo.plotname or iocsv.__class__.__name__
1814
+ values.append(name)
1815
+ lio = len(iocsv)
1816
+ values.append(lio)
1817
+ # If length > 0, get each value
1818
+ if lio:
1819
+ values.extend(map(lambda line: line[0], iocsv.lines.itersize()))
1820
+ else:
1821
+ values.extend([""] * iocsv.lines.size())
1822
+
1823
+ return values
1824
+
1825
+ def getwriterinfo(self):
1826
+ """Get comprehensive writer information including params and analysis.
1827
+
1828
+ Returns:
1829
+ AutoOrderedDict: Nested structure containing params, indicators,
1830
+ observers, and analyzer results
1831
+ """
1832
+ # Initialize writer info as AutoOrderedDict
1833
+ wrinfo = AutoOrderedDict()
1834
+ # Set parameters
1835
+ wrinfo["Params"] = self.p._getkwargs()
1836
+
1837
+ sections = [["Indicators", self.getindicators_lines()], ["Observers", self.getobservers()]]
1838
+ # Loop through indicators and observers
1839
+ for sectname, sectitems in sections:
1840
+ # Set specific values
1841
+ sinfo = wrinfo[sectname]
1842
+ for item in sectitems:
1843
+ itname = item.__class__.__name__
1844
+ sinfo[itname].Lines = item.lines.getlinealiases() or None
1845
+ sinfo[itname].Params = item.p._getkwargs() or None
1846
+ # Set analyzer values
1847
+ ainfo = wrinfo.Analyzers
1848
+
1849
+ # Internal Value Analyzer
1850
+ ainfo.Value.Begin = self.broker.startingcash
1851
+ ainfo.Value.End = self.broker.getvalue()
1852
+
1853
+ # No slave analyzers for a writer
1854
+ for aname, analyzer in self.analyzers.getitems():
1855
+ ainfo[aname].Params = analyzer.p._getkwargs() or None
1856
+ ainfo[aname].Analysis = analyzer.get_analysis()
1857
+
1858
+ return wrinfo
1859
+
1860
+ def nextstart(self):
1861
+ # Iteration 29 lifecycle INFO: minperiod first satisfied (once).
1862
+ logger.info(
1863
+ "strategy nextstart: strategy=%s minperiod=%d", type(self).__name__, self._minperiod
1864
+ )
1865
+ super().nextstart()
1866
+
1867
+ def _stop(self):
1868
+ # Iteration 29 lifecycle INFO (once per strategy).
1869
+ logger.info("strategy stopping: strategy=%s", type(self).__name__)
1870
+ # CRITICAL FIX: In runonce mode, ensure indicator lencount matches strategy length
1871
+ # This must be done BEFORE calling user's stop() method, as tests check len(indicator) == len(strategy)
1872
+ try:
1873
+ strategy_len = len(self)
1874
+ strategy_clock = getattr(self, "_clock", None)
1875
+ # Update lencount for all indicators to match strategy length
1876
+ # This is critical for runonce mode where indicators are pre-calculated but lencount may not match
1877
+ if hasattr(self, "_lineiterators"):
1878
+ from .lineiterator import LineIterator
1879
+
1880
+ for indicator in self._lineiterators.get(LineIterator.IndType, []):
1881
+ if getattr(indicator, "_clock", None) is not strategy_clock:
1882
+ continue
1883
+ # Update lencount for all lines in the indicator to match strategy length
1884
+ if hasattr(indicator, "lines") and hasattr(indicator.lines, "lines"):
1885
+ for line in indicator.lines.lines:
1886
+ if hasattr(line, "lencount"):
1887
+ # In runonce mode, set lencount to match strategy length (which equals data length)
1888
+ # This ensures len(indicator) == len(strategy) for test assertions
1889
+ line.lencount = strategy_len
1890
+ except Exception:
1891
+ logger.warning("Failed to update indicator lencount in _stop", exc_info=True)
1892
+
1893
+ # CRITICAL FIX: Restore last valid datetime before calling user's stop()
1894
+ # This ensures datetime[0] is valid for logging in stop() method
1895
+ if hasattr(self, "_last_valid_datetime") and self._last_valid_datetime > 0:
1896
+ try:
1897
+ # Restore strategy datetime
1898
+ self.lines.datetime[0] = self._last_valid_datetime
1899
+ # CRITICAL: Also restore all data feed datetimes
1900
+ for data in self.datas:
1901
+ try:
1902
+ data.datetime[0] = self._last_valid_datetime
1903
+ except Exception:
1904
+ logger.debug(
1905
+ "Failed to restore datetime for data %s in _stop",
1906
+ getattr(data, "_name", data),
1907
+ )
1908
+ except Exception:
1909
+ logger.debug("Failed to restore strategy datetime in _stop", exc_info=True)
1910
+
1911
+ # Call user's stop() method - can be overridden in strategy subclass
1912
+ self.stop()
1913
+ # Stop analyzers (both user-added and slave analyzers for observers)
1914
+ for analyzer in itertools.chain(self.analyzers, self._slave_analyzers):
1915
+ analyzer._stop()
1916
+
1917
+ # Stop observers (flush logs, etc.)
1918
+ for observer in self._get_all_observers():
1919
+ try:
1920
+ if hasattr(observer, "stop"):
1921
+ observer.stop()
1922
+ except Exception:
1923
+ logger.warning(
1924
+ "Observer %s.stop() raised an exception",
1925
+ type(observer).__name__,
1926
+ exc_info=True,
1927
+ )
1928
+
1929
+ # Change operators back to stage 1 - allows reuse of datas
1930
+ self._stage1()
1931
+
1932
+ def stop(self):
1933
+ """Called right before the backtesting is about to be stopped.
1934
+
1935
+ This is a hook for strategies to perform cleanup or final logging.
1936
+ """
1937
+
1938
+ def set_tradehistory(self, onoff=True):
1939
+ """Enable or disable trade history tracking.
1940
+
1941
+ Args:
1942
+ onoff: If True, keep full trade history; if False, only track current trade
1943
+ """
1944
+ self._tradehistoryon = onoff
1945
+
1946
+ def clear(self):
1947
+ """Clear pending orders and trades.
1948
+
1949
+ Moves pending orders to _orders list and clears pending trades.
1950
+ """
1951
+ self._orders.extend(self._orderspending)
1952
+ self._orderspending = []
1953
+ self._tradespending = []
1954
+
1955
+ def _addnotification(self, order, quicknotify=False):
1956
+ """Add order notification and process trade updates.
1957
+
1958
+ Args:
1959
+ order: The order that has been updated
1960
+ quicknotify: If True, immediately process notification without queueing
1961
+ """
1962
+ # If not simulated trading, add order to pending orders
1963
+ if not order.p.simulated:
1964
+ self._orderspending.append(order)
1965
+ # If in quick notify mode, initialize qorders and qtrades
1966
+ if quicknotify:
1967
+ qorders = [order]
1968
+ qtrades: list = []
1969
+ # If order has no executed volume
1970
+ if not order.executed.size:
1971
+ # If in quick notify mode, call _notify with info
1972
+ if quicknotify:
1973
+ self._notify(qorders=qorders, qtrades=qtrades)
1974
+ return
1975
+ # Get trade data - if order.data._compensate is None, use order.data; otherwise use order.data._compensate
1976
+ tradedata = getattr(order.data, "_compensate", None)
1977
+ if tradedata is None:
1978
+ tradedata = order.data
1979
+ # Get trade data - if trade exists in _trades, use the last one; otherwise create a new trade and save to datatrades
1980
+ tradekey = trade_key_from_order(order)
1981
+ datatrades = self._trades[tradedata][tradekey]
1982
+ if not datatrades:
1983
+ trade = Trade(data=tradedata, tradeid=tradekey, historyon=self._tradehistoryon)
1984
+ datatrades.append(trade)
1985
+ else:
1986
+ trade = datatrades[-1]
1987
+ # Loop through order execution bits
1988
+ for exbit in order.executed.iterpending():
1989
+ # If execution bit is None, break loop
1990
+ if exbit is None:
1991
+ break
1992
+ # If execution bit indicates closed position
1993
+ if exbit.closed:
1994
+ # Update trade
1995
+ trade.update(
1996
+ order,
1997
+ exbit.closed,
1998
+ exbit.price,
1999
+ exbit.closedvalue,
2000
+ exbit.closedcomm,
2001
+ exbit.pnl,
2002
+ comminfo=order.comminfo,
2003
+ )
2004
+ # If trade is closed
2005
+ if trade.isclosed:
2006
+ # Copy trade and add to _tradespending
2007
+ self._tradespending.append(copy.copy(trade))
2008
+ # If quick notify needed, copy trade and add to qtrades
2009
+ if quicknotify:
2010
+ qtrades.append(copy.copy(trade))
2011
+
2012
+ # Update it if needed
2013
+ # If order execution bit indicates opened position
2014
+ if exbit.opened:
2015
+ # If trade is closed, create new trade and save to datatrades
2016
+ if trade.isclosed:
2017
+ trade = Trade(data=tradedata, tradeid=tradekey, historyon=self._tradehistoryon)
2018
+ datatrades.append(trade)
2019
+ # Update trade
2020
+ trade.update(
2021
+ order,
2022
+ exbit.opened,
2023
+ exbit.price,
2024
+ exbit.openedvalue,
2025
+ exbit.openedcomm,
2026
+ exbit.pnl,
2027
+ comminfo=order.comminfo,
2028
+ )
2029
+
2030
+ # This extra check covers the case in which different tradeid
2031
+ # orders have put the position down to 0 and the next order
2032
+ # "opens" a position but "closes" the trade
2033
+ # If trade is closed
2034
+ if trade.isclosed:
2035
+ # Copy trade and add to _tradespending
2036
+ self._tradespending.append(copy.copy(trade))
2037
+ # If quick notify needed, copy trade and add to qtrades
2038
+ if quicknotify:
2039
+ qtrades.append(copy.copy(trade))
2040
+ # If trade was just opened
2041
+ if trade.justopened:
2042
+ # Copy trade and add to _tradespending
2043
+ self._tradespending.append(copy.copy(trade))
2044
+ # If quick notify needed, copy trade and add to qtrades
2045
+ if quicknotify:
2046
+ qtrades.append(copy.copy(trade))
2047
+ # If quick notify needed, call _notify
2048
+ if quicknotify:
2049
+ self._notify(qorders=qorders, qtrades=qtrades)
2050
+
2051
+ def _notify(self, qorders=None, qtrades=None):
2052
+ """Notify order and trade events to strategy and analyzers.
2053
+
2054
+ Args:
2055
+ qorders: Quick notify orders (empty list if not in quick notify mode)
2056
+ qtrades: Quick notify trades (empty list if not in quick notify mode)
2057
+ """
2058
+ # If quick notify is enabled
2059
+ try:
2060
+ quicknotify_enabled = self._quicknotify
2061
+ except AttributeError:
2062
+ quicknotify_enabled = self.cerebro.p.quicknotify
2063
+
2064
+ if quicknotify_enabled:
2065
+ if qorders is None:
2066
+ qorders = []
2067
+ if qtrades is None:
2068
+ qtrades = []
2069
+ # Need to know if quicknotify is on, to not reprocess pendingorders
2070
+ # and pendingtrades, which have to exist for things like observers
2071
+ # which look into it
2072
+ # Pending orders and trades are qorders and qtrades
2073
+ procorders = qorders
2074
+ proctrades = qtrades
2075
+ # Otherwise use orders and trades saved in _orderspending and _tradespending
2076
+ else:
2077
+ if qorders is None:
2078
+ qorders = ()
2079
+ procorders = self._orderspending
2080
+ proctrades = self._tradespending
2081
+
2082
+ # PERFORMANCE OPTIMIZATION: Cache merged analyzer list to avoid repeated itertools.chain
2083
+ # This is called 688K+ times, so caching makes a significant difference
2084
+ try:
2085
+ all_analyzers = self._all_analyzers_cache
2086
+ except AttributeError:
2087
+ all_analyzers = list(self.analyzers) + list(self._slave_analyzers)
2088
+ self._all_analyzers_cache = all_analyzers
2089
+
2090
+ try:
2091
+ notify_cashvalue_default = self._notify_cashvalue_default
2092
+ except AttributeError:
2093
+ notify_cashvalue_default = (
2094
+ "notify_cashvalue" not in self.__dict__
2095
+ and type(self).notify_cashvalue is Strategy.notify_cashvalue
2096
+ )
2097
+ try:
2098
+ notify_fund_default = self._notify_fund_default
2099
+ except AttributeError:
2100
+ notify_fund_default = (
2101
+ "notify_fund" not in self.__dict__
2102
+ and type(self).notify_fund is Strategy.notify_fund
2103
+ )
2104
+
2105
+ if (
2106
+ not quicknotify_enabled
2107
+ and not qorders
2108
+ and not procorders
2109
+ and not proctrades
2110
+ and not all_analyzers
2111
+ and notify_cashvalue_default
2112
+ and notify_fund_default
2113
+ ):
2114
+ return
2115
+
2116
+ # Loop through pending orders
2117
+ for order in procorders:
2118
+ # If order execution type is not Historical or histnotify, notify order
2119
+ if order.exectype != order.Historical or order.histnotify:
2120
+ self.notify_order(order)
2121
+ # Notify order to analyzers (both user and slave analyzers)
2122
+ for analyzer in all_analyzers:
2123
+ analyzer._notify_order(order)
2124
+ # Notify order to observers (e.g., TradeLogger)
2125
+ self._notify_order_to_observers(order)
2126
+ # Loop through pending trades, notify, and notify analyzers
2127
+ for trade in proctrades:
2128
+ self.notify_trade(trade)
2129
+ for analyzer in all_analyzers:
2130
+ analyzer._notify_trade(trade)
2131
+ # Notify trade to observers (e.g., TradeLogger)
2132
+ self._notify_trade_to_observers(trade)
2133
+ # If qorders is not empty, return after processing orders
2134
+ if qorders:
2135
+ return # cash is notified regularly
2136
+ # Get cash, value, fundvalue, fundshares
2137
+ cash = self.broker.getcash()
2138
+ value = self.broker.getvalue()
2139
+ fundvalue = self.broker.fundvalue
2140
+ fundshares = self.broker.fundshares
2141
+ # Notify cash and value values, and notify analyzers
2142
+ if not notify_cashvalue_default:
2143
+ self.notify_cashvalue(cash, value)
2144
+ # Notify fund values, and notify analyzers
2145
+ if not notify_fund_default:
2146
+ self.notify_fund(cash, value, fundvalue, fundshares)
2147
+ for analyzer in all_analyzers:
2148
+ analyzer._notify_cashvalue(cash, value)
2149
+ analyzer._notify_fund(cash, value, fundvalue, fundshares)
2150
+
2151
+ def add_timer(
2152
+ self,
2153
+ when,
2154
+ offset=datetime.timedelta(),
2155
+ repeat=datetime.timedelta(),
2156
+ weekdays=None,
2157
+ weekcarry=False,
2158
+ monthdays=None,
2159
+ monthcarry=True,
2160
+ allow=None,
2161
+ tzdata=None,
2162
+ cheat=False,
2163
+ *args,
2164
+ **kwargs,
2165
+ ):
2166
+ """Schedule a timer to invoke notify_timer or a callback.
2167
+
2168
+ Note: Can be called during __init__ or start
2169
+
2170
+ Schedules a timer to invoke either a specified callback or the
2171
+ notify_timer of one or more strategies.
2172
+
2173
+ Args:
2174
+ when: Can be:
2175
+ - datetime.time instance (see tzdata below)
2176
+ - bt.timer.SESSION_START to reference session start
2177
+ - bt.timer.SESSION_END to reference session end
2178
+ offset (datetime.timedelta): Offset the when value. Used with
2179
+ SESSION_START/SESSION_END to trigger after session start/end.
2180
+ repeat (datetime.timedelta): If set, timer repeats at this interval
2181
+ within the same session. Resets to original when after session end.
2182
+ weekdays (list): Sorted iterable with integers (Monday=1, Sunday=7)
2183
+ indicating which days the timer can be invoked. Empty = all days.
2184
+ weekcarry (bool): If True and weekday not seen (e.g., holiday),
2185
+ execute on next day (even if in new week).
2186
+ monthdays (list): Sorted iterable with integers (1-31) indicating
2187
+ which days of month to execute. Empty = all days.
2188
+ monthcarry (bool): If True and day not seen (weekend, holiday),
2189
+ execute on next available day.
2190
+ allow (callable): Callback receiving datetime.date, returns True if
2191
+ date is allowed for timer execution.
2192
+ tzdata: Timezone data - None, pytz instance, or data feed instance.
2193
+ If None and when is SESSION_START/END, uses first data feed.
2194
+ cheat (bool): If True, timer called before broker evaluates orders,
2195
+ allowing orders based on opening price.
2196
+ *args: Additional args passed to notify_timer
2197
+ **kwargs: Additional kwargs passed to notify_timer
2198
+
2199
+ Returns:
2200
+ The created timer instance
2201
+ """
2202
+ return self.cerebro._add_timer(
2203
+ owner=self,
2204
+ when=when,
2205
+ offset=offset,
2206
+ repeat=repeat,
2207
+ weekdays=weekdays,
2208
+ weekcarry=weekcarry,
2209
+ monthdays=monthdays,
2210
+ monthcarry=monthcarry,
2211
+ allow=allow,
2212
+ tzdata=tzdata,
2213
+ strats=False,
2214
+ cheat=cheat,
2215
+ *args,
2216
+ **kwargs,
2217
+ )
2218
+
2219
+ def notify_timer(self, timer, when, *args, **kwargs):
2220
+ """Receive timer notifications.
2221
+
2222
+ Receives a timer notification where ``timer`` is the timer instance
2223
+ returned by ``add_timer``, and ``when`` is the calling time. ``args``
2224
+ and ``kwargs`` are any additional arguments passed to ``add_timer``.
2225
+
2226
+ The actual ``when`` time can be later than expected, as the system may
2227
+ not have been able to call the timer before. This value is the timer's
2228
+ scheduled time, not the actual system time.
2229
+
2230
+ Args:
2231
+ timer: The timer instance created by add_timer
2232
+ when: The scheduled time when the timer was triggered
2233
+ *args: Additional positional arguments passed to add_timer
2234
+ **kwargs: Additional keyword arguments passed to add_timer
2235
+ """
2236
+
2237
+ def notify_idle(self):
2238
+ """Receive a live-engine poll when no data bar or tick was produced.
2239
+
2240
+ Live brokers may still need strategies to advance execution deadlines,
2241
+ reconciliation and risk controls while market data is silent. The
2242
+ default hook is intentionally empty and is only dispatched for strategy
2243
+ classes that override it.
2244
+ """
2245
+
2246
+ def notify_cashvalue(self, cash, value):
2247
+ """Notify the current cash and value of the strategy's broker.
2248
+
2249
+ Args:
2250
+ cash: Current cash amount
2251
+ value: Current portfolio value
2252
+ """
2253
+
2254
+ def notify_fund(self, cash, value, fundvalue, shares):
2255
+ """Notify the current cash, value, fund value, and fund shares.
2256
+
2257
+ Args:
2258
+ cash: Current cash amount
2259
+ value: Current portfolio value
2260
+ fundvalue: Current fund value
2261
+ shares: Current fund shares
2262
+ """
2263
+
2264
+ def notify_order(self, order):
2265
+ """Receive notification when an order status changes.
2266
+
2267
+ Args:
2268
+ order: The order with changed status
2269
+ """
2270
+
2271
+ def notify_trade(self, trade):
2272
+ """Receive notification when a trade status changes.
2273
+
2274
+ Args:
2275
+ trade: The trade with changed status
2276
+ """
2277
+
2278
+ def notify_store(self, msg, *args, **kwargs):
2279
+ """Receive notification from a store provider.
2280
+
2281
+ Args:
2282
+ msg: Message from the store
2283
+ *args: Additional positional arguments
2284
+ **kwargs: Additional keyword arguments
2285
+ """
2286
+
2287
+ def notify_data(self, data, status, *args, **kwargs):
2288
+ """Receive notification from a data feed.
2289
+
2290
+ Args:
2291
+ data: The data feed sending the notification
2292
+ status: Status code
2293
+ *args: Additional positional arguments
2294
+ **kwargs: Additional keyword arguments
2295
+ """
2296
+
2297
+ # ========== Tick/Channel Event Callbacks ==========
2298
+
2299
+ def notify_tick(self, tick):
2300
+ """Called when a new tick event arrives.
2301
+
2302
+ Override this method to implement tick-level trading logic.
2303
+
2304
+ Args:
2305
+ tick: TickEvent instance with price, volume, direction, etc.
2306
+ """
2307
+
2308
+ def notify_orderbook(self, orderbook):
2309
+ """Called when a new order book snapshot arrives.
2310
+
2311
+ Override this method to implement orderbook-based trading logic.
2312
+
2313
+ Args:
2314
+ orderbook: OrderBookSnapshot instance with bids, asks, spread, etc.
2315
+ """
2316
+
2317
+ def notify_funding(self, funding):
2318
+ """Called when a new funding rate event arrives.
2319
+
2320
+ Override this method to implement funding rate arbitrage or
2321
+ position management.
2322
+
2323
+ Args:
2324
+ funding: FundingEvent instance with rate, mark_price, etc.
2325
+ """
2326
+
2327
+ def notify_bar(self, bar):
2328
+ """Called when a bar event arrives from the channel system.
2329
+
2330
+ This is different from the standard next() method which processes
2331
+ bars from LineSeries data feeds. This callback handles BarEvents
2332
+ from the channel/queue system.
2333
+
2334
+ Args:
2335
+ bar: BarEvent instance with open, high, low, close, volume.
2336
+ """
2337
+
2338
+ def get_last_tick(self, symbol=None):
2339
+ """Get the last tick for a symbol.
2340
+
2341
+ Args:
2342
+ symbol: Symbol name. If None, returns first available.
2343
+
2344
+ Returns:
2345
+ TickEvent or None.
2346
+ """
2347
+ if symbol:
2348
+ return self._last_tick.get(symbol)
2349
+ if self._last_tick:
2350
+ return next(iter(self._last_tick.values()))
2351
+ return None
2352
+
2353
+ def get_last_orderbook(self, symbol=None):
2354
+ """Get the last order book for a symbol.
2355
+
2356
+ Args:
2357
+ symbol: Symbol name. If None, returns first available.
2358
+
2359
+ Returns:
2360
+ OrderBookSnapshot or None.
2361
+ """
2362
+ if symbol:
2363
+ return self._last_ob.get(symbol)
2364
+ if self._last_ob:
2365
+ return next(iter(self._last_ob.values()))
2366
+ return None
2367
+
2368
+ def get_last_funding(self, symbol=None):
2369
+ """Get the last funding rate for a symbol.
2370
+
2371
+ Args:
2372
+ symbol: Symbol name. If None, returns first available.
2373
+
2374
+ Returns:
2375
+ FundingEvent or None.
2376
+ """
2377
+ if symbol:
2378
+ return self._last_funding.get(symbol)
2379
+ if self._last_funding:
2380
+ return next(iter(self._last_funding.values()))
2381
+ return None
2382
+
2383
+ # ========== Data Access Methods ==========
2384
+
2385
+ def _register_hft_data(self, data):
2386
+ """Register a channel-mode data reference for HFT order routing."""
2387
+ symbol = getattr(data, "symbol", None) or getattr(data, "_name", None)
2388
+ if symbol is None:
2389
+ return None
2390
+ if not hasattr(self, "_hft_data_refs"):
2391
+ self._hft_data_refs = {}
2392
+ self._hft_data_refs[str(symbol)] = data
2393
+ return data
2394
+
2395
+ def get_hft_data(self, symbol=None):
2396
+ """Return a data reference suitable for channel-mode HFT orders.
2397
+
2398
+ In channel-only runs there may be no LineSeries data feed attached
2399
+ to the strategy, but standard order APIs still need a data identity.
2400
+ Cerebro registers lightweight per-symbol references before invoking
2401
+ ``notify_tick`` / ``notify_orderbook`` / ``notify_bar`` callbacks.
2402
+
2403
+ Args:
2404
+ symbol: Optional symbol to look up. If omitted and exactly one
2405
+ HFT data reference exists, that reference is returned.
2406
+
2407
+ Returns:
2408
+ A LineSeries data feed or a channel-mode data reference.
2409
+
2410
+ Raises:
2411
+ KeyError: If the requested symbol is unknown or no reference has
2412
+ been registered yet.
2413
+ ValueError: If ``symbol`` is omitted but more than one HFT data
2414
+ reference is available.
2415
+ """
2416
+ if not hasattr(self, "_hft_data_refs"):
2417
+ self._hft_data_refs = {}
2418
+
2419
+ if symbol is None:
2420
+ if len(self._hft_data_refs) == 1:
2421
+ return next(iter(self._hft_data_refs.values()))
2422
+ if self.datas:
2423
+ return self.datas[0]
2424
+ if not self._hft_data_refs:
2425
+ raise KeyError("No HFT channel data reference is available yet")
2426
+ raise ValueError("Multiple HFT data references exist; pass symbol explicitly")
2427
+
2428
+ symbol = str(symbol)
2429
+ data = self._hft_data_refs.get(symbol)
2430
+ if data is not None:
2431
+ return data
2432
+
2433
+ if symbol in self.env.datasbyname:
2434
+ return self.env.datasbyname[symbol]
2435
+
2436
+ raise KeyError(f"No HFT channel data reference for symbol {symbol!r}")
2437
+
2438
+ def getdatanames(self):
2439
+ """Get a list of all data names in the system.
2440
+
2441
+ Returns:
2442
+ list: Names of all data feeds
2443
+ """
2444
+ return keys(self.env.datasbyname)
2445
+
2446
+ def getdatabyname(self, name):
2447
+ """Get a data feed by its name.
2448
+
2449
+ Args:
2450
+ name: Name of the data feed
2451
+
2452
+ Returns:
2453
+ The data feed with the given name
2454
+ """
2455
+ return self.env.datasbyname[name]
2456
+
2457
+ def cancel(self, order):
2458
+ """Cancel an order in the broker.
2459
+
2460
+ Args:
2461
+ order: The order to cancel
2462
+ """
2463
+ self.broker.cancel(order)
2464
+
2465
+ def buy(
2466
+ self,
2467
+ data=None,
2468
+ size=None,
2469
+ price=None,
2470
+ plimit=None,
2471
+ exectype=None,
2472
+ valid=None,
2473
+ tradeid=0,
2474
+ oco=None,
2475
+ trailamount=None,
2476
+ trailpercent=None,
2477
+ parent=None,
2478
+ transmit=True,
2479
+ **kwargs,
2480
+ ) -> Optional[Order]:
2481
+ """Create a buy (long) order and send it to the broker.
2482
+
2483
+ Args:
2484
+ data: The data feed for the order. If None, uses the first data feed
2485
+ (self.data).
2486
+ size: Size to use (positive) for the order. If None, the sizer
2487
+ instance retrieved via getsizer will determine the size.
2488
+ price: Price to use. None is valid for Market and Close orders.
2489
+ For Limit, Stop and StopLimit orders this determines the
2490
+ trigger point.
2491
+ plimit: Only applicable to StopLimit orders. This is the price at
2492
+ which to set the implicit Limit order, once the Stop has been
2493
+ triggered.
2494
+ trailamount: For StopTrail/StopTrailLimit orders, an absolute amount
2495
+ which determines the distance to the price to keep the trailing
2496
+ stop.
2497
+ trailpercent: For StopTrail/StopTrailLimit orders, a percentage
2498
+ amount which determines the distance to the price to keep the
2499
+ trailing stop.
2500
+ exectype: Execution type. Possible values:
2501
+ - Order.Market or None: Market order
2502
+ - Order.Limit: Limit order
2503
+ - Order.Stop: Stop order
2504
+ - Order.StopLimit: Stop-limit order
2505
+ - Order.Close: Close order
2506
+ - Order.StopTrail: Stop-trail order
2507
+ - Order.StopTrailLimit: Stop-trail-limit order
2508
+ valid: Order validity. Possible values:
2509
+ - None: Good till cancel
2510
+ - datetime.datetime/date: Good till date
2511
+ - Order.DAY: Day order
2512
+ tradeid: Internal value to track overlapping trades.
2513
+ oco: Another order instance for OCO (Order Cancel Others) group.
2514
+ parent: Controls the relationship of a group of orders (e.g., bracket
2515
+ orders).
2516
+ transmit: If True, transmit the order to the broker. Used for
2517
+ controlling bracket orders.
2518
+ **kwargs: Additional broker-specific parameters.
2519
+
2520
+ Returns:
2521
+ The submitted order, or None if size is 0.
2522
+
2523
+ Example:
2524
+ Create a market buy order:
2525
+ >>> order = self.buy()
2526
+
2527
+ Create a limit buy order:
2528
+ >>> order = self.buy(price=100.0, exectype=Order.Limit)
2529
+ """
2530
+ # Resolve data argument
2531
+ if isinstance(data, string_types):
2532
+ data = self.getdatabyname(data)
2533
+ elif data is None:
2534
+ if self.datas:
2535
+ data = self.datas[0]
2536
+ else:
2537
+ raise ValueError(
2538
+ "No data feed available. In channel mode, pass a data "
2539
+ "object explicitly or use self.get_hft_data(symbol)"
2540
+ )
2541
+ # Use the provided size, otherwise calculate via getsizer
2542
+ size = size if size is not None else self.getsizing(data, isbuy=True)
2543
+ # If size is non-zero, submit the order
2544
+ if size:
2545
+ order = self.broker.buy(
2546
+ self,
2547
+ data,
2548
+ size=abs(size),
2549
+ price=price,
2550
+ plimit=plimit,
2551
+ exectype=exectype,
2552
+ valid=valid,
2553
+ tradeid=tradeid,
2554
+ oco=oco,
2555
+ trailamount=trailamount,
2556
+ trailpercent=trailpercent,
2557
+ parent=parent,
2558
+ transmit=transmit,
2559
+ **kwargs,
2560
+ )
2561
+ # Auto-notify signal to TradeLogger observers
2562
+ signal_price = (
2563
+ price
2564
+ if price is not None
2565
+ else (
2566
+ data.close[0]
2567
+ if hasattr(data, "close") and hasattr(data.close, "__getitem__")
2568
+ else 0
2569
+ )
2570
+ )
2571
+ self._notify_signal_to_observers("buy", abs(size), signal_price, data)
2572
+ return order
2573
+
2574
+ return None
2575
+
2576
+ def sell(
2577
+ self,
2578
+ data=None,
2579
+ size=None,
2580
+ price=None,
2581
+ plimit=None,
2582
+ exectype=None,
2583
+ valid=None,
2584
+ tradeid=0,
2585
+ oco=None,
2586
+ trailamount=None,
2587
+ trailpercent=None,
2588
+ parent=None,
2589
+ transmit=True,
2590
+ **kwargs,
2591
+ ) -> Optional[Order]:
2592
+ """Create a sell (short) order and send it to the broker.
2593
+
2594
+ See the documentation for ``buy`` for an explanation of the parameters.
2595
+
2596
+ Returns:
2597
+ The submitted order, or None if no order was created
2598
+ """
2599
+ # Resolve data argument
2600
+ if isinstance(data, string_types):
2601
+ data = self.getdatabyname(data)
2602
+ elif data is None:
2603
+ if self.datas:
2604
+ data = self.datas[0]
2605
+ else:
2606
+ raise ValueError(
2607
+ "No data feed available. In channel mode, pass a data "
2608
+ "object explicitly or use self.get_hft_data(symbol)"
2609
+ )
2610
+ size = size if size is not None else self.getsizing(data, isbuy=False)
2611
+ if size:
2612
+ order = self.broker.sell(
2613
+ self,
2614
+ data,
2615
+ size=abs(size),
2616
+ price=price,
2617
+ plimit=plimit,
2618
+ exectype=exectype,
2619
+ valid=valid,
2620
+ tradeid=tradeid,
2621
+ oco=oco,
2622
+ trailamount=trailamount,
2623
+ trailpercent=trailpercent,
2624
+ parent=parent,
2625
+ transmit=transmit,
2626
+ **kwargs,
2627
+ )
2628
+ # Auto-notify signal to TradeLogger observers
2629
+ signal_price = (
2630
+ price
2631
+ if price is not None
2632
+ else (
2633
+ data.close[0]
2634
+ if hasattr(data, "close") and hasattr(data.close, "__getitem__")
2635
+ else 0
2636
+ )
2637
+ )
2638
+ self._notify_signal_to_observers("sell", abs(size), signal_price, data)
2639
+ return order
2640
+
2641
+ return None
2642
+
2643
+ def close(self, data=None, size=None, **kwargs) -> Optional[Order]:
2644
+ """Close a long or short position.
2645
+
2646
+ Creates an order that counters the existing position to close it.
2647
+
2648
+ Args:
2649
+ data: The data feed for which to close the position.
2650
+ If None, uses the default data feed.
2651
+ size: The size to close. If None, closes the entire position.
2652
+ **kwargs: Additional keyword arguments passed to the order.
2653
+
2654
+ Note:
2655
+ If size is not provided, it is automatically calculated from the
2656
+ existing position to fully close it.
2657
+
2658
+ Returns:
2659
+ The submitted order, or None if no position exists
2660
+ """
2661
+ # Get the data feed
2662
+ if isinstance(data, string_types):
2663
+ data = self.getdatabyname(data)
2664
+ elif data is None:
2665
+ data = self.data
2666
+ position_side = kwargs.pop("position_side", None)
2667
+ position_side = normalize_position_side(position_side)
2668
+ broker_mode = normalize_position_mode(
2669
+ getattr(self.broker, "get_param", lambda *_args, **_kwargs: "net")(
2670
+ "position_mode", "net"
2671
+ )
2672
+ )
2673
+
2674
+ if position_side is not None or broker_mode == POSITION_MODE_DUAL_SIDE:
2675
+ if position_side is None:
2676
+ long_size = abs(self.getposition(data, self.broker, side=POSITION_SIDE_LONG).size)
2677
+ short_size = abs(self.getposition(data, self.broker, side=POSITION_SIDE_SHORT).size)
2678
+ if long_size and short_size:
2679
+ raise ValueError(
2680
+ "close() requires position_side when both long and short legs are open"
2681
+ )
2682
+ if long_size:
2683
+ position_side = POSITION_SIDE_LONG
2684
+ possize = long_size
2685
+ elif short_size:
2686
+ position_side = POSITION_SIDE_SHORT
2687
+ possize = short_size
2688
+ else:
2689
+ return None
2690
+ else:
2691
+ possize = abs(self.getposition(data, self.broker, side=position_side).size)
2692
+
2693
+ size = abs(size if size is not None else possize)
2694
+ if not size:
2695
+ return None
2696
+
2697
+ kwargs.setdefault("position_side", position_side)
2698
+ kwargs.setdefault("offset", POSITION_OFFSET_CLOSE)
2699
+ if position_side == POSITION_SIDE_LONG:
2700
+ return self.sell(data=data, size=size, **kwargs)
2701
+ return self.buy(data=data, size=size, **kwargs)
2702
+ # Get the current position size
2703
+ possize = self.getposition(data, self.broker).size
2704
+ # If size is None, close the entire position; otherwise close the specified size
2705
+ size = abs(size if size is not None else possize)
2706
+ # If position is long (positive), sell to close
2707
+ if possize > 0:
2708
+ return self.sell(data=data, size=size, **kwargs)
2709
+ # If position is short (negative), buy to close
2710
+ if possize < 0:
2711
+ return self.buy(data=data, size=size, **kwargs)
2712
+
2713
+ return None
2714
+
2715
+ def buy_bracket(
2716
+ self,
2717
+ data=None,
2718
+ size=None,
2719
+ price=None,
2720
+ plimit=None,
2721
+ exectype=Order.Limit,
2722
+ valid=None,
2723
+ tradeid=0,
2724
+ trailamount=None,
2725
+ trailpercent=None,
2726
+ oargs=None,
2727
+ stopprice=None,
2728
+ stopexec=Order.Stop,
2729
+ stopargs=None,
2730
+ limitprice=None,
2731
+ limitexec=Order.Limit,
2732
+ limitargs=None,
2733
+ **kwargs,
2734
+ ):
2735
+ """Create a bracket order group (buy order with stop-loss and take-profit).
2736
+
2737
+ Creates a bracket order group consisting of:
2738
+ - A main **buy** order with the specified execution type (default: Limit)
2739
+ - A *low side* bracket **sell** stop-loss order
2740
+ - A *high side* bracket **sell** take-profit order
2741
+
2742
+ Args:
2743
+ - ``data`` (default: ``None``): The data feed for the order. If None,
2744
+ uses the first data feed (self.data).
2745
+
2746
+ - ``size`` (default: ``None``): Size for the order. If None, the sizer
2747
+ determines the size. The same size is applied to all three orders.
2748
+
2749
+ - ``price`` (default: ``None``): Price for the main buy order. None
2750
+ is valid for Market and Close orders.
2751
+
2752
+ - ``plimit`` (default: ``None``): Price limit for StopLimit orders.
2753
+
2754
+ - ``trailamount`` (default: ``None``): Absolute trailing amount for
2755
+ StopTrail/StopTrailLimit orders.
2756
+
2757
+ - ``trailpercent`` (default: ``None``): Percentage trailing amount for
2758
+ StopTrail/StopTrailLimit orders.
2759
+
2760
+ - ``exectype`` (default: ``bt.Order.Limit``): Execution type for the
2761
+ main order. See buy() documentation for possible values.
2762
+
2763
+ - ``valid`` (default: ``None``): Order validity period. See buy()
2764
+ documentation for possible values.
2765
+
2766
+ - ``tradeid`` (default: ``0``): Trade ID for tracking overlapping trades.
2767
+
2768
+ - ``oargs`` (default: ``{}``): Specific keyword arguments (dict) for
2769
+ the main side order. Applied before **kwargs.
2770
+
2771
+ - ``**kwargs``: Additional keyword arguments applied to all three
2772
+ orders. See buy() documentation for possible values.
2773
+
2774
+ - ``stopprice`` (default: ``None``): Specific price for the stop-loss
2775
+ order.
2776
+
2777
+ - ``stopexec`` (default: ``bt.Order.Stop``): Execution type for the
2778
+ stop-loss order.
2779
+
2780
+ - ``stopargs`` (default: ``{}``): Specific keyword arguments (dict)
2781
+ for the stop-loss order.
2782
+
2783
+ - ``limitprice`` (default: ``None``): Specific price for the take-profit
2784
+ order.
2785
+
2786
+ - ``limitexec`` (default: ``bt.Order.Limit``): Execution type for the
2787
+ take-profit order.
2788
+
2789
+ - ``limitargs`` (default: ``{}``): Specific keyword arguments (dict)
2790
+ for the take-profit order.
2791
+
2792
+ Returns:
2793
+ A list containing the three orders [main_order, stop_order, limit_order].
2794
+ Suppressed orders are represented as None.
2795
+
2796
+ Note:
2797
+ High/Low side orders can be suppressed by setting limitexec=None or
2798
+ stopexec=None.
2799
+ """
2800
+ # Normalize mutable-default placeholders (B006); these dicts are only
2801
+ # read via kargs.update(...), never mutated, so None==empty is equivalent.
2802
+ oargs = {} if oargs is None else oargs
2803
+ stopargs = {} if stopargs is None else stopargs
2804
+ limitargs = {} if limitargs is None else limitargs
2805
+ # Build parameter dictionary
2806
+ kargs = {
2807
+ "size": size,
2808
+ "data": data,
2809
+ "price": price,
2810
+ "plimit": plimit,
2811
+ "exectype": exectype,
2812
+ "valid": valid,
2813
+ "tradeid": tradeid,
2814
+ "trailamount": trailamount,
2815
+ "trailpercent": trailpercent,
2816
+ }
2817
+ # Update with main side order specific arguments
2818
+ kargs.update(oargs)
2819
+ # Update with general keyword arguments
2820
+ kargs.update(kwargs)
2821
+ # Set transmit flag: only transmit if both stop and limit are None
2822
+ kargs["transmit"] = limitexec is None and stopexec is None
2823
+ # Create the main buy order
2824
+ o = self.buy(**kargs)
2825
+
2826
+ # Create stop-loss order
2827
+ if stopexec is not None:
2828
+ # low side / stop
2829
+ kargs = {
2830
+ "data": data,
2831
+ "price": stopprice,
2832
+ "exectype": stopexec,
2833
+ "valid": valid,
2834
+ "tradeid": tradeid,
2835
+ }
2836
+ kargs.update(stopargs)
2837
+ kargs.update(kwargs)
2838
+ kargs["parent"] = o
2839
+ kargs["transmit"] = limitexec is None
2840
+ kargs["size"] = o.size
2841
+ ostop = self.sell(**kargs)
2842
+ else:
2843
+ ostop = None
2844
+
2845
+ # Create take-profit order
2846
+ if limitexec is not None:
2847
+ # high side / limit
2848
+ kargs = {
2849
+ "data": data,
2850
+ "price": limitprice,
2851
+ "exectype": limitexec,
2852
+ "valid": valid,
2853
+ "tradeid": tradeid,
2854
+ }
2855
+ kargs.update(limitargs)
2856
+ kargs.update(kwargs)
2857
+ kargs["parent"] = o
2858
+ kargs["transmit"] = True
2859
+ kargs["size"] = o.size
2860
+ olimit = self.sell(**kargs)
2861
+ else:
2862
+ olimit = None
2863
+
2864
+ return [o, ostop, olimit]
2865
+
2866
+ def sell_bracket(
2867
+ self,
2868
+ data=None,
2869
+ size=None,
2870
+ price=None,
2871
+ plimit=None,
2872
+ exectype=Order.Limit,
2873
+ valid=None,
2874
+ tradeid=0,
2875
+ trailamount=None,
2876
+ trailpercent=None,
2877
+ oargs=None,
2878
+ stopprice=None,
2879
+ stopexec=Order.Stop,
2880
+ stopargs=None,
2881
+ limitprice=None,
2882
+ limitexec=Order.Limit,
2883
+ limitargs=None,
2884
+ **kwargs,
2885
+ ):
2886
+ """Create a sell bracket order group (sell order with stop-loss and take-profit).
2887
+
2888
+ Creates a bracket order group consisting of:
2889
+ - A main **sell** order with the specified execution type (default: Limit)
2890
+ - A *high side* bracket **buy** stop-loss order
2891
+ - A *low side* bracket **buy** take-profit order
2892
+
2893
+ Args:
2894
+ See buy_bracket() for parameter documentation.
2895
+
2896
+ Returns:
2897
+ A list containing the three orders [main_order, stop_order, limit_order].
2898
+ Suppressed orders are represented as None.
2899
+
2900
+ Note:
2901
+ High/Low side orders can be suppressed by setting limitexec=None or
2902
+ stopexec=None.
2903
+ """
2904
+ # Normalize mutable-default placeholders (B006); read-only via update().
2905
+ oargs = {} if oargs is None else oargs
2906
+ stopargs = {} if stopargs is None else stopargs
2907
+ limitargs = {} if limitargs is None else limitargs
2908
+ kargs = {
2909
+ "size": size,
2910
+ "data": data,
2911
+ "price": price,
2912
+ "plimit": plimit,
2913
+ "exectype": exectype,
2914
+ "valid": valid,
2915
+ "tradeid": tradeid,
2916
+ "trailamount": trailamount,
2917
+ "trailpercent": trailpercent,
2918
+ }
2919
+ kargs.update(oargs)
2920
+ kargs.update(kwargs)
2921
+ kargs["transmit"] = limitexec is None and stopexec is None
2922
+ o = self.sell(**kargs)
2923
+
2924
+ if stopexec is not None:
2925
+ # high side / stop
2926
+ kargs = {
2927
+ "data": data,
2928
+ "price": stopprice,
2929
+ "exectype": stopexec,
2930
+ "valid": valid,
2931
+ "tradeid": tradeid,
2932
+ }
2933
+ kargs.update(stopargs)
2934
+ kargs.update(kwargs)
2935
+ kargs["parent"] = o
2936
+ kargs["transmit"] = limitexec is None # transmit if last
2937
+ kargs["size"] = o.size
2938
+ ostop = self.buy(**kargs)
2939
+ else:
2940
+ ostop = None
2941
+
2942
+ if limitexec is not None:
2943
+ # low side / limit
2944
+ kargs = {
2945
+ "data": data,
2946
+ "price": limitprice,
2947
+ "exectype": limitexec,
2948
+ "valid": valid,
2949
+ "tradeid": tradeid,
2950
+ }
2951
+ kargs.update(limitargs)
2952
+ kargs.update(kwargs)
2953
+ kargs["parent"] = o
2954
+ kargs["transmit"] = True
2955
+ kargs["size"] = o.size
2956
+ olimit = self.buy(**kargs)
2957
+ else:
2958
+ olimit = None
2959
+
2960
+ return [o, ostop, olimit]
2961
+
2962
+ def order_target_size(self, data=None, target=0, **kwargs) -> Optional[Order]:
2963
+ """Place an order to achieve a target position size.
2964
+
2965
+ Rebalances the current position to reach the specified target size.
2966
+
2967
+ Args:
2968
+ data: The data feed for the order. If None, uses the default data feed.
2969
+ target: Target position size.
2970
+ - If target > pos.size: buy (target - pos.size)
2971
+ - If target < pos.size: sell (pos.size - target)
2972
+ - If target == 0: close the entire position
2973
+ **kwargs: Additional keyword arguments passed to buy/sell.
2974
+
2975
+ Returns:
2976
+ The generated order, or None if target == current position size.
2977
+ """
2978
+ # Get the specific data feed
2979
+ if isinstance(data, string_types):
2980
+ data = self.getdatabyname(data)
2981
+ elif data is None:
2982
+ data = self.data
2983
+
2984
+ # Get the current position size
2985
+ possize = self.getposition(data, self.broker).size
2986
+ # If target is 0 and position exists, close the position
2987
+ if not target and possize:
2988
+ return self.close(data=data, size=possize, **kwargs)
2989
+ # If target is greater than current position, buy to increase
2990
+ if target > possize:
2991
+ return self.buy(data=data, size=target - possize, **kwargs)
2992
+ # If target is less than current position, sell to decrease
2993
+ if target < possize:
2994
+ return self.sell(data=data, size=possize - target, **kwargs)
2995
+
2996
+ return None # no execution target == possize
2997
+
2998
+ def order_target_value(self, data=None, target=0.0, price=None, **kwargs) -> Optional[Order]:
2999
+ """Place an order to achieve a target position value.
3000
+
3001
+ Rebalances the position to reach the specified target value.
3002
+
3003
+ Args:
3004
+ data: The data feed for the order. If None, uses the default data feed.
3005
+ target: Target position value in currency units.
3006
+ - If target is 0: close position
3007
+ - If target > value: buy to increase value
3008
+ - If target < value: sell to decrease value
3009
+ price: Price for size calculation. If None, uses data.close[0].
3010
+ **kwargs: Additional keyword arguments passed to buy/sell.
3011
+
3012
+ Returns:
3013
+ The generated order, or None if no order was issued.
3014
+ """
3015
+ # Get the data feed
3016
+ if isinstance(data, string_types):
3017
+ data = self.getdatabyname(data)
3018
+ elif data is None:
3019
+ data = self.data
3020
+ # Get the current position size
3021
+ possize = self.getposition(data, self.broker).size
3022
+ # If target is 0 and position exists, close the position
3023
+ if not target and possize: # closing a position
3024
+ return self.close(data=data, size=possize, price=price, **kwargs)
3025
+ # Otherwise, rebalance to target value
3026
+ # Get the current value of this data
3027
+ value = self.broker.getvalue(datas=[data])
3028
+ # Get commission info for size calculation
3029
+ comminfo = self.broker.getcommissioninfo(data)
3030
+ # Get price: use provided price or default to close price
3031
+ # Make sure a price is there
3032
+ price = price if price is not None else data.close[0]
3033
+ # If target value is greater than current value, buy
3034
+ if target > value:
3035
+ size = comminfo.getsize(price, target - value)
3036
+ return self.buy(data=data, size=size, price=price, **kwargs)
3037
+ # If target value is less than current value, sell
3038
+ if target < value:
3039
+ size = comminfo.getsize(price, value - target)
3040
+ return self.sell(data=data, size=size, price=price, **kwargs)
3041
+
3042
+ return None # no execution size == possize
3043
+
3044
+ def order_target_percent(self, data=None, target=0.0, **kwargs) -> Optional[Order]:
3045
+ """Place an order to achieve a target percentage of portfolio value.
3046
+
3047
+ Rebalances the position so its value equals the target percentage
3048
+ of the total portfolio value.
3049
+
3050
+ Args:
3051
+ data: The data feed for the order. If None, uses the default data feed.
3052
+ target: Target percentage as a decimal (e.g., 0.05 for 5%).
3053
+ **kwargs: Additional keyword arguments passed to order_target_value.
3054
+
3055
+ Returns:
3056
+ The generated order, or None if no order was issued.
3057
+
3058
+ Example:
3059
+ With target=0.05 and portfolio value of 100:
3060
+ - Target value = 0.05 * 100 = 5
3061
+ - Orders are placed through order_target_value
3062
+
3063
+ Note:
3064
+ Position direction (long/short) is considered:
3065
+ - If target > value: buy if pos.size >= 0, sell if pos.size < 0
3066
+ - If target < value: sell if pos.size >= 0, buy if pos.size < 0
3067
+ """
3068
+ # Get the data feed
3069
+ if isinstance(data, string_types):
3070
+ data = self.getdatabyname(data)
3071
+ elif data is None:
3072
+ data = self.data
3073
+ # Calculate target value based on portfolio value
3074
+ # Note: Getting position size here is not necessary
3075
+ # possize = self.getposition(data, self.broker).size
3076
+ target *= self.broker.getvalue()
3077
+
3078
+ return self.order_target_value(data=data, target=target, **kwargs)
3079
+
3080
+ def getposition(self, data=None, broker=None, side=None, **kwargs):
3081
+ """Get the current position for a data feed.
3082
+
3083
+ Args:
3084
+ data: The data feed to get position for. If None, uses the first data feed.
3085
+ broker: The broker to query. If None, uses the default broker.
3086
+
3087
+ Returns:
3088
+ The current Position object.
3089
+
3090
+ Note:
3091
+ A property ``position`` is also available as a shortcut.
3092
+ """
3093
+ data = data if data is not None else self.datas[0]
3094
+ broker = broker or self.broker
3095
+ return broker.getposition(data, side=side, **kwargs)
3096
+
3097
+ # Property to access position for the default data feed
3098
+ position = property(getposition)
3099
+
3100
+ def getpositionbyname(self, name=None, broker=None, side=None, **kwargs):
3101
+ """Get the current position for a data feed by name.
3102
+
3103
+ Args:
3104
+ name: Name of the data feed. If None, uses the first data feed.
3105
+ broker: The broker to query. If None, uses the default broker.
3106
+
3107
+ Returns:
3108
+ The current Position object.
3109
+
3110
+ Note:
3111
+ A property ``positionbyname`` is also available as a shortcut.
3112
+ """
3113
+ data = self.datas[0] if not name else self.getdatabyname(name)
3114
+ broker = broker or self.broker
3115
+ return broker.getposition(data, side=side, **kwargs)
3116
+
3117
+ # Property to access position by name
3118
+ positionbyname = property(getpositionbyname)
3119
+
3120
+ def getpositions(self, broker=None):
3121
+ """Get all positions from the broker.
3122
+
3123
+ Args:
3124
+ broker: The broker to query. If None, uses the default broker.
3125
+
3126
+ Returns:
3127
+ Dictionary mapping data feeds to Position objects.
3128
+
3129
+ Note:
3130
+ A property ``positions`` is also available as a shortcut.
3131
+ """
3132
+ broker = broker or self.broker
3133
+ return broker.positions
3134
+
3135
+ # Property to access all positions
3136
+ positions = property(getpositions)
3137
+
3138
+ def getpositionsbyname(self, broker=None):
3139
+ """Get all positions from the broker indexed by data name.
3140
+
3141
+ Args:
3142
+ broker: The broker to query. If None, uses the default broker.
3143
+
3144
+ Returns:
3145
+ OrderedDict mapping data names to Position objects.
3146
+
3147
+ Note:
3148
+ A property ``positionsbyname`` is also available as a shortcut.
3149
+ """
3150
+ broker = broker or self.broker
3151
+ positions = broker.positions
3152
+
3153
+ posbyname = collections.OrderedDict()
3154
+ for name, data in iteritems(self.env.datasbyname):
3155
+ posbyname[name] = positions[data]
3156
+
3157
+ return posbyname
3158
+
3159
+ # Property to access positions by name
3160
+ positionsbyname = property(getpositionsbyname)
3161
+
3162
+ def _addsizer(self, sizer, *args, **kwargs):
3163
+ """Add a sizer to the strategy.
3164
+
3165
+ If sizer is None, uses FixedSize sizer. Otherwise instantiates
3166
+ the provided sizer class and sets it.
3167
+
3168
+ Args:
3169
+ sizer: Sizer class or None
3170
+ *args: Positional arguments for sizer instantiation
3171
+ **kwargs: Keyword arguments for sizer instantiation
3172
+ """
3173
+ if sizer is None:
3174
+ self.setsizer(FixedSize())
3175
+ else:
3176
+ self.setsizer(sizer(*args, **kwargs))
3177
+
3178
+ def setsizer(self, sizer):
3179
+ """Set the sizer for automatic stake calculation.
3180
+
3181
+ Args:
3182
+ sizer: The sizer instance to use
3183
+
3184
+ Returns:
3185
+ The sizer instance
3186
+ """
3187
+ self._sizer = sizer
3188
+ sizer.set(self, self.broker)
3189
+ return sizer
3190
+
3191
+ def getsizer(self):
3192
+ """Get the current sizer for automatic stake calculation.
3193
+
3194
+ Returns:
3195
+ The current sizer instance
3196
+
3197
+ Note:
3198
+ Also available as the ``sizer`` property.
3199
+ """
3200
+ return self._sizer
3201
+
3202
+ sizer = property(getsizer, setsizer)
3203
+
3204
+ def getsizing(self, data=None, isbuy=True):
3205
+ """Get the order size from the sizer.
3206
+
3207
+ Uses the configured sizer to calculate the appropriate stake size
3208
+ for the next order.
3209
+
3210
+ Args:
3211
+ data: The data feed for the order. If None, uses the default data.
3212
+ isbuy: True for buy orders, False for sell orders.
3213
+
3214
+ Returns:
3215
+ The calculated stake size.
3216
+ """
3217
+ # Ensure sizer has broker reference
3218
+ if hasattr(self._sizer, "broker") and self._sizer.broker is None:
3219
+ self._sizer.set(self, self.broker)
3220
+ return self._sizer.getsizing(data, isbuy)
3221
+
3222
+
3223
+ class SignalStrategy(Strategy):
3224
+ """A strategy subclass that automatically operates using signals.
3225
+
3226
+ This strategy subclass responds to signal indicators to automatically
3227
+ enter and exit positions based on signal values.
3228
+
3229
+ Signal values:
3230
+ - ``> 0`` indicates a long (buy) signal
3231
+ - ``< 0`` indicates a short (sell) signal
3232
+
3233
+ There are five types of signals, broken into two groups:
3234
+
3235
+ **Main Group**:
3236
+
3237
+ - ``LONGSHORT``: Both long and short indications from this signal
3238
+ are taken. The strategy will go long or short based on the sign.
3239
+
3240
+ - ``LONG``:
3241
+ - Positive (long) indications: Go long
3242
+ - Negative (short) indications: Close long position
3243
+ - If ``LONGEXIT`` exists, it is used to exit longs
3244
+ - If ``SHORT`` signal exists and no ``LONGEXIT``, it will close
3245
+ longs before opening a short
3246
+
3247
+ - ``SHORT``:
3248
+ - Negative (short) indications: Go short
3249
+ - Positive (long) indications: Close short position
3250
+ - If ``SHORTEXIT`` exists, it is used to exit shorts
3251
+ - If ``LONG`` signal exists and no ``SHORTEXIT``, it will close
3252
+ shorts before opening a long
3253
+
3254
+ **Exit Group**:
3255
+ These signals override others to provide explicit exit criteria:
3256
+
3257
+ - ``LONGEXIT``: Negative indications are taken to exit long positions
3258
+ - ``SHORTEXIT``: Positive indications are taken to exit short positions
3259
+
3260
+ **Order Issuing**
3261
+
3262
+ Orders are placed with Market execution type and Good-Until-Canceled
3263
+ validity.
3264
+
3265
+ Params:
3266
+
3267
+ - ``signals`` (default: ``[]``): A list/tuple of lists/tuples for signal
3268
+ instantiation and type allocation. This parameter is typically managed
3269
+ through ``cerebro.add_signal``.
3270
+
3271
+ - ``_accumulate`` (default: ``False``): Allow entering the market even if
3272
+ already in a position (accumulate positions).
3273
+
3274
+ - ``_concurrent`` (default: ``False``): Allow issuing orders even when
3275
+ orders are already pending execution.
3276
+
3277
+ - ``_data`` (default: ``None``): If multiple datas are present in the
3278
+ system which is the target for orders. This can be
3279
+
3280
+ - ``None``: The first data in the system will be used
3281
+
3282
+ - An ``int``: indicating the data that was inserted at that position
3283
+
3284
+ - An ``str``: name given to the data when creating it (parameter
3285
+ ``name``) or when adding it cerebro with ``cerebro.adddata(...,
3286
+ name=)``
3287
+
3288
+ - A ``data`` instance
3289
+
3290
+ """
3291
+
3292
+ # Parameters for signal strategy
3293
+ params: tuple = (
3294
+ ("signals", []),
3295
+ ("_accumulate", False),
3296
+ ("_concurrent", False),
3297
+ ("_data", None),
3298
+ )
3299
+
3300
+ def __new__(cls, *args, **kwargs):
3301
+ """Override __new__ to handle next method remapping that was done in MetaSigStrategy"""
3302
+ # Handle next method remapping like the old MetaSigStrategy.__new__ did
3303
+ if hasattr(cls, "next") and not hasattr(cls, "_next_custom"):
3304
+ cls._next_custom = cls.next
3305
+
3306
+ # Create the instance
3307
+ instance = super().__new__(cls, *args, **kwargs)
3308
+
3309
+ # Set the next method to _next_catch (from MetaSigStrategy)
3310
+ instance.next = instance._next_catch
3311
+
3312
+ return instance
3313
+
3314
+ def __init__(self, *args, **kwargs):
3315
+ """Initialize the signal strategy with functionality from MetaSigStrategy methods"""
3316
+ # Handle the functionality that was in MetaSigStrategy.dopreinit
3317
+ self._signals = collections.defaultdict(list)
3318
+
3319
+ # Set the data target (from MetaSigStrategy.dopreinit)
3320
+ _data = getattr(self.p, "_data", None)
3321
+ if _data is None:
3322
+ self._dtarget = self.data0
3323
+ elif isinstance(_data, integer_types):
3324
+ self._dtarget = self.datas[_data]
3325
+ elif isinstance(_data, string_types):
3326
+ self._dtarget = self.getdatabyname(_data)
3327
+ elif isinstance(_data, LineRoot):
3328
+ self._dtarget = _data
3329
+ else:
3330
+ self._dtarget = self.data0
3331
+
3332
+ # Filter out strategy parameter kwargs to prevent them from reaching parent __init__
3333
+ filtered_kwargs = kwargs.copy()
3334
+ if hasattr(self.__class__, "_params") and self.__class__._params is not None:
3335
+ params_cls = self.__class__._params
3336
+ param_names = set()
3337
+
3338
+ # Get all parameter names from the class
3339
+ if hasattr(params_cls, "_getpairs"):
3340
+ param_names.update(params_cls._getpairs().keys())
3341
+ elif hasattr(params_cls, "_gettuple"):
3342
+ param_names.update(key for key, value in params_cls._gettuple())
3343
+
3344
+ # Remove strategy parameter kwargs
3345
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k not in param_names}
3346
+
3347
+ # Call parent initialization with filtered kwargs
3348
+ # Don't pass *args to avoid object.__init__() error, consistent with Strategy.__init__ fix
3349
+ if filtered_kwargs:
3350
+ super().__init__(**filtered_kwargs)
3351
+ else:
3352
+ super().__init__()
3353
+
3354
+ # Handle the functionality that was in MetaSigStrategy.dopostinit
3355
+ # Add signals from params
3356
+ # CRITICAL FIX: Pass self._dtarget as data source for signal indicators
3357
+ # and register them with the strategy so they get processed
3358
+ for sigtype, sigcls, sigargs, sigkwargs in self.p.signals:
3359
+ sig_indicator = sigcls(self._dtarget, *sigargs, **sigkwargs)
3360
+ self._signals[sigtype].append(sig_indicator)
3361
+ # CRITICAL FIX: Register signal indicator with strategy's _lineiterators
3362
+ # so its once()/next() methods get called during processing
3363
+ if hasattr(sig_indicator, "_ltype"):
3364
+ ltype = sig_indicator._ltype
3365
+ if sig_indicator not in self._lineiterators[ltype]:
3366
+ self._lineiterators[ltype].append(sig_indicator)
3367
+ sig_indicator._owner = self
3368
+
3369
+ # Record types of signals
3370
+ self._longshort = bool(self._signals[SIGNAL_LONGSHORT])
3371
+
3372
+ self._long = bool(self._signals[SIGNAL_LONG])
3373
+ self._short = bool(self._signals[SIGNAL_SHORT])
3374
+
3375
+ self._longexit = bool(self._signals[SIGNAL_LONGEXIT])
3376
+ self._shortexit = bool(self._signals[SIGNAL_SHORTEXIT])
3377
+
3378
+ def _start(self):
3379
+ """Start the signal strategy and initialize the order sentinel."""
3380
+ self._sentinel = None # sentinel for order concurrency
3381
+ super()._start()
3382
+
3383
+ def signal_add(self, sigtype, signal):
3384
+ """Add a signal indicator to the strategy.
3385
+
3386
+ Args:
3387
+ sigtype: Type of signal (e.g., SIGNAL_LONG, SIGNAL_SHORT)
3388
+ signal: The signal indicator instance
3389
+ """
3390
+ self._signals[sigtype].append(signal)
3391
+
3392
+ def _notify(self, qorders=None, qtrades=None):
3393
+ """Process notifications and reset sentinel when order completes.
3394
+
3395
+ Args:
3396
+ qorders: Quick notify orders
3397
+ qtrades: Quick notify trades
3398
+ """
3399
+ if qorders is None:
3400
+ qorders = []
3401
+ if qtrades is None:
3402
+ qtrades = []
3403
+ # Nullify the sentinel if done
3404
+ procorders = qorders or self._orderspending
3405
+ if self._sentinel is not None:
3406
+ for order in procorders:
3407
+ if order == self._sentinel and not order.alive():
3408
+ self._sentinel = None
3409
+ break
3410
+
3411
+ super()._notify(qorders=qorders, qtrades=qtrades)
3412
+
3413
+ def _next_catch(self):
3414
+ """Catch method that routes to signal processing and custom next."""
3415
+ self._next_signal()
3416
+ if hasattr(self, "_next_custom"):
3417
+ self._next_custom()
3418
+
3419
+ @staticmethod
3420
+ def _all_pos(sig, nosig):
3421
+ """True if every value in ``sig`` (or ``nosig`` when empty) is > 0."""
3422
+ return all(x[0] > 0.0 for x in sig or nosig)
3423
+
3424
+ @staticmethod
3425
+ def _all_neg(sig, nosig):
3426
+ """True if every value in ``sig`` (or ``nosig`` when empty) is < 0."""
3427
+ return all(x[0] < 0.0 for x in sig or nosig)
3428
+
3429
+ @staticmethod
3430
+ def _all_any(sig, nosig):
3431
+ """True if every value in ``sig`` (or ``nosig`` when empty) is truthy."""
3432
+ return all(x[0] for x in sig or nosig)
3433
+
3434
+ def _evaluate_signals(self):
3435
+ """Evaluate all signal collections into entry/exit/reversal flags.
3436
+
3437
+ Pure helper (no order side effects) extracted from _next_signal for
3438
+ readability. Returns a tuple of booleans consumed by the position
3439
+ decision logic:
3440
+ (ls_long, ls_short, l_enter, s_enter, l_exit, s_exit,
3441
+ l_rev, s_rev, l_leave, s_leave)
3442
+ """
3443
+ # Get signal collections
3444
+ sigs = self._signals
3445
+ # Default no-signal value
3446
+ nosig = [[0.0]]
3447
+ pos = self._all_pos
3448
+ neg = self._all_neg
3449
+ anyv = self._all_any
3450
+
3451
+ # Calculate current status of the signals
3452
+ # If SIGNAL_LONGSHORT is empty, loop through nosig
3453
+ ls_long = pos(sigs[SIGNAL_LONGSHORT], nosig)
3454
+ ls_short = neg(sigs[SIGNAL_LONGSHORT], nosig)
3455
+ # Long entry: direct (>0), inverted (<0) or any (truthy)
3456
+ l_enter = (
3457
+ pos(sigs[SIGNAL_LONG], nosig)
3458
+ or neg(sigs[SIGNAL_LONG_INV], nosig)
3459
+ or anyv(sigs[SIGNAL_LONG_ANY], nosig)
3460
+ )
3461
+ # Short entry: direct (<0), inverted (>0) or any (truthy)
3462
+ s_enter = (
3463
+ neg(sigs[SIGNAL_SHORT], nosig)
3464
+ or pos(sigs[SIGNAL_SHORT_INV], nosig)
3465
+ or anyv(sigs[SIGNAL_SHORT_ANY], nosig)
3466
+ )
3467
+ # Long exit: direct (<0), inverted (>0) or any (truthy)
3468
+ l_exit = (
3469
+ neg(sigs[SIGNAL_LONGEXIT], nosig)
3470
+ or pos(sigs[SIGNAL_LONGEXIT_INV], nosig)
3471
+ or anyv(sigs[SIGNAL_LONGEXIT_ANY], nosig)
3472
+ )
3473
+ # Short exit: direct (>0), inverted (<0) or any (truthy)
3474
+ s_exit = (
3475
+ pos(sigs[SIGNAL_SHORTEXIT], nosig)
3476
+ or neg(sigs[SIGNAL_SHORTEXIT_INV], nosig)
3477
+ or anyv(sigs[SIGNAL_SHORTEXIT_ANY], nosig)
3478
+ )
3479
+
3480
+ # Use opposite signals to start reversal (by closing)
3481
+ # but only if no "xxxExit" exists
3482
+ # Long reversal: no long exit and short entry signal
3483
+ l_rev = not self._longexit and s_enter
3484
+ # Short reversal: no short exit and long entry signal
3485
+ s_rev = not self._shortexit and l_enter
3486
+
3487
+ # Opposite of individual long and short (leave = exit on opposite signal)
3488
+ # Long leave: direct (<0), inverted (>0) or any (truthy)
3489
+ l_leave = (
3490
+ neg(sigs[SIGNAL_LONG], nosig)
3491
+ or pos(sigs[SIGNAL_LONG_INV], nosig)
3492
+ or anyv(sigs[SIGNAL_LONG_ANY], nosig)
3493
+ )
3494
+ # Short leave: direct (>0), inverted (<0) or any (truthy)
3495
+ s_leave = (
3496
+ pos(sigs[SIGNAL_SHORT], nosig)
3497
+ or neg(sigs[SIGNAL_SHORT_INV], nosig)
3498
+ or anyv(sigs[SIGNAL_SHORT_ANY], nosig)
3499
+ )
3500
+
3501
+ # Invalidate long leave if longexit signals are available
3502
+ # If longexit exists, disable l_leave; otherwise keep l_leave
3503
+ l_leave = not self._longexit and l_leave
3504
+ # Invalidate short leave if shortexit signals are available
3505
+ # If shortexit exists, disable s_leave; otherwise keep s_leave
3506
+ s_leave = not self._shortexit and s_leave
3507
+
3508
+ return (
3509
+ ls_long,
3510
+ ls_short,
3511
+ l_enter,
3512
+ s_enter,
3513
+ l_exit,
3514
+ s_exit,
3515
+ l_rev,
3516
+ s_rev,
3517
+ l_leave,
3518
+ s_leave,
3519
+ )
3520
+
3521
+ def _next_signal(self):
3522
+ """Process signals and generate orders based on signal values.
3523
+
3524
+ Evaluates all signal types and generates buy/sell orders based on:
3525
+ - Current position status
3526
+ - Signal values (positive/negative)
3527
+ - Accumulation and concurrency settings
3528
+ """
3529
+ # If concurrent orders are disabled and an order is active, return
3530
+ if self._sentinel is not None and not self.p._concurrent:
3531
+ return # order active and more than 1 not allowed
3532
+
3533
+ # Evaluate all signal collections into decision flags
3534
+ (
3535
+ ls_long,
3536
+ ls_short,
3537
+ l_enter,
3538
+ s_enter,
3539
+ l_exit,
3540
+ s_exit,
3541
+ l_rev,
3542
+ s_rev,
3543
+ l_leave,
3544
+ s_leave,
3545
+ ) = self._evaluate_signals()
3546
+
3547
+ # Take size and start logic
3548
+ # Get current position size
3549
+ size = self.getposition(self._dtarget).size
3550
+ # If no position
3551
+ if not size:
3552
+ # Enter new position based on signals
3553
+ if ls_long or l_enter:
3554
+ self._sentinel = self.buy(self._dtarget)
3555
+
3556
+ elif ls_short or s_enter:
3557
+ self._sentinel = self.sell(self._dtarget)
3558
+
3559
+ # If current position is long (positive)
3560
+ elif size > 0: # current long position
3561
+ if ls_short or l_exit or l_rev or l_leave:
3562
+ # closing position - not relevant for concurrency
3563
+ self.close(self._dtarget)
3564
+
3565
+ if ls_short or l_rev:
3566
+ self._sentinel = self.sell(self._dtarget)
3567
+
3568
+ if ls_long or l_enter:
3569
+ if self.p._accumulate:
3570
+ self._sentinel = self.buy(self._dtarget)
3571
+ # If current position is short (negative)
3572
+ elif size < 0: # current short position
3573
+ if ls_long or s_exit or s_rev or s_leave:
3574
+ # closing position - not relevant for concurrency
3575
+ self.close(self._dtarget)
3576
+
3577
+ if ls_long or s_rev:
3578
+ self._sentinel = self.buy(self._dtarget)
3579
+
3580
+ if ls_short or s_enter:
3581
+ if self.p._accumulate:
3582
+ self._sentinel = self.sell(self._dtarget)
3583
+
3584
+
3585
+ class BtApiStrategy(Strategy):
3586
+ """A Strategy subclass with built-in logging capabilities.
3587
+
3588
+ This strategy class extends the base Strategy class with automatic
3589
+ logger initialization using the SpdLogManager. It provides a default
3590
+ log() method for logging messages and custom notification handling.
3591
+
3592
+ Attributes:
3593
+ logger: The configured logger instance from SpdLogManager.
3594
+
3595
+ Params:
3596
+ log_file_name: Optional custom log file name. If not provided,
3597
+ defaults to "{ClassName}.log".
3598
+
3599
+ Example:
3600
+ class MyStrategy(bt.BtApiStrategy):
3601
+ params = (('log_file_name', 'my_strategy.log'),)
3602
+
3603
+ def next(self):
3604
+ self.log(f'Close price: {self.data.close[0]:.2f}')
3605
+ """
3606
+
3607
+ def __init__(self):
3608
+ """Initialize the strategy with a logger instance."""
3609
+ self.logger = self.init_logger(self.p.get("log_file_name", None))
3610
+
3611
+ def init_logger(self, log_file_name=None):
3612
+ """Initialize and return a logger instance.
3613
+
3614
+ Creates a logger using SpdLogManager with the specified or default
3615
+ log file name.
3616
+
3617
+ Args:
3618
+ log_file_name: Optional custom log file name. If None, uses
3619
+ "{ClassName}.log" as the default.
3620
+
3621
+ Returns:
3622
+ A configured logger instance.
3623
+ """
3624
+ if log_file_name is None:
3625
+ logger = SpdLogManager(
3626
+ file_name=self.__class__.__name__ + ".log", logger_name="strategy", print_info=True
3627
+ ).create_logger()
3628
+ else:
3629
+ logger = SpdLogManager(
3630
+ file_name=log_file_name, logger_name="strategy", print_info=True
3631
+ ).create_logger()
3632
+ return logger
3633
+
3634
+ def log(self, txt):
3635
+ """Log a message at INFO level.
3636
+
3637
+ Args:
3638
+ txt: The message text to log.
3639
+ """
3640
+ self.logger.info(txt)
3641
+
3642
+ def _addnotification(self, data, quicknotify=True):
3643
+ """Process notifications for orders and trades with logging.
3644
+
3645
+ This method extends the base notification handling to route
3646
+ notifications to the appropriate handler methods.
3647
+
3648
+ Args:
3649
+ data: The notification data, which can be an order or trade.
3650
+ quicknotify: If True, immediately process notification without queueing.
3651
+ """
3652
+ if data.data_type == "order":
3653
+ self.notify_order(data)
3654
+ if data.data_type == "trade":
3655
+ self.notify_trade(data)