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.
- wbportfolio/admin/orders/order_proposals.py +2 -0
- wbportfolio/admin/orders/orders.py +2 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/api_clients/ubs.py +23 -11
- wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/contrib/company_portfolio/serializers.py +32 -22
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/factories/assets.py +1 -1
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +8 -3
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/filters/assets.py +0 -1
- wbportfolio/filters/orders/order_proposals.py +3 -6
- wbportfolio/filters/portfolios.py +18 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/fees.py +0 -2
- wbportfolio/filters/transactions/trades.py +0 -1
- wbportfolio/import_export/backends/ubs/__init__.py +1 -0
- wbportfolio/import_export/backends/ubs/trade.py +48 -0
- wbportfolio/import_export/handlers/asset_position.py +9 -5
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +2 -2
- wbportfolio/import_export/handlers/trade.py +4 -4
- wbportfolio/import_export/parsers/default_mapping.py +1 -1
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
- wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
- wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
- wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/equity.py +2 -1
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/resources/trades.py +1 -1
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
- wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
- wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
- wbportfolio/models/adjustments.py +1 -1
- wbportfolio/models/asset.py +7 -3
- wbportfolio/models/builder.py +25 -5
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/portfolio.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
- wbportfolio/models/orders/order_proposals.py +620 -490
- wbportfolio/models/orders/orders.py +237 -75
- wbportfolio/models/portfolio.py +79 -18
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +3 -0
- wbportfolio/models/rebalancing.py +4 -1
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/claim.py +6 -5
- wbportfolio/models/transactions/dividends.py +1 -0
- wbportfolio/models/transactions/trades.py +4 -0
- wbportfolio/models/transactions/transactions.py +16 -4
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +16 -0
- wbportfolio/order_routing/adapters/__init__.py +14 -6
- wbportfolio/order_routing/adapters/ubs.py +104 -70
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +115 -103
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
- wbportfolio/risk_management/backends/__init__.py +1 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
- wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
- wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
- wbportfolio/serializers/orders/order_proposals.py +6 -2
- wbportfolio/serializers/orders/orders.py +119 -26
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/tasks.py +42 -4
- wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
- wbportfolio/tests/models/test_portfolios.py +9 -9
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
- wbportfolio/tests/rebalancing/test_models.py +2 -2
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +1 -1
- wbportfolio/viewsets/charts/assets.py +8 -4
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
- wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
- wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
- wbportfolio/viewsets/orders/order_proposals.py +92 -21
- wbportfolio/viewsets/orders/orders.py +79 -26
- wbportfolio/viewsets/portfolios.py +24 -0
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/fdm/tasks.py +0 -42
- wbportfolio/models/orders/routing.py +0 -54
- wbportfolio/pms/trading/handler.py +0 -211
- /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
- {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"]
|
wbportfolio/admin/portfolio.py
CHANGED
|
@@ -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 = [
|
wbportfolio/api_clients/ubs.py
CHANGED
|
@@ -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
|
|
5
|
-
from wbcore.contrib.directory.viewsets.display.entries import PersonModelDisplay as
|
|
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(
|
|
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(
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
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(
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
from wbcore.contrib.directory.filters import PersonFilter as
|
|
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(
|
|
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
|
|
108
|
+
return BaseCompanyFilter
|
|
109
109
|
|
|
110
|
-
class Meta(
|
|
110
|
+
class Meta(BaseCompanyFilter.Meta):
|
|
111
111
|
fields = {
|
|
112
|
-
**
|
|
112
|
+
**BaseCompanyFilter.Meta.fields,
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
class PersonFilter(
|
|
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
|
|
122
|
+
return BasePersonFilter
|
|
123
123
|
|
|
124
|
-
class Meta(
|
|
124
|
+
class Meta(BasePersonFilter.Meta):
|
|
125
125
|
fields = {
|
|
126
|
-
**
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
|
|
115
109
|
potential = serializers.DecimalField(**_get_potential_kwargs())
|
|
116
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
+
)
|
wbportfolio/factories/assets.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
26
|
-
|
|
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()
|