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
wbportfolio/pms/typing.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import enum
|
|
1
2
|
from dataclasses import asdict, dataclass, field, fields
|
|
3
|
+
from datetime import date
|
|
2
4
|
from datetime import date as date_lib
|
|
3
5
|
from decimal import Decimal
|
|
6
|
+
from typing import Self
|
|
4
7
|
|
|
5
8
|
import pandas as pd
|
|
6
9
|
from django.core.exceptions import ValidationError
|
|
7
10
|
|
|
11
|
+
from wbportfolio.order_routing import ExecutionInstruction
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
@dataclass(frozen=True)
|
|
10
15
|
class Valuation:
|
|
@@ -13,12 +18,13 @@ class Valuation:
|
|
|
13
18
|
outstanding_shares: Decimal = Decimal(0)
|
|
14
19
|
|
|
15
20
|
|
|
16
|
-
@dataclass(
|
|
21
|
+
@dataclass()
|
|
17
22
|
class Position:
|
|
18
23
|
underlying_instrument: int
|
|
19
24
|
weighting: Decimal
|
|
20
25
|
date: date_lib
|
|
21
26
|
|
|
27
|
+
daily_return: Decimal = Decimal("0")
|
|
22
28
|
currency: int | None = None
|
|
23
29
|
instrument_type: int | None = None
|
|
24
30
|
asset_valuation_date: date_lib | None = None
|
|
@@ -37,6 +43,10 @@ class Position:
|
|
|
37
43
|
volume_usd: float = None
|
|
38
44
|
price: float = None
|
|
39
45
|
|
|
46
|
+
def __post_init__(self):
|
|
47
|
+
self.daily_return = round(self.daily_return, 16)
|
|
48
|
+
self.weighting = round(self.weighting, 8)
|
|
49
|
+
|
|
40
50
|
def __add__(self, other):
|
|
41
51
|
return Position(
|
|
42
52
|
weighting=self.weighting + other.weighting,
|
|
@@ -44,59 +54,70 @@ class Position:
|
|
|
44
54
|
**{f.name: getattr(self, f.name) for f in fields(Position) if f.name not in ["weighting", "shares"]},
|
|
45
55
|
)
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
positions_map: dict[Position] = field(init=False, repr=False)
|
|
52
|
-
|
|
53
|
-
def __post_init__(self):
|
|
54
|
-
positions_map = {}
|
|
55
|
-
for pos in self.positions:
|
|
56
|
-
if pos.underlying_instrument in positions_map:
|
|
57
|
-
positions_map[pos.underlying_instrument] += pos
|
|
58
|
-
else:
|
|
59
|
-
positions_map[pos.underlying_instrument] = pos
|
|
60
|
-
object.__setattr__(self, "positions_map", positions_map)
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def total_weight(self):
|
|
64
|
-
return round(sum([pos.weighting for pos in self.positions]), 6)
|
|
65
|
-
|
|
66
|
-
@property
|
|
67
|
-
def total_shares(self):
|
|
68
|
-
return sum([pos.target_shares for pos in self.positions if pos.target_shares is not None])
|
|
69
|
-
|
|
70
|
-
def to_df(self):
|
|
71
|
-
return pd.DataFrame([asdict(pos) for pos in self.positions])
|
|
72
|
-
|
|
73
|
-
def to_dict(self) -> dict[int, Decimal]:
|
|
74
|
-
return {underlying_instrument: pos.weighting for underlying_instrument, pos in self.positions_map.items()}
|
|
75
|
-
|
|
76
|
-
def __len__(self):
|
|
77
|
-
return len(self.positions)
|
|
57
|
+
def copy(self, **kwargs):
|
|
58
|
+
attrs = {f.name: getattr(self, f.name) for f in fields(Position)}
|
|
59
|
+
attrs.update(kwargs)
|
|
60
|
+
return Position(**attrs)
|
|
78
61
|
|
|
79
62
|
|
|
80
63
|
@dataclass(frozen=True)
|
|
64
|
+
class Order:
|
|
65
|
+
class AssetType(enum.Enum):
|
|
66
|
+
EQUITY = "EQUITY"
|
|
67
|
+
AMERICAN_DEPOSITORY_RECEIPT = "AMERICAN_DEPOSITORY_RECEIPT"
|
|
68
|
+
|
|
69
|
+
id: int | str
|
|
70
|
+
trade_date: date
|
|
71
|
+
target_weight: float
|
|
72
|
+
|
|
73
|
+
# Instrument identifier
|
|
74
|
+
asset_class: AssetType
|
|
75
|
+
refinitiv_identifier_code: str | None = None
|
|
76
|
+
bloomberg_ticker: str | None = None
|
|
77
|
+
sedol: str | None = None
|
|
78
|
+
|
|
79
|
+
weighting: float | None = None
|
|
80
|
+
target_shares: float | None = None
|
|
81
|
+
shares: float | None = None
|
|
82
|
+
execution_price: float | None = None
|
|
83
|
+
execution_instruction: ExecutionInstruction = ExecutionInstruction.MARKET_ON_CLOSE.value
|
|
84
|
+
execution_instruction_parameters: dict | None = None
|
|
85
|
+
comment: str = ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass()
|
|
81
89
|
class Trade:
|
|
82
90
|
underlying_instrument: int
|
|
83
|
-
instrument_type:
|
|
91
|
+
instrument_type: int
|
|
84
92
|
currency: int
|
|
85
93
|
date: date_lib
|
|
86
|
-
|
|
94
|
+
price: Decimal
|
|
87
95
|
effective_weight: Decimal
|
|
88
96
|
target_weight: Decimal
|
|
97
|
+
currency_fx_rate: Decimal = Decimal("1")
|
|
98
|
+
effective_shares: Decimal = Decimal("0")
|
|
99
|
+
target_shares: Decimal = Decimal("0")
|
|
100
|
+
daily_return: Decimal = Decimal("0")
|
|
101
|
+
effective_quantization_error: Decimal = Decimal("0")
|
|
102
|
+
target_quantization_error: Decimal = Decimal("0")
|
|
103
|
+
|
|
89
104
|
id: int | None = None
|
|
90
|
-
|
|
105
|
+
is_cash: bool = False
|
|
106
|
+
|
|
107
|
+
def __post_init__(self):
|
|
108
|
+
self.effective_weight = round(self.effective_weight, 8)
|
|
109
|
+
# ensure a trade target weight cannot be lower than 0
|
|
110
|
+
self.target_weight = max(round(self.target_weight, 8), Decimal("0"))
|
|
111
|
+
self.daily_return = round(self.daily_return, 16)
|
|
91
112
|
|
|
92
113
|
def __add__(self, other):
|
|
93
114
|
return Trade(
|
|
94
115
|
underlying_instrument=self.underlying_instrument,
|
|
95
|
-
effective_weight=self.effective_weight
|
|
116
|
+
effective_weight=self.effective_weight,
|
|
96
117
|
target_weight=self.target_weight + other.target_weight,
|
|
97
|
-
effective_shares=self.effective_shares
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
effective_shares=self.effective_shares,
|
|
119
|
+
target_shares=self.target_shares + other.target_shares,
|
|
120
|
+
daily_return=self.daily_return,
|
|
100
121
|
**{
|
|
101
122
|
f.name: getattr(self, f.name)
|
|
102
123
|
for f in fields(Trade)
|
|
@@ -105,14 +126,29 @@ class Trade:
|
|
|
105
126
|
"effective_weight",
|
|
106
127
|
"target_weight",
|
|
107
128
|
"effective_shares",
|
|
129
|
+
"target_shares",
|
|
108
130
|
"underlying_instrument",
|
|
131
|
+
"daily_return",
|
|
109
132
|
]
|
|
110
133
|
},
|
|
111
134
|
)
|
|
112
135
|
|
|
136
|
+
def copy(self, **kwargs):
|
|
137
|
+
attrs = {f.name: getattr(self, f.name) for f in fields(Trade)}
|
|
138
|
+
attrs.update(kwargs)
|
|
139
|
+
return Trade(**attrs)
|
|
140
|
+
|
|
113
141
|
@property
|
|
114
142
|
def delta_weight(self) -> Decimal:
|
|
115
|
-
return self.target_weight - self.effective_weight
|
|
143
|
+
return (self.target_weight + self.target_quantization_error) - self.effective_weight
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def delta_shares(self) -> Decimal:
|
|
147
|
+
return self.target_shares - self.effective_shares
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def price_fx_portfolio(self) -> Decimal:
|
|
151
|
+
return self.price * self.currency_fx_rate
|
|
116
152
|
|
|
117
153
|
def validate(self):
|
|
118
154
|
return True
|
|
@@ -121,21 +157,30 @@ class Trade:
|
|
|
121
157
|
# if self.target_weight < 0 or self.target_weight > 1.0:
|
|
122
158
|
# raise ValidationError("Target Weight needs to be in range [0, 1]")
|
|
123
159
|
|
|
124
|
-
def normalize_target(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
def normalize_target(
|
|
161
|
+
self, factor: Decimal | None = None, target_shares: Decimal | int | None = None, target_weight: Decimal = None
|
|
162
|
+
):
|
|
163
|
+
if factor is None:
|
|
164
|
+
if target_shares is not None:
|
|
165
|
+
factor = target_shares / self.target_shares if self.target_shares else Decimal("1")
|
|
166
|
+
elif target_weight is not None:
|
|
167
|
+
factor = target_weight / self.target_weight if self.target_weight else Decimal("1")
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError("Target weight and shares cannot be both None")
|
|
170
|
+
return self.copy(target_weight=self.target_weight * factor, target_shares=self.target_shares * factor)
|
|
130
171
|
|
|
131
172
|
|
|
132
173
|
@dataclass(frozen=True)
|
|
133
174
|
class TradeBatch:
|
|
134
|
-
trades:
|
|
175
|
+
trades: list[Trade]
|
|
135
176
|
trades_map: dict[Trade] = field(init=False, repr=False)
|
|
136
177
|
|
|
137
178
|
def __post_init__(self):
|
|
138
179
|
trade_map = {}
|
|
180
|
+
if self.total_effective_weight and (quant_error := Decimal("1") - self.total_effective_weight):
|
|
181
|
+
self.largest_effective_order.effective_quantization_error = quant_error
|
|
182
|
+
if self.total_target_weight and (quant_error := Decimal("1") - self.total_target_weight):
|
|
183
|
+
self.largest_effective_order.target_quantization_error = quant_error
|
|
139
184
|
for trade in self.trades:
|
|
140
185
|
if trade.underlying_instrument in trade_map:
|
|
141
186
|
trade_map[trade.underlying_instrument] += trade
|
|
@@ -143,17 +188,21 @@ class TradeBatch:
|
|
|
143
188
|
trade_map[trade.underlying_instrument] = trade
|
|
144
189
|
object.__setattr__(self, "trades_map", trade_map)
|
|
145
190
|
|
|
191
|
+
@property
|
|
192
|
+
def largest_effective_order(self) -> Trade:
|
|
193
|
+
return max(self.trades, key=lambda obj: obj.effective_weight)
|
|
194
|
+
|
|
146
195
|
@property
|
|
147
196
|
def total_target_weight(self) -> Decimal:
|
|
148
|
-
return
|
|
197
|
+
return sum([trade.target_weight for trade in self.trades], Decimal("0"))
|
|
149
198
|
|
|
150
199
|
@property
|
|
151
200
|
def total_effective_weight(self) -> Decimal:
|
|
152
|
-
return
|
|
201
|
+
return sum([trade.effective_weight for trade in self.trades], Decimal("0"))
|
|
153
202
|
|
|
154
203
|
@property
|
|
155
|
-
def
|
|
156
|
-
return sum([abs(trade.delta_weight) for trade in self.trades])
|
|
204
|
+
def total_abs_delta_weight(self) -> Decimal:
|
|
205
|
+
return sum([abs(trade.delta_weight) for trade in self.trades], Decimal("0"))
|
|
157
206
|
|
|
158
207
|
def __add__(self, other):
|
|
159
208
|
return TradeBatch(tuple(self.trades + other.trades))
|
|
@@ -161,20 +210,106 @@ class TradeBatch:
|
|
|
161
210
|
def __len__(self):
|
|
162
211
|
return len(self.trades)
|
|
163
212
|
|
|
213
|
+
def __iter__(self):
|
|
214
|
+
return iter(self.trades_map.values())
|
|
215
|
+
|
|
164
216
|
def validate(self):
|
|
165
217
|
if round(float(self.total_target_weight), 4) != 1: # we do that to remove decimal over precision
|
|
166
218
|
raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
|
|
167
219
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
220
|
+
|
|
221
|
+
@dataclass(frozen=True)
|
|
222
|
+
class Portfolio:
|
|
223
|
+
positions: list[Position] | list
|
|
224
|
+
positions_map: dict[int, Position] = field(init=False, repr=False)
|
|
225
|
+
|
|
226
|
+
def __post_init__(self):
|
|
227
|
+
positions_map = {}
|
|
228
|
+
|
|
229
|
+
for pos in self.positions:
|
|
230
|
+
if pos.underlying_instrument in positions_map:
|
|
231
|
+
positions_map[pos.underlying_instrument] += pos
|
|
232
|
+
else:
|
|
233
|
+
positions_map[pos.underlying_instrument] = pos
|
|
234
|
+
object.__setattr__(self, "positions_map", positions_map)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def total_weight(self):
|
|
238
|
+
return sum([pos.weighting for pos in self.positions])
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def total_shares(self):
|
|
242
|
+
return sum([pos.target_shares for pos in self.positions if pos.target_shares is not None])
|
|
243
|
+
|
|
244
|
+
def to_df(self, exclude_cash: bool = False) -> pd.DataFrame:
|
|
245
|
+
return pd.DataFrame([asdict(pos) for pos in self.positions if not exclude_cash or not pos.is_cash])
|
|
246
|
+
|
|
247
|
+
def to_dict(self) -> dict[int, Decimal]:
|
|
248
|
+
return {underlying_instrument: pos.weighting for underlying_instrument, pos in self.positions_map.items()}
|
|
249
|
+
|
|
250
|
+
def get_orders(self, target_portfolio: Self) -> TradeBatch:
|
|
251
|
+
instruments = self.positions_map.copy()
|
|
252
|
+
instruments.update(target_portfolio.positions_map)
|
|
253
|
+
|
|
254
|
+
trades: list[Trade] = []
|
|
255
|
+
for instrument_id, pos in instruments.items():
|
|
256
|
+
effective_weight = target_weight = 0
|
|
257
|
+
effective_shares = target_shares = 0
|
|
258
|
+
daily_return = 0
|
|
259
|
+
price = Decimal("0")
|
|
260
|
+
is_cash = False
|
|
261
|
+
trade_date = None
|
|
262
|
+
if effective_pos := self.positions_map.get(instrument_id, None):
|
|
263
|
+
effective_weight = effective_pos.weighting
|
|
264
|
+
effective_shares = effective_pos.shares
|
|
265
|
+
daily_return = effective_pos.daily_return
|
|
266
|
+
is_cash = effective_pos.is_cash
|
|
267
|
+
price = effective_pos.price
|
|
268
|
+
trade_date = effective_pos.date
|
|
269
|
+
|
|
270
|
+
if target_pos := target_portfolio.positions_map.get(instrument_id, None):
|
|
271
|
+
target_weight = target_pos.weighting
|
|
272
|
+
is_cash = target_pos.is_cash
|
|
273
|
+
if target_pos.shares is not None:
|
|
274
|
+
target_shares = target_pos.shares
|
|
275
|
+
if target_pos.price:
|
|
276
|
+
price = target_pos.price
|
|
277
|
+
trade_date = target_pos.date
|
|
278
|
+
|
|
279
|
+
trade = Trade(
|
|
280
|
+
underlying_instrument=instrument_id,
|
|
281
|
+
effective_weight=effective_weight,
|
|
282
|
+
target_weight=target_weight,
|
|
283
|
+
effective_shares=effective_shares,
|
|
284
|
+
target_shares=target_shares,
|
|
285
|
+
date=trade_date,
|
|
286
|
+
instrument_type=pos.instrument_type,
|
|
287
|
+
currency=pos.currency,
|
|
288
|
+
price=Decimal(price) if price else Decimal("0"),
|
|
289
|
+
currency_fx_rate=Decimal(pos.currency_fx_rate),
|
|
290
|
+
daily_return=Decimal(daily_return),
|
|
291
|
+
is_cash=is_cash,
|
|
179
292
|
)
|
|
180
|
-
|
|
293
|
+
trades.append(trade)
|
|
294
|
+
return TradeBatch(trades)
|
|
295
|
+
|
|
296
|
+
def __len__(self):
|
|
297
|
+
return len(self.positions)
|
|
298
|
+
|
|
299
|
+
def __bool__(self):
|
|
300
|
+
return len(self.positions) > 0
|
|
301
|
+
|
|
302
|
+
def normalize_cash(self, target_cash_weight: Decimal):
|
|
303
|
+
"""
|
|
304
|
+
Normalize the instantiate portfolio so that the sum of the cash position equals to the new target cash position
|
|
305
|
+
"""
|
|
306
|
+
positions = list(filter(lambda pos: not pos.is_cash, self.positions))
|
|
307
|
+
cash_position = next(filter(lambda pos: pos.is_cash, self.positions), None)
|
|
308
|
+
total_non_cash_weight = sum(map(lambda pos: pos.weighting, positions))
|
|
309
|
+
target_weight = Decimal("1") - target_cash_weight
|
|
310
|
+
for pos in positions:
|
|
311
|
+
pos.weighting = pos.weighting * target_weight / total_non_cash_weight
|
|
312
|
+
if cash_position:
|
|
313
|
+
cash_position.weighting = target_cash_weight
|
|
314
|
+
positions.append(cash_position)
|
|
315
|
+
return Portfolio(positions)
|
wbportfolio/rebalancing/base.py
CHANGED
|
@@ -8,10 +8,21 @@ class AbstractRebalancingModel:
|
|
|
8
8
|
def validation_errors(self) -> str:
|
|
9
9
|
return getattr(self, "_validation_errors", "Rebalacing cannot applied for these parameters")
|
|
10
10
|
|
|
11
|
-
def __init__(
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
portfolio,
|
|
14
|
+
trade_date: date,
|
|
15
|
+
last_effective_date: date,
|
|
16
|
+
effective_portfolio: PortfolioDTO | None = None,
|
|
17
|
+
**kwargs,
|
|
18
|
+
):
|
|
12
19
|
self.portfolio = portfolio
|
|
13
20
|
self.trade_date = trade_date
|
|
14
21
|
self.last_effective_date = last_effective_date
|
|
22
|
+
self.effective_portfolio = effective_portfolio
|
|
23
|
+
# we try to get the portfolio at the trade date
|
|
24
|
+
if not self.effective_portfolio:
|
|
25
|
+
self.effective_portfolio = self.portfolio._build_dto(self.last_effective_date)
|
|
15
26
|
|
|
16
27
|
def is_valid(self) -> bool:
|
|
17
28
|
return True
|
|
@@ -2,7 +2,7 @@ def register(model_name: str):
|
|
|
2
2
|
"""
|
|
3
3
|
Decorator to include when a backend need automatic registration
|
|
4
4
|
"""
|
|
5
|
-
from wbportfolio.models.
|
|
5
|
+
from wbportfolio.models.rebalancing import RebalancingModel
|
|
6
6
|
|
|
7
7
|
def _decorator(backend_class):
|
|
8
8
|
defaults = {
|
|
@@ -3,7 +3,6 @@ from decimal import Decimal
|
|
|
3
3
|
from django.core.exceptions import ObjectDoesNotExist
|
|
4
4
|
from wbfdm.models import InstrumentPrice
|
|
5
5
|
|
|
6
|
-
from wbportfolio.models import Trade
|
|
7
6
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
8
7
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
9
8
|
from wbportfolio.rebalancing.decorators import register
|
|
@@ -14,21 +13,18 @@ class CompositeRebalancing(AbstractRebalancingModel):
|
|
|
14
13
|
@property
|
|
15
14
|
def base_assets(self) -> dict[int, Decimal]:
|
|
16
15
|
"""
|
|
17
|
-
Return a dictionary representation (instrument_id: target weight) of this
|
|
16
|
+
Return a dictionary representation (instrument_id: target weight) of this order proposal
|
|
18
17
|
Returns:
|
|
19
18
|
A dictionary representation
|
|
20
19
|
|
|
21
20
|
"""
|
|
22
21
|
try:
|
|
23
|
-
|
|
24
|
-
status="
|
|
22
|
+
latest_order_proposal = self.portfolio.order_proposals.filter(
|
|
23
|
+
status="CONFIRMED", trade_date__lt=self.trade_date
|
|
25
24
|
).latest("trade_date")
|
|
26
25
|
return {
|
|
27
26
|
v["underlying_instrument"]: v["target_weight"]
|
|
28
|
-
for v in
|
|
29
|
-
.annotate_base_info()
|
|
30
|
-
.filter(status=Trade.Status.EXECUTED)
|
|
31
|
-
.values("underlying_instrument", "target_weight")
|
|
27
|
+
for v in latest_order_proposal.get_orders().values("underlying_instrument", "target_weight")
|
|
32
28
|
}
|
|
33
29
|
except ObjectDoesNotExist:
|
|
34
30
|
return dict()
|
|
@@ -10,23 +10,25 @@ from wbportfolio.rebalancing.decorators import register
|
|
|
10
10
|
@register("Equally Weighted Rebalancing")
|
|
11
11
|
class EquallyWeightedRebalancing(AbstractRebalancingModel):
|
|
12
12
|
def __init__(self, *args, **kwargs):
|
|
13
|
-
super().__init__(*args, **kwargs)
|
|
14
|
-
|
|
13
|
+
super(EquallyWeightedRebalancing, self).__init__(*args, **kwargs)
|
|
14
|
+
if not self.effective_portfolio:
|
|
15
|
+
self.effective_portfolio = self.portfolio._build_dto(self.trade_date)
|
|
15
16
|
|
|
16
17
|
def is_valid(self) -> bool:
|
|
17
18
|
return (
|
|
18
|
-
self.
|
|
19
|
+
len(self.effective_portfolio.positions) > 0
|
|
19
20
|
and InstrumentPrice.objects.filter(
|
|
20
|
-
date=self.trade_date, instrument__in=self.
|
|
21
|
+
date=self.trade_date, instrument__in=self.effective_portfolio.positions_map.keys()
|
|
21
22
|
).exists()
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
def get_target_portfolio(self) -> Portfolio:
|
|
25
26
|
positions = []
|
|
26
|
-
assets =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
assets = list(filter(lambda p: not p.is_cash, self.effective_portfolio.positions))
|
|
28
|
+
for position in assets:
|
|
29
|
+
positions.append(
|
|
30
|
+
position.copy(
|
|
31
|
+
weighting=Decimal(1 / len(assets)), date=self.trade_date, asset_valuation_date=self.trade_date
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return Portfolio(positions)
|
|
@@ -12,7 +12,9 @@ from wbfdm.models import (
|
|
|
12
12
|
InstrumentListThroughModel,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
+
from wbportfolio.pms.analytics.utils import fix_quantization_error
|
|
15
16
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
17
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
16
18
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
17
19
|
from wbportfolio.rebalancing.decorators import register
|
|
18
20
|
|
|
@@ -20,9 +22,17 @@ from wbportfolio.rebalancing.decorators import register
|
|
|
20
22
|
@register("Market Capitalization Rebalancing")
|
|
21
23
|
class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
22
24
|
TARGET_CURRENCY: str = "USD"
|
|
25
|
+
MIN_WEIGHT: float = 1e-5 # we allow only weight of minimum 0.01%
|
|
23
26
|
|
|
24
|
-
def __init__(
|
|
25
|
-
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*args,
|
|
30
|
+
bypass_exchange_check: bool = False,
|
|
31
|
+
ffill_market_cap_limit: int = 5,
|
|
32
|
+
effective_portfolio: PortfolioDTO | None = None,
|
|
33
|
+
**kwargs,
|
|
34
|
+
):
|
|
35
|
+
super().__init__(*args, effective_portfolio=effective_portfolio, **kwargs)
|
|
26
36
|
self.bypass_exchange_check = bypass_exchange_check
|
|
27
37
|
instruments = self._get_instruments(**kwargs)
|
|
28
38
|
self.market_cap_df = pd.DataFrame(
|
|
@@ -78,9 +88,7 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
78
88
|
)
|
|
79
89
|
|
|
80
90
|
if not instrument_ids:
|
|
81
|
-
instrument_ids = list(
|
|
82
|
-
self.portfolio.assets.filter(date=self.trade_date).values_list("underlying_instrument", flat=True)
|
|
83
|
-
)
|
|
91
|
+
instrument_ids = list(self.effective_portfolio.positions_map.keys())
|
|
84
92
|
|
|
85
93
|
return (
|
|
86
94
|
Instrument.objects.filter(id__in=instrument_ids)
|
|
@@ -100,22 +108,21 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
100
108
|
return df.any()
|
|
101
109
|
else:
|
|
102
110
|
if missing_exchanges.exists():
|
|
103
|
-
|
|
104
|
-
self,
|
|
105
|
-
"_validation_errors",
|
|
106
|
-
f"Couldn't find any market capitalization for exchanges {', '.join([str(e) for e in missing_exchanges])}",
|
|
107
|
-
)
|
|
111
|
+
self._validation_errors = f"Couldn't find any market capitalization for exchanges {', '.join([str(e) for e in missing_exchanges])}"
|
|
108
112
|
return df.all()
|
|
109
113
|
return False
|
|
110
114
|
|
|
111
115
|
def get_target_portfolio(self) -> Portfolio:
|
|
112
116
|
positions = []
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
df = self.market_cap_df / self.market_cap_df.dropna().sum()
|
|
118
|
+
df = df[df > self.MIN_WEIGHT]
|
|
119
|
+
df = df / df.sum()
|
|
120
|
+
df = fix_quantization_error(df, 8)
|
|
121
|
+
for underlying_instrument, weighting in df.to_dict().items():
|
|
122
|
+
if np.isnan(weighting):
|
|
116
123
|
weighting = Decimal(0)
|
|
117
124
|
else:
|
|
118
|
-
weighting = Decimal(
|
|
125
|
+
weighting = round(Decimal(weighting), 8)
|
|
119
126
|
positions.append(
|
|
120
127
|
Position(
|
|
121
128
|
underlying_instrument=underlying_instrument,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from wbfdm.models import InstrumentPrice
|
|
2
|
-
|
|
3
1
|
from wbportfolio.pms.typing import Portfolio
|
|
4
2
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
5
3
|
from wbportfolio.rebalancing.decorators import register
|
|
@@ -9,6 +7,11 @@ from wbportfolio.rebalancing.decorators import register
|
|
|
9
7
|
class ModelPortfolioRebalancing(AbstractRebalancingModel):
|
|
10
8
|
def __init__(self, *args, **kwargs):
|
|
11
9
|
super().__init__(*args, **kwargs)
|
|
10
|
+
self.value_date = (
|
|
11
|
+
self.trade_date
|
|
12
|
+
if self.model_portfolio.assets.filter(date=self.trade_date).exists()
|
|
13
|
+
else self.last_effective_date
|
|
14
|
+
)
|
|
12
15
|
|
|
13
16
|
@property
|
|
14
17
|
def model_portfolio_rel(self):
|
|
@@ -16,26 +19,19 @@ class ModelPortfolioRebalancing(AbstractRebalancingModel):
|
|
|
16
19
|
|
|
17
20
|
@property
|
|
18
21
|
def model_portfolio(self):
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
return self.model_portfolio_rel.dependency_portfolio if self.model_portfolio_rel else None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def assets(self):
|
|
26
|
+
return self.model_portfolio.get_positions(self.value_date) if self.model_portfolio else []
|
|
21
27
|
|
|
22
28
|
def is_valid(self) -> bool:
|
|
23
|
-
|
|
24
|
-
assets = model_portfolio.get_positions(self.last_effective_date)
|
|
25
|
-
return (
|
|
26
|
-
assets.exists()
|
|
27
|
-
and InstrumentPrice.objects.filter(
|
|
28
|
-
date=self.trade_date, instrument__in=assets.values("underlying_quote")
|
|
29
|
-
).exists()
|
|
30
|
-
)
|
|
31
|
-
return False
|
|
29
|
+
return len(self.assets) > 0
|
|
32
30
|
|
|
33
31
|
def get_target_portfolio(self) -> Portfolio:
|
|
34
32
|
positions = []
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
asset.date = self.trade_date
|
|
39
|
-
asset.asset_valuation_date = self.trade_date
|
|
33
|
+
for asset in self.assets:
|
|
34
|
+
asset.date = self.value_date
|
|
35
|
+
asset.asset_valuation_date = self.value_date
|
|
40
36
|
positions.append(asset._build_dto())
|
|
41
37
|
return Portfolio(positions=tuple(positions))
|
|
@@ -2,7 +2,7 @@ from typing import Generator
|
|
|
2
2
|
|
|
3
3
|
from wbcompliance.models.risk_management import backend
|
|
4
4
|
from wbcompliance.models.risk_management.dispatch import register
|
|
5
|
-
from wbcompliance.models.risk_management.
|
|
5
|
+
from wbcompliance.models.risk_management.incidents import RiskIncidentType
|
|
6
6
|
from wbcore import serializers as wb_serializers
|
|
7
7
|
from wbfdm.enums import ESGControveryFlag
|
|
8
8
|
from wbfdm.models import Instrument
|
|
@@ -44,7 +44,7 @@ class RuleBackend(ActivePortfolioRelationshipMixin):
|
|
|
44
44
|
return RuleBackendSerializer
|
|
45
45
|
|
|
46
46
|
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
47
|
-
for instrument_id
|
|
47
|
+
for instrument_id in portfolio.positions_map.keys():
|
|
48
48
|
instrument = Instrument.objects.get(id=instrument_id)
|
|
49
49
|
if (
|
|
50
50
|
controversies := Controversy.objects.filter(
|