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
|
@@ -49,8 +49,8 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
|
|
|
49
49
|
),
|
|
50
50
|
dp.LegendItem(
|
|
51
51
|
icon=WBColor.GREEN.value,
|
|
52
|
-
label=OrderProposal.Status.
|
|
53
|
-
value=OrderProposal.Status.
|
|
52
|
+
label=OrderProposal.Status.CONFIRMED.label,
|
|
53
|
+
value=OrderProposal.Status.CONFIRMED.value,
|
|
54
54
|
),
|
|
55
55
|
dp.LegendItem(
|
|
56
56
|
icon=WBColor.GREY.value,
|
|
@@ -78,7 +78,7 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
|
|
|
78
78
|
),
|
|
79
79
|
dp.FormattingRule(
|
|
80
80
|
style={"backgroundColor": WBColor.GREEN.value},
|
|
81
|
-
condition=("==", OrderProposal.Status.
|
|
81
|
+
condition=("==", OrderProposal.Status.CONFIRMED.value),
|
|
82
82
|
),
|
|
83
83
|
dp.FormattingRule(
|
|
84
84
|
style={"backgroundColor": WBColor.GREY.value},
|
|
@@ -125,33 +125,33 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
|
|
|
125
125
|
),
|
|
126
126
|
)
|
|
127
127
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
],
|
|
142
|
-
),
|
|
143
|
-
},
|
|
128
|
+
|
|
129
|
+
main_info_page = Page(
|
|
130
|
+
title="Main Information",
|
|
131
|
+
layouts={
|
|
132
|
+
default(): Layout(
|
|
133
|
+
grid_template_areas=[
|
|
134
|
+
["status", "status", "status", "status"],
|
|
135
|
+
["trade_date", "total_cash_weight", "min_order_value", "min_weighting"],
|
|
136
|
+
["rebalancing_model", "rebalancing_model", "target_portfolio", "target_portfolio"]
|
|
137
|
+
if self.view.new_mode
|
|
138
|
+
else ["rebalancing_model", "rebalancing_model", "creator", "approver"],
|
|
139
|
+
["comment", "comment", "comment", "comment"],
|
|
140
|
+
],
|
|
144
141
|
),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
orders_page = Page(
|
|
145
|
+
title="Orders",
|
|
146
|
+
layouts={
|
|
147
|
+
default(): Layout(
|
|
148
|
+
grid_template_areas=orders_grid_template_areas,
|
|
149
|
+
grid_template_rows=orders_grid_template_rows,
|
|
150
|
+
inlines=[Inline(key="orders", endpoint="orders")],
|
|
151
|
+
sections=sections,
|
|
155
152
|
),
|
|
156
|
-
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
return Display(
|
|
156
|
+
pages=[orders_page, main_info_page] if "pk" in self.view.kwargs else [main_info_page, orders_page]
|
|
157
157
|
)
|
|
@@ -71,17 +71,27 @@ class OrderOrderProposalDisplayConfig(DisplayViewConfig):
|
|
|
71
71
|
fields = [
|
|
72
72
|
dp.Field(
|
|
73
73
|
label="Instrument",
|
|
74
|
-
open_by_default=
|
|
74
|
+
open_by_default=False,
|
|
75
75
|
key=None,
|
|
76
76
|
children=[
|
|
77
77
|
dp.Field(key="underlying_instrument", label="Name", width=Unit.PIXEL(250)),
|
|
78
78
|
dp.Field(key="underlying_instrument_isin", label="ISIN", width=Unit.PIXEL(125)),
|
|
79
|
-
dp.Field(key="underlying_instrument_ticker", label="Ticker", width=Unit.PIXEL(100)),
|
|
79
|
+
dp.Field(key="underlying_instrument_ticker", label="Ticker", width=Unit.PIXEL(100), show="open"),
|
|
80
|
+
dp.Field(
|
|
81
|
+
key="underlying_instrument_refinitiv_identifier_code",
|
|
82
|
+
label="RIC",
|
|
83
|
+
width=Unit.PIXEL(100),
|
|
84
|
+
show="open",
|
|
85
|
+
),
|
|
80
86
|
dp.Field(
|
|
81
|
-
key="
|
|
87
|
+
key="underlying_instrument_instrument_type",
|
|
88
|
+
label="Asset Class",
|
|
89
|
+
width=Unit.PIXEL(125),
|
|
90
|
+
show="open",
|
|
91
|
+
),
|
|
92
|
+
dp.Field(
|
|
93
|
+
key="underlying_instrument_exchange", label="Exchange", width=Unit.PIXEL(125), show="open"
|
|
82
94
|
),
|
|
83
|
-
dp.Field(key="underlying_instrument_instrument_type", label="Asset Class", width=Unit.PIXEL(125)),
|
|
84
|
-
dp.Field(key="underlying_instrument_exchange", label="Exchange", width=Unit.PIXEL(125)),
|
|
85
95
|
],
|
|
86
96
|
),
|
|
87
97
|
dp.Field(
|
|
@@ -157,23 +167,52 @@ class OrderOrderProposalDisplayConfig(DisplayViewConfig):
|
|
|
157
167
|
formatting_rules=ORDER_TYPE_FORMATTING_RULES,
|
|
158
168
|
width=Unit.PIXEL(125),
|
|
159
169
|
),
|
|
160
|
-
dp.Field(
|
|
161
|
-
|
|
170
|
+
dp.Field(
|
|
171
|
+
key="desired_target_weight", label="Desired Target Weight", show="open", width=Unit.PIXEL(100)
|
|
172
|
+
),
|
|
173
|
+
dp.Field(key="daily_return", label="Daily Return", show="open", width=Unit.PIXEL(100)),
|
|
174
|
+
dp.Field(key="currency_fx_rate", label="FX Rate", show="open", width=Unit.PIXEL(100)),
|
|
175
|
+
dp.Field(key="price", label="Price", show="open", width=Unit.PIXEL(100)),
|
|
176
|
+
dp.Field(key="order", label="Order", show="open", width=Unit.PIXEL(50)),
|
|
177
|
+
dp.Field(key="comment", label="Comment", show="open", width=Unit.PIXEL(250)),
|
|
162
178
|
],
|
|
163
179
|
)
|
|
164
180
|
)
|
|
181
|
+
execution_fields = [
|
|
182
|
+
dp.Field(
|
|
183
|
+
label="Instruction",
|
|
184
|
+
open_by_default=False,
|
|
185
|
+
key=None,
|
|
186
|
+
children=[
|
|
187
|
+
dp.Field(key="execution_instruction", label="Type", width=Unit.PIXEL(125)),
|
|
188
|
+
dp.Field(
|
|
189
|
+
key="execution_instruction_parameters_repr",
|
|
190
|
+
label="Parameters",
|
|
191
|
+
width=Unit.PIXEL(125),
|
|
192
|
+
show="open",
|
|
193
|
+
),
|
|
194
|
+
],
|
|
195
|
+
)
|
|
196
|
+
]
|
|
197
|
+
|
|
165
198
|
if order_proposal.execution_status:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
label="
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
execution_fields.extend(
|
|
200
|
+
[
|
|
201
|
+
dp.Field(key="execution_status", label="Status", width=Unit.PIXEL(100)),
|
|
202
|
+
dp.Field(key="execution_comment", label="Comment", width=Unit.PIXEL(150), show="open"),
|
|
203
|
+
dp.Field(
|
|
204
|
+
label="Trade",
|
|
205
|
+
open_by_default=False,
|
|
206
|
+
key=None,
|
|
207
|
+
children=[
|
|
208
|
+
dp.Field(key="execution_date", label="Date", width=Unit.PIXEL(100), show="open"),
|
|
209
|
+
dp.Field(key="execution_price", label="Price", width=Unit.PIXEL(100), show="open"),
|
|
210
|
+
dp.Field(key="execution_traded_shares", label="Shares", width=Unit.PIXEL(100)),
|
|
211
|
+
],
|
|
212
|
+
),
|
|
213
|
+
]
|
|
176
214
|
)
|
|
215
|
+
fields.append(dp.Field(label="Execution", open_by_default=False, key=None, children=execution_fields))
|
|
177
216
|
return dp.ListDisplay(
|
|
178
217
|
fields=fields,
|
|
179
218
|
legends=[ORDER_STATUS_LEGENDS],
|
|
@@ -9,7 +9,7 @@ class OrderProposalEndpointConfig(EndpointViewConfig):
|
|
|
9
9
|
def get_delete_endpoint(self, **kwargs):
|
|
10
10
|
if pk := self.view.kwargs.get("pk", None):
|
|
11
11
|
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
12
|
-
if order_proposal.status
|
|
12
|
+
if order_proposal.status in [OrderProposal.Status.DRAFT, OrderProposal.Status.DENIED]:
|
|
13
13
|
return super().get_endpoint()
|
|
14
14
|
return None
|
|
15
15
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from contextlib import suppress
|
|
2
|
-
|
|
3
1
|
from rest_framework.reverse import reverse
|
|
4
2
|
from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
5
3
|
|
|
@@ -18,9 +16,13 @@ class OrderOrderProposalEndpointConfig(EndpointViewConfig):
|
|
|
18
16
|
)
|
|
19
17
|
return None
|
|
20
18
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if
|
|
25
|
-
return
|
|
26
|
-
|
|
19
|
+
def get_update_endpoint(self, **kwargs):
|
|
20
|
+
if order_proposal_id := self.view.kwargs.get("order_proposal_id", None):
|
|
21
|
+
order_proposal = OrderProposal.objects.get(id=order_proposal_id)
|
|
22
|
+
if order_proposal.status == OrderProposal.Status.DRAFT or order_proposal.can_be_confirmed:
|
|
23
|
+
return reverse(
|
|
24
|
+
"wbportfolio:orderproposal-order-list",
|
|
25
|
+
args=[self.view.kwargs["order_proposal_id"]],
|
|
26
|
+
request=self.request,
|
|
27
|
+
)
|
|
28
|
+
return None
|
|
@@ -2,7 +2,7 @@ from contextlib import suppress
|
|
|
2
2
|
from datetime import date
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
|
|
5
|
-
from django.contrib.messages import
|
|
5
|
+
from django.contrib.messages import error, warning
|
|
6
6
|
from django.shortcuts import get_object_or_404
|
|
7
7
|
from django.utils.functional import cached_property
|
|
8
8
|
from pandas._libs.tslibs.offsets import BDay
|
|
@@ -19,8 +19,9 @@ from wbcore.metadata.configs.display.instance_display import (
|
|
|
19
19
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
20
20
|
from wbcore.utils.views import CloneMixin
|
|
21
21
|
|
|
22
|
-
from wbportfolio.models import AssetPosition, OrderProposal
|
|
22
|
+
from wbportfolio.models import AssetPosition, Order, OrderProposal
|
|
23
23
|
from wbportfolio.models.orders.order_proposals import (
|
|
24
|
+
execute_orders_as_task,
|
|
24
25
|
push_model_change_as_task,
|
|
25
26
|
replay_as_task,
|
|
26
27
|
)
|
|
@@ -31,7 +32,8 @@ from wbportfolio.serializers import (
|
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
from ...filters.orders import OrderProposalFilterSet
|
|
34
|
-
from ...order_routing import ExecutionStatus
|
|
35
|
+
from ...order_routing import ExecutionStatus, RoutingException
|
|
36
|
+
from ...permissions import IsPortfolioManager
|
|
35
37
|
from ..mixins import UserPortfolioRequestPermissionMixin
|
|
36
38
|
from .configs import (
|
|
37
39
|
OrderProposalButtonConfig,
|
|
@@ -48,6 +50,7 @@ class OrderProposalRepresentationViewSet(InternalUserPermissionMixin, viewsets.R
|
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserPermissionMixin, viewsets.ModelViewSet):
|
|
53
|
+
IDENTIFIER = "wbportfolio:order"
|
|
51
54
|
ordering_fields = ("trade_date",)
|
|
52
55
|
ordering = ("-trade_date",)
|
|
53
56
|
search_fields = ("comment",)
|
|
@@ -87,16 +90,16 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
87
90
|
|
|
88
91
|
def add_messages(self, request, instance: OrderProposal | None = None, **kwargs):
|
|
89
92
|
if instance:
|
|
90
|
-
if instance.status == OrderProposal.Status.APPROVED and not instance.portfolio.is_manageable:
|
|
91
|
-
info(request, "This order proposal cannot be approved the portfolio is considered unmanaged.")
|
|
92
93
|
if instance.status == OrderProposal.Status.PENDING and instance.has_non_successful_checks:
|
|
93
94
|
warning(
|
|
94
95
|
request,
|
|
95
96
|
"This order proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid order proposal",
|
|
96
97
|
)
|
|
97
98
|
if (
|
|
98
|
-
instance.
|
|
99
|
-
and instance.orders.
|
|
99
|
+
instance.status == OrderProposal.Status.EXECUTION
|
|
100
|
+
and instance.orders.exclude(shares=0, weighting=0)
|
|
101
|
+
.filter(execution_status=Order.ExecutionStatus.FAILED)
|
|
102
|
+
.exists()
|
|
100
103
|
):
|
|
101
104
|
warning(request, "Some orders failed confirmation. Check the list for further details.")
|
|
102
105
|
if instance.execution_status in [
|
|
@@ -108,12 +111,17 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
108
111
|
request,
|
|
109
112
|
f"The execution status is {ExecutionStatus[instance.execution_status].label}. Detail: {instance.execution_comment}",
|
|
110
113
|
)
|
|
114
|
+
elif instance.can_be_executed and instance.approver == request.user.profile:
|
|
115
|
+
warning(
|
|
116
|
+
request,
|
|
117
|
+
"As the approver of these orders, you are not authorized to execute them yourself. Please assign execution to another qualified individual.",
|
|
118
|
+
)
|
|
111
119
|
|
|
112
120
|
@classmethod
|
|
113
121
|
def _get_risk_checks_button_title(cls) -> str:
|
|
114
122
|
return "Pre-Trade Checks"
|
|
115
123
|
|
|
116
|
-
@action(detail=True, methods=["PATCH"])
|
|
124
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
117
125
|
def reset(self, request, pk=None):
|
|
118
126
|
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
119
127
|
use_desired_target_weight = request.GET.get("use_desired_target_weight") == "true"
|
|
@@ -123,16 +131,16 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
123
131
|
return Response({"send": True})
|
|
124
132
|
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
125
133
|
|
|
126
|
-
@action(detail=True, methods=["PATCH"])
|
|
134
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
127
135
|
def normalize(self, request, pk=None):
|
|
128
136
|
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
129
137
|
total_cash_weight = Decimal(request.data.get("total_cash_weight", Decimal("0.0")))
|
|
130
138
|
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
131
|
-
order_proposal.normalize_orders(
|
|
139
|
+
order_proposal.normalize_orders(total_cash_weight)
|
|
132
140
|
return Response({"send": True})
|
|
133
141
|
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
134
142
|
|
|
135
|
-
@action(detail=True, methods=["PATCH"])
|
|
143
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
136
144
|
def replay(self, request, pk=None):
|
|
137
145
|
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
138
146
|
if order_proposal.portfolio.is_manageable:
|
|
@@ -140,22 +148,14 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
140
148
|
return Response({"send": True})
|
|
141
149
|
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
142
150
|
|
|
143
|
-
@action(detail=True, methods=["PATCH"])
|
|
144
|
-
def deleteall(self, request, pk=None):
|
|
145
|
-
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
146
|
-
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
147
|
-
order_proposal.orders.all().delete()
|
|
148
|
-
return Response({"send": True})
|
|
149
|
-
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
150
|
-
|
|
151
|
-
@action(detail=True, methods=["PATCH"])
|
|
151
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
152
152
|
def pushmodelchange(self, request, pk=None):
|
|
153
153
|
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
154
154
|
only_for_portfolio_ids = list(
|
|
155
155
|
map(lambda o: int(o), filter(lambda r: r, request.data.get("only_for_portfolio_ids", "").split(",")))
|
|
156
156
|
)
|
|
157
157
|
approve_automatically = request.data.get("approve_automatically") == "true"
|
|
158
|
-
if order_proposal.status == OrderProposal.Status.
|
|
158
|
+
if order_proposal.status == OrderProposal.Status.APPROVED and order_proposal.portfolio.is_model:
|
|
159
159
|
push_model_change_as_task.delay(
|
|
160
160
|
order_proposal.id,
|
|
161
161
|
request.user.id,
|
|
@@ -168,6 +168,77 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
168
168
|
status=status.HTTP_400_BAD_REQUEST,
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
172
|
+
def execute(self, request, pk=None):
|
|
173
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
174
|
+
if order_proposal.can_execute(request.user):
|
|
175
|
+
prioritize_target_weight = request.data.get("prioritize_target_weight") == "true"
|
|
176
|
+
order_proposal.execution_status = ExecutionStatus.PENDING
|
|
177
|
+
order_proposal.execution_comment = "Waiting for custodian confirmation"
|
|
178
|
+
order_proposal.save()
|
|
179
|
+
execute_orders_as_task.delay(order_proposal.id, prioritize_target_weight=prioritize_target_weight)
|
|
180
|
+
return Response({"send": True})
|
|
181
|
+
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
182
|
+
|
|
183
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
184
|
+
def cancelexecution(self, request, pk=None):
|
|
185
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
186
|
+
if order_proposal.execution_status and order_proposal.execution_status != ExecutionStatus.CANCELLED:
|
|
187
|
+
try:
|
|
188
|
+
if not order_proposal.cancel_rebalancing():
|
|
189
|
+
warning(
|
|
190
|
+
request,
|
|
191
|
+
"We could not cancel the rebalancing. It is probably already executed. Please refresh status or check with an administrator.",
|
|
192
|
+
)
|
|
193
|
+
except (RoutingException, ValueError) as e:
|
|
194
|
+
error(request, f"Could not cancel orders proposal {order_proposal}: {str(e)}")
|
|
195
|
+
return Response({"send": True})
|
|
196
|
+
return Response(
|
|
197
|
+
{"status": "Order Proposal is not in an execution phase, therefore, it cannot be cancelled."},
|
|
198
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
202
|
+
def updateexecutionstatus(self, request, pk=None):
|
|
203
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
204
|
+
if order_proposal.execution_status:
|
|
205
|
+
try:
|
|
206
|
+
if not order_proposal.custodian_router:
|
|
207
|
+
raise RoutingException(
|
|
208
|
+
"There is no custodian router for this portfolio. Please check with an administrator."
|
|
209
|
+
)
|
|
210
|
+
order_proposal.refresh_execution_status()
|
|
211
|
+
except (RoutingException, ValueError) as e:
|
|
212
|
+
error(request, f"Could not update rebalancing status: {str(e)}")
|
|
213
|
+
return Response(
|
|
214
|
+
{
|
|
215
|
+
"send": True,
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
return Response(
|
|
219
|
+
{"status": "Order Proposal is not in an execution phase, therefore, its status cannot be fetched."},
|
|
220
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
224
|
+
def refreshreturn(self, request, pk=None):
|
|
225
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
226
|
+
order_proposal.refresh_returns()
|
|
227
|
+
return Response(
|
|
228
|
+
{"status": "Returns were refreshed with success"},
|
|
229
|
+
status=status.HTTP_200_OK,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
@action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
|
|
233
|
+
def refreshpretradechecks(self, request, pk=None):
|
|
234
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
235
|
+
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
236
|
+
order_proposal.evaluate_pretrade_checks()
|
|
237
|
+
return Response(
|
|
238
|
+
{"status": "Evaluate pretrade checks"},
|
|
239
|
+
status=status.HTTP_200_OK,
|
|
240
|
+
)
|
|
241
|
+
|
|
171
242
|
|
|
172
243
|
class OrderProposalPortfolioModelViewSet(UserPortfolioRequestPermissionMixin, OrderProposalModelViewSet):
|
|
173
244
|
endpoint_config_class = OrderProposalPortfolioEndpointConfig
|
|
@@ -3,13 +3,17 @@ from decimal import Decimal
|
|
|
3
3
|
from django.contrib.messages import error, info
|
|
4
4
|
from django.db.models import (
|
|
5
5
|
Case,
|
|
6
|
+
CharField,
|
|
6
7
|
F,
|
|
8
|
+
Func,
|
|
7
9
|
Sum,
|
|
8
10
|
Value,
|
|
9
11
|
When,
|
|
10
12
|
)
|
|
11
13
|
from django.shortcuts import get_object_or_404
|
|
12
14
|
from django.utils.functional import cached_property
|
|
15
|
+
from rest_framework.decorators import action
|
|
16
|
+
from rest_framework.response import Response
|
|
13
17
|
from wbcore import viewsets
|
|
14
18
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
15
19
|
from wbcore.utils.strings import format_number
|
|
@@ -24,17 +28,20 @@ from wbportfolio.serializers import (
|
|
|
24
28
|
)
|
|
25
29
|
|
|
26
30
|
from ...filters.orders import OrderFilterSet
|
|
31
|
+
from ...permissions import IsPortfolioManager
|
|
27
32
|
from ..mixins import UserPortfolioRequestPermissionMixin
|
|
28
33
|
from .configs import (
|
|
29
34
|
OrderOrderProposalButtonConfig,
|
|
30
35
|
OrderOrderProposalDisplayConfig,
|
|
31
36
|
OrderOrderProposalEndpointConfig,
|
|
32
37
|
)
|
|
38
|
+
from .configs.buttons.orders import ExecutionInstructionSerializer
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
class OrderOrderProposalModelViewSet(
|
|
36
42
|
UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, OrderableMixin, viewsets.ModelViewSet
|
|
37
43
|
):
|
|
44
|
+
IDENTIFIER = "wbportfolio:order"
|
|
38
45
|
IMPORT_ALLOWED = True
|
|
39
46
|
ordering = (
|
|
40
47
|
"order_proposal",
|
|
@@ -53,7 +60,7 @@ class OrderOrderProposalModelViewSet(
|
|
|
53
60
|
"shares",
|
|
54
61
|
"weighting",
|
|
55
62
|
)
|
|
56
|
-
IDENTIFIER = "wbportfolio:
|
|
63
|
+
IDENTIFIER = "wbportfolio:order"
|
|
57
64
|
search_fields = ("underlying_instrument__name",)
|
|
58
65
|
queryset = Order.objects.none()
|
|
59
66
|
filterset_class = OrderFilterSet
|
|
@@ -92,10 +99,10 @@ class OrderOrderProposalModelViewSet(
|
|
|
92
99
|
sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
|
|
93
100
|
)
|
|
94
101
|
# weights aggregates
|
|
95
|
-
cash_sum_effective_weight = (
|
|
96
|
-
|
|
102
|
+
cash_sum_effective_weight = self.order_proposal.total_effective_portfolio_weight - (
|
|
103
|
+
noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
97
104
|
)
|
|
98
|
-
cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
|
|
105
|
+
cash_sum_target_cash_weight = Decimal("1.0") - (noncash_aggregates["sum_target_weight"] or Decimal(0))
|
|
99
106
|
noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
100
107
|
noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
|
|
101
108
|
sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
@@ -171,11 +178,25 @@ class OrderOrderProposalModelViewSet(
|
|
|
171
178
|
return agg
|
|
172
179
|
|
|
173
180
|
def get_serializer_class(self):
|
|
174
|
-
if self.order_proposal.status != OrderProposal.Status.DRAFT:
|
|
181
|
+
if self.order_proposal.status != OrderProposal.Status.DRAFT and not self.order_proposal.can_be_confirmed:
|
|
175
182
|
return ReadOnlyOrderOrderProposalModelSerializer
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
if not self.new_mode and "pk" not in self.kwargs:
|
|
184
|
+
serializer_base_class = OrderOrderProposalListModelSerializer
|
|
185
|
+
else:
|
|
186
|
+
serializer_base_class = OrderOrderProposalModelSerializer
|
|
187
|
+
if not self.order_proposal.portfolio_total_asset_value:
|
|
188
|
+
|
|
189
|
+
class OnlyWeightSerializerClass(serializer_base_class):
|
|
190
|
+
class Meta(serializer_base_class.Meta):
|
|
191
|
+
read_only_fields = list(serializer_base_class.Meta.read_only_fields) + [
|
|
192
|
+
"shares",
|
|
193
|
+
"target_shares",
|
|
194
|
+
"total_value_fx_portfolio",
|
|
195
|
+
"target_total_value_fx_portfolio",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
return OnlyWeightSerializerClass
|
|
199
|
+
return serializer_base_class
|
|
179
200
|
|
|
180
201
|
def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
|
|
181
202
|
if self.orders.exists() and self.order_proposal.status in [
|
|
@@ -202,23 +223,55 @@ class OrderOrderProposalModelViewSet(
|
|
|
202
223
|
return qs
|
|
203
224
|
|
|
204
225
|
def get_queryset(self):
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
return (
|
|
227
|
+
self.orders.filter(underlying_instrument__is_cash=False)
|
|
228
|
+
.annotate( # .exclude(underlying_instrument__is_cash=True)
|
|
229
|
+
underlying_instrument_isin=F("underlying_instrument__isin"),
|
|
230
|
+
underlying_instrument_ticker=F("underlying_instrument__ticker"),
|
|
231
|
+
underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
|
|
232
|
+
underlying_instrument_instrument_type=Case(
|
|
233
|
+
When(
|
|
234
|
+
underlying_instrument__parent__is_security=True,
|
|
235
|
+
then=F("underlying_instrument__parent__instrument_type__short_name"),
|
|
236
|
+
),
|
|
237
|
+
default=F("underlying_instrument__instrument_type__short_name"),
|
|
213
238
|
),
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
underlying_instrument_exchange=F("underlying_instrument__exchange__name"),
|
|
240
|
+
effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
|
|
241
|
+
target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
|
|
242
|
+
portfolio_currency=F("portfolio__currency__symbol"),
|
|
243
|
+
underlying_instrument_currency=F("underlying_instrument__currency__symbol"),
|
|
244
|
+
security=F("underlying_instrument__parent"),
|
|
245
|
+
company=F("underlying_instrument__parent__parent"),
|
|
246
|
+
execution_instruction_parameters_repr=Func(
|
|
247
|
+
"execution_instruction_parameters",
|
|
248
|
+
function="string_agg",
|
|
249
|
+
template="(SELECT string_agg(key || '=' || value, ',') FROM jsonb_each_text(%(expressions)s))",
|
|
250
|
+
output_field=CharField(),
|
|
251
|
+
),
|
|
252
|
+
execution_date=F("execution_trade__transaction_date"),
|
|
253
|
+
execution_price=F("execution_trade__price"),
|
|
254
|
+
execution_traded_shares=F("execution_trade__shares"),
|
|
255
|
+
)
|
|
256
|
+
.select_related(
|
|
257
|
+
"underlying_instrument", "underlying_instrument__parent", "underlying_instrument__parent__parent"
|
|
258
|
+
)
|
|
224
259
|
)
|
|
260
|
+
|
|
261
|
+
@action(detail=True, methods=["PUT"], permission_classes=[IsPortfolioManager])
|
|
262
|
+
def changeexecutioninstruction(self, request, pk=None, order_proposal_id=None, **kwargs):
|
|
263
|
+
serializer = ExecutionInstructionSerializer(data=request.data)
|
|
264
|
+
order_proposal = get_object_or_404(OrderProposal, pk=order_proposal_id)
|
|
265
|
+
if serializer.is_valid(raise_exception=True):
|
|
266
|
+
parameters = dict(serializer.data)
|
|
267
|
+
orders_to_update = order_proposal.orders.all()
|
|
268
|
+
execution_instruction = parameters.pop("execution_instruction")
|
|
269
|
+
apply_execution_instruction_to_all_orders = parameters.pop("apply_execution_instruction_to_all_orders")
|
|
270
|
+
execution_parameters = {k: v for k, v in parameters.items() if v}
|
|
271
|
+
if not apply_execution_instruction_to_all_orders:
|
|
272
|
+
orders_to_update = orders_to_update.filter(id=pk)
|
|
273
|
+
orders_to_update.update(
|
|
274
|
+
execution_instruction=execution_instruction, execution_instruction_parameters=execution_parameters
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return Response({"send": True})
|
|
@@ -2,6 +2,7 @@ from contextlib import suppress
|
|
|
2
2
|
from datetime import date, datetime
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
|
+
from celery import chain
|
|
5
6
|
from django.db.models import OuterRef, Q, Subquery, Sum
|
|
6
7
|
from django.http import HttpResponse
|
|
7
8
|
from django.shortcuts import get_object_or_404
|
|
@@ -14,6 +15,7 @@ from rest_framework.reverse import reverse
|
|
|
14
15
|
from wbcore import viewsets
|
|
15
16
|
from wbcore.contrib.currency.models import Currency
|
|
16
17
|
from wbcore.contrib.io.viewsets import ExportPandasAPIViewSet
|
|
18
|
+
from wbcore.contrib.notifications.dispatch import send_notification_as_task
|
|
17
19
|
from wbcore.pandas import fields as pf
|
|
18
20
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
19
21
|
from wbfdm.models import Instrument
|
|
@@ -35,6 +37,8 @@ from wbportfolio.serializers import (
|
|
|
35
37
|
)
|
|
36
38
|
|
|
37
39
|
from ..models.graphs.portfolio import PortfolioGraph
|
|
40
|
+
from ..models.utils import adjust_quote_as_task
|
|
41
|
+
from ..permissions import IsPortfolioManager
|
|
38
42
|
from .configs import (
|
|
39
43
|
PortfolioButtonConfig,
|
|
40
44
|
PortfolioDisplayConfig,
|
|
@@ -178,6 +182,26 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
178
182
|
|
|
179
183
|
return HttpResponse("Bad arguments", status=400)
|
|
180
184
|
|
|
185
|
+
@action(detail=False, methods=["POST"], permission_classes=[IsPortfolioManager])
|
|
186
|
+
def adjustquote(self, request, pk=None):
|
|
187
|
+
old_quote = get_object_or_404(Instrument, pk=request.POST["old_quote"])
|
|
188
|
+
new_quote = get_object_or_404(Instrument, pk=request.POST["new_quote"])
|
|
189
|
+
adjust_after = parse_date(request.data["adjust_after"]) if "adjust_after" in request.data else None
|
|
190
|
+
only_portfolio_ids = request.data["only_portfolios"].split(",") if "only_portfolios" in request.data else []
|
|
191
|
+
|
|
192
|
+
chain(
|
|
193
|
+
adjust_quote_as_task.si(
|
|
194
|
+
old_quote.id, new_quote.id, adjust_after=adjust_after, only_portfolio_ids=only_portfolio_ids
|
|
195
|
+
),
|
|
196
|
+
send_notification_as_task.si(
|
|
197
|
+
"wbportfolio.portfolio.action_done",
|
|
198
|
+
f"Quote adjustment from {old_quote} to {new_quote} is done",
|
|
199
|
+
"The associated positions and orders were successfully adjusted",
|
|
200
|
+
request.user.id,
|
|
201
|
+
),
|
|
202
|
+
).apply_async()
|
|
203
|
+
return HttpResponse("Ok", status=200)
|
|
204
|
+
|
|
181
205
|
|
|
182
206
|
class PortfolioPortfolioThroughModelViewSet(InternalUserPermissionMixin, viewsets.ModelViewSet):
|
|
183
207
|
serializer_class = PortfolioPortfolioThroughModelSerializer
|