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
backtrader/strategy.py
ADDED
|
@@ -0,0 +1,3655 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Strategy module - Base class for user-defined trading strategies.
|
|
3
|
+
|
|
4
|
+
This module provides the Strategy class which serves as the foundation for
|
|
5
|
+
all user-defined trading strategies in Backtrader. It handles order management,
|
|
6
|
+
position tracking, indicator integration, and the event-driven execution model.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- Order creation and management (buy, sell, close, cancel)
|
|
10
|
+
- Position tracking per data feed
|
|
11
|
+
- Integration with indicators and analyzers
|
|
12
|
+
- Event notifications (order, trade, data, timer)
|
|
13
|
+
- Support for multiple data feeds and timeframes
|
|
14
|
+
- Signal-based trading via SignalStrategy
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
Basic strategy implementation::
|
|
18
|
+
|
|
19
|
+
import backtrader as bt
|
|
20
|
+
|
|
21
|
+
class MyStrategy(bt.Strategy):
|
|
22
|
+
params = (('period', 20),)
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.sma = bt.indicators.SMA(period=self.p.period)
|
|
26
|
+
|
|
27
|
+
def next(self):
|
|
28
|
+
if self.data.close[0] > self.sma[0]:
|
|
29
|
+
self.buy()
|
|
30
|
+
elif self.data.close[0] < self.sma[0]:
|
|
31
|
+
self.sell()
|
|
32
|
+
|
|
33
|
+
Classes:
|
|
34
|
+
Strategy: Main base class for trading strategies.
|
|
35
|
+
SignalStrategy: Strategy subclass that responds to signal indicators.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
39
|
+
|
|
40
|
+
import collections
|
|
41
|
+
import copy
|
|
42
|
+
import datetime
|
|
43
|
+
import itertools
|
|
44
|
+
from typing import Optional
|
|
45
|
+
|
|
46
|
+
from .lineiterator import LineIterator, StrategyBase
|
|
47
|
+
from .lineroot import LineRoot, LineSingle
|
|
48
|
+
from .lineseries import LineSeriesStub
|
|
49
|
+
from .metabase import ItemCollection, OwnerContext, findowner
|
|
50
|
+
from .order import Order
|
|
51
|
+
from .parameters import make_legacy_parameter_accessor
|
|
52
|
+
from .position_modes import (
|
|
53
|
+
POSITION_MODE_DUAL_SIDE,
|
|
54
|
+
POSITION_OFFSET_CLOSE,
|
|
55
|
+
POSITION_SIDE_LONG,
|
|
56
|
+
POSITION_SIDE_SHORT,
|
|
57
|
+
normalize_position_mode,
|
|
58
|
+
normalize_position_side,
|
|
59
|
+
trade_key_from_order,
|
|
60
|
+
)
|
|
61
|
+
from .signal import (
|
|
62
|
+
SIGNAL_LONG,
|
|
63
|
+
SIGNAL_LONG_ANY,
|
|
64
|
+
SIGNAL_LONG_INV,
|
|
65
|
+
SIGNAL_LONGEXIT,
|
|
66
|
+
SIGNAL_LONGEXIT_ANY,
|
|
67
|
+
SIGNAL_LONGEXIT_INV,
|
|
68
|
+
SIGNAL_LONGSHORT,
|
|
69
|
+
SIGNAL_SHORT,
|
|
70
|
+
SIGNAL_SHORT_ANY,
|
|
71
|
+
SIGNAL_SHORT_INV,
|
|
72
|
+
SIGNAL_SHORTEXIT,
|
|
73
|
+
SIGNAL_SHORTEXIT_ANY,
|
|
74
|
+
SIGNAL_SHORTEXIT_INV,
|
|
75
|
+
)
|
|
76
|
+
from .sizers.fixedsize import FixedSize
|
|
77
|
+
from .trade import Trade
|
|
78
|
+
from .utils import AutoDictList, AutoOrderedDict
|
|
79
|
+
from .utils.log_message import SpdLogManager, get_logger
|
|
80
|
+
from .utils.py3 import MAXINT, filter, integer_types, iteritems, keys, map, string_types
|
|
81
|
+
|
|
82
|
+
logger = get_logger(__name__)
|
|
83
|
+
_INF = float("inf")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _set_current_datetime(line, value):
|
|
87
|
+
"""Set current datetime line slot directly when no bindings need propagation."""
|
|
88
|
+
try:
|
|
89
|
+
bindings = line.bindings
|
|
90
|
+
except AttributeError:
|
|
91
|
+
line[0] = value
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if bindings:
|
|
95
|
+
line[0] = value
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
idx = line._idx
|
|
100
|
+
array = line.array
|
|
101
|
+
except AttributeError:
|
|
102
|
+
line[0] = value
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if idx < 0:
|
|
106
|
+
line[0] = value
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
array[idx] = value if value >= 1.0 else 1.0
|
|
111
|
+
except IndexError:
|
|
112
|
+
line[0] = value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Strategy(StrategyBase):
|
|
116
|
+
"""Base class for user-defined trading strategies.
|
|
117
|
+
|
|
118
|
+
This class provides the core functionality for implementing trading
|
|
119
|
+
strategies including order management, position tracking, and event
|
|
120
|
+
handling. Users should subclass this to create custom strategies.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
env: Reference to the Cerebro environment.
|
|
124
|
+
cerebro: Alias for env.
|
|
125
|
+
broker: Reference to the broker for order execution.
|
|
126
|
+
datas: List of data feeds available to the strategy.
|
|
127
|
+
data: Shortcut to the first data feed (datas[0]).
|
|
128
|
+
position: Current position for the main data feed.
|
|
129
|
+
stats: Collection of observer instances.
|
|
130
|
+
analyzers: Collection of analyzer instances.
|
|
131
|
+
|
|
132
|
+
Methods to Override:
|
|
133
|
+
__init__: Initialize indicators and strategy state.
|
|
134
|
+
start: Called when the strategy starts running.
|
|
135
|
+
prenext: Called before minimum period is reached.
|
|
136
|
+
nextstart: Called once when minimum period is first reached.
|
|
137
|
+
next: Main strategy logic, called on each bar.
|
|
138
|
+
stop: Called when the strategy stops running.
|
|
139
|
+
notify_order: Receive order status notifications.
|
|
140
|
+
notify_trade: Receive trade notifications.
|
|
141
|
+
notify_data: Receive data feed notifications.
|
|
142
|
+
notify_timer: Receive timer notifications.
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
class MyStrategy(bt.Strategy):
|
|
146
|
+
params = (('period', 20),)
|
|
147
|
+
|
|
148
|
+
def __init__(self):
|
|
149
|
+
self.sma = bt.indicators.SMA(period=self.p.period)
|
|
150
|
+
|
|
151
|
+
def next(self):
|
|
152
|
+
if not self.position:
|
|
153
|
+
if self.data.close[0] > self.sma[0]:
|
|
154
|
+
self.buy()
|
|
155
|
+
else:
|
|
156
|
+
if self.data.close[0] < self.sma[0]:
|
|
157
|
+
self.close()
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
# Class-level storage for strategies
|
|
161
|
+
_indcol: dict = {}
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def _create_strategy_safely(cls, *args, **kwargs):
|
|
165
|
+
"""Safely create a strategy instance with proper parameter filtering.
|
|
166
|
+
|
|
167
|
+
Separates __new__ (parameter setup) from __init__ (data/indicator setup)
|
|
168
|
+
to ensure parameters are fully processed before __init__ runs.
|
|
169
|
+
"""
|
|
170
|
+
# __new__ processes all kwargs into _params_instance
|
|
171
|
+
instance = cls.__new__(cls, *args, **kwargs)
|
|
172
|
+
|
|
173
|
+
# __init__ handles data setup, clock, and user subclass init.
|
|
174
|
+
# Pass original kwargs so __init__ can filter out param kwargs itself.
|
|
175
|
+
if instance is not None:
|
|
176
|
+
Strategy.__init__(instance, *args, **kwargs)
|
|
177
|
+
|
|
178
|
+
return instance
|
|
179
|
+
|
|
180
|
+
def __new__(cls, *args, **kwargs):
|
|
181
|
+
"""Override __new__ to handle method renaming that was done in MetaStrategy"""
|
|
182
|
+
# CRITICAL: First call StrategyBase.__new__ to properly set up data arguments and lines
|
|
183
|
+
# This ensures strategies get their data arguments processed correctly
|
|
184
|
+
instance = super().__new__(cls, *args, **kwargs)
|
|
185
|
+
|
|
186
|
+
# Store the original kwargs for parameter processing
|
|
187
|
+
instance._strategy_init_kwargs = kwargs
|
|
188
|
+
|
|
189
|
+
# CRITICAL FIX: Manually set up parameters here since Strategy inherits from ParamsMixin
|
|
190
|
+
# But we need to ensure the kwargs from cerebro.addstrategy are properly processed
|
|
191
|
+
if hasattr(cls, "_params") and cls._params is not None:
|
|
192
|
+
params_cls = cls._params
|
|
193
|
+
param_names = set()
|
|
194
|
+
|
|
195
|
+
# Get all parameter names from the class
|
|
196
|
+
if hasattr(params_cls, "_getpairs"):
|
|
197
|
+
param_names.update(params_cls._getpairs().keys())
|
|
198
|
+
elif hasattr(params_cls, "_gettuple"):
|
|
199
|
+
param_names.update(key for key, value in params_cls._gettuple())
|
|
200
|
+
|
|
201
|
+
# Filter parameter kwargs
|
|
202
|
+
param_kwargs = {k: v for k, v in kwargs.items() if k in param_names}
|
|
203
|
+
|
|
204
|
+
# Create parameter instance
|
|
205
|
+
try:
|
|
206
|
+
instance._params_instance = params_cls()
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
logger.error("strategy:208 re-raising Exception", exc_info=True)
|
|
209
|
+
raise TypeError(
|
|
210
|
+
f"Failed to create params instance for {cls.__name__}: {exc}"
|
|
211
|
+
) from exc
|
|
212
|
+
|
|
213
|
+
# Set all parameter values - first defaults, then custom values
|
|
214
|
+
if hasattr(params_cls, "_getpairs"):
|
|
215
|
+
for key, value in params_cls._getpairs().items():
|
|
216
|
+
# Use custom value if provided, otherwise use default
|
|
217
|
+
final_value = param_kwargs.get(key, value)
|
|
218
|
+
setattr(instance._params_instance, key, final_value)
|
|
219
|
+
elif hasattr(params_cls, "_gettuple"):
|
|
220
|
+
for key, value in params_cls._gettuple():
|
|
221
|
+
# Use custom value if provided, otherwise use default
|
|
222
|
+
final_value = param_kwargs.get(key, value)
|
|
223
|
+
setattr(instance._params_instance, key, final_value)
|
|
224
|
+
|
|
225
|
+
# Set any extra parameters that were passed but not in the params definition
|
|
226
|
+
for key, value in param_kwargs.items():
|
|
227
|
+
if not hasattr(instance._params_instance, key):
|
|
228
|
+
setattr(instance._params_instance, key, value)
|
|
229
|
+
|
|
230
|
+
else:
|
|
231
|
+
instance._params_instance = make_legacy_parameter_accessor(
|
|
232
|
+
values=kwargs, name="ParamsInstance"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Create p property for parameter access
|
|
236
|
+
instance.p = instance._params_instance
|
|
237
|
+
|
|
238
|
+
# Handle method renaming like the old MetaStrategy.__new__ did
|
|
239
|
+
if hasattr(cls, "notify") and not hasattr(cls, "notify_order"):
|
|
240
|
+
cls.notify_order = cls.notify
|
|
241
|
+
delattr(cls, "notify")
|
|
242
|
+
if hasattr(cls, "notify_operation") and not hasattr(cls, "notify_trade"):
|
|
243
|
+
cls.notify_trade = cls.notify_operation
|
|
244
|
+
delattr(cls, "notify_operation")
|
|
245
|
+
|
|
246
|
+
# Register subclasses (from MetaStrategy.__init__)
|
|
247
|
+
if (
|
|
248
|
+
not getattr(cls, "aliased", False)
|
|
249
|
+
and cls.__name__ != "Strategy"
|
|
250
|
+
and not cls.__name__.startswith("_")
|
|
251
|
+
):
|
|
252
|
+
cls._indcol[cls.__name__] = cls
|
|
253
|
+
|
|
254
|
+
# Initialize critical attributes early (from MetaStrategy.donew and dopreinit)
|
|
255
|
+
# These need to be available before __init__ completes since methods might be called
|
|
256
|
+
from .cerebro import Cerebro
|
|
257
|
+
|
|
258
|
+
instance.env = instance.cerebro = cerebro = findowner(instance, Cerebro)
|
|
259
|
+
instance._id = cerebro._next_stid()
|
|
260
|
+
instance.broker = instance.env.broker
|
|
261
|
+
from .sizers import FixedSize
|
|
262
|
+
|
|
263
|
+
instance._sizer = FixedSize()
|
|
264
|
+
|
|
265
|
+
instance.stats = instance.observers = ItemCollection()
|
|
266
|
+
instance.analyzers = ItemCollection()
|
|
267
|
+
instance._alnames = collections.defaultdict(itertools.count)
|
|
268
|
+
instance.writers = []
|
|
269
|
+
instance._slave_analyzers = []
|
|
270
|
+
instance._tradehistoryon = False
|
|
271
|
+
instance._orders = []
|
|
272
|
+
instance._orderspending = []
|
|
273
|
+
instance._trades = collections.defaultdict(AutoDictList)
|
|
274
|
+
instance._tradespending = []
|
|
275
|
+
|
|
276
|
+
return instance
|
|
277
|
+
|
|
278
|
+
def __reduce_ex__(self, protocol):
|
|
279
|
+
"""Restore saved state without re-entering the run-time owner setup.
|
|
280
|
+
|
|
281
|
+
Optimization results are unpickled outside an active OwnerContext.
|
|
282
|
+
Calling Strategy.__new__ there would request a new strategy ID from
|
|
283
|
+
a nonexistent Cerebro and kill the multiprocessing result thread.
|
|
284
|
+
Keep the default state/slot handling and any user-defined reducer,
|
|
285
|
+
changing only the allocation of an otherwise ordinary instance.
|
|
286
|
+
``object.__new__`` also resolves in older framework installations.
|
|
287
|
+
"""
|
|
288
|
+
reduced = super().__reduce_ex__(protocol)
|
|
289
|
+
if type(self).__reduce__ is not object.__reduce__:
|
|
290
|
+
return reduced
|
|
291
|
+
return (object.__new__, (type(self),), *reduced[2:])
|
|
292
|
+
|
|
293
|
+
def __init__(self, *args, **kwargs):
|
|
294
|
+
"""Initialize with functionality from MetaStrategy methods"""
|
|
295
|
+
# Critical attributes already initialized in __new__
|
|
296
|
+
# Handle the functionality that was in MetaStrategy.dopostinit
|
|
297
|
+
self._sizer.set(self, self.broker)
|
|
298
|
+
|
|
299
|
+
# OPTIMIZED: Simple and fast data extraction from args
|
|
300
|
+
# Cerebro passes datas at the beginning of args (cerebro.py:1433)
|
|
301
|
+
if not hasattr(self, "datas") or not self.datas:
|
|
302
|
+
self.datas = []
|
|
303
|
+
|
|
304
|
+
# Quick method: Extract datas directly from args
|
|
305
|
+
# Cerebro prepends all datas to args, so we just need to identify them
|
|
306
|
+
if args:
|
|
307
|
+
for arg in args:
|
|
308
|
+
# Fast check: data feeds have 'lines' and 'datetime' attributes
|
|
309
|
+
if hasattr(arg, "lines") and hasattr(arg, "datetime"):
|
|
310
|
+
self.datas.append(arg)
|
|
311
|
+
# No need for nested loops or complex checks
|
|
312
|
+
|
|
313
|
+
# Fallback: Try cerebro.datas directly (fast)
|
|
314
|
+
if not self.datas and hasattr(self, "cerebro") and self.cerebro is not None:
|
|
315
|
+
if hasattr(self.cerebro, "datas") and self.cerebro.datas:
|
|
316
|
+
self.datas = list(self.cerebro.datas)
|
|
317
|
+
|
|
318
|
+
# Set up primary data reference and data0/data1 aliases
|
|
319
|
+
if self.datas:
|
|
320
|
+
self.data = self.datas[0]
|
|
321
|
+
for d, data in enumerate(self.datas):
|
|
322
|
+
setattr(self, f"data{d}", data)
|
|
323
|
+
else:
|
|
324
|
+
self.data = None
|
|
325
|
+
self._data_assignment_pending = False
|
|
326
|
+
|
|
327
|
+
# Set up clock - this is critical for strategy execution
|
|
328
|
+
if not hasattr(self, "_clock") or self._clock is None:
|
|
329
|
+
if self.datas:
|
|
330
|
+
self._clock = self.datas[0]
|
|
331
|
+
else:
|
|
332
|
+
self._clock = None
|
|
333
|
+
# CRITICAL FIX: Don't create MinimalClock fallback
|
|
334
|
+
# It causes problems with indicator clock detection in _periodset()
|
|
335
|
+
# If no datas, leave _clock as None and let it be set later
|
|
336
|
+
|
|
337
|
+
# Call user subclass __init__ if this is a Strategy subclass
|
|
338
|
+
if self.__class__ != Strategy:
|
|
339
|
+
from backtrader.lineiterator import StrategyBase
|
|
340
|
+
|
|
341
|
+
# Guard against recursive calls when user's __init__ calls super().__init__()
|
|
342
|
+
if not getattr(self, "_user_init_called", False):
|
|
343
|
+
self._user_init_called = True
|
|
344
|
+
|
|
345
|
+
for cls in self.__class__.__mro__:
|
|
346
|
+
if (
|
|
347
|
+
cls not in (Strategy, StrategyBase)
|
|
348
|
+
and hasattr(cls, "__init__")
|
|
349
|
+
and "__init__" in cls.__dict__
|
|
350
|
+
):
|
|
351
|
+
# Use _original_init if available to avoid calling patched_init
|
|
352
|
+
# (prevents infinite recursion when ParamsMixin patches __init__)
|
|
353
|
+
if hasattr(cls, "_original_init"):
|
|
354
|
+
user_init = cls._original_init
|
|
355
|
+
else:
|
|
356
|
+
user_init = cls.__dict__["__init__"]
|
|
357
|
+
# Use OwnerContext so indicators find this strategy as owner
|
|
358
|
+
with OwnerContext.set_owner(self):
|
|
359
|
+
user_init(self)
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
# Initialize tick/channel callback state (auto, no manual init needed)
|
|
363
|
+
if not hasattr(self, "_tick_count"):
|
|
364
|
+
self._tick_count = 0
|
|
365
|
+
self._event_count = 0
|
|
366
|
+
self._last_tick = {}
|
|
367
|
+
self._last_ob = {}
|
|
368
|
+
self._last_funding = {}
|
|
369
|
+
if not hasattr(self, "_hft_data_refs"):
|
|
370
|
+
self._hft_data_refs = {}
|
|
371
|
+
|
|
372
|
+
# Initialize critical attributes that are expected by strategy execution
|
|
373
|
+
# These should be available before any user code runs
|
|
374
|
+
if not hasattr(self, "_dlens"):
|
|
375
|
+
self._dlens = [len(data) for data in self.datas]
|
|
376
|
+
|
|
377
|
+
# CRITICAL FIX: DO NOT call super().__init__() here!
|
|
378
|
+
# StrategyBase.__init__ already calls super().__init__() which eventually
|
|
379
|
+
# calls Strategy.__init__. Calling super() again would create infinite recursion.
|
|
380
|
+
# The parent initialization is already done by StrategyBase.
|
|
381
|
+
|
|
382
|
+
# Clean up the temporary attribute
|
|
383
|
+
if hasattr(self, "_strategy_init_kwargs"):
|
|
384
|
+
delattr(self, "_strategy_init_kwargs")
|
|
385
|
+
|
|
386
|
+
# Line type is strategy type
|
|
387
|
+
_ltype = LineIterator.StratType
|
|
388
|
+
# CSV default is True
|
|
389
|
+
csv = True
|
|
390
|
+
# Old clock update methodology, default is False
|
|
391
|
+
_oldsync = False # update the clock using old methodology: data 0
|
|
392
|
+
|
|
393
|
+
# Keep the latest delivered data date in the line
|
|
394
|
+
lines = ("datetime",)
|
|
395
|
+
|
|
396
|
+
def log(self, txt, dt=None, level="info"):
|
|
397
|
+
"""Log a message with optional datetime.
|
|
398
|
+
|
|
399
|
+
This method provides basic logging functionality. For comprehensive
|
|
400
|
+
logging including orders, trades, positions, indicators, and signals,
|
|
401
|
+
use the TradeLogger observer.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
txt: The message text to log.
|
|
405
|
+
dt: Optional datetime. If None, uses current bar datetime.
|
|
406
|
+
level: Log level ('info', 'warning', 'error', 'debug').
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
>>> self.log(f'Close price: {self.data.close[0]:.2f}')
|
|
410
|
+
>>> self.log('Warning message', level='warning')
|
|
411
|
+
"""
|
|
412
|
+
dt = dt or self.datetime.datetime()
|
|
413
|
+
print(f"[{dt}] {txt}")
|
|
414
|
+
|
|
415
|
+
def _notify_signal_to_observers(self, action, size, price, data=None, reason=None):
|
|
416
|
+
"""Notify all TradeLogger observers about a trading signal.
|
|
417
|
+
|
|
418
|
+
This is called automatically by buy() and sell() methods to record
|
|
419
|
+
signals without requiring user intervention.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
action: 'buy' or 'sell'
|
|
423
|
+
size: Order size
|
|
424
|
+
price: Signal price
|
|
425
|
+
data: Data feed (optional)
|
|
426
|
+
reason: Signal reason (optional)
|
|
427
|
+
"""
|
|
428
|
+
# Notify all observers that have log_signal method (TradeLogger)
|
|
429
|
+
if hasattr(self, "stats") and self.stats:
|
|
430
|
+
for observer in self.stats:
|
|
431
|
+
if hasattr(observer, "log_signal"):
|
|
432
|
+
data_name = getattr(data, "_name", None) if data else None
|
|
433
|
+
observer.log_signal(action, size, price, data_name, reason)
|
|
434
|
+
|
|
435
|
+
def _notify_order_to_observers(self, order):
|
|
436
|
+
"""Notify all observers about an order status change.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
order: The order object with status change
|
|
440
|
+
"""
|
|
441
|
+
if hasattr(self, "stats") and self.stats:
|
|
442
|
+
for observer in self.stats:
|
|
443
|
+
if hasattr(observer, "notify_order"):
|
|
444
|
+
observer.notify_order(order)
|
|
445
|
+
|
|
446
|
+
def _notify_trade_to_observers(self, trade):
|
|
447
|
+
"""Notify all observers about a trade.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
trade: The trade object
|
|
451
|
+
"""
|
|
452
|
+
if hasattr(self, "stats") and self.stats:
|
|
453
|
+
for observer in self.stats:
|
|
454
|
+
if hasattr(observer, "notify_trade"):
|
|
455
|
+
observer.notify_trade(trade)
|
|
456
|
+
|
|
457
|
+
def _notify_store_to_observers(self, msg, *args, **kwargs):
|
|
458
|
+
"""Forward store notifications to observers that support runtime events."""
|
|
459
|
+
if hasattr(self, "stats") and self.stats:
|
|
460
|
+
for observer in self.stats:
|
|
461
|
+
if hasattr(observer, "notify_store_event"):
|
|
462
|
+
observer.notify_store_event(msg, *args, **kwargs)
|
|
463
|
+
|
|
464
|
+
def _notify_data_to_observers(self, data, status, *args, **kwargs):
|
|
465
|
+
"""Forward data-feed notifications to observers that support runtime events."""
|
|
466
|
+
if hasattr(self, "stats") and self.stats:
|
|
467
|
+
for observer in self.stats:
|
|
468
|
+
if hasattr(observer, "notify_data_event"):
|
|
469
|
+
observer.notify_data_event(data, status, *args, **kwargs)
|
|
470
|
+
|
|
471
|
+
def _notify_tick_to_observers(self, tick):
|
|
472
|
+
"""Forward tick events to observers that support tick logging."""
|
|
473
|
+
if hasattr(self, "stats") and self.stats:
|
|
474
|
+
for observer in self.stats:
|
|
475
|
+
if hasattr(observer, "notify_tick_event"):
|
|
476
|
+
observer.notify_tick_event(tick)
|
|
477
|
+
|
|
478
|
+
def _notify_bar_to_observers(self, bar):
|
|
479
|
+
"""Forward bar events to observers that support bar logging."""
|
|
480
|
+
if hasattr(self, "stats") and self.stats:
|
|
481
|
+
for observer in self.stats:
|
|
482
|
+
if hasattr(observer, "notify_bar_event"):
|
|
483
|
+
observer.notify_bar_event(bar)
|
|
484
|
+
|
|
485
|
+
def qbuffer(self, savemem=0, replaying=False):
|
|
486
|
+
"""Enable the memory saving schemes. Possible values for ``savemem``:
|
|
487
|
+
|
|
488
|
+
0: No savings. Each line object keeps in memory all values
|
|
489
|
+
|
|
490
|
+
1: All lines objects save memory, using the strict minimum needed
|
|
491
|
+
|
|
492
|
+
Negative values are meant to be used when plotting is required:
|
|
493
|
+
|
|
494
|
+
-1: Indicators at Strategy Level and Observers do not enable memory
|
|
495
|
+
savings (but anything declared below it does)
|
|
496
|
+
|
|
497
|
+
-2: Same as -1 plus activation of memory saving for any indicators
|
|
498
|
+
which has declared *plotinfo.plot* as False (will not be plotted)
|
|
499
|
+
"""
|
|
500
|
+
# If savemem < 0
|
|
501
|
+
if savemem < 0:
|
|
502
|
+
# Get any attribute that labels itself as Indicator
|
|
503
|
+
for ind in self._lineiterators[self.IndType]:
|
|
504
|
+
# Check if this ind is a single line
|
|
505
|
+
subsave = isinstance(ind, (LineSingle,))
|
|
506
|
+
# If not a single line and savemem == -2, check plotinfo.plot
|
|
507
|
+
if not subsave and savemem < -1:
|
|
508
|
+
subsave = not ind.plotinfo.plot
|
|
509
|
+
# Apply memory saving based on subsave flag
|
|
510
|
+
ind.qbuffer(savemem=subsave)
|
|
511
|
+
# If savemem > 0
|
|
512
|
+
elif savemem > 0:
|
|
513
|
+
# Apply memory saving to all data feeds
|
|
514
|
+
for data in self.datas:
|
|
515
|
+
data.qbuffer(replaying=replaying)
|
|
516
|
+
# Apply memory saving to all lines
|
|
517
|
+
for line in self.lines:
|
|
518
|
+
line.qbuffer(savemem=1)
|
|
519
|
+
# Apply memory saving to all lineiterators based on the strategy
|
|
520
|
+
for itcls in self._lineiterators:
|
|
521
|
+
for it in self._lineiterators[itcls]:
|
|
522
|
+
it.qbuffer(savemem=1)
|
|
523
|
+
# If savemem == 0, no action needed
|
|
524
|
+
else:
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
def _iter_strategy_lineactions(self):
|
|
528
|
+
"""Yield LineActions stored as strategy attributes."""
|
|
529
|
+
from .linebuffer import LineActions
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
cache = object.__getattribute__(self, "_strategy_lineactions_cache")
|
|
533
|
+
except AttributeError:
|
|
534
|
+
cache = None
|
|
535
|
+
|
|
536
|
+
if cache is not None:
|
|
537
|
+
yield from cache
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
seen = set()
|
|
541
|
+
registered = set()
|
|
542
|
+
|
|
543
|
+
def mark_registered(lineiter):
|
|
544
|
+
lineiter_id = id(lineiter)
|
|
545
|
+
if lineiter_id in registered:
|
|
546
|
+
return
|
|
547
|
+
registered.add(lineiter_id)
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
child_lists = lineiter._lineiterators.values()
|
|
551
|
+
except AttributeError:
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
for children in child_lists:
|
|
555
|
+
for child in children:
|
|
556
|
+
mark_registered(child)
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
for children in self._lineiterators.values():
|
|
560
|
+
for child in children:
|
|
561
|
+
mark_registered(child)
|
|
562
|
+
except AttributeError:
|
|
563
|
+
# No sub-iterator registry yet; nothing to pre-mark as registered.
|
|
564
|
+
logger.debug("strategy:548 ignored AttributeError")
|
|
565
|
+
|
|
566
|
+
def visit(value):
|
|
567
|
+
value_id = id(value)
|
|
568
|
+
if value_id in seen:
|
|
569
|
+
return
|
|
570
|
+
seen.add(value_id)
|
|
571
|
+
|
|
572
|
+
if isinstance(value, LineActions):
|
|
573
|
+
for attr_name in ("_parent_a", "_parent_b", "a", "b", "cond"):
|
|
574
|
+
try:
|
|
575
|
+
dependency = getattr(value, attr_name)
|
|
576
|
+
except AttributeError:
|
|
577
|
+
logger.debug("strategy:561 ignored AttributeError")
|
|
578
|
+
continue
|
|
579
|
+
yield from visit(dependency)
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
args = value.args
|
|
583
|
+
except AttributeError:
|
|
584
|
+
args = ()
|
|
585
|
+
for dependency in args:
|
|
586
|
+
yield from visit(dependency)
|
|
587
|
+
|
|
588
|
+
if value_id not in registered:
|
|
589
|
+
yield value
|
|
590
|
+
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
if isinstance(value, dict):
|
|
594
|
+
for item in value.values():
|
|
595
|
+
yield from visit(item)
|
|
596
|
+
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
597
|
+
for item in value:
|
|
598
|
+
yield from visit(item)
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
attrs = object.__getattribute__(self, "__dict__")
|
|
602
|
+
except AttributeError:
|
|
603
|
+
attrs = {}
|
|
604
|
+
|
|
605
|
+
result = []
|
|
606
|
+
for attr_name, attr_value in attrs.items():
|
|
607
|
+
if attr_name in {"_strategy_lineactions_cache", "_strategy_next_lineactions_cache"}:
|
|
608
|
+
continue
|
|
609
|
+
result.extend(visit(attr_value))
|
|
610
|
+
|
|
611
|
+
cache = tuple(result)
|
|
612
|
+
object.__setattr__(self, "_strategy_lineactions_cache", cache)
|
|
613
|
+
yield from cache
|
|
614
|
+
|
|
615
|
+
def _get_strategy_lineactions(self):
|
|
616
|
+
try:
|
|
617
|
+
return object.__getattribute__(self, "_strategy_lineactions_cache")
|
|
618
|
+
except AttributeError:
|
|
619
|
+
return tuple(self._iter_strategy_lineactions())
|
|
620
|
+
|
|
621
|
+
def _get_strategy_next_lineactions(self):
|
|
622
|
+
try:
|
|
623
|
+
return object.__getattribute__(self, "_strategy_next_lineactions_cache")
|
|
624
|
+
except AttributeError:
|
|
625
|
+
# Cache not built yet; compute and store it below.
|
|
626
|
+
logger.debug("strategy:609 ignored AttributeError")
|
|
627
|
+
|
|
628
|
+
cache = tuple(
|
|
629
|
+
(lineaction, getattr(lineaction, "_clock", None))
|
|
630
|
+
for lineaction in self._get_strategy_lineactions()
|
|
631
|
+
if hasattr(lineaction, "_next")
|
|
632
|
+
)
|
|
633
|
+
object.__setattr__(self, "_strategy_next_lineactions_cache", cache)
|
|
634
|
+
return cache
|
|
635
|
+
|
|
636
|
+
def _stage2(self):
|
|
637
|
+
super()._stage2()
|
|
638
|
+
for lineaction in self._get_strategy_lineactions():
|
|
639
|
+
try:
|
|
640
|
+
lineaction._stage2()
|
|
641
|
+
except Exception:
|
|
642
|
+
logger.debug("Failed to stage2 strategy LineActions", exc_info=True)
|
|
643
|
+
|
|
644
|
+
def _stage1(self):
|
|
645
|
+
super()._stage1()
|
|
646
|
+
for lineaction in self._get_strategy_lineactions():
|
|
647
|
+
try:
|
|
648
|
+
lineaction._stage1()
|
|
649
|
+
except Exception:
|
|
650
|
+
logger.debug("Failed to stage1 strategy LineActions", exc_info=True)
|
|
651
|
+
|
|
652
|
+
def _next_strategy_lineactions(self):
|
|
653
|
+
"""Advance LineActions stored directly on the strategy.
|
|
654
|
+
|
|
655
|
+
LineActions created as strategy attributes, such as bt.Cmp/bt.If/bt.Max,
|
|
656
|
+
are not LineIterator indicators. They still need to be evaluated before
|
|
657
|
+
user next/prenext/nextstart reads them.
|
|
658
|
+
"""
|
|
659
|
+
for lineaction, clock in self._get_strategy_next_lineactions():
|
|
660
|
+
if clock is not None:
|
|
661
|
+
try:
|
|
662
|
+
if len(clock) <= len(lineaction):
|
|
663
|
+
try:
|
|
664
|
+
current_value = lineaction[0]
|
|
665
|
+
except Exception: # nosec B112
|
|
666
|
+
# Per-bar hot path: the line buffer may not be
|
|
667
|
+
# populated yet for this index. Skipping is the
|
|
668
|
+
# intended behaviour; logging here would fire every
|
|
669
|
+
# bar. No control-flow change.
|
|
670
|
+
continue
|
|
671
|
+
if current_value == current_value:
|
|
672
|
+
continue
|
|
673
|
+
lineaction.next()
|
|
674
|
+
continue
|
|
675
|
+
except Exception: # nosec B110
|
|
676
|
+
# Clock/value comparison unavailable; fall back to plain
|
|
677
|
+
# _next(). Hot path, intentionally silent (no logging).
|
|
678
|
+
pass
|
|
679
|
+
|
|
680
|
+
lineaction._next()
|
|
681
|
+
|
|
682
|
+
def _periodset(self):
|
|
683
|
+
"""Calculate and set the minimum period required for strategy execution.
|
|
684
|
+
|
|
685
|
+
This method determines the minimum number of bars needed before
|
|
686
|
+
the strategy's next() method can be called, based on the minimum
|
|
687
|
+
periods of all indicators and data feeds.
|
|
688
|
+
"""
|
|
689
|
+
|
|
690
|
+
def iter_indicator_tree(indicators):
|
|
691
|
+
seen = set()
|
|
692
|
+
stack = list(indicators)
|
|
693
|
+
while stack:
|
|
694
|
+
lineiter = stack.pop(0)
|
|
695
|
+
lineiter_id = id(lineiter)
|
|
696
|
+
if lineiter_id in seen:
|
|
697
|
+
continue
|
|
698
|
+
seen.add(lineiter_id)
|
|
699
|
+
yield lineiter
|
|
700
|
+
|
|
701
|
+
try:
|
|
702
|
+
children = lineiter._lineiterators[LineIterator.IndType]
|
|
703
|
+
except (AttributeError, KeyError):
|
|
704
|
+
logger.debug("strategy:687 ignored AttributeError,KeyError")
|
|
705
|
+
continue
|
|
706
|
+
stack.extend(children)
|
|
707
|
+
|
|
708
|
+
# Data IDs
|
|
709
|
+
dataids = [id(data) for data in self.datas]
|
|
710
|
+
# Data minimum periods
|
|
711
|
+
_dminperiods = collections.defaultdict(list)
|
|
712
|
+
# Loop through all indicators
|
|
713
|
+
all_indicators = list(iter_indicator_tree(self._lineiterators[LineIterator.IndType]))
|
|
714
|
+
|
|
715
|
+
# CRITICAL FIX: bind secondary-feed indicator advance clocks now that
|
|
716
|
+
# the whole indicator tree is built. During construction, an indicator
|
|
717
|
+
# built on another indicator or a LinesOperation that follows a
|
|
718
|
+
# non-primary feed (e.g. SMA((h1.high + h1.low) / 2.0) or
|
|
719
|
+
# EMA(EMA(h4.close))) often has its clock defaulted to the strategy's
|
|
720
|
+
# primary feed because the parent clocks were not yet finalized. That
|
|
721
|
+
# makes the indicator warm up and emit values on the primary (fast)
|
|
722
|
+
# clock instead of the secondary (slow) feed in runonce mode. Here every
|
|
723
|
+
# clock is final, so we resolve each indicator's data dependency to the
|
|
724
|
+
# concrete feed it follows and, when that feed is a *secondary* feed,
|
|
725
|
+
# pin _resolved_secondary_clock to it (used by the runonce advance
|
|
726
|
+
# loop and Indicator.advance). See docs/DEV_REGRESSION_FAILURES.md.
|
|
727
|
+
from .lineiterator import _line_like_source_clock as _llsc
|
|
728
|
+
|
|
729
|
+
primary_feed = self.datas[0] if self.datas else None
|
|
730
|
+
|
|
731
|
+
# Map each feed's individual lines back to the owning feed so a clock
|
|
732
|
+
# that resolves to a feed *line* (e.g. data.high) can be attributed to
|
|
733
|
+
# its feed.
|
|
734
|
+
_line_to_feed = {}
|
|
735
|
+
for _data in self.datas:
|
|
736
|
+
_dlines = getattr(_data, "lines", None)
|
|
737
|
+
if _dlines is None:
|
|
738
|
+
continue
|
|
739
|
+
try:
|
|
740
|
+
for _ln in _dlines:
|
|
741
|
+
_line_to_feed[id(_ln)] = _data
|
|
742
|
+
except TypeError:
|
|
743
|
+
# Feed lines not iterable; skip mapping this feed's lines.
|
|
744
|
+
logger.debug("strategy:726 ignored TypeError")
|
|
745
|
+
|
|
746
|
+
def _feed_of(node, _seen=None):
|
|
747
|
+
"""Resolve a data node to the concrete feed it ultimately follows."""
|
|
748
|
+
if _seen is None:
|
|
749
|
+
_seen = set()
|
|
750
|
+
if node is None or id(node) in _seen:
|
|
751
|
+
return None
|
|
752
|
+
_seen.add(id(node))
|
|
753
|
+
if id(node) in dataids:
|
|
754
|
+
return node
|
|
755
|
+
if id(node) in _line_to_feed:
|
|
756
|
+
return _line_to_feed[id(node)]
|
|
757
|
+
try:
|
|
758
|
+
src = _llsc(node)
|
|
759
|
+
except Exception:
|
|
760
|
+
logger.warning("strategy:745 fallback on Exception")
|
|
761
|
+
src = None
|
|
762
|
+
if src is not None and src is not node:
|
|
763
|
+
if id(src) in dataids:
|
|
764
|
+
return src
|
|
765
|
+
if id(src) in _line_to_feed:
|
|
766
|
+
return _line_to_feed[id(src)]
|
|
767
|
+
found = _feed_of(src, _seen)
|
|
768
|
+
if found is not None:
|
|
769
|
+
return found
|
|
770
|
+
nxt = getattr(node, "_clock", None)
|
|
771
|
+
if nxt is not None and nxt.__class__.__name__ != "MinimalClock":
|
|
772
|
+
found = _feed_of(nxt, _seen)
|
|
773
|
+
if found is not None:
|
|
774
|
+
return found
|
|
775
|
+
ndatas = getattr(node, "datas", None)
|
|
776
|
+
if ndatas:
|
|
777
|
+
found = _feed_of(ndatas[0], _seen)
|
|
778
|
+
if found is not None:
|
|
779
|
+
return found
|
|
780
|
+
# Walk owner references: a node may be an indicator output line
|
|
781
|
+
# (LineBuffer) whose owning indicator follows the target feed. The
|
|
782
|
+
# owner can be the indicator directly, or a Lines container whose
|
|
783
|
+
# _owner_ref points to it.
|
|
784
|
+
for owner_attr in ("_owner", "_owner_ref"):
|
|
785
|
+
owner = getattr(node, owner_attr, None)
|
|
786
|
+
if owner is not None and id(owner) not in _seen:
|
|
787
|
+
# A Lines container exposes _owner_ref to the real owner.
|
|
788
|
+
ref = getattr(owner, "_owner_ref", None)
|
|
789
|
+
if ref is not None and id(ref) not in _seen:
|
|
790
|
+
found = _feed_of(ref, _seen)
|
|
791
|
+
if found is not None:
|
|
792
|
+
return found
|
|
793
|
+
found = _feed_of(owner, _seen)
|
|
794
|
+
if found is not None:
|
|
795
|
+
return found
|
|
796
|
+
# Operands of a LinesOperation.
|
|
797
|
+
for op_attr in ("a", "b", "_parent_a", "_parent_b"):
|
|
798
|
+
operand = getattr(node, op_attr, None)
|
|
799
|
+
if operand is not None and id(operand) not in _seen:
|
|
800
|
+
found = _feed_of(operand, _seen)
|
|
801
|
+
if found is not None:
|
|
802
|
+
return found
|
|
803
|
+
return None
|
|
804
|
+
|
|
805
|
+
if primary_feed is not None and len(self.datas) > 1:
|
|
806
|
+
for lineiter in all_indicators:
|
|
807
|
+
idatas = getattr(lineiter, "datas", None)
|
|
808
|
+
if not idatas:
|
|
809
|
+
continue
|
|
810
|
+
feed = _feed_of(idatas[0])
|
|
811
|
+
if feed is not None and feed is not primary_feed and id(feed) in dataids:
|
|
812
|
+
# Pin only the advance clock used by the runonce post-phase
|
|
813
|
+
# loop and Indicator.advance(); deliberately do NOT change
|
|
814
|
+
# lineiter._clock here so the existing minperiod-to-feed
|
|
815
|
+
# attribution below (and thus the strategy warmup / bar_num)
|
|
816
|
+
# stays identical to the pre-fix behavior. Changing _clock
|
|
817
|
+
# perturbed multi-feed warmup for unrelated strategies.
|
|
818
|
+
lineiter._resolved_secondary_clock = feed
|
|
819
|
+
|
|
820
|
+
for lineiter in all_indicators:
|
|
821
|
+
# If multiple datas are used and multiple timeframes, the larger
|
|
822
|
+
# timeframe may place larger time constraints in calling next.
|
|
823
|
+
# Get the indicator's _clock attribute
|
|
824
|
+
clk = getattr(lineiter, "_clock", None)
|
|
825
|
+
|
|
826
|
+
# CRITICAL FIX: If clock is MinimalClock, use the indicator's actual data source
|
|
827
|
+
if (
|
|
828
|
+
clk is not None
|
|
829
|
+
and hasattr(clk, "__class__")
|
|
830
|
+
and "MinimalClock" in clk.__class__.__name__
|
|
831
|
+
):
|
|
832
|
+
if self.datas:
|
|
833
|
+
# Find which data feed the indicator's data source belongs to
|
|
834
|
+
clock_set = False
|
|
835
|
+
if hasattr(lineiter, "datas") and lineiter.datas:
|
|
836
|
+
ind_data = lineiter.datas[0]
|
|
837
|
+
for data_feed in self.datas:
|
|
838
|
+
# Check if ind_data is the data feed itself
|
|
839
|
+
if ind_data is data_feed:
|
|
840
|
+
clk = data_feed
|
|
841
|
+
clock_set = True
|
|
842
|
+
break
|
|
843
|
+
# Check if ind_data is one of the lines of this data feed
|
|
844
|
+
if hasattr(data_feed, "lines") and ind_data in data_feed.lines:
|
|
845
|
+
clk = data_feed
|
|
846
|
+
clock_set = True
|
|
847
|
+
break
|
|
848
|
+
if not clock_set:
|
|
849
|
+
clk = self.datas[0]
|
|
850
|
+
lineiter._clock = clk # Update indicator's clock
|
|
851
|
+
else:
|
|
852
|
+
clk = None
|
|
853
|
+
|
|
854
|
+
# If the attribute value is None
|
|
855
|
+
if clk is None:
|
|
856
|
+
# Get the indicator's owner's _clock attribute value
|
|
857
|
+
clk = getattr(lineiter._owner, "_clock", None)
|
|
858
|
+
# CRITICAL FIX: If owner's clock is also MinimalClock, use data
|
|
859
|
+
if (
|
|
860
|
+
clk is not None
|
|
861
|
+
and hasattr(clk, "__class__")
|
|
862
|
+
and "MinimalClock" in clk.__class__.__name__
|
|
863
|
+
):
|
|
864
|
+
if self.datas:
|
|
865
|
+
clk = self.datas[0]
|
|
866
|
+
else:
|
|
867
|
+
clk = None
|
|
868
|
+
if clk is None:
|
|
869
|
+
continue
|
|
870
|
+
# If clk is not None
|
|
871
|
+
while True:
|
|
872
|
+
# If clk is a data feed, break
|
|
873
|
+
if id(clk) in dataids:
|
|
874
|
+
break # already top-level clock (data feed)
|
|
875
|
+
|
|
876
|
+
# See if the current clock has higher level clocks
|
|
877
|
+
# Check if current clk has further _clock attribute
|
|
878
|
+
clk2 = getattr(clk, "_clock", None)
|
|
879
|
+
# If clk2 is None, get clk owner's _clock attribute value
|
|
880
|
+
if clk2 is None:
|
|
881
|
+
clk2 = getattr(clk._owner, "_clock", None)
|
|
882
|
+
if clk2 is None:
|
|
883
|
+
break # if no clock found, bail out
|
|
884
|
+
# If clk2 is not None, set clk to clk2
|
|
885
|
+
clk = clk2 # keep the ref and try to go up the hierarchy
|
|
886
|
+
# This check ensures clk is not None before proceeding
|
|
887
|
+
if clk is None:
|
|
888
|
+
continue # no clock found, go to next
|
|
889
|
+
|
|
890
|
+
# LineSeriesStub wraps a line and the clock is the wrapped line and
|
|
891
|
+
# not the wrapper itself.
|
|
892
|
+
# If clk is LineSeriesStub (multi-line object), get first line as clk
|
|
893
|
+
if isinstance(clk, LineSeriesStub):
|
|
894
|
+
clk = clk.lines[0]
|
|
895
|
+
# Save minimum period
|
|
896
|
+
_dminperiods[clk].append(lineiter._minperiod)
|
|
897
|
+
|
|
898
|
+
# Set minimum periods to empty list
|
|
899
|
+
self._minperiods = []
|
|
900
|
+
# Loop through all data feeds
|
|
901
|
+
for data in self.datas:
|
|
902
|
+
# Do not only consider the data as clock but also its lines, which
|
|
903
|
+
# may have been individually passed as clock references and
|
|
904
|
+
# discovered as clocks above
|
|
905
|
+
|
|
906
|
+
# Initialize with a data min period if any
|
|
907
|
+
# Minimum period needed for data to generate indicator lines
|
|
908
|
+
dlminperiods = _dminperiods[data]
|
|
909
|
+
# Loop through each line of data, add minperiods if line is in _dminperiods
|
|
910
|
+
for line in data.lines: # search each line for min periods
|
|
911
|
+
if line in _dminperiods:
|
|
912
|
+
dlminperiods += _dminperiods[line] # found, add it
|
|
913
|
+
|
|
914
|
+
# Keep the reference to the line if any was found
|
|
915
|
+
# If dlminperiods is not empty, calculate max value, else empty list
|
|
916
|
+
_dminperiods[data] = [max(dlminperiods)] if dlminperiods else []
|
|
917
|
+
# Data minimum period
|
|
918
|
+
dminperiod = max(_dminperiods[data] or [data._minperiod])
|
|
919
|
+
# Save minimum period to dminperiod
|
|
920
|
+
self._minperiods.append(dminperiod)
|
|
921
|
+
|
|
922
|
+
# Set the minperiod
|
|
923
|
+
# Indicator minimum periods
|
|
924
|
+
minperiods = [x._minperiod for x in all_indicators]
|
|
925
|
+
|
|
926
|
+
# Strategy-owned LineActions are not necessarily registered indicators.
|
|
927
|
+
# Original backtrader's metaclass machinery advanced them regardless of
|
|
928
|
+
# whether users kept them in public or private attributes. Reuse the
|
|
929
|
+
# recursive strategy-attribute scan here so private containers such as
|
|
930
|
+
# self._Type / self._OptionType participate in minperiod and execution.
|
|
931
|
+
strategy_lineactions = tuple(self._get_strategy_lineactions())
|
|
932
|
+
for attr in strategy_lineactions:
|
|
933
|
+
if (
|
|
934
|
+
hasattr(attr, "_minperiod")
|
|
935
|
+
and attr not in self._lineiterators[LineIterator.IndType]
|
|
936
|
+
):
|
|
937
|
+
minperiods.append(attr._minperiod)
|
|
938
|
+
|
|
939
|
+
# Set strategy minimum period to max of indicator and data minperiods
|
|
940
|
+
self._minperiod = max(minperiods or [self._minperiod])
|
|
941
|
+
|
|
942
|
+
# Update _minperiods for strategy-owned LineActions, but only for their
|
|
943
|
+
# associated data. For multi-data strategies, LineActions minperiod
|
|
944
|
+
# should only affect the source data that clocks the expression.
|
|
945
|
+
if self._minperiods:
|
|
946
|
+
for attr in strategy_lineactions:
|
|
947
|
+
try:
|
|
948
|
+
if not hasattr(attr, "_minperiod"):
|
|
949
|
+
continue
|
|
950
|
+
# Try to determine which data this LineActions is associated
|
|
951
|
+
# with by checking its _clock or data sources.
|
|
952
|
+
data_idx = 0 # Default to data[0]
|
|
953
|
+
if hasattr(attr, "_clock") and attr._clock is not None:
|
|
954
|
+
for i, d in enumerate(self.datas):
|
|
955
|
+
if attr._clock is d or attr._clock in d.lines:
|
|
956
|
+
data_idx = i
|
|
957
|
+
break
|
|
958
|
+
# Only update minperiod for the specific data.
|
|
959
|
+
if data_idx < len(self._minperiods):
|
|
960
|
+
self._minperiods[data_idx] = max(
|
|
961
|
+
self._minperiods[data_idx], attr._minperiod
|
|
962
|
+
)
|
|
963
|
+
except (AttributeError, TypeError):
|
|
964
|
+
# Attribute access/typecheck failed; skip this attribute.
|
|
965
|
+
logger.debug("strategy:946 ignored AttributeError,TypeError")
|
|
966
|
+
|
|
967
|
+
def _addwriter(self, writer):
|
|
968
|
+
"""Add a writer to the strategy.
|
|
969
|
+
|
|
970
|
+
Unlike the other _addxxx functions, this one receives an instance
|
|
971
|
+
because the writer works at cerebro level and is only passed to the
|
|
972
|
+
strategy to simplify the logic.
|
|
973
|
+
"""
|
|
974
|
+
self.writers.append(writer)
|
|
975
|
+
|
|
976
|
+
def _addindicator(self, indcls, *indargs, **indkwargs):
|
|
977
|
+
"""Add an indicator to the strategy.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
indcls: Indicator class to instantiate
|
|
981
|
+
*indargs: Positional arguments for the indicator
|
|
982
|
+
**indkwargs: Keyword arguments for the indicator
|
|
983
|
+
"""
|
|
984
|
+
indcls(*indargs, **indkwargs)
|
|
985
|
+
|
|
986
|
+
def _addanalyzer_slave(self, ancls, *anargs, **ankwargs):
|
|
987
|
+
"""Add a slave analyzer for internal use.
|
|
988
|
+
|
|
989
|
+
Like _addanalyzer but meant for observers (or other entities) which
|
|
990
|
+
rely on the output of an analyzer for the data. These analyzers have
|
|
991
|
+
not been added by the user and are kept separate from the main
|
|
992
|
+
analyzers.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
ancls: Analyzer class to instantiate
|
|
996
|
+
*anargs: Positional arguments for the analyzer
|
|
997
|
+
**ankwargs: Keyword arguments for the analyzer
|
|
998
|
+
|
|
999
|
+
Returns:
|
|
1000
|
+
The created analyzer instance
|
|
1001
|
+
"""
|
|
1002
|
+
# Use OwnerContext so analyzer's findowner() can find this strategy
|
|
1003
|
+
with OwnerContext.set_owner(self):
|
|
1004
|
+
analyzer = ancls(*anargs, **ankwargs)
|
|
1005
|
+
self._slave_analyzers.append(analyzer)
|
|
1006
|
+
return analyzer
|
|
1007
|
+
|
|
1008
|
+
def _getanalyzer_slave(self, idx):
|
|
1009
|
+
"""Get a slave analyzer by index.
|
|
1010
|
+
|
|
1011
|
+
Note: This appears to have a syntax bug - should use [] not append()
|
|
1012
|
+
"""
|
|
1013
|
+
return self._slave_analyzers.append[idx]
|
|
1014
|
+
|
|
1015
|
+
def _addanalyzer(self, ancls, *anargs, **ankwargs):
|
|
1016
|
+
"""Add an analyzer to the strategy.
|
|
1017
|
+
|
|
1018
|
+
Args:
|
|
1019
|
+
ancls: Analyzer class to instantiate
|
|
1020
|
+
*anargs: Positional arguments for the analyzer
|
|
1021
|
+
**ankwargs: Keyword arguments for the analyzer, may include _name
|
|
1022
|
+
"""
|
|
1023
|
+
anname = ankwargs.pop("_name", "") or ancls.__name__.lower()
|
|
1024
|
+
nsuffix = next(self._alnames[anname])
|
|
1025
|
+
anname += str(nsuffix or "") # 0 (first instance) gets no suffix
|
|
1026
|
+
# Use OwnerContext so analyzer's findowner() can find this strategy
|
|
1027
|
+
with OwnerContext.set_owner(self):
|
|
1028
|
+
analyzer = ancls(*anargs, **ankwargs)
|
|
1029
|
+
# PERFORMANCE FIX: Explicitly set analyzer's owner to ensure it has access to strategy
|
|
1030
|
+
analyzer._parent = self
|
|
1031
|
+
analyzer._owner = self
|
|
1032
|
+
self.analyzers.append(analyzer, anname)
|
|
1033
|
+
|
|
1034
|
+
def _addobserver(self, multi, obscls, *obsargs, **obskwargs):
|
|
1035
|
+
"""Add an observer to the strategy.
|
|
1036
|
+
|
|
1037
|
+
Args:
|
|
1038
|
+
multi: If True, create one observer per data feed; if False, create single observer
|
|
1039
|
+
obscls: Observer class to instantiate
|
|
1040
|
+
*obsargs: Positional arguments for the observer
|
|
1041
|
+
**obskwargs: Keyword arguments for the observer, may include obsname
|
|
1042
|
+
"""
|
|
1043
|
+
obsname = obskwargs.pop("obsname", "")
|
|
1044
|
+
if not obsname:
|
|
1045
|
+
obsname = obscls.__name__.lower()
|
|
1046
|
+
|
|
1047
|
+
if not multi:
|
|
1048
|
+
newargs = list(itertools.chain(self.datas, obsargs))
|
|
1049
|
+
# Use OwnerContext so observer's findowner() can find this strategy
|
|
1050
|
+
with OwnerContext.set_owner(self):
|
|
1051
|
+
obs = obscls(*newargs, **obskwargs)
|
|
1052
|
+
# PERFORMANCE FIX: Explicitly set observer's owner to ensure it has access to strategy
|
|
1053
|
+
obs._parent = self
|
|
1054
|
+
obs._owner = self
|
|
1055
|
+
self._register_observer(obs)
|
|
1056
|
+
self.stats.append(obs, obsname)
|
|
1057
|
+
return
|
|
1058
|
+
|
|
1059
|
+
setattr(self.stats, obsname, [])
|
|
1060
|
+
obs_list = getattr(self.stats, obsname)
|
|
1061
|
+
|
|
1062
|
+
for data in self.datas:
|
|
1063
|
+
# Use OwnerContext so observer's findowner() can find this strategy
|
|
1064
|
+
with OwnerContext.set_owner(self):
|
|
1065
|
+
obs = obscls(data, *obsargs, **obskwargs)
|
|
1066
|
+
# PERFORMANCE FIX: Explicitly set observer's owner to ensure it has access to strategy
|
|
1067
|
+
obs._parent = self
|
|
1068
|
+
obs._owner = self
|
|
1069
|
+
self._register_observer(obs)
|
|
1070
|
+
obs_list.append(obs)
|
|
1071
|
+
|
|
1072
|
+
def _register_observer(self, obs):
|
|
1073
|
+
"""Prepare an observer for execution.
|
|
1074
|
+
|
|
1075
|
+
Ensures the observer has _analyzers and has its clock set properly
|
|
1076
|
+
for strategy-wide observers. Does NOT register in _lineiterators
|
|
1077
|
+
to avoid double-processing (observers are processed bar-by-bar
|
|
1078
|
+
via _next_observers, not via _once batch processing).
|
|
1079
|
+
|
|
1080
|
+
Args:
|
|
1081
|
+
obs: Observer instance to register.
|
|
1082
|
+
"""
|
|
1083
|
+
# Ensure _analyzers exists (some observers don't call super().__init__)
|
|
1084
|
+
if not hasattr(obs, "_analyzers"):
|
|
1085
|
+
obs._analyzers = []
|
|
1086
|
+
# Set clock for strategy-wide observers
|
|
1087
|
+
if getattr(obs, "_stclock", False):
|
|
1088
|
+
obs._clock = self
|
|
1089
|
+
|
|
1090
|
+
def _getminperstatus(self):
|
|
1091
|
+
"""Check if minimum period requirements are satisfied.
|
|
1092
|
+
|
|
1093
|
+
Returns the maximum difference between required minimum periods
|
|
1094
|
+
and current data lengths.
|
|
1095
|
+
|
|
1096
|
+
Returns:
|
|
1097
|
+
int: Maximum value of (minperiod - current_length) across all data feeds.
|
|
1098
|
+
Negative values indicate all minimum periods are satisfied.
|
|
1099
|
+
"""
|
|
1100
|
+
datas = self.datas
|
|
1101
|
+
minperiods = self._minperiods
|
|
1102
|
+
data_count = len(datas)
|
|
1103
|
+
if not data_count:
|
|
1104
|
+
raise ValueError("max() arg is an empty sequence") from None
|
|
1105
|
+
|
|
1106
|
+
minperstatus = minperiods[0] - len(datas[0])
|
|
1107
|
+
for index in range(1, data_count):
|
|
1108
|
+
status = minperiods[index] - len(datas[index])
|
|
1109
|
+
if status > minperstatus:
|
|
1110
|
+
minperstatus = status
|
|
1111
|
+
|
|
1112
|
+
self._minperstatus = minperstatus
|
|
1113
|
+
return minperstatus
|
|
1114
|
+
|
|
1115
|
+
def prenext_open(self):
|
|
1116
|
+
"""Called before next() during prenext phase.
|
|
1117
|
+
|
|
1118
|
+
This is a hook for strategies to take action at the open of each bar
|
|
1119
|
+
before minimum period is reached.
|
|
1120
|
+
"""
|
|
1121
|
+
|
|
1122
|
+
def nextstart_open(self):
|
|
1123
|
+
"""Called at the open of the first bar where minimum period is satisfied.
|
|
1124
|
+
|
|
1125
|
+
This is called only once, transitioning from prenext to next phase.
|
|
1126
|
+
"""
|
|
1127
|
+
self.next_open()
|
|
1128
|
+
|
|
1129
|
+
def next_open(self):
|
|
1130
|
+
"""Called at the open of each bar during normal execution.
|
|
1131
|
+
|
|
1132
|
+
This is a hook for strategies to take action at the open of each bar.
|
|
1133
|
+
"""
|
|
1134
|
+
|
|
1135
|
+
def _oncepost_open(self):
|
|
1136
|
+
"""Prepare for _oncepost execution based on minimum period status.
|
|
1137
|
+
|
|
1138
|
+
Routes to appropriate method based on minperstatus:
|
|
1139
|
+
- minperstatus < 0: All data satisfied, call next_open()
|
|
1140
|
+
- minperstatus == 0: First bar with satisfied data, call nextstart_open()
|
|
1141
|
+
- minperstatus > 0: Data not ready, call prenext_open()
|
|
1142
|
+
"""
|
|
1143
|
+
minperstatus = self._minperstatus
|
|
1144
|
+
if minperstatus < 0:
|
|
1145
|
+
self.next_open()
|
|
1146
|
+
elif minperstatus == 0:
|
|
1147
|
+
self.nextstart_open() # only called for the 1st value
|
|
1148
|
+
else:
|
|
1149
|
+
self.prenext_open()
|
|
1150
|
+
|
|
1151
|
+
def _oncepost(self, dt):
|
|
1152
|
+
"""Execute oncepost processing for a single time step.
|
|
1153
|
+
|
|
1154
|
+
Args:
|
|
1155
|
+
dt: Current datetime
|
|
1156
|
+
"""
|
|
1157
|
+
# CRITICAL FIX: Ensure _clock is set to actual data, not MinimalClock
|
|
1158
|
+
# During initialization, _clock might be set to MinimalClock if datas weren't available yet
|
|
1159
|
+
if hasattr(self, "_clock") and self._clock is not None:
|
|
1160
|
+
clock_type_name = type(self._clock).__name__
|
|
1161
|
+
if clock_type_name == "MinimalClock" and self.datas:
|
|
1162
|
+
# Replace MinimalClock with actual first data
|
|
1163
|
+
self._clock = self.datas[0]
|
|
1164
|
+
elif not hasattr(self, "_clock") or self._clock is None:
|
|
1165
|
+
# Set clock to first data if not set
|
|
1166
|
+
if self.datas:
|
|
1167
|
+
self._clock = self.datas[0]
|
|
1168
|
+
|
|
1169
|
+
# Loop through indicators, advance if indicator clock length exceeds indicator length
|
|
1170
|
+
for indicator in self._lineiterators[LineIterator.IndType]:
|
|
1171
|
+
# Honor a pinned secondary-feed clock (set in _periodset) so
|
|
1172
|
+
# indicators following a non-primary feed advance in sync with it.
|
|
1173
|
+
adv_clock = getattr(indicator, "_resolved_secondary_clock", None) or indicator._clock
|
|
1174
|
+
if len(adv_clock) > len(indicator):
|
|
1175
|
+
indicator.advance()
|
|
1176
|
+
# If using old data sync method, call advance; otherwise call forward
|
|
1177
|
+
if self._oldsync:
|
|
1178
|
+
# Strategy has not been reset, the line is there
|
|
1179
|
+
self.advance()
|
|
1180
|
+
else:
|
|
1181
|
+
# strategy has been reset to beginning. advance step by step
|
|
1182
|
+
self.forward()
|
|
1183
|
+
# Set datetime - and save it as the last valid datetime for use in stop()
|
|
1184
|
+
_set_current_datetime(self.lines.datetime, dt)
|
|
1185
|
+
if dt > 0:
|
|
1186
|
+
self._last_valid_datetime = dt
|
|
1187
|
+
# Notify
|
|
1188
|
+
self._notify()
|
|
1189
|
+
|
|
1190
|
+
try:
|
|
1191
|
+
has_strategy_next_lineactions = self._has_strategy_next_lineactions
|
|
1192
|
+
except AttributeError:
|
|
1193
|
+
has_strategy_next_lineactions = True
|
|
1194
|
+
if has_strategy_next_lineactions:
|
|
1195
|
+
self._next_strategy_lineactions()
|
|
1196
|
+
|
|
1197
|
+
# CRITICAL FIX: In runonce mode, ensure indicator lencount matches strategy length
|
|
1198
|
+
# This ensures len(indicator) == len(strategy) at the end of processing
|
|
1199
|
+
try:
|
|
1200
|
+
strategy_len = len(self)
|
|
1201
|
+
strategy_clock = getattr(self, "_clock", None)
|
|
1202
|
+
for indicator in self._lineiterators[LineIterator.IndType]:
|
|
1203
|
+
if getattr(indicator, "_clock", None) is not strategy_clock:
|
|
1204
|
+
continue
|
|
1205
|
+
# Only update if indicator was processed in runonce mode
|
|
1206
|
+
if hasattr(indicator, "_once_called") and indicator._once_called:
|
|
1207
|
+
# Update lencount for all lines in the indicator
|
|
1208
|
+
if hasattr(indicator, "lines") and hasattr(indicator.lines, "lines"):
|
|
1209
|
+
for line in indicator.lines.lines:
|
|
1210
|
+
if hasattr(line, "lencount"):
|
|
1211
|
+
# Set lencount to match strategy length (which equals data length)
|
|
1212
|
+
# Use the maximum of current lencount and strategy_len to ensure we don't decrease it
|
|
1213
|
+
line.lencount = max(line.lencount, strategy_len)
|
|
1214
|
+
except Exception:
|
|
1215
|
+
logger.debug("Failed to update indicator lencount in _oncepost_nextday", exc_info=True)
|
|
1216
|
+
|
|
1217
|
+
# Get current minimum period status and route to appropriate method
|
|
1218
|
+
# If all data satisfied, call next()
|
|
1219
|
+
# If first bar with all data satisfied, call nextstart()
|
|
1220
|
+
# If not all data satisfied, call prenext()
|
|
1221
|
+
try:
|
|
1222
|
+
minperstatus = self._single_minperiod - len(self._single_minperiod_data)
|
|
1223
|
+
self._minperstatus = minperstatus
|
|
1224
|
+
except AttributeError:
|
|
1225
|
+
minperstatus = self._getminperstatus()
|
|
1226
|
+
if minperstatus < 0:
|
|
1227
|
+
self.next()
|
|
1228
|
+
elif minperstatus == 0:
|
|
1229
|
+
self.nextstart() # only called for the 1st value
|
|
1230
|
+
else:
|
|
1231
|
+
self.prenext()
|
|
1232
|
+
# Update analyzers with minimum period status
|
|
1233
|
+
try:
|
|
1234
|
+
has_analyzers = self._has_analyzers
|
|
1235
|
+
except AttributeError:
|
|
1236
|
+
has_analyzers = bool(self.analyzers)
|
|
1237
|
+
if has_analyzers:
|
|
1238
|
+
self._next_analyzers(minperstatus, once=True)
|
|
1239
|
+
# Update observers with minimum period status
|
|
1240
|
+
try:
|
|
1241
|
+
has_observers = self._has_observers
|
|
1242
|
+
except AttributeError:
|
|
1243
|
+
has_observers = bool(self.stats.items)
|
|
1244
|
+
if has_observers:
|
|
1245
|
+
self._next_observers(minperstatus, once=True)
|
|
1246
|
+
# Clear pending orders and trades
|
|
1247
|
+
if self._orderspending or self._tradespending:
|
|
1248
|
+
self.clear()
|
|
1249
|
+
|
|
1250
|
+
def _clk_update(self):
|
|
1251
|
+
"""Update the clock and advance strategy state if needed.
|
|
1252
|
+
|
|
1253
|
+
Returns:
|
|
1254
|
+
int: Current length of the strategy
|
|
1255
|
+
"""
|
|
1256
|
+
# CRITICAL FIX: Ensure data is available before clock operations
|
|
1257
|
+
data_assignment_pending = self._data_assignment_pending
|
|
1258
|
+
clock = self._clock
|
|
1259
|
+
|
|
1260
|
+
if data_assignment_pending or clock is None:
|
|
1261
|
+
# Try to get data assignment from cerebro if not already done
|
|
1262
|
+
ensure_data_available = self.__dict__.get("_ensure_data_available")
|
|
1263
|
+
if ensure_data_available is not None:
|
|
1264
|
+
ensure_data_available()
|
|
1265
|
+
|
|
1266
|
+
# If using old data sync method
|
|
1267
|
+
if self._oldsync:
|
|
1268
|
+
# Call strategy's _clk_update() method
|
|
1269
|
+
clk_len = super()._clk_update()
|
|
1270
|
+
# Set datetime
|
|
1271
|
+
if self.datas:
|
|
1272
|
+
max_datetime = None
|
|
1273
|
+
for data in self.datas:
|
|
1274
|
+
if not len(data):
|
|
1275
|
+
continue
|
|
1276
|
+
dt_value = data.datetime[0]
|
|
1277
|
+
try:
|
|
1278
|
+
valid_dt = dt_value > 0 and dt_value < _INF
|
|
1279
|
+
except TypeError:
|
|
1280
|
+
valid_dt = False
|
|
1281
|
+
if valid_dt:
|
|
1282
|
+
if max_datetime is None or dt_value > max_datetime:
|
|
1283
|
+
max_datetime = dt_value
|
|
1284
|
+
if max_datetime is not None:
|
|
1285
|
+
_set_current_datetime(self.lines.datetime, max_datetime)
|
|
1286
|
+
# Return data length
|
|
1287
|
+
return clk_len
|
|
1288
|
+
|
|
1289
|
+
# CRITICAL FIX: Initialize _dlens if not present
|
|
1290
|
+
datas = self.datas
|
|
1291
|
+
try:
|
|
1292
|
+
olddlens = self._dlens
|
|
1293
|
+
except AttributeError:
|
|
1294
|
+
olddlens = [len(d) for d in datas]
|
|
1295
|
+
self._dlens = olddlens
|
|
1296
|
+
try:
|
|
1297
|
+
datetime_line = self._datetime_line
|
|
1298
|
+
except AttributeError:
|
|
1299
|
+
datetime_line = self.lines.datetime
|
|
1300
|
+
try:
|
|
1301
|
+
datetime_line_direct = self._datetime_line_direct
|
|
1302
|
+
except AttributeError:
|
|
1303
|
+
datetime_line_direct = False
|
|
1304
|
+
|
|
1305
|
+
try:
|
|
1306
|
+
data = self._single_clock_data
|
|
1307
|
+
except AttributeError:
|
|
1308
|
+
data = None
|
|
1309
|
+
|
|
1310
|
+
if data is not None:
|
|
1311
|
+
try:
|
|
1312
|
+
data_len = self._single_clock_len_line.lencount
|
|
1313
|
+
except AttributeError:
|
|
1314
|
+
data_len = len(data)
|
|
1315
|
+
if data_len > olddlens[0]:
|
|
1316
|
+
try:
|
|
1317
|
+
forward_line = self._single_line_forward_line
|
|
1318
|
+
except AttributeError:
|
|
1319
|
+
forward_line = None
|
|
1320
|
+
if forward_line is not None and forward_line.mode != forward_line.QBuffer:
|
|
1321
|
+
if forward_line.lencount < data_len:
|
|
1322
|
+
forward_line._idx += 1
|
|
1323
|
+
forward_line.lencount += 1
|
|
1324
|
+
forward_line.array.append(forward_line._default_value)
|
|
1325
|
+
else:
|
|
1326
|
+
self.forward()
|
|
1327
|
+
if data_len:
|
|
1328
|
+
try:
|
|
1329
|
+
data_datetime_line = self._single_clock_datetime_line
|
|
1330
|
+
data_datetime_idx = data_datetime_line._idx
|
|
1331
|
+
if data_datetime_idx >= 0:
|
|
1332
|
+
dt_value = data_datetime_line.array[data_datetime_idx]
|
|
1333
|
+
else:
|
|
1334
|
+
dt_value = data_datetime_line[0]
|
|
1335
|
+
except (AttributeError, IndexError):
|
|
1336
|
+
dt_value = data.datetime[0]
|
|
1337
|
+
try:
|
|
1338
|
+
valid_dt = dt_value > 0 and dt_value < _INF
|
|
1339
|
+
except TypeError:
|
|
1340
|
+
valid_dt = False
|
|
1341
|
+
if valid_dt:
|
|
1342
|
+
if datetime_line_direct:
|
|
1343
|
+
idx = datetime_line._idx
|
|
1344
|
+
if idx >= 0:
|
|
1345
|
+
try:
|
|
1346
|
+
datetime_line.array[idx] = dt_value if dt_value >= 1.0 else 1.0
|
|
1347
|
+
except IndexError:
|
|
1348
|
+
_set_current_datetime(datetime_line, dt_value)
|
|
1349
|
+
else:
|
|
1350
|
+
_set_current_datetime(datetime_line, dt_value)
|
|
1351
|
+
else:
|
|
1352
|
+
_set_current_datetime(datetime_line, dt_value)
|
|
1353
|
+
olddlens[0] = data_len
|
|
1354
|
+
try:
|
|
1355
|
+
return datetime_line.lencount
|
|
1356
|
+
except AttributeError:
|
|
1357
|
+
return len(self)
|
|
1358
|
+
|
|
1359
|
+
# Current new data lengths and valid datetimes in a single pass.
|
|
1360
|
+
newdlens = []
|
|
1361
|
+
max_datetime = None
|
|
1362
|
+
for data in datas:
|
|
1363
|
+
data_len = len(data)
|
|
1364
|
+
newdlens.append(data_len)
|
|
1365
|
+
if data_len:
|
|
1366
|
+
dt_value = data.datetime[0]
|
|
1367
|
+
try:
|
|
1368
|
+
valid_dt = dt_value > 0 and dt_value < _INF
|
|
1369
|
+
except TypeError:
|
|
1370
|
+
valid_dt = False
|
|
1371
|
+
if valid_dt:
|
|
1372
|
+
if max_datetime is None or dt_value > max_datetime:
|
|
1373
|
+
max_datetime = dt_value
|
|
1374
|
+
|
|
1375
|
+
# If new data length > old data length, forward
|
|
1376
|
+
if any(nl > old_len for old_len, nl in zip(olddlens, newdlens)):
|
|
1377
|
+
self.forward()
|
|
1378
|
+
# Set datetime to max of current datetimes - only update if we have valid datetimes
|
|
1379
|
+
if max_datetime is not None:
|
|
1380
|
+
_set_current_datetime(datetime_line, max_datetime)
|
|
1381
|
+
# Old data length equals new data length
|
|
1382
|
+
self._dlens = newdlens
|
|
1383
|
+
|
|
1384
|
+
try:
|
|
1385
|
+
return datetime_line.lencount
|
|
1386
|
+
except AttributeError:
|
|
1387
|
+
return len(self)
|
|
1388
|
+
|
|
1389
|
+
def _next_open(self):
|
|
1390
|
+
"""Execute next_open phase based on minimum period status.
|
|
1391
|
+
|
|
1392
|
+
Same logic as _oncepost_open().
|
|
1393
|
+
"""
|
|
1394
|
+
minperstatus = self._minperstatus
|
|
1395
|
+
if minperstatus < 0:
|
|
1396
|
+
self.next_open()
|
|
1397
|
+
elif minperstatus == 0:
|
|
1398
|
+
self.nextstart_open() # only called for the 1st value
|
|
1399
|
+
else:
|
|
1400
|
+
self.prenext_open()
|
|
1401
|
+
|
|
1402
|
+
def _next_fast_simple(self):
|
|
1403
|
+
"""Fast _next implementation for simple single-strategy runs."""
|
|
1404
|
+
if self._orderspending or self._tradespending:
|
|
1405
|
+
Strategy._next(self)
|
|
1406
|
+
return
|
|
1407
|
+
|
|
1408
|
+
if self._fast_simple_clock_update:
|
|
1409
|
+
olddlens = self._dlens
|
|
1410
|
+
data_len = self._single_clock_len_line.lencount
|
|
1411
|
+
datetime_line = self._datetime_line
|
|
1412
|
+
forward_line = self._single_line_forward_line
|
|
1413
|
+
needs_forward = data_len > olddlens[0] and forward_line.lencount < data_len
|
|
1414
|
+
if data_len:
|
|
1415
|
+
data_datetime_line = self._single_clock_datetime_line
|
|
1416
|
+
data_datetime_idx = data_datetime_line._idx
|
|
1417
|
+
try:
|
|
1418
|
+
dt_value = data_datetime_line.array[data_datetime_idx]
|
|
1419
|
+
except IndexError:
|
|
1420
|
+
dt_value = data_datetime_line[0]
|
|
1421
|
+
try:
|
|
1422
|
+
valid_dt = dt_value > 0 and dt_value < _INF
|
|
1423
|
+
except TypeError:
|
|
1424
|
+
valid_dt = False
|
|
1425
|
+
if needs_forward:
|
|
1426
|
+
forward_line._idx += 1
|
|
1427
|
+
forward_line.lencount += 1
|
|
1428
|
+
forward_line.array.append(
|
|
1429
|
+
dt_value if valid_dt and dt_value >= 1.0 else forward_line._default_value
|
|
1430
|
+
)
|
|
1431
|
+
elif valid_dt:
|
|
1432
|
+
idx = datetime_line._idx
|
|
1433
|
+
if idx >= 0:
|
|
1434
|
+
try:
|
|
1435
|
+
datetime_line.array[idx] = dt_value if dt_value >= 1.0 else 1.0
|
|
1436
|
+
except IndexError:
|
|
1437
|
+
_set_current_datetime(datetime_line, dt_value)
|
|
1438
|
+
else:
|
|
1439
|
+
_set_current_datetime(datetime_line, dt_value)
|
|
1440
|
+
elif needs_forward:
|
|
1441
|
+
forward_line._idx += 1
|
|
1442
|
+
forward_line.lencount += 1
|
|
1443
|
+
forward_line.array.append(forward_line._default_value)
|
|
1444
|
+
olddlens[0] = data_len
|
|
1445
|
+
else:
|
|
1446
|
+
self._clk_update()
|
|
1447
|
+
minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
|
|
1448
|
+
object.__setattr__(self, "_minperstatus", minperstatus)
|
|
1449
|
+
if minperstatus < 0:
|
|
1450
|
+
self.next()
|
|
1451
|
+
elif minperstatus == 0:
|
|
1452
|
+
self.nextstart()
|
|
1453
|
+
else:
|
|
1454
|
+
self.prenext()
|
|
1455
|
+
if self._orderspending or self._tradespending:
|
|
1456
|
+
self.clear()
|
|
1457
|
+
|
|
1458
|
+
def _next_fast_simple_direct_clock(self):
|
|
1459
|
+
"""Fast _next for trusted direct single-data clocks."""
|
|
1460
|
+
if self._orderspending or self._tradespending:
|
|
1461
|
+
Strategy._next(self)
|
|
1462
|
+
return
|
|
1463
|
+
|
|
1464
|
+
data_datetime_line = self._single_clock_datetime_line
|
|
1465
|
+
dt_value = data_datetime_line.array[data_datetime_line._idx]
|
|
1466
|
+
forward_line = self._single_line_forward_line
|
|
1467
|
+
forward_line._idx += 1
|
|
1468
|
+
forward_line.lencount += 1
|
|
1469
|
+
forward_line.array.append(dt_value)
|
|
1470
|
+
self._dlens[0] = data_datetime_line.lencount
|
|
1471
|
+
|
|
1472
|
+
minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
|
|
1473
|
+
object.__setattr__(self, "_minperstatus", minperstatus)
|
|
1474
|
+
if minperstatus < 0:
|
|
1475
|
+
self.next()
|
|
1476
|
+
elif minperstatus == 0:
|
|
1477
|
+
self.nextstart()
|
|
1478
|
+
else:
|
|
1479
|
+
self.prenext()
|
|
1480
|
+
if self._orderspending or self._tradespending:
|
|
1481
|
+
self.clear()
|
|
1482
|
+
|
|
1483
|
+
def _next(self):
|
|
1484
|
+
"""Execute next() method and update analyzers and observers.
|
|
1485
|
+
|
|
1486
|
+
Gets minimum period status and passes it to analyzers and observers,
|
|
1487
|
+
then clears pending orders and trades.
|
|
1488
|
+
"""
|
|
1489
|
+
try:
|
|
1490
|
+
fast_simple_next = self._fast_simple_next
|
|
1491
|
+
except AttributeError:
|
|
1492
|
+
fast_simple_next = False
|
|
1493
|
+
if fast_simple_next and not self._orderspending and not self._tradespending:
|
|
1494
|
+
if self._fast_simple_clock_update:
|
|
1495
|
+
olddlens = self._dlens
|
|
1496
|
+
data_len = self._single_clock_len_line.lencount
|
|
1497
|
+
datetime_line = self._datetime_line
|
|
1498
|
+
forward_line = self._single_line_forward_line
|
|
1499
|
+
needs_forward = data_len > olddlens[0] and forward_line.lencount < data_len
|
|
1500
|
+
if data_len:
|
|
1501
|
+
data_datetime_line = self._single_clock_datetime_line
|
|
1502
|
+
data_datetime_idx = data_datetime_line._idx
|
|
1503
|
+
try:
|
|
1504
|
+
dt_value = data_datetime_line.array[data_datetime_idx]
|
|
1505
|
+
except IndexError:
|
|
1506
|
+
dt_value = data_datetime_line[0]
|
|
1507
|
+
try:
|
|
1508
|
+
valid_dt = dt_value > 0 and dt_value < _INF
|
|
1509
|
+
except TypeError:
|
|
1510
|
+
valid_dt = False
|
|
1511
|
+
if needs_forward:
|
|
1512
|
+
forward_line._idx += 1
|
|
1513
|
+
forward_line.lencount += 1
|
|
1514
|
+
forward_line.array.append(
|
|
1515
|
+
dt_value
|
|
1516
|
+
if valid_dt and dt_value >= 1.0
|
|
1517
|
+
else forward_line._default_value
|
|
1518
|
+
)
|
|
1519
|
+
elif valid_dt:
|
|
1520
|
+
idx = datetime_line._idx
|
|
1521
|
+
if idx >= 0:
|
|
1522
|
+
try:
|
|
1523
|
+
datetime_line.array[idx] = dt_value if dt_value >= 1.0 else 1.0
|
|
1524
|
+
except IndexError:
|
|
1525
|
+
_set_current_datetime(datetime_line, dt_value)
|
|
1526
|
+
else:
|
|
1527
|
+
_set_current_datetime(datetime_line, dt_value)
|
|
1528
|
+
elif needs_forward:
|
|
1529
|
+
forward_line._idx += 1
|
|
1530
|
+
forward_line.lencount += 1
|
|
1531
|
+
forward_line.array.append(forward_line._default_value)
|
|
1532
|
+
olddlens[0] = data_len
|
|
1533
|
+
else:
|
|
1534
|
+
self._clk_update()
|
|
1535
|
+
minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
|
|
1536
|
+
object.__setattr__(self, "_minperstatus", minperstatus)
|
|
1537
|
+
if minperstatus < 0:
|
|
1538
|
+
self.next()
|
|
1539
|
+
elif minperstatus == 0:
|
|
1540
|
+
self.nextstart()
|
|
1541
|
+
else:
|
|
1542
|
+
self.prenext()
|
|
1543
|
+
if self._orderspending or self._tradespending:
|
|
1544
|
+
self.clear()
|
|
1545
|
+
return
|
|
1546
|
+
|
|
1547
|
+
super()._next()
|
|
1548
|
+
|
|
1549
|
+
try:
|
|
1550
|
+
minperstatus = self._single_minperiod - self._single_minperiod_len_line.lencount
|
|
1551
|
+
object.__setattr__(self, "_minperstatus", minperstatus)
|
|
1552
|
+
except AttributeError:
|
|
1553
|
+
minperstatus = self._getminperstatus()
|
|
1554
|
+
try:
|
|
1555
|
+
has_analyzers = self._has_analyzers
|
|
1556
|
+
except AttributeError:
|
|
1557
|
+
has_analyzers = bool(self.analyzers)
|
|
1558
|
+
if has_analyzers:
|
|
1559
|
+
self._next_analyzers(minperstatus)
|
|
1560
|
+
try:
|
|
1561
|
+
has_observers = self._has_observers
|
|
1562
|
+
except AttributeError:
|
|
1563
|
+
has_observers = bool(self.stats.items)
|
|
1564
|
+
if has_observers:
|
|
1565
|
+
self._next_observers(minperstatus)
|
|
1566
|
+
|
|
1567
|
+
if self._orderspending or self._tradespending:
|
|
1568
|
+
self.clear()
|
|
1569
|
+
|
|
1570
|
+
def _get_all_observers(self):
|
|
1571
|
+
"""Get all observer instances from self.stats.
|
|
1572
|
+
|
|
1573
|
+
Handles both single observers and multi-data observer lists.
|
|
1574
|
+
Ensures each observer has _analyzers attribute.
|
|
1575
|
+
|
|
1576
|
+
Returns:
|
|
1577
|
+
list: Flat list of all observer instances.
|
|
1578
|
+
"""
|
|
1579
|
+
result = []
|
|
1580
|
+
for item in self.stats.items:
|
|
1581
|
+
if isinstance(item, list):
|
|
1582
|
+
for obs in item:
|
|
1583
|
+
if not hasattr(obs, "_analyzers"):
|
|
1584
|
+
obs._analyzers = []
|
|
1585
|
+
result.append(obs)
|
|
1586
|
+
else:
|
|
1587
|
+
if not hasattr(item, "_analyzers"):
|
|
1588
|
+
item._analyzers = []
|
|
1589
|
+
result.append(item)
|
|
1590
|
+
return result
|
|
1591
|
+
|
|
1592
|
+
def _next_observers(self, minperstatus, once=False):
|
|
1593
|
+
"""Update observers based on minimum period status.
|
|
1594
|
+
|
|
1595
|
+
Iterates over self.stats.items (populated by _addobserver) instead of
|
|
1596
|
+
_lineiterators[ObsType] which is intentionally kept empty to avoid
|
|
1597
|
+
double-processing with _once() batch mode.
|
|
1598
|
+
|
|
1599
|
+
Note: Some pre-existing observers may have broken __init__ chains
|
|
1600
|
+
(due to ObserverBase.__init_subclass__ wrapped_init not calling
|
|
1601
|
+
super().__init__). Their next() calls are wrapped in try/except
|
|
1602
|
+
to prevent one broken observer from crashing the entire run.
|
|
1603
|
+
|
|
1604
|
+
Args:
|
|
1605
|
+
minperstatus: Current minimum period status
|
|
1606
|
+
once: If True, running in runonce mode; otherwise running in next() mode
|
|
1607
|
+
"""
|
|
1608
|
+
# Collect all observers from _lineiterators
|
|
1609
|
+
observers_to_process = list(self._lineiterators[LineIterator.ObsType])
|
|
1610
|
+
# Also include TradeLogger observers from stats that are not in _lineiterators
|
|
1611
|
+
# (TradeLogger needs next() to be called for position/indicator logging)
|
|
1612
|
+
for obs in self.stats:
|
|
1613
|
+
if obs not in observers_to_process:
|
|
1614
|
+
# Only add TradeLogger type observers to avoid breaking other observers
|
|
1615
|
+
if obs.__class__.__name__ == "TradeLogger":
|
|
1616
|
+
observers_to_process.append(obs)
|
|
1617
|
+
|
|
1618
|
+
# Loop through observers
|
|
1619
|
+
for observer in observers_to_process:
|
|
1620
|
+
# For each analyzer in the observer (if observer has _analyzers)
|
|
1621
|
+
for analyzer in getattr(observer, "_analyzers", []):
|
|
1622
|
+
# Route to appropriate analyzer method based on minperstatus
|
|
1623
|
+
if minperstatus < 0:
|
|
1624
|
+
analyzer._next()
|
|
1625
|
+
elif minperstatus == 0:
|
|
1626
|
+
analyzer._nextstart() # only called for the 1st value
|
|
1627
|
+
else:
|
|
1628
|
+
analyzer._prenext()
|
|
1629
|
+
# If running in once mode
|
|
1630
|
+
if once:
|
|
1631
|
+
# If current data length > observer length
|
|
1632
|
+
if len(self) > len(observer):
|
|
1633
|
+
# If using old data sync method, call advance, else call forward
|
|
1634
|
+
if self._oldsync:
|
|
1635
|
+
observer.advance()
|
|
1636
|
+
else:
|
|
1637
|
+
observer.forward()
|
|
1638
|
+
# Route to appropriate observer method based on minperstatus
|
|
1639
|
+
if minperstatus < 0:
|
|
1640
|
+
observer.next()
|
|
1641
|
+
elif minperstatus == 0:
|
|
1642
|
+
observer.nextstart() # only called for the 1st value
|
|
1643
|
+
elif len(observer):
|
|
1644
|
+
observer.prenext()
|
|
1645
|
+
# If not in once mode, call _next()
|
|
1646
|
+
else:
|
|
1647
|
+
observer._next()
|
|
1648
|
+
|
|
1649
|
+
def _next_analyzers(self, minperstatus, once=False):
|
|
1650
|
+
"""Update analyzers based on minimum period status.
|
|
1651
|
+
|
|
1652
|
+
Args:
|
|
1653
|
+
minperstatus: Current minimum period status
|
|
1654
|
+
once: If True, running in runonce mode (unused but kept for consistency)
|
|
1655
|
+
"""
|
|
1656
|
+
for analyzer in self.analyzers:
|
|
1657
|
+
if minperstatus < 0:
|
|
1658
|
+
analyzer._next()
|
|
1659
|
+
elif minperstatus == 0:
|
|
1660
|
+
analyzer._nextstart() # only called for the 1st value
|
|
1661
|
+
else:
|
|
1662
|
+
analyzer._prenext()
|
|
1663
|
+
|
|
1664
|
+
def _settz(self, tz):
|
|
1665
|
+
"""Set timezone for strategy's datetime line.
|
|
1666
|
+
|
|
1667
|
+
Args:
|
|
1668
|
+
tz: Timezone to set
|
|
1669
|
+
"""
|
|
1670
|
+
self.lines.datetime._settz(tz)
|
|
1671
|
+
|
|
1672
|
+
def _start(self):
|
|
1673
|
+
"""Initialize strategy and start execution.
|
|
1674
|
+
|
|
1675
|
+
Calculates minimum periods, starts analyzers and observers,
|
|
1676
|
+
and calls user's start() method.
|
|
1677
|
+
"""
|
|
1678
|
+
# Calculate and set required minimum period
|
|
1679
|
+
self._periodset()
|
|
1680
|
+
# Start analyzers
|
|
1681
|
+
for analyzer in itertools.chain(self.analyzers, self._slave_analyzers):
|
|
1682
|
+
analyzer._start()
|
|
1683
|
+
# Start observers
|
|
1684
|
+
for obs in self.observers:
|
|
1685
|
+
if not isinstance(obs, list):
|
|
1686
|
+
obs = [obs] # support of multi-data observers
|
|
1687
|
+
|
|
1688
|
+
for o in obs:
|
|
1689
|
+
o._start()
|
|
1690
|
+
|
|
1691
|
+
# Change operators to stage 2
|
|
1692
|
+
self._stage2()
|
|
1693
|
+
# Current length of each data
|
|
1694
|
+
self._dlens = [len(data) for data in self.datas]
|
|
1695
|
+
self._datetime_line = self.lines.datetime
|
|
1696
|
+
try:
|
|
1697
|
+
self._datetime_line_direct = not self._datetime_line.bindings
|
|
1698
|
+
except AttributeError:
|
|
1699
|
+
self._datetime_line_direct = False
|
|
1700
|
+
try:
|
|
1701
|
+
strategy_lines = self.lines.lines
|
|
1702
|
+
if (
|
|
1703
|
+
len(strategy_lines) == 1
|
|
1704
|
+
and strategy_lines[0] is self._datetime_line
|
|
1705
|
+
and self._datetime_line_direct
|
|
1706
|
+
and self._datetime_line.mode != self._datetime_line.QBuffer
|
|
1707
|
+
):
|
|
1708
|
+
self._single_line_forward_line = self._datetime_line
|
|
1709
|
+
else:
|
|
1710
|
+
self._single_line_forward_line = None
|
|
1711
|
+
except AttributeError:
|
|
1712
|
+
self._single_line_forward_line = None
|
|
1713
|
+
if len(self.datas) == 1:
|
|
1714
|
+
self._single_clock_data = self.datas[0]
|
|
1715
|
+
self._single_clock_datetime_line = self._single_clock_data.datetime
|
|
1716
|
+
self._single_clock_len_line = self._single_clock_datetime_line
|
|
1717
|
+
if len(self.datas) == 1 and len(self._minperiods) == 1:
|
|
1718
|
+
self._single_minperiod_data = self.datas[0]
|
|
1719
|
+
self._single_minperiod = self._minperiods[0]
|
|
1720
|
+
self._single_minperiod_len_line = self._single_minperiod_data.lines[0]
|
|
1721
|
+
from .linebuffer import LineActions
|
|
1722
|
+
|
|
1723
|
+
self._lineaction_datas = tuple(
|
|
1724
|
+
data for data in self.datas if isinstance(data, LineActions) and hasattr(data, "_next")
|
|
1725
|
+
)
|
|
1726
|
+
# Current minimum period status defaults to MAXINT (start in prenext)
|
|
1727
|
+
self._minperstatus = MAXINT
|
|
1728
|
+
# Call user's start()
|
|
1729
|
+
self.start()
|
|
1730
|
+
self._quicknotify = self.cerebro.p.quicknotify
|
|
1731
|
+
self._strategy_next_lineactions_cache = self._get_strategy_next_lineactions()
|
|
1732
|
+
self._has_strategy_next_lineactions = bool(self._strategy_next_lineactions_cache)
|
|
1733
|
+
self._all_analyzers_cache = list(self.analyzers) + list(self._slave_analyzers)
|
|
1734
|
+
self._has_analyzers = bool(self.analyzers)
|
|
1735
|
+
self._has_observers = bool(self.stats.items)
|
|
1736
|
+
self._notify_cashvalue_default = (
|
|
1737
|
+
"notify_cashvalue" not in self.__dict__
|
|
1738
|
+
and type(self).notify_cashvalue is Strategy.notify_cashvalue
|
|
1739
|
+
)
|
|
1740
|
+
self._notify_fund_default = (
|
|
1741
|
+
"notify_fund" not in self.__dict__ and type(self).notify_fund is Strategy.notify_fund
|
|
1742
|
+
)
|
|
1743
|
+
self._skip_empty_notify = (
|
|
1744
|
+
not self._quicknotify
|
|
1745
|
+
and not self._all_analyzers_cache
|
|
1746
|
+
and self._notify_cashvalue_default
|
|
1747
|
+
and self._notify_fund_default
|
|
1748
|
+
)
|
|
1749
|
+
self._fast_simple_next = (
|
|
1750
|
+
len(self.datas) == 1
|
|
1751
|
+
and len(self._minperiods) == 1
|
|
1752
|
+
and not self._lineiterators[LineIterator.IndType]
|
|
1753
|
+
and not self._lineaction_datas
|
|
1754
|
+
and not self._has_strategy_next_lineactions
|
|
1755
|
+
and self._skip_empty_notify
|
|
1756
|
+
and not self._has_analyzers
|
|
1757
|
+
and not self._has_observers
|
|
1758
|
+
)
|
|
1759
|
+
self._fast_simple_clock_update = (
|
|
1760
|
+
self._fast_simple_next
|
|
1761
|
+
and not self._oldsync
|
|
1762
|
+
and len(self.datas) == 1
|
|
1763
|
+
and self._single_line_forward_line is self._datetime_line
|
|
1764
|
+
and self._single_clock_len_line is self._single_clock_datetime_line
|
|
1765
|
+
and not self._data_assignment_pending
|
|
1766
|
+
and self._clock is not None
|
|
1767
|
+
)
|
|
1768
|
+
if self._fast_simple_next and type(self)._next is Strategy._next:
|
|
1769
|
+
object.__setattr__(self, "_next", self._next_fast_simple)
|
|
1770
|
+
|
|
1771
|
+
def start(self):
|
|
1772
|
+
"""Called right before the backtesting is about to be started.
|
|
1773
|
+
|
|
1774
|
+
This is a hook for strategies to perform initialization before
|
|
1775
|
+
the backtesting loop begins.
|
|
1776
|
+
"""
|
|
1777
|
+
|
|
1778
|
+
def getwriterheaders(self):
|
|
1779
|
+
"""Get the CSV headers for writer output.
|
|
1780
|
+
|
|
1781
|
+
Returns:
|
|
1782
|
+
list: Headers including indicator/observer names and line aliases
|
|
1783
|
+
"""
|
|
1784
|
+
# Filter indicators and observers for CSV output
|
|
1785
|
+
self.indobscsv = [self]
|
|
1786
|
+
# Filter indicators and observers, include only those with csv=True
|
|
1787
|
+
indobs = itertools.chain(self.getindicators_lines(), self.getobservers())
|
|
1788
|
+
self.indobscsv.extend(filter(lambda x: x.csv, indobs))
|
|
1789
|
+
# Initialize headers as empty list
|
|
1790
|
+
headers = []
|
|
1791
|
+
|
|
1792
|
+
# Prepare the indicators/observers data headers
|
|
1793
|
+
# Loop through indicators/observers marked for CSV output
|
|
1794
|
+
for iocsv in self.indobscsv:
|
|
1795
|
+
# Get indicator/observer name or class name
|
|
1796
|
+
name = iocsv.plotinfo.plotname or iocsv.__class__.__name__
|
|
1797
|
+
# Add name, length, and line aliases to headers
|
|
1798
|
+
headers.append(name)
|
|
1799
|
+
headers.append("len")
|
|
1800
|
+
headers.extend(iocsv.getlinealiases())
|
|
1801
|
+
# Return headers
|
|
1802
|
+
return headers
|
|
1803
|
+
|
|
1804
|
+
def getwritervalues(self):
|
|
1805
|
+
"""Get current values for writer output.
|
|
1806
|
+
|
|
1807
|
+
Returns:
|
|
1808
|
+
list: Current values from indicators and observers
|
|
1809
|
+
"""
|
|
1810
|
+
values = []
|
|
1811
|
+
# Loop through indicators/observers
|
|
1812
|
+
for iocsv in self.indobscsv:
|
|
1813
|
+
name = iocsv.plotinfo.plotname or iocsv.__class__.__name__
|
|
1814
|
+
values.append(name)
|
|
1815
|
+
lio = len(iocsv)
|
|
1816
|
+
values.append(lio)
|
|
1817
|
+
# If length > 0, get each value
|
|
1818
|
+
if lio:
|
|
1819
|
+
values.extend(map(lambda line: line[0], iocsv.lines.itersize()))
|
|
1820
|
+
else:
|
|
1821
|
+
values.extend([""] * iocsv.lines.size())
|
|
1822
|
+
|
|
1823
|
+
return values
|
|
1824
|
+
|
|
1825
|
+
def getwriterinfo(self):
|
|
1826
|
+
"""Get comprehensive writer information including params and analysis.
|
|
1827
|
+
|
|
1828
|
+
Returns:
|
|
1829
|
+
AutoOrderedDict: Nested structure containing params, indicators,
|
|
1830
|
+
observers, and analyzer results
|
|
1831
|
+
"""
|
|
1832
|
+
# Initialize writer info as AutoOrderedDict
|
|
1833
|
+
wrinfo = AutoOrderedDict()
|
|
1834
|
+
# Set parameters
|
|
1835
|
+
wrinfo["Params"] = self.p._getkwargs()
|
|
1836
|
+
|
|
1837
|
+
sections = [["Indicators", self.getindicators_lines()], ["Observers", self.getobservers()]]
|
|
1838
|
+
# Loop through indicators and observers
|
|
1839
|
+
for sectname, sectitems in sections:
|
|
1840
|
+
# Set specific values
|
|
1841
|
+
sinfo = wrinfo[sectname]
|
|
1842
|
+
for item in sectitems:
|
|
1843
|
+
itname = item.__class__.__name__
|
|
1844
|
+
sinfo[itname].Lines = item.lines.getlinealiases() or None
|
|
1845
|
+
sinfo[itname].Params = item.p._getkwargs() or None
|
|
1846
|
+
# Set analyzer values
|
|
1847
|
+
ainfo = wrinfo.Analyzers
|
|
1848
|
+
|
|
1849
|
+
# Internal Value Analyzer
|
|
1850
|
+
ainfo.Value.Begin = self.broker.startingcash
|
|
1851
|
+
ainfo.Value.End = self.broker.getvalue()
|
|
1852
|
+
|
|
1853
|
+
# No slave analyzers for a writer
|
|
1854
|
+
for aname, analyzer in self.analyzers.getitems():
|
|
1855
|
+
ainfo[aname].Params = analyzer.p._getkwargs() or None
|
|
1856
|
+
ainfo[aname].Analysis = analyzer.get_analysis()
|
|
1857
|
+
|
|
1858
|
+
return wrinfo
|
|
1859
|
+
|
|
1860
|
+
def nextstart(self):
|
|
1861
|
+
# Iteration 29 lifecycle INFO: minperiod first satisfied (once).
|
|
1862
|
+
logger.info(
|
|
1863
|
+
"strategy nextstart: strategy=%s minperiod=%d", type(self).__name__, self._minperiod
|
|
1864
|
+
)
|
|
1865
|
+
super().nextstart()
|
|
1866
|
+
|
|
1867
|
+
def _stop(self):
|
|
1868
|
+
# Iteration 29 lifecycle INFO (once per strategy).
|
|
1869
|
+
logger.info("strategy stopping: strategy=%s", type(self).__name__)
|
|
1870
|
+
# CRITICAL FIX: In runonce mode, ensure indicator lencount matches strategy length
|
|
1871
|
+
# This must be done BEFORE calling user's stop() method, as tests check len(indicator) == len(strategy)
|
|
1872
|
+
try:
|
|
1873
|
+
strategy_len = len(self)
|
|
1874
|
+
strategy_clock = getattr(self, "_clock", None)
|
|
1875
|
+
# Update lencount for all indicators to match strategy length
|
|
1876
|
+
# This is critical for runonce mode where indicators are pre-calculated but lencount may not match
|
|
1877
|
+
if hasattr(self, "_lineiterators"):
|
|
1878
|
+
from .lineiterator import LineIterator
|
|
1879
|
+
|
|
1880
|
+
for indicator in self._lineiterators.get(LineIterator.IndType, []):
|
|
1881
|
+
if getattr(indicator, "_clock", None) is not strategy_clock:
|
|
1882
|
+
continue
|
|
1883
|
+
# Update lencount for all lines in the indicator to match strategy length
|
|
1884
|
+
if hasattr(indicator, "lines") and hasattr(indicator.lines, "lines"):
|
|
1885
|
+
for line in indicator.lines.lines:
|
|
1886
|
+
if hasattr(line, "lencount"):
|
|
1887
|
+
# In runonce mode, set lencount to match strategy length (which equals data length)
|
|
1888
|
+
# This ensures len(indicator) == len(strategy) for test assertions
|
|
1889
|
+
line.lencount = strategy_len
|
|
1890
|
+
except Exception:
|
|
1891
|
+
logger.warning("Failed to update indicator lencount in _stop", exc_info=True)
|
|
1892
|
+
|
|
1893
|
+
# CRITICAL FIX: Restore last valid datetime before calling user's stop()
|
|
1894
|
+
# This ensures datetime[0] is valid for logging in stop() method
|
|
1895
|
+
if hasattr(self, "_last_valid_datetime") and self._last_valid_datetime > 0:
|
|
1896
|
+
try:
|
|
1897
|
+
# Restore strategy datetime
|
|
1898
|
+
self.lines.datetime[0] = self._last_valid_datetime
|
|
1899
|
+
# CRITICAL: Also restore all data feed datetimes
|
|
1900
|
+
for data in self.datas:
|
|
1901
|
+
try:
|
|
1902
|
+
data.datetime[0] = self._last_valid_datetime
|
|
1903
|
+
except Exception:
|
|
1904
|
+
logger.debug(
|
|
1905
|
+
"Failed to restore datetime for data %s in _stop",
|
|
1906
|
+
getattr(data, "_name", data),
|
|
1907
|
+
)
|
|
1908
|
+
except Exception:
|
|
1909
|
+
logger.debug("Failed to restore strategy datetime in _stop", exc_info=True)
|
|
1910
|
+
|
|
1911
|
+
# Call user's stop() method - can be overridden in strategy subclass
|
|
1912
|
+
self.stop()
|
|
1913
|
+
# Stop analyzers (both user-added and slave analyzers for observers)
|
|
1914
|
+
for analyzer in itertools.chain(self.analyzers, self._slave_analyzers):
|
|
1915
|
+
analyzer._stop()
|
|
1916
|
+
|
|
1917
|
+
# Stop observers (flush logs, etc.)
|
|
1918
|
+
for observer in self._get_all_observers():
|
|
1919
|
+
try:
|
|
1920
|
+
if hasattr(observer, "stop"):
|
|
1921
|
+
observer.stop()
|
|
1922
|
+
except Exception:
|
|
1923
|
+
logger.warning(
|
|
1924
|
+
"Observer %s.stop() raised an exception",
|
|
1925
|
+
type(observer).__name__,
|
|
1926
|
+
exc_info=True,
|
|
1927
|
+
)
|
|
1928
|
+
|
|
1929
|
+
# Change operators back to stage 1 - allows reuse of datas
|
|
1930
|
+
self._stage1()
|
|
1931
|
+
|
|
1932
|
+
def stop(self):
|
|
1933
|
+
"""Called right before the backtesting is about to be stopped.
|
|
1934
|
+
|
|
1935
|
+
This is a hook for strategies to perform cleanup or final logging.
|
|
1936
|
+
"""
|
|
1937
|
+
|
|
1938
|
+
def set_tradehistory(self, onoff=True):
|
|
1939
|
+
"""Enable or disable trade history tracking.
|
|
1940
|
+
|
|
1941
|
+
Args:
|
|
1942
|
+
onoff: If True, keep full trade history; if False, only track current trade
|
|
1943
|
+
"""
|
|
1944
|
+
self._tradehistoryon = onoff
|
|
1945
|
+
|
|
1946
|
+
def clear(self):
|
|
1947
|
+
"""Clear pending orders and trades.
|
|
1948
|
+
|
|
1949
|
+
Moves pending orders to _orders list and clears pending trades.
|
|
1950
|
+
"""
|
|
1951
|
+
self._orders.extend(self._orderspending)
|
|
1952
|
+
self._orderspending = []
|
|
1953
|
+
self._tradespending = []
|
|
1954
|
+
|
|
1955
|
+
def _addnotification(self, order, quicknotify=False):
|
|
1956
|
+
"""Add order notification and process trade updates.
|
|
1957
|
+
|
|
1958
|
+
Args:
|
|
1959
|
+
order: The order that has been updated
|
|
1960
|
+
quicknotify: If True, immediately process notification without queueing
|
|
1961
|
+
"""
|
|
1962
|
+
# If not simulated trading, add order to pending orders
|
|
1963
|
+
if not order.p.simulated:
|
|
1964
|
+
self._orderspending.append(order)
|
|
1965
|
+
# If in quick notify mode, initialize qorders and qtrades
|
|
1966
|
+
if quicknotify:
|
|
1967
|
+
qorders = [order]
|
|
1968
|
+
qtrades: list = []
|
|
1969
|
+
# If order has no executed volume
|
|
1970
|
+
if not order.executed.size:
|
|
1971
|
+
# If in quick notify mode, call _notify with info
|
|
1972
|
+
if quicknotify:
|
|
1973
|
+
self._notify(qorders=qorders, qtrades=qtrades)
|
|
1974
|
+
return
|
|
1975
|
+
# Get trade data - if order.data._compensate is None, use order.data; otherwise use order.data._compensate
|
|
1976
|
+
tradedata = getattr(order.data, "_compensate", None)
|
|
1977
|
+
if tradedata is None:
|
|
1978
|
+
tradedata = order.data
|
|
1979
|
+
# Get trade data - if trade exists in _trades, use the last one; otherwise create a new trade and save to datatrades
|
|
1980
|
+
tradekey = trade_key_from_order(order)
|
|
1981
|
+
datatrades = self._trades[tradedata][tradekey]
|
|
1982
|
+
if not datatrades:
|
|
1983
|
+
trade = Trade(data=tradedata, tradeid=tradekey, historyon=self._tradehistoryon)
|
|
1984
|
+
datatrades.append(trade)
|
|
1985
|
+
else:
|
|
1986
|
+
trade = datatrades[-1]
|
|
1987
|
+
# Loop through order execution bits
|
|
1988
|
+
for exbit in order.executed.iterpending():
|
|
1989
|
+
# If execution bit is None, break loop
|
|
1990
|
+
if exbit is None:
|
|
1991
|
+
break
|
|
1992
|
+
# If execution bit indicates closed position
|
|
1993
|
+
if exbit.closed:
|
|
1994
|
+
# Update trade
|
|
1995
|
+
trade.update(
|
|
1996
|
+
order,
|
|
1997
|
+
exbit.closed,
|
|
1998
|
+
exbit.price,
|
|
1999
|
+
exbit.closedvalue,
|
|
2000
|
+
exbit.closedcomm,
|
|
2001
|
+
exbit.pnl,
|
|
2002
|
+
comminfo=order.comminfo,
|
|
2003
|
+
)
|
|
2004
|
+
# If trade is closed
|
|
2005
|
+
if trade.isclosed:
|
|
2006
|
+
# Copy trade and add to _tradespending
|
|
2007
|
+
self._tradespending.append(copy.copy(trade))
|
|
2008
|
+
# If quick notify needed, copy trade and add to qtrades
|
|
2009
|
+
if quicknotify:
|
|
2010
|
+
qtrades.append(copy.copy(trade))
|
|
2011
|
+
|
|
2012
|
+
# Update it if needed
|
|
2013
|
+
# If order execution bit indicates opened position
|
|
2014
|
+
if exbit.opened:
|
|
2015
|
+
# If trade is closed, create new trade and save to datatrades
|
|
2016
|
+
if trade.isclosed:
|
|
2017
|
+
trade = Trade(data=tradedata, tradeid=tradekey, historyon=self._tradehistoryon)
|
|
2018
|
+
datatrades.append(trade)
|
|
2019
|
+
# Update trade
|
|
2020
|
+
trade.update(
|
|
2021
|
+
order,
|
|
2022
|
+
exbit.opened,
|
|
2023
|
+
exbit.price,
|
|
2024
|
+
exbit.openedvalue,
|
|
2025
|
+
exbit.openedcomm,
|
|
2026
|
+
exbit.pnl,
|
|
2027
|
+
comminfo=order.comminfo,
|
|
2028
|
+
)
|
|
2029
|
+
|
|
2030
|
+
# This extra check covers the case in which different tradeid
|
|
2031
|
+
# orders have put the position down to 0 and the next order
|
|
2032
|
+
# "opens" a position but "closes" the trade
|
|
2033
|
+
# If trade is closed
|
|
2034
|
+
if trade.isclosed:
|
|
2035
|
+
# Copy trade and add to _tradespending
|
|
2036
|
+
self._tradespending.append(copy.copy(trade))
|
|
2037
|
+
# If quick notify needed, copy trade and add to qtrades
|
|
2038
|
+
if quicknotify:
|
|
2039
|
+
qtrades.append(copy.copy(trade))
|
|
2040
|
+
# If trade was just opened
|
|
2041
|
+
if trade.justopened:
|
|
2042
|
+
# Copy trade and add to _tradespending
|
|
2043
|
+
self._tradespending.append(copy.copy(trade))
|
|
2044
|
+
# If quick notify needed, copy trade and add to qtrades
|
|
2045
|
+
if quicknotify:
|
|
2046
|
+
qtrades.append(copy.copy(trade))
|
|
2047
|
+
# If quick notify needed, call _notify
|
|
2048
|
+
if quicknotify:
|
|
2049
|
+
self._notify(qorders=qorders, qtrades=qtrades)
|
|
2050
|
+
|
|
2051
|
+
def _notify(self, qorders=None, qtrades=None):
|
|
2052
|
+
"""Notify order and trade events to strategy and analyzers.
|
|
2053
|
+
|
|
2054
|
+
Args:
|
|
2055
|
+
qorders: Quick notify orders (empty list if not in quick notify mode)
|
|
2056
|
+
qtrades: Quick notify trades (empty list if not in quick notify mode)
|
|
2057
|
+
"""
|
|
2058
|
+
# If quick notify is enabled
|
|
2059
|
+
try:
|
|
2060
|
+
quicknotify_enabled = self._quicknotify
|
|
2061
|
+
except AttributeError:
|
|
2062
|
+
quicknotify_enabled = self.cerebro.p.quicknotify
|
|
2063
|
+
|
|
2064
|
+
if quicknotify_enabled:
|
|
2065
|
+
if qorders is None:
|
|
2066
|
+
qorders = []
|
|
2067
|
+
if qtrades is None:
|
|
2068
|
+
qtrades = []
|
|
2069
|
+
# Need to know if quicknotify is on, to not reprocess pendingorders
|
|
2070
|
+
# and pendingtrades, which have to exist for things like observers
|
|
2071
|
+
# which look into it
|
|
2072
|
+
# Pending orders and trades are qorders and qtrades
|
|
2073
|
+
procorders = qorders
|
|
2074
|
+
proctrades = qtrades
|
|
2075
|
+
# Otherwise use orders and trades saved in _orderspending and _tradespending
|
|
2076
|
+
else:
|
|
2077
|
+
if qorders is None:
|
|
2078
|
+
qorders = ()
|
|
2079
|
+
procorders = self._orderspending
|
|
2080
|
+
proctrades = self._tradespending
|
|
2081
|
+
|
|
2082
|
+
# PERFORMANCE OPTIMIZATION: Cache merged analyzer list to avoid repeated itertools.chain
|
|
2083
|
+
# This is called 688K+ times, so caching makes a significant difference
|
|
2084
|
+
try:
|
|
2085
|
+
all_analyzers = self._all_analyzers_cache
|
|
2086
|
+
except AttributeError:
|
|
2087
|
+
all_analyzers = list(self.analyzers) + list(self._slave_analyzers)
|
|
2088
|
+
self._all_analyzers_cache = all_analyzers
|
|
2089
|
+
|
|
2090
|
+
try:
|
|
2091
|
+
notify_cashvalue_default = self._notify_cashvalue_default
|
|
2092
|
+
except AttributeError:
|
|
2093
|
+
notify_cashvalue_default = (
|
|
2094
|
+
"notify_cashvalue" not in self.__dict__
|
|
2095
|
+
and type(self).notify_cashvalue is Strategy.notify_cashvalue
|
|
2096
|
+
)
|
|
2097
|
+
try:
|
|
2098
|
+
notify_fund_default = self._notify_fund_default
|
|
2099
|
+
except AttributeError:
|
|
2100
|
+
notify_fund_default = (
|
|
2101
|
+
"notify_fund" not in self.__dict__
|
|
2102
|
+
and type(self).notify_fund is Strategy.notify_fund
|
|
2103
|
+
)
|
|
2104
|
+
|
|
2105
|
+
if (
|
|
2106
|
+
not quicknotify_enabled
|
|
2107
|
+
and not qorders
|
|
2108
|
+
and not procorders
|
|
2109
|
+
and not proctrades
|
|
2110
|
+
and not all_analyzers
|
|
2111
|
+
and notify_cashvalue_default
|
|
2112
|
+
and notify_fund_default
|
|
2113
|
+
):
|
|
2114
|
+
return
|
|
2115
|
+
|
|
2116
|
+
# Loop through pending orders
|
|
2117
|
+
for order in procorders:
|
|
2118
|
+
# If order execution type is not Historical or histnotify, notify order
|
|
2119
|
+
if order.exectype != order.Historical or order.histnotify:
|
|
2120
|
+
self.notify_order(order)
|
|
2121
|
+
# Notify order to analyzers (both user and slave analyzers)
|
|
2122
|
+
for analyzer in all_analyzers:
|
|
2123
|
+
analyzer._notify_order(order)
|
|
2124
|
+
# Notify order to observers (e.g., TradeLogger)
|
|
2125
|
+
self._notify_order_to_observers(order)
|
|
2126
|
+
# Loop through pending trades, notify, and notify analyzers
|
|
2127
|
+
for trade in proctrades:
|
|
2128
|
+
self.notify_trade(trade)
|
|
2129
|
+
for analyzer in all_analyzers:
|
|
2130
|
+
analyzer._notify_trade(trade)
|
|
2131
|
+
# Notify trade to observers (e.g., TradeLogger)
|
|
2132
|
+
self._notify_trade_to_observers(trade)
|
|
2133
|
+
# If qorders is not empty, return after processing orders
|
|
2134
|
+
if qorders:
|
|
2135
|
+
return # cash is notified regularly
|
|
2136
|
+
# Get cash, value, fundvalue, fundshares
|
|
2137
|
+
cash = self.broker.getcash()
|
|
2138
|
+
value = self.broker.getvalue()
|
|
2139
|
+
fundvalue = self.broker.fundvalue
|
|
2140
|
+
fundshares = self.broker.fundshares
|
|
2141
|
+
# Notify cash and value values, and notify analyzers
|
|
2142
|
+
if not notify_cashvalue_default:
|
|
2143
|
+
self.notify_cashvalue(cash, value)
|
|
2144
|
+
# Notify fund values, and notify analyzers
|
|
2145
|
+
if not notify_fund_default:
|
|
2146
|
+
self.notify_fund(cash, value, fundvalue, fundshares)
|
|
2147
|
+
for analyzer in all_analyzers:
|
|
2148
|
+
analyzer._notify_cashvalue(cash, value)
|
|
2149
|
+
analyzer._notify_fund(cash, value, fundvalue, fundshares)
|
|
2150
|
+
|
|
2151
|
+
def add_timer(
|
|
2152
|
+
self,
|
|
2153
|
+
when,
|
|
2154
|
+
offset=datetime.timedelta(),
|
|
2155
|
+
repeat=datetime.timedelta(),
|
|
2156
|
+
weekdays=None,
|
|
2157
|
+
weekcarry=False,
|
|
2158
|
+
monthdays=None,
|
|
2159
|
+
monthcarry=True,
|
|
2160
|
+
allow=None,
|
|
2161
|
+
tzdata=None,
|
|
2162
|
+
cheat=False,
|
|
2163
|
+
*args,
|
|
2164
|
+
**kwargs,
|
|
2165
|
+
):
|
|
2166
|
+
"""Schedule a timer to invoke notify_timer or a callback.
|
|
2167
|
+
|
|
2168
|
+
Note: Can be called during __init__ or start
|
|
2169
|
+
|
|
2170
|
+
Schedules a timer to invoke either a specified callback or the
|
|
2171
|
+
notify_timer of one or more strategies.
|
|
2172
|
+
|
|
2173
|
+
Args:
|
|
2174
|
+
when: Can be:
|
|
2175
|
+
- datetime.time instance (see tzdata below)
|
|
2176
|
+
- bt.timer.SESSION_START to reference session start
|
|
2177
|
+
- bt.timer.SESSION_END to reference session end
|
|
2178
|
+
offset (datetime.timedelta): Offset the when value. Used with
|
|
2179
|
+
SESSION_START/SESSION_END to trigger after session start/end.
|
|
2180
|
+
repeat (datetime.timedelta): If set, timer repeats at this interval
|
|
2181
|
+
within the same session. Resets to original when after session end.
|
|
2182
|
+
weekdays (list): Sorted iterable with integers (Monday=1, Sunday=7)
|
|
2183
|
+
indicating which days the timer can be invoked. Empty = all days.
|
|
2184
|
+
weekcarry (bool): If True and weekday not seen (e.g., holiday),
|
|
2185
|
+
execute on next day (even if in new week).
|
|
2186
|
+
monthdays (list): Sorted iterable with integers (1-31) indicating
|
|
2187
|
+
which days of month to execute. Empty = all days.
|
|
2188
|
+
monthcarry (bool): If True and day not seen (weekend, holiday),
|
|
2189
|
+
execute on next available day.
|
|
2190
|
+
allow (callable): Callback receiving datetime.date, returns True if
|
|
2191
|
+
date is allowed for timer execution.
|
|
2192
|
+
tzdata: Timezone data - None, pytz instance, or data feed instance.
|
|
2193
|
+
If None and when is SESSION_START/END, uses first data feed.
|
|
2194
|
+
cheat (bool): If True, timer called before broker evaluates orders,
|
|
2195
|
+
allowing orders based on opening price.
|
|
2196
|
+
*args: Additional args passed to notify_timer
|
|
2197
|
+
**kwargs: Additional kwargs passed to notify_timer
|
|
2198
|
+
|
|
2199
|
+
Returns:
|
|
2200
|
+
The created timer instance
|
|
2201
|
+
"""
|
|
2202
|
+
return self.cerebro._add_timer(
|
|
2203
|
+
owner=self,
|
|
2204
|
+
when=when,
|
|
2205
|
+
offset=offset,
|
|
2206
|
+
repeat=repeat,
|
|
2207
|
+
weekdays=weekdays,
|
|
2208
|
+
weekcarry=weekcarry,
|
|
2209
|
+
monthdays=monthdays,
|
|
2210
|
+
monthcarry=monthcarry,
|
|
2211
|
+
allow=allow,
|
|
2212
|
+
tzdata=tzdata,
|
|
2213
|
+
strats=False,
|
|
2214
|
+
cheat=cheat,
|
|
2215
|
+
*args,
|
|
2216
|
+
**kwargs,
|
|
2217
|
+
)
|
|
2218
|
+
|
|
2219
|
+
def notify_timer(self, timer, when, *args, **kwargs):
|
|
2220
|
+
"""Receive timer notifications.
|
|
2221
|
+
|
|
2222
|
+
Receives a timer notification where ``timer`` is the timer instance
|
|
2223
|
+
returned by ``add_timer``, and ``when`` is the calling time. ``args``
|
|
2224
|
+
and ``kwargs`` are any additional arguments passed to ``add_timer``.
|
|
2225
|
+
|
|
2226
|
+
The actual ``when`` time can be later than expected, as the system may
|
|
2227
|
+
not have been able to call the timer before. This value is the timer's
|
|
2228
|
+
scheduled time, not the actual system time.
|
|
2229
|
+
|
|
2230
|
+
Args:
|
|
2231
|
+
timer: The timer instance created by add_timer
|
|
2232
|
+
when: The scheduled time when the timer was triggered
|
|
2233
|
+
*args: Additional positional arguments passed to add_timer
|
|
2234
|
+
**kwargs: Additional keyword arguments passed to add_timer
|
|
2235
|
+
"""
|
|
2236
|
+
|
|
2237
|
+
def notify_idle(self):
|
|
2238
|
+
"""Receive a live-engine poll when no data bar or tick was produced.
|
|
2239
|
+
|
|
2240
|
+
Live brokers may still need strategies to advance execution deadlines,
|
|
2241
|
+
reconciliation and risk controls while market data is silent. The
|
|
2242
|
+
default hook is intentionally empty and is only dispatched for strategy
|
|
2243
|
+
classes that override it.
|
|
2244
|
+
"""
|
|
2245
|
+
|
|
2246
|
+
def notify_cashvalue(self, cash, value):
|
|
2247
|
+
"""Notify the current cash and value of the strategy's broker.
|
|
2248
|
+
|
|
2249
|
+
Args:
|
|
2250
|
+
cash: Current cash amount
|
|
2251
|
+
value: Current portfolio value
|
|
2252
|
+
"""
|
|
2253
|
+
|
|
2254
|
+
def notify_fund(self, cash, value, fundvalue, shares):
|
|
2255
|
+
"""Notify the current cash, value, fund value, and fund shares.
|
|
2256
|
+
|
|
2257
|
+
Args:
|
|
2258
|
+
cash: Current cash amount
|
|
2259
|
+
value: Current portfolio value
|
|
2260
|
+
fundvalue: Current fund value
|
|
2261
|
+
shares: Current fund shares
|
|
2262
|
+
"""
|
|
2263
|
+
|
|
2264
|
+
def notify_order(self, order):
|
|
2265
|
+
"""Receive notification when an order status changes.
|
|
2266
|
+
|
|
2267
|
+
Args:
|
|
2268
|
+
order: The order with changed status
|
|
2269
|
+
"""
|
|
2270
|
+
|
|
2271
|
+
def notify_trade(self, trade):
|
|
2272
|
+
"""Receive notification when a trade status changes.
|
|
2273
|
+
|
|
2274
|
+
Args:
|
|
2275
|
+
trade: The trade with changed status
|
|
2276
|
+
"""
|
|
2277
|
+
|
|
2278
|
+
def notify_store(self, msg, *args, **kwargs):
|
|
2279
|
+
"""Receive notification from a store provider.
|
|
2280
|
+
|
|
2281
|
+
Args:
|
|
2282
|
+
msg: Message from the store
|
|
2283
|
+
*args: Additional positional arguments
|
|
2284
|
+
**kwargs: Additional keyword arguments
|
|
2285
|
+
"""
|
|
2286
|
+
|
|
2287
|
+
def notify_data(self, data, status, *args, **kwargs):
|
|
2288
|
+
"""Receive notification from a data feed.
|
|
2289
|
+
|
|
2290
|
+
Args:
|
|
2291
|
+
data: The data feed sending the notification
|
|
2292
|
+
status: Status code
|
|
2293
|
+
*args: Additional positional arguments
|
|
2294
|
+
**kwargs: Additional keyword arguments
|
|
2295
|
+
"""
|
|
2296
|
+
|
|
2297
|
+
# ========== Tick/Channel Event Callbacks ==========
|
|
2298
|
+
|
|
2299
|
+
def notify_tick(self, tick):
|
|
2300
|
+
"""Called when a new tick event arrives.
|
|
2301
|
+
|
|
2302
|
+
Override this method to implement tick-level trading logic.
|
|
2303
|
+
|
|
2304
|
+
Args:
|
|
2305
|
+
tick: TickEvent instance with price, volume, direction, etc.
|
|
2306
|
+
"""
|
|
2307
|
+
|
|
2308
|
+
def notify_orderbook(self, orderbook):
|
|
2309
|
+
"""Called when a new order book snapshot arrives.
|
|
2310
|
+
|
|
2311
|
+
Override this method to implement orderbook-based trading logic.
|
|
2312
|
+
|
|
2313
|
+
Args:
|
|
2314
|
+
orderbook: OrderBookSnapshot instance with bids, asks, spread, etc.
|
|
2315
|
+
"""
|
|
2316
|
+
|
|
2317
|
+
def notify_funding(self, funding):
|
|
2318
|
+
"""Called when a new funding rate event arrives.
|
|
2319
|
+
|
|
2320
|
+
Override this method to implement funding rate arbitrage or
|
|
2321
|
+
position management.
|
|
2322
|
+
|
|
2323
|
+
Args:
|
|
2324
|
+
funding: FundingEvent instance with rate, mark_price, etc.
|
|
2325
|
+
"""
|
|
2326
|
+
|
|
2327
|
+
def notify_bar(self, bar):
|
|
2328
|
+
"""Called when a bar event arrives from the channel system.
|
|
2329
|
+
|
|
2330
|
+
This is different from the standard next() method which processes
|
|
2331
|
+
bars from LineSeries data feeds. This callback handles BarEvents
|
|
2332
|
+
from the channel/queue system.
|
|
2333
|
+
|
|
2334
|
+
Args:
|
|
2335
|
+
bar: BarEvent instance with open, high, low, close, volume.
|
|
2336
|
+
"""
|
|
2337
|
+
|
|
2338
|
+
def get_last_tick(self, symbol=None):
|
|
2339
|
+
"""Get the last tick for a symbol.
|
|
2340
|
+
|
|
2341
|
+
Args:
|
|
2342
|
+
symbol: Symbol name. If None, returns first available.
|
|
2343
|
+
|
|
2344
|
+
Returns:
|
|
2345
|
+
TickEvent or None.
|
|
2346
|
+
"""
|
|
2347
|
+
if symbol:
|
|
2348
|
+
return self._last_tick.get(symbol)
|
|
2349
|
+
if self._last_tick:
|
|
2350
|
+
return next(iter(self._last_tick.values()))
|
|
2351
|
+
return None
|
|
2352
|
+
|
|
2353
|
+
def get_last_orderbook(self, symbol=None):
|
|
2354
|
+
"""Get the last order book for a symbol.
|
|
2355
|
+
|
|
2356
|
+
Args:
|
|
2357
|
+
symbol: Symbol name. If None, returns first available.
|
|
2358
|
+
|
|
2359
|
+
Returns:
|
|
2360
|
+
OrderBookSnapshot or None.
|
|
2361
|
+
"""
|
|
2362
|
+
if symbol:
|
|
2363
|
+
return self._last_ob.get(symbol)
|
|
2364
|
+
if self._last_ob:
|
|
2365
|
+
return next(iter(self._last_ob.values()))
|
|
2366
|
+
return None
|
|
2367
|
+
|
|
2368
|
+
def get_last_funding(self, symbol=None):
|
|
2369
|
+
"""Get the last funding rate for a symbol.
|
|
2370
|
+
|
|
2371
|
+
Args:
|
|
2372
|
+
symbol: Symbol name. If None, returns first available.
|
|
2373
|
+
|
|
2374
|
+
Returns:
|
|
2375
|
+
FundingEvent or None.
|
|
2376
|
+
"""
|
|
2377
|
+
if symbol:
|
|
2378
|
+
return self._last_funding.get(symbol)
|
|
2379
|
+
if self._last_funding:
|
|
2380
|
+
return next(iter(self._last_funding.values()))
|
|
2381
|
+
return None
|
|
2382
|
+
|
|
2383
|
+
# ========== Data Access Methods ==========
|
|
2384
|
+
|
|
2385
|
+
def _register_hft_data(self, data):
|
|
2386
|
+
"""Register a channel-mode data reference for HFT order routing."""
|
|
2387
|
+
symbol = getattr(data, "symbol", None) or getattr(data, "_name", None)
|
|
2388
|
+
if symbol is None:
|
|
2389
|
+
return None
|
|
2390
|
+
if not hasattr(self, "_hft_data_refs"):
|
|
2391
|
+
self._hft_data_refs = {}
|
|
2392
|
+
self._hft_data_refs[str(symbol)] = data
|
|
2393
|
+
return data
|
|
2394
|
+
|
|
2395
|
+
def get_hft_data(self, symbol=None):
|
|
2396
|
+
"""Return a data reference suitable for channel-mode HFT orders.
|
|
2397
|
+
|
|
2398
|
+
In channel-only runs there may be no LineSeries data feed attached
|
|
2399
|
+
to the strategy, but standard order APIs still need a data identity.
|
|
2400
|
+
Cerebro registers lightweight per-symbol references before invoking
|
|
2401
|
+
``notify_tick`` / ``notify_orderbook`` / ``notify_bar`` callbacks.
|
|
2402
|
+
|
|
2403
|
+
Args:
|
|
2404
|
+
symbol: Optional symbol to look up. If omitted and exactly one
|
|
2405
|
+
HFT data reference exists, that reference is returned.
|
|
2406
|
+
|
|
2407
|
+
Returns:
|
|
2408
|
+
A LineSeries data feed or a channel-mode data reference.
|
|
2409
|
+
|
|
2410
|
+
Raises:
|
|
2411
|
+
KeyError: If the requested symbol is unknown or no reference has
|
|
2412
|
+
been registered yet.
|
|
2413
|
+
ValueError: If ``symbol`` is omitted but more than one HFT data
|
|
2414
|
+
reference is available.
|
|
2415
|
+
"""
|
|
2416
|
+
if not hasattr(self, "_hft_data_refs"):
|
|
2417
|
+
self._hft_data_refs = {}
|
|
2418
|
+
|
|
2419
|
+
if symbol is None:
|
|
2420
|
+
if len(self._hft_data_refs) == 1:
|
|
2421
|
+
return next(iter(self._hft_data_refs.values()))
|
|
2422
|
+
if self.datas:
|
|
2423
|
+
return self.datas[0]
|
|
2424
|
+
if not self._hft_data_refs:
|
|
2425
|
+
raise KeyError("No HFT channel data reference is available yet")
|
|
2426
|
+
raise ValueError("Multiple HFT data references exist; pass symbol explicitly")
|
|
2427
|
+
|
|
2428
|
+
symbol = str(symbol)
|
|
2429
|
+
data = self._hft_data_refs.get(symbol)
|
|
2430
|
+
if data is not None:
|
|
2431
|
+
return data
|
|
2432
|
+
|
|
2433
|
+
if symbol in self.env.datasbyname:
|
|
2434
|
+
return self.env.datasbyname[symbol]
|
|
2435
|
+
|
|
2436
|
+
raise KeyError(f"No HFT channel data reference for symbol {symbol!r}")
|
|
2437
|
+
|
|
2438
|
+
def getdatanames(self):
|
|
2439
|
+
"""Get a list of all data names in the system.
|
|
2440
|
+
|
|
2441
|
+
Returns:
|
|
2442
|
+
list: Names of all data feeds
|
|
2443
|
+
"""
|
|
2444
|
+
return keys(self.env.datasbyname)
|
|
2445
|
+
|
|
2446
|
+
def getdatabyname(self, name):
|
|
2447
|
+
"""Get a data feed by its name.
|
|
2448
|
+
|
|
2449
|
+
Args:
|
|
2450
|
+
name: Name of the data feed
|
|
2451
|
+
|
|
2452
|
+
Returns:
|
|
2453
|
+
The data feed with the given name
|
|
2454
|
+
"""
|
|
2455
|
+
return self.env.datasbyname[name]
|
|
2456
|
+
|
|
2457
|
+
def cancel(self, order):
|
|
2458
|
+
"""Cancel an order in the broker.
|
|
2459
|
+
|
|
2460
|
+
Args:
|
|
2461
|
+
order: The order to cancel
|
|
2462
|
+
"""
|
|
2463
|
+
self.broker.cancel(order)
|
|
2464
|
+
|
|
2465
|
+
def buy(
|
|
2466
|
+
self,
|
|
2467
|
+
data=None,
|
|
2468
|
+
size=None,
|
|
2469
|
+
price=None,
|
|
2470
|
+
plimit=None,
|
|
2471
|
+
exectype=None,
|
|
2472
|
+
valid=None,
|
|
2473
|
+
tradeid=0,
|
|
2474
|
+
oco=None,
|
|
2475
|
+
trailamount=None,
|
|
2476
|
+
trailpercent=None,
|
|
2477
|
+
parent=None,
|
|
2478
|
+
transmit=True,
|
|
2479
|
+
**kwargs,
|
|
2480
|
+
) -> Optional[Order]:
|
|
2481
|
+
"""Create a buy (long) order and send it to the broker.
|
|
2482
|
+
|
|
2483
|
+
Args:
|
|
2484
|
+
data: The data feed for the order. If None, uses the first data feed
|
|
2485
|
+
(self.data).
|
|
2486
|
+
size: Size to use (positive) for the order. If None, the sizer
|
|
2487
|
+
instance retrieved via getsizer will determine the size.
|
|
2488
|
+
price: Price to use. None is valid for Market and Close orders.
|
|
2489
|
+
For Limit, Stop and StopLimit orders this determines the
|
|
2490
|
+
trigger point.
|
|
2491
|
+
plimit: Only applicable to StopLimit orders. This is the price at
|
|
2492
|
+
which to set the implicit Limit order, once the Stop has been
|
|
2493
|
+
triggered.
|
|
2494
|
+
trailamount: For StopTrail/StopTrailLimit orders, an absolute amount
|
|
2495
|
+
which determines the distance to the price to keep the trailing
|
|
2496
|
+
stop.
|
|
2497
|
+
trailpercent: For StopTrail/StopTrailLimit orders, a percentage
|
|
2498
|
+
amount which determines the distance to the price to keep the
|
|
2499
|
+
trailing stop.
|
|
2500
|
+
exectype: Execution type. Possible values:
|
|
2501
|
+
- Order.Market or None: Market order
|
|
2502
|
+
- Order.Limit: Limit order
|
|
2503
|
+
- Order.Stop: Stop order
|
|
2504
|
+
- Order.StopLimit: Stop-limit order
|
|
2505
|
+
- Order.Close: Close order
|
|
2506
|
+
- Order.StopTrail: Stop-trail order
|
|
2507
|
+
- Order.StopTrailLimit: Stop-trail-limit order
|
|
2508
|
+
valid: Order validity. Possible values:
|
|
2509
|
+
- None: Good till cancel
|
|
2510
|
+
- datetime.datetime/date: Good till date
|
|
2511
|
+
- Order.DAY: Day order
|
|
2512
|
+
tradeid: Internal value to track overlapping trades.
|
|
2513
|
+
oco: Another order instance for OCO (Order Cancel Others) group.
|
|
2514
|
+
parent: Controls the relationship of a group of orders (e.g., bracket
|
|
2515
|
+
orders).
|
|
2516
|
+
transmit: If True, transmit the order to the broker. Used for
|
|
2517
|
+
controlling bracket orders.
|
|
2518
|
+
**kwargs: Additional broker-specific parameters.
|
|
2519
|
+
|
|
2520
|
+
Returns:
|
|
2521
|
+
The submitted order, or None if size is 0.
|
|
2522
|
+
|
|
2523
|
+
Example:
|
|
2524
|
+
Create a market buy order:
|
|
2525
|
+
>>> order = self.buy()
|
|
2526
|
+
|
|
2527
|
+
Create a limit buy order:
|
|
2528
|
+
>>> order = self.buy(price=100.0, exectype=Order.Limit)
|
|
2529
|
+
"""
|
|
2530
|
+
# Resolve data argument
|
|
2531
|
+
if isinstance(data, string_types):
|
|
2532
|
+
data = self.getdatabyname(data)
|
|
2533
|
+
elif data is None:
|
|
2534
|
+
if self.datas:
|
|
2535
|
+
data = self.datas[0]
|
|
2536
|
+
else:
|
|
2537
|
+
raise ValueError(
|
|
2538
|
+
"No data feed available. In channel mode, pass a data "
|
|
2539
|
+
"object explicitly or use self.get_hft_data(symbol)"
|
|
2540
|
+
)
|
|
2541
|
+
# Use the provided size, otherwise calculate via getsizer
|
|
2542
|
+
size = size if size is not None else self.getsizing(data, isbuy=True)
|
|
2543
|
+
# If size is non-zero, submit the order
|
|
2544
|
+
if size:
|
|
2545
|
+
order = self.broker.buy(
|
|
2546
|
+
self,
|
|
2547
|
+
data,
|
|
2548
|
+
size=abs(size),
|
|
2549
|
+
price=price,
|
|
2550
|
+
plimit=plimit,
|
|
2551
|
+
exectype=exectype,
|
|
2552
|
+
valid=valid,
|
|
2553
|
+
tradeid=tradeid,
|
|
2554
|
+
oco=oco,
|
|
2555
|
+
trailamount=trailamount,
|
|
2556
|
+
trailpercent=trailpercent,
|
|
2557
|
+
parent=parent,
|
|
2558
|
+
transmit=transmit,
|
|
2559
|
+
**kwargs,
|
|
2560
|
+
)
|
|
2561
|
+
# Auto-notify signal to TradeLogger observers
|
|
2562
|
+
signal_price = (
|
|
2563
|
+
price
|
|
2564
|
+
if price is not None
|
|
2565
|
+
else (
|
|
2566
|
+
data.close[0]
|
|
2567
|
+
if hasattr(data, "close") and hasattr(data.close, "__getitem__")
|
|
2568
|
+
else 0
|
|
2569
|
+
)
|
|
2570
|
+
)
|
|
2571
|
+
self._notify_signal_to_observers("buy", abs(size), signal_price, data)
|
|
2572
|
+
return order
|
|
2573
|
+
|
|
2574
|
+
return None
|
|
2575
|
+
|
|
2576
|
+
def sell(
|
|
2577
|
+
self,
|
|
2578
|
+
data=None,
|
|
2579
|
+
size=None,
|
|
2580
|
+
price=None,
|
|
2581
|
+
plimit=None,
|
|
2582
|
+
exectype=None,
|
|
2583
|
+
valid=None,
|
|
2584
|
+
tradeid=0,
|
|
2585
|
+
oco=None,
|
|
2586
|
+
trailamount=None,
|
|
2587
|
+
trailpercent=None,
|
|
2588
|
+
parent=None,
|
|
2589
|
+
transmit=True,
|
|
2590
|
+
**kwargs,
|
|
2591
|
+
) -> Optional[Order]:
|
|
2592
|
+
"""Create a sell (short) order and send it to the broker.
|
|
2593
|
+
|
|
2594
|
+
See the documentation for ``buy`` for an explanation of the parameters.
|
|
2595
|
+
|
|
2596
|
+
Returns:
|
|
2597
|
+
The submitted order, or None if no order was created
|
|
2598
|
+
"""
|
|
2599
|
+
# Resolve data argument
|
|
2600
|
+
if isinstance(data, string_types):
|
|
2601
|
+
data = self.getdatabyname(data)
|
|
2602
|
+
elif data is None:
|
|
2603
|
+
if self.datas:
|
|
2604
|
+
data = self.datas[0]
|
|
2605
|
+
else:
|
|
2606
|
+
raise ValueError(
|
|
2607
|
+
"No data feed available. In channel mode, pass a data "
|
|
2608
|
+
"object explicitly or use self.get_hft_data(symbol)"
|
|
2609
|
+
)
|
|
2610
|
+
size = size if size is not None else self.getsizing(data, isbuy=False)
|
|
2611
|
+
if size:
|
|
2612
|
+
order = self.broker.sell(
|
|
2613
|
+
self,
|
|
2614
|
+
data,
|
|
2615
|
+
size=abs(size),
|
|
2616
|
+
price=price,
|
|
2617
|
+
plimit=plimit,
|
|
2618
|
+
exectype=exectype,
|
|
2619
|
+
valid=valid,
|
|
2620
|
+
tradeid=tradeid,
|
|
2621
|
+
oco=oco,
|
|
2622
|
+
trailamount=trailamount,
|
|
2623
|
+
trailpercent=trailpercent,
|
|
2624
|
+
parent=parent,
|
|
2625
|
+
transmit=transmit,
|
|
2626
|
+
**kwargs,
|
|
2627
|
+
)
|
|
2628
|
+
# Auto-notify signal to TradeLogger observers
|
|
2629
|
+
signal_price = (
|
|
2630
|
+
price
|
|
2631
|
+
if price is not None
|
|
2632
|
+
else (
|
|
2633
|
+
data.close[0]
|
|
2634
|
+
if hasattr(data, "close") and hasattr(data.close, "__getitem__")
|
|
2635
|
+
else 0
|
|
2636
|
+
)
|
|
2637
|
+
)
|
|
2638
|
+
self._notify_signal_to_observers("sell", abs(size), signal_price, data)
|
|
2639
|
+
return order
|
|
2640
|
+
|
|
2641
|
+
return None
|
|
2642
|
+
|
|
2643
|
+
def close(self, data=None, size=None, **kwargs) -> Optional[Order]:
|
|
2644
|
+
"""Close a long or short position.
|
|
2645
|
+
|
|
2646
|
+
Creates an order that counters the existing position to close it.
|
|
2647
|
+
|
|
2648
|
+
Args:
|
|
2649
|
+
data: The data feed for which to close the position.
|
|
2650
|
+
If None, uses the default data feed.
|
|
2651
|
+
size: The size to close. If None, closes the entire position.
|
|
2652
|
+
**kwargs: Additional keyword arguments passed to the order.
|
|
2653
|
+
|
|
2654
|
+
Note:
|
|
2655
|
+
If size is not provided, it is automatically calculated from the
|
|
2656
|
+
existing position to fully close it.
|
|
2657
|
+
|
|
2658
|
+
Returns:
|
|
2659
|
+
The submitted order, or None if no position exists
|
|
2660
|
+
"""
|
|
2661
|
+
# Get the data feed
|
|
2662
|
+
if isinstance(data, string_types):
|
|
2663
|
+
data = self.getdatabyname(data)
|
|
2664
|
+
elif data is None:
|
|
2665
|
+
data = self.data
|
|
2666
|
+
position_side = kwargs.pop("position_side", None)
|
|
2667
|
+
position_side = normalize_position_side(position_side)
|
|
2668
|
+
broker_mode = normalize_position_mode(
|
|
2669
|
+
getattr(self.broker, "get_param", lambda *_args, **_kwargs: "net")(
|
|
2670
|
+
"position_mode", "net"
|
|
2671
|
+
)
|
|
2672
|
+
)
|
|
2673
|
+
|
|
2674
|
+
if position_side is not None or broker_mode == POSITION_MODE_DUAL_SIDE:
|
|
2675
|
+
if position_side is None:
|
|
2676
|
+
long_size = abs(self.getposition(data, self.broker, side=POSITION_SIDE_LONG).size)
|
|
2677
|
+
short_size = abs(self.getposition(data, self.broker, side=POSITION_SIDE_SHORT).size)
|
|
2678
|
+
if long_size and short_size:
|
|
2679
|
+
raise ValueError(
|
|
2680
|
+
"close() requires position_side when both long and short legs are open"
|
|
2681
|
+
)
|
|
2682
|
+
if long_size:
|
|
2683
|
+
position_side = POSITION_SIDE_LONG
|
|
2684
|
+
possize = long_size
|
|
2685
|
+
elif short_size:
|
|
2686
|
+
position_side = POSITION_SIDE_SHORT
|
|
2687
|
+
possize = short_size
|
|
2688
|
+
else:
|
|
2689
|
+
return None
|
|
2690
|
+
else:
|
|
2691
|
+
possize = abs(self.getposition(data, self.broker, side=position_side).size)
|
|
2692
|
+
|
|
2693
|
+
size = abs(size if size is not None else possize)
|
|
2694
|
+
if not size:
|
|
2695
|
+
return None
|
|
2696
|
+
|
|
2697
|
+
kwargs.setdefault("position_side", position_side)
|
|
2698
|
+
kwargs.setdefault("offset", POSITION_OFFSET_CLOSE)
|
|
2699
|
+
if position_side == POSITION_SIDE_LONG:
|
|
2700
|
+
return self.sell(data=data, size=size, **kwargs)
|
|
2701
|
+
return self.buy(data=data, size=size, **kwargs)
|
|
2702
|
+
# Get the current position size
|
|
2703
|
+
possize = self.getposition(data, self.broker).size
|
|
2704
|
+
# If size is None, close the entire position; otherwise close the specified size
|
|
2705
|
+
size = abs(size if size is not None else possize)
|
|
2706
|
+
# If position is long (positive), sell to close
|
|
2707
|
+
if possize > 0:
|
|
2708
|
+
return self.sell(data=data, size=size, **kwargs)
|
|
2709
|
+
# If position is short (negative), buy to close
|
|
2710
|
+
if possize < 0:
|
|
2711
|
+
return self.buy(data=data, size=size, **kwargs)
|
|
2712
|
+
|
|
2713
|
+
return None
|
|
2714
|
+
|
|
2715
|
+
def buy_bracket(
|
|
2716
|
+
self,
|
|
2717
|
+
data=None,
|
|
2718
|
+
size=None,
|
|
2719
|
+
price=None,
|
|
2720
|
+
plimit=None,
|
|
2721
|
+
exectype=Order.Limit,
|
|
2722
|
+
valid=None,
|
|
2723
|
+
tradeid=0,
|
|
2724
|
+
trailamount=None,
|
|
2725
|
+
trailpercent=None,
|
|
2726
|
+
oargs=None,
|
|
2727
|
+
stopprice=None,
|
|
2728
|
+
stopexec=Order.Stop,
|
|
2729
|
+
stopargs=None,
|
|
2730
|
+
limitprice=None,
|
|
2731
|
+
limitexec=Order.Limit,
|
|
2732
|
+
limitargs=None,
|
|
2733
|
+
**kwargs,
|
|
2734
|
+
):
|
|
2735
|
+
"""Create a bracket order group (buy order with stop-loss and take-profit).
|
|
2736
|
+
|
|
2737
|
+
Creates a bracket order group consisting of:
|
|
2738
|
+
- A main **buy** order with the specified execution type (default: Limit)
|
|
2739
|
+
- A *low side* bracket **sell** stop-loss order
|
|
2740
|
+
- A *high side* bracket **sell** take-profit order
|
|
2741
|
+
|
|
2742
|
+
Args:
|
|
2743
|
+
- ``data`` (default: ``None``): The data feed for the order. If None,
|
|
2744
|
+
uses the first data feed (self.data).
|
|
2745
|
+
|
|
2746
|
+
- ``size`` (default: ``None``): Size for the order. If None, the sizer
|
|
2747
|
+
determines the size. The same size is applied to all three orders.
|
|
2748
|
+
|
|
2749
|
+
- ``price`` (default: ``None``): Price for the main buy order. None
|
|
2750
|
+
is valid for Market and Close orders.
|
|
2751
|
+
|
|
2752
|
+
- ``plimit`` (default: ``None``): Price limit for StopLimit orders.
|
|
2753
|
+
|
|
2754
|
+
- ``trailamount`` (default: ``None``): Absolute trailing amount for
|
|
2755
|
+
StopTrail/StopTrailLimit orders.
|
|
2756
|
+
|
|
2757
|
+
- ``trailpercent`` (default: ``None``): Percentage trailing amount for
|
|
2758
|
+
StopTrail/StopTrailLimit orders.
|
|
2759
|
+
|
|
2760
|
+
- ``exectype`` (default: ``bt.Order.Limit``): Execution type for the
|
|
2761
|
+
main order. See buy() documentation for possible values.
|
|
2762
|
+
|
|
2763
|
+
- ``valid`` (default: ``None``): Order validity period. See buy()
|
|
2764
|
+
documentation for possible values.
|
|
2765
|
+
|
|
2766
|
+
- ``tradeid`` (default: ``0``): Trade ID for tracking overlapping trades.
|
|
2767
|
+
|
|
2768
|
+
- ``oargs`` (default: ``{}``): Specific keyword arguments (dict) for
|
|
2769
|
+
the main side order. Applied before **kwargs.
|
|
2770
|
+
|
|
2771
|
+
- ``**kwargs``: Additional keyword arguments applied to all three
|
|
2772
|
+
orders. See buy() documentation for possible values.
|
|
2773
|
+
|
|
2774
|
+
- ``stopprice`` (default: ``None``): Specific price for the stop-loss
|
|
2775
|
+
order.
|
|
2776
|
+
|
|
2777
|
+
- ``stopexec`` (default: ``bt.Order.Stop``): Execution type for the
|
|
2778
|
+
stop-loss order.
|
|
2779
|
+
|
|
2780
|
+
- ``stopargs`` (default: ``{}``): Specific keyword arguments (dict)
|
|
2781
|
+
for the stop-loss order.
|
|
2782
|
+
|
|
2783
|
+
- ``limitprice`` (default: ``None``): Specific price for the take-profit
|
|
2784
|
+
order.
|
|
2785
|
+
|
|
2786
|
+
- ``limitexec`` (default: ``bt.Order.Limit``): Execution type for the
|
|
2787
|
+
take-profit order.
|
|
2788
|
+
|
|
2789
|
+
- ``limitargs`` (default: ``{}``): Specific keyword arguments (dict)
|
|
2790
|
+
for the take-profit order.
|
|
2791
|
+
|
|
2792
|
+
Returns:
|
|
2793
|
+
A list containing the three orders [main_order, stop_order, limit_order].
|
|
2794
|
+
Suppressed orders are represented as None.
|
|
2795
|
+
|
|
2796
|
+
Note:
|
|
2797
|
+
High/Low side orders can be suppressed by setting limitexec=None or
|
|
2798
|
+
stopexec=None.
|
|
2799
|
+
"""
|
|
2800
|
+
# Normalize mutable-default placeholders (B006); these dicts are only
|
|
2801
|
+
# read via kargs.update(...), never mutated, so None==empty is equivalent.
|
|
2802
|
+
oargs = {} if oargs is None else oargs
|
|
2803
|
+
stopargs = {} if stopargs is None else stopargs
|
|
2804
|
+
limitargs = {} if limitargs is None else limitargs
|
|
2805
|
+
# Build parameter dictionary
|
|
2806
|
+
kargs = {
|
|
2807
|
+
"size": size,
|
|
2808
|
+
"data": data,
|
|
2809
|
+
"price": price,
|
|
2810
|
+
"plimit": plimit,
|
|
2811
|
+
"exectype": exectype,
|
|
2812
|
+
"valid": valid,
|
|
2813
|
+
"tradeid": tradeid,
|
|
2814
|
+
"trailamount": trailamount,
|
|
2815
|
+
"trailpercent": trailpercent,
|
|
2816
|
+
}
|
|
2817
|
+
# Update with main side order specific arguments
|
|
2818
|
+
kargs.update(oargs)
|
|
2819
|
+
# Update with general keyword arguments
|
|
2820
|
+
kargs.update(kwargs)
|
|
2821
|
+
# Set transmit flag: only transmit if both stop and limit are None
|
|
2822
|
+
kargs["transmit"] = limitexec is None and stopexec is None
|
|
2823
|
+
# Create the main buy order
|
|
2824
|
+
o = self.buy(**kargs)
|
|
2825
|
+
|
|
2826
|
+
# Create stop-loss order
|
|
2827
|
+
if stopexec is not None:
|
|
2828
|
+
# low side / stop
|
|
2829
|
+
kargs = {
|
|
2830
|
+
"data": data,
|
|
2831
|
+
"price": stopprice,
|
|
2832
|
+
"exectype": stopexec,
|
|
2833
|
+
"valid": valid,
|
|
2834
|
+
"tradeid": tradeid,
|
|
2835
|
+
}
|
|
2836
|
+
kargs.update(stopargs)
|
|
2837
|
+
kargs.update(kwargs)
|
|
2838
|
+
kargs["parent"] = o
|
|
2839
|
+
kargs["transmit"] = limitexec is None
|
|
2840
|
+
kargs["size"] = o.size
|
|
2841
|
+
ostop = self.sell(**kargs)
|
|
2842
|
+
else:
|
|
2843
|
+
ostop = None
|
|
2844
|
+
|
|
2845
|
+
# Create take-profit order
|
|
2846
|
+
if limitexec is not None:
|
|
2847
|
+
# high side / limit
|
|
2848
|
+
kargs = {
|
|
2849
|
+
"data": data,
|
|
2850
|
+
"price": limitprice,
|
|
2851
|
+
"exectype": limitexec,
|
|
2852
|
+
"valid": valid,
|
|
2853
|
+
"tradeid": tradeid,
|
|
2854
|
+
}
|
|
2855
|
+
kargs.update(limitargs)
|
|
2856
|
+
kargs.update(kwargs)
|
|
2857
|
+
kargs["parent"] = o
|
|
2858
|
+
kargs["transmit"] = True
|
|
2859
|
+
kargs["size"] = o.size
|
|
2860
|
+
olimit = self.sell(**kargs)
|
|
2861
|
+
else:
|
|
2862
|
+
olimit = None
|
|
2863
|
+
|
|
2864
|
+
return [o, ostop, olimit]
|
|
2865
|
+
|
|
2866
|
+
def sell_bracket(
|
|
2867
|
+
self,
|
|
2868
|
+
data=None,
|
|
2869
|
+
size=None,
|
|
2870
|
+
price=None,
|
|
2871
|
+
plimit=None,
|
|
2872
|
+
exectype=Order.Limit,
|
|
2873
|
+
valid=None,
|
|
2874
|
+
tradeid=0,
|
|
2875
|
+
trailamount=None,
|
|
2876
|
+
trailpercent=None,
|
|
2877
|
+
oargs=None,
|
|
2878
|
+
stopprice=None,
|
|
2879
|
+
stopexec=Order.Stop,
|
|
2880
|
+
stopargs=None,
|
|
2881
|
+
limitprice=None,
|
|
2882
|
+
limitexec=Order.Limit,
|
|
2883
|
+
limitargs=None,
|
|
2884
|
+
**kwargs,
|
|
2885
|
+
):
|
|
2886
|
+
"""Create a sell bracket order group (sell order with stop-loss and take-profit).
|
|
2887
|
+
|
|
2888
|
+
Creates a bracket order group consisting of:
|
|
2889
|
+
- A main **sell** order with the specified execution type (default: Limit)
|
|
2890
|
+
- A *high side* bracket **buy** stop-loss order
|
|
2891
|
+
- A *low side* bracket **buy** take-profit order
|
|
2892
|
+
|
|
2893
|
+
Args:
|
|
2894
|
+
See buy_bracket() for parameter documentation.
|
|
2895
|
+
|
|
2896
|
+
Returns:
|
|
2897
|
+
A list containing the three orders [main_order, stop_order, limit_order].
|
|
2898
|
+
Suppressed orders are represented as None.
|
|
2899
|
+
|
|
2900
|
+
Note:
|
|
2901
|
+
High/Low side orders can be suppressed by setting limitexec=None or
|
|
2902
|
+
stopexec=None.
|
|
2903
|
+
"""
|
|
2904
|
+
# Normalize mutable-default placeholders (B006); read-only via update().
|
|
2905
|
+
oargs = {} if oargs is None else oargs
|
|
2906
|
+
stopargs = {} if stopargs is None else stopargs
|
|
2907
|
+
limitargs = {} if limitargs is None else limitargs
|
|
2908
|
+
kargs = {
|
|
2909
|
+
"size": size,
|
|
2910
|
+
"data": data,
|
|
2911
|
+
"price": price,
|
|
2912
|
+
"plimit": plimit,
|
|
2913
|
+
"exectype": exectype,
|
|
2914
|
+
"valid": valid,
|
|
2915
|
+
"tradeid": tradeid,
|
|
2916
|
+
"trailamount": trailamount,
|
|
2917
|
+
"trailpercent": trailpercent,
|
|
2918
|
+
}
|
|
2919
|
+
kargs.update(oargs)
|
|
2920
|
+
kargs.update(kwargs)
|
|
2921
|
+
kargs["transmit"] = limitexec is None and stopexec is None
|
|
2922
|
+
o = self.sell(**kargs)
|
|
2923
|
+
|
|
2924
|
+
if stopexec is not None:
|
|
2925
|
+
# high side / stop
|
|
2926
|
+
kargs = {
|
|
2927
|
+
"data": data,
|
|
2928
|
+
"price": stopprice,
|
|
2929
|
+
"exectype": stopexec,
|
|
2930
|
+
"valid": valid,
|
|
2931
|
+
"tradeid": tradeid,
|
|
2932
|
+
}
|
|
2933
|
+
kargs.update(stopargs)
|
|
2934
|
+
kargs.update(kwargs)
|
|
2935
|
+
kargs["parent"] = o
|
|
2936
|
+
kargs["transmit"] = limitexec is None # transmit if last
|
|
2937
|
+
kargs["size"] = o.size
|
|
2938
|
+
ostop = self.buy(**kargs)
|
|
2939
|
+
else:
|
|
2940
|
+
ostop = None
|
|
2941
|
+
|
|
2942
|
+
if limitexec is not None:
|
|
2943
|
+
# low side / limit
|
|
2944
|
+
kargs = {
|
|
2945
|
+
"data": data,
|
|
2946
|
+
"price": limitprice,
|
|
2947
|
+
"exectype": limitexec,
|
|
2948
|
+
"valid": valid,
|
|
2949
|
+
"tradeid": tradeid,
|
|
2950
|
+
}
|
|
2951
|
+
kargs.update(limitargs)
|
|
2952
|
+
kargs.update(kwargs)
|
|
2953
|
+
kargs["parent"] = o
|
|
2954
|
+
kargs["transmit"] = True
|
|
2955
|
+
kargs["size"] = o.size
|
|
2956
|
+
olimit = self.buy(**kargs)
|
|
2957
|
+
else:
|
|
2958
|
+
olimit = None
|
|
2959
|
+
|
|
2960
|
+
return [o, ostop, olimit]
|
|
2961
|
+
|
|
2962
|
+
def order_target_size(self, data=None, target=0, **kwargs) -> Optional[Order]:
|
|
2963
|
+
"""Place an order to achieve a target position size.
|
|
2964
|
+
|
|
2965
|
+
Rebalances the current position to reach the specified target size.
|
|
2966
|
+
|
|
2967
|
+
Args:
|
|
2968
|
+
data: The data feed for the order. If None, uses the default data feed.
|
|
2969
|
+
target: Target position size.
|
|
2970
|
+
- If target > pos.size: buy (target - pos.size)
|
|
2971
|
+
- If target < pos.size: sell (pos.size - target)
|
|
2972
|
+
- If target == 0: close the entire position
|
|
2973
|
+
**kwargs: Additional keyword arguments passed to buy/sell.
|
|
2974
|
+
|
|
2975
|
+
Returns:
|
|
2976
|
+
The generated order, or None if target == current position size.
|
|
2977
|
+
"""
|
|
2978
|
+
# Get the specific data feed
|
|
2979
|
+
if isinstance(data, string_types):
|
|
2980
|
+
data = self.getdatabyname(data)
|
|
2981
|
+
elif data is None:
|
|
2982
|
+
data = self.data
|
|
2983
|
+
|
|
2984
|
+
# Get the current position size
|
|
2985
|
+
possize = self.getposition(data, self.broker).size
|
|
2986
|
+
# If target is 0 and position exists, close the position
|
|
2987
|
+
if not target and possize:
|
|
2988
|
+
return self.close(data=data, size=possize, **kwargs)
|
|
2989
|
+
# If target is greater than current position, buy to increase
|
|
2990
|
+
if target > possize:
|
|
2991
|
+
return self.buy(data=data, size=target - possize, **kwargs)
|
|
2992
|
+
# If target is less than current position, sell to decrease
|
|
2993
|
+
if target < possize:
|
|
2994
|
+
return self.sell(data=data, size=possize - target, **kwargs)
|
|
2995
|
+
|
|
2996
|
+
return None # no execution target == possize
|
|
2997
|
+
|
|
2998
|
+
def order_target_value(self, data=None, target=0.0, price=None, **kwargs) -> Optional[Order]:
|
|
2999
|
+
"""Place an order to achieve a target position value.
|
|
3000
|
+
|
|
3001
|
+
Rebalances the position to reach the specified target value.
|
|
3002
|
+
|
|
3003
|
+
Args:
|
|
3004
|
+
data: The data feed for the order. If None, uses the default data feed.
|
|
3005
|
+
target: Target position value in currency units.
|
|
3006
|
+
- If target is 0: close position
|
|
3007
|
+
- If target > value: buy to increase value
|
|
3008
|
+
- If target < value: sell to decrease value
|
|
3009
|
+
price: Price for size calculation. If None, uses data.close[0].
|
|
3010
|
+
**kwargs: Additional keyword arguments passed to buy/sell.
|
|
3011
|
+
|
|
3012
|
+
Returns:
|
|
3013
|
+
The generated order, or None if no order was issued.
|
|
3014
|
+
"""
|
|
3015
|
+
# Get the data feed
|
|
3016
|
+
if isinstance(data, string_types):
|
|
3017
|
+
data = self.getdatabyname(data)
|
|
3018
|
+
elif data is None:
|
|
3019
|
+
data = self.data
|
|
3020
|
+
# Get the current position size
|
|
3021
|
+
possize = self.getposition(data, self.broker).size
|
|
3022
|
+
# If target is 0 and position exists, close the position
|
|
3023
|
+
if not target and possize: # closing a position
|
|
3024
|
+
return self.close(data=data, size=possize, price=price, **kwargs)
|
|
3025
|
+
# Otherwise, rebalance to target value
|
|
3026
|
+
# Get the current value of this data
|
|
3027
|
+
value = self.broker.getvalue(datas=[data])
|
|
3028
|
+
# Get commission info for size calculation
|
|
3029
|
+
comminfo = self.broker.getcommissioninfo(data)
|
|
3030
|
+
# Get price: use provided price or default to close price
|
|
3031
|
+
# Make sure a price is there
|
|
3032
|
+
price = price if price is not None else data.close[0]
|
|
3033
|
+
# If target value is greater than current value, buy
|
|
3034
|
+
if target > value:
|
|
3035
|
+
size = comminfo.getsize(price, target - value)
|
|
3036
|
+
return self.buy(data=data, size=size, price=price, **kwargs)
|
|
3037
|
+
# If target value is less than current value, sell
|
|
3038
|
+
if target < value:
|
|
3039
|
+
size = comminfo.getsize(price, value - target)
|
|
3040
|
+
return self.sell(data=data, size=size, price=price, **kwargs)
|
|
3041
|
+
|
|
3042
|
+
return None # no execution size == possize
|
|
3043
|
+
|
|
3044
|
+
def order_target_percent(self, data=None, target=0.0, **kwargs) -> Optional[Order]:
|
|
3045
|
+
"""Place an order to achieve a target percentage of portfolio value.
|
|
3046
|
+
|
|
3047
|
+
Rebalances the position so its value equals the target percentage
|
|
3048
|
+
of the total portfolio value.
|
|
3049
|
+
|
|
3050
|
+
Args:
|
|
3051
|
+
data: The data feed for the order. If None, uses the default data feed.
|
|
3052
|
+
target: Target percentage as a decimal (e.g., 0.05 for 5%).
|
|
3053
|
+
**kwargs: Additional keyword arguments passed to order_target_value.
|
|
3054
|
+
|
|
3055
|
+
Returns:
|
|
3056
|
+
The generated order, or None if no order was issued.
|
|
3057
|
+
|
|
3058
|
+
Example:
|
|
3059
|
+
With target=0.05 and portfolio value of 100:
|
|
3060
|
+
- Target value = 0.05 * 100 = 5
|
|
3061
|
+
- Orders are placed through order_target_value
|
|
3062
|
+
|
|
3063
|
+
Note:
|
|
3064
|
+
Position direction (long/short) is considered:
|
|
3065
|
+
- If target > value: buy if pos.size >= 0, sell if pos.size < 0
|
|
3066
|
+
- If target < value: sell if pos.size >= 0, buy if pos.size < 0
|
|
3067
|
+
"""
|
|
3068
|
+
# Get the data feed
|
|
3069
|
+
if isinstance(data, string_types):
|
|
3070
|
+
data = self.getdatabyname(data)
|
|
3071
|
+
elif data is None:
|
|
3072
|
+
data = self.data
|
|
3073
|
+
# Calculate target value based on portfolio value
|
|
3074
|
+
# Note: Getting position size here is not necessary
|
|
3075
|
+
# possize = self.getposition(data, self.broker).size
|
|
3076
|
+
target *= self.broker.getvalue()
|
|
3077
|
+
|
|
3078
|
+
return self.order_target_value(data=data, target=target, **kwargs)
|
|
3079
|
+
|
|
3080
|
+
def getposition(self, data=None, broker=None, side=None, **kwargs):
|
|
3081
|
+
"""Get the current position for a data feed.
|
|
3082
|
+
|
|
3083
|
+
Args:
|
|
3084
|
+
data: The data feed to get position for. If None, uses the first data feed.
|
|
3085
|
+
broker: The broker to query. If None, uses the default broker.
|
|
3086
|
+
|
|
3087
|
+
Returns:
|
|
3088
|
+
The current Position object.
|
|
3089
|
+
|
|
3090
|
+
Note:
|
|
3091
|
+
A property ``position`` is also available as a shortcut.
|
|
3092
|
+
"""
|
|
3093
|
+
data = data if data is not None else self.datas[0]
|
|
3094
|
+
broker = broker or self.broker
|
|
3095
|
+
return broker.getposition(data, side=side, **kwargs)
|
|
3096
|
+
|
|
3097
|
+
# Property to access position for the default data feed
|
|
3098
|
+
position = property(getposition)
|
|
3099
|
+
|
|
3100
|
+
def getpositionbyname(self, name=None, broker=None, side=None, **kwargs):
|
|
3101
|
+
"""Get the current position for a data feed by name.
|
|
3102
|
+
|
|
3103
|
+
Args:
|
|
3104
|
+
name: Name of the data feed. If None, uses the first data feed.
|
|
3105
|
+
broker: The broker to query. If None, uses the default broker.
|
|
3106
|
+
|
|
3107
|
+
Returns:
|
|
3108
|
+
The current Position object.
|
|
3109
|
+
|
|
3110
|
+
Note:
|
|
3111
|
+
A property ``positionbyname`` is also available as a shortcut.
|
|
3112
|
+
"""
|
|
3113
|
+
data = self.datas[0] if not name else self.getdatabyname(name)
|
|
3114
|
+
broker = broker or self.broker
|
|
3115
|
+
return broker.getposition(data, side=side, **kwargs)
|
|
3116
|
+
|
|
3117
|
+
# Property to access position by name
|
|
3118
|
+
positionbyname = property(getpositionbyname)
|
|
3119
|
+
|
|
3120
|
+
def getpositions(self, broker=None):
|
|
3121
|
+
"""Get all positions from the broker.
|
|
3122
|
+
|
|
3123
|
+
Args:
|
|
3124
|
+
broker: The broker to query. If None, uses the default broker.
|
|
3125
|
+
|
|
3126
|
+
Returns:
|
|
3127
|
+
Dictionary mapping data feeds to Position objects.
|
|
3128
|
+
|
|
3129
|
+
Note:
|
|
3130
|
+
A property ``positions`` is also available as a shortcut.
|
|
3131
|
+
"""
|
|
3132
|
+
broker = broker or self.broker
|
|
3133
|
+
return broker.positions
|
|
3134
|
+
|
|
3135
|
+
# Property to access all positions
|
|
3136
|
+
positions = property(getpositions)
|
|
3137
|
+
|
|
3138
|
+
def getpositionsbyname(self, broker=None):
|
|
3139
|
+
"""Get all positions from the broker indexed by data name.
|
|
3140
|
+
|
|
3141
|
+
Args:
|
|
3142
|
+
broker: The broker to query. If None, uses the default broker.
|
|
3143
|
+
|
|
3144
|
+
Returns:
|
|
3145
|
+
OrderedDict mapping data names to Position objects.
|
|
3146
|
+
|
|
3147
|
+
Note:
|
|
3148
|
+
A property ``positionsbyname`` is also available as a shortcut.
|
|
3149
|
+
"""
|
|
3150
|
+
broker = broker or self.broker
|
|
3151
|
+
positions = broker.positions
|
|
3152
|
+
|
|
3153
|
+
posbyname = collections.OrderedDict()
|
|
3154
|
+
for name, data in iteritems(self.env.datasbyname):
|
|
3155
|
+
posbyname[name] = positions[data]
|
|
3156
|
+
|
|
3157
|
+
return posbyname
|
|
3158
|
+
|
|
3159
|
+
# Property to access positions by name
|
|
3160
|
+
positionsbyname = property(getpositionsbyname)
|
|
3161
|
+
|
|
3162
|
+
def _addsizer(self, sizer, *args, **kwargs):
|
|
3163
|
+
"""Add a sizer to the strategy.
|
|
3164
|
+
|
|
3165
|
+
If sizer is None, uses FixedSize sizer. Otherwise instantiates
|
|
3166
|
+
the provided sizer class and sets it.
|
|
3167
|
+
|
|
3168
|
+
Args:
|
|
3169
|
+
sizer: Sizer class or None
|
|
3170
|
+
*args: Positional arguments for sizer instantiation
|
|
3171
|
+
**kwargs: Keyword arguments for sizer instantiation
|
|
3172
|
+
"""
|
|
3173
|
+
if sizer is None:
|
|
3174
|
+
self.setsizer(FixedSize())
|
|
3175
|
+
else:
|
|
3176
|
+
self.setsizer(sizer(*args, **kwargs))
|
|
3177
|
+
|
|
3178
|
+
def setsizer(self, sizer):
|
|
3179
|
+
"""Set the sizer for automatic stake calculation.
|
|
3180
|
+
|
|
3181
|
+
Args:
|
|
3182
|
+
sizer: The sizer instance to use
|
|
3183
|
+
|
|
3184
|
+
Returns:
|
|
3185
|
+
The sizer instance
|
|
3186
|
+
"""
|
|
3187
|
+
self._sizer = sizer
|
|
3188
|
+
sizer.set(self, self.broker)
|
|
3189
|
+
return sizer
|
|
3190
|
+
|
|
3191
|
+
def getsizer(self):
|
|
3192
|
+
"""Get the current sizer for automatic stake calculation.
|
|
3193
|
+
|
|
3194
|
+
Returns:
|
|
3195
|
+
The current sizer instance
|
|
3196
|
+
|
|
3197
|
+
Note:
|
|
3198
|
+
Also available as the ``sizer`` property.
|
|
3199
|
+
"""
|
|
3200
|
+
return self._sizer
|
|
3201
|
+
|
|
3202
|
+
sizer = property(getsizer, setsizer)
|
|
3203
|
+
|
|
3204
|
+
def getsizing(self, data=None, isbuy=True):
|
|
3205
|
+
"""Get the order size from the sizer.
|
|
3206
|
+
|
|
3207
|
+
Uses the configured sizer to calculate the appropriate stake size
|
|
3208
|
+
for the next order.
|
|
3209
|
+
|
|
3210
|
+
Args:
|
|
3211
|
+
data: The data feed for the order. If None, uses the default data.
|
|
3212
|
+
isbuy: True for buy orders, False for sell orders.
|
|
3213
|
+
|
|
3214
|
+
Returns:
|
|
3215
|
+
The calculated stake size.
|
|
3216
|
+
"""
|
|
3217
|
+
# Ensure sizer has broker reference
|
|
3218
|
+
if hasattr(self._sizer, "broker") and self._sizer.broker is None:
|
|
3219
|
+
self._sizer.set(self, self.broker)
|
|
3220
|
+
return self._sizer.getsizing(data, isbuy)
|
|
3221
|
+
|
|
3222
|
+
|
|
3223
|
+
class SignalStrategy(Strategy):
|
|
3224
|
+
"""A strategy subclass that automatically operates using signals.
|
|
3225
|
+
|
|
3226
|
+
This strategy subclass responds to signal indicators to automatically
|
|
3227
|
+
enter and exit positions based on signal values.
|
|
3228
|
+
|
|
3229
|
+
Signal values:
|
|
3230
|
+
- ``> 0`` indicates a long (buy) signal
|
|
3231
|
+
- ``< 0`` indicates a short (sell) signal
|
|
3232
|
+
|
|
3233
|
+
There are five types of signals, broken into two groups:
|
|
3234
|
+
|
|
3235
|
+
**Main Group**:
|
|
3236
|
+
|
|
3237
|
+
- ``LONGSHORT``: Both long and short indications from this signal
|
|
3238
|
+
are taken. The strategy will go long or short based on the sign.
|
|
3239
|
+
|
|
3240
|
+
- ``LONG``:
|
|
3241
|
+
- Positive (long) indications: Go long
|
|
3242
|
+
- Negative (short) indications: Close long position
|
|
3243
|
+
- If ``LONGEXIT`` exists, it is used to exit longs
|
|
3244
|
+
- If ``SHORT`` signal exists and no ``LONGEXIT``, it will close
|
|
3245
|
+
longs before opening a short
|
|
3246
|
+
|
|
3247
|
+
- ``SHORT``:
|
|
3248
|
+
- Negative (short) indications: Go short
|
|
3249
|
+
- Positive (long) indications: Close short position
|
|
3250
|
+
- If ``SHORTEXIT`` exists, it is used to exit shorts
|
|
3251
|
+
- If ``LONG`` signal exists and no ``SHORTEXIT``, it will close
|
|
3252
|
+
shorts before opening a long
|
|
3253
|
+
|
|
3254
|
+
**Exit Group**:
|
|
3255
|
+
These signals override others to provide explicit exit criteria:
|
|
3256
|
+
|
|
3257
|
+
- ``LONGEXIT``: Negative indications are taken to exit long positions
|
|
3258
|
+
- ``SHORTEXIT``: Positive indications are taken to exit short positions
|
|
3259
|
+
|
|
3260
|
+
**Order Issuing**
|
|
3261
|
+
|
|
3262
|
+
Orders are placed with Market execution type and Good-Until-Canceled
|
|
3263
|
+
validity.
|
|
3264
|
+
|
|
3265
|
+
Params:
|
|
3266
|
+
|
|
3267
|
+
- ``signals`` (default: ``[]``): A list/tuple of lists/tuples for signal
|
|
3268
|
+
instantiation and type allocation. This parameter is typically managed
|
|
3269
|
+
through ``cerebro.add_signal``.
|
|
3270
|
+
|
|
3271
|
+
- ``_accumulate`` (default: ``False``): Allow entering the market even if
|
|
3272
|
+
already in a position (accumulate positions).
|
|
3273
|
+
|
|
3274
|
+
- ``_concurrent`` (default: ``False``): Allow issuing orders even when
|
|
3275
|
+
orders are already pending execution.
|
|
3276
|
+
|
|
3277
|
+
- ``_data`` (default: ``None``): If multiple datas are present in the
|
|
3278
|
+
system which is the target for orders. This can be
|
|
3279
|
+
|
|
3280
|
+
- ``None``: The first data in the system will be used
|
|
3281
|
+
|
|
3282
|
+
- An ``int``: indicating the data that was inserted at that position
|
|
3283
|
+
|
|
3284
|
+
- An ``str``: name given to the data when creating it (parameter
|
|
3285
|
+
``name``) or when adding it cerebro with ``cerebro.adddata(...,
|
|
3286
|
+
name=)``
|
|
3287
|
+
|
|
3288
|
+
- A ``data`` instance
|
|
3289
|
+
|
|
3290
|
+
"""
|
|
3291
|
+
|
|
3292
|
+
# Parameters for signal strategy
|
|
3293
|
+
params: tuple = (
|
|
3294
|
+
("signals", []),
|
|
3295
|
+
("_accumulate", False),
|
|
3296
|
+
("_concurrent", False),
|
|
3297
|
+
("_data", None),
|
|
3298
|
+
)
|
|
3299
|
+
|
|
3300
|
+
def __new__(cls, *args, **kwargs):
|
|
3301
|
+
"""Override __new__ to handle next method remapping that was done in MetaSigStrategy"""
|
|
3302
|
+
# Handle next method remapping like the old MetaSigStrategy.__new__ did
|
|
3303
|
+
if hasattr(cls, "next") and not hasattr(cls, "_next_custom"):
|
|
3304
|
+
cls._next_custom = cls.next
|
|
3305
|
+
|
|
3306
|
+
# Create the instance
|
|
3307
|
+
instance = super().__new__(cls, *args, **kwargs)
|
|
3308
|
+
|
|
3309
|
+
# Set the next method to _next_catch (from MetaSigStrategy)
|
|
3310
|
+
instance.next = instance._next_catch
|
|
3311
|
+
|
|
3312
|
+
return instance
|
|
3313
|
+
|
|
3314
|
+
def __init__(self, *args, **kwargs):
|
|
3315
|
+
"""Initialize the signal strategy with functionality from MetaSigStrategy methods"""
|
|
3316
|
+
# Handle the functionality that was in MetaSigStrategy.dopreinit
|
|
3317
|
+
self._signals = collections.defaultdict(list)
|
|
3318
|
+
|
|
3319
|
+
# Set the data target (from MetaSigStrategy.dopreinit)
|
|
3320
|
+
_data = getattr(self.p, "_data", None)
|
|
3321
|
+
if _data is None:
|
|
3322
|
+
self._dtarget = self.data0
|
|
3323
|
+
elif isinstance(_data, integer_types):
|
|
3324
|
+
self._dtarget = self.datas[_data]
|
|
3325
|
+
elif isinstance(_data, string_types):
|
|
3326
|
+
self._dtarget = self.getdatabyname(_data)
|
|
3327
|
+
elif isinstance(_data, LineRoot):
|
|
3328
|
+
self._dtarget = _data
|
|
3329
|
+
else:
|
|
3330
|
+
self._dtarget = self.data0
|
|
3331
|
+
|
|
3332
|
+
# Filter out strategy parameter kwargs to prevent them from reaching parent __init__
|
|
3333
|
+
filtered_kwargs = kwargs.copy()
|
|
3334
|
+
if hasattr(self.__class__, "_params") and self.__class__._params is not None:
|
|
3335
|
+
params_cls = self.__class__._params
|
|
3336
|
+
param_names = set()
|
|
3337
|
+
|
|
3338
|
+
# Get all parameter names from the class
|
|
3339
|
+
if hasattr(params_cls, "_getpairs"):
|
|
3340
|
+
param_names.update(params_cls._getpairs().keys())
|
|
3341
|
+
elif hasattr(params_cls, "_gettuple"):
|
|
3342
|
+
param_names.update(key for key, value in params_cls._gettuple())
|
|
3343
|
+
|
|
3344
|
+
# Remove strategy parameter kwargs
|
|
3345
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k not in param_names}
|
|
3346
|
+
|
|
3347
|
+
# Call parent initialization with filtered kwargs
|
|
3348
|
+
# Don't pass *args to avoid object.__init__() error, consistent with Strategy.__init__ fix
|
|
3349
|
+
if filtered_kwargs:
|
|
3350
|
+
super().__init__(**filtered_kwargs)
|
|
3351
|
+
else:
|
|
3352
|
+
super().__init__()
|
|
3353
|
+
|
|
3354
|
+
# Handle the functionality that was in MetaSigStrategy.dopostinit
|
|
3355
|
+
# Add signals from params
|
|
3356
|
+
# CRITICAL FIX: Pass self._dtarget as data source for signal indicators
|
|
3357
|
+
# and register them with the strategy so they get processed
|
|
3358
|
+
for sigtype, sigcls, sigargs, sigkwargs in self.p.signals:
|
|
3359
|
+
sig_indicator = sigcls(self._dtarget, *sigargs, **sigkwargs)
|
|
3360
|
+
self._signals[sigtype].append(sig_indicator)
|
|
3361
|
+
# CRITICAL FIX: Register signal indicator with strategy's _lineiterators
|
|
3362
|
+
# so its once()/next() methods get called during processing
|
|
3363
|
+
if hasattr(sig_indicator, "_ltype"):
|
|
3364
|
+
ltype = sig_indicator._ltype
|
|
3365
|
+
if sig_indicator not in self._lineiterators[ltype]:
|
|
3366
|
+
self._lineiterators[ltype].append(sig_indicator)
|
|
3367
|
+
sig_indicator._owner = self
|
|
3368
|
+
|
|
3369
|
+
# Record types of signals
|
|
3370
|
+
self._longshort = bool(self._signals[SIGNAL_LONGSHORT])
|
|
3371
|
+
|
|
3372
|
+
self._long = bool(self._signals[SIGNAL_LONG])
|
|
3373
|
+
self._short = bool(self._signals[SIGNAL_SHORT])
|
|
3374
|
+
|
|
3375
|
+
self._longexit = bool(self._signals[SIGNAL_LONGEXIT])
|
|
3376
|
+
self._shortexit = bool(self._signals[SIGNAL_SHORTEXIT])
|
|
3377
|
+
|
|
3378
|
+
def _start(self):
|
|
3379
|
+
"""Start the signal strategy and initialize the order sentinel."""
|
|
3380
|
+
self._sentinel = None # sentinel for order concurrency
|
|
3381
|
+
super()._start()
|
|
3382
|
+
|
|
3383
|
+
def signal_add(self, sigtype, signal):
|
|
3384
|
+
"""Add a signal indicator to the strategy.
|
|
3385
|
+
|
|
3386
|
+
Args:
|
|
3387
|
+
sigtype: Type of signal (e.g., SIGNAL_LONG, SIGNAL_SHORT)
|
|
3388
|
+
signal: The signal indicator instance
|
|
3389
|
+
"""
|
|
3390
|
+
self._signals[sigtype].append(signal)
|
|
3391
|
+
|
|
3392
|
+
def _notify(self, qorders=None, qtrades=None):
|
|
3393
|
+
"""Process notifications and reset sentinel when order completes.
|
|
3394
|
+
|
|
3395
|
+
Args:
|
|
3396
|
+
qorders: Quick notify orders
|
|
3397
|
+
qtrades: Quick notify trades
|
|
3398
|
+
"""
|
|
3399
|
+
if qorders is None:
|
|
3400
|
+
qorders = []
|
|
3401
|
+
if qtrades is None:
|
|
3402
|
+
qtrades = []
|
|
3403
|
+
# Nullify the sentinel if done
|
|
3404
|
+
procorders = qorders or self._orderspending
|
|
3405
|
+
if self._sentinel is not None:
|
|
3406
|
+
for order in procorders:
|
|
3407
|
+
if order == self._sentinel and not order.alive():
|
|
3408
|
+
self._sentinel = None
|
|
3409
|
+
break
|
|
3410
|
+
|
|
3411
|
+
super()._notify(qorders=qorders, qtrades=qtrades)
|
|
3412
|
+
|
|
3413
|
+
def _next_catch(self):
|
|
3414
|
+
"""Catch method that routes to signal processing and custom next."""
|
|
3415
|
+
self._next_signal()
|
|
3416
|
+
if hasattr(self, "_next_custom"):
|
|
3417
|
+
self._next_custom()
|
|
3418
|
+
|
|
3419
|
+
@staticmethod
|
|
3420
|
+
def _all_pos(sig, nosig):
|
|
3421
|
+
"""True if every value in ``sig`` (or ``nosig`` when empty) is > 0."""
|
|
3422
|
+
return all(x[0] > 0.0 for x in sig or nosig)
|
|
3423
|
+
|
|
3424
|
+
@staticmethod
|
|
3425
|
+
def _all_neg(sig, nosig):
|
|
3426
|
+
"""True if every value in ``sig`` (or ``nosig`` when empty) is < 0."""
|
|
3427
|
+
return all(x[0] < 0.0 for x in sig or nosig)
|
|
3428
|
+
|
|
3429
|
+
@staticmethod
|
|
3430
|
+
def _all_any(sig, nosig):
|
|
3431
|
+
"""True if every value in ``sig`` (or ``nosig`` when empty) is truthy."""
|
|
3432
|
+
return all(x[0] for x in sig or nosig)
|
|
3433
|
+
|
|
3434
|
+
def _evaluate_signals(self):
|
|
3435
|
+
"""Evaluate all signal collections into entry/exit/reversal flags.
|
|
3436
|
+
|
|
3437
|
+
Pure helper (no order side effects) extracted from _next_signal for
|
|
3438
|
+
readability. Returns a tuple of booleans consumed by the position
|
|
3439
|
+
decision logic:
|
|
3440
|
+
(ls_long, ls_short, l_enter, s_enter, l_exit, s_exit,
|
|
3441
|
+
l_rev, s_rev, l_leave, s_leave)
|
|
3442
|
+
"""
|
|
3443
|
+
# Get signal collections
|
|
3444
|
+
sigs = self._signals
|
|
3445
|
+
# Default no-signal value
|
|
3446
|
+
nosig = [[0.0]]
|
|
3447
|
+
pos = self._all_pos
|
|
3448
|
+
neg = self._all_neg
|
|
3449
|
+
anyv = self._all_any
|
|
3450
|
+
|
|
3451
|
+
# Calculate current status of the signals
|
|
3452
|
+
# If SIGNAL_LONGSHORT is empty, loop through nosig
|
|
3453
|
+
ls_long = pos(sigs[SIGNAL_LONGSHORT], nosig)
|
|
3454
|
+
ls_short = neg(sigs[SIGNAL_LONGSHORT], nosig)
|
|
3455
|
+
# Long entry: direct (>0), inverted (<0) or any (truthy)
|
|
3456
|
+
l_enter = (
|
|
3457
|
+
pos(sigs[SIGNAL_LONG], nosig)
|
|
3458
|
+
or neg(sigs[SIGNAL_LONG_INV], nosig)
|
|
3459
|
+
or anyv(sigs[SIGNAL_LONG_ANY], nosig)
|
|
3460
|
+
)
|
|
3461
|
+
# Short entry: direct (<0), inverted (>0) or any (truthy)
|
|
3462
|
+
s_enter = (
|
|
3463
|
+
neg(sigs[SIGNAL_SHORT], nosig)
|
|
3464
|
+
or pos(sigs[SIGNAL_SHORT_INV], nosig)
|
|
3465
|
+
or anyv(sigs[SIGNAL_SHORT_ANY], nosig)
|
|
3466
|
+
)
|
|
3467
|
+
# Long exit: direct (<0), inverted (>0) or any (truthy)
|
|
3468
|
+
l_exit = (
|
|
3469
|
+
neg(sigs[SIGNAL_LONGEXIT], nosig)
|
|
3470
|
+
or pos(sigs[SIGNAL_LONGEXIT_INV], nosig)
|
|
3471
|
+
or anyv(sigs[SIGNAL_LONGEXIT_ANY], nosig)
|
|
3472
|
+
)
|
|
3473
|
+
# Short exit: direct (>0), inverted (<0) or any (truthy)
|
|
3474
|
+
s_exit = (
|
|
3475
|
+
pos(sigs[SIGNAL_SHORTEXIT], nosig)
|
|
3476
|
+
or neg(sigs[SIGNAL_SHORTEXIT_INV], nosig)
|
|
3477
|
+
or anyv(sigs[SIGNAL_SHORTEXIT_ANY], nosig)
|
|
3478
|
+
)
|
|
3479
|
+
|
|
3480
|
+
# Use opposite signals to start reversal (by closing)
|
|
3481
|
+
# but only if no "xxxExit" exists
|
|
3482
|
+
# Long reversal: no long exit and short entry signal
|
|
3483
|
+
l_rev = not self._longexit and s_enter
|
|
3484
|
+
# Short reversal: no short exit and long entry signal
|
|
3485
|
+
s_rev = not self._shortexit and l_enter
|
|
3486
|
+
|
|
3487
|
+
# Opposite of individual long and short (leave = exit on opposite signal)
|
|
3488
|
+
# Long leave: direct (<0), inverted (>0) or any (truthy)
|
|
3489
|
+
l_leave = (
|
|
3490
|
+
neg(sigs[SIGNAL_LONG], nosig)
|
|
3491
|
+
or pos(sigs[SIGNAL_LONG_INV], nosig)
|
|
3492
|
+
or anyv(sigs[SIGNAL_LONG_ANY], nosig)
|
|
3493
|
+
)
|
|
3494
|
+
# Short leave: direct (>0), inverted (<0) or any (truthy)
|
|
3495
|
+
s_leave = (
|
|
3496
|
+
pos(sigs[SIGNAL_SHORT], nosig)
|
|
3497
|
+
or neg(sigs[SIGNAL_SHORT_INV], nosig)
|
|
3498
|
+
or anyv(sigs[SIGNAL_SHORT_ANY], nosig)
|
|
3499
|
+
)
|
|
3500
|
+
|
|
3501
|
+
# Invalidate long leave if longexit signals are available
|
|
3502
|
+
# If longexit exists, disable l_leave; otherwise keep l_leave
|
|
3503
|
+
l_leave = not self._longexit and l_leave
|
|
3504
|
+
# Invalidate short leave if shortexit signals are available
|
|
3505
|
+
# If shortexit exists, disable s_leave; otherwise keep s_leave
|
|
3506
|
+
s_leave = not self._shortexit and s_leave
|
|
3507
|
+
|
|
3508
|
+
return (
|
|
3509
|
+
ls_long,
|
|
3510
|
+
ls_short,
|
|
3511
|
+
l_enter,
|
|
3512
|
+
s_enter,
|
|
3513
|
+
l_exit,
|
|
3514
|
+
s_exit,
|
|
3515
|
+
l_rev,
|
|
3516
|
+
s_rev,
|
|
3517
|
+
l_leave,
|
|
3518
|
+
s_leave,
|
|
3519
|
+
)
|
|
3520
|
+
|
|
3521
|
+
def _next_signal(self):
|
|
3522
|
+
"""Process signals and generate orders based on signal values.
|
|
3523
|
+
|
|
3524
|
+
Evaluates all signal types and generates buy/sell orders based on:
|
|
3525
|
+
- Current position status
|
|
3526
|
+
- Signal values (positive/negative)
|
|
3527
|
+
- Accumulation and concurrency settings
|
|
3528
|
+
"""
|
|
3529
|
+
# If concurrent orders are disabled and an order is active, return
|
|
3530
|
+
if self._sentinel is not None and not self.p._concurrent:
|
|
3531
|
+
return # order active and more than 1 not allowed
|
|
3532
|
+
|
|
3533
|
+
# Evaluate all signal collections into decision flags
|
|
3534
|
+
(
|
|
3535
|
+
ls_long,
|
|
3536
|
+
ls_short,
|
|
3537
|
+
l_enter,
|
|
3538
|
+
s_enter,
|
|
3539
|
+
l_exit,
|
|
3540
|
+
s_exit,
|
|
3541
|
+
l_rev,
|
|
3542
|
+
s_rev,
|
|
3543
|
+
l_leave,
|
|
3544
|
+
s_leave,
|
|
3545
|
+
) = self._evaluate_signals()
|
|
3546
|
+
|
|
3547
|
+
# Take size and start logic
|
|
3548
|
+
# Get current position size
|
|
3549
|
+
size = self.getposition(self._dtarget).size
|
|
3550
|
+
# If no position
|
|
3551
|
+
if not size:
|
|
3552
|
+
# Enter new position based on signals
|
|
3553
|
+
if ls_long or l_enter:
|
|
3554
|
+
self._sentinel = self.buy(self._dtarget)
|
|
3555
|
+
|
|
3556
|
+
elif ls_short or s_enter:
|
|
3557
|
+
self._sentinel = self.sell(self._dtarget)
|
|
3558
|
+
|
|
3559
|
+
# If current position is long (positive)
|
|
3560
|
+
elif size > 0: # current long position
|
|
3561
|
+
if ls_short or l_exit or l_rev or l_leave:
|
|
3562
|
+
# closing position - not relevant for concurrency
|
|
3563
|
+
self.close(self._dtarget)
|
|
3564
|
+
|
|
3565
|
+
if ls_short or l_rev:
|
|
3566
|
+
self._sentinel = self.sell(self._dtarget)
|
|
3567
|
+
|
|
3568
|
+
if ls_long or l_enter:
|
|
3569
|
+
if self.p._accumulate:
|
|
3570
|
+
self._sentinel = self.buy(self._dtarget)
|
|
3571
|
+
# If current position is short (negative)
|
|
3572
|
+
elif size < 0: # current short position
|
|
3573
|
+
if ls_long or s_exit or s_rev or s_leave:
|
|
3574
|
+
# closing position - not relevant for concurrency
|
|
3575
|
+
self.close(self._dtarget)
|
|
3576
|
+
|
|
3577
|
+
if ls_long or s_rev:
|
|
3578
|
+
self._sentinel = self.buy(self._dtarget)
|
|
3579
|
+
|
|
3580
|
+
if ls_short or s_enter:
|
|
3581
|
+
if self.p._accumulate:
|
|
3582
|
+
self._sentinel = self.sell(self._dtarget)
|
|
3583
|
+
|
|
3584
|
+
|
|
3585
|
+
class BtApiStrategy(Strategy):
|
|
3586
|
+
"""A Strategy subclass with built-in logging capabilities.
|
|
3587
|
+
|
|
3588
|
+
This strategy class extends the base Strategy class with automatic
|
|
3589
|
+
logger initialization using the SpdLogManager. It provides a default
|
|
3590
|
+
log() method for logging messages and custom notification handling.
|
|
3591
|
+
|
|
3592
|
+
Attributes:
|
|
3593
|
+
logger: The configured logger instance from SpdLogManager.
|
|
3594
|
+
|
|
3595
|
+
Params:
|
|
3596
|
+
log_file_name: Optional custom log file name. If not provided,
|
|
3597
|
+
defaults to "{ClassName}.log".
|
|
3598
|
+
|
|
3599
|
+
Example:
|
|
3600
|
+
class MyStrategy(bt.BtApiStrategy):
|
|
3601
|
+
params = (('log_file_name', 'my_strategy.log'),)
|
|
3602
|
+
|
|
3603
|
+
def next(self):
|
|
3604
|
+
self.log(f'Close price: {self.data.close[0]:.2f}')
|
|
3605
|
+
"""
|
|
3606
|
+
|
|
3607
|
+
def __init__(self):
|
|
3608
|
+
"""Initialize the strategy with a logger instance."""
|
|
3609
|
+
self.logger = self.init_logger(self.p.get("log_file_name", None))
|
|
3610
|
+
|
|
3611
|
+
def init_logger(self, log_file_name=None):
|
|
3612
|
+
"""Initialize and return a logger instance.
|
|
3613
|
+
|
|
3614
|
+
Creates a logger using SpdLogManager with the specified or default
|
|
3615
|
+
log file name.
|
|
3616
|
+
|
|
3617
|
+
Args:
|
|
3618
|
+
log_file_name: Optional custom log file name. If None, uses
|
|
3619
|
+
"{ClassName}.log" as the default.
|
|
3620
|
+
|
|
3621
|
+
Returns:
|
|
3622
|
+
A configured logger instance.
|
|
3623
|
+
"""
|
|
3624
|
+
if log_file_name is None:
|
|
3625
|
+
logger = SpdLogManager(
|
|
3626
|
+
file_name=self.__class__.__name__ + ".log", logger_name="strategy", print_info=True
|
|
3627
|
+
).create_logger()
|
|
3628
|
+
else:
|
|
3629
|
+
logger = SpdLogManager(
|
|
3630
|
+
file_name=log_file_name, logger_name="strategy", print_info=True
|
|
3631
|
+
).create_logger()
|
|
3632
|
+
return logger
|
|
3633
|
+
|
|
3634
|
+
def log(self, txt):
|
|
3635
|
+
"""Log a message at INFO level.
|
|
3636
|
+
|
|
3637
|
+
Args:
|
|
3638
|
+
txt: The message text to log.
|
|
3639
|
+
"""
|
|
3640
|
+
self.logger.info(txt)
|
|
3641
|
+
|
|
3642
|
+
def _addnotification(self, data, quicknotify=True):
|
|
3643
|
+
"""Process notifications for orders and trades with logging.
|
|
3644
|
+
|
|
3645
|
+
This method extends the base notification handling to route
|
|
3646
|
+
notifications to the appropriate handler methods.
|
|
3647
|
+
|
|
3648
|
+
Args:
|
|
3649
|
+
data: The notification data, which can be an order or trade.
|
|
3650
|
+
quicknotify: If True, immediately process notification without queueing.
|
|
3651
|
+
"""
|
|
3652
|
+
if data.data_type == "order":
|
|
3653
|
+
self.notify_order(data)
|
|
3654
|
+
if data.data_type == "trade":
|
|
3655
|
+
self.notify_trade(data)
|