wbportfolio 1.48.0__py2.py3-none-any.whl → 1.49.1__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/factories/__init__.py +1 -3
- wbportfolio/factories/dividends.py +1 -0
- wbportfolio/factories/portfolios.py +0 -12
- wbportfolio/factories/product_groups.py +8 -1
- wbportfolio/factories/products.py +18 -0
- wbportfolio/factories/trades.py +5 -1
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +1 -1
- wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -1
- wbportfolio/import_export/handlers/trade.py +13 -2
- wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -1
- wbportfolio/migrations/0075_portfolio_initial_position_date_and_more.py +28 -0
- wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py +126 -0
- wbportfolio/models/portfolio.py +15 -16
- wbportfolio/models/transactions/claim.py +8 -7
- wbportfolio/models/transactions/dividends.py +3 -20
- wbportfolio/models/transactions/rebalancing.py +7 -1
- wbportfolio/models/transactions/trade_proposals.py +163 -63
- wbportfolio/models/transactions/trades.py +24 -22
- wbportfolio/models/transactions/transactions.py +37 -37
- wbportfolio/pms/trading/handler.py +1 -1
- wbportfolio/pms/typing.py +3 -0
- wbportfolio/rebalancing/models/composite.py +14 -1
- wbportfolio/risk_management/backends/exposure_portfolio.py +30 -1
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +47 -0
- wbportfolio/serializers/portfolios.py +26 -0
- wbportfolio/serializers/transactions/trades.py +13 -0
- wbportfolio/tests/models/test_portfolios.py +2 -2
- wbportfolio/tests/models/transactions/test_trade_proposals.py +381 -0
- wbportfolio/tests/models/transactions/test_trades.py +14 -0
- wbportfolio/tests/signals.py +1 -1
- wbportfolio/tests/viewsets/test_performances.py +2 -1
- wbportfolio/viewsets/configs/display/assets.py +0 -11
- wbportfolio/viewsets/configs/display/portfolios.py +58 -14
- wbportfolio/viewsets/configs/display/products.py +0 -13
- wbportfolio/viewsets/configs/display/trades.py +24 -17
- wbportfolio/viewsets/configs/endpoints/trades.py +9 -0
- wbportfolio/viewsets/configs/endpoints/transactions.py +6 -0
- wbportfolio/viewsets/portfolios.py +39 -8
- wbportfolio/viewsets/transactions/trade_proposals.py +1 -1
- wbportfolio/viewsets/transactions/trades.py +105 -13
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.1.dist-info}/METADATA +1 -1
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.1.dist-info}/RECORD +46 -43
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.1.dist-info}/WHEEL +0 -0
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,8 +6,9 @@ from faker import Faker
|
|
|
6
6
|
from pandas.tseries.offsets import BDay, BMonthEnd, BYearEnd
|
|
7
7
|
from rest_framework.reverse import reverse
|
|
8
8
|
from rest_framework.test import force_authenticate
|
|
9
|
+
from wbfdm.factories import InstrumentPriceFactory
|
|
9
10
|
|
|
10
|
-
from wbportfolio.factories import
|
|
11
|
+
from wbportfolio.factories import ProductFactory
|
|
11
12
|
from wbportfolio.viewsets.product_performance import PerformanceComparisonPandasView
|
|
12
13
|
|
|
13
14
|
fake = Faker()
|
|
@@ -148,17 +148,6 @@ class AssetPositionInstrumentDisplayConfig(DisplayViewConfig):
|
|
|
148
148
|
items=[dp.LegendItem(icon=WBIcon.UNFILTER.icon, label="Not Invested", value=True)],
|
|
149
149
|
)
|
|
150
150
|
],
|
|
151
|
-
formatting=[
|
|
152
|
-
dp.Formatting(
|
|
153
|
-
column="is_invested",
|
|
154
|
-
formatting_rules=[
|
|
155
|
-
dp.FormattingRule(
|
|
156
|
-
icon=WBIcon.UNFILTER.icon,
|
|
157
|
-
condition=("==", False),
|
|
158
|
-
)
|
|
159
|
-
],
|
|
160
|
-
)
|
|
161
|
-
],
|
|
162
151
|
)
|
|
163
152
|
|
|
164
153
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
from wbcore.enums import Unit
|
|
4
5
|
from wbcore.metadata.configs import display as dp
|
|
5
6
|
from wbcore.metadata.configs.display import Layout, Page, default
|
|
6
7
|
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
@@ -17,17 +18,60 @@ class PortfolioDisplayConfig(DisplayViewConfig):
|
|
|
17
18
|
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
18
19
|
return dp.ListDisplay(
|
|
19
20
|
fields=[
|
|
20
|
-
dp.Field(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
dp.Field(
|
|
22
|
+
label="Information",
|
|
23
|
+
open_by_default=False,
|
|
24
|
+
key=None,
|
|
25
|
+
children=[
|
|
26
|
+
dp.Field(key="name", label="Name", width=Unit.PIXEL(300)),
|
|
27
|
+
dp.Field(key="currency", label="CCY", width=Unit.PIXEL(75)),
|
|
28
|
+
dp.Field(key="hedged_currency", label="Hedged CCY", width=Unit.PIXEL(100), show="open"),
|
|
29
|
+
dp.Field(key="updated_at", label="Updated At", width=Unit.PIXEL(150)),
|
|
30
|
+
dp.Field(key="depends_on", label="Depends on", show="open", width=Unit.PIXEL(300)),
|
|
31
|
+
dp.Field(key="invested_timespan", label="Invested", show="open"),
|
|
32
|
+
dp.Field(key="instruments", label="Instruments", width=Unit.PIXEL(250)),
|
|
33
|
+
],
|
|
34
|
+
),
|
|
35
|
+
dp.Field(
|
|
36
|
+
label="Valuation & Position",
|
|
37
|
+
open_by_default=False,
|
|
38
|
+
key=None,
|
|
39
|
+
children=[
|
|
40
|
+
dp.Field(key="initial_position_date", label="Issue Date", width=Unit.PIXEL(150)),
|
|
41
|
+
dp.Field(key="last_position_date", label="Last Position Date", width=Unit.PIXEL(150)),
|
|
42
|
+
dp.Field(
|
|
43
|
+
key="last_asset_under_management_usd", label="AUM ($)", width=Unit.PIXEL(100), show="open"
|
|
44
|
+
),
|
|
45
|
+
dp.Field(key="last_positions", label="Position", width=Unit.PIXEL(100), show="open"),
|
|
46
|
+
],
|
|
47
|
+
),
|
|
48
|
+
dp.Field(
|
|
49
|
+
label="Rebalancing",
|
|
50
|
+
open_by_default=False,
|
|
51
|
+
key=None,
|
|
52
|
+
children=[
|
|
53
|
+
dp.Field(key="automatic_rebalancer", label="Automatic Rebalancer"),
|
|
54
|
+
dp.Field(key="last_trade_proposal_date", label="Last Rebalance", width=Unit.PIXEL(250)),
|
|
55
|
+
dp.Field(
|
|
56
|
+
key="next_expected_trade_proposal_date",
|
|
57
|
+
label="Next Rebalancing",
|
|
58
|
+
width=Unit.PIXEL(250),
|
|
59
|
+
show="open",
|
|
60
|
+
),
|
|
61
|
+
],
|
|
62
|
+
),
|
|
63
|
+
dp.Field(
|
|
64
|
+
label="Administration",
|
|
65
|
+
open_by_default=False,
|
|
66
|
+
key=None,
|
|
67
|
+
children=[
|
|
68
|
+
dp.Field(key="is_manageable", label="Managed", width=Unit.PIXEL(100)),
|
|
69
|
+
dp.Field(key="is_tracked", label="Tracked", width=Unit.PIXEL(100), show="open"),
|
|
70
|
+
dp.Field(key="only_weighting", label="Only-Weight", width=Unit.PIXEL(100), show="open"),
|
|
71
|
+
dp.Field(key="is_lookthrough", label="Look through", width=Unit.PIXEL(100), show="open"),
|
|
72
|
+
dp.Field(key="is_composition", label="Composition", width=Unit.PIXEL(100), show="open"),
|
|
73
|
+
],
|
|
74
|
+
),
|
|
31
75
|
]
|
|
32
76
|
)
|
|
33
77
|
|
|
@@ -156,14 +200,14 @@ class TopDownPortfolioCompositionPandasDisplayConfig(DisplayViewConfig):
|
|
|
156
200
|
rebalancing_column_label = "Last Rebalancing"
|
|
157
201
|
effective_column_label = "Actual"
|
|
158
202
|
if self.view.last_rebalancing_date:
|
|
159
|
-
rebalancing_column_label += f" ({self.view.last_rebalancing_date:%Y
|
|
203
|
+
rebalancing_column_label += f" ({self.view.last_rebalancing_date:%Y-%m-%d})"
|
|
160
204
|
if self.view.last_effective_date:
|
|
161
205
|
effective_column_label += f" ({self.view.last_effective_date:%Y-%m-%d})"
|
|
162
206
|
return dp.ListDisplay(
|
|
163
207
|
fields=[
|
|
164
208
|
dp.Field(key="instrument", label="Instrument"),
|
|
165
|
-
dp.Field(key="
|
|
166
|
-
dp.Field(key="
|
|
209
|
+
dp.Field(key="rebalancing_weights", label=rebalancing_column_label),
|
|
210
|
+
dp.Field(key="effective_weights", label=effective_column_label),
|
|
167
211
|
],
|
|
168
212
|
tree=True,
|
|
169
213
|
tree_group_pinned="left",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from django.utils.translation import gettext_lazy as _
|
|
4
|
-
from wbcore.contrib.icons import WBIcon
|
|
5
4
|
from wbcore.enums import Unit
|
|
6
5
|
from wbcore.metadata.configs import display as dp
|
|
7
6
|
from wbcore.metadata.configs.display.formatting import Condition, Operator
|
|
@@ -27,18 +26,6 @@ class ProductDisplayConfig(DisplayViewConfig):
|
|
|
27
26
|
label=_("Information"),
|
|
28
27
|
open_by_default=False,
|
|
29
28
|
children=[
|
|
30
|
-
dp.Field(
|
|
31
|
-
key="is_invested",
|
|
32
|
-
label="",
|
|
33
|
-
formatting_rules=[
|
|
34
|
-
dp.FormattingRule(
|
|
35
|
-
icon=WBIcon.UNFILTER.icon,
|
|
36
|
-
condition=Condition(Operator("=="), False),
|
|
37
|
-
),
|
|
38
|
-
],
|
|
39
|
-
width=30,
|
|
40
|
-
show="open",
|
|
41
|
-
),
|
|
42
29
|
dp.Field(key="name_repr", label="Name", width=250),
|
|
43
30
|
dp.Field(key="parent", label="Parent"),
|
|
44
31
|
dp.Field(key="isin", label="ISIN"),
|
|
@@ -267,15 +267,6 @@ class SubscriptionRedemptionDisplayConfig(TradeDisplayConfig):
|
|
|
267
267
|
)
|
|
268
268
|
],
|
|
269
269
|
),
|
|
270
|
-
dp.Formatting(
|
|
271
|
-
column="pending",
|
|
272
|
-
formatting_rules=[
|
|
273
|
-
dp.FormattingRule(
|
|
274
|
-
icon=WBIcon.FOLDERS_ADD.icon,
|
|
275
|
-
condition=("==", True),
|
|
276
|
-
)
|
|
277
|
-
],
|
|
278
|
-
),
|
|
279
270
|
],
|
|
280
271
|
)
|
|
281
272
|
|
|
@@ -300,14 +291,27 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
300
291
|
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
301
292
|
trade_proposal = get_object_or_404(TradeProposal, pk=self.view.kwargs.get("trade_proposal_id", None))
|
|
302
293
|
fields = [
|
|
303
|
-
dp.Field(
|
|
294
|
+
dp.Field(
|
|
295
|
+
label="Instrument",
|
|
296
|
+
open_by_default=True,
|
|
297
|
+
key=None,
|
|
298
|
+
children=[
|
|
299
|
+
dp.Field(key="underlying_instrument", label="Name", width=Unit.PIXEL(250)),
|
|
300
|
+
dp.Field(key="underlying_instrument_isin", label="ISIN", width=Unit.PIXEL(125)),
|
|
301
|
+
dp.Field(key="underlying_instrument_ticker", label="Ticker", width=Unit.PIXEL(100)),
|
|
302
|
+
dp.Field(
|
|
303
|
+
key="underlying_instrument_refinitiv_identifier_code", label="RIC", width=Unit.PIXEL(100)
|
|
304
|
+
),
|
|
305
|
+
dp.Field(key="underlying_instrument_instrument_type", label="Asset Class", width=Unit.PIXEL(125)),
|
|
306
|
+
],
|
|
307
|
+
),
|
|
304
308
|
dp.Field(
|
|
305
309
|
label="Weight",
|
|
306
310
|
open_by_default=False,
|
|
307
311
|
key=None,
|
|
308
312
|
children=[
|
|
309
|
-
dp.Field(key="effective_weight", label="Effective Weight", show="open"),
|
|
310
|
-
dp.Field(key="target_weight", label="Target Weight", show="open"),
|
|
313
|
+
dp.Field(key="effective_weight", label="Effective Weight", show="open", width=Unit.PIXEL(150)),
|
|
314
|
+
dp.Field(key="target_weight", label="Target Weight", show="open", width=Unit.PIXEL(150)),
|
|
311
315
|
dp.Field(
|
|
312
316
|
key="weighting",
|
|
313
317
|
label="Delta Weight",
|
|
@@ -321,6 +325,7 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
321
325
|
condition=(">", 0),
|
|
322
326
|
),
|
|
323
327
|
],
|
|
328
|
+
width=Unit.PIXEL(150),
|
|
324
329
|
),
|
|
325
330
|
],
|
|
326
331
|
),
|
|
@@ -332,8 +337,8 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
332
337
|
open_by_default=False,
|
|
333
338
|
key=None,
|
|
334
339
|
children=[
|
|
335
|
-
dp.Field(key="effective_shares", label="Effective Shares", show="open"),
|
|
336
|
-
dp.Field(key="target_shares", label="Target Shares", show="open"),
|
|
340
|
+
dp.Field(key="effective_shares", label="Effective Shares", show="open", width=Unit.PIXEL(150)),
|
|
341
|
+
dp.Field(key="target_shares", label="Target Shares", show="open", width=Unit.PIXEL(150)),
|
|
337
342
|
dp.Field(
|
|
338
343
|
key="shares",
|
|
339
344
|
label="Shares",
|
|
@@ -347,6 +352,7 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
347
352
|
condition=(">", 0),
|
|
348
353
|
),
|
|
349
354
|
],
|
|
355
|
+
width=Unit.PIXEL(150),
|
|
350
356
|
),
|
|
351
357
|
],
|
|
352
358
|
)
|
|
@@ -370,16 +376,17 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
370
376
|
condition=("==", Trade.Type.BUY.name),
|
|
371
377
|
),
|
|
372
378
|
],
|
|
379
|
+
width=Unit.PIXEL(125),
|
|
373
380
|
),
|
|
374
|
-
dp.Field(key="comment", label="Comment"),
|
|
375
|
-
dp.Field(key="order", label="Order", show="open"),
|
|
381
|
+
dp.Field(key="comment", label="Comment", width=Unit.PIXEL(250)),
|
|
382
|
+
dp.Field(key="order", label="Order", show="open", width=Unit.PIXEL(100)),
|
|
376
383
|
],
|
|
377
384
|
)
|
|
378
385
|
)
|
|
379
386
|
return dp.ListDisplay(
|
|
380
387
|
fields=fields,
|
|
381
388
|
legends=[TRADE_STATUS_LEGENDS],
|
|
382
|
-
formatting=[
|
|
389
|
+
formatting=[TRADE_STATUS_FORMATTING],
|
|
383
390
|
)
|
|
384
391
|
|
|
385
392
|
def get_instance_display(self) -> Display:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
|
|
1
3
|
from django.shortcuts import get_object_or_404
|
|
2
4
|
from rest_framework.reverse import reverse
|
|
3
5
|
from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
@@ -77,6 +79,13 @@ class TradeTradeProposalEndpointConfig(EndpointViewConfig):
|
|
|
77
79
|
return self.get_list_endpoint()
|
|
78
80
|
return None
|
|
79
81
|
|
|
82
|
+
def get_delete_endpoint(self, **kwargs):
|
|
83
|
+
with suppress(AttributeError, AssertionError):
|
|
84
|
+
trade = self.view.get_object()
|
|
85
|
+
if trade._effective_weight: # we make sure trade with a valid effective position cannot be deleted
|
|
86
|
+
return None
|
|
87
|
+
return super().get_delete_endpoint(**kwargs)
|
|
88
|
+
|
|
80
89
|
|
|
81
90
|
class SubscriptionRedemptionEndpointConfig(TradeEndpointConfig):
|
|
82
91
|
def get_endpoint(self, **kwargs):
|
|
@@ -3,6 +3,12 @@ from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class TransactionEndpointConfig(EndpointViewConfig):
|
|
6
|
+
def get_endpoint(self, **kwargs):
|
|
7
|
+
return None
|
|
8
|
+
|
|
9
|
+
def get_list_endpoint(self, **kwargs):
|
|
10
|
+
return reverse("wbportfolio:transaction-list", request=self.request)
|
|
11
|
+
|
|
6
12
|
def get_instance_endpoint(self, **kwargs):
|
|
7
13
|
model = "{{transaction_url_type}}"
|
|
8
14
|
return f"{self.request.scheme}://{self.request.get_host()}/api/portfolio/{model}/"
|
|
@@ -2,7 +2,7 @@ from contextlib import suppress
|
|
|
2
2
|
from datetime import date, datetime
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
|
-
from django.db.models import Q
|
|
5
|
+
from django.db.models import OuterRef, Q, Subquery, Sum
|
|
6
6
|
from django.http import HttpResponse
|
|
7
7
|
from django.shortcuts import get_object_or_404
|
|
8
8
|
from django.utils.dateparse import parse_date
|
|
@@ -67,7 +67,23 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
67
67
|
queryset = Portfolio.objects.all()
|
|
68
68
|
|
|
69
69
|
search_fields = ("currency__key", "name")
|
|
70
|
-
ordering_fields =
|
|
70
|
+
ordering_fields = (
|
|
71
|
+
"name",
|
|
72
|
+
"currency",
|
|
73
|
+
"hedged_currency",
|
|
74
|
+
"updated_at",
|
|
75
|
+
"initial_position_date",
|
|
76
|
+
"last_position_date",
|
|
77
|
+
"last_asset_under_management_usd",
|
|
78
|
+
"last_positions",
|
|
79
|
+
"automatic_rebalancer",
|
|
80
|
+
"last_trade_proposal_date",
|
|
81
|
+
"is_manageable",
|
|
82
|
+
"is_tracked",
|
|
83
|
+
"only_weighting",
|
|
84
|
+
"is_lookthrough",
|
|
85
|
+
"is_composition",
|
|
86
|
+
)
|
|
71
87
|
ordering = ["name"]
|
|
72
88
|
|
|
73
89
|
display_config_class = PortfolioDisplayConfig
|
|
@@ -80,11 +96,26 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
80
96
|
return (
|
|
81
97
|
super()
|
|
82
98
|
.get_queryset()
|
|
83
|
-
.select_related(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
.select_related("currency", "hedged_currency")
|
|
100
|
+
.prefetch_related("depends_on", "instruments")
|
|
101
|
+
.annotate(
|
|
102
|
+
last_asset_under_management_usd=Subquery(
|
|
103
|
+
AssetPosition.objects.filter(portfolio=OuterRef("pk"), date=OuterRef("last_position_date"))
|
|
104
|
+
.values("portfolio")
|
|
105
|
+
.annotate(s=Sum("total_value_fx_usd"))
|
|
106
|
+
.values("s")[:1]
|
|
107
|
+
),
|
|
108
|
+
last_positions=Subquery(
|
|
109
|
+
AssetPosition.objects.filter(portfolio=OuterRef("pk"), date=OuterRef("last_position_date"))
|
|
110
|
+
.values("portfolio")
|
|
111
|
+
.annotate(s=Sum("shares"))
|
|
112
|
+
.values("s")[:1]
|
|
113
|
+
),
|
|
114
|
+
last_trade_proposal_date=Subquery(
|
|
115
|
+
TradeProposal.objects.filter(portfolio=OuterRef("pk"))
|
|
116
|
+
.order_by("-trade_date")
|
|
117
|
+
.values("trade_date")[:1]
|
|
118
|
+
),
|
|
88
119
|
)
|
|
89
120
|
)
|
|
90
121
|
|
|
@@ -210,7 +241,7 @@ class TopDownPortfolioCompositionPandasAPIView(UserPortfolioRequestPermissionMix
|
|
|
210
241
|
|
|
211
242
|
@cached_property
|
|
212
243
|
def last_rebalancing_date(self) -> date | None:
|
|
213
|
-
if self.composition_portfolio:
|
|
244
|
+
if self.composition_portfolio and self.last_effective_date:
|
|
214
245
|
with suppress(TradeProposal.DoesNotExist):
|
|
215
246
|
return (
|
|
216
247
|
self.composition_portfolio.trade_proposals.filter(trade_date__lte=self.last_effective_date)
|
|
@@ -82,7 +82,7 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
82
82
|
if instance and instance.status == TradeProposal.Status.SUBMIT:
|
|
83
83
|
if not instance.portfolio.is_manageable:
|
|
84
84
|
info(request, "This trade proposal cannot be approved the portfolio is considered unmanaged.")
|
|
85
|
-
if
|
|
85
|
+
if instance.has_non_successful_checks:
|
|
86
86
|
warning(
|
|
87
87
|
request,
|
|
88
88
|
"This trade proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid trade proposal",
|
|
@@ -25,6 +25,7 @@ from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
|
25
25
|
from wbcore.utils.strings import format_number
|
|
26
26
|
from wbcore.viewsets.mixins import OrderableMixin
|
|
27
27
|
from wbcrm.models import Account
|
|
28
|
+
from wbfdm.models import Cash
|
|
28
29
|
|
|
29
30
|
from wbportfolio.filters import (
|
|
30
31
|
SubscriptionRedemptionFilterSet,
|
|
@@ -366,7 +367,19 @@ class TradeTradeProposalModelViewSet(
|
|
|
366
367
|
"trade_proposal",
|
|
367
368
|
"order",
|
|
368
369
|
)
|
|
369
|
-
ordering_fields = (
|
|
370
|
+
ordering_fields = (
|
|
371
|
+
"underlying_instrument__name",
|
|
372
|
+
"underlying_instrument_isin",
|
|
373
|
+
"underlying_instrument_ticker",
|
|
374
|
+
"underlying_instrument_refinitiv_identifier_code",
|
|
375
|
+
"underlying_instrument_instrument_type",
|
|
376
|
+
"target_weight",
|
|
377
|
+
"effective_weight",
|
|
378
|
+
"effective_shares",
|
|
379
|
+
"target_shares",
|
|
380
|
+
"shares",
|
|
381
|
+
"weighting",
|
|
382
|
+
)
|
|
370
383
|
IDENTIFIER = "wbportfolio:tradeproposal"
|
|
371
384
|
search_fields = ("underlying_instrument__name",)
|
|
372
385
|
filterset_fields = {}
|
|
@@ -391,19 +404,82 @@ class TradeTradeProposalModelViewSet(
|
|
|
391
404
|
def get_resource_class(self):
|
|
392
405
|
return TradeProposalTradeResource
|
|
393
406
|
|
|
394
|
-
def get_aggregates(self, queryset,
|
|
407
|
+
def get_aggregates(self, queryset, *args, **kwargs):
|
|
408
|
+
agg = {}
|
|
395
409
|
if queryset.exists():
|
|
396
|
-
|
|
410
|
+
cash_target_cash_weight, cash_target_cash_shares = self.trade_proposal.get_estimated_target_cash(
|
|
411
|
+
self.trade_proposal.portfolio.currency
|
|
412
|
+
)
|
|
413
|
+
extra_existing_cash_components = Cash.objects.filter(
|
|
414
|
+
id__in=self.trade_proposal.trades.filter(underlying_instrument__is_cash=True).values(
|
|
415
|
+
"underlying_instrument"
|
|
416
|
+
)
|
|
417
|
+
).exclude(currency=self.trade_proposal.portfolio.currency)
|
|
418
|
+
|
|
419
|
+
for cash_component in extra_existing_cash_components:
|
|
420
|
+
extra_cash_target_cash_weight, extra_cash_target_cash_shares = (
|
|
421
|
+
self.trade_proposal.get_estimated_target_cash(cash_component.currency)
|
|
422
|
+
)
|
|
423
|
+
cash_target_cash_weight += extra_cash_target_cash_weight
|
|
424
|
+
cash_target_cash_shares += extra_cash_target_cash_shares
|
|
425
|
+
noncash_aggregates = queryset.filter(underlying_instrument__is_cash=False).aggregate(
|
|
397
426
|
sum_target_weight=Sum(F("target_weight")),
|
|
398
427
|
sum_effective_weight=Sum(F("effective_weight")),
|
|
399
|
-
|
|
428
|
+
sum_target_shares=Sum(F("target_shares")),
|
|
429
|
+
sum_effective_shares=Sum(F("effective_shares")),
|
|
400
430
|
)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
431
|
+
cash_aggregates = queryset.filter(underlying_instrument__is_cash=True).aggregate(
|
|
432
|
+
sum_effective_weight=Sum(F("effective_weight")),
|
|
433
|
+
sum_effective_shares=Sum(F("effective_shares")),
|
|
434
|
+
)
|
|
435
|
+
# weights aggregates
|
|
436
|
+
cash_sum_effective_weight = cash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
437
|
+
noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
438
|
+
noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
|
|
439
|
+
sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
440
|
+
sum_sell_weight = queryset.filter(weighting__lt=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
441
|
+
|
|
442
|
+
# shares aggregates
|
|
443
|
+
cash_sum_effective_shares = cash_aggregates["sum_effective_shares"] or Decimal(0)
|
|
444
|
+
noncash_sum_effective_shares = noncash_aggregates["sum_effective_shares"] or Decimal(0)
|
|
445
|
+
noncash_sum_target_shares = noncash_aggregates["sum_target_shares"] or Decimal(0)
|
|
446
|
+
sum_buy_shares = queryset.filter(shares__gte=0).aggregate(s=Sum(F("shares")))["s"] or Decimal(0)
|
|
447
|
+
sum_sell_shares = queryset.filter(shares__lt=0).aggregate(s=Sum(F("shares")))["s"] or Decimal(0)
|
|
448
|
+
|
|
449
|
+
agg = {
|
|
450
|
+
"effective_weight": {
|
|
451
|
+
"Cash": format_number(cash_sum_effective_weight, decimal=6),
|
|
452
|
+
"Non-Cash": format_number(noncash_sum_effective_weight, decimal=6),
|
|
453
|
+
"Total": format_number(noncash_sum_effective_weight + cash_sum_effective_weight, decimal=6),
|
|
454
|
+
},
|
|
455
|
+
"target_weight": {
|
|
456
|
+
"Cash": format_number(cash_target_cash_weight, decimal=6),
|
|
457
|
+
"Non-Cash": format_number(noncash_sum_target_weight, decimal=6),
|
|
458
|
+
"Total": format_number(cash_target_cash_weight + noncash_sum_target_weight, decimal=6),
|
|
459
|
+
},
|
|
460
|
+
"effective_shares": {
|
|
461
|
+
"Cash": format_number(cash_sum_effective_shares, decimal=6),
|
|
462
|
+
"Non-Cash": format_number(noncash_sum_effective_shares, decimal=6),
|
|
463
|
+
"Total": format_number(cash_sum_effective_shares + noncash_sum_effective_shares, decimal=6),
|
|
464
|
+
},
|
|
465
|
+
"target_shares": {
|
|
466
|
+
"Cash": format_number(cash_target_cash_shares, decimal=6),
|
|
467
|
+
"Non-Cash": format_number(noncash_sum_target_shares, decimal=6),
|
|
468
|
+
"Total": format_number(cash_target_cash_shares + noncash_sum_target_shares, decimal=6),
|
|
469
|
+
},
|
|
470
|
+
"weighting": {
|
|
471
|
+
"Cash Flow": format_number(cash_sum_effective_weight - cash_target_cash_weight, decimal=6),
|
|
472
|
+
"Buy": format_number(sum_buy_weight, decimal=6),
|
|
473
|
+
"Sell": format_number(sum_sell_weight, decimal=6),
|
|
474
|
+
},
|
|
475
|
+
"shares": {
|
|
476
|
+
"Cash Flow": format_number(cash_sum_effective_shares - cash_target_cash_shares, decimal=6),
|
|
477
|
+
"Buy": format_number(sum_buy_shares, decimal=6),
|
|
478
|
+
"Sell": format_number(sum_sell_shares, decimal=6),
|
|
479
|
+
},
|
|
405
480
|
}
|
|
406
|
-
|
|
481
|
+
|
|
482
|
+
return agg
|
|
407
483
|
|
|
408
484
|
def get_serializer_class(self):
|
|
409
485
|
if self.trade_proposal.status != TradeProposal.Status.DRAFT:
|
|
@@ -414,14 +490,30 @@ class TradeTradeProposalModelViewSet(
|
|
|
414
490
|
if queryset is not None and queryset.exists():
|
|
415
491
|
total_target_weight = queryset.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
|
|
416
492
|
if round(total_target_weight, 3) != 1:
|
|
417
|
-
warning(
|
|
493
|
+
warning(
|
|
494
|
+
request,
|
|
495
|
+
"The total target weight does not equal 1. To avoid automatic cash allocation, please adjust the trade weights to sum up to 1. Otherwise, a cash component will be added when this trade proposal is submitted.",
|
|
496
|
+
)
|
|
418
497
|
if queryset.filter(status=Trade.Status.FAILED):
|
|
419
498
|
error(
|
|
420
499
|
request,
|
|
421
|
-
"Some
|
|
500
|
+
"Some trades failed preparation. To resolve this, please revert the trade proposal to draft, review and correct the trades, and then resubmit.",
|
|
422
501
|
)
|
|
423
502
|
|
|
424
503
|
def get_queryset(self):
|
|
425
504
|
if self.is_portfolio_manager:
|
|
426
|
-
|
|
427
|
-
|
|
505
|
+
qs = super().get_queryset().filter(trade_proposal=self.kwargs["trade_proposal_id"]).annotate_base_info()
|
|
506
|
+
else:
|
|
507
|
+
qs = TradeProposal.objects.none()
|
|
508
|
+
return qs.annotate(
|
|
509
|
+
underlying_instrument_isin=F("underlying_instrument__isin"),
|
|
510
|
+
underlying_instrument_ticker=F("underlying_instrument__ticker"),
|
|
511
|
+
underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
|
|
512
|
+
underlying_instrument_instrument_type=Case(
|
|
513
|
+
When(
|
|
514
|
+
underlying_instrument__parent__is_security=True,
|
|
515
|
+
then=F("underlying_instrument__parent__instrument_type__short_name"),
|
|
516
|
+
),
|
|
517
|
+
default=F("underlying_instrument__instrument_type__short_name"),
|
|
518
|
+
),
|
|
519
|
+
)
|