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.
Files changed (465) hide show
  1. back_trader_python-1.4.0.dist-info/METADATA +1491 -0
  2. back_trader_python-1.4.0.dist-info/RECORD +465 -0
  3. back_trader_python-1.4.0.dist-info/WHEEL +5 -0
  4. back_trader_python-1.4.0.dist-info/licenses/LICENSE +674 -0
  5. back_trader_python-1.4.0.dist-info/top_level.txt +1 -0
  6. backtrader/__init__.py +148 -0
  7. backtrader/_cerebro/__init__.py +5 -0
  8. backtrader/_cerebro/channel.py +382 -0
  9. backtrader/_cerebro/execution.py +377 -0
  10. backtrader/_cerebro/lifecycle.py +143 -0
  11. backtrader/_cerebro/notifications.py +150 -0
  12. backtrader/_cerebro/presentation.py +230 -0
  13. backtrader/_cerebro/registry.py +593 -0
  14. backtrader/_cerebro/runnext.py +551 -0
  15. backtrader/_cerebro/runonce.py +142 -0
  16. backtrader/analyzer.py +594 -0
  17. backtrader/analyzers/__init__.py +50 -0
  18. backtrader/analyzers/annualreturn.py +226 -0
  19. backtrader/analyzers/calmar.py +165 -0
  20. backtrader/analyzers/drawdown.py +287 -0
  21. backtrader/analyzers/leverage.py +112 -0
  22. backtrader/analyzers/logreturnsrolling.py +190 -0
  23. backtrader/analyzers/periodstats.py +153 -0
  24. backtrader/analyzers/positions.py +119 -0
  25. backtrader/analyzers/pyfolio.py +470 -0
  26. backtrader/analyzers/returns.py +192 -0
  27. backtrader/analyzers/sharpe.py +307 -0
  28. backtrader/analyzers/sharpe_ratio_stats.py +534 -0
  29. backtrader/analyzers/sqn.py +112 -0
  30. backtrader/analyzers/timereturn.py +192 -0
  31. backtrader/analyzers/total_value.py +75 -0
  32. backtrader/analyzers/tradeanalyzer.py +278 -0
  33. backtrader/analyzers/transactions.py +141 -0
  34. backtrader/analyzers/vwr.py +245 -0
  35. backtrader/bokeh/__init__.py +155 -0
  36. backtrader/bokeh/analyzers/__init__.py +13 -0
  37. backtrader/bokeh/analyzers/plot.py +192 -0
  38. backtrader/bokeh/analyzers/recorder.py +181 -0
  39. backtrader/bokeh/app.py +1094 -0
  40. backtrader/bokeh/live/__init__.py +11 -0
  41. backtrader/bokeh/live/client.py +352 -0
  42. backtrader/bokeh/live/datahandler.py +346 -0
  43. backtrader/bokeh/plot_adapter.py +200 -0
  44. backtrader/bokeh/schemes/__init__.py +14 -0
  45. backtrader/bokeh/schemes/blackly.py +76 -0
  46. backtrader/bokeh/schemes/scheme.py +150 -0
  47. backtrader/bokeh/schemes/tradimo.py +82 -0
  48. backtrader/bokeh/tab.py +125 -0
  49. backtrader/bokeh/tabs/__init__.py +30 -0
  50. backtrader/bokeh/tabs/analyzer.py +120 -0
  51. backtrader/bokeh/tabs/config.py +154 -0
  52. backtrader/bokeh/tabs/live.py +109 -0
  53. backtrader/bokeh/tabs/log.py +185 -0
  54. backtrader/bokeh/tabs/metadata.py +182 -0
  55. backtrader/bokeh/tabs/performance.py +359 -0
  56. backtrader/bokeh/tabs/source.py +70 -0
  57. backtrader/bokeh/utils/__init__.py +8 -0
  58. backtrader/bokeh/utils/helpers.py +167 -0
  59. backtrader/bokeh/webapp.py +164 -0
  60. backtrader/broker.py +478 -0
  61. backtrader/brokers/__init__.py +36 -0
  62. backtrader/brokers/bbroker.py +2576 -0
  63. backtrader/brokers/btapibroker.py +8227 -0
  64. backtrader/brokers/hft/__init__.py +89 -0
  65. backtrader/brokers/hft/binance_bbo.py +625 -0
  66. backtrader/brokers/hft/binance_bbo_compare.py +1398 -0
  67. backtrader/brokers/hft/examples.py +1228 -0
  68. backtrader/brokers/hft/exchange.py +380 -0
  69. backtrader/brokers/hft/latency.py +309 -0
  70. backtrader/brokers/hft/matching_core.py +572 -0
  71. backtrader/brokers/hft/queue.py +238 -0
  72. backtrader/brokers/hft/recorder.py +88 -0
  73. backtrader/brokers/hft/state.py +138 -0
  74. backtrader/brokers/impact_models.py +118 -0
  75. backtrader/brokers/mixbroker.py +895 -0
  76. backtrader/brokers/tickbroker.py +1991 -0
  77. backtrader/btrun/__init__.py +12 -0
  78. backtrader/btrun/btrun.py +1218 -0
  79. backtrader/cerebro.py +828 -0
  80. backtrader/channel.py +682 -0
  81. backtrader/channels/__init__.py +23 -0
  82. backtrader/channels/bridge.py +186 -0
  83. backtrader/channels/funding.py +248 -0
  84. backtrader/channels/live_queue.py +216 -0
  85. backtrader/channels/live_validator.py +294 -0
  86. backtrader/channels/orderbook.py +257 -0
  87. backtrader/channels/tick.py +202 -0
  88. backtrader/comminfo.py +665 -0
  89. backtrader/commissions/__init__.py +106 -0
  90. backtrader/commissions/ctpoption.py +993 -0
  91. backtrader/configs/account_config_example.yaml +8 -0
  92. backtrader/dataseries.py +379 -0
  93. backtrader/errors.py +106 -0
  94. backtrader/events.py +980 -0
  95. backtrader/feed.py +1523 -0
  96. backtrader/feeds/__init__.py +75 -0
  97. backtrader/feeds/barrier.py +2006 -0
  98. backtrader/feeds/blaze.py +118 -0
  99. backtrader/feeds/btapifeed.py +1538 -0
  100. backtrader/feeds/btcsv.py +203 -0
  101. backtrader/feeds/chainer.py +114 -0
  102. backtrader/feeds/cryptohftdata.py +164 -0
  103. backtrader/feeds/csvgeneric.py +1205 -0
  104. backtrader/feeds/ctpcohort.py +1051 -0
  105. backtrader/feeds/influxfeed.py +158 -0
  106. backtrader/feeds/livefeed.py +71 -0
  107. backtrader/feeds/mixed_channel.py +108 -0
  108. backtrader/feeds/mt4csv.py +42 -0
  109. backtrader/feeds/pandafeed.py +381 -0
  110. backtrader/feeds/quandl.py +256 -0
  111. backtrader/feeds/rollover.py +229 -0
  112. backtrader/feeds/sierrachart.py +30 -0
  113. backtrader/feeds/vchart.py +162 -0
  114. backtrader/feeds/vchartcsv.py +84 -0
  115. backtrader/feeds/vchartfile.py +153 -0
  116. backtrader/feeds/yahoo.py +399 -0
  117. backtrader/fillers.py +148 -0
  118. backtrader/filters/__init__.py +34 -0
  119. backtrader/filters/bsplitter.py +127 -0
  120. backtrader/filters/calendardays.py +121 -0
  121. backtrader/filters/datafiller.py +192 -0
  122. backtrader/filters/datafilter.py +74 -0
  123. backtrader/filters/daysteps.py +96 -0
  124. backtrader/filters/heikinashi.py +63 -0
  125. backtrader/filters/renko.py +164 -0
  126. backtrader/filters/session.py +289 -0
  127. backtrader/flt.py +80 -0
  128. backtrader/functions.py +960 -0
  129. backtrader/indicator.py +449 -0
  130. backtrader/indicators/__init__.py +148 -0
  131. backtrader/indicators/accdecoscillator.py +110 -0
  132. backtrader/indicators/aroon.py +300 -0
  133. backtrader/indicators/atr.py +315 -0
  134. backtrader/indicators/awesomeoscillator.py +122 -0
  135. backtrader/indicators/basicops.py +834 -0
  136. backtrader/indicators/bollinger.py +223 -0
  137. backtrader/indicators/cci.py +89 -0
  138. backtrader/indicators/channels_ext.py +83 -0
  139. backtrader/indicators/contrib/__init__.py +228 -0
  140. backtrader/indicators/contrib/absolutely_no_lag_lwma.py +28 -0
  141. backtrader/indicators/contrib/absolutely_no_lag_lwma_color.py +44 -0
  142. backtrader/indicators/contrib/accumulation_distribution_line.py +92 -0
  143. backtrader/indicators/contrib/adx_cross_hull_style_indicator.py +249 -0
  144. backtrader/indicators/contrib/adxdmi.py +34 -0
  145. backtrader/indicators/contrib/ai_acceleration_deceleration_oscillator.py +34 -0
  146. backtrader/indicators/contrib/altr_trend_signal_v22.py +85 -0
  147. backtrader/indicators/contrib/anchored_momentum_line.py +115 -0
  148. backtrader/indicators/contrib/any_range_cld_tail_indicator.py +82 -0
  149. backtrader/indicators/contrib/aroon_horn_sign_indicator.py +96 -0
  150. backtrader/indicators/contrib/aroon_oscillator_sign_alert.py +50 -0
  151. backtrader/indicators/contrib/arrows_curves_indicator.py +112 -0
  152. backtrader/indicators/contrib/as_ctrend_indicator.py +143 -0
  153. backtrader/indicators/contrib/asimmetric_stoch_nr_indicator.py +187 -0
  154. backtrader/indicators/contrib/atr_normalize_histogram.py +118 -0
  155. backtrader/indicators/contrib/average_change_candle.py +165 -0
  156. backtrader/indicators/contrib/bb_squeeze_indicator.py +60 -0
  157. backtrader/indicators/contrib/bezier_st_dev_indicator.py +135 -0
  158. backtrader/indicators/contrib/binary_wave_indicator.py +233 -0
  159. backtrader/indicators/contrib/blau_c_momentum_indicator.py +123 -0
  160. backtrader/indicators/contrib/blau_cmi_indicator.py +141 -0
  161. backtrader/indicators/contrib/blau_csi.py +76 -0
  162. backtrader/indicators/contrib/blau_ergodic.py +53 -0
  163. backtrader/indicators/contrib/blau_t_stoch_i.py +72 -0
  164. backtrader/indicators/contrib/blau_ts_stochastic.py +85 -0
  165. backtrader/indicators/contrib/blau_tvi.py +55 -0
  166. backtrader/indicators/contrib/brain_trend2_indicator.py +128 -0
  167. backtrader/indicators/contrib/brain_trend_signal_proxy.py +47 -0
  168. backtrader/indicators/contrib/brake_parb_indicator.py +85 -0
  169. backtrader/indicators/contrib/breakout_bars_trend_v2.py +121 -0
  170. backtrader/indicators/contrib/bsi_indicator.py +87 -0
  171. backtrader/indicators/contrib/bulls_bears_eyes.py +67 -0
  172. backtrader/indicators/contrib/bulls_power.py +56 -0
  173. backtrader/indicators/contrib/bw_wise_man1_signal.py +102 -0
  174. backtrader/indicators/contrib/bykov_trend_indicator.py +85 -0
  175. backtrader/indicators/contrib/candle_stop_color.py +46 -0
  176. backtrader/indicators/contrib/candles_x_smoothed_indicator.py +69 -0
  177. backtrader/indicators/contrib/candlesticks_bw.py +45 -0
  178. backtrader/indicators/contrib/caudate_x_period_candle_color.py +56 -0
  179. backtrader/indicators/contrib/cci_histogram_indicator.py +53 -0
  180. backtrader/indicators/contrib/cci_woodies_indicator.py +80 -0
  181. backtrader/indicators/contrib/center_of_gravity_candle_indicator.py +83 -0
  182. backtrader/indicators/contrib/center_of_gravity_indicator.py +70 -0
  183. backtrader/indicators/contrib/cg_oscillator.py +40 -0
  184. backtrader/indicators/contrib/close_line_cci.py +38 -0
  185. backtrader/indicators/contrib/close_price_fractals.py +47 -0
  186. backtrader/indicators/contrib/color3rd_gen_xma_indicator.py +122 -0
  187. backtrader/indicators/contrib/color_bb_candles_indicator.py +108 -0
  188. backtrader/indicators/contrib/color_coppock_indicator.py +157 -0
  189. backtrader/indicators/contrib/color_hma.py +71 -0
  190. backtrader/indicators/contrib/color_j_variation_indicator.py +53 -0
  191. backtrader/indicators/contrib/color_metro_de_marker_indicator.py +78 -0
  192. backtrader/indicators/contrib/color_metro_stochastic_indicator.py +93 -0
  193. backtrader/indicators/contrib/color_metro_wpr_indicator.py +85 -0
  194. backtrader/indicators/contrib/color_schaff_de_marker_trend_cycle.py +92 -0
  195. backtrader/indicators/contrib/color_schaff_trend_cycle_indicator.py +203 -0
  196. backtrader/indicators/contrib/color_step_xccx_indicator.py +193 -0
  197. backtrader/indicators/contrib/color_x2_ma.py +49 -0
  198. backtrader/indicators/contrib/color_x_derivative.py +63 -0
  199. backtrader/indicators/contrib/color_zerolag_de_marker.py +84 -0
  200. backtrader/indicators/contrib/corrected_average_indicator.py +127 -0
  201. backtrader/indicators/contrib/darvas_boxes_system.py +73 -0
  202. backtrader/indicators/contrib/dema_range_channel_color.py +42 -0
  203. backtrader/indicators/contrib/derivative_indicator.py +95 -0
  204. backtrader/indicators/contrib/digital_ft01_indicator.py +112 -0
  205. backtrader/indicators/contrib/digital_macd.py +200 -0
  206. backtrader/indicators/contrib/donchian_channels_system.py +45 -0
  207. backtrader/indicators/contrib/dots_indicator.py +93 -0
  208. backtrader/indicators/contrib/ef_distance_indicator.py +82 -0
  209. backtrader/indicators/contrib/ema_rsi_va.py +80 -0
  210. backtrader/indicators/contrib/envelopes_jp_alonso.py +32 -0
  211. backtrader/indicators/contrib/f2a_ao_indicator.py +120 -0
  212. backtrader/indicators/contrib/fatl_filter.py +179 -0
  213. backtrader/indicators/contrib/fibo_candles_indicator.py +78 -0
  214. backtrader/indicators/contrib/fine_tuning_ma.py +100 -0
  215. backtrader/indicators/contrib/fisher_org_v1.py +102 -0
  216. backtrader/indicators/contrib/fisher_org_v1_sign.py +118 -0
  217. backtrader/indicators/contrib/force_index_ema.py +96 -0
  218. backtrader/indicators/contrib/force_index_ema_2.py +27 -0
  219. backtrader/indicators/contrib/forecast_oscilator.py +145 -0
  220. backtrader/indicators/contrib/fractal_amambk.py +81 -0
  221. backtrader/indicators/contrib/frama_series.py +84 -0
  222. backtrader/indicators/contrib/frasm_av2_indicator.py +104 -0
  223. backtrader/indicators/contrib/go_indicator.py +93 -0
  224. backtrader/indicators/contrib/hlr_indicator.py +95 -0
  225. backtrader/indicators/contrib/hma.py +50 -0
  226. backtrader/indicators/contrib/i4_drfv2.py +34 -0
  227. backtrader/indicators/contrib/i4_drfv3.py +38 -0
  228. backtrader/indicators/contrib/i_anch_mom_indicator.py +72 -0
  229. backtrader/indicators/contrib/i_de_marker_sign_indicator.py +64 -0
  230. backtrader/indicators/contrib/i_gap_indicator.py +45 -0
  231. backtrader/indicators/contrib/i_stoch_komposter_indicator.py +77 -0
  232. backtrader/indicators/contrib/i_trend_indicator.py +125 -0
  233. backtrader/indicators/contrib/iamma_indicator.py +39 -0
  234. backtrader/indicators/contrib/indexed_moving_average.py +33 -0
  235. backtrader/indicators/contrib/instantaneous_trend_filter_indicator.py +51 -0
  236. backtrader/indicators/contrib/inverse_reaction_indicator.py +41 -0
  237. backtrader/indicators/contrib/irsi_sign_indicator.py +95 -0
  238. backtrader/indicators/contrib/iwpr_sign_indicator.py +59 -0
  239. backtrader/indicators/contrib/j_brain_trend1_sig_indicator.py +233 -0
  240. backtrader/indicators/contrib/j_tpo_proxy.py +32 -0
  241. backtrader/indicators/contrib/jma_slope_indicator.py +73 -0
  242. backtrader/indicators/contrib/kalman_filter_indicator.py +119 -0
  243. backtrader/indicators/contrib/kalman_filter_line.py +127 -0
  244. backtrader/indicators/contrib/kama_indicator.py +150 -0
  245. backtrader/indicators/contrib/karacatica_indicator.py +99 -0
  246. backtrader/indicators/contrib/kdj_indicator.py +59 -0
  247. backtrader/indicators/contrib/kwan_ccc_indicator.py +195 -0
  248. backtrader/indicators/contrib/kwan_nrp_indicator.py +113 -0
  249. backtrader/indicators/contrib/kwan_rdp_indicator.py +192 -0
  250. backtrader/indicators/contrib/laguerre_adx_indicator.py +85 -0
  251. backtrader/indicators/contrib/laguerre_filter_indicator.py +66 -0
  252. backtrader/indicators/contrib/laguerre_plus_di_proxy.py +57 -0
  253. backtrader/indicators/contrib/laguerre_roc_indicator.py +81 -0
  254. backtrader/indicators/contrib/le_man_signal_indicator.py +63 -0
  255. backtrader/indicators/contrib/linear_reg_slope_v2_indicator.py +136 -0
  256. backtrader/indicators/contrib/loco_indicator.py +88 -0
  257. backtrader/indicators/contrib/lrma_indicator.py +185 -0
  258. backtrader/indicators/contrib/lsma_angle_indicator.py +106 -0
  259. backtrader/indicators/contrib/ma_rounding_channel_indicator.py +149 -0
  260. backtrader/indicators/contrib/macd2_indicator.py +61 -0
  261. backtrader/indicators/contrib/macd_candle_indicator.py +80 -0
  262. backtrader/indicators/contrib/malr_indicator.py +77 -0
  263. backtrader/indicators/contrib/momentum_candle_sign_indicator.py +51 -0
  264. backtrader/indicators/contrib/moving_average_fn_indicator.py +139 -0
  265. backtrader/indicators/contrib/mt5_stochastic_close_close.py +57 -0
  266. backtrader/indicators/contrib/muv_nor_diff_cloud_indicator.py +107 -0
  267. backtrader/indicators/contrib/non_lag_dot_indicator.py +124 -0
  268. backtrader/indicators/contrib/nrtr_extr_indicator.py +95 -0
  269. backtrader/indicators/contrib/nrtr_indicator.py +95 -0
  270. backtrader/indicators/contrib/p_channel_system.py +40 -0
  271. backtrader/indicators/contrib/percent_envelope.py +37 -0
  272. backtrader/indicators/contrib/percentage_crossover_channel.py +47 -0
  273. backtrader/indicators/contrib/pivot_zig_zag_proxy.py +47 -0
  274. backtrader/indicators/contrib/price_channel_stop_indicator.py +104 -0
  275. backtrader/indicators/contrib/price_extreme_channel.py +35 -0
  276. backtrader/indicators/contrib/qqe_cloud_indicator.py +129 -0
  277. backtrader/indicators/contrib/ravi_indicator.py +40 -0
  278. backtrader/indicators/contrib/raw_close_close_stochastic.py +74 -0
  279. backtrader/indicators/contrib/rd_trend_trigger_indicator.py +51 -0
  280. backtrader/indicators/contrib/renko_level.py +85 -0
  281. backtrader/indicators/contrib/renko_line_break.py +91 -0
  282. backtrader/indicators/contrib/rftl_indicator.py +41 -0
  283. backtrader/indicators/contrib/rkd_indicator.py +53 -0
  284. backtrader/indicators/contrib/roc2_vg_indicator.py +68 -0
  285. backtrader/indicators/contrib/rsi_histogram_indicator.py +43 -0
  286. backtrader/indicators/contrib/rsi_slowdown.py +57 -0
  287. backtrader/indicators/contrib/rsioma_v2.py +41 -0
  288. backtrader/indicators/contrib/rvi_histogram_indicator.py +107 -0
  289. backtrader/indicators/contrib/safe_adx.py +89 -0
  290. backtrader/indicators/contrib/shared_strategy_indicators.py +1651 -0
  291. backtrader/indicators/contrib/sidus_indicator.py +105 -0
  292. backtrader/indicators/contrib/silver_trend_indicator.py +79 -0
  293. backtrader/indicators/contrib/sliding_range_color.py +56 -0
  294. backtrader/indicators/contrib/slow_stoch.py +42 -0
  295. backtrader/indicators/contrib/smoothed_adx_indicator.py +86 -0
  296. backtrader/indicators/contrib/smoothed_rsi.py +31 -0
  297. backtrader/indicators/contrib/spearman_rank_correlation_histogram.py +60 -0
  298. backtrader/indicators/contrib/stalin_indicator.py +152 -0
  299. backtrader/indicators/contrib/starter_laguerre_filter.py +62 -0
  300. backtrader/indicators/contrib/step_manrtr_indicator.py +137 -0
  301. backtrader/indicators/contrib/stochastic_histogram_indicator.py +143 -0
  302. backtrader/indicators/contrib/t3_alarm_indicator.py +125 -0
  303. backtrader/indicators/contrib/t3_average.py +76 -0
  304. backtrader/indicators/contrib/t3_indicator.py +40 -0
  305. backtrader/indicators/contrib/the20s_v020_signal.py +93 -0
  306. backtrader/indicators/contrib/three_candles_indicator.py +70 -0
  307. backtrader/indicators/contrib/three_line_break_indicator.py +64 -0
  308. backtrader/indicators/contrib/time_line.py +57 -0
  309. backtrader/indicators/contrib/trading_channel_index_proxy.py +48 -0
  310. backtrader/indicators/contrib/trend_arrows_indicator.py +109 -0
  311. backtrader/indicators/contrib/trend_continuation_indicator.py +127 -0
  312. backtrader/indicators/contrib/trend_intensity_index_proxy.py +51 -0
  313. backtrader/indicators/contrib/trend_manager_indicator.py +39 -0
  314. backtrader/indicators/contrib/tri_x_candle_indicator.py +51 -0
  315. backtrader/indicators/contrib/trigger_line.py +66 -0
  316. backtrader/indicators/contrib/triple_ema_rate.py +34 -0
  317. backtrader/indicators/contrib/trvi_indicator.py +194 -0
  318. backtrader/indicators/contrib/two_pb_ideal_xosma_indicator.py +127 -0
  319. backtrader/indicators/contrib/ultra_absolutely_no_lag_lwma_color.py +92 -0
  320. backtrader/indicators/contrib/ultra_wpr_indicator.py +173 -0
  321. backtrader/indicators/contrib/up_down_candle_strength.py +68 -0
  322. backtrader/indicators/contrib/vinin_i_trend_indicator.py +139 -0
  323. backtrader/indicators/contrib/volume_weighted_ma_indicator.py +78 -0
  324. backtrader/indicators/contrib/volume_weighted_ma_st_dev_indicator.py +111 -0
  325. backtrader/indicators/contrib/vwap_close_indicator.py +65 -0
  326. backtrader/indicators/contrib/vwma_candle.py +57 -0
  327. backtrader/indicators/contrib/vwma_digit_system.py +70 -0
  328. backtrader/indicators/contrib/wami.py +43 -0
  329. backtrader/indicators/contrib/wprsi_signal_indicator.py +105 -0
  330. backtrader/indicators/contrib/x_de_marker_histogram_vol_direct_indicator.py +145 -0
  331. backtrader/indicators/contrib/x_fisher_indicator.py +64 -0
  332. backtrader/indicators/contrib/xcci_histogram_vol_direct_indicator.py +56 -0
  333. backtrader/indicators/contrib/xcci_histogram_vol_indicator.py +85 -0
  334. backtrader/indicators/contrib/xma_ichimoku.py +163 -0
  335. backtrader/indicators/contrib/xma_ishimoku_channel_indicator.py +65 -0
  336. backtrader/indicators/contrib/xma_ishimoku_line.py +68 -0
  337. backtrader/indicators/contrib/xma_range_bands_indicator.py +107 -0
  338. backtrader/indicators/contrib/xmacd_indicator.py +70 -0
  339. backtrader/indicators/contrib/xrsi_de_marker_histogram.py +67 -0
  340. backtrader/indicators/contrib/xrsi_histogram_vol_direct_indicator.py +52 -0
  341. backtrader/indicators/contrib/xrsi_histogram_vol_indicator.py +81 -0
  342. backtrader/indicators/contrib/xrvi_indicator.py +130 -0
  343. backtrader/indicators/contrib/zero_lag_macd.py +36 -0
  344. backtrader/indicators/contrib/zig_zag_recent_pivot_signal.py +90 -0
  345. backtrader/indicators/contrib/zpf_indicator.py +115 -0
  346. backtrader/indicators/crossover.py +337 -0
  347. backtrader/indicators/dema.py +175 -0
  348. backtrader/indicators/demarker.py +270 -0
  349. backtrader/indicators/deviation.py +284 -0
  350. backtrader/indicators/directionalmove.py +1071 -0
  351. backtrader/indicators/dma.py +112 -0
  352. backtrader/indicators/dpo.py +96 -0
  353. backtrader/indicators/dv2.py +56 -0
  354. backtrader/indicators/ema.py +145 -0
  355. backtrader/indicators/envelope.py +475 -0
  356. backtrader/indicators/hadelta.py +198 -0
  357. backtrader/indicators/heikinashi.py +153 -0
  358. backtrader/indicators/hma.py +153 -0
  359. backtrader/indicators/hurst.py +151 -0
  360. backtrader/indicators/ichimoku.py +267 -0
  361. backtrader/indicators/kama.py +181 -0
  362. backtrader/indicators/kst.py +159 -0
  363. backtrader/indicators/lrsi.py +125 -0
  364. backtrader/indicators/mabase.py +147 -0
  365. backtrader/indicators/macd.py +322 -0
  366. backtrader/indicators/momentum.py +267 -0
  367. backtrader/indicators/moneyflow.py +237 -0
  368. backtrader/indicators/mt5atr.py +124 -0
  369. backtrader/indicators/myind.py +179 -0
  370. backtrader/indicators/obv.py +94 -0
  371. backtrader/indicators/ols.py +265 -0
  372. backtrader/indicators/oscillator.py +161 -0
  373. backtrader/indicators/percentchange.py +83 -0
  374. backtrader/indicators/percentrank.py +46 -0
  375. backtrader/indicators/pivotpoint.py +469 -0
  376. backtrader/indicators/prettygoodoscillator.py +113 -0
  377. backtrader/indicators/priceops_ext.py +123 -0
  378. backtrader/indicators/priceoscillator.py +262 -0
  379. backtrader/indicators/psar.py +212 -0
  380. backtrader/indicators/rmi.py +69 -0
  381. backtrader/indicators/rsi.py +440 -0
  382. backtrader/indicators/sma.py +141 -0
  383. backtrader/indicators/smma.py +116 -0
  384. backtrader/indicators/spread.py +54 -0
  385. backtrader/indicators/stochastic.py +263 -0
  386. backtrader/indicators/supertrend.py +436 -0
  387. backtrader/indicators/trend_ext.py +105 -0
  388. backtrader/indicators/trix.py +202 -0
  389. backtrader/indicators/tsi.py +155 -0
  390. backtrader/indicators/ultimateoscillator.py +158 -0
  391. backtrader/indicators/vortex.py +62 -0
  392. backtrader/indicators/williams.py +194 -0
  393. backtrader/indicators/wma.py +103 -0
  394. backtrader/indicators/zlema.py +135 -0
  395. backtrader/indicators/zlind.py +104 -0
  396. backtrader/linebuffer.py +3155 -0
  397. backtrader/lineiterator.py +2911 -0
  398. backtrader/lineroot.py +1106 -0
  399. backtrader/lineseries.py +2559 -0
  400. backtrader/live_trading/__init__.py +31 -0
  401. backtrader/live_trading/interface.py +404 -0
  402. backtrader/mathsupport.py +94 -0
  403. backtrader/metabase.py +1804 -0
  404. backtrader/mixins/__init__.py +21 -0
  405. backtrader/mixins/singleton.py +118 -0
  406. backtrader/observer.py +106 -0
  407. backtrader/observers/__init__.py +45 -0
  408. backtrader/observers/benchmark.py +126 -0
  409. backtrader/observers/broker.py +184 -0
  410. backtrader/observers/buysell.py +144 -0
  411. backtrader/observers/drawdown.py +161 -0
  412. backtrader/observers/logreturns.py +113 -0
  413. backtrader/observers/timereturn.py +86 -0
  414. backtrader/observers/trade_logger.py +2972 -0
  415. backtrader/observers/tradelogger.py +6 -0
  416. backtrader/observers/trades.py +258 -0
  417. backtrader/order.py +1114 -0
  418. backtrader/parameters.py +2345 -0
  419. backtrader/plot/__init__.py +54 -0
  420. backtrader/plot/finance.py +1022 -0
  421. backtrader/plot/formatters.py +200 -0
  422. backtrader/plot/locator.py +353 -0
  423. backtrader/plot/multicursor.py +495 -0
  424. backtrader/plot/plot.py +2500 -0
  425. backtrader/plot/plot_plotly.py +1351 -0
  426. backtrader/plot/scheme.py +253 -0
  427. backtrader/plot/utils.py +104 -0
  428. backtrader/position.py +290 -0
  429. backtrader/position_modes.py +132 -0
  430. backtrader/profiles.py +254 -0
  431. backtrader/reports/__init__.py +39 -0
  432. backtrader/reports/charts.py +371 -0
  433. backtrader/reports/performance.py +620 -0
  434. backtrader/reports/reporter.py +660 -0
  435. backtrader/resamplerfilter.py +1001 -0
  436. backtrader/signal.py +118 -0
  437. backtrader/signals/__init__.py +17 -0
  438. backtrader/sizer.py +114 -0
  439. backtrader/sizers/__init__.py +26 -0
  440. backtrader/sizers/fixedsize.py +161 -0
  441. backtrader/sizers/percents_sizer.py +119 -0
  442. backtrader/store.py +221 -0
  443. backtrader/stores/__init__.py +33 -0
  444. backtrader/stores/btapistore.py +15506 -0
  445. backtrader/stores/livestore.py +137 -0
  446. backtrader/stores/vchartfile.py +96 -0
  447. backtrader/strategy.py +3655 -0
  448. backtrader/talib.py +280 -0
  449. backtrader/test_helpers.py +96 -0
  450. backtrader/timer.py +358 -0
  451. backtrader/trade.py +442 -0
  452. backtrader/tradingcal.py +361 -0
  453. backtrader/utils/__init__.py +68 -0
  454. backtrader/utils/autodict.py +251 -0
  455. backtrader/utils/date.py +71 -0
  456. backtrader/utils/dateintern.py +509 -0
  457. backtrader/utils/flushfile.py +94 -0
  458. backtrader/utils/fractal.py +101 -0
  459. backtrader/utils/get_metrics.py +101 -0
  460. backtrader/utils/load_data.py +209 -0
  461. backtrader/utils/log_message.py +998 -0
  462. backtrader/utils/ordereddefaultdict.py +75 -0
  463. backtrader/utils/py3.py +296 -0
  464. backtrader/version.py +21 -0
  465. 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
+ ]