wbportfolio 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 wbportfolio might be problematic. Click here for more details.

Files changed (486) hide show
  1. wbportfolio/__init__.py +1 -0
  2. wbportfolio/admin/__init__.py +12 -0
  3. wbportfolio/admin/asset.py +47 -0
  4. wbportfolio/admin/custodians.py +9 -0
  5. wbportfolio/admin/portfolio.py +127 -0
  6. wbportfolio/admin/portfolio_relationships.py +22 -0
  7. wbportfolio/admin/product_groups.py +42 -0
  8. wbportfolio/admin/products.py +80 -0
  9. wbportfolio/admin/reconciliations.py +14 -0
  10. wbportfolio/admin/registers.py +17 -0
  11. wbportfolio/admin/roles.py +19 -0
  12. wbportfolio/admin/synchronization/__init__.py +2 -0
  13. wbportfolio/admin/synchronization/admin.py +114 -0
  14. wbportfolio/admin/synchronization/portfolio_synchronization.py +18 -0
  15. wbportfolio/admin/synchronization/price_computation.py +21 -0
  16. wbportfolio/admin/transactions/__init__.py +5 -0
  17. wbportfolio/admin/transactions/claim.py +16 -0
  18. wbportfolio/admin/transactions/dividends.py +14 -0
  19. wbportfolio/admin/transactions/fees.py +35 -0
  20. wbportfolio/admin/transactions/trades.py +49 -0
  21. wbportfolio/admin/transactions/transactions.py +37 -0
  22. wbportfolio/analysis/__init__.py +0 -0
  23. wbportfolio/analysis/claims.py +235 -0
  24. wbportfolio/apps.py +5 -0
  25. wbportfolio/contrib/__init__.py +0 -0
  26. wbportfolio/contrib/company_portfolio/__init__.py +0 -0
  27. wbportfolio/contrib/company_portfolio/admin.py +28 -0
  28. wbportfolio/contrib/company_portfolio/apps.py +29 -0
  29. wbportfolio/contrib/company_portfolio/configs/__init__.py +3 -0
  30. wbportfolio/contrib/company_portfolio/configs/display.py +182 -0
  31. wbportfolio/contrib/company_portfolio/configs/endpoints.py +34 -0
  32. wbportfolio/contrib/company_portfolio/configs/previews.py +37 -0
  33. wbportfolio/contrib/company_portfolio/constants.py +1 -0
  34. wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py +87 -0
  35. wbportfolio/contrib/company_portfolio/factories.py +32 -0
  36. wbportfolio/contrib/company_portfolio/filters.py +127 -0
  37. wbportfolio/contrib/company_portfolio/management.py +19 -0
  38. wbportfolio/contrib/company_portfolio/migrations/0001_initial.py +214 -0
  39. wbportfolio/contrib/company_portfolio/migrations/__init__.py +0 -0
  40. wbportfolio/contrib/company_portfolio/models.py +334 -0
  41. wbportfolio/contrib/company_portfolio/scripts.py +76 -0
  42. wbportfolio/contrib/company_portfolio/serializers.py +303 -0
  43. wbportfolio/contrib/company_portfolio/tasks.py +19 -0
  44. wbportfolio/contrib/company_portfolio/tests/__init__.py +0 -0
  45. wbportfolio/contrib/company_portfolio/tests/conftest.py +161 -0
  46. wbportfolio/contrib/company_portfolio/tests/test_models.py +161 -0
  47. wbportfolio/contrib/company_portfolio/urls.py +29 -0
  48. wbportfolio/contrib/company_portfolio/viewsets.py +195 -0
  49. wbportfolio/defaults/__init__.py +0 -0
  50. wbportfolio/defaults/fees/__init__.py +0 -0
  51. wbportfolio/defaults/fees/default.py +92 -0
  52. wbportfolio/defaults/portfolio/__init__.py +0 -0
  53. wbportfolio/defaults/portfolio/default_rebalancing.py +45 -0
  54. wbportfolio/dynamic_preferences_registry.py +58 -0
  55. wbportfolio/factories/__init__.py +35 -0
  56. wbportfolio/factories/adjustments.py +17 -0
  57. wbportfolio/factories/assets.py +75 -0
  58. wbportfolio/factories/claim.py +39 -0
  59. wbportfolio/factories/custodians.py +11 -0
  60. wbportfolio/factories/dividends.py +14 -0
  61. wbportfolio/factories/fees.py +15 -0
  62. wbportfolio/factories/indexes.py +17 -0
  63. wbportfolio/factories/portfolio_cash_flow.py +20 -0
  64. wbportfolio/factories/portfolio_cash_targets.py +15 -0
  65. wbportfolio/factories/portfolio_swing_pricings.py +15 -0
  66. wbportfolio/factories/portfolios.py +59 -0
  67. wbportfolio/factories/product_groups.py +28 -0
  68. wbportfolio/factories/products.py +56 -0
  69. wbportfolio/factories/pytest_utils.py +121 -0
  70. wbportfolio/factories/reconciliations.py +23 -0
  71. wbportfolio/factories/roles.py +20 -0
  72. wbportfolio/factories/synchronization.py +40 -0
  73. wbportfolio/factories/trades.py +35 -0
  74. wbportfolio/factories/transactions.py +21 -0
  75. wbportfolio/fdm/__init__.py +0 -0
  76. wbportfolio/fdm/tasks.py +12 -0
  77. wbportfolio/filters/__init__.py +32 -0
  78. wbportfolio/filters/assets.py +485 -0
  79. wbportfolio/filters/assets_and_net_new_money_progression.py +42 -0
  80. wbportfolio/filters/custodians.py +10 -0
  81. wbportfolio/filters/esg.py +22 -0
  82. wbportfolio/filters/performances.py +171 -0
  83. wbportfolio/filters/portfolios.py +24 -0
  84. wbportfolio/filters/positions.py +178 -0
  85. wbportfolio/filters/products.py +157 -0
  86. wbportfolio/filters/roles.py +26 -0
  87. wbportfolio/filters/signals.py +92 -0
  88. wbportfolio/filters/transactions/__init__.py +20 -0
  89. wbportfolio/filters/transactions/claim.py +394 -0
  90. wbportfolio/filters/transactions/fees.py +66 -0
  91. wbportfolio/filters/transactions/trades.py +224 -0
  92. wbportfolio/filters/transactions/transactions.py +98 -0
  93. wbportfolio/import_export/__init__.py +0 -0
  94. wbportfolio/import_export/backends/__init__.py +2 -0
  95. wbportfolio/import_export/backends/ubs/__init__.py +3 -0
  96. wbportfolio/import_export/backends/ubs/asset_position.py +45 -0
  97. wbportfolio/import_export/backends/ubs/fees.py +63 -0
  98. wbportfolio/import_export/backends/ubs/instrument_price.py +44 -0
  99. wbportfolio/import_export/backends/ubs/mixin.py +15 -0
  100. wbportfolio/import_export/backends/utils.py +58 -0
  101. wbportfolio/import_export/backends/wbfdm/__init__.py +2 -0
  102. wbportfolio/import_export/backends/wbfdm/adjustment.py +50 -0
  103. wbportfolio/import_export/backends/wbfdm/dividend.py +16 -0
  104. wbportfolio/import_export/backends/wbfdm/mixin.py +15 -0
  105. wbportfolio/import_export/handlers/__init__.py +0 -0
  106. wbportfolio/import_export/handlers/adjustment.py +39 -0
  107. wbportfolio/import_export/handlers/asset_position.py +167 -0
  108. wbportfolio/import_export/handlers/dividend.py +80 -0
  109. wbportfolio/import_export/handlers/fees.py +58 -0
  110. wbportfolio/import_export/handlers/portfolio_cash_flow.py +57 -0
  111. wbportfolio/import_export/handlers/register.py +43 -0
  112. wbportfolio/import_export/handlers/trade.py +191 -0
  113. wbportfolio/import_export/parsers/__init__.py +0 -0
  114. wbportfolio/import_export/parsers/default_mapping.py +30 -0
  115. wbportfolio/import_export/parsers/jpmorgan/__init__.py +0 -0
  116. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +63 -0
  117. wbportfolio/import_export/parsers/jpmorgan/fees.py +64 -0
  118. wbportfolio/import_export/parsers/jpmorgan/strategy.py +116 -0
  119. wbportfolio/import_export/parsers/jpmorgan/valuation.py +41 -0
  120. wbportfolio/import_export/parsers/leonteq/__init__.py +0 -0
  121. wbportfolio/import_export/parsers/leonteq/customer_trade.py +47 -0
  122. wbportfolio/import_export/parsers/leonteq/equity.py +81 -0
  123. wbportfolio/import_export/parsers/leonteq/fees.py +70 -0
  124. wbportfolio/import_export/parsers/leonteq/trade.py +94 -0
  125. wbportfolio/import_export/parsers/leonteq/valuation.py +39 -0
  126. wbportfolio/import_export/parsers/natixis/__init__.py +0 -0
  127. wbportfolio/import_export/parsers/natixis/customer_trade.py +62 -0
  128. wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +66 -0
  129. wbportfolio/import_export/parsers/natixis/d1_equity.py +80 -0
  130. wbportfolio/import_export/parsers/natixis/d1_fees.py +58 -0
  131. wbportfolio/import_export/parsers/natixis/d1_trade.py +70 -0
  132. wbportfolio/import_export/parsers/natixis/d1_valuation.py +41 -0
  133. wbportfolio/import_export/parsers/natixis/dividend.py +53 -0
  134. wbportfolio/import_export/parsers/natixis/equity.py +60 -0
  135. wbportfolio/import_export/parsers/natixis/fees.py +53 -0
  136. wbportfolio/import_export/parsers/natixis/trade.py +63 -0
  137. wbportfolio/import_export/parsers/natixis/utils.py +76 -0
  138. wbportfolio/import_export/parsers/natixis/valuation.py +46 -0
  139. wbportfolio/import_export/parsers/refinitiv/__init__.py +0 -0
  140. wbportfolio/import_export/parsers/refinitiv/adjustment.py +24 -0
  141. wbportfolio/import_export/parsers/sg_lux/__init__.py +0 -0
  142. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +70 -0
  143. wbportfolio/import_export/parsers/sg_lux/customer_trade.py +75 -0
  144. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +140 -0
  145. wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +80 -0
  146. wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +57 -0
  147. wbportfolio/import_export/parsers/sg_lux/equity.py +137 -0
  148. wbportfolio/import_export/parsers/sg_lux/fees.py +56 -0
  149. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +51 -0
  150. wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +29 -0
  151. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +36 -0
  152. wbportfolio/import_export/parsers/sg_lux/registers.py +210 -0
  153. wbportfolio/import_export/parsers/sg_lux/sylk.py +248 -0
  154. wbportfolio/import_export/parsers/sg_lux/utils.py +36 -0
  155. wbportfolio/import_export/parsers/sg_lux/valuation.py +53 -0
  156. wbportfolio/import_export/parsers/societe_generale/__init__.py +0 -0
  157. wbportfolio/import_export/parsers/societe_generale/customer_trade.py +54 -0
  158. wbportfolio/import_export/parsers/societe_generale/strategy.py +94 -0
  159. wbportfolio/import_export/parsers/societe_generale/valuation.py +37 -0
  160. wbportfolio/import_export/parsers/tellco/__init__.py +0 -0
  161. wbportfolio/import_export/parsers/tellco/customer_trade.py +64 -0
  162. wbportfolio/import_export/parsers/tellco/equity.py +86 -0
  163. wbportfolio/import_export/parsers/tellco/valuation.py +52 -0
  164. wbportfolio/import_export/parsers/ubs/__init__.py +0 -0
  165. wbportfolio/import_export/parsers/ubs/api/__init__.py +0 -0
  166. wbportfolio/import_export/parsers/ubs/api/asset_position.py +106 -0
  167. wbportfolio/import_export/parsers/ubs/api/fees.py +31 -0
  168. wbportfolio/import_export/parsers/ubs/api/instrument_price.py +20 -0
  169. wbportfolio/import_export/parsers/ubs/api/utils.py +0 -0
  170. wbportfolio/import_export/parsers/ubs/customer_trade.py +60 -0
  171. wbportfolio/import_export/parsers/ubs/equity.py +97 -0
  172. wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +67 -0
  173. wbportfolio/import_export/parsers/ubs/valuation.py +52 -0
  174. wbportfolio/import_export/parsers/vontobel/__init__.py +0 -0
  175. wbportfolio/import_export/parsers/vontobel/asset_position.py +97 -0
  176. wbportfolio/import_export/parsers/vontobel/customer_trade.py +54 -0
  177. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +40 -0
  178. wbportfolio/import_export/parsers/vontobel/instrument.py +34 -0
  179. wbportfolio/import_export/parsers/vontobel/management_fees.py +86 -0
  180. wbportfolio/import_export/parsers/vontobel/performance_fees.py +35 -0
  181. wbportfolio/import_export/parsers/vontobel/trade.py +38 -0
  182. wbportfolio/import_export/parsers/vontobel/utils.py +17 -0
  183. wbportfolio/import_export/parsers/vontobel/valuation.py +29 -0
  184. wbportfolio/import_export/resources/__init__.py +0 -0
  185. wbportfolio/import_export/resources/assets.py +68 -0
  186. wbportfolio/import_export/resources/trades.py +41 -0
  187. wbportfolio/import_export/utils.py +42 -0
  188. wbportfolio/metric/__init__.py +0 -0
  189. wbportfolio/metric/backends/__init__.py +2 -0
  190. wbportfolio/metric/backends/base.py +86 -0
  191. wbportfolio/metric/backends/constants.py +222 -0
  192. wbportfolio/metric/backends/portfolio_base.py +255 -0
  193. wbportfolio/metric/backends/portfolio_esg.py +66 -0
  194. wbportfolio/metric/tests/__init__.py +0 -0
  195. wbportfolio/metric/tests/conftest.py +4 -0
  196. wbportfolio/metric/tests/test_portfolio_base.py +135 -0
  197. wbportfolio/metric/tests/test_portfolio_esg.py +69 -0
  198. wbportfolio/migrations/0001_initial_squashed.py +13848 -0
  199. wbportfolio/migrations/0002_product_default_sub_account_squashed_0039_alter_assetallocation_company_and_more.py +3836 -0
  200. wbportfolio/migrations/0040_instrument_financial_instrument.py +26 -0
  201. wbportfolio/migrations/0041_remove_listresearch_research_ptr_and_more.py +129 -0
  202. wbportfolio/migrations/0042_instrumentlist_instrumentlistthroughmodel_and_more.py +71 -0
  203. wbportfolio/migrations/0043_alter_instrumentlistthroughmodel_options_and_more.py +238 -0
  204. wbportfolio/migrations/0044_alter_instrumentlist_identifier.py +35 -0
  205. wbportfolio/migrations/0045_alter_instrument_financial_instrument.py +26 -0
  206. wbportfolio/migrations/0046_add_product_default_account.py +166 -0
  207. wbportfolio/migrations/0047_remove_product_default_sub_account.py +14 -0
  208. wbportfolio/migrations/0048_alter_trade_status.py +29 -0
  209. wbportfolio/migrations/0049_trade_claimed_shares.py +25 -0
  210. wbportfolio/migrations/0050_fees_fee_date_fees_wbportfolio_transac_1f7a29_idx.py +44 -0
  211. wbportfolio/migrations/0051_delete_macroreview.py +11 -0
  212. wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +888 -0
  213. wbportfolio/migrations/0053_remove_product_group.py +132 -0
  214. wbportfolio/migrations/0054_portfolioinstrumentpreferredclassificationthroughmodel_and_more.py +270 -0
  215. wbportfolio/migrations/0055_remove_product__custom_management_rebates_and_more.py +139 -0
  216. wbportfolio/migrations/0056_remove_companyportfoliodata_assets_under_management_currency_and_more.py +56 -0
  217. wbportfolio/migrations/0057_alter_portfolio_preferred_instrument_classifications_and_more.py +36 -0
  218. wbportfolio/migrations/0058_pmsinstrument.py +23 -0
  219. wbportfolio/migrations/0059_fees_unique_fees.py +51 -0
  220. wbportfolio/migrations/0060_alter_portfolioportfoliothroughmodel_type.py +21 -0
  221. wbportfolio/migrations/0061_portfolio_bank_accounts_product_bank_account_and_more.py +175 -0
  222. wbportfolio/migrations/0062_alter_dailyportfoliocashflow_options.py +20 -0
  223. wbportfolio/migrations/0063_accountreconciliation_accountreconciliationline_and_more.py +133 -0
  224. wbportfolio/migrations/0064_alter_portfolio_managers_portfolio_is_tracked_and_more.py +40 -0
  225. wbportfolio/migrations/0065_alter_portfolio_managers_claim_as_shares_and_more.py +73 -0
  226. wbportfolio/migrations/0066_assetposition_initial_shares_at_custodian_and_more.py +108 -0
  227. wbportfolio/migrations/0067_assetposition_unique_asset_position.py +77 -0
  228. wbportfolio/migrations/0068_trade_internal_trade_trade_marked_as_internal_and_more.py +59 -0
  229. wbportfolio/migrations/0069_remove_portfolio_is_invested_and_more.py +56 -0
  230. wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +82 -0
  231. wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py +22 -0
  232. wbportfolio/migrations/__init__.py +0 -0
  233. wbportfolio/models/__init__.py +26 -0
  234. wbportfolio/models/adjustments.py +246 -0
  235. wbportfolio/models/asset.py +869 -0
  236. wbportfolio/models/custodians.py +101 -0
  237. wbportfolio/models/indexes.py +33 -0
  238. wbportfolio/models/mixins/__init__.py +0 -0
  239. wbportfolio/models/mixins/instruments.py +127 -0
  240. wbportfolio/models/mixins/liquidity_stress_test.py +1307 -0
  241. wbportfolio/models/portfolio.py +1039 -0
  242. wbportfolio/models/portfolio_cash_flow.py +167 -0
  243. wbportfolio/models/portfolio_cash_targets.py +46 -0
  244. wbportfolio/models/portfolio_relationship.py +135 -0
  245. wbportfolio/models/portfolio_swing_pricings.py +51 -0
  246. wbportfolio/models/product_groups.py +230 -0
  247. wbportfolio/models/products.py +569 -0
  248. wbportfolio/models/reconciliations/__init__.py +2 -0
  249. wbportfolio/models/reconciliations/account_reconciliation_lines.py +192 -0
  250. wbportfolio/models/reconciliations/account_reconciliations.py +102 -0
  251. wbportfolio/models/reconciliations/reconciliations.py +25 -0
  252. wbportfolio/models/registers.py +132 -0
  253. wbportfolio/models/roles.py +208 -0
  254. wbportfolio/models/synchronization/__init__.py +3 -0
  255. wbportfolio/models/synchronization/portfolio_synchronization.py +292 -0
  256. wbportfolio/models/synchronization/price_computation.py +200 -0
  257. wbportfolio/models/synchronization/synchronization.py +188 -0
  258. wbportfolio/models/transactions/__init__.py +7 -0
  259. wbportfolio/models/transactions/claim.py +634 -0
  260. wbportfolio/models/transactions/dividends.py +31 -0
  261. wbportfolio/models/transactions/expiry.py +7 -0
  262. wbportfolio/models/transactions/fees.py +153 -0
  263. wbportfolio/models/transactions/trade_proposals.py +502 -0
  264. wbportfolio/models/transactions/trades.py +704 -0
  265. wbportfolio/models/transactions/transactions.py +211 -0
  266. wbportfolio/models/utils.py +12 -0
  267. wbportfolio/permissions.py +13 -0
  268. wbportfolio/pms/__init__.py +0 -0
  269. wbportfolio/pms/statistics/__init__.py +0 -0
  270. wbportfolio/pms/trading/__init__.py +1 -0
  271. wbportfolio/pms/trading/handler.py +164 -0
  272. wbportfolio/pms/typing.py +194 -0
  273. wbportfolio/preferences.py +6 -0
  274. wbportfolio/reports/__init__.py +0 -0
  275. wbportfolio/reports/monthly_position_report.py +74 -0
  276. wbportfolio/risk_management/__init__.py +0 -0
  277. wbportfolio/risk_management/backends/__init__.py +11 -0
  278. wbportfolio/risk_management/backends/accounts.py +166 -0
  279. wbportfolio/risk_management/backends/controversy_portfolio.py +63 -0
  280. wbportfolio/risk_management/backends/exposure_portfolio.py +203 -0
  281. wbportfolio/risk_management/backends/instrument_list_portfolio.py +89 -0
  282. wbportfolio/risk_management/backends/liquidity_risk.py +86 -0
  283. wbportfolio/risk_management/backends/liquidity_stress_instrument.py +86 -0
  284. wbportfolio/risk_management/backends/mixins.py +220 -0
  285. wbportfolio/risk_management/backends/product_integrity.py +111 -0
  286. wbportfolio/risk_management/backends/stop_loss_instrument.py +24 -0
  287. wbportfolio/risk_management/backends/stop_loss_portfolio.py +36 -0
  288. wbportfolio/risk_management/backends/ucits_portfolio.py +63 -0
  289. wbportfolio/risk_management/tests/__init__.py +0 -0
  290. wbportfolio/risk_management/tests/conftest.py +15 -0
  291. wbportfolio/risk_management/tests/test_accounts.py +98 -0
  292. wbportfolio/risk_management/tests/test_controversy_portfolio.py +33 -0
  293. wbportfolio/risk_management/tests/test_exposure_portfolio.py +94 -0
  294. wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +60 -0
  295. wbportfolio/risk_management/tests/test_liquidity_risk.py +47 -0
  296. wbportfolio/risk_management/tests/test_product_integrity.py +55 -0
  297. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +110 -0
  298. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +119 -0
  299. wbportfolio/risk_management/tests/test_ucits_portfolio.py +39 -0
  300. wbportfolio/serializers/__init__.py +42 -0
  301. wbportfolio/serializers/adjustments.py +24 -0
  302. wbportfolio/serializers/assets.py +166 -0
  303. wbportfolio/serializers/custodians.py +26 -0
  304. wbportfolio/serializers/portfolio_cash_flow.py +48 -0
  305. wbportfolio/serializers/portfolio_cash_targets.py +20 -0
  306. wbportfolio/serializers/portfolio_relationship.py +53 -0
  307. wbportfolio/serializers/portfolio_swing_pricing.py +20 -0
  308. wbportfolio/serializers/portfolios.py +143 -0
  309. wbportfolio/serializers/positions.py +76 -0
  310. wbportfolio/serializers/product_group.py +88 -0
  311. wbportfolio/serializers/products.py +331 -0
  312. wbportfolio/serializers/reconciliations.py +171 -0
  313. wbportfolio/serializers/registers.py +72 -0
  314. wbportfolio/serializers/roles.py +60 -0
  315. wbportfolio/serializers/signals.py +157 -0
  316. wbportfolio/serializers/synchronization.py +18 -0
  317. wbportfolio/serializers/transactions/__init__.py +24 -0
  318. wbportfolio/serializers/transactions/claim.py +310 -0
  319. wbportfolio/serializers/transactions/dividends.py +18 -0
  320. wbportfolio/serializers/transactions/expiry.py +18 -0
  321. wbportfolio/serializers/transactions/fees.py +32 -0
  322. wbportfolio/serializers/transactions/trades.py +315 -0
  323. wbportfolio/serializers/transactions/transactions.py +84 -0
  324. wbportfolio/tasks.py +125 -0
  325. wbportfolio/tests/__init__.py +0 -0
  326. wbportfolio/tests/conftest.py +164 -0
  327. wbportfolio/tests/models/__init__.py +0 -0
  328. wbportfolio/tests/models/test_account_reconciliation.py +191 -0
  329. wbportfolio/tests/models/test_assets.py +193 -0
  330. wbportfolio/tests/models/test_custodians.py +12 -0
  331. wbportfolio/tests/models/test_customer_trades.py +113 -0
  332. wbportfolio/tests/models/test_dividends.py +7 -0
  333. wbportfolio/tests/models/test_imports.py +192 -0
  334. wbportfolio/tests/models/test_instrument_mixins.py +48 -0
  335. wbportfolio/tests/models/test_merge.py +133 -0
  336. wbportfolio/tests/models/test_portfolio_cash_flow.py +112 -0
  337. wbportfolio/tests/models/test_portfolio_cash_targets.py +27 -0
  338. wbportfolio/tests/models/test_portfolio_swing_pricings.py +42 -0
  339. wbportfolio/tests/models/test_portfolios.py +676 -0
  340. wbportfolio/tests/models/test_product_groups.py +80 -0
  341. wbportfolio/tests/models/test_products.py +187 -0
  342. wbportfolio/tests/models/test_roles.py +82 -0
  343. wbportfolio/tests/models/test_splits.py +233 -0
  344. wbportfolio/tests/models/test_synchronization.py +617 -0
  345. wbportfolio/tests/models/transactions/__init__.py +0 -0
  346. wbportfolio/tests/models/transactions/test_claim.py +129 -0
  347. wbportfolio/tests/models/transactions/test_fees.py +65 -0
  348. wbportfolio/tests/models/transactions/test_trades.py +204 -0
  349. wbportfolio/tests/models/utils.py +13 -0
  350. wbportfolio/tests/serializers/__init__.py +0 -0
  351. wbportfolio/tests/serializers/test_claims.py +21 -0
  352. wbportfolio/tests/signals.py +151 -0
  353. wbportfolio/tests/tests.py +31 -0
  354. wbportfolio/tests/viewsets/__init__.py +0 -0
  355. wbportfolio/tests/viewsets/test_assets.py +67 -0
  356. wbportfolio/tests/viewsets/test_performances.py +72 -0
  357. wbportfolio/tests/viewsets/test_products.py +92 -0
  358. wbportfolio/tests/viewsets/transactions/__init__.py +0 -0
  359. wbportfolio/tests/viewsets/transactions/test_claims.py +146 -0
  360. wbportfolio/urls.py +247 -0
  361. wbportfolio/utils.py +30 -0
  362. wbportfolio/viewsets/__init__.py +57 -0
  363. wbportfolio/viewsets/adjustments.py +46 -0
  364. wbportfolio/viewsets/assets.py +562 -0
  365. wbportfolio/viewsets/assets_and_net_new_money_progression.py +117 -0
  366. wbportfolio/viewsets/charts/__init__.py +1 -0
  367. wbportfolio/viewsets/charts/assets.py +247 -0
  368. wbportfolio/viewsets/configs/__init__.py +6 -0
  369. wbportfolio/viewsets/configs/buttons/__init__.py +23 -0
  370. wbportfolio/viewsets/configs/buttons/adjustments.py +13 -0
  371. wbportfolio/viewsets/configs/buttons/assets.py +145 -0
  372. wbportfolio/viewsets/configs/buttons/claims.py +83 -0
  373. wbportfolio/viewsets/configs/buttons/custodians.py +76 -0
  374. wbportfolio/viewsets/configs/buttons/fees.py +14 -0
  375. wbportfolio/viewsets/configs/buttons/mixins.py +88 -0
  376. wbportfolio/viewsets/configs/buttons/portfolios.py +115 -0
  377. wbportfolio/viewsets/configs/buttons/products.py +41 -0
  378. wbportfolio/viewsets/configs/buttons/reconciliations.py +65 -0
  379. wbportfolio/viewsets/configs/buttons/registers.py +11 -0
  380. wbportfolio/viewsets/configs/buttons/signals.py +68 -0
  381. wbportfolio/viewsets/configs/buttons/trade_proposals.py +25 -0
  382. wbportfolio/viewsets/configs/buttons/trades.py +144 -0
  383. wbportfolio/viewsets/configs/display/__init__.py +61 -0
  384. wbportfolio/viewsets/configs/display/adjustments.py +81 -0
  385. wbportfolio/viewsets/configs/display/assets.py +265 -0
  386. wbportfolio/viewsets/configs/display/claim.py +299 -0
  387. wbportfolio/viewsets/configs/display/custodians.py +24 -0
  388. wbportfolio/viewsets/configs/display/esg.py +88 -0
  389. wbportfolio/viewsets/configs/display/fees.py +133 -0
  390. wbportfolio/viewsets/configs/display/portfolio_cash_flow.py +103 -0
  391. wbportfolio/viewsets/configs/display/portfolio_relationship.py +38 -0
  392. wbportfolio/viewsets/configs/display/portfolios.py +125 -0
  393. wbportfolio/viewsets/configs/display/positions.py +75 -0
  394. wbportfolio/viewsets/configs/display/product_groups.py +54 -0
  395. wbportfolio/viewsets/configs/display/product_performance.py +241 -0
  396. wbportfolio/viewsets/configs/display/products.py +249 -0
  397. wbportfolio/viewsets/configs/display/reconciliations.py +151 -0
  398. wbportfolio/viewsets/configs/display/registers.py +71 -0
  399. wbportfolio/viewsets/configs/display/roles.py +49 -0
  400. wbportfolio/viewsets/configs/display/trade_proposals.py +97 -0
  401. wbportfolio/viewsets/configs/display/trades.py +359 -0
  402. wbportfolio/viewsets/configs/display/transactions.py +55 -0
  403. wbportfolio/viewsets/configs/endpoints/__init__.py +75 -0
  404. wbportfolio/viewsets/configs/endpoints/adjustments.py +17 -0
  405. wbportfolio/viewsets/configs/endpoints/assets.py +115 -0
  406. wbportfolio/viewsets/configs/endpoints/claim.py +106 -0
  407. wbportfolio/viewsets/configs/endpoints/custodians.py +6 -0
  408. wbportfolio/viewsets/configs/endpoints/esg.py +14 -0
  409. wbportfolio/viewsets/configs/endpoints/fees.py +26 -0
  410. wbportfolio/viewsets/configs/endpoints/portfolio_relationship.py +23 -0
  411. wbportfolio/viewsets/configs/endpoints/portfolios.py +43 -0
  412. wbportfolio/viewsets/configs/endpoints/positions.py +18 -0
  413. wbportfolio/viewsets/configs/endpoints/product_groups.py +11 -0
  414. wbportfolio/viewsets/configs/endpoints/product_performance.py +29 -0
  415. wbportfolio/viewsets/configs/endpoints/products.py +37 -0
  416. wbportfolio/viewsets/configs/endpoints/reconciliations.py +31 -0
  417. wbportfolio/viewsets/configs/endpoints/roles.py +9 -0
  418. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +17 -0
  419. wbportfolio/viewsets/configs/endpoints/trades.py +82 -0
  420. wbportfolio/viewsets/configs/endpoints/transactions.py +17 -0
  421. wbportfolio/viewsets/configs/menu/__init__.py +30 -0
  422. wbportfolio/viewsets/configs/menu/adjustments.py +8 -0
  423. wbportfolio/viewsets/configs/menu/assets.py +8 -0
  424. wbportfolio/viewsets/configs/menu/claim.py +41 -0
  425. wbportfolio/viewsets/configs/menu/custodians.py +11 -0
  426. wbportfolio/viewsets/configs/menu/fees.py +13 -0
  427. wbportfolio/viewsets/configs/menu/instrument_prices.py +10 -0
  428. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +8 -0
  429. wbportfolio/viewsets/configs/menu/portfolios.py +15 -0
  430. wbportfolio/viewsets/configs/menu/positions.py +14 -0
  431. wbportfolio/viewsets/configs/menu/product_groups.py +10 -0
  432. wbportfolio/viewsets/configs/menu/product_performance.py +25 -0
  433. wbportfolio/viewsets/configs/menu/products.py +15 -0
  434. wbportfolio/viewsets/configs/menu/reconciliations.py +7 -0
  435. wbportfolio/viewsets/configs/menu/registers.py +10 -0
  436. wbportfolio/viewsets/configs/menu/roles.py +16 -0
  437. wbportfolio/viewsets/configs/menu/trades.py +18 -0
  438. wbportfolio/viewsets/configs/menu/transactions.py +8 -0
  439. wbportfolio/viewsets/configs/previews/__init__.py +1 -0
  440. wbportfolio/viewsets/configs/previews/portfolios.py +21 -0
  441. wbportfolio/viewsets/configs/titles/__init__.py +65 -0
  442. wbportfolio/viewsets/configs/titles/adjustments.py +19 -0
  443. wbportfolio/viewsets/configs/titles/assets.py +57 -0
  444. wbportfolio/viewsets/configs/titles/assets_and_net_new_money_progression.py +6 -0
  445. wbportfolio/viewsets/configs/titles/claim.py +81 -0
  446. wbportfolio/viewsets/configs/titles/custodians.py +12 -0
  447. wbportfolio/viewsets/configs/titles/esg.py +10 -0
  448. wbportfolio/viewsets/configs/titles/fees.py +25 -0
  449. wbportfolio/viewsets/configs/titles/instrument_prices.py +20 -0
  450. wbportfolio/viewsets/configs/titles/portfolios.py +32 -0
  451. wbportfolio/viewsets/configs/titles/positions.py +11 -0
  452. wbportfolio/viewsets/configs/titles/product_groups.py +12 -0
  453. wbportfolio/viewsets/configs/titles/product_performance.py +16 -0
  454. wbportfolio/viewsets/configs/titles/products.py +6 -0
  455. wbportfolio/viewsets/configs/titles/registers.py +12 -0
  456. wbportfolio/viewsets/configs/titles/roles.py +23 -0
  457. wbportfolio/viewsets/configs/titles/trades.py +51 -0
  458. wbportfolio/viewsets/configs/titles/transactions.py +8 -0
  459. wbportfolio/viewsets/custodians.py +66 -0
  460. wbportfolio/viewsets/esg.py +165 -0
  461. wbportfolio/viewsets/mixins.py +48 -0
  462. wbportfolio/viewsets/portfolio_cash_flow.py +31 -0
  463. wbportfolio/viewsets/portfolio_cash_targets.py +8 -0
  464. wbportfolio/viewsets/portfolio_relationship.py +46 -0
  465. wbportfolio/viewsets/portfolio_swing_pricing.py +8 -0
  466. wbportfolio/viewsets/portfolios.py +154 -0
  467. wbportfolio/viewsets/positions.py +292 -0
  468. wbportfolio/viewsets/product_groups.py +84 -0
  469. wbportfolio/viewsets/product_performance.py +646 -0
  470. wbportfolio/viewsets/products.py +529 -0
  471. wbportfolio/viewsets/reconciliations.py +160 -0
  472. wbportfolio/viewsets/registers.py +75 -0
  473. wbportfolio/viewsets/roles.py +44 -0
  474. wbportfolio/viewsets/signals.py +42 -0
  475. wbportfolio/viewsets/synchronization.py +25 -0
  476. wbportfolio/viewsets/transactions/__init__.py +40 -0
  477. wbportfolio/viewsets/transactions/claim.py +933 -0
  478. wbportfolio/viewsets/transactions/fees.py +190 -0
  479. wbportfolio/viewsets/transactions/mixins.py +19 -0
  480. wbportfolio/viewsets/transactions/trade_proposals.py +93 -0
  481. wbportfolio/viewsets/transactions/trades.py +395 -0
  482. wbportfolio/viewsets/transactions/transactions.py +123 -0
  483. wbportfolio-2.2.1.dist-info/METADATA +21 -0
  484. wbportfolio-2.2.1.dist-info/RECORD +486 -0
  485. wbportfolio-2.2.1.dist-info/WHEEL +5 -0
  486. wbportfolio-2.2.1.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,869 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+
4
+ from django.contrib import admin
5
+ from django.db import models
6
+ from django.db.models import (
7
+ Case,
8
+ CharField,
9
+ Exists,
10
+ ExpressionWrapper,
11
+ F,
12
+ FloatField,
13
+ OuterRef,
14
+ Q,
15
+ QuerySet,
16
+ Subquery,
17
+ Sum,
18
+ Value,
19
+ When,
20
+ Window,
21
+ )
22
+ from django.db.models.functions import Coalesce
23
+ from django.db.models.signals import post_save
24
+ from django.dispatch import receiver
25
+ from django.utils.functional import cached_property
26
+ from wbcore.contrib.currency.models import CurrencyFXRates
27
+ from wbcore.contrib.io.mixins import ImportMixin
28
+ from wbcore.signals import pre_merge
29
+ from wbcore.utils.enum import ChoiceEnum
30
+ from wbfdm.models import Classification, ClassificationGroup, Instrument
31
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
32
+ from wbfdm.signals import add_instrument_to_investable_universe
33
+ from wbportfolio.import_export.handlers.asset_position import AssetPositionImportHandler
34
+ from wbportfolio.models.portfolio_relationship import (
35
+ InstrumentPortfolioThroughModel,
36
+ PortfolioInstrumentPreferredClassificationThroughModel,
37
+ )
38
+ from wbportfolio.models.roles import PortfolioRole
39
+ from wbportfolio.pms.typing import Position as PositionDTO
40
+
41
+ MARKETCAP_S = 2_000_000_000
42
+ MARKETCAP_M = 10_000_000_000
43
+ MARKETCAP_L = 50_000_000_000
44
+ MARKETCAP_XL = 300_000_000_000
45
+ LIQUIDITY_SMALL = 3.0
46
+ LIQUIDITY_LARGE = 5.0
47
+
48
+ MINUTE = 60
49
+ HOUR = MINUTE * 60
50
+ DAY = HOUR * 24
51
+
52
+
53
+ class AssetPositionDefaultQueryset(QuerySet):
54
+ def annotate_classification_for_group(
55
+ self, classification_group: ClassificationGroup, classification_height: int = 0, **kwargs
56
+ ) -> QuerySet:
57
+ return classification_group.annotate_queryset(self, classification_height, "underlying_instrument", **kwargs)
58
+
59
+ def annotate_preferred_classification_for_group(
60
+ self, classification_group: ClassificationGroup, classification_height: int = 0
61
+ ) -> QuerySet:
62
+ ref_title = f"classification__{'parent__' * classification_height}name"
63
+ ref_code = f"classification__{'parent__' * classification_height}code_aggregated"
64
+
65
+ base_qs = PortfolioInstrumentPreferredClassificationThroughModel.objects.filter(
66
+ classification_group=classification_group,
67
+ instrument=OuterRef("underlying_instrument"),
68
+ instrument__tree_id=models.OuterRef("underlying_instrument__tree_id"),
69
+ instrument__lft__lte=models.OuterRef("underlying_instrument__lft"),
70
+ instrument__rght__gte=models.OuterRef("underlying_instrument__rght"),
71
+ portfolio=models.OuterRef("portfolio"),
72
+ )
73
+ return self.annotate(
74
+ classification_id=Subquery(base_qs.values(ref_code)[:1]),
75
+ classification_title=Subquery(base_qs.values(ref_title)[:1]),
76
+ )
77
+
78
+
79
+ class DefaultAssetPositionManager(models.Manager):
80
+ def annotate_classification_for_group(
81
+ self, classification_group, classification_height: int = 0, **kwargs
82
+ ) -> QuerySet:
83
+ return self.get_queryset().annotate_classification_for_group(
84
+ classification_group, classification_height=classification_height, **kwargs
85
+ )
86
+
87
+ def annotate_preferred_classification_for_group(
88
+ self, classification_group, classification_height: int = 0
89
+ ) -> QuerySet:
90
+ return self.get_queryset().annotate_preferred_classification_for_group(
91
+ classification_group, classification_height=classification_height
92
+ )
93
+
94
+ def get_queryset(self) -> QuerySet["AssetPosition"]:
95
+ return AssetPositionDefaultQueryset(self.model).annotate(
96
+ adjusting_factor=Coalesce(
97
+ F("applied_adjustment__cumulative_factor") * F("applied_adjustment__factor"), Decimal(1.0)
98
+ ),
99
+ shares=F("initial_shares") / F("adjusting_factor"),
100
+ price=Coalesce(F("underlying_instrument_price__net_value"), F("initial_price")),
101
+ market_capitalization=ExpressionWrapper(
102
+ Coalesce(
103
+ F("underlying_instrument_price__market_capitalization_consolidated"),
104
+ F("underlying_instrument_price__market_capitalization"),
105
+ ),
106
+ output_field=models.DecimalField(),
107
+ ),
108
+ beta=ExpressionWrapper(F("underlying_instrument_price__beta"), output_field=models.DecimalField()),
109
+ correlation=ExpressionWrapper(
110
+ F("underlying_instrument_price__correlation"), output_field=models.DecimalField()
111
+ ),
112
+ sharpe_ratio=ExpressionWrapper(
113
+ F("underlying_instrument_price__sharpe_ratio"), output_field=models.DecimalField()
114
+ ),
115
+ volume=Coalesce(
116
+ ExpressionWrapper(F("underlying_instrument_price__volume"), output_field=models.DecimalField()),
117
+ Decimal(0),
118
+ ),
119
+ volume_50d=Coalesce(
120
+ ExpressionWrapper(F("underlying_instrument_price__volume_50d"), output_field=models.DecimalField()),
121
+ Decimal(0),
122
+ ),
123
+ volume_200d=Coalesce(
124
+ ExpressionWrapper(F("underlying_instrument_price__volume_200d"), output_field=models.DecimalField()),
125
+ Decimal(0),
126
+ ),
127
+ currency_fx_rate_instrument_to_usd_rate=Case(
128
+ When(currency_fx_rate_instrument_to_usd__value=0, then=Value(Decimal(1.0))),
129
+ default=1 / F("currency_fx_rate_instrument_to_usd__value"),
130
+ ),
131
+ currency_fx_rate_portfolio_to_usd_rate=F("currency_fx_rate_portfolio_to_usd__value"),
132
+ currency_fx_rate=Coalesce(
133
+ F("currency_fx_rate_portfolio_to_usd_rate") * F("currency_fx_rate_instrument_to_usd_rate"),
134
+ F("initial_currency_fx_rate"),
135
+ ),
136
+ currency_symbol=F("currency__symbol"),
137
+ portfolio_currency_symbol=F("portfolio__currency__symbol"),
138
+ price_fx_usd=F("price") * F("currency_fx_rate_instrument_to_usd_rate"),
139
+ price_fx_portfolio=F("price") * F("currency_fx_rate"),
140
+ total_value=F("price") * F("shares"),
141
+ total_value_fx_usd=F("price") * F("shares") * F("currency_fx_rate_instrument_to_usd_rate"),
142
+ total_value_fx_portfolio=F("price") * F("shares") * F("currency_fx_rate"),
143
+ market_share=Case(
144
+ When(market_capitalization=0, then=Value(None)),
145
+ default=F("total_value") / F("market_capitalization"),
146
+ ),
147
+ liquidity=Case(
148
+ When(volume_50d=0, then=Value(None)),
149
+ default=ExpressionWrapper(F("shares") / F("volume_50d") / 0.33, output_field=FloatField()),
150
+ ),
151
+ market_capitalization_usd=F("market_capitalization") * F("currency_fx_rate_instrument_to_usd_rate"),
152
+ volume_usd=ExpressionWrapper(
153
+ (F("price_fx_portfolio") * F("currency_fx_rate_instrument_to_usd_rate") * F("volume_50d")),
154
+ output_field=FloatField(),
155
+ ),
156
+ is_invested=Case(
157
+ When(
158
+ Q(portfolio__invested_timespan__startswith__lte=F("date"))
159
+ & Q(portfolio__invested_timespan__endswith__gt=F("date")),
160
+ then=Value(True),
161
+ ),
162
+ default=Value(False),
163
+ output_field=models.BooleanField(),
164
+ ),
165
+ underlying_security=Case( # Annotate the parent security if exists
166
+ When(underlying_instrument__parent__isnull=False, then=F("underlying_instrument__parent")),
167
+ default=F("underlying_instrument"),
168
+ ),
169
+ underlying_security_instrument_type_key=Case( # Annotate the parent security if exists
170
+ When(
171
+ underlying_instrument__parent__isnull=False,
172
+ then=F("underlying_instrument__parent__instrument_type__key"),
173
+ ),
174
+ default=F("underlying_instrument__instrument_type__key"),
175
+ ),
176
+ )
177
+
178
+
179
+ class AnalyticalAssetPositionManager(DefaultAssetPositionManager):
180
+ def get_queryset(self):
181
+ qs_default = super().get_queryset()
182
+ return qs_default.annotate(
183
+ last_portfolio_date=Subquery(
184
+ qs_default.filter(
185
+ portfolio=OuterRef("portfolio"),
186
+ date__lt=OuterRef("date"),
187
+ )
188
+ .order_by("-date")
189
+ .values("date")[:1]
190
+ ),
191
+ previous_price_usd=Subquery(
192
+ qs_default.filter(
193
+ date=OuterRef("last_portfolio_date"),
194
+ underlying_instrument=OuterRef("underlying_instrument"),
195
+ portfolio=OuterRef("portfolio"),
196
+ )
197
+ .order_by("-date")
198
+ .values("price_fx_usd")[:1]
199
+ ),
200
+ performance=ExpressionWrapper(F("price_fx_usd") / F("previous_price_usd") - 1, output_field=FloatField()),
201
+ contribution=ExpressionWrapper(F("performance") * F("weighting"), output_field=FloatField()),
202
+ cumulative_contribution=Window(Sum("contribution"), order_by=F("date").asc()),
203
+ )
204
+
205
+
206
+ class AssetPositionGroupBy(ChoiceEnum):
207
+ INDUSTRY = "Industry"
208
+ COUNTRY = "Country"
209
+ CURRENCY = "Currency"
210
+ CASH = "Cash"
211
+ MARKET_CAPITALIZATION = "Market Cap"
212
+ LIQUIDITY = "Liquidity"
213
+
214
+ @classmethod
215
+ def get_class_method_group_by(cls, name: str):
216
+ _map = {
217
+ "INDUSTRY": "industry",
218
+ "COUNTRY": AssetPosition.country_group_by,
219
+ "CURRENCY": AssetPosition.currency_group_by,
220
+ "CASH": AssetPosition.cash_group_by,
221
+ "MARKET_CAPITALIZATION": AssetPosition.marketcap_group_by,
222
+ "LIQUIDITY": AssetPosition.liquidity_group_by,
223
+ }
224
+ return _map[name]
225
+
226
+
227
+ class AssetPosition(ImportMixin, models.Model):
228
+ """
229
+ The Asset Model holds all information needed to compute the value of the asset and contribution to its wbportfolio. All
230
+ information which are regarded as Meta-Information, such as country, industry, currency allocation is held in the
231
+ asset information, which is accessible through a FK, depending on the asset type of the asset.
232
+ """
233
+
234
+ import_export_handler_class = AssetPositionImportHandler
235
+ is_estimated = models.BooleanField(
236
+ default=False,
237
+ verbose_name="Estimated Asset",
238
+ help_text="True if the data is "
239
+ "forward-estimated "
240
+ "based on last day "
241
+ "data. If the data is "
242
+ "overridden by the "
243
+ "importer or by the "
244
+ "synchronization, "
245
+ "this field becomes "
246
+ "False.",
247
+ )
248
+
249
+ date = models.DateField(default=date.today)
250
+
251
+ initial_price = models.DecimalField(max_digits=16, decimal_places=4, verbose_name="Initial Price")
252
+ initial_currency_fx_rate = models.DecimalField(
253
+ decimal_places=14,
254
+ max_digits=28,
255
+ verbose_name="Initial Currency FX Rate",
256
+ help_text="The Currency Exchange Rate that is applied to the Asset to convert it into the Portfolio's currency.",
257
+ default=Decimal(1),
258
+ )
259
+ initial_shares = models.DecimalField(
260
+ decimal_places=4,
261
+ max_digits=18,
262
+ null=True,
263
+ blank=True,
264
+ verbose_name="Initial Quantity",
265
+ help_text="The amount of Units of the Asset on the price date of the Asset.",
266
+ )
267
+ applied_adjustment = models.ForeignKey(
268
+ "wbportfolio.Adjustment", on_delete=models.SET_NULL, blank=True, null=True, related_name="adjustmented_assets"
269
+ )
270
+
271
+ asset_valuation_date = models.DateField(
272
+ verbose_name="Alternate Valuation Date",
273
+ help_text="An alternate Valuation Date, if the price date of the Asset is different from the overlying Portfolio.",
274
+ )
275
+
276
+ exchange = models.ForeignKey(
277
+ to="wbfdm.Exchange",
278
+ null=True,
279
+ blank=True,
280
+ related_name="assets",
281
+ on_delete=models.PROTECT,
282
+ verbose_name="Exchange",
283
+ help_text="The exchange where this asset is.",
284
+ )
285
+
286
+ ########################################################
287
+ # VALUATION #
288
+ ########################################################
289
+
290
+ class PriceDenotation(models.TextChoices):
291
+ PERCENT = "PERCENT", "%"
292
+ CURRENCY = "CURRENCY", "Currency"
293
+
294
+ price_denotation = models.CharField(
295
+ max_length=16,
296
+ choices=PriceDenotation.choices,
297
+ default=PriceDenotation.CURRENCY,
298
+ verbose_name="Price Denotation",
299
+ help_text="The denotation of the price.",
300
+ )
301
+
302
+ # adjusting_factor = models.DecimalField(
303
+ # decimal_places=4,
304
+ # max_digits=16,
305
+ # default=Decimal(1),
306
+ # verbose_name="Share Multiplier",
307
+ # help_text="The Share Multiplier of an Asset.",
308
+ # )
309
+
310
+ weighting = models.DecimalField(
311
+ decimal_places=6,
312
+ max_digits=7,
313
+ default=Decimal(0),
314
+ verbose_name="Weight",
315
+ help_text="The Weight of the Asset on the price date of the Asset.",
316
+ )
317
+
318
+ ########################################################
319
+ # PORTFOLIO #
320
+ ########################################################
321
+
322
+ portfolio = models.ForeignKey(
323
+ to="wbportfolio.Portfolio",
324
+ related_name="assets",
325
+ on_delete=models.CASCADE,
326
+ verbose_name="Portfolio",
327
+ help_text="The Portfolio the Asset belongs to.",
328
+ )
329
+
330
+ portfolio_created = models.ForeignKey(
331
+ to="wbportfolio.Portfolio",
332
+ null=True,
333
+ blank=True,
334
+ related_name="assets_created",
335
+ on_delete=models.CASCADE,
336
+ verbose_name="Portfolio Created",
337
+ help_text="The Portfolio that created the Asset.",
338
+ )
339
+
340
+ ########################################################
341
+ # CURRENCY #
342
+ ########################################################
343
+
344
+ currency = models.ForeignKey(
345
+ to="currency.Currency",
346
+ related_name="portfolio_currencies_asset",
347
+ on_delete=models.PROTECT,
348
+ verbose_name="Currency",
349
+ help_text="The Currency of the Asset.",
350
+ )
351
+
352
+ currency_fx_rate_instrument_to_usd = models.ForeignKey(
353
+ to="currency.CurrencyFXRates",
354
+ related_name="instrument_assets",
355
+ on_delete=models.PROTECT,
356
+ blank=True,
357
+ null=True,
358
+ verbose_name="Instrument Currency Rate",
359
+ help_text="Rate to between instrument currency and USD",
360
+ )
361
+ currency_fx_rate_portfolio_to_usd = models.ForeignKey(
362
+ to="currency.CurrencyFXRates",
363
+ related_name="portfolio_assets",
364
+ on_delete=models.PROTECT,
365
+ blank=True,
366
+ null=True,
367
+ verbose_name="Portfolio Currency Rate",
368
+ help_text="Rate to between portfolio currency and USD",
369
+ )
370
+ ########################################################
371
+ # Underlying #
372
+ ########################################################
373
+
374
+ underlying_instrument = models.ForeignKey(
375
+ to="wbfdm.Instrument",
376
+ related_name="assets",
377
+ limit_choices_to=models.Q(children__isnull=True),
378
+ on_delete=models.PROTECT,
379
+ verbose_name="Underlying Instrument",
380
+ help_text="The instrument that is this asset.",
381
+ )
382
+
383
+ underlying_instrument_price = models.ForeignKey(
384
+ to="wbfdm.InstrumentPrice",
385
+ related_name="assets",
386
+ on_delete=models.SET_NULL,
387
+ blank=True,
388
+ null=True,
389
+ verbose_name="Underlying Instrument Price",
390
+ help_text="The instrument price that is this asset.",
391
+ )
392
+ # objects = models.Manager()
393
+ objects = DefaultAssetPositionManager()
394
+ analytical_objects = AnalyticalAssetPositionManager()
395
+ unannotated_objects = models.Manager()
396
+
397
+ def save(self, *args, create_underlying_instrument_price_if_missing: bool = False, **kwargs):
398
+ if not getattr(self, "currency", None):
399
+ self.currency = self.underlying_instrument.currency
400
+ if not self.asset_valuation_date:
401
+ self.asset_valuation_date = self.date
402
+
403
+ if not self.underlying_instrument_price:
404
+ try:
405
+ # We get only the instrument price (and don't create it) because we don't want to create product instrument price on asset position propagation
406
+ # Instead, we deciced to opt for a post_save based system that will assign the missing position price when a price is created
407
+ self.underlying_instrument_price = InstrumentPrice.objects.get(
408
+ calculated=False, instrument=self.underlying_instrument, date=self.asset_valuation_date
409
+ )
410
+ except InstrumentPrice.DoesNotExist:
411
+ # if we create instrument price automatically, we need to ensure that the position is not estimated and not from a fake portfolio (e.g. JPM morgan root portfolio)
412
+ if create_underlying_instrument_price_if_missing and not self.is_estimated:
413
+ net_value = self.initial_price
414
+ # in case the position currency and the linked underlying_instrument currency don't correspond, we convert the rate accordingly
415
+ if self.currency != self.underlying_instrument.currency:
416
+ net_value *= self.currency.convert(
417
+ self.asset_valuation_date, self.underlying_instrument.currency
418
+ )
419
+ self.underlying_instrument_price = InstrumentPrice.objects.create(
420
+ calculated=False,
421
+ instrument=self.underlying_instrument,
422
+ date=self.asset_valuation_date,
423
+ net_value=net_value,
424
+ )
425
+ else: # sometime, the asset valuation date does not correspond to a valid market date. In that case, we get the latest valid instrument price for that product
426
+ self.underlying_instrument_price = (
427
+ InstrumentPrice.objects.filter(
428
+ calculated=False,
429
+ instrument=self.underlying_instrument,
430
+ date__lte=self.asset_valuation_date,
431
+ )
432
+ .order_by("date")
433
+ .last()
434
+ )
435
+
436
+ if not self.currency_fx_rate_instrument_to_usd or (self.currency_fx_rate_instrument_to_usd.date != self.date):
437
+ rates = CurrencyFXRates.objects.filter(date__lte=self.date, currency=self.underlying_instrument.currency)
438
+ if rates.exists():
439
+ self.currency_fx_rate_instrument_to_usd = rates.latest("date")
440
+
441
+ if not self.currency_fx_rate_portfolio_to_usd or (self.currency_fx_rate_portfolio_to_usd.date != self.date):
442
+ rates = CurrencyFXRates.objects.filter(date__lte=self.date, currency=self.portfolio.currency)
443
+ if rates.exists():
444
+ self.currency_fx_rate_portfolio_to_usd = rates.latest("date")
445
+ super().save(*args, **kwargs)
446
+
447
+ class Meta:
448
+ verbose_name = "Asset Position"
449
+ verbose_name_plural = "Asset Positions"
450
+ indexes = [
451
+ models.Index(fields=["date", "underlying_instrument", "portfolio"]),
452
+ models.Index(fields=["date", "underlying_instrument"]),
453
+ ]
454
+ constraints = [
455
+ models.CheckConstraint(
456
+ check=~models.Q(date__week_day__in=[1, 7]),
457
+ name="%(app_label)s_%(class)s_weekday_constraint",
458
+ ),
459
+ models.UniqueConstraint(
460
+ fields=["portfolio", "date", "underlying_instrument", "portfolio_created"],
461
+ name="unique_asset_position",
462
+ nulls_distinct=False,
463
+ ),
464
+ ]
465
+
466
+ def __str__(self):
467
+ return f"{self.initial_price} - {self.initial_shares} ({self.date}) ({str(self.underlying_instrument)})"
468
+
469
+ def set_weighting(self, new_weighting: Decimal):
470
+ # Use this method to set the new weighting and ensure that the relative shares are updated accordingly
471
+ self.weighting = new_weighting
472
+ if self.initial_shares is not None:
473
+ if self.weighting == 0 or self.initial_shares == 0:
474
+ self.initial_shares = new_weighting * self.get_total_value_portfolio()
475
+ else:
476
+ self.initial_shares = (new_weighting / self.weighting) * self.initial_shares
477
+ self.save()
478
+
479
+ def get_total_value_portfolio(self) -> Decimal:
480
+ return self.portfolio.assets.filter(date=self.date).aggregate(s=Sum("total_value_fx_portfolio"))[
481
+ "s"
482
+ ] or Decimal(0.0)
483
+
484
+ def _build_dto(self, new_weight: Decimal = None) -> PositionDTO:
485
+ """
486
+ Data Transfer Object
487
+ Returns:
488
+ DTO position object
489
+ """
490
+ return PositionDTO(
491
+ underlying_instrument=self.underlying_instrument.id,
492
+ weighting=self.weighting if new_weight is None else new_weight,
493
+ shares=self._shares,
494
+ date=self.date,
495
+ asset_valuation_date=self.asset_valuation_date,
496
+ instrument_type=self.underlying_instrument.security_instrument_type.id,
497
+ currency=self.underlying_instrument.currency.id,
498
+ country=self.underlying_instrument.country.id if self.underlying_instrument.country else None,
499
+ is_cash=self.underlying_instrument.is_cash,
500
+ primary_classification=(
501
+ self.underlying_instrument.primary_classification.id
502
+ if self.underlying_instrument.primary_classification
503
+ else None
504
+ ),
505
+ favorite_classification=(
506
+ self.underlying_instrument.favorite_classification.id
507
+ if self.underlying_instrument.favorite_classification
508
+ else None
509
+ ),
510
+ market_capitalization_usd=self._market_capitalization_usd,
511
+ market_share=self._market_share,
512
+ daily_liquidity=self._liquidity,
513
+ volume_usd=self._volume_usd,
514
+ price=self._price,
515
+ currency_fx_rate=self._currency_fx_rate,
516
+ portfolio_created=self.portfolio_created.id if self.portfolio_created else None,
517
+ )
518
+
519
+ @cached_property
520
+ @admin.display(description="Adjusting Factor (adjustment)")
521
+ def _adjusting_factor(self) -> Decimal:
522
+ return (
523
+ self.applied_adjustment.cumulative_factor * self.applied_adjustment.factor
524
+ if self.applied_adjustment
525
+ else Decimal(1.0)
526
+ )
527
+
528
+ @cached_property
529
+ @admin.display(description="Price (Portfolio)")
530
+ def _shares(self) -> Decimal:
531
+ if self.initial_shares:
532
+ return self.initial_shares / self._adjusting_factor
533
+ return Decimal(0)
534
+
535
+ @cached_property
536
+ @admin.display(description="Market Capitalization")
537
+ def _market_capitalization(self) -> float:
538
+ return (
539
+ self.underlying_instrument_price.market_capitalization_consolidated
540
+ if self.underlying_instrument_price
541
+ else None
542
+ )
543
+
544
+ @cached_property
545
+ @admin.display(description="Volume 50d")
546
+ def _volume_50d(self) -> float:
547
+ return self.underlying_instrument_price.volume_50d if self.underlying_instrument_price else None
548
+
549
+ @cached_property
550
+ @admin.display(description="Price (Instrument)")
551
+ def _price(self) -> Decimal:
552
+ return self.underlying_instrument_price.net_value if self.underlying_instrument_price else self.initial_price
553
+
554
+ @cached_property
555
+ @admin.display(description="FX rate")
556
+ def _currency_fx_rate(self) -> Decimal:
557
+ return (
558
+ self.currency_fx_rate_portfolio_to_usd.value / self.currency_fx_rate_instrument_to_usd.value
559
+ if (self.currency_fx_rate_portfolio_to_usd and self.currency_fx_rate_instrument_to_usd)
560
+ else self.initial_currency_fx_rate
561
+ )
562
+
563
+ @cached_property
564
+ @admin.display(description="Price (Portfolio)")
565
+ def _price_fx_portfolio(self) -> Decimal:
566
+ return self._price * self._currency_fx_rate
567
+
568
+ @cached_property
569
+ @admin.display(description="Total Value (Instrument)")
570
+ def _total_value(self) -> Decimal:
571
+ if self._shares is not None:
572
+ return self._price * self._shares
573
+ return Decimal(0)
574
+
575
+ @cached_property
576
+ @admin.display(description="Total Value (USD)")
577
+ def _total_value_fx_usd(self) -> Decimal:
578
+ fx_rate = (
579
+ self.currency_fx_rate_instrument_to_usd.value if self.currency_fx_rate_instrument_to_usd else Decimal(1.0)
580
+ )
581
+ if self._shares is not None:
582
+ return self._price * self._shares * fx_rate
583
+ return Decimal(0)
584
+
585
+ @cached_property
586
+ @admin.display(description="Total Value (Portfolio)")
587
+ def _total_value_fx_portfolio(self) -> Decimal:
588
+ if self._shares is not None:
589
+ return self._price * self._shares * self._currency_fx_rate
590
+ return Decimal(0)
591
+
592
+ @cached_property
593
+ @admin.display(description="Market Share")
594
+ def _market_share(self) -> Decimal:
595
+ if self._total_value is not None and self._market_capitalization:
596
+ return self._total_value / Decimal(self._market_capitalization)
597
+ return Decimal(0)
598
+
599
+ @cached_property
600
+ @admin.display(description="Liquidity")
601
+ def _liquidity(self) -> float:
602
+ if self._total_value is not None and self._volume_50d:
603
+ return float(self._shares) / self._volume_50d / 0.33
604
+ return 0.0
605
+
606
+ @cached_property
607
+ @admin.display(description="Market Capitalization (USD)")
608
+ def _market_capitalization_usd(self) -> float:
609
+ if self._market_capitalization is not None and self.currency_fx_rate_instrument_to_usd.value is not None:
610
+ return self._market_capitalization / float(self.currency_fx_rate_instrument_to_usd.value)
611
+ return 0.0
612
+
613
+ @cached_property
614
+ @admin.display(description="Volume (USD)")
615
+ def _volume_usd(self) -> float:
616
+ if (
617
+ self._price_fx_portfolio is not None
618
+ and self.currency_fx_rate_instrument_to_usd.value is not None
619
+ and self._volume_50d is not None
620
+ ):
621
+ return (
622
+ float(self._price_fx_portfolio)
623
+ * float(self.currency_fx_rate_instrument_to_usd.value)
624
+ * self._volume_50d
625
+ )
626
+ return 0.0
627
+
628
+ @classmethod
629
+ def get_endpoint_basename(cls) -> str:
630
+ return "wbportfolio:assetposition"
631
+
632
+ @classmethod
633
+ def currency_group_by(cls, qs, field_name: str | None = "key"):
634
+ return (
635
+ qs.filter(currency__isnull=False)
636
+ .values("currency")
637
+ .annotate(
638
+ groupby_id=models.F("currency__id"),
639
+ aggregated_title=models.F(f"currency__{field_name}"),
640
+ )
641
+ )
642
+
643
+ @classmethod
644
+ def country_group_by(cls, qs, field_name: str | None = "name"):
645
+ return (
646
+ qs.filter(underlying_instrument__country__isnull=False)
647
+ .values("underlying_instrument__country")
648
+ .annotate(
649
+ groupby_id=models.F("underlying_instrument__country__id"),
650
+ aggregated_title=models.F(f"underlying_instrument__country__{field_name}"),
651
+ )
652
+ )
653
+
654
+ @classmethod
655
+ def exchange_group_by(cls, qs, field_name: str | None = "name"):
656
+ return qs.values("exchange").annotate(
657
+ groupby_id=models.F("exchange"),
658
+ aggregated_title=models.F(f"exchange__{field_name}"),
659
+ )
660
+
661
+ @classmethod
662
+ def cash_group_by(cls, qs, **kwargs):
663
+ return (
664
+ qs.annotate(
665
+ underlying_security_instrument_type_name_repr=Case( # Annotate the parent security if exists
666
+ When(
667
+ underlying_instrument__parent__isnull=False,
668
+ then=F("underlying_instrument__parent__instrument_type__name_repr"),
669
+ ),
670
+ default=F("underlying_instrument__instrument_type__name_repr"),
671
+ ),
672
+ is_cash=Case(
673
+ When(
674
+ Q(underlying_instrument__is_cash=True) | Q(underlying_instrument__is_cash_equivalent=True),
675
+ then=Value("Cash"),
676
+ ),
677
+ default=F("underlying_security_instrument_type_name_repr"),
678
+ output_field=CharField(),
679
+ ),
680
+ )
681
+ .values("is_cash")
682
+ .annotate(groupby_id=models.F("is_cash"), aggregated_title=models.F("is_cash"))
683
+ )
684
+
685
+ @classmethod
686
+ def equity_group_by(cls, qs, field_name: str | None = "name"):
687
+ return (
688
+ qs.filter(underlying_instrument__isnull=False)
689
+ .values("underlying_instrument")
690
+ .annotate(
691
+ groupby_id=models.F("underlying_instrument__id"),
692
+ aggregated_title=models.F(f"underlying_instrument__{field_name}"),
693
+ )
694
+ )
695
+
696
+ @classmethod
697
+ def marketcap_group_by(cls, qs, **kwargs):
698
+ qs = qs.filter(market_capitalization_usd__isnull=False).annotate(
699
+ mktcap_allocation=Case(
700
+ When(market_capitalization_usd__isnull=True, then=Value("None")),
701
+ When(market_capitalization_usd__gt=MARKETCAP_XL, then=Value("> 300B")),
702
+ When(
703
+ market_capitalization_usd__gt=MARKETCAP_L,
704
+ then=Value("50B to 300B"),
705
+ ),
706
+ When(
707
+ market_capitalization_usd__gt=MARKETCAP_M,
708
+ then=Value("10B to 50B"),
709
+ ),
710
+ When(
711
+ market_capitalization_usd__gt=MARKETCAP_S,
712
+ then=Value("2B to 10B"),
713
+ ),
714
+ default=Value("< 2B"),
715
+ output_field=CharField(),
716
+ )
717
+ )
718
+ return qs.values("mktcap_allocation").annotate(
719
+ groupby_id=F("mktcap_allocation"), aggregated_title=F("mktcap_allocation")
720
+ )
721
+
722
+ @classmethod
723
+ def liquidity_group_by(cls, qs, **kwargs):
724
+ qs = qs.annotate(
725
+ liquidity_allocation=Case(
726
+ When(liquidity__isnull=True, then=Value("None")),
727
+ When(
728
+ liquidity__gt=LIQUIDITY_LARGE,
729
+ then=Value(f"More than {int(LIQUIDITY_LARGE)} days to liquidate"),
730
+ ),
731
+ When(
732
+ liquidity__lt=LIQUIDITY_SMALL,
733
+ then=Value(f"Less than {int(LIQUIDITY_SMALL)} days to liquidate"),
734
+ ),
735
+ default=Value(f"{int(LIQUIDITY_SMALL)} to {int(LIQUIDITY_LARGE)} days to liquidate"),
736
+ output_field=CharField(),
737
+ ),
738
+ )
739
+ return qs.values("liquidity_allocation").annotate(
740
+ groupby_id=F("liquidity_allocation"), aggregated_title=F("liquidity_allocation")
741
+ )
742
+
743
+ @classmethod
744
+ def group_by_primary(cls, qs: models.QuerySet, height: int = 0, **kwargs):
745
+ qs = (
746
+ qs.annotate_classification_for_group(
747
+ ClassificationGroup.objects.get(is_primary=True), classification_height=height, unique=True
748
+ )
749
+ .annotate(
750
+ classification_id=F("classifications"),
751
+ classification_title=Subquery(
752
+ Classification.objects.filter(id=OuterRef("classifications")).values("name")[:1]
753
+ ),
754
+ )
755
+ .filter(classification_id__isnull=False)
756
+ )
757
+ return qs.values("classification_id").annotate(
758
+ groupby_id=F("classification_id"), aggregated_title=F("classification_title")
759
+ )
760
+
761
+ @classmethod
762
+ def group_by_preferred_classification(cls, qs: models.QuerySet, height: int, **kwargs):
763
+ qs = qs.annotate_preferred_classification_for_group(
764
+ ClassificationGroup.objects.get(is_primary=True), classification_height=height
765
+ )
766
+ return qs.values("classification_id").annotate(
767
+ id=F("classification_id"), aggregated_title=F("classification_title")
768
+ )
769
+
770
+ @classmethod
771
+ def get_shown_positions(cls, person):
772
+ from wbportfolio.models.portfolio import Portfolio
773
+
774
+ today = date.today()
775
+ if person.user_account.is_superuser:
776
+ return AssetPosition.objects.all()
777
+ else:
778
+ portfolios = Portfolio.objects.annotate(
779
+ nb_roles=Coalesce(
780
+ Subquery(
781
+ PortfolioRole.objects.filter(
782
+ (
783
+ (
784
+ Q(person=person)
785
+ & Q(
786
+ role_type__in=[
787
+ PortfolioRole.RoleType.MANAGER,
788
+ PortfolioRole.RoleType.RISK_MANAGER,
789
+ ]
790
+ )
791
+ )
792
+ | (
793
+ Q(person=person)
794
+ & Q(
795
+ role_type__in=[
796
+ PortfolioRole.RoleType.PORTFOLIO_MANAGER,
797
+ PortfolioRole.RoleType.ANALYST,
798
+ ]
799
+ )
800
+ & (Q(instrument=OuterRef("instruments")) | Q(instrument__isnull=True))
801
+ )
802
+ )
803
+ & (Q(start__isnull=True) | Q(start__lte=today))
804
+ & (Q(end__isnull=True) | Q(end__gte=today))
805
+ )
806
+ .annotate(c=models.Count("*"))
807
+ .values("c")[:1]
808
+ ),
809
+ 0,
810
+ )
811
+ ).filter(nb_roles__gt=0)
812
+ return AssetPosition.objects.filter(portfolio__id__in=portfolios.values_list("id", flat=True))
813
+
814
+ @classmethod
815
+ def get_invested_instruments(cls, only_on_date: date, portfolio=None):
816
+ product_portfolios = InstrumentPortfolioThroughModel.objects.filter(instrument__instrument_type__key="product")
817
+ if portfolio:
818
+ product_portfolios = product_portfolios.filter(portfolio=portfolio)
819
+ asset_positions = AssetPosition.objects.filter(
820
+ portfolio__in=product_portfolios.values("portfolio"), date=only_on_date
821
+ )
822
+ return (
823
+ Instrument.annotated_objects.filter(is_investable_universe=True)
824
+ .annotate(has_position=Exists(asset_positions.filter(underlying_instrument=OuterRef("id"))))
825
+ .filter(has_position=True)
826
+ )
827
+
828
+
829
+ @receiver(post_save, sender="wbfdm.InstrumentPrice")
830
+ def post_instrument_price_creation(sender, instance, created, raw, **kwargs):
831
+ if not raw and created and not instance.calculated:
832
+ AssetPosition.objects.filter(
833
+ Q(asset_valuation_date=instance.date)
834
+ & Q(underlying_instrument=instance.instrument)
835
+ & (
836
+ Q(underlying_instrument_price__isnull=True)
837
+ | ~Q(asset_valuation_date=F("underlying_instrument_price__date"))
838
+ )
839
+ ).update(underlying_instrument_price=instance)
840
+
841
+
842
+ @receiver(pre_merge, sender="wbfdm.Instrument")
843
+ def pre_merge_instrument(sender: models.Model, merged_object: Instrument, main_object: Instrument, **kwargs):
844
+ """
845
+ Simply reassign the instrument price linked to the merged instrument to the main instrument if they don't already exist. Otherwise, delete them
846
+ """
847
+ merged_object.assets.annotate(
848
+ new_price=InstrumentPrice.objects.filter(
849
+ instrument=main_object, date=OuterRef("date"), calculated=False
850
+ ).values("id")[:1]
851
+ ).update(underlying_instrument=main_object, underlying_instrument_price=F("new_price"))
852
+
853
+
854
+ @receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
855
+ def add_instrument_to_investable_universe(sender: models.Model, **kwargs) -> list[int]:
856
+ """
857
+ register all instrument linked to assets as within the investible universe
858
+ """
859
+ return list(
860
+ (
861
+ Instrument.objects.annotate(
862
+ assets_exists=Exists(
863
+ AssetPosition.objects.filter(portfolio__is_tracked=True, underlying_instrument=OuterRef("pk"))
864
+ )
865
+ ).filter(Q(assets_exists=True) | Q(portfolios__isnull=False))
866
+ )
867
+ .distinct()
868
+ .values_list("id", flat=True)
869
+ )