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

@@ -85,33 +85,38 @@ class PositionDictConverter:
85
85
  self.fx_rates[currency][val_date] = fx_rate
86
86
  return fx_rate
87
87
 
88
- def dict_to_model(self, val_date: date, instrument_id: int, weighting: float) -> AssetPosition:
88
+ def dict_to_model(self, val_date: date, instrument_id: int, weighting: float) -> AssetPosition | None:
89
89
  underlying_quote = self._get_instrument(instrument_id)
90
90
  currency_fx_rate_portfolio_to_usd = self._get_fx_rate(val_date, self.portfolio.currency)
91
91
  currency_fx_rate_instrument_to_usd = self._get_fx_rate(val_date, underlying_quote.currency)
92
- position = AssetPosition(
93
- underlying_quote=underlying_quote,
94
- weighting=weighting,
95
- date=val_date,
96
- asset_valuation_date=val_date,
97
- is_estimated=True,
98
- portfolio=self.portfolio,
99
- currency=underlying_quote.currency,
100
- initial_price=self.prices.loc[pd.Timestamp(val_date), instrument_id],
101
- currency_fx_rate_portfolio_to_usd=currency_fx_rate_portfolio_to_usd,
102
- currency_fx_rate_instrument_to_usd=currency_fx_rate_instrument_to_usd,
103
- initial_currency_fx_rate=None,
104
- underlying_quote_price=None,
105
- underlying_instrument=None,
106
- )
107
- position.pre_save(
108
- infer_underlying_quote_price=self.infer_underlying_quote_price
109
- ) # inferring underlying quote price is potentially very slow for big dataset of positions, it's not very needed for model portfolio so we disable it
110
- return position
92
+ initial_price = self.prices.loc[pd.Timestamp(val_date), instrument_id]
93
+ # if we cannot find a price for the instrument, we return an empty position
94
+ if initial_price is not None:
95
+ position = AssetPosition(
96
+ underlying_quote=underlying_quote,
97
+ weighting=weighting,
98
+ date=val_date,
99
+ asset_valuation_date=val_date,
100
+ is_estimated=True,
101
+ portfolio=self.portfolio,
102
+ currency=underlying_quote.currency,
103
+ initial_price=self.prices.loc[pd.Timestamp(val_date), instrument_id],
104
+ currency_fx_rate_portfolio_to_usd=currency_fx_rate_portfolio_to_usd,
105
+ currency_fx_rate_instrument_to_usd=currency_fx_rate_instrument_to_usd,
106
+ initial_currency_fx_rate=None,
107
+ underlying_quote_price=None,
108
+ underlying_instrument=None,
109
+ )
110
+ position.pre_save(
111
+ infer_underlying_quote_price=self.infer_underlying_quote_price
112
+ ) # inferring underlying quote price is potentially very slow for big dataset of positions, it's not very needed for model portfolio so we disable it
113
+ return position
111
114
 
112
115
  def convert(self, positions: dict[date, dict[int, float]]) -> Iterable[AssetPosition]:
113
116
  for val_date, weights in positions.items():
114
- yield from map(lambda row: self.dict_to_model(val_date, row[0], row[1]), weights.items())
117
+ yield from filter(
118
+ lambda x: x, map(lambda row: self.dict_to_model(val_date, row[0], row[1]), weights.items())
119
+ )
115
120
 
116
121
 
117
122
  class DefaultPortfolioQueryset(QuerySet):
@@ -371,9 +376,8 @@ class Portfolio(DeleteToDisableMixin, WBModel):
371
376
  The instantiated analytic portfolio
372
377
  """
373
378
  weights = self.get_weights(val_date)
374
- instrument_ids = weights.keys()
375
379
  return_date = (val_date + BDay(1)).date()
376
- returns, prices = self.get_returns(instrument_ids, (val_date - BDay(2)).date(), return_date, **kwargs)
380
+ returns, prices = self.get_returns((val_date - BDay(2)).date(), return_date, **kwargs)
377
381
  if pd.Timestamp(return_date) not in returns.index:
378
382
  raise InvalidAnalyticPortfolio()
379
383
  returns = returns.fillna(0) # not sure this is what we want
@@ -753,7 +757,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
753
757
  next_trade_proposal = None
754
758
  rebalancing_date = None
755
759
  logger.info(f"compute next weights in batch for {self} from {start_date:%Y-%m-%d} to {end_date:%Y-%m-%d}")
756
- returns, prices = self.get_returns(weights.keys(), (start_date - BDay(3)).date(), end_date, ffill_returns=True)
760
+ returns, prices = self.get_returns((start_date - BDay(3)).date(), end_date, ffill_returns=True)
757
761
  for to_date_ts in pd.date_range(start_date + timedelta(days=1), end_date, freq="B"):
758
762
  to_date = to_date_ts.date()
759
763
  logger.info(f"Processing {to_date:%Y-%m-%d}")
@@ -767,7 +771,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
767
771
  positions[to_date] = weights
768
772
  except KeyError: # if no return for that date, we break and continue
769
773
  break
770
-
771
774
  positions_generator = PositionDictConverter(self, prices)
772
775
  positions = list(positions_generator.convert(positions))
773
776
  self.bulk_create_positions(positions, delete_leftovers=True, compute_metrics=False, evaluate_rebalancer=False)
@@ -1003,7 +1006,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
1003
1006
 
1004
1007
  def get_returns(
1005
1008
  self,
1006
- instruments: Iterable,
1007
1009
  from_date: date,
1008
1010
  to_date: date,
1009
1011
  ffill_returns: bool = True,
@@ -1013,13 +1015,17 @@ class Portfolio(DeleteToDisableMixin, WBModel):
1013
1015
  Utility methods to get instrument returns for a given date range
1014
1016
 
1015
1017
  Args:
1016
- instruments: instrument to get the returns from
1017
1018
  from_date: date range lower bound
1018
1019
  to_date: date range upper bound
1019
1020
 
1020
1021
  Returns:
1021
1022
  Return a tuple of the returns and the last prices series for conveniance
1022
1023
  """
1024
+ instruments = list(
1025
+ self.assets.filter(date__gte=from_date, date__lte=to_date)
1026
+ .values_list("underlying_quote", flat=True)
1027
+ .distinct("underlying_quote")
1028
+ )
1023
1029
  prices = InstrumentPrice.objects.filter(
1024
1030
  instrument__in=instruments, date__gte=from_date, date__lte=to_date
1025
1031
  ).annotate(
@@ -1044,7 +1050,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
1044
1050
  if ffill_returns:
1045
1051
  prices_df = prices_df.ffill()
1046
1052
  returns = prices_to_returns(prices_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
1047
- return returns.replace([np.inf, -np.inf, np.nan], 0), prices_df
1053
+ return returns.replace([np.inf, -np.inf, np.nan], 0), prices_df.replace([np.inf, -np.inf, np.nan], None)
1048
1054
 
1049
1055
  @classmethod
1050
1056
  def get_contribution_df(
@@ -1118,7 +1118,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1118
1118
  res = list(Portfolio.objects.all().to_dependency_iterator(weekday))
1119
1119
  assert res == [index_portfolio, dependency_portfolio, dependant_portfolio, undependant_portfolio]
1120
1120
 
1121
- def test_get_returns(self, instrument_factory, instrument_price_factory, portfolio):
1121
+ def test_get_returns(self, instrument_factory, instrument_price_factory, asset_position_factory, portfolio):
1122
1122
  v1 = date(2025, 1, 1)
1123
1123
  v2 = date(2025, 1, 2)
1124
1124
  v3 = date(2025, 1, 3)
@@ -1129,17 +1129,17 @@ class TestPortfolioModel(PortfolioTestMixin):
1129
1129
  i11 = instrument_price_factory.create(date=v1, instrument=i1)
1130
1130
  i12 = instrument_price_factory.create(date=v2, instrument=i1)
1131
1131
  i13 = instrument_price_factory.create(date=v3, instrument=i1)
1132
+ asset_position_factory.create(date=v1, portfolio=portfolio, underlying_instrument=i1)
1133
+ asset_position_factory.create(date=v3, portfolio=portfolio, underlying_instrument=i2)
1132
1134
  i11.refresh_from_db()
1133
1135
  i12.refresh_from_db()
1134
1136
  i13.refresh_from_db()
1135
- returns, _ = portfolio.get_returns(
1136
- [i1.id, i2.id], from_date=v1, to_date=v3, convert_to_portfolio_currency=False
1137
- )
1137
+ returns, _ = portfolio.get_returns(from_date=v1, to_date=v3, convert_to_portfolio_currency=False)
1138
1138
 
1139
1139
  expected_returns = pd.DataFrame(
1140
- [[i12.net_value / i11.net_value - 1], [i13.net_value / i12.net_value - 1]],
1140
+ [[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
1141
1141
  index=[v2, v3],
1142
- columns=[i1.id],
1142
+ columns=[i1.id, i2.id],
1143
1143
  dtype="float64",
1144
1144
  )
1145
1145
  expected_returns.index = pd.to_datetime(expected_returns.index)
@@ -1,11 +1,9 @@
1
1
  from wbcore.menus import ItemPermission, MenuItem
2
2
 
3
- from wbportfolio.permissions import is_portfolio_manager
4
-
5
3
  PortfolioMenuItem = MenuItem(
6
4
  label="Portfolios",
7
5
  endpoint="wbportfolio:portfolio-list",
8
- permission=ItemPermission(method=is_portfolio_manager, permissions=["wbportfolio.view_portfolio"]),
6
+ permission=ItemPermission(permissions=["wbportfolio.view_portfolio"]),
9
7
  )
10
8
 
11
9
 
@@ -13,5 +11,5 @@ ModelPortfolioMenuItem = MenuItem(
13
11
  label="Managed Portfolios",
14
12
  endpoint="wbportfolio:portfolio-list",
15
13
  endpoint_get_parameters={"is_manageable": True},
16
- permission=ItemPermission(method=is_portfolio_manager, permissions=["wbportfolio.view_portfolio"]),
14
+ permission=ItemPermission(permissions=["wbportfolio.view_portfolio"]),
17
15
  )
@@ -411,7 +411,7 @@ class TradeTradeProposalModelViewSet(
411
411
  return TradeTradeProposalModelSerializer
412
412
 
413
413
  def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
414
- if queryset is not None:
414
+ if queryset is not None and queryset.exists():
415
415
  total_target_weight = queryset.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
416
416
  if round(total_target_weight, 3) != 1:
417
417
  warning(request, "The total target weight does not equals to 1")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.46.14
3
+ Version: 1.47.1
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -247,7 +247,7 @@ wbportfolio/models/asset.py,sha256=TEOsSlbog6Yw0PZiMxjnCccp6ApXAAJNd3BIx_wU7D8,3
247
247
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
248
248
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
249
249
  wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
250
- wbportfolio/models/portfolio.py,sha256=xDT-XL7ZtAN9JD5BXOv6l98fnvu_YXqXbO-ZCTipWWg,58028
250
+ wbportfolio/models/portfolio.py,sha256=rpo5jUTbwrlD_Zx9_LYTy5XNgk73PTRX3yn7j8AG9DE,58447
251
251
  wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
252
252
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
253
253
  wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
@@ -372,7 +372,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
372
372
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
373
373
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
374
374
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
375
- wbportfolio/tests/models/test_portfolios.py,sha256=ZXFqvCI2ozJmVTPmhtAEVaBaTz2BW73re1afsmKXJNc,52003
375
+ wbportfolio/tests/models/test_portfolios.py,sha256=idIUT2kc6Fte-bzwOS9NtOYRVnwTBijSo7deHkRon_k,52194
376
376
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
377
377
  wbportfolio/tests/models/test_products.py,sha256=5YYmQreFnaKLbWmrSib103wgLalqn8u01Fnh3A0XMz8,8217
378
378
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -480,7 +480,7 @@ wbportfolio/viewsets/configs/menu/custodians.py,sha256=W01URxeqDzcu--5R51QCBzbhS
480
480
  wbportfolio/viewsets/configs/menu/fees.py,sha256=iYAMzavfTKSYo6CBzVrSggxl7kKwE1h_jHGCgV4pYm8,499
481
481
  wbportfolio/viewsets/configs/menu/instrument_prices.py,sha256=9bYMHq7AoPnaqdZ_6w3sEpk4X8Ptr_RnTlwxoE8lVow,358
482
482
  wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py,sha256=JqTtceY6cH0fldMMT4AbEtrG855LWSJn4SG45-2-xec,343
483
- wbportfolio/viewsets/configs/menu/portfolios.py,sha256=R3Ppz_Q1jpkig0O2js-gonPyvwAEWz7n66JvjNlIS-A,583
483
+ wbportfolio/viewsets/configs/menu/portfolios.py,sha256=ezR5a1gwAbjsdNV9YsfZ-c1f_oRFHpFhRQlU2x-VPV8,467
484
484
  wbportfolio/viewsets/configs/menu/positions.py,sha256=ejq6ZMcaYptogh8UMX-jwRdxXi8I2MsCcanYJanxesI,608
485
485
  wbportfolio/viewsets/configs/menu/product_groups.py,sha256=c8DK7nW81NCMCdktLyiFmWlIJ7M0q15D_vfrDaREM4U,367
486
486
  wbportfolio/viewsets/configs/menu/product_performance.py,sha256=aZ72NGMVuRmARwUB4Z3fGtk15_2RVzw9DzW-rYUbc6k,947
@@ -516,9 +516,9 @@ wbportfolio/viewsets/transactions/fees.py,sha256=7VUXIogmRrXCz_D9tvDiiTae0t5j09W
516
516
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
517
517
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
518
518
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=gXdJJ00D6UavZfBczvZQb5cYPQlU6-xOg_-TnMBzEO0,4742
519
- wbportfolio/viewsets/transactions/trades.py,sha256=mo5b1wFm0twvGVp-CYnzpGLYMqPcHN8GjH4G_WwFFwc,16237
519
+ wbportfolio/viewsets/transactions/trades.py,sha256=E0QV6oPohOgeVWzNT6xfWOFvTB4KoP2DzDYnGtm3Hz0,16259
520
520
  wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
521
- wbportfolio-1.46.14.dist-info/METADATA,sha256=q0SRunfArVhuy4pL69WWm4X8C_Qoc7zIz4jL0wTEmyE,735
522
- wbportfolio-1.46.14.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
523
- wbportfolio-1.46.14.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
524
- wbportfolio-1.46.14.dist-info/RECORD,,
521
+ wbportfolio-1.47.1.dist-info/METADATA,sha256=vtVwamOZDhra4lfIxs9TrSKi-uf9devn_BUlO9ZruD8,734
522
+ wbportfolio-1.47.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
523
+ wbportfolio-1.47.1.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
524
+ wbportfolio-1.47.1.dist-info/RECORD,,