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.
- wbportfolio/admin/indexes.py +1 -1
- wbportfolio/admin/product_groups.py +1 -1
- wbportfolio/admin/products.py +2 -1
- wbportfolio/admin/rebalancing.py +1 -1
- wbportfolio/api_clients/__init__.py +0 -0
- wbportfolio/api_clients/ubs.py +150 -0
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +10 -2
- wbportfolio/factories/portfolios.py +1 -1
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/filters/assets.py +10 -2
- wbportfolio/filters/orders/__init__.py +1 -0
- wbportfolio/filters/orders/order_proposals.py +58 -0
- wbportfolio/filters/portfolios.py +20 -0
- wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
- wbportfolio/import_export/backends/ubs/fees.py +10 -20
- wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
- wbportfolio/import_export/backends/utils.py +0 -17
- wbportfolio/import_export/handlers/asset_position.py +1 -1
- wbportfolio/import_export/handlers/orders.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/equity.py +1 -1
- wbportfolio/import_export/parsers/societe_generale/strategy.py +2 -2
- wbportfolio/import_export/parsers/ubs/equity.py +1 -1
- wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
- wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
- wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
- wbportfolio/models/asset.py +6 -20
- wbportfolio/models/builder.py +74 -31
- wbportfolio/models/mixins/instruments.py +7 -0
- wbportfolio/models/orders/order_proposals.py +549 -167
- wbportfolio/models/orders/orders.py +24 -11
- wbportfolio/models/orders/routing.py +54 -0
- wbportfolio/models/portfolio.py +77 -41
- wbportfolio/models/products.py +9 -0
- wbportfolio/models/rebalancing.py +6 -6
- wbportfolio/models/transactions/transactions.py +10 -6
- wbportfolio/order_routing/__init__.py +19 -0
- wbportfolio/order_routing/adapters/__init__.py +57 -0
- wbportfolio/order_routing/adapters/ubs.py +161 -0
- wbportfolio/pms/trading/handler.py +4 -1
- wbportfolio/pms/typing.py +62 -8
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/equally_weighted.py +3 -3
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -1
- wbportfolio/rebalancing/models/model_portfolio.py +9 -10
- wbportfolio/serializers/orders/order_proposals.py +25 -21
- wbportfolio/serializers/orders/orders.py +5 -2
- wbportfolio/serializers/positions.py +2 -2
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/tests/conftest.py +6 -2
- wbportfolio/tests/models/orders/test_order_proposals.py +90 -30
- wbportfolio/tests/models/test_imports.py +5 -3
- wbportfolio/tests/models/test_portfolios.py +57 -23
- wbportfolio/tests/models/test_products.py +11 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +2 -2
- wbportfolio/tests/rebalancing/test_models.py +3 -5
- wbportfolio/tests/signals.py +0 -10
- wbportfolio/tests/tests.py +2 -0
- wbportfolio/viewsets/__init__.py +7 -4
- wbportfolio/viewsets/assets.py +1 -215
- wbportfolio/viewsets/charts/__init__.py +6 -1
- wbportfolio/viewsets/charts/assets.py +341 -155
- wbportfolio/viewsets/configs/buttons/products.py +32 -2
- wbportfolio/viewsets/configs/display/assets.py +6 -19
- wbportfolio/viewsets/configs/display/products.py +1 -1
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/menu/__init__.py +1 -0
- wbportfolio/viewsets/configs/menu/orders.py +11 -0
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +66 -12
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +55 -9
- wbportfolio/viewsets/orders/configs/displays/orders.py +13 -0
- wbportfolio/viewsets/orders/order_proposals.py +47 -7
- wbportfolio/viewsets/orders/orders.py +31 -29
- wbportfolio/viewsets/portfolios.py +3 -3
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/METADATA +3 -1
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/RECORD +78 -68
- wbportfolio/viewsets/signals.py +0 -43
- {wbportfolio-1.54.22.dist-info → wbportfolio-1.55.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
a2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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.
|
|
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,
|
|
259
|
+
portfolio.change_at_date(weekday, fix_quantization=True)
|
|
259
260
|
|
|
260
|
-
|
|
261
|
-
|
|
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,
|
|
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.
|
|
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 == "
|
|
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 == "
|
|
1071
|
-
rebalancing_order_proposal.
|
|
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
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
wbportfolio/tests/signals.py
CHANGED
|
@@ -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()
|
wbportfolio/tests/tests.py
CHANGED
|
@@ -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
|
],
|
wbportfolio/viewsets/__init__.py
CHANGED
|
@@ -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 *
|