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.

@@ -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
- return sum([product.get_total_aum_usd(val_date) for product in Product.active_objects.all()])
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, company):
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
- if (tier := company_portfolio_data.get_tiering(self.total_assets_under_management)) is not None:
62
- company.tier = tier
63
- company.customer_status = company_portfolio_data.get_customer_status()
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
- with suppress(CurrencyFXRates.DoesNotExist):
144
- fx = CurrencyFXRates.objects.get(currency=self.assets_under_management_currency, date=val_date).value
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
- aum_usd = self.assets_under_management / fx
147
- aum_potential = Decimal(0)
148
- for asset_allocation in self.company.asset_allocations.all():
149
- aum_potential += aum_usd * asset_allocation.percent * asset_allocation.max_investment
150
- invested_aum = self.invested_assets_under_management_usd or Decimal(0.0)
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
- return aum_potential - invested_aum
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", "_detail")
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
- updater.update_company_data(company)
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.portfolio.get_total_asset_value(self.last_effective_date)
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=self.total_expected_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
- # # if we cannot automatically find a price, we consider the stock is invalid and we sell it
650
- # if not order.price and order.weighting > 0:
651
- # order.price = Decimal("0.0")
652
- # order.weighting = -order_dto.effective_weight
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
- # we try to get the price if not provided directly from the underlying instrument
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.value_date,
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
- self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
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(pos.price) if pos.price is not None else Decimal("0"),
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", "daily_return", "desired_target_weight"]
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(Portfolio, "get_total_asset_value")
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(Portfolio, "get_total_asset_value")
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(Portfolio, "get_total_asset_value")
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(Portfolio, "get_total_asset_value")
532
- def test_submit_round_fractional_shares(self, mock_fct, instrument_price_factory, order_proposal, order_factory):
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
- self.order_proposal.total_effective_portfolio_weight - noncash_aggregates["sum_effective_weight"]
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.56.4
3
+ Version: 1.56.5
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -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=dfrIR6sI3He1TIHCjdAP3yIZ0cPvc97ZUX_ZzkkhzGg,12943
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=Y4Osxp0M7m_wy7DSbOt7ESfbob8KrfPBCnbzAuEsmmk,12160
46
- wbportfolio/contrib/company_portfolio/tasks.py,sha256=ZXezolvlj8_b5hvfcjH5IO5tXvwb8goNG8xPfk6YFNA,835
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=4t4IoZ8kqaDnQ1KrOoxinzbzabXnyMkTEV-lOt1G4Dc,59686
303
- wbportfolio/models/orders/orders.py,sha256=v23DQmX2a_v2EKbdXYEQXruOqzbrGOvx6wNlNlPUu2o,12446
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=sGwtwerozgiCVOTeQgJ0Q9g1zzQCPuydkf0aWn6NnGs,8646
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=WzuUDO907XG55NFMudVhJ5c2q7wrJbuquNzabGrD9yM,10030
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=Qv8lk2Fehtpgg79JyEZHRGrp02C4uayeRf4EuWeGNSI,36510
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=uNChI-EIenUZ5rlfKUQuM2VCQhDbCKPqdTWcszdDAxE,11767
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.4.dist-info/METADATA,sha256=HFEeWWRQErXdtvuxsVVPWmlUdvtmQPWGOrALtIaZ-lM,751
571
- wbportfolio-1.56.4.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
572
- wbportfolio-1.56.4.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
573
- wbportfolio-1.56.4.dist-info/RECORD,,
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,,