wbfdm 2.2.1__py2.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.

Potentially problematic release.


This version of wbfdm might be problematic. Click here for more details.

Files changed (337) hide show
  1. wbfdm/__init__.py +2 -0
  2. wbfdm/admin/__init__.py +42 -0
  3. wbfdm/admin/classifications.py +39 -0
  4. wbfdm/admin/esg.py +23 -0
  5. wbfdm/admin/exchanges.py +53 -0
  6. wbfdm/admin/instrument_lists.py +23 -0
  7. wbfdm/admin/instrument_prices.py +62 -0
  8. wbfdm/admin/instrument_requests.py +33 -0
  9. wbfdm/admin/instruments.py +117 -0
  10. wbfdm/admin/instruments_relationships.py +25 -0
  11. wbfdm/admin/options.py +101 -0
  12. wbfdm/analysis/__init__.py +2 -0
  13. wbfdm/analysis/esg/__init__.py +0 -0
  14. wbfdm/analysis/esg/enums.py +82 -0
  15. wbfdm/analysis/esg/esg_analysis.py +217 -0
  16. wbfdm/analysis/esg/utils.py +13 -0
  17. wbfdm/analysis/financial_analysis/__init__.py +1 -0
  18. wbfdm/analysis/financial_analysis/financial_metric_analysis.py +88 -0
  19. wbfdm/analysis/financial_analysis/financial_ratio_analysis.py +125 -0
  20. wbfdm/analysis/financial_analysis/financial_statistics_analysis.py +271 -0
  21. wbfdm/analysis/financial_analysis/statement_with_estimates.py +558 -0
  22. wbfdm/analysis/financial_analysis/utils.py +316 -0
  23. wbfdm/analysis/technical_analysis/__init__.py +1 -0
  24. wbfdm/analysis/technical_analysis/technical_analysis.py +138 -0
  25. wbfdm/analysis/technical_analysis/traces.py +165 -0
  26. wbfdm/analysis/utils.py +32 -0
  27. wbfdm/apps.py +14 -0
  28. wbfdm/contrib/__init__.py +0 -0
  29. wbfdm/contrib/dsws/__init__.py +0 -0
  30. wbfdm/contrib/dsws/client.py +285 -0
  31. wbfdm/contrib/internal/__init__.py +0 -0
  32. wbfdm/contrib/internal/dataloaders/__init__.py +0 -0
  33. wbfdm/contrib/internal/dataloaders/market_data.py +87 -0
  34. wbfdm/contrib/metric/__init__.py +0 -0
  35. wbfdm/contrib/metric/admin/__init__.py +2 -0
  36. wbfdm/contrib/metric/admin/instruments.py +12 -0
  37. wbfdm/contrib/metric/admin/metrics.py +43 -0
  38. wbfdm/contrib/metric/apps.py +10 -0
  39. wbfdm/contrib/metric/backends/__init__.py +2 -0
  40. wbfdm/contrib/metric/backends/base.py +159 -0
  41. wbfdm/contrib/metric/backends/performances.py +265 -0
  42. wbfdm/contrib/metric/backends/statistics.py +182 -0
  43. wbfdm/contrib/metric/decorators.py +14 -0
  44. wbfdm/contrib/metric/dispatch.py +23 -0
  45. wbfdm/contrib/metric/dto.py +88 -0
  46. wbfdm/contrib/metric/exceptions.py +6 -0
  47. wbfdm/contrib/metric/factories.py +33 -0
  48. wbfdm/contrib/metric/filters.py +28 -0
  49. wbfdm/contrib/metric/migrations/0001_initial.py +88 -0
  50. wbfdm/contrib/metric/migrations/0002_remove_instrumentmetric_unique_instrument_metric_and_more.py +26 -0
  51. wbfdm/contrib/metric/migrations/__init__.py +0 -0
  52. wbfdm/contrib/metric/models.py +180 -0
  53. wbfdm/contrib/metric/orchestrators.py +94 -0
  54. wbfdm/contrib/metric/registry.py +80 -0
  55. wbfdm/contrib/metric/serializers.py +44 -0
  56. wbfdm/contrib/metric/tasks.py +27 -0
  57. wbfdm/contrib/metric/tests/__init__.py +0 -0
  58. wbfdm/contrib/metric/tests/backends/__init__.py +0 -0
  59. wbfdm/contrib/metric/tests/backends/test_performances.py +152 -0
  60. wbfdm/contrib/metric/tests/backends/test_statistics.py +48 -0
  61. wbfdm/contrib/metric/tests/conftest.py +92 -0
  62. wbfdm/contrib/metric/tests/test_dto.py +73 -0
  63. wbfdm/contrib/metric/tests/test_models.py +72 -0
  64. wbfdm/contrib/metric/tests/test_tasks.py +24 -0
  65. wbfdm/contrib/metric/tests/test_viewsets.py +79 -0
  66. wbfdm/contrib/metric/urls.py +19 -0
  67. wbfdm/contrib/metric/viewsets/__init__.py +1 -0
  68. wbfdm/contrib/metric/viewsets/configs/__init__.py +1 -0
  69. wbfdm/contrib/metric/viewsets/configs/display.py +92 -0
  70. wbfdm/contrib/metric/viewsets/configs/menus.py +11 -0
  71. wbfdm/contrib/metric/viewsets/configs/utils.py +137 -0
  72. wbfdm/contrib/metric/viewsets/mixins.py +245 -0
  73. wbfdm/contrib/metric/viewsets/viewsets.py +40 -0
  74. wbfdm/contrib/msci/__init__.py +0 -0
  75. wbfdm/contrib/msci/client.py +92 -0
  76. wbfdm/contrib/msci/dataloaders/__init__.py +0 -0
  77. wbfdm/contrib/msci/dataloaders/esg.py +87 -0
  78. wbfdm/contrib/msci/dataloaders/esg_controversies.py +81 -0
  79. wbfdm/contrib/msci/sync.py +58 -0
  80. wbfdm/contrib/msci/tests/__init__.py +0 -0
  81. wbfdm/contrib/msci/tests/conftest.py +1 -0
  82. wbfdm/contrib/msci/tests/test_client.py +70 -0
  83. wbfdm/contrib/qa/__init__.py +0 -0
  84. wbfdm/contrib/qa/apps.py +22 -0
  85. wbfdm/contrib/qa/database_routers.py +25 -0
  86. wbfdm/contrib/qa/dataloaders/__init__.py +0 -0
  87. wbfdm/contrib/qa/dataloaders/adjustments.py +56 -0
  88. wbfdm/contrib/qa/dataloaders/corporate_actions.py +59 -0
  89. wbfdm/contrib/qa/dataloaders/financials.py +83 -0
  90. wbfdm/contrib/qa/dataloaders/market_data.py +117 -0
  91. wbfdm/contrib/qa/dataloaders/officers.py +59 -0
  92. wbfdm/contrib/qa/dataloaders/reporting_dates.py +67 -0
  93. wbfdm/contrib/qa/dataloaders/statements.py +267 -0
  94. wbfdm/contrib/qa/tasks.py +0 -0
  95. wbfdm/dataloaders/__init__.py +0 -0
  96. wbfdm/dataloaders/cache.py +129 -0
  97. wbfdm/dataloaders/protocols.py +112 -0
  98. wbfdm/dataloaders/proxies.py +201 -0
  99. wbfdm/dataloaders/types.py +209 -0
  100. wbfdm/dynamic_preferences_registry.py +45 -0
  101. wbfdm/enums.py +657 -0
  102. wbfdm/factories/__init__.py +13 -0
  103. wbfdm/factories/classifications.py +56 -0
  104. wbfdm/factories/controversies.py +27 -0
  105. wbfdm/factories/exchanges.py +21 -0
  106. wbfdm/factories/instrument_list.py +22 -0
  107. wbfdm/factories/instrument_prices.py +79 -0
  108. wbfdm/factories/instruments.py +63 -0
  109. wbfdm/factories/instruments_relationships.py +31 -0
  110. wbfdm/factories/options.py +66 -0
  111. wbfdm/figures/__init__.py +1 -0
  112. wbfdm/figures/financials/__init__.py +1 -0
  113. wbfdm/figures/financials/financial_analysis_charts.py +469 -0
  114. wbfdm/figures/financials/financials_charts.py +711 -0
  115. wbfdm/filters/__init__.py +31 -0
  116. wbfdm/filters/classifications.py +100 -0
  117. wbfdm/filters/exchanges.py +22 -0
  118. wbfdm/filters/financials.py +95 -0
  119. wbfdm/filters/financials_analysis.py +119 -0
  120. wbfdm/filters/instrument_prices.py +112 -0
  121. wbfdm/filters/instruments.py +198 -0
  122. wbfdm/filters/utils.py +44 -0
  123. wbfdm/import_export/__init__.py +0 -0
  124. wbfdm/import_export/backends/__init__.py +0 -0
  125. wbfdm/import_export/backends/cbinsights/__init__.py +2 -0
  126. wbfdm/import_export/backends/cbinsights/deals.py +44 -0
  127. wbfdm/import_export/backends/cbinsights/equities.py +41 -0
  128. wbfdm/import_export/backends/cbinsights/mixin.py +15 -0
  129. wbfdm/import_export/backends/cbinsights/utils/__init__.py +0 -0
  130. wbfdm/import_export/backends/cbinsights/utils/classifications.py +4150 -0
  131. wbfdm/import_export/backends/cbinsights/utils/client.py +217 -0
  132. wbfdm/import_export/backends/refinitiv/__init__.py +5 -0
  133. wbfdm/import_export/backends/refinitiv/daily_fundamental.py +36 -0
  134. wbfdm/import_export/backends/refinitiv/fiscal_period.py +63 -0
  135. wbfdm/import_export/backends/refinitiv/forecast.py +178 -0
  136. wbfdm/import_export/backends/refinitiv/fundamental.py +103 -0
  137. wbfdm/import_export/backends/refinitiv/geographic_segment.py +32 -0
  138. wbfdm/import_export/backends/refinitiv/instrument.py +55 -0
  139. wbfdm/import_export/backends/refinitiv/instrument_price.py +77 -0
  140. wbfdm/import_export/backends/refinitiv/mixin.py +29 -0
  141. wbfdm/import_export/backends/refinitiv/utils/__init__.py +1 -0
  142. wbfdm/import_export/backends/refinitiv/utils/controller.py +182 -0
  143. wbfdm/import_export/handlers/__init__.py +0 -0
  144. wbfdm/import_export/handlers/instrument.py +253 -0
  145. wbfdm/import_export/handlers/instrument_list.py +101 -0
  146. wbfdm/import_export/handlers/instrument_price.py +71 -0
  147. wbfdm/import_export/handlers/option.py +54 -0
  148. wbfdm/import_export/handlers/private_equities.py +49 -0
  149. wbfdm/import_export/parsers/__init__.py +0 -0
  150. wbfdm/import_export/parsers/cbinsights/__init__.py +0 -0
  151. wbfdm/import_export/parsers/cbinsights/deals.py +39 -0
  152. wbfdm/import_export/parsers/cbinsights/equities.py +56 -0
  153. wbfdm/import_export/parsers/cbinsights/fundamentals.py +45 -0
  154. wbfdm/import_export/parsers/refinitiv/__init__.py +0 -0
  155. wbfdm/import_export/parsers/refinitiv/daily_fundamental.py +7 -0
  156. wbfdm/import_export/parsers/refinitiv/forecast.py +7 -0
  157. wbfdm/import_export/parsers/refinitiv/fundamental.py +9 -0
  158. wbfdm/import_export/parsers/refinitiv/geographic_segment.py +7 -0
  159. wbfdm/import_export/parsers/refinitiv/instrument.py +75 -0
  160. wbfdm/import_export/parsers/refinitiv/instrument_price.py +26 -0
  161. wbfdm/import_export/parsers/refinitiv/utils.py +96 -0
  162. wbfdm/import_export/resources/__init__.py +0 -0
  163. wbfdm/import_export/resources/classification.py +23 -0
  164. wbfdm/import_export/resources/instrument_prices.py +33 -0
  165. wbfdm/import_export/resources/instruments.py +176 -0
  166. wbfdm/jinja2.py +7 -0
  167. wbfdm/management/__init__.py +30 -0
  168. wbfdm/menu.py +11 -0
  169. wbfdm/migrations/0001_initial.py +71 -0
  170. wbfdm/migrations/0002_rename_statements_instrumentlookup_financials_and_more.py +144 -0
  171. wbfdm/migrations/0003_instrument_estimate_backend_and_more.py +34 -0
  172. wbfdm/migrations/0004_rename_financials_instrumentlookup_statements_and_more.py +86 -0
  173. wbfdm/migrations/0005_instrument_corporate_action_backend.py +29 -0
  174. wbfdm/migrations/0006_instrument_officer_backend.py +29 -0
  175. wbfdm/migrations/0007_instrument_country_instrument_currency_and_more.py +117 -0
  176. wbfdm/migrations/0008_controversy.py +75 -0
  177. wbfdm/migrations/0009_alter_controversy_flag_alter_controversy_initiated_and_more.py +85 -0
  178. wbfdm/migrations/0010_classification_classificationgroup_deal_exchange_and_more.py +1299 -0
  179. wbfdm/migrations/0011_delete_instrumentlookup_instrument_corporate_actions_and_more.py +169 -0
  180. wbfdm/migrations/0012_instrumentprice_created_instrumentprice_modified.py +564 -0
  181. wbfdm/migrations/0013_instrument_is_investable_universe_and_more.py +199 -0
  182. wbfdm/migrations/0014_alter_controversy_instrument.py +22 -0
  183. wbfdm/migrations/0015_instrument_instrument_investible_index.py +16 -0
  184. wbfdm/migrations/0016_instrumenttype_name_repr.py +18 -0
  185. wbfdm/migrations/0017_instrument_instrument_security_index.py +16 -0
  186. wbfdm/migrations/0018_instrument_instrument_level_index.py +20 -0
  187. wbfdm/migrations/0019_alter_controversy_source.py +17 -0
  188. wbfdm/migrations/0020_optionaggregate_option_and_more.py +249 -0
  189. wbfdm/migrations/0021_delete_instrumentdailystatistics.py +15 -0
  190. wbfdm/migrations/0022_instrument_cusip_option_open_interest_20d_and_more.py +91 -0
  191. wbfdm/migrations/0023_instrument_unique_ric_instrument_unique_rmc_and_more.py +53 -0
  192. wbfdm/migrations/0024_option_open_interest_10d_option_volume_10d_and_more.py +36 -0
  193. wbfdm/migrations/0025_instrument_is_primary_and_more.py +29 -0
  194. wbfdm/migrations/0026_instrument_is_cash_equivalent.py +30 -0
  195. wbfdm/migrations/0027_remove_instrument_unique_ric_and_more.py +100 -0
  196. wbfdm/migrations/__init__.py +0 -0
  197. wbfdm/models/__init__.py +4 -0
  198. wbfdm/models/esg/__init__.py +1 -0
  199. wbfdm/models/esg/controversies.py +81 -0
  200. wbfdm/models/exchanges/__init__.py +1 -0
  201. wbfdm/models/exchanges/exchanges.py +223 -0
  202. wbfdm/models/fields.py +117 -0
  203. wbfdm/models/fk_fields.py +403 -0
  204. wbfdm/models/indicators.py +0 -0
  205. wbfdm/models/instruments/__init__.py +19 -0
  206. wbfdm/models/instruments/classifications.py +265 -0
  207. wbfdm/models/instruments/instrument_lists.py +120 -0
  208. wbfdm/models/instruments/instrument_prices.py +540 -0
  209. wbfdm/models/instruments/instrument_relationships.py +251 -0
  210. wbfdm/models/instruments/instrument_requests.py +196 -0
  211. wbfdm/models/instruments/instruments.py +991 -0
  212. wbfdm/models/instruments/llm/__init__.py +1 -0
  213. wbfdm/models/instruments/llm/create_instrument_news_relationships.py +78 -0
  214. wbfdm/models/instruments/mixin/__init__.py +0 -0
  215. wbfdm/models/instruments/mixin/financials_computed.py +804 -0
  216. wbfdm/models/instruments/mixin/financials_serializer_fields.py +1407 -0
  217. wbfdm/models/instruments/mixin/instruments.py +294 -0
  218. wbfdm/models/instruments/options.py +225 -0
  219. wbfdm/models/instruments/private_equities.py +59 -0
  220. wbfdm/models/instruments/querysets.py +73 -0
  221. wbfdm/models/instruments/utils.py +41 -0
  222. wbfdm/preferences.py +21 -0
  223. wbfdm/serializers/__init__.py +4 -0
  224. wbfdm/serializers/esg.py +36 -0
  225. wbfdm/serializers/exchanges.py +39 -0
  226. wbfdm/serializers/instruments/__init__.py +37 -0
  227. wbfdm/serializers/instruments/classifications.py +139 -0
  228. wbfdm/serializers/instruments/instrument_lists.py +61 -0
  229. wbfdm/serializers/instruments/instrument_prices.py +73 -0
  230. wbfdm/serializers/instruments/instrument_relationships.py +170 -0
  231. wbfdm/serializers/instruments/instrument_requests.py +61 -0
  232. wbfdm/serializers/instruments/instruments.py +274 -0
  233. wbfdm/serializers/instruments/mixins.py +104 -0
  234. wbfdm/serializers/officers.py +20 -0
  235. wbfdm/signals.py +7 -0
  236. wbfdm/sync/__init__.py +0 -0
  237. wbfdm/sync/abstract.py +31 -0
  238. wbfdm/sync/runner.py +22 -0
  239. wbfdm/tasks.py +69 -0
  240. wbfdm/tests/__init__.py +0 -0
  241. wbfdm/tests/analysis/__init__.py +0 -0
  242. wbfdm/tests/analysis/financial_analysis/__init__.py +0 -0
  243. wbfdm/tests/analysis/financial_analysis/test_statement_with_estimates.py +392 -0
  244. wbfdm/tests/analysis/financial_analysis/test_utils.py +322 -0
  245. wbfdm/tests/analysis/test_esg.py +159 -0
  246. wbfdm/tests/conftest.py +92 -0
  247. wbfdm/tests/dataloaders/__init__.py +0 -0
  248. wbfdm/tests/dataloaders/test_cache.py +73 -0
  249. wbfdm/tests/models/__init__.py +0 -0
  250. wbfdm/tests/models/test_classifications.py +99 -0
  251. wbfdm/tests/models/test_exchanges.py +7 -0
  252. wbfdm/tests/models/test_instrument_list.py +117 -0
  253. wbfdm/tests/models/test_instrument_prices.py +306 -0
  254. wbfdm/tests/models/test_instruments.py +202 -0
  255. wbfdm/tests/models/test_merge.py +99 -0
  256. wbfdm/tests/models/test_options.py +69 -0
  257. wbfdm/tests/test_tasks.py +6 -0
  258. wbfdm/tests/tests.py +10 -0
  259. wbfdm/urls.py +222 -0
  260. wbfdm/utils.py +54 -0
  261. wbfdm/viewsets/__init__.py +10 -0
  262. wbfdm/viewsets/configs/__init__.py +5 -0
  263. wbfdm/viewsets/configs/buttons/__init__.py +8 -0
  264. wbfdm/viewsets/configs/buttons/classifications.py +23 -0
  265. wbfdm/viewsets/configs/buttons/exchanges.py +9 -0
  266. wbfdm/viewsets/configs/buttons/instrument_prices.py +49 -0
  267. wbfdm/viewsets/configs/buttons/instruments.py +283 -0
  268. wbfdm/viewsets/configs/display/__init__.py +22 -0
  269. wbfdm/viewsets/configs/display/classifications.py +138 -0
  270. wbfdm/viewsets/configs/display/esg.py +75 -0
  271. wbfdm/viewsets/configs/display/exchanges.py +42 -0
  272. wbfdm/viewsets/configs/display/instrument_lists.py +137 -0
  273. wbfdm/viewsets/configs/display/instrument_prices.py +199 -0
  274. wbfdm/viewsets/configs/display/instrument_requests.py +116 -0
  275. wbfdm/viewsets/configs/display/instruments.py +618 -0
  276. wbfdm/viewsets/configs/display/instruments_relationships.py +65 -0
  277. wbfdm/viewsets/configs/display/monthly_performances.py +72 -0
  278. wbfdm/viewsets/configs/display/officers.py +16 -0
  279. wbfdm/viewsets/configs/display/prices.py +21 -0
  280. wbfdm/viewsets/configs/display/statement_with_estimates.py +101 -0
  281. wbfdm/viewsets/configs/display/statements.py +48 -0
  282. wbfdm/viewsets/configs/endpoints/__init__.py +41 -0
  283. wbfdm/viewsets/configs/endpoints/classifications.py +87 -0
  284. wbfdm/viewsets/configs/endpoints/esg.py +20 -0
  285. wbfdm/viewsets/configs/endpoints/exchanges.py +6 -0
  286. wbfdm/viewsets/configs/endpoints/financials_analysis.py +65 -0
  287. wbfdm/viewsets/configs/endpoints/instrument_lists.py +38 -0
  288. wbfdm/viewsets/configs/endpoints/instrument_prices.py +51 -0
  289. wbfdm/viewsets/configs/endpoints/instrument_requests.py +20 -0
  290. wbfdm/viewsets/configs/endpoints/instruments.py +13 -0
  291. wbfdm/viewsets/configs/endpoints/instruments_relationships.py +31 -0
  292. wbfdm/viewsets/configs/endpoints/statements.py +6 -0
  293. wbfdm/viewsets/configs/menus/__init__.py +9 -0
  294. wbfdm/viewsets/configs/menus/classifications.py +19 -0
  295. wbfdm/viewsets/configs/menus/exchanges.py +10 -0
  296. wbfdm/viewsets/configs/menus/instrument_lists.py +10 -0
  297. wbfdm/viewsets/configs/menus/instruments.py +20 -0
  298. wbfdm/viewsets/configs/menus/instruments_relationships.py +33 -0
  299. wbfdm/viewsets/configs/titles/__init__.py +42 -0
  300. wbfdm/viewsets/configs/titles/classifications.py +79 -0
  301. wbfdm/viewsets/configs/titles/esg.py +11 -0
  302. wbfdm/viewsets/configs/titles/exchanges.py +12 -0
  303. wbfdm/viewsets/configs/titles/financial_ratio_analysis.py +6 -0
  304. wbfdm/viewsets/configs/titles/financials_analysis.py +50 -0
  305. wbfdm/viewsets/configs/titles/instrument_prices.py +50 -0
  306. wbfdm/viewsets/configs/titles/instrument_requests.py +16 -0
  307. wbfdm/viewsets/configs/titles/instruments.py +31 -0
  308. wbfdm/viewsets/configs/titles/instruments_relationships.py +21 -0
  309. wbfdm/viewsets/configs/titles/market_data.py +13 -0
  310. wbfdm/viewsets/configs/titles/prices.py +15 -0
  311. wbfdm/viewsets/configs/titles/statement_with_estimates.py +10 -0
  312. wbfdm/viewsets/esg.py +72 -0
  313. wbfdm/viewsets/exchanges.py +63 -0
  314. wbfdm/viewsets/financial_analysis/__init__.py +3 -0
  315. wbfdm/viewsets/financial_analysis/financial_metric_analysis.py +85 -0
  316. wbfdm/viewsets/financial_analysis/financial_ratio_analysis.py +85 -0
  317. wbfdm/viewsets/financial_analysis/statement_with_estimates.py +145 -0
  318. wbfdm/viewsets/instruments/__init__.py +80 -0
  319. wbfdm/viewsets/instruments/classifications.py +279 -0
  320. wbfdm/viewsets/instruments/financials_analysis.py +614 -0
  321. wbfdm/viewsets/instruments/instrument_lists.py +77 -0
  322. wbfdm/viewsets/instruments/instrument_prices.py +542 -0
  323. wbfdm/viewsets/instruments/instrument_requests.py +51 -0
  324. wbfdm/viewsets/instruments/instruments.py +106 -0
  325. wbfdm/viewsets/instruments/instruments_relationships.py +235 -0
  326. wbfdm/viewsets/instruments/utils.py +27 -0
  327. wbfdm/viewsets/market_data.py +172 -0
  328. wbfdm/viewsets/mixins.py +9 -0
  329. wbfdm/viewsets/officers.py +27 -0
  330. wbfdm/viewsets/prices.py +62 -0
  331. wbfdm/viewsets/statements/__init__.py +1 -0
  332. wbfdm/viewsets/statements/statements.py +100 -0
  333. wbfdm/viewsets/technical_analysis/__init__.py +1 -0
  334. wbfdm/viewsets/technical_analysis/monthly_performances.py +93 -0
  335. wbfdm-2.2.1.dist-info/METADATA +15 -0
  336. wbfdm-2.2.1.dist-info/RECORD +337 -0
  337. wbfdm-2.2.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,711 @@
1
+ import datetime as dt
2
+ import math
3
+ import re
4
+ from typing import Optional
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import plotly.graph_objects as go
9
+ from dateutil.relativedelta import relativedelta
10
+ from django.db.models import ExpressionWrapper, F, FloatField, QuerySet
11
+ from pandas.tseries.offsets import BYearEnd
12
+ from plotly.subplots import make_subplots
13
+ from wbcore.contrib.currency.models import CurrencyFXRates
14
+ from wbfdm.enums import MarketData
15
+ from wbfdm.models import Instrument, InstrumentPrice
16
+
17
+
18
+ class FinancialsChartGenerator:
19
+ def __init__(self, instrument: Instrument):
20
+ self.instrument = instrument
21
+
22
+ def _make_presentable_fields(self, fields_list: list, bold: bool = True) -> pd.Index:
23
+ full_capitalize_fields = self.get_full_capitalize_fields()
24
+ percent_fields = self.get_percent_fields()
25
+ finance_friendly = self._rename_fields_into_finance_friendly()
26
+ pattern = re.compile(r"\b(" + "|".join(finance_friendly.keys()) + r")\b")
27
+
28
+ for i, x in enumerate(fields_list):
29
+ y = pattern.sub(lambda z: finance_friendly[z.group()], x)
30
+ if x in percent_fields:
31
+ x = y + "_(%)"
32
+ else:
33
+ x = y
34
+ fields_list[i] = x
35
+
36
+ fields_list = [x.split("_") for x in fields_list]
37
+ for i, primary_list in enumerate(fields_list):
38
+ for j, word in enumerate(primary_list):
39
+ primary_list[j] = word.upper() if word in full_capitalize_fields else word.capitalize()
40
+
41
+ fields_list[i] = " ".join(primary_list)
42
+ fields_list = [f"<b>{x}</b>" for x in fields_list] if bold else fields_list
43
+ return pd.Index(fields_list)
44
+
45
+ def _make_pretty_table(self, df: pd.DataFrame) -> go.Figure:
46
+ previous_years = [elem for elem in df.columns if elem.find("E") == -1]
47
+ big_int_fields = self.get_big_int_fields()
48
+ percent_fields = self.get_percent_fields()
49
+ multiples_fields = self.get_multiples_fields()
50
+ big_int_fields = df.index.intersection(big_int_fields)
51
+ percent_fields = df.index.intersection(percent_fields)
52
+ multiples_fields = df.index.intersection(multiples_fields)
53
+ other_fields = df.index.difference(big_int_fields.append(percent_fields).append(multiples_fields))
54
+ df.loc[big_int_fields] = (
55
+ df.loc[big_int_fields].div(1000000).applymap(lambda x: f"{x:,.0f}" if not np.isnan(x) else "")
56
+ )
57
+ df.loc[percent_fields] = df.loc[percent_fields].applymap(lambda x: f"{x:,.1%}" if not np.isnan(x) else "")
58
+ df.loc[multiples_fields] = df.loc[multiples_fields].applymap(lambda x: f"{x:,.1f}x" if not np.isnan(x) else "")
59
+ df.loc[other_fields] = df.loc[other_fields].applymap(lambda x: f"{x:,.2f}" if not np.isnan(x) else "")
60
+ df.insert(
61
+ 0,
62
+ f"{self.instrument.name_repr} in USD mn",
63
+ self._make_presentable_fields(fields_list=df.index.tolist()),
64
+ )
65
+ df_table = df.reset_index().drop("index", axis=1)
66
+ df_table.columns = df_table.columns.map("<b>{}</b>".format)
67
+ colors = ["white", "#EEEEF1"] * math.floor(len(df_table.index) / 2)
68
+ colors += ["white"] if len(df_table.index) % 2 == 1 else ""
69
+ fig = go.Figure(
70
+ go.Table(
71
+ columnwidth=[3] + [1] * len(df_table.columns),
72
+ header=dict(
73
+ values=df_table.columns.tolist(),
74
+ line_color="darkslategray",
75
+ fill_color="grey",
76
+ font=dict(color="white", size=13),
77
+ align=["left", "center"],
78
+ height=40,
79
+ ),
80
+ cells=dict(
81
+ values=df.T.values.tolist(),
82
+ align=["left", "center"],
83
+ line_color="darkslategray",
84
+ fill_color=["lightgrey"] + [colors] * len(previous_years) + ["lightyellow"],
85
+ height=30,
86
+ ),
87
+ ),
88
+ )
89
+ return fig
90
+
91
+ @staticmethod
92
+ def _rename_fields_into_finance_friendly() -> dict:
93
+ return {
94
+ "gross_profit_margin": "gross_profit_margin_[non-gaap]",
95
+ "ebitda_margin": "ebitda_margin_[non-gaap]",
96
+ "net_profit_margin": "net_profit_margin_[gaap]",
97
+ "net_profit": "net_profit_[non-gaap]",
98
+ "reported_net_profit": "net_profit_[gaap]",
99
+ "eps": "eps_[non-gaap]",
100
+ "eps_growth": "eps_growth_[non-gaap]",
101
+ }
102
+
103
+ @staticmethod
104
+ def get_big_int_fields() -> list[str]:
105
+ return [
106
+ "revenue",
107
+ "cost_of_good_sold",
108
+ "gross_profit",
109
+ "ebitda",
110
+ "ebit",
111
+ "net_profit",
112
+ "reported_net_profit",
113
+ "cost_research_development",
114
+ "sga",
115
+ "free_cash_flow",
116
+ "cash_from_operation",
117
+ "working_capital",
118
+ "capital_expenditures",
119
+ "investment_cash",
120
+ "financing_cash",
121
+ "shareholder_equity",
122
+ "total_assets",
123
+ "current_liabilities",
124
+ "total_debt",
125
+ "cash_and_cash_equivalents",
126
+ "net_debt",
127
+ "cash_and_short_term_investments",
128
+ "net_change_in_cash",
129
+ "receivables",
130
+ "inventories",
131
+ "payables",
132
+ "current_assets",
133
+ "entreprise_value",
134
+ "free_cash",
135
+ "burn_rate",
136
+ "operating_burn_rate",
137
+ "free_cash_burn_rate",
138
+ "operating_cash_flow",
139
+ "investing_cash_flow",
140
+ "unlevered_free_cash_flow",
141
+ ]
142
+
143
+ @staticmethod
144
+ def get_percent_fields() -> list[str]:
145
+ return [
146
+ "revenue_growth",
147
+ "revenue_growth_5y_cagr",
148
+ "gross_profit_margin",
149
+ "ebitda_margin",
150
+ "ebit_margin",
151
+ "net_profit_margin",
152
+ "free_cash_flow_growth",
153
+ "return_on_equity",
154
+ "return_on_assets",
155
+ "return_on_capital_employed",
156
+ "operating_cash_flow_growth",
157
+ "investing_cash_flow_growth",
158
+ "unlevered_free_cash_flow_growth",
159
+ "unlevered_free_cash_flow_margin",
160
+ "diluted_eps_growth",
161
+ "eps_growth",
162
+ ]
163
+
164
+ @staticmethod
165
+ def get_full_capitalize_fields() -> list[str]:
166
+ return ["ebitda", "ebit", "eps", "sga", "ytd", "[gaap]", "[non-gaap]"]
167
+
168
+ @staticmethod
169
+ def get_multiples_fields() -> list[str]:
170
+ return ["net_debt_to_ebitda_ratio"]
171
+
172
+ def get_latest_year(self, queryset: Optional[QuerySet] = None) -> int:
173
+ # if queryset is None:
174
+ # fundamentals = Fundamental.annual_objects.filter(instrument=self.instrument) # noqa
175
+ # if fundamentals.exists():
176
+ # return fundamentals.latest("period__date_range").period.period_year
177
+ # elif queryset.exists(): # not empty
178
+ # return queryset.latest("period__date_range").period.period_year
179
+
180
+ return dt.date.today().year - 1 # if no data provided, take last previous year
181
+
182
+ def fundamentals_df(self, from_n_years_before: int = 3) -> pd.DataFrame:
183
+ return pd.DataFrame()
184
+ # fundamentals_fields_list = Fundamental.get_number_serializer_fields().keys() # noqa
185
+ # qs_fundamentals = Fundamental.annual_objects.filter(instrument=self.instrument) # noqa
186
+ # latest_year = self.get_latest_year(queryset=qs_fundamentals)
187
+ # qs_fundamentals = (
188
+ # qs_fundamentals.filter(
189
+ # period__period_year__gte=latest_year - from_n_years_before,
190
+ # period__period_year__lte=latest_year,
191
+ # )
192
+ # .order_by("period__period_year")
193
+ # .values(*Fundamental.get_number_serializer_fields().keys(), "period__period_year") # noqa
194
+ # )
195
+ # df_fundamentals = pd.DataFrame(qs_fundamentals)
196
+ # if not df_fundamentals.empty:
197
+ # df_fundamentals["period__period_year"] = df_fundamentals.period__period_year.astype(str)
198
+ # df_fundamentals = df_fundamentals.set_index("period__period_year")[fundamentals_fields_list].T
199
+ # return df_fundamentals
200
+ # return df_fundamentals.reindex(fundamentals_fields_list)
201
+
202
+ def forecasts_df(self) -> pd.DataFrame:
203
+ return pd.DataFrame()
204
+ # forecasts_fields_list = list(Forecast.get_number_serializer_fields().keys()) # noqa
205
+ # latest_year = self.get_latest_year(queryset=None)
206
+ # forecasts = self.instrument.forecasts.filter(revenue_y1__isnull=False)
207
+ # if forecasts.exists():
208
+ # last_forecast = forecasts.latest("date")
209
+ # df_forecasts = pd.DataFrame.from_dict(model_to_dict(last_forecast), orient="index").T[
210
+ # forecasts_fields_list
211
+ # ]
212
+ # else:
213
+ # df_forecasts = pd.DataFrame(index=forecasts_fields_list)
214
+ # forecasts_fields_list += ["reported_net_profit"]
215
+ #
216
+ # def split_year_fields(_year_n: str) -> list:
217
+ # return [field for field in forecasts_fields_list if field.find(_year_n) != -1]
218
+ #
219
+ # next_years = list(str(year) + "E" for year in range(latest_year + 1, latest_year + 6))
220
+ # df_forecasts_ordered = pd.DataFrame(index=forecasts_fields_list)
221
+ #
222
+ # if forecasts.exists():
223
+ # for year, e_year in zip(["_y1", "_y2", "_y3", "_y4", "_y5"], next_years):
224
+ # year_fields = split_year_fields(_year_n=year)
225
+ # index = list(map(lambda x: x.replace(year, ""), year_fields))
226
+ # tmp = pd.DataFrame(index=[e_year], columns=index, data=df_forecasts[year_fields].values.tolist()).T
227
+ # df_forecasts_ordered = pd.concat([df_forecasts_ordered, tmp], axis=1)
228
+ # return df_forecasts_ordered
229
+
230
+ def combine_fundamentals_and_forecasts_df(self) -> pd.DataFrame:
231
+ df_fundamentals = self.fundamentals_df()
232
+ df_forecasts = self.forecasts_df()
233
+
234
+ # Hardcode rename for those which do not have same field name:
235
+ df_forecasts.rename(index={"adjusted_net_profit": "net_profit"}, inplace=True)
236
+
237
+ df_table = pd.concat([df_fundamentals, df_forecasts], axis=1).astype(float)
238
+
239
+ return df_table
240
+
241
+ def get_instrument_price_data(
242
+ self,
243
+ from_date: dt.date | None = None,
244
+ to_date: dt.date | None = None,
245
+ ) -> pd.DataFrame:
246
+ if not from_date:
247
+ from_date = dt.date(dt.date.today().year - 1, 1, 1)
248
+
249
+ df_prices = pd.DataFrame(
250
+ Instrument.objects.filter(id=self.instrument.id).dl.market_data(
251
+ values=[MarketData.CLOSE, MarketData.OPEN, MarketData.LOW, MarketData.HIGH, MarketData.VOLUME],
252
+ from_date=from_date,
253
+ to_date=to_date,
254
+ )
255
+ )
256
+ if df_prices.empty:
257
+ return pd.DataFrame()
258
+ df_prices = df_prices[
259
+ [
260
+ MarketData.CLOSE.value,
261
+ MarketData.OPEN.value,
262
+ MarketData.LOW.value,
263
+ MarketData.HIGH.value,
264
+ MarketData.VOLUME.value,
265
+ "valuation_date",
266
+ ]
267
+ ].rename(columns={"valuation_date": "date"})
268
+ df_prices = df_prices.set_index("date").sort_index().astype(float)
269
+ timeline = pd.date_range(df_prices.index[0], df_prices.index[-1])
270
+ df_prices = df_prices.reindex(timeline, method="ffill")
271
+ return df_prices[df_prices["close"] != 0]
272
+
273
+ def summary_table_chart(self) -> go.Figure:
274
+ df_table = self.combine_fundamentals_and_forecasts_df()
275
+
276
+ if not df_table.empty:
277
+ df_table.loc["reported_net_profit", :].replace([np.nan, np.inf, -np.inf], None, inplace=True)
278
+ df_table = df_table.loc[
279
+ [
280
+ "revenue",
281
+ "revenue_growth",
282
+ "gross_profit_margin",
283
+ "ebitda_margin",
284
+ "reported_net_profit",
285
+ "net_profit_margin",
286
+ "net_profit",
287
+ "eps",
288
+ "return_on_equity",
289
+ "return_on_assets",
290
+ "return_on_capital_employed",
291
+ "return_on_invested_capital",
292
+ "net_debt_to_ebitda_ratio",
293
+ "interest_coverage_ratio",
294
+ "employee_count",
295
+ "employee_count_growth",
296
+ ],
297
+ :,
298
+ ]
299
+ df_table.loc["eps_growth", :] = df_table.loc["eps", :].pct_change()
300
+ df_table = df_table.reindex(
301
+ [
302
+ "revenue",
303
+ "revenue_growth",
304
+ "gross_profit_margin",
305
+ "ebitda_margin",
306
+ "reported_net_profit",
307
+ "net_profit_margin",
308
+ "net_profit",
309
+ "eps",
310
+ "eps_growth",
311
+ "return_on_equity",
312
+ "return_on_assets",
313
+ "return_on_capital_employed",
314
+ "return_on_invested_capital",
315
+ "net_debt_to_ebitda_ratio",
316
+ "interest_coverage_ratio",
317
+ "employee_count",
318
+ "employee_count_growth",
319
+ ]
320
+ )
321
+
322
+ fig = self._make_pretty_table(df=df_table)
323
+ return fig
324
+
325
+ @staticmethod
326
+ def replace_hovertemplate(fig: go.Figure, i_position: int, text: str) -> go.Figure:
327
+ try:
328
+ fig["data"][i_position]["hovertemplate"] = text
329
+ except IndexError:
330
+ pass
331
+ return fig
332
+
333
+ def financials_chart(self) -> go.Figure:
334
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
335
+ df_table = self.combine_fundamentals_and_forecasts_df()
336
+ if not df_table.empty:
337
+ df_table = df_table.loc[["revenue", "net_profit", "ebitda_margin", "net_profit_margin"], :]
338
+
339
+ for variable in ["revenue", "net_profit"]:
340
+ text = "Revenue" if variable == "revenue" else "Net Profit"
341
+ fig.add_bar(
342
+ y=df_table.loc[variable, :].dropna().div(1000000).round(2).values,
343
+ x=df_table.loc[variable, :].dropna().index,
344
+ name="Revenue" if variable == "revenue" else "Net Profit",
345
+ yaxis="y",
346
+ hovertemplate="<b>" + text + "</b><br>%{x}<br>%{y:.2f} Mio.<extra></extra>",
347
+ )
348
+ for variable in ["ebitda_margin", "net_profit_margin"]:
349
+ text = "EBITDA Margin" if variable == "ebitda_margin" else "Net Profit Margin"
350
+ fig.add_scatter(
351
+ y=df_table.loc[variable, :].dropna().mul(100).values,
352
+ x=df_table.loc[variable, :].dropna().index,
353
+ name=text,
354
+ yaxis="y2",
355
+ hovertemplate="<b>" + text + "</b><br>%{x}<br>%{y:.2f}%<extra></extra>",
356
+ )
357
+ fig.update_xaxes(nticks=len(df_table.columns))
358
+ symbol = self.instrument.currency.symbol
359
+ fig.update_yaxes(title=f"{symbol if symbol else ''} in Million", secondary_y=False)
360
+ fig.update_yaxes(ticksuffix="%", secondary_y=True)
361
+ return fig
362
+
363
+ def profitability_ratios_chart(self) -> go.Figure:
364
+ df_table = self.combine_fundamentals_and_forecasts_df()
365
+ if not df_table.empty:
366
+ df_table = df_table.loc[
367
+ ["return_on_equity", "return_on_assets", "return_on_capital_employed", "return_on_invested_capital"]
368
+ ]
369
+
370
+ df_table.rename(
371
+ index={
372
+ "return_on_equity": "ROE",
373
+ "return_on_assets": "ROA",
374
+ "return_on_capital_employed": "ROCE",
375
+ "return_on_invested_capital": "ROIC",
376
+ },
377
+ inplace=True,
378
+ )
379
+ fig = go.Figure()
380
+ for index_name in df_table.index:
381
+ fig.add_trace(
382
+ go.Scatter(
383
+ x=df_table.loc[index_name, :].dropna().index,
384
+ y=df_table.loc[index_name, :].mul(100).dropna().values,
385
+ mode="lines+markers",
386
+ name=index_name,
387
+ hovertemplate=f"<b>{index_name}</b>" + "<br>%{y:.2f}%<extra></extra>",
388
+ )
389
+ )
390
+ fig.update_yaxes(ticksuffix="%")
391
+ fig.update_layout(hovermode="x")
392
+ return fig
393
+
394
+ def stock_performance_summary_chart(self) -> go.Figure:
395
+ df_prices = self.get_instrument_price_data()
396
+ if df_prices.empty:
397
+ return go.Figure()
398
+ last_day = df_prices.index[-1]
399
+ last_year_idx = df_prices.index.get_indexer([(last_day - relativedelta(years=1))], method="nearest")[
400
+ 0
401
+ ] # one year from last price date
402
+ last_month_idx = df_prices.index.get_indexer([(last_day - relativedelta(months=1))], method="nearest")[0]
403
+ three_months_idx = df_prices.index.get_indexer([(last_day - relativedelta(months=3))], method="nearest")[0]
404
+ six_months_idx = df_prices.index.get_indexer([(last_day - relativedelta(months=6))], method="nearest")[0]
405
+ ytd_idx = df_prices.index.get_indexer([(last_day - BYearEnd())], method="nearest")[0]
406
+ performances = pd.Series(name="performance", dtype=float)
407
+
408
+ performances["1_month"] = df_prices.iloc[-1]["close"] / df_prices.iloc[last_month_idx]["close"] - 1
409
+ performances["3_months"] = df_prices.iloc[-1]["close"] / df_prices.iloc[three_months_idx]["close"] - 1
410
+ performances["6_months"] = df_prices.iloc[-1]["close"] / df_prices.iloc[six_months_idx]["close"] - 1
411
+ performances["12_months"] = df_prices.iloc[-1]["close"] / df_prices.iloc[last_year_idx]["close"] - 1
412
+ performances["ytd"] = df_prices.iloc[-1]["close"] / df_prices.iloc[ytd_idx]["close"] - 1
413
+ if df_prices.index[ytd_idx] > df_prices.index[last_month_idx]:
414
+ performances = performances.reindex(["ytd", "1_month", "3_months", "6_months", "12_months"])
415
+ elif df_prices.index[ytd_idx] > df_prices.index[three_months_idx]:
416
+ performances = performances.reindex(["1_month", "ytd", "3_months", "6_months", "12_months"])
417
+ elif df_prices.index[ytd_idx] > df_prices.index[six_months_idx]:
418
+ performances = performances.reindex(["1_month", "3_months", "ytd", "6_months", "12_months"])
419
+ else:
420
+ performances = performances.reindex(["1_month", "3_months", "6_months", "ytd", "12_months"])
421
+ colors = performances.copy()
422
+ colors.loc[performances >= 0], colors.loc[performances < 0] = "green", "darkred"
423
+
424
+ performances = performances.mul(100).round(2)
425
+ fig = go.Figure(
426
+ go.Bar(
427
+ x=self._make_presentable_fields(fields_list=performances.index.tolist(), bold=False),
428
+ y=performances.values,
429
+ texttemplate="%{y:.2f}%",
430
+ marker={"color": colors},
431
+ )
432
+ )
433
+ fig.update_yaxes(ticksuffix="%")
434
+ fig.update_layout(title=f'Last Price Date: {last_day.strftime("%Y-%m-%d")}')
435
+ return fig
436
+
437
+ def price_and_volume_chart(
438
+ self,
439
+ from_date: dt.date | None = None,
440
+ to_date: dt.date | None = None,
441
+ benchmarks: Optional[list[str]] = None,
442
+ normalize: bool = False,
443
+ short_sma: Optional[int] = 50,
444
+ long_sma: Optional[int] = 200,
445
+ candle_chart: bool = False,
446
+ overlay_volume: bool = False,
447
+ ) -> go.Figure:
448
+ pd.options.plotting.backend = "plotly"
449
+ df_prices = self.get_instrument_price_data(from_date=from_date, to_date=to_date).sort_index()
450
+ if df_prices.empty:
451
+ return go.Figure()
452
+
453
+ df_prices = df_prices.asfreq("B")
454
+ df_prices["short_sma"] = df_prices.close.rolling(short_sma).mean() if short_sma else pd.NA
455
+ df_prices["long_sma"] = df_prices.close.rolling(long_sma).mean() if long_sma else pd.NA
456
+ df_prices["volume_diff"] = df_prices.volume.diff()
457
+ df_prices = df_prices.loc[from_date:to_date]
458
+
459
+ bar_chart_name = "Volume"
460
+ df_prices["color_volume"] = "green"
461
+ if self.instrument.is_managed:
462
+ bar_chart_name = "Inflow/Outflow"
463
+ df_prices.loc[df_prices.volume < 0, "color_volume"] = "red"
464
+ else:
465
+ perf = df_prices.close.pct_change()
466
+ df_prices.loc[perf < 0, "color_volume"] = "red"
467
+ if overlay_volume:
468
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
469
+ else:
470
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.03, row_width=[0.2, 0.7])
471
+ if not candle_chart:
472
+ instrument_prices = df_prices.loc[:, "close"].dropna()
473
+ cumulative_returns = instrument_prices.pct_change().add(1).cumprod().sub(1).mul(100).fillna(0)
474
+ subfig1 = go.Scatter(
475
+ x=instrument_prices.round(2).index,
476
+ y=instrument_prices.round(2).values,
477
+ mode="lines",
478
+ name=f"{self.instrument.name_repr} - Close Price",
479
+ customdata=cumulative_returns,
480
+ hovertemplate=(
481
+ f"<b>{self.instrument.name_repr} - Close Price</b>"
482
+ + "<br>%{x}<br>%{y}<br><i>Cumulative Return</i>: %{customdata:.2f}%<extra></extra>"
483
+ ),
484
+ fill="tozeroy",
485
+ fillcolor="rgba(0,0,255,0.15)",
486
+ )
487
+ else:
488
+ subfig1 = go.Candlestick(
489
+ x=df_prices.index,
490
+ open=df_prices["open"].dropna(),
491
+ high=df_prices["high"].dropna(),
492
+ low=df_prices["low"].dropna(),
493
+ close=df_prices["close"].dropna(),
494
+ name=f"{self.instrument.name_repr}",
495
+ )
496
+
497
+ fig.add_trace(subfig1, row=1, col=1) if not overlay_volume else fig.add_trace(subfig1, secondary_y=False)
498
+
499
+ for sma in ["short_sma", "long_sma"]:
500
+ name_str = f"Short SMA ({short_sma} days)" if sma == "short_sma" else f"Long SMA ({long_sma} days)"
501
+ sma_fig = go.Scatter(
502
+ x=df_prices.loc[:, sma].dropna().round(2).index,
503
+ y=df_prices.loc[:, sma].dropna().round(2).values,
504
+ mode="lines",
505
+ name=name_str,
506
+ hovertemplate=f"<b>{name_str}</b>" + "<br>%{x}<br>%{y}<extra></extra>",
507
+ )
508
+ fig.add_trace(sma_fig, row=1, col=1) if not overlay_volume else fig.add_trace(sma_fig, secondary_y=False)
509
+
510
+ benchmarks = [] if not benchmarks else benchmarks
511
+ qs_benchmarks = (
512
+ InstrumentPrice.objects.filter(
513
+ instrument__in=benchmarks,
514
+ date__range=[from_date, to_date],
515
+ calculated=False,
516
+ )
517
+ .order_by("date", "instrument")
518
+ .annotate(
519
+ fx_rate=CurrencyFXRates.get_fx_rates_subquery_for_two_currencies(
520
+ "date", "instrument__currency", self.instrument.currency
521
+ ),
522
+ convert_value=ExpressionWrapper(F("net_value") * F("fx_rate"), output_field=FloatField()),
523
+ )
524
+ .values("date", "instrument__name_repr", "convert_value")
525
+ )
526
+ if qs_benchmarks.exists():
527
+ df_benchmarks = pd.DataFrame(qs_benchmarks).set_index(["date", "instrument__name_repr"])
528
+ df_benchmarks = df_benchmarks.convert_value.unstack("instrument__name_repr").astype(float).asfreq("B")
529
+ for benchmark in df_benchmarks.columns:
530
+ df_benchmark = df_benchmarks.loc[:, benchmark].dropna()
531
+ benchmark_cumulative_returns = df_benchmark.pct_change().add(1).cumprod().sub(1).mul(100).fillna(0)
532
+ if normalize and df_prices.index[0] and df_prices.index[0] <= df_benchmark.index[0]:
533
+ mul_factor = df_prices.loc[df_benchmark.index[0], "close"] / df_benchmark.iat[0]
534
+ df_benchmark *= mul_factor
535
+ benchmark_fig = go.Scatter(
536
+ x=df_benchmark.index,
537
+ y=df_benchmark.round(2).values,
538
+ mode="lines",
539
+ name=benchmark,
540
+ customdata=benchmark_cumulative_returns,
541
+ hovertemplate=(
542
+ f"<b>{benchmark}</b>"
543
+ + "<br>%{x}<br>%{y}<br><i>Cumulative Return</i>: %{customdata:.2f}%<extra></extra>"
544
+ ),
545
+ )
546
+ (
547
+ fig.add_trace(benchmark_fig, row=1, col=1)
548
+ if not overlay_volume
549
+ else fig.add_trace(benchmark_fig, secondary_y=False)
550
+ )
551
+ subfig2 = go.Bar(
552
+ x=df_prices.loc[:, "close"].dropna().round(2).index,
553
+ y=df_prices.loc[:, "volume"].dropna().round(2).values,
554
+ marker={"color": df_prices.color_volume, "opacity": 0.4},
555
+ name=bar_chart_name,
556
+ hovertemplate="<b>" + bar_chart_name + "</b><br>%{x}<br>%{y:.4s}<extra></extra>",
557
+ )
558
+ fig.add_trace(subfig2, row=2, col=1) if not overlay_volume else fig.add_trace(subfig2, secondary_y=True)
559
+
560
+ symbol = self.instrument.currency.symbol
561
+ if overlay_volume:
562
+ fig.update_yaxes(ticksuffix=symbol if symbol else "", title="Price", secondary_y=False)
563
+ fig.update_yaxes(tickformat=".3s", title="Volume", showgrid=False, secondary_y=True)
564
+ else:
565
+ fig.update_yaxes(ticksuffix=symbol if symbol else "", title="Price", row=1, col=1)
566
+ fig.update_yaxes(tickformat=".3s", title="Volume", row=2, col=1)
567
+ fig.update_yaxes(spikesnap="cursor")
568
+ fig.update_xaxes(spikesnap="cursor")
569
+ fig.update_layout(xaxis_rangeslider_visible=False)
570
+ fig.update_layout(showlegend=False)
571
+ fig.update_layout(yaxis=dict(range=[df_prices.close.min(), df_prices.close.max()]))
572
+ return fig
573
+
574
+ def instrument_vs_benchmark_prices_chart(
575
+ self,
576
+ benchmark: Optional[Instrument] = None,
577
+ from_date: dt.date | None = None,
578
+ to_date: dt.date | None = None,
579
+ ) -> go.Figure:
580
+ df_prices = self.get_instrument_price_data(from_date=from_date, to_date=to_date)
581
+ if df_prices.empty:
582
+ return go.Figure()
583
+ pd.options.plotting.backend = "plotly"
584
+ df_prices = df_prices.asfreq("B").close.to_frame()
585
+ df_prices = df_prices.loc[from_date:to_date]
586
+ if benchmark and (benchmark_prices := benchmark.prices.filter(date__gte=from_date, date__lte=to_date)):
587
+ df_benchmark_prices = pd.DataFrame(benchmark_prices.values("date", "close"))
588
+ df_benchmark_prices = (
589
+ df_benchmark_prices.set_index("date").rename(columns={"close": benchmark.name_repr}).astype(float)
590
+ )
591
+ df_prices = df_prices.join(df_benchmark_prices)
592
+ instrument_name = self.instrument.name_repr
593
+ df_prices = df_prices.rename(columns={"close": instrument_name})
594
+ df_prices = df_prices.pct_change().add(1).cumprod().sub(1).fillna(0)
595
+ fig = df_prices[f"{instrument_name}"].mul(100).plot.line()
596
+ fig.update_traces(
597
+ hovertemplate=f"<b>{instrument_name}</b>" + "<br>%{y:.2f}%<extra></extra>",
598
+ )
599
+ if benchmark and df_prices.columns.isin([f"{benchmark.name_repr}"]).any():
600
+ fig.add_trace(
601
+ go.Scatter(
602
+ x=df_prices.loc[:, benchmark.name_repr].dropna().index,
603
+ y=df_prices.loc[:, benchmark.name_repr].mul(100).dropna().values,
604
+ mode="lines",
605
+ name=benchmark.name_repr,
606
+ hovertemplate=f"<b>{benchmark.name_repr}</b>" + "<br>%{y:.2f}%<extra></extra>",
607
+ )
608
+ )
609
+ fig.update_yaxes(title=None, ticksuffix="%")
610
+ fig.update_xaxes(title="Time")
611
+ fig.update_layout(legend_title_text=None, hovermode="x unified")
612
+ return fig
613
+
614
+ def cash_flow_analysis_table_chart(self):
615
+ df_table = self.combine_fundamentals_and_forecasts_df()
616
+ if not df_table.empty:
617
+ df_table = df_table.loc[
618
+ [
619
+ "revenue",
620
+ "revenue_growth",
621
+ "ebitda",
622
+ "ebitda_margin",
623
+ "ebit",
624
+ "capital_expenditures",
625
+ "cash_from_operation",
626
+ "investment_cash",
627
+ "free_cash_flow",
628
+ "interest_expense",
629
+ "company_tax_rate",
630
+ ],
631
+ :,
632
+ ]
633
+
634
+ df_table.loc["depreciation_and_amortization", :] = df_table.loc["ebitda", :] - df_table.loc["ebit", :]
635
+ df_table.loc["operating_cash_flow_growth", :] = df_table.loc["cash_from_operation", :].pct_change()
636
+ df_table.loc["investing_cash_flow_growth", :] = df_table.loc["investment_cash", :].pct_change()
637
+ df_table.loc["capex_/_depreciation_and_amortization", :] = (
638
+ df_table.loc["capital_expenditures", :] / df_table.loc["depreciation_and_amortization", :]
639
+ )
640
+ df_table.loc["unlevered_free_cash_flow", :] = df_table.loc["free_cash_flow", :] + df_table.loc[
641
+ "interest_expense", :
642
+ ] * (1 - df_table.loc["company_tax_rate", :])
643
+ df_table.loc["unlevered_free_cash_flow_growth", :] = df_table.loc[
644
+ "unlevered_free_cash_flow", :
645
+ ].pct_change()
646
+ df_table.loc["unlevered_free_cash_flow_margin", :] = (
647
+ df_table.loc["unlevered_free_cash_flow", :] / df_table.loc["revenue", :]
648
+ )
649
+ df_table.drop(
650
+ [
651
+ "depreciation_and_amortization",
652
+ "capital_expenditures",
653
+ "ebit",
654
+ "interest_expense",
655
+ "company_tax_rate",
656
+ "free_cash_flow",
657
+ ],
658
+ axis=0,
659
+ inplace=True,
660
+ )
661
+ fig = self._make_pretty_table(df=df_table)
662
+ return fig
663
+
664
+ def cash_flow_analysis_bar_chart(self):
665
+ fig = go.Figure()
666
+ df_table = self.combine_fundamentals_and_forecasts_df()
667
+ if not df_table.empty:
668
+ df_table.loc["unlevered_free_cash_flow", :] = df_table.loc["free_cash_flow", :] + df_table.loc[
669
+ "interest_expense", :
670
+ ] * (1 - df_table.loc["company_tax_rate", :])
671
+ df_table = df_table.loc[["cash_from_operation", "investment_cash", "unlevered_free_cash_flow"], :]
672
+ df_table.index = self._make_presentable_fields(df_table.index.tolist(), bold=False)
673
+ for variable in df_table.index:
674
+ y = df_table.loc[variable, :].dropna().div(1000000).round(2).values
675
+ fig.add_bar(
676
+ y=y,
677
+ x=df_table.loc[variable, :].dropna().index,
678
+ name=variable,
679
+ hovertemplate="<b>" + variable + "</b><br>%{x}<br>%{y:.2f} Mio.<extra></extra>",
680
+ text=y,
681
+ textposition="auto",
682
+ )
683
+ symbol = self.instrument.currency.symbol
684
+ fig.update_yaxes(title=f"{symbol if symbol else ''} in Million")
685
+ return fig
686
+
687
+ def net_debt_and_ebitda_chart(self):
688
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
689
+ df_table = self.combine_fundamentals_and_forecasts_df()
690
+ if not df_table.empty:
691
+ df_table = df_table.loc[["net_debt", "net_debt_to_ebitda_ratio"], :]
692
+ net_debt_values = df_table.loc["net_debt", :].dropna().div(1000000).round(2).values
693
+ fig.add_bar(
694
+ x=df_table.loc["net_debt", :].dropna().index,
695
+ y=net_debt_values,
696
+ name="Net debt",
697
+ hovertemplate="<b>Net Debt</b><br>%{x}<br>%{y:.2f} Mio.<extra></extra>",
698
+ text=net_debt_values,
699
+ textposition="auto",
700
+ )
701
+ fig.add_scatter(
702
+ x=df_table.loc["net_debt_to_ebitda_ratio", :].dropna().index,
703
+ y=df_table.loc["net_debt_to_ebitda_ratio", :].dropna().mul(100).values,
704
+ name="Net debt / EBITDA",
705
+ yaxis="y2",
706
+ hovertemplate="<b>Net debt / EBITDA</b><br>%{x}<br>%{y:.2f}%<extra></extra>",
707
+ )
708
+ symbol = self.instrument.currency.symbol
709
+ fig.update_yaxes(title=f"{symbol if symbol else ''} in Million", secondary_y=False)
710
+ fig.update_yaxes(ticksuffix="%", secondary_y=True)
711
+ return fig