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,991 @@
1
+ import re
2
+ from contextlib import suppress
3
+ from datetime import date, timedelta
4
+ from typing import Any, Generator, Iterator, Self, TypeVar
5
+
6
+ from celery import shared_task
7
+ from colorfield.fields import ColorField
8
+ from django.contrib import admin
9
+ from django.contrib.contenttypes.models import ContentType
10
+ from django.contrib.postgres.fields import ArrayField
11
+ from django.contrib.postgres.indexes import GinIndex
12
+ from django.contrib.postgres.search import SearchVector, SearchVectorField
13
+ from django.core.exceptions import ObjectDoesNotExist, ValidationError
14
+ from django.db import models, transaction
15
+ from django.db.models import Q, Value
16
+ from django.db.models.signals import post_delete, post_save, pre_delete, pre_save
17
+ from django.dispatch import receiver
18
+ from dynamic_preferences.registries import global_preferences_registry
19
+ from mptt.models import MPTTModel, TreeForeignKey, TreeManager
20
+ from pandas.tseries.offsets import BDay
21
+ from rest_framework.reverse import reverse
22
+ from slugify import slugify
23
+ from tqdm import tqdm
24
+ from wbcore.content_type.utils import get_ancestors_content_type
25
+ from wbcore.contrib.dataloader.models import Entity
26
+ from wbcore.contrib.io.mixins import ImportMixin
27
+ from wbcore.contrib.io.models import ImportedObjectProviderRelationship
28
+ from wbcore.contrib.notifications.utils import create_notification_type
29
+ from wbcore.contrib.tags.models import TagModelMixin
30
+ from wbcore.models import WBModel
31
+ from wbcore.signals import pre_merge
32
+ from wbcore.utils.models import ComplexToStringMixin
33
+ from wbfdm.analysis import TechnicalAnalysis
34
+ from wbfdm.contrib.internal.dataloaders.market_data import MarketDataDataloader
35
+ from wbfdm.contrib.metric.dispatch import compute_metrics
36
+ from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
37
+ from wbfdm.models.instruments.llm.create_instrument_news_relationships import (
38
+ run_company_extraction_llm,
39
+ )
40
+ from wbfdm.preferences import get_default_classification_group
41
+ from wbfdm.signals import (
42
+ add_instrument_to_investable_universe,
43
+ instrument_price_imported,
44
+ )
45
+ from wbnews.models import News
46
+ from wbnews.signals import create_news_relationships
47
+
48
+ from ...dataloaders.proxies import InstrumentDataloaderProxy
49
+ from .instrument_relationships import RelatedInstrumentThroughModel
50
+ from .mixin.instruments import InstrumentPMSMixin
51
+ from .querysets import InstrumentQuerySet
52
+ from .utils import re_bloomberg, re_isin, re_mnemonic, re_ric
53
+
54
+
55
+ class InstrumentManager(TreeManager):
56
+ def __init__(self, with_annotation: bool = False, *args, **kwargs):
57
+ self.with_annotation = with_annotation
58
+ super().__init__(*args, **kwargs)
59
+
60
+ def _custom_rebuild_helper(self, node, left, tree_id, nodes_to_update, level):
61
+ right = left + 1
62
+
63
+ for child in node.children.all():
64
+ right = self._custom_rebuild_helper(
65
+ node=child,
66
+ left=right,
67
+ tree_id=tree_id,
68
+ nodes_to_update=nodes_to_update,
69
+ level=level + 1,
70
+ )
71
+
72
+ setattr(node, self._rebuild_fields["left"], left)
73
+ setattr(node, self._rebuild_fields["right"], right)
74
+ setattr(node, self._rebuild_fields["level"], level)
75
+ setattr(node, self._rebuild_fields["tree_id"], tree_id)
76
+ nodes_to_update.append(node)
77
+
78
+ return right + 1
79
+
80
+ def rebuild(self, batch_size=1000, debug: bool = False, **filters) -> None:
81
+ """
82
+ We supercharge MPTT rebuild manager method to avoid loading all instrument into memory
83
+ """
84
+ self._find_out_rebuild_fields()
85
+
86
+ parents = self._get_parents(**filters)
87
+ tree_id = filters.get("tree_id", 1)
88
+ nodes_to_update = []
89
+ if debug:
90
+ gen = tqdm(enumerate(parents), total=len(parents))
91
+ else:
92
+ gen = enumerate(parents)
93
+ for index, parent in gen:
94
+ self._custom_rebuild_helper(
95
+ node=parent,
96
+ left=1,
97
+ tree_id=tree_id + index,
98
+ nodes_to_update=nodes_to_update,
99
+ level=0,
100
+ )
101
+ if len(nodes_to_update) >= batch_size:
102
+ self.bulk_update(
103
+ nodes_to_update,
104
+ self._rebuild_fields.values(),
105
+ )
106
+ nodes_to_update = []
107
+
108
+ self.bulk_update(
109
+ nodes_to_update,
110
+ self._rebuild_fields.values(),
111
+ )
112
+
113
+ def get_queryset(self) -> InstrumentQuerySet:
114
+ qs = InstrumentQuerySet(self.model, using=self._db)
115
+ if self.with_annotation:
116
+ qs = qs.annotate_all()
117
+ return qs
118
+
119
+ def annotate_classification_for_group(
120
+ self, classification_group, classification_height: int = 0, **kwargs
121
+ ) -> models.QuerySet:
122
+ return self.get_queryset().annotate_classification_for_group(
123
+ classification_group, classification_height=classification_height, **kwargs
124
+ )
125
+
126
+ def annotate_base_data(self):
127
+ return self.get_queryset().annotate_base_data()
128
+
129
+ def annotate_all(self):
130
+ return self.get_queryset().annotate_all()
131
+
132
+
133
+ class SecurityInstrumentManager(InstrumentManager):
134
+ def get_queryset(self) -> InstrumentQuerySet:
135
+ return super().get_queryset().filter(is_security=True)
136
+
137
+
138
+ class ClassifiableInstrumentManager(InstrumentManager):
139
+ def get_queryset(self) -> InstrumentQuerySet:
140
+ return super().get_queryset().filter(instrument_type__is_classifiable=True, level=0)
141
+
142
+
143
+ class ActiveInstrumentManager(InstrumentManager):
144
+ def get_queryset_at_date(self, val_date: date):
145
+ return (
146
+ super()
147
+ .get_queryset()
148
+ .filter(
149
+ (models.Q(delisted_date__isnull=True) | models.Q(delisted_date__gte=val_date))
150
+ & models.Q(inception_date__isnull=False)
151
+ & models.Q(inception_date__lte=val_date)
152
+ )
153
+ )
154
+
155
+ def get_queryset(self):
156
+ return self.get_queryset_at_date(date.today())
157
+
158
+
159
+ class InvestableUniverseManager(InstrumentManager):
160
+ def get_queryset(self) -> InstrumentQuerySet:
161
+ instrument_ids = set()
162
+ for _, ids in add_instrument_to_investable_universe.send(sender=Instrument):
163
+ instrument_ids.update(ids)
164
+ return (
165
+ super()
166
+ .get_queryset()
167
+ .annotate_base_data()
168
+ .filter(is_investable=True)
169
+ .filter(
170
+ Q(is_investable_universe=True)
171
+ | Q(
172
+ dependent_instruments_through__isnull=False
173
+ ) # we consider instrument that are "related" to other instrument as within the investable universe by default
174
+ | Q(id__in=instrument_ids)
175
+ | Q(is_managed=True)
176
+ | Q(
177
+ dl_parameters__market_data__path="wbfdm.contrib.internal.dataloaders.market_data.MarketDataDataloader"
178
+ )
179
+ )
180
+ )
181
+
182
+
183
+ class InvestableInstrumentManager(InstrumentManager):
184
+ def get_queryset(self) -> InstrumentQuerySet:
185
+ return super().get_queryset().filter(children__isnull=True)
186
+
187
+
188
+ SelfInstrument = TypeVar("SelfInstrument", bound="Instrument")
189
+
190
+
191
+ class InstrumentType(models.Model):
192
+ name = models.CharField(max_length=128, verbose_name="Name")
193
+ short_name = models.CharField(max_length=128, verbose_name="Short Name")
194
+ name_repr = models.CharField(max_length=128, verbose_name="Name (Representation)")
195
+ key = models.CharField(max_length=32, verbose_name="Key", unique=True)
196
+ description = models.TextField(verbose_name="Description", blank=True)
197
+
198
+ is_classifiable = models.BooleanField(default=True, verbose_name="Classifiable")
199
+ is_security = models.BooleanField(default=True, verbose_name="Security")
200
+
201
+ @classmethod
202
+ @property
203
+ def PRODUCT(cls):
204
+ return InstrumentType.objects.get_or_create(
205
+ key="product", defaults={"name": "Product", "short_name": "Product"}
206
+ )[0]
207
+
208
+ @classmethod
209
+ @property
210
+ def EQUITY(cls):
211
+ return InstrumentType.objects.get_or_create(key="equity", defaults={"name": "equity", "short_name": "equity"})[
212
+ 0
213
+ ]
214
+
215
+ @classmethod
216
+ @property
217
+ def INDEX(cls):
218
+ return InstrumentType.objects.get_or_create(key="index", defaults={"name": "Index", "short_name": "Index"})[0]
219
+
220
+ @classmethod
221
+ @property
222
+ def CASH(cls):
223
+ return InstrumentType.objects.get_or_create(key="cash", defaults={"name": "Cash", "short_name": "Cash"})[0]
224
+
225
+ @classmethod
226
+ @property
227
+ def CASHEQUIVALENT(cls):
228
+ return InstrumentType.objects.get_or_create(
229
+ key="cash_equivalent", defaults={"name": "Cash Equivalents", "short_name": "Cash Equivalents"}
230
+ )[0]
231
+
232
+ @classmethod
233
+ @property
234
+ def PRODUCT_GROUP(cls):
235
+ return InstrumentType.objects.get_or_create(
236
+ key="product_group", defaults={"name": "Product Group", "short_name": "Product Group"}
237
+ )[0]
238
+
239
+ def save(self, *args, **kwargs):
240
+ if not self.short_name:
241
+ self.short_name = self.name
242
+ if not self.key:
243
+ self.key = slugify(self.name, separator="_")
244
+ if not self.name_repr:
245
+ self.name_repr = self.name
246
+ super().save(*args, **kwargs)
247
+
248
+ def __str__(self) -> str:
249
+ return f"{self.name}"
250
+
251
+ @classmethod
252
+ def get_representation_endpoint(cls):
253
+ return "wbfdm:instrumenttyperepresentation-list"
254
+
255
+ @classmethod
256
+ def get_representation_value_key(cls):
257
+ return "id"
258
+
259
+ @classmethod
260
+ def get_representation_label_key(cls):
261
+ return "{{name}}"
262
+
263
+
264
+ class Instrument(ComplexToStringMixin, TagModelMixin, ImportMixin, InstrumentPMSMixin, WBModel, Entity, MPTTModel):
265
+ COMPUTED_STR_RECOMPUTE_PERIODICALLY: bool = False
266
+ # COMPUTED_STR_RECOMPUTE_ON_SAVE: bool = False # I am commenting this out because we need computed str to be recomputed on save but do not know why this would be an issue
267
+
268
+ import_export_handler_class = InstrumentImportHandler
269
+ dl_proxy = InstrumentDataloaderProxy
270
+
271
+ parent = TreeForeignKey(
272
+ "self",
273
+ related_name="children",
274
+ null=True,
275
+ blank=True,
276
+ on_delete=models.CASCADE,
277
+ verbose_name="Parent Instrument",
278
+ )
279
+ name = models.CharField(max_length=512)
280
+ description = models.TextField(default="", blank=True, null=True)
281
+ instrument_type = models.ForeignKey(
282
+ "wbfdm.InstrumentType", related_name="instruments", null=True, blank=True, on_delete=models.PROTECT
283
+ )
284
+
285
+ inception_date = models.DateField(null=True, blank=True)
286
+ delisted_date = models.DateField(null=True, blank=True)
287
+ last_valuation_date = models.DateField(
288
+ null=True, blank=True, verbose_name="Last Valuation Date", help_text="Last Valuation Date"
289
+ )
290
+ last_price_date = models.DateField(
291
+ null=True, blank=True, verbose_name="Last Price Date", help_text="Last Price Date"
292
+ )
293
+ # The report date fields store the actual dates when a report happens, not the end of a period.
294
+ last_annual_report = models.DateField(null=True, blank=True)
295
+ last_interim_report = models.DateField(null=True, blank=True)
296
+ next_annual_report = models.DateField(null=True, blank=True)
297
+ next_interim_report = models.DateField(null=True, blank=True)
298
+
299
+ country = models.ForeignKey(
300
+ to="geography.Geography",
301
+ null=True,
302
+ blank=True,
303
+ limit_choices_to={"level": 1},
304
+ on_delete=models.SET_NULL,
305
+ )
306
+
307
+ currency = models.ForeignKey(
308
+ to="currency.Currency",
309
+ null=True,
310
+ blank=True,
311
+ on_delete=models.SET_NULL,
312
+ )
313
+ exchange = models.ForeignKey(
314
+ to="wbfdm.Exchange", null=True, blank=True, on_delete=models.PROTECT, related_name="instruments"
315
+ )
316
+
317
+ source_id = models.CharField(max_length=64, null=True, blank=True)
318
+ source = models.CharField(max_length=64, null=True, blank=True)
319
+
320
+ # Other fields from PMS
321
+ founded_year = models.IntegerField(null=True, blank=True, verbose_name="Founded Year")
322
+ identifier = models.CharField(
323
+ max_length=255,
324
+ verbose_name="Identifier",
325
+ null=True,
326
+ blank=True,
327
+ )
328
+ name_repr = models.CharField(max_length=255, null=True, blank=True, verbose_name="Name (Representation)")
329
+ last_update = models.DateTimeField(auto_now=True, blank=True, null=True)
330
+ alternative_names = ArrayField(models.CharField(blank=True, null=True, max_length=255), default=list, blank=True)
331
+ isin = models.CharField(
332
+ null=True,
333
+ blank=True,
334
+ max_length=12,
335
+ verbose_name="ISIN",
336
+ help_text="The ISIN provided by the bank.",
337
+ )
338
+
339
+ ticker = models.CharField(
340
+ max_length=255,
341
+ verbose_name="Ticker Bloomberg",
342
+ help_text="The Bloomberg ticker without the exchange (e.g. AAPL)",
343
+ blank=True,
344
+ null=True,
345
+ )
346
+
347
+ old_isins = ArrayField(
348
+ base_field=models.CharField(max_length=12),
349
+ default=list,
350
+ blank=True,
351
+ verbose_name="Old ISINS",
352
+ help_text="These old ISINs are stored for this instrument to retrieve it more easily later.",
353
+ )
354
+ sedol = models.CharField(
355
+ max_length=255,
356
+ verbose_name="SEDOL",
357
+ help_text="Stock Exchange Daily Official List",
358
+ blank=True,
359
+ null=True,
360
+ )
361
+
362
+ valoren = models.CharField(
363
+ max_length=255,
364
+ verbose_name="Valoren Number",
365
+ help_text="Valoren Number",
366
+ blank=True,
367
+ null=True,
368
+ )
369
+
370
+ cusip = models.CharField(
371
+ max_length=255,
372
+ verbose_name="CUSIP",
373
+ help_text="CUSIP",
374
+ blank=True,
375
+ null=True,
376
+ )
377
+
378
+ refinitiv_ticker = models.CharField(
379
+ max_length=255,
380
+ verbose_name="Refinitiv Ticker",
381
+ help_text="Refinitiv Refinitiv",
382
+ blank=True,
383
+ null=True,
384
+ )
385
+ refinitiv_identifier_code = models.CharField(
386
+ max_length=255,
387
+ verbose_name="RIC",
388
+ help_text="Refinitiv Identifier Code",
389
+ blank=True,
390
+ null=True,
391
+ )
392
+
393
+ refinitiv_mnemonic_code = models.CharField(
394
+ max_length=255,
395
+ verbose_name="Refinitiv Datastream Mnemonic Code",
396
+ help_text="Refinitiv Datastream Mnemonic Code",
397
+ blank=True,
398
+ null=True,
399
+ )
400
+
401
+ headquarter_address = models.CharField(
402
+ max_length=512, blank=True, null=True, help_text="The company Headquarter address"
403
+ )
404
+ headquarter_city = models.ForeignKey(
405
+ "geography.Geography",
406
+ related_name="headquarters_of",
407
+ null=True,
408
+ blank=True,
409
+ on_delete=models.SET_NULL,
410
+ verbose_name="Headquarter City",
411
+ help_text="The company's headquarter city",
412
+ limit_choices_to={"level": 3},
413
+ )
414
+ employees = models.IntegerField(null=True, blank=True)
415
+
416
+ primary_url = models.URLField(blank=True, null=True, help_text="The Company website url")
417
+ additional_urls = ArrayField(models.URLField(blank=True, null=True), default=list, blank=True)
418
+
419
+ related_instruments = models.ManyToManyField(
420
+ "self",
421
+ symmetrical=False,
422
+ related_name="benchmarks_of",
423
+ through="wbfdm.RelatedInstrumentThroughModel",
424
+ through_fields=("instrument", "related_instrument"),
425
+ blank=True,
426
+ verbose_name="The Related Instruments",
427
+ )
428
+
429
+ classifications = models.ManyToManyField(
430
+ "wbfdm.Classification",
431
+ through="wbfdm.InstrumentClassificationThroughModel",
432
+ limit_choices_to=models.Q(level=models.F("group__max_depth")),
433
+ related_name="instruments",
434
+ blank=True,
435
+ verbose_name="Classifications",
436
+ )
437
+ is_cash = models.BooleanField(default=False)
438
+ is_cash_equivalent = models.BooleanField(default=False)
439
+ issue_price = models.PositiveIntegerField(
440
+ default=100,
441
+ verbose_name="Issue Price",
442
+ help_text="The initial issue price that is displayed on the factsheet",
443
+ )
444
+ base_color = ColorField(
445
+ blank=True, null=True, max_length=64, default="#FF0000"
446
+ ) # we need this field for pms breakdown.
447
+
448
+ is_security = models.BooleanField(default=False)
449
+ is_managed = models.BooleanField(default=False)
450
+ is_primary = models.BooleanField(null=True, blank=True)
451
+ is_investable_universe = models.BooleanField(
452
+ default=False,
453
+ verbose_name="In Investable Universe",
454
+ help_text="If True, the instrument belongs to the investable universe",
455
+ )
456
+
457
+ search_vector = SearchVectorField(null=True)
458
+ trigram_search_vector = models.CharField(max_length=1024, null=True, blank=True)
459
+
460
+ objects = InstrumentManager()
461
+ annotated_objects = InstrumentManager(with_annotation=True)
462
+ active_objects = ActiveInstrumentManager()
463
+ securities = SecurityInstrumentManager()
464
+ classifiables = ClassifiableInstrumentManager()
465
+ investables = InvestableInstrumentManager()
466
+ investable_universe = InvestableUniverseManager()
467
+
468
+ class Meta:
469
+ verbose_name = "Instrument"
470
+ verbose_name_plural = "Instruments"
471
+ permissions = (("administrate_instrument", "Can administrate Instrument"),)
472
+ constraints = [
473
+ models.UniqueConstraint(fields=["source_id", "source"], name="unique_source"),
474
+ models.UniqueConstraint(
475
+ fields=["refinitiv_identifier_code"],
476
+ name="unique_ric",
477
+ condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
478
+ ),
479
+ models.UniqueConstraint(
480
+ fields=["refinitiv_mnemonic_code"],
481
+ name="unique_rmc",
482
+ condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
483
+ ),
484
+ models.UniqueConstraint(
485
+ fields=["isin"],
486
+ name="unique_isin",
487
+ condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
488
+ ),
489
+ models.UniqueConstraint(
490
+ fields=["sedol"],
491
+ name="unique_sedol",
492
+ condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
493
+ ),
494
+ models.UniqueConstraint(
495
+ fields=["valoren"],
496
+ name="unique_valoren",
497
+ condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
498
+ ),
499
+ models.UniqueConstraint(
500
+ fields=["cusip"],
501
+ name="unique_cusip",
502
+ condition=models.Q(is_security=True) & models.Q(delisted_date__isnull=True),
503
+ ),
504
+ models.UniqueConstraint(
505
+ fields=["parent", "is_primary"],
506
+ name="unique_instrument_primary",
507
+ condition=models.Q(is_primary=True) & models.Q(is_managed=False),
508
+ ),
509
+ ]
510
+ indexes = [
511
+ models.Index(fields=["parent"], name="instrument_parent_index"),
512
+ models.Index(fields=["parent", "exchange", "isin"], name="instrument_children_index"),
513
+ models.Index(fields=["is_investable_universe"], name="instrument_investible_index"),
514
+ models.Index(fields=["is_security"], name="instrument_security_index"),
515
+ models.Index(fields=["level"], name="instrument_level_index"),
516
+ GinIndex(fields=["search_vector"], name="instrument_sv_gin_idx"), # type: ignore
517
+ GinIndex(fields=["trigram_search_vector"], opclasses=["gin_trgm_ops"], name="instrument_trigram_sv_gin_idx"), # type: ignore
518
+ ]
519
+ notification_types = [
520
+ create_notification_type(
521
+ code="wbfdm.instrument.notify",
522
+ title="Instrument Notification",
523
+ help_text="Sends a notification when there is an update about an instrument",
524
+ )
525
+ ]
526
+
527
+ def get_tag_detail_endpoint(self):
528
+ return reverse("wbfdm:instrument-detail", [self.id])
529
+
530
+ def get_tag_representation(self):
531
+ return self.computed_str
532
+
533
+ @property
534
+ @admin.display(description="Is Investable")
535
+ def _is_investable(self):
536
+ return getattr(self, "is_investable", not self.children.exists())
537
+
538
+ @property
539
+ @admin.display(description="Primary Benchmark")
540
+ def primary_benchmark(self):
541
+ if primary_through := RelatedInstrumentThroughModel.objects.filter(
542
+ instrument=self, is_primary=True, related_type=RelatedInstrumentThroughModel.RelatedTypeChoices.BENCHMARK
543
+ ).first():
544
+ return primary_through.related_instrument
545
+ return None
546
+
547
+ @property
548
+ @admin.display(description="Primary Peer")
549
+ def primary_peer(self):
550
+ if primary_through := RelatedInstrumentThroughModel.objects.filter(
551
+ instrument=self, is_primary=True, related_type=RelatedInstrumentThroughModel.RelatedTypeChoices.PEER
552
+ ).first():
553
+ return primary_through.related_instrument
554
+ return None
555
+
556
+ @property
557
+ @admin.display(description="Primary Risk Instrument")
558
+ def primary_risk_instrument(self):
559
+ if primary_through := RelatedInstrumentThroughModel.objects.filter(
560
+ instrument=self,
561
+ is_primary=True,
562
+ related_type=RelatedInstrumentThroughModel.RelatedTypeChoices.RISK_INSTRUMENT,
563
+ ).first():
564
+ return primary_through.related_instrument
565
+ return None
566
+
567
+ @property
568
+ @admin.display(description="Primary Classification")
569
+ def primary_classification(self):
570
+ if primary_classification := self.classifications.filter(group__is_primary=True).first():
571
+ return primary_classification
572
+
573
+ @property
574
+ @admin.display(description="Favorite Classification")
575
+ def favorite_classification(self):
576
+ if favorite_classification := self.classifications.filter(group=get_default_classification_group()).first():
577
+ return favorite_classification
578
+
579
+ @property
580
+ def active(self) -> bool:
581
+ today = date.today()
582
+ return (
583
+ self.inception_date
584
+ and self.inception_date <= today
585
+ and (not self.delisted_date or self.delisted_date > today)
586
+ )
587
+
588
+ @property
589
+ def identifier_repr(self) -> str:
590
+ identifier_repr = ""
591
+ if self.instrument_type and self.instrument_type.key == "product":
592
+ # Then we prioritize ISIN over ticker
593
+ identifiers = ["isin", "ticker", "refinitiv_identifier_code", "refinitiv_mnemonic_code"]
594
+ else:
595
+ identifiers = ["ticker", "isin", "refinitiv_identifier_code", "refinitiv_mnemonic_code"]
596
+ for identifier in identifiers:
597
+ if v := getattr(self, identifier, None):
598
+ identifier_repr = v
599
+ break
600
+ return identifier_repr.replace(":", "-")
601
+
602
+ @property
603
+ def valuations(self):
604
+ try:
605
+ return self.prices.filter(calculated=False)
606
+ except (
607
+ ValueError
608
+ ): # ValueError because if this property is called before the instance has a primary key, it will fail
609
+ return Instrument.objects.none()
610
+
611
+ @property
612
+ def security_instrument_type(self):
613
+ while not self.instrument_type.is_security and self.parent:
614
+ return self.parent.security_instrument_type
615
+ return self.instrument_type
616
+
617
+ def update_search_vectors(self):
618
+ names = list(map(lambda x: Value(x), filter(None, [self.name, *self.alternative_names])))
619
+ if names:
620
+ isins = list(map(lambda x: Value(x), filter(None, [self.isin, *self.old_isins])))
621
+ identifiers = list(
622
+ map(
623
+ lambda x: Value(re.sub(r"[^a-zA-Z0-9]", "", x.lower())),
624
+ filter(
625
+ None,
626
+ [
627
+ self.ticker,
628
+ self.refinitiv_identifier_code,
629
+ self.refinitiv_mnemonic_code,
630
+ self.valoren,
631
+ self.sedol,
632
+ ],
633
+ ),
634
+ )
635
+ )
636
+ self.search_vector = SearchVector(*names, weight="D")
637
+ if identifiers:
638
+ self.search_vector += SearchVector(*identifiers, weight="A")
639
+ if isins:
640
+ self.search_vector += SearchVector(*isins, weight="A")
641
+ self.trigram_search_vector = f"{self.name} {' '.join(self.alternative_names)}".strip()
642
+
643
+ def clean(self):
644
+ if self.is_investable_universe and self.id and self.children.exists():
645
+ raise ValidationError("An instrument in the investable universe cannot have children")
646
+ return self
647
+
648
+ def pre_save(self):
649
+ if self.instrument_type:
650
+ self.is_security = self.instrument_type.is_security
651
+ if self.delisted_date:
652
+ self.is_security = False
653
+ if not self.name_repr:
654
+ self.name_repr = self.name
655
+ if not self.founded_year and self.inception_date:
656
+ self.founded_year = self.inception_date.year
657
+ if not self.inception_date:
658
+ self.inception_date = date.today() - timedelta(days=1)
659
+ if not self.founded_year:
660
+ self.founded_year = self.inception_date.year
661
+ if self.level is None:
662
+ self.level = 0
663
+ self.rght = 0
664
+ self.lft = 0
665
+ self.tree_id = 0
666
+
667
+ # ensure all identifiers are stored in uppercase and do not contain any whitespace
668
+ for identifier_key in ["isin", "refinitiv_mnemonic_code", "ticker", "sedol", "cusip", "identifier"]:
669
+ if identifier := getattr(self, identifier_key, None):
670
+ setattr(self, identifier_key, identifier.upper().replace(" ", ""))
671
+ if self.refinitiv_identifier_code:
672
+ self.refinitiv_identifier_code = self.refinitiv_identifier_code.replace(
673
+ " ", ""
674
+ ) # RIC cannot be uppercased because its symbology implies meaning for lowercase characters
675
+ self.update_search_vectors()
676
+ if self.is_primary and (parent := self.parent):
677
+ # we have a unique constraint on parent. We take the time to make sure no other children is already primary = True (otherwise, update will fail)
678
+ Instrument.objects.filter(parent=parent, is_primary=True).exclude(
679
+ source=self.source, source_id=self.source_id
680
+ ).update(is_primary=False)
681
+ if not self.parent:
682
+ self.is_primary = True
683
+ if self.id and (not self.instrument_type or not self.currency) and self.children.count() == 1:
684
+ child = self.children.first()
685
+ if not self.instrument_type:
686
+ self.instrument_type = child.instrument_type
687
+ if not self.currency:
688
+ self.currency = child.currency
689
+
690
+ def save(self, *args, **kwargs):
691
+ self.pre_save()
692
+ if self.is_primary is None:
693
+ self.is_primary = not self.parent or (self.exchange is not None and self.parent.exchange == self.exchange)
694
+ super().save(*args, **kwargs)
695
+
696
+ def compute_str(self):
697
+ repr = self.name_repr # we follow bloomberg instrument representation format
698
+ if self.instrument_type:
699
+ repr += f" {self.instrument_type.short_name}"
700
+ if self.identifier_repr:
701
+ repr += f" - {self.identifier_repr}"
702
+ if self.exchange:
703
+ repr += f" ({str(self.exchange)})"
704
+ return repr
705
+
706
+ def is_active_at_date(self, today: date) -> bool:
707
+ return (
708
+ self.inception_date is not None
709
+ and self.inception_date <= today
710
+ and (not self.delisted_date or self.delisted_date > today)
711
+ )
712
+
713
+ def update_last_valuation_date(self):
714
+ if self.valuations.exists():
715
+ earliest_valuation = self.valuations.earliest("date")
716
+ last_valuation = self.valuations.latest("date").date
717
+ if not self.inception_date or (
718
+ not earliest_valuation.calculated and self.inception_date > earliest_valuation.date
719
+ ):
720
+ self.inception_date = earliest_valuation.date
721
+ if not self.last_valuation_date or last_valuation >= self.last_valuation_date:
722
+ self.last_valuation_date = last_valuation
723
+ if self.prices.exists():
724
+ last_price = self.prices.latest("date").date
725
+ if not self.last_price_date or last_price >= self.last_price_date:
726
+ self.last_price_date = last_price
727
+ self.save()
728
+ compute_metrics(last_price, basket=self)
729
+
730
+ def get_prices(self, only_instrument_price: bool = False, **kwargs) -> Iterator[dict[str, any]]:
731
+ qs = Instrument.objects.filter(id=self.id)
732
+ if "market_data" in self.dl_parameters and not only_instrument_price:
733
+ return qs.dl.market_data(**kwargs)
734
+ # if not dataloader is found, we default to the internal instrument price dataloader
735
+ return MarketDataDataloader(qs).market_data(**kwargs)
736
+
737
+ def get_classifable_ancestor(self, include_self: bool = True) -> Self:
738
+ root = self.get_root()
739
+ if root.instrument_type and root.instrument_type.is_classifiable:
740
+ return root
741
+
742
+ def get_security_ancestor(self, include_self: bool = True) -> Self:
743
+ if include_self:
744
+ parent = self
745
+ else:
746
+ parent = self.parent
747
+ while parent:
748
+ if parent.instrument_type and parent.instrument_type.is_security:
749
+ return parent
750
+ parent = parent.parent
751
+
752
+ def merge(self, merged_instrument: SelfInstrument, dispatch: bool = True, override_fields_to_copy: bool = False):
753
+ """
754
+ This method handle the deletion of an instrument ("merged_instrument") in favor of another one (self).
755
+
756
+ After handling all the instrument base logic reassignment, it calls a signal that all modules can implement in
757
+ order to implement their own reassignment logic.
758
+
759
+ The function is atomic, it either succeed or fail (i.e. If merged_instrument is deleted, the merge was succeesful)
760
+
761
+ Args:
762
+ merged_instrument: The Instrument that is supposed to be merged and deleted
763
+ """
764
+ if self == merged_instrument:
765
+ return
766
+ with transaction.atomic(): # We want this to either succeed fully or fail
767
+ # Get the base
768
+ if dispatch:
769
+ pre_merge.send(
770
+ sender=Instrument, merged_object=merged_instrument, main_object=self
771
+ ) # default signal dispatch for the Instrument class
772
+
773
+ # if the self type is different than Instrument, it's a polymorphic call. We fire also the pre_merge signal with this child type
774
+ if type(self) is not Instrument: # noqa
775
+ pre_merge.send(sender=self.__class__, merged_object=merged_instrument, main_object=self)
776
+
777
+ # We refresh the reference in case the underlying signal receivers modify these objects
778
+ self.refresh_from_db()
779
+ merged_instrument.refresh_from_db()
780
+
781
+ # We delete finally the merged instrument. All unlikage should have been done in the signal receivers function
782
+ merged_instrument.delete()
783
+
784
+ # Finally, we copy the potentially missing field from merged instrument to self (Only if self.field is None)
785
+ field_to_copy = [
786
+ "founded_year",
787
+ "inception_date",
788
+ "delisted_date",
789
+ "name",
790
+ "name_repr",
791
+ "description",
792
+ "ticker",
793
+ "country",
794
+ "headquarter_address",
795
+ "headquarter_city",
796
+ "primary_url",
797
+ "identifier",
798
+ "currency",
799
+ "isin",
800
+ "sedol",
801
+ "valoren",
802
+ "refinitiv_ticker",
803
+ "refinitiv_identifier_code",
804
+ "refinitiv_mnemonic_code",
805
+ "exchange",
806
+ "source",
807
+ "source_id",
808
+ "employees",
809
+ "last_annual_report",
810
+ "last_interim_report",
811
+ "next_annual_report",
812
+ "next_interim_report",
813
+ "parent",
814
+ ]
815
+ many_fields = [
816
+ "alternative_names",
817
+ "old_isins",
818
+ "additional_urls",
819
+ ]
820
+ for field in field_to_copy:
821
+ if (new_value := getattr(merged_instrument, field, None)) is not None and (
822
+ getattr(self, field, None) is None or override_fields_to_copy
823
+ ):
824
+ setattr(self, field, new_value)
825
+ for field in many_fields:
826
+ current_values = getattr(self, field, [])
827
+ new_values = getattr(merged_instrument, field, [])
828
+ setattr(self, field, list(set(current_values + new_values)))
829
+ self.save()
830
+
831
+ def handle_backend_lookup(self, attribute: str, method: str):
832
+ try:
833
+ backend = self.lookups.get(**{attribute: True}).load_backend()
834
+ return getattr(backend, method)()
835
+ except ObjectDoesNotExist:
836
+ return self.__class__.objects.none()
837
+
838
+ def technical_analysis(self, from_date: date | None = None, to_date: date | None = None):
839
+ return TechnicalAnalysis.init_full_from_instrument(self, from_date, to_date)
840
+
841
+ def technical_benchmark_analysis(self, from_date: date | None = None, to_date: date | None = None):
842
+ return TechnicalAnalysis.init_close_from_instrument(self, from_date, to_date)
843
+
844
+ def import_prices(self, start: date | None = None, end: date | None = None, clear: bool = False):
845
+ if not self.is_leaf_node():
846
+ raise ValueError("Cannot import price on a non-leaf node")
847
+ if not start:
848
+ start = (
849
+ self.inception_date
850
+ if self.inception_date
851
+ else global_preferences_registry.manager()["wbfdm__default_start_date_historical_import"]
852
+ )
853
+ if not end:
854
+ end = (
855
+ date.today() - BDay(1)
856
+ ).date() # we don't import today price in case the dataloader returns duplicates (e.g. DSWS)
857
+
858
+ # we detect when was the last date price imported before start and switch the start date from there
859
+ with suppress(ObjectDoesNotExist):
860
+ start = self.prices.filter(date__lte=start).latest("date").date
861
+ # Import instrument prices
862
+ self.save_prices_in_db(start, end, clear=clear)
863
+
864
+ # compute daily statistics & performances
865
+ self.update_last_valuation_date()
866
+
867
+ instrument_price_imported.send(sender=Instrument, instrument=self, start=start, end=end)
868
+
869
+ @classmethod
870
+ def parse_content_for_identifiers(cls, content: str) -> Generator[dict[str, Any], None, None]:
871
+ for ric in re_ric(content):
872
+ yield {"refinitiv_identifier_code": ric}
873
+ for isin in re_isin(content):
874
+ yield {"isin": isin}
875
+ for ticker in re_bloomberg(content):
876
+ yield {"ticker": ticker}
877
+ for mnemonic in re_mnemonic(content):
878
+ yield {"refinitiv_mnemonic_code": mnemonic}
879
+
880
+ @classmethod
881
+ def get_endpoint_basename(cls):
882
+ return "wbfdm:instrument"
883
+
884
+ @classmethod
885
+ def get_representation_endpoint(cls):
886
+ return "wbfdm:instrumentrepresentation-list"
887
+
888
+ @classmethod
889
+ def get_representation_value_key(cls):
890
+ return "id"
891
+
892
+ @classmethod
893
+ def get_representation_label_key(cls):
894
+ return "{{computed_str}}"
895
+
896
+
897
+ @shared_task(queue="portfolio")
898
+ def import_prices_as_task(instrument_id, **kwargs):
899
+ instrument = Instrument.objects.get(id=instrument_id)
900
+ instrument.import_prices(**kwargs)
901
+
902
+
903
+ @receiver(pre_delete, sender="wbfdm.Instrument")
904
+ def pre_delete_instrument(sender, instance, **kwargs):
905
+ ImportedObjectProviderRelationship.objects.filter(
906
+ content_type__in=get_ancestors_content_type(ContentType.objects.get_for_model(instance)), object_id=instance.id
907
+ ).delete()
908
+
909
+
910
+ @receiver(pre_save, sender="wbfdm.Instrument")
911
+ def pre_save_instrument(sender, instance, raw, **kwargs):
912
+ if not raw:
913
+ pre_instance = None
914
+ if instance.id:
915
+ pre_instance = sender.objects.get(id=instance.id)
916
+ # Remove duplicates if existings
917
+ instance.old_isins = list(set(instance.old_isins))
918
+ if pre_instance:
919
+ if (
920
+ pre_instance.isin
921
+ and instance.isin
922
+ and pre_instance.isin != instance.isin
923
+ and pre_instance.isin not in instance.old_isins
924
+ ):
925
+ instance.old_isins = [*instance.old_isins, pre_instance.isin]
926
+ if pre_instance.name_repr != instance.name_repr:
927
+ # if a family member get is name representation updated, we update it for the whole family
928
+ pre_instance.get_family().update(name_repr=instance.name_repr)
929
+
930
+ # the instrument was manually included into the investable universe, in that case, we need to fetch the price
931
+ if (
932
+ instance.is_leaf_node()
933
+ and (not pre_instance or not pre_instance.is_investable_universe)
934
+ and instance.is_investable_universe
935
+ ):
936
+ import_prices_as_task.apply_async(
937
+ (instance.id,), {"clear": True}, countdown=15
938
+ ) # we need to introduce a countdown to avoid racing condition where save resumed after shared task picked up its instance reference.
939
+
940
+
941
+ @receiver(post_save, sender="wbfdm.Classification")
942
+ def ensure_classification_0_height(sender, instance, created, raw, **kwargs):
943
+ # Ensure that if a leaf classification becomes non-leaf, then all instruments linked to it are updated automatically
944
+ # with the new leaf classiciation
945
+ if not raw and instance.parent and instance.height == 0:
946
+ for instrument in Instrument.objects.filter(classifications=instance.parent):
947
+ instrument.classifications.remove(instance.parent)
948
+ instrument.classifications.add(instance)
949
+
950
+
951
+ @receiver(post_delete, sender="wbfdm.InstrumentPrice")
952
+ def post_delete_valuation(sender, instance, **kwargs):
953
+ if not instance.calculated and instance.instrument.last_valuation_date == instance.date:
954
+ instance.instrument.update_last_valuation_date()
955
+
956
+
957
+ class CashManager(InstrumentManager):
958
+ def get_queryset(self) -> InstrumentQuerySet:
959
+ return super().get_queryset().filter(instrument_type=InstrumentType.CASH)
960
+
961
+
962
+ class Cash(Instrument):
963
+ objects = CashManager()
964
+
965
+ class Meta:
966
+ proxy = True
967
+
968
+
969
+ @receiver(post_save, sender="currency.Currency")
970
+ def create_cash_from_currency(sender, instance, created, raw, **kwargs):
971
+ if created:
972
+ Instrument.objects.get_or_create(
973
+ instrument_type=InstrumentType.CASH, currency=instance, defaults={"name": f"Cash {instance.key}"}
974
+ )
975
+
976
+
977
+ class EquityManager(InstrumentManager):
978
+ def get_queryset(self) -> InstrumentQuerySet:
979
+ return super().get_queryset().filter(instrument_type__key="equity")
980
+
981
+
982
+ class Equity(Instrument):
983
+ objects = EquityManager()
984
+
985
+ class Meta:
986
+ proxy = True
987
+
988
+
989
+ @receiver(create_news_relationships, sender="wbnews.News")
990
+ def get_news_relationships_for_instruments_task(sender: type, instance: "News", **kwargs) -> shared_task:
991
+ return run_company_extraction_llm.s(instance.title, instance.description, instance.summary)