wbportfolio 1.46.1__py2.py3-none-any.whl → 1.46.2__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/apps.py +16 -0
- wbportfolio/import_export/handlers/asset_position.py +7 -7
- wbportfolio/import_export/handlers/trade.py +57 -51
- wbportfolio/import_export/resources/trades.py +26 -2
- wbportfolio/migrations/0074_alter_rebalancer_frequency_and_more.py +24 -0
- wbportfolio/models/mixins/instruments.py +9 -0
- wbportfolio/models/portfolio.py +23 -25
- wbportfolio/models/transactions/rebalancing.py +8 -2
- wbportfolio/models/transactions/trade_proposals.py +25 -18
- wbportfolio/models/transactions/trades.py +5 -5
- wbportfolio/rebalancing/base.py +4 -0
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +57 -19
- wbportfolio/serializers/transactions/trade_proposals.py +5 -5
- wbportfolio/tests/rebalancing/test_models.py +5 -5
- wbportfolio/viewsets/transactions/trades.py +9 -0
- {wbportfolio-1.46.1.dist-info → wbportfolio-1.46.2.dist-info}/METADATA +1 -1
- {wbportfolio-1.46.1.dist-info → wbportfolio-1.46.2.dist-info}/RECORD +19 -18
- {wbportfolio-1.46.1.dist-info → wbportfolio-1.46.2.dist-info}/WHEEL +0 -0
- {wbportfolio-1.46.1.dist-info → wbportfolio-1.46.2.dist-info}/licenses/LICENSE +0 -0
wbportfolio/apps.py
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
from django.apps import AppConfig
|
|
2
|
+
from django.apps import apps as global_apps
|
|
3
|
+
from django.db import DEFAULT_DB_ALIAS
|
|
4
|
+
from django.db.models.signals import post_migrate
|
|
5
|
+
from django.utils.module_loading import autodiscover_modules
|
|
2
6
|
|
|
3
7
|
|
|
4
8
|
class WbportfolioConfig(AppConfig):
|
|
5
9
|
name = "wbportfolio"
|
|
10
|
+
|
|
11
|
+
def ready(self):
|
|
12
|
+
def autodiscover_backends(
|
|
13
|
+
app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs
|
|
14
|
+
):
|
|
15
|
+
# we wrap the autodiscover into a post_migrate receiver because we expect db calls
|
|
16
|
+
autodiscover_modules("rebalancing")
|
|
17
|
+
|
|
18
|
+
post_migrate.connect(
|
|
19
|
+
autodiscover_backends,
|
|
20
|
+
dispatch_uid="wbportfolio.autodiscover_rebalancing",
|
|
21
|
+
)
|
|
@@ -53,13 +53,6 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
53
53
|
underlying_quote_data, only_security=False, read_only=True
|
|
54
54
|
)[0]
|
|
55
55
|
|
|
56
|
-
# number type deserialization and sanitization
|
|
57
|
-
# ensure the provided Decimal field are of type Decimal
|
|
58
|
-
decimal_fields = ["initial_currency_fx_rate", "initial_price", "initial_shares", "weighting"]
|
|
59
|
-
for field in decimal_fields:
|
|
60
|
-
if not (value := data.get(field, None)) is None:
|
|
61
|
-
data[field] = Decimal(value)
|
|
62
|
-
|
|
63
56
|
# Ensure that for shares and weighting, a None value default to 0
|
|
64
57
|
if "initial_shares" in data and data["initial_shares"] is None:
|
|
65
58
|
data["initial_shares"] = Decimal(0)
|
|
@@ -81,6 +74,13 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
81
74
|
except ValueError:
|
|
82
75
|
raise DeserializationError("Price not provided but can not be found automatically")
|
|
83
76
|
|
|
77
|
+
# number type deserialization and sanitization
|
|
78
|
+
# ensure the provided Decimal field are of type Decimal
|
|
79
|
+
decimal_fields = ["initial_currency_fx_rate", "initial_price", "initial_shares", "weighting"]
|
|
80
|
+
for field in decimal_fields:
|
|
81
|
+
if not (value := data.get(field, None)) is None:
|
|
82
|
+
data[field] = Decimal(value)
|
|
83
|
+
|
|
84
84
|
def _process_raw_data(self, data: Dict[str, Any]):
|
|
85
85
|
if prices := data.get("prices", None):
|
|
86
86
|
self.import_source.log += "Instrument Prices found: Importing"
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import math
|
|
2
1
|
from datetime import datetime
|
|
3
|
-
from decimal import Decimal
|
|
4
2
|
from typing import Any, Dict, Optional
|
|
5
3
|
|
|
6
4
|
from django.db import models
|
|
@@ -28,43 +26,45 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
28
26
|
|
|
29
27
|
def _data_changed(self, _object, change_data: Dict[str, Any], initial_data: Dict[str, Any], **kwargs):
|
|
30
28
|
if (new_register := change_data.get("register")) and (current_register := _object.register):
|
|
31
|
-
# we
|
|
29
|
+
# we replace the register only if the new one gives us more information
|
|
32
30
|
if new_register.register_reference == current_register.global_register_reference:
|
|
33
31
|
del change_data["register"]
|
|
34
32
|
return super()._data_changed(_object, change_data, initial_data, **kwargs)
|
|
35
33
|
|
|
36
34
|
def _deserialize(self, data: Dict[str, Any]):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if transaction_date_str := data.get("transaction_date", None):
|
|
40
|
-
data["transaction_date"] = datetime.strptime(transaction_date_str, "%Y-%m-%d").date()
|
|
41
|
-
if value_date_str := data.get("value_date", None):
|
|
42
|
-
data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
|
|
43
|
-
if book_date_str := data.get("book_date", None):
|
|
44
|
-
data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
|
|
35
|
+
from wbportfolio.models.transactions.trade_proposals import TradeProposal
|
|
36
|
+
|
|
45
37
|
if underlying_instrument := data.get("underlying_instrument", None):
|
|
46
38
|
data["underlying_instrument"] = self.instrument_handler.process_object(
|
|
47
39
|
underlying_instrument, only_security=False, read_only=True
|
|
48
40
|
)[0]
|
|
49
|
-
data["portfolio"] = Portfolio._get_or_create_portfolio(
|
|
50
|
-
self.instrument_handler, data.get("portfolio", data["underlying_instrument"])
|
|
51
|
-
)
|
|
52
|
-
if currency_data := data.get("currency", None):
|
|
53
|
-
data["currency"] = self.currency_handler.process_object(currency_data, read_only=True)[0]
|
|
54
41
|
|
|
55
|
-
if
|
|
56
|
-
|
|
42
|
+
if trade_proposal_id := data.pop("trade_proposal_id", None):
|
|
43
|
+
trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
|
|
44
|
+
data["value_date"] = trade_proposal.last_effective_date
|
|
45
|
+
data["transaction_date"] = trade_proposal.trade_date
|
|
46
|
+
data["trade_proposal"] = trade_proposal
|
|
47
|
+
data["portfolio"] = trade_proposal.portfolio
|
|
48
|
+
data["status"] = "DRAFT"
|
|
49
|
+
else:
|
|
50
|
+
if external_identifier2 := data.get("external_identifier2", None):
|
|
51
|
+
data["external_identifier2"] = str(external_identifier2)
|
|
52
|
+
if transaction_date_str := data.get("transaction_date", None):
|
|
53
|
+
data["transaction_date"] = datetime.strptime(transaction_date_str, "%Y-%m-%d").date()
|
|
54
|
+
if value_date_str := data.get("value_date", None):
|
|
55
|
+
data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
|
|
56
|
+
if book_date_str := data.get("book_date", None):
|
|
57
|
+
data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
|
|
58
|
+
data["portfolio"] = Portfolio._get_or_create_portfolio(
|
|
59
|
+
self.instrument_handler, data.get("portfolio", data["underlying_instrument"])
|
|
60
|
+
)
|
|
61
|
+
if currency_data := data.get("currency", None):
|
|
62
|
+
data["currency"] = self.currency_handler.process_object(currency_data, read_only=True)[0]
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
q = 1 / (math.pow(10, 4))
|
|
61
|
-
data[field.name] = Decimal(value).quantize(Decimal(str(q)))
|
|
64
|
+
if register_data := data.get("register", None):
|
|
65
|
+
data["register"] = self.register_handler.process_object(register_data)[0]
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
data["total_value"] = data["price"] * data["shares"]
|
|
65
|
-
if "total_value_fx_portfolio" in data:
|
|
66
|
-
data["total_value_fx_portfolio"] = data["price"] * data["shares"] * data["currency_fx_rate"]
|
|
67
|
-
data["marked_for_deletion"] = data.get("marked_for_deletion", False)
|
|
67
|
+
data["marked_for_deletion"] = data.get("marked_for_deletion", False)
|
|
68
68
|
|
|
69
69
|
def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
|
|
70
70
|
if "transaction_date" not in data: # we might get only book date and not transaction date
|
|
@@ -81,7 +81,7 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
81
81
|
dates_lookup = {"book_date": book_date}
|
|
82
82
|
else:
|
|
83
83
|
raise DeserializationError("date lookup is missing from data")
|
|
84
|
-
self.import_source.log += f"\nParameter: Product={data['underlying_instrument']} Trade-Date={transaction_date} Shares={data
|
|
84
|
+
self.import_source.log += f"\nParameter: Product={data['underlying_instrument']} Trade-Date={transaction_date} Shares={data.get('shares')} Weighting={data.get('weighting')}"
|
|
85
85
|
|
|
86
86
|
if history.exists():
|
|
87
87
|
queryset = history
|
|
@@ -89,10 +89,10 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
89
89
|
queryset = self.model.objects.filter(marked_for_deletion=False)
|
|
90
90
|
|
|
91
91
|
queryset = queryset.filter(
|
|
92
|
-
models.Q(underlying_instrument=data["underlying_instrument"])
|
|
93
|
-
& models.Q(**dates_lookup)
|
|
94
|
-
& models.Q(shares=data["shares"])
|
|
92
|
+
models.Q(underlying_instrument=data["underlying_instrument"]) & models.Q(**dates_lookup)
|
|
95
93
|
)
|
|
94
|
+
if "shares" in data:
|
|
95
|
+
queryset = queryset.filter(shares=data["shares"])
|
|
96
96
|
|
|
97
97
|
if _id := data.get("id", None):
|
|
98
98
|
self.import_source.log += f"ID {_id} provided -> Load CustomerTrade"
|
|
@@ -108,7 +108,7 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
108
108
|
if portfolio := data.get("portfolio", None):
|
|
109
109
|
queryset = queryset.filter(portfolio=portfolio)
|
|
110
110
|
if queryset.exists():
|
|
111
|
-
if bank := data
|
|
111
|
+
if bank := data.get("bank"):
|
|
112
112
|
self.import_source.log += (
|
|
113
113
|
f"\n{queryset.count()} Trades found. The bank will tried to be matched against {bank}"
|
|
114
114
|
)
|
|
@@ -138,24 +138,27 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
138
138
|
self.import_source.log += "\nNo trade was successfully matched."
|
|
139
139
|
|
|
140
140
|
def _get_history(self, history: Dict[str, Any]) -> models.QuerySet:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
pending=False,
|
|
144
|
-
transaction_subtype__in=[
|
|
145
|
-
self.model.Type.SUBSCRIPTION,
|
|
146
|
-
self.model.Type.REDEMPTION,
|
|
147
|
-
], # we cannot exclude marked for deleted trade because otherwise they are never consider in the history
|
|
148
|
-
)
|
|
149
|
-
if transaction_date := history.get("transaction_date"):
|
|
150
|
-
trades = trades.filter(transaction_date__lte=transaction_date)
|
|
151
|
-
elif book_date := history.get("book_date"):
|
|
152
|
-
trades = trades.filter(book_date__lte=book_date)
|
|
153
|
-
if "underlying_instrument" in history:
|
|
154
|
-
trades = trades.filter(underlying_instrument__id=history["underlying_instrument"])
|
|
155
|
-
elif "underlying_instruments" in history:
|
|
156
|
-
trades = trades.filter(underlying_instrument__id__in=history["underlying_instruments"])
|
|
141
|
+
if trade_proposal_id := history.get("trade_proposal_id"):
|
|
142
|
+
trades = self.model.objects.filter(trade_proposal_id=trade_proposal_id)
|
|
157
143
|
else:
|
|
158
|
-
|
|
144
|
+
trades = self.model.objects.filter(
|
|
145
|
+
exclude_from_history=False,
|
|
146
|
+
pending=False,
|
|
147
|
+
transaction_subtype__in=[
|
|
148
|
+
self.model.Type.SUBSCRIPTION,
|
|
149
|
+
self.model.Type.REDEMPTION,
|
|
150
|
+
], # we cannot exclude marked for deleted trade because otherwise they are never consider in the history
|
|
151
|
+
)
|
|
152
|
+
if transaction_date := history.get("transaction_date"):
|
|
153
|
+
trades = trades.filter(transaction_date__lte=transaction_date)
|
|
154
|
+
elif book_date := history.get("book_date"):
|
|
155
|
+
trades = trades.filter(book_date__lte=book_date)
|
|
156
|
+
if "underlying_instrument" in history:
|
|
157
|
+
trades = trades.filter(underlying_instrument__id=history["underlying_instrument"])
|
|
158
|
+
elif "underlying_instruments" in history:
|
|
159
|
+
trades = trades.filter(underlying_instrument__id__in=history["underlying_instruments"])
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError("We cannot estimate history without at least the underlying instrument")
|
|
159
162
|
return trades
|
|
160
163
|
|
|
161
164
|
def _post_processing_objects(
|
|
@@ -188,5 +191,8 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
188
191
|
self.import_source.log += (
|
|
189
192
|
f"{trade.transaction_date:%d.%m.%Y}: {trade.shares} {trade.bank} ==> Marked for deletion"
|
|
190
193
|
)
|
|
191
|
-
trade.
|
|
192
|
-
|
|
194
|
+
if trade.trade_proposal:
|
|
195
|
+
trade.delete()
|
|
196
|
+
else:
|
|
197
|
+
trade.marked_for_deletion = True
|
|
198
|
+
trade.save()
|
|
@@ -5,7 +5,7 @@ from import_export.widgets import ForeignKeyWidget
|
|
|
5
5
|
from wbcore.contrib.io.resources import FilterModelResource
|
|
6
6
|
from wbfdm.models import Instrument
|
|
7
7
|
|
|
8
|
-
from wbportfolio.models import Trade
|
|
8
|
+
from wbportfolio.models import Trade, TradeProposal
|
|
9
9
|
|
|
10
10
|
fake = Faker()
|
|
11
11
|
|
|
@@ -15,6 +15,30 @@ class TradeProposalTradeResource(FilterModelResource):
|
|
|
15
15
|
Trade Resource class to use to import trade from the trade proposal
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
+
def __init__(self, **kwargs):
|
|
19
|
+
self.trade_proposal = TradeProposal.objects.get(pk=kwargs["trade_proposal_id"])
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
|
|
22
|
+
def before_import(self, dataset, **kwargs):
|
|
23
|
+
Trade.objects.filter(trade_proposal=self.trade_proposal).delete()
|
|
24
|
+
|
|
25
|
+
def get_or_init_instance(self, instance_loader, row):
|
|
26
|
+
try:
|
|
27
|
+
return Trade.objects.get(
|
|
28
|
+
trade_proposal=self.trade_proposal, underlying_instrument=row["underlying_instrument"]
|
|
29
|
+
)
|
|
30
|
+
except Trade.DoesNotExist:
|
|
31
|
+
return Trade(
|
|
32
|
+
trade_proposal=self.trade_proposal,
|
|
33
|
+
underlying_instrument=row["underlying_instrument"],
|
|
34
|
+
transaction_subtype=Trade.Type.BUY if row["weighting"] > 0 else Trade.Type.SELL,
|
|
35
|
+
currency=row["underlying_instrument"].currency,
|
|
36
|
+
transaction_date=self.trade_proposal.trade_date,
|
|
37
|
+
portfolio=self.trade_proposal.portfolio,
|
|
38
|
+
weighting=row["weighting"],
|
|
39
|
+
status=Trade.Status.DRAFT,
|
|
40
|
+
)
|
|
41
|
+
|
|
18
42
|
DUMMY_FIELD_MAP = {
|
|
19
43
|
"underlying_instrument": lambda: rstr.xeger("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})"),
|
|
20
44
|
"weighting": 1.0,
|
|
@@ -29,7 +53,7 @@ class TradeProposalTradeResource(FilterModelResource):
|
|
|
29
53
|
)
|
|
30
54
|
|
|
31
55
|
class Meta:
|
|
32
|
-
import_id_fields = ("
|
|
56
|
+
import_id_fields = ("underlying_instrument",)
|
|
33
57
|
fields = (
|
|
34
58
|
"id",
|
|
35
59
|
"underlying_instrument",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by Django 5.0.12 on 2025-02-14 13:10
|
|
2
|
+
|
|
3
|
+
import django_fsm
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('wbportfolio', '0073_remove_product_price_computation_and_more'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AlterField(
|
|
15
|
+
model_name='rebalancer',
|
|
16
|
+
name='frequency',
|
|
17
|
+
field=models.CharField(default='RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1', help_text='The Evaluation Frequency in RRULE format', max_length=256, verbose_name='Evaluation Frequency'),
|
|
18
|
+
),
|
|
19
|
+
migrations.AlterField(
|
|
20
|
+
model_name='tradeproposal',
|
|
21
|
+
name='status',
|
|
22
|
+
field=django_fsm.FSMField(choices=[('DRAFT', 'Draft'), ('SUBMIT', 'Submit'), ('APPROVED', 'Approved'), ('DENIED', 'Denied'), ('FAILED', 'Failed')], default='DRAFT', max_length=50, verbose_name='Status'),
|
|
23
|
+
),
|
|
24
|
+
]
|
|
@@ -61,6 +61,15 @@ class PMSInstrument(Instrument):
|
|
|
61
61
|
def nominal_value(self, val_date):
|
|
62
62
|
return self.total_shares(val_date) * self.share_price
|
|
63
63
|
|
|
64
|
+
def get_latest_price(self, val_date: date) -> InstrumentPrice | None:
|
|
65
|
+
try:
|
|
66
|
+
return InstrumentPrice.objects.filter_only_valid_prices().get(instrument=self, date=val_date)
|
|
67
|
+
except InstrumentPrice.DoesNotExist:
|
|
68
|
+
if not self.inception_date or not self.prices.filter(date__lte=val_date).exists():
|
|
69
|
+
return InstrumentPrice.objects.get_or_create(
|
|
70
|
+
instrument=self, date=val_date, defaults={"calculated": False, "net_value": self.issue_price}
|
|
71
|
+
)[0]
|
|
72
|
+
|
|
64
73
|
def get_latest_valid_price(self, val_date: Optional[date] = None) -> models.Model:
|
|
65
74
|
qs = self.valuations.exclude(net_value=0)
|
|
66
75
|
if val_date and qs.filter(date__lte=val_date).exists():
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -112,7 +112,7 @@ class ActiveTrackedPortfolioManager(DefaultPortfolioManager):
|
|
|
112
112
|
return (
|
|
113
113
|
super()
|
|
114
114
|
.get_queryset()
|
|
115
|
-
.annotate(asset_exists=Exists(AssetPosition.
|
|
115
|
+
.annotate(asset_exists=Exists(AssetPosition.unannotated_objects.filter(portfolio=OuterRef("pk"))))
|
|
116
116
|
.filter(asset_exists=True, is_tracked=True)
|
|
117
117
|
)
|
|
118
118
|
|
|
@@ -558,7 +558,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
558
558
|
return Portfolio.get_contribution_df(qs, hedged_currency=hedged_currency)
|
|
559
559
|
|
|
560
560
|
def check_related_portfolio_at_date(self, val_date: date, related_portfolio: "Portfolio"):
|
|
561
|
-
assets = AssetPosition.
|
|
561
|
+
assets = AssetPosition.unannotated_objects.filter(
|
|
562
562
|
date=val_date, underlying_instrument__is_cash=False, underlying_instrument__is_cash_equivalent=False
|
|
563
563
|
).values("underlying_instrument", "shares")
|
|
564
564
|
assets1 = assets.filter(portfolio=self)
|
|
@@ -569,9 +569,9 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
569
569
|
child_portfolios = set()
|
|
570
570
|
if pms_instruments := list(self.pms_instruments):
|
|
571
571
|
for parent_portfolio in Portfolio.objects.filter(
|
|
572
|
-
id__in=AssetPosition.
|
|
573
|
-
|
|
574
|
-
)
|
|
572
|
+
id__in=AssetPosition.unannotated_objects.filter(
|
|
573
|
+
date=val_date, underlying_quote__in=pms_instruments
|
|
574
|
+
).values("portfolio")
|
|
575
575
|
):
|
|
576
576
|
child_portfolios.add(parent_portfolio)
|
|
577
577
|
return child_portfolios
|
|
@@ -589,6 +589,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
589
589
|
recompute_weighting: bool = False,
|
|
590
590
|
force_recompute_weighting: bool = False,
|
|
591
591
|
compute_metrics: bool = False,
|
|
592
|
+
evaluate_rebalancer: bool = True,
|
|
592
593
|
):
|
|
593
594
|
logger.info(f"change at date for {self} at {val_date}")
|
|
594
595
|
qs = self.assets.filter(date=val_date).filter(
|
|
@@ -612,7 +613,8 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
612
613
|
|
|
613
614
|
# We check if there is an instrument attached to the portfolio with calculated NAV and price computation method
|
|
614
615
|
self.estimate_net_asset_values(val_date)
|
|
615
|
-
|
|
616
|
+
if evaluate_rebalancer:
|
|
617
|
+
self.evaluate_rebalancing(val_date)
|
|
616
618
|
|
|
617
619
|
self.updated_at = timezone.now()
|
|
618
620
|
self.save()
|
|
@@ -716,17 +718,19 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
716
718
|
Returns: The trade proposal generated by the rebalancing, if any (otherwise None)
|
|
717
719
|
"""
|
|
718
720
|
analytic_portfolio = self.get_analytic_portfolio(start_date)
|
|
721
|
+
rebalancer = getattr(self, "automatic_rebalancer", None)
|
|
719
722
|
initial_assets = analytic_portfolio.assets
|
|
720
723
|
positions = []
|
|
721
724
|
next_trade_proposal = None
|
|
722
725
|
rebalancing_date = None
|
|
726
|
+
logger.info(f"compute next weights in batch for {self} from {start_date:%Y-%m-%d} to {end_date:%Y-%m-%d}")
|
|
723
727
|
returns, prices = self.get_returns(initial_assets, (start_date - BDay(3)).date(), end_date, ffill_returns=True)
|
|
724
728
|
for to_date_ts in pd.date_range(start_date + timedelta(days=1), end_date, freq="B"):
|
|
725
729
|
to_date = to_date_ts.date()
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
+
logger.info(f"Processing {to_date:%Y-%m-%d}")
|
|
731
|
+
if rebalancer and rebalancer.is_valid(to_date):
|
|
732
|
+
rebalancing_date = to_date
|
|
733
|
+
break
|
|
730
734
|
# with suppress(IndexError):
|
|
731
735
|
last_returns = returns.loc[[to_date_ts], :]
|
|
732
736
|
next_weights = analytic_portfolio.get_next_weights(last_returns.iloc[-1, :].T)
|
|
@@ -737,7 +741,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
737
741
|
)
|
|
738
742
|
analytic_portfolio = AnalyticPortfolio(X=last_returns, weights=next_weights)
|
|
739
743
|
|
|
740
|
-
self.bulk_create_positions(positions, delete_leftovers=True, compute_metrics=False)
|
|
744
|
+
self.bulk_create_positions(positions, delete_leftovers=True, compute_metrics=False, evaluate_rebalancer=False)
|
|
741
745
|
if rebalancing_date:
|
|
742
746
|
next_trade_proposal = rebalancer.evaluate_rebalancing(rebalancing_date)
|
|
743
747
|
|
|
@@ -759,7 +763,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
759
763
|
# function to handle the position modification after instantiation
|
|
760
764
|
if from_is_active and not to_is_active:
|
|
761
765
|
asset.weighting = Decimal(0.0)
|
|
762
|
-
asset.initial_shares = AssetPosition.
|
|
766
|
+
asset.initial_shares = AssetPosition.unannotated_objects.filter(
|
|
763
767
|
date=from_date, underlying_quote=asset.underlying_quote, portfolio=self
|
|
764
768
|
).aggregate(sum_shares=Sum("initial_shares"))["sum_shares"]
|
|
765
769
|
return asset
|
|
@@ -769,6 +773,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
769
773
|
if (
|
|
770
774
|
self.is_tracked and not self.is_lookthrough and not is_target_portfolio_imported and from_is_active
|
|
771
775
|
): # we cannot propagate a new portfolio for untracked, or look-through or already imported or inactive portfolios
|
|
776
|
+
logger.info(f"computing next weight for {self} from {from_date:%Y-%m-%d} to {to_date:%Y-%m-%d}")
|
|
772
777
|
analytic_portfolio = self.get_analytic_portfolio(from_date)
|
|
773
778
|
returns, prices = self.get_returns(analytic_portfolio.assets, (from_date - BDay(3)).date(), to_date)
|
|
774
779
|
if not returns.empty:
|
|
@@ -929,7 +934,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
929
934
|
update_dates.add(position.date)
|
|
930
935
|
self.assets.filter(date__in=update_dates, is_estimated=True).delete()
|
|
931
936
|
leftover_positions = self.assets.filter(date__in=update_dates).all()
|
|
932
|
-
objs = AssetPosition.
|
|
937
|
+
objs = AssetPosition.unannotated_objects.bulk_create(
|
|
933
938
|
positions,
|
|
934
939
|
update_fields=[
|
|
935
940
|
"weighting",
|
|
@@ -1186,16 +1191,9 @@ def default_estimate_net_value(val_date: date, instrument: Instrument) -> float
|
|
|
1186
1191
|
previous_date := portfolio.get_latest_asset_position_date(val_date - BDay(1), with_estimated=True)
|
|
1187
1192
|
) and portfolio.assets.filter(date=val_date).exists():
|
|
1188
1193
|
analytic_portfolio = portfolio.get_analytic_portfolio(val_date, with_previous_weights=True)
|
|
1189
|
-
with suppress(
|
|
1190
|
-
if
|
|
1191
|
-
|
|
1192
|
-
else:
|
|
1193
|
-
previous_net_asset_value = (
|
|
1194
|
-
InstrumentPrice.objects.filter_only_valid_prices()
|
|
1195
|
-
.get(instrument=instrument, date=previous_date)
|
|
1196
|
-
.net_value
|
|
1197
|
-
)
|
|
1198
|
-
return analytic_portfolio.get_estimate_net_value(float(previous_net_asset_value))
|
|
1194
|
+
with suppress(IndexError):
|
|
1195
|
+
if last_price := instrument.get_latest_price(previous_date):
|
|
1196
|
+
return analytic_portfolio.get_estimate_net_value(float(last_price.net_value))
|
|
1199
1197
|
|
|
1200
1198
|
|
|
1201
1199
|
@receiver(post_save, sender="wbportfolio.Product")
|
|
@@ -1240,10 +1238,10 @@ def batch_recompute_lookthrough_as_task(portfolio_id: int, start: date, end: dat
|
|
|
1240
1238
|
@receiver(investable_universe_updated, sender="wbfdm.Instrument")
|
|
1241
1239
|
def update_portfolio_after_investable_universe(*args, end_date: date | None = None, **kwargs):
|
|
1242
1240
|
if not end_date:
|
|
1243
|
-
end_date =
|
|
1241
|
+
end_date = date.today()
|
|
1242
|
+
end_date = (end_date + timedelta(days=1) - BDay(1)).date() # shift in case of business day
|
|
1244
1243
|
from_date = (end_date - BDay(1)).date()
|
|
1245
1244
|
for portfolio in Portfolio.tracked_objects.filter(is_lookthrough=False).to_dependency_iterator(from_date):
|
|
1246
|
-
logger.info(f"computing next weight for {portfolio} from {from_date:%Y-%m-%d} to {end_date:%Y-%m-%d}")
|
|
1247
1245
|
try:
|
|
1248
1246
|
portfolio.propagate_or_update_assets(from_date, end_date)
|
|
1249
1247
|
except Exception as e:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from datetime import date
|
|
2
3
|
|
|
3
4
|
from dateutil import rrule
|
|
@@ -18,6 +19,8 @@ from wbportfolio.models.transactions.trade_proposals import TradeProposal
|
|
|
18
19
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
19
20
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
20
21
|
|
|
22
|
+
logger = logging.getLogger("pms")
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
class RebalancingModel(models.Model):
|
|
23
26
|
name = models.CharField(max_length=64, verbose_name="Name")
|
|
@@ -44,7 +47,7 @@ class RebalancingModel(models.Model):
|
|
|
44
47
|
) -> PortfolioDTO:
|
|
45
48
|
model = self.model_class(portfolio, trade_date, last_effective_date, **kwargs)
|
|
46
49
|
if not model.is_valid():
|
|
47
|
-
raise ValidationError(
|
|
50
|
+
raise ValidationError(model.validation_errors)
|
|
48
51
|
return model.get_target_portfolio()
|
|
49
52
|
|
|
50
53
|
@classmethod
|
|
@@ -74,7 +77,7 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
74
77
|
activation_date = models.DateField(verbose_name="Activation Date")
|
|
75
78
|
frequency = models.CharField(
|
|
76
79
|
default="RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1",
|
|
77
|
-
max_length=
|
|
80
|
+
max_length=256,
|
|
78
81
|
verbose_name=_("Evaluation Frequency"),
|
|
79
82
|
help_text=_("The Evaluation Frequency in RRULE format"),
|
|
80
83
|
)
|
|
@@ -113,6 +116,9 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
113
116
|
"rebalancing_model": self.rebalancing_model,
|
|
114
117
|
},
|
|
115
118
|
)
|
|
119
|
+
logger.info(
|
|
120
|
+
f"Getting target portfolio ({self.portfolio}) for rebalancing model {self.rebalancing_model} for trade date {trade_date:%Y-%m-%d}"
|
|
121
|
+
)
|
|
116
122
|
if trade_proposal.rebalancing_model == self.rebalancing_model:
|
|
117
123
|
trade_proposal.status = TradeProposal.Status.DRAFT
|
|
118
124
|
try:
|
|
@@ -195,8 +195,12 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
195
195
|
|
|
196
196
|
def _get_target_portfolio(self, **kwargs) -> PortfolioDTO:
|
|
197
197
|
if self.rebalancing_model:
|
|
198
|
+
params = {}
|
|
199
|
+
if rebalancer := getattr(self.portfolio, "automatic_rebalancer", None):
|
|
200
|
+
params.update(rebalancer.parameters)
|
|
201
|
+
params.update(kwargs)
|
|
198
202
|
return self.rebalancing_model.get_target_portfolio(
|
|
199
|
-
self.portfolio, self.trade_date, self.last_effective_date, **
|
|
203
|
+
self.portfolio, self.trade_date, self.last_effective_date, **params
|
|
200
204
|
)
|
|
201
205
|
# Return the current portfolio by default
|
|
202
206
|
return self.portfolio._build_dto(self.last_effective_date)
|
|
@@ -245,28 +249,31 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
245
249
|
trade.save()
|
|
246
250
|
|
|
247
251
|
def replay(self):
|
|
248
|
-
|
|
249
|
-
while
|
|
250
|
-
logger.info(f"Replaying trade proposal {
|
|
251
|
-
|
|
252
|
-
date=
|
|
252
|
+
last_trade_proposal = self
|
|
253
|
+
while last_trade_proposal and last_trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
254
|
+
logger.info(f"Replaying trade proposal {last_trade_proposal}")
|
|
255
|
+
last_trade_proposal.portfolio.assets.filter(
|
|
256
|
+
date=last_trade_proposal.trade_date
|
|
253
257
|
).delete() # we delete the existing position and we reapply the trade proposal
|
|
254
|
-
if not
|
|
255
|
-
if
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
if not last_trade_proposal.portfolio.assets.filter(date=last_trade_proposal.trade_date).exists():
|
|
259
|
+
if last_trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
260
|
+
logger.info("Reverting trade proposal ...")
|
|
261
|
+
last_trade_proposal.revert()
|
|
262
|
+
if last_trade_proposal.status == TradeProposal.Status.DRAFT:
|
|
263
|
+
logger.info("Submitting trade proposal ...")
|
|
264
|
+
last_trade_proposal.submit()
|
|
265
|
+
if last_trade_proposal.status == TradeProposal.Status.SUBMIT:
|
|
266
|
+
logger.info("Approving trade proposal ...")
|
|
267
|
+
last_trade_proposal.approve()
|
|
268
|
+
last_trade_proposal.save()
|
|
269
|
+
next_trade_proposal = last_trade_proposal.next_trade_proposal
|
|
263
270
|
next_trade_date = (
|
|
264
271
|
next_trade_proposal.trade_date - timedelta(days=1) if next_trade_proposal else date.today()
|
|
265
272
|
)
|
|
266
|
-
overriding_trade_proposal =
|
|
267
|
-
|
|
273
|
+
overriding_trade_proposal = last_trade_proposal.portfolio.batch_portfolio(
|
|
274
|
+
last_trade_proposal.trade_date, next_trade_date
|
|
268
275
|
)
|
|
269
|
-
|
|
276
|
+
last_trade_proposal = overriding_trade_proposal or next_trade_proposal
|
|
270
277
|
|
|
271
278
|
def estimate_shares(self, trade: Trade) -> Decimal | None:
|
|
272
279
|
if not self.portfolio.only_weighting and (quote := trade.underlying_quote_price):
|
|
@@ -42,7 +42,7 @@ class TradeQueryset(OrderedModelQuerySet):
|
|
|
42
42
|
def annotate_base_info(self):
|
|
43
43
|
return self.annotate(
|
|
44
44
|
last_effective_date=Subquery(
|
|
45
|
-
AssetPosition.
|
|
45
|
+
AssetPosition.unannotated_objects.filter(
|
|
46
46
|
date__lte=OuterRef("value_date"),
|
|
47
47
|
portfolio=OuterRef("portfolio"),
|
|
48
48
|
)
|
|
@@ -51,7 +51,7 @@ class TradeQueryset(OrderedModelQuerySet):
|
|
|
51
51
|
),
|
|
52
52
|
effective_weight=Coalesce(
|
|
53
53
|
Subquery(
|
|
54
|
-
AssetPosition.
|
|
54
|
+
AssetPosition.unannotated_objects.filter(
|
|
55
55
|
underlying_quote=OuterRef("underlying_instrument"),
|
|
56
56
|
date=OuterRef("last_effective_date"),
|
|
57
57
|
portfolio=OuterRef("portfolio"),
|
|
@@ -389,7 +389,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
389
389
|
)
|
|
390
390
|
def revert(self, to_date=None, **kwargs):
|
|
391
391
|
with suppress(AssetPosition.DoesNotExist):
|
|
392
|
-
asset = AssetPosition.
|
|
392
|
+
asset = AssetPosition.unannotated_objects.get(
|
|
393
393
|
underlying_quote=self.underlying_instrument,
|
|
394
394
|
portfolio=self.portfolio,
|
|
395
395
|
date=self.transaction_date,
|
|
@@ -412,7 +412,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
412
412
|
if hasattr(self, "last_effective_date"):
|
|
413
413
|
return self.last_effective_date
|
|
414
414
|
elif (
|
|
415
|
-
assets := AssetPosition.
|
|
415
|
+
assets := AssetPosition.unannotated_objects.filter(
|
|
416
416
|
underlying_quote=self.underlying_instrument,
|
|
417
417
|
date__lt=self.transaction_date,
|
|
418
418
|
portfolio=self.portfolio,
|
|
@@ -426,7 +426,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
426
426
|
return getattr(
|
|
427
427
|
self,
|
|
428
428
|
"effective_weight",
|
|
429
|
-
AssetPosition.
|
|
429
|
+
AssetPosition.unannotated_objects.filter(
|
|
430
430
|
underlying_quote=self.underlying_instrument,
|
|
431
431
|
date=self._last_effective_date,
|
|
432
432
|
portfolio=self.portfolio,
|
wbportfolio/rebalancing/base.py
CHANGED
|
@@ -4,6 +4,10 @@ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class AbstractRebalancingModel:
|
|
7
|
+
@property
|
|
8
|
+
def validation_errors(self) -> str:
|
|
9
|
+
return getattr(self, "_validation_errors", "Rebalacing cannot applied for these parameters")
|
|
10
|
+
|
|
7
11
|
def __init__(self, portfolio, trade_date: date, last_effective_date: date, **kwargs):
|
|
8
12
|
self.portfolio = portfolio
|
|
9
13
|
self.trade_date = trade_date
|
|
@@ -3,7 +3,13 @@ from decimal import Decimal
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
from django.db.models import Q, QuerySet
|
|
5
5
|
from wbfdm.enums import MarketData
|
|
6
|
-
from wbfdm.models import
|
|
6
|
+
from wbfdm.models import (
|
|
7
|
+
Classification,
|
|
8
|
+
Exchange,
|
|
9
|
+
Instrument,
|
|
10
|
+
InstrumentClassificationThroughModel,
|
|
11
|
+
InstrumentListThroughModel,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
9
15
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
@@ -17,8 +23,7 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
17
23
|
def __init__(self, *args, **kwargs):
|
|
18
24
|
super().__init__(*args, **kwargs)
|
|
19
25
|
instruments = self._get_instruments(**kwargs)
|
|
20
|
-
self.market_cap_df = pd.
|
|
21
|
-
df = pd.DataFrame(
|
|
26
|
+
self.market_cap_df = pd.DataFrame(
|
|
22
27
|
instruments.dl.market_data(
|
|
23
28
|
values=[MarketData.MARKET_CAPITALIZATION],
|
|
24
29
|
from_date=self.last_effective_date,
|
|
@@ -26,17 +31,22 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
26
31
|
target_currency=self.TARGET_CURRENCY,
|
|
27
32
|
)
|
|
28
33
|
)
|
|
34
|
+
self.exchange_df = pd.DataFrame(
|
|
35
|
+
instruments.values_list("id", "exchange"), columns=["id", "exchange"]
|
|
36
|
+
).set_index("id")
|
|
29
37
|
try:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
self.market_cap_df = df.iloc[-1, :].transpose()
|
|
38
|
+
self.market_cap_df = self.market_cap_df[
|
|
39
|
+
["valuation_date", "market_capitalization", "instrument_id"]
|
|
40
|
+
].pivot_table(index="valuation_date", columns="instrument_id", values="market_capitalization")
|
|
41
|
+
# self.market_cap_df = df.iloc[-1, :].transpose()
|
|
35
42
|
except (IndexError, KeyError):
|
|
36
|
-
self.market_cap_df = pd.
|
|
43
|
+
self.market_cap_df = pd.DataFrame()
|
|
37
44
|
|
|
38
45
|
def _get_instruments(
|
|
39
|
-
self,
|
|
46
|
+
self,
|
|
47
|
+
classification_ids: list[int] | None = None,
|
|
48
|
+
instrument_ids: list[int] | None = None,
|
|
49
|
+
instrument_list_id: int | None = None,
|
|
40
50
|
) -> QuerySet[Instrument]:
|
|
41
51
|
"""
|
|
42
52
|
Use the provided kwargs to return a list of instruments as universe.
|
|
@@ -44,34 +54,62 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
44
54
|
- Or directly from a static list of instrument ids
|
|
45
55
|
- fallback to the last effective portfolio underlying instruments list
|
|
46
56
|
"""
|
|
57
|
+
if not instrument_ids:
|
|
58
|
+
instrument_ids = []
|
|
47
59
|
if classification_ids:
|
|
48
60
|
classifications = set()
|
|
49
61
|
for classification in Classification.objects.filter(id__in=classification_ids):
|
|
50
62
|
for children in classification.get_descendants(include_self=True):
|
|
51
63
|
classifications.add(children)
|
|
52
|
-
instrument_ids
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
instrument_ids.extend(
|
|
65
|
+
list(
|
|
66
|
+
InstrumentClassificationThroughModel.objects.filter(
|
|
67
|
+
classification__in=classifications
|
|
68
|
+
).values_list("id", flat=True)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
if instrument_list_id:
|
|
72
|
+
instrument_ids.extend(
|
|
73
|
+
list(
|
|
74
|
+
InstrumentListThroughModel.objects.filter(instrument_list_id=instrument_list_id).values_list(
|
|
75
|
+
"instrument", flat=True
|
|
76
|
+
)
|
|
55
77
|
)
|
|
56
78
|
)
|
|
57
|
-
|
|
79
|
+
|
|
80
|
+
if not instrument_ids:
|
|
58
81
|
instrument_ids = list(
|
|
59
82
|
self.portfolio.assets.filter(date=self.last_effective_date).values_list(
|
|
60
83
|
"underlying_instrument", flat=True
|
|
61
84
|
)
|
|
62
85
|
)
|
|
86
|
+
|
|
63
87
|
return Instrument.objects.filter(id__in=instrument_ids).filter(
|
|
64
|
-
Q(delisted_date__isnull=True) | Q(delisted_date__gt=self.trade_date)
|
|
88
|
+
(Q(delisted_date__isnull=True) | Q(delisted_date__gt=self.trade_date))
|
|
65
89
|
)
|
|
66
90
|
|
|
67
91
|
def is_valid(self) -> bool:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
if not self.market_cap_df.empty:
|
|
93
|
+
trade_date_mktp_cap = (
|
|
94
|
+
self.market_cap_df.loc[self.trade_date, :].transpose().rename("market_capitalization")
|
|
95
|
+
)
|
|
96
|
+
df = pd.concat(
|
|
97
|
+
[trade_date_mktp_cap, self.exchange_df], axis=1
|
|
98
|
+
) # if we are missing any market cap for not-delisted instrument, we consider the rebalancing not valid
|
|
99
|
+
df = df.groupby("exchange", dropna=False)["market_capitalization"].any()
|
|
100
|
+
missing_exchanges = Exchange.objects.filter(id__in=df[~df].index.to_list())
|
|
101
|
+
if missing_exchanges.exists():
|
|
102
|
+
setattr(
|
|
103
|
+
self,
|
|
104
|
+
"_validation_errors",
|
|
105
|
+
f"Couldn't find any market capitalization for exchanges {', '.join([str(e) for e in missing_exchanges])}",
|
|
106
|
+
)
|
|
107
|
+
return df.all()
|
|
108
|
+
return False
|
|
71
109
|
|
|
72
110
|
def get_target_portfolio(self) -> Portfolio:
|
|
73
111
|
positions = []
|
|
74
|
-
market_cap_df = self.market_cap_df
|
|
112
|
+
market_cap_df = self.market_cap_df.ffill().loc[self.trade_date, :].transpose()
|
|
75
113
|
total_market_cap = market_cap_df.sum()
|
|
76
114
|
|
|
77
115
|
for underlying_instrument, market_cap in market_cap_df.to_dict().items():
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from django.contrib.messages import warning
|
|
3
2
|
from django.core.exceptions import ValidationError
|
|
4
3
|
from rest_framework.reverse import reverse
|
|
5
4
|
from wbcore import serializers as wb_serializers
|
|
@@ -43,10 +42,11 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
43
42
|
)
|
|
44
43
|
):
|
|
45
44
|
target_portfolio_dto = target_portfolio._build_dto(last_effective_date)
|
|
46
|
-
|
|
47
|
-
ValidationError
|
|
48
|
-
): # we ignore validation error at this point as the trade are automatically create for convenience
|
|
45
|
+
try:
|
|
49
46
|
obj.reset_trades(target_portfolio=target_portfolio_dto)
|
|
47
|
+
except ValidationError as e:
|
|
48
|
+
if request := self.context.get("request"):
|
|
49
|
+
warning(request, str(e))
|
|
50
50
|
return obj
|
|
51
51
|
|
|
52
52
|
@wb_serializers.register_only_instance_resource()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
|
-
import numpy as np
|
|
4
3
|
import pytest
|
|
5
4
|
from pandas._libs.tslibs.offsets import BDay
|
|
6
5
|
from wbfdm.models import InstrumentPrice
|
|
@@ -145,13 +144,14 @@ class TestMarketCapitalizationRebalancing:
|
|
|
145
144
|
return MarketCapitalizationRebalancing(portfolio, weekday, last_effective_date, instrument_ids=[i1.id, i2.id])
|
|
146
145
|
|
|
147
146
|
def test_is_valid(self, portfolio, weekday, model, instrument_factory, instrument_price_factory):
|
|
148
|
-
assert model.is_valid()
|
|
149
|
-
model.market_cap_df.iloc[0] = np.nan
|
|
150
147
|
assert not model.is_valid()
|
|
148
|
+
i2 = model.market_cap_df.columns[1]
|
|
149
|
+
model.market_cap_df.loc[weekday, i2] = 1000 # some value
|
|
150
|
+
assert model.is_valid()
|
|
151
151
|
|
|
152
152
|
def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
|
|
153
|
-
i1 = model.market_cap_df.
|
|
154
|
-
i2 = model.market_cap_df.
|
|
153
|
+
i1 = model.market_cap_df.columns[0]
|
|
154
|
+
i2 = model.market_cap_df.columns[1]
|
|
155
155
|
mkt12 = InstrumentPrice.objects.get(instrument_id=i1, date=model.trade_date).market_capitalization
|
|
156
156
|
mkt21 = InstrumentPrice.objects.get(instrument_id=i2, date=model.last_effective_date).market_capitalization
|
|
157
157
|
|
|
@@ -362,6 +362,7 @@ class CustomerDistributionInstrumentChartViewSet(UserPortfolioRequestPermissionM
|
|
|
362
362
|
class TradeTradeProposalModelViewSet(
|
|
363
363
|
UserPortfolioRequestPermissionMixin, InternalUserPermissionMixin, OrderableMixin, viewsets.ModelViewSet
|
|
364
364
|
):
|
|
365
|
+
IMPORT_ALLOWED = True
|
|
365
366
|
ordering = (
|
|
366
367
|
"trade_proposal",
|
|
367
368
|
"order",
|
|
@@ -381,6 +382,14 @@ class TradeTradeProposalModelViewSet(
|
|
|
381
382
|
def trade_proposal(self):
|
|
382
383
|
return get_object_or_404(TradeProposal, pk=self.kwargs["trade_proposal_id"])
|
|
383
384
|
|
|
385
|
+
def has_import_permission(self, request) -> bool: # allow import only on draft trade proposal
|
|
386
|
+
return super().has_import_permission(request) and self.trade_proposal.status == TradeProposal.Status.DRAFT
|
|
387
|
+
|
|
388
|
+
def get_import_resource_kwargs(self):
|
|
389
|
+
resource_kwargs = super().get_import_resource_kwargs()
|
|
390
|
+
resource_kwargs["columns_mapping"] = {"underlying_instrument": "underlying_instrument__isin"}
|
|
391
|
+
return resource_kwargs
|
|
392
|
+
|
|
384
393
|
def get_resource_class(self):
|
|
385
394
|
return TradeProposalTradeResource
|
|
386
395
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
wbportfolio/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
2
|
-
wbportfolio/apps.py,sha256=
|
|
2
|
+
wbportfolio/apps.py,sha256=tybcoLFiw5tLdGHYV68X96n6jNZqx4BYx7Ao8mPflH8,749
|
|
3
3
|
wbportfolio/dynamic_preferences_registry.py,sha256=iPGAiCScrIADX2NA8oChpQquAmTxVLO6O6ty_1n5dFg,2030
|
|
4
4
|
wbportfolio/permissions.py,sha256=F147DXfitbw6IdMQGEFfymCJkiG5YGkWKsLdVVliPyw,320
|
|
5
5
|
wbportfolio/preferences.py,sha256=yS3a7wmFdFTLJwus2dmg2oHbSSUL2lXNzHCVKGgUKGY,246
|
|
@@ -110,12 +110,12 @@ wbportfolio/import_export/backends/wbfdm/dividend.py,sha256=iAQXnYPXmtG_Jrc8THAJ
|
|
|
110
110
|
wbportfolio/import_export/backends/wbfdm/mixin.py,sha256=JNtjgqGLson1nu_Chqb8MWyuiF3Ws8ox2vapxIRBYKE,400
|
|
111
111
|
wbportfolio/import_export/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
112
|
wbportfolio/import_export/handlers/adjustment.py,sha256=6bdTIYFmc8_HFxcdwtnYwglMyCfAD8XrTIrEb2zWY0g,1757
|
|
113
|
-
wbportfolio/import_export/handlers/asset_position.py,sha256=
|
|
113
|
+
wbportfolio/import_export/handlers/asset_position.py,sha256=5wFnHcbq_zGp9rBUec_JEpzjCA0_v17VrV9F8Ps1ETs,8645
|
|
114
114
|
wbportfolio/import_export/handlers/dividend.py,sha256=tftdVdAzNpKSSvouOtvJfzWL362HUPIC94F6Noha8CE,3998
|
|
115
115
|
wbportfolio/import_export/handlers/fees.py,sha256=XYH752IkNGYhhhwatp8nYa1zG1-YZFDkYW15dyQgOIg,2824
|
|
116
116
|
wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=2ODaquuC83RfmNwmQ-8TdhiASObfIems_B1g0yqaYTs,2733
|
|
117
117
|
wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
|
|
118
|
-
wbportfolio/import_export/handlers/trade.py,sha256=
|
|
118
|
+
wbportfolio/import_export/handlers/trade.py,sha256=LIzXNEN7-tRWCUBvisgy8eTDL85JWO5c7iN06yJA-TI,10395
|
|
119
119
|
wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
120
120
|
wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
|
|
121
121
|
wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -190,7 +190,7 @@ wbportfolio/import_export/parsers/vontobel/valuation.py,sha256=iav8_xYpTJchmTa7K
|
|
|
190
190
|
wbportfolio/import_export/parsers/vontobel/valuation_api.py,sha256=WLkZ5z-WqhFraNorWlOhIpSx1pQ2fnjdsLHwSTA7O2o,882
|
|
191
191
|
wbportfolio/import_export/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
192
192
|
wbportfolio/import_export/resources/assets.py,sha256=zjgHlQWpud41jHrKdRyqGUt1KUJQm9Z7pt0uVh4qBWQ,2234
|
|
193
|
-
wbportfolio/import_export/resources/trades.py,sha256=
|
|
193
|
+
wbportfolio/import_export/resources/trades.py,sha256=LC1SBFmoxT160s0__cYDV2i99uECVrR7OHN0vAJ2LPQ,2232
|
|
194
194
|
wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql,sha256=BvMnLsVlXIqU5hhwLDbwZ6nBqWULIBDVcgLJ2B4sdS4,4440
|
|
195
195
|
wbportfolio/kpi_handlers/nnm.py,sha256=hCn0oG0C-6dQ0G-6S4r31nAS633NZdlOT-ntZrzvXZI,7180
|
|
196
196
|
wbportfolio/metric/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -239,13 +239,14 @@ wbportfolio/migrations/0070_remove_assetposition_unique_asset_position_and_more.
|
|
|
239
239
|
wbportfolio/migrations/0071_alter_trade_options_alter_trade_order.py,sha256=QjAyQr1eSs2X73zL03uG_MjfcGZhSJV9YQ0UJ39FpVk,695
|
|
240
240
|
wbportfolio/migrations/0072_trade_diff_shares.py,sha256=aTKa1SbIiwmlXaFtBg-ENrSxfM_cf3RPNQBQlk2VEZ0,635
|
|
241
241
|
wbportfolio/migrations/0073_remove_product_price_computation_and_more.py,sha256=J4puisDFwnbnfv2VLWaiCQ7ost6PCOkin9qKVQoLIWM,18725
|
|
242
|
+
wbportfolio/migrations/0074_alter_rebalancer_frequency_and_more.py,sha256=o01rBj-ADgwCRtAai3e5z27alPGEzaiNxUqCwWm6peY,918
|
|
242
243
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
243
244
|
wbportfolio/models/__init__.py,sha256=PDLJry5w1zE4N4arQh20_uFi2v7gy9QyavJ_rfGE21Q,882
|
|
244
245
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
245
246
|
wbportfolio/models/asset.py,sha256=V-fSAF1bsfj_Td3VfdKyhCgcwB_xPS6vTI90Dh5iDx8,37502
|
|
246
247
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
247
248
|
wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
|
|
248
|
-
wbportfolio/models/portfolio.py,sha256
|
|
249
|
+
wbportfolio/models/portfolio.py,sha256=NsgxZu32H7AfSHyn2Rwa65EkjvZt-YtQHzWhWlV6zf0,55513
|
|
249
250
|
wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
|
|
250
251
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
251
252
|
wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
|
|
@@ -260,7 +261,7 @@ wbportfolio/models/graphs/portfolio.py,sha256=NwkehWvTcyTYrKO5ku3eNNaYLuBwuLdSbT
|
|
|
260
261
|
wbportfolio/models/graphs/utils.py,sha256=1AMpEE9mDuUZ82XgN2irxjCW1-LmziROhKevEBo0mJE,2347
|
|
261
262
|
wbportfolio/models/llm/wbcrm/analyze_relationship.py,sha256=_y2Myc-M2hXQDkRGXvzsM0ZNC31dmxSHHz5BKMtymww,2106
|
|
262
263
|
wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
263
|
-
wbportfolio/models/mixins/instruments.py,sha256=
|
|
264
|
+
wbportfolio/models/mixins/instruments.py,sha256=IucFwxbSxyqLwDbaMYg_vKh1BPwGqo1VERpj0U-nS0Q,6728
|
|
264
265
|
wbportfolio/models/mixins/liquidity_stress_test.py,sha256=whkzjtbOyl_ncNyaQBORb_Z_rDgcvfdTYPgqPolu7dA,58865
|
|
265
266
|
wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
|
|
266
267
|
wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
|
|
@@ -271,9 +272,9 @@ wbportfolio/models/transactions/claim.py,sha256=agdpGqxpO0FSzYDWV-Gv1tQY46k0LN9C
|
|
|
271
272
|
wbportfolio/models/transactions/dividends.py,sha256=naL5xeDQfUBf5KyGt7y-tTcHL22nzZumT8DV6AaG8Bg,1064
|
|
272
273
|
wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
|
|
273
274
|
wbportfolio/models/transactions/fees.py,sha256=ffvqo8I4A0l5rLi00jJ6sGot0jmnkoxaNsbDzdPLwCg,5712
|
|
274
|
-
wbportfolio/models/transactions/rebalancing.py,sha256=
|
|
275
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
276
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
275
|
+
wbportfolio/models/transactions/rebalancing.py,sha256=vEYdYhog9jiPw_xQd_XQunNzcpHZ8Zk18kQhsN4HuhA,6596
|
|
276
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=tpxg6EJ8Z6sB7VJ39GVLH-sBVJ9_ypUOqaUE0T5Rvhk,20804
|
|
277
|
+
wbportfolio/models/transactions/trades.py,sha256=7IAC9eDx-ouKGn1RPR_Ggd5XjLgr3rE4xBn_ub0o1TE,27589
|
|
277
278
|
wbportfolio/models/transactions/transactions.py,sha256=4THsE4xqdigZAwWKYfTNRLPJlkmAmsgE70Ribp9Lnrk,7127
|
|
278
279
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
279
280
|
wbportfolio/pms/typing.py,sha256=lRWh9alcstZzwA04hFSPZfOFbCjaVPWtUpWnurnsh8c,6014
|
|
@@ -283,12 +284,12 @@ wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
283
284
|
wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
|
|
284
285
|
wbportfolio/pms/trading/handler.py,sha256=Xpgo719S0jE1wUTTyGFpYccPEIg9GXghWEAdYawJbrk,7165
|
|
285
286
|
wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
286
|
-
wbportfolio/rebalancing/base.py,sha256=
|
|
287
|
+
wbportfolio/rebalancing/base.py,sha256=NwTGZtBm1f35gj5Jp6iTyyFvDT1GSIztN990cKBvYzQ,637
|
|
287
288
|
wbportfolio/rebalancing/decorators.py,sha256=JhQ2vkcIuGBTGvNmpkQerdw2-vLq-RAb0KAPjOrETkw,515
|
|
288
289
|
wbportfolio/rebalancing/models/__init__.py,sha256=AQjG7Tu5vlmhqncVoYOjpBKU2UIvgo9FuP2_jD2w-UI,232
|
|
289
290
|
wbportfolio/rebalancing/models/composite.py,sha256=XAjJqLRNsV-MuBKrat3THEfAWs6PXQNSO0g8k8MtBXo,1157
|
|
290
291
|
wbportfolio/rebalancing/models/equally_weighted.py,sha256=U29MOHJMQMIg7Y7W_8t5K3nXjaznzt4ArIxQSiv0Xok,863
|
|
291
|
-
wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=
|
|
292
|
+
wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=RskXZ41iSK9g4SSYJRNv0hX6z9Ol73nJY2N7A3IxCLw,5051
|
|
292
293
|
wbportfolio/rebalancing/models/model_portfolio.py,sha256=XQdvs03-0M9YUnL4DidwZC4E6k-ANCNcZ--T_aaOXTQ,1233
|
|
293
294
|
wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
294
295
|
wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
|
|
@@ -338,7 +339,7 @@ wbportfolio/serializers/transactions/claim.py,sha256=kC4E2RZRrpd9i8tGfoiV-gpWDk3
|
|
|
338
339
|
wbportfolio/serializers/transactions/dividends.py,sha256=EULwKDumHBv4r2HsdEGZMZGFaye4dRUNNyXg6-wZXzc,520
|
|
339
340
|
wbportfolio/serializers/transactions/expiry.py,sha256=K3XOSbCyef-xRzOjCr4Qg_YFJ_JuuiJ9u6tDS86l0hg,477
|
|
340
341
|
wbportfolio/serializers/transactions/fees.py,sha256=uPmSWuCeoV2bwVS6RmEz3a0VRBWJHIQr0WhklYc1UAI,1068
|
|
341
|
-
wbportfolio/serializers/transactions/trade_proposals.py,sha256=
|
|
342
|
+
wbportfolio/serializers/transactions/trade_proposals.py,sha256=h8Ub69YPEhds1fC0dcbvXmSmpszor0WWxsodUeJmPVY,3414
|
|
342
343
|
wbportfolio/serializers/transactions/trades.py,sha256=pONV5NSqrXUnoTEoAxovnnQqu37cZGuB33TYvIOK3rE,10009
|
|
343
344
|
wbportfolio/serializers/transactions/transactions.py,sha256=O137zeCndK-nxIWSRLEj7bXbBZDGa4d6qK6pJIIYK3g,4170
|
|
344
345
|
wbportfolio/static/wbportfolio/css/macro_review.css,sha256=FAVVO8nModxwPXcTKpcfzVxBGPZGJVK1Xn-0dkSfGyc,233
|
|
@@ -384,7 +385,7 @@ wbportfolio/tests/models/transactions/test_trades.py,sha256=z0CCZjB648ECDSEdwmzq
|
|
|
384
385
|
wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
385
386
|
wbportfolio/tests/pms/test_analytics.py,sha256=FrvVsV_uUiTgmRUfsaB-_sGzY30CqknbOY2DvmwR_70,1141
|
|
386
387
|
wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
387
|
-
wbportfolio/tests/rebalancing/test_models.py,sha256=
|
|
388
|
+
wbportfolio/tests/rebalancing/test_models.py,sha256=00VGrz_UZtYSqpTg2J0XCt-zInOAvYSNezad8KKxNnw,7660
|
|
388
389
|
wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
389
390
|
wbportfolio/tests/serializers/test_claims.py,sha256=vQrg73xQXRFEgvx3KI9ivFre_wpBFzdO0p0J13PkvdY,582
|
|
390
391
|
wbportfolio/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -514,9 +515,9 @@ wbportfolio/viewsets/transactions/fees.py,sha256=7VUXIogmRrXCz_D9tvDiiTae0t5j09W
|
|
|
514
515
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
515
516
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
516
517
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=fYTvvRk7k5xsBzbIgJvU4I4OrllF0VkhlrekD4GVgDk,4296
|
|
517
|
-
wbportfolio/viewsets/transactions/trades.py,sha256=
|
|
518
|
+
wbportfolio/viewsets/transactions/trades.py,sha256=wdtEWN1V5wsmesR3mRxPmTJUIAmDmqaNsfIhOB57kqY,16330
|
|
518
519
|
wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
|
|
519
|
-
wbportfolio-1.46.
|
|
520
|
-
wbportfolio-1.46.
|
|
521
|
-
wbportfolio-1.46.
|
|
522
|
-
wbportfolio-1.46.
|
|
520
|
+
wbportfolio-1.46.2.dist-info/METADATA,sha256=bEYm3Lj9tFZ1b7iRLZKLFmRjfGKhl5HLDHXbJJIr7Eo,734
|
|
521
|
+
wbportfolio-1.46.2.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
522
|
+
wbportfolio-1.46.2.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
523
|
+
wbportfolio-1.46.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|