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
|
@@ -61,6 +61,9 @@ class InstrumentPortfolioThroughModel(models.Model):
|
|
|
61
61
|
models.UniqueConstraint(fields=["instrument", "portfolio"], name="unique_portfolio_relationship"),
|
|
62
62
|
]
|
|
63
63
|
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return f"{self.instrument} - {self.portfolio}"
|
|
66
|
+
|
|
64
67
|
@classmethod
|
|
65
68
|
def get_portfolio(cls, instrument):
|
|
66
69
|
with suppress(InstrumentPortfolioThroughModel.DoesNotExist):
|
|
@@ -99,6 +102,9 @@ class PortfolioInstrumentPreferredClassificationThroughModel(models.Model):
|
|
|
99
102
|
related_name="preferred_classification_group_throughs",
|
|
100
103
|
)
|
|
101
104
|
|
|
105
|
+
def __str__(self) -> str:
|
|
106
|
+
return f"{self.portfolio} - {self.instrument}: ({self.classification})"
|
|
107
|
+
|
|
102
108
|
def save(self, *args, **kwargs) -> None:
|
|
103
109
|
if not self.classification_group and self.classification:
|
|
104
110
|
self.classification_group = self.classification.group
|
wbportfolio/models/products.py
CHANGED
|
@@ -4,6 +4,7 @@ from decimal import Decimal
|
|
|
4
4
|
|
|
5
5
|
from celery import shared_task
|
|
6
6
|
from django.contrib import admin
|
|
7
|
+
from django.contrib.contenttypes.fields import GenericRelation
|
|
7
8
|
from django.contrib.postgres.constraints import ExclusionConstraint
|
|
8
9
|
from django.contrib.postgres.fields import DateRangeField, RangeOperators
|
|
9
10
|
from django.db import models
|
|
@@ -33,6 +34,7 @@ from wbcore.utils.enum import ChoiceEnum
|
|
|
33
34
|
from wbcrm.models.accounts import Account
|
|
34
35
|
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
35
36
|
from wbfdm.models.instruments.instruments import InstrumentManager, InstrumentType
|
|
37
|
+
from wbreport.models import Report
|
|
36
38
|
|
|
37
39
|
from wbportfolio.models.portfolio_relationship import InstrumentPortfolioThroughModel
|
|
38
40
|
|
|
@@ -188,6 +190,9 @@ class FeeProductPercentage(models.Model):
|
|
|
188
190
|
),
|
|
189
191
|
]
|
|
190
192
|
|
|
193
|
+
def __str__(self) -> str:
|
|
194
|
+
return f"{self.product.name} ({self.type})"
|
|
195
|
+
|
|
191
196
|
@property
|
|
192
197
|
def net_percent(self) -> Decimal:
|
|
193
198
|
return self.percent
|
|
@@ -198,6 +203,8 @@ class FeeProductPercentage(models.Model):
|
|
|
198
203
|
|
|
199
204
|
|
|
200
205
|
class Product(PMSInstrumentAbstractModel):
|
|
206
|
+
reports = GenericRelation(Report)
|
|
207
|
+
|
|
201
208
|
share_price = models.PositiveIntegerField(
|
|
202
209
|
default=100,
|
|
203
210
|
verbose_name="Share Price",
|
|
@@ -344,6 +351,11 @@ class Product(PMSInstrumentAbstractModel):
|
|
|
344
351
|
|
|
345
352
|
self.is_managed = True
|
|
346
353
|
|
|
354
|
+
def save(self, *args, **kwargs):
|
|
355
|
+
super().save(*args, **kwargs)
|
|
356
|
+
if self.delisted_date and self.delisted_date <= date.today():
|
|
357
|
+
self.reports.update(is_active=False)
|
|
358
|
+
|
|
347
359
|
def get_title(self):
|
|
348
360
|
if self.parent:
|
|
349
361
|
return f"{self.parent.name} ({self.name})"
|
|
@@ -14,8 +14,8 @@ from wbcore.utils.importlib import import_from_dotted_path
|
|
|
14
14
|
from wbcore.utils.models import ComplexToStringMixin
|
|
15
15
|
from wbcore.utils.rrules import convert_rrulestr_to_dict, humanize_rrule
|
|
16
16
|
|
|
17
|
+
from wbportfolio.models.orders.order_proposals import OrderProposal
|
|
17
18
|
from wbportfolio.models.portfolio import Portfolio
|
|
18
|
-
from wbportfolio.models.transactions.trade_proposals import TradeProposal
|
|
19
19
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
20
20
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
21
21
|
|
|
@@ -43,9 +43,16 @@ class RebalancingModel(models.Model):
|
|
|
43
43
|
return import_from_dotted_path(self.class_path)
|
|
44
44
|
|
|
45
45
|
def get_target_portfolio(
|
|
46
|
-
self,
|
|
46
|
+
self,
|
|
47
|
+
portfolio: Portfolio,
|
|
48
|
+
trade_date: date,
|
|
49
|
+
last_effective_date: date,
|
|
50
|
+
effective_portfolio: PortfolioDTO | None = None,
|
|
51
|
+
**kwargs,
|
|
47
52
|
) -> PortfolioDTO:
|
|
48
|
-
model = self.model_class(
|
|
53
|
+
model = self.model_class(
|
|
54
|
+
portfolio, trade_date, last_effective_date, effective_portfolio=effective_portfolio, **kwargs
|
|
55
|
+
)
|
|
49
56
|
if not model.is_valid():
|
|
50
57
|
raise ValidationError(model.validation_errors)
|
|
51
58
|
return model.get_target_portfolio()
|
|
@@ -71,8 +78,8 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
71
78
|
RebalancingModel, on_delete=models.PROTECT, related_name="rebalancers", verbose_name="Rebalancing Model"
|
|
72
79
|
)
|
|
73
80
|
parameters = models.JSONField(default=dict, verbose_name="Parameters", blank=True)
|
|
74
|
-
|
|
75
|
-
default=False, verbose_name="Apply
|
|
81
|
+
apply_order_proposal_automatically = models.BooleanField(
|
|
82
|
+
default=False, verbose_name="Apply Order Proposal Automatically"
|
|
76
83
|
)
|
|
77
84
|
activation_date = models.DateField(verbose_name="Activation Date")
|
|
78
85
|
frequency = models.CharField(
|
|
@@ -82,6 +89,9 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
82
89
|
help_text=_("The Evaluation Frequency in RRULE format"),
|
|
83
90
|
)
|
|
84
91
|
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
return f"{self.portfolio.name} ({self.rebalancing_model})"
|
|
94
|
+
|
|
85
95
|
def save(self, *args, **kwargs):
|
|
86
96
|
if not self.activation_date:
|
|
87
97
|
try:
|
|
@@ -92,19 +102,19 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
92
102
|
|
|
93
103
|
def _get_next_valid_date(self, valid_date: date) -> date:
|
|
94
104
|
pivot_date = valid_date
|
|
95
|
-
while
|
|
96
|
-
portfolio=self.portfolio, status=
|
|
105
|
+
while OrderProposal.objects.filter(
|
|
106
|
+
portfolio=self.portfolio, status=OrderProposal.Status.FAILED, trade_date=pivot_date
|
|
97
107
|
).exists():
|
|
98
108
|
pivot_date = (pivot_date + BDay(1)).date()
|
|
99
109
|
return pivot_date
|
|
100
110
|
|
|
101
111
|
def is_valid(self, trade_date: date) -> bool:
|
|
102
|
-
if
|
|
112
|
+
if OrderProposal.objects.filter(
|
|
103
113
|
portfolio=self.portfolio,
|
|
104
|
-
status=
|
|
114
|
+
status=OrderProposal.Status.CONFIRMED,
|
|
105
115
|
trade_date=trade_date,
|
|
106
116
|
rebalancing_model__isnull=True,
|
|
107
|
-
).exists(): # if a already
|
|
117
|
+
).exists(): # if a already applied order proposal exists, we do not allow a re-evaluatioon of the rebalancing (only possible if "replayed")
|
|
108
118
|
return False
|
|
109
119
|
for initial_valid_datetime in self.get_rrule(trade_date):
|
|
110
120
|
initial_valid_date = initial_valid_datetime.date()
|
|
@@ -115,8 +125,8 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
115
125
|
break
|
|
116
126
|
return False
|
|
117
127
|
|
|
118
|
-
def evaluate_rebalancing(self, trade_date: date):
|
|
119
|
-
|
|
128
|
+
def evaluate_rebalancing(self, trade_date: date, effective_portfolio=None):
|
|
129
|
+
order_proposal, _ = OrderProposal.objects.get_or_create(
|
|
120
130
|
trade_date=trade_date,
|
|
121
131
|
portfolio=self.portfolio,
|
|
122
132
|
defaults={
|
|
@@ -124,27 +134,30 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
124
134
|
"rebalancing_model": self.rebalancing_model,
|
|
125
135
|
},
|
|
126
136
|
)
|
|
127
|
-
|
|
128
|
-
if
|
|
129
|
-
trade_proposal.status = TradeProposal.Status.DRAFT
|
|
137
|
+
order_proposal.portfolio = self.portfolio
|
|
138
|
+
if order_proposal.rebalancing_model == self.rebalancing_model:
|
|
130
139
|
try:
|
|
131
140
|
logger.info(
|
|
132
141
|
f"Getting target portfolio ({self.portfolio}) for rebalancing model {self.rebalancing_model} for trade date {trade_date:%Y-%m-%d}"
|
|
133
142
|
)
|
|
134
143
|
target_portfolio = self.rebalancing_model.get_target_portfolio(
|
|
135
|
-
self.portfolio,
|
|
144
|
+
self.portfolio,
|
|
145
|
+
order_proposal.trade_date,
|
|
146
|
+
order_proposal.value_date,
|
|
147
|
+
effective_portfolio=effective_portfolio,
|
|
148
|
+
**self.parameters,
|
|
136
149
|
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return
|
|
150
|
+
order_proposal.apply_workflow(
|
|
151
|
+
apply_automatically=self.apply_order_proposal_automatically,
|
|
152
|
+
target_portfolio=target_portfolio,
|
|
153
|
+
effective_portfolio=effective_portfolio,
|
|
154
|
+
)
|
|
155
|
+
except ValidationError as e:
|
|
156
|
+
logger.warning(f"Validation error while approving the orders: {e}")
|
|
157
|
+
# If we encountered a validation error, we set the order proposal as failed
|
|
158
|
+
order_proposal.status = OrderProposal.Status.FAILED
|
|
159
|
+
order_proposal.save()
|
|
160
|
+
return order_proposal
|
|
148
161
|
|
|
149
162
|
@property
|
|
150
163
|
def rrule(self):
|
wbportfolio/models/roles.py
CHANGED
|
@@ -52,16 +52,10 @@ class PortfolioRole(models.Model):
|
|
|
52
52
|
return f"{self.role_type} {self.person.computed_str}"
|
|
53
53
|
|
|
54
54
|
def save(self, *args, **kwargs):
|
|
55
|
-
|
|
56
|
-
self.
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
self.RoleType.ANALYST,
|
|
60
|
-
], self.default_error_messages["manager"].format(model="instrument")
|
|
61
|
-
|
|
62
|
-
assert (self.start and self.end and self.start < self.end) or (
|
|
63
|
-
not self.start or not self.end
|
|
64
|
-
), self.default_error_messages["start_end"]
|
|
55
|
+
if self.role_type in [self.RoleType.MANAGER, self.RoleType.RISK_MANAGER] and self.instrument:
|
|
56
|
+
raise ValueError(self.default_error_messages["manager"].format(model="instrument"))
|
|
57
|
+
if self.start and self.end and self.start > self.end:
|
|
58
|
+
raise ValueError(self.default_error_messages["start_end"])
|
|
65
59
|
|
|
66
60
|
super().save(*args, **kwargs)
|
|
67
61
|
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
from .claim import Claim
|
|
2
2
|
from .dividends import DividendTransaction
|
|
3
|
-
from .expiry import Expiry
|
|
4
3
|
from .fees import FeeCalculation, Fees
|
|
5
|
-
from .trade_proposals import TradeProposal
|
|
6
|
-
from .rebalancing import RebalancingModel, Rebalancer
|
|
7
4
|
from .trades import Trade
|
|
8
|
-
from .transactions import Transaction
|
|
@@ -259,9 +259,10 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
259
259
|
return f"{self.reference_id} {self.product.name} ({self.bank} - {self.shares:,} shares - {self.date}) "
|
|
260
260
|
|
|
261
261
|
def save(self, *args, auto_match: bool = True, **kwargs):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
if self.shares is None and self.nominal_amount is None:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Either shares or nominal amount have to be provided. Shares={self.shares}, Nominal={self.nominal_amount}"
|
|
265
|
+
)
|
|
265
266
|
if self.product:
|
|
266
267
|
if self.shares is not None:
|
|
267
268
|
self.nominal_amount = self.shares * self.product.share_price
|
|
@@ -447,7 +448,7 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
447
448
|
return self.can_approve()
|
|
448
449
|
|
|
449
450
|
def auto_match(self) -> Trade | None:
|
|
450
|
-
|
|
451
|
+
shares_epsilon = 1 # share
|
|
451
452
|
auto_match_trade = None
|
|
452
453
|
# Obvious filtering
|
|
453
454
|
trades = Trade.valid_customer_trade_objects.filter(
|
|
@@ -458,7 +459,7 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
458
459
|
trades = trades.filter(underlying_instrument=self.product)
|
|
459
460
|
# Find trades by shares (or remaining to be claimed)
|
|
460
461
|
trades = trades.filter(
|
|
461
|
-
Q(diff_shares__lte=self.shares +
|
|
462
|
+
Q(diff_shares__lte=self.shares + shares_epsilon) & Q(diff_shares__gte=self.shares - shares_epsilon)
|
|
462
463
|
)
|
|
463
464
|
if trades.count() == 1:
|
|
464
465
|
auto_match_trade = trades.first()
|
|
@@ -477,7 +478,7 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
477
478
|
# Find trades by external_id
|
|
478
479
|
if not auto_match_trade and self.external_id and trades.count() > 1:
|
|
479
480
|
trades = trades.filter(
|
|
480
|
-
Q(external_id__icontains=self.external_id) | Q(
|
|
481
|
+
Q(external_id__icontains=self.external_id) | Q(external_id_alternative__icontains=self.external_id)
|
|
481
482
|
)
|
|
482
483
|
if trades.count() == 1:
|
|
483
484
|
auto_match_trade = trades.first()
|
|
@@ -1,15 +1,52 @@
|
|
|
1
|
-
from decimal import Decimal
|
|
2
|
-
|
|
3
1
|
from django.db import models
|
|
2
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
4
3
|
|
|
5
4
|
from wbportfolio.import_export.handlers.dividend import DividendImportHandler
|
|
6
5
|
|
|
7
|
-
from .transactions import
|
|
6
|
+
from .transactions import TransactionMixin
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
class DividendTransaction(
|
|
9
|
+
class DividendTransaction(TransactionMixin, ImportMixin, models.Model):
|
|
11
10
|
import_export_handler_class = DividendImportHandler
|
|
11
|
+
|
|
12
|
+
class DistributionMethod(models.TextChoices):
|
|
13
|
+
PAYMENT = "Payment", "Payment"
|
|
14
|
+
REINVESTMENT = "Reinvestment", "Reinvestment"
|
|
15
|
+
|
|
16
|
+
ex_date = models.DateField(
|
|
17
|
+
verbose_name="Ex-Dividend Date",
|
|
18
|
+
help_text="The date on which the stock starts trading without the dividend",
|
|
19
|
+
)
|
|
20
|
+
record_date = models.DateField(
|
|
21
|
+
verbose_name="Record Date",
|
|
22
|
+
help_text="The date on which the holder must own the shares to be eligible for the dividend",
|
|
23
|
+
)
|
|
24
|
+
distribution_method = models.CharField(
|
|
25
|
+
max_length=255, verbose_name="Type", choices=DistributionMethod.choices, default=DistributionMethod.PAYMENT
|
|
26
|
+
)
|
|
12
27
|
retrocession = models.FloatField(default=1)
|
|
28
|
+
price = models.DecimalField(
|
|
29
|
+
max_digits=15,
|
|
30
|
+
decimal_places=4,
|
|
31
|
+
help_text="The amount paid per share",
|
|
32
|
+
verbose_name="DPS",
|
|
33
|
+
)
|
|
34
|
+
total_value_gross = models.GeneratedField(
|
|
35
|
+
expression=models.F("price") * models.F("shares") * models.F("retrocession"),
|
|
36
|
+
output_field=models.DecimalField(
|
|
37
|
+
max_digits=20,
|
|
38
|
+
decimal_places=4,
|
|
39
|
+
),
|
|
40
|
+
db_persist=True,
|
|
41
|
+
)
|
|
13
42
|
|
|
14
43
|
def save(self, *args, **kwargs):
|
|
15
|
-
|
|
44
|
+
self.pre_save()
|
|
45
|
+
if not self.record_date and self.ex_date:
|
|
46
|
+
self.record_date = self.ex_date
|
|
47
|
+
elif self.record_date and not self.ex_date:
|
|
48
|
+
self.ex_date = self.record_date
|
|
49
|
+
super().save(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return f"{self.total_value} - {self.value_date:%d.%m.%Y} : {str(self.underlying_instrument)} (in {str(self.portfolio)})"
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
from contextlib import suppress
|
|
3
|
+
from decimal import Decimal
|
|
3
4
|
|
|
4
5
|
from celery import shared_task
|
|
5
6
|
from django.db import models
|
|
6
7
|
from django.db.models import Exists, OuterRef, Q, QuerySet
|
|
7
8
|
from django.dispatch import receiver
|
|
9
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
8
10
|
from wbfdm.models.instruments.instrument_prices import InstrumentPrice
|
|
9
11
|
|
|
10
12
|
from wbportfolio.import_export.handlers.fees import FeesImportHandler
|
|
11
13
|
from wbportfolio.models.products import Product
|
|
12
14
|
|
|
13
|
-
from .transactions import Transaction
|
|
14
|
-
|
|
15
15
|
|
|
16
16
|
class ValidFeesQueryset(QuerySet):
|
|
17
17
|
def filter_only_valid_fees(self) -> QuerySet:
|
|
@@ -22,7 +22,7 @@ class ValidFeesQueryset(QuerySet):
|
|
|
22
22
|
real_fees_exists=Exists(
|
|
23
23
|
self.filter(
|
|
24
24
|
transaction_subtype=OuterRef("transaction_subtype"),
|
|
25
|
-
|
|
25
|
+
product=OuterRef("product"),
|
|
26
26
|
fee_date=OuterRef("fee_date"),
|
|
27
27
|
calculated=False,
|
|
28
28
|
)
|
|
@@ -43,7 +43,7 @@ class ValidFeesManager(DefaultFeesManager):
|
|
|
43
43
|
return super().get_queryset().filter_only_valid_fees()
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
class Fees(
|
|
46
|
+
class Fees(ImportMixin, models.Model):
|
|
47
47
|
import_export_handler_class = FeesImportHandler
|
|
48
48
|
|
|
49
49
|
class Type(models.TextChoices):
|
|
@@ -57,31 +57,61 @@ class Fees(Transaction):
|
|
|
57
57
|
transaction_subtype = models.CharField(
|
|
58
58
|
max_length=255, verbose_name="Fees Type", choices=Type.choices, default=Type.MANAGEMENT
|
|
59
59
|
)
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
fee_date = models.DateField(
|
|
61
|
+
verbose_name="Fees Date",
|
|
62
|
+
help_text="The date that this fee was paid.",
|
|
63
|
+
) # needed for indexing
|
|
64
|
+
product = models.ForeignKey(
|
|
65
|
+
"wbportfolio.Product",
|
|
66
|
+
related_name="fees",
|
|
67
|
+
on_delete=models.PROTECT,
|
|
68
|
+
verbose_name="Product",
|
|
69
|
+
)
|
|
70
|
+
currency = models.ForeignKey(
|
|
71
|
+
"currency.Currency",
|
|
72
|
+
related_name="fees",
|
|
73
|
+
on_delete=models.PROTECT,
|
|
74
|
+
verbose_name="Currency",
|
|
75
|
+
)
|
|
76
|
+
currency_fx_rate = models.DecimalField(
|
|
77
|
+
max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
|
|
78
|
+
)
|
|
79
|
+
total_value = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value")
|
|
80
|
+
total_value_gross = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value Gross")
|
|
81
|
+
total_value_fx_portfolio = models.GeneratedField(
|
|
82
|
+
expression=models.F("currency_fx_rate") * models.F("total_value"),
|
|
83
|
+
output_field=models.DecimalField(
|
|
84
|
+
max_digits=20,
|
|
85
|
+
decimal_places=4,
|
|
86
|
+
),
|
|
87
|
+
db_persist=True,
|
|
88
|
+
)
|
|
89
|
+
total_value_gross_fx_portfolio = models.GeneratedField(
|
|
90
|
+
expression=models.F("currency_fx_rate") * models.F("total_value_gross"),
|
|
91
|
+
output_field=models.DecimalField(
|
|
92
|
+
max_digits=20,
|
|
93
|
+
decimal_places=4,
|
|
94
|
+
),
|
|
95
|
+
db_persist=True,
|
|
96
|
+
)
|
|
62
97
|
calculated = models.BooleanField(
|
|
63
98
|
default=True,
|
|
64
99
|
help_text="A marker whether the fees were calculated or supplied.",
|
|
65
100
|
verbose_name="Is calculated",
|
|
66
101
|
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"wbportfolio.Product",
|
|
70
|
-
related_name="transactionfees",
|
|
71
|
-
on_delete=models.PROTECT,
|
|
72
|
-
verbose_name="Product",
|
|
73
|
-
)
|
|
102
|
+
created = models.DateTimeField(auto_now_add=True)
|
|
103
|
+
updated = models.DateTimeField(auto_now=True)
|
|
74
104
|
|
|
75
105
|
class Meta:
|
|
76
106
|
verbose_name = "Fees"
|
|
77
107
|
verbose_name_plural = "Fees"
|
|
78
108
|
indexes = [
|
|
79
|
-
models.Index(fields=["
|
|
80
|
-
models.Index(fields=["transaction_subtype", "
|
|
109
|
+
models.Index(fields=["product"]),
|
|
110
|
+
models.Index(fields=["transaction_subtype", "product", "fee_date", "calculated"]),
|
|
81
111
|
]
|
|
82
112
|
constraints = [
|
|
83
113
|
models.UniqueConstraint(
|
|
84
|
-
fields=["
|
|
114
|
+
fields=["product", "fee_date", "transaction_subtype", "calculated"], name="unique_fees"
|
|
85
115
|
),
|
|
86
116
|
]
|
|
87
117
|
|
|
@@ -89,11 +119,14 @@ class Fees(Transaction):
|
|
|
89
119
|
valid_objects = ValidFeesManager()
|
|
90
120
|
|
|
91
121
|
def save(self, *args, **kwargs):
|
|
92
|
-
self.
|
|
122
|
+
if self.total_value_gross is None and self.total_value is not None:
|
|
123
|
+
self.total_value_gross = self.total_value
|
|
124
|
+
elif self.total_value is None and self.total_value_gross is not None:
|
|
125
|
+
self.total_value = self.total_value_gross
|
|
93
126
|
super().save(*args, **kwargs)
|
|
94
127
|
|
|
95
128
|
def __str__(self):
|
|
96
|
-
return f"{self.
|
|
129
|
+
return f"{self.fee_date:%d.%m.%Y} - {self.Type[self.transaction_subtype]}: {self.product.name}"
|
|
97
130
|
|
|
98
131
|
@classmethod
|
|
99
132
|
def get_endpoint_basename(cls):
|
|
@@ -111,8 +144,8 @@ class FeeCalculation(models.Model):
|
|
|
111
144
|
calculation_module = importlib.import_module(import_path)
|
|
112
145
|
for new_fees in calculation_module.fees_calculation(price.id):
|
|
113
146
|
Fees.objects.update_or_create(
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
product=new_fees.pop("product"),
|
|
148
|
+
fee_date=new_fees.pop("fee_date"),
|
|
116
149
|
transaction_subtype=new_fees.pop("transaction_subtype"),
|
|
117
150
|
calculated=True,
|
|
118
151
|
defaults=new_fees,
|
|
@@ -145,10 +178,10 @@ def update_or_create_fees_post(sender, instance, created, raw, **kwargs):
|
|
|
145
178
|
# .filter(
|
|
146
179
|
# transaction_date=instance.transaction_date,
|
|
147
180
|
# transaction_subtype=instance.transaction_subtype,
|
|
148
|
-
#
|
|
181
|
+
# product=instance.product,
|
|
149
182
|
# )
|
|
150
183
|
# .exists()
|
|
151
184
|
# ):
|
|
152
185
|
# raise ValueError(
|
|
153
|
-
# f"A fees object already exists with date, type and product = {instance.transaction_date}, {instance.
|
|
186
|
+
# f"A fees object already exists with date, type and product = {instance.transaction_date}, {instance.type}, {instance.product}"
|
|
154
187
|
# )
|