wbportfolio 1.44.5__py2.py3-none-any.whl → 1.45.1__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 +1 -1
- wbportfolio/admin/asset.py +2 -1
- wbportfolio/admin/custodians.py +1 -0
- wbportfolio/admin/indexes.py +15 -0
- wbportfolio/admin/portfolio.py +12 -7
- wbportfolio/admin/portfolio_relationships.py +1 -0
- wbportfolio/admin/product_groups.py +2 -0
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/reconciliations.py +1 -0
- wbportfolio/admin/registers.py +1 -0
- wbportfolio/admin/roles.py +1 -0
- wbportfolio/admin/transactions/__init__.py +1 -0
- wbportfolio/admin/transactions/claim.py +1 -0
- wbportfolio/admin/transactions/dividends.py +1 -0
- wbportfolio/admin/transactions/fees.py +1 -0
- wbportfolio/admin/transactions/rebalancing.py +26 -0
- wbportfolio/admin/transactions/trades.py +4 -3
- wbportfolio/admin/transactions/transactions.py +1 -0
- wbportfolio/analysis/claims.py +2 -1
- wbportfolio/contrib/company_portfolio/models.py +3 -6
- wbportfolio/contrib/company_portfolio/tests/conftest.py +0 -12
- wbportfolio/contrib/company_portfolio/tests/test_models.py +1 -0
- wbportfolio/defaults/fees/default.py +1 -0
- wbportfolio/factories/__init__.py +1 -7
- wbportfolio/factories/adjustments.py +1 -0
- wbportfolio/factories/assets.py +13 -7
- wbportfolio/factories/claim.py +1 -0
- wbportfolio/factories/custodians.py +1 -0
- wbportfolio/factories/dividends.py +1 -0
- wbportfolio/factories/fees.py +1 -0
- wbportfolio/factories/indexes.py +1 -0
- wbportfolio/factories/portfolio_cash_flow.py +1 -0
- wbportfolio/factories/portfolio_cash_targets.py +1 -0
- wbportfolio/factories/portfolio_swing_pricings.py +1 -0
- wbportfolio/factories/portfolios.py +3 -0
- wbportfolio/factories/product_groups.py +1 -0
- wbportfolio/factories/products.py +1 -0
- wbportfolio/factories/rebalancing.py +23 -0
- wbportfolio/factories/reconciliations.py +1 -0
- wbportfolio/factories/roles.py +1 -0
- wbportfolio/factories/trades.py +1 -0
- wbportfolio/factories/transactions.py +1 -0
- wbportfolio/fdm/tasks.py +1 -0
- wbportfolio/filters/__init__.py +1 -1
- wbportfolio/filters/assets.py +8 -9
- wbportfolio/filters/assets_and_net_new_money_progression.py +1 -0
- wbportfolio/filters/custodians.py +1 -0
- wbportfolio/filters/esg.py +1 -0
- wbportfolio/filters/performances.py +7 -6
- wbportfolio/filters/portfolios.py +21 -1
- wbportfolio/filters/positions.py +1 -0
- wbportfolio/filters/products.py +1 -0
- wbportfolio/filters/roles.py +1 -0
- wbportfolio/filters/signals.py +1 -0
- wbportfolio/filters/transactions/claim.py +1 -0
- wbportfolio/filters/transactions/fees.py +1 -0
- wbportfolio/filters/transactions/trades.py +2 -1
- wbportfolio/filters/transactions/transactions.py +1 -0
- wbportfolio/import_export/backends/ubs/mixin.py +1 -0
- wbportfolio/import_export/backends/wbfdm/adjustment.py +1 -0
- wbportfolio/import_export/handlers/asset_position.py +11 -13
- wbportfolio/import_export/handlers/fees.py +1 -0
- wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -0
- wbportfolio/import_export/handlers/trade.py +1 -0
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/jpmorgan/fees.py +1 -0
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +5 -4
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +1 -0
- wbportfolio/import_export/parsers/leonteq/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/leonteq/equity.py +13 -12
- wbportfolio/import_export/parsers/leonteq/fees.py +1 -0
- wbportfolio/import_export/parsers/leonteq/trade.py +1 -0
- wbportfolio/import_export/parsers/leonteq/valuation.py +1 -0
- wbportfolio/import_export/parsers/natixis/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_equity.py +3 -2
- wbportfolio/import_export/parsers/natixis/d1_fees.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_valuation.py +1 -0
- wbportfolio/import_export/parsers/natixis/equity.py +5 -5
- wbportfolio/import_export/parsers/natixis/trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/utils.py +8 -7
- wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +2 -1
- wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +2 -1
- wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/equity.py +7 -8
- wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/registers.py +2 -1
- wbportfolio/import_export/parsers/societe_generale/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/societe_generale/strategy.py +8 -9
- wbportfolio/import_export/parsers/societe_generale/valuation.py +1 -0
- wbportfolio/import_export/parsers/tellco/equity.py +5 -4
- wbportfolio/import_export/parsers/ubs/api/asset_position.py +15 -14
- wbportfolio/import_export/parsers/ubs/api/fees.py +1 -0
- wbportfolio/import_export/parsers/ubs/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/ubs/equity.py +3 -2
- wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +1 -0
- wbportfolio/import_export/parsers/ubs/valuation.py +1 -0
- wbportfolio/import_export/parsers/vontobel/asset_position.py +19 -19
- wbportfolio/import_export/parsers/vontobel/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +1 -0
- wbportfolio/import_export/parsers/vontobel/management_fees.py +1 -0
- wbportfolio/import_export/parsers/vontobel/performance_fees.py +1 -0
- wbportfolio/import_export/parsers/vontobel/trade.py +1 -0
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +23 -0
- wbportfolio/import_export/resources/assets.py +4 -3
- wbportfolio/import_export/resources/trades.py +1 -0
- wbportfolio/metric/backends/base.py +1 -0
- wbportfolio/metric/backends/portfolio_base.py +1 -0
- wbportfolio/metric/backends/portfolio_esg.py +1 -0
- wbportfolio/metric/tests/test_portfolio_base.py +1 -0
- wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +1 -131
- wbportfolio/migrations/0067_assetposition_unique_asset_position.py +1 -1
- wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +1 -1
- wbportfolio/migrations/0073_remove_product_price_computation_and_more.py +407 -0
- wbportfolio/models/__init__.py +0 -5
- wbportfolio/models/adjustments.py +8 -2
- wbportfolio/models/asset.py +117 -98
- wbportfolio/models/graphs/portfolio.py +161 -0
- wbportfolio/models/graphs/utils.py +83 -0
- wbportfolio/models/indexes.py +2 -13
- wbportfolio/models/mixins/instruments.py +28 -8
- wbportfolio/models/portfolio.py +538 -332
- wbportfolio/models/portfolio_cash_flow.py +1 -0
- wbportfolio/models/portfolio_relationship.py +6 -2
- wbportfolio/models/product_groups.py +3 -2
- wbportfolio/models/products.py +3 -17
- wbportfolio/models/reconciliations/account_reconciliation_lines.py +1 -0
- wbportfolio/models/reconciliations/account_reconciliations.py +1 -0
- wbportfolio/models/registers.py +1 -0
- wbportfolio/models/transactions/__init__.py +1 -0
- wbportfolio/models/transactions/claim.py +8 -8
- wbportfolio/models/transactions/dividends.py +1 -0
- wbportfolio/models/transactions/fees.py +1 -0
- wbportfolio/models/transactions/rebalancing.py +153 -0
- wbportfolio/models/transactions/trade_proposals.py +153 -155
- wbportfolio/models/transactions/trades.py +48 -40
- wbportfolio/models/transactions/transactions.py +6 -12
- wbportfolio/models/utils.py +1 -0
- wbportfolio/pms/analytics/__init__.py +0 -0
- wbportfolio/pms/analytics/portfolio.py +28 -0
- wbportfolio/pms/trading/handler.py +13 -16
- wbportfolio/pms/typing.py +13 -29
- wbportfolio/rebalancing/__init__.py +0 -0
- wbportfolio/rebalancing/base.py +16 -0
- wbportfolio/rebalancing/decorators.py +17 -0
- wbportfolio/rebalancing/models/__init__.py +3 -0
- wbportfolio/rebalancing/models/composite.py +31 -0
- wbportfolio/rebalancing/models/equally_weighted.py +21 -0
- wbportfolio/rebalancing/models/model_portfolio.py +35 -0
- wbportfolio/reports/monthly_position_report.py +1 -1
- wbportfolio/risk_management/backends/accounts.py +7 -6
- wbportfolio/risk_management/backends/controversy_portfolio.py +1 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +1 -0
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +1 -0
- wbportfolio/risk_management/backends/liquidity_risk.py +1 -0
- wbportfolio/risk_management/backends/liquidity_stress_instrument.py +1 -0
- wbportfolio/risk_management/backends/mixins.py +1 -0
- wbportfolio/risk_management/backends/product_integrity.py +6 -1
- wbportfolio/risk_management/backends/stop_loss_instrument.py +1 -0
- wbportfolio/risk_management/backends/stop_loss_portfolio.py +1 -0
- wbportfolio/risk_management/backends/ucits_portfolio.py +12 -5
- wbportfolio/risk_management/tests/test_accounts.py +1 -0
- wbportfolio/risk_management/tests/test_controversy_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_liquidity_risk.py +1 -0
- wbportfolio/risk_management/tests/test_product_integrity.py +1 -0
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +1 -0
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_ucits_portfolio.py +2 -1
- wbportfolio/serializers/__init__.py +5 -5
- wbportfolio/serializers/adjustments.py +1 -0
- wbportfolio/serializers/assets.py +18 -19
- wbportfolio/serializers/custodians.py +1 -0
- wbportfolio/serializers/portfolio_cash_flow.py +1 -0
- wbportfolio/serializers/portfolio_cash_targets.py +1 -0
- wbportfolio/serializers/portfolio_relationship.py +1 -0
- wbportfolio/serializers/portfolio_swing_pricing.py +1 -0
- wbportfolio/serializers/portfolios.py +61 -40
- wbportfolio/serializers/positions.py +1 -0
- wbportfolio/serializers/product_group.py +1 -0
- wbportfolio/serializers/products.py +4 -7
- wbportfolio/serializers/rebalancing.py +57 -0
- wbportfolio/serializers/reconciliations.py +2 -1
- wbportfolio/serializers/registers.py +1 -0
- wbportfolio/serializers/roles.py +1 -0
- wbportfolio/serializers/signals.py +10 -15
- wbportfolio/serializers/transactions/__init__.py +1 -1
- wbportfolio/serializers/transactions/claim.py +1 -0
- wbportfolio/serializers/transactions/fees.py +1 -0
- wbportfolio/serializers/transactions/trade_proposals.py +85 -0
- wbportfolio/serializers/transactions/trades.py +9 -51
- wbportfolio/serializers/transactions/transactions.py +4 -3
- wbportfolio/tasks.py +1 -78
- wbportfolio/tests/conftest.py +6 -13
- wbportfolio/tests/models/test_account_reconciliation.py +2 -0
- wbportfolio/tests/models/test_assets.py +27 -19
- wbportfolio/tests/models/test_customer_trades.py +1 -0
- wbportfolio/tests/models/test_imports.py +5 -1
- wbportfolio/tests/models/test_merge.py +5 -4
- wbportfolio/tests/models/test_portfolio_cash_flow.py +8 -6
- wbportfolio/tests/models/test_portfolios.py +619 -154
- wbportfolio/tests/models/test_product_groups.py +1 -0
- wbportfolio/tests/models/test_products.py +6 -3
- wbportfolio/tests/models/test_roles.py +1 -0
- wbportfolio/tests/models/test_splits.py +1 -0
- wbportfolio/tests/models/transactions/test_claim.py +1 -0
- wbportfolio/tests/models/transactions/test_fees.py +1 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +81 -0
- wbportfolio/tests/models/transactions/test_trades.py +1 -0
- wbportfolio/tests/models/utils.py +1 -0
- wbportfolio/tests/pms/__init__.py +0 -0
- wbportfolio/tests/pms/test_analytics.py +35 -0
- wbportfolio/tests/rebalancing/__init__.py +0 -0
- wbportfolio/tests/rebalancing/test_models.py +127 -0
- wbportfolio/tests/serializers/test_claims.py +1 -0
- wbportfolio/tests/signals.py +1 -7
- wbportfolio/tests/tests.py +2 -0
- wbportfolio/tests/viewsets/test_assets.py +1 -0
- wbportfolio/tests/viewsets/test_performances.py +1 -0
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/tests/viewsets/transactions/test_claims.py +1 -0
- wbportfolio/urls.py +26 -12
- wbportfolio/viewsets/__init__.py +2 -5
- wbportfolio/viewsets/adjustments.py +1 -0
- wbportfolio/viewsets/assets.py +62 -51
- wbportfolio/viewsets/assets_and_net_new_money_progression.py +1 -0
- wbportfolio/viewsets/charts/assets.py +3 -1
- wbportfolio/viewsets/configs/buttons/__init__.py +1 -1
- wbportfolio/viewsets/configs/buttons/assets.py +1 -0
- wbportfolio/viewsets/configs/buttons/custodians.py +1 -0
- wbportfolio/viewsets/configs/buttons/mixins.py +1 -20
- wbportfolio/viewsets/configs/buttons/portfolios.py +90 -76
- wbportfolio/viewsets/configs/buttons/signals.py +1 -0
- wbportfolio/viewsets/configs/buttons/trades.py +1 -0
- wbportfolio/viewsets/configs/display/__init__.py +2 -1
- wbportfolio/viewsets/configs/display/adjustments.py +1 -0
- wbportfolio/viewsets/configs/display/assets.py +7 -6
- wbportfolio/viewsets/configs/display/claim.py +1 -0
- wbportfolio/viewsets/configs/display/portfolios.py +127 -79
- wbportfolio/viewsets/configs/display/product_performance.py +1 -0
- wbportfolio/viewsets/configs/display/rebalancing.py +27 -0
- wbportfolio/viewsets/configs/display/trade_proposals.py +7 -4
- wbportfolio/viewsets/configs/display/trades.py +75 -42
- wbportfolio/viewsets/configs/endpoints/__init__.py +3 -1
- wbportfolio/viewsets/configs/endpoints/assets.py +12 -0
- wbportfolio/viewsets/configs/endpoints/claim.py +1 -0
- wbportfolio/viewsets/configs/endpoints/portfolios.py +23 -7
- wbportfolio/viewsets/configs/endpoints/rebalancing.py +6 -0
- wbportfolio/viewsets/configs/endpoints/reconciliations.py +1 -0
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +1 -0
- wbportfolio/viewsets/configs/endpoints/trades.py +1 -0
- wbportfolio/viewsets/configs/menu/adjustments.py +1 -0
- wbportfolio/viewsets/configs/menu/assets.py +1 -0
- wbportfolio/viewsets/configs/menu/fees.py +1 -0
- wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +1 -0
- wbportfolio/viewsets/configs/menu/portfolios.py +4 -2
- wbportfolio/viewsets/configs/menu/positions.py +1 -0
- wbportfolio/viewsets/configs/menu/roles.py +1 -0
- wbportfolio/viewsets/configs/menu/transactions.py +1 -0
- wbportfolio/viewsets/configs/previews/portfolios.py +1 -6
- wbportfolio/viewsets/configs/titles/__init__.py +1 -1
- wbportfolio/viewsets/configs/titles/assets.py +1 -0
- wbportfolio/viewsets/configs/titles/fees.py +1 -0
- wbportfolio/viewsets/configs/titles/instrument_prices.py +1 -0
- wbportfolio/viewsets/configs/titles/portfolios.py +13 -11
- wbportfolio/viewsets/configs/titles/roles.py +1 -0
- wbportfolio/viewsets/configs/titles/trades.py +1 -0
- wbportfolio/viewsets/configs/titles/transactions.py +1 -0
- wbportfolio/viewsets/custodians.py +1 -0
- wbportfolio/viewsets/esg.py +1 -0
- wbportfolio/viewsets/mixins.py +1 -0
- wbportfolio/viewsets/portfolio_cash_flow.py +1 -0
- wbportfolio/viewsets/portfolio_cash_targets.py +1 -0
- wbportfolio/viewsets/portfolio_relationship.py +1 -0
- wbportfolio/viewsets/portfolio_swing_pricing.py +1 -0
- wbportfolio/viewsets/portfolios.py +228 -61
- wbportfolio/viewsets/positions.py +3 -2
- wbportfolio/viewsets/product_groups.py +1 -0
- wbportfolio/viewsets/product_performance.py +1 -0
- wbportfolio/viewsets/products.py +1 -0
- wbportfolio/viewsets/reconciliations.py +1 -0
- wbportfolio/viewsets/registers.py +1 -0
- wbportfolio/viewsets/roles.py +1 -0
- wbportfolio/viewsets/signals.py +1 -0
- wbportfolio/viewsets/transactions/__init__.py +1 -0
- wbportfolio/viewsets/transactions/claim.py +2 -1
- wbportfolio/viewsets/transactions/fees.py +1 -0
- wbportfolio/viewsets/transactions/mixins.py +1 -0
- wbportfolio/viewsets/transactions/rebalancing.py +31 -0
- wbportfolio/viewsets/transactions/trade_proposals.py +25 -5
- wbportfolio/viewsets/transactions/trades.py +16 -9
- wbportfolio/viewsets/transactions/transactions.py +1 -0
- {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/METADATA +4 -1
- wbportfolio-1.45.1.dist-info/RECORD +521 -0
- wbportfolio/admin/synchronization/__init__.py +0 -2
- wbportfolio/admin/synchronization/admin.py +0 -114
- wbportfolio/admin/synchronization/portfolio_synchronization.py +0 -18
- wbportfolio/admin/synchronization/price_computation.py +0 -21
- wbportfolio/defaults/portfolio/default_rebalancing.py +0 -45
- wbportfolio/factories/pytest_utils.py +0 -121
- wbportfolio/factories/synchronization.py +0 -40
- wbportfolio/models/synchronization/__init__.py +0 -3
- wbportfolio/models/synchronization/portfolio_synchronization.py +0 -292
- wbportfolio/models/synchronization/price_computation.py +0 -200
- wbportfolio/models/synchronization/synchronization.py +0 -188
- wbportfolio/serializers/synchronization.py +0 -18
- wbportfolio/tests/models/test_synchronization.py +0 -617
- wbportfolio/viewsets/synchronization.py +0 -25
- wbportfolio-1.44.5.dist-info/RECORD +0 -508
- /wbportfolio/{defaults/portfolio → models/graphs}/__init__.py +0 -0
- {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/WHEEL +0 -0
- {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.1.dist-info}/licenses/LICENSE +0 -0
wbportfolio/models/asset.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
1
2
|
from datetime import date
|
|
2
3
|
from decimal import Decimal
|
|
3
4
|
|
|
@@ -30,6 +31,7 @@ from wbcore.utils.enum import ChoiceEnum
|
|
|
30
31
|
from wbfdm.models import Classification, ClassificationGroup, Instrument
|
|
31
32
|
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
32
33
|
from wbfdm.signals import add_instrument_to_investable_universe
|
|
34
|
+
|
|
33
35
|
from wbportfolio.import_export.handlers.asset_position import AssetPositionImportHandler
|
|
34
36
|
from wbportfolio.models.portfolio_relationship import (
|
|
35
37
|
InstrumentPortfolioThroughModel,
|
|
@@ -97,31 +99,31 @@ class DefaultAssetPositionManager(models.Manager):
|
|
|
97
99
|
F("applied_adjustment__cumulative_factor") * F("applied_adjustment__factor"), Decimal(1.0)
|
|
98
100
|
),
|
|
99
101
|
shares=F("initial_shares") / F("adjusting_factor"),
|
|
100
|
-
price=Coalesce(F("
|
|
102
|
+
price=Coalesce(F("underlying_quote_price__net_value"), F("initial_price")),
|
|
101
103
|
market_capitalization=ExpressionWrapper(
|
|
102
104
|
Coalesce(
|
|
103
|
-
F("
|
|
104
|
-
F("
|
|
105
|
+
F("underlying_quote_price__market_capitalization_consolidated"),
|
|
106
|
+
F("underlying_quote_price__market_capitalization"),
|
|
105
107
|
),
|
|
106
108
|
output_field=models.DecimalField(),
|
|
107
109
|
),
|
|
108
|
-
beta=ExpressionWrapper(F("
|
|
110
|
+
beta=ExpressionWrapper(F("underlying_quote_price__beta"), output_field=models.DecimalField()),
|
|
109
111
|
correlation=ExpressionWrapper(
|
|
110
|
-
F("
|
|
112
|
+
F("underlying_quote_price__correlation"), output_field=models.DecimalField()
|
|
111
113
|
),
|
|
112
114
|
sharpe_ratio=ExpressionWrapper(
|
|
113
|
-
F("
|
|
115
|
+
F("underlying_quote_price__sharpe_ratio"), output_field=models.DecimalField()
|
|
114
116
|
),
|
|
115
117
|
volume=Coalesce(
|
|
116
|
-
ExpressionWrapper(F("
|
|
118
|
+
ExpressionWrapper(F("underlying_quote_price__volume"), output_field=models.DecimalField()),
|
|
117
119
|
Decimal(0),
|
|
118
120
|
),
|
|
119
121
|
volume_50d=Coalesce(
|
|
120
|
-
ExpressionWrapper(F("
|
|
122
|
+
ExpressionWrapper(F("underlying_quote_price__volume_50d"), output_field=models.DecimalField()),
|
|
121
123
|
Decimal(0),
|
|
122
124
|
),
|
|
123
125
|
volume_200d=Coalesce(
|
|
124
|
-
ExpressionWrapper(F("
|
|
126
|
+
ExpressionWrapper(F("underlying_quote_price__volume_200d"), output_field=models.DecimalField()),
|
|
125
127
|
Decimal(0),
|
|
126
128
|
),
|
|
127
129
|
currency_fx_rate_instrument_to_usd_rate=Case(
|
|
@@ -162,17 +164,6 @@ class DefaultAssetPositionManager(models.Manager):
|
|
|
162
164
|
default=Value(False),
|
|
163
165
|
output_field=models.BooleanField(),
|
|
164
166
|
),
|
|
165
|
-
underlying_security=Case( # Annotate the parent security if exists
|
|
166
|
-
When(underlying_instrument__parent__isnull=False, then=F("underlying_instrument__parent")),
|
|
167
|
-
default=F("underlying_instrument"),
|
|
168
|
-
),
|
|
169
|
-
underlying_security_instrument_type_key=Case( # Annotate the parent security if exists
|
|
170
|
-
When(
|
|
171
|
-
underlying_instrument__parent__isnull=False,
|
|
172
|
-
then=F("underlying_instrument__parent__instrument_type__key"),
|
|
173
|
-
),
|
|
174
|
-
default=F("underlying_instrument__instrument_type__key"),
|
|
175
|
-
),
|
|
176
167
|
)
|
|
177
168
|
|
|
178
169
|
|
|
@@ -191,7 +182,7 @@ class AnalyticalAssetPositionManager(DefaultAssetPositionManager):
|
|
|
191
182
|
previous_price_usd=Subquery(
|
|
192
183
|
qs_default.filter(
|
|
193
184
|
date=OuterRef("last_portfolio_date"),
|
|
194
|
-
|
|
185
|
+
underlying_quote=OuterRef("underlying_quote"),
|
|
195
186
|
portfolio=OuterRef("portfolio"),
|
|
196
187
|
)
|
|
197
188
|
.order_by("-date")
|
|
@@ -373,14 +364,22 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
373
364
|
|
|
374
365
|
underlying_instrument = models.ForeignKey(
|
|
375
366
|
to="wbfdm.Instrument",
|
|
376
|
-
related_name="
|
|
377
|
-
limit_choices_to=models.Q(children__isnull=True),
|
|
367
|
+
related_name="instrument_assets",
|
|
378
368
|
on_delete=models.PROTECT,
|
|
379
369
|
verbose_name="Underlying Instrument",
|
|
380
370
|
help_text="The instrument that is this asset.",
|
|
381
371
|
)
|
|
382
372
|
|
|
383
|
-
|
|
373
|
+
underlying_quote = models.ForeignKey(
|
|
374
|
+
to="wbfdm.Instrument",
|
|
375
|
+
related_name="assets",
|
|
376
|
+
limit_choices_to=models.Q(children__isnull=True),
|
|
377
|
+
on_delete=models.PROTECT,
|
|
378
|
+
verbose_name="Underlying Quote",
|
|
379
|
+
help_text="The quote that is this asset.",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
underlying_quote_price = models.ForeignKey(
|
|
384
383
|
to="wbfdm.InstrumentPrice",
|
|
385
384
|
related_name="assets",
|
|
386
385
|
on_delete=models.SET_NULL,
|
|
@@ -394,42 +393,58 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
394
393
|
analytical_objects = AnalyticalAssetPositionManager()
|
|
395
394
|
unannotated_objects = models.Manager()
|
|
396
395
|
|
|
397
|
-
def
|
|
398
|
-
if not getattr(self, "currency", None):
|
|
399
|
-
self.currency = self.underlying_instrument.currency
|
|
396
|
+
def pre_save(self, create_underlying_quote_price_if_missing: bool = False):
|
|
400
397
|
if not self.asset_valuation_date:
|
|
401
398
|
self.asset_valuation_date = self.date
|
|
402
399
|
|
|
403
|
-
if
|
|
400
|
+
if (
|
|
401
|
+
(not hasattr(self, "underlying_instrument") or not self.underlying_instrument)
|
|
402
|
+
and hasattr(self, "underlying_quote")
|
|
403
|
+
and self.underlying_quote
|
|
404
|
+
):
|
|
405
|
+
self.underlying_instrument = (
|
|
406
|
+
self.underlying_quote.parent if self.underlying_quote.parent else self.underlying_quote
|
|
407
|
+
)
|
|
408
|
+
elif (
|
|
409
|
+
hasattr(self, "underlying_instrument")
|
|
410
|
+
and self.underlying_instrument
|
|
411
|
+
and (not hasattr(self, "underlying_quote") or not self.underlying_quote)
|
|
412
|
+
):
|
|
413
|
+
try:
|
|
414
|
+
self.underlying_quote = self.underlying_instrument.children.get(is_primary=True)
|
|
415
|
+
except:
|
|
416
|
+
self.underlying_quote = self.underlying_instrument
|
|
417
|
+
|
|
418
|
+
if not getattr(self, "currency", None):
|
|
419
|
+
self.currency = self.underlying_quote.currency
|
|
420
|
+
if not self.underlying_quote_price:
|
|
404
421
|
try:
|
|
405
422
|
# We get only the instrument price (and don't create it) because we don't want to create product instrument price on asset position propagation
|
|
406
423
|
# Instead, we deciced to opt for a post_save based system that will assign the missing position price when a price is created
|
|
407
|
-
self.
|
|
408
|
-
calculated=False, instrument=self.
|
|
424
|
+
self.underlying_quote_price = InstrumentPrice.objects.get(
|
|
425
|
+
calculated=False, instrument=self.underlying_quote, date=self.asset_valuation_date
|
|
409
426
|
)
|
|
410
427
|
except InstrumentPrice.DoesNotExist:
|
|
411
428
|
# if we create instrument price automatically, we need to ensure that the position is not estimated and not from a fake portfolio (e.g. JPM morgan root portfolio)
|
|
412
|
-
if
|
|
429
|
+
if create_underlying_quote_price_if_missing and not self.is_estimated:
|
|
413
430
|
net_value = self.initial_price
|
|
414
|
-
# in case the position currency and the linked
|
|
415
|
-
if self.currency != self.
|
|
416
|
-
net_value *= self.currency.convert(
|
|
417
|
-
|
|
418
|
-
)
|
|
419
|
-
self.underlying_instrument_price = InstrumentPrice(
|
|
431
|
+
# in case the position currency and the linked underlying_quote currency don't correspond, we convert the rate accordingly
|
|
432
|
+
if self.currency != self.underlying_quote.currency:
|
|
433
|
+
net_value *= self.currency.convert(self.asset_valuation_date, self.underlying_quote.currency)
|
|
434
|
+
self.underlying_quote_price = InstrumentPrice.objects.create(
|
|
420
435
|
calculated=False,
|
|
421
|
-
instrument=self.
|
|
436
|
+
instrument=self.underlying_quote,
|
|
422
437
|
date=self.asset_valuation_date,
|
|
423
438
|
net_value=net_value,
|
|
424
439
|
import_source=self.import_source, # we set the import source to know where this price is coming from
|
|
425
440
|
)
|
|
426
|
-
self.
|
|
427
|
-
self.
|
|
441
|
+
self.underlying_quote_price.fill_market_capitalization()
|
|
442
|
+
self.underlying_quote_price.save()
|
|
428
443
|
else: # sometime, the asset valuation date does not correspond to a valid market date. In that case, we get the latest valid instrument price for that product
|
|
429
|
-
self.
|
|
444
|
+
self.underlying_quote_price = (
|
|
430
445
|
InstrumentPrice.objects.filter(
|
|
431
446
|
calculated=False,
|
|
432
|
-
instrument=self.
|
|
447
|
+
instrument=self.underlying_quote,
|
|
433
448
|
date__lte=self.asset_valuation_date,
|
|
434
449
|
)
|
|
435
450
|
.order_by("date")
|
|
@@ -437,14 +452,28 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
437
452
|
)
|
|
438
453
|
|
|
439
454
|
if not self.currency_fx_rate_instrument_to_usd or (self.currency_fx_rate_instrument_to_usd.date != self.date):
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
455
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
456
|
+
self.currency_fx_rate_instrument_to_usd = CurrencyFXRates.objects.get(
|
|
457
|
+
date=self.date, currency=self.underlying_quote.currency
|
|
458
|
+
)
|
|
443
459
|
|
|
444
460
|
if not self.currency_fx_rate_portfolio_to_usd or (self.currency_fx_rate_portfolio_to_usd.date != self.date):
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
461
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
462
|
+
self.currency_fx_rate_portfolio_to_usd = CurrencyFXRates.objects.get(
|
|
463
|
+
date=self.date, currency=self.portfolio.currency
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if not self.initial_price and self.underlying_quote_price:
|
|
467
|
+
self.initial_price = self.underlying_quote_price.net_value
|
|
468
|
+
if self.initial_currency_fx_rate is None:
|
|
469
|
+
self.initial_currency_fx_rate = Decimal(1.0)
|
|
470
|
+
if self.currency_fx_rate_portfolio_to_usd and self.currency_fx_rate_instrument_to_usd:
|
|
471
|
+
self.initial_currency_fx_rate = (
|
|
472
|
+
self.currency_fx_rate_portfolio_to_usd.value / self.currency_fx_rate_instrument_to_usd.value
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def save(self, *args, create_underlying_quote_price_if_missing: bool = False, **kwargs):
|
|
476
|
+
self.pre_save(create_underlying_quote_price_if_missing=create_underlying_quote_price_if_missing)
|
|
448
477
|
super().save(*args, **kwargs)
|
|
449
478
|
|
|
450
479
|
class Meta:
|
|
@@ -460,29 +489,27 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
460
489
|
name="%(app_label)s_%(class)s_weekday_constraint",
|
|
461
490
|
),
|
|
462
491
|
models.UniqueConstraint(
|
|
463
|
-
fields=["portfolio", "date", "
|
|
492
|
+
fields=["portfolio", "date", "underlying_quote", "portfolio_created"],
|
|
464
493
|
name="unique_asset_position",
|
|
465
494
|
nulls_distinct=False,
|
|
466
495
|
),
|
|
467
496
|
]
|
|
468
497
|
|
|
469
498
|
def __str__(self):
|
|
470
|
-
return f"{self.initial_price} - {self.initial_shares} ({self.date}) ({str(self.
|
|
499
|
+
return f"{self.initial_price} - {self.initial_shares} ({self.date}) ({str(self.underlying_quote)})"
|
|
471
500
|
|
|
472
501
|
def set_weighting(self, new_weighting: Decimal):
|
|
473
502
|
# Use this method to set the new weighting and ensure that the relative shares are updated accordingly
|
|
474
503
|
self.weighting = new_weighting
|
|
475
504
|
if self.initial_shares is not None:
|
|
476
505
|
if self.weighting == 0 or self.initial_shares == 0:
|
|
477
|
-
self.initial_shares = new_weighting * self.
|
|
506
|
+
self.initial_shares = new_weighting * self.get_portfolio_total_asset_value()
|
|
478
507
|
else:
|
|
479
508
|
self.initial_shares = (new_weighting / self.weighting) * self.initial_shares
|
|
480
509
|
self.save()
|
|
481
510
|
|
|
482
|
-
def
|
|
483
|
-
return self.portfolio.
|
|
484
|
-
"s"
|
|
485
|
-
] or Decimal(0.0)
|
|
511
|
+
def get_portfolio_total_asset_value(self) -> Decimal:
|
|
512
|
+
return self.portfolio.get_total_asset_value(self.date)
|
|
486
513
|
|
|
487
514
|
def _build_dto(self, new_weight: Decimal = None) -> PositionDTO:
|
|
488
515
|
"""
|
|
@@ -491,23 +518,23 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
491
518
|
DTO position object
|
|
492
519
|
"""
|
|
493
520
|
return PositionDTO(
|
|
494
|
-
underlying_instrument=self.
|
|
521
|
+
underlying_instrument=self.underlying_quote.id,
|
|
495
522
|
weighting=self.weighting if new_weight is None else new_weight,
|
|
496
523
|
shares=self._shares,
|
|
497
524
|
date=self.date,
|
|
498
525
|
asset_valuation_date=self.asset_valuation_date,
|
|
499
|
-
instrument_type=self.
|
|
500
|
-
currency=self.
|
|
501
|
-
country=self.
|
|
502
|
-
is_cash=self.
|
|
526
|
+
instrument_type=self.underlying_quote.instrument_type.id,
|
|
527
|
+
currency=self.underlying_quote.currency.id,
|
|
528
|
+
country=self.underlying_quote.country.id if self.underlying_quote.country else None,
|
|
529
|
+
is_cash=self.underlying_quote.is_cash,
|
|
503
530
|
primary_classification=(
|
|
504
|
-
self.
|
|
505
|
-
if self.
|
|
531
|
+
self.underlying_quote.primary_classification.id
|
|
532
|
+
if self.underlying_quote.primary_classification
|
|
506
533
|
else None
|
|
507
534
|
),
|
|
508
535
|
favorite_classification=(
|
|
509
|
-
self.
|
|
510
|
-
if self.
|
|
536
|
+
self.underlying_quote.favorite_classification.id
|
|
537
|
+
if self.underlying_quote.favorite_classification
|
|
511
538
|
else None
|
|
512
539
|
),
|
|
513
540
|
market_capitalization_usd=self._market_capitalization_usd,
|
|
@@ -538,21 +565,17 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
538
565
|
@cached_property
|
|
539
566
|
@admin.display(description="Market Capitalization")
|
|
540
567
|
def _market_capitalization(self) -> float:
|
|
541
|
-
return
|
|
542
|
-
self.underlying_instrument_price.market_capitalization_consolidated
|
|
543
|
-
if self.underlying_instrument_price
|
|
544
|
-
else None
|
|
545
|
-
)
|
|
568
|
+
return self.underlying_quote_price.market_capitalization_consolidated if self.underlying_quote_price else None
|
|
546
569
|
|
|
547
570
|
@cached_property
|
|
548
571
|
@admin.display(description="Volume 50d")
|
|
549
572
|
def _volume_50d(self) -> float:
|
|
550
|
-
return self.
|
|
573
|
+
return self.underlying_quote_price.volume_50d if self.underlying_quote_price else None
|
|
551
574
|
|
|
552
575
|
@cached_property
|
|
553
576
|
@admin.display(description="Price (Instrument)")
|
|
554
577
|
def _price(self) -> Decimal:
|
|
555
|
-
return self.
|
|
578
|
+
return self.underlying_quote_price.net_value if self.underlying_quote_price else self.initial_price
|
|
556
579
|
|
|
557
580
|
@cached_property
|
|
558
581
|
@admin.display(description="FX rate")
|
|
@@ -575,14 +598,17 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
575
598
|
return self._price * self._shares
|
|
576
599
|
return Decimal(0)
|
|
577
600
|
|
|
601
|
+
@cached_property
|
|
602
|
+
def fx_usd(self) -> Decimal:
|
|
603
|
+
if self.currency_fx_rate_instrument_to_usd:
|
|
604
|
+
return self.currency_fx_rate_instrument_to_usd.value
|
|
605
|
+
return Decimal(1.0)
|
|
606
|
+
|
|
578
607
|
@cached_property
|
|
579
608
|
@admin.display(description="Total Value (USD)")
|
|
580
609
|
def _total_value_fx_usd(self) -> Decimal:
|
|
581
|
-
fx_rate = (
|
|
582
|
-
self.currency_fx_rate_instrument_to_usd.value if self.currency_fx_rate_instrument_to_usd else Decimal(1.0)
|
|
583
|
-
)
|
|
584
610
|
if self._shares is not None:
|
|
585
|
-
return self._price * self._shares *
|
|
611
|
+
return self._price * self._shares * self.fx_usd
|
|
586
612
|
return Decimal(0)
|
|
587
613
|
|
|
588
614
|
@cached_property
|
|
@@ -609,23 +635,15 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
609
635
|
@cached_property
|
|
610
636
|
@admin.display(description="Market Capitalization (USD)")
|
|
611
637
|
def _market_capitalization_usd(self) -> float:
|
|
612
|
-
if self._market_capitalization is not None
|
|
613
|
-
return self._market_capitalization
|
|
638
|
+
if self._market_capitalization is not None:
|
|
639
|
+
return self._market_capitalization * float(self.fx_usd)
|
|
614
640
|
return 0.0
|
|
615
641
|
|
|
616
642
|
@cached_property
|
|
617
643
|
@admin.display(description="Volume (USD)")
|
|
618
644
|
def _volume_usd(self) -> float:
|
|
619
|
-
if
|
|
620
|
-
self._price_fx_portfolio
|
|
621
|
-
and self.currency_fx_rate_instrument_to_usd.value is not None
|
|
622
|
-
and self._volume_50d is not None
|
|
623
|
-
):
|
|
624
|
-
return (
|
|
625
|
-
float(self._price_fx_portfolio)
|
|
626
|
-
* float(self.currency_fx_rate_instrument_to_usd.value)
|
|
627
|
-
* self._volume_50d
|
|
628
|
-
)
|
|
645
|
+
if self._price_fx_portfolio is not None and self._volume_50d is not None:
|
|
646
|
+
return float(self._price_fx_portfolio) * float(self.fx_usd) * self._volume_50d
|
|
629
647
|
return 0.0
|
|
630
648
|
|
|
631
649
|
@classmethod
|
|
@@ -667,8 +685,8 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
667
685
|
qs.annotate(
|
|
668
686
|
underlying_security_instrument_type_name_repr=Case( # Annotate the parent security if exists
|
|
669
687
|
When(
|
|
670
|
-
|
|
671
|
-
then=F("
|
|
688
|
+
underlying_instrument__isnull=False,
|
|
689
|
+
then=F("underlying_instrument__instrument_type__name_repr"),
|
|
672
690
|
),
|
|
673
691
|
default=F("underlying_instrument__instrument_type__name_repr"),
|
|
674
692
|
),
|
|
@@ -824,7 +842,7 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
824
842
|
)
|
|
825
843
|
return (
|
|
826
844
|
Instrument.annotated_objects.filter(is_investable_universe=True)
|
|
827
|
-
.annotate(has_position=Exists(asset_positions.filter(
|
|
845
|
+
.annotate(has_position=Exists(asset_positions.filter(underlying_quote=OuterRef("id"))))
|
|
828
846
|
.filter(has_position=True)
|
|
829
847
|
)
|
|
830
848
|
|
|
@@ -834,12 +852,9 @@ def post_instrument_price_creation(sender, instance, created, raw, **kwargs):
|
|
|
834
852
|
if not raw and created and not instance.calculated:
|
|
835
853
|
AssetPosition.objects.filter(
|
|
836
854
|
Q(asset_valuation_date=instance.date)
|
|
837
|
-
& Q(
|
|
838
|
-
& (
|
|
839
|
-
|
|
840
|
-
| ~Q(asset_valuation_date=F("underlying_instrument_price__date"))
|
|
841
|
-
)
|
|
842
|
-
).update(underlying_instrument_price=instance)
|
|
855
|
+
& Q(underlying_quote=instance.instrument)
|
|
856
|
+
& (Q(underlying_quote_price__isnull=True) | ~Q(asset_valuation_date=F("underlying_quote_price__date")))
|
|
857
|
+
).update(underlying_quote_price=instance)
|
|
843
858
|
|
|
844
859
|
|
|
845
860
|
@receiver(pre_merge, sender="wbfdm.Instrument")
|
|
@@ -851,7 +866,11 @@ def pre_merge_instrument(sender: models.Model, merged_object: Instrument, main_o
|
|
|
851
866
|
new_price=InstrumentPrice.objects.filter(
|
|
852
867
|
instrument=main_object, date=OuterRef("date"), calculated=False
|
|
853
868
|
).values("id")[:1]
|
|
854
|
-
).update(
|
|
869
|
+
).update(
|
|
870
|
+
underlying_quote=main_object,
|
|
871
|
+
underlying_instrument=main_object.parent if main_object.parent else main_object,
|
|
872
|
+
underlying_quote_price=F("new_price"),
|
|
873
|
+
)
|
|
855
874
|
|
|
856
875
|
|
|
857
876
|
@receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
|
|
@@ -863,7 +882,7 @@ def add_instrument_to_investable_universe(sender: models.Model, **kwargs) -> lis
|
|
|
863
882
|
(
|
|
864
883
|
Instrument.objects.annotate(
|
|
865
884
|
assets_exists=Exists(
|
|
866
|
-
AssetPosition.objects.filter(portfolio__is_tracked=True,
|
|
885
|
+
AssetPosition.objects.filter(portfolio__is_tracked=True, underlying_quote=OuterRef("pk"))
|
|
867
886
|
)
|
|
868
887
|
).filter(Q(assets_exists=True) | Q(portfolios__isnull=False))
|
|
869
888
|
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import textwrap
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
import networkx as nx
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
import pydot
|
|
8
|
+
from django.db.models import Q
|
|
9
|
+
|
|
10
|
+
from wbportfolio.models import Portfolio, PortfolioPortfolioThroughModel
|
|
11
|
+
|
|
12
|
+
from .utils import networkx_graph_to_plotly
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PortfolioGraph:
|
|
16
|
+
def __init__(self, portfolio: Portfolio, val_date: date, **graph_kwargs):
|
|
17
|
+
self.graph = pydot.Dot("Portfolio Tree", strict=True, **graph_kwargs)
|
|
18
|
+
self.base_portfolio = portfolio
|
|
19
|
+
self.discovered_portfolios = set()
|
|
20
|
+
self.val_date = val_date
|
|
21
|
+
self._extend_portfolio_graph(portfolio)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _convert_to_multilines(cls, name: str) -> str:
|
|
25
|
+
lines = textwrap.wrap(name, 30)
|
|
26
|
+
return "\n ".join(lines)
|
|
27
|
+
|
|
28
|
+
def _extend_parent_portfolios_to_graph(self, portfolio):
|
|
29
|
+
for parent_portfolio, weighting in portfolio.get_parent_portfolios(self.val_date):
|
|
30
|
+
if parent_portfolio.assets.filter(date=self.val_date).exists():
|
|
31
|
+
self.graph.add_node(
|
|
32
|
+
pydot.Node(
|
|
33
|
+
str(parent_portfolio.id),
|
|
34
|
+
label=self._convert_to_multilines(str(parent_portfolio)),
|
|
35
|
+
**self._get_node_kwargs(parent_portfolio),
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
self.graph.del_edge((str(portfolio.id), str(parent_portfolio.id)))
|
|
39
|
+
self.graph.add_edge(
|
|
40
|
+
pydot.Edge(
|
|
41
|
+
str(portfolio.id),
|
|
42
|
+
str(parent_portfolio.id),
|
|
43
|
+
label=f"Invest in ({weighting:.2%})",
|
|
44
|
+
style="dashed",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
# composition_edges.append((str(portfolio.id), str(parent_portfolio.id)))
|
|
48
|
+
self._extend_parent_portfolios_to_graph(parent_portfolio)
|
|
49
|
+
self._extend_portfolio_graph(parent_portfolio)
|
|
50
|
+
|
|
51
|
+
def _extend_child_portfolios_to_graph(self, portfolio):
|
|
52
|
+
for child_portfolio in portfolio.get_child_portfolios(self.val_date):
|
|
53
|
+
self.graph.add_node(
|
|
54
|
+
pydot.Node(
|
|
55
|
+
str(child_portfolio.id),
|
|
56
|
+
label=self._convert_to_multilines(str(child_portfolio)),
|
|
57
|
+
**self._get_node_kwargs(child_portfolio),
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
# we add this edge only if the opposite relationship is not already added
|
|
61
|
+
graph_edge = pydot.Edge(str(child_portfolio.id), str(portfolio.id), label="implements", style="dashed")
|
|
62
|
+
if (graph_edge.get_source(), graph_edge.get_destination()) not in self.graph.obj_dict["edges"]:
|
|
63
|
+
self.graph.add_edge(graph_edge)
|
|
64
|
+
# composition_edges.append((str(child_portfolio.id), str(portfolio.id)))
|
|
65
|
+
self._extend_child_portfolios_to_graph(child_portfolio)
|
|
66
|
+
|
|
67
|
+
def _get_node_kwargs(self, portfolio):
|
|
68
|
+
node_args = {
|
|
69
|
+
"shape": "circle",
|
|
70
|
+
"orientation": "45",
|
|
71
|
+
}
|
|
72
|
+
if portfolio == self.base_portfolio:
|
|
73
|
+
node_args.update({"style": "filled", "fillcolor": "lightgrey"})
|
|
74
|
+
else:
|
|
75
|
+
node_args.update(
|
|
76
|
+
{
|
|
77
|
+
"style": "solid",
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
return node_args
|
|
81
|
+
|
|
82
|
+
def _extend_portfolio_graph(self, portfolio):
|
|
83
|
+
self.graph.add_node(
|
|
84
|
+
pydot.Node(
|
|
85
|
+
str(portfolio.id),
|
|
86
|
+
label=self._convert_to_multilines(str(portfolio)),
|
|
87
|
+
**self._get_node_kwargs(portfolio),
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self._extend_child_portfolios_to_graph(portfolio)
|
|
92
|
+
self._extend_parent_portfolios_to_graph(portfolio)
|
|
93
|
+
|
|
94
|
+
# composition_edges = []
|
|
95
|
+
# if composition_edges:
|
|
96
|
+
# with self.graph.subgraph(label=f'composition_{portfolio.id}') as a:
|
|
97
|
+
# a.edges(composition_edges)
|
|
98
|
+
# a.attr(color='blue')
|
|
99
|
+
# a.attr(label='Portfolio Composition')
|
|
100
|
+
self.discovered_portfolios.add(portfolio)
|
|
101
|
+
|
|
102
|
+
for rel in PortfolioPortfolioThroughModel.objects.filter(
|
|
103
|
+
Q(portfolio=portfolio) | Q(dependency_portfolio=portfolio)
|
|
104
|
+
):
|
|
105
|
+
self.graph.add_node(
|
|
106
|
+
pydot.Node(
|
|
107
|
+
str(rel.portfolio.id),
|
|
108
|
+
label=self._convert_to_multilines(str(rel.portfolio)),
|
|
109
|
+
**self._get_node_kwargs(rel.portfolio),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
self.graph.add_node(
|
|
113
|
+
pydot.Node(
|
|
114
|
+
str(rel.dependency_portfolio.id),
|
|
115
|
+
label=self._convert_to_multilines(str(rel.dependency_portfolio)),
|
|
116
|
+
**self._get_node_kwargs(rel.dependency_portfolio),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
label = PortfolioPortfolioThroughModel.Type[rel.type].label
|
|
120
|
+
if rel.dependency_portfolio.is_composition:
|
|
121
|
+
label += " (Composition)"
|
|
122
|
+
|
|
123
|
+
if rel.portfolio.is_lookthrough and rel.type == PortfolioPortfolioThroughModel.Type.PRIMARY:
|
|
124
|
+
self.graph.add_edge(
|
|
125
|
+
pydot.Edge(
|
|
126
|
+
str(rel.portfolio.id), str(rel.dependency_portfolio.id), label="Look-Through", style="dotted"
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
self.graph.add_edge(
|
|
131
|
+
pydot.Edge(
|
|
132
|
+
str(rel.portfolio.id),
|
|
133
|
+
str(rel.dependency_portfolio.id),
|
|
134
|
+
label=label,
|
|
135
|
+
style="bold",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
if rel.dependency_portfolio not in self.discovered_portfolios:
|
|
139
|
+
self._extend_portfolio_graph(rel.dependency_portfolio)
|
|
140
|
+
if rel.portfolio not in self.discovered_portfolios:
|
|
141
|
+
self._extend_portfolio_graph(rel.portfolio)
|
|
142
|
+
|
|
143
|
+
def to_string(self) -> str:
|
|
144
|
+
return self.graph.to_string()
|
|
145
|
+
|
|
146
|
+
def to_networkx(self) -> nx.Graph:
|
|
147
|
+
return nx.drawing.nx_pydot.from_pydot(self.graph)
|
|
148
|
+
|
|
149
|
+
def to_plotly(self, **kwargs) -> go.Figure:
|
|
150
|
+
node_labels = {
|
|
151
|
+
node.get_name(): node.obj_dict["attributes"].get("label", node.get_name())
|
|
152
|
+
for node in self.graph.get_node_list()
|
|
153
|
+
}
|
|
154
|
+
return networkx_graph_to_plotly(self.to_networkx(), labels=node_labels, **kwargs)
|
|
155
|
+
|
|
156
|
+
def to_svg(self) -> str:
|
|
157
|
+
svg = self.graph.create_svg().decode("utf-8")
|
|
158
|
+
svg_matches = re.findall(r"<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>", svg, flags=re.DOTALL)
|
|
159
|
+
if svg_matches:
|
|
160
|
+
return svg_matches[0]
|
|
161
|
+
return svg
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
from networkx.drawing.nx_agraph import graphviz_layout
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def reformat_graph_layout(G, layout):
|
|
7
|
+
"""
|
|
8
|
+
this method provide positions based on layout algorithm
|
|
9
|
+
:param G:
|
|
10
|
+
:param layout:
|
|
11
|
+
:return:
|
|
12
|
+
"""
|
|
13
|
+
if layout == "graphviz":
|
|
14
|
+
positions = graphviz_layout(G)
|
|
15
|
+
elif layout == "spring":
|
|
16
|
+
positions = nx.fruchterman_reingold_layout(G, k=0.5, iterations=1000)
|
|
17
|
+
elif layout == "spectral":
|
|
18
|
+
positions = nx.spectral_layout(G, scale=0.1)
|
|
19
|
+
elif layout == "random":
|
|
20
|
+
positions = nx.random_layout(G)
|
|
21
|
+
else:
|
|
22
|
+
raise Exception("please specify the layout from graphviz, spring, spectral or random")
|
|
23
|
+
|
|
24
|
+
return positions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def networkx_graph_to_plotly(
|
|
28
|
+
G: nx.Graph,
|
|
29
|
+
labels: dict[str, str] | None = None,
|
|
30
|
+
node_size: int = 10,
|
|
31
|
+
edge_weight: int = 1,
|
|
32
|
+
edge_color: str = "black",
|
|
33
|
+
layout: str = "graphviz",
|
|
34
|
+
title: str = "",
|
|
35
|
+
) -> go.Figure:
|
|
36
|
+
"""
|
|
37
|
+
Visualize a NetworkX graph using Plotly.
|
|
38
|
+
"""
|
|
39
|
+
positions = reformat_graph_layout(G, layout)
|
|
40
|
+
if not labels:
|
|
41
|
+
labels = {}
|
|
42
|
+
# Initialize edge traces
|
|
43
|
+
edge_traces = []
|
|
44
|
+
for edge in G.edges():
|
|
45
|
+
x0, y0 = positions[edge[0]]
|
|
46
|
+
x1, y1 = positions[edge[1]]
|
|
47
|
+
|
|
48
|
+
edge_trace = go.Scatter(
|
|
49
|
+
x=[x0, x1], y=[y0, y1], line=dict(width=edge_weight, color=edge_color), hoverinfo="none", mode="lines"
|
|
50
|
+
)
|
|
51
|
+
edge_traces.append(edge_trace)
|
|
52
|
+
|
|
53
|
+
# Initialize node trace
|
|
54
|
+
node_x, node_y, node_colors, node_labels = [], [], [], []
|
|
55
|
+
for node in G.nodes():
|
|
56
|
+
x, y = positions[node]
|
|
57
|
+
node_x.append(x)
|
|
58
|
+
node_y.append(y)
|
|
59
|
+
node_labels.append(labels.get(node, node))
|
|
60
|
+
node_colors.append(len(list(G.neighbors(node)))) # Color based on degree
|
|
61
|
+
|
|
62
|
+
node_trace = go.Scatter(
|
|
63
|
+
x=node_x,
|
|
64
|
+
y=node_y,
|
|
65
|
+
text=node_labels,
|
|
66
|
+
mode="markers+text",
|
|
67
|
+
textfont=dict(family="Calibri (Body)", size=15, color="grey"),
|
|
68
|
+
marker=dict(
|
|
69
|
+
size=node_size,
|
|
70
|
+
color=node_colors,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Assemble the figure
|
|
75
|
+
fig = go.Figure(
|
|
76
|
+
data=edge_traces + [node_trace],
|
|
77
|
+
layout=go.Layout(
|
|
78
|
+
showlegend=False,
|
|
79
|
+
template="plotly_white",
|
|
80
|
+
margin=dict(l=50, r=50, t=0, b=40),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
return fig
|