wbportfolio 1.55.9__py2.py3-none-any.whl → 1.56.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 (73) hide show
  1. wbportfolio/api_clients/ubs.py +11 -9
  2. wbportfolio/contrib/company_portfolio/configs/display.py +4 -4
  3. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  4. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  5. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  6. wbportfolio/factories/product_groups.py +3 -3
  7. wbportfolio/factories/products.py +3 -3
  8. wbportfolio/filters/assets.py +0 -1
  9. wbportfolio/filters/positions.py +0 -1
  10. wbportfolio/filters/transactions/fees.py +0 -2
  11. wbportfolio/filters/transactions/trades.py +0 -1
  12. wbportfolio/import_export/handlers/asset_position.py +3 -3
  13. wbportfolio/import_export/handlers/dividend.py +1 -1
  14. wbportfolio/import_export/handlers/fees.py +2 -2
  15. wbportfolio/import_export/handlers/trade.py +4 -4
  16. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  17. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  18. wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
  19. wbportfolio/import_export/parsers/jpmorgan/strategy.py +2 -2
  20. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  21. wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
  22. wbportfolio/import_export/parsers/natixis/equity.py +21 -3
  23. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  24. wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
  25. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  26. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  27. wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
  28. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  29. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  30. wbportfolio/import_export/parsers/ubs/equity.py +2 -1
  31. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  32. wbportfolio/import_export/utils.py +3 -1
  33. wbportfolio/metric/backends/base.py +2 -2
  34. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  35. wbportfolio/models/asset.py +3 -2
  36. wbportfolio/models/builder.py +0 -1
  37. wbportfolio/models/custodians.py +3 -3
  38. wbportfolio/models/exceptions.py +1 -1
  39. wbportfolio/models/graphs/utils.py +11 -11
  40. wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
  41. wbportfolio/models/orders/order_proposals.py +11 -8
  42. wbportfolio/models/orders/orders.py +84 -62
  43. wbportfolio/models/portfolio.py +7 -7
  44. wbportfolio/models/portfolio_relationship.py +6 -0
  45. wbportfolio/models/products.py +3 -0
  46. wbportfolio/models/rebalancing.py +3 -0
  47. wbportfolio/models/roles.py +4 -10
  48. wbportfolio/models/transactions/claim.py +6 -5
  49. wbportfolio/models/transactions/dividends.py +1 -0
  50. wbportfolio/models/transactions/trades.py +1 -0
  51. wbportfolio/models/transactions/transactions.py +16 -4
  52. wbportfolio/pms/typing.py +1 -1
  53. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
  54. wbportfolio/risk_management/backends/controversy_portfolio.py +1 -1
  55. wbportfolio/risk_management/backends/exposure_portfolio.py +2 -2
  56. wbportfolio/risk_management/backends/instrument_list_portfolio.py +1 -1
  57. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  58. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  59. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  60. wbportfolio/serializers/orders/orders.py +56 -23
  61. wbportfolio/serializers/transactions/claim.py +2 -2
  62. wbportfolio/tests/models/orders/test_order_proposals.py +27 -7
  63. wbportfolio/tests/models/test_portfolios.py +5 -5
  64. wbportfolio/tests/models/test_splits.py +1 -6
  65. wbportfolio/tests/viewsets/test_products.py +1 -0
  66. wbportfolio/viewsets/charts/assets.py +8 -4
  67. wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
  68. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  69. wbportfolio/viewsets/orders/orders.py +22 -4
  70. {wbportfolio-1.55.9.dist-info → wbportfolio-1.56.0.dist-info}/METADATA +1 -1
  71. {wbportfolio-1.55.9.dist-info → wbportfolio-1.56.0.dist-info}/RECORD +73 -72
  72. {wbportfolio-1.55.9.dist-info → wbportfolio-1.56.0.dist-info}/WHEEL +0 -0
  73. {wbportfolio-1.55.9.dist-info → wbportfolio-1.56.0.dist-info}/licenses/LICENSE +0 -0
@@ -194,6 +194,7 @@ class Trade(TransactionMixin, ImportMixin, models.Model):
194
194
  # notification_email_template = "portfolio/email/trade_notification.html"
195
195
 
196
196
  def save(self, *args, **kwargs):
197
+ self.pre_save()
197
198
  if abs(self.weighting) < 10e-6:
198
199
  self.weighting = Decimal("0")
199
200
  if not self.price:
@@ -71,6 +71,22 @@ class TransactionMixin(models.Model):
71
71
  ),
72
72
  db_persist=True,
73
73
  )
74
+ price_fx_portfolio = models.GeneratedField(
75
+ expression=models.F("currency_fx_rate") * models.F("price"),
76
+ output_field=models.DecimalField(
77
+ max_digits=20,
78
+ decimal_places=4,
79
+ ),
80
+ db_persist=True,
81
+ )
82
+ price_gross_fx_portfolio = models.GeneratedField(
83
+ expression=models.F("currency_fx_rate") * models.F("price_gross"),
84
+ output_field=models.DecimalField(
85
+ max_digits=20,
86
+ decimal_places=4,
87
+ ),
88
+ db_persist=True,
89
+ )
74
90
  total_value_fx_portfolio = models.GeneratedField(
75
91
  expression=models.F("currency_fx_rate") * models.F("price") * models.F("shares"),
76
92
  output_field=models.DecimalField(
@@ -100,14 +116,10 @@ class TransactionMixin(models.Model):
100
116
  self.price_gross = self.price
101
117
  elif self.price_gross is not None and self.price is None:
102
118
  self.price = self.price_gross
103
-
104
- def save(self, *args, **kwargs):
105
- self.pre_save()
106
119
  if self.currency_fx_rate is None:
107
120
  self.currency_fx_rate = self.underlying_instrument.currency.convert(
108
121
  self.value_date, self.portfolio.currency, exact_lookup=True
109
122
  )
110
- super().save(*args, **kwargs)
111
123
 
112
124
  class Meta:
113
125
  abstract = True
wbportfolio/pms/typing.py CHANGED
@@ -269,7 +269,7 @@ class TradeBatch:
269
269
 
270
270
  def convert_to_portfolio(self, use_effective: bool = False, *extra_positions):
271
271
  positions = []
272
- for instrument, trade in self.trades_map.items():
272
+ for trade in self.trades_map.values():
273
273
  positions.append(
274
274
  Position(
275
275
  underlying_instrument=trade.underlying_instrument,
@@ -108,11 +108,7 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
108
108
  return df.any()
109
109
  else:
110
110
  if missing_exchanges.exists():
111
- setattr(
112
- self,
113
- "_validation_errors",
114
- f"Couldn't find any market capitalization for exchanges {', '.join([str(e) for e in missing_exchanges])}",
115
- )
111
+ self._validation_errors = f"Couldn't find any market capitalization for exchanges {', '.join([str(e) for e in missing_exchanges])}"
116
112
  return df.all()
117
113
  return False
118
114
 
@@ -44,7 +44,7 @@ class RuleBackend(ActivePortfolioRelationshipMixin):
44
44
  return RuleBackendSerializer
45
45
 
46
46
  def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
47
- for instrument_id, weight in portfolio.positions_map.items():
47
+ for instrument_id in portfolio.positions_map.keys():
48
48
  instrument = Instrument.objects.get(id=instrument_id)
49
49
  if (
50
50
  controversies := Controversy.objects.filter(
@@ -180,7 +180,7 @@ class RuleBackend(
180
180
  breached_value = f'<span style="color:green">{breached_value}</span>'
181
181
  yield backend.IncidentResult(
182
182
  breached_object=obj,
183
- breached_object_repr=obj_repr,
183
+ breached_object_repr=str(obj_repr),
184
184
  breached_value=breached_value,
185
185
  report_details=self.report_details,
186
186
  severity=severity,
@@ -218,7 +218,7 @@ class RuleBackend(
218
218
  obj = Instrument.objects.get(id=pivot_object_id)
219
219
  return obj, str(obj)
220
220
  case self.GroupbyChoices.ASSET_TYPE:
221
- return None, InstrumentType.objects.get(id=pivot_object_id)
221
+ return None, InstrumentType.objects.get(id=pivot_object_id).name
222
222
  case self.GroupbyChoices.CASH:
223
223
  return None, "Cash"
224
224
  case self.GroupbyChoices.COUNTRY:
@@ -64,7 +64,7 @@ class RuleBackend(ActivePortfolioRelationshipMixin):
64
64
  return RuleBackendSerializer
65
65
 
66
66
  def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
67
- for instrument_id, weight in portfolio.positions_map.items():
67
+ for instrument_id in portfolio.positions_map.keys():
68
68
  instrument = Instrument.objects.get(id=instrument_id)
69
69
  relationships = self.instruments_relationship.filter(instrument=instrument, validated=True)
70
70
 
@@ -138,5 +138,5 @@ class TestExposurePortfolioRuleModel(PortfolioTestMixin):
138
138
  incidents = list(exposure_portfolio_backend.check_rule())
139
139
  assert len(incidents) == 1
140
140
  incident = incidents[0]
141
- assert incident.breached_object_repr == i1.instrument_type
141
+ assert incident.breached_object_repr == i1.instrument_type.name
142
142
  assert incident.breached_value == '<span style="color:green">+5.00%</span>'
@@ -92,7 +92,7 @@ class TestStopLossInstrumentRuleModel(PortfolioTestMixin):
92
92
  date=weekday, net_value=500, calculated=False, instrument=benchmark
93
93
  )
94
94
  RelatedInstrumentThroughModel.objects.create(instrument=product, related_instrument=benchmark, is_primary=True)
95
- setattr(stop_loss_instrument_backend, "dynamic_benchmark_type", "PRIMARY_BENCHMARK")
95
+ stop_loss_instrument_backend.dynamic_benchmark_type = "PRIMARY_BENCHMARK"
96
96
 
97
97
  res = list(stop_loss_instrument_backend.check_rule())
98
98
  assert len(res) == 0
@@ -105,7 +105,7 @@ class TestStopLossInstrumentRuleModel(PortfolioTestMixin):
105
105
  assert len(res) == 1
106
106
  assert res[0].breached_object.id == product.id
107
107
 
108
- setattr(stop_loss_instrument_backend, "static_benchmark", benchmark)
108
+ stop_loss_instrument_backend.static_benchmark = benchmark
109
109
  res = list(stop_loss_instrument_backend.check_rule())
110
110
  assert len(res) == 1
111
111
  assert res[0].breached_object.id == product.id
@@ -119,7 +119,7 @@ class TestStopLossPortfolioRuleModel(PortfolioTestMixin):
119
119
  res = list(stop_loss_portfolio_backend.check_rule())
120
120
  assert len(res) == 0
121
121
 
122
- setattr(stop_loss_portfolio_backend, "static_benchmark", benchmark)
122
+ stop_loss_portfolio_backend.static_benchmark = benchmark
123
123
  res = list(stop_loss_portfolio_backend.check_rule())
124
124
  assert len(res) == 1
125
125
  assert res[0].breached_object.id == instrument.id
@@ -44,29 +44,26 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
44
44
  underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
45
45
  underlying_instrument_exchange = wb_serializers.CharField(read_only=True)
46
46
 
47
- target_weight = wb_serializers.DecimalField(
47
+ effective_weight = wb_serializers.DecimalField(
48
+ read_only=True,
48
49
  max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
49
50
  decimal_places=Order.ORDER_WEIGHTING_PRECISION,
50
- required=False,
51
51
  default=0,
52
52
  )
53
- effective_weight = wb_serializers.DecimalField(
54
- read_only=True,
53
+ target_weight = wb_serializers.DecimalField(
55
54
  max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
56
55
  decimal_places=Order.ORDER_WEIGHTING_PRECISION,
57
- default=0,
56
+ required=False,
58
57
  )
59
58
 
60
59
  effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
61
- target_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
60
+ target_shares = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=6)
62
61
 
63
- total_value_fx_portfolio = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=2, default=0)
64
62
  effective_total_value_fx_portfolio = wb_serializers.DecimalField(
65
63
  read_only=True, max_digits=16, decimal_places=2, default=0
66
64
  )
67
- target_total_value_fx_portfolio = wb_serializers.DecimalField(
68
- read_only=True, max_digits=16, decimal_places=2, default=0
69
- )
65
+ target_total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
66
+ total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
70
67
 
71
68
  portfolio_currency = wb_serializers.CharField(read_only=True)
72
69
  has_warnings = wb_serializers.BooleanField(read_only=True)
@@ -81,17 +78,57 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
81
78
  }
82
79
  )
83
80
  effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
84
- weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
85
- if (target_weight := data.pop("target_weight", None)) is not None:
86
- weighting = target_weight - effective_weight
87
- data["desired_target_weight"] = target_weight
88
- if weighting >= 0:
89
- data["order_type"] = "BUY"
90
- else:
91
- data["order_type"] = "SELL"
92
- data["weighting"] = weighting
81
+ effective_shares = self.instance._effective_shares if self.instance else Decimal(0.0)
82
+ portfolio_value = (
83
+ self.context["view"].order_proposal.portfolio_total_asset_value if "view" in self.context else Decimal(0.0)
84
+ )
85
+ if (total_value_fx_portfolio := data.pop("total_value_fx_portfolio", None)) is not None and portfolio_value:
86
+ data["weighting"] = total_value_fx_portfolio / portfolio_value
87
+ if (
88
+ target_total_value_fx_portfolio := data.pop("target_total_value_fx_portfolio", None)
89
+ ) is not None and portfolio_value:
90
+ data["target_weight"] = target_total_value_fx_portfolio / portfolio_value
91
+
92
+ if data.get("weighting") or data.get("target_weight"):
93
+ weighting = data.pop("weighting", None)
94
+ if (target_weight := data.pop("target_weight", None)) is not None:
95
+ weighting = target_weight - effective_weight
96
+ data["desired_target_weight"] = target_weight
97
+ if weighting is not None:
98
+ data["weighting"] = weighting
99
+ data.pop("shares", None)
100
+ data.pop("target_shares", None)
101
+
102
+ if data.get("shares") or data.get("target_shares"):
103
+ shares = data.pop("shares", None)
104
+ if (target_shares := data.pop("target_shares", None)) is not None:
105
+ shares = target_shares - effective_shares
106
+ if shares is not None:
107
+ data["shares"] = shares
93
108
  return super().validate(data)
94
109
 
110
+ def update(self, instance, validated_data):
111
+ weighting = validated_data.pop("weighting", None)
112
+ shares = validated_data.pop("shares", None)
113
+ portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
114
+ if weighting is not None:
115
+ instance.set_weighting(weighting, portfolio_total_asset_value)
116
+ if shares is not None:
117
+ instance.set_shares(shares, portfolio_total_asset_value)
118
+ return super().update(instance, validated_data)
119
+
120
+ def create(self, validated_data):
121
+ weighting = validated_data.pop("weighting", None)
122
+ shares = validated_data.pop("shares", None)
123
+ instance = super().create(validated_data)
124
+ portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
125
+ if weighting is not None:
126
+ instance.set_weighting(weighting, portfolio_total_asset_value)
127
+ if shares is not None:
128
+ instance.set_shares(shares, portfolio_total_asset_value)
129
+ instance.save()
130
+ return instance
131
+
95
132
  class Meta:
96
133
  model = Order
97
134
  percent_fields = ["effective_weight", "target_weight", "weighting"]
@@ -108,12 +145,8 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
108
145
  }
109
146
  read_only_fields = (
110
147
  "order_type",
111
- "shares",
112
148
  "effective_shares",
113
- "target_shares",
114
- "total_value_fx_portfolio",
115
149
  "effective_total_value_fx_portfolio",
116
- "target_total_value_fx_portfolio",
117
150
  "has_warnings",
118
151
  )
119
152
  fields = (
@@ -49,8 +49,8 @@ class ClaimAPIModelSerializer(serializers.ModelSerializer):
49
49
  if "isin" in request.data:
50
50
  try:
51
51
  data["product"] = Product.objects.get(isin=request.data["isin"])
52
- except Product.DoesNotExist:
53
- raise ValidationError({"isin": "A product with this ISIN does not exist."})
52
+ except Product.DoesNotExist as e:
53
+ raise ValidationError({"isin": "A product with this ISIN does not exist."}) from e
54
54
  if trade := data.get("trade", None):
55
55
  if not trade.is_claimable:
56
56
  raise ValidationError({"trade": "Only a claimable trade can be selected"})
@@ -500,17 +500,25 @@ class TestOrderProposal:
500
500
  instrument.exchange.save()
501
501
  assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
502
502
 
503
- def test_submit_round_lot_size(self, order_proposal, order_factory, instrument_factory):
503
+ @patch.object(Portfolio, "get_total_asset_value")
504
+ def test_submit_round_lot_size(self, mock_fct, order_proposal, instrument_price_factory, order_factory):
505
+ initial_shares = Decimal("70")
506
+ price = instrument_price_factory.create(date=order_proposal.trade_date)
507
+ net_value = round(price.net_value, 4)
508
+ portfolio_value = initial_shares * net_value
509
+ mock_fct.return_value = portfolio_value
510
+
504
511
  order_proposal.portfolio.only_weighting = False
505
512
  order_proposal.portfolio.save()
506
- instrument = instrument_factory.create()
513
+ instrument = price.instrument
507
514
  instrument.round_lot_size = 100
508
515
  instrument.save()
509
516
  trade = order_factory.create(
510
- underlying_instrument=instrument,
511
- shares=70,
517
+ shares=initial_shares,
512
518
  order_proposal=order_proposal,
513
519
  weighting=Decimal("1.0"),
520
+ underlying_instrument=price.instrument,
521
+ price=net_value,
514
522
  )
515
523
  warnings = order_proposal.submit()
516
524
  order_proposal.save()
@@ -520,18 +528,31 @@ class TestOrderProposal:
520
528
  trade.refresh_from_db()
521
529
  assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
522
530
 
523
- def test_submit_round_fractional_shares(self, order_proposal, order_factory):
531
+ @patch.object(Portfolio, "get_total_asset_value")
532
+ def test_submit_round_fractional_shares(self, mock_fct, instrument_price_factory, order_proposal, order_factory):
533
+ initial_shares = Decimal("5.6")
534
+ price = instrument_price_factory.create(date=order_proposal.trade_date)
535
+ net_value = round(price.net_value, 4)
536
+ portfolio_value = initial_shares * net_value
537
+ mock_fct.return_value = portfolio_value
538
+
524
539
  order_proposal.portfolio.only_weighting = False
525
540
  order_proposal.portfolio.save()
526
541
  trade = order_factory.create(
527
- shares=5.6,
542
+ shares=Decimal("5.6"),
528
543
  order_proposal=order_proposal,
529
544
  weighting=Decimal("1.0"),
545
+ underlying_instrument=price.instrument,
546
+ price=net_value,
530
547
  )
531
548
  order_proposal.submit()
532
549
  order_proposal.save()
533
550
  trade.refresh_from_db()
534
551
  assert trade.shares == 6 # we expect the fractional share to be rounded
552
+ assert trade.weighting == round((trade.shares * net_value) / portfolio_value, 8)
553
+ assert trade.weighting == round(
554
+ Decimal("1") + ((Decimal("6") - initial_shares) * net_value) / portfolio_value, 8
555
+ ) # we expect the weighting to be updated accrodingly
535
556
 
536
557
  def test_ex_post(
537
558
  self, instrument_factory, asset_position_factory, instrument_price_factory, order_proposal_factory, portfolio
@@ -719,7 +740,6 @@ class TestOrderProposal:
719
740
  order_proposal.approve()
720
741
  order_proposal.apply()
721
742
  order_proposal.save()
722
-
723
743
  assert order_proposal.portfolio.assets.get(
724
744
  date=order_proposal.trade_date, underlying_quote=o1.underlying_instrument
725
745
  ).weighting == Decimal("0.8")
@@ -924,13 +924,13 @@ class TestPortfolioModel(PortfolioTestMixin):
924
924
 
925
925
  analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
926
926
  assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
927
- expected_X = pd.DataFrame(
927
+ expected_x = pd.DataFrame(
928
928
  [[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
929
929
  columns=[i1.id, i2.id],
930
930
  index=[(weekday + BDay(1)).date()],
931
931
  )
932
- expected_X.index = pd.to_datetime(expected_X.index)
933
- pd.testing.assert_frame_equal(analytic_portfolio.X, expected_X, check_names=False, check_freq=False)
932
+ expected_x.index = pd.to_datetime(expected_x.index)
933
+ pd.testing.assert_frame_equal(analytic_portfolio.X, expected_x, check_names=False, check_freq=False)
934
934
 
935
935
  def test_get_total_asset_value(self, weekday, portfolio, asset_position_factory):
936
936
  a1 = asset_position_factory.create(date=weekday, portfolio=portfolio)
@@ -1010,7 +1010,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1010
1010
  assert len(res) == 1
1011
1011
  assert a.date == weekday
1012
1012
  assert a.underlying_quote == instrument
1013
- assert a.underlying_quote_price == None
1013
+ assert a.underlying_quote_price is None
1014
1014
  assert a.initial_price == p.net_value
1015
1015
  assert a.weighting == pytest.approx(weights[instrument.id], abs=Decimal(10e-6))
1016
1016
  assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
@@ -1053,7 +1053,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1053
1053
  assert next(gen)[0] == middle_date, "Drifting weight with a non automatic rebalancer stops the iteration"
1054
1054
  try:
1055
1055
  next(gen)
1056
- assert False, "the next iteration should stop and return the rebalancing"
1056
+ raise AssertionError("the next iteration should stop and return the rebalancing")
1057
1057
  except StopIteration as e:
1058
1058
  rebalancing_order_proposal = e.value
1059
1059
  assert rebalancing_order_proposal.trade_date == rebalancing_date
@@ -14,10 +14,6 @@ fake = Faker()
14
14
 
15
15
  @pytest.mark.django_db
16
16
  class TestAdjustmentModel:
17
- @pytest.fixture()
18
- def applied_adjustment(self):
19
- return AdjustmentFactory.create(status=Adjustment.Status.APPLIED)
20
-
21
17
  @pytest.fixture()
22
18
  def old_adjustment(self):
23
19
  return AdjustmentFactory.create(status=Adjustment.Status.PENDING, date=fake.past_date())
@@ -205,7 +201,7 @@ class TestAdjustmentModel:
205
201
  post_adjustment_on_prices(adjustment.id)
206
202
  a1.refresh_from_db()
207
203
  adjustment.refresh_from_db()
208
- a1.applied_adjustment == adjustment
204
+ assert a1.applied_adjustment == adjustment
209
205
  assert adjustment.status == Adjustment.Status.APPLIED
210
206
 
211
207
  @patch("wbportfolio.models.adjustments.send_notification")
@@ -230,5 +226,4 @@ class TestAdjustmentModel:
230
226
  mock_check_fct.return_value = False
231
227
  post_adjustment_on_prices(adjustment.id)
232
228
  adjustment.refresh_from_db()
233
- mock_delay_fct.call_args[0] == user_porftolio_manager.id
234
229
  assert adjustment.status == Adjustment.Status.PENDING
@@ -23,6 +23,7 @@ class TestProductModelViewSet:
23
23
  portfolio_factory.create_batch(4, invested_timespan=DateRange(date.min, date.max))
24
24
  + portfolio_factory.create_batch(2, invested_timespan=DateRange(date.min, date.max)),
25
25
  product_factory.create_batch(6),
26
+ strict=False,
26
27
  ):
27
28
  InstrumentPortfolioThroughModel.objects.update_or_create(
28
29
  instrument=product, defaults={"portfolio": portfolio}
@@ -101,7 +101,9 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
101
101
  columns = {}
102
102
  level_representations = self.classification_group.get_levels_representation()
103
103
  for key, label in zip(
104
- reversed(self.classification_group.get_fields_names(sep="_")), reversed(level_representations[1:])
104
+ reversed(self.classification_group.get_fields_names(sep="_")),
105
+ reversed(level_representations[1:]),
106
+ strict=False,
105
107
  ):
106
108
  columns[key] = label
107
109
  columns["label"] = "Classification"
@@ -158,6 +160,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
158
160
  return df.reset_index(names="id")
159
161
 
160
162
  def manipulate_dataframe(self, df):
163
+ df["id"] = df["id"].fillna(-1)
161
164
  if self.group_by == AssetPositionGroupBy.INDUSTRY:
162
165
  if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=self.portfolio):
163
166
  df["equity"] = ""
@@ -169,8 +172,8 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
169
172
  dict(Classification.objects.filter(id__in=df["classification"].dropna()).values_list("id", "name"))
170
173
  )
171
174
  elif self.group_by == AssetPositionGroupBy.CASH:
172
- df.loc[df["id"] == True, "label"] = "Cash"
173
- df.loc[df["id"] == False, "label"] = "Non-Cash"
175
+ df.loc[df["id"], "label"] = "Cash"
176
+ df.loc[~df["id"], "label"] = "Non-Cash"
174
177
  elif self.group_by == AssetPositionGroupBy.COUNTRY:
175
178
  df["label"] = df["id"].map(dict(Geography.objects.filter(id__in=df["id"]).values_list("id", "name")))
176
179
  elif self.group_by == AssetPositionGroupBy.CURRENCY:
@@ -180,6 +183,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
180
183
  df["label"] = df["id"].map(
181
184
  dict(InstrumentType.objects.filter(id__in=df["id"]).values_list("id", "short_name"))
182
185
  )
186
+ df.loc[df["id"] == -1, "label"] = "N/A"
183
187
  df.sort_values(by="weighting", ascending=False, inplace=True)
184
188
  return df
185
189
 
@@ -307,7 +311,7 @@ class ContributorPortfolioChartView(UserPortfolioRequestPermissionMixin, viewset
307
311
 
308
312
  text_forex = df_forex.contribution_forex.apply(lambda x: f"{x:,.2%}")
309
313
  text_equity = contribution_equity.apply(lambda x: f"{x:,.2%}")
310
- setattr(self, "nb_rows", df.shape[0])
314
+ self.nb_rows = df.shape[0]
311
315
  fig.add_trace(
312
316
  go.Bar(
313
317
  y=df.instrument_id,
@@ -7,7 +7,7 @@ from wbfdm.models.instruments import Instrument
7
7
 
8
8
  class InstrumentButtonMixin:
9
9
  @classmethod
10
- def add_instrument_request_button(self, request=None, view=None, pk=None, **kwargs):
10
+ def add_instrument_request_button(cls, request=None, view=None, pk=None, **kwargs):
11
11
  buttons = [
12
12
  bt.WidgetButton(key="assets", label="Implemented Portfolios (Assets)"),
13
13
  # bt.WidgetButton(
@@ -44,7 +44,7 @@ class InstrumentButtonMixin:
44
44
  )
45
45
 
46
46
  @classmethod
47
- def add_transactions_request_button(self, request=None, view=None, pk=None, **kwargs):
47
+ def add_transactions_request_button(cls, request=None, view=None, pk=None, **kwargs):
48
48
  return bt.DropDownButton(
49
49
  label="Transactions",
50
50
  icon=WBIcon.UNFOLD.icon,
@@ -49,14 +49,14 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
49
49
  ]
50
50
  editable = [dp.FormattingRule(style={"fontWeight": "bold"})]
51
51
  equal = [dp.FormattingRule(condition=("==", False, "is_equal"), style={"backgroundColor": "orange"})]
52
- borderLeft = [
52
+ border_left = [
53
53
  dp.FormattingRule(
54
54
  style={
55
55
  "borderLeft": "1px solid #bdc3c7",
56
56
  }
57
57
  )
58
58
  ]
59
- borderRight = [
59
+ border_right = [
60
60
  dp.FormattingRule(
61
61
  style={
62
62
  "borderRight": "1px solid #bdc3c7",
@@ -111,7 +111,7 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
111
111
  key="shares_external",
112
112
  label="Shares",
113
113
  width=90,
114
- formatting_rules=[*equal, *editable, *borderLeft],
114
+ formatting_rules=[*equal, *editable, *border_left],
115
115
  ),
116
116
  dp.Field(
117
117
  key="nominal_value_external",
@@ -128,7 +128,7 @@ class AccountReconciliationLineDisplayViewConfig(DisplayViewConfig):
128
128
  key="assets_under_management_external",
129
129
  label="AuM",
130
130
  width=120,
131
- formatting_rules=[*equal, *borderRight],
131
+ formatting_rules=[*equal, *border_right],
132
132
  ),
133
133
  ],
134
134
  ),
@@ -173,9 +173,27 @@ class OrderOrderProposalModelViewSet(
173
173
  def get_serializer_class(self):
174
174
  if self.order_proposal.status != OrderProposal.Status.DRAFT:
175
175
  return ReadOnlyOrderOrderProposalModelSerializer
176
- elif not self.new_mode and "pk" not in self.kwargs:
177
- return OrderOrderProposalListModelSerializer
178
- return OrderOrderProposalModelSerializer
176
+ if not self.new_mode and "pk" not in self.kwargs:
177
+ serializer_base_class = OrderOrderProposalListModelSerializer
178
+ else:
179
+ serializer_base_class = OrderOrderProposalModelSerializer
180
+ if not self.order_proposal.portfolio_total_asset_value:
181
+
182
+ class OnlyWeightSerializerClass(serializer_base_class):
183
+ class Meta(serializer_base_class.Meta):
184
+ read_only_fields = (
185
+ "order_type",
186
+ "effective_shares",
187
+ "effective_total_value_fx_portfolio",
188
+ "has_warnings",
189
+ "shares",
190
+ "target_shares",
191
+ "total_value_fx_portfolio",
192
+ "target_total_value_fx_portfolio",
193
+ )
194
+
195
+ return OnlyWeightSerializerClass
196
+ return serializer_base_class
179
197
 
180
198
  def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
181
199
  if self.orders.exists() and self.order_proposal.status in [
@@ -214,7 +232,7 @@ class OrderOrderProposalModelViewSet(
214
232
  default=F("underlying_instrument__instrument_type__short_name"),
215
233
  ),
216
234
  underlying_instrument_exchange=F("underlying_instrument__exchange__name"),
217
- effective_total_value_fx_portfolio=F("previous_weight") * Value(self.portfolio_total_asset_value),
235
+ effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
218
236
  target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
219
237
  portfolio_currency=F("portfolio__currency__symbol"),
220
238
  security=F("underlying_instrument__parent"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.55.9
3
+ Version: 1.56.0
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*