wbportfolio 1.54.23__py2.py3-none-any.whl → 1.55.4__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 (64) hide show
  1. wbportfolio/admin/indexes.py +1 -1
  2. wbportfolio/admin/product_groups.py +1 -1
  3. wbportfolio/admin/products.py +2 -1
  4. wbportfolio/admin/rebalancing.py +1 -1
  5. wbportfolio/api_clients/__init__.py +0 -0
  6. wbportfolio/api_clients/ubs.py +150 -0
  7. wbportfolio/factories/orders/order_proposals.py +3 -1
  8. wbportfolio/factories/portfolios.py +1 -1
  9. wbportfolio/factories/rebalancing.py +1 -1
  10. wbportfolio/filters/orders/__init__.py +1 -0
  11. wbportfolio/filters/orders/order_proposals.py +58 -0
  12. wbportfolio/filters/portfolios.py +20 -0
  13. wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
  14. wbportfolio/import_export/backends/ubs/fees.py +10 -20
  15. wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
  16. wbportfolio/import_export/backends/utils.py +0 -17
  17. wbportfolio/import_export/handlers/asset_position.py +1 -1
  18. wbportfolio/import_export/handlers/trade.py +2 -2
  19. wbportfolio/import_export/parsers/sg_lux/equity.py +1 -1
  20. wbportfolio/import_export/parsers/societe_generale/strategy.py +2 -2
  21. wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
  22. wbportfolio/import_export/parsers/ubs/equity.py +1 -1
  23. wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
  24. wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
  25. wbportfolio/models/builder.py +70 -25
  26. wbportfolio/models/mixins/instruments.py +7 -0
  27. wbportfolio/models/orders/order_proposals.py +512 -161
  28. wbportfolio/models/orders/orders.py +20 -10
  29. wbportfolio/models/orders/routing.py +54 -0
  30. wbportfolio/models/portfolio.py +76 -41
  31. wbportfolio/models/rebalancing.py +6 -6
  32. wbportfolio/models/transactions/transactions.py +10 -6
  33. wbportfolio/order_routing/__init__.py +19 -0
  34. wbportfolio/order_routing/adapters/__init__.py +57 -0
  35. wbportfolio/order_routing/adapters/ubs.py +161 -0
  36. wbportfolio/pms/trading/handler.py +4 -0
  37. wbportfolio/pms/typing.py +62 -8
  38. wbportfolio/rebalancing/models/composite.py +1 -1
  39. wbportfolio/rebalancing/models/equally_weighted.py +3 -3
  40. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -1
  41. wbportfolio/rebalancing/models/model_portfolio.py +9 -10
  42. wbportfolio/serializers/orders/order_proposals.py +23 -3
  43. wbportfolio/serializers/orders/orders.py +5 -2
  44. wbportfolio/serializers/positions.py +2 -2
  45. wbportfolio/serializers/rebalancing.py +1 -1
  46. wbportfolio/tests/models/orders/test_order_proposals.py +29 -15
  47. wbportfolio/tests/models/test_imports.py +7 -6
  48. wbportfolio/tests/models/test_portfolios.py +57 -23
  49. wbportfolio/tests/models/transactions/test_rebalancing.py +2 -2
  50. wbportfolio/tests/rebalancing/test_models.py +3 -5
  51. wbportfolio/viewsets/charts/assets.py +4 -1
  52. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  53. wbportfolio/viewsets/configs/menu/__init__.py +1 -0
  54. wbportfolio/viewsets/configs/menu/orders.py +11 -0
  55. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +56 -12
  56. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +55 -9
  57. wbportfolio/viewsets/orders/configs/displays/orders.py +13 -0
  58. wbportfolio/viewsets/orders/order_proposals.py +42 -4
  59. wbportfolio/viewsets/orders/orders.py +33 -31
  60. wbportfolio/viewsets/portfolios.py +3 -3
  61. {wbportfolio-1.54.23.dist-info → wbportfolio-1.55.4.dist-info}/METADATA +1 -1
  62. {wbportfolio-1.54.23.dist-info → wbportfolio-1.55.4.dist-info}/RECORD +64 -54
  63. {wbportfolio-1.54.23.dist-info → wbportfolio-1.55.4.dist-info}/WHEEL +0 -0
  64. {wbportfolio-1.54.23.dist-info → wbportfolio-1.55.4.dist-info}/licenses/LICENSE +0 -0
@@ -145,12 +145,15 @@ class TradingService:
145
145
  previous_weight = target_weight = 0
146
146
  effective_shares = target_shares = 0
147
147
  daily_return = 0
148
+ is_cash = False
148
149
  if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
149
150
  previous_weight = effective_pos.weighting
150
151
  effective_shares = effective_pos.shares
151
152
  daily_return = effective_pos.daily_return
153
+ is_cash = effective_pos.is_cash
152
154
  if target_pos := target_portfolio.positions_map.get(instrument_id, None):
153
155
  target_weight = target_pos.weighting
156
+ is_cash = target_pos.is_cash
154
157
  if target_pos.shares is not None:
155
158
  target_shares = target_pos.shares
156
159
  trade = Trade(
@@ -166,6 +169,7 @@ class TradingService:
166
169
  currency_fx_rate=Decimal(pos.currency_fx_rate),
167
170
  daily_return=Decimal(daily_return),
168
171
  portfolio_contribution=effective_portfolio.portfolio_contribution,
172
+ is_cash=is_cash,
169
173
  )
170
174
  trades.append(trade)
171
175
  return TradeBatch(trades)
wbportfolio/pms/typing.py CHANGED
@@ -1,4 +1,6 @@
1
+ import enum
1
2
  from dataclasses import asdict, dataclass, field, fields
3
+ from datetime import date
2
4
  from datetime import date as date_lib
3
5
  from decimal import Decimal
4
6
 
@@ -13,7 +15,7 @@ class Valuation:
13
15
  outstanding_shares: Decimal = Decimal(0)
14
16
 
15
17
 
16
- @dataclass(frozen=True)
18
+ @dataclass()
17
19
  class Position:
18
20
  underlying_instrument: int
19
21
  weighting: Decimal
@@ -38,6 +40,10 @@ class Position:
38
40
  volume_usd: float = None
39
41
  price: float = None
40
42
 
43
+ def __post_init__(self):
44
+ self.daily_return = round(self.daily_return, 16)
45
+ self.weighting = round(self.weighting, 8)
46
+
41
47
  def __add__(self, other):
42
48
  return Position(
43
49
  weighting=self.weighting + other.weighting,
@@ -58,6 +64,7 @@ class Portfolio:
58
64
 
59
65
  def __post_init__(self):
60
66
  positions_map = {}
67
+
61
68
  for pos in self.positions:
62
69
  if pos.underlying_instrument in positions_map:
63
70
  positions_map[pos.underlying_instrument] += pos
@@ -67,7 +74,7 @@ class Portfolio:
67
74
 
68
75
  @property
69
76
  def total_weight(self):
70
- return round(sum([pos.weighting for pos in self.positions]), 8)
77
+ return sum([pos.weighting for pos in self.positions])
71
78
 
72
79
  @property
73
80
  def total_shares(self):
@@ -75,7 +82,7 @@ class Portfolio:
75
82
 
76
83
  @property
77
84
  def portfolio_contribution(self) -> Decimal:
78
- return sum(map(lambda pos: pos.weighting * (Decimal("1") + pos.daily_return), self.positions))
85
+ return round(sum(map(lambda pos: pos.weighting * (Decimal("1") + pos.daily_return), self.positions)), 16)
79
86
 
80
87
  def to_df(self):
81
88
  return pd.DataFrame([asdict(pos) for pos in self.positions])
@@ -91,6 +98,28 @@ class Portfolio:
91
98
 
92
99
 
93
100
  @dataclass(frozen=True)
101
+ class Order:
102
+ class AssetType(enum.Enum):
103
+ EQUITY = "EQUITY"
104
+
105
+ id: int | str
106
+ trade_date: date
107
+ target_weight: float
108
+
109
+ # Instrument identifier
110
+ asset_class: AssetType
111
+ refinitiv_identifier_code: str | None = None
112
+ bloomberg_ticker: str | None = None
113
+ sedol: str | None = None
114
+
115
+ weighting: float | None = None
116
+ target_shares: float | None = None
117
+ shares: float | None = None
118
+ execution_instruction: str | None = None
119
+ comment: str = ""
120
+
121
+
122
+ @dataclass()
94
123
  class Trade:
95
124
  underlying_instrument: int
96
125
  instrument_type: int
@@ -104,9 +133,19 @@ class Trade:
104
133
  target_shares: Decimal = Decimal("0")
105
134
  daily_return: Decimal = Decimal("0")
106
135
  portfolio_contribution: Decimal = Decimal("1")
136
+ quantization_error: Decimal = Decimal("0")
137
+
107
138
  id: int | None = None
108
139
  is_cash: bool = False
109
140
 
141
+ def __post_init__(self):
142
+ self.previous_weight = round(self.previous_weight, 8)
143
+ # ensure a trade target weight cannot be lower than 0
144
+ self.target_weight = round(self.target_weight, 8)
145
+ if self.target_weight < Decimal("1e-7"):
146
+ self.target_weight = Decimal("0")
147
+ self.daily_return = round(self.daily_return, 16)
148
+
110
149
  def __add__(self, other):
111
150
  return Trade(
112
151
  underlying_instrument=self.underlying_instrument,
@@ -137,12 +176,20 @@ class Trade:
137
176
  attrs.update(kwargs)
138
177
  return Trade(**attrs)
139
178
 
179
+ def set_quantization_error(self, quantization_error: Decimal):
180
+ self.quantization_error = quantization_error
181
+ self.target_weight += quantization_error
182
+
140
183
  @property
141
184
  def effective_weight(self) -> Decimal:
142
185
  return (
143
- self.previous_weight * (round(self.daily_return, 16) + 1) / self.portfolio_contribution
144
- if self.portfolio_contribution
145
- else self.previous_weight
186
+ round(
187
+ self.previous_weight * (self.daily_return + 1) / self.portfolio_contribution
188
+ if self.portfolio_contribution
189
+ else self.previous_weight,
190
+ 8,
191
+ )
192
+ + self.quantization_error
146
193
  )
147
194
 
148
195
  @property
@@ -184,6 +231,9 @@ class TradeBatch:
184
231
 
185
232
  def __post_init__(self):
186
233
  trade_map = {}
234
+ total_effective_weight = self.total_effective_weight
235
+ if total_effective_weight and (quant_error := Decimal("1") - self.total_effective_weight):
236
+ self.largest_effective_order.set_quantization_error(quant_error)
187
237
  for trade in self.trades:
188
238
  if trade.underlying_instrument in trade_map:
189
239
  trade_map[trade.underlying_instrument] += trade
@@ -191,13 +241,17 @@ class TradeBatch:
191
241
  trade_map[trade.underlying_instrument] = trade
192
242
  object.__setattr__(self, "trades_map", trade_map)
193
243
 
244
+ @property
245
+ def largest_effective_order(self) -> Trade:
246
+ return max(self.trades, key=lambda obj: obj.previous_weight)
247
+
194
248
  @property
195
249
  def total_target_weight(self) -> Decimal:
196
- return round(sum([trade.target_weight for trade in self.trades], Decimal("0")), 8)
250
+ return sum([trade.target_weight for trade in self.trades], Decimal("0"))
197
251
 
198
252
  @property
199
253
  def total_effective_weight(self) -> Decimal:
200
- return round(sum([trade.effective_weight for trade in self.trades], Decimal("0")), 8)
254
+ return sum([trade.effective_weight for trade in self.trades], Decimal("0"))
201
255
 
202
256
  @property
203
257
  def total_abs_delta_weight(self) -> Decimal:
@@ -20,7 +20,7 @@ class CompositeRebalancing(AbstractRebalancingModel):
20
20
  """
21
21
  try:
22
22
  latest_order_proposal = self.portfolio.order_proposals.filter(
23
- status="APPROVED", trade_date__lt=self.trade_date
23
+ status="APPLIED", trade_date__lt=self.trade_date
24
24
  ).latest("trade_date")
25
25
  return {
26
26
  v["underlying_instrument"]: v["target_weight"]
@@ -24,11 +24,11 @@ class EquallyWeightedRebalancing(AbstractRebalancingModel):
24
24
 
25
25
  def get_target_portfolio(self) -> Portfolio:
26
26
  positions = []
27
- nb_assets = len(self.effective_portfolio.positions)
28
- for position in self.effective_portfolio.positions:
27
+ assets = list(filter(lambda p: not p.is_cash, self.effective_portfolio.positions))
28
+ for position in assets:
29
29
  positions.append(
30
30
  position.copy(
31
- weighting=Decimal(1 / nb_assets), date=self.trade_date, asset_valuation_date=self.trade_date
31
+ weighting=Decimal(1 / len(assets)), date=self.trade_date, asset_valuation_date=self.trade_date
32
32
  )
33
33
  )
34
34
  return Portfolio(positions)
@@ -22,7 +22,7 @@ from wbportfolio.rebalancing.decorators import register
22
22
  @register("Market Capitalization Rebalancing")
23
23
  class MarketCapitalizationRebalancing(AbstractRebalancingModel):
24
24
  TARGET_CURRENCY: str = "USD"
25
- MIN_WEIGHT: float = 10e-5 # we allow only weight of minimum 0.01%
25
+ MIN_WEIGHT: float = 1e-5 # we allow only weight of minimum 0.01%
26
26
 
27
27
  def __init__(
28
28
  self,
@@ -1,5 +1,3 @@
1
- from wbfdm.models import InstrumentPrice
2
-
3
1
  from wbportfolio.pms.typing import Portfolio
4
2
  from wbportfolio.rebalancing.base import AbstractRebalancingModel
5
3
  from wbportfolio.rebalancing.decorators import register
@@ -9,6 +7,11 @@ from wbportfolio.rebalancing.decorators import register
9
7
  class ModelPortfolioRebalancing(AbstractRebalancingModel):
10
8
  def __init__(self, *args, **kwargs):
11
9
  super().__init__(*args, **kwargs)
10
+ self.value_date = (
11
+ self.trade_date
12
+ if self.model_portfolio.assets.filter(date=self.trade_date).exists()
13
+ else self.last_effective_date
14
+ )
12
15
 
13
16
  @property
14
17
  def model_portfolio_rel(self):
@@ -20,19 +23,15 @@ class ModelPortfolioRebalancing(AbstractRebalancingModel):
20
23
 
21
24
  @property
22
25
  def assets(self):
23
- return self.model_portfolio.get_positions(self.last_effective_date) if self.model_portfolio else []
26
+ return self.model_portfolio.get_positions(self.value_date) if self.model_portfolio else []
24
27
 
25
28
  def is_valid(self) -> bool:
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
- )
29
+ return len(self.assets) > 0
31
30
 
32
31
  def get_target_portfolio(self) -> Portfolio:
33
32
  positions = []
34
33
  for asset in self.assets:
35
- asset.date = self.trade_date
36
- asset.asset_valuation_date = self.trade_date
34
+ asset.date = self.value_date
35
+ asset.asset_valuation_date = self.value_date
37
36
  positions.append(asset._build_dto())
38
37
  return Portfolio(positions=tuple(positions))
@@ -2,7 +2,8 @@ from django.contrib.messages import warning
2
2
  from django.core.exceptions import ValidationError
3
3
  from rest_framework.reverse import reverse
4
4
  from wbcore import serializers as wb_serializers
5
- from wbcore.serializers import DefaultFromView
5
+ from wbcore.contrib.directory.serializers import PersonRepresentationSerializer
6
+ from wbcore.serializers import CharField, DefaultFromView
6
7
 
7
8
  from wbportfolio.models import OrderProposal, Portfolio, RebalancingModel
8
9
 
@@ -18,6 +19,7 @@ class OrderProposalRepresentationSerializer(wb_serializers.RepresentationSeriali
18
19
 
19
20
 
20
21
  class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
22
+ _portfolio = PortfolioRepresentationSerializer(source="portfolio")
21
23
  rebalancing_model = wb_serializers.PrimaryKeyRelatedField(queryset=RebalancingModel.objects.all(), required=False)
22
24
  _rebalancing_model = RebalancingModelRepresentationSerializer(source="rebalancing_model")
23
25
  target_portfolio = wb_serializers.PrimaryKeyRelatedField(
@@ -27,6 +29,15 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
27
29
  trade_date = wb_serializers.DateField(
28
30
  read_only=lambda view: not view.new_mode, default=DefaultFromView("default_trade_date")
29
31
  )
32
+ _creator = PersonRepresentationSerializer(source="creator")
33
+ _approver = PersonRepresentationSerializer(source="approver")
34
+ execution_status_repr = wb_serializers.SerializerMethodField(label="Status", field_class=CharField, read_only=True)
35
+
36
+ def get_execution_status_repr(self, obj):
37
+ repr = obj.execution_status
38
+ if obj.execution_status_detail:
39
+ repr += f" (Custodian: {obj.execution_status_detail})"
40
+ return repr
30
41
 
31
42
  def create(self, validated_data):
32
43
  target_portfolio = validated_data.pop("target_portfolio", None)
@@ -53,7 +64,7 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
53
64
  @wb_serializers.register_only_instance_resource()
54
65
  def additional_resources(self, instance, request, user, **kwargs):
55
66
  res = {}
56
- if instance.status == OrderProposal.Status.APPROVED:
67
+ if instance.status == OrderProposal.Status.APPLIED:
57
68
  res["replay"] = reverse("wbportfolio:orderproposal-replay", args=[instance.id], request=request)
58
69
  if instance.status == OrderProposal.Status.DRAFT:
59
70
  res["reset"] = reverse("wbportfolio:orderproposal-reset", args=[instance.id], request=request)
@@ -73,15 +84,24 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
73
84
  fields = (
74
85
  "id",
75
86
  "trade_date",
87
+ "portfolio",
88
+ "_portfolio",
76
89
  "total_cash_weight",
77
90
  "comment",
78
91
  "status",
79
92
  "min_order_value",
80
- "portfolio",
81
93
  "_rebalancing_model",
82
94
  "rebalancing_model",
83
95
  "target_portfolio",
84
96
  "_target_portfolio",
97
+ "creator",
98
+ "approver",
99
+ "_creator",
100
+ "_approver",
101
+ "execution_status",
102
+ "execution_status_detail",
103
+ "execution_comment",
104
+ "execution_status_repr",
85
105
  "_additional_resources",
86
106
  )
87
107
 
@@ -42,6 +42,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
42
42
  underlying_instrument_ticker = wb_serializers.CharField(read_only=True)
43
43
  underlying_instrument_refinitiv_identifier_code = wb_serializers.CharField(read_only=True)
44
44
  underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
45
+ underlying_instrument_exchange = wb_serializers.CharField(read_only=True)
45
46
 
46
47
  target_weight = wb_serializers.DecimalField(
47
48
  max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
@@ -83,8 +84,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
83
84
  weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
84
85
  if (target_weight := data.pop("target_weight", None)) is not None:
85
86
  weighting = target_weight - effective_weight
86
- if (target_weight := data.pop("target_weight", None)) is not None:
87
- weighting = target_weight - effective_weight
87
+ data["desired_target_weight"] = target_weight
88
88
  if weighting >= 0:
89
89
  data["order_type"] = "BUY"
90
90
  else:
@@ -124,6 +124,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
124
124
  "underlying_instrument_ticker",
125
125
  "underlying_instrument_refinitiv_identifier_code",
126
126
  "underlying_instrument_instrument_type",
127
+ "underlying_instrument_exchange",
127
128
  "order_type",
128
129
  "comment",
129
130
  "effective_weight",
@@ -138,6 +139,8 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
138
139
  "target_total_value_fx_portfolio",
139
140
  "portfolio_currency",
140
141
  "has_warnings",
142
+ "execution_confirmed",
143
+ "execution_comment",
141
144
  )
142
145
 
143
146
 
@@ -11,8 +11,8 @@ class AggregatedAssetPositionModelSerializer(wb_serializers.ModelSerializer):
11
11
  sum_total_value = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=4)
12
12
  weighting = wb_serializers.DecimalField(
13
13
  read_only=True,
14
- max_digits=6,
15
- decimal_places=4,
14
+ max_digits=9,
15
+ decimal_places=8,
16
16
  percent=True,
17
17
  decorators=[{"position": "right", "value": "%"}],
18
18
  )
@@ -49,7 +49,7 @@ class RebalancerModelSerializer(wb_serializers.ModelSerializer):
49
49
  "computed_str",
50
50
  "_rebalancing_model",
51
51
  "rebalancing_model",
52
- "approve_order_proposal_automatically",
52
+ "apply_order_proposal_automatically",
53
53
  "activation_date",
54
54
  "frequency",
55
55
  "frequency_repr",
@@ -118,13 +118,13 @@ class TestOrderProposal:
118
118
  """
119
119
  tp = order_proposal_factory.create()
120
120
  tp_previous_submit = order_proposal_factory.create( # noqa
121
- portfolio=tp.portfolio, status=OrderProposal.Status.SUBMIT, trade_date=(tp.trade_date - BDay(1)).date()
121
+ portfolio=tp.portfolio, status=OrderProposal.Status.PENDING, trade_date=(tp.trade_date - BDay(1)).date()
122
122
  )
123
123
  tp_previous_approve = order_proposal_factory.create(
124
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(2)).date()
124
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date - BDay(2)).date()
125
125
  )
126
126
  tp_next_approve = order_proposal_factory.create( # noqa
127
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(1)).date()
127
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date + BDay(1)).date()
128
128
  )
129
129
 
130
130
  # The previous valid order proposal should be the approved one strictly before the current proposal
@@ -139,13 +139,13 @@ class TestOrderProposal:
139
139
  """
140
140
  tp = order_proposal_factory.create()
141
141
  tp_previous_approve = order_proposal_factory.create( # noqa
142
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
142
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date - BDay(1)).date()
143
143
  )
144
144
  tp_next_submit = order_proposal_factory.create( # noqa
145
- portfolio=tp.portfolio, status=OrderProposal.Status.SUBMIT, trade_date=(tp.trade_date + BDay(1)).date()
145
+ portfolio=tp.portfolio, status=OrderProposal.Status.PENDING, trade_date=(tp.trade_date + BDay(1)).date()
146
146
  )
147
147
  tp_next_approve = order_proposal_factory.create(
148
- portfolio=tp.portfolio, status=OrderProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(2)).date()
148
+ portfolio=tp.portfolio, status=OrderProposal.Status.APPLIED, trade_date=(tp.trade_date + BDay(2)).date()
149
149
  )
150
150
 
151
151
  # The next valid order proposal should be the approved one strictly after the current proposal
@@ -255,11 +255,12 @@ class TestOrderProposal:
255
255
 
256
256
  # Test resetting orders
257
257
  def test_reset_orders(
258
- self, order_proposal, instrument_factory, cash, instrument_price_factory, asset_position_factory
258
+ self, order_proposal, instrument_factory, cash_factory, instrument_price_factory, asset_position_factory
259
259
  ):
260
260
  """
261
261
  Verify orders are correctly reset based on effective and target portfolios.
262
262
  """
263
+ cash = cash_factory.create()
263
264
  effective_date = order_proposal.last_effective_date
264
265
 
265
266
  # Create instruments for testing
@@ -389,15 +390,15 @@ class TestOrderProposal:
389
390
  mock_fct.return_value = iter([])
390
391
 
391
392
  # Create approved order proposals for testing
392
- tp0 = order_proposal_factory.create(status=OrderProposal.Status.APPROVED)
393
+ tp0 = order_proposal_factory.create(status=OrderProposal.Status.APPLIED)
393
394
  tp1 = order_proposal_factory.create(
394
395
  portfolio=tp0.portfolio,
395
- status=OrderProposal.Status.APPROVED,
396
+ status=OrderProposal.Status.APPLIED,
396
397
  trade_date=(tp0.trade_date + BusinessMonthEnd(1)).date(),
397
398
  )
398
399
  tp2 = order_proposal_factory.create(
399
400
  portfolio=tp0.portfolio,
400
- status=OrderProposal.Status.APPROVED,
401
+ status=OrderProposal.Status.APPLIED,
401
402
  trade_date=(tp1.trade_date + BusinessMonthEnd(1)).date(),
402
403
  )
403
404
 
@@ -499,9 +500,10 @@ class TestOrderProposal:
499
500
  instrument.exchange.save()
500
501
  assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
501
502
 
502
- def test_submit_round_lot_size(self, order_proposal, order_factory, instrument):
503
+ def test_submit_round_lot_size(self, order_proposal, order_factory, instrument_factory):
503
504
  order_proposal.portfolio.only_weighting = False
504
505
  order_proposal.portfolio.save()
506
+ instrument = instrument_factory.create()
505
507
  instrument.round_lot_size = 100
506
508
  instrument.save()
507
509
  trade = order_factory.create(
@@ -518,11 +520,10 @@ class TestOrderProposal:
518
520
  trade.refresh_from_db()
519
521
  assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
520
522
 
521
- def test_submit_round_fractional_shares(self, order_proposal, order_factory, instrument):
523
+ def test_submit_round_fractional_shares(self, order_proposal, order_factory):
522
524
  order_proposal.portfolio.only_weighting = False
523
525
  order_proposal.portfolio.save()
524
526
  trade = order_factory.create(
525
- underlying_instrument=instrument,
526
527
  shares=5.6,
527
528
  order_proposal=order_proposal,
528
529
  weighting=Decimal("1.0"),
@@ -640,6 +641,7 @@ class TestOrderProposal:
640
641
  order_proposal.submit()
641
642
  order_proposal.save()
642
643
  order_proposal.approve()
644
+ order_proposal.apply()
643
645
  order_proposal.save()
644
646
 
645
647
  # Final check that weights have been updated to 50%
@@ -651,8 +653,9 @@ class TestOrderProposal:
651
653
  )
652
654
 
653
655
  def test_replay_reset_draft_order_proposal(
654
- self, instrument, instrument_price_factory, order_factory, order_proposal_factory
656
+ self, instrument_factory, instrument_price_factory, order_factory, order_proposal_factory
655
657
  ):
658
+ instrument = instrument_factory.create()
656
659
  order_proposal = order_proposal_factory.create(trade_date=date.today() - BDay(2))
657
660
  instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(2))
658
661
  instrument_price_factory.create(instrument=instrument, date=date.today() - BDay(1))
@@ -663,7 +666,8 @@ class TestOrderProposal:
663
666
  weighting=1,
664
667
  )
665
668
  order_proposal.submit()
666
- order_proposal.approve(replay=False)
669
+ order_proposal.approve()
670
+ order_proposal.apply(replay=False)
667
671
  order_proposal.save()
668
672
 
669
673
  draft_tp = order_proposal_factory.create(portfolio=order_proposal.portfolio, trade_date=date.today() - BDay(1))
@@ -700,6 +704,7 @@ class TestOrderProposal:
700
704
  )
701
705
  order_proposal.submit()
702
706
  order_proposal.approve()
707
+ order_proposal.apply()
703
708
  order_proposal.save()
704
709
 
705
710
  order1.refresh_from_db()
@@ -733,3 +738,12 @@ class TestOrderProposal:
733
738
  assert order_proposal.get_orders().aggregate(s=Sum("target_weight"))["s"] == Decimal(
734
739
  "0.98"
735
740
  ), "The total target weight leftover does not equal the stored total cash weight"
741
+
742
+ def test_convert_to_portfolio_always_100percent(self, order_proposal, order_factory):
743
+ o1 = order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.5"))
744
+ o2 = order_factory.create(order_proposal=order_proposal, weighting=Decimal("0.3"))
745
+
746
+ portfolio = order_proposal.convert_to_portfolio()
747
+ assert portfolio.positions_map[o1.underlying_instrument.id].weighting == Decimal("0.5")
748
+ assert portfolio.positions_map[o2.underlying_instrument.id].weighting == Decimal("0.3")
749
+ assert portfolio.positions_map[order_proposal.cash_component.id].weighting == Decimal("0.2")
@@ -31,16 +31,15 @@ class TestImportMixinModel:
31
31
 
32
32
  trade = trade_factory.build()
33
33
  data = {"data": [serialize(trade)]}
34
- handler = TradeImportHandler(import_source)
35
34
 
36
35
  # Import non existing data
37
- handler.process(data)
36
+ TradeImportHandler(import_source).process(data)
38
37
  assert Trade.objects.count() == 1
39
38
 
40
39
  # Import already existing data
41
40
  # import_source.data['data'][0]['shares'] *= 2
42
41
 
43
- handler.process(data)
42
+ TradeImportHandler(import_source).process(data)
44
43
  assert Trade.objects.count() == 1
45
44
 
46
45
  def test_import_price(self, import_source, product, instrument_price_factory, instrument):
@@ -137,7 +136,9 @@ class TestImportMixinModel:
137
136
  data = {
138
137
  "data": [
139
138
  self._serialize_position(
140
- asset_position_factory.build(date=val_date, underlying_instrument=instrument),
139
+ asset_position_factory.build(
140
+ date=val_date, underlying_instrument=instrument, weighting=Decimal("0.25")
141
+ ),
141
142
  product_portfolio,
142
143
  instrument,
143
144
  )
@@ -171,7 +172,7 @@ class TestImportMixinModel:
171
172
  def test_import_assetposition_product_group(
172
173
  self, import_source, product_group, currency, equity, asset_position_factory
173
174
  ):
174
- positions = asset_position_factory.build(underlying_instrument=equity)
175
+ positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
175
176
  data = {"data": [self._serialize_position(positions, product_group, equity)]}
176
177
 
177
178
  # Import non existing data
@@ -183,7 +184,7 @@ class TestImportMixinModel:
183
184
  def test_import_assetposition_index(
184
185
  self, import_source, index, portfolio, currency, equity, asset_position_factory
185
186
  ):
186
- positions = asset_position_factory.build(underlying_instrument=equity)
187
+ positions = asset_position_factory.build(underlying_instrument=equity, weighting=Decimal("1"))
187
188
  index.portfolios.add(portfolio)
188
189
  data = {"data": [self._serialize_position(positions, index, equity)]}
189
190