wbportfolio 1.56.4__py2.py3-none-any.whl → 1.56.5__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/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/serializers.py +2 -4
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/models/orders/order_proposals.py +33 -11
- wbportfolio/models/orders/orders.py +34 -18
- wbportfolio/pms/trading/handler.py +4 -1
- wbportfolio/serializers/orders/orders.py +1 -1
- wbportfolio/tests/models/orders/test_order_proposals.py +8 -5
- wbportfolio/viewsets/orders/orders.py +3 -3
- {wbportfolio-1.56.4.dist-info → wbportfolio-1.56.5.dist-info}/METADATA +1 -1
- {wbportfolio-1.56.4.dist-info → wbportfolio-1.56.5.dist-info}/RECORD +13 -13
- {wbportfolio-1.56.4.dist-info → wbportfolio-1.56.5.dist-info}/WHEEL +0 -0
- {wbportfolio-1.56.4.dist-info → wbportfolio-1.56.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,7 @@ from datetime import date
|
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
|
+
from django.core.cache import cache
|
|
6
7
|
from django.db import models
|
|
7
8
|
from django.db.models.signals import post_save
|
|
8
9
|
from django.dispatch import receiver
|
|
@@ -16,7 +17,12 @@ from wbportfolio.models import Claim, Product
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def get_total_assets_under_management(val_date: date) -> Decimal:
|
|
19
|
-
|
|
20
|
+
cache_key = f"total_assets_under_management:{val_date.isoformat()}"
|
|
21
|
+
return cache.get_or_set(
|
|
22
|
+
cache_key,
|
|
23
|
+
lambda: sum([product.get_total_aum_usd(val_date) for product in Product.active_objects.all()]),
|
|
24
|
+
60 * 60 * 24,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
def get_lost_client_customer_status():
|
|
@@ -44,9 +50,8 @@ class Updater:
|
|
|
44
50
|
self.val_date = val_date
|
|
45
51
|
self.total_assets_under_management = get_total_assets_under_management(val_date)
|
|
46
52
|
|
|
47
|
-
def update_company_data(self,
|
|
53
|
+
def update_company_data(self, company_portfolio_data) -> tuple[str, str]:
|
|
48
54
|
# save company portfolio data
|
|
49
|
-
company_portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
|
|
50
55
|
if (
|
|
51
56
|
invested_assets_under_management_usd := company_portfolio_data.get_assets_under_management_usd(
|
|
52
57
|
self.val_date
|
|
@@ -55,19 +60,11 @@ class Updater:
|
|
|
55
60
|
company_portfolio_data.invested_assets_under_management_usd = invested_assets_under_management_usd
|
|
56
61
|
if (potential := company_portfolio_data.get_potential(self.val_date)) is not None:
|
|
57
62
|
company_portfolio_data.potential = potential
|
|
58
|
-
company_portfolio_data.save()
|
|
59
63
|
|
|
60
64
|
# update the company object itself
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
company.save()
|
|
65
|
-
|
|
66
|
-
# def update_all_companies(self, val_date: date):
|
|
67
|
-
# for company in tqdm(qs, total=qs.count()):
|
|
68
|
-
# with suppress(CompanyPortfolioData.DoesNotExist):
|
|
69
|
-
# company_portfolio = CompanyPortfolioData.objects.get(company=company)
|
|
70
|
-
# company_portfolio.update_data(date.today())
|
|
65
|
+
tier = company_portfolio_data.get_tiering(self.total_assets_under_management)
|
|
66
|
+
customer_status = company_portfolio_data.get_customer_status()
|
|
67
|
+
return customer_status, tier
|
|
71
68
|
|
|
72
69
|
|
|
73
70
|
class CompanyPortfolioData(models.Model):
|
|
@@ -104,14 +101,6 @@ class CompanyPortfolioData(models.Model):
|
|
|
104
101
|
verbose_name="AUM",
|
|
105
102
|
help_text="The Assets under Management (AUM) that is managed by this company or this person's primary employer.",
|
|
106
103
|
)
|
|
107
|
-
invested_assets_under_management_usd = models.DecimalField(
|
|
108
|
-
max_digits=17,
|
|
109
|
-
decimal_places=2,
|
|
110
|
-
null=True,
|
|
111
|
-
blank=True,
|
|
112
|
-
help_text="The invested Assets under Management (AUM).",
|
|
113
|
-
verbose_name="Invested AUM ($)",
|
|
114
|
-
)
|
|
115
104
|
|
|
116
105
|
investment_discretion = models.CharField(
|
|
117
106
|
max_length=21,
|
|
@@ -120,10 +109,6 @@ class CompanyPortfolioData(models.Model):
|
|
|
120
109
|
help_text="What discretion this company or this person's primary employer has to invest its assets.",
|
|
121
110
|
verbose_name="Investment Discretion",
|
|
122
111
|
)
|
|
123
|
-
|
|
124
|
-
potential = models.DecimalField(
|
|
125
|
-
decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
|
|
126
|
-
)
|
|
127
112
|
potential_currency = models.ForeignKey(
|
|
128
113
|
to="currency.Currency",
|
|
129
114
|
related_name="wbportfolio_potential_currencies",
|
|
@@ -132,6 +117,25 @@ class CompanyPortfolioData(models.Model):
|
|
|
132
117
|
on_delete=models.PROTECT,
|
|
133
118
|
)
|
|
134
119
|
|
|
120
|
+
# Dynamic fields
|
|
121
|
+
invested_assets_under_management_usd = models.DecimalField(
|
|
122
|
+
max_digits=17,
|
|
123
|
+
decimal_places=2,
|
|
124
|
+
null=True,
|
|
125
|
+
blank=True,
|
|
126
|
+
help_text="The invested Assets under Management (AUM).",
|
|
127
|
+
verbose_name="Invested AUM ($)",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
potential = models.DecimalField(
|
|
131
|
+
decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def update(self):
|
|
135
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
136
|
+
val_date = CurrencyFXRates.objects.latest("date").date
|
|
137
|
+
self.company.customer_status, self.company.tier = Updater(val_date).update_company_data(self)
|
|
138
|
+
|
|
135
139
|
def get_assets_under_management_usd(self, val_date: date) -> Decimal:
|
|
136
140
|
return Claim.objects.filter(status=Claim.Status.APPROVED).filter_for_customer(
|
|
137
141
|
self.company
|
|
@@ -140,16 +144,17 @@ class CompanyPortfolioData(models.Model):
|
|
|
140
144
|
)["invested_aum_usd"] or Decimal(0)
|
|
141
145
|
|
|
142
146
|
def _get_default_potential(self, val_date: date) -> Decimal:
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
if self.assets_under_management:
|
|
148
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
149
|
+
fx = CurrencyFXRates.objects.get(currency=self.assets_under_management_currency, date=val_date).value
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
aum_usd = self.assets_under_management / fx
|
|
152
|
+
aum_potential = Decimal(0)
|
|
153
|
+
for asset_allocation in self.company.asset_allocations.all():
|
|
154
|
+
aum_potential += aum_usd * asset_allocation.percent * asset_allocation.max_investment
|
|
155
|
+
invested_aum = self.invested_assets_under_management_usd or Decimal(0.0)
|
|
151
156
|
|
|
152
|
-
|
|
157
|
+
return aum_potential - invested_aum
|
|
153
158
|
|
|
154
159
|
def get_potential(self, val_date: date) -> Decimal:
|
|
155
160
|
if module_path := getattr(settings, "PORTFOLIO_COMPANY_DATA_POTENTIAL_METHOD", None):
|
|
@@ -225,11 +230,6 @@ class CompanyPortfolioData(models.Model):
|
|
|
225
230
|
verbose_name_plural = "Company Portfolio Data"
|
|
226
231
|
|
|
227
232
|
|
|
228
|
-
@receiver(post_save, sender="directory.Company")
|
|
229
|
-
def create_company_portfolio_data(sender, instance, created, **kwargs):
|
|
230
|
-
CompanyPortfolioData.objects.get_or_create(company=instance)
|
|
231
|
-
|
|
232
|
-
|
|
233
233
|
class AssetAllocationType(WBModel):
|
|
234
234
|
name = models.CharField(max_length=255)
|
|
235
235
|
default_max_investment = models.DecimalField(
|
|
@@ -329,3 +329,33 @@ class GeographicFocus(models.Model):
|
|
|
329
329
|
@classmethod
|
|
330
330
|
def get_representation_label_key(cls):
|
|
331
331
|
return "{{company}}: {{percent}} {{country}}"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@receiver(post_save, sender=AssetAllocation)
|
|
335
|
+
@receiver(post_save, sender=GeographicFocus)
|
|
336
|
+
def post_save_company_data(sender, instance, created, **kwargs):
|
|
337
|
+
company = instance.company
|
|
338
|
+
portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=company)
|
|
339
|
+
if not created:
|
|
340
|
+
portfolio_data.update()
|
|
341
|
+
portfolio_data.save()
|
|
342
|
+
portfolio_data.company.save()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@receiver(post_save, sender="directory.Company")
|
|
346
|
+
def handle_company_portfolio_data(sender, instance, created, **kwargs):
|
|
347
|
+
# create default asset allocation type (equity 50/50)
|
|
348
|
+
if not instance.asset_allocations.exists():
|
|
349
|
+
equity_asset_type = AssetAllocationType.objects.get_or_create(name="Equity")[0]
|
|
350
|
+
AssetAllocation.objects.create(
|
|
351
|
+
company=instance,
|
|
352
|
+
asset_type=equity_asset_type,
|
|
353
|
+
percent=0.5,
|
|
354
|
+
max_investment=0.5,
|
|
355
|
+
)
|
|
356
|
+
if created:
|
|
357
|
+
portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=instance)
|
|
358
|
+
if not created:
|
|
359
|
+
portfolio_data.update()
|
|
360
|
+
portfolio_data.save()
|
|
361
|
+
portfolio_data.company.save()
|
|
@@ -91,7 +91,7 @@ def update_portfolio_data(company_portfolio_data, portfolio_data):
|
|
|
91
91
|
|
|
92
92
|
if investment_discretion := portfolio_data["investment_discretion"]:
|
|
93
93
|
company_portfolio_data.investment_discretion = investment_discretion
|
|
94
|
-
|
|
94
|
+
company_portfolio_data.update()
|
|
95
95
|
company_portfolio_data.save()
|
|
96
96
|
|
|
97
97
|
|
|
@@ -243,11 +243,9 @@ class PersonModelListSerializer(CompanyPortfolioDataMixin, BasePersonModelListSe
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
class AssetAllocationTypeRepresentationSerializer(serializers.RepresentationSerializer):
|
|
246
|
-
_detail = serializers.HyperlinkField(reverse_name="company_portfolio:assetallocationtyperepresentation-detail")
|
|
247
|
-
|
|
248
246
|
class Meta:
|
|
249
247
|
model = AssetAllocationType
|
|
250
|
-
fields = ("id", "name"
|
|
248
|
+
fields = ("id", "name")
|
|
251
249
|
|
|
252
250
|
|
|
253
251
|
class AssetAllocationTypeModelSerializer(serializers.ModelSerializer):
|
|
@@ -19,5 +19,16 @@ def update_all_portfolio_data(val_date: date | None = None):
|
|
|
19
19
|
has_account=Exists(Account.objects.filter(owner=OuterRef("pk"))),
|
|
20
20
|
has_portfolio_data=Exists(CompanyPortfolioData.objects.filter(company=OuterRef("pk"))),
|
|
21
21
|
)
|
|
22
|
+
company_objs = []
|
|
23
|
+
portfolio_data_objs = []
|
|
22
24
|
for company in tqdm(qs, total=qs.count()):
|
|
23
|
-
|
|
25
|
+
portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
|
|
26
|
+
company.customer_status, company.tier = updater.update_company_data(portfolio_data)
|
|
27
|
+
portfolio_data_objs.append(portfolio_data)
|
|
28
|
+
company_objs.append(company)
|
|
29
|
+
if company_objs:
|
|
30
|
+
Company.objects.bulk_update(company_objs, ["customer_status", "tier"])
|
|
31
|
+
if portfolio_data_objs:
|
|
32
|
+
CompanyPortfolioData.objects.bulk_update(
|
|
33
|
+
portfolio_data_objs, ["invested_assets_under_management_usd", "potential"]
|
|
34
|
+
)
|
|
@@ -172,7 +172,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
172
172
|
|
|
173
173
|
@cached_property
|
|
174
174
|
def portfolio_total_asset_value(self) -> Decimal:
|
|
175
|
-
return self.
|
|
175
|
+
return self.get_portfolio_total_asset_value()
|
|
176
176
|
|
|
177
177
|
@cached_property
|
|
178
178
|
def validated_trading_service(self) -> TradingService:
|
|
@@ -248,6 +248,25 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
248
248
|
# TODO make this dynamically configurable
|
|
249
249
|
return "MARKET_ON_CLOSE"
|
|
250
250
|
|
|
251
|
+
def get_portfolio_total_asset_value(self):
|
|
252
|
+
return self.portfolio.get_total_asset_value(self.last_effective_date)
|
|
253
|
+
# return self.orders.annotate(
|
|
254
|
+
# effective_shares=Coalesce(
|
|
255
|
+
# Subquery(
|
|
256
|
+
# AssetPosition.objects.filter(
|
|
257
|
+
# underlying_quote=OuterRef("underlying_instrument"),
|
|
258
|
+
# date=self.last_effective_date,
|
|
259
|
+
# portfolio=self.portfolio,
|
|
260
|
+
# )
|
|
261
|
+
# .values("portfolio")
|
|
262
|
+
# .annotate(s=Sum("shares"))
|
|
263
|
+
# .values("s")[:1]
|
|
264
|
+
# ),
|
|
265
|
+
# Decimal(0),
|
|
266
|
+
# ),
|
|
267
|
+
# effective_total_value_fx_portfolio=F("effective_shares") * F("currency_fx_rate") * F("price"),
|
|
268
|
+
# ).aggregate(s=Sum("effective_total_value_fx_portfolio"))["s"] or Decimal(0.0)
|
|
269
|
+
|
|
251
270
|
def get_orders(self):
|
|
252
271
|
# TODO Issue here: the cash is subqueried on the portfolio, on portfolio such as the fund, there is multiple cash component, that we exclude in the orders (and use a unique cash position instead)
|
|
253
272
|
# so the subquery returns the previous position (probably USD), but is missing the other cash aggregation. We need to find a way to handle that properly
|
|
@@ -428,7 +447,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
428
447
|
|
|
429
448
|
# 3. Prepare a mapping from instrument IDs to weights for analytic calculations.
|
|
430
449
|
previous_weights = {instrument.id: float(info["weighting"]) for instrument, info in portfolio.items()}
|
|
431
|
-
|
|
432
450
|
# 4. Attempt to fetch analytic returns and portfolio contribution. Default on error.
|
|
433
451
|
try:
|
|
434
452
|
last_returns, contribution = self.portfolio.get_analytic_portfolio(
|
|
@@ -437,7 +455,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
437
455
|
last_returns = last_returns.to_dict()
|
|
438
456
|
except ValueError:
|
|
439
457
|
last_returns, contribution = {}, 1
|
|
440
|
-
|
|
441
458
|
positions = []
|
|
442
459
|
total_weighting = Decimal("0")
|
|
443
460
|
|
|
@@ -609,12 +626,16 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
609
626
|
target_portfolio = self._get_default_target_portfolio(use_desired_target_weight=use_desired_target_weight)
|
|
610
627
|
if not effective_portfolio:
|
|
611
628
|
effective_portfolio = self._get_default_effective_portfolio()
|
|
629
|
+
if use_desired_target_weight:
|
|
630
|
+
total_target_weight = target_portfolio.total_weight
|
|
631
|
+
else:
|
|
632
|
+
total_target_weight = self.total_expected_target_weight
|
|
612
633
|
if target_portfolio:
|
|
613
634
|
service = TradingService(
|
|
614
635
|
self.trade_date,
|
|
615
636
|
effective_portfolio=effective_portfolio,
|
|
616
637
|
target_portfolio=target_portfolio,
|
|
617
|
-
total_target_weight=
|
|
638
|
+
total_target_weight=total_target_weight,
|
|
618
639
|
)
|
|
619
640
|
if validate_order:
|
|
620
641
|
service.is_valid()
|
|
@@ -640,16 +661,17 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
640
661
|
weighting=weighting,
|
|
641
662
|
daily_return=daily_return,
|
|
642
663
|
)
|
|
643
|
-
order.pre_save()
|
|
644
|
-
order.set_weighting(weighting, portfolio_value)
|
|
645
|
-
order.desired_target_weight = order_dto.target_weight
|
|
646
664
|
order.order_type = Order.get_type(
|
|
647
665
|
weighting, round(order_dto.previous_weight, 8), round(order_dto.target_weight, 8)
|
|
648
666
|
)
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
667
|
+
order.pre_save()
|
|
668
|
+
order.set_weighting(weighting, portfolio_value)
|
|
669
|
+
order.desired_target_weight = order_dto.target_weight
|
|
670
|
+
|
|
671
|
+
# if we cannot automatically find a price, we consider the stock is invalid and we sell it
|
|
672
|
+
if not order.price and order.weighting > 0:
|
|
673
|
+
order.price = Decimal("0.0")
|
|
674
|
+
order.weighting = -order_dto.effective_weight
|
|
653
675
|
objs.append(order)
|
|
654
676
|
Order.objects.bulk_create(
|
|
655
677
|
objs,
|
|
@@ -10,6 +10,7 @@ from django.db.models import (
|
|
|
10
10
|
from ordered_model.models import OrderedModel
|
|
11
11
|
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
12
12
|
from wbcore.contrib.io.mixins import ImportMixin
|
|
13
|
+
from wbfdm.models import Instrument
|
|
13
14
|
|
|
14
15
|
from wbportfolio.import_export.handlers.orders import OrderImportHandler
|
|
15
16
|
from wbportfolio.models.asset import AssetPosition
|
|
@@ -164,27 +165,28 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
164
165
|
self.set_currency_fx_rate()
|
|
165
166
|
|
|
166
167
|
if not self.price:
|
|
167
|
-
|
|
168
|
-
if self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent:
|
|
169
|
-
self.price = Decimal("1")
|
|
170
|
-
else:
|
|
171
|
-
self.price = self.get_price()
|
|
168
|
+
self.set_price()
|
|
172
169
|
if not self.portfolio.only_weighting and not self.shares:
|
|
173
170
|
estimated_shares = self.order_proposal.get_estimated_shares(
|
|
174
171
|
self.weighting, self.underlying_instrument, self.price
|
|
175
172
|
)
|
|
176
173
|
if estimated_shares:
|
|
177
174
|
self.shares = estimated_shares
|
|
175
|
+
if effective_shares := self.get_effective_shares():
|
|
176
|
+
if self.order_type == self.Type.SELL:
|
|
177
|
+
self.shares = -effective_shares
|
|
178
|
+
else:
|
|
179
|
+
self.shares = max(self.shares, -effective_shares)
|
|
178
180
|
super().pre_save()
|
|
179
181
|
|
|
180
182
|
def save(self, *args, **kwargs):
|
|
183
|
+
if self.id:
|
|
184
|
+
self.set_type()
|
|
181
185
|
self.pre_save()
|
|
182
186
|
if not self.underlying_instrument.is_investable_universe:
|
|
183
187
|
self.underlying_instrument.is_investable_universe = True
|
|
184
188
|
self.underlying_instrument.save()
|
|
185
189
|
|
|
186
|
-
if self.id:
|
|
187
|
-
self.set_type()
|
|
188
190
|
super().save(*args, **kwargs)
|
|
189
191
|
|
|
190
192
|
@classmethod
|
|
@@ -203,21 +205,38 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
203
205
|
else:
|
|
204
206
|
return Order.Type.SELL
|
|
205
207
|
|
|
206
|
-
def get_price(self) -> Decimal:
|
|
207
|
-
try:
|
|
208
|
-
return self.underlying_instrument.get_price(self.value_date)
|
|
209
|
-
except ValueError:
|
|
210
|
-
return Decimal("0")
|
|
211
|
-
|
|
212
208
|
def get_effective_shares(self) -> Decimal:
|
|
213
209
|
return AssetPosition.objects.filter(
|
|
214
210
|
underlying_quote=self.underlying_instrument,
|
|
215
|
-
date=self.
|
|
211
|
+
date=self.order_proposal.last_effective_date,
|
|
216
212
|
portfolio=self.portfolio,
|
|
217
213
|
).aggregate(s=Sum("shares"))["s"] or Decimal("0")
|
|
218
214
|
|
|
219
215
|
def set_type(self):
|
|
220
|
-
|
|
216
|
+
effective_weight = self._effective_weight
|
|
217
|
+
self.order_type = self.get_type(self.weighting, effective_weight, effective_weight + self.weighting)
|
|
218
|
+
|
|
219
|
+
def set_price(self):
|
|
220
|
+
daily_return = last_price = Decimal("0")
|
|
221
|
+
|
|
222
|
+
effective_date = self.order_proposal.last_effective_date
|
|
223
|
+
if self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent:
|
|
224
|
+
last_price = Decimal("1")
|
|
225
|
+
else:
|
|
226
|
+
prices, returns = Instrument.objects.filter(id=self.underlying_instrument.id).get_returns_df(
|
|
227
|
+
from_date=effective_date,
|
|
228
|
+
to_date=self.value_date,
|
|
229
|
+
to_currency=self.order_proposal.portfolio.currency,
|
|
230
|
+
use_dl=True,
|
|
231
|
+
)
|
|
232
|
+
with suppress(IndexError):
|
|
233
|
+
daily_return = Decimal(returns.iloc[-1, 0])
|
|
234
|
+
with suppress(KeyError):
|
|
235
|
+
last_price = Decimal(
|
|
236
|
+
prices.get(self.value_date, prices[effective_date])[self.underlying_instrument.id]
|
|
237
|
+
)
|
|
238
|
+
self.daily_return = daily_return
|
|
239
|
+
self.price = last_price
|
|
221
240
|
|
|
222
241
|
def set_currency_fx_rate(self):
|
|
223
242
|
self.currency_fx_rate = Decimal("1")
|
|
@@ -235,8 +254,6 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
235
254
|
self.shares = total_value / price_fx_portfolio
|
|
236
255
|
else:
|
|
237
256
|
self.shares = Decimal("0")
|
|
238
|
-
if effective_shares := self.get_effective_shares():
|
|
239
|
-
self.shares = max(self.shares, -effective_shares)
|
|
240
257
|
|
|
241
258
|
def set_shares(self, shares: Decimal, portfolio_value: Decimal):
|
|
242
259
|
if portfolio_value:
|
|
@@ -257,7 +274,6 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
257
274
|
|
|
258
275
|
def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
|
|
259
276
|
warnings = []
|
|
260
|
-
|
|
261
277
|
# 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
|
|
262
278
|
if self._target_weight:
|
|
263
279
|
if self.order_proposal and not self.portfolio.only_weighting:
|
|
@@ -145,17 +145,20 @@ class TradingService:
|
|
|
145
145
|
previous_weight = target_weight = 0
|
|
146
146
|
effective_shares = target_shares = 0
|
|
147
147
|
daily_return = 0
|
|
148
|
+
price = Decimal("0")
|
|
148
149
|
is_cash = False
|
|
149
150
|
if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
|
|
150
151
|
previous_weight = effective_pos.weighting
|
|
151
152
|
effective_shares = effective_pos.shares
|
|
152
153
|
daily_return = effective_pos.daily_return
|
|
153
154
|
is_cash = effective_pos.is_cash
|
|
155
|
+
price = effective_pos.price
|
|
154
156
|
if target_pos := target_portfolio.positions_map.get(instrument_id, None):
|
|
155
157
|
target_weight = target_pos.weighting
|
|
156
158
|
is_cash = target_pos.is_cash
|
|
157
159
|
if target_pos.shares is not None:
|
|
158
160
|
target_shares = target_pos.shares
|
|
161
|
+
price = target_pos.price
|
|
159
162
|
trade = Trade(
|
|
160
163
|
underlying_instrument=instrument_id,
|
|
161
164
|
previous_weight=previous_weight,
|
|
@@ -165,7 +168,7 @@ class TradingService:
|
|
|
165
168
|
date=self.trade_date,
|
|
166
169
|
instrument_type=pos.instrument_type,
|
|
167
170
|
currency=pos.currency,
|
|
168
|
-
price=Decimal(
|
|
171
|
+
price=Decimal(price) if price else Decimal("0"),
|
|
169
172
|
currency_fx_rate=Decimal(pos.currency_fx_rate),
|
|
170
173
|
daily_return=Decimal(daily_return),
|
|
171
174
|
portfolio_contribution=effective_portfolio.portfolio_contribution,
|
|
@@ -132,7 +132,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
132
132
|
|
|
133
133
|
class Meta:
|
|
134
134
|
model = Order
|
|
135
|
-
percent_fields = ["effective_weight", "target_weight", "weighting", "
|
|
135
|
+
percent_fields = ["effective_weight", "target_weight", "weighting", "desired_target_weight"]
|
|
136
136
|
decorators = {
|
|
137
137
|
"total_value_fx_portfolio": wb_serializers.decorator(
|
|
138
138
|
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
@@ -422,7 +422,7 @@ class TestOrderProposal:
|
|
|
422
422
|
mock_fct.assert_has_calls(expected_calls)
|
|
423
423
|
|
|
424
424
|
# Test estimating shares for a trade
|
|
425
|
-
@patch.object(
|
|
425
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
426
426
|
def test_get_estimated_shares(
|
|
427
427
|
self, mock_fct, order_proposal, order_factory, instrument_price_factory, instrument_factory
|
|
428
428
|
):
|
|
@@ -449,7 +449,7 @@ class TestOrderProposal:
|
|
|
449
449
|
== Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
|
|
450
450
|
)
|
|
451
451
|
|
|
452
|
-
@patch.object(
|
|
452
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
453
453
|
def test_get_estimated_target_cash(self, mock_fct, order_proposal, order_factory, cash_factory):
|
|
454
454
|
order_proposal.portfolio.only_weighting = False
|
|
455
455
|
order_proposal.portfolio.save()
|
|
@@ -500,7 +500,7 @@ class TestOrderProposal:
|
|
|
500
500
|
instrument.exchange.save()
|
|
501
501
|
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
502
502
|
|
|
503
|
-
@patch.object(
|
|
503
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
504
504
|
def test_submit_round_lot_size(self, mock_fct, order_proposal, instrument_price_factory, order_factory):
|
|
505
505
|
initial_shares = Decimal("70")
|
|
506
506
|
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
@@ -528,8 +528,10 @@ class TestOrderProposal:
|
|
|
528
528
|
trade.refresh_from_db()
|
|
529
529
|
assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
|
|
530
530
|
|
|
531
|
-
@patch.object(
|
|
532
|
-
def test_submit_round_fractional_shares(
|
|
531
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
532
|
+
def test_submit_round_fractional_shares(
|
|
533
|
+
self, mock_fct, instrument_price_factory, order_proposal, order_factory, asset_position_factory
|
|
534
|
+
):
|
|
533
535
|
initial_shares = Decimal("5.6")
|
|
534
536
|
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
535
537
|
net_value = round(price.net_value, 4)
|
|
@@ -538,6 +540,7 @@ class TestOrderProposal:
|
|
|
538
540
|
|
|
539
541
|
order_proposal.portfolio.only_weighting = False
|
|
540
542
|
order_proposal.portfolio.save()
|
|
543
|
+
|
|
541
544
|
trade = order_factory.create(
|
|
542
545
|
shares=Decimal("5.6"),
|
|
543
546
|
order_proposal=order_proposal,
|
|
@@ -92,10 +92,10 @@ class OrderOrderProposalModelViewSet(
|
|
|
92
92
|
sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
|
|
93
93
|
)
|
|
94
94
|
# weights aggregates
|
|
95
|
-
cash_sum_effective_weight = (
|
|
96
|
-
|
|
95
|
+
cash_sum_effective_weight = self.order_proposal.total_effective_portfolio_weight - (
|
|
96
|
+
noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
97
97
|
)
|
|
98
|
-
cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
|
|
98
|
+
cash_sum_target_cash_weight = Decimal("1.0") - (noncash_aggregates["sum_target_weight"] or Decimal(0))
|
|
99
99
|
noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
100
100
|
noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
|
|
101
101
|
sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
@@ -40,10 +40,10 @@ wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py,sha256=k69
|
|
|
40
40
|
wbportfolio/contrib/company_portfolio/factories.py,sha256=LvEYhBURUz3aK5KtTshBZpU66ph_DbLXKKDzf3Sw4Vo,1243
|
|
41
41
|
wbportfolio/contrib/company_portfolio/filters.py,sha256=jLDo3PRhofuqR2EA7G2R2aiAd_pTCZO4BvUZh0AN1HQ,4995
|
|
42
42
|
wbportfolio/contrib/company_portfolio/management.py,sha256=CofBifGGGKfA06uWRDWXY_9y7y-PGNRz3VhsVaDeAz8,857
|
|
43
|
-
wbportfolio/contrib/company_portfolio/models.py,sha256=
|
|
43
|
+
wbportfolio/contrib/company_portfolio/models.py,sha256=0YlWZfpBTUy4nCE0FYY6Kqcx3H28-AbeIVXgxcF6hM0,13963
|
|
44
44
|
wbportfolio/contrib/company_portfolio/scripts.py,sha256=wFfc1L-it0UTaBwNFs8hhGARbJoaNzmnlsXh6KEbslU,2497
|
|
45
|
-
wbportfolio/contrib/company_portfolio/serializers.py,sha256=
|
|
46
|
-
wbportfolio/contrib/company_portfolio/tasks.py,sha256=
|
|
45
|
+
wbportfolio/contrib/company_portfolio/serializers.py,sha256=i4lXu79Mkr_b4zOO4iQ73Tl6DucLK7Aj9npaG8ox7js,12067
|
|
46
|
+
wbportfolio/contrib/company_portfolio/tasks.py,sha256=nIBTqC36JCakCug7YVt1aBqai5bKDsgZd2HS7Jwpf5U,1384
|
|
47
47
|
wbportfolio/contrib/company_portfolio/urls.py,sha256=eyq6IpaDPG1oL8-AqPS5nC5BeFewKPA1BN_HI7M3i1c,1118
|
|
48
48
|
wbportfolio/contrib/company_portfolio/viewsets.py,sha256=o-_D_bEt283xngDbsMwdRQxFyuK7rfSChI8u5dGYyLM,8312
|
|
49
49
|
wbportfolio/contrib/company_portfolio/configs/__init__.py,sha256=AVH0v-uOJBPRKggz7fO7IpRyVJ1YlQiF-if_cMsOvvM,72
|
|
@@ -299,8 +299,8 @@ wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
299
299
|
wbportfolio/models/mixins/instruments.py,sha256=SuMPquQ93D4pZMK-4hQbJtV58_NOyf3wVOctQq7LNXQ,7054
|
|
300
300
|
wbportfolio/models/mixins/liquidity_stress_test.py,sha256=I_pgJ3QVFBtq0SeljJerhtlrZESRDNAe4On6QfMHvXc,58834
|
|
301
301
|
wbportfolio/models/orders/__init__.py,sha256=EH9UacGR3npBMje5FGTeLOh1xqFBh9kc24WbGmBIA3g,69
|
|
302
|
-
wbportfolio/models/orders/order_proposals.py,sha256=
|
|
303
|
-
wbportfolio/models/orders/orders.py,sha256=
|
|
302
|
+
wbportfolio/models/orders/order_proposals.py,sha256=bg0Xv5XKwcSa1cAcGkn4g-krYEBPrpjJ4D5Rh7W0dYU,60716
|
|
303
|
+
wbportfolio/models/orders/orders.py,sha256=zKSMUJV3OqSbqokF2EoxAAGVhP08jZQjayV9IfkNASQ,13188
|
|
304
304
|
wbportfolio/models/orders/routing.py,sha256=7nu7-3zmGsVA3tyymKK_ywY7V7RtKkGcXkyk2V8dXMw,2191
|
|
305
305
|
wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
|
|
306
306
|
wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
|
|
@@ -322,7 +322,7 @@ wbportfolio/pms/analytics/portfolio.py,sha256=u_S-e6HUQwAyq90gweDmxyTHWrIc5nd84s
|
|
|
322
322
|
wbportfolio/pms/analytics/utils.py,sha256=EfhKdo9B2ABaUPppb8DgZSqpNkSze8Rjej1xDjv-XcQ,282
|
|
323
323
|
wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
324
324
|
wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
|
|
325
|
-
wbportfolio/pms/trading/handler.py,sha256=
|
|
325
|
+
wbportfolio/pms/trading/handler.py,sha256=p56de1nm-QZh7tfQ7uITp1HuVE2XRaGj4T3oYBtp4Iw,8744
|
|
326
326
|
wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
327
327
|
wbportfolio/rebalancing/base.py,sha256=wpeoxdkLz5osxm5mRjkOoML7YkYvwuAlqSLLtHBbWp8,984
|
|
328
328
|
wbportfolio/rebalancing/decorators.py,sha256=162ZmXV2YQGI830LWvEnJ95RexMzHvaCGfcVOnXTOXM,502
|
|
@@ -376,7 +376,7 @@ wbportfolio/serializers/roles.py,sha256=T-9NqTldpvaEMFy-Bib5MB6MeboygEOqcMP61mzz
|
|
|
376
376
|
wbportfolio/serializers/signals.py,sha256=hD6R4oFtwhvnsJPteytPKy2JwEelmxrapdfoLSnluaE,7053
|
|
377
377
|
wbportfolio/serializers/orders/__init__.py,sha256=PKJRksA1pWsh8nVfGASoB0m3LyUzVRnq1m9VPp90J7k,271
|
|
378
378
|
wbportfolio/serializers/orders/order_proposals.py,sha256=Jxea2-Ze8Id5URv4UV-vTfCQGt11tjR27vRRfCs0gXU,4791
|
|
379
|
-
wbportfolio/serializers/orders/orders.py,sha256=
|
|
379
|
+
wbportfolio/serializers/orders/orders.py,sha256=7FOjeT-YAACzKjnJ2Ki2ads7wMq2Td8Zy7ot4TvJ8qs,10014
|
|
380
380
|
wbportfolio/serializers/transactions/__init__.py,sha256=-7Pan4n7YI3iDvGXff6okzk4ycEURRxp5n_SHCY_g_I,493
|
|
381
381
|
wbportfolio/serializers/transactions/claim.py,sha256=mEt67F2v8HC6roemDT3S0dD0cZIVl1U9sASbLW3Vpyo,11611
|
|
382
382
|
wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
|
|
@@ -420,7 +420,7 @@ wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo
|
|
|
420
420
|
wbportfolio/tests/models/test_splits.py,sha256=0PvW6WunBByDYRhtFV2LbK-DV92q-1s3kn45NvBL5Es,9500
|
|
421
421
|
wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
|
|
422
422
|
wbportfolio/tests/models/orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
423
|
-
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=
|
|
423
|
+
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=LjQCw10kA1C_21sI_Mm1pkvQLd7fEDHSwODcdt6HGpc,36605
|
|
424
424
|
wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
425
425
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
426
426
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
|
|
@@ -549,7 +549,7 @@ wbportfolio/viewsets/configs/titles/roles.py,sha256=9LoJa3jgenXJ5UWRlIErTzdbjpSW
|
|
|
549
549
|
wbportfolio/viewsets/configs/titles/trades.py,sha256=29XCLxvY0Xe3a2tjCno3tN2rRXCr9RWpbWnzurJfnYI,1986
|
|
550
550
|
wbportfolio/viewsets/orders/__init__.py,sha256=N8v9jdEXryOzrLlc7ML3iBCO2lmNXph9_TWoQ7PTvi4,195
|
|
551
551
|
wbportfolio/viewsets/orders/order_proposals.py,sha256=0z85RnnsJPzH_LVBLqLYY1xuFExkYMbc1gSyNjpvD-4,8110
|
|
552
|
-
wbportfolio/viewsets/orders/orders.py,sha256=
|
|
552
|
+
wbportfolio/viewsets/orders/orders.py,sha256=rZ0u1_FnmtJxUM4dkC3Y4BzS6KY1iZRyuNGXitKAies,11797
|
|
553
553
|
wbportfolio/viewsets/orders/configs/__init__.py,sha256=5MU57JXiKi32_PicHtiNr7YHmMN020FrlF5NFJf_Wds,94
|
|
554
554
|
wbportfolio/viewsets/orders/configs/buttons/__init__.py,sha256=EHzNmAfa0UQFITEF-wxj_s4wn3Y5DE3DCbEUmmvCTIs,106
|
|
555
555
|
wbportfolio/viewsets/orders/configs/buttons/order_proposals.py,sha256=1BPkIYv0-K2DDGa4Gua2_Pxsx7fNurTZ2tYNdL66On0,6495
|
|
@@ -567,7 +567,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
|
|
|
567
567
|
wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
|
|
568
568
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
569
569
|
wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
|
|
570
|
-
wbportfolio-1.56.
|
|
571
|
-
wbportfolio-1.56.
|
|
572
|
-
wbportfolio-1.56.
|
|
573
|
-
wbportfolio-1.56.
|
|
570
|
+
wbportfolio-1.56.5.dist-info/METADATA,sha256=RROv2cPMXS4c5q03yrXpQTM5ncGQRYdjH8PR-i62Hfw,751
|
|
571
|
+
wbportfolio-1.56.5.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
572
|
+
wbportfolio-1.56.5.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
573
|
+
wbportfolio-1.56.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|