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.
- back_trader_python-1.4.0.dist-info/METADATA +1491 -0
- back_trader_python-1.4.0.dist-info/RECORD +465 -0
- back_trader_python-1.4.0.dist-info/WHEEL +5 -0
- back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
- back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
- backtrader/__init__.py +148 -0
- backtrader/_cerebro/__init__.py +5 -0
- backtrader/_cerebro/channel.py +382 -0
- backtrader/_cerebro/execution.py +377 -0
- backtrader/_cerebro/lifecycle.py +143 -0
- backtrader/_cerebro/notifications.py +150 -0
- backtrader/_cerebro/presentation.py +230 -0
- backtrader/_cerebro/registry.py +593 -0
- backtrader/_cerebro/runnext.py +551 -0
- backtrader/_cerebro/runonce.py +142 -0
- backtrader/analyzer.py +594 -0
- backtrader/analyzers/__init__.py +50 -0
- backtrader/analyzers/annualreturn.py +226 -0
- backtrader/analyzers/calmar.py +165 -0
- backtrader/analyzers/drawdown.py +287 -0
- backtrader/analyzers/leverage.py +112 -0
- backtrader/analyzers/logreturnsrolling.py +190 -0
- backtrader/analyzers/periodstats.py +153 -0
- backtrader/analyzers/positions.py +119 -0
- backtrader/analyzers/pyfolio.py +470 -0
- backtrader/analyzers/returns.py +192 -0
- backtrader/analyzers/sharpe.py +307 -0
- backtrader/analyzers/sharpe_ratio_stats.py +534 -0
- backtrader/analyzers/sqn.py +112 -0
- backtrader/analyzers/timereturn.py +192 -0
- backtrader/analyzers/total_value.py +75 -0
- backtrader/analyzers/tradeanalyzer.py +278 -0
- backtrader/analyzers/transactions.py +141 -0
- backtrader/analyzers/vwr.py +245 -0
- backtrader/bokeh/__init__.py +155 -0
- backtrader/bokeh/analyzers/__init__.py +13 -0
- backtrader/bokeh/analyzers/plot.py +192 -0
- backtrader/bokeh/analyzers/recorder.py +181 -0
- backtrader/bokeh/app.py +1094 -0
- backtrader/bokeh/live/__init__.py +11 -0
- backtrader/bokeh/live/client.py +352 -0
- backtrader/bokeh/live/datahandler.py +346 -0
- backtrader/bokeh/plot_adapter.py +200 -0
- backtrader/bokeh/schemes/__init__.py +14 -0
- backtrader/bokeh/schemes/blackly.py +76 -0
- backtrader/bokeh/schemes/scheme.py +150 -0
- backtrader/bokeh/schemes/tradimo.py +82 -0
- backtrader/bokeh/tab.py +125 -0
- backtrader/bokeh/tabs/__init__.py +30 -0
- backtrader/bokeh/tabs/analyzer.py +120 -0
- backtrader/bokeh/tabs/config.py +154 -0
- backtrader/bokeh/tabs/live.py +109 -0
- backtrader/bokeh/tabs/log.py +185 -0
- backtrader/bokeh/tabs/metadata.py +182 -0
- backtrader/bokeh/tabs/performance.py +359 -0
- backtrader/bokeh/tabs/source.py +70 -0
- backtrader/bokeh/utils/__init__.py +8 -0
- backtrader/bokeh/utils/helpers.py +167 -0
- backtrader/bokeh/webapp.py +164 -0
- backtrader/broker.py +478 -0
- backtrader/brokers/__init__.py +36 -0
- backtrader/brokers/bbroker.py +2576 -0
- backtrader/brokers/btapibroker.py +8227 -0
- backtrader/brokers/hft/__init__.py +89 -0
- backtrader/brokers/hft/binance_bbo.py +625 -0
- backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
- backtrader/brokers/hft/examples.py +1228 -0
- backtrader/brokers/hft/exchange.py +380 -0
- backtrader/brokers/hft/latency.py +309 -0
- backtrader/brokers/hft/matching_core.py +572 -0
- backtrader/brokers/hft/queue.py +238 -0
- backtrader/brokers/hft/recorder.py +88 -0
- backtrader/brokers/hft/state.py +138 -0
- backtrader/brokers/impact_models.py +118 -0
- backtrader/brokers/mixbroker.py +895 -0
- backtrader/brokers/tickbroker.py +1991 -0
- backtrader/btrun/__init__.py +12 -0
- backtrader/btrun/btrun.py +1218 -0
- backtrader/cerebro.py +828 -0
- backtrader/channel.py +682 -0
- backtrader/channels/__init__.py +23 -0
- backtrader/channels/bridge.py +186 -0
- backtrader/channels/funding.py +248 -0
- backtrader/channels/live_queue.py +216 -0
- backtrader/channels/live_validator.py +294 -0
- backtrader/channels/orderbook.py +257 -0
- backtrader/channels/tick.py +202 -0
- backtrader/comminfo.py +665 -0
- backtrader/commissions/__init__.py +106 -0
- backtrader/commissions/ctpoption.py +993 -0
- backtrader/configs/account_config_example.yaml +8 -0
- backtrader/dataseries.py +379 -0
- backtrader/errors.py +106 -0
- backtrader/events.py +980 -0
- backtrader/feed.py +1523 -0
- backtrader/feeds/__init__.py +75 -0
- backtrader/feeds/barrier.py +2006 -0
- backtrader/feeds/blaze.py +118 -0
- backtrader/feeds/btapifeed.py +1538 -0
- backtrader/feeds/btcsv.py +203 -0
- backtrader/feeds/chainer.py +114 -0
- backtrader/feeds/cryptohftdata.py +164 -0
- backtrader/feeds/csvgeneric.py +1205 -0
- backtrader/feeds/ctpcohort.py +1051 -0
- backtrader/feeds/influxfeed.py +158 -0
- backtrader/feeds/livefeed.py +71 -0
- backtrader/feeds/mixed_channel.py +108 -0
- backtrader/feeds/mt4csv.py +42 -0
- backtrader/feeds/pandafeed.py +381 -0
- backtrader/feeds/quandl.py +256 -0
- backtrader/feeds/rollover.py +229 -0
- backtrader/feeds/sierrachart.py +30 -0
- backtrader/feeds/vchart.py +162 -0
- backtrader/feeds/vchartcsv.py +84 -0
- backtrader/feeds/vchartfile.py +153 -0
- backtrader/feeds/yahoo.py +399 -0
- backtrader/fillers.py +148 -0
- backtrader/filters/__init__.py +34 -0
- backtrader/filters/bsplitter.py +127 -0
- backtrader/filters/calendardays.py +121 -0
- backtrader/filters/datafiller.py +192 -0
- backtrader/filters/datafilter.py +74 -0
- backtrader/filters/daysteps.py +96 -0
- backtrader/filters/heikinashi.py +63 -0
- backtrader/filters/renko.py +164 -0
- backtrader/filters/session.py +289 -0
- backtrader/flt.py +80 -0
- backtrader/functions.py +960 -0
- backtrader/indicator.py +449 -0
- backtrader/indicators/__init__.py +148 -0
- backtrader/indicators/accdecoscillator.py +110 -0
- backtrader/indicators/aroon.py +300 -0
- backtrader/indicators/atr.py +315 -0
- backtrader/indicators/awesomeoscillator.py +122 -0
- backtrader/indicators/basicops.py +834 -0
- backtrader/indicators/bollinger.py +223 -0
- backtrader/indicators/cci.py +89 -0
- backtrader/indicators/channels_ext.py +83 -0
- backtrader/indicators/contrib/__init__.py +228 -0
- backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
- backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
- backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
- backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
- backtrader/indicators/contrib/adxdmi.py +34 -0
- backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
- backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
- backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
- backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
- backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
- backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
- backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
- backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
- backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
- backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
- backtrader/indicators/contrib/average_change_candle.py +165 -0
- backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
- backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
- backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
- backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
- backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
- backtrader/indicators/contrib/blau_csi.py +76 -0
- backtrader/indicators/contrib/blau_ergodic.py +53 -0
- backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
- backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
- backtrader/indicators/contrib/blau_tvi.py +55 -0
- backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
- backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
- backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
- backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
- backtrader/indicators/contrib/bsi_indicator.py +87 -0
- backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
- backtrader/indicators/contrib/bulls_power.py +56 -0
- backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
- backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
- backtrader/indicators/contrib/candle_stop_color.py +46 -0
- backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
- backtrader/indicators/contrib/candlesticks_bw.py +45 -0
- backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
- backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
- backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
- backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
- backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
- backtrader/indicators/contrib/cg_oscillator.py +40 -0
- backtrader/indicators/contrib/close_line_cci.py +38 -0
- backtrader/indicators/contrib/close_price_fractals.py +47 -0
- backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
- backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
- backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
- backtrader/indicators/contrib/color_hma.py +71 -0
- backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
- backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
- backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
- backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
- backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
- backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
- backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
- backtrader/indicators/contrib/color_x2_ma.py +49 -0
- backtrader/indicators/contrib/color_x_derivative.py +63 -0
- backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
- backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
- backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
- backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
- backtrader/indicators/contrib/derivative_indicator.py +95 -0
- backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
- backtrader/indicators/contrib/digital_macd.py +200 -0
- backtrader/indicators/contrib/donchian_channels_system.py +45 -0
- backtrader/indicators/contrib/dots_indicator.py +93 -0
- backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
- backtrader/indicators/contrib/ema_rsi_va.py +80 -0
- backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
- backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
- backtrader/indicators/contrib/fatl_filter.py +179 -0
- backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
- backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
- backtrader/indicators/contrib/fisher_org_v1.py +102 -0
- backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
- backtrader/indicators/contrib/force_index_ema.py +96 -0
- backtrader/indicators/contrib/force_index_ema_2.py +27 -0
- backtrader/indicators/contrib/forecast_oscilator.py +145 -0
- backtrader/indicators/contrib/fractal_amambk.py +81 -0
- backtrader/indicators/contrib/frama_series.py +84 -0
- backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
- backtrader/indicators/contrib/go_indicator.py +93 -0
- backtrader/indicators/contrib/hlr_indicator.py +95 -0
- backtrader/indicators/contrib/hma.py +50 -0
- backtrader/indicators/contrib/i4_drfv2.py +34 -0
- backtrader/indicators/contrib/i4_drfv3.py +38 -0
- backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
- backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
- backtrader/indicators/contrib/i_gap_indicator.py +45 -0
- backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
- backtrader/indicators/contrib/i_trend_indicator.py +125 -0
- backtrader/indicators/contrib/iamma_indicator.py +39 -0
- backtrader/indicators/contrib/indexed_moving_average.py +33 -0
- backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
- backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
- backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
- backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
- backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
- backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
- backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
- backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
- backtrader/indicators/contrib/kalman_filter_line.py +127 -0
- backtrader/indicators/contrib/kama_indicator.py +150 -0
- backtrader/indicators/contrib/karacatica_indicator.py +99 -0
- backtrader/indicators/contrib/kdj_indicator.py +59 -0
- backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
- backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
- backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
- backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
- backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
- backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
- backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
- backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
- backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
- backtrader/indicators/contrib/loco_indicator.py +88 -0
- backtrader/indicators/contrib/lrma_indicator.py +185 -0
- backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
- backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
- backtrader/indicators/contrib/macd2_indicator.py +61 -0
- backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
- backtrader/indicators/contrib/malr_indicator.py +77 -0
- backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
- backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
- backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
- backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
- backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
- backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
- backtrader/indicators/contrib/nrtr_indicator.py +95 -0
- backtrader/indicators/contrib/p_channel_system.py +40 -0
- backtrader/indicators/contrib/percent_envelope.py +37 -0
- backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
- backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
- backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
- backtrader/indicators/contrib/price_extreme_channel.py +35 -0
- backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
- backtrader/indicators/contrib/ravi_indicator.py +40 -0
- backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
- backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
- backtrader/indicators/contrib/renko_level.py +85 -0
- backtrader/indicators/contrib/renko_line_break.py +91 -0
- backtrader/indicators/contrib/rftl_indicator.py +41 -0
- backtrader/indicators/contrib/rkd_indicator.py +53 -0
- backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
- backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
- backtrader/indicators/contrib/rsi_slowdown.py +57 -0
- backtrader/indicators/contrib/rsioma_v2.py +41 -0
- backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
- backtrader/indicators/contrib/safe_adx.py +89 -0
- backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
- backtrader/indicators/contrib/sidus_indicator.py +105 -0
- backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
- backtrader/indicators/contrib/sliding_range_color.py +56 -0
- backtrader/indicators/contrib/slow_stoch.py +42 -0
- backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
- backtrader/indicators/contrib/smoothed_rsi.py +31 -0
- backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
- backtrader/indicators/contrib/stalin_indicator.py +152 -0
- backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
- backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
- backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
- backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
- backtrader/indicators/contrib/t3_average.py +76 -0
- backtrader/indicators/contrib/t3_indicator.py +40 -0
- backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
- backtrader/indicators/contrib/three_candles_indicator.py +70 -0
- backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
- backtrader/indicators/contrib/time_line.py +57 -0
- backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
- backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
- backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
- backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
- backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
- backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
- backtrader/indicators/contrib/trigger_line.py +66 -0
- backtrader/indicators/contrib/triple_ema_rate.py +34 -0
- backtrader/indicators/contrib/trvi_indicator.py +194 -0
- backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
- backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
- backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
- backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
- backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
- backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
- backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
- backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
- backtrader/indicators/contrib/vwma_candle.py +57 -0
- backtrader/indicators/contrib/vwma_digit_system.py +70 -0
- backtrader/indicators/contrib/wami.py +43 -0
- backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
- backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
- backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
- backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
- backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
- backtrader/indicators/contrib/xma_ichimoku.py +163 -0
- backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
- backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
- backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
- backtrader/indicators/contrib/xmacd_indicator.py +70 -0
- backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
- backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
- backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
- backtrader/indicators/contrib/xrvi_indicator.py +130 -0
- backtrader/indicators/contrib/zero_lag_macd.py +36 -0
- backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
- backtrader/indicators/contrib/zpf_indicator.py +115 -0
- backtrader/indicators/crossover.py +337 -0
- backtrader/indicators/dema.py +175 -0
- backtrader/indicators/demarker.py +270 -0
- backtrader/indicators/deviation.py +284 -0
- backtrader/indicators/directionalmove.py +1071 -0
- backtrader/indicators/dma.py +112 -0
- backtrader/indicators/dpo.py +96 -0
- backtrader/indicators/dv2.py +56 -0
- backtrader/indicators/ema.py +145 -0
- backtrader/indicators/envelope.py +475 -0
- backtrader/indicators/hadelta.py +198 -0
- backtrader/indicators/heikinashi.py +153 -0
- backtrader/indicators/hma.py +153 -0
- backtrader/indicators/hurst.py +151 -0
- backtrader/indicators/ichimoku.py +267 -0
- backtrader/indicators/kama.py +181 -0
- backtrader/indicators/kst.py +159 -0
- backtrader/indicators/lrsi.py +125 -0
- backtrader/indicators/mabase.py +147 -0
- backtrader/indicators/macd.py +322 -0
- backtrader/indicators/momentum.py +267 -0
- backtrader/indicators/moneyflow.py +237 -0
- backtrader/indicators/mt5atr.py +124 -0
- backtrader/indicators/myind.py +179 -0
- backtrader/indicators/obv.py +94 -0
- backtrader/indicators/ols.py +265 -0
- backtrader/indicators/oscillator.py +161 -0
- backtrader/indicators/percentchange.py +83 -0
- backtrader/indicators/percentrank.py +46 -0
- backtrader/indicators/pivotpoint.py +469 -0
- backtrader/indicators/prettygoodoscillator.py +113 -0
- backtrader/indicators/priceops_ext.py +123 -0
- backtrader/indicators/priceoscillator.py +262 -0
- backtrader/indicators/psar.py +212 -0
- backtrader/indicators/rmi.py +69 -0
- backtrader/indicators/rsi.py +440 -0
- backtrader/indicators/sma.py +141 -0
- backtrader/indicators/smma.py +116 -0
- backtrader/indicators/spread.py +54 -0
- backtrader/indicators/stochastic.py +263 -0
- backtrader/indicators/supertrend.py +436 -0
- backtrader/indicators/trend_ext.py +105 -0
- backtrader/indicators/trix.py +202 -0
- backtrader/indicators/tsi.py +155 -0
- backtrader/indicators/ultimateoscillator.py +158 -0
- backtrader/indicators/vortex.py +62 -0
- backtrader/indicators/williams.py +194 -0
- backtrader/indicators/wma.py +103 -0
- backtrader/indicators/zlema.py +135 -0
- backtrader/indicators/zlind.py +104 -0
- backtrader/linebuffer.py +3155 -0
- backtrader/lineiterator.py +2911 -0
- backtrader/lineroot.py +1106 -0
- backtrader/lineseries.py +2559 -0
- backtrader/live_trading/__init__.py +31 -0
- backtrader/live_trading/interface.py +404 -0
- backtrader/mathsupport.py +94 -0
- backtrader/metabase.py +1804 -0
- backtrader/mixins/__init__.py +21 -0
- backtrader/mixins/singleton.py +118 -0
- backtrader/observer.py +106 -0
- backtrader/observers/__init__.py +45 -0
- backtrader/observers/benchmark.py +126 -0
- backtrader/observers/broker.py +184 -0
- backtrader/observers/buysell.py +144 -0
- backtrader/observers/drawdown.py +161 -0
- backtrader/observers/logreturns.py +113 -0
- backtrader/observers/timereturn.py +86 -0
- backtrader/observers/trade_logger.py +2972 -0
- backtrader/observers/tradelogger.py +6 -0
- backtrader/observers/trades.py +258 -0
- backtrader/order.py +1114 -0
- backtrader/parameters.py +2345 -0
- backtrader/plot/__init__.py +54 -0
- backtrader/plot/finance.py +1022 -0
- backtrader/plot/formatters.py +200 -0
- backtrader/plot/locator.py +353 -0
- backtrader/plot/multicursor.py +495 -0
- backtrader/plot/plot.py +2500 -0
- backtrader/plot/plot_plotly.py +1351 -0
- backtrader/plot/scheme.py +253 -0
- backtrader/plot/utils.py +104 -0
- backtrader/position.py +290 -0
- backtrader/position_modes.py +132 -0
- backtrader/profiles.py +254 -0
- backtrader/reports/__init__.py +39 -0
- backtrader/reports/charts.py +371 -0
- backtrader/reports/performance.py +620 -0
- backtrader/reports/reporter.py +660 -0
- backtrader/resamplerfilter.py +1001 -0
- backtrader/signal.py +118 -0
- backtrader/signals/__init__.py +17 -0
- backtrader/sizer.py +114 -0
- backtrader/sizers/__init__.py +26 -0
- backtrader/sizers/fixedsize.py +161 -0
- backtrader/sizers/percents_sizer.py +119 -0
- backtrader/store.py +221 -0
- backtrader/stores/__init__.py +33 -0
- backtrader/stores/btapistore.py +15506 -0
- backtrader/stores/livestore.py +137 -0
- backtrader/stores/vchartfile.py +96 -0
- backtrader/strategy.py +3655 -0
- backtrader/talib.py +280 -0
- backtrader/test_helpers.py +96 -0
- backtrader/timer.py +358 -0
- backtrader/trade.py +442 -0
- backtrader/tradingcal.py +361 -0
- backtrader/utils/__init__.py +68 -0
- backtrader/utils/autodict.py +251 -0
- backtrader/utils/date.py +71 -0
- backtrader/utils/dateintern.py +509 -0
- backtrader/utils/flushfile.py +94 -0
- backtrader/utils/fractal.py +101 -0
- backtrader/utils/get_metrics.py +101 -0
- backtrader/utils/load_data.py +209 -0
- backtrader/utils/log_message.py +998 -0
- backtrader/utils/ordereddefaultdict.py +75 -0
- backtrader/utils/py3.py +296 -0
- backtrader/version.py +21 -0
- 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
|