wbportfolio 1.55.8__py2.py3-none-any.whl → 1.59.4__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/orders/order_proposals.py +2 -0
- wbportfolio/admin/orders/orders.py +2 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/api_clients/ubs.py +23 -11
- wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/contrib/company_portfolio/serializers.py +32 -22
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/factories/assets.py +1 -1
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +8 -3
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/filters/assets.py +0 -1
- wbportfolio/filters/orders/order_proposals.py +3 -6
- wbportfolio/filters/portfolios.py +18 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/fees.py +0 -2
- wbportfolio/filters/transactions/trades.py +0 -1
- wbportfolio/import_export/backends/ubs/__init__.py +1 -0
- wbportfolio/import_export/backends/ubs/trade.py +48 -0
- wbportfolio/import_export/handlers/asset_position.py +9 -5
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +2 -2
- wbportfolio/import_export/handlers/trade.py +4 -4
- wbportfolio/import_export/parsers/default_mapping.py +1 -1
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
- wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
- wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
- wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/equity.py +2 -1
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/resources/trades.py +1 -1
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
- wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
- wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
- wbportfolio/models/adjustments.py +1 -1
- wbportfolio/models/asset.py +7 -3
- wbportfolio/models/builder.py +25 -5
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/portfolio.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
- wbportfolio/models/orders/order_proposals.py +620 -490
- wbportfolio/models/orders/orders.py +237 -75
- wbportfolio/models/portfolio.py +79 -18
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +3 -0
- wbportfolio/models/rebalancing.py +4 -1
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/claim.py +6 -5
- wbportfolio/models/transactions/dividends.py +1 -0
- wbportfolio/models/transactions/trades.py +4 -0
- wbportfolio/models/transactions/transactions.py +16 -4
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +16 -0
- wbportfolio/order_routing/adapters/__init__.py +14 -6
- wbportfolio/order_routing/adapters/ubs.py +104 -70
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +115 -103
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
- wbportfolio/risk_management/backends/__init__.py +1 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
- wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
- wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
- wbportfolio/serializers/orders/order_proposals.py +6 -2
- wbportfolio/serializers/orders/orders.py +119 -26
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/tasks.py +42 -4
- wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
- wbportfolio/tests/models/test_portfolios.py +9 -9
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
- wbportfolio/tests/rebalancing/test_models.py +2 -2
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +1 -1
- wbportfolio/viewsets/charts/assets.py +8 -4
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
- wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
- wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
- wbportfolio/viewsets/orders/order_proposals.py +92 -21
- wbportfolio/viewsets/orders/orders.py +79 -26
- wbportfolio/viewsets/portfolios.py +24 -0
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/fdm/tasks.py +0 -42
- wbportfolio/models/orders/routing.py +0 -54
- wbportfolio/pms/trading/handler.py +0 -211
- /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Import necessary modules
|
|
2
2
|
from datetime import date, timedelta
|
|
3
3
|
from decimal import Decimal
|
|
4
|
-
from unittest.mock import call, patch
|
|
4
|
+
from unittest.mock import MagicMock, PropertyMock, call, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from django.db.models import Sum
|
|
@@ -9,12 +9,20 @@ from faker import Faker
|
|
|
9
9
|
from pandas._libs.tslibs.offsets import BDay, BusinessMonthEnd
|
|
10
10
|
|
|
11
11
|
from wbportfolio.models import Order, OrderProposal, Portfolio, RebalancingModel
|
|
12
|
+
from wbportfolio.order_routing import ExecutionInstruction, ExecutionStatus, RoutingException
|
|
13
|
+
from wbportfolio.pms.typing import Order as OrderDTO
|
|
12
14
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
13
15
|
from wbportfolio.pms.typing import Position
|
|
14
16
|
|
|
15
17
|
fake = Faker()
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def mock_adapter():
|
|
22
|
+
adapter = MagicMock()
|
|
23
|
+
return adapter
|
|
24
|
+
|
|
25
|
+
|
|
18
26
|
# Mark tests to use Django's database
|
|
19
27
|
@pytest.mark.django_db
|
|
20
28
|
class TestOrderProposal:
|
|
@@ -36,45 +44,62 @@ class TestOrderProposal:
|
|
|
36
44
|
assert order_proposal.check_evaluation_date == order_proposal.trade_date
|
|
37
45
|
|
|
38
46
|
# Test the validated trading service functionality
|
|
39
|
-
def test_validated_trading_service(
|
|
47
|
+
def test_validated_trading_service(
|
|
48
|
+
self, order_proposal, asset_position_factory, instrument_price_factory, instrument_factory, order_factory
|
|
49
|
+
):
|
|
40
50
|
"""
|
|
41
51
|
Validate that the effective and target portfolios are correctly calculated.
|
|
42
52
|
"""
|
|
43
53
|
effective_date = (order_proposal.trade_date - BDay(1)).date()
|
|
44
54
|
|
|
55
|
+
i1 = instrument_factory.create()
|
|
56
|
+
i2 = instrument_factory.create()
|
|
57
|
+
|
|
58
|
+
p10 = instrument_price_factory.create(instrument=i1, date=effective_date)
|
|
59
|
+
p11 = instrument_price_factory.create(instrument=i1, date=order_proposal.trade_date)
|
|
60
|
+
|
|
61
|
+
p20 = instrument_price_factory.create(instrument=i2, date=effective_date)
|
|
62
|
+
p21 = instrument_price_factory.create(instrument=i2, date=order_proposal.trade_date)
|
|
63
|
+
|
|
45
64
|
# Create asset positions for testing
|
|
46
65
|
a1 = asset_position_factory.create(
|
|
47
|
-
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.3")
|
|
66
|
+
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.3"), underlying_instrument=i1
|
|
48
67
|
)
|
|
49
68
|
a2 = asset_position_factory.create(
|
|
50
|
-
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.7")
|
|
69
|
+
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.7"), underlying_instrument=i2
|
|
51
70
|
)
|
|
71
|
+
r1 = p11.net_value / p10.net_value - Decimal("1")
|
|
72
|
+
r2 = p21.net_value / p20.net_value - Decimal("1")
|
|
73
|
+
p_return = a1.weighting * (Decimal("1") + r1) + a2.weighting * (Decimal("1") + r2)
|
|
74
|
+
order_proposal.total_effective_portfolio_contribution = p_return
|
|
75
|
+
order_proposal.save()
|
|
52
76
|
|
|
53
77
|
# Create orders for testing
|
|
54
78
|
o1 = order_factory.create(
|
|
55
79
|
order_proposal=order_proposal,
|
|
56
80
|
weighting=Decimal("0.05"),
|
|
57
81
|
portfolio=order_proposal.portfolio,
|
|
58
|
-
underlying_instrument=
|
|
82
|
+
underlying_instrument=i1,
|
|
59
83
|
)
|
|
60
84
|
o2 = order_factory.create(
|
|
61
85
|
order_proposal=order_proposal,
|
|
62
86
|
weighting=Decimal("-0.05"),
|
|
63
87
|
portfolio=order_proposal.portfolio,
|
|
64
|
-
underlying_instrument=
|
|
88
|
+
underlying_instrument=i2,
|
|
65
89
|
)
|
|
66
90
|
|
|
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)
|
|
70
91
|
# Get the validated trading service
|
|
71
|
-
trades = order_proposal.
|
|
92
|
+
trades = order_proposal.get_trades_batch().trades_map
|
|
72
93
|
t1 = trades[a1.underlying_quote.id]
|
|
73
94
|
t2 = trades[a2.underlying_quote.id]
|
|
74
95
|
|
|
75
96
|
# Assert effective and target portfolios are as expected
|
|
76
|
-
assert t1.
|
|
77
|
-
|
|
97
|
+
assert t1.effective_weight == pytest.approx(
|
|
98
|
+
a1.weighting * ((r1 + Decimal("1")) / p_return), abs=Decimal("1e-8")
|
|
99
|
+
)
|
|
100
|
+
assert t2.effective_weight == pytest.approx(
|
|
101
|
+
a2.weighting * ((r2 + Decimal("1")) / p_return), abs=Decimal("1e-8")
|
|
102
|
+
)
|
|
78
103
|
assert t1.target_weight == pytest.approx(
|
|
79
104
|
a1.weighting * ((r1 + Decimal("1")) / p_return) + o1.weighting, abs=Decimal("1e-8")
|
|
80
105
|
)
|
|
@@ -121,10 +146,10 @@ class TestOrderProposal:
|
|
|
121
146
|
portfolio=tp.portfolio, status=OrderProposal.Status.PENDING, trade_date=(tp.trade_date - BDay(1)).date()
|
|
122
147
|
)
|
|
123
148
|
tp_previous_approve = order_proposal_factory.create(
|
|
124
|
-
portfolio=tp.portfolio, status=OrderProposal.Status.
|
|
149
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.CONFIRMED, trade_date=(tp.trade_date - BDay(2)).date()
|
|
125
150
|
)
|
|
126
151
|
tp_next_approve = order_proposal_factory.create( # noqa
|
|
127
|
-
portfolio=tp.portfolio, status=OrderProposal.Status.
|
|
152
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.CONFIRMED, trade_date=(tp.trade_date + BDay(1)).date()
|
|
128
153
|
)
|
|
129
154
|
|
|
130
155
|
# The previous valid order proposal should be the approved one strictly before the current proposal
|
|
@@ -139,13 +164,13 @@ class TestOrderProposal:
|
|
|
139
164
|
"""
|
|
140
165
|
tp = order_proposal_factory.create()
|
|
141
166
|
tp_previous_approve = order_proposal_factory.create( # noqa
|
|
142
|
-
portfolio=tp.portfolio, status=OrderProposal.Status.
|
|
167
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.CONFIRMED, trade_date=(tp.trade_date - BDay(1)).date()
|
|
143
168
|
)
|
|
144
169
|
tp_next_submit = order_proposal_factory.create( # noqa
|
|
145
170
|
portfolio=tp.portfolio, status=OrderProposal.Status.PENDING, trade_date=(tp.trade_date + BDay(1)).date()
|
|
146
171
|
)
|
|
147
172
|
tp_next_approve = order_proposal_factory.create(
|
|
148
|
-
portfolio=tp.portfolio, status=OrderProposal.Status.
|
|
173
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.CONFIRMED, trade_date=(tp.trade_date + BDay(2)).date()
|
|
149
174
|
)
|
|
150
175
|
|
|
151
176
|
# The next valid order proposal should be the approved one strictly after the current proposal
|
|
@@ -218,12 +243,12 @@ class TestOrderProposal:
|
|
|
218
243
|
t1 = order_factory.create(
|
|
219
244
|
order_proposal=order_proposal,
|
|
220
245
|
portfolio=order_proposal.portfolio,
|
|
221
|
-
weighting=Decimal(0.
|
|
246
|
+
weighting=Decimal(0.05),
|
|
222
247
|
)
|
|
223
248
|
t2 = order_factory.create(
|
|
224
249
|
order_proposal=order_proposal,
|
|
225
250
|
portfolio=order_proposal.portfolio,
|
|
226
|
-
weighting=Decimal(0.
|
|
251
|
+
weighting=Decimal(0.22),
|
|
227
252
|
)
|
|
228
253
|
t3 = order_factory.create(
|
|
229
254
|
order_proposal=order_proposal,
|
|
@@ -232,26 +257,19 @@ class TestOrderProposal:
|
|
|
232
257
|
)
|
|
233
258
|
|
|
234
259
|
# Normalize orders
|
|
235
|
-
order_proposal.normalize_orders()
|
|
260
|
+
order_proposal.normalize_orders(Decimal("0.18"))
|
|
236
261
|
|
|
237
262
|
# Refresh orders from the database
|
|
238
263
|
t1.refresh_from_db()
|
|
239
264
|
t2.refresh_from_db()
|
|
240
265
|
t3.refresh_from_db()
|
|
266
|
+
cash = order_proposal.orders.get(underlying_instrument__is_cash=True)
|
|
241
267
|
|
|
242
268
|
# Expected normalized weights
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
# Calculate quantization error
|
|
248
|
-
quantize_error = Decimal(1) - (normalized_t1_weight + normalized_t2_weight + normalized_t3_weight)
|
|
249
|
-
|
|
250
|
-
# Assert quantization error exists and weights are normalized correctly
|
|
251
|
-
assert quantize_error
|
|
252
|
-
assert t1.weighting == normalized_t1_weight
|
|
253
|
-
assert t2.weighting == normalized_t2_weight + quantize_error # Add quantize error to the largest position
|
|
254
|
-
assert t3.weighting == normalized_t3_weight
|
|
269
|
+
assert t1.weighting == Decimal("0.10")
|
|
270
|
+
assert t2.weighting == Decimal("0.44")
|
|
271
|
+
assert t3.weighting == Decimal("0.28")
|
|
272
|
+
assert cash.weighting == Decimal("0.18")
|
|
255
273
|
|
|
256
274
|
# Test resetting orders
|
|
257
275
|
def test_reset_orders(
|
|
@@ -390,15 +408,15 @@ class TestOrderProposal:
|
|
|
390
408
|
mock_fct.return_value = iter([])
|
|
391
409
|
|
|
392
410
|
# Create approved order proposals for testing
|
|
393
|
-
tp0 = order_proposal_factory.create(status=OrderProposal.Status.
|
|
411
|
+
tp0 = order_proposal_factory.create(status=OrderProposal.Status.CONFIRMED)
|
|
394
412
|
tp1 = order_proposal_factory.create(
|
|
395
413
|
portfolio=tp0.portfolio,
|
|
396
|
-
status=OrderProposal.Status.
|
|
414
|
+
status=OrderProposal.Status.CONFIRMED,
|
|
397
415
|
trade_date=(tp0.trade_date + BusinessMonthEnd(1)).date(),
|
|
398
416
|
)
|
|
399
417
|
tp2 = order_proposal_factory.create(
|
|
400
418
|
portfolio=tp0.portfolio,
|
|
401
|
-
status=OrderProposal.Status.
|
|
419
|
+
status=OrderProposal.Status.CONFIRMED,
|
|
402
420
|
trade_date=(tp1.trade_date + BusinessMonthEnd(1)).date(),
|
|
403
421
|
)
|
|
404
422
|
|
|
@@ -422,7 +440,7 @@ class TestOrderProposal:
|
|
|
422
440
|
mock_fct.assert_has_calls(expected_calls)
|
|
423
441
|
|
|
424
442
|
# Test estimating shares for a trade
|
|
425
|
-
@patch.object(
|
|
443
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
426
444
|
def test_get_estimated_shares(
|
|
427
445
|
self, mock_fct, order_proposal, order_factory, instrument_price_factory, instrument_factory
|
|
428
446
|
):
|
|
@@ -449,7 +467,7 @@ class TestOrderProposal:
|
|
|
449
467
|
== Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
|
|
450
468
|
)
|
|
451
469
|
|
|
452
|
-
@patch.object(
|
|
470
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
453
471
|
def test_get_estimated_target_cash(self, mock_fct, order_proposal, order_factory, cash_factory):
|
|
454
472
|
order_proposal.portfolio.only_weighting = False
|
|
455
473
|
order_proposal.portfolio.save()
|
|
@@ -500,17 +518,25 @@ class TestOrderProposal:
|
|
|
500
518
|
instrument.exchange.save()
|
|
501
519
|
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
502
520
|
|
|
503
|
-
|
|
521
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
522
|
+
def test_submit_round_lot_size(self, mock_fct, order_proposal, instrument_price_factory, order_factory):
|
|
523
|
+
initial_shares = Decimal("70")
|
|
524
|
+
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
525
|
+
net_value = round(price.net_value, 4)
|
|
526
|
+
portfolio_value = initial_shares * net_value
|
|
527
|
+
mock_fct.return_value = portfolio_value
|
|
528
|
+
|
|
504
529
|
order_proposal.portfolio.only_weighting = False
|
|
505
530
|
order_proposal.portfolio.save()
|
|
506
|
-
instrument =
|
|
531
|
+
instrument = price.instrument
|
|
507
532
|
instrument.round_lot_size = 100
|
|
508
533
|
instrument.save()
|
|
509
534
|
trade = order_factory.create(
|
|
510
|
-
|
|
511
|
-
shares=70,
|
|
535
|
+
shares=initial_shares,
|
|
512
536
|
order_proposal=order_proposal,
|
|
513
537
|
weighting=Decimal("1.0"),
|
|
538
|
+
underlying_instrument=price.instrument,
|
|
539
|
+
price=net_value,
|
|
514
540
|
)
|
|
515
541
|
warnings = order_proposal.submit()
|
|
516
542
|
order_proposal.save()
|
|
@@ -520,18 +546,34 @@ class TestOrderProposal:
|
|
|
520
546
|
trade.refresh_from_db()
|
|
521
547
|
assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
|
|
522
548
|
|
|
523
|
-
|
|
549
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
550
|
+
def test_submit_round_fractional_shares(
|
|
551
|
+
self, mock_fct, instrument_price_factory, order_proposal, order_factory, asset_position_factory
|
|
552
|
+
):
|
|
553
|
+
initial_shares = Decimal("5.6")
|
|
554
|
+
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
555
|
+
net_value = round(price.net_value, 4)
|
|
556
|
+
portfolio_value = initial_shares * net_value
|
|
557
|
+
mock_fct.return_value = portfolio_value
|
|
558
|
+
|
|
524
559
|
order_proposal.portfolio.only_weighting = False
|
|
525
560
|
order_proposal.portfolio.save()
|
|
561
|
+
|
|
526
562
|
trade = order_factory.create(
|
|
527
|
-
shares=5.6,
|
|
563
|
+
shares=Decimal("5.6"),
|
|
528
564
|
order_proposal=order_proposal,
|
|
529
565
|
weighting=Decimal("1.0"),
|
|
566
|
+
underlying_instrument=price.instrument,
|
|
567
|
+
price=net_value,
|
|
530
568
|
)
|
|
531
569
|
order_proposal.submit()
|
|
532
570
|
order_proposal.save()
|
|
533
571
|
trade.refresh_from_db()
|
|
534
572
|
assert trade.shares == 6 # we expect the fractional share to be rounded
|
|
573
|
+
assert trade.weighting == round((trade.shares * net_value) / portfolio_value, 8)
|
|
574
|
+
assert trade.weighting == round(
|
|
575
|
+
Decimal("1") + ((Decimal("6") - initial_shares) * net_value) / portfolio_value, 8
|
|
576
|
+
) # we expect the weighting to be updated accrodingly
|
|
535
577
|
|
|
536
578
|
def test_ex_post(
|
|
537
579
|
self, instrument_factory, asset_position_factory, instrument_price_factory, order_proposal_factory, portfolio
|
|
@@ -667,12 +709,11 @@ class TestOrderProposal:
|
|
|
667
709
|
)
|
|
668
710
|
order_proposal.submit()
|
|
669
711
|
order_proposal.approve()
|
|
670
|
-
order_proposal.
|
|
712
|
+
order_proposal.confirm()
|
|
671
713
|
order_proposal.save()
|
|
672
714
|
|
|
673
715
|
draft_tp = order_proposal_factory.create(portfolio=order_proposal.portfolio, trade_date=date.today() - BDay(1))
|
|
674
716
|
assert not Order.objects.filter(order_proposal=draft_tp).exists()
|
|
675
|
-
|
|
676
717
|
order_proposal.replay()
|
|
677
718
|
|
|
678
719
|
assert Order.objects.filter(order_proposal=draft_tp).count() == 1
|
|
@@ -695,6 +736,37 @@ class TestOrderProposal:
|
|
|
695
736
|
assert order.shares == Decimal(0)
|
|
696
737
|
assert order.weighting == Decimal(0)
|
|
697
738
|
|
|
739
|
+
def test_order_submit_bellow_minimum_weighting(self, order_factory, order_proposal):
|
|
740
|
+
o1 = order_factory.create(order_proposal=order_proposal, price=Decimal(1), weighting=Decimal("0.8"))
|
|
741
|
+
o2 = order_factory.create(order_proposal=order_proposal, price=Decimal(1), weighting=Decimal("0.2"))
|
|
742
|
+
order_proposal.submit()
|
|
743
|
+
order_proposal.save()
|
|
744
|
+
|
|
745
|
+
o1.refresh_from_db()
|
|
746
|
+
o2.refresh_from_db()
|
|
747
|
+
assert o1.weighting == Decimal("0.8")
|
|
748
|
+
assert o2.weighting == Decimal("0.2")
|
|
749
|
+
|
|
750
|
+
order_proposal.min_weighting = Decimal("0.21")
|
|
751
|
+
order_proposal.backtodraft()
|
|
752
|
+
order_proposal.submit()
|
|
753
|
+
order_proposal.save()
|
|
754
|
+
|
|
755
|
+
o1.refresh_from_db()
|
|
756
|
+
o2.refresh_from_db()
|
|
757
|
+
assert o1.weighting == Decimal("0.8")
|
|
758
|
+
assert o2.weighting == Decimal("0")
|
|
759
|
+
|
|
760
|
+
order_proposal.approve()
|
|
761
|
+
order_proposal.apply()
|
|
762
|
+
order_proposal.save()
|
|
763
|
+
assert order_proposal.portfolio.assets.get(
|
|
764
|
+
date=order_proposal.trade_date, underlying_quote=o1.underlying_instrument
|
|
765
|
+
).weighting == Decimal("0.8")
|
|
766
|
+
assert order_proposal.portfolio.assets.get(
|
|
767
|
+
date=order_proposal.trade_date, underlying_quote=order_proposal.cash_component
|
|
768
|
+
).weighting == Decimal("0.2")
|
|
769
|
+
|
|
698
770
|
def test_reset_order_use_desired_target_weight(self, order_proposal, order_factory):
|
|
699
771
|
order1 = order_factory.create(
|
|
700
772
|
order_proposal=order_proposal, weighting=Decimal("0.5"), desired_target_weight=Decimal("0.7")
|
|
@@ -735,15 +807,240 @@ class TestOrderProposal:
|
|
|
735
807
|
order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.2"))
|
|
736
808
|
|
|
737
809
|
order_proposal.reset_orders()
|
|
738
|
-
assert order_proposal.get_orders().
|
|
739
|
-
"
|
|
740
|
-
), "The total target weight leftover does not equal the stored total cash weight"
|
|
810
|
+
assert order_proposal.get_orders().exclude(underlying_instrument__is_cash=True).aggregate(
|
|
811
|
+
s=Sum("target_weight")
|
|
812
|
+
)["s"] == Decimal("0.98"), "The total target weight leftover does not equal the stored total cash weight"
|
|
741
813
|
|
|
742
814
|
def test_convert_to_portfolio_always_100percent(self, order_proposal, order_factory):
|
|
743
815
|
o1 = order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.5"))
|
|
744
816
|
o2 = order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.3"))
|
|
745
817
|
|
|
746
|
-
portfolio = order_proposal.
|
|
818
|
+
portfolio = order_proposal._get_default_effective_portfolio(include_delta_weight=True)
|
|
747
819
|
assert portfolio.positions_map[o1.underlying_instrument.id].weighting == Decimal("0.5")
|
|
748
820
|
assert portfolio.positions_map[o2.underlying_instrument.id].weighting == Decimal("0.3")
|
|
749
821
|
assert portfolio.positions_map[order_proposal.cash_component.id].weighting == Decimal("0.2")
|
|
822
|
+
|
|
823
|
+
@patch.object(OrderProposal, "custodian_router", new_callable=PropertyMock)
|
|
824
|
+
@patch.object(OrderProposal, "has_non_successful_checks", new_callable=PropertyMock)
|
|
825
|
+
def test_can_execute(
|
|
826
|
+
self, mock_has_non_successful_checks, mock_router, order_proposal, user_factory, mock_adapter
|
|
827
|
+
):
|
|
828
|
+
user = user_factory.create()
|
|
829
|
+
mock_router.return_value = mock_adapter
|
|
830
|
+
mock_has_non_successful_checks.return_value = False
|
|
831
|
+
order_proposal.status = OrderProposal.Status.APPROVED
|
|
832
|
+
order_proposal.execution_status = ""
|
|
833
|
+
|
|
834
|
+
assert order_proposal.can_execute(user) is True
|
|
835
|
+
order_proposal.approver = user.profile
|
|
836
|
+
assert order_proposal.can_execute(user) is False
|
|
837
|
+
user.is_superuser = True
|
|
838
|
+
assert order_proposal.can_execute(user) is True
|
|
839
|
+
|
|
840
|
+
mock_router.return_value = None
|
|
841
|
+
assert order_proposal.can_execute(user) is False
|
|
842
|
+
|
|
843
|
+
mock_router.return_value = mock_adapter
|
|
844
|
+
mock_has_non_successful_checks.return_value = True
|
|
845
|
+
assert order_proposal.can_execute(user) is False
|
|
846
|
+
|
|
847
|
+
mock_has_non_successful_checks.return_value = False
|
|
848
|
+
order_proposal.status = OrderProposal.Status.PENDING
|
|
849
|
+
assert order_proposal.can_execute(user) is False
|
|
850
|
+
|
|
851
|
+
order_proposal.status = OrderProposal.Status.APPROVED
|
|
852
|
+
order_proposal.execution_status = "something"
|
|
853
|
+
assert order_proposal.can_execute(user) is False
|
|
854
|
+
|
|
855
|
+
@patch.object(OrderProposal, "custodian_router", new_callable=PropertyMock)
|
|
856
|
+
def test_refresh_execution_status(self, mock_custodian_router, order_proposal, mock_adapter):
|
|
857
|
+
mock_custodian_router.return_value = mock_adapter
|
|
858
|
+
mock_adapter.get_rebalance_status.return_value = (ExecutionStatus.PENDING, "detail")
|
|
859
|
+
|
|
860
|
+
with patch.object(order_proposal, "save", wraps=order_proposal.save) as mock_save:
|
|
861
|
+
order_proposal.refresh_execution_status()
|
|
862
|
+
assert order_proposal.execution_status == ExecutionStatus.PENDING
|
|
863
|
+
assert order_proposal.execution_status_detail == "detail"
|
|
864
|
+
mock_save.assert_called_once()
|
|
865
|
+
|
|
866
|
+
@patch.object(OrderProposal, "custodian_router", new_callable=PropertyMock)
|
|
867
|
+
def test_cancel_rebalancing_success(self, mock_custodian_router, order_proposal, mock_adapter):
|
|
868
|
+
mock_custodian_router.return_value = mock_adapter
|
|
869
|
+
mock_adapter.cancel_rebalancing.return_value = True
|
|
870
|
+
with patch.object(order_proposal, "save", wraps=order_proposal.save) as mock_save:
|
|
871
|
+
result = order_proposal.cancel_rebalancing()
|
|
872
|
+
assert result is True
|
|
873
|
+
assert order_proposal.execution_status == ExecutionStatus.CANCELLED
|
|
874
|
+
assert order_proposal.execution_comment == ""
|
|
875
|
+
assert order_proposal.execution_status_detail == ""
|
|
876
|
+
mock_save.assert_called_once()
|
|
877
|
+
|
|
878
|
+
@patch.object(OrderProposal, "custodian_router", new_callable=PropertyMock)
|
|
879
|
+
def test_cancel_rebalancing_failure(self, mock_custodian_router, order_proposal, mock_adapter):
|
|
880
|
+
mock_custodian_router.return_value = mock_adapter
|
|
881
|
+
mock_adapter.cancel_rebalancing.return_value = False
|
|
882
|
+
with patch.object(order_proposal, "save", wraps=order_proposal.save) as mock_save:
|
|
883
|
+
result = order_proposal.cancel_rebalancing()
|
|
884
|
+
assert result is False
|
|
885
|
+
# No change in status or calls to save
|
|
886
|
+
mock_save.assert_not_called()
|
|
887
|
+
|
|
888
|
+
@patch.object(OrderProposal, "custodian_router", new_callable=PropertyMock)
|
|
889
|
+
@patch.object(OrderProposal, "prepare_orders_for_execution")
|
|
890
|
+
@patch.object(OrderProposal, "handle_orders")
|
|
891
|
+
def test_execute_orders_success(self, mock_handler_error, mock_fct, mock_router, order_proposal, mock_adapter):
|
|
892
|
+
mock_router.return_value = mock_adapter
|
|
893
|
+
# Arrange
|
|
894
|
+
orders = ["order1", "order2"]
|
|
895
|
+
mock_fct.return_value = orders
|
|
896
|
+
confirmed_orders = ["confirmed1", "confirmed2"]
|
|
897
|
+
status = ExecutionStatus.PENDING
|
|
898
|
+
comment = "Success"
|
|
899
|
+
mock_adapter.submit_rebalancing.return_value = (confirmed_orders, (status, comment))
|
|
900
|
+
|
|
901
|
+
# Act
|
|
902
|
+
with patch.object(order_proposal, "save") as mock_save:
|
|
903
|
+
order_proposal.execute_orders(prioritize_target_weight=True)
|
|
904
|
+
|
|
905
|
+
# Assert
|
|
906
|
+
mock_fct.assert_called_once_with(prioritize_target_weight=True)
|
|
907
|
+
mock_adapter.submit_rebalancing.assert_called_once_with(orders)
|
|
908
|
+
mock_handler_error.assert_called_once_with(confirmed_orders)
|
|
909
|
+
assert order_proposal.execution_status == status
|
|
910
|
+
assert order_proposal.execution_comment == comment
|
|
911
|
+
mock_save.assert_called_once()
|
|
912
|
+
|
|
913
|
+
@patch.object(OrderProposal, "custodian_router", new_callable=PropertyMock)
|
|
914
|
+
@patch.object(OrderProposal, "prepare_orders_for_execution")
|
|
915
|
+
@patch.object(OrderProposal, "handle_orders")
|
|
916
|
+
def test_execute_orders_on_failure(self, mock_handler_error, mock_fct, mock_router, order_proposal, mock_adapter):
|
|
917
|
+
mock_router.return_value = mock_adapter
|
|
918
|
+
# Arrange
|
|
919
|
+
orders = ["order1", "order2"]
|
|
920
|
+
mock_fct.return_value = orders
|
|
921
|
+
mock_adapter.submit_rebalancing.side_effect = RoutingException("Failure!")
|
|
922
|
+
|
|
923
|
+
# Act
|
|
924
|
+
with patch.object(order_proposal, "save") as mock_save:
|
|
925
|
+
order_proposal.execute_orders(prioritize_target_weight=True)
|
|
926
|
+
|
|
927
|
+
# Assert
|
|
928
|
+
mock_fct.assert_called_once_with(prioritize_target_weight=True)
|
|
929
|
+
mock_adapter.submit_rebalancing.assert_called_once_with(orders)
|
|
930
|
+
mock_handler_error.assert_not_called()
|
|
931
|
+
assert order_proposal.execution_status == ExecutionStatus.FAILED
|
|
932
|
+
assert order_proposal.execution_comment == "Failure!"
|
|
933
|
+
mock_save.assert_called_once()
|
|
934
|
+
|
|
935
|
+
def test_prepare_orders_for_execution(self, order_proposal, order_factory, instrument_factory, equity_factory):
|
|
936
|
+
invalid_equity = equity_factory.create(refinitiv_identifier_code=None, ticker=None, sedol=None)
|
|
937
|
+
exotic_instrument = instrument_factory.create()
|
|
938
|
+
cash_instrument = instrument_factory.create(is_cash=True)
|
|
939
|
+
order_valid = order_factory.create(
|
|
940
|
+
order_proposal=order_proposal,
|
|
941
|
+
weighting=Decimal(0.6),
|
|
942
|
+
shares=Decimal(800),
|
|
943
|
+
execution_instruction=ExecutionInstruction.LIMIT_ORDER,
|
|
944
|
+
underlying_instrument=equity_factory.create(),
|
|
945
|
+
)
|
|
946
|
+
order_valid_but_unsupported_asset_class = order_factory.create(
|
|
947
|
+
order_proposal=order_proposal,
|
|
948
|
+
weighting=Decimal(0.6),
|
|
949
|
+
shares=Decimal(800),
|
|
950
|
+
execution_instruction=ExecutionInstruction.LIMIT_ORDER,
|
|
951
|
+
underlying_instrument=exotic_instrument,
|
|
952
|
+
)
|
|
953
|
+
order_invalid_instrument = order_factory.create(
|
|
954
|
+
order_proposal=order_proposal,
|
|
955
|
+
weighting=Decimal(0.3),
|
|
956
|
+
shares=Decimal(800),
|
|
957
|
+
underlying_instrument=invalid_equity,
|
|
958
|
+
)
|
|
959
|
+
order_zero_delta = order_factory.create(
|
|
960
|
+
order_proposal=order_proposal,
|
|
961
|
+
weighting=Decimal(0),
|
|
962
|
+
shares=Decimal(0),
|
|
963
|
+
underlying_instrument=equity_factory.create(),
|
|
964
|
+
)
|
|
965
|
+
order_cash = order_factory.create(
|
|
966
|
+
order_proposal=order_proposal,
|
|
967
|
+
weighting=Decimal(0.1),
|
|
968
|
+
shares=Decimal(200),
|
|
969
|
+
underlying_instrument=cash_instrument,
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
orders_dto = order_proposal.prepare_orders_for_execution()
|
|
973
|
+
assert len(orders_dto) == 1
|
|
974
|
+
order = orders_dto[0]
|
|
975
|
+
assert order.refinitiv_identifier_code == order_valid.underlying_instrument.refinitiv_identifier_code
|
|
976
|
+
assert (
|
|
977
|
+
order.bloomberg_ticker
|
|
978
|
+
== order_valid.underlying_instrument.ticker
|
|
979
|
+
+ " "
|
|
980
|
+
+ order_valid.underlying_instrument.exchange.bbg_composite
|
|
981
|
+
)
|
|
982
|
+
assert order.sedol == order_valid.underlying_instrument.sedol
|
|
983
|
+
assert order.execution_instruction == ExecutionInstruction.LIMIT_ORDER
|
|
984
|
+
assert order.target_shares == order_valid.shares
|
|
985
|
+
assert order.shares == order_valid.shares
|
|
986
|
+
assert order.weighting == order_valid.weighting
|
|
987
|
+
assert order.target_weight == order_valid.weighting
|
|
988
|
+
assert order.trade_date == order_proposal.trade_date
|
|
989
|
+
|
|
990
|
+
order_invalid_instrument.refresh_from_db()
|
|
991
|
+
order_valid_but_unsupported_asset_class.refresh_from_db()
|
|
992
|
+
order_zero_delta.refresh_from_db()
|
|
993
|
+
order_cash.refresh_from_db()
|
|
994
|
+
|
|
995
|
+
assert order_zero_delta.execution_status == Order.ExecutionStatus.IGNORED
|
|
996
|
+
assert order_cash.execution_status == Order.ExecutionStatus.IGNORED
|
|
997
|
+
assert order_invalid_instrument.execution_status == Order.ExecutionStatus.FAILED
|
|
998
|
+
assert order_invalid_instrument.execution_comment == "Underlying instrument does not have a valid identifier."
|
|
999
|
+
assert order_valid_but_unsupported_asset_class.execution_status == Order.ExecutionStatus.FAILED
|
|
1000
|
+
assert order_valid_but_unsupported_asset_class.execution_comment.startswith("Unsupported asset class")
|
|
1001
|
+
|
|
1002
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
1003
|
+
def test_handle_orders(self, mock_fct, order_proposal, order_factory):
|
|
1004
|
+
o1 = order_factory.create(
|
|
1005
|
+
order_proposal=order_proposal, weighting=Decimal("0.8"), shares=Decimal(800), price=Decimal(2)
|
|
1006
|
+
)
|
|
1007
|
+
o2 = order_factory.create(
|
|
1008
|
+
order_proposal=order_proposal, weighting=Decimal("0.2"), shares=Decimal(200), price=Decimal(2)
|
|
1009
|
+
)
|
|
1010
|
+
portfolio_value = Decimal(800) * Decimal(2) + Decimal(200) * Decimal(2)
|
|
1011
|
+
mock_fct.return_value = portfolio_value
|
|
1012
|
+
|
|
1013
|
+
expected_shares = round(800 * 2 / 1.2)
|
|
1014
|
+
order_proposal.handle_orders(
|
|
1015
|
+
[
|
|
1016
|
+
OrderDTO(
|
|
1017
|
+
id=o1.id,
|
|
1018
|
+
asset_class=OrderDTO.AssetType.EQUITY,
|
|
1019
|
+
target_weight=0.8,
|
|
1020
|
+
weighting=0.8,
|
|
1021
|
+
trade_date=o1.value_date,
|
|
1022
|
+
execution_price=1.2,
|
|
1023
|
+
shares=expected_shares, # we simulate a market fluctuation
|
|
1024
|
+
target_shares=expected_shares,
|
|
1025
|
+
comment="some comment",
|
|
1026
|
+
)
|
|
1027
|
+
]
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
o1.refresh_from_db()
|
|
1031
|
+
with pytest.raises(Order.DoesNotExist):
|
|
1032
|
+
o2.refresh_from_db()
|
|
1033
|
+
|
|
1034
|
+
assert o1.execution_status == Order.ExecutionStatus.CONFIRMED
|
|
1035
|
+
assert o1.execution_comment == "some comment"
|
|
1036
|
+
|
|
1037
|
+
# We do not update these fields anymore, we keep the test around in case it comes back
|
|
1038
|
+
# assert o1.price == Decimal("1.2") # check the the new execution price was updated
|
|
1039
|
+
# assert (
|
|
1040
|
+
# o1.shares == expected_shares
|
|
1041
|
+
# ) # check that the new shares based on the execution price got updated as well
|
|
1042
|
+
# assert (
|
|
1043
|
+
# o1.weighting == round(Decimal(expected_shares * 1.2), 2) / portfolio_value
|
|
1044
|
+
# ) # weighting should change slightly as we round the number of shares
|
|
1045
|
+
|
|
1046
|
+
assert order_proposal.orders.get(underlying_instrument__is_cash=True).weighting == Decimal("1") - o1.weighting
|
|
@@ -924,13 +924,13 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
924
924
|
|
|
925
925
|
analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
|
|
926
926
|
assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
|
|
927
|
-
|
|
927
|
+
expected_x = pd.DataFrame(
|
|
928
928
|
[[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
|
|
929
929
|
columns=[i1.id, i2.id],
|
|
930
930
|
index=[(weekday + BDay(1)).date()],
|
|
931
931
|
)
|
|
932
|
-
|
|
933
|
-
pd.testing.assert_frame_equal(analytic_portfolio.X,
|
|
932
|
+
expected_x.index = pd.to_datetime(expected_x.index)
|
|
933
|
+
pd.testing.assert_frame_equal(analytic_portfolio.X, expected_x, check_names=False, check_freq=False)
|
|
934
934
|
|
|
935
935
|
def test_get_total_asset_value(self, weekday, portfolio, asset_position_factory):
|
|
936
936
|
a1 = asset_position_factory.create(date=weekday, portfolio=portfolio)
|
|
@@ -1010,7 +1010,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1010
1010
|
assert len(res) == 1
|
|
1011
1011
|
assert a.date == weekday
|
|
1012
1012
|
assert a.underlying_quote == instrument
|
|
1013
|
-
assert a.underlying_quote_price
|
|
1013
|
+
assert a.underlying_quote_price is None
|
|
1014
1014
|
assert a.initial_price == p.net_value
|
|
1015
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
|
|
@@ -1053,11 +1053,11 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1053
1053
|
assert next(gen)[0] == middle_date, "Drifting weight with a non automatic rebalancer stops the iteration"
|
|
1054
1054
|
try:
|
|
1055
1055
|
next(gen)
|
|
1056
|
-
|
|
1056
|
+
raise AssertionError("the next iteration should stop and return the rebalancing")
|
|
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 == "PENDING"
|
|
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()
|
|
@@ -1066,10 +1066,10 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1066
1066
|
assert t1._target_weight == Decimal("0.5")
|
|
1067
1067
|
assert t2._target_weight == Decimal("0.5")
|
|
1068
1068
|
|
|
1069
|
+
rebalancing_order_proposal.approve()
|
|
1070
|
+
rebalancing_order_proposal.save()
|
|
1069
1071
|
# we approve the rebalancing order proposal
|
|
1070
1072
|
assert rebalancing_order_proposal.status == "APPROVED"
|
|
1071
|
-
rebalancing_order_proposal.apply(replay=False)
|
|
1072
|
-
rebalancing_order_proposal.save()
|
|
1073
1073
|
|
|
1074
1074
|
# check that the rebalancing was applied and position reflect that
|
|
1075
1075
|
assert portfolio.assets.get(date=rebalancing_date, underlying_instrument=i1).weighting == Decimal("0.5")
|
|
@@ -1131,7 +1131,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1131
1131
|
PortfolioPortfolioThroughModel.objects.create(
|
|
1132
1132
|
portfolio=lookthrough_portfolio,
|
|
1133
1133
|
dependency_portfolio=primary_portfolio,
|
|
1134
|
-
type=PortfolioPortfolioThroughModel.Type.
|
|
1134
|
+
type=PortfolioPortfolioThroughModel.Type.LOOK_THROUGH,
|
|
1135
1135
|
)
|
|
1136
1136
|
|
|
1137
1137
|
primary_portfolio.handle_controlling_portfolio_change_at_date(weekday)
|
|
@@ -14,10 +14,6 @@ fake = Faker()
|
|
|
14
14
|
|
|
15
15
|
@pytest.mark.django_db
|
|
16
16
|
class TestAdjustmentModel:
|
|
17
|
-
@pytest.fixture()
|
|
18
|
-
def applied_adjustment(self):
|
|
19
|
-
return AdjustmentFactory.create(status=Adjustment.Status.APPLIED)
|
|
20
|
-
|
|
21
17
|
@pytest.fixture()
|
|
22
18
|
def old_adjustment(self):
|
|
23
19
|
return AdjustmentFactory.create(status=Adjustment.Status.PENDING, date=fake.past_date())
|
|
@@ -205,7 +201,7 @@ class TestAdjustmentModel:
|
|
|
205
201
|
post_adjustment_on_prices(adjustment.id)
|
|
206
202
|
a1.refresh_from_db()
|
|
207
203
|
adjustment.refresh_from_db()
|
|
208
|
-
a1.applied_adjustment == adjustment
|
|
204
|
+
assert a1.applied_adjustment == adjustment
|
|
209
205
|
assert adjustment.status == Adjustment.Status.APPLIED
|
|
210
206
|
|
|
211
207
|
@patch("wbportfolio.models.adjustments.send_notification")
|
|
@@ -230,5 +226,4 @@ class TestAdjustmentModel:
|
|
|
230
226
|
mock_check_fct.return_value = False
|
|
231
227
|
post_adjustment_on_prices(adjustment.id)
|
|
232
228
|
adjustment.refresh_from_db()
|
|
233
|
-
mock_delay_fct.call_args[0] == user_porftolio_manager.id
|
|
234
229
|
assert adjustment.status == Adjustment.Status.PENDING
|