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.
- wbportfolio/factories/__init__.py +1 -3
- wbportfolio/factories/portfolios.py +0 -12
- wbportfolio/factories/product_groups.py +8 -1
- wbportfolio/factories/products.py +18 -0
- wbportfolio/factories/trades.py +5 -1
- wbportfolio/import_export/handlers/trade.py +8 -0
- wbportfolio/migrations/0075_portfolio_initial_position_date_and_more.py +28 -0
- wbportfolio/models/portfolio.py +8 -13
- wbportfolio/models/transactions/rebalancing.py +7 -1
- wbportfolio/models/transactions/trade_proposals.py +146 -49
- wbportfolio/models/transactions/trades.py +16 -11
- wbportfolio/pms/trading/handler.py +1 -1
- wbportfolio/pms/typing.py +3 -0
- wbportfolio/rebalancing/models/composite.py +14 -1
- wbportfolio/risk_management/backends/exposure_portfolio.py +30 -1
- wbportfolio/risk_management/tests/test_exposure_portfolio.py +47 -0
- wbportfolio/serializers/portfolios.py +26 -0
- wbportfolio/serializers/transactions/trades.py +13 -0
- wbportfolio/tests/models/test_portfolios.py +1 -1
- wbportfolio/tests/models/transactions/test_trade_proposals.py +381 -0
- wbportfolio/tests/models/transactions/test_trades.py +14 -0
- wbportfolio/tests/signals.py +1 -1
- wbportfolio/tests/viewsets/test_performances.py +2 -1
- wbportfolio/viewsets/configs/display/portfolios.py +58 -14
- wbportfolio/viewsets/configs/display/trades.py +23 -8
- wbportfolio/viewsets/configs/endpoints/trades.py +9 -0
- wbportfolio/viewsets/portfolios.py +22 -7
- wbportfolio/viewsets/transactions/trade_proposals.py +1 -1
- wbportfolio/viewsets/transactions/trades.py +86 -12
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/METADATA +1 -1
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/RECORD +33 -31
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.48.0.dist-info → wbportfolio-1.49.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -136,6 +136,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
136
136
|
REDEMPTION = "REDEMPTION", "Redemption"
|
|
137
137
|
BUY = "BUY", "Buy"
|
|
138
138
|
SELL = "SELL", "Sell"
|
|
139
|
+
NO_CHANGE = "NO_CHANGE", "No Change" # default transaction subtype if weighing is 0
|
|
139
140
|
|
|
140
141
|
external_identifier2 = models.CharField(
|
|
141
142
|
max_length=255,
|
|
@@ -224,7 +225,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
224
225
|
source=Status.DRAFT,
|
|
225
226
|
target=GET_STATE(
|
|
226
227
|
lambda self, **kwargs: (
|
|
227
|
-
self.Status.SUBMIT if self.
|
|
228
|
+
self.Status.SUBMIT if self.last_underlying_quote_price is not None else self.Status.FAILED
|
|
228
229
|
),
|
|
229
230
|
states=[Status.SUBMIT, Status.FAILED],
|
|
230
231
|
),
|
|
@@ -245,7 +246,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
245
246
|
on_error="FAILED",
|
|
246
247
|
)
|
|
247
248
|
def submit(self, by=None, description=None, **kwargs):
|
|
248
|
-
if not self.
|
|
249
|
+
if not self.last_underlying_quote_price:
|
|
249
250
|
self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
|
|
250
251
|
|
|
251
252
|
def can_submit(self):
|
|
@@ -263,13 +264,15 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
263
264
|
self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
|
|
264
265
|
|
|
265
266
|
@cached_property
|
|
266
|
-
def
|
|
267
|
+
def last_underlying_quote_price(self) -> InstrumentPrice | None:
|
|
267
268
|
try:
|
|
269
|
+
# we try t0 first
|
|
268
270
|
return InstrumentPrice.objects.filter_only_valid_prices().get(
|
|
269
|
-
instrument=self.underlying_instrument, date=self.
|
|
271
|
+
instrument=self.underlying_instrument, date=self.transaction_date
|
|
270
272
|
)
|
|
271
273
|
except InstrumentPrice.DoesNotExist:
|
|
272
274
|
with suppress(InstrumentPrice.DoesNotExist):
|
|
275
|
+
# we fall back to the latest price before t0
|
|
273
276
|
return (
|
|
274
277
|
InstrumentPrice.objects.filter_only_valid_prices()
|
|
275
278
|
.filter(instrument=self.underlying_instrument, date__lte=self.value_date)
|
|
@@ -296,7 +299,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
296
299
|
},
|
|
297
300
|
)
|
|
298
301
|
def execute(self, **kwargs):
|
|
299
|
-
if self.
|
|
302
|
+
if self.last_underlying_quote_price:
|
|
300
303
|
asset, created = AssetPosition.unannotated_objects.update_or_create(
|
|
301
304
|
underlying_quote=self.underlying_instrument,
|
|
302
305
|
portfolio_created=None,
|
|
@@ -305,9 +308,9 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
305
308
|
defaults={
|
|
306
309
|
"initial_currency_fx_rate": self.currency_fx_rate,
|
|
307
310
|
"weighting": self._target_weight,
|
|
308
|
-
"initial_price": self.
|
|
311
|
+
"initial_price": self.last_underlying_quote_price.net_value,
|
|
309
312
|
"initial_shares": None,
|
|
310
|
-
"underlying_quote_price": self.
|
|
313
|
+
"underlying_quote_price": self.last_underlying_quote_price,
|
|
311
314
|
"asset_valuation_date": self.transaction_date,
|
|
312
315
|
"currency": self.currency,
|
|
313
316
|
"is_estimated": False,
|
|
@@ -316,7 +319,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
316
319
|
asset.set_weighting(self._target_weight)
|
|
317
320
|
|
|
318
321
|
def can_execute(self):
|
|
319
|
-
if not self.
|
|
322
|
+
if not self.last_underlying_quote_price:
|
|
320
323
|
return {"underlying_instrument": "Cannot execute a trade without a valid quote price"}
|
|
321
324
|
if not self.portfolio.is_manageable:
|
|
322
325
|
return {"portfolio": "The portfolio needs to be a model portfolio in order to execute this trade manually"}
|
|
@@ -494,8 +497,8 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
494
497
|
self.portfolio = self.trade_proposal.portfolio
|
|
495
498
|
self.transaction_date = self.trade_proposal.trade_date
|
|
496
499
|
self.value_date = self.trade_proposal.last_effective_date
|
|
497
|
-
if self.
|
|
498
|
-
self.shares = self.
|
|
500
|
+
if not self.portfolio.only_weighting:
|
|
501
|
+
self.shares = self.trade_proposal.get_estimated_shares(self.weighting, self.underlying_instrument)
|
|
499
502
|
|
|
500
503
|
if not self.custodian and self.bank:
|
|
501
504
|
self.custodian = Custodian.get_by_mapping(self.bank)
|
|
@@ -513,7 +516,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
513
516
|
self.total_value_gross = self.price_gross * self.shares
|
|
514
517
|
self.transaction_type = Transaction.Type.TRADE
|
|
515
518
|
|
|
516
|
-
if self.transaction_subtype is None:
|
|
519
|
+
if self.transaction_subtype is None or self.trade_proposal:
|
|
517
520
|
# if subtype not provided, we extract it automatically from the existing data.
|
|
518
521
|
self._set_transaction_subtype()
|
|
519
522
|
if self.id and hasattr(self, "claims"):
|
|
@@ -525,6 +528,8 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
525
528
|
super().save(*args, **kwargs)
|
|
526
529
|
|
|
527
530
|
def _set_transaction_subtype(self):
|
|
531
|
+
if self.weighting == 0:
|
|
532
|
+
self.transaction_subtype = Trade.Type.NO_CHANGE
|
|
528
533
|
if self.underlying_instrument.instrument_type.key == "product":
|
|
529
534
|
if self.shares is not None:
|
|
530
535
|
if self.shares > 0:
|
|
@@ -62,7 +62,7 @@ class TradingService:
|
|
|
62
62
|
Test the given value against all the validators on the field,
|
|
63
63
|
and either raise a `ValidationError` or simply return.
|
|
64
64
|
"""
|
|
65
|
-
TradeBatch(validated_trades).validate()
|
|
65
|
+
# TradeBatch(validated_trades).validate()
|
|
66
66
|
if self.effective_portfolio:
|
|
67
67
|
for trade in validated_trades:
|
|
68
68
|
if (
|
wbportfolio/pms/typing.py
CHANGED
|
@@ -70,6 +70,9 @@ class Portfolio:
|
|
|
70
70
|
def to_df(self):
|
|
71
71
|
return pd.DataFrame([asdict(pos) for pos in self.positions])
|
|
72
72
|
|
|
73
|
+
def to_dict(self) -> dict[int, Decimal]:
|
|
74
|
+
return {underlying_instrument: pos.weighting for underlying_instrument, pos in self.positions_map.items()}
|
|
75
|
+
|
|
73
76
|
def __len__(self):
|
|
74
77
|
return len(self.positions)
|
|
75
78
|
|
|
@@ -2,6 +2,7 @@ from decimal import Decimal
|
|
|
2
2
|
|
|
3
3
|
from django.core.exceptions import ObjectDoesNotExist
|
|
4
4
|
|
|
5
|
+
from wbportfolio.models import Trade
|
|
5
6
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
6
7
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
7
8
|
from wbportfolio.rebalancing.decorators import register
|
|
@@ -11,11 +12,23 @@ from wbportfolio.rebalancing.decorators import register
|
|
|
11
12
|
class CompositeRebalancing(AbstractRebalancingModel):
|
|
12
13
|
@property
|
|
13
14
|
def base_assets(self) -> dict[int, Decimal]:
|
|
15
|
+
"""
|
|
16
|
+
Return a dictionary representation (instrument_id: target weight) of this trade proposal
|
|
17
|
+
Returns:
|
|
18
|
+
A dictionary representation
|
|
19
|
+
|
|
20
|
+
"""
|
|
14
21
|
try:
|
|
15
22
|
latest_trade_proposal = self.portfolio.trade_proposals.filter(
|
|
16
23
|
status="APPROVED", trade_date__lte=self.trade_date
|
|
17
24
|
).latest("trade_date")
|
|
18
|
-
return
|
|
25
|
+
return {
|
|
26
|
+
v["underlying_instrument"]: v["target_weight"]
|
|
27
|
+
for v in latest_trade_proposal.trades.all()
|
|
28
|
+
.annotate_base_info()
|
|
29
|
+
.filter(status=Trade.Status.EXECUTED)
|
|
30
|
+
.values("underlying_instrument", "target_weight")
|
|
31
|
+
}
|
|
19
32
|
except ObjectDoesNotExist:
|
|
20
33
|
return dict()
|
|
21
34
|
|
|
@@ -101,6 +101,24 @@ class RuleBackend(
|
|
|
101
101
|
)
|
|
102
102
|
_classifications = ClassificationRepresentationSerializer(many=True, source="parameters__classifications")
|
|
103
103
|
|
|
104
|
+
extra_filter_field = wb_serializers.ChoiceField(
|
|
105
|
+
choices=cls.Field.choices,
|
|
106
|
+
default=None,
|
|
107
|
+
allow_null=True,
|
|
108
|
+
label="Extra Filter Field",
|
|
109
|
+
help_text="Specify if we need to narrow done the position applying a filter on that field and the corresponding range",
|
|
110
|
+
)
|
|
111
|
+
extra_filter_field_lower_bound = wb_serializers.FloatField(
|
|
112
|
+
default=None,
|
|
113
|
+
allow_null=True,
|
|
114
|
+
label="Extra Filter Field Lower bound",
|
|
115
|
+
)
|
|
116
|
+
extra_filter_field_upper_bound = wb_serializers.FloatField(
|
|
117
|
+
default=None,
|
|
118
|
+
allow_null=True,
|
|
119
|
+
label="Extra Filter Field Lower bound",
|
|
120
|
+
)
|
|
121
|
+
|
|
104
122
|
@classmethod
|
|
105
123
|
def get_parameter_fields(cls):
|
|
106
124
|
return [
|
|
@@ -111,6 +129,9 @@ class RuleBackend(
|
|
|
111
129
|
"currencies",
|
|
112
130
|
"countries",
|
|
113
131
|
"classifications",
|
|
132
|
+
"extra_filter_field",
|
|
133
|
+
"extra_filter_field_lower_bound",
|
|
134
|
+
"extra_filter_field_upper_bound",
|
|
114
135
|
]
|
|
115
136
|
|
|
116
137
|
return RuleBackendSerializer
|
|
@@ -131,6 +152,10 @@ class RuleBackend(
|
|
|
131
152
|
repr["Only Countries"] = ", ".join(map(lambda o: o.code_2, self.countries))
|
|
132
153
|
if self.classifications:
|
|
133
154
|
repr["Only Classifications"] = ", ".join(map(lambda o: o.name, self.classifications))
|
|
155
|
+
if self.extra_filter_field:
|
|
156
|
+
repr["Extra Filter Field"] = (
|
|
157
|
+
f"{self.extra_filter_field}=[{self.extra_filter_field_lower_bound},{self.extra_filter_field_upper_bound}["
|
|
158
|
+
)
|
|
134
159
|
return repr
|
|
135
160
|
|
|
136
161
|
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
@@ -180,7 +205,11 @@ class RuleBackend(
|
|
|
180
205
|
& df["primary_classification"].isin(list(map(lambda o: o.id, self.classifications)))
|
|
181
206
|
)
|
|
182
207
|
]
|
|
183
|
-
|
|
208
|
+
if self.extra_filter_field:
|
|
209
|
+
if (lower_bound := self.extra_filter_field_lower_bound) is not None:
|
|
210
|
+
df = df[df[self.extra_filter_field] >= lower_bound]
|
|
211
|
+
if (upper_bound := self.extra_filter_field_upper_bound) is not None:
|
|
212
|
+
df = df[df[self.extra_filter_field] < upper_bound]
|
|
184
213
|
return df
|
|
185
214
|
|
|
186
215
|
def _get_obj_repr(self, pivot_object_id) -> tuple[models.Model | None, str]:
|
|
@@ -93,3 +93,50 @@ class TestExposurePortfolioRuleModel(PortfolioTestMixin):
|
|
|
93
93
|
ExposurePortfolioRuleBackend.GroupbyChoices.FAVORITE_CLASSIFICATION,
|
|
94
94
|
]:
|
|
95
95
|
assert incidents[0].breached_object == classification
|
|
96
|
+
|
|
97
|
+
def test_exposure_rule_with_extra_filter_field(
|
|
98
|
+
self, weekday, portfolio, asset_position_factory, instrument_price_factory, instrument_factory
|
|
99
|
+
):
|
|
100
|
+
parameters = {
|
|
101
|
+
"group_by": "instrument_type",
|
|
102
|
+
"field": ExposurePortfolioRuleBackend.Field.WEIGHTING.value,
|
|
103
|
+
"extra_filter_field": "market_capitalization_usd",
|
|
104
|
+
"extra_filter_field_lower_bound": 1000.0,
|
|
105
|
+
"extra_filter_field_upper_bound": 10000.0,
|
|
106
|
+
}
|
|
107
|
+
exposure_portfolio_backend = ExposurePortfolioRuleBackend(
|
|
108
|
+
weekday,
|
|
109
|
+
portfolio,
|
|
110
|
+
parameters,
|
|
111
|
+
[RuleThresholdFactory.create(range=NumericRange(lower=0.05, upper=None))], # type: ignore
|
|
112
|
+
)
|
|
113
|
+
i1 = instrument_factory.create()
|
|
114
|
+
i2 = instrument_factory.create(instrument_type=i1.instrument_type)
|
|
115
|
+
i3 = instrument_factory.create(instrument_type=i1.instrument_type)
|
|
116
|
+
instrument_price_factory.create(instrument=i1, date=weekday, market_capitalization=1000)
|
|
117
|
+
instrument_price_factory.create(instrument=i2, date=weekday, market_capitalization=9999)
|
|
118
|
+
instrument_price_factory.create(instrument=i3, date=weekday, market_capitalization=10001)
|
|
119
|
+
asset_position_factory.create(
|
|
120
|
+
date=weekday,
|
|
121
|
+
weighting=0.025,
|
|
122
|
+
underlying_instrument=i1,
|
|
123
|
+
portfolio=portfolio,
|
|
124
|
+
)
|
|
125
|
+
asset_position_factory.create(
|
|
126
|
+
date=weekday,
|
|
127
|
+
weighting=0.025,
|
|
128
|
+
underlying_instrument=i2,
|
|
129
|
+
portfolio=portfolio,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
asset_position_factory.create(
|
|
133
|
+
date=weekday,
|
|
134
|
+
weighting=0.95,
|
|
135
|
+
underlying_instrument=i3,
|
|
136
|
+
portfolio=portfolio,
|
|
137
|
+
)
|
|
138
|
+
incidents = list(exposure_portfolio_backend.check_rule())
|
|
139
|
+
assert len(incidents) == 1
|
|
140
|
+
incident = incidents[0]
|
|
141
|
+
assert incident.breached_object_repr == i1.instrument_type
|
|
142
|
+
assert incident.breached_value == '<span style="color:green">+5.00%</span>'
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
1
3
|
from rest_framework.reverse import reverse
|
|
2
4
|
from wbcore import serializers as wb_serializers
|
|
3
5
|
from wbcore.contrib.currency.serializers import CurrencyRepresentationSerializer
|
|
4
6
|
from wbcore.contrib.directory.models import BankingContact
|
|
7
|
+
from wbfdm.serializers import InstrumentRepresentationSerializer
|
|
5
8
|
|
|
6
9
|
from wbportfolio.models import Portfolio, PortfolioPortfolioThroughModel
|
|
7
10
|
from wbportfolio.serializers.rebalancing import RebalancerRepresentationSerializer
|
|
@@ -21,12 +24,25 @@ class PortfolioRepresentationSerializer(wb_serializers.RepresentationSerializer)
|
|
|
21
24
|
|
|
22
25
|
class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
23
26
|
_currency = CurrencyRepresentationSerializer(source="currency")
|
|
27
|
+
_hedged_currency = CurrencyRepresentationSerializer(source="hedged_currency")
|
|
24
28
|
_depends_on = PortfolioRepresentationSerializer(source="depends_on", many=True)
|
|
25
29
|
automatic_rebalancer = wb_serializers.PrimaryKeyRelatedField(read_only=True)
|
|
26
30
|
_automatic_rebalancer = RebalancerRepresentationSerializer(source="automatic_rebalancer")
|
|
31
|
+
_instruments = InstrumentRepresentationSerializer(source="instruments", many=True)
|
|
27
32
|
|
|
28
33
|
create_index = wb_serializers.BooleanField(write_only=True, default=False)
|
|
29
34
|
|
|
35
|
+
last_asset_under_management_usd = wb_serializers.FloatField(read_only=True)
|
|
36
|
+
last_positions = wb_serializers.FloatField(read_only=True)
|
|
37
|
+
last_trade_proposal_date = wb_serializers.DateField(read_only=True)
|
|
38
|
+
next_expected_trade_proposal_date = wb_serializers.SerializerMethodField(read_only=True)
|
|
39
|
+
|
|
40
|
+
def get_next_expected_trade_proposal_date(self, obj):
|
|
41
|
+
if (automatic_rebalancer := getattr(obj, "automatic_rebalancer", None)) and (
|
|
42
|
+
_d := automatic_rebalancer.get_next_rebalancing_date(date.today())
|
|
43
|
+
):
|
|
44
|
+
return _d.strftime("%Y-%m-%d")
|
|
45
|
+
|
|
30
46
|
def create(self, validated_data):
|
|
31
47
|
create_index = validated_data.pop("create_index", False)
|
|
32
48
|
obj = super().create(validated_data)
|
|
@@ -133,8 +149,12 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
133
149
|
"updated_at",
|
|
134
150
|
"_depends_on",
|
|
135
151
|
"depends_on",
|
|
152
|
+
"_instruments",
|
|
153
|
+
"instruments",
|
|
136
154
|
"_currency",
|
|
137
155
|
"currency",
|
|
156
|
+
"_hedged_currency",
|
|
157
|
+
"hedged_currency",
|
|
138
158
|
"automatic_rebalancer",
|
|
139
159
|
"_automatic_rebalancer",
|
|
140
160
|
"invested_timespan",
|
|
@@ -145,6 +165,12 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
145
165
|
"is_composition",
|
|
146
166
|
"_additional_resources",
|
|
147
167
|
"create_index",
|
|
168
|
+
"initial_position_date",
|
|
169
|
+
"last_position_date",
|
|
170
|
+
"last_asset_under_management_usd",
|
|
171
|
+
"last_positions",
|
|
172
|
+
"last_trade_proposal_date",
|
|
173
|
+
"next_expected_trade_proposal_date",
|
|
148
174
|
)
|
|
149
175
|
|
|
150
176
|
|
|
@@ -4,6 +4,7 @@ from decimal import Decimal
|
|
|
4
4
|
from rest_framework import serializers
|
|
5
5
|
from rest_framework.reverse import reverse
|
|
6
6
|
from wbcore import serializers as wb_serializers
|
|
7
|
+
from wbfdm.serializers import InvestableInstrumentRepresentationSerializer
|
|
7
8
|
|
|
8
9
|
from wbportfolio.models import PortfolioRole, Trade, TradeProposal
|
|
9
10
|
from wbportfolio.models.transactions.claim import Claim
|
|
@@ -212,6 +213,14 @@ class TradeModelSerializer(TransactionModelSerializer):
|
|
|
212
213
|
|
|
213
214
|
|
|
214
215
|
class TradeTradeProposalModelSerializer(TradeModelSerializer):
|
|
216
|
+
underlying_instrument_isin = wb_serializers.CharField(read_only=True)
|
|
217
|
+
underlying_instrument_ticker = wb_serializers.CharField(read_only=True)
|
|
218
|
+
underlying_instrument_refinitiv_identifier_code = wb_serializers.CharField(read_only=True)
|
|
219
|
+
underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
|
|
220
|
+
_underlying_instrument = InvestableInstrumentRepresentationSerializer(
|
|
221
|
+
source="underlying_instrument", label_key="name"
|
|
222
|
+
)
|
|
223
|
+
|
|
215
224
|
status = wb_serializers.ChoiceField(default=Trade.Status.DRAFT, choices=Trade.Status.choices)
|
|
216
225
|
target_weight = wb_serializers.DecimalField(max_digits=16, decimal_places=6, required=False, default=0)
|
|
217
226
|
effective_weight = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
@@ -249,6 +258,10 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
|
|
|
249
258
|
fields = (
|
|
250
259
|
"id",
|
|
251
260
|
"shares",
|
|
261
|
+
"underlying_instrument_isin",
|
|
262
|
+
"underlying_instrument_ticker",
|
|
263
|
+
"underlying_instrument_refinitiv_identifier_code",
|
|
264
|
+
"underlying_instrument_instrument_type",
|
|
252
265
|
"underlying_instrument",
|
|
253
266
|
"_underlying_instrument",
|
|
254
267
|
"transaction_subtype",
|
|
@@ -1062,7 +1062,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1062
1062
|
|
|
1063
1063
|
# we approve the rebalancing trade proposal
|
|
1064
1064
|
assert rebalancing_trade_proposal.status == "SUBMIT"
|
|
1065
|
-
rebalancing_trade_proposal.approve()
|
|
1065
|
+
rebalancing_trade_proposal.approve(no_replay=True)
|
|
1066
1066
|
rebalancing_trade_proposal.save()
|
|
1067
1067
|
|
|
1068
1068
|
# check that the rebalancing was applied and position reflect that
|