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/feed.py
ADDED
|
@@ -0,0 +1,1523 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Data Feed Module - Financial data feed implementations.
|
|
3
|
+
|
|
4
|
+
This module provides the base classes and implementations for data feeds
|
|
5
|
+
in backtrader. Data feeds are the source of price/volume data for strategies
|
|
6
|
+
and indicators.
|
|
7
|
+
|
|
8
|
+
Key Classes:
|
|
9
|
+
AbstractDataBase: Base class for all data feeds with core functionality.
|
|
10
|
+
DataBase: Full-featured data feed with replay/resample support.
|
|
11
|
+
CSVDataBase: Base class for CSV file data feeds.
|
|
12
|
+
FeedBase: Base for live/real-time data feeds.
|
|
13
|
+
|
|
14
|
+
Data feeds provide:
|
|
15
|
+
- OHLCV (Open, High, Low, Close, Volume) data
|
|
16
|
+
- Timeline management and session handling
|
|
17
|
+
- Replay and resampling capabilities
|
|
18
|
+
- Live data support for trading
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
Creating a custom data feed:
|
|
22
|
+
>>> class MyDataFeed(CSVDataBase):
|
|
23
|
+
... params = (('dataname', 'data.csv'),)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import collections
|
|
27
|
+
import datetime
|
|
28
|
+
import inspect
|
|
29
|
+
import os.path
|
|
30
|
+
|
|
31
|
+
from . import dataseries, metabase
|
|
32
|
+
from .dataseries import SimpleFilterWrapper, TimeFrame
|
|
33
|
+
from .resamplerfilter import Replayer, Resampler
|
|
34
|
+
from .tradingcal import PandasMarketCalendar
|
|
35
|
+
from .utils import date2num, num2date, time2num, tzparse
|
|
36
|
+
from .utils.date import Localizer
|
|
37
|
+
from .utils.log_message import get_logger
|
|
38
|
+
from .utils.py3 import range, string_types, zip
|
|
39
|
+
|
|
40
|
+
logger = get_logger(__name__)
|
|
41
|
+
|
|
42
|
+
_INF = float("inf")
|
|
43
|
+
_NEG_INF = float("-inf")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Refactor: Remove metaclass, use normal class and initialization method
|
|
47
|
+
class AbstractDataBase(dataseries.OHLCDateTime):
|
|
48
|
+
"""Base class for all data feed implementations.
|
|
49
|
+
|
|
50
|
+
Provides the core functionality for data feeds including:
|
|
51
|
+
- Data loading and preprocessing
|
|
52
|
+
- Timeline management
|
|
53
|
+
- Session handling
|
|
54
|
+
- Live data support
|
|
55
|
+
- Notification system for data status changes
|
|
56
|
+
|
|
57
|
+
States:
|
|
58
|
+
CONNECTED, DISCONNECTED, CONNBROKEN, DELAYED, LIVE,
|
|
59
|
+
NOTSUBSCRIBED, NOTSUPPORTED_TF, UNKNOWN
|
|
60
|
+
|
|
61
|
+
Params:
|
|
62
|
+
dataname: Data source identifier (filename, URL, etc.).
|
|
63
|
+
name: Display name for the data feed.
|
|
64
|
+
compression: Timeframe compression factor.
|
|
65
|
+
timeframe: TimeFrame period (Days, Minutes, etc.).
|
|
66
|
+
fromdate: Start date for data filtering.
|
|
67
|
+
todate: End date for data filtering.
|
|
68
|
+
sessionstart: Session start time.
|
|
69
|
+
sessionend: Session end time.
|
|
70
|
+
filters: List of data filters to apply.
|
|
71
|
+
tz: Output timezone.
|
|
72
|
+
tzinput: Input timezone.
|
|
73
|
+
qcheck: Timeout in seconds for live event checking.
|
|
74
|
+
calendar: Trading calendar to use.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
>>> data = AbstractDataBase(dataname='data.csv')
|
|
78
|
+
>>> cerebro.adddata(data)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
# Class-level registry dictionary, replacing metaclass _indcol functionality
|
|
82
|
+
_registry: dict = {}
|
|
83
|
+
|
|
84
|
+
# Parameter initialization settings - use _params_tuple to save original definition
|
|
85
|
+
_params_tuple: tuple = (
|
|
86
|
+
("dataname", None),
|
|
87
|
+
("name", ""),
|
|
88
|
+
("compression", 1),
|
|
89
|
+
("timeframe", TimeFrame.Days),
|
|
90
|
+
("fromdate", None),
|
|
91
|
+
("todate", None),
|
|
92
|
+
("sessionstart", None),
|
|
93
|
+
("sessionend", None),
|
|
94
|
+
("filters", []),
|
|
95
|
+
("tz", None),
|
|
96
|
+
("tzinput", None),
|
|
97
|
+
("qcheck", 0.0), # timeout in seconds (float) to check for events
|
|
98
|
+
("calendar", None),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Keep original params definition for compatibility with metaclass system
|
|
102
|
+
params = _params_tuple
|
|
103
|
+
|
|
104
|
+
# Eight different states of data
|
|
105
|
+
(
|
|
106
|
+
CONNECTED,
|
|
107
|
+
DISCONNECTED,
|
|
108
|
+
CONNBROKEN,
|
|
109
|
+
DELAYED,
|
|
110
|
+
LIVE,
|
|
111
|
+
NOTSUBSCRIBED,
|
|
112
|
+
NOTSUPPORTED_TF,
|
|
113
|
+
UNKNOWN,
|
|
114
|
+
) = range(8)
|
|
115
|
+
|
|
116
|
+
# Notification names
|
|
117
|
+
_NOTIFNAMES = [
|
|
118
|
+
"CONNECTED",
|
|
119
|
+
"DISCONNECTED",
|
|
120
|
+
"CONNBROKEN",
|
|
121
|
+
"DELAYED",
|
|
122
|
+
"LIVE",
|
|
123
|
+
"NOTSUBSCRIBED",
|
|
124
|
+
"NOTSUPPORTED_TIMEFRAME",
|
|
125
|
+
"UNKNOWN",
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def __init__(self, *args, **kwargs):
|
|
129
|
+
"""Initialize the data feed.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
*args: Positional arguments.
|
|
133
|
+
**kwargs: Keyword arguments for data feed parameters.
|
|
134
|
+
"""
|
|
135
|
+
# Execute the original metaclass dopreinit functionality
|
|
136
|
+
self._init_preinit(*args, **kwargs)
|
|
137
|
+
|
|
138
|
+
# Call parent class initialization
|
|
139
|
+
super().__init__(*args, **kwargs)
|
|
140
|
+
|
|
141
|
+
# Execute the original metaclass dopostinit functionality
|
|
142
|
+
self._init_postinit(*args, **kwargs)
|
|
143
|
+
|
|
144
|
+
# CRITICAL FIX: Mark all lines as belonging to a data feed
|
|
145
|
+
# This must be done AFTER _init_postinit to ensure lines are fully initialized
|
|
146
|
+
# This allows LineSeries.__getitem__ and LineBuffer.__getitem__ to correctly
|
|
147
|
+
# raise IndexError when accessing out-of-range indices
|
|
148
|
+
# This is essential for expire_order_close() to detect insufficient data
|
|
149
|
+
if hasattr(self, "lines") and self.lines is not None:
|
|
150
|
+
try:
|
|
151
|
+
for line in self.lines:
|
|
152
|
+
if hasattr(line, "__dict__"):
|
|
153
|
+
line._is_data_feed_line = True
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.warning("Failed to mark data feed lines: %s", e)
|
|
156
|
+
|
|
157
|
+
# CRITICAL FIX: Also explicitly mark the datetime line
|
|
158
|
+
# The datetime line might be accessed separately (e.g., self.datas[0].datetime)
|
|
159
|
+
# and needs to raise IndexError when accessing out of bounds
|
|
160
|
+
if hasattr(self, "datetime") and self.datetime is not None:
|
|
161
|
+
try:
|
|
162
|
+
if hasattr(self.datetime, "__dict__"):
|
|
163
|
+
self.datetime._is_data_feed_line = True
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning("Failed to mark datetime line: %s", e)
|
|
166
|
+
|
|
167
|
+
# Original content from __init__
|
|
168
|
+
self._env = None
|
|
169
|
+
self._barstash: collections.deque = collections.deque()
|
|
170
|
+
self._barstack: collections.deque = collections.deque()
|
|
171
|
+
self._laststatus = None
|
|
172
|
+
|
|
173
|
+
def _init_preinit(self, *args, **kwargs):
|
|
174
|
+
"""Replace the original MetaAbstractDataBase.dopreinit"""
|
|
175
|
+
# Find the owner and store it
|
|
176
|
+
self._feed = self._find_feed_owner()
|
|
177
|
+
# Initialize a queue to store notifications from cerebro
|
|
178
|
+
self.notifs: collections.deque = collections.deque() # store notifications for cerebro
|
|
179
|
+
# Get _dataname value from parameters
|
|
180
|
+
self._dataname = getattr(self.p, "dataname", None)
|
|
181
|
+
# Default _name attribute is empty
|
|
182
|
+
self._name = ""
|
|
183
|
+
|
|
184
|
+
def _init_postinit(self, *args, **kwargs):
|
|
185
|
+
"""Replace the original MetaAbstractDataBase.dopostinit"""
|
|
186
|
+
# Either set by subclass or the parameter or use the dataname (ticker)
|
|
187
|
+
# Reset _name attribute, if _name is not empty, keep it; if empty, set it to name parameter value
|
|
188
|
+
self._name = self._name or getattr(self.p, "name", "")
|
|
189
|
+
# If _name attribute value is still empty and dataname parameter value is string, set _name to dataname value
|
|
190
|
+
if not self._name and isinstance(getattr(self.p, "dataname", None), string_types):
|
|
191
|
+
self._name = self.p.dataname
|
|
192
|
+
# _compression value equals the compression parameter value
|
|
193
|
+
self._compression = getattr(self.p, "compression", 1)
|
|
194
|
+
# _timeframe value equals the timeframe parameter value
|
|
195
|
+
self._timeframe = getattr(self.p, "timeframe", TimeFrame.Days)
|
|
196
|
+
|
|
197
|
+
# Only set sessionstart/sessionend defaults if they weren't explicitly passed
|
|
198
|
+
# If start time is datetime format, equals specific time from sessionstart; if None, equals minimum time
|
|
199
|
+
sessionstart = getattr(self.p, "sessionstart", None)
|
|
200
|
+
if isinstance(sessionstart, datetime.datetime):
|
|
201
|
+
self.p.sessionstart = sessionstart.time()
|
|
202
|
+
elif sessionstart is None:
|
|
203
|
+
# CRITICAL FIX: Always set default if None (kwargs check was unreliable)
|
|
204
|
+
self.p.sessionstart = datetime.time.min
|
|
205
|
+
|
|
206
|
+
# If end time is datetime format, equals specific time from sessionend; if None, equals 23:59:59.999990
|
|
207
|
+
sessionend = getattr(self.p, "sessionend", None)
|
|
208
|
+
if isinstance(sessionend, datetime.datetime):
|
|
209
|
+
self.p.sessionend = sessionend.time()
|
|
210
|
+
elif sessionend is None:
|
|
211
|
+
# CRITICAL FIX: Always set default if None (kwargs check was unreliable)
|
|
212
|
+
# remove 9 to avoid precision rounding errors
|
|
213
|
+
self.p.sessionend = datetime.time(23, 59, 59, 999990)
|
|
214
|
+
|
|
215
|
+
# If start date is date format and has no hour attribute, add sessionstart time to convert start date to date+time format
|
|
216
|
+
fromdate = getattr(self.p, "fromdate", None)
|
|
217
|
+
if isinstance(fromdate, datetime.date):
|
|
218
|
+
# push it to the end of the day, or else intraday
|
|
219
|
+
# values before the end of the day would be gone
|
|
220
|
+
if not hasattr(fromdate, "hour"):
|
|
221
|
+
self.p.fromdate = datetime.datetime.combine(fromdate, self.p.sessionstart)
|
|
222
|
+
|
|
223
|
+
# If end date is date format and has no hour attribute, add sessionend time to convert start date to date+time format
|
|
224
|
+
todate = getattr(self.p, "todate", None)
|
|
225
|
+
if isinstance(todate, datetime.date):
|
|
226
|
+
# push it to the end of the day, or else intraday
|
|
227
|
+
# values before the end of the day would be gone
|
|
228
|
+
if not hasattr(todate, "hour"):
|
|
229
|
+
self.p.todate = datetime.datetime.combine(todate, self.p.sessionend)
|
|
230
|
+
|
|
231
|
+
# Set _barstack and _barstash as queues for filter operations
|
|
232
|
+
self._barstack = collections.deque() # for filter operations
|
|
233
|
+
self._barstash = collections.deque() # for filter operations
|
|
234
|
+
# Set _filters and _ffilters as empty lists
|
|
235
|
+
self._filters: list = []
|
|
236
|
+
self._ffilters: list = []
|
|
237
|
+
|
|
238
|
+
# Iterate through filters in parameters, first check if it's a class; if class, instantiate first; if instance has last attribute, add filter to _ffilters
|
|
239
|
+
# If not a class, directly add filter to _filters
|
|
240
|
+
filters = getattr(self.p, "filters", [])
|
|
241
|
+
for fp in filters:
|
|
242
|
+
if inspect.isclass(fp):
|
|
243
|
+
fp = fp(self)
|
|
244
|
+
if hasattr(fp, "last"):
|
|
245
|
+
self._ffilters.append((fp, [], {}))
|
|
246
|
+
|
|
247
|
+
self._filters.append((fp, [], {}))
|
|
248
|
+
|
|
249
|
+
def _find_feed_owner(self):
|
|
250
|
+
"""Find the feed owner using metabase.findowner.
|
|
251
|
+
|
|
252
|
+
This method delegates to metabase.findowner which uses
|
|
253
|
+
OwnerContext for explicit owner management.
|
|
254
|
+
"""
|
|
255
|
+
# Use findowner which checks OwnerContext for owner lookup
|
|
256
|
+
return metabase.findowner(self, FeedBase)
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def _getstatusname(cls, status):
|
|
260
|
+
return cls._NOTIFNAMES[status]
|
|
261
|
+
|
|
262
|
+
# Initialize the following variables, may be used in live trading
|
|
263
|
+
_compensate = None
|
|
264
|
+
_feed = None
|
|
265
|
+
_store = None
|
|
266
|
+
|
|
267
|
+
_clone = False
|
|
268
|
+
_qcheck = 0.0
|
|
269
|
+
|
|
270
|
+
# Time offset
|
|
271
|
+
_tmoffset = datetime.timedelta()
|
|
272
|
+
|
|
273
|
+
# Set to non 0 if resampling/replaying
|
|
274
|
+
# Whether resampling or replaying, if not, set to 0
|
|
275
|
+
resampling = 0
|
|
276
|
+
replaying = 0
|
|
277
|
+
|
|
278
|
+
# Whether started
|
|
279
|
+
_started = False
|
|
280
|
+
|
|
281
|
+
def _start_finish(self):
|
|
282
|
+
# A live feed (for example) may have learnt something about the
|
|
283
|
+
# timezones after the start, and that's why the date/time related
|
|
284
|
+
# parameters are converted at this late stage
|
|
285
|
+
# Get the output timezone (if any)
|
|
286
|
+
# Get specific timezone
|
|
287
|
+
self._tz = self._gettz()
|
|
288
|
+
# Lines have already been created, set the tz
|
|
289
|
+
# Set specific timezone for time
|
|
290
|
+
self.lines.datetime._settz(self._tz)
|
|
291
|
+
|
|
292
|
+
# This should probably be also called from an override-able method
|
|
293
|
+
# Localize input timezone
|
|
294
|
+
self._tzinput = Localizer(self._gettzinput())
|
|
295
|
+
|
|
296
|
+
# Convert user input times to the output timezone (or min/max)
|
|
297
|
+
# Convert user input start and end times to specific numbers; if None, start time is negative infinity, end time is positive infinity
|
|
298
|
+
# If specific time, use date2num to convert to specific number
|
|
299
|
+
if self.p.fromdate is None:
|
|
300
|
+
self.fromdate = float("-inf")
|
|
301
|
+
else:
|
|
302
|
+
self.fromdate = self.date2num(self.p.fromdate)
|
|
303
|
+
|
|
304
|
+
if self.p.todate is None:
|
|
305
|
+
self.todate = float("inf")
|
|
306
|
+
else:
|
|
307
|
+
self.todate = self.date2num(self.p.todate)
|
|
308
|
+
|
|
309
|
+
# Used by resamplerfilter and DataClone
|
|
310
|
+
self.sessionstart = time2num(self.p.sessionstart)
|
|
311
|
+
self.sessionend = time2num(self.p.sessionend)
|
|
312
|
+
|
|
313
|
+
# Get calendar from parameters; if calendar is None, look for _tradingcal in local environment; if string, use PandasMarketCalendar
|
|
314
|
+
self._calendar = cal = self.p.calendar
|
|
315
|
+
if cal is None:
|
|
316
|
+
self._calendar = self._env._tradingcal if self._env else None
|
|
317
|
+
elif isinstance(cal, string_types):
|
|
318
|
+
self._calendar = PandasMarketCalendar(calendar=cal)
|
|
319
|
+
# Start state
|
|
320
|
+
self._started = True
|
|
321
|
+
|
|
322
|
+
def _start(self):
|
|
323
|
+
self.start()
|
|
324
|
+
# If not in start state yet, initialize first, then enter start state
|
|
325
|
+
if not self._started:
|
|
326
|
+
self._start_finish()
|
|
327
|
+
|
|
328
|
+
def _timeoffset(self):
|
|
329
|
+
# Time offset
|
|
330
|
+
return self._tmoffset
|
|
331
|
+
|
|
332
|
+
# Return next trading day end time in datetime format and numeric format
|
|
333
|
+
def _getnexteos(self):
|
|
334
|
+
"""Returns the next eos using a trading calendar if available"""
|
|
335
|
+
if self._clone:
|
|
336
|
+
return self.data._getnexteos()
|
|
337
|
+
|
|
338
|
+
if not len(self):
|
|
339
|
+
return datetime.datetime.min, 0.0
|
|
340
|
+
|
|
341
|
+
dt = self.lines.datetime[0]
|
|
342
|
+
dtime = num2date(dt)
|
|
343
|
+
if self._calendar is None:
|
|
344
|
+
nexteos = datetime.datetime.combine(dtime, self.p.sessionend)
|
|
345
|
+
nextdteos = self.date2num(nexteos) # locl'ed -> utc-like
|
|
346
|
+
nexteos = num2date(nextdteos) # utc
|
|
347
|
+
while dtime > nexteos:
|
|
348
|
+
nexteos += datetime.timedelta(days=1) # already utc-like
|
|
349
|
+
|
|
350
|
+
nextdteos = date2num(nexteos) # -> utc-like
|
|
351
|
+
|
|
352
|
+
else:
|
|
353
|
+
# returns times in utc
|
|
354
|
+
_, nexteos = self._calendar.schedule(dtime, self._tz)
|
|
355
|
+
nextdteos = date2num(nexteos) # nextos is already utc
|
|
356
|
+
|
|
357
|
+
return nexteos, nextdteos
|
|
358
|
+
|
|
359
|
+
# Parse tzinput and return
|
|
360
|
+
def _gettzinput(self):
|
|
361
|
+
"""Can be overriden by classes to return a timezone for input"""
|
|
362
|
+
return tzparse(self.p.tzinput)
|
|
363
|
+
|
|
364
|
+
# Parse tz and return
|
|
365
|
+
def _gettz(self):
|
|
366
|
+
"""To be overriden by subclasses which may auto-calculate the
|
|
367
|
+
timezone"""
|
|
368
|
+
return tzparse(self.p.tz)
|
|
369
|
+
|
|
370
|
+
# Convert time to number; if timezone info is not None, localize time first, then convert
|
|
371
|
+
def date2num(self, dt):
|
|
372
|
+
"""Convert datetime to internal numeric format.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
dt: datetime object to convert.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
float: Internal numeric representation of the datetime.
|
|
379
|
+
"""
|
|
380
|
+
if self._tz is not None:
|
|
381
|
+
return date2num(self._tz.localize(dt))
|
|
382
|
+
|
|
383
|
+
return date2num(dt)
|
|
384
|
+
|
|
385
|
+
# Convert number to date+time
|
|
386
|
+
def num2date(self, dt=None, tz=None, naive=True):
|
|
387
|
+
"""Convert internal numeric format to datetime.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
dt: Numeric datetime value (uses current if None).
|
|
391
|
+
tz: Timezone to use (uses feed tz if None).
|
|
392
|
+
naive: Return naive datetime if True.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
datetime: Converted datetime object.
|
|
396
|
+
"""
|
|
397
|
+
if dt is None:
|
|
398
|
+
return num2date(self.lines.datetime[0], tz or self._tz, naive)
|
|
399
|
+
|
|
400
|
+
return num2date(dt, tz or self._tz, naive)
|
|
401
|
+
|
|
402
|
+
# Whether has live data; default is False; if has live data, needs override
|
|
403
|
+
def haslivedata(self):
|
|
404
|
+
"""Check if this data feed has live data.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
bool: False for base class, override for live data feeds.
|
|
408
|
+
"""
|
|
409
|
+
return False # must be overriden for those that can
|
|
410
|
+
|
|
411
|
+
# Wait interval when resampling live data
|
|
412
|
+
def do_qcheck(self, onoff, qlapse):
|
|
413
|
+
"""Calculate wait interval for queue checking.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
onoff: Whether queue checking is enabled.
|
|
417
|
+
qlapse: Time elapsed since last check.
|
|
418
|
+
"""
|
|
419
|
+
# if onoff is True, the data will wait p.qcheck for incoming live data
|
|
420
|
+
# on its queue.
|
|
421
|
+
if not onoff:
|
|
422
|
+
self._qcheck = 0.0
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
self._qcheck = max(0.0, self.p.qcheck - qlapse)
|
|
426
|
+
|
|
427
|
+
# Whether is live data; default is False; if True, cerebro will not use preload and runonce, because live data needs
|
|
428
|
+
# to be fetched tick by tick or bar by bar
|
|
429
|
+
def islive(self):
|
|
430
|
+
"""If this returns True, ``Cerebro`` will deactivate ``preload`` and
|
|
431
|
+
``runonce`` because a live data source must be fetched tick by tick (or
|
|
432
|
+
bar by bar)"""
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
# If latest status differs from current status, need to add info to notifs to update latest status
|
|
436
|
+
def put_notification(self, status, *args, **kwargs):
|
|
437
|
+
"""Add arguments to notification queue"""
|
|
438
|
+
if self._laststatus != status:
|
|
439
|
+
self.notifs.append((status, args, kwargs))
|
|
440
|
+
self._laststatus = status
|
|
441
|
+
|
|
442
|
+
# Get notification info, save to notifs and return as result
|
|
443
|
+
def get_notifications(self):
|
|
444
|
+
"""Return the pending "store" notifications"""
|
|
445
|
+
# The background thread could keep on adding notifications. The None
|
|
446
|
+
# mark allows to identify which is the last notification to deliver
|
|
447
|
+
# Add a None, when None is retrieved, it means the queue is empty and all info has been retrieved
|
|
448
|
+
self.notifs.append(None) # put a mark
|
|
449
|
+
notifs = []
|
|
450
|
+
while True:
|
|
451
|
+
notif = self.notifs.popleft()
|
|
452
|
+
if notif is None: # mark is reached
|
|
453
|
+
break
|
|
454
|
+
notifs.append(notif)
|
|
455
|
+
|
|
456
|
+
return notifs
|
|
457
|
+
|
|
458
|
+
# Get feed
|
|
459
|
+
def getfeed(self):
|
|
460
|
+
"""Get the parent feed object.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
FeedBase or None: The parent feed instance.
|
|
464
|
+
"""
|
|
465
|
+
return self._feed
|
|
466
|
+
|
|
467
|
+
# Amount of cached data
|
|
468
|
+
def qbuffer(self, savemem=0, replaying=False):
|
|
469
|
+
"""Apply queued buffering to all lines.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
savemem: Memory saving mode.
|
|
473
|
+
replaying: Whether replaying is active.
|
|
474
|
+
"""
|
|
475
|
+
extrasize = self.resampling or replaying
|
|
476
|
+
for line in self.lines:
|
|
477
|
+
line.qbuffer(savemem=savemem, extrasize=extrasize)
|
|
478
|
+
|
|
479
|
+
# Start, reset _barstack and _barstash
|
|
480
|
+
def start(self):
|
|
481
|
+
"""Start the data feed.
|
|
482
|
+
|
|
483
|
+
Resets internal queues and sets initial status to CONNECTED.
|
|
484
|
+
"""
|
|
485
|
+
self._barstack = collections.deque()
|
|
486
|
+
self._barstash = collections.deque()
|
|
487
|
+
self._laststatus = self.CONNECTED
|
|
488
|
+
|
|
489
|
+
# End
|
|
490
|
+
def stop(self):
|
|
491
|
+
"""Stop the data feed.
|
|
492
|
+
|
|
493
|
+
Override in subclasses for cleanup.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
# Clone data
|
|
497
|
+
def clone(self, **kwargs):
|
|
498
|
+
"""Create a clone of this data feed.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
**kwargs: Additional keyword arguments for the clone.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
DataClone: A cloned data feed.
|
|
505
|
+
"""
|
|
506
|
+
return DataClone(dataname=self, **kwargs)
|
|
507
|
+
|
|
508
|
+
# Copy data and give it a different name
|
|
509
|
+
def copyas(self, _dataname, **kwargs):
|
|
510
|
+
"""Copy the data feed with a different name.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
_dataname: New name for the data feed.
|
|
514
|
+
**kwargs: Additional keyword arguments.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
DataClone: A cloned data feed with the new name.
|
|
518
|
+
"""
|
|
519
|
+
d = DataClone(dataname=self, **kwargs)
|
|
520
|
+
d._dataname = _dataname
|
|
521
|
+
d._name = _dataname
|
|
522
|
+
return d
|
|
523
|
+
|
|
524
|
+
# Set environment
|
|
525
|
+
def setenvironment(self, env):
|
|
526
|
+
"""Keep a reference to the environment"""
|
|
527
|
+
self._env = env
|
|
528
|
+
|
|
529
|
+
# Get environment
|
|
530
|
+
def getenvironment(self):
|
|
531
|
+
"""Get the cerebro environment reference.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
The cerebro environment instance.
|
|
535
|
+
"""
|
|
536
|
+
return self._env
|
|
537
|
+
|
|
538
|
+
# Add simple filter
|
|
539
|
+
def addfilter_simple(self, f, *args, **kwargs):
|
|
540
|
+
"""Add a simple filter wrapper to this data feed.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
f: Filter function to apply.
|
|
544
|
+
*args: Positional arguments for the filter.
|
|
545
|
+
**kwargs: Keyword arguments for the filter.
|
|
546
|
+
"""
|
|
547
|
+
fp = SimpleFilterWrapper(self, f, *args, **kwargs)
|
|
548
|
+
self._filters.append((fp, fp.args, fp.kwargs))
|
|
549
|
+
|
|
550
|
+
# Add filter
|
|
551
|
+
def addfilter(self, p, *args, **kwargs):
|
|
552
|
+
"""Add a filter to this data feed.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
p: Filter class or instance.
|
|
556
|
+
*args: Positional arguments for filter creation.
|
|
557
|
+
**kwargs: Keyword arguments for filter creation.
|
|
558
|
+
"""
|
|
559
|
+
if inspect.isclass(p):
|
|
560
|
+
pobj = p(self, *args, **kwargs)
|
|
561
|
+
self._filters.append((pobj, [], {}))
|
|
562
|
+
|
|
563
|
+
if hasattr(pobj, "last"):
|
|
564
|
+
self._ffilters.append((pobj, [], {}))
|
|
565
|
+
|
|
566
|
+
else:
|
|
567
|
+
self._filters.append((p, args, kwargs))
|
|
568
|
+
|
|
569
|
+
# Compensate
|
|
570
|
+
def compensate(self, other):
|
|
571
|
+
"""Call it to let the broker know that actions on this asset will
|
|
572
|
+
compensate open positions in another"""
|
|
573
|
+
|
|
574
|
+
self._compensate = other
|
|
575
|
+
|
|
576
|
+
# Set tick_+name attribute to None for non-datetime names, mainly used when synthesizing low-frequency data from high-frequency data
|
|
577
|
+
def _tick_nullify(self):
|
|
578
|
+
# These are the updating prices in case the new bar is "updated"
|
|
579
|
+
# and the length doesn't change like if a replay is happening or
|
|
580
|
+
# a real-time data feed is in use and 1-minute bars are being
|
|
581
|
+
# constructed with 5-second updates
|
|
582
|
+
# PERFORMANCE OPTIMIZATION: Cache tick attribute names to avoid repeated string concat
|
|
583
|
+
tick_cache = getattr(self, "_tick_cache", None)
|
|
584
|
+
if tick_cache is None:
|
|
585
|
+
tick_cache = ["tick_" + alias for alias in self.getlinealiases() if alias != "datetime"]
|
|
586
|
+
self._tick_cache = tick_cache
|
|
587
|
+
|
|
588
|
+
set_attr = object.__setattr__
|
|
589
|
+
for tick_name in tick_cache:
|
|
590
|
+
set_attr(self, tick_name, None)
|
|
591
|
+
|
|
592
|
+
set_attr(self, "tick_last", None)
|
|
593
|
+
set_attr(self, "_tick_direct_filled", False)
|
|
594
|
+
|
|
595
|
+
# If tick_xxx related attribute value is None, need to consider using bar data to fill
|
|
596
|
+
def _tick_fill(self, force=False):
|
|
597
|
+
# If nothing filled the tick_xxx attributes, the bar is the tick
|
|
598
|
+
# PERFORMANCE OPTIMIZATION: Cache tick name/line pairs for faster access
|
|
599
|
+
if not force:
|
|
600
|
+
try:
|
|
601
|
+
if object.__getattribute__(self, "_tick_direct_filled"):
|
|
602
|
+
return
|
|
603
|
+
except AttributeError:
|
|
604
|
+
logger.debug("feed:604 ignored AttributeError")
|
|
605
|
+
|
|
606
|
+
try:
|
|
607
|
+
tick_line_cache = self._tick_line_cache
|
|
608
|
+
except AttributeError:
|
|
609
|
+
tick_line_cache = None
|
|
610
|
+
if tick_line_cache is None:
|
|
611
|
+
tick_line_cache = []
|
|
612
|
+
alias0 = self._getlinealias(0)
|
|
613
|
+
self._tick_alias0 = "tick_" + alias0
|
|
614
|
+
self._tick_last_line = getattr(self.lines, alias0)
|
|
615
|
+
for lalias in self.getlinealiases():
|
|
616
|
+
if lalias != "datetime":
|
|
617
|
+
tick_line_cache.append(
|
|
618
|
+
("tick_" + lalias, getattr(self.lines, lalias), lalias == alias0)
|
|
619
|
+
)
|
|
620
|
+
self._tick_line_cache = tick_line_cache
|
|
621
|
+
|
|
622
|
+
if force:
|
|
623
|
+
should_fill = True
|
|
624
|
+
else:
|
|
625
|
+
try:
|
|
626
|
+
should_fill = object.__getattribute__(self, self._tick_alias0) is None
|
|
627
|
+
except AttributeError:
|
|
628
|
+
should_fill = True
|
|
629
|
+
|
|
630
|
+
if should_fill:
|
|
631
|
+
set_attr = object.__setattr__
|
|
632
|
+
tick_last = None
|
|
633
|
+
for tick_name, line, is_last in self._tick_line_cache:
|
|
634
|
+
current_idx = line._idx
|
|
635
|
+
line_array = line.array
|
|
636
|
+
try:
|
|
637
|
+
value = line_array[current_idx]
|
|
638
|
+
except IndexError:
|
|
639
|
+
lencount = line.lencount
|
|
640
|
+
if lencount > 0 and current_idx >= lencount:
|
|
641
|
+
value = line_array[lencount - 1]
|
|
642
|
+
else:
|
|
643
|
+
raise
|
|
644
|
+
if value in (_INF, _NEG_INF):
|
|
645
|
+
value = 0.0
|
|
646
|
+
set_attr(self, tick_name, value)
|
|
647
|
+
if is_last:
|
|
648
|
+
tick_last = value
|
|
649
|
+
|
|
650
|
+
if tick_last is None:
|
|
651
|
+
line = self._tick_last_line
|
|
652
|
+
current_idx = line._idx
|
|
653
|
+
line_array = line.array
|
|
654
|
+
try:
|
|
655
|
+
tick_last = line_array[current_idx]
|
|
656
|
+
except IndexError:
|
|
657
|
+
lencount = line.lencount
|
|
658
|
+
if lencount > 0 and current_idx >= lencount:
|
|
659
|
+
tick_last = line_array[lencount - 1]
|
|
660
|
+
else:
|
|
661
|
+
raise
|
|
662
|
+
if tick_last in (_INF, _NEG_INF):
|
|
663
|
+
tick_last = 0.0
|
|
664
|
+
|
|
665
|
+
set_attr(self, "tick_last", tick_last)
|
|
666
|
+
|
|
667
|
+
# Get time of next bar
|
|
668
|
+
# PERFORMANCE OPTIMIZATION: Cache float("inf") as module-level constant
|
|
669
|
+
_INF = float("inf")
|
|
670
|
+
|
|
671
|
+
def advance_peek(self):
|
|
672
|
+
"""Peek at the datetime of the next bar.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
float: Numeric datetime of next bar, or inf if unavailable.
|
|
676
|
+
"""
|
|
677
|
+
# PERFORMANCE OPTIMIZATION: Use cached _INF, avoid repeated float("inf") creation
|
|
678
|
+
_inf = self._INF
|
|
679
|
+
try:
|
|
680
|
+
if len(self) < self.buflen():
|
|
681
|
+
# CRITICAL FIX: Check if datetime[1] is valid before returning
|
|
682
|
+
try:
|
|
683
|
+
next_dt = self.lines.datetime[1]
|
|
684
|
+
# If next_dt is 0 or invalid, return inf
|
|
685
|
+
if next_dt is None or next_dt <= 0:
|
|
686
|
+
return _inf
|
|
687
|
+
return next_dt
|
|
688
|
+
except (IndexError, KeyError):
|
|
689
|
+
# If accessing datetime[1] fails, we're at the end
|
|
690
|
+
return _inf
|
|
691
|
+
return _inf # max date else
|
|
692
|
+
except Exception as e:
|
|
693
|
+
logger.debug("Exception in _gettz for %s: %s", getattr(self, "_name", ""), e)
|
|
694
|
+
return _inf
|
|
695
|
+
|
|
696
|
+
# Move data forward by size
|
|
697
|
+
def advance(self, size=1, datamaster=None, ticks=True):
|
|
698
|
+
"""Advance the data feed by the specified size.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
size: Number of bars to advance (default: 1).
|
|
702
|
+
datamaster: Master data feed for synchronization.
|
|
703
|
+
ticks: Whether to process tick data.
|
|
704
|
+
"""
|
|
705
|
+
if ticks:
|
|
706
|
+
self._tick_nullify()
|
|
707
|
+
|
|
708
|
+
# Need intercepting this call to support datas with
|
|
709
|
+
# different lengths (timeframes)
|
|
710
|
+
self.lines.advance(size)
|
|
711
|
+
|
|
712
|
+
if datamaster is not None:
|
|
713
|
+
if len(self) > self.buflen():
|
|
714
|
+
# if no bar can be delivered, fill with an empty bar
|
|
715
|
+
self.rewind()
|
|
716
|
+
self.lines.forward()
|
|
717
|
+
return
|
|
718
|
+
|
|
719
|
+
if self.lines.datetime[0] > datamaster.lines.datetime[0]:
|
|
720
|
+
self.lines.rewind()
|
|
721
|
+
else:
|
|
722
|
+
if ticks:
|
|
723
|
+
self._tick_fill()
|
|
724
|
+
elif len(self) < self.buflen():
|
|
725
|
+
# a resampler may have advance us past the last point
|
|
726
|
+
if ticks:
|
|
727
|
+
self._tick_fill()
|
|
728
|
+
|
|
729
|
+
# What happens on data when next is called
|
|
730
|
+
def next(self, datamaster=None, ticks=True):
|
|
731
|
+
"""Move to the next bar.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
datamaster: Master data feed for synchronization.
|
|
735
|
+
ticks: Whether to process tick data.
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
bool: True if a bar is available, False otherwise.
|
|
739
|
+
"""
|
|
740
|
+
# If data length is greater than cached data length, if it's ticks data, call _tick_nullify to generate tick_xxx attributes, then call load to try getting next bar; if ret is empty
|
|
741
|
+
# return ret. If master data is None, if it's ticks data, need to call _tick_fill.
|
|
742
|
+
# If own length is less than cached data length, move forward
|
|
743
|
+
try:
|
|
744
|
+
line_datetime = self._datetime_line
|
|
745
|
+
needs_load = line_datetime.lencount >= (
|
|
746
|
+
len(line_datetime.array) - line_datetime.extension
|
|
747
|
+
)
|
|
748
|
+
except AttributeError:
|
|
749
|
+
needs_load = len(self) >= self.buflen()
|
|
750
|
+
|
|
751
|
+
if needs_load:
|
|
752
|
+
if ticks:
|
|
753
|
+
self._tick_nullify()
|
|
754
|
+
|
|
755
|
+
# not preloaded - request next bar
|
|
756
|
+
ret = self.load()
|
|
757
|
+
if not ret:
|
|
758
|
+
# if the load cannot produce bars - forward the result
|
|
759
|
+
return ret
|
|
760
|
+
|
|
761
|
+
if datamaster is None:
|
|
762
|
+
# bar is there and no master ... return load's result
|
|
763
|
+
if ticks:
|
|
764
|
+
try:
|
|
765
|
+
tick_direct_filled = self._tick_direct_filled
|
|
766
|
+
except AttributeError:
|
|
767
|
+
tick_direct_filled = False
|
|
768
|
+
if not tick_direct_filled:
|
|
769
|
+
self._tick_fill()
|
|
770
|
+
return ret
|
|
771
|
+
else:
|
|
772
|
+
self.advance(ticks=ticks)
|
|
773
|
+
# If master data is not None, if current data time is greater than master data time, need to adjust backward;
|
|
774
|
+
# If current data time is not greater than master data time and data is ticks data, need to fill current data
|
|
775
|
+
# If master data is None and data is ticks data, need to fill current day data
|
|
776
|
+
# a bar is "loaded" or was preloaded - index has been moved to it
|
|
777
|
+
if datamaster is not None:
|
|
778
|
+
# there is a time reference to check against
|
|
779
|
+
if self.lines.datetime[0] > datamaster.lines.datetime[0]:
|
|
780
|
+
# can't deliver new bar, too early, go back
|
|
781
|
+
self.rewind()
|
|
782
|
+
return False
|
|
783
|
+
if ticks:
|
|
784
|
+
try:
|
|
785
|
+
tick_direct_filled = self._tick_direct_filled
|
|
786
|
+
except AttributeError:
|
|
787
|
+
tick_direct_filled = False
|
|
788
|
+
if not tick_direct_filled:
|
|
789
|
+
self._tick_fill()
|
|
790
|
+
|
|
791
|
+
else:
|
|
792
|
+
if ticks:
|
|
793
|
+
try:
|
|
794
|
+
tick_direct_filled = self._tick_direct_filled
|
|
795
|
+
except AttributeError:
|
|
796
|
+
tick_direct_filled = False
|
|
797
|
+
if not tick_direct_filled:
|
|
798
|
+
self._tick_fill()
|
|
799
|
+
|
|
800
|
+
# tell the world there is a bar (either the new or the previous
|
|
801
|
+
# Indicate current bar exists
|
|
802
|
+
return True
|
|
803
|
+
|
|
804
|
+
# Preload data
|
|
805
|
+
def preload(self):
|
|
806
|
+
"""Preload all available data from the data feed.
|
|
807
|
+
|
|
808
|
+
Loads all bars and resets position to the beginning.
|
|
809
|
+
"""
|
|
810
|
+
# Load data
|
|
811
|
+
while self.load():
|
|
812
|
+
pass
|
|
813
|
+
|
|
814
|
+
self._last()
|
|
815
|
+
self.home()
|
|
816
|
+
|
|
817
|
+
# Last chance to use filters
|
|
818
|
+
def _last(self, datamaster=None):
|
|
819
|
+
# A last chance for filters to deliver something
|
|
820
|
+
|
|
821
|
+
ret = 0
|
|
822
|
+
for ff, fargs, fkwargs in self._ffilters:
|
|
823
|
+
ret += ff.last(self, *fargs, **fkwargs)
|
|
824
|
+
|
|
825
|
+
doticks = False
|
|
826
|
+
if datamaster is not None and self._barstack:
|
|
827
|
+
doticks = True
|
|
828
|
+
|
|
829
|
+
while self._fromstack(forward=True):
|
|
830
|
+
# consume bar(s) produced by "last"s - adding room
|
|
831
|
+
pass
|
|
832
|
+
|
|
833
|
+
if doticks:
|
|
834
|
+
self._tick_fill()
|
|
835
|
+
|
|
836
|
+
return bool(ret)
|
|
837
|
+
|
|
838
|
+
# Check if verification is needed
|
|
839
|
+
def _check(self, forcedata=None):
|
|
840
|
+
for ff, fargs, fkwargs in self._filters:
|
|
841
|
+
if not hasattr(ff, "check"):
|
|
842
|
+
continue
|
|
843
|
+
ff.check(self, _forcedata=forcedata, *fargs, **fkwargs)
|
|
844
|
+
|
|
845
|
+
# Load data
|
|
846
|
+
def load(self):
|
|
847
|
+
"""Load the next bar from the data feed.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
bool: True if a bar was loaded, False if no more data.
|
|
851
|
+
|
|
852
|
+
This method handles:
|
|
853
|
+
- Forwarding the data pointer
|
|
854
|
+
- Processing filters
|
|
855
|
+
- Checking date boundaries
|
|
856
|
+
"""
|
|
857
|
+
while True:
|
|
858
|
+
# move a data pointer forward for new bar
|
|
859
|
+
# Move data pointer forward by one
|
|
860
|
+
try:
|
|
861
|
+
forward_lines = self._load_forward_lines
|
|
862
|
+
except AttributeError:
|
|
863
|
+
try:
|
|
864
|
+
lines = self.lines.lines
|
|
865
|
+
if any(line.mode == line.QBuffer or line._clock is not None for line in lines):
|
|
866
|
+
self._load_forward_lines = None
|
|
867
|
+
forward_lines = None
|
|
868
|
+
else:
|
|
869
|
+
forward_lines = tuple(lines)
|
|
870
|
+
self._load_forward_lines = forward_lines
|
|
871
|
+
except AttributeError:
|
|
872
|
+
self._load_forward_lines = None
|
|
873
|
+
forward_lines = None
|
|
874
|
+
|
|
875
|
+
if forward_lines is None:
|
|
876
|
+
self.forward()
|
|
877
|
+
else:
|
|
878
|
+
for line in forward_lines:
|
|
879
|
+
line._idx += 1
|
|
880
|
+
line.lencount += 1
|
|
881
|
+
line.array.append(line._default_value)
|
|
882
|
+
|
|
883
|
+
# If data has been retrieved from self._barstack and saved to line, directly return True
|
|
884
|
+
if self._barstack and self._fromstack(): # bar is available
|
|
885
|
+
return True
|
|
886
|
+
# If data cannot be retrieved from self._barstash, run the following code
|
|
887
|
+
if not self._barstash:
|
|
888
|
+
# _load() returns False, following code must run, but seems unnecessary to call this function or check following result, these two statements seem redundant
|
|
889
|
+
### Cannot be 100% certain for now, will review after code comments are completed #fix
|
|
890
|
+
_loadret = self._load()
|
|
891
|
+
if not _loadret: # no bar use force to make sure in exactbars
|
|
892
|
+
# the pointer is undone this covers especially (but not
|
|
893
|
+
# uniquely) the case in which the last bar has been seen
|
|
894
|
+
# and a backwards would ruin pointer accounting in the
|
|
895
|
+
# "stop" method of the strategy
|
|
896
|
+
self.backwards(force=True) # undo data pointer
|
|
897
|
+
|
|
898
|
+
# Return the actual returned value which may be None to
|
|
899
|
+
# signal no bar is available, but the data feed is not
|
|
900
|
+
# done. False means game over
|
|
901
|
+
return _loadret
|
|
902
|
+
else:
|
|
903
|
+
self._fromstack(stash=True)
|
|
904
|
+
|
|
905
|
+
# If bar was not retrieved from self._barstack but bar was retrieved from self._barstash, need to process bar
|
|
906
|
+
# Get a reference to current loaded time
|
|
907
|
+
# Get current time
|
|
908
|
+
try:
|
|
909
|
+
line_datetime = self._datetime_line
|
|
910
|
+
except AttributeError:
|
|
911
|
+
line_datetime = self.lines.datetime
|
|
912
|
+
try:
|
|
913
|
+
datetime_idx = line_datetime._idx
|
|
914
|
+
if datetime_idx >= 0:
|
|
915
|
+
dt = line_datetime.array[datetime_idx]
|
|
916
|
+
else:
|
|
917
|
+
dt = line_datetime[0]
|
|
918
|
+
except (AttributeError, IndexError):
|
|
919
|
+
dt = line_datetime[0]
|
|
920
|
+
|
|
921
|
+
# A bar has been loaded, adapt the time
|
|
922
|
+
# If timezone processing is needed for input time, convert number to time, localize time, convert time to number, update current time
|
|
923
|
+
if self._tzinput:
|
|
924
|
+
# Input has been converted at face value, but it's not UTC in
|
|
925
|
+
# the input stream
|
|
926
|
+
dtime = num2date(dt) # get it in a naive datetime
|
|
927
|
+
# localize it
|
|
928
|
+
dtime = self._tzinput.localize(dtime) # pytz compatible-ized
|
|
929
|
+
line_datetime[0] = dt = date2num(dtime) # keep UTC val
|
|
930
|
+
|
|
931
|
+
# Check standard date from/to filters
|
|
932
|
+
# If current time is less than start time, move backward to discard bar and continue
|
|
933
|
+
if dt < self.fromdate:
|
|
934
|
+
# discard loaded bar and carry on
|
|
935
|
+
self.backwards()
|
|
936
|
+
continue
|
|
937
|
+
# If time is greater than end time, move backward and undo data pointer, then break
|
|
938
|
+
if dt > self.todate:
|
|
939
|
+
# discard loaded bar and break out
|
|
940
|
+
self.backwards(force=True)
|
|
941
|
+
break
|
|
942
|
+
|
|
943
|
+
# Pass through filters
|
|
944
|
+
# Iterate through each filter
|
|
945
|
+
retff = False
|
|
946
|
+
for ff, fargs, fkwargs in self._filters:
|
|
947
|
+
# previous filter may have put things onto the stack
|
|
948
|
+
# If self._barstack is not empty
|
|
949
|
+
if self._barstack:
|
|
950
|
+
# Perform self._barstack number of _fromstack function calls, call filter ff
|
|
951
|
+
for i in range(len(self._barstack)):
|
|
952
|
+
self._fromstack(forward=True)
|
|
953
|
+
retff = ff(self, *fargs, **fkwargs)
|
|
954
|
+
# If self._barstack is empty, call filter once
|
|
955
|
+
else:
|
|
956
|
+
retff = ff(self, *fargs, **fkwargs)
|
|
957
|
+
# If retff is True, break out of filter loop
|
|
958
|
+
if retff: # bar removed from systemn
|
|
959
|
+
break # out of the inner loop
|
|
960
|
+
# If True, continue
|
|
961
|
+
if retff: # bar removed from system - loop to get new bar
|
|
962
|
+
continue # in the greater loop
|
|
963
|
+
|
|
964
|
+
# Checks let the bar through ... notify it
|
|
965
|
+
return True
|
|
966
|
+
# End loop, return False, no more bars or reached end date
|
|
967
|
+
# Out of the loop ... no more bars or past todate
|
|
968
|
+
return False
|
|
969
|
+
|
|
970
|
+
# Function that returns False
|
|
971
|
+
def _load(self):
|
|
972
|
+
return False
|
|
973
|
+
|
|
974
|
+
# Add bar data to self._barstack or self._barstash
|
|
975
|
+
def _add2stack(self, bar, stash=False):
|
|
976
|
+
"""Saves given bar (list of values) to the stack for later retrieval"""
|
|
977
|
+
if not stash:
|
|
978
|
+
self._barstack.append(bar)
|
|
979
|
+
else:
|
|
980
|
+
self._barstash.append(bar)
|
|
981
|
+
|
|
982
|
+
# Get bar data and save to self._barstack or self._barstash, provides parameter to delete bar
|
|
983
|
+
def _save2stack(self, erase=False, force=False, stash=False):
|
|
984
|
+
"""Saves current bar to the bar stack for later retrieval
|
|
985
|
+
|
|
986
|
+
Parameter ``erase`` determines removal from the data stream
|
|
987
|
+
"""
|
|
988
|
+
|
|
989
|
+
bar = [line[0] for line in self.itersize()]
|
|
990
|
+
if not stash:
|
|
991
|
+
self._barstack.append(bar)
|
|
992
|
+
else:
|
|
993
|
+
self._barstash.append(bar)
|
|
994
|
+
|
|
995
|
+
if erase: # remove bar if requested
|
|
996
|
+
self.backwards(force=force)
|
|
997
|
+
|
|
998
|
+
# This comment has issues, this function is used to update bar data to specific lines
|
|
999
|
+
def _updatebar(self, bar, forward=False, ago=0):
|
|
1000
|
+
"""Load a value from the stack onto the lines to form the new bar
|
|
1001
|
+
|
|
1002
|
+
Returns True if values are present, False otherwise
|
|
1003
|
+
"""
|
|
1004
|
+
if forward:
|
|
1005
|
+
self.forward()
|
|
1006
|
+
|
|
1007
|
+
for line, val in zip(self.itersize(), bar):
|
|
1008
|
+
line[0 + ago] = val
|
|
1009
|
+
|
|
1010
|
+
# Get data from self._barstack or self._barstash, then save to line; if successful return True, if not return False
|
|
1011
|
+
def _fromstack(self, forward=False, stash=False):
|
|
1012
|
+
"""Load a value from the stack onto the lines to form the new bar
|
|
1013
|
+
|
|
1014
|
+
Returns True if values are present, False otherwise
|
|
1015
|
+
"""
|
|
1016
|
+
# When stash is False, coll equals self._barstack, otherwise it's self._barstash
|
|
1017
|
+
coll = self._barstack if not stash else self._barstash
|
|
1018
|
+
# If coll has data
|
|
1019
|
+
if coll:
|
|
1020
|
+
# If forward is True, call forward
|
|
1021
|
+
if forward:
|
|
1022
|
+
self.forward()
|
|
1023
|
+
# Add data to line
|
|
1024
|
+
for line, val in zip(self.itersize(), coll.popleft()):
|
|
1025
|
+
line[0] = val
|
|
1026
|
+
|
|
1027
|
+
self._tick_fill(force=True)
|
|
1028
|
+
return True
|
|
1029
|
+
|
|
1030
|
+
return False
|
|
1031
|
+
|
|
1032
|
+
# Add resample filter
|
|
1033
|
+
def resample(self, **kwargs):
|
|
1034
|
+
"""Add a resampling filter to this data feed.
|
|
1035
|
+
|
|
1036
|
+
Resampling converts data to a different timeframe (e.g., minutes to days).
|
|
1037
|
+
|
|
1038
|
+
Args:
|
|
1039
|
+
**kwargs: Arguments for the Resampler filter.
|
|
1040
|
+
"""
|
|
1041
|
+
self.addfilter(Resampler, **kwargs)
|
|
1042
|
+
|
|
1043
|
+
# Add replay filter
|
|
1044
|
+
def replay(self, **kwargs):
|
|
1045
|
+
"""Add a replay filter to this data feed.
|
|
1046
|
+
|
|
1047
|
+
Replay filters process tick data into bars with precise control.
|
|
1048
|
+
|
|
1049
|
+
Args:
|
|
1050
|
+
**kwargs: Arguments for the Replayer filter.
|
|
1051
|
+
"""
|
|
1052
|
+
self.addfilter(Replayer, **kwargs)
|
|
1053
|
+
|
|
1054
|
+
@classmethod
|
|
1055
|
+
def _gettuple(cls):
|
|
1056
|
+
"""For compatibility, provide _gettuple method"""
|
|
1057
|
+
return cls._params_tuple if hasattr(cls, "_params_tuple") else cls.params
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
# DataBase class, directly inherits from abstract DataBase
|
|
1061
|
+
class DataBase(AbstractDataBase):
|
|
1062
|
+
"""Full-featured data feed class.
|
|
1063
|
+
|
|
1064
|
+
Inherits all functionality from AbstractDataBase.
|
|
1065
|
+
This is the standard data feed class for most use cases.
|
|
1066
|
+
"""
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
# Refactor: Remove MetaParams metaclass, use normal parameter processing
|
|
1070
|
+
class FeedBase:
|
|
1071
|
+
"""Base class for feed containers.
|
|
1072
|
+
|
|
1073
|
+
Manages multiple data feeds and provides parameter processing
|
|
1074
|
+
without using metaclasses.
|
|
1075
|
+
"""
|
|
1076
|
+
|
|
1077
|
+
# Parameter processing, originally merged parameters automatically via metaclass, now manual processing
|
|
1078
|
+
def __init__(self, **kwargs):
|
|
1079
|
+
"""Initialize the feed base.
|
|
1080
|
+
|
|
1081
|
+
Args:
|
|
1082
|
+
**kwargs: Keyword arguments for parameters.
|
|
1083
|
+
"""
|
|
1084
|
+
# Manually set parameters, replacing original metaclass functionality
|
|
1085
|
+
self.p = self._create_params(**kwargs)
|
|
1086
|
+
self.datas = []
|
|
1087
|
+
|
|
1088
|
+
def _create_params(self, **kwargs):
|
|
1089
|
+
"""Manually create parameter object, replacing metaclass parameter processing"""
|
|
1090
|
+
|
|
1091
|
+
# Create a simple parameter object
|
|
1092
|
+
class Params:
|
|
1093
|
+
"""Parameter container for FeedBase.
|
|
1094
|
+
|
|
1095
|
+
Stores parameter values and provides access via _getitems.
|
|
1096
|
+
"""
|
|
1097
|
+
|
|
1098
|
+
def _getitems(self):
|
|
1099
|
+
"""Simulate original _getitems method.
|
|
1100
|
+
|
|
1101
|
+
Returns:
|
|
1102
|
+
list: List of (attribute_name, value) tuples for non-private attributes.
|
|
1103
|
+
"""
|
|
1104
|
+
# OPTIMIZED: Use __dict__ instead of dir() for better performance
|
|
1105
|
+
items = []
|
|
1106
|
+
for attr_name, value in self.__dict__.items():
|
|
1107
|
+
if not attr_name.startswith("_") and not callable(value):
|
|
1108
|
+
items.append((attr_name, value))
|
|
1109
|
+
return items
|
|
1110
|
+
|
|
1111
|
+
params_obj = Params()
|
|
1112
|
+
|
|
1113
|
+
# Get default parameters from DataBase
|
|
1114
|
+
if hasattr(DataBase, "params"):
|
|
1115
|
+
base_params = DataBase.params
|
|
1116
|
+
if isinstance(base_params, (tuple, list)):
|
|
1117
|
+
for param_tuple in base_params:
|
|
1118
|
+
if isinstance(param_tuple, (tuple, list)) and len(param_tuple) >= 2:
|
|
1119
|
+
param_name, param_default = param_tuple[0], param_tuple[1]
|
|
1120
|
+
setattr(params_obj, param_name, kwargs.get(param_name, param_default))
|
|
1121
|
+
|
|
1122
|
+
# Set other passed parameters
|
|
1123
|
+
for key, value in kwargs.items():
|
|
1124
|
+
if not hasattr(params_obj, key):
|
|
1125
|
+
setattr(params_obj, key, value)
|
|
1126
|
+
|
|
1127
|
+
return params_obj
|
|
1128
|
+
|
|
1129
|
+
# Data start
|
|
1130
|
+
def start(self):
|
|
1131
|
+
"""Start all managed data feeds."""
|
|
1132
|
+
for data in self.datas:
|
|
1133
|
+
data.start()
|
|
1134
|
+
|
|
1135
|
+
# Data end
|
|
1136
|
+
def stop(self):
|
|
1137
|
+
"""Stop all managed data feeds."""
|
|
1138
|
+
for data in self.datas:
|
|
1139
|
+
data.stop()
|
|
1140
|
+
|
|
1141
|
+
# Get data based on dataname and add data to self.datas
|
|
1142
|
+
def getdata(self, dataname, name=None, **kwargs):
|
|
1143
|
+
"""Get or create a data feed and add it to the managed datas.
|
|
1144
|
+
|
|
1145
|
+
Args:
|
|
1146
|
+
dataname: Data source identifier (filename, URL, etc.).
|
|
1147
|
+
name: Display name for the data feed.
|
|
1148
|
+
**kwargs: Additional parameters for the data feed.
|
|
1149
|
+
|
|
1150
|
+
Returns:
|
|
1151
|
+
DataBase: The created or retrieved data feed instance.
|
|
1152
|
+
"""
|
|
1153
|
+
# Merge parameters
|
|
1154
|
+
final_kwargs = {}
|
|
1155
|
+
if hasattr(self.p, "_getitems"):
|
|
1156
|
+
for pname, pvalue in self.p._getitems():
|
|
1157
|
+
final_kwargs[pname] = pvalue
|
|
1158
|
+
elif hasattr(self.p, "__dict__"):
|
|
1159
|
+
final_kwargs.update(self.p.__dict__)
|
|
1160
|
+
|
|
1161
|
+
final_kwargs.update(kwargs)
|
|
1162
|
+
final_kwargs["dataname"] = dataname
|
|
1163
|
+
|
|
1164
|
+
data = self._getdata(**final_kwargs)
|
|
1165
|
+
data._name = name
|
|
1166
|
+
self.datas.append(data)
|
|
1167
|
+
return data
|
|
1168
|
+
|
|
1169
|
+
def _getdata(self, dataname, **kwargs):
|
|
1170
|
+
# Set keyword arguments
|
|
1171
|
+
final_kwargs = {}
|
|
1172
|
+
if hasattr(self.p, "_getitems"):
|
|
1173
|
+
for pname, pvalue in self.p._getitems():
|
|
1174
|
+
final_kwargs[pname] = pvalue
|
|
1175
|
+
elif hasattr(self.p, "__dict__"):
|
|
1176
|
+
final_kwargs.update(self.p.__dict__)
|
|
1177
|
+
|
|
1178
|
+
final_kwargs.update(kwargs)
|
|
1179
|
+
final_kwargs["dataname"] = dataname
|
|
1180
|
+
return self.DataCls(**final_kwargs)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
# Refactor: Remove MetaCSVDataBase metaclass, use normal initialization method
|
|
1184
|
+
class CSVDataBase(DataBase):
|
|
1185
|
+
"""
|
|
1186
|
+
Base class for classes implementing CSV DataFeeds
|
|
1187
|
+
|
|
1188
|
+
The class takes care of opening the file, reading the lines and
|
|
1189
|
+
tokenizing them.
|
|
1190
|
+
|
|
1191
|
+
Subclasses do only need to override:
|
|
1192
|
+
|
|
1193
|
+
- _loadline(tokens)
|
|
1194
|
+
|
|
1195
|
+
The return value of ``_loadline`` (True/False) will be the return value
|
|
1196
|
+
of ``_load`` which has been overriden by this base class
|
|
1197
|
+
"""
|
|
1198
|
+
|
|
1199
|
+
# Data defaults to None
|
|
1200
|
+
f = None
|
|
1201
|
+
# Set specific parameters, merge parent class parameters - use _params_tuple to save original definition
|
|
1202
|
+
_params_tuple = (
|
|
1203
|
+
("headers", True),
|
|
1204
|
+
("separator", ","),
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
# Keep original params definition for compatibility with metaclass system
|
|
1208
|
+
params = _params_tuple
|
|
1209
|
+
|
|
1210
|
+
# Get data and simple processing
|
|
1211
|
+
def __init__(self, *args, **kwargs):
|
|
1212
|
+
"""Initialize the CSV data base.
|
|
1213
|
+
|
|
1214
|
+
Args:
|
|
1215
|
+
*args: Positional arguments.
|
|
1216
|
+
**kwargs: Keyword arguments for parameters.
|
|
1217
|
+
"""
|
|
1218
|
+
# Execute original metaclass MetaCSVDataBase.dopostinit functionality
|
|
1219
|
+
self._csv_postinit(**kwargs)
|
|
1220
|
+
|
|
1221
|
+
# Call parent class initialization
|
|
1222
|
+
super().__init__(*args, **kwargs)
|
|
1223
|
+
|
|
1224
|
+
self.separator = None
|
|
1225
|
+
|
|
1226
|
+
def _csv_postinit(self, **kwargs):
|
|
1227
|
+
"""Replace original MetaCSVDataBase.dopostinit"""
|
|
1228
|
+
# If parameter has no name and _name attribute is empty, get specific name from data file name
|
|
1229
|
+
# Use existing parameter system
|
|
1230
|
+
dataname = getattr(self, "p", None) and getattr(self.p, "dataname", None)
|
|
1231
|
+
if not dataname:
|
|
1232
|
+
dataname = kwargs.get("dataname", "")
|
|
1233
|
+
name = getattr(self, "p", None) and getattr(self.p, "name", None)
|
|
1234
|
+
if not name:
|
|
1235
|
+
name = kwargs.get("name", "")
|
|
1236
|
+
|
|
1237
|
+
if not name and not getattr(self, "_name", ""):
|
|
1238
|
+
if isinstance(dataname, string_types):
|
|
1239
|
+
self._name, _ = os.path.splitext(os.path.basename(dataname))
|
|
1240
|
+
|
|
1241
|
+
def start(self):
|
|
1242
|
+
"""Start the CSV data feed.
|
|
1243
|
+
|
|
1244
|
+
Opens the CSV file and optionally skips headers.
|
|
1245
|
+
"""
|
|
1246
|
+
super().start()
|
|
1247
|
+
# If data is None
|
|
1248
|
+
if self.f is None:
|
|
1249
|
+
# If dataname parameter has readline attribute, it means dataname is a data source, directly set f to data in parameter
|
|
1250
|
+
if hasattr(self.p.dataname, "readline"):
|
|
1251
|
+
self.f = self.p.dataname
|
|
1252
|
+
# If no readline attribute, it means dataname is a path, open file based on path to get data
|
|
1253
|
+
else:
|
|
1254
|
+
# Let an exception propagate to let the caller know
|
|
1255
|
+
self.f = open(self.p.dataname)
|
|
1256
|
+
# If there are headers, read a line and skip headers
|
|
1257
|
+
if self.p.headers:
|
|
1258
|
+
self.f.readline() # skip the headers
|
|
1259
|
+
# Separator for each line of data
|
|
1260
|
+
self.separator = self.p.separator
|
|
1261
|
+
|
|
1262
|
+
# Stop
|
|
1263
|
+
def stop(self):
|
|
1264
|
+
"""Stop the CSV data feed.
|
|
1265
|
+
|
|
1266
|
+
Closes the CSV file if open.
|
|
1267
|
+
"""
|
|
1268
|
+
super().stop()
|
|
1269
|
+
# If data file is not None, close file and set to None
|
|
1270
|
+
if self.f is not None:
|
|
1271
|
+
self.f.close()
|
|
1272
|
+
self.f = None
|
|
1273
|
+
|
|
1274
|
+
# Preload data
|
|
1275
|
+
def preload(self):
|
|
1276
|
+
"""Preload all data from the CSV file.
|
|
1277
|
+
|
|
1278
|
+
Loads all available data and closes the file handle.
|
|
1279
|
+
"""
|
|
1280
|
+
# Load data
|
|
1281
|
+
while self.load():
|
|
1282
|
+
pass
|
|
1283
|
+
# Settings after load is finished
|
|
1284
|
+
self._last()
|
|
1285
|
+
self.home()
|
|
1286
|
+
|
|
1287
|
+
# preloaded - no need to keep the object around - breaks multip in 3.x
|
|
1288
|
+
# Close data file and set to None
|
|
1289
|
+
if self.f is not None:
|
|
1290
|
+
self.f.close()
|
|
1291
|
+
self.f = None
|
|
1292
|
+
|
|
1293
|
+
# Load a line of data
|
|
1294
|
+
def _load(self):
|
|
1295
|
+
# If data file is None, return False; if line cannot be read, return False; process line, call _loadline to load
|
|
1296
|
+
if self.f is None:
|
|
1297
|
+
return False
|
|
1298
|
+
|
|
1299
|
+
# Let an exception propagate to let the caller know
|
|
1300
|
+
line = self.f.readline()
|
|
1301
|
+
|
|
1302
|
+
if not line:
|
|
1303
|
+
return False
|
|
1304
|
+
|
|
1305
|
+
line = line.rstrip("\n")
|
|
1306
|
+
linetokens = line.split(self.separator)
|
|
1307
|
+
return self._loadline(linetokens)
|
|
1308
|
+
|
|
1309
|
+
# Get next line of data
|
|
1310
|
+
def _getnextline(self):
|
|
1311
|
+
# This function is very similar to previous one, just previous one gets linetokens with additional _loadline call
|
|
1312
|
+
if self.f is None:
|
|
1313
|
+
return None
|
|
1314
|
+
|
|
1315
|
+
# Let an exception propagate to let the caller know
|
|
1316
|
+
line = self.f.readline()
|
|
1317
|
+
|
|
1318
|
+
if not line:
|
|
1319
|
+
return None
|
|
1320
|
+
|
|
1321
|
+
line = line.rstrip("\n")
|
|
1322
|
+
linetokens = line.split(self.separator)
|
|
1323
|
+
return linetokens
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
class CSVFeedBase(FeedBase):
|
|
1327
|
+
"""Base class for CSV feed containers.
|
|
1328
|
+
|
|
1329
|
+
Manages CSV data feeds with support for base path prefixing.
|
|
1330
|
+
"""
|
|
1331
|
+
|
|
1332
|
+
# Set parameters
|
|
1333
|
+
def __init__(self, basepath="", **kwargs):
|
|
1334
|
+
"""Initialize the CSV feed base.
|
|
1335
|
+
|
|
1336
|
+
Args:
|
|
1337
|
+
basepath: Base path to prepend to data file names.
|
|
1338
|
+
**kwargs: Additional keyword arguments for parameters.
|
|
1339
|
+
"""
|
|
1340
|
+
self.basepath = basepath
|
|
1341
|
+
# Merge CSVDataBase parameters
|
|
1342
|
+
csv_params = {}
|
|
1343
|
+
if hasattr(CSVDataBase, "params"):
|
|
1344
|
+
csv_base_params = CSVDataBase.params
|
|
1345
|
+
if isinstance(csv_base_params, (tuple, list)):
|
|
1346
|
+
for param_tuple in csv_base_params:
|
|
1347
|
+
if isinstance(param_tuple, (tuple, list)) and len(param_tuple) >= 2:
|
|
1348
|
+
param_name, param_default = param_tuple[0], param_tuple[1]
|
|
1349
|
+
csv_params[param_name] = kwargs.get(param_name, param_default)
|
|
1350
|
+
|
|
1351
|
+
kwargs.update(csv_params)
|
|
1352
|
+
super().__init__(**kwargs)
|
|
1353
|
+
|
|
1354
|
+
# Get data
|
|
1355
|
+
def _getdata(self, dataname, **kwargs):
|
|
1356
|
+
final_kwargs = {}
|
|
1357
|
+
if hasattr(self.p, "_getitems"):
|
|
1358
|
+
for pname, pvalue in self.p._getitems():
|
|
1359
|
+
final_kwargs[pname] = pvalue
|
|
1360
|
+
elif hasattr(self.p, "__dict__"):
|
|
1361
|
+
final_kwargs.update(self.p.__dict__)
|
|
1362
|
+
|
|
1363
|
+
final_kwargs.update(kwargs)
|
|
1364
|
+
return self.DataCls(dataname=self.basepath + dataname, **final_kwargs)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
# Data clone
|
|
1368
|
+
class DataClone(AbstractDataBase):
|
|
1369
|
+
"""Clones an existing data feed.
|
|
1370
|
+
|
|
1371
|
+
Creates a new data feed that references an existing data feed.
|
|
1372
|
+
Useful for creating multiple views of the same data with
|
|
1373
|
+
different parameters or filters.
|
|
1374
|
+
"""
|
|
1375
|
+
|
|
1376
|
+
# Set _clone attribute to True
|
|
1377
|
+
_clone = True
|
|
1378
|
+
|
|
1379
|
+
# Initialize, data equals dataname parameter value, _datename equals data's _dataname attribute value
|
|
1380
|
+
# Then copy date, time, trading interval, compression parameters
|
|
1381
|
+
def __init__(self, *args, **kwargs):
|
|
1382
|
+
"""Initialize the data clone.
|
|
1383
|
+
|
|
1384
|
+
Args:
|
|
1385
|
+
*args: Positional arguments.
|
|
1386
|
+
**kwargs: Keyword arguments, must include 'dataname' (the source data feed).
|
|
1387
|
+
|
|
1388
|
+
Raises:
|
|
1389
|
+
ValueError: If 'dataname' parameter is not provided.
|
|
1390
|
+
"""
|
|
1391
|
+
# CRITICAL FIX: Initialize these attributes BEFORE calling super().__init__
|
|
1392
|
+
# to ensure they exist when parent class methods access them
|
|
1393
|
+
self._dlen = 0
|
|
1394
|
+
self._preloading = None
|
|
1395
|
+
|
|
1396
|
+
# Get dataname and set it as self.data
|
|
1397
|
+
dataname = kwargs.get("dataname")
|
|
1398
|
+
if dataname is None and hasattr(self, "p"):
|
|
1399
|
+
dataname = getattr(self.p, "dataname", None)
|
|
1400
|
+
if dataname is None:
|
|
1401
|
+
raise ValueError("DataClone requires 'dataname' parameter")
|
|
1402
|
+
|
|
1403
|
+
# CRITICAL FIX: Store data reference using object.__setattr__ to bypass
|
|
1404
|
+
# any custom __setattr__ that might interfere
|
|
1405
|
+
object.__setattr__(self, "data", dataname)
|
|
1406
|
+
self._dataname = getattr(self.data, "_dataname", None)
|
|
1407
|
+
|
|
1408
|
+
# Copy date/session parameters from source data
|
|
1409
|
+
if hasattr(self.data, "p"):
|
|
1410
|
+
kwargs.setdefault("fromdate", getattr(self.data.p, "fromdate", None))
|
|
1411
|
+
kwargs.setdefault("todate", getattr(self.data.p, "todate", None))
|
|
1412
|
+
kwargs.setdefault("sessionstart", getattr(self.data.p, "sessionstart", None))
|
|
1413
|
+
kwargs.setdefault("sessionend", getattr(self.data.p, "sessionend", None))
|
|
1414
|
+
kwargs.setdefault("timeframe", getattr(self.data.p, "timeframe", None))
|
|
1415
|
+
kwargs.setdefault("compression", getattr(self.data.p, "compression", None))
|
|
1416
|
+
|
|
1417
|
+
super().__init__(*args, **kwargs)
|
|
1418
|
+
|
|
1419
|
+
# CRITICAL FIX: Ensure self.data is still set after parent init
|
|
1420
|
+
# Re-set it to be safe, in case parent class __init__ cleared attributes
|
|
1421
|
+
if not hasattr(self, "data") or object.__getattribute__(self, "data") is None:
|
|
1422
|
+
object.__setattr__(self, "data", dataname)
|
|
1423
|
+
|
|
1424
|
+
def _start(self):
|
|
1425
|
+
# redefine to copy data bits from guest data
|
|
1426
|
+
self.start()
|
|
1427
|
+
|
|
1428
|
+
# Copy tz infos
|
|
1429
|
+
if hasattr(self.data, "_tz"):
|
|
1430
|
+
self._tz = self.data._tz
|
|
1431
|
+
self.lines.datetime._settz(self._tz)
|
|
1432
|
+
|
|
1433
|
+
if hasattr(self.data, "_calendar"):
|
|
1434
|
+
self._calendar = self.data._calendar
|
|
1435
|
+
|
|
1436
|
+
# guest data have already converted input
|
|
1437
|
+
self._tzinput = None # no need to further converr
|
|
1438
|
+
|
|
1439
|
+
# Copy dates/session infos
|
|
1440
|
+
if hasattr(self.data, "fromdate"):
|
|
1441
|
+
self.fromdate = self.data.fromdate
|
|
1442
|
+
if hasattr(self.data, "todate"):
|
|
1443
|
+
self.todate = self.data.todate
|
|
1444
|
+
|
|
1445
|
+
if hasattr(self.data, "sessionstart"):
|
|
1446
|
+
self.sessionstart = self.data.sessionstart
|
|
1447
|
+
if hasattr(self.data, "sessionend"):
|
|
1448
|
+
self.sessionend = self.data.sessionend
|
|
1449
|
+
|
|
1450
|
+
# Start
|
|
1451
|
+
def start(self):
|
|
1452
|
+
"""Start the data clone.
|
|
1453
|
+
|
|
1454
|
+
Initializes internal tracking variables.
|
|
1455
|
+
"""
|
|
1456
|
+
super().start()
|
|
1457
|
+
self._dlen = 0
|
|
1458
|
+
self._preloading = False
|
|
1459
|
+
|
|
1460
|
+
# Preload data
|
|
1461
|
+
def preload(self):
|
|
1462
|
+
"""Preload data from the source data feed.
|
|
1463
|
+
|
|
1464
|
+
After preloading, resets the source data's position.
|
|
1465
|
+
"""
|
|
1466
|
+
self._preloading = True
|
|
1467
|
+
super().preload()
|
|
1468
|
+
if hasattr(self.data, "home"):
|
|
1469
|
+
self.data.home() # preloading data was pushed forward
|
|
1470
|
+
self._preloading = False
|
|
1471
|
+
|
|
1472
|
+
# Load data
|
|
1473
|
+
def _load(self):
|
|
1474
|
+
"""Load data from the source data feed.
|
|
1475
|
+
|
|
1476
|
+
Returns:
|
|
1477
|
+
bool: True if data was loaded, False otherwise.
|
|
1478
|
+
"""
|
|
1479
|
+
# assumption: the data is in the system
|
|
1480
|
+
# copy the lines
|
|
1481
|
+
# If preparing to preload, run following code to copy specific data bit by bit
|
|
1482
|
+
if self._preloading:
|
|
1483
|
+
# data is preloaded, we are preloading too, can move
|
|
1484
|
+
# forward until have full bar or a data source is exhausted
|
|
1485
|
+
# Move data forward
|
|
1486
|
+
if hasattr(self.data, "advance"):
|
|
1487
|
+
self.data.advance()
|
|
1488
|
+
# If current data is greater than data buffer length, return False
|
|
1489
|
+
if len(self.data) > self.data.buflen():
|
|
1490
|
+
return False
|
|
1491
|
+
# If current data length is not greater than buffered data length, set line data to dline data
|
|
1492
|
+
for line, dline in zip(self.lines, self.data.lines):
|
|
1493
|
+
line[0] = dline[0]
|
|
1494
|
+
# Return True after successful setting
|
|
1495
|
+
return True
|
|
1496
|
+
|
|
1497
|
+
# Not preloading
|
|
1498
|
+
# This syntax is not very efficient, changing to len(self.data)<=self._dlen might save one comparison
|
|
1499
|
+
if len(self.data) <= self._dlen:
|
|
1500
|
+
# if not (len(self.data) > self._dlen): # backtrader built-in
|
|
1501
|
+
# Data not beyond last seen bar
|
|
1502
|
+
return False
|
|
1503
|
+
|
|
1504
|
+
# Increase data length by 1
|
|
1505
|
+
self._dlen += 1
|
|
1506
|
+
|
|
1507
|
+
# Set line data to dline data
|
|
1508
|
+
for line, dline in zip(self.lines, self.data.lines):
|
|
1509
|
+
line[0] = dline[0]
|
|
1510
|
+
|
|
1511
|
+
return True
|
|
1512
|
+
|
|
1513
|
+
# Move forward by size
|
|
1514
|
+
def advance(self, size=1, datamaster=None, ticks=True):
|
|
1515
|
+
"""Advance the data clone by the specified size.
|
|
1516
|
+
|
|
1517
|
+
Args:
|
|
1518
|
+
size: Number of bars to advance.
|
|
1519
|
+
datamaster: Master data feed for synchronization (unused).
|
|
1520
|
+
ticks: Whether to process tick data.
|
|
1521
|
+
"""
|
|
1522
|
+
self._dlen += size
|
|
1523
|
+
super().advance(size, datamaster, ticks=ticks)
|