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,77 @@
1
+ # Generated by Django 5.0.8 on 2024-10-01 14:43
2
+
3
+ from collections import defaultdict
4
+ from contextlib import suppress
5
+
6
+ from django.db import migrations, models
7
+ from tqdm import tqdm
8
+
9
+
10
+ def remove_duplicates(apps, schema_editor):
11
+ changed_portfolios = defaultdict(set)
12
+ from wbportfolio.models import Portfolio
13
+
14
+ AssetPosition = apps.get_model("wbportfolio", "AssetPosition")
15
+ for row in (
16
+ AssetPosition.objects.values("portfolio", "date", "underlying_instrument", "portfolio_created")
17
+ .annotate(c=models.Count("*"))
18
+ .filter(c__gt=1)
19
+ ):
20
+ with suppress(Exception):
21
+ qs = AssetPosition.objects.filter(
22
+ date=row["date"],
23
+ portfolio=row["portfolio"],
24
+ underlying_instrument=row["underlying_instrument"],
25
+ portfolio_created=row["portfolio_created"],
26
+ )
27
+ qs.filter(weighting=0).delete()
28
+ base_p = qs.first()
29
+ for a in qs.exclude(id=base_p.id):
30
+ if (
31
+ abs(base_p.weighting / a.weighting - 1) < 0.02
32
+ or (a.import_source is None and a.portfolio_created is None)
33
+ or (a.portfolio.id == 117 and a.underlying_instrument.id == 214)
34
+ ):
35
+ changed_portfolios[a.portfolio].add(a.date)
36
+ a.delete()
37
+ if base_p.import_source and (
38
+ qs.filter(import_source=base_p.import_source).count() > 1
39
+ or (base_p.portfolio.id == 123 and base_p.underlying_instrument.id == 214)
40
+ ):
41
+ base_p.weighting += qs.aggregate(c=models.Sum("weighting"))["c"]
42
+ base_p.shares += qs.aggregate(c=models.Sum("shares"))["c"]
43
+ base_p.save()
44
+ changed_portfolios[base_p.portfolio].add(base_p.date)
45
+ qs.filter(import_source=base_p.import_source).delete()
46
+
47
+ # Normalizing portfolio weights
48
+ for portfolio, dates in tqdm(changed_portfolios.items()):
49
+ portfolio = Portfolio.all_objects.get(id=portfolio.id)
50
+ for _d in dates:
51
+ portfolio.change_at_date(_d, recompute_weighting=True, force_recompute_weighting=True, synchronize=False)
52
+
53
+
54
+ class Migration(migrations.Migration):
55
+ dependencies = [
56
+ ("currency", "0001_initial"),
57
+ ("io", "0007_alter_exportsource_query_params"),
58
+ ("wbfdm", "0026_instrument_is_cash_equivalent"),
59
+ ("wbportfolio", "0066_assetposition_initial_shares_at_custodian_and_more"),
60
+ ]
61
+
62
+ operations = [
63
+ migrations.RunSQL(
64
+ sql="SET CONSTRAINTS ALL IMMEDIATE;",
65
+ ),
66
+ migrations.RunPython(remove_duplicates),
67
+ migrations.RunSQL(
68
+ sql="SET CONSTRAINTS ALL DEFERRED;",
69
+ ),
70
+ migrations.AddConstraint(
71
+ model_name="assetposition",
72
+ constraint=models.UniqueConstraint(
73
+ fields=("portfolio", "date", "underlying_instrument", "portfolio_created"),
74
+ name="unique_asset_position",
75
+ ),
76
+ ),
77
+ ]
@@ -0,0 +1,59 @@
1
+ # Generated by Django 5.0.9 on 2024-10-16 12:15
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ("wbportfolio", "0067_assetposition_unique_asset_position"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="trade",
15
+ name="internal_trade",
16
+ field=models.OneToOneField(
17
+ blank=True,
18
+ null=True,
19
+ on_delete=django.db.models.deletion.SET_NULL,
20
+ related_name="internal_subscription_redemption_trade",
21
+ to="wbportfolio.trade",
22
+ ),
23
+ ),
24
+ migrations.AddField(
25
+ model_name="trade",
26
+ name="marked_as_internal",
27
+ field=models.BooleanField(
28
+ default=False,
29
+ help_text="If this is checked, then this subscription or redemption is considered internal and will not be considered in any AUM computation",
30
+ verbose_name="Internal",
31
+ ),
32
+ ),
33
+ migrations.AddConstraint(
34
+ model_name="trade",
35
+ constraint=models.CheckConstraint(
36
+ check=models.Q(
37
+ ("marked_as_internal", False),
38
+ models.Q(
39
+ ("marked_as_internal", True), ("transaction_subtype__in", ["REDEMPTION", "SUBSCRIPTION"])
40
+ ),
41
+ _connector="OR",
42
+ ),
43
+ name="marked_as_internal_only_for_subred",
44
+ ),
45
+ ),
46
+ migrations.AddConstraint(
47
+ model_name="trade",
48
+ constraint=models.CheckConstraint(
49
+ check=models.Q(
50
+ ("internal_trade__isnull", True),
51
+ models.Q(
52
+ ("internal_trade__isnull", False), ("transaction_subtype__in", ["REDEMPTION", "SUBSCRIPTION"])
53
+ ),
54
+ _connector="OR",
55
+ ),
56
+ name="internal_trade_set_only_for_subred",
57
+ ),
58
+ ),
59
+ ]
@@ -0,0 +1,56 @@
1
+ # Generated by Django 5.0.9 on 2024-10-18 07:35
2
+
3
+ from datetime import date, timedelta
4
+
5
+ import django.contrib.postgres.fields.ranges
6
+ from django.db import migrations
7
+ from psycopg.types.range import DateRange
8
+
9
+
10
+ def migrate_is_invested(apps, schema_editor):
11
+ Portfolio = apps.get_model("wbportfolio", "Portfolio")
12
+ from wbportfolio.models import Product
13
+
14
+ for portfolio in Portfolio.objects.filter(is_invested=True):
15
+ if portfolio.instruments.exists():
16
+ min_inception_date = portfolio.instruments.earliest("inception_date").inception_date
17
+ else:
18
+ min_inception_date = date.min
19
+ portfolio.invested_timespan = DateRange(min_inception_date if min_inception_date else date.min, date.max)
20
+ portfolio.save()
21
+ for product in Product.objects.filter(
22
+ portfolios__in=Portfolio.objects.filter(is_invested=False).values("id")
23
+ ).distinct():
24
+ portfolio = product.portfolio
25
+ if product.delisted_date:
26
+ portfolio.invested_timespan = DateRange(product.inception_date, product.delisted_date)
27
+ else:
28
+ portfolio.invested_timespan = DateRange(
29
+ product.inception_date if product.inception_date else date.min,
30
+ product.primary_portfolio.assets.filter(underlying_instrument__instrument_type__key="product")
31
+ .earliest("date")
32
+ .date
33
+ - timedelta(days=1),
34
+ )
35
+ portfolio.save()
36
+
37
+
38
+ class Migration(migrations.Migration):
39
+ dependencies = [
40
+ ("wbportfolio", "0068_trade_internal_trade_trade_marked_as_internal_and_more"),
41
+ ]
42
+
43
+ operations = [
44
+ migrations.AddField(
45
+ model_name="portfolio",
46
+ name="invested_timespan",
47
+ field=django.contrib.postgres.fields.ranges.DateRangeField(
48
+ blank=True, help_text="Define when this portfolio is considered invested", null=True
49
+ ),
50
+ ),
51
+ migrations.RunPython(migrate_is_invested),
52
+ migrations.RemoveField(
53
+ model_name="portfolio",
54
+ name="is_invested",
55
+ ),
56
+ ]
@@ -0,0 +1,82 @@
1
+ # Generated by Django 5.0.9 on 2024-11-06 13:31
2
+
3
+ from collections import defaultdict
4
+ from contextlib import suppress
5
+
6
+ from django.db import migrations, models
7
+ from tqdm import tqdm
8
+
9
+
10
+ def remove_duplicates(apps, schema_editor):
11
+ changed_portfolios = defaultdict(set)
12
+ from wbportfolio.models import Portfolio
13
+
14
+ AssetPosition = apps.get_model("wbportfolio", "AssetPosition")
15
+ for row in (
16
+ AssetPosition.objects.values("portfolio", "date", "underlying_instrument", "portfolio_created")
17
+ .annotate(c=models.Count("*"))
18
+ .filter(c__gt=1)
19
+ ):
20
+ with suppress(Exception):
21
+ qs = AssetPosition.objects.filter(
22
+ date=row["date"],
23
+ portfolio=row["portfolio"],
24
+ underlying_instrument=row["underlying_instrument"],
25
+ portfolio_created=row["portfolio_created"],
26
+ )
27
+ qs.filter(weighting=0).delete()
28
+ base_p = qs.first()
29
+ for a in qs.exclude(id=base_p.id):
30
+ if (
31
+ abs(base_p.weighting / a.weighting - 1) < 0.02
32
+ or (a.import_source is None and a.portfolio_created is None)
33
+ or (a.portfolio.id == 117 and a.underlying_instrument.id == 214)
34
+ ):
35
+ changed_portfolios[a.portfolio].add(a.date)
36
+ a.delete()
37
+ if base_p.import_source and (
38
+ qs.filter(import_source=base_p.import_source).count() > 1
39
+ or (base_p.portfolio.id == 123 and base_p.underlying_instrument.id == 214)
40
+ ):
41
+ base_p.weighting += qs.aggregate(c=models.Sum("weighting"))["c"]
42
+ base_p.shares += qs.aggregate(c=models.Sum("shares"))["c"]
43
+ base_p.save()
44
+ changed_portfolios[base_p.portfolio].add(base_p.date)
45
+ qs.filter(import_source=base_p.import_source).delete()
46
+
47
+ # Normalizing portfolio weights
48
+ for portfolio, dates in tqdm(changed_portfolios.items()):
49
+ portfolio = Portfolio.all_objects.get(id=portfolio.id)
50
+ for _d in dates:
51
+ portfolio.change_at_date(_d, recompute_weighting=True, force_recompute_weighting=True, synchronize=False)
52
+
53
+
54
+ class Migration(migrations.Migration):
55
+ dependencies = [
56
+ ("currency", "0001_initial"),
57
+ ("io", "0007_alter_exportsource_query_params"),
58
+ ("wbfdm", "0027_remove_instrument_unique_ric_and_more"),
59
+ ("wbportfolio", "0069_remove_portfolio_is_invested_and_more"),
60
+ ]
61
+
62
+ operations = [
63
+ migrations.RemoveConstraint(
64
+ model_name="assetposition",
65
+ name="unique_asset_position",
66
+ ),
67
+ migrations.RunSQL(
68
+ sql="SET CONSTRAINTS ALL IMMEDIATE;",
69
+ ),
70
+ migrations.RunPython(remove_duplicates),
71
+ migrations.RunSQL(
72
+ sql="SET CONSTRAINTS ALL DEFERRED;",
73
+ ),
74
+ migrations.AddConstraint(
75
+ model_name="assetposition",
76
+ constraint=models.UniqueConstraint(
77
+ fields=("portfolio", "date", "underlying_instrument", "portfolio_created"),
78
+ name="unique_asset_position",
79
+ nulls_distinct=False,
80
+ ),
81
+ ),
82
+ ]
@@ -0,0 +1,22 @@
1
+ # Generated by Django 5.0.9 on 2024-11-11 11:59
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("wbportfolio", "0070_remove_assetposition_unique_asset_position_and_more"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AlterModelOptions(
13
+ name="trade",
14
+ options={"ordering": ("order",), "verbose_name": "Trade", "verbose_name_plural": "Trades"},
15
+ ),
16
+ migrations.AlterField(
17
+ model_name="trade",
18
+ name="order",
19
+ field=models.PositiveIntegerField(db_index=True, default=0, editable=False, verbose_name="order"),
20
+ preserve_default=False,
21
+ ),
22
+ ]
File without changes
@@ -0,0 +1,26 @@
1
+ # Import CRM relevant data
2
+
3
+ from .adjustments import Adjustment
4
+ from .asset import AssetPosition, AssetPositionGroupBy
5
+ from .custodians import Custodian
6
+ from .products import Product, FeeProductPercentage
7
+ from .indexes import Index
8
+ from .product_groups import ProductGroup, ProductGroupRepresentant
9
+ from .portfolio_relationship import (
10
+ PortfolioInstrumentPreferredClassificationThroughModel,
11
+ InstrumentPortfolioThroughModel,
12
+ PortfolioBankAccountThroughModel,
13
+ )
14
+ from .portfolio import Portfolio, PortfolioPortfolioThroughModel
15
+ from .portfolio_cash_targets import PortfolioCashTarget
16
+ from .portfolio_cash_flow import DailyPortfolioCashFlow
17
+ from .portfolio_swing_pricings import PortfolioSwingPricing
18
+ from .registers import Register
19
+ from .roles import PortfolioRole
20
+ from .synchronization import (
21
+ PortfolioSynchronization,
22
+ PriceComputation,
23
+ SynchronizationTask,
24
+ )
25
+ from .transactions import *
26
+ from .reconciliations import AccountReconciliation, AccountReconciliationLine
@@ -0,0 +1,246 @@
1
+ import datetime
2
+ import logging
3
+ import operator
4
+ from decimal import Decimal
5
+ from functools import reduce
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ from celery import chain, shared_task
10
+ from django.db import models
11
+ from django.db.models.signals import post_delete, post_save
12
+ from django.dispatch import receiver
13
+ from django_fsm import FSMField, transition
14
+ from pandas.tseries.offsets import BDay
15
+ from wbcore.contrib.authentication.models import User
16
+ from wbcore.contrib.icons import WBIcon
17
+ from wbcore.contrib.io.mixins import ImportMixin
18
+ from wbcore.contrib.notifications.dispatch import send_notification
19
+ from wbcore.enums import RequestType
20
+ from wbcore.metadata.configs.buttons import ActionButton
21
+ from wbcore.signals import pre_merge
22
+ from wbfdm.models.instruments.instruments import Instrument, import_prices_as_task
23
+ from wbportfolio.import_export.handlers.adjustment import AdjustmentImportHandler
24
+ from wbportfolio.models.roles import PortfolioRole
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class Adjustment(ImportMixin, models.Model):
30
+ import_export_handler_class = AdjustmentImportHandler
31
+
32
+ class Status(models.TextChoices):
33
+ PENDING = "PENDING", "Pending"
34
+ APPLIED = "APPLIED", "Applied"
35
+ DENIED = "DENIED", "Denied"
36
+
37
+ status = FSMField(default=Status.PENDING, choices=Status.choices, verbose_name="Status")
38
+
39
+ date = models.DateField(verbose_name="Executive Date")
40
+ instrument = models.ForeignKey(
41
+ "wbfdm.Instrument",
42
+ on_delete=models.CASCADE,
43
+ related_name="pms_adjustments",
44
+ limit_choices_to=models.Q(("children__isnull", True)),
45
+ )
46
+ factor = models.DecimalField(default=Decimal(1), max_digits=30, decimal_places=17, verbose_name="Factor")
47
+ cumulative_factor = models.DecimalField(
48
+ default=Decimal(1), max_digits=30, decimal_places=17, verbose_name="Cumulative Factor"
49
+ )
50
+ last_handler = models.ForeignKey(
51
+ "directory.Person", blank=True, null=True, on_delete=models.SET_NULL, related_name="last_handled_adjustments"
52
+ )
53
+
54
+ def __str__(self) -> str:
55
+ return f"Instrument {str(self.instrument)} (date: {self.date}, factor: {self.factor})"
56
+
57
+ @property
58
+ def adjustment_date(self) -> datetime.date:
59
+ return (self.date - BDay(1)).date()
60
+
61
+ def save(self, *args, **kwargs):
62
+ if not self.cumulative_factor or self.cumulative_factor == Decimal(1):
63
+ self.cumulative_factor = self.get_cumulative_factor()
64
+ super().save(*args, **kwargs)
65
+
66
+ def get_cumulative_factor(self) -> Decimal:
67
+ applied_parent_adjustments_factors = list(
68
+ Adjustment.objects.exclude(id=self.id)
69
+ .filter(status=self.Status.APPLIED, instrument=self.instrument, date__gte=self.date)
70
+ .values_list("factor", flat=True)
71
+ )
72
+ return Decimal(np.prod([*applied_parent_adjustments_factors]))
73
+
74
+ def automatically_applied_adjustments_on_assets(self) -> bool:
75
+ return reduce(operator.or_, [1 / self.factor % i == 0 for i in range(2, 5)])
76
+
77
+ def apply_adjustment_on_assets(self):
78
+ self.instrument.assets.filter(
79
+ models.Q(date__lte=self.adjustment_date)
80
+ | (models.Q(applied_adjustment__isnull=True) & models.Q(applied_adjustment__date__lt=self.date))
81
+ ).update(applied_adjustment=self)
82
+
83
+ def revert_adjustment_on_assets(self):
84
+ self.adjustmented_assets.update(applied_adjustment=None)
85
+ future_adjustments = self.instrument.pms_adjustments.filter(date__gt=self.date)
86
+ if future_adjustments.exists():
87
+ future_adjustments.earliest(
88
+ "date"
89
+ ).apply_adjustment_on_assets() # We reevalute the next future adjustment in order to check if this one can replace the reverted adjustment
90
+ self.status = self.Status.PENDING
91
+ self.save()
92
+
93
+ @transition(
94
+ field=status,
95
+ source=[Status.PENDING],
96
+ target=Status.APPLIED,
97
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(user.profile),
98
+ custom={
99
+ "_transition_button": ActionButton(
100
+ method=RequestType.PATCH,
101
+ identifiers=("wbportfolio:adjustment",),
102
+ icon=WBIcon.APPROVE.icon,
103
+ key="apply",
104
+ label="Apply",
105
+ action_label="Apply",
106
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
107
+ )
108
+ },
109
+ )
110
+ def apply(self, by: Optional["User"] = None, description: Optional[str] = None, **kwargs):
111
+ if profile := getattr(by, "profile", None):
112
+ self.last_handler = profile
113
+ apply_adjustment_on_assets_as_task.delay(self.id)
114
+
115
+ @transition(
116
+ field=status,
117
+ source=[Status.PENDING],
118
+ target=Status.DENIED,
119
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(user.profile),
120
+ custom={
121
+ "_transition_button": ActionButton(
122
+ method=RequestType.PATCH,
123
+ identifiers=("wbportfolio:adjustment",),
124
+ icon=WBIcon.DENY.icon,
125
+ key="deny",
126
+ label="Deny",
127
+ action_label="Deny",
128
+ # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
129
+ )
130
+ },
131
+ )
132
+ def deny(self, by=None, description=None, **kwargs):
133
+ if profile := getattr(by, "profile", None):
134
+ self.last_handler = profile
135
+
136
+ @transition(
137
+ field=status,
138
+ source=[Status.APPLIED],
139
+ target=Status.PENDING,
140
+ permission=lambda instance, user: PortfolioRole.is_portfolio_manager(user.profile),
141
+ custom={
142
+ "_transition_button": ActionButton(
143
+ method=RequestType.PATCH,
144
+ identifiers=("wbportfolio:adjustment",),
145
+ icon=WBIcon.UNDO.icon,
146
+ key="revert",
147
+ label="Revert",
148
+ action_label="Revert",
149
+ )
150
+ },
151
+ )
152
+ def revert(self, by=None, description=None, **kwargs):
153
+ if profile := getattr(by, "profile", None):
154
+ self.last_handler = profile
155
+ revert_adjustment_on_assets_as_task.delay(self.id)
156
+
157
+ def import_instrument_prices(self):
158
+ # If the adjusted instrument is not within any portfolio, we automatically approve (and applied) the adjustments
159
+ # We reimport prices when a adjustment comes in, not really the most efficient but at least, we are sure we keep having corrected price
160
+ chain(
161
+ import_prices_as_task.si(self.instrument.id, clear=True),
162
+ post_adjustment_on_prices.si(self.id, automatically_confirm_approve_adjustment_on_assets=True),
163
+ ).apply_async()
164
+
165
+ class Meta:
166
+ verbose_name = "Datum Adjustment"
167
+ verbose_name_plural = "Data Adjustment"
168
+ unique_together = ("date", "instrument")
169
+ indexes = [
170
+ models.Index(
171
+ name="instrument_adjustment_idx",
172
+ fields=["instrument", "-date"],
173
+ )
174
+ ]
175
+
176
+ @classmethod
177
+ def get_endpoint_basename(cls):
178
+ return "wbportfolio:adjustment"
179
+
180
+
181
+ # Shared tasks
182
+
183
+
184
+ @shared_task(queue="portfolio")
185
+ def apply_adjustment_on_assets_as_task(adjustment_id):
186
+ adjustment = Adjustment.objects.get(id=adjustment_id)
187
+ adjustment.apply_adjustment_on_assets()
188
+
189
+
190
+ @shared_task(queue="portfolio")
191
+ def revert_adjustment_on_assets_as_task(adjustment_id):
192
+ adjustment = Adjustment.objects.get(id=adjustment_id)
193
+ adjustment.revert_adjustment_on_assets()
194
+
195
+
196
+ @shared_task(queue="portfolio")
197
+ def post_adjustment_on_prices(adjustment_id, automatically_confirm_approve_adjustment_on_assets: bool | None = False):
198
+ # We just notified the concerned users that a adjustment has been processed, prices have been reimported and as there
199
+ # are assets on the instrument, manually confirmation is needed before apply the adjustment
200
+ adjustment = Adjustment.objects.get(id=adjustment_id)
201
+ if adjustment.instrument.assets.filter(date__lte=adjustment.date).exists():
202
+ if (
203
+ adjustment.automatically_applied_adjustments_on_assets()
204
+ or automatically_confirm_approve_adjustment_on_assets
205
+ ):
206
+ adjustment.apply_adjustment_on_assets()
207
+ adjustment.status = Adjustment.Status.APPLIED
208
+ else:
209
+ for user in User.objects.filter(profile__in=PortfolioRole.portfolio_managers()):
210
+ send_notification(
211
+ code="wbportfolio.adjustment.add",
212
+ title="A new adjustment was imported",
213
+ body=f"A adjustment for {str(adjustment.instrument)} with ex Date {adjustment.date} and factor {adjustment.factor} was imported. Please confirm (to apply) or deny it.",
214
+ user=user,
215
+ reverse_name="wbportfolio:adjustment-detail",
216
+ reverse_args=[adjustment.id],
217
+ )
218
+ else:
219
+ adjustment.status = Adjustment.Status.APPLIED
220
+ adjustment.save()
221
+
222
+
223
+ # Receivers
224
+
225
+
226
+ @receiver(post_delete, sender=Adjustment)
227
+ def post_delete_adjustment(sender, instance, **kwargs):
228
+ for adjustment in Adjustment.objects.filter(instrument=instance.instrument).order_by("date"):
229
+ adjustment.save()
230
+
231
+
232
+ @receiver(post_save, sender="wbportfolio.Adjustment")
233
+ def post_save_adjustment(sender, instance, created, raw, **kwargs):
234
+ if created and (instance.instrument.assets.exists() or instance.instrument.prices.exists()):
235
+ instance.import_instrument_prices()
236
+
237
+
238
+ @receiver(pre_merge, sender="wbfdm.Instrument")
239
+ def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
240
+ """
241
+ Simply reassign the adjustments of the merged instrument to the main instrument if they don't already exist for that day, otherwise simply delete them
242
+ """
243
+ merged_object.pms_adjustments.annotate(
244
+ already_exists=models.Exists(Adjustment.objects.filter(date=models.OuterRef("date"), instrument=main_object))
245
+ ).filter(already_exists=True).delete()
246
+ merged_object.pms_adjustments.update(instrument=main_object)