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,529 @@
1
+ from collections import defaultdict
2
+ from datetime import date, timedelta
3
+ from decimal import Decimal
4
+
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ from django.contrib.messages import info
9
+ from django.db.models import (
10
+ ExpressionWrapper,
11
+ F,
12
+ FloatField,
13
+ OuterRef,
14
+ Q,
15
+ Subquery,
16
+ Sum,
17
+ )
18
+ from django.db.models.functions import Coalesce
19
+ from django.dispatch import receiver
20
+ from django.utils.functional import cached_property
21
+ from wbcore import viewsets
22
+ from wbcore.cache.decorators import cache_table
23
+ from wbcore.contrib.currency.models import Currency, CurrencyFXRates
24
+ from wbcore.permissions.permissions import InternalUserPermissionMixin
25
+ from wbcore.signals.instance_buttons import add_instance_button
26
+ from wbcore.utils.date import (
27
+ get_date_interval_from_request,
28
+ get_next_day_timedelta,
29
+ get_start_and_end_date_from_date,
30
+ )
31
+ from wbcore.utils.figures import (
32
+ get_default_timeserie_figure,
33
+ get_hovertemplate_timeserie,
34
+ )
35
+ from wbcore.utils.strings import format_number
36
+ from wbfdm.contrib.metric.backends.performances import PERFORMANCE_METRIC
37
+ from wbfdm.contrib.metric.viewsets.mixins import InstrumentMetricMixin
38
+ from wbfdm.models import Instrument, InstrumentPrice
39
+ from wbportfolio import serializers
40
+ from wbportfolio.filters import (
41
+ BaseProductFilterSet,
42
+ ProductCustomerFilter,
43
+ ProductFeeFilter,
44
+ ProductFilter,
45
+ )
46
+ from wbportfolio.models import Fees, Product, Trade
47
+ from wbportfolio.viewsets.configs.buttons.mixins import InstrumentButtonMixin
48
+
49
+ from .configs import (
50
+ AUMProductEndpointConfig,
51
+ AUMProductTitleConfig,
52
+ InstrumentPriceAUMDataEndpointConfig,
53
+ InstrumentPriceAUMDataTitleConfig,
54
+ NominalProductEndpointConfig,
55
+ NominalProductTitleConfig,
56
+ ProductButtonConfig,
57
+ ProductCustomerButtonConfig,
58
+ ProductCustomerDisplayConfig,
59
+ ProductCustomerEndpointConfig,
60
+ ProductDisplayConfig,
61
+ ProductPerformanceFeesDisplayConfig,
62
+ ProductPerformanceFeesEndpointConfig,
63
+ ProductPerformanceFeesTitleConfig,
64
+ )
65
+ from .mixins import UserPortfolioRequestPermissionMixin
66
+
67
+
68
+ class ProductRepresentationViewSet(viewsets.RepresentationViewSet):
69
+ IDENTIFIER = "wbportfolio:product"
70
+ serializer_class = serializers.ProductRepresentationSerializer
71
+ filterset_class = BaseProductFilterSet
72
+
73
+ queryset = Product.objects.all()
74
+ ordering_fields = ordering = ("computed_str",)
75
+ search_fields = ("computed_str",)
76
+
77
+ def get_queryset(self):
78
+ return Product.get_products(self.request.user.profile).annotate(bank_name=F("bank__name"))
79
+
80
+
81
+ class ProductModelViewSet(InstrumentMetricMixin, viewsets.ModelViewSet):
82
+ filterset_class = ProductFilter
83
+ queryset = Product.objects.all()
84
+ METRIC_KEYS = (PERFORMANCE_METRIC,)
85
+ METRIC_WITH_PREFIXED_KEYS = True
86
+
87
+ @property
88
+ def metric_basket_class(self):
89
+ return Instrument
90
+
91
+ ordering_fields = (
92
+ "is_invested",
93
+ "name_repr",
94
+ "parent__name__nulls_last",
95
+ "bank__name",
96
+ "current_issuer_fees_percent",
97
+ "currency__key",
98
+ "isin",
99
+ "ticker",
100
+ "last_valuation_date",
101
+ "net_value",
102
+ "assets_under_management",
103
+ "assets_under_management_usd",
104
+ "nnm_weekly",
105
+ "nnm_monthly",
106
+ "nnm_year_to_date",
107
+ "nnm_yearly",
108
+ "tags__title__nulls_last",
109
+ )
110
+ search_fields = (
111
+ "name",
112
+ "isin",
113
+ "ticker",
114
+ "white_label_customers__computed_str",
115
+ "parent__name",
116
+ "computed_str",
117
+ )
118
+ ordering = ["name_repr"]
119
+
120
+ display_config_class = ProductDisplayConfig
121
+ button_config_class = ProductButtonConfig
122
+
123
+ def get_aggregates(self, queryset, paginated_queryset):
124
+ aggregates = {}
125
+ if self.request.user.profile.is_internal or self.request.user.is_superuser:
126
+ df = pd.DataFrame(
127
+ queryset.values(
128
+ "currency",
129
+ "nnm_weekly",
130
+ "nnm_monthly",
131
+ "nnm_year_to_date",
132
+ "nnm_yearly",
133
+ "assets_under_management",
134
+ "assets_under_management_usd",
135
+ "is_invested",
136
+ )
137
+ ) # we use pandas to avoid unnecessary db calls
138
+ product_aggregates = defaultdict(dict)
139
+ if not df.empty:
140
+ currency_symbol_map = dict(Currency.objects.filter(id__in=df.currency).values_list("id", "symbol"))
141
+ for currency_id in df.currency.unique():
142
+ currency_symbol = currency_symbol_map[currency_id]
143
+
144
+ for field in ["nnm_weekly", "nnm_monthly", "nnm_year_to_date", "nnm_yearly"]:
145
+ product_aggregates[field][currency_symbol] = format_number(
146
+ df.loc[df["currency"] == currency_id, field].sum()
147
+ )
148
+
149
+ dff = df.loc[(df["currency"] == currency_id) & (df["is_invested"])]
150
+ product_aggregates["assets_under_management"][currency_symbol] = format_number(
151
+ dff["assets_under_management"].sum()
152
+ )
153
+ product_aggregates["assets_under_management_usd"]["Σ"] = format_number(
154
+ df[df["is_invested"]]["assets_under_management_usd"].sum()
155
+ )
156
+
157
+ # Double Accounting with only is_invested = False
158
+ product_aggregates["assets_under_management_usd"]["Σ (DA)"] = format_number(
159
+ df[~df["is_invested"]]["assets_under_management_usd"].sum()
160
+ )
161
+
162
+ aggregates.update(product_aggregates)
163
+
164
+ return aggregates
165
+
166
+ def get_serializer_class(self):
167
+ if getattr(self, "action", None) == "list":
168
+ return serializers.ProductListModelSerializer
169
+ return serializers.ProductModelSerializer
170
+
171
+ def get_queryset(self):
172
+ today = date.today()
173
+ base_qs = Product.annotate_last_aum(
174
+ Product.get_products(self.request.user.profile, base_qs=super().get_queryset())
175
+ )
176
+ return (
177
+ base_qs.annotate(
178
+ nnm_weekly=Coalesce(
179
+ Subquery(
180
+ Trade.valid_external_customer_trade_objects.filter(
181
+ underlying_instrument=OuterRef("pk"), transaction_date__gte=today - timedelta(days=7)
182
+ )
183
+ .values("underlying_instrument")
184
+ .annotate(sum_aum=Sum("total_value"))
185
+ .values("sum_aum")[:1]
186
+ ),
187
+ Decimal(0),
188
+ ),
189
+ nnm_monthly=Coalesce(
190
+ Subquery(
191
+ Trade.valid_external_customer_trade_objects.filter(
192
+ underlying_instrument=OuterRef("pk"), transaction_date__gte=today - timedelta(days=30)
193
+ )
194
+ .values("underlying_instrument")
195
+ .annotate(sum_aum=Sum("total_value"))
196
+ .values("sum_aum")[:1]
197
+ ),
198
+ Decimal(0),
199
+ ),
200
+ nnm_year_to_date=Coalesce(
201
+ Subquery(
202
+ Trade.valid_external_customer_trade_objects.filter(
203
+ underlying_instrument=OuterRef("pk"), transaction_date__year=today.year
204
+ )
205
+ .values("underlying_instrument")
206
+ .annotate(sum_aum=Sum("total_value"))
207
+ .values("sum_aum")[:1]
208
+ ),
209
+ Decimal(0),
210
+ ),
211
+ nnm_yearly=Coalesce(
212
+ Subquery(
213
+ Trade.valid_external_customer_trade_objects.filter(
214
+ underlying_instrument=OuterRef("pk"), transaction_date__gte=today - timedelta(days=365)
215
+ )
216
+ .values("underlying_instrument")
217
+ .annotate(sum_aum=Sum("total_value"))
218
+ .values("sum_aum")[:1]
219
+ ),
220
+ Decimal(0),
221
+ ),
222
+ )
223
+ .select_related("currency", "bank", "parent")
224
+ .prefetch_related("white_label_customers", "classifications")
225
+ )
226
+
227
+
228
+ class ProductCustomerModelViewSet(viewsets.ModelViewSet):
229
+ IDENTIFIER = "wbportfolio:productcustomer"
230
+
231
+ serializer_class = serializers.ProductCustomerModelSerializer
232
+ filterset_class = ProductCustomerFilter
233
+ queryset = Product.active_objects.all()
234
+
235
+ search_fields = ("name", "isin", "ticker")
236
+ ordering_fields = ("name", "isin", "ticker")
237
+ ordering = ["name"]
238
+
239
+ display_config_class = ProductCustomerDisplayConfig
240
+ button_config_class = ProductCustomerButtonConfig
241
+ endpoint_config_class = ProductCustomerEndpointConfig
242
+
243
+ def get_queryset(self):
244
+ qs = (
245
+ Product.get_products(self.request.user.profile)
246
+ .filter(id__in=Product.active_objects.values("id"))
247
+ .annotate(
248
+ net_value=InstrumentPrice.subquery_closest_value(
249
+ "net_value",
250
+ val_date=None,
251
+ date_name="last_valuation_date",
252
+ instrument_pk_name="pk",
253
+ date_lookup="exact",
254
+ ),
255
+ bank_repr=F("bank__name"),
256
+ currency_symbol=F("currency__symbol"),
257
+ )
258
+ )
259
+ return qs.filter(net_value__isnull=False)
260
+
261
+
262
+ class ProductPerformanceFeesModelViewSet(
263
+ UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, viewsets.ModelViewSet
264
+ ):
265
+ search_fields = ("name", "isin", "ticker", "computed_str")
266
+ ordering_fields = (
267
+ "computed_str",
268
+ "isin",
269
+ "ticker",
270
+ "sum_management_fees",
271
+ "sum_management_fees_usd",
272
+ "sum_performance_fees_net",
273
+ "sum_total",
274
+ "sum_performance_fees_net_usd",
275
+ "sum_total_usd",
276
+ )
277
+ ordering = ["computed_str"]
278
+ queryset = Product.active_objects.all()
279
+
280
+ serializer_class = serializers.ProductFeesModelSerializer
281
+ filterset_class = ProductFeeFilter
282
+
283
+ display_config_class = ProductPerformanceFeesDisplayConfig
284
+ title_config_class = ProductPerformanceFeesTitleConfig
285
+ endpoint_config_class = ProductPerformanceFeesEndpointConfig
286
+
287
+ def get_aggregates(self, queryset, paginated_queryset):
288
+ if not queryset.exists():
289
+ return dict()
290
+ return {
291
+ # NOTE: This does not work at the moment. Somehow grouped by currency is a problem...Weird
292
+ "sum_management_fees_usd": {
293
+ "Σ": format_number(queryset.aggregate(s=Sum(F("sum_management_fees_usd")))["s"])
294
+ },
295
+ "sum_performance_fees_net_usd": {
296
+ "Σ": format_number(queryset.aggregate(s=Sum(F("sum_performance_fees_net_usd")))["s"])
297
+ },
298
+ "sum_total_usd": {"Σ": format_number(queryset.aggregate(s=Sum(F("sum_total_usd")))["s"])},
299
+ }
300
+
301
+ @cached_property
302
+ def latest_rate_date(self) -> date:
303
+ try:
304
+ return CurrencyFXRates.objects.latest("date").date
305
+ except CurrencyFXRates.DoesNotExist:
306
+ return date.today()
307
+
308
+ def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
309
+ return info(request, f"The FX Rates from the {self.latest_rate_date:%d.%m.%Y} were used.")
310
+
311
+ def get_queryset(self):
312
+ if (latest_rate_date := self.latest_rate_date) and self.is_manager:
313
+ # Check for Filter because they are fake
314
+ date_gte, date_lte = get_date_interval_from_request(self.request, exclude_weekend=True)
315
+
316
+ if date_lte is None and date_gte is None:
317
+ date_gte, date_lte = get_start_and_end_date_from_date(date.today())
318
+
319
+ fees = Fees.valid_objects.filter(linked_product=OuterRef("pk"))
320
+ if date_gte:
321
+ fees = fees.filter(transaction_date__gte=date_gte)
322
+ if date_lte:
323
+ fees = fees.filter(transaction_date__lte=date_lte)
324
+
325
+ management_fees = Coalesce(
326
+ Subquery(
327
+ fees.filter(transaction_subtype=Fees.Type.MANAGEMENT)
328
+ .values("linked_product")
329
+ .annotate(sum_management_fees=Sum("total_value"))
330
+ .values("sum_management_fees")[:1],
331
+ output_field=FloatField(),
332
+ ),
333
+ 0.0,
334
+ )
335
+
336
+ performance_fees_net = Coalesce(
337
+ Subquery(
338
+ fees.filter(
339
+ Q(transaction_subtype=Fees.Type.PERFORMANCE)
340
+ | Q(transaction_subtype=Fees.Type.PERFORMANCE_CRYSTALIZED)
341
+ )
342
+ .values("linked_product")
343
+ .annotate(sum_performance_fees_net=Sum("total_value"))
344
+ .values("sum_performance_fees_net")[:1],
345
+ output_field=FloatField(),
346
+ ),
347
+ 0.0,
348
+ )
349
+
350
+ qs = (
351
+ Product.get_products(self.request.user.profile)
352
+ .annotate(
353
+ sum_management_fees=management_fees,
354
+ sum_performance_fees_net=performance_fees_net,
355
+ fx_rate=CurrencyFXRates.get_fx_rates_subquery(
356
+ latest_rate_date, currency="currency", lookup_expr="exact"
357
+ ),
358
+ sum_management_fees_usd=ExpressionWrapper(
359
+ F("sum_management_fees") * F("fx_rate"), output_field=FloatField()
360
+ ),
361
+ sum_performance_fees_net_usd=ExpressionWrapper(
362
+ F("sum_performance_fees_net") * F("fx_rate"),
363
+ output_field=FloatField(),
364
+ ),
365
+ sum_total=F("sum_management_fees") + F("sum_performance_fees_net"),
366
+ sum_total_usd=F("sum_management_fees_usd") + F("sum_performance_fees_net_usd"),
367
+ )
368
+ .select_related("currency")
369
+ .prefetch_related("transactions", "prices")
370
+ )
371
+ return qs
372
+
373
+ return Product.objects.none()
374
+
375
+
376
+ # Product based InstrumentPrice Chartview sets
377
+
378
+
379
+ class NominalProductChartView(viewsets.ChartViewSet):
380
+ IDENTIFIER = "wbportfolio:price"
381
+ filterset_fields = {
382
+ "date": ["gte", "exact", "lte"],
383
+ }
384
+ queryset = InstrumentPrice.objects.all()
385
+
386
+ title_config_class = NominalProductTitleConfig
387
+ endpoint_config_class = NominalProductEndpointConfig
388
+
389
+ def get_plotly(self, queryset):
390
+ fig = get_default_timeserie_figure()
391
+ product = Product.objects.get(id=self.kwargs["product_id"])
392
+ df = pd.DataFrame(queryset.values("date", "outstanding_shares", "calculated"))
393
+ df = df.sort_values(by="calculated").groupby("date").first().dropna()
394
+ df["nominal_value"] = df["outstanding_shares"] * product.share_price
395
+ fig.add_trace(
396
+ go.Scatter(
397
+ x=df.index,
398
+ y=df.nominal_value,
399
+ mode="lines",
400
+ fill="tozeroy",
401
+ name=f"Nominal Value ({product.currency.key})",
402
+ hovertemplate=get_hovertemplate_timeserie(currency=""),
403
+ )
404
+ )
405
+ return fig
406
+
407
+ def get_queryset(self):
408
+ return InstrumentPrice.objects.filter(instrument=self.kwargs["product_id"], outstanding_shares__isnull=False)
409
+
410
+
411
+ class AUMProductChartView(viewsets.ChartViewSet):
412
+ IDENTIFIER = "wbportfolio:price"
413
+ filterset_fields = {
414
+ "date": ["gte", "exact", "lte"],
415
+ }
416
+ queryset = InstrumentPrice.objects.all()
417
+
418
+ title_config_class = AUMProductTitleConfig
419
+ endpoint_config_class = AUMProductEndpointConfig
420
+
421
+ def get_plotly(self, queryset):
422
+ fig = get_default_timeserie_figure()
423
+ product = Product.objects.get(id=self.kwargs["product_id"])
424
+ df = pd.DataFrame(queryset.values("date", "outstanding_shares", "net_value", "calculated"))
425
+ df = df.sort_values(by="calculated").groupby("date").first().dropna()
426
+ df["assets_under_management"] = df["outstanding_shares"] * df["net_value"]
427
+
428
+ fig.add_trace(
429
+ go.Scatter(
430
+ x=df.index,
431
+ y=df.assets_under_management,
432
+ mode="lines",
433
+ fill="tozeroy",
434
+ name=f"AUM ({product.currency.key})",
435
+ hovertemplate=get_hovertemplate_timeserie(currency=""),
436
+ )
437
+ )
438
+
439
+ return fig
440
+
441
+ def get_queryset(self):
442
+ return super().get_queryset().filter(instrument=self.kwargs["product_id"], outstanding_shares__isnull=False)
443
+
444
+
445
+ @cache_table(timeout=get_next_day_timedelta(), periodic_caching=True)
446
+ class InstrumentPriceAUMDataChartView(viewsets.ChartViewSet):
447
+ IDENTIFIER = "wbportfolio:aum-price"
448
+ LIST_TITLE = "Assets under Management"
449
+
450
+ queryset = InstrumentPrice.objects.annotate_base_data()
451
+
452
+ title_config_class = InstrumentPriceAUMDataTitleConfig
453
+ endpoint_config_class = InstrumentPriceAUMDataEndpointConfig
454
+
455
+ def get_plotly(self, queryset):
456
+ fig = get_default_timeserie_figure(add_rangeslider=False)
457
+
458
+ df_net_value = pd.DataFrame(
459
+ queryset.filter(calculated=False, net_value_usd__isnull=False)
460
+ .exclude(net_value_usd=0)
461
+ .values("instrument", "date", "net_value_usd")
462
+ )
463
+ df_outstanding_shares = pd.DataFrame(
464
+ queryset.filter(outstanding_shares__isnull=False)
465
+ .exclude(outstanding_shares=0)
466
+ .values("instrument", "date", "outstanding_shares", "calculated")
467
+ )
468
+ if not df_outstanding_shares.empty and not df_net_value.empty:
469
+ # get the none calculated outstanding share if possible, otherwise get the calculated one
470
+ df_outstanding_shares = (
471
+ (
472
+ df_outstanding_shares.sort_values(by=["instrument", "date", "calculated"])
473
+ .groupby(["instrument", "date"])
474
+ .first()
475
+ )
476
+ .groupby(level=0)
477
+ .ffill()
478
+ )
479
+ df_net_value = df_net_value.set_index(["instrument", "date"])
480
+ df = pd.concat([df_net_value["net_value_usd"], df_outstanding_shares["outstanding_shares"]], axis=1)
481
+
482
+ # we reindex to account for missing pos.
483
+ df = df.reindex(
484
+ pd.MultiIndex.from_product(
485
+ [
486
+ df.index.levels[0],
487
+ pd.date_range(df.index.levels[1].min(), df.index.levels[1].max(), freq="W-MON"),
488
+ ], # we downsample to make the chart easier on the frontend
489
+ names=["instrument", "date"],
490
+ )
491
+ )
492
+
493
+ # forward fill around the instrument
494
+ df = df.groupby(level=0).ffill().reset_index()
495
+
496
+ df["market_capitalization_usd"] = df["outstanding_shares"] * df["net_value_usd"]
497
+
498
+ df["instrument_repr"] = df["instrument"].map(dict(Product.objects.values_list("id", "computed_str")))
499
+ df["instrument"] = df["instrument"].map(dict(Product.objects.values_list("id", "isin")))
500
+ df = df.where(pd.notnull(df), None)
501
+ fig = px.area(
502
+ df,
503
+ x="date",
504
+ y="market_capitalization_usd",
505
+ line_group="instrument",
506
+ hover_name="instrument_repr",
507
+ color="instrument",
508
+ )
509
+ return fig
510
+
511
+ def get_queryset(self):
512
+ return (
513
+ super()
514
+ .get_queryset()
515
+ .filter(
516
+ instrument__in=Product.objects.values("id"),
517
+ )
518
+ .select_related("instrument")
519
+ )
520
+
521
+
522
+ @receiver(add_instance_button, sender=ProductModelViewSet)
523
+ def add_product_instrument_request_button(sender, many, *args, request=None, view=None, pk=None, **kwargs):
524
+ return InstrumentButtonMixin.add_instrument_request_button(request=request, view=view, pk=pk)
525
+
526
+
527
+ @receiver(add_instance_button, sender=ProductModelViewSet)
528
+ def add_product_transactions_request_button(sender, many, *args, request=None, view=None, pk=None, **kwargs):
529
+ return InstrumentButtonMixin.add_transactions_request_button(request=request, view=view, pk=pk)
@@ -0,0 +1,160 @@
1
+ from collections import defaultdict
2
+ from itertools import product
3
+ from typing import TYPE_CHECKING
4
+
5
+ import pandas as pd
6
+ from django.db.models import Exists, F, OuterRef, Q
7
+ from django.utils import timezone
8
+ from rest_framework import status
9
+ from rest_framework.decorators import action
10
+ from rest_framework.response import Response
11
+ from wbcore import viewsets
12
+ from wbcore.contrib.notifications.dispatch import send_notification
13
+ from wbcore.utils.strings import format_number
14
+ from wbportfolio.models import AccountReconciliation, AccountReconciliationLine
15
+ from wbportfolio.models.reconciliations.account_reconciliation_lines import (
16
+ AccountReconciliationLineQuerySet,
17
+ )
18
+ from wbportfolio.serializers import (
19
+ AccountReconciliationLineModelSerializer,
20
+ AccountReconciliationModelSerializer,
21
+ )
22
+ from wbportfolio.viewsets.configs.buttons import (
23
+ AccountReconciliationButtonViewConfig,
24
+ AccountReconciliationLineButtonViewConfig,
25
+ )
26
+ from wbportfolio.viewsets.configs.display import (
27
+ AccountReconciliationDisplayViewConfig,
28
+ AccountReconciliationLineDisplayViewConfig,
29
+ )
30
+ from wbportfolio.viewsets.configs.endpoints.reconciliations import (
31
+ AccountReconciliationEndpointViewConfig,
32
+ AccountReconciliationLineEndpointViewConfig,
33
+ )
34
+
35
+ if TYPE_CHECKING:
36
+ from rest_framework.request import Request
37
+
38
+
39
+ class AccountReconciliationModelViewSet(viewsets.ModelViewSet):
40
+ INSTANCE_DOCUMENTATION = "wbportfolio/markdown/documentation/account_holding_reconciliation.md"
41
+
42
+ queryset = AccountReconciliation.objects.all()
43
+ serializer_class = AccountReconciliationModelSerializer
44
+ display_config_class = AccountReconciliationDisplayViewConfig
45
+ button_config_class = AccountReconciliationButtonViewConfig
46
+ endpoint_config_class = AccountReconciliationEndpointViewConfig
47
+
48
+ filterset_fields = {"account": ["exact"]}
49
+ ordering = ordering_fields = ["-reconciliation_date"]
50
+
51
+ def get_queryset(self):
52
+ return (
53
+ AccountReconciliation.objects.filter_for_user(self.request.user)
54
+ .annotate(
55
+ unequal_exists=Exists(
56
+ AccountReconciliationLine.objects.filter(
57
+ Q(reconciliation_id=OuterRef("pk")) & ~Q(shares=F("shares_external"))
58
+ )
59
+ )
60
+ )
61
+ .select_related("account")
62
+ .select_related("creator")
63
+ .select_related("creator__profile")
64
+ .select_related("approved_by")
65
+ .select_related("approved_by__profile")
66
+ )
67
+
68
+ @action(methods=["PATCH"], detail=True)
69
+ def agree_customer(self, request: "Request", pk: int | None = None) -> Response:
70
+ reconciliation = self.get_object()
71
+ if reconciliation and isinstance(reconciliation, AccountReconciliation):
72
+ reconciliation.approved_by = request.user
73
+ reconciliation.approved_dt = timezone.now()
74
+ reconciliation.save()
75
+ send_notification(
76
+ code="wbportfolio.account_reconciliation.notify",
77
+ title=f"{reconciliation.approved_by} has agreed to the calculations for the account {reconciliation.account}.",
78
+ body=f"The account {reconciliation.account} has been reconciled and the calculations have been agreed to by {reconciliation.approved_by}. You may proceed.",
79
+ user=reconciliation.creator,
80
+ reverse_name="wbportfolio:accountreconciliation-detail",
81
+ reverse_args=[reconciliation.id],
82
+ )
83
+ return Response({"__notification": "You have aggreed to the calculations, thank you."})
84
+ return Response(
85
+ {"__notification": "There has been an issue with agreeing to the calculations."},
86
+ status=status.HTTP_400_BAD_REQUEST,
87
+ )
88
+
89
+ @action(methods=["PATCH"], detail=True)
90
+ def disagree_customer(self, request: "Request", pk: int | None = None) -> Response:
91
+ reconciliation = self.get_object()
92
+ if reconciliation and isinstance(reconciliation, AccountReconciliation):
93
+ send_notification(
94
+ code="wbportfolio.account_reconciliation.notify",
95
+ title=f"{request.user} has disagreed with your calculations for the account {reconciliation.account}",
96
+ body=f"Please reach out to {request.user} to adjust the reconciliation.",
97
+ user=reconciliation.creator,
98
+ reverse_name="wbportfolio:accountreconciliation-detail",
99
+ reverse_args=[reconciliation.id],
100
+ )
101
+ return Response()
102
+
103
+ @action(methods=["PATCH"], detail=True)
104
+ def recompute(self, request: "Request", pk: int | None = None) -> Response:
105
+ AccountReconciliationLine.objects.update_or_create_for_reconciliation(self.get_object())
106
+ return Response()
107
+
108
+ @action(methods=["PATCH"], detail=True)
109
+ def notify(self, request: "Request", pk: int | None = None) -> Response:
110
+ self.get_object().notify(update=True)
111
+ return Response()
112
+
113
+
114
+ class AccountReconciliationLineModelViewSet(viewsets.ModelViewSet):
115
+ DEPENDANT_IDENTIFIER = ["wbportfolio:accountreconciliation"]
116
+
117
+ queryset = AccountReconciliationLine.objects.all()
118
+ serializer_class = AccountReconciliationLineModelSerializer
119
+ display_config_class = AccountReconciliationLineDisplayViewConfig
120
+ button_config_class = AccountReconciliationLineButtonViewConfig
121
+ endpoint_config_class = AccountReconciliationLineEndpointViewConfig
122
+
123
+ def get_aggregates(self, queryset, paginated_queryset):
124
+ fields = [
125
+ "assets_under_management",
126
+ "assets_under_management_external",
127
+ "assets_under_management_diff",
128
+ ]
129
+ df = pd.DataFrame(queryset.values("currency_key", *fields))
130
+ aggregates = defaultdict(dict)
131
+ if not df.empty:
132
+ for field in fields:
133
+ aggregates[field]["Your"] = "Total AuM"
134
+
135
+ for currency_symbol, field in product(df.currency_key.unique(), fields):
136
+ dff = df.loc[df["currency_key"] == currency_symbol]
137
+ aggregates[field][f"{currency_symbol}"] = format_number(dff[field].sum())
138
+
139
+ return aggregates
140
+
141
+ def get_queryset(self) -> "AccountReconciliationLineQuerySet":
142
+ queryset: "AccountReconciliationLineQuerySet" = super().get_queryset()
143
+
144
+ if accountreconciliation_id := self.kwargs.get("accountreconciliation_id"):
145
+ queryset = queryset.filter(reconciliation_id=accountreconciliation_id)
146
+
147
+ return (
148
+ queryset.annotate_currency()
149
+ .annotate_currency_key()
150
+ .annotate_assets_under_management()
151
+ .annotate_assets_under_management_external()
152
+ .annotate_shares_diff()
153
+ .annotate_nominal_value_diff()
154
+ .annotate_assets_under_management_diff()
155
+ .annotate_pct_diff()
156
+ .annotate_is_equal()
157
+ .select_related("reconciliation")
158
+ .select_related("reconciliation__account")
159
+ .select_related("product")
160
+ )