wbportfolio 1.45.1__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/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/tests/rebalancing/test_models.py +34 -0
- wbportfolio/viewsets/portfolios.py +61 -59
- {wbportfolio-1.45.1.dist-info → wbportfolio-1.46.0.dist-info}/METADATA +1 -1
- {wbportfolio-1.45.1.dist-info → wbportfolio-1.46.0.dist-info}/RECORD +16 -15
- {wbportfolio-1.45.1.dist-info → wbportfolio-1.46.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.45.1.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"])
|
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
|
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|