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

@@ -708,7 +708,7 @@ class AssetPosition(ImportMixin, models.Model):
708
708
  instrument_type=self.underlying_quote.security_instrument_type.id,
709
709
  currency=self.underlying_quote.currency.id,
710
710
  country=self.underlying_quote.country.id if self.underlying_quote.country else None,
711
- is_cash=self.underlying_quote.is_cash,
711
+ is_cash=self.underlying_quote.is_cash or self.underlying_quote.is_cash_equivalent,
712
712
  primary_classification=(
713
713
  self.underlying_quote.primary_classification.id
714
714
  if self.underlying_quote.primary_classification
@@ -926,7 +926,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
926
926
  if self.is_composition:
927
927
  assets = list(self.get_lookthrough_positions(val_date, **kwargs))
928
928
  else:
929
- assets = self.assets.filter(date=val_date)
929
+ assets = list(self.assets.filter(date=val_date))
930
930
  return assets
931
931
 
932
932
  def compute_lookthrough(self, from_date: date, to_date: date | None = None):
@@ -2,10 +2,9 @@ import logging
2
2
  from contextlib import suppress
3
3
  from datetime import date, timedelta
4
4
  from decimal import Decimal
5
- from typing import TypeVar
5
+ from typing import Any, TypeVar
6
6
 
7
7
  from celery import shared_task
8
- from django.contrib.messages import error, info
9
8
  from django.core.exceptions import ValidationError
10
9
  from django.db import models
11
10
  from django.utils.functional import cached_property
@@ -92,14 +91,14 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
92
91
  )
93
92
  super().save(*args, **kwargs)
94
93
 
95
- @property
96
- def checked_object(self):
97
- return self.portfolio
98
-
99
94
  @property
100
95
  def check_evaluation_date(self):
101
96
  return self.trade_date
102
97
 
98
+ @property
99
+ def checked_object(self) -> Any:
100
+ return self.portfolio
101
+
103
102
  @cached_property
104
103
  def portfolio_total_asset_value(self) -> Decimal:
105
104
  return self.portfolio.get_total_asset_value(self.last_effective_date)
@@ -109,10 +108,13 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
109
108
  """
110
109
  This property holds the validated trading services and cache it.This property expect to be set only if is_valid return True
111
110
  """
111
+ target_portfolio = self._build_dto().convert_to_portfolio()
112
+
112
113
  return TradingService(
113
114
  self.trade_date,
114
115
  effective_portfolio=self.portfolio._build_dto(self.last_effective_date),
115
- target_portfolio=self._build_dto().convert_to_portfolio(),
116
+ target_portfolio=target_portfolio,
117
+ total_target_weight=target_portfolio.total_weight,
116
118
  )
117
119
 
118
120
  @cached_property
@@ -197,25 +199,30 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
197
199
 
198
200
  return trade_proposal_clone
199
201
 
200
- def normalize_trades(self):
202
+ def normalize_trades(self, total_target_weight: Decimal = Decimal("1.0")):
201
203
  """
202
204
  Call the trading service with the existing trades and normalize them in order to obtain a total sum target weight of 100%
203
205
  The existing trade will be modified directly with the given normalization factor
204
206
  """
205
- service = TradingService(self.trade_date, trades_batch=self._build_dto())
206
- service.normalize()
207
+ service = TradingService(
208
+ self.trade_date,
209
+ effective_portfolio=self.portfolio._build_dto(self.last_effective_date),
210
+ target_portfolio=self._build_dto().convert_to_portfolio(),
211
+ total_target_weight=total_target_weight,
212
+ )
207
213
  leftovers_trades = self.trades.all()
208
- total_target_weight = Decimal("0.0")
209
214
  for underlying_instrument_id, trade_dto in service.trades_batch.trades_map.items():
210
215
  with suppress(Trade.DoesNotExist):
211
216
  trade = self.trades.get(underlying_instrument_id=underlying_instrument_id)
212
217
  trade.weighting = round(trade_dto.delta_weight, 6)
213
218
  trade.save()
214
- total_target_weight += trade._target_weight
215
219
  leftovers_trades = leftovers_trades.exclude(id=trade.id)
216
220
  leftovers_trades.delete()
221
+ t_weight = self.trades.all().annotate_base_info().aggregate(models.Sum("target_weight"))[
222
+ "target_weight__sum"
223
+ ] or Decimal("0.0")
217
224
  # we handle quantization error due to the decimal max digits. In that case, we take the biggest trade (highest weight) and we remove the quantization error
218
- if quantize_error := (total_target_weight - Decimal("1.0")):
225
+ if quantize_error := (t_weight - total_target_weight):
219
226
  biggest_trade = self.trades.latest("weighting")
220
227
  biggest_trade.weighting -= quantize_error
221
228
  biggest_trade.save()
@@ -234,7 +241,12 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
234
241
  # Return the current portfolio by default
235
242
  return self.portfolio._build_dto(self.last_effective_date)
236
243
 
237
- def reset_trades(self, target_portfolio: PortfolioDTO | None = None, validate_trade: bool = True):
244
+ def reset_trades(
245
+ self,
246
+ target_portfolio: PortfolioDTO | None = None,
247
+ validate_trade: bool = True,
248
+ total_target_weight: Decimal = Decimal("1.0"),
249
+ ):
238
250
  """
239
251
  Will delete all existing trades and recreate them from the method `create_or_update_trades`
240
252
  """
@@ -249,6 +261,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
249
261
  self.trade_date,
250
262
  effective_portfolio=effective_portfolio,
251
263
  target_portfolio=target_portfolio,
264
+ total_target_weight=total_target_weight,
252
265
  )
253
266
  if validate_trade:
254
267
  service.is_valid()
@@ -279,6 +292,11 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
279
292
  status=Trade.Status.DRAFT,
280
293
  currency_fx_rate=currency_fx_rate,
281
294
  )
295
+ trade.price = trade.get_price()
296
+ # if we cannot automatically find a price, we consider the stock is invalid and we sell it
297
+ if trade.price is None:
298
+ trade.price = Decimal("0.0")
299
+ trade.weighting = -trade_dto.effective_weight
282
300
  trade.save()
283
301
 
284
302
  def replay(self):
@@ -329,7 +347,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
329
347
  last_trade_proposal_created = False
330
348
  last_trade_proposal = next_trade_proposal
331
349
 
332
- def get_estimated_shares(self, weight: Decimal, underlying_quote: Instrument) -> Decimal:
350
+ def get_estimated_shares(self, weight: Decimal, underlying_quote: Instrument, quote_price: Decimal) -> Decimal:
333
351
  """
334
352
  Estimates the number of shares for a trade based on the given weight and underlying quote.
335
353
 
@@ -342,23 +360,19 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
342
360
  Returns:
343
361
  Decimal | None: The estimated number of shares or None if the calculation fails.
344
362
  """
345
- try:
346
- # Retrieve the price of the underlying quote on the trade date TODO: this is very slow and probably due to the to_date argument to the dl which slowdown drastically the query
347
- quote_price = Decimal(underlying_quote.get_price(self.trade_date))
363
+ # Retrieve the price of the underlying quote on the trade date TODO: this is very slow and probably due to the to_date argument to the dl which slowdown drastically the query
348
364
 
349
- # Calculate the trade's total value in the portfolio's currency
350
- trade_total_value_fx_portfolio = self.portfolio_total_asset_value * weight
365
+ # Calculate the trade's total value in the portfolio's currency
366
+ trade_total_value_fx_portfolio = self.portfolio_total_asset_value * weight
351
367
 
352
- # Convert the quote price to the portfolio's currency
353
- price_fx_portfolio = quote_price * underlying_quote.currency.convert(
354
- self.trade_date, self.portfolio.currency, exact_lookup=False
355
- )
368
+ # Convert the quote price to the portfolio's currency
369
+ price_fx_portfolio = quote_price * underlying_quote.currency.convert(
370
+ self.trade_date, self.portfolio.currency, exact_lookup=False
371
+ )
356
372
 
357
- # If the price is valid, calculate and return the estimated shares
358
- if price_fx_portfolio:
359
- return trade_total_value_fx_portfolio / price_fx_portfolio
360
- except Exception:
361
- raise ValueError("We couldn't estimate the number of shares")
373
+ # If the price is valid, calculate and return the estimated shares
374
+ if price_fx_portfolio:
375
+ return trade_total_value_fx_portfolio / price_fx_portfolio
362
376
 
363
377
  def get_estimated_target_cash(self, currency: Currency) -> AssetPosition:
364
378
  """
@@ -399,7 +413,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
399
413
 
400
414
  # Estimate the target shares for the cash component
401
415
  with suppress(ValueError):
402
- total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component)
416
+ total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component, Decimal("1.0"))
403
417
 
404
418
  cash_component = Cash.objects.get_or_create(
405
419
  currency=self.portfolio.currency, defaults={"name": self.portfolio.currency.title}
@@ -447,46 +461,23 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
447
461
  },
448
462
  )
449
463
  def submit(self, by=None, description=None, **kwargs):
450
- self.reset_trades(target_portfolio=self._build_dto().convert_to_portfolio())
451
464
  trades = []
452
- trades_validation_errors = []
465
+ trades_validation_warnings = []
453
466
  for trade in self.trades.all():
454
- if not trade.last_underlying_quote_price:
455
- trade.status = Trade.Status.FAILED
456
- trades_validation_errors.append(
457
- f"Trade failed because no price is found for {trade.underlying_instrument.computed_str} on {trade.transaction_date:%Y-%m-%d}"
458
- )
459
- else:
460
- trade.status = Trade.Status.SUBMIT
467
+ trade_warnings = trade.submit(by=by, description=description, **kwargs)
468
+ if trade_warnings:
469
+ trades_validation_warnings.extend(trade_warnings)
461
470
  trades.append(trade)
462
471
 
463
- Trade.objects.bulk_update(trades, ["status"])
472
+ Trade.objects.bulk_update(trades, ["status", "shares", "weighting"])
464
473
 
465
474
  # If we estimate cash on this trade proposal, we make sure to create the corresponding cash component
466
475
  estimated_cash_position = self.get_estimated_target_cash(self.portfolio.currency)
467
- target_portfolio = self.validated_trading_service.target_portfolio
468
- if estimated_cash_position.weighting:
469
- if existing_cash_position := target_portfolio.positions_map.get(
470
- estimated_cash_position.underlying_quote.id
471
- ):
472
- existing_cash_position += estimated_cash_position
473
- else:
474
- target_portfolio.positions_map[estimated_cash_position.underlying_quote.id] = (
475
- estimated_cash_position._build_dto()
476
- )
477
- target_portfolio = PortfolioDTO(positions=tuple(target_portfolio.positions_map.values()))
476
+ target_portfolio = self.validated_trading_service.trades_batch.convert_to_portfolio(
477
+ estimated_cash_position._build_dto()
478
+ )
478
479
  self.evaluate_active_rules(self.trade_date, target_portfolio, asynchronously=True)
479
-
480
- if (
481
- trades_validation_errors
482
- and (fsm_context := getattr(self, "fsm_context", None))
483
- and (request := fsm_context.get("request", None))
484
- ):
485
- msg = f"""The reason for the failed trades are: <ul>
486
- {''.join(map(lambda o: '<li>' + o + '</li>', trades_validation_errors))}
487
- </ul>
488
- """
489
- error(request, msg, extra_tags="auto_close=0")
480
+ return trades_validation_warnings
490
481
 
491
482
  def can_submit(self):
492
483
  errors = dict()
@@ -496,7 +487,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
496
487
  service = self.validated_trading_service
497
488
  try:
498
489
  service.is_valid(ignore_error=True)
499
- # if service.trades_batch.totat_abs_delta_weight == 0:
490
+ # if service.trades_batch.total_abs_delta_weight == 0:
500
491
  # errors_list.append(
501
492
  # "There is no change detected in this trade proposal. Please submit at last one valid trade"
502
493
  # )
@@ -542,6 +533,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
542
533
  raise ValueError("Non-Rebalanceable portfolio cannot be traded manually.")
543
534
  trades = []
544
535
  assets = []
536
+ warnings = []
545
537
  # We do not want to create the estimated cash position if there is not trades in the trade proposal (shouldn't be possible anyway)
546
538
  estimated_cash_position = self.get_estimated_target_cash(self.portfolio.currency)
547
539
 
@@ -556,13 +548,11 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
556
548
 
557
549
  # if there is cash leftover, we create an extra asset position to hold the cash component
558
550
  if estimated_cash_position.weighting and len(trades) > 0:
551
+ warnings.append(
552
+ f"We created automatically a cash position of weight {estimated_cash_position.weighting:.2%}"
553
+ )
559
554
  estimated_cash_position.pre_save()
560
555
  assets.append(estimated_cash_position)
561
- if (fsm_context := getattr(self, "fsm_context", None)) and (request := fsm_context.get("request", None)):
562
- info(
563
- request,
564
- f"We created automatically a cash position of weight {estimated_cash_position.weighting:.2%}",
565
- )
566
556
 
567
557
  Trade.objects.bulk_update(trades, ["status"])
568
558
  self.portfolio.bulk_create_positions(
@@ -570,6 +560,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
570
560
  )
571
561
  if replay and self.portfolio.is_manageable:
572
562
  replay_as_task.delay(self.id, user_id=by.id if by else None)
563
+ return warnings
573
564
 
574
565
  def can_approve(self):
575
566
  errors = dict()
@@ -249,9 +249,7 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
249
249
  field=status,
250
250
  source=Status.DRAFT,
251
251
  target=GET_STATE(
252
- lambda self, **kwargs: (
253
- self.Status.SUBMIT if self.last_underlying_quote_price is not None else self.Status.FAILED
254
- ),
252
+ lambda self, **kwargs: (self.Status.SUBMIT if self.price else self.Status.FAILED),
255
253
  states=[Status.SUBMIT, Status.FAILED],
256
254
  ),
257
255
  permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
@@ -271,7 +269,12 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
271
269
  on_error="FAILED",
272
270
  )
273
271
  def submit(self, by=None, description=None, **kwargs):
274
- pass
272
+ errors = []
273
+ if not self.price:
274
+ errors.append(
275
+ f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
276
+ )
277
+ return errors
275
278
 
276
279
  def can_submit(self):
277
280
  pass
@@ -287,6 +290,7 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
287
290
  def fail(self, **kwargs):
288
291
  pass
289
292
 
293
+ # TODO To be removed
290
294
  @cached_property
291
295
  def last_underlying_quote_price(self) -> InstrumentPrice | None:
292
296
  try:
@@ -343,7 +347,7 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
343
347
  )
344
348
 
345
349
  def can_execute(self):
346
- if not self.last_underlying_quote_price:
350
+ if not self.price:
347
351
  return {"underlying_instrument": [_("Cannot execute a trade without a valid quote price")]}
348
352
  if not self.portfolio.is_manageable:
349
353
  return {
@@ -531,19 +535,22 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
531
535
 
532
536
  def save(self, *args, **kwargs):
533
537
  if self.trade_proposal:
538
+ if not self.underlying_instrument.is_investable_universe:
539
+ self.underlying_instrument.is_investable_universe = True
540
+ self.underlying_instrument.save()
534
541
  self.portfolio = self.trade_proposal.portfolio
535
542
  self.transaction_date = self.trade_proposal.trade_date
536
543
  self.value_date = self.trade_proposal.last_effective_date
537
- if not self.portfolio.only_weighting:
538
- with suppress(ValueError):
539
- self.shares = self.trade_proposal.get_estimated_shares(self.weighting, self.underlying_instrument)
544
+ if self.price is None:
545
+ # we try to get the price if not provided directly from the underlying instrument
546
+ self.price = self.get_price()
547
+ if self.trade_proposal and not self.portfolio.only_weighting:
548
+ self.shares = self.trade_proposal.get_estimated_shares(
549
+ self.weighting, self.underlying_instrument, self.price
550
+ )
540
551
 
541
552
  if not self.custodian and self.bank:
542
553
  self.custodian = Custodian.get_by_mapping(self.bank)
543
- if self.price is None:
544
- # we try to get the price if not provided directly from the underlying instrument
545
- with suppress(Exception):
546
- self.price = self.underlying_instrument.get_price(self.value_date)
547
554
 
548
555
  if self.transaction_subtype is None or self.trade_proposal:
549
556
  # if subtype not provided, we extract it automatically from the existing data.
@@ -588,9 +595,6 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
588
595
  """
589
596
 
590
597
  def get_asset(self) -> AssetPosition:
591
- last_underlying_quote_price = self.last_underlying_quote_price
592
- if not last_underlying_quote_price:
593
- raise ValueError("No price found")
594
598
  asset = AssetPosition(
595
599
  underlying_quote=self.underlying_instrument,
596
600
  portfolio_created=None,
@@ -598,9 +602,8 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
598
602
  date=self.transaction_date,
599
603
  initial_currency_fx_rate=self.currency_fx_rate,
600
604
  weighting=self._target_weight,
601
- initial_price=self.last_underlying_quote_price.net_value,
605
+ initial_price=self.price,
602
606
  initial_shares=None,
603
- underlying_quote_price=self.last_underlying_quote_price,
604
607
  asset_valuation_date=self.transaction_date,
605
608
  currency=self.currency,
606
609
  is_estimated=False,
@@ -609,6 +612,11 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
609
612
  asset.pre_save()
610
613
  return asset
611
614
 
615
+ def get_price(self) -> Decimal | None:
616
+ if self.value_date:
617
+ with suppress(ValueError):
618
+ return Decimal(self.underlying_instrument.get_price(self.value_date))
619
+
612
620
  def delete(self, **kwargs):
613
621
  pre_collection.send(sender=self.__class__, instance=self)
614
622
  super().delete(**kwargs)
@@ -631,6 +639,7 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
631
639
  instrument_type=self.underlying_instrument.security_instrument_type.id,
632
640
  currency=self.underlying_instrument.currency,
633
641
  date=self.transaction_date,
642
+ is_cash=self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent,
634
643
  )
635
644
 
636
645
  def get_alternative_valid_trades(self, share_delta: float = 0):
@@ -17,25 +17,20 @@ class TradingService:
17
17
  trade_date: date,
18
18
  effective_portfolio: Portfolio | None = None,
19
19
  target_portfolio: Portfolio | None = None,
20
- trades_batch: TradeBatch | None = None,
21
- total_value: Decimal = None,
20
+ total_target_weight: Decimal = Decimal("1.0"),
22
21
  ):
23
- self.total_value = total_value
24
22
  self.trade_date = trade_date
25
23
  if target_portfolio is None:
26
24
  target_portfolio = Portfolio(positions=())
27
25
  if effective_portfolio is None:
28
26
  effective_portfolio = Portfolio(positions=())
29
- # If effective portfoolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
30
- trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio, trades_batch=trades_batch)
31
- # if no trade but a effective portfolio is provided, we get the trade batch only from the effective portofolio (and the target portfolio if provided, but optional. Without it, the trade delta weight will be 0 )
32
- # Finally, we compute the target portfolio
33
- if trades_batch and not target_portfolio:
34
- target_portfolio = trades_batch.convert_to_portfolio()
27
+ # If effective portfolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
28
+ self.trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio).normalize(
29
+ total_target_weight
30
+ )
35
31
 
36
- self.trades_batch = trades_batch
37
- self.effective_portfolio = effective_portfolio
38
- self.target_portfolio = target_portfolio
32
+ self._effective_portfolio = effective_portfolio
33
+ self._target_portfolio = target_portfolio
39
34
 
40
35
  @property
41
36
  def errors(self) -> list[str]:
@@ -62,12 +57,11 @@ class TradingService:
62
57
  Test the given value against all the validators on the field,
63
58
  and either raise a `ValidationError` or simply return.
64
59
  """
65
- # TradeBatch(validated_trades).validate()
66
- if self.effective_portfolio:
60
+ if self._effective_portfolio:
67
61
  for trade in validated_trades:
68
62
  if (
69
63
  trade.effective_weight
70
- and trade.underlying_instrument not in self.effective_portfolio.positions_map
64
+ and trade.underlying_instrument not in self._effective_portfolio.positions_map
71
65
  ):
72
66
  raise ValidationError("All effective position needs to be matched with a validated trade")
73
67
 
@@ -75,7 +69,6 @@ class TradingService:
75
69
  self,
76
70
  effective_portfolio: Portfolio,
77
71
  target_portfolio: Portfolio,
78
- trades_batch: TradeBatch | None = None,
79
72
  ) -> TradeBatch:
80
73
  """
81
74
  Given combination of effective portfolio and either a trades batch or a target portfolio, ensure all theres variables are set
@@ -87,39 +80,35 @@ class TradingService:
87
80
 
88
81
  Returns: The normalized trades batch
89
82
  """
90
- instruments = list(effective_portfolio.positions_map.keys())
91
- instruments.extend(list(target_portfolio.positions_map.keys()))
92
- if trades_batch:
93
- instruments.extend(list(trades_batch.trades_map.keys()))
94
- _trades: list[Trade] = []
95
- for instrument in set(instruments):
96
- effective_weight = target_weight = 0
97
- effective_shares = 0
98
- instrument_type = currency = None
99
- if effective_pos := effective_portfolio.positions_map.get(instrument, None):
100
- effective_weight = effective_pos.weighting
101
- effective_shares = effective_pos.shares
102
- instrument_type, currency = effective_pos.instrument_type, effective_pos.currency
103
- if target_pos := target_portfolio.positions_map.get(instrument, None):
104
- target_weight = target_pos.weighting
105
- instrument_type, currency = target_pos.instrument_type, target_pos.currency
106
- if trades_batch and (trade := trades_batch.trades_map.get(instrument, None)):
107
- effective_weight, target_weight = trade.effective_weight, trade.target_weight
108
- effective_shares = trade.effective_shares
109
- instrument_type, currency = trade.instrument_type, trade.currency
110
-
111
- _trades.append(
112
- Trade(
113
- underlying_instrument=instrument,
114
- effective_weight=effective_weight,
115
- target_weight=target_weight,
116
- effective_shares=effective_shares,
117
- date=self.trade_date,
118
- instrument_type=instrument_type,
119
- currency=currency,
83
+ instruments = effective_portfolio.positions_map.copy()
84
+ instruments.update(target_portfolio.positions_map)
85
+
86
+ trades: list[Trade] = []
87
+ for instrument_id, pos in instruments.items():
88
+ if not pos.is_cash:
89
+ effective_weight = target_weight = 0
90
+ effective_shares = 0
91
+ instrument_type = currency = None
92
+ if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
93
+ effective_weight = effective_pos.weighting
94
+ effective_shares = effective_pos.shares
95
+ instrument_type, currency = effective_pos.instrument_type, effective_pos.currency
96
+ if target_pos := target_portfolio.positions_map.get(instrument_id, None):
97
+ target_weight = target_pos.weighting
98
+ instrument_type, currency = target_pos.instrument_type, target_pos.currency
99
+
100
+ trades.append(
101
+ Trade(
102
+ underlying_instrument=instrument_id,
103
+ effective_weight=effective_weight,
104
+ target_weight=target_weight,
105
+ effective_shares=effective_shares,
106
+ date=self.trade_date,
107
+ instrument_type=instrument_type,
108
+ currency=currency,
109
+ )
120
110
  )
121
- )
122
- return TradeBatch(tuple(_trades))
111
+ return TradeBatch(tuple(trades))
123
112
 
124
113
  def is_valid(self, ignore_error: bool = False) -> bool:
125
114
  """
@@ -151,11 +140,3 @@ class TradingService:
151
140
  raise ValidationError(self.errors)
152
141
 
153
142
  return not bool(self._errors)
154
-
155
- def normalize(self):
156
- """
157
- Normalize the instantiate trades batch so that the target weight is 100%
158
- """
159
- self.trades_batch = TradeBatch(
160
- [trade.normalize_target(self.trades_batch.total_target_weight) for trade in self.trades_batch.trades]
161
- )
wbportfolio/pms/typing.py CHANGED
@@ -48,7 +48,7 @@ class Position:
48
48
  @dataclass(frozen=True)
49
49
  class Portfolio:
50
50
  positions: tuple[Position] | tuple
51
- positions_map: dict[Position] = field(init=False, repr=False)
51
+ positions_map: dict[int, Position] = field(init=False, repr=False)
52
52
 
53
53
  def __post_init__(self):
54
54
  positions_map = {}
@@ -80,7 +80,7 @@ class Portfolio:
80
80
  @dataclass(frozen=True)
81
81
  class Trade:
82
82
  underlying_instrument: int
83
- instrument_type: str
83
+ instrument_type: int
84
84
  currency: int
85
85
  date: date_lib
86
86
 
@@ -88,6 +88,7 @@ class Trade:
88
88
  target_weight: Decimal
89
89
  id: int | None = None
90
90
  effective_shares: Decimal = None
91
+ is_cash: bool = False
91
92
 
92
93
  def __add__(self, other):
93
94
  return Trade(
@@ -121,9 +122,9 @@ class Trade:
121
122
  # if self.target_weight < 0 or self.target_weight > 1.0:
122
123
  # raise ValidationError("Target Weight needs to be in range [0, 1]")
123
124
 
124
- def normalize_target(self, total_target_weight: Decimal):
125
+ def normalize_target(self, factor: Decimal):
125
126
  t = Trade(
126
- target_weight=self.target_weight / total_target_weight if total_target_weight else self.target_weight,
127
+ target_weight=self.target_weight * factor,
127
128
  **{f.name: getattr(self, f.name) for f in fields(Trade) if f.name not in ["target_weight"]},
128
129
  )
129
130
  return t
@@ -145,15 +146,15 @@ class TradeBatch:
145
146
 
146
147
  @property
147
148
  def total_target_weight(self) -> Decimal:
148
- return round(sum([trade.target_weight for trade in self.trades]), 6)
149
+ return round(sum([trade.target_weight for trade in self.trades], Decimal("0")), 6)
149
150
 
150
151
  @property
151
152
  def total_effective_weight(self) -> Decimal:
152
- return round(sum([trade.effective_weight for trade in self.trades]), 6)
153
+ return round(sum([trade.effective_weight for trade in self.trades], Decimal("0")), 6)
153
154
 
154
155
  @property
155
- def totat_abs_delta_weight(self) -> Decimal:
156
- return sum([abs(trade.delta_weight) for trade in self.trades])
156
+ def total_abs_delta_weight(self) -> Decimal:
157
+ return sum([abs(trade.delta_weight) for trade in self.trades], Decimal("0"))
157
158
 
158
159
  def __add__(self, other):
159
160
  return TradeBatch(tuple(self.trades + other.trades))
@@ -165,7 +166,7 @@ class TradeBatch:
165
166
  if round(float(self.total_target_weight), 4) != 1: # we do that to remove decimal over precision
166
167
  raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
167
168
 
168
- def convert_to_portfolio(self):
169
+ def convert_to_portfolio(self, *extra_positions):
169
170
  positions = []
170
171
  for instrument, trade in self.trades_map.items():
171
172
  positions.append(
@@ -175,6 +176,23 @@ class TradeBatch:
175
176
  weighting=trade.target_weight,
176
177
  currency=trade.currency,
177
178
  date=trade.date,
179
+ is_cash=trade.is_cash,
178
180
  )
179
181
  )
182
+ for position in extra_positions:
183
+ if position.weighting:
184
+ positions.append(position)
180
185
  return Portfolio(tuple(positions))
186
+
187
+ def normalize(self, total_target_weight: Decimal = Decimal("1.0")):
188
+ """
189
+ Normalize the instantiate trades batch so that the target weight is 100%
190
+ """
191
+ normalization_factor = (
192
+ total_target_weight / self.total_target_weight if self.total_target_weight else Decimal("0.0")
193
+ )
194
+ normalized_trades = []
195
+ for trade in self.trades:
196
+ normalized_trades.append(trade.normalize_target(normalization_factor))
197
+ tb = TradeBatch(normalized_trades)
198
+ return tb
@@ -16,25 +16,22 @@ class ModelPortfolioRebalancing(AbstractRebalancingModel):
16
16
 
17
17
  @property
18
18
  def model_portfolio(self):
19
- if model_portfolio_rel := self.model_portfolio_rel:
20
- return model_portfolio_rel.dependency_portfolio
19
+ return self.model_portfolio_rel.dependency_portfolio if self.model_portfolio_rel else None
20
+
21
+ @property
22
+ def assets(self):
23
+ return self.model_portfolio.get_positions(self.last_effective_date) if self.model_portfolio else []
21
24
 
22
25
  def is_valid(self) -> bool:
23
- if model_portfolio := self.model_portfolio:
24
- assets = model_portfolio.get_positions(self.last_effective_date)
25
- return (
26
- assets.exists()
27
- and InstrumentPrice.objects.filter(
28
- date=self.trade_date, instrument__in=assets.values("underlying_quote")
29
- ).exists()
30
- )
31
- return False
26
+ instruments = list(map(lambda o: o.underlying_quote, self.assets))
27
+ return (
28
+ len(self.assets) > 0
29
+ and InstrumentPrice.objects.filter(date=self.trade_date, instrument__in=instruments).exists()
30
+ )
32
31
 
33
32
  def get_target_portfolio(self) -> Portfolio:
34
33
  positions = []
35
- assets = self.model_portfolio.get_positions(self.last_effective_date)
36
-
37
- for asset in assets:
34
+ for asset in self.assets:
38
35
  asset.date = self.trade_date
39
36
  asset.asset_valuation_date = self.trade_date
40
37
  positions.append(asset._build_dto())
@@ -1,3 +1,5 @@
1
+ from decimal import Decimal
2
+
1
3
  from django.contrib.messages import warning
2
4
  from django.core.exceptions import ValidationError
3
5
  from rest_framework.reverse import reverse
@@ -16,6 +18,17 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
16
18
  queryset=Portfolio.objects.all(), write_only=True, required=False, default=DefaultFromView("portfolio")
17
19
  )
18
20
  _target_portfolio = PortfolioRepresentationSerializer(source="target_portfolio")
21
+ total_cash_weight = wb_serializers.DecimalField(
22
+ default=0,
23
+ decimal_places=4,
24
+ max_digits=5,
25
+ write_only=True,
26
+ required=False,
27
+ precision=4,
28
+ percent=True,
29
+ label="Target Cash",
30
+ help_text="Enter the desired percentage for the cash component. The remaining percentage (100% minus this value) will be allocated to total target weighting. Default is 0%.",
31
+ )
19
32
 
20
33
  trade_date = wb_serializers.DateField(
21
34
  read_only=lambda view: not view.new_mode, default=DefaultFromView("default_trade_date")
@@ -23,6 +36,7 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
23
36
 
24
37
  def create(self, validated_data):
25
38
  target_portfolio = validated_data.pop("target_portfolio", None)
39
+ total_cash_weight = validated_data.pop("total_cash_weight", Decimal("0.0"))
26
40
  rebalancing_model = validated_data.get("rebalancing_model", None)
27
41
  if request := self.context.get("request"):
28
42
  validated_data["creator"] = request.user.profile
@@ -32,7 +46,9 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
32
46
  if target_portfolio and not rebalancing_model and (last_effective_date := obj.last_effective_date):
33
47
  target_portfolio_dto = target_portfolio._build_dto(last_effective_date)
34
48
  try:
35
- obj.reset_trades(target_portfolio=target_portfolio_dto)
49
+ obj.reset_trades(
50
+ target_portfolio=target_portfolio_dto, total_target_weight=Decimal("1.0") - total_cash_weight
51
+ )
36
52
  except ValidationError as e:
37
53
  if request := self.context.get("request"):
38
54
  warning(request, str(e), extra_tags="auto_close=0")
@@ -60,6 +76,7 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
60
76
  fields = (
61
77
  "id",
62
78
  "trade_date",
79
+ "total_cash_weight",
63
80
  "comment",
64
81
  "status",
65
82
  "portfolio",
@@ -320,6 +320,16 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
320
320
  effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
321
321
  target_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
322
322
 
323
+ total_value_fx_portfolio = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=2, default=0)
324
+ effective_total_value_fx_portfolio = wb_serializers.DecimalField(
325
+ read_only=True, max_digits=16, decimal_places=2, default=0
326
+ )
327
+ target_total_value_fx_portfolio = wb_serializers.DecimalField(
328
+ read_only=True, max_digits=16, decimal_places=2, default=0
329
+ )
330
+
331
+ portfolio_currency = wb_serializers.CharField(read_only=True)
332
+
323
333
  def validate(self, data):
324
334
  data.pop("company", None)
325
335
  data.pop("security", None)
@@ -331,6 +341,8 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
331
341
  )
332
342
  effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
333
343
  weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
344
+ if (target_weight := data.pop("target_weight", None)) is not None:
345
+ weighting = target_weight - effective_weight
334
346
  if (target_weight := data.pop("target_weight", None)) is not None:
335
347
  weighting = target_weight - effective_weight
336
348
  if weighting >= 0:
@@ -343,14 +355,25 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
343
355
  class Meta:
344
356
  model = Trade
345
357
  percent_fields = ["effective_weight", "target_weight", "weighting"]
358
+ decorators = {
359
+ "total_value_fx_portfolio": wb_serializers.decorator(
360
+ decorator_type="text", position="left", value="{{portfolio_currency}}"
361
+ ),
362
+ "effective_total_value_fx_portfolio": wb_serializers.decorator(
363
+ decorator_type="text", position="left", value="{{portfolio_currency}}"
364
+ ),
365
+ "target_total_value_fx_portfolio": wb_serializers.decorator(
366
+ decorator_type="text", position="left", value="{{portfolio_currency}}"
367
+ ),
368
+ }
346
369
  read_only_fields = (
347
370
  "transaction_subtype",
348
371
  "shares",
349
- # "underlying_instrument",
350
- # "_underlying_instrument",
351
- "shares",
352
372
  "effective_shares",
353
373
  "target_shares",
374
+ "total_value_fx_portfolio",
375
+ "effective_total_value_fx_portfolio",
376
+ "target_total_value_fx_portfolio",
354
377
  )
355
378
  fields = (
356
379
  "id",
@@ -375,6 +398,10 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
375
398
  "order",
376
399
  "effective_shares",
377
400
  "target_shares",
401
+ "total_value_fx_portfolio",
402
+ "effective_total_value_fx_portfolio",
403
+ "target_total_value_fx_portfolio",
404
+ "portfolio_currency",
378
405
  )
379
406
 
380
407
 
@@ -66,11 +66,11 @@ class TestTradeProposal:
66
66
  validated_trading_service = trade_proposal.validated_trading_service
67
67
 
68
68
  # Assert effective and target portfolios are as expected
69
- assert validated_trading_service.effective_portfolio.to_dict() == {
69
+ assert validated_trading_service._effective_portfolio.to_dict() == {
70
70
  a1.underlying_quote.id: a1.weighting,
71
71
  a2.underlying_quote.id: a2.weighting,
72
72
  }
73
- assert validated_trading_service.target_portfolio.to_dict() == {
73
+ assert validated_trading_service._target_portfolio.to_dict() == {
74
74
  a1.underlying_quote.id: a1.weighting + t1.weighting,
75
75
  a2.underlying_quote.id: a2.weighting + t2.weighting,
76
76
  }
@@ -361,6 +361,8 @@ class TestTradeProposal:
361
361
  """
362
362
  portfolio = trade_proposal.portfolio
363
363
  instrument = instrument_factory.create(currency=portfolio.currency)
364
+ underlying_quote_price = instrument_price_factory.create(instrument=instrument, date=trade_proposal.trade_date)
365
+ mock_fct.return_value = Decimal(1_000_000) # 1 million cash
364
366
  trade = trade_factory.create(
365
367
  trade_proposal=trade_proposal,
366
368
  transaction_date=trade_proposal.trade_date,
@@ -368,12 +370,12 @@ class TestTradeProposal:
368
370
  underlying_instrument=instrument,
369
371
  )
370
372
  trade.refresh_from_db()
371
- underlying_quote_price = instrument_price_factory.create(instrument=instrument, date=trade.transaction_date)
372
- mock_fct.return_value = Decimal(1_000_000) # 1 million cash
373
373
 
374
374
  # Assert estimated shares are correctly calculated
375
375
  assert (
376
- trade_proposal.get_estimated_shares(trade.weighting, trade.underlying_instrument)
376
+ trade_proposal.get_estimated_shares(
377
+ trade.weighting, trade.underlying_instrument, underlying_quote_price.net_value
378
+ )
377
379
  == Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
378
380
  )
379
381
 
@@ -38,30 +38,24 @@ class TestModelPortfolioRebalancing:
38
38
  def model(self, portfolio, weekday):
39
39
  from wbportfolio.rebalancing.models import ModelPortfolioRebalancing
40
40
 
41
+ PortfolioPortfolioThroughModel.objects.create(
42
+ portfolio=portfolio,
43
+ dependency_portfolio=PortfolioFactory.create(),
44
+ type=PortfolioPortfolioThroughModel.Type.MODEL,
45
+ )
41
46
  return ModelPortfolioRebalancing(portfolio, (weekday + BDay(1)).date(), weekday)
42
47
 
43
48
  def test_is_valid(self, portfolio, weekday, model, asset_position_factory, instrument_price_factory):
44
49
  assert not model.is_valid()
45
50
  asset_position_factory.create(portfolio=model.portfolio, date=model.last_effective_date)
46
51
  assert not model.is_valid()
47
- model_portfolio = PortfolioFactory.create()
48
- PortfolioPortfolioThroughModel.objects.create(
49
- portfolio=model.portfolio,
50
- dependency_portfolio=model_portfolio,
51
- type=PortfolioPortfolioThroughModel.Type.MODEL,
52
- )
52
+
53
53
  a = asset_position_factory.create(portfolio=model.model_portfolio, date=model.last_effective_date)
54
54
  assert not model.is_valid()
55
55
  instrument_price_factory.create(instrument=a.underlying_quote, date=model.trade_date)
56
56
  assert model.is_valid()
57
57
 
58
58
  def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
59
- model_portfolio = PortfolioFactory.create()
60
- PortfolioPortfolioThroughModel.objects.create(
61
- portfolio=model.portfolio,
62
- dependency_portfolio=model_portfolio,
63
- type=PortfolioPortfolioThroughModel.Type.MODEL,
64
- )
65
59
  asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
66
60
  asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
67
61
  a1 = asset_position_factory(weighting=0.8, portfolio=portfolio.model_portfolio, date=model.last_effective_date)
@@ -91,7 +85,7 @@ class TestCompositeRebalancing:
91
85
  transaction_date=model.last_effective_date,
92
86
  transaction_subtype=Trade.Type.BUY,
93
87
  trade_proposal=trade_proposal,
94
- weighting=0.7,
88
+ weighting=Decimal(0.7),
95
89
  status=Trade.Status.EXECUTED,
96
90
  )
97
91
  TradeFactory.create(
@@ -99,7 +93,7 @@ class TestCompositeRebalancing:
99
93
  transaction_date=model.last_effective_date,
100
94
  transaction_subtype=Trade.Type.BUY,
101
95
  trade_proposal=trade_proposal,
102
- weighting=0.3,
96
+ weighting=Decimal(0.3),
103
97
  status=Trade.Status.EXECUTED,
104
98
  )
105
99
  assert not model.is_valid()
@@ -117,7 +111,7 @@ class TestCompositeRebalancing:
117
111
  transaction_date=model.last_effective_date,
118
112
  transaction_subtype=Trade.Type.BUY,
119
113
  trade_proposal=trade_proposal,
120
- weighting=0.8,
114
+ weighting=Decimal(0.8),
121
115
  status=Trade.Status.EXECUTED,
122
116
  )
123
117
  t2 = TradeFactory.create(
@@ -125,7 +119,7 @@ class TestCompositeRebalancing:
125
119
  transaction_date=model.last_effective_date,
126
120
  transaction_subtype=Trade.Type.BUY,
127
121
  trade_proposal=trade_proposal,
128
- weighting=0.2,
122
+ weighting=Decimal(0.2),
129
123
  status=Trade.Status.EXECUTED,
130
124
  )
131
125
  target_portfolio = model.get_target_portfolio()
@@ -1,7 +1,13 @@
1
+ from wbcore import serializers as wb_serializers
1
2
  from wbcore.contrib.icons import WBIcon
2
3
  from wbcore.enums import RequestType
3
4
  from wbcore.metadata.configs import buttons as bt
4
5
  from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
6
+ from wbcore.metadata.configs.display import create_simple_display
7
+
8
+
9
+ class NormalizeSerializer(wb_serializers.Serializer):
10
+ total_cash_weight = wb_serializers.FloatField(default=0, precision=4, percent=True)
5
11
 
6
12
 
7
13
  class TradeProposalButtonConfig(ButtonViewConfig):
@@ -41,10 +47,12 @@ class TradeProposalButtonConfig(ButtonViewConfig):
41
47
  icon=WBIcon.EDIT.icon,
42
48
  label="Normalize Trades",
43
49
  description_fields="""
44
- <p>Make sure all trades normalize to a total target weight of 100%</p>
50
+ <p>Make sure all trades normalize to a total target weight of (100 - {{total_cash_weight}})%</p>
45
51
  """,
46
52
  action_label="Normalize Trades",
47
53
  title="Normalize Trades",
54
+ serializer=NormalizeSerializer,
55
+ instance_display=create_simple_display([["total_cash_weight"]]),
48
56
  ),
49
57
  bt.ActionButton(
50
58
  method=RequestType.PATCH,
@@ -77,11 +77,12 @@ class TradeProposalDisplayConfig(DisplayViewConfig):
77
77
  layouts={
78
78
  default(): Layout(
79
79
  grid_template_areas=[
80
- ["status", "status", "status"],
81
- ["trade_date", "rebalancing_model", "target_portfolio"]
80
+ ["status", "status"],
81
+ ["trade_date", "total_cash_weight"],
82
+ ["rebalancing_model", "target_portfolio"]
82
83
  if self.view.new_mode
83
- else ["trade_date", "rebalancing_model", "rebalancing_model"],
84
- ["comment", "comment", "comment"],
84
+ else ["rebalancing_model", "rebalancing_model"],
85
+ ["comment", "comment"],
85
86
  ],
86
87
  ),
87
88
  },
@@ -357,6 +357,42 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
357
357
  ],
358
358
  )
359
359
  )
360
+ fields.append(
361
+ dp.Field(
362
+ label="Total Value",
363
+ open_by_default=False,
364
+ key=None,
365
+ children=[
366
+ dp.Field(
367
+ key="effective_total_value_fx_portfolio",
368
+ label="Effective Total Value",
369
+ show="open",
370
+ width=Unit.PIXEL(150),
371
+ ),
372
+ dp.Field(
373
+ key="target_total_value_fx_portfolio",
374
+ label="Target Total Value",
375
+ show="open",
376
+ width=Unit.PIXEL(150),
377
+ ),
378
+ dp.Field(
379
+ key="total_value_fx_portfolio",
380
+ label="Total Value",
381
+ formatting_rules=[
382
+ dp.FormattingRule(
383
+ style={"color": WBColor.RED_DARK.value, "fontWeight": "bold"},
384
+ condition=("<", 0),
385
+ ),
386
+ dp.FormattingRule(
387
+ style={"color": WBColor.GREEN_DARK.value, "fontWeight": "bold"},
388
+ condition=(">", 0),
389
+ ),
390
+ ],
391
+ width=Unit.PIXEL(150),
392
+ ),
393
+ ],
394
+ )
395
+ )
360
396
  fields.append(
361
397
  dp.Field(
362
398
  label="Information",
@@ -1,5 +1,6 @@
1
1
  from contextlib import suppress
2
2
  from datetime import date
3
+ from decimal import Decimal
3
4
 
4
5
  from django.contrib.messages import info, warning
5
6
  from django.shortcuts import get_object_or_404
@@ -106,8 +107,9 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
106
107
  @action(detail=True, methods=["PATCH"])
107
108
  def normalize(self, request, pk=None):
108
109
  trade_proposal = get_object_or_404(TradeProposal, pk=pk)
110
+ total_cash_weight = Decimal(request.data.get("total_cash_weight", Decimal("0.0")))
109
111
  if trade_proposal.status == TradeProposal.Status.DRAFT:
110
- trade_proposal.normalize_trades()
112
+ trade_proposal.normalize_trades(total_target_weight=Decimal("1.0") - total_cash_weight)
111
113
  return Response({"send": True})
112
114
  return Response({"status": "Trade proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
113
115
 
@@ -25,7 +25,6 @@ from wbcore.permissions.permissions import InternalUserPermissionMixin
25
25
  from wbcore.utils.strings import format_number
26
26
  from wbcore.viewsets.mixins import OrderableMixin
27
27
  from wbcrm.models import Account
28
- from wbfdm.models import Cash
29
28
 
30
29
  from wbportfolio.filters import (
31
30
  SubscriptionRedemptionFilterSet,
@@ -395,6 +394,10 @@ class TradeTradeProposalModelViewSet(
395
394
  def trade_proposal(self):
396
395
  return get_object_or_404(TradeProposal, pk=self.kwargs["trade_proposal_id"])
397
396
 
397
+ @cached_property
398
+ def portfolio_total_asset_value(self):
399
+ return self.trade_proposal.portfolio_total_asset_value
400
+
398
401
  def has_import_permission(self, request) -> bool: # allow import only on draft trade proposal
399
402
  return super().has_import_permission(request) and self.trade_proposal.status == TradeProposal.Status.DRAFT
400
403
 
@@ -409,46 +412,36 @@ class TradeTradeProposalModelViewSet(
409
412
  def get_aggregates(self, queryset, *args, **kwargs):
410
413
  agg = {}
411
414
  if queryset.exists():
412
- cash_target_position = self.trade_proposal.get_estimated_target_cash(
413
- self.trade_proposal.portfolio.currency
414
- )
415
- cash_target_cash_weight, cash_target_cash_shares = (
416
- cash_target_position.weighting,
417
- cash_target_position.initial_shares,
418
- )
419
- extra_existing_cash_components = Cash.objects.filter(
420
- id__in=self.trade_proposal.trades.filter(underlying_instrument__is_cash=True).values(
421
- "underlying_instrument"
422
- )
423
- ).exclude(currency=self.trade_proposal.portfolio.currency)
424
-
425
- for cash_component in extra_existing_cash_components:
426
- extra_cash_position = self.trade_proposal.get_estimated_target_cash(cash_component.currency)
427
- cash_target_cash_weight += extra_cash_position.weighting
428
- cash_target_cash_shares += extra_cash_position.initial_shares
429
415
  noncash_aggregates = queryset.filter(underlying_instrument__is_cash=False).aggregate(
430
416
  sum_target_weight=Sum(F("target_weight")),
431
417
  sum_effective_weight=Sum(F("effective_weight")),
432
- sum_target_shares=Sum(F("target_shares")),
433
- sum_effective_shares=Sum(F("effective_shares")),
434
- )
435
- cash_aggregates = queryset.filter(underlying_instrument__is_cash=True).aggregate(
436
- sum_effective_weight=Sum(F("effective_weight")),
437
- sum_effective_shares=Sum(F("effective_shares")),
418
+ sum_target_total_value_fx_portfolio=Sum(F("target_total_value_fx_portfolio")),
419
+ sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
438
420
  )
421
+
439
422
  # weights aggregates
440
- cash_sum_effective_weight = cash_aggregates["sum_effective_weight"] or Decimal(0)
423
+ cash_sum_effective_weight = Decimal("1.0") - noncash_aggregates["sum_effective_weight"]
424
+ cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
441
425
  noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
442
426
  noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
443
427
  sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
444
428
  sum_sell_weight = queryset.filter(weighting__lt=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
445
429
 
446
430
  # shares aggregates
447
- cash_sum_effective_shares = cash_aggregates["sum_effective_shares"] or Decimal(0)
448
- noncash_sum_effective_shares = noncash_aggregates["sum_effective_shares"] or Decimal(0)
449
- noncash_sum_target_shares = noncash_aggregates["sum_target_shares"] or Decimal(0)
450
- sum_buy_shares = queryset.filter(shares__gte=0).aggregate(s=Sum(F("shares")))["s"] or Decimal(0)
451
- sum_sell_shares = queryset.filter(shares__lt=0).aggregate(s=Sum(F("shares")))["s"] or Decimal(0)
431
+ cash_sum_effective_total_value_fx_portfolio = cash_sum_effective_weight * self.portfolio_total_asset_value
432
+ cash_sum_target_total_value_fx_portfolio = cash_sum_target_cash_weight * self.portfolio_total_asset_value
433
+ noncash_sum_effective_total_value_fx_portfolio = noncash_aggregates[
434
+ "sum_effective_total_value_fx_portfolio"
435
+ ] or Decimal(0)
436
+ noncash_sum_target_total_value_fx_portfolio = noncash_aggregates[
437
+ "sum_target_total_value_fx_portfolio"
438
+ ] or Decimal(0)
439
+ sum_buy_total_value_fx_portfolio = queryset.filter(total_value_fx_portfolio__gte=0).aggregate(
440
+ s=Sum(F("total_value_fx_portfolio"))
441
+ )["s"] or Decimal(0)
442
+ sum_sell_total_value_fx_portfolio = queryset.filter(total_value_fx_portfolio__lt=0).aggregate(
443
+ s=Sum(F("total_value_fx_portfolio"))
444
+ )["s"] or Decimal(0)
452
445
 
453
446
  agg = {
454
447
  "effective_weight": {
@@ -457,29 +450,38 @@ class TradeTradeProposalModelViewSet(
457
450
  "Total": format_number(noncash_sum_effective_weight + cash_sum_effective_weight, decimal=6),
458
451
  },
459
452
  "target_weight": {
460
- "Cash": format_number(cash_target_cash_weight, decimal=6),
453
+ "Cash": format_number(cash_sum_target_cash_weight, decimal=6),
461
454
  "Non-Cash": format_number(noncash_sum_target_weight, decimal=6),
462
- "Total": format_number(cash_target_cash_weight + noncash_sum_target_weight, decimal=6),
455
+ "Total": format_number(cash_sum_target_cash_weight + noncash_sum_target_weight, decimal=6),
463
456
  },
464
- "effective_shares": {
465
- "Cash": format_number(cash_sum_effective_shares, decimal=6),
466
- "Non-Cash": format_number(noncash_sum_effective_shares, decimal=6),
467
- "Total": format_number(cash_sum_effective_shares + noncash_sum_effective_shares, decimal=6),
457
+ "effective_total_value_fx_portfolio": {
458
+ "Cash": format_number(cash_sum_effective_total_value_fx_portfolio, decimal=6),
459
+ "Non-Cash": format_number(noncash_sum_effective_total_value_fx_portfolio, decimal=6),
460
+ "Total": format_number(
461
+ cash_sum_effective_total_value_fx_portfolio + noncash_sum_effective_total_value_fx_portfolio,
462
+ decimal=6,
463
+ ),
468
464
  },
469
- "target_shares": {
470
- "Cash": format_number(cash_target_cash_shares, decimal=6),
471
- "Non-Cash": format_number(noncash_sum_target_shares, decimal=6),
472
- "Total": format_number(cash_target_cash_shares + noncash_sum_target_shares, decimal=6),
465
+ "target_total_value_fx_portfolio": {
466
+ "Cash": format_number(cash_sum_target_total_value_fx_portfolio, decimal=6),
467
+ "Non-Cash": format_number(noncash_sum_target_total_value_fx_portfolio, decimal=6),
468
+ "Total": format_number(
469
+ cash_sum_target_total_value_fx_portfolio + noncash_sum_target_total_value_fx_portfolio,
470
+ decimal=6,
471
+ ),
473
472
  },
474
473
  "weighting": {
475
- "Cash Flow": format_number(cash_sum_effective_weight - cash_target_cash_weight, decimal=6),
474
+ "Cash Flow": format_number(cash_sum_target_cash_weight - cash_sum_effective_weight, decimal=6),
476
475
  "Buy": format_number(sum_buy_weight, decimal=6),
477
476
  "Sell": format_number(sum_sell_weight, decimal=6),
478
477
  },
479
- "shares": {
480
- "Cash Flow": format_number(cash_sum_effective_shares - cash_target_cash_shares, decimal=6),
481
- "Buy": format_number(sum_buy_shares, decimal=6),
482
- "Sell": format_number(sum_sell_shares, decimal=6),
478
+ "total_value_fx_portfolio": {
479
+ "Cash Flow": format_number(
480
+ cash_sum_target_total_value_fx_portfolio - cash_sum_effective_total_value_fx_portfolio,
481
+ decimal=6,
482
+ ),
483
+ "Buy": format_number(sum_buy_total_value_fx_portfolio, decimal=6),
484
+ "Sell": format_number(sum_sell_total_value_fx_portfolio, decimal=6),
483
485
  },
484
486
  }
485
487
 
@@ -509,6 +511,7 @@ class TradeTradeProposalModelViewSet(
509
511
  qs = super().get_queryset().filter(trade_proposal=self.kwargs["trade_proposal_id"]).annotate_base_info()
510
512
  else:
511
513
  qs = TradeProposal.objects.none()
514
+
512
515
  return qs.annotate(
513
516
  underlying_instrument_isin=F("underlying_instrument__isin"),
514
517
  underlying_instrument_ticker=F("underlying_instrument__ticker"),
@@ -520,4 +523,7 @@ class TradeTradeProposalModelViewSet(
520
523
  ),
521
524
  default=F("underlying_instrument__instrument_type__short_name"),
522
525
  ),
526
+ effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
527
+ target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
528
+ portfolio_currency=F("portfolio__currency__symbol"),
523
529
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.52.5
3
+ Version: 1.52.6
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -250,11 +250,11 @@ wbportfolio/migrations/0077_remove_transaction_currency_and_more.py,sha256=Yf4a3
250
250
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
251
251
  wbportfolio/models/__init__.py,sha256=HSpa5xwh_MHQaBpNrq9E0CbdEE5Iq-pDLIsPzZ-TRTg,904
252
252
  wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
253
- wbportfolio/models/asset.py,sha256=MgEdE1AZQAxHDQX6iVCr1zD9QbsIn95OQzOHoTKjAEg,45446
253
+ wbportfolio/models/asset.py,sha256=WwbLjpGHzz17yJxrkuTP4MbErqcTJMlKkDrQI92p5II,45490
254
254
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
255
255
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
256
256
  wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
257
- wbportfolio/models/portfolio.py,sha256=afk2XnVxMIiAmLPrDIGDbeOyhvdDzXs7K-MCaUKKKyc,56275
257
+ wbportfolio/models/portfolio.py,sha256=yQ8cqAZLOMwsiejC9RzOLoqLDV5q5ZWYWMOc2BlcHdw,56281
258
258
  wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
259
259
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
260
260
  wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
@@ -281,16 +281,16 @@ wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWK
281
281
  wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
282
282
  wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
283
283
  wbportfolio/models/transactions/rebalancing.py,sha256=obzgewWKOD4kJbCoF5fhtfDk502QkbrjPKh8T9KDGew,7355
284
- wbportfolio/models/transactions/trade_proposals.py,sha256=b1VbgR91-3LFq08iNnafcTjvsDboimU7ZRpf5jtQk7w,32234
285
- wbportfolio/models/transactions/trades.py,sha256=wTOa0Wu4OejO3CeeJnwvPzoDUVWMsHRbyEA7QllaLRs,31231
284
+ wbportfolio/models/transactions/trade_proposals.py,sha256=Jd7rfiE42Qtqp9VXQ448OF9m_DKoFR8ISjSh-1TDi8o,31592
285
+ wbportfolio/models/transactions/trades.py,sha256=og8R4Sg7LCRQsuX09uYjZXq9q4VsUb0j8W8iCTyx0pQ,31571
286
286
  wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
287
287
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
288
- wbportfolio/pms/typing.py,sha256=b2pBWYt1E8ok-Kqm0lEFIakSnWJ6Ib57z-VX3C3gkQc,6081
288
+ wbportfolio/pms/typing.py,sha256=qcqEs94HA7qNB631LErV7FVQ4ytYBLetuPDcilrnlW0,6798
289
289
  wbportfolio/pms/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
290
290
  wbportfolio/pms/analytics/portfolio.py,sha256=vE0KA6Z037bUdmBTkYuBqXElt80nuYObNzY_kWvxEZY,1360
291
291
  wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
292
292
  wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
293
- wbportfolio/pms/trading/handler.py,sha256=EzvCP8NNzd0jjE3ilCupbmFBCaojKvlrbq7n_JirahU,7151
293
+ wbportfolio/pms/trading/handler.py,sha256=ORvtwAG2VX1GvNvNE2HnDvS-4UNrgBwCMPydnfphtY0,5992
294
294
  wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
295
295
  wbportfolio/rebalancing/base.py,sha256=NwTGZtBm1f35gj5Jp6iTyyFvDT1GSIztN990cKBvYzQ,637
296
296
  wbportfolio/rebalancing/decorators.py,sha256=JhQ2vkcIuGBTGvNmpkQerdw2-vLq-RAb0KAPjOrETkw,515
@@ -298,7 +298,7 @@ wbportfolio/rebalancing/models/__init__.py,sha256=AQjG7Tu5vlmhqncVoYOjpBKU2UIvgo
298
298
  wbportfolio/rebalancing/models/composite.py,sha256=XEgK3oMurrE_d_l5uN0stBKRrtvnKQzRWyXNXuBYfmc,1818
299
299
  wbportfolio/rebalancing/models/equally_weighted.py,sha256=FCpSKOs49ckNYVgoYIiHB0BqPT9OeCMuFoet4Ixbp-Y,1210
300
300
  wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=6ZsR8iJg6l89CsxHqoxJSXlTaj8Pmb8_bFPrXhnxaRs,5295
301
- wbportfolio/rebalancing/models/model_portfolio.py,sha256=DNg9vEDYDUwXTOnIpk26FQSPHC0qxkuvW2sJWX0VodQ,1489
301
+ wbportfolio/rebalancing/models/model_portfolio.py,sha256=Dk3tw9u3WG1jCP3V2-R05GS4-DmDBBtxH4h6p7pRe4g,1393
302
302
  wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
303
303
  wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
304
304
  wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -346,8 +346,8 @@ wbportfolio/serializers/transactions/__init__.py,sha256=oAfidhjjCKP0exeHbzJgGuBd
346
346
  wbportfolio/serializers/transactions/claim.py,sha256=kC4E2RZRrpd9i8tGfoiV-gpWDk3ikR5F1Wf0v_IGIvw,11599
347
347
  wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
348
348
  wbportfolio/serializers/transactions/fees.py,sha256=3mBzs6vdfST9roeQB-bmLJhipY7i5jBtAXjoTTE-GOg,2388
349
- wbportfolio/serializers/transactions/trade_proposals.py,sha256=fiGpL6za5ERLtdbud5wrciCVXHX6-3SjeF8Zaa6Zhzg,3410
350
- wbportfolio/serializers/transactions/trades.py,sha256=Rhv0UezIpnDxWmj5v9dQECfS0un7yQUIpnkOtD6z6YY,15452
349
+ wbportfolio/serializers/transactions/trade_proposals.py,sha256=ufYBYiSttz5KAlAaaXbnf98oUFT_qNfxF_VUM4ClXE8,4072
350
+ wbportfolio/serializers/transactions/trades.py,sha256=JtL2jrvIjBVsmB2N5ngw_1UyzK7BbxGK5yuNTCRSfkU,16815
351
351
  wbportfolio/static/wbportfolio/css/macro_review.css,sha256=FAVVO8nModxwPXcTKpcfzVxBGPZGJVK1Xn-0dkSfGyc,233
352
352
  wbportfolio/static/wbportfolio/markdown/documentation/account_holding_reconciliation.md,sha256=MabxOvOne8s5gl6osDoow6-3ghaXLAYg9THWpvy6G5I,921
353
353
  wbportfolio/static/wbportfolio/markdown/documentation/aggregate_asset_position_liquidity.md,sha256=HEgXB7uqmqfty-GBCCXYxrAN-teqmxWuqDLK_liKWVc,1090
@@ -387,12 +387,12 @@ wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
387
387
  wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
388
388
  wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
389
389
  wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
390
- wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=UB8Dvs7nT42D_uznuLyzni7PYiY-dabLCDXmV2W_yb4,18928
390
+ wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=ERGDbniHctEnIG5yFLkrZX2Jw1jEew5rWceVKuKskto,18997
391
391
  wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0Wq9ycA2oeeEBhO3dzV7sbXEU,9863
392
392
  wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
393
393
  wbportfolio/tests/pms/test_analytics.py,sha256=fAuY1zcXibttFpBh2GhKVyzdYfi1kz_b7SPa9xZQXY0,1086
394
394
  wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
395
- wbportfolio/tests/rebalancing/test_models.py,sha256=_gT_7UtpOWceDwT7FbTUW6P6ZpCVLBpgXWM0goIljWc,8090
395
+ wbportfolio/tests/rebalancing/test_models.py,sha256=4rSb05xKH881ft12G0B8ZSeFgJvutKcW_7vFgVQa0DI,7813
396
396
  wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
397
397
  wbportfolio/tests/serializers/test_claims.py,sha256=vQrg73xQXRFEgvx3KI9ivFre_wpBFzdO0p0J13PkvdY,582
398
398
  wbportfolio/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -436,7 +436,7 @@ wbportfolio/viewsets/configs/buttons/products.py,sha256=bHOs2ftfaLbIbJ7E8yFqQqSu
436
436
  wbportfolio/viewsets/configs/buttons/reconciliations.py,sha256=lw4r22GHpqKPUF1MrB6P9dOkL-FHe5iiBJ0--f8D74E,3145
437
437
  wbportfolio/viewsets/configs/buttons/registers.py,sha256=aS89TsYHql83k-NHojOrLDqtBpnpsUUO8x63PiMfrXM,445
438
438
  wbportfolio/viewsets/configs/buttons/signals.py,sha256=6sKBQI_eDvZuZR5bUUwvur5R67A3oChAGxPfayWUelE,2739
439
- wbportfolio/viewsets/configs/buttons/trade_proposals.py,sha256=eZBfYk5ZhancCVcu7bvRKPGBKTl_tGlgz6BZrGPpPxQ,2953
439
+ wbportfolio/viewsets/configs/buttons/trade_proposals.py,sha256=Os5jT-aL0jMNkdm2sJpGRZvY4OzreR6Ah7eHex_6zB4,3383
440
440
  wbportfolio/viewsets/configs/buttons/trades.py,sha256=bPOqqwdgadSUbU9l5aSirqr5UAMFe2AiFNsXLRsKwn8,2535
441
441
  wbportfolio/viewsets/configs/display/__init__.py,sha256=jJqSCCAfw_vjbcsIkzKr39LdAZA810LUBfSH1EA7MAI,2086
442
442
  wbportfolio/viewsets/configs/display/adjustments.py,sha256=jIOEc23OCYBguLaZRlZxC916kocYT35ZV9Jsiocs9nk,3334
@@ -456,8 +456,8 @@ wbportfolio/viewsets/configs/display/rebalancing.py,sha256=yw9X1Nf2-V_KP_mCX4pVK
456
456
  wbportfolio/viewsets/configs/display/reconciliations.py,sha256=YvMAuwmpX0HExvGsuf5UvcRQxe4eMo1iyNJX68GGC_k,6021
457
457
  wbportfolio/viewsets/configs/display/registers.py,sha256=1np75exIk5rfct6UkVN_RnfJ9ozvIkcWJgFV4_4rJns,3182
458
458
  wbportfolio/viewsets/configs/display/roles.py,sha256=SFUyCdxSlHZ3NsMrJmpVBSlg-XKGaEFteV89nyLMMAQ,1815
459
- wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=sRLSUjKlarBhnTwg7tX_Juldor3beswJlyvZfFPvNEk,4315
460
- wbportfolio/viewsets/configs/display/trades.py,sha256=PbGI8Sjs8YMBLfeYdgXRbv_tvel2aV3dB-7oZXnLwz4,17587
459
+ wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=gM5CuUmNcVtwJHYE9Myg-RR_WmAc6VDJztwUqQ1uZ7g,4335
460
+ wbportfolio/viewsets/configs/display/trades.py,sha256=ZYZ2ceE4Hyw2SpQjW00VDnzGz7Wlu3jjPoK_J2GZLyk,19181
461
461
  wbportfolio/viewsets/configs/endpoints/__init__.py,sha256=KR3AsSxl71VAVkYBRVowgs3PZB8vaKa34WHHUvC-2MY,2807
462
462
  wbportfolio/viewsets/configs/endpoints/adjustments.py,sha256=KRZLqv4ZeB27d-_s7Qlqj9LI3I9WFJkjc6Mw8agDueE,606
463
463
  wbportfolio/viewsets/configs/endpoints/assets.py,sha256=B8W6ATQQfT292TllE4sUDYv5wHUCo99HEqymJuMtFAc,1909
@@ -517,9 +517,9 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
517
517
  wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
518
518
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
519
519
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
520
- wbportfolio/viewsets/transactions/trade_proposals.py,sha256=rCJoJSL11qpjxciX25Q5ZU3ewELyyDBUPXcj4Z88Vdg,6037
521
- wbportfolio/viewsets/transactions/trades.py,sha256=wCyTTyVKjYuMRsrFcmwD3_yggWxCNm-mdoYzVnhGP0U,21727
522
- wbportfolio-1.52.5.dist-info/METADATA,sha256=eHgs5EUj3Ss3i8U5cBrhqUW-4kXfD6umFAiF2Gq8aHw,702
523
- wbportfolio-1.52.5.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
524
- wbportfolio-1.52.5.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
525
- wbportfolio-1.52.5.dist-info/RECORD,,
520
+ wbportfolio/viewsets/transactions/trade_proposals.py,sha256=zdb9yqyAE1-hcMmxxN3CYePWoPVWIW3WP3v8agErDeY,6210
521
+ wbportfolio/viewsets/transactions/trades.py,sha256=DcfiJReBnnR6oBgNKZyBYWhXurtxJG_DCzxq5WCionA,22025
522
+ wbportfolio-1.52.6.dist-info/METADATA,sha256=9YI1D7f4Omg3dI2yJs8v_cUOHJ4cdzujpm3l7QfGh38,702
523
+ wbportfolio-1.52.6.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
524
+ wbportfolio-1.52.6.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
525
+ wbportfolio-1.52.6.dist-info/RECORD,,