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,11 @@
|
|
|
1
|
+
from .accounts import *
|
|
2
|
+
from .controversy_portfolio import *
|
|
3
|
+
from .exposure_portfolio import *
|
|
4
|
+
from .instrument_list_portfolio import *
|
|
5
|
+
from .liquidity_stress_instrument import *
|
|
6
|
+
from .stop_loss_instrument import *
|
|
7
|
+
from .stop_loss_portfolio import *
|
|
8
|
+
from .ucits_portfolio import *
|
|
9
|
+
from .accounts import *
|
|
10
|
+
from .product_integrity import *
|
|
11
|
+
from .liquidity_risk import *
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from typing import Generator
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from django.contrib.contenttypes.models import ContentType
|
|
6
|
+
from django.db import models
|
|
7
|
+
from wbcompliance.models.risk_management import backend
|
|
8
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
9
|
+
from wbcore import serializers as wb_serializers
|
|
10
|
+
from wbcore.contrib.directory.models import Entry
|
|
11
|
+
from wbcrm.models import Account
|
|
12
|
+
from wbfdm.models import Classification
|
|
13
|
+
from wbfdm.preferences import get_default_classification_group
|
|
14
|
+
from wbportfolio.analysis.claims import ConsolidatedTradeSummary
|
|
15
|
+
from wbportfolio.models import Product, ProductGroup
|
|
16
|
+
from wbportfolio.models.transactions.claim import Claim, ClaimGroupbyChoice
|
|
17
|
+
from wbportfolio.serializers import ProductRepresentationSerializer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register("Account Shares Rule Backend", rule_group_key="sales")
|
|
21
|
+
class RuleBackend(backend.AbstractRuleBackend):
|
|
22
|
+
OBJECT_FIELD_NAME: str = "customer"
|
|
23
|
+
|
|
24
|
+
customer: Entry
|
|
25
|
+
|
|
26
|
+
class FieldChoices(models.TextChoices):
|
|
27
|
+
SHARES = "SHARES", "Shares"
|
|
28
|
+
AUM = "AUM", "AUM"
|
|
29
|
+
|
|
30
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
31
|
+
super().__init__(*args, **kwargs)
|
|
32
|
+
self.end_date = self.evaluation_date
|
|
33
|
+
self.start_date = (self.evaluation_date - pd.tseries.offsets.BDay(self.business_days_interval)).date()
|
|
34
|
+
|
|
35
|
+
self.group_by = ClaimGroupbyChoice[self.group_by]
|
|
36
|
+
self.field = self.FieldChoices[self.field]
|
|
37
|
+
|
|
38
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
39
|
+
return Claim.objects.filter_for_customer(self.customer).filter(date__lte=self.evaluation_date).exists()
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_allowed_content_type(cls) -> "ContentType":
|
|
43
|
+
return ContentType.objects.get_for_model(Entry)
|
|
44
|
+
|
|
45
|
+
def _build_dto_args(self):
|
|
46
|
+
qs = Claim.objects.filter_for_customer(self.customer).filter(
|
|
47
|
+
status=Claim.Status.APPROVED, date__lte=self.evaluation_date
|
|
48
|
+
)
|
|
49
|
+
if self.only_products:
|
|
50
|
+
qs = qs.filter(product__in=self.only_products)
|
|
51
|
+
groupby_map = ClaimGroupbyChoice.get_map(self.group_by.name)
|
|
52
|
+
pivot = groupby_map["pk"]
|
|
53
|
+
pivot_label = groupby_map["title_key"]
|
|
54
|
+
cts_generator = ConsolidatedTradeSummary(
|
|
55
|
+
qs,
|
|
56
|
+
self.start_date,
|
|
57
|
+
self.end_date + timedelta(days=1), # we shift by one because end date is excluded
|
|
58
|
+
pivot,
|
|
59
|
+
pivot_label,
|
|
60
|
+
classification_group=get_default_classification_group(),
|
|
61
|
+
)
|
|
62
|
+
return (cts_generator,)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_all_active_relationships(cls) -> models.QuerySet:
|
|
66
|
+
return Entry.objects.annotate(
|
|
67
|
+
has_open_account=models.Exists(Account.open_objects.filter(owner=models.OuterRef("pk")))
|
|
68
|
+
).filter(has_open_account=True)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
72
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
73
|
+
business_days_interval = wb_serializers.IntegerField(default=7)
|
|
74
|
+
moving_average_window = wb_serializers.IntegerField(default=1) # 1 means the initial time series
|
|
75
|
+
|
|
76
|
+
only_products = wb_serializers.PrimaryKeyRelatedField(
|
|
77
|
+
queryset=Product.objects.all(),
|
|
78
|
+
many=True,
|
|
79
|
+
default=None,
|
|
80
|
+
allow_null=True,
|
|
81
|
+
label="Only Products",
|
|
82
|
+
)
|
|
83
|
+
_only_products = ProductRepresentationSerializer(many=True, source="parameters__only_products")
|
|
84
|
+
|
|
85
|
+
group_by = wb_serializers.ChoiceField(
|
|
86
|
+
choices=ClaimGroupbyChoice.choices(),
|
|
87
|
+
default=ClaimGroupbyChoice.ACCOUNT,
|
|
88
|
+
allow_null=True,
|
|
89
|
+
help_text="Choose how to group by shares",
|
|
90
|
+
label="Group By",
|
|
91
|
+
)
|
|
92
|
+
field = wb_serializers.ChoiceField(
|
|
93
|
+
choices=cls.FieldChoices.choices,
|
|
94
|
+
default=cls.FieldChoices.SHARES,
|
|
95
|
+
allow_null=True,
|
|
96
|
+
help_text="Choose which metric to choose",
|
|
97
|
+
label="Field",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_parameter_fields(cls):
|
|
102
|
+
return [
|
|
103
|
+
"field",
|
|
104
|
+
"group_by",
|
|
105
|
+
"business_days_interval",
|
|
106
|
+
"moving_average_window",
|
|
107
|
+
"only_products",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
return RuleBackendSerializer
|
|
111
|
+
|
|
112
|
+
def _process_dto(
|
|
113
|
+
self, cts_generator: ConsolidatedTradeSummary, **kwargs
|
|
114
|
+
) -> Generator[backend.IncidentResult, None, None]:
|
|
115
|
+
df = cts_generator.get_aum_df()
|
|
116
|
+
if df.empty:
|
|
117
|
+
return
|
|
118
|
+
if self.field == self.FieldChoices.SHARES:
|
|
119
|
+
perf = df["sum_shares_perf"]
|
|
120
|
+
start_df = df["sum_shares_start"]
|
|
121
|
+
end_df = df["sum_shares_end"]
|
|
122
|
+
else:
|
|
123
|
+
perf = df["sum_aum_perf"]
|
|
124
|
+
start_df = df["sum_aum_start"]
|
|
125
|
+
end_df = df["sum_aum_end"]
|
|
126
|
+
perf = perf.dropna()
|
|
127
|
+
if not perf.empty:
|
|
128
|
+
for threshold in self.thresholds:
|
|
129
|
+
numerical_range = threshold.numerical_range
|
|
130
|
+
breached_perf = perf[(perf > numerical_range[0]) & (perf < numerical_range[1])].dropna()
|
|
131
|
+
if not breached_perf.empty:
|
|
132
|
+
for breached_obj_id, percentage in breached_perf.to_dict().items():
|
|
133
|
+
breached_obj = None
|
|
134
|
+
if self.group_by == ClaimGroupbyChoice.PRODUCT:
|
|
135
|
+
breached_obj = Product.objects.get(id=breached_obj_id)
|
|
136
|
+
elif self.group_by == ClaimGroupbyChoice.PRODUCT_GROUP:
|
|
137
|
+
breached_obj = ProductGroup.objects.get(id=breached_obj_id)
|
|
138
|
+
elif self.group_by == ClaimGroupbyChoice.CLASSIFICATION:
|
|
139
|
+
breached_obj = Classification.objects.get(id=breached_obj_id)
|
|
140
|
+
elif self.group_by in [ClaimGroupbyChoice.ACCOUNT, ClaimGroupbyChoice.ROOT_ACCOUNT]:
|
|
141
|
+
breached_obj = Account.objects.get(id=breached_obj_id)
|
|
142
|
+
elif self.group_by in [
|
|
143
|
+
ClaimGroupbyChoice.ACCOUNT_OWNER,
|
|
144
|
+
ClaimGroupbyChoice.ROOT_ACCOUNT_OWNER,
|
|
145
|
+
]:
|
|
146
|
+
breached_obj = Entry.objects.get(id=breached_obj_id)
|
|
147
|
+
report_details = {
|
|
148
|
+
"Period": f"{cts_generator.start_date:%d.%m.%Y} - {cts_generator.end_date:%d.%m.%Y}",
|
|
149
|
+
}
|
|
150
|
+
color = "red" if percentage < 0 else "green"
|
|
151
|
+
if self.field == "AUM":
|
|
152
|
+
report_details[
|
|
153
|
+
"AUM Change"
|
|
154
|
+
] = f'<span style="color:{color}">{start_df.loc[breached_obj_id]:.0f} $ → {end_df.loc[breached_obj_id]:.0f} $</span>'
|
|
155
|
+
else:
|
|
156
|
+
report_details[
|
|
157
|
+
"Shares Change"
|
|
158
|
+
] = f"<span style='color:{color}'>{start_df.loc[breached_obj_id]:.0f} → {end_df.loc[breached_obj_id]:.0f}</span>"
|
|
159
|
+
report_details["Group By"] = self.group_by.value
|
|
160
|
+
yield backend.IncidentResult(
|
|
161
|
+
breached_object=breached_obj,
|
|
162
|
+
breached_object_repr=str(breached_obj),
|
|
163
|
+
breached_value=f'<span style="color:{color}">{percentage:+,.2%}</span>',
|
|
164
|
+
report_details=report_details,
|
|
165
|
+
severity=threshold.severity,
|
|
166
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
from wbcompliance.models.risk_management import backend
|
|
4
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
5
|
+
from wbcompliance.models.risk_management.rules import RiskIncidentType
|
|
6
|
+
from wbcore import serializers as wb_serializers
|
|
7
|
+
from wbfdm.enums import ESGControveryFlag
|
|
8
|
+
from wbfdm.models import Instrument
|
|
9
|
+
from wbfdm.models.esg.controversies import Controversy
|
|
10
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
11
|
+
|
|
12
|
+
from .mixins import ActivePortfolioRelationshipMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register("Controversy Portfolio Rule Backend", rule_group_key="portfolio")
|
|
16
|
+
class RuleBackend(ActivePortfolioRelationshipMixin):
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
|
|
20
|
+
# if thresholds are attached to this rule, we take the first as severity. Otherwise, we get the risk incident with the highest severity (e.g. Critical)
|
|
21
|
+
if self.thresholds:
|
|
22
|
+
self.severity = self.thresholds[0].severity
|
|
23
|
+
else:
|
|
24
|
+
self.severity = RiskIncidentType.objects.order_by("-severity_order").first()
|
|
25
|
+
self.flags_repr = [ESGControveryFlag[f].label for f in self.flags]
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
29
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
30
|
+
flags = wb_serializers.MultipleChoiceField(
|
|
31
|
+
choices=ESGControveryFlag.choices,
|
|
32
|
+
default=[ESGControveryFlag.ORANGE.value, ESGControveryFlag.RED.value],
|
|
33
|
+
label="Flags",
|
|
34
|
+
help_text="Set the flags that will trigger the rule",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_parameter_fields(cls):
|
|
39
|
+
return [
|
|
40
|
+
"flags",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
return RuleBackendSerializer
|
|
44
|
+
|
|
45
|
+
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
46
|
+
for instrument_id, weight in portfolio.positions_map.items():
|
|
47
|
+
instrument = Instrument.objects.get(id=instrument_id)
|
|
48
|
+
if (
|
|
49
|
+
controversies := Controversy.objects.filter(
|
|
50
|
+
instrument__in=instrument.get_ancestors(include_self=True), flag__in=self.flags
|
|
51
|
+
)
|
|
52
|
+
).exists():
|
|
53
|
+
controversies_headlines = "".join([f"<li>{c.headline}</li>" for c in controversies])
|
|
54
|
+
yield backend.IncidentResult(
|
|
55
|
+
breached_object=instrument,
|
|
56
|
+
breached_object_repr=str(instrument),
|
|
57
|
+
breached_value=f"# {controversies.count()}",
|
|
58
|
+
report_details={
|
|
59
|
+
"Controversies Flags": ", ".join(self.flags_repr),
|
|
60
|
+
"Controversies Headlines": controversies_headlines,
|
|
61
|
+
},
|
|
62
|
+
severity=self.severity,
|
|
63
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from wbcompliance.models.risk_management import backend
|
|
5
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
6
|
+
from wbcompliance.models.risk_management.rules import RiskIncidentType
|
|
7
|
+
from wbcore import serializers as wb_serializers
|
|
8
|
+
from wbcore.contrib.currency.models import Currency
|
|
9
|
+
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
10
|
+
from wbcore.contrib.geography.models import Geography
|
|
11
|
+
from wbcore.contrib.geography.serializers import CountryRepresentationSerializer
|
|
12
|
+
from wbfdm.models import Classification, Instrument, InstrumentType
|
|
13
|
+
from wbfdm.serializers import (
|
|
14
|
+
ClassificationRepresentationSerializer,
|
|
15
|
+
InstrumentTypeRepresentationSerializer,
|
|
16
|
+
)
|
|
17
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
18
|
+
|
|
19
|
+
from .mixins import ActivePortfolioRelationshipMixin
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register("Exposure Portfolio Rule Backend", rule_group_key="portfolio")
|
|
23
|
+
class RuleBackend(
|
|
24
|
+
ActivePortfolioRelationshipMixin,
|
|
25
|
+
):
|
|
26
|
+
class GroupbyChoices(models.TextChoices):
|
|
27
|
+
UNDERLYING_INSTRUMENT = "underlying_instrument", "Underlying Instrument"
|
|
28
|
+
ASSET_TYPE = "instrument_type", "Asset Type"
|
|
29
|
+
CASH = "is_cash", "Cash"
|
|
30
|
+
CURRENCY = "currency", "Currency"
|
|
31
|
+
COUNTRY = "country", "Country"
|
|
32
|
+
PRIMARY_CLASSIFICATION = "primary_classification", "Primary Classification"
|
|
33
|
+
FAVORITE_CLASSIFICATION = "favorite_classification", "Favorite Classification"
|
|
34
|
+
|
|
35
|
+
class Field(models.TextChoices):
|
|
36
|
+
WEIGHTING = "weighting", "Weighting"
|
|
37
|
+
MARKET_CAPITALIZATION_USD = "market_capitalization_usd", "Market Capitalization (USD)"
|
|
38
|
+
MARKET_SHARE = "market_share", "Market Shares"
|
|
39
|
+
DAILY_LIQUIDITY = "daily_liquidity", "Daily Liquidity"
|
|
40
|
+
VOLUME_USD = "volume_usd", "Dollar Volume"
|
|
41
|
+
|
|
42
|
+
def __init__(self, *args, **kwargs):
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
self.group_by = self.GroupbyChoices(self.group_by)
|
|
45
|
+
self.field = self.Field(self.field)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
49
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
50
|
+
group_by = wb_serializers.ChoiceField(
|
|
51
|
+
choices=cls.GroupbyChoices.choices,
|
|
52
|
+
default=cls.GroupbyChoices.ASSET_TYPE,
|
|
53
|
+
allow_null=True,
|
|
54
|
+
help_text="Choose how the position will be aggregated before evaluating the rule",
|
|
55
|
+
label="Group By",
|
|
56
|
+
)
|
|
57
|
+
field = wb_serializers.ChoiceField(
|
|
58
|
+
choices=cls.Field.choices,
|
|
59
|
+
default=cls.Field.WEIGHTING,
|
|
60
|
+
allow_null=True,
|
|
61
|
+
label="Field",
|
|
62
|
+
help_text="Choose which field will be evaluated after aggregatation",
|
|
63
|
+
)
|
|
64
|
+
is_cash = wb_serializers.BooleanField(
|
|
65
|
+
default=None, allow_null=True, label="Cash", help_text="Exclude cash position"
|
|
66
|
+
)
|
|
67
|
+
asset_classes = wb_serializers.PrimaryKeyRelatedField(
|
|
68
|
+
queryset=InstrumentType.objects.all(),
|
|
69
|
+
many=True,
|
|
70
|
+
default=None,
|
|
71
|
+
allow_null=True,
|
|
72
|
+
label="Only Asset Classes",
|
|
73
|
+
)
|
|
74
|
+
_asset_classes = InstrumentTypeRepresentationSerializer(source="asset_classes", many=True)
|
|
75
|
+
|
|
76
|
+
currencies = wb_serializers.PrimaryKeyRelatedField(
|
|
77
|
+
queryset=Currency.objects.all(),
|
|
78
|
+
many=True,
|
|
79
|
+
default=None,
|
|
80
|
+
allow_null=True,
|
|
81
|
+
label="Only Currencies",
|
|
82
|
+
)
|
|
83
|
+
_currencies = CurrencyRepresentationSerializer(many=True, source="parameters__currencies")
|
|
84
|
+
countries = wb_serializers.PrimaryKeyRelatedField(
|
|
85
|
+
queryset=Geography.countries.all(),
|
|
86
|
+
many=True,
|
|
87
|
+
default=None,
|
|
88
|
+
allow_null=True,
|
|
89
|
+
label="Only Countries",
|
|
90
|
+
)
|
|
91
|
+
_countries = CountryRepresentationSerializer(
|
|
92
|
+
many=True, source="parameters__countries", filter_params={"level": 1}
|
|
93
|
+
)
|
|
94
|
+
classifications = wb_serializers.PrimaryKeyRelatedField(
|
|
95
|
+
queryset=Classification.objects.all(),
|
|
96
|
+
many=True,
|
|
97
|
+
default=None,
|
|
98
|
+
allow_null=True,
|
|
99
|
+
label="Classifications",
|
|
100
|
+
)
|
|
101
|
+
_classifications = ClassificationRepresentationSerializer(many=True, source="parameters__classifications")
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def get_parameter_fields(cls):
|
|
105
|
+
return [
|
|
106
|
+
"group_by",
|
|
107
|
+
"field",
|
|
108
|
+
"is_cash",
|
|
109
|
+
"asset_classes",
|
|
110
|
+
"currencies",
|
|
111
|
+
"countries",
|
|
112
|
+
"classifications",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
return RuleBackendSerializer
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def report_details(self) -> dict[str, str]:
|
|
119
|
+
repr = {
|
|
120
|
+
"Field": self.field.label,
|
|
121
|
+
"Group By": self.group_by.label,
|
|
122
|
+
}
|
|
123
|
+
if self.is_cash:
|
|
124
|
+
repr["Only Cash"] = "True"
|
|
125
|
+
if self.asset_classes:
|
|
126
|
+
repr["Only Types"] = ", ".join(map(lambda o: o.name, self.asset_classes))
|
|
127
|
+
if self.currencies:
|
|
128
|
+
repr["Only Currencies"] = ", ".join(map(lambda o: o.key, self.currencies))
|
|
129
|
+
if self.countries:
|
|
130
|
+
repr["Only Countries"] = ", ".join(map(lambda o: o.code_2, self.countries))
|
|
131
|
+
if self.classifications:
|
|
132
|
+
repr["Only Classifications"] = ", ".join(map(lambda o: o.name, self.classifications))
|
|
133
|
+
return repr
|
|
134
|
+
|
|
135
|
+
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
136
|
+
if not (df := self._filter_df(portfolio.to_df())).empty:
|
|
137
|
+
df = df[[self.group_by.value, self.field.value]].groupby(self.group_by.value).sum().astype(float)
|
|
138
|
+
for threshold in self.thresholds:
|
|
139
|
+
dff = df.copy()
|
|
140
|
+
numerical_range = threshold.numerical_range
|
|
141
|
+
dff = dff[(dff[self.field.value] > numerical_range[0]) & (dff[self.field.value] < numerical_range[1])]
|
|
142
|
+
if not dff.empty:
|
|
143
|
+
for id, row in dff.to_dict("index").items():
|
|
144
|
+
obj, obj_repr = self._get_obj_repr(id)
|
|
145
|
+
severity: RiskIncidentType = threshold.severity
|
|
146
|
+
if self.field == self.Field.WEIGHTING:
|
|
147
|
+
breached_value = f"{row[self.field.value]:+,.2%}"
|
|
148
|
+
else:
|
|
149
|
+
breached_value = f"{row[self.field.value]:,.3f}"
|
|
150
|
+
if row[self.field.value] < 0:
|
|
151
|
+
breached_value = f'<span style="color:red">{breached_value}</span>'
|
|
152
|
+
else:
|
|
153
|
+
breached_value = f'<span style="color:green">{breached_value}</span>'
|
|
154
|
+
yield backend.IncidentResult(
|
|
155
|
+
breached_object=obj,
|
|
156
|
+
breached_object_repr=obj_repr,
|
|
157
|
+
breached_value=breached_value,
|
|
158
|
+
report_details=self.report_details,
|
|
159
|
+
severity=severity,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _filter_df(self, df):
|
|
163
|
+
if df.empty:
|
|
164
|
+
return df
|
|
165
|
+
if self.is_cash is True or self.is_cash is False:
|
|
166
|
+
df = df[df["is_cash"] == self.is_cash]
|
|
167
|
+
|
|
168
|
+
if self.asset_classes:
|
|
169
|
+
df = df[df["instrument_type"].isin(list(map(lambda o: o.id, self.asset_classes)))]
|
|
170
|
+
if self.countries:
|
|
171
|
+
df = df[(~df["country"].isnull() & df["country"].isin(list(map(lambda o: o.id, self.countries))))]
|
|
172
|
+
if self.currencies:
|
|
173
|
+
df = df[(~df["currency"].isnull() & df["currency"].isin(list(map(lambda o: o.id, self.currencies))))]
|
|
174
|
+
if self.classifications:
|
|
175
|
+
df = df[
|
|
176
|
+
(
|
|
177
|
+
~df["primary_classification"].isnull()
|
|
178
|
+
& df["primary_classification"].isin(list(map(lambda o: o.id, self.classifications)))
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
return df
|
|
182
|
+
|
|
183
|
+
def _get_obj_repr(self, pivot_object_id) -> tuple[models.Model | None, str]:
|
|
184
|
+
match self.group_by:
|
|
185
|
+
case self.GroupbyChoices.UNDERLYING_INSTRUMENT:
|
|
186
|
+
obj = Instrument.objects.get(id=pivot_object_id)
|
|
187
|
+
return obj, str(obj)
|
|
188
|
+
case self.GroupbyChoices.ASSET_TYPE:
|
|
189
|
+
return None, InstrumentType.objects.get(id=pivot_object_id)
|
|
190
|
+
case self.GroupbyChoices.CASH:
|
|
191
|
+
return None, "Cash"
|
|
192
|
+
case self.GroupbyChoices.COUNTRY:
|
|
193
|
+
obj = Geography.countries.get(id=pivot_object_id)
|
|
194
|
+
return obj, str(obj)
|
|
195
|
+
case self.GroupbyChoices.CURRENCY:
|
|
196
|
+
obj = Currency.objects.get(id=pivot_object_id)
|
|
197
|
+
return obj, str(obj)
|
|
198
|
+
case self.GroupbyChoices.PRIMARY_CLASSIFICATION:
|
|
199
|
+
obj = Classification.objects.get(id=pivot_object_id)
|
|
200
|
+
return obj, str(obj)
|
|
201
|
+
case self.GroupbyChoices.FAVORITE_CLASSIFICATION:
|
|
202
|
+
obj = Classification.objects.get(id=pivot_object_id)
|
|
203
|
+
return obj, str(obj)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
from django.db.models import Q
|
|
4
|
+
from wbcompliance.models.risk_management import backend
|
|
5
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
6
|
+
from wbcore import serializers as wb_serializers
|
|
7
|
+
from wbfdm.models import Instrument, InstrumentList, InstrumentListThroughModel
|
|
8
|
+
from wbfdm.serializers.instruments.instrument_lists import (
|
|
9
|
+
InstrumentListRepresentationSerializer,
|
|
10
|
+
)
|
|
11
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
12
|
+
|
|
13
|
+
from .mixins import ActivePortfolioRelationshipMixin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@register("Instrument List Portfolio Rule Backend", rule_group_key="portfolio")
|
|
17
|
+
class RuleBackend(ActivePortfolioRelationshipMixin):
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
if self.instrument_list_type:
|
|
21
|
+
self.instrument_lists = InstrumentList.objects.filter(instrument_list_type=self.instrument_list_type)
|
|
22
|
+
self.instruments_relationship = InstrumentListThroughModel.objects.filter(
|
|
23
|
+
Q(instrument_list__in=self.instrument_lists)
|
|
24
|
+
& (Q(from_date__isnull=True) | Q(from_date__lte=self.evaluation_date))
|
|
25
|
+
& (Q(to_date__isnull=True) | Q(to_date__gt=self.evaluation_date))
|
|
26
|
+
)
|
|
27
|
+
self.instrument_lists_repr = " ,".join(map(lambda x: x.name, self.instrument_lists))
|
|
28
|
+
self.severity = self.thresholds[0].severity
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
32
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
33
|
+
exclude = wb_serializers.BooleanField(
|
|
34
|
+
default=True,
|
|
35
|
+
label="Exclude",
|
|
36
|
+
help_text="If true, the rule will check that the portfolio composition DOES NOT intersect the given instrument list",
|
|
37
|
+
)
|
|
38
|
+
instrument_list_type = wb_serializers.ChoiceField(
|
|
39
|
+
choices=InstrumentList.InstrumentListType.choices,
|
|
40
|
+
required=False,
|
|
41
|
+
default=None,
|
|
42
|
+
allow_null=True,
|
|
43
|
+
help_text="If specified, will dynamically load the list of instrument list to check of the same specified type",
|
|
44
|
+
label="Instrument List Type",
|
|
45
|
+
)
|
|
46
|
+
instrument_lists = wb_serializers.PrimaryKeyRelatedField(
|
|
47
|
+
queryset=InstrumentList.objects.all(),
|
|
48
|
+
many=True,
|
|
49
|
+
default=None,
|
|
50
|
+
allow_null=True,
|
|
51
|
+
label="Instrument Lists",
|
|
52
|
+
)
|
|
53
|
+
_instrument_lists = InstrumentListRepresentationSerializer(many=True)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_parameter_fields(cls):
|
|
57
|
+
return [
|
|
58
|
+
"exclude",
|
|
59
|
+
"instrument_list_type",
|
|
60
|
+
"instrument_lists",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
return RuleBackendSerializer
|
|
64
|
+
|
|
65
|
+
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
66
|
+
for instrument_id, weight in portfolio.positions_map.items():
|
|
67
|
+
instrument = Instrument.objects.get(id=instrument_id)
|
|
68
|
+
relationships = self.instruments_relationship.filter(instrument=instrument, validated=True)
|
|
69
|
+
|
|
70
|
+
if self.exclude and relationships.exists():
|
|
71
|
+
report_details = {
|
|
72
|
+
"Instrument Lists": ", ".join(relationships.values_list("instrument_list__name", flat=True)),
|
|
73
|
+
}
|
|
74
|
+
yield backend.IncidentResult(
|
|
75
|
+
breached_object=instrument,
|
|
76
|
+
breached_object_repr=str(instrument),
|
|
77
|
+
breached_value=f"# {relationships.count()}",
|
|
78
|
+
report_details=report_details,
|
|
79
|
+
severity=self.severity,
|
|
80
|
+
)
|
|
81
|
+
elif not self.exclude and not relationships.exists():
|
|
82
|
+
report_details = {"Instrument Lists": self.instrument_lists_repr}
|
|
83
|
+
yield backend.IncidentResult(
|
|
84
|
+
breached_object=instrument,
|
|
85
|
+
breached_object_repr=str(instrument),
|
|
86
|
+
breached_value=f"# {relationships.count()}",
|
|
87
|
+
report_details=report_details,
|
|
88
|
+
severity=self.severity,
|
|
89
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from math import ceil
|
|
3
|
+
from typing import Generator
|
|
4
|
+
|
|
5
|
+
from django.contrib.contenttypes.models import ContentType
|
|
6
|
+
from django.db import models
|
|
7
|
+
from django.db.models import Exists, OuterRef, Sum
|
|
8
|
+
from wbcompliance.models.risk_management import backend
|
|
9
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
10
|
+
from wbcore import serializers as wb_serializers
|
|
11
|
+
from wbfdm.models import Instrument, InstrumentPrice
|
|
12
|
+
from wbportfolio.models import AssetPosition
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register("Liquidity Risk", rule_group_key="portfolio")
|
|
16
|
+
class RuleBackend(backend.AbstractRuleBackend):
|
|
17
|
+
OBJECT_FIELD_NAME: str = "instrument"
|
|
18
|
+
|
|
19
|
+
instrument: Instrument
|
|
20
|
+
|
|
21
|
+
def get_queryset(self):
|
|
22
|
+
return self.instrument.assets.filter(
|
|
23
|
+
date=self.evaluation_date, shares__isnull=False, is_invested=True
|
|
24
|
+
).exclude(shares=0)
|
|
25
|
+
|
|
26
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
27
|
+
return self.get_queryset().exists()
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_allowed_content_type(cls) -> "ContentType":
|
|
31
|
+
return ContentType.objects.get_for_model(Instrument)
|
|
32
|
+
|
|
33
|
+
def _build_dto_args(self):
|
|
34
|
+
with suppress(InstrumentPrice.DoesNotExist):
|
|
35
|
+
last_price = self.instrument.valuations.get(date=self.evaluation_date)
|
|
36
|
+
total_shares = self.get_queryset().aggregate(c=Sum("shares"))["c"]
|
|
37
|
+
if last_price.volume_50d and total_shares:
|
|
38
|
+
return float(total_shares), last_price.volume_50d
|
|
39
|
+
return tuple()
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_all_active_relationships(cls) -> models.QuerySet:
|
|
43
|
+
try:
|
|
44
|
+
base_qs = AssetPosition.objects.filter(shares__isnull=False, is_invested=True).exclude(shares=0)
|
|
45
|
+
last_asset_position = base_qs.latest("date").date
|
|
46
|
+
|
|
47
|
+
return Instrument.objects.annotate(
|
|
48
|
+
has_assets=Exists(base_qs.filter(underlying_instrument=OuterRef("pk"), date=last_asset_position))
|
|
49
|
+
).filter(has_assets=True, children__isnull=True)
|
|
50
|
+
except AssetPosition.DoesNotExist:
|
|
51
|
+
return Instrument.objects.none()
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
55
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
56
|
+
liquidation_factor = wb_serializers.FloatField(default=3.0, label="Liquidation Factor")
|
|
57
|
+
redemption_pct = wb_serializers.FloatField(default=0.80, label="Redemption Percentage", percent=True)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_parameter_fields(cls):
|
|
61
|
+
return [
|
|
62
|
+
"below_x_days",
|
|
63
|
+
"redemption_pct",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
return RuleBackendSerializer
|
|
67
|
+
|
|
68
|
+
def _process_dto(
|
|
69
|
+
self, total_shares: float, volume_50d: float, **kwargs
|
|
70
|
+
) -> Generator[backend.IncidentResult, None, None]:
|
|
71
|
+
days_to_liquidate = (total_shares * self.redemption_pct * self.liquidation_factor) / volume_50d
|
|
72
|
+
for threshold in self.thresholds:
|
|
73
|
+
numerical_range = threshold.numerical_range
|
|
74
|
+
if days_to_liquidate >= numerical_range[0] and days_to_liquidate < numerical_range[1]:
|
|
75
|
+
yield backend.IncidentResult(
|
|
76
|
+
breached_object=self.instrument,
|
|
77
|
+
breached_object_repr=str(self.instrument),
|
|
78
|
+
breached_value=f"{ceil(days_to_liquidate)} Days",
|
|
79
|
+
report_details={
|
|
80
|
+
"Volume 50D": volume_50d,
|
|
81
|
+
"Total Shares": total_shares,
|
|
82
|
+
"Redemption Percentage": f"{self.redemption_pct:.1%}",
|
|
83
|
+
"Liquidation Factor": self.liquidation_factor,
|
|
84
|
+
},
|
|
85
|
+
severity=threshold.severity,
|
|
86
|
+
)
|