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,86 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
from django.contrib.contenttypes.models import ContentType
|
|
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 wbcore.serializers.fields.number import percent_decorator
|
|
8
|
+
from wbfdm.models import Instrument
|
|
9
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
10
|
+
|
|
11
|
+
from .mixins import ActiveProductRelationshipMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register("Liquidity Stress Instrument Rule Backend", rule_group_key="portfolio")
|
|
15
|
+
class RuleBackend(ActiveProductRelationshipMixin, backend.AbstractRuleBackend):
|
|
16
|
+
@classmethod
|
|
17
|
+
def get_parameter_fields(cls):
|
|
18
|
+
return [
|
|
19
|
+
"group_by",
|
|
20
|
+
"field",
|
|
21
|
+
"is_cash",
|
|
22
|
+
"asset_classes",
|
|
23
|
+
"currencies",
|
|
24
|
+
"countries",
|
|
25
|
+
"classification_height",
|
|
26
|
+
"classifications",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
31
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
32
|
+
below_x_days = wb_serializers.IntegerField(default=5, label="Below X days")
|
|
33
|
+
liquidity_factor = wb_serializers.FloatField(default=0.33, label="Liquidity factory")
|
|
34
|
+
pct_worst_volume = wb_serializers.FloatField(
|
|
35
|
+
default=100.0, label="Worst volume (%)", percent=True, decorators=[percent_decorator]
|
|
36
|
+
)
|
|
37
|
+
pct_redemption = wb_serializers.FloatField(
|
|
38
|
+
default=100.0, label="Redemption (%)", percent=True, decorators=[percent_decorator]
|
|
39
|
+
)
|
|
40
|
+
last_x_trading_dates = wb_serializers.IntegerField(default=60, label="Number of trading days")
|
|
41
|
+
is_slicing = wb_serializers.BooleanField(default=True, label="Slicing")
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def get_parameter_fields(cls):
|
|
45
|
+
return [
|
|
46
|
+
"below_x_days",
|
|
47
|
+
"liquidity_factor",
|
|
48
|
+
"pct_worst_volume",
|
|
49
|
+
"pct_redemption",
|
|
50
|
+
"last_x_trading_dates",
|
|
51
|
+
"is_slicing",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
return RuleBackendSerializer
|
|
55
|
+
|
|
56
|
+
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
57
|
+
# TODO adapt to DTO framework
|
|
58
|
+
factor = self.instrument.pct_liquidated_below_n_days(
|
|
59
|
+
self.evaluation_date,
|
|
60
|
+
below_n_days=self.below_x_days,
|
|
61
|
+
liq_factor=self.liquidity_factor,
|
|
62
|
+
pct_worst_volume=self.pct_worst_volume,
|
|
63
|
+
pct_redemption=self.pct_redemption,
|
|
64
|
+
last_x_trading_dates=self.last_x_trading_dates,
|
|
65
|
+
is_slicing=self.is_slicing,
|
|
66
|
+
)
|
|
67
|
+
for threshold in self.thresholds:
|
|
68
|
+
if factor is not None and threshold.is_inrange(factor):
|
|
69
|
+
yield backend.IncidentResult(
|
|
70
|
+
breached_object=self.instrument,
|
|
71
|
+
breached_object_repr=str(self.instrument),
|
|
72
|
+
breached_value=str(factor),
|
|
73
|
+
report_details=dict(),
|
|
74
|
+
severity=threshold.severity,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def get_allowed_content_type(cls) -> "ContentType":
|
|
79
|
+
return ContentType.objects.get_for_model(Instrument)
|
|
80
|
+
|
|
81
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
82
|
+
if not self.instrument.portfolio:
|
|
83
|
+
return False
|
|
84
|
+
return (self.instrument.portfolio.imported_assets.filter(date=self.evaluation_date).exists()) and (
|
|
85
|
+
self.instrument.valuations.filter(date=self.evaluation_date).exists()
|
|
86
|
+
)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from datetime import date, 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 pandas.tseries.offsets import BDay
|
|
8
|
+
from wbcompliance.models.risk_management import backend
|
|
9
|
+
from wbcore import serializers as wb_serializers
|
|
10
|
+
from wbfdm.backends.dto import PriceDTO
|
|
11
|
+
from wbfdm.models import Instrument, InstrumentPrice, InstrumentType
|
|
12
|
+
from wbfdm.serializers import (
|
|
13
|
+
InstrumentTypeRepresentationSerializer,
|
|
14
|
+
SecurityRepresentationSerializer,
|
|
15
|
+
)
|
|
16
|
+
from wbportfolio.models import InstrumentPortfolioThroughModel, Portfolio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ActivePortfolioRelationshipMixin(backend.AbstractRuleBackend):
|
|
20
|
+
OBJECT_FIELD_NAME: str = "portfolio"
|
|
21
|
+
|
|
22
|
+
portfolio: Portfolio
|
|
23
|
+
|
|
24
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
25
|
+
return self.portfolio.imported_assets.filter(date=self.evaluation_date).exists()
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def get_allowed_content_type(cls) -> "ContentType":
|
|
29
|
+
return ContentType.objects.get_for_model(Portfolio)
|
|
30
|
+
|
|
31
|
+
def _build_dto_args(self):
|
|
32
|
+
return (self.portfolio._build_dto(self.evaluation_date, is_estimated=False),)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_all_active_relationships(cls) -> models.QuerySet:
|
|
36
|
+
valid_relationships = InstrumentPortfolioThroughModel.objects.filter(
|
|
37
|
+
instrument__instrument_type__key="product"
|
|
38
|
+
).values("portfolio")
|
|
39
|
+
return Portfolio.objects.filter(id__in=valid_relationships, is_tracked=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ActiveProductRelationshipMixin(backend.AbstractRuleBackend):
|
|
43
|
+
OBJECT_FIELD_NAME: str = "product"
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def get_allowed_content_type(cls) -> "ContentType":
|
|
47
|
+
return ContentType.objects.get_for_model(Instrument)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get_all_active_relationships(cls) -> models.QuerySet:
|
|
51
|
+
return Instrument.active_objects.filter(instrument_type__key="product")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StopLossMixin(backend.AbstractRuleBackend):
|
|
55
|
+
class FreqChoices(models.TextChoices):
|
|
56
|
+
BUSINESS_DAY = "B", "Business Day"
|
|
57
|
+
WEEKLY_FRIDAY = "W-FRI", "Friday to Friday"
|
|
58
|
+
BUSINESS_MONTHLY = "BME", "Business Monthly"
|
|
59
|
+
BUSINESS_YEARLY = "BYE", "Business Yearly"
|
|
60
|
+
|
|
61
|
+
class DateIntervalOption(models.TextChoices):
|
|
62
|
+
ROLLING_WINDOWS = "ROLLING_WINDOWS", "Rolling Window"
|
|
63
|
+
FREQUENCY = "FREQUENCY", "Frequency"
|
|
64
|
+
|
|
65
|
+
class DynamicBenchmarkType(models.TextChoices):
|
|
66
|
+
PORTFOLIO = "PORTFOLIO", "Primary Portfolio"
|
|
67
|
+
PRIMARY_BENCHMARK = "PRIMARY_BENCHMARK", "Primary Benchmark"
|
|
68
|
+
|
|
69
|
+
class FieldType(models.TextChoices):
|
|
70
|
+
OUTSTANDING_SHARES = "outstanding_shares", "Outstanding shares"
|
|
71
|
+
NET_VALUE = "net_value", "Net Value"
|
|
72
|
+
|
|
73
|
+
def __init__(self, *args, **kwargs):
|
|
74
|
+
super().__init__(*args, **kwargs)
|
|
75
|
+
self.portfolio = self.product.portfolio
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
79
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
80
|
+
freq = wb_serializers.ChoiceField(
|
|
81
|
+
choices=cls.FreqChoices,
|
|
82
|
+
default=cls.FreqChoices.WEEKLY_FRIDAY,
|
|
83
|
+
label="Frequency",
|
|
84
|
+
help_text="Valid only if the interval Option is Frequency. Specify the frequency use for aggregation",
|
|
85
|
+
)
|
|
86
|
+
date_interval_option = wb_serializers.ChoiceField(
|
|
87
|
+
choices=cls.DateIntervalOption,
|
|
88
|
+
default=cls.DateIntervalOption.ROLLING_WINDOWS,
|
|
89
|
+
label="Interval Option",
|
|
90
|
+
)
|
|
91
|
+
dynamic_benchmark_type = wb_serializers.ChoiceField(
|
|
92
|
+
choices=cls.DynamicBenchmarkType,
|
|
93
|
+
default=None,
|
|
94
|
+
allow_null=True,
|
|
95
|
+
label="Benchmark Type",
|
|
96
|
+
help_text="If specified, will compare the stop loss against the instrument related potential benchmark performance",
|
|
97
|
+
)
|
|
98
|
+
rolling_window_interval = wb_serializers.IntegerField(
|
|
99
|
+
default=7,
|
|
100
|
+
label="Rolling Window Interval",
|
|
101
|
+
help_text="Valid only if interval option is Rolling Window. Specify the number of day for sampling",
|
|
102
|
+
)
|
|
103
|
+
static_benchmark = wb_serializers.PrimaryKeyRelatedField(
|
|
104
|
+
queryset=Instrument.objects.all(),
|
|
105
|
+
default=None,
|
|
106
|
+
allow_null=True,
|
|
107
|
+
label="Static Benchmark (If any)",
|
|
108
|
+
help_text="If specified, will compare the stop loss against this benchmark",
|
|
109
|
+
)
|
|
110
|
+
asset_class = wb_serializers.PrimaryKeyRelatedField(
|
|
111
|
+
queryset=InstrumentType.objects.all(),
|
|
112
|
+
default=None,
|
|
113
|
+
allow_null=True,
|
|
114
|
+
label="Only Asset Class",
|
|
115
|
+
)
|
|
116
|
+
_asset_class = InstrumentTypeRepresentationSerializer(source="asset_class")
|
|
117
|
+
|
|
118
|
+
_static_benchmark = SecurityRepresentationSerializer(
|
|
119
|
+
source="static_benchmark", default=None, allow_null=True
|
|
120
|
+
)
|
|
121
|
+
field = wb_serializers.ChoiceField(
|
|
122
|
+
choices=cls.FieldType.choices,
|
|
123
|
+
default=cls.FieldType.NET_VALUE,
|
|
124
|
+
label="Field",
|
|
125
|
+
)
|
|
126
|
+
penny_stock_max_abs_net_value = wb_serializers.FloatField(
|
|
127
|
+
default=0.001, label="Penny Stock Max Absolute Net value"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def get_parameter_fields(cls):
|
|
132
|
+
return [
|
|
133
|
+
"freq",
|
|
134
|
+
"date_interval_option",
|
|
135
|
+
"dynamic_benchmark_type",
|
|
136
|
+
"rolling_window_interval",
|
|
137
|
+
"static_benchmark",
|
|
138
|
+
"asset_class",
|
|
139
|
+
"field",
|
|
140
|
+
"penny_stock_max_abs_net_value",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
return RuleBackendSerializer
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def benchmark(self):
|
|
147
|
+
if self.dynamic_benchmark_type == self.DynamicBenchmarkType.PRIMARY_BENCHMARK.name:
|
|
148
|
+
return self.product.primary_benchmark
|
|
149
|
+
elif self.dynamic_benchmark_type == self.DynamicBenchmarkType.PORTFOLIO.name:
|
|
150
|
+
return self.product
|
|
151
|
+
elif self.static_benchmark:
|
|
152
|
+
return self.static_benchmark
|
|
153
|
+
|
|
154
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
155
|
+
try:
|
|
156
|
+
last_price = self.product.get_price(self.evaluation_date)
|
|
157
|
+
base_condition = last_price != 0
|
|
158
|
+
if (benchmark := self.benchmark) and (last_benchmark_price := benchmark.get_price(self.evaluation_date)):
|
|
159
|
+
return (last_benchmark_price != 0) and base_condition
|
|
160
|
+
return base_condition
|
|
161
|
+
except ValueError:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def _generate_incidents(
|
|
165
|
+
self,
|
|
166
|
+
tested_instrument_id: int,
|
|
167
|
+
perf_instrument: float,
|
|
168
|
+
perf_benchmark: float,
|
|
169
|
+
) -> Generator[backend.IncidentResult, None, None]:
|
|
170
|
+
total_perf = perf_instrument if perf_benchmark is None else perf_instrument - perf_benchmark
|
|
171
|
+
field_label = self.FieldType(self.field).label
|
|
172
|
+
for threshold in self.thresholds:
|
|
173
|
+
if threshold.is_inrange(total_perf):
|
|
174
|
+
instrument = Instrument.objects.get(id=tested_instrument_id)
|
|
175
|
+
report_details = {
|
|
176
|
+
"Absolute Percentage": f"{perf_instrument:,.3%}",
|
|
177
|
+
"Field": field_label,
|
|
178
|
+
}
|
|
179
|
+
if self.benchmark:
|
|
180
|
+
report_details[f"Relative Percentage VS {str(self.benchmark)}"] = f"{total_perf:,.3%}"
|
|
181
|
+
if total_perf < 0:
|
|
182
|
+
breached_value = f'<span style="color:red">{total_perf:+,.2%}</span>'
|
|
183
|
+
else:
|
|
184
|
+
breached_value = f'<span style="color:green">{total_perf:+,.2%}</span>'
|
|
185
|
+
yield backend.IncidentResult(
|
|
186
|
+
breached_object=instrument,
|
|
187
|
+
breached_object_repr=str(instrument),
|
|
188
|
+
breached_value=breached_value,
|
|
189
|
+
report_details=report_details,
|
|
190
|
+
severity=threshold.severity,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _get_start_interval(self) -> date:
|
|
194
|
+
if self.date_interval_option == self.DateIntervalOption.FREQUENCY.name:
|
|
195
|
+
return pd.date_range(end=self.evaluation_date, periods=2, freq=self.FreqChoices(self.freq).value)[0].date()
|
|
196
|
+
else:
|
|
197
|
+
return (self.evaluation_date - timedelta(days=self.rolling_window_interval - 1) - BDay(1)).date()
|
|
198
|
+
|
|
199
|
+
def _get_performance(self, valuation_dto: PriceDTO) -> float:
|
|
200
|
+
if not valuation_dto:
|
|
201
|
+
return 0.0
|
|
202
|
+
start_date = self._get_start_interval()
|
|
203
|
+
if (
|
|
204
|
+
qs := InstrumentPrice.objects.filter(
|
|
205
|
+
calculated=False,
|
|
206
|
+
instrument_id=valuation_dto.instrument,
|
|
207
|
+
date__lte=start_date,
|
|
208
|
+
date__gte=start_date - BDay(2), # we allow 2 business day interval in case of market holiday
|
|
209
|
+
).exclude(
|
|
210
|
+
net_value__lte=self.penny_stock_max_abs_net_value, net_value__gte=-self.penny_stock_max_abs_net_value
|
|
211
|
+
)
|
|
212
|
+
).exists():
|
|
213
|
+
last_value = float(getattr(qs.latest("date"), self.field))
|
|
214
|
+
return float(getattr(valuation_dto, self.field)) / last_value - 1
|
|
215
|
+
return 0.0
|
|
216
|
+
|
|
217
|
+
def _build_dto_args(self) -> tuple[PriceDTO, PriceDTO | None]:
|
|
218
|
+
if benchmark := self.benchmark:
|
|
219
|
+
return self.product._build_dto(self.evaluation_date), benchmark._build_dto(self.evaluation_date)
|
|
220
|
+
return self.product._build_dto(self.evaluation_date), None
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from django.contrib.contenttypes.models import ContentType
|
|
5
|
+
from django.db import models
|
|
6
|
+
from wbcompliance.models.risk_management import backend
|
|
7
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
8
|
+
from wbcore import serializers as wb_serializers
|
|
9
|
+
from wbfdm.models import InstrumentPrice
|
|
10
|
+
from wbportfolio.models import AssetPosition, Product
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register("Product Data Integrity", rule_group_key="portfolio")
|
|
14
|
+
class RuleBackend(backend.AbstractRuleBackend):
|
|
15
|
+
OBJECT_FIELD_NAME: str = "product"
|
|
16
|
+
|
|
17
|
+
class DataTypeChoices(models.TextChoices):
|
|
18
|
+
INSTRUMENT_PRICE = "INSTRUMENT_PRICE", "Valuation"
|
|
19
|
+
ASSET_POSITION = "ASSET_POSITION", "Asset Position"
|
|
20
|
+
|
|
21
|
+
product: Product
|
|
22
|
+
|
|
23
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
24
|
+
return self.product.is_active_at_date(self.evaluation_date)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def get_allowed_content_type(cls) -> "ContentType":
|
|
28
|
+
return ContentType.objects.get_for_model(Product)
|
|
29
|
+
|
|
30
|
+
def _build_dto_args(self):
|
|
31
|
+
return (self.product,)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_all_active_relationships(cls) -> models.QuerySet:
|
|
35
|
+
return Product.active_objects.all()
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
39
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
40
|
+
data_type = wb_serializers.MultipleChoiceField(
|
|
41
|
+
choices=cls.DataTypeChoices.choices,
|
|
42
|
+
default=[cls.DataTypeChoices.ASSET_POSITION.value, cls.DataTypeChoices.INSTRUMENT_PRICE.value],
|
|
43
|
+
label="Flags",
|
|
44
|
+
help_text="Set the flags that will trigger the rule",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_parameter_fields(cls):
|
|
49
|
+
return ["data_type"]
|
|
50
|
+
|
|
51
|
+
return RuleBackendSerializer
|
|
52
|
+
|
|
53
|
+
def _process_dto(self, product: Product, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
54
|
+
for lag_threshold in self.thresholds:
|
|
55
|
+
numerical_range = lag_threshold.numerical_range
|
|
56
|
+
|
|
57
|
+
last_asset_position_date = (
|
|
58
|
+
AssetPosition.objects.filter(is_estimated=False, portfolio=product.portfolio).latest("date").date
|
|
59
|
+
)
|
|
60
|
+
last_instrument_price_date = (
|
|
61
|
+
InstrumentPrice.objects.filter(calculated=False, instrument=product).latest("date").date
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
asset_position_lag = (
|
|
65
|
+
len(
|
|
66
|
+
pd.date_range(
|
|
67
|
+
min([self.evaluation_date, last_asset_position_date]), self.evaluation_date, freq="B"
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
- 1
|
|
71
|
+
)
|
|
72
|
+
instrument_price_lag = (
|
|
73
|
+
len(
|
|
74
|
+
pd.date_range(
|
|
75
|
+
min([self.evaluation_date, last_instrument_price_date]), self.evaluation_date, freq="B"
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
- 1
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
self.DataTypeChoices.ASSET_POSITION.value in self.data_type
|
|
83
|
+
and asset_position_lag >= numerical_range[0]
|
|
84
|
+
and asset_position_lag < numerical_range[1]
|
|
85
|
+
):
|
|
86
|
+
yield backend.IncidentResult(
|
|
87
|
+
breached_object=product,
|
|
88
|
+
breached_object_repr=str(product),
|
|
89
|
+
breached_value=str(asset_position_lag),
|
|
90
|
+
report_details={
|
|
91
|
+
"Last Datapoint": f"{last_asset_position_date:%d.%m.%Y}",
|
|
92
|
+
"Data Type": "Asset Position",
|
|
93
|
+
},
|
|
94
|
+
severity=lag_threshold.severity,
|
|
95
|
+
)
|
|
96
|
+
if (
|
|
97
|
+
self.DataTypeChoices.INSTRUMENT_PRICE.value in self.data_type
|
|
98
|
+
and instrument_price_lag >= numerical_range[0]
|
|
99
|
+
and instrument_price_lag < numerical_range[1]
|
|
100
|
+
):
|
|
101
|
+
yield backend.IncidentResult(
|
|
102
|
+
breached_object=product,
|
|
103
|
+
breached_object_repr=str(product),
|
|
104
|
+
breached_value=str(instrument_price_lag),
|
|
105
|
+
report_details={
|
|
106
|
+
"Lag": instrument_price_lag,
|
|
107
|
+
"Last Datapoint": f"{last_instrument_price_date:%d.%m.%Y}",
|
|
108
|
+
"Data Type": "Valuation",
|
|
109
|
+
},
|
|
110
|
+
severity=lag_threshold.severity,
|
|
111
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
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 wbportfolio.pms.typing import Valuation as ValuationDTO
|
|
6
|
+
|
|
7
|
+
from .mixins import ActiveProductRelationshipMixin, StopLossMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register("Stop Loss Instrument Rule Backend", rule_group_key="portfolio")
|
|
11
|
+
class RuleBackend(ActiveProductRelationshipMixin, StopLossMixin, backend.AbstractRuleBackend):
|
|
12
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
13
|
+
return (
|
|
14
|
+
super().is_passive_evaluation_valid()
|
|
15
|
+
and self.product
|
|
16
|
+
and self.product.valuations.filter(date=self.evaluation_date).exists()
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def _process_dto(
|
|
20
|
+
self, instrument_valuation_dto: ValuationDTO, benchmark_valuation_dto: ValuationDTO = None, *args, **kwargs
|
|
21
|
+
) -> Generator[backend.IncidentResult, None, None]:
|
|
22
|
+
perf_instrument = self._get_performance(instrument_valuation_dto)
|
|
23
|
+
perf_benchmark = self._get_performance(benchmark_valuation_dto)
|
|
24
|
+
yield from self._generate_incidents(self.product.id, perf_instrument, perf_benchmark)
|
|
@@ -0,0 +1,36 @@
|
|
|
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 wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
6
|
+
from wbportfolio.pms.typing import Valuation as ValuationDTO
|
|
7
|
+
|
|
8
|
+
from .mixins import ActiveProductRelationshipMixin, StopLossMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register("Stop Loss Portfolio Rule Backend", rule_group_key="portfolio")
|
|
12
|
+
class RuleBackend(ActiveProductRelationshipMixin, StopLossMixin, backend.AbstractRuleBackend):
|
|
13
|
+
def _build_dto_args(self) -> tuple[PortfolioDTO, ValuationDTO | None]:
|
|
14
|
+
return (
|
|
15
|
+
self.portfolio._build_dto(self.evaluation_date),
|
|
16
|
+
super()._build_dto_args()[1],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def is_passive_evaluation_valid(self) -> bool:
|
|
20
|
+
return (
|
|
21
|
+
super().is_passive_evaluation_valid()
|
|
22
|
+
and self.portfolio
|
|
23
|
+
and self.portfolio.assets.filter(date=self.evaluation_date).exists()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def _process_dto(
|
|
27
|
+
self, portfolio: PortfolioDTO, benchmark_valuation_dto: ValuationDTO = None, **kwargs
|
|
28
|
+
) -> Generator[backend.IncidentResult, None, None]:
|
|
29
|
+
if self.asset_class:
|
|
30
|
+
portfolio = PortfolioDTO(filter(lambda x: x.instrument_type == self.asset_class.id, portfolio.positions))
|
|
31
|
+
perf_benchmark = self._get_performance(benchmark_valuation_dto)
|
|
32
|
+
for instrument_id, pos in portfolio.positions_map.items():
|
|
33
|
+
if pos.price is not None:
|
|
34
|
+
perf_instrument = self._get_performance(ValuationDTO(instrument=instrument_id, net_value=pos.price))
|
|
35
|
+
|
|
36
|
+
yield from self._generate_incidents(instrument_id, perf_instrument, perf_benchmark)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Generator
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from wbcompliance.models import RiskIncidentType
|
|
5
|
+
from wbcompliance.models.risk_management import backend
|
|
6
|
+
from wbcompliance.models.risk_management.dispatch import register
|
|
7
|
+
from wbcore import serializers as wb_serializers
|
|
8
|
+
from wbcore.serializers.fields.number import percent_decorator
|
|
9
|
+
from wbfdm.models import Instrument
|
|
10
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
11
|
+
|
|
12
|
+
from .mixins import ActivePortfolioRelationshipMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register("UCITS 5|10|40 Portfolio Rule Backend", rule_group_key="portfolio")
|
|
16
|
+
class RuleBackend(ActivePortfolioRelationshipMixin, backend.AbstractRuleBackend):
|
|
17
|
+
DEFAULT_THRESHOLD_1: float = 0.05
|
|
18
|
+
DEFAULT_THRESHOLD_2: float = 0.10
|
|
19
|
+
DEFAULT_THRESHOLD_3: float = 0.40
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def get_serializer_class(cls) -> wb_serializers.Serializer:
|
|
23
|
+
class RuleBackendSerializer(wb_serializers.Serializer):
|
|
24
|
+
threshold_1 = wb_serializers.FloatField(
|
|
25
|
+
default=cls.DEFAULT_THRESHOLD_1, label="Threshold 1", percent=True, decorators=[percent_decorator]
|
|
26
|
+
)
|
|
27
|
+
threshold_2 = wb_serializers.FloatField(
|
|
28
|
+
default=cls.DEFAULT_THRESHOLD_2, label="Threshold 2", percent=True, decorators=[percent_decorator]
|
|
29
|
+
)
|
|
30
|
+
threshold_3 = wb_serializers.FloatField(
|
|
31
|
+
default=cls.DEFAULT_THRESHOLD_3, label="Threshold 3", percent=True, decorators=[percent_decorator]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_parameter_fields(cls):
|
|
36
|
+
return ["threshold_1", "threshold_2", "threshold_3"]
|
|
37
|
+
|
|
38
|
+
return RuleBackendSerializer
|
|
39
|
+
|
|
40
|
+
def _filter_df(self, df):
|
|
41
|
+
if df.empty:
|
|
42
|
+
return df
|
|
43
|
+
return df[(df["weighting"] >= self.threshold_1) & (~df["is_cash"])]
|
|
44
|
+
|
|
45
|
+
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
46
|
+
if not (df := self._filter_df(pd.DataFrame(portfolio.to_df()).astype({"weighting": float}))).empty:
|
|
47
|
+
df = df[["underlying_instrument", "weighting"]].groupby("underlying_instrument").sum()
|
|
48
|
+
total_weight_threshold_1_2 = df.loc[df["weighting"] < self.threshold_2, "weighting"].sum()
|
|
49
|
+
highest_incident_type = RiskIncidentType.objects.order_by("-severity_order").first()
|
|
50
|
+
for id, row in df.to_dict("index").items():
|
|
51
|
+
if (row["weighting"] > self.threshold_2) or (total_weight_threshold_1_2 > self.threshold_3):
|
|
52
|
+
instrument = Instrument.objects.get(id=id)
|
|
53
|
+
yield backend.IncidentResult(
|
|
54
|
+
breached_object=instrument,
|
|
55
|
+
breached_object_repr=str(instrument),
|
|
56
|
+
breached_value=f'∑[0%, {self.threshold_2:.2%}]: {row["weighting"]:+.2%} | ∑[{self.threshold_1:.2%}, {self.threshold_2:.2%}]: {total_weight_threshold_1_2:+.2%}',
|
|
57
|
+
report_details={
|
|
58
|
+
"Breach Thresholds": f"{self.threshold_1}|{self.threshold_2}|{self.threshold_3}",
|
|
59
|
+
"Weighting": f"{row['weighting']:.3f}",
|
|
60
|
+
f"Sum of positions whose weight are between {self.threshold_1:.2%} and {self.threshold_2:.2%}": f"{total_weight_threshold_1_2:.3f}",
|
|
61
|
+
},
|
|
62
|
+
severity=highest_incident_type,
|
|
63
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pytest_factoryboy import register
|
|
2
|
+
from wbcompliance.factories.risk_management import (
|
|
3
|
+
RiskIncidentTypeFactory,
|
|
4
|
+
RiskRuleFactory,
|
|
5
|
+
RuleBackendFactory,
|
|
6
|
+
RuleThresholdFactory,
|
|
7
|
+
)
|
|
8
|
+
from wbfdm.factories import ControversyFactory
|
|
9
|
+
from wbportfolio.tests.conftest import * # noqa
|
|
10
|
+
|
|
11
|
+
register(RiskIncidentTypeFactory)
|
|
12
|
+
register(RiskRuleFactory)
|
|
13
|
+
register(RuleBackendFactory)
|
|
14
|
+
register(RuleThresholdFactory)
|
|
15
|
+
register(ControversyFactory)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from faker import Faker
|
|
3
|
+
from pandas.tseries.offsets import BDay
|
|
4
|
+
from psycopg.types.range import NumericRange
|
|
5
|
+
from wbcompliance.factories.risk_management import RuleThresholdFactory
|
|
6
|
+
from wbportfolio.models.transactions.claim import ClaimGroupbyChoice
|
|
7
|
+
from wbportfolio.risk_management.backends.accounts import (
|
|
8
|
+
RuleBackend as AccountRuleBackend,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
fake = Faker()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.django_db
|
|
15
|
+
class TestAccountRuleModel:
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def account_backend(
|
|
18
|
+
self,
|
|
19
|
+
entry,
|
|
20
|
+
weekday,
|
|
21
|
+
business_days_interval,
|
|
22
|
+
group_by=ClaimGroupbyChoice.ACCOUNT.name,
|
|
23
|
+
extra_parameters=None,
|
|
24
|
+
):
|
|
25
|
+
parameters = {"group_by": group_by, "business_days_interval": business_days_interval, "field": "SHARES"}
|
|
26
|
+
if extra_parameters:
|
|
27
|
+
parameters.update(extra_parameters)
|
|
28
|
+
return AccountRuleBackend(
|
|
29
|
+
weekday,
|
|
30
|
+
entry,
|
|
31
|
+
parameters,
|
|
32
|
+
[RuleThresholdFactory.create(range=NumericRange(lower=None, upper=-0.2))], # detect any -20% perf
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize(
|
|
36
|
+
"business_days_interval",
|
|
37
|
+
[7],
|
|
38
|
+
)
|
|
39
|
+
def test_check_rule_groupby_account(
|
|
40
|
+
self,
|
|
41
|
+
weekday,
|
|
42
|
+
entry,
|
|
43
|
+
business_days_interval,
|
|
44
|
+
product,
|
|
45
|
+
claim_factory,
|
|
46
|
+
customer_trade_factory,
|
|
47
|
+
account_factory,
|
|
48
|
+
account_backend,
|
|
49
|
+
):
|
|
50
|
+
# Simple test to test if a valid drop in performance outside the rule window will not be detected but a one within will.
|
|
51
|
+
account = account_factory.create(owner=entry)
|
|
52
|
+
other_account = account_factory.create()
|
|
53
|
+
claim_factory.create(
|
|
54
|
+
date=(weekday - BDay(business_days_interval + 2)).date(),
|
|
55
|
+
account=account,
|
|
56
|
+
trade=customer_trade_factory.create(underlying_instrument=product),
|
|
57
|
+
status="APPROVED",
|
|
58
|
+
shares=100,
|
|
59
|
+
)
|
|
60
|
+
claim_factory.create(
|
|
61
|
+
date=(weekday - BDay(business_days_interval + 1)).date(),
|
|
62
|
+
account=account,
|
|
63
|
+
trade=customer_trade_factory.create(underlying_instrument=product),
|
|
64
|
+
status="APPROVED",
|
|
65
|
+
shares=-50,
|
|
66
|
+
) # this drop should not be detected
|
|
67
|
+
|
|
68
|
+
claim_factory.create(
|
|
69
|
+
date=(weekday - BDay(business_days_interval)).date(),
|
|
70
|
+
account=account,
|
|
71
|
+
trade=customer_trade_factory.create(underlying_instrument=product),
|
|
72
|
+
status="APPROVED",
|
|
73
|
+
shares=150,
|
|
74
|
+
)
|
|
75
|
+
claim_factory.create(
|
|
76
|
+
date=weekday,
|
|
77
|
+
account=account,
|
|
78
|
+
trade=customer_trade_factory.create(underlying_instrument=product),
|
|
79
|
+
status="APPROVED",
|
|
80
|
+
shares=-50,
|
|
81
|
+
) # this drop should be detected
|
|
82
|
+
|
|
83
|
+
claim_factory.create(
|
|
84
|
+
date=(weekday - BDay(business_days_interval)).date(),
|
|
85
|
+
account=other_account,
|
|
86
|
+
trade=customer_trade_factory.create(underlying_instrument=product),
|
|
87
|
+
status="APPROVED",
|
|
88
|
+
shares=150,
|
|
89
|
+
)
|
|
90
|
+
claim_factory.create(
|
|
91
|
+
date=weekday,
|
|
92
|
+
account=other_account,
|
|
93
|
+
trade=customer_trade_factory.create(underlying_instrument=product),
|
|
94
|
+
status="APPROVED",
|
|
95
|
+
shares=-50,
|
|
96
|
+
) # this drop is valid but an another account so won't be detected
|
|
97
|
+
res = list(account_backend.check_rule())
|
|
98
|
+
assert len(res) == 1
|