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,211 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from django.apps import apps
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.dispatch import receiver
|
|
6
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
7
|
+
from wbcore.signals import pre_merge
|
|
8
|
+
from wbfdm.models.instruments.instruments import Instrument
|
|
9
|
+
from wbfdm.signals import add_instrument_to_investable_universe
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShareMixin(models.Model):
|
|
13
|
+
shares = models.DecimalField(
|
|
14
|
+
max_digits=15,
|
|
15
|
+
decimal_places=4,
|
|
16
|
+
null=True,
|
|
17
|
+
blank=True,
|
|
18
|
+
help_text="The number of shares that were traded.",
|
|
19
|
+
verbose_name="Shares",
|
|
20
|
+
)
|
|
21
|
+
price = models.DecimalField(
|
|
22
|
+
max_digits=16,
|
|
23
|
+
decimal_places=4,
|
|
24
|
+
null=True,
|
|
25
|
+
blank=True,
|
|
26
|
+
help_text="The price per share.",
|
|
27
|
+
verbose_name="Price",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
price_gross = models.DecimalField(
|
|
31
|
+
max_digits=16,
|
|
32
|
+
decimal_places=4,
|
|
33
|
+
null=True,
|
|
34
|
+
blank=True,
|
|
35
|
+
help_text="The gross price per share.",
|
|
36
|
+
verbose_name="Gross Price",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
class Meta:
|
|
40
|
+
abstract = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Transaction(ImportMixin, models.Model):
|
|
44
|
+
class Type(models.TextChoices):
|
|
45
|
+
# Standart Asset Types
|
|
46
|
+
TRADE = "Trade", "Trade"
|
|
47
|
+
DIVIDEND_TRANSACTION = "DividendTransaction", "Dividend Transaction"
|
|
48
|
+
EXPIRY = "Expiry", "Expiry"
|
|
49
|
+
FEES = "Fees", "Fees"
|
|
50
|
+
|
|
51
|
+
transaction_type = models.CharField(max_length=255, verbose_name="Type", choices=Type.choices, default=Type.TRADE)
|
|
52
|
+
|
|
53
|
+
portfolio = models.ForeignKey(
|
|
54
|
+
"wbportfolio.Portfolio", related_name="transactions", on_delete=models.PROTECT, verbose_name="Portfolio"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
underlying_instrument = models.ForeignKey(
|
|
58
|
+
to="wbfdm.Instrument",
|
|
59
|
+
related_name="transactions",
|
|
60
|
+
limit_choices_to=models.Q(children__isnull=True),
|
|
61
|
+
on_delete=models.PROTECT,
|
|
62
|
+
verbose_name="Underlying Instrument",
|
|
63
|
+
help_text="The instrument that is this transaction.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
transaction_date = models.DateField(
|
|
67
|
+
verbose_name="Trade Date",
|
|
68
|
+
help_text="The date that this transaction was traded.",
|
|
69
|
+
)
|
|
70
|
+
book_date = models.DateField(
|
|
71
|
+
null=True,
|
|
72
|
+
blank=True,
|
|
73
|
+
verbose_name="Trade Date",
|
|
74
|
+
help_text="The date that this transaction was booked.",
|
|
75
|
+
)
|
|
76
|
+
value_date = models.DateField(
|
|
77
|
+
null=True,
|
|
78
|
+
blank=True,
|
|
79
|
+
verbose_name="Value Date",
|
|
80
|
+
help_text="The date that this transaction was valuated.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
currency = models.ForeignKey(
|
|
84
|
+
"currency.Currency",
|
|
85
|
+
related_name="transactions",
|
|
86
|
+
on_delete=models.PROTECT,
|
|
87
|
+
verbose_name="Currency",
|
|
88
|
+
)
|
|
89
|
+
currency_fx_rate = models.DecimalField(
|
|
90
|
+
max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
|
|
91
|
+
)
|
|
92
|
+
total_value = models.DecimalField(
|
|
93
|
+
max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value"
|
|
94
|
+
)
|
|
95
|
+
total_value_fx_portfolio = models.DecimalField(
|
|
96
|
+
max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Fx Portfolio"
|
|
97
|
+
)
|
|
98
|
+
total_value_gross = models.DecimalField(
|
|
99
|
+
max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Gross"
|
|
100
|
+
)
|
|
101
|
+
total_value_gross_fx_portfolio = models.DecimalField(
|
|
102
|
+
max_digits=20, decimal_places=4, null=True, blank=True, verbose_name="Total Value Gross Fx Portfolio"
|
|
103
|
+
)
|
|
104
|
+
external_id = models.CharField(
|
|
105
|
+
max_length=255,
|
|
106
|
+
null=True,
|
|
107
|
+
blank=True,
|
|
108
|
+
help_text="An external identifier that was supplied.",
|
|
109
|
+
verbose_name="External Identifier",
|
|
110
|
+
)
|
|
111
|
+
comment = models.TextField(default="", verbose_name="Comment", blank=True)
|
|
112
|
+
|
|
113
|
+
def save(self, *args, **kwargs):
|
|
114
|
+
if not getattr(self, "currency", None) and self.underlying_instrument:
|
|
115
|
+
self.currency = self.underlying_instrument.currency
|
|
116
|
+
if not self.currency_fx_rate:
|
|
117
|
+
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
118
|
+
self.transaction_date, self.portfolio.currency, exact_lookup=True
|
|
119
|
+
)
|
|
120
|
+
if not self.transaction_type:
|
|
121
|
+
self.transaction_type = self.__class__.__name__
|
|
122
|
+
if not self.value_date:
|
|
123
|
+
self.value_date = self.transaction_date
|
|
124
|
+
# try:
|
|
125
|
+
# # we try to find the next valid date (i.e. the one with position on the underlying instrument"
|
|
126
|
+
# self.value_date = (
|
|
127
|
+
# self.underlying_instrument.valuations.filter(date__gt=self.transaction_date).earliest("date").date
|
|
128
|
+
# )
|
|
129
|
+
# except ObjectDoesNotExist:
|
|
130
|
+
# self.value_date = (self.transaction_date + BDay(1)).date()
|
|
131
|
+
if not self.book_date:
|
|
132
|
+
self.book_date = self.transaction_date
|
|
133
|
+
if (
|
|
134
|
+
self.total_value is not None
|
|
135
|
+
and self.currency_fx_rate is not None
|
|
136
|
+
and self.total_value_fx_portfolio is None
|
|
137
|
+
):
|
|
138
|
+
self.total_value_fx_portfolio = self.total_value * self.currency_fx_rate
|
|
139
|
+
|
|
140
|
+
if self.total_value is not None and self.total_value_gross is None and self.total_value_gross is None:
|
|
141
|
+
self.total_value_gross = self.total_value
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
self.currency_fx_rate is not None
|
|
145
|
+
and self.total_value_gross is not None
|
|
146
|
+
and self.total_value_gross_fx_portfolio is None
|
|
147
|
+
):
|
|
148
|
+
self.total_value_gross_fx_portfolio = self.total_value_gross * self.currency_fx_rate
|
|
149
|
+
|
|
150
|
+
super().save(*args, **kwargs)
|
|
151
|
+
|
|
152
|
+
def __str__(self):
|
|
153
|
+
return f"{self.total_value} - {self.transaction_date:%d.%m.%Y} : {str(self.underlying_instrument)} (in {str(self.portfolio)})"
|
|
154
|
+
|
|
155
|
+
def get_casted_model(self):
|
|
156
|
+
return apps.get_model(app_label="wbportfolio", model_name=self.transaction_type)
|
|
157
|
+
|
|
158
|
+
def get_casted_transaction(self) -> models.Model:
|
|
159
|
+
"""
|
|
160
|
+
Cast the asset into its child representative
|
|
161
|
+
"""
|
|
162
|
+
model = self.get_casted_model()
|
|
163
|
+
return model.objects.get(pk=self.pk)
|
|
164
|
+
|
|
165
|
+
class Meta:
|
|
166
|
+
verbose_name = "Transaction"
|
|
167
|
+
verbose_name_plural = "Transactions"
|
|
168
|
+
indexes = [
|
|
169
|
+
models.Index(fields=["underlying_instrument", "transaction_date"]),
|
|
170
|
+
# models.Index(fields=["date", "underlying_instrument"]),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
objects = models.Manager()
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def get_representation_value_key(cls):
|
|
177
|
+
return "id"
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def get_representation_label_key(cls):
|
|
181
|
+
return "{{total_value}}{{transaction_date}}"
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def get_endpoint_basename(cls):
|
|
185
|
+
return "wbportfolio:transaction"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@receiver(pre_merge, sender="wbfdm.Instrument")
|
|
189
|
+
def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
|
|
190
|
+
"""
|
|
191
|
+
Simply reassign the transactions linked to the merged instrument to the main instrument
|
|
192
|
+
"""
|
|
193
|
+
merged_object.transactions.update(underlying_instrument=main_object)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
|
|
197
|
+
def add_instrument_to_investable_universe_from_transactions(sender: models.Model, **kwargs) -> list[int]:
|
|
198
|
+
"""
|
|
199
|
+
register all instrument linked to assets as within the investible universe
|
|
200
|
+
"""
|
|
201
|
+
return list(
|
|
202
|
+
(
|
|
203
|
+
Instrument.objects.annotate(
|
|
204
|
+
transaction_exists=models.Exists(
|
|
205
|
+
Transaction.objects.filter(underlying_instrument=models.OuterRef("pk"))
|
|
206
|
+
)
|
|
207
|
+
).filter(transaction_exists=True)
|
|
208
|
+
)
|
|
209
|
+
.distinct()
|
|
210
|
+
.values_list("id", flat=True)
|
|
211
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from wbfdm.models import Instrument
|
|
2
|
+
from wbportfolio.models import Index, Product
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_casted_portfolio_instrument(instrument: Instrument) -> Product | Index | None:
|
|
6
|
+
try:
|
|
7
|
+
return Product.objects.get(id=instrument.id)
|
|
8
|
+
except Product.DoesNotExist:
|
|
9
|
+
try:
|
|
10
|
+
return Index.objects.get(id=instrument.id)
|
|
11
|
+
except Index.DoesNotExist:
|
|
12
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from wbportfolio.models import PortfolioRole
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_manager(request):
|
|
5
|
+
return PortfolioRole.is_manager(request.user.profile)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_portfolio_manager(request):
|
|
9
|
+
return PortfolioRole.is_portfolio_manager(request.user.profile)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_analyst(request):
|
|
13
|
+
return PortfolioRole.is_analyst(request.user.profile)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .handler import TradingService
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from django.core.exceptions import ValidationError
|
|
5
|
+
from wbportfolio.pms.typing import Portfolio, Trade, TradeBatch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TradingService:
|
|
9
|
+
"""
|
|
10
|
+
This class represents the trading service. It can be instantiated either with the target portfolio and the effective portfolio or given a direct list of trade
|
|
11
|
+
In any case, it will compute all three states
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
trade_date: date,
|
|
17
|
+
effective_portfolio: Portfolio | None = None,
|
|
18
|
+
target_portfolio: Portfolio | None = None,
|
|
19
|
+
trades_batch: TradeBatch | None = None,
|
|
20
|
+
total_value: Decimal = None,
|
|
21
|
+
):
|
|
22
|
+
if not target_portfolio and not trades_batch:
|
|
23
|
+
raise ValueError("Either target positions or trades needs to be provided")
|
|
24
|
+
self.total_value = total_value
|
|
25
|
+
self.trade_date = trade_date
|
|
26
|
+
# If effective portfoolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
|
|
27
|
+
if effective_portfolio and trades_batch:
|
|
28
|
+
trades_batch = self.build_trade_batch(effective_portfolio, trades_batch=trades_batch)
|
|
29
|
+
# if no trade but a effective portfolio is provided, we get the trade batch only from the effective portofolio (and the target portfolio if provided, but optional. Without it, the trade delta weight will be 0 )
|
|
30
|
+
elif not trades_batch and effective_portfolio:
|
|
31
|
+
# If no trade batch is provided but effetive_portfolio is, we estimate the trade from the given portfolios
|
|
32
|
+
trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio=target_portfolio)
|
|
33
|
+
# Finally, we compute the target portfolio
|
|
34
|
+
if trades_batch and not target_portfolio:
|
|
35
|
+
target_portfolio = trades_batch.convert_to_portfolio()
|
|
36
|
+
|
|
37
|
+
self.trades_batch = trades_batch
|
|
38
|
+
self.effective_portfolio = effective_portfolio
|
|
39
|
+
self.target_portfolio = target_portfolio
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def errors(self) -> list[str]:
|
|
43
|
+
"""
|
|
44
|
+
Returned the list of errors stored during the validation process. Can only be called after is_valid
|
|
45
|
+
"""
|
|
46
|
+
if not hasattr(self, "_errors"):
|
|
47
|
+
msg = "You must call `.is_valid()` before accessing `.errors`."
|
|
48
|
+
raise AssertionError(msg)
|
|
49
|
+
return self._errors
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def validated_trades(self) -> list[Trade]:
|
|
53
|
+
"""
|
|
54
|
+
Returned the list of validated trade stored during the validation process. Can only be called after is_valid
|
|
55
|
+
"""
|
|
56
|
+
if not hasattr(self, "_validated_trades"):
|
|
57
|
+
msg = "You must call `.is_valid()` before accessing `.validated_trades`."
|
|
58
|
+
raise AssertionError(msg)
|
|
59
|
+
return self._validated_trades
|
|
60
|
+
|
|
61
|
+
def run_validation(self, validated_trades: list[Trade]):
|
|
62
|
+
"""
|
|
63
|
+
Test the given value against all the validators on the field,
|
|
64
|
+
and either raise a `ValidationError` or simply return.
|
|
65
|
+
"""
|
|
66
|
+
TradeBatch(validated_trades).validate()
|
|
67
|
+
if self.effective_portfolio:
|
|
68
|
+
for trade in validated_trades:
|
|
69
|
+
if (
|
|
70
|
+
trade.effective_weight
|
|
71
|
+
and trade.underlying_instrument not in self.effective_portfolio.positions_map
|
|
72
|
+
):
|
|
73
|
+
raise ValidationError("All effective position needs to be matched with a validated trade")
|
|
74
|
+
|
|
75
|
+
def build_trade_batch(
|
|
76
|
+
self,
|
|
77
|
+
effective_portfolio: Portfolio,
|
|
78
|
+
target_portfolio: Portfolio | None = None,
|
|
79
|
+
trades_batch: TradeBatch | None = None,
|
|
80
|
+
) -> TradeBatch:
|
|
81
|
+
"""
|
|
82
|
+
Given combination of effective portfolio and either a trades batch or a target portfolio, ensure all theres variables are set
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
effective_portfolio: The effective portfolio
|
|
86
|
+
target_portfolio: The optional target portfolio
|
|
87
|
+
trades_batch: The optional trades batch
|
|
88
|
+
|
|
89
|
+
Returns: The normalized trades batch
|
|
90
|
+
"""
|
|
91
|
+
instruments = list(effective_portfolio.positions_map.keys())
|
|
92
|
+
if target_portfolio:
|
|
93
|
+
instruments.extend(list(target_portfolio.positions_map.keys()))
|
|
94
|
+
if trades_batch:
|
|
95
|
+
instruments.extend(list(trades_batch.trades_map.keys()))
|
|
96
|
+
_trades: list[Trade] = []
|
|
97
|
+
for instrument in set(instruments):
|
|
98
|
+
effective_weight = target_weight = 0
|
|
99
|
+
effective_shares = target_shares = 0
|
|
100
|
+
instrument_type = currency = None
|
|
101
|
+
if effective_pos := effective_portfolio.positions_map.get(instrument, None):
|
|
102
|
+
effective_weight = target_weight = effective_pos.weighting
|
|
103
|
+
effective_shares = target_shares = effective_pos.shares
|
|
104
|
+
instrument_type, currency = effective_pos.instrument_type, effective_pos.currency
|
|
105
|
+
if target_portfolio and (target_pos := target_portfolio.positions_map.get(instrument, None)):
|
|
106
|
+
target_weight = target_pos.weighting
|
|
107
|
+
target_shares = target_pos.shares
|
|
108
|
+
if trades_batch and (trade := trades_batch.trades_map.get(instrument, None)):
|
|
109
|
+
effective_weight, target_weight = trade.effective_weight, trade.target_weight
|
|
110
|
+
effective_shares, target_shares = trade.effective_shares, trade.target_shares
|
|
111
|
+
instrument_type, currency = trade.instrument_type, trade.currency
|
|
112
|
+
|
|
113
|
+
_trades.append(
|
|
114
|
+
Trade(
|
|
115
|
+
underlying_instrument=instrument,
|
|
116
|
+
effective_weight=effective_weight,
|
|
117
|
+
target_weight=target_weight,
|
|
118
|
+
effective_shares=effective_shares,
|
|
119
|
+
target_shares=target_shares,
|
|
120
|
+
date=self.trade_date,
|
|
121
|
+
instrument_type=instrument_type,
|
|
122
|
+
currency=currency,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
return TradeBatch(tuple(_trades))
|
|
126
|
+
|
|
127
|
+
def is_valid(self, ignore_error: bool = False) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Validate the trade batch against a set of default rules. Populate the validated_trades and errors property.
|
|
130
|
+
Ignore error by default
|
|
131
|
+
Args:
|
|
132
|
+
ignore_error: If true, will raise the error. False by default
|
|
133
|
+
|
|
134
|
+
Returns: True if the trades batch is valid
|
|
135
|
+
"""
|
|
136
|
+
if not hasattr(self, "_validated_trades"):
|
|
137
|
+
self._validated_trades = []
|
|
138
|
+
self._errors = []
|
|
139
|
+
# Run validation for every trade. If a trade is not valid, we simply exclude it from the validated trades list
|
|
140
|
+
for _, trade in self.trades_batch.trades_map.items():
|
|
141
|
+
try:
|
|
142
|
+
trade.validate()
|
|
143
|
+
self._validated_trades.append(trade)
|
|
144
|
+
except ValidationError as exc:
|
|
145
|
+
self._errors.append(exc.message)
|
|
146
|
+
try:
|
|
147
|
+
# Check the overall validity of the trade batch. If this fail, we consider all trade invalids
|
|
148
|
+
self.run_validation(self._validated_trades)
|
|
149
|
+
except ValidationError as exc:
|
|
150
|
+
self._validated_trades = []
|
|
151
|
+
self._errors.append(exc.message)
|
|
152
|
+
|
|
153
|
+
if self._errors and not ignore_error:
|
|
154
|
+
raise ValidationError(self.errors)
|
|
155
|
+
|
|
156
|
+
return not bool(self._errors)
|
|
157
|
+
|
|
158
|
+
def normalize(self):
|
|
159
|
+
"""
|
|
160
|
+
Normalize the instantiate trades batch so that the target weight is 100%
|
|
161
|
+
"""
|
|
162
|
+
self.trades_batch = TradeBatch(
|
|
163
|
+
[trade.normalize_target(self.trades_batch.total_target_weight) for trade in self.trades_batch.trades]
|
|
164
|
+
)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from dataclasses import asdict, dataclass, field, fields
|
|
2
|
+
from datetime import date as date_lib
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from django.core.exceptions import ValidationError
|
|
7
|
+
from django.utils.functional import cached_property
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Valuation:
|
|
12
|
+
instrument: int
|
|
13
|
+
net_value: Decimal
|
|
14
|
+
outstanding_shares: Decimal = Decimal(0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Position:
|
|
19
|
+
underlying_instrument: int
|
|
20
|
+
instrument_type: int
|
|
21
|
+
weighting: Decimal
|
|
22
|
+
currency: int
|
|
23
|
+
date: date_lib
|
|
24
|
+
|
|
25
|
+
asset_valuation_date: date_lib | None = None
|
|
26
|
+
portfolio_created: int = None
|
|
27
|
+
exchange: int = None
|
|
28
|
+
is_estimated: bool = False
|
|
29
|
+
country: int = None
|
|
30
|
+
shares: Decimal | None = None
|
|
31
|
+
is_cash: bool = False
|
|
32
|
+
primary_classification: int = None
|
|
33
|
+
favorite_classification: int = None
|
|
34
|
+
market_capitalization_usd: float = None
|
|
35
|
+
currency_fx_rate: float = 1
|
|
36
|
+
market_share: float = None
|
|
37
|
+
daily_liquidity: float = None
|
|
38
|
+
volume_usd: float = None
|
|
39
|
+
price: float = None
|
|
40
|
+
|
|
41
|
+
def __add__(self, other):
|
|
42
|
+
return Position(
|
|
43
|
+
weighting=self.weighting + other.weighting,
|
|
44
|
+
shares=self.shares + other.shares if (self.shares is not None and other.shares is not None) else None,
|
|
45
|
+
**{f.name: getattr(self, f.name) for f in fields(Position) if f.name not in ["weighting", "shares"]},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Portfolio:
|
|
51
|
+
positions: tuple[Position]
|
|
52
|
+
positions_map: dict[Position] = field(init=False, repr=False)
|
|
53
|
+
|
|
54
|
+
def __post_init__(self):
|
|
55
|
+
positions_map = {}
|
|
56
|
+
for pos in self.positions:
|
|
57
|
+
if pos.underlying_instrument in positions_map:
|
|
58
|
+
positions_map[pos.underlying_instrument] += pos
|
|
59
|
+
else:
|
|
60
|
+
positions_map[pos.underlying_instrument] = pos
|
|
61
|
+
object.__setattr__(self, "positions_map", positions_map)
|
|
62
|
+
|
|
63
|
+
@cached_property
|
|
64
|
+
def total_weight(self):
|
|
65
|
+
return round(sum([pos.weighting for pos in self.positions]), 4)
|
|
66
|
+
|
|
67
|
+
@cached_property
|
|
68
|
+
def total_shares(self):
|
|
69
|
+
return sum([pos.target_shares for pos in self.positions if pos.target_shares is not None])
|
|
70
|
+
|
|
71
|
+
def to_df(self):
|
|
72
|
+
return pd.DataFrame([asdict(pos) for pos in self.positions])
|
|
73
|
+
|
|
74
|
+
def __len__(self):
|
|
75
|
+
return len(self.positions)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class Trade:
|
|
80
|
+
underlying_instrument: int
|
|
81
|
+
instrument_type: str
|
|
82
|
+
currency: int
|
|
83
|
+
date: date_lib
|
|
84
|
+
|
|
85
|
+
effective_weight: Decimal
|
|
86
|
+
target_weight: Decimal
|
|
87
|
+
id: int | None = None
|
|
88
|
+
effective_shares: Decimal = None
|
|
89
|
+
target_shares: Decimal = None
|
|
90
|
+
|
|
91
|
+
def __add__(self, other):
|
|
92
|
+
return Trade(
|
|
93
|
+
underlying_instrument=self.underlying_instrument,
|
|
94
|
+
effective_weight=self.effective_weight + other.effective_weight,
|
|
95
|
+
target_weight=self.target_weight + other.target_weight,
|
|
96
|
+
target_shares=self.target_shares + other.target_shares
|
|
97
|
+
if (self.target_shares is not None and other.target_shares is not None)
|
|
98
|
+
else None,
|
|
99
|
+
effective_shares=self.effective_shares + other.effective_shares
|
|
100
|
+
if (self.effective_shares is not None and other.effective_shares is not None)
|
|
101
|
+
else None,
|
|
102
|
+
**{
|
|
103
|
+
f.name: getattr(self, f.name)
|
|
104
|
+
for f in fields(Trade)
|
|
105
|
+
if f.name
|
|
106
|
+
not in [
|
|
107
|
+
"effective_weight",
|
|
108
|
+
"target_weight",
|
|
109
|
+
"target_shares",
|
|
110
|
+
"effective_shares",
|
|
111
|
+
"underlying_instrument",
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@cached_property
|
|
117
|
+
def delta_weight(self) -> Decimal:
|
|
118
|
+
return self.target_weight - self.effective_weight
|
|
119
|
+
|
|
120
|
+
def validate(self):
|
|
121
|
+
if self.effective_weight < 0 or self.effective_weight > 1.0:
|
|
122
|
+
raise ValidationError("Effective Weight needs to be in range [0, 1]")
|
|
123
|
+
if self.target_weight < 0 or self.target_weight > 1.0:
|
|
124
|
+
raise ValidationError("Target Weight needs to be in range [0, 1]")
|
|
125
|
+
|
|
126
|
+
def normalize_target(self, total_target_weight: Decimal):
|
|
127
|
+
t = Trade(
|
|
128
|
+
target_weight=self.target_weight / total_target_weight if total_target_weight else self.target_weight,
|
|
129
|
+
target_shares=self.target_shares / total_target_weight
|
|
130
|
+
if (self.target_shares and total_target_weight)
|
|
131
|
+
else self.target_shares,
|
|
132
|
+
**{
|
|
133
|
+
f.name: getattr(self, f.name)
|
|
134
|
+
for f in fields(Trade)
|
|
135
|
+
if f.name not in ["target_weight", "target_shares"]
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
return t
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class TradeBatch:
|
|
143
|
+
trades: tuple[Trade]
|
|
144
|
+
trades_map: dict[Trade] = field(init=False, repr=False)
|
|
145
|
+
|
|
146
|
+
def __post_init__(self):
|
|
147
|
+
trade_map = {}
|
|
148
|
+
for trade in self.trades:
|
|
149
|
+
if trade.underlying_instrument in trade_map:
|
|
150
|
+
trade_map[trade.underlying_instrument] += trade
|
|
151
|
+
else:
|
|
152
|
+
trade_map[trade.underlying_instrument] = trade
|
|
153
|
+
object.__setattr__(self, "trades_map", trade_map)
|
|
154
|
+
|
|
155
|
+
@cached_property
|
|
156
|
+
def total_target_weight(self) -> Decimal:
|
|
157
|
+
return round(sum([trade.target_weight for trade in self.trades]), 4)
|
|
158
|
+
|
|
159
|
+
@cached_property
|
|
160
|
+
def total_effective_weight(self) -> Decimal:
|
|
161
|
+
return round(sum([trade.effective_weight for trade in self.trades]), 4)
|
|
162
|
+
|
|
163
|
+
@cached_property
|
|
164
|
+
def total_shares(self) -> Decimal:
|
|
165
|
+
return sum([trade.target_shares for trade in self.trades if trade.target_shares is not None]) or Decimal(0)
|
|
166
|
+
|
|
167
|
+
@cached_property
|
|
168
|
+
def totat_abs_delta_weight(self) -> Decimal:
|
|
169
|
+
return sum([abs(trade.delta_weight) for trade in self.trades])
|
|
170
|
+
|
|
171
|
+
def __add__(self, other):
|
|
172
|
+
return TradeBatch(tuple(self.trades + other.trades))
|
|
173
|
+
|
|
174
|
+
def __len__(self):
|
|
175
|
+
return len(self.trades)
|
|
176
|
+
|
|
177
|
+
def validate(self):
|
|
178
|
+
if float(self.total_target_weight) != 1.0: # we do that to remove decimal over precision
|
|
179
|
+
raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
|
|
180
|
+
|
|
181
|
+
def convert_to_portfolio(self):
|
|
182
|
+
positions = []
|
|
183
|
+
for instrument, trade in self.trades_map.items():
|
|
184
|
+
positions.append(
|
|
185
|
+
Position(
|
|
186
|
+
underlying_instrument=trade.underlying_instrument,
|
|
187
|
+
instrument_type=trade.instrument_type,
|
|
188
|
+
weighting=trade.target_weight,
|
|
189
|
+
shares=trade.target_shares,
|
|
190
|
+
currency=trade.currency,
|
|
191
|
+
date=trade.date,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
return Portfolio(tuple(positions))
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pandas.tseries.offsets import BMonthEnd
|
|
7
|
+
from wbfdm.models import InstrumentList
|
|
8
|
+
from wbreport.mixins import ReportMixin
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from wbcore.contrib.authentication.models import User
|
|
12
|
+
from wbreport.models import Report, ReportVersion
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReportClass(ReportMixin):
|
|
16
|
+
@classmethod
|
|
17
|
+
def parse_parameters(cls, parameters: dict[str, str]) -> dict[str, Any]:
|
|
18
|
+
return {
|
|
19
|
+
"end": datetime.strptime(parameters["end"], "%Y-%m-%d").date(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_next_parameters(cls, parameters: dict[str, Any]) -> dict[str, Any]:
|
|
24
|
+
parse_parameters = cls.parse_parameters(parameters)
|
|
25
|
+
return {
|
|
26
|
+
"end": datetime.strftime((parse_parameters["end"] + BMonthEnd(1)).date(), "%Y-%m-%d"),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_version_date(cls, parameters) -> datetime.date:
|
|
31
|
+
parameters = cls.parse_parameters(parameters)
|
|
32
|
+
return parameters["end"]
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_version_title(cls, report_title: str, parameters: dict[str, Any]) -> str:
|
|
36
|
+
parameters = cls.parse_parameters(parameters)
|
|
37
|
+
return f"{report_title} - {parameters['end']:%b}"
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def has_view_permission(cls, report: "Report", user: "User") -> bool:
|
|
41
|
+
return user.has_perms(["wbreport.view_report", "wbportfolio.view_assetposition"])
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def get_context(cls, version: "ReportVersion") -> dict[str, Any]:
|
|
45
|
+
instrument_list: "InstrumentList" = version.report.content_object
|
|
46
|
+
end_date = version.parameters.get("end")
|
|
47
|
+
positions = []
|
|
48
|
+
|
|
49
|
+
for instrument in instrument_list.instruments.all():
|
|
50
|
+
if portfolio := instrument.primary_portfolio:
|
|
51
|
+
for position in portfolio.assets.filter(date=end_date):
|
|
52
|
+
positions.append(
|
|
53
|
+
{
|
|
54
|
+
"portfolio": instrument.name,
|
|
55
|
+
"isin": position.underlying_instrument.isin,
|
|
56
|
+
"title": position.underlying_instrument.name_repr,
|
|
57
|
+
"instrument_type": position.underlying_instrument.security_instrument_type.short_name,
|
|
58
|
+
"weight": float(position.weighting),
|
|
59
|
+
"date": position.date.strftime("%Y-%m-%d"),
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return {"positions": positions}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def generate_file(cls, context: dict[str, Any]) -> BytesIO:
|
|
67
|
+
stream = BytesIO()
|
|
68
|
+
if positions := context.get("positions", None):
|
|
69
|
+
df = pd.DataFrame(positions).sort_values(by=["weight"], ascending=False)
|
|
70
|
+
writer = pd.ExcelWriter(stream, engine="xlsxwriter")
|
|
71
|
+
for portfolio, dff in df.groupby("portfolio"):
|
|
72
|
+
dff.to_excel(writer, sheet_name=portfolio[0:31], index=False)
|
|
73
|
+
writer.save()
|
|
74
|
+
return stream
|
|
File without changes
|