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,64 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
from wbcompliance.models.risk_management import backend
|
|
4
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
5
|
+
from wbcore import serializers as wb_serializers
|
|
6
|
+
from wbfdm.analysis.esg.enums import ESGAggregation
|
|
7
|
+
from wbfdm.analysis.esg.esg_analysis import DataLoader
|
|
8
|
+
from wbfdm.models import Instrument
|
|
9
|
+
|
|
10
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
11
|
+
|
|
12
|
+
from .mixins import ActivePortfolioRelationshipMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register("ESG Aggregation Portfolio Rule Backend", rule_group_key="portfolio")
|
|
16
|
+
class RuleBackend(ActivePortfolioRelationshipMixin):
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.esg_aggregation = ESGAggregation[self.esg_aggregation]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
23
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
24
|
+
esg_aggregation = wb_serializers.ChoiceField(
|
|
25
|
+
choices=ESGAggregation.choices(),
|
|
26
|
+
default=ESGAggregation.GHG_EMISSIONS_SCOPE_1.name,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_parameter_fields(cls):
|
|
31
|
+
return [
|
|
32
|
+
"esg_aggregation",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
return RuleBackendSerializer
|
|
36
|
+
|
|
37
|
+
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
38
|
+
esg_data = self.esg_aggregation.get_esg_data(Instrument.objects.filter(id__in=portfolio.positions_map.keys()))
|
|
39
|
+
df = portfolio.to_df(exclude_cash=True)
|
|
40
|
+
df["total_value"] = (df["price"] * df["shares"] * df["currency_fx_rate"]).astype(float)
|
|
41
|
+
df = df[["total_value", "weighting", "underlying_instrument"]].set_index("underlying_instrument")
|
|
42
|
+
df["weighting"] = df["weighting"] / df["weighting"].sum()
|
|
43
|
+
dataloader = DataLoader(
|
|
44
|
+
df["weighting"].astype(float), esg_data, self.evaluation_date, total_value_fx_usd=df["total_value"]
|
|
45
|
+
)
|
|
46
|
+
metrics = dataloader.compute(self.esg_aggregation)
|
|
47
|
+
for threshold in self.thresholds:
|
|
48
|
+
numerical_range = threshold.numerical_range
|
|
49
|
+
incident_df = metrics[(metrics >= numerical_range[0]) & (metrics < numerical_range[1])]
|
|
50
|
+
for instrument_id, metric in incident_df.to_dict().items():
|
|
51
|
+
instrument = Instrument.objects.get(id=instrument_id)
|
|
52
|
+
breached_value = metric
|
|
53
|
+
|
|
54
|
+
if metric < 0:
|
|
55
|
+
breached_value = f'<span style="color:red">{breached_value}</span>'
|
|
56
|
+
else:
|
|
57
|
+
breached_value = f'<span style="color:green">{breached_value}</span>'
|
|
58
|
+
yield backend.IncidentResult(
|
|
59
|
+
breached_object=instrument,
|
|
60
|
+
breached_object_repr=str(instrument),
|
|
61
|
+
breached_value=breached_value,
|
|
62
|
+
report_details={"Aggregation": self.esg_aggregation.value},
|
|
63
|
+
severity=threshold.severity,
|
|
64
|
+
)
|
|
@@ -3,7 +3,7 @@ from typing import Generator
|
|
|
3
3
|
from django.db import models
|
|
4
4
|
from wbcompliance.models.risk_management import backend
|
|
5
5
|
from wbcompliance.models.risk_management.dispatch import register
|
|
6
|
-
from wbcompliance.models.risk_management.
|
|
6
|
+
from wbcompliance.models.risk_management.incidents import RiskIncidentType
|
|
7
7
|
from wbcore import serializers as wb_serializers
|
|
8
8
|
from wbcore.contrib.currency.models import Currency
|
|
9
9
|
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
@@ -160,7 +160,7 @@ class RuleBackend(
|
|
|
160
160
|
|
|
161
161
|
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
162
162
|
if not (df := self._filter_df(portfolio.to_df())).empty:
|
|
163
|
-
df = df[[self.group_by.value, self.field.value]].groupby(self.group_by.value).sum().astype(float)
|
|
163
|
+
df = df[[self.group_by.value, self.field.value]].dropna().groupby(self.group_by.value).sum().astype(float)
|
|
164
164
|
for threshold in self.thresholds:
|
|
165
165
|
numerical_range = threshold.numerical_range
|
|
166
166
|
incident_df = df[
|
|
@@ -180,7 +180,7 @@ class RuleBackend(
|
|
|
180
180
|
breached_value = f'<span style="color:green">{breached_value}</span>'
|
|
181
181
|
yield backend.IncidentResult(
|
|
182
182
|
breached_object=obj,
|
|
183
|
-
breached_object_repr=obj_repr,
|
|
183
|
+
breached_object_repr=str(obj_repr),
|
|
184
184
|
breached_value=breached_value,
|
|
185
185
|
report_details=self.report_details,
|
|
186
186
|
severity=severity,
|
|
@@ -218,7 +218,7 @@ class RuleBackend(
|
|
|
218
218
|
obj = Instrument.objects.get(id=pivot_object_id)
|
|
219
219
|
return obj, str(obj)
|
|
220
220
|
case self.GroupbyChoices.ASSET_TYPE:
|
|
221
|
-
return None, InstrumentType.objects.get(id=pivot_object_id)
|
|
221
|
+
return None, InstrumentType.objects.get(id=pivot_object_id).name
|
|
222
222
|
case self.GroupbyChoices.CASH:
|
|
223
223
|
return None, "Cash"
|
|
224
224
|
case self.GroupbyChoices.COUNTRY:
|
|
@@ -64,10 +64,10 @@ class RuleBackend(ActivePortfolioRelationshipMixin):
|
|
|
64
64
|
return RuleBackendSerializer
|
|
65
65
|
|
|
66
66
|
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
67
|
-
for instrument_id
|
|
67
|
+
for instrument_id in portfolio.positions_map.keys():
|
|
68
68
|
instrument = Instrument.objects.get(id=instrument_id)
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
ancestors = instrument.get_ancestors(include_self=True)
|
|
70
|
+
relationships = self.instruments_relationship.filter(instrument__in=ancestors, validated=True)
|
|
71
71
|
if self.exclude and relationships.exists():
|
|
72
72
|
report_details = {
|
|
73
73
|
"Instrument Lists": ", ".join(relationships.values_list("instrument_list__name", flat=True)),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
from faker import Faker
|
|
6
|
+
from psycopg.types.range import NumericRange
|
|
7
|
+
from wbcompliance.factories.risk_management import RuleThresholdFactory
|
|
8
|
+
from wbfdm.analysis.esg.esg_analysis import DataLoader
|
|
9
|
+
|
|
10
|
+
from wbportfolio.risk_management.backends.esg_aggregation_portfolio import (
|
|
11
|
+
RuleBackend as ESGAggregationPortfolioBackend,
|
|
12
|
+
)
|
|
13
|
+
from wbportfolio.tests.models.utils import PortfolioTestMixin
|
|
14
|
+
|
|
15
|
+
fake = Faker()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.django_db
|
|
19
|
+
class TestEsgAggregationPortfolioRuleModel(PortfolioTestMixin):
|
|
20
|
+
@patch.object(DataLoader, "compute")
|
|
21
|
+
def test_eval(
|
|
22
|
+
self,
|
|
23
|
+
mock_fct,
|
|
24
|
+
weekday,
|
|
25
|
+
asset_position_factory,
|
|
26
|
+
portfolio,
|
|
27
|
+
):
|
|
28
|
+
parameters = {"esg_aggregation": "GHG_EMISSIONS_SCOPE_1"}
|
|
29
|
+
backend = ESGAggregationPortfolioBackend(
|
|
30
|
+
weekday,
|
|
31
|
+
portfolio,
|
|
32
|
+
parameters,
|
|
33
|
+
[RuleThresholdFactory.create(range=NumericRange(lower=0.02, upper=0.03))], # type: ignore
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
a1 = asset_position_factory.create(
|
|
37
|
+
date=weekday,
|
|
38
|
+
portfolio=portfolio,
|
|
39
|
+
) # Breached position
|
|
40
|
+
|
|
41
|
+
a2 = asset_position_factory.create(
|
|
42
|
+
date=weekday,
|
|
43
|
+
portfolio=portfolio,
|
|
44
|
+
)
|
|
45
|
+
mock_fct.return_value = pd.Series(index=[a1.underlying_quote.id, a2.underlying_quote.id], data=[0.01, 0.025])
|
|
46
|
+
incidents = list(backend.check_rule())
|
|
47
|
+
assert len(incidents) == 1
|
|
48
|
+
incident = incidents[0]
|
|
49
|
+
assert incident.breached_object == a2.underlying_quote
|
|
@@ -138,5 +138,5 @@ class TestExposurePortfolioRuleModel(PortfolioTestMixin):
|
|
|
138
138
|
incidents = list(exposure_portfolio_backend.check_rule())
|
|
139
139
|
assert len(incidents) == 1
|
|
140
140
|
incident = incidents[0]
|
|
141
|
-
assert incident.breached_object_repr == i1.instrument_type
|
|
141
|
+
assert incident.breached_object_repr == i1.instrument_type.name
|
|
142
142
|
assert incident.breached_value == '<span style="color:green">+5.00%</span>'
|
|
@@ -92,7 +92,7 @@ class TestStopLossInstrumentRuleModel(PortfolioTestMixin):
|
|
|
92
92
|
date=weekday, net_value=500, calculated=False, instrument=benchmark
|
|
93
93
|
)
|
|
94
94
|
RelatedInstrumentThroughModel.objects.create(instrument=product, related_instrument=benchmark, is_primary=True)
|
|
95
|
-
|
|
95
|
+
stop_loss_instrument_backend.dynamic_benchmark_type = "PRIMARY_BENCHMARK"
|
|
96
96
|
|
|
97
97
|
res = list(stop_loss_instrument_backend.check_rule())
|
|
98
98
|
assert len(res) == 0
|
|
@@ -105,7 +105,7 @@ class TestStopLossInstrumentRuleModel(PortfolioTestMixin):
|
|
|
105
105
|
assert len(res) == 1
|
|
106
106
|
assert res[0].breached_object.id == product.id
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
stop_loss_instrument_backend.static_benchmark = benchmark
|
|
109
109
|
res = list(stop_loss_instrument_backend.check_rule())
|
|
110
110
|
assert len(res) == 1
|
|
111
111
|
assert res[0].breached_object.id == product.id
|
|
@@ -119,7 +119,7 @@ class TestStopLossPortfolioRuleModel(PortfolioTestMixin):
|
|
|
119
119
|
res = list(stop_loss_portfolio_backend.check_rule())
|
|
120
120
|
assert len(res) == 0
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
stop_loss_portfolio_backend.static_benchmark = benchmark
|
|
123
123
|
res = list(stop_loss_portfolio_backend.check_rule())
|
|
124
124
|
assert len(res) == 1
|
|
125
125
|
assert res[0].breached_object.id == instrument.id
|
|
@@ -27,6 +27,7 @@ from .registers import RegisterModelSerializer, RegisterRepresentationSerializer
|
|
|
27
27
|
from .roles import PortfolioRoleModelSerializer, PortfolioRoleProjectModelSerializer
|
|
28
28
|
from .signals import *
|
|
29
29
|
from .transactions import *
|
|
30
|
+
from .orders import *
|
|
30
31
|
from .products import (
|
|
31
32
|
ProductRepresentationSerializer,
|
|
32
33
|
ProductCustomerRepresentationSerializer,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from django.contrib.messages import warning
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
3
|
+
from rest_framework.reverse import reverse
|
|
4
|
+
from wbcore import serializers as wb_serializers
|
|
5
|
+
from wbcore.contrib.directory.serializers import PersonRepresentationSerializer
|
|
6
|
+
from wbcore.serializers import CharField, DefaultFromView
|
|
7
|
+
|
|
8
|
+
from wbportfolio.models import OrderProposal, Portfolio, RebalancingModel
|
|
9
|
+
|
|
10
|
+
from .. import PortfolioRepresentationSerializer, RebalancingModelRepresentationSerializer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OrderProposalRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
14
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:orderproposal-detail")
|
|
15
|
+
|
|
16
|
+
class Meta:
|
|
17
|
+
model = OrderProposal
|
|
18
|
+
fields = ("id", "trade_date", "status", "_detail")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
22
|
+
_portfolio = PortfolioRepresentationSerializer(source="portfolio")
|
|
23
|
+
rebalancing_model = wb_serializers.PrimaryKeyRelatedField(queryset=RebalancingModel.objects.all(), required=False)
|
|
24
|
+
_rebalancing_model = RebalancingModelRepresentationSerializer(source="rebalancing_model")
|
|
25
|
+
target_portfolio = wb_serializers.PrimaryKeyRelatedField(
|
|
26
|
+
queryset=Portfolio.objects.all(), write_only=True, required=False
|
|
27
|
+
)
|
|
28
|
+
_target_portfolio = PortfolioRepresentationSerializer(source="target_portfolio")
|
|
29
|
+
trade_date = wb_serializers.DateField(
|
|
30
|
+
read_only=lambda view: not view.new_mode, default=DefaultFromView("default_trade_date")
|
|
31
|
+
)
|
|
32
|
+
_creator = PersonRepresentationSerializer(source="creator")
|
|
33
|
+
_approver = PersonRepresentationSerializer(source="approver")
|
|
34
|
+
execution_status_repr = wb_serializers.SerializerMethodField(label="Status", field_class=CharField, read_only=True)
|
|
35
|
+
|
|
36
|
+
def get_execution_status_repr(self, obj):
|
|
37
|
+
repr = obj.execution_status
|
|
38
|
+
if obj.execution_status_detail:
|
|
39
|
+
repr += f" (Custodian: {obj.execution_status_detail})"
|
|
40
|
+
return repr
|
|
41
|
+
|
|
42
|
+
def create(self, validated_data):
|
|
43
|
+
target_portfolio = validated_data.pop("target_portfolio", None)
|
|
44
|
+
rebalancing_model = validated_data.get("rebalancing_model", None)
|
|
45
|
+
if request := self.context.get("request"):
|
|
46
|
+
validated_data["creator"] = request.user.profile
|
|
47
|
+
obj = super().create(validated_data)
|
|
48
|
+
|
|
49
|
+
target_portfolio_dto = None
|
|
50
|
+
if target_portfolio:
|
|
51
|
+
target_portfolio_dto = target_portfolio._build_dto(obj.trade_date)
|
|
52
|
+
elif rebalancing_model:
|
|
53
|
+
target_portfolio_dto = rebalancing_model.get_target_portfolio(
|
|
54
|
+
obj.portfolio, obj.trade_date, obj.last_effective_date
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
obj.reset_orders(target_portfolio=target_portfolio_dto)
|
|
59
|
+
except ValidationError as e:
|
|
60
|
+
if request := self.context.get("request"):
|
|
61
|
+
warning(request, str(e), extra_tags="auto_close=0")
|
|
62
|
+
return obj
|
|
63
|
+
|
|
64
|
+
@wb_serializers.register_only_instance_resource()
|
|
65
|
+
def additional_resources(self, instance, request, user, **kwargs):
|
|
66
|
+
res = {}
|
|
67
|
+
if instance.status == OrderProposal.Status.CONFIRMED:
|
|
68
|
+
res["replay"] = reverse("wbportfolio:orderproposal-replay", args=[instance.id], request=request)
|
|
69
|
+
if instance.status == OrderProposal.Status.DRAFT:
|
|
70
|
+
res["reset"] = reverse("wbportfolio:orderproposal-reset", args=[instance.id], request=request)
|
|
71
|
+
res["normalize"] = reverse("wbportfolio:orderproposal-normalize", args=[instance.id], request=request)
|
|
72
|
+
if instance.status == OrderProposal.Status.DRAFT or instance.can_be_confirmed:
|
|
73
|
+
res["refresh_return"] = reverse(
|
|
74
|
+
"wbportfolio:orderproposal-refreshreturn", args=[instance.id], request=request
|
|
75
|
+
)
|
|
76
|
+
res["orders"] = reverse(
|
|
77
|
+
"wbportfolio:orderproposal-order-list",
|
|
78
|
+
args=[instance.id],
|
|
79
|
+
request=request,
|
|
80
|
+
)
|
|
81
|
+
return res
|
|
82
|
+
|
|
83
|
+
class Meta:
|
|
84
|
+
model = OrderProposal
|
|
85
|
+
only_fsm_transition_on_instance = True
|
|
86
|
+
percent_fields = ["total_cash_weight"]
|
|
87
|
+
fields = (
|
|
88
|
+
"id",
|
|
89
|
+
"trade_date",
|
|
90
|
+
"portfolio",
|
|
91
|
+
"_portfolio",
|
|
92
|
+
"total_cash_weight",
|
|
93
|
+
"comment",
|
|
94
|
+
"status",
|
|
95
|
+
"min_order_value",
|
|
96
|
+
"min_weighting",
|
|
97
|
+
"_rebalancing_model",
|
|
98
|
+
"rebalancing_model",
|
|
99
|
+
"target_portfolio",
|
|
100
|
+
"_target_portfolio",
|
|
101
|
+
"creator",
|
|
102
|
+
"approver",
|
|
103
|
+
"_creator",
|
|
104
|
+
"_approver",
|
|
105
|
+
"execution_status",
|
|
106
|
+
"execution_status_detail",
|
|
107
|
+
"execution_comment",
|
|
108
|
+
"execution_status_repr",
|
|
109
|
+
"_additional_resources",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ReadOnlyOrderProposalModelSerializer(OrderProposalModelSerializer):
|
|
114
|
+
class Meta(OrderProposalModelSerializer.Meta):
|
|
115
|
+
read_only_fields = OrderProposalModelSerializer.Meta.fields
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from rest_framework import serializers
|
|
4
|
+
from rest_framework.reverse import reverse
|
|
5
|
+
from rest_framework.validators import UniqueTogetherValidator
|
|
6
|
+
from wbcore import serializers as wb_serializers
|
|
7
|
+
from wbcore.metadata.configs.display.list_display import BaseTreeGroupLevelOption
|
|
8
|
+
from wbfdm.models import Instrument
|
|
9
|
+
from wbfdm.serializers import InvestableInstrumentRepresentationSerializer
|
|
10
|
+
from wbfdm.serializers.instruments.instruments import (
|
|
11
|
+
CompanyRepresentationSerializer,
|
|
12
|
+
SecurityRepresentationSerializer,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from wbportfolio.models import Order, OrderProposal
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GetSecurityDefault:
|
|
19
|
+
requires_context = True
|
|
20
|
+
|
|
21
|
+
def __call__(self, serializer_instance):
|
|
22
|
+
try:
|
|
23
|
+
instance = serializer_instance.view.get_object()
|
|
24
|
+
return instance.underlying_instrument.parent or instance.underlying_instrument
|
|
25
|
+
except Exception:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GetCompanyDefault:
|
|
30
|
+
requires_context = True
|
|
31
|
+
|
|
32
|
+
def __call__(self, serializer_instance):
|
|
33
|
+
try:
|
|
34
|
+
instance = serializer_instance.view.get_object()
|
|
35
|
+
security = instance.underlying_instrument.parent or instance.underlying_instrument
|
|
36
|
+
return security.parent or security
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
42
|
+
underlying_instrument = wb_serializers.SlugRelatedField(read_only=True, slug_field="name")
|
|
43
|
+
underlying_instrument_isin = wb_serializers.CharField(read_only=True)
|
|
44
|
+
underlying_instrument_ticker = wb_serializers.CharField(read_only=True)
|
|
45
|
+
underlying_instrument_refinitiv_identifier_code = wb_serializers.CharField(read_only=True)
|
|
46
|
+
underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
|
|
47
|
+
underlying_instrument_exchange = wb_serializers.CharField(read_only=True)
|
|
48
|
+
|
|
49
|
+
effective_weight = wb_serializers.DecimalField(
|
|
50
|
+
read_only=True,
|
|
51
|
+
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
52
|
+
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
53
|
+
default=0,
|
|
54
|
+
)
|
|
55
|
+
target_weight = wb_serializers.DecimalField(
|
|
56
|
+
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
57
|
+
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
58
|
+
required=False,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
62
|
+
target_shares = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=6)
|
|
63
|
+
|
|
64
|
+
effective_total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
65
|
+
read_only=True, max_digits=16, decimal_places=2, default=0
|
|
66
|
+
)
|
|
67
|
+
target_total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
|
|
68
|
+
total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
|
|
69
|
+
|
|
70
|
+
portfolio_currency = wb_serializers.CharField(read_only=True)
|
|
71
|
+
underlying_instrument_currency = wb_serializers.CharField(read_only=True)
|
|
72
|
+
has_warnings = wb_serializers.BooleanField(read_only=True)
|
|
73
|
+
execution_instruction_parameters_repr = wb_serializers.CharField(read_only=True)
|
|
74
|
+
execution_date = wb_serializers.DateField(read_only=True)
|
|
75
|
+
execution_price = wb_serializers.FloatField(read_only=True)
|
|
76
|
+
execution_traded_shares = wb_serializers.FloatField(read_only=True)
|
|
77
|
+
|
|
78
|
+
@wb_serializers.register_resource()
|
|
79
|
+
def additional_resources(self, instance, request, user):
|
|
80
|
+
if (view := request.parser_context.get("view")) and view.order_proposal.status in [
|
|
81
|
+
OrderProposal.Status.DRAFT,
|
|
82
|
+
OrderProposal.Status.PENDING,
|
|
83
|
+
OrderProposal.Status.APPROVED,
|
|
84
|
+
]:
|
|
85
|
+
return {
|
|
86
|
+
"execution_instruction": reverse(
|
|
87
|
+
"wbportfolio:orderproposal-order-changeexecutioninstruction",
|
|
88
|
+
args=[view.order_proposal.id, instance.id],
|
|
89
|
+
request=request,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return {}
|
|
93
|
+
|
|
94
|
+
def validate(self, data):
|
|
95
|
+
data.pop("company", None)
|
|
96
|
+
data.pop("security", None)
|
|
97
|
+
if self.instance and "underlying_instrument" in data:
|
|
98
|
+
raise serializers.ValidationError(
|
|
99
|
+
{
|
|
100
|
+
"underlying_instrument": "You cannot modify the underlying instrument other than creating a new entry"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
|
|
105
|
+
effective_shares = self.instance._effective_shares if self.instance else Decimal(0.0)
|
|
106
|
+
portfolio_value = (
|
|
107
|
+
self.context["view"].order_proposal.portfolio_total_asset_value if "view" in self.context else Decimal(0.0)
|
|
108
|
+
)
|
|
109
|
+
if (total_value_fx_portfolio := data.pop("total_value_fx_portfolio", None)) is not None and portfolio_value:
|
|
110
|
+
data["weighting"] = total_value_fx_portfolio / portfolio_value
|
|
111
|
+
if (
|
|
112
|
+
target_total_value_fx_portfolio := data.pop("target_total_value_fx_portfolio", None)
|
|
113
|
+
) is not None and portfolio_value:
|
|
114
|
+
data["target_weight"] = target_total_value_fx_portfolio / portfolio_value
|
|
115
|
+
|
|
116
|
+
if data.get("weighting") is not None or data.get("target_weight") is not None:
|
|
117
|
+
weighting = data.pop("weighting", None)
|
|
118
|
+
if (target_weight := data.pop("target_weight", None)) is not None:
|
|
119
|
+
weighting = target_weight - effective_weight
|
|
120
|
+
data["desired_target_weight"] = target_weight
|
|
121
|
+
if weighting is not None:
|
|
122
|
+
data["weighting"] = weighting
|
|
123
|
+
data.pop("shares", None)
|
|
124
|
+
data.pop("target_shares", None)
|
|
125
|
+
|
|
126
|
+
if data.get("shares") is not None or data.get("target_shares") is not None:
|
|
127
|
+
shares = data.pop("shares", None)
|
|
128
|
+
if (target_shares := data.pop("target_shares", None)) is not None:
|
|
129
|
+
shares = target_shares - effective_shares
|
|
130
|
+
if shares is not None:
|
|
131
|
+
data["shares"] = shares
|
|
132
|
+
return super().validate(data)
|
|
133
|
+
|
|
134
|
+
def update(self, instance, validated_data):
|
|
135
|
+
weighting = validated_data.pop("weighting", None)
|
|
136
|
+
shares = validated_data.pop("shares", None)
|
|
137
|
+
portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
|
|
138
|
+
if weighting is not None:
|
|
139
|
+
instance.set_weighting(weighting, portfolio_total_asset_value)
|
|
140
|
+
if shares is not None:
|
|
141
|
+
instance.set_shares(shares, portfolio_total_asset_value)
|
|
142
|
+
return super().update(instance, validated_data)
|
|
143
|
+
|
|
144
|
+
def create(self, validated_data):
|
|
145
|
+
weighting = validated_data.pop("weighting", None)
|
|
146
|
+
shares = validated_data.pop("shares", None)
|
|
147
|
+
instance = super().create(validated_data)
|
|
148
|
+
portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
|
|
149
|
+
if weighting is not None:
|
|
150
|
+
instance.set_weighting(weighting, portfolio_total_asset_value)
|
|
151
|
+
if shares is not None:
|
|
152
|
+
instance.set_shares(shares, portfolio_total_asset_value)
|
|
153
|
+
instance.save()
|
|
154
|
+
return instance
|
|
155
|
+
|
|
156
|
+
def get_unique_together_validators(self):
|
|
157
|
+
return [
|
|
158
|
+
UniqueTogetherValidator(
|
|
159
|
+
queryset=Order.objects.all(),
|
|
160
|
+
fields=("order_proposal", "underlying_instrument"),
|
|
161
|
+
message="This instrument is already in the orders list.",
|
|
162
|
+
)
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
class Meta:
|
|
166
|
+
model = Order
|
|
167
|
+
percent_fields = ["effective_weight", "target_weight", "weighting", "desired_target_weight"]
|
|
168
|
+
decorators = {
|
|
169
|
+
"total_value_fx_portfolio": wb_serializers.decorator(
|
|
170
|
+
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
171
|
+
),
|
|
172
|
+
"effective_total_value_fx_portfolio": wb_serializers.decorator(
|
|
173
|
+
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
174
|
+
),
|
|
175
|
+
"target_total_value_fx_portfolio": wb_serializers.decorator(
|
|
176
|
+
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
177
|
+
),
|
|
178
|
+
"price": wb_serializers.decorator(position="left", value="{{underlying_instrument_currency}}"),
|
|
179
|
+
}
|
|
180
|
+
read_only_fields = (
|
|
181
|
+
"order_type",
|
|
182
|
+
"effective_shares",
|
|
183
|
+
"effective_total_value_fx_portfolio",
|
|
184
|
+
"has_warnings",
|
|
185
|
+
"desired_target_weight",
|
|
186
|
+
"daily_return",
|
|
187
|
+
"currency_fx_rate",
|
|
188
|
+
"price",
|
|
189
|
+
"execution_instruction",
|
|
190
|
+
"execution_instruction_parameters_repr",
|
|
191
|
+
"execution_date",
|
|
192
|
+
"execution_price",
|
|
193
|
+
"execution_traded_shares",
|
|
194
|
+
)
|
|
195
|
+
extra_kwargs = {
|
|
196
|
+
"price": {"required": False},
|
|
197
|
+
}
|
|
198
|
+
fields = (
|
|
199
|
+
"id",
|
|
200
|
+
"shares",
|
|
201
|
+
"underlying_instrument",
|
|
202
|
+
"underlying_instrument_isin",
|
|
203
|
+
"underlying_instrument_ticker",
|
|
204
|
+
"underlying_instrument_refinitiv_identifier_code",
|
|
205
|
+
"underlying_instrument_instrument_type",
|
|
206
|
+
"underlying_instrument_exchange",
|
|
207
|
+
"order_type",
|
|
208
|
+
"comment",
|
|
209
|
+
"effective_weight",
|
|
210
|
+
"target_weight",
|
|
211
|
+
"weighting",
|
|
212
|
+
"order_proposal",
|
|
213
|
+
"order",
|
|
214
|
+
"effective_shares",
|
|
215
|
+
"target_shares",
|
|
216
|
+
"total_value_fx_portfolio",
|
|
217
|
+
"effective_total_value_fx_portfolio",
|
|
218
|
+
"target_total_value_fx_portfolio",
|
|
219
|
+
"portfolio_currency",
|
|
220
|
+
"underlying_instrument_currency",
|
|
221
|
+
"has_warnings",
|
|
222
|
+
"desired_target_weight",
|
|
223
|
+
"daily_return",
|
|
224
|
+
"currency_fx_rate",
|
|
225
|
+
"price",
|
|
226
|
+
"execution_status",
|
|
227
|
+
"execution_instruction",
|
|
228
|
+
"execution_instruction_parameters",
|
|
229
|
+
"execution_comment",
|
|
230
|
+
"execution_instruction_parameters_repr",
|
|
231
|
+
"execution_date",
|
|
232
|
+
"execution_price",
|
|
233
|
+
"execution_traded_shares",
|
|
234
|
+
"_additional_resources",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class OrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
|
|
239
|
+
company = wb_serializers.PrimaryKeyRelatedField(
|
|
240
|
+
queryset=Instrument.objects.filter(level=0),
|
|
241
|
+
required=False,
|
|
242
|
+
read_only=lambda view: not view.new_mode,
|
|
243
|
+
default=GetCompanyDefault(),
|
|
244
|
+
)
|
|
245
|
+
_company = CompanyRepresentationSerializer(source="company", required=False)
|
|
246
|
+
|
|
247
|
+
security = wb_serializers.PrimaryKeyRelatedField(
|
|
248
|
+
queryset=Instrument.objects.filter(is_security=True),
|
|
249
|
+
required=False,
|
|
250
|
+
read_only=lambda view: not view.new_mode,
|
|
251
|
+
default=GetSecurityDefault(),
|
|
252
|
+
)
|
|
253
|
+
_security = SecurityRepresentationSerializer(
|
|
254
|
+
source="security",
|
|
255
|
+
optional_get_parameters={"company": "parent"},
|
|
256
|
+
depends_on=[{"field": "company", "options": {}}],
|
|
257
|
+
required=False,
|
|
258
|
+
select_first_choice=True,
|
|
259
|
+
)
|
|
260
|
+
underlying_instrument = wb_serializers.PrimaryKeyRelatedField(
|
|
261
|
+
queryset=Instrument.objects.all(), label="Quote", read_only=lambda view: not view.new_mode
|
|
262
|
+
)
|
|
263
|
+
_underlying_instrument = InvestableInstrumentRepresentationSerializer(
|
|
264
|
+
source="underlying_instrument",
|
|
265
|
+
optional_get_parameters={"security": "parent"},
|
|
266
|
+
depends_on=[{"field": "security", "options": {}}],
|
|
267
|
+
tree_config=BaseTreeGroupLevelOption(clear_filter=True, filter_key="parent"),
|
|
268
|
+
select_first_choice=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
class Meta(OrderOrderProposalListModelSerializer.Meta):
|
|
272
|
+
fields = list(OrderOrderProposalListModelSerializer.Meta.fields) + [
|
|
273
|
+
"company",
|
|
274
|
+
"_company",
|
|
275
|
+
"security",
|
|
276
|
+
"_security",
|
|
277
|
+
"_underlying_instrument",
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ReadOnlyOrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
|
|
282
|
+
class Meta(OrderOrderProposalListModelSerializer.Meta):
|
|
283
|
+
read_only_fields = OrderOrderProposalListModelSerializer.Meta.fields
|
|
@@ -34,10 +34,10 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
34
34
|
|
|
35
35
|
last_asset_under_management_usd = wb_serializers.FloatField(read_only=True)
|
|
36
36
|
last_positions = wb_serializers.FloatField(read_only=True)
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
last_order_proposal_date = wb_serializers.DateField(read_only=True)
|
|
38
|
+
next_expected_order_proposal_date = wb_serializers.SerializerMethodField(read_only=True)
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def get_next_expected_order_proposal_date(self, obj):
|
|
41
41
|
if (automatic_rebalancer := getattr(obj, "automatic_rebalancer", None)) and (
|
|
42
42
|
_d := automatic_rebalancer.get_next_rebalancing_date(date.today())
|
|
43
43
|
):
|
|
@@ -107,8 +107,8 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
107
107
|
args=[instance.id],
|
|
108
108
|
request=request,
|
|
109
109
|
)
|
|
110
|
-
additional_resources["
|
|
111
|
-
"wbportfolio:portfolio-
|
|
110
|
+
additional_resources["order_proposals"] = reverse(
|
|
111
|
+
"wbportfolio:portfolio-orderproposal-list",
|
|
112
112
|
args=[instance.id],
|
|
113
113
|
request=request,
|
|
114
114
|
)
|
|
@@ -169,8 +169,8 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
169
169
|
"last_position_date",
|
|
170
170
|
"last_asset_under_management_usd",
|
|
171
171
|
"last_positions",
|
|
172
|
-
"
|
|
173
|
-
"
|
|
172
|
+
"last_order_proposal_date",
|
|
173
|
+
"next_expected_order_proposal_date",
|
|
174
174
|
)
|
|
175
175
|
|
|
176
176
|
|
|
@@ -11,8 +11,8 @@ class AggregatedAssetPositionModelSerializer(wb_serializers.ModelSerializer):
|
|
|
11
11
|
sum_total_value = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=4)
|
|
12
12
|
weighting = wb_serializers.DecimalField(
|
|
13
13
|
read_only=True,
|
|
14
|
-
max_digits=
|
|
15
|
-
decimal_places=
|
|
14
|
+
max_digits=9,
|
|
15
|
+
decimal_places=8,
|
|
16
16
|
percent=True,
|
|
17
17
|
decorators=[{"position": "right", "value": "%"}],
|
|
18
18
|
)
|