wbportfolio 1.48.0__py2.py3-none-any.whl → 1.49.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wbportfolio might be problematic. Click here for more details.

Files changed (33) hide show
  1. wbportfolio/factories/__init__.py +1 -3
  2. wbportfolio/factories/portfolios.py +0 -12
  3. wbportfolio/factories/product_groups.py +8 -1
  4. wbportfolio/factories/products.py +18 -0
  5. wbportfolio/factories/trades.py +5 -1
  6. wbportfolio/import_export/handlers/trade.py +8 -0
  7. wbportfolio/migrations/0075_portfolio_initial_position_date_and_more.py +28 -0
  8. wbportfolio/models/portfolio.py +8 -13
  9. wbportfolio/models/transactions/rebalancing.py +7 -1
  10. wbportfolio/models/transactions/trade_proposals.py +146 -49
  11. wbportfolio/models/transactions/trades.py +16 -11
  12. wbportfolio/pms/trading/handler.py +1 -1
  13. wbportfolio/pms/typing.py +3 -0
  14. wbportfolio/rebalancing/models/composite.py +14 -1
  15. wbportfolio/risk_management/backends/exposure_portfolio.py +30 -1
  16. wbportfolio/risk_management/tests/test_exposure_portfolio.py +47 -0
  17. wbportfolio/serializers/portfolios.py +26 -0
  18. wbportfolio/serializers/transactions/trades.py +13 -0
  19. wbportfolio/tests/models/test_portfolios.py +1 -1
  20. wbportfolio/tests/models/transactions/test_trade_proposals.py +381 -0
  21. wbportfolio/tests/models/transactions/test_trades.py +14 -0
  22. wbportfolio/tests/signals.py +1 -1
  23. wbportfolio/tests/viewsets/test_performances.py +2 -1
  24. wbportfolio/viewsets/configs/display/portfolios.py +58 -14
  25. wbportfolio/viewsets/configs/display/trades.py +23 -8
  26. wbportfolio/viewsets/configs/endpoints/trades.py +9 -0
  27. wbportfolio/viewsets/portfolios.py +22 -7
  28. wbportfolio/viewsets/transactions/trade_proposals.py +1 -1
  29. wbportfolio/viewsets/transactions/trades.py +86 -12
  30. {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/METADATA +1 -1
  31. {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/RECORD +33 -31
  32. {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/WHEEL +0 -0
  33. {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,381 @@
1
+ # Import necessary modules
2
+ from datetime import date, timedelta
3
+ from decimal import Decimal
4
+ from unittest.mock import call, patch
5
+
6
+ import pytest
7
+ from faker import Faker
8
+ from pandas._libs.tslibs.offsets import BDay, MonthEnd
9
+
10
+ from wbportfolio.models import Portfolio, RebalancingModel, TradeProposal
11
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
12
+ from wbportfolio.pms.typing import Position
13
+
14
+ fake = Faker()
15
+
16
+
17
+ # Mark tests to use Django's database
18
+ @pytest.mark.django_db
19
+ class TestTradeProposal:
20
+ # Test that the checked object is correctly set to the portfolio
21
+ def test_checked_object(self, trade_proposal):
22
+ """
23
+ Verify that the checked object is the portfolio associated with the trade proposal.
24
+ """
25
+ assert trade_proposal.checked_object == trade_proposal.portfolio
26
+
27
+ # Test that the evaluation date matches the trade date
28
+ def test_check_evaluation_date(self, trade_proposal):
29
+ """
30
+ Ensure the evaluation date is the same as the trade date.
31
+ """
32
+ assert trade_proposal.check_evaluation_date == trade_proposal.trade_date
33
+
34
+ # Test the validated trading service functionality
35
+ def test_validated_trading_service(self, trade_proposal, asset_position_factory, trade_factory):
36
+ """
37
+ Validate that the effective and target portfolios are correctly calculated.
38
+ """
39
+ effective_date = (trade_proposal.trade_date - BDay(1)).date()
40
+
41
+ # Create asset positions for testing
42
+ a1 = asset_position_factory.create(
43
+ portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.3")
44
+ )
45
+ a2 = asset_position_factory.create(
46
+ portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.7")
47
+ )
48
+
49
+ # Create trades for testing
50
+ t1 = trade_factory.create(
51
+ trade_proposal=trade_proposal,
52
+ weighting=Decimal("0.05"),
53
+ portfolio=trade_proposal.portfolio,
54
+ transaction_date=trade_proposal.trade_date,
55
+ underlying_instrument=a1.underlying_quote,
56
+ )
57
+ t2 = trade_factory.create(
58
+ trade_proposal=trade_proposal,
59
+ weighting=Decimal("-0.05"),
60
+ portfolio=trade_proposal.portfolio,
61
+ transaction_date=trade_proposal.trade_date,
62
+ underlying_instrument=a2.underlying_quote,
63
+ )
64
+
65
+ # Get the validated trading service
66
+ validated_trading_service = trade_proposal.validated_trading_service
67
+
68
+ # Assert effective and target portfolios are as expected
69
+ assert validated_trading_service.effective_portfolio.to_dict() == {
70
+ a1.underlying_quote.id: a1.weighting,
71
+ a2.underlying_quote.id: a2.weighting,
72
+ }
73
+ assert validated_trading_service.target_portfolio.to_dict() == {
74
+ a1.underlying_quote.id: a1.weighting + t1.weighting,
75
+ a2.underlying_quote.id: a2.weighting + t2.weighting,
76
+ }
77
+
78
+ # Test the calculation of the last effective date
79
+ def test_last_effective_date(self, trade_proposal, asset_position_factory):
80
+ """
81
+ Verify the last effective date is correctly determined based on asset positions.
82
+ """
83
+ # Without any positions, it should be the day before the trade date
84
+ assert (
85
+ trade_proposal.last_effective_date == (trade_proposal.trade_date - BDay(1)).date()
86
+ ), "Last effective date without position should be t-1"
87
+
88
+ # Create an asset position before the trade date
89
+ a1 = asset_position_factory.create(
90
+ portfolio=trade_proposal.portfolio, date=(trade_proposal.trade_date - BDay(5)).date()
91
+ )
92
+ a_noise = asset_position_factory.create(portfolio=trade_proposal.portfolio, date=trade_proposal.trade_date) # noqa
93
+
94
+ # The last effective date should still be the day before the trade date due to caching
95
+ assert (
96
+ trade_proposal.last_effective_date == (trade_proposal.trade_date - BDay(1)).date()
97
+ ), "last effective date is cached, so it won't change as is"
98
+
99
+ # Reset the cache property to recalculate
100
+ del trade_proposal.last_effective_date
101
+
102
+ # Now it should be the date of the latest position before the trade date
103
+ assert (
104
+ trade_proposal.last_effective_date == a1.date
105
+ ), "last effective date is the latest position strictly lower than trade date"
106
+
107
+ # Test finding the previous trade proposal
108
+ def test_previous_trade_proposal(self, trade_proposal_factory):
109
+ """
110
+ Ensure the previous trade proposal is correctly identified as the last approved proposal before the current one.
111
+ """
112
+ tp = trade_proposal_factory.create()
113
+ tp_previous_submit = trade_proposal_factory.create( # noqa
114
+ portfolio=tp.portfolio, status=TradeProposal.Status.SUBMIT, trade_date=(tp.trade_date - BDay(1)).date()
115
+ )
116
+ tp_previous_approve = trade_proposal_factory.create(
117
+ portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(2)).date()
118
+ )
119
+ tp_next_approve = trade_proposal_factory.create( # noqa
120
+ portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(1)).date()
121
+ )
122
+
123
+ # The previous valid trade proposal should be the approved one strictly before the current proposal
124
+ assert (
125
+ tp.previous_trade_proposal == tp_previous_approve
126
+ ), "the previous valid trade proposal is the strictly before and approved trade proposal"
127
+
128
+ # Test finding the next trade proposal
129
+ def test_next_trade_proposal(self, trade_proposal_factory):
130
+ """
131
+ Verify the next trade proposal is correctly identified as the first approved proposal after the current one.
132
+ """
133
+ tp = trade_proposal_factory.create()
134
+ tp_next_submit = trade_proposal_factory.create( # noqa
135
+ portfolio=tp.portfolio, status=TradeProposal.Status.SUBMIT, trade_date=(tp.trade_date + BDay(1)).date()
136
+ )
137
+ tp_next_approve = trade_proposal_factory.create(
138
+ portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(2)).date()
139
+ )
140
+ tp_previous_approve = trade_proposal_factory.create( # noqa
141
+ portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
142
+ )
143
+
144
+ # The next valid trade proposal should be the approved one strictly after the current proposal
145
+ assert (
146
+ tp.next_trade_proposal == tp_next_approve
147
+ ), "the next valid trade proposal is the strictly after and approved trade proposal"
148
+
149
+ # Test getting the default target portfolio
150
+ def test__get_default_target_portfolio(self, trade_proposal, asset_position_factory):
151
+ """
152
+ Ensure the default target portfolio is set to the effective portfolio from the day before the trade date.
153
+ """
154
+ effective_date = (trade_proposal.trade_date - BDay(1)).date()
155
+
156
+ # Create asset positions for testing
157
+ a1 = asset_position_factory.create(
158
+ portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.3")
159
+ )
160
+ a2 = asset_position_factory.create(
161
+ portfolio=trade_proposal.portfolio, date=effective_date, weighting=Decimal("0.7")
162
+ )
163
+ asset_position_factory.create(portfolio=trade_proposal.portfolio, date=trade_proposal.trade_date) # noise
164
+
165
+ # The default target portfolio should match the effective portfolio
166
+ assert trade_proposal._get_default_target_portfolio().to_dict() == {
167
+ a1.underlying_quote.id: a1.weighting,
168
+ a2.underlying_quote.id: a2.weighting,
169
+ }
170
+
171
+ # Test getting the default target portfolio with a rebalancing model
172
+ @patch.object(RebalancingModel, "get_target_portfolio")
173
+ def test__get_default_target_portfolio_with_rebalancer_model(self, mock_fct, trade_proposal, rebalancer_factory):
174
+ """
175
+ Verify that the target portfolio is correctly obtained from a rebalancing model.
176
+ """
177
+ # Expected target portfolio from the rebalancing model
178
+ expected_target_portfolio = PortfolioDTO(
179
+ positions=(Position(underlying_instrument=1, weighting=Decimal(1), date=trade_proposal.trade_date),)
180
+ )
181
+ mock_fct.return_value = expected_target_portfolio
182
+
183
+ # Create a rebalancer for testing
184
+ rebalancer = rebalancer_factory.create(
185
+ portfolio=trade_proposal.portfolio, parameters={"rebalancer_parameter": "A"}
186
+ )
187
+ trade_proposal.rebalancing_model = rebalancer.rebalancing_model
188
+ trade_proposal.save()
189
+
190
+ # Additional keyword arguments for the rebalancing model
191
+ extra_kwargs = {"test": "test"}
192
+
193
+ # Combine rebalancer parameters with extra keyword arguments
194
+ expected_kwargs = rebalancer.parameters
195
+ expected_kwargs.update(extra_kwargs)
196
+
197
+ # Assert the target portfolio matches the expected output from the rebalancing model
198
+ assert (
199
+ trade_proposal._get_default_target_portfolio(**extra_kwargs) == expected_target_portfolio
200
+ ), "We expect the target portfolio to be whatever is returned by the rebalancer model"
201
+ mock_fct.assert_called_once_with(
202
+ trade_proposal.portfolio, trade_proposal.trade_date, trade_proposal.last_effective_date, **expected_kwargs
203
+ )
204
+
205
+ # Test normalizing trades
206
+ def test_normalize_trades(self, trade_proposal, trade_factory):
207
+ """
208
+ Ensure trades are normalized to sum up to 1, handling quantization errors.
209
+ """
210
+ # Create trades for testing
211
+ t1 = trade_factory.create(
212
+ trade_proposal=trade_proposal,
213
+ transaction_date=trade_proposal.trade_date,
214
+ portfolio=trade_proposal.portfolio,
215
+ weighting=Decimal(0.2),
216
+ )
217
+ t2 = trade_factory.create(
218
+ trade_proposal=trade_proposal,
219
+ transaction_date=trade_proposal.trade_date,
220
+ portfolio=trade_proposal.portfolio,
221
+ weighting=Decimal(0.26),
222
+ )
223
+ t3 = trade_factory.create(
224
+ trade_proposal=trade_proposal,
225
+ transaction_date=trade_proposal.trade_date,
226
+ portfolio=trade_proposal.portfolio,
227
+ weighting=Decimal(0.14),
228
+ )
229
+
230
+ # Normalize trades
231
+ trade_proposal.normalize_trades()
232
+
233
+ # Refresh trades from the database
234
+ t1.refresh_from_db()
235
+ t2.refresh_from_db()
236
+ t3.refresh_from_db()
237
+
238
+ # Expected normalized weights
239
+ normalized_t1_weight = Decimal("0.333333")
240
+ normalized_t2_weight = Decimal("0.433333")
241
+ normalized_t3_weight = Decimal("0.233333")
242
+
243
+ # Calculate quantization error
244
+ quantize_error = Decimal(1) - (normalized_t1_weight + normalized_t2_weight + normalized_t3_weight)
245
+
246
+ # Assert quantization error exists and weights are normalized correctly
247
+ assert quantize_error
248
+ assert t1.weighting == normalized_t1_weight
249
+ assert t2.weighting == normalized_t2_weight + quantize_error # Add quantize error to the largest position
250
+ assert t3.weighting == normalized_t3_weight
251
+
252
+ # Test resetting trades
253
+ def test_reset_trades(self, trade_proposal, instrument_factory, asset_position_factory):
254
+ """
255
+ Verify trades are correctly reset based on effective and target portfolios.
256
+ """
257
+ effective_date = trade_proposal.last_effective_date
258
+
259
+ # Create instruments for testing
260
+ i1 = instrument_factory.create(currency=trade_proposal.portfolio.currency)
261
+ i2 = instrument_factory.create(currency=trade_proposal.portfolio.currency)
262
+ i3 = instrument_factory.create(currency=trade_proposal.portfolio.currency)
263
+
264
+ # Build initial effective portfolio constituting only from two positions of i1 and i2
265
+ asset_position_factory.create(
266
+ portfolio=trade_proposal.portfolio, date=effective_date, underlying_instrument=i1, weighting=Decimal("0.7")
267
+ )
268
+ asset_position_factory.create(
269
+ portfolio=trade_proposal.portfolio, date=effective_date, underlying_instrument=i2, weighting=Decimal("0.3")
270
+ )
271
+
272
+ # build the target portfolio
273
+ target_portfolio = PortfolioDTO(
274
+ positions=(
275
+ Position(underlying_instrument=i2.id, date=trade_proposal.trade_date, weighting=Decimal("0.4")),
276
+ Position(underlying_instrument=i3.id, date=trade_proposal.trade_date, weighting=Decimal("0.6")),
277
+ )
278
+ )
279
+
280
+ # Reset trades
281
+ trade_proposal.reset_trades(target_portfolio=target_portfolio)
282
+
283
+ # Get trades for each instrument
284
+ t1 = trade_proposal.trades.get(underlying_instrument=i1)
285
+ t2 = trade_proposal.trades.get(underlying_instrument=i2)
286
+ t3 = trade_proposal.trades.get(underlying_instrument=i3)
287
+
288
+ # Assert trade weights are correctly reset
289
+ assert t1.weighting == Decimal("-0.7")
290
+ assert t2.weighting == Decimal("0.1")
291
+ assert t3.weighting == Decimal("0.6")
292
+
293
+ # Test replaying trade proposals
294
+ @patch.object(Portfolio, "batch_portfolio")
295
+ def test_replay(self, mock_fct, trade_proposal_factory):
296
+ """
297
+ Ensure replaying trade proposals correctly calls batch_portfolio for each period.
298
+ """
299
+ mock_fct.return_value = None
300
+
301
+ # Create approved trade proposals for testing
302
+ tp0 = trade_proposal_factory.create(status=TradeProposal.Status.APPROVED)
303
+ tp1 = trade_proposal_factory.create(
304
+ portfolio=tp0.portfolio,
305
+ status=TradeProposal.Status.APPROVED,
306
+ trade_date=(tp0.trade_date + MonthEnd(1)).date(),
307
+ )
308
+ tp2 = trade_proposal_factory.create(
309
+ portfolio=tp0.portfolio,
310
+ status=TradeProposal.Status.APPROVED,
311
+ trade_date=(tp1.trade_date + MonthEnd(1)).date(),
312
+ )
313
+
314
+ # Replay trade proposals
315
+ tp0.replay()
316
+
317
+ # Expected calls to batch_portfolio
318
+ expected_calls = [
319
+ call(tp0.trade_date, tp1.trade_date - timedelta(days=1)),
320
+ call(tp1.trade_date, tp2.trade_date - timedelta(days=1)),
321
+ call(tp2.trade_date, date.today()),
322
+ ]
323
+
324
+ # Assert batch_portfolio was called as expected
325
+ mock_fct.assert_has_calls(expected_calls)
326
+
327
+ # Test stopping replay on a non-approved proposal
328
+ tp1.status = TradeProposal.Status.FAILED
329
+ tp1.save()
330
+ expected_calls = [call(tp0.trade_date, tp1.trade_date - timedelta(days=1))]
331
+ mock_fct.assert_has_calls(expected_calls)
332
+
333
+ # Test estimating shares for a trade
334
+ @patch.object(Portfolio, "get_total_asset_value")
335
+ def test_get_estimated_shares(
336
+ self, mock_fct, trade_proposal, trade_factory, instrument_price_factory, instrument_factory
337
+ ):
338
+ """
339
+ Verify shares estimation based on trade weighting and instrument price.
340
+ """
341
+ portfolio = trade_proposal.portfolio
342
+ instrument = instrument_factory.create(currency=portfolio.currency)
343
+ trade = trade_factory.create(
344
+ trade_proposal=trade_proposal,
345
+ transaction_date=trade_proposal.trade_date,
346
+ portfolio=portfolio,
347
+ underlying_instrument=instrument,
348
+ )
349
+ trade.refresh_from_db()
350
+ underlying_quote_price = instrument_price_factory.create(instrument=instrument, date=trade.transaction_date)
351
+ mock_fct.return_value = Decimal(1_000_000) # 1 million cash
352
+
353
+ # Assert estimated shares are correctly calculated
354
+ assert (
355
+ trade_proposal.get_estimated_shares(trade.weighting, trade.underlying_instrument)
356
+ == Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
357
+ )
358
+
359
+ @patch.object(Portfolio, "get_total_asset_value")
360
+ def test_get_estimated_target_cash(self, mock_fct, trade_proposal, trade_factory, cash_factory):
361
+ mock_fct.return_value = Decimal(1_000_000) # 1 million cash
362
+ cash = cash_factory.create(currency=trade_proposal.portfolio.currency)
363
+ trade_factory.create( # equity trade
364
+ trade_proposal=trade_proposal,
365
+ transaction_date=trade_proposal.trade_date,
366
+ portfolio=trade_proposal.portfolio,
367
+ weighting=Decimal("0.7"),
368
+ )
369
+ trade_factory.create( # cash trade
370
+ trade_proposal=trade_proposal,
371
+ transaction_date=trade_proposal.trade_date,
372
+ portfolio=trade_proposal.portfolio,
373
+ underlying_instrument=cash,
374
+ weighting=Decimal("0.2"),
375
+ )
376
+
377
+ target_cash_weight, total_target_shares = trade_proposal.get_estimated_target_cash(
378
+ trade_proposal.portfolio.currency
379
+ )
380
+ assert target_cash_weight == Decimal("0.2") + Decimal("1.0") - (Decimal("0.7") + Decimal("0.2"))
381
+ assert total_target_shares == Decimal(1_000_000) * Decimal("0.3")
@@ -3,6 +3,7 @@ from decimal import Decimal
3
3
 
4
4
  import pytest
5
5
  from faker import Faker
6
+ from pandas._libs.tslibs.offsets import BDay
6
7
 
7
8
  from wbportfolio.models import Product, Trade
8
9
 
@@ -203,3 +204,16 @@ class TestTradeInstrumentPrice:
203
204
  subscription_trade1.refresh_from_db()
204
205
  assert subscription_trade1.internal_trade == internal_trade
205
206
  assert subscription_trade1.marked_as_internal is True
207
+
208
+ def test_last_underlying_quote_price(self, weekday, trade_factory, instrument_price_factory):
209
+ trade = trade_factory.create(transaction_date=weekday, value_date=(weekday - BDay(1)).date())
210
+ assert trade.last_underlying_quote_price is None
211
+ del trade.last_underlying_quote_price
212
+
213
+ # test that underlying quote price returns any price found at transaction_date, or then at value_date (in that order)
214
+ p0 = instrument_price_factory.create(instrument=trade.underlying_instrument, date=trade.value_date)
215
+ assert trade.last_underlying_quote_price == p0
216
+ del trade.last_underlying_quote_price
217
+
218
+ p1 = instrument_price_factory.create(instrument=trade.underlying_instrument, date=trade.transaction_date)
219
+ assert trade.last_underlying_quote_price == p1
@@ -2,10 +2,10 @@ from django.dispatch import receiver
2
2
  from wbcore.contrib.currency.factories import CurrencyFXRatesFactory
3
3
  from wbcore.contrib.directory.factories import CompanyFactory
4
4
  from wbcore.test.signals import custom_update_kwargs, get_custom_factory
5
+ from wbfdm.factories import InstrumentPriceFactory
5
6
 
6
7
  from wbportfolio.factories import (
7
8
  CustomerTradeFactory,
8
- InstrumentPriceFactory,
9
9
  ProductFactory,
10
10
  ProductPortfolioRoleFactory,
11
11
  TradeProposalFactory,
@@ -6,8 +6,9 @@ from faker import Faker
6
6
  from pandas.tseries.offsets import BDay, BMonthEnd, BYearEnd
7
7
  from rest_framework.reverse import reverse
8
8
  from rest_framework.test import force_authenticate
9
+ from wbfdm.factories import InstrumentPriceFactory
9
10
 
10
- from wbportfolio.factories import InstrumentPriceFactory, ProductFactory
11
+ from wbportfolio.factories import ProductFactory
11
12
  from wbportfolio.viewsets.product_performance import PerformanceComparisonPandasView
12
13
 
13
14
  fake = Faker()
@@ -1,6 +1,7 @@
1
1
  from typing import Optional
2
2
 
3
3
  from django.utils.translation import gettext_lazy as _
4
+ from wbcore.enums import Unit
4
5
  from wbcore.metadata.configs import display as dp
5
6
  from wbcore.metadata.configs.display import Layout, Page, default
6
7
  from wbcore.metadata.configs.display.instance_display.shortcuts import (
@@ -17,17 +18,60 @@ class PortfolioDisplayConfig(DisplayViewConfig):
17
18
  def get_list_display(self) -> Optional[dp.ListDisplay]:
18
19
  return dp.ListDisplay(
19
20
  fields=[
20
- dp.Field(key="name", label="Name"),
21
- dp.Field(key="currency", label="Currency"),
22
- dp.Field(key="updated_at", label="Updated At"),
23
- dp.Field(key="depends_on", label="Depends on"),
24
- dp.Field(key="automatic_rebalancer", label="Rebalancer"),
25
- dp.Field(key="invested_timespan", label="Invested"),
26
- dp.Field(key="is_manageable", label="Managed"),
27
- dp.Field(key="is_tracked", label="Tracked"),
28
- dp.Field(key="only_weighting", label="Only-Weight"),
29
- dp.Field(key="is_lookthrough", label="Lookthrough"),
30
- dp.Field(key="is_composition", label="Composition"),
21
+ dp.Field(
22
+ label="Information",
23
+ open_by_default=False,
24
+ key=None,
25
+ children=[
26
+ dp.Field(key="name", label="Name", width=Unit.PIXEL(300)),
27
+ dp.Field(key="currency", label="CCY", width=Unit.PIXEL(75)),
28
+ dp.Field(key="hedged_currency", label="Hedged CCY", width=Unit.PIXEL(100), show="open"),
29
+ dp.Field(key="updated_at", label="Updated At", width=Unit.PIXEL(150)),
30
+ dp.Field(key="depends_on", label="Depends on", show="open", width=Unit.PIXEL(300)),
31
+ dp.Field(key="invested_timespan", label="Invested", show="open"),
32
+ dp.Field(key="instruments", label="Instruments", width=Unit.PIXEL(250)),
33
+ ],
34
+ ),
35
+ dp.Field(
36
+ label="Valuation & Position",
37
+ open_by_default=False,
38
+ key=None,
39
+ children=[
40
+ dp.Field(key="initial_position_date", label="Issue Date", width=Unit.PIXEL(150)),
41
+ dp.Field(key="last_position_date", label="Last Position Date", width=Unit.PIXEL(150)),
42
+ dp.Field(
43
+ key="last_asset_under_management_usd", label="AUM ($)", width=Unit.PIXEL(100), show="open"
44
+ ),
45
+ dp.Field(key="last_positions", label="Position", width=Unit.PIXEL(100), show="open"),
46
+ ],
47
+ ),
48
+ dp.Field(
49
+ label="Rebalancing",
50
+ open_by_default=False,
51
+ key=None,
52
+ children=[
53
+ dp.Field(key="automatic_rebalancer", label="Automatic Rebalancer"),
54
+ dp.Field(key="last_trade_proposal_date", label="Last Rebalance", width=Unit.PIXEL(250)),
55
+ dp.Field(
56
+ key="next_expected_trade_proposal_date",
57
+ label="Next Rebalancing",
58
+ width=Unit.PIXEL(250),
59
+ show="open",
60
+ ),
61
+ ],
62
+ ),
63
+ dp.Field(
64
+ label="Administration",
65
+ open_by_default=False,
66
+ key=None,
67
+ children=[
68
+ dp.Field(key="is_manageable", label="Managed", width=Unit.PIXEL(100)),
69
+ dp.Field(key="is_tracked", label="Tracked", width=Unit.PIXEL(100), show="open"),
70
+ dp.Field(key="only_weighting", label="Only-Weight", width=Unit.PIXEL(100), show="open"),
71
+ dp.Field(key="is_lookthrough", label="Look through", width=Unit.PIXEL(100), show="open"),
72
+ dp.Field(key="is_composition", label="Composition", width=Unit.PIXEL(100), show="open"),
73
+ ],
74
+ ),
31
75
  ]
32
76
  )
33
77
 
@@ -156,14 +200,14 @@ class TopDownPortfolioCompositionPandasDisplayConfig(DisplayViewConfig):
156
200
  rebalancing_column_label = "Last Rebalancing"
157
201
  effective_column_label = "Actual"
158
202
  if self.view.last_rebalancing_date:
159
- rebalancing_column_label += f" ({self.view.last_rebalancing_date:%Y-ok, %m-%d})"
203
+ rebalancing_column_label += f" ({self.view.last_rebalancing_date:%Y-%m-%d})"
160
204
  if self.view.last_effective_date:
161
205
  effective_column_label += f" ({self.view.last_effective_date:%Y-%m-%d})"
162
206
  return dp.ListDisplay(
163
207
  fields=[
164
208
  dp.Field(key="instrument", label="Instrument"),
165
- dp.Field(key="effective_weights", label=rebalancing_column_label),
166
- dp.Field(key="rebalancing_weights", label=effective_column_label),
209
+ dp.Field(key="rebalancing_weights", label=rebalancing_column_label),
210
+ dp.Field(key="effective_weights", label=effective_column_label),
167
211
  ],
168
212
  tree=True,
169
213
  tree_group_pinned="left",
@@ -300,14 +300,26 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
300
300
  def get_list_display(self) -> Optional[dp.ListDisplay]:
301
301
  trade_proposal = get_object_or_404(TradeProposal, pk=self.view.kwargs.get("trade_proposal_id", None))
302
302
  fields = [
303
- dp.Field(key="underlying_instrument", label="Instrument"),
303
+ dp.Field(
304
+ label="Instrument",
305
+ open_by_default=True,
306
+ key=None,
307
+ children=[
308
+ dp.Field(key="underlying_instrument", label="Name", width=Unit.PIXEL(250)),
309
+ dp.Field(key="underlying_instrument_isin", label="ISIN", width=Unit.PIXEL(125)),
310
+ dp.Field(key="underlying_instrument_ticker", label="Ticker", width=Unit.PIXEL(100)),
311
+ dp.Field(
312
+ key="underlying_instrument_refinitiv_identifier_code", label="RIC", width=Unit.PIXEL(100)
313
+ ),
314
+ ],
315
+ ),
304
316
  dp.Field(
305
317
  label="Weight",
306
318
  open_by_default=False,
307
319
  key=None,
308
320
  children=[
309
- dp.Field(key="effective_weight", label="Effective Weight", show="open"),
310
- dp.Field(key="target_weight", label="Target Weight", show="open"),
321
+ dp.Field(key="effective_weight", label="Effective Weight", show="open", width=Unit.PIXEL(150)),
322
+ dp.Field(key="target_weight", label="Target Weight", show="open", width=Unit.PIXEL(150)),
311
323
  dp.Field(
312
324
  key="weighting",
313
325
  label="Delta Weight",
@@ -321,6 +333,7 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
321
333
  condition=(">", 0),
322
334
  ),
323
335
  ],
336
+ width=Unit.PIXEL(150),
324
337
  ),
325
338
  ],
326
339
  ),
@@ -332,8 +345,8 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
332
345
  open_by_default=False,
333
346
  key=None,
334
347
  children=[
335
- dp.Field(key="effective_shares", label="Effective Shares", show="open"),
336
- dp.Field(key="target_shares", label="Target Shares", show="open"),
348
+ dp.Field(key="effective_shares", label="Effective Shares", show="open", width=Unit.PIXEL(150)),
349
+ dp.Field(key="target_shares", label="Target Shares", show="open", width=Unit.PIXEL(150)),
337
350
  dp.Field(
338
351
  key="shares",
339
352
  label="Shares",
@@ -347,6 +360,7 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
347
360
  condition=(">", 0),
348
361
  ),
349
362
  ],
363
+ width=Unit.PIXEL(150),
350
364
  ),
351
365
  ],
352
366
  )
@@ -370,16 +384,17 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
370
384
  condition=("==", Trade.Type.BUY.name),
371
385
  ),
372
386
  ],
387
+ width=Unit.PIXEL(125),
373
388
  ),
374
- dp.Field(key="comment", label="Comment"),
375
- dp.Field(key="order", label="Order", show="open"),
389
+ dp.Field(key="comment", label="Comment", width=Unit.PIXEL(250)),
390
+ dp.Field(key="order", label="Order", show="open", width=Unit.PIXEL(100)),
376
391
  ],
377
392
  )
378
393
  )
379
394
  return dp.ListDisplay(
380
395
  fields=fields,
381
396
  legends=[TRADE_STATUS_LEGENDS],
382
- formatting=[SHARE_FORMATTING, TRADE_STATUS_FORMATTING],
397
+ formatting=[TRADE_STATUS_FORMATTING],
383
398
  )
384
399
 
385
400
  def get_instance_display(self) -> Display:
@@ -1,3 +1,5 @@
1
+ from contextlib import suppress
2
+
1
3
  from django.shortcuts import get_object_or_404
2
4
  from rest_framework.reverse import reverse
3
5
  from wbcore.metadata.configs.endpoints import EndpointViewConfig
@@ -77,6 +79,13 @@ class TradeTradeProposalEndpointConfig(EndpointViewConfig):
77
79
  return self.get_list_endpoint()
78
80
  return None
79
81
 
82
+ def get_delete_endpoint(self, **kwargs):
83
+ with suppress(AttributeError, AssertionError):
84
+ trade = self.view.get_object()
85
+ if trade._effective_weight: # we make sure trade with a valid effective position cannot be deleted
86
+ return None
87
+ return super().get_delete_endpoint(**kwargs)
88
+
80
89
 
81
90
  class SubscriptionRedemptionEndpointConfig(TradeEndpointConfig):
82
91
  def get_endpoint(self, **kwargs):