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.

Files changed (46) hide show
  1. wbportfolio/factories/__init__.py +1 -3
  2. wbportfolio/factories/dividends.py +1 -0
  3. wbportfolio/factories/portfolios.py +0 -12
  4. wbportfolio/factories/product_groups.py +8 -1
  5. wbportfolio/factories/products.py +18 -0
  6. wbportfolio/factories/trades.py +5 -1
  7. wbportfolio/import_export/handlers/dividend.py +1 -1
  8. wbportfolio/import_export/handlers/fees.py +1 -1
  9. wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -1
  10. wbportfolio/import_export/handlers/trade.py +13 -2
  11. wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -1
  12. wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -1
  13. wbportfolio/migrations/0075_portfolio_initial_position_date_and_more.py +28 -0
  14. wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py +126 -0
  15. wbportfolio/models/portfolio.py +15 -16
  16. wbportfolio/models/transactions/claim.py +8 -7
  17. wbportfolio/models/transactions/dividends.py +3 -20
  18. wbportfolio/models/transactions/rebalancing.py +7 -1
  19. wbportfolio/models/transactions/trade_proposals.py +163 -63
  20. wbportfolio/models/transactions/trades.py +24 -22
  21. wbportfolio/models/transactions/transactions.py +37 -37
  22. wbportfolio/pms/trading/handler.py +1 -1
  23. wbportfolio/pms/typing.py +3 -0
  24. wbportfolio/rebalancing/models/composite.py +14 -1
  25. wbportfolio/risk_management/backends/exposure_portfolio.py +30 -1
  26. wbportfolio/risk_management/tests/test_exposure_portfolio.py +47 -0
  27. wbportfolio/serializers/portfolios.py +26 -0
  28. wbportfolio/serializers/transactions/trades.py +13 -0
  29. wbportfolio/tests/models/test_portfolios.py +2 -2
  30. wbportfolio/tests/models/transactions/test_trade_proposals.py +381 -0
  31. wbportfolio/tests/models/transactions/test_trades.py +14 -0
  32. wbportfolio/tests/signals.py +1 -1
  33. wbportfolio/tests/viewsets/test_performances.py +2 -1
  34. wbportfolio/viewsets/configs/display/assets.py +0 -11
  35. wbportfolio/viewsets/configs/display/portfolios.py +58 -14
  36. wbportfolio/viewsets/configs/display/products.py +0 -13
  37. wbportfolio/viewsets/configs/display/trades.py +24 -17
  38. wbportfolio/viewsets/configs/endpoints/trades.py +9 -0
  39. wbportfolio/viewsets/configs/endpoints/transactions.py +6 -0
  40. wbportfolio/viewsets/portfolios.py +39 -8
  41. wbportfolio/viewsets/transactions/trade_proposals.py +1 -1
  42. wbportfolio/viewsets/transactions/trades.py +105 -13
  43. {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.1.dist-info}/METADATA +1 -1
  44. {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.1.dist-info}/RECORD +46 -43
  45. {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.1.dist-info}/WHEEL +0 -0
  46. {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 InstrumentPriceFactory, ProductFactory
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(key="name", label="Name"),
21
- dp.Field(key="currency", label="Currency"),
22
- dp.Field(key="updated_at", label="Updated At"),
23
- dp.Field(key="depends_on", label="Depends on"),
24
- dp.Field(key="automatic_rebalancer", label="Rebalancer"),
25
- dp.Field(key="invested_timespan", label="Invested"),
26
- dp.Field(key="is_manageable", label="Managed"),
27
- dp.Field(key="is_tracked", label="Tracked"),
28
- dp.Field(key="only_weighting", label="Only-Weight"),
29
- dp.Field(key="is_lookthrough", label="Lookthrough"),
30
- dp.Field(key="is_composition", label="Composition"),
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-ok, %m-%d})"
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="effective_weights", label=rebalancing_column_label),
166
- dp.Field(key="rebalancing_weights", label=effective_column_label),
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(key="underlying_instrument", label="Instrument"),
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=[SHARE_FORMATTING, TRADE_STATUS_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 = search_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
- "currency",
85
- )
86
- .prefetch_related(
87
- "depends_on",
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 not instance.has_all_check_completed_and_succeed:
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 = ("target_weight", "effective_weight", "effective_shares", "target_shares", "shares", "weighting")
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, paginated_queryset):
407
+ def get_aggregates(self, queryset, *args, **kwargs):
408
+ agg = {}
395
409
  if queryset.exists():
396
- aggregates = queryset.aggregate(
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
- sum_weighting=Sum(F("weighting")),
428
+ sum_target_shares=Sum(F("target_shares")),
429
+ sum_effective_shares=Sum(F("effective_shares")),
400
430
  )
401
- return {
402
- "target_weight": {"Σ": format_number(aggregates["sum_target_weight"], decimal=5)},
403
- "effective_weight": {"Σ": format_number(aggregates["sum_effective_weight"], decimal=5)},
404
- "weighting": {"Σ": format_number(aggregates["sum_weighting"], decimal=5)},
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
- return {}
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(request, "The total target weight does not equals to 1")
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 Trades couldn't be succesfully prepared. Please revert back to draft and correct these trades accordingly.",
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
- return super().get_queryset().filter(trade_proposal=self.kwargs["trade_proposal_id"]).annotate_base_info()
427
- return TradeProposal.objects.none()
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.48.0
3
+ Version: 1.49.1
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*