wbportfolio 1.56.3__py2.py3-none-any.whl → 1.56.5__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.

@@ -90,7 +90,7 @@ class UBSNeoAPIClient:
90
90
  isin = self._validate_isin(isin, test=test)
91
91
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/submit/{isin}"
92
92
  payload = {"items": items}
93
- response = requests.post(url, json=payload, headers=self._get_headers(), timeout=10)
93
+ response = requests.post(url, json=payload, headers=self._get_headers(), timeout=60)
94
94
  self._raise_for_status(response)
95
95
  return self._get_json(response)
96
96
 
@@ -99,7 +99,7 @@ class UBSNeoAPIClient:
99
99
  isin = self._validate_isin(isin)
100
100
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/savedraft/{isin}"
101
101
  payload = {"items": items}
102
- response = requests.post(url, json=payload, headers=self._get_headers(), timeout=10)
102
+ response = requests.post(url, json=payload, headers=self._get_headers(), timeout=60)
103
103
  self._raise_for_status(response)
104
104
  return self._get_json(response)
105
105
 
@@ -107,14 +107,14 @@ class UBSNeoAPIClient:
107
107
  """Cancel a rebalance request."""
108
108
  isin = self._validate_isin(isin)
109
109
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/cancel/{isin}"
110
- response = requests.delete(url, headers=self._get_headers(), timeout=10)
110
+ response = requests.delete(url, headers=self._get_headers(), timeout=60)
111
111
  self._raise_for_status(response)
112
112
  return self._get_json(response)
113
113
 
114
114
  def get_current_rebalance_request(self, isin: str) -> dict:
115
115
  """Fetch the current rebalance request for a certificate."""
116
116
  url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/currentRebalanceRequest/{isin}"
117
- response = requests.get(url, headers=self._get_headers(), timeout=10)
117
+ response = requests.get(url, headers=self._get_headers(), timeout=60)
118
118
  self._raise_for_status(response)
119
119
  return self._get_json(response)
120
120
 
@@ -130,7 +130,7 @@ class UBSNeoAPIClient:
130
130
  url,
131
131
  headers=self._get_headers(),
132
132
  params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
133
- timeout=10,
133
+ timeout=30,
134
134
  )
135
135
  self._raise_for_status(response)
136
136
  return self._get_json(response)
@@ -141,7 +141,7 @@ class UBSNeoAPIClient:
141
141
  url,
142
142
  headers=self._get_headers(),
143
143
  params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
144
- timeout=10,
144
+ timeout=30,
145
145
  )
146
146
  self._raise_for_status(response)
147
147
  return self._get_json(response)
@@ -3,6 +3,7 @@ from datetime import date
3
3
  from decimal import Decimal
4
4
 
5
5
  from django.conf import settings
6
+ from django.core.cache import cache
6
7
  from django.db import models
7
8
  from django.db.models.signals import post_save
8
9
  from django.dispatch import receiver
@@ -16,7 +17,12 @@ from wbportfolio.models import Claim, Product
16
17
 
17
18
 
18
19
  def get_total_assets_under_management(val_date: date) -> Decimal:
19
- return sum([product.get_total_aum_usd(val_date) for product in Product.active_objects.all()])
20
+ cache_key = f"total_assets_under_management:{val_date.isoformat()}"
21
+ return cache.get_or_set(
22
+ cache_key,
23
+ lambda: sum([product.get_total_aum_usd(val_date) for product in Product.active_objects.all()]),
24
+ 60 * 60 * 24,
25
+ )
20
26
 
21
27
 
22
28
  def get_lost_client_customer_status():
@@ -44,9 +50,8 @@ class Updater:
44
50
  self.val_date = val_date
45
51
  self.total_assets_under_management = get_total_assets_under_management(val_date)
46
52
 
47
- def update_company_data(self, company):
53
+ def update_company_data(self, company_portfolio_data) -> tuple[str, str]:
48
54
  # save company portfolio data
49
- company_portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
50
55
  if (
51
56
  invested_assets_under_management_usd := company_portfolio_data.get_assets_under_management_usd(
52
57
  self.val_date
@@ -55,19 +60,11 @@ class Updater:
55
60
  company_portfolio_data.invested_assets_under_management_usd = invested_assets_under_management_usd
56
61
  if (potential := company_portfolio_data.get_potential(self.val_date)) is not None:
57
62
  company_portfolio_data.potential = potential
58
- company_portfolio_data.save()
59
63
 
60
64
  # update the company object itself
61
- if (tier := company_portfolio_data.get_tiering(self.total_assets_under_management)) is not None:
62
- company.tier = tier
63
- company.customer_status = company_portfolio_data.get_customer_status()
64
- company.save()
65
-
66
- # def update_all_companies(self, val_date: date):
67
- # for company in tqdm(qs, total=qs.count()):
68
- # with suppress(CompanyPortfolioData.DoesNotExist):
69
- # company_portfolio = CompanyPortfolioData.objects.get(company=company)
70
- # company_portfolio.update_data(date.today())
65
+ tier = company_portfolio_data.get_tiering(self.total_assets_under_management)
66
+ customer_status = company_portfolio_data.get_customer_status()
67
+ return customer_status, tier
71
68
 
72
69
 
73
70
  class CompanyPortfolioData(models.Model):
@@ -104,14 +101,6 @@ class CompanyPortfolioData(models.Model):
104
101
  verbose_name="AUM",
105
102
  help_text="The Assets under Management (AUM) that is managed by this company or this person's primary employer.",
106
103
  )
107
- invested_assets_under_management_usd = models.DecimalField(
108
- max_digits=17,
109
- decimal_places=2,
110
- null=True,
111
- blank=True,
112
- help_text="The invested Assets under Management (AUM).",
113
- verbose_name="Invested AUM ($)",
114
- )
115
104
 
116
105
  investment_discretion = models.CharField(
117
106
  max_length=21,
@@ -120,10 +109,6 @@ class CompanyPortfolioData(models.Model):
120
109
  help_text="What discretion this company or this person's primary employer has to invest its assets.",
121
110
  verbose_name="Investment Discretion",
122
111
  )
123
-
124
- potential = models.DecimalField(
125
- decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
126
- )
127
112
  potential_currency = models.ForeignKey(
128
113
  to="currency.Currency",
129
114
  related_name="wbportfolio_potential_currencies",
@@ -132,6 +117,25 @@ class CompanyPortfolioData(models.Model):
132
117
  on_delete=models.PROTECT,
133
118
  )
134
119
 
120
+ # Dynamic fields
121
+ invested_assets_under_management_usd = models.DecimalField(
122
+ max_digits=17,
123
+ decimal_places=2,
124
+ null=True,
125
+ blank=True,
126
+ help_text="The invested Assets under Management (AUM).",
127
+ verbose_name="Invested AUM ($)",
128
+ )
129
+
130
+ potential = models.DecimalField(
131
+ decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
132
+ )
133
+
134
+ def update(self):
135
+ with suppress(CurrencyFXRates.DoesNotExist):
136
+ val_date = CurrencyFXRates.objects.latest("date").date
137
+ self.company.customer_status, self.company.tier = Updater(val_date).update_company_data(self)
138
+
135
139
  def get_assets_under_management_usd(self, val_date: date) -> Decimal:
136
140
  return Claim.objects.filter(status=Claim.Status.APPROVED).filter_for_customer(
137
141
  self.company
@@ -140,16 +144,17 @@ class CompanyPortfolioData(models.Model):
140
144
  )["invested_aum_usd"] or Decimal(0)
141
145
 
142
146
  def _get_default_potential(self, val_date: date) -> Decimal:
143
- with suppress(CurrencyFXRates.DoesNotExist):
144
- fx = CurrencyFXRates.objects.get(currency=self.assets_under_management_currency, date=val_date).value
147
+ if self.assets_under_management:
148
+ with suppress(CurrencyFXRates.DoesNotExist):
149
+ fx = CurrencyFXRates.objects.get(currency=self.assets_under_management_currency, date=val_date).value
145
150
 
146
- aum_usd = self.assets_under_management / fx
147
- aum_potential = Decimal(0)
148
- for asset_allocation in self.company.asset_allocations.all():
149
- aum_potential += aum_usd * asset_allocation.percent * asset_allocation.max_investment
150
- invested_aum = self.invested_assets_under_management_usd or Decimal(0.0)
151
+ aum_usd = self.assets_under_management / fx
152
+ aum_potential = Decimal(0)
153
+ for asset_allocation in self.company.asset_allocations.all():
154
+ aum_potential += aum_usd * asset_allocation.percent * asset_allocation.max_investment
155
+ invested_aum = self.invested_assets_under_management_usd or Decimal(0.0)
151
156
 
152
- return aum_potential - invested_aum
157
+ return aum_potential - invested_aum
153
158
 
154
159
  def get_potential(self, val_date: date) -> Decimal:
155
160
  if module_path := getattr(settings, "PORTFOLIO_COMPANY_DATA_POTENTIAL_METHOD", None):
@@ -225,11 +230,6 @@ class CompanyPortfolioData(models.Model):
225
230
  verbose_name_plural = "Company Portfolio Data"
226
231
 
227
232
 
228
- @receiver(post_save, sender="directory.Company")
229
- def create_company_portfolio_data(sender, instance, created, **kwargs):
230
- CompanyPortfolioData.objects.get_or_create(company=instance)
231
-
232
-
233
233
  class AssetAllocationType(WBModel):
234
234
  name = models.CharField(max_length=255)
235
235
  default_max_investment = models.DecimalField(
@@ -329,3 +329,33 @@ class GeographicFocus(models.Model):
329
329
  @classmethod
330
330
  def get_representation_label_key(cls):
331
331
  return "{{company}}: {{percent}} {{country}}"
332
+
333
+
334
+ @receiver(post_save, sender=AssetAllocation)
335
+ @receiver(post_save, sender=GeographicFocus)
336
+ def post_save_company_data(sender, instance, created, **kwargs):
337
+ company = instance.company
338
+ portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=company)
339
+ if not created:
340
+ portfolio_data.update()
341
+ portfolio_data.save()
342
+ portfolio_data.company.save()
343
+
344
+
345
+ @receiver(post_save, sender="directory.Company")
346
+ def handle_company_portfolio_data(sender, instance, created, **kwargs):
347
+ # create default asset allocation type (equity 50/50)
348
+ if not instance.asset_allocations.exists():
349
+ equity_asset_type = AssetAllocationType.objects.get_or_create(name="Equity")[0]
350
+ AssetAllocation.objects.create(
351
+ company=instance,
352
+ asset_type=equity_asset_type,
353
+ percent=0.5,
354
+ max_investment=0.5,
355
+ )
356
+ if created:
357
+ portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=instance)
358
+ if not created:
359
+ portfolio_data.update()
360
+ portfolio_data.save()
361
+ portfolio_data.company.save()
@@ -91,7 +91,7 @@ def update_portfolio_data(company_portfolio_data, portfolio_data):
91
91
 
92
92
  if investment_discretion := portfolio_data["investment_discretion"]:
93
93
  company_portfolio_data.investment_discretion = investment_discretion
94
-
94
+ company_portfolio_data.update()
95
95
  company_portfolio_data.save()
96
96
 
97
97
 
@@ -243,11 +243,9 @@ class PersonModelListSerializer(CompanyPortfolioDataMixin, BasePersonModelListSe
243
243
 
244
244
 
245
245
  class AssetAllocationTypeRepresentationSerializer(serializers.RepresentationSerializer):
246
- _detail = serializers.HyperlinkField(reverse_name="company_portfolio:assetallocationtyperepresentation-detail")
247
-
248
246
  class Meta:
249
247
  model = AssetAllocationType
250
- fields = ("id", "name", "_detail")
248
+ fields = ("id", "name")
251
249
 
252
250
 
253
251
  class AssetAllocationTypeModelSerializer(serializers.ModelSerializer):
@@ -19,5 +19,16 @@ def update_all_portfolio_data(val_date: date | None = None):
19
19
  has_account=Exists(Account.objects.filter(owner=OuterRef("pk"))),
20
20
  has_portfolio_data=Exists(CompanyPortfolioData.objects.filter(company=OuterRef("pk"))),
21
21
  )
22
+ company_objs = []
23
+ portfolio_data_objs = []
22
24
  for company in tqdm(qs, total=qs.count()):
23
- updater.update_company_data(company)
25
+ portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
26
+ company.customer_status, company.tier = updater.update_company_data(portfolio_data)
27
+ portfolio_data_objs.append(portfolio_data)
28
+ company_objs.append(company)
29
+ if company_objs:
30
+ Company.objects.bulk_update(company_objs, ["customer_status", "tier"])
31
+ if portfolio_data_objs:
32
+ CompanyPortfolioData.objects.bulk_update(
33
+ portfolio_data_objs, ["invested_assets_under_management_usd", "potential"]
34
+ )
@@ -1,6 +1,6 @@
1
1
  from collections import defaultdict
2
2
  from contextlib import suppress
3
- from datetime import datetime
3
+ from datetime import datetime, timedelta
4
4
  from decimal import Decimal
5
5
  from itertools import chain
6
6
  from typing import Any, Dict, List, Optional
@@ -31,7 +31,7 @@ class AssetPositionImportHandler(ImportExportHandler):
31
31
  self.instrument_price_handler = InstrumentPriceImportHandler(self.import_source)
32
32
  self.currency_handler = CurrencyImportHandler(self.import_source)
33
33
 
34
- def _deserialize(self, data: Dict[str, Any]):
34
+ def _deserialize(self, data: Dict[str, Any]): # noqa: C901
35
35
  from wbportfolio.models import Portfolio
36
36
 
37
37
  portfolio_data = data.pop("portfolio", None)
@@ -45,6 +45,10 @@ class AssetPositionImportHandler(ImportExportHandler):
45
45
  data["initial_price"] /= 1000
46
46
  data["currency"] = currency
47
47
  data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
48
+
49
+ # ensure that the position falls into a weekday
50
+ if data["date"].weekday() == 5:
51
+ data["date"] -= timedelta(days=1)
48
52
  if data.get("asset_valuation_date", None):
49
53
  data["asset_valuation_date"] = datetime.strptime(data["asset_valuation_date"], "%Y-%m-%d").date()
50
54
  else:
@@ -43,16 +43,16 @@ def parse(import_source):
43
43
  df = pd.read_excel(import_source.file, engine="openpyxl", sheet_name="Basket Valuation")
44
44
  except BadZipFile:
45
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
-
46
+ xx, yy = np.where(df.isin(["Ticker", "Code"]))
47
+ if xx.size > 0 and yy.size > 0:
48
+ df = df.iloc[xx[0] :, yy[0] :]
49
+ df = df.rename(columns=df.iloc[0]).drop(df.index[0]).dropna(how="all")
50
+ df["Quotity/Adj. factor"] = 1.0
51
+ df = df.rename(columns={"Code": "Ticker"})
52
+ else:
53
+ return {}
54
54
  df = df.rename(columns=FIELD_MAP)
55
- df = df.dropna(subset=["initial_price"])
55
+ df = df.dropna(subset=["initial_price", "Name"], how="any")
56
56
  df["initial_price"] = df["initial_price"].astype("str").str.replace(" ", "").astype("float")
57
57
  df["underlying_quote"] = df[["Ticker", "Name", "currency__key"]].apply(
58
58
  lambda x: _get_underlying_instrument(*x), axis=1
@@ -448,7 +448,10 @@ class AssetPosition(ImportMixin, models.Model):
448
448
  net_value = self.initial_price
449
449
  # in case the position currency and the linked underlying_quote currency don't correspond, we convert the rate accordingly
450
450
  if self.currency != self.underlying_quote.currency:
451
- net_value *= self.currency.convert(self.asset_valuation_date, self.underlying_quote.currency)
451
+ with suppress(CurrencyFXRates.DoesNotExist):
452
+ net_value *= self.currency.convert(
453
+ self.asset_valuation_date, self.underlying_quote.currency
454
+ )
452
455
  self.underlying_quote_price = InstrumentPrice.objects.create(
453
456
  calculated=False,
454
457
  instrument=self.underlying_quote,
@@ -20,7 +20,7 @@ from django.db.models.functions import Coalesce, Round
20
20
  from django.db.models.signals import post_save
21
21
  from django.dispatch import receiver
22
22
  from django.utils.functional import cached_property
23
- from django.utils.translation import gettext_lazy as _
23
+ from django.utils.translation import gettext_lazy
24
24
  from django_fsm import FSMField, transition
25
25
  from pandas._libs.tslibs.offsets import BDay
26
26
  from wbcompliance.models.risk_management.mixins import RiskCheckMixin
@@ -172,7 +172,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
172
172
 
173
173
  @cached_property
174
174
  def portfolio_total_asset_value(self) -> Decimal:
175
- return self.portfolio.get_total_asset_value(self.last_effective_date)
175
+ return self.get_portfolio_total_asset_value()
176
176
 
177
177
  @cached_property
178
178
  def validated_trading_service(self) -> TradingService:
@@ -248,6 +248,25 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
248
248
  # TODO make this dynamically configurable
249
249
  return "MARKET_ON_CLOSE"
250
250
 
251
+ def get_portfolio_total_asset_value(self):
252
+ return self.portfolio.get_total_asset_value(self.last_effective_date)
253
+ # return self.orders.annotate(
254
+ # effective_shares=Coalesce(
255
+ # Subquery(
256
+ # AssetPosition.objects.filter(
257
+ # underlying_quote=OuterRef("underlying_instrument"),
258
+ # date=self.last_effective_date,
259
+ # portfolio=self.portfolio,
260
+ # )
261
+ # .values("portfolio")
262
+ # .annotate(s=Sum("shares"))
263
+ # .values("s")[:1]
264
+ # ),
265
+ # Decimal(0),
266
+ # ),
267
+ # effective_total_value_fx_portfolio=F("effective_shares") * F("currency_fx_rate") * F("price"),
268
+ # ).aggregate(s=Sum("effective_total_value_fx_portfolio"))["s"] or Decimal(0.0)
269
+
251
270
  def get_orders(self):
252
271
  # TODO Issue here: the cash is subqueried on the portfolio, on portfolio such as the fund, there is multiple cash component, that we exclude in the orders (and use a unique cash position instead)
253
272
  # so the subquery returns the previous position (probably USD), but is missing the other cash aggregation. We need to find a way to handle that properly
@@ -428,7 +447,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
428
447
 
429
448
  # 3. Prepare a mapping from instrument IDs to weights for analytic calculations.
430
449
  previous_weights = {instrument.id: float(info["weighting"]) for instrument, info in portfolio.items()}
431
-
432
450
  # 4. Attempt to fetch analytic returns and portfolio contribution. Default on error.
433
451
  try:
434
452
  last_returns, contribution = self.portfolio.get_analytic_portfolio(
@@ -437,7 +455,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
437
455
  last_returns = last_returns.to_dict()
438
456
  except ValueError:
439
457
  last_returns, contribution = {}, 1
440
-
441
458
  positions = []
442
459
  total_weighting = Decimal("0")
443
460
 
@@ -458,6 +475,27 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
458
475
  weighting = drifted_weight + row["delta_weight"]
459
476
 
460
477
  # Assemble the position object
478
+ trade_date = self.last_effective_date if use_effective else self.trade_date
479
+ price_data = {}
480
+ with suppress(InstrumentPrice.DoesNotExist):
481
+ instrument_price = instrument.valuations.get(date=trade_date)
482
+ if fx_rate := instrument_price.currency_fx_rate_to_usd:
483
+ price_data = {
484
+ "volume_usd": instrument_price.volume
485
+ * float(instrument_price.net_value)
486
+ / float(fx_rate.value)
487
+ }
488
+ if instrument_price.market_capitalization:
489
+ price_data["market_capitalization_usd"] = instrument_price.market_capitalization / float(
490
+ fx_rate.value
491
+ )
492
+ if row["shares"] is not None:
493
+ if instrument_price.market_capitalization:
494
+ price_data["market_share"] = (
495
+ float(row["shares"]) * float(row["price"]) / instrument_price.market_capitalization
496
+ )
497
+ if instrument_price.volume_50d:
498
+ price_data["daily_liquidity"] = float(row["shares"]) / instrument_price.volume_50d / 0.33
461
499
  positions.append(
462
500
  PositionDTO(
463
501
  underlying_instrument=instrument.id,
@@ -466,10 +504,14 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
466
504
  daily_return=daily_return if use_effective else Decimal("0"),
467
505
  shares=row["shares"],
468
506
  currency=instrument.currency.id,
469
- date=self.last_effective_date if use_effective else self.trade_date,
507
+ date=trade_date,
508
+ asset_valuation_date=trade_date,
470
509
  is_cash=instrument.is_cash or instrument.is_cash_equivalent,
471
510
  price=row["price"],
472
511
  currency_fx_rate=row["currency_fx_rate"],
512
+ exchange=instrument.exchange.id if instrument.exchange else None,
513
+ country=instrument.country.id if instrument.country else None,
514
+ **price_data,
473
515
  )
474
516
  )
475
517
  total_weighting += weighting
@@ -584,12 +626,16 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
584
626
  target_portfolio = self._get_default_target_portfolio(use_desired_target_weight=use_desired_target_weight)
585
627
  if not effective_portfolio:
586
628
  effective_portfolio = self._get_default_effective_portfolio()
629
+ if use_desired_target_weight:
630
+ total_target_weight = target_portfolio.total_weight
631
+ else:
632
+ total_target_weight = self.total_expected_target_weight
587
633
  if target_portfolio:
588
634
  service = TradingService(
589
635
  self.trade_date,
590
636
  effective_portfolio=effective_portfolio,
591
637
  target_portfolio=target_portfolio,
592
- total_target_weight=self.total_expected_target_weight,
638
+ total_target_weight=total_target_weight,
593
639
  )
594
640
  if validate_order:
595
641
  service.is_valid()
@@ -601,15 +647,11 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
601
647
  portfolio_value = self.portfolio_total_asset_value
602
648
  for order_dto in orders:
603
649
  instrument = Instrument.objects.get(id=order_dto.underlying_instrument)
604
- currency_fx_rate = instrument.currency.convert(
605
- self.value_date, self.portfolio.currency, exact_lookup=True
606
- )
607
650
  # we cannot do a bulk-create because Order is a multi table inheritance
608
651
  weighting = round(order_dto.delta_weight, Order.ORDER_WEIGHTING_PRECISION)
609
652
  daily_return = order_dto.daily_return
610
653
  try:
611
654
  order = self.orders.get(underlying_instrument=instrument)
612
- order.currency_fx_rate = currency_fx_rate
613
655
  order.daily_return = daily_return
614
656
  except Order.DoesNotExist:
615
657
  order = Order(
@@ -618,18 +660,18 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
618
660
  value_date=self.trade_date,
619
661
  weighting=weighting,
620
662
  daily_return=daily_return,
621
- currency_fx_rate=currency_fx_rate,
622
663
  )
623
- order.pre_save()
624
- order.set_weighting(weighting, portfolio_value)
625
- order.desired_target_weight = order_dto.target_weight
626
664
  order.order_type = Order.get_type(
627
665
  weighting, round(order_dto.previous_weight, 8), round(order_dto.target_weight, 8)
628
666
  )
629
- # # if we cannot automatically find a price, we consider the stock is invalid and we sell it
630
- # if not order.price and order.weighting > 0:
631
- # order.price = Decimal("0.0")
632
- # order.weighting = -order_dto.effective_weight
667
+ order.pre_save()
668
+ order.set_weighting(weighting, portfolio_value)
669
+ order.desired_target_weight = order_dto.target_weight
670
+
671
+ # if we cannot automatically find a price, we consider the stock is invalid and we sell it
672
+ if not order.price and order.weighting > 0:
673
+ order.price = Decimal("0.0")
674
+ order.weighting = -order_dto.effective_weight
633
675
  objs.append(order)
634
676
  Order.objects.bulk_create(
635
677
  objs,
@@ -916,7 +958,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
916
958
  # "There is no change detected in this order proposal. Please submit at last one valid order"
917
959
  # )
918
960
  if len(service.validated_trades) == 0:
919
- errors_list.append(_("There is no valid order on this proposal"))
961
+ errors_list.append(gettext_lazy("There is no valid order on this proposal"))
920
962
  if service.errors:
921
963
  errors_list.extend(service.errors)
922
964
  if errors_list:
@@ -1043,22 +1085,26 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
1043
1085
  errors = dict()
1044
1086
  orders = self.get_orders()
1045
1087
  if not self.portfolio.can_be_rebalanced:
1046
- errors["non_field_errors"] = [_("The portfolio does not allow manual rebalanced")]
1088
+ errors["non_field_errors"] = [gettext_lazy("The portfolio does not allow manual rebalanced")]
1047
1089
  if not orders.exists():
1048
1090
  errors["non_field_errors"] = [
1049
- _("At least one order needs to be submitted to be able to apply this proposal")
1091
+ gettext_lazy("At least one order needs to be submitted to be able to apply this proposal")
1050
1092
  ]
1051
1093
  if not self.portfolio.can_be_rebalanced:
1052
1094
  errors["portfolio"] = [
1053
- [_("The portfolio needs to be a model portfolio in order to apply this order proposal manually")]
1095
+ [
1096
+ gettext_lazy(
1097
+ "The portfolio needs to be a model portfolio in order to apply this order proposal manually"
1098
+ )
1099
+ ]
1054
1100
  ]
1055
1101
  if self.has_non_successful_checks:
1056
- errors["non_field_errors"] = [_("The pre orders rules did not passed successfully")]
1102
+ errors["non_field_errors"] = [gettext_lazy("The pre orders rules did not passed successfully")]
1057
1103
  if orders.filter(has_warnings=True).filter(
1058
1104
  underlying_instrument__is_cash=False, underlying_instrument__is_cash_equivalent=False
1059
1105
  ):
1060
1106
  errors["non_field_errors"] = [
1061
- _("There is warning that needs to be addresses on the orders before approval.")
1107
+ gettext_lazy("There is warning that needs to be addresses on the orders before approval.")
1062
1108
  ]
1063
1109
  return errors
1064
1110
 
@@ -1122,7 +1168,9 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
1122
1168
  errors = dict()
1123
1169
  if not self.portfolio.can_be_rebalanced:
1124
1170
  errors["portfolio"] = [
1125
- _("The portfolio needs to be a model portfolio in order to revert this order proposal manually")
1171
+ gettext_lazy(
1172
+ "The portfolio needs to be a model portfolio in order to revert this order proposal manually"
1173
+ )
1126
1174
  ]
1127
1175
  return errors
1128
1176
 
@@ -1,3 +1,4 @@
1
+ from contextlib import suppress
1
2
  from datetime import date
2
3
  from decimal import Decimal
3
4
 
@@ -7,7 +8,9 @@ from django.db.models import (
7
8
  Sum,
8
9
  )
9
10
  from ordered_model.models import OrderedModel
11
+ from wbcore.contrib.currency.models import CurrencyFXRates
10
12
  from wbcore.contrib.io.mixins import ImportMixin
13
+ from wbfdm.models import Instrument
11
14
 
12
15
  from wbportfolio.import_export.handlers.orders import OrderImportHandler
13
16
  from wbportfolio.models.asset import AssetPosition
@@ -137,12 +140,7 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
137
140
  return getattr(
138
141
  self,
139
142
  "effective_shares",
140
- AssetPosition.objects.filter(
141
- underlying_quote=self.underlying_instrument,
142
- date=self.value_date,
143
- portfolio=self.portfolio,
144
- ).aggregate(s=Sum("shares"))["s"]
145
- or Decimal(0),
143
+ self.get_effective_shares(),
146
144
  )
147
145
 
148
146
  @property
@@ -164,29 +162,31 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
164
162
  def pre_save(self):
165
163
  self.portfolio = self.order_proposal.portfolio
166
164
  self.value_date = self.order_proposal.trade_date
165
+ self.set_currency_fx_rate()
167
166
 
168
167
  if not self.price:
169
- # we try to get the price if not provided directly from the underlying instrument
170
- if self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent:
171
- self.price = Decimal("1")
172
- else:
173
- self.price = self.get_price()
174
- if not self.portfolio.only_weighting:
168
+ self.set_price()
169
+ if not self.portfolio.only_weighting and not self.shares:
175
170
  estimated_shares = self.order_proposal.get_estimated_shares(
176
171
  self.weighting, self.underlying_instrument, self.price
177
172
  )
178
173
  if estimated_shares:
179
174
  self.shares = estimated_shares
175
+ if effective_shares := self.get_effective_shares():
176
+ if self.order_type == self.Type.SELL:
177
+ self.shares = -effective_shares
178
+ else:
179
+ self.shares = max(self.shares, -effective_shares)
180
180
  super().pre_save()
181
181
 
182
182
  def save(self, *args, **kwargs):
183
+ if self.id:
184
+ self.set_type()
183
185
  self.pre_save()
184
186
  if not self.underlying_instrument.is_investable_universe:
185
187
  self.underlying_instrument.is_investable_universe = True
186
188
  self.underlying_instrument.save()
187
189
 
188
- if self.id:
189
- self.set_type()
190
190
  super().save(*args, **kwargs)
191
191
 
192
192
  @classmethod
@@ -205,14 +205,46 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
205
205
  else:
206
206
  return Order.Type.SELL
207
207
 
208
- def get_price(self) -> Decimal:
209
- try:
210
- return self.underlying_instrument.get_price(self.value_date)
211
- except ValueError:
212
- return Decimal("0")
208
+ def get_effective_shares(self) -> Decimal:
209
+ return AssetPosition.objects.filter(
210
+ underlying_quote=self.underlying_instrument,
211
+ date=self.order_proposal.last_effective_date,
212
+ portfolio=self.portfolio,
213
+ ).aggregate(s=Sum("shares"))["s"] or Decimal("0")
213
214
 
214
215
  def set_type(self):
215
- self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
216
+ effective_weight = self._effective_weight
217
+ self.order_type = self.get_type(self.weighting, effective_weight, effective_weight + self.weighting)
218
+
219
+ def set_price(self):
220
+ daily_return = last_price = Decimal("0")
221
+
222
+ effective_date = self.order_proposal.last_effective_date
223
+ if self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent:
224
+ last_price = Decimal("1")
225
+ else:
226
+ prices, returns = Instrument.objects.filter(id=self.underlying_instrument.id).get_returns_df(
227
+ from_date=effective_date,
228
+ to_date=self.value_date,
229
+ to_currency=self.order_proposal.portfolio.currency,
230
+ use_dl=True,
231
+ )
232
+ with suppress(IndexError):
233
+ daily_return = Decimal(returns.iloc[-1, 0])
234
+ with suppress(KeyError):
235
+ last_price = Decimal(
236
+ prices.get(self.value_date, prices[effective_date])[self.underlying_instrument.id]
237
+ )
238
+ self.daily_return = daily_return
239
+ self.price = last_price
240
+
241
+ def set_currency_fx_rate(self):
242
+ self.currency_fx_rate = Decimal("1")
243
+ if self.order_proposal.portfolio.currency != self.underlying_instrument.currency:
244
+ with suppress(CurrencyFXRates.DoesNotExist):
245
+ self.currency_fx_rate = self.underlying_instrument.currency.convert(
246
+ self.value_date, self.portfolio.currency, exact_lookup=True
247
+ )
216
248
 
217
249
  def set_weighting(self, weighting: Decimal, portfolio_value: Decimal):
218
250
  self.weighting = weighting
@@ -242,28 +274,28 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
242
274
 
243
275
  def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
244
276
  warnings = []
245
-
246
277
  # 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:
278
+ if self._target_weight:
279
+ if self.order_proposal and not self.portfolio.only_weighting:
280
+ shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
281
+ if shares != self.shares:
282
+ warnings.append(
283
+ 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}"
284
+ )
285
+ shares = round(shares) # ensure fractional shares are converted into integer
286
+ # we need to recompute the delta weight has we changed the number of shares
287
+ if shares != self.shares:
288
+ self.set_shares(shares, portfolio_total_asset_value)
289
+ if abs(self.weighting) < self.order_proposal.min_weighting:
250
290
  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}"
291
+ f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
252
292
  )
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)
293
+ self.set_weighting(Decimal("0"), portfolio_total_asset_value)
294
+ if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
295
+ warnings.append(
296
+ 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})"
297
+ )
298
+ self.set_weighting(Decimal("0"), portfolio_total_asset_value)
267
299
  if not self.price:
268
300
  warnings.append(f"No price for {self.underlying_instrument.computed_str}")
269
301
  if (
@@ -145,17 +145,20 @@ class TradingService:
145
145
  previous_weight = target_weight = 0
146
146
  effective_shares = target_shares = 0
147
147
  daily_return = 0
148
+ price = Decimal("0")
148
149
  is_cash = False
149
150
  if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
150
151
  previous_weight = effective_pos.weighting
151
152
  effective_shares = effective_pos.shares
152
153
  daily_return = effective_pos.daily_return
153
154
  is_cash = effective_pos.is_cash
155
+ price = effective_pos.price
154
156
  if target_pos := target_portfolio.positions_map.get(instrument_id, None):
155
157
  target_weight = target_pos.weighting
156
158
  is_cash = target_pos.is_cash
157
159
  if target_pos.shares is not None:
158
160
  target_shares = target_pos.shares
161
+ price = target_pos.price
159
162
  trade = Trade(
160
163
  underlying_instrument=instrument_id,
161
164
  previous_weight=previous_weight,
@@ -165,7 +168,7 @@ class TradingService:
165
168
  date=self.trade_date,
166
169
  instrument_type=pos.instrument_type,
167
170
  currency=pos.currency,
168
- price=Decimal(pos.price) if pos.price is not None else Decimal("0"),
171
+ price=Decimal(price) if price else Decimal("0"),
169
172
  currency_fx_rate=Decimal(pos.currency_fx_rate),
170
173
  daily_return=Decimal(daily_return),
171
174
  portfolio_contribution=effective_portfolio.portfolio_contribution,
@@ -160,7 +160,7 @@ class RuleBackend(
160
160
 
161
161
  def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
162
162
  if not (df := self._filter_df(portfolio.to_df())).empty:
163
- df = df[[self.group_by.value, self.field.value]].groupby(self.group_by.value).sum().astype(float)
163
+ df = df[[self.group_by.value, self.field.value]].dropna().groupby(self.group_by.value).sum().astype(float)
164
164
  for threshold in self.thresholds:
165
165
  numerical_range = threshold.numerical_range
166
166
  incident_df = df[
@@ -66,6 +66,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
66
66
  total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
67
67
 
68
68
  portfolio_currency = wb_serializers.CharField(read_only=True)
69
+ underlying_instrument_currency = wb_serializers.CharField(read_only=True)
69
70
  has_warnings = wb_serializers.BooleanField(read_only=True)
70
71
 
71
72
  def validate(self, data):
@@ -131,7 +132,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
131
132
 
132
133
  class Meta:
133
134
  model = Order
134
- percent_fields = ["effective_weight", "target_weight", "weighting"]
135
+ percent_fields = ["effective_weight", "target_weight", "weighting", "desired_target_weight"]
135
136
  decorators = {
136
137
  "total_value_fx_portfolio": wb_serializers.decorator(
137
138
  decorator_type="text", position="left", value="{{portfolio_currency}}"
@@ -142,12 +143,17 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
142
143
  "target_total_value_fx_portfolio": wb_serializers.decorator(
143
144
  decorator_type="text", position="left", value="{{portfolio_currency}}"
144
145
  ),
146
+ "price": wb_serializers.decorator(position="left", value="{{underlying_instrument_currency}}"),
145
147
  }
146
148
  read_only_fields = (
147
149
  "order_type",
148
150
  "effective_shares",
149
151
  "effective_total_value_fx_portfolio",
150
152
  "has_warnings",
153
+ "desired_target_weight",
154
+ "daily_return",
155
+ "currency_fx_rate",
156
+ "price",
151
157
  )
152
158
  fields = (
153
159
  "id",
@@ -171,9 +177,14 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
171
177
  "effective_total_value_fx_portfolio",
172
178
  "target_total_value_fx_portfolio",
173
179
  "portfolio_currency",
180
+ "underlying_instrument_currency",
174
181
  "has_warnings",
175
182
  "execution_confirmed",
176
183
  "execution_comment",
184
+ "desired_target_weight",
185
+ "daily_return",
186
+ "currency_fx_rate",
187
+ "price",
177
188
  )
178
189
 
179
190
 
@@ -422,7 +422,7 @@ class TestOrderProposal:
422
422
  mock_fct.assert_has_calls(expected_calls)
423
423
 
424
424
  # Test estimating shares for a trade
425
- @patch.object(Portfolio, "get_total_asset_value")
425
+ @patch.object(OrderProposal, "get_portfolio_total_asset_value")
426
426
  def test_get_estimated_shares(
427
427
  self, mock_fct, order_proposal, order_factory, instrument_price_factory, instrument_factory
428
428
  ):
@@ -449,7 +449,7 @@ class TestOrderProposal:
449
449
  == Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
450
450
  )
451
451
 
452
- @patch.object(Portfolio, "get_total_asset_value")
452
+ @patch.object(OrderProposal, "get_portfolio_total_asset_value")
453
453
  def test_get_estimated_target_cash(self, mock_fct, order_proposal, order_factory, cash_factory):
454
454
  order_proposal.portfolio.only_weighting = False
455
455
  order_proposal.portfolio.save()
@@ -500,7 +500,7 @@ class TestOrderProposal:
500
500
  instrument.exchange.save()
501
501
  assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
502
502
 
503
- @patch.object(Portfolio, "get_total_asset_value")
503
+ @patch.object(OrderProposal, "get_portfolio_total_asset_value")
504
504
  def test_submit_round_lot_size(self, mock_fct, order_proposal, instrument_price_factory, order_factory):
505
505
  initial_shares = Decimal("70")
506
506
  price = instrument_price_factory.create(date=order_proposal.trade_date)
@@ -528,8 +528,10 @@ class TestOrderProposal:
528
528
  trade.refresh_from_db()
529
529
  assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
530
530
 
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):
531
+ @patch.object(OrderProposal, "get_portfolio_total_asset_value")
532
+ def test_submit_round_fractional_shares(
533
+ self, mock_fct, instrument_price_factory, order_proposal, order_factory, asset_position_factory
534
+ ):
533
535
  initial_shares = Decimal("5.6")
534
536
  price = instrument_price_factory.create(date=order_proposal.trade_date)
535
537
  net_value = round(price.net_value, 4)
@@ -538,6 +540,7 @@ class TestOrderProposal:
538
540
 
539
541
  order_proposal.portfolio.only_weighting = False
540
542
  order_proposal.portfolio.save()
543
+
541
544
  trade = order_factory.create(
542
545
  shares=Decimal("5.6"),
543
546
  order_proposal=order_proposal,
@@ -157,7 +157,13 @@ class OrderOrderProposalDisplayConfig(DisplayViewConfig):
157
157
  formatting_rules=ORDER_TYPE_FORMATTING_RULES,
158
158
  width=Unit.PIXEL(125),
159
159
  ),
160
- dp.Field(key="comment", label="Comment", width=Unit.PIXEL(250)),
160
+ dp.Field(key="comment", label="Comment", show="open", width=Unit.PIXEL(250)),
161
+ dp.Field(
162
+ key="desired_target_weight", label="Desired Target Weight", show="open", width=Unit.PIXEL(100)
163
+ ),
164
+ dp.Field(key="daily_return", label="Daily Return", show="open", width=Unit.PIXEL(100)),
165
+ dp.Field(key="currency_fx_rate", label="FX Rate", show="open", width=Unit.PIXEL(100)),
166
+ dp.Field(key="price", label="Price", show="open", width=Unit.PIXEL(100)),
161
167
  dp.Field(key="order", label="Order", show="open", width=Unit.PIXEL(100)),
162
168
  ],
163
169
  )
@@ -2,7 +2,7 @@ from contextlib import suppress
2
2
  from datetime import date
3
3
  from decimal import Decimal
4
4
 
5
- from django.contrib.messages import info, warning
5
+ from django.contrib.messages import warning
6
6
  from django.shortcuts import get_object_or_404
7
7
  from django.utils.functional import cached_property
8
8
  from pandas._libs.tslibs.offsets import BDay
@@ -87,8 +87,6 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
87
87
 
88
88
  def add_messages(self, request, instance: OrderProposal | None = None, **kwargs):
89
89
  if instance:
90
- if instance.status == OrderProposal.Status.APPROVED and not instance.portfolio.is_manageable:
91
- info(request, "This order proposal cannot be approved the portfolio is considered unmanaged.")
92
90
  if instance.status == OrderProposal.Status.PENDING and instance.has_non_successful_checks:
93
91
  warning(
94
92
  request,
@@ -92,10 +92,10 @@ class OrderOrderProposalModelViewSet(
92
92
  sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
93
93
  )
94
94
  # weights aggregates
95
- cash_sum_effective_weight = (
96
- self.order_proposal.total_effective_portfolio_weight - noncash_aggregates["sum_effective_weight"]
95
+ cash_sum_effective_weight = self.order_proposal.total_effective_portfolio_weight - (
96
+ noncash_aggregates["sum_effective_weight"] or Decimal(0)
97
97
  )
98
- cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
98
+ cash_sum_target_cash_weight = Decimal("1.0") - (noncash_aggregates["sum_target_weight"] or Decimal(0))
99
99
  noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
100
100
  noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
101
101
  sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
@@ -235,6 +235,7 @@ class OrderOrderProposalModelViewSet(
235
235
  effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
236
236
  target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
237
237
  portfolio_currency=F("portfolio__currency__symbol"),
238
+ underlying_instrument_currency=F("underlying_instrument__currency__symbol"),
238
239
  security=F("underlying_instrument__parent"),
239
240
  company=F("underlying_instrument__parent__parent"),
240
241
  ).select_related(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.56.3
3
+ Version: 1.56.5
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -30,7 +30,7 @@ wbportfolio/admin/transactions/trades.py,sha256=LanpSm6iaR9cmNvSJwpRFPkOgN46K_Y9
30
30
  wbportfolio/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  wbportfolio/analysis/claims.py,sha256=wcTniEVdDnmME8LHrYbo-WWpQ5EzNPgAT2KJIGQ0IIA,10688
32
32
  wbportfolio/api_clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- wbportfolio/api_clients/ubs.py,sha256=DezqTcZ_TJnMsTRQnnHisWCu_Vxx-CtE0pwr_SYQBi0,6742
33
+ wbportfolio/api_clients/ubs.py,sha256=VSNEND87iHEpQgHY-erWAc4QMAa_BzrqYXdK9JHqQFM,6742
34
34
  wbportfolio/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  wbportfolio/contrib/company_portfolio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  wbportfolio/contrib/company_portfolio/admin.py,sha256=roUsuOjYDWX0OGWVBjCcW3MAcwJHJYHnqZ_pLpwakmQ,549
@@ -40,10 +40,10 @@ wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py,sha256=k69
40
40
  wbportfolio/contrib/company_portfolio/factories.py,sha256=LvEYhBURUz3aK5KtTshBZpU66ph_DbLXKKDzf3Sw4Vo,1243
41
41
  wbportfolio/contrib/company_portfolio/filters.py,sha256=jLDo3PRhofuqR2EA7G2R2aiAd_pTCZO4BvUZh0AN1HQ,4995
42
42
  wbportfolio/contrib/company_portfolio/management.py,sha256=CofBifGGGKfA06uWRDWXY_9y7y-PGNRz3VhsVaDeAz8,857
43
- wbportfolio/contrib/company_portfolio/models.py,sha256=dfrIR6sI3He1TIHCjdAP3yIZ0cPvc97ZUX_ZzkkhzGg,12943
43
+ wbportfolio/contrib/company_portfolio/models.py,sha256=0YlWZfpBTUy4nCE0FYY6Kqcx3H28-AbeIVXgxcF6hM0,13963
44
44
  wbportfolio/contrib/company_portfolio/scripts.py,sha256=wFfc1L-it0UTaBwNFs8hhGARbJoaNzmnlsXh6KEbslU,2497
45
- wbportfolio/contrib/company_portfolio/serializers.py,sha256=Y4Osxp0M7m_wy7DSbOt7ESfbob8KrfPBCnbzAuEsmmk,12160
46
- wbportfolio/contrib/company_portfolio/tasks.py,sha256=ZXezolvlj8_b5hvfcjH5IO5tXvwb8goNG8xPfk6YFNA,835
45
+ wbportfolio/contrib/company_portfolio/serializers.py,sha256=i4lXu79Mkr_b4zOO4iQ73Tl6DucLK7Aj9npaG8ox7js,12067
46
+ wbportfolio/contrib/company_portfolio/tasks.py,sha256=nIBTqC36JCakCug7YVt1aBqai5bKDsgZd2HS7Jwpf5U,1384
47
47
  wbportfolio/contrib/company_portfolio/urls.py,sha256=eyq6IpaDPG1oL8-AqPS5nC5BeFewKPA1BN_HI7M3i1c,1118
48
48
  wbportfolio/contrib/company_portfolio/viewsets.py,sha256=o-_D_bEt283xngDbsMwdRQxFyuK7rfSChI8u5dGYyLM,8312
49
49
  wbportfolio/contrib/company_portfolio/configs/__init__.py,sha256=AVH0v-uOJBPRKggz7fO7IpRyVJ1YlQiF-if_cMsOvvM,72
@@ -118,7 +118,7 @@ wbportfolio/import_export/backends/wbfdm/dividend.py,sha256=iAQXnYPXmtG_Jrc8THAJ
118
118
  wbportfolio/import_export/backends/wbfdm/mixin.py,sha256=JNtjgqGLson1nu_Chqb8MWyuiF3Ws8ox2vapxIRBYKE,400
119
119
  wbportfolio/import_export/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
120
  wbportfolio/import_export/handlers/adjustment.py,sha256=6bdTIYFmc8_HFxcdwtnYwglMyCfAD8XrTIrEb2zWY0g,1757
121
- wbportfolio/import_export/handlers/asset_position.py,sha256=VNfPOmUta5f4YO_nRFvsiVeGaDwzLF1QDhXFQd0fCX4,9137
121
+ wbportfolio/import_export/handlers/asset_position.py,sha256=ruZoAiwhDmJaiD-OVSZGJ66QrObBeU_JySxsxFmZY30,9305
122
122
  wbportfolio/import_export/handlers/dividend.py,sha256=34iu1SfnyCVSObklo9eiF-ECTQhSvkiZn2pUmrWOaV4,4408
123
123
  wbportfolio/import_export/handlers/fees.py,sha256=RoOaBwnIMzCx5q7qSzCQbS3L_eOa9Y-RSZ_ZG3QHbNo,2507
124
124
  wbportfolio/import_export/handlers/orders.py,sha256=GU3_tIy-tAw9aU-ifsnmMZPBB9sqfkFC_S1d9VziTwg,3136
@@ -146,7 +146,7 @@ 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=v5ai-9DyG2fayOzIR21cbwASStXtCobN0Devb1snCLo,3292
149
+ wbportfolio/import_export/parsers/natixis/equity.py,sha256=oitpx_OYrsGbrYodoF37zjNzsigy2pkTRJ_E6TWpPYU,3341
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
152
  wbportfolio/import_export/parsers/natixis/utils.py,sha256=ImBHHLkC621ZrUDWI2p-F3AGElprT1FVb7xkPwKwvx0,2490
@@ -274,7 +274,7 @@ wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py,s
274
274
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
275
275
  wbportfolio/models/__init__.py,sha256=qU4e7HKyh8NL_0Mg92PcbHTewCv7Ya2gei1DMGe1LWE,980
276
276
  wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
277
- wbportfolio/models/asset.py,sha256=g4HDZKCXVXNY3MKfgbyjFuGTARJqTnBMVgT2xb0H7V8,39210
277
+ wbportfolio/models/asset.py,sha256=zVJm3M5FIYYEMLl3mS0GxWfotadRgItOgIpNIto-0jw,39345
278
278
  wbportfolio/models/builder.py,sha256=lVYW0iW8-RFL6OEpn1bC4VPUY89dSU1YNuFwC4yGIAo,14027
279
279
  wbportfolio/models/custodians.py,sha256=QhSC3mfd6rSPp8wizabLbKmFDrrskZTSkxNdBJMdtCk,3815
280
280
  wbportfolio/models/exceptions.py,sha256=EZnqSr5PxikiS4oDknRakEmlninJh_c-tOHRYv3IMjE,57
@@ -299,8 +299,8 @@ wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
299
299
  wbportfolio/models/mixins/instruments.py,sha256=SuMPquQ93D4pZMK-4hQbJtV58_NOyf3wVOctQq7LNXQ,7054
300
300
  wbportfolio/models/mixins/liquidity_stress_test.py,sha256=I_pgJ3QVFBtq0SeljJerhtlrZESRDNAe4On6QfMHvXc,58834
301
301
  wbportfolio/models/orders/__init__.py,sha256=EH9UacGR3npBMje5FGTeLOh1xqFBh9kc24WbGmBIA3g,69
302
- wbportfolio/models/orders/order_proposals.py,sha256=UYNuW5PguEjVD0h66vyGjagWlWqq46cpv80BORDiT3U,58281
303
- wbportfolio/models/orders/orders.py,sha256=-bIle6Ftt_eRfP5R_J2Y4LEh9HqOUF0sbLUxQftd684,11588
302
+ wbportfolio/models/orders/order_proposals.py,sha256=bg0Xv5XKwcSa1cAcGkn4g-krYEBPrpjJ4D5Rh7W0dYU,60716
303
+ wbportfolio/models/orders/orders.py,sha256=zKSMUJV3OqSbqokF2EoxAAGVhP08jZQjayV9IfkNASQ,13188
304
304
  wbportfolio/models/orders/routing.py,sha256=7nu7-3zmGsVA3tyymKK_ywY7V7RtKkGcXkyk2V8dXMw,2191
305
305
  wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
306
306
  wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
@@ -322,7 +322,7 @@ wbportfolio/pms/analytics/portfolio.py,sha256=u_S-e6HUQwAyq90gweDmxyTHWrIc5nd84s
322
322
  wbportfolio/pms/analytics/utils.py,sha256=EfhKdo9B2ABaUPppb8DgZSqpNkSze8Rjej1xDjv-XcQ,282
323
323
  wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
324
324
  wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
325
- wbportfolio/pms/trading/handler.py,sha256=sGwtwerozgiCVOTeQgJ0Q9g1zzQCPuydkf0aWn6NnGs,8646
325
+ wbportfolio/pms/trading/handler.py,sha256=p56de1nm-QZh7tfQ7uITp1HuVE2XRaGj4T3oYBtp4Iw,8744
326
326
  wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
327
327
  wbportfolio/rebalancing/base.py,sha256=wpeoxdkLz5osxm5mRjkOoML7YkYvwuAlqSLLtHBbWp8,984
328
328
  wbportfolio/rebalancing/decorators.py,sha256=162ZmXV2YQGI830LWvEnJ95RexMzHvaCGfcVOnXTOXM,502
@@ -337,7 +337,7 @@ wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
337
337
  wbportfolio/risk_management/backends/__init__.py,sha256=4N3GYcIb0XgZUQWEHxCejOCRabUtLM6ha9UdOWJKUfI,368
338
338
  wbportfolio/risk_management/backends/accounts.py,sha256=VMHwhzeFV2bzobb1RlEJadTVix1lkbbfdpo-zG3YN5c,7988
339
339
  wbportfolio/risk_management/backends/controversy_portfolio.py,sha256=aoRt29QGFNWPf_7yr0Dpjv2AwsA0SJpZdNf8NCfe7JY,2843
340
- wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=yqWodIYNkjelTModuLCV1kRXIRiYa55zKrNzlQ9Irso,10770
340
+ wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=Tumcg47pX4dVTu1L67bSdgPC9QwOwDewLW_xtyKnAKE,10779
341
341
  wbportfolio/risk_management/backends/instrument_list_portfolio.py,sha256=PS-OtT0ReFnZ-bsOCtAQhPjhE0Nc5AyEqy4gLya3fhk,4177
342
342
  wbportfolio/risk_management/backends/liquidity_risk.py,sha256=OnuUypmN86RDnch1JOb8UJo5j1z1_X7sjazqAo7cbwM,3694
343
343
  wbportfolio/risk_management/backends/liquidity_stress_instrument.py,sha256=oitzsaZu-HhYn9Avku3322GtDmf6QGsfyRzGPGZoM1Y,3612
@@ -376,7 +376,7 @@ wbportfolio/serializers/roles.py,sha256=T-9NqTldpvaEMFy-Bib5MB6MeboygEOqcMP61mzz
376
376
  wbportfolio/serializers/signals.py,sha256=hD6R4oFtwhvnsJPteytPKy2JwEelmxrapdfoLSnluaE,7053
377
377
  wbportfolio/serializers/orders/__init__.py,sha256=PKJRksA1pWsh8nVfGASoB0m3LyUzVRnq1m9VPp90J7k,271
378
378
  wbportfolio/serializers/orders/order_proposals.py,sha256=Jxea2-Ze8Id5URv4UV-vTfCQGt11tjR27vRRfCs0gXU,4791
379
- wbportfolio/serializers/orders/orders.py,sha256=sab4cGfo-zGPz_5W9DuwOqeo_ja0VobZGU01PjcZUHM,9521
379
+ wbportfolio/serializers/orders/orders.py,sha256=7FOjeT-YAACzKjnJ2Ki2ads7wMq2Td8Zy7ot4TvJ8qs,10014
380
380
  wbportfolio/serializers/transactions/__init__.py,sha256=-7Pan4n7YI3iDvGXff6okzk4ycEURRxp5n_SHCY_g_I,493
381
381
  wbportfolio/serializers/transactions/claim.py,sha256=mEt67F2v8HC6roemDT3S0dD0cZIVl1U9sASbLW3Vpyo,11611
382
382
  wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
@@ -420,7 +420,7 @@ wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo
420
420
  wbportfolio/tests/models/test_splits.py,sha256=0PvW6WunBByDYRhtFV2LbK-DV92q-1s3kn45NvBL5Es,9500
421
421
  wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
422
422
  wbportfolio/tests/models/orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
423
- wbportfolio/tests/models/orders/test_order_proposals.py,sha256=Qv8lk2Fehtpgg79JyEZHRGrp02C4uayeRf4EuWeGNSI,36510
423
+ wbportfolio/tests/models/orders/test_order_proposals.py,sha256=LjQCw10kA1C_21sI_Mm1pkvQLd7fEDHSwODcdt6HGpc,36605
424
424
  wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
425
425
  wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
426
426
  wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
@@ -548,15 +548,15 @@ wbportfolio/viewsets/configs/titles/registers.py,sha256=-C-YeGBhGva48oER6EIQ8CxW
548
548
  wbportfolio/viewsets/configs/titles/roles.py,sha256=9LoJa3jgenXJ5UWRlIErTzdbjpSWMKsyJZtv-eDRTK4,739
549
549
  wbportfolio/viewsets/configs/titles/trades.py,sha256=29XCLxvY0Xe3a2tjCno3tN2rRXCr9RWpbWnzurJfnYI,1986
550
550
  wbportfolio/viewsets/orders/__init__.py,sha256=N8v9jdEXryOzrLlc7ML3iBCO2lmNXph9_TWoQ7PTvi4,195
551
- wbportfolio/viewsets/orders/order_proposals.py,sha256=nWUlyVN4Ox54nVHyqN2ryHf_BPXuFjrW-FK5sp5WrHA,8333
552
- wbportfolio/viewsets/orders/orders.py,sha256=O6Mo5t18FEGDLzAZZvgl8PY1STLiLgv1fyYVhupuSSY,11678
551
+ wbportfolio/viewsets/orders/order_proposals.py,sha256=0z85RnnsJPzH_LVBLqLYY1xuFExkYMbc1gSyNjpvD-4,8110
552
+ wbportfolio/viewsets/orders/orders.py,sha256=rZ0u1_FnmtJxUM4dkC3Y4BzS6KY1iZRyuNGXitKAies,11797
553
553
  wbportfolio/viewsets/orders/configs/__init__.py,sha256=5MU57JXiKi32_PicHtiNr7YHmMN020FrlF5NFJf_Wds,94
554
554
  wbportfolio/viewsets/orders/configs/buttons/__init__.py,sha256=EHzNmAfa0UQFITEF-wxj_s4wn3Y5DE3DCbEUmmvCTIs,106
555
555
  wbportfolio/viewsets/orders/configs/buttons/order_proposals.py,sha256=1BPkIYv0-K2DDGa4Gua2_Pxsx7fNurTZ2tYNdL66On0,6495
556
556
  wbportfolio/viewsets/orders/configs/buttons/orders.py,sha256=GDO4Y33wkjhDxzpf7B1d_rKzAixegLv5rHam1DV3WkM,290
557
557
  wbportfolio/viewsets/orders/configs/displays/__init__.py,sha256=__YJBbz_ZnKpE8WMMDR2PC9Nng-EVlRpGTEQucdrhRA,108
558
558
  wbportfolio/viewsets/orders/configs/displays/order_proposals.py,sha256=zLoLEw4N1i_LD0e3hJnxzO8ORN2byFZoCxWnAz8DBx0,7362
559
- wbportfolio/viewsets/orders/configs/displays/orders.py,sha256=R1oaDTGosL430xnYZpJz7ihnCOCO6eAFhDQz-wGYT24,7611
559
+ wbportfolio/viewsets/orders/configs/displays/orders.py,sha256=JHG7vax5cYWLfeq64U_4SDfBfixfBfE1FPZVBANMFIY,8105
560
560
  wbportfolio/viewsets/orders/configs/endpoints/__init__.py,sha256=IB8GEadiEtBDclhkgpcJGXWfCF6qRK_42hxJ4pcdZDU,148
561
561
  wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py,sha256=fY3y2YWR9nY-KE8k078NszDsnwOgqOIjHUgLvQjDivU,822
562
562
  wbportfolio/viewsets/orders/configs/endpoints/orders.py,sha256=Iic9RWLHiP2zxq6xve99lwVYCSMLX4T2euS7cJh-uOQ,1088
@@ -567,7 +567,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
567
567
  wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
568
568
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
569
569
  wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
570
- wbportfolio-1.56.3.dist-info/METADATA,sha256=OBSV7sthodbAftQh-j9KrB9hzIFPcV1j5cLbX6tPwcE,751
571
- wbportfolio-1.56.3.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
572
- wbportfolio-1.56.3.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
573
- wbportfolio-1.56.3.dist-info/RECORD,,
570
+ wbportfolio-1.56.5.dist-info/METADATA,sha256=RROv2cPMXS4c5q03yrXpQTM5ncGQRYdjH8PR-i62Hfw,751
571
+ wbportfolio-1.56.5.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
572
+ wbportfolio-1.56.5.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
573
+ wbportfolio-1.56.5.dist-info/RECORD,,