wbportfolio 1.54.13__py2.py3-none-any.whl → 1.54.15__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} +289 -245
- wbportfolio/models/orders/orders.py +243 -0
- wbportfolio/models/portfolio.py +17 -20
- 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 +6 -4
- 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} +23 -15
- 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} +218 -246
- 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} +22 -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 -45
- 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.13.dist-info → wbportfolio-1.54.15.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.13.dist-info → wbportfolio-1.54.15.dist-info}/RECORD +85 -58
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- {wbportfolio-1.54.13.dist-info → wbportfolio-1.54.15.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.13.dist-info → wbportfolio-1.54.15.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,103 @@ 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
|
-
cash = cash_factory.create(currency=
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
portfolio=
|
|
426
|
+
cash = cash_factory.create(currency=order_proposal.portfolio.currency)
|
|
427
|
+
order_factory.create( # equity trade
|
|
428
|
+
order_proposal=order_proposal,
|
|
429
|
+
value_date=order_proposal.trade_date,
|
|
430
|
+
portfolio=order_proposal.portfolio,
|
|
437
431
|
weighting=Decimal("0.7"),
|
|
438
432
|
)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
portfolio=
|
|
433
|
+
order_factory.create( # cash trade
|
|
434
|
+
order_proposal=order_proposal,
|
|
435
|
+
value_date=order_proposal.trade_date,
|
|
436
|
+
portfolio=order_proposal.portfolio,
|
|
443
437
|
underlying_instrument=cash,
|
|
444
438
|
weighting=Decimal("0.2"),
|
|
445
439
|
)
|
|
446
440
|
|
|
447
|
-
target_cash_position =
|
|
441
|
+
target_cash_position = order_proposal.get_estimated_target_cash(order_proposal.portfolio.currency)
|
|
448
442
|
assert target_cash_position.weighting == Decimal("0.2") + Decimal("1.0") - (Decimal("0.7") + Decimal("0.2"))
|
|
449
443
|
assert target_cash_position.initial_shares == Decimal(1_000_000) * Decimal("0.3")
|
|
450
444
|
|
|
451
|
-
def
|
|
452
|
-
# Check that if we create a prior
|
|
445
|
+
def test_order_proposal_update_inception_date(self, order_proposal_factory, portfolio, instrument_factory):
|
|
446
|
+
# Check that if we create a prior order proposal, the instrument inception date is updated accordingly
|
|
453
447
|
instrument = instrument_factory.create(inception_date=None)
|
|
454
448
|
instrument.portfolios.add(portfolio)
|
|
455
|
-
tp =
|
|
449
|
+
tp = order_proposal_factory.create(portfolio=portfolio)
|
|
456
450
|
instrument.refresh_from_db()
|
|
457
451
|
assert instrument.inception_date == (tp.trade_date + BDay(1)).date()
|
|
458
452
|
|
|
459
|
-
tp2 =
|
|
453
|
+
tp2 = order_proposal_factory.create(portfolio=portfolio, trade_date=tp.trade_date - BDay(1))
|
|
460
454
|
instrument.refresh_from_db()
|
|
461
455
|
assert instrument.inception_date == (tp2.trade_date + BDay(1)).date()
|
|
462
456
|
|
|
463
|
-
def test_get_round_lot_size(self,
|
|
457
|
+
def test_get_round_lot_size(self, order_proposal, instrument):
|
|
464
458
|
# without a round lot size, we expect no normalization of shares
|
|
465
|
-
assert
|
|
459
|
+
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
466
460
|
instrument.round_lot_size = 100
|
|
467
461
|
instrument.save()
|
|
468
462
|
|
|
469
463
|
# 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
|
|
464
|
+
assert order_proposal.get_round_lot_size(Decimal(66.0), instrument) == Decimal("100")
|
|
465
|
+
assert order_proposal.get_round_lot_size(Decimal(-66.0), instrument) == Decimal(-66.0)
|
|
466
|
+
assert order_proposal.get_round_lot_size(Decimal(-120), instrument) == Decimal(-200)
|
|
473
467
|
|
|
474
468
|
# exchange can disable rounding based on the lot size
|
|
475
469
|
instrument.exchange.apply_round_lot_size = False
|
|
476
470
|
instrument.exchange.save()
|
|
477
|
-
assert
|
|
471
|
+
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
478
472
|
|
|
479
|
-
def test_submit_round_lot_size(self,
|
|
480
|
-
|
|
481
|
-
|
|
473
|
+
def test_submit_round_lot_size(self, order_proposal, order_factory, instrument):
|
|
474
|
+
order_proposal.portfolio.only_weighting = False
|
|
475
|
+
order_proposal.portfolio.save()
|
|
482
476
|
instrument.round_lot_size = 100
|
|
483
477
|
instrument.save()
|
|
484
|
-
trade =
|
|
485
|
-
status="DRAFT",
|
|
478
|
+
trade = order_factory.create(
|
|
486
479
|
underlying_instrument=instrument,
|
|
487
480
|
shares=70,
|
|
488
|
-
|
|
481
|
+
order_proposal=order_proposal,
|
|
489
482
|
weighting=Decimal("1.0"),
|
|
490
483
|
)
|
|
491
|
-
warnings =
|
|
492
|
-
|
|
484
|
+
warnings = order_proposal.submit()
|
|
485
|
+
order_proposal.save()
|
|
493
486
|
assert (
|
|
494
487
|
len(warnings) == 1
|
|
495
488
|
) # ensure that submit returns a warning concerning the rounded trade based on the lot size
|
|
496
489
|
trade.refresh_from_db()
|
|
497
490
|
assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
|
|
498
491
|
|
|
499
|
-
def test_submit_round_fractional_shares(self,
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
trade =
|
|
503
|
-
status="DRAFT",
|
|
492
|
+
def test_submit_round_fractional_shares(self, order_proposal, order_factory, instrument):
|
|
493
|
+
order_proposal.portfolio.only_weighting = False
|
|
494
|
+
order_proposal.portfolio.save()
|
|
495
|
+
trade = order_factory.create(
|
|
504
496
|
underlying_instrument=instrument,
|
|
505
497
|
shares=5.6,
|
|
506
|
-
|
|
498
|
+
order_proposal=order_proposal,
|
|
507
499
|
weighting=Decimal("1.0"),
|
|
508
500
|
)
|
|
509
|
-
|
|
510
|
-
|
|
501
|
+
order_proposal.submit()
|
|
502
|
+
order_proposal.save()
|
|
511
503
|
trade.refresh_from_db()
|
|
512
504
|
assert trade.shares == 6 # we expect the fractional share to be rounded
|
|
513
505
|
|
|
514
506
|
def test_ex_post(
|
|
515
|
-
self, instrument_factory, asset_position_factory, instrument_price_factory,
|
|
507
|
+
self, instrument_factory, asset_position_factory, instrument_price_factory, order_proposal_factory, portfolio
|
|
516
508
|
):
|
|
517
509
|
"""
|
|
518
510
|
Tests the ex-post rebalancing mechanism of a portfolio with two instruments.
|
|
519
|
-
Verifies that weights are correctly recalculated after submitting and approving a
|
|
511
|
+
Verifies that weights are correctly recalculated after submitting and approving a order proposal.
|
|
520
512
|
"""
|
|
521
513
|
|
|
522
514
|
# --- Create instruments ---
|
|
@@ -575,102 +567,82 @@ class TestTradeProposal:
|
|
|
575
567
|
)
|
|
576
568
|
|
|
577
569
|
# --- 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
570
|
|
|
593
|
-
# Check that weights on
|
|
594
|
-
|
|
595
|
-
assert
|
|
571
|
+
# Check that weights on d2 sum to 1
|
|
572
|
+
total_weight_d2 = msft_a2.weighting + apple_a2.weighting
|
|
573
|
+
assert total_weight_d2 == pytest.approx(Decimal("1.0"), abs=Decimal("1e-6"))
|
|
596
574
|
|
|
597
|
-
# --- Create a
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
# Retrieve
|
|
601
|
-
|
|
602
|
-
|
|
575
|
+
# --- Create a order proposal on d3 ---
|
|
576
|
+
order_proposal = order_proposal_factory.create(portfolio=portfolio, trade_date=d3)
|
|
577
|
+
order_proposal.reset_orders()
|
|
578
|
+
# Retrieve orders for each instrument
|
|
579
|
+
orders = order_proposal.get_orders()
|
|
580
|
+
trade_msft = orders.get(underlying_instrument=msft)
|
|
581
|
+
trade_apple = orders.get(underlying_instrument=apple)
|
|
603
582
|
# Check that trade weights are initially zero
|
|
604
583
|
assert trade_msft.weighting == Decimal("0")
|
|
605
584
|
assert trade_apple.weighting == Decimal("0")
|
|
606
585
|
|
|
586
|
+
msft_drifted = msft_a2.weighting * (Decimal("1.0") + msft_r3) / portfolio_r3
|
|
587
|
+
apple_drifted = apple_a2.weighting * (Decimal("1.0") + apple_r3) / portfolio_r3
|
|
607
588
|
# --- Adjust trade weights to target 50% each ---
|
|
608
589
|
target_weight = Decimal("0.5")
|
|
609
|
-
trade_msft.weighting = target_weight -
|
|
590
|
+
trade_msft.weighting = target_weight - msft_drifted
|
|
610
591
|
trade_msft.save()
|
|
611
592
|
|
|
612
|
-
trade_apple.weighting = target_weight -
|
|
593
|
+
trade_apple.weighting = target_weight - apple_drifted
|
|
613
594
|
trade_apple.save()
|
|
595
|
+
orders = order_proposal.get_orders()
|
|
596
|
+
trade_msft = orders.get(underlying_instrument=msft)
|
|
597
|
+
trade_apple = orders.get(underlying_instrument=apple)
|
|
614
598
|
|
|
615
599
|
# --- Check drift factors and effective weights ---
|
|
616
|
-
assert trade_msft.
|
|
617
|
-
assert trade_apple.
|
|
600
|
+
assert trade_msft.daily_return == pytest.approx(msft_r3, abs=Decimal("1e-6"))
|
|
601
|
+
assert trade_apple.daily_return == pytest.approx(apple_r3, abs=Decimal("1e-6"))
|
|
618
602
|
|
|
619
|
-
assert trade_msft._effective_weight == pytest.approx(
|
|
620
|
-
assert trade_apple._effective_weight == pytest.approx(
|
|
603
|
+
assert trade_msft._effective_weight == pytest.approx(msft_drifted, abs=Decimal("1e-6"))
|
|
604
|
+
assert trade_apple._effective_weight == pytest.approx(apple_drifted, abs=Decimal("1e-6"))
|
|
621
605
|
|
|
622
606
|
# 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
607
|
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
608
|
assert trade_apple._target_weight == pytest.approx(target_weight, abs=Decimal("1e-6"))
|
|
634
609
|
|
|
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()
|
|
610
|
+
# --- Submit and approve the order proposal ---
|
|
611
|
+
order_proposal.submit()
|
|
612
|
+
order_proposal.save()
|
|
613
|
+
order_proposal.approve()
|
|
614
|
+
order_proposal.save()
|
|
644
615
|
|
|
645
616
|
# Final check that weights have been updated to 50%
|
|
646
|
-
assert
|
|
647
|
-
|
|
617
|
+
assert order_proposal.portfolio.assets.get(underlying_instrument=msft).weighting == pytest.approx(
|
|
618
|
+
target_weight, abs=Decimal("1e-6")
|
|
619
|
+
)
|
|
620
|
+
assert order_proposal.portfolio.assets.get(underlying_instrument=apple).weighting == pytest.approx(
|
|
621
|
+
target_weight, abs=Decimal("1e-6")
|
|
622
|
+
)
|
|
648
623
|
|
|
649
|
-
def
|
|
650
|
-
self, instrument, instrument_price_factory,
|
|
624
|
+
def test_replay_reset_draft_order_proposal(
|
|
625
|
+
self, instrument, instrument_price_factory, order_factory, order_proposal_factory
|
|
651
626
|
):
|
|
652
|
-
|
|
653
|
-
status=TradeProposal.Status.DRAFT, trade_date=date.today() - BDay(2)
|
|
654
|
-
)
|
|
627
|
+
order_proposal = order_proposal_factory.create(trade_date=date.today() - BDay(2))
|
|
655
628
|
instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(2))
|
|
656
629
|
instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(1))
|
|
657
630
|
instrument_price_factory.create(instrument=instrument, date=date.today())
|
|
658
|
-
trade =
|
|
631
|
+
trade = order_factory.create(
|
|
659
632
|
underlying_instrument=instrument,
|
|
660
|
-
|
|
633
|
+
order_proposal=order_proposal,
|
|
661
634
|
weighting=1,
|
|
662
|
-
status=TradeProposal.Status.DRAFT,
|
|
663
635
|
)
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
636
|
+
order_proposal.submit()
|
|
637
|
+
order_proposal.approve(replay=False)
|
|
638
|
+
order_proposal.save()
|
|
667
639
|
|
|
668
|
-
draft_tp =
|
|
669
|
-
assert not
|
|
640
|
+
draft_tp = order_proposal_factory.create(portfolio=order_proposal.portfolio, trade_date=date.today() - BDay(1))
|
|
641
|
+
assert not Order.objects.filter(order_proposal=draft_tp).exists()
|
|
670
642
|
|
|
671
|
-
|
|
643
|
+
order_proposal.replay()
|
|
672
644
|
|
|
673
|
-
assert
|
|
674
|
-
assert
|
|
675
|
-
|
|
645
|
+
assert Order.objects.filter(order_proposal=draft_tp).count() == 1
|
|
646
|
+
assert Order.objects.get(
|
|
647
|
+
order_proposal=draft_tp, underlying_instrument=trade.underlying_instrument
|
|
676
648
|
).weighting == Decimal("0")
|