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
|
@@ -49,7 +49,7 @@ class RebalancerModelSerializer(wb_serializers.ModelSerializer):
|
|
|
49
49
|
"computed_str",
|
|
50
50
|
"_rebalancing_model",
|
|
51
51
|
"rebalancing_model",
|
|
52
|
-
"
|
|
52
|
+
"apply_order_proposal_automatically",
|
|
53
53
|
"activation_date",
|
|
54
54
|
"frequency",
|
|
55
55
|
"frequency_repr",
|
|
@@ -84,7 +84,7 @@ def transactions_resources(sender, serializer, instance, request, user, **kwargs
|
|
|
84
84
|
additional_resources = dict()
|
|
85
85
|
portfolio = instance.primary_portfolio
|
|
86
86
|
if portfolio:
|
|
87
|
-
if portfolio.
|
|
87
|
+
if portfolio.trades.exists() and user.profile.is_internal:
|
|
88
88
|
additional_resources["portfolio_subscriptionsredemptions"] = (
|
|
89
89
|
f'{reverse("wbportfolio:portfolio-trade-list", args=[portfolio.id], request=request)}?is_customer_trade=True'
|
|
90
90
|
)
|
|
@@ -92,17 +92,6 @@ def transactions_resources(sender, serializer, instance, request, user, **kwargs
|
|
|
92
92
|
f'{reverse("wbportfolio:portfolio-trade-list", args=[portfolio.id], request=request)}?is_customer_trade=False'
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
-
additional_resources["portfolio_transactions"] = reverse(
|
|
96
|
-
"wbportfolio:portfolio-transaction-list", args=[portfolio.id], request=request
|
|
97
|
-
)
|
|
98
|
-
if PortfolioRole.is_portfolio_manager(user.profile, portfolio=portfolio):
|
|
99
|
-
additional_resources["portfolio_fees"] = reverse(
|
|
100
|
-
"wbportfolio:portfolio-fees-list", args=[portfolio.id], request=request
|
|
101
|
-
)
|
|
102
|
-
additional_resources["portfolio_aggregatedfees"] = reverse(
|
|
103
|
-
"wbportfolio:portfolio-feesaggregated-list", args=[portfolio.id], request=request
|
|
104
|
-
)
|
|
105
|
-
|
|
106
95
|
return additional_resources
|
|
107
96
|
|
|
108
97
|
|
|
@@ -130,6 +119,14 @@ def product_resources(sender, serializer, instance, request, user, **kwargs):
|
|
|
130
119
|
args=[instance.id],
|
|
131
120
|
request=request,
|
|
132
121
|
)
|
|
122
|
+
if instance.fees.exists() and PortfolioRole.is_portfolio_manager(user.profile, instrument=instance):
|
|
123
|
+
additional_resources["product_fees"] = reverse(
|
|
124
|
+
"wbportfolio:product-fees-list", args=[instance.id], request=request
|
|
125
|
+
)
|
|
126
|
+
additional_resources["product_aggregatedfees"] = reverse(
|
|
127
|
+
"wbportfolio:product-feesaggregated-list", args=[instance.id], request=request
|
|
128
|
+
)
|
|
129
|
+
|
|
133
130
|
return additional_resources
|
|
134
131
|
|
|
135
132
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from .trade_proposals import TradeProposalModelSerializer, ReadOnlyTradeProposalModelSerializer
|
|
2
1
|
from .claim import (
|
|
3
2
|
ClaimAccountSerializer,
|
|
4
3
|
ClaimAPIModelSerializer,
|
|
@@ -9,16 +8,8 @@ from .claim import (
|
|
|
9
8
|
NegativeTermimalAccountPerProductModelSerializer,
|
|
10
9
|
)
|
|
11
10
|
from .dividends import DividendModelSerializer, DividendRepresentationSerializer
|
|
12
|
-
from .expiry import ExpiryModelSerializer, ExpiryRepresentationSerializer
|
|
13
11
|
from .fees import FeesModelSerializer, FeesRepresentationSerializer
|
|
14
12
|
from .trades import (
|
|
15
13
|
TradeModelSerializer,
|
|
16
|
-
|
|
17
|
-
TradeRepresentationSerializer,
|
|
18
|
-
TradeTradeProposalModelSerializer,
|
|
19
|
-
ReadOnlyTradeTradeProposalModelSerializer,
|
|
20
|
-
)
|
|
21
|
-
from .transactions import (
|
|
22
|
-
TransactionModelSerializer,
|
|
23
|
-
TransactionRepresentationSerializer,
|
|
14
|
+
TradeRepresentationSerializer
|
|
24
15
|
)
|
|
@@ -49,8 +49,8 @@ class ClaimAPIModelSerializer(serializers.ModelSerializer):
|
|
|
49
49
|
if "isin" in request.data:
|
|
50
50
|
try:
|
|
51
51
|
data["product"] = Product.objects.get(isin=request.data["isin"])
|
|
52
|
-
except Product.DoesNotExist:
|
|
53
|
-
raise ValidationError({"isin": "A product with this ISIN does not exist."})
|
|
52
|
+
except Product.DoesNotExist as e:
|
|
53
|
+
raise ValidationError({"isin": "A product with this ISIN does not exist."}) from e
|
|
54
54
|
if trade := data.get("trade", None):
|
|
55
55
|
if not trade.is_claimable:
|
|
56
56
|
raise ValidationError({"trade": "Only a claimable trade can be selected"})
|
|
@@ -1,18 +1,46 @@
|
|
|
1
|
-
from
|
|
1
|
+
from wbcore import serializers as wb_serializers
|
|
2
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
3
|
+
from wbfdm.serializers import InvestableUniverseRepresentationSerializer
|
|
2
4
|
|
|
3
|
-
from .
|
|
4
|
-
|
|
5
|
-
TransactionRepresentationSerializer,
|
|
6
|
-
)
|
|
5
|
+
from wbportfolio.models import DividendTransaction
|
|
6
|
+
from wbportfolio.serializers import PortfolioRepresentationSerializer
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class DividendRepresentationSerializer(
|
|
9
|
+
class DividendRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
10
10
|
class Meta:
|
|
11
11
|
model = DividendTransaction
|
|
12
|
-
fields =
|
|
12
|
+
fields = ("id", "value_date")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DividendModelSerializer(wb_serializers.ModelSerializer):
|
|
16
|
+
_portfolio = PortfolioRepresentationSerializer(source="portfolio")
|
|
17
|
+
_underlying_instrument = InvestableUniverseRepresentationSerializer(source="underlying_instrument")
|
|
18
|
+
_currency = CurrencyRepresentationSerializer(source="currency")
|
|
13
19
|
|
|
20
|
+
total_value = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
|
|
21
|
+
total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
22
|
+
max_digits=14, decimal_places=2, read_only=True, required=False
|
|
23
|
+
)
|
|
24
|
+
total_value_gross = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
|
|
25
|
+
total_value_gross_fx_portfolio = wb_serializers.DecimalField(
|
|
26
|
+
max_digits=14, decimal_places=2, read_only=True, required=False
|
|
27
|
+
)
|
|
14
28
|
|
|
15
|
-
class DividendModelSerializer(TransactionModelSerializer):
|
|
16
29
|
class Meta:
|
|
17
30
|
model = DividendTransaction
|
|
18
|
-
fields =
|
|
31
|
+
fields = (
|
|
32
|
+
"id",
|
|
33
|
+
"value_date",
|
|
34
|
+
"record_date",
|
|
35
|
+
"ex_date",
|
|
36
|
+
"portfolio",
|
|
37
|
+
"underlying_instrument",
|
|
38
|
+
"currency",
|
|
39
|
+
"_portfolio",
|
|
40
|
+
"_underlying_instrument",
|
|
41
|
+
"_currency",
|
|
42
|
+
"total_value",
|
|
43
|
+
"total_value_fx_portfolio",
|
|
44
|
+
"total_value_gross",
|
|
45
|
+
"total_value_gross_fx_portfolio",
|
|
46
|
+
)
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
from wbcore import serializers as wb_serializers
|
|
2
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
2
3
|
|
|
3
4
|
from wbportfolio.models import Fees
|
|
5
|
+
from wbportfolio.serializers.products import ProductRepresentationSerializer
|
|
4
6
|
|
|
5
|
-
from .transactions import (
|
|
6
|
-
TransactionModelSerializer,
|
|
7
|
-
TransactionRepresentationSerializer,
|
|
8
|
-
)
|
|
9
7
|
|
|
8
|
+
class FeesRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
9
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:fees-detail")
|
|
10
10
|
|
|
11
|
-
class FeesRepresentationSerializer(TransactionRepresentationSerializer):
|
|
12
11
|
class Meta:
|
|
13
12
|
model = Fees
|
|
14
|
-
fields = ("transaction_subtype",
|
|
13
|
+
fields = ("transaction_subtype", "total_value", "_detail")
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
class FeesModelSerializer(
|
|
16
|
+
class FeesModelSerializer(wb_serializers.ModelSerializer):
|
|
17
|
+
_product = ProductRepresentationSerializer(source="product")
|
|
18
|
+
_currency = CurrencyRepresentationSerializer(source="currency")
|
|
19
|
+
total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
20
|
+
max_digits=14, decimal_places=2, read_only=True, required=False
|
|
21
|
+
)
|
|
22
|
+
total_value_gross_fx_portfolio = wb_serializers.DecimalField(
|
|
23
|
+
max_digits=14, decimal_places=2, read_only=True, required=False
|
|
24
|
+
)
|
|
25
|
+
|
|
18
26
|
@wb_serializers.register_resource()
|
|
19
27
|
def additional_resources(self, instance, request, user):
|
|
20
28
|
if (view := request.parser_context.get("view")) and view.is_manager and instance.import_source:
|
|
@@ -23,11 +31,32 @@ class FeesModelSerializer(TransactionModelSerializer):
|
|
|
23
31
|
|
|
24
32
|
class Meta:
|
|
25
33
|
model = Fees
|
|
26
|
-
decorators =
|
|
34
|
+
decorators = {
|
|
35
|
+
"total_value": wb_serializers.decorator(
|
|
36
|
+
decorator_type="text", position="left", value="{{_currency.symbol}}"
|
|
37
|
+
),
|
|
38
|
+
"total_value_fx_portfolio": wb_serializers.decorator(
|
|
39
|
+
position="left", value="{{_portfolio.currency_symbol}}"
|
|
40
|
+
),
|
|
41
|
+
"total_value_gross": wb_serializers.decorator(
|
|
42
|
+
decorator_type="text", position="left", value="{{_currency.symbol}}"
|
|
43
|
+
),
|
|
44
|
+
"total_value_gross_fx_portfolio": wb_serializers.decorator(
|
|
45
|
+
position="left", value="{{_portfolio.currency_symbol}}"
|
|
46
|
+
),
|
|
47
|
+
}
|
|
27
48
|
fields = (
|
|
49
|
+
"id",
|
|
28
50
|
"_additional_resources",
|
|
29
51
|
"transaction_subtype",
|
|
30
52
|
"calculated",
|
|
31
53
|
"fee_date",
|
|
32
|
-
"
|
|
33
|
-
|
|
54
|
+
"product",
|
|
55
|
+
"currency",
|
|
56
|
+
"_product",
|
|
57
|
+
"_currency",
|
|
58
|
+
"total_value",
|
|
59
|
+
"total_value_fx_portfolio",
|
|
60
|
+
"total_value_gross",
|
|
61
|
+
"total_value_gross_fx_portfolio",
|
|
62
|
+
)
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
from datetime import timedelta
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
|
|
4
|
-
from rest_framework import serializers
|
|
5
4
|
from rest_framework.reverse import reverse
|
|
6
5
|
from wbcore import serializers as wb_serializers
|
|
7
|
-
from wbcore.
|
|
8
|
-
from wbfdm.
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
7
|
+
from wbfdm.serializers.instruments.instruments import (
|
|
8
|
+
InvestableUniverseRepresentationSerializer,
|
|
9
|
+
)
|
|
11
10
|
|
|
12
|
-
from wbportfolio.models import PortfolioRole, Trade
|
|
11
|
+
from wbportfolio.models import PortfolioRole, Trade
|
|
13
12
|
from wbportfolio.models.transactions.claim import Claim
|
|
14
13
|
from wbportfolio.serializers.custodians import CustodianRepresentationSerializer
|
|
15
14
|
from wbportfolio.serializers.registers import RegisterRepresentationSerializer
|
|
16
15
|
|
|
17
|
-
from
|
|
18
|
-
TransactionModelSerializer,
|
|
19
|
-
TransactionRepresentationSerializer,
|
|
20
|
-
)
|
|
16
|
+
from .. import PortfolioRepresentationSerializer
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
class CopyClaimRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
@@ -39,21 +35,23 @@ class CopyClaimRepresentationSerializer(wb_serializers.RepresentationSerializer)
|
|
|
39
35
|
)
|
|
40
36
|
|
|
41
37
|
|
|
42
|
-
class
|
|
43
|
-
_detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:tradeproposal-detail")
|
|
44
|
-
|
|
45
|
-
class Meta:
|
|
46
|
-
model = TradeProposal
|
|
47
|
-
fields = ("id", "trade_date", "status", "_detail")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class TradeRepresentationSerializer(TransactionRepresentationSerializer):
|
|
38
|
+
class TradeRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
51
39
|
_detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:trade-detail")
|
|
52
|
-
diff_shares = wb_serializers.DecimalField(max_digits=15, decimal_places=4)
|
|
40
|
+
diff_shares = wb_serializers.DecimalField(max_digits=15, decimal_places=4, read_only=True)
|
|
41
|
+
total_value = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
|
|
53
42
|
|
|
54
43
|
class Meta:
|
|
55
44
|
model = Trade
|
|
56
|
-
fields = (
|
|
45
|
+
fields = (
|
|
46
|
+
"id",
|
|
47
|
+
"transaction_date",
|
|
48
|
+
"total_value",
|
|
49
|
+
"_detail",
|
|
50
|
+
"claimed_shares",
|
|
51
|
+
"diff_shares",
|
|
52
|
+
"bank",
|
|
53
|
+
"shares",
|
|
54
|
+
)
|
|
57
55
|
|
|
58
56
|
|
|
59
57
|
class TradeClaimRepresentationSerializer(TradeRepresentationSerializer):
|
|
@@ -78,7 +76,11 @@ class TradeClaimRepresentationSerializer(TradeRepresentationSerializer):
|
|
|
78
76
|
return {}
|
|
79
77
|
|
|
80
78
|
|
|
81
|
-
class TradeModelSerializer(
|
|
79
|
+
class TradeModelSerializer(wb_serializers.ModelSerializer):
|
|
80
|
+
_portfolio = PortfolioRepresentationSerializer(source="portfolio")
|
|
81
|
+
_underlying_instrument = InvestableUniverseRepresentationSerializer(source="underlying_instrument")
|
|
82
|
+
_currency = CurrencyRepresentationSerializer(source="currency")
|
|
83
|
+
|
|
82
84
|
marked_for_deletion = wb_serializers.BooleanField(required=False, read_only=True)
|
|
83
85
|
marked_as_internal = wb_serializers.BooleanField()
|
|
84
86
|
|
|
@@ -91,6 +93,19 @@ class TradeModelSerializer(TransactionModelSerializer):
|
|
|
91
93
|
completely_claimed = wb_serializers.BooleanField(required=False, read_only=True, default=Decimal(0.0))
|
|
92
94
|
completely_claimed_if_approved = wb_serializers.BooleanField(required=False, read_only=True, default=Decimal(0.0))
|
|
93
95
|
|
|
96
|
+
total_value = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
|
|
97
|
+
total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
98
|
+
max_digits=14, decimal_places=2, read_only=True, required=False
|
|
99
|
+
)
|
|
100
|
+
total_value_gross = wb_serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True, required=False)
|
|
101
|
+
total_value_gross_fx_portfolio = wb_serializers.DecimalField(
|
|
102
|
+
max_digits=14, decimal_places=2, read_only=True, required=False
|
|
103
|
+
)
|
|
104
|
+
external_id = wb_serializers.CharField(required=False, read_only=True)
|
|
105
|
+
value_date = wb_serializers.DateField(required=False, read_only=True)
|
|
106
|
+
total_value_usd = wb_serializers.FloatField(default=0, read_only=True, label="Total Value ($)")
|
|
107
|
+
total_value_gross_usd = wb_serializers.FloatField(default=0, read_only=True, label="Total Value Gross ($)")
|
|
108
|
+
|
|
94
109
|
# We commented it because people needs to filter with lookup equals onto these fields
|
|
95
110
|
# shares = wb_serializers.DecimalField(max_digits=16, decimal_places=2)
|
|
96
111
|
# price = wb_serializers.DecimalField(max_digits=16, decimal_places=2)
|
|
@@ -127,7 +142,24 @@ class TradeModelSerializer(TransactionModelSerializer):
|
|
|
127
142
|
|
|
128
143
|
class Meta:
|
|
129
144
|
model = Trade
|
|
130
|
-
decorators =
|
|
145
|
+
decorators = {
|
|
146
|
+
"total_value": wb_serializers.decorator(
|
|
147
|
+
decorator_type="text", position="left", value="{{_currency.symbol}}"
|
|
148
|
+
),
|
|
149
|
+
"total_value_gross": wb_serializers.decorator(
|
|
150
|
+
decorator_type="text", position="left", value="{{_currency.symbol}}"
|
|
151
|
+
),
|
|
152
|
+
"total_value_usd": wb_serializers.decorator(decorator_type="text", position="left", value="{{$}}"),
|
|
153
|
+
"total_value_gross_usd": wb_serializers.decorator(decorator_type="text", position="left", value="{{$}}"),
|
|
154
|
+
"total_value_fx_portfolio": wb_serializers.decorator(
|
|
155
|
+
position="left", value="{{_portfolio.currency_symbol}}"
|
|
156
|
+
),
|
|
157
|
+
"total_value_gross_fx_portfolio": wb_serializers.decorator(
|
|
158
|
+
position="left", value="{{_portfolio.currency_symbol}}"
|
|
159
|
+
),
|
|
160
|
+
# "total_value_fx_portfolio": wb_serializers.decorator(decorator_type="text", position="left", value="{{_portfolio.currency_symbol}}}"),
|
|
161
|
+
# "total_value_gross_fx_portfolio": wb_serializers.decorator(decorator_type="text", position="left", value="{{_portfolio.currency_symbol}}"),
|
|
162
|
+
}
|
|
131
163
|
read_only_fields = (
|
|
132
164
|
"id",
|
|
133
165
|
"shares",
|
|
@@ -145,9 +177,6 @@ class TradeModelSerializer(TransactionModelSerializer):
|
|
|
145
177
|
"marked_for_deletion",
|
|
146
178
|
"_claims",
|
|
147
179
|
"claims",
|
|
148
|
-
"transaction_type",
|
|
149
|
-
"transaction_url_type",
|
|
150
|
-
"transaction_underlying_type",
|
|
151
180
|
"portfolio",
|
|
152
181
|
"_portfolio",
|
|
153
182
|
"underlying_instrument",
|
|
@@ -185,9 +214,6 @@ class TradeModelSerializer(TransactionModelSerializer):
|
|
|
185
214
|
"marked_for_deletion",
|
|
186
215
|
"_claims",
|
|
187
216
|
"claims",
|
|
188
|
-
"transaction_type",
|
|
189
|
-
"transaction_url_type",
|
|
190
|
-
"transaction_underlying_type",
|
|
191
217
|
"portfolio",
|
|
192
218
|
"_portfolio",
|
|
193
219
|
"underlying_instrument",
|
|
@@ -213,131 +239,3 @@ class TradeModelSerializer(TransactionModelSerializer):
|
|
|
213
239
|
"_internal_trade",
|
|
214
240
|
"_additional_resources",
|
|
215
241
|
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
class GetSecurityDefault:
|
|
219
|
-
requires_context = True
|
|
220
|
-
|
|
221
|
-
def __call__(self, serializer_instance):
|
|
222
|
-
try:
|
|
223
|
-
instance = serializer_instance.view.get_object()
|
|
224
|
-
return instance.underlying_instrument.parent or instance.underlying_instrument
|
|
225
|
-
except Exception:
|
|
226
|
-
return None
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
class GetCompanyDefault:
|
|
230
|
-
requires_context = True
|
|
231
|
-
|
|
232
|
-
def __call__(self, serializer_instance):
|
|
233
|
-
try:
|
|
234
|
-
instance = serializer_instance.view.get_object()
|
|
235
|
-
security = instance.underlying_instrument.parent or instance.underlying_instrument
|
|
236
|
-
return security.parent or security
|
|
237
|
-
except Exception:
|
|
238
|
-
return None
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
class TradeTradeProposalModelSerializer(TradeModelSerializer):
|
|
242
|
-
underlying_instrument_isin = wb_serializers.CharField(read_only=True)
|
|
243
|
-
underlying_instrument_ticker = wb_serializers.CharField(read_only=True)
|
|
244
|
-
underlying_instrument_refinitiv_identifier_code = wb_serializers.CharField(read_only=True)
|
|
245
|
-
underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
|
|
246
|
-
|
|
247
|
-
company = wb_serializers.PrimaryKeyRelatedField(
|
|
248
|
-
queryset=Instrument.objects.filter(level=0),
|
|
249
|
-
required=False,
|
|
250
|
-
read_only=lambda view: not view.new_mode,
|
|
251
|
-
default=GetCompanyDefault(),
|
|
252
|
-
)
|
|
253
|
-
_company = CompanyRepresentationSerializer(source="company", required=False)
|
|
254
|
-
|
|
255
|
-
security = wb_serializers.PrimaryKeyRelatedField(
|
|
256
|
-
queryset=Instrument.objects.filter(is_security=True),
|
|
257
|
-
required=False,
|
|
258
|
-
read_only=lambda view: not view.new_mode,
|
|
259
|
-
default=GetSecurityDefault(),
|
|
260
|
-
)
|
|
261
|
-
_security = SecurityRepresentationSerializer(
|
|
262
|
-
source="security",
|
|
263
|
-
optional_get_parameters={"company": "parent"},
|
|
264
|
-
depends_on=[{"field": "company", "options": {}}],
|
|
265
|
-
required=False,
|
|
266
|
-
)
|
|
267
|
-
underlying_instrument = wb_serializers.PrimaryKeyRelatedField(
|
|
268
|
-
queryset=Instrument.objects.all(), label="Quote", read_only=lambda view: not view.new_mode
|
|
269
|
-
)
|
|
270
|
-
_underlying_instrument = InvestableInstrumentRepresentationSerializer(
|
|
271
|
-
source="underlying_instrument",
|
|
272
|
-
optional_get_parameters={"security": "parent"},
|
|
273
|
-
depends_on=[{"field": "security", "options": {}}],
|
|
274
|
-
tree_config=BaseTreeGroupLevelOption(clear_filter=True, filter_key="parent"),
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
status = wb_serializers.ChoiceField(default=Trade.Status.DRAFT, choices=Trade.Status.choices)
|
|
278
|
-
target_weight = wb_serializers.DecimalField(max_digits=16, decimal_places=6, required=False, default=0)
|
|
279
|
-
effective_weight = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
280
|
-
effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
281
|
-
target_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
282
|
-
|
|
283
|
-
def validate(self, data):
|
|
284
|
-
data.pop("company", None)
|
|
285
|
-
data.pop("security", None)
|
|
286
|
-
if self.instance and "underlying_instrument" in data:
|
|
287
|
-
raise serializers.ValidationError(
|
|
288
|
-
{
|
|
289
|
-
"underlying_instrument": "You cannot modify the underlying instrument other than creating a new entry"
|
|
290
|
-
}
|
|
291
|
-
)
|
|
292
|
-
effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
|
|
293
|
-
weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
|
|
294
|
-
if (target_weight := data.pop("target_weight", None)) is not None:
|
|
295
|
-
weighting = target_weight - effective_weight
|
|
296
|
-
if weighting >= 0:
|
|
297
|
-
data["transaction_subtype"] = "BUY"
|
|
298
|
-
else:
|
|
299
|
-
data["transaction_subtype"] = "SELL"
|
|
300
|
-
data["weighting"] = weighting
|
|
301
|
-
return super().validate(data)
|
|
302
|
-
|
|
303
|
-
class Meta:
|
|
304
|
-
model = Trade
|
|
305
|
-
percent_fields = ["effective_weight", "target_weight", "weighting"]
|
|
306
|
-
read_only_fields = (
|
|
307
|
-
"transaction_subtype",
|
|
308
|
-
"shares",
|
|
309
|
-
# "underlying_instrument",
|
|
310
|
-
# "_underlying_instrument",
|
|
311
|
-
"shares",
|
|
312
|
-
"effective_shares",
|
|
313
|
-
"target_shares",
|
|
314
|
-
)
|
|
315
|
-
fields = (
|
|
316
|
-
"id",
|
|
317
|
-
"shares",
|
|
318
|
-
"underlying_instrument_isin",
|
|
319
|
-
"underlying_instrument_ticker",
|
|
320
|
-
"underlying_instrument_refinitiv_identifier_code",
|
|
321
|
-
"underlying_instrument_instrument_type",
|
|
322
|
-
"company",
|
|
323
|
-
"_company",
|
|
324
|
-
"security",
|
|
325
|
-
"_security",
|
|
326
|
-
"underlying_instrument",
|
|
327
|
-
"_underlying_instrument",
|
|
328
|
-
"transaction_subtype",
|
|
329
|
-
"status",
|
|
330
|
-
"comment",
|
|
331
|
-
"effective_weight",
|
|
332
|
-
"target_weight",
|
|
333
|
-
"weighting",
|
|
334
|
-
"trade_proposal",
|
|
335
|
-
"order",
|
|
336
|
-
"effective_shares",
|
|
337
|
-
"target_shares",
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
class ReadOnlyTradeTradeProposalModelSerializer(TradeTradeProposalModelSerializer):
|
|
342
|
-
class Meta(TradeTradeProposalModelSerializer.Meta):
|
|
343
|
-
read_only_fields = TradeTradeProposalModelSerializer.Meta.fields
|
wbportfolio/tasks.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from contextlib import suppress
|
|
2
2
|
from datetime import date, timedelta
|
|
3
3
|
|
|
4
|
+
from celery import shared_task
|
|
4
5
|
from django.db.models import ProtectedError, Q
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
from wbfdm.models import Controversy, Instrument
|
|
5
8
|
|
|
6
|
-
from wbportfolio.models import Portfolio, Trade
|
|
7
|
-
from wbportfolio.models.products import Product, update_outstanding_shares_as_task
|
|
8
|
-
|
|
9
|
-
from .fdm.tasks import * # noqa
|
|
9
|
+
from wbportfolio.models import AssetPosition, Portfolio, Product, Trade
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@shared_task(queue="portfolio")
|
|
@@ -15,7 +15,7 @@ def daily_active_product_task(today: date | None = None):
|
|
|
15
15
|
today = date.today()
|
|
16
16
|
qs = Product.active_objects.all()
|
|
17
17
|
for product in tqdm(qs, total=qs.count()):
|
|
18
|
-
|
|
18
|
+
product.update_outstanding_shares()
|
|
19
19
|
product.check_and_notify_product_termination_on_date(today)
|
|
20
20
|
|
|
21
21
|
|
|
@@ -49,3 +49,41 @@ def update_preferred_classification_per_instrument_and_portfolio_as_task():
|
|
|
49
49
|
# - propagate (or update) t-2 asset positions into t-1
|
|
50
50
|
# - Synchronize wbportfolio at t-1
|
|
51
51
|
# - Compute Instrument Price estimate at t-1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@shared_task(queue="portfolio")
|
|
55
|
+
def synchronize_portfolio_controversies():
|
|
56
|
+
active_portfolios = Portfolio.objects.filter_active_and_tracked()
|
|
57
|
+
qs = (
|
|
58
|
+
AssetPosition.objects.filter(portfolio__in=active_portfolios)
|
|
59
|
+
.values("underlying_instrument")
|
|
60
|
+
.distinct("underlying_instrument")
|
|
61
|
+
)
|
|
62
|
+
objs = {}
|
|
63
|
+
securities = Instrument.objects.filter(id__in=qs.values("underlying_instrument"))
|
|
64
|
+
securities_mapping = {security.id: security.get_root() for security in securities}
|
|
65
|
+
for controversy in securities.dl.esg_controversies():
|
|
66
|
+
instrument = securities_mapping[controversy["instrument_id"]]
|
|
67
|
+
obj = Controversy.dict_to_model(controversy, instrument)
|
|
68
|
+
objs[obj.external_id] = obj
|
|
69
|
+
|
|
70
|
+
Controversy.objects.bulk_create(
|
|
71
|
+
objs.values(),
|
|
72
|
+
update_fields=[
|
|
73
|
+
"instrument",
|
|
74
|
+
"headline",
|
|
75
|
+
"description",
|
|
76
|
+
"source",
|
|
77
|
+
"direct_involvement",
|
|
78
|
+
"company_response",
|
|
79
|
+
"review",
|
|
80
|
+
"initiated",
|
|
81
|
+
"flag",
|
|
82
|
+
"status",
|
|
83
|
+
"type",
|
|
84
|
+
"severity",
|
|
85
|
+
],
|
|
86
|
+
unique_fields=["external_id"],
|
|
87
|
+
update_conflicts=True,
|
|
88
|
+
batch_size=10000,
|
|
89
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pandas._libs.tslibs.offsets import BDay
|
|
5
|
+
|
|
6
|
+
from wbportfolio.analysis.claims import ConsolidatedTradeSummary
|
|
7
|
+
from wbportfolio.models import Claim
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.django_db
|
|
11
|
+
class TestConsolidatedTradeSummary:
|
|
12
|
+
def test_base(self, claim_factory, customer_trade_factory, product, instrument_price_factory):
|
|
13
|
+
d1 = date(2024, 1, 1)
|
|
14
|
+
d2 = date(2025, 1, 1)
|
|
15
|
+
p1 = instrument_price_factory.create(date=d1, instrument=product) # noqa
|
|
16
|
+
p2 = instrument_price_factory.create(date=d2, instrument=product)
|
|
17
|
+
t1 = customer_trade_factory.create(
|
|
18
|
+
shares=1000, transaction_date=d1, underlying_instrument=product, portfolio=product.primary_portfolio
|
|
19
|
+
)
|
|
20
|
+
t2 = customer_trade_factory.create(
|
|
21
|
+
shares=500, transaction_date=d2, underlying_instrument=product, portfolio=product.primary_portfolio
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
c0 = claim_factory.create() # noqa
|
|
25
|
+
c1 = claim_factory.create(shares=1000, status="APPROVED", trade=t1, date=d2, product=product)
|
|
26
|
+
c2 = claim_factory.create(shares=500, status="APPROVED", trade=t2, date=d1, product=product)
|
|
27
|
+
cts = ConsolidatedTradeSummary(Claim.objects.all(), d1, d2, "account", "account__title")
|
|
28
|
+
|
|
29
|
+
valid_date = dict(cts.queryset.values_list("id", "valid_date"))
|
|
30
|
+
assert valid_date == {c1.id: d2, c2.id: d2}
|
|
31
|
+
|
|
32
|
+
date_considered = dict(cts.queryset.values_list("id", "date_considered"))
|
|
33
|
+
assert date_considered == {c1.id: d2 + timedelta(days=1), c2.id: d2 + timedelta(days=1)}
|
|
34
|
+
|
|
35
|
+
aum = dict(cts.queryset.values_list("id", "aum"))
|
|
36
|
+
assert aum == {c1.id: p2.net_value * c1.shares, c2.id: p2.net_value * c2.shares}
|
|
37
|
+
|
|
38
|
+
def test_get_aum_df(self, claim_factory, product, instrument_price_factory, customer_trade_factory):
|
|
39
|
+
t1 = customer_trade_factory.create(
|
|
40
|
+
shares=10,
|
|
41
|
+
transaction_date=date(2024, 12, 31),
|
|
42
|
+
underlying_instrument=product,
|
|
43
|
+
portfolio=product.primary_portfolio,
|
|
44
|
+
)
|
|
45
|
+
t2 = customer_trade_factory.create(
|
|
46
|
+
shares=100,
|
|
47
|
+
transaction_date=date(2025, 2, 3),
|
|
48
|
+
underlying_instrument=product,
|
|
49
|
+
portfolio=product.primary_portfolio,
|
|
50
|
+
)
|
|
51
|
+
c1 = claim_factory.create(trade=t1, status="APPROVED")
|
|
52
|
+
c2 = claim_factory.create(trade=t2, status="APPROVED", account=c1.account)
|
|
53
|
+
d1 = date(2025, 1, 1)
|
|
54
|
+
d2 = date(2025, 3, 3)
|
|
55
|
+
product.prices.all().delete()
|
|
56
|
+
p1 = instrument_price_factory.create(
|
|
57
|
+
date=(d1 - BDay(7)).date(), instrument=product
|
|
58
|
+
) # we test that the CTS uses the earliest date
|
|
59
|
+
p2 = instrument_price_factory.create(date=c2.date, instrument=product) # noqa
|
|
60
|
+
p3 = instrument_price_factory.create(date=d2, instrument=product)
|
|
61
|
+
|
|
62
|
+
cts = ConsolidatedTradeSummary(Claim.objects.all(), d1, d2, "account", "account__title")
|
|
63
|
+
aum_start = float(round(p1.net_value * c1.shares))
|
|
64
|
+
aum_end = float(round(p3.net_value * (c1.shares + c2.shares)))
|
|
65
|
+
assert cts.get_aum_df().to_dict(orient="index") == {
|
|
66
|
+
c1.account.id: {
|
|
67
|
+
"sum_shares_start": c1.shares,
|
|
68
|
+
"sum_shares_end": c1.shares + c2.shares,
|
|
69
|
+
"sum_aum_start": aum_start,
|
|
70
|
+
"sum_aum_end": aum_end,
|
|
71
|
+
"sum_shares_diff": c2.shares,
|
|
72
|
+
"sum_shares_perf": (c1.shares + c2.shares) / c1.shares - 1,
|
|
73
|
+
"sum_aum_diff": aum_end - aum_start,
|
|
74
|
+
"sum_aum_perf": aum_end / aum_start - 1,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
p0 = instrument_price_factory.create(date=c1.date, instrument=product) # noqa
|
|
79
|
+
|
|
80
|
+
assert cts.get_nnm_df().to_dict(orient="index") == {
|
|
81
|
+
c1.account.id: {
|
|
82
|
+
"sum_nnm_2025-02": float(round(c2.shares * p2.net_value)),
|
|
83
|
+
"sum_nnm_total": float(round(c2.shares * p2.net_value)),
|
|
84
|
+
}
|
|
85
|
+
}, "Ensure the first claim that is considered in t+1 (on the CTS lower bound range) is excluded from the NNM"
|