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,1307 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
from pandas.tseries.offsets import BDay
|
|
8
|
+
from plotly.subplots import make_subplots
|
|
9
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
10
|
+
from wbfdm.enums import MarketData
|
|
11
|
+
from wbfdm.models import Instrument as InstrumentFDM
|
|
12
|
+
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LiquidityStressMixin:
|
|
16
|
+
def get_product_ids_from_group_product_or_product(self) -> [None, list]:
|
|
17
|
+
"""
|
|
18
|
+
The function returns a list of id(s) if:
|
|
19
|
+
- a simple product id if instrument_type == Product.
|
|
20
|
+
- the list of the product ids if instrument_type == ProductGroup.
|
|
21
|
+
- the list of the products ids if instrument_type == Product and if the product belongs to a group.
|
|
22
|
+
Otherwise the function returns None.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if self.instrument_type not in ["Product", "ProductGroup"]:
|
|
26
|
+
return None
|
|
27
|
+
products_id = [self.pk] # if self is a Product and has not a self.group, it keeps this value.
|
|
28
|
+
casted_instrument = self.get_casted_instrument()
|
|
29
|
+
if self.instrument_type.key == "product_group":
|
|
30
|
+
products_id = casted_instrument.products.values_list("id", flat=True)
|
|
31
|
+
elif self.instrument_type.key == "product" and casted_instrument.group:
|
|
32
|
+
products_id = casted_instrument.group.products.values_list("id", flat=True)
|
|
33
|
+
|
|
34
|
+
return products_id
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def waterfall_and_slicing_calculation(
|
|
38
|
+
mean_volume_by_id: pd.Series, price_change: float, df_asset_positions: pd.DataFrame, liq_factor: float
|
|
39
|
+
) -> pd.DataFrame:
|
|
40
|
+
df = pd.DataFrame(index=df_asset_positions.index, columns=["waterfall", "slicing"])
|
|
41
|
+
df["waterfall"] = df_asset_positions.shares.div(mean_volume_by_id * liq_factor, level="instrument")
|
|
42
|
+
df["waterfall"] = df["waterfall"].mask(df["waterfall"] > 360, 360)
|
|
43
|
+
stock_index = df.loc[:, "waterfall"].dropna().index
|
|
44
|
+
df.loc[stock_index, "slicing"] = 1 / ((1 + price_change) / df["waterfall"]).loc[("Equity", slice(None))].min()
|
|
45
|
+
return df
|
|
46
|
+
|
|
47
|
+
def get_volumes_from_scenario_stress_test(
|
|
48
|
+
self, weights_date: date, df_volumes: pd.DataFrame, df_asset_positions: pd.DataFrame, liq_factor: float
|
|
49
|
+
) -> pd.DataFrame:
|
|
50
|
+
# Depending on the scenario, we slice the volume DataFrame for the corresponding dates.
|
|
51
|
+
scenarios = [
|
|
52
|
+
"Baseline Scenario",
|
|
53
|
+
"COVID-19",
|
|
54
|
+
"Lehman",
|
|
55
|
+
"Lehman Spring",
|
|
56
|
+
"Debt Crisis",
|
|
57
|
+
"China Crisis",
|
|
58
|
+
"Dotcom",
|
|
59
|
+
"Volume falls by 60 pct",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def get_volume_period_scenario(scenario_name: str):
|
|
63
|
+
import datetime as dt
|
|
64
|
+
|
|
65
|
+
from pandas.tseries.offsets import BDay
|
|
66
|
+
|
|
67
|
+
if scenario_name == "Baseline Scenario" or scenario_name == "Volume falls by 60 pct":
|
|
68
|
+
start, end = (weights_date - BDay(50)).date(), weights_date
|
|
69
|
+
|
|
70
|
+
elif scenario_name == "COVID-19":
|
|
71
|
+
start, end = dt.date(2021, 2, 10), dt.date(2021, 3, 23)
|
|
72
|
+
|
|
73
|
+
elif scenario_name == "Lehman":
|
|
74
|
+
start, end = dt.date(2008, 10, 3), dt.date(2008, 10, 10)
|
|
75
|
+
|
|
76
|
+
elif scenario_name == "Lehman Spring":
|
|
77
|
+
start, end = dt.date(2009, 2, 16), dt.date(2009, 2, 23)
|
|
78
|
+
|
|
79
|
+
elif scenario_name == "Debt Crisis":
|
|
80
|
+
start, end = dt.date(2011, 8, 1), dt.date(2011, 8, 11)
|
|
81
|
+
|
|
82
|
+
elif scenario_name == "China Crisis":
|
|
83
|
+
start, end = dt.date(2015, 8, 15), dt.date(2015, 8, 24)
|
|
84
|
+
|
|
85
|
+
elif scenario_name == "Dotcom":
|
|
86
|
+
start, end = dt.date(2002, 7, 15), dt.date(2002, 7, 22)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
return pd.DataFrame() # No existing scenario
|
|
90
|
+
|
|
91
|
+
df_volume_scenario = df_volumes.loc[:, start:end]
|
|
92
|
+
return df_volume_scenario
|
|
93
|
+
|
|
94
|
+
# Depending to the method, the "days to liquidate"'s result is different.
|
|
95
|
+
methods = ["waterfall", "slicing"]
|
|
96
|
+
|
|
97
|
+
# ---- SCENARIOS CALCULATION ---- :
|
|
98
|
+
price_change_factor = 0
|
|
99
|
+
multi_columns = pd.MultiIndex.from_product([scenarios, methods], names=["scenario", "method"])
|
|
100
|
+
days_to_liquidate = pd.DataFrame(index=df_asset_positions.index, columns=multi_columns)
|
|
101
|
+
liquidity_equivalent_one_day = pd.DataFrame(index=df_asset_positions.index, columns=scenarios)
|
|
102
|
+
|
|
103
|
+
for scenario in scenarios:
|
|
104
|
+
volume_scenario = get_volume_period_scenario(scenario)
|
|
105
|
+
if volume_scenario.empty:
|
|
106
|
+
continue
|
|
107
|
+
mean_volume = volume_scenario.mean(axis=1)
|
|
108
|
+
if scenario == "Volume falls by 60 pct":
|
|
109
|
+
mean_volume *= 0.4
|
|
110
|
+
|
|
111
|
+
days_to_liquidate.loc[:, (scenario, methods)] = self.waterfall_and_slicing_calculation(
|
|
112
|
+
mean_volume, price_change_factor, df_asset_positions, liq_factor
|
|
113
|
+
).values
|
|
114
|
+
liquidity_equivalent_one_day[scenario] = (
|
|
115
|
+
(1 + price_change_factor)
|
|
116
|
+
* df_asset_positions.total_value_usd
|
|
117
|
+
/ days_to_liquidate.loc[(slice(None), mean_volume.index), (scenario, "waterfall")]
|
|
118
|
+
)
|
|
119
|
+
return days_to_liquidate
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def get_percentile_worst_dollar_volume(df_dollar_volume: pd.DataFrame, method: str = "mean_below_worst"):
|
|
123
|
+
list_pct_worst_volume = range(0, 101)
|
|
124
|
+
multi_index = pd.MultiIndex.from_product(
|
|
125
|
+
[list_pct_worst_volume, df_dollar_volume.index.values], names=["x%_worst_volume", "instrument"]
|
|
126
|
+
)
|
|
127
|
+
average_worst_dollar_volume = pd.Series(index=multi_index, dtype=float)
|
|
128
|
+
for pct_worst_volume in list_pct_worst_volume:
|
|
129
|
+
if method == "mean_below_worst":
|
|
130
|
+
average_worst_dollar_volume.loc[(pct_worst_volume, slice(None))] = (
|
|
131
|
+
df_dollar_volume.where(
|
|
132
|
+
df_dollar_volume.le(
|
|
133
|
+
df_dollar_volume.quantile(pct_worst_volume / 100, axis=1, interpolation="midpoint"), axis=0
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
.mean(axis=1)
|
|
137
|
+
.values
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
average_worst_dollar_volume.loc[(pct_worst_volume, slice(None))] = df_dollar_volume.quantile(
|
|
141
|
+
pct_worst_volume / 100, axis=1, interpolation="midpoint"
|
|
142
|
+
).values
|
|
143
|
+
return average_worst_dollar_volume
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def reverse_stress_test(
|
|
147
|
+
total_aum: float,
|
|
148
|
+
df_asset_positions: pd.DataFrame,
|
|
149
|
+
average_worst_dollar_volume: pd.Series,
|
|
150
|
+
liq_factor: float,
|
|
151
|
+
below_x_days: int = 5,
|
|
152
|
+
) -> pd.DataFrame:
|
|
153
|
+
# Remove cash instruments from the instruments liquidity estimation
|
|
154
|
+
cash_instruments = df_asset_positions.loc[("Cash", slice(None))].index.unique("instrument")
|
|
155
|
+
instruments_id = average_worst_dollar_volume.index.unique("instrument")
|
|
156
|
+
instruments_id = instruments_id.drop(cash_instruments)
|
|
157
|
+
|
|
158
|
+
x_pct = average_worst_dollar_volume.index.unique(0)
|
|
159
|
+
aum_multiplication = range(1, 51)
|
|
160
|
+
multi_index = pd.MultiIndex.from_product(
|
|
161
|
+
[instruments_id, x_pct, x_pct],
|
|
162
|
+
names=["instrument", "x%_redemption", "x%_worst_volume"],
|
|
163
|
+
)
|
|
164
|
+
df = pd.DataFrame(index=multi_index)
|
|
165
|
+
|
|
166
|
+
df = df.join(pd.DataFrame(average_worst_dollar_volume, columns=["average_worst_dollar_volume"]))
|
|
167
|
+
df = df.join(df_asset_positions.weighting.droplevel("type"), on="instrument")
|
|
168
|
+
for multiplication in aum_multiplication:
|
|
169
|
+
amount_to_liquidate = (
|
|
170
|
+
total_aum * multiplication * df.index.get_level_values("x%_redemption") / 100 * df["weighting"]
|
|
171
|
+
)
|
|
172
|
+
df[f"days_to_liquidate {multiplication}"] = amount_to_liquidate / (
|
|
173
|
+
liq_factor * df["average_worst_dollar_volume"]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
df_days_to_liquidate = df.filter(like="days_to_liquidate")
|
|
177
|
+
df_weights_sold = (1 / df_days_to_liquidate).mul(df["weighting"], axis="index") * below_x_days
|
|
178
|
+
|
|
179
|
+
# TODO: cannot use mask on series because it does not work with pandas 1.5.3.
|
|
180
|
+
weights = df.weighting.to_frame(name=df_weights_sold.columns[0])
|
|
181
|
+
weights = weights.reindex(df_weights_sold.columns, axis=1).ffill(axis=1)
|
|
182
|
+
df_weights_sold = df_weights_sold.mask(df_weights_sold > weights, weights)
|
|
183
|
+
df_weights_sold = df_weights_sold.groupby(["x%_redemption", "x%_worst_volume"]).sum()
|
|
184
|
+
df = df_weights_sold.unstack(level=0)
|
|
185
|
+
|
|
186
|
+
# Add cash weights
|
|
187
|
+
cash = df_asset_positions.loc[("Cash", slice(None))].weighting.sum()
|
|
188
|
+
df += cash
|
|
189
|
+
return df
|
|
190
|
+
|
|
191
|
+
def stress_volume_bid_ask_test(
|
|
192
|
+
self,
|
|
193
|
+
df_asset_positions: pd.DataFrame,
|
|
194
|
+
df_dollar_volume: pd.DataFrame,
|
|
195
|
+
df_volumes: pd.DataFrame,
|
|
196
|
+
df_bid_ask_spread: pd.DataFrame,
|
|
197
|
+
liq_factor: float,
|
|
198
|
+
pct_worst_dollar_volume: float = 0.1,
|
|
199
|
+
pct_worst_volume: float = 0.1,
|
|
200
|
+
pct_higher_bid_ask_spread: float = 0.9,
|
|
201
|
+
price_change_factor: float = 0,
|
|
202
|
+
acceptable_loss: float = 0.3,
|
|
203
|
+
) -> pd.DataFrame:
|
|
204
|
+
worst_dollar_volumes = df_dollar_volume.where(
|
|
205
|
+
df_dollar_volume.le(df_dollar_volume.quantile(pct_worst_dollar_volume, axis=1), axis=0)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# volumes:
|
|
209
|
+
worst_corresponding_volumes = df_volumes.where(worst_dollar_volumes.notnull())
|
|
210
|
+
worst_volumes = worst_corresponding_volumes.where(
|
|
211
|
+
worst_corresponding_volumes.le(worst_corresponding_volumes.quantile(pct_worst_volume, axis=1), axis=0)
|
|
212
|
+
)
|
|
213
|
+
mean_worst_volumes = worst_volumes.mean(axis=1)
|
|
214
|
+
|
|
215
|
+
scenarios = ["S-T Worst Volume", "S-T Worst B-A"]
|
|
216
|
+
methods = ["waterfall", "slicing"]
|
|
217
|
+
multi_columns = pd.MultiIndex.from_product([scenarios, methods], names=["scenario", "method"])
|
|
218
|
+
days_to_liquidate = pd.DataFrame(index=df_asset_positions.index, columns=multi_columns)
|
|
219
|
+
|
|
220
|
+
days_to_liquidate.loc[:, ("S-T Worst Volume", methods)] = self.waterfall_and_slicing_calculation(
|
|
221
|
+
mean_worst_volumes, price_change_factor, df_asset_positions, liq_factor
|
|
222
|
+
).values
|
|
223
|
+
|
|
224
|
+
# bid-ask spread:
|
|
225
|
+
worst_corresponding_bid_ask_spread = df_bid_ask_spread.where(worst_dollar_volumes.notnull())
|
|
226
|
+
worst_bid_ask_spread = worst_corresponding_bid_ask_spread.where(
|
|
227
|
+
worst_corresponding_bid_ask_spread.ge(
|
|
228
|
+
worst_corresponding_bid_ask_spread.quantile(pct_higher_bid_ask_spread, axis=1), axis=0
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# we do not use the function to calculate waterfall here because formula is different.
|
|
233
|
+
days_to_liquidate.loc[(slice(None), worst_bid_ask_spread.index), ("S-T Worst B-A", "waterfall")] = (
|
|
234
|
+
worst_bid_ask_spread.mean(axis=1) / acceptable_loss
|
|
235
|
+
).values
|
|
236
|
+
days_to_liquidate.loc[(slice(None), worst_bid_ask_spread.index), ("S-T Worst B-A", "slicing")] = (
|
|
237
|
+
1 / ((1 + price_change_factor) / days_to_liquidate.loc[:, ("S-T Worst B-A", "waterfall")]).min()
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return days_to_liquidate
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def aggregate_days_to_liquidate(days_to_liquidate: pd.DataFrame) -> pd.DataFrame:
|
|
244
|
+
# Aggregation:
|
|
245
|
+
instrument_types = days_to_liquidate.index.get_level_values("type").drop_duplicates()
|
|
246
|
+
timeline = ["1 day or less", "2-7 days", "8-15 days", "16-30 days", "31-60 days", "61-180 days"]
|
|
247
|
+
df_aggregate = pd.DataFrame(
|
|
248
|
+
index=pd.MultiIndex.from_product([instrument_types, timeline], names=["type", "time"]),
|
|
249
|
+
columns=days_to_liquidate.drop("instrument", axis=1, level=0).columns,
|
|
250
|
+
)
|
|
251
|
+
_dict = {
|
|
252
|
+
"1 day or less": [0, 1],
|
|
253
|
+
"2-7 days": [1, 7],
|
|
254
|
+
"8-15 days": [7, 15],
|
|
255
|
+
"16-30 days": [15, 30],
|
|
256
|
+
"31-60 days": [30, 60],
|
|
257
|
+
"61-180 days": [60, 180],
|
|
258
|
+
}
|
|
259
|
+
df_waterfall = days_to_liquidate.loc[:, (slice(None), "waterfall")]
|
|
260
|
+
df_slicing = days_to_liquidate.loc[:, (slice(None), "slicing")]
|
|
261
|
+
type_index = pd.Index(instrument_types)
|
|
262
|
+
for period, value in _dict.items():
|
|
263
|
+
tmp_waterfall = df_waterfall.where((df_waterfall >= value[0]) & (df_waterfall <= value[1]))
|
|
264
|
+
for scenario in days_to_liquidate.drop(("instrument", "weighting"), axis=1).columns:
|
|
265
|
+
all_index = days_to_liquidate.loc[:, scenario].dropna().index
|
|
266
|
+
total_weight = days_to_liquidate.loc[all_index, ("instrument", "weighting")]
|
|
267
|
+
if scenario[1] == "waterfall":
|
|
268
|
+
selection_idx = tmp_waterfall.loc[:, scenario].dropna().index
|
|
269
|
+
total_weight = total_weight.sum()
|
|
270
|
+
sliced_weight = (
|
|
271
|
+
days_to_liquidate.loc[selection_idx, ("instrument", "weighting")]
|
|
272
|
+
.groupby("type")
|
|
273
|
+
.sum()
|
|
274
|
+
.reindex(type_index)
|
|
275
|
+
)
|
|
276
|
+
df_aggregate.loc[(slice(None), period), scenario] = (sliced_weight / total_weight).values
|
|
277
|
+
else: # slicing
|
|
278
|
+
tmp_slicing = df_slicing.loc[:, scenario]
|
|
279
|
+
total_weight = total_weight.groupby("type").sum() / total_weight.sum()
|
|
280
|
+
total_weight.name = ("instrument", "agg_weights")
|
|
281
|
+
df_aggregate.loc[(slice(None), period), scenario] = (
|
|
282
|
+
total_weight / tmp_slicing.groupby("type").max() * value[1]
|
|
283
|
+
).values
|
|
284
|
+
df_aggregate = df_aggregate.join(total_weight)
|
|
285
|
+
df_aggregate.loc[(slice(None), period), scenario] = df_aggregate.loc[
|
|
286
|
+
(slice(None), period), scenario
|
|
287
|
+
].mask(
|
|
288
|
+
(
|
|
289
|
+
df_aggregate.loc[(slice(None), period), scenario]
|
|
290
|
+
> df_aggregate.loc[(slice(None), period), ("instrument", "agg_weights")]
|
|
291
|
+
)
|
|
292
|
+
| (df_aggregate.loc[(slice(None), period), scenario] == -np.inf),
|
|
293
|
+
df_aggregate.loc[(slice(None), period), ("instrument", "agg_weights")],
|
|
294
|
+
)
|
|
295
|
+
df_aggregate.drop(("instrument", "agg_weights"), axis=1, inplace=True)
|
|
296
|
+
df_aggregate.loc[:, (slice(None), "waterfall")] = (
|
|
297
|
+
df_aggregate.loc[:, (slice(None), "waterfall")].fillna(0).groupby("type").cumsum()
|
|
298
|
+
)
|
|
299
|
+
df_aggregate_portfolio = df_aggregate.stack("method").groupby(["time", "method"]).sum()
|
|
300
|
+
df_aggregate_portfolio = pd.concat({"Portfolio": df_aggregate_portfolio}, names=["type"]).unstack("method")
|
|
301
|
+
df_aggregate_portfolio = df_aggregate_portfolio.reindex(labels=timeline, level="time")
|
|
302
|
+
df_aggregate = pd.concat([df_aggregate_portfolio, df_aggregate])
|
|
303
|
+
return df_aggregate
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def series_of_colors(portfolio_value: pd.Series) -> pd.DataFrame:
|
|
307
|
+
portfolio_value_colors = pd.DataFrame(columns=["colors", "message"])
|
|
308
|
+
portfolio_value_colors.loc[:, "colors"] = portfolio_value.astype(float).round(4)
|
|
309
|
+
for k, v in portfolio_value_colors.colors.items():
|
|
310
|
+
v_adj = round(v * 100, 2)
|
|
311
|
+
if k == "1 day or less":
|
|
312
|
+
portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 0.7 else "#FBE426"
|
|
313
|
+
portfolio_value_colors.at[k, "message"] = (
|
|
314
|
+
pd.NA if v >= 0.7 else f"to liquidate {v_adj}%, more than 1 day is needed"
|
|
315
|
+
)
|
|
316
|
+
elif k == "2-7 days":
|
|
317
|
+
portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 0.8 else "#FBE426"
|
|
318
|
+
portfolio_value_colors.at[k, "message"] = (
|
|
319
|
+
pd.NA if v >= 0.8 else f"to liquidate {v_adj}%, more than 7 days are needed"
|
|
320
|
+
)
|
|
321
|
+
elif k == "8-15 days":
|
|
322
|
+
portfolio_value_colors.at[k, "colors"] = (
|
|
323
|
+
"#0FFBA6" if v >= 0.9 else "#FBE426" if v >= 0.7 else "#FC6955"
|
|
324
|
+
)
|
|
325
|
+
portfolio_value_colors.at[k, "message"] = (
|
|
326
|
+
pd.NA if v >= 0.9 else f"to liquidate {v_adj}%, more than 15 days are needed"
|
|
327
|
+
)
|
|
328
|
+
elif k == "16-30 days":
|
|
329
|
+
portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 1 else "#FBE426" if v >= 0.9 else "#FC6955"
|
|
330
|
+
portfolio_value_colors.at[k, "message"] = (
|
|
331
|
+
pd.NA if v >= 1 else f"to liquidate {v_adj}%, more than 30 days are needed"
|
|
332
|
+
)
|
|
333
|
+
elif k == "31-60 days":
|
|
334
|
+
portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 1 else "#FBE426" if v >= 0.95 else "#FC6955"
|
|
335
|
+
portfolio_value_colors.at[k, "message"] = (
|
|
336
|
+
pd.NA if v >= 1 else f"to liquidate {v_adj}%, more than 60 days are needed"
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
portfolio_value_colors.at[k, "colors"] = "#0FFBA6" if v >= 1 else "#FBE426" if v >= 0.99 else "#FC6955"
|
|
340
|
+
portfolio_value_colors.at[k, "message"] = (
|
|
341
|
+
pd.NA if v >= 1 else f"to liquidate {v_adj}%, more than 180 days are needed"
|
|
342
|
+
)
|
|
343
|
+
return portfolio_value_colors
|
|
344
|
+
|
|
345
|
+
def liquidity_monitor_graph(self, portfolio_value: pd.Series, expectation_net_redemption_df: pd.DataFrame):
|
|
346
|
+
index = portfolio_value.index
|
|
347
|
+
# portfolio_value = df_aggregate.loc[("Portfolio", slice(None)), (scenario, method)].droplevel(0, axis=0)
|
|
348
|
+
portfolio_value_colors = self.series_of_colors(portfolio_value)
|
|
349
|
+
|
|
350
|
+
portfolio_value_list = portfolio_value.mul(100).values.tolist()
|
|
351
|
+
expectation = expectation_net_redemption_df.mul(100).values.tolist()
|
|
352
|
+
fig = make_subplots(rows=1, cols=2, subplot_titles=("Liquidity Balance Overview", "Redemption Coverage Ratio"))
|
|
353
|
+
fig.add_bar(
|
|
354
|
+
x=index,
|
|
355
|
+
y=portfolio_value_list,
|
|
356
|
+
row=1,
|
|
357
|
+
col=1,
|
|
358
|
+
marker=dict(color=portfolio_value_colors.colors),
|
|
359
|
+
name=portfolio_value.name[1],
|
|
360
|
+
)
|
|
361
|
+
fig.add_bar(
|
|
362
|
+
x=index,
|
|
363
|
+
y=expectation,
|
|
364
|
+
row=1,
|
|
365
|
+
col=1,
|
|
366
|
+
marker=dict(color="rgb(179, 179, 179)"),
|
|
367
|
+
name="Expected Net Redemption",
|
|
368
|
+
)
|
|
369
|
+
fig.update_yaxes(ticksuffix="%", row=1, col=1, range=[0, 108])
|
|
370
|
+
fig.update_traces(texttemplate="%{value}", row=1, col=1)
|
|
371
|
+
|
|
372
|
+
rcr = pd.Series(portfolio_value_list, index=index) / pd.Series(expectation, index=index) * 100
|
|
373
|
+
rcr = rcr.mask(rcr > 200, 200.00001)
|
|
374
|
+
rcr_colors = rcr.map(lambda x: "#0FFBA6" if x > 200 else "#FC6955" if x < 120 else "#FBE426")
|
|
375
|
+
fig.add_bar(
|
|
376
|
+
x=rcr,
|
|
377
|
+
y=index,
|
|
378
|
+
row=1,
|
|
379
|
+
col=2,
|
|
380
|
+
marker_color=rcr_colors,
|
|
381
|
+
orientation="h",
|
|
382
|
+
text=[f"{val: .0f}%" if val < 200 else f">{val: .0f}%" for val in rcr],
|
|
383
|
+
)
|
|
384
|
+
fig["data"][2].width = 0.6
|
|
385
|
+
fig["data"][2]["showlegend"] = False
|
|
386
|
+
fig.update_xaxes(ticksuffix="%", range=[0, 225], row=1, col=2)
|
|
387
|
+
|
|
388
|
+
fig.update_traces(textposition="outside")
|
|
389
|
+
fig.update_layout(
|
|
390
|
+
barmode="group",
|
|
391
|
+
font_size=12,
|
|
392
|
+
uniformtext_mode="hide",
|
|
393
|
+
title_font_size=20,
|
|
394
|
+
yaxis_title="Percent (%)",
|
|
395
|
+
legend=dict(orientation="h", yanchor="bottom", y=1.1, xanchor="left"),
|
|
396
|
+
)
|
|
397
|
+
return fig
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def asset_liquidity_graph(df_aggregate, scenario, method):
|
|
401
|
+
pd.options.plotting.backend = "plotly"
|
|
402
|
+
df = (df_aggregate.loc[:, (scenario, method)] * 100).astype(float).round(2)
|
|
403
|
+
fig = make_subplots(
|
|
404
|
+
rows=1,
|
|
405
|
+
cols=2,
|
|
406
|
+
specs=[[{"type": "bar"}, {"type": "table"}]],
|
|
407
|
+
subplot_titles=("Liquidity profile - Breakdown per asset type", "Available Resources (% NAV)"),
|
|
408
|
+
)
|
|
409
|
+
index_bar = df.index.drop("Portfolio", level=0) # for bar plot, we do not need portfolio (total) value
|
|
410
|
+
for instrument_type in index_bar.get_level_values(0).drop_duplicates()[::-1]:
|
|
411
|
+
series = df.loc[(instrument_type, slice(None))]
|
|
412
|
+
fig.add_bar(name=instrument_type, x=series.index, y=series.values, row=1, col=1)
|
|
413
|
+
fig.update_yaxes(ticksuffix="%", title="Percent (%)", row=1, col=1)
|
|
414
|
+
|
|
415
|
+
df_table = df.unstack() # Asset Type in index ; Time bucket in columns
|
|
416
|
+
df_table = df_table.reindex(df.index.get_level_values(1).drop_duplicates(), axis=1) # preserve columns order
|
|
417
|
+
df_table = df_table.reindex(df.index.get_level_values(0).drop_duplicates(), axis=0) # preserve index order
|
|
418
|
+
df_values = df_table.T.values.tolist()
|
|
419
|
+
df_values.insert(0, df_table.index.to_list()) # Index in values for the table.
|
|
420
|
+
fig.add_table(
|
|
421
|
+
header=dict(
|
|
422
|
+
values=df_table.columns.insert(0, "Asset Type").to_list(),
|
|
423
|
+
line_color="darkslategray",
|
|
424
|
+
fill_color="royalblue",
|
|
425
|
+
align=["left", "center"],
|
|
426
|
+
font=dict(color="white", size=12),
|
|
427
|
+
height=50,
|
|
428
|
+
),
|
|
429
|
+
cells=dict(
|
|
430
|
+
values=df_values,
|
|
431
|
+
line_color="darkslategray",
|
|
432
|
+
align=["left", "center"],
|
|
433
|
+
font=dict(color="black", size=11),
|
|
434
|
+
suffix=[None] + ["%"] * 5,
|
|
435
|
+
fill=dict(color=["paleturquoise", ["lightgrey", "white"]]),
|
|
436
|
+
height=40,
|
|
437
|
+
),
|
|
438
|
+
row=1,
|
|
439
|
+
col=2,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
fig.update_layout(barmode="stack", legend=dict(orientation="h", yanchor="bottom", y=1.1, xanchor="left"))
|
|
443
|
+
|
|
444
|
+
return fig
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def liquidity_bucketing_graph(df_aggregate, scenario):
|
|
448
|
+
pd.options.plotting.backend = "plotly"
|
|
449
|
+
df = (
|
|
450
|
+
(df_aggregate.loc[("Portfolio", slice(None)), (scenario, slice(None))] * 100)
|
|
451
|
+
.astype(float)
|
|
452
|
+
.round(2)
|
|
453
|
+
.droplevel(0)
|
|
454
|
+
.droplevel(0, axis=1)
|
|
455
|
+
)
|
|
456
|
+
df = df.reindex(["waterfall", "slicing"], axis="columns")
|
|
457
|
+
df.rename(
|
|
458
|
+
index={
|
|
459
|
+
"1 day or less": "Very High Liquidity",
|
|
460
|
+
"2-7 days": "High Liquidity",
|
|
461
|
+
"8-15 days": "Medium Liquidity",
|
|
462
|
+
"16-30 days": "Low Liquidity",
|
|
463
|
+
"31-60 days": "Very Low Liquidity",
|
|
464
|
+
"61-180 days": "Almost No Liquidity",
|
|
465
|
+
},
|
|
466
|
+
inplace=True,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
colors = ["green", "lightgreen", "lightblue", "#FC6955", "orange", "red"]
|
|
470
|
+
fig = make_subplots(rows=1, cols=2, subplot_titles=(None, "Delta"))
|
|
471
|
+
fig.add_traces(df.diff().fillna(df).abs().T.plot.bar()["data"], rows=1, cols=1)
|
|
472
|
+
|
|
473
|
+
fig.update_layout(
|
|
474
|
+
barmode="group", legend=dict(orientation="h", yanchor="bottom", y=1.1, xanchor="left"), font_size=10
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
for i in range(len(fig["data"])):
|
|
478
|
+
fig["data"][i]["marker"]["color"] = colors[i]
|
|
479
|
+
|
|
480
|
+
df = df["waterfall"] - df["slicing"]
|
|
481
|
+
fig2 = df.plot.bar()["data"][0]
|
|
482
|
+
fig2["marker_color"] = colors
|
|
483
|
+
fig2["showlegend"] = False
|
|
484
|
+
fig.add_trace(fig2, row=1, col=2)
|
|
485
|
+
fig.update_yaxes(ticksuffix="%", title="Percent (%)", range=[0, 105])
|
|
486
|
+
|
|
487
|
+
return fig
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def liability_liquidity_profile_expectations_graph(df_redemption):
|
|
491
|
+
new_index = ["1 day or less", "2-7 days", "8-15 days", "16-30 days", "31-60 days", "61-180 days"]
|
|
492
|
+
|
|
493
|
+
def get_expected_values(net=True):
|
|
494
|
+
col_filter = "net_perc_net_red" if net else "net_perc_gross_red"
|
|
495
|
+
expected_redemptions = pd.Series(
|
|
496
|
+
df_redemption.filter(like=col_filter).mean().sort_values().round(4).values, index=new_index
|
|
497
|
+
)
|
|
498
|
+
return expected_redemptions * 100
|
|
499
|
+
|
|
500
|
+
expected_net_redemption = get_expected_values(net=True)
|
|
501
|
+
expected_gross_redemption = get_expected_values(net=False)
|
|
502
|
+
|
|
503
|
+
fig = make_subplots(rows=1, cols=2, subplot_titles=("Expected Net Redemptions", "Expected Gross Redemptions"))
|
|
504
|
+
fig.add_trace(expected_net_redemption.plot.bar()["data"][0], row=1, col=1)
|
|
505
|
+
fig.add_trace(expected_gross_redemption.plot.bar()["data"][0], row=1, col=2)
|
|
506
|
+
fig.update_yaxes(ticksuffix="%", range=[0, 108])
|
|
507
|
+
fig.update_traces(textposition="outside", texttemplate="%{value}")
|
|
508
|
+
fig.update_layout(
|
|
509
|
+
font_size=12,
|
|
510
|
+
uniformtext_mode="hide",
|
|
511
|
+
title_font_size=20,
|
|
512
|
+
yaxis_title="Percent (%)",
|
|
513
|
+
showlegend=False,
|
|
514
|
+
legend_title="",
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return fig
|
|
518
|
+
|
|
519
|
+
@staticmethod
|
|
520
|
+
def liability_liquidity_profile_metrics_graph(df_redemption):
|
|
521
|
+
fig = make_subplots(
|
|
522
|
+
rows=1,
|
|
523
|
+
cols=2,
|
|
524
|
+
specs=[[{"type": "table"}, {"type": "table"}]],
|
|
525
|
+
subplot_titles=("Net Redemptions", "Gross Redemptions"),
|
|
526
|
+
)
|
|
527
|
+
df = (df_redemption.filter(like="net_perc").max() * 100).round(1)
|
|
528
|
+
gross = df.filter(like="net_perc_gross").sort_values()
|
|
529
|
+
net = df.filter(like="net_perc_net").sort_values()
|
|
530
|
+
gross_cells = [
|
|
531
|
+
["Max Daily", "Max Weekly", "Max 2 Weeks", "Max 1 Month", "Max 2 Months", "Max 6 Months"],
|
|
532
|
+
gross.values,
|
|
533
|
+
]
|
|
534
|
+
net_cells = [
|
|
535
|
+
["Max Daily", "Max Weekly", "Max 2 Weeks", "Max 1 Month", "Max 2 Months", "Max 6 Months"],
|
|
536
|
+
net.values,
|
|
537
|
+
]
|
|
538
|
+
fig.add_table(
|
|
539
|
+
columnorder=[1, 2],
|
|
540
|
+
columnwidth=[100, 400],
|
|
541
|
+
header=dict(
|
|
542
|
+
values=["Liquidity Metrics", "Aggregate"],
|
|
543
|
+
line_color="darkslategray",
|
|
544
|
+
fill_color="royalblue",
|
|
545
|
+
align="center",
|
|
546
|
+
font=dict(color="white", size=12),
|
|
547
|
+
height=30,
|
|
548
|
+
),
|
|
549
|
+
cells=dict(
|
|
550
|
+
values=net_cells,
|
|
551
|
+
line_color="darkslategray",
|
|
552
|
+
align="center",
|
|
553
|
+
font=dict(color="black", size=11),
|
|
554
|
+
suffix=[None] + ["%"],
|
|
555
|
+
fill=dict(color=["paleturquoise", "white"]),
|
|
556
|
+
height=30,
|
|
557
|
+
),
|
|
558
|
+
row=1,
|
|
559
|
+
col=1,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
fig.add_table(
|
|
563
|
+
columnorder=[1, 2],
|
|
564
|
+
columnwidth=[100, 400],
|
|
565
|
+
header=dict(
|
|
566
|
+
values=["Liquidity Metrics", "Aggregate"],
|
|
567
|
+
line_color="darkslategray",
|
|
568
|
+
fill_color="royalblue",
|
|
569
|
+
align="center",
|
|
570
|
+
font=dict(color="white", size=12),
|
|
571
|
+
height=30,
|
|
572
|
+
),
|
|
573
|
+
cells=dict(
|
|
574
|
+
values=gross_cells,
|
|
575
|
+
line_color="darkslategray",
|
|
576
|
+
align="center",
|
|
577
|
+
font=dict(color="black", size=11),
|
|
578
|
+
suffix=[None] + ["%"],
|
|
579
|
+
fill=dict(color=["paleturquoise", "white"]),
|
|
580
|
+
height=30,
|
|
581
|
+
),
|
|
582
|
+
row=1,
|
|
583
|
+
col=2,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return fig
|
|
587
|
+
|
|
588
|
+
@staticmethod
|
|
589
|
+
def liquidity_monitor_stress_testing_tables(df_aggregate, df_redemption, method):
|
|
590
|
+
tmp = df_aggregate.loc[("Portfolio", slice(None)), (slice(None), method)].droplevel(0, axis=0)
|
|
591
|
+
tmp = tmp.mask(tmp < 0, df_aggregate.loc[("Equity", slice(None)), (slice(None), method)].droplevel(0, axis=0))
|
|
592
|
+
df_aggregate = tmp.droplevel(1, axis=1)
|
|
593
|
+
df_redemption = df_redemption.filter(like="net_perc").mean()
|
|
594
|
+
gross = (
|
|
595
|
+
df_redemption.filter(like="net_perc_gross")
|
|
596
|
+
.sort_values()
|
|
597
|
+
.rename(
|
|
598
|
+
index={
|
|
599
|
+
"net_perc_gross_red": "1 day or less",
|
|
600
|
+
"net_perc_gross_red 5D": "2-7 days",
|
|
601
|
+
"net_perc_gross_red 12D": "8-15 days",
|
|
602
|
+
"net_perc_gross_red 23D": "16-30 days",
|
|
603
|
+
"net_perc_gross_red 45D": "31-60 days",
|
|
604
|
+
"net_perc_gross_red 120D": "61-180 days",
|
|
605
|
+
}
|
|
606
|
+
)
|
|
607
|
+
)
|
|
608
|
+
net = (
|
|
609
|
+
df_redemption.filter(like="net_perc_net")
|
|
610
|
+
.sort_values()
|
|
611
|
+
.rename(
|
|
612
|
+
index={
|
|
613
|
+
"net_perc_net_red": "1 day or less",
|
|
614
|
+
"net_perc_net_red 5D": "2-7 days",
|
|
615
|
+
"net_perc_net_red 12D": "8-15 days",
|
|
616
|
+
"net_perc_net_red 23D": "16-30 days",
|
|
617
|
+
"net_perc_net_red 45D": "31-60 days",
|
|
618
|
+
"net_perc_net_red 120D": "61-180 days",
|
|
619
|
+
}
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
lcr_vs_net = df_aggregate.div(net, axis=0) * 100
|
|
623
|
+
lcr_vs_net = lcr_vs_net.reindex(df_aggregate.index, axis=0) # preserve index order
|
|
624
|
+
lcr_vs_net_colors = lcr_vs_net.applymap(
|
|
625
|
+
lambda x: "#0FFBA6" if x > 200 else "#FC6955" if x < 120 else "#FBE426"
|
|
626
|
+
)
|
|
627
|
+
lcr_vs_net = lcr_vs_net.applymap(lambda x: f"{x: .0f}%" if x <= 200 else ">200%")
|
|
628
|
+
lcr_vs_gross = df_aggregate.div(gross, axis=0) * 100
|
|
629
|
+
lcr_vs_gross = lcr_vs_gross.reindex(df_aggregate.index, axis=0) # preserve index order
|
|
630
|
+
lcr_vs_gross_colors = lcr_vs_gross.applymap(
|
|
631
|
+
lambda x: "#0FFBA6" if x > 200 else "#FC6955" if x < 120 else "#FBE426"
|
|
632
|
+
)
|
|
633
|
+
lcr_vs_gross = lcr_vs_gross.applymap(lambda x: f"{x: .0f}%" if x <= 200 else ">200%")
|
|
634
|
+
|
|
635
|
+
fig = make_subplots(
|
|
636
|
+
rows=1,
|
|
637
|
+
cols=2,
|
|
638
|
+
specs=[[{"type": "table"}, {"type": "table"}]],
|
|
639
|
+
subplot_titles=(
|
|
640
|
+
"Liquidity Coverage Ratio (vs net redemptions)",
|
|
641
|
+
"Liquidity Coverage Ratio (vs gross redemptions)",
|
|
642
|
+
),
|
|
643
|
+
)
|
|
644
|
+
headers = df_aggregate.index.to_list()
|
|
645
|
+
headers.insert(0, "Scenarios")
|
|
646
|
+
net_cells = lcr_vs_net.values.tolist()
|
|
647
|
+
net_cells.insert(0, lcr_vs_net.columns.to_list())
|
|
648
|
+
gross_cells = lcr_vs_gross.values.tolist()
|
|
649
|
+
gross_cells.insert(0, lcr_vs_gross.columns.to_list())
|
|
650
|
+
|
|
651
|
+
fig.add_table(
|
|
652
|
+
columnwidth=[50, 40],
|
|
653
|
+
header=dict(
|
|
654
|
+
values=headers,
|
|
655
|
+
line_color="darkslategray",
|
|
656
|
+
fill_color="royalblue",
|
|
657
|
+
align="center",
|
|
658
|
+
font=dict(color="white", size=12),
|
|
659
|
+
height=40,
|
|
660
|
+
),
|
|
661
|
+
cells=dict(
|
|
662
|
+
values=net_cells,
|
|
663
|
+
line_color="darkslategray",
|
|
664
|
+
align="center",
|
|
665
|
+
font=dict(color="black", size=11),
|
|
666
|
+
fill=dict(color=["paleturquoise"] + lcr_vs_net_colors.values.tolist()),
|
|
667
|
+
height=30,
|
|
668
|
+
),
|
|
669
|
+
row=1,
|
|
670
|
+
col=1,
|
|
671
|
+
)
|
|
672
|
+
fig.add_table(
|
|
673
|
+
columnwidth=[50, 40],
|
|
674
|
+
header=dict(
|
|
675
|
+
values=headers,
|
|
676
|
+
line_color="darkslategray",
|
|
677
|
+
fill_color="royalblue",
|
|
678
|
+
align="center",
|
|
679
|
+
font=dict(color="white", size=12),
|
|
680
|
+
height=40,
|
|
681
|
+
),
|
|
682
|
+
cells=dict(
|
|
683
|
+
values=gross_cells,
|
|
684
|
+
line_color="darkslategray",
|
|
685
|
+
align="center",
|
|
686
|
+
font=dict(color="black", size=11),
|
|
687
|
+
fill=dict(color=["paleturquoise"] + lcr_vs_gross_colors.values.tolist()),
|
|
688
|
+
height=30,
|
|
689
|
+
),
|
|
690
|
+
row=1,
|
|
691
|
+
col=2,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
return fig
|
|
695
|
+
|
|
696
|
+
@staticmethod
|
|
697
|
+
def asset_liquidity_profile_stress_testing_bar_char(df_aggregate, method):
|
|
698
|
+
pd.options.plotting.backend = "plotly"
|
|
699
|
+
df = (
|
|
700
|
+
(df_aggregate.loc[("Portfolio", slice(None)), (slice(None), method)] * 100)
|
|
701
|
+
.astype(float)
|
|
702
|
+
.round(2)
|
|
703
|
+
.droplevel(0)
|
|
704
|
+
.droplevel(1, axis=1)
|
|
705
|
+
)
|
|
706
|
+
df = df.drop("Baseline Scenario", axis=1)
|
|
707
|
+
|
|
708
|
+
colors = ["green", "lightgreen", "lightblue", "lightsalmon", "orange", "red"]
|
|
709
|
+
|
|
710
|
+
fig = df.diff().fillna(df).T.plot.bar()
|
|
711
|
+
fig.update_yaxes(ticksuffix="%", title="Percent (%)", range=[0, 105])
|
|
712
|
+
fig.update_traces(texttemplate="%{value}", textposition="outside")
|
|
713
|
+
fig.update_layout(
|
|
714
|
+
barmode="group",
|
|
715
|
+
legend=dict(orientation="h", yanchor="bottom", y=1.01, xanchor="left", font=dict(size=15), title=""),
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
for i in range(len(fig["data"])):
|
|
719
|
+
fig["data"][i]["marker"]["color"] = colors[i]
|
|
720
|
+
|
|
721
|
+
return fig
|
|
722
|
+
|
|
723
|
+
@staticmethod
|
|
724
|
+
def asset_liquidity_profile_stress_testing_table(df_aggregate, scenario1, scenario2, method):
|
|
725
|
+
fig = make_subplots(
|
|
726
|
+
rows=1,
|
|
727
|
+
cols=2,
|
|
728
|
+
specs=[[{"type": "table"}, {"type": "table"}]],
|
|
729
|
+
subplot_titles=(f"{scenario1}", f"{scenario2}"),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
def add_table_to_fig(scenario, col):
|
|
733
|
+
df = (df_aggregate.loc[:, (scenario, method)] * 100).astype(float).round(2)
|
|
734
|
+
df_table = df.unstack().reindex(df.index.get_level_values(0).drop_duplicates())
|
|
735
|
+
df_table = df_table.reindex(df.index.get_level_values(1).drop_duplicates(), axis=1)
|
|
736
|
+
headers = df_table.columns.insert(0, "Asset Type")
|
|
737
|
+
cells_values = df_table.T.values.tolist()
|
|
738
|
+
cells_values.insert(0, df_table.index.to_list()) # Index in values for the table.
|
|
739
|
+
fig.add_table(
|
|
740
|
+
header=dict(
|
|
741
|
+
values=headers,
|
|
742
|
+
line_color="darkslategray",
|
|
743
|
+
fill_color="royalblue",
|
|
744
|
+
font=dict(color="white", size=12),
|
|
745
|
+
align="center",
|
|
746
|
+
height=30,
|
|
747
|
+
),
|
|
748
|
+
cells=dict(
|
|
749
|
+
values=cells_values, # 2nd column
|
|
750
|
+
line_color="darkslategray",
|
|
751
|
+
fill_color=["paleturquoise", "white"],
|
|
752
|
+
font=dict(color="black", size=11),
|
|
753
|
+
suffix=[None] + ["%"],
|
|
754
|
+
align="center",
|
|
755
|
+
height=30,
|
|
756
|
+
),
|
|
757
|
+
row=1,
|
|
758
|
+
col=col,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
if scenario1:
|
|
762
|
+
add_table_to_fig(scenario1, 1)
|
|
763
|
+
if scenario2:
|
|
764
|
+
add_table_to_fig(scenario2, 2)
|
|
765
|
+
fig.update_layout(height=350)
|
|
766
|
+
return fig
|
|
767
|
+
|
|
768
|
+
@staticmethod
|
|
769
|
+
def asset_liquidity_profile_color_table():
|
|
770
|
+
df = pd.DataFrame(
|
|
771
|
+
index=["1 day is needed"] + list(map(lambda x: x + " days are needed", ["2-7", "8-15", "16-30", ">30"])),
|
|
772
|
+
columns=list(map(lambda x: "To liquidate " + str(x) + "% of AUM", [100, 90, 80, 70])),
|
|
773
|
+
)
|
|
774
|
+
df_colors = df.copy()
|
|
775
|
+
df.fillna("", inplace=True)
|
|
776
|
+
df_colors.iloc[0, :] = "#0FFBA6"
|
|
777
|
+
df_colors.loc["2-7 days are needed"] = ["#0FFBA6"] * 3 + ["#FBE426"]
|
|
778
|
+
df_colors.loc["8-15 days are needed"] = ["#0FFBA6"] + ["#FBE426"] * 2 + ["#FC6955"]
|
|
779
|
+
df_colors.loc["16-30 days are needed"] = ["#FBE426"] * 3 + ["#FC6955"]
|
|
780
|
+
df_colors.loc[">30 days are needed"] = ["#FBE426"] + ["#FC6955"] * 3
|
|
781
|
+
|
|
782
|
+
fig = go.Figure(
|
|
783
|
+
data=go.Table(
|
|
784
|
+
columnwidth=[3, 2],
|
|
785
|
+
header=dict(
|
|
786
|
+
values=["Colors"] + df.columns.tolist(),
|
|
787
|
+
line_color="darkslategray",
|
|
788
|
+
fill_color="white",
|
|
789
|
+
align="center",
|
|
790
|
+
font=dict(color="black", size=12),
|
|
791
|
+
height=30,
|
|
792
|
+
),
|
|
793
|
+
cells=dict(
|
|
794
|
+
values=[df.index.tolist()] + df.T.values.tolist(),
|
|
795
|
+
line_color="black",
|
|
796
|
+
fill_color=["white"] + df_colors.T.values.tolist(),
|
|
797
|
+
align="center",
|
|
798
|
+
font=dict(color="black", size=11),
|
|
799
|
+
height=30,
|
|
800
|
+
),
|
|
801
|
+
)
|
|
802
|
+
)
|
|
803
|
+
return fig
|
|
804
|
+
|
|
805
|
+
@staticmethod
|
|
806
|
+
def summary_ratings(series):
|
|
807
|
+
pass
|
|
808
|
+
|
|
809
|
+
@staticmethod
|
|
810
|
+
def liability_liquidity_profile_color_table():
|
|
811
|
+
s = pd.Series(
|
|
812
|
+
index=list(map(lambda x: "Redemption Coverage Ratio " + x, [">200%", "120%-200%", "<120%"])), dtype=float
|
|
813
|
+
)
|
|
814
|
+
s_colors = s.copy()
|
|
815
|
+
s.fillna("", inplace=True)
|
|
816
|
+
s_colors.iat[0] = "#0FFBA6"
|
|
817
|
+
s_colors.iat[1] = "#FBE426"
|
|
818
|
+
s_colors.iat[2] = "#FC6955"
|
|
819
|
+
|
|
820
|
+
fig = go.Figure(
|
|
821
|
+
data=go.Table(
|
|
822
|
+
header=dict(height=0),
|
|
823
|
+
cells=dict(
|
|
824
|
+
values=[s.index.tolist()] + [s.values.tolist()],
|
|
825
|
+
line_color="black",
|
|
826
|
+
fill_color=["white"] + [s_colors.values.tolist()],
|
|
827
|
+
align="center",
|
|
828
|
+
font=dict(color="black", size=11),
|
|
829
|
+
height=30,
|
|
830
|
+
),
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
return fig
|
|
834
|
+
|
|
835
|
+
""" The main function for the liquidity stress tests """
|
|
836
|
+
|
|
837
|
+
def liquidity_stress_test(
|
|
838
|
+
self,
|
|
839
|
+
report_date: Optional[date] = None,
|
|
840
|
+
weights_date: Optional[date] = None,
|
|
841
|
+
liq_factor: float = 1 / 3,
|
|
842
|
+
below_x_days: int = 5,
|
|
843
|
+
) -> dict:
|
|
844
|
+
if not (product_ids := self.get_product_ids_from_group_product_or_product()):
|
|
845
|
+
# In the case the model is not a Product or ProductGroup, we return an empty DataFrame
|
|
846
|
+
return dict()
|
|
847
|
+
|
|
848
|
+
# We test if a date is None, if yes, we stop the code to avoid errors.
|
|
849
|
+
# Weights date cannot being after report date.
|
|
850
|
+
# if report_date is None or weights_date > report_date:
|
|
851
|
+
if weights_date > report_date:
|
|
852
|
+
return dict()
|
|
853
|
+
|
|
854
|
+
assets = self.portfolio.assets.filter(date=weights_date)
|
|
855
|
+
qs_assets = assets.order_by("underlying_instrument")
|
|
856
|
+
if not qs_assets.exists():
|
|
857
|
+
return dict()
|
|
858
|
+
|
|
859
|
+
assets_fields = [
|
|
860
|
+
"date",
|
|
861
|
+
"underlying_instrument__id",
|
|
862
|
+
"underlying_instrument__instrument_type",
|
|
863
|
+
"total_value_fx_usd",
|
|
864
|
+
"weighting",
|
|
865
|
+
"shares",
|
|
866
|
+
]
|
|
867
|
+
df_assets = (
|
|
868
|
+
pd.DataFrame(list(qs_assets.values_list(*assets_fields)), columns=assets_fields)
|
|
869
|
+
.rename(
|
|
870
|
+
{
|
|
871
|
+
"underlying_instrument__instrument_type": "type",
|
|
872
|
+
"underlying_instrument__id": "instrument",
|
|
873
|
+
"total_value_fx_usd": "total_value_usd",
|
|
874
|
+
},
|
|
875
|
+
axis=1,
|
|
876
|
+
)
|
|
877
|
+
.set_index("instrument")
|
|
878
|
+
.astype(dtype={"shares": "float", "total_value_usd": "float", "weighting": "float"})
|
|
879
|
+
)
|
|
880
|
+
instrument_ids = df_assets.index.unique("instrument")
|
|
881
|
+
start_date = date(2000, 1, 1)
|
|
882
|
+
qs_prices = (
|
|
883
|
+
InstrumentPrice.objects.filter(
|
|
884
|
+
calculated=False,
|
|
885
|
+
date__gte=start_date,
|
|
886
|
+
date__lte=weights_date,
|
|
887
|
+
instrument__in=instrument_ids,
|
|
888
|
+
)
|
|
889
|
+
.annotate_base_data()
|
|
890
|
+
.order_by("date", "instrument")
|
|
891
|
+
.select_related("currency_fx_rate_to_usd")
|
|
892
|
+
)
|
|
893
|
+
if not qs_prices.exists():
|
|
894
|
+
return {}
|
|
895
|
+
|
|
896
|
+
price_fields = ["date", "instrument", "net_value_usd", "volume_usd", "volume"]
|
|
897
|
+
df_prices = pd.DataFrame(list(qs_prices.values_list(*price_fields)), columns=price_fields)
|
|
898
|
+
df_prices.set_index(["date", "instrument"], inplace=True)
|
|
899
|
+
df_prices = df_prices.astype(float)
|
|
900
|
+
|
|
901
|
+
qs_fdm = InstrumentFDM.objects.filter(pms_instrument__in=instrument_ids)
|
|
902
|
+
if not qs_fdm.exists():
|
|
903
|
+
return {}
|
|
904
|
+
|
|
905
|
+
df_instrument_eq = pd.DataFrame(
|
|
906
|
+
list(qs_fdm.values_list("id", "pms_instrument")), columns=["id", "pms_instrument"]
|
|
907
|
+
)
|
|
908
|
+
df_instrument_eq.rename(columns={"id": "instrument_id"}, inplace=True)
|
|
909
|
+
df_ask_bid = pd.DataFrame(qs_fdm.dl.market_data(values=[MarketData.ASK, MarketData.BID], from_date=start_date))
|
|
910
|
+
if df_ask_bid.empty:
|
|
911
|
+
return {}
|
|
912
|
+
df_ask_bid = df_ask_bid.join(df_instrument_eq.set_index("instrument_id"), on="instrument_id")
|
|
913
|
+
df_ask_bid = df_ask_bid[["ask", "bid", "pms_instrument", "valuation_date", "currency"]]
|
|
914
|
+
df_ask_bid.rename(columns={"valuation_date": "date", "pms_instrument": "instrument"}, inplace=True)
|
|
915
|
+
qs_fx_rate = CurrencyFXRates.objects.filter(
|
|
916
|
+
currency__key__in=df_ask_bid.currency.unique(),
|
|
917
|
+
date__gte=date(2000, 1, 1),
|
|
918
|
+
date__lte=weights_date,
|
|
919
|
+
)
|
|
920
|
+
if not qs_fx_rate.exists():
|
|
921
|
+
return {}
|
|
922
|
+
currency_fields = ["date", "currency__key", "value"]
|
|
923
|
+
df_fx_rates = pd.DataFrame(list(qs_fx_rate.values_list(*currency_fields)), columns=currency_fields)
|
|
924
|
+
df_fx_rates.rename(columns={"currency__key": "currency", "value": "fx_rate"}, inplace=True)
|
|
925
|
+
df_fx_rates.set_index(["date", "currency"], inplace=True)
|
|
926
|
+
df_ask_bid = df_ask_bid.join(df_fx_rates, on=["date", "currency"]).dropna()
|
|
927
|
+
if df_ask_bid.empty:
|
|
928
|
+
return {}
|
|
929
|
+
|
|
930
|
+
df_ask_bid = df_ask_bid.set_index(["date", "instrument"]).drop("currency", axis=1).astype(float)
|
|
931
|
+
df_ask_bid.loc[:, ["bid", "ask"]] = df_ask_bid.loc[:, ["bid", "ask"]].div(df_ask_bid.fx_rate, axis=0)
|
|
932
|
+
df_ask_bid.drop(columns="fx_rate", inplace=True)
|
|
933
|
+
|
|
934
|
+
if not qs_prices.exists() or df_ask_bid.empty:
|
|
935
|
+
return dict()
|
|
936
|
+
|
|
937
|
+
df_prices = pd.concat([df_prices, df_ask_bid], axis=1).sort_index()
|
|
938
|
+
df_prices.rename(columns={"volume_usd": "dollar_volume"}, inplace=True)
|
|
939
|
+
|
|
940
|
+
df_assets.type = df_assets.type.replace("Index", "Cash")
|
|
941
|
+
df_assets = df_assets.set_index("type", append=True).swaplevel().sort_index()
|
|
942
|
+
|
|
943
|
+
# cleaning volumes
|
|
944
|
+
df_prices.dollar_volume = df_prices.dollar_volume.where(df_prices.dollar_volume > 10000)
|
|
945
|
+
df_prices.volume = df_prices.where(df_prices.dollar_volume.notnull()).volume
|
|
946
|
+
|
|
947
|
+
df_prices["bid_ask_spread"] = (df_prices.ask - df_prices.bid) / df_prices.ask
|
|
948
|
+
|
|
949
|
+
qs_product_price = InstrumentPrice.objects.filter(
|
|
950
|
+
calculated=True, date__lte=report_date, instrument__in=product_ids
|
|
951
|
+
).order_by("date", "instrument")
|
|
952
|
+
if not qs_product_price.exists():
|
|
953
|
+
return dict()
|
|
954
|
+
|
|
955
|
+
product_fields = ["date", "instrument", "net_value_usd", "outstanding_shares"]
|
|
956
|
+
df_products_price = (
|
|
957
|
+
pd.DataFrame(list(qs_product_price.values(*product_fields)), columns=product_fields)
|
|
958
|
+
.set_index(["date", "instrument"])
|
|
959
|
+
.astype(float)
|
|
960
|
+
.groupby(level=["date", "instrument"])
|
|
961
|
+
.ffill()
|
|
962
|
+
)
|
|
963
|
+
df_products_price["aum"] = df_products_price.outstanding_shares * df_products_price.net_value_usd
|
|
964
|
+
df_aum = df_products_price.groupby(level="date").aum.sum().replace(0, method="ffill").to_frame()
|
|
965
|
+
from wbportfolio.models.transactions.trades import Trade
|
|
966
|
+
|
|
967
|
+
qs_trades = Trade.objects.filter(
|
|
968
|
+
underlying_instrument__in=product_ids,
|
|
969
|
+
transaction_subtype__in=["SUBSCRIPTION", "REDEMPTION"],
|
|
970
|
+
transaction_date__lte=report_date,
|
|
971
|
+
).order_by("transaction_date")
|
|
972
|
+
if not qs_trades.exists():
|
|
973
|
+
return {}
|
|
974
|
+
|
|
975
|
+
trades_fields = [
|
|
976
|
+
"transaction_date",
|
|
977
|
+
"transaction_subtype",
|
|
978
|
+
"underlying_instrument",
|
|
979
|
+
"underlying_instrument__currency",
|
|
980
|
+
"total_value",
|
|
981
|
+
]
|
|
982
|
+
df_trades = (
|
|
983
|
+
pd.DataFrame(list(qs_trades.values_list(*trades_fields)), columns=trades_fields)
|
|
984
|
+
.rename({"underlying_instrument__currency": "currency", "transaction_date": "date"}, axis=1)
|
|
985
|
+
.set_index(["date", "currency", "underlying_instrument"])
|
|
986
|
+
.astype(dtype={"total_value": "float"})
|
|
987
|
+
)
|
|
988
|
+
qs_fx_rate = CurrencyFXRates.objects.filter(
|
|
989
|
+
currency__in=df_trades.index.unique("currency"),
|
|
990
|
+
date__in=df_trades.index.unique("date"),
|
|
991
|
+
).order_by("date", "currency")
|
|
992
|
+
fx_rates_fields = ["date", "currency", "value"]
|
|
993
|
+
df_fx_rates = pd.DataFrame(list(qs_fx_rate.values_list(*fx_rates_fields)), columns=fx_rates_fields)
|
|
994
|
+
df_fx_rates.rename(columns={"value": "fx_rate"}, inplace=True)
|
|
995
|
+
df_trades = df_trades.join(df_fx_rates.set_index(["date", "currency"]).astype(float))
|
|
996
|
+
df_trades["total_value_usd"] = df_trades.total_value / df_trades.fx_rate
|
|
997
|
+
df_trades = df_trades.droplevel(level="currency")
|
|
998
|
+
accumulated_days_list = [5, 12, 23, 45, 120]
|
|
999
|
+
|
|
1000
|
+
# df_trades.transaction_date = pd.to_datetime(df_trades["transaction_date"]) # to use df.rolling
|
|
1001
|
+
# Gross Redemption
|
|
1002
|
+
df_gross_redemption = df_trades.where(df_trades.transaction_subtype == "REDEMPTION")
|
|
1003
|
+
df_gross_redemption = df_gross_redemption.groupby("date").total_value_usd.sum()
|
|
1004
|
+
df_gross_redemption.name = "gross_redemption"
|
|
1005
|
+
df_gross_redemption = df_aum.join(df_gross_redemption)
|
|
1006
|
+
df_gross_redemption.gross_redemption = df_gross_redemption.gross_redemption.fillna(0)
|
|
1007
|
+
df_gross_redemption["net_perc_gross_red"] = abs(df_gross_redemption.gross_redemption) / df_gross_redemption.aum
|
|
1008
|
+
for acc_days in accumulated_days_list:
|
|
1009
|
+
df_gross_redemption[f"net_perc_gross_red {acc_days}D"] = df_gross_redemption.net_perc_gross_red.rolling(
|
|
1010
|
+
acc_days
|
|
1011
|
+
).sum()
|
|
1012
|
+
|
|
1013
|
+
# Net Redemption
|
|
1014
|
+
df_trades_by_day = df_trades.groupby("date").total_value_usd.sum()
|
|
1015
|
+
df_net_redemption = df_trades_by_day.where(df_trades_by_day < 0)
|
|
1016
|
+
df_net_redemption.name = "net_redemption"
|
|
1017
|
+
df_net_redemption = df_aum.join(df_net_redemption)
|
|
1018
|
+
df_net_redemption.net_redemption = df_net_redemption.net_redemption.fillna(0)
|
|
1019
|
+
df_net_redemption.loc[:, "net_perc_net_red"] = abs(df_net_redemption.net_redemption) / df_net_redemption.aum
|
|
1020
|
+
|
|
1021
|
+
# Accumulated net percent redemption for both - gross and net.
|
|
1022
|
+
for acc_days in accumulated_days_list:
|
|
1023
|
+
df_net_redemption[f"net_perc_net_red {acc_days}D"] = df_net_redemption.net_perc_net_red.rolling(
|
|
1024
|
+
acc_days
|
|
1025
|
+
).sum()
|
|
1026
|
+
cols_to_use = df_net_redemption.columns.difference(df_gross_redemption.columns)[::-1]
|
|
1027
|
+
df_redemptions = pd.concat([df_gross_redemption, df_net_redemption[cols_to_use]], axis=1)
|
|
1028
|
+
|
|
1029
|
+
expected_net_redemption = df_redemptions.filter(like="net_perc_net_red").mean().sort_values().round(4)
|
|
1030
|
+
gross_total_portfolio_value_usd = df_assets.total_value_usd.sum()
|
|
1031
|
+
net_total_portfolio_value_usd = df_redemptions.aum.values[-1] # which is for the report date
|
|
1032
|
+
df_assets.loc[:, "weighting"] = df_assets.loc[:, "total_value_usd"] / df_assets.loc[:, "total_value_usd"].sum()
|
|
1033
|
+
df_volumes = df_prices.volume.unstack("date")
|
|
1034
|
+
df_dollar_volume = df_prices.dollar_volume.unstack("date")
|
|
1035
|
+
df_bid_ask_spread = df_prices.bid_ask_spread.unstack("date")
|
|
1036
|
+
days_to_liquidate = self.get_volumes_from_scenario_stress_test(weights_date, df_volumes, df_assets, liq_factor)
|
|
1037
|
+
average_worst_dollar_volume = self.get_percentile_worst_dollar_volume(df_dollar_volume, method="other")
|
|
1038
|
+
rst_analysis = self.reverse_stress_test(
|
|
1039
|
+
gross_total_portfolio_value_usd, df_assets, average_worst_dollar_volume, liq_factor, below_x_days
|
|
1040
|
+
)
|
|
1041
|
+
stress_tests_analysis = self.stress_volume_bid_ask_test(
|
|
1042
|
+
df_assets, df_dollar_volume, df_volumes, df_bid_ask_spread, liq_factor
|
|
1043
|
+
)
|
|
1044
|
+
days_to_liquidate = days_to_liquidate.join(stress_tests_analysis)
|
|
1045
|
+
days_to_liquidate.loc[("Cash", slice(None))] = 0 # cash is instantaneous
|
|
1046
|
+
|
|
1047
|
+
tmp = df_assets.loc[:, "weighting"].to_frame(name=("instrument", "weighting")).copy()
|
|
1048
|
+
days_to_liquidate = days_to_liquidate.join(tmp)
|
|
1049
|
+
df_aggregate = self.aggregate_days_to_liquidate(days_to_liquidate)
|
|
1050
|
+
|
|
1051
|
+
portfolio_waterfall_baseline = df_aggregate.loc[
|
|
1052
|
+
("Portfolio", slice(None)), ("Baseline Scenario", "waterfall")
|
|
1053
|
+
].droplevel(0, axis=0)
|
|
1054
|
+
portfolio_slicing_baseline = df_aggregate.loc[
|
|
1055
|
+
("Portfolio", slice(None)), ("Baseline Scenario", "slicing")
|
|
1056
|
+
].droplevel(0, axis=0)
|
|
1057
|
+
portfolio_risk_waterfall = self.series_of_colors(portfolio_waterfall_baseline)
|
|
1058
|
+
portfolio_risk_slicing = self.series_of_colors(portfolio_slicing_baseline)
|
|
1059
|
+
|
|
1060
|
+
asset_liquidity_message = ""
|
|
1061
|
+
asset_liquidity_color = "#0FFBA6"
|
|
1062
|
+
if not portfolio_risk_waterfall.message.dropna().empty:
|
|
1063
|
+
asset_liquidity_message = portfolio_risk_waterfall.message.dropna().iat[0]
|
|
1064
|
+
asset_liquidity_color = portfolio_risk_waterfall.dropna().colors.iat[0]
|
|
1065
|
+
elif not portfolio_risk_slicing.message.dropna().empty:
|
|
1066
|
+
asset_liquidity_message = portfolio_risk_slicing.message.dropna().iat[0]
|
|
1067
|
+
asset_liquidity_color = portfolio_risk_slicing.dropna().colors.iat[0]
|
|
1068
|
+
|
|
1069
|
+
liability_slicing = pd.DataFrame(portfolio_slicing_baseline.values / expected_net_redemption.values)
|
|
1070
|
+
color_slicing = "#0FFBA6"
|
|
1071
|
+
if not liability_slicing.where((liability_slicing < 2) & (liability_slicing >= 1.2)).dropna().empty:
|
|
1072
|
+
color_slicing = "#FBE426"
|
|
1073
|
+
elif not liability_slicing.where(liability_slicing < 1.2).dropna().empty:
|
|
1074
|
+
color_slicing = "#FC6955"
|
|
1075
|
+
# ------------- PLOTLY GRAPHS ------------------ #
|
|
1076
|
+
# BASELINE SCENARIO WATERFALL FIGURE
|
|
1077
|
+
fig1 = self.liquidity_monitor_graph(portfolio_waterfall_baseline, expected_net_redemption)
|
|
1078
|
+
|
|
1079
|
+
fig2 = self.liquidity_monitor_graph(portfolio_slicing_baseline, expected_net_redemption)
|
|
1080
|
+
|
|
1081
|
+
fig4 = self.asset_liquidity_graph(df_aggregate, "Baseline Scenario", "waterfall")
|
|
1082
|
+
|
|
1083
|
+
fig5 = self.asset_liquidity_graph(df_aggregate, "Baseline Scenario", "slicing")
|
|
1084
|
+
|
|
1085
|
+
fig6 = self.liquidity_bucketing_graph(df_aggregate, "Baseline Scenario")
|
|
1086
|
+
|
|
1087
|
+
fig7 = self.liability_liquidity_profile_expectations_graph(df_redemptions)
|
|
1088
|
+
|
|
1089
|
+
fig8 = self.liability_liquidity_profile_metrics_graph(df_redemptions)
|
|
1090
|
+
|
|
1091
|
+
fig9 = self.liquidity_monitor_stress_testing_tables(df_aggregate, df_redemptions, "waterfall")
|
|
1092
|
+
|
|
1093
|
+
fig10 = self.liquidity_monitor_stress_testing_tables(df_aggregate, df_redemptions, "slicing")
|
|
1094
|
+
|
|
1095
|
+
fig11 = self.asset_liquidity_profile_stress_testing_bar_char(df_aggregate, "waterfall")
|
|
1096
|
+
|
|
1097
|
+
fig12 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "COVID-19", "Lehman", "waterfall")
|
|
1098
|
+
fig13 = self.asset_liquidity_profile_stress_testing_table(
|
|
1099
|
+
df_aggregate, "Lehman Spring", "Debt Crisis", "waterfall"
|
|
1100
|
+
)
|
|
1101
|
+
fig14 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "China Crisis", "Dotcom", "waterfall")
|
|
1102
|
+
fig15 = self.asset_liquidity_profile_stress_testing_table(
|
|
1103
|
+
df_aggregate, "Volume falls by 60 pct", "S-T Worst Volume", "waterfall"
|
|
1104
|
+
)
|
|
1105
|
+
fig16 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "S-T Worst B-A", None, "waterfall")
|
|
1106
|
+
|
|
1107
|
+
fig17 = self.asset_liquidity_profile_stress_testing_bar_char(df_aggregate, "slicing")
|
|
1108
|
+
|
|
1109
|
+
fig18 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "COVID-19", "Lehman", "slicing")
|
|
1110
|
+
fig19 = self.asset_liquidity_profile_stress_testing_table(
|
|
1111
|
+
df_aggregate, "Lehman Spring", "Debt Crisis", "slicing"
|
|
1112
|
+
)
|
|
1113
|
+
fig20 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "China Crisis", "Dotcom", "slicing")
|
|
1114
|
+
fig21 = self.asset_liquidity_profile_stress_testing_table(
|
|
1115
|
+
df_aggregate, "Volume falls by 60 pct", "S-T Worst Volume", "slicing"
|
|
1116
|
+
)
|
|
1117
|
+
fig22 = self.asset_liquidity_profile_stress_testing_table(df_aggregate, "S-T Worst B-A", None, "slicing")
|
|
1118
|
+
|
|
1119
|
+
# ------------- RST FIGURE -------------------------- #
|
|
1120
|
+
|
|
1121
|
+
# Create figure
|
|
1122
|
+
fig3 = go.Figure()
|
|
1123
|
+
# Add traces, one for each slider step
|
|
1124
|
+
for col_i, col in enumerate(rst_analysis.columns.unique(0), start=1):
|
|
1125
|
+
fig3.add_trace(
|
|
1126
|
+
go.Surface(
|
|
1127
|
+
visible=False,
|
|
1128
|
+
colorscale=[
|
|
1129
|
+
(0, "rgb(166,206,227)"),
|
|
1130
|
+
(0.1, "rgb(227,26,28)"),
|
|
1131
|
+
(0.6999, "rgb(251,154,153)"),
|
|
1132
|
+
(0.70, "rgb(247,224,45)"),
|
|
1133
|
+
(0.80, "rgb(247,224,45)"),
|
|
1134
|
+
(0.80001, "rgb(178,223,138)"),
|
|
1135
|
+
(1, "rgb(51,160,44)"),
|
|
1136
|
+
],
|
|
1137
|
+
name="Aum x" + str(col_i),
|
|
1138
|
+
z=(rst_analysis.loc[:, (col, slice(None))] * 100).round(2).values,
|
|
1139
|
+
hovertemplate=(
|
|
1140
|
+
"<br><b>% Weights sold:: %{z}%</b><br>"
|
|
1141
|
+
+ "<br>% Redemption: %{x}%<br>"
|
|
1142
|
+
+ "<br>% Worst Dollar Volume: %{y}%<br>"
|
|
1143
|
+
),
|
|
1144
|
+
)
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
# Make 10th trace visible
|
|
1148
|
+
fig3.data[0].visible = True
|
|
1149
|
+
|
|
1150
|
+
# Create and add slider
|
|
1151
|
+
steps = []
|
|
1152
|
+
for i in range(len(fig3.data)):
|
|
1153
|
+
step = dict(
|
|
1154
|
+
method="update",
|
|
1155
|
+
args=[
|
|
1156
|
+
{"visible": [False] * len(fig3.data)},
|
|
1157
|
+
{
|
|
1158
|
+
"title": "Slider switched to AUM x "
|
|
1159
|
+
+ str(i + 1)
|
|
1160
|
+
+ " = $"
|
|
1161
|
+
+ str(round((i + 1) * net_total_portfolio_value_usd / 10**6))
|
|
1162
|
+
+ "M"
|
|
1163
|
+
},
|
|
1164
|
+
], # layout attribute
|
|
1165
|
+
)
|
|
1166
|
+
step["args"][0]["visible"][i] = True # Toggle i'th trace to "visible"
|
|
1167
|
+
steps.append(step)
|
|
1168
|
+
|
|
1169
|
+
sliders = [dict(active=0, currentvalue={"prefix": "AUM: "}, pad={"t": 50}, steps=steps)]
|
|
1170
|
+
|
|
1171
|
+
fig3.update_layout(
|
|
1172
|
+
sliders=sliders,
|
|
1173
|
+
scene=dict(xaxis_title="% Redemption", yaxis_title="% Worst Dollar Volume", zaxis_title="% Weights sold"),
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
# Edit slider labels
|
|
1177
|
+
fig3["layout"]["sliders"][0]["currentvalue"]["prefix"] = "AUM x "
|
|
1178
|
+
for i in range(len(rst_analysis.columns.get_level_values(0).drop_duplicates())):
|
|
1179
|
+
fig3["layout"]["sliders"][0]["steps"][i]["label"] = i + 1
|
|
1180
|
+
nb_of_holdings = len(df_assets.loc[(df_assets.index.get_level_values("type") != "Cash", slice(None))])
|
|
1181
|
+
_dict = {
|
|
1182
|
+
"report_date": report_date,
|
|
1183
|
+
"weights_date": weights_date,
|
|
1184
|
+
"name": self.portfolio.name,
|
|
1185
|
+
"currency": self.portfolio.currency.key,
|
|
1186
|
+
"nb_of_holdings": nb_of_holdings,
|
|
1187
|
+
"total": f"{round(net_total_portfolio_value_usd):,}".replace(",", "'"),
|
|
1188
|
+
"asset_liquidity_message": asset_liquidity_message,
|
|
1189
|
+
"asset_liquidity_color": asset_liquidity_color,
|
|
1190
|
+
"liability_liquidity_color": color_slicing,
|
|
1191
|
+
"fig1": fig1,
|
|
1192
|
+
"fig2": fig2,
|
|
1193
|
+
"fig3": fig3,
|
|
1194
|
+
"fig4": fig4,
|
|
1195
|
+
"fig5": fig5,
|
|
1196
|
+
"fig6": fig6,
|
|
1197
|
+
"fig7": fig7,
|
|
1198
|
+
"fig8": fig8,
|
|
1199
|
+
"fig9": fig9,
|
|
1200
|
+
"fig10": fig10,
|
|
1201
|
+
"fig11": fig11,
|
|
1202
|
+
"fig12": fig12,
|
|
1203
|
+
"fig13": fig13,
|
|
1204
|
+
"fig14": fig14,
|
|
1205
|
+
"fig15": fig15,
|
|
1206
|
+
"fig16": fig16,
|
|
1207
|
+
"fig17": fig17,
|
|
1208
|
+
"fig18": fig18,
|
|
1209
|
+
"fig19": fig19,
|
|
1210
|
+
"fig20": fig20,
|
|
1211
|
+
"fig21": fig21,
|
|
1212
|
+
"fig22": fig22,
|
|
1213
|
+
"fig23": self.asset_liquidity_profile_color_table(),
|
|
1214
|
+
"fig24": self.liability_liquidity_profile_color_table(),
|
|
1215
|
+
}
|
|
1216
|
+
return _dict
|
|
1217
|
+
|
|
1218
|
+
def pct_liquidated_below_n_days(
|
|
1219
|
+
self,
|
|
1220
|
+
evaluation_date: date,
|
|
1221
|
+
below_n_days: int = 5,
|
|
1222
|
+
liq_factor: float = 1 / 3,
|
|
1223
|
+
pct_worst_volume: int = 100,
|
|
1224
|
+
pct_redemption: int = 100,
|
|
1225
|
+
last_x_trading_dates: int = 60,
|
|
1226
|
+
is_slicing: bool = True,
|
|
1227
|
+
) -> [float, None]:
|
|
1228
|
+
# Allows to get ids from all share classes if product is a group of product,
|
|
1229
|
+
# otherwise it just returns the product id.
|
|
1230
|
+
if not (product_ids := self.get_product_ids_from_group_product_or_product()):
|
|
1231
|
+
return None
|
|
1232
|
+
|
|
1233
|
+
aum = float(self.get_redemption_analysis_df(product_ids=product_ids, report_date=evaluation_date).AUM.iat[-1])
|
|
1234
|
+
|
|
1235
|
+
qs_assets = self.portfolio.assets.filter(date=evaluation_date)
|
|
1236
|
+
|
|
1237
|
+
if not qs_assets:
|
|
1238
|
+
return None
|
|
1239
|
+
|
|
1240
|
+
qs_assets = (
|
|
1241
|
+
self.portfolio.assets.filter(date=evaluation_date)
|
|
1242
|
+
.order_by("underlying_instrument")
|
|
1243
|
+
.values("underlying_instrument", "underlying_instrument__instrument_type", "weighting")
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
df_assets = (
|
|
1247
|
+
pd.DataFrame(qs_assets)
|
|
1248
|
+
.rename(columns={"underlying_instrument__instrument_type": "instrument_type"})
|
|
1249
|
+
.set_index("underlying_instrument")
|
|
1250
|
+
.astype({"weighting": "float"})
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
first_trading_date = (evaluation_date - BDay(last_x_trading_dates)).date()
|
|
1254
|
+
|
|
1255
|
+
qs_price = (
|
|
1256
|
+
InstrumentPrice.objects.filter(
|
|
1257
|
+
instrument__in=df_assets.index, date__gte=first_trading_date, date__lte=evaluation_date
|
|
1258
|
+
)
|
|
1259
|
+
.order_by("date", "instrument")
|
|
1260
|
+
.values("date", "instrument__currency", "instrument", "net_value", "volume")
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
df_prices = (
|
|
1264
|
+
pd.DataFrame(qs_price)
|
|
1265
|
+
.rename(columns={"instrument__currency": "currency"})
|
|
1266
|
+
.set_index(["date", "currency", "instrument"])
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
qs_currencies = (
|
|
1270
|
+
CurrencyFXRates.objects.filter(
|
|
1271
|
+
currency__in=df_prices.index.get_level_values("currency"),
|
|
1272
|
+
date__gte=first_trading_date,
|
|
1273
|
+
date__lte=evaluation_date,
|
|
1274
|
+
)
|
|
1275
|
+
.order_by("date", "currency__id")
|
|
1276
|
+
.values("date", "currency", "value")
|
|
1277
|
+
)
|
|
1278
|
+
df_currencies = pd.DataFrame(qs_currencies).set_index(["date", "currency"])
|
|
1279
|
+
df_prices = df_prices.join(df_currencies)
|
|
1280
|
+
df_prices["net_value_usd"] = df_prices.net_value / df_prices.value
|
|
1281
|
+
df_prices["dollar_volume"] = df_prices.net_value_usd * df_prices.volume
|
|
1282
|
+
|
|
1283
|
+
dollar_volume = (
|
|
1284
|
+
df_prices.dollar_volume.where(df_prices.dollar_volume > 10000).dropna().droplevel("currency").unstack()
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
average_worst_dollar_volume = dollar_volume.where(
|
|
1288
|
+
dollar_volume.le(dollar_volume.astype(float).quantile(pct_worst_volume / 100))
|
|
1289
|
+
).mean()
|
|
1290
|
+
|
|
1291
|
+
days_to_liquidate = below_n_days / (
|
|
1292
|
+
aum * pct_redemption / 100 * df_assets.weighting / (liq_factor * average_worst_dollar_volume)
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
if is_slicing: # if False, computations are done for Waterfall already.
|
|
1296
|
+
days_to_liquidate.loc[
|
|
1297
|
+
df_assets[(df_assets.instrument_type != "Cash") & (df_assets.instrument_type != "Index")].index
|
|
1298
|
+
] = days_to_liquidate.min()
|
|
1299
|
+
|
|
1300
|
+
weights_sold = days_to_liquidate.mask(days_to_liquidate > 1, 1).mul(df_assets.weighting).sum()
|
|
1301
|
+
weights_sold += df_assets.loc[
|
|
1302
|
+
df_assets[(df_assets.instrument_type.key == "cash") | (df_assets.instrument_type.key == "index")].index,
|
|
1303
|
+
"weighting",
|
|
1304
|
+
].sum()
|
|
1305
|
+
|
|
1306
|
+
# readable_result = f"{round(weights_sold * 100, 2)}%"
|
|
1307
|
+
return weights_sold
|