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,153 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
|
|
4
|
+
from celery import shared_task
|
|
5
|
+
from django.db import models
|
|
6
|
+
from django.db.models import Exists, OuterRef, Q, QuerySet
|
|
7
|
+
from django.dispatch import receiver
|
|
8
|
+
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
9
|
+
from wbportfolio.import_export.handlers.fees import FeesImportHandler
|
|
10
|
+
from wbportfolio.models.products import Product
|
|
11
|
+
|
|
12
|
+
from .transactions import Transaction
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ValidFeesQueryset(QuerySet):
|
|
16
|
+
def filter_only_valid_fees(self) -> QuerySet:
|
|
17
|
+
"""
|
|
18
|
+
Filter the queryset to remove duplicate in case calculated and non-calculated fees are present for the same date/product/type
|
|
19
|
+
"""
|
|
20
|
+
return self.annotate(
|
|
21
|
+
real_fees_exists=Exists(
|
|
22
|
+
self.filter(
|
|
23
|
+
transaction_subtype=OuterRef("transaction_subtype"),
|
|
24
|
+
linked_product=OuterRef("linked_product"),
|
|
25
|
+
fee_date=OuterRef("fee_date"),
|
|
26
|
+
calculated=False,
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
).filter(Q(calculated=False) | (Q(real_fees_exists=False) & Q(calculated=True)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DefaultFeesManager(models.Manager):
|
|
33
|
+
def get_queryset(self) -> ValidFeesQueryset:
|
|
34
|
+
return ValidFeesQueryset(self.model)
|
|
35
|
+
|
|
36
|
+
def filter_only_valid_fees(self) -> QuerySet:
|
|
37
|
+
return self.get_queryset().filter_only_valid_fees()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ValidFeesManager(DefaultFeesManager):
|
|
41
|
+
def get_queryset(self) -> QuerySet:
|
|
42
|
+
return super().get_queryset().filter_only_valid_fees()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Fees(Transaction):
|
|
46
|
+
import_export_handler_class = FeesImportHandler
|
|
47
|
+
|
|
48
|
+
class Type(models.TextChoices):
|
|
49
|
+
TRANSACTION = "TRANSACTION", "Transaction"
|
|
50
|
+
PERFORMANCE_CRYSTALIZED = "PERFORMANCE_CRYSTALIZED", "Performance Crystalized"
|
|
51
|
+
PERFORMANCE = "PERFORMANCE", "Performance"
|
|
52
|
+
MANAGEMENT = "MANAGEMENT", "Management"
|
|
53
|
+
ISSUER = "ISSUER", "Issuer"
|
|
54
|
+
OTHER = "OTHER", "Other"
|
|
55
|
+
|
|
56
|
+
transaction_subtype = models.CharField(
|
|
57
|
+
max_length=255, verbose_name="Fees Type", choices=Type.choices, default=Type.MANAGEMENT
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
fee_date = models.DateField() # needed for indexing
|
|
61
|
+
calculated = models.BooleanField(
|
|
62
|
+
default=True,
|
|
63
|
+
help_text="A marker whether the fees were calculated or supplied.",
|
|
64
|
+
verbose_name="Is calculated",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
linked_product = models.ForeignKey(
|
|
68
|
+
"wbportfolio.Product",
|
|
69
|
+
related_name="transactionfees",
|
|
70
|
+
on_delete=models.PROTECT,
|
|
71
|
+
verbose_name="Product",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
class Meta:
|
|
75
|
+
verbose_name = "Fees"
|
|
76
|
+
verbose_name_plural = "Fees"
|
|
77
|
+
indexes = [
|
|
78
|
+
models.Index(fields=["linked_product"]),
|
|
79
|
+
models.Index(fields=["transaction_subtype", "linked_product", "fee_date", "calculated"]),
|
|
80
|
+
]
|
|
81
|
+
constraints = [
|
|
82
|
+
models.UniqueConstraint(
|
|
83
|
+
fields=["linked_product", "fee_date", "transaction_subtype", "calculated"], name="unique_fees"
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
objects = DefaultFeesManager()
|
|
88
|
+
valid_objects = ValidFeesManager()
|
|
89
|
+
|
|
90
|
+
def save(self, *args, **kwargs):
|
|
91
|
+
self.fee_date = self.transaction_date
|
|
92
|
+
super().save(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
def __str__(self):
|
|
95
|
+
return f"{self.transaction_date:%d.%m.%Y} - {self.Type[self.transaction_subtype]}: {self.portfolio.name}"
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def get_endpoint_basename(cls):
|
|
99
|
+
return "wbportfolio:fees"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FeeCalculation(models.Model):
|
|
103
|
+
name = models.CharField(max_length=128, verbose_name="Name")
|
|
104
|
+
import_path = models.CharField(max_length=512, verbose_name="Import Path", default="restbench.fees.default")
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def compute_fee_from_price(cls, price):
|
|
108
|
+
product = Product.objects.get(id=price.instrument.id)
|
|
109
|
+
if (fee_calculation := product.fee_calculation) and (import_path := fee_calculation.import_path):
|
|
110
|
+
calculation_module = importlib.import_module(import_path)
|
|
111
|
+
for new_fees in calculation_module.fees_calculation(price.id):
|
|
112
|
+
Fees.objects.update_or_create(
|
|
113
|
+
linked_product=new_fees.pop("linked_product"),
|
|
114
|
+
transaction_date=new_fees.pop("transaction_date"),
|
|
115
|
+
transaction_subtype=new_fees.pop("transaction_subtype"),
|
|
116
|
+
calculated=True,
|
|
117
|
+
defaults=new_fees,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def __str__(self) -> str:
|
|
121
|
+
return self.name
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@shared_task
|
|
125
|
+
def compute_fee_from_price_as_task(price_id):
|
|
126
|
+
price = InstrumentPrice.objects.get(id=price_id)
|
|
127
|
+
FeeCalculation.compute_fee_from_price(price)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@receiver(models.signals.post_save, sender="wbfdm.InstrumentPrice")
|
|
131
|
+
def update_or_create_fees_post(sender, instance, created, raw, **kwargs):
|
|
132
|
+
"""Gets or create the fees for a given price and updates them if necessary"""
|
|
133
|
+
if not raw and created and not instance.calculated and instance.instrument:
|
|
134
|
+
with suppress(Product.DoesNotExist):
|
|
135
|
+
product = Product.objects.get(id=instance.instrument.id)
|
|
136
|
+
if product.fee_calculation:
|
|
137
|
+
compute_fee_from_price_as_task.delay(instance.id)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# @receiver(models.signals.pre_save, sender="wbportfolio.Fees")
|
|
141
|
+
# def check_uniqueness(sender, instance, raw, **kwargs):
|
|
142
|
+
# if (
|
|
143
|
+
# Fees.objects.exclude(id=instance.id)
|
|
144
|
+
# .filter(
|
|
145
|
+
# transaction_date=instance.transaction_date,
|
|
146
|
+
# transaction_subtype=instance.transaction_subtype,
|
|
147
|
+
# linked_product=instance.linked_product,
|
|
148
|
+
# )
|
|
149
|
+
# .exists()
|
|
150
|
+
# ):
|
|
151
|
+
# raise ValueError(
|
|
152
|
+
# f"A fees object already exists with date, type and product = {instance.transaction_date}, {instance.transaction_subtype}, {instance.linked_product}"
|
|
153
|
+
# )
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from celery import shared_task
|
|
7
|
+
from django.core.exceptions import ValidationError
|
|
8
|
+
from django.db import models
|
|
9
|
+
from django.db.models.signals import post_save
|
|
10
|
+
from django.dispatch import receiver
|
|
11
|
+
from django.utils import timezone
|
|
12
|
+
from django.utils.functional import cached_property
|
|
13
|
+
from django_fsm import FSMField, transition
|
|
14
|
+
from pandas.tseries.offsets import BDay
|
|
15
|
+
from wbcompliance.models.risk_management.mixins import RiskCheckMixin
|
|
16
|
+
from wbcore.contrib.icons import WBIcon
|
|
17
|
+
from wbcore.enums import RequestType
|
|
18
|
+
from wbcore.metadata.configs.buttons import ActionButton
|
|
19
|
+
from wbcore.models import WBModel
|
|
20
|
+
from wbfdm.models.instruments.instruments import Instrument
|
|
21
|
+
from wbportfolio.models.roles import PortfolioRole
|
|
22
|
+
from wbportfolio.pms.trading import TradingService
|
|
23
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
24
|
+
from wbportfolio.pms.typing import TradeBatch as TradeBatchDTO
|
|
25
|
+
|
|
26
|
+
from .trades import Trade
|
|
27
|
+
|
|
28
|
+
SelfTradeProposal = TypeVar("SelfTradeProposal", bound="TradeProposal")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TradeProposal(RiskCheckMixin, WBModel):
|
|
32
|
+
trade_date = models.DateField(verbose_name="Trading Date")
|
|
33
|
+
|
|
34
|
+
class Status(models.TextChoices):
|
|
35
|
+
DRAFT = "DRAFT", "Draft"
|
|
36
|
+
SUBMIT = "SUBMIT", "Submit"
|
|
37
|
+
APPROVED = "APPROVED", "Approved"
|
|
38
|
+
DENIED = "DENIED", "Denied"
|
|
39
|
+
|
|
40
|
+
comment = models.TextField(default="", verbose_name="Trade Comment", blank=True)
|
|
41
|
+
status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name="Status")
|
|
42
|
+
model_portfolio = models.ForeignKey(
|
|
43
|
+
"wbportfolio.Portfolio",
|
|
44
|
+
blank=True,
|
|
45
|
+
null=True,
|
|
46
|
+
related_name="model_trade_proposals",
|
|
47
|
+
on_delete=models.PROTECT,
|
|
48
|
+
verbose_name="Model Portfolio",
|
|
49
|
+
)
|
|
50
|
+
portfolio = models.ForeignKey(
|
|
51
|
+
"wbportfolio.Portfolio", related_name="trade_proposals", on_delete=models.PROTECT, verbose_name="Portfolio"
|
|
52
|
+
)
|
|
53
|
+
creator = models.ForeignKey(
|
|
54
|
+
"directory.Person",
|
|
55
|
+
blank=True,
|
|
56
|
+
null=True,
|
|
57
|
+
related_name="trade_proposals",
|
|
58
|
+
on_delete=models.PROTECT,
|
|
59
|
+
verbose_name="Owner",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _get_checked_object_field_name(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Mandatory function from the Riskcheck mixin that returns the field (aka portfolio), representing the object to check the rules against.
|
|
65
|
+
"""
|
|
66
|
+
return "portfolio"
|
|
67
|
+
|
|
68
|
+
@cached_property
|
|
69
|
+
def validated_trading_service(self) -> TradingService:
|
|
70
|
+
"""
|
|
71
|
+
This property holds the validated trading services and cache it.This property expect to be set only if is_valid return True
|
|
72
|
+
"""
|
|
73
|
+
return TradingService(
|
|
74
|
+
self.trade_date,
|
|
75
|
+
effective_portfolio=self.portfolio._build_dto(self.trade_date),
|
|
76
|
+
trades_batch=self._build_dto(),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def previous_trade_proposal(self) -> SelfTradeProposal | None:
|
|
81
|
+
future_proposals = TradeProposal.objects.filter(portfolio=self.portfolio).filter(
|
|
82
|
+
trade_date__lt=self.trade_date, status=TradeProposal.Status.APPROVED
|
|
83
|
+
)
|
|
84
|
+
if future_proposals.exists():
|
|
85
|
+
return future_proposals.latest("trade_date")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def next_trade_proposal(self) -> SelfTradeProposal | None:
|
|
90
|
+
future_proposals = TradeProposal.objects.filter(portfolio=self.portfolio).filter(
|
|
91
|
+
trade_date__gt=self.trade_date, status=TradeProposal.Status.APPROVED
|
|
92
|
+
)
|
|
93
|
+
if future_proposals.exists():
|
|
94
|
+
return future_proposals.earliest("trade_date")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
@cached_property
|
|
98
|
+
def base_assets(self):
|
|
99
|
+
"""
|
|
100
|
+
Return a dictionary representation (instrument_id: target weight) of this trade proposal
|
|
101
|
+
Returns:
|
|
102
|
+
A dictionary representation
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
return {
|
|
106
|
+
v["underlying_instrument"]: v["target_weight"]
|
|
107
|
+
for v in self.trades.filter(status=Trade.Status.EXECUTED).values("underlying_instrument", "target_weight")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def __str__(self) -> str:
|
|
111
|
+
return f"{self.portfolio.name}: {self.trade_date} ({self.status})"
|
|
112
|
+
|
|
113
|
+
def save(self, *args, **kwargs):
|
|
114
|
+
if not self.model_portfolio:
|
|
115
|
+
self.model_portfolio = self.portfolio
|
|
116
|
+
super().save(*args, **kwargs)
|
|
117
|
+
|
|
118
|
+
def _build_dto(self) -> TradeBatchDTO:
|
|
119
|
+
"""
|
|
120
|
+
Data Transfer Object
|
|
121
|
+
Returns:
|
|
122
|
+
DTO trade object
|
|
123
|
+
"""
|
|
124
|
+
return (
|
|
125
|
+
TradeBatchDTO(tuple([trade._build_dto() for trade in self.trades.all()])) if self.trades.exists() else None
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Start tools methods
|
|
129
|
+
def clone(self, **kwargs) -> SelfTradeProposal:
|
|
130
|
+
"""
|
|
131
|
+
Method to clone self as a new trade proposal. It will automatically shift the trade date if a proposal already exists
|
|
132
|
+
Args:
|
|
133
|
+
**kwargs: The keyword arguments
|
|
134
|
+
Returns:
|
|
135
|
+
The cloned trade proposal
|
|
136
|
+
"""
|
|
137
|
+
trade_date = kwargs.get("trade_date", self.trade_date)
|
|
138
|
+
|
|
139
|
+
# Find the next valid trade date
|
|
140
|
+
while TradeProposal.objects.filter(portfolio=self.portfolio, trade_date=trade_date).exists():
|
|
141
|
+
trade_date += timedelta(days=1)
|
|
142
|
+
|
|
143
|
+
trade_proposal_clone = TradeProposal.objects.create(
|
|
144
|
+
trade_date=trade_date,
|
|
145
|
+
comment=kwargs.get("comment", self.comment),
|
|
146
|
+
status=TradeProposal.Status.DRAFT,
|
|
147
|
+
model_portfolio=self.model_portfolio,
|
|
148
|
+
portfolio=self.portfolio,
|
|
149
|
+
creator=self.creator,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# For all existing trades, copy them to the new trade proposal
|
|
153
|
+
for trade in self.trades.all():
|
|
154
|
+
trade.pk = None
|
|
155
|
+
trade.trade_proposal = trade_proposal_clone
|
|
156
|
+
trade.transaction_date = trade_proposal_clone.trade_date
|
|
157
|
+
trade.save()
|
|
158
|
+
return trade_proposal_clone
|
|
159
|
+
|
|
160
|
+
def normalize_trades(self):
|
|
161
|
+
"""
|
|
162
|
+
Call the trading service with the existing trades and normalize them in order to obtain a total sum target weight of 100%
|
|
163
|
+
The existing trade will be modified directly with the given normalization factor
|
|
164
|
+
"""
|
|
165
|
+
service = TradingService(self.trade_date, trades_batch=self._build_dto())
|
|
166
|
+
service.normalize()
|
|
167
|
+
leftovers_trades = self.trades.all()
|
|
168
|
+
for _, trade in service.trades_batch.trades_map.items():
|
|
169
|
+
with suppress(Trade.DoesNotExist):
|
|
170
|
+
self.trades.update_or_create(
|
|
171
|
+
id=trade.id,
|
|
172
|
+
defaults={
|
|
173
|
+
"weighting": trade.delta_weight,
|
|
174
|
+
"shares": trade.target_shares,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
leftovers_trades = leftovers_trades.exclude(id=trade.id)
|
|
178
|
+
leftovers_trades.delete()
|
|
179
|
+
|
|
180
|
+
def reset_trades(self):
|
|
181
|
+
"""
|
|
182
|
+
Will delete all existing trades and recreate them from the method `create_or_update_trades`
|
|
183
|
+
"""
|
|
184
|
+
# delete all existing trades
|
|
185
|
+
self.trades.all().delete()
|
|
186
|
+
# recreate them from scratch (if the portfolio has positions)
|
|
187
|
+
self.create_or_update_trades()
|
|
188
|
+
|
|
189
|
+
def apply_trades(self):
|
|
190
|
+
# We validate trade which will create or update the initial asset positions
|
|
191
|
+
self.trades.exclude(status=Trade.Status.SUBMIT).update(status=Trade.Status.SUBMIT)
|
|
192
|
+
for trade in self.trades.all():
|
|
193
|
+
trade.execute()
|
|
194
|
+
trade.save()
|
|
195
|
+
# We propagate the new portfolio composition until the next trade proposal or today if it doesn't exist yet
|
|
196
|
+
to_date = self.next_trade_proposal.trade_date if self.next_trade_proposal else timezone.now().date()
|
|
197
|
+
|
|
198
|
+
for from_date in pd.date_range(self.trade_date, to_date - timedelta(days=1), freq="B"):
|
|
199
|
+
to_date = (from_date + BDay(1)).date()
|
|
200
|
+
self.portfolio.propagate_or_update_assets(
|
|
201
|
+
from_date.date(),
|
|
202
|
+
to_date,
|
|
203
|
+
forward_price=False,
|
|
204
|
+
base_assets=self.base_assets,
|
|
205
|
+
delete_existing_assets=True,
|
|
206
|
+
)
|
|
207
|
+
self.portfolio.change_at_date(to_date, base_assets=self.base_assets, force_recompute_weighting=True)
|
|
208
|
+
|
|
209
|
+
def revert_trades(self):
|
|
210
|
+
self.trades.exclude(status=Trade.Status.EXECUTED).update(status=Trade.Status.EXECUTED)
|
|
211
|
+
for trade in self.trades.all():
|
|
212
|
+
trade.revert()
|
|
213
|
+
trade.save()
|
|
214
|
+
if previous_trade_proposal := self.previous_trade_proposal:
|
|
215
|
+
previous_trade_proposal.apply_trades()
|
|
216
|
+
|
|
217
|
+
def create_or_update_trades(
|
|
218
|
+
self, target_portfolio: PortfolioDTO = None, effective_portfolio: PortfolioDTO = None, reset: bool = False
|
|
219
|
+
):
|
|
220
|
+
"""
|
|
221
|
+
This function talk to the trading service layer in order to generate a list of valid trades to attach to the proposal
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
target_portfolio: The target portfolio that the trades needs to execute to. Absence of position means a sell
|
|
225
|
+
effective_portfolio: The current or effective portfolio to derivative effective weight from. Absence of position means a buy
|
|
226
|
+
reset: If true, delete the current attached trades
|
|
227
|
+
"""
|
|
228
|
+
# if the target portfolio is not provided, we try to build it
|
|
229
|
+
if (
|
|
230
|
+
not target_portfolio
|
|
231
|
+
and (assets := self.model_portfolio.assets.filter(date__lte=self.trade_date)).exists()
|
|
232
|
+
and (latest_pos := assets.latest("date"))
|
|
233
|
+
):
|
|
234
|
+
target_portfolio = self.model_portfolio._build_dto(latest_pos.date)
|
|
235
|
+
|
|
236
|
+
# if the effective portfolio is not provided, we try to build it
|
|
237
|
+
if (
|
|
238
|
+
not effective_portfolio
|
|
239
|
+
and (assets := self.portfolio.assets.filter(date__lte=self.trade_date)).exists()
|
|
240
|
+
and (latest_pos := assets.latest("date"))
|
|
241
|
+
):
|
|
242
|
+
effective_portfolio = self.portfolio._build_dto(latest_pos.date)
|
|
243
|
+
# Build trades DTO from the attached trades
|
|
244
|
+
trade_batch = self._build_dto()
|
|
245
|
+
if target_portfolio or effective_portfolio or trade_batch:
|
|
246
|
+
service = TradingService(
|
|
247
|
+
self.trade_date,
|
|
248
|
+
effective_portfolio=effective_portfolio,
|
|
249
|
+
target_portfolio=target_portfolio,
|
|
250
|
+
trades_batch=trade_batch,
|
|
251
|
+
)
|
|
252
|
+
# with suppress(ValidationError):
|
|
253
|
+
# Normalize the trades and validate it
|
|
254
|
+
service.normalize()
|
|
255
|
+
service.is_valid()
|
|
256
|
+
if reset:
|
|
257
|
+
self.trades.all().delete()
|
|
258
|
+
for trade_dto in service.validated_trades:
|
|
259
|
+
instrument = Instrument.objects.get(id=trade_dto.underlying_instrument)
|
|
260
|
+
t, c = Trade.objects.update_or_create(
|
|
261
|
+
underlying_instrument=instrument,
|
|
262
|
+
currency=instrument.currency,
|
|
263
|
+
transaction_date=self.trade_date,
|
|
264
|
+
trade_proposal=self,
|
|
265
|
+
portfolio=self.portfolio,
|
|
266
|
+
defaults={
|
|
267
|
+
"shares": trade_dto.target_shares,
|
|
268
|
+
"weighting": trade_dto.delta_weight,
|
|
269
|
+
"status": Trade.Status.DRAFT,
|
|
270
|
+
"currency_fx_rate": instrument.currency.convert(self.trade_date, self.portfolio.currency),
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# End tools methods
|
|
275
|
+
|
|
276
|
+
# Start FSM logics
|
|
277
|
+
|
|
278
|
+
@transition(
|
|
279
|
+
field=status,
|
|
280
|
+
source=Status.DRAFT,
|
|
281
|
+
target=Status.SUBMIT,
|
|
282
|
+
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
283
|
+
user.profile, portfolio=instance.portfolio
|
|
284
|
+
),
|
|
285
|
+
custom={
|
|
286
|
+
"_transition_button": ActionButton(
|
|
287
|
+
method=RequestType.PATCH,
|
|
288
|
+
identifiers=("wbportfolio:tradeproposal",),
|
|
289
|
+
icon=WBIcon.SEND.icon,
|
|
290
|
+
key="submit",
|
|
291
|
+
label="Submit",
|
|
292
|
+
action_label="Submit",
|
|
293
|
+
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
294
|
+
)
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
def submit(self, by=None, description=None, **kwargs):
|
|
298
|
+
self.trades.update(status=Trade.Status.SUBMIT)
|
|
299
|
+
self.evaluate_active_rules(
|
|
300
|
+
self.trade_date, self.validated_trading_service.target_portfolio, asynchronously=True
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def can_submit(self):
|
|
304
|
+
errors = dict()
|
|
305
|
+
errors_list = []
|
|
306
|
+
if self.trades.exists() and self.trades.exclude(status=Trade.Status.DRAFT).exists():
|
|
307
|
+
errors_list.append("All trades need to be draft before submitting")
|
|
308
|
+
service = self.validated_trading_service
|
|
309
|
+
try:
|
|
310
|
+
service.is_valid(ignore_error=True)
|
|
311
|
+
# if service.trades_batch.totat_abs_delta_weight == 0:
|
|
312
|
+
# errors_list.append(
|
|
313
|
+
# "There is no change detected in this trade proposal. Please submit at last one valid trade"
|
|
314
|
+
# )
|
|
315
|
+
if len(service.validated_trades) == 0:
|
|
316
|
+
errors_list.append("There is no valid trade on this proposal")
|
|
317
|
+
if service.errors:
|
|
318
|
+
errors_list.extend(service.errors)
|
|
319
|
+
if errors_list:
|
|
320
|
+
errors["non_field_errors"] = errors_list
|
|
321
|
+
except ValidationError:
|
|
322
|
+
errors["non_field_errors"] = service.errors
|
|
323
|
+
with suppress(KeyError):
|
|
324
|
+
del self.__dict__["validated_trading_service"]
|
|
325
|
+
return errors
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def can_be_approved_or_denied(self):
|
|
329
|
+
return self.has_no_rule_or_all_checked_succeed and self.portfolio.is_manageable
|
|
330
|
+
|
|
331
|
+
@transition(
|
|
332
|
+
field=status,
|
|
333
|
+
source=Status.SUBMIT,
|
|
334
|
+
target=Status.APPROVED,
|
|
335
|
+
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
336
|
+
user.profile, portfolio=instance.portfolio
|
|
337
|
+
)
|
|
338
|
+
and instance.can_be_approved_or_denied,
|
|
339
|
+
custom={
|
|
340
|
+
"_transition_button": ActionButton(
|
|
341
|
+
method=RequestType.PATCH,
|
|
342
|
+
identifiers=("wbportfolio:tradeproposal",),
|
|
343
|
+
icon=WBIcon.APPROVE.icon,
|
|
344
|
+
key="approve",
|
|
345
|
+
label="Approve",
|
|
346
|
+
action_label="Approve",
|
|
347
|
+
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
348
|
+
)
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
def approve(self, by=None, description=None, **kwargs):
|
|
352
|
+
apply_trades_proposal_as_task.delay(self.id)
|
|
353
|
+
|
|
354
|
+
def can_approve(self):
|
|
355
|
+
errors = dict()
|
|
356
|
+
if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
|
|
357
|
+
errors["non_field_errors"] = "At least one trade needs to be submitted to be able to approve this proposal"
|
|
358
|
+
if not self.portfolio.is_manageable:
|
|
359
|
+
errors[
|
|
360
|
+
"portfolio"
|
|
361
|
+
] = "The portfolio needs to be a model portfolio in order to approve this trade proposal manually"
|
|
362
|
+
if self.has_assigned_active_rules and not self.has_all_check_completed_and_succeed:
|
|
363
|
+
errors["non_field_errors"] = "The pre trades rules did not passed successfully"
|
|
364
|
+
return errors
|
|
365
|
+
|
|
366
|
+
@transition(
|
|
367
|
+
field=status,
|
|
368
|
+
source=Status.SUBMIT,
|
|
369
|
+
target=Status.DENIED,
|
|
370
|
+
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
371
|
+
user.profile, portfolio=instance.portfolio
|
|
372
|
+
)
|
|
373
|
+
and instance.can_be_approved_or_denied,
|
|
374
|
+
custom={
|
|
375
|
+
"_transition_button": ActionButton(
|
|
376
|
+
method=RequestType.PATCH,
|
|
377
|
+
identifiers=("wbportfolio:tradeproposal",),
|
|
378
|
+
icon=WBIcon.DENY.icon,
|
|
379
|
+
key="deny",
|
|
380
|
+
label="Deny",
|
|
381
|
+
action_label="Deny",
|
|
382
|
+
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
383
|
+
)
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
def deny(self, by=None, description=None, **kwargs):
|
|
387
|
+
self.trades.all().delete()
|
|
388
|
+
with suppress(KeyError):
|
|
389
|
+
del self.__dict__["validated_trading_service"]
|
|
390
|
+
|
|
391
|
+
def can_deny(self):
|
|
392
|
+
errors = dict()
|
|
393
|
+
if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
|
|
394
|
+
errors["non_field_errors"] = "At least one trade needs to be submitted to be able to deny this proposal"
|
|
395
|
+
return errors
|
|
396
|
+
|
|
397
|
+
@transition(
|
|
398
|
+
field=status,
|
|
399
|
+
source=Status.SUBMIT,
|
|
400
|
+
target=Status.DRAFT,
|
|
401
|
+
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
402
|
+
user.profile, portfolio=instance.portfolio
|
|
403
|
+
)
|
|
404
|
+
and instance.has_all_check_completed, # we wait for all checks to succeed before proposing the back to draft transition
|
|
405
|
+
custom={
|
|
406
|
+
"_transition_button": ActionButton(
|
|
407
|
+
method=RequestType.PATCH,
|
|
408
|
+
identifiers=("wbportfolio:tradeproposal",),
|
|
409
|
+
icon=WBIcon.UNDO.icon,
|
|
410
|
+
key="backtodraft",
|
|
411
|
+
label="Back to Draft",
|
|
412
|
+
action_label="backtodraft",
|
|
413
|
+
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
414
|
+
)
|
|
415
|
+
},
|
|
416
|
+
)
|
|
417
|
+
def backtodraft(self, **kwargs):
|
|
418
|
+
with suppress(KeyError):
|
|
419
|
+
del self.__dict__["validated_trading_service"]
|
|
420
|
+
self.trades.update(status=Trade.Status.DRAFT)
|
|
421
|
+
self.checks.delete()
|
|
422
|
+
|
|
423
|
+
def can_backtodraft(self):
|
|
424
|
+
errors = dict()
|
|
425
|
+
if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
|
|
426
|
+
errors["non_field_errors"] = "All trades need to be submitted before reverting back to draft"
|
|
427
|
+
return errors
|
|
428
|
+
|
|
429
|
+
@transition(
|
|
430
|
+
field=status,
|
|
431
|
+
source=Status.APPROVED,
|
|
432
|
+
target=Status.DRAFT,
|
|
433
|
+
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
434
|
+
user.profile, portfolio=instance.portfolio
|
|
435
|
+
),
|
|
436
|
+
custom={
|
|
437
|
+
"_transition_button": ActionButton(
|
|
438
|
+
method=RequestType.PATCH,
|
|
439
|
+
identifiers=("wbportfolio:tradeproposal",),
|
|
440
|
+
icon=WBIcon.REGENERATE.icon,
|
|
441
|
+
key="revert",
|
|
442
|
+
label="Revert",
|
|
443
|
+
action_label="revert",
|
|
444
|
+
description_fields="<p>Unapply trades and move everything back to draft (i.e. The underlying asset positions will change like the trades were never applied)</p>",
|
|
445
|
+
)
|
|
446
|
+
},
|
|
447
|
+
)
|
|
448
|
+
def revert(self, **kwargs):
|
|
449
|
+
with suppress(KeyError):
|
|
450
|
+
del self.__dict__["validated_trading_service"]
|
|
451
|
+
revert_trade_proposal_as_task.delay(self.id, **kwargs)
|
|
452
|
+
|
|
453
|
+
def can_revert(self):
|
|
454
|
+
errors = dict()
|
|
455
|
+
if self.trades.exclude(status=Trade.Status.EXECUTED).exists():
|
|
456
|
+
errors["non_field_errors"] = "All trades need to be executed before reverting"
|
|
457
|
+
if not self.portfolio.is_manageable:
|
|
458
|
+
errors[
|
|
459
|
+
"portfolio"
|
|
460
|
+
] = "The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
|
|
461
|
+
return errors
|
|
462
|
+
|
|
463
|
+
# End FSM logics
|
|
464
|
+
|
|
465
|
+
@classmethod
|
|
466
|
+
def get_endpoint_basename(cls) -> str:
|
|
467
|
+
return "wbportfolio:tradeproposal"
|
|
468
|
+
|
|
469
|
+
@classmethod
|
|
470
|
+
def get_representation_endpoint(cls) -> str:
|
|
471
|
+
return "wbportfolio:tradeproposalrepresentation-list"
|
|
472
|
+
|
|
473
|
+
@classmethod
|
|
474
|
+
def get_representation_value_key(cls) -> str:
|
|
475
|
+
return "id"
|
|
476
|
+
|
|
477
|
+
@classmethod
|
|
478
|
+
def get_representation_label_key(cls) -> str:
|
|
479
|
+
return "{{_portfolio.name}} ({{trade_date}})"
|
|
480
|
+
|
|
481
|
+
class Meta:
|
|
482
|
+
verbose_name = "Trade Proposal"
|
|
483
|
+
verbose_name_plural = "Trade Proposals"
|
|
484
|
+
unique_together = ["portfolio", "trade_date"]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@shared_task(queue="portfolio")
|
|
488
|
+
def apply_trades_proposal_as_task(trade_proposal_id):
|
|
489
|
+
trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
|
|
490
|
+
trade_proposal.apply_trades()
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@shared_task(queue="portfolio")
|
|
494
|
+
def revert_trade_proposal_as_task(trade_proposal_id, **kwargs):
|
|
495
|
+
trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
|
|
496
|
+
trade_proposal.revert_trades()
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@receiver(post_save, sender="wbportfolio.TradeProposal")
|
|
500
|
+
def post_save_trade_proposal(sender, instance, created, raw, **kwargs):
|
|
501
|
+
if created and not raw and instance.portfolio.assets.filter(date__lte=instance.trade_date).exists():
|
|
502
|
+
instance.create_or_update_trades(reset=True)
|