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.
- wbportfolio/admin/indexes.py +1 -1
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/rebalancing.py +1 -1
- wbportfolio/api_clients/__init__.py +0 -0
- wbportfolio/api_clients/ubs.py +150 -0
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +10 -2
- wbportfolio/factories/portfolios.py +1 -1
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/filters/assets.py +10 -2
- wbportfolio/filters/orders/__init__.py +1 -0
- wbportfolio/filters/orders/order_proposals.py +58 -0
- wbportfolio/filters/portfolios.py +20 -0
- wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
- wbportfolio/import_export/backends/ubs/fees.py +10 -20
- wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
- wbportfolio/import_export/backends/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +1 -1
- wbportfolio/import_export/handlers/orders.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +1 -1
- wbportfolio/import_export/parsers/societe_generale/strategy.py +2 -2
- wbportfolio/import_export/parsers/ubs/equity.py +1 -1
- wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
- wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
- wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
- wbportfolio/models/asset.py +6 -20
- wbportfolio/models/builder.py +74 -31
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/orders/order_proposals.py +549 -167
- wbportfolio/models/orders/orders.py +24 -11
- wbportfolio/models/orders/routing.py +54 -0
- wbportfolio/models/portfolio.py +77 -41
- wbportfolio/models/products.py +9 -0
- wbportfolio/models/rebalancing.py +6 -6
- wbportfolio/models/transactions/transactions.py +10 -6
- wbportfolio/order_routing/__init__.py +19 -0
- wbportfolio/order_routing/adapters/__init__.py +57 -0
- wbportfolio/order_routing/adapters/ubs.py +161 -0
- wbportfolio/pms/trading/handler.py +4 -1
- wbportfolio/pms/typing.py +62 -8
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/equally_weighted.py +3 -3
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -1
- wbportfolio/rebalancing/models/model_portfolio.py +9 -10
- wbportfolio/serializers/orders/order_proposals.py +25 -21
- wbportfolio/serializers/orders/orders.py +5 -2
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/tests/conftest.py +6 -2
- wbportfolio/tests/models/orders/test_order_proposals.py +90 -30
- wbportfolio/tests/models/test_imports.py +5 -3
- wbportfolio/tests/models/test_portfolios.py +57 -23
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +2 -2
- wbportfolio/tests/rebalancing/test_models.py +3 -5
- wbportfolio/tests/signals.py +0 -10
- wbportfolio/tests/tests.py +2 -0
- wbportfolio/viewsets/__init__.py +7 -4
- wbportfolio/viewsets/assets.py +1 -215
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +341 -155
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/menu/__init__.py +1 -0
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +66 -12
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +55 -9
- wbportfolio/viewsets/orders/configs/displays/orders.py +13 -0
- wbportfolio/viewsets/orders/order_proposals.py +47 -7
- wbportfolio/viewsets/orders/orders.py +31 -29
- wbportfolio/viewsets/portfolios.py +3 -3
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/METADATA +3 -1
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/RECORD +78 -68
- wbportfolio/viewsets/signals.py +0 -43
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/WHEEL +0 -0
- {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", "
|
|
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="
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
33
|
-
value=OrderProposal.Status.
|
|
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.
|
|
47
|
-
label=OrderProposal.Status.
|
|
48
|
-
value=OrderProposal.Status.
|
|
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.
|
|
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", "
|
|
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=
|
|
105
|
-
grid_template_rows=
|
|
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
|
|
86
|
-
if
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
"
|
|
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 =
|
|
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
|
|
180
|
-
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
151
|
-
request.POST.get("
|
|
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
|
-
"
|
|
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.
|
|
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.*
|