wbportfolio 1.55.8__py2.py3-none-any.whl → 1.59.4__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 (128) hide show
  1. wbportfolio/admin/orders/order_proposals.py +2 -0
  2. wbportfolio/admin/orders/orders.py +2 -0
  3. wbportfolio/admin/portfolio.py +11 -5
  4. wbportfolio/api_clients/ubs.py +23 -11
  5. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  6. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  7. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  8. wbportfolio/contrib/company_portfolio/models.py +69 -39
  9. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  10. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  11. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  12. wbportfolio/factories/assets.py +1 -1
  13. wbportfolio/factories/orders/order_proposals.py +3 -1
  14. wbportfolio/factories/orders/orders.py +8 -3
  15. wbportfolio/factories/product_groups.py +3 -3
  16. wbportfolio/factories/products.py +3 -3
  17. wbportfolio/filters/assets.py +0 -1
  18. wbportfolio/filters/orders/order_proposals.py +3 -6
  19. wbportfolio/filters/portfolios.py +18 -1
  20. wbportfolio/filters/positions.py +0 -1
  21. wbportfolio/filters/transactions/fees.py +0 -2
  22. wbportfolio/filters/transactions/trades.py +0 -1
  23. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  24. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  25. wbportfolio/import_export/handlers/asset_position.py +9 -5
  26. wbportfolio/import_export/handlers/dividend.py +1 -1
  27. wbportfolio/import_export/handlers/fees.py +2 -2
  28. wbportfolio/import_export/handlers/trade.py +4 -4
  29. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  30. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  31. wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
  32. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  33. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  34. wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
  35. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  36. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  37. wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
  38. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  39. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  40. wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
  41. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  42. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  43. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  44. wbportfolio/import_export/parsers/ubs/equity.py +2 -1
  45. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  46. wbportfolio/import_export/resources/trades.py +1 -1
  47. wbportfolio/import_export/utils.py +3 -1
  48. wbportfolio/metric/backends/base.py +2 -2
  49. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  50. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  51. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  52. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  53. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  54. wbportfolio/models/adjustments.py +1 -1
  55. wbportfolio/models/asset.py +7 -3
  56. wbportfolio/models/builder.py +25 -5
  57. wbportfolio/models/custodians.py +3 -3
  58. wbportfolio/models/exceptions.py +1 -1
  59. wbportfolio/models/graphs/portfolio.py +1 -1
  60. wbportfolio/models/graphs/utils.py +11 -11
  61. wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
  62. wbportfolio/models/orders/order_proposals.py +620 -490
  63. wbportfolio/models/orders/orders.py +237 -75
  64. wbportfolio/models/portfolio.py +79 -18
  65. wbportfolio/models/portfolio_relationship.py +6 -0
  66. wbportfolio/models/products.py +3 -0
  67. wbportfolio/models/rebalancing.py +4 -1
  68. wbportfolio/models/roles.py +4 -10
  69. wbportfolio/models/transactions/claim.py +6 -5
  70. wbportfolio/models/transactions/dividends.py +1 -0
  71. wbportfolio/models/transactions/trades.py +4 -0
  72. wbportfolio/models/transactions/transactions.py +16 -4
  73. wbportfolio/models/utils.py +100 -1
  74. wbportfolio/order_routing/__init__.py +16 -0
  75. wbportfolio/order_routing/adapters/__init__.py +14 -6
  76. wbportfolio/order_routing/adapters/ubs.py +104 -70
  77. wbportfolio/order_routing/router.py +33 -0
  78. wbportfolio/order_routing/tests/test_router.py +110 -0
  79. wbportfolio/permissions.py +7 -0
  80. wbportfolio/pms/trading/__init__.py +0 -1
  81. wbportfolio/pms/trading/optimizer.py +61 -0
  82. wbportfolio/pms/typing.py +115 -103
  83. wbportfolio/rebalancing/models/composite.py +1 -1
  84. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
  85. wbportfolio/risk_management/backends/__init__.py +1 -0
  86. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  87. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  88. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  89. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  90. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  91. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  92. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  93. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  94. wbportfolio/serializers/orders/order_proposals.py +6 -2
  95. wbportfolio/serializers/orders/orders.py +119 -26
  96. wbportfolio/serializers/transactions/claim.py +2 -2
  97. wbportfolio/tasks.py +42 -4
  98. wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
  99. wbportfolio/tests/models/test_portfolios.py +9 -9
  100. wbportfolio/tests/models/test_splits.py +1 -6
  101. wbportfolio/tests/models/test_utils.py +140 -0
  102. wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
  103. wbportfolio/tests/rebalancing/test_models.py +2 -2
  104. wbportfolio/tests/viewsets/test_products.py +1 -0
  105. wbportfolio/urls.py +1 -1
  106. wbportfolio/viewsets/charts/assets.py +8 -4
  107. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  108. wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
  109. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  110. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  111. wbportfolio/viewsets/esg.py +3 -5
  112. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
  113. wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
  114. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
  115. wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
  116. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
  117. wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
  118. wbportfolio/viewsets/orders/order_proposals.py +92 -21
  119. wbportfolio/viewsets/orders/orders.py +79 -26
  120. wbportfolio/viewsets/portfolios.py +24 -0
  121. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
  122. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
  123. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  124. wbportfolio/fdm/tasks.py +0 -42
  125. wbportfolio/models/orders/routing.py +0 -54
  126. wbportfolio/pms/trading/handler.py +0 -211
  127. /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
  128. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,140 @@
1
+ from decimal import Decimal
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+ from pandas._libs.tslibs.offsets import BDay
6
+
7
+ from wbportfolio.models import AssetPosition, Order, OrderProposal, Portfolio
8
+ from wbportfolio.models.utils import adjust_assets, adjust_orders, adjust_quote, get_adjusted_shares
9
+
10
+
11
+ def test_get_adjusted_shares():
12
+ assert get_adjusted_shares(Decimal("150"), Decimal("100"), Decimal("200")) == Decimal("75")
13
+
14
+
15
+ @pytest.mark.django_db
16
+ @patch.object(Order, "_get_price")
17
+ def test_adjust_orders(mock_get_price, weekday, order_factory, instrument_factory):
18
+ mock_get_price.return_value = (Decimal("100"), Decimal("0"))
19
+ old_quote = instrument_factory.create()
20
+ new_quote = instrument_factory.create()
21
+ o1 = order_factory.create(
22
+ underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("10")
23
+ )
24
+ o2 = order_factory.create(
25
+ underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("20")
26
+ )
27
+ adjust_orders(Order.objects.filter(underlying_instrument=old_quote), new_quote)
28
+
29
+ o1.refresh_from_db()
30
+ o2.refresh_from_db()
31
+
32
+ assert o1.underlying_instrument == new_quote
33
+ assert o2.underlying_instrument == new_quote
34
+ assert o1.price == Decimal("100")
35
+ assert o2.price == Decimal("100")
36
+ assert o1.shares == Decimal("10")
37
+ assert o2.shares == Decimal("20")
38
+
39
+ mock_get_price.return_value = (Decimal("200"), Decimal("0"))
40
+ adjust_orders(Order.objects.filter(underlying_instrument=new_quote), old_quote)
41
+
42
+ o1.refresh_from_db()
43
+ o2.refresh_from_db()
44
+
45
+ assert o1.underlying_instrument == old_quote
46
+ assert o2.underlying_instrument == old_quote
47
+ assert o1.price == Decimal("200")
48
+ assert o2.price == Decimal("200")
49
+ assert o1.shares == Decimal("5")
50
+ assert o2.shares == Decimal("10")
51
+
52
+
53
+ @pytest.mark.django_db
54
+ def test_adjust_assets(weekday, asset_position_factory, instrument_factory, instrument_price_factory):
55
+ old_quote = instrument_factory.create()
56
+ new_quote = instrument_factory.create()
57
+ old_quote_price = instrument_price_factory.create(
58
+ instrument=old_quote, net_value=Decimal("100"), date=weekday, calculated=False
59
+ )
60
+ new_quote_price = instrument_price_factory.create(
61
+ instrument=new_quote, net_value=Decimal("100"), date=weekday, calculated=False
62
+ )
63
+
64
+ a1 = asset_position_factory.create(
65
+ underlying_quote=old_quote, date=weekday, initial_price=Decimal("100"), initial_shares=Decimal("10")
66
+ )
67
+ a2 = asset_position_factory.create(
68
+ underlying_quote=old_quote, date=weekday, initial_price=Decimal("100"), initial_shares=Decimal("20")
69
+ )
70
+ adjust_assets(AssetPosition.objects.filter(underlying_quote=old_quote), new_quote)
71
+
72
+ a1.refresh_from_db()
73
+ a2.refresh_from_db()
74
+
75
+ assert a1.underlying_quote == new_quote
76
+ assert a2.underlying_quote == new_quote
77
+ assert a1.underlying_quote_price == new_quote_price
78
+ assert a2.underlying_quote_price == new_quote_price
79
+ assert a1.initial_price == Decimal("100")
80
+ assert a2.initial_price == Decimal("100")
81
+ assert a1.initial_shares == Decimal("10")
82
+ assert a2.initial_shares == Decimal("20")
83
+
84
+ old_quote_price.net_value = new_quote_price.net_value = Decimal("200")
85
+ old_quote_price.save()
86
+ new_quote_price.save()
87
+
88
+ adjust_assets(AssetPosition.objects.filter(underlying_quote=new_quote), old_quote)
89
+
90
+ a1.refresh_from_db()
91
+ a2.refresh_from_db()
92
+
93
+ assert a1.underlying_quote == old_quote
94
+ assert a2.underlying_quote == old_quote
95
+ assert a1.underlying_quote_price == old_quote_price
96
+ assert a2.underlying_quote_price == old_quote_price
97
+ assert a1.initial_price == Decimal("200")
98
+ assert a2.initial_price == Decimal("200")
99
+ assert a1.initial_shares == Decimal("5")
100
+ assert a2.initial_shares == Decimal("10")
101
+
102
+
103
+ @pytest.mark.django_db
104
+ @patch("wbportfolio.models.utils.adjust_assets")
105
+ @patch("wbportfolio.models.utils.adjust_orders")
106
+ @patch.object(OrderProposal, "replay")
107
+ def test_adjust_quote(mock_replay, mock_adjust_orders, mock_adjust_assets, weekday, order_factory, instrument_factory):
108
+ old_quote = instrument_factory.create()
109
+ new_quote = instrument_factory.create()
110
+ o1 = order_factory.create( # noqa: F841
111
+ underlying_instrument=old_quote, order_proposal__trade_date=weekday, price=Decimal("100"), shares=Decimal("10")
112
+ )
113
+ o2 = order_factory.create( # noqa: F841
114
+ underlying_instrument=old_quote,
115
+ order_proposal__trade_date=(weekday + BDay(1)),
116
+ price=Decimal("100"),
117
+ shares=Decimal("10"),
118
+ )
119
+ o3 = order_factory.create(
120
+ underlying_instrument=old_quote,
121
+ order_proposal__trade_date=(weekday + BDay(1)),
122
+ price=Decimal("100"),
123
+ shares=Decimal("10"),
124
+ )
125
+ adjust_quote(
126
+ old_quote,
127
+ new_quote,
128
+ adjust_after=weekday,
129
+ only_portfolios=Portfolio.objects.filter(id=o3.order_proposal.portfolio.id),
130
+ )
131
+
132
+ mock_adjust_assets.asser_called_once()
133
+ assert set(mock_adjust_assets.call_args[0][0]) == set(AssetPosition.objects.none())
134
+ assert mock_adjust_assets.call_args[0][1] == new_quote
135
+
136
+ mock_adjust_orders.assert_called_once()
137
+ assert set(mock_adjust_orders.call_args[0][0]) == set(Order.objects.filter(id=o3.id))
138
+ assert mock_adjust_orders.call_args[0][1] == new_quote
139
+
140
+ mock_replay.assert_called_once_with(reapply_order_proposal=True)
@@ -65,7 +65,7 @@ class TestRebalancer:
65
65
  )
66
66
  order_proposal = rebalancer.evaluate_rebalancing(trade_date)
67
67
  assert order_proposal.orders.count() == 2
68
- assert order_proposal.status == OrderProposal.Status.APPLIED
68
+ assert order_proposal.status == OrderProposal.Status.CONFIRMED
69
69
  assert AssetPosition.objects.get(
70
70
  portfolio=rebalancer.portfolio, date=trade_date, underlying_quote=a1.underlying_instrument
71
71
  ).weighting == Decimal(0.5)
@@ -82,7 +82,7 @@ class TestCompositeRebalancing:
82
82
  assert not model.is_valid()
83
83
 
84
84
  order_proposal = OrderProposalFactory.create(
85
- portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.APPLIED
85
+ portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.CONFIRMED
86
86
  )
87
87
  t1 = OrderFactory.create(
88
88
  portfolio=model.portfolio,
@@ -104,7 +104,7 @@ class TestCompositeRebalancing:
104
104
 
105
105
  def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
106
106
  order_proposal = OrderProposalFactory.create(
107
- portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.APPLIED
107
+ portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.CONFIRMED
108
108
  )
109
109
  asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
110
110
  asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
@@ -23,6 +23,7 @@ class TestProductModelViewSet:
23
23
  portfolio_factory.create_batch(4, invested_timespan=DateRange(date.min, date.max))
24
24
  + portfolio_factory.create_batch(2, invested_timespan=DateRange(date.min, date.max)),
25
25
  product_factory.create_batch(6),
26
+ strict=False,
26
27
  ):
27
28
  InstrumentPortfolioThroughModel.objects.update_or_create(
28
29
  instrument=product, defaults={"portfolio": portfolio}
wbportfolio/urls.py CHANGED
@@ -139,7 +139,7 @@ product_router.register(r"fees", viewsets.FeesProductModelViewSet, basename="pro
139
139
 
140
140
  # Subrouter for Order Proposal
141
141
  order_proposal_router = WBCoreRouter()
142
- order_proposal_router.register(r"trade", viewsets.OrderOrderProposalModelViewSet, basename="orderproposal-order")
142
+ order_proposal_router.register(r"order", viewsets.OrderOrderProposalModelViewSet, basename="orderproposal-order")
143
143
 
144
144
  trade_router = WBCoreRouter()
145
145
  trade_router.register(r"claim", viewsets.ClaimTradeModelViewSet, basename="trade-claim")
@@ -101,7 +101,9 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
101
101
  columns = {}
102
102
  level_representations = self.classification_group.get_levels_representation()
103
103
  for key, label in zip(
104
- reversed(self.classification_group.get_fields_names(sep="_")), reversed(level_representations[1:])
104
+ reversed(self.classification_group.get_fields_names(sep="_")),
105
+ reversed(level_representations[1:]),
106
+ strict=False,
105
107
  ):
106
108
  columns[key] = label
107
109
  columns["label"] = "Classification"
@@ -158,6 +160,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
158
160
  return df.reset_index(names="id")
159
161
 
160
162
  def manipulate_dataframe(self, df):
163
+ df["id"] = df["id"].fillna(-1)
161
164
  if self.group_by == AssetPositionGroupBy.INDUSTRY:
162
165
  if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=self.portfolio):
163
166
  df["equity"] = ""
@@ -169,8 +172,8 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
169
172
  dict(Classification.objects.filter(id__in=df["classification"].dropna()).values_list("id", "name"))
170
173
  )
171
174
  elif self.group_by == AssetPositionGroupBy.CASH:
172
- df.loc[df["id"] == True, "label"] = "Cash"
173
- df.loc[df["id"] == False, "label"] = "Non-Cash"
175
+ df.loc[df["id"], "label"] = "Cash"
176
+ df.loc[~df["id"], "label"] = "Non-Cash"
174
177
  elif self.group_by == AssetPositionGroupBy.COUNTRY:
175
178
  df["label"] = df["id"].map(dict(Geography.objects.filter(id__in=df["id"]).values_list("id", "name")))
176
179
  elif self.group_by == AssetPositionGroupBy.CURRENCY:
@@ -180,6 +183,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
180
183
  df["label"] = df["id"].map(
181
184
  dict(InstrumentType.objects.filter(id__in=df["id"]).values_list("id", "short_name"))
182
185
  )
186
+ df.loc[df["id"] == -1, "label"] = "N/A"
183
187
  df.sort_values(by="weighting", ascending=False, inplace=True)
184
188
  return df
185
189
 
@@ -307,7 +311,7 @@ class ContributorPortfolioChartView(UserPortfolioRequestPermissionMixin, viewset
307
311
 
308
312
  text_forex = df_forex.contribution_forex.apply(lambda x: f"{x:,.2%}")
309
313
  text_equity = contribution_equity.apply(lambda x: f"{x:,.2%}")
310
- setattr(self, "nb_rows", df.shape[0])
314
+ self.nb_rows = df.shape[0]
311
315
  fig.add_trace(
312
316
  go.Bar(
313
317
  y=df.instrument_id,
@@ -96,7 +96,7 @@ class AssetPositionPortfolioButtonConfig(AssetPositionButtonConfig):
96
96
  request=self.request,
97
97
  )
98
98
  ),
99
- label=f"{PortfolioPortfolioThroughModel.Type[rel.type].label} Portfolio",
99
+ label=f"Dependency Portfolio ({PortfolioPortfolioThroughModel.Type[rel.type].label})",
100
100
  )
101
101
  )
102
102
  return set(btns)
@@ -7,7 +7,7 @@ from wbfdm.models.instruments import Instrument
7
7
 
8
8
  class InstrumentButtonMixin:
9
9
  @classmethod
10
- def add_instrument_request_button(self, request=None, view=None, pk=None, **kwargs):
10
+ def add_instrument_request_button(cls, request=None, view=None, pk=None, **kwargs):
11
11
  buttons = [
12
12
  bt.WidgetButton(key="assets", label="Implemented Portfolios (Assets)"),
13
13
  # bt.WidgetButton(
@@ -44,7 +44,7 @@ class InstrumentButtonMixin:
44
44
  )
45
45
 
46
46
  @classmethod
47
- def add_transactions_request_button(self, request=None, view=None, pk=None, **kwargs):
47
+ def add_transactions_request_button(cls, request=None, view=None, pk=None, **kwargs):
48
48
  return bt.DropDownButton(
49
49
  label="Transactions",
50
50
  icon=WBIcon.UNFOLD.icon,
@@ -2,6 +2,7 @@ from contextlib import suppress
2
2
  from datetime import date
3
3
 
4
4
  from pandas._libs.tslibs.offsets import BDay
5
+ from rest_framework.reverse import reverse
5
6
  from wbcore import serializers as wb_serializers
6
7
  from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
7
8
  from wbcore.contrib.icons import WBIcon
@@ -11,9 +12,11 @@ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
11
12
  from wbcore.metadata.configs.display.instance_display.shortcuts import (
12
13
  create_simple_display,
13
14
  )
15
+ from wbfdm.models import Instrument
16
+ from wbfdm.serializers import InvestableUniverseRepresentationSerializer
14
17
 
15
18
  from wbportfolio.models import AssetPosition, Portfolio
16
- from wbportfolio.serializers import RebalancerModelSerializer
19
+ from wbportfolio.serializers import PortfolioRepresentationSerializer, RebalancerModelSerializer
17
20
  from wbportfolio.viewsets.configs.display.rebalancing import RebalancerDisplayConfig
18
21
 
19
22
 
@@ -32,6 +35,28 @@ class CreateModelPortfolioSerializer(wb_serializers.ModelSerializer):
32
35
  )
33
36
 
34
37
 
38
+ class AdjustQuoteSerializer(wb_serializers.Serializer):
39
+ old_quote = wb_serializers.PrimaryKeyRelatedField(queryset=Instrument.objects.all())
40
+ _old_quote = InvestableUniverseRepresentationSerializer(source="old_quote")
41
+
42
+ new_quote = wb_serializers.PrimaryKeyRelatedField(queryset=Instrument.objects.all())
43
+ _new_quote = InvestableUniverseRepresentationSerializer(
44
+ source="new_quote",
45
+ optional_get_parameters={"old_quote": "sibling_of"},
46
+ depends_on=[{"field": "old_quote", "options": {}}],
47
+ )
48
+
49
+ only_portfolios = wb_serializers.PrimaryKeyRelatedField(queryset=Portfolio.objects.all(), required=False)
50
+ _only_portfolios = PortfolioRepresentationSerializer(
51
+ source="only_portfolios",
52
+ optional_get_parameters={"old_quote": "invests_in"},
53
+ depends_on=[{"field": "new_quote", "options": {}}],
54
+ many=True,
55
+ )
56
+
57
+ adjust_after = wb_serializers.DateField(required=False)
58
+
59
+
35
60
  def _get_portfolio_start_end_serializer_class(portfolio):
36
61
  today = date.today()
37
62
 
@@ -60,6 +85,25 @@ def _get_rebalance_serializer_class(portfolio):
60
85
 
61
86
 
62
87
  class PortfolioButtonConfig(ButtonViewConfig):
88
+ def get_custom_buttons(self) -> set:
89
+ return {
90
+ bt.ActionButton(
91
+ method=RequestType.POST,
92
+ identifiers=("wbportfolio:portfolio",),
93
+ endpoint=reverse("wbportfolio:portfolio-adjustquote", args=[], request=self.request),
94
+ label="Adjust quote",
95
+ serializer=AdjustQuoteSerializer,
96
+ action_label="Action triggered",
97
+ title="Adjust Quote",
98
+ instance_display=create_simple_display(
99
+ [
100
+ ["old_quote", "new_quote", "adjust_after"],
101
+ ["only_portfolios", "only_portfolios", "only_portfolios"],
102
+ ]
103
+ ),
104
+ ),
105
+ }
106
+
63
107
  def get_custom_instance_buttons(self):
64
108
  admin_buttons = [
65
109
  bt.ActionButton(
@@ -49,14 +49,14 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
49
49
  ]
50
50
  editable = [dp.FormattingRule(style={"fontWeight": "bold"})]
51
51
  equal = [dp.FormattingRule(condition=("==", False, "is_equal"), style={"backgroundColor": "orange"})]
52
- borderLeft = [
52
+ border_left = [
53
53
  dp.FormattingRule(
54
54
  style={
55
55
  "borderLeft": "1px solid #bdc3c7",
56
56
  }
57
57
  )
58
58
  ]
59
- borderRight = [
59
+ border_right = [
60
60
  dp.FormattingRule(
61
61
  style={
62
62
  "borderRight": "1px solid #bdc3c7",
@@ -111,7 +111,7 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
111
111
  key="shares_external",
112
112
  label="Shares",
113
113
  width=90,
114
- formatting_rules=[*equal, *editable, *borderLeft],
114
+ formatting_rules=[*equal, *editable, *border_left],
115
115
  ),
116
116
  dp.Field(
117
117
  key="nominal_value_external",
@@ -128,7 +128,7 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
128
128
  key="assets_under_management_external",
129
129
  label="AuM",
130
130
  width=120,
131
- formatting_rules=[*equal, *borderRight],
131
+ formatting_rules=[*equal, *border_right],
132
132
  ),
133
133
  ],
134
134
  ),
@@ -58,12 +58,10 @@ class ESGMetricAggregationPortfolioPandasViewSet(UserPortfolioRequestPermissionM
58
58
  df = (
59
59
  pd.DataFrame(
60
60
  AssetPosition.objects.filter(portfolio=self.portfolio, date=self.val_date)
61
- .exclude(
62
- Q(underlying_instrument__is_cash=True) | Q(underlying_instrument__is_cash_equivalent=True)
63
- )
64
- .values("underlying_instrument", "weighting", "total_value_fx_usd")
61
+ .exclude(Q(underlying_quote__is_cash=True) | Q(underlying_quote__is_cash_equivalent=True))
62
+ .values("underlying_quote", "weighting", "total_value_fx_usd")
65
63
  )
66
- .groupby("underlying_instrument")
64
+ .groupby("underlying_quote")
67
65
  .sum()
68
66
  .astype(float)
69
67
  )
@@ -9,6 +9,7 @@ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
9
9
  from wbcore.metadata.configs.display import create_simple_display
10
10
 
11
11
  from wbportfolio.models import OrderProposal, Portfolio
12
+ from wbportfolio.order_routing import ExecutionStatus
12
13
  from wbportfolio.serializers import PortfolioRepresentationSerializer
13
14
 
14
15
 
@@ -24,12 +25,61 @@ class ResetSerializer(wb_serializers.Serializer):
24
25
  )
25
26
 
26
27
 
28
+ class ExecuteSerializer(wb_serializers.Serializer):
29
+ prioritize_target_weight = wb_serializers.BooleanField(
30
+ default=False,
31
+ label="Prioritize Target Weight",
32
+ help_text="If True, we will communicate to the custodian to prioritize target weight in case both shares and target weight are communicated.",
33
+ )
34
+
35
+
27
36
  class OrderProposalButtonConfig(ButtonViewConfig):
28
37
  def get_custom_list_instance_buttons(self):
29
38
  buttons = []
30
39
  with suppress(AttributeError, AssertionError):
31
40
  order_proposal = self.view.get_object()
32
- if order_proposal.status == OrderProposal.Status.APPLIED.value and order_proposal.portfolio.is_model:
41
+ if order_proposal.can_execute(self.request.user):
42
+ buttons.append(
43
+ bt.ActionButton(
44
+ method=RequestType.PATCH,
45
+ identifiers=("wbportfolio:order",),
46
+ icon=WBIcon.DEAL_MONEY.icon,
47
+ endpoint=reverse("wbportfolio:orderproposal-execute", args=[order_proposal.id]),
48
+ label="Execute",
49
+ action_label="Execute",
50
+ description_fields="<p>Execute the orders through the setup custodian.</p>",
51
+ serializer=ExecuteSerializer,
52
+ instance_display=create_simple_display([["prioritize_target_weight"]]),
53
+ )
54
+ )
55
+
56
+ elif order_proposal.status == OrderProposal.Status.EXECUTION and order_proposal.execution_status in [
57
+ ExecutionStatus.PENDING,
58
+ ExecutionStatus.IN_DRAFT,
59
+ ]:
60
+ buttons.append(
61
+ bt.ActionButton(
62
+ method=RequestType.PATCH,
63
+ identifiers=("wbportfolio:order",),
64
+ icon=WBIcon.PREVIOUS.icon,
65
+ endpoint=reverse("wbportfolio:orderproposal-cancelexecution", args=[order_proposal.id]),
66
+ label="Cancel Execution",
67
+ action_label="Cancel Execution",
68
+ description_fields="<p>Cancel the current requested execution. Time sensitive operation.</p>",
69
+ )
70
+ )
71
+ buttons.append(
72
+ bt.ActionButton(
73
+ method=RequestType.PATCH,
74
+ identifiers=("wbportfolio:order",),
75
+ icon=WBIcon.REFRESH.icon,
76
+ endpoint=reverse("wbportfolio:orderproposal-updateexecutionstatus", args=[order_proposal.id]),
77
+ label="Update Execution Status",
78
+ action_label="Update Execution Status",
79
+ description_fields="<p>Update Execution Status.<p>",
80
+ )
81
+ )
82
+ if order_proposal.can_be_confirmed and order_proposal.portfolio.is_model:
33
83
 
34
84
  class PushModelChangeSerializer(wb_serializers.Serializer):
35
85
  only_for_portfolio_ids = wb_serializers.PrimaryKeyRelatedField(
@@ -48,7 +98,7 @@ class OrderProposalButtonConfig(ButtonViewConfig):
48
98
  buttons.append(
49
99
  bt.ActionButton(
50
100
  method=RequestType.PATCH,
51
- identifiers=("wbportfolio:orderproposal",),
101
+ identifiers=("wbportfolio:order",),
52
102
  endpoint=reverse("wbportfolio:orderproposal-pushmodelchange", args=[order_proposal.id]),
53
103
  icon=WBIcon.BROADCAST.icon,
54
104
  label="Push Model Changes",
@@ -63,13 +113,25 @@ class OrderProposalButtonConfig(ButtonViewConfig):
63
113
  ),
64
114
  )
65
115
  )
116
+ if order_proposal.status == OrderProposal.Status.DRAFT:
117
+ buttons.append(
118
+ bt.ActionButton(
119
+ method=RequestType.PATCH,
120
+ identifiers=("wbportfolio:order",),
121
+ endpoint=reverse("wbportfolio:orderproposal-refreshpretradechecks", args=[order_proposal.id]),
122
+ icon=WBIcon.RUNNING.icon,
123
+ label="Evaluate Pre-Trade Checks",
124
+ action_label="Checks are running.",
125
+ title="Evaluate Pre-Trade Checks",
126
+ )
127
+ )
66
128
  buttons.append(
67
129
  bt.DropDownButton(
68
130
  label="Tools",
69
131
  buttons=(
70
132
  bt.ActionButton(
71
133
  method=RequestType.PATCH,
72
- identifiers=("wbportfolio:orderproposal",),
134
+ identifiers=("wbportfolio:order",),
73
135
  key="replay",
74
136
  icon=WBIcon.SYNCHRONIZE.icon,
75
137
  label="Replay Orders",
@@ -81,7 +143,7 @@ class OrderProposalButtonConfig(ButtonViewConfig):
81
143
  ),
82
144
  bt.ActionButton(
83
145
  method=RequestType.PATCH,
84
- identifiers=("wbportfolio:orderproposal",),
146
+ identifiers=("wbportfolio:order",),
85
147
  key="reset",
86
148
  icon=WBIcon.REGENERATE.icon,
87
149
  label="Reset Orders",
@@ -96,12 +158,12 @@ class OrderProposalButtonConfig(ButtonViewConfig):
96
158
  ),
97
159
  bt.ActionButton(
98
160
  method=RequestType.PATCH,
99
- identifiers=("wbportfolio:orderproposal",),
161
+ identifiers=("wbportfolio:order",),
100
162
  key="normalize",
101
163
  icon=WBIcon.EDIT.icon,
102
164
  label="Normalize Orders",
103
165
  description_fields="""
104
- <p>Make sure all orders normalize to a total target weight of (100 - {{total_cash_weight}})%</p>
166
+ <p>Make sure all orders normalize to a total target weight of (1 - {{total_cash_weight}})</p>
105
167
  """,
106
168
  action_label="Normalize Orders",
107
169
  title="Normalize Orders",
@@ -110,15 +172,12 @@ class OrderProposalButtonConfig(ButtonViewConfig):
110
172
  ),
111
173
  bt.ActionButton(
112
174
  method=RequestType.PATCH,
113
- identifiers=("wbportfolio:orderproposal",),
114
- key="deleteall",
115
- icon=WBIcon.DELETE.icon,
116
- label="Delete All Orders",
117
- description_fields="""
118
- <p>Delete all orders from this order proposal?</p>
119
- """,
120
- action_label="Delete All Orders",
121
- title="Delete All Orders",
175
+ identifiers=("wbportfolio:order",),
176
+ key="refresh_return",
177
+ icon=WBIcon.REFRESH.icon,
178
+ label="Refresh Returns & Price",
179
+ action_label="Refresh Returns & Price",
180
+ title="Refresh Returns & Price",
122
181
  ),
123
182
  ),
124
183
  )
@@ -1,5 +1,86 @@
1
+ from wbcore import serializers as wb_serializers
2
+ from wbcore.contrib.icons import WBIcon
3
+ from wbcore.enums import RequestType
4
+ from wbcore.metadata.configs import buttons as bt
1
5
  from wbcore.metadata.configs.buttons.enums import Button
2
6
  from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
7
+ from wbcore.metadata.configs.display import create_simple_display
8
+
9
+ from wbportfolio.order_routing import ExecutionInstruction
10
+
11
+
12
+ class ExecutionInstructionSerializer(wb_serializers.Serializer):
13
+ execution_instruction = wb_serializers.ChoiceField(
14
+ choices=ExecutionInstruction.choices,
15
+ default=ExecutionInstruction.MARKET_ON_CLOSE.value,
16
+ clear_dependent_fields=False,
17
+ )
18
+ apply_execution_instruction_to_all_orders = wb_serializers.BooleanField(
19
+ default=False, label="Apply Execution instruction to all orders"
20
+ )
21
+
22
+ percentage = wb_serializers.FloatField(
23
+ label="Percentage [1% - 25%]",
24
+ percent=True,
25
+ required=False,
26
+ allow_null=True,
27
+ on_unsatisfied_deps="hide",
28
+ depends_on=[
29
+ {
30
+ "field": "execution_instruction",
31
+ "options": {"activates_on": [ExecutionInstruction.IN_LINE_WITH_VOLUME.value]},
32
+ }
33
+ ],
34
+ )
35
+ price = wb_serializers.FloatField(
36
+ label="Price (optional)",
37
+ required=False,
38
+ allow_null=True,
39
+ on_unsatisfied_deps="hide",
40
+ depends_on=[
41
+ {
42
+ "field": "execution_instruction",
43
+ "options": {"activates_on": [ExecutionInstruction.LIMIT_ORDER.value]},
44
+ }
45
+ ],
46
+ )
47
+ good_for_date = wb_serializers.DateField(
48
+ label="Good For Date (optional)",
49
+ required=False,
50
+ allow_null=True,
51
+ on_unsatisfied_deps="hide",
52
+ depends_on=[
53
+ {
54
+ "field": "execution_instruction",
55
+ "options": {"activates_on": [ExecutionInstruction.LIMIT_ORDER.value]},
56
+ }
57
+ ],
58
+ )
59
+ period = wb_serializers.IntegerField(
60
+ label="Period in minutes",
61
+ required=False,
62
+ allow_null=True,
63
+ on_unsatisfied_deps="hide",
64
+ depends_on=[
65
+ {
66
+ "field": "execution_instruction",
67
+ "options": {"activates_on": [ExecutionInstruction.VWAP.value, ExecutionInstruction.TWAP.value]},
68
+ }
69
+ ],
70
+ )
71
+ time = wb_serializers.TimeField(
72
+ label="Time (UTC)",
73
+ format="%H:%M",
74
+ required=False,
75
+ allow_null=True,
76
+ on_unsatisfied_deps="hide",
77
+ depends_on=[
78
+ {
79
+ "field": "execution_instruction",
80
+ "options": {"activates_on": [ExecutionInstruction.VWAP.value, ExecutionInstruction.TWAP.value]},
81
+ }
82
+ ],
83
+ )
3
84
 
4
85
 
5
86
  class OrderOrderProposalButtonConfig(ButtonViewConfig):
@@ -7,3 +88,26 @@ class OrderOrderProposalButtonConfig(ButtonViewConfig):
7
88
  return {
8
89
  Button.SAVE_AND_CLOSE.value,
9
90
  }
91
+
92
+ def get_custom_list_instance_buttons(self) -> set:
93
+ return {
94
+ bt.ActionButton(
95
+ method=RequestType.PUT,
96
+ identifiers=("wbportfolio:order",),
97
+ icon=WBIcon.DEAL_MONEY.icon,
98
+ key="execution_instruction",
99
+ label="Change Execution",
100
+ action_label="Execution changed",
101
+ description_fields="<p>Change Execution</p>",
102
+ serializer=ExecutionInstructionSerializer,
103
+ instance_display=create_simple_display(
104
+ [
105
+ ["execution_instruction", "execution_instruction"],
106
+ ["percentage", "percentage"],
107
+ ["price", "good_for_date"],
108
+ ["period", "time"],
109
+ ["apply_execution_instruction_to_all_orders", "."],
110
+ ]
111
+ ),
112
+ )
113
+ }