wbportfolio 1.54.22__py2.py3-none-any.whl → 1.55.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbportfolio might be problematic. Click here for more details.
- wbportfolio/admin/indexes.py +1 -1
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/rebalancing.py +1 -1
- wbportfolio/api_clients/__init__.py +0 -0
- wbportfolio/api_clients/ubs.py +150 -0
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +10 -2
- wbportfolio/factories/portfolios.py +1 -1
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/filters/assets.py +10 -2
- wbportfolio/filters/orders/__init__.py +1 -0
- wbportfolio/filters/orders/order_proposals.py +58 -0
- wbportfolio/filters/portfolios.py +20 -0
- wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
- wbportfolio/import_export/backends/ubs/fees.py +10 -20
- wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
- wbportfolio/import_export/backends/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +1 -1
- wbportfolio/import_export/handlers/orders.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +1 -1
- wbportfolio/import_export/parsers/societe_generale/strategy.py +2 -2
- wbportfolio/import_export/parsers/ubs/equity.py +1 -1
- wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
- wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
- wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
- wbportfolio/models/asset.py +6 -20
- wbportfolio/models/builder.py +74 -31
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/orders/order_proposals.py +549 -167
- wbportfolio/models/orders/orders.py +24 -11
- wbportfolio/models/orders/routing.py +54 -0
- wbportfolio/models/portfolio.py +77 -41
- wbportfolio/models/products.py +9 -0
- wbportfolio/models/rebalancing.py +6 -6
- wbportfolio/models/transactions/transactions.py +10 -6
- wbportfolio/order_routing/__init__.py +19 -0
- wbportfolio/order_routing/adapters/__init__.py +57 -0
- wbportfolio/order_routing/adapters/ubs.py +161 -0
- wbportfolio/pms/trading/handler.py +4 -1
- wbportfolio/pms/typing.py +62 -8
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/equally_weighted.py +3 -3
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -1
- wbportfolio/rebalancing/models/model_portfolio.py +9 -10
- wbportfolio/serializers/orders/order_proposals.py +25 -21
- wbportfolio/serializers/orders/orders.py +5 -2
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/tests/conftest.py +6 -2
- wbportfolio/tests/models/orders/test_order_proposals.py +90 -30
- wbportfolio/tests/models/test_imports.py +5 -3
- wbportfolio/tests/models/test_portfolios.py +57 -23
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +2 -2
- wbportfolio/tests/rebalancing/test_models.py +3 -5
- wbportfolio/tests/signals.py +0 -10
- wbportfolio/tests/tests.py +2 -0
- wbportfolio/viewsets/__init__.py +7 -4
- wbportfolio/viewsets/assets.py +1 -215
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +341 -155
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/menu/__init__.py +1 -0
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +66 -12
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +55 -9
- wbportfolio/viewsets/orders/configs/displays/orders.py +13 -0
- wbportfolio/viewsets/orders/order_proposals.py +47 -7
- wbportfolio/viewsets/orders/orders.py +31 -29
- wbportfolio/viewsets/portfolios.py +3 -3
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/METADATA +3 -1
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/RECORD +78 -68
- wbportfolio/viewsets/signals.py +0 -43
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/licenses/LICENSE +0 -0
wbportfolio/viewsets/assets.py
CHANGED
|
@@ -2,23 +2,15 @@ from contextlib import suppress
|
|
|
2
2
|
from datetime import date, datetime
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
|
-
import plotly.graph_objects as go
|
|
6
5
|
from django.db.models import Exists, F, OuterRef, Q, Sum
|
|
7
6
|
from django.utils.dateparse import parse_date
|
|
8
7
|
from django.utils.functional import cached_property
|
|
9
|
-
from plotly.subplots import make_subplots
|
|
10
8
|
from wbcore import viewsets
|
|
11
|
-
from wbcore.contrib.currency.models import
|
|
9
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
12
10
|
from wbcore.contrib.io.viewsets import ExportPandasAPIViewSet
|
|
13
|
-
from wbcore.filters import DjangoFilterBackend
|
|
14
11
|
from wbcore.pandas import fields as pf
|
|
15
12
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
16
13
|
from wbcore.serializers import decorator
|
|
17
|
-
from wbcore.utils.date import get_date_interval_from_request
|
|
18
|
-
from wbcore.utils.figures import (
|
|
19
|
-
get_default_timeserie_figure,
|
|
20
|
-
get_hovertemplate_timeserie,
|
|
21
|
-
)
|
|
22
14
|
from wbcore.utils.strings import format_number
|
|
23
15
|
from wbfdm.contrib.metric.viewsets.mixins import InstrumentMetricMixin
|
|
24
16
|
from wbfdm.models import Instrument
|
|
@@ -27,11 +19,8 @@ from wbportfolio.filters import (
|
|
|
27
19
|
AssetPositionFilter,
|
|
28
20
|
AssetPositionInstrumentFilter,
|
|
29
21
|
AssetPositionPortfolioFilter,
|
|
30
|
-
AssetPositionUnderlyingInstrumentChartFilter,
|
|
31
22
|
CashPositionPortfolioFilterSet,
|
|
32
|
-
CompositionContributionChartFilter,
|
|
33
23
|
CompositionModelPortfolioPandasFilter,
|
|
34
|
-
ContributionChartFilter,
|
|
35
24
|
)
|
|
36
25
|
from wbportfolio.import_export.resources.assets import AssetPositionResource
|
|
37
26
|
from wbportfolio.metric.backends.portfolio_base import (
|
|
@@ -69,16 +58,12 @@ from .configs import (
|
|
|
69
58
|
AssetPositionPortfolioEndpointConfig,
|
|
70
59
|
AssetPositionPortfolioTitleConfig,
|
|
71
60
|
AssetPositionTitleConfig,
|
|
72
|
-
AssetPositionUnderlyingInstrumentChartEndpointConfig,
|
|
73
|
-
AssetPositionUnderlyingInstrumentChartTitleConfig,
|
|
74
61
|
CashPositionPortfolioDisplayConfig,
|
|
75
62
|
CashPositionPortfolioEndpointConfig,
|
|
76
63
|
CashPositionPortfolioTitleConfig,
|
|
77
64
|
CompositionModelPortfolioPandasDisplayConfig,
|
|
78
65
|
CompositionModelPortfolioPandasEndpointConfig,
|
|
79
66
|
CompositionModelPortfolioPandasTitleConfig,
|
|
80
|
-
ContributorPortfolioChartEndpointConfig,
|
|
81
|
-
ContributorPortfolioChartTitleConfig,
|
|
82
67
|
)
|
|
83
68
|
from .mixins import UserPortfolioRequestPermissionMixin
|
|
84
69
|
|
|
@@ -344,205 +329,6 @@ class CashPositionPortfolioPandasAPIView(
|
|
|
344
329
|
return AssetPosition.objects.none()
|
|
345
330
|
|
|
346
331
|
|
|
347
|
-
# ##### CHART VIEWS #####
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
class ContributorPortfolioChartView(UserPortfolioRequestPermissionMixin, viewsets.ChartViewSet):
|
|
351
|
-
filterset_class = ContributionChartFilter
|
|
352
|
-
filter_backends = (DjangoFilterBackend,)
|
|
353
|
-
IDENTIFIER = "wbportfolio:portfolio-contributor"
|
|
354
|
-
queryset = AssetPosition.objects.all()
|
|
355
|
-
|
|
356
|
-
title_config_class = ContributorPortfolioChartTitleConfig
|
|
357
|
-
endpoint_config_class = ContributorPortfolioChartEndpointConfig
|
|
358
|
-
|
|
359
|
-
ROW_HEIGHT: int = 20
|
|
360
|
-
|
|
361
|
-
@property
|
|
362
|
-
def min_height(self):
|
|
363
|
-
if hasattr(self, "nb_rows"):
|
|
364
|
-
return self.nb_rows * self.ROW_HEIGHT
|
|
365
|
-
return "300px"
|
|
366
|
-
|
|
367
|
-
@cached_property
|
|
368
|
-
def hedged_currency(self) -> Currency | None:
|
|
369
|
-
if "hedged_currency" in self.request.GET:
|
|
370
|
-
with suppress(Currency.DoesNotExist):
|
|
371
|
-
return Currency.objects.get(pk=self.request.GET["hedged_currency"])
|
|
372
|
-
|
|
373
|
-
@cached_property
|
|
374
|
-
def show_lookthrough(self) -> bool:
|
|
375
|
-
return self.portfolio.is_composition and self.request.GET.get("show_lookthrough", "false").lower() == "true"
|
|
376
|
-
|
|
377
|
-
def get_filterset_class(self, request):
|
|
378
|
-
if self.portfolio.is_composition:
|
|
379
|
-
return CompositionContributionChartFilter
|
|
380
|
-
return ContributionChartFilter
|
|
381
|
-
|
|
382
|
-
def get_plotly(self, queryset):
|
|
383
|
-
fig = go.Figure()
|
|
384
|
-
data = []
|
|
385
|
-
if self.show_lookthrough:
|
|
386
|
-
d1, d2 = get_date_interval_from_request(self.request)
|
|
387
|
-
for _d in pd.date_range(d1, d2):
|
|
388
|
-
for pos in self.portfolio.get_lookthrough_positions(_d.date()):
|
|
389
|
-
data.append(
|
|
390
|
-
[
|
|
391
|
-
pos.date,
|
|
392
|
-
pos.initial_price,
|
|
393
|
-
pos.initial_currency_fx_rate,
|
|
394
|
-
pos.underlying_instrument_id,
|
|
395
|
-
pos.weighting,
|
|
396
|
-
]
|
|
397
|
-
)
|
|
398
|
-
else:
|
|
399
|
-
data = queryset.annotate_hedged_currency_fx_rate(self.hedged_currency).values_list(
|
|
400
|
-
"date", "price", "hedged_currency_fx_rate", "underlying_instrument", "weighting"
|
|
401
|
-
)
|
|
402
|
-
df = Portfolio.get_contribution_df(data).rename(columns={"group_key": "underlying_instrument"})
|
|
403
|
-
if not df.empty:
|
|
404
|
-
df = df[["contribution_total", "contribution_forex", "underlying_instrument"]].sort_values(
|
|
405
|
-
by="contribution_total", ascending=True
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
df["instrument_id"] = df.underlying_instrument.map(
|
|
409
|
-
dict(Instrument.objects.filter(id__in=df["underlying_instrument"]).values_list("id", "name_repr"))
|
|
410
|
-
)
|
|
411
|
-
df_forex = df[["instrument_id", "contribution_forex"]]
|
|
412
|
-
df_forex = df_forex[df_forex.contribution_forex != 0]
|
|
413
|
-
|
|
414
|
-
contribution_equity = df.contribution_total - df.contribution_forex
|
|
415
|
-
|
|
416
|
-
text_forex = df_forex.contribution_forex.apply(lambda x: f"{x:,.2%}")
|
|
417
|
-
text_equity = contribution_equity.apply(lambda x: f"{x:,.2%}")
|
|
418
|
-
setattr(self, "nb_rows", df.shape[0])
|
|
419
|
-
fig.add_trace(
|
|
420
|
-
go.Bar(
|
|
421
|
-
y=df.instrument_id,
|
|
422
|
-
x=contribution_equity,
|
|
423
|
-
name="Contribution Equity",
|
|
424
|
-
orientation="h",
|
|
425
|
-
marker=dict(
|
|
426
|
-
color="rgba(247,110,91,0.6)",
|
|
427
|
-
line=dict(color="rgb(247,110,91,1.0)", width=2),
|
|
428
|
-
),
|
|
429
|
-
text=text_equity.values,
|
|
430
|
-
textposition="auto",
|
|
431
|
-
)
|
|
432
|
-
)
|
|
433
|
-
fig.add_trace(
|
|
434
|
-
go.Bar(
|
|
435
|
-
y=df_forex.instrument_id,
|
|
436
|
-
x=df_forex.contribution_forex,
|
|
437
|
-
name="Contribution Forex",
|
|
438
|
-
orientation="h",
|
|
439
|
-
marker=dict(
|
|
440
|
-
color="rgba(58, 71, 80, 0.6)",
|
|
441
|
-
line=dict(color="rgba(58, 71, 80, 1.0)", width=2),
|
|
442
|
-
),
|
|
443
|
-
text=text_forex.values,
|
|
444
|
-
textposition="outside",
|
|
445
|
-
)
|
|
446
|
-
)
|
|
447
|
-
fig.update_layout(
|
|
448
|
-
barmode="relative",
|
|
449
|
-
xaxis=dict(showgrid=False, showline=False, zeroline=False, tickformat=".2%"),
|
|
450
|
-
yaxis=dict(showgrid=False, showline=False, zeroline=False, tickmode="linear"),
|
|
451
|
-
margin=dict(b=0, r=20, l=20, t=0, pad=20),
|
|
452
|
-
paper_bgcolor="rgba(0,0,0,0)",
|
|
453
|
-
plot_bgcolor="rgba(0,0,0,0)",
|
|
454
|
-
font=dict(family="roboto", size=12, color="black"),
|
|
455
|
-
bargap=0.3,
|
|
456
|
-
)
|
|
457
|
-
# fig = get_horizontal_barplot(df, x_label="contribution_total", y_label="name")
|
|
458
|
-
return fig
|
|
459
|
-
|
|
460
|
-
def parse_figure_dict(self, figure_dict: dict[str, any]) -> dict[str, any]:
|
|
461
|
-
figure_dict = super().parse_figure_dict(figure_dict)
|
|
462
|
-
figure_dict["style"]["minHeight"] = self.min_height
|
|
463
|
-
return figure_dict
|
|
464
|
-
|
|
465
|
-
def get_queryset(self):
|
|
466
|
-
if self.has_portfolio_access:
|
|
467
|
-
return super().get_queryset().filter(portfolio=self.portfolio)
|
|
468
|
-
return AssetPosition.objects.none()
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
class AssetPositionUnderlyingInstrumentChartViewSet(UserPortfolioRequestPermissionMixin, viewsets.ChartViewSet):
|
|
472
|
-
IDENTIFIER = "wbportfolio:assetpositionchart"
|
|
473
|
-
|
|
474
|
-
queryset = AssetPosition.objects.all()
|
|
475
|
-
|
|
476
|
-
title_config_class = AssetPositionUnderlyingInstrumentChartTitleConfig
|
|
477
|
-
endpoint_config_class = AssetPositionUnderlyingInstrumentChartEndpointConfig
|
|
478
|
-
filterset_class = AssetPositionUnderlyingInstrumentChartFilter
|
|
479
|
-
|
|
480
|
-
def get_queryset(self):
|
|
481
|
-
return AssetPosition.objects.filter(underlying_quote__in=self.instrument.get_descendants(include_self=True))
|
|
482
|
-
|
|
483
|
-
def get_plotly(self, queryset):
|
|
484
|
-
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
485
|
-
fig = get_default_timeserie_figure(fig)
|
|
486
|
-
if queryset.exists():
|
|
487
|
-
df_weight = pd.DataFrame(queryset.values("date", "weighting", "portfolio__name"))
|
|
488
|
-
df_weight = df_weight.where(pd.notnull(df_weight), 0)
|
|
489
|
-
df_weight = df_weight.groupby(["date", "portfolio__name"]).sum().reset_index()
|
|
490
|
-
min_date = df_weight["date"].min()
|
|
491
|
-
max_date = df_weight["date"].max()
|
|
492
|
-
|
|
493
|
-
df_price = (
|
|
494
|
-
pd.DataFrame(
|
|
495
|
-
self.instrument.prices.filter_only_valid_prices()
|
|
496
|
-
.annotate_base_data()
|
|
497
|
-
.filter(date__gte=min_date, date__lte=max_date)
|
|
498
|
-
.values_list("date", "net_value_usd"),
|
|
499
|
-
columns=["date", "price_fx_usd"],
|
|
500
|
-
)
|
|
501
|
-
.set_index("date")
|
|
502
|
-
.sort_index()
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
fig.add_trace(
|
|
506
|
-
go.Scatter(
|
|
507
|
-
x=df_price.index, y=df_price.price_fx_usd, mode="lines", marker_color="green", name="Price"
|
|
508
|
-
),
|
|
509
|
-
secondary_y=False,
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
df_weight = pd.DataFrame(queryset.values("date", "weighting", "portfolio__name"))
|
|
513
|
-
df_weight = df_weight.where(pd.notnull(df_weight), 0)
|
|
514
|
-
df_weight = df_weight.groupby(["date", "portfolio__name"]).sum().reset_index()
|
|
515
|
-
for portfolio_name, df_tmp in df_weight.groupby("portfolio__name"):
|
|
516
|
-
fig.add_trace(
|
|
517
|
-
go.Scatter(
|
|
518
|
-
x=df_tmp.date,
|
|
519
|
-
y=df_tmp.weighting,
|
|
520
|
-
hovertemplate=get_hovertemplate_timeserie(is_percent=True),
|
|
521
|
-
mode="lines",
|
|
522
|
-
name=f"Allocation: {portfolio_name}",
|
|
523
|
-
),
|
|
524
|
-
secondary_y=True,
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
# Set x-axis title
|
|
528
|
-
fig.update_xaxes(title_text="Date")
|
|
529
|
-
# Set y-axes titles
|
|
530
|
-
fig.update_yaxes(
|
|
531
|
-
title_text="<b>Price</b>",
|
|
532
|
-
secondary_y=False,
|
|
533
|
-
titlefont=dict(color="green"),
|
|
534
|
-
tickfont=dict(color="green"),
|
|
535
|
-
)
|
|
536
|
-
fig.update_yaxes(
|
|
537
|
-
title_text="<b>Portfolio Allocation (%)</b>",
|
|
538
|
-
secondary_y=True,
|
|
539
|
-
titlefont=dict(color="blue"),
|
|
540
|
-
tickfont=dict(color="blue"),
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
return fig
|
|
544
|
-
|
|
545
|
-
|
|
546
332
|
class CompositionModelPortfolioPandasView(
|
|
547
333
|
UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, ExportPandasAPIViewSet
|
|
548
334
|
):
|