wbportfolio 1.55.8__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 (128) hide show
  1. wbportfolio/admin/orders/order_proposals.py +2 -0
  2. wbportfolio/admin/orders/orders.py +2 -0
  3. wbportfolio/admin/portfolio.py +11 -5
  4. wbportfolio/api_clients/ubs.py +23 -11
  5. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  6. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  7. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  8. wbportfolio/contrib/company_portfolio/models.py +69 -39
  9. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  10. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  11. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  12. wbportfolio/factories/assets.py +1 -1
  13. wbportfolio/factories/orders/order_proposals.py +3 -1
  14. wbportfolio/factories/orders/orders.py +8 -3
  15. wbportfolio/factories/product_groups.py +3 -3
  16. wbportfolio/factories/products.py +3 -3
  17. wbportfolio/filters/assets.py +0 -1
  18. wbportfolio/filters/orders/order_proposals.py +3 -6
  19. wbportfolio/filters/portfolios.py +18 -1
  20. wbportfolio/filters/positions.py +0 -1
  21. wbportfolio/filters/transactions/fees.py +0 -2
  22. wbportfolio/filters/transactions/trades.py +0 -1
  23. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  24. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  25. wbportfolio/import_export/handlers/asset_position.py +9 -5
  26. wbportfolio/import_export/handlers/dividend.py +1 -1
  27. wbportfolio/import_export/handlers/fees.py +2 -2
  28. wbportfolio/import_export/handlers/trade.py +4 -4
  29. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  30. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  31. wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
  32. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  33. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  34. wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
  35. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  36. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  37. wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
  38. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  39. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  40. wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
  41. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  42. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  43. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  44. wbportfolio/import_export/parsers/ubs/equity.py +2 -1
  45. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  46. wbportfolio/import_export/resources/trades.py +1 -1
  47. wbportfolio/import_export/utils.py +3 -1
  48. wbportfolio/metric/backends/base.py +2 -2
  49. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  50. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  51. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  52. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  53. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  54. wbportfolio/models/adjustments.py +1 -1
  55. wbportfolio/models/asset.py +7 -3
  56. wbportfolio/models/builder.py +25 -5
  57. wbportfolio/models/custodians.py +3 -3
  58. wbportfolio/models/exceptions.py +1 -1
  59. wbportfolio/models/graphs/portfolio.py +1 -1
  60. wbportfolio/models/graphs/utils.py +11 -11
  61. wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
  62. wbportfolio/models/orders/order_proposals.py +620 -490
  63. wbportfolio/models/orders/orders.py +237 -75
  64. wbportfolio/models/portfolio.py +79 -18
  65. wbportfolio/models/portfolio_relationship.py +6 -0
  66. wbportfolio/models/products.py +3 -0
  67. wbportfolio/models/rebalancing.py +4 -1
  68. wbportfolio/models/roles.py +4 -10
  69. wbportfolio/models/transactions/claim.py +6 -5
  70. wbportfolio/models/transactions/dividends.py +1 -0
  71. wbportfolio/models/transactions/trades.py +4 -0
  72. wbportfolio/models/transactions/transactions.py +16 -4
  73. wbportfolio/models/utils.py +100 -1
  74. wbportfolio/order_routing/__init__.py +16 -0
  75. wbportfolio/order_routing/adapters/__init__.py +14 -6
  76. wbportfolio/order_routing/adapters/ubs.py +104 -70
  77. wbportfolio/order_routing/router.py +33 -0
  78. wbportfolio/order_routing/tests/test_router.py +110 -0
  79. wbportfolio/permissions.py +7 -0
  80. wbportfolio/pms/trading/__init__.py +0 -1
  81. wbportfolio/pms/trading/optimizer.py +61 -0
  82. wbportfolio/pms/typing.py +115 -103
  83. wbportfolio/rebalancing/models/composite.py +1 -1
  84. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
  85. wbportfolio/risk_management/backends/__init__.py +1 -0
  86. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  87. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  88. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  89. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  90. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  91. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  92. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  93. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  94. wbportfolio/serializers/orders/order_proposals.py +6 -2
  95. wbportfolio/serializers/orders/orders.py +119 -26
  96. wbportfolio/serializers/transactions/claim.py +2 -2
  97. wbportfolio/tasks.py +42 -4
  98. wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
  99. wbportfolio/tests/models/test_portfolios.py +9 -9
  100. wbportfolio/tests/models/test_splits.py +1 -6
  101. wbportfolio/tests/models/test_utils.py +140 -0
  102. wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
  103. wbportfolio/tests/rebalancing/test_models.py +2 -2
  104. wbportfolio/tests/viewsets/test_products.py +1 -0
  105. wbportfolio/urls.py +1 -1
  106. wbportfolio/viewsets/charts/assets.py +8 -4
  107. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  108. wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
  109. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  110. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  111. wbportfolio/viewsets/esg.py +3 -5
  112. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
  113. wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
  114. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
  115. wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
  116. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
  117. wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
  118. wbportfolio/viewsets/orders/order_proposals.py +92 -21
  119. wbportfolio/viewsets/orders/orders.py +79 -26
  120. wbportfolio/viewsets/portfolios.py +24 -0
  121. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
  122. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
  123. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  124. wbportfolio/fdm/tasks.py +0 -42
  125. wbportfolio/models/orders/routing.py +0 -54
  126. wbportfolio/pms/trading/handler.py +0 -211
  127. /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
  128. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
@@ -12,3 +12,5 @@ class OrderProposalAdmin(admin.ModelAdmin):
12
12
  list_display = ("portfolio", "rebalancing_model", "trade_date", "status")
13
13
  autocomplete_fields = ["portfolio", "rebalancing_model"]
14
14
  inlines = [OrderTabularInline]
15
+
16
+ raw_id_fields = ["portfolio", "creator", "approver"]
@@ -28,3 +28,5 @@ class OrderTabularInline(admin.TabularInline):
28
28
  "shares",
29
29
  "daily_return",
30
30
  ]
31
+
32
+ raw_id_fields = ["underlying_instrument"]
@@ -10,10 +10,6 @@ from wbportfolio.models import (
10
10
  PortfolioSwingPricing,
11
11
  )
12
12
 
13
- from .portfolio_relationships import (
14
- PortfolioInstrumentPreferredClassificationThroughInlineModelAdmin,
15
- )
16
-
17
13
 
18
14
  @admin.register(PortfolioBankAccountThroughModel)
19
15
  class PortfolioBankAccountThroughModelModelAdmin(admin.ModelAdmin):
@@ -64,6 +60,16 @@ class PortfolioModelAdmin(admin.ModelAdmin):
64
60
  )
65
61
  },
66
62
  ),
63
+ (
64
+ "OMS",
65
+ {
66
+ "fields": (
67
+ "default_order_proposal_min_order_value",
68
+ "default_order_proposal_min_weighting",
69
+ "default_order_proposal_total_cash_weight",
70
+ )
71
+ },
72
+ ),
67
73
  )
68
74
  readonly_fields = ["updated_at"]
69
75
  list_display = (
@@ -78,7 +84,7 @@ class PortfolioModelAdmin(admin.ModelAdmin):
78
84
  inlines = [
79
85
  PortfolioBankAccountThroughModelAdmin,
80
86
  InstrumentPortfolioThroughModelAdmin,
81
- PortfolioInstrumentPreferredClassificationThroughInlineModelAdmin,
87
+ # PortfolioInstrumentPreferredClassificationThroughInlineModelAdmin,
82
88
  PortfolioPortfolioThroughModelInlineAdmin,
83
89
  ]
84
90
  raw_id_fields = [
@@ -16,7 +16,6 @@ class UBSNeoAPIClient:
16
16
  self,
17
17
  initial_jwt_token: str,
18
18
  jwt_token_expiry_timestamp: datetime | None = None,
19
- default_execution_instruction: str = "MARKET_ON_CLOSE",
20
19
  ):
21
20
  """
22
21
  :param service_account_id: Identifier for your UBS service account (for reference).
@@ -27,7 +26,6 @@ class UBSNeoAPIClient:
27
26
  self.jwt_token_expiry = (
28
27
  jwt_token_expiry_timestamp if jwt_token_expiry_timestamp else timezone.now() + timedelta(days=1)
29
28
  )
30
- self.default_execution_instruction = default_execution_instruction
31
29
 
32
30
  def _is_token_expired(self) -> bool:
33
31
  """Check if the current token is expired or near expiry (e.g. within 5 minutes)."""
@@ -62,21 +60,21 @@ class UBSNeoAPIClient:
62
60
  def _raise_for_status(self, response):
63
61
  try:
64
62
  response.raise_for_status()
65
- except HTTPError:
63
+ except HTTPError as e:
66
64
  json_response = response.json()
67
- raise HTTPError(json_response.get("errors", json_response.get("message")))
65
+ raise HTTPError(json_response.get("errors", json_response.get("message"))) from e
68
66
 
69
67
  def get_rebalance_service_status(self) -> dict:
70
68
  """Check API connection status."""
71
69
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/status"
72
- response = requests.get(url, headers=self._get_headers())
70
+ response = requests.get(url, headers=self._get_headers(), timeout=10)
73
71
  self._raise_for_status(response)
74
72
  return self._get_json(response)
75
73
 
76
74
  def get_rebalance_status_for_isin(self, isin: str) -> dict:
77
75
  """Check certificate accessibility and workflow status for given ISIN."""
78
76
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/status/{isin}"
79
- response = requests.get(url, headers=self._get_headers())
77
+ response = requests.get(url, headers=self._get_headers(), timeout=10)
80
78
  self._raise_for_status(response)
81
79
  return self._get_json(response)
82
80
 
@@ -90,7 +88,7 @@ class UBSNeoAPIClient:
90
88
  isin = self._validate_isin(isin, test=test)
91
89
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/submit/{isin}"
92
90
  payload = {"items": items}
93
- response = requests.post(url, json=payload, headers=self._get_headers())
91
+ response = requests.post(url, json=payload, headers=self._get_headers(), timeout=60)
94
92
  self._raise_for_status(response)
95
93
  return self._get_json(response)
96
94
 
@@ -99,7 +97,7 @@ class UBSNeoAPIClient:
99
97
  isin = self._validate_isin(isin)
100
98
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/savedraft/{isin}"
101
99
  payload = {"items": items}
102
- response = requests.post(url, json=payload, headers=self._get_headers())
100
+ response = requests.post(url, json=payload, headers=self._get_headers(), timeout=60)
103
101
  self._raise_for_status(response)
104
102
  return self._get_json(response)
105
103
 
@@ -107,20 +105,32 @@ class UBSNeoAPIClient:
107
105
  """Cancel a rebalance request."""
108
106
  isin = self._validate_isin(isin)
109
107
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/cancel/{isin}"
110
- response = requests.delete(url, headers=self._get_headers())
108
+ response = requests.delete(url, headers=self._get_headers(), timeout=60)
111
109
  self._raise_for_status(response)
112
110
  return self._get_json(response)
113
111
 
114
112
  def get_current_rebalance_request(self, isin: str) -> dict:
115
113
  """Fetch the current rebalance request for a certificate."""
116
114
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/currentRebalanceRequest/{isin}"
117
- response = requests.get(url, headers=self._get_headers())
115
+ response = requests.get(url, headers=self._get_headers(), timeout=60)
116
+ self._raise_for_status(response)
117
+ return self._get_json(response)
118
+
119
+ def get_rebalance_reports(self, isin: str, from_date: date, to_date: date) -> dict:
120
+ """Fetch the current rebalance request for a certificate."""
121
+ url = f"{self.BASE_URL}/ged-amc/external/report/v1/rebalance/{isin}"
122
+ response = requests.get(
123
+ url,
124
+ headers=self._get_headers(),
125
+ timeout=60,
126
+ params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
127
+ )
118
128
  self._raise_for_status(response)
119
129
  return self._get_json(response)
120
130
 
121
131
  def get_portfolio_at_date(self, isin: str, val_date: date) -> dict:
122
132
  url = f"https://neo.ubs.com/api/ged-amc/external/report/v1/valuation/{isin}/{val_date:%Y-%m-%d}"
123
- response = requests.get(url, headers=self._get_headers())
133
+ response = requests.get(url, headers=self._get_headers(), timeout=10)
124
134
  self._raise_for_status(response)
125
135
  return self._get_json(response)
126
136
 
@@ -130,6 +140,7 @@ class UBSNeoAPIClient:
130
140
  url,
131
141
  headers=self._get_headers(),
132
142
  params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
143
+ timeout=30,
133
144
  )
134
145
  self._raise_for_status(response)
135
146
  return self._get_json(response)
@@ -140,6 +151,7 @@ class UBSNeoAPIClient:
140
151
  url,
141
152
  headers=self._get_headers(),
142
153
  params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
154
+ timeout=30,
143
155
  )
144
156
  self._raise_for_status(response)
145
157
  return self._get_json(response)
@@ -1,8 +1,8 @@
1
1
  from typing import Optional
2
2
 
3
3
  from django.utils.translation import gettext as _
4
- from wbcore.contrib.directory.viewsets.display.entries import CompanyModelDisplay as CMD
5
- from wbcore.contrib.directory.viewsets.display.entries import PersonModelDisplay as PMD
4
+ from wbcore.contrib.directory.viewsets.display.entries import CompanyModelDisplay as BaseCompanyModelDisplay
5
+ from wbcore.contrib.directory.viewsets.display.entries import PersonModelDisplay as BasePersonModelDisplay
6
6
  from wbcore.metadata.configs import display as dp
7
7
  from wbcore.metadata.configs.display.instance_display import (
8
8
  Inline,
@@ -62,14 +62,20 @@ AUM_FIELDS = Section(
62
62
  )
63
63
 
64
64
 
65
- class CompanyModelDisplay(CMD):
65
+ class CompanyModelDisplay(BaseCompanyModelDisplay):
66
66
  def get_list_display(self) -> Optional[dp.ListDisplay]:
67
67
  list_display = super().get_list_display()
68
68
  list_display.fields = (
69
69
  *list_display.fields[:5],
70
- dp.Field(key="invested_assets_under_management_usd", label="AUM Invested"),
71
- dp.Field(key="asset_under_management", label="AUM"),
72
- dp.Field(key="potential", label="Potential"),
70
+ dp.Field(
71
+ key=None,
72
+ label=_("AUM"),
73
+ children=[
74
+ dp.Field(key="invested_assets_under_management_usd", label="AUM Invested", width=100),
75
+ dp.Field(key="asset_under_management", label="AUM", width=100),
76
+ dp.Field(key="potential", label="Potential", width=100),
77
+ ],
78
+ ),
73
79
  *list_display.fields[5:],
74
80
  )
75
81
  return list_display
@@ -134,15 +140,21 @@ class CompanyModelDisplay(CMD):
134
140
  return instance_display
135
141
 
136
142
 
137
- class PersonModelDisplay(PMD):
143
+ class PersonModelDisplay(BasePersonModelDisplay):
138
144
  def get_list_display(self) -> Optional[dp.ListDisplay]:
139
145
  list_display = super().get_list_display()
140
146
 
141
147
  list_display.fields = (
142
148
  *list_display.fields[:7],
143
- dp.Field(key="invested_assets_under_management_usd", label="AUM Invested"),
144
- dp.Field(key="asset_under_management", label="AUM"),
145
- dp.Field(key="potential", label="Potential"),
149
+ dp.Field(
150
+ key=None,
151
+ label=_("AUM"),
152
+ children=[
153
+ dp.Field(key="invested_assets_under_management_usd", label="AUM Invested", width=100),
154
+ dp.Field(key="asset_under_management", label="AUM", width=100),
155
+ dp.Field(key="potential", label="Potential", width=100),
156
+ ],
157
+ ),
146
158
  *list_display.fields[7:],
147
159
  )
148
160
  return list_display
@@ -1,3 +1,5 @@
1
+ from contextlib import suppress
2
+
1
3
  from wbcore.contrib.directory.viewsets.previews import EntryPreviewConfig
2
4
  from wbcore.contrib.icons import WBIcon
3
5
  from wbcore.metadata.configs import buttons as bt
@@ -17,12 +19,10 @@ class CompanyPreviewConfig(EntryPreviewConfig):
17
19
  ["asset_under_management", "invested_assets_under_management_usd"],
18
20
  [repeat_field(2, "potential")],
19
21
  ]
20
- try:
22
+ with suppress(Exception):
21
23
  entry = self.view.get_object()
22
24
  if entry.profile_image:
23
25
  fields.insert(0, [repeat_field(2, "profile_image")])
24
- except Exception:
25
- pass
26
26
 
27
27
  return create_simple_display(fields)
28
28
 
@@ -1,6 +1,6 @@
1
1
  from wbcore import filters
2
- from wbcore.contrib.directory.filters import CompanyFilter as CF
3
- from wbcore.contrib.directory.filters import PersonFilter as PF
2
+ from wbcore.contrib.directory.filters import CompanyFilter as BaseCompanyFilter
3
+ from wbcore.contrib.directory.filters import PersonFilter as BasePersonFilter
4
4
  from wbcore.contrib.directory.models import Company, Person
5
5
 
6
6
 
@@ -99,29 +99,29 @@ class EntryPortfolioFilter(filters.FilterSet):
99
99
  return queryset
100
100
 
101
101
 
102
- class CompanyFilter(CF, EntryPortfolioFilter):
102
+ class CompanyFilter(BaseCompanyFilter, EntryPortfolioFilter):
103
103
  @classmethod
104
104
  def get_filter_class_for_remote_filter(cls):
105
105
  """
106
106
  Define which filterset class sender to user for remote filter registration
107
107
  """
108
- return CF
108
+ return BaseCompanyFilter
109
109
 
110
- class Meta(CF.Meta):
110
+ class Meta(BaseCompanyFilter.Meta):
111
111
  fields = {
112
- **CF.Meta.fields,
112
+ **BaseCompanyFilter.Meta.fields,
113
113
  }
114
114
 
115
115
 
116
- class PersonFilter(PF, EntryPortfolioFilter):
116
+ class PersonFilter(BasePersonFilter, EntryPortfolioFilter):
117
117
  @classmethod
118
118
  def get_filter_class_for_remote_filter(cls):
119
119
  """
120
120
  Define which filterset class sender to user for remote filter registration
121
121
  """
122
- return PF
122
+ return BasePersonFilter
123
123
 
124
- class Meta(PF.Meta):
124
+ class Meta(BasePersonFilter.Meta):
125
125
  fields = {
126
- **PF.Meta.fields,
126
+ **BasePersonFilter.Meta.fields,
127
127
  }
@@ -3,6 +3,7 @@ from datetime import date
3
3
  from decimal import Decimal
4
4
 
5
5
  from django.conf import settings
6
+ from django.core.cache import cache
6
7
  from django.db import models
7
8
  from django.db.models.signals import post_save
8
9
  from django.dispatch import receiver
@@ -16,7 +17,12 @@ from wbportfolio.models import Claim, Product
16
17
 
17
18
 
18
19
  def get_total_assets_under_management(val_date: date) -> Decimal:
19
- return sum([product.get_total_aum_usd(val_date) for product in Product.active_objects.all()])
20
+ cache_key = f"total_assets_under_management:{val_date.isoformat()}"
21
+ return cache.get_or_set(
22
+ cache_key,
23
+ lambda: sum([product.get_total_aum_usd(val_date) for product in Product.active_objects.all()]),
24
+ 60 * 60 * 24,
25
+ )
20
26
 
21
27
 
22
28
  def get_lost_client_customer_status():
@@ -44,9 +50,8 @@ class Updater:
44
50
  self.val_date = val_date
45
51
  self.total_assets_under_management = get_total_assets_under_management(val_date)
46
52
 
47
- def update_company_data(self, company):
53
+ def update_company_data(self, company_portfolio_data) -> tuple[str, str]:
48
54
  # save company portfolio data
49
- company_portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
50
55
  if (
51
56
  invested_assets_under_management_usd := company_portfolio_data.get_assets_under_management_usd(
52
57
  self.val_date
@@ -55,19 +60,11 @@ class Updater:
55
60
  company_portfolio_data.invested_assets_under_management_usd = invested_assets_under_management_usd
56
61
  if (potential := company_portfolio_data.get_potential(self.val_date)) is not None:
57
62
  company_portfolio_data.potential = potential
58
- company_portfolio_data.save()
59
63
 
60
64
  # update the company object itself
61
- if (tier := company_portfolio_data.get_tiering(self.total_assets_under_management)) is not None:
62
- company.tier = tier
63
- company.customer_status = company_portfolio_data.get_customer_status()
64
- company.save()
65
-
66
- # def update_all_companies(self, val_date: date):
67
- # for company in tqdm(qs, total=qs.count()):
68
- # with suppress(CompanyPortfolioData.DoesNotExist):
69
- # company_portfolio = CompanyPortfolioData.objects.get(company=company)
70
- # company_portfolio.update_data(date.today())
65
+ tier = company_portfolio_data.get_tiering(self.total_assets_under_management)
66
+ customer_status = company_portfolio_data.get_customer_status()
67
+ return customer_status, tier
71
68
 
72
69
 
73
70
  class CompanyPortfolioData(models.Model):
@@ -104,14 +101,6 @@ class CompanyPortfolioData(models.Model):
104
101
  verbose_name="AUM",
105
102
  help_text="The Assets under Management (AUM) that is managed by this company or this person's primary employer.",
106
103
  )
107
- invested_assets_under_management_usd = models.DecimalField(
108
- max_digits=17,
109
- decimal_places=2,
110
- null=True,
111
- blank=True,
112
- help_text="The invested Assets under Management (AUM).",
113
- verbose_name="Invested AUM ($)",
114
- )
115
104
 
116
105
  investment_discretion = models.CharField(
117
106
  max_length=21,
@@ -120,10 +109,6 @@ class CompanyPortfolioData(models.Model):
120
109
  help_text="What discretion this company or this person's primary employer has to invest its assets.",
121
110
  verbose_name="Investment Discretion",
122
111
  )
123
-
124
- potential = models.DecimalField(
125
- decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
126
- )
127
112
  potential_currency = models.ForeignKey(
128
113
  to="currency.Currency",
129
114
  related_name="wbportfolio_potential_currencies",
@@ -132,6 +117,25 @@ class CompanyPortfolioData(models.Model):
132
117
  on_delete=models.PROTECT,
133
118
  )
134
119
 
120
+ # Dynamic fields
121
+ invested_assets_under_management_usd = models.DecimalField(
122
+ max_digits=17,
123
+ decimal_places=2,
124
+ null=True,
125
+ blank=True,
126
+ help_text="The invested Assets under Management (AUM).",
127
+ verbose_name="Invested AUM ($)",
128
+ )
129
+
130
+ potential = models.DecimalField(
131
+ decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
132
+ )
133
+
134
+ def update(self):
135
+ with suppress(CurrencyFXRates.DoesNotExist):
136
+ val_date = CurrencyFXRates.objects.latest("date").date
137
+ self.company.customer_status, self.company.tier = Updater(val_date).update_company_data(self)
138
+
135
139
  def get_assets_under_management_usd(self, val_date: date) -> Decimal:
136
140
  return Claim.objects.filter(status=Claim.Status.APPROVED).filter_for_customer(
137
141
  self.company
@@ -140,16 +144,17 @@ class CompanyPortfolioData(models.Model):
140
144
  )["invested_aum_usd"] or Decimal(0)
141
145
 
142
146
  def _get_default_potential(self, val_date: date) -> Decimal:
143
- with suppress(CurrencyFXRates.DoesNotExist):
144
- fx = CurrencyFXRates.objects.get(currency=self.assets_under_management_currency, date=val_date).value
147
+ if self.assets_under_management:
148
+ with suppress(CurrencyFXRates.DoesNotExist):
149
+ fx = CurrencyFXRates.objects.get(currency=self.assets_under_management_currency, date=val_date).value
145
150
 
146
- aum_usd = self.assets_under_management / fx
147
- aum_potential = Decimal(0)
148
- for asset_allocation in self.company.asset_allocations.all():
149
- aum_potential += aum_usd * asset_allocation.percent * asset_allocation.max_investment
150
- invested_aum = self.invested_assets_under_management_usd or Decimal(0.0)
151
+ aum_usd = self.assets_under_management / fx
152
+ aum_potential = Decimal(0)
153
+ for asset_allocation in self.company.asset_allocations.all():
154
+ aum_potential += aum_usd * asset_allocation.percent * asset_allocation.max_investment
155
+ invested_aum = self.invested_assets_under_management_usd or Decimal(0.0)
151
156
 
152
- return aum_potential - invested_aum
157
+ return aum_potential - invested_aum
153
158
 
154
159
  def get_potential(self, val_date: date) -> Decimal:
155
160
  if module_path := getattr(settings, "PORTFOLIO_COMPANY_DATA_POTENTIAL_METHOD", None):
@@ -225,11 +230,6 @@ class CompanyPortfolioData(models.Model):
225
230
  verbose_name_plural = "Company Portfolio Data"
226
231
 
227
232
 
228
- @receiver(post_save, sender="directory.Company")
229
- def create_company_portfolio_data(sender, instance, created, **kwargs):
230
- CompanyPortfolioData.objects.get_or_create(company=instance)
231
-
232
-
233
233
  class AssetAllocationType(WBModel):
234
234
  name = models.CharField(max_length=255)
235
235
  default_max_investment = models.DecimalField(
@@ -329,3 +329,33 @@ class GeographicFocus(models.Model):
329
329
  @classmethod
330
330
  def get_representation_label_key(cls):
331
331
  return "{{company}}: {{percent}} {{country}}"
332
+
333
+
334
+ @receiver(post_save, sender=AssetAllocation)
335
+ @receiver(post_save, sender=GeographicFocus)
336
+ def post_save_company_data(sender, instance, created, **kwargs):
337
+ company = instance.company
338
+ portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=company)
339
+ if not created:
340
+ portfolio_data.update()
341
+ portfolio_data.save()
342
+ portfolio_data.company.save()
343
+
344
+
345
+ @receiver(post_save, sender="directory.Company")
346
+ def handle_company_portfolio_data(sender, instance, created, **kwargs):
347
+ # create default asset allocation type (equity 50/50)
348
+ if not instance.asset_allocations.exists():
349
+ equity_asset_type = AssetAllocationType.objects.get_or_create(name="Equity")[0]
350
+ AssetAllocation.objects.create(
351
+ company=instance,
352
+ asset_type=equity_asset_type,
353
+ percent=0.5,
354
+ max_investment=0.5,
355
+ )
356
+ if created:
357
+ portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=instance)
358
+ if not created:
359
+ portfolio_data.update()
360
+ portfolio_data.save()
361
+ portfolio_data.company.save()
@@ -1,8 +1,11 @@
1
+ import logging
1
2
  import re
2
3
 
3
4
  from wbcore.contrib.currency.models import Currency
4
5
  from wbcore.contrib.directory.models import Company
5
6
 
7
+ logger = logging.getLogger("pms")
8
+
6
9
 
7
10
  def get_currency_and_assets_under_management(
8
11
  aum_string, currency_mapping, default_currency, multiplier_mapping, default_multiplier
@@ -72,5 +75,7 @@ def assign_aum():
72
75
  portfolio_data.assets_under_management_currency = currency
73
76
  try:
74
77
  portfolio_data.save()
75
- except Exception:
76
- pass
78
+ except Exception as e:
79
+ logger.error(
80
+ f"while we try to save the customer portfolio aum data for {company}, we encounter the error: {e}"
81
+ )
@@ -91,7 +91,7 @@ def update_portfolio_data(company_portfolio_data, portfolio_data):
91
91
 
92
92
  if investment_discretion := portfolio_data["investment_discretion"]:
93
93
  company_portfolio_data.investment_discretion = investment_discretion
94
-
94
+ company_portfolio_data.update()
95
95
  company_portfolio_data.save()
96
96
 
97
97
 
@@ -103,22 +103,11 @@ class CompanyPortfolioDataMixin(serializers.ModelSerializer):
103
103
  read_only=True,
104
104
  **_get_assets_under_management_kwargs(field_name="invested_assets_under_management_usd"),
105
105
  )
106
- investment_discretion = serializers.ChoiceField(**_get_investment_discretion_kwargs())
107
- assets_under_management_currency = serializers.PrimaryKeyRelatedField(
108
- required=False,
109
- queryset=Currency.objects.all(),
110
- default=None,
111
- label=CompanyPortfolioData.assets_under_management_currency.field.verbose_name,
112
- )
106
+
113
107
  assets_under_management_currency_repr = serializers.CharField(read_only=True, required=False)
114
- _assets_under_management_currency = CurrencyRepresentationSerializer(source="assets_under_management_currency")
108
+
115
109
  potential = serializers.DecimalField(**_get_potential_kwargs())
116
- potential_currency = serializers.PrimaryKeyRelatedField(
117
- required=False,
118
- queryset=Currency.objects.all(),
119
- label=CompanyPortfolioData.potential_currency.field.verbose_name,
120
- )
121
- _potential_currency = CurrencyRepresentationSerializer(source="potential_currency")
110
+ investment_discretion = serializers.ChoiceField(**_get_investment_discretion_kwargs())
122
111
 
123
112
  # Not sure why read_only_fields does not work...
124
113
  tier = serializers.ChoiceField(
@@ -144,10 +133,6 @@ class CompanyPortfolioDataMixin(serializers.ModelSerializer):
144
133
  "asset_under_management", # TODO: add an s after asset - After removing this field from the base model
145
134
  "assets_under_management_currency_repr",
146
135
  "invested_assets_under_management_usd",
147
- "potential_currency",
148
- "_potential_currency",
149
- "assets_under_management_currency",
150
- "_assets_under_management_currency",
151
136
  "investment_discretion",
152
137
  "potential",
153
138
  "tier",
@@ -156,6 +141,19 @@ class CompanyPortfolioDataMixin(serializers.ModelSerializer):
156
141
 
157
142
  class CompanyModelSerializer(CompanyPortfolioDataMixin, BaseCompanyModelSerializer):
158
143
  SERIALIZER_CLASS_FOR_REMOTE_ADDITIONAL_RESOURCES = BasePersonModelSerializer
144
+ assets_under_management_currency = serializers.PrimaryKeyRelatedField(
145
+ required=False,
146
+ queryset=Currency.objects.all(),
147
+ default=None,
148
+ label=CompanyPortfolioData.assets_under_management_currency.field.verbose_name,
149
+ )
150
+ _assets_under_management_currency = CurrencyRepresentationSerializer(source="assets_under_management_currency")
151
+ potential_currency = serializers.PrimaryKeyRelatedField(
152
+ required=False,
153
+ queryset=Currency.objects.all(),
154
+ label=CompanyPortfolioData.potential_currency.field.verbose_name,
155
+ )
156
+ _potential_currency = CurrencyRepresentationSerializer(source="potential_currency")
159
157
 
160
158
  def update(self, instance, validated_data):
161
159
  validated_data, portfolio_data = get_portfolio_data(validated_data)
@@ -209,6 +207,20 @@ class CompanyModelListSerializer(CompanyPortfolioDataMixin, BaseCompanyModelList
209
207
  class PersonModelSerializer(CompanyPortfolioDataMixin, BasePersonModelSerializer):
210
208
  SERIALIZER_CLASS_FOR_REMOTE_ADDITIONAL_RESOURCES = BasePersonModelSerializer
211
209
 
210
+ assets_under_management_currency = serializers.PrimaryKeyRelatedField(
211
+ required=False,
212
+ queryset=Currency.objects.all(),
213
+ default=None,
214
+ label=CompanyPortfolioData.assets_under_management_currency.field.verbose_name,
215
+ )
216
+ _assets_under_management_currency = CurrencyRepresentationSerializer(source="assets_under_management_currency")
217
+ potential_currency = serializers.PrimaryKeyRelatedField(
218
+ required=False,
219
+ queryset=Currency.objects.all(),
220
+ label=CompanyPortfolioData.potential_currency.field.verbose_name,
221
+ )
222
+ _potential_currency = CurrencyRepresentationSerializer(source="potential_currency")
223
+
212
224
  asset_under_management = serializers.DecimalField(
213
225
  **_get_assets_under_management_kwargs(field_name="assets_under_management"),
214
226
  read_only=True,
@@ -243,11 +255,9 @@ class PersonModelListSerializer(CompanyPortfolioDataMixin, BasePersonModelListSe
243
255
 
244
256
 
245
257
  class AssetAllocationTypeRepresentationSerializer(serializers.RepresentationSerializer):
246
- _detail = serializers.HyperlinkField(reverse_name="company_portfolio:assetallocationtyperepresentation-detail")
247
-
248
258
  class Meta:
249
259
  model = AssetAllocationType
250
- fields = ("id", "name", "_detail")
260
+ fields = ("id", "name")
251
261
 
252
262
 
253
263
  class AssetAllocationTypeModelSerializer(serializers.ModelSerializer):
@@ -19,5 +19,16 @@ def update_all_portfolio_data(val_date: date | None = None):
19
19
  has_account=Exists(Account.objects.filter(owner=OuterRef("pk"))),
20
20
  has_portfolio_data=Exists(CompanyPortfolioData.objects.filter(company=OuterRef("pk"))),
21
21
  )
22
+ company_objs = []
23
+ portfolio_data_objs = []
22
24
  for company in tqdm(qs, total=qs.count()):
23
- updater.update_company_data(company)
25
+ portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
26
+ company.customer_status, company.tier = updater.update_company_data(portfolio_data)
27
+ portfolio_data_objs.append(portfolio_data)
28
+ company_objs.append(company)
29
+ if company_objs:
30
+ Company.objects.bulk_update(company_objs, ["customer_status", "tier"])
31
+ if portfolio_data_objs:
32
+ CompanyPortfolioData.objects.bulk_update(
33
+ portfolio_data_objs, ["invested_assets_under_management_usd", "potential"]
34
+ )
@@ -37,7 +37,7 @@ class AssetPositionFactory(factory.django.DjangoModelFactory):
37
37
  initial_price = factory.Faker("pydecimal", min_value=100, max_value=120, right_digits=4)
38
38
  underlying_quote_price = factory.LazyAttribute(
39
39
  lambda o: InstrumentPriceFactory.create(
40
- instrument=o.underlying_instrument or o.underlying_quote,
40
+ instrument=o.underlying_quote or o.underlying_instrument,
41
41
  calculated=False,
42
42
  date=o.date,
43
43
  net_value=o.initial_price,
@@ -1,6 +1,7 @@
1
1
  import factory
2
2
  from faker import Faker
3
3
  from pandas._libs.tslibs.offsets import BDay
4
+ from wbcore.contrib.authentication.factories import UserFactory
4
5
  from wbcore.contrib.currency.factories import CurrencyFactory
5
6
 
6
7
  from wbportfolio.factories import PortfolioFactory
@@ -16,4 +17,5 @@ class OrderProposalFactory(factory.django.DjangoModelFactory):
16
17
  trade_date = factory.LazyAttribute(lambda o: (fake.date_object() + BDay(1)).date())
17
18
  comment = factory.Faker("paragraph")
18
19
  portfolio = factory.LazyAttribute(lambda o: PortfolioFactory.create(currency=CurrencyFactory.create(key="USD")))
19
- creator = factory.SubFactory("wbcore.contrib.directory.factories.PersonFactory")
20
+ creator = factory.LazyAttribute(lambda o: UserFactory.create().profile)
21
+ approver = factory.LazyAttribute(lambda o: UserFactory.create().profile)
@@ -22,8 +22,13 @@ class OrderFactory(factory.django.DjangoModelFactory):
22
22
  @factory.post_generation
23
23
  def create_price(self, create, extracted, **kwargs):
24
24
  if create:
25
- p = InstrumentPriceFactory.create(
26
- instrument=self.underlying_instrument, date=self.value_date, calculated=False
27
- )
25
+ if self.price:
26
+ p = InstrumentPriceFactory.create(
27
+ instrument=self.underlying_instrument, date=self.value_date, calculated=False, net_value=self.price
28
+ )
29
+ else:
30
+ p = InstrumentPriceFactory.create(
31
+ instrument=self.underlying_instrument, date=self.value_date, calculated=False
32
+ )
28
33
  self.price = p.net_value
29
34
  self.save()