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

@@ -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
  }
@@ -267,15 +267,25 @@ class TestTradeProposal:
267
267
  asset_position_factory.create(
268
268
  portfolio=trade_proposal.portfolio, date=effective_date, underlying_instrument=i2, weighting=Decimal("0.3")
269
269
  )
270
- instrument_price_factory.create(instrument=i1, date=effective_date)
271
- instrument_price_factory.create(instrument=i2, date=effective_date)
272
- instrument_price_factory.create(instrument=i3, date=effective_date)
270
+ p1 = instrument_price_factory.create(instrument=i1, date=effective_date)
271
+ p2 = instrument_price_factory.create(instrument=i2, date=effective_date)
272
+ p3 = instrument_price_factory.create(instrument=i3, date=effective_date)
273
273
 
274
274
  # build the target portfolio
275
275
  target_portfolio = PortfolioDTO(
276
276
  positions=(
277
- Position(underlying_instrument=i2.id, date=trade_proposal.trade_date, weighting=Decimal("0.4")),
278
- Position(underlying_instrument=i3.id, date=trade_proposal.trade_date, weighting=Decimal("0.6")),
277
+ Position(
278
+ underlying_instrument=i2.id,
279
+ date=trade_proposal.trade_date,
280
+ weighting=Decimal("0.4"),
281
+ price=float(p2.net_value),
282
+ ),
283
+ Position(
284
+ underlying_instrument=i3.id,
285
+ date=trade_proposal.trade_date,
286
+ weighting=Decimal("0.6"),
287
+ price=float(p3.net_value),
288
+ ),
279
289
  )
280
290
  )
281
291
 
@@ -295,9 +305,24 @@ class TestTradeProposal:
295
305
  # build the target portfolio
296
306
  new_target_portfolio = PortfolioDTO(
297
307
  positions=(
298
- Position(underlying_instrument=i1.id, date=trade_proposal.trade_date, weighting=Decimal("0.2")),
299
- Position(underlying_instrument=i2.id, date=trade_proposal.trade_date, weighting=Decimal("0.3")),
300
- Position(underlying_instrument=i3.id, date=trade_proposal.trade_date, weighting=Decimal("0.5")),
308
+ Position(
309
+ underlying_instrument=i1.id,
310
+ date=trade_proposal.trade_date,
311
+ weighting=Decimal("0.2"),
312
+ price=float(p1.net_value),
313
+ ),
314
+ Position(
315
+ underlying_instrument=i2.id,
316
+ date=trade_proposal.trade_date,
317
+ weighting=Decimal("0.3"),
318
+ price=float(p2.net_value),
319
+ ),
320
+ Position(
321
+ underlying_instrument=i3.id,
322
+ date=trade_proposal.trade_date,
323
+ weighting=Decimal("0.5"),
324
+ price=float(p3.net_value),
325
+ ),
301
326
  )
302
327
  )
303
328
 
@@ -361,6 +386,8 @@ class TestTradeProposal:
361
386
  """
362
387
  portfolio = trade_proposal.portfolio
363
388
  instrument = instrument_factory.create(currency=portfolio.currency)
389
+ underlying_quote_price = instrument_price_factory.create(instrument=instrument, date=trade_proposal.trade_date)
390
+ mock_fct.return_value = Decimal(1_000_000) # 1 million cash
364
391
  trade = trade_factory.create(
365
392
  trade_proposal=trade_proposal,
366
393
  transaction_date=trade_proposal.trade_date,
@@ -368,17 +395,19 @@ class TestTradeProposal:
368
395
  underlying_instrument=instrument,
369
396
  )
370
397
  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
398
 
374
399
  # Assert estimated shares are correctly calculated
375
400
  assert (
376
- trade_proposal.get_estimated_shares(trade.weighting, trade.underlying_instrument)
401
+ trade_proposal.get_estimated_shares(
402
+ trade.weighting, trade.underlying_instrument, underlying_quote_price.net_value
403
+ )
377
404
  == Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
378
405
  )
379
406
 
380
407
  @patch.object(Portfolio, "get_total_asset_value")
381
408
  def test_get_estimated_target_cash(self, mock_fct, trade_proposal, trade_factory, cash_factory):
409
+ trade_proposal.portfolio.only_weighting = False
410
+ trade_proposal.portfolio.save()
382
411
  mock_fct.return_value = Decimal(1_000_000) # 1 million cash
383
412
  cash = cash_factory.create(currency=trade_proposal.portfolio.currency)
384
413
  trade_factory.create( # equity trade
@@ -410,3 +439,191 @@ class TestTradeProposal:
410
439
  tp2 = trade_proposal_factory.create(portfolio=portfolio, trade_date=tp.trade_date - BDay(1))
411
440
  instrument.refresh_from_db()
412
441
  assert instrument.inception_date == (tp2.trade_date + BDay(1)).date()
442
+
443
+ def test_get_round_lot_size(self, trade_proposal, instrument):
444
+ # without a round lot size, we expect no normalization of shares
445
+ assert trade_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
446
+ instrument.round_lot_size = 100
447
+ instrument.save()
448
+
449
+ # if instrument has a round lot size different than 1, we expect different behavior based on whether shares is positive or negative
450
+ assert trade_proposal.get_round_lot_size(Decimal(66.0), instrument) == Decimal("100")
451
+ assert trade_proposal.get_round_lot_size(Decimal(-66.0), instrument) == Decimal(-66.0)
452
+ assert trade_proposal.get_round_lot_size(Decimal(-120), instrument) == Decimal(-200)
453
+
454
+ # exchange can disable rounding based on the lot size
455
+ instrument.exchange.apply_round_lot_size = False
456
+ instrument.exchange.save()
457
+ assert trade_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
458
+
459
+ def test_submit_round_lot_size(self, trade_proposal, trade_factory, instrument):
460
+ trade_proposal.portfolio.only_weighting = False
461
+ trade_proposal.portfolio.save()
462
+ instrument.round_lot_size = 100
463
+ instrument.save()
464
+ trade = trade_factory.create(
465
+ status="DRAFT",
466
+ underlying_instrument=instrument,
467
+ shares=70,
468
+ trade_proposal=trade_proposal,
469
+ weighting=Decimal("1.0"),
470
+ )
471
+ warnings = trade_proposal.submit()
472
+ trade_proposal.save()
473
+ assert (
474
+ len(warnings) == 1
475
+ ) # ensure that submit returns a warning concerning the rounded trade based on the lot size
476
+ trade.refresh_from_db()
477
+ assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
478
+
479
+ def test_submit_round_fractional_shares(self, trade_proposal, trade_factory, instrument):
480
+ trade_proposal.portfolio.only_weighting = False
481
+ trade_proposal.portfolio.save()
482
+ trade = trade_factory.create(
483
+ status="DRAFT",
484
+ underlying_instrument=instrument,
485
+ shares=5.6,
486
+ trade_proposal=trade_proposal,
487
+ weighting=Decimal("1.0"),
488
+ )
489
+ trade_proposal.submit()
490
+ trade_proposal.save()
491
+ trade.refresh_from_db()
492
+ assert trade.shares == 6 # we expect the fractional share to be rounded
493
+
494
+ def test_ex_post(
495
+ self, instrument_factory, asset_position_factory, instrument_price_factory, trade_proposal_factory, portfolio
496
+ ):
497
+ """
498
+ Tests the ex-post rebalancing mechanism of a portfolio with two instruments.
499
+ Verifies that weights are correctly recalculated after submitting and approving a trade proposal.
500
+ """
501
+
502
+ # --- Create instruments ---
503
+ msft = instrument_factory.create(currency=portfolio.currency)
504
+ apple = instrument_factory.create(currency=portfolio.currency)
505
+
506
+ # --- Key dates ---
507
+ d1 = date(2025, 6, 24)
508
+ d2 = date(2025, 6, 25)
509
+ d3 = date(2025, 6, 26)
510
+ d4 = date(2025, 6, 27)
511
+
512
+ # --- Create MSFT prices ---
513
+ msft_p1 = instrument_price_factory.create(instrument=msft, date=d1, net_value=Decimal("10"))
514
+ msft_p2 = instrument_price_factory.create(instrument=msft, date=d2, net_value=Decimal("8"))
515
+ msft_p3 = instrument_price_factory.create(instrument=msft, date=d3, net_value=Decimal("12"))
516
+ msft_p4 = instrument_price_factory.create(instrument=msft, date=d4, net_value=Decimal("15")) # noqa
517
+
518
+ # Calculate MSFT returns between dates
519
+ msft_r2 = msft_p2.net_value / msft_p1.net_value - Decimal("1") # noqa
520
+ msft_r3 = msft_p3.net_value / msft_p2.net_value - Decimal("1")
521
+
522
+ # --- Create Apple prices (stable) ---
523
+ apple_p1 = instrument_price_factory.create(instrument=apple, date=d1, net_value=Decimal("100"))
524
+ apple_p2 = instrument_price_factory.create(instrument=apple, date=d2, net_value=Decimal("100"))
525
+ apple_p3 = instrument_price_factory.create(instrument=apple, date=d3, net_value=Decimal("100"))
526
+ apple_p4 = instrument_price_factory.create(instrument=apple, date=d4, net_value=Decimal("100")) # noqa
527
+
528
+ # Apple returns (always 0 since price is stable)
529
+ apple_r2 = apple_p2.net_value / apple_p1.net_value - Decimal("1") # noqa
530
+ apple_r3 = apple_p3.net_value / apple_p2.net_value - Decimal("1")
531
+
532
+ # --- Create positions on d2 ---
533
+ msft_a2 = asset_position_factory.create(
534
+ portfolio=portfolio,
535
+ underlying_quote=msft,
536
+ date=d2,
537
+ initial_shares=10,
538
+ weighting=Decimal("0.44"),
539
+ )
540
+ apple_a2 = asset_position_factory.create(
541
+ portfolio=portfolio,
542
+ underlying_quote=apple,
543
+ date=d2,
544
+ initial_shares=1,
545
+ weighting=Decimal("0.56"),
546
+ )
547
+
548
+ # Check that initial weights sum to 1
549
+ total_weight_d2 = msft_a2.weighting + apple_a2.weighting
550
+ assert total_weight_d2 == pytest.approx(Decimal("1.0"), abs=Decimal("1e-6"))
551
+
552
+ # --- Calculate total portfolio return between d2 and d3 ---
553
+ portfolio_r3 = msft_a2.weighting * (Decimal("1.0") + msft_r3) + apple_a2.weighting * (
554
+ Decimal("1.0") + apple_r3
555
+ )
556
+
557
+ # --- Create positions on d3 with weights adjusted for returns ---
558
+ msft_a3 = asset_position_factory.create(
559
+ portfolio=portfolio,
560
+ underlying_quote=msft,
561
+ date=d3,
562
+ initial_shares=10,
563
+ weighting=msft_a2.weighting * (Decimal("1.0") + msft_r3) / portfolio_r3,
564
+ )
565
+ apple_a3 = asset_position_factory.create(
566
+ portfolio=portfolio,
567
+ underlying_quote=apple,
568
+ date=d3,
569
+ initial_shares=1,
570
+ weighting=apple_a2.weighting * (Decimal("1.0") + apple_r3) / portfolio_r3,
571
+ )
572
+
573
+ # Check that weights on d3 sum to 1
574
+ total_weight_d3 = msft_a3.weighting + apple_a3.weighting
575
+ assert total_weight_d3 == pytest.approx(Decimal("1.0"), abs=Decimal("1e-6"))
576
+
577
+ # --- Create a trade proposal on d3 ---
578
+ trade_proposal = trade_proposal_factory.create(portfolio=portfolio, trade_date=d3)
579
+ trade_proposal.reset_trades()
580
+
581
+ # Retrieve trades for each instrument
582
+ trade_msft = trade_proposal.trades.get(underlying_instrument=msft)
583
+ trade_apple = trade_proposal.trades.get(underlying_instrument=apple)
584
+
585
+ # Check that trade weights are initially zero
586
+ assert trade_msft.weighting == Decimal("0")
587
+ assert trade_apple.weighting == Decimal("0")
588
+
589
+ # --- Adjust trade weights to target 50% each ---
590
+ target_weight = Decimal("0.5")
591
+ trade_msft.weighting = target_weight - msft_a3.weighting
592
+ trade_msft.save()
593
+
594
+ trade_apple.weighting = target_weight - apple_a3.weighting
595
+ trade_apple.save()
596
+
597
+ # --- Check drift factors and effective weights ---
598
+ assert trade_msft.drift_factor == pytest.approx(msft_a3.weighting / msft_a2.weighting, abs=Decimal("1e-6"))
599
+ assert trade_apple.drift_factor == pytest.approx(apple_a3.weighting / apple_a2.weighting, abs=Decimal("1e-6"))
600
+
601
+ assert trade_msft._effective_weight == pytest.approx(msft_a3.weighting, abs=Decimal("1e-6"))
602
+ assert trade_apple._effective_weight == pytest.approx(apple_a3.weighting, abs=Decimal("1e-6"))
603
+
604
+ # Check that the target weight is the sum of drifted weight and adjustment
605
+ assert trade_msft._target_weight == pytest.approx(
606
+ msft_a2.weighting * trade_msft.drift_factor + trade_msft.weighting,
607
+ abs=Decimal("1e-6"),
608
+ )
609
+ assert trade_msft._target_weight == pytest.approx(target_weight, abs=Decimal("1e-6"))
610
+
611
+ assert trade_apple._target_weight == pytest.approx(
612
+ apple_a2.weighting * trade_apple.drift_factor + trade_apple.weighting,
613
+ abs=Decimal("1e-6"),
614
+ )
615
+ assert trade_apple._target_weight == pytest.approx(target_weight, abs=Decimal("1e-6"))
616
+
617
+ # --- Submit and approve the trade proposal ---
618
+ trade_proposal.submit()
619
+ trade_proposal.save()
620
+ trade_proposal.approve()
621
+ trade_proposal.save()
622
+
623
+ # --- Refresh positions after ex-post rebalancing ---
624
+ msft_a3.refresh_from_db()
625
+ apple_a3.refresh_from_db()
626
+
627
+ # Final check that weights have been updated to 50%
628
+ assert msft_a3.weighting == pytest.approx(target_weight, abs=Decimal("1e-6"))
629
+ assert apple_a3.weighting == pytest.approx(target_weight, abs=Decimal("1e-6"))
@@ -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,9 @@ 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"),
529
+ security=F("underlying_instrument__parent"),
530
+ company=F("underlying_instrument__parent__parent"),
523
531
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.52.5
3
+ Version: 1.53.0
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*