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

@@ -0,0 +1,19 @@
1
+ # Generated by Django 5.0.14 on 2025-07-16 12:39
2
+
3
+ from decimal import Decimal
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('wbportfolio', '0080_alter_trade_drift_factor_alter_trade_weighting'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='trade',
16
+ name='drift_factor',
17
+ field=models.DecimalField(decimal_places=16, default=Decimal('1'), help_text='Drift factor to be applied to the previous portfolio weight to get the actual effective weight including daily return', max_digits=19, verbose_name='Drift Factor'),
18
+ ),
19
+ ]
@@ -11,21 +11,15 @@ 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
- )
14
+ from django.db.models import Case, Exists, F, OuterRef, Q, QuerySet, Sum, Value, When
15
+ from django.db.models.functions import Coalesce
22
16
  from django.db.models.signals import post_save
23
17
  from django.dispatch import receiver
24
18
  from django.utils import timezone
25
19
  from django.utils.functional import cached_property
26
20
  from pandas._libs.tslibs.offsets import BDay
27
21
  from skfolio.preprocessing import prices_to_returns
28
- from wbcore.contrib.currency.models import Currency
22
+ from wbcore.contrib.currency.models import Currency, CurrencyFXRates
29
23
  from wbcore.contrib.notifications.utils import create_notification_type
30
24
  from wbcore.models import WBModel
31
25
  from wbcore.utils.importlib import import_from_dotted_path
@@ -62,7 +56,7 @@ def get_returns(
62
56
  from_date: date,
63
57
  to_date: date,
64
58
  to_currency: Currency | None = None,
65
- ffill_returns: bool = True,
59
+ use_dl: bool = False,
66
60
  ) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
67
61
  """
68
62
  Utility methods to get instrument returns for a given date range
@@ -70,46 +64,55 @@ def get_returns(
70
64
  Args:
71
65
  from_date: date range lower bound
72
66
  to_date: date range upper bound
67
+ to_currency: currency to use for returns
68
+ use_dl: whether to get data straight from the dataloader or use the internal table
73
69
 
74
70
  Returns:
75
- Return a tuple of the returns and the last prices series for conveniance
71
+ Return a tuple of the raw prices and the returns dataframe
76
72
  """
77
- # if to_currency:
78
- # fx_rate = CurrencyFXRates.get_fx_rates_subquery_for_two_currencies("date", "instrument__currency", to_currency)
79
- # else:
80
- # fx_rate = Value(Decimal(1.0))
81
- # prices = InstrumentPrice.objects.filter(
82
- # instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
83
- # ).annotate(fx_rate=fx_rate, price_fx_portfolio=F("net_value") * F("fx_rate"))
84
- # prices_df = (
85
- # pd.DataFrame(
86
- # prices.filter_only_valid_prices().values_list("instrument", "price_fx_portfolio", "date"),
87
- # columns=["instrument", "price_fx_portfolio", "date"],
88
- # )
89
- # .pivot_table(index="date", values="price_fx_portfolio", columns="instrument")
90
- # .astype(float)
91
- # .sort_index()
92
- # )
93
- kwargs = dict(from_date=from_date, to_date=to_date, values=[MarketData.CLOSE], apply_fx_rate=False)
94
- if to_currency:
95
- kwargs["target_currency"] = to_currency.key
96
- df = pd.DataFrame(Instrument.objects.filter(id__in=instrument_ids).dl.market_data(**kwargs))
73
+ if use_dl:
74
+ kwargs = dict(from_date=from_date, to_date=to_date, values=[MarketData.CLOSE], apply_fx_rate=False)
75
+ if to_currency:
76
+ kwargs["target_currency"] = to_currency.key
77
+ df = pd.DataFrame(Instrument.objects.filter(id__in=instrument_ids).dl.market_data(**kwargs))
78
+ if df.empty:
79
+ raise InvalidAnalyticPortfolio()
80
+ df = df[["instrument_id", "fx_rate", "close", "valuation_date"]]
81
+ else:
82
+ if to_currency:
83
+ fx_rate = Coalesce(
84
+ CurrencyFXRates.get_fx_rates_subquery_for_two_currencies("date", "instrument__currency", to_currency),
85
+ Decimal("1"),
86
+ )
87
+ else:
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)
90
+ prices = InstrumentPrice.objects.filter(
91
+ instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
92
+ ).annotate(fx_rate=Case(When(calculated=False, then=fx_rate), default=None))
93
+ df = pd.DataFrame(
94
+ prices.filter_only_valid_prices().values_list("instrument", "fx_rate", "net_value", "date"),
95
+ columns=["instrument_id", "fx_rate", "close", "valuation_date"],
96
+ )
97
97
  if df.empty:
98
98
  raise InvalidAnalyticPortfolio()
99
- df = df[["instrument_id", "fx_rate", "close", "valuation_date"]].pivot(
100
- index="valuation_date", columns="instrument_id", values=["fx_rate", "close"]
99
+ df = (
100
+ df.pivot_table(index="valuation_date", columns="instrument_id", values=["fx_rate", "close"])
101
+ .astype(float)
102
+ .sort_index()
101
103
  )
102
104
  ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
103
105
  df = df.reindex(ts)
104
- if ffill_returns:
105
- df = df.ffill()
106
+ df = df.ffill()
106
107
  df.index = pd.to_datetime(df.index)
107
-
108
108
  prices_df = df["close"]
109
- fx_rate_df = df["fx_rate"]
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)
110
113
 
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(
114
+ returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=True)
115
+ return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
113
116
  [np.inf, -np.inf, np.nan], 0
114
117
  )
115
118
 
@@ -348,11 +351,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
348
351
  instrument.delisted_date = date.today() - timedelta(days=1)
349
352
  instrument.save()
350
353
 
351
- 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:
352
355
  "returns the dto representation of this portfolio at the specified date"
353
356
  assets = self.assets.filter(date=val_date, **extra_kwargs)
354
357
  try:
355
- 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()
356
359
  except InvalidAnalyticPortfolio:
357
360
  drifted_weights = {}
358
361
  positions = []
@@ -387,7 +390,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
387
390
  )
388
391
 
389
392
  def get_analytic_portfolio(
390
- 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
391
394
  ) -> AnalyticPortfolio:
392
395
  """
393
396
  Return the analytic portfolio associated with this portfolio at the given date
@@ -407,6 +410,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
407
410
  (val_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
408
411
  return_date,
409
412
  to_currency=self.currency,
413
+ use_dl=use_dl,
410
414
  **kwargs,
411
415
  )
412
416
  if pd.Timestamp(return_date) not in returns.index:
@@ -832,7 +836,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
832
836
  (start_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
833
837
  end_date,
834
838
  to_currency=self.currency,
835
- ffill_returns=True,
839
+ use_dl=True,
836
840
  )
837
841
  # Get raw prices to speed up asset position creation
838
842
  # Instantiate the position iterator with the initial weights
@@ -849,7 +853,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
849
853
  drifted_weights = analytic_portfolio.get_next_weights()
850
854
  except KeyError: # if no return for that date, we break and continue
851
855
  drifted_weights = weights
852
-
853
856
  if rebalancer and rebalancer.is_valid(to_date):
854
857
  effective_portfolio = PortfolioDTO(
855
858
  positions=[
@@ -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 = {}
@@ -366,6 +366,8 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
366
366
  trade.weighting = -trade_dto.effective_weight
367
367
 
368
368
  trade.save()
369
+ # final sanity check to make sure invalid trade with effective and target weight of 0 are automatically removed:
370
+ self.trades.all().annotate_base_info().filter(target_weight=0, effective_weight=0).delete()
369
371
 
370
372
  def approve_workflow(
371
373
  self,
@@ -230,8 +230,10 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
230
230
  help_text="The Trade Proposal this trade is coming from",
231
231
  )
232
232
  drift_factor = models.DecimalField(
233
- max_digits=16,
234
- decimal_places=TRADE_WEIGHTING_PRECISION,
233
+ max_digits=TRADE_WEIGHTING_PRECISION * 2
234
+ + 3, # we don't expect any drift factor to be in the order of magnitude greater than 1000
235
+ decimal_places=TRADE_WEIGHTING_PRECISION
236
+ * 2, # we need a higher precision for this factor to avoid float inprecision
235
237
  default=Decimal(1.0),
236
238
  verbose_name="Drift Factor",
237
239
  help_text="Drift factor to be applied to the previous portfolio weight to get the actual effective weight including daily return",
@@ -16,10 +16,10 @@ class Portfolio(BasePortfolio):
16
16
  )
17
17
  return df
18
18
 
19
- def get_next_weights(self) -> dict[int, float]:
19
+ def get_next_weights(self, round_precision: int = 8) -> dict[int, float]:
20
20
  """
21
21
  Given the next returns, compute the drifted weights of this portfolio
22
-
22
+ round_precision: Round the weight to the given round number and ensure the total weight reflects this. Default to 8 decimals
23
23
  Returns:
24
24
  A dictionary of weights (instrument ids as keys and weights as values)
25
25
  """
@@ -29,7 +29,12 @@ class Portfolio(BasePortfolio):
29
29
  next_weights = weights * (returns + 1.0) / portfolio_returns
30
30
  next_weights = next_weights.dropna()
31
31
  next_weights = next_weights / next_weights.sum()
32
- return next_weights.to_dict()
32
+ if round_precision and not next_weights.empty:
33
+ next_weights = next_weights.round(round_precision)
34
+ quantization_error = 1.0 - next_weights.sum()
35
+ largest_weight = next_weights.idxmax()
36
+ next_weights.loc[largest_weight] = next_weights.loc[largest_weight] + quantization_error
37
+ return {i: round(w, round_precision) for i, w in next_weights.items()} # handle float precision manually
33
38
 
34
39
  def get_estimate_net_value(self, previous_net_asset_value: float) -> float:
35
40
  expected_returns = self.weights @ self.X.iloc[-1, :].T
@@ -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)
@@ -336,6 +336,26 @@ class TestTradeProposal:
336
336
  assert t2.weighting == Decimal("0")
337
337
  assert t3.weighting == Decimal("0.5")
338
338
 
339
+ def test_reset_trades_remove_invalid_trades(self, trade_proposal, trade_factory, instrument_price_factory):
340
+ # create a invalid trade and its price
341
+ invalid_trade = trade_factory.create(trade_proposal=trade_proposal, weighting=0)
342
+ instrument_price_factory.create(
343
+ date=invalid_trade.transaction_date, instrument=invalid_trade.underlying_instrument
344
+ )
345
+
346
+ # create a valid trade and its price
347
+ valid_trade = trade_factory.create(trade_proposal=trade_proposal, weighting=1.0)
348
+ instrument_price_factory.create(
349
+ date=valid_trade.transaction_date, instrument=valid_trade.underlying_instrument
350
+ )
351
+
352
+ trade_proposal.reset_trades()
353
+ assert trade_proposal.trades.get(underlying_instrument=valid_trade.underlying_instrument).weighting == Decimal(
354
+ "1"
355
+ )
356
+ with pytest.raises(Trade.DoesNotExist):
357
+ trade_proposal.trades.get(underlying_instrument=invalid_trade.underlying_instrument)
358
+
339
359
  # Test replaying trade proposals
340
360
  @patch.object(Portfolio, "drift_weights")
341
361
  def test_replay(self, mock_fct, trade_proposal_factory):
@@ -16,9 +16,27 @@ def test_get_next_weights():
16
16
  portfolio = Portfolio(X=pd.DataFrame([returns]), weights=pd.Series(weights))
17
17
  next_weights = portfolio.get_next_weights()
18
18
 
19
- assert next_weights[0] == pytest.approx(w0 * (r0 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-6)
20
- assert next_weights[1] == pytest.approx(w1 * (r1 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-6)
21
- assert next_weights[2] == pytest.approx(w2 * (r2 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-6)
19
+ assert next_weights[0] == pytest.approx(w0 * (r0 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-8)
20
+ assert next_weights[1] == pytest.approx(w1 * (r1 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-8)
21
+ assert next_weights[2] == pytest.approx(w2 * (r2 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-8)
22
+
23
+
24
+ def test_get_next_weights_solve_quantization_error():
25
+ w0 = 0.33333334
26
+ w1 = 0.33333333
27
+ w2 = 0.33333333
28
+ weights = [w0, w1, w2]
29
+ returns = [1.0, 1.0, 1.0] # no returns
30
+ portfolio = Portfolio(X=pd.DataFrame([returns]), weights=pd.Series(weights))
31
+ next_weights = portfolio.get_next_weights(round_precision=8) # no rounding as number are all 8 decimals
32
+ assert sum(next_weights.values()) == 1.0
33
+ next_weights = portfolio.get_next_weights(
34
+ round_precision=7
35
+ ) # we expect the weight to be rounded to 6 decimals, which would lead to a total sum of 0.999999
36
+
37
+ assert next_weights[0] == 0.3333334
38
+ assert next_weights[1] == 0.3333333
39
+ assert next_weights[2] == 0.3333333
22
40
 
23
41
 
24
42
  def test_get_estimate_net_value():
@@ -133,7 +133,7 @@ class AssetPositionModelViewSet(
133
133
  weighting = queryset.aggregate(s=Sum(F("weighting")))["s"]
134
134
  aggregates.update(
135
135
  {
136
- "weighting": {"Σ": format_number(weighting, decimal=4)},
136
+ "weighting": {"Σ": format_number(weighting, decimal=8)},
137
137
  "total_value_fx_usd": {"Σ": format_number(total_value_fx_usd)},
138
138
  }
139
139
  )
@@ -204,7 +204,7 @@ class AssetPositionPortfolioModelViewSet(InstrumentMetricMixin, AssetPositionMod
204
204
  total_value_fx_portfolio = queryset.aggregate(s=Sum(F("total_value_fx_portfolio")))["s"]
205
205
  aggregates = super().get_aggregates(queryset, paginated_queryset)
206
206
  aggregates["total_value_fx_portfolio"] = {"Σ": format_number(total_value_fx_portfolio)}
207
- aggregates["weighting"] = {"Σ": format_number(weighting, decimal=4)}
207
+ aggregates["weighting"] = {"Σ": format_number(weighting, decimal=8)}
208
208
  return aggregates
209
209
 
210
210
  def get_queryset(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.54.10
3
+ Version: 1.54.12
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -250,6 +250,7 @@ wbportfolio/migrations/0077_remove_transaction_currency_and_more.py,sha256=Yf4a3
250
250
  wbportfolio/migrations/0078_trade_drift_factor.py,sha256=26Z3yoiBhMueB-k2R9HaIzg5Qr7BYpdtzlU-65T_cH0,999
251
251
  wbportfolio/migrations/0079_alter_trade_drift_factor.py,sha256=2tvPecUxEy60-ELy9wtiuTR2dhJ8HucJjvOEuia4Pp4,627
252
252
  wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py,sha256=vCPJSrM7x24jUJseGkHEVBD0c5nEpDTrb9-zkJ-0QoI,569
253
+ wbportfolio/migrations/0081_alter_trade_drift_factor.py,sha256=rF3HA1MQJ0hltr0dExJAx47w8XxUCWRbDUyxQLoQB2I,656
253
254
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
254
255
  wbportfolio/models/__init__.py,sha256=HSpa5xwh_MHQaBpNrq9E0CbdEE5Iq-pDLIsPzZ-TRTg,904
255
256
  wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
@@ -257,7 +258,7 @@ wbportfolio/models/asset.py,sha256=b0vPt4LwNrxcMiK7UmBKViYnbNNlZzPTagvU5vFuyrc,4
257
258
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
258
259
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
259
260
  wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
260
- wbportfolio/models/portfolio.py,sha256=2m35YgEsF1gkt_JN5O4UlOtNLAqSfKi8JUp0G-1HPmQ,57997
261
+ wbportfolio/models/portfolio.py,sha256=5CddquPvVp-ImPlteNMqwDeY_mCFwdWMy7ynMRDDxak,58578
261
262
  wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
262
263
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
263
264
  wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
@@ -284,13 +285,13 @@ wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWK
284
285
  wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
285
286
  wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
286
287
  wbportfolio/models/transactions/rebalancing.py,sha256=rwePcmTZOYgfSWnBQcBrZ3DQHRJ3w17hdO_hgrRbbhI,7696
287
- wbportfolio/models/transactions/trade_proposals.py,sha256=UYhV2pjQE4V-k0tVTBeZgPPK0C-tBeY345iroDzulfg,38106
288
- wbportfolio/models/transactions/trades.py,sha256=oAUWCPPdBcMf5bcYI6BwvnrFKK588ZLRHPyPr65h-0U,33966
288
+ wbportfolio/models/transactions/trade_proposals.py,sha256=1xOxHVOUwgTKCcY7mN4u5SoFtdJrQkNg6xA3O2-f4Yw,38340
289
+ wbportfolio/models/transactions/trades.py,sha256=1gmAdavuWu1Iko90s9prMxsK_NuDKIUBIKMDuHiKzow,34176
289
290
  wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
290
291
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
291
292
  wbportfolio/pms/typing.py,sha256=BV4dzazNHdfpfLV99bLVyYGcETmbQSnFV6ipc4fNKfg,8470
292
293
  wbportfolio/pms/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
293
- wbportfolio/pms/analytics/portfolio.py,sha256=n_tv4gX-mOFfKCxhSGblqQzW62Bx-yoM2QcJxQd3dLE,1384
294
+ wbportfolio/pms/analytics/portfolio.py,sha256=QaYArF-8Dk9MY0ZLUZ1IHaz3T-uYG81P5SqS_jSar8A,1950
294
295
  wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
295
296
  wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
296
297
  wbportfolio/pms/trading/handler.py,sha256=ZOwgnOU4ScVIhTMRQ0SLR2cCCZP9whmVv-S5hF-TOME,8593
@@ -380,7 +381,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
380
381
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
381
382
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
382
383
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
383
- wbportfolio/tests/models/test_portfolios.py,sha256=sWbt9NE8Cludu7hUhxxnIJwOSQ_U_yC6UPFPkbpV8Qk,54010
384
+ wbportfolio/tests/models/test_portfolios.py,sha256=H3mgrQLdTkrljFZgJLRTXuN6J8fasRtlYoaRCnirdwU,56165
384
385
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
385
386
  wbportfolio/tests/models/test_products.py,sha256=IcBzw9hrGiWFMRwPBTMukCMWrhqnjOVA2hhb90xYOW8,9580
386
387
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -390,10 +391,10 @@ wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
390
391
  wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
391
392
  wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
392
393
  wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
393
- wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=_EH7N2-oTqgz13NGSahD1d0rIiu93Ce2rcA9A8gUlSg,29800
394
+ wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=INZb4J7qodXiT8FwbRNjEoXOm9HjO6W-vkA9uW8e3CI,30804
394
395
  wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0Wq9ycA2oeeEBhO3dzV7sbXEU,9863
395
396
  wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
396
- wbportfolio/tests/pms/test_analytics.py,sha256=WHicJBjAjpIRL1-AW2nZ4VD9oJRpMoeH6V1Qx2D95-w,1178
397
+ wbportfolio/tests/pms/test_analytics.py,sha256=KzgZqZ9yYB1gsokw6IU-uKYWr1eFHWyFte8RSpMVRg8,1897
397
398
  wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
398
399
  wbportfolio/tests/rebalancing/test_models.py,sha256=QMfcYDvFew1bH6kPm-jVJLC_RqmPE-oGTqUldx1KVgg,8025
399
400
  wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -406,7 +407,7 @@ wbportfolio/tests/viewsets/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5J
406
407
  wbportfolio/tests/viewsets/transactions/test_claims.py,sha256=QEZfMAW07dyoZ63t2umSwGOqvaTULfYfbN_F4ZoSAcw,6368
407
408
  wbportfolio/viewsets/__init__.py,sha256=3kUaQ66ybvROwejd3bEcSt4XKzfOlPDaeoStMvlz7qY,2294
408
409
  wbportfolio/viewsets/adjustments.py,sha256=ugbX4aFRCaD4Yj1hxL-VIPaNI7GF_wt0FrkN6mq1YjU,1524
409
- wbportfolio/viewsets/assets.py,sha256=nMMVh7P531KZ-zpFogIMFKbfvRD3KdG2oIAAHQ42lpc,24634
410
+ wbportfolio/viewsets/assets.py,sha256=MCE81rmDwbMGxO_LsD8AvU9tHWWgi-OkX5ecLFH-KGY,24634
410
411
  wbportfolio/viewsets/assets_and_net_new_money_progression.py,sha256=Jl4vEQP4N2OFL5IGBXoKcj-0qaPviU0I8npvQLw4Io0,4464
411
412
  wbportfolio/viewsets/custodians.py,sha256=CTFqkqVP1R3AV7lhdvcdICxB5DfwDYCyikNSI5kbYEo,2322
412
413
  wbportfolio/viewsets/esg.py,sha256=27MxxdXQH3Cq_1UEYmcrF7htUOg6i81fUpbVQXAAKJI,6985
@@ -522,7 +523,7 @@ wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIH
522
523
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
523
524
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=kQCojTNKBEyn2NcenL3a9auzBH4sIgLEx8rLAYCGLGg,6161
524
525
  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,,
526
+ wbportfolio-1.54.12.dist-info/METADATA,sha256=8ZNVEtEaAspKx8rNxEGdkon_4d95uFFaJnwzVcHUxpc,703
527
+ wbportfolio-1.54.12.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
528
+ wbportfolio-1.54.12.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
529
+ wbportfolio-1.54.12.dist-info/RECORD,,