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,120 @@
1
+ from django.db import models
2
+ from slugify import slugify
3
+ from wbcore.contrib.io.mixins import ImportMixin
4
+ from wbcore.models import WBModel
5
+ from wbcore.utils.models import ComplexToStringMixin
6
+ from wbfdm.import_export.handlers.instrument_list import InstrumentListImportHandler
7
+ from wbfdm.models.instruments.instruments import Instrument
8
+
9
+
10
+ class InstrumentListThroughModel(ImportMixin, ComplexToStringMixin):
11
+ import_export_handler_class = InstrumentListImportHandler
12
+ """
13
+ This model is not a Through model from a programming point of view, however it allows to link instrument list to
14
+ instruments.
15
+ """
16
+
17
+ instrument_str = models.CharField(max_length=256)
18
+ instrument = models.ForeignKey(
19
+ to="wbfdm.Instrument",
20
+ null=True,
21
+ blank=True,
22
+ limit_choices_to=models.Q(is_security=True),
23
+ on_delete=models.SET_NULL,
24
+ )
25
+ instrument_list = models.ForeignKey(
26
+ to="wbfdm.InstrumentList",
27
+ on_delete=models.CASCADE,
28
+ )
29
+
30
+ from_date = models.DateField(null=True, blank=True)
31
+ to_date = models.DateField(null=True, blank=True)
32
+ comment = models.TextField(default="", blank=True)
33
+ validated = models.BooleanField(default=False)
34
+
35
+ def compute_str(self) -> str:
36
+ """
37
+ Method to compute the string representation of the instance. It will save the string value to the computed_str
38
+ field.
39
+
40
+ Returns:
41
+ The string representation of the instance.
42
+ """
43
+ if self.instrument and self.instrument.name_repr:
44
+ return f"{self.instrument.name_repr} - {self.instrument_list.name}"
45
+ return f"{self.instrument_str} - {self.instrument_list.name}"
46
+
47
+ class Meta:
48
+ verbose_name = "Instrument in Instrument List"
49
+ constraints = [
50
+ models.UniqueConstraint(fields=["instrument", "instrument_list"], name="unique_instrument_per_list")
51
+ ]
52
+
53
+ notification_types = [
54
+ (
55
+ "wbfdm.instrument_list_add",
56
+ "Instrument added to Instrument List",
57
+ "A notification when an instrument gets added to a list.",
58
+ True,
59
+ True,
60
+ True,
61
+ ),
62
+ ]
63
+
64
+
65
+ class InstrumentList(WBModel):
66
+ class InstrumentListType(models.TextChoices):
67
+ WATCH = "WATCH", "Watch List"
68
+ EXCLUSION = "EXCLUSION", "Exclusion List"
69
+
70
+ name = models.CharField(max_length=255)
71
+ identifier = models.CharField(max_length=255, unique=True, blank=True)
72
+ instrument_list_type = models.CharField(max_length=32, choices=InstrumentListType.choices, null=True, blank=True)
73
+
74
+ def __str__(self):
75
+ return self.name
76
+
77
+ @property
78
+ def instruments(self) -> models.QuerySet[Instrument]:
79
+ """
80
+ Returns a QuerySet of Instrument objects associated with the current instrument list.
81
+
82
+ This property filters the Instrument objects based on the related InstrumentListThroughModel
83
+ and returns only those Instruments where the foreign key is not null.
84
+
85
+ Returns:
86
+ models.QuerySet[Instrument]: A QuerySet of Instrument objects.
87
+ """
88
+ return Instrument.objects.filter(
89
+ id__in=(
90
+ InstrumentListThroughModel.objects.filter(instrument_list=self, instrument__isnull=False).values(
91
+ "instrument"
92
+ )
93
+ )
94
+ )
95
+
96
+ def save(self, *args, **kwargs):
97
+ if not self.identifier:
98
+ self.identifier = slugify(f"{self.name}-{self.id}")
99
+ super().save(*args, **kwargs)
100
+
101
+ class Meta:
102
+ verbose_name = "Instrument List"
103
+ verbose_name_plural = "Instrument Lists"
104
+ permissions = (("administrate_instrumentlist", "Can administrate Instrument List"),)
105
+
106
+ @classmethod
107
+ def get_endpoint_basename(cls):
108
+ return "wbfdm:instrumentlist"
109
+
110
+ @classmethod
111
+ def get_representation_endpoint(cls):
112
+ return "wbfdm:instrumentlistrepresentation-list"
113
+
114
+ @classmethod
115
+ def get_representation_value_key(cls):
116
+ return "id"
117
+
118
+ @classmethod
119
+ def get_representation_label_key(cls):
120
+ return "{{name}}"
@@ -0,0 +1,540 @@
1
+ from contextlib import suppress
2
+ from decimal import Decimal
3
+
4
+ import pandas as pd
5
+ from celery import shared_task
6
+ from django.db import models, transaction
7
+ from django.db.models import (
8
+ Case,
9
+ DecimalField,
10
+ Exists,
11
+ ExpressionWrapper,
12
+ F,
13
+ OuterRef,
14
+ Q,
15
+ QuerySet,
16
+ Subquery,
17
+ Value,
18
+ When,
19
+ )
20
+ from django.db.models.signals import post_save
21
+ from django.dispatch import receiver
22
+ from wbcore.contrib.currency.models import CurrencyFXRates
23
+ from wbcore.contrib.io.mixins import ImportMixin
24
+ from wbcore.models import DynamicDecimalField, DynamicFloatField, DynamicModel, WBModel
25
+ from wbcore.signals import pre_merge
26
+ from wbfdm.analysis.financial_analysis.financial_statistics_analysis import (
27
+ FinancialStatistics,
28
+ )
29
+ from wbfdm.import_export.handlers.instrument_price import InstrumentPriceImportHandler
30
+
31
+ from .mixin.financials_computed import InstrumentPriceComputedMixin
32
+
33
+
34
+ class ValidPricesQueryset(QuerySet):
35
+ def filter_only_valid_prices(self) -> QuerySet:
36
+ """
37
+ Filter the queryset to remove duplicate in case calculated and non-calculated prices are present for the same date/product/type
38
+ """
39
+ return self.annotate(
40
+ real_price_exists=Exists(
41
+ self.filter(
42
+ instrument=OuterRef("instrument"),
43
+ date=OuterRef("date"),
44
+ calculated=False,
45
+ )
46
+ )
47
+ ).filter(Q(calculated=False) | (Q(real_price_exists=False) & Q(calculated=True)))
48
+
49
+ def annotate_market_data(self):
50
+ base_qs = ValidPricesQueryset(self.model, using=self._db)
51
+
52
+ return self.annotate(
53
+ currency_fx_rate_to_usd_rate=F("currency_fx_rate_to_usd__value"),
54
+ calculated_outstanding_shares=Subquery(
55
+ base_qs.filter(instrument=OuterRef("instrument"), calculated=True, date=OuterRef("date")).values(
56
+ "outstanding_shares"
57
+ )[:1]
58
+ ),
59
+ internal_outstanding_shares=ExpressionWrapper(
60
+ Case( # Annotate the parent security if exists
61
+ When(
62
+ outstanding_shares__isnull=False,
63
+ then=F("outstanding_shares"),
64
+ ),
65
+ default=F("calculated_outstanding_shares"),
66
+ ),
67
+ output_field=DecimalField(max_digits=4),
68
+ ),
69
+ internal_market_capitalization=Case(
70
+ When(
71
+ market_capitalization__isnull=True,
72
+ then=ExpressionWrapper(
73
+ F("internal_outstanding_shares") * F("net_value"),
74
+ output_field=DecimalField(max_digits=4),
75
+ ),
76
+ ),
77
+ default=ExpressionWrapper(F("market_capitalization"), output_field=DecimalField(max_digits=4)),
78
+ ),
79
+ internal_market_capitalization_usd=F("internal_market_capitalization") / F("currency_fx_rate_to_usd_rate"),
80
+ calculated_volume=Subquery(
81
+ base_qs.filter(instrument=OuterRef("instrument"), calculated=True, date=OuterRef("date")).values(
82
+ "volume"
83
+ )[:1]
84
+ ),
85
+ internal_volume=ExpressionWrapper(
86
+ Case( # Annotate the parent security if exists
87
+ When(
88
+ volume__isnull=False,
89
+ then=F("volume"),
90
+ ),
91
+ default=F("calculated_volume"),
92
+ ),
93
+ output_field=models.FloatField(),
94
+ ),
95
+ )
96
+
97
+ def annotate_base_data(self):
98
+ return self.annotate(
99
+ currency_fx_rate_to_usd_rate=F("currency_fx_rate_to_usd__value"),
100
+ market_capitalization_usd=ExpressionWrapper(
101
+ F("market_capitalization") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField(max_digits=4)
102
+ ),
103
+ net_value_usd=F("net_value") / F("currency_fx_rate_to_usd_rate"),
104
+ gross_value_usd=F("gross_value") / F("currency_fx_rate_to_usd_rate"),
105
+ volume_usd=ExpressionWrapper(
106
+ F("volume") * F("net_value") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField()
107
+ ),
108
+ volume_50d_usd=ExpressionWrapper(
109
+ F("volume_50d") * F("net_value") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField()
110
+ ),
111
+ volume_200d_usd=ExpressionWrapper(
112
+ F("volume_200d") * F("net_value") / F("currency_fx_rate_to_usd_rate"), output_field=DecimalField()
113
+ ),
114
+ )
115
+
116
+ def annotate_security_data(self):
117
+ return self.annotate(
118
+ security=Case( # Annotate the parent security if exists
119
+ When(instrument__parent__isnull=False, then=F("instrument__parent")),
120
+ default=F("instrument"),
121
+ ),
122
+ security_instrument_type_key=Case( # Annotate the parent security if exists
123
+ When(
124
+ instrument__parent__isnull=False,
125
+ then=F("instrument__parent__instrument_type__key"),
126
+ ),
127
+ default=F("instrument__instrument_type__key"),
128
+ ),
129
+ )
130
+
131
+ def annotate_all(self):
132
+ return self.annotate_market_data().annotate_security_data().annotate_base_data()
133
+
134
+
135
+ class InstrumentPriceManager(models.Manager):
136
+ def __init__(self, with_annotation: bool = False, *args, **kwargs):
137
+ self.with_annotation = with_annotation
138
+ super().__init__(*args, **kwargs)
139
+
140
+ def get_queryset(self) -> ValidPricesQueryset:
141
+ qs = ValidPricesQueryset(self.model)
142
+ if self.with_annotation:
143
+ qs = qs.annotate_all()
144
+ return qs
145
+
146
+ def filtered_by_instruments(self, instrument_queryset, *other_instruments):
147
+ return self.filter(models.Q(instrument__in=instrument_queryset) | models.Q(instrument__in=other_instruments))
148
+
149
+ def filter_only_valid_prices(self) -> QuerySet:
150
+ return self.get_queryset().filter_only_valid_prices()
151
+
152
+ def annotate_market_data(self) -> QuerySet:
153
+ return self.get_queryset().annotate_market_data()
154
+
155
+ def annotate_base_data(self) -> QuerySet:
156
+ return self.get_queryset().annotate_base_data()
157
+
158
+ def annotate_security_data(self) -> QuerySet:
159
+ return self.get_queryset().annotate_security_data()
160
+
161
+ def annotate_all(self) -> QuerySet:
162
+ return self.get_queryset().annotate_all()
163
+
164
+
165
+ class AnnotatedInstrumentPriceManager(InstrumentPriceManager):
166
+ def get_queryset(self):
167
+ return super().get_queryset().annotate_market_data().annotate_base_data().annotate_security_data()
168
+
169
+
170
+ class InstrumentPrice(
171
+ ImportMixin,
172
+ InstrumentPriceComputedMixin,
173
+ DynamicModel,
174
+ WBModel,
175
+ ):
176
+ import_export_handler_class = InstrumentPriceImportHandler
177
+ # Base fields
178
+ instrument = models.ForeignKey(
179
+ to="wbfdm.Instrument",
180
+ related_name="prices",
181
+ on_delete=models.PROTECT,
182
+ limit_choices_to=models.Q(children__isnull=True),
183
+ verbose_name="Instrument",
184
+ blank=True,
185
+ null=True,
186
+ )
187
+ date = models.DateField(verbose_name="Date")
188
+ calculated = models.BooleanField(default=False, verbose_name="Is Calculated")
189
+
190
+ net_value = models.DecimalField(max_digits=16, decimal_places=6, verbose_name="Value (Net)")
191
+ gross_value = DynamicDecimalField(
192
+ max_digits=16,
193
+ decimal_places=6,
194
+ verbose_name="Value (Gross)",
195
+ ) # TODO: I think we need to remove this field that is not really used here.
196
+
197
+ outstanding_shares = DynamicDecimalField(
198
+ decimal_places=4,
199
+ max_digits=16,
200
+ verbose_name="Outstanding Shares",
201
+ help_text="The amount of outstanding share for this instrument",
202
+ )
203
+ outstanding_shares_consolidated = DynamicDecimalField(
204
+ decimal_places=4,
205
+ max_digits=16,
206
+ verbose_name="Outstanding Shares (Consolidated)",
207
+ help_text="The amount of outstanding share for this instrument",
208
+ )
209
+ ########################################################
210
+ # ASSET STATISTICS #
211
+ ########################################################
212
+
213
+ volume = models.FloatField(
214
+ null=True,
215
+ blank=True,
216
+ verbose_name="Volume",
217
+ help_text="The Volume of the Asset on the price date of the Asset.",
218
+ )
219
+
220
+ volume_50d = DynamicFloatField(
221
+ verbose_name="Average Volume (50 Days)",
222
+ help_text="The Average Volume of the Asset over the last 50 days from the price date of the Asset.",
223
+ )
224
+
225
+ volume_200d = models.FloatField(
226
+ null=True,
227
+ blank=True,
228
+ verbose_name="Average Volume (200 Days)",
229
+ help_text="The Average Volume of the Asset over the last 200 days from the price date of the Asset.",
230
+ )
231
+ market_capitalization = models.FloatField(
232
+ null=True,
233
+ blank=True,
234
+ verbose_name="Market Capitalization",
235
+ help_text="The Market Capitalization of the Asset the price date of the Asset.",
236
+ )
237
+ market_capitalization_consolidated = models.FloatField(
238
+ null=True,
239
+ blank=True,
240
+ verbose_name="Market Capitalization (Consolidated)",
241
+ help_text="the consolidated market value of a company in local currency.",
242
+ )
243
+
244
+ currency_fx_rate_to_usd = models.ForeignKey(
245
+ to="currency.CurrencyFXRates",
246
+ related_name="instrument_prices",
247
+ on_delete=models.PROTECT,
248
+ blank=True,
249
+ null=True,
250
+ verbose_name="Instrument Currency Rate",
251
+ help_text="Rate to between instrument currency and USD",
252
+ )
253
+
254
+ # Statistics
255
+ lock_statistics = models.BooleanField(
256
+ default=False,
257
+ help_text="If True, a save will not override the beta, correlation and sharpe ratio",
258
+ )
259
+ sharpe_ratio = models.FloatField(blank=True, null=True, verbose_name="Sharpe Ratio")
260
+ correlation = models.FloatField(blank=True, null=True, verbose_name="Correlation")
261
+ beta = models.FloatField(blank=True, null=True, verbose_name="Beta")
262
+
263
+ created = models.DateTimeField(auto_now_add=True, verbose_name="Created")
264
+ modified = models.DateTimeField(
265
+ verbose_name="Modified",
266
+ auto_now=True,
267
+ )
268
+ # custom_beta_180d = DynamicFloatField(verbose_name="Custom Beta (180 days)")
269
+ # custom_beta_1y = DynamicFloatField(verbose_name="Custom Beta (1 Years)")
270
+ # custom_beta_2y = DynamicFloatField(verbose_name="Custom Beta (2 Years)")
271
+ # custom_beta_3y = DynamicFloatField(verbose_name="Custom Beta (3 Years)")
272
+ # custom_beta_5y = DynamicFloatField(verbose_name="Custom Beta (4 Years)")
273
+ #
274
+ # # Performances fields
275
+ # performance_1d = DynamicDecimalField(
276
+ # verbose_name="Performance 1D", help_text="Performance 1D", max_digits=16, decimal_places=6
277
+ # )
278
+ # performance_7d = DynamicDecimalField(
279
+ # verbose_name="Performance (1W)", help_text="Performance 7 days rolling", max_digits=16, decimal_places=6
280
+ # )
281
+ # performance_30d = DynamicDecimalField(
282
+ # verbose_name="Performance (1M)", help_text="Performance 30 days rolling", max_digits=16, decimal_places=6
283
+ # )
284
+ # performance_90d = DynamicDecimalField(
285
+ # verbose_name="Performance (1Q)", help_text="Performance 90 days rolling", max_digits=16, decimal_places=6
286
+ # )
287
+ # performance_365d = DynamicDecimalField(
288
+ # verbose_name="Performance (1Y)", help_text="Performance 365 days rolling", max_digits=16, decimal_places=6
289
+ # )
290
+ # performance_ytd = DynamicDecimalField(
291
+ # verbose_name="Performance (YTD)", help_text="Performance Year-to-date", max_digits=16, decimal_places=6
292
+ # )
293
+ # performance_inception = DynamicDecimalField(
294
+ # verbose_name="Performance (Inception)",
295
+ # help_text="Performance since inception",
296
+ # max_digits=16,
297
+ # decimal_places=6,
298
+ # )
299
+
300
+ objects = InstrumentPriceManager()
301
+ annotated_objects = InstrumentPriceManager(with_annotation=True)
302
+
303
+ class Meta:
304
+ verbose_name = "Instrument Price"
305
+ verbose_name_plural = "Instrument Prices"
306
+ constraints = [
307
+ models.CheckConstraint(
308
+ check=~models.Q(date__week_day__in=[1, 7]),
309
+ name="%(app_label)s_%(class)s_weekday_constraint",
310
+ ),
311
+ models.UniqueConstraint(fields=["instrument", "date", "calculated"], name="unique_price"),
312
+ ]
313
+ indexes = [
314
+ models.Index(
315
+ name="fdm_instrumentprice_base_idx",
316
+ fields=["calculated", "date", "instrument"],
317
+ ),
318
+ models.Index(
319
+ name="fdm_instrumentprice_idx1",
320
+ fields=["calculated", "instrument"],
321
+ ),
322
+ models.Index(
323
+ name="fdm_instrumentprice_idx2",
324
+ fields=["instrument"],
325
+ ),
326
+ ]
327
+
328
+ @property
329
+ def _net_value_usd(self):
330
+ if self.currency_fx_rate_to_usd:
331
+ return getattr(self, "net_value_usd", self.net_value * self.currency_fx_rate_to_usd.value)
332
+
333
+ def save(self, *args, **kwargs):
334
+ if not self.currency_fx_rate_to_usd:
335
+ with suppress(CurrencyFXRates.DoesNotExist):
336
+ self.currency_fx_rate_to_usd = CurrencyFXRates.objects.get(
337
+ date=self.date, currency=self.instrument.currency
338
+ )
339
+
340
+ if self.market_capitalization_consolidated is None:
341
+ self.market_capitalization_consolidated = self.market_capitalization
342
+ super().save(*args, **kwargs)
343
+
344
+ def __str__(self):
345
+ return f"{self.instrument.name}: {self.net_value} {self.date:%d.%m.%Y}"
346
+
347
+ @property
348
+ def previous_price(self):
349
+ """Returns the previous instrument prices if one exists or None
350
+
351
+ Returns:
352
+ instrument.InstrumentPrice | None -- Previous InstrumentPrice
353
+ """
354
+ try:
355
+ return InstrumentPrice.objects.filter(
356
+ instrument=self.instrument, date__lt=self.date, calculated=self.calculated
357
+ ).latest("date")
358
+ except InstrumentPrice.DoesNotExist:
359
+ return None
360
+
361
+ @property
362
+ def next_price(self):
363
+ """Returns the next instrument prices if one exists or None
364
+
365
+ Returns:
366
+ instrument.InstrumentPrice | None -- Next InstrumentPrice
367
+ """
368
+ try:
369
+ return self.instrument.prices.filter(date__gt=self.date, calculated=self.calculated).earliest("date")
370
+ except InstrumentPrice.DoesNotExist:
371
+ return None
372
+
373
+ @property
374
+ def shares(self):
375
+ """Returns the number of shares of a instrument
376
+
377
+ The number of shares are the sum of all customer trades
378
+
379
+ Returns:
380
+ int -- Shares
381
+ """
382
+ return self.instrument.total_shares(self.date)
383
+
384
+ @property
385
+ def valid_outstanding_shares(self):
386
+ prices = self.instrument.prices.filter(date=self.date, outstanding_shares__isnull=False).order_by("calculated")
387
+ return prices.last().outstanding_shares if prices.exists() else self.shares
388
+
389
+ @property
390
+ def nominal_value(self):
391
+ """Returns the nominal value of a instrument
392
+
393
+ The nominal value is the number of current shares multiplied by the share price of a instrument
394
+
395
+ Returns:
396
+ int -- Nominal Value
397
+ """
398
+ return self.instrument.nominal_value(self.date)
399
+
400
+ def compute_and_update_statistics(self, min_period: int = 20):
401
+ prices_df = (
402
+ pd.DataFrame(
403
+ InstrumentPrice.objects.filter_only_valid_prices()
404
+ .filter(instrument=self.instrument, date__lte=self.date)
405
+ .values_list("date", "net_value"),
406
+ columns=["date", "net_value"],
407
+ )
408
+ .set_index("date")
409
+ .sort_index()["net_value"]
410
+ )
411
+ if not prices_df.empty and prices_df.shape[0] >= min_period:
412
+ financial_statistics = FinancialStatistics(prices_df)
413
+ min_date = prices_df.index.min()
414
+ if risk_free_rate := self.instrument.primary_risk_instrument:
415
+ risk_free_rate_df = (
416
+ pd.DataFrame(
417
+ risk_free_rate.valuations.filter(date__gte=min_date, date__lte=self.date).values_list(
418
+ "date", "net_value"
419
+ ),
420
+ columns=["date", "net_value"],
421
+ )
422
+ .set_index("date")
423
+ .sort_index()["net_value"]
424
+ )
425
+ if sharpe_ratio := financial_statistics.get_sharpe_ratio(risk_free_rate_df):
426
+ self.sharpe_ratio = sharpe_ratio
427
+ if benchmark := self.instrument.primary_benchmark:
428
+ benchmark_df = (
429
+ pd.DataFrame(
430
+ benchmark.valuations.filter(date__gte=min_date, date__lte=self.date).values_list(
431
+ "date", "net_value"
432
+ ),
433
+ columns=["date", "net_value"],
434
+ )
435
+ .set_index("date")
436
+ .sort_index()["net_value"]
437
+ )
438
+ if (beta := financial_statistics.get_beta(benchmark_df)) is not None:
439
+ self.beta = beta
440
+ if (correlation := financial_statistics.get_correlation(benchmark_df)) is not None:
441
+ self.correlation = correlation
442
+
443
+ @classmethod
444
+ def subquery_closest_value(
445
+ cls,
446
+ field_name,
447
+ val_date=None,
448
+ date_name="date",
449
+ instrument_pk_name="instrument__pk",
450
+ date_lookup="lte",
451
+ order_by="-date",
452
+ calculated_filter_value=False,
453
+ ):
454
+ index_filter_params = {}
455
+ if calculated_filter_value is not None:
456
+ index_filter_params["calculated"] = calculated_filter_value
457
+ if not val_date and date_name:
458
+ index_filter_params[f"date__{date_lookup}"] = models.OuterRef(date_name)
459
+ elif val_date:
460
+ index_filter_params[f"date__{date_lookup}"] = val_date
461
+ index_filter_params["instrument"] = models.OuterRef(instrument_pk_name)
462
+ qs = cls.objects.filter(**index_filter_params).filter(**{f"{field_name}__isnull": False})
463
+ return models.Subquery(qs.order_by(order_by).values(field_name)[:1])
464
+
465
+ @classmethod
466
+ def annotate_sum_shares(cls, queryset, val_date, date_key="date"):
467
+ """
468
+ Efficient way to annotate sum of shares without destroying indexing
469
+ """
470
+ return queryset.annotate(
471
+ sum_shares_calculated=InstrumentPrice.subquery_closest_value(
472
+ "outstanding_shares",
473
+ date_name=date_key,
474
+ instrument_pk_name="pk",
475
+ date_lookup="lte",
476
+ order_by="-date",
477
+ calculated_filter_value=True,
478
+ ), # We get the last price whose outstanding_shares is not none and date below date_key
479
+ sum_shares_real=InstrumentPrice.subquery_closest_value(
480
+ "outstanding_shares",
481
+ val_date=val_date,
482
+ date_name=date_key,
483
+ instrument_pk_name="pk",
484
+ date_lookup="exact",
485
+ calculated_filter_value=False,
486
+ ),
487
+ sum_shares=Case(
488
+ When(sum_shares_real__isnull=False, then=F("sum_shares_real")),
489
+ When(sum_shares_calculated__isnull=False, then=F("sum_shares_calculated")),
490
+ default=Value(Decimal(0)),
491
+ output_field=DecimalField(),
492
+ ),
493
+ )
494
+
495
+ @classmethod
496
+ def get_endpoint_basename(cls):
497
+ return "wbfdm:price"
498
+
499
+ @classmethod
500
+ def get_representation_value_key(cls):
501
+ return "id"
502
+
503
+ @classmethod
504
+ def get_representation_label_key(cls):
505
+ return "{{instrument} {{date}}"
506
+
507
+ @classmethod
508
+ def get_representation_endpoint(cls):
509
+ return "wbfdm:price-list"
510
+
511
+
512
+ @receiver(post_save, sender="currency.CurrencyFXRates")
513
+ def rate_creation(sender, instance, created, raw, **kwargs):
514
+ if not raw and created:
515
+ transaction.on_commit(lambda: update_currency_fx_rate_from_created_rate.delay(instance.id))
516
+
517
+
518
+ @receiver(pre_merge, sender="wbfdm.Instrument")
519
+ def pre_merge_instrument(sender: models.Model, merged_object, main_object, **kwargs):
520
+ """
521
+ Simply reassign the prices of the merged instrument to the main instrument if they don't already exist for that day, otherwise simply delete them
522
+ """
523
+ merged_object.prices.annotate(
524
+ already_exists=Exists(
525
+ InstrumentPrice.objects.filter(
526
+ calculated=OuterRef("calculated"), instrument=main_object, date=OuterRef("date")
527
+ )
528
+ )
529
+ ).filter(already_exists=True).delete()
530
+ merged_object.prices.update(instrument=main_object)
531
+
532
+
533
+ @shared_task(queue="portfolio")
534
+ def update_currency_fx_rate_from_created_rate(rate_id):
535
+ currency_rate = CurrencyFXRates.objects.get(id=rate_id)
536
+ for price in InstrumentPrice.objects.filter(
537
+ instrument__currency=currency_rate.currency, date=currency_rate.date, currency_fx_rate_to_usd__isnull=True
538
+ ):
539
+ price.currency_fx_rate_to_usd = currency_rate
540
+ price.save()