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.

Files changed (86) hide show
  1. wbportfolio/admin/__init__.py +2 -0
  2. wbportfolio/admin/orders/__init__.py +2 -0
  3. wbportfolio/admin/orders/order_proposals.py +14 -0
  4. wbportfolio/admin/orders/orders.py +30 -0
  5. wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
  6. wbportfolio/admin/transactions/__init__.py +0 -1
  7. wbportfolio/admin/transactions/trades.py +2 -17
  8. wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
  9. wbportfolio/factories/__init__.py +2 -1
  10. wbportfolio/factories/orders/__init__.py +2 -0
  11. wbportfolio/factories/orders/order_proposals.py +17 -0
  12. wbportfolio/factories/orders/orders.py +21 -0
  13. wbportfolio/factories/rebalancing.py +1 -1
  14. wbportfolio/factories/trades.py +2 -13
  15. wbportfolio/filters/orders/__init__.py +1 -0
  16. wbportfolio/filters/orders/orders.py +11 -0
  17. wbportfolio/import_export/handlers/trade.py +20 -20
  18. wbportfolio/import_export/resources/trades.py +2 -2
  19. wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
  20. wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
  21. wbportfolio/models/__init__.py +2 -0
  22. wbportfolio/models/orders/__init__.py +2 -0
  23. wbportfolio/models/{transactions/trade_proposals.py → orders/order_proposals.py} +288 -244
  24. wbportfolio/models/orders/orders.py +243 -0
  25. wbportfolio/models/portfolio.py +16 -19
  26. wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +18 -18
  27. wbportfolio/models/transactions/__init__.py +0 -2
  28. wbportfolio/models/transactions/trades.py +10 -450
  29. wbportfolio/pms/analytics/portfolio.py +10 -6
  30. wbportfolio/pms/analytics/utils.py +9 -0
  31. wbportfolio/pms/trading/handler.py +6 -4
  32. wbportfolio/pms/typing.py +18 -7
  33. wbportfolio/rebalancing/decorators.py +1 -1
  34. wbportfolio/rebalancing/models/composite.py +3 -7
  35. wbportfolio/rebalancing/models/market_capitalization_weighted.py +3 -1
  36. wbportfolio/serializers/__init__.py +1 -0
  37. wbportfolio/serializers/orders/__init__.py +2 -0
  38. wbportfolio/serializers/{transactions/trade_proposals.py → orders/order_proposals.py} +23 -15
  39. wbportfolio/serializers/orders/orders.py +187 -0
  40. wbportfolio/serializers/portfolios.py +7 -7
  41. wbportfolio/serializers/rebalancing.py +1 -1
  42. wbportfolio/serializers/transactions/__init__.py +1 -5
  43. wbportfolio/serializers/transactions/trades.py +1 -182
  44. wbportfolio/tests/conftest.py +4 -2
  45. wbportfolio/tests/models/orders/__init__.py +0 -0
  46. wbportfolio/tests/models/{transactions/test_trade_proposals.py → orders/test_order_proposals.py} +218 -246
  47. wbportfolio/tests/models/test_portfolios.py +11 -10
  48. wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
  49. wbportfolio/tests/models/transactions/test_trades.py +0 -20
  50. wbportfolio/tests/rebalancing/test_models.py +24 -28
  51. wbportfolio/tests/signals.py +10 -10
  52. wbportfolio/tests/tests.py +1 -1
  53. wbportfolio/urls.py +7 -7
  54. wbportfolio/viewsets/__init__.py +2 -0
  55. wbportfolio/viewsets/configs/buttons/__init__.py +2 -3
  56. wbportfolio/viewsets/configs/buttons/trades.py +0 -8
  57. wbportfolio/viewsets/configs/display/__init__.py +0 -2
  58. wbportfolio/viewsets/configs/display/portfolios.py +5 -5
  59. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  60. wbportfolio/viewsets/configs/display/trades.py +1 -225
  61. wbportfolio/viewsets/configs/endpoints/__init__.py +0 -3
  62. wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
  63. wbportfolio/viewsets/orders/__init__.py +6 -0
  64. wbportfolio/viewsets/orders/configs/__init__.py +4 -0
  65. wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
  66. wbportfolio/viewsets/{configs/buttons/trade_proposals.py → orders/configs/buttons/order_proposals.py} +21 -21
  67. wbportfolio/viewsets/orders/configs/buttons/orders.py +9 -0
  68. wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
  69. wbportfolio/viewsets/{configs/display/trade_proposals.py → orders/configs/displays/order_proposals.py} +21 -21
  70. wbportfolio/viewsets/orders/configs/displays/orders.py +180 -0
  71. wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
  72. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
  73. wbportfolio/viewsets/orders/configs/endpoints/orders.py +26 -0
  74. wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
  75. wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
  76. wbportfolio/viewsets/{transactions/trade_proposals.py → orders/order_proposals.py} +46 -46
  77. wbportfolio/viewsets/orders/orders.py +219 -0
  78. wbportfolio/viewsets/portfolios.py +12 -12
  79. wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
  80. wbportfolio/viewsets/transactions/__init__.py +1 -7
  81. wbportfolio/viewsets/transactions/trades.py +1 -199
  82. {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.15.dist-info}/METADATA +1 -1
  83. {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.15.dist-info}/RECORD +85 -58
  84. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
  85. {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.15.dist-info}/WHEEL +0 -0
  86. {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,2 @@
1
+ from .order_proposals import OrderProposalEndpointConfig, OrderProposalPortfolioEndpointConfig
2
+ from .orders import OrderOrderProposalEndpointConfig
@@ -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, TradeProposal
23
- from wbportfolio.models.transactions.trade_proposals import (
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
- ReadOnlyTradeProposalModelSerializer,
28
- TradeProposalModelSerializer,
29
- TradeProposalRepresentationSerializer,
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 TradeProposalRepresentationViewSet(InternalUserPermissionMixin, viewsets.RepresentationViewSet):
41
+ class OrderProposalRepresentationViewSet(InternalUserPermissionMixin, viewsets.RepresentationViewSet):
42
42
  IDENTIFIER = "wbportfolio:trade"
43
- queryset = TradeProposal.objects.all()
44
- serializer_class = TradeProposalRepresentationSerializer
43
+ queryset = OrderProposal.objects.all()
44
+ serializer_class = OrderProposalRepresentationSerializer
45
45
 
46
46
 
47
- class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserPermissionMixin, viewsets.ModelViewSet):
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 = TradeProposal.objects.select_related("rebalancing_model", "portfolio")
54
- serializer_class = TradeProposalModelSerializer
55
- display_config_class = TradeProposalDisplayConfig
56
- button_config_class = TradeProposalButtonConfig
57
- endpoint_config_class = TradeProposalEndpointConfig
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 == TradeProposal.Status.DRAFT
61
+ "pk" in self.kwargs and (obj := self.get_object()) and obj.status == OrderProposal.Status.DRAFT
62
62
  ):
63
- return TradeProposalModelSerializer
64
- return ReadOnlyTradeProposalModelSerializer
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 == TradeProposal.Status.SUBMIT:
85
+ if instance and instance.status == OrderProposal.Status.SUBMIT:
86
86
  if not instance.portfolio.is_manageable:
87
- info(request, "This trade proposal cannot be approved the portfolio is considered unmanaged.")
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 trade proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid trade proposal",
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
- trade_proposal = get_object_or_404(TradeProposal, pk=pk)
101
- if trade_proposal.status == TradeProposal.Status.DRAFT:
102
- trade_proposal.trades.all().update(weighting=0)
103
- trade_proposal.reset_trades()
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": "Trade proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
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
- trade_proposal = get_object_or_404(TradeProposal, pk=pk)
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 trade_proposal.status == TradeProposal.Status.DRAFT:
112
- trade_proposal.normalize_trades(total_target_weight=Decimal("1.0") - total_cash_weight)
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": "Trade proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
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
- trade_proposal = get_object_or_404(TradeProposal, pk=pk)
119
- if trade_proposal.portfolio.is_manageable:
120
- replay_as_task.delay(trade_proposal.id, user_id=self.request.user.id)
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": "Trade proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
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
- trade_proposal = get_object_or_404(TradeProposal, pk=pk)
127
- if trade_proposal.status == TradeProposal.Status.DRAFT:
128
- trade_proposal.trades.all().delete()
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": "Trade proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
130
+ return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
131
131
 
132
132
 
133
- class TradeProposalPortfolioModelViewSet(UserPortfolioRequestPermissionMixin, TradeProposalModelViewSet):
134
- endpoint_config_class = TradeProposalPortfolioEndpointConfig
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 TradeProposal.objects.filter(portfolio=self.kwargs["portfolio_id"])
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
+ )