wbportfolio 2.2.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/__init__.py +1 -0
- wbportfolio/admin/__init__.py +12 -0
- wbportfolio/admin/asset.py +47 -0
- wbportfolio/admin/custodians.py +9 -0
- wbportfolio/admin/portfolio.py +127 -0
- wbportfolio/admin/portfolio_relationships.py +22 -0
- wbportfolio/admin/product_groups.py +42 -0
- wbportfolio/admin/products.py +80 -0
- wbportfolio/admin/reconciliations.py +14 -0
- wbportfolio/admin/registers.py +17 -0
- wbportfolio/admin/roles.py +19 -0
- wbportfolio/admin/synchronization/__init__.py +2 -0
- wbportfolio/admin/synchronization/admin.py +114 -0
- wbportfolio/admin/synchronization/portfolio_synchronization.py +18 -0
- wbportfolio/admin/synchronization/price_computation.py +21 -0
- wbportfolio/admin/transactions/__init__.py +5 -0
- wbportfolio/admin/transactions/claim.py +16 -0
- wbportfolio/admin/transactions/dividends.py +14 -0
- wbportfolio/admin/transactions/fees.py +35 -0
- wbportfolio/admin/transactions/trades.py +49 -0
- wbportfolio/admin/transactions/transactions.py +37 -0
- wbportfolio/analysis/__init__.py +0 -0
- wbportfolio/analysis/claims.py +235 -0
- wbportfolio/apps.py +5 -0
- wbportfolio/contrib/__init__.py +0 -0
- wbportfolio/contrib/company_portfolio/__init__.py +0 -0
- wbportfolio/contrib/company_portfolio/admin.py +28 -0
- wbportfolio/contrib/company_portfolio/apps.py +29 -0
- wbportfolio/contrib/company_portfolio/configs/__init__.py +3 -0
- wbportfolio/contrib/company_portfolio/configs/display.py +182 -0
- wbportfolio/contrib/company_portfolio/configs/endpoints.py +34 -0
- wbportfolio/contrib/company_portfolio/configs/previews.py +37 -0
- wbportfolio/contrib/company_portfolio/constants.py +1 -0
- wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py +87 -0
- wbportfolio/contrib/company_portfolio/factories.py +32 -0
- wbportfolio/contrib/company_portfolio/filters.py +127 -0
- wbportfolio/contrib/company_portfolio/management.py +19 -0
- wbportfolio/contrib/company_portfolio/migrations/0001_initial.py +214 -0
- wbportfolio/contrib/company_portfolio/migrations/__init__.py +0 -0
- wbportfolio/contrib/company_portfolio/models.py +334 -0
- wbportfolio/contrib/company_portfolio/scripts.py +76 -0
- wbportfolio/contrib/company_portfolio/serializers.py +303 -0
- wbportfolio/contrib/company_portfolio/tasks.py +19 -0
- wbportfolio/contrib/company_portfolio/tests/__init__.py +0 -0
- wbportfolio/contrib/company_portfolio/tests/conftest.py +161 -0
- wbportfolio/contrib/company_portfolio/tests/test_models.py +161 -0
- wbportfolio/contrib/company_portfolio/urls.py +29 -0
- wbportfolio/contrib/company_portfolio/viewsets.py +195 -0
- wbportfolio/defaults/__init__.py +0 -0
- wbportfolio/defaults/fees/__init__.py +0 -0
- wbportfolio/defaults/fees/default.py +92 -0
- wbportfolio/defaults/portfolio/__init__.py +0 -0
- wbportfolio/defaults/portfolio/default_rebalancing.py +45 -0
- wbportfolio/dynamic_preferences_registry.py +58 -0
- wbportfolio/factories/__init__.py +35 -0
- wbportfolio/factories/adjustments.py +17 -0
- wbportfolio/factories/assets.py +75 -0
- wbportfolio/factories/claim.py +39 -0
- wbportfolio/factories/custodians.py +11 -0
- wbportfolio/factories/dividends.py +14 -0
- wbportfolio/factories/fees.py +15 -0
- wbportfolio/factories/indexes.py +17 -0
- wbportfolio/factories/portfolio_cash_flow.py +20 -0
- wbportfolio/factories/portfolio_cash_targets.py +15 -0
- wbportfolio/factories/portfolio_swing_pricings.py +15 -0
- wbportfolio/factories/portfolios.py +59 -0
- wbportfolio/factories/product_groups.py +28 -0
- wbportfolio/factories/products.py +56 -0
- wbportfolio/factories/pytest_utils.py +121 -0
- wbportfolio/factories/reconciliations.py +23 -0
- wbportfolio/factories/roles.py +20 -0
- wbportfolio/factories/synchronization.py +40 -0
- wbportfolio/factories/trades.py +35 -0
- wbportfolio/factories/transactions.py +21 -0
- wbportfolio/fdm/__init__.py +0 -0
- wbportfolio/fdm/tasks.py +12 -0
- wbportfolio/filters/__init__.py +32 -0
- wbportfolio/filters/assets.py +485 -0
- wbportfolio/filters/assets_and_net_new_money_progression.py +42 -0
- wbportfolio/filters/custodians.py +10 -0
- wbportfolio/filters/esg.py +22 -0
- wbportfolio/filters/performances.py +171 -0
- wbportfolio/filters/portfolios.py +24 -0
- wbportfolio/filters/positions.py +178 -0
- wbportfolio/filters/products.py +157 -0
- wbportfolio/filters/roles.py +26 -0
- wbportfolio/filters/signals.py +92 -0
- wbportfolio/filters/transactions/__init__.py +20 -0
- wbportfolio/filters/transactions/claim.py +394 -0
- wbportfolio/filters/transactions/fees.py +66 -0
- wbportfolio/filters/transactions/trades.py +224 -0
- wbportfolio/filters/transactions/transactions.py +98 -0
- wbportfolio/import_export/__init__.py +0 -0
- wbportfolio/import_export/backends/__init__.py +2 -0
- wbportfolio/import_export/backends/ubs/__init__.py +3 -0
- wbportfolio/import_export/backends/ubs/asset_position.py +45 -0
- wbportfolio/import_export/backends/ubs/fees.py +63 -0
- wbportfolio/import_export/backends/ubs/instrument_price.py +44 -0
- wbportfolio/import_export/backends/ubs/mixin.py +15 -0
- wbportfolio/import_export/backends/utils.py +58 -0
- wbportfolio/import_export/backends/wbfdm/__init__.py +2 -0
- wbportfolio/import_export/backends/wbfdm/adjustment.py +50 -0
- wbportfolio/import_export/backends/wbfdm/dividend.py +16 -0
- wbportfolio/import_export/backends/wbfdm/mixin.py +15 -0
- wbportfolio/import_export/handlers/__init__.py +0 -0
- wbportfolio/import_export/handlers/adjustment.py +39 -0
- wbportfolio/import_export/handlers/asset_position.py +167 -0
- wbportfolio/import_export/handlers/dividend.py +80 -0
- wbportfolio/import_export/handlers/fees.py +58 -0
- wbportfolio/import_export/handlers/portfolio_cash_flow.py +57 -0
- wbportfolio/import_export/handlers/register.py +43 -0
- wbportfolio/import_export/handlers/trade.py +191 -0
- wbportfolio/import_export/parsers/__init__.py +0 -0
- wbportfolio/import_export/parsers/default_mapping.py +30 -0
- wbportfolio/import_export/parsers/jpmorgan/__init__.py +0 -0
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +63 -0
- wbportfolio/import_export/parsers/jpmorgan/fees.py +64 -0
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +116 -0
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +41 -0
- wbportfolio/import_export/parsers/leonteq/__init__.py +0 -0
- wbportfolio/import_export/parsers/leonteq/customer_trade.py +47 -0
- wbportfolio/import_export/parsers/leonteq/equity.py +81 -0
- wbportfolio/import_export/parsers/leonteq/fees.py +70 -0
- wbportfolio/import_export/parsers/leonteq/trade.py +94 -0
- wbportfolio/import_export/parsers/leonteq/valuation.py +39 -0
- wbportfolio/import_export/parsers/natixis/__init__.py +0 -0
- wbportfolio/import_export/parsers/natixis/customer_trade.py +62 -0
- wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +66 -0
- wbportfolio/import_export/parsers/natixis/d1_equity.py +80 -0
- wbportfolio/import_export/parsers/natixis/d1_fees.py +58 -0
- wbportfolio/import_export/parsers/natixis/d1_trade.py +70 -0
- wbportfolio/import_export/parsers/natixis/d1_valuation.py +41 -0
- wbportfolio/import_export/parsers/natixis/dividend.py +53 -0
- wbportfolio/import_export/parsers/natixis/equity.py +60 -0
- wbportfolio/import_export/parsers/natixis/fees.py +53 -0
- wbportfolio/import_export/parsers/natixis/trade.py +63 -0
- wbportfolio/import_export/parsers/natixis/utils.py +76 -0
- wbportfolio/import_export/parsers/natixis/valuation.py +46 -0
- wbportfolio/import_export/parsers/refinitiv/__init__.py +0 -0
- wbportfolio/import_export/parsers/refinitiv/adjustment.py +24 -0
- wbportfolio/import_export/parsers/sg_lux/__init__.py +0 -0
- wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +70 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade.py +75 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +140 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +80 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +57 -0
- wbportfolio/import_export/parsers/sg_lux/equity.py +137 -0
- wbportfolio/import_export/parsers/sg_lux/fees.py +56 -0
- wbportfolio/import_export/parsers/sg_lux/perf_fees.py +51 -0
- wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +29 -0
- wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +36 -0
- wbportfolio/import_export/parsers/sg_lux/registers.py +210 -0
- wbportfolio/import_export/parsers/sg_lux/sylk.py +248 -0
- wbportfolio/import_export/parsers/sg_lux/utils.py +36 -0
- wbportfolio/import_export/parsers/sg_lux/valuation.py +53 -0
- wbportfolio/import_export/parsers/societe_generale/__init__.py +0 -0
- wbportfolio/import_export/parsers/societe_generale/customer_trade.py +54 -0
- wbportfolio/import_export/parsers/societe_generale/strategy.py +94 -0
- wbportfolio/import_export/parsers/societe_generale/valuation.py +37 -0
- wbportfolio/import_export/parsers/tellco/__init__.py +0 -0
- wbportfolio/import_export/parsers/tellco/customer_trade.py +64 -0
- wbportfolio/import_export/parsers/tellco/equity.py +86 -0
- wbportfolio/import_export/parsers/tellco/valuation.py +52 -0
- wbportfolio/import_export/parsers/ubs/__init__.py +0 -0
- wbportfolio/import_export/parsers/ubs/api/__init__.py +0 -0
- wbportfolio/import_export/parsers/ubs/api/asset_position.py +106 -0
- wbportfolio/import_export/parsers/ubs/api/fees.py +31 -0
- wbportfolio/import_export/parsers/ubs/api/instrument_price.py +20 -0
- wbportfolio/import_export/parsers/ubs/api/utils.py +0 -0
- wbportfolio/import_export/parsers/ubs/customer_trade.py +60 -0
- wbportfolio/import_export/parsers/ubs/equity.py +97 -0
- wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +67 -0
- wbportfolio/import_export/parsers/ubs/valuation.py +52 -0
- wbportfolio/import_export/parsers/vontobel/__init__.py +0 -0
- wbportfolio/import_export/parsers/vontobel/asset_position.py +97 -0
- wbportfolio/import_export/parsers/vontobel/customer_trade.py +54 -0
- wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +40 -0
- wbportfolio/import_export/parsers/vontobel/instrument.py +34 -0
- wbportfolio/import_export/parsers/vontobel/management_fees.py +86 -0
- wbportfolio/import_export/parsers/vontobel/performance_fees.py +35 -0
- wbportfolio/import_export/parsers/vontobel/trade.py +38 -0
- wbportfolio/import_export/parsers/vontobel/utils.py +17 -0
- wbportfolio/import_export/parsers/vontobel/valuation.py +29 -0
- wbportfolio/import_export/resources/__init__.py +0 -0
- wbportfolio/import_export/resources/assets.py +68 -0
- wbportfolio/import_export/resources/trades.py +41 -0
- wbportfolio/import_export/utils.py +42 -0
- wbportfolio/metric/__init__.py +0 -0
- wbportfolio/metric/backends/__init__.py +2 -0
- wbportfolio/metric/backends/base.py +86 -0
- wbportfolio/metric/backends/constants.py +222 -0
- wbportfolio/metric/backends/portfolio_base.py +255 -0
- wbportfolio/metric/backends/portfolio_esg.py +66 -0
- wbportfolio/metric/tests/__init__.py +0 -0
- wbportfolio/metric/tests/conftest.py +4 -0
- wbportfolio/metric/tests/test_portfolio_base.py +135 -0
- wbportfolio/metric/tests/test_portfolio_esg.py +69 -0
- wbportfolio/migrations/0001_initial_squashed.py +13848 -0
- wbportfolio/migrations/0002_product_default_sub_account_squashed_0039_alter_assetallocation_company_and_more.py +3836 -0
- wbportfolio/migrations/0040_instrument_financial_instrument.py +26 -0
- wbportfolio/migrations/0041_remove_listresearch_research_ptr_and_more.py +129 -0
- wbportfolio/migrations/0042_instrumentlist_instrumentlistthroughmodel_and_more.py +71 -0
- wbportfolio/migrations/0043_alter_instrumentlistthroughmodel_options_and_more.py +238 -0
- wbportfolio/migrations/0044_alter_instrumentlist_identifier.py +35 -0
- wbportfolio/migrations/0045_alter_instrument_financial_instrument.py +26 -0
- wbportfolio/migrations/0046_add_product_default_account.py +166 -0
- wbportfolio/migrations/0047_remove_product_default_sub_account.py +14 -0
- wbportfolio/migrations/0048_alter_trade_status.py +29 -0
- wbportfolio/migrations/0049_trade_claimed_shares.py +25 -0
- wbportfolio/migrations/0050_fees_fee_date_fees_wbportfolio_transac_1f7a29_idx.py +44 -0
- wbportfolio/migrations/0051_delete_macroreview.py +11 -0
- wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +888 -0
- wbportfolio/migrations/0053_remove_product_group.py +132 -0
- wbportfolio/migrations/0054_portfolioinstrumentpreferredclassificationthroughmodel_and_more.py +270 -0
- wbportfolio/migrations/0055_remove_product__custom_management_rebates_and_more.py +139 -0
- wbportfolio/migrations/0056_remove_companyportfoliodata_assets_under_management_currency_and_more.py +56 -0
- wbportfolio/migrations/0057_alter_portfolio_preferred_instrument_classifications_and_more.py +36 -0
- wbportfolio/migrations/0058_pmsinstrument.py +23 -0
- wbportfolio/migrations/0059_fees_unique_fees.py +51 -0
- wbportfolio/migrations/0060_alter_portfolioportfoliothroughmodel_type.py +21 -0
- wbportfolio/migrations/0061_portfolio_bank_accounts_product_bank_account_and_more.py +175 -0
- wbportfolio/migrations/0062_alter_dailyportfoliocashflow_options.py +20 -0
- wbportfolio/migrations/0063_accountreconciliation_accountreconciliationline_and_more.py +133 -0
- wbportfolio/migrations/0064_alter_portfolio_managers_portfolio_is_tracked_and_more.py +40 -0
- wbportfolio/migrations/0065_alter_portfolio_managers_claim_as_shares_and_more.py +73 -0
- wbportfolio/migrations/0066_assetposition_initial_shares_at_custodian_and_more.py +108 -0
- wbportfolio/migrations/0067_assetposition_unique_asset_position.py +77 -0
- wbportfolio/migrations/0068_trade_internal_trade_trade_marked_as_internal_and_more.py +59 -0
- wbportfolio/migrations/0069_remove_portfolio_is_invested_and_more.py +56 -0
- wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +82 -0
- wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py +22 -0
- wbportfolio/migrations/__init__.py +0 -0
- wbportfolio/models/__init__.py +26 -0
- wbportfolio/models/adjustments.py +246 -0
- wbportfolio/models/asset.py +869 -0
- wbportfolio/models/custodians.py +101 -0
- wbportfolio/models/indexes.py +33 -0
- wbportfolio/models/mixins/__init__.py +0 -0
- wbportfolio/models/mixins/instruments.py +127 -0
- wbportfolio/models/mixins/liquidity_stress_test.py +1307 -0
- wbportfolio/models/portfolio.py +1039 -0
- wbportfolio/models/portfolio_cash_flow.py +167 -0
- wbportfolio/models/portfolio_cash_targets.py +46 -0
- wbportfolio/models/portfolio_relationship.py +135 -0
- wbportfolio/models/portfolio_swing_pricings.py +51 -0
- wbportfolio/models/product_groups.py +230 -0
- wbportfolio/models/products.py +569 -0
- wbportfolio/models/reconciliations/__init__.py +2 -0
- wbportfolio/models/reconciliations/account_reconciliation_lines.py +192 -0
- wbportfolio/models/reconciliations/account_reconciliations.py +102 -0
- wbportfolio/models/reconciliations/reconciliations.py +25 -0
- wbportfolio/models/registers.py +132 -0
- wbportfolio/models/roles.py +208 -0
- wbportfolio/models/synchronization/__init__.py +3 -0
- wbportfolio/models/synchronization/portfolio_synchronization.py +292 -0
- wbportfolio/models/synchronization/price_computation.py +200 -0
- wbportfolio/models/synchronization/synchronization.py +188 -0
- wbportfolio/models/transactions/__init__.py +7 -0
- wbportfolio/models/transactions/claim.py +634 -0
- wbportfolio/models/transactions/dividends.py +31 -0
- wbportfolio/models/transactions/expiry.py +7 -0
- wbportfolio/models/transactions/fees.py +153 -0
- wbportfolio/models/transactions/trade_proposals.py +502 -0
- wbportfolio/models/transactions/trades.py +704 -0
- wbportfolio/models/transactions/transactions.py +211 -0
- wbportfolio/models/utils.py +12 -0
- wbportfolio/permissions.py +13 -0
- wbportfolio/pms/__init__.py +0 -0
- wbportfolio/pms/statistics/__init__.py +0 -0
- wbportfolio/pms/trading/__init__.py +1 -0
- wbportfolio/pms/trading/handler.py +164 -0
- wbportfolio/pms/typing.py +194 -0
- wbportfolio/preferences.py +6 -0
- wbportfolio/reports/__init__.py +0 -0
- wbportfolio/reports/monthly_position_report.py +74 -0
- wbportfolio/risk_management/__init__.py +0 -0
- wbportfolio/risk_management/backends/__init__.py +11 -0
- wbportfolio/risk_management/backends/accounts.py +166 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +63 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +203 -0
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +89 -0
- wbportfolio/risk_management/backends/liquidity_risk.py +86 -0
- wbportfolio/risk_management/backends/liquidity_stress_instrument.py +86 -0
- wbportfolio/risk_management/backends/mixins.py +220 -0
- wbportfolio/risk_management/backends/product_integrity.py +111 -0
- wbportfolio/risk_management/backends/stop_loss_instrument.py +24 -0
- wbportfolio/risk_management/backends/stop_loss_portfolio.py +36 -0
- wbportfolio/risk_management/backends/ucits_portfolio.py +63 -0
- wbportfolio/risk_management/tests/__init__.py +0 -0
- wbportfolio/risk_management/tests/conftest.py +15 -0
- wbportfolio/risk_management/tests/test_accounts.py +98 -0
- wbportfolio/risk_management/tests/test_controversy_portfolio.py +33 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +94 -0
- wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +60 -0
- wbportfolio/risk_management/tests/test_liquidity_risk.py +47 -0
- wbportfolio/risk_management/tests/test_product_integrity.py +55 -0
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +110 -0
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +119 -0
- wbportfolio/risk_management/tests/test_ucits_portfolio.py +39 -0
- wbportfolio/serializers/__init__.py +42 -0
- wbportfolio/serializers/adjustments.py +24 -0
- wbportfolio/serializers/assets.py +166 -0
- wbportfolio/serializers/custodians.py +26 -0
- wbportfolio/serializers/portfolio_cash_flow.py +48 -0
- wbportfolio/serializers/portfolio_cash_targets.py +20 -0
- wbportfolio/serializers/portfolio_relationship.py +53 -0
- wbportfolio/serializers/portfolio_swing_pricing.py +20 -0
- wbportfolio/serializers/portfolios.py +143 -0
- wbportfolio/serializers/positions.py +76 -0
- wbportfolio/serializers/product_group.py +88 -0
- wbportfolio/serializers/products.py +331 -0
- wbportfolio/serializers/reconciliations.py +171 -0
- wbportfolio/serializers/registers.py +72 -0
- wbportfolio/serializers/roles.py +60 -0
- wbportfolio/serializers/signals.py +157 -0
- wbportfolio/serializers/synchronization.py +18 -0
- wbportfolio/serializers/transactions/__init__.py +24 -0
- wbportfolio/serializers/transactions/claim.py +310 -0
- wbportfolio/serializers/transactions/dividends.py +18 -0
- wbportfolio/serializers/transactions/expiry.py +18 -0
- wbportfolio/serializers/transactions/fees.py +32 -0
- wbportfolio/serializers/transactions/trades.py +315 -0
- wbportfolio/serializers/transactions/transactions.py +84 -0
- wbportfolio/tasks.py +125 -0
- wbportfolio/tests/__init__.py +0 -0
- wbportfolio/tests/conftest.py +164 -0
- wbportfolio/tests/models/__init__.py +0 -0
- wbportfolio/tests/models/test_account_reconciliation.py +191 -0
- wbportfolio/tests/models/test_assets.py +193 -0
- wbportfolio/tests/models/test_custodians.py +12 -0
- wbportfolio/tests/models/test_customer_trades.py +113 -0
- wbportfolio/tests/models/test_dividends.py +7 -0
- wbportfolio/tests/models/test_imports.py +192 -0
- wbportfolio/tests/models/test_instrument_mixins.py +48 -0
- wbportfolio/tests/models/test_merge.py +133 -0
- wbportfolio/tests/models/test_portfolio_cash_flow.py +112 -0
- wbportfolio/tests/models/test_portfolio_cash_targets.py +27 -0
- wbportfolio/tests/models/test_portfolio_swing_pricings.py +42 -0
- wbportfolio/tests/models/test_portfolios.py +676 -0
- wbportfolio/tests/models/test_product_groups.py +80 -0
- wbportfolio/tests/models/test_products.py +187 -0
- wbportfolio/tests/models/test_roles.py +82 -0
- wbportfolio/tests/models/test_splits.py +233 -0
- wbportfolio/tests/models/test_synchronization.py +617 -0
- wbportfolio/tests/models/transactions/__init__.py +0 -0
- wbportfolio/tests/models/transactions/test_claim.py +129 -0
- wbportfolio/tests/models/transactions/test_fees.py +65 -0
- wbportfolio/tests/models/transactions/test_trades.py +204 -0
- wbportfolio/tests/models/utils.py +13 -0
- wbportfolio/tests/serializers/__init__.py +0 -0
- wbportfolio/tests/serializers/test_claims.py +21 -0
- wbportfolio/tests/signals.py +151 -0
- wbportfolio/tests/tests.py +31 -0
- wbportfolio/tests/viewsets/__init__.py +0 -0
- wbportfolio/tests/viewsets/test_assets.py +67 -0
- wbportfolio/tests/viewsets/test_performances.py +72 -0
- wbportfolio/tests/viewsets/test_products.py +92 -0
- wbportfolio/tests/viewsets/transactions/__init__.py +0 -0
- wbportfolio/tests/viewsets/transactions/test_claims.py +146 -0
- wbportfolio/urls.py +247 -0
- wbportfolio/utils.py +30 -0
- wbportfolio/viewsets/__init__.py +57 -0
- wbportfolio/viewsets/adjustments.py +46 -0
- wbportfolio/viewsets/assets.py +562 -0
- wbportfolio/viewsets/assets_and_net_new_money_progression.py +117 -0
- wbportfolio/viewsets/charts/__init__.py +1 -0
- wbportfolio/viewsets/charts/assets.py +247 -0
- wbportfolio/viewsets/configs/__init__.py +6 -0
- wbportfolio/viewsets/configs/buttons/__init__.py +23 -0
- wbportfolio/viewsets/configs/buttons/adjustments.py +13 -0
- wbportfolio/viewsets/configs/buttons/assets.py +145 -0
- wbportfolio/viewsets/configs/buttons/claims.py +83 -0
- wbportfolio/viewsets/configs/buttons/custodians.py +76 -0
- wbportfolio/viewsets/configs/buttons/fees.py +14 -0
- wbportfolio/viewsets/configs/buttons/mixins.py +88 -0
- wbportfolio/viewsets/configs/buttons/portfolios.py +115 -0
- wbportfolio/viewsets/configs/buttons/products.py +41 -0
- wbportfolio/viewsets/configs/buttons/reconciliations.py +65 -0
- wbportfolio/viewsets/configs/buttons/registers.py +11 -0
- wbportfolio/viewsets/configs/buttons/signals.py +68 -0
- wbportfolio/viewsets/configs/buttons/trade_proposals.py +25 -0
- wbportfolio/viewsets/configs/buttons/trades.py +144 -0
- wbportfolio/viewsets/configs/display/__init__.py +61 -0
- wbportfolio/viewsets/configs/display/adjustments.py +81 -0
- wbportfolio/viewsets/configs/display/assets.py +265 -0
- wbportfolio/viewsets/configs/display/claim.py +299 -0
- wbportfolio/viewsets/configs/display/custodians.py +24 -0
- wbportfolio/viewsets/configs/display/esg.py +88 -0
- wbportfolio/viewsets/configs/display/fees.py +133 -0
- wbportfolio/viewsets/configs/display/portfolio_cash_flow.py +103 -0
- wbportfolio/viewsets/configs/display/portfolio_relationship.py +38 -0
- wbportfolio/viewsets/configs/display/portfolios.py +125 -0
- wbportfolio/viewsets/configs/display/positions.py +75 -0
- wbportfolio/viewsets/configs/display/product_groups.py +54 -0
- wbportfolio/viewsets/configs/display/product_performance.py +241 -0
- wbportfolio/viewsets/configs/display/products.py +249 -0
- wbportfolio/viewsets/configs/display/reconciliations.py +151 -0
- wbportfolio/viewsets/configs/display/registers.py +71 -0
- wbportfolio/viewsets/configs/display/roles.py +49 -0
- wbportfolio/viewsets/configs/display/trade_proposals.py +97 -0
- wbportfolio/viewsets/configs/display/trades.py +359 -0
- wbportfolio/viewsets/configs/display/transactions.py +55 -0
- wbportfolio/viewsets/configs/endpoints/__init__.py +75 -0
- wbportfolio/viewsets/configs/endpoints/adjustments.py +17 -0
- wbportfolio/viewsets/configs/endpoints/assets.py +115 -0
- wbportfolio/viewsets/configs/endpoints/claim.py +106 -0
- wbportfolio/viewsets/configs/endpoints/custodians.py +6 -0
- wbportfolio/viewsets/configs/endpoints/esg.py +14 -0
- wbportfolio/viewsets/configs/endpoints/fees.py +26 -0
- wbportfolio/viewsets/configs/endpoints/portfolio_relationship.py +23 -0
- wbportfolio/viewsets/configs/endpoints/portfolios.py +43 -0
- wbportfolio/viewsets/configs/endpoints/positions.py +18 -0
- wbportfolio/viewsets/configs/endpoints/product_groups.py +11 -0
- wbportfolio/viewsets/configs/endpoints/product_performance.py +29 -0
- wbportfolio/viewsets/configs/endpoints/products.py +37 -0
- wbportfolio/viewsets/configs/endpoints/reconciliations.py +31 -0
- wbportfolio/viewsets/configs/endpoints/roles.py +9 -0
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +17 -0
- wbportfolio/viewsets/configs/endpoints/trades.py +82 -0
- wbportfolio/viewsets/configs/endpoints/transactions.py +17 -0
- wbportfolio/viewsets/configs/menu/__init__.py +30 -0
- wbportfolio/viewsets/configs/menu/adjustments.py +8 -0
- wbportfolio/viewsets/configs/menu/assets.py +8 -0
- wbportfolio/viewsets/configs/menu/claim.py +41 -0
- wbportfolio/viewsets/configs/menu/custodians.py +11 -0
- wbportfolio/viewsets/configs/menu/fees.py +13 -0
- wbportfolio/viewsets/configs/menu/instrument_prices.py +10 -0
- wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +8 -0
- wbportfolio/viewsets/configs/menu/portfolios.py +15 -0
- wbportfolio/viewsets/configs/menu/positions.py +14 -0
- wbportfolio/viewsets/configs/menu/product_groups.py +10 -0
- wbportfolio/viewsets/configs/menu/product_performance.py +25 -0
- wbportfolio/viewsets/configs/menu/products.py +15 -0
- wbportfolio/viewsets/configs/menu/reconciliations.py +7 -0
- wbportfolio/viewsets/configs/menu/registers.py +10 -0
- wbportfolio/viewsets/configs/menu/roles.py +16 -0
- wbportfolio/viewsets/configs/menu/trades.py +18 -0
- wbportfolio/viewsets/configs/menu/transactions.py +8 -0
- wbportfolio/viewsets/configs/previews/__init__.py +1 -0
- wbportfolio/viewsets/configs/previews/portfolios.py +21 -0
- wbportfolio/viewsets/configs/titles/__init__.py +65 -0
- wbportfolio/viewsets/configs/titles/adjustments.py +19 -0
- wbportfolio/viewsets/configs/titles/assets.py +57 -0
- wbportfolio/viewsets/configs/titles/assets_and_net_new_money_progression.py +6 -0
- wbportfolio/viewsets/configs/titles/claim.py +81 -0
- wbportfolio/viewsets/configs/titles/custodians.py +12 -0
- wbportfolio/viewsets/configs/titles/esg.py +10 -0
- wbportfolio/viewsets/configs/titles/fees.py +25 -0
- wbportfolio/viewsets/configs/titles/instrument_prices.py +20 -0
- wbportfolio/viewsets/configs/titles/portfolios.py +32 -0
- wbportfolio/viewsets/configs/titles/positions.py +11 -0
- wbportfolio/viewsets/configs/titles/product_groups.py +12 -0
- wbportfolio/viewsets/configs/titles/product_performance.py +16 -0
- wbportfolio/viewsets/configs/titles/products.py +6 -0
- wbportfolio/viewsets/configs/titles/registers.py +12 -0
- wbportfolio/viewsets/configs/titles/roles.py +23 -0
- wbportfolio/viewsets/configs/titles/trades.py +51 -0
- wbportfolio/viewsets/configs/titles/transactions.py +8 -0
- wbportfolio/viewsets/custodians.py +66 -0
- wbportfolio/viewsets/esg.py +165 -0
- wbportfolio/viewsets/mixins.py +48 -0
- wbportfolio/viewsets/portfolio_cash_flow.py +31 -0
- wbportfolio/viewsets/portfolio_cash_targets.py +8 -0
- wbportfolio/viewsets/portfolio_relationship.py +46 -0
- wbportfolio/viewsets/portfolio_swing_pricing.py +8 -0
- wbportfolio/viewsets/portfolios.py +154 -0
- wbportfolio/viewsets/positions.py +292 -0
- wbportfolio/viewsets/product_groups.py +84 -0
- wbportfolio/viewsets/product_performance.py +646 -0
- wbportfolio/viewsets/products.py +529 -0
- wbportfolio/viewsets/reconciliations.py +160 -0
- wbportfolio/viewsets/registers.py +75 -0
- wbportfolio/viewsets/roles.py +44 -0
- wbportfolio/viewsets/signals.py +42 -0
- wbportfolio/viewsets/synchronization.py +25 -0
- wbportfolio/viewsets/transactions/__init__.py +40 -0
- wbportfolio/viewsets/transactions/claim.py +933 -0
- wbportfolio/viewsets/transactions/fees.py +190 -0
- wbportfolio/viewsets/transactions/mixins.py +19 -0
- wbportfolio/viewsets/transactions/trade_proposals.py +93 -0
- wbportfolio/viewsets/transactions/trades.py +395 -0
- wbportfolio/viewsets/transactions/transactions.py +123 -0
- wbportfolio-2.2.1.dist-info/METADATA +21 -0
- wbportfolio-2.2.1.dist-info/RECORD +486 -0
- wbportfolio-2.2.1.dist-info/WHEEL +5 -0
- wbportfolio-2.2.1.dist-info/licenses/LICENSE +4 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date, datetime, timedelta
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from math import isclose
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from celery import shared_task
|
|
10
|
+
from django.contrib.postgres.fields import DateRangeField
|
|
11
|
+
from django.db import models
|
|
12
|
+
from django.db.models import (
|
|
13
|
+
BooleanField,
|
|
14
|
+
Case,
|
|
15
|
+
Exists,
|
|
16
|
+
F,
|
|
17
|
+
OuterRef,
|
|
18
|
+
Q,
|
|
19
|
+
QuerySet,
|
|
20
|
+
Sum,
|
|
21
|
+
Value,
|
|
22
|
+
When,
|
|
23
|
+
)
|
|
24
|
+
from django.db.models.signals import post_save
|
|
25
|
+
from django.dispatch import receiver
|
|
26
|
+
from psycopg.types.range import DateRange
|
|
27
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
28
|
+
from wbcore.models import WBModel
|
|
29
|
+
from wbcore.utils.models import ActiveObjectManager, DeleteToDisableMixin
|
|
30
|
+
from wbfdm.contrib.metric.dispatch import compute_metrics
|
|
31
|
+
from wbfdm.models import Instrument
|
|
32
|
+
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
33
|
+
from wbportfolio.models.asset import AssetPosition
|
|
34
|
+
from wbportfolio.models.indexes import Index
|
|
35
|
+
from wbportfolio.models.portfolio_relationship import (
|
|
36
|
+
InstrumentPortfolioThroughModel,
|
|
37
|
+
PortfolioInstrumentPreferredClassificationThroughModel,
|
|
38
|
+
)
|
|
39
|
+
from wbportfolio.models.products import Product
|
|
40
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
41
|
+
|
|
42
|
+
from .utils import get_casted_portfolio_instrument
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DefaultPortfolioQueryset(QuerySet):
|
|
46
|
+
def filter_invested_at_date(self, val_date: date) -> QuerySet:
|
|
47
|
+
"""
|
|
48
|
+
Filter the queryset to get only portfolio invested at the given date
|
|
49
|
+
"""
|
|
50
|
+
return self.filter(invested_timespan__startswith__lte=val_date, invested_timespan__endswith__gt=val_date)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DefaultPortfolioManager(ActiveObjectManager):
|
|
54
|
+
def get_queryset(self):
|
|
55
|
+
return DefaultPortfolioQueryset(self.model).filter(is_active=True)
|
|
56
|
+
|
|
57
|
+
def filter_invested_at_date(self, val_date: date):
|
|
58
|
+
return self.get_queryset().filter_invested_at_date(val_date)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ActiveTrackedPortfolioManager(DefaultPortfolioManager):
|
|
62
|
+
def get_queryset(self):
|
|
63
|
+
return (
|
|
64
|
+
super()
|
|
65
|
+
.get_queryset()
|
|
66
|
+
.annotate(asset_exists=Exists(AssetPosition.objects.filter(portfolio=OuterRef("pk"))))
|
|
67
|
+
.filter(asset_exists=True, is_tracked=True)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PortfolioPortfolioThroughModel(models.Model):
|
|
72
|
+
class Type(models.TextChoices):
|
|
73
|
+
PRIMARY = "PRIMARY", "Primary"
|
|
74
|
+
MODEL = "MODEL", "Model"
|
|
75
|
+
BENCHMARK = "BENCHMARK", "Benchmark"
|
|
76
|
+
INDEX = "INDEX", "Index"
|
|
77
|
+
CUSTODIAN = "CUSTODIAN", "Custodian"
|
|
78
|
+
|
|
79
|
+
portfolio = models.ForeignKey("wbportfolio.Portfolio", on_delete=models.CASCADE, related_name="dependency_through")
|
|
80
|
+
dependency_portfolio = models.ForeignKey(
|
|
81
|
+
"wbportfolio.Portfolio", on_delete=models.CASCADE, related_name="dependent_through"
|
|
82
|
+
)
|
|
83
|
+
type = models.CharField(choices=Type.choices, default=Type.PRIMARY, verbose_name="Type")
|
|
84
|
+
|
|
85
|
+
class Meta:
|
|
86
|
+
constraints = [
|
|
87
|
+
models.UniqueConstraint(fields=["portfolio", "type"], name="unique_primary", condition=Q(type="PRIMARY")),
|
|
88
|
+
models.UniqueConstraint(fields=["portfolio", "type"], name="unique_model", condition=Q(type="MODEL")),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Portfolio(DeleteToDisableMixin, WBModel):
|
|
93
|
+
assets: models.QuerySet[AssetPosition]
|
|
94
|
+
|
|
95
|
+
name = models.CharField(
|
|
96
|
+
max_length=255,
|
|
97
|
+
verbose_name="Name",
|
|
98
|
+
default="",
|
|
99
|
+
help_text="The Name of the Portfolio",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
currency = models.ForeignKey(
|
|
103
|
+
to="currency.Currency",
|
|
104
|
+
related_name="portfolios",
|
|
105
|
+
on_delete=models.PROTECT,
|
|
106
|
+
verbose_name="Currency",
|
|
107
|
+
help_text="The currency of the portfolio.",
|
|
108
|
+
)
|
|
109
|
+
hedged_currency = models.ForeignKey(
|
|
110
|
+
to="currency.Currency",
|
|
111
|
+
related_name="hedged_portfolios",
|
|
112
|
+
on_delete=models.PROTECT,
|
|
113
|
+
blank=True,
|
|
114
|
+
null=True,
|
|
115
|
+
verbose_name="Hedged Currency",
|
|
116
|
+
help_text="The hedged currency of the portfolio.",
|
|
117
|
+
)
|
|
118
|
+
depends_on = models.ManyToManyField(
|
|
119
|
+
"wbportfolio.Portfolio",
|
|
120
|
+
symmetrical=False,
|
|
121
|
+
related_name="dependent_portfolios",
|
|
122
|
+
through="wbportfolio.PortfolioPortfolioThroughModel",
|
|
123
|
+
through_fields=("portfolio", "dependency_portfolio"),
|
|
124
|
+
blank=True,
|
|
125
|
+
verbose_name="The portfolios this portfolio depends on",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
portfolio_synchronization = models.ForeignKey(
|
|
129
|
+
"wbportfolio.PortfolioSynchronization",
|
|
130
|
+
null=True,
|
|
131
|
+
blank=True,
|
|
132
|
+
on_delete=models.SET_NULL,
|
|
133
|
+
related_name="portfolios",
|
|
134
|
+
verbose_name="Portfolio Synchronization Method",
|
|
135
|
+
)
|
|
136
|
+
preferred_instrument_classifications = models.ManyToManyField(
|
|
137
|
+
"wbfdm.Instrument",
|
|
138
|
+
limit_choices_to=(models.Q(instrument_type__is_classifiable=True) & models.Q(level=0)),
|
|
139
|
+
related_name="preferred_portfolio_classifications",
|
|
140
|
+
through="wbportfolio.PortfolioInstrumentPreferredClassificationThroughModel",
|
|
141
|
+
through_fields=("portfolio", "instrument"),
|
|
142
|
+
blank=True,
|
|
143
|
+
verbose_name="The Preferred classification per instrument",
|
|
144
|
+
)
|
|
145
|
+
instruments = models.ManyToManyField(
|
|
146
|
+
"wbfdm.Instrument",
|
|
147
|
+
through=InstrumentPortfolioThroughModel,
|
|
148
|
+
related_name="portfolios",
|
|
149
|
+
blank=True,
|
|
150
|
+
verbose_name="Instruments",
|
|
151
|
+
help_text="Instruments linked to this instrument",
|
|
152
|
+
)
|
|
153
|
+
invested_timespan = DateRangeField(
|
|
154
|
+
null=True, blank=True, help_text="Define when this portfolio is considered invested"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
is_manageable = models.BooleanField(
|
|
158
|
+
default=False,
|
|
159
|
+
help_text="True if the portfolio can be manually modified (e.g. Trade proposal be submitted or total weight recomputed)",
|
|
160
|
+
)
|
|
161
|
+
is_tracked = models.BooleanField(
|
|
162
|
+
default=True,
|
|
163
|
+
help_text="True if the internal updating mechanism (e.g., Propagation, Synchronization etc...) needs to apply to this portfolio",
|
|
164
|
+
)
|
|
165
|
+
only_weighting = models.BooleanField(
|
|
166
|
+
default=False,
|
|
167
|
+
help_text="Indicates that this portfolio is only utilizing weights and disregards shares, e.g. a model portfolio",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
last_synchronization = models.DateTimeField(blank=True, null=True, verbose_name="Last Synchronization Date")
|
|
171
|
+
bank_accounts = models.ManyToManyField(
|
|
172
|
+
to="directory.BankingContact",
|
|
173
|
+
related_name="wbportfolio_portfolios",
|
|
174
|
+
through="wbportfolio.PortfolioBankAccountThroughModel",
|
|
175
|
+
blank=True,
|
|
176
|
+
)
|
|
177
|
+
objects = DefaultPortfolioManager()
|
|
178
|
+
tracked_objects = ActiveTrackedPortfolioManager()
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def primary_portfolio(self):
|
|
182
|
+
with suppress(PortfolioPortfolioThroughModel.DoesNotExist):
|
|
183
|
+
return PortfolioPortfolioThroughModel.objects.get(
|
|
184
|
+
portfolio=self, type=PortfolioPortfolioThroughModel.Type.PRIMARY
|
|
185
|
+
).dependency_portfolio
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def model_portfolio(self):
|
|
189
|
+
with suppress(PortfolioPortfolioThroughModel.DoesNotExist):
|
|
190
|
+
return PortfolioPortfolioThroughModel.objects.get(
|
|
191
|
+
portfolio=self, type=PortfolioPortfolioThroughModel.Type.MODEL
|
|
192
|
+
).dependency_portfolio
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def benchmark_portfolio(self):
|
|
196
|
+
with suppress(PortfolioPortfolioThroughModel.DoesNotExist):
|
|
197
|
+
return PortfolioPortfolioThroughModel.objects.get(
|
|
198
|
+
portfolio=self, type=PortfolioPortfolioThroughModel.Type.BENCHMARK
|
|
199
|
+
).dependency_portfolio
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def imported_assets(self):
|
|
203
|
+
return self.assets.filter(is_estimated=False)
|
|
204
|
+
|
|
205
|
+
def delete(self, **kwargs):
|
|
206
|
+
super().delete(**kwargs)
|
|
207
|
+
# We check if for all linked instruments, this portfolio was the last active one (if yes, we disable the instrument)
|
|
208
|
+
if self.id:
|
|
209
|
+
for instrument in self.instruments.iterator():
|
|
210
|
+
if not instrument.portfolios.filter(is_active=True).exists():
|
|
211
|
+
instrument.delisted_date = date.today() - timedelta(days=1)
|
|
212
|
+
instrument.save()
|
|
213
|
+
|
|
214
|
+
def _build_dto(self, val_date: date, **extra_filter_kwargs) -> PortfolioDTO:
|
|
215
|
+
"returns the dto representation of this portfolio at the specified date"
|
|
216
|
+
return PortfolioDTO(
|
|
217
|
+
tuple([pos._build_dto() for pos in self.assets.filter(date=val_date, **extra_filter_kwargs)])
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def is_invested_at_date(self, val_date: date) -> bool:
|
|
221
|
+
return (
|
|
222
|
+
self.invested_timespan
|
|
223
|
+
and self.invested_timespan.upper > val_date
|
|
224
|
+
and self.invested_timespan.lower <= val_date
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def __str__(self):
|
|
228
|
+
return f"{self.id:06} ({self.name})"
|
|
229
|
+
|
|
230
|
+
class Meta:
|
|
231
|
+
verbose_name = "Portfolio"
|
|
232
|
+
verbose_name_plural = "Portfolios"
|
|
233
|
+
|
|
234
|
+
notification_types = [
|
|
235
|
+
(
|
|
236
|
+
"wbportfolio.portfolio.check_custodian_portfolio",
|
|
237
|
+
"Check Custodian Portfolio",
|
|
238
|
+
"Sends a notification when a portfolio does not match with its custodian portfolio",
|
|
239
|
+
True,
|
|
240
|
+
True,
|
|
241
|
+
True,
|
|
242
|
+
),
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def create_model_portfolio(cls, name, currency, portfolio_synchronization=None, index_parameters=dict()):
|
|
247
|
+
portfolio = cls.objects.create(
|
|
248
|
+
is_manageable=True,
|
|
249
|
+
name=name,
|
|
250
|
+
currency=currency,
|
|
251
|
+
portfolio_synchronization=portfolio_synchronization,
|
|
252
|
+
)
|
|
253
|
+
if index_parameters:
|
|
254
|
+
index = Index.objects.create(name=name, currency=currency, **index_parameters)
|
|
255
|
+
index.portfolios.all().delete()
|
|
256
|
+
InstrumentPortfolioThroughModel.objects.update_or_create(
|
|
257
|
+
instrument=index, defaults={"portfolio": portfolio}
|
|
258
|
+
)
|
|
259
|
+
return portfolio
|
|
260
|
+
|
|
261
|
+
def is_active_at_date(self, val_date: date) -> bool:
|
|
262
|
+
"""
|
|
263
|
+
Return if the base instrument has a total aum greater than 0
|
|
264
|
+
:val_date: the date at which we need to evaluate if the portfolio is considered active
|
|
265
|
+
"""
|
|
266
|
+
active_portfolio = self.is_active or self.deletion_datetime.date() > val_date
|
|
267
|
+
if self.instruments.exists():
|
|
268
|
+
return active_portfolio and any(
|
|
269
|
+
[instrument.is_active_at_date(val_date) for instrument in self.instruments.all()]
|
|
270
|
+
)
|
|
271
|
+
return active_portfolio
|
|
272
|
+
|
|
273
|
+
def get_aum(self, val_date: date) -> Decimal:
|
|
274
|
+
"""
|
|
275
|
+
Return the total asset under management of the portfolio at the specified valuation date
|
|
276
|
+
Args:
|
|
277
|
+
val_date: The date at which aum needs to be computed
|
|
278
|
+
Returns:
|
|
279
|
+
The total AUM (0 if there is no position)
|
|
280
|
+
"""
|
|
281
|
+
return self.assets.filter(date=val_date).aggregate(s=Sum("total_value_fx_portfolio"))["s"] or Decimal(0.0)
|
|
282
|
+
|
|
283
|
+
def get_total_value(self, val_date):
|
|
284
|
+
from wbportfolio.models.transactions.trades import Trade
|
|
285
|
+
|
|
286
|
+
trades = Trade.valid_customer_trade_objects.filter(portfolio=self, transaction_date__lte=val_date)
|
|
287
|
+
|
|
288
|
+
total_aum = Decimal(0)
|
|
289
|
+
for underlying_instrument_id, sum_shares in (
|
|
290
|
+
trades.values("underlying_instrument")
|
|
291
|
+
.annotate(
|
|
292
|
+
sum_shares=Sum("shares"),
|
|
293
|
+
)
|
|
294
|
+
.values_list("underlying_instrument", "sum_shares")
|
|
295
|
+
):
|
|
296
|
+
with suppress(Instrument.DoesNotExist, InstrumentPrice.DoesNotExist):
|
|
297
|
+
instrument = Instrument.objects.get(id=underlying_instrument_id)
|
|
298
|
+
last_price = instrument.valuations.filter(date__lte=val_date).latest("date").net_value
|
|
299
|
+
fx_rate = instrument.currency.convert(val_date, self.currency)
|
|
300
|
+
total_aum += last_price * sum_shares * fx_rate
|
|
301
|
+
return total_aum
|
|
302
|
+
|
|
303
|
+
def _get_assets(self, with_estimated=True, with_cash=True):
|
|
304
|
+
qs = self.assets
|
|
305
|
+
if not with_estimated:
|
|
306
|
+
qs = qs.filter(is_estimated=False)
|
|
307
|
+
if not with_cash:
|
|
308
|
+
qs = qs.exclude(underlying_instrument__is_cash=True)
|
|
309
|
+
return qs
|
|
310
|
+
|
|
311
|
+
def get_earliest_asset_position_date(self, val_date=None, with_estimated=False):
|
|
312
|
+
qs = self._get_assets(with_estimated=with_estimated)
|
|
313
|
+
if val_date:
|
|
314
|
+
qs = qs.filter(date__gte=val_date)
|
|
315
|
+
if qs.exists():
|
|
316
|
+
return qs.earliest("date").date
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def get_latest_asset_position_date(self, val_date=None, with_estimated=False):
|
|
320
|
+
qs = self._get_assets(with_estimated=with_estimated)
|
|
321
|
+
if val_date:
|
|
322
|
+
qs = qs.filter(date__lte=val_date)
|
|
323
|
+
|
|
324
|
+
if qs.exists():
|
|
325
|
+
return qs.latest("date").date
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
# Asset Position Utility Functions
|
|
329
|
+
def get_holding(self, val_date, exclude_cash=True, exclude_index=True):
|
|
330
|
+
qs = self._get_assets(with_cash=not exclude_cash).filter(date=val_date, weighting__gt=0)
|
|
331
|
+
if exclude_index:
|
|
332
|
+
qs = qs.exclude(underlying_security_instrument_type_key="index")
|
|
333
|
+
return (
|
|
334
|
+
qs.values("underlying_instrument__name")
|
|
335
|
+
.annotate(total_value_fx_portfolio=Sum("total_value_fx_portfolio"), weighting=Sum("weighting"))
|
|
336
|
+
.order_by("-total_value_fx_portfolio")
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _get_groupedby_df(
|
|
340
|
+
self,
|
|
341
|
+
group_by,
|
|
342
|
+
val_date: date,
|
|
343
|
+
exclude_cash: bool | None = False,
|
|
344
|
+
exclude_index: bool | None = False,
|
|
345
|
+
extra_filter_parameters: dict[str, Any] = None,
|
|
346
|
+
**groupby_kwargs,
|
|
347
|
+
):
|
|
348
|
+
qs = self._get_assets(with_cash=not exclude_cash).filter(date=val_date)
|
|
349
|
+
if exclude_index:
|
|
350
|
+
# We exclude only index that are not considered as cash. Setting exclude_cash to true convers this case.
|
|
351
|
+
qs = qs.exclude(
|
|
352
|
+
Q(underlying_security_instrument_type_key="index") & Q(underlying_instrument__is_cash=False)
|
|
353
|
+
)
|
|
354
|
+
if extra_filter_parameters:
|
|
355
|
+
qs = qs.filter(**extra_filter_parameters)
|
|
356
|
+
qs = group_by(qs, **groupby_kwargs).annotate(sum_weighting=Sum(F("weighting"))).order_by("-sum_weighting")
|
|
357
|
+
df = pd.DataFrame(
|
|
358
|
+
qs.values_list("aggregated_title", "sum_weighting"), columns=["aggregated_title", "weighting"]
|
|
359
|
+
)
|
|
360
|
+
if not df.empty:
|
|
361
|
+
df.weighting = df.weighting.astype("float")
|
|
362
|
+
df.weighting = df.weighting / df.weighting.sum()
|
|
363
|
+
df = df.sort_values(by=["weighting"])
|
|
364
|
+
return df.where(pd.notnull(df), None)
|
|
365
|
+
|
|
366
|
+
def get_geographical_breakdown(self, val_date, **kwargs):
|
|
367
|
+
df = self._get_groupedby_df(
|
|
368
|
+
AssetPosition.country_group_by, val_date=val_date, exclude_cash=True, exclude_index=True, **kwargs
|
|
369
|
+
)
|
|
370
|
+
if not df.empty:
|
|
371
|
+
df = df[df["weighting"] != 0]
|
|
372
|
+
return df
|
|
373
|
+
|
|
374
|
+
def get_currency_exposure(self, val_date, **kwargs):
|
|
375
|
+
df = self._get_groupedby_df(AssetPosition.currency_group_by, val_date=val_date, **kwargs)
|
|
376
|
+
if not df.empty:
|
|
377
|
+
df = df[df["weighting"] != 0]
|
|
378
|
+
return df
|
|
379
|
+
|
|
380
|
+
def get_equity_market_cap_distribution(self, val_date, **kwargs):
|
|
381
|
+
df = self._get_groupedby_df(
|
|
382
|
+
AssetPosition.marketcap_group_by,
|
|
383
|
+
val_date=val_date,
|
|
384
|
+
exclude_cash=True,
|
|
385
|
+
exclude_index=True,
|
|
386
|
+
extra_filter_parameters={"underlying_security_instrument_type_key": "equity"},
|
|
387
|
+
**kwargs,
|
|
388
|
+
)
|
|
389
|
+
if not df.empty:
|
|
390
|
+
df = df[df["weighting"] != 0]
|
|
391
|
+
return df
|
|
392
|
+
|
|
393
|
+
def get_equity_liquidity(self, val_date, **kwargs):
|
|
394
|
+
df = self._get_groupedby_df(
|
|
395
|
+
AssetPosition.liquidity_group_by,
|
|
396
|
+
val_date=val_date,
|
|
397
|
+
exclude_cash=True,
|
|
398
|
+
exclude_index=True,
|
|
399
|
+
extra_filter_parameters={"underlying_security_instrument_type_key": "equity"},
|
|
400
|
+
**kwargs,
|
|
401
|
+
)
|
|
402
|
+
if not df.empty:
|
|
403
|
+
df = df[df["weighting"] != 0]
|
|
404
|
+
return df
|
|
405
|
+
|
|
406
|
+
def get_industry_exposure(self, val_date=None, **kwargs):
|
|
407
|
+
df = self._get_groupedby_df(
|
|
408
|
+
AssetPosition.group_by_primary, val_date=val_date, exclude_cash=True, exclude_index=True, **kwargs
|
|
409
|
+
)
|
|
410
|
+
if not df.empty:
|
|
411
|
+
df = df[df["weighting"] != 0]
|
|
412
|
+
return df
|
|
413
|
+
|
|
414
|
+
def get_asset_allocation(self, val_date=None, **kwargs):
|
|
415
|
+
df = self._get_groupedby_df(AssetPosition.cash_group_by, val_date=val_date, **kwargs)
|
|
416
|
+
if not df.empty:
|
|
417
|
+
df = df[df["weighting"] != 0]
|
|
418
|
+
return df
|
|
419
|
+
|
|
420
|
+
def get_adjusted_child_positions(self, val_date):
|
|
421
|
+
if (
|
|
422
|
+
child_positions := self.assets.exclude(underlying_instrument__is_cash=True).filter(date=val_date)
|
|
423
|
+
).count() == 1:
|
|
424
|
+
if portfolio := child_positions.first().underlying_instrument.primary_portfolio:
|
|
425
|
+
child_positions = portfolio.assets.exclude(underlying_instrument__is_cash=True).filter(date=val_date)
|
|
426
|
+
for position in child_positions:
|
|
427
|
+
if child_portfolio := position.underlying_instrument.primary_portfolio:
|
|
428
|
+
index_positions = child_portfolio.assets.exclude(underlying_instrument__is_cash=True).filter(
|
|
429
|
+
date=val_date
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
for index_position in index_positions.all():
|
|
433
|
+
weighting = index_position.weighting * position.weighting
|
|
434
|
+
if weighting != 0:
|
|
435
|
+
yield {
|
|
436
|
+
"underlying_instrument_id": index_position.underlying_instrument.id,
|
|
437
|
+
"weighting": weighting,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
def get_longshort_distribution(self, val_date):
|
|
441
|
+
df = pd.DataFrame(self.get_adjusted_child_positions(val_date))
|
|
442
|
+
|
|
443
|
+
if not df.empty:
|
|
444
|
+
df["is_cash"] = df.underlying_instrument_id.apply(lambda x: Instrument.objects.get(id=x).is_cash)
|
|
445
|
+
df = df[~df["is_cash"]]
|
|
446
|
+
df = (
|
|
447
|
+
df[["underlying_instrument_id", "weighting"]].groupby("underlying_instrument_id").sum().astype("float")
|
|
448
|
+
)
|
|
449
|
+
df.weighting = df.weighting / df.weighting.sum()
|
|
450
|
+
short_weight = df[df.weighting < 0].weighting.abs().sum()
|
|
451
|
+
long_weight = df[df.weighting > 0].weighting.sum()
|
|
452
|
+
total_weight = long_weight + short_weight
|
|
453
|
+
return pd.DataFrame(
|
|
454
|
+
[
|
|
455
|
+
{"title": "Long", "weighting": long_weight / total_weight},
|
|
456
|
+
{"title": "Short", "weighting": short_weight / total_weight},
|
|
457
|
+
]
|
|
458
|
+
)
|
|
459
|
+
return df
|
|
460
|
+
|
|
461
|
+
def get_portfolio_contribution_df(self, start, end, with_cash=True, hedged_currency=None, only_equity=False):
|
|
462
|
+
qs = self._get_assets(with_cash=with_cash).filter(date__gte=start, date__lte=end)
|
|
463
|
+
if only_equity:
|
|
464
|
+
qs = qs.filter(underlying_security_instrument_type_key="equity")
|
|
465
|
+
return Portfolio.get_contribution_df(qs, hedged_currency=hedged_currency)
|
|
466
|
+
|
|
467
|
+
def check_related_portfolio_at_date(self, val_date: date, related_portfolio: "Portfolio"):
|
|
468
|
+
assets = AssetPosition.objects.filter(
|
|
469
|
+
date=val_date, underlying_instrument__is_cash=False, underlying_instrument__is_cash_equivalent=False
|
|
470
|
+
).values("underlying_instrument__parent", "shares")
|
|
471
|
+
assets1 = assets.filter(portfolio=self)
|
|
472
|
+
assets2 = assets.filter(portfolio=related_portfolio)
|
|
473
|
+
return assets1.difference(assets2)
|
|
474
|
+
|
|
475
|
+
def change_at_date(
|
|
476
|
+
self,
|
|
477
|
+
val_date: date,
|
|
478
|
+
recompute_weighting: bool = False,
|
|
479
|
+
force_recompute_weighting: bool = False,
|
|
480
|
+
synchronize: bool = True,
|
|
481
|
+
**sync_kwargs,
|
|
482
|
+
):
|
|
483
|
+
qs = (
|
|
484
|
+
self.assets.filter(date=val_date)
|
|
485
|
+
.filter(Q(total_value_fx_portfolio__isnull=False) | Q(weighting__isnull=False))
|
|
486
|
+
.distinct()
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# We normalize weight across the portfolio for a given date
|
|
490
|
+
if (self.portfolio_synchronization or self.is_manageable or force_recompute_weighting) and qs.exists():
|
|
491
|
+
total_weighting = qs.aggregate(s=Sum("weighting"))["s"]
|
|
492
|
+
# We check if this actually necessary
|
|
493
|
+
# (i.e. if the weight is already summed to 100%, it is already normalized)
|
|
494
|
+
if not total_weighting or not isclose(total_weighting, Decimal(1.0), abs_tol=0.001) or recompute_weighting:
|
|
495
|
+
total_value = qs.aggregate(s=Sum("total_value_fx_portfolio"))["s"]
|
|
496
|
+
# TODO we change this because postgres doesn't support join statement in update (and total_value_fx_portfolio is a joined annoted field)
|
|
497
|
+
for asset in qs:
|
|
498
|
+
if total_value:
|
|
499
|
+
asset.weighting = asset._total_value_fx_portfolio / total_value
|
|
500
|
+
elif total_weighting:
|
|
501
|
+
asset.weighting = asset.weighting / total_weighting
|
|
502
|
+
asset.save()
|
|
503
|
+
if synchronize:
|
|
504
|
+
for dependent_portfolio in self.dependent_portfolios.exclude(id=self.id).distinct():
|
|
505
|
+
# Check if the dependent portfolio has a synchronization method and has assets at the specified date
|
|
506
|
+
if (synchronization := dependent_portfolio.portfolio_synchronization) and (
|
|
507
|
+
dependent_portfolio.assets.filter(date__gte=val_date).exists()
|
|
508
|
+
):
|
|
509
|
+
# If this is true, we want to apply the synchronization at every synchronization period
|
|
510
|
+
# (scheduled crontab) from val_date to now.
|
|
511
|
+
if synchronization.propagate_history:
|
|
512
|
+
for _d in synchronization.dates_range(
|
|
513
|
+
val_date, dependent_portfolio.assets.latest("date").date, filter_daily=True
|
|
514
|
+
):
|
|
515
|
+
synchronization.synchronize_as_task_si(
|
|
516
|
+
dependent_portfolio, _d, override_execution_datetime_validity=True
|
|
517
|
+
).apply_async()
|
|
518
|
+
# Otherwise, we simply call a unique task for that date
|
|
519
|
+
else:
|
|
520
|
+
synchronization.synchronize_as_task_si(
|
|
521
|
+
dependent_portfolio, val_date, override_execution_datetime_validity=True
|
|
522
|
+
).apply_async()
|
|
523
|
+
|
|
524
|
+
# We check if there is an instrument attached to the portfolio with calculated NAV and price computation method
|
|
525
|
+
for instrument in self.instruments.all():
|
|
526
|
+
if price_computation := getattr(
|
|
527
|
+
get_casted_portfolio_instrument(instrument), "price_computation", None
|
|
528
|
+
):
|
|
529
|
+
inception_date = instrument.inception_date
|
|
530
|
+
if isinstance(inception_date, datetime):
|
|
531
|
+
inception_date = inception_date.date()
|
|
532
|
+
|
|
533
|
+
if isinstance(val_date, datetime):
|
|
534
|
+
val_date = val_date.date()
|
|
535
|
+
|
|
536
|
+
if inception_date is None or inception_date > val_date:
|
|
537
|
+
instrument.inception_date = val_date
|
|
538
|
+
instrument.save()
|
|
539
|
+
price_computation.compute(instrument, val_date, override_execution_datetime_validity=True)
|
|
540
|
+
compute_metrics(val_date, basket=self)
|
|
541
|
+
|
|
542
|
+
def propagate_or_update_assets(
|
|
543
|
+
self,
|
|
544
|
+
from_date: date,
|
|
545
|
+
to_date: date,
|
|
546
|
+
forward_price: bool | None = True,
|
|
547
|
+
base_assets: dict[str, str] | None = None,
|
|
548
|
+
delete_existing_assets: bool | None = False,
|
|
549
|
+
):
|
|
550
|
+
# we don't propagate on already imported portfolio by default
|
|
551
|
+
is_target_portfolio_imported = self.assets.filter(date=to_date, is_estimated=False).exists()
|
|
552
|
+
if not base_assets:
|
|
553
|
+
base_assets = dict()
|
|
554
|
+
|
|
555
|
+
def _get_next_asset_valuation_date(current_asset_valuation_date):
|
|
556
|
+
return (current_asset_valuation_date + pd.offsets.BDay(np.busday_count(from_date, to_date))).date()
|
|
557
|
+
|
|
558
|
+
last_fx_date = CurrencyFXRates.objects.filter(date__lte=to_date).latest("date").date
|
|
559
|
+
fx_rates = CurrencyFXRates.objects.filter(date=last_fx_date)
|
|
560
|
+
assets = self.assets.filter(date=from_date)
|
|
561
|
+
|
|
562
|
+
from_is_active = self.is_active_at_date(from_date)
|
|
563
|
+
to_is_active = self.is_active_at_date(to_date)
|
|
564
|
+
# # We check is the current assets are already stored and if there is no already stored valid assets
|
|
565
|
+
# # With this, we ensure that we don't overwrite imported asset position with propagated ones.
|
|
566
|
+
# assets_positions_next_day_count = self.assets.filter(date=to_date).count()
|
|
567
|
+
if assets.exists() or base_assets:
|
|
568
|
+
# Remove already existing assets
|
|
569
|
+
if delete_existing_assets:
|
|
570
|
+
self.assets.filter(date=to_date).delete()
|
|
571
|
+
asset_list = list()
|
|
572
|
+
# If base_assets is provided,
|
|
573
|
+
# we assume that the portfolio composition is injected by this list of dictionary
|
|
574
|
+
if base_assets:
|
|
575
|
+
base_assets = (
|
|
576
|
+
base_assets
|
|
577
|
+
if isinstance(base_assets, dict)
|
|
578
|
+
else {asset_id: Decimal(1 / len(base_assets)) for asset_id in base_assets}
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
remaining_base_assets = base_assets.copy()
|
|
582
|
+
# Loop over existing assets and construct the propagation assets list
|
|
583
|
+
for asset in assets.all():
|
|
584
|
+
# if a composition is provided, we ensure that existing assets don't deviate from it
|
|
585
|
+
if (base_assets and asset.underlying_instrument.id in base_assets.keys()) or not base_assets:
|
|
586
|
+
next_asset_valuation_date = _get_next_asset_valuation_date(asset.asset_valuation_date)
|
|
587
|
+
with suppress(ValueError):
|
|
588
|
+
asset_list.append(
|
|
589
|
+
{
|
|
590
|
+
"initial_price": (
|
|
591
|
+
asset.initial_price
|
|
592
|
+
if asset._price is not None
|
|
593
|
+
else asset.underlying_instrument.get_price(from_date)
|
|
594
|
+
),
|
|
595
|
+
"asset_valuation_date": next_asset_valuation_date,
|
|
596
|
+
"weighting": asset.weighting,
|
|
597
|
+
"next_initial_price": asset.underlying_instrument.get_price(next_asset_valuation_date),
|
|
598
|
+
"underlying_instrument": asset.underlying_instrument,
|
|
599
|
+
"exchange": asset.exchange,
|
|
600
|
+
"portfolio": asset.portfolio,
|
|
601
|
+
"portfolio_created": asset.portfolio_created,
|
|
602
|
+
"currency": asset.currency,
|
|
603
|
+
"initial_shares": asset.initial_shares,
|
|
604
|
+
}
|
|
605
|
+
)
|
|
606
|
+
remaining_base_assets.pop(asset.underlying_instrument.id, None)
|
|
607
|
+
# We ensure that the propagation assets list contains the proposed composition
|
|
608
|
+
for asset_id, weighting in remaining_base_assets.items():
|
|
609
|
+
instrument = Instrument.objects.get(id=asset_id)
|
|
610
|
+
with suppress(ValueError):
|
|
611
|
+
asset_list.append(
|
|
612
|
+
{
|
|
613
|
+
"underlying_instrument": instrument,
|
|
614
|
+
"initial_price": instrument.get_price(from_date),
|
|
615
|
+
"next_initial_price": instrument.get_price(to_date),
|
|
616
|
+
"asset_valuation_date": to_date,
|
|
617
|
+
"initial_shares": None,
|
|
618
|
+
"portfolio": self,
|
|
619
|
+
"currency": instrument.currency,
|
|
620
|
+
"weighting": weighting,
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
df = pd.DataFrame(asset_list)
|
|
625
|
+
if not df.empty:
|
|
626
|
+
df[["initial_price", "weighting", "next_initial_price"]] = df[
|
|
627
|
+
["initial_price", "weighting", "next_initial_price"]
|
|
628
|
+
].astype("float")
|
|
629
|
+
idxx = pd.isnull(df["initial_price"]) & ~pd.isnull(df["next_initial_price"])
|
|
630
|
+
df.loc[idxx, "initial_price"] = df.loc[idxx, "next_initial_price"]
|
|
631
|
+
if forward_price:
|
|
632
|
+
idx = pd.isnull(df["next_initial_price"])
|
|
633
|
+
df.loc[idx, "next_initial_price"] = df.loc[idx, "initial_price"]
|
|
634
|
+
df = df.dropna(axis=0, subset=["next_initial_price", "initial_price"])
|
|
635
|
+
# Normalize weight to 100%. Exclude portfolio were sum of weight equals 0 (e.g. short/long portfolio)
|
|
636
|
+
if df.weighting.sum() != 0:
|
|
637
|
+
df["weighting"] /= df.weighting.sum()
|
|
638
|
+
df.loc[:, "perf"] = df.loc[:, "next_initial_price"] / df.loc[:, "initial_price"]
|
|
639
|
+
df["contribution"] = df.perf * df.weighting
|
|
640
|
+
df.loc[:, "next_weighting"] = df.contribution
|
|
641
|
+
|
|
642
|
+
if df.contribution.sum() != 0:
|
|
643
|
+
df.loc[:, "next_weighting"] /= df.contribution.sum()
|
|
644
|
+
|
|
645
|
+
# Normalize next weighting
|
|
646
|
+
if df.next_weighting.sum() != 0:
|
|
647
|
+
df.next_weighting /= df.next_weighting.sum()
|
|
648
|
+
df = df.replace([np.inf, -np.inf, np.nan], None)
|
|
649
|
+
df.loc[(df["next_weighting"] < -1) | (df["next_weighting"] > 1), "next_weighting"] = df.loc[
|
|
650
|
+
(df["next_weighting"] < -1) | (df["next_weighting"] > 1), "weighting"
|
|
651
|
+
] # if the next weighting is not including within -1 and 1 range, we default to the initial weighting
|
|
652
|
+
if not df.empty:
|
|
653
|
+
for row in df.to_dict("records"):
|
|
654
|
+
weighting = Decimal(row["next_weighting"]) if row["next_weighting"] else row["weighting"]
|
|
655
|
+
if from_is_active and not to_is_active:
|
|
656
|
+
weighting = Decimal(0.0)
|
|
657
|
+
try:
|
|
658
|
+
initial_currency_fx_rate = (
|
|
659
|
+
fx_rates.get(currency=self.currency).value
|
|
660
|
+
/ fx_rates.get(currency=row["currency"]).value
|
|
661
|
+
)
|
|
662
|
+
except CurrencyFXRates.DoesNotExist:
|
|
663
|
+
initial_currency_fx_rate = Decimal(1)
|
|
664
|
+
defaults = {
|
|
665
|
+
"initial_currency_fx_rate": initial_currency_fx_rate,
|
|
666
|
+
"weighting": weighting,
|
|
667
|
+
"initial_price": Decimal(row["next_initial_price"]),
|
|
668
|
+
"initial_shares": row["initial_shares"],
|
|
669
|
+
"asset_valuation_date": row["asset_valuation_date"],
|
|
670
|
+
"is_estimated": True,
|
|
671
|
+
}
|
|
672
|
+
get_parameters = {
|
|
673
|
+
"underlying_instrument": row["underlying_instrument"],
|
|
674
|
+
"portfolio": self,
|
|
675
|
+
"currency": row["currency"],
|
|
676
|
+
"date": to_date,
|
|
677
|
+
}
|
|
678
|
+
if exchange := row.get("exchange", None):
|
|
679
|
+
get_parameters["exchange"] = exchange
|
|
680
|
+
if portfolio_created := row.get("portfolio_created", None):
|
|
681
|
+
get_parameters["portfolio_created"] = portfolio_created
|
|
682
|
+
# We check if an asset position already exists and if so, if it is estimated
|
|
683
|
+
# (otherwise we don't propagate it)
|
|
684
|
+
if _asset := AssetPosition.objects.filter(**get_parameters).first():
|
|
685
|
+
_asset.underlying_instrument_price = None # we unset the previously linked underlying instrument price in case it was linked to the wrong underlying price (e.g too early)
|
|
686
|
+
if not from_is_active and not to_is_active:
|
|
687
|
+
_asset.delete()
|
|
688
|
+
elif not is_target_portfolio_imported and _asset.is_estimated:
|
|
689
|
+
for k, v in defaults.items():
|
|
690
|
+
setattr(_asset, k, v)
|
|
691
|
+
_asset.save()
|
|
692
|
+
elif from_is_active and to_is_active and not is_target_portfolio_imported:
|
|
693
|
+
AssetPosition.objects.create(**get_parameters, **defaults)
|
|
694
|
+
|
|
695
|
+
def import_positions_at_date(self, portfolio: PortfolioDTO, val_date: date, post_processing: bool = False):
|
|
696
|
+
if not portfolio:
|
|
697
|
+
return
|
|
698
|
+
left_over_positions = self.assets.filter(date=val_date)
|
|
699
|
+
|
|
700
|
+
# We convert the positions into a dataframe in order to handle positions that are considered duplicates
|
|
701
|
+
# In that case, we sum up fields such as weighting and shares.
|
|
702
|
+
# Position are assumed serialized otherwise the groupby on dataframe can't handle django object
|
|
703
|
+
index_columns = ["portfolio_id", "date", "underlying_instrument_id", "portfolio_created_id"]
|
|
704
|
+
float_columns = [
|
|
705
|
+
"weighting",
|
|
706
|
+
"initial_currency_fx_rate",
|
|
707
|
+
"initial_shares",
|
|
708
|
+
"initial_price",
|
|
709
|
+
]
|
|
710
|
+
df = portfolio.to_df().rename(
|
|
711
|
+
columns={
|
|
712
|
+
"currency_fx_rate": "initial_currency_fx_rate",
|
|
713
|
+
"shares": "initial_shares",
|
|
714
|
+
"price": "initial_price",
|
|
715
|
+
"currency": "currency_id",
|
|
716
|
+
"underlying_instrument": "underlying_instrument_id",
|
|
717
|
+
"portfolio_created": "portfolio_created_id",
|
|
718
|
+
"exchange": "exchange_id",
|
|
719
|
+
}
|
|
720
|
+
)
|
|
721
|
+
df["portfolio_id"] = self.id
|
|
722
|
+
df = df[index_columns + float_columns + ["is_estimated", "currency_id"]]
|
|
723
|
+
df[float_columns] = df[float_columns].astype("float")
|
|
724
|
+
df = df.groupby(index_columns, as_index=False, dropna=False).agg(
|
|
725
|
+
{
|
|
726
|
+
**{field: "first" for field in df.columns.difference(index_columns + float_columns)},
|
|
727
|
+
"weighting": "sum",
|
|
728
|
+
"initial_shares": "sum",
|
|
729
|
+
"initial_currency_fx_rate": "mean",
|
|
730
|
+
"initial_price": "mean",
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
df = df.replace([np.inf, -np.inf, np.nan], None)
|
|
734
|
+
|
|
735
|
+
for position in df.to_dict("records"):
|
|
736
|
+
obj, _ = AssetPosition.unannotated_objects.update_or_create(
|
|
737
|
+
portfolio_id=position["portfolio_id"],
|
|
738
|
+
date=position["date"],
|
|
739
|
+
underlying_instrument_id=position["underlying_instrument_id"],
|
|
740
|
+
portfolio_created_id=position["portfolio_created_id"],
|
|
741
|
+
defaults=position,
|
|
742
|
+
)
|
|
743
|
+
left_over_positions = left_over_positions.exclude(id=obj.id)
|
|
744
|
+
left_over_positions.delete()
|
|
745
|
+
if post_processing:
|
|
746
|
+
trigger_portfolio_change_as_task.delay(self.id, val_date)
|
|
747
|
+
|
|
748
|
+
def resynchronize_history(self, from_date: date, to_date: date, instrument: Instrument | None = None):
|
|
749
|
+
if (synchronisation_method := self.portfolio_synchronization) and self.assets.exists():
|
|
750
|
+
if not from_date:
|
|
751
|
+
from_date = self.assets.earliest("date").date
|
|
752
|
+
if not to_date:
|
|
753
|
+
to_date = self.assets.latest("date").date
|
|
754
|
+
# loop over every week day and trigger synchronization task in order
|
|
755
|
+
if to_date <= from_date:
|
|
756
|
+
raise ValueError("bound needs to be valid")
|
|
757
|
+
for sync_datetime in synchronisation_method.dates_range(from_date, to_date, filter_daily=True):
|
|
758
|
+
synchronisation_method.synchronize(
|
|
759
|
+
self, sync_datetime.date(), override_execution_datetime_validity=True
|
|
760
|
+
)
|
|
761
|
+
if instrument:
|
|
762
|
+
price_computation_method = None
|
|
763
|
+
try:
|
|
764
|
+
price_computation_method = Product.objects.get(id=instrument.id).price_computation
|
|
765
|
+
except Product.DoesNotExist:
|
|
766
|
+
with suppress(Index.DoesNotExist):
|
|
767
|
+
price_computation_method = Index.objects.get(id=instrument.id).price_computation
|
|
768
|
+
if price_computation_method and instrument.prices.exists():
|
|
769
|
+
if to_date <= from_date:
|
|
770
|
+
raise ValueError("bound needs to be valid")
|
|
771
|
+
if not from_date:
|
|
772
|
+
from_date = instrument.prices.earliest("date").date
|
|
773
|
+
if not to_date:
|
|
774
|
+
to_date = instrument.prices.latest("date").date
|
|
775
|
+
# loop over every week day and trigger synchronization task in order
|
|
776
|
+
for sync_datetime in price_computation_method.dates_range(from_date, to_date, filter_daily=True):
|
|
777
|
+
price_computation_method.compute(
|
|
778
|
+
instrument, sync_datetime.date(), override_execution_datetime_validity=True
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
def update_preferred_classification_per_instrument(self):
|
|
782
|
+
# Function to automatically assign Preferred instrument based on the assets' underlying instruments of the
|
|
783
|
+
# attached wbportfolio
|
|
784
|
+
instruments = filter(
|
|
785
|
+
None,
|
|
786
|
+
map(
|
|
787
|
+
lambda x: Instrument.objects.get(id=x["underlying_instrument"]).get_classifable_ancestor(
|
|
788
|
+
include_self=True
|
|
789
|
+
),
|
|
790
|
+
self.assets.values("underlying_instrument").distinct("underlying_instrument"),
|
|
791
|
+
),
|
|
792
|
+
)
|
|
793
|
+
leftovers_instruments = list(
|
|
794
|
+
PortfolioInstrumentPreferredClassificationThroughModel.objects.filter(portfolio=self).values_list(
|
|
795
|
+
"instrument", flat=True
|
|
796
|
+
)
|
|
797
|
+
)
|
|
798
|
+
for instrument in instruments:
|
|
799
|
+
other_classifications = instrument.classifications.filter(group__is_primary=False)
|
|
800
|
+
default_classification = None
|
|
801
|
+
if other_classifications.count() == 1:
|
|
802
|
+
default_classification = other_classifications.first()
|
|
803
|
+
if not PortfolioInstrumentPreferredClassificationThroughModel.objects.filter(
|
|
804
|
+
portfolio=self, instrument=instrument
|
|
805
|
+
).exists():
|
|
806
|
+
PortfolioInstrumentPreferredClassificationThroughModel.objects.create(
|
|
807
|
+
portfolio=self,
|
|
808
|
+
instrument=instrument,
|
|
809
|
+
classification=default_classification,
|
|
810
|
+
classification_group=default_classification.group if default_classification else None,
|
|
811
|
+
)
|
|
812
|
+
if instrument.id in leftovers_instruments:
|
|
813
|
+
leftovers_instruments.remove(instrument.id)
|
|
814
|
+
|
|
815
|
+
for instrument_id in leftovers_instruments:
|
|
816
|
+
PortfolioInstrumentPreferredClassificationThroughModel.objects.filter(
|
|
817
|
+
portfolio=self, instrument=instrument_id
|
|
818
|
+
).delete()
|
|
819
|
+
|
|
820
|
+
@classmethod
|
|
821
|
+
def get_endpoint_basename(cls):
|
|
822
|
+
return "wbportfolio:portfolio"
|
|
823
|
+
|
|
824
|
+
@classmethod
|
|
825
|
+
def get_representation_endpoint(cls):
|
|
826
|
+
return "wbportfolio:portfoliorepresentation-list"
|
|
827
|
+
|
|
828
|
+
@classmethod
|
|
829
|
+
def get_representation_value_key(cls):
|
|
830
|
+
return "id"
|
|
831
|
+
|
|
832
|
+
@classmethod
|
|
833
|
+
def get_representation_label_key(cls):
|
|
834
|
+
return "{{name}}"
|
|
835
|
+
|
|
836
|
+
@classmethod
|
|
837
|
+
def _get_or_create_portfolio(cls, instrument_handler, portfolio_data):
|
|
838
|
+
if isinstance(portfolio_data, int):
|
|
839
|
+
return Portfolio.objects.get(id=portfolio_data)
|
|
840
|
+
instrument = portfolio_data
|
|
841
|
+
if isinstance(portfolio_data, dict):
|
|
842
|
+
instrument = instrument_handler.process_object(instrument, only_security=False, read_only=True)[0]
|
|
843
|
+
return instrument.primary_portfolio
|
|
844
|
+
|
|
845
|
+
def check_share_diff(self, val_date: date) -> bool:
|
|
846
|
+
return self.assets.filter(Q(date=val_date) & ~Q(initial_shares=F("initial_shares_at_custodian"))).exists()
|
|
847
|
+
|
|
848
|
+
@classmethod
|
|
849
|
+
def get_contribution_df(
|
|
850
|
+
cls,
|
|
851
|
+
qs,
|
|
852
|
+
need_normalize=False,
|
|
853
|
+
groupby_label_id="underlying_security",
|
|
854
|
+
groubpy_label_title="underlying_instrument__name_repr",
|
|
855
|
+
currency_fx_rate_label="currency_fx_rate",
|
|
856
|
+
hedged_currency=None,
|
|
857
|
+
):
|
|
858
|
+
# qs = AssetPosition.annotate_underlying_instrument(qs)
|
|
859
|
+
weight_label = "weighting" if not need_normalize else "total_value_fx_portfolio"
|
|
860
|
+
qs = qs.annotate(
|
|
861
|
+
is_hedged=Case(
|
|
862
|
+
When(
|
|
863
|
+
underlying_instrument__currency__isnull=False,
|
|
864
|
+
underlying_instrument__currency=hedged_currency,
|
|
865
|
+
then=Value(True),
|
|
866
|
+
),
|
|
867
|
+
default=Value(False),
|
|
868
|
+
output_field=BooleanField(),
|
|
869
|
+
),
|
|
870
|
+
coalesce_currency_fx_rate=Case(
|
|
871
|
+
When(is_hedged=True, then=Value(Decimal(1.0))),
|
|
872
|
+
default=F(currency_fx_rate_label),
|
|
873
|
+
output_field=models.BooleanField(),
|
|
874
|
+
),
|
|
875
|
+
).select_related("underlying_instrument")
|
|
876
|
+
df = pd.DataFrame(
|
|
877
|
+
qs.values(
|
|
878
|
+
"date",
|
|
879
|
+
"price",
|
|
880
|
+
"coalesce_currency_fx_rate",
|
|
881
|
+
groupby_label_id,
|
|
882
|
+
groubpy_label_title,
|
|
883
|
+
weight_label,
|
|
884
|
+
),
|
|
885
|
+
columns=[
|
|
886
|
+
"date",
|
|
887
|
+
"price",
|
|
888
|
+
"coalesce_currency_fx_rate",
|
|
889
|
+
groupby_label_id,
|
|
890
|
+
groubpy_label_title,
|
|
891
|
+
weight_label,
|
|
892
|
+
],
|
|
893
|
+
)
|
|
894
|
+
if not df.empty:
|
|
895
|
+
df = df[df[weight_label] != 0]
|
|
896
|
+
df.date = pd.to_datetime(df.date)
|
|
897
|
+
df["price_fx_portfolio"] = df.price * df.coalesce_currency_fx_rate
|
|
898
|
+
|
|
899
|
+
df[["price", "price_fx_portfolio", weight_label, "coalesce_currency_fx_rate"]] = df[
|
|
900
|
+
["price", "price_fx_portfolio", weight_label, "coalesce_currency_fx_rate"]
|
|
901
|
+
].astype("float")
|
|
902
|
+
|
|
903
|
+
df[groupby_label_id] = df[groupby_label_id].fillna(0)
|
|
904
|
+
df[groubpy_label_title] = df[groubpy_label_title].fillna("N/A")
|
|
905
|
+
df_static = df[[groupby_label_id, groubpy_label_title]].groupby(groupby_label_id, dropna=False).first()
|
|
906
|
+
|
|
907
|
+
df = (
|
|
908
|
+
df[
|
|
909
|
+
[
|
|
910
|
+
groupby_label_id,
|
|
911
|
+
"date",
|
|
912
|
+
"price",
|
|
913
|
+
"price_fx_portfolio",
|
|
914
|
+
weight_label,
|
|
915
|
+
"coalesce_currency_fx_rate",
|
|
916
|
+
]
|
|
917
|
+
]
|
|
918
|
+
.groupby(["date", groupby_label_id], dropna=False)
|
|
919
|
+
.agg(
|
|
920
|
+
{
|
|
921
|
+
"price": "mean",
|
|
922
|
+
"price_fx_portfolio": "mean",
|
|
923
|
+
weight_label: "sum",
|
|
924
|
+
"coalesce_currency_fx_rate": "mean",
|
|
925
|
+
}
|
|
926
|
+
)
|
|
927
|
+
.reset_index()
|
|
928
|
+
.set_index("date")
|
|
929
|
+
.sort_index()
|
|
930
|
+
)
|
|
931
|
+
df[weight_label] = df[weight_label].fillna(0)
|
|
932
|
+
value = df.pivot_table(
|
|
933
|
+
index="date",
|
|
934
|
+
columns=[groupby_label_id],
|
|
935
|
+
values=weight_label,
|
|
936
|
+
fill_value=0,
|
|
937
|
+
aggfunc="sum",
|
|
938
|
+
)
|
|
939
|
+
weights_ = value
|
|
940
|
+
if need_normalize:
|
|
941
|
+
total_value_price = df[weight_label].groupby("date", dropna=False).sum()
|
|
942
|
+
weights_ = value.divide(total_value_price, axis=0)
|
|
943
|
+
prices_usd = (
|
|
944
|
+
df.pivot_table(
|
|
945
|
+
index="date",
|
|
946
|
+
columns=[groupby_label_id],
|
|
947
|
+
values="price_fx_portfolio",
|
|
948
|
+
aggfunc="mean",
|
|
949
|
+
)
|
|
950
|
+
.replace(0, np.nan)
|
|
951
|
+
.bfill()
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
rates_fx = (
|
|
955
|
+
df.pivot_table(
|
|
956
|
+
index="date",
|
|
957
|
+
columns=[groupby_label_id],
|
|
958
|
+
values="coalesce_currency_fx_rate",
|
|
959
|
+
aggfunc="mean",
|
|
960
|
+
)
|
|
961
|
+
.replace(0, np.nan)
|
|
962
|
+
.bfill()
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
prices_usd = prices_usd.ffill()
|
|
966
|
+
performance_prices = prices_usd / prices_usd.shift(1, axis=0) - 1
|
|
967
|
+
contributions_prices = performance_prices.multiply(weights_.shift(1, axis=0)).dropna(how="all")
|
|
968
|
+
total_contrib_prices = (1 + contributions_prices.sum(axis=1)).shift(1, fill_value=1.0).cumprod()
|
|
969
|
+
contributions_prices = contributions_prices.multiply(total_contrib_prices, axis=0).sum(skipna=False)
|
|
970
|
+
monthly_perf_prices = (1 + performance_prices).dropna(how="all").product(axis=0, skipna=False) - 1
|
|
971
|
+
|
|
972
|
+
rates_fx = rates_fx.ffill()
|
|
973
|
+
performance_rates_fx = rates_fx / rates_fx.shift(1, axis=0) - 1
|
|
974
|
+
contributions_rates_fx = performance_rates_fx.multiply(weights_.shift(1, axis=0)).dropna(how="all")
|
|
975
|
+
total_contrib_rates_fx = (1 + contributions_rates_fx.sum(axis=1)).shift(1, fill_value=1.0).cumprod()
|
|
976
|
+
contributions_rates_fx = contributions_rates_fx.multiply(total_contrib_rates_fx, axis=0).sum(skipna=False)
|
|
977
|
+
monthly_perf_rates_fx = (1 + performance_rates_fx).dropna(how="all").product(axis=0, skipna=False) - 1
|
|
978
|
+
|
|
979
|
+
res = pd.concat(
|
|
980
|
+
[
|
|
981
|
+
df_static,
|
|
982
|
+
monthly_perf_prices,
|
|
983
|
+
monthly_perf_rates_fx,
|
|
984
|
+
contributions_prices,
|
|
985
|
+
contributions_rates_fx,
|
|
986
|
+
weights_.iloc[0, :],
|
|
987
|
+
weights_.iloc[-1, :],
|
|
988
|
+
value.iloc[0, :],
|
|
989
|
+
value.iloc[-1, :],
|
|
990
|
+
],
|
|
991
|
+
axis=1,
|
|
992
|
+
).reset_index()
|
|
993
|
+
res.columns = [
|
|
994
|
+
groupby_label_id,
|
|
995
|
+
groubpy_label_title,
|
|
996
|
+
"performance_total",
|
|
997
|
+
"performance_forex",
|
|
998
|
+
"contribution_total",
|
|
999
|
+
"contribution_forex",
|
|
1000
|
+
"allocation_start",
|
|
1001
|
+
"allocation_end",
|
|
1002
|
+
"total_value_start",
|
|
1003
|
+
"total_value_end",
|
|
1004
|
+
]
|
|
1005
|
+
|
|
1006
|
+
return res.replace([np.inf, -np.inf, np.nan], 0)
|
|
1007
|
+
return pd.DataFrame()
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@receiver(post_save, sender="wbportfolio.Product")
|
|
1011
|
+
@receiver(post_save, sender="wbportfolio.ProductGroup")
|
|
1012
|
+
@receiver(post_save, sender="wbportfolio.Index")
|
|
1013
|
+
def post_product_creation(sender, instance, created, raw, **kwargs):
|
|
1014
|
+
if not raw and (created or not InstrumentPortfolioThroughModel.objects.filter(instrument=instance).exists()):
|
|
1015
|
+
portfolio = Portfolio.objects.create(
|
|
1016
|
+
name=f"Portfolio: {instance.name}",
|
|
1017
|
+
currency=instance.currency,
|
|
1018
|
+
invested_timespan=DateRange(instance.inception_date if instance.inception_date else date.min, date.max),
|
|
1019
|
+
)
|
|
1020
|
+
InstrumentPortfolioThroughModel.objects.get_or_create(instrument=instance, defaults={"portfolio": portfolio})
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
@shared_task(queue="portfolio")
|
|
1024
|
+
def resynchronize_history_as_task(portfolio_id: int, from_date: date, to_date: date, instrument_id: int | None = None):
|
|
1025
|
+
portfolio = Portfolio.objects.get(id=portfolio_id)
|
|
1026
|
+
instrument = Instrument.objects.get(id=instrument_id) if instrument_id else None
|
|
1027
|
+
portfolio.resynchronize_history(from_date, to_date, instrument=instrument)
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
@shared_task(queue="portfolio")
|
|
1031
|
+
def trigger_portfolio_change_as_task(portfolio_id, val_date, **kwargs):
|
|
1032
|
+
portfolio = Portfolio.all_objects.get(id=portfolio_id)
|
|
1033
|
+
portfolio.change_at_date(val_date, **kwargs)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
@shared_task(queue="portfolio")
|
|
1037
|
+
def propagate_or_update_portfolio_assets_as_task(portfolio_id, from_date, to_date, **kwargs):
|
|
1038
|
+
portfolio = Portfolio.objects.get(id=portfolio_id)
|
|
1039
|
+
portfolio.propagate_or_update_assets(from_date, to_date, **kwargs)
|