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,614 @@
1
+ from datetime import timedelta
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import plotly.express as px
6
+ import plotly.graph_objects as go
7
+ from plotly.subplots import make_subplots
8
+ from wbcore import viewsets
9
+ from wbcore.utils.date import get_date_interval_from_request
10
+ from wbfdm.figures.financials import FinancialsChartGenerator
11
+ from wbfdm.figures.financials.financial_analysis_charts import (
12
+ FinancialAnalysisGenerator,
13
+ PeriodChoices,
14
+ VariableChoices,
15
+ )
16
+ from wbfdm.filters import (
17
+ EarningsAnalysisFilterSet,
18
+ FinancialAnalysisFilterSet,
19
+ FinancialAnalysisValuationRatiosFilterSet,
20
+ )
21
+ from wbfdm.models import Instrument
22
+ from wbfdm.viewsets.configs import (
23
+ CashFlowAnalysisInstrumentBarChartEndpointConfig,
24
+ CashFlowAnalysisInstrumentBarChartTitle,
25
+ CashFlowAnalysisInstrumentTableChartEndpointConfig,
26
+ CashFlowAnalysisInstrumentTableChartTitle,
27
+ EarningsInstrumentChartEndpointConfig,
28
+ EarningsInstrumentChartTitle,
29
+ FinancialAnalysisGeneratorTitleConfig,
30
+ FinancialsGraphInstrumentChartEndpointConfig,
31
+ FinancialsGraphInstrumentChartTitle,
32
+ NetDebtAndEbitdaInstrumentChartEndpointConfig,
33
+ NetDebtAndEbitdaInstrumentChartTitle,
34
+ ProfitabilityRatiosInstrumentChartEndpointConfig,
35
+ ProfitabilityRatiosInstrumentChartTitle,
36
+ SummaryTableInstrumentChartEndpointConfig,
37
+ SummaryTableInstrumentChartTitle,
38
+ )
39
+
40
+ from ..mixins import InstrumentMixin
41
+
42
+
43
+ class SummaryTableInstrumentChartViewSet(InstrumentMixin, viewsets.ChartViewSet):
44
+ queryset = Instrument.objects.all()
45
+ title_config_class = SummaryTableInstrumentChartTitle
46
+ endpoint_config_class = SummaryTableInstrumentChartEndpointConfig
47
+ filterset_class = FinancialAnalysisFilterSet
48
+
49
+ def get_queryset(self):
50
+ return Instrument.objects.filter(id=self.instrument.id)
51
+
52
+ def get_plotly(self, queryset):
53
+ instrument = queryset.first()
54
+ instance_generator = FinancialsChartGenerator(instrument=instrument)
55
+ fig = instance_generator.summary_table_chart()
56
+ return fig
57
+
58
+
59
+ class FinancialsGraphInstrumentChartViewSet(SummaryTableInstrumentChartViewSet):
60
+ title_config_class = FinancialsGraphInstrumentChartTitle
61
+ endpoint_config_class = FinancialsGraphInstrumentChartEndpointConfig
62
+
63
+ def get_plotly(self, queryset):
64
+ instrument = queryset.first()
65
+ instance_generator = FinancialsChartGenerator(instrument=instrument)
66
+ fig = instance_generator.financials_chart()
67
+ return fig
68
+
69
+
70
+ class ProfitabilityRatiosInstrumentChartViewSet(SummaryTableInstrumentChartViewSet):
71
+ title_config_class = ProfitabilityRatiosInstrumentChartTitle
72
+ endpoint_config_class = ProfitabilityRatiosInstrumentChartEndpointConfig
73
+
74
+ def get_plotly(self, queryset):
75
+ instrument = queryset.first()
76
+ instance_generator = FinancialsChartGenerator(instrument=instrument)
77
+ fig = instance_generator.profitability_ratios_chart()
78
+ return fig
79
+
80
+
81
+ class CashFlowAnalysisInstrumentTableViewSet(SummaryTableInstrumentChartViewSet):
82
+ title_config_class = CashFlowAnalysisInstrumentTableChartTitle
83
+ endpoint_config_class = CashFlowAnalysisInstrumentTableChartEndpointConfig
84
+
85
+ def get_plotly(self, queryset):
86
+ instrument = queryset.first()
87
+ instance_generator = FinancialsChartGenerator(instrument=instrument)
88
+ fig = instance_generator.cash_flow_analysis_table_chart()
89
+ return fig
90
+
91
+
92
+ class CashFlowAnalysisInstrumentBarChartViewSet(SummaryTableInstrumentChartViewSet):
93
+ title_config_class = CashFlowAnalysisInstrumentBarChartTitle
94
+ endpoint_config_class = CashFlowAnalysisInstrumentBarChartEndpointConfig
95
+
96
+ def get_plotly(self, queryset):
97
+ instrument = queryset.first()
98
+ instance_generator = FinancialsChartGenerator(instrument=instrument)
99
+ fig = instance_generator.cash_flow_analysis_bar_chart()
100
+ return fig
101
+
102
+
103
+ class NetDebtAndEbitdaInstrumentChartViewSet(SummaryTableInstrumentChartViewSet):
104
+ title_config_class = NetDebtAndEbitdaInstrumentChartTitle
105
+ endpoint_config_class = NetDebtAndEbitdaInstrumentChartEndpointConfig
106
+
107
+ def get_plotly(self, queryset):
108
+ instrument = queryset.first()
109
+ instance_generator = FinancialsChartGenerator(instrument=instrument)
110
+ fig = instance_generator.net_debt_and_ebitda_chart()
111
+ return fig
112
+
113
+
114
+ class ValuationRatiosChartView(InstrumentMixin, viewsets.ChartViewSet):
115
+ queryset = Instrument.objects.all()
116
+ title_config_class = FinancialAnalysisGeneratorTitleConfig
117
+ filterset_class = FinancialAnalysisValuationRatiosFilterSet
118
+ LIST_DOCUMENTATION = "wbfdm/markdown/documentation/financial_analysis_instrument_ratios.md"
119
+
120
+ def get_plotly(self, queryset):
121
+ # Set plotly as the default plotting lib
122
+ pd.options.plotting.backend = "plotly"
123
+
124
+ # GET data from fake filters
125
+ date1, date2 = get_date_interval_from_request(self.request)
126
+ if not date1 or not date2:
127
+ return go.Figure()
128
+ period = getattr(PeriodChoices, self.request.GET.get("period", "NTM"), PeriodChoices.NTM)
129
+ output = self.request.GET.get("output", "CHART")
130
+ vs_related = self.request.GET.get("vs_related", "false") == "true"
131
+ clean_data = self.request.GET.get("clean_data", "true") == "true"
132
+ ranges = self.request.GET.get("ranges", "false") == "true"
133
+ range_type = self.request.GET.get("range_type", "MINMAX")
134
+ range_period = int(self.request.GET.get("range_period", "120"))
135
+ x_axis_var = getattr(VariableChoices, self.request.GET.get("x_axis_var", "EPSG"), VariableChoices.EPSG)
136
+ x_axis, x_axis_title, x_axis_format = x_axis_var.lower(), x_axis_var.chart_label, x_axis_var.format
137
+ y_axis_var = getattr(VariableChoices, self.request.GET.get("y_axis_var", "PE"), VariableChoices.PE)
138
+ y_axis, y_axis_title, y_axis_format = y_axis_var.lower(), y_axis_var.chart_label, y_axis_var.format
139
+ z_axis_var = getattr(VariableChoices, self.request.GET.get("z_axis_var", "MKTCAP"), VariableChoices.MKTCAP)
140
+ z_axis, z_axis_title, z_axis_format = z_axis_var.lower(), z_axis_var.chart_label, z_axis_var.format
141
+ median = self.request.GET.get("median", "true") == "true"
142
+
143
+ # Fail early if filter combination doesn't make sense or the period is too large for the peer chart
144
+ if output == "TSTABLE" and vs_related:
145
+ fig = go.Figure()
146
+ fig.update_layout(
147
+ title_text="To provide the best user experience, you may not use <b>'versus related'</b> instruments in the <b>'time-series'</b> table view."
148
+ )
149
+ return fig
150
+
151
+ if output == "CHART" and vs_related and ranges:
152
+ fig = go.Figure()
153
+ fig.update_layout(
154
+ title_text="You may only <b>'draw ranges'</b> for a single instrument <b>('versus related' = NO)</b>."
155
+ )
156
+ return fig
157
+
158
+ if output == "CHART" and vs_related and (date2 - date1).days > 900:
159
+ fig = go.Figure()
160
+ fig.update_layout(
161
+ title_text="To provide the best user experience, it is possible to display <b>up to 2.5 calendar years</b> in the chart view with <b>'versus related = YES'</b>."
162
+ )
163
+ return fig
164
+
165
+ if output == "CHART" and ranges and range_type == "ROLLING" and range_period < 1:
166
+ fig = go.Figure()
167
+ fig.update_layout(title_text="<b>Rolling period</b> cannot be lower than 1.")
168
+ return fig
169
+
170
+ # Get all ratios
171
+ generator = FinancialAnalysisGenerator([self.instrument], date1, date2, vs_related)
172
+ ratios = generator.get_common_valuation_ratios_df(period, clean_data)
173
+
174
+ # If ratios are empty, return early
175
+ if ratios.empty:
176
+ fig = go.Figure()
177
+ fig.update_layout(title_text="No data available.")
178
+ return fig
179
+
180
+ # Label dates
181
+ ratios["datetxt"] = pd.to_datetime(ratios["date"]).dt.strftime("%Y-%m-%d")
182
+
183
+ if output == FinancialAnalysisValuationRatiosFilterSet.OutputChoices.TSTABLE:
184
+ fig = go.Figure(
185
+ data=[
186
+ go.Table(
187
+ header=dict(
188
+ values=[
189
+ "Date",
190
+ "Price-to-Earnings PE " + period,
191
+ "Price-to-Earnings-to-Growth PEG " + period,
192
+ "Price-to-Sales PS " + period,
193
+ "EV-to-EBITDA " + period,
194
+ "Price-to-FreeCashflow P/FCF " + period,
195
+ ]
196
+ ),
197
+ cells=dict(
198
+ values=[
199
+ ratios["datetxt"],
200
+ ratios["pe"].astype(float).round(1).replace([np.nan], None).replace([None], ""),
201
+ ratios["peg"].astype(float).round(2).replace([np.nan], None).replace([None], ""),
202
+ ratios["ps"].astype(float).round(1).replace([np.nan], None).replace([None], ""),
203
+ ratios["evebitda"].astype(float).round(1).replace([np.nan], None).replace([None], ""),
204
+ ratios["pfcf"].astype(float).round(1).replace([np.nan], None).replace([None], ""),
205
+ ],
206
+ ),
207
+ ),
208
+ ]
209
+ )
210
+ return fig
211
+
212
+ elif output == FinancialAnalysisValuationRatiosFilterSet.OutputChoices.TABLE:
213
+ ratios = ratios.groupby("instrument").tail(1)
214
+ ratios = ratios.reset_index(drop=True)
215
+
216
+ # If table and additional instruments, add median and mean rows
217
+ if vs_related:
218
+ ratios.fillna(value=np.nan, inplace=True)
219
+ ratios_median = ratios.median(numeric_only=True, axis=0)
220
+ ratios.loc["Mean"] = ratios.mean(numeric_only=True, axis=0)
221
+ ratios.loc[ratios.index[-1], "instrument_title"] = "Mean"
222
+ ratios.loc["Median"] = ratios_median
223
+ ratios.loc[ratios.index[-1], "instrument_title"] = "Median"
224
+ ratios.rename(index={"Mean": ratios["date"][0]}, inplace=True)
225
+ ratios.rename(index={"Median": ratios["date"][0]}, inplace=True)
226
+ ratios.replace(np.nan, None, inplace=True)
227
+
228
+ # Round all variables to 1 decimal but peg
229
+ columns = ratios.columns.difference(["instrument", "instrument_title", "peg", "date", "datetxt"])
230
+ for col in columns:
231
+ ratios[col] = ratios[col].astype(float).round(1).replace([np.nan], None).replace([None], "")
232
+ ratios["peg"] = ratios["peg"].astype(float).round(2).replace([np.nan], None).replace([None], "")
233
+
234
+ # last two lines in bold
235
+ if vs_related:
236
+ for i in [0, -2, -1]:
237
+ row = ratios.iloc[i, :].values
238
+ bold_row = ["<b>" + str(entry) + "</b>" for entry in row]
239
+ ratios.iloc[i, :] = bold_row
240
+
241
+ return go.Figure(
242
+ data=[
243
+ go.Table(
244
+ header=dict(
245
+ values=[
246
+ period + " on " + str(ratios["datetxt"].head(1).item()),
247
+ "Price-to-Earnings PE",
248
+ "Price-to-Earnings-to-Growth PEG",
249
+ "Price-to-Sales PS",
250
+ "EV-to-EBITDA",
251
+ "Price-to-FreeCashFlow P/FCF",
252
+ ]
253
+ ),
254
+ cells=dict(
255
+ values=[
256
+ ratios["instrument_title"],
257
+ ratios["pe"],
258
+ ratios["peg"],
259
+ ratios["ps"],
260
+ ratios["evebitda"],
261
+ ratios["pfcf"],
262
+ ],
263
+ ),
264
+ )
265
+ ]
266
+ )
267
+
268
+ else:
269
+ # Calculate custom axis ranges for better visual representation
270
+ def axis_range(series, type="lower"):
271
+ if type == "lower":
272
+ a = series.min()
273
+ if pd.isna(a):
274
+ return 0
275
+ if a > 0:
276
+ _range = 0.75 * float(str(a).replace("nan", "0"))
277
+ else:
278
+ _range = 1.25 * float(str(a).replace("nan", "-1"))
279
+ else:
280
+ a = series.max()
281
+ if pd.isna(a):
282
+ return 1
283
+ if a > 0:
284
+ _range = 1.25 * float(str(a).replace("nan", "30"))
285
+ else:
286
+ _range = 0.75 * float(str(a).replace("nan", "0"))
287
+ return _range
288
+
289
+ if vs_related:
290
+ # Get biweekly dates to run the loop for plotly sliders
291
+ def daterange(start_date, end_date):
292
+ for n in range(1 + int((end_date - start_date).days / 14)):
293
+ if n == len(range(int((end_date - start_date).days / 14))):
294
+ yield end_date
295
+ yield start_date + timedelta(14 * n)
296
+
297
+ fig = go.Figure()
298
+ dates = []
299
+ for index, single_date in enumerate(daterange(date1, date2)):
300
+ ratio_trace = ratios[
301
+ (ratios["datetxt"] == single_date.strftime("%Y-%m-%d")) & (ratios[z_axis] > 0)
302
+ ]
303
+ if (
304
+ not ratio_trace.empty
305
+ and not all(v is None for v in ratio_trace[x_axis])
306
+ and not all(v is None for v in ratio_trace[y_axis])
307
+ ):
308
+ dates.append(single_date.strftime("%Y-%m-%d"))
309
+ ratio_main = ratio_trace.head(1)
310
+ ratio_other = ratio_trace.iloc[1:]
311
+ fig.add_trace(
312
+ go.Scatter(
313
+ visible=False,
314
+ x=ratio_main[x_axis],
315
+ y=ratio_main[y_axis],
316
+ customdata=np.stack((ratio_main["instrument_title"], ratio_main[x_axis]), axis=-1),
317
+ mode="markers",
318
+ marker=dict(
319
+ color="green",
320
+ opacity=0.5,
321
+ size=ratio_main[z_axis].astype(float),
322
+ sizeref=(2.0 * np.nanmax(ratios[z_axis].fillna(0))) / (20.0**2),
323
+ ),
324
+ hovertemplate="<br>".join(
325
+ [
326
+ s.replace(" ", "&nbsp;")
327
+ for s in [
328
+ "<b>%{customdata[0]}</b>",
329
+ "X: <b>" + x_axis_title + ":</b> %{x:" + str(x_axis_format),
330
+ "Y: <b>" + y_axis_title + ":</b> %{y:" + str(y_axis_format),
331
+ "Bubble: <b>"
332
+ + z_axis_title
333
+ + ":</b> %{marker.size:"
334
+ + str(z_axis_format)
335
+ + "<extra></extra>",
336
+ ]
337
+ ]
338
+ ),
339
+ )
340
+ )
341
+ colors = px.colors.qualitative.Pastel
342
+ fig.add_trace(
343
+ go.Scatter(
344
+ visible=False,
345
+ name="Ratios: " + str(ratio_trace["datetxt"]),
346
+ x=ratio_other[x_axis],
347
+ y=ratio_other[y_axis],
348
+ customdata=np.stack((ratio_other["instrument_title"], ratio_other[x_axis]), axis=-1),
349
+ mode="markers",
350
+ marker=dict(
351
+ color=colors[0 : len(ratio_other["instrument_title"])],
352
+ opacity=0.5,
353
+ size=ratio_other[z_axis].astype(float),
354
+ sizeref=(2.0 * np.nanmax(ratios[z_axis].fillna(0))) / (20.0**2),
355
+ ),
356
+ hovertemplate="<br>".join(
357
+ [
358
+ s.replace(" ", "&nbsp;")
359
+ for s in [
360
+ "<b>%{customdata[0]}</b>",
361
+ "X: <b>" + x_axis_title + ":</b> %{x:" + str(x_axis_format),
362
+ "Y: <b>" + y_axis_title + ":</b> %{y:" + str(y_axis_format),
363
+ "Bubble: <b>"
364
+ + z_axis_title
365
+ + ":</b> %{marker.size:"
366
+ + str(z_axis_format)
367
+ + "<extra></extra>",
368
+ ]
369
+ ]
370
+ ),
371
+ )
372
+ )
373
+ fig.update_layout(title_text="Relative Valuation <b>" + period + "</b>", showlegend=False)
374
+ fig.update_xaxes(
375
+ title_text=x_axis_title + " <b>" + period + "</b>",
376
+ hoverformat=x_axis_format,
377
+ tickformat=x_axis_format.split("}")[0],
378
+ range=[axis_range(ratios[x_axis], type="lower"), axis_range(ratios[x_axis], type="upper")],
379
+ )
380
+ fig.update_yaxes(
381
+ title_text=y_axis_title + " <b>" + period + "</b>",
382
+ hoverformat=y_axis_title,
383
+ tickformat=y_axis_format.split("}")[0],
384
+ range=[axis_range(ratios[y_axis], type="lower"), axis_range(ratios[y_axis], type="upper")],
385
+ )
386
+ fig.add_hline(y=0, line_width=0.15, line_color="grey")
387
+ fig.add_vline(x=0, line_width=0.15, line_color="grey")
388
+ if median:
389
+ fig.add_hline(y=ratios[y_axis].median(), line_width=1, line_dash="dash", line_color="grey")
390
+ fig.add_vline(x=ratios[x_axis].median(), line_width=1, line_dash="dash", line_color="grey")
391
+
392
+ if len(dates) == 0:
393
+ fig.update_layout(title_text="No data available.")
394
+ return fig
395
+ else:
396
+ steps = []
397
+ for i in range(len(dates)):
398
+ step = dict(
399
+ method="update",
400
+ args=[
401
+ {"visible": [False] * len(dates) * 2},
402
+ {"name": "Relative Valuation <b>" + period + "</b>. Chosen date: " + str(dates[i])},
403
+ ],
404
+ )
405
+ step["args"][0]["visible"][i * 2] = True
406
+ step["args"][0]["visible"][(i * 2) + 1] = True
407
+ steps.append(step)
408
+
409
+ sliders = [
410
+ dict(active=len(dates) - 1, currentvalue={"prefix": "Date: "}, pad={"t": 50}, steps=steps)
411
+ ]
412
+
413
+ fig.data[-1].visible = True
414
+ fig.data[-2].visible = True
415
+
416
+ fig.update_layout(sliders=sliders)
417
+
418
+ for i in range(len(dates)):
419
+ fig["layout"]["sliders"][0]["steps"][i]["label"] = str(dates[i])
420
+
421
+ return fig
422
+
423
+ if not ranges:
424
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
425
+
426
+ for index, plot in enumerate(
427
+ [
428
+ ("pe", "#ff6361", VariableChoices.PE.chart_label, VariableChoices.PE, False),
429
+ ("peg", "#ffa600", VariableChoices.PEG.chart_label, VariableChoices.PEG, True),
430
+ ("ps", "#58508d", VariableChoices.PS.chart_label, VariableChoices.PS, False),
431
+ ("evebitda", "#003f5c", VariableChoices.EVEBITDA.chart_label, VariableChoices.EVEBITDA, False),
432
+ ("pfcf", "#bc5090", VariableChoices.PFCF.chart_label, VariableChoices.PFCF, False),
433
+ ],
434
+ start=1,
435
+ ):
436
+ fig.add_trace(
437
+ go.Scatter(
438
+ x=ratios["date"],
439
+ y=ratios[plot[0]],
440
+ line=dict(color=plot[1], width=2),
441
+ name=plot[2] + " " + period,
442
+ hovertemplate="%{x}: <b>" + plot[3] + " " + period + "</b> %{y} <extra></extra>",
443
+ ),
444
+ secondary_y=plot[4],
445
+ )
446
+
447
+ fig.update_layout(title_text="Relative Valuation " + period, yaxis_ticksuffix="x")
448
+ fig.update_yaxes(title_text="Ratios " + period, hoverformat=".2f", secondary_y=False)
449
+ if abs(axis_range(ratios["peg"], type="upper")) >= 1:
450
+ ticks = ".1f"
451
+ else:
452
+ ticks = ".2f"
453
+ fig.update_yaxes(
454
+ title_text="PEG " + period,
455
+ hoverformat=".2f",
456
+ tickformat=ticks,
457
+ title_font_color="#ffa600",
458
+ secondary_y=True,
459
+ )
460
+ fig.update_yaxes(
461
+ rangemode="tozero", scaleanchor="y2", scaleratio=0.1, constraintoward="bottom", secondary_y=False
462
+ )
463
+ fig.update_yaxes(
464
+ rangemode="tozero", scaleanchor="y", scaleratio=1, constraintoward="bottom", secondary_y=True
465
+ )
466
+ fig.add_hline(y=0, line_width=3, line_dash="dash", line_color="grey")
467
+ fig["layout"]["yaxis2"]["showgrid"] = False
468
+ return fig
469
+
470
+ fig = make_subplots(rows=5, cols=1, vertical_spacing=0.01, shared_xaxes=True)
471
+ x_range = [ratios["date"].iloc[0], ratios["date"].iloc[-1]]
472
+
473
+ for index, plot in enumerate(
474
+ [
475
+ ("pe", "#ff6361"),
476
+ ("peg", "#ffa600"),
477
+ ("ps", "#58508d"),
478
+ ("evebitda", "#003f5c"),
479
+ ("pfcf", "#bc5090"),
480
+ ],
481
+ start=1,
482
+ ):
483
+ fig.add_trace(
484
+ go.Scatter(x=ratios["date"], y=ratios[plot[0]], line=dict(color=plot[1], width=2)),
485
+ row=index,
486
+ col=1,
487
+ )
488
+ if range_type == FinancialAnalysisValuationRatiosFilterSet.RangeChoices.MINMAX:
489
+ if not ratios[plot[0]].isnull().all():
490
+ fig.add_trace(
491
+ go.Scatter(
492
+ x=x_range,
493
+ y=[ratios[plot[0]].max(), ratios[plot[0]].max()],
494
+ mode="lines",
495
+ line_width=1,
496
+ line_dash="dash",
497
+ line_color=plot[1],
498
+ ),
499
+ row=index,
500
+ col=1,
501
+ )
502
+ fig.add_trace(
503
+ go.Scatter(
504
+ x=x_range,
505
+ y=[ratios[plot[0]].min(), ratios[plot[0]].min()],
506
+ mode="lines",
507
+ line_width=1,
508
+ line_dash="dash",
509
+ line_color=plot[1],
510
+ ),
511
+ row=index,
512
+ col=1,
513
+ )
514
+ else:
515
+ fig.add_trace(
516
+ go.Scatter(
517
+ x=ratios["date"],
518
+ y=ratios[plot[0]].rolling(range_period, min_periods=1).max().fillna(""),
519
+ mode="lines",
520
+ line_width=0.75,
521
+ line_dash="dash",
522
+ line_color=plot[1],
523
+ ),
524
+ row=index,
525
+ col=1,
526
+ )
527
+ fig.add_trace(
528
+ go.Scatter(
529
+ x=ratios["date"],
530
+ y=ratios[plot[0]].rolling(range_period, min_periods=1).min().fillna(""),
531
+ mode="lines",
532
+ line_width=0.75,
533
+ line_dash="dash",
534
+ line_color=plot[1],
535
+ ),
536
+ row=index,
537
+ col=1,
538
+ )
539
+
540
+ fig.update_layout(
541
+ yaxis=dict(title_text="P/E", ticksuffix="x", hoverformat=".1f"),
542
+ yaxis2=dict(title_text="PEG", tickformat=".1f", hoverformat=".1f"),
543
+ yaxis3=dict(title_text="P/S", ticksuffix="x", hoverformat=".1f"),
544
+ yaxis4=dict(title_text="EV/EBITDA", ticksuffix="x", hoverformat=".1f"),
545
+ yaxis5=dict(title_text="P/FCF", ticksuffix="x", hoverformat=".1f"),
546
+ )
547
+
548
+ fig.update_traces(hovertemplate="%{x}: %{y} <extra></extra>")
549
+
550
+ if range_type == FinancialAnalysisValuationRatiosFilterSet.RangeChoices.MINMAX:
551
+ fig.update_layout(
552
+ title_text=period + " Valuation ratios with Min/Max range", title_x=0, showlegend=False
553
+ )
554
+ else:
555
+ fig.update_layout(
556
+ title_text=period + " Valuation ratios with " + str(range_period) + " days rolling min-max range",
557
+ title_x=0,
558
+ showlegend=False,
559
+ )
560
+
561
+ return fig
562
+
563
+
564
+ class EarningsInstrumentChartViewSet(InstrumentMixin, viewsets.ChartViewSet):
565
+ title_config_class = EarningsInstrumentChartTitle
566
+ endpoint_config_class = EarningsInstrumentChartEndpointConfig
567
+ queryset = Instrument.objects.all()
568
+ filterset_class = EarningsAnalysisFilterSet
569
+ LIST_DOCUMENTATION = "wbfdm/markdown/documentation/earnings_instrument.md"
570
+
571
+ def get_plotly(self, queryset):
572
+ # GET data from fake filters
573
+ date1, date2 = get_date_interval_from_request(self.request)
574
+ if not date1 or not date2:
575
+ return go.Figure()
576
+ period = getattr(PeriodChoices, self.request.GET.get("period", "NTM"), PeriodChoices.NTM)
577
+ analysis = self.request.GET.get("output", "EPS")
578
+ vs_related = self.request.GET.get("vs_related", "false") == "true"
579
+
580
+ # Get all earnings data
581
+ generator = FinancialAnalysisGenerator([self.instrument], date1, date2, vs_related)
582
+ earnings = generator.get_earnings_df(period, clean_data=True)
583
+
584
+ # If earnings are empty, return early
585
+ if earnings.empty:
586
+ fig = go.Figure()
587
+ fig.update_layout(title_text="No data available.")
588
+ return fig
589
+
590
+ if period == PeriodChoices.TTM:
591
+ hover_helper = " EPS "
592
+ title_helper = "Reported Earnings "
593
+ else:
594
+ hover_helper = " Consensus EPS "
595
+ title_helper = "Consensus Earnings Estimates "
596
+
597
+ fig = go.Figure()
598
+ earnings = earnings.set_index(["instrument", "instrument_title", "date"]).unstack(level=1)
599
+ earnings = earnings.reset_index().drop("instrument", axis=1).replace(np.nan, None).set_index("date")
600
+ if analysis == "EPS":
601
+ for col in earnings.columns:
602
+ fig.add_trace(
603
+ go.Scatter(
604
+ x=earnings.index,
605
+ y=earnings[col],
606
+ name=col[1],
607
+ hovertemplate="%{x}: <b>" + col[1] + hover_helper + period + "</b> %{y} <extra></extra>",
608
+ ),
609
+ )
610
+
611
+ fig.update_layout(title_text=title_helper + period, yaxis_tickprefix="$")
612
+ fig.update_yaxes(title_text="EPS ($) " + period, hoverformat=".2f", tickformat=".2f")
613
+
614
+ return fig