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,105 +1,94 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
|
-
from django.apps import apps
|
|
4
3
|
from django.db import models
|
|
5
|
-
from django.dispatch import receiver
|
|
6
|
-
from wbcore.contrib.io.mixins import ImportMixin
|
|
7
|
-
from wbcore.signals import pre_merge
|
|
8
|
-
from wbfdm.models.instruments.instruments import Instrument
|
|
9
|
-
from wbfdm.signals import add_instrument_to_investable_universe
|
|
10
4
|
|
|
11
5
|
|
|
12
|
-
class
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
verbose_name="
|
|
6
|
+
class TransactionMixin(models.Model):
|
|
7
|
+
value_date = models.DateField(
|
|
8
|
+
verbose_name="Value Date",
|
|
9
|
+
help_text="The date that this transaction was valuated/paid.",
|
|
10
|
+
)
|
|
11
|
+
portfolio = models.ForeignKey(
|
|
12
|
+
"wbportfolio.Portfolio", related_name="%(class)ss", on_delete=models.PROTECT, verbose_name="Portfolio"
|
|
13
|
+
)
|
|
14
|
+
underlying_instrument = models.ForeignKey(
|
|
15
|
+
to="wbfdm.Instrument",
|
|
16
|
+
related_name="%(class)ss",
|
|
17
|
+
limit_choices_to=models.Q(children__isnull=True),
|
|
18
|
+
on_delete=models.PROTECT,
|
|
19
|
+
verbose_name="Underlying Instrument",
|
|
20
|
+
help_text="The instrument that is this transaction.",
|
|
21
|
+
)
|
|
22
|
+
currency = models.ForeignKey(
|
|
23
|
+
"currency.Currency",
|
|
24
|
+
related_name="%(class)ss",
|
|
25
|
+
on_delete=models.PROTECT,
|
|
26
|
+
verbose_name="Currency",
|
|
27
|
+
)
|
|
28
|
+
currency_fx_rate = models.DecimalField(
|
|
29
|
+
max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
|
|
19
30
|
)
|
|
20
31
|
price = models.DecimalField(
|
|
21
32
|
max_digits=16,
|
|
22
33
|
decimal_places=4,
|
|
23
|
-
default=Decimal(
|
|
24
|
-
"0.0"
|
|
25
|
-
), # we shouldn't default to anything but we have trade with price=None. Needs to be handled carefully
|
|
26
34
|
help_text="The price per share.",
|
|
27
35
|
verbose_name="Price",
|
|
28
36
|
)
|
|
29
|
-
|
|
30
37
|
price_gross = models.DecimalField(
|
|
31
38
|
max_digits=16,
|
|
32
39
|
decimal_places=4,
|
|
33
40
|
help_text="The gross price per share.",
|
|
34
41
|
verbose_name="Gross Price",
|
|
35
42
|
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
):
|
|
43
|
-
if self.price_gross is None:
|
|
44
|
-
self.price_gross = self.price
|
|
45
|
-
|
|
46
|
-
self.total_value = self.price * self.shares * factor
|
|
47
|
-
self.total_value_gross = self.price_gross * self.shares * factor
|
|
48
|
-
super().save(*args, **kwargs)
|
|
49
|
-
|
|
50
|
-
class Meta:
|
|
51
|
-
abstract = True
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class Transaction(ImportMixin, models.Model):
|
|
55
|
-
class Type(models.TextChoices):
|
|
56
|
-
# Standart Asset Types
|
|
57
|
-
TRADE = "Trade", "Trade"
|
|
58
|
-
DIVIDEND_TRANSACTION = "DividendTransaction", "Dividend Transaction"
|
|
59
|
-
EXPIRY = "Expiry", "Expiry"
|
|
60
|
-
FEES = "Fees", "Fees"
|
|
61
|
-
|
|
62
|
-
transaction_type = models.CharField(max_length=255, verbose_name="Type", choices=Type.choices, default=Type.TRADE)
|
|
63
|
-
|
|
64
|
-
portfolio = models.ForeignKey(
|
|
65
|
-
"wbportfolio.Portfolio", related_name="transactions", on_delete=models.PROTECT, verbose_name="Portfolio"
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
underlying_instrument = models.ForeignKey(
|
|
69
|
-
to="wbfdm.Instrument",
|
|
70
|
-
related_name="transactions",
|
|
71
|
-
limit_choices_to=models.Q(children__isnull=True),
|
|
72
|
-
on_delete=models.PROTECT,
|
|
73
|
-
verbose_name="Underlying Instrument",
|
|
74
|
-
help_text="The instrument that is this transaction.",
|
|
43
|
+
shares = models.DecimalField(
|
|
44
|
+
max_digits=15,
|
|
45
|
+
decimal_places=4,
|
|
46
|
+
default=Decimal("0.0"),
|
|
47
|
+
help_text="The number of shares held at record date, used to calculate the dividend",
|
|
48
|
+
verbose_name="Shares / Quantity",
|
|
75
49
|
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
50
|
+
fees = models.GeneratedField(
|
|
51
|
+
expression=models.F("price_gross") - models.F("price"),
|
|
52
|
+
output_field=models.DecimalField(
|
|
53
|
+
max_digits=20,
|
|
54
|
+
decimal_places=4,
|
|
55
|
+
),
|
|
56
|
+
db_persist=True,
|
|
80
57
|
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
58
|
+
total_value_gross = models.GeneratedField(
|
|
59
|
+
expression=models.F("price_gross") * models.F("shares"),
|
|
60
|
+
output_field=models.DecimalField(
|
|
61
|
+
max_digits=20,
|
|
62
|
+
decimal_places=4,
|
|
63
|
+
),
|
|
64
|
+
db_persist=True,
|
|
84
65
|
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
total_value = models.GeneratedField(
|
|
67
|
+
expression=models.F("price") * models.F("shares"),
|
|
68
|
+
output_field=models.DecimalField(
|
|
69
|
+
max_digits=20,
|
|
70
|
+
decimal_places=4,
|
|
71
|
+
),
|
|
72
|
+
db_persist=True,
|
|
88
73
|
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
74
|
+
price_fx_portfolio = models.GeneratedField(
|
|
75
|
+
expression=models.F("currency_fx_rate") * models.F("price"),
|
|
76
|
+
output_field=models.DecimalField(
|
|
77
|
+
max_digits=20,
|
|
78
|
+
decimal_places=4,
|
|
79
|
+
),
|
|
80
|
+
db_persist=True,
|
|
95
81
|
)
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
price_gross_fx_portfolio = models.GeneratedField(
|
|
83
|
+
expression=models.F("currency_fx_rate") * models.F("price_gross"),
|
|
84
|
+
output_field=models.DecimalField(
|
|
85
|
+
max_digits=20,
|
|
86
|
+
decimal_places=4,
|
|
87
|
+
),
|
|
88
|
+
db_persist=True,
|
|
98
89
|
)
|
|
99
|
-
total_value = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value")
|
|
100
|
-
total_value_gross = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value Gross")
|
|
101
90
|
total_value_fx_portfolio = models.GeneratedField(
|
|
102
|
-
expression=models.F("currency_fx_rate") * models.F("
|
|
91
|
+
expression=models.F("currency_fx_rate") * models.F("price") * models.F("shares"),
|
|
103
92
|
output_field=models.DecimalField(
|
|
104
93
|
max_digits=20,
|
|
105
94
|
decimal_places=4,
|
|
@@ -107,99 +96,30 @@ class Transaction(ImportMixin, models.Model):
|
|
|
107
96
|
db_persist=True,
|
|
108
97
|
)
|
|
109
98
|
total_value_gross_fx_portfolio = models.GeneratedField(
|
|
110
|
-
expression=models.F("currency_fx_rate") * models.F("
|
|
99
|
+
expression=models.F("currency_fx_rate") * models.F("price_gross") * models.F("shares"),
|
|
111
100
|
output_field=models.DecimalField(
|
|
112
101
|
max_digits=20,
|
|
113
102
|
decimal_places=4,
|
|
114
103
|
),
|
|
115
104
|
db_persist=True,
|
|
116
105
|
)
|
|
117
|
-
external_id = models.CharField(
|
|
118
|
-
max_length=255,
|
|
119
|
-
null=True,
|
|
120
|
-
blank=True,
|
|
121
|
-
help_text="An external identifier that was supplied.",
|
|
122
|
-
verbose_name="External Identifier",
|
|
123
|
-
)
|
|
124
|
-
comment = models.TextField(default="", verbose_name="Comment", blank=True)
|
|
125
106
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if not self.book_date:
|
|
130
|
-
self.book_date = self.transaction_date
|
|
107
|
+
comment = models.TextField(default="", verbose_name="Comment", blank=True)
|
|
108
|
+
created = models.DateTimeField(auto_now_add=True)
|
|
109
|
+
updated = models.DateTimeField(auto_now=True)
|
|
131
110
|
|
|
132
|
-
|
|
111
|
+
def pre_save(self):
|
|
112
|
+
if self.underlying_instrument:
|
|
133
113
|
self.currency = self.underlying_instrument.currency
|
|
114
|
+
|
|
115
|
+
if self.price is not None and self.price_gross is None:
|
|
116
|
+
self.price_gross = self.price
|
|
117
|
+
elif self.price_gross is not None and self.price is None:
|
|
118
|
+
self.price = self.price_gross
|
|
134
119
|
if self.currency_fx_rate is None:
|
|
135
120
|
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
136
121
|
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
137
122
|
)
|
|
138
|
-
if not self.transaction_type:
|
|
139
|
-
self.transaction_type = self.__class__.__name__
|
|
140
|
-
|
|
141
|
-
if self.total_value_gross is None:
|
|
142
|
-
self.total_value_gross = self.total_value
|
|
143
|
-
|
|
144
|
-
super().save(*args, **kwargs)
|
|
145
|
-
|
|
146
|
-
def __str__(self):
|
|
147
|
-
return f"{self.total_value} - {self.transaction_date:%d.%m.%Y} : {str(self.underlying_instrument)} (in {str(self.portfolio)})"
|
|
148
|
-
|
|
149
|
-
def get_casted_model(self):
|
|
150
|
-
return apps.get_model(app_label="wbportfolio", model_name=self.transaction_type)
|
|
151
|
-
|
|
152
|
-
def get_casted_transaction(self) -> models.Model:
|
|
153
|
-
"""
|
|
154
|
-
Cast the asset into its child representative
|
|
155
|
-
"""
|
|
156
|
-
model = self.get_casted_model()
|
|
157
|
-
return model.objects.get(pk=self.pk)
|
|
158
123
|
|
|
159
124
|
class Meta:
|
|
160
|
-
|
|
161
|
-
verbose_name_plural = "Transactions"
|
|
162
|
-
indexes = [
|
|
163
|
-
models.Index(fields=["underlying_instrument", "transaction_date"]),
|
|
164
|
-
# models.Index(fields=["date", "underlying_instrument"]),
|
|
165
|
-
]
|
|
166
|
-
|
|
167
|
-
objects = models.Manager()
|
|
168
|
-
|
|
169
|
-
@classmethod
|
|
170
|
-
def get_representation_value_key(cls):
|
|
171
|
-
return "id"
|
|
172
|
-
|
|
173
|
-
@classmethod
|
|
174
|
-
def get_representation_label_key(cls):
|
|
175
|
-
return "{{total_value}}{{transaction_date}}"
|
|
176
|
-
|
|
177
|
-
@classmethod
|
|
178
|
-
def get_endpoint_basename(cls):
|
|
179
|
-
return "wbportfolio:transaction"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@receiver(pre_merge, sender="wbfdm.Instrument")
|
|
183
|
-
def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
|
|
184
|
-
"""
|
|
185
|
-
Simply reassign the transactions linked to the merged instrument to the main instrument
|
|
186
|
-
"""
|
|
187
|
-
merged_object.transactions.update(underlying_instrument=main_object)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
|
|
191
|
-
def add_instrument_to_investable_universe_from_transactions(sender: models.Model, **kwargs) -> list[int]:
|
|
192
|
-
"""
|
|
193
|
-
register all instrument linked to assets as within the investible universe
|
|
194
|
-
"""
|
|
195
|
-
return list(
|
|
196
|
-
(
|
|
197
|
-
Instrument.objects.annotate(
|
|
198
|
-
transaction_exists=models.Exists(
|
|
199
|
-
Transaction.objects.filter(underlying_instrument=models.OuterRef("pk"))
|
|
200
|
-
)
|
|
201
|
-
).filter(transaction_exists=True)
|
|
202
|
-
)
|
|
203
|
-
.distinct()
|
|
204
|
-
.values_list("id", flat=True)
|
|
205
|
-
)
|
|
125
|
+
abstract = True
|
wbportfolio/models/utils.py
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import date
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Iterator
|
|
5
|
+
|
|
6
|
+
from celery import shared_task
|
|
7
|
+
from django.db.models import F, QuerySet, Window
|
|
8
|
+
from django.db.models.functions import RowNumber
|
|
9
|
+
from tqdm import tqdm
|
|
1
10
|
from wbfdm.models import Instrument
|
|
2
11
|
|
|
3
|
-
from wbportfolio.models import Index, Product
|
|
12
|
+
from wbportfolio.models import AssetPosition, Index, Order, OrderProposal, Portfolio, Product
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("pms")
|
|
4
15
|
|
|
5
16
|
|
|
6
17
|
def get_casted_portfolio_instrument(instrument: Instrument) -> Product | Index | None:
|
|
@@ -11,3 +22,91 @@ def get_casted_portfolio_instrument(instrument: Instrument) -> Product | Index |
|
|
|
11
22
|
return Index.objects.get(id=instrument.id)
|
|
12
23
|
except Index.DoesNotExist:
|
|
13
24
|
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_adjusted_shares(old_shares: Decimal, old_price: Decimal, new_price: Decimal) -> Decimal:
|
|
28
|
+
return old_shares * (old_price / new_price)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def adjust_assets(qs: Iterator[AssetPosition], underlying_quote: Instrument):
|
|
32
|
+
objs = []
|
|
33
|
+
logger.info("adjusting asset positions...")
|
|
34
|
+
for a in qs:
|
|
35
|
+
old_price: Decimal = a.initial_price
|
|
36
|
+
a.initial_price = a.underlying_instrument = a.underlying_quote_price = None
|
|
37
|
+
a.underlying_quote = underlying_quote
|
|
38
|
+
a.pre_save()
|
|
39
|
+
if a.initial_shares and a.initial_price and old_price != a.initial_price:
|
|
40
|
+
a.initial_shares = get_adjusted_shares(a.initial_shares, old_price, a.initial_price)
|
|
41
|
+
objs.append(a)
|
|
42
|
+
AssetPosition.objects.bulk_update(
|
|
43
|
+
objs,
|
|
44
|
+
["underlying_quote", "underlying_quote_price", "underlying_instrument", "initial_price", "initial_shares"],
|
|
45
|
+
batch_size=1000,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def adjust_orders(qs: Iterator[Order], underlying_quote: Instrument):
|
|
50
|
+
objs = []
|
|
51
|
+
logger.info("adjusting orders...")
|
|
52
|
+
for o in qs:
|
|
53
|
+
old_price: Decimal = o.price
|
|
54
|
+
o.underlying_instrument = underlying_quote
|
|
55
|
+
o.set_price()
|
|
56
|
+
if o.price and old_price != o.price and o.shares:
|
|
57
|
+
o.shares = get_adjusted_shares(o.shares, old_price, o.price)
|
|
58
|
+
objs.append(o)
|
|
59
|
+
Order.objects.bulk_update(objs, ["price", "shares", "underlying_instrument"], batch_size=1000)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def adjust_quote(
|
|
63
|
+
old_quote: Instrument,
|
|
64
|
+
new_quote: Instrument,
|
|
65
|
+
adjust_after: date | None = None,
|
|
66
|
+
only_portfolios: QuerySet[Portfolio] | None = None,
|
|
67
|
+
debug: bool = False,
|
|
68
|
+
):
|
|
69
|
+
if old_quote.currency != new_quote.currency:
|
|
70
|
+
raise ValueError("cannot safely switch quotes that are not of the same currency")
|
|
71
|
+
assets_to_change = AssetPosition.objects.filter(underlying_quote=old_quote)
|
|
72
|
+
orders_to_change = Order.objects.filter(underlying_instrument=old_quote)
|
|
73
|
+
new_quote.import_prices()
|
|
74
|
+
if adjust_after:
|
|
75
|
+
assets_to_change = assets_to_change.filter(date__gt=adjust_after)
|
|
76
|
+
orders_to_change = orders_to_change.filter(value_date__gt=adjust_after)
|
|
77
|
+
if only_portfolios is not None:
|
|
78
|
+
assets_to_change = assets_to_change.filter(portfolio__in=only_portfolios)
|
|
79
|
+
orders_to_change = orders_to_change.filter(order_proposal__portfolio__in=only_portfolios)
|
|
80
|
+
if debug:
|
|
81
|
+
assets_to_change = tqdm(assets_to_change, total=assets_to_change.count())
|
|
82
|
+
orders_to_change = tqdm(orders_to_change, total=orders_to_change.count())
|
|
83
|
+
|
|
84
|
+
# gather the list of order proposal to replay (if the quote led to missing position, we want to replay it to correct automatically the issue)
|
|
85
|
+
latest_orders = orders_to_change.annotate(
|
|
86
|
+
row_number=Window(
|
|
87
|
+
expression=RowNumber(), partition_by=[F("order_proposal__portfolio")], order_by=F("value_date").desc()
|
|
88
|
+
)
|
|
89
|
+
).filter(row_number=1)
|
|
90
|
+
order_proposals_to_replay = OrderProposal.objects.filter(
|
|
91
|
+
portfolio__is_manageable=True, id__in=latest_orders.values("order_proposal")
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Adjust assets to the new quote
|
|
95
|
+
adjust_assets(assets_to_change, new_quote)
|
|
96
|
+
|
|
97
|
+
# Adjust orders to the new quote
|
|
98
|
+
adjust_orders(orders_to_change, new_quote)
|
|
99
|
+
|
|
100
|
+
# replay latest order proposal
|
|
101
|
+
for op in order_proposals_to_replay:
|
|
102
|
+
op.replay(reapply_order_proposal=True)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@shared_task(queue="portfolio")
|
|
106
|
+
def adjust_quote_as_task(
|
|
107
|
+
old_quote_id: int, new_quote_id: int, adjust_after: date | None = None, only_portfolio_ids: list[int] | None = None
|
|
108
|
+
):
|
|
109
|
+
old_quote = Instrument.objects.get(id=old_quote_id)
|
|
110
|
+
new_quote = Instrument.objects.get(id=new_quote_id)
|
|
111
|
+
only_portfolios = Portfolio.objects.filter(id__in=only_portfolio_ids) if only_portfolio_ids else None
|
|
112
|
+
adjust_quote(old_quote, new_quote, adjust_after=adjust_after, only_portfolios=only_portfolios)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from django.db.models import TextChoices
|
|
2
|
+
|
|
3
|
+
class ExecutionStatus(TextChoices):
|
|
4
|
+
PENDING = "PENDING", "Pending"
|
|
5
|
+
IN_DRAFT = "IN_DRAFT", "In Draft"
|
|
6
|
+
COMPLETED = "COMPLETED", "Completed"
|
|
7
|
+
CANCELLED = "CANCELLED", "Cancelled"
|
|
8
|
+
REJECTED = "REJECTED", "Rejected"
|
|
9
|
+
FAILED = "FAILED", "Failed"
|
|
10
|
+
UNKNOWN = "UNKNOWN", "Unknown"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ExecutionInstruction(TextChoices):
|
|
15
|
+
|
|
16
|
+
MARKET_ON_CLOSE = "MARKET_ON_CLOSE", "Market On Close" # no parameter
|
|
17
|
+
GUARANTEED_MARKET_ON_CLOSE = "GUARANTEED_MARKET_ON_CLOSE", "Guaranteed Market On Close" # no parameter
|
|
18
|
+
GUARANTEED_MARKET_ON_OPEN = "GUARANTEED_MARKET_ON_OPEN", "Guaranteed Market On Open" # no parameter
|
|
19
|
+
GPW_MARKET_ON_CLOSE = "GPW_MARKET_ON_CLOSE", "GPW Market On Close" # no parameter
|
|
20
|
+
MARKET_ON_OPEN = "MARKET_ON_OPEN", "Market On Open" # no parameter
|
|
21
|
+
IN_LINE_WITH_VOLUME = "IN_LINE_WITH_VOLUME", "In Line With Volume" # 1 parameter "Percentage"
|
|
22
|
+
LIMIT_ORDER = "LIMIT_ORDER", "Limit Order" # 2 parameters "limit and cutoff"
|
|
23
|
+
VWAP = "VWAP", "VWAP" # 2 parameters
|
|
24
|
+
TWAP = "TWAP", "TWAP" # 2 paramters
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RoutingException(Exception):
|
|
29
|
+
def __init__(self, errors):
|
|
30
|
+
# messages: a list of strings
|
|
31
|
+
super().__init__() # You can pass a summary to the base Exception
|
|
32
|
+
self.errors = errors
|
|
33
|
+
|
|
34
|
+
def __str__(self):
|
|
35
|
+
return str(self.errors)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from wbportfolio.order_routing import ExecutionStatus
|
|
4
|
+
from wbportfolio.pms.typing import Order
|
|
5
|
+
|
|
6
|
+
class BaseCustodianAdapter(ABC):
|
|
7
|
+
|
|
8
|
+
def __init__(self, isin: str, **identifiers):
|
|
9
|
+
self.isin = isin
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def errors(self):
|
|
13
|
+
if not hasattr(self, '_errors'):
|
|
14
|
+
raise ValueError("is_valid needs to call before accessing errors")
|
|
15
|
+
return
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def authenticate(self) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Authenticate or renew tokens with the custodian API.
|
|
20
|
+
Raises an exception if authentication fails.
|
|
21
|
+
"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def is_valid(self) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Check whether the given isin is valid and can be rebalanced
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def serialize_orders(self, orders: list[Order]) -> list[dict[str, str]]:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def deserialize_items(self, items: list[dict[str, str]]) -> list[Order]:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def get_rebalance_status(self) -> tuple[ExecutionStatus, str]:
|
|
41
|
+
"""
|
|
42
|
+
Return the rebalance status as a string (in the custodian format)
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def submit_rebalancing(self, items: list[dict[str, str]], as_draft: bool = True) -> tuple[list[dict[str, str]], str]:
|
|
48
|
+
"""
|
|
49
|
+
Submit a rebalance order for the certificate.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def cancel_current_rebalancing(self) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Cancel an existing rebalance order identified by ISIN.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_current_rebalancing(self) -> list[dict[str, str]]:
|
|
62
|
+
"""
|
|
63
|
+
Fetch the current rebalance request details for a certificate.
|
|
64
|
+
"""
|
|
65
|
+
pass
|