back-trader-python 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- back_trader_python-1.4.0.dist-info/METADATA +1491 -0
- back_trader_python-1.4.0.dist-info/RECORD +465 -0
- back_trader_python-1.4.0.dist-info/WHEEL +5 -0
- back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
- back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
- backtrader/__init__.py +148 -0
- backtrader/_cerebro/__init__.py +5 -0
- backtrader/_cerebro/channel.py +382 -0
- backtrader/_cerebro/execution.py +377 -0
- backtrader/_cerebro/lifecycle.py +143 -0
- backtrader/_cerebro/notifications.py +150 -0
- backtrader/_cerebro/presentation.py +230 -0
- backtrader/_cerebro/registry.py +593 -0
- backtrader/_cerebro/runnext.py +551 -0
- backtrader/_cerebro/runonce.py +142 -0
- backtrader/analyzer.py +594 -0
- backtrader/analyzers/__init__.py +50 -0
- backtrader/analyzers/annualreturn.py +226 -0
- backtrader/analyzers/calmar.py +165 -0
- backtrader/analyzers/drawdown.py +287 -0
- backtrader/analyzers/leverage.py +112 -0
- backtrader/analyzers/logreturnsrolling.py +190 -0
- backtrader/analyzers/periodstats.py +153 -0
- backtrader/analyzers/positions.py +119 -0
- backtrader/analyzers/pyfolio.py +470 -0
- backtrader/analyzers/returns.py +192 -0
- backtrader/analyzers/sharpe.py +307 -0
- backtrader/analyzers/sharpe_ratio_stats.py +534 -0
- backtrader/analyzers/sqn.py +112 -0
- backtrader/analyzers/timereturn.py +192 -0
- backtrader/analyzers/total_value.py +75 -0
- backtrader/analyzers/tradeanalyzer.py +278 -0
- backtrader/analyzers/transactions.py +141 -0
- backtrader/analyzers/vwr.py +245 -0
- backtrader/bokeh/__init__.py +155 -0
- backtrader/bokeh/analyzers/__init__.py +13 -0
- backtrader/bokeh/analyzers/plot.py +192 -0
- backtrader/bokeh/analyzers/recorder.py +181 -0
- backtrader/bokeh/app.py +1094 -0
- backtrader/bokeh/live/__init__.py +11 -0
- backtrader/bokeh/live/client.py +352 -0
- backtrader/bokeh/live/datahandler.py +346 -0
- backtrader/bokeh/plot_adapter.py +200 -0
- backtrader/bokeh/schemes/__init__.py +14 -0
- backtrader/bokeh/schemes/blackly.py +76 -0
- backtrader/bokeh/schemes/scheme.py +150 -0
- backtrader/bokeh/schemes/tradimo.py +82 -0
- backtrader/bokeh/tab.py +125 -0
- backtrader/bokeh/tabs/__init__.py +30 -0
- backtrader/bokeh/tabs/analyzer.py +120 -0
- backtrader/bokeh/tabs/config.py +154 -0
- backtrader/bokeh/tabs/live.py +109 -0
- backtrader/bokeh/tabs/log.py +185 -0
- backtrader/bokeh/tabs/metadata.py +182 -0
- backtrader/bokeh/tabs/performance.py +359 -0
- backtrader/bokeh/tabs/source.py +70 -0
- backtrader/bokeh/utils/__init__.py +8 -0
- backtrader/bokeh/utils/helpers.py +167 -0
- backtrader/bokeh/webapp.py +164 -0
- backtrader/broker.py +478 -0
- backtrader/brokers/__init__.py +36 -0
- backtrader/brokers/bbroker.py +2576 -0
- backtrader/brokers/btapibroker.py +8227 -0
- backtrader/brokers/hft/__init__.py +89 -0
- backtrader/brokers/hft/binance_bbo.py +625 -0
- backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
- backtrader/brokers/hft/examples.py +1228 -0
- backtrader/brokers/hft/exchange.py +380 -0
- backtrader/brokers/hft/latency.py +309 -0
- backtrader/brokers/hft/matching_core.py +572 -0
- backtrader/brokers/hft/queue.py +238 -0
- backtrader/brokers/hft/recorder.py +88 -0
- backtrader/brokers/hft/state.py +138 -0
- backtrader/brokers/impact_models.py +118 -0
- backtrader/brokers/mixbroker.py +895 -0
- backtrader/brokers/tickbroker.py +1991 -0
- backtrader/btrun/__init__.py +12 -0
- backtrader/btrun/btrun.py +1218 -0
- backtrader/cerebro.py +828 -0
- backtrader/channel.py +682 -0
- backtrader/channels/__init__.py +23 -0
- backtrader/channels/bridge.py +186 -0
- backtrader/channels/funding.py +248 -0
- backtrader/channels/live_queue.py +216 -0
- backtrader/channels/live_validator.py +294 -0
- backtrader/channels/orderbook.py +257 -0
- backtrader/channels/tick.py +202 -0
- backtrader/comminfo.py +665 -0
- backtrader/commissions/__init__.py +106 -0
- backtrader/commissions/ctpoption.py +993 -0
- backtrader/configs/account_config_example.yaml +8 -0
- backtrader/dataseries.py +379 -0
- backtrader/errors.py +106 -0
- backtrader/events.py +980 -0
- backtrader/feed.py +1523 -0
- backtrader/feeds/__init__.py +75 -0
- backtrader/feeds/barrier.py +2006 -0
- backtrader/feeds/blaze.py +118 -0
- backtrader/feeds/btapifeed.py +1538 -0
- backtrader/feeds/btcsv.py +203 -0
- backtrader/feeds/chainer.py +114 -0
- backtrader/feeds/cryptohftdata.py +164 -0
- backtrader/feeds/csvgeneric.py +1205 -0
- backtrader/feeds/ctpcohort.py +1051 -0
- backtrader/feeds/influxfeed.py +158 -0
- backtrader/feeds/livefeed.py +71 -0
- backtrader/feeds/mixed_channel.py +108 -0
- backtrader/feeds/mt4csv.py +42 -0
- backtrader/feeds/pandafeed.py +381 -0
- backtrader/feeds/quandl.py +256 -0
- backtrader/feeds/rollover.py +229 -0
- backtrader/feeds/sierrachart.py +30 -0
- backtrader/feeds/vchart.py +162 -0
- backtrader/feeds/vchartcsv.py +84 -0
- backtrader/feeds/vchartfile.py +153 -0
- backtrader/feeds/yahoo.py +399 -0
- backtrader/fillers.py +148 -0
- backtrader/filters/__init__.py +34 -0
- backtrader/filters/bsplitter.py +127 -0
- backtrader/filters/calendardays.py +121 -0
- backtrader/filters/datafiller.py +192 -0
- backtrader/filters/datafilter.py +74 -0
- backtrader/filters/daysteps.py +96 -0
- backtrader/filters/heikinashi.py +63 -0
- backtrader/filters/renko.py +164 -0
- backtrader/filters/session.py +289 -0
- backtrader/flt.py +80 -0
- backtrader/functions.py +960 -0
- backtrader/indicator.py +449 -0
- backtrader/indicators/__init__.py +148 -0
- backtrader/indicators/accdecoscillator.py +110 -0
- backtrader/indicators/aroon.py +300 -0
- backtrader/indicators/atr.py +315 -0
- backtrader/indicators/awesomeoscillator.py +122 -0
- backtrader/indicators/basicops.py +834 -0
- backtrader/indicators/bollinger.py +223 -0
- backtrader/indicators/cci.py +89 -0
- backtrader/indicators/channels_ext.py +83 -0
- backtrader/indicators/contrib/__init__.py +228 -0
- backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
- backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
- backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
- backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
- backtrader/indicators/contrib/adxdmi.py +34 -0
- backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
- backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
- backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
- backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
- backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
- backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
- backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
- backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
- backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
- backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
- backtrader/indicators/contrib/average_change_candle.py +165 -0
- backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
- backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
- backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
- backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
- backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
- backtrader/indicators/contrib/blau_csi.py +76 -0
- backtrader/indicators/contrib/blau_ergodic.py +53 -0
- backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
- backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
- backtrader/indicators/contrib/blau_tvi.py +55 -0
- backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
- backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
- backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
- backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
- backtrader/indicators/contrib/bsi_indicator.py +87 -0
- backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
- backtrader/indicators/contrib/bulls_power.py +56 -0
- backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
- backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
- backtrader/indicators/contrib/candle_stop_color.py +46 -0
- backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
- backtrader/indicators/contrib/candlesticks_bw.py +45 -0
- backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
- backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
- backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
- backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
- backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
- backtrader/indicators/contrib/cg_oscillator.py +40 -0
- backtrader/indicators/contrib/close_line_cci.py +38 -0
- backtrader/indicators/contrib/close_price_fractals.py +47 -0
- backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
- backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
- backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
- backtrader/indicators/contrib/color_hma.py +71 -0
- backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
- backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
- backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
- backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
- backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
- backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
- backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
- backtrader/indicators/contrib/color_x2_ma.py +49 -0
- backtrader/indicators/contrib/color_x_derivative.py +63 -0
- backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
- backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
- backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
- backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
- backtrader/indicators/contrib/derivative_indicator.py +95 -0
- backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
- backtrader/indicators/contrib/digital_macd.py +200 -0
- backtrader/indicators/contrib/donchian_channels_system.py +45 -0
- backtrader/indicators/contrib/dots_indicator.py +93 -0
- backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
- backtrader/indicators/contrib/ema_rsi_va.py +80 -0
- backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
- backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
- backtrader/indicators/contrib/fatl_filter.py +179 -0
- backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
- backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
- backtrader/indicators/contrib/fisher_org_v1.py +102 -0
- backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
- backtrader/indicators/contrib/force_index_ema.py +96 -0
- backtrader/indicators/contrib/force_index_ema_2.py +27 -0
- backtrader/indicators/contrib/forecast_oscilator.py +145 -0
- backtrader/indicators/contrib/fractal_amambk.py +81 -0
- backtrader/indicators/contrib/frama_series.py +84 -0
- backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
- backtrader/indicators/contrib/go_indicator.py +93 -0
- backtrader/indicators/contrib/hlr_indicator.py +95 -0
- backtrader/indicators/contrib/hma.py +50 -0
- backtrader/indicators/contrib/i4_drfv2.py +34 -0
- backtrader/indicators/contrib/i4_drfv3.py +38 -0
- backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
- backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
- backtrader/indicators/contrib/i_gap_indicator.py +45 -0
- backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
- backtrader/indicators/contrib/i_trend_indicator.py +125 -0
- backtrader/indicators/contrib/iamma_indicator.py +39 -0
- backtrader/indicators/contrib/indexed_moving_average.py +33 -0
- backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
- backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
- backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
- backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
- backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
- backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
- backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
- backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
- backtrader/indicators/contrib/kalman_filter_line.py +127 -0
- backtrader/indicators/contrib/kama_indicator.py +150 -0
- backtrader/indicators/contrib/karacatica_indicator.py +99 -0
- backtrader/indicators/contrib/kdj_indicator.py +59 -0
- backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
- backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
- backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
- backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
- backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
- backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
- backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
- backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
- backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
- backtrader/indicators/contrib/loco_indicator.py +88 -0
- backtrader/indicators/contrib/lrma_indicator.py +185 -0
- backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
- backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
- backtrader/indicators/contrib/macd2_indicator.py +61 -0
- backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
- backtrader/indicators/contrib/malr_indicator.py +77 -0
- backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
- backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
- backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
- backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
- backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
- backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
- backtrader/indicators/contrib/nrtr_indicator.py +95 -0
- backtrader/indicators/contrib/p_channel_system.py +40 -0
- backtrader/indicators/contrib/percent_envelope.py +37 -0
- backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
- backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
- backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
- backtrader/indicators/contrib/price_extreme_channel.py +35 -0
- backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
- backtrader/indicators/contrib/ravi_indicator.py +40 -0
- backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
- backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
- backtrader/indicators/contrib/renko_level.py +85 -0
- backtrader/indicators/contrib/renko_line_break.py +91 -0
- backtrader/indicators/contrib/rftl_indicator.py +41 -0
- backtrader/indicators/contrib/rkd_indicator.py +53 -0
- backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
- backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
- backtrader/indicators/contrib/rsi_slowdown.py +57 -0
- backtrader/indicators/contrib/rsioma_v2.py +41 -0
- backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
- backtrader/indicators/contrib/safe_adx.py +89 -0
- backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
- backtrader/indicators/contrib/sidus_indicator.py +105 -0
- backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
- backtrader/indicators/contrib/sliding_range_color.py +56 -0
- backtrader/indicators/contrib/slow_stoch.py +42 -0
- backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
- backtrader/indicators/contrib/smoothed_rsi.py +31 -0
- backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
- backtrader/indicators/contrib/stalin_indicator.py +152 -0
- backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
- backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
- backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
- backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
- backtrader/indicators/contrib/t3_average.py +76 -0
- backtrader/indicators/contrib/t3_indicator.py +40 -0
- backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
- backtrader/indicators/contrib/three_candles_indicator.py +70 -0
- backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
- backtrader/indicators/contrib/time_line.py +57 -0
- backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
- backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
- backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
- backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
- backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
- backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
- backtrader/indicators/contrib/trigger_line.py +66 -0
- backtrader/indicators/contrib/triple_ema_rate.py +34 -0
- backtrader/indicators/contrib/trvi_indicator.py +194 -0
- backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
- backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
- backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
- backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
- backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
- backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
- backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
- backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
- backtrader/indicators/contrib/vwma_candle.py +57 -0
- backtrader/indicators/contrib/vwma_digit_system.py +70 -0
- backtrader/indicators/contrib/wami.py +43 -0
- backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
- backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
- backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
- backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
- backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
- backtrader/indicators/contrib/xma_ichimoku.py +163 -0
- backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
- backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
- backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
- backtrader/indicators/contrib/xmacd_indicator.py +70 -0
- backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
- backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
- backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
- backtrader/indicators/contrib/xrvi_indicator.py +130 -0
- backtrader/indicators/contrib/zero_lag_macd.py +36 -0
- backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
- backtrader/indicators/contrib/zpf_indicator.py +115 -0
- backtrader/indicators/crossover.py +337 -0
- backtrader/indicators/dema.py +175 -0
- backtrader/indicators/demarker.py +270 -0
- backtrader/indicators/deviation.py +284 -0
- backtrader/indicators/directionalmove.py +1071 -0
- backtrader/indicators/dma.py +112 -0
- backtrader/indicators/dpo.py +96 -0
- backtrader/indicators/dv2.py +56 -0
- backtrader/indicators/ema.py +145 -0
- backtrader/indicators/envelope.py +475 -0
- backtrader/indicators/hadelta.py +198 -0
- backtrader/indicators/heikinashi.py +153 -0
- backtrader/indicators/hma.py +153 -0
- backtrader/indicators/hurst.py +151 -0
- backtrader/indicators/ichimoku.py +267 -0
- backtrader/indicators/kama.py +181 -0
- backtrader/indicators/kst.py +159 -0
- backtrader/indicators/lrsi.py +125 -0
- backtrader/indicators/mabase.py +147 -0
- backtrader/indicators/macd.py +322 -0
- backtrader/indicators/momentum.py +267 -0
- backtrader/indicators/moneyflow.py +237 -0
- backtrader/indicators/mt5atr.py +124 -0
- backtrader/indicators/myind.py +179 -0
- backtrader/indicators/obv.py +94 -0
- backtrader/indicators/ols.py +265 -0
- backtrader/indicators/oscillator.py +161 -0
- backtrader/indicators/percentchange.py +83 -0
- backtrader/indicators/percentrank.py +46 -0
- backtrader/indicators/pivotpoint.py +469 -0
- backtrader/indicators/prettygoodoscillator.py +113 -0
- backtrader/indicators/priceops_ext.py +123 -0
- backtrader/indicators/priceoscillator.py +262 -0
- backtrader/indicators/psar.py +212 -0
- backtrader/indicators/rmi.py +69 -0
- backtrader/indicators/rsi.py +440 -0
- backtrader/indicators/sma.py +141 -0
- backtrader/indicators/smma.py +116 -0
- backtrader/indicators/spread.py +54 -0
- backtrader/indicators/stochastic.py +263 -0
- backtrader/indicators/supertrend.py +436 -0
- backtrader/indicators/trend_ext.py +105 -0
- backtrader/indicators/trix.py +202 -0
- backtrader/indicators/tsi.py +155 -0
- backtrader/indicators/ultimateoscillator.py +158 -0
- backtrader/indicators/vortex.py +62 -0
- backtrader/indicators/williams.py +194 -0
- backtrader/indicators/wma.py +103 -0
- backtrader/indicators/zlema.py +135 -0
- backtrader/indicators/zlind.py +104 -0
- backtrader/linebuffer.py +3155 -0
- backtrader/lineiterator.py +2911 -0
- backtrader/lineroot.py +1106 -0
- backtrader/lineseries.py +2559 -0
- backtrader/live_trading/__init__.py +31 -0
- backtrader/live_trading/interface.py +404 -0
- backtrader/mathsupport.py +94 -0
- backtrader/metabase.py +1804 -0
- backtrader/mixins/__init__.py +21 -0
- backtrader/mixins/singleton.py +118 -0
- backtrader/observer.py +106 -0
- backtrader/observers/__init__.py +45 -0
- backtrader/observers/benchmark.py +126 -0
- backtrader/observers/broker.py +184 -0
- backtrader/observers/buysell.py +144 -0
- backtrader/observers/drawdown.py +161 -0
- backtrader/observers/logreturns.py +113 -0
- backtrader/observers/timereturn.py +86 -0
- backtrader/observers/trade_logger.py +2972 -0
- backtrader/observers/tradelogger.py +6 -0
- backtrader/observers/trades.py +258 -0
- backtrader/order.py +1114 -0
- backtrader/parameters.py +2345 -0
- backtrader/plot/__init__.py +54 -0
- backtrader/plot/finance.py +1022 -0
- backtrader/plot/formatters.py +200 -0
- backtrader/plot/locator.py +353 -0
- backtrader/plot/multicursor.py +495 -0
- backtrader/plot/plot.py +2500 -0
- backtrader/plot/plot_plotly.py +1351 -0
- backtrader/plot/scheme.py +253 -0
- backtrader/plot/utils.py +104 -0
- backtrader/position.py +290 -0
- backtrader/position_modes.py +132 -0
- backtrader/profiles.py +254 -0
- backtrader/reports/__init__.py +39 -0
- backtrader/reports/charts.py +371 -0
- backtrader/reports/performance.py +620 -0
- backtrader/reports/reporter.py +660 -0
- backtrader/resamplerfilter.py +1001 -0
- backtrader/signal.py +118 -0
- backtrader/signals/__init__.py +17 -0
- backtrader/sizer.py +114 -0
- backtrader/sizers/__init__.py +26 -0
- backtrader/sizers/fixedsize.py +161 -0
- backtrader/sizers/percents_sizer.py +119 -0
- backtrader/store.py +221 -0
- backtrader/stores/__init__.py +33 -0
- backtrader/stores/btapistore.py +15506 -0
- backtrader/stores/livestore.py +137 -0
- backtrader/stores/vchartfile.py +96 -0
- backtrader/strategy.py +3655 -0
- backtrader/talib.py +280 -0
- backtrader/test_helpers.py +96 -0
- backtrader/timer.py +358 -0
- backtrader/trade.py +442 -0
- backtrader/tradingcal.py +361 -0
- backtrader/utils/__init__.py +68 -0
- backtrader/utils/autodict.py +251 -0
- backtrader/utils/date.py +71 -0
- backtrader/utils/dateintern.py +509 -0
- backtrader/utils/flushfile.py +94 -0
- backtrader/utils/fractal.py +101 -0
- backtrader/utils/get_metrics.py +101 -0
- backtrader/utils/load_data.py +209 -0
- backtrader/utils/log_message.py +998 -0
- backtrader/utils/ordereddefaultdict.py +75 -0
- backtrader/utils/py3.py +296 -0
- backtrader/version.py +21 -0
- backtrader/writer.py +372 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
"""Mixed-mode broker for tick-driven mid-frequency coordination.
|
|
2
|
+
|
|
3
|
+
MixBroker keeps TickBroker as the only execution path for orders while
|
|
4
|
+
maintaining low-frequency bar state and high-frequency order book windows
|
|
5
|
+
for strategy-side queries.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
Using MixBroker with Cerebro:
|
|
9
|
+
cerebro = bt.Cerebro()
|
|
10
|
+
cerebro.setbroker(MixBroker(cash=100000))
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import collections
|
|
14
|
+
import copy
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from decimal import Decimal, InvalidOperation
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import fcntl as _fcntl
|
|
25
|
+
except ImportError: # pragma: no cover - unavailable on Windows
|
|
26
|
+
_fcntl = None
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import msvcrt as _msvcrt
|
|
30
|
+
except ImportError: # pragma: no cover - unavailable on POSIX hosts
|
|
31
|
+
_msvcrt = None
|
|
32
|
+
|
|
33
|
+
from backtrader.brokers.tickbroker import TickBroker
|
|
34
|
+
from backtrader.parameters import ParameterDescriptor
|
|
35
|
+
|
|
36
|
+
from ..utils.log_message import get_logger
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
__all__ = ["MixBroker", "MidFreqContext"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _acquire_nonblocking_file_lock(handle):
|
|
44
|
+
"""Acquire one portable, process-scoped exclusive lock for ``handle``.
|
|
45
|
+
|
|
46
|
+
``fcntl.flock`` is not available on Windows. The Windows CRT lock is
|
|
47
|
+
byte-range based, so reserve byte zero in the lock file before taking its
|
|
48
|
+
non-blocking lock. The lock file's JSON payload remains diagnostic only;
|
|
49
|
+
ownership is still established by the operating-system lease.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if _fcntl is not None:
|
|
53
|
+
_fcntl.flock(handle.fileno(), _fcntl.LOCK_EX | _fcntl.LOCK_NB)
|
|
54
|
+
return
|
|
55
|
+
if _msvcrt is not None:
|
|
56
|
+
handle.seek(0, os.SEEK_END)
|
|
57
|
+
if handle.tell() == 0:
|
|
58
|
+
handle.write(" ")
|
|
59
|
+
handle.flush()
|
|
60
|
+
handle.seek(0)
|
|
61
|
+
try:
|
|
62
|
+
_msvcrt.locking(handle.fileno(), _msvcrt.LK_NBLCK, 1)
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
raise BlockingIOError from exc
|
|
65
|
+
return
|
|
66
|
+
raise RuntimeError("account_risk_locking_unavailable")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _release_file_lock(handle):
|
|
70
|
+
"""Release a lock acquired by :func:`_acquire_nonblocking_file_lock`."""
|
|
71
|
+
|
|
72
|
+
if _fcntl is not None:
|
|
73
|
+
_fcntl.flock(handle.fileno(), _fcntl.LOCK_UN)
|
|
74
|
+
elif _msvcrt is not None:
|
|
75
|
+
handle.seek(0)
|
|
76
|
+
_msvcrt.locking(handle.fileno(), _msvcrt.LK_UNLCK, 1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _fsync_directory(path):
|
|
80
|
+
"""Persist a POSIX rename without treating unsupported Windows APIs as failure."""
|
|
81
|
+
|
|
82
|
+
# Windows cannot open a directory through ``os.open`` for ``os.fsync``.
|
|
83
|
+
# The replacement has already been flushed and fsynced; that is the
|
|
84
|
+
# strongest portable guarantee exposed by the standard library there.
|
|
85
|
+
if os.name == "nt":
|
|
86
|
+
return
|
|
87
|
+
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
|
|
88
|
+
try:
|
|
89
|
+
os.fsync(descriptor)
|
|
90
|
+
finally:
|
|
91
|
+
os.close(descriptor)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _durable_replace(source, target):
|
|
95
|
+
"""Replace one same-directory ledger file with the strongest host primitive."""
|
|
96
|
+
|
|
97
|
+
if os.name != "nt":
|
|
98
|
+
os.replace(source, target)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# ``os.replace`` does not request a write-through move on Windows. The
|
|
102
|
+
# ledger is the fence that blocks unsafe paper trading after a crash, so
|
|
103
|
+
# fail closed if the Win32 durable move cannot be requested.
|
|
104
|
+
import ctypes
|
|
105
|
+
|
|
106
|
+
move_file = ctypes.WinDLL("kernel32", use_last_error=True).MoveFileExW
|
|
107
|
+
move_file.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint)
|
|
108
|
+
move_file.restype = ctypes.c_int
|
|
109
|
+
movefile_replace_existing = 0x00000001
|
|
110
|
+
movefile_write_through = 0x00000008
|
|
111
|
+
if not move_file(str(source), str(target), movefile_replace_existing | movefile_write_through):
|
|
112
|
+
error_code = ctypes.get_last_error()
|
|
113
|
+
raise OSError(error_code, "MoveFileExW write-through replacement failed", str(target))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MixBroker(TickBroker):
|
|
117
|
+
"""Experimental coordination broker for mid-frequency backtests.
|
|
118
|
+
|
|
119
|
+
:class:`MixBroker` extends :class:`TickBroker` to maintain additional
|
|
120
|
+
mid-frequency state on top of the tick-driven execution path. In
|
|
121
|
+
particular it keeps:
|
|
122
|
+
|
|
123
|
+
* a per-symbol rolling window of recent order book snapshots
|
|
124
|
+
(size controlled by :attr:`max_ob_window`),
|
|
125
|
+
* a per-symbol rolling buffer of completed bars
|
|
126
|
+
(size controlled by :attr:`max_bar_history`),
|
|
127
|
+
* an incrementally maintained simple moving average per symbol
|
|
128
|
+
(period controlled by :attr:`default_sma_period`).
|
|
129
|
+
|
|
130
|
+
Strategies read this state through the broker's
|
|
131
|
+
:class:`MidFreqContext`, which exposes deep-copied views so the
|
|
132
|
+
underlying buffers are never mutated by user code.
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
max_ob_window: Per-symbol order book window size (deque maxlen).
|
|
136
|
+
max_bar_history: Per-symbol completed bar history size (deque maxlen).
|
|
137
|
+
default_sma_period: Period of the incrementally maintained SMA.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
max_ob_window = ParameterDescriptor(default=100, doc="Per-symbol order book window size")
|
|
141
|
+
max_bar_history = ParameterDescriptor(default=200, doc="Per-symbol completed bar history size")
|
|
142
|
+
default_sma_period = ParameterDescriptor(default=20, doc="Incrementally maintained SMA period")
|
|
143
|
+
account_risk_ledger_path = ParameterDescriptor(
|
|
144
|
+
default=None,
|
|
145
|
+
doc="Ignored local path for durable paper account-risk evidence",
|
|
146
|
+
)
|
|
147
|
+
account_risk_venues = ParameterDescriptor(
|
|
148
|
+
default=(),
|
|
149
|
+
doc="Canonical provider ids covered by the paper account-risk ledger",
|
|
150
|
+
)
|
|
151
|
+
account_risk_persist_interval = ParameterDescriptor(
|
|
152
|
+
default=0.05,
|
|
153
|
+
doc="Minimum seconds between mark-to-market ledger writes",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def __init__(self, **kwargs):
|
|
157
|
+
"""Initialize the broker and its mid-frequency state containers.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
**kwargs: Forwarded to :class:`TickBroker`'s constructor.
|
|
161
|
+
"""
|
|
162
|
+
super().__init__(**kwargs)
|
|
163
|
+
self._account_risk_lock_handle = None
|
|
164
|
+
self._account_risk_lock_acquired = False
|
|
165
|
+
self._account_risk_owner_token = uuid.uuid4().hex
|
|
166
|
+
self._account_risk_realized_net = Decimal("0")
|
|
167
|
+
self._account_risk_last_persist_ns = 0
|
|
168
|
+
self._account_risk_failed = True
|
|
169
|
+
self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
|
|
170
|
+
"account_risk_ledger_not_started"
|
|
171
|
+
)
|
|
172
|
+
self._reset_midfreq_state()
|
|
173
|
+
|
|
174
|
+
def start(self):
|
|
175
|
+
"""Reset the mid-frequency state containers when the run starts.
|
|
176
|
+
|
|
177
|
+
The base :class:`TickBroker` is started first; this method then
|
|
178
|
+
rebuilds the per-symbol order book window, completed-bar
|
|
179
|
+
history, and indicator buffers.
|
|
180
|
+
"""
|
|
181
|
+
super().start()
|
|
182
|
+
self._reset_midfreq_state()
|
|
183
|
+
self._start_account_risk_ledger()
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _canonical_risk_venue(venue):
|
|
187
|
+
return str(venue or "").partition("___")[0].strip().lower()
|
|
188
|
+
|
|
189
|
+
def _configured_risk_venues(self):
|
|
190
|
+
raw = self.get_param("account_risk_venues") or ()
|
|
191
|
+
if isinstance(raw, str):
|
|
192
|
+
raw = [item for item in raw.split(",") if item.strip()]
|
|
193
|
+
return sorted(
|
|
194
|
+
{
|
|
195
|
+
self._canonical_risk_venue(venue)
|
|
196
|
+
for venue in raw
|
|
197
|
+
if self._canonical_risk_venue(venue)
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _account_risk_identity_sha256(self):
|
|
202
|
+
"""Bind paper-risk evidence to its exact durable ledger and venue set."""
|
|
203
|
+
ledger_value = self.get_param("account_risk_ledger_path")
|
|
204
|
+
payload = {
|
|
205
|
+
"authority": "MixBroker",
|
|
206
|
+
"configured_venues": self._configured_risk_venues(),
|
|
207
|
+
"ledger_path": (
|
|
208
|
+
str(Path(ledger_value).expanduser().resolve())
|
|
209
|
+
if ledger_value not in (None, "")
|
|
210
|
+
else ""
|
|
211
|
+
),
|
|
212
|
+
}
|
|
213
|
+
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
214
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
215
|
+
|
|
216
|
+
def _unavailable_account_risk_snapshot(self, error_code):
|
|
217
|
+
return {
|
|
218
|
+
"baseline_equity": None,
|
|
219
|
+
"current_equity": None,
|
|
220
|
+
"realized_net": None,
|
|
221
|
+
"configured_venues": self._configured_risk_venues(),
|
|
222
|
+
"generation": 0,
|
|
223
|
+
"fencing_epoch": 0,
|
|
224
|
+
"as_of_monotonic_ns": 0,
|
|
225
|
+
"owner_pid": os.getpid(),
|
|
226
|
+
"clock_domain_id": f"process:{os.getpid()}:monotonic",
|
|
227
|
+
"identity_binding_sha256": self._account_risk_identity_sha256(),
|
|
228
|
+
"durable": False,
|
|
229
|
+
"trading_blocked": True,
|
|
230
|
+
"evidence_complete": False,
|
|
231
|
+
"evidence_errors": [str(error_code)],
|
|
232
|
+
"error_code": str(error_code),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _decimal(value, name):
|
|
237
|
+
try:
|
|
238
|
+
result = Decimal(str(value))
|
|
239
|
+
except (InvalidOperation, TypeError, ValueError) as exc:
|
|
240
|
+
logger.error(
|
|
241
|
+
"mixbroker:161 re-raising InvalidOperation,TypeError,ValueError", exc_info=True
|
|
242
|
+
)
|
|
243
|
+
raise ValueError(f"invalid_{name}") from exc
|
|
244
|
+
if not result.is_finite():
|
|
245
|
+
raise ValueError(f"invalid_{name}")
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
def _start_account_risk_ledger(self):
|
|
249
|
+
"""Acquire one writer lease and continue the durable paper account."""
|
|
250
|
+
self._release_account_risk_ledger()
|
|
251
|
+
self._account_risk_owner_token = uuid.uuid4().hex
|
|
252
|
+
self._account_risk_realized_net = Decimal("0")
|
|
253
|
+
self._account_risk_last_persist_ns = 0
|
|
254
|
+
self._account_risk_failed = True
|
|
255
|
+
ledger_value = self.get_param("account_risk_ledger_path")
|
|
256
|
+
venues = self._configured_risk_venues()
|
|
257
|
+
if ledger_value in (None, ""):
|
|
258
|
+
self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
|
|
259
|
+
"account_risk_ledger_path_required"
|
|
260
|
+
)
|
|
261
|
+
return
|
|
262
|
+
if not venues:
|
|
263
|
+
self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
|
|
264
|
+
"account_risk_venues_required"
|
|
265
|
+
)
|
|
266
|
+
return
|
|
267
|
+
if _fcntl is None and _msvcrt is None:
|
|
268
|
+
self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
|
|
269
|
+
"account_risk_locking_unavailable"
|
|
270
|
+
)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
ledger_path = Path(ledger_value).expanduser().resolve()
|
|
274
|
+
lock_path = ledger_path.with_name(f"{ledger_path.name}.lock")
|
|
275
|
+
try:
|
|
276
|
+
ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
|
277
|
+
lock_handle = lock_path.open("a+", encoding="utf-8")
|
|
278
|
+
self._account_risk_lock_handle = lock_handle
|
|
279
|
+
_acquire_nonblocking_file_lock(lock_handle)
|
|
280
|
+
self._account_risk_lock_acquired = True
|
|
281
|
+
|
|
282
|
+
generation = 1
|
|
283
|
+
fencing_epoch = 1
|
|
284
|
+
baseline = self._decimal(self.getvalue(), "baseline_equity")
|
|
285
|
+
current = baseline
|
|
286
|
+
if ledger_path.exists():
|
|
287
|
+
prior = json.loads(ledger_path.read_text(encoding="utf-8"))
|
|
288
|
+
if not isinstance(prior, dict) or prior.get("schema_version") != 2:
|
|
289
|
+
raise ValueError("account_risk_ledger_schema_invalid")
|
|
290
|
+
if prior.get("session_state") != "closed":
|
|
291
|
+
raise ValueError("account_risk_ledger_previous_session_active")
|
|
292
|
+
prior_venues = sorted(
|
|
293
|
+
self._canonical_risk_venue(venue)
|
|
294
|
+
for venue in prior.get("configured_venues", ())
|
|
295
|
+
)
|
|
296
|
+
if prior_venues != venues:
|
|
297
|
+
raise ValueError("account_risk_ledger_venues_mismatch")
|
|
298
|
+
generation = int(prior.get("generation", 0)) + 1
|
|
299
|
+
fencing_epoch = int(prior.get("fencing_epoch", 0)) + 1
|
|
300
|
+
if generation <= 1 or fencing_epoch <= 1:
|
|
301
|
+
raise ValueError("account_risk_ledger_fence_invalid")
|
|
302
|
+
baseline = self._decimal(prior.get("baseline_equity"), "baseline_equity")
|
|
303
|
+
current = self._decimal(prior.get("current_equity"), "current_equity")
|
|
304
|
+
realized = self._decimal(prior.get("realized_net"), "realized_net")
|
|
305
|
+
if baseline <= 0:
|
|
306
|
+
raise ValueError("account_risk_ledger_baseline_invalid")
|
|
307
|
+
self._account_risk_realized_net = realized
|
|
308
|
+
# A sealed paper epoch is required to be flat. Hydrate the
|
|
309
|
+
# new in-memory broker from its durable equity instead of
|
|
310
|
+
# silently restoring the configured starting cash.
|
|
311
|
+
self._cash = float(current)
|
|
312
|
+
self._value = self._cash
|
|
313
|
+
self.startingcash = self._cash
|
|
314
|
+
self.startingvalue = self._value
|
|
315
|
+
|
|
316
|
+
lock_handle.seek(0)
|
|
317
|
+
lock_handle.truncate()
|
|
318
|
+
json.dump(
|
|
319
|
+
{
|
|
320
|
+
"owner_token": self._account_risk_owner_token,
|
|
321
|
+
"owner_pid": os.getpid(),
|
|
322
|
+
"generation": generation,
|
|
323
|
+
"fencing_epoch": fencing_epoch,
|
|
324
|
+
},
|
|
325
|
+
lock_handle,
|
|
326
|
+
separators=(",", ":"),
|
|
327
|
+
)
|
|
328
|
+
lock_handle.flush()
|
|
329
|
+
os.fsync(lock_handle.fileno())
|
|
330
|
+
self._account_risk_snapshot = {
|
|
331
|
+
"baseline_equity": baseline,
|
|
332
|
+
"current_equity": current,
|
|
333
|
+
"realized_net": self._account_risk_realized_net,
|
|
334
|
+
"configured_venues": venues,
|
|
335
|
+
"generation": generation,
|
|
336
|
+
"fencing_epoch": fencing_epoch,
|
|
337
|
+
"as_of_monotonic_ns": 0,
|
|
338
|
+
"owner_pid": os.getpid(),
|
|
339
|
+
"clock_domain_id": f"process:{os.getpid()}:monotonic",
|
|
340
|
+
"identity_binding_sha256": self._account_risk_identity_sha256(),
|
|
341
|
+
"durable": False,
|
|
342
|
+
"trading_blocked": True,
|
|
343
|
+
"evidence_complete": False,
|
|
344
|
+
}
|
|
345
|
+
self._account_risk_failed = False
|
|
346
|
+
self._persist_account_risk_snapshot(force=True)
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
logger.warning(
|
|
349
|
+
"MixBroker account-risk ledger startup failed: %s",
|
|
350
|
+
type(exc).__name__,
|
|
351
|
+
exc_info=True,
|
|
352
|
+
)
|
|
353
|
+
code = "account_risk_ledger_locked" if isinstance(exc, BlockingIOError) else str(exc)
|
|
354
|
+
if not code.startswith("account_risk_"):
|
|
355
|
+
code = "account_risk_ledger_start_failed"
|
|
356
|
+
self._account_risk_snapshot = self._unavailable_account_risk_snapshot(code)
|
|
357
|
+
self._release_account_risk_ledger()
|
|
358
|
+
|
|
359
|
+
def _atomic_write_account_risk(self, payload):
|
|
360
|
+
ledger_path = Path(self.get_param("account_risk_ledger_path")).expanduser().resolve()
|
|
361
|
+
temporary = ledger_path.with_name(
|
|
362
|
+
f".{ledger_path.name}.{self._account_risk_owner_token}.tmp"
|
|
363
|
+
)
|
|
364
|
+
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
365
|
+
descriptor = None
|
|
366
|
+
try:
|
|
367
|
+
descriptor = os.open(
|
|
368
|
+
temporary,
|
|
369
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
|
|
370
|
+
0o600,
|
|
371
|
+
)
|
|
372
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
373
|
+
descriptor = None
|
|
374
|
+
handle.write(encoded)
|
|
375
|
+
handle.flush()
|
|
376
|
+
os.fsync(handle.fileno())
|
|
377
|
+
_durable_replace(temporary, ledger_path)
|
|
378
|
+
_fsync_directory(ledger_path.parent)
|
|
379
|
+
finally:
|
|
380
|
+
if descriptor is not None:
|
|
381
|
+
os.close(descriptor)
|
|
382
|
+
try:
|
|
383
|
+
temporary.unlink()
|
|
384
|
+
except FileNotFoundError:
|
|
385
|
+
logger.warning("mixbroker:302 suppressed FileNotFoundError")
|
|
386
|
+
|
|
387
|
+
def _persist_account_risk_snapshot(self, *, force=False, session_state="active"):
|
|
388
|
+
if self._account_risk_lock_handle is None or self._account_risk_failed:
|
|
389
|
+
return False
|
|
390
|
+
now_ns = time.monotonic_ns()
|
|
391
|
+
interval_ns = int(
|
|
392
|
+
max(float(self.get_param("account_risk_persist_interval") or 0.0), 0.0) * 1_000_000_000
|
|
393
|
+
)
|
|
394
|
+
if not force and now_ns - self._account_risk_last_persist_ns < interval_ns:
|
|
395
|
+
return True
|
|
396
|
+
try:
|
|
397
|
+
current = self._decimal(self.getvalue(), "current_equity")
|
|
398
|
+
snapshot = {
|
|
399
|
+
**self._account_risk_snapshot,
|
|
400
|
+
"current_equity": current,
|
|
401
|
+
"realized_net": self._account_risk_realized_net,
|
|
402
|
+
"as_of_monotonic_ns": now_ns,
|
|
403
|
+
"owner_pid": os.getpid(),
|
|
404
|
+
"clock_domain_id": f"process:{os.getpid()}:monotonic",
|
|
405
|
+
"durable": True,
|
|
406
|
+
"trading_blocked": False,
|
|
407
|
+
"evidence_complete": True,
|
|
408
|
+
}
|
|
409
|
+
payload = {
|
|
410
|
+
**snapshot,
|
|
411
|
+
"schema_version": 2,
|
|
412
|
+
"session_state": session_state,
|
|
413
|
+
"broker": "MixBroker",
|
|
414
|
+
"baseline_equity": str(snapshot["baseline_equity"]),
|
|
415
|
+
"current_equity": str(snapshot["current_equity"]),
|
|
416
|
+
"realized_net": str(snapshot["realized_net"]),
|
|
417
|
+
}
|
|
418
|
+
self._atomic_write_account_risk(payload)
|
|
419
|
+
self._account_risk_snapshot = snapshot
|
|
420
|
+
self._account_risk_last_persist_ns = now_ns
|
|
421
|
+
return True
|
|
422
|
+
except Exception as exc:
|
|
423
|
+
logger.warning(
|
|
424
|
+
"MixBroker account-risk ledger persistence failed: %s",
|
|
425
|
+
type(exc).__name__,
|
|
426
|
+
exc_info=True,
|
|
427
|
+
)
|
|
428
|
+
self._account_risk_failed = True
|
|
429
|
+
self._account_risk_snapshot = {
|
|
430
|
+
**self._unavailable_account_risk_snapshot("account_risk_ledger_persist_failed"),
|
|
431
|
+
"configured_venues": self._configured_risk_venues(),
|
|
432
|
+
}
|
|
433
|
+
self._release_account_risk_ledger()
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
def _release_account_risk_ledger(self):
|
|
437
|
+
handle = self._account_risk_lock_handle
|
|
438
|
+
self._account_risk_lock_handle = None
|
|
439
|
+
lock_acquired = self._account_risk_lock_acquired
|
|
440
|
+
self._account_risk_lock_acquired = False
|
|
441
|
+
if handle is None:
|
|
442
|
+
return
|
|
443
|
+
try:
|
|
444
|
+
if lock_acquired:
|
|
445
|
+
_release_file_lock(handle)
|
|
446
|
+
finally:
|
|
447
|
+
handle.close()
|
|
448
|
+
|
|
449
|
+
def get_account_risk_snapshot(self):
|
|
450
|
+
"""Return a copy of the last atomically persisted paper-risk state."""
|
|
451
|
+
self._persist_account_risk_snapshot(force=False)
|
|
452
|
+
return copy.deepcopy(self._account_risk_snapshot)
|
|
453
|
+
|
|
454
|
+
def stop(self):
|
|
455
|
+
"""Seal the durable paper epoch and release its single-writer lease."""
|
|
456
|
+
flat = not any(order.alive() for order in self.pending_orders) and not any(
|
|
457
|
+
abs(float(position.size or 0.0)) > 1e-12
|
|
458
|
+
for positions in (self.positions, self.long_positions, self.short_positions)
|
|
459
|
+
for position in positions.values()
|
|
460
|
+
)
|
|
461
|
+
try:
|
|
462
|
+
self._persist_account_risk_snapshot(
|
|
463
|
+
force=True,
|
|
464
|
+
session_state="closed" if flat else "unsafe_open_exposure",
|
|
465
|
+
)
|
|
466
|
+
finally:
|
|
467
|
+
self._release_account_risk_ledger()
|
|
468
|
+
return super().stop()
|
|
469
|
+
|
|
470
|
+
def _reset_midfreq_state(self):
|
|
471
|
+
"""(Re)create the per-symbol windows, history buffers and context.
|
|
472
|
+
|
|
473
|
+
Allocates ``_ob_window`` and ``_completed_bars`` as defaultdicts
|
|
474
|
+
of bounded deques sized by ``max_ob_window`` and
|
|
475
|
+
``max_bar_history`` respectively, plus per-symbol indicator
|
|
476
|
+
dicts and the :class:`MidFreqContext` used by strategies.
|
|
477
|
+
"""
|
|
478
|
+
self._ob_window = collections.defaultdict(
|
|
479
|
+
lambda: collections.deque(maxlen=self.get_param("max_ob_window"))
|
|
480
|
+
)
|
|
481
|
+
self._completed_bars = collections.defaultdict(
|
|
482
|
+
lambda: collections.deque(maxlen=self.get_param("max_bar_history"))
|
|
483
|
+
)
|
|
484
|
+
self._bar_indicators: dict = collections.defaultdict(dict)
|
|
485
|
+
self._bar_indicator_state: dict = collections.defaultdict(dict)
|
|
486
|
+
self._context = MidFreqContext(self)
|
|
487
|
+
|
|
488
|
+
def process_tick(self, tick_event, data=None):
|
|
489
|
+
"""Forward the tick to :class:`TickBroker` for execution."""
|
|
490
|
+
super().process_tick(tick_event, data)
|
|
491
|
+
self._persist_account_risk_snapshot(force=False)
|
|
492
|
+
|
|
493
|
+
def process_orderbook(self, ob_event, data=None):
|
|
494
|
+
"""Forward the order-book update and append it to the per-symbol window.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
ob_event: The order book event to record. Its ``symbol``
|
|
498
|
+
attribute selects the per-symbol window.
|
|
499
|
+
data: Optional feed reference forwarded to the base class.
|
|
500
|
+
"""
|
|
501
|
+
super().process_orderbook(ob_event, data)
|
|
502
|
+
self._ob_window[ob_event.symbol].append(copy.deepcopy(ob_event))
|
|
503
|
+
self._persist_account_risk_snapshot(force=False)
|
|
504
|
+
|
|
505
|
+
def process_bar(self, bar_event, data=None):
|
|
506
|
+
"""Record the completed bar and refresh its rolling indicators.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
bar_event: The completed bar to record. Its ``symbol`` and
|
|
510
|
+
``close`` attributes are used for window insertion and
|
|
511
|
+
indicator maintenance.
|
|
512
|
+
data: Optional feed reference (unused here, kept for
|
|
513
|
+
signature parity with the base class).
|
|
514
|
+
"""
|
|
515
|
+
symbol = bar_event.symbol
|
|
516
|
+
self._completed_bars[symbol].append(copy.deepcopy(bar_event))
|
|
517
|
+
self._update_bar_indicators(symbol)
|
|
518
|
+
self._persist_account_risk_snapshot(force=False)
|
|
519
|
+
|
|
520
|
+
def _record_account_risk_fills(self, history_start):
|
|
521
|
+
if self._account_risk_lock_handle is None or self._account_risk_failed:
|
|
522
|
+
return
|
|
523
|
+
try:
|
|
524
|
+
for row in self._order_history[history_start:]:
|
|
525
|
+
pnl = self._decimal(row.get("pnl", 0), "realized_pnl")
|
|
526
|
+
commission = self._decimal(row.get("commission", 0), "commission")
|
|
527
|
+
self._account_risk_realized_net += pnl - commission
|
|
528
|
+
if len(self._order_history) > history_start:
|
|
529
|
+
self._persist_account_risk_snapshot(force=True)
|
|
530
|
+
except Exception:
|
|
531
|
+
logger.warning("mixbroker:443 fallback on Exception")
|
|
532
|
+
self._account_risk_failed = True
|
|
533
|
+
self._account_risk_snapshot = self._unavailable_account_risk_snapshot(
|
|
534
|
+
"account_risk_fill_accounting_failed"
|
|
535
|
+
)
|
|
536
|
+
self._release_account_risk_ledger()
|
|
537
|
+
|
|
538
|
+
def _execute(self, order, fill_price, fill_size, event, source="tick"):
|
|
539
|
+
history_start = len(self._order_history)
|
|
540
|
+
result = super()._execute(order, fill_price, fill_size, event, source=source)
|
|
541
|
+
if not self._is_dual_side_mode():
|
|
542
|
+
self._record_account_risk_fills(history_start)
|
|
543
|
+
return result
|
|
544
|
+
|
|
545
|
+
def _execute_dual_side(self, order, fill_price, fill_size, event, source="tick"):
|
|
546
|
+
history_start = len(self._order_history)
|
|
547
|
+
result = super()._execute_dual_side(order, fill_price, fill_size, event, source=source)
|
|
548
|
+
self._record_account_risk_fills(history_start)
|
|
549
|
+
return result
|
|
550
|
+
|
|
551
|
+
def _update_bar_indicators(self, symbol):
|
|
552
|
+
"""Incrementally maintain the SMA indicator for ``symbol``.
|
|
553
|
+
|
|
554
|
+
Maintains a running sum of the last ``default_sma_period``
|
|
555
|
+
closing prices per symbol, with a slide-in / slide-out rule
|
|
556
|
+
based on ``bar_count``. Once the buffer is full the SMA is
|
|
557
|
+
written to ``_bar_indicators[symbol][f"sma_{period}"]``; if
|
|
558
|
+
not, the indicator is removed (if present) so callers see
|
|
559
|
+
``None`` until the buffer is warm.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
symbol: The per-symbol indicator key to update.
|
|
563
|
+
"""
|
|
564
|
+
bars = self._completed_bars.get(symbol)
|
|
565
|
+
if not bars:
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
indicators = self._bar_indicators[symbol]
|
|
569
|
+
indicator_state = self._bar_indicator_state[symbol]
|
|
570
|
+
sma_period = int(self.get_param("default_sma_period"))
|
|
571
|
+
if sma_period <= 0:
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
previous_sum = float(indicator_state.get("sma_sum", 0.0))
|
|
575
|
+
previous_len = int(indicator_state.get("bar_count", 0))
|
|
576
|
+
current_bar = bars[-1]
|
|
577
|
+
rolling_sum = previous_sum + float(current_bar.close)
|
|
578
|
+
if previous_len >= sma_period and len(bars) > sma_period:
|
|
579
|
+
rolling_sum -= float(bars[-(sma_period + 1)].close)
|
|
580
|
+
elif len(bars) <= sma_period:
|
|
581
|
+
rolling_sum = sum(float(bar.close) for bar in bars)
|
|
582
|
+
|
|
583
|
+
indicator_state["bar_count"] = len(bars)
|
|
584
|
+
indicator_state["sma_sum"] = rolling_sum
|
|
585
|
+
|
|
586
|
+
if len(bars) >= sma_period:
|
|
587
|
+
indicators[f"sma_{sma_period}"] = rolling_sum / float(sma_period)
|
|
588
|
+
else:
|
|
589
|
+
indicators.pop(f"sma_{sma_period}", None)
|
|
590
|
+
|
|
591
|
+
def get_context(self):
|
|
592
|
+
"""Return the singleton :class:`MidFreqContext` for strategy queries."""
|
|
593
|
+
return self._context
|
|
594
|
+
|
|
595
|
+
def get_ob_window(self, symbol, n=30):
|
|
596
|
+
"""Return a deep-copied list of the last ``n`` order book snapshots.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
symbol: Symbol whose window to return.
|
|
600
|
+
n: Maximum number of snapshots to return. ``None`` returns
|
|
601
|
+
the entire stored window. The tail is preferred when the
|
|
602
|
+
window is larger than ``n``.
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
list: Deep-copied order book snapshots (independent of the
|
|
606
|
+
broker's internal state).
|
|
607
|
+
"""
|
|
608
|
+
window = list(self._ob_window.get(symbol, ()))
|
|
609
|
+
if n is not None:
|
|
610
|
+
window = window[-n:]
|
|
611
|
+
return [copy.deepcopy(snapshot) for snapshot in window]
|
|
612
|
+
|
|
613
|
+
def get_completed_bars(self, symbol, n=20):
|
|
614
|
+
"""Return a deep-copied list of the last ``n`` completed bars.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
symbol: Symbol whose bars to return.
|
|
618
|
+
n: Maximum number of bars to return. ``None`` returns the
|
|
619
|
+
full buffer.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
list: Deep-copied bar events.
|
|
623
|
+
"""
|
|
624
|
+
bars = list(self._completed_bars.get(symbol, ()))
|
|
625
|
+
if n is not None:
|
|
626
|
+
bars = bars[-n:]
|
|
627
|
+
return [copy.deepcopy(bar) for bar in bars]
|
|
628
|
+
|
|
629
|
+
def get_bar_indicator(self, symbol, indicator_name):
|
|
630
|
+
"""Return the value of a maintained per-symbol bar indicator.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
symbol: Symbol the indicator is for.
|
|
634
|
+
indicator_name: Indicator key (e.g. ``"sma_20"``).
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
float | None: The indicator value, or ``None`` if the buffer
|
|
638
|
+
has not warmed up or the indicator is unknown.
|
|
639
|
+
"""
|
|
640
|
+
return self._bar_indicators.get(symbol, {}).get(indicator_name)
|
|
641
|
+
|
|
642
|
+
def get_symbol_snapshot(self, symbol):
|
|
643
|
+
"""Return a deep-copied snapshot dict for ``symbol``.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
symbol: Symbol to snapshot.
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
dict: Snapshot produced by :meth:`MidFreqContext.snapshot`.
|
|
650
|
+
"""
|
|
651
|
+
return self._context.snapshot(symbol)
|
|
652
|
+
|
|
653
|
+
def get_symbol_snapshots(self, symbols=None):
|
|
654
|
+
"""Return deep-copied snapshots for one, many or all symbols.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
symbols: Iterable of symbols to snapshot. ``None`` snapshots
|
|
658
|
+
every symbol currently known to the broker.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
dict: The structure produced by
|
|
662
|
+
:meth:`MidFreqContext.snapshot_all`.
|
|
663
|
+
"""
|
|
664
|
+
return self._context.snapshot_all(symbols=symbols)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class MidFreqContext:
|
|
668
|
+
"""Read-only strategy-facing view over a :class:`MixBroker`.
|
|
669
|
+
|
|
670
|
+
Exposes a curated set of per-symbol and per-account helpers
|
|
671
|
+
(last tick, last order book, completed bars, simple indicators,
|
|
672
|
+
position, portfolio value, full snapshots) without leaking the
|
|
673
|
+
broker's internal mutable state. Returned values are always
|
|
674
|
+
deep-copied so callers cannot accidentally mutate broker state.
|
|
675
|
+
"""
|
|
676
|
+
|
|
677
|
+
def __init__(self, broker):
|
|
678
|
+
"""Bind the context to its owning broker.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
broker: The :class:`MixBroker` instance this context will
|
|
682
|
+
query.
|
|
683
|
+
"""
|
|
684
|
+
self._broker = broker
|
|
685
|
+
|
|
686
|
+
def get_last_tick(self, symbol):
|
|
687
|
+
"""Return a deep-copied last-tick event for ``symbol``.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
symbol: Symbol whose last tick to return.
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
object | None: Deep-copied tick event, or ``None`` if no
|
|
694
|
+
tick has been seen for ``symbol`` yet.
|
|
695
|
+
"""
|
|
696
|
+
tick = self._broker._last_tick.get(symbol)
|
|
697
|
+
return copy.deepcopy(tick) if tick is not None else None
|
|
698
|
+
|
|
699
|
+
def get_last_orderbook(self, symbol):
|
|
700
|
+
"""Return a deep-copied last order-book event for ``symbol``.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
symbol: Symbol whose last order book to return.
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
object | None: Deep-copied order book, or ``None`` if no
|
|
707
|
+
order book update has been seen for ``symbol`` yet.
|
|
708
|
+
"""
|
|
709
|
+
orderbook = self._broker._last_orderbook.get(symbol)
|
|
710
|
+
return copy.deepcopy(orderbook) if orderbook is not None else None
|
|
711
|
+
|
|
712
|
+
def get_last_price(self, symbol):
|
|
713
|
+
"""Return the price of the last tick for ``symbol`` (or ``None``).
|
|
714
|
+
|
|
715
|
+
This is a convenience that avoids forcing callers to fetch the
|
|
716
|
+
full tick event just to read ``tick.price``.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
symbol: Symbol whose last-tick price to return.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
float | None: Last price, or ``None`` if no tick has been
|
|
723
|
+
received or the stored tick lacks a ``price`` attribute.
|
|
724
|
+
"""
|
|
725
|
+
tick = self._broker._last_tick.get(symbol)
|
|
726
|
+
return getattr(tick, "price", None)
|
|
727
|
+
|
|
728
|
+
def get_ob_window(self, symbol, n=30):
|
|
729
|
+
"""Delegate to :meth:`MixBroker.get_ob_window` (alias for symmetry)."""
|
|
730
|
+
return self._broker.get_ob_window(symbol, n)
|
|
731
|
+
|
|
732
|
+
def get_ob_ratio(self, symbol, levels=10, window=30):
|
|
733
|
+
"""Compute the bid/ask notional ratio over the recent book window.
|
|
734
|
+
|
|
735
|
+
For each of the last ``window`` order book snapshots, sum the
|
|
736
|
+
``price * qty`` product of the first ``levels`` bid and ask
|
|
737
|
+
entries. Returns ``bid_total / ask_total`` or ``None`` if there
|
|
738
|
+
are no snapshots or the ask total is effectively zero.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
symbol: Symbol to compute the ratio for.
|
|
742
|
+
levels: Maximum depth level (per side) to include.
|
|
743
|
+
window: Number of recent snapshots to fold over.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
float | None: Bid/ask notional ratio, or ``None`` if the
|
|
747
|
+
denominator is too small to be meaningful.
|
|
748
|
+
"""
|
|
749
|
+
snapshots = self._broker.get_ob_window(symbol, window)
|
|
750
|
+
if not snapshots:
|
|
751
|
+
return None
|
|
752
|
+
|
|
753
|
+
total_bid_amount = 0.0
|
|
754
|
+
total_ask_amount = 0.0
|
|
755
|
+
for snapshot in snapshots:
|
|
756
|
+
for level, (price, qty) in enumerate(snapshot.bids or []):
|
|
757
|
+
if level >= levels:
|
|
758
|
+
break
|
|
759
|
+
total_bid_amount += float(price) * float(qty)
|
|
760
|
+
for level, (price, qty) in enumerate(snapshot.asks or []):
|
|
761
|
+
if level >= levels:
|
|
762
|
+
break
|
|
763
|
+
total_ask_amount += float(price) * float(qty)
|
|
764
|
+
|
|
765
|
+
if total_ask_amount < 1e-12:
|
|
766
|
+
return None
|
|
767
|
+
return total_bid_amount / total_ask_amount
|
|
768
|
+
|
|
769
|
+
def get_completed_bars(self, symbol, n=20):
|
|
770
|
+
"""Delegate to :meth:`MixBroker.get_completed_bars` (alias)."""
|
|
771
|
+
return self._broker.get_completed_bars(symbol, n)
|
|
772
|
+
|
|
773
|
+
def get_sma(self, symbol, period=20):
|
|
774
|
+
"""Return the simple moving average of the last ``period`` closes.
|
|
775
|
+
|
|
776
|
+
Uses the maintained ``sma_{period}`` indicator when ``period ==
|
|
777
|
+
20`` (the default and most common case) and falls back to
|
|
778
|
+
computing a fresh average from the completed bars otherwise.
|
|
779
|
+
Returns ``None`` when fewer than ``period`` bars are available.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
symbol: Symbol to compute the SMA for.
|
|
783
|
+
period: Lookback period in completed bars.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
float | None: Simple moving average, or ``None`` if the
|
|
787
|
+
buffer is not yet warm.
|
|
788
|
+
"""
|
|
789
|
+
if period == 20:
|
|
790
|
+
return self._broker.get_bar_indicator(symbol, "sma_20")
|
|
791
|
+
|
|
792
|
+
bars = self._broker.get_completed_bars(symbol, period)
|
|
793
|
+
if len(bars) < period:
|
|
794
|
+
return None
|
|
795
|
+
return sum(bar.close for bar in bars) / float(period)
|
|
796
|
+
|
|
797
|
+
def get_last_bar(self, symbol):
|
|
798
|
+
"""Return the most recent completed bar for ``symbol``.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
symbol: Symbol whose last bar to return.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
object | None: Deep-copied bar, or ``None`` if no bar has
|
|
805
|
+
completed for ``symbol`` yet.
|
|
806
|
+
"""
|
|
807
|
+
bars = self._broker.get_completed_bars(symbol, 1)
|
|
808
|
+
return bars[0] if bars else None
|
|
809
|
+
|
|
810
|
+
def get_cash(self):
|
|
811
|
+
"""Return the broker's current available cash."""
|
|
812
|
+
return self._broker.getcash()
|
|
813
|
+
|
|
814
|
+
def get_position(self, symbol):
|
|
815
|
+
"""Return a deep-copied position for ``symbol``.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
symbol: Symbol whose position to fetch.
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
object | None: Deep-copied position, or ``None`` if no
|
|
822
|
+
position is currently held for ``symbol``.
|
|
823
|
+
"""
|
|
824
|
+
position = self._broker._positions.get(symbol)
|
|
825
|
+
return copy.deepcopy(position) if position is not None else None
|
|
826
|
+
|
|
827
|
+
def get_portfolio_value(self):
|
|
828
|
+
"""Return the broker's total portfolio value."""
|
|
829
|
+
return self._broker.getvalue()
|
|
830
|
+
|
|
831
|
+
def get_symbols(self):
|
|
832
|
+
"""Return the sorted union of every symbol known to the broker.
|
|
833
|
+
|
|
834
|
+
The set is built from the keys of the last-tick, last-orderbook
|
|
835
|
+
and completed-bars buffers, plus the symbols of all open
|
|
836
|
+
positions. The result is sorted alphabetically for deterministic
|
|
837
|
+
iteration.
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
list[str]: Sorted list of known symbol identifiers.
|
|
841
|
+
"""
|
|
842
|
+
symbols = set()
|
|
843
|
+
symbols.update(self._broker._last_tick.keys())
|
|
844
|
+
symbols.update(self._broker._last_orderbook.keys())
|
|
845
|
+
symbols.update(self._broker._completed_bars.keys())
|
|
846
|
+
symbols.update(
|
|
847
|
+
symbol
|
|
848
|
+
for symbol, position in self._broker._positions.items()
|
|
849
|
+
if getattr(position, "size", 0)
|
|
850
|
+
)
|
|
851
|
+
return sorted(symbols)
|
|
852
|
+
|
|
853
|
+
def snapshot(self, symbol):
|
|
854
|
+
"""Return a flat per-symbol snapshot dict.
|
|
855
|
+
|
|
856
|
+
The returned dict includes the symbol identifier, the last tick
|
|
857
|
+
and order book (deep-copied), the last completed bar, the
|
|
858
|
+
``sma_20`` indicator, the bid/ask ratio, and the current
|
|
859
|
+
position. All values are deep-copied so the caller cannot
|
|
860
|
+
mutate the broker's state.
|
|
861
|
+
|
|
862
|
+
Args:
|
|
863
|
+
symbol: Symbol to snapshot.
|
|
864
|
+
|
|
865
|
+
Returns:
|
|
866
|
+
dict: Per-symbol snapshot as described above.
|
|
867
|
+
"""
|
|
868
|
+
return {
|
|
869
|
+
"symbol": symbol,
|
|
870
|
+
"last_tick": self.get_last_tick(symbol),
|
|
871
|
+
"last_orderbook": self.get_last_orderbook(symbol),
|
|
872
|
+
"last_bar": self.get_last_bar(symbol),
|
|
873
|
+
"sma_20": self.get_sma(symbol, 20),
|
|
874
|
+
"ob_ratio": self.get_ob_ratio(symbol),
|
|
875
|
+
"position": self.get_position(symbol),
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
def snapshot_all(self, symbols=None):
|
|
879
|
+
"""Return a full portfolio snapshot including cash and per-symbol state.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
symbols: Iterable of symbols to include. ``None`` includes
|
|
883
|
+
every symbol returned by :meth:`get_symbols`. The list is
|
|
884
|
+
deduped and sorted for deterministic output.
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
dict: ``{"cash": float, "portfolio_value": float,
|
|
888
|
+
"symbols": {symbol: snapshot_dict}}``.
|
|
889
|
+
"""
|
|
890
|
+
selected_symbols = self.get_symbols() if symbols is None else sorted(set(symbols))
|
|
891
|
+
return {
|
|
892
|
+
"cash": self.get_cash(),
|
|
893
|
+
"portfolio_value": self.get_portfolio_value(),
|
|
894
|
+
"symbols": {symbol: self.snapshot(symbol) for symbol in selected_symbols},
|
|
895
|
+
}
|