wbportfolio 1.55.10rc0__py2.py3-none-any.whl → 1.56.1__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 (26) hide show
  1. wbportfolio/filters/assets.py +0 -1
  2. wbportfolio/filters/positions.py +0 -1
  3. wbportfolio/filters/transactions/fees.py +0 -2
  4. wbportfolio/filters/transactions/trades.py +0 -1
  5. wbportfolio/import_export/handlers/trade.py +1 -1
  6. wbportfolio/import_export/parsers/natixis/equity.py +21 -3
  7. wbportfolio/import_export/parsers/natixis/utils.py +13 -23
  8. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  9. wbportfolio/models/asset.py +1 -1
  10. wbportfolio/models/builder.py +0 -1
  11. wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
  12. wbportfolio/models/orders/order_proposals.py +9 -6
  13. wbportfolio/models/orders/orders.py +84 -62
  14. wbportfolio/models/transactions/dividends.py +1 -0
  15. wbportfolio/models/transactions/trades.py +1 -0
  16. wbportfolio/models/transactions/transactions.py +16 -4
  17. wbportfolio/risk_management/backends/exposure_portfolio.py +2 -2
  18. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  19. wbportfolio/serializers/orders/orders.py +56 -23
  20. wbportfolio/tests/models/orders/test_order_proposals.py +27 -7
  21. wbportfolio/viewsets/charts/assets.py +2 -0
  22. wbportfolio/viewsets/orders/orders.py +22 -4
  23. {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.1.dist-info}/METADATA +1 -1
  24. {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.1.dist-info}/RECORD +26 -25
  25. {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.1.dist-info}/WHEEL +0 -0
  26. {wbportfolio-1.55.10rc0.dist-info → wbportfolio-1.56.1.dist-info}/licenses/LICENSE +0 -0
@@ -456,7 +456,6 @@ def get_default_hedged_currency(field, request, view):
456
456
 
457
457
  class ContributionChartFilter(wb_filters.FilterSet):
458
458
  date = wb_filters.FinancialPerformanceDateRangeFilter(
459
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
460
459
  label="Date Range",
461
460
  required=True,
462
461
  clearable=False,
@@ -76,7 +76,6 @@ class AssetPositionPandasFilter(DateFilterMixin, PandasFilterSetMixin, wb_filter
76
76
  date = total_value_fx_usd = total_value_fx_usd__gte = total_value_fx_usd__lte = None
77
77
 
78
78
  date = wb_filters.FinancialPerformanceDateRangeFilter(
79
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
80
79
  label="Date Range",
81
80
  required=True,
82
81
  clearable=False,
@@ -16,7 +16,6 @@ class FeesFilter(wb_filters.FilterSet):
16
16
  """
17
17
 
18
18
  fee_date = wb_filters.DateRangeFilter(
19
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
20
19
  label="Date Range",
21
20
  initial=get_transaction_default_date_range,
22
21
  required=True,
@@ -52,7 +51,6 @@ class FeesProductFilterSet(FeesFilter):
52
51
  class FeesAggregatedFilter(PandasFilterSetMixin, wb_filters.FilterSet):
53
52
  fee_date = wb_filters.DateRangeFilter(
54
53
  label="Date Range",
55
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
56
54
  required=True,
57
55
  clearable=False,
58
56
  initial=current_quarter_date_range,
@@ -14,7 +14,6 @@ from .utils import get_transaction_default_date_range
14
14
 
15
15
  class TradeFilter(OppositeSharesFieldMethodMixin, wb_filters.FilterSet):
16
16
  transaction_date = wb_filters.DateRangeFilter(
17
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
18
17
  label="Date Range",
19
18
  initial=get_transaction_default_date_range,
20
19
  )
@@ -86,7 +86,7 @@ class TradeImportHandler(ImportExportHandler):
86
86
  data["transaction_date"] = data["book_date"]
87
87
  return self.model.objects.create(**data, import_source=self.import_source)
88
88
 
89
- def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
89
+ def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model: # noqa: C901
90
90
  self.import_source.log += "\nGet Trade Instance."
91
91
  if transaction_date := data.get("transaction_date"):
92
92
  dates_lookup = {"transaction_date": transaction_date}
@@ -1,3 +1,5 @@
1
+ from zipfile import BadZipFile
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
@@ -29,13 +31,26 @@ def _apply_adjusting_factor(row):
29
31
  def parse(import_source):
30
32
  # Parse the Parts of the filename into the different parts
31
33
  parts = file_name_parse_isin(import_source.file.name)
32
-
33
34
  # Get the valuation date and investment from the parts list
34
35
  valuation_date = parts["valuation_date"]
35
36
  product_data = parts["product"]
36
37
 
37
38
  # Load file into a CSV DictReader
38
- df = pd.read_csv(import_source.file, encoding="utf-16", delimiter=";")
39
+ if import_source.file.name.lower().endswith(".csv"):
40
+ df = pd.read_csv(import_source.file, encoding="utf-16", delimiter=";")
41
+ else:
42
+ try:
43
+ df = pd.read_excel(import_source.file, engine="openpyxl", sheet_name="Basket Valuation")
44
+ except BadZipFile:
45
+ df = pd.read_excel(import_source.file, engine="xlrd", sheet_name="Basket Valuation")
46
+ xx, yy = np.where(df == "Ticker")
47
+ if not xx and not yy:
48
+ xx, yy = np.where(df == "Code")
49
+ df = df.iloc[xx[0] :, yy[0] :]
50
+ df = df.rename(columns=df.iloc[0]).drop(df.index[0]).dropna(how="all")
51
+ df["Quotity/Adj. factor"] = 1.0
52
+ df = df.rename(columns={"Code": "Ticker"})
53
+
39
54
  df = df.rename(columns=FIELD_MAP)
40
55
  df = df.dropna(subset=["initial_price"])
41
56
  df["initial_price"] = df["initial_price"].astype("str").str.replace(" ", "").astype("float")
@@ -50,7 +65,10 @@ def parse(import_source):
50
65
  df = df.drop(columns=df.columns.difference(FIELD_MAP.values()))
51
66
 
52
67
  df["portfolio__instrument_type"] = "product"
53
- df["portfolio__isin"] = product_data["isin"]
68
+ if "isin" in product_data:
69
+ df["portfolio__isin"] = product_data["isin"]
70
+ if "ticker" in product_data:
71
+ df["portfolio__ticker"] = product_data["ticker"]
54
72
  df["is_estimated"] = False
55
73
  df["date"] = valuation_date.strftime("%Y-%m-%d")
56
74
  df["asset_valuation_date"] = pd.to_datetime(df["asset_valuation_date"], dayfirst=True).dt.strftime("%Y-%m-%d")
@@ -1,6 +1,7 @@
1
- import datetime
2
1
  import re
3
2
 
3
+ from django.utils.dateparse import parse_date
4
+
4
5
  from wbportfolio.models import Product
5
6
 
6
7
  INSTRUMENT_MAP_NAME = {"EDA23_AtonRa Z class": "LU2170995018"}
@@ -53,25 +54,14 @@ def _get_underlying_instrument(bbg_code, name, currency, instrument_type="equity
53
54
 
54
55
 
55
56
  def file_name_parse_isin(file_name):
56
- dates = re.findall(r"_([0-9]{4}-?[0-9]{2}-?[0-9]{2})", file_name)
57
- identifier = re.findall(r"([A-Z]{2}(?![A-Z]{10}\b)[A-Z0-9]{10})_", file_name)
58
-
59
- if len(dates) != 2:
60
- raise ValueError("Not 2 dates found in the filename")
61
-
62
- if len(identifier) != 1:
63
- raise ValueError("Not exactly 1 identifier found in the filename")
64
- try:
65
- valuation_date = datetime.datetime.strptime(dates[0], "%Y-%m-%d").date()
66
- except ValueError:
67
- valuation_date = datetime.datetime.strptime(dates[0], "%Y%m%d").date()
68
- try:
69
- generation_date = datetime.datetime.strptime(dates[1], "%Y-%m-%d").date()
70
- except ValueError:
71
- generation_date = datetime.datetime.strptime(dates[1], "%Y%m%d").date()
72
-
73
- return {
74
- "product": {"isin": identifier[0]},
75
- "valuation_date": valuation_date,
76
- "generation_date": generation_date,
77
- }
57
+ dates = re.findall(r"_([0-9]{4}[-_]?[0-9]{2}[-_]?[0-9]{2})", file_name)
58
+ isin = re.findall(r"([A-Z]{2}(?![A-Z]{10}\b)[A-Z0-9]{10})_", file_name)
59
+ ticker = re.findall(r"(NX[A-Z]*)_", file_name)
60
+ if len(dates) == 0:
61
+ raise ValueError("Not dates found in the filename")
62
+ res = {"valuation_date": parse_date(dates[0].replace("_", "-"))}
63
+ if len(isin) >= 1:
64
+ res["product"] = {"isin": isin[0]}
65
+ elif len(ticker) == 1:
66
+ res["product"] = {"ticker": ticker[0]}
67
+ return res
@@ -0,0 +1,44 @@
1
+ # Generated by Django 5.0.12 on 2025-09-18 08:48
2
+
3
+ import django.db.models.expressions
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('wbportfolio', '0089_orderproposal_min_weighting'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='dividendtransaction',
16
+ name='price_fx_portfolio',
17
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
18
+ ),
19
+ migrations.AddField(
20
+ model_name='dividendtransaction',
21
+ name='price_gross_fx_portfolio',
22
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
23
+ ),
24
+ migrations.AddField(
25
+ model_name='order',
26
+ name='price_fx_portfolio',
27
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
28
+ ),
29
+ migrations.AddField(
30
+ model_name='order',
31
+ name='price_gross_fx_portfolio',
32
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
33
+ ),
34
+ migrations.AddField(
35
+ model_name='trade',
36
+ name='price_fx_portfolio',
37
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
38
+ ),
39
+ migrations.AddField(
40
+ model_name='trade',
41
+ name='price_gross_fx_portfolio',
42
+ field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
43
+ ),
44
+ ]
@@ -409,7 +409,7 @@ class AssetPosition(ImportMixin, models.Model):
409
409
  analytical_objects = AnalyticalAssetPositionManager()
410
410
  unannotated_objects = models.Manager()
411
411
 
412
- def pre_save(
412
+ def pre_save( # noqa: C901
413
413
  self, create_underlying_quote_price_if_missing: bool = False, infer_underlying_quote_price: bool = True
414
414
  ):
415
415
  if not self.asset_valuation_date:
@@ -225,7 +225,6 @@ class AssetPositionBuilder:
225
225
 
226
226
  def bulk_create_positions(self, delete_leftovers: bool = False, force_save: bool = False, **kwargs):
227
227
  positions = list(self.get_positions(**kwargs))
228
-
229
228
  # we need to delete the existing estimated portfolio because otherwise we risk to have existing and not
230
229
  # overlapping positions remaining (as they will not be updating by the bulk create). E.g. when someone
231
230
  # change completely the trades of a portfolio model and drift it.
@@ -834,7 +834,7 @@ class LiquidityStressMixin:
834
834
 
835
835
  """ The main function for the liquidity stress tests """
836
836
 
837
- def liquidity_stress_test(
837
+ def liquidity_stress_test( # noqa: C901
838
838
  self,
839
839
  report_date: Optional[date] = None,
840
840
  weights_date: Optional[date] = None,
@@ -527,10 +527,11 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
527
527
  total_target_weight=self.total_expected_target_weight,
528
528
  )
529
529
  leftovers_orders = self.orders.all()
530
+ portfolio_value = self.portfolio_total_asset_value
530
531
  for underlying_instrument_id, order_dto in service.trades_batch.trades_map.items():
531
532
  with suppress(Order.DoesNotExist):
532
533
  order = self.orders.get(underlying_instrument_id=underlying_instrument_id)
533
- order.weighting = round(order_dto.delta_weight, Order.ORDER_WEIGHTING_PRECISION)
534
+ order.set_weighting(round(order_dto.delta_weight, Order.ORDER_WEIGHTING_PRECISION), portfolio_value)
534
535
  order.save()
535
536
  leftovers_orders = leftovers_orders.exclude(id=order.id)
536
537
  leftovers_orders.delete()
@@ -539,11 +540,12 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
539
540
  def fix_quantization(self):
540
541
  if self.orders.exists():
541
542
  orders = self.get_orders()
543
+ portfolio_value = self.portfolio_total_asset_value
542
544
  t_weight = orders.aggregate(models.Sum("target_weight"))["target_weight__sum"] or Decimal("0.0")
543
545
  # we handle quantization error due to the decimal max digits. In that case, we take the biggest order (highest weight) and we remove the quantization error
544
546
  if quantize_error := (t_weight - self.total_expected_target_weight):
545
547
  biggest_order = orders.exclude(underlying_instrument__is_cash=True).latest("target_weight")
546
- biggest_order.weighting -= quantize_error
548
+ biggest_order.set_weighting(biggest_order.weighting - quantize_error, portfolio_value)
547
549
  biggest_order.save()
548
550
 
549
551
  def _get_default_target_portfolio(self, use_desired_target_weight: bool = False, **kwargs) -> PortfolioDTO:
@@ -594,6 +596,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
594
596
  orders = service.trades_batch.trades_map.values()
595
597
 
596
598
  objs = []
599
+ portfolio_value = self.portfolio_total_asset_value
597
600
  for order_dto in orders:
598
601
  instrument = Instrument.objects.get(id=order_dto.underlying_instrument)
599
602
  currency_fx_rate = instrument.currency.convert(
@@ -604,7 +607,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
604
607
  daily_return = order_dto.daily_return
605
608
  try:
606
609
  order = self.orders.get(underlying_instrument=instrument)
607
- order.weighting = weighting
608
610
  order.currency_fx_rate = currency_fx_rate
609
611
  order.daily_return = daily_return
610
612
  except Order.DoesNotExist:
@@ -616,11 +618,12 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
616
618
  daily_return=daily_return,
617
619
  currency_fx_rate=currency_fx_rate,
618
620
  )
621
+ order.pre_save()
622
+ order.set_weighting(weighting, portfolio_value)
619
623
  order.desired_target_weight = order_dto.target_weight
620
624
  order.order_type = Order.get_type(
621
625
  weighting, round(order_dto.previous_weight, 8), round(order_dto.target_weight, 8)
622
626
  )
623
- order.pre_save()
624
627
  # # if we cannot automatically find a price, we consider the stock is invalid and we sell it
625
628
  # if not order.price and order.weighting > 0:
626
629
  # order.price = Decimal("0.0")
@@ -638,7 +641,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
638
641
  "price",
639
642
  "price_gross",
640
643
  "desired_target_weight",
641
- # "shares"
644
+ "shares",
642
645
  ],
643
646
  unique_fields=["order_proposal", "underlying_instrument"],
644
647
  update_conflicts=True,
@@ -790,7 +793,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
790
793
  price_fx_portfolio = quote_price * underlying_quote.currency.convert(
791
794
  self.trade_date, self.portfolio.currency, exact_lookup=False
792
795
  )
793
-
794
796
  # If the price is valid, calculate and return the estimated shares
795
797
  if price_fx_portfolio:
796
798
  return trade_total_value_fx_portfolio / price_fx_portfolio
@@ -888,6 +890,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
888
890
  order_warnings = order.submit(
889
891
  by=by, description=description, portfolio_total_asset_value=self.portfolio_total_asset_value, **kwargs
890
892
  )
893
+
891
894
  if order_warnings:
892
895
  orders_validation_warnings.extend(order_warnings)
893
896
  orders.append(order)
@@ -71,44 +71,25 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
71
71
  execution_confirmed = models.BooleanField(default=False, verbose_name="Execution Confirmed")
72
72
  execution_comment = models.TextField(default="", blank=True, verbose_name="Execution Comment")
73
73
 
74
- def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
75
- warnings = []
74
+ order_with_respect_to = "order_proposal"
76
75
 
77
- # if shares is defined and the underlying instrument defines a round lot size different than 1 and exchange allows its application, we round the share accordingly
78
- if self.order_proposal and not self.portfolio.only_weighting:
79
- shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
80
- if shares != self.shares:
81
- warnings.append(
82
- f"{self.underlying_instrument.computed_str} has a round lot size of {self.underlying_instrument.round_lot_size}: shares were rounded from {self.shares} to {shares}"
83
- )
84
- shares = round(shares) # ensure fractional shares are converted into integer
85
- # we need to recompute the delta weight has we changed the number of shares
86
- if shares != self.shares:
87
- self.shares = shares
88
- if portfolio_total_asset_value:
89
- self.weighting = self.shares * self.price * self.currency_fx_rate / portfolio_total_asset_value
90
- if abs(self.weighting) < self.order_proposal.min_weighting:
91
- warnings.append(
92
- f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
93
- )
94
- self.shares = Decimal("0")
95
- self.weighting = Decimal("0")
96
- if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
97
- warnings.append(
98
- f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
99
- )
100
- self.shares = Decimal("0")
101
- self.weighting = Decimal("0")
102
- if not self.price:
103
- warnings.append(f"No price for {self.underlying_instrument.computed_str}")
104
- if (
105
- not self.underlying_instrument.is_cash
106
- and not self.underlying_instrument.is_cash_equivalent
107
- and self._target_weight < -1e-8
108
- ): # any value below -1e8 will be considered zero
109
- warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
110
- self.desired_target_weight = self._target_weight
111
- return warnings
76
+ class Meta(OrderedModel.Meta):
77
+ verbose_name = "Order"
78
+ verbose_name_plural = "Orders"
79
+ indexes = [
80
+ models.Index(fields=["order_proposal"]),
81
+ models.Index(fields=["underlying_instrument", "value_date"]),
82
+ models.Index(fields=["portfolio", "underlying_instrument", "value_date"]),
83
+ models.Index(fields=["order_proposal", "underlying_instrument"]),
84
+ # models.Index(fields=["date", "underlying_instrument"]),
85
+ ]
86
+ constraints = [
87
+ models.UniqueConstraint(
88
+ fields=["order_proposal", "underlying_instrument"],
89
+ name="unique_order",
90
+ ),
91
+ ]
92
+ # notification_email_template = "portfolio/email/trade_notification.html"
112
93
 
113
94
  @property
114
95
  def product(self):
@@ -176,25 +157,9 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
176
157
  def _target_shares(self) -> Decimal:
177
158
  return getattr(self, "target_shares", self._effective_shares + self.shares)
178
159
 
179
- order_with_respect_to = "order_proposal"
180
-
181
- class Meta(OrderedModel.Meta):
182
- verbose_name = "Order"
183
- verbose_name_plural = "Orders"
184
- indexes = [
185
- models.Index(fields=["order_proposal"]),
186
- models.Index(fields=["underlying_instrument", "value_date"]),
187
- models.Index(fields=["portfolio", "underlying_instrument", "value_date"]),
188
- models.Index(fields=["order_proposal", "underlying_instrument"]),
189
- # models.Index(fields=["date", "underlying_instrument"]),
190
- ]
191
- constraints = [
192
- models.UniqueConstraint(
193
- fields=["order_proposal", "underlying_instrument"],
194
- name="unique_order",
195
- ),
196
- ]
197
- # notification_email_template = "portfolio/email/trade_notification.html"
160
+ def __str__(self):
161
+ ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
162
+ return f"{ticker}{self.weighting}"
198
163
 
199
164
  def pre_save(self):
200
165
  self.portfolio = self.order_proposal.portfolio
@@ -240,15 +205,72 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
240
205
  else:
241
206
  return Order.Type.SELL
242
207
 
243
- def set_type(self):
244
- self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
245
-
246
208
  def get_price(self) -> Decimal:
247
209
  try:
248
210
  return self.underlying_instrument.get_price(self.value_date)
249
211
  except ValueError:
250
212
  return Decimal("0")
251
213
 
252
- def __str__(self):
253
- ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
254
- return f"{ticker}{self.weighting}"
214
+ def set_type(self):
215
+ self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
216
+
217
+ def set_weighting(self, weighting: Decimal, portfolio_value: Decimal):
218
+ self.weighting = weighting
219
+ price_fx_portfolio = self.price * self.currency_fx_rate
220
+ if price_fx_portfolio and portfolio_value:
221
+ total_value = self.weighting * portfolio_value
222
+ self.shares = total_value / price_fx_portfolio
223
+ else:
224
+ self.shares = Decimal("0")
225
+
226
+ def set_shares(self, shares: Decimal, portfolio_value: Decimal):
227
+ if portfolio_value:
228
+ price_fx_portfolio = self.price * self.currency_fx_rate
229
+ self.shares = shares
230
+ total_value = shares * price_fx_portfolio
231
+ self.weighting = total_value / portfolio_value
232
+ else:
233
+ self.weighting = self.shares = Decimal("0")
234
+
235
+ def set_total_value_fx_portfolio(self, total_value_fx_portfolio: Decimal, portfolio_value: Decimal):
236
+ price_fx_portfolio = self.price * self.currency_fx_rate
237
+ if price_fx_portfolio and portfolio_value:
238
+ self.shares = total_value_fx_portfolio / price_fx_portfolio
239
+ self.weighting = total_value_fx_portfolio / portfolio_value
240
+ else:
241
+ self.weighting = self.shares = Decimal("0")
242
+
243
+ def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
244
+ warnings = []
245
+
246
+ # if shares is defined and the underlying instrument defines a round lot size different than 1 and exchange allows its application, we round the share accordingly
247
+ if self.order_proposal and not self.portfolio.only_weighting:
248
+ shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
249
+ if shares != self.shares:
250
+ warnings.append(
251
+ f"{self.underlying_instrument.computed_str} has a round lot size of {self.underlying_instrument.round_lot_size}: shares were rounded from {self.shares} to {shares}"
252
+ )
253
+ shares = round(shares) # ensure fractional shares are converted into integer
254
+ # we need to recompute the delta weight has we changed the number of shares
255
+ if shares != self.shares:
256
+ self.set_shares(shares, portfolio_total_asset_value)
257
+ if abs(self.weighting) < self.order_proposal.min_weighting:
258
+ warnings.append(
259
+ f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
260
+ )
261
+ self.set_weighting(Decimal("0"), portfolio_total_asset_value)
262
+ if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
263
+ warnings.append(
264
+ f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
265
+ )
266
+ self.set_weighting(Decimal("0"), portfolio_total_asset_value)
267
+ if not self.price:
268
+ warnings.append(f"No price for {self.underlying_instrument.computed_str}")
269
+ if (
270
+ not self.underlying_instrument.is_cash
271
+ and not self.underlying_instrument.is_cash_equivalent
272
+ and self._target_weight < -1e-8
273
+ ): # any value below -1e8 will be considered zero
274
+ warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
275
+ self.desired_target_weight = self._target_weight
276
+ return warnings
@@ -41,6 +41,7 @@ class DividendTransaction(TransactionMixin, ImportMixin, models.Model):
41
41
  )
42
42
 
43
43
  def save(self, *args, **kwargs):
44
+ self.pre_save()
44
45
  if not self.record_date and self.ex_date:
45
46
  self.record_date = self.ex_date
46
47
  elif self.record_date and not self.ex_date:
@@ -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
@@ -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:
@@ -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>'
@@ -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 = (
@@ -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")
@@ -160,6 +160,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
160
160
  return df.reset_index(names="id")
161
161
 
162
162
  def manipulate_dataframe(self, df):
163
+ df["id"] = df["id"].fillna(-1)
163
164
  if self.group_by == AssetPositionGroupBy.INDUSTRY:
164
165
  if not PortfolioRole.is_analyst(self.request.user.profile, portfolio=self.portfolio):
165
166
  df["equity"] = ""
@@ -182,6 +183,7 @@ class AbstractDistributionMixin(UserPortfolioRequestPermissionMixin):
182
183
  df["label"] = df["id"].map(
183
184
  dict(InstrumentType.objects.filter(id__in=df["id"]).values_list("id", "short_name"))
184
185
  )
186
+ df.loc[df["id"] == -1, "label"] = "N/A"
185
187
  df.sort_values(by="weighting", ascending=False, inplace=True)
186
188
  return df
187
189
 
@@ -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.10rc0
3
+ Version: 1.56.1
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -80,13 +80,13 @@ wbportfolio/factories/orders/__init__.py,sha256=QG7on_F9GiuJgvGbM0BNDVdfGwwBz_WG
80
80
  wbportfolio/factories/orders/order_proposals.py,sha256=3QGG6eI2sWYGJenn17gz37A2PJgjZLlPqAsqci1lFXI,702
81
81
  wbportfolio/factories/orders/orders.py,sha256=tTw2U-xvUsFpG4zad3KCHZPQx1FVg-zPjwEN73XEOjg,907
82
82
  wbportfolio/filters/__init__.py,sha256=m44CdyYMtku8rMGyiX8KSmMdvyqaElKW4UMkEqOC1VM,1374
83
- wbportfolio/filters/assets.py,sha256=rvBNtQ8OcvYXSmaVdgjH7IITvaBZ558Y9sCj0B_WDpw,18866
83
+ wbportfolio/filters/assets.py,sha256=1nf8rswmwYPEI_pbD107f4SkyJjKLJMqrUEjp7oHFtQ,18793
84
84
  wbportfolio/filters/assets_and_net_new_money_progression.py,sha256=IYl_qMFO9QKhXhCTtz2JrOjlAKdEkykNlu4DO_H7HkQ,1403
85
85
  wbportfolio/filters/custodians.py,sha256=pStQgPQPhPpnt57_V7BuXbFXmRiZBEAiEeMFuQmt2NE,287
86
86
  wbportfolio/filters/esg.py,sha256=ZEUQh3IQxLSshVrgtPqHC_ToirrllkCXR_KzUlCUtkA,688
87
87
  wbportfolio/filters/performances.py,sha256=xJC7fe8XNPz159cewMD8Es8u89vzgpZT4D2Gm1YToM8,7340
88
88
  wbportfolio/filters/portfolios.py,sha256=rQL77Cs1YVR8I_kSZwYppJ9yPj8ebWrDQHs_sOI_Ndk,2247
89
- wbportfolio/filters/positions.py,sha256=7eMqwqnNd_owi2Mg-1Uf9K9K9SHSgdZ8CcYgWw49qgI,8548
89
+ wbportfolio/filters/positions.py,sha256=JymXAxo3t_t-5g0yzIHNz9s0MmuozEuK6itwpg7hsec,8475
90
90
  wbportfolio/filters/products.py,sha256=jOoRKDm_2n2a-Y1dHWl1zjdrwy4Cek57Qa3Q10p_zzY,6492
91
91
  wbportfolio/filters/roles.py,sha256=jb8WLzZ_e03x8XkAWuYAdPgMyre3h-rtgYXKJ3dqkhw,920
92
92
  wbportfolio/filters/signals.py,sha256=XZ1d50yas47Xy57ZfmvCVjMSKojaoqrc5FHJBcVadtk,3144
@@ -95,9 +95,9 @@ wbportfolio/filters/orders/order_proposals.py,sha256=V_lICJBEG1aOVaYiaIi1KcIn8uE
95
95
  wbportfolio/filters/orders/orders.py,sha256=eRVb9w7zHq6bPnHOd3gC1k6OJYSpQ6PeXxu2dxJe1A0,289
96
96
  wbportfolio/filters/transactions/__init__.py,sha256=TXiHB7_ItZPVcXCCGejpLBRD4R7rtt3Qrq59z8D5ChA,582
97
97
  wbportfolio/filters/transactions/claim.py,sha256=eSykgNzcytMWWTmlGlaqBuh98vfOKoTLvA3cglc_jZ0,17951
98
- wbportfolio/filters/transactions/fees.py,sha256=SdUaFCdsET9s2FpBA41I8Fw7Zfd_ZGoDGXs-_q3gtq8,1754
98
+ wbportfolio/filters/transactions/fees.py,sha256=osm7vOqoKFJi3KaylIr5axHmmhDzd4rKSvU8zs-nVRk,1608
99
99
  wbportfolio/filters/transactions/mixins.py,sha256=TEV3MUsiQTeu4NdFYHMIIMonmC7CdFF80JTpWYIvfRQ,550
100
- wbportfolio/filters/transactions/trades.py,sha256=uJHaEwaOBGAnLf9OEWf1-F1FKFP_pnjYEFhIW5g2Stc,10064
100
+ wbportfolio/filters/transactions/trades.py,sha256=7k_r1m1zDiQWCx3g6eUeWjPV18bbU4EV2Jeic9FjnyA,9991
101
101
  wbportfolio/filters/transactions/utils.py,sha256=hU133SdUDuD30wy9QLvCCJbGsiZzxvdOp8xtnGbUzaU,1510
102
102
  wbportfolio/fixtures/product_factsheets.yaml,sha256=z5o-viDbgwWkssDJXZYayasFgw1nJ0uiOiYrJNDkRtg,5455
103
103
  wbportfolio/fixtures/wbportfolio.yaml.gz,sha256=902nxQZM6VcVcc0wI9AYaSedcJIfsK5letLF31j1Jdg,1479453
@@ -124,7 +124,7 @@ wbportfolio/import_export/handlers/fees.py,sha256=RoOaBwnIMzCx5q7qSzCQbS3L_eOa9Y
124
124
  wbportfolio/import_export/handlers/orders.py,sha256=GU3_tIy-tAw9aU-ifsnmMZPBB9sqfkFC_S1d9VziTwg,3136
125
125
  wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
126
126
  wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
127
- wbportfolio/import_export/handlers/trade.py,sha256=TvnL0IhEzoEXxyr7o3a4id6fkB4InJfScUZC1YPfNq0,11187
127
+ wbportfolio/import_export/handlers/trade.py,sha256=d0FSyUQLX8JKBl2fA9BF-AiXCO6TNUWE-oHL5NJavQE,11201
128
128
  wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
129
129
  wbportfolio/import_export/parsers/default_mapping.py,sha256=YdqvoRNo4CbMnMpeYA5DSHSj9fc7rZA4_q9Zeojg344,1393
130
130
  wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -146,10 +146,10 @@ wbportfolio/import_export/parsers/natixis/d1_fees.py,sha256=RmzwlNqLSlGDp7JMTwHu
146
146
  wbportfolio/import_export/parsers/natixis/d1_trade.py,sha256=JsAVgRH2iBo096Spz8ThJOGPpOCPyYrJ9GJ1XnzjBJQ,2092
147
147
  wbportfolio/import_export/parsers/natixis/d1_valuation.py,sha256=M12am0ZenUJxkvEGp6tM1HfeukqYS3b6tfbyfC0XwvY,1216
148
148
  wbportfolio/import_export/parsers/natixis/dividend.py,sha256=qx8DXu6NXMy1M_iunA4ITM845_6iNd1aoVCdoAKDNsY,1964
149
- wbportfolio/import_export/parsers/natixis/equity.py,sha256=GSObiFxvhlbGuSZnJq-D8yRgLlsAxJ4oMxYeR6GmFNE,2501
149
+ wbportfolio/import_export/parsers/natixis/equity.py,sha256=v5ai-9DyG2fayOzIR21cbwASStXtCobN0Devb1snCLo,3292
150
150
  wbportfolio/import_export/parsers/natixis/fees.py,sha256=oIC9moGm4s-6525q4_Syt4Yexj3SDgB5kYSfcsdzLcY,1666
151
151
  wbportfolio/import_export/parsers/natixis/trade.py,sha256=BJum1W0fYe9emHU7RgL-AjHkiclrckQYkG8VkyCnfvM,2579
152
- wbportfolio/import_export/parsers/natixis/utils.py,sha256=oy0Qq_f7lqOcAoK5sKZ3s_sdfiouJMRtajdy_Q_-8Gc,2828
152
+ wbportfolio/import_export/parsers/natixis/utils.py,sha256=ImBHHLkC621ZrUDWI2p-F3AGElprT1FVb7xkPwKwvx0,2490
153
153
  wbportfolio/import_export/parsers/natixis/valuation.py,sha256=qk5eDz-b9bIttDGnZxQZnm9pDIalb-LCGxmeTyMKRmQ,1523
154
154
  wbportfolio/import_export/parsers/refinitiv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
155
155
  wbportfolio/import_export/parsers/refinitiv/adjustment.py,sha256=64AQoLOZQWn5HCLpflJ4OgQB3gCB3w_6Y4YLhcmuClg,819
@@ -270,11 +270,12 @@ wbportfolio/migrations/0086_orderproposal_total_cash_weight.py,sha256=IE65OYYBOS
270
270
  wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py,sha256=4OHTjdXNwJ-My8EgUGYWM_vRBccjYgghjN4fLV0n1yQ,4564
271
271
  wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py,sha256=6Q41GSbWg_L8r6FUcQ9MrdpRazOdUlc7tD8_bmg8a3Y,521
272
272
  wbportfolio/migrations/0089_orderproposal_min_weighting.py,sha256=DgJdIRka1eZSrEZqkUBHiVPpCfOvChWPrWdx-ZExO6Q,3906
273
+ wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py,sha256=2HOOaTe9dV5RKKLFNXJwP6u3NMBFZwv7ctN_EJ6Z6A8,2447
273
274
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
274
275
  wbportfolio/models/__init__.py,sha256=qU4e7HKyh8NL_0Mg92PcbHTewCv7Ya2gei1DMGe1LWE,980
275
276
  wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
276
- wbportfolio/models/asset.py,sha256=EBq_pse5mmV3gQ-7yjfMBYmm8de9LS1wIiq2Dw_JF_Y,39196
277
- wbportfolio/models/builder.py,sha256=txZE_gEDgkPgSxSF0AlvZiRyhRQixD4Ame0wEF6Fjoo,14028
277
+ wbportfolio/models/asset.py,sha256=g4HDZKCXVXNY3MKfgbyjFuGTARJqTnBMVgT2xb0H7V8,39210
278
+ wbportfolio/models/builder.py,sha256=lVYW0iW8-RFL6OEpn1bC4VPUY89dSU1YNuFwC4yGIAo,14027
278
279
  wbportfolio/models/custodians.py,sha256=QhSC3mfd6rSPp8wizabLbKmFDrrskZTSkxNdBJMdtCk,3815
279
280
  wbportfolio/models/exceptions.py,sha256=EZnqSr5PxikiS4oDknRakEmlninJh_c-tOHRYv3IMjE,57
280
281
  wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
@@ -296,10 +297,10 @@ wbportfolio/models/graphs/utils.py,sha256=4QlUkRaTToXwkHBWEPj_eWABntpgl-8gkTqEEf
296
297
  wbportfolio/models/llm/wbcrm/analyze_relationship.py,sha256=etKdmt9m6Ra5u7EA9t98RVSqFKpbFd4iwBVaH9T26M8,2320
297
298
  wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
298
299
  wbportfolio/models/mixins/instruments.py,sha256=SuMPquQ93D4pZMK-4hQbJtV58_NOyf3wVOctQq7LNXQ,7054
299
- wbportfolio/models/mixins/liquidity_stress_test.py,sha256=iQVzT3QM7VtHnqfj9gT6KUIe4wC4MJXery-AXJHUYns,58820
300
+ wbportfolio/models/mixins/liquidity_stress_test.py,sha256=I_pgJ3QVFBtq0SeljJerhtlrZESRDNAe4On6QfMHvXc,58834
300
301
  wbportfolio/models/orders/__init__.py,sha256=EH9UacGR3npBMje5FGTeLOh1xqFBh9kc24WbGmBIA3g,69
301
- wbportfolio/models/orders/order_proposals.py,sha256=JmSZXYqaQtS9tV-iHYgIulG_G5_6bKZqTqsmJK9iY0w,58198
302
- wbportfolio/models/orders/orders.py,sha256=OpUbKzY2g6XA3n8hyU-FTauEQE5UKLEdOPQkdi4rktU,10520
302
+ wbportfolio/models/orders/order_proposals.py,sha256=fzj6UwikU5IIs04s4wjPD9S71I-CzVHPgY8ZgcC7EpE,58463
303
+ wbportfolio/models/orders/orders.py,sha256=-bIle6Ftt_eRfP5R_J2Y4LEh9HqOUF0sbLUxQftd684,11588
303
304
  wbportfolio/models/orders/routing.py,sha256=OpeP-rfMm1UN7a3vbNxBEQxqtGfE57AeGWnbTZa1fRQ,2223
304
305
  wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
305
306
  wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
@@ -307,10 +308,10 @@ wbportfolio/models/reconciliations/account_reconciliations.py,sha256=rofSxetFfEJ
307
308
  wbportfolio/models/reconciliations/reconciliations.py,sha256=kF-BNhUoT4TCn1RIgPSkdEk1iX4NQeZlGGFd_ZulAZU,686
308
309
  wbportfolio/models/transactions/__init__.py,sha256=NrnVHxcpi0Kc6SkTiSlWn4JMWoelvkE9bNouClcvqGA,133
309
310
  wbportfolio/models/transactions/claim.py,sha256=A_1OBULlkOWCNr-d-g9ySSTeqCdao4_7BDwChBpB7io,25900
310
- wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
311
+ wbportfolio/models/transactions/dividends.py,sha256=KNqOMRv6KB_Du5dyap3Q7f9AZ9dMAxpaY9I7-YuNNjs,1914
311
312
  wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
312
- wbportfolio/models/transactions/trades.py,sha256=2cDNjf1hJwFwrDT1SLacUU-Nkt5kU62zjI3WIj_Q0vQ,16340
313
- wbportfolio/models/transactions/transactions.py,sha256=6resBETl_3an6TFLdnSYLXt6QKg5m4JpC9XAGvEK2Ts,3817
313
+ wbportfolio/models/transactions/trades.py,sha256=fzt52pD7GnvIhVGX8NuArFVzxKEQfPc-Qyow-pAcVvI,16364
314
+ wbportfolio/models/transactions/transactions.py,sha256=X7vT0t6UNHFKVU_vCZsA_YQJuOpYisF1vVEeKaZD5CA,4245
314
315
  wbportfolio/order_routing/__init__.py,sha256=kYloUGytPBNB8KVy5ysm9PlEEU_sGPk5c3ch46dXeJo,604
315
316
  wbportfolio/order_routing/adapters/__init__.py,sha256=YiM5FFvLucDTBgIn3mCBjKNSRU7K2pokHdbWBoaZA94,1549
316
317
  wbportfolio/order_routing/adapters/ubs.py,sha256=_Z9-j1a6_F08VlNIjLLn5XmhoKSZQTTH14lGeTgbu9A,6328
@@ -336,7 +337,7 @@ wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
336
337
  wbportfolio/risk_management/backends/__init__.py,sha256=4N3GYcIb0XgZUQWEHxCejOCRabUtLM6ha9UdOWJKUfI,368
337
338
  wbportfolio/risk_management/backends/accounts.py,sha256=VMHwhzeFV2bzobb1RlEJadTVix1lkbbfdpo-zG3YN5c,7988
338
339
  wbportfolio/risk_management/backends/controversy_portfolio.py,sha256=aoRt29QGFNWPf_7yr0Dpjv2AwsA0SJpZdNf8NCfe7JY,2843
339
- wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=8T97J0pbnGdme_Ji9ntA5FV9MCouSxDz5grMvRvpl70,10760
340
+ wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=yqWodIYNkjelTModuLCV1kRXIRiYa55zKrNzlQ9Irso,10770
340
341
  wbportfolio/risk_management/backends/instrument_list_portfolio.py,sha256=PS-OtT0ReFnZ-bsOCtAQhPjhE0Nc5AyEqy4gLya3fhk,4177
341
342
  wbportfolio/risk_management/backends/liquidity_risk.py,sha256=OnuUypmN86RDnch1JOb8UJo5j1z1_X7sjazqAo7cbwM,3694
342
343
  wbportfolio/risk_management/backends/liquidity_stress_instrument.py,sha256=oitzsaZu-HhYn9Avku3322GtDmf6QGsfyRzGPGZoM1Y,3612
@@ -349,7 +350,7 @@ wbportfolio/risk_management/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
349
350
  wbportfolio/risk_management/tests/conftest.py,sha256=ChVocx8SDmh13ixkRmnKe_MMudQAX0495o7nG4NDlsg,440
350
351
  wbportfolio/risk_management/tests/test_accounts.py,sha256=q9Z3hDw0Yw86VealdmUfiJBkzRcqDwfybteGxS0hAJA,3990
351
352
  wbportfolio/risk_management/tests/test_controversy_portfolio.py,sha256=TrvDQ8JEXYFMl7z6MsPAAXV5d8LILO1RYRjMtF9YVTU,1366
352
- wbportfolio/risk_management/tests/test_exposure_portfolio.py,sha256=hKTygdBdZB-ZEIL_-J3QZBvPGtb1N5FWQBBS3rT8lrs,5536
353
+ wbportfolio/risk_management/tests/test_exposure_portfolio.py,sha256=nlEm63OovBKxw0NgE93mY4lvN6i79iS1bHEYeejBkc4,5541
353
354
  wbportfolio/risk_management/tests/test_instrument_list_portfolio.py,sha256=1pAsuMhxOWZHxQnVlTnRuzq8qIe6_86EYbkTnPlWRjE,2763
354
355
  wbportfolio/risk_management/tests/test_liquidity_risk.py,sha256=UtzBrW7LeY0Z0ARX9pg4CUjgdw8u5Hfrdkddro5EEZ8,1713
355
356
  wbportfolio/risk_management/tests/test_product_integrity.py,sha256=wqJ4b5DO2BrEVQF4eVX2B9AvfrRMAyC_lppGrgKc_ug,1959
@@ -375,7 +376,7 @@ wbportfolio/serializers/roles.py,sha256=T-9NqTldpvaEMFy-Bib5MB6MeboygEOqcMP61mzz
375
376
  wbportfolio/serializers/signals.py,sha256=hD6R4oFtwhvnsJPteytPKy2JwEelmxrapdfoLSnluaE,7053
376
377
  wbportfolio/serializers/orders/__init__.py,sha256=PKJRksA1pWsh8nVfGASoB0m3LyUzVRnq1m9VPp90J7k,271
377
378
  wbportfolio/serializers/orders/order_proposals.py,sha256=Jxea2-Ze8Id5URv4UV-vTfCQGt11tjR27vRRfCs0gXU,4791
378
- wbportfolio/serializers/orders/orders.py,sha256=7zIl5lwt7RatSVcrmNmLpWX2r2VkND33HQOUkryGjYo,7582
379
+ wbportfolio/serializers/orders/orders.py,sha256=OfE2ETFu0MvxDWh7o6_4nFunqmKQ-edNT0bNxyF-No4,9473
379
380
  wbportfolio/serializers/transactions/__init__.py,sha256=-7Pan4n7YI3iDvGXff6okzk4ycEURRxp5n_SHCY_g_I,493
380
381
  wbportfolio/serializers/transactions/claim.py,sha256=mEt67F2v8HC6roemDT3S0dD0cZIVl1U9sASbLW3Vpyo,11611
381
382
  wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
@@ -419,7 +420,7 @@ wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo
419
420
  wbportfolio/tests/models/test_splits.py,sha256=0PvW6WunBByDYRhtFV2LbK-DV92q-1s3kn45NvBL5Es,9500
420
421
  wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
421
422
  wbportfolio/tests/models/orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
422
- wbportfolio/tests/models/orders/test_order_proposals.py,sha256=N_l7wgFgd-1uFCE6ZbWCzXEOmOgWaiee-w1VCqKxLnw,35401
423
+ wbportfolio/tests/models/orders/test_order_proposals.py,sha256=Qv8lk2Fehtpgg79JyEZHRGrp02C4uayeRf4EuWeGNSI,36510
423
424
  wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
424
425
  wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
425
426
  wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
@@ -458,7 +459,7 @@ wbportfolio/viewsets/reconciliations.py,sha256=tRS7UBDtMn-07uA9QqYvnCtp_mZGVeULF
458
459
  wbportfolio/viewsets/registers.py,sha256=eOuEhW2McHsOapCKue7M4eazrJnVpCBVxImpFccq0z0,2401
459
460
  wbportfolio/viewsets/roles.py,sha256=vnzS1mlJMZS3GObrss6SfXOr3KRb-OHB1s4Msvtn7xQ,1556
460
461
  wbportfolio/viewsets/charts/__init__.py,sha256=zE3bxS0V-nM_yZ610M7qsaToNOJXvTjwFLhH1it3h4M,169
461
- wbportfolio/viewsets/charts/assets.py,sha256=OcxLOmlrGj2EMp-Id0R356vjGdGlIHmUaRHcz_zXZkI,17926
462
+ wbportfolio/viewsets/charts/assets.py,sha256=shhM9O9qa4fwX3BpqBnfqhZEyPrmCwM81e56cUsqz9g,18013
462
463
  wbportfolio/viewsets/configs/__init__.py,sha256=K5opfVQvgGwSyw3XwDGkv3lo5i9jBxOJXYhLqyCdfeQ,137
463
464
  wbportfolio/viewsets/configs/buttons/__init__.py,sha256=hyvpxwh9ss8Q0xcE006qsTLuSsJeWt9saHRcU4g96tA,791
464
465
  wbportfolio/viewsets/configs/buttons/adjustments.py,sha256=sUY_3vxqP0kuqs8i5hklfboZI6QiAOrmu30eb29Xupo,492
@@ -548,7 +549,7 @@ wbportfolio/viewsets/configs/titles/roles.py,sha256=9LoJa3jgenXJ5UWRlIErTzdbjpSW
548
549
  wbportfolio/viewsets/configs/titles/trades.py,sha256=29XCLxvY0Xe3a2tjCno3tN2rRXCr9RWpbWnzurJfnYI,1986
549
550
  wbportfolio/viewsets/orders/__init__.py,sha256=N8v9jdEXryOzrLlc7ML3iBCO2lmNXph9_TWoQ7PTvi4,195
550
551
  wbportfolio/viewsets/orders/order_proposals.py,sha256=IMiRh7kFqzmHjC34k-FjV21NSxkWVBi5NTEtUysN9qg,8302
551
- wbportfolio/viewsets/orders/orders.py,sha256=bSJ8na5XO5HcmrDdLkD6gH1G6J90nLa3bOTe89IWoAI,10922
552
+ wbportfolio/viewsets/orders/orders.py,sha256=O6Mo5t18FEGDLzAZZvgl8PY1STLiLgv1fyYVhupuSSY,11678
552
553
  wbportfolio/viewsets/orders/configs/__init__.py,sha256=5MU57JXiKi32_PicHtiNr7YHmMN020FrlF5NFJf_Wds,94
553
554
  wbportfolio/viewsets/orders/configs/buttons/__init__.py,sha256=EHzNmAfa0UQFITEF-wxj_s4wn3Y5DE3DCbEUmmvCTIs,106
554
555
  wbportfolio/viewsets/orders/configs/buttons/order_proposals.py,sha256=1BPkIYv0-K2DDGa4Gua2_Pxsx7fNurTZ2tYNdL66On0,6495
@@ -566,7 +567,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
566
567
  wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
567
568
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
568
569
  wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
569
- wbportfolio-1.55.10rc0.dist-info/METADATA,sha256=6GizbbXgUlDYAtWJbLypIw1ijtGXI58Zke9yPWQnIO8,755
570
- wbportfolio-1.55.10rc0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
571
- wbportfolio-1.55.10rc0.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
572
- wbportfolio-1.55.10rc0.dist-info/RECORD,,
570
+ wbportfolio-1.56.1.dist-info/METADATA,sha256=cyu99QOKfM_ltC_DHi7dFqeTEoyf241oW0zDFIECNkc,751
571
+ wbportfolio-1.56.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
572
+ wbportfolio-1.56.1.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
573
+ wbportfolio-1.56.1.dist-info/RECORD,,