wbportfolio 1.54.9__py2.py3-none-any.whl → 1.54.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,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
@@ -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
+ ]
@@ -18,14 +18,16 @@ from django.db.models import (
18
18
  Q,
19
19
  QuerySet,
20
20
  Sum,
21
+ Value,
21
22
  )
23
+ from django.db.models.functions import Coalesce
22
24
  from django.db.models.signals import post_save
23
25
  from django.dispatch import receiver
24
26
  from django.utils import timezone
25
27
  from django.utils.functional import cached_property
26
28
  from pandas._libs.tslibs.offsets import BDay
27
29
  from skfolio.preprocessing import prices_to_returns
28
- from wbcore.contrib.currency.models import Currency
30
+ from wbcore.contrib.currency.models import Currency, CurrencyFXRates
29
31
  from wbcore.contrib.notifications.utils import create_notification_type
30
32
  from wbcore.models import WBModel
31
33
  from wbcore.utils.importlib import import_from_dotted_path
@@ -54,6 +56,8 @@ logger = logging.getLogger("pms")
54
56
  if TYPE_CHECKING:
55
57
  from wbportfolio.models.transactions.trade_proposals import TradeProposal
56
58
 
59
+ MARKET_HOLIDAY_MAX_DURATION = 15
60
+
57
61
 
58
62
  def get_returns(
59
63
  instrument_ids: list[int],
@@ -61,6 +65,7 @@ def get_returns(
61
65
  to_date: date,
62
66
  to_currency: Currency | None = None,
63
67
  ffill_returns: bool = True,
68
+ use_dl: bool = False,
64
69
  ) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
65
70
  """
66
71
  Utility methods to get instrument returns for a given date range
@@ -68,45 +73,53 @@ def get_returns(
68
73
  Args:
69
74
  from_date: date range lower bound
70
75
  to_date: date range upper bound
76
+ to_currency: currency to use for returns
77
+ ffill_returns: whether to ffill returns and prices
78
+ use_dl: whether to get data straight from the dataloader or use the internal table
71
79
 
72
80
  Returns:
73
- Return a tuple of the returns and the last prices series for conveniance
81
+ Return a tuple of the raw prices and the returns dataframe
74
82
  """
75
- # if to_currency:
76
- # fx_rate = CurrencyFXRates.get_fx_rates_subquery_for_two_currencies("date", "instrument__currency", to_currency)
77
- # else:
78
- # fx_rate = Value(Decimal(1.0))
79
- # prices = InstrumentPrice.objects.filter(
80
- # instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
81
- # ).annotate(fx_rate=fx_rate, price_fx_portfolio=F("net_value") * F("fx_rate"))
82
- # prices_df = (
83
- # pd.DataFrame(
84
- # prices.filter_only_valid_prices().values_list("instrument", "price_fx_portfolio", "date"),
85
- # columns=["instrument", "price_fx_portfolio", "date"],
86
- # )
87
- # .pivot_table(index="date", values="price_fx_portfolio", columns="instrument")
88
- # .astype(float)
89
- # .sort_index()
90
- # )
91
- kwargs = dict(from_date=from_date, to_date=to_date, values=[MarketData.CLOSE], apply_fx_rate=False)
92
- if to_currency:
93
- kwargs["target_currency"] = to_currency.key
94
- df = pd.DataFrame(Instrument.objects.filter(id__in=instrument_ids).dl.market_data(**kwargs))
83
+ if use_dl:
84
+ kwargs = dict(from_date=from_date, to_date=to_date, values=[MarketData.CLOSE], apply_fx_rate=False)
85
+ if to_currency:
86
+ kwargs["target_currency"] = to_currency.key
87
+ df = pd.DataFrame(Instrument.objects.filter(id__in=instrument_ids).dl.market_data(**kwargs))
88
+ if df.empty:
89
+ raise InvalidAnalyticPortfolio()
90
+ df = df[["instrument_id", "fx_rate", "close", "valuation_date"]]
91
+ else:
92
+ if to_currency:
93
+ fx_rate = Coalesce(
94
+ CurrencyFXRates.get_fx_rates_subquery_for_two_currencies("date", "instrument__currency", to_currency),
95
+ Decimal("1"),
96
+ )
97
+ else:
98
+ fx_rate = Value(Decimal("1"))
99
+ prices = InstrumentPrice.objects.filter(
100
+ instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
101
+ ).annotate(fx_rate=fx_rate)
102
+ df = pd.DataFrame(
103
+ prices.filter_only_valid_prices().values_list("instrument", "fx_rate", "net_value", "date"),
104
+ columns=["instrument_id", "fx_rate", "close", "valuation_date"],
105
+ )
106
+
95
107
  if df.empty:
96
108
  raise InvalidAnalyticPortfolio()
97
- df = df[["instrument_id", "fx_rate", "close", "valuation_date"]].pivot(
98
- index="valuation_date", columns="instrument_id", values=["fx_rate", "close"]
109
+ df = (
110
+ df.pivot_table(index="valuation_date", columns="instrument_id", values=["fx_rate", "close"])
111
+ .astype(float)
112
+ .sort_index()
99
113
  )
100
114
  ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
101
115
  df = df.reindex(ts)
102
116
  if ffill_returns:
103
117
  df = df.ffill()
104
118
  df.index = pd.to_datetime(df.index)
105
-
106
119
  prices_df = df["close"]
107
120
  fx_rate_df = df["fx_rate"]
108
121
  returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
109
- return {ts.date(): row for ts, row in prices_df.replace(np.nan, None).to_dict("index").items()}, returns.replace(
122
+ return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
110
123
  [np.inf, -np.inf, np.nan], 0
111
124
  )
112
125
 
@@ -400,7 +413,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
400
413
  weights = self.get_weights(val_date)
401
414
  return_date = (val_date + BDay(1)).date()
402
415
  _, returns = get_returns(
403
- list(weights.keys()), (val_date - BDay(2)).date(), return_date, to_currency=self.currency, **kwargs
416
+ list(weights.keys()),
417
+ (val_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
418
+ return_date,
419
+ to_currency=self.currency,
420
+ **kwargs,
404
421
  )
405
422
  if pd.Timestamp(return_date) not in returns.index:
406
423
  raise InvalidAnalyticPortfolio()
@@ -822,10 +839,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
822
839
 
823
840
  prices, returns = get_returns(
824
841
  instrument_ids,
825
- (start_date - BDay(3)).date(),
842
+ (start_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
826
843
  end_date,
827
844
  to_currency=self.currency,
828
845
  ffill_returns=True,
846
+ use_dl=True,
829
847
  )
830
848
  # Get raw prices to speed up asset position creation
831
849
  # Instantiate the position iterator with the initial weights
@@ -842,7 +860,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
842
860
  drifted_weights = analytic_portfolio.get_next_weights()
843
861
  except KeyError: # if no return for that date, we break and continue
844
862
  drifted_weights = weights
845
-
846
863
  if rebalancer and rebalancer.is_valid(to_date):
847
864
  effective_portfolio = PortfolioDTO(
848
865
  positions=[
@@ -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,
@@ -397,7 +399,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
397
399
  if approve_automatically and self.portfolio.can_be_rebalanced:
398
400
  self.approve(replay=False, broadcast_changes_at_date=broadcast_changes_at_date)
399
401
 
400
- def replay(self, force_reset_trade: bool = False, broadcast_changes_at_date: bool = True):
402
+ def replay(self, broadcast_changes_at_date: bool = True):
401
403
  last_trade_proposal = self
402
404
  last_trade_proposal_created = False
403
405
  while last_trade_proposal and last_trade_proposal.status == TradeProposal.Status.APPROVED:
@@ -405,7 +407,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
405
407
  logger.info(f"Replaying trade proposal {last_trade_proposal}")
406
408
  last_trade_proposal.approve_workflow(
407
409
  silent_exception=True,
408
- force_reset_trade=force_reset_trade,
410
+ force_reset_trade=True,
409
411
  broadcast_changes_at_date=broadcast_changes_at_date,
410
412
  )
411
413
  last_trade_proposal.save()
@@ -803,9 +805,8 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
803
805
  ) # we delete the existing portfolio as it has been reverted
804
806
  for trade in self.trades.all():
805
807
  trade.status = Trade.Status.DRAFT
806
- trade.drift_factor = Decimal("1")
807
808
  trades.append(trade)
808
- Trade.objects.bulk_update(trades, ["status", "drift_factor"])
809
+ Trade.objects.bulk_update(trades, ["status"])
809
810
 
810
811
  def can_revert(self):
811
812
  errors = dict()
@@ -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",
@@ -439,7 +441,7 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
439
441
  method=RequestType.PATCH,
440
442
  identifiers=("wbportfolio:trade",),
441
443
  icon=WBIcon.UNDO.icon,
442
- key="reverte",
444
+ key="revert",
443
445
  label="Revert",
444
446
  action_label="revert",
445
447
  # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
@@ -518,6 +520,11 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
518
520
  self, "target_weight", round(self._effective_weight + self.weighting, self.TRADE_WEIGHTING_PRECISION)
519
521
  )
520
522
 
523
+ @_target_weight.setter
524
+ def _target_weight(self, target_weight):
525
+ self.weighting = Decimal(target_weight) - self._effective_weight
526
+ self._set_type()
527
+
521
528
  @property
522
529
  @admin.display(description="Target Shares")
523
530
  def _target_shares(self) -> Decimal:
@@ -558,12 +565,6 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
558
565
  ]
559
566
  # notification_email_template = "portfolio/email/trade_notification.html"
560
567
 
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
568
  def save(self, *args, **kwargs):
568
569
  if abs(self.weighting) < 10e-6:
569
570
  self.weighting = Decimal("0")
@@ -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
@@ -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):
@@ -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.9
3
+ Version: 1.54.11
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=oLYNEAFyDeInvHjuAbB4pe60q6VmHX0wGcyuEGofbKM,12700
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
@@ -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=9gbT3tuo73SfhBn4_HIes71feyjTXnZnn9lX2KL_1pQ,57861
261
+ wbportfolio/models/portfolio.py,sha256=5gOPWWv-_ke-p-B14S9ZNKRvvGQVHNBzR2r5xL-AKm0,58341
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=2EGi03APBC9QWU99LVRA1e-_-o8AUtEW332Iuxvb-_E,38214
288
- wbportfolio/models/transactions/trades.py,sha256=vTCoSBAmd6FPFyDRm5fqTqbxwzcH3ApWn0Dz2tkCCR8,34132
288
+ wbportfolio/models/transactions/trade_proposals.py,sha256=Iilb67WdUh4XuBhxMqnnxTj_43NxTloGNNtJQh1izD8,38327
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
@@ -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
@@ -521,8 +522,8 @@ wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0
521
522
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
522
523
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
523
524
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=kQCojTNKBEyn2NcenL3a9auzBH4sIgLEx8rLAYCGLGg,6161
524
- wbportfolio/viewsets/transactions/trades.py,sha256=GHOw5jtcqoaHiRrxxxL29c9405QiPisEn4coGELKDrE,22146
525
- wbportfolio-1.54.9.dist-info/METADATA,sha256=SL5iDKkrof3J3-X4ykL9jJslbUo8ltmEwJzni_emoXY,702
526
- wbportfolio-1.54.9.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
527
- wbportfolio-1.54.9.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
528
- wbportfolio-1.54.9.dist-info/RECORD,,
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,,