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.
- wbportfolio/constants.py +1 -0
- wbportfolio/factories/orders/orders.py +10 -2
- wbportfolio/filters/assets.py +10 -2
- wbportfolio/import_export/handlers/orders.py +1 -1
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
- wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
- wbportfolio/models/asset.py +6 -20
- wbportfolio/models/builder.py +11 -11
- wbportfolio/models/orders/order_proposals.py +53 -20
- wbportfolio/models/orders/orders.py +4 -1
- wbportfolio/models/portfolio.py +16 -8
- wbportfolio/models/products.py +9 -0
- wbportfolio/pms/trading/handler.py +0 -1
- wbportfolio/serializers/orders/order_proposals.py +2 -18
- wbportfolio/tests/conftest.py +6 -2
- wbportfolio/tests/models/orders/test_order_proposals.py +61 -15
- wbportfolio/tests/models/test_products.py +11 -0
- 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 +337 -154
- 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/orders/configs/buttons/order_proposals.py +12 -2
- wbportfolio/viewsets/orders/order_proposals.py +2 -1
- wbportfolio/viewsets/positions.py +3 -2
- {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/METADATA +3 -1
- {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/RECORD +33 -32
- wbportfolio/viewsets/signals.py +0 -43
- {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.21.dist-info → wbportfolio-1.54.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,6 +4,7 @@ from decimal import Decimal
|
|
|
4
4
|
from unittest.mock import call, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
+
from django.db.models import Sum
|
|
7
8
|
from faker import Faker
|
|
8
9
|
from pandas._libs.tslibs.offsets import BDay, BusinessMonthEnd
|
|
9
10
|
|
|
@@ -50,31 +51,36 @@ class TestOrderProposal:
|
|
|
50
51
|
)
|
|
51
52
|
|
|
52
53
|
# Create orders for testing
|
|
53
|
-
|
|
54
|
+
o1 = order_factory.create(
|
|
54
55
|
order_proposal=order_proposal,
|
|
55
56
|
weighting=Decimal("0.05"),
|
|
56
57
|
portfolio=order_proposal.portfolio,
|
|
57
58
|
underlying_instrument=a1.underlying_quote,
|
|
58
59
|
)
|
|
59
|
-
|
|
60
|
+
o2 = order_factory.create(
|
|
60
61
|
order_proposal=order_proposal,
|
|
61
62
|
weighting=Decimal("-0.05"),
|
|
62
63
|
portfolio=order_proposal.portfolio,
|
|
63
64
|
underlying_instrument=a2.underlying_quote,
|
|
64
65
|
)
|
|
65
66
|
|
|
67
|
+
r1 = o1.price / a1.initial_price - Decimal("1")
|
|
68
|
+
r2 = o2.price / a2.initial_price - Decimal("1")
|
|
69
|
+
p_return = a1.weighting * (Decimal("1") + r1) + a2.weighting * (Decimal("1") + r2)
|
|
66
70
|
# Get the validated trading service
|
|
67
|
-
|
|
71
|
+
trades = order_proposal.validated_trading_service.trades_batch.trades_map
|
|
72
|
+
t1 = trades[a1.underlying_quote.id]
|
|
73
|
+
t2 = trades[a2.underlying_quote.id]
|
|
68
74
|
|
|
69
75
|
# Assert effective and target portfolios are as expected
|
|
70
|
-
assert
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
a2.
|
|
77
|
-
|
|
76
|
+
assert t1.previous_weight == a1.weighting
|
|
77
|
+
assert t2.previous_weight == a2.weighting
|
|
78
|
+
assert t1.target_weight == pytest.approx(
|
|
79
|
+
a1.weighting * ((r1 + Decimal("1")) / p_return) + o1.weighting, abs=Decimal("1e-8")
|
|
80
|
+
)
|
|
81
|
+
assert t2.target_weight == pytest.approx(
|
|
82
|
+
a2.weighting * ((r2 + Decimal("1")) / p_return) + o2.weighting, abs=Decimal("1e-8")
|
|
83
|
+
)
|
|
78
84
|
|
|
79
85
|
# Test the calculation of the last effective date
|
|
80
86
|
def test_last_effective_date(self, order_proposal, asset_position_factory):
|
|
@@ -361,15 +367,12 @@ class TestOrderProposal:
|
|
|
361
367
|
assert order_proposal.orders.get(underlying_instrument=i2).weighting == Decimal("-0.3")
|
|
362
368
|
assert order_proposal.orders.get(underlying_instrument=cash).weighting == Decimal("0.5")
|
|
363
369
|
|
|
364
|
-
def test_reset_orders_remove_invalid_orders(self, order_proposal, order_factory
|
|
370
|
+
def test_reset_orders_remove_invalid_orders(self, order_proposal, order_factory):
|
|
365
371
|
# create a invalid trade and its price
|
|
366
372
|
invalid_trade = order_factory.create(order_proposal=order_proposal, weighting=Decimal(0))
|
|
367
|
-
instrument_price_factory.create(date=invalid_trade.value_date, instrument=invalid_trade.underlying_instrument)
|
|
368
373
|
|
|
369
374
|
# create a valid trade and its price
|
|
370
375
|
valid_trade = order_factory.create(order_proposal=order_proposal, weighting=Decimal(1))
|
|
371
|
-
instrument_price_factory.create(date=valid_trade.value_date, instrument=valid_trade.underlying_instrument)
|
|
372
|
-
|
|
373
376
|
order_proposal.reset_orders()
|
|
374
377
|
assert order_proposal.orders.get(underlying_instrument=valid_trade.underlying_instrument).weighting == Decimal(
|
|
375
378
|
"1"
|
|
@@ -687,3 +690,46 @@ class TestOrderProposal:
|
|
|
687
690
|
order.save()
|
|
688
691
|
assert order.shares == Decimal(0)
|
|
689
692
|
assert order.weighting == Decimal(0)
|
|
693
|
+
|
|
694
|
+
def test_reset_order_use_desired_target_weight(self, order_proposal, order_factory):
|
|
695
|
+
order1 = order_factory.create(
|
|
696
|
+
order_proposal=order_proposal, weighting=Decimal("0.5"), desired_target_weight=Decimal("0.7")
|
|
697
|
+
)
|
|
698
|
+
order2 = order_factory.create(
|
|
699
|
+
order_proposal=order_proposal, weighting=Decimal("0.5"), desired_target_weight=Decimal("0.3")
|
|
700
|
+
)
|
|
701
|
+
order_proposal.submit()
|
|
702
|
+
order_proposal.approve()
|
|
703
|
+
order_proposal.save()
|
|
704
|
+
|
|
705
|
+
order1.refresh_from_db()
|
|
706
|
+
order2.refresh_from_db()
|
|
707
|
+
assert order1.desired_target_weight == Decimal("0.5")
|
|
708
|
+
assert order2.desired_target_weight == Decimal("0.5")
|
|
709
|
+
assert order1.weighting == Decimal("0.5")
|
|
710
|
+
assert order2.weighting == Decimal("0.5")
|
|
711
|
+
|
|
712
|
+
order1.desired_target_weight = Decimal("0.7")
|
|
713
|
+
order2.desired_target_weight = Decimal("0.3")
|
|
714
|
+
order1.save()
|
|
715
|
+
order2.save()
|
|
716
|
+
|
|
717
|
+
order_proposal.reset_orders(use_desired_target_weight=True)
|
|
718
|
+
order1.refresh_from_db()
|
|
719
|
+
order2.refresh_from_db()
|
|
720
|
+
assert order1.weighting == Decimal("0.7")
|
|
721
|
+
assert order2.weighting == Decimal("0.3")
|
|
722
|
+
|
|
723
|
+
def test_reset_order_proposal_keeps_target_cash_weight(self, order_factory, order_proposal_factory):
|
|
724
|
+
order_proposal = order_proposal_factory.create(
|
|
725
|
+
total_cash_weight=Decimal("0.02")
|
|
726
|
+
) # create a OP with total cash weight of 2%
|
|
727
|
+
|
|
728
|
+
# create orders that total weight account for only 50%
|
|
729
|
+
order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.3"))
|
|
730
|
+
order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.2"))
|
|
731
|
+
|
|
732
|
+
order_proposal.reset_orders()
|
|
733
|
+
assert order_proposal.get_orders().aggregate(s=Sum("target_weight"))["s"] == Decimal(
|
|
734
|
+
"0.98"
|
|
735
|
+
), "The total target weight leftover does not equal the stored total cash weight"
|
|
@@ -215,3 +215,14 @@ class TestProductModel(PortfolioTestMixin):
|
|
|
215
215
|
body=f"The product {product} will be terminated on the {product.delisted_date:%Y-%m-%d}",
|
|
216
216
|
user=internal_user,
|
|
217
217
|
)
|
|
218
|
+
|
|
219
|
+
def test_delist_product_disable_report(self, product, report_factory):
|
|
220
|
+
report = report_factory.create(content_object=product, is_active=True)
|
|
221
|
+
assert product.delisted_date is None
|
|
222
|
+
assert report.is_active
|
|
223
|
+
|
|
224
|
+
product.delisted_date = datetime.date.today()
|
|
225
|
+
product.save()
|
|
226
|
+
|
|
227
|
+
report.refresh_from_db()
|
|
228
|
+
assert report.is_active is False
|
wbportfolio/tests/signals.py
CHANGED
|
@@ -17,8 +17,6 @@ from wbportfolio.viewsets import (
|
|
|
17
17
|
ClaimEntryModelViewSet,
|
|
18
18
|
CustodianDistributionInstrumentChartViewSet,
|
|
19
19
|
CustomerDistributionInstrumentChartViewSet,
|
|
20
|
-
DistributionChartViewSet,
|
|
21
|
-
DistributionTableViewSet,
|
|
22
20
|
NominalProductChartView,
|
|
23
21
|
OrderOrderProposalModelViewSet,
|
|
24
22
|
OrderProposalPortfolioModelViewSet,
|
|
@@ -96,14 +94,6 @@ def receive_kwargs_portfolio_role_instrument(sender, *args, **kwargs):
|
|
|
96
94
|
return {}
|
|
97
95
|
|
|
98
96
|
|
|
99
|
-
@receiver(custom_update_kwargs, sender=DistributionChartViewSet)
|
|
100
|
-
@receiver(custom_update_kwargs, sender=DistributionTableViewSet)
|
|
101
|
-
def receive_kwargs_distribution_chart_instrument_id(sender, *args, **kwargs):
|
|
102
|
-
if instrument_id := kwargs.get("underlying_instrument_id"):
|
|
103
|
-
return {"instrument_id": instrument_id}
|
|
104
|
-
return {}
|
|
105
|
-
|
|
106
|
-
|
|
107
97
|
@receiver(custom_update_kwargs, sender=ProductPerformanceFeesModelViewSet)
|
|
108
98
|
def receive_kwargs_product_performance_fees(sender, *args, **kwargs):
|
|
109
99
|
CurrencyFXRatesFactory()
|
wbportfolio/tests/tests.py
CHANGED
|
@@ -19,6 +19,8 @@ for key, value in default_config.items():
|
|
|
19
19
|
"AccountReconciliationLine",
|
|
20
20
|
"TopDownPortfolioCompositionPandasAPIView",
|
|
21
21
|
"CompositionModelPortfolioPandasView",
|
|
22
|
+
"DistributionChartViewSet",
|
|
23
|
+
"DistributionTableViewSet",
|
|
22
24
|
# "ClaimModelViewSet",
|
|
23
25
|
# "ClaimModelSerializer",
|
|
24
26
|
],
|
wbportfolio/viewsets/__init__.py
CHANGED
|
@@ -2,12 +2,16 @@ from .assets import (
|
|
|
2
2
|
AssetPositionInstrumentModelViewSet,
|
|
3
3
|
AssetPositionModelViewSet,
|
|
4
4
|
AssetPositionPortfolioModelViewSet,
|
|
5
|
-
AssetPositionUnderlyingInstrumentChartViewSet,
|
|
6
5
|
CashPositionPortfolioPandasAPIView,
|
|
7
6
|
CompositionModelPortfolioPandasView,
|
|
8
|
-
ContributorPortfolioChartView,
|
|
9
7
|
)
|
|
10
|
-
from .charts import
|
|
8
|
+
from .charts import (
|
|
9
|
+
DistributionChartViewSet,
|
|
10
|
+
DistributionTableViewSet,
|
|
11
|
+
AssetPositionUnderlyingInstrumentChartViewSet,
|
|
12
|
+
ContributorPortfolioChartView
|
|
13
|
+
|
|
14
|
+
)
|
|
11
15
|
from .custodians import CustodianModelViewSet, CustodianRepresentationViewSet
|
|
12
16
|
from .portfolio_relationship import InstrumentPreferedClassificationThroughProductModelViewSet
|
|
13
17
|
from .portfolios import (
|
|
@@ -23,7 +27,6 @@ from .positions import (
|
|
|
23
27
|
)
|
|
24
28
|
from .registers import RegisterModelViewSet, RegisterRepresentationViewSet
|
|
25
29
|
from .roles import PortfolioRoleInstrumentModelViewSet, PortfolioRoleModelViewSet
|
|
26
|
-
from .signals import *
|
|
27
30
|
from .rebalancing import RebalancingModelRepresentationViewSet, RebalancerRepresentationViewSet, RebalancerModelViewSet
|
|
28
31
|
from .transactions import *
|
|
29
32
|
from .orders import *
|
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
|
):
|