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

@@ -11,15 +11,7 @@ from celery import shared_task
11
11
  from django.contrib.contenttypes.models import ContentType
12
12
  from django.contrib.postgres.fields import DateRangeField
13
13
  from django.db import models
14
- from django.db.models import (
15
- Exists,
16
- F,
17
- OuterRef,
18
- Q,
19
- QuerySet,
20
- Sum,
21
- Value,
22
- )
14
+ from django.db.models import Case, Exists, F, OuterRef, Q, QuerySet, Sum, Value, When
23
15
  from django.db.models.functions import Coalesce
24
16
  from django.db.models.signals import post_save
25
17
  from django.dispatch import receiver
@@ -64,7 +56,6 @@ def get_returns(
64
56
  from_date: date,
65
57
  to_date: date,
66
58
  to_currency: Currency | None = None,
67
- ffill_returns: bool = True,
68
59
  use_dl: bool = False,
69
60
  ) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
70
61
  """
@@ -74,7 +65,6 @@ def get_returns(
74
65
  from_date: date range lower bound
75
66
  to_date: date range upper bound
76
67
  to_currency: currency to use for returns
77
- ffill_returns: whether to ffill returns and prices
78
68
  use_dl: whether to get data straight from the dataloader or use the internal table
79
69
 
80
70
  Returns:
@@ -96,14 +86,14 @@ def get_returns(
96
86
  )
97
87
  else:
98
88
  fx_rate = Value(Decimal("1"))
89
+ # annotate fx rate only if the price is not calculated, in that case we assume the instrument is not tradable and we set a forex of None (to be fast forward filled)
99
90
  prices = InstrumentPrice.objects.filter(
100
91
  instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
101
- ).annotate(fx_rate=fx_rate)
92
+ ).annotate(fx_rate=Case(When(calculated=False, then=fx_rate), default=None))
102
93
  df = pd.DataFrame(
103
94
  prices.filter_only_valid_prices().values_list("instrument", "fx_rate", "net_value", "date"),
104
95
  columns=["instrument_id", "fx_rate", "close", "valuation_date"],
105
96
  )
106
-
107
97
  if df.empty:
108
98
  raise InvalidAnalyticPortfolio()
109
99
  df = (
@@ -113,12 +103,15 @@ def get_returns(
113
103
  )
114
104
  ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
115
105
  df = df.reindex(ts)
116
- if ffill_returns:
117
- df = df.ffill()
106
+ df = df.ffill()
118
107
  df.index = pd.to_datetime(df.index)
119
108
  prices_df = df["close"]
120
- fx_rate_df = df["fx_rate"]
121
- returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
109
+ if "fx_rate" in df.columns:
110
+ fx_rate_df = df["fx_rate"].fillna(1.0)
111
+ else:
112
+ fx_rate_df = pd.DataFrame(np.ones(prices_df.shape), index=prices_df.index, columns=prices_df.columns)
113
+
114
+ returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=True)
122
115
  return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
123
116
  [np.inf, -np.inf, np.nan], 0
124
117
  )
@@ -358,11 +351,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
358
351
  instrument.delisted_date = date.today() - timedelta(days=1)
359
352
  instrument.save()
360
353
 
361
- def _build_dto(self, val_date: date, include_drift_factor: bool = False, **extra_kwargs) -> PortfolioDTO:
354
+ def _build_dto(self, val_date: date, **extra_kwargs) -> PortfolioDTO:
362
355
  "returns the dto representation of this portfolio at the specified date"
363
356
  assets = self.assets.filter(date=val_date, **extra_kwargs)
364
357
  try:
365
- drifted_weights = self.get_analytic_portfolio(val_date).get_next_weights()
358
+ drifted_weights = self.get_analytic_portfolio(val_date, use_dl=True).get_next_weights()
366
359
  except InvalidAnalyticPortfolio:
367
360
  drifted_weights = {}
368
361
  positions = []
@@ -397,7 +390,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
397
390
  )
398
391
 
399
392
  def get_analytic_portfolio(
400
- self, val_date: date, weights: dict[int, float] | None = None, **kwargs
393
+ self, val_date: date, weights: dict[int, float] | None = None, use_dl: bool = False, **kwargs
401
394
  ) -> AnalyticPortfolio:
402
395
  """
403
396
  Return the analytic portfolio associated with this portfolio at the given date
@@ -417,6 +410,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
417
410
  (val_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
418
411
  return_date,
419
412
  to_currency=self.currency,
413
+ use_dl=use_dl,
420
414
  **kwargs,
421
415
  )
422
416
  if pd.Timestamp(return_date) not in returns.index:
@@ -842,7 +836,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
842
836
  (start_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
843
837
  end_date,
844
838
  to_currency=self.currency,
845
- ffill_returns=True,
846
839
  use_dl=True,
847
840
  )
848
841
  # Get raw prices to speed up asset position creation
@@ -196,7 +196,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
196
196
  previous_weights = dict(map(lambda r: (r[0].id, float(r[1]["weighting"])), portfolio.items()))
197
197
  try:
198
198
  drifted_weights = self.portfolio.get_analytic_portfolio(
199
- self.value_date, weights=previous_weights
199
+ self.value_date, weights=previous_weights, use_dl=True
200
200
  ).get_next_weights()
201
201
  except InvalidAnalyticPortfolio:
202
202
  drifted_weights = {}
@@ -1169,6 +1169,47 @@ class TestPortfolioModel(PortfolioTestMixin):
1169
1169
  assert prices[v3][i2.id] == float(i21.net_value)
1170
1170
  assert prices[v4][i2.id] == float(i21.net_value)
1171
1171
 
1172
+ def test_get_returns_fix_forex_on_holiday(
1173
+ self, instrument, instrument_price_factory, currency_fx_rates_factory, currency_factory
1174
+ ):
1175
+ v1 = date(2024, 12, 31)
1176
+ v2 = date(2025, 1, 1)
1177
+ v3 = date(2025, 1, 2)
1178
+
1179
+ target_currency = currency_factory.create()
1180
+ fx_target1 = currency_fx_rates_factory.create(currency=target_currency, date=v1)
1181
+ fx_target2 = currency_fx_rates_factory.create(currency=target_currency, date=v2) # noqa
1182
+ fx_target3 = currency_fx_rates_factory.create(currency=target_currency, date=v3)
1183
+
1184
+ fx1 = currency_fx_rates_factory.create(currency=instrument.currency, date=v1)
1185
+ fx2 = currency_fx_rates_factory.create(currency=instrument.currency, date=v2) # noqa
1186
+ fx3 = currency_fx_rates_factory.create(currency=instrument.currency, date=v3)
1187
+
1188
+ i1 = instrument_price_factory.create(net_value=Decimal("100"), date=v1, instrument=instrument)
1189
+ i2 = instrument_price_factory.create(net_value=Decimal("100"), date=v2, instrument=instrument, calculated=True)
1190
+ i3 = instrument_price_factory.create(net_value=Decimal("200"), date=v3, instrument=instrument)
1191
+
1192
+ prices, returns = get_returns([instrument.id], from_date=v1, to_date=v3, to_currency=target_currency)
1193
+ returns.index = pd.to_datetime(returns.index)
1194
+ assert prices[v1][instrument.id] == float(i1.net_value)
1195
+ assert prices[v2][instrument.id] == float(i2.net_value)
1196
+ assert prices[v3][instrument.id] == float(i3.net_value)
1197
+
1198
+ assert returns.loc[pd.Timestamp(v2), instrument.id] == pytest.approx(
1199
+ float(
1200
+ (i2.net_value * fx_target1.value / fx1.value) / (i1.net_value * fx_target1.value / fx1.value)
1201
+ - Decimal("1")
1202
+ ),
1203
+ abs=10e-8,
1204
+ ) # as v2 as a calculated price, the forex won't apply to it
1205
+ assert returns.loc[pd.Timestamp(v3), instrument.id] == pytest.approx(
1206
+ float(
1207
+ (i3.net_value * fx_target3.value / fx3.value) / (i2.net_value * fx_target1.value / fx1.value)
1208
+ - Decimal("1")
1209
+ ),
1210
+ abs=10e-8,
1211
+ )
1212
+
1172
1213
  @patch.object(Portfolio, "compute_lookthrough", autospec=True)
1173
1214
  def test_handle_controlling_portfolio_change_at_date(self, mock_compute_lookthrough, weekday, portfolio_factory):
1174
1215
  primary_portfolio = portfolio_factory.create(only_weighting=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.54.11
3
+ Version: 1.54.13
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -258,7 +258,7 @@ wbportfolio/models/asset.py,sha256=b0vPt4LwNrxcMiK7UmBKViYnbNNlZzPTagvU5vFuyrc,4
258
258
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
259
259
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
260
260
  wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
261
- wbportfolio/models/portfolio.py,sha256=5gOPWWv-_ke-p-B14S9ZNKRvvGQVHNBzR2r5xL-AKm0,58341
261
+ wbportfolio/models/portfolio.py,sha256=5CddquPvVp-ImPlteNMqwDeY_mCFwdWMy7ynMRDDxak,58578
262
262
  wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
263
263
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
264
264
  wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
@@ -285,7 +285,7 @@ wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWK
285
285
  wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
286
286
  wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
287
287
  wbportfolio/models/transactions/rebalancing.py,sha256=rwePcmTZOYgfSWnBQcBrZ3DQHRJ3w17hdO_hgrRbbhI,7696
288
- wbportfolio/models/transactions/trade_proposals.py,sha256=Iilb67WdUh4XuBhxMqnnxTj_43NxTloGNNtJQh1izD8,38327
288
+ wbportfolio/models/transactions/trade_proposals.py,sha256=1xOxHVOUwgTKCcY7mN4u5SoFtdJrQkNg6xA3O2-f4Yw,38340
289
289
  wbportfolio/models/transactions/trades.py,sha256=1gmAdavuWu1Iko90s9prMxsK_NuDKIUBIKMDuHiKzow,34176
290
290
  wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
291
291
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -381,7 +381,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
381
381
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
382
382
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
383
383
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
384
- wbportfolio/tests/models/test_portfolios.py,sha256=sWbt9NE8Cludu7hUhxxnIJwOSQ_U_yC6UPFPkbpV8Qk,54010
384
+ wbportfolio/tests/models/test_portfolios.py,sha256=H3mgrQLdTkrljFZgJLRTXuN6J8fasRtlYoaRCnirdwU,56165
385
385
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
386
386
  wbportfolio/tests/models/test_products.py,sha256=IcBzw9hrGiWFMRwPBTMukCMWrhqnjOVA2hhb90xYOW8,9580
387
387
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -523,7 +523,7 @@ wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIH
523
523
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
524
524
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=kQCojTNKBEyn2NcenL3a9auzBH4sIgLEx8rLAYCGLGg,6161
525
525
  wbportfolio/viewsets/transactions/trades.py,sha256=Y8v2cM0vpspHysaAvu8qqhzt86dNtb2Q3puo4HCJsTI,22629
526
- wbportfolio-1.54.11.dist-info/METADATA,sha256=PhblDUHcEGHQmY_KCEcvlc3oIl98hhageKfJGRshiPg,703
527
- wbportfolio-1.54.11.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
528
- wbportfolio-1.54.11.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
529
- wbportfolio-1.54.11.dist-info/RECORD,,
526
+ wbportfolio-1.54.13.dist-info/METADATA,sha256=_ndN0PmfKx6Ro3nobejfixoQ24CM1gUlhn8OkG_vsX4,703
527
+ wbportfolio-1.54.13.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
528
+ wbportfolio-1.54.13.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
529
+ wbportfolio-1.54.13.dist-info/RECORD,,