wbportfolio 1.54.8__py2.py3-none-any.whl → 1.54.10__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,6 +88,8 @@ class TradeImportHandler(ImportExportHandler):
88
88
  1 / (math.pow(10, 4))
89
89
  ) # we need that convertion mechanism otherwise there is floating point approximation error while casting to decimal and get_instance does not work as expected
90
90
  data[field.name] = Decimal(value).quantize(Decimal(str(q)))
91
+ if (target_weight := data.pop("target_weight", None)) is not None:
92
+ data["_target_weight"] = target_weight
91
93
 
92
94
  def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
93
95
  if "transaction_date" not in data: # we might get only book date and not transaction date
@@ -158,9 +160,13 @@ class TradeImportHandler(ImportExportHandler):
158
160
  self.import_source.log += "\nNo trade was successfully matched."
159
161
 
160
162
  def _get_history(self, history: Dict[str, Any]) -> models.QuerySet:
163
+ from wbportfolio.models.transactions.trade_proposals import TradeProposal
164
+
161
165
  if trade_proposal_id := history.get("trade_proposal_id"):
162
166
  # if a trade proposal is provided, we delete the existing history first as otherwise, it would mess with the target weight computation
163
- self.model.objects.filter(trade_proposal_id=trade_proposal_id).delete()
167
+ trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
168
+ trade_proposal.trades.all().delete()
169
+ trade_proposal.reset_trades()
164
170
  trades = self.model.objects.none()
165
171
  else:
166
172
  trades = self.model.objects.filter(
@@ -54,6 +54,8 @@ logger = logging.getLogger("pms")
54
54
  if TYPE_CHECKING:
55
55
  from wbportfolio.models.transactions.trade_proposals import TradeProposal
56
56
 
57
+ MARKET_HOLIDAY_MAX_DURATION = 15
58
+
57
59
 
58
60
  def get_returns(
59
61
  instrument_ids: list[int],
@@ -94,22 +96,20 @@ def get_returns(
94
96
  df = pd.DataFrame(Instrument.objects.filter(id__in=instrument_ids).dl.market_data(**kwargs))
95
97
  if df.empty:
96
98
  raise InvalidAnalyticPortfolio()
97
- fx_rate_df = df[["instrument_id", "fx_rate", "valuation_date"]].pivot(
98
- index="valuation_date", columns="instrument_id", values="fx_rate"
99
+ df = df[["instrument_id", "fx_rate", "close", "valuation_date"]].pivot(
100
+ index="valuation_date", columns="instrument_id", values=["fx_rate", "close"]
99
101
  )
102
+ ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
103
+ df = df.reindex(ts)
104
+ if ffill_returns:
105
+ df = df.ffill()
106
+ df.index = pd.to_datetime(df.index)
100
107
 
101
- prices_df = df[["instrument_id", "close", "valuation_date"]].pivot(
102
- index="valuation_date", columns="instrument_id", values="close"
103
- )
104
- price_fx_portfolio_df = fx_rate_df * prices_df
108
+ prices_df = df["close"]
109
+ fx_rate_df = df["fx_rate"]
105
110
 
106
- ts = pd.bdate_range(price_fx_portfolio_df.index.min(), price_fx_portfolio_df.index.max(), freq="B")
107
- price_fx_portfolio_df = price_fx_portfolio_df.reindex(ts)
108
- if ffill_returns:
109
- price_fx_portfolio_df = price_fx_portfolio_df.ffill()
110
- price_fx_portfolio_df.index = pd.to_datetime(price_fx_portfolio_df.index)
111
- returns = prices_to_returns(price_fx_portfolio_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
112
- return {dt: row for dt, row in prices_df.replace(np.nan, None).to_dict("index").items()}, returns.replace(
111
+ returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
112
+ return {ts.date(): row for ts, row in prices_df.replace(np.nan, None).to_dict("index").items()}, returns.replace(
113
113
  [np.inf, -np.inf, np.nan], 0
114
114
  )
115
115
 
@@ -403,7 +403,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
403
403
  weights = self.get_weights(val_date)
404
404
  return_date = (val_date + BDay(1)).date()
405
405
  _, returns = get_returns(
406
- list(weights.keys()), (val_date - BDay(2)).date(), return_date, to_currency=self.currency, **kwargs
406
+ list(weights.keys()),
407
+ (val_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
408
+ return_date,
409
+ to_currency=self.currency,
410
+ **kwargs,
407
411
  )
408
412
  if pd.Timestamp(return_date) not in returns.index:
409
413
  raise InvalidAnalyticPortfolio()
@@ -825,7 +829,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
825
829
 
826
830
  prices, returns = get_returns(
827
831
  instrument_ids,
828
- (start_date - BDay(3)).date(),
832
+ (start_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
829
833
  end_date,
830
834
  to_currency=self.currency,
831
835
  ffill_returns=True,
@@ -397,7 +397,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
397
397
  if approve_automatically and self.portfolio.can_be_rebalanced:
398
398
  self.approve(replay=False, broadcast_changes_at_date=broadcast_changes_at_date)
399
399
 
400
- def replay(self, force_reset_trade: bool = False, broadcast_changes_at_date: bool = True):
400
+ def replay(self, broadcast_changes_at_date: bool = True):
401
401
  last_trade_proposal = self
402
402
  last_trade_proposal_created = False
403
403
  while last_trade_proposal and last_trade_proposal.status == TradeProposal.Status.APPROVED:
@@ -405,7 +405,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
405
405
  logger.info(f"Replaying trade proposal {last_trade_proposal}")
406
406
  last_trade_proposal.approve_workflow(
407
407
  silent_exception=True,
408
- force_reset_trade=force_reset_trade,
408
+ force_reset_trade=True,
409
409
  broadcast_changes_at_date=broadcast_changes_at_date,
410
410
  )
411
411
  last_trade_proposal.save()
@@ -803,9 +803,8 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
803
803
  ) # we delete the existing portfolio as it has been reverted
804
804
  for trade in self.trades.all():
805
805
  trade.status = Trade.Status.DRAFT
806
- trade.drift_factor = Decimal("1")
807
806
  trades.append(trade)
808
- Trade.objects.bulk_update(trades, ["status", "drift_factor"])
807
+ Trade.objects.bulk_update(trades, ["status"])
809
808
 
810
809
  def can_revert(self):
811
810
  errors = dict()
@@ -439,7 +439,7 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
439
439
  method=RequestType.PATCH,
440
440
  identifiers=("wbportfolio:trade",),
441
441
  icon=WBIcon.UNDO.icon,
442
- key="reverte",
442
+ key="revert",
443
443
  label="Revert",
444
444
  action_label="revert",
445
445
  # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
@@ -518,6 +518,11 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
518
518
  self, "target_weight", round(self._effective_weight + self.weighting, self.TRADE_WEIGHTING_PRECISION)
519
519
  )
520
520
 
521
+ @_target_weight.setter
522
+ def _target_weight(self, target_weight):
523
+ self.weighting = Decimal(target_weight) - self._effective_weight
524
+ self._set_type()
525
+
521
526
  @property
522
527
  @admin.display(description="Target Shares")
523
528
  def _target_shares(self) -> Decimal:
@@ -558,12 +563,6 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
558
563
  ]
559
564
  # notification_email_template = "portfolio/email/trade_notification.html"
560
565
 
561
- def __init__(self, *args, target_weight: Decimal | None = None, **kwargs):
562
- super().__init__(*args, **kwargs)
563
- if target_weight is not None: # if target weight is provided, we guess the corresponding weighting
564
- self.weighting = Decimal(target_weight) - self._effective_weight
565
- self._set_type()
566
-
567
566
  def save(self, *args, **kwargs):
568
567
  if abs(self.weighting) < 10e-6:
569
568
  self.weighting = Decimal("0")
@@ -1134,31 +1134,40 @@ class TestPortfolioModel(PortfolioTestMixin):
1134
1134
  assert res == [index_portfolio, dependency_portfolio, dependant_portfolio, undependant_portfolio]
1135
1135
 
1136
1136
  def test_get_returns(self, instrument_factory, instrument_price_factory, asset_position_factory, portfolio):
1137
- v1 = date(2025, 1, 1)
1138
- v2 = date(2025, 1, 2)
1139
- v3 = date(2025, 1, 3)
1137
+ v1 = date(2024, 12, 31)
1138
+ v2 = date(2025, 1, 1)
1139
+ v3 = date(2025, 1, 2)
1140
+ v4 = date(2025, 1, 3)
1140
1141
 
1141
1142
  i1 = instrument_factory.create()
1142
1143
  i2 = instrument_factory.create()
1143
1144
 
1144
1145
  i11 = instrument_price_factory.create(date=v1, instrument=i1)
1145
1146
  i12 = instrument_price_factory.create(date=v2, instrument=i1)
1146
- i13 = instrument_price_factory.create(date=v3, instrument=i1)
1147
- asset_position_factory.create(date=v1, portfolio=portfolio, underlying_instrument=i1)
1148
- asset_position_factory.create(date=v3, portfolio=portfolio, underlying_instrument=i2)
1147
+ i14 = instrument_price_factory.create(date=v4, instrument=i1)
1148
+ i21 = instrument_price_factory.create(date=v1, instrument=i2)
1149
1149
  i11.refresh_from_db()
1150
1150
  i12.refresh_from_db()
1151
- i13.refresh_from_db()
1152
- _, returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1151
+ i14.refresh_from_db()
1152
+ prices, returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v4)
1153
1153
 
1154
1154
  expected_returns = pd.DataFrame(
1155
- [[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
1156
- index=[v2, v3],
1155
+ [[i12.net_value / i11.net_value - 1, 0.0], [0.0, 0.0], [i14.net_value / i12.net_value - 1, 0.0]],
1156
+ index=[v2, v3, v4],
1157
1157
  columns=[i1.id, i2.id],
1158
1158
  dtype="float64",
1159
1159
  )
1160
1160
  expected_returns.index = pd.to_datetime(expected_returns.index)
1161
1161
  pd.testing.assert_frame_equal(returns, expected_returns, check_names=False, check_freq=False, atol=1e-6)
1162
+ assert prices[v1][i1.id] == float(i11.net_value)
1163
+ assert prices[v2][i1.id] == float(i12.net_value)
1164
+ assert prices[v3][i1.id] == float(i12.net_value)
1165
+ assert prices[v4][i1.id] == float(i14.net_value)
1166
+ # test that the returned price are ffill
1167
+ assert prices[v1][i2.id] == float(i21.net_value)
1168
+ assert prices[v2][i2.id] == float(i21.net_value)
1169
+ assert prices[v3][i2.id] == float(i21.net_value)
1170
+ assert prices[v4][i2.id] == float(i21.net_value)
1162
1171
 
1163
1172
  @patch.object(Portfolio, "compute_lookthrough", autospec=True)
1164
1173
  def test_handle_controlling_portfolio_change_at_date(self, mock_compute_lookthrough, weekday, portfolio_factory):
@@ -445,14 +445,20 @@ class TradeTradeProposalModelViewSet(
445
445
 
446
446
  agg = {
447
447
  "effective_weight": {
448
- "Cash": format_number(cash_sum_effective_weight, decimal=6),
449
- "Non-Cash": format_number(noncash_sum_effective_weight, decimal=6),
450
- "Total": format_number(noncash_sum_effective_weight + cash_sum_effective_weight, decimal=6),
448
+ "Cash": format_number(cash_sum_effective_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
449
+ "Non-Cash": format_number(noncash_sum_effective_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
450
+ "Total": format_number(
451
+ noncash_sum_effective_weight + cash_sum_effective_weight,
452
+ decimal=Trade.TRADE_WEIGHTING_PRECISION,
453
+ ),
451
454
  },
452
455
  "target_weight": {
453
- "Cash": format_number(cash_sum_target_cash_weight, decimal=6),
454
- "Non-Cash": format_number(noncash_sum_target_weight, decimal=6),
455
- "Total": format_number(cash_sum_target_cash_weight + noncash_sum_target_weight, decimal=6),
456
+ "Cash": format_number(cash_sum_target_cash_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
457
+ "Non-Cash": format_number(noncash_sum_target_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
458
+ "Total": format_number(
459
+ cash_sum_target_cash_weight + noncash_sum_target_weight,
460
+ decimal=Trade.TRADE_WEIGHTING_PRECISION,
461
+ ),
456
462
  },
457
463
  "effective_total_value_fx_portfolio": {
458
464
  "Cash": format_number(cash_sum_effective_total_value_fx_portfolio, decimal=6),
@@ -471,9 +477,12 @@ class TradeTradeProposalModelViewSet(
471
477
  ),
472
478
  },
473
479
  "weighting": {
474
- "Cash Flow": format_number(cash_sum_target_cash_weight - cash_sum_effective_weight, decimal=6),
475
- "Buy": format_number(sum_buy_weight, decimal=6),
476
- "Sell": format_number(sum_sell_weight, decimal=6),
480
+ "Cash Flow": format_number(
481
+ cash_sum_target_cash_weight - cash_sum_effective_weight,
482
+ decimal=Trade.TRADE_WEIGHTING_PRECISION,
483
+ ),
484
+ "Buy": format_number(sum_buy_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
485
+ "Sell": format_number(sum_sell_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
477
486
  },
478
487
  "total_value_fx_portfolio": {
479
488
  "Cash Flow": format_number(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.54.8
3
+ Version: 1.54.10
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -113,7 +113,7 @@ wbportfolio/import_export/handlers/dividend.py,sha256=F0oLfNt2B_QQAjHBCRpxa5HSkf
113
113
  wbportfolio/import_export/handlers/fees.py,sha256=BOFHAvSTlvVLaxnm6KD_fcza1TlPc02HOR9J0_jjswI,2495
114
114
  wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
115
115
  wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
116
- wbportfolio/import_export/handlers/trade.py,sha256=t-iezNZN6s834EsszGh_5sHnFGgLb3MZpcHrF2pWMG8,12533
116
+ wbportfolio/import_export/handlers/trade.py,sha256=_-P1ImDX6jfObm1WKiLtzz7RXEnXjtARAw7cxoHReCM,12826
117
117
  wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
118
118
  wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
119
119
  wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -257,7 +257,7 @@ wbportfolio/models/asset.py,sha256=b0vPt4LwNrxcMiK7UmBKViYnbNNlZzPTagvU5vFuyrc,4
257
257
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
258
258
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
259
259
  wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
260
- wbportfolio/models/portfolio.py,sha256=RjT836r83838kYvjYr-D8PYE0FX6tshMSJTKNg27REQ,58136
260
+ wbportfolio/models/portfolio.py,sha256=2m35YgEsF1gkt_JN5O4UlOtNLAqSfKi8JUp0G-1HPmQ,57997
261
261
  wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
262
262
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
263
263
  wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
@@ -284,8 +284,8 @@ wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWK
284
284
  wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
285
285
  wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
286
286
  wbportfolio/models/transactions/rebalancing.py,sha256=rwePcmTZOYgfSWnBQcBrZ3DQHRJ3w17hdO_hgrRbbhI,7696
287
- wbportfolio/models/transactions/trade_proposals.py,sha256=2EGi03APBC9QWU99LVRA1e-_-o8AUtEW332Iuxvb-_E,38214
288
- wbportfolio/models/transactions/trades.py,sha256=vTCoSBAmd6FPFyDRm5fqTqbxwzcH3ApWn0Dz2tkCCR8,34132
287
+ wbportfolio/models/transactions/trade_proposals.py,sha256=UYhV2pjQE4V-k0tVTBeZgPPK0C-tBeY345iroDzulfg,38106
288
+ wbportfolio/models/transactions/trades.py,sha256=oAUWCPPdBcMf5bcYI6BwvnrFKK588ZLRHPyPr65h-0U,33966
289
289
  wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
290
290
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
291
291
  wbportfolio/pms/typing.py,sha256=BV4dzazNHdfpfLV99bLVyYGcETmbQSnFV6ipc4fNKfg,8470
@@ -380,7 +380,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
380
380
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
381
381
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
382
382
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
383
- wbportfolio/tests/models/test_portfolios.py,sha256=oSlB1KdSHdR5lXm6G7wpjMjRgM79OrHM2FY_PHhna58,53570
383
+ wbportfolio/tests/models/test_portfolios.py,sha256=sWbt9NE8Cludu7hUhxxnIJwOSQ_U_yC6UPFPkbpV8Qk,54010
384
384
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
385
385
  wbportfolio/tests/models/test_products.py,sha256=IcBzw9hrGiWFMRwPBTMukCMWrhqnjOVA2hhb90xYOW8,9580
386
386
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -521,8 +521,8 @@ wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0
521
521
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
522
522
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
523
523
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=kQCojTNKBEyn2NcenL3a9auzBH4sIgLEx8rLAYCGLGg,6161
524
- wbportfolio/viewsets/transactions/trades.py,sha256=GHOw5jtcqoaHiRrxxxL29c9405QiPisEn4coGELKDrE,22146
525
- wbportfolio-1.54.8.dist-info/METADATA,sha256=WUtizk46d6wCImXgIUUy40yTpRz9S53Jtb9clFVtaP4,702
526
- wbportfolio-1.54.8.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
527
- wbportfolio-1.54.8.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
528
- wbportfolio-1.54.8.dist-info/RECORD,,
524
+ wbportfolio/viewsets/transactions/trades.py,sha256=Y8v2cM0vpspHysaAvu8qqhzt86dNtb2Q3puo4HCJsTI,22629
525
+ wbportfolio-1.54.10.dist-info/METADATA,sha256=uKqFJps-zs7857cq_8X_JmogU7gB5LG3RaZORctTI-k,703
526
+ wbportfolio-1.54.10.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
527
+ wbportfolio-1.54.10.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
528
+ wbportfolio-1.54.10.dist-info/RECORD,,