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,292 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from celery import shared_task
|
|
9
|
+
from celery.canvas import Signature
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.db.models import F
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
from wbfdm.enums import MarketData
|
|
14
|
+
from wbfdm.models.instruments import Instrument
|
|
15
|
+
from wbportfolio.models import Portfolio
|
|
16
|
+
from wbportfolio.models.transactions.trade_proposals import TradeProposal
|
|
17
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
18
|
+
from wbportfolio.pms.typing import Position as PositionDTO
|
|
19
|
+
|
|
20
|
+
from .synchronization import SynchronizationTask
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def convert_currency(x, val_date, other_currency):
|
|
24
|
+
instrument = Instrument.objects.get(id=x)
|
|
25
|
+
try:
|
|
26
|
+
return instrument.currency.convert(val_date, other_currency)
|
|
27
|
+
except Exception:
|
|
28
|
+
return np.nan
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PortfolioSynchronization(SynchronizationTask):
|
|
32
|
+
is_automatic_validation = models.BooleanField(
|
|
33
|
+
default=True,
|
|
34
|
+
verbose_name="Automatic validation",
|
|
35
|
+
help_text="Set to True if you want to automatically implement proposed positions",
|
|
36
|
+
)
|
|
37
|
+
propagate_history = models.BooleanField(
|
|
38
|
+
default=False,
|
|
39
|
+
verbose_name="Propagate History",
|
|
40
|
+
help_text="If true, when the depends on portfolio changes at a certain date, this method will trigger a synchronization for each date (at the scheduled frequency) from that date to the latest valid date",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def synchronize(
|
|
44
|
+
self,
|
|
45
|
+
portfolio: models.Model,
|
|
46
|
+
sync_date: date,
|
|
47
|
+
task_execution_datetime: Optional[datetime] = None,
|
|
48
|
+
override_execution_datetime_validity: Optional[bool] = False,
|
|
49
|
+
post_processing: bool = True,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
This function compute the new portfolio composition after synchronization (returns from `_import_method`) for a
|
|
54
|
+
given date and either update or create the portfolio or create a trade proposal given the new portfolio constituent.
|
|
55
|
+
|
|
56
|
+
:param portfolio: The portfolio to synchronize the positions from
|
|
57
|
+
:param sync_date: The date at which we need to synchronize the given portfolio
|
|
58
|
+
:param task_execution_datetime: An optional datetime specifying at which time this task was initially executed.
|
|
59
|
+
:param override_execution_datetime_validity: If true, we don't valide `task_execution_datetime`
|
|
60
|
+
:param kwargs: keyword arguments
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
initkwargs = {**kwargs, **self.cast_kwargs}
|
|
64
|
+
if not task_execution_datetime:
|
|
65
|
+
task_execution_datetime = timezone.now()
|
|
66
|
+
if portfolio.is_active_at_date(sync_date):
|
|
67
|
+
if self.is_valid_date(task_execution_datetime) or override_execution_datetime_validity:
|
|
68
|
+
if import_res := list(zip(*self._import_method(portfolio, sync_date, **initkwargs))):
|
|
69
|
+
effective_positions = list(filter(lambda x: x, import_res[0]))
|
|
70
|
+
target_positions = list(filter(lambda x: x, import_res[1]))
|
|
71
|
+
if len(target_positions) > 0:
|
|
72
|
+
target_portfolio = PortfolioDTO(target_positions)
|
|
73
|
+
|
|
74
|
+
effective_portfolio = (
|
|
75
|
+
PortfolioDTO(effective_positions) if len(effective_positions) > 0 else None
|
|
76
|
+
)
|
|
77
|
+
if self.is_automatic_validation:
|
|
78
|
+
# We process these positions automatically
|
|
79
|
+
portfolio.import_positions_at_date(
|
|
80
|
+
target_portfolio, sync_date, post_processing=post_processing
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
trade_proposal, created = TradeProposal.objects.get_or_create(
|
|
84
|
+
trade_date=sync_date,
|
|
85
|
+
portfolio=portfolio,
|
|
86
|
+
defaults={"comment": "Automatic rebalancing"},
|
|
87
|
+
)
|
|
88
|
+
trade_proposal.create_or_update_trades(
|
|
89
|
+
target_portfolio=target_portfolio, effective_portfolio=effective_portfolio
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
portfolio.last_synchronization = timezone.now()
|
|
93
|
+
portfolio.save()
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
logging.info(
|
|
97
|
+
f"Synchronization invalid: {portfolio.name} synchronization with {self.name} was triggered for {sync_date} but date not valid for crontab schedule {str(self.crontab)}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def synchronize_as_task_si(self, portfolio: models.Model, sync_date: date, **kwargs: Any) -> Signature:
|
|
101
|
+
"""
|
|
102
|
+
Utility function that returns the signature of the synchronize method
|
|
103
|
+
"""
|
|
104
|
+
return synchronize_portfolio_as_task.si(self.id, portfolio.id, sync_date, **kwargs)
|
|
105
|
+
|
|
106
|
+
def _tasks_signature(self, sync_date: Optional[date] = None, **kwargs: Any) -> Signature:
|
|
107
|
+
"""
|
|
108
|
+
Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
|
|
109
|
+
This method is expected to be implemented at each inheriting class.
|
|
110
|
+
:param args: list
|
|
111
|
+
:param kwargs: dict
|
|
112
|
+
:return: list[signature]
|
|
113
|
+
"""
|
|
114
|
+
for portfolio in self.portfolios.all():
|
|
115
|
+
portfolio_sync_dates = []
|
|
116
|
+
if sync_date:
|
|
117
|
+
portfolio_sync_dates = [sync_date]
|
|
118
|
+
elif not sync_date and portfolio.assets.exists() and (latest_asset := portfolio.assets.latest("date")):
|
|
119
|
+
portfolio_sync_dates = map(
|
|
120
|
+
lambda x: x.date(), pd.date_range(latest_asset.date, date.today(), freq="B", inclusive="left")
|
|
121
|
+
)
|
|
122
|
+
for portfolio_sync_date in portfolio_sync_dates:
|
|
123
|
+
if portfolio.is_active_at_date(portfolio_sync_date):
|
|
124
|
+
yield synchronize_portfolio_as_task.si(self.id, portfolio.id, portfolio_sync_date, **kwargs)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def _default_callback(
|
|
128
|
+
cls,
|
|
129
|
+
portfolio: Portfolio,
|
|
130
|
+
sync_date: date,
|
|
131
|
+
portfolio_created: Optional[Portfolio] = None,
|
|
132
|
+
adjusted_weighting: Optional[Decimal] = Decimal(1.0),
|
|
133
|
+
adjusted_currency_fx_rate: Optional[Decimal] = Decimal(1.0),
|
|
134
|
+
is_estimated: Optional[bool] = False,
|
|
135
|
+
portfolio_total_value: Optional[float] = None,
|
|
136
|
+
**kwargs: Any,
|
|
137
|
+
):
|
|
138
|
+
"""Recursively calculates the position for a portfolio
|
|
139
|
+
|
|
140
|
+
Arguments:
|
|
141
|
+
portfolio {portfolio.Portfolio} -- The Portfolio on which the assets will be computed
|
|
142
|
+
sync_date {datetime.date} -- The date on which the assets will be computed
|
|
143
|
+
|
|
144
|
+
Keyword Arguments:
|
|
145
|
+
portfolio {portfolio.Portfolio} -- The core portfolio from which the computed position are created (default: {None})
|
|
146
|
+
adjusted_weighting {int} -- the adjusted weight of the current level of index (default: {1})
|
|
147
|
+
adjusted_currency_fx_rate {int} -- the adjusted currency exchange rate on the current level of index (default: {1})
|
|
148
|
+
|
|
149
|
+
Yields:
|
|
150
|
+
tuple[dict, dict] -- Two dictionaries: One with filter parameters and one with default values
|
|
151
|
+
"""
|
|
152
|
+
is_root_position_estimated = False
|
|
153
|
+
if not portfolio_created:
|
|
154
|
+
if portfolio_created := portfolio.primary_portfolio:
|
|
155
|
+
is_root_position_estimated = (
|
|
156
|
+
portfolio_created.assets.filter(date=sync_date).count() == 1
|
|
157
|
+
and portfolio_created.assets.filter(date=sync_date, is_estimated=True).count() == 1
|
|
158
|
+
)
|
|
159
|
+
if portfolio_created:
|
|
160
|
+
child_positions = portfolio_created.assets.filter(date=sync_date)
|
|
161
|
+
asset_positions = child_positions.all()
|
|
162
|
+
# Compute the total portfolio value based on the root position child (otherwise the value is passed as
|
|
163
|
+
# parameters in the recursion
|
|
164
|
+
if not portfolio_total_value:
|
|
165
|
+
portfolio_total_value = child_positions.aggregate(tv=models.Sum(F("total_value_fx_portfolio")))["tv"]
|
|
166
|
+
if not portfolio_total_value:
|
|
167
|
+
portfolio_total_value = portfolio_created.get_total_value(sync_date)
|
|
168
|
+
for position in child_positions:
|
|
169
|
+
if child_portfolio := position.underlying_instrument.portfolio:
|
|
170
|
+
if child_portfolio.assets.filter(date=sync_date).exists() and position.weighting is not None:
|
|
171
|
+
asset_positions = asset_positions.exclude(id=position.id)
|
|
172
|
+
yield from cls._default_callback(
|
|
173
|
+
portfolio,
|
|
174
|
+
sync_date,
|
|
175
|
+
portfolio_created=child_portfolio,
|
|
176
|
+
adjusted_weighting=position.weighting * adjusted_weighting,
|
|
177
|
+
portfolio_total_value=portfolio_total_value,
|
|
178
|
+
adjusted_currency_fx_rate=position.currency_fx_rate * adjusted_currency_fx_rate,
|
|
179
|
+
is_estimated=False
|
|
180
|
+
if is_root_position_estimated
|
|
181
|
+
else (is_estimated and position.is_estimated),
|
|
182
|
+
)
|
|
183
|
+
df = pd.DataFrame(
|
|
184
|
+
asset_positions.values_list(
|
|
185
|
+
"currency_fx_rate",
|
|
186
|
+
"price",
|
|
187
|
+
"weighting",
|
|
188
|
+
"shares",
|
|
189
|
+
"is_estimated",
|
|
190
|
+
"underlying_instrument",
|
|
191
|
+
"currency",
|
|
192
|
+
"exchange",
|
|
193
|
+
),
|
|
194
|
+
columns=[
|
|
195
|
+
"currency_fx_rate",
|
|
196
|
+
"price",
|
|
197
|
+
"weighting",
|
|
198
|
+
"shares",
|
|
199
|
+
"is_estimated",
|
|
200
|
+
"underlying_instrument",
|
|
201
|
+
"currency",
|
|
202
|
+
"exchange",
|
|
203
|
+
],
|
|
204
|
+
)
|
|
205
|
+
if not df.empty:
|
|
206
|
+
df.currency_fx_rate = df.currency_fx_rate * adjusted_currency_fx_rate
|
|
207
|
+
df.weighting = df.weighting * adjusted_weighting
|
|
208
|
+
|
|
209
|
+
df = (
|
|
210
|
+
df.groupby(["underlying_instrument", "currency", "exchange"], dropna=False)
|
|
211
|
+
.agg(
|
|
212
|
+
{
|
|
213
|
+
"currency_fx_rate": "first",
|
|
214
|
+
"price": "first",
|
|
215
|
+
"weighting": "sum",
|
|
216
|
+
"shares": "sum",
|
|
217
|
+
"is_estimated": "first",
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
.reset_index()
|
|
221
|
+
)
|
|
222
|
+
df[["underlying_instrument", "currency", "exchange"]] = df[
|
|
223
|
+
["underlying_instrument", "currency", "exchange"]
|
|
224
|
+
].astype("object")
|
|
225
|
+
df[["currency_fx_rate", "price", "weighting", "shares"]] = df[
|
|
226
|
+
["currency_fx_rate", "price", "weighting", "shares"]
|
|
227
|
+
].astype("float")
|
|
228
|
+
|
|
229
|
+
df["actual_currency_fx_rate"] = df.underlying_instrument.apply(
|
|
230
|
+
lambda x: convert_currency(x, sync_date, portfolio.currency)
|
|
231
|
+
).astype("float")
|
|
232
|
+
df["actual_currency_fx_rate"] = df["actual_currency_fx_rate"].fillna(df["currency_fx_rate"])
|
|
233
|
+
|
|
234
|
+
df = df.where(pd.notnull(df), None).set_index("underlying_instrument")
|
|
235
|
+
missing_prices = df.loc[df["price"].isnull(), "price"]
|
|
236
|
+
if not missing_prices.empty:
|
|
237
|
+
prices_df = pd.DataFrame(
|
|
238
|
+
Instrument.objects.filter(id__in=missing_prices.index).dl.market_data(
|
|
239
|
+
values=[MarketData.CLOSE], exact_date=sync_date
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
if not prices_df.empty:
|
|
243
|
+
prices_df = prices_df[["close", "instrument_id"]].set_index("instrument_id").astype("float")
|
|
244
|
+
df.loc[prices_df.index, "price"] = prices_df
|
|
245
|
+
|
|
246
|
+
if portfolio_total_value is not None:
|
|
247
|
+
df["shares"] = (df["weighting"] * float(portfolio_total_value)) / (
|
|
248
|
+
df["price"] * df["actual_currency_fx_rate"]
|
|
249
|
+
)
|
|
250
|
+
if is_estimated:
|
|
251
|
+
df["is_estimated"] = True
|
|
252
|
+
for underlying_instrument, asset_position in df.to_dict("index").items():
|
|
253
|
+
if (
|
|
254
|
+
asset_position["weighting"] or asset_position["shares"]
|
|
255
|
+
): # We don't yield empty position (pos with shares and weight equal to 0 or None)
|
|
256
|
+
# We return the position as a serialized dictionary
|
|
257
|
+
yield None, PositionDTO(
|
|
258
|
+
date=sync_date,
|
|
259
|
+
asset_valuation_date=sync_date,
|
|
260
|
+
portfolio_created=portfolio_created.id,
|
|
261
|
+
underlying_instrument=underlying_instrument,
|
|
262
|
+
instrument_type=Instrument.objects.get(id=underlying_instrument).security_instrument_type,
|
|
263
|
+
currency=asset_position["currency"],
|
|
264
|
+
exchange=asset_position["exchange"],
|
|
265
|
+
shares=asset_position["shares"],
|
|
266
|
+
price=asset_position["price"],
|
|
267
|
+
currency_fx_rate=asset_position["actual_currency_fx_rate"],
|
|
268
|
+
weighting=asset_position["weighting"],
|
|
269
|
+
is_estimated=asset_position["is_estimated"],
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def __str__(self) -> str:
|
|
273
|
+
return self.name
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def get_representation_endpoint(cls) -> str:
|
|
277
|
+
return "wbportfolio:portfoliosynchronizationrepresentation-list"
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def get_representation_value_key(cls) -> str:
|
|
281
|
+
return "id"
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
def get_representation_label_key(cls) -> str:
|
|
285
|
+
return "{{name}}"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@shared_task(queue="portfolio")
|
|
289
|
+
def synchronize_portfolio_as_task(synchronization_method_id: int, portfolio_id: int, sync_date: date, **kwargs: Any):
|
|
290
|
+
portfolio = Portfolio.objects.get(id=portfolio_id)
|
|
291
|
+
synchronization_method = PortfolioSynchronization.objects.get(id=synchronization_method_id)
|
|
292
|
+
synchronization_method.synchronize(portfolio, sync_date, **kwargs)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import math
|
|
3
|
+
from datetime import date, datetime, timedelta
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from celery import shared_task
|
|
9
|
+
from celery.canvas import Signature
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.utils import timezone
|
|
12
|
+
from pandas.tseries.offsets import BDay
|
|
13
|
+
from wbfdm.models import Instrument
|
|
14
|
+
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
15
|
+
|
|
16
|
+
from .synchronization import SynchronizationTask
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PriceComputation(SynchronizationTask):
|
|
20
|
+
@property
|
|
21
|
+
def instruments(self):
|
|
22
|
+
for instrument in Instrument.active_objects.filter(
|
|
23
|
+
models.Q(id__in=self.products.values("id")) | models.Q(id__in=self.indexes.values("id"))
|
|
24
|
+
):
|
|
25
|
+
yield instrument
|
|
26
|
+
|
|
27
|
+
def compute(
|
|
28
|
+
self,
|
|
29
|
+
instrument: models.Model,
|
|
30
|
+
sync_date: date,
|
|
31
|
+
task_execution_datetime: Optional[datetime] = None,
|
|
32
|
+
override_execution_datetime_validity: Optional[bool] = False,
|
|
33
|
+
**kwargs: Any,
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Given positions at t and t-1, we compute the performance and estimate the instrument price at t
|
|
37
|
+
If a price already exists at that date and is estimated already, we update it. If no price exists, we create it as estimated.
|
|
38
|
+
Otherwise, we don't do anything to protect imported real prices.
|
|
39
|
+
:param instrument: The instrument to compute the new price from
|
|
40
|
+
:param sync_date: The date at which we need to compute the new price
|
|
41
|
+
:param task_execution_datetime: An optional datetime specifying at which time this task was initially executed.
|
|
42
|
+
:param override_execution_datetime_validity: If true, we don't valide `task_execution_datetime`
|
|
43
|
+
:param kwargs: keyword arguments
|
|
44
|
+
"""
|
|
45
|
+
if not task_execution_datetime:
|
|
46
|
+
task_execution_datetime = timezone.now()
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
(self.is_valid_date(task_execution_datetime) or override_execution_datetime_validity)
|
|
50
|
+
and instrument.is_active_at_date(sync_date)
|
|
51
|
+
and not sync_date.weekday() in [5, 6]
|
|
52
|
+
):
|
|
53
|
+
price_data = self._import_method(instrument, sync_date, **kwargs)
|
|
54
|
+
if (
|
|
55
|
+
price_data
|
|
56
|
+
and (_instrument := price_data.pop("instrument", None))
|
|
57
|
+
and (_date := price_data.pop("date", None))
|
|
58
|
+
):
|
|
59
|
+
calculated = price_data.pop("calculated", True)
|
|
60
|
+
try:
|
|
61
|
+
price = InstrumentPrice.objects.get(instrument=_instrument, date=_date, calculated=calculated)
|
|
62
|
+
for k, v in price_data.items():
|
|
63
|
+
setattr(price, k, v)
|
|
64
|
+
price.save()
|
|
65
|
+
except InstrumentPrice.DoesNotExist:
|
|
66
|
+
price = InstrumentPrice.objects.create(
|
|
67
|
+
instrument=_instrument, date=_date, calculated=calculated, **price_data
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
price.save() # trigger explicitly save logic as update_or_create doesn't
|
|
71
|
+
if (
|
|
72
|
+
_date == _instrument.prices.latest("date").date
|
|
73
|
+
): # if price date is the latest instrument price date, we recomputet the last valuation data
|
|
74
|
+
_instrument.update_last_valuation_date()
|
|
75
|
+
else:
|
|
76
|
+
logging.info(
|
|
77
|
+
f"Price Computation invalid: {str(instrument)} price computation with {self.name} was triggered for {sync_date} but date not valid for crontab schedule {str(self.crontab)}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def compute_price_as_task_si(self, instrument: models.Model, sync_date: date, **kwargs: Any) -> Signature:
|
|
81
|
+
"""
|
|
82
|
+
Utility function that returns the signature of the compute method
|
|
83
|
+
"""
|
|
84
|
+
return compute_price_as_task.si(self.id, instrument.id, sync_date, **kwargs)
|
|
85
|
+
|
|
86
|
+
def _tasks_signature(
|
|
87
|
+
self, sync_date: Optional[date] = None, to_date: Optional[date] = None, **kwargs: Any
|
|
88
|
+
) -> Signature:
|
|
89
|
+
"""
|
|
90
|
+
Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
|
|
91
|
+
This method is expected to be implemented at each inheriting class.
|
|
92
|
+
:param args: list
|
|
93
|
+
:param kwargs: dict
|
|
94
|
+
:return: list[signature]
|
|
95
|
+
"""
|
|
96
|
+
if not to_date:
|
|
97
|
+
to_date = date.today()
|
|
98
|
+
for instrument in self.instruments:
|
|
99
|
+
# Get latest valuation date + 1 Bday if not given by the key word arguments
|
|
100
|
+
instrument_sync_dates = []
|
|
101
|
+
if sync_date:
|
|
102
|
+
instrument_sync_dates = [sync_date]
|
|
103
|
+
elif not sync_date and instrument.prices.exists() and (last_price := instrument.prices.latest("date")):
|
|
104
|
+
instrument_sync_dates = map(
|
|
105
|
+
lambda x: x.date(),
|
|
106
|
+
pd.date_range(
|
|
107
|
+
max(last_price.date, to_date - timedelta(days=7)),
|
|
108
|
+
to_date,
|
|
109
|
+
freq="B",
|
|
110
|
+
inclusive="left",
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
for instrument_sync_date in instrument_sync_dates:
|
|
114
|
+
if instrument.is_active_at_date(instrument_sync_date):
|
|
115
|
+
yield compute_price_as_task.si(self.id, instrument.id, instrument_sync_date, **kwargs)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def _default_callback(cls, instrument: Instrument, val_date: date, **kwargs: Any):
|
|
119
|
+
"""
|
|
120
|
+
Default NAV computation function. We simply compute the performance given two positions for two dates and estimate the new price
|
|
121
|
+
based on the overall performance.
|
|
122
|
+
TODO: If exit/buy of positions, is this function still correct?
|
|
123
|
+
:param instrument: The instrument to compute the new price from
|
|
124
|
+
:param val_date: The date at which we need to compute the new price
|
|
125
|
+
:param kwargs: keyword arguments
|
|
126
|
+
"""
|
|
127
|
+
if portfolio := instrument.portfolio:
|
|
128
|
+
# check if the asset portfolio of this instruments exists and has positions at the synchronization date
|
|
129
|
+
if previous_date := portfolio.get_latest_asset_position_date(val_date - BDay(1), with_estimated=True):
|
|
130
|
+
# Get the previous valid price date before sync_date, checks if it exists and if positions are available
|
|
131
|
+
# at that date.
|
|
132
|
+
# If asset position exists on the previous day but not at the sync date, maybe propagation were not done, and we try it
|
|
133
|
+
if portfolio.assets.filter(date=previous_date).exists():
|
|
134
|
+
portfolio.propagate_or_update_assets(previous_date, val_date)
|
|
135
|
+
if portfolio.assets.filter(date=val_date).exists():
|
|
136
|
+
last_price = None
|
|
137
|
+
if (
|
|
138
|
+
last_valuation := instrument.prices.filter(date=previous_date)
|
|
139
|
+
.order_by("calculated")
|
|
140
|
+
.first()
|
|
141
|
+
):
|
|
142
|
+
last_price = last_valuation.net_value
|
|
143
|
+
elif not instrument.valuations.filter(date__lt=previous_date).exists():
|
|
144
|
+
last_price = instrument.issue_price
|
|
145
|
+
if last_price:
|
|
146
|
+
weights = pd.DataFrame(
|
|
147
|
+
portfolio.assets.filter(date=previous_date).values(
|
|
148
|
+
"weighting", "date", "underlying_instrument"
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
weights = weights.pivot_table(
|
|
152
|
+
index="date", columns=["underlying_instrument"], values="weighting", aggfunc="sum"
|
|
153
|
+
).astype("float")
|
|
154
|
+
weights = weights.iloc[-1, :]
|
|
155
|
+
perfs = pd.DataFrame(
|
|
156
|
+
portfolio.assets.filter(date__in=[previous_date, val_date]).values(
|
|
157
|
+
"date", "price_fx_portfolio", "underlying_instrument"
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
perfs = perfs.pivot_table(
|
|
161
|
+
index="date",
|
|
162
|
+
columns=["underlying_instrument"],
|
|
163
|
+
values="price_fx_portfolio",
|
|
164
|
+
aggfunc="mean",
|
|
165
|
+
).astype("float")
|
|
166
|
+
perfs = perfs / perfs.shift(1, axis=0) - 1.0
|
|
167
|
+
perfs = perfs.fillna(0).iloc[-1, :]
|
|
168
|
+
total_perfs = float((perfs * weights).sum())
|
|
169
|
+
new_gross_valuation = float(last_price) * (1.0 + total_perfs)
|
|
170
|
+
if new_gross_valuation and not math.isnan(new_gross_valuation):
|
|
171
|
+
return {
|
|
172
|
+
"instrument": instrument,
|
|
173
|
+
"date": val_date,
|
|
174
|
+
"gross_value": Decimal(new_gross_valuation),
|
|
175
|
+
"net_value": Decimal(new_gross_valuation),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def __str__(self) -> str:
|
|
181
|
+
return self.name
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_representation_endpoint(cls) -> str:
|
|
185
|
+
return "wbportfolio:pricecomputationrepresentation-list"
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def get_representation_value_key(cls) -> str:
|
|
189
|
+
return "id"
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def get_representation_label_key(cls) -> str:
|
|
193
|
+
return "{{name}}"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@shared_task(queue="portfolio")
|
|
197
|
+
def compute_price_as_task(price_computation_method_id: int, instrument_id: int, sync_date: date, **kwargs: Any):
|
|
198
|
+
instrument = Instrument.objects.get(id=instrument_id)
|
|
199
|
+
price_computation = PriceComputation.objects.get(id=price_computation_method_id)
|
|
200
|
+
price_computation.compute(instrument, sync_date, **kwargs)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from datetime import date, datetime, time
|
|
4
|
+
from json import loads
|
|
5
|
+
from typing import Any, Iterator, Optional
|
|
6
|
+
|
|
7
|
+
from celery import chord, group, shared_task
|
|
8
|
+
from celery.canvas import Signature, signature
|
|
9
|
+
from croniter import croniter, croniter_range
|
|
10
|
+
from django.contrib.contenttypes.models import ContentType
|
|
11
|
+
from django.db import models
|
|
12
|
+
from django.db.models.signals import post_save
|
|
13
|
+
from django.dispatch import receiver
|
|
14
|
+
from django.utils import timezone
|
|
15
|
+
from django_celery_beat.models import CrontabSchedule, PeriodicTask, cronexp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SynchronizationTask(PeriodicTask):
|
|
19
|
+
RELATIVE_TASK_MODULE_PATH = "wbportfolio.models.synchronization.synchronization.task"
|
|
20
|
+
|
|
21
|
+
dependent_task = models.ForeignKey(
|
|
22
|
+
"self", related_name="dependency_tasks", null=True, blank=True, on_delete=models.SET_NULL
|
|
23
|
+
)
|
|
24
|
+
import_path = models.CharField(max_length=512, verbose_name="Import Path", default="", blank=True)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def cast_args(self) -> list[Any]:
|
|
28
|
+
"""
|
|
29
|
+
transform the string args representation into list. We except this to become unnecessary when django beat move to jsonfield
|
|
30
|
+
:return: list
|
|
31
|
+
"""
|
|
32
|
+
return loads(self.args or "[]")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def cast_kwargs(self) -> dict[Any, Any]:
|
|
36
|
+
"""
|
|
37
|
+
transform the string kwargs representation into dictionary. We except this to become unnecessary when django beat move to jsonfield
|
|
38
|
+
:return: list
|
|
39
|
+
"""
|
|
40
|
+
return loads(self.kwargs or "{}")
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def _import_method(self) -> Callable[[Any], Any]:
|
|
44
|
+
"""
|
|
45
|
+
If a custom task is specified, return the loaded module as callabck. Otherwise, returns the default synchronization
|
|
46
|
+
function defined in `_default_callback
|
|
47
|
+
:return: Callable
|
|
48
|
+
"""
|
|
49
|
+
if import_path := self.import_path:
|
|
50
|
+
synchronize_module = importlib.import_module(import_path)
|
|
51
|
+
return synchronize_module.callback
|
|
52
|
+
else:
|
|
53
|
+
return self._default_callback
|
|
54
|
+
|
|
55
|
+
def schedule_str(self, filter_daily: Optional[bool] = False) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Returns the crontab string representation. If fitler_daily is true, we cast the crontab so that the lowest frequency
|
|
58
|
+
becomes daily.
|
|
59
|
+
:param filter_daily: bool (optional)
|
|
60
|
+
:return: crontab string representation
|
|
61
|
+
"""
|
|
62
|
+
crontab_minute = cronexp(self.crontab.minute)
|
|
63
|
+
crontab_hour = cronexp(self.crontab.hour)
|
|
64
|
+
if filter_daily:
|
|
65
|
+
if crontab_minute == "*":
|
|
66
|
+
crontab_minute = "0"
|
|
67
|
+
if crontab_hour == "*":
|
|
68
|
+
crontab_hour = "0"
|
|
69
|
+
return "{0} {1} {2} {3} {4}".format(
|
|
70
|
+
crontab_minute,
|
|
71
|
+
crontab_hour,
|
|
72
|
+
cronexp(self.crontab.day_of_month),
|
|
73
|
+
cronexp(self.crontab.month_of_year),
|
|
74
|
+
cronexp(self.crontab.day_of_week),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def dates_range(self, from_date: date, to_date: date, filter_daily: Optional[bool] = False) -> Iterator[datetime]:
|
|
78
|
+
"""
|
|
79
|
+
returns a list of valid dates given an interval and a specific crontab schedule.
|
|
80
|
+
:param from_date: date
|
|
81
|
+
:param to_date: date
|
|
82
|
+
:param filter_daily: bool (optional)
|
|
83
|
+
:return: list[date]
|
|
84
|
+
"""
|
|
85
|
+
min_datetime = datetime.combine(from_date, time.min)
|
|
86
|
+
max_datetime = datetime.combine(to_date, time.max)
|
|
87
|
+
return croniter_range(min_datetime, max_datetime, self.schedule_str(filter_daily=filter_daily))
|
|
88
|
+
|
|
89
|
+
def is_valid_date(self, sync_datetime: datetime) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
check wether a date is valid given the stored crontab schedule
|
|
92
|
+
:param sync_datetime: datetime
|
|
93
|
+
:return: bool
|
|
94
|
+
"""
|
|
95
|
+
return croniter.match(self.schedule_str(), sync_datetime)
|
|
96
|
+
|
|
97
|
+
def save(self, *args: Any, **kwargs: Any):
|
|
98
|
+
self.task = self.RELATIVE_TASK_MODULE_PATH
|
|
99
|
+
if not self.schedule:
|
|
100
|
+
self.crontab, _ = CrontabSchedule.objects.get_or_create(
|
|
101
|
+
minute="0",
|
|
102
|
+
hour="*",
|
|
103
|
+
day_of_week="*",
|
|
104
|
+
day_of_month="*",
|
|
105
|
+
month_of_year="*",
|
|
106
|
+
)
|
|
107
|
+
if not self.crontab:
|
|
108
|
+
raise ValueError("Synchronization task supports only Crontab Schedule")
|
|
109
|
+
if self.crontab.minute == "*":
|
|
110
|
+
raise ValueError("The minimum crontab frequency supported is hourly.")
|
|
111
|
+
super().save(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
def _tasks_signature(self, *args, **kwargs: Any) -> Signature:
|
|
114
|
+
"""
|
|
115
|
+
Gather all tasks that needs to run under this synchronization job as a list of celery signatures.
|
|
116
|
+
This method is expected to be implemented at each inheriting class.
|
|
117
|
+
:param args: list
|
|
118
|
+
:param kwargs: dict
|
|
119
|
+
:return: list[signature]
|
|
120
|
+
"""
|
|
121
|
+
raise NotImplementedError()
|
|
122
|
+
|
|
123
|
+
def _default_callback(self, *args: Any, **kwargs: Any) -> Any:
|
|
124
|
+
"""
|
|
125
|
+
The default synchronization function executed if no custom task is provided for this synchronization object.
|
|
126
|
+
This method is expected to be implemented at each inheriting class.
|
|
127
|
+
:param args: list
|
|
128
|
+
:param kwargs: dict
|
|
129
|
+
:return: callable
|
|
130
|
+
"""
|
|
131
|
+
raise NotImplementedError()
|
|
132
|
+
|
|
133
|
+
def _get_kwargs(self) -> Any:
|
|
134
|
+
"""
|
|
135
|
+
return the base keyword argument to be injected into the `_tasks_signature` method. Define as a standalone function
|
|
136
|
+
in order to allow subclass definition.
|
|
137
|
+
:return: dict
|
|
138
|
+
"""
|
|
139
|
+
return {"task_execution_datetime": timezone.now()}
|
|
140
|
+
|
|
141
|
+
def _end_task_signature(self, **kwargs: Any) -> Signature:
|
|
142
|
+
"""
|
|
143
|
+
A synchronization object can defined a dependant task that will be executed at this end of all returned task from
|
|
144
|
+
_tasks_signatures.
|
|
145
|
+
This function returns the signature of this chained task.
|
|
146
|
+
:param kwargs:
|
|
147
|
+
:return: signature
|
|
148
|
+
"""
|
|
149
|
+
if self.dependent_task:
|
|
150
|
+
kwargs = {"override_execution_datetime_validity": True, **self.dependent_task.cast_kwargs, **kwargs}
|
|
151
|
+
return signature(
|
|
152
|
+
self.dependent_task.task, args=self.dependent_task.cast_args, kwargs=kwargs, immutable=True
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def chord(self, **kwargs: Any) -> chord:
|
|
157
|
+
"""
|
|
158
|
+
This function is the main entry point of the synchronization worklow. It is called from within the shared_task `task`
|
|
159
|
+
and create the celery chord containing the list of tasks chained to the end task (if any)
|
|
160
|
+
:param kwargs:
|
|
161
|
+
:return: chord
|
|
162
|
+
"""
|
|
163
|
+
kwargs = {**kwargs, **self._get_kwargs()}
|
|
164
|
+
tasks = list(self._tasks_signature(**kwargs))
|
|
165
|
+
if end_task := self._end_task_signature(**kwargs):
|
|
166
|
+
return chord(tasks, end_task)
|
|
167
|
+
return group(tasks)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@receiver(post_save, sender="wbportfolio.SynchronizationTask")
|
|
171
|
+
@receiver(post_save, sender="wbportfolio.PortfolioSynchronization")
|
|
172
|
+
@receiver(post_save, sender="wbportfolio.PriceComputation")
|
|
173
|
+
def post_save_synchronization_task(sender, instance: models.Model, created: bool, raw: bool, **kwargs: Any):
|
|
174
|
+
"""
|
|
175
|
+
Ensure args attribute contains the necessary arguments to retrieve the calling job from within asynchronous task
|
|
176
|
+
"""
|
|
177
|
+
if (created and not raw) or not instance.args:
|
|
178
|
+
content_type = ContentType.objects.get_for_model(instance)
|
|
179
|
+
instance.args = f'["{instance.id}", "{content_type.app_label}", "{content_type.model}"]'
|
|
180
|
+
instance.save()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@shared_task
|
|
184
|
+
def task(synchronization_object_id: int, app_label: str, model: str, **kwargs: Any):
|
|
185
|
+
synchronization_object = ContentType.objects.get(app_label=app_label, model=model).get_object_for_this_type(
|
|
186
|
+
id=synchronization_object_id
|
|
187
|
+
)
|
|
188
|
+
synchronization_object.chord(**kwargs).apply_async()
|