wbportfolio 1.54.14__py2.py3-none-any.whl → 1.54.16__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/__init__.py +2 -0
- wbportfolio/admin/orders/__init__.py +2 -0
- wbportfolio/admin/orders/order_proposals.py +14 -0
- wbportfolio/admin/orders/orders.py +30 -0
- wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
- wbportfolio/admin/transactions/__init__.py +0 -1
- wbportfolio/admin/transactions/trades.py +2 -17
- wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
- wbportfolio/factories/__init__.py +2 -1
- wbportfolio/factories/orders/__init__.py +2 -0
- wbportfolio/factories/orders/order_proposals.py +17 -0
- wbportfolio/factories/orders/orders.py +21 -0
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/factories/trades.py +2 -13
- wbportfolio/filters/orders/__init__.py +1 -0
- wbportfolio/filters/orders/orders.py +11 -0
- wbportfolio/import_export/handlers/trade.py +20 -20
- wbportfolio/import_export/resources/trades.py +2 -2
- wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
- wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
- wbportfolio/models/__init__.py +2 -0
- wbportfolio/models/orders/__init__.py +2 -0
- wbportfolio/models/{transactions/trade_proposals.py → orders/order_proposals.py} +304 -264
- wbportfolio/models/orders/orders.py +243 -0
- wbportfolio/models/portfolio.py +16 -19
- wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +18 -18
- wbportfolio/models/transactions/__init__.py +0 -2
- wbportfolio/models/transactions/trades.py +10 -450
- wbportfolio/pms/analytics/portfolio.py +10 -6
- wbportfolio/pms/analytics/utils.py +9 -0
- wbportfolio/pms/trading/handler.py +28 -27
- wbportfolio/pms/typing.py +18 -7
- wbportfolio/rebalancing/decorators.py +1 -1
- wbportfolio/rebalancing/models/composite.py +3 -7
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +3 -1
- wbportfolio/serializers/__init__.py +1 -0
- wbportfolio/serializers/orders/__init__.py +2 -0
- wbportfolio/serializers/{transactions/trade_proposals.py → orders/order_proposals.py} +30 -17
- wbportfolio/serializers/orders/orders.py +187 -0
- wbportfolio/serializers/portfolios.py +7 -7
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/serializers/transactions/__init__.py +1 -5
- wbportfolio/serializers/transactions/trades.py +1 -182
- wbportfolio/tests/conftest.py +4 -2
- wbportfolio/tests/models/orders/__init__.py +0 -0
- wbportfolio/tests/models/{transactions/test_trade_proposals.py → orders/test_order_proposals.py} +214 -250
- wbportfolio/tests/models/test_portfolios.py +11 -10
- wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
- wbportfolio/tests/models/transactions/test_trades.py +0 -20
- wbportfolio/tests/rebalancing/test_models.py +24 -28
- wbportfolio/tests/signals.py +10 -10
- wbportfolio/tests/tests.py +1 -1
- wbportfolio/urls.py +7 -7
- wbportfolio/viewsets/__init__.py +2 -0
- wbportfolio/viewsets/configs/buttons/__init__.py +2 -3
- wbportfolio/viewsets/configs/buttons/trades.py +0 -8
- wbportfolio/viewsets/configs/display/__init__.py +0 -2
- wbportfolio/viewsets/configs/display/portfolios.py +5 -5
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/display/trades.py +1 -225
- wbportfolio/viewsets/configs/endpoints/__init__.py +0 -3
- wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
- wbportfolio/viewsets/orders/__init__.py +6 -0
- wbportfolio/viewsets/orders/configs/__init__.py +4 -0
- wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
- wbportfolio/viewsets/{configs/buttons/trade_proposals.py → orders/configs/buttons/order_proposals.py} +21 -21
- wbportfolio/viewsets/orders/configs/buttons/orders.py +9 -0
- wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
- wbportfolio/viewsets/{configs/display/trade_proposals.py → orders/configs/displays/order_proposals.py} +21 -21
- wbportfolio/viewsets/orders/configs/displays/orders.py +180 -0
- wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +26 -0
- wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
- wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
- wbportfolio/viewsets/{transactions/trade_proposals.py → orders/order_proposals.py} +46 -46
- wbportfolio/viewsets/orders/orders.py +219 -0
- wbportfolio/viewsets/portfolios.py +12 -12
- wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
- wbportfolio/viewsets/transactions/__init__.py +1 -7
- wbportfolio/viewsets/transactions/trades.py +1 -199
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/RECORD +85 -58
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/licenses/LICENSE +0 -0
wbportfolio/tests/models/{transactions/test_trade_proposals.py → orders/test_order_proposals.py}
RENAMED
|
@@ -7,7 +7,7 @@ import pytest
|
|
|
7
7
|
from faker import Faker
|
|
8
8
|
from pandas._libs.tslibs.offsets import BDay, BusinessMonthEnd
|
|
9
9
|
|
|
10
|
-
from wbportfolio.models import
|
|
10
|
+
from wbportfolio.models import Order, OrderProposal, Portfolio, RebalancingModel
|
|
11
11
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
12
12
|
from wbportfolio.pms.typing import Position
|
|
13
13
|
|
|
@@ -16,54 +16,55 @@ fake = Faker()
|
|
|
16
16
|
|
|
17
17
|
# Mark tests to use Django's database
|
|
18
18
|
@pytest.mark.django_db
|
|
19
|
-
class
|
|
19
|
+
class TestOrderProposal:
|
|
20
|
+
def test_init(self, order_proposal):
|
|
21
|
+
assert order_proposal.id is not None
|
|
22
|
+
|
|
20
23
|
# Test that the checked object is correctly set to the portfolio
|
|
21
|
-
def test_checked_object(self,
|
|
24
|
+
def test_checked_object(self, order_proposal):
|
|
22
25
|
"""
|
|
23
|
-
Verify that the checked object is the portfolio associated with the
|
|
26
|
+
Verify that the checked object is the portfolio associated with the order proposal.
|
|
24
27
|
"""
|
|
25
|
-
assert
|
|
28
|
+
assert order_proposal.checked_object == order_proposal.portfolio
|
|
26
29
|
|
|
27
30
|
# Test that the evaluation date matches the trade date
|
|
28
|
-
def test_check_evaluation_date(self,
|
|
31
|
+
def test_check_evaluation_date(self, order_proposal):
|
|
29
32
|
"""
|
|
30
33
|
Ensure the evaluation date is the same as the trade date.
|
|
31
34
|
"""
|
|
32
|
-
assert
|
|
35
|
+
assert order_proposal.check_evaluation_date == order_proposal.trade_date
|
|
33
36
|
|
|
34
37
|
# Test the validated trading service functionality
|
|
35
|
-
def test_validated_trading_service(self,
|
|
38
|
+
def test_validated_trading_service(self, order_proposal, asset_position_factory, order_factory):
|
|
36
39
|
"""
|
|
37
40
|
Validate that the effective and target portfolios are correctly calculated.
|
|
38
41
|
"""
|
|
39
|
-
effective_date = (
|
|
42
|
+
effective_date = (order_proposal.trade_date - BDay(1)).date()
|
|
40
43
|
|
|
41
44
|
# Create asset positions for testing
|
|
42
45
|
a1 = asset_position_factory.create(
|
|
43
|
-
portfolio=
|
|
46
|
+
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.3")
|
|
44
47
|
)
|
|
45
48
|
a2 = asset_position_factory.create(
|
|
46
|
-
portfolio=
|
|
49
|
+
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.7")
|
|
47
50
|
)
|
|
48
51
|
|
|
49
|
-
# Create
|
|
50
|
-
t1 =
|
|
51
|
-
|
|
52
|
+
# Create orders for testing
|
|
53
|
+
t1 = order_factory.create(
|
|
54
|
+
order_proposal=order_proposal,
|
|
52
55
|
weighting=Decimal("0.05"),
|
|
53
|
-
portfolio=
|
|
54
|
-
transaction_date=trade_proposal.trade_date,
|
|
56
|
+
portfolio=order_proposal.portfolio,
|
|
55
57
|
underlying_instrument=a1.underlying_quote,
|
|
56
58
|
)
|
|
57
|
-
t2 =
|
|
58
|
-
|
|
59
|
+
t2 = order_factory.create(
|
|
60
|
+
order_proposal=order_proposal,
|
|
59
61
|
weighting=Decimal("-0.05"),
|
|
60
|
-
portfolio=
|
|
61
|
-
transaction_date=trade_proposal.trade_date,
|
|
62
|
+
portfolio=order_proposal.portfolio,
|
|
62
63
|
underlying_instrument=a2.underlying_quote,
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
# Get the validated trading service
|
|
66
|
-
validated_trading_service =
|
|
67
|
+
validated_trading_service = order_proposal.validated_trading_service
|
|
67
68
|
|
|
68
69
|
# Assert effective and target portfolios are as expected
|
|
69
70
|
assert validated_trading_service._effective_portfolio.to_dict() == {
|
|
@@ -76,116 +77,116 @@ class TestTradeProposal:
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
# Test the calculation of the last effective date
|
|
79
|
-
def test_last_effective_date(self,
|
|
80
|
+
def test_last_effective_date(self, order_proposal, asset_position_factory):
|
|
80
81
|
"""
|
|
81
82
|
Verify the last effective date is correctly determined based on asset positions.
|
|
82
83
|
"""
|
|
83
84
|
# Without any positions, it should be the day before the trade date
|
|
84
85
|
assert (
|
|
85
|
-
|
|
86
|
+
order_proposal.last_effective_date == (order_proposal.trade_date - BDay(1)).date()
|
|
86
87
|
), "Last effective date without position should be t-1"
|
|
87
88
|
|
|
88
89
|
# Create an asset position before the trade date
|
|
89
90
|
a1 = asset_position_factory.create(
|
|
90
|
-
portfolio=
|
|
91
|
+
portfolio=order_proposal.portfolio, date=(order_proposal.trade_date - BDay(5)).date()
|
|
91
92
|
)
|
|
92
|
-
a_noise = asset_position_factory.create(portfolio=
|
|
93
|
+
a_noise = asset_position_factory.create(portfolio=order_proposal.portfolio, date=order_proposal.trade_date) # noqa
|
|
93
94
|
|
|
94
95
|
# The last effective date should still be the day before the trade date due to caching
|
|
95
96
|
assert (
|
|
96
|
-
|
|
97
|
+
order_proposal.last_effective_date == (order_proposal.trade_date - BDay(1)).date()
|
|
97
98
|
), "last effective date is cached, so it won't change as is"
|
|
98
99
|
|
|
99
100
|
# Reset the cache property to recalculate
|
|
100
|
-
del
|
|
101
|
+
del order_proposal.last_effective_date
|
|
101
102
|
|
|
102
103
|
# Now it should be the date of the latest position before the trade date
|
|
103
104
|
assert (
|
|
104
|
-
|
|
105
|
+
order_proposal.last_effective_date == a1.date
|
|
105
106
|
), "last effective date is the latest position strictly lower than trade date"
|
|
106
107
|
|
|
107
|
-
# Test finding the previous
|
|
108
|
-
def
|
|
108
|
+
# Test finding the previous order proposal
|
|
109
|
+
def test_previous_order_proposal(self, order_proposal_factory):
|
|
109
110
|
"""
|
|
110
|
-
Ensure the previous
|
|
111
|
+
Ensure the previous order proposal is correctly identified as the last approved proposal before the current one.
|
|
111
112
|
"""
|
|
112
|
-
tp =
|
|
113
|
-
tp_previous_submit =
|
|
114
|
-
portfolio=tp.portfolio, status=
|
|
113
|
+
tp = order_proposal_factory.create()
|
|
114
|
+
tp_previous_submit = order_proposal_factory.create( # noqa
|
|
115
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.SUBMIT, trade_date=(tp.trade_date - BDay(1)).date()
|
|
115
116
|
)
|
|
116
|
-
tp_previous_approve =
|
|
117
|
-
portfolio=tp.portfolio, status=
|
|
117
|
+
tp_previous_approve = order_proposal_factory.create(
|
|
118
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(2)).date()
|
|
118
119
|
)
|
|
119
|
-
tp_next_approve =
|
|
120
|
-
portfolio=tp.portfolio, status=
|
|
120
|
+
tp_next_approve = order_proposal_factory.create( # noqa
|
|
121
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(1)).date()
|
|
121
122
|
)
|
|
122
123
|
|
|
123
|
-
# The previous valid
|
|
124
|
+
# The previous valid order proposal should be the approved one strictly before the current proposal
|
|
124
125
|
assert (
|
|
125
|
-
tp.
|
|
126
|
-
), "the previous valid
|
|
126
|
+
tp.previous_order_proposal == tp_previous_approve
|
|
127
|
+
), "the previous valid order proposal is the strictly before and approved order proposal"
|
|
127
128
|
|
|
128
|
-
# Test finding the next
|
|
129
|
-
def
|
|
129
|
+
# Test finding the next order proposal
|
|
130
|
+
def test_next_order_proposal(self, order_proposal_factory):
|
|
130
131
|
"""
|
|
131
|
-
Verify the next
|
|
132
|
+
Verify the next order proposal is correctly identified as the first approved proposal after the current one.
|
|
132
133
|
"""
|
|
133
|
-
tp =
|
|
134
|
-
tp_previous_approve =
|
|
135
|
-
portfolio=tp.portfolio, status=
|
|
134
|
+
tp = order_proposal_factory.create()
|
|
135
|
+
tp_previous_approve = order_proposal_factory.create( # noqa
|
|
136
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
|
|
136
137
|
)
|
|
137
|
-
tp_next_submit =
|
|
138
|
-
portfolio=tp.portfolio, status=
|
|
138
|
+
tp_next_submit = order_proposal_factory.create( # noqa
|
|
139
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.SUBMIT, trade_date=(tp.trade_date + BDay(1)).date()
|
|
139
140
|
)
|
|
140
|
-
tp_next_approve =
|
|
141
|
-
portfolio=tp.portfolio, status=
|
|
141
|
+
tp_next_approve = order_proposal_factory.create(
|
|
142
|
+
portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(2)).date()
|
|
142
143
|
)
|
|
143
144
|
|
|
144
|
-
# The next valid
|
|
145
|
+
# The next valid order proposal should be the approved one strictly after the current proposal
|
|
145
146
|
assert (
|
|
146
|
-
tp.
|
|
147
|
-
), "the next valid
|
|
147
|
+
tp.next_order_proposal == tp_next_approve
|
|
148
|
+
), "the next valid order proposal is the strictly after and approved order proposal"
|
|
148
149
|
|
|
149
150
|
# Test getting the default target portfolio
|
|
150
|
-
def test__get_default_target_portfolio(self,
|
|
151
|
+
def test__get_default_target_portfolio(self, order_proposal, asset_position_factory):
|
|
151
152
|
"""
|
|
152
153
|
Ensure the default target portfolio is set to the effective portfolio from the day before the trade date.
|
|
153
154
|
"""
|
|
154
|
-
effective_date = (
|
|
155
|
+
effective_date = (order_proposal.trade_date - BDay(1)).date()
|
|
155
156
|
|
|
156
157
|
# Create asset positions for testing
|
|
157
158
|
a1 = asset_position_factory.create(
|
|
158
|
-
portfolio=
|
|
159
|
+
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.3")
|
|
159
160
|
)
|
|
160
161
|
a2 = asset_position_factory.create(
|
|
161
|
-
portfolio=
|
|
162
|
+
portfolio=order_proposal.portfolio, date=effective_date, weighting=Decimal("0.7")
|
|
162
163
|
)
|
|
163
|
-
asset_position_factory.create(portfolio=
|
|
164
|
+
asset_position_factory.create(portfolio=order_proposal.portfolio, date=order_proposal.trade_date) # noise
|
|
164
165
|
|
|
165
166
|
# The default target portfolio should match the effective portfolio
|
|
166
|
-
assert
|
|
167
|
+
assert order_proposal._get_default_target_portfolio().to_dict() == {
|
|
167
168
|
a1.underlying_quote.id: a1.weighting,
|
|
168
169
|
a2.underlying_quote.id: a2.weighting,
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
# Test getting the default target portfolio with a rebalancing model
|
|
172
173
|
@patch.object(RebalancingModel, "get_target_portfolio")
|
|
173
|
-
def test__get_default_target_portfolio_with_rebalancer_model(self, mock_fct,
|
|
174
|
+
def test__get_default_target_portfolio_with_rebalancer_model(self, mock_fct, order_proposal, rebalancer_factory):
|
|
174
175
|
"""
|
|
175
176
|
Verify that the target portfolio is correctly obtained from a rebalancing model.
|
|
176
177
|
"""
|
|
177
178
|
# Expected target portfolio from the rebalancing model
|
|
178
179
|
expected_target_portfolio = PortfolioDTO(
|
|
179
|
-
positions=(Position(underlying_instrument=1, weighting=Decimal(1), date=
|
|
180
|
+
positions=(Position(underlying_instrument=1, weighting=Decimal(1), date=order_proposal.trade_date),)
|
|
180
181
|
)
|
|
181
182
|
mock_fct.return_value = expected_target_portfolio
|
|
182
183
|
|
|
183
184
|
# Create a rebalancer for testing
|
|
184
185
|
rebalancer = rebalancer_factory.create(
|
|
185
|
-
portfolio=
|
|
186
|
+
portfolio=order_proposal.portfolio, parameters={"rebalancer_parameter": "A"}
|
|
186
187
|
)
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
order_proposal.rebalancing_model = rebalancer.rebalancing_model
|
|
189
|
+
order_proposal.save()
|
|
189
190
|
|
|
190
191
|
# Additional keyword arguments for the rebalancing model
|
|
191
192
|
extra_kwargs = {"test": "test"}
|
|
@@ -196,41 +197,38 @@ class TestTradeProposal:
|
|
|
196
197
|
|
|
197
198
|
# Assert the target portfolio matches the expected output from the rebalancing model
|
|
198
199
|
assert (
|
|
199
|
-
|
|
200
|
+
order_proposal._get_default_target_portfolio(**extra_kwargs) == expected_target_portfolio
|
|
200
201
|
), "We expect the target portfolio to be whatever is returned by the rebalancer model"
|
|
201
202
|
mock_fct.assert_called_once_with(
|
|
202
|
-
|
|
203
|
+
order_proposal.portfolio, order_proposal.trade_date, order_proposal.last_effective_date, **expected_kwargs
|
|
203
204
|
)
|
|
204
205
|
|
|
205
|
-
# Test normalizing
|
|
206
|
-
def
|
|
206
|
+
# Test normalizing orders
|
|
207
|
+
def test_normalize_orders(self, order_proposal, order_factory):
|
|
207
208
|
"""
|
|
208
|
-
Ensure
|
|
209
|
+
Ensure orders are normalized to sum up to 1, handling quantization errors.
|
|
209
210
|
"""
|
|
210
|
-
# Create
|
|
211
|
-
t1 =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
portfolio=trade_proposal.portfolio,
|
|
211
|
+
# Create orders for testing
|
|
212
|
+
t1 = order_factory.create(
|
|
213
|
+
order_proposal=order_proposal,
|
|
214
|
+
portfolio=order_proposal.portfolio,
|
|
215
215
|
weighting=Decimal(0.2),
|
|
216
216
|
)
|
|
217
|
-
t2 =
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
portfolio=trade_proposal.portfolio,
|
|
217
|
+
t2 = order_factory.create(
|
|
218
|
+
order_proposal=order_proposal,
|
|
219
|
+
portfolio=order_proposal.portfolio,
|
|
221
220
|
weighting=Decimal(0.26),
|
|
222
221
|
)
|
|
223
|
-
t3 =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
portfolio=trade_proposal.portfolio,
|
|
222
|
+
t3 = order_factory.create(
|
|
223
|
+
order_proposal=order_proposal,
|
|
224
|
+
portfolio=order_proposal.portfolio,
|
|
227
225
|
weighting=Decimal(0.14),
|
|
228
226
|
)
|
|
229
227
|
|
|
230
|
-
# Normalize
|
|
231
|
-
|
|
228
|
+
# Normalize orders
|
|
229
|
+
order_proposal.normalize_orders()
|
|
232
230
|
|
|
233
|
-
# Refresh
|
|
231
|
+
# Refresh orders from the database
|
|
234
232
|
t1.refresh_from_db()
|
|
235
233
|
t2.refresh_from_db()
|
|
236
234
|
t3.refresh_from_db()
|
|
@@ -249,23 +247,23 @@ class TestTradeProposal:
|
|
|
249
247
|
assert t2.weighting == normalized_t2_weight + quantize_error # Add quantize error to the largest position
|
|
250
248
|
assert t3.weighting == normalized_t3_weight
|
|
251
249
|
|
|
252
|
-
# Test resetting
|
|
253
|
-
def
|
|
250
|
+
# Test resetting orders
|
|
251
|
+
def test_reset_orders(self, order_proposal, instrument_factory, instrument_price_factory, asset_position_factory):
|
|
254
252
|
"""
|
|
255
|
-
Verify
|
|
253
|
+
Verify orders are correctly reset based on effective and target portfolios.
|
|
256
254
|
"""
|
|
257
|
-
effective_date =
|
|
255
|
+
effective_date = order_proposal.last_effective_date
|
|
258
256
|
|
|
259
257
|
# Create instruments for testing
|
|
260
|
-
i1 = instrument_factory.create(currency=
|
|
261
|
-
i2 = instrument_factory.create(currency=
|
|
262
|
-
i3 = instrument_factory.create(currency=
|
|
258
|
+
i1 = instrument_factory.create(currency=order_proposal.portfolio.currency)
|
|
259
|
+
i2 = instrument_factory.create(currency=order_proposal.portfolio.currency)
|
|
260
|
+
i3 = instrument_factory.create(currency=order_proposal.portfolio.currency)
|
|
263
261
|
# Build initial effective portfolio constituting only from two positions of i1 and i2
|
|
264
262
|
asset_position_factory.create(
|
|
265
|
-
portfolio=
|
|
263
|
+
portfolio=order_proposal.portfolio, date=effective_date, underlying_instrument=i1, weighting=Decimal("0.7")
|
|
266
264
|
)
|
|
267
265
|
asset_position_factory.create(
|
|
268
|
-
portfolio=
|
|
266
|
+
portfolio=order_proposal.portfolio, date=effective_date, underlying_instrument=i2, weighting=Decimal("0.3")
|
|
269
267
|
)
|
|
270
268
|
p1 = instrument_price_factory.create(instrument=i1, date=effective_date)
|
|
271
269
|
p2 = instrument_price_factory.create(instrument=i2, date=effective_date)
|
|
@@ -276,26 +274,26 @@ class TestTradeProposal:
|
|
|
276
274
|
[
|
|
277
275
|
Position(
|
|
278
276
|
underlying_instrument=i2.id,
|
|
279
|
-
date=
|
|
277
|
+
date=order_proposal.trade_date,
|
|
280
278
|
weighting=Decimal("0.4"),
|
|
281
279
|
price=float(p2.net_value),
|
|
282
280
|
),
|
|
283
281
|
Position(
|
|
284
282
|
underlying_instrument=i3.id,
|
|
285
|
-
date=
|
|
283
|
+
date=order_proposal.trade_date,
|
|
286
284
|
weighting=Decimal("0.6"),
|
|
287
285
|
price=float(p3.net_value),
|
|
288
286
|
),
|
|
289
287
|
]
|
|
290
288
|
)
|
|
291
289
|
|
|
292
|
-
# Reset
|
|
293
|
-
|
|
290
|
+
# Reset orders
|
|
291
|
+
order_proposal.reset_orders(target_portfolio=target_portfolio)
|
|
294
292
|
|
|
295
|
-
# Get
|
|
296
|
-
t1 =
|
|
297
|
-
t2 =
|
|
298
|
-
t3 =
|
|
293
|
+
# Get orders for each instrument
|
|
294
|
+
t1 = order_proposal.orders.get(underlying_instrument=i1)
|
|
295
|
+
t2 = order_proposal.orders.get(underlying_instrument=i2)
|
|
296
|
+
t3 = order_proposal.orders.get(underlying_instrument=i3)
|
|
299
297
|
|
|
300
298
|
# Assert trade weights are correctly reset
|
|
301
299
|
assert t1.weighting == Decimal("-0.7")
|
|
@@ -307,27 +305,27 @@ class TestTradeProposal:
|
|
|
307
305
|
[
|
|
308
306
|
Position(
|
|
309
307
|
underlying_instrument=i1.id,
|
|
310
|
-
date=
|
|
308
|
+
date=order_proposal.trade_date,
|
|
311
309
|
weighting=Decimal("0.2"),
|
|
312
310
|
price=float(p1.net_value),
|
|
313
311
|
),
|
|
314
312
|
Position(
|
|
315
313
|
underlying_instrument=i2.id,
|
|
316
|
-
date=
|
|
314
|
+
date=order_proposal.trade_date,
|
|
317
315
|
weighting=Decimal("0.3"),
|
|
318
316
|
price=float(p2.net_value),
|
|
319
317
|
),
|
|
320
318
|
Position(
|
|
321
319
|
underlying_instrument=i3.id,
|
|
322
|
-
date=
|
|
320
|
+
date=order_proposal.trade_date,
|
|
323
321
|
weighting=Decimal("0.5"),
|
|
324
322
|
price=float(p3.net_value),
|
|
325
323
|
),
|
|
326
324
|
]
|
|
327
325
|
)
|
|
328
326
|
|
|
329
|
-
|
|
330
|
-
# Refetch the
|
|
327
|
+
order_proposal.reset_orders(target_portfolio=new_target_portfolio)
|
|
328
|
+
# Refetch the orders for each instrument
|
|
331
329
|
t1.refresh_from_db()
|
|
332
330
|
t2.refresh_from_db()
|
|
333
331
|
t3.refresh_from_db()
|
|
@@ -336,48 +334,44 @@ class TestTradeProposal:
|
|
|
336
334
|
assert t2.weighting == Decimal("0")
|
|
337
335
|
assert t3.weighting == Decimal("0.5")
|
|
338
336
|
|
|
339
|
-
def
|
|
337
|
+
def test_reset_orders_remove_invalid_orders(self, order_proposal, order_factory, instrument_price_factory):
|
|
340
338
|
# create a invalid trade and its price
|
|
341
|
-
invalid_trade =
|
|
342
|
-
instrument_price_factory.create(
|
|
343
|
-
date=invalid_trade.transaction_date, instrument=invalid_trade.underlying_instrument
|
|
344
|
-
)
|
|
339
|
+
invalid_trade = order_factory.create(order_proposal=order_proposal, weighting=Decimal(0))
|
|
340
|
+
instrument_price_factory.create(date=invalid_trade.value_date, instrument=invalid_trade.underlying_instrument)
|
|
345
341
|
|
|
346
342
|
# create a valid trade and its price
|
|
347
|
-
valid_trade =
|
|
348
|
-
instrument_price_factory.create(
|
|
349
|
-
date=valid_trade.transaction_date, instrument=valid_trade.underlying_instrument
|
|
350
|
-
)
|
|
343
|
+
valid_trade = order_factory.create(order_proposal=order_proposal, weighting=Decimal(1))
|
|
344
|
+
instrument_price_factory.create(date=valid_trade.value_date, instrument=valid_trade.underlying_instrument)
|
|
351
345
|
|
|
352
|
-
|
|
353
|
-
assert
|
|
346
|
+
order_proposal.reset_orders()
|
|
347
|
+
assert order_proposal.orders.get(underlying_instrument=valid_trade.underlying_instrument).weighting == Decimal(
|
|
354
348
|
"1"
|
|
355
349
|
)
|
|
356
|
-
with pytest.raises(
|
|
357
|
-
|
|
350
|
+
with pytest.raises(Order.DoesNotExist):
|
|
351
|
+
order_proposal.orders.get(underlying_instrument=invalid_trade.underlying_instrument)
|
|
358
352
|
|
|
359
|
-
# Test replaying
|
|
353
|
+
# Test replaying order proposals
|
|
360
354
|
@patch.object(Portfolio, "drift_weights")
|
|
361
|
-
def test_replay(self, mock_fct,
|
|
355
|
+
def test_replay(self, mock_fct, order_proposal_factory):
|
|
362
356
|
"""
|
|
363
|
-
Ensure replaying
|
|
357
|
+
Ensure replaying order proposals correctly calls drift_weights for each period.
|
|
364
358
|
"""
|
|
365
359
|
mock_fct.return_value = None, None
|
|
366
360
|
|
|
367
|
-
# Create approved
|
|
368
|
-
tp0 =
|
|
369
|
-
tp1 =
|
|
361
|
+
# Create approved order proposals for testing
|
|
362
|
+
tp0 = order_proposal_factory.create(status=OrderProposal.Status.APPROVED)
|
|
363
|
+
tp1 = order_proposal_factory.create(
|
|
370
364
|
portfolio=tp0.portfolio,
|
|
371
|
-
status=
|
|
365
|
+
status=OrderProposal.Status.APPROVED,
|
|
372
366
|
trade_date=(tp0.trade_date + BusinessMonthEnd(1)).date(),
|
|
373
367
|
)
|
|
374
|
-
tp2 =
|
|
368
|
+
tp2 = order_proposal_factory.create(
|
|
375
369
|
portfolio=tp0.portfolio,
|
|
376
|
-
status=
|
|
370
|
+
status=OrderProposal.Status.APPROVED,
|
|
377
371
|
trade_date=(tp1.trade_date + BusinessMonthEnd(1)).date(),
|
|
378
372
|
)
|
|
379
373
|
|
|
380
|
-
# Replay
|
|
374
|
+
# Replay order proposals
|
|
381
375
|
tp0.replay()
|
|
382
376
|
|
|
383
377
|
# Expected calls to drift_weights
|
|
@@ -391,7 +385,7 @@ class TestTradeProposal:
|
|
|
391
385
|
mock_fct.assert_has_calls(expected_calls)
|
|
392
386
|
|
|
393
387
|
# Test stopping replay on a non-approved proposal
|
|
394
|
-
tp1.status =
|
|
388
|
+
tp1.status = OrderProposal.Status.FAILED
|
|
395
389
|
tp1.save()
|
|
396
390
|
expected_calls = [call(tp0.trade_date, tp1.trade_date - timedelta(days=1), stop_at_rebalancing=True)]
|
|
397
391
|
mock_fct.assert_has_calls(expected_calls)
|
|
@@ -399,18 +393,18 @@ class TestTradeProposal:
|
|
|
399
393
|
# Test estimating shares for a trade
|
|
400
394
|
@patch.object(Portfolio, "get_total_asset_value")
|
|
401
395
|
def test_get_estimated_shares(
|
|
402
|
-
self, mock_fct,
|
|
396
|
+
self, mock_fct, order_proposal, order_factory, instrument_price_factory, instrument_factory
|
|
403
397
|
):
|
|
404
398
|
"""
|
|
405
399
|
Verify shares estimation based on trade weighting and instrument price.
|
|
406
400
|
"""
|
|
407
|
-
portfolio =
|
|
401
|
+
portfolio = order_proposal.portfolio
|
|
408
402
|
instrument = instrument_factory.create(currency=portfolio.currency)
|
|
409
|
-
underlying_quote_price = instrument_price_factory.create(instrument=instrument, date=
|
|
403
|
+
underlying_quote_price = instrument_price_factory.create(instrument=instrument, date=order_proposal.trade_date)
|
|
410
404
|
mock_fct.return_value = Decimal(1_000_000) # 1 million cash
|
|
411
|
-
trade =
|
|
412
|
-
|
|
413
|
-
|
|
405
|
+
trade = order_factory.create(
|
|
406
|
+
order_proposal=order_proposal,
|
|
407
|
+
value_date=order_proposal.trade_date,
|
|
414
408
|
portfolio=portfolio,
|
|
415
409
|
underlying_instrument=instrument,
|
|
416
410
|
)
|
|
@@ -418,105 +412,95 @@ class TestTradeProposal:
|
|
|
418
412
|
|
|
419
413
|
# Assert estimated shares are correctly calculated
|
|
420
414
|
assert (
|
|
421
|
-
|
|
415
|
+
order_proposal.get_estimated_shares(
|
|
422
416
|
trade.weighting, trade.underlying_instrument, underlying_quote_price.net_value
|
|
423
417
|
)
|
|
424
418
|
== Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
|
|
425
419
|
)
|
|
426
420
|
|
|
427
421
|
@patch.object(Portfolio, "get_total_asset_value")
|
|
428
|
-
def test_get_estimated_target_cash(self, mock_fct,
|
|
429
|
-
|
|
430
|
-
|
|
422
|
+
def test_get_estimated_target_cash(self, mock_fct, order_proposal, order_factory, cash_factory):
|
|
423
|
+
order_proposal.portfolio.only_weighting = False
|
|
424
|
+
order_proposal.portfolio.save()
|
|
431
425
|
mock_fct.return_value = Decimal(1_000_000) # 1 million cash
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
portfolio=trade_proposal.portfolio,
|
|
426
|
+
order_factory.create( # equity trade
|
|
427
|
+
order_proposal=order_proposal,
|
|
428
|
+
value_date=order_proposal.trade_date,
|
|
429
|
+
portfolio=order_proposal.portfolio,
|
|
437
430
|
weighting=Decimal("0.7"),
|
|
438
431
|
)
|
|
439
|
-
trade_factory.create( # cash trade
|
|
440
|
-
trade_proposal=trade_proposal,
|
|
441
|
-
transaction_date=trade_proposal.trade_date,
|
|
442
|
-
portfolio=trade_proposal.portfolio,
|
|
443
|
-
underlying_instrument=cash,
|
|
444
|
-
weighting=Decimal("0.2"),
|
|
445
|
-
)
|
|
446
432
|
|
|
447
|
-
target_cash_position =
|
|
448
|
-
assert target_cash_position.weighting == Decimal("0.
|
|
433
|
+
target_cash_position = order_proposal.get_estimated_target_cash()
|
|
434
|
+
assert target_cash_position.weighting == Decimal("0.3")
|
|
449
435
|
assert target_cash_position.initial_shares == Decimal(1_000_000) * Decimal("0.3")
|
|
450
436
|
|
|
451
|
-
def
|
|
452
|
-
# Check that if we create a prior
|
|
437
|
+
def test_order_proposal_update_inception_date(self, order_proposal_factory, portfolio, instrument_factory):
|
|
438
|
+
# Check that if we create a prior order proposal, the instrument inception date is updated accordingly
|
|
453
439
|
instrument = instrument_factory.create(inception_date=None)
|
|
454
440
|
instrument.portfolios.add(portfolio)
|
|
455
|
-
tp =
|
|
441
|
+
tp = order_proposal_factory.create(portfolio=portfolio)
|
|
456
442
|
instrument.refresh_from_db()
|
|
457
443
|
assert instrument.inception_date == (tp.trade_date + BDay(1)).date()
|
|
458
444
|
|
|
459
|
-
tp2 =
|
|
445
|
+
tp2 = order_proposal_factory.create(portfolio=portfolio, trade_date=tp.trade_date - BDay(1))
|
|
460
446
|
instrument.refresh_from_db()
|
|
461
447
|
assert instrument.inception_date == (tp2.trade_date + BDay(1)).date()
|
|
462
448
|
|
|
463
|
-
def test_get_round_lot_size(self,
|
|
449
|
+
def test_get_round_lot_size(self, order_proposal, instrument):
|
|
464
450
|
# without a round lot size, we expect no normalization of shares
|
|
465
|
-
assert
|
|
451
|
+
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
466
452
|
instrument.round_lot_size = 100
|
|
467
453
|
instrument.save()
|
|
468
454
|
|
|
469
455
|
# if instrument has a round lot size different than 1, we expect different behavior based on whether shares is positive or negative
|
|
470
|
-
assert
|
|
471
|
-
assert
|
|
472
|
-
assert
|
|
456
|
+
assert order_proposal.get_round_lot_size(Decimal(66.0), instrument) == Decimal("100")
|
|
457
|
+
assert order_proposal.get_round_lot_size(Decimal(-66.0), instrument) == Decimal(-66.0)
|
|
458
|
+
assert order_proposal.get_round_lot_size(Decimal(-120), instrument) == Decimal(-200)
|
|
473
459
|
|
|
474
460
|
# exchange can disable rounding based on the lot size
|
|
475
461
|
instrument.exchange.apply_round_lot_size = False
|
|
476
462
|
instrument.exchange.save()
|
|
477
|
-
assert
|
|
463
|
+
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
478
464
|
|
|
479
|
-
def test_submit_round_lot_size(self,
|
|
480
|
-
|
|
481
|
-
|
|
465
|
+
def test_submit_round_lot_size(self, order_proposal, order_factory, instrument):
|
|
466
|
+
order_proposal.portfolio.only_weighting = False
|
|
467
|
+
order_proposal.portfolio.save()
|
|
482
468
|
instrument.round_lot_size = 100
|
|
483
469
|
instrument.save()
|
|
484
|
-
trade =
|
|
485
|
-
status="DRAFT",
|
|
470
|
+
trade = order_factory.create(
|
|
486
471
|
underlying_instrument=instrument,
|
|
487
472
|
shares=70,
|
|
488
|
-
|
|
473
|
+
order_proposal=order_proposal,
|
|
489
474
|
weighting=Decimal("1.0"),
|
|
490
475
|
)
|
|
491
|
-
warnings =
|
|
492
|
-
|
|
476
|
+
warnings = order_proposal.submit()
|
|
477
|
+
order_proposal.save()
|
|
493
478
|
assert (
|
|
494
479
|
len(warnings) == 1
|
|
495
480
|
) # ensure that submit returns a warning concerning the rounded trade based on the lot size
|
|
496
481
|
trade.refresh_from_db()
|
|
497
482
|
assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
|
|
498
483
|
|
|
499
|
-
def test_submit_round_fractional_shares(self,
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
trade =
|
|
503
|
-
status="DRAFT",
|
|
484
|
+
def test_submit_round_fractional_shares(self, order_proposal, order_factory, instrument):
|
|
485
|
+
order_proposal.portfolio.only_weighting = False
|
|
486
|
+
order_proposal.portfolio.save()
|
|
487
|
+
trade = order_factory.create(
|
|
504
488
|
underlying_instrument=instrument,
|
|
505
489
|
shares=5.6,
|
|
506
|
-
|
|
490
|
+
order_proposal=order_proposal,
|
|
507
491
|
weighting=Decimal("1.0"),
|
|
508
492
|
)
|
|
509
|
-
|
|
510
|
-
|
|
493
|
+
order_proposal.submit()
|
|
494
|
+
order_proposal.save()
|
|
511
495
|
trade.refresh_from_db()
|
|
512
496
|
assert trade.shares == 6 # we expect the fractional share to be rounded
|
|
513
497
|
|
|
514
498
|
def test_ex_post(
|
|
515
|
-
self, instrument_factory, asset_position_factory, instrument_price_factory,
|
|
499
|
+
self, instrument_factory, asset_position_factory, instrument_price_factory, order_proposal_factory, portfolio
|
|
516
500
|
):
|
|
517
501
|
"""
|
|
518
502
|
Tests the ex-post rebalancing mechanism of a portfolio with two instruments.
|
|
519
|
-
Verifies that weights are correctly recalculated after submitting and approving a
|
|
503
|
+
Verifies that weights are correctly recalculated after submitting and approving a order proposal.
|
|
520
504
|
"""
|
|
521
505
|
|
|
522
506
|
# --- Create instruments ---
|
|
@@ -575,102 +559,82 @@ class TestTradeProposal:
|
|
|
575
559
|
)
|
|
576
560
|
|
|
577
561
|
# --- Create positions on d3 with weights adjusted for returns ---
|
|
578
|
-
msft_a3 = asset_position_factory.create(
|
|
579
|
-
portfolio=portfolio,
|
|
580
|
-
underlying_quote=msft,
|
|
581
|
-
date=d3,
|
|
582
|
-
initial_shares=10,
|
|
583
|
-
weighting=msft_a2.weighting * (Decimal("1.0") + msft_r3) / portfolio_r3,
|
|
584
|
-
)
|
|
585
|
-
apple_a3 = asset_position_factory.create(
|
|
586
|
-
portfolio=portfolio,
|
|
587
|
-
underlying_quote=apple,
|
|
588
|
-
date=d3,
|
|
589
|
-
initial_shares=1,
|
|
590
|
-
weighting=apple_a2.weighting * (Decimal("1.0") + apple_r3) / portfolio_r3,
|
|
591
|
-
)
|
|
592
562
|
|
|
593
|
-
# Check that weights on
|
|
594
|
-
|
|
595
|
-
assert
|
|
563
|
+
# Check that weights on d2 sum to 1
|
|
564
|
+
total_weight_d2 = msft_a2.weighting + apple_a2.weighting
|
|
565
|
+
assert total_weight_d2 == pytest.approx(Decimal("1.0"), abs=Decimal("1e-6"))
|
|
596
566
|
|
|
597
|
-
# --- Create a
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
# Retrieve
|
|
601
|
-
|
|
602
|
-
|
|
567
|
+
# --- Create a order proposal on d3 ---
|
|
568
|
+
order_proposal = order_proposal_factory.create(portfolio=portfolio, trade_date=d3)
|
|
569
|
+
order_proposal.reset_orders()
|
|
570
|
+
# Retrieve orders for each instrument
|
|
571
|
+
orders = order_proposal.get_orders()
|
|
572
|
+
trade_msft = orders.get(underlying_instrument=msft)
|
|
573
|
+
trade_apple = orders.get(underlying_instrument=apple)
|
|
603
574
|
# Check that trade weights are initially zero
|
|
604
575
|
assert trade_msft.weighting == Decimal("0")
|
|
605
576
|
assert trade_apple.weighting == Decimal("0")
|
|
606
577
|
|
|
578
|
+
msft_drifted = msft_a2.weighting * (Decimal("1.0") + msft_r3) / portfolio_r3
|
|
579
|
+
apple_drifted = apple_a2.weighting * (Decimal("1.0") + apple_r3) / portfolio_r3
|
|
607
580
|
# --- Adjust trade weights to target 50% each ---
|
|
608
581
|
target_weight = Decimal("0.5")
|
|
609
|
-
trade_msft.weighting = target_weight -
|
|
582
|
+
trade_msft.weighting = target_weight - msft_drifted
|
|
610
583
|
trade_msft.save()
|
|
611
584
|
|
|
612
|
-
trade_apple.weighting = target_weight -
|
|
585
|
+
trade_apple.weighting = target_weight - apple_drifted
|
|
613
586
|
trade_apple.save()
|
|
587
|
+
orders = order_proposal.get_orders()
|
|
588
|
+
trade_msft = orders.get(underlying_instrument=msft)
|
|
589
|
+
trade_apple = orders.get(underlying_instrument=apple)
|
|
614
590
|
|
|
615
591
|
# --- Check drift factors and effective weights ---
|
|
616
|
-
assert trade_msft.
|
|
617
|
-
assert trade_apple.
|
|
592
|
+
assert trade_msft.daily_return == pytest.approx(msft_r3, abs=Decimal("1e-6"))
|
|
593
|
+
assert trade_apple.daily_return == pytest.approx(apple_r3, abs=Decimal("1e-6"))
|
|
618
594
|
|
|
619
|
-
assert trade_msft._effective_weight == pytest.approx(
|
|
620
|
-
assert trade_apple._effective_weight == pytest.approx(
|
|
595
|
+
assert trade_msft._effective_weight == pytest.approx(msft_drifted, abs=Decimal("1e-6"))
|
|
596
|
+
assert trade_apple._effective_weight == pytest.approx(apple_drifted, abs=Decimal("1e-6"))
|
|
621
597
|
|
|
622
598
|
# Check that the target weight is the sum of drifted weight and adjustment
|
|
623
|
-
assert trade_msft._target_weight == pytest.approx(
|
|
624
|
-
msft_a2.weighting * trade_msft.drift_factor + trade_msft.weighting,
|
|
625
|
-
abs=Decimal("1e-6"),
|
|
626
|
-
)
|
|
627
599
|
assert trade_msft._target_weight == pytest.approx(target_weight, abs=Decimal("1e-6"))
|
|
628
|
-
|
|
629
|
-
assert trade_apple._target_weight == pytest.approx(
|
|
630
|
-
apple_a2.weighting * trade_apple.drift_factor + trade_apple.weighting,
|
|
631
|
-
abs=Decimal("1e-6"),
|
|
632
|
-
)
|
|
633
600
|
assert trade_apple._target_weight == pytest.approx(target_weight, abs=Decimal("1e-6"))
|
|
634
601
|
|
|
635
|
-
# --- Submit and approve the
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
# --- Refresh positions after ex-post rebalancing ---
|
|
642
|
-
msft_a3.refresh_from_db()
|
|
643
|
-
apple_a3.refresh_from_db()
|
|
602
|
+
# --- Submit and approve the order proposal ---
|
|
603
|
+
order_proposal.submit()
|
|
604
|
+
order_proposal.save()
|
|
605
|
+
order_proposal.approve()
|
|
606
|
+
order_proposal.save()
|
|
644
607
|
|
|
645
608
|
# Final check that weights have been updated to 50%
|
|
646
|
-
assert
|
|
647
|
-
|
|
609
|
+
assert order_proposal.portfolio.assets.get(underlying_instrument=msft).weighting == pytest.approx(
|
|
610
|
+
target_weight, abs=Decimal("1e-6")
|
|
611
|
+
)
|
|
612
|
+
assert order_proposal.portfolio.assets.get(underlying_instrument=apple).weighting == pytest.approx(
|
|
613
|
+
target_weight, abs=Decimal("1e-6")
|
|
614
|
+
)
|
|
648
615
|
|
|
649
|
-
def
|
|
650
|
-
self, instrument, instrument_price_factory,
|
|
616
|
+
def test_replay_reset_draft_order_proposal(
|
|
617
|
+
self, instrument, instrument_price_factory, order_factory, order_proposal_factory
|
|
651
618
|
):
|
|
652
|
-
|
|
653
|
-
status=TradeProposal.Status.DRAFT, trade_date=date.today() - BDay(2)
|
|
654
|
-
)
|
|
619
|
+
order_proposal = order_proposal_factory.create(trade_date=date.today() - BDay(2))
|
|
655
620
|
instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(2))
|
|
656
621
|
instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(1))
|
|
657
622
|
instrument_price_factory.create(instrument=instrument, date=date.today())
|
|
658
|
-
trade =
|
|
623
|
+
trade = order_factory.create(
|
|
659
624
|
underlying_instrument=instrument,
|
|
660
|
-
|
|
625
|
+
order_proposal=order_proposal,
|
|
661
626
|
weighting=1,
|
|
662
|
-
status=TradeProposal.Status.DRAFT,
|
|
663
627
|
)
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
628
|
+
order_proposal.submit()
|
|
629
|
+
order_proposal.approve(replay=False)
|
|
630
|
+
order_proposal.save()
|
|
667
631
|
|
|
668
|
-
draft_tp =
|
|
669
|
-
assert not
|
|
632
|
+
draft_tp = order_proposal_factory.create(portfolio=order_proposal.portfolio, trade_date=date.today() - BDay(1))
|
|
633
|
+
assert not Order.objects.filter(order_proposal=draft_tp).exists()
|
|
670
634
|
|
|
671
|
-
|
|
635
|
+
order_proposal.replay()
|
|
672
636
|
|
|
673
|
-
assert
|
|
674
|
-
assert
|
|
675
|
-
|
|
637
|
+
assert Order.objects.filter(order_proposal=draft_tp).count() == 1
|
|
638
|
+
assert Order.objects.get(
|
|
639
|
+
order_proposal=draft_tp, underlying_instrument=trade.underlying_instrument
|
|
676
640
|
).weighting == Decimal("0")
|