wbportfolio 1.54.22__py2.py3-none-any.whl → 1.55.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.

Files changed (79) hide show
  1. wbportfolio/admin/indexes.py +1 -1
  2. wbportfolio/admin/product_groups.py +1 -1
  3. wbportfolio/admin/products.py +2 -1
  4. wbportfolio/admin/rebalancing.py +1 -1
  5. wbportfolio/api_clients/__init__.py +0 -0
  6. wbportfolio/api_clients/ubs.py +150 -0
  7. wbportfolio/factories/orders/order_proposals.py +3 -1
  8. wbportfolio/factories/orders/orders.py +10 -2
  9. wbportfolio/factories/portfolios.py +1 -1
  10. wbportfolio/factories/rebalancing.py +1 -1
  11. wbportfolio/filters/assets.py +10 -2
  12. wbportfolio/filters/orders/__init__.py +1 -0
  13. wbportfolio/filters/orders/order_proposals.py +58 -0
  14. wbportfolio/filters/portfolios.py +20 -0
  15. wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
  16. wbportfolio/import_export/backends/ubs/fees.py +10 -20
  17. wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
  18. wbportfolio/import_export/backends/utils.py +0 -17
  19. wbportfolio/import_export/handlers/asset_position.py +1 -1
  20. wbportfolio/import_export/handlers/orders.py +1 -1
  21. wbportfolio/import_export/parsers/sg_lux/equity.py +1 -1
  22. wbportfolio/import_export/parsers/societe_generale/strategy.py +2 -2
  23. wbportfolio/import_export/parsers/ubs/equity.py +1 -1
  24. wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
  25. wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
  26. wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
  27. wbportfolio/models/asset.py +6 -20
  28. wbportfolio/models/builder.py +74 -31
  29. wbportfolio/models/mixins/instruments.py +7 -0
  30. wbportfolio/models/orders/order_proposals.py +549 -167
  31. wbportfolio/models/orders/orders.py +24 -11
  32. wbportfolio/models/orders/routing.py +54 -0
  33. wbportfolio/models/portfolio.py +77 -41
  34. wbportfolio/models/products.py +9 -0
  35. wbportfolio/models/rebalancing.py +6 -6
  36. wbportfolio/models/transactions/transactions.py +10 -6
  37. wbportfolio/order_routing/__init__.py +19 -0
  38. wbportfolio/order_routing/adapters/__init__.py +57 -0
  39. wbportfolio/order_routing/adapters/ubs.py +161 -0
  40. wbportfolio/pms/trading/handler.py +4 -1
  41. wbportfolio/pms/typing.py +62 -8
  42. wbportfolio/rebalancing/models/composite.py +1 -1
  43. wbportfolio/rebalancing/models/equally_weighted.py +3 -3
  44. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -1
  45. wbportfolio/rebalancing/models/model_portfolio.py +9 -10
  46. wbportfolio/serializers/orders/order_proposals.py +25 -21
  47. wbportfolio/serializers/orders/orders.py +5 -2
  48. wbportfolio/serializers/positions.py +2 -2
  49. wbportfolio/serializers/rebalancing.py +1 -1
  50. wbportfolio/tests/conftest.py +6 -2
  51. wbportfolio/tests/models/orders/test_order_proposals.py +90 -30
  52. wbportfolio/tests/models/test_imports.py +5 -3
  53. wbportfolio/tests/models/test_portfolios.py +57 -23
  54. wbportfolio/tests/models/test_products.py +11 -0
  55. wbportfolio/tests/models/transactions/test_rebalancing.py +2 -2
  56. wbportfolio/tests/rebalancing/test_models.py +3 -5
  57. wbportfolio/tests/signals.py +0 -10
  58. wbportfolio/tests/tests.py +2 -0
  59. wbportfolio/viewsets/__init__.py +7 -4
  60. wbportfolio/viewsets/assets.py +1 -215
  61. wbportfolio/viewsets/charts/__init__.py +6 -1
  62. wbportfolio/viewsets/charts/assets.py +341 -155
  63. wbportfolio/viewsets/configs/buttons/products.py +32 -2
  64. wbportfolio/viewsets/configs/display/assets.py +6 -19
  65. wbportfolio/viewsets/configs/display/products.py +1 -1
  66. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  67. wbportfolio/viewsets/configs/menu/__init__.py +1 -0
  68. wbportfolio/viewsets/configs/menu/orders.py +11 -0
  69. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +66 -12
  70. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +55 -9
  71. wbportfolio/viewsets/orders/configs/displays/orders.py +13 -0
  72. wbportfolio/viewsets/orders/order_proposals.py +47 -7
  73. wbportfolio/viewsets/orders/orders.py +31 -29
  74. wbportfolio/viewsets/portfolios.py +3 -3
  75. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/METADATA +3 -1
  76. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/RECORD +78 -68
  77. wbportfolio/viewsets/signals.py +0 -43
  78. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/WHEEL +0 -0
  79. {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,6 +4,7 @@ from decimal import Decimal
4
4
  from unittest.mock import call, patch
5
5
 
6
6
  import pytest
7
+ from django.db.models import Sum
7
8
  from faker import Faker
8
9
  from pandas._libs.tslibs.offsets import BDay, BusinessMonthEnd
9
10
 
@@ -50,31 +51,36 @@ class TestOrderProposal:
50
51
  )
51
52
 
52
53
  # Create orders for testing
53
- t1 = order_factory.create(
54
+ o1 = order_factory.create(
54
55
  order_proposal=order_proposal,
55
56
  weighting=Decimal("0.05"),
56
57
  portfolio=order_proposal.portfolio,
57
58
  underlying_instrument=a1.underlying_quote,
58
59
  )
59
- t2 = order_factory.create(
60
+ o2 = order_factory.create(
60
61
  order_proposal=order_proposal,
61
62
  weighting=Decimal("-0.05"),
62
63
  portfolio=order_proposal.portfolio,
63
64
  underlying_instrument=a2.underlying_quote,
64
65
  )
65
66
 
67
+ r1 = o1.price / a1.initial_price - Decimal("1")
68
+ r2 = o2.price / a2.initial_price - Decimal("1")
69
+ p_return = a1.weighting * (Decimal("1") + r1) + a2.weighting * (Decimal("1") + r2)
66
70
  # Get the validated trading service
67
- validated_trading_service = order_proposal.validated_trading_service
71
+ trades = order_proposal.validated_trading_service.trades_batch.trades_map
72
+ t1 = trades[a1.underlying_quote.id]
73
+ t2 = trades[a2.underlying_quote.id]
68
74
 
69
75
  # Assert effective and target portfolios are as expected
70
- assert validated_trading_service._effective_portfolio.to_dict() == {
71
- a1.underlying_quote.id: a1.weighting,
72
- a2.underlying_quote.id: a2.weighting,
73
- }
74
- assert validated_trading_service._target_portfolio.to_dict() == {
75
- a1.underlying_quote.id: a1.weighting + t1.weighting,
76
- a2.underlying_quote.id: a2.weighting + t2.weighting,
77
- }
76
+ assert t1.previous_weight == a1.weighting
77
+ assert t2.previous_weight == a2.weighting
78
+ assert t1.target_weight == pytest.approx(
79
+ a1.weighting * ((r1 + Decimal("1")) / p_return) + o1.weighting, abs=Decimal("1e-8")
80
+ )
81
+ assert t2.target_weight == pytest.approx(
82
+ a2.weighting * ((r2 + Decimal("1")) / p_return) + o2.weighting, abs=Decimal("1e-8")
83
+ )
78
84
 
79
85
  # Test the calculation of the last effective date
80
86
  def test_last_effective_date(self, order_proposal, asset_position_factory):
@@ -112,13 +118,13 @@ class TestOrderProposal:
112
118
  """
113
119
  tp = order_proposal_factory.create()
114
120
  tp_previous_submit = order_proposal_factory.create( # noqa
115
- portfolio=tp.portfolio, status=OrderProposal.Status.SUBMIT, trade_date=(tp.trade_date - BDay(1)).date()
121
+ portfolio=tp.portfolio, status=OrderProposal.Status.PENDING, trade_date=(tp.trade_date - BDay(1)).date()
116
122
  )
117
123
  tp_previous_approve = order_proposal_factory.create(
118
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(2)).date()
124
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date - BDay(2)).date()
119
125
  )
120
126
  tp_next_approve = order_proposal_factory.create( # noqa
121
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(1)).date()
127
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date + BDay(1)).date()
122
128
  )
123
129
 
124
130
  # The previous valid order proposal should be the approved one strictly before the current proposal
@@ -133,13 +139,13 @@ class TestOrderProposal:
133
139
  """
134
140
  tp = order_proposal_factory.create()
135
141
  tp_previous_approve = order_proposal_factory.create( # noqa
136
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
142
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date - BDay(1)).date()
137
143
  )
138
144
  tp_next_submit = order_proposal_factory.create( # noqa
139
- portfolio=tp.portfolio, status=OrderProposal.Status.SUBMIT, trade_date=(tp.trade_date + BDay(1)).date()
145
+ portfolio=tp.portfolio, status=OrderProposal.Status.PENDING, trade_date=(tp.trade_date + BDay(1)).date()
140
146
  )
141
147
  tp_next_approve = order_proposal_factory.create(
142
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(2)).date()
148
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date + BDay(2)).date()
143
149
  )
144
150
 
145
151
  # The next valid order proposal should be the approved one strictly after the current proposal
@@ -249,11 +255,12 @@ class TestOrderProposal:
249
255
 
250
256
  # Test resetting orders
251
257
  def test_reset_orders(
252
- self, order_proposal, instrument_factory, cash, instrument_price_factory, asset_position_factory
258
+ self, order_proposal, instrument_factory, cash_factory, instrument_price_factory, asset_position_factory
253
259
  ):
254
260
  """
255
261
  Verify orders are correctly reset based on effective and target portfolios.
256
262
  """
263
+ cash = cash_factory.create()
257
264
  effective_date = order_proposal.last_effective_date
258
265
 
259
266
  # Create instruments for testing
@@ -361,15 +368,12 @@ class TestOrderProposal:
361
368
  assert order_proposal.orders.get(underlying_instrument=i2).weighting == Decimal("-0.3")
362
369
  assert order_proposal.orders.get(underlying_instrument=cash).weighting == Decimal("0.5")
363
370
 
364
- def test_reset_orders_remove_invalid_orders(self, order_proposal, order_factory, instrument_price_factory):
371
+ def test_reset_orders_remove_invalid_orders(self, order_proposal, order_factory):
365
372
  # create a invalid trade and its price
366
373
  invalid_trade = order_factory.create(order_proposal=order_proposal, weighting=Decimal(0))
367
- instrument_price_factory.create(date=invalid_trade.value_date, instrument=invalid_trade.underlying_instrument)
368
374
 
369
375
  # create a valid trade and its price
370
376
  valid_trade = order_factory.create(order_proposal=order_proposal, weighting=Decimal(1))
371
- instrument_price_factory.create(date=valid_trade.value_date, instrument=valid_trade.underlying_instrument)
372
-
373
377
  order_proposal.reset_orders()
374
378
  assert order_proposal.orders.get(underlying_instrument=valid_trade.underlying_instrument).weighting == Decimal(
375
379
  "1"
@@ -386,15 +390,15 @@ class TestOrderProposal:
386
390
  mock_fct.return_value = iter([])
387
391
 
388
392
  # Create approved order proposals for testing
389
- tp0 = order_proposal_factory.create(status=OrderProposal.Status.APPROVED)
393
+ tp0 = order_proposal_factory.create(status=OrderProposal.Status.APPLIED)
390
394
  tp1 = order_proposal_factory.create(
391
395
  portfolio=tp0.portfolio,
392
- status=OrderProposal.Status.APPROVED,
396
+ status=OrderProposal.Status.APPLIED,
393
397
  trade_date=(tp0.trade_date + BusinessMonthEnd(1)).date(),
394
398
  )
395
399
  tp2 = order_proposal_factory.create(
396
400
  portfolio=tp0.portfolio,
397
- status=OrderProposal.Status.APPROVED,
401
+ status=OrderProposal.Status.APPLIED,
398
402
  trade_date=(tp1.trade_date + BusinessMonthEnd(1)).date(),
399
403
  )
400
404
 
@@ -496,9 +500,10 @@ class TestOrderProposal:
496
500
  instrument.exchange.save()
497
501
  assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
498
502
 
499
- def test_submit_round_lot_size(self, order_proposal, order_factory, instrument):
503
+ def test_submit_round_lot_size(self, order_proposal, order_factory, instrument_factory):
500
504
  order_proposal.portfolio.only_weighting = False
501
505
  order_proposal.portfolio.save()
506
+ instrument = instrument_factory.create()
502
507
  instrument.round_lot_size = 100
503
508
  instrument.save()
504
509
  trade = order_factory.create(
@@ -515,11 +520,10 @@ class TestOrderProposal:
515
520
  trade.refresh_from_db()
516
521
  assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
517
522
 
518
- def test_submit_round_fractional_shares(self, order_proposal, order_factory, instrument):
523
+ def test_submit_round_fractional_shares(self, order_proposal, order_factory):
519
524
  order_proposal.portfolio.only_weighting = False
520
525
  order_proposal.portfolio.save()
521
526
  trade = order_factory.create(
522
- underlying_instrument=instrument,
523
527
  shares=5.6,
524
528
  order_proposal=order_proposal,
525
529
  weighting=Decimal("1.0"),
@@ -637,6 +641,7 @@ class TestOrderProposal:
637
641
  order_proposal.submit()
638
642
  order_proposal.save()
639
643
  order_proposal.approve()
644
+ order_proposal.apply()
640
645
  order_proposal.save()
641
646
 
642
647
  # Final check that weights have been updated to 50%
@@ -648,8 +653,9 @@ class TestOrderProposal:
648
653
  )
649
654
 
650
655
  def test_replay_reset_draft_order_proposal(
651
- self, instrument, instrument_price_factory, order_factory, order_proposal_factory
656
+ self, instrument_factory, instrument_price_factory, order_factory, order_proposal_factory
652
657
  ):
658
+ instrument = instrument_factory.create()
653
659
  order_proposal = order_proposal_factory.create(trade_date=date.today() - BDay(2))
654
660
  instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(2))
655
661
  instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(1))
@@ -660,7 +666,8 @@ class TestOrderProposal:
660
666
  weighting=1,
661
667
  )
662
668
  order_proposal.submit()
663
- order_proposal.approve(replay=False)
669
+ order_proposal.approve()
670
+ order_proposal.apply(replay=False)
664
671
  order_proposal.save()
665
672
 
666
673
  draft_tp = order_proposal_factory.create(portfolio=order_proposal.portfolio, trade_date=date.today() - BDay(1))
@@ -687,3 +694,56 @@ class TestOrderProposal:
687
694
  order.save()
688
695
  assert order.shares == Decimal(0)
689
696
  assert order.weighting == Decimal(0)
697
+
698
+ def test_reset_order_use_desired_target_weight(self, order_proposal, order_factory):
699
+ order1 = order_factory.create(
700
+ order_proposal=order_proposal, weighting=Decimal("0.5"), desired_target_weight=Decimal("0.7")
701
+ )
702
+ order2 = order_factory.create(
703
+ order_proposal=order_proposal, weighting=Decimal("0.5"), desired_target_weight=Decimal("0.3")
704
+ )
705
+ order_proposal.submit()
706
+ order_proposal.approve()
707
+ order_proposal.apply()
708
+ order_proposal.save()
709
+
710
+ order1.refresh_from_db()
711
+ order2.refresh_from_db()
712
+ assert order1.desired_target_weight == Decimal("0.5")
713
+ assert order2.desired_target_weight == Decimal("0.5")
714
+ assert order1.weighting == Decimal("0.5")
715
+ assert order2.weighting == Decimal("0.5")
716
+
717
+ order1.desired_target_weight = Decimal("0.7")
718
+ order2.desired_target_weight = Decimal("0.3")
719
+ order1.save()
720
+ order2.save()
721
+
722
+ order_proposal.reset_orders(use_desired_target_weight=True)
723
+ order1.refresh_from_db()
724
+ order2.refresh_from_db()
725
+ assert order1.weighting == Decimal("0.7")
726
+ assert order2.weighting == Decimal("0.3")
727
+
728
+ def test_reset_order_proposal_keeps_target_cash_weight(self, order_factory, order_proposal_factory):
729
+ order_proposal = order_proposal_factory.create(
730
+ total_cash_weight=Decimal("0.02")
731
+ ) # create a OP with total cash weight of 2%
732
+
733
+ # create orders that total weight account for only 50%
734
+ order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.3"))
735
+ order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.2"))
736
+
737
+ order_proposal.reset_orders()
738
+ assert order_proposal.get_orders().aggregate(s=Sum("target_weight"))["s"] == Decimal(
739
+ "0.98"
740
+ ), "The total target weight leftover does not equal the stored total cash weight"
741
+
742
+ def test_convert_to_portfolio_always_100percent(self, order_proposal, order_factory):
743
+ o1 = order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.5"))
744
+ o2 = order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.3"))
745
+
746
+ portfolio = order_proposal.convert_to_portfolio()
747
+ assert portfolio.positions_map[o1.underlying_instrument.id].weighting == Decimal("0.5")
748
+ assert portfolio.positions_map[o2.underlying_instrument.id].weighting == Decimal("0.3")
749
+ assert portfolio.positions_map[order_proposal.cash_component.id].weighting == Decimal("0.2")
@@ -137,7 +137,9 @@ class TestImportMixinModel:
137
137
  data = {
138
138
  "data": [
139
139
  self._serialize_position(
140
- asset_position_factory.build(date=val_date, underlying_instrument=instrument),
140
+ asset_position_factory.build(
141
+ date=val_date, underlying_instrument=instrument, weighting=Decimal("0.25")
142
+ ),
141
143
  product_portfolio,
142
144
  instrument,
143
145
  )
@@ -171,7 +173,7 @@ class TestImportMixinModel:
171
173
  def test_import_assetposition_product_group(
172
174
  self, import_source, product_group, currency, equity, asset_position_factory
173
175
  ):
174
- positions = asset_position_factory.build(underlying_instrument=equity)
176
+ positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
175
177
  data = {"data": [self._serialize_position(positions, product_group, equity)]}
176
178
 
177
179
  # Import non existing data
@@ -183,7 +185,7 @@ class TestImportMixinModel:
183
185
  def test_import_assetposition_index(
184
186
  self, import_source, index, portfolio, currency, equity, asset_position_factory
185
187
  ):
186
- positions = asset_position_factory.build(underlying_instrument=equity)
188
+ positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
187
189
  index.portfolios.add(portfolio)
188
190
  data = {"data": [self._serialize_position(positions, index, equity)]}
189
191
 
@@ -10,6 +10,7 @@ from django.forms.models import model_to_dict
10
10
  from faker import Faker
11
11
  from pandas.tseries.offsets import BDay
12
12
  from psycopg.types.range import DateRange
13
+ from wbcore.contrib.currency.factories import CurrencyFactory
13
14
  from wbcore.contrib.geography.factories import CountryFactory
14
15
 
15
16
  from wbportfolio.models import (
@@ -106,11 +107,10 @@ class TestPortfolioModel(PortfolioTestMixin):
106
107
  )
107
108
  assert portfolio.get_geographical_breakdown(weekday).shape[0] == 2
108
109
 
109
- def test_get_currency_exposure(self, portfolio, asset_position_factory, currency_factory, equity_factory, weekday):
110
+ def test_get_currency_exposure(self, portfolio, asset_position_factory, equity_factory, weekday):
110
111
  a1 = asset_position_factory.create(
111
112
  portfolio=portfolio,
112
- underlying_instrument=equity_factory.create(),
113
- currency=currency_factory.create(),
113
+ underlying_instrument=equity_factory.create(currency=CurrencyFactory.create()),
114
114
  date=weekday,
115
115
  )
116
116
  asset_position_factory.create(
@@ -118,8 +118,7 @@ class TestPortfolioModel(PortfolioTestMixin):
118
118
  )
119
119
  asset_position_factory.create(
120
120
  portfolio=portfolio,
121
- underlying_instrument=equity_factory.create(),
122
- currency=currency_factory.create(),
121
+ underlying_instrument=equity_factory.create(currency=CurrencyFactory.create()),
123
122
  date=weekday,
124
123
  )
125
124
  assert portfolio.get_currency_exposure(weekday).shape[0] == 2
@@ -253,14 +252,14 @@ class TestPortfolioModel(PortfolioTestMixin):
253
252
 
254
253
  @patch.object(Portfolio, "estimate_net_asset_values", autospec=True)
255
254
  def test_change_at_date(self, mock_estimate_net_asset_values, asset_position_factory, portfolio, weekday):
256
- asset_position_factory.create_batch(10, portfolio=portfolio, date=weekday)
255
+ a1 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.1"))
256
+ a2 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.3"))
257
+ a3 = asset_position_factory.create(portfolio=portfolio, date=weekday, weighting=Decimal("0.5"))
257
258
 
258
- portfolio.change_at_date(weekday, recompute_weighting=True)
259
+ portfolio.change_at_date(weekday, fix_quantization=True)
259
260
 
260
- # test that change at date normalize the weighting
261
- total_value = AssetPosition.objects.aggregate(s=Sum("total_value_fx_portfolio"))["s"]
262
- for pos in AssetPosition.objects.all():
263
- assert float(pos.weighting) == pytest.approx(float(pos.total_value_fx_portfolio / total_value), rel=1e-2)
261
+ cash_pos = portfolio.assets.get(date=weekday, portfolio=portfolio, underlying_quote=portfolio.cash_component)
262
+ assert cash_pos.weighting == Decimal("1.0") - (a1.weighting + a2.weighting + a3.weighting)
264
263
 
265
264
  mock_estimate_net_asset_values.assert_called_once_with(
266
265
  portfolio, (weekday + BDay(1)).date(), analytic_portfolio=None
@@ -530,8 +529,9 @@ class TestPortfolioModel(PortfolioTestMixin):
530
529
  assert res1
531
530
 
532
531
  def test_get_total_asset_under_management(
533
- self, portfolio, customer_trade_factory, instrument_factory, instrument_price_factory, weekday
532
+ self, portfolio_factory, customer_trade_factory, instrument_factory, instrument_price_factory, weekday
534
533
  ):
534
+ portfolio = portfolio_factory.create()
535
535
  i1 = instrument_factory.create()
536
536
  i2 = instrument_factory.create()
537
537
  previous_day = (weekday - BDay(5)).date()
@@ -1001,8 +1001,8 @@ class TestPortfolioModel(PortfolioTestMixin):
1001
1001
  fx_portfolio = currency_fx_rates_factory.create(currency=portfolio.currency, date=weekday)
1002
1002
  fx_instrument = currency_fx_rates_factory.create(currency=instrument.currency, date=weekday)
1003
1003
  instrument_id: int = instrument.id
1004
- weights = {instrument_id: random.random()}
1005
- portfolio.builder.set_prices({weekday: {instrument_id: p.net_value}})
1004
+ weights = {instrument_id: Decimal(random.random())}
1005
+ portfolio.builder.prices = {weekday: {instrument_id: p.net_value}}
1006
1006
  portfolio.builder.add((weekday, weights), infer_underlying_quote_price=False)
1007
1007
 
1008
1008
  res = list(portfolio.builder.get_positions())
@@ -1012,7 +1012,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1012
1012
  assert a.underlying_quote == instrument
1013
1013
  assert a.underlying_quote_price == None
1014
1014
  assert a.initial_price == p.net_value
1015
- assert a.weighting == pytest.approx(weights[instrument.id], abs=10e-6)
1015
+ assert a.weighting == pytest.approx(weights[instrument.id], abs=Decimal(10e-6))
1016
1016
  assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
1017
1017
  assert a.currency_fx_rate_instrument_to_usd == fx_instrument
1018
1018
 
@@ -1057,7 +1057,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1057
1057
  except StopIteration as e:
1058
1058
  rebalancing_order_proposal = e.value
1059
1059
  assert rebalancing_order_proposal.trade_date == rebalancing_date
1060
- assert rebalancing_order_proposal.status == "SUBMIT"
1060
+ assert rebalancing_order_proposal.status == "APPROVED"
1061
1061
 
1062
1062
  # we expect a equally rebalancing (default) so both orders needs to be created
1063
1063
  orders = rebalancing_order_proposal.get_orders()
@@ -1067,8 +1067,8 @@ class TestPortfolioModel(PortfolioTestMixin):
1067
1067
  assert t2._target_weight == Decimal("0.5")
1068
1068
 
1069
1069
  # we approve the rebalancing order proposal
1070
- assert rebalancing_order_proposal.status == "SUBMIT"
1071
- rebalancing_order_proposal.approve(replay=False)
1070
+ assert rebalancing_order_proposal.status == "APPROVED"
1071
+ rebalancing_order_proposal.apply(replay=False)
1072
1072
  rebalancing_order_proposal.save()
1073
1073
 
1074
1074
  # check that the rebalancing was applied and position reflect that
@@ -1084,17 +1084,17 @@ class TestPortfolioModel(PortfolioTestMixin):
1084
1084
  a1 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i1)
1085
1085
 
1086
1086
  # check initial creation
1087
- portfolio.builder.add([a1]).bulk_create_positions(compute_metrics=True)
1087
+ portfolio.builder.add([a1]).bulk_create_positions(fix_quantization=False, compute_metrics=True)
1088
1088
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a1.weighting
1089
1089
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i1
1090
1090
 
1091
- # check that if we change key value, an already exising position will be updated accordingly
1091
+ # check that if we change key value, an already existing position will be updated accordingly
1092
1092
  a1.weighting = Decimal(0.5)
1093
- portfolio.builder.add([a1]).bulk_create_positions()
1093
+ portfolio.builder.add([a1]).bulk_create_positions(fix_quantization=False)
1094
1094
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == Decimal(0.5)
1095
1095
 
1096
1096
  a2 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i2)
1097
- portfolio.builder.add([a2]).bulk_create_positions()
1097
+ portfolio.builder.add([a2]).bulk_create_positions(fix_quantization=False)
1098
1098
  assert (
1099
1099
  AssetPosition.objects.get(portfolio=portfolio, date=weekday, underlying_instrument=i1).weighting
1100
1100
  == a1.weighting
@@ -1105,7 +1105,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1105
1105
  )
1106
1106
 
1107
1107
  a3 = asset_position_factory.build(date=weekday, portfolio=portfolio, underlying_instrument=i3)
1108
- portfolio.builder.add([a3]).bulk_create_positions(delete_leftovers=True)
1108
+ portfolio.builder.add([a3]).bulk_create_positions(delete_leftovers=True, fix_quantization=False)
1109
1109
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).weighting == a3.weighting
1110
1110
  assert AssetPosition.objects.get(portfolio=portfolio, date=weekday).underlying_instrument == i3
1111
1111
 
@@ -1136,3 +1136,37 @@ class TestPortfolioModel(PortfolioTestMixin):
1136
1136
 
1137
1137
  primary_portfolio.handle_controlling_portfolio_change_at_date(weekday)
1138
1138
  mock_compute_lookthrough.assert_called_once_with(lookthrough_portfolio, weekday)
1139
+
1140
+ def test_get_model_portfolio_relationships(self, portfolio_factory, asset_position_factory, weekday):
1141
+ model_portfolio = portfolio_factory.create()
1142
+ model_index = model_portfolio.get_or_create_index()
1143
+ dependent_portfolio = portfolio_factory.create()
1144
+ re1 = PortfolioPortfolioThroughModel.objects.create(
1145
+ portfolio=dependent_portfolio,
1146
+ dependency_portfolio=model_portfolio,
1147
+ type=PortfolioPortfolioThroughModel.Type.MODEL,
1148
+ )
1149
+
1150
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
1151
+ re1,
1152
+ }
1153
+ parent_portfolio = portfolio_factory.create()
1154
+ child_portfolio = portfolio_factory.create()
1155
+ re2 = PortfolioPortfolioThroughModel.objects.create(
1156
+ portfolio=child_portfolio,
1157
+ dependency_portfolio=parent_portfolio,
1158
+ type=PortfolioPortfolioThroughModel.Type.MODEL,
1159
+ )
1160
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
1161
+ re1,
1162
+ } # child portfolio is not considered in the tree because there is no position yet
1163
+
1164
+ asset_position_factory.create(portfolio=parent_portfolio, underlying_instrument=model_index, date=weekday)
1165
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {re1, re2}
1166
+
1167
+ dependent_portfolio.is_active = False # disable this portfolio
1168
+ dependent_portfolio.deletion_datetime = weekday - timedelta(days=1)
1169
+ dependent_portfolio.save()
1170
+ assert set(model_portfolio.get_model_portfolio_relationships(weekday)) == {
1171
+ re2,
1172
+ }
@@ -215,3 +215,14 @@ class TestProductModel(PortfolioTestMixin):
215
215
  body=f"The product {product} will be terminated on the {product.delisted_date:%Y-%m-%d}",
216
216
  user=internal_user,
217
217
  )
218
+
219
+ def test_delist_product_disable_report(self, product, report_factory):
220
+ report = report_factory.create(content_object=product, is_active=True)
221
+ assert product.delisted_date is None
222
+ assert report.is_active
223
+
224
+ product.delisted_date = datetime.date.today()
225
+ product.save()
226
+
227
+ report.refresh_from_db()
228
+ assert report.is_active is False
@@ -47,7 +47,7 @@ class TestRebalancer:
47
47
  def test_evaluate_rebalancing(
48
48
  self, weekday, rebalancer_factory, asset_position_factory, instrument_factory, instrument_price_factory
49
49
  ):
50
- rebalancer = rebalancer_factory.create(approve_order_proposal_automatically=True)
50
+ rebalancer = rebalancer_factory.create(apply_order_proposal_automatically=True)
51
51
  trade_date = (weekday + BDay(1)).date()
52
52
 
53
53
  i1 = instrument_factory.create()
@@ -65,7 +65,7 @@ class TestRebalancer:
65
65
  )
66
66
  order_proposal = rebalancer.evaluate_rebalancing(trade_date)
67
67
  assert order_proposal.orders.count() == 2
68
- assert order_proposal.status == OrderProposal.Status.APPROVED
68
+ assert order_proposal.status == OrderProposal.Status.APPLIED
69
69
  assert AssetPosition.objects.get(
70
70
  portfolio=rebalancer.portfolio, date=trade_date, underlying_quote=a1.underlying_instrument
71
71
  ).weighting == Decimal(0.5)
@@ -56,9 +56,7 @@ class TestModelPortfolioRebalancing:
56
56
  asset_position_factory.create(portfolio=model.portfolio, date=model.last_effective_date)
57
57
  assert not model.is_valid()
58
58
 
59
- a = asset_position_factory.create(portfolio=model.model_portfolio, date=model.last_effective_date)
60
- assert not model.is_valid()
61
- instrument_price_factory.create(instrument=a.underlying_quote, date=model.trade_date)
59
+ asset_position_factory.create(portfolio=model.model_portfolio, date=model.last_effective_date)
62
60
  assert model.is_valid()
63
61
 
64
62
  def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
@@ -84,7 +82,7 @@ class TestCompositeRebalancing:
84
82
  assert not model.is_valid()
85
83
 
86
84
  order_proposal = OrderProposalFactory.create(
87
- portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.APPROVED
85
+ portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.APPLIED
88
86
  )
89
87
  t1 = OrderFactory.create(
90
88
  portfolio=model.portfolio,
@@ -106,7 +104,7 @@ class TestCompositeRebalancing:
106
104
 
107
105
  def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
108
106
  order_proposal = OrderProposalFactory.create(
109
- portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.APPROVED
107
+ portfolio=model.portfolio, trade_date=model.last_effective_date, status=OrderProposal.Status.APPLIED
110
108
  )
111
109
  asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
112
110
  asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
@@ -17,8 +17,6 @@ from wbportfolio.viewsets import (
17
17
  ClaimEntryModelViewSet,
18
18
  CustodianDistributionInstrumentChartViewSet,
19
19
  CustomerDistributionInstrumentChartViewSet,
20
- DistributionChartViewSet,
21
- DistributionTableViewSet,
22
20
  NominalProductChartView,
23
21
  OrderOrderProposalModelViewSet,
24
22
  OrderProposalPortfolioModelViewSet,
@@ -96,14 +94,6 @@ def receive_kwargs_portfolio_role_instrument(sender, *args, **kwargs):
96
94
  return {}
97
95
 
98
96
 
99
- @receiver(custom_update_kwargs, sender=DistributionChartViewSet)
100
- @receiver(custom_update_kwargs, sender=DistributionTableViewSet)
101
- def receive_kwargs_distribution_chart_instrument_id(sender, *args, **kwargs):
102
- if instrument_id := kwargs.get("underlying_instrument_id"):
103
- return {"instrument_id": instrument_id}
104
- return {}
105
-
106
-
107
97
  @receiver(custom_update_kwargs, sender=ProductPerformanceFeesModelViewSet)
108
98
  def receive_kwargs_product_performance_fees(sender, *args, **kwargs):
109
99
  CurrencyFXRatesFactory()
@@ -19,6 +19,8 @@ for key, value in default_config.items():
19
19
  "AccountReconciliationLine",
20
20
  "TopDownPortfolioCompositionPandasAPIView",
21
21
  "CompositionModelPortfolioPandasView",
22
+ "DistributionChartViewSet",
23
+ "DistributionTableViewSet",
22
24
  # "ClaimModelViewSet",
23
25
  # "ClaimModelSerializer",
24
26
  ],
@@ -2,12 +2,16 @@ from .assets import (
2
2
  AssetPositionInstrumentModelViewSet,
3
3
  AssetPositionModelViewSet,
4
4
  AssetPositionPortfolioModelViewSet,
5
- AssetPositionUnderlyingInstrumentChartViewSet,
6
5
  CashPositionPortfolioPandasAPIView,
7
6
  CompositionModelPortfolioPandasView,
8
- ContributorPortfolioChartView,
9
7
  )
10
- from .charts import *
8
+ from .charts import (
9
+ DistributionChartViewSet,
10
+ DistributionTableViewSet,
11
+ AssetPositionUnderlyingInstrumentChartViewSet,
12
+ ContributorPortfolioChartView
13
+
14
+ )
11
15
  from .custodians import CustodianModelViewSet, CustodianRepresentationViewSet
12
16
  from .portfolio_relationship import InstrumentPreferedClassificationThroughProductModelViewSet
13
17
  from .portfolios import (
@@ -23,7 +27,6 @@ from .positions import (
23
27
  )
24
28
  from .registers import RegisterModelViewSet, RegisterRepresentationViewSet
25
29
  from .roles import PortfolioRoleInstrumentModelViewSet, PortfolioRoleModelViewSet
26
- from .signals import *
27
30
  from .rebalancing import RebalancingModelRepresentationViewSet, RebalancerRepresentationViewSet, RebalancerModelViewSet
28
31
  from .transactions import *
29
32
  from .orders import *