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,993 @@
|
|
|
1
|
+
"""Explicit premium-style accounting for CTP options.
|
|
2
|
+
|
|
3
|
+
The CTP option contract is deliberately separate from the futures commission
|
|
4
|
+
schemes. A long option consumes premium, while a short option needs an
|
|
5
|
+
authoritative total-margin observation. This module only performs the
|
|
6
|
+
single-leg accounting projection; it does not reserve cash or maintain a live
|
|
7
|
+
account ledger.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
import datetime as _dt
|
|
14
|
+
import math
|
|
15
|
+
import re
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from ..comminfo import CommInfoBase
|
|
21
|
+
from ..parameters import ParameterDescriptor
|
|
22
|
+
from ..utils.log_message import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OptionAccountingError(ValueError):
|
|
28
|
+
"""Raised when option cost or evidence cannot be established safely."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, code: str, message: str | None = None):
|
|
31
|
+
self.code = str(code)
|
|
32
|
+
super().__init__(message or self.code)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class CtpOptionSellerMarginEvidence:
|
|
37
|
+
"""Typed form of one account-bound seller margin observation.
|
|
38
|
+
|
|
39
|
+
``source_kind='synthetic'`` is intentionally supported for offline
|
|
40
|
+
contract tests only. It is retained as provenance and is never inferred
|
|
41
|
+
from the futures ``FixedMargin``/``MiniMargin``/``Royalty`` fields.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
account_fingerprint: str
|
|
45
|
+
trading_day: str
|
|
46
|
+
connection_generation: int
|
|
47
|
+
instrument_id: str
|
|
48
|
+
exchange_id: str
|
|
49
|
+
hedge_flag: str
|
|
50
|
+
currency: str
|
|
51
|
+
price_basis: Any
|
|
52
|
+
expiry: Any
|
|
53
|
+
source_hash: str
|
|
54
|
+
expires_at_utc: Any
|
|
55
|
+
total_margin: float
|
|
56
|
+
quantity: float = 1.0
|
|
57
|
+
source_kind: str = "sdk"
|
|
58
|
+
|
|
59
|
+
def as_mapping(self) -> dict[str, Any]:
|
|
60
|
+
"""Return a plain mapping suitable for validation and serialization."""
|
|
61
|
+
return {
|
|
62
|
+
"account_fingerprint": self.account_fingerprint,
|
|
63
|
+
"trading_day": self.trading_day,
|
|
64
|
+
"connection_generation": self.connection_generation,
|
|
65
|
+
"instrument_id": self.instrument_id,
|
|
66
|
+
"exchange_id": self.exchange_id,
|
|
67
|
+
"hedge_flag": self.hedge_flag,
|
|
68
|
+
"currency": self.currency,
|
|
69
|
+
"price_basis": self.price_basis,
|
|
70
|
+
"expiry": self.expiry,
|
|
71
|
+
"source_hash": self.source_hash,
|
|
72
|
+
"expires_at_utc": self.expires_at_utc,
|
|
73
|
+
"total_margin": self.total_margin,
|
|
74
|
+
"quantity": self.quantity,
|
|
75
|
+
"source_kind": self.source_kind,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _lookup(mapping: Mapping[str, Any], *keys: str) -> Any:
|
|
80
|
+
if not isinstance(mapping, Mapping):
|
|
81
|
+
return None
|
|
82
|
+
for key in keys:
|
|
83
|
+
value = mapping.get(key)
|
|
84
|
+
if value not in (None, ""):
|
|
85
|
+
return value
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _alias_value(
|
|
90
|
+
mapping: Mapping[str, Any] | None,
|
|
91
|
+
keys: tuple[str, ...],
|
|
92
|
+
code: str,
|
|
93
|
+
) -> Any:
|
|
94
|
+
"""Read one explicit field and reject contradictory aliases."""
|
|
95
|
+
if not isinstance(mapping, Mapping):
|
|
96
|
+
return None
|
|
97
|
+
values = [(key, mapping[key]) for key in keys if mapping.get(key) not in (None, "")]
|
|
98
|
+
if not values:
|
|
99
|
+
return None
|
|
100
|
+
first = values[0][1]
|
|
101
|
+
for key, value in values[1:]:
|
|
102
|
+
if value != first:
|
|
103
|
+
raise OptionAccountingError(
|
|
104
|
+
code,
|
|
105
|
+
f"{code}: conflicting aliases {values[0][0]!r} and {key!r}",
|
|
106
|
+
)
|
|
107
|
+
return first
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _finite_number(value: Any, code: str, *, positive: bool = False) -> float:
|
|
111
|
+
if isinstance(value, bool):
|
|
112
|
+
raise OptionAccountingError(code, f"{code}: boolean is not a numeric value")
|
|
113
|
+
try:
|
|
114
|
+
number = float(value)
|
|
115
|
+
except (TypeError, ValueError) as exc:
|
|
116
|
+
logger.error("ctpoption:115 re-raising TypeError,ValueError", exc_info=True)
|
|
117
|
+
raise OptionAccountingError(code, f"{code}: expected a finite number") from exc
|
|
118
|
+
if not math.isfinite(number) or (positive and number <= 0.0):
|
|
119
|
+
raise OptionAccountingError(code, f"{code}: expected a positive finite number")
|
|
120
|
+
return number
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _as_utc(value: Any, code: str) -> _dt.datetime:
|
|
124
|
+
if isinstance(value, _dt.datetime):
|
|
125
|
+
parsed = value
|
|
126
|
+
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
127
|
+
try:
|
|
128
|
+
parsed = _dt.datetime.fromtimestamp(float(value), tz=_dt.timezone.utc)
|
|
129
|
+
except (OverflowError, OSError, ValueError) as exc:
|
|
130
|
+
logger.error("ctpoption:128 re-raising OverflowError,OSError,ValueError", exc_info=True)
|
|
131
|
+
raise OptionAccountingError(code, f"{code}: invalid epoch timestamp") from exc
|
|
132
|
+
elif isinstance(value, str):
|
|
133
|
+
text = value.strip()
|
|
134
|
+
if not text:
|
|
135
|
+
raise OptionAccountingError(code, f"{code}: value is missing")
|
|
136
|
+
if text.endswith("Z"):
|
|
137
|
+
text = text[:-1] + "+00:00"
|
|
138
|
+
try:
|
|
139
|
+
parsed = _dt.datetime.fromisoformat(text)
|
|
140
|
+
except ValueError as exc:
|
|
141
|
+
logger.error("ctpoption:138 re-raising ValueError", exc_info=True)
|
|
142
|
+
raise OptionAccountingError(code, f"{code}: invalid ISO timestamp") from exc
|
|
143
|
+
else:
|
|
144
|
+
raise OptionAccountingError(code, f"{code}: unsupported timestamp type")
|
|
145
|
+
|
|
146
|
+
if parsed.tzinfo is None:
|
|
147
|
+
raise OptionAccountingError(
|
|
148
|
+
f"{code}_timezone_missing",
|
|
149
|
+
f"{code}_timezone_missing: timestamp must carry an explicit UTC offset",
|
|
150
|
+
)
|
|
151
|
+
return parsed.astimezone(_dt.timezone.utc)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _expiry_date(value: Any) -> _dt.date | None:
|
|
155
|
+
if isinstance(value, _dt.datetime):
|
|
156
|
+
return value.date()
|
|
157
|
+
if isinstance(value, _dt.date):
|
|
158
|
+
return value
|
|
159
|
+
text = str(value or "").strip()
|
|
160
|
+
if not text:
|
|
161
|
+
return None
|
|
162
|
+
formats = (
|
|
163
|
+
(r"\d{8}", "%Y%m%d"),
|
|
164
|
+
(r"\d{4}-\d{2}-\d{2}", "%Y-%m-%d"),
|
|
165
|
+
(r"\d{4}/\d{2}/\d{2}", "%Y/%m/%d"),
|
|
166
|
+
)
|
|
167
|
+
for pattern, fmt in formats:
|
|
168
|
+
if not re.fullmatch(pattern, text):
|
|
169
|
+
continue
|
|
170
|
+
try:
|
|
171
|
+
return _dt.datetime.strptime(text, fmt).date()
|
|
172
|
+
except ValueError:
|
|
173
|
+
logger.debug("ctpoption:169 ignored ValueError")
|
|
174
|
+
continue
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _scope_value(mapping: Mapping[str, Any], field: str) -> Any:
|
|
179
|
+
if not isinstance(mapping, Mapping):
|
|
180
|
+
return None
|
|
181
|
+
aliases = {
|
|
182
|
+
"account_fingerprint": ("account_fingerprint", "account_id", "account"),
|
|
183
|
+
"trading_day": ("trading_day", "trade_date", "TradingDay", "date"),
|
|
184
|
+
"connection_generation": (
|
|
185
|
+
"connection_generation",
|
|
186
|
+
"generation",
|
|
187
|
+
"connectionGeneration",
|
|
188
|
+
),
|
|
189
|
+
"instrument_id": ("instrument_id", "InstrumentID", "instrument", "symbol"),
|
|
190
|
+
"exchange_id": ("exchange_id", "ExchangeID", "exchange"),
|
|
191
|
+
"hedge_flag": ("hedge_flag", "HedgeFlag", "hedge", "hedge_mode"),
|
|
192
|
+
"currency": ("currency", "margin_currency", "settle_currency"),
|
|
193
|
+
"expiry": ("expiry", "option_expiry", "expiry_date", "ExpireDate"),
|
|
194
|
+
"source_hash": ("source_hash", "source_hash_sha256", "sourcehash"),
|
|
195
|
+
}
|
|
196
|
+
return _alias_value(mapping, aliases[field], f"seller_margin_{field}_alias_conflict")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _validity_value(mapping: Mapping[str, Any]) -> Any:
|
|
200
|
+
return _alias_value(
|
|
201
|
+
mapping,
|
|
202
|
+
(
|
|
203
|
+
"expires_at_utc",
|
|
204
|
+
"valid_until_utc",
|
|
205
|
+
"valid_until",
|
|
206
|
+
"expires_at",
|
|
207
|
+
"expiry_timestamp",
|
|
208
|
+
),
|
|
209
|
+
"seller_margin_validity_alias_conflict",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _normalise_scope_value(field: str, value: Any) -> Any:
|
|
214
|
+
if field == "connection_generation":
|
|
215
|
+
try:
|
|
216
|
+
return int(value)
|
|
217
|
+
except (TypeError, ValueError):
|
|
218
|
+
return str(value).strip()
|
|
219
|
+
if field in {"account_fingerprint", "instrument_id", "exchange_id", "hedge_flag", "currency"}:
|
|
220
|
+
return str(value).strip()
|
|
221
|
+
if field == "expiry":
|
|
222
|
+
parsed = _expiry_date(value)
|
|
223
|
+
if parsed is not None:
|
|
224
|
+
return parsed.isoformat()
|
|
225
|
+
return str(value).strip()
|
|
226
|
+
if field == "source_hash":
|
|
227
|
+
return str(value).strip().lower()
|
|
228
|
+
if field == "trading_day":
|
|
229
|
+
parsed = _expiry_date(value)
|
|
230
|
+
if parsed is not None:
|
|
231
|
+
return parsed.isoformat()
|
|
232
|
+
return str(value).strip()
|
|
233
|
+
return value
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _source_hash(value: Any, code: str) -> str:
|
|
237
|
+
text = str(value or "").strip()
|
|
238
|
+
if not re.fullmatch(r"[0-9a-fA-F]{64}", text):
|
|
239
|
+
raise OptionAccountingError(
|
|
240
|
+
code,
|
|
241
|
+
f"{code}: expected a 64-character hexadecimal source hash",
|
|
242
|
+
)
|
|
243
|
+
return text.lower()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _finite_result(value: Any, code: str) -> float:
|
|
247
|
+
try:
|
|
248
|
+
number = float(value)
|
|
249
|
+
except (TypeError, ValueError) as exc:
|
|
250
|
+
logger.error("ctpoption:245 re-raising TypeError,ValueError", exc_info=True)
|
|
251
|
+
raise OptionAccountingError(code, f"{code}: result is not numeric") from exc
|
|
252
|
+
if not math.isfinite(number):
|
|
253
|
+
raise OptionAccountingError(code, f"{code}: result is not finite")
|
|
254
|
+
return number
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _validate_price_basis(value: Any) -> Any:
|
|
258
|
+
if isinstance(value, Mapping):
|
|
259
|
+
basis = dict(value)
|
|
260
|
+
option_price = _alias_value(
|
|
261
|
+
basis,
|
|
262
|
+
("option_price", "premium_price", "input_price"),
|
|
263
|
+
"seller_margin_option_price_alias_conflict",
|
|
264
|
+
)
|
|
265
|
+
underlying_price = _alias_value(
|
|
266
|
+
basis,
|
|
267
|
+
("underlying_price", "futures_price", "underlying_mark_price"),
|
|
268
|
+
"seller_margin_underlying_price_alias_conflict",
|
|
269
|
+
)
|
|
270
|
+
option_price = _finite_number(
|
|
271
|
+
option_price,
|
|
272
|
+
"seller_margin_option_price_invalid",
|
|
273
|
+
positive=True,
|
|
274
|
+
)
|
|
275
|
+
underlying_price = _finite_number(
|
|
276
|
+
underlying_price,
|
|
277
|
+
"seller_margin_underlying_price_invalid",
|
|
278
|
+
positive=True,
|
|
279
|
+
)
|
|
280
|
+
basis_time = _alias_value(
|
|
281
|
+
basis,
|
|
282
|
+
("as_of_utc", "basis_time_utc", "timestamp_utc", "observed_at_utc"),
|
|
283
|
+
"seller_margin_price_basis_time_alias_conflict",
|
|
284
|
+
)
|
|
285
|
+
if basis_time in (None, ""):
|
|
286
|
+
raise OptionAccountingError(
|
|
287
|
+
"seller_margin_price_basis_time_missing",
|
|
288
|
+
"seller_margin_price_basis_time_missing: price basis time is required",
|
|
289
|
+
)
|
|
290
|
+
basis_source_hash = _alias_value(
|
|
291
|
+
basis,
|
|
292
|
+
("source_hash", "source_hash_sha256", "sourcehash"),
|
|
293
|
+
"seller_margin_price_basis_source_alias_conflict",
|
|
294
|
+
)
|
|
295
|
+
if basis_source_hash in (None, ""):
|
|
296
|
+
raise OptionAccountingError(
|
|
297
|
+
"seller_margin_price_basis_source_missing",
|
|
298
|
+
"seller_margin_price_basis_source_missing: price basis source is required",
|
|
299
|
+
)
|
|
300
|
+
normalized = dict(basis)
|
|
301
|
+
normalized["option_price"] = option_price
|
|
302
|
+
normalized["underlying_price"] = underlying_price
|
|
303
|
+
normalized["as_of_utc"] = _as_utc(basis_time, "seller_margin_price_basis_time_invalid")
|
|
304
|
+
normalized["source_hash"] = _source_hash(
|
|
305
|
+
basis_source_hash, "seller_margin_price_basis_source_invalid"
|
|
306
|
+
)
|
|
307
|
+
return normalized
|
|
308
|
+
|
|
309
|
+
raise OptionAccountingError(
|
|
310
|
+
"seller_margin_price_basis_invalid",
|
|
311
|
+
"seller_margin_price_basis_invalid: option and underlying prices are required",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def validate_seller_margin_evidence(
|
|
316
|
+
evidence: Mapping[str, Any] | CtpOptionSellerMarginEvidence | None,
|
|
317
|
+
*,
|
|
318
|
+
expected_scope: Mapping[str, Any] | None = None,
|
|
319
|
+
now: _dt.datetime | None = None,
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
"""Validate and normalize an account-bound seller margin observation."""
|
|
322
|
+
if isinstance(evidence, CtpOptionSellerMarginEvidence):
|
|
323
|
+
evidence = evidence.as_mapping()
|
|
324
|
+
if not isinstance(evidence, Mapping):
|
|
325
|
+
raise OptionAccountingError(
|
|
326
|
+
"seller_margin_evidence_missing",
|
|
327
|
+
"seller_margin_evidence_missing: explicit total-margin evidence is required",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
required = (
|
|
331
|
+
"account_fingerprint",
|
|
332
|
+
"trading_day",
|
|
333
|
+
"connection_generation",
|
|
334
|
+
"instrument_id",
|
|
335
|
+
"exchange_id",
|
|
336
|
+
"hedge_flag",
|
|
337
|
+
"currency",
|
|
338
|
+
"expiry",
|
|
339
|
+
"source_hash",
|
|
340
|
+
"price_basis",
|
|
341
|
+
"expires_at_utc",
|
|
342
|
+
"total_margin",
|
|
343
|
+
)
|
|
344
|
+
normalized: dict[str, Any] = {}
|
|
345
|
+
for field in required:
|
|
346
|
+
if field == "price_basis":
|
|
347
|
+
value = _alias_value(
|
|
348
|
+
evidence,
|
|
349
|
+
("price_basis", "pricebasis", "price_basis_evidence"),
|
|
350
|
+
"seller_margin_price_basis_alias_conflict",
|
|
351
|
+
)
|
|
352
|
+
elif field == "expires_at_utc":
|
|
353
|
+
value = _validity_value(evidence)
|
|
354
|
+
elif field == "total_margin":
|
|
355
|
+
value = _alias_value(
|
|
356
|
+
evidence,
|
|
357
|
+
("total_margin", "seller_total_margin", "TotalMargin"),
|
|
358
|
+
"seller_margin_total_alias_conflict",
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
value = _scope_value(evidence, field)
|
|
362
|
+
if value in (None, ""):
|
|
363
|
+
raise OptionAccountingError(
|
|
364
|
+
f"seller_margin_{field}_missing",
|
|
365
|
+
f"seller_margin_{field}_missing: seller evidence is incomplete",
|
|
366
|
+
)
|
|
367
|
+
normalized[field] = value
|
|
368
|
+
|
|
369
|
+
normalized["connection_generation"] = _finite_number(
|
|
370
|
+
normalized["connection_generation"], "seller_margin_generation_invalid", positive=True
|
|
371
|
+
)
|
|
372
|
+
if normalized["connection_generation"] != int(normalized["connection_generation"]):
|
|
373
|
+
raise OptionAccountingError(
|
|
374
|
+
"seller_margin_generation_invalid",
|
|
375
|
+
"seller_margin_generation_invalid: generation must be an integer",
|
|
376
|
+
)
|
|
377
|
+
normalized["connection_generation"] = int(normalized["connection_generation"])
|
|
378
|
+
normalized["price_basis"] = _validate_price_basis(normalized["price_basis"])
|
|
379
|
+
normalized["source_hash"] = _source_hash(
|
|
380
|
+
normalized["source_hash"], "seller_margin_source_hash_invalid"
|
|
381
|
+
)
|
|
382
|
+
normalized["total_margin"] = _finite_number(
|
|
383
|
+
normalized["total_margin"], "seller_margin_total_invalid", positive=True
|
|
384
|
+
)
|
|
385
|
+
quantity_value = _alias_value(
|
|
386
|
+
evidence,
|
|
387
|
+
("quantity", "qty", "volume"),
|
|
388
|
+
"seller_margin_quantity_alias_conflict",
|
|
389
|
+
)
|
|
390
|
+
if quantity_value in (None, ""):
|
|
391
|
+
raise OptionAccountingError(
|
|
392
|
+
"seller_margin_quantity_missing",
|
|
393
|
+
"seller_margin_quantity_missing: approved evidence quantity is required",
|
|
394
|
+
)
|
|
395
|
+
normalized["quantity"] = _finite_number(
|
|
396
|
+
quantity_value,
|
|
397
|
+
"seller_margin_quantity_invalid",
|
|
398
|
+
positive=True,
|
|
399
|
+
)
|
|
400
|
+
source_kind = _alias_value(
|
|
401
|
+
evidence,
|
|
402
|
+
("source_kind", "evidence_kind", "provenance", "source"),
|
|
403
|
+
"seller_margin_source_kind_alias_conflict",
|
|
404
|
+
)
|
|
405
|
+
normalized["source_kind"] = str(source_kind or "").strip().lower()
|
|
406
|
+
if not normalized["source_kind"]:
|
|
407
|
+
raise OptionAccountingError(
|
|
408
|
+
"seller_margin_source_kind_missing",
|
|
409
|
+
"seller_margin_source_kind_missing: provenance is required",
|
|
410
|
+
)
|
|
411
|
+
synthetic_sources = {"synthetic", "offline", "fixture", "test"}
|
|
412
|
+
sdk_sources = {
|
|
413
|
+
"sdk",
|
|
414
|
+
"sdk_public",
|
|
415
|
+
"sdk_query",
|
|
416
|
+
"ctp",
|
|
417
|
+
"ctp_sdk",
|
|
418
|
+
"ctp_direct",
|
|
419
|
+
"native",
|
|
420
|
+
"native_sdk",
|
|
421
|
+
"authoritative",
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
normalized["source_kind"] not in synthetic_sources
|
|
425
|
+
and normalized["source_kind"] not in sdk_sources
|
|
426
|
+
):
|
|
427
|
+
raise OptionAccountingError(
|
|
428
|
+
"seller_margin_source_kind_unknown",
|
|
429
|
+
"seller_margin_source_kind_unknown: evidence provenance is not recognized",
|
|
430
|
+
)
|
|
431
|
+
normalized["expires_at_utc"] = _as_utc(
|
|
432
|
+
normalized["expires_at_utc"], "seller_margin_expiry_invalid"
|
|
433
|
+
)
|
|
434
|
+
check_now = now or _dt.datetime.now(_dt.timezone.utc)
|
|
435
|
+
if check_now.tzinfo is None:
|
|
436
|
+
raise OptionAccountingError(
|
|
437
|
+
"seller_margin_clock_timezone_missing",
|
|
438
|
+
"seller_margin_clock_timezone_missing: validation clock needs an explicit UTC offset",
|
|
439
|
+
)
|
|
440
|
+
check_now = check_now.astimezone(_dt.timezone.utc)
|
|
441
|
+
basis_time = normalized["price_basis"]["as_of_utc"]
|
|
442
|
+
if basis_time > check_now:
|
|
443
|
+
raise OptionAccountingError(
|
|
444
|
+
"seller_margin_price_basis_future",
|
|
445
|
+
"seller_margin_price_basis_future: price basis is from the future",
|
|
446
|
+
)
|
|
447
|
+
if normalized["expires_at_utc"] <= basis_time:
|
|
448
|
+
raise OptionAccountingError(
|
|
449
|
+
"seller_margin_evidence_expiry_invalid",
|
|
450
|
+
"seller_margin_evidence_expiry_invalid: evidence expires before its price basis",
|
|
451
|
+
)
|
|
452
|
+
if normalized["price_basis"]["source_hash"] != normalized["source_hash"]:
|
|
453
|
+
raise OptionAccountingError(
|
|
454
|
+
"seller_margin_source_hash_mismatch",
|
|
455
|
+
"seller_margin_source_hash_mismatch: price basis and evidence source differ",
|
|
456
|
+
)
|
|
457
|
+
if normalized["expires_at_utc"] <= check_now:
|
|
458
|
+
raise OptionAccountingError(
|
|
459
|
+
"seller_margin_evidence_expired",
|
|
460
|
+
"seller_margin_evidence_expired: total-margin evidence is stale",
|
|
461
|
+
)
|
|
462
|
+
trading_day = _expiry_date(normalized["trading_day"])
|
|
463
|
+
if trading_day is None:
|
|
464
|
+
raise OptionAccountingError(
|
|
465
|
+
"seller_margin_trading_day_invalid",
|
|
466
|
+
"seller_margin_trading_day_invalid: trading day must be an explicit date",
|
|
467
|
+
)
|
|
468
|
+
normalized["trading_day"] = trading_day.isoformat()
|
|
469
|
+
expiry_date = _expiry_date(normalized["expiry"])
|
|
470
|
+
if expiry_date is None:
|
|
471
|
+
raise OptionAccountingError(
|
|
472
|
+
"seller_margin_expiry_invalid",
|
|
473
|
+
"seller_margin_expiry_invalid: option expiry must be an explicit date",
|
|
474
|
+
)
|
|
475
|
+
if expiry_date is not None and expiry_date < check_now.date():
|
|
476
|
+
raise OptionAccountingError(
|
|
477
|
+
"seller_margin_contract_expired",
|
|
478
|
+
"seller_margin_contract_expired: option contract has expired",
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
for field in required:
|
|
482
|
+
if field in {"price_basis", "expires_at_utc", "total_margin"}:
|
|
483
|
+
continue
|
|
484
|
+
expected = _scope_value(expected_scope, field) if expected_scope else None
|
|
485
|
+
if expected in (None, ""):
|
|
486
|
+
continue
|
|
487
|
+
actual = normalized[field]
|
|
488
|
+
if _normalise_scope_value(field, actual) != _normalise_scope_value(field, expected):
|
|
489
|
+
raise OptionAccountingError(
|
|
490
|
+
f"seller_margin_{field}_scope_mismatch",
|
|
491
|
+
f"seller_margin_{field}_scope_mismatch: evidence scope does not match order",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
expected_basis = (
|
|
495
|
+
_alias_value(
|
|
496
|
+
expected_scope,
|
|
497
|
+
("price_basis", "pricebasis", "price_basis_evidence"),
|
|
498
|
+
"seller_margin_expected_price_basis_alias_conflict",
|
|
499
|
+
)
|
|
500
|
+
if expected_scope
|
|
501
|
+
else None
|
|
502
|
+
)
|
|
503
|
+
if expected_basis not in (None, ""):
|
|
504
|
+
actual_basis = normalized["price_basis"]
|
|
505
|
+
expected_basis = _validate_price_basis(expected_basis)
|
|
506
|
+
if isinstance(actual_basis, Mapping) and isinstance(expected_basis, Mapping):
|
|
507
|
+
basis_keys = {
|
|
508
|
+
"option_price",
|
|
509
|
+
"premium_price",
|
|
510
|
+
"input_price",
|
|
511
|
+
"price",
|
|
512
|
+
"mark_price",
|
|
513
|
+
"underlying_price",
|
|
514
|
+
"futures_price",
|
|
515
|
+
"reference_price",
|
|
516
|
+
}
|
|
517
|
+
for key in basis_keys.intersection(expected_basis):
|
|
518
|
+
if key not in actual_basis:
|
|
519
|
+
raise OptionAccountingError(
|
|
520
|
+
"seller_margin_price_basis_scope_mismatch",
|
|
521
|
+
"seller_margin_price_basis_scope_mismatch: price basis differs",
|
|
522
|
+
)
|
|
523
|
+
try:
|
|
524
|
+
if float(actual_basis[key]) != float(expected_basis[key]):
|
|
525
|
+
raise OptionAccountingError(
|
|
526
|
+
"seller_margin_price_basis_scope_mismatch",
|
|
527
|
+
"seller_margin_price_basis_scope_mismatch: price basis differs",
|
|
528
|
+
)
|
|
529
|
+
except (TypeError, ValueError) as exc:
|
|
530
|
+
logger.error("ctpoption:524 re-raising TypeError,ValueError", exc_info=True)
|
|
531
|
+
raise OptionAccountingError(
|
|
532
|
+
"seller_margin_price_basis_scope_mismatch",
|
|
533
|
+
"seller_margin_price_basis_scope_mismatch: price basis differs",
|
|
534
|
+
) from exc
|
|
535
|
+
for key in ("as_of_utc", "source_hash"):
|
|
536
|
+
if actual_basis.get(key) != expected_basis.get(key):
|
|
537
|
+
raise OptionAccountingError(
|
|
538
|
+
"seller_margin_price_basis_scope_mismatch",
|
|
539
|
+
"seller_margin_price_basis_scope_mismatch: price basis differs",
|
|
540
|
+
)
|
|
541
|
+
elif actual_basis != expected_basis:
|
|
542
|
+
raise OptionAccountingError(
|
|
543
|
+
"seller_margin_price_basis_scope_mismatch",
|
|
544
|
+
"seller_margin_price_basis_scope_mismatch: price basis differs",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return normalized
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class CtpOptionPremium(CommInfoBase):
|
|
551
|
+
"""Commission and value rules for explicit premium-style CTP options."""
|
|
552
|
+
|
|
553
|
+
stocklike = ParameterDescriptor(default=True, type_=bool)
|
|
554
|
+
commtype = ParameterDescriptor(default=CommInfoBase.COMM_FIXED, type_=int)
|
|
555
|
+
percabs = ParameterDescriptor(default=True, type_=bool)
|
|
556
|
+
premium_style = ParameterDescriptor(default=None)
|
|
557
|
+
option_type = ParameterDescriptor(default=None)
|
|
558
|
+
open_commission_by_money = ParameterDescriptor(default=None)
|
|
559
|
+
open_commission_by_volume = ParameterDescriptor(default=None)
|
|
560
|
+
close_commission_by_money = ParameterDescriptor(default=None)
|
|
561
|
+
close_commission_by_volume = ParameterDescriptor(default=None)
|
|
562
|
+
close_today_commission_by_money = ParameterDescriptor(default=None)
|
|
563
|
+
close_today_commission_by_volume = ParameterDescriptor(default=None)
|
|
564
|
+
close_yesterday_commission_by_money = ParameterDescriptor(default=None)
|
|
565
|
+
close_yesterday_commission_by_volume = ParameterDescriptor(default=None)
|
|
566
|
+
seller_margin_evidence = ParameterDescriptor(default=None)
|
|
567
|
+
evidence_scope = ParameterDescriptor(default=None)
|
|
568
|
+
|
|
569
|
+
_FEE_ALIASES = {
|
|
570
|
+
"open_commission_by_money": (
|
|
571
|
+
"open_fee_rate",
|
|
572
|
+
"open_commission_rate",
|
|
573
|
+
"OpenRatioByMoney",
|
|
574
|
+
),
|
|
575
|
+
"open_commission_by_volume": (
|
|
576
|
+
"open_fee_amount",
|
|
577
|
+
"open_commission_amount",
|
|
578
|
+
"OpenRatioByVolume",
|
|
579
|
+
),
|
|
580
|
+
"close_commission_by_money": (
|
|
581
|
+
"close_fee_rate",
|
|
582
|
+
"close_commission_rate",
|
|
583
|
+
"CloseRatioByMoney",
|
|
584
|
+
),
|
|
585
|
+
"close_commission_by_volume": (
|
|
586
|
+
"close_fee_amount",
|
|
587
|
+
"close_commission_amount",
|
|
588
|
+
"CloseRatioByVolume",
|
|
589
|
+
),
|
|
590
|
+
"close_today_commission_by_money": (
|
|
591
|
+
"close_today_fee_rate",
|
|
592
|
+
"close_today_commission_rate",
|
|
593
|
+
"CloseTodayRatioByMoney",
|
|
594
|
+
),
|
|
595
|
+
"close_today_commission_by_volume": (
|
|
596
|
+
"close_today_fee_amount",
|
|
597
|
+
"close_today_commission_amount",
|
|
598
|
+
"CloseTodayRatioByVolume",
|
|
599
|
+
),
|
|
600
|
+
"close_yesterday_commission_by_money": (
|
|
601
|
+
"close_yesterday_fee_rate",
|
|
602
|
+
"close_yesterday_commission_rate",
|
|
603
|
+
"CloseYesterdayRatioByMoney",
|
|
604
|
+
),
|
|
605
|
+
"close_yesterday_commission_by_volume": (
|
|
606
|
+
"close_yesterday_fee_amount",
|
|
607
|
+
"close_yesterday_commission_amount",
|
|
608
|
+
"CloseYesterdayRatioByVolume",
|
|
609
|
+
),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
def __init__(self, **kwargs):
|
|
613
|
+
kwargs = dict(kwargs)
|
|
614
|
+
if "mult" in kwargs:
|
|
615
|
+
_finite_number(kwargs["mult"], "option_multiplier_invalid", positive=True)
|
|
616
|
+
for canonical, aliases in self._FEE_ALIASES.items():
|
|
617
|
+
values = [
|
|
618
|
+
(canonical, kwargs[canonical])
|
|
619
|
+
for _ in (0,)
|
|
620
|
+
if canonical in kwargs and kwargs[canonical] not in (None, "")
|
|
621
|
+
]
|
|
622
|
+
values.extend(
|
|
623
|
+
(alias, kwargs[alias])
|
|
624
|
+
for alias in aliases
|
|
625
|
+
if alias in kwargs and kwargs[alias] not in (None, "")
|
|
626
|
+
)
|
|
627
|
+
if values:
|
|
628
|
+
numbers = [
|
|
629
|
+
(
|
|
630
|
+
name,
|
|
631
|
+
_finite_number(value, "option_fee_alias_invalid"),
|
|
632
|
+
)
|
|
633
|
+
for name, value in values
|
|
634
|
+
]
|
|
635
|
+
first = numbers[0][1]
|
|
636
|
+
if any(number != first for _, number in numbers[1:]):
|
|
637
|
+
raise OptionAccountingError(
|
|
638
|
+
"option_fee_alias_conflict",
|
|
639
|
+
"option_fee_alias_conflict: contradictory fee aliases",
|
|
640
|
+
)
|
|
641
|
+
if canonical not in kwargs or kwargs[canonical] in (None, ""):
|
|
642
|
+
kwargs[canonical] = values[0][1]
|
|
643
|
+
|
|
644
|
+
super().__init__(**kwargs)
|
|
645
|
+
_finite_number(self.get_param("mult"), "option_multiplier_invalid", positive=True)
|
|
646
|
+
style = str(self.get_param("premium_style") or "").strip().lower()
|
|
647
|
+
if style not in {"premium", "premium_style", "premium-style"}:
|
|
648
|
+
raise OptionAccountingError(
|
|
649
|
+
"option_premium_style_required",
|
|
650
|
+
"option_premium_style_required: only explicit premium-style options are supported",
|
|
651
|
+
)
|
|
652
|
+
self._premium_style = "premium"
|
|
653
|
+
self._seller_margin_evidence = copy.deepcopy(self.get_param("seller_margin_evidence"))
|
|
654
|
+
self._evidence_scope = copy.deepcopy(self.get_param("evidence_scope"))
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def seller_margin_source_kind(self) -> str | None:
|
|
658
|
+
evidence = self._seller_margin_evidence
|
|
659
|
+
if isinstance(evidence, CtpOptionSellerMarginEvidence):
|
|
660
|
+
return evidence.source_kind.strip().lower()
|
|
661
|
+
if isinstance(evidence, Mapping):
|
|
662
|
+
return (
|
|
663
|
+
str(_lookup(evidence, "source_kind", "evidence_kind", "provenance", "source") or "")
|
|
664
|
+
.strip()
|
|
665
|
+
.lower()
|
|
666
|
+
or None
|
|
667
|
+
)
|
|
668
|
+
return None
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def seller_margin_is_synthetic(self) -> bool:
|
|
672
|
+
return self.seller_margin_source_kind in {"synthetic", "offline", "fixture", "test"}
|
|
673
|
+
|
|
674
|
+
def validate_seller_margin_evidence(self, *, now: _dt.datetime | None = None) -> dict[str, Any]:
|
|
675
|
+
return validate_seller_margin_evidence(
|
|
676
|
+
self._seller_margin_evidence,
|
|
677
|
+
expected_scope=self._evidence_scope,
|
|
678
|
+
now=now,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
def seller_margin_status(self) -> str:
|
|
682
|
+
if self._seller_margin_evidence is None:
|
|
683
|
+
return "BLOCKED_MISSING"
|
|
684
|
+
try:
|
|
685
|
+
evidence = self.validate_seller_margin_evidence()
|
|
686
|
+
except OptionAccountingError:
|
|
687
|
+
return "BLOCKED_INVALID"
|
|
688
|
+
if evidence["source_kind"] in {"synthetic", "offline", "fixture", "test"}:
|
|
689
|
+
return "SYNTHETIC_OFFLINE_ONLY"
|
|
690
|
+
return "STRUCTURALLY_VALID_UNVERIFIED"
|
|
691
|
+
|
|
692
|
+
def _fee_pair(self, role: str | None) -> tuple[float, float]:
|
|
693
|
+
role_text = str(role or "open").strip().lower().replace("-", "_")
|
|
694
|
+
if role_text in {"open", "opened"}:
|
|
695
|
+
prefix = "open"
|
|
696
|
+
elif role_text in {"close_today", "closetoday"}:
|
|
697
|
+
prefix = "close_today"
|
|
698
|
+
elif role_text in {"close_yesterday", "closeyesterday"}:
|
|
699
|
+
prefix = "close_yesterday"
|
|
700
|
+
elif role_text in {"close", "closed"}:
|
|
701
|
+
prefix = "close"
|
|
702
|
+
elif role_text in {"maker", "taker"}:
|
|
703
|
+
prefix = "open"
|
|
704
|
+
else:
|
|
705
|
+
raise OptionAccountingError(
|
|
706
|
+
"option_fee_role_unknown", f"option_fee_role_unknown: unsupported role {role!r}"
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
money = self.get_param(f"{prefix}_commission_by_money")
|
|
710
|
+
volume = self.get_param(f"{prefix}_commission_by_volume")
|
|
711
|
+
if prefix == "close_yesterday" and money is None and volume is None:
|
|
712
|
+
# Older CTP metadata has one close dimension. Inheriting a fully
|
|
713
|
+
# specified close pair is explicit and keeps that compatibility;
|
|
714
|
+
# a partially specified pair still fails closed.
|
|
715
|
+
money = self.get_param("close_commission_by_money")
|
|
716
|
+
volume = self.get_param("close_commission_by_volume")
|
|
717
|
+
if money is None or volume is None:
|
|
718
|
+
raise OptionAccountingError(
|
|
719
|
+
f"option_fee_{prefix}_incomplete",
|
|
720
|
+
f"option_fee_{prefix}_incomplete: ByMoney and ByVolume are both required",
|
|
721
|
+
)
|
|
722
|
+
money = _finite_number(money, f"option_fee_{prefix}_invalid")
|
|
723
|
+
volume = _finite_number(volume, f"option_fee_{prefix}_invalid")
|
|
724
|
+
if money < 0.0 or volume < 0.0:
|
|
725
|
+
raise OptionAccountingError(
|
|
726
|
+
f"option_fee_{prefix}_invalid",
|
|
727
|
+
f"option_fee_{prefix}_invalid: option fees cannot be negative",
|
|
728
|
+
)
|
|
729
|
+
return money, volume
|
|
730
|
+
|
|
731
|
+
def _option_price(self, price: Any) -> float:
|
|
732
|
+
return _finite_number(price, "option_price_invalid", positive=True)
|
|
733
|
+
|
|
734
|
+
def _valuation_price(self, price: Any) -> float:
|
|
735
|
+
value = _finite_number(price, "option_valuation_price_invalid")
|
|
736
|
+
if value < 0.0:
|
|
737
|
+
raise OptionAccountingError(
|
|
738
|
+
"option_valuation_price_invalid",
|
|
739
|
+
"option_valuation_price_invalid: valuation price cannot be negative",
|
|
740
|
+
)
|
|
741
|
+
return value
|
|
742
|
+
|
|
743
|
+
def _option_size(self, size: Any) -> float:
|
|
744
|
+
value = _finite_number(size, "option_size_invalid")
|
|
745
|
+
if value < 0.0:
|
|
746
|
+
value = abs(value)
|
|
747
|
+
return value
|
|
748
|
+
|
|
749
|
+
@staticmethod
|
|
750
|
+
def _is_buy_side(is_buy: Any = True, side: Any = None) -> bool:
|
|
751
|
+
value = side if side is not None else is_buy
|
|
752
|
+
if isinstance(value, str):
|
|
753
|
+
text = value.strip().lower()
|
|
754
|
+
if text in {"buy", "b", "long", "1", "true"}:
|
|
755
|
+
return True
|
|
756
|
+
if text in {"sell", "s", "short", "0", "false"}:
|
|
757
|
+
return False
|
|
758
|
+
raise OptionAccountingError("option_side_unknown", f"option_side_unknown: {value!r}")
|
|
759
|
+
return bool(value)
|
|
760
|
+
|
|
761
|
+
@staticmethod
|
|
762
|
+
def _role_text(role: Any) -> str:
|
|
763
|
+
return str(role or "open").strip().lower().replace("-", "_")
|
|
764
|
+
|
|
765
|
+
def _seller_margin_per_unit(self, price: Any = None, quantity: Any = None) -> float:
|
|
766
|
+
evidence = self.validate_seller_margin_evidence()
|
|
767
|
+
if evidence["source_kind"] not in {"synthetic", "offline", "fixture", "test"}:
|
|
768
|
+
raise OptionAccountingError(
|
|
769
|
+
"seller_margin_evidence_unverified",
|
|
770
|
+
"seller_margin_evidence_unverified: no trusted SDK total-margin issuer is available",
|
|
771
|
+
)
|
|
772
|
+
if quantity is not None and not math.isclose(
|
|
773
|
+
float(quantity), evidence["quantity"], rel_tol=0.0, abs_tol=1e-12
|
|
774
|
+
):
|
|
775
|
+
raise OptionAccountingError(
|
|
776
|
+
"seller_margin_quantity_unapproved",
|
|
777
|
+
"seller_margin_quantity_unapproved: evidence covers a different quantity",
|
|
778
|
+
)
|
|
779
|
+
if price is not None and isinstance(evidence["price_basis"], Mapping):
|
|
780
|
+
sourced_price = _lookup(
|
|
781
|
+
evidence["price_basis"],
|
|
782
|
+
"option_price",
|
|
783
|
+
"premium_price",
|
|
784
|
+
"input_price",
|
|
785
|
+
"price",
|
|
786
|
+
)
|
|
787
|
+
if sourced_price not in (None, ""):
|
|
788
|
+
sourced_price = _finite_number(
|
|
789
|
+
sourced_price,
|
|
790
|
+
"seller_margin_price_basis_invalid",
|
|
791
|
+
positive=True,
|
|
792
|
+
)
|
|
793
|
+
requested_price = self._option_price(price)
|
|
794
|
+
if not math.isclose(requested_price, sourced_price, rel_tol=0.0, abs_tol=1e-12):
|
|
795
|
+
raise OptionAccountingError(
|
|
796
|
+
"seller_margin_price_scope_mismatch",
|
|
797
|
+
"seller_margin_price_scope_mismatch: evidence price differs from order",
|
|
798
|
+
)
|
|
799
|
+
return _finite_result(
|
|
800
|
+
evidence["total_margin"] / evidence["quantity"],
|
|
801
|
+
"seller_margin_unit_nonfinite",
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def getoperationcost(self, size, price, is_buy=None, *, side=None, role="open"):
|
|
805
|
+
"""Return premium cost or an approved opening seller margin."""
|
|
806
|
+
quantity = self._option_size(size)
|
|
807
|
+
if quantity == 0.0:
|
|
808
|
+
return 0.0
|
|
809
|
+
price_value = self._option_price(price)
|
|
810
|
+
signed_size = _finite_number(size, "option_size_invalid")
|
|
811
|
+
if side is not None and is_buy is not None:
|
|
812
|
+
if self._is_buy_side(is_buy) != self._is_buy_side(side):
|
|
813
|
+
raise OptionAccountingError(
|
|
814
|
+
"option_side_conflict",
|
|
815
|
+
"option_side_conflict: is_buy and side disagree",
|
|
816
|
+
)
|
|
817
|
+
if is_buy is None and side is None:
|
|
818
|
+
is_buy = signed_size >= 0.0
|
|
819
|
+
elif signed_size < 0.0 and (
|
|
820
|
+
(side is None and self._is_buy_side(is_buy))
|
|
821
|
+
or (side is not None and self._is_buy_side(side))
|
|
822
|
+
):
|
|
823
|
+
raise OptionAccountingError(
|
|
824
|
+
"option_side_conflict",
|
|
825
|
+
"option_side_conflict: negative size cannot be an explicit buy",
|
|
826
|
+
)
|
|
827
|
+
is_buy = self._is_buy_side(is_buy, side)
|
|
828
|
+
role_text = self._role_text(role)
|
|
829
|
+
if role_text not in {
|
|
830
|
+
"open",
|
|
831
|
+
"opened",
|
|
832
|
+
"close",
|
|
833
|
+
"closed",
|
|
834
|
+
"close_today",
|
|
835
|
+
"closetoday",
|
|
836
|
+
"close_yesterday",
|
|
837
|
+
"closeyesterday",
|
|
838
|
+
}:
|
|
839
|
+
raise OptionAccountingError(
|
|
840
|
+
"option_accounting_role_unknown",
|
|
841
|
+
f"option_accounting_role_unknown: unsupported role {role!r}",
|
|
842
|
+
)
|
|
843
|
+
if role_text not in {"open", "opened"}:
|
|
844
|
+
return _finite_result(
|
|
845
|
+
quantity * price_value * self.get_param("mult"),
|
|
846
|
+
"option_cost_nonfinite",
|
|
847
|
+
)
|
|
848
|
+
if is_buy:
|
|
849
|
+
return _finite_result(
|
|
850
|
+
quantity * price_value * self.get_param("mult"),
|
|
851
|
+
"option_cost_nonfinite",
|
|
852
|
+
)
|
|
853
|
+
return _finite_result(
|
|
854
|
+
quantity * self._seller_margin_per_unit(price_value, quantity),
|
|
855
|
+
"option_cost_nonfinite",
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
def getpremiumvalue(self, size, price):
|
|
859
|
+
"""Return the premium transaction value for any execution side.
|
|
860
|
+
|
|
861
|
+
Opening short risk uses a separately sourced margin value in
|
|
862
|
+
:meth:`getoperationcost`. A broker fill's executed value is always
|
|
863
|
+
the traded premium, regardless of whether that fill opens or closes a
|
|
864
|
+
long or short position.
|
|
865
|
+
"""
|
|
866
|
+
quantity = self._option_size(size)
|
|
867
|
+
price_value = self._option_price(price)
|
|
868
|
+
return _finite_result(
|
|
869
|
+
quantity * price_value * self.get_param("mult"),
|
|
870
|
+
"option_execution_value_invalid",
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
def getsize(self, price, cash):
|
|
874
|
+
"""Return buyer quantity using premium plus the complete open fee."""
|
|
875
|
+
price_value = self._option_price(price)
|
|
876
|
+
available = max(_finite_number(cash, "option_cash_invalid"), 0.0)
|
|
877
|
+
unit_premium = _finite_result(price_value * self.get_param("mult"), "option_cost_nonfinite")
|
|
878
|
+
money_fee, volume_fee = self._fee_pair("open")
|
|
879
|
+
unit_fee = _finite_result(unit_premium * money_fee + volume_fee, "option_fee_nonfinite")
|
|
880
|
+
if unit_premium + unit_fee <= 0.0:
|
|
881
|
+
return 0
|
|
882
|
+
quantity = _finite_result(available // (unit_premium + unit_fee), "option_size_nonfinite")
|
|
883
|
+
return int(quantity)
|
|
884
|
+
|
|
885
|
+
def getvaluesize(self, size, price):
|
|
886
|
+
"""Return signed option position value at a mark price."""
|
|
887
|
+
return _finite_result(
|
|
888
|
+
_finite_number(size, "option_size_invalid")
|
|
889
|
+
* self._valuation_price(price)
|
|
890
|
+
* self.get_param("mult"),
|
|
891
|
+
"option_value_nonfinite",
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
def getvalue(self, position, price):
|
|
895
|
+
"""Return signed position value; shorts remain negative."""
|
|
896
|
+
return self.getvaluesize(position.size, price)
|
|
897
|
+
|
|
898
|
+
def _getcommission(self, size, price, pseudoexec, role=None):
|
|
899
|
+
_ = pseudoexec
|
|
900
|
+
quantity = self._option_size(size)
|
|
901
|
+
if quantity == 0.0:
|
|
902
|
+
return 0.0
|
|
903
|
+
price_value = self._option_price(price)
|
|
904
|
+
money_fee, volume_fee = self._fee_pair(role)
|
|
905
|
+
return _finite_result(
|
|
906
|
+
quantity * (price_value * self.get_param("mult") * money_fee + volume_fee),
|
|
907
|
+
"option_fee_nonfinite",
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
def profitandloss(self, size, price, newprice):
|
|
911
|
+
"""Return linear signed option PnL."""
|
|
912
|
+
return _finite_result(
|
|
913
|
+
_finite_number(size, "option_size_invalid")
|
|
914
|
+
* (self._valuation_price(newprice) - self._valuation_price(price))
|
|
915
|
+
* self.get_param("mult"),
|
|
916
|
+
"option_pnl_nonfinite",
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
def cashadjust(self, size, price, newprice):
|
|
920
|
+
"""Premium-style options settle through execution; no mark cash flow."""
|
|
921
|
+
_ = size, price, newprice
|
|
922
|
+
return 0.0
|
|
923
|
+
|
|
924
|
+
def get_margin(self, price):
|
|
925
|
+
"""Return only an explicitly sourced seller margin per contract."""
|
|
926
|
+
return self._seller_margin_per_unit(price)
|
|
927
|
+
|
|
928
|
+
def accounting_projection(self, size, price, *, is_buy=True, role="open") -> dict[str, Any]:
|
|
929
|
+
"""Return a reviewable single-leg projection without changing cash."""
|
|
930
|
+
quantity = self._option_size(size)
|
|
931
|
+
premium = _finite_result(
|
|
932
|
+
quantity * self._option_price(price) * self.get_param("mult"),
|
|
933
|
+
"option_value_nonfinite",
|
|
934
|
+
)
|
|
935
|
+
commission = self.getcommission(quantity, price, role=role)
|
|
936
|
+
role_text = self._role_text(role)
|
|
937
|
+
is_open = role_text in {"open", "opened"}
|
|
938
|
+
if role_text not in {
|
|
939
|
+
"open",
|
|
940
|
+
"opened",
|
|
941
|
+
"close",
|
|
942
|
+
"closed",
|
|
943
|
+
"close_today",
|
|
944
|
+
"closetoday",
|
|
945
|
+
"close_yesterday",
|
|
946
|
+
"closeyesterday",
|
|
947
|
+
}:
|
|
948
|
+
raise OptionAccountingError(
|
|
949
|
+
"option_accounting_role_unknown",
|
|
950
|
+
f"option_accounting_role_unknown: unsupported role {role!r}",
|
|
951
|
+
)
|
|
952
|
+
if is_open and self._is_buy_side(is_buy):
|
|
953
|
+
margin = 0.0
|
|
954
|
+
cashflow = -(premium + commission)
|
|
955
|
+
premium_cashflow = -premium
|
|
956
|
+
source = "buyer_premium"
|
|
957
|
+
elif is_open:
|
|
958
|
+
margin = _finite_result(
|
|
959
|
+
self._seller_margin_per_unit(price, quantity) * quantity,
|
|
960
|
+
"option_margin_nonfinite",
|
|
961
|
+
)
|
|
962
|
+
cashflow = premium - commission
|
|
963
|
+
premium_cashflow = premium
|
|
964
|
+
source = self.seller_margin_status()
|
|
965
|
+
else:
|
|
966
|
+
margin = 0.0
|
|
967
|
+
premium_cashflow = -premium if self._is_buy_side(is_buy) else premium
|
|
968
|
+
cashflow = premium_cashflow - commission
|
|
969
|
+
source = "closing_premium"
|
|
970
|
+
return {
|
|
971
|
+
"quantity": quantity,
|
|
972
|
+
"premium": premium,
|
|
973
|
+
"premium_cashflow": premium_cashflow,
|
|
974
|
+
"margin": margin,
|
|
975
|
+
"commission": commission,
|
|
976
|
+
"cashflow": cashflow,
|
|
977
|
+
"cashadjust": 0.0,
|
|
978
|
+
"source": source,
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
# Names used by the surrounding CTP examples and by older integration code.
|
|
983
|
+
ComminfoCtpOptionPremium = CtpOptionPremium
|
|
984
|
+
CtpOptionComminfo = CtpOptionPremium
|
|
985
|
+
|
|
986
|
+
__all__ = [
|
|
987
|
+
"CtpOptionPremium",
|
|
988
|
+
"ComminfoCtpOptionPremium",
|
|
989
|
+
"CtpOptionComminfo",
|
|
990
|
+
"CtpOptionSellerMarginEvidence",
|
|
991
|
+
"OptionAccountingError",
|
|
992
|
+
"validate_seller_margin_evidence",
|
|
993
|
+
]
|