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

@@ -24,6 +24,10 @@ def file_name_parse(file_name):
24
24
 
25
25
 
26
26
  def parse(import_source):
27
+ bank_exclusion_list = [
28
+ excluded_bank.strip().lower()
29
+ for excluded_bank in import_source.source.import_parameters.get("bank_exclusion_list", [])
30
+ ]
27
31
  # Load files into a CSV DictReader
28
32
  df = pd.read_csv(import_source.file, encoding="latin1", delimiter=",")
29
33
  df = df.replace([np.inf, -np.inf, np.nan], None)
@@ -42,18 +46,22 @@ def parse(import_source):
42
46
  # Check whether it is a buy or a sell and convert the value correspondely
43
47
  shares = shares if nominal_data["Side"] == "S" else shares * -1
44
48
  portfolio = product.primary_portfolio
45
- data.append(
46
- {
47
- "underlying_instrument": {"id": product.id, "instrument_type": "product"},
48
- "transaction_date": nominal_data["Trade Date"].strftime("%Y-%m-%d"),
49
- "shares": shares,
50
- "portfolio": portfolio.id,
51
- # 'currency': product.currency.key,
52
- "transaction_subtype": Trade.Type.REDEMPTION if shares < 0 else Trade.Type.SUBSCRIPTION,
53
- "bank": nominal_data["CounterParty Name"],
54
- "price": convert_string_to_number(nominal_data["Price"]),
55
- }
56
- )
49
+ bank = nominal_data["CounterParty Name"]
50
+ if (
51
+ len(bank_exclusion_list) == 0 or bank.strip().lower() not in bank_exclusion_list
52
+ ): # we do basic string comparison to exclude appropriate banks. We might want to include regex if bank data is inconsistent
53
+ data.append(
54
+ {
55
+ "underlying_instrument": {"id": product.id, "instrument_type": "product"},
56
+ "transaction_date": nominal_data["Trade Date"].strftime("%Y-%m-%d"),
57
+ "shares": shares,
58
+ "portfolio": portfolio.id,
59
+ # 'currency': product.currency.key,
60
+ "transaction_subtype": Trade.Type.REDEMPTION if shares < 0 else Trade.Type.SUBSCRIPTION,
61
+ "bank": bank,
62
+ "price": convert_string_to_number(nominal_data["Price"]),
63
+ }
64
+ )
57
65
  import_data = {"data": data}
58
66
  if "isin" in parts:
59
67
  product = Product.objects.get(isin=parts["isin"])
@@ -2,6 +2,8 @@ import json
2
2
  from contextlib import suppress
3
3
  from datetime import datetime
4
4
 
5
+ from wbportfolio.models import Product
6
+
5
7
 
6
8
  def parse(import_source):
7
9
  data = []
@@ -9,12 +11,13 @@ def parse(import_source):
9
11
  series_data = json.loads(import_source.file.read())["payload"]["series"]
10
12
  for series in series_data:
11
13
  isin = series["item"]["priceIdentifier"]
12
- for point in series["points"]:
13
- data.append(
14
- {
15
- "instrument": {"isin": isin},
16
- "date": datetime.fromtimestamp(int(point["timestamp"]) / 1000).strftime("%Y-%m-%d"),
17
- "net_value": point["close"],
18
- }
19
- )
14
+ if Product.objects.filter(isin=isin).exists(): # ensure the timeseries contain data for products we handle
15
+ for point in series["points"]:
16
+ data.append(
17
+ {
18
+ "instrument": {"isin": isin},
19
+ "date": datetime.fromtimestamp(int(point["timestamp"]) / 1000).strftime("%Y-%m-%d"),
20
+ "net_value": point["close"],
21
+ }
22
+ )
20
23
  return {"data": data}
@@ -15,6 +15,7 @@ from .utils import networkx_graph_to_plotly
15
15
  class PortfolioGraph:
16
16
  def __init__(self, portfolio: Portfolio, val_date: date, **graph_kwargs):
17
17
  self.graph = pydot.Dot("Portfolio Tree", strict=True, **graph_kwargs)
18
+ self.base_portfolio = portfolio
18
19
  self.discovered_portfolios = set()
19
20
  self.val_date = val_date
20
21
  self._extend_portfolio_graph(portfolio)
@@ -31,13 +32,17 @@ class PortfolioGraph:
31
32
  pydot.Node(
32
33
  str(parent_portfolio.id),
33
34
  label=self._convert_to_multilines(str(parent_portfolio)),
34
- shape="circle",
35
- orientation="45",
36
- style="solid",
35
+ **self._get_node_kwargs(parent_portfolio),
37
36
  )
38
37
  )
38
+ self.graph.del_edge((str(portfolio.id), str(parent_portfolio.id)))
39
39
  self.graph.add_edge(
40
- pydot.Edge(str(portfolio.id), str(parent_portfolio.id), label=f"{weighting:.2%}", style="dashed")
40
+ pydot.Edge(
41
+ str(portfolio.id),
42
+ str(parent_portfolio.id),
43
+ label=f"Invest in ({weighting:.2%})",
44
+ style="dashed",
45
+ )
41
46
  )
42
47
  # composition_edges.append((str(portfolio.id), str(parent_portfolio.id)))
43
48
  self._extend_parent_portfolios_to_graph(parent_portfolio)
@@ -49,28 +54,42 @@ class PortfolioGraph:
49
54
  pydot.Node(
50
55
  str(child_portfolio.id),
51
56
  label=self._convert_to_multilines(str(child_portfolio)),
52
- shape="circle",
53
- orientation="45",
54
- style="solid",
57
+ **self._get_node_kwargs(child_portfolio),
55
58
  )
56
59
  )
57
- # self.graph.add_edge(pydot.Edge(str(child_portfolio.id), str(portfolio.id), label="child", style="dashed"))
60
+ # we add this edge only if the opposite relationship is not already added
61
+ graph_edge = pydot.Edge(str(child_portfolio.id), str(portfolio.id), label="implements", style="dashed")
62
+ if (graph_edge.get_source(), graph_edge.get_destination()) not in self.graph.obj_dict["edges"]:
63
+ self.graph.add_edge(graph_edge)
58
64
  # composition_edges.append((str(child_portfolio.id), str(portfolio.id)))
59
65
  self._extend_child_portfolios_to_graph(child_portfolio)
60
66
 
67
+ def _get_node_kwargs(self, portfolio):
68
+ node_args = {
69
+ "shape": "circle",
70
+ "orientation": "45",
71
+ }
72
+ if portfolio == self.base_portfolio:
73
+ node_args.update({"style": "filled", "fillcolor": "lightgrey"})
74
+ else:
75
+ node_args.update(
76
+ {
77
+ "style": "solid",
78
+ }
79
+ )
80
+ return node_args
81
+
61
82
  def _extend_portfolio_graph(self, portfolio):
62
83
  self.graph.add_node(
63
84
  pydot.Node(
64
85
  str(portfolio.id),
65
86
  label=self._convert_to_multilines(str(portfolio)),
66
- shape="circle",
67
- orientation="45",
68
- style="solid",
87
+ **self._get_node_kwargs(portfolio),
69
88
  )
70
89
  )
71
90
 
72
- self._extend_parent_portfolios_to_graph(portfolio)
73
91
  self._extend_child_portfolios_to_graph(portfolio)
92
+ self._extend_parent_portfolios_to_graph(portfolio)
74
93
 
75
94
  # composition_edges = []
76
95
  # if composition_edges:
@@ -87,37 +106,35 @@ class PortfolioGraph:
87
106
  pydot.Node(
88
107
  str(rel.portfolio.id),
89
108
  label=self._convert_to_multilines(str(rel.portfolio)),
90
- shape="square",
91
- orientation="45",
92
- style="solid",
109
+ **self._get_node_kwargs(rel.portfolio),
93
110
  )
94
111
  )
95
112
  self.graph.add_node(
96
113
  pydot.Node(
97
114
  str(rel.dependency_portfolio.id),
98
115
  label=self._convert_to_multilines(str(rel.dependency_portfolio)),
99
- shape="square",
100
- orientation="45",
101
- style="solid",
116
+ **self._get_node_kwargs(rel.dependency_portfolio),
102
117
  )
103
118
  )
104
119
  label = PortfolioPortfolioThroughModel.Type[rel.type].label
105
120
  if rel.dependency_portfolio.is_composition:
106
121
  label += " (Composition)"
107
- self.graph.add_edge(
108
- pydot.Edge(
109
- str(rel.dependency_portfolio.id),
110
- str(rel.portfolio.id),
111
- label=label,
112
- style="bold",
113
- )
114
- )
122
+
115
123
  if rel.portfolio.is_lookthrough and rel.type == PortfolioPortfolioThroughModel.Type.PRIMARY:
116
124
  self.graph.add_edge(
117
125
  pydot.Edge(
118
126
  str(rel.portfolio.id), str(rel.dependency_portfolio.id), label="Look-Through", style="dotted"
119
127
  )
120
128
  )
129
+ else:
130
+ self.graph.add_edge(
131
+ pydot.Edge(
132
+ str(rel.portfolio.id),
133
+ str(rel.dependency_portfolio.id),
134
+ label=label,
135
+ style="bold",
136
+ )
137
+ )
121
138
  if rel.dependency_portfolio not in self.discovered_portfolios:
122
139
  self._extend_portfolio_graph(rel.dependency_portfolio)
123
140
  if rel.portfolio not in self.discovered_portfolios:
@@ -802,7 +802,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
802
802
  adjusted_currency_fx_rate,
803
803
  adjusted_is_estimated,
804
804
  portfolio_created=None,
805
+ path=None,
805
806
  ):
807
+ if not path:
808
+ path = []
809
+ path.append(parent_portfolio)
806
810
  for position in parent_portfolio.assets.filter(date=sync_date):
807
811
  position.id = None
808
812
  position.weighting = adjusted_weighting * position.weighting
@@ -811,7 +815,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
811
815
  position.weighting == 1.0
812
816
  )
813
817
  position.portfolio_created = portfolio_created
814
- position.parent_portfolio = parent_portfolio
818
+ setattr(position, "path", path)
815
819
  position.initial_shares = None
816
820
  if portfolio_total_asset_value:
817
821
  position.initial_shares = (position.weighting * portfolio_total_asset_value) / (
@@ -826,8 +830,9 @@ class Portfolio(DeleteToDisableMixin, WBModel):
826
830
  position.currency_fx_rate,
827
831
  position.is_estimated,
828
832
  portfolio_created=child_portfolio,
833
+ path=path.copy(),
829
834
  )
830
- elif position.weighting: # we do not yield postion with weight 0 because of issue with certain multi-thematic portfolios which contain duplicates
835
+ elif position.weighting: # we do not yield position with weight 0 because of issue with certain multi-thematic portfolios which contain duplicates
831
836
  yield position
832
837
 
833
838
  yield from _crawl_portfolio(self, Decimal(1.0), Decimal(1.0), False)
@@ -8,6 +8,7 @@ from django.dispatch import receiver
8
8
  from django.utils.functional import cached_property
9
9
  from django.utils.module_loading import autodiscover_modules
10
10
  from django.utils.translation import gettext_lazy as _
11
+ from pandas._libs.tslibs.offsets import BDay
11
12
  from wbcore.utils.importlib import import_from_dotted_path
12
13
  from wbcore.utils.models import ComplexToStringMixin
13
14
  from wbcore.utils.rrules import convert_rrulestr_to_dict, humanize_rrule
@@ -41,10 +42,10 @@ class RebalancingModel(models.Model):
41
42
  def get_target_portfolio(
42
43
  self, portfolio: Portfolio, trade_date: date, last_effective_date: date, **kwargs
43
44
  ) -> PortfolioDTO:
44
- model = self.model_class(portfolio, trade_date, last_effective_date)
45
+ model = self.model_class(portfolio, trade_date, last_effective_date, **kwargs)
45
46
  if not model.is_valid():
46
47
  raise ValidationError("Rebalacing cannot applied for these parameters")
47
- return model.get_target_portfolio(**kwargs)
48
+ return model.get_target_portfolio()
48
49
 
49
50
  @classmethod
50
51
  def get_representation_endpoint(cls) -> str:
@@ -86,9 +87,22 @@ class Rebalancer(ComplexToStringMixin, models.Model):
86
87
  self.activation_date = date.today()
87
88
  super().save(*args, **kwargs)
88
89
 
90
+ def _get_next_valid_date(self, valid_date: date) -> date:
91
+ pivot_date = valid_date
92
+ while TradeProposal.objects.filter(
93
+ portfolio=self.portfolio, status=TradeProposal.Status.FAILED, trade_date=pivot_date
94
+ ).exists():
95
+ pivot_date += BDay(1)
96
+ return pivot_date
97
+
89
98
  def is_valid(self, trade_date: date) -> bool:
90
- valid_dates = [_d.date() for _d in self.get_rrule(trade_date)]
91
- return trade_date in valid_dates
99
+ for valid_datetime in self.get_rrule(trade_date):
100
+ valid_date = self._get_next_valid_date(valid_datetime.date())
101
+ if valid_date == trade_date:
102
+ return True
103
+ if valid_date > trade_date:
104
+ break
105
+ return False
92
106
 
93
107
  def evaluate_rebalancing(self, trade_date: date):
94
108
  trade_proposal, _ = TradeProposal.objects.get_or_create(
@@ -101,17 +115,18 @@ class Rebalancer(ComplexToStringMixin, models.Model):
101
115
  )
102
116
  if trade_proposal.rebalancing_model == self.rebalancing_model:
103
117
  trade_proposal.status = TradeProposal.Status.DRAFT
104
- target_portfolio = self.rebalancing_model.get_target_portfolio(
105
- self.portfolio, trade_date, trade_proposal.last_effective_date, **self.parameters
106
- )
107
118
  try:
119
+ target_portfolio = self.rebalancing_model.get_target_portfolio(
120
+ self.portfolio, trade_date, trade_proposal.last_effective_date, **self.parameters
121
+ )
108
122
  trade_proposal.reset_trades(target_portfolio)
109
123
  trade_proposal.submit()
124
+ if self.approve_trade_proposal_automatically and self.portfolio.can_be_rebalanced:
125
+ trade_proposal.approve()
110
126
  except ValidationError:
111
- pass # Do something
127
+ # If we encountered a validation error, we set the trade proposal as failed
128
+ trade_proposal.status = TradeProposal.Status.FAILED
112
129
 
113
- if self.approve_trade_proposal_automatically and self.portfolio.can_be_rebalanced:
114
- trade_proposal.approve()
115
130
  trade_proposal.save()
116
131
 
117
132
  return trade_proposal
@@ -38,6 +38,7 @@ class TradeProposal(RiskCheckMixin, WBModel):
38
38
  SUBMIT = "SUBMIT", "Submit"
39
39
  APPROVED = "APPROVED", "Approved"
40
40
  DENIED = "DENIED", "Denied"
41
+ FAILED = "FAILED", "Failed"
41
42
 
42
43
  comment = models.TextField(default="", verbose_name="Trade Comment", blank=True)
43
44
  status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name="Status")
@@ -4,7 +4,7 @@ from wbportfolio.pms.typing import Portfolio as PortfolioDTO
4
4
 
5
5
 
6
6
  class AbstractRebalancingModel:
7
- def __init__(self, portfolio, trade_date: date, last_effective_date: date):
7
+ def __init__(self, portfolio, trade_date: date, last_effective_date: date, **kwargs):
8
8
  self.portfolio = portfolio
9
9
  self.trade_date = trade_date
10
10
  self.last_effective_date = last_effective_date
@@ -12,5 +12,5 @@ class AbstractRebalancingModel:
12
12
  def is_valid(self) -> bool:
13
13
  return True
14
14
 
15
- def get_target_portfolio(self, **kwargs) -> PortfolioDTO:
15
+ def get_target_portfolio(self) -> PortfolioDTO:
16
16
  raise NotImplementedError()
@@ -1,3 +1,4 @@
1
1
  from .composite import CompositeRebalancing
2
2
  from .model_portfolio import ModelPortfolioRebalancing
3
3
  from .equally_weighted import EquallyWeightedRebalancing
4
+ from .market_capitalization_weighted import MarketCapitalizationRebalancing
@@ -22,7 +22,7 @@ class CompositeRebalancing(AbstractRebalancingModel):
22
22
  def is_valid(self) -> bool:
23
23
  return len(self.base_assets.keys()) > 0
24
24
 
25
- def get_target_portfolio(self, **kwargs) -> Portfolio:
25
+ def get_target_portfolio(self) -> Portfolio:
26
26
  positions = []
27
27
  for underlying_instrument, weighting in self.base_assets.items():
28
28
  positions.append(
@@ -10,7 +10,7 @@ class EquallyWeightedRebalancing(AbstractRebalancingModel):
10
10
  def is_valid(self) -> bool:
11
11
  return self.portfolio.assets.filter(date=self.last_effective_date).exists()
12
12
 
13
- def get_target_portfolio(self, **kwargs) -> Portfolio:
13
+ def get_target_portfolio(self) -> Portfolio:
14
14
  positions = []
15
15
  assets = self.portfolio.assets.filter(date=self.last_effective_date)
16
16
  nb_assets = assets.count()
@@ -0,0 +1,85 @@
1
+ from decimal import Decimal
2
+
3
+ import pandas as pd
4
+ from django.db.models import Q, QuerySet
5
+ from wbfdm.enums import MarketData
6
+ from wbfdm.models import Classification, Instrument, InstrumentClassificationThroughModel
7
+
8
+ from wbportfolio.pms.typing import Portfolio, Position
9
+ from wbportfolio.rebalancing.base import AbstractRebalancingModel
10
+ from wbportfolio.rebalancing.decorators import register
11
+
12
+
13
+ @register("Market Capitalization Rebalancing")
14
+ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
15
+ TARGET_CURRENCY: str = "USD"
16
+
17
+ def __init__(self, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+ instruments = self._get_instruments(**kwargs)
20
+ self.market_cap_df = pd.Series()
21
+ df = pd.DataFrame(
22
+ instruments.dl.market_data(
23
+ values=[MarketData.MARKET_CAPITALIZATION],
24
+ from_date=self.last_effective_date,
25
+ to_date=self.trade_date,
26
+ target_currency=self.TARGET_CURRENCY,
27
+ )
28
+ )
29
+ try:
30
+ df = df[["valuation_date", "market_capitalization", "instrument_id"]].pivot_table(
31
+ index="valuation_date", columns="instrument_id", values="market_capitalization"
32
+ )
33
+ df = df.ffill()
34
+ self.market_cap_df = df.iloc[-1, :].transpose()
35
+ except (IndexError, KeyError):
36
+ self.market_cap_df = pd.Series()
37
+
38
+ def _get_instruments(
39
+ self, classification_ids: list[int] | None = None, instrument_ids: list[int] | None = None, **kwargs
40
+ ) -> QuerySet[Instrument]:
41
+ """
42
+ Use the provided kwargs to return a list of instruments as universe.
43
+ - If classifications are given, we returns all the instrument linked to these classifications
44
+ - Or directly from a static list of instrument ids
45
+ - fallback to the last effective portfolio underlying instruments list
46
+ """
47
+ if classification_ids:
48
+ classifications = set()
49
+ for classification in Classification.objects.filter(id__in=classification_ids):
50
+ for children in classification.get_descendants(include_self=True):
51
+ classifications.add(children)
52
+ instrument_ids = list(
53
+ InstrumentClassificationThroughModel.objects.filter(classification__in=classifications).values_list(
54
+ "id", flat=True
55
+ )
56
+ )
57
+ elif not instrument_ids:
58
+ instrument_ids = list(
59
+ self.portfolio.assets.filter(date=self.last_effective_date).values_list(
60
+ "underlying_instrument", flat=True
61
+ )
62
+ )
63
+ return Instrument.objects.filter(id__in=instrument_ids).filter(
64
+ Q(delisted_date__isnull=True) | Q(delisted_date__gt=self.trade_date)
65
+ )
66
+
67
+ def is_valid(self) -> bool:
68
+ return (
69
+ not self.market_cap_df.empty and not self.market_cap_df.isnull().any()
70
+ ) # if we are missing any market cap for not-delisted instrument, we consider the rebalancing not valid
71
+
72
+ def get_target_portfolio(self) -> Portfolio:
73
+ positions = []
74
+ market_cap_df = self.market_cap_df
75
+ total_market_cap = market_cap_df.sum()
76
+
77
+ for underlying_instrument, market_cap in market_cap_df.to_dict().items():
78
+ positions.append(
79
+ Position(
80
+ underlying_instrument=underlying_instrument,
81
+ weighting=Decimal(market_cap / total_market_cap),
82
+ date=self.trade_date,
83
+ )
84
+ )
85
+ return Portfolio(positions=tuple(positions))
@@ -24,7 +24,7 @@ class ModelPortfolioRebalancing(AbstractRebalancingModel):
24
24
  else False
25
25
  )
26
26
 
27
- def get_target_portfolio(self, **kwargs) -> Portfolio:
27
+ def get_target_portfolio(self) -> Portfolio:
28
28
  positions = []
29
29
  assets = self.model_portfolio.get_positions(self.last_effective_date)
30
30
 
@@ -46,19 +46,25 @@ class RuleBackend(ActivePortfolioRelationshipMixin, backend.AbstractRuleBackend)
46
46
  def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
47
47
  if not (df := self._filter_df(pd.DataFrame(portfolio.to_df()).astype({"weighting": float}))).empty:
48
48
  df = df[["underlying_instrument", "weighting"]].groupby("underlying_instrument").sum()
49
- total_weight_threshold_1_2 = df.loc[df["weighting"] < self.threshold_2, "weighting"].sum()
49
+ total_weight_threshold_1_2 = df["weighting"].sum()
50
50
  highest_incident_type = RiskIncidentType.objects.order_by("-severity_order").first()
51
+
51
52
  for id, row in df.to_dict("index").items():
52
53
  if (row["weighting"] > self.threshold_2) or (total_weight_threshold_1_2 > self.threshold_3):
53
54
  instrument = Instrument.objects.get(id=id)
55
+ breached_value = f"""
56
+ Sum >= {self.threshold_1:.2%}: {total_weight_threshold_1_2:+.2%}
57
+ """
58
+ if row["weighting"] > self.threshold_2:
59
+ breached_value += f"<br>Instrument >= {self.threshold_2:.2%}: {instrument.name_repr}"
54
60
  yield backend.IncidentResult(
55
61
  breached_object=instrument,
56
62
  breached_object_repr=str(instrument),
57
- breached_value=f'∑[0%, {self.threshold_2:.2%}]: {row["weighting"]:+.2%} | ∑[{self.threshold_1:.2%}, {self.threshold_2:.2%}]: {total_weight_threshold_1_2:+.2%}',
63
+ breached_value=breached_value,
58
64
  report_details={
59
- "Breach Thresholds": f"{self.threshold_1}|{self.threshold_2}|{self.threshold_3}",
60
- "Weighting": f"{row['weighting']:.3f}",
61
- f"Sum of positions whose weight are between {self.threshold_1:.2%} and {self.threshold_2:.2%}": f"{total_weight_threshold_1_2:.3f}",
65
+ "Breach Thresholds": f"{self.threshold_1:.2%}|{self.threshold_2:.2%}|{self.threshold_3:.2%}",
66
+ "Weighting": f"{row['weighting']:+.2%}",
67
+ f"Sum of positions > {self.threshold_1:.2%}": f"{total_weight_threshold_1_2:+.2%}",
62
68
  },
63
69
  severity=highest_incident_type,
64
70
  )
@@ -19,7 +19,7 @@ class TestUcitsRuleModel(PortfolioTestMixin):
19
19
  # Check No single asset can represent more than 10% of the fund's assets;
20
20
  asset_position_factory.create(date=weekday, weighting=0.05, portfolio=portfolio)
21
21
  asset_position_factory.create(date=weekday, weighting=0.05, portfolio=portfolio)
22
- a3 = asset_position_factory.create(date=weekday, weighting=0.90, portfolio=portfolio)
22
+ a3 = asset_position_factory.create(date=weekday, weighting=0.15, portfolio=portfolio)
23
23
 
24
24
  res = list(ucits_backend.check_rule())
25
25
  assert len(res) == 1
@@ -1114,3 +1114,28 @@ class TestPortfolioModel(PortfolioTestMixin):
1114
1114
  undependant_portfolio = portfolio_factory.create(name="undependant portfolio", id=4)
1115
1115
  res = list(Portfolio.objects.all().to_dependency_iterator(weekday))
1116
1116
  assert res == [index_portfolio, dependency_portfolio, dependant_portfolio, undependant_portfolio]
1117
+
1118
+ def test_get_returns(self, instrument_factory, instrument_price_factory, portfolio):
1119
+ v1 = date(2025, 1, 1)
1120
+ v2 = date(2025, 1, 2)
1121
+ v3 = date(2025, 1, 3)
1122
+
1123
+ i1 = instrument_factory.create()
1124
+ i2 = instrument_factory.create()
1125
+
1126
+ i11 = instrument_price_factory.create(date=v1, instrument=i1)
1127
+ i12 = instrument_price_factory.create(date=v2, instrument=i1)
1128
+ i13 = instrument_price_factory.create(date=v3, instrument=i1)
1129
+ i11.refresh_from_db()
1130
+ i12.refresh_from_db()
1131
+ i13.refresh_from_db()
1132
+ returns, _ = portfolio.get_returns([i1.id, i2.id], from_date=v1, to_date=v3)
1133
+
1134
+ expected_returns = pd.DataFrame(
1135
+ [[i12.net_value / i11.net_value - 1], [i13.net_value / i12.net_value - 1]],
1136
+ index=[v2, v3],
1137
+ columns=[i1.id],
1138
+ dtype="float64",
1139
+ )
1140
+ expected_returns.index = pd.to_datetime(expected_returns.index)
1141
+ pd.testing.assert_frame_equal(returns, expected_returns, check_names=False, check_freq=False, atol=1e-6)
@@ -1,7 +1,9 @@
1
1
  from decimal import Decimal
2
2
 
3
+ import numpy as np
3
4
  import pytest
4
5
  from pandas._libs.tslibs.offsets import BDay
6
+ from wbfdm.models import InstrumentPrice
5
7
 
6
8
  from wbportfolio.factories import PortfolioFactory, TradeFactory, TradeProposalFactory
7
9
  from wbportfolio.models import PortfolioPortfolioThroughModel, Trade, TradeProposal
@@ -125,3 +127,35 @@ class TestCompositeRebalancing:
125
127
  target_positions = target_portfolio.positions_map
126
128
  assert target_positions[t1.underlying_instrument.id].weighting == Decimal("0.800000")
127
129
  assert target_positions[t2.underlying_instrument.id].weighting == Decimal("0.200000")
130
+
131
+
132
+ @pytest.mark.django_db
133
+ class TestMarketCapitalizationRebalancing:
134
+ @pytest.fixture()
135
+ def model(self, portfolio, weekday, instrument_factory, instrument_price_factory):
136
+ from wbportfolio.rebalancing.models import MarketCapitalizationRebalancing
137
+
138
+ last_effective_date = (weekday - BDay(1)).date()
139
+
140
+ i1 = instrument_factory()
141
+ i2 = instrument_factory()
142
+ instrument_price_factory.create(instrument=i1, date=last_effective_date)
143
+ instrument_price_factory.create(instrument=i1, date=weekday)
144
+ instrument_price_factory.create(instrument=i2, date=last_effective_date) # weekday is ffill
145
+ return MarketCapitalizationRebalancing(portfolio, weekday, last_effective_date, instrument_ids=[i1.id, i2.id])
146
+
147
+ def test_is_valid(self, portfolio, weekday, model, instrument_factory, instrument_price_factory):
148
+ assert model.is_valid()
149
+ model.market_cap_df.iloc[0] = np.nan
150
+ assert not model.is_valid()
151
+
152
+ def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
153
+ i1 = model.market_cap_df.index[0]
154
+ i2 = model.market_cap_df.index[1]
155
+ mkt12 = InstrumentPrice.objects.get(instrument_id=i1, date=model.trade_date).market_capitalization
156
+ mkt21 = InstrumentPrice.objects.get(instrument_id=i2, date=model.last_effective_date).market_capitalization
157
+
158
+ target_portfolio = model.get_target_portfolio()
159
+ target_positions = target_portfolio.positions_map
160
+ assert target_positions[i1].weighting == mkt12 / (mkt12 + mkt21)
161
+ assert target_positions[i2].weighting == mkt21 / (mkt12 + mkt21)
@@ -56,6 +56,8 @@ class AssetPositionProductGroupEndpointConfig(AssetPositionEndpointConfig):
56
56
 
57
57
 
58
58
  class CashPositionPortfolioEndpointConfig(AssetPositionEndpointConfig):
59
+ PK_FIELD = "portfolio"
60
+
59
61
  def get_list_endpoint(self, **kwargs):
60
62
  return reverse(
61
63
  "wbportfolio:productcashposition-list",
@@ -63,6 +65,16 @@ class CashPositionPortfolioEndpointConfig(AssetPositionEndpointConfig):
63
65
  request=self.request,
64
66
  )
65
67
 
68
+ def get_instance_endpoint(self, **kwargs):
69
+ return reverse(
70
+ "wbportfolio:portfolio-list",
71
+ args=[],
72
+ request=self.request,
73
+ )
74
+
75
+ def get_update_endpoint(self, **kwargs):
76
+ return None
77
+
66
78
 
67
79
  class ContributorPortfolioChartEndpointConfig(AssetPositionEndpointConfig):
68
80
  def get_list_endpoint(self, **kwargs):
@@ -181,7 +181,7 @@ class PortfolioTreeGraphChartViewSet(UserPortfolioRequestPermissionMixin, viewse
181
181
  return parse_date(self.request.GET["date"])
182
182
 
183
183
  def get_html(self, queryset) -> str:
184
- portfolio_graph = PortfolioGraph(self.portfolio, self.val_date)
184
+ portfolio_graph = PortfolioGraph(self.portfolio, self.val_date, rankdir="LR", size="20,11")
185
185
  return portfolio_graph.to_svg()
186
186
 
187
187
 
@@ -196,7 +196,7 @@ class TopDownPortfolioCompositionPandasAPIView(UserPortfolioRequestPermissionMix
196
196
  pandas_fields = pf.PandasFields(
197
197
  fields=[
198
198
  pf.PKField(key="id", label="ID"),
199
- pf.CharField(key="_group_key", label="Group Key"),
199
+ pf.IntegerField(key="_group_key", label="Group Key"),
200
200
  pf.IntegerField(key="parent_row_id", label="Parent Row"),
201
201
  pf.CharField(key="instrument", label="Instrument"),
202
202
  pf.FloatField(key="effective_weights", label="Effective Weights", precision=2, percent=True),
@@ -229,7 +229,7 @@ class TopDownPortfolioCompositionPandasAPIView(UserPortfolioRequestPermissionMix
229
229
 
230
230
  def _get_parent_instrument_id(portfolio):
231
231
  with suppress(AttributeError):
232
- return portfolio.instruments.first().id
232
+ return str(portfolio.instruments.first().id)
233
233
 
234
234
  if (
235
235
  self.has_portfolio_access
@@ -237,85 +237,87 @@ class TopDownPortfolioCompositionPandasAPIView(UserPortfolioRequestPermissionMix
237
237
  and self.last_rebalancing_date
238
238
  and self.last_effective_date
239
239
  ):
240
- tree_positions_effective_date_df = (
241
- pd.DataFrame(
242
- map(
243
- lambda x: {
244
- "instrument": x.underlying_instrument.id,
245
- "parent_instrument": _get_parent_instrument_id(
246
- getattr(x, "parent_portfolio", "portfolio")
247
- ),
248
- "effective_weights": x.weighting,
249
- },
250
- self.composition_portfolio.get_positions(
251
- self.last_effective_date, with_intermediary_position=True
240
+ df = pd.DataFrame(
241
+ map(
242
+ lambda x: {
243
+ "instrument": str(x.underlying_instrument.id),
244
+ "path": "-".join(
245
+ map(
246
+ lambda o: _get_parent_instrument_id(o),
247
+ getattr(x, "path", [self.composition_portfolio]),
248
+ )
252
249
  ),
250
+ "effective_weights": x.weighting,
251
+ },
252
+ self.composition_portfolio.get_positions(
253
+ self.last_effective_date, with_intermediary_position=True
253
254
  ),
254
- columns=["instrument", "parent_instrument", "effective_weights"],
255
- )
256
- .groupby(["parent_instrument", "instrument"])
257
- .sum()
258
- )
259
- tree_positions_rebalancing_date_df = (
260
- pd.DataFrame(
255
+ ),
256
+ columns=["instrument", "path", "effective_weights"],
257
+ ).set_index(["path", "instrument"])
258
+
259
+ if last_rebalancing_date := self.last_rebalancing_date:
260
+ tree_positions_rebalancing_date_df = pd.DataFrame(
261
261
  map(
262
262
  lambda x: {
263
- "instrument": x.underlying_instrument.id,
264
- "parent_instrument": _get_parent_instrument_id(
265
- getattr(x, "parent_portfolio", "portfolio")
263
+ "instrument": str(x.underlying_instrument.id),
264
+ "path": "-".join(
265
+ map(
266
+ lambda o: _get_parent_instrument_id(o),
267
+ getattr(x, "path", [self.composition_portfolio]),
268
+ )
266
269
  ),
267
270
  "rebalancing_weights": x.weighting,
268
271
  },
269
272
  self.composition_portfolio.get_positions(
270
- self.last_rebalancing_date, with_intermediary_position=True
273
+ last_rebalancing_date, with_intermediary_position=True
271
274
  ),
272
275
  ),
273
- columns=["instrument", "parent_instrument", "rebalancing_weights"],
274
- )
275
- .groupby(["parent_instrument", "instrument"])
276
- .sum()
277
- )
276
+ columns=["instrument", "path", "rebalancing_weights"],
277
+ ).set_index(["path", "instrument"])
278
278
 
279
+ df = pd.concat([df, tree_positions_rebalancing_date_df], axis=1)
280
+ df = df.reset_index()
279
281
  df = pd.concat(
280
- [tree_positions_effective_date_df, tree_positions_rebalancing_date_df], axis=1
281
- ).reset_index()
282
-
283
- if not df["parent_instrument"].isnull().any():
284
- df = pd.concat(
285
- [
286
- pd.DataFrame(
287
- [
288
- {
289
- "instrument": self.portfolio.instruments.first().id,
290
- "parent_instrument": None,
291
- "effective_weights": 1.0,
292
- "rebalancing_weights": 1.0,
293
- }
294
- ]
295
- ),
296
- df,
297
- ],
298
- ignore_index=True,
299
- )
282
+ [
283
+ pd.DataFrame(
284
+ [
285
+ {
286
+ "instrument": str(self.portfolio.instruments.first().id),
287
+ "path": "",
288
+ "effective_weights": 1.0,
289
+ "rebalancing_weights": 1.0,
290
+ }
291
+ ]
292
+ ),
293
+ df,
294
+ ],
295
+ ignore_index=True,
296
+ )
300
297
 
301
298
  df = df.reset_index(names="id")
302
299
 
303
- def _get_parent_id(x):
304
- dff = df.loc[df["instrument"] == x, "id"]
300
+ def _get_group_key(x):
301
+ return str(int(x)) if not df.loc[df["parent_row_id"] == x, :].empty else None
302
+
303
+ def _get_parent_row_id(path):
304
+ s = path.split("-")
305
+ instrument = s[-1]
306
+ parent_path = "-".join(s[:-1]) if len(s) > 1 else ""
307
+ dff = df.loc[(df["instrument"] == instrument) & (df["path"] == parent_path), "id"]
305
308
  if not dff.empty:
306
309
  return dff.iloc[0]
307
310
  return None
308
311
 
309
- def _get_group_key(x):
310
- return str(int(x)) if not df.loc[df["parent_row_id"] == x, :].empty else None
311
-
312
- df["parent_row_id"] = df["parent_instrument"].apply(lambda x: _get_parent_id(x))
312
+ df["parent_row_id"] = df["path"].apply(lambda x: _get_parent_row_id(x))
313
313
  df["_group_key"] = df["id"].apply(lambda x: _get_group_key(x))
314
- df = df.drop(columns=["parent_instrument"])
314
+ df = df.drop(columns=["path"])
315
315
  return df
316
316
 
317
317
  def manipulate_dataframe(self, df):
318
- df["instrument"] = df["instrument"].map(
319
- dict(Instrument.objects.filter(id__in=df["instrument"]).values_list("id", "name_repr"))
318
+ df["instrument"] = (
319
+ df["instrument"]
320
+ .astype(int)
321
+ .map(dict(Instrument.objects.filter(id__in=df["instrument"]).values_list("id", "name_repr")))
320
322
  )
321
323
  return df
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbportfolio
3
- Version: 1.45.0
3
+ Version: 1.46.0
4
4
  Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
5
5
  License-File: LICENSE
6
6
  Requires-Dist: cryptography==3.4.*
@@ -119,7 +119,7 @@ wbportfolio/import_export/handlers/trade.py,sha256=LydAyFtqHPNfCF6j1yY9Md0xWf6NM
119
119
  wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
120
120
  wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
121
121
  wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
122
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py,sha256=UXi71aP0BGtqJpkiemWlgt0Ez3Aj5JKgjDaRPAz9vj0,2380
122
+ wbportfolio/import_export/parsers/jpmorgan/customer_trade.py,sha256=AMB7vXkhRctGBzgdCdWz0lrWVOIbmuqTjyotEJvTKYE,2859
123
123
  wbportfolio/import_export/parsers/jpmorgan/fees.py,sha256=l1eoU_QJZdE3rL2ql_s5qGUDfDqnOZKN5mrSV4qxEqs,2179
124
124
  wbportfolio/import_export/parsers/jpmorgan/strategy.py,sha256=OAIKv9NyKj8WiWLXsVIj7ujjfxBl5OLCNNXlx0oJIiE,4463
125
125
  wbportfolio/import_export/parsers/jpmorgan/valuation.py,sha256=-PGYjAjvHyW6muwPeA3HCFWtD-U7elzXThgrcOfQe58,1306
@@ -187,7 +187,7 @@ wbportfolio/import_export/parsers/vontobel/performance_fees.py,sha256=DlpP2Skih2
187
187
  wbportfolio/import_export/parsers/vontobel/trade.py,sha256=Eu5qR-cjnkA_jwtrJ78xoi9ealysjHDLXIKSandqDe0,1570
188
188
  wbportfolio/import_export/parsers/vontobel/utils.py,sha256=toleo4gRb4YFtiD8m2d7z1GGGdG-O3mpTMjOO9R_E1Q,516
189
189
  wbportfolio/import_export/parsers/vontobel/valuation.py,sha256=iav8_xYpTJchmTa7KOPmFr1gi9xxLwq3e-VcZ9MDiRk,1220
190
- wbportfolio/import_export/parsers/vontobel/valuation_api.py,sha256=4-PwFCPt9pmSBgHgBGF62uEaYTNZalc2P_5piJYjmvw,690
190
+ wbportfolio/import_export/parsers/vontobel/valuation_api.py,sha256=WLkZ5z-WqhFraNorWlOhIpSx1pQ2fnjdsLHwSTA7O2o,882
191
191
  wbportfolio/import_export/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
192
192
  wbportfolio/import_export/resources/assets.py,sha256=zjgHlQWpud41jHrKdRyqGUt1KUJQm9Z7pt0uVh4qBWQ,2234
193
193
  wbportfolio/import_export/resources/trades.py,sha256=5HC0YrE1fnc_dOMfkOEj7vsICv1JN_OEZiGryg2GboY,1125
@@ -245,7 +245,7 @@ wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQt
245
245
  wbportfolio/models/asset.py,sha256=V-fSAF1bsfj_Td3VfdKyhCgcwB_xPS6vTI90Dh5iDx8,37502
246
246
  wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
247
247
  wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
248
- wbportfolio/models/portfolio.py,sha256=ao9S7qV_EmVCnWjSQ8hU3zIeMeyDdplHexEmpNbp98k,55313
248
+ wbportfolio/models/portfolio.py,sha256=-B--vg_vGsZjHtUsW1hrkmg8LoGeyBO_ilcE6BEE3e0,55459
249
249
  wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
250
250
  wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
251
251
  wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
@@ -256,7 +256,7 @@ wbportfolio/models/registers.py,sha256=qA6T33t4gxFYnabQFBMd90WGIr6wxxirDLKDFqjOf
256
256
  wbportfolio/models/roles.py,sha256=34BwZleaPMHnUqDK1nHett45xaNNsUqSHY44844itW8,7387
257
257
  wbportfolio/models/utils.py,sha256=iBdMjRCvr6aOL0nLgfSCWUKe0h39h3IGmUbYo6l9t6w,394
258
258
  wbportfolio/models/graphs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
259
- wbportfolio/models/graphs/portfolio.py,sha256=yztomiujS_HwXAslc3WwkIA4DH3OTE8sk71kiIHOVWU,5814
259
+ wbportfolio/models/graphs/portfolio.py,sha256=NwkehWvTcyTYrKO5ku3eNNaYLuBwuLdSbTEuugGuSIU,6541
260
260
  wbportfolio/models/graphs/utils.py,sha256=1AMpEE9mDuUZ82XgN2irxjCW1-LmziROhKevEBo0mJE,2347
261
261
  wbportfolio/models/llm/wbcrm/analyze_relationship.py,sha256=_y2Myc-M2hXQDkRGXvzsM0ZNC31dmxSHHz5BKMtymww,2106
262
262
  wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -271,8 +271,8 @@ wbportfolio/models/transactions/claim.py,sha256=agdpGqxpO0FSzYDWV-Gv1tQY46k0LN9C
271
271
  wbportfolio/models/transactions/dividends.py,sha256=naL5xeDQfUBf5KyGt7y-tTcHL22nzZumT8DV6AaG8Bg,1064
272
272
  wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
273
273
  wbportfolio/models/transactions/fees.py,sha256=ffvqo8I4A0l5rLi00jJ6sGot0jmnkoxaNsbDzdPLwCg,5712
274
- wbportfolio/models/transactions/rebalancing.py,sha256=t-bQcFTfPTD-ciCNTKEtKW0r29RZ63PmeVJmBAKyj3Y,5719
275
- wbportfolio/models/transactions/trade_proposals.py,sha256=xekzZcY1i9jsguVTTMTeLRwEMJogNWxV4OvJTF3z7uI,20275
274
+ wbportfolio/models/transactions/rebalancing.py,sha256=9EdElkaqPoH14FBSK9QOCLFzVpr4YFa3YrgCJV9XvaI,6396
275
+ wbportfolio/models/transactions/trade_proposals.py,sha256=ZpMWOKXfWo85AW2cqp0X5aUGabGECB4W1bK4RaD5-QM,20311
276
276
  wbportfolio/models/transactions/trades.py,sha256=3HthfL0wgzPmwwXRVzA5yvm-UPiQco7HhN30ztsOjvI,27529
277
277
  wbportfolio/models/transactions/transactions.py,sha256=4THsE4xqdigZAwWKYfTNRLPJlkmAmsgE70Ribp9Lnrk,7127
278
278
  wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -283,12 +283,13 @@ wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
283
283
  wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
284
284
  wbportfolio/pms/trading/handler.py,sha256=Xpgo719S0jE1wUTTyGFpYccPEIg9GXghWEAdYawJbrk,7165
285
285
  wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
286
- wbportfolio/rebalancing/base.py,sha256=HttfNmOjclSw9Kfm283LmyHajjBJi4CTjv25EP9_JeQ,481
286
+ wbportfolio/rebalancing/base.py,sha256=Z5IAG8zu6_RLkQE1AgIoROBUDXWza0myzKhKx_4ONwA,481
287
287
  wbportfolio/rebalancing/decorators.py,sha256=JhQ2vkcIuGBTGvNmpkQerdw2-vLq-RAb0KAPjOrETkw,515
288
- wbportfolio/rebalancing/models/__init__.py,sha256=AQHlyjyIdlBRSim6eCdWDf4-M-supY9VP-xkuyBWYnA,156
289
- wbportfolio/rebalancing/models/composite.py,sha256=H6AgpAAegW2H0pF2xbKVuAubUwbWsQU_L_4lLJBowiI,1167
290
- wbportfolio/rebalancing/models/equally_weighted.py,sha256=L_XFiVnik3HQjwA0F05FjASlQEXv_X5L8g_Rq0TvVaM,873
291
- wbportfolio/rebalancing/models/model_portfolio.py,sha256=bGkThc3t80XIvAEp9prstZHsdlt9tGUsLEenzH9XmKc,1243
288
+ wbportfolio/rebalancing/models/__init__.py,sha256=AQjG7Tu5vlmhqncVoYOjpBKU2UIvgo9FuP2_jD2w-UI,232
289
+ wbportfolio/rebalancing/models/composite.py,sha256=XAjJqLRNsV-MuBKrat3THEfAWs6PXQNSO0g8k8MtBXo,1157
290
+ wbportfolio/rebalancing/models/equally_weighted.py,sha256=U29MOHJMQMIg7Y7W_8t5K3nXjaznzt4ArIxQSiv0Xok,863
291
+ wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=AYVoLg_bf7s7DSXVjBuIOltFpkySVmv2lrDD-pKBH9I,3643
292
+ wbportfolio/rebalancing/models/model_portfolio.py,sha256=XQdvs03-0M9YUnL4DidwZC4E6k-ANCNcZ--T_aaOXTQ,1233
292
293
  wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
293
294
  wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
294
295
  wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -303,7 +304,7 @@ wbportfolio/risk_management/backends/mixins.py,sha256=anuOlop9gc9X8tywx7_wbH7fAJ
303
304
  wbportfolio/risk_management/backends/product_integrity.py,sha256=3BbQBYYzLdj2iuQb9QOIgPjWMz1FaPkvEJoNgxWVfz0,4598
304
305
  wbportfolio/risk_management/backends/stop_loss_instrument.py,sha256=qIzeV26TRdbgG8-Nsh5O0NPMrPmIXeaRdPR-EF_uaLo,1128
305
306
  wbportfolio/risk_management/backends/stop_loss_portfolio.py,sha256=yiDa3FkZKdrmHSf8p1XmT8RhJW6KyiMuypYpY39SiO8,1674
306
- wbportfolio/risk_management/backends/ucits_portfolio.py,sha256=tBj3HWHVu4fEwLQuVhQsO4tnpOQieSpwnjPMGzWgVNU,3366
307
+ wbportfolio/risk_management/backends/ucits_portfolio.py,sha256=WAw2emAcwDBtYuYoieW0KqGST_CJ27UQuKY0B6jav_Q,3477
307
308
  wbportfolio/risk_management/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
308
309
  wbportfolio/risk_management/tests/conftest.py,sha256=ChVocx8SDmh13ixkRmnKe_MMudQAX0495o7nG4NDlsg,440
309
310
  wbportfolio/risk_management/tests/test_accounts.py,sha256=q9Z3hDw0Yw86VealdmUfiJBkzRcqDwfybteGxS0hAJA,3990
@@ -314,7 +315,7 @@ wbportfolio/risk_management/tests/test_liquidity_risk.py,sha256=UtzBrW7LeY0Z0ARX
314
315
  wbportfolio/risk_management/tests/test_product_integrity.py,sha256=wqJ4b5DO2BrEVQF4eVX2B9AvfrRMAyC_lppGrgKc_ug,1959
315
316
  wbportfolio/risk_management/tests/test_stop_loss_instrument.py,sha256=ZueuA9qQOv6JdKuRiPLGV3auOLRnoUCZzfqcQzGJGig,4197
316
317
  wbportfolio/risk_management/tests/test_stop_loss_portfolio.py,sha256=bFQqIUs1FZVn1SwiNkzt7hVrvT-qs3EcxOfI20xAjOM,4595
317
- wbportfolio/risk_management/tests/test_ucits_portfolio.py,sha256=7ZYVfIN0vdAcVUmu8RJtjKgzPHIwjHkPewzRrULybVY,1807
318
+ wbportfolio/risk_management/tests/test_ucits_portfolio.py,sha256=UcZLhatl8iU9AhNNYv6OBs_cjILZKaws2nvUen6orkc,1807
318
319
  wbportfolio/serializers/__init__.py,sha256=w1nPxkapcnGngYwiU7BGSLfyhJ67_PQWVjuya7YSxX0,1795
319
320
  wbportfolio/serializers/adjustments.py,sha256=0yTPgDjGRyzSa-ecCklENKtLJXZhq1dryMI3nTRsQRo,783
320
321
  wbportfolio/serializers/assets.py,sha256=EHTPwicRbdklLHvKS0_Y7pNtw7YC5iW_AuecPoTfJnc,6477
@@ -369,7 +370,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
369
370
  wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
370
371
  wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
371
372
  wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
372
- wbportfolio/tests/models/test_portfolios.py,sha256=tfQCTmJBZvhzd1iC5B7NW8vi3IySrioWG6ZAHFk4_oA,50675
373
+ wbportfolio/tests/models/test_portfolios.py,sha256=YF1SRS-REJFQhbtxs1LfIUs91jmHpI7pXBE70pW4W7E,51735
373
374
  wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
374
375
  wbportfolio/tests/models/test_products.py,sha256=5YYmQreFnaKLbWmrSib103wgLalqn8u01Fnh3A0XMz8,8217
375
376
  wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
@@ -383,7 +384,7 @@ wbportfolio/tests/models/transactions/test_trades.py,sha256=z0CCZjB648ECDSEdwmzq
383
384
  wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
384
385
  wbportfolio/tests/pms/test_analytics.py,sha256=FrvVsV_uUiTgmRUfsaB-_sGzY30CqknbOY2DvmwR_70,1141
385
386
  wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
386
- wbportfolio/tests/rebalancing/test_models.py,sha256=I9yHsDge2MgULcSpdp2YtlqvUSsC1tLAgKUCUH2cVkc,5916
387
+ wbportfolio/tests/rebalancing/test_models.py,sha256=Zph5NFWeIJVFxv06F6CBrJC0xPr3TB5ns0EdgzKdh8U,7610
387
388
  wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
388
389
  wbportfolio/tests/serializers/test_claims.py,sha256=vQrg73xQXRFEgvx3KI9ivFre_wpBFzdO0p0J13PkvdY,582
389
390
  wbportfolio/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -403,7 +404,7 @@ wbportfolio/viewsets/portfolio_cash_flow.py,sha256=jkBfdZRQ3KsxGMJpltRjmdrZ2qEFJ
403
404
  wbportfolio/viewsets/portfolio_cash_targets.py,sha256=CvHlrDE8qnnnfRpTYnFu-Uu15MDbF5d5gTmEKth2S24,322
404
405
  wbportfolio/viewsets/portfolio_relationship.py,sha256=RGyvxd8NfFEs8YdqEvVD3VbrISvAO5UtCTlocSIuWQw,2109
405
406
  wbportfolio/viewsets/portfolio_swing_pricing.py,sha256=-57l3WLQZRslIV67OT0ucHE5JXTtTtLvd3t7MppdVn8,357
406
- wbportfolio/viewsets/portfolios.py,sha256=PZ9ZLZavQuFv6sMQzB1rCq0Ycj5Cc2Q0ddXDD-I9xRo,13014
407
+ wbportfolio/viewsets/portfolios.py,sha256=ZQjlBUZYAM576V0kE8nfRdyCkOPhvt54yFz9drUZilE,13128
407
408
  wbportfolio/viewsets/positions.py,sha256=MDf_0x9La2qE6qjaIqBtfV5VC0RfJ1chZIim45Emk10,13198
408
409
  wbportfolio/viewsets/product_groups.py,sha256=YvmuXPPy98K1J_rz6YPsx9gNK-tCS2P-wc1uRYgfyo0,2399
409
410
  wbportfolio/viewsets/product_performance.py,sha256=dRfRgifjGS1RgZSu9uJRM0SmB7eLnNUkPuqARMO4gyo,28371
@@ -452,7 +453,7 @@ wbportfolio/viewsets/configs/display/trades.py,sha256=e61wLzTEDwewgctpztDa4Exvie
452
453
  wbportfolio/viewsets/configs/display/transactions.py,sha256=DOM3eV1DxBwX6Iiw3C2sJamWh6A_3ZSYC9447Jc3Wmo,2586
453
454
  wbportfolio/viewsets/configs/endpoints/__init__.py,sha256=E13AYY3CIW4CZtmqwBVMPDYA5zNyKJeRZtiXKtad68Y,2871
454
455
  wbportfolio/viewsets/configs/endpoints/adjustments.py,sha256=9CcnfNuFcxsZ8YvfUitSeyCvpLxY-jU-gIw3GG0mIp4,697
455
- wbportfolio/viewsets/configs/endpoints/assets.py,sha256=VfM9IwyYDLVcN-Hemj5IHx_5ylzhPzksqSEmOM94ys4,3618
456
+ wbportfolio/viewsets/configs/endpoints/assets.py,sha256=fe_D-x3z4xzzAJ7ktvvvwGuoX51l8iTPdDNugagxHYc,3891
456
457
  wbportfolio/viewsets/configs/endpoints/claim.py,sha256=H7oUbatjyq6HiSrMzgSMV8KqzH_PNQ5XcwHuL1yX1aA,3650
457
458
  wbportfolio/viewsets/configs/endpoints/custodians.py,sha256=Ve7TxfvFnmvm9x0cx6taO502EVWJb23fdVT9yX3ME5o,194
458
459
  wbportfolio/viewsets/configs/endpoints/esg.py,sha256=9wGxw1fW8UQJtGd-cT9XWl5gfKN68SSft3wqmkUK4MQ,467
@@ -515,7 +516,7 @@ wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQV
515
516
  wbportfolio/viewsets/transactions/trade_proposals.py,sha256=fYTvvRk7k5xsBzbIgJvU4I4OrllF0VkhlrekD4GVgDk,4296
516
517
  wbportfolio/viewsets/transactions/trades.py,sha256=6iTIM5g7TUlRtiCjIG4EdYvyfaoB6K3UC2WRX9BD9Jg,15850
517
518
  wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
518
- wbportfolio-1.45.0.dist-info/METADATA,sha256=KiCMJmtZLiBOiNHnBttN66pzpw2q1_LZU31dGkbhH_0,734
519
- wbportfolio-1.45.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
520
- wbportfolio-1.45.0.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
521
- wbportfolio-1.45.0.dist-info/RECORD,,
519
+ wbportfolio-1.46.0.dist-info/METADATA,sha256=bH78CovhJCtF2hEmjiCmmyo_CstuGHyjdNkqpRSaomk,734
520
+ wbportfolio-1.46.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
521
+ wbportfolio-1.46.0.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
522
+ wbportfolio-1.46.0.dist-info/RECORD,,