wbportfolio 1.44.5__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 +28 -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.5.dist-info → wbportfolio-1.45.0.dist-info}/METADATA +4 -1
- {wbportfolio-1.44.5.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.5.dist-info → wbportfolio-1.45.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.44.5.dist-info → wbportfolio-1.45.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,30 +1,32 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from contextlib import suppress
|
|
2
|
-
from datetime import timedelta
|
|
3
|
+
from datetime import date, timedelta
|
|
4
|
+
from decimal import Decimal
|
|
3
5
|
from typing import TypeVar
|
|
4
6
|
|
|
5
|
-
import pandas as pd
|
|
6
7
|
from celery import shared_task
|
|
7
8
|
from django.core.exceptions import ValidationError
|
|
8
9
|
from django.db import models
|
|
9
|
-
from django.db.models.signals import post_save
|
|
10
|
-
from django.dispatch import receiver
|
|
11
|
-
from django.utils import timezone
|
|
12
10
|
from django.utils.functional import cached_property
|
|
13
11
|
from django_fsm import FSMField, transition
|
|
14
|
-
from pandas.
|
|
12
|
+
from pandas._libs.tslibs.offsets import BDay
|
|
15
13
|
from wbcompliance.models.risk_management.mixins import RiskCheckMixin
|
|
16
14
|
from wbcore.contrib.icons import WBIcon
|
|
17
15
|
from wbcore.enums import RequestType
|
|
18
16
|
from wbcore.metadata.configs.buttons import ActionButton
|
|
19
17
|
from wbcore.models import WBModel
|
|
20
18
|
from wbfdm.models.instruments.instruments import Instrument
|
|
19
|
+
|
|
21
20
|
from wbportfolio.models.roles import PortfolioRole
|
|
22
21
|
from wbportfolio.pms.trading import TradingService
|
|
23
22
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
24
23
|
from wbportfolio.pms.typing import TradeBatch as TradeBatchDTO
|
|
25
24
|
|
|
25
|
+
from .. import AssetPosition
|
|
26
26
|
from .trades import Trade
|
|
27
27
|
|
|
28
|
+
logger = logging.getLogger("pms")
|
|
29
|
+
|
|
28
30
|
SelfTradeProposal = TypeVar("SelfTradeProposal", bound="TradeProposal")
|
|
29
31
|
|
|
30
32
|
|
|
@@ -39,13 +41,14 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
39
41
|
|
|
40
42
|
comment = models.TextField(default="", verbose_name="Trade Comment", blank=True)
|
|
41
43
|
status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name="Status")
|
|
42
|
-
|
|
43
|
-
"wbportfolio.
|
|
44
|
+
rebalancing_model = models.ForeignKey(
|
|
45
|
+
"wbportfolio.RebalancingModel",
|
|
46
|
+
on_delete=models.SET_NULL,
|
|
44
47
|
blank=True,
|
|
45
48
|
null=True,
|
|
46
|
-
related_name="
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
related_name="trade_proposals",
|
|
50
|
+
verbose_name="Rebalancing Model",
|
|
51
|
+
help_text="Rebalancing Model that generates the target portfolio",
|
|
49
52
|
)
|
|
50
53
|
portfolio = models.ForeignKey(
|
|
51
54
|
"wbportfolio.Portfolio", related_name="trade_proposals", on_delete=models.PROTECT, verbose_name="Portfolio"
|
|
@@ -59,6 +62,23 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
59
62
|
verbose_name="Owner",
|
|
60
63
|
)
|
|
61
64
|
|
|
65
|
+
class Meta:
|
|
66
|
+
verbose_name = "Trade Proposal"
|
|
67
|
+
verbose_name_plural = "Trade Proposals"
|
|
68
|
+
constraints = [
|
|
69
|
+
models.UniqueConstraint(
|
|
70
|
+
fields=["portfolio", "trade_date"],
|
|
71
|
+
name="unique_trade_proposal",
|
|
72
|
+
),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
def save(self, *args, **kwargs):
|
|
76
|
+
if not self.trade_date and self.portfolio.assets.exists():
|
|
77
|
+
self.trade_date = (self.portfolio.assets.latest("date").date + BDay(1)).date()
|
|
78
|
+
if not self.rebalancing_model and (rebalancer := getattr(self.portfolio, "automatic_rebalancer", None)):
|
|
79
|
+
self.rebalancing_model = rebalancer.rebalancing_model
|
|
80
|
+
super().save(*args, **kwargs)
|
|
81
|
+
|
|
62
82
|
def _get_checked_object_field_name(self) -> str:
|
|
63
83
|
"""
|
|
64
84
|
Mandatory function from the Riskcheck mixin that returns the field (aka portfolio), representing the object to check the rules against.
|
|
@@ -76,6 +96,13 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
76
96
|
trades_batch=self._build_dto(),
|
|
77
97
|
)
|
|
78
98
|
|
|
99
|
+
@cached_property
|
|
100
|
+
def last_effective_date(self) -> date:
|
|
101
|
+
try:
|
|
102
|
+
return self.portfolio.assets.filter(date__lt=self.trade_date).latest("date").date
|
|
103
|
+
except AssetPosition.DoesNotExist:
|
|
104
|
+
return (self.trade_date - BDay(1)).date()
|
|
105
|
+
|
|
79
106
|
@property
|
|
80
107
|
def previous_trade_proposal(self) -> SelfTradeProposal | None:
|
|
81
108
|
future_proposals = TradeProposal.objects.filter(portfolio=self.portfolio).filter(
|
|
@@ -94,8 +121,8 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
94
121
|
return future_proposals.earliest("trade_date")
|
|
95
122
|
return None
|
|
96
123
|
|
|
97
|
-
@
|
|
98
|
-
def base_assets(self):
|
|
124
|
+
@property
|
|
125
|
+
def base_assets(self) -> dict[int, Decimal]:
|
|
99
126
|
"""
|
|
100
127
|
Return a dictionary representation (instrument_id: target weight) of this trade proposal
|
|
101
128
|
Returns:
|
|
@@ -104,17 +131,15 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
104
131
|
"""
|
|
105
132
|
return {
|
|
106
133
|
v["underlying_instrument"]: v["target_weight"]
|
|
107
|
-
for v in self.trades.
|
|
134
|
+
for v in self.trades.all()
|
|
135
|
+
.annotate_base_info()
|
|
136
|
+
.filter(status=Trade.Status.EXECUTED)
|
|
137
|
+
.values("underlying_instrument", "target_weight")
|
|
108
138
|
}
|
|
109
139
|
|
|
110
140
|
def __str__(self) -> str:
|
|
111
141
|
return f"{self.portfolio.name}: {self.trade_date} ({self.status})"
|
|
112
142
|
|
|
113
|
-
def save(self, *args, **kwargs):
|
|
114
|
-
if not self.model_portfolio:
|
|
115
|
-
self.model_portfolio = self.portfolio
|
|
116
|
-
super().save(*args, **kwargs)
|
|
117
|
-
|
|
118
143
|
def _build_dto(self) -> TradeBatchDTO:
|
|
119
144
|
"""
|
|
120
145
|
Data Transfer Object
|
|
@@ -144,17 +169,10 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
144
169
|
trade_date=trade_date,
|
|
145
170
|
comment=kwargs.get("comment", self.comment),
|
|
146
171
|
status=TradeProposal.Status.DRAFT,
|
|
147
|
-
|
|
172
|
+
rebalancing_model=self.rebalancing_model,
|
|
148
173
|
portfolio=self.portfolio,
|
|
149
174
|
creator=self.creator,
|
|
150
175
|
)
|
|
151
|
-
|
|
152
|
-
# For all existing trades, copy them to the new trade proposal
|
|
153
|
-
for trade in self.trades.all():
|
|
154
|
-
trade.pk = None
|
|
155
|
-
trade.trade_proposal = trade_proposal_clone
|
|
156
|
-
trade.transaction_date = trade_proposal_clone.trade_date
|
|
157
|
-
trade.save()
|
|
158
176
|
return trade_proposal_clone
|
|
159
177
|
|
|
160
178
|
def normalize_trades(self):
|
|
@@ -167,111 +185,96 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
167
185
|
leftovers_trades = self.trades.all()
|
|
168
186
|
for _, trade in service.trades_batch.trades_map.items():
|
|
169
187
|
with suppress(Trade.DoesNotExist):
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"shares": trade.target_shares,
|
|
175
|
-
},
|
|
176
|
-
)
|
|
188
|
+
trade = Trade.objects.get(id=trade.id)
|
|
189
|
+
trade.weighting = trade.delta_weight
|
|
190
|
+
trade.shares = self.estimate_shares(trade)
|
|
191
|
+
trade.save()
|
|
177
192
|
leftovers_trades = leftovers_trades.exclude(id=trade.id)
|
|
178
193
|
leftovers_trades.delete()
|
|
179
194
|
|
|
180
|
-
def
|
|
195
|
+
def _get_target_portfolio(self, **kwargs) -> PortfolioDTO:
|
|
196
|
+
if self.rebalancing_model:
|
|
197
|
+
return self.rebalancing_model.get_target_portfolio(
|
|
198
|
+
self.portfolio, self.trade_date, self.last_effective_date, **kwargs
|
|
199
|
+
)
|
|
200
|
+
# Return the current portfolio by default
|
|
201
|
+
return self.portfolio._build_dto(self.last_effective_date)
|
|
202
|
+
|
|
203
|
+
def reset_trades(self, target_portfolio: PortfolioDTO | None = None):
|
|
181
204
|
"""
|
|
182
205
|
Will delete all existing trades and recreate them from the method `create_or_update_trades`
|
|
183
206
|
"""
|
|
207
|
+
if self.status != TradeProposal.Status.DRAFT:
|
|
208
|
+
raise ValueError("Cannot reset non-draft trade proposal. Revert this trade proposal first.")
|
|
184
209
|
# delete all existing trades
|
|
185
210
|
self.trades.all().delete()
|
|
186
|
-
|
|
187
|
-
|
|
211
|
+
last_effective_date = self.last_effective_date
|
|
212
|
+
# Get effective and target portfolio
|
|
213
|
+
effective_portfolio = self.portfolio._build_dto(last_effective_date)
|
|
214
|
+
if not target_portfolio:
|
|
215
|
+
target_portfolio = self._get_target_portfolio()
|
|
216
|
+
# if not effective_portfolio:
|
|
217
|
+
# effective_portfolio = target_portfolio
|
|
218
|
+
service = TradingService(
|
|
219
|
+
self.trade_date,
|
|
220
|
+
effective_portfolio=effective_portfolio,
|
|
221
|
+
target_portfolio=target_portfolio,
|
|
222
|
+
)
|
|
223
|
+
service.normalize()
|
|
224
|
+
service.is_valid()
|
|
188
225
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
trade.execute()
|
|
194
|
-
trade.save()
|
|
195
|
-
# We propagate the new portfolio composition until the next trade proposal or today if it doesn't exist yet
|
|
196
|
-
to_date = self.next_trade_proposal.trade_date if self.next_trade_proposal else timezone.now().date()
|
|
197
|
-
|
|
198
|
-
for from_date in pd.date_range(self.trade_date, to_date - timedelta(days=1), freq="B"):
|
|
199
|
-
to_date = (from_date + BDay(1)).date()
|
|
200
|
-
self.portfolio.propagate_or_update_assets(
|
|
201
|
-
from_date.date(),
|
|
202
|
-
to_date,
|
|
203
|
-
forward_price=False,
|
|
204
|
-
base_assets=self.base_assets,
|
|
205
|
-
delete_existing_assets=True,
|
|
226
|
+
for trade_dto in service.validated_trades:
|
|
227
|
+
instrument = Instrument.objects.get(id=trade_dto.underlying_instrument)
|
|
228
|
+
currency_fx_rate = instrument.currency.convert(
|
|
229
|
+
last_effective_date, self.portfolio.currency, exact_lookup=True
|
|
206
230
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
231
|
+
trade = Trade(
|
|
232
|
+
underlying_instrument=instrument,
|
|
233
|
+
transaction_subtype=Trade.Type.BUY if trade_dto.delta_weight > 0 else Trade.Type.SELL,
|
|
234
|
+
currency=instrument.currency,
|
|
235
|
+
value_date=last_effective_date,
|
|
236
|
+
transaction_date=self.trade_date,
|
|
237
|
+
trade_proposal=self,
|
|
238
|
+
portfolio=self.portfolio,
|
|
239
|
+
weighting=trade_dto.delta_weight,
|
|
240
|
+
status=Trade.Status.DRAFT,
|
|
241
|
+
currency_fx_rate=currency_fx_rate,
|
|
242
|
+
)
|
|
243
|
+
trade.shares = self.estimate_shares(trade)
|
|
213
244
|
trade.save()
|
|
214
|
-
if previous_trade_proposal := self.previous_trade_proposal:
|
|
215
|
-
previous_trade_proposal.apply_trades()
|
|
216
245
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
def replay(self):
|
|
247
|
+
trade_proposal = self
|
|
248
|
+
while trade_proposal and trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
249
|
+
logger.info(f"Replaying trade proposal {self}")
|
|
250
|
+
trade_proposal.portfolio.assets.filter(
|
|
251
|
+
date=trade_proposal.trade_date
|
|
252
|
+
).delete() # we delete the existing position and we reapply the trade proposal
|
|
253
|
+
if not trade_proposal.portfolio.assets.filter(date=trade_proposal.trade_date).exists():
|
|
254
|
+
if trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
255
|
+
trade_proposal.revert()
|
|
256
|
+
if trade_proposal.status == TradeProposal.Status.DRAFT:
|
|
257
|
+
trade_proposal.submit()
|
|
258
|
+
if trade_proposal.status == TradeProposal.Status.SUBMIT:
|
|
259
|
+
trade_proposal.approve()
|
|
260
|
+
trade_proposal.save()
|
|
261
|
+
next_trade_proposal = trade_proposal.next_trade_proposal
|
|
262
|
+
next_trade_date = (
|
|
263
|
+
next_trade_proposal.trade_date - timedelta(days=1) if next_trade_proposal else date.today()
|
|
264
|
+
)
|
|
265
|
+
overriding_trade_proposal = trade_proposal.portfolio.batch_portfolio(
|
|
266
|
+
trade_proposal.trade_date, next_trade_date
|
|
267
|
+
)
|
|
268
|
+
trade_proposal = overriding_trade_proposal or next_trade_proposal
|
|
222
269
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"""
|
|
228
|
-
# if the target portfolio is not provided, we try to build it
|
|
229
|
-
if (
|
|
230
|
-
not target_portfolio
|
|
231
|
-
and (assets := self.model_portfolio.assets.filter(date__lte=self.trade_date)).exists()
|
|
232
|
-
and (latest_pos := assets.latest("date"))
|
|
233
|
-
):
|
|
234
|
-
target_portfolio = self.model_portfolio._build_dto(latest_pos.date)
|
|
235
|
-
|
|
236
|
-
# if the effective portfolio is not provided, we try to build it
|
|
237
|
-
if (
|
|
238
|
-
not effective_portfolio
|
|
239
|
-
and (assets := self.portfolio.assets.filter(date__lte=self.trade_date)).exists()
|
|
240
|
-
and (latest_pos := assets.latest("date"))
|
|
241
|
-
):
|
|
242
|
-
effective_portfolio = self.portfolio._build_dto(latest_pos.date)
|
|
243
|
-
# Build trades DTO from the attached trades
|
|
244
|
-
trade_batch = self._build_dto()
|
|
245
|
-
if target_portfolio or effective_portfolio or trade_batch:
|
|
246
|
-
service = TradingService(
|
|
247
|
-
self.trade_date,
|
|
248
|
-
effective_portfolio=effective_portfolio,
|
|
249
|
-
target_portfolio=target_portfolio,
|
|
250
|
-
trades_batch=trade_batch,
|
|
270
|
+
def estimate_shares(self, trade: Trade) -> Decimal | None:
|
|
271
|
+
if not self.portfolio.only_weighting and (quote := trade.underlying_quote_price):
|
|
272
|
+
trade_total_value_fx_portfolio = (
|
|
273
|
+
self.portfolio.get_total_asset_value(trade.value_date) * trade._target_weight
|
|
251
274
|
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
service.is_valid()
|
|
256
|
-
if reset:
|
|
257
|
-
self.trades.all().delete()
|
|
258
|
-
for trade_dto in service.validated_trades:
|
|
259
|
-
instrument = Instrument.objects.get(id=trade_dto.underlying_instrument)
|
|
260
|
-
t, c = Trade.objects.update_or_create(
|
|
261
|
-
underlying_instrument=instrument,
|
|
262
|
-
currency=instrument.currency,
|
|
263
|
-
transaction_date=self.trade_date,
|
|
264
|
-
trade_proposal=self,
|
|
265
|
-
portfolio=self.portfolio,
|
|
266
|
-
defaults={
|
|
267
|
-
"shares": trade_dto.target_shares,
|
|
268
|
-
"weighting": trade_dto.delta_weight,
|
|
269
|
-
"status": Trade.Status.DRAFT,
|
|
270
|
-
"currency_fx_rate": instrument.currency.convert(self.trade_date, self.portfolio.currency),
|
|
271
|
-
},
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
# End tools methods
|
|
275
|
+
price_fx_portfolio = quote.net_value * trade.currency_fx_rate
|
|
276
|
+
if price_fx_portfolio:
|
|
277
|
+
return trade_total_value_fx_portfolio / price_fx_portfolio
|
|
275
278
|
|
|
276
279
|
# Start FSM logics
|
|
277
280
|
|
|
@@ -295,7 +298,10 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
295
298
|
},
|
|
296
299
|
)
|
|
297
300
|
def submit(self, by=None, description=None, **kwargs):
|
|
298
|
-
self.trades.update(status=Trade.Status.
|
|
301
|
+
self.trades.update(comment="", status=Trade.Status.DRAFT)
|
|
302
|
+
for trade in self.trades.all():
|
|
303
|
+
trade.submit()
|
|
304
|
+
trade.save()
|
|
299
305
|
self.evaluate_active_rules(
|
|
300
306
|
self.trade_date, self.validated_trading_service.target_portfolio, asynchronously=True
|
|
301
307
|
)
|
|
@@ -348,17 +354,28 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
348
354
|
)
|
|
349
355
|
},
|
|
350
356
|
)
|
|
351
|
-
def approve(self, by=None, description=None, **kwargs):
|
|
352
|
-
|
|
357
|
+
def approve(self, by=None, description=None, synchronous=False, **kwargs):
|
|
358
|
+
# We validate trade which will create or update the initial asset positions
|
|
359
|
+
if not self.portfolio.can_be_rebalanced:
|
|
360
|
+
raise ValueError("Non-Rebalanceable portfolio cannot be traded manually.")
|
|
361
|
+
self.trades.update(status=Trade.Status.SUBMIT)
|
|
362
|
+
self.portfolio.assets.filter(date=self.trade_date).delete() # we delete position to avoid having leftovers
|
|
363
|
+
for trade in self.trades.all():
|
|
364
|
+
trade.execute()
|
|
365
|
+
trade.save()
|
|
366
|
+
self.portfolio.change_at_date(self.trade_date)
|
|
367
|
+
# replay_as_task.delay(self.id)
|
|
353
368
|
|
|
354
369
|
def can_approve(self):
|
|
355
370
|
errors = dict()
|
|
371
|
+
if not self.portfolio.can_be_rebalanced:
|
|
372
|
+
errors["non_field_errors"] = "The portfolio does not allow manual rebalanced"
|
|
356
373
|
if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
|
|
357
374
|
errors["non_field_errors"] = "At least one trade needs to be submitted to be able to approve this proposal"
|
|
358
|
-
if not self.portfolio.
|
|
359
|
-
errors[
|
|
360
|
-
"portfolio"
|
|
361
|
-
|
|
375
|
+
if not self.portfolio.can_be_rebalanced:
|
|
376
|
+
errors["portfolio"] = (
|
|
377
|
+
"The portfolio needs to be a model portfolio in order to approve this trade proposal manually"
|
|
378
|
+
)
|
|
362
379
|
if self.has_assigned_active_rules and not self.has_all_check_completed_and_succeed:
|
|
363
380
|
errors["non_field_errors"] = "The pre trades rules did not passed successfully"
|
|
364
381
|
return errors
|
|
@@ -421,10 +438,7 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
421
438
|
self.checks.delete()
|
|
422
439
|
|
|
423
440
|
def can_backtodraft(self):
|
|
424
|
-
|
|
425
|
-
if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
|
|
426
|
-
errors["non_field_errors"] = "All trades need to be submitted before reverting back to draft"
|
|
427
|
-
return errors
|
|
441
|
+
pass
|
|
428
442
|
|
|
429
443
|
@transition(
|
|
430
444
|
field=status,
|
|
@@ -448,16 +462,17 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
448
462
|
def revert(self, **kwargs):
|
|
449
463
|
with suppress(KeyError):
|
|
450
464
|
del self.__dict__["validated_trading_service"]
|
|
451
|
-
|
|
465
|
+
for trade in self.trades.filter(status=Trade.Status.EXECUTED):
|
|
466
|
+
trade.revert()
|
|
467
|
+
trade.save()
|
|
468
|
+
# replay_as_task.delay(self.id)
|
|
452
469
|
|
|
453
470
|
def can_revert(self):
|
|
454
471
|
errors = dict()
|
|
455
|
-
if self.
|
|
456
|
-
errors["
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
"portfolio"
|
|
460
|
-
] = "The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
|
|
472
|
+
if not self.portfolio.can_be_rebalanced:
|
|
473
|
+
errors["portfolio"] = (
|
|
474
|
+
"The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
|
|
475
|
+
)
|
|
461
476
|
return errors
|
|
462
477
|
|
|
463
478
|
# End FSM logics
|
|
@@ -478,25 +493,8 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
478
493
|
def get_representation_label_key(cls) -> str:
|
|
479
494
|
return "{{_portfolio.name}} ({{trade_date}})"
|
|
480
495
|
|
|
481
|
-
class Meta:
|
|
482
|
-
verbose_name = "Trade Proposal"
|
|
483
|
-
verbose_name_plural = "Trade Proposals"
|
|
484
|
-
unique_together = ["portfolio", "trade_date"]
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
@shared_task(queue="portfolio")
|
|
488
|
-
def apply_trades_proposal_as_task(trade_proposal_id):
|
|
489
|
-
trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
|
|
490
|
-
trade_proposal.apply_trades()
|
|
491
|
-
|
|
492
496
|
|
|
493
497
|
@shared_task(queue="portfolio")
|
|
494
|
-
def
|
|
498
|
+
def replay_as_task(trade_proposal_id):
|
|
495
499
|
trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
|
|
496
|
-
trade_proposal.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
@receiver(post_save, sender="wbportfolio.TradeProposal")
|
|
500
|
-
def post_save_trade_proposal(sender, instance, created, raw, **kwargs):
|
|
501
|
-
if created and not raw and instance.portfolio.assets.filter(date__lte=instance.trade_date).exists():
|
|
502
|
-
instance.create_or_update_trades(reset=True)
|
|
500
|
+
trade_proposal.replay()
|