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,634 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from datetime import date as date_lib
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from django.db import models
|
|
7
|
+
from django.db.models import (
|
|
8
|
+
DateField,
|
|
9
|
+
Exists,
|
|
10
|
+
ExpressionWrapper,
|
|
11
|
+
OuterRef,
|
|
12
|
+
Q,
|
|
13
|
+
QuerySet,
|
|
14
|
+
Sum,
|
|
15
|
+
)
|
|
16
|
+
from django.db.models.functions import Greatest
|
|
17
|
+
from django.dispatch import receiver
|
|
18
|
+
from django_fsm import FSMField, transition
|
|
19
|
+
from wbcore.contrib.ai.llm.config import add_llm_prompt
|
|
20
|
+
from wbcore.contrib.authentication.models import User
|
|
21
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
22
|
+
from wbcore.contrib.directory.models import Entry
|
|
23
|
+
from wbcore.contrib.icons import WBIcon
|
|
24
|
+
from wbcore.enums import RequestType
|
|
25
|
+
from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
|
|
26
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
27
|
+
create_simple_display,
|
|
28
|
+
)
|
|
29
|
+
from wbcore.models import WBModel
|
|
30
|
+
from wbcore.signals import pre_merge
|
|
31
|
+
from wbcore.signals.models import pre_collection
|
|
32
|
+
from wbcore.utils.enum import ChoiceEnum
|
|
33
|
+
from wbcore.utils.strings import ReferenceIDMixin
|
|
34
|
+
from wbcrm.models import AccountRole
|
|
35
|
+
from wbcrm.models.accounts import Account
|
|
36
|
+
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
37
|
+
from wbportfolio.models.llm.wbcrm.analyze_relationship import get_performances_prompt
|
|
38
|
+
|
|
39
|
+
from ..custodians import Custodian
|
|
40
|
+
from ..roles import PortfolioRole
|
|
41
|
+
from .trades import Trade
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def can_administrate_claim(claim, user):
|
|
45
|
+
today = date_lib.today()
|
|
46
|
+
return (
|
|
47
|
+
user.profile.portfolio_roles.filter(
|
|
48
|
+
Q(role_type=PortfolioRole.RoleType.MANAGER)
|
|
49
|
+
& (Q(start__isnull=True) | Q(start__lte=today))
|
|
50
|
+
& (Q(end__isnull=True) | Q(end__gte=today))
|
|
51
|
+
).exists()
|
|
52
|
+
or user.is_superuser
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def can_edit_claim(claim, user):
|
|
57
|
+
return (claim.claimant and claim.claimant.id == user.profile.id) or user.profile.is_internal or user.is_superuser
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ClaimGroupbyChoice(ChoiceEnum):
|
|
61
|
+
ROOT_ACCOUNT = "Root Account"
|
|
62
|
+
ACCOUNT = "Account"
|
|
63
|
+
ROOT_ACCOUNT_OWNER = "Root Account Owner"
|
|
64
|
+
ACCOUNT_OWNER = "Account Owner"
|
|
65
|
+
PRODUCT = "Product"
|
|
66
|
+
PRODUCT_GROUP = "ProductGroup"
|
|
67
|
+
CLASSIFICATION = "Classification"
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
@property
|
|
71
|
+
def map(cls):
|
|
72
|
+
return {
|
|
73
|
+
"ROOT_ACCOUNT": {
|
|
74
|
+
"pk": "root_account",
|
|
75
|
+
"title_key": "root_account_repr",
|
|
76
|
+
"search_fields": ["root_account_repr"],
|
|
77
|
+
},
|
|
78
|
+
"ACCOUNT": {
|
|
79
|
+
"pk": "account",
|
|
80
|
+
"title_key": "account__computed_str",
|
|
81
|
+
"search_fields": ["account__computed_str"],
|
|
82
|
+
},
|
|
83
|
+
"ROOT_ACCOUNT_OWNER": {
|
|
84
|
+
"pk": "root_account_owner",
|
|
85
|
+
"title_key": "root_account_owner_repr",
|
|
86
|
+
"search_fields": ["root_account_owner_repr"],
|
|
87
|
+
},
|
|
88
|
+
"ACCOUNT_OWNER": {
|
|
89
|
+
"pk": "account__owner",
|
|
90
|
+
"title_key": "account__owner__computed_str",
|
|
91
|
+
"search_fields": ["account__owner__computed_str"],
|
|
92
|
+
},
|
|
93
|
+
"PRODUCT": {
|
|
94
|
+
"pk": "product",
|
|
95
|
+
"title_key": "product__computed_str",
|
|
96
|
+
"search_fields": ["product__computed_str"],
|
|
97
|
+
},
|
|
98
|
+
"PRODUCT_GROUP": {
|
|
99
|
+
"pk": "product__parent",
|
|
100
|
+
"title_key": "product__parent__name",
|
|
101
|
+
"search_fields": ["product__parent__name"],
|
|
102
|
+
},
|
|
103
|
+
"CLASSIFICATION": {
|
|
104
|
+
"pk": "classification_id",
|
|
105
|
+
"title_key": "classification_title",
|
|
106
|
+
"search_fields": ["classification_title"],
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def get_map(cls, name):
|
|
112
|
+
return cls.map[name]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ClaimDefaultQueryset(QuerySet):
|
|
116
|
+
def filter_for_user(self, user: User, validity_date: date_lib | None = None) -> QuerySet:
|
|
117
|
+
"""
|
|
118
|
+
Protect the chained queryset and filter the claims that this user cannot see based on the following rules:
|
|
119
|
+
"""
|
|
120
|
+
if user.has_perm("wbcrm.administrate_account"):
|
|
121
|
+
return self
|
|
122
|
+
allowed_accounts = Account.objects.filter_for_user(user, validity_date=validity_date).values(
|
|
123
|
+
"id"
|
|
124
|
+
) # This is way faster
|
|
125
|
+
# accounts = self.annotate(can_see_account=Exists(allowed_accounts.filter(id=OuterRef("account"))))
|
|
126
|
+
if user.profile.is_internal:
|
|
127
|
+
return self.filter(Q(account__isnull=True) | Q(account__in=allowed_accounts))
|
|
128
|
+
return self.filter(Q(account__in=allowed_accounts) | (Q(account__isnull=True) & Q(creator_id=user.profile.id)))
|
|
129
|
+
|
|
130
|
+
def filter_for_customer(self, customer: Entry, include_related_roles: bool = False) -> QuerySet:
|
|
131
|
+
"""
|
|
132
|
+
Filter the chained queryset to return only the claim that belongs to a certain customer
|
|
133
|
+
"""
|
|
134
|
+
customer_account_ids = Account.get_accounts_for_customer(customer).values("id")
|
|
135
|
+
|
|
136
|
+
if not include_related_roles:
|
|
137
|
+
return self.filter(account__in=customer_account_ids)
|
|
138
|
+
return self.annotate(
|
|
139
|
+
role_exists=Exists(AccountRole.objects.filter(entry=customer, account=OuterRef("account")))
|
|
140
|
+
).filter(Q(account__in=customer_account_ids) | Q(role_exists=True))
|
|
141
|
+
|
|
142
|
+
def annotate_asset_under_management_for_date(self, val_date: date):
|
|
143
|
+
return self.annotate(
|
|
144
|
+
net_value=InstrumentPrice.subquery_closest_value("net_value", val_date, instrument_pk_name="product"),
|
|
145
|
+
fx_rate=CurrencyFXRates.get_fx_rates_subquery(val_date),
|
|
146
|
+
asset_under_management=models.ExpressionWrapper(
|
|
147
|
+
models.F("net_value") * models.F("shares"),
|
|
148
|
+
output_field=models.DecimalField(max_digits=16, decimal_places=4, default=0.0),
|
|
149
|
+
),
|
|
150
|
+
asset_under_management_usd=models.ExpressionWrapper(
|
|
151
|
+
models.F("asset_under_management") * models.F("fx_rate"),
|
|
152
|
+
output_field=models.DecimalField(max_digits=16, decimal_places=4, default=0.0),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ClaimManager(models.Manager):
|
|
158
|
+
def get_queryset(self) -> ClaimDefaultQueryset:
|
|
159
|
+
return ClaimDefaultQueryset(self.model)
|
|
160
|
+
|
|
161
|
+
def filter_for_user(self, user: User, validity_date: date_lib | None = None) -> QuerySet:
|
|
162
|
+
return self.get_queryset().filter_for_user(user, validity_date=validity_date)
|
|
163
|
+
|
|
164
|
+
def filter_for_customer(self, customer: Entry, include_related_roles: bool = False) -> QuerySet:
|
|
165
|
+
return self.get_queryset().filter_for_customer(customer)
|
|
166
|
+
|
|
167
|
+
def annotate_asset_under_management_for_date(self, val_date: date) -> QuerySet:
|
|
168
|
+
return self.get_queryset().annotate_asset_under_management_for_date(val_date)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# @workflow.register(serializer_class="wbportfolio.serializers.transactions.claim.ClaimModelSerializer") #we don't register for now. Uncomment as soon as we want to enable workflow on that model
|
|
172
|
+
class Claim(ReferenceIDMixin, WBModel):
|
|
173
|
+
"""A customer can claim that a trade or part of a trade was executed my them"""
|
|
174
|
+
|
|
175
|
+
class Status(models.TextChoices):
|
|
176
|
+
PENDING = "PENDING", "Pending"
|
|
177
|
+
APPROVED = "APPROVED", "Approved"
|
|
178
|
+
WITHDRAWN = "WITHDRAWN", "Withdrawn"
|
|
179
|
+
DRAFT = "DRAFT", "Draft"
|
|
180
|
+
AUTO_MATCHED = "AUTO_MATCHED", "Auto-Matched"
|
|
181
|
+
|
|
182
|
+
status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name="Status")
|
|
183
|
+
account = models.ForeignKey(
|
|
184
|
+
to="wbcrm.Account",
|
|
185
|
+
related_name="claims",
|
|
186
|
+
null=True,
|
|
187
|
+
blank=True,
|
|
188
|
+
limit_choices_to=models.Q(is_terminal_account=True),
|
|
189
|
+
on_delete=models.SET_NULL,
|
|
190
|
+
verbose_name="Account",
|
|
191
|
+
# help_text="The account the claim is assigned to. If no sub-account is provided it will be tried to be assigned to a sub account based on the given claimant."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
trade = models.ForeignKey(
|
|
195
|
+
to="wbportfolio.Trade",
|
|
196
|
+
related_name="claims",
|
|
197
|
+
blank=True,
|
|
198
|
+
null=True,
|
|
199
|
+
on_delete=models.PROTECT, # We protect the claim in case of trade deletion. This needs to be handled upstream
|
|
200
|
+
verbose_name="Trade",
|
|
201
|
+
help_text="Please select a Product first. The customer-trade the claim is consolidated against. The customer-trade and the claim don't necessarily have to have the same date, number of shares, etc.",
|
|
202
|
+
)
|
|
203
|
+
product = models.ForeignKey(
|
|
204
|
+
to="wbportfolio.Product", related_name="claims", on_delete=models.PROTECT, verbose_name="Product"
|
|
205
|
+
)
|
|
206
|
+
claimant = models.ForeignKey(
|
|
207
|
+
to="directory.Entry",
|
|
208
|
+
related_name="claimed",
|
|
209
|
+
null=True,
|
|
210
|
+
blank=True,
|
|
211
|
+
on_delete=models.SET_NULL,
|
|
212
|
+
verbose_name="Claimant",
|
|
213
|
+
# help_text="The person / company that claims this trade. If no claimant is given, it will assigned to the current user."
|
|
214
|
+
)
|
|
215
|
+
creator = models.ForeignKey(
|
|
216
|
+
to="directory.Entry",
|
|
217
|
+
related_name="claims_created",
|
|
218
|
+
null=True,
|
|
219
|
+
blank=True,
|
|
220
|
+
on_delete=models.SET_NULL,
|
|
221
|
+
verbose_name="Creator",
|
|
222
|
+
)
|
|
223
|
+
date = models.DateField(verbose_name="Trade Date")
|
|
224
|
+
bank = models.CharField(max_length=255, blank=True, verbose_name="Bank")
|
|
225
|
+
reference = models.CharField(max_length=255, null=True, blank=True, verbose_name="Additional Reference")
|
|
226
|
+
shares = models.DecimalField(
|
|
227
|
+
max_digits=15,
|
|
228
|
+
decimal_places=4,
|
|
229
|
+
help_text="The amount of shares purchased / sold.",
|
|
230
|
+
null=True,
|
|
231
|
+
blank=True,
|
|
232
|
+
verbose_name="Shares",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
nominal_amount = models.DecimalField(
|
|
236
|
+
max_digits=15,
|
|
237
|
+
decimal_places=2,
|
|
238
|
+
help_text="The nominal amount purchased / sold. Either shares or nominal_amount has to be provided.",
|
|
239
|
+
null=True,
|
|
240
|
+
blank=True,
|
|
241
|
+
verbose_name="Nominal Amount",
|
|
242
|
+
)
|
|
243
|
+
external_id = models.CharField(
|
|
244
|
+
max_length=255,
|
|
245
|
+
null=True,
|
|
246
|
+
blank=True,
|
|
247
|
+
help_text="An External ID to reference a claim.",
|
|
248
|
+
verbose_name="External Identifier",
|
|
249
|
+
)
|
|
250
|
+
as_shares = models.BooleanField(default=True)
|
|
251
|
+
|
|
252
|
+
objects = ClaimManager()
|
|
253
|
+
|
|
254
|
+
class Meta:
|
|
255
|
+
verbose_name = "Claim"
|
|
256
|
+
verbose_name_plural = "Claims"
|
|
257
|
+
|
|
258
|
+
def __str__(self):
|
|
259
|
+
return f"{self.reference_id} {self.product.name} ({self.bank} - {self.shares:,} shares - {self.date}) "
|
|
260
|
+
|
|
261
|
+
def save(self, *args, auto_match: bool = True, **kwargs):
|
|
262
|
+
assert (
|
|
263
|
+
self.shares is not None or self.nominal_amount is not None
|
|
264
|
+
), f"Either shares or nominal amount have to be provided. Shares={self.shares}, Nominal={self.nominal_amount}"
|
|
265
|
+
if self.product:
|
|
266
|
+
if self.shares is not None:
|
|
267
|
+
self.nominal_amount = self.shares * self.product.share_price
|
|
268
|
+
else:
|
|
269
|
+
self.shares = self.nominal_amount / self.product.share_price
|
|
270
|
+
if not self.trade and self.status == self.Status.DRAFT and auto_match:
|
|
271
|
+
self.trade = self.auto_match()
|
|
272
|
+
if self.status == self.Status.WITHDRAWN and self.trade:
|
|
273
|
+
self.trade = None
|
|
274
|
+
if self.trade:
|
|
275
|
+
if not self.trade.is_claimable:
|
|
276
|
+
raise ValueError("The selected trade is not a valid customer trade")
|
|
277
|
+
if self.product and self.trade.underlying_instrument.id != self.product.id:
|
|
278
|
+
raise ValueError("The selected product does not match the trade underlying instrument")
|
|
279
|
+
super().save(*args, **kwargs)
|
|
280
|
+
|
|
281
|
+
@classmethod
|
|
282
|
+
def get_valid_and_approved_claims(cls, val_date: date_lib | None = None, account: Account | None = None):
|
|
283
|
+
claims = cls.objects.annotate(
|
|
284
|
+
date_considered=ExpressionWrapper(
|
|
285
|
+
Greatest("trade__transaction_date", "date") + 1, output_field=DateField()
|
|
286
|
+
)
|
|
287
|
+
).filter(Q(account__is_terminal_account=True) & Q(account__is_active=True) & Q(status=cls.Status.APPROVED))
|
|
288
|
+
if val_date:
|
|
289
|
+
claims = claims.filter(date_considered__lt=val_date)
|
|
290
|
+
if account:
|
|
291
|
+
claims = claims.filter(account__in=account.get_descendants(include_self=True))
|
|
292
|
+
return claims
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def nominal_value(self):
|
|
296
|
+
"""Returns the nominal value of a claim
|
|
297
|
+
|
|
298
|
+
Shares x Share Price
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Decimal -- Nominal Value
|
|
302
|
+
"""
|
|
303
|
+
return self.shares * self.product.share_price
|
|
304
|
+
|
|
305
|
+
@transition(
|
|
306
|
+
status,
|
|
307
|
+
[Status.PENDING, Status.AUTO_MATCHED],
|
|
308
|
+
Status.APPROVED,
|
|
309
|
+
permission=can_administrate_claim,
|
|
310
|
+
custom={
|
|
311
|
+
"_transition_button": ActionButton(
|
|
312
|
+
method=RequestType.PATCH,
|
|
313
|
+
identifiers=("wbportfolio:claim",),
|
|
314
|
+
icon=WBIcon.APPROVE.icon,
|
|
315
|
+
color=ButtonDefaultColor.SUCCESS,
|
|
316
|
+
key="approve",
|
|
317
|
+
label="Approve",
|
|
318
|
+
action_label="Approve",
|
|
319
|
+
description_fields="<p>Date: {{date}}</p><p>Shares: {{shares}}</p><p>Product: {{_product.name}} ({{_product.isin}})</p>",
|
|
320
|
+
instance_display=create_simple_display(
|
|
321
|
+
[
|
|
322
|
+
["date", "shares", "bank"],
|
|
323
|
+
["product", "claimant", "."],
|
|
324
|
+
["account", "account", "account"],
|
|
325
|
+
["trade", "trade", "trade"],
|
|
326
|
+
]
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
def approve(self, **kwargs):
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
def can_approve(self):
|
|
335
|
+
errors = dict()
|
|
336
|
+
|
|
337
|
+
if not self.trade:
|
|
338
|
+
errors["trade"] = ["With this status, this has to be provided."]
|
|
339
|
+
|
|
340
|
+
if not self.product:
|
|
341
|
+
errors["product"] = ["With this status, this has to be provided."]
|
|
342
|
+
|
|
343
|
+
if not self.account:
|
|
344
|
+
errors["account"] = ["With this status, this has to be provided."]
|
|
345
|
+
|
|
346
|
+
# check if the specified product have a valid nav at the specified date
|
|
347
|
+
if (
|
|
348
|
+
(product := self.product)
|
|
349
|
+
and (claim_date := self.date)
|
|
350
|
+
and not product.valuations.filter(date=claim_date).exists()
|
|
351
|
+
):
|
|
352
|
+
if (prices_qs := product.valuations.filter(date__lt=claim_date)).exists():
|
|
353
|
+
errors[
|
|
354
|
+
"date"
|
|
355
|
+
] = f"For product {product.name}, the latest valid valuation date before {claim_date:%Y-%m-%d} is {prices_qs.latest('date').date:%Y-%m-%d}: Please select a valid date."
|
|
356
|
+
else:
|
|
357
|
+
errors[
|
|
358
|
+
"date"
|
|
359
|
+
] = f"There is no valuation before {claim_date:%Y-%m-%d} for product {product.name}: Please select a valid date."
|
|
360
|
+
return errors
|
|
361
|
+
|
|
362
|
+
@transition(
|
|
363
|
+
status,
|
|
364
|
+
[Status.PENDING, Status.AUTO_MATCHED],
|
|
365
|
+
Status.DRAFT,
|
|
366
|
+
permission=lambda claim, user: user.profile.is_internal or user.is_superuser,
|
|
367
|
+
custom={
|
|
368
|
+
"_transition_button": ActionButton(
|
|
369
|
+
method=RequestType.PATCH,
|
|
370
|
+
color=ButtonDefaultColor.ERROR,
|
|
371
|
+
identifiers=("wbportfolio:claim",),
|
|
372
|
+
icon=WBIcon.UNDO.icon,
|
|
373
|
+
key="backtodraft",
|
|
374
|
+
label="Back to Draft",
|
|
375
|
+
action_label="Back to Draft",
|
|
376
|
+
description_fields="<p>Date: {{date}}</p><p>Shares: {{shares}}</p>",
|
|
377
|
+
)
|
|
378
|
+
},
|
|
379
|
+
)
|
|
380
|
+
def backtodraft(self, **kwargs):
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
@transition(
|
|
384
|
+
status,
|
|
385
|
+
[Status.DRAFT],
|
|
386
|
+
Status.WITHDRAWN,
|
|
387
|
+
permission=can_edit_claim,
|
|
388
|
+
custom={
|
|
389
|
+
"_transition_button": ActionButton(
|
|
390
|
+
method=RequestType.PATCH,
|
|
391
|
+
identifiers=("wbportfolio:claim",),
|
|
392
|
+
color=ButtonDefaultColor.ERROR,
|
|
393
|
+
icon=WBIcon.DELETE.icon,
|
|
394
|
+
key="withdraw",
|
|
395
|
+
label="Withdraw",
|
|
396
|
+
action_label="Withdraw",
|
|
397
|
+
description_fields="<p>Date: {{date}}</p><p>Shares: {{shares}}</p>",
|
|
398
|
+
)
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
def withdraw(self, **kwargs):
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
@transition(
|
|
405
|
+
status,
|
|
406
|
+
[Status.APPROVED],
|
|
407
|
+
Status.DRAFT,
|
|
408
|
+
permission=lambda claim, user: user.profile.is_internal or user.is_superuser,
|
|
409
|
+
custom={
|
|
410
|
+
"_transition_button": ActionButton(
|
|
411
|
+
method=RequestType.PATCH,
|
|
412
|
+
identifiers=("wbportfolio:claim",),
|
|
413
|
+
icon=WBIcon.EDIT.icon,
|
|
414
|
+
color=ButtonDefaultColor.WARNING,
|
|
415
|
+
key="revise",
|
|
416
|
+
label="Revise",
|
|
417
|
+
action_label="Revise",
|
|
418
|
+
description_fields="<p>Date: {{date}}</p><p>Product: {{_product.name}}</p>",
|
|
419
|
+
)
|
|
420
|
+
},
|
|
421
|
+
)
|
|
422
|
+
def revise(self, **kwargs):
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
@transition(
|
|
426
|
+
status,
|
|
427
|
+
[Status.DRAFT, Status.AUTO_MATCHED],
|
|
428
|
+
Status.PENDING,
|
|
429
|
+
permission=lambda claim, user: user.profile.is_internal or user.is_superuser,
|
|
430
|
+
custom={
|
|
431
|
+
"_transition_button": ActionButton(
|
|
432
|
+
method=RequestType.PATCH,
|
|
433
|
+
identifiers=("wbportfolio:claim",),
|
|
434
|
+
icon=WBIcon.SEND.icon,
|
|
435
|
+
color=ButtonDefaultColor.WARNING,
|
|
436
|
+
key="submit",
|
|
437
|
+
label="Submit for approval",
|
|
438
|
+
action_label="Submit",
|
|
439
|
+
description_fields="<p>Date: {{date}}</p><p>Product: {{_product.name}}</p>",
|
|
440
|
+
)
|
|
441
|
+
},
|
|
442
|
+
)
|
|
443
|
+
def submit(self, **kwargs):
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
def can_submit(self):
|
|
447
|
+
return self.can_approve()
|
|
448
|
+
|
|
449
|
+
def auto_match(self) -> Trade | None:
|
|
450
|
+
SHARES_EPSILON = 1 # share
|
|
451
|
+
auto_match_trade = None
|
|
452
|
+
# Obvious filtering
|
|
453
|
+
trades = Trade.valid_customer_trade_objects.filter(
|
|
454
|
+
Q(transaction_date__gte=self.date - timedelta(days=Trade.TRADE_WINDOW_INTERVAL))
|
|
455
|
+
& Q(transaction_date__lte=self.date + timedelta(days=Trade.TRADE_WINDOW_INTERVAL))
|
|
456
|
+
)
|
|
457
|
+
if self.product:
|
|
458
|
+
trades = trades.filter(underlying_instrument=self.product)
|
|
459
|
+
# Find trades by shares (or remaining to be claimed)
|
|
460
|
+
trades = trades.filter(
|
|
461
|
+
Q(diff_shares__lte=self.shares + SHARES_EPSILON) & Q(diff_shares__gte=self.shares - SHARES_EPSILON)
|
|
462
|
+
)
|
|
463
|
+
if trades.count() == 1:
|
|
464
|
+
auto_match_trade = trades.first()
|
|
465
|
+
|
|
466
|
+
# Find trades by custodian
|
|
467
|
+
if (
|
|
468
|
+
not auto_match_trade
|
|
469
|
+
and self.bank
|
|
470
|
+
and (custodian := Custodian.get_by_mapping(self.bank, use_similarity=True, create_missing=False))
|
|
471
|
+
):
|
|
472
|
+
if trades.filter(custodian=custodian).exists():
|
|
473
|
+
trades = trades.filter(custodian=custodian)
|
|
474
|
+
if trades.count() == 1:
|
|
475
|
+
auto_match_trade = trades.first()
|
|
476
|
+
|
|
477
|
+
# Find trades by external_id
|
|
478
|
+
if not auto_match_trade and self.external_id and trades.count() > 1:
|
|
479
|
+
trades = trades.filter(
|
|
480
|
+
Q(external_id__icontains=self.external_id) | Q(external_identifier2__icontains=self.external_id)
|
|
481
|
+
)
|
|
482
|
+
if trades.count() == 1:
|
|
483
|
+
auto_match_trade = trades.first()
|
|
484
|
+
|
|
485
|
+
if auto_match_trade:
|
|
486
|
+
self.status = self.Status.AUTO_MATCHED
|
|
487
|
+
return auto_match_trade
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def subquery_assets_under_management_for_account(cls, claims, price_date, account_key="account__pk"):
|
|
491
|
+
"""Returns a subquery which annotates the assets in USD for each Sub Account based on a given queryset
|
|
492
|
+
|
|
493
|
+
Arguments:
|
|
494
|
+
claims {QuerySet<commission.Claim>} -- Prefiltered queryset of claims
|
|
495
|
+
price_date {datetime.date} -- the date which is used to calculate everything
|
|
496
|
+
account_key {str} -- the outer reference to the sub account pk (default: account__pk)
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Subquery -- Sub Account with assets in USD
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
return models.Subquery(
|
|
503
|
+
claims.filter(
|
|
504
|
+
status=cls.Status.APPROVED,
|
|
505
|
+
account__pk=models.OuterRef(account_key),
|
|
506
|
+
trade__transaction_date__lte=price_date,
|
|
507
|
+
)
|
|
508
|
+
.annotate_asset_under_management_for_date(price_date)
|
|
509
|
+
.values("account")
|
|
510
|
+
.annotate(sum_assets_usd=models.Sum("assets_under_management_usd"))
|
|
511
|
+
.values("sum_assets_usd")[:1],
|
|
512
|
+
output_field=models.DecimalField(max_digits=16, decimal_places=4, default=0.0),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
@classmethod
|
|
516
|
+
def subquery_claim_sum_for_account(cls, claims, price_date, account_key="account__pk"):
|
|
517
|
+
return models.Subquery(
|
|
518
|
+
claims.filter(
|
|
519
|
+
status=cls.Status.APPROVED,
|
|
520
|
+
account__pk=models.OuterRef(account_key),
|
|
521
|
+
trade__transaction_date__lte=price_date,
|
|
522
|
+
)
|
|
523
|
+
.values("account")
|
|
524
|
+
.annotate(claim_sum=models.Sum("shares"))
|
|
525
|
+
.values("claim_sum")[:1],
|
|
526
|
+
output_field=models.DecimalField(max_digits=16, decimal_places=0, default=0.0),
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
@classmethod
|
|
530
|
+
def get_endpoint_basename(cls):
|
|
531
|
+
return "wbportfolio:claim"
|
|
532
|
+
|
|
533
|
+
@classmethod
|
|
534
|
+
def get_representation_endpoint(cls):
|
|
535
|
+
return "wbportfolio:claimrepresentation-list"
|
|
536
|
+
|
|
537
|
+
@classmethod
|
|
538
|
+
def get_representation_value_key(cls):
|
|
539
|
+
return "id"
|
|
540
|
+
|
|
541
|
+
@classmethod
|
|
542
|
+
def get_representation_label_key(cls):
|
|
543
|
+
return "{{shares}} ({{account}})"
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@receiver(models.signals.post_save, sender="wbportfolio.Trade")
|
|
547
|
+
def post_save_trade(sender, instance, created, raw, **kwargs):
|
|
548
|
+
"""
|
|
549
|
+
For every claimable trade, first try to autoclaim the already existing draft claims with this new trade. And auto claim them as well if the product has a default sub account
|
|
550
|
+
"""
|
|
551
|
+
if not raw and instance.is_claimable and created:
|
|
552
|
+
min_transaction_date = instance.transaction_date - timedelta(days=Trade.TRADE_WINDOW_INTERVAL)
|
|
553
|
+
max_transaction_date = instance.transaction_date + timedelta(days=Trade.TRADE_WINDOW_INTERVAL)
|
|
554
|
+
unlinked_claims = Claim.objects.filter(
|
|
555
|
+
status=Claim.Status.DRAFT,
|
|
556
|
+
date__gte=min_transaction_date,
|
|
557
|
+
date__lte=max_transaction_date,
|
|
558
|
+
trade__isnull=True,
|
|
559
|
+
product__id=instance.underlying_instrument.id,
|
|
560
|
+
)
|
|
561
|
+
for claim in unlinked_claims.all():
|
|
562
|
+
claim.trade = claim.auto_match()
|
|
563
|
+
claim.save()
|
|
564
|
+
if instance.product and (account := instance.product.default_account):
|
|
565
|
+
shares = instance.shares or Decimal(0)
|
|
566
|
+
claimed_shares = instance.claims.filter(status=Claim.Status.APPROVED).aggregate(s=models.Sum("shares"))[
|
|
567
|
+
"s"
|
|
568
|
+
] or Decimal(0)
|
|
569
|
+
rest_shares = shares - claimed_shares
|
|
570
|
+
if rest_shares != 0:
|
|
571
|
+
Claim.objects.create(
|
|
572
|
+
trade=instance,
|
|
573
|
+
shares=rest_shares,
|
|
574
|
+
bank=instance.bank,
|
|
575
|
+
date=instance.transaction_date,
|
|
576
|
+
product=instance.product,
|
|
577
|
+
account=account,
|
|
578
|
+
claimant=account.owner,
|
|
579
|
+
)
|
|
580
|
+
for claim in instance.claims.exclude(status=Claim.Status.APPROVED):
|
|
581
|
+
if claim.status == Claim.Status.DRAFT:
|
|
582
|
+
claim.submit()
|
|
583
|
+
if claim.status == Claim.Status.PENDING:
|
|
584
|
+
claim.approve()
|
|
585
|
+
claim.save()
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@receiver(pre_collection, sender="wbportfolio.Trade")
|
|
589
|
+
def pre_trade_deletion(sender, instance, **kwargs):
|
|
590
|
+
# We can't use pre_delete signal here because the collector is collected before the signal and thus, it will
|
|
591
|
+
for claim in instance.claims.filter(
|
|
592
|
+
status__in=[Claim.Status.DRAFT, Claim.Status.AUTO_MATCHED, Claim.Status.WITHDRAWN]
|
|
593
|
+
).all():
|
|
594
|
+
claim.trade = None
|
|
595
|
+
claim.save(auto_match=False)
|
|
596
|
+
|
|
597
|
+
if (instance.marked_for_deletion or instance.pending) and instance.claims.exists():
|
|
598
|
+
# If a default account exists on the trade's product, it means that the product's trade are automatically claims. In that case, we are sure that a valid trade still exists and the marked for deletion's claim can be safely remove
|
|
599
|
+
if instance.product and instance.product.default_account:
|
|
600
|
+
instance.claims.all().delete()
|
|
601
|
+
else:
|
|
602
|
+
similar_trades = Trade.objects.filter(
|
|
603
|
+
underlying_instrument=instance.underlying_instrument,
|
|
604
|
+
portfolio=instance.portfolio,
|
|
605
|
+
marked_for_deletion=True,
|
|
606
|
+
transaction_date=instance.transaction_date,
|
|
607
|
+
)
|
|
608
|
+
# We check if the sum of marked for deletion trades share sums to 0, in that case, we delete them without regards and set their potential claims to draft
|
|
609
|
+
if similar_trades.exists() and similar_trades.aggregate(s=Sum("shares"))["s"] == 0:
|
|
610
|
+
for t in similar_trades.all():
|
|
611
|
+
t.claims.update(trade=None, status=Claim.Status.DRAFT)
|
|
612
|
+
elif (other_unclaims_similar_trades := instance.get_alternative_valid_trades()).exists():
|
|
613
|
+
if other_unclaims_similar_trades.count() > 1:
|
|
614
|
+
other_unclaims_similar_trades = other_unclaims_similar_trades.filter(bank=instance.bank)
|
|
615
|
+
if other_trade := other_unclaims_similar_trades.first():
|
|
616
|
+
# If we find an unclaim trade with similar attributes, we forward the marked_for_deletion attribute to it, which will be handled/deleted in a next delete iteration
|
|
617
|
+
instance.claims.update(trade=other_trade)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@receiver(pre_merge, sender="wbcrm.Account")
|
|
621
|
+
def handle_pre_merge_account_for_claim(
|
|
622
|
+
sender: models.Model, merged_object: "Account", main_object: "Account", **kwargs
|
|
623
|
+
):
|
|
624
|
+
"""
|
|
625
|
+
Simply reassign the claim linked to the merged account to the main account
|
|
626
|
+
"""
|
|
627
|
+
Claim.objects.filter(account=merged_object).update(account=main_object, reference=merged_object.reference_id)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@receiver(add_llm_prompt, sender="wbcrm.Account")
|
|
631
|
+
def add_holdings_to_account_heat(sender, instance, key, **kwargs):
|
|
632
|
+
if key == "analyze_relationship":
|
|
633
|
+
return get_performances_prompt(instance)
|
|
634
|
+
return []
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from wbportfolio.import_export.handlers.dividend import DividendImportHandler
|
|
3
|
+
|
|
4
|
+
from .transactions import ShareMixin, Transaction
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DividendTransaction(Transaction, ShareMixin, models.Model):
|
|
8
|
+
import_export_handler_class = DividendImportHandler
|
|
9
|
+
retrocession = models.FloatField(default=1)
|
|
10
|
+
|
|
11
|
+
def save(self, *args, **kwargs):
|
|
12
|
+
if (
|
|
13
|
+
self.shares is not None
|
|
14
|
+
and self.price is not None
|
|
15
|
+
and self.retrocession is not None
|
|
16
|
+
and self.total_value is None
|
|
17
|
+
):
|
|
18
|
+
self.total_value = self.shares * self.price * self.retrocession
|
|
19
|
+
|
|
20
|
+
if self.price is not None and self.price_gross is None:
|
|
21
|
+
self.price_gross = self.price
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
self.price_gross is not None
|
|
25
|
+
and self.retrocession is not None
|
|
26
|
+
and self.shares is not None
|
|
27
|
+
and self.total_value_gross is None
|
|
28
|
+
):
|
|
29
|
+
self.total_value_gross = self.shares * self.price_gross * self.retrocession
|
|
30
|
+
|
|
31
|
+
super().save(*args, **kwargs)
|