wbportfolio 1.54.15__py2.py3-none-any.whl → 1.54.16__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.

@@ -24,7 +24,6 @@ from django_fsm import FSMField, transition
24
24
  from pandas._libs.tslibs.offsets import BDay
25
25
  from wbcompliance.models.risk_management.mixins import RiskCheckMixin
26
26
  from wbcore.contrib.authentication.models import User
27
- from wbcore.contrib.currency.models import Currency
28
27
  from wbcore.contrib.icons import WBIcon
29
28
  from wbcore.contrib.notifications.dispatch import send_notification
30
29
  from wbcore.enums import RequestType
@@ -188,6 +187,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
188
187
  effective_weight=Round(
189
188
  F("contribution") / Value(portfolio_contribution), precision=Order.ORDER_WEIGHTING_PRECISION
190
189
  ),
190
+ tmp_effective_weight=F("contribution") / Value(portfolio_contribution),
191
191
  target_weight=Round(F("effective_weight") + F("weighting"), precision=Order.ORDER_WEIGHTING_PRECISION),
192
192
  effective_shares=Coalesce(
193
193
  Subquery(
@@ -206,7 +206,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
206
206
  )
207
207
  total_effective_weight = orders.aggregate(s=models.Sum("effective_weight"))["s"] or Decimal("1")
208
208
  with suppress(Order.DoesNotExist):
209
- largest_order = orders.latest("weighting")
209
+ largest_order = orders.latest("effective_weight")
210
210
  if quant_error := Decimal("1") - total_effective_weight:
211
211
  orders = orders.annotate(
212
212
  effective_weight=models.Case(
@@ -226,7 +226,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
226
226
  def __str__(self) -> str:
227
227
  return f"{self.portfolio.name}: {self.trade_date} ({self.status})"
228
228
 
229
- def convert_to_portfolio(self, use_effective: bool = False) -> PortfolioDTO:
229
+ def convert_to_portfolio(self, use_effective: bool = False, with_cash: bool = True) -> PortfolioDTO:
230
230
  """
231
231
  Data Transfer Object
232
232
  Returns:
@@ -258,6 +258,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
258
258
  except InvalidAnalyticPortfolio:
259
259
  last_returns, portfolio_contribution = {}, 1
260
260
  positions = []
261
+ total_weighting = Decimal("0")
261
262
  for instrument, row in portfolio.items():
262
263
  weighting = row["weighting"]
263
264
  daily_return = Decimal(last_returns.get(instrument.id, 0))
@@ -285,6 +286,10 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
285
286
  currency_fx_rate=row["currency_fx_rate"],
286
287
  )
287
288
  )
289
+ total_weighting += weighting
290
+ if with_cash and (cash_weight := Decimal("1") - total_weighting):
291
+ cash_position = self.get_estimated_target_cash(target_cash_weight=cash_weight)
292
+ positions.append(cash_position._build_dto())
288
293
  return PortfolioDTO(positions)
289
294
 
290
295
  # Start tools methods
@@ -325,7 +330,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
325
330
  service = TradingService(
326
331
  self.trade_date,
327
332
  effective_portfolio=self._get_default_effective_portfolio(),
328
- target_portfolio=self.convert_to_portfolio(),
333
+ target_portfolio=self.convert_to_portfolio(use_effective=False, with_cash=False),
329
334
  total_target_weight=total_target_weight,
330
335
  )
331
336
  leftovers_orders = self.orders.all()
@@ -352,7 +357,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
352
357
  return self.rebalancing_model.get_target_portfolio(
353
358
  self.portfolio, self.trade_date, self.value_date, **params
354
359
  )
355
- return self.convert_to_portfolio()
360
+ return self.convert_to_portfolio(use_effective=False)
356
361
 
357
362
  def _get_default_effective_portfolio(self):
358
363
  return self.convert_to_portfolio(use_effective=True)
@@ -455,8 +460,8 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
455
460
  last_order_proposal = self
456
461
  last_order_proposal_created = False
457
462
  while last_order_proposal and last_order_proposal.status == OrderProposal.Status.APPROVED:
463
+ logger.info(f"Replaying order proposal {last_order_proposal}")
458
464
  if not last_order_proposal_created:
459
- logger.info(f"Replaying order proposal {last_order_proposal}")
460
465
  last_order_proposal.approve_workflow(
461
466
  silent_exception=True,
462
467
  force_reset_order=True,
@@ -557,50 +562,41 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
557
562
  shares = math.floor(shares / round_lot_size) * round_lot_size
558
563
  return shares
559
564
 
560
- def get_estimated_target_cash(self, currency: Currency) -> AssetPosition:
565
+ def get_estimated_target_cash(self, target_cash_weight: Decimal | None = None) -> AssetPosition:
561
566
  """
562
567
  Estimates the target cash weight and shares for a order proposal.
563
568
 
564
569
  This method calculates the target cash weight by summing the weights of cash orders and adding any leftover weight from non-cash orders. It then estimates the target shares for this cash component if the portfolio is not only weighting-based.
565
570
 
566
571
  Args:
567
- currency (Currency): The currency for the target currency component
572
+ target_cash_weight (Decimal): the expected target cash weight (Optional). If not provided, we estimate from the existing orders
568
573
 
569
574
  Returns:
570
575
  tuple[Decimal, Decimal]: A tuple containing the target cash weight and the estimated target shares.
571
576
  """
572
577
  # Retrieve orders with base information
573
578
  orders = self.get_orders()
579
+ currency = self.portfolio.currency
574
580
 
575
- # Calculate the target cash weight from cash orders
576
- target_cash_weight = orders.filter(
577
- underlying_instrument__is_cash=True, underlying_instrument__currency=currency
578
- ).aggregate(s=models.Sum("target_weight"))["s"] or Decimal(0)
579
- # if the specified currency match the portfolio's currency, we include the weight leftover to this cash compoenent
580
- if currency == self.portfolio.currency:
581
- # Calculate the total target weight of all orders
582
- total_target_weight = orders.aggregate(s=models.Sum("target_weight"))["s"] or Decimal(0)
581
+ # Calculate the total target weight of all orders
582
+ total_target_weight = orders.aggregate(s=models.Sum("target_weight"))["s"] or Decimal(0)
583
583
 
584
- # Add any leftover weight as cash
585
- target_cash_weight += Decimal(1) - total_target_weight
584
+ if target_cash_weight is None:
585
+ target_cash_weight = Decimal("1") - total_target_weight
586
586
 
587
587
  # Initialize target shares to zero
588
588
  total_target_shares = Decimal(0)
589
589
 
590
+ # Get or create a cash component for the portfolio's currency
591
+ cash_component = Cash.objects.get_or_create(
592
+ currency=currency, defaults={"is_cash": True, "name": currency.title}
593
+ )[0]
590
594
  # If the portfolio is not only weighting-based, estimate the target shares for the cash component
591
595
  if not self.portfolio.only_weighting:
592
- # Get or create a cash component for the portfolio's currency
593
- cash_component = Cash.objects.get_or_create(
594
- currency=currency, defaults={"is_cash": True, "name": currency.title}
595
- )[0]
596
-
597
596
  # Estimate the target shares for the cash component
598
597
  with suppress(ValueError):
599
598
  total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component, Decimal("1.0"))
600
599
 
601
- cash_component = Cash.objects.get_or_create(
602
- currency=self.portfolio.currency, defaults={"name": self.portfolio.currency.title}
603
- )[0]
604
600
  # otherwise, we create a new position
605
601
  underlying_quote_price = InstrumentPrice.objects.get_or_create(
606
602
  instrument=cash_component,
@@ -657,7 +653,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
657
653
  Order.objects.bulk_update(orders, ["shares", "weighting"])
658
654
 
659
655
  # If we estimate cash on this order proposal, we make sure to create the corresponding cash component
660
- estimated_cash_position = self.get_estimated_target_cash(self.portfolio.currency)
656
+ estimated_cash_position = self.get_estimated_target_cash()
661
657
  target_portfolio = self.validated_trading_service.trades_batch.convert_to_portfolio(
662
658
  estimated_cash_position._build_dto()
663
659
  )
@@ -717,7 +713,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
717
713
  assets = []
718
714
  warnings = []
719
715
  # We do not want to create the estimated cash position if there is not orders in the order proposal (shouldn't be possible anyway)
720
- estimated_cash_position = self.get_estimated_target_cash(self.portfolio.currency)
716
+ estimated_cash_position = self.get_estimated_target_cash()
721
717
  for order in self.get_orders():
722
718
  with suppress(ValueError):
723
719
  asset = order.get_asset()
@@ -143,33 +143,32 @@ class TradingService:
143
143
 
144
144
  trades: list[Trade] = []
145
145
  for instrument_id, pos in instruments.items():
146
- if not pos.is_cash:
147
- previous_weight = target_weight = 0
148
- effective_shares = target_shares = 0
149
- daily_return = 0
150
- if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
151
- previous_weight = effective_pos.weighting
152
- effective_shares = effective_pos.shares
153
- daily_return = effective_pos.daily_return
154
- if target_pos := target_portfolio.positions_map.get(instrument_id, None):
155
- target_weight = target_pos.weighting
156
- if target_pos.shares is not None:
157
- target_shares = target_pos.shares
158
- trade = Trade(
159
- underlying_instrument=instrument_id,
160
- previous_weight=previous_weight,
161
- target_weight=target_weight,
162
- effective_shares=effective_shares,
163
- target_shares=target_shares,
164
- date=self.trade_date,
165
- instrument_type=pos.instrument_type,
166
- currency=pos.currency,
167
- price=Decimal(pos.price) if pos.price is not None else Decimal("0"),
168
- currency_fx_rate=Decimal(pos.currency_fx_rate),
169
- daily_return=Decimal(daily_return),
170
- portfolio_contribution=effective_portfolio.portfolio_contribution,
171
- )
172
- trades.append(trade)
146
+ previous_weight = target_weight = 0
147
+ effective_shares = target_shares = 0
148
+ daily_return = 0
149
+ if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
150
+ previous_weight = effective_pos.weighting
151
+ effective_shares = effective_pos.shares
152
+ daily_return = effective_pos.daily_return
153
+ if target_pos := target_portfolio.positions_map.get(instrument_id, None):
154
+ target_weight = target_pos.weighting
155
+ if target_pos.shares is not None:
156
+ target_shares = target_pos.shares
157
+ trade = Trade(
158
+ underlying_instrument=instrument_id,
159
+ previous_weight=previous_weight,
160
+ target_weight=target_weight,
161
+ effective_shares=effective_shares,
162
+ target_shares=target_shares,
163
+ date=self.trade_date,
164
+ instrument_type=pos.instrument_type,
165
+ currency=pos.currency,
166
+ price=Decimal(pos.price) if pos.price is not None else Decimal("0"),
167
+ currency_fx_rate=Decimal(pos.currency_fx_rate),
168
+ daily_return=Decimal(daily_return),
169
+ portfolio_contribution=effective_portfolio.portfolio_contribution,
170
+ )
171
+ trades.append(trade)
173
172
  return TradeBatch(trades)
174
173
 
175
174
  def is_valid(self, ignore_error: bool = False) -> bool:
@@ -23,7 +23,7 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
23
23
  rebalancing_model = wb_serializers.PrimaryKeyRelatedField(queryset=RebalancingModel.objects.all(), required=False)
24
24
  _rebalancing_model = RebalancingModelRepresentationSerializer(source="rebalancing_model")
25
25
  target_portfolio = wb_serializers.PrimaryKeyRelatedField(
26
- queryset=Portfolio.objects.all(), write_only=True, required=False, default=DefaultFromView("portfolio")
26
+ queryset=Portfolio.objects.all(), write_only=True, required=False
27
27
  )
28
28
  _target_portfolio = PortfolioRepresentationSerializer(source="target_portfolio")
29
29
  total_cash_weight = wb_serializers.DecimalField(
@@ -51,8 +51,13 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
51
51
  obj = super().create(validated_data)
52
52
 
53
53
  target_portfolio_dto = None
54
- if target_portfolio and not rebalancing_model:
54
+ if target_portfolio:
55
55
  target_portfolio_dto = target_portfolio._build_dto(obj.trade_date)
56
+ elif rebalancing_model:
57
+ target_portfolio_dto = rebalancing_model.get_target_portfolio(
58
+ obj.portfolio, obj.trade_date, obj.last_effective_date
59
+ )
60
+
56
61
  try:
57
62
  obj.reset_orders(
58
63
  target_portfolio=target_portfolio_dto, total_target_weight=Decimal("1.0") - total_cash_weight
@@ -423,23 +423,15 @@ class TestOrderProposal:
423
423
  order_proposal.portfolio.only_weighting = False
424
424
  order_proposal.portfolio.save()
425
425
  mock_fct.return_value = Decimal(1_000_000) # 1 million cash
426
- cash = cash_factory.create(currency=order_proposal.portfolio.currency)
427
426
  order_factory.create( # equity trade
428
427
  order_proposal=order_proposal,
429
428
  value_date=order_proposal.trade_date,
430
429
  portfolio=order_proposal.portfolio,
431
430
  weighting=Decimal("0.7"),
432
431
  )
433
- order_factory.create( # cash trade
434
- order_proposal=order_proposal,
435
- value_date=order_proposal.trade_date,
436
- portfolio=order_proposal.portfolio,
437
- underlying_instrument=cash,
438
- weighting=Decimal("0.2"),
439
- )
440
432
 
441
- target_cash_position = order_proposal.get_estimated_target_cash(order_proposal.portfolio.currency)
442
- assert target_cash_position.weighting == Decimal("0.2") + Decimal("1.0") - (Decimal("0.7") + Decimal("0.2"))
433
+ target_cash_position = order_proposal.get_estimated_target_cash()
434
+ assert target_cash_position.weighting == Decimal("0.3")
443
435
  assert target_cash_position.initial_shares == Decimal(1_000_000) * Decimal("0.3")
444
436
 
445
437
  def test_order_proposal_update_inception_date(self, order_proposal_factory, portfolio, instrument_factory):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.54.15
3
+ Version: 1.54.16
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -288,7 +288,7 @@ wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
288
288
  wbportfolio/models/mixins/instruments.py,sha256=SgBreTpa_X3uyCWo7t8B0VaTtl49IjmBMe4Pab6TjAM,6796
289
289
  wbportfolio/models/mixins/liquidity_stress_test.py,sha256=iQVzT3QM7VtHnqfj9gT6KUIe4wC4MJXery-AXJHUYns,58820
290
290
  wbportfolio/models/orders/__init__.py,sha256=EH9UacGR3npBMje5FGTeLOh1xqFBh9kc24WbGmBIA3g,69
291
- wbportfolio/models/orders/order_proposals.py,sha256=Yb8KVUyfYsE7-HgYLEfyOxFoQXzqFU-MhvR5Yrcf9oE,40464
291
+ wbportfolio/models/orders/order_proposals.py,sha256=uo0hWOjY6lKpGwa9fnBvskQmI0S_Usg7LYbY9PCJ4xo,40333
292
292
  wbportfolio/models/orders/orders.py,sha256=hVVw7NAFmAFHosMMs39V9DjGmWyFC_msSxF8rpDDG60,9683
293
293
  wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
294
294
  wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
@@ -307,7 +307,7 @@ wbportfolio/pms/analytics/portfolio.py,sha256=u_S-e6HUQwAyq90gweDmxyTHWrIc5nd84s
307
307
  wbportfolio/pms/analytics/utils.py,sha256=EfhKdo9B2ABaUPppb8DgZSqpNkSze8Rjej1xDjv-XcQ,282
308
308
  wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
309
309
  wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
310
- wbportfolio/pms/trading/handler.py,sha256=iNcqiFgHqABD1gOniFo3_uBHG2s1lDhMZ8D8mEF27wo,8678
310
+ wbportfolio/pms/trading/handler.py,sha256=o31jtevfnSSv0aSzAsnthz2luM7F-D5d061LaqfOHUw,8542
311
311
  wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
312
312
  wbportfolio/rebalancing/base.py,sha256=wpeoxdkLz5osxm5mRjkOoML7YkYvwuAlqSLLtHBbWp8,984
313
313
  wbportfolio/rebalancing/decorators.py,sha256=162ZmXV2YQGI830LWvEnJ95RexMzHvaCGfcVOnXTOXM,502
@@ -360,7 +360,7 @@ wbportfolio/serializers/registers.py,sha256=zhdKH_mHEBE0VOhm6xpY54bTMtcSaY5BskEa
360
360
  wbportfolio/serializers/roles.py,sha256=T-9NqTldpvaEMFy-Bib5MB6MeboygEOqcMP61mzzD3Q,2146
361
361
  wbportfolio/serializers/signals.py,sha256=hD6R4oFtwhvnsJPteytPKy2JwEelmxrapdfoLSnluaE,7053
362
362
  wbportfolio/serializers/orders/__init__.py,sha256=PKJRksA1pWsh8nVfGASoB0m3LyUzVRnq1m9VPp90J7k,271
363
- wbportfolio/serializers/orders/order_proposals.py,sha256=pXduRWC-Ad9-L5OlCPr3PK1Wa7LZdCcS6e_MkpbSNos,4301
363
+ wbportfolio/serializers/orders/order_proposals.py,sha256=FegiVe1d1AZoLtOuNOUfLN7HwGy4mKGrdYHKDwuUjPY,4430
364
364
  wbportfolio/serializers/orders/orders.py,sha256=pAKjJLRANOo1iMlcv18twuQ0aAVDVKYt-pPx6avnWRQ,7464
365
365
  wbportfolio/serializers/transactions/__init__.py,sha256=-7Pan4n7YI3iDvGXff6okzk4ycEURRxp5n_SHCY_g_I,493
366
366
  wbportfolio/serializers/transactions/claim.py,sha256=kC4E2RZRrpd9i8tGfoiV-gpWDk3ikR5F1Wf0v_IGIvw,11599
@@ -403,7 +403,7 @@ wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo
403
403
  wbportfolio/tests/models/test_splits.py,sha256=ytKcHsI_90kj1L4s8It-KEcc24rkDcElxwQ8q0QxEvk,9689
404
404
  wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
405
405
  wbportfolio/tests/models/orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
406
- wbportfolio/tests/models/orders/test_order_proposals.py,sha256=LrPs6HXU9BXsV8JNiFG_Xuaidw2kbRKd8-e4FYCOrbA,29791
406
+ wbportfolio/tests/models/orders/test_order_proposals.py,sha256=xVoKYHhzm_UihJuEY5P8G1kUURCYYSnyFVAR4aPFmUA,29353
407
407
  wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
408
408
  wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
409
409
  wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
@@ -550,7 +550,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
550
550
  wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
551
551
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
552
552
  wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
553
- wbportfolio-1.54.15.dist-info/METADATA,sha256=M1xqTeTLrYRvHviRzlNHBsh7GU-zCmYS1p5MlJBtiL4,703
554
- wbportfolio-1.54.15.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
555
- wbportfolio-1.54.15.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
556
- wbportfolio-1.54.15.dist-info/RECORD,,
553
+ wbportfolio-1.54.16.dist-info/METADATA,sha256=ikKYoJRibnFOVA5vPklrR-QM2077_-0GboeKE0Xfp1A,703
554
+ wbportfolio-1.54.16.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
555
+ wbportfolio-1.54.16.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
556
+ wbportfolio-1.54.16.dist-info/RECORD,,