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,1398 @@
|
|
|
1
|
+
"""Cross-check backtrader's HFT engine against hftbacktest on Binance BBO data.
|
|
2
|
+
|
|
3
|
+
Diagnostic/validation script that runs the same Binance BBO/depth dataset
|
|
4
|
+
through both the backtrader tick broker and the reference ``hftbacktest``
|
|
5
|
+
engine and compares fills/snapshots, used to verify matching-engine fidelity.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import bisect
|
|
11
|
+
import json
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from dataclasses import asdict, dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import SimpleNamespace
|
|
16
|
+
from typing import Iterator, Optional
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from backtrader.brokers.hft.examples import build_quote_builder, get_hftbacktest_example_spec
|
|
21
|
+
from backtrader.brokers.hft.exchange import FillRole, OrderResult, QueueExchangeModel
|
|
22
|
+
from backtrader.brokers.hft.queue import ProbQueueModel
|
|
23
|
+
from backtrader.brokers.tickbroker import TickBroker
|
|
24
|
+
from backtrader.channels.orderbook import OrderBookChannel
|
|
25
|
+
from backtrader.channels.tick import TickChannel
|
|
26
|
+
from backtrader.order import Order
|
|
27
|
+
|
|
28
|
+
from ...utils.log_message import get_logger
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ComparisonFill:
|
|
35
|
+
"""Represents a fill (trade) for comparison purposes.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
side: Order side ('buy' or 'sell').
|
|
39
|
+
price: Fill price.
|
|
40
|
+
size: Fill quantity.
|
|
41
|
+
timestamp_ns: Timestamp in nanoseconds.
|
|
42
|
+
local_timestamp_ns: Local timestamp in nanoseconds.
|
|
43
|
+
exch_timestamp_ns: Exchange timestamp in nanoseconds.
|
|
44
|
+
order_ref: Order reference ID.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
side: str
|
|
48
|
+
price: float
|
|
49
|
+
size: float
|
|
50
|
+
timestamp_ns: Optional[int] = None
|
|
51
|
+
local_timestamp_ns: Optional[int] = None
|
|
52
|
+
exch_timestamp_ns: Optional[int] = None
|
|
53
|
+
order_ref: Optional[int] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class EngineResult:
|
|
58
|
+
"""Result from a backtesting engine.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
balance: Final account balance.
|
|
62
|
+
position: Final position quantity.
|
|
63
|
+
num_trades: Total number of trades.
|
|
64
|
+
fills: List of all comparison fills.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
balance: float
|
|
68
|
+
position: float
|
|
69
|
+
num_trades: int
|
|
70
|
+
fills: list[ComparisonFill]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class StrategyComparisonResult:
|
|
75
|
+
"""Comparison result between backtrader and hftbacktest engines.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
strategy: Strategy name.
|
|
79
|
+
decision_interval_ns: Decision interval in nanoseconds.
|
|
80
|
+
backtrader: Engine result from backtrader.
|
|
81
|
+
hftbacktest: Engine result from hftbacktest.
|
|
82
|
+
matches: Dictionary of match status by field.
|
|
83
|
+
deltas: Dictionary of delta values by field.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
strategy: str
|
|
87
|
+
decision_interval_ns: int
|
|
88
|
+
backtrader: EngineResult
|
|
89
|
+
hftbacktest: EngineResult
|
|
90
|
+
matches: dict[str, bool]
|
|
91
|
+
deltas: dict[str, float]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _DataRef:
|
|
95
|
+
"""Reference data object for market data symbols."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, symbol: str):
|
|
98
|
+
"""Initialize a data reference.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
symbol: Trading symbol.
|
|
102
|
+
"""
|
|
103
|
+
self._name = symbol
|
|
104
|
+
self.name = symbol
|
|
105
|
+
self.symbol = symbol
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_BACKTRADER_COMPARISON_INITIAL_CASH = 1_000_000_000.0
|
|
109
|
+
_DEPTH_EVENT = 1
|
|
110
|
+
_TRADE_EVENT = 2
|
|
111
|
+
_BUY_EVENT = 1 << 29
|
|
112
|
+
_SELL_EVENT = 1 << 28
|
|
113
|
+
_EXCH_EVENT = 1 << 31
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _build_runtime_builder(spec, market_data_path):
|
|
117
|
+
builder = build_quote_builder(spec)
|
|
118
|
+
requirement = next(
|
|
119
|
+
(item for item in spec.input_requirements if item.name == "precompute_data"), None
|
|
120
|
+
)
|
|
121
|
+
if requirement is None:
|
|
122
|
+
return builder
|
|
123
|
+
precompute_path = _resolve_required_input_path(market_data_path, requirement.patterns)
|
|
124
|
+
if precompute_path is None:
|
|
125
|
+
raise FileNotFoundError(
|
|
126
|
+
f"Missing required precompute_data for strategy '{spec.name}'. Expected one of: {', '.join(requirement.patterns)}"
|
|
127
|
+
)
|
|
128
|
+
precompute = _load_precompute_data(precompute_path)
|
|
129
|
+
if hasattr(builder, "precompute_data"):
|
|
130
|
+
builder.precompute_data = precompute
|
|
131
|
+
return builder
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _resolve_required_input_path(market_data_path, patterns):
|
|
135
|
+
market_data_path = Path(market_data_path)
|
|
136
|
+
for pattern in patterns:
|
|
137
|
+
for base in (market_data_path.parent, *market_data_path.parents):
|
|
138
|
+
candidate = base / pattern
|
|
139
|
+
if candidate.exists():
|
|
140
|
+
return candidate
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _load_precompute_data(precompute_path):
|
|
145
|
+
with np.load(str(precompute_path)) as payload:
|
|
146
|
+
if "data" in payload:
|
|
147
|
+
return payload["data"]
|
|
148
|
+
keys = list(payload.keys())
|
|
149
|
+
if keys:
|
|
150
|
+
return payload[keys[0]]
|
|
151
|
+
raise ValueError(f"Unable to load precompute data from {precompute_path}")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _builder_order_qty(builder) -> float:
|
|
155
|
+
return float(getattr(builder, "current_order_qty", getattr(builder, "order_qty", 1.0)))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class _NoPartialQueueExchangeModel(QueueExchangeModel):
|
|
159
|
+
"""Exchange model that simulates no partial fills.
|
|
160
|
+
|
|
161
|
+
Used for comparing backtrader against hftbacktest which uses
|
|
162
|
+
no-partial-fill exchange semantics.
|
|
163
|
+
|
|
164
|
+
Attributes:
|
|
165
|
+
queue_model_power: Power parameter for probability queue model.
|
|
166
|
+
lot_size: Minimum order quantity.
|
|
167
|
+
tick_size: Minimum price increment.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self, queue_model_power: float = 2.0, lot_size: float = 1.0, tick_size: float = None
|
|
172
|
+
):
|
|
173
|
+
"""Initialize the no-partial-fill exchange model.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
queue_model_power: Queue model power parameter (default: 2.0).
|
|
177
|
+
lot_size: Minimum lot size (default: 1.0).
|
|
178
|
+
tick_size: Minimum price increment (default: None).
|
|
179
|
+
"""
|
|
180
|
+
super().__init__(
|
|
181
|
+
queue_model=ProbQueueModel(power=queue_model_power, lot_size=lot_size),
|
|
182
|
+
tick_size=tick_size,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def on_new_order(self, order, ob_snapshot):
|
|
186
|
+
"""Handle a new order event.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
order: The incoming order.
|
|
190
|
+
ob_snapshot: Current order book snapshot.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
OrderResult indicating the order action.
|
|
194
|
+
"""
|
|
195
|
+
if getattr(order, "_fill_role", None) == FillRole.MAKER:
|
|
196
|
+
return OrderResult(action="PENDING")
|
|
197
|
+
result = super().on_new_order(order, ob_snapshot)
|
|
198
|
+
if result.action == "PENDING" and getattr(order, "_fill_role", None) == FillRole.MAKER:
|
|
199
|
+
levels = (
|
|
200
|
+
getattr(ob_snapshot, "bids", None)
|
|
201
|
+
if order.isbuy()
|
|
202
|
+
else getattr(ob_snapshot, "asks", None)
|
|
203
|
+
)
|
|
204
|
+
level_qty = self._level_qty(levels, float(order.price))
|
|
205
|
+
order._queue_wait_for_first_visible_level = (
|
|
206
|
+
order.isbuy()
|
|
207
|
+
and level_qty <= 1e-12
|
|
208
|
+
and float(getattr(order, "_queue_initial_ahead", 0.0)) <= 1e-12
|
|
209
|
+
)
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
def _same_price_tick(self, price_a: float, price_b: float) -> bool:
|
|
213
|
+
if self._tick_size is not None and self._tick_size > 0:
|
|
214
|
+
return round(price_a / self._tick_size) == round(price_b / self._tick_size)
|
|
215
|
+
return price_a == price_b
|
|
216
|
+
|
|
217
|
+
def _level_qty(self, levels, price: float) -> float:
|
|
218
|
+
if not levels:
|
|
219
|
+
return 0.0
|
|
220
|
+
for level_price, level_qty in levels:
|
|
221
|
+
if self._same_price_tick(float(level_price), float(price)):
|
|
222
|
+
return float(level_qty)
|
|
223
|
+
return 0.0
|
|
224
|
+
|
|
225
|
+
def _trade_reaches_order(
|
|
226
|
+
self, order_price: float, trade_price: float, is_buy_order: bool
|
|
227
|
+
) -> bool:
|
|
228
|
+
if self._tick_size is not None and self._tick_size > 0:
|
|
229
|
+
order_tick = round(order_price / self._tick_size)
|
|
230
|
+
trade_tick = round(trade_price / self._tick_size)
|
|
231
|
+
return trade_tick <= order_tick if is_buy_order else trade_tick >= order_tick
|
|
232
|
+
return trade_price <= order_price if is_buy_order else trade_price >= order_price
|
|
233
|
+
|
|
234
|
+
def on_trade(self, trade_event, pending_orders):
|
|
235
|
+
"""Handle a trade event and determine fills.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
trade_event: Trade event with price and direction.
|
|
239
|
+
pending_orders: List of pending maker orders.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of (order, price, remaining_size, role) tuples for fills.
|
|
243
|
+
"""
|
|
244
|
+
fills: list = []
|
|
245
|
+
trade_price = getattr(trade_event, "price", None)
|
|
246
|
+
direction = str(getattr(trade_event, "direction", "")).lower()
|
|
247
|
+
if trade_price is None or direction not in {"buy", "sell"}:
|
|
248
|
+
return fills
|
|
249
|
+
|
|
250
|
+
for order in pending_orders:
|
|
251
|
+
if getattr(order, "_fill_role", None) != FillRole.MAKER:
|
|
252
|
+
continue
|
|
253
|
+
if direction == "buy":
|
|
254
|
+
if order.isbuy():
|
|
255
|
+
continue
|
|
256
|
+
if not self._trade_reaches_order(
|
|
257
|
+
float(order.price), float(trade_price), is_buy_order=False
|
|
258
|
+
):
|
|
259
|
+
continue
|
|
260
|
+
else:
|
|
261
|
+
if not order.isbuy():
|
|
262
|
+
continue
|
|
263
|
+
if not self._trade_reaches_order(
|
|
264
|
+
float(order.price), float(trade_price), is_buy_order=True
|
|
265
|
+
):
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
remaining = getattr(getattr(order, "executed", None), "remsize", None)
|
|
269
|
+
if remaining is None:
|
|
270
|
+
remaining = getattr(order, "size", 0.0)
|
|
271
|
+
remaining = abs(float(remaining))
|
|
272
|
+
if remaining <= 0.0:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
if self._same_price_tick(float(order.price), float(trade_price)):
|
|
276
|
+
if getattr(order, "_queue_wait_for_first_visible_level", False):
|
|
277
|
+
continue
|
|
278
|
+
fillable = self._queue_model.update_on_trade(order, trade_event)
|
|
279
|
+
if fillable <= 1e-12 and float(getattr(order, "_queue_ahead", 0.0)) >= 0.0:
|
|
280
|
+
continue
|
|
281
|
+
fills.append((order, float(order.price), remaining, FillRole.MAKER))
|
|
282
|
+
return fills
|
|
283
|
+
|
|
284
|
+
def on_depth_update(self, ob_event, pending_orders):
|
|
285
|
+
"""Handle an order book depth update event.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
ob_event: Order book event with bids/asks.
|
|
289
|
+
pending_orders: List of pending maker orders.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
List of (order, price, remaining_size, role) tuples for fills.
|
|
293
|
+
"""
|
|
294
|
+
fills: list = []
|
|
295
|
+
prev_bids = getattr(ob_event, "previous_bids", None) or []
|
|
296
|
+
prev_asks = getattr(ob_event, "previous_asks", None) or []
|
|
297
|
+
curr_bids = getattr(ob_event, "bids", None) or []
|
|
298
|
+
curr_asks = getattr(ob_event, "asks", None) or []
|
|
299
|
+
|
|
300
|
+
for order in pending_orders:
|
|
301
|
+
if getattr(order, "_fill_role", None) != FillRole.MAKER:
|
|
302
|
+
continue
|
|
303
|
+
price = getattr(order, "price", None)
|
|
304
|
+
if price is None:
|
|
305
|
+
continue
|
|
306
|
+
prev_qty = self._level_qty(prev_bids if order.isbuy() else prev_asks, float(price))
|
|
307
|
+
new_qty = self._level_qty(curr_bids if order.isbuy() else curr_asks, float(price))
|
|
308
|
+
if (
|
|
309
|
+
getattr(order, "_queue_wait_for_first_visible_level", False)
|
|
310
|
+
and prev_qty <= 1e-12
|
|
311
|
+
and new_qty > 1e-12
|
|
312
|
+
):
|
|
313
|
+
order._queue_ahead = float(new_qty)
|
|
314
|
+
order._queue_initial_ahead = max(
|
|
315
|
+
float(getattr(order, "_queue_initial_ahead", 0.0)), float(new_qty)
|
|
316
|
+
)
|
|
317
|
+
order._queue_trade_qty = 0.0
|
|
318
|
+
order._queue_fillable = 0.0
|
|
319
|
+
order._queue_wait_for_first_visible_level = False
|
|
320
|
+
continue
|
|
321
|
+
if abs(prev_qty - new_qty) <= 1e-12:
|
|
322
|
+
continue
|
|
323
|
+
self._queue_model.update_on_depth(order, prev_qty, new_qty)
|
|
324
|
+
return fills
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _fill_counter(fills: list[ComparisonFill]) -> Counter:
|
|
328
|
+
return Counter(
|
|
329
|
+
(item.side, round(float(item.price), 8), round(float(item.size), 8)) for item in fills
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _ordered_fill_sequence(fills: list[ComparisonFill]) -> list[tuple[object, ...]]:
|
|
334
|
+
return [(item.side, round(float(item.price), 8), round(float(item.size), 8)) for item in fills]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _normalized_fill_sequence(fills: list[ComparisonFill]) -> list[tuple[object, ...]]:
|
|
338
|
+
return sorted(
|
|
339
|
+
(
|
|
340
|
+
int(item.timestamp_ns or item.exch_timestamp_ns or item.local_timestamp_ns or 0),
|
|
341
|
+
item.side,
|
|
342
|
+
round(float(item.price), 8),
|
|
343
|
+
round(float(item.size), 8),
|
|
344
|
+
int(item.exch_timestamp_ns or 0),
|
|
345
|
+
)
|
|
346
|
+
for item in fills
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def compare_binance_bbo_strategy(
|
|
351
|
+
strategy_name: str,
|
|
352
|
+
orderbook_path,
|
|
353
|
+
tick_path,
|
|
354
|
+
market_data_path,
|
|
355
|
+
tick_size: float,
|
|
356
|
+
lot_size: float,
|
|
357
|
+
symbol: str = "ETH/USDT",
|
|
358
|
+
decision_interval_ns: Optional[int] = None,
|
|
359
|
+
maker_commission: Optional[float] = None,
|
|
360
|
+
taker_commission: Optional[float] = None,
|
|
361
|
+
queue_model_power: Optional[float] = None,
|
|
362
|
+
max_decisions: Optional[int] = None,
|
|
363
|
+
) -> StrategyComparisonResult:
|
|
364
|
+
"""Compare backtrader HFT engine against hftbacktest on Binance BBO data.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
strategy_name: Name of the HFT strategy to compare.
|
|
368
|
+
orderbook_path: Path to orderbook depth data file.
|
|
369
|
+
tick_path: Path to tick trade data file.
|
|
370
|
+
market_data_path: Base path for market data.
|
|
371
|
+
tick_size: Minimum price increment.
|
|
372
|
+
lot_size: Minimum order quantity.
|
|
373
|
+
symbol: Trading symbol (default: "ETH/USDT").
|
|
374
|
+
decision_interval_ns: Decision interval in nanoseconds.
|
|
375
|
+
maker_commission: Maker commission rate (overrides spec).
|
|
376
|
+
taker_commission: Taker commission rate (overrides spec).
|
|
377
|
+
queue_model_power: Queue model power (overrides spec).
|
|
378
|
+
max_decisions: Maximum number of decisions to process.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
StrategyComparisonResult with backtrader and hftbacktest results.
|
|
382
|
+
"""
|
|
383
|
+
spec = get_hftbacktest_example_spec(strategy_name)
|
|
384
|
+
interval_ns = int(decision_interval_ns or spec.strategy.interval_ns)
|
|
385
|
+
maker_fee = float(
|
|
386
|
+
maker_commission
|
|
387
|
+
if maker_commission is not None
|
|
388
|
+
else spec.asset_parameters.get("maker_commission", 0.0)
|
|
389
|
+
)
|
|
390
|
+
taker_fee = float(
|
|
391
|
+
taker_commission
|
|
392
|
+
if taker_commission is not None
|
|
393
|
+
else spec.asset_parameters.get("taker_commission", 0.0)
|
|
394
|
+
)
|
|
395
|
+
queue_power = float(
|
|
396
|
+
queue_model_power
|
|
397
|
+
if queue_model_power is not None
|
|
398
|
+
else spec.asset_parameters.get("queue_model_power", 2.0)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
backtrader_builder = _build_runtime_builder(spec, market_data_path)
|
|
402
|
+
hftbacktest_builder = _build_runtime_builder(spec, market_data_path)
|
|
403
|
+
decision_anchor_ns = _market_data_exchange_anchor_ns(market_data_path)
|
|
404
|
+
exchange_book = _market_data_exchange_book(market_data_path)
|
|
405
|
+
|
|
406
|
+
backtrader_result = _run_backtrader_strategy(
|
|
407
|
+
orderbook_path=orderbook_path,
|
|
408
|
+
tick_path=tick_path,
|
|
409
|
+
market_data_path=market_data_path,
|
|
410
|
+
symbol=symbol,
|
|
411
|
+
builder=backtrader_builder,
|
|
412
|
+
tick_size=tick_size,
|
|
413
|
+
lot_size=lot_size,
|
|
414
|
+
decision_anchor_ns=decision_anchor_ns,
|
|
415
|
+
exchange_book=exchange_book,
|
|
416
|
+
interval_ns=interval_ns,
|
|
417
|
+
maker_commission=maker_fee,
|
|
418
|
+
taker_commission=taker_fee,
|
|
419
|
+
queue_model_power=queue_power,
|
|
420
|
+
max_decisions=max_decisions,
|
|
421
|
+
)
|
|
422
|
+
hftbacktest_result = _run_hftbacktest_strategy(
|
|
423
|
+
market_data_path=market_data_path,
|
|
424
|
+
builder=hftbacktest_builder,
|
|
425
|
+
tick_size=tick_size,
|
|
426
|
+
lot_size=lot_size,
|
|
427
|
+
interval_ns=interval_ns,
|
|
428
|
+
maker_commission=maker_fee,
|
|
429
|
+
taker_commission=taker_fee,
|
|
430
|
+
queue_model_power=queue_power,
|
|
431
|
+
max_decisions=max_decisions,
|
|
432
|
+
)
|
|
433
|
+
bt_fills = _fill_counter(backtrader_result.fills)
|
|
434
|
+
hft_fills = _fill_counter(hftbacktest_result.fills)
|
|
435
|
+
bt_in_order = _ordered_fill_sequence(backtrader_result.fills)
|
|
436
|
+
hft_in_order = _ordered_fill_sequence(hftbacktest_result.fills)
|
|
437
|
+
bt_normalized = _normalized_fill_sequence(backtrader_result.fills)
|
|
438
|
+
hft_normalized = _normalized_fill_sequence(hftbacktest_result.fills)
|
|
439
|
+
|
|
440
|
+
return StrategyComparisonResult(
|
|
441
|
+
strategy=strategy_name,
|
|
442
|
+
decision_interval_ns=interval_ns,
|
|
443
|
+
backtrader=backtrader_result,
|
|
444
|
+
hftbacktest=hftbacktest_result,
|
|
445
|
+
matches={
|
|
446
|
+
"balance": abs(backtrader_result.balance - hftbacktest_result.balance) < 1e-5,
|
|
447
|
+
"position": abs(backtrader_result.position - hftbacktest_result.position) < 1e-9,
|
|
448
|
+
"num_trades": backtrader_result.num_trades == hftbacktest_result.num_trades,
|
|
449
|
+
"fills": bt_fills == hft_fills,
|
|
450
|
+
"fills_in_order": bt_in_order == hft_in_order,
|
|
451
|
+
"fills_normalized_order": bt_normalized == hft_normalized,
|
|
452
|
+
},
|
|
453
|
+
deltas={
|
|
454
|
+
"balance": backtrader_result.balance - hftbacktest_result.balance,
|
|
455
|
+
"position": backtrader_result.position - hftbacktest_result.position,
|
|
456
|
+
"num_trades": float(backtrader_result.num_trades - hftbacktest_result.num_trades),
|
|
457
|
+
},
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def comparison_to_json(result: StrategyComparisonResult) -> str:
|
|
462
|
+
"""Convert a strategy comparison result to JSON string.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
result: StrategyComparisonResult to serialize.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
JSON-formatted string representation.
|
|
469
|
+
"""
|
|
470
|
+
payload = asdict(result)
|
|
471
|
+
return json.dumps(payload, indent=2)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def engine_result_to_json(result: EngineResult) -> str:
|
|
475
|
+
"""Convert an engine result to JSON string.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
result: EngineResult to serialize.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
JSON-formatted string representation.
|
|
482
|
+
"""
|
|
483
|
+
payload = asdict(result)
|
|
484
|
+
return json.dumps(payload, indent=2)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def run_binance_bbo_backtrader_strategy(
|
|
488
|
+
strategy_name: str,
|
|
489
|
+
orderbook_path,
|
|
490
|
+
tick_path,
|
|
491
|
+
market_data_path,
|
|
492
|
+
tick_size: float,
|
|
493
|
+
lot_size: float,
|
|
494
|
+
symbol: str = "ETH/USDT",
|
|
495
|
+
decision_interval_ns: Optional[int] = None,
|
|
496
|
+
maker_commission: Optional[float] = None,
|
|
497
|
+
taker_commission: Optional[float] = None,
|
|
498
|
+
queue_model_power: Optional[float] = None,
|
|
499
|
+
max_decisions: Optional[int] = None,
|
|
500
|
+
) -> EngineResult:
|
|
501
|
+
"""Run backtrader HFT strategy on Binance BBO data.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
strategy_name: Name of the HFT strategy to run.
|
|
505
|
+
orderbook_path: Path to orderbook depth data file.
|
|
506
|
+
tick_path: Path to tick trade data file.
|
|
507
|
+
market_data_path: Base path for market data.
|
|
508
|
+
tick_size: Minimum price increment.
|
|
509
|
+
lot_size: Minimum order quantity.
|
|
510
|
+
symbol: Trading symbol (default: "ETH/USDT").
|
|
511
|
+
decision_interval_ns: Decision interval in nanoseconds.
|
|
512
|
+
maker_commission: Maker commission rate (overrides spec).
|
|
513
|
+
taker_commission: Taker commission rate (overrides spec).
|
|
514
|
+
queue_model_power: Queue model power (overrides spec).
|
|
515
|
+
max_decisions: Maximum number of decisions to process.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
EngineResult with backtrader backtest results.
|
|
519
|
+
"""
|
|
520
|
+
spec = get_hftbacktest_example_spec(strategy_name)
|
|
521
|
+
interval_ns = int(decision_interval_ns or spec.strategy.interval_ns)
|
|
522
|
+
maker_fee = float(
|
|
523
|
+
maker_commission
|
|
524
|
+
if maker_commission is not None
|
|
525
|
+
else spec.asset_parameters.get("maker_commission", 0.0)
|
|
526
|
+
)
|
|
527
|
+
taker_fee = float(
|
|
528
|
+
taker_commission
|
|
529
|
+
if taker_commission is not None
|
|
530
|
+
else spec.asset_parameters.get("taker_commission", 0.0)
|
|
531
|
+
)
|
|
532
|
+
queue_power = float(
|
|
533
|
+
queue_model_power
|
|
534
|
+
if queue_model_power is not None
|
|
535
|
+
else spec.asset_parameters.get("queue_model_power", 2.0)
|
|
536
|
+
)
|
|
537
|
+
builder = _build_runtime_builder(spec, market_data_path)
|
|
538
|
+
decision_anchor_ns = _market_data_exchange_anchor_ns(market_data_path)
|
|
539
|
+
exchange_book = _market_data_exchange_book(market_data_path)
|
|
540
|
+
return _run_backtrader_strategy(
|
|
541
|
+
orderbook_path=orderbook_path,
|
|
542
|
+
tick_path=tick_path,
|
|
543
|
+
market_data_path=market_data_path,
|
|
544
|
+
symbol=symbol,
|
|
545
|
+
builder=builder,
|
|
546
|
+
tick_size=tick_size,
|
|
547
|
+
lot_size=lot_size,
|
|
548
|
+
decision_anchor_ns=decision_anchor_ns,
|
|
549
|
+
exchange_book=exchange_book,
|
|
550
|
+
interval_ns=interval_ns,
|
|
551
|
+
maker_commission=maker_fee,
|
|
552
|
+
taker_commission=taker_fee,
|
|
553
|
+
queue_model_power=queue_power,
|
|
554
|
+
max_decisions=max_decisions,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def run_binance_bbo_hftbacktest_strategy(
|
|
559
|
+
strategy_name: str,
|
|
560
|
+
orderbook_path,
|
|
561
|
+
tick_path,
|
|
562
|
+
market_data_path,
|
|
563
|
+
tick_size: float,
|
|
564
|
+
lot_size: float,
|
|
565
|
+
symbol: str = "ETH/USDT",
|
|
566
|
+
decision_interval_ns: Optional[int] = None,
|
|
567
|
+
maker_commission: Optional[float] = None,
|
|
568
|
+
taker_commission: Optional[float] = None,
|
|
569
|
+
queue_model_power: Optional[float] = None,
|
|
570
|
+
max_decisions: Optional[int] = None,
|
|
571
|
+
) -> EngineResult:
|
|
572
|
+
"""Run hftbacktest strategy on Binance BBO data.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
strategy_name: Name of the HFT strategy to run.
|
|
576
|
+
orderbook_path: Path to orderbook depth data file (unused, for API compat).
|
|
577
|
+
tick_path: Path to tick trade data file (unused, for API compat).
|
|
578
|
+
market_data_path: Base path for market data.
|
|
579
|
+
tick_size: Minimum price increment.
|
|
580
|
+
lot_size: Minimum order quantity.
|
|
581
|
+
symbol: Trading symbol (default: "ETH/USDT").
|
|
582
|
+
decision_interval_ns: Decision interval in nanoseconds.
|
|
583
|
+
maker_commission: Maker commission rate (overrides spec).
|
|
584
|
+
taker_commission: Taker commission rate (overrides spec).
|
|
585
|
+
queue_model_power: Queue model power (overrides spec).
|
|
586
|
+
max_decisions: Maximum number of decisions to process.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
EngineResult with hftbacktest results.
|
|
590
|
+
"""
|
|
591
|
+
_ = (orderbook_path, tick_path, symbol)
|
|
592
|
+
spec = get_hftbacktest_example_spec(strategy_name)
|
|
593
|
+
interval_ns = int(decision_interval_ns or spec.strategy.interval_ns)
|
|
594
|
+
maker_fee = float(
|
|
595
|
+
maker_commission
|
|
596
|
+
if maker_commission is not None
|
|
597
|
+
else spec.asset_parameters.get("maker_commission", 0.0)
|
|
598
|
+
)
|
|
599
|
+
taker_fee = float(
|
|
600
|
+
taker_commission
|
|
601
|
+
if taker_commission is not None
|
|
602
|
+
else spec.asset_parameters.get("taker_commission", 0.0)
|
|
603
|
+
)
|
|
604
|
+
queue_power = float(
|
|
605
|
+
queue_model_power
|
|
606
|
+
if queue_model_power is not None
|
|
607
|
+
else spec.asset_parameters.get("queue_model_power", 2.0)
|
|
608
|
+
)
|
|
609
|
+
builder = _build_runtime_builder(spec, market_data_path)
|
|
610
|
+
return _run_hftbacktest_strategy(
|
|
611
|
+
market_data_path=market_data_path,
|
|
612
|
+
builder=builder,
|
|
613
|
+
tick_size=tick_size,
|
|
614
|
+
lot_size=lot_size,
|
|
615
|
+
interval_ns=interval_ns,
|
|
616
|
+
maker_commission=maker_fee,
|
|
617
|
+
taker_commission=taker_fee,
|
|
618
|
+
queue_model_power=queue_power,
|
|
619
|
+
max_decisions=max_decisions,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _run_backtrader_strategy(
|
|
624
|
+
orderbook_path,
|
|
625
|
+
tick_path,
|
|
626
|
+
market_data_path,
|
|
627
|
+
symbol: str,
|
|
628
|
+
builder,
|
|
629
|
+
tick_size: float,
|
|
630
|
+
lot_size: float,
|
|
631
|
+
decision_anchor_ns: Optional[int],
|
|
632
|
+
exchange_book,
|
|
633
|
+
interval_ns: int,
|
|
634
|
+
maker_commission: float,
|
|
635
|
+
taker_commission: float,
|
|
636
|
+
queue_model_power: float,
|
|
637
|
+
max_decisions: Optional[int],
|
|
638
|
+
) -> EngineResult:
|
|
639
|
+
data = _DataRef(symbol)
|
|
640
|
+
broker = TickBroker(
|
|
641
|
+
cash=_BACKTRADER_COMPARISON_INITIAL_CASH,
|
|
642
|
+
checksubmit=False,
|
|
643
|
+
allow_partial=False,
|
|
644
|
+
exchange_model=_NoPartialQueueExchangeModel(
|
|
645
|
+
queue_model_power=queue_model_power, lot_size=lot_size, tick_size=tick_size
|
|
646
|
+
),
|
|
647
|
+
)
|
|
648
|
+
broker.setcommission(
|
|
649
|
+
commission=0.0,
|
|
650
|
+
maker_commission=maker_commission,
|
|
651
|
+
taker_commission=taker_commission,
|
|
652
|
+
name=data.name,
|
|
653
|
+
)
|
|
654
|
+
local_orderbooks = iter(
|
|
655
|
+
OrderBookChannel(symbol=symbol, dataname=str(orderbook_path), depth=1).load()
|
|
656
|
+
)
|
|
657
|
+
next_local_orderbook = next(local_orderbooks, None)
|
|
658
|
+
exchange_events = _iter_exchange_market_events(market_data_path, symbol)
|
|
659
|
+
depth_probe = _create_depth_probe(market_data_path, tick_size=tick_size, lot_size=lot_size)
|
|
660
|
+
depth_probe_timestamp_ns = (
|
|
661
|
+
int(getattr(depth_probe, "current_timestamp", 0) or 0) if depth_probe is not None else None
|
|
662
|
+
)
|
|
663
|
+
working_orders: dict = {}
|
|
664
|
+
latest_snapshot = None
|
|
665
|
+
latest_exchange_snapshot = None
|
|
666
|
+
next_decision_ns = None
|
|
667
|
+
decisions = 0
|
|
668
|
+
decision_trades: list = []
|
|
669
|
+
|
|
670
|
+
for channel_type, event in exchange_events:
|
|
671
|
+
event_ns = _event_timestamp_ns(event)
|
|
672
|
+
reached_limit = False
|
|
673
|
+
if next_decision_ns is None:
|
|
674
|
+
next_decision_ns = (
|
|
675
|
+
int(decision_anchor_ns + interval_ns)
|
|
676
|
+
if decision_anchor_ns is not None
|
|
677
|
+
else int(event_ns + interval_ns)
|
|
678
|
+
)
|
|
679
|
+
while next_decision_ns is not None and event_ns > next_decision_ns:
|
|
680
|
+
decision_ts = float(next_decision_ns / 1_000_000_000.0)
|
|
681
|
+
latest_snapshot, next_local_orderbook = _advance_local_orderbook_snapshot(
|
|
682
|
+
latest_snapshot,
|
|
683
|
+
next_local_orderbook,
|
|
684
|
+
local_orderbooks,
|
|
685
|
+
decision_ts,
|
|
686
|
+
)
|
|
687
|
+
if latest_snapshot is None:
|
|
688
|
+
next_decision_ns += interval_ns
|
|
689
|
+
continue
|
|
690
|
+
builder_context = {
|
|
691
|
+
"timestamp_ns": int(next_decision_ns),
|
|
692
|
+
"last_trades": tuple(decision_trades),
|
|
693
|
+
}
|
|
694
|
+
quotes = builder(broker.getposition(data).size, latest_snapshot, builder_context)
|
|
695
|
+
decision_trades = []
|
|
696
|
+
depth_probe_timestamp_ns, decision_depth = _advance_depth_probe(
|
|
697
|
+
depth_probe, depth_probe_timestamp_ns, next_decision_ns
|
|
698
|
+
)
|
|
699
|
+
finalized_exchange_snapshot: Optional[object] = (
|
|
700
|
+
latest_exchange_snapshot
|
|
701
|
+
if int(getattr(latest_exchange_snapshot, "timestamp_ns", 0) or 0)
|
|
702
|
+
== int(next_decision_ns)
|
|
703
|
+
else None
|
|
704
|
+
)
|
|
705
|
+
if finalized_exchange_snapshot is not None:
|
|
706
|
+
base_submission_snapshot = finalized_exchange_snapshot
|
|
707
|
+
elif decision_depth is not None and _is_finite_book(
|
|
708
|
+
float(decision_depth.best_bid), float(decision_depth.best_ask)
|
|
709
|
+
):
|
|
710
|
+
base_submission_snapshot = SimpleNamespace(
|
|
711
|
+
bids=[(float(decision_depth.best_bid), float(decision_depth.best_bid_qty))],
|
|
712
|
+
asks=[(float(decision_depth.best_ask), float(decision_depth.best_ask_qty))],
|
|
713
|
+
)
|
|
714
|
+
else:
|
|
715
|
+
base_submission_snapshot = (
|
|
716
|
+
latest_exchange_snapshot
|
|
717
|
+
or _lookup_exchange_snapshot(exchange_book, next_decision_ns)
|
|
718
|
+
or latest_snapshot
|
|
719
|
+
)
|
|
720
|
+
submission_snapshot = base_submission_snapshot
|
|
721
|
+
if submission_snapshot is not None:
|
|
722
|
+
submission_fallback_snapshot = (
|
|
723
|
+
None if finalized_exchange_snapshot is not None else latest_snapshot
|
|
724
|
+
)
|
|
725
|
+
submission_snapshot = _augment_submission_snapshot(
|
|
726
|
+
base_submission_snapshot,
|
|
727
|
+
decision_depth,
|
|
728
|
+
quotes,
|
|
729
|
+
tick_size=float(tick_size),
|
|
730
|
+
fallback_snapshot=submission_fallback_snapshot,
|
|
731
|
+
)
|
|
732
|
+
working_orders = _submit_or_replace_quotes(
|
|
733
|
+
broker,
|
|
734
|
+
data,
|
|
735
|
+
working_orders,
|
|
736
|
+
quotes,
|
|
737
|
+
order_qty=_builder_order_qty(builder),
|
|
738
|
+
tick_size=float(tick_size),
|
|
739
|
+
snapshot=submission_snapshot,
|
|
740
|
+
activation_timestamp_ns=int(next_decision_ns),
|
|
741
|
+
)
|
|
742
|
+
decisions += 1
|
|
743
|
+
next_decision_ns += interval_ns
|
|
744
|
+
if max_decisions is not None and decisions >= max_decisions:
|
|
745
|
+
reached_limit = True
|
|
746
|
+
break
|
|
747
|
+
if channel_type == "orderbook":
|
|
748
|
+
depth_probe_timestamp_ns, event_depth = _advance_depth_probe(
|
|
749
|
+
depth_probe, depth_probe_timestamp_ns, event_ns
|
|
750
|
+
)
|
|
751
|
+
orderbook_event = _augment_orderbook_snapshot_for_orders(
|
|
752
|
+
event,
|
|
753
|
+
event_depth,
|
|
754
|
+
list(broker._orders_by_symbol.get(data.name, [])),
|
|
755
|
+
tick_size=float(tick_size),
|
|
756
|
+
)
|
|
757
|
+
latest_exchange_snapshot = orderbook_event
|
|
758
|
+
broker.process_orderbook(orderbook_event)
|
|
759
|
+
else:
|
|
760
|
+
decision_trades.append(event)
|
|
761
|
+
broker.process_tick(event)
|
|
762
|
+
if reached_limit:
|
|
763
|
+
break
|
|
764
|
+
|
|
765
|
+
fills = [
|
|
766
|
+
ComparisonFill(
|
|
767
|
+
side=item["side"],
|
|
768
|
+
price=float(item["price"]),
|
|
769
|
+
size=float(item["size"]),
|
|
770
|
+
timestamp_ns=int(item.get("timestamp_ns", 0)) or None,
|
|
771
|
+
local_timestamp_ns=int(item.get("timestamp_ns", 0)) or None,
|
|
772
|
+
exch_timestamp_ns=int(item.get("timestamp_ns", 0)) or None,
|
|
773
|
+
order_ref=int(item.get("order_ref")) if item.get("order_ref") is not None else None,
|
|
774
|
+
)
|
|
775
|
+
for item in broker.order_history
|
|
776
|
+
if item.get("status") in ("Partial", "Completed") and float(item.get("size", 0.0)) > 0.0
|
|
777
|
+
]
|
|
778
|
+
state = broker.state_values(data)
|
|
779
|
+
return EngineResult(
|
|
780
|
+
balance=float(state["balance"] - _BACKTRADER_COMPARISON_INITIAL_CASH + state["fee"]),
|
|
781
|
+
position=float(broker.getposition(data).size),
|
|
782
|
+
num_trades=len(fills),
|
|
783
|
+
fills=fills,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _run_hftbacktest_strategy(
|
|
788
|
+
market_data_path,
|
|
789
|
+
builder,
|
|
790
|
+
tick_size: float,
|
|
791
|
+
lot_size: float,
|
|
792
|
+
interval_ns: int,
|
|
793
|
+
maker_commission: float,
|
|
794
|
+
taker_commission: float,
|
|
795
|
+
queue_model_power: float,
|
|
796
|
+
max_decisions: Optional[int],
|
|
797
|
+
) -> EngineResult:
|
|
798
|
+
try:
|
|
799
|
+
from hftbacktest import BacktestAsset, HashMapMarketDepthBacktest
|
|
800
|
+
from hftbacktest.order import BUY, GTX, LIMIT, PARTIALLY_FILLED, SELL
|
|
801
|
+
except Exception as exc:
|
|
802
|
+
logger.error("binance_bbo_compare:800 re-raising Exception", exc_info=True)
|
|
803
|
+
raise RuntimeError("hftbacktest is required to run this comparison") from exc
|
|
804
|
+
|
|
805
|
+
asset = (
|
|
806
|
+
BacktestAsset()
|
|
807
|
+
.data([str(Path(market_data_path))])
|
|
808
|
+
.linear_asset(1.0)
|
|
809
|
+
.constant_order_latency(0, 0)
|
|
810
|
+
.power_prob_queue_model(float(queue_model_power))
|
|
811
|
+
.no_partial_fill_exchange()
|
|
812
|
+
.trading_value_fee_model(float(maker_commission), float(taker_commission))
|
|
813
|
+
.tick_size(float(tick_size))
|
|
814
|
+
.lot_size(float(lot_size))
|
|
815
|
+
)
|
|
816
|
+
hbt = HashMapMarketDepthBacktest([asset])
|
|
817
|
+
fills: list = []
|
|
818
|
+
seen_exec_qty: dict = {}
|
|
819
|
+
decisions = 0
|
|
820
|
+
|
|
821
|
+
while hbt.elapse(interval_ns) == 0:
|
|
822
|
+
depth = hbt.depth(0)
|
|
823
|
+
if not _is_finite_book(depth.best_bid, depth.best_ask):
|
|
824
|
+
continue
|
|
825
|
+
|
|
826
|
+
_collect_hft_fills(hbt.orders(0), seen_exec_qty, fills)
|
|
827
|
+
hbt.clear_inactive_orders(0)
|
|
828
|
+
last_trades = list(hbt.last_trades(0))
|
|
829
|
+
|
|
830
|
+
snapshot = SimpleNamespace(
|
|
831
|
+
bids=[(float(depth.best_bid), float(depth.best_bid_qty))],
|
|
832
|
+
asks=[(float(depth.best_ask), float(depth.best_ask_qty))],
|
|
833
|
+
)
|
|
834
|
+
quotes = builder(
|
|
835
|
+
float(hbt.position(0)),
|
|
836
|
+
snapshot,
|
|
837
|
+
{
|
|
838
|
+
"timestamp_ns": int(getattr(hbt, "current_timestamp", 0)),
|
|
839
|
+
"last_trades": tuple(last_trades),
|
|
840
|
+
},
|
|
841
|
+
)
|
|
842
|
+
if last_trades and hasattr(hbt, "clear_last_trades"):
|
|
843
|
+
hbt.clear_last_trades(0)
|
|
844
|
+
_replace_hft_orders(
|
|
845
|
+
hbt=hbt,
|
|
846
|
+
quotes=quotes,
|
|
847
|
+
tick_size=float(tick_size),
|
|
848
|
+
order_qty=_builder_order_qty(builder),
|
|
849
|
+
buy_flag=BUY,
|
|
850
|
+
sell_flag=SELL,
|
|
851
|
+
gtx_flag=GTX,
|
|
852
|
+
limit_flag=LIMIT,
|
|
853
|
+
partial_filled_flag=PARTIALLY_FILLED,
|
|
854
|
+
)
|
|
855
|
+
decisions += 1
|
|
856
|
+
if max_decisions is not None and decisions >= max_decisions:
|
|
857
|
+
break
|
|
858
|
+
|
|
859
|
+
_collect_hft_fills(hbt.orders(0), seen_exec_qty, fills)
|
|
860
|
+
state = hbt.state_values(0)
|
|
861
|
+
return EngineResult(
|
|
862
|
+
balance=float(state.balance),
|
|
863
|
+
position=float(state.position),
|
|
864
|
+
num_trades=int(state.num_trades),
|
|
865
|
+
fills=fills,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _iter_market_events(orderbook_path, tick_path, symbol: str) -> Iterator[tuple[str, object]]:
|
|
870
|
+
orderbooks = iter(OrderBookChannel(symbol=symbol, dataname=str(orderbook_path), depth=1).load())
|
|
871
|
+
ticks = iter(TickChannel(symbol=symbol, dataname=str(tick_path)).load())
|
|
872
|
+
next_orderbook = next(orderbooks, None)
|
|
873
|
+
next_tick = next(ticks, None)
|
|
874
|
+
while next_orderbook is not None or next_tick is not None:
|
|
875
|
+
if next_tick is None:
|
|
876
|
+
yield "orderbook", next_orderbook
|
|
877
|
+
next_orderbook = next(orderbooks, None)
|
|
878
|
+
continue
|
|
879
|
+
if next_orderbook is None:
|
|
880
|
+
yield "tick", next_tick
|
|
881
|
+
next_tick = next(ticks, None)
|
|
882
|
+
continue
|
|
883
|
+
if next_orderbook.timestamp <= next_tick.timestamp:
|
|
884
|
+
yield "orderbook", next_orderbook
|
|
885
|
+
next_orderbook = next(orderbooks, None)
|
|
886
|
+
else:
|
|
887
|
+
yield "tick", next_tick
|
|
888
|
+
next_tick = next(ticks, None)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _iter_exchange_market_events(market_data_path, symbol: str) -> Iterator[tuple[str, object]]:
|
|
892
|
+
with np.load(str(Path(market_data_path))) as payload:
|
|
893
|
+
data = payload["data"]
|
|
894
|
+
bid_price = None
|
|
895
|
+
ask_price = None
|
|
896
|
+
bid_qty = 0.0
|
|
897
|
+
ask_qty = 0.0
|
|
898
|
+
previous_bid_price = None
|
|
899
|
+
previous_ask_price = None
|
|
900
|
+
previous_bid_qty = 0.0
|
|
901
|
+
previous_ask_qty = 0.0
|
|
902
|
+
event_seq = 0
|
|
903
|
+
for row in data:
|
|
904
|
+
ev = int(row["ev"])
|
|
905
|
+
if not (ev & _EXCH_EVENT):
|
|
906
|
+
continue
|
|
907
|
+
timestamp = float(int(row["exch_ts"]) / 1_000_000_000.0)
|
|
908
|
+
if ev & _TRADE_EVENT:
|
|
909
|
+
event_seq += 1
|
|
910
|
+
yield (
|
|
911
|
+
"tick",
|
|
912
|
+
SimpleNamespace(
|
|
913
|
+
timestamp=timestamp,
|
|
914
|
+
timestamp_ns=int(row["exch_ts"]),
|
|
915
|
+
event_seq=event_seq,
|
|
916
|
+
symbol=symbol,
|
|
917
|
+
price=float(row["px"]),
|
|
918
|
+
volume=float(row["qty"]),
|
|
919
|
+
direction="buy" if (ev & _BUY_EVENT) else "sell",
|
|
920
|
+
bid_price=bid_price,
|
|
921
|
+
ask_price=ask_price,
|
|
922
|
+
bid_volume=bid_qty,
|
|
923
|
+
ask_volume=ask_qty,
|
|
924
|
+
),
|
|
925
|
+
)
|
|
926
|
+
if ev & _DEPTH_EVENT:
|
|
927
|
+
previous_bid_price = bid_price
|
|
928
|
+
previous_ask_price = ask_price
|
|
929
|
+
previous_bid_qty = bid_qty
|
|
930
|
+
previous_ask_qty = ask_qty
|
|
931
|
+
if ev & _BUY_EVENT:
|
|
932
|
+
bid_price = float(row["px"])
|
|
933
|
+
bid_qty = float(row["qty"])
|
|
934
|
+
elif ev & _SELL_EVENT:
|
|
935
|
+
ask_price = float(row["px"])
|
|
936
|
+
ask_qty = float(row["qty"])
|
|
937
|
+
if bid_price is None or ask_price is None:
|
|
938
|
+
continue
|
|
939
|
+
event_seq += 1
|
|
940
|
+
yield (
|
|
941
|
+
"orderbook",
|
|
942
|
+
SimpleNamespace(
|
|
943
|
+
timestamp=timestamp,
|
|
944
|
+
timestamp_ns=int(row["exch_ts"]),
|
|
945
|
+
event_seq=event_seq,
|
|
946
|
+
symbol=symbol,
|
|
947
|
+
previous_bids=(
|
|
948
|
+
[(previous_bid_price, previous_bid_qty)]
|
|
949
|
+
if previous_bid_price is not None
|
|
950
|
+
else []
|
|
951
|
+
),
|
|
952
|
+
previous_asks=(
|
|
953
|
+
[(previous_ask_price, previous_ask_qty)]
|
|
954
|
+
if previous_ask_price is not None
|
|
955
|
+
else []
|
|
956
|
+
),
|
|
957
|
+
bids=[(bid_price, bid_qty)],
|
|
958
|
+
asks=[(ask_price, ask_qty)],
|
|
959
|
+
),
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _advance_local_orderbook_snapshot(
|
|
964
|
+
latest_snapshot, next_orderbook, orderbooks, target_ts: float
|
|
965
|
+
):
|
|
966
|
+
while next_orderbook is not None and float(next_orderbook.timestamp) <= float(target_ts):
|
|
967
|
+
latest_snapshot = next_orderbook
|
|
968
|
+
next_orderbook = next(orderbooks, None)
|
|
969
|
+
return latest_snapshot, next_orderbook
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def _create_depth_probe(market_data_path, tick_size: float, lot_size: float):
|
|
973
|
+
try:
|
|
974
|
+
from hftbacktest import BacktestAsset, HashMapMarketDepthBacktest
|
|
975
|
+
except Exception:
|
|
976
|
+
logger.warning("binance_bbo_compare:974 fallback on Exception")
|
|
977
|
+
return None
|
|
978
|
+
|
|
979
|
+
asset = (
|
|
980
|
+
BacktestAsset()
|
|
981
|
+
.data([str(Path(market_data_path))])
|
|
982
|
+
.linear_asset(1.0)
|
|
983
|
+
.constant_order_latency(0, 0)
|
|
984
|
+
.power_prob_queue_model(2.0)
|
|
985
|
+
.no_partial_fill_exchange()
|
|
986
|
+
.trading_value_fee_model(0.0, 0.0)
|
|
987
|
+
.tick_size(float(tick_size))
|
|
988
|
+
.lot_size(float(lot_size))
|
|
989
|
+
)
|
|
990
|
+
return HashMapMarketDepthBacktest([asset])
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _augment_submission_snapshot(
|
|
994
|
+
base_snapshot, depth, quotes, tick_size: float, fallback_snapshot=None
|
|
995
|
+
):
|
|
996
|
+
if base_snapshot is None:
|
|
997
|
+
return None
|
|
998
|
+
if not getattr(base_snapshot, "bids", None) or not getattr(base_snapshot, "asks", None):
|
|
999
|
+
return base_snapshot
|
|
1000
|
+
base_best_bid = float(base_snapshot.bids[0][0])
|
|
1001
|
+
base_best_ask = float(base_snapshot.asks[0][0])
|
|
1002
|
+
depth_usable = False
|
|
1003
|
+
if depth is not None:
|
|
1004
|
+
probe_best_bid = float(depth.best_bid)
|
|
1005
|
+
probe_best_ask = float(depth.best_ask)
|
|
1006
|
+
if _is_finite_book(probe_best_bid, probe_best_ask):
|
|
1007
|
+
depth_usable = (
|
|
1008
|
+
abs(base_best_bid - probe_best_bid) <= 1e-12
|
|
1009
|
+
and abs(base_best_ask - probe_best_ask) <= 1e-12
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
bid_levels = [(float(price), float(qty)) for price, qty in base_snapshot.bids]
|
|
1013
|
+
ask_levels = [(float(price), float(qty)) for price, qty in base_snapshot.asks]
|
|
1014
|
+
seen_bid_ticks = {_price_tick(price, tick_size) for price, _ in bid_levels}
|
|
1015
|
+
seen_ask_ticks = {_price_tick(price, tick_size) for price, _ in ask_levels}
|
|
1016
|
+
|
|
1017
|
+
def _snapshot_level_qty(levels, target_tick: int) -> float:
|
|
1018
|
+
if not levels:
|
|
1019
|
+
return 0.0
|
|
1020
|
+
for level_price, level_qty in levels:
|
|
1021
|
+
if _price_tick(level_price, tick_size) == target_tick:
|
|
1022
|
+
return float(level_qty)
|
|
1023
|
+
return 0.0
|
|
1024
|
+
|
|
1025
|
+
for side, price_tick, price in _normalize_quotes(quotes, tick_size=tick_size):
|
|
1026
|
+
if side == "buy":
|
|
1027
|
+
if price_tick in seen_bid_ticks:
|
|
1028
|
+
continue
|
|
1029
|
+
qty = float(depth.bid_qty_at_tick(price_tick)) if depth_usable else 0.0
|
|
1030
|
+
if qty <= 0.0 and fallback_snapshot is not None:
|
|
1031
|
+
qty = _snapshot_level_qty(getattr(fallback_snapshot, "bids", None), price_tick)
|
|
1032
|
+
if qty > 0.0:
|
|
1033
|
+
bid_levels.append((price, qty))
|
|
1034
|
+
seen_bid_ticks.add(price_tick)
|
|
1035
|
+
continue
|
|
1036
|
+
if price_tick in seen_ask_ticks:
|
|
1037
|
+
continue
|
|
1038
|
+
qty = float(depth.ask_qty_at_tick(price_tick)) if depth_usable else 0.0
|
|
1039
|
+
if qty <= 0.0 and fallback_snapshot is not None:
|
|
1040
|
+
qty = _snapshot_level_qty(getattr(fallback_snapshot, "asks", None), price_tick)
|
|
1041
|
+
if qty > 0.0:
|
|
1042
|
+
ask_levels.append((price, qty))
|
|
1043
|
+
seen_ask_ticks.add(price_tick)
|
|
1044
|
+
|
|
1045
|
+
bid_levels.sort(key=lambda item: item[0], reverse=True)
|
|
1046
|
+
ask_levels.sort(key=lambda item: item[0])
|
|
1047
|
+
payload = dict(getattr(base_snapshot, "__dict__", {}))
|
|
1048
|
+
payload["bids"] = bid_levels
|
|
1049
|
+
payload["asks"] = ask_levels
|
|
1050
|
+
return SimpleNamespace(**payload)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _advance_depth_probe(
|
|
1054
|
+
depth_probe, current_timestamp_ns: Optional[int], target_timestamp_ns: int
|
|
1055
|
+
):
|
|
1056
|
+
if depth_probe is None:
|
|
1057
|
+
return current_timestamp_ns, None
|
|
1058
|
+
if current_timestamp_ns is None:
|
|
1059
|
+
current_timestamp_ns = int(getattr(depth_probe, "current_timestamp", 0) or 0)
|
|
1060
|
+
target_timestamp_ns = int(target_timestamp_ns)
|
|
1061
|
+
if target_timestamp_ns > current_timestamp_ns:
|
|
1062
|
+
status = depth_probe.elapse(target_timestamp_ns - current_timestamp_ns)
|
|
1063
|
+
current_timestamp_ns = target_timestamp_ns
|
|
1064
|
+
if status != 0:
|
|
1065
|
+
return current_timestamp_ns, None
|
|
1066
|
+
return current_timestamp_ns, depth_probe.depth(0)
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def _augment_orderbook_snapshot_for_orders(base_snapshot, depth, pending_orders, tick_size: float):
|
|
1070
|
+
if base_snapshot is None:
|
|
1071
|
+
return None
|
|
1072
|
+
if depth is None:
|
|
1073
|
+
return base_snapshot
|
|
1074
|
+
if not getattr(base_snapshot, "bids", None) or not getattr(base_snapshot, "asks", None):
|
|
1075
|
+
return base_snapshot
|
|
1076
|
+
base_best_bid = float(base_snapshot.bids[0][0])
|
|
1077
|
+
base_best_ask = float(base_snapshot.asks[0][0])
|
|
1078
|
+
probe_best_bid = float(depth.best_bid)
|
|
1079
|
+
probe_best_ask = float(depth.best_ask)
|
|
1080
|
+
if not _is_finite_book(probe_best_bid, probe_best_ask):
|
|
1081
|
+
return base_snapshot
|
|
1082
|
+
if abs(base_best_bid - probe_best_bid) > 1e-12 or abs(base_best_ask - probe_best_ask) > 1e-12:
|
|
1083
|
+
return base_snapshot
|
|
1084
|
+
|
|
1085
|
+
bid_levels = [(float(price), float(qty)) for price, qty in base_snapshot.bids]
|
|
1086
|
+
ask_levels = [(float(price), float(qty)) for price, qty in base_snapshot.asks]
|
|
1087
|
+
seen_bid_ticks = {_price_tick(price, tick_size) for price, _ in bid_levels}
|
|
1088
|
+
seen_ask_ticks = {_price_tick(price, tick_size) for price, _ in ask_levels}
|
|
1089
|
+
|
|
1090
|
+
for order in pending_orders:
|
|
1091
|
+
if getattr(order, "_fill_role", None) != FillRole.MAKER:
|
|
1092
|
+
continue
|
|
1093
|
+
price = getattr(order, "price", None)
|
|
1094
|
+
if price is None:
|
|
1095
|
+
continue
|
|
1096
|
+
price = float(price)
|
|
1097
|
+
price_tick = _price_tick(price, tick_size)
|
|
1098
|
+
if order.isbuy():
|
|
1099
|
+
if price_tick in seen_bid_ticks:
|
|
1100
|
+
continue
|
|
1101
|
+
qty = float(depth.bid_qty_at_tick(price_tick))
|
|
1102
|
+
if qty > 0.0:
|
|
1103
|
+
bid_levels.append((price, qty))
|
|
1104
|
+
seen_bid_ticks.add(price_tick)
|
|
1105
|
+
continue
|
|
1106
|
+
if price_tick in seen_ask_ticks:
|
|
1107
|
+
continue
|
|
1108
|
+
qty = float(depth.ask_qty_at_tick(price_tick))
|
|
1109
|
+
if qty > 0.0:
|
|
1110
|
+
ask_levels.append((price, qty))
|
|
1111
|
+
seen_ask_ticks.add(price_tick)
|
|
1112
|
+
|
|
1113
|
+
bid_levels.sort(key=lambda item: item[0], reverse=True)
|
|
1114
|
+
ask_levels.sort(key=lambda item: item[0])
|
|
1115
|
+
payload = dict(getattr(base_snapshot, "__dict__", {}))
|
|
1116
|
+
payload["bids"] = bid_levels
|
|
1117
|
+
payload["asks"] = ask_levels
|
|
1118
|
+
return SimpleNamespace(**payload)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _submit_or_replace_quotes(
|
|
1122
|
+
broker,
|
|
1123
|
+
data,
|
|
1124
|
+
working_orders,
|
|
1125
|
+
quotes,
|
|
1126
|
+
order_qty: float,
|
|
1127
|
+
tick_size: float,
|
|
1128
|
+
snapshot,
|
|
1129
|
+
activation_timestamp_ns: int | None = None,
|
|
1130
|
+
activation_event_seq: int | None = None,
|
|
1131
|
+
):
|
|
1132
|
+
working_orders = {
|
|
1133
|
+
key: order
|
|
1134
|
+
for key, order in working_orders.items()
|
|
1135
|
+
if order.alive() and order.status not in (Order.Canceled, Order.Rejected)
|
|
1136
|
+
}
|
|
1137
|
+
normalized = _normalize_quotes(quotes, tick_size=tick_size)
|
|
1138
|
+
target_keys = {(side, price_tick) for side, price_tick, _ in normalized}
|
|
1139
|
+
|
|
1140
|
+
for key in list(working_orders):
|
|
1141
|
+
if key in target_keys:
|
|
1142
|
+
continue
|
|
1143
|
+
broker.cancel(working_orders[key])
|
|
1144
|
+
working_orders.pop(key, None)
|
|
1145
|
+
|
|
1146
|
+
for side, price_tick, price in normalized:
|
|
1147
|
+
key = (side, price_tick)
|
|
1148
|
+
if key in working_orders:
|
|
1149
|
+
continue
|
|
1150
|
+
if side == "buy":
|
|
1151
|
+
order = broker.buy(
|
|
1152
|
+
owner=None, data=data, size=order_qty, price=price, exectype=Order.Limit
|
|
1153
|
+
)
|
|
1154
|
+
else:
|
|
1155
|
+
order = broker.sell(
|
|
1156
|
+
owner=None, data=data, size=order_qty, price=price, exectype=Order.Limit
|
|
1157
|
+
)
|
|
1158
|
+
order.time_in_force = "GTX"
|
|
1159
|
+
if activation_timestamp_ns is not None:
|
|
1160
|
+
order._active_after_timestamp_ns = int(activation_timestamp_ns)
|
|
1161
|
+
if activation_event_seq is not None:
|
|
1162
|
+
order._active_after_event_seq = int(activation_event_seq)
|
|
1163
|
+
if not _prime_backtrader_order(broker, order, snapshot):
|
|
1164
|
+
continue
|
|
1165
|
+
working_orders[key] = order
|
|
1166
|
+
return {
|
|
1167
|
+
key: order
|
|
1168
|
+
for key, order in working_orders.items()
|
|
1169
|
+
if order.alive() and order.status not in (Order.Canceled, Order.Rejected)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def _prime_backtrader_order(broker, order, snapshot) -> bool:
|
|
1174
|
+
if snapshot is None or broker._exchange_model is None:
|
|
1175
|
+
return True
|
|
1176
|
+
if order.exectype not in (Order.Market, Order.Limit):
|
|
1177
|
+
return True
|
|
1178
|
+
exchange_result = broker._exchange_model.on_new_order(order, snapshot)
|
|
1179
|
+
if exchange_result.action == "REJECT":
|
|
1180
|
+
broker._remove_pending_order(order)
|
|
1181
|
+
order.addinfo(reject_reason=exchange_result.reject_reason)
|
|
1182
|
+
order.reject(broker)
|
|
1183
|
+
broker.notify(order)
|
|
1184
|
+
return False
|
|
1185
|
+
if exchange_result.action == "FILL":
|
|
1186
|
+
fill_price, fill_size = broker._aggregate_exchange_fills(exchange_result.fills)
|
|
1187
|
+
if fill_size > 0:
|
|
1188
|
+
broker._execute(order, fill_price, fill_size, snapshot, source="orderbook_depth")
|
|
1189
|
+
broker._remove_pending_order(order)
|
|
1190
|
+
return False
|
|
1191
|
+
return True
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _process_backtrader_depth_crosses(broker, data, ob_event) -> None:
|
|
1195
|
+
best_bid = ob_event.bids[0][0] if ob_event.bids else None
|
|
1196
|
+
best_ask = ob_event.asks[0][0] if ob_event.asks else None
|
|
1197
|
+
matched = []
|
|
1198
|
+
for order in list(broker._orders_by_symbol.get(data.name, [])):
|
|
1199
|
+
if getattr(order, "_fill_role", None) != FillRole.MAKER:
|
|
1200
|
+
continue
|
|
1201
|
+
if order.status in (
|
|
1202
|
+
Order.Canceled,
|
|
1203
|
+
Order.Rejected,
|
|
1204
|
+
Order.Completed,
|
|
1205
|
+
Order.Expired,
|
|
1206
|
+
Order.Margin,
|
|
1207
|
+
):
|
|
1208
|
+
continue
|
|
1209
|
+
if order.isbuy():
|
|
1210
|
+
if best_ask is None or float(order.price) <= float(best_ask):
|
|
1211
|
+
continue
|
|
1212
|
+
else:
|
|
1213
|
+
if best_bid is None or float(order.price) >= float(best_bid):
|
|
1214
|
+
continue
|
|
1215
|
+
fill_size = broker._get_remaining_size(order)
|
|
1216
|
+
if fill_size <= 0:
|
|
1217
|
+
continue
|
|
1218
|
+
broker._execute(order, float(order.price), float(fill_size), ob_event, source="maker")
|
|
1219
|
+
matched.append(order)
|
|
1220
|
+
|
|
1221
|
+
for order in matched:
|
|
1222
|
+
broker._remove_pending_order(order)
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _replace_hft_orders(
|
|
1226
|
+
hbt,
|
|
1227
|
+
quotes,
|
|
1228
|
+
tick_size: float,
|
|
1229
|
+
order_qty: float,
|
|
1230
|
+
buy_flag,
|
|
1231
|
+
sell_flag,
|
|
1232
|
+
gtx_flag,
|
|
1233
|
+
limit_flag,
|
|
1234
|
+
partial_filled_flag,
|
|
1235
|
+
):
|
|
1236
|
+
target_keys = {
|
|
1237
|
+
(side, price_tick) for side, price_tick, _ in _normalize_quotes(quotes, tick_size=tick_size)
|
|
1238
|
+
}
|
|
1239
|
+
active_orders = {}
|
|
1240
|
+
values = hbt.orders(0).values()
|
|
1241
|
+
while True:
|
|
1242
|
+
order = values.next()
|
|
1243
|
+
if order is None:
|
|
1244
|
+
break
|
|
1245
|
+
side = "buy" if order.side == buy_flag else "sell"
|
|
1246
|
+
price_tick = int(round(float(order.price) / tick_size))
|
|
1247
|
+
active_orders[(side, price_tick)] = order
|
|
1248
|
+
if (side, price_tick) not in target_keys and order.cancellable:
|
|
1249
|
+
hbt.cancel(0, int(order.order_id), True)
|
|
1250
|
+
|
|
1251
|
+
for side, price_tick, price in _normalize_quotes(quotes, tick_size=tick_size):
|
|
1252
|
+
if (side, price_tick) in active_orders:
|
|
1253
|
+
continue
|
|
1254
|
+
order_id = _order_id(side, price_tick)
|
|
1255
|
+
if side == "buy":
|
|
1256
|
+
hbt.submit_buy_order(0, order_id, price, order_qty, gtx_flag, limit_flag, True)
|
|
1257
|
+
else:
|
|
1258
|
+
hbt.submit_sell_order(0, order_id, price, order_qty, gtx_flag, limit_flag, True)
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def _hft_order_key(order):
|
|
1262
|
+
return (
|
|
1263
|
+
int(order.order_id),
|
|
1264
|
+
int(getattr(order, "local_timestamp", 0)),
|
|
1265
|
+
int(getattr(order, "exch_timestamp", 0)),
|
|
1266
|
+
int(getattr(order, "side", 0)),
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def _collect_hft_fills(order_dict, seen_exec_qty, fills):
|
|
1271
|
+
values = order_dict.values()
|
|
1272
|
+
while True:
|
|
1273
|
+
order = values.next()
|
|
1274
|
+
if order is None:
|
|
1275
|
+
break
|
|
1276
|
+
key = _hft_order_key(order)
|
|
1277
|
+
exec_qty = float(order.exec_qty)
|
|
1278
|
+
previous = seen_exec_qty.get(key, 0.0)
|
|
1279
|
+
if exec_qty <= previous + 1e-12:
|
|
1280
|
+
continue
|
|
1281
|
+
fills.append(
|
|
1282
|
+
ComparisonFill(
|
|
1283
|
+
side="buy" if int(order.side) > 0 else "sell",
|
|
1284
|
+
price=float(order.exec_price),
|
|
1285
|
+
size=exec_qty - previous,
|
|
1286
|
+
timestamp_ns=int(getattr(order, "exch_timestamp", 0)) or None,
|
|
1287
|
+
local_timestamp_ns=int(getattr(order, "local_timestamp", 0)) or None,
|
|
1288
|
+
exch_timestamp_ns=int(getattr(order, "exch_timestamp", 0)) or None,
|
|
1289
|
+
order_ref=int(order.order_id),
|
|
1290
|
+
)
|
|
1291
|
+
)
|
|
1292
|
+
seen_exec_qty[key] = exec_qty
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def _normalize_quotes(quotes, tick_size: float = 0.01):
|
|
1296
|
+
normalized = []
|
|
1297
|
+
seen = set()
|
|
1298
|
+
for side, value in quotes.items():
|
|
1299
|
+
prices = value if isinstance(value, (list, tuple)) else [value]
|
|
1300
|
+
for price in prices:
|
|
1301
|
+
price_tick = _price_tick(float(price), tick_size)
|
|
1302
|
+
key = (side, price_tick)
|
|
1303
|
+
if key in seen:
|
|
1304
|
+
continue
|
|
1305
|
+
seen.add(key)
|
|
1306
|
+
normalized_price = round(price_tick * tick_size, 12)
|
|
1307
|
+
normalized.append((side, price_tick, normalized_price))
|
|
1308
|
+
return normalized
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def _price_tick(price: float, tick_size: float) -> int:
|
|
1312
|
+
return int(round(price / tick_size))
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def _order_id(side: str, price_tick: int) -> int:
|
|
1316
|
+
return price_tick if side == "buy" else 1_000_000_000 + price_tick
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _is_finite_book(best_bid: float, best_ask: float) -> bool:
|
|
1320
|
+
return float(best_bid) == float(best_bid) and float(best_ask) == float(best_ask)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def _event_timestamp_ns(event) -> int:
|
|
1324
|
+
timestamp_ns = getattr(event, "timestamp_ns", None)
|
|
1325
|
+
if timestamp_ns is not None:
|
|
1326
|
+
return int(timestamp_ns)
|
|
1327
|
+
timestamp = getattr(event, "local_time", None) or getattr(event, "timestamp", 0.0)
|
|
1328
|
+
return int(round(float(timestamp) * 1_000_000_000.0))
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def _market_data_exchange_anchor(market_data_path) -> Optional[float]:
|
|
1332
|
+
anchor_ns = _market_data_exchange_anchor_ns(market_data_path)
|
|
1333
|
+
if anchor_ns is None:
|
|
1334
|
+
return None
|
|
1335
|
+
return float(anchor_ns / 1_000_000_000.0)
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _market_data_exchange_anchor_ns(market_data_path) -> Optional[int]:
|
|
1339
|
+
try:
|
|
1340
|
+
with np.load(str(Path(market_data_path))) as payload:
|
|
1341
|
+
data = payload["data"]
|
|
1342
|
+
if len(data) == 0:
|
|
1343
|
+
return None
|
|
1344
|
+
return int(np.min(data["exch_ts"]))
|
|
1345
|
+
except Exception:
|
|
1346
|
+
logger.warning("binance_bbo_compare:1343 fallback on Exception")
|
|
1347
|
+
return None
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _market_data_exchange_book(market_data_path):
|
|
1351
|
+
try:
|
|
1352
|
+
with np.load(str(Path(market_data_path))) as payload:
|
|
1353
|
+
data = payload["data"]
|
|
1354
|
+
if len(data) == 0:
|
|
1355
|
+
return None
|
|
1356
|
+
mask = (data["ev"] & np.uint64(_EXCH_EVENT) != 0) & (
|
|
1357
|
+
data["ev"] & np.uint64(_DEPTH_EVENT) != 0
|
|
1358
|
+
)
|
|
1359
|
+
rows = data[mask]
|
|
1360
|
+
if len(rows) == 0:
|
|
1361
|
+
return None
|
|
1362
|
+
order = np.argsort(rows["exch_ts"], kind="mergesort")
|
|
1363
|
+
rows = rows[order]
|
|
1364
|
+
exch_ts = []
|
|
1365
|
+
best_bids = []
|
|
1366
|
+
best_asks = []
|
|
1367
|
+
best_bid = None
|
|
1368
|
+
best_ask = None
|
|
1369
|
+
for row in rows:
|
|
1370
|
+
ev = int(row["ev"])
|
|
1371
|
+
if ev & _BUY_EVENT:
|
|
1372
|
+
best_bid = float(row["px"])
|
|
1373
|
+
elif ev & _SELL_EVENT:
|
|
1374
|
+
best_ask = float(row["px"])
|
|
1375
|
+
if best_bid is None or best_ask is None:
|
|
1376
|
+
continue
|
|
1377
|
+
exch_ts.append(int(row["exch_ts"]))
|
|
1378
|
+
best_bids.append(best_bid)
|
|
1379
|
+
best_asks.append(best_ask)
|
|
1380
|
+
if not exch_ts:
|
|
1381
|
+
return None
|
|
1382
|
+
return (exch_ts, best_bids, best_asks)
|
|
1383
|
+
except Exception:
|
|
1384
|
+
logger.warning("binance_bbo_compare:1380 fallback on Exception")
|
|
1385
|
+
return None
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def _lookup_exchange_snapshot(exchange_book, timestamp_ns: int):
|
|
1389
|
+
if exchange_book is None:
|
|
1390
|
+
return None
|
|
1391
|
+
exch_ts, best_bids, best_asks = exchange_book
|
|
1392
|
+
index = bisect.bisect_right(exch_ts, int(timestamp_ns)) - 1
|
|
1393
|
+
if index < 0:
|
|
1394
|
+
return None
|
|
1395
|
+
return SimpleNamespace(
|
|
1396
|
+
bids=[(float(best_bids[index]), 0.0)],
|
|
1397
|
+
asks=[(float(best_asks[index]), 0.0)],
|
|
1398
|
+
)
|