wbportfolio 1.52.0__py2.py3-none-any.whl → 1.59.4__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 +3 -1
- wbportfolio/admin/indexes.py +1 -1
- wbportfolio/admin/orders/__init__.py +2 -0
- wbportfolio/admin/orders/order_proposals.py +16 -0
- wbportfolio/admin/orders/orders.py +32 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
- wbportfolio/admin/transactions/__init__.py +0 -2
- wbportfolio/admin/transactions/dividends.py +40 -4
- wbportfolio/admin/transactions/fees.py +24 -14
- wbportfolio/admin/transactions/trades.py +34 -27
- wbportfolio/analysis/claims.py +5 -6
- wbportfolio/api_clients/ubs.py +162 -0
- wbportfolio/constants.py +1 -0
- wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/contrib/company_portfolio/serializers.py +32 -22
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
- wbportfolio/defaults/fees/default.py +7 -15
- wbportfolio/factories/__init__.py +2 -2
- wbportfolio/factories/assets.py +1 -1
- wbportfolio/factories/dividends.py +8 -3
- wbportfolio/factories/fees.py +8 -4
- wbportfolio/factories/orders/__init__.py +2 -0
- wbportfolio/factories/orders/order_proposals.py +21 -0
- wbportfolio/factories/orders/orders.py +34 -0
- wbportfolio/factories/portfolios.py +2 -1
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/factories/trades.py +12 -16
- wbportfolio/filters/assets.py +18 -4
- wbportfolio/filters/orders/__init__.py +2 -0
- wbportfolio/filters/orders/order_proposals.py +55 -0
- wbportfolio/filters/orders/orders.py +11 -0
- wbportfolio/filters/portfolios.py +38 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/__init__.py +1 -2
- wbportfolio/filters/transactions/fees.py +5 -12
- wbportfolio/filters/transactions/trades.py +16 -8
- wbportfolio/filters/transactions/utils.py +42 -0
- wbportfolio/import_export/backends/ubs/__init__.py +1 -0
- wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
- wbportfolio/import_export/backends/ubs/fees.py +10 -20
- wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
- wbportfolio/import_export/backends/ubs/trade.py +48 -0
- wbportfolio/import_export/backends/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +22 -10
- wbportfolio/import_export/handlers/dividend.py +8 -8
- wbportfolio/import_export/handlers/fees.py +13 -23
- wbportfolio/import_export/handlers/orders.py +71 -0
- wbportfolio/import_export/handlers/trade.py +53 -77
- wbportfolio/import_export/parsers/default_mapping.py +1 -1
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
- wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
- wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
- wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/fees.py +7 -9
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
- wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
- wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
- wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
- wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
- wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
- wbportfolio/import_export/parsers/ubs/equity.py +3 -2
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
- wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
- wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
- wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
- wbportfolio/import_export/resources/trades.py +3 -3
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
- wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
- wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
- wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
- wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
- wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
- wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
- wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
- wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
- wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
- wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
- wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
- wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
- wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
- wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
- wbportfolio/models/__init__.py +2 -0
- wbportfolio/models/adjustments.py +1 -1
- wbportfolio/models/asset.py +28 -170
- wbportfolio/models/builder.py +323 -0
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/portfolio.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
- wbportfolio/models/orders/__init__.py +2 -0
- wbportfolio/models/orders/order_proposals.py +1414 -0
- wbportfolio/models/orders/orders.py +410 -0
- wbportfolio/models/portfolio.py +311 -289
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +12 -0
- wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/__init__.py +0 -4
- wbportfolio/models/transactions/claim.py +7 -6
- wbportfolio/models/transactions/dividends.py +42 -5
- wbportfolio/models/transactions/fees.py +55 -22
- wbportfolio/models/transactions/trades.py +121 -442
- wbportfolio/models/transactions/transactions.py +78 -158
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +35 -0
- wbportfolio/order_routing/adapters/__init__.py +65 -0
- wbportfolio/order_routing/adapters/ubs.py +195 -0
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/__init__.py +0 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/analytics/portfolio.py +17 -9
- wbportfolio/pms/analytics/utils.py +9 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +198 -63
- wbportfolio/rebalancing/base.py +12 -1
- wbportfolio/rebalancing/decorators.py +1 -1
- wbportfolio/rebalancing/models/composite.py +4 -8
- wbportfolio/rebalancing/models/equally_weighted.py +13 -11
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
- wbportfolio/rebalancing/models/model_portfolio.py +14 -18
- wbportfolio/risk_management/backends/__init__.py +1 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
- wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
- wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
- wbportfolio/serializers/__init__.py +1 -0
- wbportfolio/serializers/orders/__init__.py +2 -0
- wbportfolio/serializers/orders/order_proposals.py +115 -0
- wbportfolio/serializers/orders/orders.py +283 -0
- wbportfolio/serializers/portfolios.py +7 -7
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/serializers/signals.py +9 -12
- wbportfolio/serializers/transactions/__init__.py +1 -10
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/serializers/transactions/dividends.py +37 -9
- wbportfolio/serializers/transactions/fees.py +39 -10
- wbportfolio/serializers/transactions/trades.py +55 -157
- wbportfolio/tasks.py +43 -5
- wbportfolio/tests/analysis/__init__.py +0 -0
- wbportfolio/tests/analysis/test_claims.py +85 -0
- wbportfolio/tests/conftest.py +12 -12
- wbportfolio/tests/models/orders/__init__.py +0 -0
- wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
- wbportfolio/tests/models/test_assets.py +7 -3
- wbportfolio/tests/models/test_imports.py +9 -13
- wbportfolio/tests/models/test_portfolios.py +102 -95
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_fees.py +7 -13
- wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
- wbportfolio/tests/models/transactions/test_trades.py +0 -20
- wbportfolio/tests/pms/test_analytics.py +22 -3
- wbportfolio/tests/rebalancing/test_models.py +51 -57
- wbportfolio/tests/signals.py +10 -20
- wbportfolio/tests/tests.py +3 -1
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +10 -13
- wbportfolio/viewsets/__init__.py +9 -4
- wbportfolio/viewsets/assets.py +3 -204
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +344 -154
- wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/__init__.py +2 -5
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/fees.py +3 -3
- wbportfolio/viewsets/configs/display/portfolios.py +5 -5
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/configs/display/trades.py +1 -189
- wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
- wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
- wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
- wbportfolio/viewsets/configs/menu/__init__.py +1 -1
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/configs/titles/__init__.py +2 -3
- wbportfolio/viewsets/configs/titles/fees.py +4 -8
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/mixins.py +5 -1
- wbportfolio/viewsets/orders/__init__.py +6 -0
- wbportfolio/viewsets/orders/configs/__init__.py +4 -0
- wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
- wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
- wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
- wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
- wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
- wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
- wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
- wbportfolio/viewsets/orders/order_proposals.py +252 -0
- wbportfolio/viewsets/orders/orders.py +277 -0
- wbportfolio/viewsets/portfolios.py +36 -12
- wbportfolio/viewsets/positions.py +3 -2
- wbportfolio/viewsets/products.py +6 -6
- wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
- wbportfolio/viewsets/transactions/__init__.py +3 -14
- wbportfolio/viewsets/transactions/fees.py +22 -22
- wbportfolio/viewsets/transactions/trades.py +1 -180
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/admin/transactions/transactions.py +0 -38
- wbportfolio/factories/transactions.py +0 -22
- wbportfolio/fdm/tasks.py +0 -13
- wbportfolio/filters/transactions/transactions.py +0 -99
- wbportfolio/models/transactions/expiry.py +0 -7
- wbportfolio/models/transactions/trade_proposals.py +0 -704
- wbportfolio/pms/trading/handler.py +0 -161
- wbportfolio/serializers/transactions/expiry.py +0 -18
- wbportfolio/serializers/transactions/trade_proposals.py +0 -76
- wbportfolio/serializers/transactions/transactions.py +0 -85
- wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
- wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
- wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
- wbportfolio/viewsets/configs/display/transactions.py +0 -55
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
- wbportfolio/viewsets/configs/menu/transactions.py +0 -9
- wbportfolio/viewsets/configs/titles/transactions.py +0 -9
- wbportfolio/viewsets/signals.py +0 -43
- wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
- wbportfolio/viewsets/transactions/transactions.py +0 -122
- /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
- {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
from
|
|
2
|
-
from datetime import date, timedelta
|
|
1
|
+
from datetime import timedelta
|
|
3
2
|
from decimal import Decimal
|
|
4
3
|
|
|
5
4
|
from celery import shared_task
|
|
6
|
-
from django.contrib import admin
|
|
7
5
|
from django.db import models
|
|
8
6
|
from django.db.models import (
|
|
9
7
|
Case,
|
|
@@ -19,85 +17,20 @@ from django.db.models import (
|
|
|
19
17
|
from django.db.models.functions import Coalesce
|
|
20
18
|
from django.db.models.signals import post_save
|
|
21
19
|
from django.dispatch import receiver
|
|
22
|
-
from
|
|
23
|
-
from
|
|
24
|
-
from django_fsm import GET_STATE, FSMField, transition
|
|
25
|
-
from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet
|
|
26
|
-
from wbcore.contrib.icons import WBIcon
|
|
27
|
-
from wbcore.enums import RequestType
|
|
28
|
-
from wbcore.metadata.configs.buttons import ActionButton
|
|
29
|
-
from wbcore.models import WBModel
|
|
20
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
21
|
+
from wbcore.signals import pre_merge
|
|
30
22
|
from wbcore.signals.models import pre_collection
|
|
23
|
+
from wbfdm.models import Instrument
|
|
31
24
|
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
25
|
+
from wbfdm.signals import add_instrument_to_investable_universe
|
|
32
26
|
|
|
33
27
|
from wbportfolio.import_export.handlers.trade import TradeImportHandler
|
|
34
|
-
from wbportfolio.models.asset import AssetPosition
|
|
35
28
|
from wbportfolio.models.custodians import Custodian
|
|
36
|
-
from wbportfolio.models.roles import PortfolioRole
|
|
37
|
-
from wbportfolio.pms.typing import Trade as TradeDTO
|
|
38
29
|
|
|
39
|
-
from .transactions import
|
|
30
|
+
from .transactions import TransactionMixin
|
|
40
31
|
|
|
41
32
|
|
|
42
|
-
class
|
|
43
|
-
def annotate_base_info(self):
|
|
44
|
-
return self.annotate(
|
|
45
|
-
last_effective_date=Subquery(
|
|
46
|
-
AssetPosition.unannotated_objects.filter(
|
|
47
|
-
date__lte=OuterRef("value_date"),
|
|
48
|
-
portfolio=OuterRef("portfolio"),
|
|
49
|
-
)
|
|
50
|
-
.order_by("-date")
|
|
51
|
-
.values("date")[:1]
|
|
52
|
-
),
|
|
53
|
-
effective_weight=Coalesce(
|
|
54
|
-
Subquery(
|
|
55
|
-
AssetPosition.unannotated_objects.filter(
|
|
56
|
-
underlying_quote=OuterRef("underlying_instrument"),
|
|
57
|
-
date=OuterRef("last_effective_date"),
|
|
58
|
-
portfolio=OuterRef("portfolio"),
|
|
59
|
-
)
|
|
60
|
-
.values("portfolio")
|
|
61
|
-
.annotate(s=Sum("weighting"))
|
|
62
|
-
.values("s")[:1]
|
|
63
|
-
),
|
|
64
|
-
Decimal(0),
|
|
65
|
-
),
|
|
66
|
-
target_weight=F("effective_weight") + F("weighting"),
|
|
67
|
-
effective_shares=Coalesce(
|
|
68
|
-
Subquery(
|
|
69
|
-
AssetPosition.objects.filter(
|
|
70
|
-
underlying_quote=OuterRef("underlying_instrument"),
|
|
71
|
-
date=OuterRef("last_effective_date"),
|
|
72
|
-
portfolio=OuterRef("portfolio"),
|
|
73
|
-
)
|
|
74
|
-
.values("portfolio")
|
|
75
|
-
.annotate(s=Sum("shares"))
|
|
76
|
-
.values("s")[:1]
|
|
77
|
-
),
|
|
78
|
-
Decimal(0),
|
|
79
|
-
),
|
|
80
|
-
target_shares=F("effective_shares") + F("shares"),
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class DefaultTradeManager(OrderedModelManager):
|
|
85
|
-
"""This manager is expect to be the trade default manager and annotate by default the effective weight (extracted
|
|
86
|
-
from the associated portfolio) and the target weight as an addition between the effective weight and the delta weight
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
def __init__(self, with_annotation: bool = False, *args, **kwargs):
|
|
90
|
-
self.with_annotation = with_annotation
|
|
91
|
-
super().__init__(*args, **kwargs)
|
|
92
|
-
|
|
93
|
-
def get_queryset(self) -> TradeQueryset:
|
|
94
|
-
qs = TradeQueryset(self.model, using=self._db)
|
|
95
|
-
if self.with_annotation:
|
|
96
|
-
qs = qs.annotate_base_info()
|
|
97
|
-
return qs
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class ValidCustomerTradeManager(DefaultTradeManager):
|
|
33
|
+
class ValidCustomerTradeManager(models.Manager):
|
|
101
34
|
def __init__(self, without_internal_trade: bool = False):
|
|
102
35
|
self.without_internal_trade = without_internal_trade
|
|
103
36
|
super().__init__()
|
|
@@ -117,18 +50,11 @@ class ValidCustomerTradeManager(DefaultTradeManager):
|
|
|
117
50
|
return qs
|
|
118
51
|
|
|
119
52
|
|
|
120
|
-
class Trade(
|
|
53
|
+
class Trade(TransactionMixin, ImportMixin, models.Model):
|
|
121
54
|
import_export_handler_class = TradeImportHandler
|
|
122
55
|
|
|
123
56
|
TRADE_WINDOW_INTERVAL = 7
|
|
124
57
|
|
|
125
|
-
class Status(models.TextChoices):
|
|
126
|
-
DRAFT = "DRAFT", "Draft"
|
|
127
|
-
SUBMIT = "SUBMIT", "Submit"
|
|
128
|
-
EXECUTED = "EXECUTED", "Executed"
|
|
129
|
-
CONFIRMED = "CONFIRMED", "Confirmed"
|
|
130
|
-
FAILED = "FAILED", "Failed"
|
|
131
|
-
|
|
132
58
|
class Type(models.TextChoices):
|
|
133
59
|
REBALANCE = "REBALANCE", "Rebalance"
|
|
134
60
|
DECREASE = "DECREASE", "Decrease"
|
|
@@ -139,55 +65,71 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
139
65
|
SELL = "SELL", "Sell"
|
|
140
66
|
NO_CHANGE = "NO_CHANGE", "No Change" # default transaction subtype if weighing is 0
|
|
141
67
|
|
|
142
|
-
external_identifier2 = models.CharField(
|
|
143
|
-
max_length=255,
|
|
144
|
-
null=True,
|
|
145
|
-
blank=True,
|
|
146
|
-
help_text="A second external identifier that was supplied.",
|
|
147
|
-
verbose_name="External Identifier 2",
|
|
148
|
-
)
|
|
149
|
-
|
|
150
68
|
transaction_subtype = models.CharField(
|
|
151
69
|
max_length=32, default=Type.BUY, choices=Type.choices, verbose_name="Trade Type"
|
|
152
70
|
)
|
|
153
|
-
|
|
71
|
+
transaction_date = models.DateField(
|
|
72
|
+
verbose_name="Trade Date",
|
|
73
|
+
help_text="The date that this transaction was traded.",
|
|
74
|
+
)
|
|
75
|
+
book_date = models.DateField(
|
|
76
|
+
verbose_name="Trade Date",
|
|
77
|
+
help_text="The date that this transaction was booked.",
|
|
78
|
+
)
|
|
79
|
+
shares = models.DecimalField(
|
|
80
|
+
max_digits=15,
|
|
81
|
+
decimal_places=4,
|
|
82
|
+
default=Decimal("0.0"),
|
|
83
|
+
help_text="The number of shares that were traded.",
|
|
84
|
+
verbose_name="Shares",
|
|
85
|
+
)
|
|
86
|
+
|
|
154
87
|
weighting = models.DecimalField(
|
|
155
|
-
max_digits=
|
|
156
|
-
decimal_places=
|
|
88
|
+
max_digits=9,
|
|
89
|
+
decimal_places=8,
|
|
157
90
|
default=Decimal(0),
|
|
158
91
|
help_text="The weight to be multiplied against the target",
|
|
159
92
|
verbose_name="Weight",
|
|
160
93
|
)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
94
|
+
claimed_shares = models.DecimalField(
|
|
95
|
+
max_digits=15,
|
|
96
|
+
decimal_places=4,
|
|
97
|
+
default=Decimal(0),
|
|
98
|
+
help_text="The number of shares that were claimed.",
|
|
99
|
+
verbose_name="Claimed Shares",
|
|
165
100
|
)
|
|
166
|
-
|
|
167
|
-
"
|
|
101
|
+
diff_shares = models.GeneratedField(
|
|
102
|
+
expression=F("shares") - F("claimed_shares"),
|
|
103
|
+
output_field=models.DecimalField(max_digits=15, decimal_places=4),
|
|
104
|
+
db_persist=True,
|
|
105
|
+
)
|
|
106
|
+
internal_trade = models.OneToOneField(
|
|
107
|
+
"wbportfolio.Trade",
|
|
108
|
+
null=True,
|
|
109
|
+
blank=True,
|
|
110
|
+
on_delete=models.SET_NULL,
|
|
111
|
+
related_name="internal_subscription_redemption_trade",
|
|
168
112
|
)
|
|
169
113
|
marked_for_deletion = models.BooleanField(
|
|
170
114
|
default=False,
|
|
171
115
|
help_text="If this is checked, then the trade is supposed to be deleted.",
|
|
172
116
|
verbose_name="To be deleted",
|
|
173
117
|
)
|
|
174
|
-
|
|
175
|
-
# Only valid for subscription and redemption trade
|
|
176
118
|
marked_as_internal = models.BooleanField(
|
|
177
119
|
default=False,
|
|
178
120
|
help_text="If this is checked, then this subscription or redemption is considered internal and will not be considered in any AUM computation",
|
|
179
121
|
verbose_name="Internal",
|
|
180
122
|
)
|
|
181
|
-
internal_trade = models.OneToOneField(
|
|
182
|
-
"wbportfolio.Trade",
|
|
183
|
-
null=True,
|
|
184
|
-
blank=True,
|
|
185
|
-
on_delete=models.SET_NULL,
|
|
186
|
-
related_name="internal_subscription_redemption_trade",
|
|
187
|
-
)
|
|
188
|
-
|
|
189
123
|
pending = models.BooleanField(default=False)
|
|
190
124
|
exclude_from_history = models.BooleanField(default=False)
|
|
125
|
+
bank = models.CharField(
|
|
126
|
+
max_length=255,
|
|
127
|
+
help_text="The bank/counterparty/custodian the trade went through.",
|
|
128
|
+
verbose_name="Counterparty",
|
|
129
|
+
)
|
|
130
|
+
custodian = models.ForeignKey(
|
|
131
|
+
"wbportfolio.Custodian", null=True, blank=True, on_delete=models.SET_NULL, related_name="trades"
|
|
132
|
+
)
|
|
191
133
|
register = models.ForeignKey(
|
|
192
134
|
to="wbportfolio.Register",
|
|
193
135
|
null=True,
|
|
@@ -195,215 +137,26 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
195
137
|
related_name="trades",
|
|
196
138
|
on_delete=models.PROTECT,
|
|
197
139
|
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
to="wbportfolio.TradeProposal",
|
|
140
|
+
external_id = models.CharField(
|
|
141
|
+
max_length=255,
|
|
201
142
|
null=True,
|
|
202
143
|
blank=True,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
help_text="The Trade Proposal this trade is coming from",
|
|
144
|
+
help_text="An external identifier that was supplied.",
|
|
145
|
+
verbose_name="External Identifier",
|
|
206
146
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
help_text="
|
|
212
|
-
verbose_name="
|
|
213
|
-
)
|
|
214
|
-
diff_shares = models.GeneratedField(
|
|
215
|
-
expression=F("shares") - F("claimed_shares"),
|
|
216
|
-
output_field=models.DecimalField(max_digits=15, decimal_places=4),
|
|
217
|
-
db_persist=True,
|
|
147
|
+
external_id_alternative = models.CharField(
|
|
148
|
+
max_length=255,
|
|
149
|
+
null=True,
|
|
150
|
+
blank=True,
|
|
151
|
+
help_text="A second external identifier that was supplied.",
|
|
152
|
+
verbose_name="Alternative External Identifier",
|
|
218
153
|
)
|
|
219
|
-
|
|
220
|
-
|
|
154
|
+
|
|
155
|
+
# Manager
|
|
156
|
+
objects = models.Manager()
|
|
221
157
|
valid_customer_trade_objects = ValidCustomerTradeManager()
|
|
222
158
|
valid_external_customer_trade_objects = ValidCustomerTradeManager(without_internal_trade=True)
|
|
223
159
|
|
|
224
|
-
@transition(
|
|
225
|
-
field=status,
|
|
226
|
-
source=Status.DRAFT,
|
|
227
|
-
target=GET_STATE(
|
|
228
|
-
lambda self, **kwargs: (
|
|
229
|
-
self.Status.SUBMIT if self.last_underlying_quote_price is not None else self.Status.FAILED
|
|
230
|
-
),
|
|
231
|
-
states=[Status.SUBMIT, Status.FAILED],
|
|
232
|
-
),
|
|
233
|
-
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
234
|
-
user.profile, portfolio=instance.portfolio
|
|
235
|
-
),
|
|
236
|
-
custom={
|
|
237
|
-
"_transition_button": ActionButton(
|
|
238
|
-
method=RequestType.PATCH,
|
|
239
|
-
identifiers=("wbportfolio:trade",),
|
|
240
|
-
icon=WBIcon.SEND.icon,
|
|
241
|
-
key="submit",
|
|
242
|
-
label="Submit",
|
|
243
|
-
action_label="Submit",
|
|
244
|
-
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
245
|
-
)
|
|
246
|
-
},
|
|
247
|
-
on_error="FAILED",
|
|
248
|
-
)
|
|
249
|
-
def submit(self, by=None, description=None, **kwargs):
|
|
250
|
-
if not self.last_underlying_quote_price:
|
|
251
|
-
self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
|
|
252
|
-
|
|
253
|
-
def can_submit(self):
|
|
254
|
-
pass
|
|
255
|
-
|
|
256
|
-
@transition(
|
|
257
|
-
field=status,
|
|
258
|
-
source=Status.DRAFT,
|
|
259
|
-
target=Status.FAILED,
|
|
260
|
-
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
261
|
-
user.profile, portfolio=instance.portfolio
|
|
262
|
-
),
|
|
263
|
-
)
|
|
264
|
-
def fail(self, **kwargs):
|
|
265
|
-
self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
|
|
266
|
-
|
|
267
|
-
@cached_property
|
|
268
|
-
def last_underlying_quote_price(self) -> InstrumentPrice | None:
|
|
269
|
-
try:
|
|
270
|
-
# we try t0 first
|
|
271
|
-
return InstrumentPrice.objects.filter_only_valid_prices().get(
|
|
272
|
-
instrument=self.underlying_instrument, date=self.transaction_date
|
|
273
|
-
)
|
|
274
|
-
except InstrumentPrice.DoesNotExist:
|
|
275
|
-
with suppress(InstrumentPrice.DoesNotExist):
|
|
276
|
-
# we fall back to the latest price before t0
|
|
277
|
-
return (
|
|
278
|
-
InstrumentPrice.objects.filter_only_valid_prices()
|
|
279
|
-
.filter(instrument=self.underlying_instrument, date__lte=self.value_date)
|
|
280
|
-
.latest("date")
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
@transition(
|
|
284
|
-
field=status,
|
|
285
|
-
source=Status.SUBMIT,
|
|
286
|
-
target=Status.EXECUTED,
|
|
287
|
-
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
288
|
-
user.profile, portfolio=instance.portfolio
|
|
289
|
-
),
|
|
290
|
-
custom={
|
|
291
|
-
"_transition_button": ActionButton(
|
|
292
|
-
method=RequestType.PATCH,
|
|
293
|
-
identifiers=("wbportfolio:trade",),
|
|
294
|
-
icon=WBIcon.CONFIRM.icon,
|
|
295
|
-
key="execute",
|
|
296
|
-
label="Execute",
|
|
297
|
-
action_label="Execute",
|
|
298
|
-
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
299
|
-
)
|
|
300
|
-
},
|
|
301
|
-
)
|
|
302
|
-
def execute(self, **kwargs):
|
|
303
|
-
with suppress(ValueError):
|
|
304
|
-
asset = self.get_asset()
|
|
305
|
-
AssetPosition.unannotated_objects.update_or_create(
|
|
306
|
-
underlying_quote=asset.underlying_quote,
|
|
307
|
-
portfolio_created=asset.portfolio_created,
|
|
308
|
-
portfolio=asset.portfolio,
|
|
309
|
-
date=asset.date,
|
|
310
|
-
defaults={
|
|
311
|
-
"initial_currency_fx_rate": asset.initial_currency_fx_rate,
|
|
312
|
-
"initial_price": asset.initial_price,
|
|
313
|
-
"initial_shares": asset.initial_shares,
|
|
314
|
-
"underlying_quote_price": asset.underlying_quote_price,
|
|
315
|
-
"asset_valuation_date": asset.asset_valuation_date,
|
|
316
|
-
"currency": asset.currency,
|
|
317
|
-
"is_estimated": asset.is_estimated,
|
|
318
|
-
"weighting": asset.weighting,
|
|
319
|
-
},
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
def can_execute(self):
|
|
323
|
-
if not self.last_underlying_quote_price:
|
|
324
|
-
return {"underlying_instrument": [_("Cannot execute a trade without a valid quote price")]}
|
|
325
|
-
if not self.portfolio.is_manageable:
|
|
326
|
-
return {
|
|
327
|
-
"portfolio": [_("The portfolio needs to be a model portfolio in order to execute this trade manually")]
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
@transition(
|
|
331
|
-
field=status,
|
|
332
|
-
source=Status.EXECUTED,
|
|
333
|
-
target=Status.CONFIRMED,
|
|
334
|
-
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
335
|
-
user.profile, portfolio=instance.portfolio
|
|
336
|
-
),
|
|
337
|
-
custom={
|
|
338
|
-
"_transition_button": ActionButton(
|
|
339
|
-
method=RequestType.PATCH,
|
|
340
|
-
identifiers=("wbportfolio:trade",),
|
|
341
|
-
icon=WBIcon.CONFIRM.icon,
|
|
342
|
-
key="confirm",
|
|
343
|
-
label="Confirm",
|
|
344
|
-
action_label="Confirme",
|
|
345
|
-
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
346
|
-
)
|
|
347
|
-
},
|
|
348
|
-
)
|
|
349
|
-
def confirm(self, by=None, description=None, **kwargs):
|
|
350
|
-
pass
|
|
351
|
-
|
|
352
|
-
def can_confirm(self):
|
|
353
|
-
pass
|
|
354
|
-
|
|
355
|
-
@transition(
|
|
356
|
-
field=status,
|
|
357
|
-
source=Status.SUBMIT,
|
|
358
|
-
target=Status.DRAFT,
|
|
359
|
-
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
360
|
-
user.profile, portfolio=instance.portfolio
|
|
361
|
-
),
|
|
362
|
-
custom={
|
|
363
|
-
"_transition_button": ActionButton(
|
|
364
|
-
method=RequestType.PATCH,
|
|
365
|
-
identifiers=("wbportfolio:trade",),
|
|
366
|
-
icon=WBIcon.UNDO.icon,
|
|
367
|
-
key="backtodraft",
|
|
368
|
-
label="Back to Draft",
|
|
369
|
-
action_label="backtodraft",
|
|
370
|
-
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
371
|
-
)
|
|
372
|
-
},
|
|
373
|
-
)
|
|
374
|
-
def backtodraft(self, **kwargs):
|
|
375
|
-
pass
|
|
376
|
-
|
|
377
|
-
@transition(
|
|
378
|
-
field=status,
|
|
379
|
-
source=Status.EXECUTED,
|
|
380
|
-
target=Status.DRAFT,
|
|
381
|
-
permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
|
|
382
|
-
user.profile, portfolio=instance.portfolio
|
|
383
|
-
),
|
|
384
|
-
custom={
|
|
385
|
-
"_transition_button": ActionButton(
|
|
386
|
-
method=RequestType.PATCH,
|
|
387
|
-
identifiers=("wbportfolio:trade",),
|
|
388
|
-
icon=WBIcon.UNDO.icon,
|
|
389
|
-
key="reverte",
|
|
390
|
-
label="Revert",
|
|
391
|
-
action_label="revert",
|
|
392
|
-
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
393
|
-
)
|
|
394
|
-
},
|
|
395
|
-
)
|
|
396
|
-
def revert(self, to_date=None, **kwargs):
|
|
397
|
-
with suppress(AssetPosition.DoesNotExist):
|
|
398
|
-
asset = AssetPosition.unannotated_objects.get(
|
|
399
|
-
underlying_quote=self.underlying_instrument,
|
|
400
|
-
portfolio=self.portfolio,
|
|
401
|
-
date=self.transaction_date,
|
|
402
|
-
is_estimated=False,
|
|
403
|
-
)
|
|
404
|
-
asset.set_weighting(asset.weighting - self.weighting)
|
|
405
|
-
asset.save()
|
|
406
|
-
|
|
407
160
|
@property
|
|
408
161
|
def product(self):
|
|
409
162
|
from wbportfolio.models.products import Product
|
|
@@ -413,63 +166,13 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
413
166
|
except Product.DoesNotExist:
|
|
414
167
|
return None
|
|
415
168
|
|
|
416
|
-
|
|
417
|
-
@admin.display(description="Last Effective Date")
|
|
418
|
-
def _last_effective_date(self) -> date:
|
|
419
|
-
if hasattr(self, "last_effective_date"):
|
|
420
|
-
return self.last_effective_date
|
|
421
|
-
elif (
|
|
422
|
-
assets := AssetPosition.unannotated_objects.filter(
|
|
423
|
-
underlying_quote=self.underlying_instrument,
|
|
424
|
-
date__lte=self.value_date,
|
|
425
|
-
portfolio=self.portfolio,
|
|
426
|
-
)
|
|
427
|
-
).exists():
|
|
428
|
-
return assets.latest("date").date
|
|
429
|
-
|
|
430
|
-
@property
|
|
431
|
-
@admin.display(description="Effective Weight")
|
|
432
|
-
def _effective_weight(self) -> Decimal:
|
|
433
|
-
return getattr(
|
|
434
|
-
self,
|
|
435
|
-
"effective_weight",
|
|
436
|
-
AssetPosition.unannotated_objects.filter(
|
|
437
|
-
underlying_quote=self.underlying_instrument,
|
|
438
|
-
date=self._last_effective_date,
|
|
439
|
-
portfolio=self.portfolio,
|
|
440
|
-
).aggregate(s=Sum("weighting"))["s"]
|
|
441
|
-
or Decimal(0),
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
@property
|
|
445
|
-
@admin.display(description="Effective Shares")
|
|
446
|
-
def _effective_shares(self) -> Decimal:
|
|
447
|
-
return getattr(
|
|
448
|
-
self,
|
|
449
|
-
"effective_shares",
|
|
450
|
-
AssetPosition.objects.filter(
|
|
451
|
-
underlying_quote=self.underlying_instrument,
|
|
452
|
-
date=self.transaction_date,
|
|
453
|
-
portfolio=self.portfolio,
|
|
454
|
-
).aggregate(s=Sum("shares"))["s"]
|
|
455
|
-
or Decimal(0),
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
@property
|
|
459
|
-
@admin.display(description="Target Weight")
|
|
460
|
-
def _target_weight(self) -> Decimal:
|
|
461
|
-
return getattr(self, "target_weight", self._effective_weight + self.weighting)
|
|
462
|
-
|
|
463
|
-
@property
|
|
464
|
-
@admin.display(description="Target Shares")
|
|
465
|
-
def _target_shares(self) -> Decimal:
|
|
466
|
-
return getattr(self, "target_shares", self._effective_shares + self.shares)
|
|
467
|
-
|
|
468
|
-
order_with_respect_to = "trade_proposal"
|
|
469
|
-
|
|
470
|
-
class Meta(OrderedModel.Meta):
|
|
169
|
+
class Meta:
|
|
471
170
|
verbose_name = "Trade"
|
|
472
171
|
verbose_name_plural = "Trades"
|
|
172
|
+
indexes = [
|
|
173
|
+
models.Index(fields=["underlying_instrument", "transaction_date"]),
|
|
174
|
+
models.Index(fields=["portfolio", "underlying_instrument", "transaction_date"]),
|
|
175
|
+
]
|
|
473
176
|
constraints = [
|
|
474
177
|
models.CheckConstraint(
|
|
475
178
|
check=models.Q(marked_as_internal=False)
|
|
@@ -490,42 +193,34 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
490
193
|
]
|
|
491
194
|
# notification_email_template = "portfolio/email/trade_notification.html"
|
|
492
195
|
|
|
493
|
-
def __init__(self, *args, target_weight: Decimal | None = None, **kwargs):
|
|
494
|
-
super().__init__(*args, **kwargs)
|
|
495
|
-
if target_weight is not None: # if target weight is provided, we guess the corresponding weighting
|
|
496
|
-
self.weighting = Decimal(target_weight) - self._effective_weight
|
|
497
|
-
self._set_transaction_subtype()
|
|
498
|
-
|
|
499
196
|
def save(self, *args, **kwargs):
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
self.
|
|
503
|
-
self.value_date = self.trade_proposal.last_effective_date
|
|
504
|
-
if not self.portfolio.only_weighting:
|
|
505
|
-
with suppress(ValueError):
|
|
506
|
-
self.shares = self.trade_proposal.get_estimated_shares(self.weighting, self.underlying_instrument)
|
|
197
|
+
self.pre_save()
|
|
198
|
+
if not self.weighting and (total_asset_value := self.portfolio.get_total_asset_value(self.transaction_date)):
|
|
199
|
+
self.weighting = self.currency_fx_rate * self.price * self.shares / total_asset_value
|
|
507
200
|
|
|
508
|
-
if
|
|
509
|
-
self.
|
|
510
|
-
if self.price
|
|
201
|
+
if abs(self.weighting) < 10e-6:
|
|
202
|
+
self.weighting = Decimal("0")
|
|
203
|
+
if not self.price:
|
|
511
204
|
# we try to get the price if not provided directly from the underlying instrument
|
|
512
|
-
|
|
513
|
-
self.price = self.underlying_instrument.get_price(self.value_date)
|
|
205
|
+
self.price = self.get_price()
|
|
514
206
|
|
|
515
|
-
self.
|
|
207
|
+
if not self.custodian and self.bank:
|
|
208
|
+
self.custodian = Custodian.get_by_mapping(self.bank)
|
|
516
209
|
|
|
517
|
-
if self.transaction_subtype is None
|
|
210
|
+
if self.transaction_subtype is None:
|
|
518
211
|
# if subtype not provided, we extract it automatically from the existing data.
|
|
519
|
-
self.
|
|
212
|
+
self._set_type()
|
|
520
213
|
if self.id and hasattr(self, "claims"):
|
|
521
|
-
self.claimed_shares = self.
|
|
522
|
-
"s"
|
|
523
|
-
] or Decimal(0)
|
|
214
|
+
self.claimed_shares = self.claims.filter(status="APPROVED").aggregate(s=Sum("shares"))["s"] or Decimal(0)
|
|
524
215
|
if self.internal_trade:
|
|
525
216
|
self.marked_as_internal = True
|
|
217
|
+
if not self.value_date:
|
|
218
|
+
self.value_date = self.transaction_date
|
|
219
|
+
if not self.book_date:
|
|
220
|
+
self.book_date = self.transaction_date
|
|
526
221
|
super().save(*args, **kwargs)
|
|
527
222
|
|
|
528
|
-
def
|
|
223
|
+
def _set_type(self):
|
|
529
224
|
if self.weighting == 0:
|
|
530
225
|
self.transaction_subtype = Trade.Type.NO_CHANGE
|
|
531
226
|
if self.underlying_instrument.instrument_type.key == "product":
|
|
@@ -536,45 +231,17 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
536
231
|
self.transaction_subtype = Trade.Type.REDEMPTION
|
|
537
232
|
elif self.weighting is not None:
|
|
538
233
|
if self.weighting > 0:
|
|
539
|
-
|
|
540
|
-
self.transaction_subtype = Trade.Type.INCREASE
|
|
541
|
-
else:
|
|
542
|
-
self.transaction_subtype = Trade.Type.BUY
|
|
234
|
+
self.transaction_subtype = Trade.Type.INCREASE
|
|
543
235
|
elif self.weighting < 0:
|
|
544
|
-
|
|
545
|
-
self.transaction_subtype = Trade.Type.DECREASE
|
|
546
|
-
else:
|
|
547
|
-
self.transaction_subtype = Trade.Type.SELL
|
|
236
|
+
self.transaction_subtype = Trade.Type.DECREASE
|
|
548
237
|
else:
|
|
549
238
|
self.transaction_subtype = Trade.Type.REBALANCE
|
|
550
239
|
|
|
551
|
-
def
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
def get_asset(self) -> AssetPosition:
|
|
558
|
-
last_underlying_quote_price = self.last_underlying_quote_price
|
|
559
|
-
if not last_underlying_quote_price:
|
|
560
|
-
raise ValueError("No price found")
|
|
561
|
-
asset = AssetPosition(
|
|
562
|
-
underlying_quote=self.underlying_instrument,
|
|
563
|
-
portfolio_created=None,
|
|
564
|
-
portfolio=self.portfolio,
|
|
565
|
-
date=self.transaction_date,
|
|
566
|
-
initial_currency_fx_rate=self.currency_fx_rate,
|
|
567
|
-
weighting=self._target_weight,
|
|
568
|
-
initial_price=self.last_underlying_quote_price.net_value,
|
|
569
|
-
initial_shares=None,
|
|
570
|
-
underlying_quote_price=self.last_underlying_quote_price,
|
|
571
|
-
asset_valuation_date=self.transaction_date,
|
|
572
|
-
currency=self.currency,
|
|
573
|
-
is_estimated=False,
|
|
574
|
-
)
|
|
575
|
-
asset.set_weighting(self._target_weight)
|
|
576
|
-
asset.pre_save()
|
|
577
|
-
return asset
|
|
240
|
+
def get_price(self) -> Decimal:
|
|
241
|
+
try:
|
|
242
|
+
return self.underlying_instrument.get_price(self.transaction_date)
|
|
243
|
+
except ValueError:
|
|
244
|
+
return Decimal("0")
|
|
578
245
|
|
|
579
246
|
def delete(self, **kwargs):
|
|
580
247
|
pre_collection.send(sender=self.__class__, instance=self)
|
|
@@ -584,22 +251,6 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
584
251
|
ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
|
|
585
252
|
return f"{ticker}{self.shares} ({self.bank})"
|
|
586
253
|
|
|
587
|
-
def _build_dto(self) -> TradeDTO:
|
|
588
|
-
"""
|
|
589
|
-
Data Transfer Object
|
|
590
|
-
Returns:
|
|
591
|
-
DTO trade object
|
|
592
|
-
"""
|
|
593
|
-
return TradeDTO(
|
|
594
|
-
id=self.id,
|
|
595
|
-
underlying_instrument=self.underlying_instrument.id,
|
|
596
|
-
effective_weight=self._effective_weight,
|
|
597
|
-
target_weight=self._target_weight,
|
|
598
|
-
instrument_type=self.underlying_instrument.security_instrument_type.id,
|
|
599
|
-
currency=self.underlying_instrument.currency,
|
|
600
|
-
date=self.transaction_date,
|
|
601
|
-
)
|
|
602
|
-
|
|
603
254
|
def get_alternative_valid_trades(self, share_delta: float = 0):
|
|
604
255
|
return Trade.objects.filter(
|
|
605
256
|
Q(underlying_instrument=self.underlying_instrument)
|
|
@@ -742,6 +393,10 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
742
393
|
def get_representation_label_key(cls):
|
|
743
394
|
return "{{|:-}}{{transaction_date}}{{|::}}{{bank}}{{|-:}} {{claimed_shares}} / {{shares}} (∆ {{diff_shares}})"
|
|
744
395
|
|
|
396
|
+
@classmethod
|
|
397
|
+
def get_representation_value_key(cls):
|
|
398
|
+
return "id"
|
|
399
|
+
|
|
745
400
|
|
|
746
401
|
@shared_task
|
|
747
402
|
def align_custodian():
|
|
@@ -756,3 +411,27 @@ def align_custodian():
|
|
|
756
411
|
def compute_claimed_shares_on_claim_save(sender, instance, created, raw, **kwargs):
|
|
757
412
|
if not raw and instance.trade:
|
|
758
413
|
instance.trade.save()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@receiver(pre_merge, sender="wbfdm.Instrument")
|
|
417
|
+
def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
|
|
418
|
+
"""
|
|
419
|
+
Simply reassign the transactions linked to the merged instrument to the main instrument
|
|
420
|
+
"""
|
|
421
|
+
merged_object.trades.update(underlying_instrument=main_object)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
|
|
425
|
+
def add_instrument_to_investable_universe_from_transactions(sender: models.Model, **kwargs) -> list[int]:
|
|
426
|
+
"""
|
|
427
|
+
register all instrument linked to assets as within the investible universe
|
|
428
|
+
"""
|
|
429
|
+
return list(
|
|
430
|
+
(
|
|
431
|
+
Instrument.objects.annotate(
|
|
432
|
+
transaction_exists=models.Exists(Trade.objects.filter(underlying_instrument=models.OuterRef("pk")))
|
|
433
|
+
).filter(transaction_exists=True)
|
|
434
|
+
)
|
|
435
|
+
.distinct()
|
|
436
|
+
.values_list("id", flat=True)
|
|
437
|
+
)
|