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,285 @@
1
+ import math
2
+ import re
3
+ from datetime import date
4
+ from typing import Generator, List, Optional
5
+
6
+ import DatastreamPy as dsweb
7
+ import numpy as np
8
+ import pandas as pd
9
+ from wbcore.contrib.currency.models import Currency, CurrencyFXRates
10
+
11
+
12
+ class Client:
13
+ MAXIMUM_ITEMS_PER_BUNDLE: int = 500
14
+ MAXIMUM_REQUESTS_PER_BUNDLE: int = 20
15
+ MAXIMUM_INSTRUMENTS_PER_REQUEST: int = 50
16
+ MAXIMUM_DATATYPES_PER_REQUEST: int = 50
17
+ MAXIMUM_ITEMS_PER_REQUEST: int = 100
18
+ PRICE_ERROR_MARGIN: float = 0.10
19
+ IBUNIT_DEFAULT_UNIT: float = 1e6
20
+
21
+ def __init__(self, username: Optional[str] = None, password: Optional[str] = None):
22
+ self.connection = dsweb.DataClient(None, username, password)
23
+ super().__init__()
24
+
25
+ @classmethod
26
+ def get_chunked_list(cls, identifiers: list[str], fields_number: int) -> list[list[str]]:
27
+ instruments_number = len(identifiers)
28
+ max_chunks_number = instruments_number * fields_number / cls.MAXIMUM_ITEMS_PER_BUNDLE
29
+ chunk_size = int(instruments_number / max_chunks_number) - 1
30
+ res = []
31
+ for x in range(0, len(identifiers), chunk_size):
32
+ res.append(identifiers[x : x + chunk_size])
33
+ return res
34
+
35
+ @classmethod
36
+ def _breakdown_requests(
37
+ cls, tickers: List[str], symbols: List[str]
38
+ ) -> Generator[tuple[list[str], list[str]], None, None]:
39
+ """
40
+ Helper function to generate tuple of ticker and symbols that respect the API data usage
41
+ Args:
42
+ tickers: List of tickers to fetch
43
+ symbols: Corresponding list of symbol to fetch
44
+
45
+ Returns:
46
+ Yield tuple of list of string
47
+ """
48
+ if len(tickers) * len(symbols) > cls.MAXIMUM_ITEMS_PER_BUNDLE:
49
+ raise ValueError(f"The maximum number of items for a bundle is {cls.MAXIMUM_ITEMS_PER_BUNDLE}")
50
+
51
+ if (
52
+ len(tickers) * len(symbols) <= cls.MAXIMUM_ITEMS_PER_REQUEST
53
+ and len(tickers) <= cls.MAXIMUM_INSTRUMENTS_PER_REQUEST
54
+ ):
55
+ yield tickers, symbols
56
+ else:
57
+ nb_tickers = min(
58
+ math.floor(cls.MAXIMUM_ITEMS_PER_REQUEST / len(symbols)),
59
+ len(tickers),
60
+ cls.MAXIMUM_INSTRUMENTS_PER_REQUEST,
61
+ )
62
+ yield tickers[0:nb_tickers], symbols
63
+ yield from cls._breakdown_requests(tickers[nb_tickers:], symbols)
64
+
65
+ def get_last_fx_rate(self, base_currency_key: str, target_currency_key: str):
66
+ last_currency_rate = CurrencyFXRates.objects.latest("date").date
67
+ try:
68
+ base_currency = Currency.objects.get(key=base_currency_key)
69
+ target_currency = Currency.objects.get(key=target_currency_key)
70
+ return float(base_currency.convert(last_currency_rate, target_currency, exact_lookup=True))
71
+ except Currency.DoesNotExist:
72
+ return 1.0
73
+
74
+ def _process_raw_requests(self, tickers: list[str], fields: list[str], **extra_client_kwargs) -> pd.DataFrame:
75
+ """
76
+ Utility function to fetch data in bulk, aggregate and clean the result
77
+ Args:
78
+ tickers: Instruments tickers
79
+ fields: Symbols
80
+ **extra_client_kwargs: Extra keyword arguments to be passed down to the client (e.g. frequency)
81
+
82
+ Returns:
83
+ An aggregated and cleaned dataframe with [Dates, Instrument] as index for timeserie and [Instrument] As index for static data
84
+ """
85
+ requests_data = list(self._breakdown_requests(tickers, fields))
86
+ reqs = []
87
+ if len(requests_data) > self.MAXIMUM_REQUESTS_PER_BUNDLE:
88
+ raise ValueError(f"number of request exceed {self.MAXIMUM_REQUESTS_PER_BUNDLE}")
89
+ # Construct the requests bundle
90
+ for request_tickers, request_symbols in requests_data:
91
+ # Convert a list of string into a valid string
92
+ converted_ticker = ",".join(request_tickers)
93
+ if "start" in extra_client_kwargs or "end" in extra_client_kwargs:
94
+ reqs.append(
95
+ self.connection.post_user_request(tickers=converted_ticker, fields=fields, **extra_client_kwargs)
96
+ )
97
+ else:
98
+ reqs.append(self.connection.post_user_request(tickers=converted_ticker, fields=fields, kind=0))
99
+ # concat the bundle results
100
+ res = self.connection.get_bundle_data(bundleRequest=reqs)
101
+
102
+ df = pd.DataFrame()
103
+ if res:
104
+ if "start" in extra_client_kwargs or "end" in extra_client_kwargs:
105
+ res = list(filter(lambda subdf: subdf.index.name == "Dates", res))
106
+ if res:
107
+ df = pd.concat(res)
108
+ if df.index.name == "Dates":
109
+ df = (
110
+ pd.melt(df, ignore_index=False)
111
+ .reset_index()
112
+ .rename(columns={"value": "Value", "Field": "Datatype"})
113
+ )
114
+ df.Value = df.Value.apply(lambda x: None if str(x).startswith("$$ER") else x)
115
+ if not df.empty:
116
+ df = df[df.Value.notnull()]
117
+ df = df.replace({"NA": None})
118
+ if "Dates" in df.columns:
119
+ df.Dates = pd.to_datetime(df.Dates, utc=True)
120
+ indexes = ["Instrument", "Dates"]
121
+ else:
122
+ indexes = ["Instrument"]
123
+ df = pd.pivot_table(
124
+ df, values="Value", index=indexes, columns="Datatype", aggfunc="first", dropna=False
125
+ )
126
+ return df
127
+
128
+ def _normalize_df_units(
129
+ self,
130
+ df: pd.DataFrame,
131
+ ibes_non_per_share_fields: list[str] | None = None,
132
+ ibes_currency_based_fields: list[str] | None = None,
133
+ ibes_fields: list[str] | None = None,
134
+ **extra_client_kwargs,
135
+ ) -> pd.DataFrame:
136
+ """
137
+ Datastream use arbitrary units for some symbols where the denominator needs to be fetch in two steps:
138
+ * Using FIELD#U to get the datastream unit
139
+ * if ibes_non_per_share_fields is provided, fetch for the static symbol IBUNIT and normalized the given columns with the result times a constant (100000)
140
+ * if ibes_currency_based_fields is provided, fetch for the static symbol IBCUR and normalized the given columns latest found currency rate.
141
+ This function uses the index named "Instrument" as ticker fields and the columns as symbols. Start, End or Frequency needs to be passed down using the extra_client_kwargs argument
142
+
143
+ Args:
144
+ df: The dataframe to be normalized
145
+ ibes_non_per_share_fields: Columns that need ibunit normalization. Defaults to None
146
+ ibes_currency_based_fields: Columns that need IBES Currency conversion from static field IBCUR
147
+ **extra_client_kwargs: Extra keywords arguments to be passed down to the client (e.g. frequency)
148
+
149
+ Returns:
150
+ A normalized dataframe (e.g. volume are in shares and not a million of shares)
151
+ """
152
+ if not ibes_fields:
153
+ ibes_fields = []
154
+ df = df.copy()
155
+ fields = df.columns.unique().tolist()
156
+ tickers = df.index.get_level_values("Instrument").unique().tolist()
157
+ dfu = self._process_raw_requests(tickers, list(map(lambda x: x + "#U", fields)), **extra_client_kwargs).fillna(
158
+ 1
159
+ )
160
+ dfu = dfu[dfu.index.isin(df.index)]
161
+ if not dfu.empty:
162
+ dfu = dfu.rename(columns=lambda x: x.replace("#U", "")).fillna(1)
163
+ dfu = dfu[dfu.columns.intersection(df.columns)]
164
+ dff = df[dfu.columns].multiply(dfu, fill_value=1)
165
+
166
+ # We do this to ensure that not provided data (nan) are not set with the multiplication with the U matrix
167
+ dff[df.isnull()] = df[df.isnull()]
168
+ dff[df.columns.difference(dff.columns)] = df[
169
+ df.columns.difference(dff.columns)
170
+ ] # We ensure that the inital colums from df are appended to the new dataframe if missing from dfu. (happens when the colum has non number values)
171
+ df = dff
172
+
173
+ # If the symbol is computed directly from refinitiv, we need to normalize it as well given the formula (define as the field name)
174
+ for mav_field in list(filter(lambda x: "MAV#" in x, fields)):
175
+ re_matches = re.findall(r"X\(([^\)]+)\)", mav_field)
176
+ if len(re_matches) > 0 and re_matches[0] in dfu.columns:
177
+ df.loc[:, mav_field] = df.loc[:, mav_field] * dfu.loc[:, re_matches[0]]
178
+
179
+ if ibes_non_per_share_fields:
180
+ df_ibunit = self.get_static_df(tickers, ["IBUNIT"]).fillna(1)
181
+ if not df_ibunit.empty:
182
+ df_ibunit = df_ibunit.set_index("Instrument")["IBUNIT"]
183
+ df[df.columns.intersection(ibes_non_per_share_fields)] = (
184
+ df[df.columns.intersection(ibes_non_per_share_fields)].multiply(
185
+ df_ibunit, axis=0, level="Instrument"
186
+ )
187
+ * self.IBUNIT_DEFAULT_UNIT
188
+ )
189
+ ibes_fields = list(set(ibes_fields + ibes_non_per_share_fields))
190
+ if ibes_currency_based_fields:
191
+ df_ibcur = self.get_static_df(tickers, ["IBCUR", "ISOCUR"]).replace(
192
+ "BPN", "GBX"
193
+ ) # BPN == GBX but our db only support the latter
194
+ if not df_ibcur.empty:
195
+ df_ibcur["rate"] = 1
196
+ different_curr_idx = df_ibcur["IBCUR"] != df_ibcur["ISOCUR"]
197
+ df_ibcur.loc[different_curr_idx, "rate"] = df_ibcur.loc[different_curr_idx].apply(
198
+ lambda x: self.get_last_fx_rate(x["IBCUR"], x["ISOCUR"]), axis=1
199
+ )
200
+ df[df.columns.intersection(ibes_currency_based_fields)] = df[
201
+ df.columns.intersection(ibes_currency_based_fields)
202
+ ].multiply(df_ibcur.set_index("Instrument")["rate"], axis=0, level="Instrument")
203
+ ibes_fields = list(set(ibes_fields + ibes_currency_based_fields))
204
+ if ibes_fields:
205
+ df[df.columns.intersection(ibes_fields)] = (
206
+ df[df.columns.intersection(ibes_fields)] * self.IBUNIT_DEFAULT_UNIT
207
+ )
208
+ return df
209
+
210
+ def get_static_df(self, tickers: List[str], fields: List[str], **kwargs) -> pd.DataFrame:
211
+ """
212
+ Public function to returns a dataframe for static symbols
213
+
214
+ Args:
215
+ tickers: Ticker to fetch
216
+ fields: Static symbols to fetch
217
+
218
+ Returns:
219
+ A valid dataframe result
220
+ """
221
+ # Breakdown tickers and fields into a valid datastream parameters subsets
222
+ return self._process_raw_requests(tickers, fields).reset_index()
223
+
224
+ def get_timeserie_df(
225
+ self,
226
+ tickers: List[str],
227
+ fields: List[str],
228
+ ibes_non_per_share_fields: Optional[list[str]] = None,
229
+ ibes_currency_based_fields: Optional[list[str]] = None,
230
+ ibes_fields: list[str] | None = None,
231
+ **extra_client_kwargs,
232
+ ) -> pd.DataFrame:
233
+ """
234
+ Public function to get timeserie type data
235
+ Args:
236
+ tickers: Instrument tickers
237
+ fields: Symbols to fetch
238
+ ibes_non_per_share_fields: Columns that need ibunit normalization. Defaults to None
239
+ ibes_currency_based_fields: Columns that need IBES Currency conversion from static field IBCUR
240
+ **extra_client_kwargs: Extra keywords arguments to be passed down to the client (e.g. frequency)
241
+
242
+ Returns:
243
+ The result as a dataframe
244
+ """
245
+ if (start := extra_client_kwargs.get("start", None)) and isinstance(start, date):
246
+ extra_client_kwargs["start"] = start.strftime("%Y-%m-%d")
247
+ if (end := extra_client_kwargs.get("end", None)) and isinstance(end, date):
248
+ extra_client_kwargs["end"] = end.strftime("%Y-%m-%d")
249
+
250
+ final_df_list = []
251
+ # Sometime, we get too many fields and therefore, we need to split them based on the maximum number of symbols allowed per request
252
+ for splited_fields in np.array_split(fields, math.ceil((len(fields) / self.MAXIMUM_DATATYPES_PER_REQUEST))):
253
+ # Breakdown tickers and fields into a valid datastream parameters subsets
254
+ df = self._process_raw_requests(tickers, list(splited_fields), **extra_client_kwargs)
255
+ if not df.empty:
256
+ df = self._normalize_df_units(
257
+ df,
258
+ ibes_non_per_share_fields=ibes_non_per_share_fields,
259
+ ibes_currency_based_fields=ibes_currency_based_fields,
260
+ ibes_fields=ibes_fields,
261
+ **extra_client_kwargs,
262
+ )
263
+ df = (
264
+ df.reset_index()
265
+ .rename_axis(None)
266
+ .replace([np.inf, -np.inf, np.nan], None)
267
+ .dropna(how="all", subset=df.columns.intersection(splited_fields))
268
+ )
269
+ if not df.empty:
270
+ if "Dates" in df.columns:
271
+ df = df.set_index(["Instrument", "Dates"])
272
+ else: # otherwise it's not a timeseries and we set index only on instrument
273
+ df = df.set_index(["Instrument"])
274
+ final_df_list.append(df)
275
+ if final_df_list:
276
+ return pd.concat(final_df_list, axis=1).reset_index()
277
+ return pd.DataFrame()
278
+
279
+ def raw_fetch(
280
+ self, tickers: List[str], fields: List[str], start: Optional[date] = None, end: Optional[date] = None
281
+ ):
282
+ """
283
+ Utility function to expose the client directly without any modification
284
+ """
285
+ return self.connection.get_data(tickers=tickers, fields=fields, start=start, end=end)
File without changes
File without changes
@@ -0,0 +1,87 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+ from typing import Iterator
4
+
5
+ from wbcore.contrib.dataloader.dataloaders import Dataloader
6
+ from wbfdm.dataloaders.protocols import MarketDataProtocol
7
+ from wbfdm.dataloaders.types import MarketDataDict
8
+ from wbfdm.enums import MarketData
9
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
10
+
11
+ MarketDataMap = {
12
+ "SHARES_OUTSTANDING": "internal_outstanding_shares",
13
+ "OPEN": "net_value",
14
+ "CLOSE": "net_value",
15
+ "HIGH": "net_value",
16
+ "LOW": "net_value",
17
+ "BID": "net_value",
18
+ "ASK": "net_value",
19
+ "VOLUME": "internal_volume",
20
+ "MARKET_CAPITALIZATION": "market_capitalization",
21
+ }
22
+
23
+ DEFAULT_VALUES = [MarketData[name] for name in MarketDataMap.keys()]
24
+
25
+
26
+ def _cast_decimal_to_float(value: float | Decimal) -> float:
27
+ if isinstance(value, Decimal):
28
+ value = float(value)
29
+ return value
30
+
31
+
32
+ class MarketDataDataloader(MarketDataProtocol, Dataloader):
33
+ def market_data(
34
+ self,
35
+ values: list[MarketData] | None = [MarketData.CLOSE],
36
+ from_date: date | None = None,
37
+ to_date: date | None = None,
38
+ exact_date: date | None = None,
39
+ calculated: bool | None = None,
40
+ **kwargs,
41
+ ) -> Iterator[MarketDataDict]:
42
+ """Get prices for instruments.
43
+
44
+ Args:
45
+ values (list[MarketData]): List of values to include in the results.
46
+ from_date (date | None): The starting date for filtering prices. Defaults to None.
47
+ to_date (date | None): The ending date for filtering prices. Defaults to None.
48
+ frequency (Frequency): The frequency of the requested data
49
+
50
+ Returns:
51
+ Iterator[MarketDataDict]: An iterator of dictionaries conforming to the DailyValuationDict.
52
+ """
53
+ prices = InstrumentPrice.objects.filter(instrument__in=self.entities).annotate_market_data() # type: ignore
54
+ if calculated is not None:
55
+ prices = prices.filter(calculated=calculated)
56
+ else:
57
+ prices = prices.filter_only_valid_prices()
58
+
59
+ prices = prices.order_by("date")
60
+ if not values:
61
+ values = DEFAULT_VALUES
62
+ values_map = {value.name: MarketDataMap[value.name] for value in values if value.name in MarketDataMap}
63
+ if exact_date:
64
+ prices = prices.filter(date=exact_date)
65
+ else:
66
+ if from_date:
67
+ prices = prices.filter(date__gte=from_date)
68
+ if to_date:
69
+ prices = prices.filter(date__lte=to_date)
70
+ for row in prices.filter_only_valid_prices().values(
71
+ "date",
72
+ "instrument",
73
+ "calculated",
74
+ *set(values_map.values()),
75
+ ):
76
+ external_id = row.pop("instrument")
77
+ val_date = row.pop("date")
78
+ if row:
79
+ yield MarketDataDict(
80
+ id=f"{external_id}_{val_date}",
81
+ valuation_date=val_date,
82
+ instrument_id=external_id,
83
+ external_id=external_id,
84
+ source="wbfdm",
85
+ calculated=row["calculated"],
86
+ **{MarketData[k].value: _cast_decimal_to_float(row[v]) for k, v in values_map.items()},
87
+ )
File without changes
@@ -0,0 +1,2 @@
1
+ from .metrics import InstrumentMetricModelAdmin, InstrumentMetricGenericInline
2
+ from .instruments import InstrumentModelAdmin
@@ -0,0 +1,12 @@
1
+ from django.contrib import admin
2
+ from wbfdm.admin.instruments import InstrumentModelAdmin as BaseInstrumentModelAdmin
3
+ from wbfdm.models.instruments import Instrument
4
+
5
+ from .metrics import InstrumentMetricGenericInline
6
+
7
+ admin.site.unregister(Instrument)
8
+
9
+
10
+ @admin.register(Instrument)
11
+ class InstrumentModelAdmin(BaseInstrumentModelAdmin):
12
+ inlines = (*BaseInstrumentModelAdmin.inlines, InstrumentMetricGenericInline)
@@ -0,0 +1,43 @@
1
+ from django.contrib import admin
2
+ from django.contrib.contenttypes.admin import GenericTabularInline
3
+
4
+ from ..models import InstrumentMetric
5
+
6
+
7
+ # Register your models here.
8
+ class InstrumentMetricGenericInline(GenericTabularInline):
9
+ model = InstrumentMetric
10
+
11
+ ordering = ["-date", "key"]
12
+ ct_field = "basket_content_type"
13
+ ct_fk_field = "basket_id"
14
+ raw_id_fields = ["basket_content_type", "instrument", "parent_metric"]
15
+
16
+ fields = [
17
+ "date",
18
+ "key",
19
+ "metrics",
20
+ ]
21
+
22
+ def get_queryset(self, request):
23
+ return super().get_queryset(request).select_related("basket_content_type", "instrument")
24
+
25
+
26
+ @admin.register(InstrumentMetric)
27
+ class InstrumentMetricModelAdmin(admin.ModelAdmin):
28
+ list_filter = ("key",)
29
+ search_fields = ("instrument__computed_str",)
30
+ list_display = (
31
+ "basket_content_type",
32
+ "basket_id",
33
+ "instrument",
34
+ "date",
35
+ "key",
36
+ )
37
+
38
+ autocomplete_fields = [
39
+ "basket_content_type",
40
+ "instrument",
41
+ ]
42
+ ordering = ("-date", "-key")
43
+ raw_id_fields = ["basket_content_type", "instrument", "parent_metric"]
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.module_loading import autodiscover_modules
3
+
4
+
5
+ class MetricConfig(AppConfig):
6
+ default_auto_field = "django.db.models.BigAutoField"
7
+ name = "wbfdm.contrib.metric"
8
+
9
+ def ready(self) -> None:
10
+ autodiscover_modules("metric.backends")
@@ -0,0 +1,2 @@
1
+ from .performances import InstrumentPerformanceMetricBackend
2
+ from .statistics import InstrumentFinancialStatisticsMetricBackend
@@ -0,0 +1,159 @@
1
+ from contextlib import suppress
2
+ from datetime import date
3
+ from decimal import Decimal
4
+ from typing import Any, Generator, Generic, Type, TypeVar
5
+
6
+ import pandas as pd
7
+ from django.contrib.contenttypes.models import ContentType
8
+ from django.db.models import F, Model, QuerySet, Value
9
+ from rest_framework.serializers import Field
10
+ from wbcore import serializers as wb_serializers
11
+ from wbcore.contrib.currency.models import CurrencyFXRates
12
+ from wbfdm.models import Instrument, InstrumentPrice
13
+
14
+ from ..dto import Metric, MetricField, MetricKey
15
+ from ..exceptions import MetricInvalidParameterException
16
+
17
+ T = TypeVar("T", bound=Model)
18
+
19
+
20
+ class AbstractBackend(Generic[T]):
21
+ BASKET_MODEL_CLASS: Type[Model]
22
+ keys: list[MetricKey]
23
+
24
+ def __init__(self, val_date: date | None):
25
+ self.val_date = val_date
26
+ if not self.BASKET_MODEL_CLASS:
27
+ raise ValueError("A class implementing AbstractBackend needs to define a BASKET_MODEL_CLASS")
28
+ self.content_type = ContentType.objects.get_for_model(self.BASKET_MODEL_CLASS)
29
+
30
+ def compute_metrics(self, basket: Any) -> Generator[Metric, None, None]:
31
+ raise NotImplementedError()
32
+
33
+ def get_queryset(self) -> QuerySet:
34
+ return self.BASKET_MODEL_CLASS.objects.all()
35
+
36
+ def get_serializer_field_attr(self, metric_field: MetricField) -> dict[str, Any]:
37
+ """
38
+ Returns all the serializer attributes for that metric
39
+
40
+ We expect the implementing backends to override this method to define custom logics
41
+ """
42
+ return {
43
+ "decorators": metric_field.decorators,
44
+ "help_text": metric_field.help_text,
45
+ **metric_field.serializer_kwargs,
46
+ }
47
+
48
+ def get_serializer_fields(
49
+ self, with_prefixed_key: bool = False, metric_key: MetricKey | None = None
50
+ ) -> dict[str, Field]:
51
+ if metric_key is None:
52
+ metric_keys = self.keys
53
+ else:
54
+ metric_keys = [metric_key]
55
+ fields = {}
56
+ for metric_key in metric_keys:
57
+ fields.update(
58
+ {
59
+ field_key: wb_serializers.FloatField(
60
+ label=field_title,
61
+ read_only=True,
62
+ **self.get_serializer_field_attr(metric_key.subfields_map[field_key]),
63
+ )
64
+ for field_key, field_title in metric_key.get_fields(with_prefixed_key=with_prefixed_key)
65
+ }
66
+ )
67
+ for extra_subfield in metric_key.extra_subfields:
68
+ fields[
69
+ f"{metric_key.key}_{extra_subfield.key}"
70
+ ] = wb_serializers.ModelSerializer.serializer_field_mapping[extra_subfield.field_type](
71
+ read_only=True, label=extra_subfield.label, **self.get_serializer_field_attr(extra_subfield)
72
+ )
73
+ return fields
74
+
75
+
76
+ class InstrumentMetricBaseBackend(AbstractBackend[Instrument]):
77
+ BASKET_MODEL_CLASS = Instrument
78
+ TARGET_CURRENCY_KEY: str | None = None
79
+
80
+ def get_queryset(self) -> QuerySet[Instrument]:
81
+ return super().get_queryset().filter(is_investable_universe=True)
82
+
83
+ def _get_valid_date(self, instrument: Instrument) -> date:
84
+ if self.val_date is None and instrument.last_price_date:
85
+ return instrument.last_price_date
86
+ elif self.val_date:
87
+ with suppress(InstrumentPrice.DoesNotExist):
88
+ return instrument.prices.filter(date__lte=self.val_date).latest("date").date
89
+ else:
90
+ raise MetricInvalidParameterException()
91
+
92
+
93
+ class BaseDataloader:
94
+ METRIC_KEY: str
95
+
96
+ def __init__(
97
+ self,
98
+ basket,
99
+ val_date: date | None = None,
100
+ min_date: date | None = None,
101
+ target_currency_key: str | None = None,
102
+ use_cached_metrics_key: str | None = None,
103
+ basket_objects: QuerySet | None = None,
104
+ ):
105
+ self.val_date = val_date
106
+ self.min_date = min_date
107
+ self.basket = basket
108
+ self.use_cached_metrics_key = use_cached_metrics_key
109
+ self.target_currency_key = target_currency_key
110
+ self.basket_objects = basket_objects
111
+ if self.basket_objects is None:
112
+ self.basket_objects = self._get_basket_basket_objects()
113
+ self.basket_object_ids = list(map(lambda x: x.id, self.basket_objects))
114
+ self.aggregate_callback = "mean"
115
+ if self.target_currency_key == "USD":
116
+ self.fx_rate_expression = F("currency_fx_rate_to_usd__value")
117
+ elif self.target_currency_key:
118
+ self.fx_rate_expression = CurrencyFXRates.get_fx_rates_subquery_for_two_currencies(
119
+ "date", start_currency="instrument__currency", target_currency=self.target_currency_key
120
+ )
121
+ else:
122
+ self.fx_rate_expression = Value(Decimal(1.0))
123
+
124
+ def _get_basket_basket_objects(self) -> QuerySet:
125
+ """
126
+ Return an iterable of instrument contains within the basket. Expected to be override for custom logic
127
+
128
+ Returns:
129
+ An iterable of instrument. Default to the basket itself
130
+ """
131
+ return Instrument.objects.filter(id=self.basket.id)
132
+
133
+ def _compute(self):
134
+ raise NotImplementedError()
135
+
136
+ def compute(self):
137
+ if self.basket_object_ids:
138
+ if self.use_cached_metrics_key:
139
+ from wbfdm.contrib.metric.models import InstrumentMetric
140
+
141
+ df = pd.json_normalize(
142
+ InstrumentMetric.objects.filter(
143
+ basket_content_type=ContentType.objects.get_for_model(self.basket_objects.model),
144
+ basket_id__in=self.basket_object_ids,
145
+ key=self.use_cached_metrics_key,
146
+ date__isnull=True,
147
+ ).values_list("metrics", flat=True)
148
+ )
149
+ if not df.empty:
150
+ if (
151
+ "date" in df.columns
152
+ ): # it can happen that the cached metric don't contain a date key value, so we ffill it in case
153
+ df["date"] = df["date"].ffill()
154
+ if isinstance(self.aggregate_callback, dict):
155
+ df = df.agg({k: v for k, v in self.aggregate_callback.items() if k in df.columns})
156
+ else:
157
+ df = df.agg(self.aggregate_callback)
158
+ return df.dropna().to_dict()
159
+ return self._compute()