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,167 @@
1
+ from contextlib import suppress
2
+ from decimal import Decimal
3
+
4
+ from django.db import models
5
+ from wbcore.contrib.io.mixins import ImportMixin
6
+ from wbcore.models import WBModel
7
+ from wbportfolio.import_export.handlers.portfolio_cash_flow import (
8
+ DailyPortfolioCashFlowImportHandler,
9
+ )
10
+ from wbportfolio.models.portfolio_cash_targets import PortfolioCashTarget
11
+ from wbportfolio.models.portfolio_swing_pricings import PortfolioSwingPricing
12
+
13
+
14
+ class DailyPortfolioCashFlow(ImportMixin, WBModel):
15
+ import_export_handler_class = DailyPortfolioCashFlowImportHandler
16
+ pending = models.BooleanField(default=False)
17
+
18
+ portfolio = models.ForeignKey(
19
+ to="wbportfolio.Portfolio",
20
+ related_name="daily_cashflows",
21
+ on_delete=models.CASCADE,
22
+ )
23
+ value_date = models.DateField()
24
+
25
+ cash = models.DecimalField(max_digits=19, decimal_places=4, blank=True)
26
+ cash_flow_forecast = models.DecimalField(max_digits=19, decimal_places=4, default=Decimal(0), blank=True)
27
+ total_assets = models.DecimalField(max_digits=19, decimal_places=4, blank=True)
28
+
29
+ min_target_cash_pct = models.DecimalField(max_digits=4, decimal_places=4, default=Decimal(0), blank=True)
30
+ target_cash_pct = models.DecimalField(max_digits=4, decimal_places=4, default=Decimal(0), blank=True)
31
+ max_target_cash_pct = models.DecimalField(max_digits=4, decimal_places=4, default=Decimal(0), blank=True)
32
+
33
+ swing_pricing = models.ForeignKey(
34
+ to="wbportfolio.PortfolioSwingPricing",
35
+ related_name="cash_flow",
36
+ on_delete=models.SET_NULL,
37
+ null=True,
38
+ blank=True,
39
+ )
40
+
41
+ estimated_total_assets = models.DecimalField(max_digits=19, decimal_places=4, blank=True)
42
+ cash_flow_asset_ratio = models.DecimalField(max_digits=5, decimal_places=4, blank=True)
43
+ true_cash = models.DecimalField(max_digits=19, decimal_places=4, blank=True)
44
+ cash_pct = models.DecimalField(max_digits=5, decimal_places=4, blank=True)
45
+ true_cash_pct = models.DecimalField(max_digits=5, decimal_places=4, blank=True)
46
+ target_cash = models.DecimalField(max_digits=19, decimal_places=4, blank=True)
47
+ excess_cash = models.DecimalField(max_digits=19, decimal_places=4, blank=True)
48
+ proposed_rebalancing = models.DecimalField(max_digits=19, decimal_places=4, blank=True, default=Decimal(0))
49
+ rebalancing = models.DecimalField(max_digits=19, decimal_places=4, blank=True, default=Decimal(0))
50
+
51
+ comment = models.TextField(default="", blank=True)
52
+
53
+ def save(self, *args, **kwargs):
54
+ # convert to decimal in case we get floats
55
+ if isinstance(self.cash_flow_forecast, float):
56
+ self.cash_flow_forecast = Decimal(self.cash_flow_forecast)
57
+
58
+ if isinstance(self.total_assets, float):
59
+ self.total_assets = Decimal(self.total_assets)
60
+
61
+ if isinstance(self.rebalancing, float):
62
+ self.rebalancing = Decimal(self.rebalancing)
63
+
64
+ with suppress(PortfolioCashTarget.DoesNotExist):
65
+ cash_target = self.portfolio.cash_targets.filter(valid_date__lte=self.value_date).latest("valid_date")
66
+ self.min_target_cash_pct = cash_target.min_target
67
+ self.target_cash_pct = cash_target.target
68
+ self.max_target_cash_pct = cash_target.max_target
69
+
70
+ with suppress(PortfolioSwingPricing.DoesNotExist):
71
+ self.swing_pricing = self.portfolio.swing_pricings.filter(valid_date__lte=self.value_date).latest(
72
+ "valid_date"
73
+ )
74
+
75
+ with suppress(self.DoesNotExist):
76
+ if self.total_assets is None or self.pending:
77
+ self.total_assets = (
78
+ self.portfolio.daily_cashflows.filter(value_date__lt=self.value_date)
79
+ .latest("value_date")
80
+ .estimated_total_assets
81
+ )
82
+
83
+ if self.total_assets is None:
84
+ self.total_assets = 0
85
+
86
+ with suppress(self.DoesNotExist):
87
+ if self.cash is None or self.pending:
88
+ earlier = self.portfolio.daily_cashflows.filter(value_date__lt=self.value_date).latest("value_date")
89
+ self.cash = earlier.true_cash
90
+
91
+ if self.cash is None:
92
+ self.cash = 0
93
+
94
+ if self.pending and self.rebalancing:
95
+ self.cash -= self.rebalancing
96
+
97
+ if self.pending:
98
+ self.estimated_total_assets = self.total_assets + self.cash_flow_forecast
99
+ elif self.estimated_total_assets is None:
100
+ self.estimated_total_assets = self.total_assets
101
+
102
+ self.cash_flow_asset_ratio = (
103
+ self.cash_flow_forecast / self.estimated_total_assets
104
+ if self.estimated_total_assets != Decimal(0)
105
+ else Decimal(0)
106
+ )
107
+ self.true_cash = self.cash + self.cash_flow_forecast
108
+ self.cash_pct = (
109
+ self.cash / self.estimated_total_assets if self.estimated_total_assets != Decimal(0) else Decimal(0)
110
+ )
111
+ self.true_cash_pct = (
112
+ self.true_cash / self.estimated_total_assets if self.estimated_total_assets != Decimal(0) else Decimal(0)
113
+ )
114
+ self.target_cash = self.estimated_total_assets * self.target_cash_pct
115
+ self.excess_cash = self.true_cash - self.target_cash
116
+
117
+ if self.true_cash_pct < self.min_target_cash_pct or self.true_cash_pct > self.max_target_cash_pct:
118
+ self.proposed_rebalancing = self.excess_cash
119
+ elif self.pending:
120
+ self.proposed_rebalancing = 0
121
+
122
+ super().save(*args, **kwargs)
123
+
124
+ if self.portfolio.daily_cashflows.filter(value_date__gt=self.value_date).exists():
125
+ self.portfolio.daily_cashflows.filter(value_date__gt=self.value_date).earliest("value_date").save()
126
+
127
+ class Meta:
128
+ verbose_name = "Daily Portfolio CashFlow"
129
+ verbose_name_plural = "Daily Portfolio CashFlow"
130
+ constraints = [
131
+ models.UniqueConstraint(fields=["portfolio", "value_date"], name="unique_portfolio_value_date"),
132
+ ]
133
+ permissions = [("administrate_dailyportfoliocashflow", "Can administrate Daily Portfolio CashFlow")]
134
+ notification_types = [
135
+ (
136
+ "wbportfolio.dailyportfoliocashflow.notify_rebalance",
137
+ "Rebalancing suggested",
138
+ "Sends a notification, when the system suggests to rebalance a portfolio due to being outside of the cash target parameters",
139
+ True,
140
+ True,
141
+ False,
142
+ ),
143
+ (
144
+ "wbportfolio.dailyportfoliocashflow.notify_swingpricing",
145
+ "Swing Pricing Notification",
146
+ "Sends a notification, when the system detects a future swing pricing event",
147
+ False,
148
+ False,
149
+ False,
150
+ ),
151
+ ]
152
+
153
+ @classmethod
154
+ def get_endpoint_basename(cls):
155
+ return "wbportfolio:portfoliocashflow"
156
+
157
+ @classmethod
158
+ def get_representation_endpoint(cls):
159
+ return "wbportfolio:portfoliocashflow-list"
160
+
161
+ @classmethod
162
+ def get_representation_value_key(cls):
163
+ return "id"
164
+
165
+ @classmethod
166
+ def get_representation_label_key(cls):
167
+ return "{{portfolio}}: {{cash}}"
@@ -0,0 +1,46 @@
1
+ from decimal import Decimal
2
+
3
+ from django.db import models
4
+ from wbcore.models import WBModel
5
+
6
+
7
+ class PortfolioCashTarget(WBModel):
8
+ """This model stores cash targets for a given portfolio"""
9
+
10
+ portfolio = models.ForeignKey(
11
+ to="wbportfolio.Portfolio",
12
+ related_name="cash_targets",
13
+ on_delete=models.CASCADE,
14
+ )
15
+
16
+ valid_date = models.DateField()
17
+ min_target = models.DecimalField(max_digits=5, decimal_places=4, null=True, blank=True)
18
+ target = models.DecimalField(max_digits=5, decimal_places=4, default=Decimal(0))
19
+ max_target = models.DecimalField(max_digits=5, decimal_places=4, null=True, blank=True)
20
+ comment = models.TextField(default="", blank=True)
21
+
22
+ def __str__(self) -> str:
23
+ return f"{self.portfolio}: {self.target:.2%} ({self.valid_date:%d.%m.%Y})"
24
+
25
+ class Meta:
26
+ verbose_name = "Portfolio Cash Target"
27
+ verbose_name_plural = "Portfolio Cash Targets"
28
+ constraints = [
29
+ models.UniqueConstraint(fields=["portfolio", "valid_date"], name="unique_portfolio_valid_date"),
30
+ ]
31
+
32
+ @classmethod
33
+ def get_endpoint_basename(cls):
34
+ return "wbportfolio:portfoliocashtarget"
35
+
36
+ @classmethod
37
+ def get_representation_endpoint(cls):
38
+ return "wbportfolio:portfoliocashtarget-list"
39
+
40
+ @classmethod
41
+ def get_representation_value_key(cls):
42
+ return "id"
43
+
44
+ @classmethod
45
+ def get_representation_label_key(cls):
46
+ return "{{portfolio}}: {{target}} ({{valid_date}})"
@@ -0,0 +1,135 @@
1
+ from contextlib import suppress
2
+
3
+ from django.db import models
4
+ from django.db.models import Q
5
+ from django.dispatch import receiver
6
+ from wbcore.signals import pre_merge
7
+ from wbfdm.models import Instrument
8
+
9
+
10
+ class PortfolioBankAccountThroughModel(models.Model):
11
+ class PortfolioBankAccountType(models.TextChoices):
12
+ CASH = "CASH", "Cash"
13
+ SECURITIES = "SECURITIES", "Securities"
14
+
15
+ portfolio = models.ForeignKey(
16
+ to="wbportfolio.Portfolio", on_delete=models.CASCADE, related_name="bank_account_through"
17
+ )
18
+ bank_account = models.ForeignKey(
19
+ to="directory.BankingContact", on_delete=models.CASCADE, related_name="portfolio_through"
20
+ )
21
+
22
+ portfolio_bank_account_type = models.CharField(
23
+ max_length=255, choices=PortfolioBankAccountType.choices, default=PortfolioBankAccountType.CASH
24
+ )
25
+
26
+ def __str__(self) -> str:
27
+ return f"{self.bank_account}: {self.portfolio} ({self.PortfolioBankAccountType[self.portfolio_bank_account_type].label})"
28
+
29
+ class Meta:
30
+ verbose_name = "Portfolio Bank Account"
31
+ verbose_name_plural = "Portfolio Bank Accounts"
32
+
33
+ constraints = [
34
+ # Each portfolio and bank account can only be connected once
35
+ models.UniqueConstraint(
36
+ fields=["portfolio", "bank_account"],
37
+ name="unique_portfolio_bank_account",
38
+ ),
39
+ # Each portfolio can only have one bank account to hold the securities
40
+ models.UniqueConstraint(
41
+ fields=["portfolio"],
42
+ name="unique_portfolio_bank_account_type",
43
+ condition=Q(portfolio_bank_account_type="SECURITIES"),
44
+ ),
45
+ ]
46
+
47
+
48
+ class InstrumentPortfolioThroughModel(models.Model):
49
+ instrument = models.ForeignKey(
50
+ "wbfdm.Instrument",
51
+ on_delete=models.CASCADE,
52
+ related_name="through_portfolios",
53
+ )
54
+ portfolio = models.ForeignKey(
55
+ "wbportfolio.Portfolio", on_delete=models.CASCADE, related_name="through_instruments"
56
+ )
57
+
58
+ class Meta:
59
+ unique_together = ("instrument", "portfolio")
60
+ constraints = [
61
+ models.UniqueConstraint(fields=["instrument"], name="unique_instrument"),
62
+ models.UniqueConstraint(fields=["instrument", "portfolio"], name="unique_portfolio_relationship"),
63
+ ]
64
+
65
+ @classmethod
66
+ def get_portfolio(cls, instrument):
67
+ with suppress(InstrumentPortfolioThroughModel.DoesNotExist):
68
+ return InstrumentPortfolioThroughModel.objects.get(instrument=instrument).portfolio
69
+
70
+ @classmethod
71
+ def get_primary_portfolio(cls, instrument):
72
+ if portfolio := cls.get_portfolio(instrument):
73
+ if primary_portfolio := portfolio.primary_portfolio:
74
+ return primary_portfolio
75
+ return portfolio
76
+
77
+
78
+ @receiver(models.signals.class_prepared)
79
+ def add_to_instrument(sender, **kwargs):
80
+ Instrument.add_to_class("portfolio", property(InstrumentPortfolioThroughModel.get_portfolio))
81
+ Instrument.add_to_class("primary_portfolio", property(InstrumentPortfolioThroughModel.get_primary_portfolio))
82
+
83
+
84
+ class PortfolioInstrumentPreferredClassificationThroughModel(models.Model):
85
+ portfolio = models.ForeignKey(
86
+ "wbportfolio.Portfolio", on_delete=models.CASCADE, related_name="preferred_classification_instruments"
87
+ )
88
+ instrument = models.ForeignKey(
89
+ "wbfdm.Instrument",
90
+ on_delete=models.CASCADE,
91
+ related_name="preferred_classification_portfolio",
92
+ limit_choices_to=(models.Q(instrument_type__is_classifiable=True) & models.Q(level=0)),
93
+ )
94
+ classification = models.ForeignKey(
95
+ "wbfdm.Classification",
96
+ on_delete=models.CASCADE,
97
+ blank=True,
98
+ null=True,
99
+ related_name="preferred_classification_throughs",
100
+ )
101
+ classification_group = models.ForeignKey(
102
+ "wbfdm.ClassificationGroup",
103
+ on_delete=models.CASCADE,
104
+ blank=True,
105
+ null=True,
106
+ related_name="preferred_classification_group_throughs",
107
+ )
108
+
109
+ def save(self, *args, **kwargs) -> None:
110
+ if not self.classification_group and self.classification:
111
+ self.classification_group = self.classification.group
112
+ return super().save(*args, **kwargs)
113
+
114
+ class Meta:
115
+ verbose_name = "Portfolio Prefered Classification Per Instrument"
116
+ verbose_name_plural = "Portfolio Prefered Classification Per Instruments"
117
+ unique_together = ("portfolio", "instrument", "classification_group")
118
+
119
+
120
+ @receiver(pre_merge, sender="wbfdm.Instrument")
121
+ def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
122
+ """
123
+ Reassign all merged instrument preferred classification relationship to the main instrument
124
+ """
125
+ for through in PortfolioInstrumentPreferredClassificationThroughModel.objects.filter(instrument=merged_object):
126
+ if classification := through.classification:
127
+ PortfolioInstrumentPreferredClassificationThroughModel.objects.get_or_create(
128
+ portfolio=through.portfolio,
129
+ instrument=main_object,
130
+ classification_group=(
131
+ through.classification_group if through.classification_group else classification.group
132
+ ),
133
+ defaults={"classification": classification},
134
+ )
135
+ through.delete()
@@ -0,0 +1,51 @@
1
+ from django.db import models
2
+ from django.db.models import Q
3
+ from wbcore.models import WBModel
4
+
5
+
6
+ class PortfolioSwingPricing(WBModel):
7
+ valid_date = models.DateField()
8
+
9
+ portfolio = models.ForeignKey(
10
+ to="wbportfolio.Portfolio",
11
+ related_name="swing_pricings",
12
+ on_delete=models.CASCADE,
13
+ )
14
+
15
+ negative_threshold = models.DecimalField(max_digits=4, decimal_places=4)
16
+ negative_swing_factor = models.DecimalField(max_digits=4, decimal_places=4)
17
+ positive_threshold = models.DecimalField(max_digits=4, decimal_places=4)
18
+ positive_swing_factor = models.DecimalField(max_digits=4, decimal_places=4)
19
+
20
+ def __str__(self) -> str:
21
+ return f"{self.portfolio}: {self.negative_swing_factor:.2%}/{self.positive_swing_factor:.2%} ({self.valid_date:%d.%m.%Y})"
22
+
23
+ class Meta:
24
+ verbose_name = "Portfolio Swing Pricing"
25
+ verbose_name_plural = "Portfolio Swing Pricings"
26
+ constraints = [
27
+ models.CheckConstraint(
28
+ check=Q(negative_threshold__lt=0)
29
+ & Q(negative_swing_factor__lt=0)
30
+ & Q(positive_threshold__gt=0)
31
+ & Q(positive_swing_factor__gt=0),
32
+ name="value_polarity",
33
+ ),
34
+ models.UniqueConstraint(fields=["valid_date", "portfolio"], name="unique_valid_date_portfolio"),
35
+ ]
36
+
37
+ @classmethod
38
+ def get_endpoint_basename(cls):
39
+ return "wbportfolio:portfolioswingpricing"
40
+
41
+ @classmethod
42
+ def get_representation_endpoint(cls):
43
+ return "wbportfolio:portfolioswingpricing-list"
44
+
45
+ @classmethod
46
+ def get_representation_value_key(cls):
47
+ return "id"
48
+
49
+ @classmethod
50
+ def get_representation_label_key(cls):
51
+ return "{{portfolio}}: {{swing_factor}} ({{valid_date}})"
@@ -0,0 +1,230 @@
1
+ from decimal import Decimal
2
+
3
+ import pandas as pd
4
+ from django.db import models
5
+ from django.db.models import F, Q, Sum
6
+ from wbcore.models import WBModel
7
+ from wbfdm.models import InstrumentType
8
+ from wbfdm.models.instruments.instrument_prices import InstrumentPrice
9
+ from wbportfolio.models.products import FeeProductPercentage, Product
10
+
11
+ from .mixins.instruments import PMSInstrument
12
+
13
+
14
+ class ProductGroup(PMSInstrument):
15
+ class ProductGroupType(models.TextChoices):
16
+ FUND = "Fund"
17
+
18
+ class ProductGroupCategory(models.TextChoices):
19
+ UCITS = "UCITS", "UCITS"
20
+ OTHER_FUNDS = "OTHER_FUNDS", "Other funds for traditional investments"
21
+
22
+ type = models.CharField(
23
+ max_length=64,
24
+ verbose_name="Type",
25
+ choices=ProductGroupType.choices,
26
+ default=ProductGroupType.FUND,
27
+ )
28
+ category = models.CharField(
29
+ max_length=64,
30
+ choices=ProductGroupCategory.choices,
31
+ default=ProductGroupCategory.UCITS,
32
+ )
33
+
34
+ umbrella = models.CharField(max_length=255, null=True, blank=True)
35
+ management_company = models.ForeignKey(
36
+ "directory.Company",
37
+ related_name="management_company_product_groups",
38
+ null=True,
39
+ blank=True,
40
+ on_delete=models.SET_NULL,
41
+ )
42
+ depositary = models.ForeignKey(
43
+ "directory.Company",
44
+ related_name="depositary_product_groups",
45
+ null=True,
46
+ blank=True,
47
+ on_delete=models.SET_NULL,
48
+ )
49
+ transfer_agent = models.ForeignKey(
50
+ "directory.Company",
51
+ related_name="transfer_agent_product_groups",
52
+ null=True,
53
+ blank=True,
54
+ on_delete=models.SET_NULL,
55
+ )
56
+ administrator = models.ForeignKey(
57
+ "directory.Company",
58
+ related_name="administrator_groups",
59
+ null=True,
60
+ blank=True,
61
+ on_delete=models.SET_NULL,
62
+ )
63
+ investment_manager = models.ForeignKey(
64
+ "directory.Company",
65
+ related_name="investment_manager_groups",
66
+ null=True,
67
+ blank=True,
68
+ on_delete=models.SET_NULL,
69
+ )
70
+ auditor = models.ForeignKey(
71
+ "directory.Company",
72
+ related_name="auditor_groups",
73
+ null=True,
74
+ blank=True,
75
+ on_delete=models.SET_NULL,
76
+ )
77
+ paying_agent = models.ForeignKey(
78
+ "directory.Company",
79
+ related_name="paying_agent_groups",
80
+ null=True,
81
+ blank=True,
82
+ on_delete=models.SET_NULL,
83
+ )
84
+
85
+ @property
86
+ def products(self): # for backward compatibility
87
+ return Product.objects.filter(id__in=self.children.values("id"))
88
+
89
+ def save(self, *args, **kwargs):
90
+ self.instrument_type = InstrumentType.objects.get_or_create(
91
+ key="product_group", defaults={"name": "Product Group", "short_name": "Product Group"}
92
+ )[0]
93
+ self.is_managed = True
94
+ super().save(*args, **kwargs)
95
+
96
+ def active_products(self, val_date):
97
+ return self.products.filter(
98
+ Q(inception_date__isnull=False)
99
+ & Q(inception_date__lte=val_date)
100
+ & (Q(delisted_date__isnull=True) | Q(delisted_date__gt=val_date))
101
+ )
102
+
103
+ def compute_str(self):
104
+ return f"{self.name} ({self.umbrella})"
105
+
106
+ class Meta:
107
+ verbose_name = "Product Group"
108
+ verbose_name_plural = "Product Groups"
109
+
110
+ def get_fund_product_table(self, val_date):
111
+ products = self.active_products(val_date).annotate(
112
+ current_net_value=InstrumentPrice.subquery_closest_value("net_value", val_date, instrument_pk_name="pk"),
113
+ )
114
+ df = pd.DataFrame(
115
+ products.values(
116
+ "name",
117
+ "isin",
118
+ "ticker",
119
+ "refinitiv_identifier_code",
120
+ "currency__key",
121
+ "currency__symbol",
122
+ "dividend",
123
+ "minimum_subscription",
124
+ "inception_date",
125
+ "current_net_value",
126
+ )
127
+ )
128
+
129
+ if not df.empty:
130
+ df["total_performance_fee"] = df["isin"].apply(
131
+ lambda x: Product.objects.get(isin=x).get_fees_percent(
132
+ val_date, fee_type=FeeProductPercentage.Type.PERFORMANCE, net=False
133
+ )
134
+ )
135
+ df["management_fees"] = df["isin"].apply(
136
+ lambda x: Product.objects.get(isin=x).get_fees_percent(
137
+ val_date, fee_type=FeeProductPercentage.Type.MANAGEMENT
138
+ )
139
+ )
140
+ df.management_fees = df.management_fees.apply(lambda x: f"{x:,.2%}" if x is not None else "")
141
+ df.total_performance_fee = df.total_performance_fee.apply(lambda x: f"{x:,.2%}" if x is not None else "")
142
+ df.current_net_value = df.current_net_value.apply(lambda x: f"{x:,.2f}" if x is not None else "")
143
+ df.inception_date = df.inception_date.apply(lambda x: f"{x:%Y-%m-%d}" if x is not None else "")
144
+
145
+ df = df[
146
+ [
147
+ "name",
148
+ "isin",
149
+ "ticker",
150
+ "refinitiv_identifier_code",
151
+ "currency__key",
152
+ "currency__symbol",
153
+ "dividend",
154
+ "management_fees",
155
+ "total_performance_fee",
156
+ "minimum_subscription",
157
+ "current_net_value",
158
+ "inception_date",
159
+ ]
160
+ ]
161
+ df = df.rename(
162
+ columns={
163
+ "name": "share class",
164
+ "ticker": "bloomberg",
165
+ "currency__key": "currency",
166
+ "currency__symbol": "currency symbol",
167
+ "minimum_subscription": "min subscription",
168
+ "management_fees": "mgmt. fees",
169
+ "total_performance_fee": "perf. fees",
170
+ "inception_date": "launch date",
171
+ "refinitiv_identifier_code": "reuters",
172
+ "current_net_value": "price",
173
+ }
174
+ )
175
+
176
+ return df
177
+
178
+ def get_total_fund_aum(self, val_date=None):
179
+ return Product.annotate_last_aum(
180
+ Product.objects.filter(parent=self, is_invested=True), val_date=val_date
181
+ ).aggregate(s=Sum(F("assets_under_management_usd")))["s"] or Decimal(0)
182
+
183
+ def total_shares(self, val_date):
184
+ return sum([product.total_shares(val_date) for product in self.products.all()])
185
+
186
+ @classmethod
187
+ def get_representation_value_key(cls):
188
+ return "id"
189
+
190
+ @classmethod
191
+ def get_representation_label_key(cls):
192
+ return "{{name_repr}}"
193
+
194
+ @classmethod
195
+ def get_representation_endpoint(cls):
196
+ return "wbportfolio:product_grouprepresentation-list"
197
+
198
+
199
+ class ProductGroupRepresentant(WBModel):
200
+ product_group = models.ForeignKey("ProductGroup", related_name="representants", on_delete=models.CASCADE)
201
+ representant = models.ForeignKey("directory.Company", on_delete=models.PROTECT)
202
+ country = models.ForeignKey(
203
+ "geography.Geography", on_delete=models.PROTECT, limit_choices_to={"level": 1}, null=True, blank=True
204
+ )
205
+
206
+ def __str__(self):
207
+ return f"{self.product_group.identifier} {self.representant.name} ({self.country.name})"
208
+
209
+ class Meta:
210
+ verbose_name = "Product Group Representant"
211
+ verbose_name_plural = "Product Group Representants"
212
+ constraints = [
213
+ models.UniqueConstraint(fields=("product_group", "country"), name="unique_country_product_group")
214
+ ]
215
+
216
+ @classmethod
217
+ def get_representation_value_key(cls):
218
+ return "id"
219
+
220
+ @classmethod
221
+ def get_representation_label_key(cls):
222
+ return "{{representant}} {{country}}"
223
+
224
+ @classmethod
225
+ def get_representation_endpoint(cls):
226
+ return None
227
+
228
+ @classmethod
229
+ def get_endpoint_basename(cls):
230
+ return "wbportfolio:product_group"