wbportfolio 1.54.22__py2.py3-none-any.whl → 1.55.0__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 (79) hide show
  1. wbportfolio/admin/indexes.py +1 -1
  2. wbportfolio/admin/product_groups.py +1 -1
  3. wbportfolio/admin/products.py +2 -1
  4. wbportfolio/admin/rebalancing.py +1 -1
  5. wbportfolio/api_clients/__init__.py +0 -0
  6. wbportfolio/api_clients/ubs.py +150 -0
  7. wbportfolio/factories/orders/order_proposals.py +3 -1
  8. wbportfolio/factories/orders/orders.py +10 -2
  9. wbportfolio/factories/portfolios.py +1 -1
  10. wbportfolio/factories/rebalancing.py +1 -1
  11. wbportfolio/filters/assets.py +10 -2
  12. wbportfolio/filters/orders/__init__.py +1 -0
  13. wbportfolio/filters/orders/order_proposals.py +58 -0
  14. wbportfolio/filters/portfolios.py +20 -0
  15. wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
  16. wbportfolio/import_export/backends/ubs/fees.py +10 -20
  17. wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
  18. wbportfolio/import_export/backends/utils.py +0 -17
  19. wbportfolio/import_export/handlers/asset_position.py +1 -1
  20. wbportfolio/import_export/handlers/orders.py +1 -1
  21. wbportfolio/import_export/parsers/sg_lux/equity.py +1 -1
  22. wbportfolio/import_export/parsers/societe_generale/strategy.py +2 -2
  23. wbportfolio/import_export/parsers/ubs/equity.py +1 -1
  24. wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
  25. wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
  26. wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
  27. wbportfolio/models/asset.py +6 -20
  28. wbportfolio/models/builder.py +74 -31
  29. wbportfolio/models/mixins/instruments.py +7 -0
  30. wbportfolio/models/orders/order_proposals.py +549 -167
  31. wbportfolio/models/orders/orders.py +24 -11
  32. wbportfolio/models/orders/routing.py +54 -0
  33. wbportfolio/models/portfolio.py +77 -41
  34. wbportfolio/models/products.py +9 -0
  35. wbportfolio/models/rebalancing.py +6 -6
  36. wbportfolio/models/transactions/transactions.py +10 -6
  37. wbportfolio/order_routing/__init__.py +19 -0
  38. wbportfolio/order_routing/adapters/__init__.py +57 -0
  39. wbportfolio/order_routing/adapters/ubs.py +161 -0
  40. wbportfolio/pms/trading/handler.py +4 -1
  41. wbportfolio/pms/typing.py +62 -8
  42. wbportfolio/rebalancing/models/composite.py +1 -1
  43. wbportfolio/rebalancing/models/equally_weighted.py +3 -3
  44. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -1
  45. wbportfolio/rebalancing/models/model_portfolio.py +9 -10
  46. wbportfolio/serializers/orders/order_proposals.py +25 -21
  47. wbportfolio/serializers/orders/orders.py +5 -2
  48. wbportfolio/serializers/positions.py +2 -2
  49. wbportfolio/serializers/rebalancing.py +1 -1
  50. wbportfolio/tests/conftest.py +6 -2
  51. wbportfolio/tests/models/orders/test_order_proposals.py +90 -30
  52. wbportfolio/tests/models/test_imports.py +5 -3
  53. wbportfolio/tests/models/test_portfolios.py +57 -23
  54. wbportfolio/tests/models/test_products.py +11 -0
  55. wbportfolio/tests/models/transactions/test_rebalancing.py +2 -2
  56. wbportfolio/tests/rebalancing/test_models.py +3 -5
  57. wbportfolio/tests/signals.py +0 -10
  58. wbportfolio/tests/tests.py +2 -0
  59. wbportfolio/viewsets/__init__.py +7 -4
  60. wbportfolio/viewsets/assets.py +1 -215
  61. wbportfolio/viewsets/charts/__init__.py +6 -1
  62. wbportfolio/viewsets/charts/assets.py +341 -155
  63. wbportfolio/viewsets/configs/buttons/products.py +32 -2
  64. wbportfolio/viewsets/configs/display/assets.py +6 -19
  65. wbportfolio/viewsets/configs/display/products.py +1 -1
  66. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  67. wbportfolio/viewsets/configs/menu/__init__.py +1 -0
  68. wbportfolio/viewsets/configs/menu/orders.py +11 -0
  69. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +66 -12
  70. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +55 -9
  71. wbportfolio/viewsets/orders/configs/displays/orders.py +13 -0
  72. wbportfolio/viewsets/orders/order_proposals.py +47 -7
  73. wbportfolio/viewsets/orders/orders.py +31 -29
  74. wbportfolio/viewsets/portfolios.py +3 -3
  75. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/METADATA +3 -1
  76. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/RECORD +78 -68
  77. wbportfolio/viewsets/signals.py +0 -43
  78. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/WHEEL +0 -0
  79. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/licenses/LICENSE +0 -0
@@ -124,7 +124,7 @@ class ProductDisplayConfig(DisplayViewConfig):
124
124
  return create_simple_display(
125
125
  [
126
126
  [repeat_field(2, "name"), repeat_field(2, "name_repr")],
127
- ["inception_date", "isin", "ticker", "id_repr"],
127
+ ["inception_date", "delisted_date", "isin", "id_repr"],
128
128
  ["share_price", "issue_price", "initial_high_water_mark", "currency"],
129
129
  [repeat_field(2, "bank"), repeat_field(2, "parent")],
130
130
  [repeat_field(4, "tags")],
@@ -11,7 +11,7 @@ class RebalancerDisplayConfig(DisplayViewConfig):
11
11
  fields=[
12
12
  dp.Field(key="portfolio", label="Portfolio"),
13
13
  dp.Field(key="rebalancing_model", label="Rebalancing Model"),
14
- dp.Field(key="approve_order_proposal_automatically", label="Approve automatically"),
14
+ dp.Field(key="apply_order_proposal_automatically", label="Approve automatically"),
15
15
  dp.Field(key="frequency_repr", label="Frequency"),
16
16
  dp.Field(key="activation_date", label="Activation Date"),
17
17
  ],
@@ -20,7 +20,7 @@ class RebalancerDisplayConfig(DisplayViewConfig):
20
20
  def get_instance_display(self) -> Display:
21
21
  return create_simple_display(
22
22
  [
23
- ["rebalancing_model", "approve_order_proposal_automatically"],
23
+ ["rebalancing_model", "apply_order_proposal_automatically"],
24
24
  ["frequency", "activation_date"],
25
25
  ["rebalancing_dates", "rebalancing_dates"],
26
26
  ]
@@ -27,3 +27,4 @@ from .roles import PORTFOLIOROLE_MENUITEM
27
27
  from .trades import SUBSCRIPTION_REDEMPTION_MENUITEM, TRADE_MENUITEM
28
28
  from .portfolio_cash_flow import PORTFOLIO_DAILY_CASH_FLOW
29
29
  from .reconciliations import ACCOUNT_RECONCILIATION_MENU_ITEM
30
+ from .orders import OrderProposalMenuItem
@@ -0,0 +1,11 @@
1
+ from wbcore.menus import ItemPermission, MenuItem
2
+ from wbcore.permissions.shortcuts import is_internal_user
3
+
4
+ OrderProposalMenuItem = MenuItem(
5
+ label="Order Proposals",
6
+ endpoint="wbportfolio:orderproposal-list",
7
+ endpoint_get_parameters={"waiting_for_input": True},
8
+ permission=ItemPermission(
9
+ permissions=["wbportfolio.view_orderproposal"], method=lambda request: is_internal_user(request.user)
10
+ ),
11
+ )
@@ -1,3 +1,6 @@
1
+ from contextlib import suppress
2
+
3
+ from rest_framework.reverse import reverse
1
4
  from wbcore import serializers as wb_serializers
2
5
  from wbcore.contrib.icons import WBIcon
3
6
  from wbcore.enums import RequestType
@@ -5,14 +8,62 @@ from wbcore.metadata.configs import buttons as bt
5
8
  from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
6
9
  from wbcore.metadata.configs.display import create_simple_display
7
10
 
11
+ from wbportfolio.models import OrderProposal, Portfolio
12
+ from wbportfolio.serializers import PortfolioRepresentationSerializer
13
+
8
14
 
9
15
  class NormalizeSerializer(wb_serializers.Serializer):
10
16
  total_cash_weight = wb_serializers.FloatField(default=0, precision=4, percent=True)
11
17
 
12
18
 
19
+ class ResetSerializer(wb_serializers.Serializer):
20
+ use_desired_target_weight = wb_serializers.BooleanField(
21
+ default=False,
22
+ label="Use initial target weight",
23
+ help_text="If True, the target weight used will be the value at the time the order proposal was submitted (as it may have changed due to previous modifications). If False, the delta weight will be set to 0 instead.",
24
+ )
25
+
26
+
13
27
  class OrderProposalButtonConfig(ButtonViewConfig):
14
28
  def get_custom_list_instance_buttons(self):
15
- return {
29
+ buttons = []
30
+ with suppress(AttributeError, AssertionError):
31
+ order_proposal = self.view.get_object()
32
+ if order_proposal.status == OrderProposal.Status.APPLIED.value and order_proposal.portfolio.is_model:
33
+
34
+ class PushModelChangeSerializer(wb_serializers.Serializer):
35
+ only_for_portfolio_ids = wb_serializers.PrimaryKeyRelatedField(
36
+ label="Only for Portfolios", queryset=Portfolio.objects.all()
37
+ )
38
+ _only_for_portfolio_ids = PortfolioRepresentationSerializer(
39
+ source="only_for_portfolio_ids",
40
+ many=True,
41
+ filter_params={"modeled_after": order_proposal.portfolio.id},
42
+ )
43
+ approve_automatically = wb_serializers.BooleanField(
44
+ default=False,
45
+ help_text="True if you want all created orders to be automatically move to the approve state.",
46
+ )
47
+
48
+ buttons.append(
49
+ bt.ActionButton(
50
+ method=RequestType.PATCH,
51
+ identifiers=("wbportfolio:orderproposal",),
52
+ endpoint=reverse("wbportfolio:orderproposal-pushmodelchange", args=[order_proposal.id]),
53
+ icon=WBIcon.BROADCAST.icon,
54
+ label="Push Model Changes",
55
+ description_fields=f"""
56
+ Push this rebalancing to all portfolios that are modeled after {order_proposal.portfolio}
57
+ """,
58
+ action_label="Push Model Changes",
59
+ title="Push Model Changes",
60
+ serializer=PushModelChangeSerializer,
61
+ instance_display=create_simple_display(
62
+ [["only_for_portfolio_ids"], ["approve_automatically"]]
63
+ ),
64
+ )
65
+ )
66
+ buttons.append(
16
67
  bt.DropDownButton(
17
68
  label="Tools",
18
69
  buttons=(
@@ -23,8 +74,8 @@ class OrderProposalButtonConfig(ButtonViewConfig):
23
74
  icon=WBIcon.SYNCHRONIZE.icon,
24
75
  label="Replay Orders",
25
76
  description_fields="""
26
- <p>Replay Orders. It will recompute all assets positions until next order proposal day (or today otherwise) </p>
27
- """,
77
+ <p>Replay Orders. It will recompute all assets positions until next order proposal day (or today otherwise) </p>
78
+ """,
28
79
  action_label="Replay Order",
29
80
  title="Replay Order",
30
81
  ),
@@ -35,11 +86,13 @@ class OrderProposalButtonConfig(ButtonViewConfig):
35
86
  icon=WBIcon.REGENERATE.icon,
36
87
  label="Reset Orders",
37
88
  description_fields="""
38
- <p><strong>Warning:</strong> This action will delete all current orders and recreate initial orders based on your last effective portfolio.</p>
39
- <p><strong>Note:</strong> All delta weights will be permanently removed. This operation cannot be undone.</p>
40
- """,
89
+ <p><strong>Warning:</strong>This action will reset the order delta weight to either 0 or the difference between the previous weight and the locked target weight, depending on the user’s choice.</p>
90
+ <p><strong>Note:</strong>This operation will change the current delta weights and cannot be undone</p>
91
+ """,
41
92
  action_label="Reset Orders",
42
93
  title="Reset Orders",
94
+ serializer=ResetSerializer,
95
+ instance_display=create_simple_display([["use_desired_target_weight"]]),
43
96
  ),
44
97
  bt.ActionButton(
45
98
  method=RequestType.PATCH,
@@ -48,8 +101,8 @@ class OrderProposalButtonConfig(ButtonViewConfig):
48
101
  icon=WBIcon.EDIT.icon,
49
102
  label="Normalize Orders",
50
103
  description_fields="""
51
- <p>Make sure all orders normalize to a total target weight of (100 - {{total_cash_weight}})%</p>
52
- """,
104
+ <p>Make sure all orders normalize to a total target weight of (100 - {{total_cash_weight}})%</p>
105
+ """,
53
106
  action_label="Normalize Orders",
54
107
  title="Normalize Orders",
55
108
  serializer=NormalizeSerializer,
@@ -62,14 +115,15 @@ class OrderProposalButtonConfig(ButtonViewConfig):
62
115
  icon=WBIcon.DELETE.icon,
63
116
  label="Delete All Orders",
64
117
  description_fields="""
65
- <p>Delete all orders from this order proposal?</p>
66
- """,
118
+ <p>Delete all orders from this order proposal?</p>
119
+ """,
67
120
  action_label="Delete All Orders",
68
121
  title="Delete All Orders",
69
122
  ),
70
123
  ),
71
- ),
72
- }
124
+ )
125
+ )
126
+ return set(buttons)
73
127
 
74
128
  def get_custom_instance_buttons(self):
75
129
  return self.get_custom_list_instance_buttons()
@@ -1,7 +1,9 @@
1
+ from contextlib import suppress
1
2
  from typing import Optional
2
3
 
3
4
  from wbcore.contrib.color.enums import WBColor
4
5
  from wbcore.metadata.configs import display as dp
6
+ from wbcore.metadata.configs.display import Section
5
7
  from wbcore.metadata.configs.display.instance_display import Inline, Layout, Page
6
8
  from wbcore.metadata.configs.display.instance_display.operators import default
7
9
  from wbcore.metadata.configs.display.instance_display.shortcuts import Display
@@ -14,9 +16,12 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
14
16
  def get_list_display(self) -> Optional[dp.ListDisplay]:
15
17
  return dp.ListDisplay(
16
18
  fields=[
19
+ dp.Field(key="portfolio", label="Portfolio") if "portfolio_id" not in self.view.kwargs else None,
17
20
  dp.Field(key="trade_date", label="Order Date"),
18
21
  dp.Field(key="rebalancing_model", label="Rebalancing Model"),
19
22
  dp.Field(key="comment", label="Comment"),
23
+ dp.Field(key="creator", label="Creator"),
24
+ dp.Field(key="approver", label="Approver"),
20
25
  ],
21
26
  legends=[
22
27
  dp.Legend(
@@ -29,8 +34,8 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
29
34
  ),
30
35
  dp.LegendItem(
31
36
  icon=WBColor.YELLOW_LIGHT.value,
32
- label=OrderProposal.Status.SUBMIT.label,
33
- value=OrderProposal.Status.SUBMIT.value,
37
+ label=OrderProposal.Status.PENDING.label,
38
+ value=OrderProposal.Status.PENDING.value,
34
39
  ),
35
40
  dp.LegendItem(
36
41
  icon=WBColor.GREEN_LIGHT.value,
@@ -43,9 +48,14 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
43
48
  value=OrderProposal.Status.DENIED.value,
44
49
  ),
45
50
  dp.LegendItem(
46
- icon=WBColor.RED_DARK.value,
47
- label=OrderProposal.Status.FAILED.label,
48
- value=OrderProposal.Status.FAILED.value,
51
+ icon=WBColor.GREEN.value,
52
+ label=OrderProposal.Status.APPLIED.label,
53
+ value=OrderProposal.Status.APPLIED.value,
54
+ ),
55
+ dp.LegendItem(
56
+ icon=WBColor.GREY.value,
57
+ label=OrderProposal.Status.EXECUTION.label,
58
+ value=OrderProposal.Status.EXECUTION.value,
49
59
  ),
50
60
  ],
51
61
  ),
@@ -60,12 +70,20 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
60
70
  ),
61
71
  dp.FormattingRule(
62
72
  style={"backgroundColor": WBColor.YELLOW_LIGHT.value},
63
- condition=("==", OrderProposal.Status.SUBMIT.value),
73
+ condition=("==", OrderProposal.Status.PENDING.value),
64
74
  ),
65
75
  dp.FormattingRule(
66
76
  style={"backgroundColor": WBColor.GREEN_LIGHT.value},
67
77
  condition=("==", OrderProposal.Status.APPROVED.value),
68
78
  ),
79
+ dp.FormattingRule(
80
+ style={"backgroundColor": WBColor.GREEN.value},
81
+ condition=("==", OrderProposal.Status.APPLIED.value),
82
+ ),
83
+ dp.FormattingRule(
84
+ style={"backgroundColor": WBColor.GREY.value},
85
+ condition=("==", OrderProposal.Status.EXECUTION.value),
86
+ ),
69
87
  dp.FormattingRule(
70
88
  style={"backgroundColor": WBColor.RED_LIGHT.value},
71
89
  condition=("==", OrderProposal.Status.DENIED.value),
@@ -80,6 +98,33 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
80
98
  )
81
99
 
82
100
  def get_instance_display(self) -> Display:
101
+ orders_grid_template_areas = [["orders"]]
102
+ orders_grid_template_rows = ["1fr"]
103
+ sections = []
104
+ with suppress(AttributeError, AssertionError):
105
+ op = self.view.get_object()
106
+ if op.execution_status:
107
+ orders_grid_template_areas = [["execution"], ["orders"]]
108
+ orders_grid_template_rows = ["100px", "1fr"]
109
+ sections.append(
110
+ Section(
111
+ key="execution",
112
+ title="Execution",
113
+ collapsed=False,
114
+ display=Display(
115
+ pages=[
116
+ Page(
117
+ layouts={
118
+ default(): Layout(
119
+ grid_template_areas=[["execution_status_repr", "execution_comment"]],
120
+ grid_template_columns=["0.3fr", "0.7fr"],
121
+ )
122
+ }
123
+ )
124
+ ]
125
+ ),
126
+ )
127
+ )
83
128
  return Display(
84
129
  pages=[
85
130
  Page(
@@ -91,7 +136,7 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
91
136
  ["trade_date", "total_cash_weight", "min_order_value"],
92
137
  ["rebalancing_model", "target_portfolio", "target_portfolio"]
93
138
  if self.view.new_mode
94
- else ["rebalancing_model", "rebalancing_model", "rebalancing_model"],
139
+ else ["rebalancing_model", "creator", "approver"],
95
140
  ["comment", "comment", "comment"],
96
141
  ],
97
142
  ),
@@ -101,9 +146,10 @@ class OrderProposalDisplayConfig(DisplayViewConfig):
101
146
  title="Orders",
102
147
  layouts={
103
148
  default(): Layout(
104
- grid_template_areas=[["orders"]],
105
- grid_template_rows=["1fr"],
149
+ grid_template_areas=orders_grid_template_areas,
150
+ grid_template_rows=orders_grid_template_rows,
106
151
  inlines=[Inline(key="orders", endpoint="orders")],
152
+ sections=sections,
107
153
  ),
108
154
  },
109
155
  ),
@@ -81,6 +81,7 @@ class OrderOrderProposalDisplayConfig(DisplayViewConfig):
81
81
  key="underlying_instrument_refinitiv_identifier_code", label="RIC", width=Unit.PIXEL(100)
82
82
  ),
83
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)),
84
85
  ],
85
86
  ),
86
87
  dp.Field(
@@ -161,6 +162,18 @@ class OrderOrderProposalDisplayConfig(DisplayViewConfig):
161
162
  ],
162
163
  )
163
164
  )
165
+ if order_proposal.execution_status:
166
+ fields.append(
167
+ dp.Field(
168
+ label="Execution",
169
+ open_by_default=True,
170
+ key=None,
171
+ children=[
172
+ dp.Field(key="execution_confirmed", label="Confirmed", width=Unit.PIXEL(50)),
173
+ dp.Field(key="execution_comment", label="Comment", width=Unit.PIXEL(100)),
174
+ ],
175
+ )
176
+ )
164
177
  return dp.ListDisplay(
165
178
  fields=fields,
166
179
  legends=[ORDER_STATUS_LEGENDS],
@@ -21,6 +21,7 @@ from wbcore.utils.views import CloneMixin
21
21
 
22
22
  from wbportfolio.models import AssetPosition, OrderProposal
23
23
  from wbportfolio.models.orders.order_proposals import (
24
+ push_model_change_as_task,
24
25
  replay_as_task,
25
26
  )
26
27
  from wbportfolio.serializers import (
@@ -29,6 +30,8 @@ from wbportfolio.serializers import (
29
30
  ReadOnlyOrderProposalModelSerializer,
30
31
  )
31
32
 
33
+ from ...filters.orders import OrderProposalFilterSet
34
+ from ...order_routing import ExecutionStatus
32
35
  from ..mixins import UserPortfolioRequestPermissionMixin
33
36
  from .configs import (
34
37
  OrderProposalButtonConfig,
@@ -52,6 +55,7 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
52
55
 
53
56
  queryset = OrderProposal.objects.select_related("rebalancing_model", "portfolio")
54
57
  serializer_class = OrderProposalModelSerializer
58
+ filterset_class = OrderProposalFilterSet
55
59
  display_config_class = OrderProposalDisplayConfig
56
60
  button_config_class = OrderProposalButtonConfig
57
61
  endpoint_config_class = OrderProposalEndpointConfig
@@ -81,14 +85,29 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
81
85
  ]
82
86
  )
83
87
 
84
- def add_messages(self, request, instance=None, **kwargs):
85
- if instance and instance.status == OrderProposal.Status.SUBMIT:
86
- if not instance.portfolio.is_manageable:
87
- info(request, "This order proposal cannot be approved the portfolio is considered unmanaged.")
88
- if instance.has_non_successful_checks:
88
+ def add_messages(self, request, instance: OrderProposal | None = None, **kwargs):
89
+ if instance:
90
+ if instance.status == OrderProposal.Status.PENDING:
91
+ if not instance.portfolio.is_manageable:
92
+ info(request, "This order proposal cannot be approved the portfolio is considered unmanaged.")
93
+ if instance.has_non_successful_checks:
94
+ warning(
95
+ request,
96
+ "This order proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid order proposal",
97
+ )
98
+ if (
99
+ instance.execution_status in [ExecutionStatus.IN_DRAFT, ExecutionStatus.COMPLETED]
100
+ and instance.orders.filter(execution_confirmed=False).exists()
101
+ ):
102
+ warning(request, "Some orders failed confirmation. Check the list for further details.")
103
+ if instance.execution_status in [
104
+ ExecutionStatus.REJECTED,
105
+ ExecutionStatus.FAILED,
106
+ ExecutionStatus.UNKNOWN,
107
+ ]:
89
108
  warning(
90
109
  request,
91
- "This order proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid order proposal",
110
+ f"The execution status is {ExecutionStatus[instance.execution_status].label}. Detail: {instance.execution_comment}",
92
111
  )
93
112
 
94
113
  @classmethod
@@ -98,9 +117,10 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
98
117
  @action(detail=True, methods=["PATCH"])
99
118
  def reset(self, request, pk=None):
100
119
  order_proposal = get_object_or_404(OrderProposal, pk=pk)
120
+ use_desired_target_weight = request.GET.get("use_desired_target_weight") == "true"
101
121
  if order_proposal.status == OrderProposal.Status.DRAFT:
102
122
  order_proposal.orders.all().update(weighting=0)
103
- order_proposal.reset_orders()
123
+ order_proposal.reset_orders(use_desired_target_weight=use_desired_target_weight)
104
124
  return Response({"send": True})
105
125
  return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
106
126
 
@@ -129,6 +149,26 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
129
149
  return Response({"send": True})
130
150
  return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
131
151
 
152
+ @action(detail=True, methods=["PATCH"])
153
+ def pushmodelchange(self, request, pk=None):
154
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
155
+ only_for_portfolio_ids = list(
156
+ map(lambda o: int(o), filter(lambda r: r, request.data.get("only_for_portfolio_ids", "").split(",")))
157
+ )
158
+ approve_automatically = request.data.get("approve_automatically") == "true"
159
+ if order_proposal.status == OrderProposal.Status.APPLIED and order_proposal.portfolio.is_model:
160
+ push_model_change_as_task.delay(
161
+ order_proposal.id,
162
+ request.user.id,
163
+ only_for_portfolio_ids=only_for_portfolio_ids,
164
+ approve_automatically=approve_automatically,
165
+ )
166
+ return Response({"send": True})
167
+ return Response(
168
+ {"status": "Order Proposal needs to be approved and linked to be a model portfolio"},
169
+ status=status.HTTP_400_BAD_REQUEST,
170
+ )
171
+
132
172
 
133
173
  class OrderProposalPortfolioModelViewSet(UserPortfolioRequestPermissionMixin, OrderProposalModelViewSet):
134
174
  endpoint_config_class = OrderProposalPortfolioEndpointConfig
@@ -92,7 +92,9 @@ class OrderOrderProposalModelViewSet(
92
92
  sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
93
93
  )
94
94
  # weights aggregates
95
- cash_sum_effective_weight = Decimal("1.0") - noncash_aggregates["sum_effective_weight"]
95
+ cash_sum_effective_weight = (
96
+ self.order_proposal.total_effective_portfolio_weight - noncash_aggregates["sum_effective_weight"]
97
+ )
96
98
  cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
97
99
  noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
98
100
  noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
@@ -176,14 +178,17 @@ class OrderOrderProposalModelViewSet(
176
178
  return OrderOrderProposalModelSerializer
177
179
 
178
180
  def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
179
- if queryset is not None and queryset.exists() and self.order_proposal.status != OrderProposal.Status.APPROVED:
180
- total_target_weight = queryset.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
181
+ if self.orders.exists() and self.order_proposal.status in [
182
+ OrderProposal.Status.PENDING,
183
+ OrderProposal.Status.DRAFT,
184
+ ]:
185
+ total_target_weight = self.orders.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
181
186
  if round(total_target_weight, 8) != 1:
182
187
  warning(
183
188
  request,
184
189
  "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.",
185
190
  )
186
- if queryset.filter(has_warnings=True).exists():
191
+ if self.orders.filter(has_warnings=True).exists():
187
192
  error(
188
193
  request,
189
194
  "Some orders failed preparation. To resolve this, please revert the order proposal to draft, review and correct the orders, and then resubmit.",
@@ -191,32 +196,29 @@ class OrderOrderProposalModelViewSet(
191
196
 
192
197
  @cached_property
193
198
  def orders(self):
194
- if self.is_portfolio_manager:
195
- return self.order_proposal.get_orders()
196
- else:
197
- return Order.objects.none()
199
+ qs = self.order_proposal.get_orders()
200
+ if not self.is_portfolio_manager:
201
+ return qs.none()
202
+ return qs
198
203
 
199
204
  def get_queryset(self):
200
- return (
201
- self.orders.exclude(underlying_instrument__is_cash=True)
202
- .annotate(
203
- underlying_instrument_isin=F("underlying_instrument__isin"),
204
- underlying_instrument_ticker=F("underlying_instrument__ticker"),
205
- underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
206
- underlying_instrument_instrument_type=Case(
207
- When(
208
- underlying_instrument__parent__is_security=True,
209
- then=F("underlying_instrument__parent__instrument_type__short_name"),
210
- ),
211
- default=F("underlying_instrument__instrument_type__short_name"),
205
+ return self.orders.annotate( # .exclude(underlying_instrument__is_cash=True)
206
+ underlying_instrument_isin=F("underlying_instrument__isin"),
207
+ underlying_instrument_ticker=F("underlying_instrument__ticker"),
208
+ underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
209
+ underlying_instrument_instrument_type=Case(
210
+ When(
211
+ underlying_instrument__parent__is_security=True,
212
+ then=F("underlying_instrument__parent__instrument_type__short_name"),
212
213
  ),
213
- effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
214
- target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
215
- portfolio_currency=F("portfolio__currency__symbol"),
216
- security=F("underlying_instrument__parent"),
217
- company=F("underlying_instrument__parent__parent"),
218
- )
219
- .select_related(
220
- "underlying_instrument", "underlying_instrument__parent", "underlying_instrument__parent__parent"
221
- )
214
+ default=F("underlying_instrument__instrument_type__short_name"),
215
+ ),
216
+ underlying_instrument_exchange=F("underlying_instrument__exchange__name"),
217
+ effective_total_value_fx_portfolio=F("previous_weight") * Value(self.portfolio_total_asset_value),
218
+ target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
219
+ portfolio_currency=F("portfolio__currency__symbol"),
220
+ security=F("underlying_instrument__parent"),
221
+ company=F("underlying_instrument__parent__parent"),
222
+ ).select_related(
223
+ "underlying_instrument", "underlying_instrument__parent", "underlying_instrument__parent__parent"
222
224
  )
@@ -147,8 +147,8 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
147
147
  activation_date = datetime.strptime(request.POST["activation_date"], "%Y-%m-%d")
148
148
  rebalancing_model = get_object_or_404(RebalancingModel, pk=request.POST["rebalancing_model"])
149
149
  frequency = request.POST["frequency"]
150
- approve_order_proposal_automatically = (
151
- request.POST.get("approve_order_proposal_automatically", "false") == "true"
150
+ apply_order_proposal_automatically = (
151
+ request.POST.get("apply_order_proposal_automatically", "false") == "true"
152
152
  )
153
153
 
154
154
  rebalancer, _ = Rebalancer.objects.update_or_create(
@@ -157,7 +157,7 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
157
157
  "rebalancing_model": rebalancing_model,
158
158
  "frequency": frequency,
159
159
  "activation_date": activation_date,
160
- "approve_order_proposal_automatically": approve_order_proposal_automatically,
160
+ "apply_order_proposal_automatically": apply_order_proposal_automatically,
161
161
  },
162
162
  )
163
163
  return Response(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.54.22
3
+ Version: 1.55.0
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -19,6 +19,8 @@ Requires-Dist: wbcompliance
19
19
  Requires-Dist: wbcore
20
20
  Requires-Dist: wbcrm
21
21
  Requires-Dist: wbfdm
22
+ Requires-Dist: wbmailing
22
23
  Requires-Dist: wbnews
24
+ Requires-Dist: wbreport
23
25
  Requires-Dist: xlrd==2.*
24
26
  Requires-Dist: xlsxwriter==3.*