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