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
|
@@ -64,12 +64,15 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
64
64
|
@wb_serializers.register_only_instance_resource()
|
|
65
65
|
def additional_resources(self, instance, request, user, **kwargs):
|
|
66
66
|
res = {}
|
|
67
|
-
if instance.status == OrderProposal.Status.
|
|
67
|
+
if instance.status == OrderProposal.Status.CONFIRMED:
|
|
68
68
|
res["replay"] = reverse("wbportfolio:orderproposal-replay", args=[instance.id], request=request)
|
|
69
69
|
if instance.status == OrderProposal.Status.DRAFT:
|
|
70
70
|
res["reset"] = reverse("wbportfolio:orderproposal-reset", args=[instance.id], request=request)
|
|
71
71
|
res["normalize"] = reverse("wbportfolio:orderproposal-normalize", args=[instance.id], request=request)
|
|
72
|
-
|
|
72
|
+
if instance.status == OrderProposal.Status.DRAFT or instance.can_be_confirmed:
|
|
73
|
+
res["refresh_return"] = reverse(
|
|
74
|
+
"wbportfolio:orderproposal-refreshreturn", args=[instance.id], request=request
|
|
75
|
+
)
|
|
73
76
|
res["orders"] = reverse(
|
|
74
77
|
"wbportfolio:orderproposal-order-list",
|
|
75
78
|
args=[instance.id],
|
|
@@ -90,6 +93,7 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
90
93
|
"comment",
|
|
91
94
|
"status",
|
|
92
95
|
"min_order_value",
|
|
96
|
+
"min_weighting",
|
|
93
97
|
"_rebalancing_model",
|
|
94
98
|
"rebalancing_model",
|
|
95
99
|
"target_portfolio",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
3
|
from rest_framework import serializers
|
|
4
|
+
from rest_framework.reverse import reverse
|
|
5
|
+
from rest_framework.validators import UniqueTogetherValidator
|
|
4
6
|
from wbcore import serializers as wb_serializers
|
|
5
7
|
from wbcore.metadata.configs.display.list_display import BaseTreeGroupLevelOption
|
|
6
8
|
from wbfdm.models import Instrument
|
|
@@ -10,7 +12,7 @@ from wbfdm.serializers.instruments.instruments import (
|
|
|
10
12
|
SecurityRepresentationSerializer,
|
|
11
13
|
)
|
|
12
14
|
|
|
13
|
-
from wbportfolio.models import Order
|
|
15
|
+
from wbportfolio.models import Order, OrderProposal
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class GetSecurityDefault:
|
|
@@ -44,32 +46,50 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
44
46
|
underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
|
|
45
47
|
underlying_instrument_exchange = wb_serializers.CharField(read_only=True)
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
effective_weight = wb_serializers.DecimalField(
|
|
50
|
+
read_only=True,
|
|
48
51
|
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
49
52
|
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
50
|
-
required=False,
|
|
51
53
|
default=0,
|
|
52
54
|
)
|
|
53
|
-
|
|
54
|
-
read_only=True,
|
|
55
|
+
target_weight = wb_serializers.DecimalField(
|
|
55
56
|
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
56
57
|
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
57
|
-
|
|
58
|
+
required=False,
|
|
58
59
|
)
|
|
59
60
|
|
|
60
61
|
effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
61
|
-
target_shares = wb_serializers.DecimalField(
|
|
62
|
+
target_shares = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=6)
|
|
62
63
|
|
|
63
|
-
total_value_fx_portfolio = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=2, default=0)
|
|
64
64
|
effective_total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
65
65
|
read_only=True, max_digits=16, decimal_places=2, default=0
|
|
66
66
|
)
|
|
67
|
-
target_total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
68
|
-
|
|
69
|
-
)
|
|
67
|
+
target_total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
|
|
68
|
+
total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
|
|
70
69
|
|
|
71
70
|
portfolio_currency = wb_serializers.CharField(read_only=True)
|
|
71
|
+
underlying_instrument_currency = wb_serializers.CharField(read_only=True)
|
|
72
72
|
has_warnings = wb_serializers.BooleanField(read_only=True)
|
|
73
|
+
execution_instruction_parameters_repr = wb_serializers.CharField(read_only=True)
|
|
74
|
+
execution_date = wb_serializers.DateField(read_only=True)
|
|
75
|
+
execution_price = wb_serializers.FloatField(read_only=True)
|
|
76
|
+
execution_traded_shares = wb_serializers.FloatField(read_only=True)
|
|
77
|
+
|
|
78
|
+
@wb_serializers.register_resource()
|
|
79
|
+
def additional_resources(self, instance, request, user):
|
|
80
|
+
if (view := request.parser_context.get("view")) and view.order_proposal.status in [
|
|
81
|
+
OrderProposal.Status.DRAFT,
|
|
82
|
+
OrderProposal.Status.PENDING,
|
|
83
|
+
OrderProposal.Status.APPROVED,
|
|
84
|
+
]:
|
|
85
|
+
return {
|
|
86
|
+
"execution_instruction": reverse(
|
|
87
|
+
"wbportfolio:orderproposal-order-changeexecutioninstruction",
|
|
88
|
+
args=[view.order_proposal.id, instance.id],
|
|
89
|
+
request=request,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return {}
|
|
73
93
|
|
|
74
94
|
def validate(self, data):
|
|
75
95
|
data.pop("company", None)
|
|
@@ -80,21 +100,71 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
80
100
|
"underlying_instrument": "You cannot modify the underlying instrument other than creating a new entry"
|
|
81
101
|
}
|
|
82
102
|
)
|
|
103
|
+
|
|
83
104
|
effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if
|
|
89
|
-
data["
|
|
90
|
-
|
|
91
|
-
data
|
|
92
|
-
|
|
105
|
+
effective_shares = self.instance._effective_shares if self.instance else Decimal(0.0)
|
|
106
|
+
portfolio_value = (
|
|
107
|
+
self.context["view"].order_proposal.portfolio_total_asset_value if "view" in self.context else Decimal(0.0)
|
|
108
|
+
)
|
|
109
|
+
if (total_value_fx_portfolio := data.pop("total_value_fx_portfolio", None)) is not None and portfolio_value:
|
|
110
|
+
data["weighting"] = total_value_fx_portfolio / portfolio_value
|
|
111
|
+
if (
|
|
112
|
+
target_total_value_fx_portfolio := data.pop("target_total_value_fx_portfolio", None)
|
|
113
|
+
) is not None and portfolio_value:
|
|
114
|
+
data["target_weight"] = target_total_value_fx_portfolio / portfolio_value
|
|
115
|
+
|
|
116
|
+
if data.get("weighting") is not None or data.get("target_weight") is not None:
|
|
117
|
+
weighting = data.pop("weighting", None)
|
|
118
|
+
if (target_weight := data.pop("target_weight", None)) is not None:
|
|
119
|
+
weighting = target_weight - effective_weight
|
|
120
|
+
data["desired_target_weight"] = target_weight
|
|
121
|
+
if weighting is not None:
|
|
122
|
+
data["weighting"] = weighting
|
|
123
|
+
data.pop("shares", None)
|
|
124
|
+
data.pop("target_shares", None)
|
|
125
|
+
|
|
126
|
+
if data.get("shares") is not None or data.get("target_shares") is not None:
|
|
127
|
+
shares = data.pop("shares", None)
|
|
128
|
+
if (target_shares := data.pop("target_shares", None)) is not None:
|
|
129
|
+
shares = target_shares - effective_shares
|
|
130
|
+
if shares is not None:
|
|
131
|
+
data["shares"] = shares
|
|
93
132
|
return super().validate(data)
|
|
94
133
|
|
|
134
|
+
def update(self, instance, validated_data):
|
|
135
|
+
weighting = validated_data.pop("weighting", None)
|
|
136
|
+
shares = validated_data.pop("shares", None)
|
|
137
|
+
portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
|
|
138
|
+
if weighting is not None:
|
|
139
|
+
instance.set_weighting(weighting, portfolio_total_asset_value)
|
|
140
|
+
if shares is not None:
|
|
141
|
+
instance.set_shares(shares, portfolio_total_asset_value)
|
|
142
|
+
return super().update(instance, validated_data)
|
|
143
|
+
|
|
144
|
+
def create(self, validated_data):
|
|
145
|
+
weighting = validated_data.pop("weighting", None)
|
|
146
|
+
shares = validated_data.pop("shares", None)
|
|
147
|
+
instance = super().create(validated_data)
|
|
148
|
+
portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
|
|
149
|
+
if weighting is not None:
|
|
150
|
+
instance.set_weighting(weighting, portfolio_total_asset_value)
|
|
151
|
+
if shares is not None:
|
|
152
|
+
instance.set_shares(shares, portfolio_total_asset_value)
|
|
153
|
+
instance.save()
|
|
154
|
+
return instance
|
|
155
|
+
|
|
156
|
+
def get_unique_together_validators(self):
|
|
157
|
+
return [
|
|
158
|
+
UniqueTogetherValidator(
|
|
159
|
+
queryset=Order.objects.all(),
|
|
160
|
+
fields=("order_proposal", "underlying_instrument"),
|
|
161
|
+
message="This instrument is already in the orders list.",
|
|
162
|
+
)
|
|
163
|
+
]
|
|
164
|
+
|
|
95
165
|
class Meta:
|
|
96
166
|
model = Order
|
|
97
|
-
percent_fields = ["effective_weight", "target_weight", "weighting"]
|
|
167
|
+
percent_fields = ["effective_weight", "target_weight", "weighting", "desired_target_weight"]
|
|
98
168
|
decorators = {
|
|
99
169
|
"total_value_fx_portfolio": wb_serializers.decorator(
|
|
100
170
|
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
@@ -105,17 +175,26 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
105
175
|
"target_total_value_fx_portfolio": wb_serializers.decorator(
|
|
106
176
|
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
107
177
|
),
|
|
178
|
+
"price": wb_serializers.decorator(position="left", value="{{underlying_instrument_currency}}"),
|
|
108
179
|
}
|
|
109
180
|
read_only_fields = (
|
|
110
181
|
"order_type",
|
|
111
|
-
"shares",
|
|
112
182
|
"effective_shares",
|
|
113
|
-
"target_shares",
|
|
114
|
-
"total_value_fx_portfolio",
|
|
115
183
|
"effective_total_value_fx_portfolio",
|
|
116
|
-
"target_total_value_fx_portfolio",
|
|
117
184
|
"has_warnings",
|
|
185
|
+
"desired_target_weight",
|
|
186
|
+
"daily_return",
|
|
187
|
+
"currency_fx_rate",
|
|
188
|
+
"price",
|
|
189
|
+
"execution_instruction",
|
|
190
|
+
"execution_instruction_parameters_repr",
|
|
191
|
+
"execution_date",
|
|
192
|
+
"execution_price",
|
|
193
|
+
"execution_traded_shares",
|
|
118
194
|
)
|
|
195
|
+
extra_kwargs = {
|
|
196
|
+
"price": {"required": False},
|
|
197
|
+
}
|
|
119
198
|
fields = (
|
|
120
199
|
"id",
|
|
121
200
|
"shares",
|
|
@@ -138,9 +217,21 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
138
217
|
"effective_total_value_fx_portfolio",
|
|
139
218
|
"target_total_value_fx_portfolio",
|
|
140
219
|
"portfolio_currency",
|
|
220
|
+
"underlying_instrument_currency",
|
|
141
221
|
"has_warnings",
|
|
142
|
-
"
|
|
222
|
+
"desired_target_weight",
|
|
223
|
+
"daily_return",
|
|
224
|
+
"currency_fx_rate",
|
|
225
|
+
"price",
|
|
226
|
+
"execution_status",
|
|
227
|
+
"execution_instruction",
|
|
228
|
+
"execution_instruction_parameters",
|
|
143
229
|
"execution_comment",
|
|
230
|
+
"execution_instruction_parameters_repr",
|
|
231
|
+
"execution_date",
|
|
232
|
+
"execution_price",
|
|
233
|
+
"execution_traded_shares",
|
|
234
|
+
"_additional_resources",
|
|
144
235
|
)
|
|
145
236
|
|
|
146
237
|
|
|
@@ -164,6 +255,7 @@ class OrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
|
|
|
164
255
|
optional_get_parameters={"company": "parent"},
|
|
165
256
|
depends_on=[{"field": "company", "options": {}}],
|
|
166
257
|
required=False,
|
|
258
|
+
select_first_choice=True,
|
|
167
259
|
)
|
|
168
260
|
underlying_instrument = wb_serializers.PrimaryKeyRelatedField(
|
|
169
261
|
queryset=Instrument.objects.all(), label="Quote", read_only=lambda view: not view.new_mode
|
|
@@ -173,6 +265,7 @@ class OrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
|
|
|
173
265
|
optional_get_parameters={"security": "parent"},
|
|
174
266
|
depends_on=[{"field": "security", "options": {}}],
|
|
175
267
|
tree_config=BaseTreeGroupLevelOption(clear_filter=True, filter_key="parent"),
|
|
268
|
+
select_first_choice=True,
|
|
176
269
|
)
|
|
177
270
|
|
|
178
271
|
class Meta(OrderOrderProposalListModelSerializer.Meta):
|
|
@@ -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"})
|
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
|
|
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")
|
|
@@ -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
|
+
)
|