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.
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +20 -12
- wbportfolio/import_export/parsers/vontobel/valuation_api.py +11 -8
- wbportfolio/models/graphs/portfolio.py +43 -26
- wbportfolio/models/portfolio.py +7 -2
- wbportfolio/models/transactions/rebalancing.py +25 -10
- wbportfolio/models/transactions/trade_proposals.py +1 -0
- wbportfolio/rebalancing/base.py +2 -2
- wbportfolio/rebalancing/models/__init__.py +1 -0
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/equally_weighted.py +1 -1
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +85 -0
- wbportfolio/rebalancing/models/model_portfolio.py +1 -1
- wbportfolio/risk_management/backends/ucits_portfolio.py +11 -5
- wbportfolio/risk_management/tests/test_ucits_portfolio.py +1 -1
- wbportfolio/tests/models/test_portfolios.py +25 -0
- wbportfolio/tests/rebalancing/test_models.py +34 -0
- wbportfolio/viewsets/configs/endpoints/assets.py +12 -0
- wbportfolio/viewsets/portfolios.py +62 -60
- {wbportfolio-1.45.0.dist-info → wbportfolio-1.46.0.dist-info}/METADATA +1 -1
- {wbportfolio-1.45.0.dist-info → wbportfolio-1.46.0.dist-info}/RECORD +22 -21
- {wbportfolio-1.45.0.dist-info → wbportfolio-1.46.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.45.0.dist-info → wbportfolio-1.46.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
53
|
-
orientation="45",
|
|
54
|
-
style="solid",
|
|
57
|
+
**self._get_node_kwargs(child_portfolio),
|
|
55
58
|
)
|
|
56
59
|
)
|
|
57
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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")
|
wbportfolio/rebalancing/base.py
CHANGED
|
@@ -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
|
|
15
|
+
def get_target_portfolio(self) -> PortfolioDTO:
|
|
16
16
|
raise NotImplementedError()
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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']
|
|
61
|
-
f"Sum of positions
|
|
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.
|
|
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.
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
"
|
|
265
|
-
|
|
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
|
-
|
|
273
|
+
last_rebalancing_date, with_intermediary_position=True
|
|
271
274
|
),
|
|
272
275
|
),
|
|
273
|
-
columns=["instrument", "
|
|
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
|
-
[
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
304
|
-
|
|
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
|
-
|
|
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=["
|
|
314
|
+
df = df.drop(columns=["path"])
|
|
315
315
|
return df
|
|
316
316
|
|
|
317
317
|
def manipulate_dataframe(self, df):
|
|
318
|
-
df["instrument"] =
|
|
319
|
-
|
|
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
|
|
@@ -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=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
275
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
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=
|
|
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=
|
|
289
|
-
wbportfolio/rebalancing/models/composite.py,sha256=
|
|
290
|
-
wbportfolio/rebalancing/models/equally_weighted.py,sha256=
|
|
291
|
-
wbportfolio/rebalancing/models/
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
519
|
-
wbportfolio-1.
|
|
520
|
-
wbportfolio-1.
|
|
521
|
-
wbportfolio-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|