wbportfolio 1.54.21__py2.py3-none-any.whl → 1.54.23__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 (34) hide show
  1. wbportfolio/constants.py +1 -0
  2. wbportfolio/factories/orders/orders.py +10 -2
  3. wbportfolio/filters/assets.py +10 -2
  4. wbportfolio/import_export/handlers/orders.py +1 -1
  5. wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
  6. wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
  7. wbportfolio/models/asset.py +6 -20
  8. wbportfolio/models/builder.py +11 -11
  9. wbportfolio/models/orders/order_proposals.py +53 -20
  10. wbportfolio/models/orders/orders.py +4 -1
  11. wbportfolio/models/portfolio.py +16 -8
  12. wbportfolio/models/products.py +9 -0
  13. wbportfolio/pms/trading/handler.py +0 -1
  14. wbportfolio/serializers/orders/order_proposals.py +2 -18
  15. wbportfolio/tests/conftest.py +6 -2
  16. wbportfolio/tests/models/orders/test_order_proposals.py +61 -15
  17. wbportfolio/tests/models/test_products.py +11 -0
  18. wbportfolio/tests/signals.py +0 -10
  19. wbportfolio/tests/tests.py +2 -0
  20. wbportfolio/viewsets/__init__.py +7 -4
  21. wbportfolio/viewsets/assets.py +1 -215
  22. wbportfolio/viewsets/charts/__init__.py +6 -1
  23. wbportfolio/viewsets/charts/assets.py +337 -154
  24. wbportfolio/viewsets/configs/buttons/products.py +32 -2
  25. wbportfolio/viewsets/configs/display/assets.py +6 -19
  26. wbportfolio/viewsets/configs/display/products.py +1 -1
  27. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +12 -2
  28. wbportfolio/viewsets/orders/order_proposals.py +2 -1
  29. wbportfolio/viewsets/positions.py +3 -2
  30. {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/METADATA +3 -1
  31. {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/RECORD +33 -32
  32. wbportfolio/viewsets/signals.py +0 -43
  33. {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/WHEEL +0 -0
  34. {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/licenses/LICENSE +0 -0
@@ -1,24 +1,41 @@
1
1
  import datetime as dt
2
+ from collections import defaultdict
3
+ from contextlib import suppress
4
+ from decimal import Decimal
2
5
 
3
- import numpy as np
4
6
  import pandas as pd
5
7
  import plotly.express as px
6
8
  import plotly.graph_objects as go
7
- from django.db.models import QuerySet
8
- from django.shortcuts import get_object_or_404
9
+ from django.db.models import F, Prefetch
9
10
  from django.utils.functional import cached_property
11
+ from plotly.subplots import make_subplots
12
+ from rest_framework.request import Request
10
13
  from wbcore import viewsets
14
+ from wbcore.contrib.currency.models import Currency
15
+ from wbcore.contrib.geography.models import Geography
11
16
  from wbcore.contrib.io.viewsets import ExportPandasAPIViewSet
17
+ from wbcore.contrib.pandas import fields as pf
12
18
  from wbcore.filters import DjangoFilterBackend
13
- from wbcore.pandas import fields as pf
19
+ from wbcore.utils.date import get_date_interval_from_request
20
+ from wbcore.utils.figures import (
21
+ get_default_timeserie_figure,
22
+ get_hovertemplate_timeserie,
23
+ )
24
+ from wbcore.utils.strings import format_number
14
25
  from wbfdm.models import (
26
+ Classification,
15
27
  ClassificationGroup,
16
28
  Instrument,
17
29
  InstrumentClassificationThroughModel,
18
30
  InstrumentType,
19
31
  )
20
32
 
21
- from wbportfolio.filters.assets import DistributionFilter
33
+ from wbportfolio.filters.assets import (
34
+ AssetPositionUnderlyingInstrumentChartFilter,
35
+ CompositionContributionChartFilter,
36
+ ContributionChartFilter,
37
+ DistributionFilter,
38
+ )
22
39
  from wbportfolio.models import (
23
40
  AssetPosition,
24
41
  AssetPositionGroupBy,
@@ -32,20 +49,37 @@ from ..configs.buttons.assets import (
32
49
  )
33
50
  from ..configs.display.assets import DistributionTableDisplayConfig
34
51
  from ..configs.endpoints.assets import (
52
+ AssetPositionUnderlyingInstrumentChartEndpointConfig,
53
+ ContributorPortfolioChartEndpointConfig,
35
54
  DistributionChartEndpointConfig,
36
55
  DistributionTableEndpointConfig,
37
56
  )
38
57
  from ..configs.titles.assets import (
58
+ AssetPositionUnderlyingInstrumentChartTitleConfig,
59
+ ContributorPortfolioChartTitleConfig,
39
60
  DistributionChartTitleConfig,
40
61
  DistributionTableTitleConfig,
41
62
  )
63
+ from ..mixins import UserPortfolioRequestPermissionMixin
42
64
 
43
65
 
44
- class AbstractDistributionMixin:
45
- AUTORESIZE = False
66
+ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
46
67
  queryset = AssetPosition.objects.all()
47
68
  filterset_class = DistributionFilter
48
69
  filter_backends = (DjangoFilterBackend,)
70
+ request: Request
71
+
72
+ @cached_property
73
+ def group_by(self) -> AssetPositionGroupBy:
74
+ return AssetPositionGroupBy(self.request.GET.get("group_by", "classification"))
75
+
76
+ @cached_property
77
+ def val_date(self) -> dt.date:
78
+ if validity_date_repr := self.request.GET.get("date"):
79
+ val_date = dt.datetime.strptime(validity_date_repr, "%Y-%m-%d")
80
+ else:
81
+ val_date = dt.date.today()
82
+ return val_date
49
83
 
50
84
  @cached_property
51
85
  def classification_group(self):
@@ -55,71 +89,95 @@ class AbstractDistributionMixin:
55
89
  return ClassificationGroup.objects.get(is_primary=True)
56
90
 
57
91
  @cached_property
58
- def classification_field_names(self):
59
- return [f"classification__{field_name}__name" for field_name in self.classification_group.get_fields_names()]
60
-
61
- @cached_property
62
- def classification_levels_representation(self):
63
- return self.classification_group.get_levels_representation()
92
+ def classification_height(self) -> int:
93
+ return int(self.request.GET.get("group_by_classification_height", "0"))
64
94
 
65
95
  @cached_property
66
- def classification_columns_map(self):
67
- return dict(
68
- zip(["classification__name", *self.classification_field_names], self.classification_levels_representation)
69
- )
70
-
71
- def _generate_classification_df(self, queryset):
72
- df = pd.DataFrame(
73
- queryset.filter(underlying_instrument__instrument_type=InstrumentType.EQUITY).values(
74
- "weighting", "underlying_instrument"
75
- ),
76
- columns=["weighting", "underlying_instrument"],
77
- )
78
- df.underlying_instrument = df.underlying_instrument.map(
79
- dict(
80
- Instrument.objects.filter(id__in=df.underlying_instrument)
81
- .annotate_base_data()
82
- .values_list("id", "root")
83
- )
84
- )
85
- df = df.groupby("underlying_instrument").sum()
86
- classifications = InstrumentClassificationThroughModel.objects.filter(
87
- classification__group=self.classification_group, instrument__in=df.index
88
- )
89
- df_classification = pd.DataFrame(
90
- classifications.values(
91
- "instrument",
92
- "classification__name",
93
- *self.classification_field_names,
94
- )
95
- )
96
- if df_classification.empty:
97
- return pd.DataFrame()
98
- return pd.concat([df, df_classification.groupby("instrument").first()], axis=1).replace(
99
- [np.inf, -np.inf, np.nan], "N/A"
100
- )
96
+ def columns_map(self) -> dict:
97
+ if self.group_by == AssetPositionGroupBy.INDUSTRY:
98
+ columns = {}
99
+ level_representations = self.classification_group.get_levels_representation()
100
+ for key, label in zip(
101
+ reversed(self.classification_group.get_fields_names(sep="_")), reversed(level_representations[1:])
102
+ ):
103
+ columns[key] = label
104
+ columns["label"] = "Classification"
105
+ columns["equity"] = "Instrument"
106
+ else:
107
+ columns = {"label": "Label"}
108
+ return columns
101
109
 
102
110
  def get_queryset(self):
103
- portfolio = get_object_or_404(Portfolio, id=self.kwargs["portfolio_id"])
104
- if (
105
- PortfolioRole.is_analyst(self.request.user.profile, portfolio=portfolio)
106
- or self.request.user.profile.is_internal
107
- ):
108
- return super().get_queryset().filter(portfolio=portfolio)
111
+ profile = self.request.user.profile
112
+ if PortfolioRole.is_analyst(profile, portfolio=self.portfolio) or profile.is_internal:
113
+ return AssetPosition.objects.filter(portfolio=self.portfolio)
109
114
  return AssetPosition.objects.none()
110
115
 
111
- @staticmethod
112
- def dataframe_group_by_instrument(df: pd.DataFrame) -> pd.DataFrame:
113
- if df.empty:
114
- return pd.DataFrame()
115
- return df.groupby("aggregated_title").sum().sort_values(by="weighting", ascending=False)
116
+ def get_dataframe(self, request, queryset, **kwargs) -> pd.DataFrame:
117
+ instruments = defaultdict(Decimal)
118
+ for asset in self.portfolio.get_positions(self.val_date):
119
+ if self.group_by != AssetPositionGroupBy.INDUSTRY:
120
+ group_field = getattr(asset.underlying_instrument, self.group_by.value)
121
+ else:
122
+ group_field = asset.underlying_instrument.get_root()
123
+ group_field = getattr(group_field, "id", group_field)
124
+ instruments[group_field] += asset.weighting
125
+ df = pd.DataFrame.from_dict(instruments, orient="index", columns=["weighting"])
126
+ if self.group_by == AssetPositionGroupBy.INDUSTRY:
127
+ classifications = InstrumentClassificationThroughModel.objects.filter(
128
+ instrument__in=instruments.keys(), classification__group=self.classification_group
129
+ )
130
+ field_names = {
131
+ field_name.replace("__", "_"): F(f"classification__{field_name}__name")
132
+ for field_name in self.classification_group.get_fields_names()
133
+ }
134
+ classifications = (
135
+ classifications.annotate(**field_names)
136
+ .select_related(
137
+ *[f"classification__{field_name}" for field_name in self.classification_group.get_fields_names()]
138
+ )
139
+ .prefetch_related(
140
+ "tags",
141
+ Prefetch("instrument", queryset=Instrument.objects.filter(classifications_through__isnull=False)),
142
+ )
143
+ )
144
+ df_classification = (
145
+ pd.DataFrame(
146
+ classifications.values_list("instrument", "classification", *field_names.keys()),
147
+ columns=["id", "classification", *field_names.keys()],
148
+ )
149
+ .groupby("id")
150
+ .first()
151
+ )
152
+ df = pd.concat([df, df_classification], axis=1)
153
+ if df.weighting.sum(): # normalize
154
+ df.weighting /= df.weighting.sum()
155
+ return df.reset_index(names="id")
116
156
 
117
- def dataframe_groupby_with_class_method(self, qs: QuerySet, class_method: classmethod):
118
- df = pd.DataFrame()
119
- if qs.exists():
120
- df = self.dataframe_group_by_instrument(
121
- pd.DataFrame(class_method(qs).values("weighting", "aggregated_title"))
157
+ def manipulate_dataframe(self, df):
158
+ if self.group_by == AssetPositionGroupBy.INDUSTRY:
159
+ if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=self.portfolio):
160
+ df["equity"] = ""
161
+ else:
162
+ df["equity"] = df["id"].map(
163
+ dict(Instrument.objects.filter(id__in=df["id"]).values_list("id", "computed_str"))
164
+ )
165
+ df["label"] = df["classification"].map(
166
+ dict(Classification.objects.filter(id__in=df["classification"].dropna()).values_list("id", "name"))
167
+ )
168
+ elif self.group_by == AssetPositionGroupBy.CASH:
169
+ df.loc[df["id"] == True, "label"] = "Cash"
170
+ df.loc[df["id"] == False, "label"] = "Non-Cash"
171
+ elif self.group_by == AssetPositionGroupBy.COUNTRY:
172
+ df["label"] = df["id"].map(dict(Geography.objects.filter(id__in=df["id"]).values_list("id", "name")))
173
+ elif self.group_by == AssetPositionGroupBy.CURRENCY:
174
+ currencies = dict(map(lambda o: (o.id, str(o)), Currency.objects.filter(id__in=df["id"])))
175
+ df["label"] = df["id"].map(currencies)
176
+ elif self.group_by == AssetPositionGroupBy.INSTRUMENT_TYPE:
177
+ df["label"] = df["id"].map(
178
+ dict(InstrumentType.objects.filter(id__in=df["id"]).values_list("id", "short_name"))
122
179
  )
180
+ df.sort_values(by="weighting", ascending=False, inplace=True)
123
181
  return df
124
182
 
125
183
 
@@ -142,39 +200,17 @@ class DistributionChartViewSet(AbstractDistributionMixin, viewsets.ChartViewSet)
142
200
 
143
201
  def get_plotly(self, queryset):
144
202
  fig = go.Figure()
145
- group_by = self.request.GET.get("group_by", "COUNTRY")
146
- class_method = AssetPositionGroupBy.get_class_method_group_by(name=group_by)
147
- queryset_without_cash = queryset.exclude(underlying_instrument__is_cash=True)
148
- if group_by not in ["INDUSTRY", "CURRENCY"]:
149
- df = self.dataframe_groupby_with_class_method(qs=queryset_without_cash, class_method=class_method)
150
- fig = self.pie_chart(df=df)
151
- elif group_by == "CURRENCY":
152
- df = self.dataframe_groupby_with_class_method(qs=queryset, class_method=class_method)
153
- fig = self.pie_chart(df=df)
154
- else:
155
- df = self._generate_classification_df(queryset_without_cash)
156
- if not df.empty:
157
- df["weighting"] = df.weighting / df.weighting.sum()
158
- df.weighting = df.weighting.astype("float")
159
- df = df.reset_index().rename(
160
- columns={**self.classification_columns_map, "weighting": "weight", "index": "Equity"}
161
- )
162
-
163
- levels = [*self.classification_levels_representation[::-1], "Equity"]
164
- df["Equity"] = df["Equity"].map(
165
- dict(Instrument.objects.filter(id__in=df["Equity"]).values_list("id", "name_repr"))
166
- )
167
- portfolio = Portfolio.objects.get(id=self.kwargs["portfolio_id"])
168
- if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=portfolio):
169
- del df["Equity"]
170
- levels.remove("Equity")
171
- fig = px.sunburst(
172
- df,
173
- path=levels,
174
- values="weight",
175
- hover_data={"weight": ":.2%"},
176
- )
177
- fig.update_traces(hovertemplate="<b>%{label}</b><br>Weight = %{customdata:.3p}")
203
+ df = self.manipulate_dataframe(self.get_dataframe(self.request, queryset))
204
+ df = df.dropna(how="any")
205
+ if not df.empty:
206
+ levels = list(self.columns_map.keys())
207
+ fig = px.sunburst(
208
+ df,
209
+ path=levels,
210
+ values="weighting",
211
+ hover_data={"weighting": ":.2%"},
212
+ )
213
+ fig.update_traces(hovertemplate="<b>%{label}</b><br>Weight = %{customdata:.3p}")
178
214
  return fig
179
215
 
180
216
 
@@ -185,65 +221,212 @@ class DistributionTableViewSet(AbstractDistributionMixin, ExportPandasAPIViewSet
185
221
  button_config_class = DistributionTableButtonConfig
186
222
 
187
223
  def get_pandas_fields(self, request):
188
- if self.request.GET.get("group_by") != "INDUSTRY":
189
- fields = [
190
- pf.PKField(key="aggregate_field", label=""),
191
- ]
192
- else:
193
- fields = [
194
- pf.PKField(key="id", label="IDS"),
195
- pf.CharField(key="equity", label="Equity"),
196
- ]
197
- for level_rep in self.classification_levels_representation:
198
- fields.append(pf.CharField(key=level_rep, label=level_rep))
199
- fields.extend([pf.FloatField(key="weighting", label="Weight", precision=2, percent=True)])
200
- return pf.PandasFields(fields=tuple(fields))
201
-
202
- def get_date_filter(self):
203
- if date_str := self.request.GET.get("date", None):
204
- val_date = dt.datetime.strptime(date_str, "%Y-%m-%d").date()
205
- elif super().get_queryset().exists():
206
- val_date = AssetPosition.objects.latest("date").date
224
+ fields = [
225
+ pf.PKField(key="id", label="id"),
226
+ pf.FloatField(key="weighting", label="Weight", precision=2, percent=True),
227
+ ]
228
+ for key, label in self.columns_map.items():
229
+ fields.append(pf.CharField(key=key, label=label))
230
+ return pf.PandasFields(fields=fields)
231
+
232
+ def get_aggregates(self, request, df):
233
+ return {"weighting": {"Σ": format_number(df["weighting"].sum())}}
234
+
235
+
236
+ # ##### CHART VIEWS #####
237
+
238
+
239
+ class ContributorPortfolioChartView(UserPortfolioRequestPermissionMixin, viewsets.ChartViewSet):
240
+ filterset_class = ContributionChartFilter
241
+ filter_backends = (DjangoFilterBackend,)
242
+ IDENTIFIER = "wbportfolio:portfolio-contributor"
243
+ queryset = AssetPosition.objects.all()
244
+
245
+ title_config_class = ContributorPortfolioChartTitleConfig
246
+ endpoint_config_class = ContributorPortfolioChartEndpointConfig
247
+
248
+ ROW_HEIGHT: int = 20
249
+
250
+ @property
251
+ def min_height(self):
252
+ if hasattr(self, "nb_rows"):
253
+ return self.nb_rows * self.ROW_HEIGHT
254
+ return "300px"
255
+
256
+ @cached_property
257
+ def hedged_currency(self) -> Currency | None:
258
+ if "hedged_currency" in self.request.GET:
259
+ with suppress(Currency.DoesNotExist):
260
+ return Currency.objects.get(pk=self.request.GET["hedged_currency"])
261
+
262
+ @cached_property
263
+ def show_lookthrough(self) -> bool:
264
+ return self.portfolio.is_composition and self.request.GET.get("show_lookthrough", "false").lower() == "true"
265
+
266
+ def get_filterset_class(self, request):
267
+ if self.portfolio.is_composition:
268
+ return CompositionContributionChartFilter
269
+ return ContributionChartFilter
270
+
271
+ def get_plotly(self, queryset):
272
+ fig = go.Figure()
273
+ data = []
274
+ if self.show_lookthrough:
275
+ d1, d2 = get_date_interval_from_request(self.request)
276
+ for _d in pd.date_range(d1, d2):
277
+ for pos in self.portfolio.get_lookthrough_positions(_d.date()):
278
+ data.append(
279
+ [
280
+ pos.date,
281
+ pos.initial_price,
282
+ pos.initial_currency_fx_rate,
283
+ pos.underlying_instrument_id,
284
+ pos.weighting,
285
+ ]
286
+ )
207
287
  else:
208
- val_date = dt.date.today()
209
- return val_date
288
+ data = queryset.annotate_hedged_currency_fx_rate(self.hedged_currency).values_list(
289
+ "date", "price", "hedged_currency_fx_rate", "underlying_instrument", "weighting"
290
+ )
291
+ df = Portfolio.get_contribution_df(data).rename(columns={"group_key": "underlying_instrument"})
292
+ if not df.empty:
293
+ df = df[["contribution_total", "contribution_forex", "underlying_instrument"]].sort_values(
294
+ by="contribution_total", ascending=True
295
+ )
296
+
297
+ df["instrument_id"] = df.underlying_instrument.map(
298
+ dict(Instrument.objects.filter(id__in=df["underlying_instrument"]).values_list("id", "name_repr"))
299
+ )
300
+ df_forex = df[["instrument_id", "contribution_forex"]]
301
+ df_forex = df_forex[df_forex.contribution_forex != 0]
302
+
303
+ contribution_equity = df.contribution_total - df.contribution_forex
304
+
305
+ text_forex = df_forex.contribution_forex.apply(lambda x: f"{x:,.2%}")
306
+ text_equity = contribution_equity.apply(lambda x: f"{x:,.2%}")
307
+ setattr(self, "nb_rows", df.shape[0])
308
+ fig.add_trace(
309
+ go.Bar(
310
+ y=df.instrument_id,
311
+ x=contribution_equity,
312
+ name="Contribution Equity",
313
+ orientation="h",
314
+ marker=dict(
315
+ color="rgba(247,110,91,0.6)",
316
+ line=dict(color="rgb(247,110,91,1.0)", width=2),
317
+ ),
318
+ text=text_equity.values,
319
+ textposition="auto",
320
+ )
321
+ )
322
+ fig.add_trace(
323
+ go.Bar(
324
+ y=df_forex.instrument_id,
325
+ x=df_forex.contribution_forex,
326
+ name="Contribution Forex",
327
+ orientation="h",
328
+ marker=dict(
329
+ color="rgba(58, 71, 80, 0.6)",
330
+ line=dict(color="rgba(58, 71, 80, 1.0)", width=2),
331
+ ),
332
+ text=text_forex.values,
333
+ textposition="outside",
334
+ )
335
+ )
336
+ fig.update_layout(
337
+ barmode="relative",
338
+ xaxis=dict(showgrid=False, showline=False, zeroline=False, tickformat=".2%"),
339
+ yaxis=dict(showgrid=False, showline=False, zeroline=False, tickmode="linear"),
340
+ margin=dict(b=0, r=20, l=20, t=0, pad=20),
341
+ paper_bgcolor="rgba(0,0,0,0)",
342
+ plot_bgcolor="rgba(0,0,0,0)",
343
+ font=dict(family="roboto", size=12, color="black"),
344
+ bargap=0.3,
345
+ )
346
+ # fig = get_horizontal_barplot(df, x_label="contribution_total", y_label="name")
347
+ return fig
348
+
349
+ def parse_figure_dict(self, figure_dict: dict[str, any]) -> dict[str, any]:
350
+ figure_dict = super().parse_figure_dict(figure_dict)
351
+ figure_dict["style"]["minHeight"] = self.min_height
352
+ return figure_dict
353
+
354
+ def get_queryset(self):
355
+ if self.has_portfolio_access:
356
+ return super().get_queryset().filter(portfolio=self.portfolio)
357
+ return AssetPosition.objects.none()
358
+
359
+
360
+ class AssetPositionUnderlyingInstrumentChartViewSet(UserPortfolioRequestPermissionMixin, viewsets.ChartViewSet):
361
+ IDENTIFIER = "wbportfolio:assetpositionchart"
362
+
363
+ queryset = AssetPosition.objects.all()
364
+
365
+ title_config_class = AssetPositionUnderlyingInstrumentChartTitleConfig
366
+ endpoint_config_class = AssetPositionUnderlyingInstrumentChartEndpointConfig
367
+ filterset_class = AssetPositionUnderlyingInstrumentChartFilter
210
368
 
211
369
  def get_queryset(self):
212
- val_date = self.get_date_filter()
213
- queryset = super().get_queryset().filter(date=val_date)
214
- return queryset
215
-
216
- def get_dataframe(self, request, queryset, **kwargs):
217
- group_by = self.request.GET.get("group_by", "COUNTRY")
218
- class_method = AssetPositionGroupBy.get_class_method_group_by(name=group_by)
219
- queryset_without_cash = queryset.exclude(underlying_instrument__is_cash=True)
220
- if group_by not in ["INDUSTRY", "CURRENCY"]:
221
- df = self.dataframe_groupby_with_class_method(qs=queryset_without_cash, class_method=class_method)
222
- elif group_by == "CURRENCY":
223
- df = self.dataframe_groupby_with_class_method(qs=queryset, class_method=class_method)
224
- else: # group_by == "INDUSTRY"
225
- df = self._generate_classification_df(queryset_without_cash)
226
-
227
- if not df.empty:
228
- df.weighting /= df.weighting.sum()
229
- df = df.reset_index().rename(columns={**self.classification_columns_map, "index": "equity"})
230
- df["equity"] = df["equity"].map(
231
- dict(Instrument.objects.filter(id__in=df["equity"]).values_list("id", "name_repr"))
370
+ return AssetPosition.objects.filter(underlying_quote__in=self.instrument.get_descendants(include_self=True))
371
+
372
+ def get_plotly(self, queryset):
373
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
374
+ fig = get_default_timeserie_figure(fig)
375
+ if queryset.exists():
376
+ df_weight = pd.DataFrame(queryset.values("date", "weighting", "portfolio__name"))
377
+ df_weight = df_weight.where(pd.notnull(df_weight), 0)
378
+ df_weight = df_weight.groupby(["date", "portfolio__name"]).sum().reset_index()
379
+ min_date = df_weight["date"].min()
380
+ max_date = df_weight["date"].max()
381
+
382
+ df_price = (
383
+ pd.DataFrame(
384
+ self.instrument.prices.filter_only_valid_prices()
385
+ .annotate_base_data()
386
+ .filter(date__gte=min_date, date__lte=max_date)
387
+ .values_list("date", "net_value_usd"),
388
+ columns=["date", "price_fx_usd"],
232
389
  )
233
- for level in self.classification_levels_representation:
234
- tmp = df.groupby(by=level).weighting.sum().astype(float).mul(100).round(1)
235
- df = df.join(tmp, on=level, rsuffix=f"_{level}")
236
- df[level] += " (" + df[f"weighting_{level}"].astype(str) + "%)"
237
- portfolio = Portfolio.objects.get(id=self.request.GET.get("portfolio"))
238
- if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=portfolio):
239
- df[["weighting", "equity"]] = None
240
- df.drop_duplicates(inplace=True)
241
- return df
390
+ .set_index("date")
391
+ .sort_index()
392
+ )
242
393
 
243
- def manipulate_dataframe(self, df):
244
- if not df.empty:
245
- df.sort_values(by="weighting", ascending=False, inplace=True)
246
- if df.weighting.sum() != 1: # normalize
247
- df.weighting /= df.weighting.sum()
248
- df = df.reset_index(names="aggregate_field" if self.request.GET.get("group_by") != "INDUSTRY" else "id")
249
- return df
394
+ fig.add_trace(
395
+ go.Scatter(
396
+ x=df_price.index, y=df_price.price_fx_usd, mode="lines", marker_color="green", name="Price"
397
+ ),
398
+ secondary_y=False,
399
+ )
400
+
401
+ df_weight = pd.DataFrame(queryset.values("date", "weighting", "portfolio__name"))
402
+ df_weight = df_weight.where(pd.notnull(df_weight), 0)
403
+ df_weight = df_weight.groupby(["date", "portfolio__name"]).sum().reset_index()
404
+ for portfolio_name, df_tmp in df_weight.groupby("portfolio__name"):
405
+ fig.add_trace(
406
+ go.Scatter(
407
+ x=df_tmp.date,
408
+ y=df_tmp.weighting,
409
+ hovertemplate=get_hovertemplate_timeserie(is_percent=True),
410
+ mode="lines",
411
+ name=f"Allocation: {portfolio_name}",
412
+ ),
413
+ secondary_y=True,
414
+ )
415
+
416
+ # Set x-axis title
417
+ fig.update_xaxes(title_text="Date")
418
+ # Set y-axes titles
419
+ fig.update_yaxes(
420
+ title_text="<b>Price</b>",
421
+ secondary_y=False,
422
+ titlefont=dict(color="green"),
423
+ tickfont=dict(color="green"),
424
+ )
425
+ fig.update_yaxes(
426
+ title_text="<b>Portfolio Allocation (%)</b>",
427
+ secondary_y=True,
428
+ titlefont=dict(color="blue"),
429
+ tickfont=dict(color="blue"),
430
+ )
431
+
432
+ return fig
@@ -1,15 +1,18 @@
1
+ from rest_framework.reverse import reverse
1
2
  from wbcore.contrib.icons import WBIcon
2
3
  from wbcore.metadata.configs import buttons as bt
3
4
  from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
4
5
  from wbfdm.viewsets.configs.buttons.instruments import InstrumentButtonViewConfig
5
6
 
7
+ from wbportfolio.models import Product
8
+
6
9
 
7
10
  class ProductButtonConfig(ButtonViewConfig):
8
11
  def get_custom_list_instance_buttons(self):
9
12
  return self.get_custom_instance_buttons()
10
13
 
11
14
  def get_custom_instance_buttons(self):
12
- return {
15
+ buttons = [
13
16
  bt.DropDownButton(
14
17
  label="Commission",
15
18
  icon=WBIcon.UNFOLD.icon,
@@ -35,7 +38,34 @@ class ProductButtonConfig(ButtonViewConfig):
35
38
  ),
36
39
  ],
37
40
  ),
38
- }
41
+ ]
42
+ if product_id := self.view.kwargs.get("pk", None):
43
+ product = Product.objects.get(id=product_id)
44
+ report_buttons = []
45
+ for report in product.reports.filter(is_active=True):
46
+ tmp_buttons = []
47
+ if primary_version := report.primary_version:
48
+ tmp_buttons.append(
49
+ bt.HyperlinkButton(
50
+ label="Public Report",
51
+ endpoint=reverse(
52
+ "public_report:report_version", args=[primary_version.lookup], request=self.request
53
+ ),
54
+ ),
55
+ )
56
+ if self.request.user.profile.is_internal or self.request.user.is_superuser:
57
+ tmp_buttons.append(
58
+ bt.WidgetButton(
59
+ label="Widget",
60
+ endpoint=reverse("wbreport:report-detail", args=[report.id], request=self.request),
61
+ ),
62
+ )
63
+ if tmp_buttons:
64
+ report_buttons.append(bt.DropDownButton(label=str(report), buttons=tmp_buttons))
65
+ if report_buttons:
66
+ buttons.append(bt.DropDownButton(label="Reports", buttons=report_buttons))
67
+
68
+ return set(buttons)
39
69
 
40
70
 
41
71
  class ProductCustomerButtonConfig(ButtonViewConfig):
@@ -234,22 +234,9 @@ class CompositionModelPortfolioPandasDisplayConfig(DisplayViewConfig):
234
234
 
235
235
  class DistributionTableDisplayConfig(DisplayViewConfig):
236
236
  def get_list_display(self) -> Optional[dp.ListDisplay]:
237
- if group_by := self.request.GET.get("group_by", ""):
238
- group_by = group_by.title().replace("_", " ")
239
-
240
- if group_by != "Industry":
241
- fields = [
242
- dp.Field(key="aggregate_field", label=group_by),
243
- dp.Field(key="weighting", label="Weight"),
244
- ]
245
- else:
246
- fields = []
247
- for level_rep in self.view.classification_levels_representation:
248
- fields.append(dp.Field(key=level_rep, label=level_rep))
249
- fields.extend(
250
- [
251
- dp.Field(key="equity", label="Equity"),
252
- dp.Field(key="weighting", label="Weight"),
253
- ]
254
- )
255
- return dp.ListDisplay(fields=tuple(fields))
237
+ fields = [
238
+ dp.Field(key="weighting", label="Weighting"),
239
+ ]
240
+ for k, v in reversed(self.view.columns_map.items()):
241
+ fields.append(dp.Field(key=k, label=v))
242
+ return dp.ListDisplay(fields=fields)
@@ -124,7 +124,7 @@ class ProductDisplayConfig(DisplayViewConfig):
124
124
  return create_simple_display(
125
125
  [
126
126
  [repeat_field(2, "name"), repeat_field(2, "name_repr")],
127
- ["inception_date", "isin", "ticker", "id_repr"],
127
+ ["inception_date", "delisted_date", "isin", "id_repr"],
128
128
  ["share_price", "issue_price", "initial_high_water_mark", "currency"],
129
129
  [repeat_field(2, "bank"), repeat_field(2, "parent")],
130
130
  [repeat_field(4, "tags")],