wbportfolio 1.54.17__py2.py3-none-any.whl → 1.54.19__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/import_export/handlers/orders.py +71 -0
- wbportfolio/import_export/handlers/trade.py +42 -70
- wbportfolio/models/orders/order_proposals.py +13 -20
- wbportfolio/models/orders/orders.py +3 -8
- wbportfolio/models/portfolio.py +13 -14
- wbportfolio/tests/models/orders/test_order_proposals.py +3 -3
- wbportfolio/viewsets/assets.py +17 -2
- wbportfolio/viewsets/orders/orders.py +0 -1
- {wbportfolio-1.54.17.dist-info → wbportfolio-1.54.19.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.17.dist-info → wbportfolio-1.54.19.dist-info}/RECORD +12 -11
- {wbportfolio-1.54.17.dist-info → wbportfolio-1.54.19.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.17.dist-info → wbportfolio-1.54.19.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from django.db import models
|
|
5
|
+
from wbcore.contrib.io.exceptions import DeserializationError
|
|
6
|
+
from wbcore.contrib.io.imports import ImportExportHandler, ImportState
|
|
7
|
+
from wbcore.contrib.io.utils import nest_row
|
|
8
|
+
from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
|
|
9
|
+
|
|
10
|
+
from wbportfolio.pms.typing import Portfolio, Position
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OrderImportHandler(ImportExportHandler):
|
|
14
|
+
MODEL_APP_LABEL: str = "wbportfolio.Order"
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
self.instrument_handler = InstrumentImportHandler(self.import_source)
|
|
19
|
+
self.order_proposal = None
|
|
20
|
+
|
|
21
|
+
def process_object(
|
|
22
|
+
self,
|
|
23
|
+
data: Dict[str, Any],
|
|
24
|
+
**kwargs,
|
|
25
|
+
):
|
|
26
|
+
from wbportfolio.models import OrderProposal
|
|
27
|
+
|
|
28
|
+
data = nest_row(data)
|
|
29
|
+
underlying_instrument = self.instrument_handler.process_object(
|
|
30
|
+
data["underlying_instrument"], only_security=False, read_only=True
|
|
31
|
+
)[0]
|
|
32
|
+
self.order_proposal = OrderProposal.objects.get(id=data.pop("order_proposal_id"))
|
|
33
|
+
weighting = data.get("target_weight", data.get("weighting"))
|
|
34
|
+
shares = data.get("target_shares", data.get("shares", 0))
|
|
35
|
+
if not weighting:
|
|
36
|
+
raise DeserializationError("We couldn't figure out the target weight column")
|
|
37
|
+
position_dto = Position(
|
|
38
|
+
underlying_instrument=underlying_instrument.id,
|
|
39
|
+
instrument_type=underlying_instrument.instrument_type.id,
|
|
40
|
+
weighting=Decimal(weighting),
|
|
41
|
+
shares=Decimal(shares),
|
|
42
|
+
currency=underlying_instrument.currency,
|
|
43
|
+
date=self.order_proposal.trade_date,
|
|
44
|
+
is_cash=underlying_instrument.is_cash,
|
|
45
|
+
)
|
|
46
|
+
return position_dto, ImportState.CREATED
|
|
47
|
+
|
|
48
|
+
def _get_history(self, history: Dict[str, Any]) -> models.QuerySet:
|
|
49
|
+
from wbportfolio.models.orders.order_proposals import OrderProposal
|
|
50
|
+
|
|
51
|
+
if order_proposal_id := history.get("order_proposal_id"):
|
|
52
|
+
# if a order proposal is provided, we delete the existing history first as otherwise, it would mess with the target weight computation
|
|
53
|
+
order_proposal = OrderProposal.objects.get(id=order_proposal_id)
|
|
54
|
+
order_proposal.orders.all().delete()
|
|
55
|
+
return self.model.objects.none()
|
|
56
|
+
|
|
57
|
+
def _post_processing_objects(self, positions: list[Position], *args, **kwargs):
|
|
58
|
+
total_weight = sum(map(lambda p: p.weighting, positions))
|
|
59
|
+
if cash_weight := Decimal("1") - total_weight:
|
|
60
|
+
cash_component = self.order_proposal.cash_component
|
|
61
|
+
positions.append(
|
|
62
|
+
Position(
|
|
63
|
+
underlying_instrument=cash_component.id,
|
|
64
|
+
instrument_type=cash_component.instrument_type.id,
|
|
65
|
+
weighting=cash_weight,
|
|
66
|
+
currency=cash_component.currency,
|
|
67
|
+
date=self.order_proposal.trade_date,
|
|
68
|
+
is_cash=cash_component.is_cash,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
self.order_proposal.reset_orders(target_portfolio=Portfolio(positions))
|
|
@@ -25,7 +25,6 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
25
25
|
self.instrument_handler = InstrumentImportHandler(self.import_source)
|
|
26
26
|
self.register_handler = RegisterImportHandler(self.import_source)
|
|
27
27
|
self.currency_handler = CurrencyImportHandler(self.import_source)
|
|
28
|
-
self.order_proposals = set()
|
|
29
28
|
|
|
30
29
|
def _data_changed(self, _object, change_data: Dict[str, Any], initial_data: Dict[str, Any], **kwargs):
|
|
31
30
|
if (new_register := change_data.get("register")) and (current_register := _object.register):
|
|
@@ -35,41 +34,32 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
35
34
|
return super()._data_changed(_object, change_data, initial_data, **kwargs)
|
|
36
35
|
|
|
37
36
|
def _deserialize(self, data: Dict[str, Any]):
|
|
38
|
-
from wbportfolio.models import
|
|
37
|
+
from wbportfolio.models import Product
|
|
39
38
|
|
|
40
39
|
if underlying_instrument := data.get("underlying_instrument", None):
|
|
41
40
|
data["underlying_instrument"] = self.instrument_handler.process_object(
|
|
42
41
|
underlying_instrument, only_security=False, read_only=True
|
|
43
42
|
)[0]
|
|
44
43
|
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
data["
|
|
49
|
-
|
|
50
|
-
data["
|
|
51
|
-
|
|
52
|
-
data["
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if transaction_date_str := data.get("transaction_date", None):
|
|
57
|
-
data["transaction_date"] = datetime.strptime(transaction_date_str, "%Y-%m-%d").date()
|
|
58
|
-
if value_date_str := data.get("value_date", None):
|
|
59
|
-
data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
|
|
60
|
-
if book_date_str := data.get("book_date", None):
|
|
61
|
-
data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
|
|
62
|
-
data["portfolio"] = Portfolio._get_or_create_portfolio(
|
|
63
|
-
self.instrument_handler, data.get("portfolio", data["underlying_instrument"])
|
|
64
|
-
)
|
|
44
|
+
if external_id_alternative := data.get("external_id_alternative", None):
|
|
45
|
+
data["external_id_alternative"] = str(external_id_alternative)
|
|
46
|
+
if transaction_date_str := data.get("transaction_date", None):
|
|
47
|
+
data["transaction_date"] = datetime.strptime(transaction_date_str, "%Y-%m-%d").date()
|
|
48
|
+
if value_date_str := data.get("value_date", None):
|
|
49
|
+
data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
|
|
50
|
+
if book_date_str := data.get("book_date", None):
|
|
51
|
+
data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
|
|
52
|
+
data["portfolio"] = Portfolio._get_or_create_portfolio(
|
|
53
|
+
self.instrument_handler, data.get("portfolio", data["underlying_instrument"])
|
|
54
|
+
)
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
if currency_data := data.get("currency", None):
|
|
57
|
+
data["currency"] = self.currency_handler.process_object(currency_data, read_only=True)[0]
|
|
68
58
|
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
if register_data := data.get("register", None):
|
|
60
|
+
data["register"] = self.register_handler.process_object(register_data)[0]
|
|
71
61
|
|
|
72
|
-
|
|
62
|
+
data["marked_for_deletion"] = data.get("marked_for_deletion", False)
|
|
73
63
|
if underlying_instrument := data.get("underlying_instrument"):
|
|
74
64
|
if nominal := data.pop("nominal", None):
|
|
75
65
|
try:
|
|
@@ -160,39 +150,30 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
160
150
|
self.import_source.log += "\nNo trade was successfully matched."
|
|
161
151
|
|
|
162
152
|
def _get_history(self, history: Dict[str, Any]) -> models.QuerySet:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
trades =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if transaction_date := history.get("transaction_date"):
|
|
181
|
-
trades = trades.filter(transaction_date__lte=transaction_date)
|
|
182
|
-
elif book_date := history.get("book_date"):
|
|
183
|
-
trades = trades.filter(book_date__lte=book_date)
|
|
184
|
-
if underlying_instrument_data := history.get("underlying_instrument"):
|
|
185
|
-
if isinstance(underlying_instrument_data, dict):
|
|
186
|
-
trades = trades.filter(
|
|
187
|
-
**{f"underlying_instrument__{k}": v for k, v in underlying_instrument_data.items()}
|
|
188
|
-
)
|
|
189
|
-
else:
|
|
190
|
-
trades = trades.filter(underlying_instrument__id=underlying_instrument_data)
|
|
191
|
-
|
|
192
|
-
elif "underlying_instruments" in history:
|
|
193
|
-
trades = trades.filter(underlying_instrument__id__in=history["underlying_instruments"])
|
|
153
|
+
trades = self.model.objects.filter(
|
|
154
|
+
exclude_from_history=False,
|
|
155
|
+
pending=False,
|
|
156
|
+
transaction_subtype__in=[
|
|
157
|
+
self.model.Type.SUBSCRIPTION,
|
|
158
|
+
self.model.Type.REDEMPTION,
|
|
159
|
+
], # we cannot exclude marked for deleted trade because otherwise they are never consider in the history
|
|
160
|
+
)
|
|
161
|
+
if transaction_date := history.get("transaction_date"):
|
|
162
|
+
trades = trades.filter(transaction_date__lte=transaction_date)
|
|
163
|
+
elif book_date := history.get("book_date"):
|
|
164
|
+
trades = trades.filter(book_date__lte=book_date)
|
|
165
|
+
if underlying_instrument_data := history.get("underlying_instrument"):
|
|
166
|
+
if isinstance(underlying_instrument_data, dict):
|
|
167
|
+
trades = trades.filter(
|
|
168
|
+
**{f"underlying_instrument__{k}": v for k, v in underlying_instrument_data.items()}
|
|
169
|
+
)
|
|
194
170
|
else:
|
|
195
|
-
|
|
171
|
+
trades = trades.filter(underlying_instrument__id=underlying_instrument_data)
|
|
172
|
+
|
|
173
|
+
elif "underlying_instruments" in history:
|
|
174
|
+
trades = trades.filter(underlying_instrument__id__in=history["underlying_instruments"])
|
|
175
|
+
else:
|
|
176
|
+
raise ValueError("We cannot estimate history without at least the underlying instrument")
|
|
196
177
|
return trades
|
|
197
178
|
|
|
198
179
|
def _post_processing_objects(
|
|
@@ -201,18 +182,12 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
201
182
|
modified_objs: list[models.Model],
|
|
202
183
|
unmodified_objs: list[models.Model],
|
|
203
184
|
):
|
|
204
|
-
from wbportfolio.models.orders.order_proposals import replay_as_task
|
|
205
|
-
|
|
206
185
|
for instrument in set(
|
|
207
186
|
map(lambda x: x.underlying_instrument, filter(lambda t: t.is_customer_trade, created_objs + modified_objs))
|
|
208
187
|
):
|
|
209
188
|
if instrument.instrument_type.key == "product":
|
|
210
189
|
update_outstanding_shares_as_task.delay(instrument.id)
|
|
211
190
|
|
|
212
|
-
# if the trade import relates to a order proposal, we reset the TP after the import to ensure it contains the deleted positions (often forgotten by user)
|
|
213
|
-
for changed_order_proposal in self.order_proposals:
|
|
214
|
-
replay_as_task.delay(changed_order_proposal.id)
|
|
215
|
-
|
|
216
191
|
def _post_processing_updated_object(self, _object):
|
|
217
192
|
if _object.marked_for_deletion:
|
|
218
193
|
_object.marked_for_deletion = False
|
|
@@ -231,8 +206,5 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
231
206
|
self.import_source.log += (
|
|
232
207
|
f"{trade.transaction_date:%d.%m.%Y}: {trade.shares} {trade.bank} ==> Marked for deletion"
|
|
233
208
|
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
else:
|
|
237
|
-
trade.marked_for_deletion = True
|
|
238
|
-
trade.save()
|
|
209
|
+
trade.marked_for_deletion = True
|
|
210
|
+
trade.save()
|
|
@@ -91,14 +91,11 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
91
91
|
]
|
|
92
92
|
|
|
93
93
|
def save(self, *args, **kwargs):
|
|
94
|
-
if not self.trade_date and self.portfolio.assets.exists():
|
|
95
|
-
self.trade_date = (self.portfolio.assets.latest("date").date + BDay(1)).date()
|
|
96
|
-
|
|
97
94
|
# if a order proposal is created before the existing earliest order proposal, we automatically shift the linked instruments inception date to allow automatic NAV computation since the new inception date
|
|
98
95
|
if not self.portfolio.order_proposals.filter(trade_date__lt=self.trade_date).exists():
|
|
99
|
-
|
|
100
|
-
self.portfolio.instruments.filter(inception_date__gt=
|
|
101
|
-
inception_date=
|
|
96
|
+
# we need to set the inception date as the first order proposal trade date (and thus, the first position date). We expect a NAV at 100 then
|
|
97
|
+
self.portfolio.instruments.filter(inception_date__gt=self.trade_date).update(
|
|
98
|
+
inception_date=self.trade_date
|
|
102
99
|
)
|
|
103
100
|
super().save(*args, **kwargs)
|
|
104
101
|
|
|
@@ -157,6 +154,12 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
157
154
|
return future_proposals.earliest("trade_date")
|
|
158
155
|
return None
|
|
159
156
|
|
|
157
|
+
@property
|
|
158
|
+
def cash_component(self) -> Cash:
|
|
159
|
+
return Cash.objects.get_or_create(
|
|
160
|
+
currency=self.portfolio.currency, defaults={"is_cash": True, "name": self.portfolio.currency.title}
|
|
161
|
+
)[0]
|
|
162
|
+
|
|
160
163
|
def get_orders(self):
|
|
161
164
|
base_qs = self.orders.all().annotate(
|
|
162
165
|
last_effective_date=Subquery(
|
|
@@ -215,7 +218,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
215
218
|
),
|
|
216
219
|
default=models.F("effective_weight"),
|
|
217
220
|
),
|
|
218
|
-
target_weight=models.F("effective_weight") + models.F("weighting"),
|
|
219
221
|
)
|
|
220
222
|
return orders.annotate(
|
|
221
223
|
has_warnings=models.Case(
|
|
@@ -287,7 +289,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
287
289
|
)
|
|
288
290
|
)
|
|
289
291
|
total_weighting += weighting
|
|
290
|
-
if with_cash and (cash_weight := Decimal("1") - total_weighting):
|
|
292
|
+
if portfolio and with_cash and (cash_weight := Decimal("1") - total_weighting):
|
|
291
293
|
cash_position = self.get_estimated_target_cash(target_cash_weight=cash_weight)
|
|
292
294
|
positions.append(cash_position._build_dto())
|
|
293
295
|
return PortfolioDTO(positions)
|
|
@@ -430,7 +432,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
430
432
|
approve_automatically: bool = True,
|
|
431
433
|
silent_exception: bool = False,
|
|
432
434
|
force_reset_order: bool = False,
|
|
433
|
-
broadcast_changes_at_date: bool = True,
|
|
434
435
|
**reset_order_kwargs,
|
|
435
436
|
):
|
|
436
437
|
if self.status == OrderProposal.Status.APPROVED:
|
|
@@ -453,7 +454,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
453
454
|
if self.status == OrderProposal.Status.SUBMIT:
|
|
454
455
|
logger.info("Approving order proposal ...")
|
|
455
456
|
if approve_automatically and self.portfolio.can_be_rebalanced:
|
|
456
|
-
self.approve(replay=False
|
|
457
|
+
self.approve(replay=False)
|
|
457
458
|
|
|
458
459
|
def replay(self, broadcast_changes_at_date: bool = True):
|
|
459
460
|
last_order_proposal = self
|
|
@@ -461,11 +462,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
461
462
|
while last_order_proposal and last_order_proposal.status == OrderProposal.Status.APPROVED:
|
|
462
463
|
logger.info(f"Replaying order proposal {last_order_proposal}")
|
|
463
464
|
if not last_order_proposal_created:
|
|
464
|
-
last_order_proposal.approve_workflow(
|
|
465
|
-
silent_exception=True,
|
|
466
|
-
force_reset_order=True,
|
|
467
|
-
broadcast_changes_at_date=broadcast_changes_at_date,
|
|
468
|
-
)
|
|
465
|
+
last_order_proposal.approve_workflow(silent_exception=True, force_reset_order=True)
|
|
469
466
|
last_order_proposal.save()
|
|
470
467
|
if last_order_proposal.status != OrderProposal.Status.APPROVED:
|
|
471
468
|
break
|
|
@@ -575,8 +572,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
575
572
|
"""
|
|
576
573
|
# Retrieve orders with base information
|
|
577
574
|
orders = self.get_orders()
|
|
578
|
-
currency = self.portfolio.currency
|
|
579
|
-
|
|
580
575
|
# Calculate the total target weight of all orders
|
|
581
576
|
total_target_weight = orders.exclude(underlying_instrument__is_cash=True).aggregate(
|
|
582
577
|
s=models.Sum("target_weight")
|
|
@@ -589,9 +584,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
589
584
|
total_target_shares = Decimal(0)
|
|
590
585
|
|
|
591
586
|
# Get or create a cash component for the portfolio's currency
|
|
592
|
-
cash_component =
|
|
593
|
-
currency=currency, defaults={"is_cash": True, "name": currency.title}
|
|
594
|
-
)[0]
|
|
587
|
+
cash_component = self.cash_component
|
|
595
588
|
# If the portfolio is not only weighting-based, estimate the target shares for the cash component
|
|
596
589
|
if not self.portfolio.only_weighting:
|
|
597
590
|
# Estimate the target shares for the cash component
|
|
@@ -9,13 +9,13 @@ from django.db.models import (
|
|
|
9
9
|
from ordered_model.models import OrderedModel
|
|
10
10
|
from wbcore.contrib.io.mixins import ImportMixin
|
|
11
11
|
|
|
12
|
-
from wbportfolio.import_export.handlers.
|
|
12
|
+
from wbportfolio.import_export.handlers.orders import OrderImportHandler
|
|
13
13
|
from wbportfolio.models.asset import AssetPosition
|
|
14
14
|
from wbportfolio.models.transactions.transactions import TransactionMixin
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
18
|
-
import_export_handler_class =
|
|
18
|
+
import_export_handler_class = OrderImportHandler
|
|
19
19
|
|
|
20
20
|
ORDER_WEIGHTING_PRECISION = (
|
|
21
21
|
8 # we need to match the asset position weighting. Skfolio advices using a even smaller number (5)
|
|
@@ -80,7 +80,7 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
80
80
|
|
|
81
81
|
if not self.price:
|
|
82
82
|
warnings.append(f"No price for {self.underlying_instrument.computed_str}")
|
|
83
|
-
if
|
|
83
|
+
if self._target_weight < 1e-8: # any value below -1e8 will be considered zero
|
|
84
84
|
warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
|
|
85
85
|
return warnings
|
|
86
86
|
|
|
@@ -145,11 +145,6 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
145
145
|
self, "target_weight", round(self._effective_weight + self.weighting, self.ORDER_WEIGHTING_PRECISION)
|
|
146
146
|
)
|
|
147
147
|
|
|
148
|
-
@_target_weight.setter
|
|
149
|
-
def _target_weight(self, target_weight):
|
|
150
|
-
self.weighting = Decimal(target_weight) - self._effective_weight
|
|
151
|
-
self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
|
|
152
|
-
|
|
153
148
|
@property
|
|
154
149
|
@admin.display(description="Target Shares")
|
|
155
150
|
def _target_shares(self) -> Decimal:
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -713,6 +713,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
713
713
|
force_recompute_weighting: bool = False,
|
|
714
714
|
evaluate_rebalancer: bool = True,
|
|
715
715
|
changed_weights: dict[int, float] | None = None,
|
|
716
|
+
broadcast_changes_at_date: bool = True,
|
|
716
717
|
**kwargs,
|
|
717
718
|
):
|
|
718
719
|
logger.info(f"change at date for {self} at {val_date}")
|
|
@@ -754,14 +755,14 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
754
755
|
if not self.initial_position_date or self.initial_position_date > val_date:
|
|
755
756
|
self.initial_position_date = val_date
|
|
756
757
|
self.save()
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
758
|
+
if broadcast_changes_at_date:
|
|
759
|
+
self.handle_controlling_portfolio_change_at_date(
|
|
760
|
+
val_date,
|
|
761
|
+
recompute_weighting=recompute_weighting,
|
|
762
|
+
force_recompute_weighting=force_recompute_weighting,
|
|
763
|
+
changed_weights=changed_weights,
|
|
764
|
+
**kwargs,
|
|
765
|
+
)
|
|
765
766
|
|
|
766
767
|
def handle_controlling_portfolio_change_at_date(self, val_date: date, **kwargs):
|
|
767
768
|
if self.is_tracked:
|
|
@@ -866,7 +867,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
866
867
|
if stop_at_rebalancing:
|
|
867
868
|
break
|
|
868
869
|
next_weights = {
|
|
869
|
-
trade.underlying_instrument: float(trade._target_weight)
|
|
870
|
+
trade.underlying_instrument.id: float(trade._target_weight)
|
|
870
871
|
for trade in last_order_proposal.get_orders()
|
|
871
872
|
}
|
|
872
873
|
positions.add((to_date, next_weights), is_estimated=False)
|
|
@@ -1048,7 +1049,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1048
1049
|
delete_leftovers: bool = False,
|
|
1049
1050
|
force_save: bool = False,
|
|
1050
1051
|
compute_metrics: bool = False,
|
|
1051
|
-
broadcast_changes_at_date: bool = True,
|
|
1052
1052
|
**kwargs,
|
|
1053
1053
|
):
|
|
1054
1054
|
if positions:
|
|
@@ -1097,10 +1097,9 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1097
1097
|
basket_id=self.id,
|
|
1098
1098
|
basket_content_type_id=ContentType.objects.get_for_model(Portfolio).id,
|
|
1099
1099
|
)
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
self.change_at_date(update_date, changed_weights=changed_weights, **kwargs)
|
|
1100
|
+
for update_date, changed_weights in positions.get_weights().items():
|
|
1101
|
+
kwargs.pop("changed_weights", None)
|
|
1102
|
+
self.change_at_date(update_date, changed_weights=changed_weights, **kwargs)
|
|
1104
1103
|
|
|
1105
1104
|
@classmethod
|
|
1106
1105
|
def _get_or_create_portfolio(cls, instrument_handler, portfolio_data):
|
|
@@ -474,11 +474,11 @@ class TestOrderProposal:
|
|
|
474
474
|
instrument.portfolios.add(portfolio)
|
|
475
475
|
tp = order_proposal_factory.create(portfolio=portfolio)
|
|
476
476
|
instrument.refresh_from_db()
|
|
477
|
-
assert instrument.inception_date ==
|
|
477
|
+
assert instrument.inception_date == tp.trade_date
|
|
478
478
|
|
|
479
|
-
tp2 = order_proposal_factory.create(portfolio=portfolio, trade_date=tp.trade_date - BDay(1))
|
|
479
|
+
tp2 = order_proposal_factory.create(portfolio=portfolio, trade_date=(tp.trade_date - BDay(1)).date())
|
|
480
480
|
instrument.refresh_from_db()
|
|
481
|
-
assert instrument.inception_date ==
|
|
481
|
+
assert instrument.inception_date == tp2.trade_date
|
|
482
482
|
|
|
483
483
|
def test_get_round_lot_size(self, order_proposal, instrument):
|
|
484
484
|
# without a round lot size, we expect no normalization of shares
|
wbportfolio/viewsets/assets.py
CHANGED
|
@@ -484,9 +484,24 @@ class AssetPositionUnderlyingInstrumentChartViewSet(UserPortfolioRequestPermissi
|
|
|
484
484
|
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
485
485
|
fig = get_default_timeserie_figure(fig)
|
|
486
486
|
if queryset.exists():
|
|
487
|
-
|
|
487
|
+
df_weight = pd.DataFrame(queryset.values("date", "weighting", "portfolio__name"))
|
|
488
|
+
df_weight = df_weight.where(pd.notnull(df_weight), 0)
|
|
489
|
+
df_weight = df_weight.groupby(["date", "portfolio__name"]).sum().reset_index()
|
|
490
|
+
min_date = df_weight["date"].min()
|
|
491
|
+
max_date = df_weight["date"].max()
|
|
492
|
+
|
|
493
|
+
df_price = (
|
|
494
|
+
pd.DataFrame(
|
|
495
|
+
self.instrument.prices.filter_only_valid_prices()
|
|
496
|
+
.annotate_base_data()
|
|
497
|
+
.filter(date__gte=min_date, date__lte=max_date)
|
|
498
|
+
.values_list("date", "net_value_usd"),
|
|
499
|
+
columns=["date", "price_fx_usd"],
|
|
500
|
+
)
|
|
501
|
+
.set_index("date")
|
|
502
|
+
.sort_index()
|
|
503
|
+
)
|
|
488
504
|
|
|
489
|
-
df_price = df_price.where(pd.notnull(df_price), 0)
|
|
490
505
|
fig.add_trace(
|
|
491
506
|
go.Scatter(
|
|
492
507
|
x=df_price.index, y=df_price.price_fx_usd, mode="lines", marker_color="green", name="Price"
|
|
@@ -91,7 +91,6 @@ class OrderOrderProposalModelViewSet(
|
|
|
91
91
|
sum_target_total_value_fx_portfolio=Sum(F("target_total_value_fx_portfolio")),
|
|
92
92
|
sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
|
|
93
93
|
)
|
|
94
|
-
|
|
95
94
|
# weights aggregates
|
|
96
95
|
cash_sum_effective_weight = Decimal("1.0") - noncash_aggregates["sum_effective_weight"]
|
|
97
96
|
cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
|
|
@@ -119,9 +119,10 @@ wbportfolio/import_export/handlers/adjustment.py,sha256=6bdTIYFmc8_HFxcdwtnYwglM
|
|
|
119
119
|
wbportfolio/import_export/handlers/asset_position.py,sha256=UZBDlEK5mxtGu9cZvwiQS6J8GNkMJqDZb7KZ3uwex0g,8698
|
|
120
120
|
wbportfolio/import_export/handlers/dividend.py,sha256=F0oLfNt2B_QQAjHBCRpxa5HSkfkAYdal_NjLJGtVckY,4408
|
|
121
121
|
wbportfolio/import_export/handlers/fees.py,sha256=BOFHAvSTlvVLaxnm6KD_fcza1TlPc02HOR9J0_jjswI,2495
|
|
122
|
+
wbportfolio/import_export/handlers/orders.py,sha256=5EWm2ocqzc2PBzZRawGIMybkHH7ySjQyHDuGsGBqjIQ,3132
|
|
122
123
|
wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
|
|
123
124
|
wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
|
|
124
|
-
wbportfolio/import_export/handlers/trade.py,sha256=
|
|
125
|
+
wbportfolio/import_export/handlers/trade.py,sha256=g2jAYYeuhZv_DuvM6zlROcq6rUlSbGaQ3tO4u6wkSRU,11140
|
|
125
126
|
wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
126
127
|
wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
|
|
127
128
|
wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -268,7 +269,7 @@ wbportfolio/models/asset.py,sha256=b0vPt4LwNrxcMiK7UmBKViYnbNNlZzPTagvU5vFuyrc,4
|
|
|
268
269
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
269
270
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
270
271
|
wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
|
|
271
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
272
|
+
wbportfolio/models/portfolio.py,sha256=b0YL4dytrEeHRt7VLn1uJYb_47vRKFYiq9Y_6JLrIJU,58419
|
|
272
273
|
wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
|
|
273
274
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
274
275
|
wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
|
|
@@ -288,8 +289,8 @@ wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
288
289
|
wbportfolio/models/mixins/instruments.py,sha256=SgBreTpa_X3uyCWo7t8B0VaTtl49IjmBMe4Pab6TjAM,6796
|
|
289
290
|
wbportfolio/models/mixins/liquidity_stress_test.py,sha256=iQVzT3QM7VtHnqfj9gT6KUIe4wC4MJXery-AXJHUYns,58820
|
|
290
291
|
wbportfolio/models/orders/__init__.py,sha256=EH9UacGR3npBMje5FGTeLOh1xqFBh9kc24WbGmBIA3g,69
|
|
291
|
-
wbportfolio/models/orders/order_proposals.py,sha256=
|
|
292
|
-
wbportfolio/models/orders/orders.py,sha256=
|
|
292
|
+
wbportfolio/models/orders/order_proposals.py,sha256=uAOPhCUPcCBkAwGPCS0b9JEpwLfnGdJfXM6z8btfzvg,39882
|
|
293
|
+
wbportfolio/models/orders/orders.py,sha256=5XKU5NNLlrB0ydQtVGm_woDagt_Qo7IMZhc7GcrQ_lI,9481
|
|
293
294
|
wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
|
|
294
295
|
wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
|
|
295
296
|
wbportfolio/models/reconciliations/account_reconciliations.py,sha256=rofSxetFfEJov6mPyoTvGxELA16HILyJZtQvm_kwYU0,4405
|
|
@@ -403,7 +404,7 @@ wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo
|
|
|
403
404
|
wbportfolio/tests/models/test_splits.py,sha256=ytKcHsI_90kj1L4s8It-KEcc24rkDcElxwQ8q0QxEvk,9689
|
|
404
405
|
wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
|
|
405
406
|
wbportfolio/tests/models/orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
406
|
-
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=
|
|
407
|
+
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=FJCP2Gj1eRQb1X7pqiWxGJk4a1qOdVTzE-aRL71fOYk,30773
|
|
407
408
|
wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
408
409
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
409
410
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
|
|
@@ -423,7 +424,7 @@ wbportfolio/tests/viewsets/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5J
|
|
|
423
424
|
wbportfolio/tests/viewsets/transactions/test_claims.py,sha256=QEZfMAW07dyoZ63t2umSwGOqvaTULfYfbN_F4ZoSAcw,6368
|
|
424
425
|
wbportfolio/viewsets/__init__.py,sha256=1U1r0TNTUBsDyrZ8k9GbdG0SF0_O1RyORN9AaHJT71o,2436
|
|
425
426
|
wbportfolio/viewsets/adjustments.py,sha256=ugbX4aFRCaD4Yj1hxL-VIPaNI7GF_wt0FrkN6mq1YjU,1524
|
|
426
|
-
wbportfolio/viewsets/assets.py,sha256=
|
|
427
|
+
wbportfolio/viewsets/assets.py,sha256=Pk565r7FOuJw7YsGg9L5SAWJqlg1lQpcjWQjnMYeH4Q,25259
|
|
427
428
|
wbportfolio/viewsets/assets_and_net_new_money_progression.py,sha256=Jl4vEQP4N2OFL5IGBXoKcj-0qaPviU0I8npvQLw4Io0,4464
|
|
428
429
|
wbportfolio/viewsets/custodians.py,sha256=CTFqkqVP1R3AV7lhdvcdICxB5DfwDYCyikNSI5kbYEo,2322
|
|
429
430
|
wbportfolio/viewsets/esg.py,sha256=27MxxdXQH3Cq_1UEYmcrF7htUOg6i81fUpbVQXAAKJI,6985
|
|
@@ -532,7 +533,7 @@ wbportfolio/viewsets/configs/titles/roles.py,sha256=9LoJa3jgenXJ5UWRlIErTzdbjpSW
|
|
|
532
533
|
wbportfolio/viewsets/configs/titles/trades.py,sha256=29XCLxvY0Xe3a2tjCno3tN2rRXCr9RWpbWnzurJfnYI,1986
|
|
533
534
|
wbportfolio/viewsets/orders/__init__.py,sha256=N8v9jdEXryOzrLlc7ML3iBCO2lmNXph9_TWoQ7PTvi4,195
|
|
534
535
|
wbportfolio/viewsets/orders/order_proposals.py,sha256=mw385zzU52nCq8p6xgionh43xLmOn5aX-2BPlCHnqlE,6214
|
|
535
|
-
wbportfolio/viewsets/orders/orders.py,sha256=
|
|
536
|
+
wbportfolio/viewsets/orders/orders.py,sha256=uqEE22CZkcatSWtZgoqskK_7tV8m_-rTfH75_jFb5Lc,10874
|
|
536
537
|
wbportfolio/viewsets/orders/configs/__init__.py,sha256=5MU57JXiKi32_PicHtiNr7YHmMN020FrlF5NFJf_Wds,94
|
|
537
538
|
wbportfolio/viewsets/orders/configs/buttons/__init__.py,sha256=EHzNmAfa0UQFITEF-wxj_s4wn3Y5DE3DCbEUmmvCTIs,106
|
|
538
539
|
wbportfolio/viewsets/orders/configs/buttons/order_proposals.py,sha256=Q_7LrsuLzjSXCIoscFQXMMHl8cuCporDNM5k735W7d8,3584
|
|
@@ -550,7 +551,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
|
|
|
550
551
|
wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
|
|
551
552
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
552
553
|
wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
|
|
553
|
-
wbportfolio-1.54.
|
|
554
|
-
wbportfolio-1.54.
|
|
555
|
-
wbportfolio-1.54.
|
|
556
|
-
wbportfolio-1.54.
|
|
554
|
+
wbportfolio-1.54.19.dist-info/METADATA,sha256=M3wWcN_z7d5N3OaGBKLieTLkg2bJ_cL3KdXsBeFjPKk,703
|
|
555
|
+
wbportfolio-1.54.19.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
556
|
+
wbportfolio-1.54.19.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
557
|
+
wbportfolio-1.54.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|