wbportfolio 1.52.0__py2.py3-none-any.whl → 1.59.4__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 (273) hide show
  1. wbportfolio/admin/__init__.py +3 -1
  2. wbportfolio/admin/indexes.py +1 -1
  3. wbportfolio/admin/orders/__init__.py +2 -0
  4. wbportfolio/admin/orders/order_proposals.py +16 -0
  5. wbportfolio/admin/orders/orders.py +32 -0
  6. wbportfolio/admin/portfolio.py +11 -5
  7. wbportfolio/admin/product_groups.py +1 -1
  8. wbportfolio/admin/products.py +2 -1
  9. wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
  10. wbportfolio/admin/transactions/__init__.py +0 -2
  11. wbportfolio/admin/transactions/dividends.py +40 -4
  12. wbportfolio/admin/transactions/fees.py +24 -14
  13. wbportfolio/admin/transactions/trades.py +34 -27
  14. wbportfolio/analysis/claims.py +5 -6
  15. wbportfolio/api_clients/ubs.py +162 -0
  16. wbportfolio/constants.py +1 -0
  17. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  18. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  19. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  20. wbportfolio/contrib/company_portfolio/models.py +69 -39
  21. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  22. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  23. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  24. wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
  25. wbportfolio/defaults/fees/default.py +7 -15
  26. wbportfolio/factories/__init__.py +2 -2
  27. wbportfolio/factories/assets.py +1 -1
  28. wbportfolio/factories/dividends.py +8 -3
  29. wbportfolio/factories/fees.py +8 -4
  30. wbportfolio/factories/orders/__init__.py +2 -0
  31. wbportfolio/factories/orders/order_proposals.py +21 -0
  32. wbportfolio/factories/orders/orders.py +34 -0
  33. wbportfolio/factories/portfolios.py +2 -1
  34. wbportfolio/factories/product_groups.py +3 -3
  35. wbportfolio/factories/products.py +3 -3
  36. wbportfolio/factories/rebalancing.py +1 -1
  37. wbportfolio/factories/trades.py +12 -16
  38. wbportfolio/filters/assets.py +18 -4
  39. wbportfolio/filters/orders/__init__.py +2 -0
  40. wbportfolio/filters/orders/order_proposals.py +55 -0
  41. wbportfolio/filters/orders/orders.py +11 -0
  42. wbportfolio/filters/portfolios.py +38 -1
  43. wbportfolio/filters/positions.py +0 -1
  44. wbportfolio/filters/transactions/__init__.py +1 -2
  45. wbportfolio/filters/transactions/fees.py +5 -12
  46. wbportfolio/filters/transactions/trades.py +16 -8
  47. wbportfolio/filters/transactions/utils.py +42 -0
  48. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  49. wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
  50. wbportfolio/import_export/backends/ubs/fees.py +10 -20
  51. wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
  52. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  53. wbportfolio/import_export/backends/utils.py +0 -17
  54. wbportfolio/import_export/handlers/asset_position.py +22 -10
  55. wbportfolio/import_export/handlers/dividend.py +8 -8
  56. wbportfolio/import_export/handlers/fees.py +13 -23
  57. wbportfolio/import_export/handlers/orders.py +71 -0
  58. wbportfolio/import_export/handlers/trade.py +53 -77
  59. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  60. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  61. wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
  62. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  63. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  64. wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
  65. wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
  66. wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
  67. wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
  68. wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
  69. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  70. wbportfolio/import_export/parsers/natixis/fees.py +7 -9
  71. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  72. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
  73. wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
  74. wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
  75. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
  76. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  77. wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
  78. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  79. wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
  80. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  81. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  82. wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
  83. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  84. wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
  85. wbportfolio/import_export/parsers/ubs/equity.py +3 -2
  86. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  87. wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
  88. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
  89. wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
  90. wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
  91. wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
  92. wbportfolio/import_export/resources/trades.py +3 -3
  93. wbportfolio/import_export/utils.py +3 -1
  94. wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
  95. wbportfolio/metric/backends/base.py +2 -2
  96. wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
  97. wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
  98. wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
  99. wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
  100. wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
  101. wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
  102. wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
  103. wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
  104. wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
  105. wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
  106. wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
  107. wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
  108. wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
  109. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  110. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  111. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  112. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  113. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  114. wbportfolio/models/__init__.py +2 -0
  115. wbportfolio/models/adjustments.py +1 -1
  116. wbportfolio/models/asset.py +28 -170
  117. wbportfolio/models/builder.py +323 -0
  118. wbportfolio/models/custodians.py +3 -3
  119. wbportfolio/models/exceptions.py +1 -1
  120. wbportfolio/models/graphs/portfolio.py +1 -1
  121. wbportfolio/models/graphs/utils.py +11 -11
  122. wbportfolio/models/mixins/instruments.py +7 -0
  123. wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
  124. wbportfolio/models/orders/__init__.py +2 -0
  125. wbportfolio/models/orders/order_proposals.py +1414 -0
  126. wbportfolio/models/orders/orders.py +410 -0
  127. wbportfolio/models/portfolio.py +311 -289
  128. wbportfolio/models/portfolio_relationship.py +6 -0
  129. wbportfolio/models/products.py +12 -0
  130. wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
  131. wbportfolio/models/roles.py +4 -10
  132. wbportfolio/models/transactions/__init__.py +0 -4
  133. wbportfolio/models/transactions/claim.py +7 -6
  134. wbportfolio/models/transactions/dividends.py +42 -5
  135. wbportfolio/models/transactions/fees.py +55 -22
  136. wbportfolio/models/transactions/trades.py +121 -442
  137. wbportfolio/models/transactions/transactions.py +78 -158
  138. wbportfolio/models/utils.py +100 -1
  139. wbportfolio/order_routing/__init__.py +35 -0
  140. wbportfolio/order_routing/adapters/__init__.py +65 -0
  141. wbportfolio/order_routing/adapters/ubs.py +195 -0
  142. wbportfolio/order_routing/router.py +33 -0
  143. wbportfolio/order_routing/tests/__init__.py +0 -0
  144. wbportfolio/order_routing/tests/test_router.py +110 -0
  145. wbportfolio/permissions.py +7 -0
  146. wbportfolio/pms/analytics/portfolio.py +17 -9
  147. wbportfolio/pms/analytics/utils.py +9 -0
  148. wbportfolio/pms/trading/__init__.py +0 -1
  149. wbportfolio/pms/trading/optimizer.py +61 -0
  150. wbportfolio/pms/typing.py +198 -63
  151. wbportfolio/rebalancing/base.py +12 -1
  152. wbportfolio/rebalancing/decorators.py +1 -1
  153. wbportfolio/rebalancing/models/composite.py +4 -8
  154. wbportfolio/rebalancing/models/equally_weighted.py +13 -11
  155. wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
  156. wbportfolio/rebalancing/models/model_portfolio.py +14 -18
  157. wbportfolio/risk_management/backends/__init__.py +1 -0
  158. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  159. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  160. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  161. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  162. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  163. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  164. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  165. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  166. wbportfolio/serializers/__init__.py +1 -0
  167. wbportfolio/serializers/orders/__init__.py +2 -0
  168. wbportfolio/serializers/orders/order_proposals.py +115 -0
  169. wbportfolio/serializers/orders/orders.py +283 -0
  170. wbportfolio/serializers/portfolios.py +7 -7
  171. wbportfolio/serializers/positions.py +2 -2
  172. wbportfolio/serializers/rebalancing.py +1 -1
  173. wbportfolio/serializers/signals.py +9 -12
  174. wbportfolio/serializers/transactions/__init__.py +1 -10
  175. wbportfolio/serializers/transactions/claim.py +2 -2
  176. wbportfolio/serializers/transactions/dividends.py +37 -9
  177. wbportfolio/serializers/transactions/fees.py +39 -10
  178. wbportfolio/serializers/transactions/trades.py +55 -157
  179. wbportfolio/tasks.py +43 -5
  180. wbportfolio/tests/analysis/__init__.py +0 -0
  181. wbportfolio/tests/analysis/test_claims.py +85 -0
  182. wbportfolio/tests/conftest.py +12 -12
  183. wbportfolio/tests/models/orders/__init__.py +0 -0
  184. wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
  185. wbportfolio/tests/models/test_assets.py +7 -3
  186. wbportfolio/tests/models/test_imports.py +9 -13
  187. wbportfolio/tests/models/test_portfolios.py +102 -95
  188. wbportfolio/tests/models/test_products.py +11 -0
  189. wbportfolio/tests/models/test_splits.py +1 -6
  190. wbportfolio/tests/models/test_utils.py +140 -0
  191. wbportfolio/tests/models/transactions/test_fees.py +7 -13
  192. wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
  193. wbportfolio/tests/models/transactions/test_trades.py +0 -20
  194. wbportfolio/tests/pms/test_analytics.py +22 -3
  195. wbportfolio/tests/rebalancing/test_models.py +51 -57
  196. wbportfolio/tests/signals.py +10 -20
  197. wbportfolio/tests/tests.py +3 -1
  198. wbportfolio/tests/viewsets/test_products.py +1 -0
  199. wbportfolio/urls.py +10 -13
  200. wbportfolio/viewsets/__init__.py +9 -4
  201. wbportfolio/viewsets/assets.py +3 -204
  202. wbportfolio/viewsets/charts/__init__.py +6 -1
  203. wbportfolio/viewsets/charts/assets.py +344 -154
  204. wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
  205. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  206. wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
  207. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  208. wbportfolio/viewsets/configs/buttons/products.py +32 -2
  209. wbportfolio/viewsets/configs/display/__init__.py +2 -5
  210. wbportfolio/viewsets/configs/display/assets.py +6 -19
  211. wbportfolio/viewsets/configs/display/fees.py +3 -3
  212. wbportfolio/viewsets/configs/display/portfolios.py +5 -5
  213. wbportfolio/viewsets/configs/display/products.py +1 -1
  214. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  215. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  216. wbportfolio/viewsets/configs/display/trades.py +1 -189
  217. wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
  218. wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
  219. wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
  220. wbportfolio/viewsets/configs/menu/__init__.py +1 -1
  221. wbportfolio/viewsets/configs/menu/orders.py +11 -0
  222. wbportfolio/viewsets/configs/titles/__init__.py +2 -3
  223. wbportfolio/viewsets/configs/titles/fees.py +4 -8
  224. wbportfolio/viewsets/esg.py +3 -5
  225. wbportfolio/viewsets/mixins.py +5 -1
  226. wbportfolio/viewsets/orders/__init__.py +6 -0
  227. wbportfolio/viewsets/orders/configs/__init__.py +4 -0
  228. wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
  229. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
  230. wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
  231. wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
  232. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
  233. wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
  234. wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
  235. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
  236. wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
  237. wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
  238. wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
  239. wbportfolio/viewsets/orders/order_proposals.py +252 -0
  240. wbportfolio/viewsets/orders/orders.py +277 -0
  241. wbportfolio/viewsets/portfolios.py +36 -12
  242. wbportfolio/viewsets/positions.py +3 -2
  243. wbportfolio/viewsets/products.py +6 -6
  244. wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
  245. wbportfolio/viewsets/transactions/__init__.py +3 -14
  246. wbportfolio/viewsets/transactions/fees.py +22 -22
  247. wbportfolio/viewsets/transactions/trades.py +1 -180
  248. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
  249. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
  250. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  251. wbportfolio/admin/transactions/transactions.py +0 -38
  252. wbportfolio/factories/transactions.py +0 -22
  253. wbportfolio/fdm/tasks.py +0 -13
  254. wbportfolio/filters/transactions/transactions.py +0 -99
  255. wbportfolio/models/transactions/expiry.py +0 -7
  256. wbportfolio/models/transactions/trade_proposals.py +0 -704
  257. wbportfolio/pms/trading/handler.py +0 -161
  258. wbportfolio/serializers/transactions/expiry.py +0 -18
  259. wbportfolio/serializers/transactions/trade_proposals.py +0 -76
  260. wbportfolio/serializers/transactions/transactions.py +0 -85
  261. wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
  262. wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
  263. wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
  264. wbportfolio/viewsets/configs/display/transactions.py +0 -55
  265. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
  266. wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
  267. wbportfolio/viewsets/configs/menu/transactions.py +0 -9
  268. wbportfolio/viewsets/configs/titles/transactions.py +0 -9
  269. wbportfolio/viewsets/signals.py +0 -43
  270. wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
  271. wbportfolio/viewsets/transactions/transactions.py +0 -122
  272. /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
  273. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
@@ -49,7 +49,7 @@ class RebalancerModelSerializer(wb_serializers.ModelSerializer):
49
49
  "computed_str",
50
50
  "_rebalancing_model",
51
51
  "rebalancing_model",
52
- "approve_trade_proposal_automatically",
52
+ "apply_order_proposal_automatically",
53
53
  "activation_date",
54
54
  "frequency",
55
55
  "frequency_repr",
@@ -84,7 +84,7 @@ def transactions_resources(sender, serializer, instance, request, user, **kwargs
84
84
  additional_resources = dict()
85
85
  portfolio = instance.primary_portfolio
86
86
  if portfolio:
87
- if portfolio.transactions.exists() and user.profile.is_internal:
87
+ if portfolio.trades.exists() and user.profile.is_internal:
88
88
  additional_resources["portfolio_subscriptionsredemptions"] = (
89
89
  f'{reverse("wbportfolio:portfolio-trade-list", args=[portfolio.id], request=request)}?is_customer_trade=True'
90
90
  )
@@ -92,17 +92,6 @@ def transactions_resources(sender, serializer, instance, request, user, **kwargs
92
92
  f'{reverse("wbportfolio:portfolio-trade-list", args=[portfolio.id], request=request)}?is_customer_trade=False'
93
93
  )
94
94
 
95
- additional_resources["portfolio_transactions"] = reverse(
96
- "wbportfolio:portfolio-transaction-list", args=[portfolio.id], request=request
97
- )
98
- if PortfolioRole.is_portfolio_manager(user.profile, portfolio=portfolio):
99
- additional_resources["portfolio_fees"] = reverse(
100
- "wbportfolio:portfolio-fees-list", args=[portfolio.id], request=request
101
- )
102
- additional_resources["portfolio_aggregatedfees"] = reverse(
103
- "wbportfolio:portfolio-feesaggregated-list", args=[portfolio.id], request=request
104
- )
105
-
106
95
  return additional_resources
107
96
 
108
97
 
@@ -130,6 +119,14 @@ def product_resources(sender, serializer, instance, request, user, **kwargs):
130
119
  args=[instance.id],
131
120
  request=request,
132
121
  )
122
+ if instance.fees.exists() and PortfolioRole.is_portfolio_manager(user.profile, instrument=instance):
123
+ additional_resources["product_fees"] = reverse(
124
+ "wbportfolio:product-fees-list", args=[instance.id], request=request
125
+ )
126
+ additional_resources["product_aggregatedfees"] = reverse(
127
+ "wbportfolio:product-feesaggregated-list", args=[instance.id], request=request
128
+ )
129
+
133
130
  return additional_resources
134
131
 
135
132
 
@@ -1,4 +1,3 @@
1
- from .trade_proposals import TradeProposalModelSerializer, ReadOnlyTradeProposalModelSerializer
2
1
  from .claim import (
3
2
  ClaimAccountSerializer,
4
3
  ClaimAPIModelSerializer,
@@ -9,16 +8,8 @@ from .claim import (
9
8
  NegativeTermimalAccountPerProductModelSerializer,
10
9
  )
11
10
  from .dividends import DividendModelSerializer, DividendRepresentationSerializer
12
- from .expiry import ExpiryModelSerializer, ExpiryRepresentationSerializer
13
11
  from .fees import FeesModelSerializer, FeesRepresentationSerializer
14
12
  from .trades import (
15
13
  TradeModelSerializer,
16
- TradeProposalRepresentationSerializer,
17
- TradeRepresentationSerializer,
18
- TradeTradeProposalModelSerializer,
19
- ReadOnlyTradeTradeProposalModelSerializer,
20
- )
21
- from .transactions import (
22
- TransactionModelSerializer,
23
- TransactionRepresentationSerializer,
14
+ TradeRepresentationSerializer
24
15
  )
@@ -49,8 +49,8 @@ class ClaimAPIModelSerializer(serializers.ModelSerializer):
49
49
  if "isin" in request.data:
50
50
  try:
51
51
  data["product"] = Product.objects.get(isin=request.data["isin"])
52
- except Product.DoesNotExist:
53
- raise ValidationError({"isin": "A product with this ISIN does not exist."})
52
+ except Product.DoesNotExist as e:
53
+ raise ValidationError({"isin": "A product with this ISIN does not exist."}) from e
54
54
  if trade := data.get("trade", None):
55
55
  if not trade.is_claimable:
56
56
  raise ValidationError({"trade": "Only a claimable trade can be selected"})
@@ -1,18 +1,46 @@
1
- from wbportfolio.models import DividendTransaction
1
+ from wbcore import serializers as wb_serializers
2
+ from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
3
+ from wbfdm.serializers import InvestableUniverseRepresentationSerializer
2
4
 
3
- from .transactions import (
4
- TransactionModelSerializer,
5
- TransactionRepresentationSerializer,
6
- )
5
+ from wbportfolio.models import DividendTransaction
6
+ from wbportfolio.serializers import PortfolioRepresentationSerializer
7
7
 
8
8
 
9
- class DividendRepresentationSerializer(TransactionRepresentationSerializer):
9
+ class DividendRepresentationSerializer(wb_serializers.RepresentationSerializer):
10
10
  class Meta:
11
11
  model = DividendTransaction
12
- fields = TransactionRepresentationSerializer.Meta.fields
12
+ fields = ("id", "value_date")
13
+
14
+
15
+ class DividendModelSerializer(wb_serializers.ModelSerializer):
16
+ _portfolio = PortfolioRepresentationSerializer(source="portfolio")
17
+ _underlying_instrument = InvestableUniverseRepresentationSerializer(source="underlying_instrument")
18
+ _currency = CurrencyRepresentationSerializer(source="currency")
13
19
 
20
+ total_value = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
21
+ total_value_fx_portfolio = wb_serializers.DecimalField(
22
+ max_digits=14, decimal_places=2, read_only=True, required=False
23
+ )
24
+ total_value_gross = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
25
+ total_value_gross_fx_portfolio = wb_serializers.DecimalField(
26
+ max_digits=14, decimal_places=2, read_only=True, required=False
27
+ )
14
28
 
15
- class DividendModelSerializer(TransactionModelSerializer):
16
29
  class Meta:
17
30
  model = DividendTransaction
18
- fields = TransactionModelSerializer.Meta.fields
31
+ fields = (
32
+ "id",
33
+ "value_date",
34
+ "record_date",
35
+ "ex_date",
36
+ "portfolio",
37
+ "underlying_instrument",
38
+ "currency",
39
+ "_portfolio",
40
+ "_underlying_instrument",
41
+ "_currency",
42
+ "total_value",
43
+ "total_value_fx_portfolio",
44
+ "total_value_gross",
45
+ "total_value_gross_fx_portfolio",
46
+ )
@@ -1,20 +1,28 @@
1
1
  from wbcore import serializers as wb_serializers
2
+ from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
2
3
 
3
4
  from wbportfolio.models import Fees
5
+ from wbportfolio.serializers.products import ProductRepresentationSerializer
4
6
 
5
- from .transactions import (
6
- TransactionModelSerializer,
7
- TransactionRepresentationSerializer,
8
- )
9
7
 
8
+ class FeesRepresentationSerializer(wb_serializers.RepresentationSerializer):
9
+ _detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:fees-detail")
10
10
 
11
- class FeesRepresentationSerializer(TransactionRepresentationSerializer):
12
11
  class Meta:
13
12
  model = Fees
14
- fields = ("transaction_subtype",) + TransactionRepresentationSerializer.Meta.fields
13
+ fields = ("transaction_subtype", "total_value", "_detail")
15
14
 
16
15
 
17
- class FeesModelSerializer(TransactionModelSerializer):
16
+ class FeesModelSerializer(wb_serializers.ModelSerializer):
17
+ _product = ProductRepresentationSerializer(source="product")
18
+ _currency = CurrencyRepresentationSerializer(source="currency")
19
+ total_value_fx_portfolio = wb_serializers.DecimalField(
20
+ max_digits=14, decimal_places=2, read_only=True, required=False
21
+ )
22
+ total_value_gross_fx_portfolio = wb_serializers.DecimalField(
23
+ max_digits=14, decimal_places=2, read_only=True, required=False
24
+ )
25
+
18
26
  @wb_serializers.register_resource()
19
27
  def additional_resources(self, instance, request, user):
20
28
  if (view := request.parser_context.get("view")) and view.is_manager and instance.import_source:
@@ -23,11 +31,32 @@ class FeesModelSerializer(TransactionModelSerializer):
23
31
 
24
32
  class Meta:
25
33
  model = Fees
26
- decorators = TransactionModelSerializer.Meta.decorators
34
+ decorators = {
35
+ "total_value": wb_serializers.decorator(
36
+ decorator_type="text", position="left", value="{{_currency.symbol}}"
37
+ ),
38
+ "total_value_fx_portfolio": wb_serializers.decorator(
39
+ position="left", value="{{_portfolio.currency_symbol}}"
40
+ ),
41
+ "total_value_gross": wb_serializers.decorator(
42
+ decorator_type="text", position="left", value="{{_currency.symbol}}"
43
+ ),
44
+ "total_value_gross_fx_portfolio": wb_serializers.decorator(
45
+ position="left", value="{{_portfolio.currency_symbol}}"
46
+ ),
47
+ }
27
48
  fields = (
49
+ "id",
28
50
  "_additional_resources",
29
51
  "transaction_subtype",
30
52
  "calculated",
31
53
  "fee_date",
32
- "linked_product",
33
- ) + TransactionModelSerializer.Meta.fields
54
+ "product",
55
+ "currency",
56
+ "_product",
57
+ "_currency",
58
+ "total_value",
59
+ "total_value_fx_portfolio",
60
+ "total_value_gross",
61
+ "total_value_gross_fx_portfolio",
62
+ )
@@ -1,23 +1,19 @@
1
1
  from datetime import timedelta
2
2
  from decimal import Decimal
3
3
 
4
- from rest_framework import serializers
5
4
  from rest_framework.reverse import reverse
6
5
  from wbcore import serializers as wb_serializers
7
- from wbcore.metadata.configs.display.list_display import BaseTreeGroupLevelOption
8
- from wbfdm.models import Instrument
9
- from wbfdm.serializers import InvestableInstrumentRepresentationSerializer
10
- from wbfdm.serializers.instruments.instruments import CompanyRepresentationSerializer, SecurityRepresentationSerializer
6
+ from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
7
+ from wbfdm.serializers.instruments.instruments import (
8
+ InvestableUniverseRepresentationSerializer,
9
+ )
11
10
 
12
- from wbportfolio.models import PortfolioRole, Trade, TradeProposal
11
+ from wbportfolio.models import PortfolioRole, Trade
13
12
  from wbportfolio.models.transactions.claim import Claim
14
13
  from wbportfolio.serializers.custodians import CustodianRepresentationSerializer
15
14
  from wbportfolio.serializers.registers import RegisterRepresentationSerializer
16
15
 
17
- from .transactions import (
18
- TransactionModelSerializer,
19
- TransactionRepresentationSerializer,
20
- )
16
+ from .. import PortfolioRepresentationSerializer
21
17
 
22
18
 
23
19
  class CopyClaimRepresentationSerializer(wb_serializers.RepresentationSerializer):
@@ -39,21 +35,23 @@ class CopyClaimRepresentationSerializer(wb_serializers.RepresentationSerializer)
39
35
  )
40
36
 
41
37
 
42
- class TradeProposalRepresentationSerializer(wb_serializers.RepresentationSerializer):
43
- _detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:tradeproposal-detail")
44
-
45
- class Meta:
46
- model = TradeProposal
47
- fields = ("id", "trade_date", "status", "_detail")
48
-
49
-
50
- class TradeRepresentationSerializer(TransactionRepresentationSerializer):
38
+ class TradeRepresentationSerializer(wb_serializers.RepresentationSerializer):
51
39
  _detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:trade-detail")
52
- diff_shares = wb_serializers.DecimalField(max_digits=15, decimal_places=4)
40
+ diff_shares = wb_serializers.DecimalField(max_digits=15, decimal_places=4, read_only=True)
41
+ total_value = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
53
42
 
54
43
  class Meta:
55
44
  model = Trade
56
- fields = ("claimed_shares", "diff_shares", "bank", "shares") + TransactionRepresentationSerializer.Meta.fields
45
+ fields = (
46
+ "id",
47
+ "transaction_date",
48
+ "total_value",
49
+ "_detail",
50
+ "claimed_shares",
51
+ "diff_shares",
52
+ "bank",
53
+ "shares",
54
+ )
57
55
 
58
56
 
59
57
  class TradeClaimRepresentationSerializer(TradeRepresentationSerializer):
@@ -78,7 +76,11 @@ class TradeClaimRepresentationSerializer(TradeRepresentationSerializer):
78
76
  return {}
79
77
 
80
78
 
81
- class TradeModelSerializer(TransactionModelSerializer):
79
+ class TradeModelSerializer(wb_serializers.ModelSerializer):
80
+ _portfolio = PortfolioRepresentationSerializer(source="portfolio")
81
+ _underlying_instrument = InvestableUniverseRepresentationSerializer(source="underlying_instrument")
82
+ _currency = CurrencyRepresentationSerializer(source="currency")
83
+
82
84
  marked_for_deletion = wb_serializers.BooleanField(required=False, read_only=True)
83
85
  marked_as_internal = wb_serializers.BooleanField()
84
86
 
@@ -91,6 +93,19 @@ class TradeModelSerializer(TransactionModelSerializer):
91
93
  completely_claimed = wb_serializers.BooleanField(required=False, read_only=True, default=Decimal(0.0))
92
94
  completely_claimed_if_approved = wb_serializers.BooleanField(required=False, read_only=True, default=Decimal(0.0))
93
95
 
96
+ total_value = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
97
+ total_value_fx_portfolio = wb_serializers.DecimalField(
98
+ max_digits=14, decimal_places=2, read_only=True, required=False
99
+ )
100
+ total_value_gross = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
101
+ total_value_gross_fx_portfolio = wb_serializers.DecimalField(
102
+ max_digits=14, decimal_places=2, read_only=True, required=False
103
+ )
104
+ external_id = wb_serializers.CharField(required=False, read_only=True)
105
+ value_date = wb_serializers.DateField(required=False, read_only=True)
106
+ total_value_usd = wb_serializers.FloatField(default=0, read_only=True, label="Total Value ($)")
107
+ total_value_gross_usd = wb_serializers.FloatField(default=0, read_only=True, label="Total Value Gross ($)")
108
+
94
109
  # We commented it because people needs to filter with lookup equals onto these fields
95
110
  # shares = wb_serializers.DecimalField(max_digits=16, decimal_places=2)
96
111
  # price = wb_serializers.DecimalField(max_digits=16, decimal_places=2)
@@ -127,7 +142,24 @@ class TradeModelSerializer(TransactionModelSerializer):
127
142
 
128
143
  class Meta:
129
144
  model = Trade
130
- decorators = TransactionModelSerializer.Meta.decorators
145
+ decorators = {
146
+ "total_value": wb_serializers.decorator(
147
+ decorator_type="text", position="left", value="{{_currency.symbol}}"
148
+ ),
149
+ "total_value_gross": wb_serializers.decorator(
150
+ decorator_type="text", position="left", value="{{_currency.symbol}}"
151
+ ),
152
+ "total_value_usd": wb_serializers.decorator(decorator_type="text", position="left", value="{{$}}"),
153
+ "total_value_gross_usd": wb_serializers.decorator(decorator_type="text", position="left", value="{{$}}"),
154
+ "total_value_fx_portfolio": wb_serializers.decorator(
155
+ position="left", value="{{_portfolio.currency_symbol}}"
156
+ ),
157
+ "total_value_gross_fx_portfolio": wb_serializers.decorator(
158
+ position="left", value="{{_portfolio.currency_symbol}}"
159
+ ),
160
+ # "total_value_fx_portfolio": wb_serializers.decorator(decorator_type="text", position="left", value="{{_portfolio.currency_symbol}}}"),
161
+ # "total_value_gross_fx_portfolio": wb_serializers.decorator(decorator_type="text", position="left", value="{{_portfolio.currency_symbol}}"),
162
+ }
131
163
  read_only_fields = (
132
164
  "id",
133
165
  "shares",
@@ -145,9 +177,6 @@ class TradeModelSerializer(TransactionModelSerializer):
145
177
  "marked_for_deletion",
146
178
  "_claims",
147
179
  "claims",
148
- "transaction_type",
149
- "transaction_url_type",
150
- "transaction_underlying_type",
151
180
  "portfolio",
152
181
  "_portfolio",
153
182
  "underlying_instrument",
@@ -185,9 +214,6 @@ class TradeModelSerializer(TransactionModelSerializer):
185
214
  "marked_for_deletion",
186
215
  "_claims",
187
216
  "claims",
188
- "transaction_type",
189
- "transaction_url_type",
190
- "transaction_underlying_type",
191
217
  "portfolio",
192
218
  "_portfolio",
193
219
  "underlying_instrument",
@@ -213,131 +239,3 @@ class TradeModelSerializer(TransactionModelSerializer):
213
239
  "_internal_trade",
214
240
  "_additional_resources",
215
241
  )
216
-
217
-
218
- class GetSecurityDefault:
219
- requires_context = True
220
-
221
- def __call__(self, serializer_instance):
222
- try:
223
- instance = serializer_instance.view.get_object()
224
- return instance.underlying_instrument.parent or instance.underlying_instrument
225
- except Exception:
226
- return None
227
-
228
-
229
- class GetCompanyDefault:
230
- requires_context = True
231
-
232
- def __call__(self, serializer_instance):
233
- try:
234
- instance = serializer_instance.view.get_object()
235
- security = instance.underlying_instrument.parent or instance.underlying_instrument
236
- return security.parent or security
237
- except Exception:
238
- return None
239
-
240
-
241
- class TradeTradeProposalModelSerializer(TradeModelSerializer):
242
- underlying_instrument_isin = wb_serializers.CharField(read_only=True)
243
- underlying_instrument_ticker = wb_serializers.CharField(read_only=True)
244
- underlying_instrument_refinitiv_identifier_code = wb_serializers.CharField(read_only=True)
245
- underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
246
-
247
- company = wb_serializers.PrimaryKeyRelatedField(
248
- queryset=Instrument.objects.filter(level=0),
249
- required=False,
250
- read_only=lambda view: not view.new_mode,
251
- default=GetCompanyDefault(),
252
- )
253
- _company = CompanyRepresentationSerializer(source="company", required=False)
254
-
255
- security = wb_serializers.PrimaryKeyRelatedField(
256
- queryset=Instrument.objects.filter(is_security=True),
257
- required=False,
258
- read_only=lambda view: not view.new_mode,
259
- default=GetSecurityDefault(),
260
- )
261
- _security = SecurityRepresentationSerializer(
262
- source="security",
263
- optional_get_parameters={"company": "parent"},
264
- depends_on=[{"field": "company", "options": {}}],
265
- required=False,
266
- )
267
- underlying_instrument = wb_serializers.PrimaryKeyRelatedField(
268
- queryset=Instrument.objects.all(), label="Quote", read_only=lambda view: not view.new_mode
269
- )
270
- _underlying_instrument = InvestableInstrumentRepresentationSerializer(
271
- source="underlying_instrument",
272
- optional_get_parameters={"security": "parent"},
273
- depends_on=[{"field": "security", "options": {}}],
274
- tree_config=BaseTreeGroupLevelOption(clear_filter=True, filter_key="parent"),
275
- )
276
-
277
- status = wb_serializers.ChoiceField(default=Trade.Status.DRAFT, choices=Trade.Status.choices)
278
- target_weight = wb_serializers.DecimalField(max_digits=16, decimal_places=6, required=False, default=0)
279
- effective_weight = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
280
- effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
281
- target_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
282
-
283
- def validate(self, data):
284
- data.pop("company", None)
285
- data.pop("security", None)
286
- if self.instance and "underlying_instrument" in data:
287
- raise serializers.ValidationError(
288
- {
289
- "underlying_instrument": "You cannot modify the underlying instrument other than creating a new entry"
290
- }
291
- )
292
- effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
293
- weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
294
- if (target_weight := data.pop("target_weight", None)) is not None:
295
- weighting = target_weight - effective_weight
296
- if weighting >= 0:
297
- data["transaction_subtype"] = "BUY"
298
- else:
299
- data["transaction_subtype"] = "SELL"
300
- data["weighting"] = weighting
301
- return super().validate(data)
302
-
303
- class Meta:
304
- model = Trade
305
- percent_fields = ["effective_weight", "target_weight", "weighting"]
306
- read_only_fields = (
307
- "transaction_subtype",
308
- "shares",
309
- # "underlying_instrument",
310
- # "_underlying_instrument",
311
- "shares",
312
- "effective_shares",
313
- "target_shares",
314
- )
315
- fields = (
316
- "id",
317
- "shares",
318
- "underlying_instrument_isin",
319
- "underlying_instrument_ticker",
320
- "underlying_instrument_refinitiv_identifier_code",
321
- "underlying_instrument_instrument_type",
322
- "company",
323
- "_company",
324
- "security",
325
- "_security",
326
- "underlying_instrument",
327
- "_underlying_instrument",
328
- "transaction_subtype",
329
- "status",
330
- "comment",
331
- "effective_weight",
332
- "target_weight",
333
- "weighting",
334
- "trade_proposal",
335
- "order",
336
- "effective_shares",
337
- "target_shares",
338
- )
339
-
340
-
341
- class ReadOnlyTradeTradeProposalModelSerializer(TradeTradeProposalModelSerializer):
342
- class Meta(TradeTradeProposalModelSerializer.Meta):
343
- read_only_fields = TradeTradeProposalModelSerializer.Meta.fields
wbportfolio/tasks.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from contextlib import suppress
2
2
  from datetime import date, timedelta
3
3
 
4
+ from celery import shared_task
4
5
  from django.db.models import ProtectedError, Q
6
+ from tqdm import tqdm
7
+ from wbfdm.models import Controversy, Instrument
5
8
 
6
- from wbportfolio.models import Portfolio, Trade
7
- from wbportfolio.models.products import Product, update_outstanding_shares_as_task
8
-
9
- from .fdm.tasks import * # noqa
9
+ from wbportfolio.models import AssetPosition, Portfolio, Product, Trade
10
10
 
11
11
 
12
12
  @shared_task(queue="portfolio")
@@ -15,7 +15,7 @@ def daily_active_product_task(today: date | None = None):
15
15
  today = date.today()
16
16
  qs = Product.active_objects.all()
17
17
  for product in tqdm(qs, total=qs.count()):
18
- update_outstanding_shares_as_task(product.id)
18
+ product.update_outstanding_shares()
19
19
  product.check_and_notify_product_termination_on_date(today)
20
20
 
21
21
 
@@ -49,3 +49,41 @@ def update_preferred_classification_per_instrument_and_portfolio_as_task():
49
49
  # - propagate (or update) t-2 asset positions into t-1
50
50
  # - Synchronize wbportfolio at t-1
51
51
  # - Compute Instrument Price estimate at t-1
52
+
53
+
54
+ @shared_task(queue="portfolio")
55
+ def synchronize_portfolio_controversies():
56
+ active_portfolios = Portfolio.objects.filter_active_and_tracked()
57
+ qs = (
58
+ AssetPosition.objects.filter(portfolio__in=active_portfolios)
59
+ .values("underlying_instrument")
60
+ .distinct("underlying_instrument")
61
+ )
62
+ objs = {}
63
+ securities = Instrument.objects.filter(id__in=qs.values("underlying_instrument"))
64
+ securities_mapping = {security.id: security.get_root() for security in securities}
65
+ for controversy in securities.dl.esg_controversies():
66
+ instrument = securities_mapping[controversy["instrument_id"]]
67
+ obj = Controversy.dict_to_model(controversy, instrument)
68
+ objs[obj.external_id] = obj
69
+
70
+ Controversy.objects.bulk_create(
71
+ objs.values(),
72
+ update_fields=[
73
+ "instrument",
74
+ "headline",
75
+ "description",
76
+ "source",
77
+ "direct_involvement",
78
+ "company_response",
79
+ "review",
80
+ "initiated",
81
+ "flag",
82
+ "status",
83
+ "type",
84
+ "severity",
85
+ ],
86
+ unique_fields=["external_id"],
87
+ update_conflicts=True,
88
+ batch_size=10000,
89
+ )
File without changes
@@ -0,0 +1,85 @@
1
+ from datetime import date, timedelta
2
+
3
+ import pytest
4
+ from pandas._libs.tslibs.offsets import BDay
5
+
6
+ from wbportfolio.analysis.claims import ConsolidatedTradeSummary
7
+ from wbportfolio.models import Claim
8
+
9
+
10
+ @pytest.mark.django_db
11
+ class TestConsolidatedTradeSummary:
12
+ def test_base(self, claim_factory, customer_trade_factory, product, instrument_price_factory):
13
+ d1 = date(2024, 1, 1)
14
+ d2 = date(2025, 1, 1)
15
+ p1 = instrument_price_factory.create(date=d1, instrument=product) # noqa
16
+ p2 = instrument_price_factory.create(date=d2, instrument=product)
17
+ t1 = customer_trade_factory.create(
18
+ shares=1000, transaction_date=d1, underlying_instrument=product, portfolio=product.primary_portfolio
19
+ )
20
+ t2 = customer_trade_factory.create(
21
+ shares=500, transaction_date=d2, underlying_instrument=product, portfolio=product.primary_portfolio
22
+ )
23
+
24
+ c0 = claim_factory.create() # noqa
25
+ c1 = claim_factory.create(shares=1000, status="APPROVED", trade=t1, date=d2, product=product)
26
+ c2 = claim_factory.create(shares=500, status="APPROVED", trade=t2, date=d1, product=product)
27
+ cts = ConsolidatedTradeSummary(Claim.objects.all(), d1, d2, "account", "account__title")
28
+
29
+ valid_date = dict(cts.queryset.values_list("id", "valid_date"))
30
+ assert valid_date == {c1.id: d2, c2.id: d2}
31
+
32
+ date_considered = dict(cts.queryset.values_list("id", "date_considered"))
33
+ assert date_considered == {c1.id: d2 + timedelta(days=1), c2.id: d2 + timedelta(days=1)}
34
+
35
+ aum = dict(cts.queryset.values_list("id", "aum"))
36
+ assert aum == {c1.id: p2.net_value * c1.shares, c2.id: p2.net_value * c2.shares}
37
+
38
+ def test_get_aum_df(self, claim_factory, product, instrument_price_factory, customer_trade_factory):
39
+ t1 = customer_trade_factory.create(
40
+ shares=10,
41
+ transaction_date=date(2024, 12, 31),
42
+ underlying_instrument=product,
43
+ portfolio=product.primary_portfolio,
44
+ )
45
+ t2 = customer_trade_factory.create(
46
+ shares=100,
47
+ transaction_date=date(2025, 2, 3),
48
+ underlying_instrument=product,
49
+ portfolio=product.primary_portfolio,
50
+ )
51
+ c1 = claim_factory.create(trade=t1, status="APPROVED")
52
+ c2 = claim_factory.create(trade=t2, status="APPROVED", account=c1.account)
53
+ d1 = date(2025, 1, 1)
54
+ d2 = date(2025, 3, 3)
55
+ product.prices.all().delete()
56
+ p1 = instrument_price_factory.create(
57
+ date=(d1 - BDay(7)).date(), instrument=product
58
+ ) # we test that the CTS uses the earliest date
59
+ p2 = instrument_price_factory.create(date=c2.date, instrument=product) # noqa
60
+ p3 = instrument_price_factory.create(date=d2, instrument=product)
61
+
62
+ cts = ConsolidatedTradeSummary(Claim.objects.all(), d1, d2, "account", "account__title")
63
+ aum_start = float(round(p1.net_value * c1.shares))
64
+ aum_end = float(round(p3.net_value * (c1.shares + c2.shares)))
65
+ assert cts.get_aum_df().to_dict(orient="index") == {
66
+ c1.account.id: {
67
+ "sum_shares_start": c1.shares,
68
+ "sum_shares_end": c1.shares + c2.shares,
69
+ "sum_aum_start": aum_start,
70
+ "sum_aum_end": aum_end,
71
+ "sum_shares_diff": c2.shares,
72
+ "sum_shares_perf": (c1.shares + c2.shares) / c1.shares - 1,
73
+ "sum_aum_diff": aum_end - aum_start,
74
+ "sum_aum_perf": aum_end / aum_start - 1,
75
+ }
76
+ }
77
+
78
+ p0 = instrument_price_factory.create(date=c1.date, instrument=product) # noqa
79
+
80
+ assert cts.get_nnm_df().to_dict(orient="index") == {
81
+ c1.account.id: {
82
+ "sum_nnm_2025-02": float(round(c2.shares * p2.net_value)),
83
+ "sum_nnm_total": float(round(c2.shares * p2.net_value)),
84
+ }
85
+ }, "Ensure the first claim that is considered in t+1 (on the CTS lower bound range) is excluded from the NNM"