wbportfolio 1.43.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 (506) 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 +153 -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 +248 -0
  92. wbportfolio/filters/transactions/transactions.py +98 -0
  93. wbportfolio/fixtures/product_factsheets.yaml +1 -0
  94. wbportfolio/fixtures/wbportfolio.yaml.gz +0 -0
  95. wbportfolio/fixtures/wbrisk_management.yaml.gz +0 -0
  96. wbportfolio/import_export/__init__.py +0 -0
  97. wbportfolio/import_export/backends/__init__.py +2 -0
  98. wbportfolio/import_export/backends/refinitiv/adjustment.py +40 -0
  99. wbportfolio/import_export/backends/ubs/__init__.py +3 -0
  100. wbportfolio/import_export/backends/ubs/asset_position.py +45 -0
  101. wbportfolio/import_export/backends/ubs/fees.py +63 -0
  102. wbportfolio/import_export/backends/ubs/instrument_price.py +44 -0
  103. wbportfolio/import_export/backends/ubs/mixin.py +15 -0
  104. wbportfolio/import_export/backends/utils.py +58 -0
  105. wbportfolio/import_export/backends/wbfdm/__init__.py +2 -0
  106. wbportfolio/import_export/backends/wbfdm/adjustment.py +50 -0
  107. wbportfolio/import_export/backends/wbfdm/dividend.py +16 -0
  108. wbportfolio/import_export/backends/wbfdm/mixin.py +15 -0
  109. wbportfolio/import_export/handlers/__init__.py +0 -0
  110. wbportfolio/import_export/handlers/adjustment.py +39 -0
  111. wbportfolio/import_export/handlers/asset_position.py +167 -0
  112. wbportfolio/import_export/handlers/dividend.py +80 -0
  113. wbportfolio/import_export/handlers/fees.py +58 -0
  114. wbportfolio/import_export/handlers/portfolio_cash_flow.py +57 -0
  115. wbportfolio/import_export/handlers/register.py +43 -0
  116. wbportfolio/import_export/handlers/trade.py +191 -0
  117. wbportfolio/import_export/parsers/__init__.py +0 -0
  118. wbportfolio/import_export/parsers/default_mapping.py +30 -0
  119. wbportfolio/import_export/parsers/jpmorgan/__init__.py +0 -0
  120. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +63 -0
  121. wbportfolio/import_export/parsers/jpmorgan/fees.py +64 -0
  122. wbportfolio/import_export/parsers/jpmorgan/strategy.py +116 -0
  123. wbportfolio/import_export/parsers/jpmorgan/valuation.py +41 -0
  124. wbportfolio/import_export/parsers/leonteq/__init__.py +0 -0
  125. wbportfolio/import_export/parsers/leonteq/customer_trade.py +47 -0
  126. wbportfolio/import_export/parsers/leonteq/equity.py +81 -0
  127. wbportfolio/import_export/parsers/leonteq/fees.py +70 -0
  128. wbportfolio/import_export/parsers/leonteq/trade.py +94 -0
  129. wbportfolio/import_export/parsers/leonteq/valuation.py +39 -0
  130. wbportfolio/import_export/parsers/natixis/__init__.py +0 -0
  131. wbportfolio/import_export/parsers/natixis/customer_trade.py +62 -0
  132. wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +66 -0
  133. wbportfolio/import_export/parsers/natixis/d1_equity.py +80 -0
  134. wbportfolio/import_export/parsers/natixis/d1_fees.py +58 -0
  135. wbportfolio/import_export/parsers/natixis/d1_trade.py +70 -0
  136. wbportfolio/import_export/parsers/natixis/d1_valuation.py +41 -0
  137. wbportfolio/import_export/parsers/natixis/dividend.py +53 -0
  138. wbportfolio/import_export/parsers/natixis/equity.py +60 -0
  139. wbportfolio/import_export/parsers/natixis/fees.py +53 -0
  140. wbportfolio/import_export/parsers/natixis/trade.py +63 -0
  141. wbportfolio/import_export/parsers/natixis/utils.py +76 -0
  142. wbportfolio/import_export/parsers/natixis/valuation.py +46 -0
  143. wbportfolio/import_export/parsers/refinitiv/__init__.py +0 -0
  144. wbportfolio/import_export/parsers/refinitiv/adjustment.py +24 -0
  145. wbportfolio/import_export/parsers/sg_lux/__init__.py +0 -0
  146. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +70 -0
  147. wbportfolio/import_export/parsers/sg_lux/customer_trade.py +75 -0
  148. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +140 -0
  149. wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +80 -0
  150. wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +57 -0
  151. wbportfolio/import_export/parsers/sg_lux/equity.py +161 -0
  152. wbportfolio/import_export/parsers/sg_lux/fees.py +56 -0
  153. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +51 -0
  154. wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +29 -0
  155. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +36 -0
  156. wbportfolio/import_export/parsers/sg_lux/registers.py +210 -0
  157. wbportfolio/import_export/parsers/sg_lux/sylk.py +248 -0
  158. wbportfolio/import_export/parsers/sg_lux/utils.py +36 -0
  159. wbportfolio/import_export/parsers/sg_lux/valuation.py +53 -0
  160. wbportfolio/import_export/parsers/societe_generale/__init__.py +0 -0
  161. wbportfolio/import_export/parsers/societe_generale/customer_trade.py +54 -0
  162. wbportfolio/import_export/parsers/societe_generale/strategy.py +94 -0
  163. wbportfolio/import_export/parsers/societe_generale/valuation.py +37 -0
  164. wbportfolio/import_export/parsers/tellco/__init__.py +0 -0
  165. wbportfolio/import_export/parsers/tellco/customer_trade.py +64 -0
  166. wbportfolio/import_export/parsers/tellco/equity.py +86 -0
  167. wbportfolio/import_export/parsers/tellco/valuation.py +52 -0
  168. wbportfolio/import_export/parsers/ubs/__init__.py +0 -0
  169. wbportfolio/import_export/parsers/ubs/api/__init__.py +0 -0
  170. wbportfolio/import_export/parsers/ubs/api/asset_position.py +106 -0
  171. wbportfolio/import_export/parsers/ubs/api/fees.py +31 -0
  172. wbportfolio/import_export/parsers/ubs/api/instrument_price.py +20 -0
  173. wbportfolio/import_export/parsers/ubs/api/utils.py +0 -0
  174. wbportfolio/import_export/parsers/ubs/customer_trade.py +60 -0
  175. wbportfolio/import_export/parsers/ubs/equity.py +97 -0
  176. wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +67 -0
  177. wbportfolio/import_export/parsers/ubs/valuation.py +52 -0
  178. wbportfolio/import_export/parsers/vontobel/__init__.py +0 -0
  179. wbportfolio/import_export/parsers/vontobel/asset_position.py +97 -0
  180. wbportfolio/import_export/parsers/vontobel/customer_trade.py +54 -0
  181. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +40 -0
  182. wbportfolio/import_export/parsers/vontobel/instrument.py +34 -0
  183. wbportfolio/import_export/parsers/vontobel/management_fees.py +86 -0
  184. wbportfolio/import_export/parsers/vontobel/performance_fees.py +35 -0
  185. wbportfolio/import_export/parsers/vontobel/trade.py +38 -0
  186. wbportfolio/import_export/parsers/vontobel/utils.py +17 -0
  187. wbportfolio/import_export/parsers/vontobel/valuation.py +29 -0
  188. wbportfolio/import_export/resources/__init__.py +0 -0
  189. wbportfolio/import_export/resources/assets.py +68 -0
  190. wbportfolio/import_export/resources/trades.py +41 -0
  191. wbportfolio/import_export/utils.py +42 -0
  192. wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +119 -0
  193. wbportfolio/kpi_handlers/nnm.py +163 -0
  194. wbportfolio/metric/__init__.py +0 -0
  195. wbportfolio/metric/backends/__init__.py +2 -0
  196. wbportfolio/metric/backends/base.py +86 -0
  197. wbportfolio/metric/backends/constants.py +222 -0
  198. wbportfolio/metric/backends/portfolio_base.py +255 -0
  199. wbportfolio/metric/backends/portfolio_esg.py +66 -0
  200. wbportfolio/metric/tests/__init__.py +0 -0
  201. wbportfolio/metric/tests/conftest.py +4 -0
  202. wbportfolio/metric/tests/test_portfolio_base.py +135 -0
  203. wbportfolio/metric/tests/test_portfolio_esg.py +69 -0
  204. wbportfolio/migrations/0001_initial_squashed.py +13848 -0
  205. wbportfolio/migrations/0002_product_default_sub_account_squashed_0039_alter_assetallocation_company_and_more.py +3836 -0
  206. wbportfolio/migrations/0040_instrument_financial_instrument.py +26 -0
  207. wbportfolio/migrations/0041_remove_listresearch_research_ptr_and_more.py +129 -0
  208. wbportfolio/migrations/0042_instrumentlist_instrumentlistthroughmodel_and_more.py +71 -0
  209. wbportfolio/migrations/0043_alter_instrumentlistthroughmodel_options_and_more.py +238 -0
  210. wbportfolio/migrations/0044_alter_instrumentlist_identifier.py +35 -0
  211. wbportfolio/migrations/0045_alter_instrument_financial_instrument.py +26 -0
  212. wbportfolio/migrations/0046_add_product_default_account.py +166 -0
  213. wbportfolio/migrations/0047_remove_product_default_sub_account.py +14 -0
  214. wbportfolio/migrations/0048_alter_trade_status.py +29 -0
  215. wbportfolio/migrations/0049_trade_claimed_shares.py +25 -0
  216. wbportfolio/migrations/0050_fees_fee_date_fees_wbportfolio_transac_1f7a29_idx.py +44 -0
  217. wbportfolio/migrations/0051_delete_macroreview.py +11 -0
  218. wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +888 -0
  219. wbportfolio/migrations/0053_remove_product_group.py +132 -0
  220. wbportfolio/migrations/0054_portfolioinstrumentpreferredclassificationthroughmodel_and_more.py +270 -0
  221. wbportfolio/migrations/0055_remove_product__custom_management_rebates_and_more.py +139 -0
  222. wbportfolio/migrations/0056_remove_companyportfoliodata_assets_under_management_currency_and_more.py +56 -0
  223. wbportfolio/migrations/0057_alter_portfolio_preferred_instrument_classifications_and_more.py +36 -0
  224. wbportfolio/migrations/0058_pmsinstrument.py +23 -0
  225. wbportfolio/migrations/0059_fees_unique_fees.py +51 -0
  226. wbportfolio/migrations/0060_alter_portfolioportfoliothroughmodel_type.py +21 -0
  227. wbportfolio/migrations/0061_portfolio_bank_accounts_product_bank_account_and_more.py +175 -0
  228. wbportfolio/migrations/0062_alter_dailyportfoliocashflow_options.py +20 -0
  229. wbportfolio/migrations/0063_accountreconciliation_accountreconciliationline_and_more.py +133 -0
  230. wbportfolio/migrations/0064_alter_portfolio_managers_portfolio_is_tracked_and_more.py +40 -0
  231. wbportfolio/migrations/0065_alter_portfolio_managers_claim_as_shares_and_more.py +73 -0
  232. wbportfolio/migrations/0066_assetposition_initial_shares_at_custodian_and_more.py +108 -0
  233. wbportfolio/migrations/0067_assetposition_unique_asset_position.py +77 -0
  234. wbportfolio/migrations/0068_trade_internal_trade_trade_marked_as_internal_and_more.py +59 -0
  235. wbportfolio/migrations/0069_remove_portfolio_is_invested_and_more.py +56 -0
  236. wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +82 -0
  237. wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py +22 -0
  238. wbportfolio/migrations/__init__.py +0 -0
  239. wbportfolio/models/__init__.py +26 -0
  240. wbportfolio/models/adjustments.py +246 -0
  241. wbportfolio/models/asset.py +869 -0
  242. wbportfolio/models/custodians.py +101 -0
  243. wbportfolio/models/indexes.py +33 -0
  244. wbportfolio/models/llm/wbcrm/analyze_relationship.py +58 -0
  245. wbportfolio/models/mixins/__init__.py +0 -0
  246. wbportfolio/models/mixins/instruments.py +127 -0
  247. wbportfolio/models/mixins/liquidity_stress_test.py +1307 -0
  248. wbportfolio/models/portfolio.py +1039 -0
  249. wbportfolio/models/portfolio_cash_flow.py +167 -0
  250. wbportfolio/models/portfolio_cash_targets.py +46 -0
  251. wbportfolio/models/portfolio_relationship.py +135 -0
  252. wbportfolio/models/portfolio_swing_pricings.py +51 -0
  253. wbportfolio/models/product_groups.py +230 -0
  254. wbportfolio/models/products.py +569 -0
  255. wbportfolio/models/reconciliations/__init__.py +2 -0
  256. wbportfolio/models/reconciliations/account_reconciliation_lines.py +192 -0
  257. wbportfolio/models/reconciliations/account_reconciliations.py +102 -0
  258. wbportfolio/models/reconciliations/reconciliations.py +25 -0
  259. wbportfolio/models/registers.py +132 -0
  260. wbportfolio/models/roles.py +208 -0
  261. wbportfolio/models/synchronization/__init__.py +3 -0
  262. wbportfolio/models/synchronization/portfolio_synchronization.py +292 -0
  263. wbportfolio/models/synchronization/price_computation.py +200 -0
  264. wbportfolio/models/synchronization/synchronization.py +188 -0
  265. wbportfolio/models/transactions/__init__.py +7 -0
  266. wbportfolio/models/transactions/claim.py +634 -0
  267. wbportfolio/models/transactions/dividends.py +31 -0
  268. wbportfolio/models/transactions/expiry.py +7 -0
  269. wbportfolio/models/transactions/fees.py +153 -0
  270. wbportfolio/models/transactions/trade_proposals.py +502 -0
  271. wbportfolio/models/transactions/trades.py +704 -0
  272. wbportfolio/models/transactions/transactions.py +211 -0
  273. wbportfolio/models/utils.py +12 -0
  274. wbportfolio/permissions.py +13 -0
  275. wbportfolio/pms/__init__.py +0 -0
  276. wbportfolio/pms/statistics/__init__.py +0 -0
  277. wbportfolio/pms/trading/__init__.py +1 -0
  278. wbportfolio/pms/trading/handler.py +164 -0
  279. wbportfolio/pms/typing.py +194 -0
  280. wbportfolio/preferences.py +6 -0
  281. wbportfolio/reports/__init__.py +0 -0
  282. wbportfolio/reports/monthly_position_report.py +74 -0
  283. wbportfolio/risk_management/__init__.py +0 -0
  284. wbportfolio/risk_management/backends/__init__.py +11 -0
  285. wbportfolio/risk_management/backends/accounts.py +166 -0
  286. wbportfolio/risk_management/backends/controversy_portfolio.py +63 -0
  287. wbportfolio/risk_management/backends/exposure_portfolio.py +203 -0
  288. wbportfolio/risk_management/backends/instrument_list_portfolio.py +89 -0
  289. wbportfolio/risk_management/backends/liquidity_risk.py +86 -0
  290. wbportfolio/risk_management/backends/liquidity_stress_instrument.py +86 -0
  291. wbportfolio/risk_management/backends/mixins.py +220 -0
  292. wbportfolio/risk_management/backends/product_integrity.py +111 -0
  293. wbportfolio/risk_management/backends/stop_loss_instrument.py +24 -0
  294. wbportfolio/risk_management/backends/stop_loss_portfolio.py +36 -0
  295. wbportfolio/risk_management/backends/ucits_portfolio.py +63 -0
  296. wbportfolio/risk_management/tests/__init__.py +0 -0
  297. wbportfolio/risk_management/tests/conftest.py +15 -0
  298. wbportfolio/risk_management/tests/test_accounts.py +98 -0
  299. wbportfolio/risk_management/tests/test_controversy_portfolio.py +33 -0
  300. wbportfolio/risk_management/tests/test_exposure_portfolio.py +94 -0
  301. wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +60 -0
  302. wbportfolio/risk_management/tests/test_liquidity_risk.py +47 -0
  303. wbportfolio/risk_management/tests/test_product_integrity.py +55 -0
  304. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +110 -0
  305. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +119 -0
  306. wbportfolio/risk_management/tests/test_ucits_portfolio.py +39 -0
  307. wbportfolio/serializers/__init__.py +42 -0
  308. wbportfolio/serializers/adjustments.py +24 -0
  309. wbportfolio/serializers/assets.py +166 -0
  310. wbportfolio/serializers/custodians.py +26 -0
  311. wbportfolio/serializers/portfolio_cash_flow.py +48 -0
  312. wbportfolio/serializers/portfolio_cash_targets.py +20 -0
  313. wbportfolio/serializers/portfolio_relationship.py +53 -0
  314. wbportfolio/serializers/portfolio_swing_pricing.py +20 -0
  315. wbportfolio/serializers/portfolios.py +143 -0
  316. wbportfolio/serializers/positions.py +76 -0
  317. wbportfolio/serializers/product_group.py +88 -0
  318. wbportfolio/serializers/products.py +331 -0
  319. wbportfolio/serializers/reconciliations.py +173 -0
  320. wbportfolio/serializers/registers.py +72 -0
  321. wbportfolio/serializers/roles.py +60 -0
  322. wbportfolio/serializers/signals.py +157 -0
  323. wbportfolio/serializers/synchronization.py +18 -0
  324. wbportfolio/serializers/transactions/__init__.py +24 -0
  325. wbportfolio/serializers/transactions/claim.py +310 -0
  326. wbportfolio/serializers/transactions/dividends.py +18 -0
  327. wbportfolio/serializers/transactions/expiry.py +18 -0
  328. wbportfolio/serializers/transactions/fees.py +32 -0
  329. wbportfolio/serializers/transactions/trades.py +315 -0
  330. wbportfolio/serializers/transactions/transactions.py +84 -0
  331. wbportfolio/static/wbportfolio/css/macro_review.css +17 -0
  332. wbportfolio/static/wbportfolio/markdown/documentation/account_holding_reconciliation.md +16 -0
  333. wbportfolio/static/wbportfolio/markdown/documentation/aggregate_asset_position_liquidity.md +25 -0
  334. wbportfolio/static/wbportfolio/markdown/documentation/company.md +78 -0
  335. wbportfolio/static/wbportfolio/markdown/documentation/earnings_instrument.md +14 -0
  336. wbportfolio/static/wbportfolio/markdown/documentation/financial_analysis_instrument_ratios.md +94 -0
  337. wbportfolio/static/wbportfolio/markdown/documentation/financial_statistics.md +44 -0
  338. wbportfolio/static/wbportfolio/markdown/documentation/person.md +70 -0
  339. wbportfolio/tasks.py +125 -0
  340. wbportfolio/templates/portfolio/email/customer_report.html +6 -0
  341. wbportfolio/templates/portfolio/email/customer_trade_notification.html +26 -0
  342. wbportfolio/templates/portfolio/email/email_base_template.html +420 -0
  343. wbportfolio/templates/portfolio/email/rebalancing_report.html +34 -0
  344. wbportfolio/templates/portfolio/macro/macro_review.html +88 -0
  345. wbportfolio/tests/__init__.py +0 -0
  346. wbportfolio/tests/conftest.py +164 -0
  347. wbportfolio/tests/models/__init__.py +0 -0
  348. wbportfolio/tests/models/test_account_reconciliation.py +191 -0
  349. wbportfolio/tests/models/test_assets.py +193 -0
  350. wbportfolio/tests/models/test_custodians.py +12 -0
  351. wbportfolio/tests/models/test_customer_trades.py +113 -0
  352. wbportfolio/tests/models/test_dividends.py +7 -0
  353. wbportfolio/tests/models/test_imports.py +192 -0
  354. wbportfolio/tests/models/test_instrument_mixins.py +48 -0
  355. wbportfolio/tests/models/test_merge.py +133 -0
  356. wbportfolio/tests/models/test_portfolio_cash_flow.py +112 -0
  357. wbportfolio/tests/models/test_portfolio_cash_targets.py +27 -0
  358. wbportfolio/tests/models/test_portfolio_swing_pricings.py +42 -0
  359. wbportfolio/tests/models/test_portfolios.py +676 -0
  360. wbportfolio/tests/models/test_product_groups.py +80 -0
  361. wbportfolio/tests/models/test_products.py +187 -0
  362. wbportfolio/tests/models/test_roles.py +82 -0
  363. wbportfolio/tests/models/test_splits.py +233 -0
  364. wbportfolio/tests/models/test_synchronization.py +617 -0
  365. wbportfolio/tests/models/transactions/__init__.py +0 -0
  366. wbportfolio/tests/models/transactions/test_claim.py +129 -0
  367. wbportfolio/tests/models/transactions/test_fees.py +65 -0
  368. wbportfolio/tests/models/transactions/test_trades.py +204 -0
  369. wbportfolio/tests/models/utils.py +13 -0
  370. wbportfolio/tests/serializers/__init__.py +0 -0
  371. wbportfolio/tests/serializers/test_claims.py +21 -0
  372. wbportfolio/tests/signals.py +151 -0
  373. wbportfolio/tests/tests.py +31 -0
  374. wbportfolio/tests/viewsets/__init__.py +0 -0
  375. wbportfolio/tests/viewsets/test_assets.py +67 -0
  376. wbportfolio/tests/viewsets/test_performances.py +72 -0
  377. wbportfolio/tests/viewsets/test_products.py +92 -0
  378. wbportfolio/tests/viewsets/transactions/__init__.py +0 -0
  379. wbportfolio/tests/viewsets/transactions/test_claims.py +146 -0
  380. wbportfolio/urls.py +247 -0
  381. wbportfolio/utils.py +30 -0
  382. wbportfolio/viewsets/__init__.py +57 -0
  383. wbportfolio/viewsets/adjustments.py +46 -0
  384. wbportfolio/viewsets/assets.py +562 -0
  385. wbportfolio/viewsets/assets_and_net_new_money_progression.py +117 -0
  386. wbportfolio/viewsets/charts/__init__.py +1 -0
  387. wbportfolio/viewsets/charts/assets.py +247 -0
  388. wbportfolio/viewsets/configs/__init__.py +6 -0
  389. wbportfolio/viewsets/configs/buttons/__init__.py +23 -0
  390. wbportfolio/viewsets/configs/buttons/adjustments.py +13 -0
  391. wbportfolio/viewsets/configs/buttons/assets.py +145 -0
  392. wbportfolio/viewsets/configs/buttons/claims.py +83 -0
  393. wbportfolio/viewsets/configs/buttons/custodians.py +76 -0
  394. wbportfolio/viewsets/configs/buttons/fees.py +14 -0
  395. wbportfolio/viewsets/configs/buttons/mixins.py +88 -0
  396. wbportfolio/viewsets/configs/buttons/portfolios.py +115 -0
  397. wbportfolio/viewsets/configs/buttons/products.py +41 -0
  398. wbportfolio/viewsets/configs/buttons/reconciliations.py +65 -0
  399. wbportfolio/viewsets/configs/buttons/registers.py +11 -0
  400. wbportfolio/viewsets/configs/buttons/signals.py +68 -0
  401. wbportfolio/viewsets/configs/buttons/trade_proposals.py +25 -0
  402. wbportfolio/viewsets/configs/buttons/trades.py +144 -0
  403. wbportfolio/viewsets/configs/display/__init__.py +61 -0
  404. wbportfolio/viewsets/configs/display/adjustments.py +81 -0
  405. wbportfolio/viewsets/configs/display/assets.py +265 -0
  406. wbportfolio/viewsets/configs/display/claim.py +299 -0
  407. wbportfolio/viewsets/configs/display/custodians.py +24 -0
  408. wbportfolio/viewsets/configs/display/esg.py +88 -0
  409. wbportfolio/viewsets/configs/display/fees.py +133 -0
  410. wbportfolio/viewsets/configs/display/portfolio_cash_flow.py +103 -0
  411. wbportfolio/viewsets/configs/display/portfolio_relationship.py +38 -0
  412. wbportfolio/viewsets/configs/display/portfolios.py +125 -0
  413. wbportfolio/viewsets/configs/display/positions.py +75 -0
  414. wbportfolio/viewsets/configs/display/product_groups.py +54 -0
  415. wbportfolio/viewsets/configs/display/product_performance.py +241 -0
  416. wbportfolio/viewsets/configs/display/products.py +249 -0
  417. wbportfolio/viewsets/configs/display/reconciliations.py +151 -0
  418. wbportfolio/viewsets/configs/display/registers.py +71 -0
  419. wbportfolio/viewsets/configs/display/roles.py +49 -0
  420. wbportfolio/viewsets/configs/display/trade_proposals.py +97 -0
  421. wbportfolio/viewsets/configs/display/trades.py +359 -0
  422. wbportfolio/viewsets/configs/display/transactions.py +55 -0
  423. wbportfolio/viewsets/configs/endpoints/__init__.py +75 -0
  424. wbportfolio/viewsets/configs/endpoints/adjustments.py +17 -0
  425. wbportfolio/viewsets/configs/endpoints/assets.py +115 -0
  426. wbportfolio/viewsets/configs/endpoints/claim.py +106 -0
  427. wbportfolio/viewsets/configs/endpoints/custodians.py +6 -0
  428. wbportfolio/viewsets/configs/endpoints/esg.py +14 -0
  429. wbportfolio/viewsets/configs/endpoints/fees.py +26 -0
  430. wbportfolio/viewsets/configs/endpoints/portfolio_relationship.py +23 -0
  431. wbportfolio/viewsets/configs/endpoints/portfolios.py +43 -0
  432. wbportfolio/viewsets/configs/endpoints/positions.py +18 -0
  433. wbportfolio/viewsets/configs/endpoints/product_groups.py +11 -0
  434. wbportfolio/viewsets/configs/endpoints/product_performance.py +29 -0
  435. wbportfolio/viewsets/configs/endpoints/products.py +37 -0
  436. wbportfolio/viewsets/configs/endpoints/reconciliations.py +31 -0
  437. wbportfolio/viewsets/configs/endpoints/roles.py +9 -0
  438. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +17 -0
  439. wbportfolio/viewsets/configs/endpoints/trades.py +82 -0
  440. wbportfolio/viewsets/configs/endpoints/transactions.py +17 -0
  441. wbportfolio/viewsets/configs/menu/__init__.py +30 -0
  442. wbportfolio/viewsets/configs/menu/adjustments.py +8 -0
  443. wbportfolio/viewsets/configs/menu/assets.py +8 -0
  444. wbportfolio/viewsets/configs/menu/claim.py +41 -0
  445. wbportfolio/viewsets/configs/menu/custodians.py +11 -0
  446. wbportfolio/viewsets/configs/menu/fees.py +13 -0
  447. wbportfolio/viewsets/configs/menu/instrument_prices.py +10 -0
  448. wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +8 -0
  449. wbportfolio/viewsets/configs/menu/portfolios.py +15 -0
  450. wbportfolio/viewsets/configs/menu/positions.py +14 -0
  451. wbportfolio/viewsets/configs/menu/product_groups.py +10 -0
  452. wbportfolio/viewsets/configs/menu/product_performance.py +25 -0
  453. wbportfolio/viewsets/configs/menu/products.py +15 -0
  454. wbportfolio/viewsets/configs/menu/reconciliations.py +7 -0
  455. wbportfolio/viewsets/configs/menu/registers.py +10 -0
  456. wbportfolio/viewsets/configs/menu/roles.py +16 -0
  457. wbportfolio/viewsets/configs/menu/trades.py +18 -0
  458. wbportfolio/viewsets/configs/menu/transactions.py +8 -0
  459. wbportfolio/viewsets/configs/previews/__init__.py +1 -0
  460. wbportfolio/viewsets/configs/previews/portfolios.py +21 -0
  461. wbportfolio/viewsets/configs/titles/__init__.py +65 -0
  462. wbportfolio/viewsets/configs/titles/adjustments.py +19 -0
  463. wbportfolio/viewsets/configs/titles/assets.py +57 -0
  464. wbportfolio/viewsets/configs/titles/assets_and_net_new_money_progression.py +6 -0
  465. wbportfolio/viewsets/configs/titles/claim.py +81 -0
  466. wbportfolio/viewsets/configs/titles/custodians.py +12 -0
  467. wbportfolio/viewsets/configs/titles/esg.py +10 -0
  468. wbportfolio/viewsets/configs/titles/fees.py +25 -0
  469. wbportfolio/viewsets/configs/titles/instrument_prices.py +20 -0
  470. wbportfolio/viewsets/configs/titles/portfolios.py +32 -0
  471. wbportfolio/viewsets/configs/titles/positions.py +11 -0
  472. wbportfolio/viewsets/configs/titles/product_groups.py +12 -0
  473. wbportfolio/viewsets/configs/titles/product_performance.py +16 -0
  474. wbportfolio/viewsets/configs/titles/products.py +6 -0
  475. wbportfolio/viewsets/configs/titles/registers.py +12 -0
  476. wbportfolio/viewsets/configs/titles/roles.py +23 -0
  477. wbportfolio/viewsets/configs/titles/trades.py +51 -0
  478. wbportfolio/viewsets/configs/titles/transactions.py +8 -0
  479. wbportfolio/viewsets/custodians.py +66 -0
  480. wbportfolio/viewsets/esg.py +165 -0
  481. wbportfolio/viewsets/mixins.py +48 -0
  482. wbportfolio/viewsets/portfolio_cash_flow.py +31 -0
  483. wbportfolio/viewsets/portfolio_cash_targets.py +8 -0
  484. wbportfolio/viewsets/portfolio_relationship.py +46 -0
  485. wbportfolio/viewsets/portfolio_swing_pricing.py +8 -0
  486. wbportfolio/viewsets/portfolios.py +154 -0
  487. wbportfolio/viewsets/positions.py +292 -0
  488. wbportfolio/viewsets/product_groups.py +84 -0
  489. wbportfolio/viewsets/product_performance.py +646 -0
  490. wbportfolio/viewsets/products.py +529 -0
  491. wbportfolio/viewsets/reconciliations.py +160 -0
  492. wbportfolio/viewsets/registers.py +75 -0
  493. wbportfolio/viewsets/roles.py +44 -0
  494. wbportfolio/viewsets/signals.py +42 -0
  495. wbportfolio/viewsets/synchronization.py +25 -0
  496. wbportfolio/viewsets/transactions/__init__.py +40 -0
  497. wbportfolio/viewsets/transactions/claim.py +933 -0
  498. wbportfolio/viewsets/transactions/fees.py +190 -0
  499. wbportfolio/viewsets/transactions/mixins.py +19 -0
  500. wbportfolio/viewsets/transactions/trade_proposals.py +93 -0
  501. wbportfolio/viewsets/transactions/trades.py +395 -0
  502. wbportfolio/viewsets/transactions/transactions.py +123 -0
  503. wbportfolio-1.43.1.dist-info/METADATA +22 -0
  504. wbportfolio-1.43.1.dist-info/RECORD +506 -0
  505. wbportfolio-1.43.1.dist-info/WHEEL +5 -0
  506. wbportfolio-1.43.1.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,50 @@
1
+ import json
2
+ from datetime import datetime
3
+ from io import BytesIO
4
+
5
+ from django.core.serializers.json import DjangoJSONEncoder
6
+ from pandas.tseries.offsets import BDay
7
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
8
+ from wbfdm.models import Instrument
9
+ from wbportfolio.import_export.backends.utils import (
10
+ get_timedelta_import_instrument_price,
11
+ )
12
+
13
+ from .mixin import DataBackendMixin
14
+
15
+
16
+ @register("Adjustment", provider_key="wbfdm", save_data_in_import_source=False, passive_only=False)
17
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
18
+ DATE_LABEL = "adjustment_date"
19
+ DEFAULT_MAPPING = {
20
+ "adjustment_date": "date",
21
+ "adjustment_factor": "factor",
22
+ "cumulative_adjustment_factor": "cumulative_factor",
23
+ "instrument_id": "instrument",
24
+ }
25
+ NONE_NULLABLE_FIELDS = list(DEFAULT_MAPPING.values())
26
+
27
+ def get_default_queryset(self):
28
+ return Instrument.objects.filter(is_investable_universe=True)
29
+
30
+ def get_files(
31
+ self,
32
+ execution_time: datetime,
33
+ **kwargs,
34
+ ) -> BytesIO:
35
+ execution_date = execution_time.date()
36
+ start = kwargs.get("start", (execution_date - BDay(get_timedelta_import_instrument_price())).date())
37
+ data = []
38
+ for dict_dto in self.get_default_queryset().dl.adjustments(
39
+ from_date=start,
40
+ to_date=execution_date,
41
+ ):
42
+ if dict_dto["adjustment_factor"] is not None:
43
+ data.append(dict_dto)
44
+ if data:
45
+ content_file = BytesIO()
46
+ content_file.write(json.dumps(data, cls=DjangoJSONEncoder).encode())
47
+ file_name = (
48
+ f"adjustment_{start:%Y-%m-%d}-{execution_date:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
49
+ )
50
+ yield file_name, content_file
@@ -0,0 +1,16 @@
1
+ from wbcore.contrib.io.backends import AbstractDataBackend, register
2
+
3
+ from .mixin import DataBackendMixin
4
+
5
+
6
+ @register("Dividend", provider_key="wbfdm", save_data_in_import_source=False, passive_only=False)
7
+ class DataBackend(DataBackendMixin, AbstractDataBackend):
8
+ TIMEDELTA = 1
9
+ ATTRIBUTE_NAME = "dividends"
10
+ FILE_NAME = "dividend"
11
+ DEFAULT_MAPPING = {
12
+ "instrument_id": "instrument",
13
+ "rate": "weighting",
14
+ "ex_dividend_date": "value_date",
15
+ "payment_date": "transaction_date",
16
+ }
@@ -0,0 +1,15 @@
1
+ from datetime import date
2
+
3
+ from django.db import models
4
+
5
+ MUTUAL_FUND_TIMEDELTA_DAY_SHIFT = 7
6
+
7
+
8
+ class DataBackendMixin:
9
+ DATE_LABEL = "valuation_date"
10
+
11
+ def is_object_valid(self, obj: models.Model) -> bool:
12
+ return super().is_object_valid(obj) and obj.assets.exists() and obj.is_active_at_date(date.today())
13
+
14
+ def get_provider_id(self, obj: models.Model) -> str:
15
+ return obj.id
File without changes
@@ -0,0 +1,39 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from wbcore.contrib.io.exceptions import DeserializationError
4
+ from wbcore.contrib.io.imports import ImportExportHandler
5
+ from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
6
+
7
+
8
+ class AdjustmentImportHandler(ImportExportHandler):
9
+ MODEL_APP_LABEL = "wbportfolio.Adjustment"
10
+
11
+ def __init__(self, *args, **kwargs):
12
+ super().__init__(*args, **kwargs)
13
+ self.instrument_handler = InstrumentImportHandler(self.import_source)
14
+
15
+ def _deserialize(self, data):
16
+ data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
17
+ data["instrument"] = self.instrument_handler.process_object(
18
+ data["instrument"], only_security=True, read_only=True
19
+ )[0]
20
+ if data["factor"] is None:
21
+ raise DeserializationError("Can't process this data: no factor")
22
+
23
+ def _get_instance(self, data, history=None, **kwargs):
24
+ self.import_source.log += "\nGet Adjustment Instance."
25
+ self.import_source.log += f"\nParameter: Instrument={data['instrument']} Date={data['date']}"
26
+
27
+ if exact_adjustment := data["instrument"].pms_adjustments.filter(date=data["date"]).first():
28
+ return exact_adjustment
29
+ potential_adjustments = data["instrument"].pms_adjustments.filter(
30
+ date__gte=data["date"] - timedelta(days=7),
31
+ date__lte=data["date"] + timedelta(days=7),
32
+ factor=data["factor"],
33
+ )
34
+ if potential_adjustments.exists():
35
+ return potential_adjustments.first()
36
+
37
+ def _create_instance(self, data, **kwargs):
38
+ self.import_source.log += "\nCreate Adjustment datapoint."
39
+ return self.model.objects.create(**data, import_source=self.import_source)
@@ -0,0 +1,167 @@
1
+ from collections import defaultdict
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+ from itertools import chain
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from django.db import models
8
+ from wbcore.contrib.authentication.authentication import User
9
+ from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
10
+ from wbcore.contrib.io.exceptions import DeserializationError
11
+ from wbcore.contrib.io.imports import ImportExportHandler
12
+ from wbcore.contrib.notifications.dispatch import send_notification
13
+ from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
14
+ from wbfdm.import_export.handlers.instrument_price import InstrumentPriceImportHandler
15
+ from wbfdm.models.exchanges import Exchange
16
+ from wbportfolio.models.roles import PortfolioRole
17
+
18
+
19
+ class AssetPositionImportHandler(ImportExportHandler):
20
+ MODEL_APP_LABEL: str = "wbportfolio.AssetPosition"
21
+ PRICE_DATE_TIMEDELTA: int = 7
22
+ MAX_PRICE_DATE_TIMEDELTA: int = 360
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ super().__init__(*args, **kwargs)
26
+ self.instrument_handler = InstrumentImportHandler(self.import_source)
27
+ self.instrument_price_handler = InstrumentPriceImportHandler(self.import_source)
28
+ self.currency_handler = CurrencyImportHandler(self.import_source)
29
+
30
+ def _deserialize(self, data: Dict[str, Any]):
31
+ from wbportfolio.models import Portfolio
32
+
33
+ portfolio_data = data.pop("portfolio", None)
34
+ underlying_instrument_data = data.pop("underlying_instrument", None)
35
+ if "currency" in data:
36
+ data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
37
+ data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
38
+ if data.get("asset_valuation_date", None):
39
+ data["asset_valuation_date"] = datetime.strptime(data["asset_valuation_date"], "%Y-%m-%d").date()
40
+ else:
41
+ data["asset_valuation_date"] = data["date"]
42
+
43
+ if exchange_data := data.pop("exchange", None):
44
+ sanitized_dict = {k: v for k, v in exchange_data.items() if v is not None}
45
+ if sanitized_dict:
46
+ data["exchange"] = Exchange.dict_to_model(sanitized_dict)
47
+
48
+ data["portfolio"] = Portfolio._get_or_create_portfolio(self.instrument_handler, portfolio_data)
49
+ data["underlying_instrument"] = self.instrument_handler.process_object(
50
+ underlying_instrument_data, only_security=False, read_only=True
51
+ )[0]
52
+
53
+ # number type deserialization and sanitization
54
+ # ensure the provided Decimal field are of type Decimal
55
+ decimal_fields = ["initial_currency_fx_rate", "initial_price", "initial_shares", "weighting"]
56
+ for field in decimal_fields:
57
+ if not (value := data.get(field, None)) is None:
58
+ data[field] = Decimal(value)
59
+
60
+ # Ensure that for shares and weighting, a None value default to 0
61
+ if "initial_shares" in data and data["initial_shares"] is None:
62
+ data["initial_shares"] = Decimal(0)
63
+ if "weighting" in data and data["weighting"] is None:
64
+ data["weighting"] = Decimal(0)
65
+
66
+ # if the initial price is not provided, we try to get it directly from the dataloader
67
+ if data.get("initial_price") is None:
68
+ try:
69
+ data["initial_price"] = data["underlying_instrument"].get_price(
70
+ data["date"], price_date_timedelta=self.PRICE_DATE_TIMEDELTA
71
+ )
72
+ except ValueError:
73
+ # If we cannot find a price with the default timedelta, we try with a bigger range (in case the instrument stopped trading for some time for instance)
74
+ try:
75
+ data["initial_price"] = data["underlying_instrument"].get_price(
76
+ data["date"], price_date_timedelta=self.MAX_PRICE_DATE_TIMEDELTA
77
+ )
78
+ except ValueError:
79
+ raise DeserializationError("Price not provided but can not be found automatically")
80
+
81
+ def _process_raw_data(self, data: Dict[str, Any]):
82
+ if prices := data.get("prices", None):
83
+ self.import_source.log += "Instrument Prices found: Importing"
84
+ self.instrument_price_handler.process({"data": prices})
85
+
86
+ def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
87
+ self.import_source.log += "\nTrying to get asset position instance"
88
+ position = data["portfolio"].assets.filter(
89
+ date=data["date"], underlying_instrument=data["underlying_instrument"]
90
+ )
91
+ if position.exists():
92
+ self.import_source.log += "\nAsset Position found."
93
+ if position.count() > 1:
94
+ position = position.filter(is_estimated=False)
95
+ if position.count() > 1:
96
+ raise ValueError(f'We should find only one Assetposition:{position.values_list("id", flat=True)}')
97
+ return position.first()
98
+ self.import_source.log += "\nAsset Position not found. A new one will be created"
99
+ return None
100
+
101
+ def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
102
+ instance = self.model(
103
+ portfolio=data["portfolio"],
104
+ underlying_instrument=data["underlying_instrument"],
105
+ date=data["date"],
106
+ asset_valuation_date=data["asset_valuation_date"],
107
+ weighting=data.get("weighting", None),
108
+ initial_price=data["initial_price"],
109
+ initial_shares=data.get("initial_shares", None),
110
+ initial_currency_fx_rate=data.get("initial_currency_fx_rate", 1),
111
+ import_source=self.import_source,
112
+ currency=data.get("currency", None),
113
+ )
114
+
115
+ return self._save_object(instance, **kwargs)
116
+
117
+ def _save_object(self, _object, **kwargs):
118
+ _object.underlying_instrument_price = (
119
+ None # detech possibly already attached instrument price to retrigger the save mechanism
120
+ )
121
+ _object.save(create_underlying_instrument_price_if_missing=True)
122
+ return _object
123
+
124
+ def _post_processing_objects(
125
+ self,
126
+ created_objs: List[models.Model],
127
+ modified_objs: List[models.Model],
128
+ unmodified_objs: List[models.Model],
129
+ ):
130
+ from wbportfolio.models.portfolio import trigger_portfolio_change_as_task
131
+
132
+ portfolio_to_resynch = defaultdict(set)
133
+ imported_ids = defaultdict(set)
134
+ for obj in chain(created_objs, modified_objs, unmodified_objs):
135
+ portfolio_to_resynch[obj.portfolio].add(obj.date)
136
+ imported_ids[obj.portfolio].add(obj.id)
137
+
138
+ for portfolio, dates in portfolio_to_resynch.items():
139
+ # We remove leftovers positions from wrongly imported file
140
+ leftovers_positions = self.model.objects.filter(portfolio=portfolio, date__in=dates)
141
+ for obj_id in imported_ids[portfolio]:
142
+ leftovers_positions = leftovers_positions.exclude(id=obj_id)
143
+ for position in leftovers_positions:
144
+ position.delete()
145
+ for val_date in sorted(dates):
146
+ trigger_portfolio_change_as_task.delay(portfolio.id, val_date, recompute_weighting=True)
147
+
148
+ portfolios = set([(obj.portfolio, obj.date) for obj in chain(created_objs, modified_objs)])
149
+
150
+ for portfolio, val_date in portfolios:
151
+ custodian = portfolio.dependency_through.filter(type="CUSTODIAN")
152
+ if custodian.exists():
153
+ differences = portfolio.check_related_portfolio_at_date(
154
+ val_date, custodian.first().dependency_portfolio
155
+ )
156
+ if differences.exists():
157
+ for user in User.objects.filter(
158
+ profile_id__in=PortfolioRole.portfolio_managers().values_list("person", flat=True)
159
+ ):
160
+ send_notification(
161
+ code="wbportfolio.portfolio.check_custodian_portfolio",
162
+ title="There is a discrepency between two portfolios",
163
+ body=f"There has been a discrepency between two portfolios: {portfolio} and {custodian.first().dependency_portfolio}.",
164
+ user=user,
165
+ reverse_name="wbportfolio:portfolio-modelcompositionpandas-list",
166
+ reverse_args=[portfolio.id],
167
+ )
@@ -0,0 +1,80 @@
1
+ import math
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+ from typing import Any, Dict
5
+
6
+ from django.db import models
7
+ from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
8
+ from wbcore.contrib.io.exceptions import DeserializationError
9
+ from wbcore.contrib.io.imports import ImportExportHandler
10
+ from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
11
+
12
+
13
+ class DividendImportHandler(ImportExportHandler):
14
+ MODEL_APP_LABEL = "wbportfolio.DividendTransaction"
15
+
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
+ self.instrument_handler = InstrumentImportHandler(self.import_source)
19
+ self.currency_handler = CurrencyImportHandler(self.import_source)
20
+
21
+ def _deserialize(self, data):
22
+ data["transaction_date"] = datetime.strptime(data["transaction_date"], "%Y-%m-%d").date()
23
+ data["value_date"] = datetime.strptime(data["value_date"], "%Y-%m-%d").date()
24
+ from wbportfolio.models import Portfolio
25
+
26
+ data["portfolio"] = Portfolio.objects.get(id=data["portfolio"])
27
+ instrument = self.instrument_handler.process_object(
28
+ data["underlying_instrument"], only_security=False, read_only=True
29
+ )[0]
30
+ if not instrument:
31
+ raise DeserializationError("Can't process this data: underlying instrument not found")
32
+ data["underlying_instrument"] = instrument
33
+ if "currency" not in data:
34
+ data["currency"] = data["portfolio"].currency
35
+ else:
36
+ data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
37
+
38
+ for field in self.model._meta.get_fields():
39
+ if not (value := data.get(field.name, None)) is None and isinstance(field, models.DecimalField):
40
+ q = 1 / (math.pow(10, 4))
41
+ data[field.name] = Decimal(value).quantize(Decimal(str(q)))
42
+
43
+ def _get_instance(self, data, history=None, **kwargs):
44
+ self.import_source.log += "\nGet DividendTransaction Instance."
45
+ self.import_source.log += f"\nParameter: Portfolio={data['portfolio']} Underlying={data['underlying_instrument']} Date={data['transaction_date']}"
46
+ dividends = history if history is not None else self.model.objects
47
+
48
+ dividends = dividends.filter(
49
+ portfolio=data["portfolio"],
50
+ transaction_date=data["transaction_date"],
51
+ value_date=data["value_date"],
52
+ underlying_instrument=data["underlying_instrument"],
53
+ price_gross=data["price_gross"],
54
+ )
55
+ if dividends.count() == 1:
56
+ self.import_source.log += "\nDividendTransaction Instance Found." ""
57
+ return dividends.first()
58
+
59
+ def _create_instance(self, data, **kwargs):
60
+ self.import_source.log += "\nCreate DividendTransaction."
61
+ return self.model.objects.create(**data, import_source=self.import_source)
62
+
63
+ def _get_history(self: models.Model, history: Dict[str, Any]) -> models.QuerySet:
64
+ val_date = datetime.strptime(history["transaction_date"], "%Y-%m-%d")
65
+ if portfolio_id := history.get("portfolio", None):
66
+ dividends = self.model.objects.filter(transaction_date__lte=val_date, portfolio=portfolio_id)
67
+ if underlying_instrument_id := history.get("underlying_instrument", None):
68
+ dividends = dividends.filter(underlying_instrument=underlying_instrument_id)
69
+ return dividends
70
+
71
+ def _post_processing_history(self: models.Model, history: models.QuerySet):
72
+ self.import_source.log += "===================="
73
+ self.import_source.log += (
74
+ "It was a historical import and the following DividendTransaction have to be deleted:"
75
+ )
76
+ for dividend in history.order_by("transaction_date"):
77
+ self.import_source.log += (
78
+ f"\n{dividend.transaction_date:%d.%m.%Y}: {dividend.shares} {dividend.price} ==> Deleted"
79
+ )
80
+ dividend.delete()
@@ -0,0 +1,58 @@
1
+ from datetime import datetime
2
+
3
+ from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
4
+ from wbcore.contrib.io.imports import ImportExportHandler
5
+ from wbfdm.models.instruments import Cash
6
+ from wbportfolio.models.products import Product
7
+
8
+
9
+ class FeesImportHandler(ImportExportHandler):
10
+ MODEL_APP_LABEL: str = "wbportfolio.Fees"
11
+
12
+ def __init__(self, *args, **kwargs):
13
+ super().__init__(*args, **kwargs)
14
+ self.currency_handler = CurrencyImportHandler(self.import_source)
15
+
16
+ def _deserialize(self, data):
17
+ data["transaction_date"] = datetime.strptime(data["transaction_date"], "%Y-%m-%d").date()
18
+ data["fee_date"] = data["transaction_date"]
19
+ if value_date_str := data.get("value_date", None):
20
+ data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
21
+ if book_date_str := data.get("book_date", None):
22
+ data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
23
+
24
+ from wbportfolio.models import Portfolio
25
+
26
+ data["linked_product"] = Product.objects.get(id=data["linked_product"])
27
+ if "porfolio" in data:
28
+ data["portfolio"] = Portfolio.objects.get(id=data["portfolio"])
29
+ else:
30
+ data["portfolio"] = data["linked_product"].primary_portfolio
31
+ data["underlying_instrument"] = Cash.objects.filter(currency=data["portfolio"].currency).first()
32
+ if "currency" not in data:
33
+ data["currency"] = data["portfolio"].currency
34
+ else:
35
+ data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
36
+ data["currency_fx_rate"] = 1.0
37
+ data["total_value"] = data.get("total_value", data.get("total_value_gross", None))
38
+ data["total_value_gross"] = data.get("total_value_gross", data["total_value"])
39
+ data["calculated"] = data.get("calculated", False)
40
+
41
+ def _get_instance(self, data, history=None, **kwargs):
42
+ self.import_source.log += "\nGet Fees Instance."
43
+ self.import_source.log += f"\nParameter: Portfolio={data['portfolio']} Date={data['transaction_date']}"
44
+ fees = self.model.objects.filter(
45
+ linked_product=data["linked_product"],
46
+ fee_date=data["fee_date"],
47
+ transaction_subtype=data["transaction_subtype"],
48
+ calculated=data["calculated"],
49
+ )
50
+ if fees.exists():
51
+ if fees.count() > 1:
52
+ raise ValueError(f'Number of similar fees found > 1: {fees.values_list("id", flat=True)}')
53
+ self.import_source.log += "\nFees Instance Found." ""
54
+ return fees.first()
55
+
56
+ def _create_instance(self, data, **kwargs):
57
+ self.import_source.log += "\nCreate Fees."
58
+ return self.model.objects.create(**data, import_source=self.import_source)
@@ -0,0 +1,57 @@
1
+ from contextlib import suppress
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+ from typing import TYPE_CHECKING
5
+
6
+ from django.contrib.auth import get_user_model
7
+ from django.db.models import Q
8
+ from wbcore.contrib.io.imports import ImportExportHandler
9
+ from wbcore.contrib.notifications.dispatch import send_notification
10
+ from wbportfolio.models import Portfolio
11
+
12
+ if TYPE_CHECKING:
13
+ from wbportfolio.models.portfolio_cash_flow import DailyPortfolioCashFlow
14
+
15
+
16
+ class DailyPortfolioCashFlowImportHandler(ImportExportHandler):
17
+ MODEL_APP_LABEL = "wbportfolio.DailyPortfolioCashFlow"
18
+
19
+ def _deserialize(self, data):
20
+ data["value_date"] = datetime.strptime(data["value_date"], "%Y-%m-%d").date()
21
+ data["portfolio"] = Portfolio.objects.get(id=data["portfolio"])
22
+ if "cash" in data:
23
+ data["cash"] = Decimal(data["cash"])
24
+
25
+ if "total_assets" in data:
26
+ data["total_assets"] = Decimal(data["total_assets"])
27
+
28
+ if "cash_flow_forecast" in data:
29
+ data["cash_flow_forecast"] = Decimal(data["cash_flow_forecast"])
30
+
31
+ def _get_instance(self, data, history=None, **kwargs):
32
+ self.import_source.log += "\nGet Daily Portfolio Cash Flow Instance."
33
+ self.import_source.log += f"\nParameter: Portfolio={data['portfolio']} Value Date={data['value_date']}"
34
+
35
+ with suppress(self.model.DoesNotExist):
36
+ return self.model.objects.get(portfolio=data["portfolio"], value_date=data["value_date"])
37
+
38
+ def _post_processing_created_object(self, _object: "DailyPortfolioCashFlow"):
39
+ if not _object.portfolio.daily_cashflows.filter(value_date__gt=_object.value_date).exists():
40
+ if _object.proposed_rebalancing != Decimal(0):
41
+ color = "red" if _object.proposed_rebalancing < 0 else "green"
42
+ for user in (
43
+ get_user_model()
44
+ .objects.filter(
45
+ Q(user_permissions__codename="view_dailyportfoliocashflow")
46
+ | Q(groups__permissions__codename="view_dailyportfoliocashflow")
47
+ )
48
+ .distinct()
49
+ ):
50
+ send_notification(
51
+ code="wbportfolio.dailyportfoliocashflow.notify_rebalance",
52
+ title=f"Proposed Rebalancing in the portfolio: {_object.portfolio}",
53
+ body=f"The workbench proposes to rebalance the portfolio {_object.portfolio} by <span style='color:{color}'>{_object.proposed_rebalancing:+,.2f}</span>",
54
+ user=user,
55
+ reverse_name="wbportfolio:portfoliocashflow-detail",
56
+ reverse_args=[_object.pk],
57
+ )
@@ -0,0 +1,43 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, Optional
3
+
4
+ from django.db import models
5
+ from wbcore.contrib.geography.models import Geography
6
+ from wbcore.contrib.io.imports import ImportExportHandler
7
+
8
+
9
+ class RegisterImportHandler(ImportExportHandler):
10
+ MODEL_APP_LABEL = "wbportfolio.Register"
11
+
12
+ def _deserialize(self, data: Dict[str, Any]):
13
+ if "opened" in data:
14
+ data["opened"] = datetime.strptime(data["opened"], "%Y-%m-%d").date()
15
+ if residence_id := data.get("residence"):
16
+ data["residence"] = Geography.countries.get(id=residence_id)
17
+ if citizenship_id := data.get("citizenship"):
18
+ data["citizenship"] = Geography.countries.get(id=citizenship_id)
19
+ if outlet_country_id := data.get("outlet_country"):
20
+ data["outlet_country"] = Geography.countries.get(id=outlet_country_id)
21
+ if custodian_country_id := data.get("custodian_country"):
22
+ data["custodian_country"] = Geography.countries.get(id=custodian_country_id)
23
+
24
+ outlet_city = data.pop("outlet_city", None)
25
+ if (outlet_country := data.get("outlet_country", None)) and outlet_city:
26
+ if res := outlet_country.lookup_descendants(outlet_city, level=Geography.Level.CITY.value):
27
+ data["outlet_city"] = res
28
+
29
+ custodian_city = data.pop("custodian_city", None)
30
+ if (custodian_country := data.get("custodian_country", None)) and custodian_city:
31
+ if res := custodian_country.lookup_descendants(custodian_city, level=Geography.Level.CITY.value):
32
+ data["custodian_city"] = res
33
+
34
+ def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
35
+ self.import_source.log += "Get Register Instance."
36
+ self.import_source.log += f"Parameter: Reference={data['register_reference']}"
37
+ if isinstance(data, int):
38
+ return self.model.objects.get(id=data)
39
+ return self.model.objects.filter(register_reference=data["register_reference"]).first()
40
+
41
+ def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
42
+ self.import_source.log += "Create Register."
43
+ return self.model.objects.create(import_source=self.import_source, **data)