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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (465) hide show
  1. back_trader_python-1.4.0.dist-info/METADATA +1491 -0
  2. back_trader_python-1.4.0.dist-info/RECORD +465 -0
  3. back_trader_python-1.4.0.dist-info/WHEEL +5 -0
  4. back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
  5. back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
  6. backtrader/__init__.py +148 -0
  7. backtrader/_cerebro/__init__.py +5 -0
  8. backtrader/_cerebro/channel.py +382 -0
  9. backtrader/_cerebro/execution.py +377 -0
  10. backtrader/_cerebro/lifecycle.py +143 -0
  11. backtrader/_cerebro/notifications.py +150 -0
  12. backtrader/_cerebro/presentation.py +230 -0
  13. backtrader/_cerebro/registry.py +593 -0
  14. backtrader/_cerebro/runnext.py +551 -0
  15. backtrader/_cerebro/runonce.py +142 -0
  16. backtrader/analyzer.py +594 -0
  17. backtrader/analyzers/__init__.py +50 -0
  18. backtrader/analyzers/annualreturn.py +226 -0
  19. backtrader/analyzers/calmar.py +165 -0
  20. backtrader/analyzers/drawdown.py +287 -0
  21. backtrader/analyzers/leverage.py +112 -0
  22. backtrader/analyzers/logreturnsrolling.py +190 -0
  23. backtrader/analyzers/periodstats.py +153 -0
  24. backtrader/analyzers/positions.py +119 -0
  25. backtrader/analyzers/pyfolio.py +470 -0
  26. backtrader/analyzers/returns.py +192 -0
  27. backtrader/analyzers/sharpe.py +307 -0
  28. backtrader/analyzers/sharpe_ratio_stats.py +534 -0
  29. backtrader/analyzers/sqn.py +112 -0
  30. backtrader/analyzers/timereturn.py +192 -0
  31. backtrader/analyzers/total_value.py +75 -0
  32. backtrader/analyzers/tradeanalyzer.py +278 -0
  33. backtrader/analyzers/transactions.py +141 -0
  34. backtrader/analyzers/vwr.py +245 -0
  35. backtrader/bokeh/__init__.py +155 -0
  36. backtrader/bokeh/analyzers/__init__.py +13 -0
  37. backtrader/bokeh/analyzers/plot.py +192 -0
  38. backtrader/bokeh/analyzers/recorder.py +181 -0
  39. backtrader/bokeh/app.py +1094 -0
  40. backtrader/bokeh/live/__init__.py +11 -0
  41. backtrader/bokeh/live/client.py +352 -0
  42. backtrader/bokeh/live/datahandler.py +346 -0
  43. backtrader/bokeh/plot_adapter.py +200 -0
  44. backtrader/bokeh/schemes/__init__.py +14 -0
  45. backtrader/bokeh/schemes/blackly.py +76 -0
  46. backtrader/bokeh/schemes/scheme.py +150 -0
  47. backtrader/bokeh/schemes/tradimo.py +82 -0
  48. backtrader/bokeh/tab.py +125 -0
  49. backtrader/bokeh/tabs/__init__.py +30 -0
  50. backtrader/bokeh/tabs/analyzer.py +120 -0
  51. backtrader/bokeh/tabs/config.py +154 -0
  52. backtrader/bokeh/tabs/live.py +109 -0
  53. backtrader/bokeh/tabs/log.py +185 -0
  54. backtrader/bokeh/tabs/metadata.py +182 -0
  55. backtrader/bokeh/tabs/performance.py +359 -0
  56. backtrader/bokeh/tabs/source.py +70 -0
  57. backtrader/bokeh/utils/__init__.py +8 -0
  58. backtrader/bokeh/utils/helpers.py +167 -0
  59. backtrader/bokeh/webapp.py +164 -0
  60. backtrader/broker.py +478 -0
  61. backtrader/brokers/__init__.py +36 -0
  62. backtrader/brokers/bbroker.py +2576 -0
  63. backtrader/brokers/btapibroker.py +8227 -0
  64. backtrader/brokers/hft/__init__.py +89 -0
  65. backtrader/brokers/hft/binance_bbo.py +625 -0
  66. backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
  67. backtrader/brokers/hft/examples.py +1228 -0
  68. backtrader/brokers/hft/exchange.py +380 -0
  69. backtrader/brokers/hft/latency.py +309 -0
  70. backtrader/brokers/hft/matching_core.py +572 -0
  71. backtrader/brokers/hft/queue.py +238 -0
  72. backtrader/brokers/hft/recorder.py +88 -0
  73. backtrader/brokers/hft/state.py +138 -0
  74. backtrader/brokers/impact_models.py +118 -0
  75. backtrader/brokers/mixbroker.py +895 -0
  76. backtrader/brokers/tickbroker.py +1991 -0
  77. backtrader/btrun/__init__.py +12 -0
  78. backtrader/btrun/btrun.py +1218 -0
  79. backtrader/cerebro.py +828 -0
  80. backtrader/channel.py +682 -0
  81. backtrader/channels/__init__.py +23 -0
  82. backtrader/channels/bridge.py +186 -0
  83. backtrader/channels/funding.py +248 -0
  84. backtrader/channels/live_queue.py +216 -0
  85. backtrader/channels/live_validator.py +294 -0
  86. backtrader/channels/orderbook.py +257 -0
  87. backtrader/channels/tick.py +202 -0
  88. backtrader/comminfo.py +665 -0
  89. backtrader/commissions/__init__.py +106 -0
  90. backtrader/commissions/ctpoption.py +993 -0
  91. backtrader/configs/account_config_example.yaml +8 -0
  92. backtrader/dataseries.py +379 -0
  93. backtrader/errors.py +106 -0
  94. backtrader/events.py +980 -0
  95. backtrader/feed.py +1523 -0
  96. backtrader/feeds/__init__.py +75 -0
  97. backtrader/feeds/barrier.py +2006 -0
  98. backtrader/feeds/blaze.py +118 -0
  99. backtrader/feeds/btapifeed.py +1538 -0
  100. backtrader/feeds/btcsv.py +203 -0
  101. backtrader/feeds/chainer.py +114 -0
  102. backtrader/feeds/cryptohftdata.py +164 -0
  103. backtrader/feeds/csvgeneric.py +1205 -0
  104. backtrader/feeds/ctpcohort.py +1051 -0
  105. backtrader/feeds/influxfeed.py +158 -0
  106. backtrader/feeds/livefeed.py +71 -0
  107. backtrader/feeds/mixed_channel.py +108 -0
  108. backtrader/feeds/mt4csv.py +42 -0
  109. backtrader/feeds/pandafeed.py +381 -0
  110. backtrader/feeds/quandl.py +256 -0
  111. backtrader/feeds/rollover.py +229 -0
  112. backtrader/feeds/sierrachart.py +30 -0
  113. backtrader/feeds/vchart.py +162 -0
  114. backtrader/feeds/vchartcsv.py +84 -0
  115. backtrader/feeds/vchartfile.py +153 -0
  116. backtrader/feeds/yahoo.py +399 -0
  117. backtrader/fillers.py +148 -0
  118. backtrader/filters/__init__.py +34 -0
  119. backtrader/filters/bsplitter.py +127 -0
  120. backtrader/filters/calendardays.py +121 -0
  121. backtrader/filters/datafiller.py +192 -0
  122. backtrader/filters/datafilter.py +74 -0
  123. backtrader/filters/daysteps.py +96 -0
  124. backtrader/filters/heikinashi.py +63 -0
  125. backtrader/filters/renko.py +164 -0
  126. backtrader/filters/session.py +289 -0
  127. backtrader/flt.py +80 -0
  128. backtrader/functions.py +960 -0
  129. backtrader/indicator.py +449 -0
  130. backtrader/indicators/__init__.py +148 -0
  131. backtrader/indicators/accdecoscillator.py +110 -0
  132. backtrader/indicators/aroon.py +300 -0
  133. backtrader/indicators/atr.py +315 -0
  134. backtrader/indicators/awesomeoscillator.py +122 -0
  135. backtrader/indicators/basicops.py +834 -0
  136. backtrader/indicators/bollinger.py +223 -0
  137. backtrader/indicators/cci.py +89 -0
  138. backtrader/indicators/channels_ext.py +83 -0
  139. backtrader/indicators/contrib/__init__.py +228 -0
  140. backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
  141. backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
  142. backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
  143. backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
  144. backtrader/indicators/contrib/adxdmi.py +34 -0
  145. backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
  146. backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
  147. backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
  148. backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
  149. backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
  150. backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
  151. backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
  152. backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
  153. backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
  154. backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
  155. backtrader/indicators/contrib/average_change_candle.py +165 -0
  156. backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
  157. backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
  158. backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
  159. backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
  160. backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
  161. backtrader/indicators/contrib/blau_csi.py +76 -0
  162. backtrader/indicators/contrib/blau_ergodic.py +53 -0
  163. backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
  164. backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
  165. backtrader/indicators/contrib/blau_tvi.py +55 -0
  166. backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
  167. backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
  168. backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
  169. backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
  170. backtrader/indicators/contrib/bsi_indicator.py +87 -0
  171. backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
  172. backtrader/indicators/contrib/bulls_power.py +56 -0
  173. backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
  174. backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
  175. backtrader/indicators/contrib/candle_stop_color.py +46 -0
  176. backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
  177. backtrader/indicators/contrib/candlesticks_bw.py +45 -0
  178. backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
  179. backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
  180. backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
  181. backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
  182. backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
  183. backtrader/indicators/contrib/cg_oscillator.py +40 -0
  184. backtrader/indicators/contrib/close_line_cci.py +38 -0
  185. backtrader/indicators/contrib/close_price_fractals.py +47 -0
  186. backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
  187. backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
  188. backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
  189. backtrader/indicators/contrib/color_hma.py +71 -0
  190. backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
  191. backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
  192. backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
  193. backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
  194. backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
  195. backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
  196. backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
  197. backtrader/indicators/contrib/color_x2_ma.py +49 -0
  198. backtrader/indicators/contrib/color_x_derivative.py +63 -0
  199. backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
  200. backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
  201. backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
  202. backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
  203. backtrader/indicators/contrib/derivative_indicator.py +95 -0
  204. backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
  205. backtrader/indicators/contrib/digital_macd.py +200 -0
  206. backtrader/indicators/contrib/donchian_channels_system.py +45 -0
  207. backtrader/indicators/contrib/dots_indicator.py +93 -0
  208. backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
  209. backtrader/indicators/contrib/ema_rsi_va.py +80 -0
  210. backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
  211. backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
  212. backtrader/indicators/contrib/fatl_filter.py +179 -0
  213. backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
  214. backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
  215. backtrader/indicators/contrib/fisher_org_v1.py +102 -0
  216. backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
  217. backtrader/indicators/contrib/force_index_ema.py +96 -0
  218. backtrader/indicators/contrib/force_index_ema_2.py +27 -0
  219. backtrader/indicators/contrib/forecast_oscilator.py +145 -0
  220. backtrader/indicators/contrib/fractal_amambk.py +81 -0
  221. backtrader/indicators/contrib/frama_series.py +84 -0
  222. backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
  223. backtrader/indicators/contrib/go_indicator.py +93 -0
  224. backtrader/indicators/contrib/hlr_indicator.py +95 -0
  225. backtrader/indicators/contrib/hma.py +50 -0
  226. backtrader/indicators/contrib/i4_drfv2.py +34 -0
  227. backtrader/indicators/contrib/i4_drfv3.py +38 -0
  228. backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
  229. backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
  230. backtrader/indicators/contrib/i_gap_indicator.py +45 -0
  231. backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
  232. backtrader/indicators/contrib/i_trend_indicator.py +125 -0
  233. backtrader/indicators/contrib/iamma_indicator.py +39 -0
  234. backtrader/indicators/contrib/indexed_moving_average.py +33 -0
  235. backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
  236. backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
  237. backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
  238. backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
  239. backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
  240. backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
  241. backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
  242. backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
  243. backtrader/indicators/contrib/kalman_filter_line.py +127 -0
  244. backtrader/indicators/contrib/kama_indicator.py +150 -0
  245. backtrader/indicators/contrib/karacatica_indicator.py +99 -0
  246. backtrader/indicators/contrib/kdj_indicator.py +59 -0
  247. backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
  248. backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
  249. backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
  250. backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
  251. backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
  252. backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
  253. backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
  254. backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
  255. backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
  256. backtrader/indicators/contrib/loco_indicator.py +88 -0
  257. backtrader/indicators/contrib/lrma_indicator.py +185 -0
  258. backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
  259. backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
  260. backtrader/indicators/contrib/macd2_indicator.py +61 -0
  261. backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
  262. backtrader/indicators/contrib/malr_indicator.py +77 -0
  263. backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
  264. backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
  265. backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
  266. backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
  267. backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
  268. backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
  269. backtrader/indicators/contrib/nrtr_indicator.py +95 -0
  270. backtrader/indicators/contrib/p_channel_system.py +40 -0
  271. backtrader/indicators/contrib/percent_envelope.py +37 -0
  272. backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
  273. backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
  274. backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
  275. backtrader/indicators/contrib/price_extreme_channel.py +35 -0
  276. backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
  277. backtrader/indicators/contrib/ravi_indicator.py +40 -0
  278. backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
  279. backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
  280. backtrader/indicators/contrib/renko_level.py +85 -0
  281. backtrader/indicators/contrib/renko_line_break.py +91 -0
  282. backtrader/indicators/contrib/rftl_indicator.py +41 -0
  283. backtrader/indicators/contrib/rkd_indicator.py +53 -0
  284. backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
  285. backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
  286. backtrader/indicators/contrib/rsi_slowdown.py +57 -0
  287. backtrader/indicators/contrib/rsioma_v2.py +41 -0
  288. backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
  289. backtrader/indicators/contrib/safe_adx.py +89 -0
  290. backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
  291. backtrader/indicators/contrib/sidus_indicator.py +105 -0
  292. backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
  293. backtrader/indicators/contrib/sliding_range_color.py +56 -0
  294. backtrader/indicators/contrib/slow_stoch.py +42 -0
  295. backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
  296. backtrader/indicators/contrib/smoothed_rsi.py +31 -0
  297. backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
  298. backtrader/indicators/contrib/stalin_indicator.py +152 -0
  299. backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
  300. backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
  301. backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
  302. backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
  303. backtrader/indicators/contrib/t3_average.py +76 -0
  304. backtrader/indicators/contrib/t3_indicator.py +40 -0
  305. backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
  306. backtrader/indicators/contrib/three_candles_indicator.py +70 -0
  307. backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
  308. backtrader/indicators/contrib/time_line.py +57 -0
  309. backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
  310. backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
  311. backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
  312. backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
  313. backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
  314. backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
  315. backtrader/indicators/contrib/trigger_line.py +66 -0
  316. backtrader/indicators/contrib/triple_ema_rate.py +34 -0
  317. backtrader/indicators/contrib/trvi_indicator.py +194 -0
  318. backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
  319. backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
  320. backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
  321. backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
  322. backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
  323. backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
  324. backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
  325. backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
  326. backtrader/indicators/contrib/vwma_candle.py +57 -0
  327. backtrader/indicators/contrib/vwma_digit_system.py +70 -0
  328. backtrader/indicators/contrib/wami.py +43 -0
  329. backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
  330. backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
  331. backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
  332. backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
  333. backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
  334. backtrader/indicators/contrib/xma_ichimoku.py +163 -0
  335. backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
  336. backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
  337. backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
  338. backtrader/indicators/contrib/xmacd_indicator.py +70 -0
  339. backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
  340. backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
  341. backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
  342. backtrader/indicators/contrib/xrvi_indicator.py +130 -0
  343. backtrader/indicators/contrib/zero_lag_macd.py +36 -0
  344. backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
  345. backtrader/indicators/contrib/zpf_indicator.py +115 -0
  346. backtrader/indicators/crossover.py +337 -0
  347. backtrader/indicators/dema.py +175 -0
  348. backtrader/indicators/demarker.py +270 -0
  349. backtrader/indicators/deviation.py +284 -0
  350. backtrader/indicators/directionalmove.py +1071 -0
  351. backtrader/indicators/dma.py +112 -0
  352. backtrader/indicators/dpo.py +96 -0
  353. backtrader/indicators/dv2.py +56 -0
  354. backtrader/indicators/ema.py +145 -0
  355. backtrader/indicators/envelope.py +475 -0
  356. backtrader/indicators/hadelta.py +198 -0
  357. backtrader/indicators/heikinashi.py +153 -0
  358. backtrader/indicators/hma.py +153 -0
  359. backtrader/indicators/hurst.py +151 -0
  360. backtrader/indicators/ichimoku.py +267 -0
  361. backtrader/indicators/kama.py +181 -0
  362. backtrader/indicators/kst.py +159 -0
  363. backtrader/indicators/lrsi.py +125 -0
  364. backtrader/indicators/mabase.py +147 -0
  365. backtrader/indicators/macd.py +322 -0
  366. backtrader/indicators/momentum.py +267 -0
  367. backtrader/indicators/moneyflow.py +237 -0
  368. backtrader/indicators/mt5atr.py +124 -0
  369. backtrader/indicators/myind.py +179 -0
  370. backtrader/indicators/obv.py +94 -0
  371. backtrader/indicators/ols.py +265 -0
  372. backtrader/indicators/oscillator.py +161 -0
  373. backtrader/indicators/percentchange.py +83 -0
  374. backtrader/indicators/percentrank.py +46 -0
  375. backtrader/indicators/pivotpoint.py +469 -0
  376. backtrader/indicators/prettygoodoscillator.py +113 -0
  377. backtrader/indicators/priceops_ext.py +123 -0
  378. backtrader/indicators/priceoscillator.py +262 -0
  379. backtrader/indicators/psar.py +212 -0
  380. backtrader/indicators/rmi.py +69 -0
  381. backtrader/indicators/rsi.py +440 -0
  382. backtrader/indicators/sma.py +141 -0
  383. backtrader/indicators/smma.py +116 -0
  384. backtrader/indicators/spread.py +54 -0
  385. backtrader/indicators/stochastic.py +263 -0
  386. backtrader/indicators/supertrend.py +436 -0
  387. backtrader/indicators/trend_ext.py +105 -0
  388. backtrader/indicators/trix.py +202 -0
  389. backtrader/indicators/tsi.py +155 -0
  390. backtrader/indicators/ultimateoscillator.py +158 -0
  391. backtrader/indicators/vortex.py +62 -0
  392. backtrader/indicators/williams.py +194 -0
  393. backtrader/indicators/wma.py +103 -0
  394. backtrader/indicators/zlema.py +135 -0
  395. backtrader/indicators/zlind.py +104 -0
  396. backtrader/linebuffer.py +3155 -0
  397. backtrader/lineiterator.py +2911 -0
  398. backtrader/lineroot.py +1106 -0
  399. backtrader/lineseries.py +2559 -0
  400. backtrader/live_trading/__init__.py +31 -0
  401. backtrader/live_trading/interface.py +404 -0
  402. backtrader/mathsupport.py +94 -0
  403. backtrader/metabase.py +1804 -0
  404. backtrader/mixins/__init__.py +21 -0
  405. backtrader/mixins/singleton.py +118 -0
  406. backtrader/observer.py +106 -0
  407. backtrader/observers/__init__.py +45 -0
  408. backtrader/observers/benchmark.py +126 -0
  409. backtrader/observers/broker.py +184 -0
  410. backtrader/observers/buysell.py +144 -0
  411. backtrader/observers/drawdown.py +161 -0
  412. backtrader/observers/logreturns.py +113 -0
  413. backtrader/observers/timereturn.py +86 -0
  414. backtrader/observers/trade_logger.py +2972 -0
  415. backtrader/observers/tradelogger.py +6 -0
  416. backtrader/observers/trades.py +258 -0
  417. backtrader/order.py +1114 -0
  418. backtrader/parameters.py +2345 -0
  419. backtrader/plot/__init__.py +54 -0
  420. backtrader/plot/finance.py +1022 -0
  421. backtrader/plot/formatters.py +200 -0
  422. backtrader/plot/locator.py +353 -0
  423. backtrader/plot/multicursor.py +495 -0
  424. backtrader/plot/plot.py +2500 -0
  425. backtrader/plot/plot_plotly.py +1351 -0
  426. backtrader/plot/scheme.py +253 -0
  427. backtrader/plot/utils.py +104 -0
  428. backtrader/position.py +290 -0
  429. backtrader/position_modes.py +132 -0
  430. backtrader/profiles.py +254 -0
  431. backtrader/reports/__init__.py +39 -0
  432. backtrader/reports/charts.py +371 -0
  433. backtrader/reports/performance.py +620 -0
  434. backtrader/reports/reporter.py +660 -0
  435. backtrader/resamplerfilter.py +1001 -0
  436. backtrader/signal.py +118 -0
  437. backtrader/signals/__init__.py +17 -0
  438. backtrader/sizer.py +114 -0
  439. backtrader/sizers/__init__.py +26 -0
  440. backtrader/sizers/fixedsize.py +161 -0
  441. backtrader/sizers/percents_sizer.py +119 -0
  442. backtrader/store.py +221 -0
  443. backtrader/stores/__init__.py +33 -0
  444. backtrader/stores/btapistore.py +15506 -0
  445. backtrader/stores/livestore.py +137 -0
  446. backtrader/stores/vchartfile.py +96 -0
  447. backtrader/strategy.py +3655 -0
  448. backtrader/talib.py +280 -0
  449. backtrader/test_helpers.py +96 -0
  450. backtrader/timer.py +358 -0
  451. backtrader/trade.py +442 -0
  452. backtrader/tradingcal.py +361 -0
  453. backtrader/utils/__init__.py +68 -0
  454. backtrader/utils/autodict.py +251 -0
  455. backtrader/utils/date.py +71 -0
  456. backtrader/utils/dateintern.py +509 -0
  457. backtrader/utils/flushfile.py +94 -0
  458. backtrader/utils/fractal.py +101 -0
  459. backtrader/utils/get_metrics.py +101 -0
  460. backtrader/utils/load_data.py +209 -0
  461. backtrader/utils/log_message.py +998 -0
  462. backtrader/utils/ordereddefaultdict.py +75 -0
  463. backtrader/utils/py3.py +296 -0
  464. backtrader/version.py +21 -0
  465. backtrader/writer.py +372 -0
@@ -0,0 +1,895 @@
1
+ """Mixed-mode broker for tick-driven mid-frequency coordination.
2
+
3
+ MixBroker keeps TickBroker as the only execution path for orders while
4
+ maintaining low-frequency bar state and high-frequency order book windows
5
+ for strategy-side queries.
6
+
7
+ Example:
8
+ Using MixBroker with Cerebro:
9
+ cerebro = bt.Cerebro()
10
+ cerebro.setbroker(MixBroker(cash=100000))
11
+ """
12
+
13
+ import collections
14
+ import copy
15
+ import hashlib
16
+ import json
17
+ import os
18
+ import time
19
+ import uuid
20
+ from decimal import Decimal, InvalidOperation
21
+ from pathlib import Path
22
+
23
+ try:
24
+ import fcntl as _fcntl
25
+ except ImportError: # pragma: no cover - unavailable on Windows
26
+ _fcntl = None
27
+
28
+ try:
29
+ import msvcrt as _msvcrt
30
+ except ImportError: # pragma: no cover - unavailable on POSIX hosts
31
+ _msvcrt = None
32
+
33
+ from backtrader.brokers.tickbroker import TickBroker
34
+ from backtrader.parameters import ParameterDescriptor
35
+
36
+ from ..utils.log_message import get_logger
37
+
38
+ logger = get_logger(__name__)
39
+
40
+ __all__ = ["MixBroker", "MidFreqContext"]
41
+
42
+
43
+ def _acquire_nonblocking_file_lock(handle):
44
+ """Acquire one portable, process-scoped exclusive lock for ``handle``.
45
+
46
+ ``fcntl.flock`` is not available on Windows. The Windows CRT lock is
47
+ byte-range based, so reserve byte zero in the lock file before taking its
48
+ non-blocking lock. The lock file's JSON payload remains diagnostic only;
49
+ ownership is still established by the operating-system lease.
50
+ """
51
+
52
+ if _fcntl is not None:
53
+ _fcntl.flock(handle.fileno(), _fcntl.LOCK_EX | _fcntl.LOCK_NB)
54
+ return
55
+ if _msvcrt is not None:
56
+ handle.seek(0, os.SEEK_END)
57
+ if handle.tell() == 0:
58
+ handle.write(" ")
59
+ handle.flush()
60
+ handle.seek(0)
61
+ try:
62
+ _msvcrt.locking(handle.fileno(), _msvcrt.LK_NBLCK, 1)
63
+ except OSError as exc:
64
+ raise BlockingIOError from exc
65
+ return
66
+ raise RuntimeError("account_risk_locking_unavailable")
67
+
68
+
69
+ def _release_file_lock(handle):
70
+ """Release a lock acquired by :func:`_acquire_nonblocking_file_lock`."""
71
+
72
+ if _fcntl is not None:
73
+ _fcntl.flock(handle.fileno(), _fcntl.LOCK_UN)
74
+ elif _msvcrt is not None:
75
+ handle.seek(0)
76
+ _msvcrt.locking(handle.fileno(), _msvcrt.LK_UNLCK, 1)
77
+
78
+
79
+ def _fsync_directory(path):
80
+ """Persist a POSIX rename without treating unsupported Windows APIs as failure."""
81
+
82
+ # Windows cannot open a directory through ``os.open`` for ``os.fsync``.
83
+ # The replacement has already been flushed and fsynced; that is the
84
+ # strongest portable guarantee exposed by the standard library there.
85
+ if os.name == "nt":
86
+ return
87
+ descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
88
+ try:
89
+ os.fsync(descriptor)
90
+ finally:
91
+ os.close(descriptor)
92
+
93
+
94
+ def _durable_replace(source, target):
95
+ """Replace one same-directory ledger file with the strongest host primitive."""
96
+
97
+ if os.name != "nt":
98
+ os.replace(source, target)
99
+ return
100
+
101
+ # ``os.replace`` does not request a write-through move on Windows. The
102
+ # ledger is the fence that blocks unsafe paper trading after a crash, so
103
+ # fail closed if the Win32 durable move cannot be requested.
104
+ import ctypes
105
+
106
+ move_file = ctypes.WinDLL("kernel32", use_last_error=True).MoveFileExW
107
+ move_file.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint)
108
+ move_file.restype = ctypes.c_int
109
+ movefile_replace_existing = 0x00000001
110
+ movefile_write_through = 0x00000008
111
+ if not move_file(str(source), str(target), movefile_replace_existing | movefile_write_through):
112
+ error_code = ctypes.get_last_error()
113
+ raise OSError(error_code, "MoveFileExW write-through replacement failed", str(target))
114
+
115
+
116
+ class MixBroker(TickBroker):
117
+ """Experimental coordination broker for mid-frequency backtests.
118
+
119
+ :class:`MixBroker` extends :class:`TickBroker` to maintain additional
120
+ mid-frequency state on top of the tick-driven execution path. In
121
+ particular it keeps:
122
+
123
+ * a per-symbol rolling window of recent order book snapshots
124
+ (size controlled by :attr:`max_ob_window`),
125
+ * a per-symbol rolling buffer of completed bars
126
+ (size controlled by :attr:`max_bar_history`),
127
+ * an incrementally maintained simple moving average per symbol
128
+ (period controlled by :attr:`default_sma_period`).
129
+
130
+ Strategies read this state through the broker's
131
+ :class:`MidFreqContext`, which exposes deep-copied views so the
132
+ underlying buffers are never mutated by user code.
133
+
134
+ Attributes:
135
+ max_ob_window: Per-symbol order book window size (deque maxlen).
136
+ max_bar_history: Per-symbol completed bar history size (deque maxlen).
137
+ default_sma_period: Period of the incrementally maintained SMA.
138
+ """
139
+
140
+ max_ob_window = ParameterDescriptor(default=100, doc="Per-symbol order book window size")
141
+ max_bar_history = ParameterDescriptor(default=200, doc="Per-symbol completed bar history size")
142
+ default_sma_period = ParameterDescriptor(default=20, doc="Incrementally maintained SMA period")
143
+ account_risk_ledger_path = ParameterDescriptor(
144
+ default=None,
145
+ doc="Ignored local path for durable paper account-risk evidence",
146
+ )
147
+ account_risk_venues = ParameterDescriptor(
148
+ default=(),
149
+ doc="Canonical provider ids covered by the paper account-risk ledger",
150
+ )
151
+ account_risk_persist_interval = ParameterDescriptor(
152
+ default=0.05,
153
+ doc="Minimum seconds between mark-to-market ledger writes",
154
+ )
155
+
156
+ def __init__(self, **kwargs):
157
+ """Initialize the broker and its mid-frequency state containers.
158
+
159
+ Args:
160
+ **kwargs: Forwarded to :class:`TickBroker`'s constructor.
161
+ """
162
+ super().__init__(**kwargs)
163
+ self._account_risk_lock_handle = None
164
+ self._account_risk_lock_acquired = False
165
+ self._account_risk_owner_token = uuid.uuid4().hex
166
+ self._account_risk_realized_net = Decimal("0")
167
+ self._account_risk_last_persist_ns = 0
168
+ self._account_risk_failed = True
169
+ self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
170
+ "account_risk_ledger_not_started"
171
+ )
172
+ self._reset_midfreq_state()
173
+
174
+ def start(self):
175
+ """Reset the mid-frequency state containers when the run starts.
176
+
177
+ The base :class:`TickBroker` is started first; this method then
178
+ rebuilds the per-symbol order book window, completed-bar
179
+ history, and indicator buffers.
180
+ """
181
+ super().start()
182
+ self._reset_midfreq_state()
183
+ self._start_account_risk_ledger()
184
+
185
+ @staticmethod
186
+ def _canonical_risk_venue(venue):
187
+ return str(venue or "").partition("___")[0].strip().lower()
188
+
189
+ def _configured_risk_venues(self):
190
+ raw = self.get_param("account_risk_venues") or ()
191
+ if isinstance(raw, str):
192
+ raw = [item for item in raw.split(",") if item.strip()]
193
+ return sorted(
194
+ {
195
+ self._canonical_risk_venue(venue)
196
+ for venue in raw
197
+ if self._canonical_risk_venue(venue)
198
+ }
199
+ )
200
+
201
+ def _account_risk_identity_sha256(self):
202
+ """Bind paper-risk evidence to its exact durable ledger and venue set."""
203
+ ledger_value = self.get_param("account_risk_ledger_path")
204
+ payload = {
205
+ "authority": "MixBroker",
206
+ "configured_venues": self._configured_risk_venues(),
207
+ "ledger_path": (
208
+ str(Path(ledger_value).expanduser().resolve())
209
+ if ledger_value not in (None, "")
210
+ else ""
211
+ ),
212
+ }
213
+ encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
214
+ return hashlib.sha256(encoded).hexdigest()
215
+
216
+ def _unavailable_account_risk_snapshot(self, error_code):
217
+ return {
218
+ "baseline_equity": None,
219
+ "current_equity": None,
220
+ "realized_net": None,
221
+ "configured_venues": self._configured_risk_venues(),
222
+ "generation": 0,
223
+ "fencing_epoch": 0,
224
+ "as_of_monotonic_ns": 0,
225
+ "owner_pid": os.getpid(),
226
+ "clock_domain_id": f"process:{os.getpid()}:monotonic",
227
+ "identity_binding_sha256": self._account_risk_identity_sha256(),
228
+ "durable": False,
229
+ "trading_blocked": True,
230
+ "evidence_complete": False,
231
+ "evidence_errors": [str(error_code)],
232
+ "error_code": str(error_code),
233
+ }
234
+
235
+ @staticmethod
236
+ def _decimal(value, name):
237
+ try:
238
+ result = Decimal(str(value))
239
+ except (InvalidOperation, TypeError, ValueError) as exc:
240
+ logger.error(
241
+ "mixbroker:161 re-raising InvalidOperation,TypeError,ValueError", exc_info=True
242
+ )
243
+ raise ValueError(f"invalid_{name}") from exc
244
+ if not result.is_finite():
245
+ raise ValueError(f"invalid_{name}")
246
+ return result
247
+
248
+ def _start_account_risk_ledger(self):
249
+ """Acquire one writer lease and continue the durable paper account."""
250
+ self._release_account_risk_ledger()
251
+ self._account_risk_owner_token = uuid.uuid4().hex
252
+ self._account_risk_realized_net = Decimal("0")
253
+ self._account_risk_last_persist_ns = 0
254
+ self._account_risk_failed = True
255
+ ledger_value = self.get_param("account_risk_ledger_path")
256
+ venues = self._configured_risk_venues()
257
+ if ledger_value in (None, ""):
258
+ self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
259
+ "account_risk_ledger_path_required"
260
+ )
261
+ return
262
+ if not venues:
263
+ self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
264
+ "account_risk_venues_required"
265
+ )
266
+ return
267
+ if _fcntl is None and _msvcrt is None:
268
+ self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
269
+ "account_risk_locking_unavailable"
270
+ )
271
+ return
272
+
273
+ ledger_path = Path(ledger_value).expanduser().resolve()
274
+ lock_path = ledger_path.with_name(f"{ledger_path.name}.lock")
275
+ try:
276
+ ledger_path.parent.mkdir(parents=True, exist_ok=True)
277
+ lock_handle = lock_path.open("a+", encoding="utf-8")
278
+ self._account_risk_lock_handle = lock_handle
279
+ _acquire_nonblocking_file_lock(lock_handle)
280
+ self._account_risk_lock_acquired = True
281
+
282
+ generation = 1
283
+ fencing_epoch = 1
284
+ baseline = self._decimal(self.getvalue(), "baseline_equity")
285
+ current = baseline
286
+ if ledger_path.exists():
287
+ prior = json.loads(ledger_path.read_text(encoding="utf-8"))
288
+ if not isinstance(prior, dict) or prior.get("schema_version") != 2:
289
+ raise ValueError("account_risk_ledger_schema_invalid")
290
+ if prior.get("session_state") != "closed":
291
+ raise ValueError("account_risk_ledger_previous_session_active")
292
+ prior_venues = sorted(
293
+ self._canonical_risk_venue(venue)
294
+ for venue in prior.get("configured_venues", ())
295
+ )
296
+ if prior_venues != venues:
297
+ raise ValueError("account_risk_ledger_venues_mismatch")
298
+ generation = int(prior.get("generation", 0)) + 1
299
+ fencing_epoch = int(prior.get("fencing_epoch", 0)) + 1
300
+ if generation <= 1 or fencing_epoch <= 1:
301
+ raise ValueError("account_risk_ledger_fence_invalid")
302
+ baseline = self._decimal(prior.get("baseline_equity"), "baseline_equity")
303
+ current = self._decimal(prior.get("current_equity"), "current_equity")
304
+ realized = self._decimal(prior.get("realized_net"), "realized_net")
305
+ if baseline <= 0:
306
+ raise ValueError("account_risk_ledger_baseline_invalid")
307
+ self._account_risk_realized_net = realized
308
+ # A sealed paper epoch is required to be flat. Hydrate the
309
+ # new in-memory broker from its durable equity instead of
310
+ # silently restoring the configured starting cash.
311
+ self._cash = float(current)
312
+ self._value = self._cash
313
+ self.startingcash = self._cash
314
+ self.startingvalue = self._value
315
+
316
+ lock_handle.seek(0)
317
+ lock_handle.truncate()
318
+ json.dump(
319
+ {
320
+ "owner_token": self._account_risk_owner_token,
321
+ "owner_pid": os.getpid(),
322
+ "generation": generation,
323
+ "fencing_epoch": fencing_epoch,
324
+ },
325
+ lock_handle,
326
+ separators=(",", ":"),
327
+ )
328
+ lock_handle.flush()
329
+ os.fsync(lock_handle.fileno())
330
+ self._account_risk_snapshot = {
331
+ "baseline_equity": baseline,
332
+ "current_equity": current,
333
+ "realized_net": self._account_risk_realized_net,
334
+ "configured_venues": venues,
335
+ "generation": generation,
336
+ "fencing_epoch": fencing_epoch,
337
+ "as_of_monotonic_ns": 0,
338
+ "owner_pid": os.getpid(),
339
+ "clock_domain_id": f"process:{os.getpid()}:monotonic",
340
+ "identity_binding_sha256": self._account_risk_identity_sha256(),
341
+ "durable": False,
342
+ "trading_blocked": True,
343
+ "evidence_complete": False,
344
+ }
345
+ self._account_risk_failed = False
346
+ self._persist_account_risk_snapshot(force=True)
347
+ except Exception as exc:
348
+ logger.warning(
349
+ "MixBroker account-risk ledger startup failed: %s",
350
+ type(exc).__name__,
351
+ exc_info=True,
352
+ )
353
+ code = "account_risk_ledger_locked" if isinstance(exc, BlockingIOError) else str(exc)
354
+ if not code.startswith("account_risk_"):
355
+ code = "account_risk_ledger_start_failed"
356
+ self._account_risk_snapshot = self._unavailable_account_risk_snapshot(code)
357
+ self._release_account_risk_ledger()
358
+
359
+ def _atomic_write_account_risk(self, payload):
360
+ ledger_path = Path(self.get_param("account_risk_ledger_path")).expanduser().resolve()
361
+ temporary = ledger_path.with_name(
362
+ f".{ledger_path.name}.{self._account_risk_owner_token}.tmp"
363
+ )
364
+ encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
365
+ descriptor = None
366
+ try:
367
+ descriptor = os.open(
368
+ temporary,
369
+ os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
370
+ 0o600,
371
+ )
372
+ with os.fdopen(descriptor, "wb") as handle:
373
+ descriptor = None
374
+ handle.write(encoded)
375
+ handle.flush()
376
+ os.fsync(handle.fileno())
377
+ _durable_replace(temporary, ledger_path)
378
+ _fsync_directory(ledger_path.parent)
379
+ finally:
380
+ if descriptor is not None:
381
+ os.close(descriptor)
382
+ try:
383
+ temporary.unlink()
384
+ except FileNotFoundError:
385
+ logger.warning("mixbroker:302 suppressed FileNotFoundError")
386
+
387
+ def _persist_account_risk_snapshot(self, *, force=False, session_state="active"):
388
+ if self._account_risk_lock_handle is None or self._account_risk_failed:
389
+ return False
390
+ now_ns = time.monotonic_ns()
391
+ interval_ns = int(
392
+ max(float(self.get_param("account_risk_persist_interval") or 0.0), 0.0) * 1_000_000_000
393
+ )
394
+ if not force and now_ns - self._account_risk_last_persist_ns < interval_ns:
395
+ return True
396
+ try:
397
+ current = self._decimal(self.getvalue(), "current_equity")
398
+ snapshot = {
399
+ **self._account_risk_snapshot,
400
+ "current_equity": current,
401
+ "realized_net": self._account_risk_realized_net,
402
+ "as_of_monotonic_ns": now_ns,
403
+ "owner_pid": os.getpid(),
404
+ "clock_domain_id": f"process:{os.getpid()}:monotonic",
405
+ "durable": True,
406
+ "trading_blocked": False,
407
+ "evidence_complete": True,
408
+ }
409
+ payload = {
410
+ **snapshot,
411
+ "schema_version": 2,
412
+ "session_state": session_state,
413
+ "broker": "MixBroker",
414
+ "baseline_equity": str(snapshot["baseline_equity"]),
415
+ "current_equity": str(snapshot["current_equity"]),
416
+ "realized_net": str(snapshot["realized_net"]),
417
+ }
418
+ self._atomic_write_account_risk(payload)
419
+ self._account_risk_snapshot = snapshot
420
+ self._account_risk_last_persist_ns = now_ns
421
+ return True
422
+ except Exception as exc:
423
+ logger.warning(
424
+ "MixBroker account-risk ledger persistence failed: %s",
425
+ type(exc).__name__,
426
+ exc_info=True,
427
+ )
428
+ self._account_risk_failed = True
429
+ self._account_risk_snapshot = {
430
+ **self._unavailable_account_risk_snapshot("account_risk_ledger_persist_failed"),
431
+ "configured_venues": self._configured_risk_venues(),
432
+ }
433
+ self._release_account_risk_ledger()
434
+ return False
435
+
436
+ def _release_account_risk_ledger(self):
437
+ handle = self._account_risk_lock_handle
438
+ self._account_risk_lock_handle = None
439
+ lock_acquired = self._account_risk_lock_acquired
440
+ self._account_risk_lock_acquired = False
441
+ if handle is None:
442
+ return
443
+ try:
444
+ if lock_acquired:
445
+ _release_file_lock(handle)
446
+ finally:
447
+ handle.close()
448
+
449
+ def get_account_risk_snapshot(self):
450
+ """Return a copy of the last atomically persisted paper-risk state."""
451
+ self._persist_account_risk_snapshot(force=False)
452
+ return copy.deepcopy(self._account_risk_snapshot)
453
+
454
+ def stop(self):
455
+ """Seal the durable paper epoch and release its single-writer lease."""
456
+ flat = not any(order.alive() for order in self.pending_orders) and not any(
457
+ abs(float(position.size or 0.0)) > 1e-12
458
+ for positions in (self.positions, self.long_positions, self.short_positions)
459
+ for position in positions.values()
460
+ )
461
+ try:
462
+ self._persist_account_risk_snapshot(
463
+ force=True,
464
+ session_state="closed" if flat else "unsafe_open_exposure",
465
+ )
466
+ finally:
467
+ self._release_account_risk_ledger()
468
+ return super().stop()
469
+
470
+ def _reset_midfreq_state(self):
471
+ """(Re)create the per-symbol windows, history buffers and context.
472
+
473
+ Allocates ``_ob_window`` and ``_completed_bars`` as defaultdicts
474
+ of bounded deques sized by ``max_ob_window`` and
475
+ ``max_bar_history`` respectively, plus per-symbol indicator
476
+ dicts and the :class:`MidFreqContext` used by strategies.
477
+ """
478
+ self._ob_window = collections.defaultdict(
479
+ lambda: collections.deque(maxlen=self.get_param("max_ob_window"))
480
+ )
481
+ self._completed_bars = collections.defaultdict(
482
+ lambda: collections.deque(maxlen=self.get_param("max_bar_history"))
483
+ )
484
+ self._bar_indicators: dict = collections.defaultdict(dict)
485
+ self._bar_indicator_state: dict = collections.defaultdict(dict)
486
+ self._context = MidFreqContext(self)
487
+
488
+ def process_tick(self, tick_event, data=None):
489
+ """Forward the tick to :class:`TickBroker` for execution."""
490
+ super().process_tick(tick_event, data)
491
+ self._persist_account_risk_snapshot(force=False)
492
+
493
+ def process_orderbook(self, ob_event, data=None):
494
+ """Forward the order-book update and append it to the per-symbol window.
495
+
496
+ Args:
497
+ ob_event: The order book event to record. Its ``symbol``
498
+ attribute selects the per-symbol window.
499
+ data: Optional feed reference forwarded to the base class.
500
+ """
501
+ super().process_orderbook(ob_event, data)
502
+ self._ob_window[ob_event.symbol].append(copy.deepcopy(ob_event))
503
+ self._persist_account_risk_snapshot(force=False)
504
+
505
+ def process_bar(self, bar_event, data=None):
506
+ """Record the completed bar and refresh its rolling indicators.
507
+
508
+ Args:
509
+ bar_event: The completed bar to record. Its ``symbol`` and
510
+ ``close`` attributes are used for window insertion and
511
+ indicator maintenance.
512
+ data: Optional feed reference (unused here, kept for
513
+ signature parity with the base class).
514
+ """
515
+ symbol = bar_event.symbol
516
+ self._completed_bars[symbol].append(copy.deepcopy(bar_event))
517
+ self._update_bar_indicators(symbol)
518
+ self._persist_account_risk_snapshot(force=False)
519
+
520
+ def _record_account_risk_fills(self, history_start):
521
+ if self._account_risk_lock_handle is None or self._account_risk_failed:
522
+ return
523
+ try:
524
+ for row in self._order_history[history_start:]:
525
+ pnl = self._decimal(row.get("pnl", 0), "realized_pnl")
526
+ commission = self._decimal(row.get("commission", 0), "commission")
527
+ self._account_risk_realized_net += pnl - commission
528
+ if len(self._order_history) > history_start:
529
+ self._persist_account_risk_snapshot(force=True)
530
+ except Exception:
531
+ logger.warning("mixbroker:443 fallback on Exception")
532
+ self._account_risk_failed = True
533
+ self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
534
+ "account_risk_fill_accounting_failed"
535
+ )
536
+ self._release_account_risk_ledger()
537
+
538
+ def _execute(self, order, fill_price, fill_size, event, source="tick"):
539
+ history_start = len(self._order_history)
540
+ result = super()._execute(order, fill_price, fill_size, event, source=source)
541
+ if not self._is_dual_side_mode():
542
+ self._record_account_risk_fills(history_start)
543
+ return result
544
+
545
+ def _execute_dual_side(self, order, fill_price, fill_size, event, source="tick"):
546
+ history_start = len(self._order_history)
547
+ result = super()._execute_dual_side(order, fill_price, fill_size, event, source=source)
548
+ self._record_account_risk_fills(history_start)
549
+ return result
550
+
551
+ def _update_bar_indicators(self, symbol):
552
+ """Incrementally maintain the SMA indicator for ``symbol``.
553
+
554
+ Maintains a running sum of the last ``default_sma_period``
555
+ closing prices per symbol, with a slide-in / slide-out rule
556
+ based on ``bar_count``. Once the buffer is full the SMA is
557
+ written to ``_bar_indicators[symbol][f"sma_{period}"]``; if
558
+ not, the indicator is removed (if present) so callers see
559
+ ``None`` until the buffer is warm.
560
+
561
+ Args:
562
+ symbol: The per-symbol indicator key to update.
563
+ """
564
+ bars = self._completed_bars.get(symbol)
565
+ if not bars:
566
+ return
567
+
568
+ indicators = self._bar_indicators[symbol]
569
+ indicator_state = self._bar_indicator_state[symbol]
570
+ sma_period = int(self.get_param("default_sma_period"))
571
+ if sma_period <= 0:
572
+ return
573
+
574
+ previous_sum = float(indicator_state.get("sma_sum", 0.0))
575
+ previous_len = int(indicator_state.get("bar_count", 0))
576
+ current_bar = bars[-1]
577
+ rolling_sum = previous_sum + float(current_bar.close)
578
+ if previous_len >= sma_period and len(bars) > sma_period:
579
+ rolling_sum -= float(bars[-(sma_period + 1)].close)
580
+ elif len(bars) <= sma_period:
581
+ rolling_sum = sum(float(bar.close) for bar in bars)
582
+
583
+ indicator_state["bar_count"] = len(bars)
584
+ indicator_state["sma_sum"] = rolling_sum
585
+
586
+ if len(bars) >= sma_period:
587
+ indicators[f"sma_{sma_period}"] = rolling_sum / float(sma_period)
588
+ else:
589
+ indicators.pop(f"sma_{sma_period}", None)
590
+
591
+ def get_context(self):
592
+ """Return the singleton :class:`MidFreqContext` for strategy queries."""
593
+ return self._context
594
+
595
+ def get_ob_window(self, symbol, n=30):
596
+ """Return a deep-copied list of the last ``n`` order book snapshots.
597
+
598
+ Args:
599
+ symbol: Symbol whose window to return.
600
+ n: Maximum number of snapshots to return. ``None`` returns
601
+ the entire stored window. The tail is preferred when the
602
+ window is larger than ``n``.
603
+
604
+ Returns:
605
+ list: Deep-copied order book snapshots (independent of the
606
+ broker's internal state).
607
+ """
608
+ window = list(self._ob_window.get(symbol, ()))
609
+ if n is not None:
610
+ window = window[-n:]
611
+ return [copy.deepcopy(snapshot) for snapshot in window]
612
+
613
+ def get_completed_bars(self, symbol, n=20):
614
+ """Return a deep-copied list of the last ``n`` completed bars.
615
+
616
+ Args:
617
+ symbol: Symbol whose bars to return.
618
+ n: Maximum number of bars to return. ``None`` returns the
619
+ full buffer.
620
+
621
+ Returns:
622
+ list: Deep-copied bar events.
623
+ """
624
+ bars = list(self._completed_bars.get(symbol, ()))
625
+ if n is not None:
626
+ bars = bars[-n:]
627
+ return [copy.deepcopy(bar) for bar in bars]
628
+
629
+ def get_bar_indicator(self, symbol, indicator_name):
630
+ """Return the value of a maintained per-symbol bar indicator.
631
+
632
+ Args:
633
+ symbol: Symbol the indicator is for.
634
+ indicator_name: Indicator key (e.g. ``"sma_20"``).
635
+
636
+ Returns:
637
+ float | None: The indicator value, or ``None`` if the buffer
638
+ has not warmed up or the indicator is unknown.
639
+ """
640
+ return self._bar_indicators.get(symbol, {}).get(indicator_name)
641
+
642
+ def get_symbol_snapshot(self, symbol):
643
+ """Return a deep-copied snapshot dict for ``symbol``.
644
+
645
+ Args:
646
+ symbol: Symbol to snapshot.
647
+
648
+ Returns:
649
+ dict: Snapshot produced by :meth:`MidFreqContext.snapshot`.
650
+ """
651
+ return self._context.snapshot(symbol)
652
+
653
+ def get_symbol_snapshots(self, symbols=None):
654
+ """Return deep-copied snapshots for one, many or all symbols.
655
+
656
+ Args:
657
+ symbols: Iterable of symbols to snapshot. ``None`` snapshots
658
+ every symbol currently known to the broker.
659
+
660
+ Returns:
661
+ dict: The structure produced by
662
+ :meth:`MidFreqContext.snapshot_all`.
663
+ """
664
+ return self._context.snapshot_all(symbols=symbols)
665
+
666
+
667
+ class MidFreqContext:
668
+ """Read-only strategy-facing view over a :class:`MixBroker`.
669
+
670
+ Exposes a curated set of per-symbol and per-account helpers
671
+ (last tick, last order book, completed bars, simple indicators,
672
+ position, portfolio value, full snapshots) without leaking the
673
+ broker's internal mutable state. Returned values are always
674
+ deep-copied so callers cannot accidentally mutate broker state.
675
+ """
676
+
677
+ def __init__(self, broker):
678
+ """Bind the context to its owning broker.
679
+
680
+ Args:
681
+ broker: The :class:`MixBroker` instance this context will
682
+ query.
683
+ """
684
+ self._broker = broker
685
+
686
+ def get_last_tick(self, symbol):
687
+ """Return a deep-copied last-tick event for ``symbol``.
688
+
689
+ Args:
690
+ symbol: Symbol whose last tick to return.
691
+
692
+ Returns:
693
+ object | None: Deep-copied tick event, or ``None`` if no
694
+ tick has been seen for ``symbol`` yet.
695
+ """
696
+ tick = self._broker._last_tick.get(symbol)
697
+ return copy.deepcopy(tick) if tick is not None else None
698
+
699
+ def get_last_orderbook(self, symbol):
700
+ """Return a deep-copied last order-book event for ``symbol``.
701
+
702
+ Args:
703
+ symbol: Symbol whose last order book to return.
704
+
705
+ Returns:
706
+ object | None: Deep-copied order book, or ``None`` if no
707
+ order book update has been seen for ``symbol`` yet.
708
+ """
709
+ orderbook = self._broker._last_orderbook.get(symbol)
710
+ return copy.deepcopy(orderbook) if orderbook is not None else None
711
+
712
+ def get_last_price(self, symbol):
713
+ """Return the price of the last tick for ``symbol`` (or ``None``).
714
+
715
+ This is a convenience that avoids forcing callers to fetch the
716
+ full tick event just to read ``tick.price``.
717
+
718
+ Args:
719
+ symbol: Symbol whose last-tick price to return.
720
+
721
+ Returns:
722
+ float | None: Last price, or ``None`` if no tick has been
723
+ received or the stored tick lacks a ``price`` attribute.
724
+ """
725
+ tick = self._broker._last_tick.get(symbol)
726
+ return getattr(tick, "price", None)
727
+
728
+ def get_ob_window(self, symbol, n=30):
729
+ """Delegate to :meth:`MixBroker.get_ob_window` (alias for symmetry)."""
730
+ return self._broker.get_ob_window(symbol, n)
731
+
732
+ def get_ob_ratio(self, symbol, levels=10, window=30):
733
+ """Compute the bid/ask notional ratio over the recent book window.
734
+
735
+ For each of the last ``window`` order book snapshots, sum the
736
+ ``price * qty`` product of the first ``levels`` bid and ask
737
+ entries. Returns ``bid_total / ask_total`` or ``None`` if there
738
+ are no snapshots or the ask total is effectively zero.
739
+
740
+ Args:
741
+ symbol: Symbol to compute the ratio for.
742
+ levels: Maximum depth level (per side) to include.
743
+ window: Number of recent snapshots to fold over.
744
+
745
+ Returns:
746
+ float | None: Bid/ask notional ratio, or ``None`` if the
747
+ denominator is too small to be meaningful.
748
+ """
749
+ snapshots = self._broker.get_ob_window(symbol, window)
750
+ if not snapshots:
751
+ return None
752
+
753
+ total_bid_amount = 0.0
754
+ total_ask_amount = 0.0
755
+ for snapshot in snapshots:
756
+ for level, (price, qty) in enumerate(snapshot.bids or []):
757
+ if level >= levels:
758
+ break
759
+ total_bid_amount += float(price) * float(qty)
760
+ for level, (price, qty) in enumerate(snapshot.asks or []):
761
+ if level >= levels:
762
+ break
763
+ total_ask_amount += float(price) * float(qty)
764
+
765
+ if total_ask_amount < 1e-12:
766
+ return None
767
+ return total_bid_amount / total_ask_amount
768
+
769
+ def get_completed_bars(self, symbol, n=20):
770
+ """Delegate to :meth:`MixBroker.get_completed_bars` (alias)."""
771
+ return self._broker.get_completed_bars(symbol, n)
772
+
773
+ def get_sma(self, symbol, period=20):
774
+ """Return the simple moving average of the last ``period`` closes.
775
+
776
+ Uses the maintained ``sma_{period}`` indicator when ``period ==
777
+ 20`` (the default and most common case) and falls back to
778
+ computing a fresh average from the completed bars otherwise.
779
+ Returns ``None`` when fewer than ``period`` bars are available.
780
+
781
+ Args:
782
+ symbol: Symbol to compute the SMA for.
783
+ period: Lookback period in completed bars.
784
+
785
+ Returns:
786
+ float | None: Simple moving average, or ``None`` if the
787
+ buffer is not yet warm.
788
+ """
789
+ if period == 20:
790
+ return self._broker.get_bar_indicator(symbol, "sma_20")
791
+
792
+ bars = self._broker.get_completed_bars(symbol, period)
793
+ if len(bars) < period:
794
+ return None
795
+ return sum(bar.close for bar in bars) / float(period)
796
+
797
+ def get_last_bar(self, symbol):
798
+ """Return the most recent completed bar for ``symbol``.
799
+
800
+ Args:
801
+ symbol: Symbol whose last bar to return.
802
+
803
+ Returns:
804
+ object | None: Deep-copied bar, or ``None`` if no bar has
805
+ completed for ``symbol`` yet.
806
+ """
807
+ bars = self._broker.get_completed_bars(symbol, 1)
808
+ return bars[0] if bars else None
809
+
810
+ def get_cash(self):
811
+ """Return the broker's current available cash."""
812
+ return self._broker.getcash()
813
+
814
+ def get_position(self, symbol):
815
+ """Return a deep-copied position for ``symbol``.
816
+
817
+ Args:
818
+ symbol: Symbol whose position to fetch.
819
+
820
+ Returns:
821
+ object | None: Deep-copied position, or ``None`` if no
822
+ position is currently held for ``symbol``.
823
+ """
824
+ position = self._broker._positions.get(symbol)
825
+ return copy.deepcopy(position) if position is not None else None
826
+
827
+ def get_portfolio_value(self):
828
+ """Return the broker's total portfolio value."""
829
+ return self._broker.getvalue()
830
+
831
+ def get_symbols(self):
832
+ """Return the sorted union of every symbol known to the broker.
833
+
834
+ The set is built from the keys of the last-tick, last-orderbook
835
+ and completed-bars buffers, plus the symbols of all open
836
+ positions. The result is sorted alphabetically for deterministic
837
+ iteration.
838
+
839
+ Returns:
840
+ list[str]: Sorted list of known symbol identifiers.
841
+ """
842
+ symbols = set()
843
+ symbols.update(self._broker._last_tick.keys())
844
+ symbols.update(self._broker._last_orderbook.keys())
845
+ symbols.update(self._broker._completed_bars.keys())
846
+ symbols.update(
847
+ symbol
848
+ for symbol, position in self._broker._positions.items()
849
+ if getattr(position, "size", 0)
850
+ )
851
+ return sorted(symbols)
852
+
853
+ def snapshot(self, symbol):
854
+ """Return a flat per-symbol snapshot dict.
855
+
856
+ The returned dict includes the symbol identifier, the last tick
857
+ and order book (deep-copied), the last completed bar, the
858
+ ``sma_20`` indicator, the bid/ask ratio, and the current
859
+ position. All values are deep-copied so the caller cannot
860
+ mutate the broker's state.
861
+
862
+ Args:
863
+ symbol: Symbol to snapshot.
864
+
865
+ Returns:
866
+ dict: Per-symbol snapshot as described above.
867
+ """
868
+ return {
869
+ "symbol": symbol,
870
+ "last_tick": self.get_last_tick(symbol),
871
+ "last_orderbook": self.get_last_orderbook(symbol),
872
+ "last_bar": self.get_last_bar(symbol),
873
+ "sma_20": self.get_sma(symbol, 20),
874
+ "ob_ratio": self.get_ob_ratio(symbol),
875
+ "position": self.get_position(symbol),
876
+ }
877
+
878
+ def snapshot_all(self, symbols=None):
879
+ """Return a full portfolio snapshot including cash and per-symbol state.
880
+
881
+ Args:
882
+ symbols: Iterable of symbols to include. ``None`` includes
883
+ every symbol returned by :meth:`get_symbols`. The list is
884
+ deduped and sorted for deterministic output.
885
+
886
+ Returns:
887
+ dict: ``{"cash": float, "portfolio_value": float,
888
+ "symbols": {symbol: snapshot_dict}}``.
889
+ """
890
+ selected_symbols = self.get_symbols() if symbols is None else sorted(set(symbols))
891
+ return {
892
+ "cash": self.get_cash(),
893
+ "portfolio_value": self.get_portfolio_value(),
894
+ "symbols": {symbol: self.snapshot(symbol) for symbol in selected_symbols},
895
+ }