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,1991 @@
1
+ """Tick-level broker for unified tick and order book matching.
2
+
3
+ Provides TickBroker which matches orders against tick data and order book
4
+ snapshots instead of bar data, supporting realistic slippage, partial fills,
5
+ depth-aware matching, and all standard order types (Market, Limit, Stop,
6
+ StopLimit).
7
+
8
+ Example:
9
+ Using TickBroker with Cerebro::
10
+
11
+ cerebro = bt.Cerebro()
12
+ cerebro.setbroker(TickBroker(cash=100000))
13
+ cerebro.run(mode='TICK')
14
+ """
15
+
16
+ import collections
17
+
18
+ from backtrader.broker import BrokerBase
19
+ from backtrader.brokers.hft import FillRole, LatencyEngine, MatchingCore, Recorder, StateTracker
20
+ from backtrader.order import BuyOrder, Order, SellOrder
21
+ from backtrader.parameters import ParameterDescriptor
22
+ from backtrader.position import Position
23
+ from backtrader.position_modes import (
24
+ POSITION_MODE_DUAL_SIDE,
25
+ POSITION_SIDE_LONG,
26
+ POSITION_SIDE_SHORT,
27
+ normalize_order_position_meta,
28
+ normalize_position_mode,
29
+ normalize_position_side,
30
+ signed_position_size,
31
+ )
32
+
33
+ from ..utils.log_message import get_logger
34
+
35
+ logger = get_logger(__name__)
36
+
37
+ __all__ = ["TickBroker"]
38
+
39
+
40
+ class TickBroker(BrokerBase):
41
+ """Broker that matches orders against tick-level data.
42
+
43
+ Unlike BackBroker which processes orders at bar boundaries, TickBroker
44
+ evaluates orders on every tick, enabling precise fill prices and
45
+ realistic partial fill simulation.
46
+
47
+ Params:
48
+ cash: Starting cash (default: 100000.0).
49
+ slippage_perc: Slippage as fraction of price (default: 0.0).
50
+ slippage_fixed: Fixed slippage amount per trade (default: 0.0).
51
+ allow_partial: Allow partial fills (default: True).
52
+ checksubmit: Check cash before accepting orders (default: True).
53
+ coo: Execute on Close-of-Order (default: False).
54
+ coc: Execute on Close-of-Cancel (default: False).
55
+ max_depth_levels: Maximum order book levels to traverse (default: 20).
56
+ enable_impact: Enable market impact adjustments (default: False).
57
+ shortcash: Increase cash when shorting stock-like assets (default: True).
58
+ int2pnl: Assign generated interest to profit and loss (default: True).
59
+ """
60
+
61
+ # Tick matching happens in process_tick/process_orderbook, so polling the
62
+ # broker while a live feed is temporarily silent is side-effect free. The
63
+ # flag also lets Cerebro drain order notifications produced by notify_idle
64
+ # risk controls without inventing a data bar.
65
+ next_without_bar = True
66
+
67
+ cash = ParameterDescriptor(default=100000.0, doc="Starting cash")
68
+ slippage_perc = ParameterDescriptor(default=0.0, doc="Slippage as fraction of price")
69
+ slippage_fixed = ParameterDescriptor(default=0.0, doc="Fixed slippage amount")
70
+ allow_partial = ParameterDescriptor(default=True, doc="Allow partial fills")
71
+ checksubmit = ParameterDescriptor(default=True, doc="Check cash before accepting")
72
+ coo = ParameterDescriptor(default=False, doc="Close-on-Open")
73
+ coc = ParameterDescriptor(default=False, doc="Close-on-Close")
74
+ max_depth_levels = ParameterDescriptor(default=20, doc="Max depth levels to traverse")
75
+ enable_impact = ParameterDescriptor(default=False, doc="Enable market impact model")
76
+ shortcash = ParameterDescriptor(
77
+ default=True, doc="Increase cash when shorting stock-like assets"
78
+ )
79
+ int2pnl = ParameterDescriptor(default=True, doc="Assign generated interest to profit and loss")
80
+ position_mode = ParameterDescriptor(default="net", doc="net | dual_side")
81
+
82
+ def __init__(
83
+ self,
84
+ impact_model=None,
85
+ latency_model=None,
86
+ state_tracker=None,
87
+ exchange_model=None,
88
+ recorder=None,
89
+ **kwargs,
90
+ ):
91
+ """Initialize the TickBroker.
92
+
93
+ Sets up internal state for cash, positions, orders, and notifications.
94
+ Uses default cash value from the 'cash' parameter.
95
+
96
+ Args:
97
+ impact_model: Optional market impact model for order book matching.
98
+ latency_model: Optional latency model for order visibility.
99
+ state_tracker: Optional state tracker instance.
100
+ exchange_model: Optional exchange model for maker/taker and TIF semantics.
101
+ recorder: Optional recorder used for timeline snapshots.
102
+ **kwargs: Additional arguments passed to BrokerBase.
103
+ """
104
+ super().__init__(**kwargs)
105
+ self._cash = self.get_param("cash")
106
+ self._value = self._cash
107
+ self.startingcash = self._cash
108
+ self.startingvalue = self._value
109
+ self._orders = []
110
+ self._pending_orders = []
111
+ self._order_history = []
112
+ self._positions: dict = collections.defaultdict(Position)
113
+ self.positions = self._positions
114
+ self.long_positions = collections.defaultdict(Position)
115
+ self.short_positions = collections.defaultdict(Position)
116
+ self._notifs: collections.deque = collections.deque()
117
+ self._fundval = self._cash
118
+ self._fundshares = 1.0
119
+ self._fundmode = False
120
+ self._last_tick = {}
121
+ self._last_orderbook = {}
122
+ self._impact_model = impact_model
123
+ self._latency_model = latency_model
124
+ self._exchange_model = exchange_model
125
+ self._state_tracker_factory = state_tracker
126
+ self._recorder_factory = recorder
127
+ self._latency_engine = LatencyEngine(latency_model=latency_model)
128
+ self._matching_core = MatchingCore(
129
+ latency_engine=self._latency_engine,
130
+ exchange_model=self._exchange_model,
131
+ )
132
+ self._state_tracker = state_tracker or StateTracker()
133
+ self._recorder = recorder or Recorder()
134
+ self._orders_by_symbol = collections.defaultdict(list)
135
+ self._last_event_ts = 0.0
136
+ self._tick_count = 0
137
+ self._position_mode_frozen = False
138
+ self._position_mode_frozen_reason = None
139
+ BrokerBase.set_param(
140
+ self, "position_mode", normalize_position_mode(self.get_param("position_mode"))
141
+ )
142
+
143
+ def start(self):
144
+ """Initialize the broker state for a new backtesting run.
145
+
146
+ Resets cash to the starting value and clears any previous state
147
+ to ensure clean backtesting across multiple runs.
148
+ """
149
+ super().start()
150
+ self._cash = self.get_param("cash")
151
+ self._value = self._cash
152
+ self.startingcash = self._cash
153
+ self.startingvalue = self._value
154
+ self._pending_orders = []
155
+ self._order_history = []
156
+ self._positions = collections.defaultdict(Position)
157
+ self.positions = self._positions
158
+ self.long_positions = collections.defaultdict(Position)
159
+ self.short_positions = collections.defaultdict(Position)
160
+ self._notifs = collections.deque()
161
+ self._last_tick = {}
162
+ self._last_orderbook = {}
163
+ self._orders_by_symbol = collections.defaultdict(list)
164
+ self._latency_engine = LatencyEngine(latency_model=self._latency_model)
165
+ self._matching_core = MatchingCore(
166
+ latency_engine=self._latency_engine,
167
+ exchange_model=self._exchange_model,
168
+ )
169
+ self._state_tracker = self._state_tracker_factory or StateTracker()
170
+ self._recorder = self._recorder_factory or Recorder()
171
+ self._last_event_ts = 0.0
172
+ self._tick_count = 0
173
+ self._freeze_position_mode("start()")
174
+
175
+ def set_param(self, name, value, validate=True):
176
+ """Override :meth:`BrokerBase.set_param` to guard ``position_mode`` changes.
177
+
178
+ The ``position_mode`` parameter is treated specially: it is
179
+ immutable once :meth:`start` has run (frozen via
180
+ :meth:`_freeze_position_mode`), and its raw value is normalized
181
+ through :func:`normalize_position_mode` so that the broker
182
+ always stores one of the canonical ``"net"`` /
183
+ ``"dual_side"`` strings.
184
+
185
+ Args:
186
+ name: Name of the parameter to set.
187
+ value: New value for the parameter. For ``position_mode`` the
188
+ value is normalized before being applied.
189
+ validate: When ``True`` (default), delegate to the base class
190
+ so that the registered validator runs. Set to ``False``
191
+ to bypass validation (used internally when applying
192
+ normalized values).
193
+
194
+ Returns:
195
+ The return value of :meth:`BrokerBase.set_param` after the
196
+ value has been applied.
197
+
198
+ Raises:
199
+ ValueError: If ``name == "position_mode"`` and the parameter
200
+ has already been frozen by :meth:`start`.
201
+ """
202
+ if name == "position_mode":
203
+ self._ensure_position_mode_mutable()
204
+ value = normalize_position_mode(value)
205
+ return super().set_param(name, value, validate=validate)
206
+
207
+ def _freeze_position_mode(self, reason):
208
+ self._position_mode_frozen = True
209
+ self._position_mode_frozen_reason = reason
210
+
211
+ def _ensure_position_mode_mutable(self):
212
+ if getattr(self, "_position_mode_frozen", False):
213
+ raise ValueError(
214
+ "position_mode is frozen after "
215
+ f"{self._position_mode_frozen_reason} and cannot be changed at runtime"
216
+ )
217
+
218
+ def _is_dual_side_mode(self):
219
+ return normalize_position_mode(self.get_param("position_mode")) == POSITION_MODE_DUAL_SIDE
220
+
221
+ def _normalize_order_meta(self, isbuy, kwargs):
222
+ local_kwargs = dict(kwargs)
223
+ position_side = local_kwargs.pop("position_side", None)
224
+ offset = local_kwargs.pop("offset", None)
225
+ position_side, offset = normalize_order_position_meta(
226
+ self.get_param("position_mode"),
227
+ isbuy,
228
+ position_side=position_side,
229
+ offset=offset,
230
+ )
231
+ return position_side, offset, local_kwargs
232
+
233
+ @staticmethod
234
+ def _attach_position_meta(order, position_side=None, offset=None, **kwargs):
235
+ if position_side is not None:
236
+ order.addinfo(position_side=position_side)
237
+ if offset is not None:
238
+ order.addinfo(offset=offset)
239
+ if kwargs:
240
+ order.addinfo(**kwargs)
241
+ return order
242
+
243
+ def _get_leg_store(self, position_side):
244
+ position_side = normalize_position_side(position_side)
245
+ if position_side == POSITION_SIDE_LONG:
246
+ return self.long_positions
247
+ if position_side == POSITION_SIDE_SHORT:
248
+ return self.short_positions
249
+ raise ValueError(f"Unsupported position_side {position_side!r}")
250
+
251
+ def _get_leg_position(self, symbol, position_side):
252
+ return self._get_leg_store(position_side)[symbol]
253
+
254
+ def _make_signed_position(self, position_side, position):
255
+ signed_position = position.clone()
256
+ signed_position.size = signed_position_size(position_side, position.size)
257
+ if not signed_position.size:
258
+ signed_position.price = 0.0
259
+ signed_position.price_orig = 0.0
260
+ return signed_position
261
+
262
+ def _apply_signed_position(self, position_side, leg_position, signed_position):
263
+ leg_position.size = abs(float(signed_position.size or 0.0))
264
+ leg_position.price = signed_position.price if leg_position.size else 0.0
265
+ leg_position.price_orig = signed_position.price_orig if leg_position.size else 0.0
266
+ leg_position.adjbase = signed_position.adjbase
267
+ leg_position.datetime = signed_position.datetime
268
+ leg_position.updt = signed_position.updt
269
+ leg_position.upopened = abs(float(signed_position.upopened or 0.0))
270
+ leg_position.upclosed = abs(float(signed_position.upclosed or 0.0))
271
+ return leg_position
272
+
273
+ def _sync_net_position(self, symbol):
274
+ long_pos = self.long_positions[symbol]
275
+ short_pos = self.short_positions[symbol]
276
+ net_pos = self._positions[symbol]
277
+ net_size = long_pos.size - short_pos.size
278
+ if net_size > 0:
279
+ net_price = long_pos.price
280
+ elif net_size < 0:
281
+ net_price = short_pos.price
282
+ else:
283
+ net_price = 0.0
284
+ net_pos.fix(net_size, net_price)
285
+ if long_pos.datetime is not None and short_pos.datetime is not None:
286
+ net_pos.datetime = max(long_pos.datetime, short_pos.datetime)
287
+ else:
288
+ net_pos.datetime = long_pos.datetime or short_pos.datetime
289
+ net_pos.adjbase = long_pos.adjbase if long_pos.size else short_pos.adjbase
290
+ return net_pos
291
+
292
+ def stop(self):
293
+ """Stop the broker and perform cleanup.
294
+
295
+ Called at the end of a backtesting run. Override in subclasses
296
+ to implement custom cleanup logic.
297
+ """
298
+
299
+ def getcash(self):
300
+ """Get current available cash."""
301
+ return self._cash
302
+
303
+ def getvalue(self, datas=None):
304
+ """Value positions from the latest tick/book and their commission scheme.
305
+
306
+ Futures cash excludes the margin frozen at entry. Add that margin
307
+ back, together with PnL since the last cash adjustment; native contract
308
+ counts are not stock quantities. Reading value never settles cash.
309
+ """
310
+ val = self._cash
311
+ if self._is_dual_side_mode():
312
+ symbols = set(self.long_positions) | set(self.short_positions) | set(self._positions)
313
+ for symbol in symbols:
314
+ for side, positions in (
315
+ (POSITION_SIDE_LONG, self.long_positions),
316
+ (POSITION_SIDE_SHORT, self.short_positions),
317
+ ):
318
+ position = positions.get(symbol)
319
+ if position is not None and position.size:
320
+ val += self._marked_position_value(
321
+ symbol, self._make_signed_position(side, position)
322
+ )
323
+ return val
324
+
325
+ for data_name, pos in self._positions.items():
326
+ if pos.size != 0:
327
+ val += self._marked_position_value(data_name, pos)
328
+ return val
329
+
330
+ def get_cached_report_state(self):
331
+ """Return local matching state for observers without provider I/O."""
332
+ positions = dict(self._positions)
333
+ position_legs = {}
334
+ if self._is_dual_side_mode():
335
+ for symbol in set(self.long_positions) | set(self.short_positions):
336
+ positions[symbol] = self._sync_net_position(symbol)
337
+ position_legs[symbol] = {
338
+ "long": self.long_positions.get(symbol),
339
+ "short": self.short_positions.get(symbol),
340
+ }
341
+ return {
342
+ "cash": self._cash,
343
+ "value": self.getvalue(),
344
+ "positions": positions,
345
+ "position_legs": position_legs,
346
+ }
347
+
348
+ def _mark_price_for_symbol(self, symbol, fallback=None):
349
+ """Return the latest local tick/book mark for one symbol.
350
+
351
+ The precedence deliberately matches :meth:`_marked_position_value`:
352
+ a newer valid order book midpoint supersedes a tick; otherwise the
353
+ most recent tick is used. No provider call is made here.
354
+ """
355
+ tick = self._last_tick.get(symbol)
356
+ book = self._last_orderbook.get(symbol)
357
+ price = fallback
358
+ if tick is not None:
359
+ price = getattr(tick, "price", price)
360
+ if book is not None and (tick is None or book.timestamp >= tick.timestamp):
361
+ if book.bids and book.asks:
362
+ price = (book.bids[0][0] + book.asks[0][0]) / 2.0
363
+ return price
364
+
365
+ def get_cached_mark_price(self, data):
366
+ """Return a local mark price for a data reference, if one is cached."""
367
+ symbol = self._get_data_name(data)
368
+ price = self._mark_price_for_symbol(symbol)
369
+ try:
370
+ return float(price) if price is not None else None
371
+ except (TypeError, ValueError):
372
+ return None
373
+
374
+ def get_mark_price(self, data):
375
+ """Compatibility alias for :meth:`get_cached_mark_price`."""
376
+ return self.get_cached_mark_price(data)
377
+
378
+ def _marked_position_value(self, symbol, position):
379
+ price = self._mark_price_for_symbol(symbol, position.price)
380
+ comminfo = self.comminfo.get(symbol, self.comminfo[None])
381
+ if comminfo.stocklike:
382
+ return position.size * price
383
+ margin = comminfo.getvalue(position, position.price) / comminfo.get_leverage()
384
+ adjusted_from = position.adjbase if position.adjbase is not None else position.price
385
+ return margin + comminfo.cashadjust(position.size, adjusted_from, price)
386
+
387
+ def getposition(self, data, side=None):
388
+ """Get current position for a data feed."""
389
+ name = getattr(data, "_name", None) or getattr(data, "symbol", str(data))
390
+ if side is not None:
391
+ if not self._is_dual_side_mode():
392
+ raise ValueError("side-specific getposition() is only available in dual_side mode")
393
+ return self._get_leg_position(name, side)
394
+ if self._is_dual_side_mode():
395
+ return self._sync_net_position(name)
396
+ return self._positions[name]
397
+
398
+ def submit(self, order):
399
+ """Submit an order for execution.
400
+
401
+ Overrides the default submit to handle tick-mode orders that
402
+ don't have LineSeries data (avoids len(data) call in Order.submit).
403
+ """
404
+ self._freeze_position_mode("first order submission")
405
+ # Matching models consume order attributes, while Strategy.buy/sell
406
+ # kwargs are retained in info. Preserve both views of the same flags.
407
+ tif = getattr(order, "time_in_force", order.info.get("time_in_force", "GTC"))
408
+ order.time_in_force = str(getattr(tif, "value", tif)).upper()
409
+ order.reduce_only = order.info.get("reduce_only", getattr(order, "reduce_only", False))
410
+ if not isinstance(order.reduce_only, bool):
411
+ order.addinfo(reject_reason="INVALID_REDUCE_ONLY")
412
+ order.reject(self)
413
+ self.notify(order)
414
+ return order
415
+ order.status = Order.Submitted
416
+ order.broker = self
417
+ order.plen = 0
418
+ self._matching_core.submit_order(order, current_ts=self._last_event_ts)
419
+ if order in self._matching_core.pending_for_symbol(self._get_data_name(order.data)):
420
+ self._queue_pending_order(order)
421
+ self.notify(order)
422
+ return order
423
+
424
+ def cancel(self, order):
425
+ """Cancel a pending order.
426
+
427
+ Removes the order from the pending orders queue and updates its
428
+ status to Cancelled. If the order is not found in the pending
429
+ queue, the method returns silently.
430
+
431
+ Args:
432
+ order: The Order instance to cancel.
433
+ """
434
+ result = self._matching_core.cancel_order(order)
435
+ if not result.success:
436
+ return
437
+ self._remove_pending_order(order)
438
+ order.cancel()
439
+ self.notify(order)
440
+
441
+ def modify(self, order, size=None, price=None, plimit=None, exectype=None, **kwargs):
442
+ """Modify an order by canceling it and submitting a replacement order."""
443
+ if order not in self._pending_orders and not order.alive():
444
+ return None
445
+
446
+ tif = kwargs.pop("time_in_force", getattr(order, "time_in_force", None))
447
+ replacement = order.__class__(
448
+ owner=order.p.owner,
449
+ data=order.data,
450
+ size=size if size is not None else self._get_remaining_size(order),
451
+ price=price if price is not None else order.price,
452
+ pricelimit=plimit if plimit is not None else order.pricelimit,
453
+ exectype=exectype if exectype is not None else order.exectype,
454
+ valid=order.valid,
455
+ tradeid=order.tradeid,
456
+ oco=order.oco,
457
+ trailamount=order.trailamount,
458
+ trailpercent=order.trailpercent,
459
+ simulated=True,
460
+ **kwargs,
461
+ )
462
+ if tif is not None:
463
+ replacement.time_in_force = tif
464
+
465
+ for key, value in getattr(order, "info", {}).items():
466
+ replacement.addinfo(**{key: value})
467
+
468
+ self.cancel(order)
469
+ order.addinfo(cancel_reason="MODIFY_REPLACED")
470
+ replacement.addinfo(modified_from=order.ref)
471
+ return self.submit(replacement)
472
+
473
+ def buy(
474
+ self,
475
+ owner,
476
+ data,
477
+ size,
478
+ price=None,
479
+ plimit=None,
480
+ exectype=None,
481
+ valid=None,
482
+ tradeid=0,
483
+ oco=None,
484
+ trailamount=None,
485
+ trailpercent=None,
486
+ **kwargs,
487
+ ):
488
+ """Create and submit a buy order.
489
+
490
+ Args:
491
+ owner: The strategy or object creating the order.
492
+ data: The data feed for this order.
493
+ size: Number of shares/contracts (positive for buy).
494
+ price: Limit price for Limit orders.
495
+ plimit: Limit price for StopLimit orders.
496
+ exectype: Order execution type (Market, Limit, Stop, etc.).
497
+ valid: Order validity period.
498
+ tradeid: User-defined trade identifier.
499
+ oco: One-Cancels-Other order group.
500
+ trailamount: Trailing stop amount.
501
+ trailpercent: Trailing stop percentage.
502
+ **kwargs: Additional order parameters.
503
+
504
+ Returns:
505
+ The submitted BuyOrder instance.
506
+ """
507
+ position_side, offset, order_kwargs = self._normalize_order_meta(True, kwargs)
508
+ order = BuyOrder(
509
+ owner=owner,
510
+ data=data,
511
+ size=size,
512
+ price=price,
513
+ pricelimit=plimit,
514
+ exectype=exectype,
515
+ valid=valid,
516
+ tradeid=tradeid,
517
+ oco=oco,
518
+ trailamount=trailamount,
519
+ trailpercent=trailpercent,
520
+ simulated=True,
521
+ )
522
+ self._attach_position_meta(
523
+ order, position_side=position_side, offset=offset, **order_kwargs
524
+ )
525
+ return self.submit(order)
526
+
527
+ def sell(
528
+ self,
529
+ owner,
530
+ data,
531
+ size,
532
+ price=None,
533
+ plimit=None,
534
+ exectype=None,
535
+ valid=None,
536
+ tradeid=0,
537
+ oco=None,
538
+ trailamount=None,
539
+ trailpercent=None,
540
+ **kwargs,
541
+ ):
542
+ """Create and submit a sell order.
543
+
544
+ Args:
545
+ owner: The strategy or object creating the order.
546
+ data: The data feed for this order.
547
+ size: Number of shares/contracts (positive for sell).
548
+ price: Limit price for Limit orders.
549
+ plimit: Limit price for StopLimit orders.
550
+ exectype: Order execution type (Market, Limit, Stop, etc.).
551
+ valid: Order validity period.
552
+ tradeid: User-defined trade identifier.
553
+ oco: One-Cancels-Other order group.
554
+ trailamount: Trailing stop amount.
555
+ trailpercent: Trailing stop percentage.
556
+ **kwargs: Additional order parameters.
557
+
558
+ Returns:
559
+ The submitted SellOrder instance.
560
+ """
561
+ position_side, offset, order_kwargs = self._normalize_order_meta(False, kwargs)
562
+ order = SellOrder(
563
+ owner=owner,
564
+ data=data,
565
+ size=size,
566
+ price=price,
567
+ pricelimit=plimit,
568
+ exectype=exectype,
569
+ valid=valid,
570
+ tradeid=tradeid,
571
+ oco=oco,
572
+ trailamount=trailamount,
573
+ trailpercent=trailpercent,
574
+ simulated=True,
575
+ )
576
+ self._attach_position_meta(
577
+ order, position_side=position_side, offset=offset, **order_kwargs
578
+ )
579
+ return self.submit(order)
580
+
581
+ def notify(self, order):
582
+ """Queue a notification for an order status change.
583
+
584
+ Stores the order in an internal queue for later retrieval by
585
+ strategies via get_notification().
586
+
587
+ Args:
588
+ order: The Order instance with updated status.
589
+ """
590
+ self._notifs.append(order.clone())
591
+
592
+ def get_notification(self):
593
+ """Get the next pending notification from the queue.
594
+
595
+ Strategies call this method to check for order status updates.
596
+
597
+ Returns:
598
+ Order if a notification is available, None otherwise.
599
+ """
600
+ try:
601
+ return self._notifs.popleft()
602
+ except IndexError:
603
+ return None
604
+
605
+ def set_fundmode(self, fundmode, fundstartval=None):
606
+ """Enable or disable fund mode for portfolio management.
607
+
608
+ Fund mode allows treating the portfolio as a fund with shares
609
+ that can be bought/sold by investors.
610
+
611
+ Args:
612
+ fundmode: Boolean to enable/disable fund mode.
613
+ fundstartval: Initial fund value (optional).
614
+ """
615
+ self._fundmode = fundmode
616
+ if fundstartval is not None:
617
+ self._fundval = fundstartval
618
+
619
+ def get_fundmode(self):
620
+ """Check if fund mode is enabled.
621
+
622
+ Returns:
623
+ bool: True if fund mode is active, False otherwise.
624
+ """
625
+ return self._fundmode
626
+
627
+ def get_fundshares(self):
628
+ """Get the current number of fund shares.
629
+
630
+ Returns:
631
+ float: Number of outstanding fund shares.
632
+ """
633
+ return self._fundshares
634
+
635
+ def get_fundvalue(self):
636
+ """Get the current net asset value of the fund.
637
+
638
+ Returns:
639
+ float: Current fund NAV.
640
+ """
641
+ return self._fundval
642
+
643
+ def process_tick(self, tick_event, data=None):
644
+ """Process a tick event and attempt to match pending orders.
645
+
646
+ This is the core method called by Cerebro on each tick. It evaluates
647
+ all pending orders against the current tick data.
648
+
649
+ Args:
650
+ tick_event: TickEvent with current price/volume.
651
+ data: The data feed associated with this tick (optional).
652
+ """
653
+ tick_event = self._latency_engine.apply_feed_latency(tick_event)
654
+ data_name = tick_event.symbol
655
+ current_ts = getattr(tick_event, "local_time", tick_event.timestamp)
656
+ self._last_event_ts = current_ts
657
+ self._last_tick[data_name] = tick_event
658
+ self._tick_count += 1
659
+ self._activate_visible_orders(current_ts)
660
+
661
+ active_orders = [
662
+ order
663
+ for order in list(self._orders_by_symbol.get(data_name, []))
664
+ if self._order_is_active_for_event(order, tick_event)
665
+ ]
666
+
667
+ matched = []
668
+ if self._exchange_model is not None:
669
+ for fill_order, fill_price, fill_size, fill_role in self._exchange_model.on_trade(
670
+ tick_event, active_orders
671
+ ):
672
+ self._execute(fill_order, fill_price, fill_size, tick_event, source=fill_role.value)
673
+ if not fill_order.alive() or not self.get_param("allow_partial"):
674
+ matched.append(fill_order)
675
+
676
+ event_timestamp_ns = int(getattr(tick_event, "timestamp_ns", 0) or 0)
677
+ for order in active_orders:
678
+ if order in matched or getattr(order, "_fill_role", None) != FillRole.MAKER:
679
+ continue
680
+ if (
681
+ float(getattr(order, "_queue_initial_ahead", 0.0)) > 1e-12
682
+ and float(getattr(order, "_queue_ahead", 0.0)) <= 1e-12
683
+ and float(getattr(order, "_queue_fillable", 0.0)) <= 1e-12
684
+ and float(getattr(order, "_queue_trade_qty", 0.0)) > 1e-12
685
+ ):
686
+ order._queue_front_trade_timestamp_ns = event_timestamp_ns
687
+ order._queue_front_trade_persisted_depth = False
688
+ elif float(getattr(order, "_queue_ahead", 0.0)) > 1e-12:
689
+ order._queue_front_trade_timestamp_ns = None
690
+ order._queue_front_trade_persisted_depth = False
691
+
692
+ for order in active_orders:
693
+ if order in matched:
694
+ continue
695
+ if getattr(order, "_fill_role", None) == FillRole.MAKER:
696
+ continue
697
+ result = self._try_match(order, tick_event)
698
+ if result is not None:
699
+ fill_price, fill_size = result
700
+ self._execute(order, fill_price, fill_size, tick_event)
701
+ if not order.alive() or not self.get_param("allow_partial"):
702
+ matched.append(order)
703
+
704
+ for order in matched:
705
+ self._remove_pending_order(order)
706
+
707
+ for order in active_orders:
708
+ self._cancel_ioc_remainder(order, tick_event, source="tick")
709
+
710
+ def process_orderbook(self, ob_event, data=None):
711
+ """Process an order book snapshot and match pending orders.
712
+
713
+ Args:
714
+ ob_event: OrderBookSnapshot with current depth.
715
+ data: The data feed associated with this snapshot (optional).
716
+ """
717
+ ob_event = self._latency_engine.apply_feed_latency(ob_event)
718
+ data_name = ob_event.symbol
719
+ current_ts = getattr(ob_event, "local_time", ob_event.timestamp)
720
+ self._last_event_ts = current_ts
721
+ previous_orderbook = self._last_orderbook.get(data_name)
722
+ ob_event.previous_bids = list(getattr(previous_orderbook, "bids", []) or [])
723
+ ob_event.previous_asks = list(getattr(previous_orderbook, "asks", []) or [])
724
+ self._last_orderbook[data_name] = ob_event
725
+ self._activate_visible_orders(current_ts)
726
+
727
+ active_orders = [
728
+ order
729
+ for order in list(self._orders_by_symbol.get(data_name, []))
730
+ if self._order_is_active_for_event(order, ob_event)
731
+ ]
732
+ for order in active_orders:
733
+ order._queue_trade_qty_before_depth_update = float(
734
+ getattr(order, "_queue_trade_qty", 0.0)
735
+ )
736
+
737
+ matched = []
738
+ if self._exchange_model is not None:
739
+ for (
740
+ fill_order,
741
+ fill_price,
742
+ fill_size,
743
+ fill_role,
744
+ ) in self._exchange_model.on_depth_update(ob_event, active_orders):
745
+ self._execute(fill_order, fill_price, fill_size, ob_event, source=fill_role.value)
746
+ if not fill_order.alive() or not self.get_param("allow_partial"):
747
+ matched.append(fill_order)
748
+
749
+ for order in active_orders:
750
+ if order in matched:
751
+ continue
752
+ if self._exchange_model is not None and order.exectype in (Order.Market, Order.Limit):
753
+ exchange_result = self._exchange_model.on_new_order(order, ob_event)
754
+ if exchange_result.action == "REJECT":
755
+ order.addinfo(reject_reason=exchange_result.reject_reason)
756
+ order.reject(self)
757
+ self.notify(order)
758
+ self._order_history.append(
759
+ {
760
+ "timestamp": ob_event.timestamp,
761
+ "symbol": data_name,
762
+ "side": "buy" if order.isbuy() else "sell",
763
+ "status": "rejected",
764
+ "reason": exchange_result.reject_reason,
765
+ "source": "orderbook_depth",
766
+ }
767
+ )
768
+ matched.append(order)
769
+ continue
770
+ if exchange_result.action == "FILL":
771
+ fill_price, fill_size = self._aggregate_exchange_fills(
772
+ exchange_result.fills, max_size=self._get_matching_size(order)
773
+ )
774
+ if fill_size > 0:
775
+ self._execute(
776
+ order, fill_price, fill_size, ob_event, source="orderbook_depth"
777
+ )
778
+ if self._cancel_ioc_remainder(order, ob_event, source="orderbook_depth"):
779
+ matched.append(order)
780
+ continue
781
+ if not order.alive() or not self.get_param("allow_partial"):
782
+ matched.append(order)
783
+ continue
784
+
785
+ if getattr(order, "_fill_role", None) == FillRole.MAKER:
786
+ if (
787
+ float(getattr(order, "_queue_initial_ahead", 0.0)) > 1e-12
788
+ and float(getattr(order, "_queue_ahead", 0.0)) > 1e-12
789
+ ):
790
+ event_timestamp_ns = int(getattr(ob_event, "timestamp_ns", 0) or 0)
791
+ if order.isbuy():
792
+ same_side_moved_away = not ob_event.bids or float(
793
+ ob_event.bids[0][0]
794
+ ) < float(order.price)
795
+ if (
796
+ same_side_moved_away
797
+ and getattr(
798
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
799
+ )
800
+ is not None
801
+ and event_timestamp_ns
802
+ == int(
803
+ getattr(
804
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
805
+ )
806
+ )
807
+ ):
808
+ fill_size = self._get_remaining_size(order)
809
+ if fill_size > 0:
810
+ self._execute(
811
+ order,
812
+ float(order.price),
813
+ fill_size,
814
+ ob_event,
815
+ source="orderbook_depth",
816
+ )
817
+ if not order.alive() or not self.get_param("allow_partial"):
818
+ matched.append(order)
819
+ continue
820
+ if (
821
+ float(getattr(order, "_queue_trade_qty_before_depth_update", 0.0))
822
+ > 1e-12
823
+ and ob_event.bids
824
+ and float(ob_event.bids[0][0]) == float(order.price)
825
+ and abs(
826
+ float(ob_event.bids[0][1])
827
+ - float(getattr(order, "_queue_ahead", 0.0))
828
+ )
829
+ <= 1e-12
830
+ ):
831
+ order._queue_trade_remainder_confirmed_timestamp_ns = (
832
+ event_timestamp_ns
833
+ )
834
+ elif getattr(
835
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
836
+ ) is not None and event_timestamp_ns == int(
837
+ getattr(
838
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
839
+ )
840
+ ):
841
+ pass
842
+ else:
843
+ order._queue_trade_remainder_confirmed_timestamp_ns = None
844
+ else:
845
+ same_side_moved_away = not ob_event.asks or float(
846
+ ob_event.asks[0][0]
847
+ ) > float(order.price)
848
+ if (
849
+ same_side_moved_away
850
+ and getattr(
851
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
852
+ )
853
+ is not None
854
+ and event_timestamp_ns
855
+ == int(
856
+ getattr(
857
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
858
+ )
859
+ )
860
+ ):
861
+ fill_size = self._get_remaining_size(order)
862
+ if fill_size > 0:
863
+ self._execute(
864
+ order,
865
+ float(order.price),
866
+ fill_size,
867
+ ob_event,
868
+ source="orderbook_depth",
869
+ )
870
+ if not order.alive() or not self.get_param("allow_partial"):
871
+ matched.append(order)
872
+ continue
873
+ if (
874
+ float(getattr(order, "_queue_trade_qty_before_depth_update", 0.0))
875
+ > 1e-12
876
+ and ob_event.asks
877
+ and float(ob_event.asks[0][0]) == float(order.price)
878
+ and abs(
879
+ float(ob_event.asks[0][1])
880
+ - float(getattr(order, "_queue_ahead", 0.0))
881
+ )
882
+ <= 1e-12
883
+ ):
884
+ order._queue_trade_remainder_confirmed_timestamp_ns = (
885
+ event_timestamp_ns
886
+ )
887
+ elif getattr(
888
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
889
+ ) is not None and event_timestamp_ns == int(
890
+ getattr(
891
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
892
+ )
893
+ ):
894
+ pass
895
+ else:
896
+ order._queue_trade_remainder_confirmed_timestamp_ns = None
897
+ order._queue_depleted_move_away_timestamp_ns = None
898
+ continue
899
+ event_timestamp_ns = int(getattr(ob_event, "timestamp_ns", 0) or 0)
900
+ queue_tracked = float(getattr(order, "_queue_initial_ahead", 0.0)) > 1e-12
901
+ if not queue_tracked:
902
+ order._queue_depleted_move_away_timestamp_ns = None
903
+ order._queue_front_trade_timestamp_ns = None
904
+ order._queue_front_trade_persisted_depth = False
905
+ order._queue_trade_remainder_confirmed_timestamp_ns = None
906
+ result = self._try_match_orderbook(order, ob_event)
907
+ if result is None:
908
+ continue
909
+ fill_price, fill_size = result
910
+ if fill_size <= 0:
911
+ continue
912
+ self._execute(
913
+ order, float(order.price), fill_size, ob_event, source="orderbook_depth"
914
+ )
915
+ if not order.alive() or not self.get_param("allow_partial"):
916
+ matched.append(order)
917
+ continue
918
+ same_side_moved_away = False
919
+ if order.isbuy():
920
+ same_side_moved_away = not ob_event.bids or float(
921
+ ob_event.bids[0][0]
922
+ ) < float(order.price)
923
+ front_trade_timestamp_ns = getattr(
924
+ order, "_queue_front_trade_timestamp_ns", None
925
+ )
926
+ if (
927
+ front_trade_timestamp_ns is not None
928
+ and event_timestamp_ns == int(front_trade_timestamp_ns)
929
+ and not same_side_moved_away
930
+ ):
931
+ order._queue_front_trade_persisted_depth = True
932
+ if (
933
+ ob_event.bids
934
+ and float(ob_event.bids[0][0]) == float(order.price)
935
+ and abs(
936
+ float(ob_event.bids[0][1])
937
+ - float(getattr(order, "_queue_ahead", 0.0))
938
+ )
939
+ <= 1e-12
940
+ and float(getattr(order, "_queue_ahead", 0.0)) > 1e-12
941
+ ):
942
+ order._queue_front_trade_remainder_confirmed_timestamp_ns = (
943
+ event_timestamp_ns
944
+ )
945
+ if not same_side_moved_away:
946
+ if (
947
+ float(getattr(order, "_queue_trade_qty_before_depth_update", 0.0))
948
+ > 1e-12
949
+ and ob_event.bids
950
+ and float(ob_event.bids[0][0]) == float(order.price)
951
+ and abs(
952
+ float(ob_event.bids[0][1])
953
+ - float(getattr(order, "_queue_ahead", 0.0))
954
+ )
955
+ <= 1e-12
956
+ and float(getattr(order, "_queue_ahead", 0.0)) > 1e-12
957
+ ):
958
+ order._queue_trade_remainder_confirmed_timestamp_ns = (
959
+ event_timestamp_ns
960
+ )
961
+ if not ob_event.asks or float(ob_event.asks[0][0]) >= float(order.price):
962
+ if queue_tracked and same_side_moved_away:
963
+ moved_away_timestamp_ns = getattr(
964
+ order, "_queue_depleted_move_away_timestamp_ns", None
965
+ )
966
+ front_trade_timestamp_ns = getattr(
967
+ order, "_queue_front_trade_timestamp_ns", None
968
+ )
969
+ front_trade_persisted_depth = bool(
970
+ getattr(order, "_queue_front_trade_persisted_depth", False)
971
+ )
972
+ remainder_confirmed_timestamp_ns = getattr(
973
+ order,
974
+ "_queue_front_trade_remainder_confirmed_timestamp_ns",
975
+ None,
976
+ )
977
+ trade_remainder_confirmed_timestamp_ns = getattr(
978
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
979
+ )
980
+ result = self._try_match_orderbook(order, ob_event)
981
+ if (
982
+ result is not None
983
+ and moved_away_timestamp_ns is not None
984
+ and event_timestamp_ns == int(moved_away_timestamp_ns)
985
+ ):
986
+ fill_price, fill_size = result
987
+ if fill_size > 0:
988
+ self._execute(
989
+ order,
990
+ float(order.price),
991
+ fill_size,
992
+ ob_event,
993
+ source="orderbook_depth",
994
+ )
995
+ if not order.alive() or not self.get_param("allow_partial"):
996
+ matched.append(order)
997
+ continue
998
+ if (
999
+ trade_remainder_confirmed_timestamp_ns is not None
1000
+ and event_timestamp_ns
1001
+ == int(trade_remainder_confirmed_timestamp_ns)
1002
+ ):
1003
+ fill_size = self._get_remaining_size(order)
1004
+ if fill_size > 0:
1005
+ self._execute(
1006
+ order,
1007
+ float(order.price),
1008
+ fill_size,
1009
+ ob_event,
1010
+ source="orderbook_depth",
1011
+ )
1012
+ if not order.alive() or not self.get_param("allow_partial"):
1013
+ matched.append(order)
1014
+ continue
1015
+ if (
1016
+ front_trade_timestamp_ns is not None
1017
+ and front_trade_persisted_depth
1018
+ ):
1019
+ if (
1020
+ remainder_confirmed_timestamp_ns is not None
1021
+ and event_timestamp_ns
1022
+ == int(remainder_confirmed_timestamp_ns)
1023
+ ):
1024
+ fill_size = self._get_remaining_size(order)
1025
+ if fill_size > 0:
1026
+ self._execute(
1027
+ order,
1028
+ float(order.price),
1029
+ fill_size,
1030
+ ob_event,
1031
+ source="orderbook_depth",
1032
+ )
1033
+ if not order.alive() or not self.get_param(
1034
+ "allow_partial"
1035
+ ):
1036
+ matched.append(order)
1037
+ continue
1038
+ order._queue_depleted_move_away_timestamp_ns = None
1039
+ continue
1040
+ if moved_away_timestamp_ns is None:
1041
+ if (
1042
+ front_trade_timestamp_ns is not None
1043
+ and event_timestamp_ns == int(front_trade_timestamp_ns)
1044
+ and not front_trade_persisted_depth
1045
+ ):
1046
+ fill_size = self._get_remaining_size(order)
1047
+ if fill_size > 0:
1048
+ self._execute(
1049
+ order,
1050
+ float(order.price),
1051
+ fill_size,
1052
+ ob_event,
1053
+ source="orderbook_depth",
1054
+ )
1055
+ if not order.alive() or not self.get_param(
1056
+ "allow_partial"
1057
+ ):
1058
+ matched.append(order)
1059
+ continue
1060
+ order._queue_depleted_move_away_timestamp_ns = (
1061
+ event_timestamp_ns
1062
+ )
1063
+ continue
1064
+ if event_timestamp_ns <= int(moved_away_timestamp_ns):
1065
+ continue
1066
+ fill_size = self._get_remaining_size(order)
1067
+ if fill_size > 0:
1068
+ self._execute(
1069
+ order,
1070
+ float(order.price),
1071
+ fill_size,
1072
+ ob_event,
1073
+ source="orderbook_depth",
1074
+ )
1075
+ if not order.alive() or not self.get_param("allow_partial"):
1076
+ matched.append(order)
1077
+ continue
1078
+ order._queue_depleted_move_away_timestamp_ns = None
1079
+ continue
1080
+ else:
1081
+ same_side_moved_away = not ob_event.asks or float(
1082
+ ob_event.asks[0][0]
1083
+ ) > float(order.price)
1084
+ front_trade_timestamp_ns = getattr(
1085
+ order, "_queue_front_trade_timestamp_ns", None
1086
+ )
1087
+ if (
1088
+ front_trade_timestamp_ns is not None
1089
+ and event_timestamp_ns == int(front_trade_timestamp_ns)
1090
+ and not same_side_moved_away
1091
+ ):
1092
+ order._queue_front_trade_persisted_depth = True
1093
+ if (
1094
+ ob_event.asks
1095
+ and float(ob_event.asks[0][0]) == float(order.price)
1096
+ and abs(
1097
+ float(ob_event.asks[0][1])
1098
+ - float(getattr(order, "_queue_ahead", 0.0))
1099
+ )
1100
+ <= 1e-12
1101
+ and float(getattr(order, "_queue_ahead", 0.0)) > 1e-12
1102
+ ):
1103
+ order._queue_front_trade_remainder_confirmed_timestamp_ns = (
1104
+ event_timestamp_ns
1105
+ )
1106
+ if not same_side_moved_away:
1107
+ if (
1108
+ float(getattr(order, "_queue_trade_qty_before_depth_update", 0.0))
1109
+ > 1e-12
1110
+ and ob_event.asks
1111
+ and float(ob_event.asks[0][0]) == float(order.price)
1112
+ and abs(
1113
+ float(ob_event.asks[0][1])
1114
+ - float(getattr(order, "_queue_ahead", 0.0))
1115
+ )
1116
+ <= 1e-12
1117
+ and float(getattr(order, "_queue_ahead", 0.0)) > 1e-12
1118
+ ):
1119
+ order._queue_trade_remainder_confirmed_timestamp_ns = (
1120
+ event_timestamp_ns
1121
+ )
1122
+ if not ob_event.bids or float(ob_event.bids[0][0]) <= float(order.price):
1123
+ if queue_tracked and same_side_moved_away:
1124
+ moved_away_timestamp_ns = getattr(
1125
+ order, "_queue_depleted_move_away_timestamp_ns", None
1126
+ )
1127
+ front_trade_timestamp_ns = getattr(
1128
+ order, "_queue_front_trade_timestamp_ns", None
1129
+ )
1130
+ front_trade_persisted_depth = bool(
1131
+ getattr(order, "_queue_front_trade_persisted_depth", False)
1132
+ )
1133
+ remainder_confirmed_timestamp_ns = getattr(
1134
+ order,
1135
+ "_queue_front_trade_remainder_confirmed_timestamp_ns",
1136
+ None,
1137
+ )
1138
+ trade_remainder_confirmed_timestamp_ns = getattr(
1139
+ order, "_queue_trade_remainder_confirmed_timestamp_ns", None
1140
+ )
1141
+ result = self._try_match_orderbook(order, ob_event)
1142
+ if (
1143
+ result is not None
1144
+ and moved_away_timestamp_ns is not None
1145
+ and event_timestamp_ns == int(moved_away_timestamp_ns)
1146
+ ):
1147
+ fill_price, fill_size = result
1148
+ if fill_size > 0:
1149
+ self._execute(
1150
+ order,
1151
+ float(order.price),
1152
+ fill_size,
1153
+ ob_event,
1154
+ source="orderbook_depth",
1155
+ )
1156
+ if not order.alive() or not self.get_param("allow_partial"):
1157
+ matched.append(order)
1158
+ continue
1159
+ if (
1160
+ trade_remainder_confirmed_timestamp_ns is not None
1161
+ and event_timestamp_ns
1162
+ == int(trade_remainder_confirmed_timestamp_ns)
1163
+ ):
1164
+ fill_size = self._get_remaining_size(order)
1165
+ if fill_size > 0:
1166
+ self._execute(
1167
+ order,
1168
+ float(order.price),
1169
+ fill_size,
1170
+ ob_event,
1171
+ source="orderbook_depth",
1172
+ )
1173
+ if not order.alive() or not self.get_param("allow_partial"):
1174
+ matched.append(order)
1175
+ continue
1176
+ if (
1177
+ front_trade_timestamp_ns is not None
1178
+ and front_trade_persisted_depth
1179
+ ):
1180
+ if (
1181
+ remainder_confirmed_timestamp_ns is not None
1182
+ and event_timestamp_ns
1183
+ == int(remainder_confirmed_timestamp_ns)
1184
+ ):
1185
+ fill_size = self._get_remaining_size(order)
1186
+ if fill_size > 0:
1187
+ self._execute(
1188
+ order,
1189
+ float(order.price),
1190
+ fill_size,
1191
+ ob_event,
1192
+ source="orderbook_depth",
1193
+ )
1194
+ if not order.alive() or not self.get_param(
1195
+ "allow_partial"
1196
+ ):
1197
+ matched.append(order)
1198
+ continue
1199
+ order._queue_depleted_move_away_timestamp_ns = None
1200
+ continue
1201
+ if moved_away_timestamp_ns is None:
1202
+ if (
1203
+ front_trade_timestamp_ns is not None
1204
+ and event_timestamp_ns == int(front_trade_timestamp_ns)
1205
+ and not front_trade_persisted_depth
1206
+ ):
1207
+ fill_size = self._get_remaining_size(order)
1208
+ if fill_size > 0:
1209
+ self._execute(
1210
+ order,
1211
+ float(order.price),
1212
+ fill_size,
1213
+ ob_event,
1214
+ source="orderbook_depth",
1215
+ )
1216
+ if not order.alive() or not self.get_param(
1217
+ "allow_partial"
1218
+ ):
1219
+ matched.append(order)
1220
+ continue
1221
+ order._queue_depleted_move_away_timestamp_ns = (
1222
+ event_timestamp_ns
1223
+ )
1224
+ continue
1225
+ if event_timestamp_ns <= int(moved_away_timestamp_ns):
1226
+ continue
1227
+ fill_size = self._get_remaining_size(order)
1228
+ if fill_size > 0:
1229
+ self._execute(
1230
+ order,
1231
+ float(order.price),
1232
+ fill_size,
1233
+ ob_event,
1234
+ source="orderbook_depth",
1235
+ )
1236
+ if not order.alive() or not self.get_param("allow_partial"):
1237
+ matched.append(order)
1238
+ continue
1239
+ order._queue_depleted_move_away_timestamp_ns = None
1240
+ continue
1241
+ order._queue_depleted_move_away_timestamp_ns = None
1242
+
1243
+ result = self._try_match_orderbook(order, ob_event)
1244
+ if result is None:
1245
+ continue
1246
+
1247
+ fill_price, fill_size = result
1248
+ if fill_size <= 0:
1249
+ continue
1250
+
1251
+ if getattr(order, "_fill_role", None) == FillRole.MAKER:
1252
+ fill_price = float(order.price)
1253
+
1254
+ self._execute(order, fill_price, fill_size, ob_event, source="orderbook_depth")
1255
+ if not order.alive() or not self.get_param("allow_partial"):
1256
+ matched.append(order)
1257
+
1258
+ for order in matched:
1259
+ self._remove_pending_order(order)
1260
+
1261
+ # An IOC which could not cross the book must not become a resting
1262
+ # maker order and fill on a later snapshot.
1263
+ for order in active_orders:
1264
+ self._cancel_ioc_remainder(order, ob_event, source="orderbook_depth")
1265
+
1266
+ def _cancel_ioc_remainder(self, order, event, source):
1267
+ """Finish an IOC after its first matching opportunity, including zero fill."""
1268
+ if getattr(order, "time_in_force", "GTC") != "IOC" or not order.alive():
1269
+ return False
1270
+ self._cancel_remainder(order, event, source, "IOC_REMAINDER_CANCELLED")
1271
+ return True
1272
+
1273
+ def _cancel_remainder(self, order, event, source, reason):
1274
+ order.addinfo(cancel_reason=reason)
1275
+ order.cancel()
1276
+ self.notify(order)
1277
+ self._remove_pending_order(order)
1278
+ self._order_history.append(
1279
+ {
1280
+ "timestamp": event.timestamp,
1281
+ "symbol": self._get_data_name(order.data),
1282
+ "side": "buy" if order.isbuy() else "sell",
1283
+ "status": "canceled",
1284
+ "reason": reason,
1285
+ "source": source,
1286
+ }
1287
+ )
1288
+
1289
+ def _try_match(self, order, tick):
1290
+ """Try to match an order against a tick.
1291
+
1292
+ Args:
1293
+ order: The order to match.
1294
+ tick: The current TickEvent.
1295
+
1296
+ Returns:
1297
+ Tuple of (fill_price, fill_size) if matched, None otherwise.
1298
+ """
1299
+ exectype = order.exectype
1300
+ price = tick.price
1301
+ size = self._get_remaining_size(order)
1302
+
1303
+ if exectype == Order.Market:
1304
+ fill_price = self._apply_slippage(price, order.isbuy())
1305
+ return (fill_price, abs(size))
1306
+
1307
+ if exectype == Order.Limit:
1308
+ limit_price = order.price
1309
+ if order.isbuy():
1310
+ if price <= limit_price:
1311
+ return (min(price, limit_price), abs(size))
1312
+ else:
1313
+ if price >= limit_price:
1314
+ return (max(price, limit_price), abs(size))
1315
+
1316
+ elif exectype == Order.Stop:
1317
+ stop_price = order.price
1318
+ if order.isbuy():
1319
+ if price >= stop_price:
1320
+ fill_price = self._apply_slippage(price, True)
1321
+ return (fill_price, abs(size))
1322
+ else:
1323
+ if price <= stop_price:
1324
+ fill_price = self._apply_slippage(price, False)
1325
+ return (fill_price, abs(size))
1326
+
1327
+ elif exectype == Order.StopLimit:
1328
+ stop_price = order.price
1329
+ limit_price = order.pricelimit
1330
+
1331
+ if not getattr(order, "_stop_triggered", False):
1332
+ if (
1333
+ order.isbuy()
1334
+ and price >= stop_price
1335
+ or not order.isbuy()
1336
+ and price <= stop_price
1337
+ ):
1338
+ order._stop_triggered = True
1339
+
1340
+ if getattr(order, "_stop_triggered", False):
1341
+ if order.isbuy():
1342
+ if price <= limit_price:
1343
+ return (min(price, limit_price), abs(size))
1344
+ else:
1345
+ if price >= limit_price:
1346
+ return (max(price, limit_price), abs(size))
1347
+
1348
+ return None
1349
+
1350
+ def _try_match_orderbook(self, order, ob_event):
1351
+ """Try to match an order against order book depth levels.
1352
+
1353
+ Args:
1354
+ order: The order to match.
1355
+ ob_event: The current OrderBookSnapshot.
1356
+
1357
+ Returns:
1358
+ Tuple of (avg_fill_price, fill_size) or None.
1359
+ """
1360
+ exectype = order.exectype
1361
+ target_size = self._get_matching_size(order)
1362
+ max_levels = self.get_param("max_depth_levels")
1363
+
1364
+ if exectype == Order.Market:
1365
+ if order.isbuy():
1366
+ return self._match_buy_orderbook(ob_event.asks, target_size, max_levels, None)
1367
+ return self._match_sell_orderbook(ob_event.bids, target_size, max_levels, None)
1368
+
1369
+ if exectype == Order.Limit:
1370
+ limit_price = order.price
1371
+ if order.isbuy():
1372
+ if ob_event.asks and ob_event.asks[0][0] <= limit_price:
1373
+ return self._match_buy_orderbook(
1374
+ ob_event.asks, target_size, max_levels, limit_price
1375
+ )
1376
+ elif ob_event.bids and ob_event.bids[0][0] >= limit_price:
1377
+ return self._match_sell_orderbook(
1378
+ ob_event.bids, target_size, max_levels, limit_price
1379
+ )
1380
+
1381
+ if exectype == Order.Stop:
1382
+ stop_price = order.price
1383
+ if order.isbuy():
1384
+ if ob_event.asks and ob_event.asks[0][0] >= stop_price:
1385
+ return self._match_buy_orderbook(ob_event.asks, target_size, max_levels, None)
1386
+ elif ob_event.bids and ob_event.bids[0][0] <= stop_price:
1387
+ return self._match_sell_orderbook(ob_event.bids, target_size, max_levels, None)
1388
+
1389
+ return None
1390
+
1391
+ def _match_buy_orderbook(self, asks, target_size, max_levels, limit_price):
1392
+ """Match a buy order against ask depth."""
1393
+ total_filled = 0.0
1394
+ total_cost = 0.0
1395
+
1396
+ for level_index, (price, qty) in enumerate(asks):
1397
+ if level_index >= max_levels:
1398
+ break
1399
+ if limit_price is not None and price > limit_price:
1400
+ break
1401
+
1402
+ remaining = target_size - total_filled
1403
+ fill_at_level = min(qty, remaining)
1404
+
1405
+ if self.get_param("enable_impact") and self._impact_model:
1406
+ price = self._apply_market_impact(price, fill_at_level, is_buy=True)
1407
+
1408
+ total_cost += price * fill_at_level
1409
+ total_filled += fill_at_level
1410
+ if total_filled >= target_size:
1411
+ break
1412
+
1413
+ if total_filled <= 0:
1414
+ return None
1415
+
1416
+ return (total_cost / total_filled, total_filled)
1417
+
1418
+ def _match_sell_orderbook(self, bids, target_size, max_levels, limit_price):
1419
+ """Match a sell order against bid depth."""
1420
+ total_filled = 0.0
1421
+ total_revenue = 0.0
1422
+
1423
+ for level_index, (price, qty) in enumerate(bids):
1424
+ if level_index >= max_levels:
1425
+ break
1426
+ if limit_price is not None and price < limit_price:
1427
+ break
1428
+
1429
+ remaining = target_size - total_filled
1430
+ fill_at_level = min(qty, remaining)
1431
+
1432
+ if self.get_param("enable_impact") and self._impact_model:
1433
+ price = self._apply_market_impact(price, fill_at_level, is_buy=False)
1434
+
1435
+ total_revenue += price * fill_at_level
1436
+ total_filled += fill_at_level
1437
+ if total_filled >= target_size:
1438
+ break
1439
+
1440
+ if total_filled <= 0:
1441
+ return None
1442
+
1443
+ return (total_revenue / total_filled, total_filled)
1444
+
1445
+ def _apply_slippage(self, price, is_buy):
1446
+ """Apply slippage to a fill price.
1447
+
1448
+ Args:
1449
+ price: Base execution price.
1450
+ is_buy: True for buy orders, False for sell.
1451
+
1452
+ Returns:
1453
+ Price with slippage applied.
1454
+ """
1455
+ perc = self.get_param("slippage_perc")
1456
+ fixed = self.get_param("slippage_fixed")
1457
+
1458
+ slip = price * perc + fixed
1459
+ if is_buy:
1460
+ return price + slip
1461
+ return price - slip
1462
+
1463
+ def _apply_market_impact(self, price, size, is_buy):
1464
+ """Apply a market impact model if enabled."""
1465
+ if self._impact_model is None:
1466
+ return price
1467
+
1468
+ impact = self._impact_model.calculate_impact(price, size)
1469
+ if is_buy:
1470
+ return price + impact
1471
+ return price - impact
1472
+
1473
+ @staticmethod
1474
+ def _aggregate_exchange_fills(fills, max_size=None):
1475
+ total_size = 0.0
1476
+ total_value = 0.0
1477
+ for price, size, _role in fills:
1478
+ if max_size is not None:
1479
+ size = min(size, max_size - total_size)
1480
+ if size <= 0:
1481
+ break
1482
+ total_value += price * size
1483
+ total_size += size
1484
+ if total_size <= 0.0:
1485
+ return (0.0, 0.0)
1486
+ return (total_value / total_size, total_size)
1487
+
1488
+ def _get_matching_size(self, order):
1489
+ """Cap depth traversal before calculating VWAP for a reduce-only order."""
1490
+ remaining = self._get_remaining_size(order)
1491
+ if not getattr(order, "reduce_only", False):
1492
+ return remaining
1493
+ data_name = self._get_data_name(order.data)
1494
+ if self._is_dual_side_mode():
1495
+ side = normalize_position_side(getattr(order.info, "position_side", None))
1496
+ position = self._make_signed_position(side, self._get_leg_position(data_name, side))
1497
+ else:
1498
+ position = self._positions[data_name]
1499
+ if position.size and (position.size > 0) != order.isbuy():
1500
+ return min(remaining, abs(position.size))
1501
+ return remaining # _execute rejects fills which cannot reduce a position.
1502
+
1503
+ @staticmethod
1504
+ def _resolve_commission_role(source):
1505
+ if source in {"maker", "taker"}:
1506
+ return source
1507
+ return "taker"
1508
+
1509
+ def _execute(self, order, fill_price, fill_size, event, source="tick"):
1510
+ """Execute a fill on an order.
1511
+
1512
+ Args:
1513
+ order: The order being filled.
1514
+ fill_price: The execution price.
1515
+ fill_size: The execution size.
1516
+ event: The event that triggered the fill.
1517
+ source: Source tag for order history.
1518
+ """
1519
+ if not order.alive():
1520
+ return None
1521
+ fill_size = min(float(fill_size), self._get_remaining_size(order))
1522
+ if fill_size <= 1e-12:
1523
+ return None
1524
+ reduce_only = bool(getattr(order, "reduce_only", False))
1525
+ if reduce_only:
1526
+ data_name = self._get_data_name(order.data)
1527
+ if self._is_dual_side_mode():
1528
+ side = normalize_position_side(getattr(order.info, "position_side", None))
1529
+ current = self._make_signed_position(side, self._get_leg_position(data_name, side))
1530
+ else:
1531
+ current = self._positions[data_name]
1532
+ # Recheck at fill time: other pending reduce-only orders may
1533
+ # already have consumed this position since submission.
1534
+ if not current.size or (current.size > 0) == order.isbuy():
1535
+ self._cancel_remainder(order, event, source, "REDUCE_ONLY_NO_POSITION")
1536
+ return None
1537
+ fill_size = min(fill_size, abs(current.size))
1538
+ if self._is_dual_side_mode():
1539
+ return self._execute_dual_side(order, fill_price, fill_size, event, source=source)
1540
+ data_name = self._get_data_name(order.data)
1541
+ position = self._positions[data_name]
1542
+ exec_size = fill_size if order.isbuy() else -fill_size
1543
+ comminfo = self.getcommissioninfo(order.data)
1544
+ commission_role = self._resolve_commission_role(source)
1545
+ pprice_orig = position.price
1546
+ psize, pprice, opened, closed = position.pseudoupdate(exec_size, fill_price)
1547
+ pnl = comminfo.profitandloss(-closed, pprice_orig, fill_price) if closed else 0.0
1548
+
1549
+ cash = self._cash
1550
+ if closed:
1551
+ if self.get_param("shortcash"):
1552
+ closedvalue = comminfo.getvaluesize(-closed, pprice_orig)
1553
+ else:
1554
+ closedvalue = comminfo.getoperationcost(closed, pprice_orig)
1555
+
1556
+ closecash = closedvalue
1557
+ if closedvalue > 0:
1558
+ closecash /= comminfo.get_leverage()
1559
+ cash += closecash + pnl * comminfo.stocklike
1560
+ closedcomm = comminfo.getcommission(closed, fill_price, role=commission_role)
1561
+ cash -= closedcomm
1562
+ if position.adjbase is not None:
1563
+ cash += comminfo.cashadjust(-closed, position.adjbase, fill_price)
1564
+ else:
1565
+ closedvalue = 0.0
1566
+ closedcomm = 0.0
1567
+
1568
+ popened = opened
1569
+ if opened:
1570
+ if self.get_param("shortcash"):
1571
+ openedvalue = comminfo.getvaluesize(opened, fill_price)
1572
+ else:
1573
+ openedvalue = comminfo.getoperationcost(opened, fill_price)
1574
+
1575
+ opencash = openedvalue
1576
+ if openedvalue > 0:
1577
+ opencash /= comminfo.get_leverage()
1578
+ cash -= opencash
1579
+ openedcomm = comminfo.getcommission(opened, fill_price, role=commission_role)
1580
+ cash -= openedcomm
1581
+
1582
+ if cash < 0.0:
1583
+ opened = 0
1584
+ openedvalue = 0.0
1585
+ openedcomm = 0.0
1586
+ else:
1587
+ if abs(psize) > abs(opened) and position.adjbase is not None:
1588
+ adjsize = psize - opened
1589
+ cash += comminfo.cashadjust(adjsize, position.adjbase, fill_price)
1590
+ position.adjbase = fill_price
1591
+ else:
1592
+ openedvalue = 0.0
1593
+ openedcomm = 0.0
1594
+
1595
+ self._cash = cash
1596
+ executed_size = closed + opened
1597
+ if not executed_size:
1598
+ if popened and not opened:
1599
+ order.margin()
1600
+ self.notify(order)
1601
+ return None
1602
+
1603
+ comminfo.confirmexec(executed_size, fill_price, role=commission_role)
1604
+ position.update(executed_size, fill_price, event.timestamp)
1605
+ order.execute(
1606
+ dt=event.timestamp,
1607
+ size=executed_size,
1608
+ price=fill_price,
1609
+ closed=closed,
1610
+ closedvalue=closedvalue,
1611
+ closedcomm=closedcomm,
1612
+ opened=opened,
1613
+ openedvalue=openedvalue,
1614
+ openedcomm=openedcomm,
1615
+ margin=comminfo.margin,
1616
+ pnl=pnl,
1617
+ psize=psize,
1618
+ pprice=pprice,
1619
+ )
1620
+ if self._get_remaining_size(order) <= 1e-12:
1621
+ order.executed.remsize = 0.0
1622
+ order.completed()
1623
+ order.addcomminfo(comminfo)
1624
+ self.notify(order)
1625
+ self._state_tracker.on_fill(
1626
+ data_name,
1627
+ fill_price,
1628
+ executed_size,
1629
+ closedcomm + openedcomm,
1630
+ role=source,
1631
+ )
1632
+
1633
+ self._order_history.append(
1634
+ {
1635
+ "timestamp": event.timestamp,
1636
+ "timestamp_ns": getattr(
1637
+ event, "timestamp_ns", int(round(float(event.timestamp) * 1_000_000_000.0))
1638
+ ),
1639
+ "symbol": data_name,
1640
+ "side": "buy" if order.isbuy() else "sell",
1641
+ "status": order.getstatusname(),
1642
+ "price": fill_price,
1643
+ "size": abs(executed_size),
1644
+ "opened": opened,
1645
+ "closed": closed,
1646
+ "pnl": pnl,
1647
+ "commission": closedcomm + openedcomm,
1648
+ "source": source,
1649
+ "role": commission_role,
1650
+ "reference_price": getattr(event, "price", None),
1651
+ "order_ref": getattr(order, "ref", None),
1652
+ }
1653
+ )
1654
+
1655
+ self._recorder.record(event.timestamp, data_name, self._order_history[-1])
1656
+
1657
+ if reduce_only and abs(position.size) <= 1e-12 and order.alive():
1658
+ self._cancel_remainder(order, event, source, "POSITION_DEPLETED")
1659
+
1660
+ if popened and not opened:
1661
+ order.margin()
1662
+ self.notify(order)
1663
+
1664
+ def _execute_dual_side(self, order, fill_price, fill_size, event, source="tick"):
1665
+ data_name = self._get_data_name(order.data)
1666
+ position_side = normalize_position_side(getattr(order.info, "position_side", None))
1667
+ leg_position = self._get_leg_position(data_name, position_side)
1668
+ signed_position = self._make_signed_position(position_side, leg_position)
1669
+ exec_size = fill_size if order.isbuy() else -fill_size
1670
+ offset = getattr(order.info, "offset", None)
1671
+
1672
+ if offset in {"close", "close_today", "close_yesterday"}:
1673
+ available = abs(float(signed_position.size or 0.0))
1674
+ if available <= 1e-12:
1675
+ order.reject()
1676
+ self.notify(order)
1677
+ self._remove_pending_order(order)
1678
+ return
1679
+ if abs(float(exec_size or 0.0)) > available + 1e-12:
1680
+ exec_size = available if order.isbuy() else -available
1681
+
1682
+ comminfo = self.getcommissioninfo(order.data)
1683
+ commission_role = self._resolve_commission_role(source)
1684
+ pprice_orig = signed_position.price
1685
+ psize, pprice, opened, closed = signed_position.pseudoupdate(exec_size, fill_price)
1686
+ pnl = comminfo.profitandloss(-closed, pprice_orig, fill_price) if closed else 0.0
1687
+
1688
+ cash = self._cash
1689
+ if closed:
1690
+ if self.get_param("shortcash"):
1691
+ closedvalue = comminfo.getvaluesize(-closed, pprice_orig)
1692
+ else:
1693
+ closedvalue = comminfo.getoperationcost(closed, pprice_orig)
1694
+
1695
+ closecash = closedvalue
1696
+ if closedvalue > 0:
1697
+ closecash /= comminfo.get_leverage()
1698
+ cash += closecash + pnl * comminfo.stocklike
1699
+ closedcomm = comminfo.getcommission(closed, fill_price, role=commission_role)
1700
+ cash -= closedcomm
1701
+ if signed_position.adjbase is not None:
1702
+ cash += comminfo.cashadjust(-closed, signed_position.adjbase, fill_price)
1703
+ else:
1704
+ closedvalue = 0.0
1705
+ closedcomm = 0.0
1706
+
1707
+ popened = opened
1708
+ if opened:
1709
+ if self.get_param("shortcash"):
1710
+ openedvalue = comminfo.getvaluesize(opened, fill_price)
1711
+ else:
1712
+ openedvalue = comminfo.getoperationcost(opened, fill_price)
1713
+
1714
+ opencash = openedvalue
1715
+ if openedvalue > 0:
1716
+ opencash /= comminfo.get_leverage()
1717
+ cash -= opencash
1718
+ openedcomm = comminfo.getcommission(opened, fill_price, role=commission_role)
1719
+ cash -= openedcomm
1720
+
1721
+ if cash < 0.0:
1722
+ opened = 0
1723
+ openedvalue = 0.0
1724
+ openedcomm = 0.0
1725
+ else:
1726
+ if abs(psize) > abs(opened) and signed_position.adjbase is not None:
1727
+ adjsize = psize - opened
1728
+ cash += comminfo.cashadjust(adjsize, signed_position.adjbase, fill_price)
1729
+ signed_position.adjbase = fill_price
1730
+ else:
1731
+ openedvalue = 0.0
1732
+ openedcomm = 0.0
1733
+
1734
+ self._cash = cash
1735
+ executed_size = closed + opened
1736
+ if not executed_size:
1737
+ if popened and not opened:
1738
+ order.margin()
1739
+ self.notify(order)
1740
+ return
1741
+
1742
+ comminfo.confirmexec(executed_size, fill_price, role=commission_role)
1743
+ signed_position.update(executed_size, fill_price, event.timestamp)
1744
+ self._apply_signed_position(position_side, leg_position, signed_position)
1745
+ self._sync_net_position(data_name)
1746
+ order.execute(
1747
+ dt=event.timestamp,
1748
+ size=executed_size,
1749
+ price=fill_price,
1750
+ closed=closed,
1751
+ closedvalue=closedvalue,
1752
+ closedcomm=closedcomm,
1753
+ opened=opened,
1754
+ openedvalue=openedvalue,
1755
+ openedcomm=openedcomm,
1756
+ margin=comminfo.margin,
1757
+ pnl=pnl,
1758
+ psize=psize,
1759
+ pprice=pprice,
1760
+ )
1761
+ if self._get_remaining_size(order) <= 1e-12:
1762
+ order.executed.remsize = 0.0
1763
+ order.completed()
1764
+ order.addcomminfo(comminfo)
1765
+ self.notify(order)
1766
+ self._state_tracker.on_fill(
1767
+ data_name,
1768
+ fill_price,
1769
+ executed_size,
1770
+ closedcomm + openedcomm,
1771
+ role=source,
1772
+ )
1773
+
1774
+ self._order_history.append(
1775
+ {
1776
+ "timestamp": event.timestamp,
1777
+ "timestamp_ns": getattr(
1778
+ event,
1779
+ "timestamp_ns",
1780
+ int(round(float(event.timestamp) * 1_000_000_000.0)),
1781
+ ),
1782
+ "symbol": data_name,
1783
+ "side": "buy" if order.isbuy() else "sell",
1784
+ "position_side": position_side,
1785
+ "offset": offset,
1786
+ "status": order.getstatusname(),
1787
+ "price": fill_price,
1788
+ "size": abs(executed_size),
1789
+ "opened": opened,
1790
+ "closed": closed,
1791
+ "pnl": pnl,
1792
+ "commission": closedcomm + openedcomm,
1793
+ "source": source,
1794
+ "role": commission_role,
1795
+ "reference_price": getattr(event, "price", None),
1796
+ "order_ref": getattr(order, "ref", None),
1797
+ }
1798
+ )
1799
+
1800
+ self._recorder.record(event.timestamp, data_name, self._order_history[-1])
1801
+
1802
+ if (
1803
+ (
1804
+ offset in {"close", "close_today", "close_yesterday"}
1805
+ or getattr(order, "reduce_only", False)
1806
+ )
1807
+ and abs(self._get_leg_position(data_name, position_side).size) <= 1e-12
1808
+ and order.alive()
1809
+ ):
1810
+ order.cancel()
1811
+ order.addinfo(cancel_reason="POSITION_DEPLETED")
1812
+ self.notify(order)
1813
+
1814
+ if popened and not opened:
1815
+ order.margin()
1816
+ self.notify(order)
1817
+
1818
+ @staticmethod
1819
+ def _get_remaining_size(order):
1820
+ """Return remaining absolute size for an order."""
1821
+ executed = getattr(order, "executed", None)
1822
+ remaining = getattr(executed, "remsize", None)
1823
+ if remaining is None:
1824
+ remaining = order.size
1825
+ unfilled = max(0.0, abs(order.size) - abs(getattr(executed, "size", 0.0)))
1826
+ return min(abs(remaining), unfilled)
1827
+
1828
+ def next(self):
1829
+ """Called by Cerebro on each iteration.
1830
+
1831
+ This is a no-op in tick mode since order matching happens via
1832
+ process_tick() instead. Provided for compatibility with bar mode.
1833
+ """
1834
+
1835
+ def add_order_history(self, orders, notify=False):
1836
+ """Add historical orders to the broker.
1837
+
1838
+ Allows preloading order history for replay scenarios.
1839
+
1840
+ Args:
1841
+ orders: Iterable of Order instances to add.
1842
+ notify: Whether to trigger notifications for added orders.
1843
+ """
1844
+
1845
+ def set_fund_history(self, fund):
1846
+ """Set historical fund data for replay scenarios.
1847
+
1848
+ Args:
1849
+ fund: Historical fund value data.
1850
+ """
1851
+
1852
+ @property
1853
+ def pending_orders(self):
1854
+ """List of currently pending orders."""
1855
+ return list(self._pending_orders)
1856
+
1857
+ def state_values(self, data=None):
1858
+ """Return aggregated state values for one data feed or all symbols."""
1859
+ if data is not None:
1860
+ symbol = self._get_data_name(data)
1861
+ mid_price = getattr(self._last_tick.get(symbol), "price", None)
1862
+ return self._state_tracker.snapshot(
1863
+ symbol,
1864
+ self._positions[symbol].size,
1865
+ self._cash,
1866
+ mid_price,
1867
+ )
1868
+
1869
+ positions = {symbol: pos.size for symbol, pos in self._positions.items()}
1870
+ mid_prices = {
1871
+ symbol: getattr(self._last_tick.get(symbol), "price", None)
1872
+ for symbol in set(self._state_tracker._states) | set(self._positions)
1873
+ }
1874
+ balances = dict.fromkeys(mid_prices, self._cash)
1875
+ return self._state_tracker.snapshot_all(positions, balances, mid_prices)
1876
+
1877
+ @property
1878
+ def order_history(self):
1879
+ """Complete order execution history."""
1880
+ return list(self._order_history)
1881
+
1882
+ @property
1883
+ def tick_count(self):
1884
+ """Number of ticks processed."""
1885
+ return self._tick_count
1886
+
1887
+ def get_last_tick(self, symbol=None):
1888
+ """Return the latest processed tick for a symbol.
1889
+
1890
+ Args:
1891
+ symbol: Symbol name. If omitted, return the first cached tick.
1892
+
1893
+ Returns:
1894
+ TickEvent or None.
1895
+ """
1896
+ if symbol is not None:
1897
+ return self._last_tick.get(str(symbol))
1898
+ if self._last_tick:
1899
+ return next(iter(self._last_tick.values()))
1900
+ return None
1901
+
1902
+ def get_last_orderbook(self, symbol=None):
1903
+ """Return the latest processed order book snapshot for a symbol.
1904
+
1905
+ Args:
1906
+ symbol: Symbol name. If omitted, return the first cached snapshot.
1907
+
1908
+ Returns:
1909
+ OrderBookSnapshot or None.
1910
+ """
1911
+ if symbol is not None:
1912
+ return self._last_orderbook.get(str(symbol))
1913
+ if self._last_orderbook:
1914
+ return next(iter(self._last_orderbook.values()))
1915
+ return None
1916
+
1917
+ def _get_data_name(self, data):
1918
+ return getattr(data, "_name", None) or getattr(data, "symbol", str(data))
1919
+
1920
+ @staticmethod
1921
+ def _event_timestamp_ns(event):
1922
+ return int(
1923
+ getattr(
1924
+ event,
1925
+ "timestamp_ns",
1926
+ int(round(float(getattr(event, "timestamp", 0.0)) * 1_000_000_000.0)),
1927
+ )
1928
+ )
1929
+
1930
+ def _order_is_active_for_event(self, order, event):
1931
+ active_after_ts = getattr(order, "_active_after_timestamp_ns", None)
1932
+ if active_after_ts is None:
1933
+ return True
1934
+ return self._event_timestamp_ns(event) > int(active_after_ts)
1935
+
1936
+ def _order_is_queue_active_for_event(self, order, event):
1937
+ active_after_seq = getattr(order, "_active_after_event_seq", None)
1938
+ event_seq = getattr(event, "event_seq", None)
1939
+ if active_after_seq is not None and event_seq is not None:
1940
+ return int(event_seq) > int(active_after_seq)
1941
+ active_after_ts = getattr(order, "_active_after_timestamp_ns", None)
1942
+ if active_after_ts is None:
1943
+ return True
1944
+ return self._event_timestamp_ns(event) > int(active_after_ts)
1945
+
1946
+ def _queue_pending_order(self, order):
1947
+ if order not in self._pending_orders:
1948
+ self._pending_orders.append(order)
1949
+ data_name = self._get_data_name(order.data)
1950
+ if order not in self._orders_by_symbol[data_name]:
1951
+ self._orders_by_symbol[data_name].append(order)
1952
+
1953
+ def _remove_pending_order(self, order):
1954
+ try:
1955
+ self._pending_orders.remove(order)
1956
+ except ValueError:
1957
+ # Order already removed from the pending list; idempotent removal.
1958
+ logger.debug("tickbroker:1958 ignored ValueError")
1959
+
1960
+ data_name = self._get_data_name(order.data)
1961
+ bucket = self._orders_by_symbol.get(data_name)
1962
+ if bucket is not None:
1963
+ try:
1964
+ bucket.remove(order)
1965
+ except ValueError:
1966
+ # Order not in this symbol bucket; idempotent removal.
1967
+ logger.debug("tickbroker:1967 ignored ValueError")
1968
+ if not bucket:
1969
+ del self._orders_by_symbol[data_name]
1970
+
1971
+ self._matching_core.remove_order(order)
1972
+
1973
+ def _activate_visible_orders(self, current_ts):
1974
+ for order in self._matching_core.activate_orders(current_ts):
1975
+ self._queue_pending_order(order)
1976
+
1977
+ @property
1978
+ def recorder(self):
1979
+ """Return the :class:`Recorder` instance owned by the broker.
1980
+
1981
+ The recorder captures every per-symbol event the matching core
1982
+ produces (orders, trades, cancels) and is exposed here so that
1983
+ callers and tests can introspect the most recent activity without
1984
+ having to instrument the matching core directly.
1985
+
1986
+ Returns:
1987
+ Recorder: The broker's recorder. Always non-``None``: if
1988
+ no ``recorder_factory`` was provided to the broker, a
1989
+ default :class:`Recorder` is created during :meth:`start`.
1990
+ """
1991
+ return self._recorder