wbportfolio 1.44.4__py2.py3-none-any.whl → 1.45.0__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/admin/__init__.py +1 -1
- wbportfolio/admin/asset.py +2 -1
- wbportfolio/admin/custodians.py +1 -0
- wbportfolio/admin/indexes.py +15 -0
- wbportfolio/admin/portfolio.py +12 -7
- wbportfolio/admin/portfolio_relationships.py +1 -0
- wbportfolio/admin/product_groups.py +2 -0
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/reconciliations.py +1 -0
- wbportfolio/admin/registers.py +1 -0
- wbportfolio/admin/roles.py +1 -0
- wbportfolio/admin/transactions/__init__.py +1 -0
- wbportfolio/admin/transactions/claim.py +1 -0
- wbportfolio/admin/transactions/dividends.py +1 -0
- wbportfolio/admin/transactions/fees.py +1 -0
- wbportfolio/admin/transactions/rebalancing.py +26 -0
- wbportfolio/admin/transactions/trades.py +4 -3
- wbportfolio/admin/transactions/transactions.py +1 -0
- wbportfolio/analysis/claims.py +2 -1
- wbportfolio/contrib/company_portfolio/models.py +3 -6
- wbportfolio/contrib/company_portfolio/tests/conftest.py +0 -12
- wbportfolio/contrib/company_portfolio/tests/test_models.py +1 -0
- wbportfolio/defaults/fees/default.py +1 -0
- wbportfolio/factories/__init__.py +1 -7
- wbportfolio/factories/adjustments.py +1 -0
- wbportfolio/factories/assets.py +13 -7
- wbportfolio/factories/claim.py +1 -0
- wbportfolio/factories/custodians.py +1 -0
- wbportfolio/factories/dividends.py +1 -0
- wbportfolio/factories/fees.py +1 -0
- wbportfolio/factories/indexes.py +1 -0
- wbportfolio/factories/portfolio_cash_flow.py +1 -0
- wbportfolio/factories/portfolio_cash_targets.py +1 -0
- wbportfolio/factories/portfolio_swing_pricings.py +1 -0
- wbportfolio/factories/portfolios.py +3 -0
- wbportfolio/factories/product_groups.py +1 -0
- wbportfolio/factories/products.py +1 -0
- wbportfolio/factories/rebalancing.py +23 -0
- wbportfolio/factories/reconciliations.py +1 -0
- wbportfolio/factories/roles.py +1 -0
- wbportfolio/factories/trades.py +1 -0
- wbportfolio/factories/transactions.py +1 -0
- wbportfolio/fdm/tasks.py +1 -0
- wbportfolio/filters/__init__.py +1 -1
- wbportfolio/filters/assets.py +8 -9
- wbportfolio/filters/assets_and_net_new_money_progression.py +1 -0
- wbportfolio/filters/custodians.py +1 -0
- wbportfolio/filters/esg.py +1 -0
- wbportfolio/filters/performances.py +7 -6
- wbportfolio/filters/portfolios.py +21 -1
- wbportfolio/filters/positions.py +1 -0
- wbportfolio/filters/products.py +1 -0
- wbportfolio/filters/roles.py +1 -0
- wbportfolio/filters/signals.py +1 -0
- wbportfolio/filters/transactions/claim.py +1 -0
- wbportfolio/filters/transactions/fees.py +1 -0
- wbportfolio/filters/transactions/trades.py +2 -1
- wbportfolio/filters/transactions/transactions.py +1 -0
- wbportfolio/import_export/backends/ubs/mixin.py +1 -0
- wbportfolio/import_export/backends/wbfdm/adjustment.py +1 -0
- wbportfolio/import_export/handlers/asset_position.py +11 -13
- wbportfolio/import_export/handlers/fees.py +1 -0
- wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -0
- wbportfolio/import_export/handlers/trade.py +1 -0
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/jpmorgan/fees.py +1 -0
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +5 -4
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +1 -0
- wbportfolio/import_export/parsers/leonteq/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/leonteq/equity.py +13 -12
- wbportfolio/import_export/parsers/leonteq/fees.py +1 -0
- wbportfolio/import_export/parsers/leonteq/trade.py +1 -0
- wbportfolio/import_export/parsers/leonteq/valuation.py +1 -0
- wbportfolio/import_export/parsers/natixis/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_customer_trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_equity.py +3 -2
- wbportfolio/import_export/parsers/natixis/d1_fees.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/d1_valuation.py +1 -0
- wbportfolio/import_export/parsers/natixis/equity.py +5 -5
- wbportfolio/import_export/parsers/natixis/trade.py +1 -0
- wbportfolio/import_export/parsers/natixis/utils.py +8 -7
- wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +2 -1
- wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py +2 -1
- wbportfolio/import_export/parsers/sg_lux/customer_trade_without_pw.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/equity.py +7 -8
- wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -0
- wbportfolio/import_export/parsers/sg_lux/registers.py +2 -1
- wbportfolio/import_export/parsers/societe_generale/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/societe_generale/strategy.py +8 -9
- wbportfolio/import_export/parsers/societe_generale/valuation.py +1 -0
- wbportfolio/import_export/parsers/tellco/equity.py +5 -4
- wbportfolio/import_export/parsers/ubs/api/asset_position.py +15 -14
- wbportfolio/import_export/parsers/ubs/api/fees.py +1 -0
- wbportfolio/import_export/parsers/ubs/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/ubs/equity.py +3 -2
- wbportfolio/import_export/parsers/ubs/historical_customer_trade.py +1 -0
- wbportfolio/import_export/parsers/ubs/valuation.py +1 -0
- wbportfolio/import_export/parsers/vontobel/asset_position.py +19 -19
- wbportfolio/import_export/parsers/vontobel/customer_trade.py +1 -0
- wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +1 -0
- wbportfolio/import_export/parsers/vontobel/management_fees.py +1 -0
- wbportfolio/import_export/parsers/vontobel/performance_fees.py +1 -0
- wbportfolio/import_export/parsers/vontobel/trade.py +1 -0
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +20 -0
- wbportfolio/import_export/resources/assets.py +4 -3
- wbportfolio/import_export/resources/trades.py +1 -0
- wbportfolio/metric/backends/base.py +1 -0
- wbportfolio/metric/backends/portfolio_base.py +1 -0
- wbportfolio/metric/backends/portfolio_esg.py +1 -0
- wbportfolio/metric/tests/test_portfolio_base.py +1 -0
- wbportfolio/migrations/0052_remove_cash_instrument_ptr_and_more.py +1 -131
- wbportfolio/migrations/0067_assetposition_unique_asset_position.py +1 -1
- wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.py +1 -1
- wbportfolio/migrations/0073_remove_product_price_computation_and_more.py +407 -0
- wbportfolio/models/__init__.py +0 -5
- wbportfolio/models/adjustments.py +8 -2
- wbportfolio/models/asset.py +117 -98
- wbportfolio/models/graphs/portfolio.py +144 -0
- wbportfolio/models/graphs/utils.py +83 -0
- wbportfolio/models/indexes.py +2 -13
- wbportfolio/models/mixins/instruments.py +30 -8
- wbportfolio/models/portfolio.py +538 -332
- wbportfolio/models/portfolio_cash_flow.py +1 -0
- wbportfolio/models/portfolio_relationship.py +6 -2
- wbportfolio/models/product_groups.py +3 -2
- wbportfolio/models/products.py +3 -17
- wbportfolio/models/reconciliations/account_reconciliation_lines.py +1 -0
- wbportfolio/models/reconciliations/account_reconciliations.py +1 -0
- wbportfolio/models/registers.py +1 -0
- wbportfolio/models/transactions/__init__.py +1 -0
- wbportfolio/models/transactions/claim.py +8 -8
- wbportfolio/models/transactions/dividends.py +1 -0
- wbportfolio/models/transactions/fees.py +1 -0
- wbportfolio/models/transactions/rebalancing.py +153 -0
- wbportfolio/models/transactions/trade_proposals.py +153 -155
- wbportfolio/models/transactions/trades.py +48 -40
- wbportfolio/models/transactions/transactions.py +6 -12
- wbportfolio/models/utils.py +1 -0
- wbportfolio/pms/analytics/__init__.py +0 -0
- wbportfolio/pms/analytics/portfolio.py +28 -0
- wbportfolio/pms/trading/handler.py +13 -16
- wbportfolio/pms/typing.py +13 -29
- wbportfolio/rebalancing/__init__.py +0 -0
- wbportfolio/rebalancing/base.py +16 -0
- wbportfolio/rebalancing/decorators.py +17 -0
- wbportfolio/rebalancing/models/__init__.py +3 -0
- wbportfolio/rebalancing/models/composite.py +31 -0
- wbportfolio/rebalancing/models/equally_weighted.py +21 -0
- wbportfolio/rebalancing/models/model_portfolio.py +35 -0
- wbportfolio/reports/monthly_position_report.py +1 -1
- wbportfolio/risk_management/backends/accounts.py +7 -6
- wbportfolio/risk_management/backends/controversy_portfolio.py +1 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +1 -0
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +1 -0
- wbportfolio/risk_management/backends/liquidity_risk.py +1 -0
- wbportfolio/risk_management/backends/liquidity_stress_instrument.py +1 -0
- wbportfolio/risk_management/backends/mixins.py +1 -0
- wbportfolio/risk_management/backends/product_integrity.py +6 -1
- wbportfolio/risk_management/backends/stop_loss_instrument.py +1 -0
- wbportfolio/risk_management/backends/stop_loss_portfolio.py +1 -0
- wbportfolio/risk_management/backends/ucits_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_accounts.py +1 -0
- wbportfolio/risk_management/tests/test_controversy_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_instrument_list_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_liquidity_risk.py +1 -0
- wbportfolio/risk_management/tests/test_product_integrity.py +1 -0
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +1 -0
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -0
- wbportfolio/risk_management/tests/test_ucits_portfolio.py +1 -0
- wbportfolio/serializers/__init__.py +5 -5
- wbportfolio/serializers/adjustments.py +1 -0
- wbportfolio/serializers/assets.py +18 -19
- wbportfolio/serializers/custodians.py +1 -0
- wbportfolio/serializers/portfolio_cash_flow.py +1 -0
- wbportfolio/serializers/portfolio_cash_targets.py +1 -0
- wbportfolio/serializers/portfolio_relationship.py +1 -0
- wbportfolio/serializers/portfolio_swing_pricing.py +1 -0
- wbportfolio/serializers/portfolios.py +61 -40
- wbportfolio/serializers/positions.py +1 -0
- wbportfolio/serializers/product_group.py +1 -0
- wbportfolio/serializers/products.py +4 -7
- wbportfolio/serializers/rebalancing.py +57 -0
- wbportfolio/serializers/reconciliations.py +2 -1
- wbportfolio/serializers/registers.py +1 -0
- wbportfolio/serializers/roles.py +1 -0
- wbportfolio/serializers/signals.py +10 -15
- wbportfolio/serializers/transactions/__init__.py +1 -1
- wbportfolio/serializers/transactions/claim.py +1 -0
- wbportfolio/serializers/transactions/fees.py +1 -0
- wbportfolio/serializers/transactions/trade_proposals.py +85 -0
- wbportfolio/serializers/transactions/trades.py +9 -51
- wbportfolio/serializers/transactions/transactions.py +4 -3
- wbportfolio/tasks.py +1 -78
- wbportfolio/tests/conftest.py +6 -13
- wbportfolio/tests/models/test_account_reconciliation.py +2 -0
- wbportfolio/tests/models/test_assets.py +27 -19
- wbportfolio/tests/models/test_customer_trades.py +1 -0
- wbportfolio/tests/models/test_imports.py +5 -1
- wbportfolio/tests/models/test_merge.py +5 -4
- wbportfolio/tests/models/test_portfolio_cash_flow.py +8 -6
- wbportfolio/tests/models/test_portfolios.py +594 -154
- wbportfolio/tests/models/test_product_groups.py +1 -0
- wbportfolio/tests/models/test_products.py +6 -3
- wbportfolio/tests/models/test_roles.py +1 -0
- wbportfolio/tests/models/test_splits.py +1 -0
- wbportfolio/tests/models/transactions/test_claim.py +1 -0
- wbportfolio/tests/models/transactions/test_fees.py +1 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +81 -0
- wbportfolio/tests/models/transactions/test_trades.py +1 -0
- wbportfolio/tests/models/utils.py +1 -0
- wbportfolio/tests/pms/__init__.py +0 -0
- wbportfolio/tests/pms/test_analytics.py +35 -0
- wbportfolio/tests/rebalancing/__init__.py +0 -0
- wbportfolio/tests/rebalancing/test_models.py +127 -0
- wbportfolio/tests/serializers/test_claims.py +1 -0
- wbportfolio/tests/signals.py +1 -7
- wbportfolio/tests/tests.py +2 -0
- wbportfolio/tests/viewsets/test_assets.py +1 -0
- wbportfolio/tests/viewsets/test_performances.py +1 -0
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/tests/viewsets/transactions/test_claims.py +1 -0
- wbportfolio/urls.py +26 -12
- wbportfolio/viewsets/__init__.py +2 -5
- wbportfolio/viewsets/adjustments.py +1 -0
- wbportfolio/viewsets/assets.py +62 -51
- wbportfolio/viewsets/assets_and_net_new_money_progression.py +1 -0
- wbportfolio/viewsets/charts/assets.py +3 -1
- wbportfolio/viewsets/configs/buttons/__init__.py +1 -1
- wbportfolio/viewsets/configs/buttons/assets.py +1 -0
- wbportfolio/viewsets/configs/buttons/custodians.py +1 -0
- wbportfolio/viewsets/configs/buttons/mixins.py +1 -20
- wbportfolio/viewsets/configs/buttons/portfolios.py +90 -76
- wbportfolio/viewsets/configs/buttons/signals.py +1 -0
- wbportfolio/viewsets/configs/buttons/trades.py +1 -0
- wbportfolio/viewsets/configs/display/__init__.py +2 -1
- wbportfolio/viewsets/configs/display/adjustments.py +1 -0
- wbportfolio/viewsets/configs/display/assets.py +7 -6
- wbportfolio/viewsets/configs/display/claim.py +1 -0
- wbportfolio/viewsets/configs/display/portfolios.py +127 -79
- wbportfolio/viewsets/configs/display/product_performance.py +1 -0
- wbportfolio/viewsets/configs/display/rebalancing.py +27 -0
- wbportfolio/viewsets/configs/display/trade_proposals.py +7 -4
- wbportfolio/viewsets/configs/display/trades.py +75 -42
- wbportfolio/viewsets/configs/endpoints/__init__.py +3 -1
- wbportfolio/viewsets/configs/endpoints/claim.py +1 -0
- wbportfolio/viewsets/configs/endpoints/portfolios.py +23 -7
- wbportfolio/viewsets/configs/endpoints/rebalancing.py +6 -0
- wbportfolio/viewsets/configs/endpoints/reconciliations.py +1 -0
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +1 -0
- wbportfolio/viewsets/configs/endpoints/trades.py +1 -0
- wbportfolio/viewsets/configs/menu/adjustments.py +1 -0
- wbportfolio/viewsets/configs/menu/assets.py +1 -0
- wbportfolio/viewsets/configs/menu/fees.py +1 -0
- wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py +1 -0
- wbportfolio/viewsets/configs/menu/portfolios.py +4 -2
- wbportfolio/viewsets/configs/menu/positions.py +1 -0
- wbportfolio/viewsets/configs/menu/roles.py +1 -0
- wbportfolio/viewsets/configs/menu/transactions.py +1 -0
- wbportfolio/viewsets/configs/previews/portfolios.py +1 -6
- wbportfolio/viewsets/configs/titles/__init__.py +1 -1
- wbportfolio/viewsets/configs/titles/assets.py +1 -0
- wbportfolio/viewsets/configs/titles/fees.py +1 -0
- wbportfolio/viewsets/configs/titles/instrument_prices.py +1 -0
- wbportfolio/viewsets/configs/titles/portfolios.py +13 -11
- wbportfolio/viewsets/configs/titles/roles.py +1 -0
- wbportfolio/viewsets/configs/titles/trades.py +1 -0
- wbportfolio/viewsets/configs/titles/transactions.py +1 -0
- wbportfolio/viewsets/custodians.py +1 -0
- wbportfolio/viewsets/esg.py +1 -0
- wbportfolio/viewsets/mixins.py +1 -0
- wbportfolio/viewsets/portfolio_cash_flow.py +1 -0
- wbportfolio/viewsets/portfolio_cash_targets.py +1 -0
- wbportfolio/viewsets/portfolio_relationship.py +1 -0
- wbportfolio/viewsets/portfolio_swing_pricing.py +1 -0
- wbportfolio/viewsets/portfolios.py +228 -61
- wbportfolio/viewsets/positions.py +3 -2
- wbportfolio/viewsets/product_groups.py +1 -0
- wbportfolio/viewsets/product_performance.py +1 -0
- wbportfolio/viewsets/products.py +1 -0
- wbportfolio/viewsets/reconciliations.py +1 -0
- wbportfolio/viewsets/registers.py +1 -0
- wbportfolio/viewsets/roles.py +1 -0
- wbportfolio/viewsets/signals.py +1 -0
- wbportfolio/viewsets/transactions/__init__.py +1 -0
- wbportfolio/viewsets/transactions/claim.py +2 -1
- wbportfolio/viewsets/transactions/fees.py +1 -0
- wbportfolio/viewsets/transactions/mixins.py +1 -0
- wbportfolio/viewsets/transactions/rebalancing.py +31 -0
- wbportfolio/viewsets/transactions/trade_proposals.py +25 -5
- wbportfolio/viewsets/transactions/trades.py +16 -9
- wbportfolio/viewsets/transactions/transactions.py +1 -0
- {wbportfolio-1.44.4.dist-info → wbportfolio-1.45.0.dist-info}/METADATA +4 -1
- {wbportfolio-1.44.4.dist-info → wbportfolio-1.45.0.dist-info}/RECORD +301 -288
- wbportfolio/admin/synchronization/__init__.py +0 -2
- wbportfolio/admin/synchronization/admin.py +0 -114
- wbportfolio/admin/synchronization/portfolio_synchronization.py +0 -18
- wbportfolio/admin/synchronization/price_computation.py +0 -21
- wbportfolio/defaults/portfolio/default_rebalancing.py +0 -45
- wbportfolio/factories/pytest_utils.py +0 -121
- wbportfolio/factories/synchronization.py +0 -40
- wbportfolio/models/synchronization/__init__.py +0 -3
- wbportfolio/models/synchronization/portfolio_synchronization.py +0 -292
- wbportfolio/models/synchronization/price_computation.py +0 -200
- wbportfolio/models/synchronization/synchronization.py +0 -188
- wbportfolio/serializers/synchronization.py +0 -18
- wbportfolio/tests/models/test_synchronization.py +0 -617
- wbportfolio/viewsets/synchronization.py +0 -25
- /wbportfolio/{defaults/portfolio → models/graphs}/__init__.py +0 -0
- {wbportfolio-1.44.4.dist-info → wbportfolio-1.45.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.44.4.dist-info → wbportfolio-1.45.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -28,6 +28,7 @@ from wbcore.metadata.configs.buttons import ActionButton
|
|
|
28
28
|
from wbcore.models import WBModel
|
|
29
29
|
from wbcore.signals.models import pre_collection
|
|
30
30
|
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
31
|
+
|
|
31
32
|
from wbportfolio.import_export.handlers.trade import TradeImportHandler
|
|
32
33
|
from wbportfolio.models.asset import AssetPosition
|
|
33
34
|
from wbportfolio.models.custodians import Custodian
|
|
@@ -42,8 +43,7 @@ class TradeQueryset(OrderedModelQuerySet):
|
|
|
42
43
|
return self.annotate(
|
|
43
44
|
last_effective_date=Subquery(
|
|
44
45
|
AssetPosition.objects.filter(
|
|
45
|
-
|
|
46
|
-
date__lt=OuterRef("transaction_date"),
|
|
46
|
+
date__lte=OuterRef("value_date"),
|
|
47
47
|
portfolio=OuterRef("portfolio"),
|
|
48
48
|
)
|
|
49
49
|
.order_by("-date")
|
|
@@ -52,7 +52,7 @@ class TradeQueryset(OrderedModelQuerySet):
|
|
|
52
52
|
effective_weight=Coalesce(
|
|
53
53
|
Subquery(
|
|
54
54
|
AssetPosition.objects.filter(
|
|
55
|
-
|
|
55
|
+
underlying_quote=OuterRef("underlying_instrument"),
|
|
56
56
|
date=OuterRef("last_effective_date"),
|
|
57
57
|
portfolio=OuterRef("portfolio"),
|
|
58
58
|
)
|
|
@@ -66,7 +66,7 @@ class TradeQueryset(OrderedModelQuerySet):
|
|
|
66
66
|
effective_shares=Coalesce(
|
|
67
67
|
Subquery(
|
|
68
68
|
AssetPosition.objects.filter(
|
|
69
|
-
|
|
69
|
+
underlying_quote=OuterRef("underlying_instrument"),
|
|
70
70
|
date=OuterRef("last_effective_date"),
|
|
71
71
|
portfolio=OuterRef("portfolio"),
|
|
72
72
|
)
|
|
@@ -76,7 +76,7 @@ class TradeQueryset(OrderedModelQuerySet):
|
|
|
76
76
|
),
|
|
77
77
|
Decimal(0),
|
|
78
78
|
),
|
|
79
|
-
target_shares=F("
|
|
79
|
+
target_shares=F("effective_shares") + F("shares"),
|
|
80
80
|
)
|
|
81
81
|
|
|
82
82
|
|
|
@@ -222,7 +222,12 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
222
222
|
@transition(
|
|
223
223
|
field=status,
|
|
224
224
|
source=Status.DRAFT,
|
|
225
|
-
target=
|
|
225
|
+
target=GET_STATE(
|
|
226
|
+
lambda self, **kwargs: (
|
|
227
|
+
self.Status.SUBMIT if self.underlying_quote_price is not None else self.Status.FAILED
|
|
228
|
+
),
|
|
229
|
+
states=[Status.SUBMIT, Status.FAILED],
|
|
230
|
+
),
|
|
226
231
|
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
227
232
|
user.profile, portfolio=instance.portfolio
|
|
228
233
|
),
|
|
@@ -237,40 +242,44 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
237
242
|
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
238
243
|
)
|
|
239
244
|
},
|
|
245
|
+
on_error="FAILED",
|
|
240
246
|
)
|
|
241
247
|
def submit(self, by=None, description=None, **kwargs):
|
|
242
|
-
|
|
248
|
+
if not self.underlying_quote_price:
|
|
249
|
+
self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
|
|
243
250
|
|
|
244
251
|
def can_submit(self):
|
|
245
252
|
pass
|
|
246
253
|
|
|
247
254
|
@transition(
|
|
248
255
|
field=status,
|
|
249
|
-
source=Status.
|
|
256
|
+
source=Status.DRAFT,
|
|
250
257
|
target=Status.FAILED,
|
|
251
258
|
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
252
259
|
user.profile, portfolio=instance.portfolio
|
|
253
260
|
),
|
|
254
261
|
)
|
|
255
262
|
def fail(self, **kwargs):
|
|
256
|
-
|
|
263
|
+
self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
|
|
257
264
|
|
|
258
265
|
@cached_property
|
|
259
|
-
def
|
|
266
|
+
def underlying_quote_price(self) -> InstrumentPrice | None:
|
|
260
267
|
try:
|
|
261
|
-
return
|
|
268
|
+
return InstrumentPrice.objects.filter_only_valid_prices().get(
|
|
269
|
+
instrument=self.underlying_instrument, date=self.value_date
|
|
270
|
+
)
|
|
262
271
|
except InstrumentPrice.DoesNotExist:
|
|
263
|
-
|
|
272
|
+
with suppress(InstrumentPrice.DoesNotExist):
|
|
273
|
+
return (
|
|
274
|
+
InstrumentPrice.objects.filter_only_valid_prices()
|
|
275
|
+
.filter(instrument=self.underlying_instrument, date__lte=self.value_date)
|
|
276
|
+
.latest("date")
|
|
277
|
+
)
|
|
264
278
|
|
|
265
279
|
@transition(
|
|
266
280
|
field=status,
|
|
267
281
|
source=Status.SUBMIT,
|
|
268
|
-
target=
|
|
269
|
-
lambda self, **kwargs: (
|
|
270
|
-
self.Status.EXECUTED if self.underlying_instrument_price is not None else self.Status.FAILED
|
|
271
|
-
),
|
|
272
|
-
states=[Status.EXECUTED, Status.FAILED],
|
|
273
|
-
),
|
|
282
|
+
target=Status.EXECUTED,
|
|
274
283
|
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
275
284
|
user.profile, portfolio=instance.portfolio
|
|
276
285
|
),
|
|
@@ -285,30 +294,30 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
285
294
|
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
286
295
|
)
|
|
287
296
|
},
|
|
288
|
-
on_error="FAILED",
|
|
289
297
|
)
|
|
290
298
|
def execute(self, **kwargs):
|
|
291
|
-
if self.
|
|
299
|
+
if self.underlying_quote_price:
|
|
292
300
|
asset, created = AssetPosition.unannotated_objects.update_or_create(
|
|
293
|
-
|
|
301
|
+
underlying_quote=self.underlying_instrument,
|
|
302
|
+
portfolio_created=None,
|
|
294
303
|
portfolio=self.portfolio,
|
|
295
304
|
date=self.transaction_date,
|
|
296
|
-
is_estimated=False,
|
|
297
305
|
defaults={
|
|
298
306
|
"initial_currency_fx_rate": self.currency_fx_rate,
|
|
299
307
|
"weighting": self._target_weight,
|
|
300
|
-
"initial_price": self.
|
|
308
|
+
"initial_price": self.underlying_quote_price.net_value,
|
|
301
309
|
"initial_shares": None,
|
|
302
|
-
"
|
|
310
|
+
"underlying_quote_price": self.underlying_quote_price,
|
|
303
311
|
"asset_valuation_date": self.transaction_date,
|
|
304
312
|
"currency": self.currency,
|
|
313
|
+
"is_estimated": False,
|
|
305
314
|
},
|
|
306
315
|
)
|
|
307
316
|
asset.set_weighting(self._target_weight)
|
|
308
|
-
else:
|
|
309
|
-
self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
|
|
310
317
|
|
|
311
318
|
def can_execute(self):
|
|
319
|
+
if not self.underlying_quote_price:
|
|
320
|
+
return {"underlying_instrument": "Cannot execute a trade without a valid quote price"}
|
|
312
321
|
if not self.portfolio.is_manageable:
|
|
313
322
|
return {"portfolio": "The portfolio needs to be a model portfolio in order to execute this trade manually"}
|
|
314
323
|
|
|
@@ -381,7 +390,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
381
390
|
def revert(self, to_date=None, **kwargs):
|
|
382
391
|
with suppress(AssetPosition.DoesNotExist):
|
|
383
392
|
asset = AssetPosition.objects.get(
|
|
384
|
-
|
|
393
|
+
underlying_quote=self.underlying_instrument,
|
|
385
394
|
portfolio=self.portfolio,
|
|
386
395
|
date=self.transaction_date,
|
|
387
396
|
is_estimated=False,
|
|
@@ -404,7 +413,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
404
413
|
return self.last_effective_date
|
|
405
414
|
elif (
|
|
406
415
|
assets := AssetPosition.objects.filter(
|
|
407
|
-
|
|
416
|
+
underlying_quote=self.underlying_instrument,
|
|
408
417
|
date__lt=self.transaction_date,
|
|
409
418
|
portfolio=self.portfolio,
|
|
410
419
|
)
|
|
@@ -418,7 +427,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
418
427
|
self,
|
|
419
428
|
"effective_weight",
|
|
420
429
|
AssetPosition.objects.filter(
|
|
421
|
-
|
|
430
|
+
underlying_quote=self.underlying_instrument,
|
|
422
431
|
date=self._last_effective_date,
|
|
423
432
|
portfolio=self.portfolio,
|
|
424
433
|
).aggregate(s=Sum("weighting"))["s"]
|
|
@@ -432,7 +441,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
432
441
|
self,
|
|
433
442
|
"effective_shares",
|
|
434
443
|
AssetPosition.objects.filter(
|
|
435
|
-
|
|
444
|
+
underlying_quote=self.underlying_instrument,
|
|
436
445
|
date=self.transaction_date,
|
|
437
446
|
portfolio=self.portfolio,
|
|
438
447
|
).aggregate(s=Sum("shares"))["s"]
|
|
@@ -447,7 +456,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
447
456
|
@cached_property
|
|
448
457
|
@admin.display(description="Target Shares")
|
|
449
458
|
def _target_shares(self) -> Decimal:
|
|
450
|
-
return getattr(self, "target_shares", self._effective_shares
|
|
459
|
+
return getattr(self, "target_shares", self._effective_shares + self.shares)
|
|
451
460
|
|
|
452
461
|
order_with_respect_to = "trade_proposal"
|
|
453
462
|
|
|
@@ -475,12 +484,19 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
475
484
|
# notification_email_template = "portfolio/email/trade_notification.html"
|
|
476
485
|
|
|
477
486
|
def save(self, *args, **kwargs):
|
|
487
|
+
if self.trade_proposal:
|
|
488
|
+
self.portfolio = self.trade_proposal.portfolio
|
|
489
|
+
self.transaction_date = self.trade_proposal.trade_date
|
|
490
|
+
self.value_date = self.trade_proposal.last_effective_date
|
|
491
|
+
if self._effective_shares:
|
|
492
|
+
self.shares = self._effective_shares * self.weighting
|
|
493
|
+
|
|
478
494
|
if not self.custodian and self.bank:
|
|
479
495
|
self.custodian = Custodian.get_by_mapping(self.bank)
|
|
480
496
|
if self.price is None:
|
|
481
497
|
# we try to get the price if not provided directly from the underlying instrument
|
|
482
498
|
with suppress(InstrumentPrice.DoesNotExist):
|
|
483
|
-
self.price = self.underlying_instrument.valuations.get(date=self.
|
|
499
|
+
self.price = self.underlying_instrument.valuations.get(date=self.value_date).net_value
|
|
484
500
|
if self.price is not None and self.price_gross is None:
|
|
485
501
|
self.price_gross = self.price
|
|
486
502
|
|
|
@@ -489,12 +505,6 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
489
505
|
|
|
490
506
|
if self.price_gross is not None and self.shares is not None and self.total_value_gross is None:
|
|
491
507
|
self.total_value_gross = self.price_gross * self.shares
|
|
492
|
-
|
|
493
|
-
if self.trade_proposal:
|
|
494
|
-
self.portfolio = self.trade_proposal.portfolio
|
|
495
|
-
self.transaction_date = self.trade_proposal.trade_date
|
|
496
|
-
if effective_shares := self._effective_shares:
|
|
497
|
-
self.shares = effective_shares * self.weighting
|
|
498
508
|
self.transaction_type = Transaction.Type.TRADE
|
|
499
509
|
|
|
500
510
|
if self.transaction_subtype is None:
|
|
@@ -516,8 +526,6 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
516
526
|
self.claimed_shares = self.trade.claims.filter(status="APPROVED").aggregate(s=Sum("shares"))[
|
|
517
527
|
"s"
|
|
518
528
|
] or Decimal(0)
|
|
519
|
-
if self.trade_proposal and self.trade_proposal.status == "DRAFT":
|
|
520
|
-
self.status = self.Status.DRAFT
|
|
521
529
|
if self.internal_trade:
|
|
522
530
|
self.marked_as_internal = True
|
|
523
531
|
super().save(*args, **kwargs)
|
|
@@ -111,25 +111,19 @@ class Transaction(ImportMixin, models.Model):
|
|
|
111
111
|
comment = models.TextField(default="", verbose_name="Comment", blank=True)
|
|
112
112
|
|
|
113
113
|
def save(self, *args, **kwargs):
|
|
114
|
+
if not self.value_date:
|
|
115
|
+
self.value_date = self.transaction_date
|
|
116
|
+
if not self.book_date:
|
|
117
|
+
self.book_date = self.transaction_date
|
|
118
|
+
|
|
114
119
|
if not getattr(self, "currency", None) and self.underlying_instrument:
|
|
115
120
|
self.currency = self.underlying_instrument.currency
|
|
116
121
|
if not self.currency_fx_rate:
|
|
117
122
|
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
118
|
-
self.
|
|
123
|
+
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
119
124
|
)
|
|
120
125
|
if not self.transaction_type:
|
|
121
126
|
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
127
|
if (
|
|
134
128
|
self.total_value is not None
|
|
135
129
|
and self.currency_fx_rate is not None
|
wbportfolio/models/utils.py
CHANGED
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from skfolio import Portfolio as BasePortfolio
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Portfolio(BasePortfolio):
|
|
6
|
+
def get_next_weights(self, returns: pd.Series) -> dict[int, float]:
|
|
7
|
+
"""
|
|
8
|
+
Given the next returns, compute the next weights of this portfolio
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
returns: The returns for the next day as a pandas series
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
A dictionary of weights (instrument ids as keys and weights as values)
|
|
15
|
+
"""
|
|
16
|
+
weights = self.weights_per_observation.iloc[-1, :].T
|
|
17
|
+
if weights.sum() != 0:
|
|
18
|
+
weights /= weights.sum()
|
|
19
|
+
contribution = weights * (returns + 1.0)
|
|
20
|
+
if contribution.sum() != 0:
|
|
21
|
+
contribution /= contribution.sum()
|
|
22
|
+
return contribution.dropna().to_dict()
|
|
23
|
+
|
|
24
|
+
def get_estimate_net_value(self, previous_net_asset_value: float) -> float:
|
|
25
|
+
if self.previous_weights is None:
|
|
26
|
+
raise ValueError("No previous weights available")
|
|
27
|
+
expected_returns = self.previous_weights @ self.X.iloc[-1, :].T
|
|
28
|
+
return previous_net_asset_value * (1.0 + expected_returns)
|
|
@@ -2,6 +2,7 @@ from datetime import date
|
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
|
|
4
4
|
from django.core.exceptions import ValidationError
|
|
5
|
+
|
|
5
6
|
from wbportfolio.pms.typing import Portfolio, Trade, TradeBatch
|
|
6
7
|
|
|
7
8
|
|
|
@@ -19,17 +20,15 @@ class TradingService:
|
|
|
19
20
|
trades_batch: TradeBatch | None = None,
|
|
20
21
|
total_value: Decimal = None,
|
|
21
22
|
):
|
|
22
|
-
if not target_portfolio and not trades_batch:
|
|
23
|
-
raise ValueError("Either target positions or trades needs to be provided")
|
|
24
23
|
self.total_value = total_value
|
|
25
24
|
self.trade_date = trade_date
|
|
25
|
+
if target_portfolio is None:
|
|
26
|
+
target_portfolio = Portfolio(positions=())
|
|
27
|
+
if effective_portfolio is None:
|
|
28
|
+
effective_portfolio = Portfolio(positions=())
|
|
26
29
|
# If effective portfoolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
|
|
27
|
-
|
|
28
|
-
trades_batch = self.build_trade_batch(effective_portfolio, trades_batch=trades_batch)
|
|
30
|
+
trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio, trades_batch=trades_batch)
|
|
29
31
|
# 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
32
|
# Finally, we compute the target portfolio
|
|
34
33
|
if trades_batch and not target_portfolio:
|
|
35
34
|
target_portfolio = trades_batch.convert_to_portfolio()
|
|
@@ -75,7 +74,7 @@ class TradingService:
|
|
|
75
74
|
def build_trade_batch(
|
|
76
75
|
self,
|
|
77
76
|
effective_portfolio: Portfolio,
|
|
78
|
-
target_portfolio: Portfolio
|
|
77
|
+
target_portfolio: Portfolio,
|
|
79
78
|
trades_batch: TradeBatch | None = None,
|
|
80
79
|
) -> TradeBatch:
|
|
81
80
|
"""
|
|
@@ -89,25 +88,24 @@ class TradingService:
|
|
|
89
88
|
Returns: The normalized trades batch
|
|
90
89
|
"""
|
|
91
90
|
instruments = list(effective_portfolio.positions_map.keys())
|
|
92
|
-
|
|
93
|
-
instruments.extend(list(target_portfolio.positions_map.keys()))
|
|
91
|
+
instruments.extend(list(target_portfolio.positions_map.keys()))
|
|
94
92
|
if trades_batch:
|
|
95
93
|
instruments.extend(list(trades_batch.trades_map.keys()))
|
|
96
94
|
_trades: list[Trade] = []
|
|
97
95
|
for instrument in set(instruments):
|
|
98
96
|
effective_weight = target_weight = 0
|
|
99
|
-
effective_shares =
|
|
97
|
+
effective_shares = 0
|
|
100
98
|
instrument_type = currency = None
|
|
101
99
|
if effective_pos := effective_portfolio.positions_map.get(instrument, None):
|
|
102
100
|
effective_weight = target_weight = effective_pos.weighting
|
|
103
|
-
effective_shares =
|
|
101
|
+
effective_shares = effective_pos.shares
|
|
104
102
|
instrument_type, currency = effective_pos.instrument_type, effective_pos.currency
|
|
105
|
-
if
|
|
103
|
+
if target_pos := target_portfolio.positions_map.get(instrument, None):
|
|
106
104
|
target_weight = target_pos.weighting
|
|
107
|
-
|
|
105
|
+
instrument_type, currency = target_pos.instrument_type, target_pos.currency
|
|
108
106
|
if trades_batch and (trade := trades_batch.trades_map.get(instrument, None)):
|
|
109
107
|
effective_weight, target_weight = trade.effective_weight, trade.target_weight
|
|
110
|
-
effective_shares
|
|
108
|
+
effective_shares = trade.effective_shares
|
|
111
109
|
instrument_type, currency = trade.instrument_type, trade.currency
|
|
112
110
|
|
|
113
111
|
_trades.append(
|
|
@@ -116,7 +114,6 @@ class TradingService:
|
|
|
116
114
|
effective_weight=effective_weight,
|
|
117
115
|
target_weight=target_weight,
|
|
118
116
|
effective_shares=effective_shares,
|
|
119
|
-
target_shares=target_shares,
|
|
120
117
|
date=self.trade_date,
|
|
121
118
|
instrument_type=instrument_type,
|
|
122
119
|
currency=currency,
|
wbportfolio/pms/typing.py
CHANGED
|
@@ -17,11 +17,11 @@ class Valuation:
|
|
|
17
17
|
@dataclass(frozen=True)
|
|
18
18
|
class Position:
|
|
19
19
|
underlying_instrument: int
|
|
20
|
-
instrument_type: int
|
|
21
20
|
weighting: Decimal
|
|
22
|
-
currency: int
|
|
23
21
|
date: date_lib
|
|
24
22
|
|
|
23
|
+
currency: int | None = None
|
|
24
|
+
instrument_type: int | None = None
|
|
25
25
|
asset_valuation_date: date_lib | None = None
|
|
26
26
|
portfolio_created: int = None
|
|
27
27
|
exchange: int = None
|
|
@@ -48,7 +48,7 @@ class Position:
|
|
|
48
48
|
|
|
49
49
|
@dataclass(frozen=True)
|
|
50
50
|
class Portfolio:
|
|
51
|
-
positions: tuple[Position]
|
|
51
|
+
positions: tuple[Position] | tuple
|
|
52
52
|
positions_map: dict[Position] = field(init=False, repr=False)
|
|
53
53
|
|
|
54
54
|
def __post_init__(self):
|
|
@@ -62,7 +62,7 @@ class Portfolio:
|
|
|
62
62
|
|
|
63
63
|
@cached_property
|
|
64
64
|
def total_weight(self):
|
|
65
|
-
return round(sum([pos.weighting for pos in self.positions]),
|
|
65
|
+
return round(sum([pos.weighting for pos in self.positions]), 6)
|
|
66
66
|
|
|
67
67
|
@cached_property
|
|
68
68
|
def total_shares(self):
|
|
@@ -86,16 +86,12 @@ class Trade:
|
|
|
86
86
|
target_weight: Decimal
|
|
87
87
|
id: int | None = None
|
|
88
88
|
effective_shares: Decimal = None
|
|
89
|
-
target_shares: Decimal = None
|
|
90
89
|
|
|
91
90
|
def __add__(self, other):
|
|
92
91
|
return Trade(
|
|
93
92
|
underlying_instrument=self.underlying_instrument,
|
|
94
93
|
effective_weight=self.effective_weight + other.effective_weight,
|
|
95
94
|
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
95
|
effective_shares=self.effective_shares + other.effective_shares
|
|
100
96
|
if (self.effective_shares is not None and other.effective_shares is not None)
|
|
101
97
|
else None,
|
|
@@ -106,7 +102,6 @@ class Trade:
|
|
|
106
102
|
not in [
|
|
107
103
|
"effective_weight",
|
|
108
104
|
"target_weight",
|
|
109
|
-
"target_shares",
|
|
110
105
|
"effective_shares",
|
|
111
106
|
"underlying_instrument",
|
|
112
107
|
]
|
|
@@ -118,22 +113,16 @@ class Trade:
|
|
|
118
113
|
return self.target_weight - self.effective_weight
|
|
119
114
|
|
|
120
115
|
def validate(self):
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
return True
|
|
117
|
+
# if self.effective_weight < 0 or self.effective_weight > 1.0:
|
|
118
|
+
# raise ValidationError("Effective Weight needs to be in range [0, 1]")
|
|
119
|
+
# if self.target_weight < 0 or self.target_weight > 1.0:
|
|
120
|
+
# raise ValidationError("Target Weight needs to be in range [0, 1]")
|
|
125
121
|
|
|
126
122
|
def normalize_target(self, total_target_weight: Decimal):
|
|
127
123
|
t = Trade(
|
|
128
124
|
target_weight=self.target_weight / total_target_weight if total_target_weight else self.target_weight,
|
|
129
|
-
|
|
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
|
-
},
|
|
125
|
+
**{f.name: getattr(self, f.name) for f in fields(Trade) if f.name not in ["target_weight"]},
|
|
137
126
|
)
|
|
138
127
|
return t
|
|
139
128
|
|
|
@@ -154,15 +143,11 @@ class TradeBatch:
|
|
|
154
143
|
|
|
155
144
|
@cached_property
|
|
156
145
|
def total_target_weight(self) -> Decimal:
|
|
157
|
-
return round(sum([trade.target_weight for trade in self.trades]),
|
|
146
|
+
return round(sum([trade.target_weight for trade in self.trades]), 6)
|
|
158
147
|
|
|
159
148
|
@cached_property
|
|
160
149
|
def total_effective_weight(self) -> Decimal:
|
|
161
|
-
return round(sum([trade.effective_weight for trade in self.trades]),
|
|
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)
|
|
150
|
+
return round(sum([trade.effective_weight for trade in self.trades]), 6)
|
|
166
151
|
|
|
167
152
|
@cached_property
|
|
168
153
|
def totat_abs_delta_weight(self) -> Decimal:
|
|
@@ -175,7 +160,7 @@ class TradeBatch:
|
|
|
175
160
|
return len(self.trades)
|
|
176
161
|
|
|
177
162
|
def validate(self):
|
|
178
|
-
if float(self.total_target_weight) != 1
|
|
163
|
+
if round(float(self.total_target_weight), 4) != 1: # we do that to remove decimal over precision
|
|
179
164
|
raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
|
|
180
165
|
|
|
181
166
|
def convert_to_portfolio(self):
|
|
@@ -186,7 +171,6 @@ class TradeBatch:
|
|
|
186
171
|
underlying_instrument=trade.underlying_instrument,
|
|
187
172
|
instrument_type=trade.instrument_type,
|
|
188
173
|
weighting=trade.target_weight,
|
|
189
|
-
shares=trade.target_shares,
|
|
190
174
|
currency=trade.currency,
|
|
191
175
|
date=trade.date,
|
|
192
176
|
)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractRebalancingModel:
|
|
7
|
+
def __init__(self, portfolio, trade_date: date, last_effective_date: date):
|
|
8
|
+
self.portfolio = portfolio
|
|
9
|
+
self.trade_date = trade_date
|
|
10
|
+
self.last_effective_date = last_effective_date
|
|
11
|
+
|
|
12
|
+
def is_valid(self) -> bool:
|
|
13
|
+
return True
|
|
14
|
+
|
|
15
|
+
def get_target_portfolio(self, **kwargs) -> PortfolioDTO:
|
|
16
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
def register(model_name: str):
|
|
2
|
+
"""
|
|
3
|
+
Decorator to include when a backend need automatic registration
|
|
4
|
+
"""
|
|
5
|
+
from wbportfolio.models.transactions.rebalancing import RebalancingModel
|
|
6
|
+
|
|
7
|
+
def _decorator(backend_class):
|
|
8
|
+
defaults = {
|
|
9
|
+
"name": model_name,
|
|
10
|
+
}
|
|
11
|
+
RebalancingModel.objects.update_or_create(
|
|
12
|
+
class_path=backend_class.__module__ + "." + backend_class.__name__,
|
|
13
|
+
defaults=defaults,
|
|
14
|
+
)
|
|
15
|
+
return backend_class
|
|
16
|
+
|
|
17
|
+
return _decorator
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
4
|
+
|
|
5
|
+
from wbportfolio.pms.typing import Portfolio, Position
|
|
6
|
+
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
7
|
+
from wbportfolio.rebalancing.decorators import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register("Composite Rebalancing")
|
|
11
|
+
class CompositeRebalancing(AbstractRebalancingModel):
|
|
12
|
+
@property
|
|
13
|
+
def base_assets(self) -> dict[int, Decimal]:
|
|
14
|
+
try:
|
|
15
|
+
latest_trade_proposal = self.portfolio.trade_proposals.filter(
|
|
16
|
+
status="APPROVED", trade_date__lte=self.trade_date
|
|
17
|
+
).latest("trade_date")
|
|
18
|
+
return latest_trade_proposal.base_assets
|
|
19
|
+
except ObjectDoesNotExist:
|
|
20
|
+
return dict()
|
|
21
|
+
|
|
22
|
+
def is_valid(self) -> bool:
|
|
23
|
+
return len(self.base_assets.keys()) > 0
|
|
24
|
+
|
|
25
|
+
def get_target_portfolio(self, **kwargs) -> Portfolio:
|
|
26
|
+
positions = []
|
|
27
|
+
for underlying_instrument, weighting in self.base_assets.items():
|
|
28
|
+
positions.append(
|
|
29
|
+
Position(underlying_instrument=underlying_instrument, weighting=weighting, date=self.trade_date)
|
|
30
|
+
)
|
|
31
|
+
return Portfolio(positions=tuple(positions))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from wbportfolio.pms.typing import Portfolio
|
|
4
|
+
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
5
|
+
from wbportfolio.rebalancing.decorators import register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register("Equally Weighted Rebalancing")
|
|
9
|
+
class EquallyWeightedRebalancing(AbstractRebalancingModel):
|
|
10
|
+
def is_valid(self) -> bool:
|
|
11
|
+
return self.portfolio.assets.filter(date=self.last_effective_date).exists()
|
|
12
|
+
|
|
13
|
+
def get_target_portfolio(self, **kwargs) -> Portfolio:
|
|
14
|
+
positions = []
|
|
15
|
+
assets = self.portfolio.assets.filter(date=self.last_effective_date)
|
|
16
|
+
nb_assets = assets.count()
|
|
17
|
+
for asset in assets:
|
|
18
|
+
asset.date = self.trade_date
|
|
19
|
+
asset.asset_valuation_date = self.trade_date
|
|
20
|
+
positions.append(asset._build_dto(new_weight=Decimal(1 / nb_assets)))
|
|
21
|
+
return Portfolio(positions=tuple(positions))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from wbportfolio.pms.typing import Portfolio
|
|
2
|
+
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
3
|
+
from wbportfolio.rebalancing.decorators import register
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register("Model Portfolio Rebalancing")
|
|
7
|
+
class ModelPortfolioRebalancing(AbstractRebalancingModel):
|
|
8
|
+
def __init__(self, *args, **kwargs):
|
|
9
|
+
super().__init__(*args, **kwargs)
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def model_portfolio_rel(self):
|
|
13
|
+
return self.portfolio.dependency_through.filter(type="MODEL").first()
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def model_portfolio(self):
|
|
17
|
+
if model_portfolio_rel := self.model_portfolio_rel:
|
|
18
|
+
return model_portfolio_rel.dependency_portfolio
|
|
19
|
+
|
|
20
|
+
def is_valid(self) -> bool:
|
|
21
|
+
return (
|
|
22
|
+
self.model_portfolio.assets.filter(date=self.last_effective_date).exists()
|
|
23
|
+
if self.model_portfolio
|
|
24
|
+
else False
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def get_target_portfolio(self, **kwargs) -> Portfolio:
|
|
28
|
+
positions = []
|
|
29
|
+
assets = self.model_portfolio.get_positions(self.last_effective_date)
|
|
30
|
+
|
|
31
|
+
for asset in assets:
|
|
32
|
+
asset.date = self.trade_date
|
|
33
|
+
asset.asset_valuation_date = self.trade_date
|
|
34
|
+
positions.append(asset._build_dto())
|
|
35
|
+
return Portfolio(positions=tuple(positions))
|
|
@@ -54,7 +54,7 @@ class ReportClass(ReportMixin):
|
|
|
54
54
|
"portfolio": instrument.name,
|
|
55
55
|
"isin": position.underlying_instrument.isin,
|
|
56
56
|
"title": position.underlying_instrument.name_repr,
|
|
57
|
-
"instrument_type": position.underlying_instrument.
|
|
57
|
+
"instrument_type": position.underlying_instrument.instrument_type.short_name,
|
|
58
58
|
"weight": float(position.weighting),
|
|
59
59
|
"date": position.date.strftime("%Y-%m-%d"),
|
|
60
60
|
}
|