wbportfolio 1.54.14__py2.py3-none-any.whl → 1.54.15__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/__init__.py +2 -0
- wbportfolio/admin/orders/__init__.py +2 -0
- wbportfolio/admin/orders/order_proposals.py +14 -0
- wbportfolio/admin/orders/orders.py +30 -0
- wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
- wbportfolio/admin/transactions/__init__.py +0 -1
- wbportfolio/admin/transactions/trades.py +2 -17
- wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
- wbportfolio/factories/__init__.py +2 -1
- wbportfolio/factories/orders/__init__.py +2 -0
- wbportfolio/factories/orders/order_proposals.py +17 -0
- wbportfolio/factories/orders/orders.py +21 -0
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/factories/trades.py +2 -13
- wbportfolio/filters/orders/__init__.py +1 -0
- wbportfolio/filters/orders/orders.py +11 -0
- wbportfolio/import_export/handlers/trade.py +20 -20
- wbportfolio/import_export/resources/trades.py +2 -2
- wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
- wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
- wbportfolio/models/__init__.py +2 -0
- wbportfolio/models/orders/__init__.py +2 -0
- wbportfolio/models/{transactions/trade_proposals.py → orders/order_proposals.py} +288 -244
- wbportfolio/models/orders/orders.py +243 -0
- wbportfolio/models/portfolio.py +16 -19
- wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +18 -18
- wbportfolio/models/transactions/__init__.py +0 -2
- wbportfolio/models/transactions/trades.py +10 -450
- wbportfolio/pms/analytics/portfolio.py +10 -6
- wbportfolio/pms/analytics/utils.py +9 -0
- wbportfolio/pms/trading/handler.py +6 -4
- wbportfolio/pms/typing.py +18 -7
- wbportfolio/rebalancing/decorators.py +1 -1
- wbportfolio/rebalancing/models/composite.py +3 -7
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +3 -1
- wbportfolio/serializers/__init__.py +1 -0
- wbportfolio/serializers/orders/__init__.py +2 -0
- wbportfolio/serializers/{transactions/trade_proposals.py → orders/order_proposals.py} +23 -15
- wbportfolio/serializers/orders/orders.py +187 -0
- wbportfolio/serializers/portfolios.py +7 -7
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/serializers/transactions/__init__.py +1 -5
- wbportfolio/serializers/transactions/trades.py +1 -182
- wbportfolio/tests/conftest.py +4 -2
- wbportfolio/tests/models/orders/__init__.py +0 -0
- wbportfolio/tests/models/{transactions/test_trade_proposals.py → orders/test_order_proposals.py} +218 -246
- wbportfolio/tests/models/test_portfolios.py +11 -10
- wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
- wbportfolio/tests/models/transactions/test_trades.py +0 -20
- wbportfolio/tests/rebalancing/test_models.py +24 -28
- wbportfolio/tests/signals.py +10 -10
- wbportfolio/tests/tests.py +1 -1
- wbportfolio/urls.py +7 -7
- wbportfolio/viewsets/__init__.py +2 -0
- wbportfolio/viewsets/configs/buttons/__init__.py +2 -3
- wbportfolio/viewsets/configs/buttons/trades.py +0 -8
- wbportfolio/viewsets/configs/display/__init__.py +0 -2
- wbportfolio/viewsets/configs/display/portfolios.py +5 -5
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/display/trades.py +1 -225
- wbportfolio/viewsets/configs/endpoints/__init__.py +0 -3
- wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
- wbportfolio/viewsets/orders/__init__.py +6 -0
- wbportfolio/viewsets/orders/configs/__init__.py +4 -0
- wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
- wbportfolio/viewsets/{configs/buttons/trade_proposals.py → orders/configs/buttons/order_proposals.py} +21 -21
- wbportfolio/viewsets/orders/configs/buttons/orders.py +9 -0
- wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
- wbportfolio/viewsets/{configs/display/trade_proposals.py → orders/configs/displays/order_proposals.py} +21 -21
- wbportfolio/viewsets/orders/configs/displays/orders.py +180 -0
- wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +26 -0
- wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
- wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
- wbportfolio/viewsets/{transactions/trade_proposals.py → orders/order_proposals.py} +46 -46
- wbportfolio/viewsets/orders/orders.py +219 -0
- wbportfolio/viewsets/portfolios.py +12 -12
- wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
- wbportfolio/viewsets/transactions/__init__.py +1 -7
- wbportfolio/viewsets/transactions/trades.py +1 -199
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.15.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.15.dist-info}/RECORD +85 -58
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.15.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from django.shortcuts import get_object_or_404
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
from wbcore.contrib.color.enums import WBColor
|
|
6
|
+
from wbcore.enums import Unit
|
|
7
|
+
from wbcore.metadata.configs import display as dp
|
|
8
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
9
|
+
Display,
|
|
10
|
+
create_simple_display,
|
|
11
|
+
)
|
|
12
|
+
from wbcore.metadata.configs.display.instance_display.utils import repeat_field
|
|
13
|
+
from wbcore.metadata.configs.display.view_config import DisplayViewConfig
|
|
14
|
+
|
|
15
|
+
from wbportfolio.models import Order, OrderProposal
|
|
16
|
+
|
|
17
|
+
ORDER_STATUS_LEGENDS = dp.Legend(
|
|
18
|
+
key="has_warnings",
|
|
19
|
+
items=[
|
|
20
|
+
dp.LegendItem(icon=WBColor.YELLOW_DARK.value, label=_("Warning"), value=True),
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
ORDER_STATUS_FORMATTING = dp.Formatting(
|
|
25
|
+
column="has_warnings",
|
|
26
|
+
formatting_rules=[
|
|
27
|
+
dp.FormattingRule(
|
|
28
|
+
style={"backgroundColor": WBColor.YELLOW_DARK.value},
|
|
29
|
+
condition=("==", True),
|
|
30
|
+
)
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
ORDER_TYPE_FORMATTING_RULES = [
|
|
34
|
+
dp.FormattingRule(
|
|
35
|
+
style={"color": WBColor.RED_DARK.value, "fontWeight": "bold"},
|
|
36
|
+
condition=("==", Order.Type.SELL.name),
|
|
37
|
+
),
|
|
38
|
+
dp.FormattingRule(
|
|
39
|
+
style={"color": WBColor.RED.value, "fontWeight": "bold"},
|
|
40
|
+
condition=("==", Order.Type.DECREASE.name),
|
|
41
|
+
),
|
|
42
|
+
dp.FormattingRule(
|
|
43
|
+
style={"color": WBColor.GREEN.value, "fontWeight": "bold"},
|
|
44
|
+
condition=("==", Order.Type.INCREASE.name),
|
|
45
|
+
),
|
|
46
|
+
dp.FormattingRule(
|
|
47
|
+
style={"color": WBColor.GREEN_DARK.value, "fontWeight": "bold"},
|
|
48
|
+
condition=("==", Order.Type.BUY.name),
|
|
49
|
+
),
|
|
50
|
+
dp.FormattingRule(
|
|
51
|
+
style={"color": WBColor.GREY.value, "fontWeight": "bold"},
|
|
52
|
+
condition=("==", Order.Type.NO_CHANGE.name),
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
VALUE_FORMATTING_RULES = [
|
|
57
|
+
dp.FormattingRule(
|
|
58
|
+
style={"color": WBColor.RED_DARK.value, "fontWeight": "bold"},
|
|
59
|
+
condition=("<", 0),
|
|
60
|
+
),
|
|
61
|
+
dp.FormattingRule(
|
|
62
|
+
style={"color": WBColor.GREEN_DARK.value, "fontWeight": "bold"},
|
|
63
|
+
condition=(">", 0),
|
|
64
|
+
),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class OrderOrderProposalDisplayConfig(DisplayViewConfig):
|
|
69
|
+
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
70
|
+
order_proposal = get_object_or_404(OrderProposal, pk=self.view.kwargs.get("order_proposal_id", None))
|
|
71
|
+
fields = [
|
|
72
|
+
dp.Field(
|
|
73
|
+
label="Instrument",
|
|
74
|
+
open_by_default=True,
|
|
75
|
+
key=None,
|
|
76
|
+
children=[
|
|
77
|
+
dp.Field(key="underlying_instrument", label="Name", width=Unit.PIXEL(250)),
|
|
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)),
|
|
80
|
+
dp.Field(
|
|
81
|
+
key="underlying_instrument_refinitiv_identifier_code", label="RIC", width=Unit.PIXEL(100)
|
|
82
|
+
),
|
|
83
|
+
dp.Field(key="underlying_instrument_instrument_type", label="Asset Class", width=Unit.PIXEL(125)),
|
|
84
|
+
],
|
|
85
|
+
),
|
|
86
|
+
dp.Field(
|
|
87
|
+
label="Weight",
|
|
88
|
+
open_by_default=False,
|
|
89
|
+
key=None,
|
|
90
|
+
children=[
|
|
91
|
+
dp.Field(key="effective_weight", label="Effective Weight", show="open", width=Unit.PIXEL(150)),
|
|
92
|
+
dp.Field(key="target_weight", label="Target Weight", show="open", width=Unit.PIXEL(150)),
|
|
93
|
+
dp.Field(
|
|
94
|
+
key="weighting",
|
|
95
|
+
label="Delta Weight",
|
|
96
|
+
formatting_rules=VALUE_FORMATTING_RULES,
|
|
97
|
+
width=Unit.PIXEL(150),
|
|
98
|
+
),
|
|
99
|
+
],
|
|
100
|
+
),
|
|
101
|
+
]
|
|
102
|
+
if not order_proposal.portfolio.only_weighting:
|
|
103
|
+
fields.append(
|
|
104
|
+
dp.Field(
|
|
105
|
+
label="Shares",
|
|
106
|
+
open_by_default=False,
|
|
107
|
+
key=None,
|
|
108
|
+
children=[
|
|
109
|
+
dp.Field(key="effective_shares", label="Effective Shares", show="open", width=Unit.PIXEL(150)),
|
|
110
|
+
dp.Field(key="target_shares", label="Target Shares", show="open", width=Unit.PIXEL(150)),
|
|
111
|
+
dp.Field(
|
|
112
|
+
key="shares",
|
|
113
|
+
label="Shares",
|
|
114
|
+
formatting_rules=VALUE_FORMATTING_RULES,
|
|
115
|
+
width=Unit.PIXEL(150),
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
fields.append(
|
|
121
|
+
dp.Field(
|
|
122
|
+
label="Total Value",
|
|
123
|
+
open_by_default=False,
|
|
124
|
+
key=None,
|
|
125
|
+
children=[
|
|
126
|
+
dp.Field(
|
|
127
|
+
key="effective_total_value_fx_portfolio",
|
|
128
|
+
label="Effective Total Value",
|
|
129
|
+
show="open",
|
|
130
|
+
width=Unit.PIXEL(150),
|
|
131
|
+
),
|
|
132
|
+
dp.Field(
|
|
133
|
+
key="target_total_value_fx_portfolio",
|
|
134
|
+
label="Target Total Value",
|
|
135
|
+
show="open",
|
|
136
|
+
width=Unit.PIXEL(150),
|
|
137
|
+
),
|
|
138
|
+
dp.Field(
|
|
139
|
+
key="total_value_fx_portfolio",
|
|
140
|
+
label="Total Value",
|
|
141
|
+
formatting_rules=VALUE_FORMATTING_RULES,
|
|
142
|
+
width=Unit.PIXEL(150),
|
|
143
|
+
),
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
fields.append(
|
|
148
|
+
dp.Field(
|
|
149
|
+
label="Information",
|
|
150
|
+
open_by_default=False,
|
|
151
|
+
key=None,
|
|
152
|
+
children=[
|
|
153
|
+
dp.Field(
|
|
154
|
+
key="order_type",
|
|
155
|
+
label="Direction",
|
|
156
|
+
formatting_rules=ORDER_TYPE_FORMATTING_RULES,
|
|
157
|
+
width=Unit.PIXEL(125),
|
|
158
|
+
),
|
|
159
|
+
dp.Field(key="comment", label="Comment", width=Unit.PIXEL(250)),
|
|
160
|
+
dp.Field(key="order", label="Order", show="open", width=Unit.PIXEL(100)),
|
|
161
|
+
],
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return dp.ListDisplay(
|
|
165
|
+
fields=fields,
|
|
166
|
+
legends=[ORDER_STATUS_LEGENDS],
|
|
167
|
+
formatting=[ORDER_STATUS_FORMATTING],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def get_instance_display(self) -> Display:
|
|
171
|
+
order_proposal = get_object_or_404(OrderProposal, pk=self.view.kwargs.get("order_proposal_id", None))
|
|
172
|
+
|
|
173
|
+
fields = [
|
|
174
|
+
["company", "security", "underlying_instrument"],
|
|
175
|
+
["effective_weight", "target_weight", "weighting"],
|
|
176
|
+
]
|
|
177
|
+
if not order_proposal.portfolio.only_weighting:
|
|
178
|
+
fields.append(["effective_shares", "target_shares", "shares"])
|
|
179
|
+
fields.append([repeat_field(3, "comment")])
|
|
180
|
+
return create_simple_display(fields)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.shortcuts import get_object_or_404
|
|
2
|
+
from rest_framework.reverse import reverse
|
|
3
|
+
from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
4
|
+
|
|
5
|
+
from wbportfolio.models import OrderProposal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OrderProposalEndpointConfig(EndpointViewConfig):
|
|
9
|
+
def get_delete_endpoint(self, **kwargs):
|
|
10
|
+
if pk := self.view.kwargs.get("pk", None):
|
|
11
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
12
|
+
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
13
|
+
return super().get_endpoint()
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OrderProposalPortfolioEndpointConfig(OrderProposalEndpointConfig):
|
|
18
|
+
def get_endpoint(self, **kwargs):
|
|
19
|
+
return reverse(
|
|
20
|
+
"wbportfolio:portfolio-orderproposal-list", args=[self.view.kwargs["portfolio_id"]], request=self.request
|
|
21
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
|
|
3
|
+
from rest_framework.reverse import reverse
|
|
4
|
+
from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
5
|
+
|
|
6
|
+
from wbportfolio.models import OrderProposal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OrderOrderProposalEndpointConfig(EndpointViewConfig):
|
|
10
|
+
def get_endpoint(self, **kwargs):
|
|
11
|
+
if order_proposal_id := self.view.kwargs.get("order_proposal_id", None):
|
|
12
|
+
order_proposal = OrderProposal.objects.get(id=order_proposal_id)
|
|
13
|
+
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
14
|
+
return reverse(
|
|
15
|
+
"wbportfolio:orderproposal-order-list",
|
|
16
|
+
args=[self.view.kwargs["order_proposal_id"]],
|
|
17
|
+
request=self.request,
|
|
18
|
+
)
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def get_delete_endpoint(self, **kwargs):
|
|
22
|
+
with suppress(AttributeError, AssertionError):
|
|
23
|
+
order = self.view.get_object()
|
|
24
|
+
if order._effective_weight: # we make sure order with a valid effective position cannot be deleted
|
|
25
|
+
return None
|
|
26
|
+
return super().get_delete_endpoint(**kwargs)
|
|
File without changes
|
|
File without changes
|
|
@@ -19,49 +19,49 @@ 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,
|
|
23
|
-
from wbportfolio.models.
|
|
22
|
+
from wbportfolio.models import AssetPosition, OrderProposal
|
|
23
|
+
from wbportfolio.models.orders.order_proposals import (
|
|
24
24
|
replay_as_task,
|
|
25
25
|
)
|
|
26
26
|
from wbportfolio.serializers import (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
OrderProposalModelSerializer,
|
|
28
|
+
OrderProposalRepresentationSerializer,
|
|
29
|
+
ReadOnlyOrderProposalModelSerializer,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
from ..configs import (
|
|
33
|
-
TradeProposalButtonConfig,
|
|
34
|
-
TradeProposalDisplayConfig,
|
|
35
|
-
TradeProposalEndpointConfig,
|
|
36
|
-
TradeProposalPortfolioEndpointConfig,
|
|
37
|
-
)
|
|
38
32
|
from ..mixins import UserPortfolioRequestPermissionMixin
|
|
33
|
+
from .configs import (
|
|
34
|
+
OrderProposalButtonConfig,
|
|
35
|
+
OrderProposalDisplayConfig,
|
|
36
|
+
OrderProposalEndpointConfig,
|
|
37
|
+
OrderProposalPortfolioEndpointConfig,
|
|
38
|
+
)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
class
|
|
41
|
+
class OrderProposalRepresentationViewSet(InternalUserPermissionMixin, viewsets.RepresentationViewSet):
|
|
42
42
|
IDENTIFIER = "wbportfolio:trade"
|
|
43
|
-
queryset =
|
|
44
|
-
serializer_class =
|
|
43
|
+
queryset = OrderProposal.objects.all()
|
|
44
|
+
serializer_class = OrderProposalRepresentationSerializer
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class
|
|
47
|
+
class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserPermissionMixin, viewsets.ModelViewSet):
|
|
48
48
|
ordering_fields = ("trade_date",)
|
|
49
49
|
ordering = ("-trade_date",)
|
|
50
50
|
search_fields = ("comment",)
|
|
51
51
|
filterset_fields = {"trade_date": ["exact", "gte", "lte"], "status": ["exact"]}
|
|
52
52
|
|
|
53
|
-
queryset =
|
|
54
|
-
serializer_class =
|
|
55
|
-
display_config_class =
|
|
56
|
-
button_config_class =
|
|
57
|
-
endpoint_config_class =
|
|
53
|
+
queryset = OrderProposal.objects.select_related("rebalancing_model", "portfolio")
|
|
54
|
+
serializer_class = OrderProposalModelSerializer
|
|
55
|
+
display_config_class = OrderProposalDisplayConfig
|
|
56
|
+
button_config_class = OrderProposalButtonConfig
|
|
57
|
+
endpoint_config_class = OrderProposalEndpointConfig
|
|
58
58
|
|
|
59
59
|
def get_serializer_class(self):
|
|
60
60
|
if self.new_mode or (
|
|
61
|
-
"pk" in self.kwargs and (obj := self.get_object()) and obj.status ==
|
|
61
|
+
"pk" in self.kwargs and (obj := self.get_object()) and obj.status == OrderProposal.Status.DRAFT
|
|
62
62
|
):
|
|
63
|
-
return
|
|
64
|
-
return
|
|
63
|
+
return OrderProposalModelSerializer
|
|
64
|
+
return ReadOnlyOrderProposalModelSerializer
|
|
65
65
|
|
|
66
66
|
# 2 methods to parametrize the clone button functionality
|
|
67
67
|
def get_clone_button_serializer_class(self, instance):
|
|
@@ -82,13 +82,13 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
def add_messages(self, request, instance=None, **kwargs):
|
|
85
|
-
if instance and instance.status ==
|
|
85
|
+
if instance and instance.status == OrderProposal.Status.SUBMIT:
|
|
86
86
|
if not instance.portfolio.is_manageable:
|
|
87
|
-
info(request, "This
|
|
87
|
+
info(request, "This order proposal cannot be approved the portfolio is considered unmanaged.")
|
|
88
88
|
if instance.has_non_successful_checks:
|
|
89
89
|
warning(
|
|
90
90
|
request,
|
|
91
|
-
"This
|
|
91
|
+
"This order proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid order proposal",
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
@classmethod
|
|
@@ -97,41 +97,41 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
97
97
|
|
|
98
98
|
@action(detail=True, methods=["PATCH"])
|
|
99
99
|
def reset(self, request, pk=None):
|
|
100
|
-
|
|
101
|
-
if
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
101
|
+
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
102
|
+
order_proposal.orders.all().update(weighting=0)
|
|
103
|
+
order_proposal.reset_orders()
|
|
104
104
|
return Response({"send": True})
|
|
105
|
-
return Response({"status": "
|
|
105
|
+
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
106
106
|
|
|
107
107
|
@action(detail=True, methods=["PATCH"])
|
|
108
108
|
def normalize(self, request, pk=None):
|
|
109
|
-
|
|
109
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
110
110
|
total_cash_weight = Decimal(request.data.get("total_cash_weight", Decimal("0.0")))
|
|
111
|
-
if
|
|
112
|
-
|
|
111
|
+
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
112
|
+
order_proposal.normalize_orders(total_target_weight=Decimal("1.0") - total_cash_weight)
|
|
113
113
|
return Response({"send": True})
|
|
114
|
-
return Response({"status": "
|
|
114
|
+
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
115
115
|
|
|
116
116
|
@action(detail=True, methods=["PATCH"])
|
|
117
117
|
def replay(self, request, pk=None):
|
|
118
|
-
|
|
119
|
-
if
|
|
120
|
-
replay_as_task.delay(
|
|
118
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
119
|
+
if order_proposal.portfolio.is_manageable:
|
|
120
|
+
replay_as_task.delay(order_proposal.id, user_id=self.request.user.id)
|
|
121
121
|
return Response({"send": True})
|
|
122
|
-
return Response({"status": "
|
|
122
|
+
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
123
123
|
|
|
124
124
|
@action(detail=True, methods=["PATCH"])
|
|
125
125
|
def deleteall(self, request, pk=None):
|
|
126
|
-
|
|
127
|
-
if
|
|
128
|
-
|
|
126
|
+
order_proposal = get_object_or_404(OrderProposal, pk=pk)
|
|
127
|
+
if order_proposal.status == OrderProposal.Status.DRAFT:
|
|
128
|
+
order_proposal.orders.all().delete()
|
|
129
129
|
return Response({"send": True})
|
|
130
|
-
return Response({"status": "
|
|
130
|
+
return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
class
|
|
134
|
-
endpoint_config_class =
|
|
133
|
+
class OrderProposalPortfolioModelViewSet(UserPortfolioRequestPermissionMixin, OrderProposalModelViewSet):
|
|
134
|
+
endpoint_config_class = OrderProposalPortfolioEndpointConfig
|
|
135
135
|
|
|
136
136
|
@cached_property
|
|
137
137
|
def default_trade_date(self) -> date | None:
|
|
@@ -139,4 +139,4 @@ class TradeProposalPortfolioModelViewSet(UserPortfolioRequestPermissionMixin, Tr
|
|
|
139
139
|
return (self.portfolio.assets.latest("date").date + BDay(1)).date()
|
|
140
140
|
|
|
141
141
|
def get_queryset(self):
|
|
142
|
-
return
|
|
142
|
+
return OrderProposal.objects.filter(portfolio=self.kwargs["portfolio_id"])
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from django.contrib.messages import error, warning
|
|
4
|
+
from django.db.models import (
|
|
5
|
+
Case,
|
|
6
|
+
F,
|
|
7
|
+
Sum,
|
|
8
|
+
Value,
|
|
9
|
+
When,
|
|
10
|
+
)
|
|
11
|
+
from django.shortcuts import get_object_or_404
|
|
12
|
+
from django.utils.functional import cached_property
|
|
13
|
+
from wbcore import viewsets
|
|
14
|
+
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
15
|
+
from wbcore.utils.strings import format_number
|
|
16
|
+
from wbcore.viewsets.mixins import OrderableMixin
|
|
17
|
+
|
|
18
|
+
from wbportfolio.import_export.resources.trades import OrderProposalTradeResource
|
|
19
|
+
from wbportfolio.models import Order, OrderProposal
|
|
20
|
+
from wbportfolio.serializers import (
|
|
21
|
+
OrderOrderProposalListModelSerializer,
|
|
22
|
+
OrderOrderProposalModelSerializer,
|
|
23
|
+
ReadOnlyOrderOrderProposalModelSerializer,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from ...filters.orders import OrderFilterSet
|
|
27
|
+
from ..mixins import UserPortfolioRequestPermissionMixin
|
|
28
|
+
from .configs import (
|
|
29
|
+
OrderOrderProposalButtonConfig,
|
|
30
|
+
OrderOrderProposalDisplayConfig,
|
|
31
|
+
OrderOrderProposalEndpointConfig,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OrderOrderProposalModelViewSet(
|
|
36
|
+
UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, OrderableMixin, viewsets.ModelViewSet
|
|
37
|
+
):
|
|
38
|
+
IMPORT_ALLOWED = True
|
|
39
|
+
ordering = (
|
|
40
|
+
"order_proposal",
|
|
41
|
+
"order",
|
|
42
|
+
)
|
|
43
|
+
ordering_fields = (
|
|
44
|
+
"underlying_instrument__name",
|
|
45
|
+
"underlying_instrument_isin",
|
|
46
|
+
"underlying_instrument_ticker",
|
|
47
|
+
"underlying_instrument_refinitiv_identifier_code",
|
|
48
|
+
"underlying_instrument_instrument_type",
|
|
49
|
+
"target_weight",
|
|
50
|
+
"effective_weight",
|
|
51
|
+
"effective_shares",
|
|
52
|
+
"target_shares",
|
|
53
|
+
"shares",
|
|
54
|
+
"weighting",
|
|
55
|
+
)
|
|
56
|
+
IDENTIFIER = "wbportfolio:orderproposal"
|
|
57
|
+
search_fields = ("underlying_instrument__name",)
|
|
58
|
+
queryset = Order.objects.none()
|
|
59
|
+
filterset_class = OrderFilterSet
|
|
60
|
+
|
|
61
|
+
display_config_class = OrderOrderProposalDisplayConfig
|
|
62
|
+
endpoint_config_class = OrderOrderProposalEndpointConfig
|
|
63
|
+
serializer_class = OrderOrderProposalModelSerializer
|
|
64
|
+
button_config_class = OrderOrderProposalButtonConfig
|
|
65
|
+
|
|
66
|
+
@cached_property
|
|
67
|
+
def order_proposal(self):
|
|
68
|
+
return get_object_or_404(OrderProposal, pk=self.kwargs["order_proposal_id"])
|
|
69
|
+
|
|
70
|
+
@cached_property
|
|
71
|
+
def portfolio_total_asset_value(self):
|
|
72
|
+
return self.order_proposal.portfolio_total_asset_value
|
|
73
|
+
|
|
74
|
+
def has_import_permission(self, request) -> bool: # allow import only on draft order proposal
|
|
75
|
+
return super().has_import_permission(request) and self.order_proposal.status == OrderProposal.Status.DRAFT
|
|
76
|
+
|
|
77
|
+
def get_import_resource_kwargs(self):
|
|
78
|
+
resource_kwargs = super().get_import_resource_kwargs()
|
|
79
|
+
resource_kwargs["columns_mapping"] = {"underlying_instrument": "underlying_instrument__isin"}
|
|
80
|
+
return resource_kwargs
|
|
81
|
+
|
|
82
|
+
def get_resource_class(self):
|
|
83
|
+
return OrderProposalTradeResource
|
|
84
|
+
|
|
85
|
+
def get_aggregates(self, queryset, *args, **kwargs):
|
|
86
|
+
agg = {}
|
|
87
|
+
if queryset.exists():
|
|
88
|
+
noncash_aggregates = queryset.filter(underlying_instrument__is_cash=False).aggregate(
|
|
89
|
+
sum_target_weight=Sum(F("target_weight")),
|
|
90
|
+
sum_effective_weight=Sum(F("effective_weight")),
|
|
91
|
+
sum_target_total_value_fx_portfolio=Sum(F("target_total_value_fx_portfolio")),
|
|
92
|
+
sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# weights aggregates
|
|
96
|
+
cash_sum_effective_weight = Decimal("1.0") - noncash_aggregates["sum_effective_weight"]
|
|
97
|
+
cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
|
|
98
|
+
noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
99
|
+
noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
|
|
100
|
+
sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
101
|
+
sum_sell_weight = queryset.filter(weighting__lt=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
102
|
+
|
|
103
|
+
# shares aggregates
|
|
104
|
+
cash_sum_effective_total_value_fx_portfolio = cash_sum_effective_weight * self.portfolio_total_asset_value
|
|
105
|
+
cash_sum_target_total_value_fx_portfolio = cash_sum_target_cash_weight * self.portfolio_total_asset_value
|
|
106
|
+
noncash_sum_effective_total_value_fx_portfolio = noncash_aggregates[
|
|
107
|
+
"sum_effective_total_value_fx_portfolio"
|
|
108
|
+
] or Decimal(0)
|
|
109
|
+
noncash_sum_target_total_value_fx_portfolio = noncash_aggregates[
|
|
110
|
+
"sum_target_total_value_fx_portfolio"
|
|
111
|
+
] or Decimal(0)
|
|
112
|
+
sum_buy_total_value_fx_portfolio = queryset.filter(total_value_fx_portfolio__gte=0).aggregate(
|
|
113
|
+
s=Sum(F("total_value_fx_portfolio"))
|
|
114
|
+
)["s"] or Decimal(0)
|
|
115
|
+
sum_sell_total_value_fx_portfolio = queryset.filter(total_value_fx_portfolio__lt=0).aggregate(
|
|
116
|
+
s=Sum(F("total_value_fx_portfolio"))
|
|
117
|
+
)["s"] or Decimal(0)
|
|
118
|
+
|
|
119
|
+
agg = {
|
|
120
|
+
"effective_weight": {
|
|
121
|
+
"Cash": format_number(cash_sum_effective_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
122
|
+
"Non-Cash": format_number(noncash_sum_effective_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
123
|
+
"Total": format_number(
|
|
124
|
+
noncash_sum_effective_weight + cash_sum_effective_weight,
|
|
125
|
+
decimal=Order.ORDER_WEIGHTING_PRECISION,
|
|
126
|
+
),
|
|
127
|
+
},
|
|
128
|
+
"target_weight": {
|
|
129
|
+
"Cash": format_number(cash_sum_target_cash_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
130
|
+
"Non-Cash": format_number(noncash_sum_target_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
131
|
+
"Total": format_number(
|
|
132
|
+
cash_sum_target_cash_weight + noncash_sum_target_weight,
|
|
133
|
+
decimal=Order.ORDER_WEIGHTING_PRECISION,
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
"effective_total_value_fx_portfolio": {
|
|
137
|
+
"Cash": format_number(cash_sum_effective_total_value_fx_portfolio, decimal=6),
|
|
138
|
+
"Non-Cash": format_number(noncash_sum_effective_total_value_fx_portfolio, decimal=6),
|
|
139
|
+
"Total": format_number(
|
|
140
|
+
cash_sum_effective_total_value_fx_portfolio + noncash_sum_effective_total_value_fx_portfolio,
|
|
141
|
+
decimal=6,
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
"target_total_value_fx_portfolio": {
|
|
145
|
+
"Cash": format_number(cash_sum_target_total_value_fx_portfolio, decimal=6),
|
|
146
|
+
"Non-Cash": format_number(noncash_sum_target_total_value_fx_portfolio, decimal=6),
|
|
147
|
+
"Total": format_number(
|
|
148
|
+
cash_sum_target_total_value_fx_portfolio + noncash_sum_target_total_value_fx_portfolio,
|
|
149
|
+
decimal=6,
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
"weighting": {
|
|
153
|
+
"Cash Flow": format_number(
|
|
154
|
+
cash_sum_target_cash_weight - cash_sum_effective_weight,
|
|
155
|
+
decimal=Order.ORDER_WEIGHTING_PRECISION,
|
|
156
|
+
),
|
|
157
|
+
"Buy": format_number(sum_buy_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
158
|
+
"Sell": format_number(sum_sell_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
159
|
+
},
|
|
160
|
+
"total_value_fx_portfolio": {
|
|
161
|
+
"Cash Flow": format_number(
|
|
162
|
+
cash_sum_target_total_value_fx_portfolio - cash_sum_effective_total_value_fx_portfolio,
|
|
163
|
+
decimal=6,
|
|
164
|
+
),
|
|
165
|
+
"Buy": format_number(sum_buy_total_value_fx_portfolio, decimal=6),
|
|
166
|
+
"Sell": format_number(sum_sell_total_value_fx_portfolio, decimal=6),
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return agg
|
|
171
|
+
|
|
172
|
+
def get_serializer_class(self):
|
|
173
|
+
if self.order_proposal.status != OrderProposal.Status.DRAFT:
|
|
174
|
+
return ReadOnlyOrderOrderProposalModelSerializer
|
|
175
|
+
elif not self.new_mode and "pk" not in self.kwargs:
|
|
176
|
+
return OrderOrderProposalListModelSerializer
|
|
177
|
+
return OrderOrderProposalModelSerializer
|
|
178
|
+
|
|
179
|
+
def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
|
|
180
|
+
if queryset is not None and queryset.exists() and self.order_proposal.status != OrderProposal.Status.APPROVED:
|
|
181
|
+
total_target_weight = queryset.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
|
|
182
|
+
if round(total_target_weight, 8) != 1:
|
|
183
|
+
warning(
|
|
184
|
+
request,
|
|
185
|
+
"The total target weight does not equal 1. To avoid automatic cash allocation, please adjust the order weights to sum up to 1. Otherwise, a cash component will be added when this order proposal is submitted.",
|
|
186
|
+
)
|
|
187
|
+
if queryset.filter(has_warnings=True).exists():
|
|
188
|
+
error(
|
|
189
|
+
request,
|
|
190
|
+
"Some orders failed preparation. To resolve this, please revert the order proposal to draft, review and correct the orders, and then resubmit.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@cached_property
|
|
194
|
+
def orders(self):
|
|
195
|
+
if self.is_portfolio_manager:
|
|
196
|
+
return self.order_proposal.get_orders()
|
|
197
|
+
else:
|
|
198
|
+
return Order.objects.none()
|
|
199
|
+
|
|
200
|
+
def get_queryset(self):
|
|
201
|
+
return self.orders.annotate(
|
|
202
|
+
underlying_instrument_isin=F("underlying_instrument__isin"),
|
|
203
|
+
underlying_instrument_ticker=F("underlying_instrument__ticker"),
|
|
204
|
+
underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
|
|
205
|
+
underlying_instrument_instrument_type=Case(
|
|
206
|
+
When(
|
|
207
|
+
underlying_instrument__parent__is_security=True,
|
|
208
|
+
then=F("underlying_instrument__parent__instrument_type__short_name"),
|
|
209
|
+
),
|
|
210
|
+
default=F("underlying_instrument__instrument_type__short_name"),
|
|
211
|
+
),
|
|
212
|
+
effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
|
|
213
|
+
target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
|
|
214
|
+
portfolio_currency=F("portfolio__currency__symbol"),
|
|
215
|
+
security=F("underlying_instrument__parent"),
|
|
216
|
+
company=F("underlying_instrument__parent__parent"),
|
|
217
|
+
).select_related(
|
|
218
|
+
"underlying_instrument", "underlying_instrument__parent", "underlying_instrument__parent__parent"
|
|
219
|
+
)
|