wbportfolio 1.52.5__py2.py3-none-any.whl → 1.53.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.

@@ -1,11 +1,69 @@
1
+ import math
1
2
  from datetime import date
2
3
  from decimal import Decimal
3
4
 
5
+ import cvxpy as cp
6
+ import numpy as np
4
7
  from django.core.exceptions import ValidationError
5
8
 
6
9
  from wbportfolio.pms.typing import Portfolio, Trade, TradeBatch
7
10
 
8
11
 
12
+ class TradeShareOptimizer:
13
+ def __init__(self, batch: TradeBatch, portfolio_total_value: float):
14
+ self.batch = batch
15
+ self.portfolio_total_value = portfolio_total_value
16
+
17
+ def optimize(self, target_cash: float = 0.99):
18
+ try:
19
+ return self.optimize_trade_share(target_cash)
20
+ except ValueError:
21
+ return self.floor_trade_share()
22
+
23
+ def optimize_trade_share(self, target_cash: float = 0.01):
24
+ prices_fx_portfolio = np.array([trade.price_fx_portfolio for trade in self.batch.trades])
25
+ target_allocs = np.array([trade.target_weight for trade in self.batch.trades])
26
+
27
+ # Decision variable: number of shares (integers)
28
+ shares = cp.Variable(len(prices_fx_portfolio), integer=True)
29
+
30
+ # Calculate portfolio values
31
+ portfolio_values = cp.multiply(shares, prices_fx_portfolio)
32
+
33
+ # Target values based on allocations
34
+ target_values = self.portfolio_total_value * target_allocs
35
+
36
+ # Objective: minimize absolute deviation from target values
37
+ objective = cp.Minimize(cp.sum(cp.abs(portfolio_values - target_values)))
38
+
39
+ # Constraints
40
+ constraints = [
41
+ shares >= 0, # No short selling
42
+ cp.sum(portfolio_values) <= self.portfolio_total_value, # Don't exceed budget
43
+ cp.sum(portfolio_values) >= (1.0 - target_cash) * self.portfolio_total_value, # Use at least 99% of budget
44
+ ]
45
+
46
+ # Solve
47
+ problem = cp.Problem(objective, constraints)
48
+ problem.solve(solver=cp.CBC)
49
+
50
+ if problem != "optimal":
51
+ raise ValueError(f"Optimization failed: {problem.status}")
52
+
53
+ shares_result = shares.value.astype(int)
54
+ return TradeBatch(
55
+ [
56
+ trade.normalize_target(target_shares=shares_result[index])
57
+ for index, trade in enumerate(self.batch.trades)
58
+ ]
59
+ )
60
+
61
+ def floor_trade_share(self):
62
+ return TradeBatch(
63
+ [trade.normalize_target(target_shares=math.floor(trade.target_shares)) for trade in self.batch.trades]
64
+ )
65
+
66
+
9
67
  class TradingService:
10
68
  """
11
69
  This class represents the trading service. It can be instantiated either with the target portfolio and the effective portfolio or given a direct list of trade
@@ -17,25 +75,20 @@ class TradingService:
17
75
  trade_date: date,
18
76
  effective_portfolio: Portfolio | None = None,
19
77
  target_portfolio: Portfolio | None = None,
20
- trades_batch: TradeBatch | None = None,
21
- total_value: Decimal = None,
78
+ total_target_weight: Decimal = Decimal("1.0"),
22
79
  ):
23
- self.total_value = total_value
24
80
  self.trade_date = trade_date
25
81
  if target_portfolio is None:
26
82
  target_portfolio = Portfolio(positions=())
27
83
  if effective_portfolio is None:
28
84
  effective_portfolio = Portfolio(positions=())
29
- # If effective portfoolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
30
- trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio, trades_batch=trades_batch)
31
- # if no trade but a effective portfolio is provided, we get the trade batch only from the effective portofolio (and the target portfolio if provided, but optional. Without it, the trade delta weight will be 0 )
32
- # Finally, we compute the target portfolio
33
- if trades_batch and not target_portfolio:
34
- target_portfolio = trades_batch.convert_to_portfolio()
85
+ # If effective portfolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
86
+ self.trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio).normalize(
87
+ total_target_weight
88
+ )
35
89
 
36
- self.trades_batch = trades_batch
37
- self.effective_portfolio = effective_portfolio
38
- self.target_portfolio = target_portfolio
90
+ self._effective_portfolio = effective_portfolio
91
+ self._target_portfolio = target_portfolio
39
92
 
40
93
  @property
41
94
  def errors(self) -> list[str]:
@@ -62,12 +115,11 @@ class TradingService:
62
115
  Test the given value against all the validators on the field,
63
116
  and either raise a `ValidationError` or simply return.
64
117
  """
65
- # TradeBatch(validated_trades).validate()
66
- if self.effective_portfolio:
118
+ if self._effective_portfolio:
67
119
  for trade in validated_trades:
68
120
  if (
69
121
  trade.effective_weight
70
- and trade.underlying_instrument not in self.effective_portfolio.positions_map
122
+ and trade.underlying_instrument not in self._effective_portfolio.positions_map
71
123
  ):
72
124
  raise ValidationError("All effective position needs to be matched with a validated trade")
73
125
 
@@ -75,7 +127,6 @@ class TradingService:
75
127
  self,
76
128
  effective_portfolio: Portfolio,
77
129
  target_portfolio: Portfolio,
78
- trades_batch: TradeBatch | None = None,
79
130
  ) -> TradeBatch:
80
131
  """
81
132
  Given combination of effective portfolio and either a trades batch or a target portfolio, ensure all theres variables are set
@@ -87,39 +138,40 @@ class TradingService:
87
138
 
88
139
  Returns: The normalized trades batch
89
140
  """
90
- instruments = list(effective_portfolio.positions_map.keys())
91
- instruments.extend(list(target_portfolio.positions_map.keys()))
92
- if trades_batch:
93
- instruments.extend(list(trades_batch.trades_map.keys()))
94
- _trades: list[Trade] = []
95
- for instrument in set(instruments):
96
- effective_weight = target_weight = 0
97
- effective_shares = 0
98
- instrument_type = currency = None
99
- if effective_pos := effective_portfolio.positions_map.get(instrument, None):
100
- effective_weight = effective_pos.weighting
101
- effective_shares = effective_pos.shares
102
- instrument_type, currency = effective_pos.instrument_type, effective_pos.currency
103
- if target_pos := target_portfolio.positions_map.get(instrument, None):
104
- target_weight = target_pos.weighting
105
- instrument_type, currency = target_pos.instrument_type, target_pos.currency
106
- if trades_batch and (trade := trades_batch.trades_map.get(instrument, None)):
107
- effective_weight, target_weight = trade.effective_weight, trade.target_weight
108
- effective_shares = trade.effective_shares
109
- instrument_type, currency = trade.instrument_type, trade.currency
110
-
111
- _trades.append(
112
- Trade(
113
- underlying_instrument=instrument,
114
- effective_weight=effective_weight,
115
- target_weight=target_weight,
116
- effective_shares=effective_shares,
117
- date=self.trade_date,
118
- instrument_type=instrument_type,
119
- currency=currency,
141
+ instruments = effective_portfolio.positions_map.copy()
142
+ instruments.update(target_portfolio.positions_map)
143
+
144
+ trades: list[Trade] = []
145
+ for instrument_id, pos in instruments.items():
146
+ if not pos.is_cash:
147
+ effective_weight = target_weight = 0
148
+ effective_shares = target_shares = 0
149
+ drift_factor = 1.0
150
+ if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
151
+ effective_weight = effective_pos.weighting
152
+ effective_shares = effective_pos.shares
153
+ drift_factor = effective_pos.drift_factor
154
+ if target_pos := target_portfolio.positions_map.get(instrument_id, None):
155
+ target_weight = target_pos.weighting
156
+ if target_pos.shares is not None:
157
+ target_shares = target_pos.shares
158
+
159
+ trades.append(
160
+ Trade(
161
+ underlying_instrument=instrument_id,
162
+ effective_weight=effective_weight,
163
+ target_weight=target_weight,
164
+ effective_shares=effective_shares,
165
+ target_shares=target_shares,
166
+ date=self.trade_date,
167
+ instrument_type=pos.instrument_type,
168
+ currency=pos.currency,
169
+ price=Decimal(pos.price),
170
+ currency_fx_rate=Decimal(pos.currency_fx_rate),
171
+ drift_factor=Decimal(drift_factor),
172
+ )
120
173
  )
121
- )
122
- return TradeBatch(tuple(_trades))
174
+ return TradeBatch(tuple(trades))
123
175
 
124
176
  def is_valid(self, ignore_error: bool = False) -> bool:
125
177
  """
@@ -152,10 +204,7 @@ class TradingService:
152
204
 
153
205
  return not bool(self._errors)
154
206
 
155
- def normalize(self):
156
- """
157
- Normalize the instantiate trades batch so that the target weight is 100%
158
- """
159
- self.trades_batch = TradeBatch(
160
- [trade.normalize_target(self.trades_batch.total_target_weight) for trade in self.trades_batch.trades]
161
- )
207
+ def get_optimized_trade_batch(self, portfolio_total_value: float, target_cash: float):
208
+ return TradeShareOptimizer(
209
+ self.trades_batch, portfolio_total_value
210
+ ).floor_trade_share() # TODO switch to the other optimization when ready
wbportfolio/pms/typing.py CHANGED
@@ -19,6 +19,7 @@ class Position:
19
19
  weighting: Decimal
20
20
  date: date_lib
21
21
 
22
+ drift_factor: float = 1.0
22
23
  currency: int | None = None
23
24
  instrument_type: int | None = None
24
25
  asset_valuation_date: date_lib | None = None
@@ -48,7 +49,7 @@ class Position:
48
49
  @dataclass(frozen=True)
49
50
  class Portfolio:
50
51
  positions: tuple[Position] | tuple
51
- positions_map: dict[Position] = field(init=False, repr=False)
52
+ positions_map: dict[int, Position] = field(init=False, repr=False)
52
53
 
53
54
  def __post_init__(self):
54
55
  positions_map = {}
@@ -80,23 +81,26 @@ class Portfolio:
80
81
  @dataclass(frozen=True)
81
82
  class Trade:
82
83
  underlying_instrument: int
83
- instrument_type: str
84
+ instrument_type: int
84
85
  currency: int
85
86
  date: date_lib
86
-
87
+ price: Decimal
87
88
  effective_weight: Decimal
88
89
  target_weight: Decimal
90
+ currency_fx_rate: Decimal = Decimal("1")
91
+ effective_shares: Decimal = Decimal("0")
92
+ target_shares: Decimal = Decimal("0")
93
+ drift_factor: Decimal = Decimal("1")
89
94
  id: int | None = None
90
- effective_shares: Decimal = None
95
+ is_cash: bool = False
91
96
 
92
97
  def __add__(self, other):
93
98
  return Trade(
94
99
  underlying_instrument=self.underlying_instrument,
95
- effective_weight=self.effective_weight + other.effective_weight,
100
+ effective_weight=self.effective_weight,
96
101
  target_weight=self.target_weight + other.target_weight,
97
- effective_shares=self.effective_shares + other.effective_shares
98
- if (self.effective_shares is not None and other.effective_shares is not None)
99
- else None,
102
+ effective_shares=self.effective_shares,
103
+ target_shares=self.target_shares + other.target_shares,
100
104
  **{
101
105
  f.name: getattr(self, f.name)
102
106
  for f in fields(Trade)
@@ -105,15 +109,29 @@ class Trade:
105
109
  "effective_weight",
106
110
  "target_weight",
107
111
  "effective_shares",
112
+ "target_shares",
108
113
  "underlying_instrument",
109
114
  ]
110
115
  },
111
116
  )
112
117
 
118
+ def copy(self, **kwargs):
119
+ attrs = {f.name: getattr(self, f.name) for f in fields(Trade)}
120
+ attrs.update(kwargs)
121
+ return Trade(**attrs)
122
+
113
123
  @property
114
124
  def delta_weight(self) -> Decimal:
115
125
  return self.target_weight - self.effective_weight
116
126
 
127
+ @property
128
+ def delta_shares(self) -> Decimal:
129
+ return self.target_shares - self.effective_shares
130
+
131
+ @property
132
+ def price_fx_portfolio(self) -> Decimal:
133
+ return self.price * self.currency_fx_rate
134
+
117
135
  def validate(self):
118
136
  return True
119
137
  # if self.effective_weight < 0 or self.effective_weight > 1.0:
@@ -121,17 +139,22 @@ class Trade:
121
139
  # if self.target_weight < 0 or self.target_weight > 1.0:
122
140
  # raise ValidationError("Target Weight needs to be in range [0, 1]")
123
141
 
124
- def normalize_target(self, total_target_weight: Decimal):
125
- t = Trade(
126
- target_weight=self.target_weight / total_target_weight if total_target_weight else self.target_weight,
127
- **{f.name: getattr(self, f.name) for f in fields(Trade) if f.name not in ["target_weight"]},
128
- )
129
- return t
142
+ def normalize_target(
143
+ self, factor: Decimal | None = None, target_shares: Decimal | int | None = None, target_weight: Decimal = None
144
+ ):
145
+ if factor is None:
146
+ if target_shares is not None:
147
+ factor = target_shares / self.target_shares if self.target_shares else Decimal("1")
148
+ elif target_weight is not None:
149
+ factor = target_weight / self.target_weight if self.target_weight else Decimal("1")
150
+ else:
151
+ raise ValueError("Target weight and shares cannot be both None")
152
+ return self.copy(target_weight=self.target_weight * factor, target_shares=self.target_shares * factor)
130
153
 
131
154
 
132
155
  @dataclass(frozen=True)
133
156
  class TradeBatch:
134
- trades: tuple[Trade]
157
+ trades: list[Trade]
135
158
  trades_map: dict[Trade] = field(init=False, repr=False)
136
159
 
137
160
  def __post_init__(self):
@@ -145,15 +168,15 @@ class TradeBatch:
145
168
 
146
169
  @property
147
170
  def total_target_weight(self) -> Decimal:
148
- return round(sum([trade.target_weight for trade in self.trades]), 6)
171
+ return round(sum([trade.target_weight for trade in self.trades], Decimal("0")), 6)
149
172
 
150
173
  @property
151
174
  def total_effective_weight(self) -> Decimal:
152
- return round(sum([trade.effective_weight for trade in self.trades]), 6)
175
+ return round(sum([trade.effective_weight for trade in self.trades], Decimal("0")), 6)
153
176
 
154
177
  @property
155
- def totat_abs_delta_weight(self) -> Decimal:
156
- return sum([abs(trade.delta_weight) for trade in self.trades])
178
+ def total_abs_delta_weight(self) -> Decimal:
179
+ return sum([abs(trade.delta_weight) for trade in self.trades], Decimal("0"))
157
180
 
158
181
  def __add__(self, other):
159
182
  return TradeBatch(tuple(self.trades + other.trades))
@@ -165,7 +188,7 @@ class TradeBatch:
165
188
  if round(float(self.total_target_weight), 4) != 1: # we do that to remove decimal over precision
166
189
  raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
167
190
 
168
- def convert_to_portfolio(self):
191
+ def convert_to_portfolio(self, *extra_positions):
169
192
  positions = []
170
193
  for instrument, trade in self.trades_map.items():
171
194
  positions.append(
@@ -173,8 +196,28 @@ class TradeBatch:
173
196
  underlying_instrument=trade.underlying_instrument,
174
197
  instrument_type=trade.instrument_type,
175
198
  weighting=trade.target_weight,
199
+ shares=trade.target_shares,
176
200
  currency=trade.currency,
177
201
  date=trade.date,
202
+ is_cash=trade.is_cash,
203
+ price=trade.price,
204
+ currency_fx_rate=trade.currency_fx_rate,
178
205
  )
179
206
  )
207
+ for position in extra_positions:
208
+ if position.weighting:
209
+ positions.append(position)
180
210
  return Portfolio(tuple(positions))
211
+
212
+ def normalize(self, total_target_weight: Decimal = Decimal("1.0")):
213
+ """
214
+ Normalize the instantiate trades batch so that the target weight is 100%
215
+ """
216
+ normalization_factor = (
217
+ total_target_weight / self.total_target_weight if self.total_target_weight else Decimal("0.0")
218
+ )
219
+ normalized_trades = []
220
+ for trade in self.trades:
221
+ normalized_trades.append(trade.normalize_target(normalization_factor))
222
+ tb = TradeBatch(normalized_trades)
223
+ return tb
@@ -16,25 +16,22 @@ class ModelPortfolioRebalancing(AbstractRebalancingModel):
16
16
 
17
17
  @property
18
18
  def model_portfolio(self):
19
- if model_portfolio_rel := self.model_portfolio_rel:
20
- return model_portfolio_rel.dependency_portfolio
19
+ return self.model_portfolio_rel.dependency_portfolio if self.model_portfolio_rel else None
20
+
21
+ @property
22
+ def assets(self):
23
+ return self.model_portfolio.get_positions(self.last_effective_date) if self.model_portfolio else []
21
24
 
22
25
  def is_valid(self) -> bool:
23
- if model_portfolio := self.model_portfolio:
24
- assets = model_portfolio.get_positions(self.last_effective_date)
25
- return (
26
- assets.exists()
27
- and InstrumentPrice.objects.filter(
28
- date=self.trade_date, instrument__in=assets.values("underlying_quote")
29
- ).exists()
30
- )
31
- return False
26
+ instruments = list(map(lambda o: o.underlying_quote, self.assets))
27
+ return (
28
+ len(self.assets) > 0
29
+ and InstrumentPrice.objects.filter(date=self.trade_date, instrument__in=instruments).exists()
30
+ )
32
31
 
33
32
  def get_target_portfolio(self) -> Portfolio:
34
33
  positions = []
35
- assets = self.model_portfolio.get_positions(self.last_effective_date)
36
-
37
- for asset in assets:
34
+ for asset in self.assets:
38
35
  asset.date = self.trade_date
39
36
  asset.asset_valuation_date = self.trade_date
40
37
  positions.append(asset._build_dto())
@@ -1,3 +1,5 @@
1
+ from decimal import Decimal
2
+
1
3
  from django.contrib.messages import warning
2
4
  from django.core.exceptions import ValidationError
3
5
  from rest_framework.reverse import reverse
@@ -16,6 +18,17 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
16
18
  queryset=Portfolio.objects.all(), write_only=True, required=False, default=DefaultFromView("portfolio")
17
19
  )
18
20
  _target_portfolio = PortfolioRepresentationSerializer(source="target_portfolio")
21
+ total_cash_weight = wb_serializers.DecimalField(
22
+ default=0,
23
+ decimal_places=4,
24
+ max_digits=5,
25
+ write_only=True,
26
+ required=False,
27
+ precision=4,
28
+ percent=True,
29
+ label="Target Cash",
30
+ help_text="Enter the desired percentage for the cash component. The remaining percentage (100% minus this value) will be allocated to total target weighting. Default is 0%.",
31
+ )
19
32
 
20
33
  trade_date = wb_serializers.DateField(
21
34
  read_only=lambda view: not view.new_mode, default=DefaultFromView("default_trade_date")
@@ -23,6 +36,7 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
23
36
 
24
37
  def create(self, validated_data):
25
38
  target_portfolio = validated_data.pop("target_portfolio", None)
39
+ total_cash_weight = validated_data.pop("total_cash_weight", Decimal("0.0"))
26
40
  rebalancing_model = validated_data.get("rebalancing_model", None)
27
41
  if request := self.context.get("request"):
28
42
  validated_data["creator"] = request.user.profile
@@ -32,7 +46,9 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
32
46
  if target_portfolio and not rebalancing_model and (last_effective_date := obj.last_effective_date):
33
47
  target_portfolio_dto = target_portfolio._build_dto(last_effective_date)
34
48
  try:
35
- obj.reset_trades(target_portfolio=target_portfolio_dto)
49
+ obj.reset_trades(
50
+ target_portfolio=target_portfolio_dto, total_target_weight=Decimal("1.0") - total_cash_weight
51
+ )
36
52
  except ValidationError as e:
37
53
  if request := self.context.get("request"):
38
54
  warning(request, str(e), extra_tags="auto_close=0")
@@ -60,6 +76,7 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
60
76
  fields = (
61
77
  "id",
62
78
  "trade_date",
79
+ "total_cash_weight",
63
80
  "comment",
64
81
  "status",
65
82
  "portfolio",
@@ -315,11 +315,22 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
315
315
  )
316
316
 
317
317
  status = wb_serializers.ChoiceField(default=Trade.Status.DRAFT, choices=Trade.Status.choices)
318
- target_weight = wb_serializers.DecimalField(max_digits=16, decimal_places=6, required=False, default=0)
319
- effective_weight = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
318
+ weighting = wb_serializers.DecimalField(max_digits=7, decimal_places=6)
319
+ target_weight = wb_serializers.DecimalField(max_digits=7, decimal_places=6, required=False, default=0)
320
+ effective_weight = wb_serializers.DecimalField(read_only=True, max_digits=7, decimal_places=6, default=0)
320
321
  effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
321
322
  target_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
322
323
 
324
+ total_value_fx_portfolio = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=2, default=0)
325
+ effective_total_value_fx_portfolio = wb_serializers.DecimalField(
326
+ read_only=True, max_digits=16, decimal_places=2, default=0
327
+ )
328
+ target_total_value_fx_portfolio = wb_serializers.DecimalField(
329
+ read_only=True, max_digits=16, decimal_places=2, default=0
330
+ )
331
+
332
+ portfolio_currency = wb_serializers.CharField(read_only=True)
333
+
323
334
  def validate(self, data):
324
335
  data.pop("company", None)
325
336
  data.pop("security", None)
@@ -331,6 +342,8 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
331
342
  )
332
343
  effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
333
344
  weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
345
+ if (target_weight := data.pop("target_weight", None)) is not None:
346
+ weighting = target_weight - effective_weight
334
347
  if (target_weight := data.pop("target_weight", None)) is not None:
335
348
  weighting = target_weight - effective_weight
336
349
  if weighting >= 0:
@@ -343,14 +356,25 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
343
356
  class Meta:
344
357
  model = Trade
345
358
  percent_fields = ["effective_weight", "target_weight", "weighting"]
359
+ decorators = {
360
+ "total_value_fx_portfolio": wb_serializers.decorator(
361
+ decorator_type="text", position="left", value="{{portfolio_currency}}"
362
+ ),
363
+ "effective_total_value_fx_portfolio": wb_serializers.decorator(
364
+ decorator_type="text", position="left", value="{{portfolio_currency}}"
365
+ ),
366
+ "target_total_value_fx_portfolio": wb_serializers.decorator(
367
+ decorator_type="text", position="left", value="{{portfolio_currency}}"
368
+ ),
369
+ }
346
370
  read_only_fields = (
347
371
  "transaction_subtype",
348
372
  "shares",
349
- # "underlying_instrument",
350
- # "_underlying_instrument",
351
- "shares",
352
373
  "effective_shares",
353
374
  "target_shares",
375
+ "total_value_fx_portfolio",
376
+ "effective_total_value_fx_portfolio",
377
+ "target_total_value_fx_portfolio",
354
378
  )
355
379
  fields = (
356
380
  "id",
@@ -375,6 +399,10 @@ class TradeTradeProposalModelSerializer(TradeModelSerializer):
375
399
  "order",
376
400
  "effective_shares",
377
401
  "target_shares",
402
+ "total_value_fx_portfolio",
403
+ "effective_total_value_fx_portfolio",
404
+ "target_total_value_fx_portfolio",
405
+ "portfolio_currency",
378
406
  )
379
407
 
380
408
 
@@ -1033,18 +1033,19 @@ class TestPortfolioModel(PortfolioTestMixin):
1033
1033
 
1034
1034
  i1 = instrument_factory.create(currency=portfolio.currency)
1035
1035
  i2 = instrument_factory.create(currency=portfolio.currency)
1036
+
1037
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=(weekday - BDay(1)).date())
1038
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=weekday)
1039
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=middle_date)
1040
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i1, date=rebalancing_date)
1041
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=(weekday - BDay(1)).date())
1042
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=weekday)
1043
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=middle_date)
1044
+ instrument_price_factory.create(net_value=Decimal("100"), instrument=i2, date=rebalancing_date)
1045
+
1036
1046
  asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i1, weighting=0.7)
1037
1047
  asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i2, weighting=0.3)
1038
1048
 
1039
- instrument_price_factory.create(instrument=i1, date=(weekday - BDay(1)).date())
1040
- instrument_price_factory.create(instrument=i1, date=weekday)
1041
- instrument_price_factory.create(instrument=i1, date=middle_date)
1042
- instrument_price_factory.create(instrument=i1, date=rebalancing_date)
1043
- instrument_price_factory.create(instrument=i2, date=(weekday - BDay(1)).date())
1044
- instrument_price_factory.create(instrument=i2, date=weekday)
1045
- instrument_price_factory.create(instrument=i2, date=middle_date)
1046
- instrument_price_factory.create(instrument=i2, date=rebalancing_date)
1047
-
1048
1049
  rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
1049
1050
  positions, rebalancing_trade_proposal = portfolio.drift_weights(weekday, (rebalancing_date + BDay(1)).date())
1050
1051
  assert rebalancing_trade_proposal.trade_date == rebalancing_date