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

wbportfolio/pms/typing.py CHANGED
@@ -19,7 +19,7 @@ class Position:
19
19
  weighting: Decimal
20
20
  date: date_lib
21
21
 
22
- drift_factor: float = 1.0
22
+ drift_factor: Decimal = Decimal("1")
23
23
  currency: int | None = None
24
24
  instrument_type: int | None = None
25
25
  asset_valuation_date: date_lib | None = None
@@ -45,10 +45,15 @@ class Position:
45
45
  **{f.name: getattr(self, f.name) for f in fields(Position) if f.name not in ["weighting", "shares"]},
46
46
  )
47
47
 
48
+ def copy(self, **kwargs):
49
+ attrs = {f.name: getattr(self, f.name) for f in fields(Position)}
50
+ attrs.update(kwargs)
51
+ return Position(**attrs)
52
+
48
53
 
49
54
  @dataclass(frozen=True)
50
55
  class Portfolio:
51
- positions: tuple[Position] | tuple
56
+ positions: list[Position] | list
52
57
  positions_map: dict[int, Position] = field(init=False, repr=False)
53
58
 
54
59
  def __post_init__(self):
@@ -62,7 +67,7 @@ class Portfolio:
62
67
 
63
68
  @property
64
69
  def total_weight(self):
65
- return round(sum([pos.weighting for pos in self.positions]), 6)
70
+ return round(sum([pos.weighting for pos in self.positions]), 8)
66
71
 
67
72
  @property
68
73
  def total_shares(self):
@@ -77,6 +82,9 @@ class Portfolio:
77
82
  def __len__(self):
78
83
  return len(self.positions)
79
84
 
85
+ def __bool__(self):
86
+ return len(self.positions) > 0
87
+
80
88
 
81
89
  @dataclass(frozen=True)
82
90
  class Trade:
@@ -85,7 +93,7 @@ class Trade:
85
93
  currency: int
86
94
  date: date_lib
87
95
  price: Decimal
88
- effective_weight: Decimal
96
+ previous_weight: Decimal
89
97
  target_weight: Decimal
90
98
  currency_fx_rate: Decimal = Decimal("1")
91
99
  effective_shares: Decimal = Decimal("0")
@@ -97,20 +105,22 @@ class Trade:
97
105
  def __add__(self, other):
98
106
  return Trade(
99
107
  underlying_instrument=self.underlying_instrument,
100
- effective_weight=self.effective_weight,
108
+ previous_weight=self.previous_weight,
101
109
  target_weight=self.target_weight + other.target_weight,
102
110
  effective_shares=self.effective_shares,
103
111
  target_shares=self.target_shares + other.target_shares,
104
- **{
112
+ drift_factor=self.drift_factor
113
+ ** {
105
114
  f.name: getattr(self, f.name)
106
115
  for f in fields(Trade)
107
116
  if f.name
108
117
  not in [
109
- "effective_weight",
118
+ "previous_weight",
110
119
  "target_weight",
111
120
  "effective_shares",
112
121
  "target_shares",
113
122
  "underlying_instrument",
123
+ "drift_factor",
114
124
  ]
115
125
  },
116
126
  )
@@ -120,6 +130,10 @@ class Trade:
120
130
  attrs.update(kwargs)
121
131
  return Trade(**attrs)
122
132
 
133
+ @property
134
+ def effective_weight(self) -> Decimal:
135
+ return self.previous_weight * self.drift_factor
136
+
123
137
  @property
124
138
  def delta_weight(self) -> Decimal:
125
139
  return self.target_weight - self.effective_weight
@@ -168,11 +182,11 @@ class TradeBatch:
168
182
 
169
183
  @property
170
184
  def total_target_weight(self) -> Decimal:
171
- return round(sum([trade.target_weight for trade in self.trades], Decimal("0")), 6)
185
+ return round(sum([trade.target_weight for trade in self.trades], Decimal("0")), 8)
172
186
 
173
187
  @property
174
188
  def total_effective_weight(self) -> Decimal:
175
- return round(sum([trade.effective_weight for trade in self.trades], Decimal("0")), 6)
189
+ return round(sum([trade.effective_weight for trade in self.trades], Decimal("0")), 8)
176
190
 
177
191
  @property
178
192
  def total_abs_delta_weight(self) -> Decimal:
@@ -188,14 +202,15 @@ class TradeBatch:
188
202
  if round(float(self.total_target_weight), 4) != 1: # we do that to remove decimal over precision
189
203
  raise ValidationError(f"Total Weight cannot be different than 1 ({float(self.total_target_weight)})")
190
204
 
191
- def convert_to_portfolio(self, *extra_positions):
205
+ def convert_to_portfolio(self, use_effective: bool = False, *extra_positions):
192
206
  positions = []
193
207
  for instrument, trade in self.trades_map.items():
194
208
  positions.append(
195
209
  Position(
196
210
  underlying_instrument=trade.underlying_instrument,
197
211
  instrument_type=trade.instrument_type,
198
- weighting=trade.target_weight,
212
+ weighting=trade.target_weight if not use_effective else trade.previous_weight,
213
+ drift_factor=trade.drift_factor if use_effective else Decimal("1"),
199
214
  shares=trade.target_shares,
200
215
  currency=trade.currency,
201
216
  date=trade.date,
@@ -8,10 +8,21 @@ class AbstractRebalancingModel:
8
8
  def validation_errors(self) -> str:
9
9
  return getattr(self, "_validation_errors", "Rebalacing cannot applied for these parameters")
10
10
 
11
- def __init__(self, portfolio, trade_date: date, last_effective_date: date, **kwargs):
11
+ def __init__(
12
+ self,
13
+ portfolio,
14
+ trade_date: date,
15
+ last_effective_date: date,
16
+ effective_portfolio: PortfolioDTO | None = None,
17
+ **kwargs,
18
+ ):
12
19
  self.portfolio = portfolio
13
20
  self.trade_date = trade_date
14
21
  self.last_effective_date = last_effective_date
22
+ self.effective_portfolio = effective_portfolio
23
+ # we try to get the portfolio at the trade date
24
+ if not self.effective_portfolio:
25
+ self.effective_portfolio = self.portfolio._build_dto(self.last_effective_date)
15
26
 
16
27
  def is_valid(self) -> bool:
17
28
  return True
@@ -9,24 +9,21 @@ from wbportfolio.rebalancing.decorators import register
9
9
 
10
10
  @register("Equally Weighted Rebalancing")
11
11
  class EquallyWeightedRebalancing(AbstractRebalancingModel):
12
- def __init__(self, *args, **kwargs):
13
- super().__init__(*args, **kwargs)
14
- self.assets = self.portfolio.assets.filter(date=self.last_effective_date)
15
-
16
12
  def is_valid(self) -> bool:
17
13
  return (
18
- self.assets.exists()
14
+ len(self.effective_portfolio.positions) > 0
19
15
  and InstrumentPrice.objects.filter(
20
- date=self.trade_date, instrument__in=self.assets.values("underlying_quote")
16
+ date=self.trade_date, instrument__in=self.effective_portfolio.positions_map.keys()
21
17
  ).exists()
22
18
  )
23
19
 
24
20
  def get_target_portfolio(self) -> Portfolio:
25
21
  positions = []
26
- assets = self.portfolio.assets.filter(date=self.last_effective_date)
27
- nb_assets = assets.count()
28
- for asset in assets:
29
- asset.date = self.trade_date
30
- asset.asset_valuation_date = self.trade_date
31
- positions.append(asset._build_dto(new_weight=Decimal(1 / nb_assets)))
32
- return Portfolio(positions=tuple(positions))
22
+ nb_assets = len(self.effective_portfolio.positions)
23
+ for position in self.effective_portfolio.positions:
24
+ positions.append(
25
+ position.copy(
26
+ weighting=Decimal(1 / nb_assets), date=self.trade_date, asset_valuation_date=self.trade_date
27
+ )
28
+ )
29
+ return Portfolio(positions)
@@ -13,6 +13,7 @@ from wbfdm.models import (
13
13
  )
14
14
 
15
15
  from wbportfolio.pms.typing import Portfolio, Position
16
+ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
16
17
  from wbportfolio.rebalancing.base import AbstractRebalancingModel
17
18
  from wbportfolio.rebalancing.decorators import register
18
19
 
@@ -20,9 +21,17 @@ from wbportfolio.rebalancing.decorators import register
20
21
  @register("Market Capitalization Rebalancing")
21
22
  class MarketCapitalizationRebalancing(AbstractRebalancingModel):
22
23
  TARGET_CURRENCY: str = "USD"
24
+ MIN_WEIGHT: float = 10e-5 # we allow only weight of minimum 0.01%
23
25
 
24
- def __init__(self, *args, bypass_exchange_check: bool = False, ffill_market_cap_limit: int = 5, **kwargs):
25
- super().__init__(*args, **kwargs)
26
+ def __init__(
27
+ self,
28
+ *args,
29
+ bypass_exchange_check: bool = False,
30
+ ffill_market_cap_limit: int = 5,
31
+ effective_portfolio: PortfolioDTO | None = None,
32
+ **kwargs,
33
+ ):
34
+ super().__init__(*args, effective_portfolio=effective_portfolio, **kwargs)
26
35
  self.bypass_exchange_check = bypass_exchange_check
27
36
  instruments = self._get_instruments(**kwargs)
28
37
  self.market_cap_df = pd.DataFrame(
@@ -78,9 +87,7 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
78
87
  )
79
88
 
80
89
  if not instrument_ids:
81
- instrument_ids = list(
82
- self.portfolio.assets.filter(date=self.trade_date).values_list("underlying_instrument", flat=True)
83
- )
90
+ instrument_ids = list(self.effective_portfolio.positions_map.keys())
84
91
 
85
92
  return (
86
93
  Instrument.objects.filter(id__in=instrument_ids)
@@ -110,12 +117,14 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
110
117
 
111
118
  def get_target_portfolio(self) -> Portfolio:
112
119
  positions = []
113
- total_market_cap = self.market_cap_df.dropna().sum()
114
- for underlying_instrument, market_cap in self.market_cap_df.to_dict().items():
115
- if np.isnan(market_cap):
120
+ df = self.market_cap_df / self.market_cap_df.dropna().sum()
121
+ df = df[df > self.MIN_WEIGHT]
122
+ df = df / df.sum()
123
+ for underlying_instrument, weighting in df.to_dict().items():
124
+ if np.isnan(weighting):
116
125
  weighting = Decimal(0)
117
126
  else:
118
- weighting = Decimal(market_cap / total_market_cap)
127
+ weighting = Decimal(weighting)
119
128
  positions.append(
120
129
  Position(
121
130
  underlying_instrument=underlying_instrument,
@@ -43,8 +43,8 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
43
43
  obj = super().create(validated_data)
44
44
 
45
45
  target_portfolio_dto = None
46
- if target_portfolio and not rebalancing_model and (last_effective_date := obj.last_effective_date):
47
- target_portfolio_dto = target_portfolio._build_dto(last_effective_date)
46
+ if target_portfolio and not rebalancing_model:
47
+ target_portfolio_dto = target_portfolio._build_dto(obj.trade_date)
48
48
  try:
49
49
  obj.reset_trades(
50
50
  target_portfolio=target_portfolio_dto, total_target_weight=Decimal("1.0") - total_cash_weight
@@ -1011,7 +1011,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1011
1011
  assert a.underlying_quote == instrument
1012
1012
  assert a.underlying_quote_price == None
1013
1013
  assert a.initial_price == p.net_value
1014
- assert a.weighting == weights[instrument.id]
1014
+ assert a.weighting == pytest.approx(weights[instrument.id], abs=10e-6)
1015
1015
  assert a.currency_fx_rate_portfolio_to_usd == fx_portfolio
1016
1016
  assert a.currency_fx_rate_instrument_to_usd == fx_instrument
1017
1017
 
@@ -1047,7 +1047,9 @@ class TestPortfolioModel(PortfolioTestMixin):
1047
1047
  asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i2, weighting=0.3)
1048
1048
 
1049
1049
  rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
1050
- positions, rebalancing_trade_proposal = portfolio.drift_weights(weekday, (rebalancing_date + BDay(1)).date())
1050
+ positions, rebalancing_trade_proposal = portfolio.drift_weights(
1051
+ weekday, (rebalancing_date + BDay(1)).date(), stop_at_rebalancing=True
1052
+ )
1051
1053
  assert rebalancing_trade_proposal.trade_date == rebalancing_date
1052
1054
  assert rebalancing_trade_proposal.status == "SUBMIT"
1053
1055
  assert set(positions.get_weights().keys()) == {
@@ -1141,7 +1143,7 @@ class TestPortfolioModel(PortfolioTestMixin):
1141
1143
  i11.refresh_from_db()
1142
1144
  i12.refresh_from_db()
1143
1145
  i13.refresh_from_db()
1144
- returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1146
+ _, returns = get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1145
1147
 
1146
1148
  expected_returns = pd.DataFrame(
1147
1149
  [[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
@@ -131,15 +131,15 @@ class TestTradeProposal:
131
131
  Verify the next trade proposal is correctly identified as the first approved proposal after the current one.
132
132
  """
133
133
  tp = trade_proposal_factory.create()
134
+ tp_previous_approve = trade_proposal_factory.create( # noqa
135
+ portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
136
+ )
134
137
  tp_next_submit = trade_proposal_factory.create( # noqa
135
138
  portfolio=tp.portfolio, status=TradeProposal.Status.SUBMIT, trade_date=(tp.trade_date + BDay(1)).date()
136
139
  )
137
140
  tp_next_approve = trade_proposal_factory.create(
138
141
  portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date + BDay(2)).date()
139
142
  )
140
- tp_previous_approve = trade_proposal_factory.create( # noqa
141
- portfolio=tp.portfolio, status=TradeProposal.Status.APPROVED, trade_date=(tp.trade_date - BDay(1)).date()
142
- )
143
143
 
144
144
  # The next valid trade proposal should be the approved one strictly after the current proposal
145
145
  assert (
@@ -236,9 +236,9 @@ class TestTradeProposal:
236
236
  t3.refresh_from_db()
237
237
 
238
238
  # Expected normalized weights
239
- normalized_t1_weight = Decimal("0.333333")
240
- normalized_t2_weight = Decimal("0.433333")
241
- normalized_t3_weight = Decimal("0.233333")
239
+ normalized_t1_weight = Decimal("0.33333333")
240
+ normalized_t2_weight = Decimal("0.43333333")
241
+ normalized_t3_weight = Decimal("0.23333333")
242
242
 
243
243
  # Calculate quantization error
244
244
  quantize_error = Decimal(1) - (normalized_t1_weight + normalized_t2_weight + normalized_t3_weight)
@@ -362,9 +362,9 @@ class TestTradeProposal:
362
362
 
363
363
  # Expected calls to drift_weights
364
364
  expected_calls = [
365
- call(tp0.trade_date, tp1.trade_date - timedelta(days=1)),
366
- call(tp1.trade_date, tp2.trade_date - timedelta(days=1)),
367
- call(tp2.trade_date, date.today()),
365
+ call(tp0.trade_date, tp1.trade_date - timedelta(days=1), stop_at_rebalancing=True),
366
+ call(tp1.trade_date, tp2.trade_date - timedelta(days=1), stop_at_rebalancing=True),
367
+ call(tp2.trade_date, date.today(), stop_at_rebalancing=True),
368
368
  ]
369
369
 
370
370
  # Assert drift_weights was called as expected
@@ -373,7 +373,7 @@ class TestTradeProposal:
373
373
  # Test stopping replay on a non-approved proposal
374
374
  tp1.status = TradeProposal.Status.FAILED
375
375
  tp1.save()
376
- expected_calls = [call(tp0.trade_date, tp1.trade_date - timedelta(days=1))]
376
+ expected_calls = [call(tp0.trade_date, tp1.trade_date - timedelta(days=1), stop_at_rebalancing=True)]
377
377
  mock_fct.assert_has_calls(expected_calls)
378
378
 
379
379
  # Test estimating shares for a trade
@@ -577,11 +577,9 @@ class TestTradeProposal:
577
577
  # --- Create a trade proposal on d3 ---
578
578
  trade_proposal = trade_proposal_factory.create(portfolio=portfolio, trade_date=d3)
579
579
  trade_proposal.reset_trades()
580
-
581
580
  # Retrieve trades for each instrument
582
581
  trade_msft = trade_proposal.trades.get(underlying_instrument=msft)
583
582
  trade_apple = trade_proposal.trades.get(underlying_instrument=apple)
584
-
585
583
  # Check that trade weights are initially zero
586
584
  assert trade_msft.weighting == Decimal("0")
587
585
  assert trade_apple.weighting == Decimal("0")
@@ -627,3 +625,6 @@ class TestTradeProposal:
627
625
  # Final check that weights have been updated to 50%
628
626
  assert msft_a3.weighting == pytest.approx(target_weight, abs=Decimal("1e-6"))
629
627
  assert apple_a3.weighting == pytest.approx(target_weight, abs=Decimal("1e-6"))
628
+
629
+ def test_invalid_future_trade_proposal(self, trade_proposal):
630
+ pass
@@ -1,4 +1,5 @@
1
1
  import pandas as pd
2
+ import pytest
2
3
 
3
4
  from wbportfolio.pms.analytics.portfolio import Portfolio
4
5
 
@@ -15,9 +16,9 @@ def test_get_next_weights():
15
16
  portfolio = Portfolio(X=pd.DataFrame([returns]), weights=pd.Series(weights))
16
17
  next_weights = portfolio.get_next_weights()
17
18
 
18
- assert next_weights[0] == w0 * (r0 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1))
19
- assert next_weights[1] == w1 * (r1 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1))
20
- assert next_weights[2] == w2 * (r2 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1))
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)
21
22
 
22
23
 
23
24
  def test_get_estimate_net_value():
@@ -10,22 +10,28 @@ from wbportfolio.models import PortfolioPortfolioThroughModel, Trade, TradePropo
10
10
 
11
11
  @pytest.mark.django_db
12
12
  class TestEquallyWeightedRebalancing:
13
- @pytest.fixture()
14
- def model(self, portfolio, weekday):
13
+ def test_is_valid(self, portfolio, weekday, asset_position_factory, instrument_price_factory):
15
14
  from wbportfolio.rebalancing.models import EquallyWeightedRebalancing
16
15
 
17
- return EquallyWeightedRebalancing(portfolio, (weekday + BDay(1)).date(), weekday)
18
-
19
- def test_is_valid(self, portfolio, weekday, model, asset_position_factory, instrument_price_factory):
16
+ trade_date = (weekday + BDay(1)).date()
17
+ model = EquallyWeightedRebalancing(portfolio, trade_date, weekday)
20
18
  assert not model.is_valid()
21
- a = asset_position_factory.create(portfolio=model.portfolio, date=model.last_effective_date)
19
+
20
+ a = asset_position_factory.create(portfolio=portfolio, date=weekday)
21
+ model = EquallyWeightedRebalancing(portfolio, trade_date, weekday)
22
22
  assert not model.is_valid()
23
- instrument_price_factory.create(instrument=a.underlying_quote, date=model.trade_date)
23
+
24
+ instrument_price_factory.create(instrument=a.underlying_quote, date=trade_date)
25
+ model = EquallyWeightedRebalancing(portfolio, trade_date, weekday)
24
26
  assert model.is_valid()
25
27
 
26
- def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
27
- a1 = asset_position_factory(weighting=0.7, portfolio=portfolio, date=model.last_effective_date)
28
- a2 = asset_position_factory(weighting=0.3, portfolio=portfolio, date=model.last_effective_date)
28
+ def test_get_target_portfolio(self, portfolio, weekday, asset_position_factory):
29
+ from wbportfolio.rebalancing.models import EquallyWeightedRebalancing
30
+
31
+ a1 = asset_position_factory(weighting=0.7, portfolio=portfolio, date=weekday)
32
+ a2 = asset_position_factory(weighting=0.3, portfolio=portfolio, date=weekday)
33
+ model = EquallyWeightedRebalancing(portfolio, (weekday + BDay(1)).date(), weekday)
34
+
29
35
  target_portfolio = model.get_target_portfolio()
30
36
  target_positions = target_portfolio.positions_map
31
37
  assert target_positions[a1.underlying_instrument.id].weighting == Decimal(0.5)
@@ -41,6 +41,11 @@ class TradeProposalDisplayConfig(DisplayViewConfig):
41
41
  label=TradeProposal.Status.DENIED.label,
42
42
  value=TradeProposal.Status.DENIED.value,
43
43
  ),
44
+ dp.LegendItem(
45
+ icon=WBColor.RED_DARK.value,
46
+ label=TradeProposal.Status.FAILED.label,
47
+ value=TradeProposal.Status.FAILED.value,
48
+ ),
44
49
  ],
45
50
  ),
46
51
  ],
@@ -64,6 +69,10 @@ class TradeProposalDisplayConfig(DisplayViewConfig):
64
69
  style={"backgroundColor": WBColor.RED_LIGHT.value},
65
70
  condition=("==", TradeProposal.Status.DENIED.value),
66
71
  ),
72
+ dp.FormattingRule(
73
+ style={"backgroundColor": WBColor.RED_DARK.value},
74
+ condition=("==", TradeProposal.Status.FAILED.value),
75
+ ),
67
76
  ],
68
77
  )
69
78
  ],
@@ -100,7 +100,7 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
100
100
  trade_proposal = get_object_or_404(TradeProposal, pk=pk)
101
101
  if trade_proposal.status == TradeProposal.Status.DRAFT:
102
102
  trade_proposal.trades.all().delete()
103
- trade_proposal.reset_trades()
103
+ trade_proposal.reset_trades(force_reset_trade=True)
104
104
  return Response({"send": True})
105
105
  return Response({"status": "Trade proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
106
106
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.54.3
3
+ Version: 1.54.4
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=VZKPG_l8qT7qzoOc8oi5bNNQHBue4USPdc8sidVHIks,12544
116
+ wbportfolio/import_export/handlers/trade.py,sha256=t-iezNZN6s834EsszGh_5sHnFGgLb3MZpcHrF2pWMG8,12533
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
@@ -249,14 +249,15 @@ wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py,sha256=4
249
249
  wbportfolio/migrations/0077_remove_transaction_currency_and_more.py,sha256=Yf4a3zn2siDtWdIEPEIsj_W87jxOIBwiFVATneU8FxU,29597
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
+ wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py,sha256=vCPJSrM7x24jUJseGkHEVBD0c5nEpDTrb9-zkJ-0QoI,569
252
253
  wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
253
254
  wbportfolio/models/__init__.py,sha256=HSpa5xwh_MHQaBpNrq9E0CbdEE5Iq-pDLIsPzZ-TRTg,904
254
255
  wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
255
- wbportfolio/models/asset.py,sha256=9wD7d8rfK8MEogj0WgaZud9wQs5JGPR61gDQRU3W5as,45522
256
+ wbportfolio/models/asset.py,sha256=hhLv3Gyzr3-yd2bFA6wwYZ_i9XuqswzUIexdW1XKPWw,45634
256
257
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
257
258
  wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
258
259
  wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
259
- wbportfolio/models/portfolio.py,sha256=evF-_6lhVdo-fs0Y9-J6EQYO8HlOEHoayrALVbT6UgY,56822
260
+ wbportfolio/models/portfolio.py,sha256=Jp6MGZten0kKzNUmzD_2_NVSPWUY17L4eF3y8bZwynU,57506
260
261
  wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
261
262
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
262
263
  wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
@@ -282,24 +283,24 @@ wbportfolio/models/transactions/__init__.py,sha256=aV6lehRHSs8cOKOanm6UgSDqOwEzR
282
283
  wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWKAE6AY6YOYTs,25875
283
284
  wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
284
285
  wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
285
- wbportfolio/models/transactions/rebalancing.py,sha256=obzgewWKOD4kJbCoF5fhtfDk502QkbrjPKh8T9KDGew,7355
286
- wbportfolio/models/transactions/trade_proposals.py,sha256=ikc3QMSBBWcBwCdl67_G-OomHuC-Tb9I-KttKkbgWwc,32399
287
- wbportfolio/models/transactions/trades.py,sha256=yCySLaU0r1f1lHKPYlJ2W6uijGBV6N38WSzFTab1X6Q,33390
286
+ wbportfolio/models/transactions/rebalancing.py,sha256=rwePcmTZOYgfSWnBQcBrZ3DQHRJ3w17hdO_hgrRbbhI,7696
287
+ wbportfolio/models/transactions/trade_proposals.py,sha256=33RLqsknglMsESUb_d0gt0ehwyDIn3i4s7nOdyvsi5E,37832
288
+ wbportfolio/models/transactions/trades.py,sha256=7btwshl10-XVOrTYF9PXNyfuHGOttA11D0hkvOLEhgs,34126
288
289
  wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
289
290
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
290
- wbportfolio/pms/typing.py,sha256=izDCw5g6AUpqXriDDWa9KHpx7D7PSGLddBqhzXgryNQ,7875
291
+ wbportfolio/pms/typing.py,sha256=BV4dzazNHdfpfLV99bLVyYGcETmbQSnFV6ipc4fNKfg,8470
291
292
  wbportfolio/pms/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
292
- wbportfolio/pms/analytics/portfolio.py,sha256=vE0KA6Z037bUdmBTkYuBqXElt80nuYObNzY_kWvxEZY,1360
293
+ wbportfolio/pms/analytics/portfolio.py,sha256=n_tv4gX-mOFfKCxhSGblqQzW62Bx-yoM2QcJxQd3dLE,1384
293
294
  wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
294
295
  wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
295
- wbportfolio/pms/trading/handler.py,sha256=kKUPuZ7bVmvzVteVQWPsmcPxBHpUAjRgO66P5Lfi7NY,8619
296
+ wbportfolio/pms/trading/handler.py,sha256=ZOwgnOU4ScVIhTMRQ0SLR2cCCZP9whmVv-S5hF-TOME,8593
296
297
  wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
297
- wbportfolio/rebalancing/base.py,sha256=NwTGZtBm1f35gj5Jp6iTyyFvDT1GSIztN990cKBvYzQ,637
298
+ wbportfolio/rebalancing/base.py,sha256=wpeoxdkLz5osxm5mRjkOoML7YkYvwuAlqSLLtHBbWp8,984
298
299
  wbportfolio/rebalancing/decorators.py,sha256=JhQ2vkcIuGBTGvNmpkQerdw2-vLq-RAb0KAPjOrETkw,515
299
300
  wbportfolio/rebalancing/models/__init__.py,sha256=AQjG7Tu5vlmhqncVoYOjpBKU2UIvgo9FuP2_jD2w-UI,232
300
301
  wbportfolio/rebalancing/models/composite.py,sha256=XEgK3oMurrE_d_l5uN0stBKRrtvnKQzRWyXNXuBYfmc,1818
301
- wbportfolio/rebalancing/models/equally_weighted.py,sha256=FCpSKOs49ckNYVgoYIiHB0BqPT9OeCMuFoet4Ixbp-Y,1210
302
- wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=6ZsR8iJg6l89CsxHqoxJSXlTaj8Pmb8_bFPrXhnxaRs,5295
302
+ wbportfolio/rebalancing/models/equally_weighted.py,sha256=nqkiCnDfazC0AZeRPMVtqRzbDHdW8P2FCMYXEvmwOMw,1062
303
+ wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=BJUoJRQ-qTG3cevPKm_3HMy4lVNk6wuxHGuUobPkrMY,5523
303
304
  wbportfolio/rebalancing/models/model_portfolio.py,sha256=Dk3tw9u3WG1jCP3V2-R05GS4-DmDBBtxH4h6p7pRe4g,1393
304
305
  wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
305
306
  wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
@@ -348,7 +349,7 @@ wbportfolio/serializers/transactions/__init__.py,sha256=oAfidhjjCKP0exeHbzJgGuBd
348
349
  wbportfolio/serializers/transactions/claim.py,sha256=kC4E2RZRrpd9i8tGfoiV-gpWDk3ikR5F1Wf0v_IGIvw,11599
349
350
  wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
350
351
  wbportfolio/serializers/transactions/fees.py,sha256=3mBzs6vdfST9roeQB-bmLJhipY7i5jBtAXjoTTE-GOg,2388
351
- wbportfolio/serializers/transactions/trade_proposals.py,sha256=ufYBYiSttz5KAlAaaXbnf98oUFT_qNfxF_VUM4ClXE8,4072
352
+ wbportfolio/serializers/transactions/trade_proposals.py,sha256=2oX04DSyiQ5C0-XkB7c0_wDJ-yC0niRgC72pXPPa_Xc,4014
352
353
  wbportfolio/serializers/transactions/trades.py,sha256=YVccQpP480P4-0uVaRfnmpPFoIdW2U0c92kJBR_fPLo,16889
353
354
  wbportfolio/static/wbportfolio/css/macro_review.css,sha256=FAVVO8nModxwPXcTKpcfzVxBGPZGJVK1Xn-0dkSfGyc,233
354
355
  wbportfolio/static/wbportfolio/markdown/documentation/account_holding_reconciliation.md,sha256=MabxOvOne8s5gl6osDoow6-3ghaXLAYg9THWpvy6G5I,921
@@ -379,7 +380,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
379
380
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
380
381
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
381
382
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
382
- wbportfolio/tests/models/test_portfolios.py,sha256=L_NTLYdLgKkllXlnjX8aBOFl95zBJq8gjQySuhQG1T0,53320
383
+ wbportfolio/tests/models/test_portfolios.py,sha256=sGmPrl-nY1-3797SDTRWAGF2xXZGD94QS3WeabfBmC0,53397
383
384
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
384
385
  wbportfolio/tests/models/test_products.py,sha256=IcBzw9hrGiWFMRwPBTMukCMWrhqnjOVA2hhb90xYOW8,9580
385
386
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -389,12 +390,12 @@ wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
389
390
  wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
390
391
  wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
391
392
  wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
392
- wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=eXSs_xjmfdmmkKy6_MMWpyzir6j2FIhKsBQUpzrHNMo,28370
393
+ wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=jnE5Ie0g05TVkuSE9uKsqfidYDU2ZN3FLg1aveLzj9c,28558
393
394
  wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0Wq9ycA2oeeEBhO3dzV7sbXEU,9863
394
395
  wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
395
- wbportfolio/tests/pms/test_analytics.py,sha256=fAuY1zcXibttFpBh2GhKVyzdYfi1kz_b7SPa9xZQXY0,1086
396
+ wbportfolio/tests/pms/test_analytics.py,sha256=WHicJBjAjpIRL1-AW2nZ4VD9oJRpMoeH6V1Qx2D95-w,1178
396
397
  wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
397
- wbportfolio/tests/rebalancing/test_models.py,sha256=4rSb05xKH881ft12G0B8ZSeFgJvutKcW_7vFgVQa0DI,7813
398
+ wbportfolio/tests/rebalancing/test_models.py,sha256=QMfcYDvFew1bH6kPm-jVJLC_RqmPE-oGTqUldx1KVgg,8025
398
399
  wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
399
400
  wbportfolio/tests/serializers/test_claims.py,sha256=vQrg73xQXRFEgvx3KI9ivFre_wpBFzdO0p0J13PkvdY,582
400
401
  wbportfolio/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -458,7 +459,7 @@ wbportfolio/viewsets/configs/display/rebalancing.py,sha256=yw9X1Nf2-V_KP_mCX4pVK
458
459
  wbportfolio/viewsets/configs/display/reconciliations.py,sha256=YvMAuwmpX0HExvGsuf5UvcRQxe4eMo1iyNJX68GGC_k,6021
459
460
  wbportfolio/viewsets/configs/display/registers.py,sha256=1np75exIk5rfct6UkVN_RnfJ9ozvIkcWJgFV4_4rJns,3182
460
461
  wbportfolio/viewsets/configs/display/roles.py,sha256=SFUyCdxSlHZ3NsMrJmpVBSlg-XKGaEFteV89nyLMMAQ,1815
461
- wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=gM5CuUmNcVtwJHYE9Myg-RR_WmAc6VDJztwUqQ1uZ7g,4335
462
+ wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=EiPNzW-jntXsJnJQ3HSKSDQBjkjQERaEXP0DCRq6F5M,4826
462
463
  wbportfolio/viewsets/configs/display/trades.py,sha256=ZYZ2ceE4Hyw2SpQjW00VDnzGz7Wlu3jjPoK_J2GZLyk,19181
463
464
  wbportfolio/viewsets/configs/endpoints/__init__.py,sha256=KR3AsSxl71VAVkYBRVowgs3PZB8vaKa34WHHUvC-2MY,2807
464
465
  wbportfolio/viewsets/configs/endpoints/adjustments.py,sha256=KRZLqv4ZeB27d-_s7Qlqj9LI3I9WFJkjc6Mw8agDueE,606
@@ -519,9 +520,9 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
519
520
  wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
520
521
  wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
521
522
  wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
522
- wbportfolio/viewsets/transactions/trade_proposals.py,sha256=zdb9yqyAE1-hcMmxxN3CYePWoPVWIW3WP3v8agErDeY,6210
523
+ wbportfolio/viewsets/transactions/trade_proposals.py,sha256=eDEIEUyMP70oEdwscnE8oR7pBStr7IjHNFMxu2JbJlY,6232
523
524
  wbportfolio/viewsets/transactions/trades.py,sha256=GHOw5jtcqoaHiRrxxxL29c9405QiPisEn4coGELKDrE,22146
524
- wbportfolio-1.54.3.dist-info/METADATA,sha256=3TfcDEJ2iLoB0u0mqHuTAJCcOUGH6yvebXUJMWpHF9c,702
525
- wbportfolio-1.54.3.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
526
- wbportfolio-1.54.3.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
527
- wbportfolio-1.54.3.dist-info/RECORD,,
525
+ wbportfolio-1.54.4.dist-info/METADATA,sha256=N_ZubDWqgnEeWE58IyCdWzurZlbqE82G1vjnkT3dIok,702
526
+ wbportfolio-1.54.4.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
527
+ wbportfolio-1.54.4.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
528
+ wbportfolio-1.54.4.dist-info/RECORD,,