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

@@ -88,7 +88,7 @@ class AssetPositionIterator:
88
88
  # Initialize data stores with type hints
89
89
  self._instruments = {}
90
90
  self._fx_rates = defaultdict(dict)
91
- self._weights = dict()
91
+ self._weights = defaultdict(dict)
92
92
  self._prices = prices or defaultdict(dict)
93
93
  self.positions = dict()
94
94
 
@@ -121,7 +121,6 @@ class AssetPositionIterator:
121
121
  currency_fx_rate_portfolio_to_usd = self._get_fx_rate(val_date, self.portfolio.currency)
122
122
  currency_fx_rate_instrument_to_usd = self._get_fx_rate(val_date, underlying_quote.currency)
123
123
  price = self._get_price(val_date, underlying_quote)
124
-
125
124
  position = AssetPosition(
126
125
  underlying_quote=underlying_quote,
127
126
  weighting=weighting,
@@ -173,10 +172,8 @@ class AssetPositionIterator:
173
172
  position.portfolio = self.portfolio
174
173
  if position.initial_price is not None:
175
174
  self.positions[key] = position
176
- try:
177
- self._weights[position.date][position.underlying_quote.id] += float(position.weighting)
178
- except KeyError:
179
- self._weights[position.date] = {position.underlying_quote.id: float(position.weighting)}
175
+ self._weights[position.date][position.underlying_quote.id] = float(position.weighting)
176
+
180
177
  return self
181
178
 
182
179
  def get_dates(self) -> list[date]:
@@ -185,7 +182,7 @@ class AssetPositionIterator:
185
182
 
186
183
  def get_weights(self) -> dict[date, dict[int, float]]:
187
184
  """Get weight structure with instrument IDs as keys"""
188
- return self._weights
185
+ return dict(self._weights)
189
186
 
190
187
  def __iter__(self):
191
188
  # return an iterable excluding the position without a valid weighting
@@ -54,7 +54,24 @@ if TYPE_CHECKING:
54
54
  from wbportfolio.models.transactions.trade_proposals import TradeProposal
55
55
 
56
56
 
57
- def to_dict(df: pd.DataFrame) -> dict[date, dict[int, float]]:
57
+ def get_prices(instrument_ids: list[int], from_date: date, to_date: date) -> dict[date, dict[int, float]]:
58
+ """
59
+ Utility to fetch raw prices
60
+ """
61
+ prices = InstrumentPrice.objects.filter(instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date)
62
+ df = (
63
+ pd.DataFrame(
64
+ prices.filter_only_valid_prices().values_list("instrument", "net_value", "date"),
65
+ columns=["instrument", "net_value", "date"],
66
+ )
67
+ .pivot_table(index="date", values="net_value", columns="instrument")
68
+ .astype(float)
69
+ .sort_index()
70
+ )
71
+ ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
72
+ df = df.reindex(ts)
73
+ df = df.ffill()
74
+ df.index = pd.to_datetime(df.index)
58
75
  return {ts.date(): row for ts, row in df.to_dict("index").items()}
59
76
 
60
77
 
@@ -64,7 +81,7 @@ def get_returns(
64
81
  to_date: date,
65
82
  to_currency: Currency | None = None,
66
83
  ffill_returns: bool = True,
67
- ) -> tuple[pd.DataFrame, pd.DataFrame]:
84
+ ) -> pd.DataFrame:
68
85
  """
69
86
  Utility methods to get instrument returns for a given date range
70
87
 
@@ -99,7 +116,7 @@ def get_returns(
99
116
  prices_df = prices_df.ffill()
100
117
  prices_df.index = pd.to_datetime(prices_df.index)
101
118
  returns = prices_to_returns(prices_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
102
- return returns.replace([np.inf, -np.inf, np.nan], 0), prices_df.replace([np.inf, -np.inf, np.nan], None)
119
+ return returns.replace([np.inf, -np.inf, np.nan], 0)
103
120
 
104
121
 
105
122
  class DefaultPortfolioQueryset(QuerySet):
@@ -364,7 +381,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
364
381
 
365
382
  def get_analytic_portfolio(
366
383
  self, val_date: date, weights: dict[int, float] | None = None, **kwargs
367
- ) -> tuple[AnalyticPortfolio, pd.DataFrame]:
384
+ ) -> AnalyticPortfolio:
368
385
  """
369
386
  Return the analytic portfolio associated with this portfolio at the given date
370
387
 
@@ -378,18 +395,15 @@ class Portfolio(DeleteToDisableMixin, WBModel):
378
395
  if not weights:
379
396
  weights = self.get_weights(val_date)
380
397
  return_date = (val_date + BDay(1)).date()
381
- returns, prices = get_returns(
398
+ returns = get_returns(
382
399
  list(weights.keys()), (val_date - BDay(2)).date(), return_date, to_currency=self.currency, **kwargs
383
400
  )
384
401
  if pd.Timestamp(return_date) not in returns.index:
385
402
  raise InvalidAnalyticPortfolio()
386
403
  returns = returns.fillna(0) # not sure this is what we want
387
- return (
388
- AnalyticPortfolio(
389
- X=returns,
390
- weights=weights,
391
- ),
392
- prices,
404
+ return AnalyticPortfolio(
405
+ X=returns,
406
+ weights=weights,
393
407
  )
394
408
 
395
409
  def is_invested_at_date(self, val_date: date) -> bool:
@@ -781,15 +795,18 @@ class Portfolio(DeleteToDisableMixin, WBModel):
781
795
  weights = drifted_positions.get_weights()[start_date]
782
796
 
783
797
  # Get returns and prices data for the whole date range
784
- returns, prices = get_returns(
785
- list(weights.keys()),
798
+ instrument_ids = list(weights.keys())
799
+ returns = get_returns(
800
+ instrument_ids,
786
801
  (start_date - BDay(3)).date(),
787
802
  end_date,
788
803
  to_currency=self.currency,
789
804
  ffill_returns=True,
790
805
  )
806
+ # Get raw prices to speed up asset position creation
807
+ prices = get_prices(instrument_ids, (start_date - BDay(3)).date(), end_date)
791
808
  # Instantiate the position iterator with the initial weights
792
- positions = AssetPositionIterator(self, prices=to_dict(prices))
809
+ positions = AssetPositionIterator(self, prices=prices)
793
810
  last_trade_proposal = None
794
811
  for to_date_ts in pd.date_range(start_date + timedelta(days=1), end_date, freq="B"):
795
812
  to_date = to_date_ts.date()
@@ -883,10 +900,8 @@ class Portfolio(DeleteToDisableMixin, WBModel):
883
900
 
884
901
  setattr(position, "path", path)
885
902
  position.initial_shares = None
886
- if portfolio_total_asset_value:
887
- position.initial_shares = (position.weighting * portfolio_total_asset_value) / (
888
- position.price * position.currency_fx_rate
889
- )
903
+ if portfolio_total_asset_value and (price_fx_portfolio := position.price * position.currency_fx_rate):
904
+ position.initial_shares = (position.weighting * portfolio_total_asset_value) / price_fx_portfolio
890
905
  if child_portfolio := position.underlying_quote.primary_portfolio:
891
906
  if with_intermediary_position:
892
907
  yield position
@@ -1191,7 +1206,7 @@ def default_estimate_net_value(
1191
1206
  with suppress(
1192
1207
  IndexError, InvalidAnalyticPortfolio
1193
1208
  ): # we silent any indexerror introduced by no returns for the past days
1194
- analytic_portfolio, _ = portfolio.get_analytic_portfolio(previous_val_date, weights=weights)
1209
+ analytic_portfolio = portfolio.get_analytic_portfolio(previous_val_date, weights=weights)
1195
1210
  return analytic_portfolio.get_estimate_net_value(float(last_price.net_value))
1196
1211
 
1197
1212
 
@@ -925,7 +925,7 @@ class TestPortfolioModel(PortfolioTestMixin):
925
925
  )
926
926
  a2.refresh_from_db()
927
927
 
928
- analytic_portfolio, _ = portfolio.get_analytic_portfolio(weekday)
928
+ analytic_portfolio = portfolio.get_analytic_portfolio(weekday)
929
929
  assert analytic_portfolio.weights.tolist() == [float(a1.weighting), float(a2.weighting)]
930
930
  expected_X = pd.DataFrame(
931
931
  [[float(p11.net_value / p10.net_value - Decimal(1)), float(p21.net_value / p20.net_value - Decimal(1))]],
@@ -1139,7 +1139,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1139
1139
  i11.refresh_from_db()
1140
1140
  i12.refresh_from_db()
1141
1141
  i13.refresh_from_db()
1142
- returns, _ = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1142
+ returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1143
1143
 
1144
1144
  expected_returns = pd.DataFrame(
1145
1145
  [[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.49.10
3
+ Version: 1.49.11
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -245,11 +245,11 @@ wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py,sha256=4
245
245
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
246
246
  wbportfolio/models/__init__.py,sha256=IIS_PNRxyX2Dcvyk1bcQOUzFt0B9SPC0WlM88CXqj04,881
247
247
  wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
248
- wbportfolio/models/asset.py,sha256=5GHcT8vbqAAQqWmGmA8g01H4TZ5_ML9hT5jNpgXrRUs,45304
248
+ wbportfolio/models/asset.py,sha256=-bw8AoIIBy68je1vnGXYbkb7Vdp_mRBNJixd0qnCWxA,45153
249
249
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
250
250
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
251
251
  wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
252
- wbportfolio/models/portfolio.py,sha256=pH-qZ3xgwC27Lg7-tAE9XZgPIyDEhMcT1CQV-dP70js,55057
252
+ wbportfolio/models/portfolio.py,sha256=Rq9ih9aPhLC1RukrIF_wrmtRQm0-3w4i9NEtWJGZ02g,55742
253
253
  wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
254
254
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
255
255
  wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
@@ -374,7 +374,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
374
374
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
375
375
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
376
376
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
377
- wbportfolio/tests/models/test_portfolios.py,sha256=UoR9rmojIHNjOo770hKVTXA4bf06Yj5l06JhFt8R1MA,53138
377
+ wbportfolio/tests/models/test_portfolios.py,sha256=2VqUF3HALY1VvCksARk4XJlQL-qr2PbF2DMmw9KQzdY,53132
378
378
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
379
379
  wbportfolio/tests/models/test_products.py,sha256=FkQLms3kXzyg6mNEEJcHUVKu8YbATY9l6lZyaRUnpjw,9314
380
380
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -521,7 +521,7 @@ wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQV
521
521
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=iQpC_Thbj56SmM05vPRsF1JZguGBDaTUH3I-_iCHCV0,5958
522
522
  wbportfolio/viewsets/transactions/trades.py,sha256=-yJ4j8NJTu2VWyhCq5BXGNND_925Ietoxx9k07SLVh0,21634
523
523
  wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
524
- wbportfolio-1.49.10.dist-info/METADATA,sha256=VBzotvqIdSqWY0CbQ_sobO0hiQPjSs8Y8uw8C5v4chE,735
525
- wbportfolio-1.49.10.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
526
- wbportfolio-1.49.10.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
527
- wbportfolio-1.49.10.dist-info/RECORD,,
524
+ wbportfolio-1.49.11.dist-info/METADATA,sha256=vbQQ5UCTEyqLH-YcOOF6BVCDXb3ujdegBOUX6k_iQ-8,735
525
+ wbportfolio-1.49.11.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
526
+ wbportfolio-1.49.11.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
527
+ wbportfolio-1.49.11.dist-info/RECORD,,