wbportfolio 1.52.0__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.
- wbportfolio/admin/__init__.py +3 -1
- wbportfolio/admin/indexes.py +1 -1
- wbportfolio/admin/orders/__init__.py +2 -0
- wbportfolio/admin/orders/order_proposals.py +16 -0
- wbportfolio/admin/orders/orders.py +32 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
- wbportfolio/admin/transactions/__init__.py +0 -2
- wbportfolio/admin/transactions/dividends.py +40 -4
- wbportfolio/admin/transactions/fees.py +24 -14
- wbportfolio/admin/transactions/trades.py +34 -27
- wbportfolio/analysis/claims.py +5 -6
- wbportfolio/api_clients/ubs.py +162 -0
- wbportfolio/constants.py +1 -0
- wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/contrib/company_portfolio/serializers.py +32 -22
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
- wbportfolio/defaults/fees/default.py +7 -15
- wbportfolio/factories/__init__.py +2 -2
- wbportfolio/factories/assets.py +1 -1
- wbportfolio/factories/dividends.py +8 -3
- wbportfolio/factories/fees.py +8 -4
- wbportfolio/factories/orders/__init__.py +2 -0
- wbportfolio/factories/orders/order_proposals.py +21 -0
- wbportfolio/factories/orders/orders.py +34 -0
- wbportfolio/factories/portfolios.py +2 -1
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/factories/trades.py +12 -16
- wbportfolio/filters/assets.py +18 -4
- wbportfolio/filters/orders/__init__.py +2 -0
- wbportfolio/filters/orders/order_proposals.py +55 -0
- wbportfolio/filters/orders/orders.py +11 -0
- wbportfolio/filters/portfolios.py +38 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/__init__.py +1 -2
- wbportfolio/filters/transactions/fees.py +5 -12
- wbportfolio/filters/transactions/trades.py +16 -8
- wbportfolio/filters/transactions/utils.py +42 -0
- wbportfolio/import_export/backends/ubs/__init__.py +1 -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/ubs/trade.py +48 -0
- wbportfolio/import_export/backends/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +22 -10
- wbportfolio/import_export/handlers/dividend.py +8 -8
- wbportfolio/import_export/handlers/fees.py +13 -23
- wbportfolio/import_export/handlers/orders.py +71 -0
- wbportfolio/import_export/handlers/trade.py +53 -77
- wbportfolio/import_export/parsers/default_mapping.py +1 -1
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
- wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
- wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
- wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/fees.py +7 -9
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
- wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
- wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
- wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
- wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
- wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
- wbportfolio/import_export/parsers/ubs/equity.py +3 -2
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
- wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
- wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
- wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
- wbportfolio/import_export/resources/trades.py +3 -3
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
- wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
- wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
- wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
- wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
- wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
- wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
- wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
- 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/migrations/0089_orderproposal_min_weighting.py +71 -0
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
- wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
- wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
- wbportfolio/models/__init__.py +2 -0
- wbportfolio/models/adjustments.py +1 -1
- wbportfolio/models/asset.py +28 -170
- wbportfolio/models/builder.py +323 -0
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/portfolio.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
- wbportfolio/models/orders/__init__.py +2 -0
- wbportfolio/models/orders/order_proposals.py +1414 -0
- wbportfolio/models/orders/orders.py +410 -0
- wbportfolio/models/portfolio.py +311 -289
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +12 -0
- wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/__init__.py +0 -4
- wbportfolio/models/transactions/claim.py +7 -6
- wbportfolio/models/transactions/dividends.py +42 -5
- wbportfolio/models/transactions/fees.py +55 -22
- wbportfolio/models/transactions/trades.py +121 -442
- wbportfolio/models/transactions/transactions.py +78 -158
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +35 -0
- wbportfolio/order_routing/adapters/__init__.py +65 -0
- wbportfolio/order_routing/adapters/ubs.py +195 -0
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/__init__.py +0 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/analytics/portfolio.py +17 -9
- wbportfolio/pms/analytics/utils.py +9 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +198 -63
- wbportfolio/rebalancing/base.py +12 -1
- wbportfolio/rebalancing/decorators.py +1 -1
- wbportfolio/rebalancing/models/composite.py +4 -8
- wbportfolio/rebalancing/models/equally_weighted.py +13 -11
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
- wbportfolio/rebalancing/models/model_portfolio.py +14 -18
- wbportfolio/risk_management/backends/__init__.py +1 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
- wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
- wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
- wbportfolio/serializers/__init__.py +1 -0
- wbportfolio/serializers/orders/__init__.py +2 -0
- wbportfolio/serializers/orders/order_proposals.py +115 -0
- wbportfolio/serializers/orders/orders.py +283 -0
- wbportfolio/serializers/portfolios.py +7 -7
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/serializers/signals.py +9 -12
- wbportfolio/serializers/transactions/__init__.py +1 -10
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/serializers/transactions/dividends.py +37 -9
- wbportfolio/serializers/transactions/fees.py +39 -10
- wbportfolio/serializers/transactions/trades.py +55 -157
- wbportfolio/tasks.py +43 -5
- wbportfolio/tests/analysis/__init__.py +0 -0
- wbportfolio/tests/analysis/test_claims.py +85 -0
- wbportfolio/tests/conftest.py +12 -12
- wbportfolio/tests/models/orders/__init__.py +0 -0
- wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
- wbportfolio/tests/models/test_assets.py +7 -3
- wbportfolio/tests/models/test_imports.py +9 -13
- wbportfolio/tests/models/test_portfolios.py +102 -95
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_fees.py +7 -13
- wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
- wbportfolio/tests/models/transactions/test_trades.py +0 -20
- wbportfolio/tests/pms/test_analytics.py +22 -3
- wbportfolio/tests/rebalancing/test_models.py +51 -57
- wbportfolio/tests/signals.py +10 -20
- wbportfolio/tests/tests.py +3 -1
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +10 -13
- wbportfolio/viewsets/__init__.py +9 -4
- wbportfolio/viewsets/assets.py +3 -204
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +344 -154
- wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/__init__.py +2 -5
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/fees.py +3 -3
- wbportfolio/viewsets/configs/display/portfolios.py +5 -5
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/configs/display/trades.py +1 -189
- wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
- wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
- wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
- wbportfolio/viewsets/configs/menu/__init__.py +1 -1
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/configs/titles/__init__.py +2 -3
- wbportfolio/viewsets/configs/titles/fees.py +4 -8
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/mixins.py +5 -1
- wbportfolio/viewsets/orders/__init__.py +6 -0
- wbportfolio/viewsets/orders/configs/__init__.py +4 -0
- wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
- wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
- wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
- wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
- wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
- wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
- wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
- wbportfolio/viewsets/orders/order_proposals.py +252 -0
- wbportfolio/viewsets/orders/orders.py +277 -0
- wbportfolio/viewsets/portfolios.py +36 -12
- wbportfolio/viewsets/positions.py +3 -2
- wbportfolio/viewsets/products.py +6 -6
- wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
- wbportfolio/viewsets/transactions/__init__.py +3 -14
- wbportfolio/viewsets/transactions/fees.py +22 -22
- wbportfolio/viewsets/transactions/trades.py +1 -180
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/admin/transactions/transactions.py +0 -38
- wbportfolio/factories/transactions.py +0 -22
- wbportfolio/fdm/tasks.py +0 -13
- wbportfolio/filters/transactions/transactions.py +0 -99
- wbportfolio/models/transactions/expiry.py +0 -7
- wbportfolio/models/transactions/trade_proposals.py +0 -704
- wbportfolio/pms/trading/handler.py +0 -161
- wbportfolio/serializers/transactions/expiry.py +0 -18
- wbportfolio/serializers/transactions/trade_proposals.py +0 -76
- wbportfolio/serializers/transactions/transactions.py +0 -85
- wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
- wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
- wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
- wbportfolio/viewsets/configs/display/transactions.py +0 -55
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
- wbportfolio/viewsets/configs/menu/transactions.py +0 -9
- wbportfolio/viewsets/configs/titles/transactions.py +0 -9
- wbportfolio/viewsets/signals.py +0 -43
- wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
- wbportfolio/viewsets/transactions/transactions.py +0 -122
- /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from django.contrib.messages import error, info
|
|
4
|
+
from django.db.models import (
|
|
5
|
+
Case,
|
|
6
|
+
CharField,
|
|
7
|
+
F,
|
|
8
|
+
Func,
|
|
9
|
+
Sum,
|
|
10
|
+
Value,
|
|
11
|
+
When,
|
|
12
|
+
)
|
|
13
|
+
from django.shortcuts import get_object_or_404
|
|
14
|
+
from django.utils.functional import cached_property
|
|
15
|
+
from rest_framework.decorators import action
|
|
16
|
+
from rest_framework.response import Response
|
|
17
|
+
from wbcore import viewsets
|
|
18
|
+
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
19
|
+
from wbcore.utils.strings import format_number
|
|
20
|
+
from wbcore.viewsets.mixins import OrderableMixin
|
|
21
|
+
|
|
22
|
+
from wbportfolio.import_export.resources.trades import OrderProposalTradeResource
|
|
23
|
+
from wbportfolio.models import Order, OrderProposal
|
|
24
|
+
from wbportfolio.serializers import (
|
|
25
|
+
OrderOrderProposalListModelSerializer,
|
|
26
|
+
OrderOrderProposalModelSerializer,
|
|
27
|
+
ReadOnlyOrderOrderProposalModelSerializer,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from ...filters.orders import OrderFilterSet
|
|
31
|
+
from ...permissions import IsPortfolioManager
|
|
32
|
+
from ..mixins import UserPortfolioRequestPermissionMixin
|
|
33
|
+
from .configs import (
|
|
34
|
+
OrderOrderProposalButtonConfig,
|
|
35
|
+
OrderOrderProposalDisplayConfig,
|
|
36
|
+
OrderOrderProposalEndpointConfig,
|
|
37
|
+
)
|
|
38
|
+
from .configs.buttons.orders import ExecutionInstructionSerializer
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OrderOrderProposalModelViewSet(
|
|
42
|
+
UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, OrderableMixin, viewsets.ModelViewSet
|
|
43
|
+
):
|
|
44
|
+
IDENTIFIER = "wbportfolio:order"
|
|
45
|
+
IMPORT_ALLOWED = True
|
|
46
|
+
ordering = (
|
|
47
|
+
"order_proposal",
|
|
48
|
+
"order",
|
|
49
|
+
)
|
|
50
|
+
ordering_fields = (
|
|
51
|
+
"underlying_instrument__name",
|
|
52
|
+
"underlying_instrument_isin",
|
|
53
|
+
"underlying_instrument_ticker",
|
|
54
|
+
"underlying_instrument_refinitiv_identifier_code",
|
|
55
|
+
"underlying_instrument_instrument_type",
|
|
56
|
+
"target_weight",
|
|
57
|
+
"effective_weight",
|
|
58
|
+
"effective_shares",
|
|
59
|
+
"target_shares",
|
|
60
|
+
"shares",
|
|
61
|
+
"weighting",
|
|
62
|
+
)
|
|
63
|
+
IDENTIFIER = "wbportfolio:order"
|
|
64
|
+
search_fields = ("underlying_instrument__name",)
|
|
65
|
+
queryset = Order.objects.none()
|
|
66
|
+
filterset_class = OrderFilterSet
|
|
67
|
+
|
|
68
|
+
display_config_class = OrderOrderProposalDisplayConfig
|
|
69
|
+
endpoint_config_class = OrderOrderProposalEndpointConfig
|
|
70
|
+
serializer_class = OrderOrderProposalModelSerializer
|
|
71
|
+
button_config_class = OrderOrderProposalButtonConfig
|
|
72
|
+
|
|
73
|
+
@cached_property
|
|
74
|
+
def order_proposal(self):
|
|
75
|
+
return get_object_or_404(OrderProposal, pk=self.kwargs["order_proposal_id"])
|
|
76
|
+
|
|
77
|
+
@cached_property
|
|
78
|
+
def portfolio_total_asset_value(self):
|
|
79
|
+
return self.order_proposal.portfolio_total_asset_value
|
|
80
|
+
|
|
81
|
+
def has_import_permission(self, request) -> bool: # allow import only on draft order proposal
|
|
82
|
+
return super().has_import_permission(request) and self.order_proposal.status == OrderProposal.Status.DRAFT
|
|
83
|
+
|
|
84
|
+
def get_import_resource_kwargs(self):
|
|
85
|
+
resource_kwargs = super().get_import_resource_kwargs()
|
|
86
|
+
resource_kwargs["columns_mapping"] = {"underlying_instrument": "underlying_instrument__isin"}
|
|
87
|
+
return resource_kwargs
|
|
88
|
+
|
|
89
|
+
def get_resource_class(self):
|
|
90
|
+
return OrderProposalTradeResource
|
|
91
|
+
|
|
92
|
+
def get_aggregates(self, queryset, *args, **kwargs):
|
|
93
|
+
agg = {}
|
|
94
|
+
if queryset.exists():
|
|
95
|
+
noncash_aggregates = queryset.filter(underlying_instrument__is_cash=False).aggregate(
|
|
96
|
+
sum_target_weight=Sum(F("target_weight")),
|
|
97
|
+
sum_effective_weight=Sum(F("effective_weight")),
|
|
98
|
+
sum_target_total_value_fx_portfolio=Sum(F("target_total_value_fx_portfolio")),
|
|
99
|
+
sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
|
|
100
|
+
)
|
|
101
|
+
# weights aggregates
|
|
102
|
+
cash_sum_effective_weight = self.order_proposal.total_effective_portfolio_weight - (
|
|
103
|
+
noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
104
|
+
)
|
|
105
|
+
cash_sum_target_cash_weight = Decimal("1.0") - (noncash_aggregates["sum_target_weight"] or Decimal(0))
|
|
106
|
+
noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
107
|
+
noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
|
|
108
|
+
sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
109
|
+
sum_sell_weight = queryset.filter(weighting__lt=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
110
|
+
|
|
111
|
+
# shares aggregates
|
|
112
|
+
cash_sum_effective_total_value_fx_portfolio = cash_sum_effective_weight * self.portfolio_total_asset_value
|
|
113
|
+
cash_sum_target_total_value_fx_portfolio = cash_sum_target_cash_weight * self.portfolio_total_asset_value
|
|
114
|
+
noncash_sum_effective_total_value_fx_portfolio = noncash_aggregates[
|
|
115
|
+
"sum_effective_total_value_fx_portfolio"
|
|
116
|
+
] or Decimal(0)
|
|
117
|
+
noncash_sum_target_total_value_fx_portfolio = noncash_aggregates[
|
|
118
|
+
"sum_target_total_value_fx_portfolio"
|
|
119
|
+
] or Decimal(0)
|
|
120
|
+
sum_buy_total_value_fx_portfolio = queryset.filter(total_value_fx_portfolio__gte=0).aggregate(
|
|
121
|
+
s=Sum(F("total_value_fx_portfolio"))
|
|
122
|
+
)["s"] or Decimal(0)
|
|
123
|
+
sum_sell_total_value_fx_portfolio = queryset.filter(total_value_fx_portfolio__lt=0).aggregate(
|
|
124
|
+
s=Sum(F("total_value_fx_portfolio"))
|
|
125
|
+
)["s"] or Decimal(0)
|
|
126
|
+
|
|
127
|
+
agg = {
|
|
128
|
+
"effective_weight": {
|
|
129
|
+
"Cash": format_number(cash_sum_effective_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
130
|
+
"Non-Cash": format_number(noncash_sum_effective_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
131
|
+
"Total": format_number(
|
|
132
|
+
noncash_sum_effective_weight + cash_sum_effective_weight,
|
|
133
|
+
decimal=Order.ORDER_WEIGHTING_PRECISION,
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
"target_weight": {
|
|
137
|
+
"Cash": format_number(cash_sum_target_cash_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
138
|
+
"Non-Cash": format_number(noncash_sum_target_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
139
|
+
"Total": format_number(
|
|
140
|
+
cash_sum_target_cash_weight + noncash_sum_target_weight,
|
|
141
|
+
decimal=Order.ORDER_WEIGHTING_PRECISION,
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
"effective_total_value_fx_portfolio": {
|
|
145
|
+
"Cash": format_number(cash_sum_effective_total_value_fx_portfolio, decimal=6),
|
|
146
|
+
"Non-Cash": format_number(noncash_sum_effective_total_value_fx_portfolio, decimal=6),
|
|
147
|
+
"Total": format_number(
|
|
148
|
+
cash_sum_effective_total_value_fx_portfolio + noncash_sum_effective_total_value_fx_portfolio,
|
|
149
|
+
decimal=6,
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
"target_total_value_fx_portfolio": {
|
|
153
|
+
"Cash": format_number(cash_sum_target_total_value_fx_portfolio, decimal=6),
|
|
154
|
+
"Non-Cash": format_number(noncash_sum_target_total_value_fx_portfolio, decimal=6),
|
|
155
|
+
"Total": format_number(
|
|
156
|
+
cash_sum_target_total_value_fx_portfolio + noncash_sum_target_total_value_fx_portfolio,
|
|
157
|
+
decimal=6,
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
"weighting": {
|
|
161
|
+
"Cash Flow": format_number(
|
|
162
|
+
sum_sell_weight + sum_buy_weight,
|
|
163
|
+
decimal=Order.ORDER_WEIGHTING_PRECISION,
|
|
164
|
+
),
|
|
165
|
+
"Buy": format_number(sum_buy_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
166
|
+
"Sell": format_number(sum_sell_weight, decimal=Order.ORDER_WEIGHTING_PRECISION),
|
|
167
|
+
},
|
|
168
|
+
"total_value_fx_portfolio": {
|
|
169
|
+
"Cash Flow": format_number(
|
|
170
|
+
cash_sum_target_total_value_fx_portfolio - cash_sum_effective_total_value_fx_portfolio,
|
|
171
|
+
decimal=6,
|
|
172
|
+
),
|
|
173
|
+
"Buy": format_number(sum_buy_total_value_fx_portfolio, decimal=6),
|
|
174
|
+
"Sell": format_number(sum_sell_total_value_fx_portfolio, decimal=6),
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return agg
|
|
179
|
+
|
|
180
|
+
def get_serializer_class(self):
|
|
181
|
+
if self.order_proposal.status != OrderProposal.Status.DRAFT and not self.order_proposal.can_be_confirmed:
|
|
182
|
+
return ReadOnlyOrderOrderProposalModelSerializer
|
|
183
|
+
if not self.new_mode and "pk" not in self.kwargs:
|
|
184
|
+
serializer_base_class = OrderOrderProposalListModelSerializer
|
|
185
|
+
else:
|
|
186
|
+
serializer_base_class = OrderOrderProposalModelSerializer
|
|
187
|
+
if not self.order_proposal.portfolio_total_asset_value:
|
|
188
|
+
|
|
189
|
+
class OnlyWeightSerializerClass(serializer_base_class):
|
|
190
|
+
class Meta(serializer_base_class.Meta):
|
|
191
|
+
read_only_fields = list(serializer_base_class.Meta.read_only_fields) + [
|
|
192
|
+
"shares",
|
|
193
|
+
"target_shares",
|
|
194
|
+
"total_value_fx_portfolio",
|
|
195
|
+
"target_total_value_fx_portfolio",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
return OnlyWeightSerializerClass
|
|
199
|
+
return serializer_base_class
|
|
200
|
+
|
|
201
|
+
def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
|
|
202
|
+
if self.orders.exists() and self.order_proposal.status in [
|
|
203
|
+
OrderProposal.Status.PENDING,
|
|
204
|
+
OrderProposal.Status.DRAFT,
|
|
205
|
+
]:
|
|
206
|
+
total_target_weight = self.orders.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
|
|
207
|
+
if round(total_target_weight, 8) != 1:
|
|
208
|
+
info(
|
|
209
|
+
request,
|
|
210
|
+
"The total target weight does not equal 1. To avoid automatic cash allocation, please adjust the order weights to sum up to 1. Otherwise, a cash component will be added when this order proposal is submitted.",
|
|
211
|
+
)
|
|
212
|
+
if self.orders.filter(has_warnings=True).exists():
|
|
213
|
+
error(
|
|
214
|
+
request,
|
|
215
|
+
"Some orders failed preparation. To resolve this, please revert the order proposal to draft, review and correct the orders, and then resubmit.",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@cached_property
|
|
219
|
+
def orders(self):
|
|
220
|
+
qs = self.order_proposal.get_orders()
|
|
221
|
+
if not self.is_portfolio_manager:
|
|
222
|
+
return qs.none()
|
|
223
|
+
return qs
|
|
224
|
+
|
|
225
|
+
def get_queryset(self):
|
|
226
|
+
return (
|
|
227
|
+
self.orders.filter(underlying_instrument__is_cash=False)
|
|
228
|
+
.annotate( # .exclude(underlying_instrument__is_cash=True)
|
|
229
|
+
underlying_instrument_isin=F("underlying_instrument__isin"),
|
|
230
|
+
underlying_instrument_ticker=F("underlying_instrument__ticker"),
|
|
231
|
+
underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
|
|
232
|
+
underlying_instrument_instrument_type=Case(
|
|
233
|
+
When(
|
|
234
|
+
underlying_instrument__parent__is_security=True,
|
|
235
|
+
then=F("underlying_instrument__parent__instrument_type__short_name"),
|
|
236
|
+
),
|
|
237
|
+
default=F("underlying_instrument__instrument_type__short_name"),
|
|
238
|
+
),
|
|
239
|
+
underlying_instrument_exchange=F("underlying_instrument__exchange__name"),
|
|
240
|
+
effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
|
|
241
|
+
target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
|
|
242
|
+
portfolio_currency=F("portfolio__currency__symbol"),
|
|
243
|
+
underlying_instrument_currency=F("underlying_instrument__currency__symbol"),
|
|
244
|
+
security=F("underlying_instrument__parent"),
|
|
245
|
+
company=F("underlying_instrument__parent__parent"),
|
|
246
|
+
execution_instruction_parameters_repr=Func(
|
|
247
|
+
"execution_instruction_parameters",
|
|
248
|
+
function="string_agg",
|
|
249
|
+
template="(SELECT string_agg(key || '=' || value, ',') FROM jsonb_each_text(%(expressions)s))",
|
|
250
|
+
output_field=CharField(),
|
|
251
|
+
),
|
|
252
|
+
execution_date=F("execution_trade__transaction_date"),
|
|
253
|
+
execution_price=F("execution_trade__price"),
|
|
254
|
+
execution_traded_shares=F("execution_trade__shares"),
|
|
255
|
+
)
|
|
256
|
+
.select_related(
|
|
257
|
+
"underlying_instrument", "underlying_instrument__parent", "underlying_instrument__parent__parent"
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@action(detail=True, methods=["PUT"], permission_classes=[IsPortfolioManager])
|
|
262
|
+
def changeexecutioninstruction(self, request, pk=None, order_proposal_id=None, **kwargs):
|
|
263
|
+
serializer = ExecutionInstructionSerializer(data=request.data)
|
|
264
|
+
order_proposal = get_object_or_404(OrderProposal, pk=order_proposal_id)
|
|
265
|
+
if serializer.is_valid(raise_exception=True):
|
|
266
|
+
parameters = dict(serializer.data)
|
|
267
|
+
orders_to_update = order_proposal.orders.all()
|
|
268
|
+
execution_instruction = parameters.pop("execution_instruction")
|
|
269
|
+
apply_execution_instruction_to_all_orders = parameters.pop("apply_execution_instruction_to_all_orders")
|
|
270
|
+
execution_parameters = {k: v for k, v in parameters.items() if v}
|
|
271
|
+
if not apply_execution_instruction_to_all_orders:
|
|
272
|
+
orders_to_update = orders_to_update.filter(id=pk)
|
|
273
|
+
orders_to_update.update(
|
|
274
|
+
execution_instruction=execution_instruction, execution_instruction_parameters=execution_parameters
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return Response({"send": True})
|
|
@@ -2,6 +2,7 @@ from contextlib import suppress
|
|
|
2
2
|
from datetime import date, datetime
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
|
+
from celery import chain
|
|
5
6
|
from django.db.models import OuterRef, Q, Subquery, Sum
|
|
6
7
|
from django.http import HttpResponse
|
|
7
8
|
from django.shortcuts import get_object_or_404
|
|
@@ -14,6 +15,7 @@ from rest_framework.reverse import reverse
|
|
|
14
15
|
from wbcore import viewsets
|
|
15
16
|
from wbcore.contrib.currency.models import Currency
|
|
16
17
|
from wbcore.contrib.io.viewsets import ExportPandasAPIViewSet
|
|
18
|
+
from wbcore.contrib.notifications.dispatch import send_notification_as_task
|
|
17
19
|
from wbcore.pandas import fields as pf
|
|
18
20
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
19
21
|
from wbfdm.models import Instrument
|
|
@@ -21,11 +23,11 @@ from wbfdm.models import Instrument
|
|
|
21
23
|
from wbportfolio.filters import PortfolioFilterSet, PortfolioTreeGraphChartFilterSet
|
|
22
24
|
from wbportfolio.models import (
|
|
23
25
|
AssetPosition,
|
|
26
|
+
OrderProposal,
|
|
24
27
|
Portfolio,
|
|
25
28
|
PortfolioPortfolioThroughModel,
|
|
26
29
|
Rebalancer,
|
|
27
30
|
RebalancingModel,
|
|
28
|
-
TradeProposal,
|
|
29
31
|
)
|
|
30
32
|
from wbportfolio.models.portfolio import compute_lookthrough_as_task
|
|
31
33
|
from wbportfolio.serializers import (
|
|
@@ -35,6 +37,8 @@ from wbportfolio.serializers import (
|
|
|
35
37
|
)
|
|
36
38
|
|
|
37
39
|
from ..models.graphs.portfolio import PortfolioGraph
|
|
40
|
+
from ..models.utils import adjust_quote_as_task
|
|
41
|
+
from ..permissions import IsPortfolioManager
|
|
38
42
|
from .configs import (
|
|
39
43
|
PortfolioButtonConfig,
|
|
40
44
|
PortfolioDisplayConfig,
|
|
@@ -77,7 +81,7 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
77
81
|
"last_asset_under_management_usd",
|
|
78
82
|
"last_positions",
|
|
79
83
|
"automatic_rebalancer",
|
|
80
|
-
"
|
|
84
|
+
"last_order_proposal_date",
|
|
81
85
|
"is_manageable",
|
|
82
86
|
"is_tracked",
|
|
83
87
|
"only_weighting",
|
|
@@ -111,8 +115,8 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
111
115
|
.annotate(s=Sum("shares"))
|
|
112
116
|
.values("s")[:1]
|
|
113
117
|
),
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
last_order_proposal_date=Subquery(
|
|
119
|
+
OrderProposal.objects.filter(portfolio=OuterRef("pk"))
|
|
116
120
|
.order_by("-trade_date")
|
|
117
121
|
.values("trade_date")[:1]
|
|
118
122
|
),
|
|
@@ -123,10 +127,10 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
123
127
|
def rebalance(self, request, pk=None):
|
|
124
128
|
if date_str := request.POST.get("trade_date", None):
|
|
125
129
|
trade_date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
order_proposal, _ = OrderProposal.objects.get_or_create(portfolio_id=pk, trade_date=trade_date)
|
|
131
|
+
order_proposal.reset_orders()
|
|
128
132
|
return Response(
|
|
129
|
-
{"endpoint": reverse("wbportfolio:
|
|
133
|
+
{"endpoint": reverse("wbportfolio:orderproposal-detail", args=[order_proposal.id], request=request)}
|
|
130
134
|
)
|
|
131
135
|
raise HttpResponse("Bad Request", status=400)
|
|
132
136
|
|
|
@@ -147,8 +151,8 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
147
151
|
activation_date = datetime.strptime(request.POST["activation_date"], "%Y-%m-%d")
|
|
148
152
|
rebalancing_model = get_object_or_404(RebalancingModel, pk=request.POST["rebalancing_model"])
|
|
149
153
|
frequency = request.POST["frequency"]
|
|
150
|
-
|
|
151
|
-
request.POST.get("
|
|
154
|
+
apply_order_proposal_automatically = (
|
|
155
|
+
request.POST.get("apply_order_proposal_automatically", "false") == "true"
|
|
152
156
|
)
|
|
153
157
|
|
|
154
158
|
rebalancer, _ = Rebalancer.objects.update_or_create(
|
|
@@ -157,7 +161,7 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
157
161
|
"rebalancing_model": rebalancing_model,
|
|
158
162
|
"frequency": frequency,
|
|
159
163
|
"activation_date": activation_date,
|
|
160
|
-
"
|
|
164
|
+
"apply_order_proposal_automatically": apply_order_proposal_automatically,
|
|
161
165
|
},
|
|
162
166
|
)
|
|
163
167
|
return Response(
|
|
@@ -178,6 +182,26 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
178
182
|
|
|
179
183
|
return HttpResponse("Bad arguments", status=400)
|
|
180
184
|
|
|
185
|
+
@action(detail=False, methods=["POST"], permission_classes=[IsPortfolioManager])
|
|
186
|
+
def adjustquote(self, request, pk=None):
|
|
187
|
+
old_quote = get_object_or_404(Instrument, pk=request.POST["old_quote"])
|
|
188
|
+
new_quote = get_object_or_404(Instrument, pk=request.POST["new_quote"])
|
|
189
|
+
adjust_after = parse_date(request.data["adjust_after"]) if "adjust_after" in request.data else None
|
|
190
|
+
only_portfolio_ids = request.data["only_portfolios"].split(",") if "only_portfolios" in request.data else []
|
|
191
|
+
|
|
192
|
+
chain(
|
|
193
|
+
adjust_quote_as_task.si(
|
|
194
|
+
old_quote.id, new_quote.id, adjust_after=adjust_after, only_portfolio_ids=only_portfolio_ids
|
|
195
|
+
),
|
|
196
|
+
send_notification_as_task.si(
|
|
197
|
+
"wbportfolio.portfolio.action_done",
|
|
198
|
+
f"Quote adjustment from {old_quote} to {new_quote} is done",
|
|
199
|
+
"The associated positions and orders were successfully adjusted",
|
|
200
|
+
request.user.id,
|
|
201
|
+
),
|
|
202
|
+
).apply_async()
|
|
203
|
+
return HttpResponse("Ok", status=200)
|
|
204
|
+
|
|
181
205
|
|
|
182
206
|
class PortfolioPortfolioThroughModelViewSet(InternalUserPermissionMixin, viewsets.ModelViewSet):
|
|
183
207
|
serializer_class = PortfolioPortfolioThroughModelSerializer
|
|
@@ -242,9 +266,9 @@ class TopDownPortfolioCompositionPandasAPIView(UserPortfolioRequestPermissionMix
|
|
|
242
266
|
@cached_property
|
|
243
267
|
def last_rebalancing_date(self) -> date | None:
|
|
244
268
|
if self.composition_portfolio and self.last_effective_date:
|
|
245
|
-
with suppress(
|
|
269
|
+
with suppress(OrderProposal.DoesNotExist):
|
|
246
270
|
return (
|
|
247
|
-
self.composition_portfolio.
|
|
271
|
+
self.composition_portfolio.order_proposals.filter(trade_date__lte=self.last_effective_date)
|
|
248
272
|
.latest("trade_date")
|
|
249
273
|
.trade_date
|
|
250
274
|
)
|
|
@@ -10,7 +10,7 @@ from wbcore.pandas import fields as pf
|
|
|
10
10
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
11
11
|
from wbcore.serializers import decorator
|
|
12
12
|
from wbcore.utils.strings import format_number
|
|
13
|
-
from wbfdm.models import Classification, ClassificationGroup, Instrument
|
|
13
|
+
from wbfdm.models import Classification, ClassificationGroup, Instrument
|
|
14
14
|
|
|
15
15
|
from wbportfolio.filters import (
|
|
16
16
|
AggregatedAssetPositionLiquidityFilter,
|
|
@@ -19,6 +19,7 @@ from wbportfolio.filters import (
|
|
|
19
19
|
from wbportfolio.filters.positions import GroupbyChoice
|
|
20
20
|
from wbportfolio.models import AssetPosition, Portfolio
|
|
21
21
|
|
|
22
|
+
from ..constants import EQUITY_TYPE_KEYS
|
|
22
23
|
from .configs import (
|
|
23
24
|
AggregatedAssetPositionLiquidityDisplayConfig,
|
|
24
25
|
AggregatedAssetPositionLiquidityEndpointConfig,
|
|
@@ -212,7 +213,7 @@ class AggregatedAssetPositionLiquidityPandasView(InternalUserPermissionMixin, Ex
|
|
|
212
213
|
|
|
213
214
|
# Take the liquidity query for the two dates.
|
|
214
215
|
qs_assets = queryset.filter(
|
|
215
|
-
|
|
216
|
+
underlying_instrument__instrument_type__key__in=EQUITY_TYPE_KEYS,
|
|
216
217
|
date__in=[historic_date, compared_date],
|
|
217
218
|
).values(
|
|
218
219
|
"date",
|
wbportfolio/viewsets/products.py
CHANGED
|
@@ -317,16 +317,16 @@ class ProductPerformanceFeesModelViewSet(
|
|
|
317
317
|
if date_lte is None and date_gte is None:
|
|
318
318
|
date_gte, date_lte = get_start_and_end_date_from_date(date.today())
|
|
319
319
|
|
|
320
|
-
fees = Fees.valid_objects.filter(
|
|
320
|
+
fees = Fees.valid_objects.filter(product=OuterRef("pk"))
|
|
321
321
|
if date_gte:
|
|
322
|
-
fees = fees.filter(
|
|
322
|
+
fees = fees.filter(fee_date__gte=date_gte)
|
|
323
323
|
if date_lte:
|
|
324
|
-
fees = fees.filter(
|
|
324
|
+
fees = fees.filter(fee_date__lte=date_lte)
|
|
325
325
|
|
|
326
326
|
management_fees = Coalesce(
|
|
327
327
|
Subquery(
|
|
328
328
|
fees.filter(transaction_subtype=Fees.Type.MANAGEMENT)
|
|
329
|
-
.values("
|
|
329
|
+
.values("product")
|
|
330
330
|
.annotate(sum_management_fees=Sum("total_value"))
|
|
331
331
|
.values("sum_management_fees")[:1],
|
|
332
332
|
output_field=FloatField(),
|
|
@@ -340,7 +340,7 @@ class ProductPerformanceFeesModelViewSet(
|
|
|
340
340
|
Q(transaction_subtype=Fees.Type.PERFORMANCE)
|
|
341
341
|
| Q(transaction_subtype=Fees.Type.PERFORMANCE_CRYSTALIZED)
|
|
342
342
|
)
|
|
343
|
-
.values("
|
|
343
|
+
.values("product")
|
|
344
344
|
.annotate(sum_performance_fees_net=Sum("total_value"))
|
|
345
345
|
.values("sum_performance_fees_net")[:1],
|
|
346
346
|
output_field=FloatField(),
|
|
@@ -367,7 +367,7 @@ class ProductPerformanceFeesModelViewSet(
|
|
|
367
367
|
sum_total_usd=F("sum_management_fees_usd") + F("sum_performance_fees_net_usd"),
|
|
368
368
|
)
|
|
369
369
|
.select_related("currency")
|
|
370
|
-
.prefetch_related("
|
|
370
|
+
.prefetch_related("fees", "prices")
|
|
371
371
|
)
|
|
372
372
|
return qs
|
|
373
373
|
|
|
@@ -8,8 +8,8 @@ from wbportfolio.serializers import (
|
|
|
8
8
|
RebalancingModelRepresentationSerializer,
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
from
|
|
12
|
-
from
|
|
11
|
+
from .configs.display import RebalancerDisplayConfig
|
|
12
|
+
from .configs.endpoints import RebalancerEndpointConfig
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class RebalancingModelRepresentationViewSet(InternalUserPermissionMixin, viewsets.RepresentationViewSet):
|
|
@@ -13,16 +13,11 @@ from .claim import (
|
|
|
13
13
|
ProfitAndLossPandasView,
|
|
14
14
|
)
|
|
15
15
|
from .fees import (
|
|
16
|
-
|
|
16
|
+
FeesAggregatedProductPandasView,
|
|
17
17
|
FeesModelViewSet,
|
|
18
|
-
|
|
19
|
-
)
|
|
20
|
-
from .rebalancing import RebalancingModelRepresentationViewSet, RebalancerRepresentationViewSet, RebalancerModelViewSet
|
|
21
|
-
from .trade_proposals import (
|
|
22
|
-
TradeProposalModelViewSet,
|
|
23
|
-
TradeProposalPortfolioModelViewSet,
|
|
24
|
-
TradeProposalRepresentationViewSet,
|
|
18
|
+
FeesProductModelViewSet,
|
|
25
19
|
)
|
|
20
|
+
|
|
26
21
|
from .trades import (
|
|
27
22
|
CustodianDistributionInstrumentChartViewSet,
|
|
28
23
|
CustomerDistributionInstrumentChartViewSet,
|
|
@@ -32,10 +27,4 @@ from .trades import (
|
|
|
32
27
|
TradeModelViewSet,
|
|
33
28
|
TradePortfolioModelViewSet,
|
|
34
29
|
TradeRepresentationViewSet,
|
|
35
|
-
TradeTradeProposalModelViewSet,
|
|
36
|
-
)
|
|
37
|
-
from .transactions import (
|
|
38
|
-
TransactionModelViewSet,
|
|
39
|
-
TransactionPortfolioModelViewSet,
|
|
40
|
-
TransactionRepresentationViewSet,
|
|
41
30
|
)
|
|
@@ -8,28 +8,28 @@ from wbcore.pandas import fields as pf
|
|
|
8
8
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
9
9
|
from wbcore.serializers import decorator
|
|
10
10
|
from wbcore.utils.strings import format_number
|
|
11
|
+
from wbcore.viewsets import ModelViewSet
|
|
11
12
|
|
|
12
|
-
from wbportfolio.filters import FeesAggregatedFilter, FeesFilter,
|
|
13
|
+
from wbportfolio.filters import FeesAggregatedFilter, FeesFilter, FeesProductFilterSet
|
|
13
14
|
from wbportfolio.models import Fees
|
|
14
15
|
from wbportfolio.serializers import FeesModelSerializer
|
|
15
16
|
|
|
16
17
|
from ..configs import (
|
|
17
18
|
FeeEndpointConfig,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
FeesAggregatedProductPandasDisplayConfig,
|
|
20
|
+
FeesAggregatedProductPandasEndpointConfig,
|
|
21
|
+
FeesAggregatedProductTitleConfig,
|
|
21
22
|
FeesButtonConfig,
|
|
22
23
|
FeesDisplayConfig,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
FeesProductDisplayConfig,
|
|
25
|
+
FeesProductEndpointConfig,
|
|
26
|
+
FeesProductTitleConfig,
|
|
26
27
|
FeesTitleConfig,
|
|
27
28
|
)
|
|
28
29
|
from ..mixins import UserPortfolioRequestPermissionMixin
|
|
29
|
-
from .transactions import TransactionModelViewSet
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class FeesModelViewSet(
|
|
32
|
+
class FeesModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, ModelViewSet):
|
|
33
33
|
filter_backends = (
|
|
34
34
|
DjangoFilterBackend,
|
|
35
35
|
filters.OrderingFilter,
|
|
@@ -56,27 +56,27 @@ class FeesModelViewSet(TransactionModelViewSet):
|
|
|
56
56
|
|
|
57
57
|
def get_queryset(self):
|
|
58
58
|
if self.is_manager:
|
|
59
|
-
return super().get_queryset().select_related("import_source", "
|
|
59
|
+
return super().get_queryset().select_related("import_source", "product")
|
|
60
60
|
return Fees.objects.none()
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
class
|
|
64
|
-
IDENTIFIER = "wbportfolio:
|
|
63
|
+
class FeesProductModelViewSet(FeesModelViewSet):
|
|
64
|
+
IDENTIFIER = "wbportfolio:product-fees"
|
|
65
65
|
|
|
66
|
-
filterset_class =
|
|
66
|
+
filterset_class = FeesProductFilterSet
|
|
67
67
|
|
|
68
|
-
display_config_class =
|
|
69
|
-
title_config_class =
|
|
70
|
-
endpoint_config_class =
|
|
68
|
+
display_config_class = FeesProductDisplayConfig
|
|
69
|
+
title_config_class = FeesProductTitleConfig
|
|
70
|
+
endpoint_config_class = FeesProductEndpointConfig
|
|
71
71
|
|
|
72
72
|
def get_queryset(self):
|
|
73
73
|
if self.is_portfolio_manager:
|
|
74
|
-
return super().get_queryset().filter(
|
|
74
|
+
return super().get_queryset().filter(product=self.product)
|
|
75
75
|
|
|
76
76
|
return Fees.objects.none()
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
class
|
|
79
|
+
class FeesAggregatedProductPandasView(
|
|
80
80
|
UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, ExportPandasAPIViewSet
|
|
81
81
|
):
|
|
82
82
|
IDENTIFIER = "wbportfolio:aggregetedfees"
|
|
@@ -85,9 +85,9 @@ class FeesAggregatedPortfolioPandasView(
|
|
|
85
85
|
|
|
86
86
|
queryset = Fees.valid_objects.all()
|
|
87
87
|
|
|
88
|
-
display_config_class =
|
|
89
|
-
title_config_class =
|
|
90
|
-
endpoint_config_class =
|
|
88
|
+
display_config_class = FeesAggregatedProductPandasDisplayConfig
|
|
89
|
+
title_config_class = FeesAggregatedProductTitleConfig
|
|
90
|
+
endpoint_config_class = FeesAggregatedProductPandasEndpointConfig
|
|
91
91
|
|
|
92
92
|
pandas_fields = pf.PandasFields(
|
|
93
93
|
fields=(
|
|
@@ -153,7 +153,7 @@ class FeesAggregatedPortfolioPandasView(
|
|
|
153
153
|
|
|
154
154
|
def get_queryset(self):
|
|
155
155
|
if self.is_portfolio_manager:
|
|
156
|
-
qs = super().get_queryset().filter(
|
|
156
|
+
qs = super().get_queryset().filter(product=self.product)
|
|
157
157
|
return qs.annotate(
|
|
158
158
|
currency_fx_rate_usd=CurrencyFXRates.get_fx_rates_subquery(
|
|
159
159
|
"fee_date", currency="currency", lookup_expr="exact"
|