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,2576 @@
1
+ #!/usr/bin/env python
2
+ """Back Broker Module - Backtesting broker simulation.
3
+
4
+ This module provides the BackBroker for simulating broker behavior
5
+ during backtesting.
6
+
7
+ Classes:
8
+ BackBroker: Broker simulator for backtesting (alias: BrokerBack).
9
+
10
+ Example:
11
+ >>> cerebro = bt.Cerebro()
12
+ >>> # Uses BackBroker by default
13
+ """
14
+
15
+ import collections
16
+ import datetime
17
+ import logging
18
+
19
+ from backtrader.broker import BrokerBase
20
+
21
+ # from backtrader.comminfo import CommInfoBase
22
+ from backtrader.order import BuyOrder, Order, SellOrder
23
+ from backtrader.parameters import Float, ParameterDescriptor
24
+ from backtrader.position import Position
25
+ from backtrader.position_modes import (
26
+ POSITION_MODE_DUAL_SIDE,
27
+ POSITION_SIDE_LONG,
28
+ POSITION_SIDE_SHORT,
29
+ normalize_order_position_meta,
30
+ normalize_position_mode,
31
+ normalize_position_side,
32
+ signed_position_size,
33
+ )
34
+ from backtrader.utils.log_message import _is_output_enabled_for, get_logger
35
+ from backtrader.utils.py3 import integer_types, string_types
36
+
37
+ logger = get_logger(__name__)
38
+
39
+ __all__ = ["BackBroker", "BrokerBack"]
40
+
41
+
42
+ class _CashDescriptor(ParameterDescriptor):
43
+ def __get__(self, obj, objtype=None):
44
+ if obj is None:
45
+ return self
46
+
47
+ try:
48
+ cash = object.__getattribute__(obj, "_cash")
49
+ if cash is not None:
50
+ return cash
51
+ except AttributeError:
52
+ # _cash not set yet (pre-init); fall back to the descriptor default.
53
+ pass
54
+
55
+ return super().__get__(obj, objtype)
56
+
57
+
58
+ class BackBroker(BrokerBase):
59
+ """Broker Simulator
60
+
61
+ The simulation supports different order types, checking a submitted order
62
+ cash requirements against current cash, keeping track of cash and value
63
+ for each iteration of ``cerebro`` and keeping the current position on
64
+ different datas.
65
+
66
+ *cash* is adjusted on each iteration for instruments like ``futures`` for
67
+ which a price change implies in real brokers the addition/subtraction of
68
+ cash.
69
+ # This backtesting simulation class supports different order types, checks if current cash meets the cash requirements for submitted orders,
70
+ # checks cash and value at each bar, and positions on different data feeds
71
+
72
+ Supported order types:
73
+
74
+ - ``Market``: to be executed with the 1st tick of the next bar (namely
75
+ the ``open`` price)
76
+
77
+ - ``Close``: meant for intraday in which the order is executed with the
78
+ closing price of the last bar of the session
79
+
80
+ - ``Limit``: executes if the given limit price is seen during the
81
+ session
82
+
83
+ - ``Stop``: executes a ``Market`` order if the given stop price is seen
84
+
85
+ - ``StopLimit``: sets a ``Limit`` order in motion if the given stop
86
+ price is seen
87
+
88
+ # Supported order types include the five basic types above. In fact, there are other order types supported. Refer to previous tutorials
89
+ # https://blog.csdn.net/qq_26948675/article/details/122868368
90
+
91
+ Because the broker is instantiated by ``Cerebro`` and there should be
92
+ (mostly) no reason to replace the broker, the params are not controlled
93
+ by the user for the instance. To change this there are two options:
94
+
95
+ 1. Manually create an instance of this class with the desired params
96
+ and use ``cerebro.broker = instance`` to set the instance as the
97
+ broker for the ``run`` execution
98
+
99
+ 2. Use the ``set_xxx`` to set the value using
100
+ ``cerebro.broker.set_xxx`` where ```xxx`` stands for the name of the
101
+ parameter to set
102
+
103
+ .. note::
104
+
105
+ ``cerebro.broker`` is a *property* supported by the ``getbroker``
106
+ and ``setbroker`` methods of ``Cerebro``
107
+
108
+ # Normally there is no need to set broker parameters. If setting is needed, there are usually two methods: first is to create a broker instance, then cerebro.broker = instance
109
+ # The second method is to use cerebro.broker.set_xxx to set different parameters
110
+
111
+
112
+ Params:
113
+ # The meanings of some parameters are below
114
+
115
+ - ``cash`` (default: ``10000``): starting cash
116
+ # cash is the starting capital amount, default is 10000
117
+
118
+ - ``commission`` (default: ``CommInfoBase(percabs=True)``)
119
+ base commission scheme which applies to all assets
120
+ # Commission class for how to charge commissions, margin, etc. for asset trading. Default is CommInfoBase(percabs=True)
121
+
122
+ - ``checksubmit`` (default: ``True``)
123
+ check margin/cash before accepting an order into the system
124
+ # Whether to check if margin and cash are sufficient when passing an order to the system. Default is to check
125
+
126
+ - ``eosbar`` (default: ``False``):
127
+ With intraday bars consider a bar with the same ``time`` as the end
128
+ of session to be the end of the session. This is not usually the
129
+ case, because some bars (final auction) are produced by many
130
+ exchanges for many products for a couple of minutes after the end of
131
+ the session
132
+ # End-of-session bar, default is False. For intraday bars, consider a bar with the same time as the end of session as the end of day's trading.
133
+ # However, this is usually not the case, because many assets' bars are formed through final auctions at many exchanges a few minutes after the end of the day's trading time
134
+
135
+ - ``filler`` (default: ``None``)
136
+
137
+ A callable with signature: ``callable(order, price, ago)``
138
+
139
+ - ``order``: obviously the order in execution. This provides access
140
+ to the *data* (and with it the *ohlc* and *volume* values), the
141
+ *execution type*, remaining size (``order.executed.remsize``) and
142
+ others.
143
+
144
+ Please check the ``Order`` documentation and reference for things
145
+ available inside an ``Order`` instance
146
+
147
+ - ``price`` the price at which the order is going to be executed in
148
+ the ``ago`` bar
149
+
150
+ - ``ago``: index meant to be used with ``order.data`` for the
151
+ extraction of the *ohlc* and *volume* prices. In most cases this
152
+ will be ``0`` but on a corner case for ``Close`` orders, this
153
+ will be ``-1``.
154
+
155
+ In order to get the bar volume (for example) do: ``volume =
156
+ order.data.voluume[ago]``
157
+
158
+ The callable must return the *executed size* (a value >= 0)
159
+
160
+ The callable may of course be an object with ``__call__`` matching
161
+ the aforementioned signature
162
+
163
+ With the default ``None`` orders will be completely executed in a
164
+ single shot
165
+
166
+ # filler is a callable object, default is None. In this case, all trading volume can be executed; if filler is not None,
167
+ # it will calculate the executable order size based on order, price, ago
168
+ # Reference articles: https://blog.csdn.net/qq_26948675/article/details/124566885?spm=1001.2014.3001.5501
169
+ # https://yunjinqi.blog.csdn.net/article/details/113445040
170
+
171
+
172
+ - ``slip_perc`` (default: ``0.0``) Percentage in absolute terms (and
173
+ positive) that should be used to slip prices up/down for buy/sell
174
+ orders
175
+
176
+ Note:
177
+
178
+ - ``0.01`` is ``1%``
179
+
180
+ - ``0.001`` is ``0.1%``
181
+ # Percentage slippage form
182
+
183
+ - ``slip_fixed`` (default: ``0.0``) Percentage in units (and positive)
184
+ that should be used to slip prices up/down for buy/sell orders
185
+
186
+ Note: if ``slip_perc`` is non zero, it takes precedence over this.
187
+
188
+ # Fixed slippage form. If percentage slippage is not 0, only percentage slippage is considered
189
+
190
+ - ``slip_open`` (default: ``False``) whether to slip prices for order
191
+ execution which would specifically used the *opening* price of the
192
+ next bar. An example would be ``Market`` order which is executed with
193
+ the next available tick, i.e: the opening price of the bar.
194
+
195
+ This also applies to some of the other executions, because the logic
196
+ tries to detect if the *opening* price would match the requested
197
+ price/execution type when moving to a new bar.
198
+ # Whether to use the next bar's opening price when calculating slippage
199
+
200
+ - ``slip_match`` (default: ``True``)
201
+
202
+ If ``True`` the broker will offer a match by capping slippage at
203
+ ``high/low`` prices in case they would be exceeded.
204
+
205
+ If ``False`` the broker will not match the order with the current
206
+ prices and will try execution during the next iteration
207
+ # If the price with slippage exceeds the high or low price, and if slip_match is set to True, the execution price will be calculated based on the high or low price
208
+ # If not set to True, it will wait for the next bar to attempt execution
209
+
210
+ - ``slip_limit`` (default: ``True``)
211
+
212
+ ``Limit`` orders, given the exact match price requested, will be
213
+ matched even if ``slip_match`` is ``False``.
214
+
215
+ This option controls that behavior.
216
+
217
+ If ``True``, then ``Limit`` orders will be matched by capping prices
218
+ to the ``limit`` / ``high/low`` prices
219
+
220
+ If ``False`` and slippage exceeds the cap, then there will be no
221
+ match
222
+ # Limit orders will seek strict matching, even when slip_match is False
223
+ # If slip_limit is set to True, limit orders will be executed if they are between the high and low prices
224
+ # If set to False, limit orders with slippage that exceeds high and low prices will not be executed
225
+
226
+ - ``slip_out`` (default: ``False``)
227
+
228
+ Provide *slippage* even if the price falls outside the ``high`` -
229
+ ``low`` range.
230
+ # When slip_out is set to True, slippage will be provided even if the price exceeds the high-low range
231
+
232
+ - ``coc`` (default: ``False``)
233
+
234
+ *Cheat-On-Close* Setting this to ``True`` with ``set_coc`` enables
235
+ matching a ``Market`` order to the closing price of the bar in which
236
+ the order was issued. This is actually *cheating*, because the bar
237
+ is *closed* and any order should first be matched against the prices
238
+ in the next bar
239
+ # When coc is set to True, when placing a market order, it allows execution at the closing price
240
+ - ``coo`` (default: ``False``)
241
+
242
+ *Cheat-On-Open* Setting this to ``True`` with ``set_coo`` enables
243
+ matching a ``Market`` order to the opening price, by for example
244
+ using a timer with ``cheat`` set to ``True``, because such a timer
245
+ gets executed before the broker has evaluated
246
+ # When coo is set to True, market orders are allowed to execute at the opening price, similar to tbquant mode
247
+
248
+ - ``int2pnl`` (default: ``True``)
249
+
250
+ Assign generated interest (if any) to the profit and loss of
251
+ operation that reduces a position (be it long or short). There may be
252
+ cases in which this is undesired, because different strategies are
253
+ competing and the interest would be assigned on a non-deterministic
254
+ basis to any of them.
255
+ ``int2pnl`` defaults to True, meaning generated interest cost is
256
+ transferred to the PnL of the position-reducing operation.
257
+
258
+ - ``shortcash`` (default: ``True``)
259
+
260
+ If True then cash will be increased when a stocklike asset is shorted
261
+ and the calculated value for the asset will be negative.
262
+
263
+ If ``False`` then the cash will be deducted as operation cost and the
264
+ calculated value will be positive to end up with the same amount
265
+
266
+ # For stock-like assets, if this parameter is set to True, when short selling, the available cash will increase, but the asset value will be negative
267
+ # If this parameter is set to False, when short selling, the available cash decreases, and the asset value is positive
268
+
269
+ - ``fundstartval`` (default: ``100.0``)
270
+
271
+ This parameter controls the start value for measuring the performance
272
+ in a fund-like way, i.e.: cash can be added and deducted increasing
273
+ the amount of shares. Performance is not measured using the net
274
+ asset value of the portfolio but using the value of the fund
275
+ # fundstartval will calculate performance in fund mode
276
+
277
+ - ``fundmode`` (default: ``False``)
278
+
279
+ If this is set to ``True`` analyzers like ``TimeReturn`` can
280
+ automatically calculate returns based on the fund value and not on
281
+ the total net asset value
282
+ # If fundmode is set to True, some analyzers like TimeReturn will use fund value to calculate returns
283
+
284
+ """
285
+
286
+ # Use the new parameter descriptor system
287
+ cash = _CashDescriptor(default=10000.0, type_=float, doc="Starting cash amount")
288
+
289
+ checksubmit = ParameterDescriptor(
290
+ default=True, type_=bool, doc="Check margin/cash before accepting orders"
291
+ )
292
+
293
+ eosbar = ParameterDescriptor(
294
+ default=False,
295
+ type_=bool,
296
+ doc="Consider bar with same time as end of session as end of session",
297
+ )
298
+
299
+ filler = ParameterDescriptor(default=None, doc="Volume filler callable for order execution")
300
+
301
+ slip_perc = ParameterDescriptor(
302
+ default=0.0, type_=float, validator=Float(min_val=0.0), doc="Percentage slippage for orders"
303
+ )
304
+
305
+ slip_fixed = ParameterDescriptor(
306
+ default=0.0, type_=float, validator=Float(min_val=0.0), doc="Fixed slippage for orders"
307
+ )
308
+
309
+ slip_open = ParameterDescriptor(
310
+ default=False, type_=bool, doc="Apply slippage to opening prices"
311
+ )
312
+
313
+ slip_match = ParameterDescriptor(
314
+ default=True, type_=bool, doc="Cap slippage at high/low prices"
315
+ )
316
+
317
+ slip_limit = ParameterDescriptor(
318
+ default=True, type_=bool, doc="Allow limit order matching with slippage capping"
319
+ )
320
+
321
+ slip_out = ParameterDescriptor(
322
+ default=False, type_=bool, doc="Provide slippage even outside high-low range"
323
+ )
324
+
325
+ coc = ParameterDescriptor(
326
+ default=False, type_=bool, doc="Cheat-On-Close: match market orders to closing price"
327
+ )
328
+
329
+ coo = ParameterDescriptor(
330
+ default=False, type_=bool, doc="Cheat-On-Open: match market orders to opening price"
331
+ )
332
+
333
+ int2pnl = ParameterDescriptor(
334
+ default=True, type_=bool, doc="Assign interest to profit and loss"
335
+ )
336
+
337
+ shortcash = ParameterDescriptor(
338
+ default=True, type_=bool, doc="Increase cash when shorting stocklike assets"
339
+ )
340
+
341
+ position_mode = ParameterDescriptor(default="net", doc="net | dual_side")
342
+
343
+ fundstartval = ParameterDescriptor(
344
+ default=100.0,
345
+ type_=float,
346
+ validator=Float(min_val=0.0),
347
+ doc="Starting value for fund-like performance measurement",
348
+ )
349
+
350
+ fundmode = ParameterDescriptor(
351
+ default=False, type_=bool, doc="Enable fund-like performance calculation"
352
+ )
353
+
354
+ def __init__(self, **kwargs):
355
+ """Initialize the BackBroker instance.
356
+
357
+ Args:
358
+ **kwargs: Keyword arguments for parameter initialization
359
+ """
360
+ super().__init__(**kwargs)
361
+ # Used to save order history records
362
+ self._cash_addition: collections.deque = collections.deque()
363
+ self._ocol = collections.defaultdict(list)
364
+ self._fundshares = 0.0
365
+ self._fundval = None
366
+ self._ocos = {}
367
+ self._pchildren = collections.defaultdict(collections.deque)
368
+ self.submitted: collections.deque = collections.deque()
369
+ self.notifs: collections.deque = collections.deque()
370
+ self.d_credit = collections.defaultdict(float)
371
+ self.positions = collections.defaultdict(Position)
372
+ self._no_open_positions = True
373
+ self._toactivate: collections.deque = collections.deque()
374
+ self.pending: collections.deque = collections.deque()
375
+ self.orders = []
376
+ self._unrealized = 0.0
377
+ self._leverage = 1.0
378
+ self._valuemktlever = 0.0
379
+ self._valuelever = 0.0
380
+ self._valuemkt = 0.0
381
+ self._value = 0.0
382
+ # Comment: Do not directly set self.cash = None, this will override the value in the parameter system
383
+ # Instead use _cash as an internal state variable, initialize it in init()
384
+ # NOTE: _cash stays None until init(); get_cash() uses that as the
385
+ # "not yet initialized -> fall back to the cash param" sentinel.
386
+ self._cash = None
387
+ self.startingcash = None
388
+ self._userhist = []
389
+ # Used to save fund history records
390
+ self._fundhist = []
391
+ # share_value, net asset value
392
+ # Used to save fund shares and net asset value
393
+ self._fhistlast = [float("NaN"), float("NaN")]
394
+ self.long_positions = collections.defaultdict(Position)
395
+ self.short_positions = collections.defaultdict(Position)
396
+ self._position_mode_frozen = False
397
+ self._position_mode_frozen_reason = None
398
+ position_mode = normalize_position_mode(self.get_param("position_mode"))
399
+ BrokerBase.set_param(self, "position_mode", position_mode)
400
+ self._dual_side_mode = position_mode == POSITION_MODE_DUAL_SIDE
401
+ self._shortcash = self.get_param("shortcash")
402
+ self._checksubmit = self.get_param("checksubmit")
403
+ self._int2pnl = self.get_param("int2pnl")
404
+
405
+ def init(self):
406
+ """Initialize broker state and internal data structures.
407
+
408
+ This method sets up the initial cash, positions, orders, and other
409
+ broker-related data structures. Called during cerebro initialization.
410
+ """
411
+ super().init()
412
+ # Initial cash at the start - obtained from parameter system
413
+ cash_param = self.get_param("cash")
414
+ self.startingcash = self._cash = cash_param
415
+ # Unleveraged account value
416
+ self._value = self._cash
417
+ # Unleveraged position value
418
+ self._valuemkt = 0.0 # no open position
419
+ # Leveraged account value
420
+ self._valuelever = 0.0 # no open position
421
+ # Leveraged position market value
422
+ self._valuemktlever = 0.0 # no open position
423
+ # Leverage
424
+ self._leverage = 1.0 # initially nothing is open
425
+ # Unrealized profit
426
+ self._unrealized = 0.0 # no open position
427
+ # Orders
428
+ self.orders = [] # will only be appending
429
+ # Double-ended queue
430
+ self.pending = collections.deque() # popleft and append(right)
431
+ self._toactivate = collections.deque() # to activate in next cycle
432
+ # Position
433
+ self.positions = collections.defaultdict(Position)
434
+ self._no_open_positions = True
435
+ self.long_positions = collections.defaultdict(Position)
436
+ self.short_positions = collections.defaultdict(Position)
437
+ # Interest rate
438
+ self.d_credit = collections.defaultdict(float) # credit per data
439
+ # Double-ended queue for notification info
440
+ self.notifs = collections.deque()
441
+ # Double-ended queue for submissions
442
+ self.submitted = collections.deque()
443
+
444
+ # to keep dependent orders if needed
445
+ # If independent orders need to be kept
446
+ self._pchildren = collections.defaultdict(collections.deque)
447
+ # ocos
448
+ self._ocos = {}
449
+ # ocol
450
+ self._ocol = collections.defaultdict(list)
451
+ # fund value
452
+ self._fundval = self.get_param("fundstartval") or 100.0
453
+ # fund shares
454
+ self._fundshares = self.get_param("cash") / self._fundval
455
+ # Cash addition
456
+ self._cash_addition = collections.deque()
457
+
458
+ def start(self):
459
+ """Start the broker and lock the ``position_mode`` parameter.
460
+
461
+ After the broker has been started the ``position_mode`` parameter
462
+ is frozen (see :meth:`_freeze_position_mode`). This mirrors the
463
+ behaviour of :class:`BtApiBroker` and prevents strategies from
464
+ silently switching between net and dual-side accounting part-way
465
+ through a run.
466
+
467
+ Returns:
468
+ None: The return value of the parent
469
+ :meth:`BrokerBase.start` is forwarded unchanged.
470
+ """
471
+ super().start()
472
+ self._freeze_position_mode("start()")
473
+
474
+ def set_param(self, name, value, validate=True):
475
+ """Override :meth:`BrokerBase.set_param` to guard ``position_mode`` changes.
476
+
477
+ The ``position_mode`` parameter is treated specially: it is
478
+ immutable once :meth:`start` has run (frozen via
479
+ :meth:`_freeze_position_mode`), and its raw value is normalized
480
+ through :func:`normalize_position_mode` so that the broker
481
+ always stores one of the canonical ``"net"`` /
482
+ ``"dual_side"`` strings.
483
+
484
+ Args:
485
+ name: Name of the parameter to set.
486
+ value: New value for the parameter. For ``position_mode`` the
487
+ value is normalized before being applied.
488
+ validate: When ``True`` (default), delegate to the base class
489
+ so that the registered validator runs. Set to ``False``
490
+ to bypass validation (used internally when applying
491
+ normalized values).
492
+
493
+ Returns:
494
+ The return value of :meth:`BrokerBase.set_param` after the
495
+ value has been applied.
496
+
497
+ Raises:
498
+ ValueError: If ``name == "position_mode"`` and the parameter
499
+ has already been frozen by :meth:`start`.
500
+ """
501
+ if name == "position_mode":
502
+ self._ensure_position_mode_mutable()
503
+ value = normalize_position_mode(value)
504
+ result = super().set_param(name, value, validate=validate)
505
+ if name == "position_mode":
506
+ self._dual_side_mode = value == POSITION_MODE_DUAL_SIDE
507
+ elif name == "shortcash":
508
+ self._shortcash = value
509
+ elif name == "checksubmit":
510
+ self._checksubmit = value
511
+ elif name == "int2pnl":
512
+ self._int2pnl = value
513
+ return result
514
+
515
+ def _freeze_position_mode(self, reason):
516
+ self._position_mode_frozen = True
517
+ self._position_mode_frozen_reason = reason
518
+
519
+ def _ensure_position_mode_mutable(self):
520
+ if getattr(self, "_position_mode_frozen", False):
521
+ raise ValueError(
522
+ "position_mode is frozen after "
523
+ f"{self._position_mode_frozen_reason} and cannot be changed at runtime"
524
+ )
525
+
526
+ def _is_dual_side_mode(self):
527
+ try:
528
+ return self._dual_side_mode
529
+ except AttributeError:
530
+ position_mode = normalize_position_mode(self.get_param("position_mode"))
531
+ self._dual_side_mode = position_mode == POSITION_MODE_DUAL_SIDE
532
+ return self._dual_side_mode
533
+
534
+ def _normalize_order_meta(self, isbuy, kwargs):
535
+ local_kwargs = dict(kwargs)
536
+ position_side = local_kwargs.pop("position_side", None)
537
+ offset = local_kwargs.pop("offset", None)
538
+ position_side, offset = normalize_order_position_meta(
539
+ self.get_param("position_mode"),
540
+ isbuy,
541
+ position_side=position_side,
542
+ offset=offset,
543
+ )
544
+ return position_side, offset, local_kwargs
545
+
546
+ @staticmethod
547
+ def _attach_position_meta(order, position_side=None, offset=None, **kwargs):
548
+ if position_side is not None:
549
+ order.addinfo(position_side=position_side)
550
+ if offset is not None:
551
+ order.addinfo(offset=offset)
552
+ if kwargs:
553
+ order.addinfo(**kwargs)
554
+ return order
555
+
556
+ @staticmethod
557
+ def _close_commission_role(offset):
558
+ offset_text = str(offset or "").strip().lower()
559
+ if offset_text in {"close_today", "closetoday"}:
560
+ return "close_today"
561
+ if offset_text in {"close_yesterday", "closeyesterday"}:
562
+ return "close_yesterday"
563
+ return "close"
564
+
565
+ @staticmethod
566
+ def _getcommission_role(comminfo, size, price, role):
567
+ try:
568
+ return comminfo.getcommission(size, price, role=role)
569
+ except TypeError:
570
+ return comminfo.getcommission(size, price)
571
+
572
+ @staticmethod
573
+ def _order_log_output_enabled(level):
574
+ """Return whether an opt-in sink can receive an order lifecycle event."""
575
+ return logger.isEnabledFor(level) and _is_output_enabled_for(level, logger)
576
+
577
+ def _log_order_submitted(self, order):
578
+ """Record a submitted order only after its status has transitioned."""
579
+ if not self._order_log_output_enabled(logging.INFO):
580
+ return
581
+
582
+ logger.info(
583
+ "order submitted: ref=%s side=%s size=%s price=%s data=%s",
584
+ order.ref,
585
+ "buy" if order.isbuy() else "sell",
586
+ order.size,
587
+ order.price,
588
+ getattr(order.data, "_name", ""),
589
+ )
590
+
591
+ def _log_order_canceled(self, order):
592
+ """Record a cancellation only after the order enters its terminal state."""
593
+ if not self._order_log_output_enabled(logging.INFO):
594
+ return
595
+
596
+ logger.info(
597
+ "order canceled: ref=%s side=%s size=%s price=%s data=%s",
598
+ order.ref,
599
+ "buy" if order.isbuy() else "sell",
600
+ order.size,
601
+ order.price,
602
+ getattr(order.data, "_name", ""),
603
+ )
604
+
605
+ def _log_order_rejected(self, order, reason):
606
+ """Record an order rejection with a static, caller-supplied reason."""
607
+ if not self._order_log_output_enabled(logging.WARNING):
608
+ return
609
+
610
+ logger.warning("order rejected: ref=%s reason=%s", order.ref, reason)
611
+
612
+ def _log_order_margin(self, order, reason):
613
+ """Record a terminal insufficient-cash or margin outcome."""
614
+ if not self._order_log_output_enabled(logging.WARNING):
615
+ return
616
+
617
+ logger.warning("order margin: ref=%s reason=%s", order.ref, reason)
618
+
619
+ def _log_order_executed(self, order, *, size, price, commission, cash, data):
620
+ """Record one execution bit rather than an order's remaining size."""
621
+ if not self._order_log_output_enabled(logging.INFO):
622
+ return
623
+
624
+ logger.info(
625
+ "order executed: ref=%s side=%s size=%s price=%s commission=%s cash=%s data=%s",
626
+ order.ref,
627
+ "buy" if order.isbuy() else "sell",
628
+ size,
629
+ price,
630
+ commission,
631
+ cash if cash is not None else "n/a",
632
+ getattr(data, "_name", ""),
633
+ )
634
+
635
+ @staticmethod
636
+ def _position_storage_key(data):
637
+ return data
638
+
639
+ def _get_leg_store(self, position_side):
640
+ position_side = normalize_position_side(position_side)
641
+ if position_side == POSITION_SIDE_LONG:
642
+ return self.long_positions
643
+ if position_side == POSITION_SIDE_SHORT:
644
+ return self.short_positions
645
+ raise ValueError(f"Unsupported position_side {position_side!r}")
646
+
647
+ def _get_leg_position(self, data, position_side):
648
+ return self._get_leg_store(position_side)[self._position_storage_key(data)]
649
+
650
+ def _make_signed_position(self, position_side, position):
651
+ signed_position = position.clone()
652
+ signed_position.size = signed_position_size(position_side, position.size)
653
+ if not signed_position.size:
654
+ signed_position.price = 0.0
655
+ signed_position.price_orig = 0.0
656
+ return signed_position
657
+
658
+ def _apply_signed_position(self, position_side, leg_position, signed_position):
659
+ leg_position.size = abs(float(signed_position.size or 0.0))
660
+ leg_position.price = signed_position.price if leg_position.size else 0.0
661
+ leg_position.price_orig = signed_position.price_orig if leg_position.size else 0.0
662
+ leg_position.adjbase = signed_position.adjbase
663
+ leg_position.datetime = signed_position.datetime
664
+ leg_position.updt = signed_position.updt
665
+ leg_position.upopened = abs(float(signed_position.upopened or 0.0))
666
+ leg_position.upclosed = abs(float(signed_position.upclosed or 0.0))
667
+ return leg_position
668
+
669
+ def _sync_net_position(self, data):
670
+ data_key = self._position_storage_key(data)
671
+ long_pos = self.long_positions[data_key]
672
+ short_pos = self.short_positions[data_key]
673
+ net_pos = self.positions[data_key]
674
+ net_size = long_pos.size - short_pos.size
675
+ if net_size > 0:
676
+ net_price = long_pos.price
677
+ elif net_size < 0:
678
+ net_price = short_pos.price
679
+ else:
680
+ net_price = 0.0
681
+ net_pos.fix(net_size, net_price)
682
+ if long_pos.datetime is not None and short_pos.datetime is not None:
683
+ net_pos.datetime = max(long_pos.datetime, short_pos.datetime)
684
+ else:
685
+ net_pos.datetime = long_pos.datetime or short_pos.datetime
686
+ net_pos.adjbase = long_pos.adjbase if long_pos.size else short_pos.adjbase
687
+ return net_pos
688
+
689
+ def _iter_dual_side_positions(self, datas=None):
690
+ if datas is not None:
691
+ iterable = datas
692
+ else:
693
+ iterable = set(self.long_positions) | set(self.short_positions) | set(self.positions)
694
+ for data in iterable:
695
+ data_key = self._position_storage_key(data)
696
+ for position_side, store in (
697
+ (POSITION_SIDE_LONG, self.long_positions),
698
+ (POSITION_SIDE_SHORT, self.short_positions),
699
+ ):
700
+ position = store[data_key]
701
+ if position.size:
702
+ yield data_key, position_side, position
703
+
704
+ def _preview_position_key(self, order):
705
+ if not self._is_dual_side_mode():
706
+ return self._position_storage_key(order.data)
707
+ return (
708
+ self._position_storage_key(order.data),
709
+ normalize_position_side(getattr(order.info, "position_side", None)),
710
+ )
711
+
712
+ def _clone_position_for_order(self, order):
713
+ if not self._is_dual_side_mode():
714
+ return self.positions[self._position_storage_key(order.data)].clone()
715
+ position_side = normalize_position_side(getattr(order.info, "position_side", None))
716
+ return self._make_signed_position(
717
+ position_side,
718
+ self._get_leg_position(order.data, position_side),
719
+ )
720
+
721
+ def _credit_key(self, data, position_side=None):
722
+ if not self._is_dual_side_mode():
723
+ return self._position_storage_key(data)
724
+ return (self._position_storage_key(data), normalize_position_side(position_side))
725
+
726
+ def _validate_close_quantity(self, order, position):
727
+ if not self._is_dual_side_mode():
728
+ return
729
+ if getattr(order.info, "offset", None) not in {"close", "close_today", "close_yesterday"}:
730
+ return
731
+ if (
732
+ abs(float(order.executed.remsize or order.size or 0.0))
733
+ > abs(float(position.size or 0.0)) + 1e-12
734
+ ):
735
+ raise ValueError(
736
+ "Close order size exceeds the available leg position in dual_side mode"
737
+ )
738
+
739
+ def get_notification(self):
740
+ """Get the next notification from the notification queue.
741
+
742
+ Returns:
743
+ Order notification if available, None otherwise
744
+ """
745
+ try:
746
+ return self.notifs.popleft()
747
+ except IndexError:
748
+ # An empty queue is the normal per-bar polling result. Logging it
749
+ # would turn DEBUG split logs into an O(bar) write path.
750
+ return None
751
+
752
+ # Set fund mode
753
+ def set_fundmode(self, fundmode, fundstartval=None):
754
+ """Set the actual fundmode (True or False)
755
+
756
+ If the argument fundstartval is not ``None``, it will use
757
+ """
758
+ self.set_param("fundmode", fundmode)
759
+ if fundstartval is not None:
760
+ self.set_fundstartval(fundstartval)
761
+
762
+ def get_fundmode(self):
763
+ """Get the current fund mode status.
764
+
765
+ Returns:
766
+ bool: True if fund mode is enabled, False otherwise
767
+ """
768
+ return self.get_param("fundmode")
769
+
770
+ def set_fundstartval(self, fundstartval):
771
+ """Set the starting value for fund-like performance tracking.
772
+
773
+ Args:
774
+ fundstartval: The starting value for the fund
775
+ """
776
+ self.set_param("fundstartval", fundstartval)
777
+
778
+ def set_int2pnl(self, int2pnl):
779
+ """Configure assignment of interest to profit and loss.
780
+
781
+ Args:
782
+ int2pnl: If True, interest is assigned to PnL when positions close
783
+ """
784
+ self.set_param("int2pnl", int2pnl)
785
+
786
+ def set_coc(self, coc):
787
+ """Configure Cheat-On-Close behavior.
788
+
789
+ When enabled, market orders can execute at the closing price of the
790
+ bar in which they were issued.
791
+
792
+ Args:
793
+ coc: If True, enable cheat-on-close
794
+ """
795
+ self.set_param("coc", coc)
796
+
797
+ def set_coo(self, coo):
798
+ """Configure Cheat-On-Open behavior.
799
+
800
+ When enabled, market orders can execute at the opening price.
801
+
802
+ Args:
803
+ coo: If True, enable cheat-on-open
804
+ """
805
+ self.set_param("coo", coo)
806
+
807
+ def set_shortcash(self, shortcash):
808
+ """Configure short cash behavior for stock-like assets.
809
+
810
+ Args:
811
+ shortcash: If True, increase cash when shorting stock-like assets
812
+ """
813
+ self.set_param("shortcash", shortcash)
814
+
815
+ def set_slippage_perc(
816
+ self, perc, slip_open=True, slip_limit=True, slip_match=True, slip_out=False
817
+ ):
818
+ """Configure percentage-based slippage.
819
+
820
+ Args:
821
+ perc: Slippage percentage (e.g., 0.01 for 1%)
822
+ slip_open: Apply slippage to opening prices
823
+ slip_limit: Allow limit order matching with slippage capping
824
+ slip_match: Cap slippage at high/low prices
825
+ slip_out: Provide slippage even outside high-low range
826
+ """
827
+ self.set_param("slip_perc", perc)
828
+ self.set_param("slip_fixed", 0.0)
829
+ self.set_param("slip_open", slip_open)
830
+ self.set_param("slip_limit", slip_limit)
831
+ self.set_param("slip_match", slip_match)
832
+ self.set_param("slip_out", slip_out)
833
+
834
+ def set_slippage_fixed(
835
+ self, fixed, slip_open=True, slip_limit=True, slip_match=True, slip_out=False
836
+ ):
837
+ """Configure fixed-point slippage.
838
+
839
+ Args:
840
+ fixed: Fixed slippage amount in price units
841
+ slip_open: Apply slippage to opening prices
842
+ slip_limit: Allow limit order matching with slippage capping
843
+ slip_match: Cap slippage at high/low prices
844
+ slip_out: Provide slippage even outside high-low range
845
+ """
846
+ self.set_param("slip_perc", 0.0)
847
+ self.set_param("slip_fixed", fixed)
848
+ self.set_param("slip_open", slip_open)
849
+ self.set_param("slip_limit", slip_limit)
850
+ self.set_param("slip_match", slip_match)
851
+ self.set_param("slip_out", slip_out)
852
+
853
+ def set_filler(self, filler):
854
+ """Set a volume filler callable for order execution.
855
+
856
+ Args:
857
+ filler: Callable with signature (order, price, ago) -> executed_size
858
+ """
859
+ self.set_param("filler", filler)
860
+
861
+ def set_checksubmit(self, checksubmit):
862
+ """Set whether to check margin/cash before accepting orders.
863
+
864
+ Args:
865
+ checksubmit: If True, validate margin/cash before order submission
866
+ """
867
+ self.set_param("checksubmit", checksubmit)
868
+
869
+ def set_eosbar(self, eosbar):
870
+ """Set end-of-session bar behavior.
871
+
872
+ Args:
873
+ eosbar: If True, consider bar with same time as end of session as EOS
874
+ """
875
+ self.set_param("eosbar", eosbar)
876
+
877
+ seteosbar = set_eosbar
878
+
879
+ def get_cash(self):
880
+ """Get the current available cash.
881
+
882
+ Returns:
883
+ float: Current cash amount. Returns parameter value if not yet
884
+ initialized, otherwise returns current cash status.
885
+ """
886
+ if hasattr(self, "_cash") and self._cash is not None:
887
+ return self._cash
888
+ return self.get_param("cash")
889
+
890
+ getcash = get_cash
891
+
892
+ __getattribute__ = object.__getattribute__
893
+
894
+ def set_cash(self, cash):
895
+ """Set the broker cash amount.
896
+
897
+ Args:
898
+ cash: Cash amount to set
899
+ """
900
+ self.startingcash = self._cash = cash
901
+ self.set_param("cash", cash)
902
+ self._value = cash
903
+
904
+ setcash = set_cash
905
+
906
+ def add_cash(self, cash):
907
+ """Add or remove cash from the system.
908
+
909
+ Args:
910
+ cash: Cash amount to add (use negative value to remove)
911
+ """
912
+ self._cash_addition.append(cash)
913
+
914
+ def get_fundshares(self):
915
+ """Get the current number of fund shares.
916
+
917
+ Returns:
918
+ float: Current number of shares in fund-like mode
919
+ """
920
+ return self._fundshares
921
+
922
+ fundshares = property(get_fundshares)
923
+
924
+ def get_fundvalue(self):
925
+ """Get the fund share value.
926
+
927
+ Returns:
928
+ float: Current fund-like share value
929
+ """
930
+ return self._fundval
931
+
932
+ fundvalue = property(get_fundvalue)
933
+
934
+ def cancel(self, order, bracket=False):
935
+ """Cancel an order.
936
+
937
+ Args:
938
+ order: The order to cancel
939
+ bracket: If True, cancel as part of bracket order
940
+
941
+ Returns:
942
+ bool: True if order was cancelled, False if not found
943
+ """
944
+ if order is None or not order.alive():
945
+ return False
946
+
947
+ if order.status not in (Order.Submitted, Order.Accepted, Order.Partial):
948
+ return False
949
+
950
+ removed = False
951
+ for queue in (self.pending, self.submitted):
952
+ try:
953
+ queue.remove(order)
954
+ except ValueError:
955
+ # An order belongs to exactly one queue. A miss in the other
956
+ # queue is expected cancellation control flow, not a DEBUG
957
+ # diagnostic.
958
+ continue
959
+ removed = True
960
+ break
961
+
962
+ if not removed:
963
+ return False
964
+
965
+ order.cancel()
966
+ self._log_order_canceled(order)
967
+ self.notify(order)
968
+ self._ococheck(order)
969
+ if not bracket:
970
+ self._bracketize(order, cancel=True)
971
+ return True
972
+
973
+ # Get value, if data is not specified, get the value of the entire account
974
+ def get_value(self, datas=None, mkt=False, lever=False):
975
+ """Returns the portfolio value of the given datas (if datas is ``None``, then
976
+ the total portfolio value will be returned (alias: ``getvalue``)
977
+ """
978
+ if datas is None:
979
+ if mkt:
980
+ return self._valuemkt if not lever else self._valuemktlever
981
+
982
+ return self._value if not lever else self._valuelever
983
+
984
+ return self._get_value(datas=datas, lever=lever)
985
+
986
+ getvalue = get_value
987
+
988
+ def _get_value_dual_side(self, datas, lever, shortcash, getcommissioninfo):
989
+ """Accumulate portfolio value across long+short legs (dual_side mode).
990
+
991
+ Returns a 4-tuple ``(direct, pos_value, unrealized, pos_value_unlever)``
992
+ where ``direct`` is non-None only for a single-data raw-value request
993
+ (caller returns it immediately); otherwise it is None and the three
994
+ accumulators are returned. Extracted verbatim from _get_value.
995
+ """
996
+ pos_value = 0.0
997
+ pos_value_unlever = 0.0
998
+ unrealized = 0.0
999
+ data_iterable = list(datas) if datas is not None else None
1000
+ single_data_request = data_iterable is not None and len(data_iterable) == 1
1001
+ for data in data_iterable or (
1002
+ set(self.long_positions) | set(self.short_positions) | set(self.positions)
1003
+ ):
1004
+ long_position = self.long_positions[self._position_storage_key(data)]
1005
+ short_position = self.short_positions[self._position_storage_key(data)]
1006
+ if not long_position.size and not short_position.size:
1007
+ if single_data_request:
1008
+ return 0.0, pos_value, unrealized, pos_value_unlever
1009
+ continue
1010
+
1011
+ comminfo = getcommissioninfo(data)
1012
+ close0 = data.close[0]
1013
+ leverage = comminfo.get_leverage()
1014
+ data_raw_value = 0.0
1015
+ data_value = 0.0
1016
+ data_value_unlever = 0.0
1017
+ data_unrealized = 0.0
1018
+
1019
+ for _position_side, leg_position in (
1020
+ (POSITION_SIDE_LONG, long_position),
1021
+ (POSITION_SIDE_SHORT, short_position),
1022
+ ):
1023
+ if not leg_position.size:
1024
+ continue
1025
+
1026
+ signed_position = self._make_signed_position(_position_side, leg_position)
1027
+ if not shortcash:
1028
+ leg_raw_value = comminfo.getvalue(signed_position, close0)
1029
+ leg_value = abs(leg_raw_value)
1030
+ else:
1031
+ leg_raw_value = comminfo.getvaluesize(signed_position.size, close0)
1032
+ leg_value = leg_raw_value
1033
+
1034
+ leg_unrealized = comminfo.profitandloss(
1035
+ signed_position.size,
1036
+ signed_position.price,
1037
+ close0,
1038
+ )
1039
+ data_raw_value += leg_raw_value
1040
+ data_value += leg_value
1041
+ data_unrealized += leg_unrealized
1042
+
1043
+ if leg_value > 0:
1044
+ leg_value -= leg_unrealized
1045
+ data_value_unlever += leg_value / leverage
1046
+ data_value_unlever += leg_unrealized
1047
+ else:
1048
+ data_value_unlever += leg_value
1049
+
1050
+ if single_data_request:
1051
+ if lever and data_raw_value > 0:
1052
+ data_raw_value -= data_unrealized
1053
+ return (
1054
+ (data_raw_value / leverage) + data_unrealized,
1055
+ pos_value,
1056
+ unrealized,
1057
+ pos_value_unlever,
1058
+ )
1059
+ return data_raw_value, pos_value, unrealized, pos_value_unlever
1060
+
1061
+ pos_value += data_value
1062
+ unrealized += data_unrealized
1063
+ pos_value_unlever += data_value_unlever
1064
+ return None, pos_value, unrealized, pos_value_unlever
1065
+
1066
+ def _get_value_net(self, datas, lever, shortcash, positions, getcommissioninfo):
1067
+ """Accumulate portfolio value across net positions (net mode).
1068
+
1069
+ Returns a 4-tuple ``(direct, pos_value, unrealized, pos_value_unlever)``
1070
+ with the same single-data early-return convention as
1071
+ _get_value_dual_side. Extracted verbatim from _get_value.
1072
+ """
1073
+ pos_value = 0.0
1074
+ pos_value_unlever = 0.0
1075
+ unrealized = 0.0
1076
+ # If datas is None, loop through self.positions; if datas is not None, loop through datas
1077
+ for data in datas or positions:
1078
+ # Get commission related info
1079
+ comminfo = getcommissioninfo(data)
1080
+ # Get data position
1081
+ position = positions[data]
1082
+ if not position:
1083
+ if datas and len(datas) == 1:
1084
+ return 0.0, pos_value, unrealized, pos_value_unlever
1085
+ continue
1086
+ close0 = data.close[0]
1087
+ # use valuesize: returns raw value, rather than negative adj val
1088
+ # If shortcash is False, use comminfo.getvalue to get data value
1089
+ # If shortcash is True, use comminfo.getvaluesize to get data value
1090
+ if not shortcash:
1091
+ dvalue = comminfo.getvalue(position, close0)
1092
+ else:
1093
+ dvalue = comminfo.getvaluesize(position.size, close0)
1094
+ # Get unrealized profit of data
1095
+ dunrealized = comminfo.profitandloss(position.size, position.price, close0)
1096
+ leverage = comminfo.get_leverage()
1097
+ # If datas is not None and datas is a list containing one data
1098
+ if datas and len(datas) == 1:
1099
+ # If lever is True and dvalue is greater than 0, calculate the initial dvalue value, then divide by leverage and add unrealized profit to get data value
1100
+ if lever and dvalue > 0:
1101
+ dvalue -= dunrealized
1102
+ return (
1103
+ (dvalue / leverage) + dunrealized,
1104
+ pos_value,
1105
+ unrealized,
1106
+ pos_value_unlever,
1107
+ )
1108
+ # If lever is False or dvalue<0 due to shortcash, return dvalue
1109
+ return dvalue, pos_value, unrealized, pos_value_unlever
1110
+ # If shortcash is False
1111
+ if not shortcash:
1112
+ dvalue = abs(dvalue) # short selling adds value in this case
1113
+ # Position value equals position value plus data value
1114
+ pos_value += dvalue
1115
+ # Unrealized profit equals unrealized profit plus data unrealized profit
1116
+ unrealized += dunrealized
1117
+ # If dvalue is greater than 0, calculate unleveraged position value
1118
+ if dvalue > 0: # long position - unlever
1119
+ dvalue -= dunrealized
1120
+ pos_value_unlever += dvalue / leverage
1121
+ pos_value_unlever += dunrealized
1122
+ else:
1123
+ pos_value_unlever += dvalue
1124
+ return None, pos_value, unrealized, pos_value_unlever
1125
+
1126
+ def _get_value(self, datas=None, lever=False):
1127
+ """Calculate portfolio value for given data feeds.
1128
+
1129
+ Args:
1130
+ datas: Data feeds to calculate value for (None for all)
1131
+ lever: If True, return leveraged value
1132
+
1133
+ Returns:
1134
+ float: Portfolio value
1135
+ """
1136
+ shortcash = self._shortcash
1137
+ positions = self.positions
1138
+ getcommissioninfo = self.getcommissioninfo
1139
+ dual_side_mode = self._dual_side_mode
1140
+
1141
+ # If cash is added, add the cash to self._cash
1142
+ cash_addition = self._cash_addition
1143
+ while cash_addition:
1144
+ c = cash_addition.popleft()
1145
+ self._fundshares += c / self._fundval if self._fundval else 0.0
1146
+ self._cash += c
1147
+
1148
+ if datas is None and not self._fundhist and not dual_side_mode:
1149
+ has_position = False
1150
+ for pos in positions.values():
1151
+ if pos:
1152
+ has_position = True
1153
+ break
1154
+ if not has_position:
1155
+ self._value = self._cash
1156
+ self._fundval = (
1157
+ self._value / self._fundshares
1158
+ if self._fundshares
1159
+ else self.get_param("fundstartval")
1160
+ )
1161
+ self._valuemkt = 0.0
1162
+ self._valuelever = self._cash
1163
+ self._valuemktlever = 0.0
1164
+ self._leverage = 0.0
1165
+ self._unrealized = 0.0
1166
+ return self._value if not lever else self._valuelever
1167
+
1168
+ if dual_side_mode:
1169
+ direct, pos_value, unrealized, pos_value_unlever = self._get_value_dual_side(
1170
+ datas, lever, shortcash, getcommissioninfo
1171
+ )
1172
+ else:
1173
+ direct, pos_value, unrealized, pos_value_unlever = self._get_value_net(
1174
+ datas, lever, shortcash, positions, getcommissioninfo
1175
+ )
1176
+ # Early-return for single-data requests (raw per-data value)
1177
+ if direct is not None:
1178
+ return direct
1179
+ # If not in fundhist mode, calculate _value and fundval
1180
+ if not self._fundhist:
1181
+ # _cash is a float here (init() ran before any backtest step);
1182
+ # None is only the pre-init sentinel used by get_cash().
1183
+ self._value = self._cash + pos_value_unlever
1184
+ self._fundval = (
1185
+ self._value / self._fundshares
1186
+ if self._fundshares
1187
+ else self.get_param("fundstartval")
1188
+ ) # update fundvalue
1189
+ # If in fundhist mode
1190
+ else:
1191
+ # Try to fetch a value
1192
+ # Call function _process_fund_history() to get fval and fvalue
1193
+ fval, fvalue = self._process_fund_history()
1194
+ # _value equals fvalue
1195
+ self._value = fvalue
1196
+ # cash equals fvalue minus unleveraged position
1197
+ self._cash = fvalue - pos_value_unlever
1198
+ # _fundval = fval
1199
+ self._fundval = fval
1200
+ # _fund shares
1201
+ self._fundshares = fvalue / fval if fval else 0.0
1202
+ # Leverage multiplier
1203
+ lev = pos_value / (pos_value_unlever or 1.0)
1204
+
1205
+ # update the calculated values above to the historical values
1206
+ # Unleveraged position value
1207
+ pos_value_unlever = fvalue
1208
+ # Leveraged position value
1209
+ pos_value = fvalue * lev
1210
+ # Unleveraged position value
1211
+ self._valuemkt = pos_value_unlever
1212
+ # Leveraged account value
1213
+ self._valuelever = self._cash + pos_value
1214
+ # Leveraged position value
1215
+ self._valuemktlever = pos_value
1216
+ # Leverage ratio
1217
+ self._leverage = pos_value / (pos_value_unlever or 1.0)
1218
+ # Unrealized profit
1219
+ self._unrealized = unrealized
1220
+
1221
+ return self._value if not lever else self._valuelever
1222
+
1223
+ def get_leverage(self):
1224
+ """Get the current account leverage ratio.
1225
+
1226
+ Returns:
1227
+ float: Current leverage ratio
1228
+ """
1229
+ return self._leverage
1230
+
1231
+ # Get pending orders
1232
+ def get_orders_open(self, safe=False):
1233
+ """Returns an iterable with the orders which are still open (either not
1234
+ executed or partially executed)
1235
+
1236
+ The orders returned must not be touched.
1237
+
1238
+ If order manipulation is needed, set the parameter ``safe`` to True
1239
+ """
1240
+ if safe:
1241
+ os = [x.clone() for x in self.pending]
1242
+ else:
1243
+ os = list(self.pending)
1244
+
1245
+ return os
1246
+
1247
+ def getposition(self, data, side=None):
1248
+ """Get the current position status for a data feed.
1249
+
1250
+ Args:
1251
+ data: Data feed to get position for
1252
+ side: Optional leg selector in dual_side mode
1253
+
1254
+ Returns:
1255
+ Position: Current position instance for the data feed
1256
+ """
1257
+ if side is not None:
1258
+ if not self._is_dual_side_mode():
1259
+ raise ValueError("side-specific getposition() is only available in dual_side mode")
1260
+ return self._get_leg_position(data, side)
1261
+ if self._is_dual_side_mode():
1262
+ return self._sync_net_position(data)
1263
+ return self.positions[data]
1264
+
1265
+ def get_cached_report_state(self):
1266
+ """Return the broker's already-computed state without recalculation."""
1267
+ positions = dict(self.positions)
1268
+ position_legs = {}
1269
+ if self._is_dual_side_mode():
1270
+ for data in set(self.long_positions) | set(self.short_positions):
1271
+ positions[data] = self._sync_net_position(data)
1272
+ position_legs[data] = {
1273
+ "long": self.long_positions.get(data),
1274
+ "short": self.short_positions.get(data),
1275
+ }
1276
+ return {
1277
+ "cash": self._cash,
1278
+ "value": self._value,
1279
+ "positions": positions,
1280
+ "position_legs": position_legs,
1281
+ }
1282
+
1283
+ def orderstatus(self, order):
1284
+ """Get the status of an order.
1285
+
1286
+ Args:
1287
+ order: Order object or order reference
1288
+
1289
+ Returns:
1290
+ Order.Status: The current status of the order
1291
+ """
1292
+ try:
1293
+ o = self.orders[self.orders.index(order)]
1294
+ except ValueError:
1295
+ o = order
1296
+
1297
+ return o.status
1298
+
1299
+ def _take_children(self, order):
1300
+ """Handle parent-child relationship for bracket orders.
1301
+
1302
+ Args:
1303
+ order: Order to process for parent-child relationship
1304
+
1305
+ Returns:
1306
+ Parent order reference if successful, None if order rejected
1307
+ """
1308
+ # Order ID
1309
+ oref = order.ref
1310
+ # Get parent order ID of order, if not found then it's itself
1311
+ pref = getattr(order.parent, "ref", oref) # parent ref or self
1312
+ # If child order ID and parent order ID are not equal
1313
+ if oref != pref:
1314
+ # If parent order ID is not in _pchildren, the order will be rejected and return None
1315
+ if pref not in self._pchildren:
1316
+ order.reject() # parent not there - may have been rejected
1317
+ self._log_order_rejected(order, "parent order missing")
1318
+ self.notify(order) # reject child, notify
1319
+ return None
1320
+ # If they are equal, return parent order ID
1321
+ return pref
1322
+
1323
+ def submit(self, order, check=True):
1324
+ """Submit an order to the broker.
1325
+
1326
+ Args:
1327
+ order: Order object to submit
1328
+ check: If True, validate order before submission
1329
+
1330
+ Returns:
1331
+ Order: The submitted order or parent order if part of bracket
1332
+ """
1333
+ self._freeze_position_mode("first order submission")
1334
+ # Get parent order ID of order or its own ID, if this ID is None, return order itself
1335
+ pref = self._take_children(order)
1336
+ if pref is None: # order has not been taken
1337
+ return order
1338
+ # pc is a deque that saves parent and children orders
1339
+ pc = self._pchildren[pref]
1340
+ pc.append(order) # store in parent/children queue
1341
+ # If order is transmit, call transmit function for orders in pc and return the last order
1342
+ if order.transmit: # if single order, sent and queue cleared
1343
+ # if parent-child, the parent will be sent, the other kept
1344
+ rets = [self.transmit(x, check=check) for x in pc]
1345
+ return rets[-1] # last one is the one triggering transmission
1346
+
1347
+ return order
1348
+
1349
+ def transmit(self, order, check=True):
1350
+ """Transmit an order for execution.
1351
+
1352
+ Args:
1353
+ order: Order to transmit
1354
+ check: If True, check margin/cash before accepting
1355
+
1356
+ Returns:
1357
+ Order: The transmitted order
1358
+ """
1359
+ self._freeze_position_mode("first order submission")
1360
+ # If check is True and checksubmit is True
1361
+ if check and self._checksubmit:
1362
+ # Orderssubmit
1363
+ order.submit()
1364
+ # Append order to submitted
1365
+ self.submitted.append(order)
1366
+ # Append order to orders
1367
+ self.orders.append(order)
1368
+ # Notify order
1369
+ self.notify(order)
1370
+ # If either check or checksubmit is False, append order to submit_accept
1371
+ else:
1372
+ self.submit_accept(order)
1373
+ # ``submit`` can hold an untransmitted bracket child or reject an
1374
+ # invalid child. Emit INFO only after this method has moved the order
1375
+ # through the real Submitted transition.
1376
+ self._log_order_submitted(order)
1377
+ # Return order
1378
+ return order
1379
+
1380
+ def check_submitted(self):
1381
+ """Check and validate submitted orders against available cash and margin.
1382
+
1383
+ Processes all orders in the submitted queue and validates them
1384
+ against current cash and margin requirements.
1385
+ """
1386
+ # Currently available cash
1387
+ cash = self._cash
1388
+ # Position
1389
+ positions: dict = {}
1390
+ # When submitted is not empty
1391
+ while self.submitted:
1392
+ # Remove leftmost order and get it
1393
+ order = self.submitted.popleft()
1394
+ # If the result of calling _take_children(order) is None, this order will be rejected, continue to next order
1395
+ if self._take_children(order) is None: # children not taken
1396
+ continue
1397
+ # Get position
1398
+ preview_key = self._preview_position_key(order)
1399
+ position = positions.setdefault(preview_key, self._clone_position_for_order(order))
1400
+ try:
1401
+ self._validate_close_quantity(order, position)
1402
+ except ValueError:
1403
+ order.reject()
1404
+ self._log_order_rejected(order, "close quantity validation failed")
1405
+ self.notify(order)
1406
+ self._ococheck(order)
1407
+ self._bracketize(order, cancel=True)
1408
+ continue
1409
+ # pseudo-execute the order to get the remaining cash after exec
1410
+ # Cash obtained after assuming order execution
1411
+ trial_position = position.clone()
1412
+ trial_cash = self._execute(order, cash=cash, position=trial_position)
1413
+ # If remaining cash is greater than 0, call submit_accept to accept order
1414
+ if trial_cash >= 0.0:
1415
+ cash = trial_cash
1416
+ positions[preview_key] = trial_position
1417
+ self.submit_accept(order)
1418
+ continue
1419
+ # If cash is less than 0, insufficient margin, notify order status, call _ococheck and _bracketize
1420
+ order.margin()
1421
+ self._log_order_margin(order, "insufficient cash or margin during submission check")
1422
+ self.notify(order)
1423
+ self._ococheck(order)
1424
+ self._bracketize(order, cancel=True)
1425
+
1426
+ def submit_accept(self, order):
1427
+ """Accept and activate a submitted order.
1428
+
1429
+ Args:
1430
+ order: Order to accept
1431
+ """
1432
+ order.pannotated = None
1433
+ # Order submit
1434
+ order.submit()
1435
+ # Order accept
1436
+ order.accept()
1437
+ # Add order to pending orders
1438
+ self.pending.append(order)
1439
+ # Notify order status
1440
+ self.notify(order)
1441
+
1442
+ def _bracketize(self, order, cancel=False):
1443
+ """Handle bracket order activation or cancellation.
1444
+
1445
+ Args:
1446
+ order: Order in a bracket order group
1447
+ cancel: If True, cancel remaining orders in bracket
1448
+ """
1449
+ # Ordersid
1450
+ oref = order.ref
1451
+ # Parent order ID or own ID
1452
+ pref = getattr(order.parent, "ref", oref)
1453
+ # If two IDs are equal, parent is True
1454
+ parent = oref == pref
1455
+ # Get order deque
1456
+ pc = self._pchildren[pref] # defdict - guaranteed
1457
+ # If cancel is True or parent is not True,
1458
+ if cancel or not parent: # cancel left or child exec -> cancel other
1459
+ # If pc has orders, will keep running, cancel orders
1460
+ while pc:
1461
+ self.cancel(pc.popleft(), bracket=True) # idempotent
1462
+ # Delete this key, value
1463
+ del self._pchildren[pref] # defdict guaranteed
1464
+ # If neither of the above conditions is met, i.e., cancel is False and parent is True
1465
+ else: # not cancel -> parent exec'd
1466
+ # Clear parent order, then change child order status to inactive
1467
+ pc.popleft() # remove parent
1468
+ for o in pc: # activate children
1469
+ self._toactivate.append(o)
1470
+
1471
+ def _ococheck(self, order):
1472
+ """Check and handle OCO (One-Cancels-Other) order relationships.
1473
+
1474
+ Args:
1475
+ order: Order to check for OCO relationships
1476
+ """
1477
+ # ocoref = self._ocos[order.ref] or order.ref # a parent or self
1478
+ parentref = self._ocos[order.ref]
1479
+ ocoref = self._ocos.get(parentref, None)
1480
+ ocol = self._ocol.pop(ocoref, None)
1481
+ if ocol:
1482
+ for queue in (self.pending, self.submitted):
1483
+ for i in range(len(queue) - 1, -1, -1):
1484
+ o = queue[i]
1485
+ if o is not None and o.ref in ocol:
1486
+ del queue[i]
1487
+ o.cancel()
1488
+ self._log_order_canceled(o)
1489
+ self.notify(o)
1490
+
1491
+ def _ocoize(self, order, oco):
1492
+ """Set up OCO (One-Cancels-Other) relationship for an order.
1493
+
1494
+ Args:
1495
+ order: Order to set up OCO relationship for
1496
+ oco: OCO order reference (None for new OCO group)
1497
+ """
1498
+ oref = order.ref
1499
+ if oco is None:
1500
+ self._ocos[oref] = oref # current order is parent
1501
+ self._ocol[oref].append(oref) # create ocogroup
1502
+ else:
1503
+ ocoref = self._ocos[oco.ref] # ref to group leader
1504
+ self._ocos[oref] = ocoref # ref to group leader
1505
+ self._ocol[ocoref].append(oref) # add to group
1506
+
1507
+ def add_order_history(self, orders, notify=True):
1508
+ """Add historical orders to the broker.
1509
+
1510
+ Args:
1511
+ orders: Iterable of historical orders to add
1512
+ notify: If True, send notifications for these orders
1513
+ """
1514
+ oiter = iter(orders)
1515
+ o = next(oiter, None)
1516
+ self._userhist.append([o, oiter, notify])
1517
+
1518
+ def set_fund_history(self, fund):
1519
+ """Set fund history for fund-like performance tracking.
1520
+
1521
+ Args:
1522
+ fund: Iterable of [datetime, share_value, net_asset_value] items
1523
+ """
1524
+ # iterable with the following pro item
1525
+ # [datetime, share_value, net asset value]
1526
+ fiter = iter(fund)
1527
+ f = list(next(fiter)) # must not be empty
1528
+ self._fundhist = [f, fiter]
1529
+ # self._fhistlast = f[1:]
1530
+
1531
+ self.set_cash(float(f[2]))
1532
+
1533
+ def buy(
1534
+ self,
1535
+ owner,
1536
+ data,
1537
+ size,
1538
+ price=None,
1539
+ plimit=None,
1540
+ exectype=None,
1541
+ valid=None,
1542
+ tradeid=0,
1543
+ oco=None,
1544
+ trailamount=None,
1545
+ trailpercent=None,
1546
+ parent=None,
1547
+ transmit=True,
1548
+ histnotify=False,
1549
+ _checksubmit=True,
1550
+ **kwargs,
1551
+ ):
1552
+ """Create and submit a buy order.
1553
+
1554
+ Args:
1555
+ owner: Strategy or object creating the order
1556
+ data: Data feed for the order
1557
+ size: Order size (positive for buy)
1558
+ price: Order price (for limit/stop orders)
1559
+ plimit: Limit price for stop-limit orders
1560
+ exectype: Order execution type
1561
+ valid: Order validity
1562
+ tradeid: Trade identifier
1563
+ oco: OCO (One-Cancels-Other) order reference
1564
+ trailamount: Trailing stop amount
1565
+ trailpercent: Trailing stop percentage
1566
+ parent: Parent order (for bracket orders)
1567
+ transmit: If True, transmit order immediately
1568
+ histnotify: If True, notify for historical orders
1569
+ _checksubmit: If True, validate order before submission
1570
+ **kwargs: Additional order parameters
1571
+
1572
+ Returns:
1573
+ Order: The submitted buy order
1574
+ """
1575
+ position_side, offset, order_kwargs = self._normalize_order_meta(True, kwargs)
1576
+ order = BuyOrder(
1577
+ owner=owner,
1578
+ data=data,
1579
+ size=size,
1580
+ price=price,
1581
+ pricelimit=plimit,
1582
+ exectype=exectype,
1583
+ valid=valid,
1584
+ tradeid=tradeid,
1585
+ trailamount=trailamount,
1586
+ trailpercent=trailpercent,
1587
+ parent=parent,
1588
+ transmit=transmit,
1589
+ histnotify=histnotify,
1590
+ )
1591
+
1592
+ self._attach_position_meta(
1593
+ order, position_side=position_side, offset=offset, **order_kwargs
1594
+ )
1595
+ self._ocoize(order, oco)
1596
+
1597
+ return self.submit(order, check=_checksubmit)
1598
+
1599
+ def sell(
1600
+ self,
1601
+ owner,
1602
+ data,
1603
+ size,
1604
+ price=None,
1605
+ plimit=None,
1606
+ exectype=None,
1607
+ valid=None,
1608
+ tradeid=0,
1609
+ oco=None,
1610
+ trailamount=None,
1611
+ trailpercent=None,
1612
+ parent=None,
1613
+ transmit=True,
1614
+ histnotify=False,
1615
+ _checksubmit=True,
1616
+ **kwargs,
1617
+ ):
1618
+ """Create and submit a sell order.
1619
+
1620
+ Args:
1621
+ owner: Strategy or object creating the order
1622
+ data: Data feed for the order
1623
+ size: Order size (positive for sell)
1624
+ price: Order price (for limit/stop orders)
1625
+ plimit: Limit price for stop-limit orders
1626
+ exectype: Order execution type
1627
+ valid: Order validity
1628
+ tradeid: Trade identifier
1629
+ oco: OCO (One-Cancels-Other) order reference
1630
+ trailamount: Trailing stop amount
1631
+ trailpercent: Trailing stop percentage
1632
+ parent: Parent order (for bracket orders)
1633
+ transmit: If True, transmit order immediately
1634
+ histnotify: If True, notify for historical orders
1635
+ _checksubmit: If True, validate order before submission
1636
+ **kwargs: Additional order parameters
1637
+
1638
+ Returns:
1639
+ Order: The submitted sell order
1640
+ """
1641
+ position_side, offset, order_kwargs = self._normalize_order_meta(False, kwargs)
1642
+ order = SellOrder(
1643
+ owner=owner,
1644
+ data=data,
1645
+ size=size,
1646
+ price=price,
1647
+ pricelimit=plimit,
1648
+ exectype=exectype,
1649
+ valid=valid,
1650
+ tradeid=tradeid,
1651
+ trailamount=trailamount,
1652
+ trailpercent=trailpercent,
1653
+ parent=parent,
1654
+ transmit=transmit,
1655
+ histnotify=histnotify,
1656
+ )
1657
+
1658
+ self._attach_position_meta(
1659
+ order, position_side=position_side, offset=offset, **order_kwargs
1660
+ )
1661
+ self._ocoize(order, oco)
1662
+
1663
+ return self.submit(order, check=_checksubmit)
1664
+
1665
+ # Execute order
1666
+ def _execute(self, order, ago=None, price=None, cash=None, position=None, dtcoc=None):
1667
+ if self._is_dual_side_mode():
1668
+ return self._execute_dual_side(
1669
+ order,
1670
+ ago=ago,
1671
+ price=price,
1672
+ cash=cash,
1673
+ position=position,
1674
+ dtcoc=dtcoc,
1675
+ )
1676
+ # ago = None is used a flag for pseudo execution
1677
+ # If ago is not None and price is None, do nothing and return
1678
+ if ago is not None and price is None:
1679
+ return None # no psuedo exec no price - no execution
1680
+
1681
+ # Get the order size to execute
1682
+ if self.get_param("filler") is None or ago is None:
1683
+ # Order gets full size or pseudo-execution
1684
+ size = order.executed.remsize
1685
+ else:
1686
+ # Execution depends on volume filler
1687
+ size = self.get_param("filler")(order, price, ago)
1688
+ if not order.isbuy():
1689
+ size = -size
1690
+
1691
+ # Get comminfo object for the data
1692
+ # Get commission info class
1693
+ comminfo = self.getcommissioninfo(order.data)
1694
+
1695
+ # Check if something has to be compensated
1696
+ # If data's _compensate is not None, get _compensate's commission info class, otherwise use data's
1697
+ if order.data._compensate is not None:
1698
+ data = order.data._compensate
1699
+ cinfocomp = self.getcommissioninfo(data) # for actual commission
1700
+ else:
1701
+ data = order.data
1702
+ cinfocomp = comminfo
1703
+
1704
+ # Adjust position with operation size
1705
+ # If ago is not None, get position, position average price, update position related info, and calculate pnl and cash
1706
+ if ago is not None:
1707
+ # Real execution with date
1708
+ position = self.positions[data]
1709
+ pprice_orig = position.price
1710
+
1711
+ psize, pprice, opened, closed = position.pseudoupdate(size, price)
1712
+
1713
+ # if part/all of a position has been closed, then there has been
1714
+ # a profitandloss ... record it
1715
+ pnl = comminfo.profitandloss(-closed, pprice_orig, price)
1716
+ cash = self._cash
1717
+ # If ago is None
1718
+ else:
1719
+ # pnl = 0
1720
+ pnl = 0
1721
+ # If cheat_on_open is False
1722
+ if not self.get_param("coo"):
1723
+ # Price
1724
+ price = pprice_orig = order.created.price
1725
+ # If cheat_on_open = True
1726
+ else:
1727
+ # When doing cheat on open, the price to be considered for a
1728
+ # market order is the opening price and not the default closing
1729
+ # price with which the order was created
1730
+ # If it's a market order, price equals the day's opening price, otherwise equals the created price
1731
+ if order.exectype == Order.Market:
1732
+ price = pprice_orig = order.data.open[0]
1733
+ else:
1734
+ price = pprice_orig = order.created.price
1735
+ # Update position size and price
1736
+ psize, pprice, opened, closed = position.update(size, price)
1737
+
1738
+ # "Closing" totally or partially is possible. Cash may be re-injected
1739
+ # If closed
1740
+ if closed:
1741
+ # Adjust to returned value for closed items & acquired opened items
1742
+ # If shortcash is True, closing value is calculated using comminfo.getvaluesize,
1743
+ # If shortcash is False, closing value is calculated using comminfo.getoperationcost
1744
+ if self._shortcash:
1745
+ closedvalue = comminfo.getvaluesize(-closed, pprice_orig)
1746
+ else:
1747
+ closedvalue = comminfo.getoperationcost(closed, pprice_orig)
1748
+
1749
+ # If closedvalue > 0, calculate closecash after adjusting for leverage
1750
+ closecash = closedvalue
1751
+ if closedvalue > 0: # long position closed
1752
+ closecash /= comminfo.get_leverage() # inc cash with lever
1753
+ # If stocklike, cash equals cash plus closecash plus pnl
1754
+ # If stocklike is False, cash equals cash + closecash
1755
+ cash += closecash + pnl * comminfo.stocklike
1756
+ # Calculate and subtract commission
1757
+ # Commission when closing position
1758
+ closedcomm = self._getcommission_role(
1759
+ comminfo,
1760
+ closed,
1761
+ price,
1762
+ self._close_commission_role(getattr(order.info, "offset", None)),
1763
+ )
1764
+ # Cash equals cash minus closing commission
1765
+ cash -= closedcomm
1766
+ # If ago is not None
1767
+ if ago is not None:
1768
+ # Cashadjust closed contracts: prev close vs exec price
1769
+ # The operation can inject or take cash out
1770
+ # Adjust cash and update
1771
+ cash += comminfo.cashadjust(-closed, position.adjbase, price)
1772
+
1773
+ # Update system cash
1774
+ self._cash = cash
1775
+ # If not closed
1776
+ else:
1777
+ closedvalue = closedcomm = 0.0
1778
+
1779
+ # If opened
1780
+ popened = opened
1781
+ if opened:
1782
+ # Calculate opening value
1783
+ if self._shortcash:
1784
+ openedvalue = comminfo.getvaluesize(opened, price)
1785
+ else:
1786
+ openedvalue = comminfo.getoperationcost(opened, price)
1787
+
1788
+ # Calculate cash used for opening
1789
+ opencash = openedvalue
1790
+ if openedvalue > 0: # long position being opened
1791
+ opencash /= comminfo.get_leverage() # dec cash with level
1792
+ # Subtract cash obtained after opening
1793
+ cash -= opencash # original behavior
1794
+ # Commission for opening
1795
+ openedcomm = self._getcommission_role(cinfocomp, opened, price, "open")
1796
+ # Cash obtained after subtracting opening commission
1797
+ cash -= openedcomm
1798
+ # If cash is less than 0, opening position is not possible
1799
+ if cash < 0.0:
1800
+ # execution is not possible - nullify
1801
+ opened = 0
1802
+ openedvalue = openedcomm = 0.0
1803
+
1804
+ # If ago is not None
1805
+ elif ago is not None: # real execution
1806
+ # If absolute position size is greater than absolute opening size
1807
+ if abs(psize) > abs(opened):
1808
+ # some futures were opened - adjust the cash of the
1809
+ # previously existing futures to the operation price and
1810
+ # use that as new adjustment base, because it already is
1811
+ # for the new futures At the end of the cycle the
1812
+ # adjustment to the close price will be done for all open
1813
+ # futures from a common base price with regard to the
1814
+ # close price
1815
+ # Size to adjust
1816
+ adjsize = psize - opened
1817
+ # Adjust cash
1818
+ cash += comminfo.cashadjust(adjsize, position.adjbase, price)
1819
+
1820
+ # record adjust price base for end of bar cash adjustment
1821
+ # Update position adjbase price
1822
+ position.adjbase = price
1823
+
1824
+ # update system cash - checking if opened is still != 0
1825
+ self._cash = cash
1826
+ # If opened is False
1827
+ else:
1828
+ openedvalue = openedcomm = 0.0
1829
+
1830
+ # If ago equals None, return cash
1831
+ if ago is None:
1832
+ # return cash from pseudo-execution
1833
+ return cash
1834
+ # Order execution size
1835
+ execsize = closed + opened
1836
+ # If order execution size is greater than 0
1837
+ if execsize:
1838
+ # Confirm the operation to the comminfo object
1839
+ comminfo.confirmexec(execsize, price)
1840
+
1841
+ # do a real position update if something was executed
1842
+ # Update position
1843
+ position.update(execsize, price, data.datetime.datetime())
1844
+ # If closed and transferring interest to pnl, closing commission includes interest charges
1845
+ if closed and self._int2pnl: # Assign accumulated interest data
1846
+ closedcomm += self.d_credit.pop(data, 0.0)
1847
+
1848
+ # Execute and notify the order
1849
+ # Execute order and notify order
1850
+ order.execute(
1851
+ dtcoc or data.datetime[ago],
1852
+ execsize,
1853
+ price,
1854
+ closed,
1855
+ closedvalue,
1856
+ closedcomm,
1857
+ opened,
1858
+ openedvalue,
1859
+ openedcomm,
1860
+ comminfo.margin,
1861
+ pnl,
1862
+ psize,
1863
+ pprice,
1864
+ )
1865
+
1866
+ order.addcomminfo(comminfo)
1867
+
1868
+ self._log_order_executed(
1869
+ order,
1870
+ size=execsize,
1871
+ price=price,
1872
+ commission=closedcomm + openedcomm,
1873
+ cash=cash,
1874
+ data=data,
1875
+ )
1876
+
1877
+ self.notify(order)
1878
+ self._ococheck(order)
1879
+
1880
+ # If opened but insufficient cash, will indicate margin
1881
+ if popened and not opened:
1882
+ # opened was not executed - not enough cash
1883
+ order.margin()
1884
+ self._log_order_margin(order, "insufficient cash or margin at execution")
1885
+ self.notify(order)
1886
+ self._ococheck(order)
1887
+ self._bracketize(order, cancel=True)
1888
+
1889
+ def _execute_dual_side(self, order, ago=None, price=None, cash=None, position=None, dtcoc=None):
1890
+ if ago is not None and price is None:
1891
+ return None
1892
+
1893
+ if self.get_param("filler") is None or ago is None:
1894
+ size = order.executed.remsize
1895
+ else:
1896
+ size = self.get_param("filler")(order, price, ago)
1897
+ if not order.isbuy():
1898
+ size = -size
1899
+
1900
+ comminfo = self.getcommissioninfo(order.data)
1901
+ if order.data._compensate is not None:
1902
+ data = order.data._compensate
1903
+ cinfocomp = self.getcommissioninfo(data)
1904
+ else:
1905
+ data = order.data
1906
+ cinfocomp = comminfo
1907
+
1908
+ position_side = normalize_position_side(getattr(order.info, "position_side", None))
1909
+ actual_leg_position = None
1910
+ if ago is not None:
1911
+ actual_leg_position = self._get_leg_position(data, position_side)
1912
+ signed_position = self._make_signed_position(position_side, actual_leg_position)
1913
+ else:
1914
+ signed_position = position
1915
+
1916
+ if getattr(order.info, "offset", None) in {"close", "close_today", "close_yesterday"}:
1917
+ available = abs(float(signed_position.size or 0.0))
1918
+ required = abs(float(size or 0.0))
1919
+ if required > available + 1e-12:
1920
+ if ago is None:
1921
+ return float("-inf")
1922
+ order.reject()
1923
+ self._log_order_rejected(order, "close quantity exceeds available position")
1924
+ self.notify(order)
1925
+ self._ococheck(order)
1926
+ self._bracketize(order, cancel=True)
1927
+ return None
1928
+
1929
+ if ago is not None:
1930
+ pprice_orig = signed_position.price
1931
+ psize, pprice, opened, closed = signed_position.pseudoupdate(size, price)
1932
+ pnl = comminfo.profitandloss(-closed, pprice_orig, price)
1933
+ cash = self._cash
1934
+ else:
1935
+ pnl = 0
1936
+ if not self.get_param("coo"):
1937
+ price = pprice_orig = order.created.price
1938
+ else:
1939
+ if order.exectype == Order.Market:
1940
+ price = pprice_orig = order.data.open[0]
1941
+ else:
1942
+ price = pprice_orig = order.created.price
1943
+ psize, pprice, opened, closed = signed_position.update(size, price)
1944
+
1945
+ if closed:
1946
+ if self._shortcash:
1947
+ closedvalue = comminfo.getvaluesize(-closed, pprice_orig)
1948
+ else:
1949
+ closedvalue = comminfo.getoperationcost(closed, pprice_orig)
1950
+
1951
+ closecash = closedvalue
1952
+ if closedvalue > 0:
1953
+ closecash /= comminfo.get_leverage()
1954
+ cash += closecash + pnl * comminfo.stocklike
1955
+ closedcomm = self._getcommission_role(
1956
+ comminfo,
1957
+ closed,
1958
+ price,
1959
+ self._close_commission_role(getattr(order.info, "offset", None)),
1960
+ )
1961
+ cash -= closedcomm
1962
+ if ago is not None:
1963
+ cash += comminfo.cashadjust(-closed, signed_position.adjbase, price)
1964
+ self._cash = cash
1965
+ else:
1966
+ closedvalue = closedcomm = 0.0
1967
+
1968
+ popened = opened
1969
+ if opened:
1970
+ if self._shortcash:
1971
+ openedvalue = comminfo.getvaluesize(opened, price)
1972
+ else:
1973
+ openedvalue = comminfo.getoperationcost(opened, price)
1974
+
1975
+ opencash = openedvalue
1976
+ if openedvalue > 0:
1977
+ opencash /= comminfo.get_leverage()
1978
+ cash -= opencash
1979
+ openedcomm = self._getcommission_role(cinfocomp, opened, price, "open")
1980
+ cash -= openedcomm
1981
+ if cash < 0.0:
1982
+ opened = 0
1983
+ openedvalue = openedcomm = 0.0
1984
+ elif ago is not None:
1985
+ if abs(psize) > abs(opened):
1986
+ adjsize = psize - opened
1987
+ cash += comminfo.cashadjust(adjsize, signed_position.adjbase, price)
1988
+ signed_position.adjbase = price
1989
+ self._cash = cash
1990
+ else:
1991
+ openedvalue = openedcomm = 0.0
1992
+
1993
+ if ago is None:
1994
+ return cash
1995
+
1996
+ execsize = closed + opened
1997
+ if execsize:
1998
+ comminfo.confirmexec(execsize, price)
1999
+ signed_position.update(execsize, price, data.datetime.datetime())
2000
+ if closed and self._int2pnl:
2001
+ closedcomm += self.d_credit.pop(self._credit_key(data, position_side), 0.0)
2002
+
2003
+ if actual_leg_position is not None:
2004
+ self._apply_signed_position(position_side, actual_leg_position, signed_position)
2005
+ self._sync_net_position(data)
2006
+
2007
+ order.execute(
2008
+ dtcoc or data.datetime[ago],
2009
+ execsize,
2010
+ price,
2011
+ closed,
2012
+ closedvalue,
2013
+ closedcomm,
2014
+ opened,
2015
+ openedvalue,
2016
+ openedcomm,
2017
+ comminfo.margin,
2018
+ pnl,
2019
+ psize,
2020
+ pprice,
2021
+ )
2022
+
2023
+ order.addcomminfo(comminfo)
2024
+
2025
+ self._log_order_executed(
2026
+ order,
2027
+ size=execsize,
2028
+ price=price,
2029
+ commission=closedcomm + openedcomm,
2030
+ cash=cash,
2031
+ data=data,
2032
+ )
2033
+ self.notify(order)
2034
+ self._ococheck(order)
2035
+
2036
+ if popened and not opened:
2037
+ order.margin()
2038
+ self._log_order_margin(order, "insufficient cash or margin at execution")
2039
+ self.notify(order)
2040
+ self._ococheck(order)
2041
+ self._bracketize(order, cancel=True)
2042
+
2043
+ def notify(self, order):
2044
+ """Add an order notification to the notification queue.
2045
+
2046
+ Args:
2047
+ order: Order to create notification for
2048
+ """
2049
+ self.notifs.append(order.clone())
2050
+
2051
+ # Try to execute historical
2052
+ def _try_exec_historical(self, order):
2053
+ self._execute(order, ago=0, price=order.created.price)
2054
+
2055
+ # Try to execute market order
2056
+ def _try_exec_market(self, order, popen, phigh, plow):
2057
+ # If cheat_on_close is True or cheat_on_open in order is True
2058
+ if self.get_param("coc") and order.info.get("coc", True):
2059
+ # Order creation time
2060
+ dtcoc = order.created.dt
2061
+ # Execution price
2062
+ exprice = order.created.pclose
2063
+ # If coc is not True
2064
+ else:
2065
+ # If current is not cheat_on_open, and data time is less than or equal to creation time, return without executing
2066
+ if not self.get_param("coo") and order.data.datetime[0] <= order.created.dt:
2067
+ return # can only execute after creation time
2068
+ # Set dtcoc to None
2069
+ dtcoc = None
2070
+ # Execution price equals popen
2071
+ exprice = popen
2072
+ # For buy and sell orders, get prices after considering slippage respectively
2073
+ if order.isbuy():
2074
+ p = self._slip_up(phigh, exprice, doslip=self.get_param("slip_open"))
2075
+ else:
2076
+ p = self._slip_down(plow, exprice, doslip=self.get_param("slip_open"))
2077
+ # Execute order
2078
+ self._execute(order, ago=0, price=p, dtcoc=dtcoc)
2079
+
2080
+ # Try to execute close order
2081
+ def _try_exec_close(self, order, pclose):
2082
+ # pannotated allows to keep track of the closing bar if there is no
2083
+ # information which lets us know that the current bar is the closing
2084
+ # bar (like matching end of session bar)
2085
+ # The actual matching will be done one bar afterwards but using the
2086
+ # information from the actual closing bar
2087
+ # Get current time
2088
+ dt0 = order.data.datetime[0]
2089
+ # don't use "len" -> in replay the close can be reached with same len
2090
+ # If current time is greater than order creation time
2091
+ if dt0 > order.created.dt: # can only execute after creation time
2092
+ # or (self.get_param('eosbar') and dt0 == order.dteos):
2093
+ # If current time is greater than or equal to order's end of day time
2094
+ if dt0 >= order.dteos:
2095
+ # past the end of session or right at it and eosbar is True
2096
+ # If order.pannotated is a price and dt0 is greater than end of day time, set ago to -1, execution price equals previous close price
2097
+ if order.pannotated is not None and dt0 > order.dteos:
2098
+ ago = -1
2099
+ execprice = order.pannotated
2100
+ # Otherwise, ago equals 0, execution price equals pclose
2101
+ else:
2102
+ ago = 0
2103
+ execprice = pclose
2104
+ # Execute order
2105
+ self._execute(order, ago=ago, price=execprice)
2106
+ return
2107
+
2108
+ # If no execution has taken place ... annotate the closing price
2109
+ # If dt0 is less than or equal to order creation time, update order's pannotated to price
2110
+ order.pannotated = pclose
2111
+
2112
+ # Try to execute limit order
2113
+ def _try_exec_limit(self, order, popen, phigh, plow, plimit):
2114
+ # If buy order
2115
+ if order.isbuy():
2116
+ # If plimit is greater than or equal to popen
2117
+ if plimit >= popen:
2118
+ # open smaller/equal than requested - buy cheaper
2119
+ # Calculate pmax
2120
+ pmax = min(phigh, plimit)
2121
+ # Calculate price after adding slippage
2122
+ p = self._slip_up(pmax, popen, doslip=self.get_param("slip_open"), lim=True)
2123
+ # Execute order
2124
+ self._execute(order, ago=0, price=p)
2125
+ # If plimit is greater than or equal to plow, execute order
2126
+ elif plimit >= plow:
2127
+ # day low below req price ... match limit price
2128
+ self._execute(order, ago=0, price=plimit)
2129
+ # Sell order
2130
+ else: # Sell
2131
+ # plimit is less than or equal to popen
2132
+ if plimit <= popen:
2133
+ # open greater/equal than requested - sell more expensive
2134
+ # Calculate price after adding slippage
2135
+ p = self._slip_down(plimit, popen, doslip=self.get_param("slip_open"), lim=True)
2136
+ # Execute order
2137
+ self._execute(order, ago=0, price=p)
2138
+ # If plimit is less than or equal to high price, execute order
2139
+ elif plimit <= phigh:
2140
+ # day high above req price ... match limit price
2141
+ self._execute(order, ago=0, price=plimit)
2142
+
2143
+ # Try to execute stop price
2144
+ def _try_exec_stop(self, order, popen, phigh, plow, pcreated, pclose):
2145
+ # Buy order
2146
+ if order.isbuy():
2147
+ # popen is greater than or equal to pcreated
2148
+ if popen >= pcreated:
2149
+ # price penetrated with an open gap - use open
2150
+ # Calculate price considering slippage
2151
+ p = self._slip_up(phigh, popen, doslip=self.get_param("slip_open"))
2152
+ # Execute order
2153
+ self._execute(order, ago=0, price=p)
2154
+ # If phigh is less than or equal to pcreated
2155
+ elif phigh >= pcreated:
2156
+ # price penetrated during the session - use trigger price
2157
+ # Calculate price considering slippage
2158
+ p = self._slip_up(phigh, pcreated)
2159
+ # Execute order
2160
+ self._execute(order, ago=0, price=p)
2161
+ # Sell order
2162
+ else: # Sell
2163
+ # If popen is less than pcreated
2164
+ if popen <= pcreated:
2165
+ # price penetrated with an open gap - use open
2166
+ # Calculate price considering slippage
2167
+ p = self._slip_down(plow, popen, doslip=self.get_param("slip_open"))
2168
+ # Execute order
2169
+ self._execute(order, ago=0, price=p)
2170
+ # If plow is less than or equal to pcreated
2171
+ elif plow <= pcreated:
2172
+ # price penetrated during the session - use trigger price
2173
+ # Calculate price considering slippage
2174
+ p = self._slip_down(plow, pcreated)
2175
+ # Execute order
2176
+ self._execute(order, ago=0, price=p)
2177
+
2178
+ # not (completely) executed and trailing stop
2179
+ # If order is alive and order type is StopTrail, adjust price based on pclose
2180
+ if order.alive() and order.exectype == Order.StopTrail:
2181
+ order.trailadjust(pclose)
2182
+
2183
+ # Try to execute stop-limit order
2184
+ def _try_exec_stoplimit(self, order, popen, phigh, plow, pclose, pcreated, plimit):
2185
+ # Similar to stop orders, except stop orders place market orders when stop is triggered, while this places limit orders
2186
+ if order.isbuy():
2187
+ if popen >= pcreated:
2188
+ order.triggered = True
2189
+ self._try_exec_limit(order, popen, phigh, plow, plimit)
2190
+
2191
+ elif phigh >= pcreated:
2192
+ # price penetrated upwards during the session
2193
+ order.triggered = True
2194
+ # can calculate execution for a few cases - datetime is fixed
2195
+ if popen > pclose:
2196
+ if plimit >= pcreated: # limit above stop trigger
2197
+ p = self._slip_up(phigh, pcreated, lim=True)
2198
+ self._execute(order, ago=0, price=p)
2199
+ elif plimit >= pclose:
2200
+ self._execute(order, ago=0, price=plimit)
2201
+ else: # popen < pclose
2202
+ if plimit >= pcreated:
2203
+ p = self._slip_up(phigh, pcreated, lim=True)
2204
+ self._execute(order, ago=0, price=p)
2205
+ else: # Sell
2206
+ if popen <= pcreated:
2207
+ # price penetrated downwards with an open gap
2208
+ order.triggered = True
2209
+ self._try_exec_limit(order, popen, phigh, plow, plimit)
2210
+
2211
+ elif plow <= pcreated:
2212
+ # price penetrated downwards during the session
2213
+ order.triggered = True
2214
+ # can calculate execution for a few cases - datetime is fixed
2215
+ if popen <= pclose:
2216
+ if plimit <= pcreated:
2217
+ p = self._slip_down(plow, pcreated, lim=True)
2218
+ self._execute(order, ago=0, price=p)
2219
+ elif plimit <= pclose:
2220
+ self._execute(order, ago=0, price=plimit)
2221
+ else:
2222
+ # popen > pclose
2223
+ if plimit <= pcreated:
2224
+ p = self._slip_down(plow, pcreated, lim=True)
2225
+ self._execute(order, ago=0, price=p)
2226
+
2227
+ # not (completely) executed and trailing stop
2228
+ if order.alive() and order.exectype == Order.StopTrailLimit:
2229
+ order.trailadjust(pclose)
2230
+
2231
+ # Add upward slippage
2232
+ def _slip_up(self, pmax, price, doslip=True, lim=False):
2233
+ if not doslip:
2234
+ return price
2235
+
2236
+ slip_perc = self.get_param("slip_perc")
2237
+ slip_fixed = self.get_param("slip_fixed")
2238
+ if slip_perc:
2239
+ pslip = price * (1 + slip_perc)
2240
+ elif slip_fixed:
2241
+ pslip = price + slip_fixed
2242
+ else:
2243
+ return price
2244
+
2245
+ if pslip <= pmax: # slipping can return price
2246
+ return pslip
2247
+ if self.get_param("slip_match") or (lim and self.get_param("slip_limit")):
2248
+ if not self.get_param("slip_out"):
2249
+ return pmax
2250
+
2251
+ return pslip # non existent price
2252
+
2253
+ return None # no price can be returned
2254
+
2255
+ # Add downward slippage
2256
+ def _slip_down(self, pmin, price, doslip=True, lim=False):
2257
+ if not doslip:
2258
+ return price
2259
+
2260
+ slip_perc = self.get_param("slip_perc")
2261
+ slip_fixed = self.get_param("slip_fixed")
2262
+ if slip_perc:
2263
+ pslip = price * (1 - slip_perc)
2264
+ elif slip_fixed:
2265
+ pslip = price - slip_fixed
2266
+ else:
2267
+ return price
2268
+
2269
+ if pslip >= pmin: # slipping can return price
2270
+ return pslip
2271
+ if self.get_param("slip_match") or (lim and self.get_param("slip_limit")):
2272
+ if not self.get_param("slip_out"):
2273
+ return pmin
2274
+
2275
+ return pslip # non existent price
2276
+
2277
+ return None # no price can be returned
2278
+
2279
+ # Try to execute order
2280
+ def _try_exec(self, order):
2281
+ # Data that generated the order
2282
+ data = order.data
2283
+ # Get open, high, low, close prices respectively, use tick data if available
2284
+ popen = getattr(data, "tick_open", None)
2285
+ if popen is None:
2286
+ popen = data.open[0]
2287
+ phigh = getattr(data, "tick_high", None)
2288
+ if phigh is None:
2289
+ phigh = data.high[0]
2290
+ plow = getattr(data, "tick_low", None)
2291
+ if plow is None:
2292
+ plow = data.low[0]
2293
+ pclose = getattr(data, "tick_close", None)
2294
+ if pclose is None:
2295
+ pclose = data.close[0]
2296
+
2297
+ pcreated = order.created.price
2298
+ plimit = order.created.pricelimit
2299
+
2300
+ # Execute separately according to different order types
2301
+ if order.exectype == Order.Market:
2302
+ self._try_exec_market(order, popen, phigh, plow)
2303
+
2304
+ elif order.exectype == Order.Close:
2305
+ self._try_exec_close(order, pclose)
2306
+
2307
+ elif order.exectype == Order.Limit:
2308
+ self._try_exec_limit(order, popen, phigh, plow, pcreated)
2309
+
2310
+ elif order.triggered and order.exectype in [Order.StopLimit, Order.StopTrailLimit]:
2311
+ self._try_exec_limit(order, popen, phigh, plow, plimit)
2312
+
2313
+ elif order.exectype in [Order.Stop, Order.StopTrail]:
2314
+ self._try_exec_stop(order, popen, phigh, plow, pcreated, pclose)
2315
+
2316
+ elif order.exectype in [Order.StopLimit, Order.StopTrailLimit]:
2317
+ self._try_exec_stoplimit(order, popen, phigh, plow, pclose, pcreated, plimit)
2318
+
2319
+ elif order.exectype == Order.Historical:
2320
+ self._try_exec_historical(order)
2321
+
2322
+ # Process fund history
2323
+ def _process_fund_history(self):
2324
+ fhist = self._fundhist # [last element, iterator]
2325
+ f, funds = fhist
2326
+ if not f:
2327
+ return self._fhistlast
2328
+
2329
+ dt = f[0] # date/datetime instance
2330
+ if isinstance(dt, string_types):
2331
+ dtfmt = "%Y-%m-%d"
2332
+ if "T" in dt:
2333
+ dtfmt += "T%H:%M:%S"
2334
+ if "." in dt:
2335
+ dtfmt += ".%f"
2336
+ dt = datetime.datetime.strptime(dt, dtfmt)
2337
+ f[0] = dt # update value
2338
+
2339
+ elif isinstance(dt, datetime.datetime):
2340
+ pass
2341
+ elif isinstance(dt, datetime.date):
2342
+ dt = datetime.datetime(year=dt.year, month=dt.month, day=dt.day)
2343
+ f[0] = dt # Update the value
2344
+
2345
+ # Synchronization with the strategy is not possible because the broker
2346
+ # is called before the strategy advances. The 2 lines below would do it
2347
+ # if possible
2348
+ # st0 = self.cerebro.runningstrats[0]
2349
+ # if dt <= st0.datetime.datetime():
2350
+ if dt <= self.cerebro._dtmaster:
2351
+ self._fhistlast = f[1:]
2352
+ fhist[0] = list(next(funds, []))
2353
+
2354
+ return self._fhistlast
2355
+
2356
+ # Process order history
2357
+ def _process_order_history(self):
2358
+ for uhist in self._userhist:
2359
+ uhorder, uhorders, uhnotify = uhist
2360
+ while uhorder is not None:
2361
+ uhorder = list(uhorder) # to support assignment (if tuple)
2362
+ try:
2363
+ dataidx = uhorder[3] # 2nd field
2364
+ except IndexError:
2365
+ dataidx = None # Field not present, use default
2366
+
2367
+ if dataidx is None:
2368
+ d = self.cerebro.datas[0]
2369
+ elif isinstance(dataidx, integer_types):
2370
+ d = self.cerebro.datas[dataidx]
2371
+ else: # assume string
2372
+ d = self.cerebro.datasbyname[dataidx]
2373
+
2374
+ if not len(d):
2375
+ break # may start later than other data feeds
2376
+
2377
+ dt = uhorder[0] # date/datetime instance
2378
+ if isinstance(dt, string_types):
2379
+ dtfmt = "%Y-%m-%d"
2380
+ if "T" in dt:
2381
+ dtfmt += "T%H:%M:%S"
2382
+ if "." in dt:
2383
+ dtfmt += ".%f"
2384
+ dt = datetime.datetime.strptime(dt, dtfmt)
2385
+ uhorder[0] = dt
2386
+ elif isinstance(dt, datetime.datetime):
2387
+ pass
2388
+ elif isinstance(dt, datetime.date):
2389
+ dt = datetime.datetime(year=dt.year, month=dt.month, day=dt.day)
2390
+ uhorder[0] = dt
2391
+
2392
+ if dt > d.datetime.datetime():
2393
+ break # cannot execute yet 1st in queue, stop processing
2394
+
2395
+ size = uhorder[1]
2396
+ price = uhorder[2]
2397
+ owner = self.cerebro.runningstrats[0]
2398
+ if size > 0:
2399
+ self.buy(
2400
+ owner=owner,
2401
+ data=d,
2402
+ size=size,
2403
+ price=price,
2404
+ exectype=Order.Historical,
2405
+ histnotify=uhnotify,
2406
+ _checksubmit=False,
2407
+ )
2408
+
2409
+ elif size < 0:
2410
+ self.sell(
2411
+ owner=owner,
2412
+ data=d,
2413
+ size=abs(size),
2414
+ price=price,
2415
+ exectype=Order.Historical,
2416
+ histnotify=uhnotify,
2417
+ _checksubmit=False,
2418
+ )
2419
+
2420
+ # update to next potential order
2421
+ uhist[0] = uhorder = next(uhorders, None)
2422
+
2423
+ def next(self):
2424
+ """Process broker operations for the current time step.
2425
+
2426
+ This method:
2427
+ - Activates pending orders
2428
+ - Validates submitted orders
2429
+ - Calculates interest charges
2430
+ - Processes order history
2431
+ - Executes pending orders
2432
+ - Adjusts cash for mark-to-market
2433
+ """
2434
+ getcommissioninfo = self.getcommissioninfo
2435
+ d_credit = self.d_credit
2436
+ pending = self.pending
2437
+ notify = self.notify
2438
+ ococheck = self._ococheck
2439
+ bracketize = self._bracketize
2440
+ try_exec = self._try_exec
2441
+ dual_side_mode = self._dual_side_mode
2442
+
2443
+ toactivate = self._toactivate
2444
+ while toactivate:
2445
+ toactivate.popleft().activate()
2446
+
2447
+ no_open_positions = False
2448
+ if not dual_side_mode and not pending and not self.submitted and not self._userhist:
2449
+ try:
2450
+ no_open_positions = self._no_open_positions
2451
+ except AttributeError:
2452
+ no_open_positions = False
2453
+
2454
+ checksubmit = self._checksubmit
2455
+ if checksubmit and self.submitted:
2456
+ self.check_submitted()
2457
+
2458
+ # Discount any cash for positions hold
2459
+ # Interest charges
2460
+ credit = 0.0
2461
+ has_position = dual_side_mode
2462
+ if dual_side_mode:
2463
+ for data, position_side, pos in self._iter_dual_side_positions():
2464
+ if pos.size:
2465
+ comminfo = getcommissioninfo(data)
2466
+ dt0 = data.datetime.datetime()
2467
+ signed_position = self._make_signed_position(position_side, pos)
2468
+ dcredit = comminfo.get_credit_interest(data, signed_position, dt0)
2469
+ d_credit[self._credit_key(data, position_side)] += dcredit
2470
+ credit += dcredit
2471
+ pos.datetime = dt0
2472
+ elif not no_open_positions:
2473
+ for data, pos in self.positions.items():
2474
+ if pos.size:
2475
+ has_position = True
2476
+ comminfo = getcommissioninfo(data)
2477
+ dt0 = data.datetime.datetime()
2478
+ dcredit = comminfo.get_credit_interest(data, pos, dt0)
2479
+ d_credit[data] += dcredit
2480
+ credit += dcredit
2481
+ pos.datetime = dt0 # mark last credit operation
2482
+
2483
+ self._cash -= credit
2484
+ # Process order history
2485
+ if self._userhist:
2486
+ self._process_order_history()
2487
+
2488
+ # Iterate once over all elements of the pending queue
2489
+ # Add a None to pending orders
2490
+ pending_processed = bool(pending)
2491
+ if pending:
2492
+ pending.append(None)
2493
+ # Loop through pending orders once, break when reaching None
2494
+ while True:
2495
+ order = pending.popleft()
2496
+ if order is None:
2497
+ break
2498
+
2499
+ if order.expire():
2500
+ notify(order)
2501
+ ococheck(order)
2502
+ bracketize(order, cancel=True)
2503
+
2504
+ elif not order.active():
2505
+ pending.append(order) # cannot yet be processed
2506
+
2507
+ else:
2508
+ try_exec(order)
2509
+ if order.alive():
2510
+ pending.append(order)
2511
+
2512
+ elif order.status == Order.Completed:
2513
+ # a bracket parent order may have been executed
2514
+ bracketize(order)
2515
+
2516
+ # Operations have been executed ... adjust cash end of bar
2517
+ # At the end of bar, adjust cash based on position info
2518
+ cash = self._cash
2519
+ if dual_side_mode:
2520
+ for data, position_side, pos in self._iter_dual_side_positions():
2521
+ if pos.size:
2522
+ comminfo = getcommissioninfo(data)
2523
+ close0 = data.close[0]
2524
+ signed_position = self._make_signed_position(position_side, pos)
2525
+ cash += comminfo.cashadjust(
2526
+ signed_position.size, signed_position.adjbase, close0
2527
+ )
2528
+ pos.adjbase = close0
2529
+ for data in set(self.long_positions) | set(self.short_positions) | set(self.positions):
2530
+ self._sync_net_position(data)
2531
+ else:
2532
+ if has_position or pending_processed or self._userhist:
2533
+ for data, pos in self.positions.items():
2534
+ # futures change cash every bar
2535
+ if pos.size:
2536
+ comminfo = getcommissioninfo(data)
2537
+ close0 = data.close[0]
2538
+ cash += comminfo.cashadjust(pos.size, pos.adjbase, close0)
2539
+ # record the last adjustment price
2540
+ pos.adjbase = close0
2541
+
2542
+ self._cash = cash
2543
+
2544
+ if not has_position and (pending_processed or self._userhist):
2545
+ if dual_side_mode:
2546
+ for _data, _position_side, pos in self._iter_dual_side_positions():
2547
+ if pos.size:
2548
+ has_position = True
2549
+ break
2550
+ else:
2551
+ for pos in self.positions.values():
2552
+ if pos.size:
2553
+ has_position = True
2554
+ break
2555
+
2556
+ if not dual_side_mode:
2557
+ self._no_open_positions = not has_position
2558
+
2559
+ if not has_position and not self._cash_addition and not self._fundhist:
2560
+ self._value = self._cash
2561
+ self._fundval = (
2562
+ self._value / self._fundshares
2563
+ if self._fundshares
2564
+ else self.get_param("fundstartval")
2565
+ )
2566
+ self._valuemkt = 0.0
2567
+ self._valuelever = self._cash
2568
+ self._valuemktlever = 0.0
2569
+ self._leverage = 0.0
2570
+ self._unrealized = 0.0
2571
+ else:
2572
+ self._get_value() # update value
2573
+
2574
+
2575
+ # Alias
2576
+ BrokerBack = BackBroker