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
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from datetime import date
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from django.contrib import admin
|
|
6
|
+
from django.db import models
|
|
7
|
+
from django.db.models import (
|
|
8
|
+
Sum,
|
|
9
|
+
)
|
|
10
|
+
from django.dispatch import receiver
|
|
11
|
+
from ordered_model.models import OrderedModel
|
|
12
|
+
from pandas import Timestamp
|
|
13
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
14
|
+
from wbcore.contrib.io.mixins import ImportMixin
|
|
15
|
+
from wbfdm.models import Instrument
|
|
16
|
+
|
|
17
|
+
from wbportfolio.import_export.handlers.orders import OrderImportHandler
|
|
18
|
+
from wbportfolio.models.asset import AssetPosition
|
|
19
|
+
from wbportfolio.models.transactions.transactions import TransactionMixin
|
|
20
|
+
from wbportfolio.order_routing import ExecutionInstruction
|
|
21
|
+
from wbportfolio.pms.typing import Position as PositionDTO
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
25
|
+
import_export_handler_class = OrderImportHandler
|
|
26
|
+
|
|
27
|
+
ORDER_WEIGHTING_PRECISION = (
|
|
28
|
+
8 # we need to match the asset position weighting. Skfolio advices using a even smaller number (5)
|
|
29
|
+
)
|
|
30
|
+
currency = None
|
|
31
|
+
|
|
32
|
+
class Type(models.TextChoices):
|
|
33
|
+
REBALANCE = "REBALANCE", "Rebalance"
|
|
34
|
+
DECREASE = "DECREASE", "Decrease"
|
|
35
|
+
INCREASE = "INCREASE", "Increase"
|
|
36
|
+
BUY = "BUY", "Buy"
|
|
37
|
+
SELL = "SELL", "Sell"
|
|
38
|
+
NO_CHANGE = "NO_CHANGE", "No Change" # default transaction subtype if weighing is 0
|
|
39
|
+
|
|
40
|
+
class ExecutionStatus(models.TextChoices):
|
|
41
|
+
PENDING = "PENDING", "Pending"
|
|
42
|
+
CONFIRMED = "CONFIRMED", "Confirmed"
|
|
43
|
+
EXECUTED = "EXECUTED", "Executed"
|
|
44
|
+
FAILED = "FAILED", "Failed"
|
|
45
|
+
IGNORED = "IGNORED", "Ignored"
|
|
46
|
+
|
|
47
|
+
order_type = models.CharField(max_length=32, default=Type.BUY, choices=Type.choices, verbose_name="Trade Type")
|
|
48
|
+
shares = models.DecimalField(
|
|
49
|
+
max_digits=15,
|
|
50
|
+
decimal_places=4,
|
|
51
|
+
default=Decimal("0.0"),
|
|
52
|
+
help_text="The number of shares that were traded.",
|
|
53
|
+
verbose_name="Shares",
|
|
54
|
+
)
|
|
55
|
+
desired_target_weight = models.DecimalField(
|
|
56
|
+
max_digits=9,
|
|
57
|
+
decimal_places=ORDER_WEIGHTING_PRECISION,
|
|
58
|
+
default=Decimal(0),
|
|
59
|
+
help_text="Desired Target Weight (for compliance and audit)",
|
|
60
|
+
verbose_name="Desired Target Weight",
|
|
61
|
+
)
|
|
62
|
+
weighting = models.DecimalField(
|
|
63
|
+
max_digits=9,
|
|
64
|
+
decimal_places=ORDER_WEIGHTING_PRECISION,
|
|
65
|
+
default=Decimal(0),
|
|
66
|
+
help_text="The weight to be multiplied against the target",
|
|
67
|
+
verbose_name="Weight",
|
|
68
|
+
)
|
|
69
|
+
order_proposal = models.ForeignKey(
|
|
70
|
+
to="wbportfolio.OrderProposal",
|
|
71
|
+
related_name="orders",
|
|
72
|
+
on_delete=models.CASCADE,
|
|
73
|
+
help_text="The Order Proposal this trade is coming from",
|
|
74
|
+
)
|
|
75
|
+
daily_return = models.DecimalField(
|
|
76
|
+
max_digits=ORDER_WEIGHTING_PRECISION * 2
|
|
77
|
+
+ 3, # we don't expect any drift factor to be in the order of magnitude greater than 1000
|
|
78
|
+
decimal_places=ORDER_WEIGHTING_PRECISION
|
|
79
|
+
* 2, # we need a higher precision for this factor to avoid float inprecision
|
|
80
|
+
default=Decimal(0.0),
|
|
81
|
+
verbose_name="Daily Return",
|
|
82
|
+
help_text="The Ex-Post daily return",
|
|
83
|
+
)
|
|
84
|
+
quantization_error = models.DecimalField(
|
|
85
|
+
max_digits=9,
|
|
86
|
+
decimal_places=ORDER_WEIGHTING_PRECISION,
|
|
87
|
+
default=Decimal(0),
|
|
88
|
+
verbose_name="Quantization Error",
|
|
89
|
+
)
|
|
90
|
+
execution_status = models.CharField(
|
|
91
|
+
max_length=12,
|
|
92
|
+
default=ExecutionStatus.PENDING.value,
|
|
93
|
+
choices=ExecutionStatus.choices,
|
|
94
|
+
verbose_name="Execution Status",
|
|
95
|
+
)
|
|
96
|
+
execution_instruction = models.CharField(
|
|
97
|
+
max_length=26,
|
|
98
|
+
choices=ExecutionInstruction.choices,
|
|
99
|
+
default=ExecutionInstruction.MARKET_ON_CLOSE.value,
|
|
100
|
+
verbose_name="Execution Instruction",
|
|
101
|
+
)
|
|
102
|
+
execution_instruction_parameters = models.JSONField(
|
|
103
|
+
default=dict, blank=True, verbose_name="Execution Instruction Parameters"
|
|
104
|
+
)
|
|
105
|
+
execution_comment = models.TextField(default="", blank=True, verbose_name="Execution Comment")
|
|
106
|
+
|
|
107
|
+
execution_trade = models.OneToOneField(
|
|
108
|
+
to="wbportfolio.Trade",
|
|
109
|
+
related_name="order",
|
|
110
|
+
on_delete=models.SET_NULL,
|
|
111
|
+
blank=True,
|
|
112
|
+
null=True,
|
|
113
|
+
help_text="The executed Trade",
|
|
114
|
+
)
|
|
115
|
+
order_with_respect_to = "order_proposal"
|
|
116
|
+
|
|
117
|
+
class Meta(OrderedModel.Meta):
|
|
118
|
+
verbose_name = "Order"
|
|
119
|
+
verbose_name_plural = "Orders"
|
|
120
|
+
indexes = [
|
|
121
|
+
models.Index(fields=["order_proposal"]),
|
|
122
|
+
models.Index(fields=["underlying_instrument", "value_date"]),
|
|
123
|
+
models.Index(fields=["portfolio", "underlying_instrument", "value_date"]),
|
|
124
|
+
models.Index(fields=["order_proposal", "underlying_instrument"]),
|
|
125
|
+
# models.Index(fields=["date", "underlying_instrument"]),
|
|
126
|
+
]
|
|
127
|
+
constraints = [
|
|
128
|
+
models.UniqueConstraint(
|
|
129
|
+
fields=["order_proposal", "underlying_instrument"],
|
|
130
|
+
name="unique_order",
|
|
131
|
+
),
|
|
132
|
+
]
|
|
133
|
+
# notification_email_template = "portfolio/email/trade_notification.html"
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def product(self):
|
|
137
|
+
from wbportfolio.models.products import Product
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
return Product.objects.get(id=self.underlying_instrument.id)
|
|
141
|
+
except Product.DoesNotExist:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
@admin.display(description="Last Effective Date")
|
|
146
|
+
def _last_effective_date(self) -> date:
|
|
147
|
+
if hasattr(self, "last_effective_date"):
|
|
148
|
+
return self.last_effective_date
|
|
149
|
+
elif (
|
|
150
|
+
assets := AssetPosition.unannotated_objects.filter(
|
|
151
|
+
date__lt=self.value_date,
|
|
152
|
+
portfolio=self.portfolio,
|
|
153
|
+
)
|
|
154
|
+
).exists():
|
|
155
|
+
return assets.latest("date").date
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
@admin.display(description="Effective Weight")
|
|
159
|
+
def _previous_weight(self) -> Decimal:
|
|
160
|
+
if hasattr(self, "previous_weight"):
|
|
161
|
+
return self.previous_weight
|
|
162
|
+
return AssetPosition.unannotated_objects.filter(
|
|
163
|
+
underlying_quote=self.underlying_instrument,
|
|
164
|
+
date=self._last_effective_date,
|
|
165
|
+
portfolio=self.portfolio,
|
|
166
|
+
).aggregate(s=Sum("weighting"))["s"] or Decimal(0)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
@admin.display(description="Effective Weight")
|
|
170
|
+
def _effective_weight(self) -> Decimal:
|
|
171
|
+
if hasattr(self, "effective_weight"):
|
|
172
|
+
return self.effective_weight
|
|
173
|
+
return self.order_proposal.get_orders().get(id=self.id).effective_weight
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
@admin.display(description="Effective Shares")
|
|
177
|
+
def _effective_shares(self) -> Decimal:
|
|
178
|
+
return getattr(
|
|
179
|
+
self,
|
|
180
|
+
"effective_shares",
|
|
181
|
+
self.get_effective_shares(),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
@admin.display(description="Target Weight")
|
|
186
|
+
def _target_weight(self) -> Decimal:
|
|
187
|
+
return getattr(
|
|
188
|
+
self, "target_weight", round(self._effective_weight + self.weighting, self.ORDER_WEIGHTING_PRECISION)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
@admin.display(description="Target Shares")
|
|
193
|
+
def _target_shares(self) -> Decimal:
|
|
194
|
+
return getattr(self, "target_shares", self._effective_shares + self.shares)
|
|
195
|
+
|
|
196
|
+
def __str__(self):
|
|
197
|
+
ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
|
|
198
|
+
return f"{ticker}{self.weighting}"
|
|
199
|
+
|
|
200
|
+
def pre_save(self):
|
|
201
|
+
self.portfolio = self.order_proposal.portfolio
|
|
202
|
+
self.value_date = self.order_proposal.trade_date
|
|
203
|
+
self.set_currency_fx_rate()
|
|
204
|
+
|
|
205
|
+
if not self.price:
|
|
206
|
+
self.set_price()
|
|
207
|
+
if not self.portfolio.only_weighting and not self.shares:
|
|
208
|
+
estimated_shares = self.order_proposal.get_estimated_shares(
|
|
209
|
+
self.weighting, self.underlying_instrument, self.price
|
|
210
|
+
)
|
|
211
|
+
if estimated_shares:
|
|
212
|
+
self.shares = estimated_shares
|
|
213
|
+
if effective_shares := self.get_effective_shares():
|
|
214
|
+
if self.order_type == self.Type.SELL:
|
|
215
|
+
self.shares = -effective_shares
|
|
216
|
+
else:
|
|
217
|
+
self.shares = max(self.shares, -effective_shares)
|
|
218
|
+
super().pre_save()
|
|
219
|
+
|
|
220
|
+
def save(self, *args, **kwargs):
|
|
221
|
+
if self.id:
|
|
222
|
+
self.set_type()
|
|
223
|
+
self.pre_save()
|
|
224
|
+
if not self.underlying_instrument.is_investable_universe:
|
|
225
|
+
self.underlying_instrument.is_investable_universe = True
|
|
226
|
+
self.underlying_instrument.save()
|
|
227
|
+
|
|
228
|
+
super().save(*args, **kwargs)
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def get_type(cls, weighting, effective_weight, target_weight) -> Type:
|
|
232
|
+
if weighting == 0:
|
|
233
|
+
return Order.Type.NO_CHANGE
|
|
234
|
+
elif weighting is not None:
|
|
235
|
+
if weighting > 0:
|
|
236
|
+
if abs(effective_weight) > 1e-8:
|
|
237
|
+
return Order.Type.INCREASE
|
|
238
|
+
else:
|
|
239
|
+
return Order.Type.BUY
|
|
240
|
+
elif weighting < 0:
|
|
241
|
+
if abs(target_weight) > 1e-8:
|
|
242
|
+
return Order.Type.DECREASE
|
|
243
|
+
else:
|
|
244
|
+
return Order.Type.SELL
|
|
245
|
+
|
|
246
|
+
def get_effective_shares(self) -> Decimal:
|
|
247
|
+
return AssetPosition.objects.filter(
|
|
248
|
+
underlying_quote=self.underlying_instrument,
|
|
249
|
+
date=self.order_proposal.last_effective_date,
|
|
250
|
+
portfolio=self.portfolio,
|
|
251
|
+
).aggregate(s=Sum("shares"))["s"] or Decimal("0")
|
|
252
|
+
|
|
253
|
+
def set_type(self):
|
|
254
|
+
effective_weight = self._effective_weight
|
|
255
|
+
self.order_type = self.get_type(self.weighting, effective_weight, effective_weight + self.weighting)
|
|
256
|
+
|
|
257
|
+
def _get_price(self) -> tuple[Decimal, Decimal]:
|
|
258
|
+
daily_return = last_price = Decimal("0")
|
|
259
|
+
|
|
260
|
+
effective_date = self.order_proposal.last_effective_date
|
|
261
|
+
if self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent:
|
|
262
|
+
last_price = Decimal("1")
|
|
263
|
+
else:
|
|
264
|
+
try:
|
|
265
|
+
last_price = Decimal(self.portfolio.builder.prices[self.value_date][self.underlying_instrument.id])
|
|
266
|
+
daily_return = self.portfolio.builder.returns.loc[
|
|
267
|
+
Timestamp(self.value_date), self.underlying_instrument.id
|
|
268
|
+
]
|
|
269
|
+
except KeyError:
|
|
270
|
+
prices, returns = Instrument.objects.filter(id=self.underlying_instrument.id).get_returns_df(
|
|
271
|
+
from_date=effective_date,
|
|
272
|
+
to_date=self.value_date,
|
|
273
|
+
to_currency=self.order_proposal.portfolio.currency,
|
|
274
|
+
use_dl=True,
|
|
275
|
+
)
|
|
276
|
+
with suppress(IndexError):
|
|
277
|
+
daily_return = Decimal(returns.iloc[-1, 0])
|
|
278
|
+
with suppress(KeyError, TypeError):
|
|
279
|
+
last_price = Decimal(
|
|
280
|
+
prices.get(self.value_date, prices[effective_date])[self.underlying_instrument.id]
|
|
281
|
+
)
|
|
282
|
+
return last_price, daily_return
|
|
283
|
+
|
|
284
|
+
def set_price(self):
|
|
285
|
+
last_price, daily_return = self._get_price()
|
|
286
|
+
self.daily_return = daily_return
|
|
287
|
+
self.price = last_price
|
|
288
|
+
|
|
289
|
+
def set_currency_fx_rate(self):
|
|
290
|
+
self.currency_fx_rate = Decimal("1")
|
|
291
|
+
if self.order_proposal.portfolio.currency != self.underlying_instrument.currency:
|
|
292
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
293
|
+
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
294
|
+
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def set_weighting(self, weighting: Decimal, portfolio_value: Decimal):
|
|
298
|
+
self.weighting = weighting
|
|
299
|
+
price_fx_portfolio = self.price * self.currency_fx_rate
|
|
300
|
+
if price_fx_portfolio and portfolio_value:
|
|
301
|
+
total_value = self.weighting * portfolio_value
|
|
302
|
+
self.shares = total_value / price_fx_portfolio
|
|
303
|
+
else:
|
|
304
|
+
self.shares = Decimal("0")
|
|
305
|
+
|
|
306
|
+
def set_shares(self, shares: Decimal, portfolio_value: Decimal):
|
|
307
|
+
if portfolio_value:
|
|
308
|
+
price_fx_portfolio = self.price * self.currency_fx_rate
|
|
309
|
+
self.shares = shares
|
|
310
|
+
total_value = shares * price_fx_portfolio
|
|
311
|
+
self.weighting = total_value / portfolio_value
|
|
312
|
+
else:
|
|
313
|
+
self.weighting = self.shares = Decimal("0")
|
|
314
|
+
|
|
315
|
+
def set_total_value_fx_portfolio(self, total_value_fx_portfolio: Decimal, portfolio_value: Decimal):
|
|
316
|
+
price_fx_portfolio = self.price * self.currency_fx_rate
|
|
317
|
+
if price_fx_portfolio and portfolio_value:
|
|
318
|
+
self.shares = total_value_fx_portfolio / price_fx_portfolio
|
|
319
|
+
self.weighting = total_value_fx_portfolio / portfolio_value
|
|
320
|
+
else:
|
|
321
|
+
self.weighting = self.shares = Decimal("0")
|
|
322
|
+
|
|
323
|
+
def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
|
|
324
|
+
warnings = []
|
|
325
|
+
# if shares is defined and the underlying instrument defines a round lot size different than 1 and exchange allows its application, we round the share accordingly
|
|
326
|
+
if self._target_weight:
|
|
327
|
+
if self.order_proposal and not self.portfolio.only_weighting:
|
|
328
|
+
shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
|
|
329
|
+
if shares != self.shares:
|
|
330
|
+
warnings.append(
|
|
331
|
+
f"{self.underlying_instrument.computed_str} has a round lot size of {self.underlying_instrument.round_lot_size}: shares were rounded from {self.shares} to {shares}"
|
|
332
|
+
)
|
|
333
|
+
shares = round(shares) # ensure fractional shares are converted into integer
|
|
334
|
+
# we need to recompute the delta weight has we changed the number of shares
|
|
335
|
+
if shares != self.shares:
|
|
336
|
+
self.set_shares(shares, portfolio_total_asset_value)
|
|
337
|
+
if abs(self.weighting) < self.order_proposal.min_weighting:
|
|
338
|
+
warnings.append(
|
|
339
|
+
f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
|
|
340
|
+
)
|
|
341
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
342
|
+
if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
|
|
343
|
+
warnings.append(
|
|
344
|
+
f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
|
|
345
|
+
)
|
|
346
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
347
|
+
if not self.price:
|
|
348
|
+
warnings.append(f"No price for {self.underlying_instrument.computed_str}")
|
|
349
|
+
if (
|
|
350
|
+
not self.underlying_instrument.is_cash
|
|
351
|
+
and not self.underlying_instrument.is_cash_equivalent
|
|
352
|
+
and self._target_weight < -1e-8
|
|
353
|
+
): # any value below -1e8 will be considered zero
|
|
354
|
+
warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
|
|
355
|
+
self.desired_target_weight = self._target_weight
|
|
356
|
+
return warnings
|
|
357
|
+
|
|
358
|
+
def to_dto(self) -> PositionDTO:
|
|
359
|
+
return self.create_dto(
|
|
360
|
+
self.underlying_instrument,
|
|
361
|
+
self._target_weight,
|
|
362
|
+
self.price,
|
|
363
|
+
self.value_date,
|
|
364
|
+
shares=self._target_shares,
|
|
365
|
+
currency_fx_rate=self.currency_fx_rate,
|
|
366
|
+
daily_return=self.daily_return,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def create_dto(
|
|
371
|
+
cls,
|
|
372
|
+
instrument: Instrument,
|
|
373
|
+
weighting: Decimal,
|
|
374
|
+
price: Decimal,
|
|
375
|
+
trade_date: date,
|
|
376
|
+
shares: Decimal | None = None,
|
|
377
|
+
**extra_param,
|
|
378
|
+
) -> PositionDTO:
|
|
379
|
+
price_data = {}
|
|
380
|
+
|
|
381
|
+
return PositionDTO(
|
|
382
|
+
underlying_instrument=instrument.id,
|
|
383
|
+
instrument_type=instrument.security_instrument_type.id,
|
|
384
|
+
weighting=weighting,
|
|
385
|
+
shares=shares,
|
|
386
|
+
currency=instrument.currency.id,
|
|
387
|
+
date=trade_date,
|
|
388
|
+
asset_valuation_date=trade_date,
|
|
389
|
+
is_cash=instrument.is_cash or instrument.is_cash_equivalent,
|
|
390
|
+
price=price,
|
|
391
|
+
exchange=instrument.exchange.id if instrument.exchange else None,
|
|
392
|
+
country=instrument.country.id if instrument.country else None,
|
|
393
|
+
**price_data,
|
|
394
|
+
**extra_param,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@receiver(models.signals.post_save, sender="wbportfolio.Trade")
|
|
399
|
+
def link_trade_to_order(sender, instance, created, raw, **kwargs):
|
|
400
|
+
"""Gets or create the fees for a given price and updates them if necessary"""
|
|
401
|
+
if not raw and created and not instance.underlying_instrument.is_cash and not instance.is_customer_trade:
|
|
402
|
+
with suppress(Order.DoesNotExist):
|
|
403
|
+
order = Order.objects.get(
|
|
404
|
+
portfolio=instance.portfolio,
|
|
405
|
+
underlying_instrument=instance.underlying_instrument,
|
|
406
|
+
value_date=instance.book_date,
|
|
407
|
+
)
|
|
408
|
+
order.execution_trade = instance
|
|
409
|
+
order.execution_status = Order.ExecutionStatus.EXECUTED
|
|
410
|
+
order.save()
|