wbportfolio 1.49.6__py2.py3-none-any.whl → 1.49.8__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/filters/__init__.py +1 -0
- wbportfolio/filters/assets.py +4 -0
- wbportfolio/filters/positions.py +19 -12
- wbportfolio/models/asset.py +23 -2
- wbportfolio/models/portfolio.py +44 -68
- wbportfolio/models/transactions/rebalancing.py +9 -5
- wbportfolio/models/transactions/trade_proposals.py +53 -19
- wbportfolio/tests/models/transactions/test_trade_proposals.py +25 -8
- wbportfolio/viewsets/assets.py +40 -7
- wbportfolio/viewsets/positions.py +7 -8
- wbportfolio/viewsets/transactions/trades.py +9 -7
- {wbportfolio-1.49.6.dist-info → wbportfolio-1.49.8.dist-info}/METADATA +1 -1
- {wbportfolio-1.49.6.dist-info → wbportfolio-1.49.8.dist-info}/RECORD +15 -15
- {wbportfolio-1.49.6.dist-info → wbportfolio-1.49.8.dist-info}/WHEEL +0 -0
- {wbportfolio-1.49.6.dist-info → wbportfolio-1.49.8.dist-info}/licenses/LICENSE +0 -0
wbportfolio/filters/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .assets import ( # ProductAllocationEquityFilter, AssetPositionAPIFilter,
|
|
|
9
9
|
CashPositionPortfolioFilterSet,
|
|
10
10
|
CompositionModelPortfolioPandasFilter,
|
|
11
11
|
ContributionChartFilter,
|
|
12
|
+
CompositionContributionChartFilter,
|
|
12
13
|
DistributionFilter,
|
|
13
14
|
)
|
|
14
15
|
from .custodians import CustodianFilterSet
|
wbportfolio/filters/assets.py
CHANGED
|
@@ -462,6 +462,10 @@ class ContributionChartFilter(wb_filters.FilterSet):
|
|
|
462
462
|
fields = {}
|
|
463
463
|
|
|
464
464
|
|
|
465
|
+
class CompositionContributionChartFilter(ContributionChartFilter):
|
|
466
|
+
show_lookthrough = wb_filters.BooleanFilter(label="Show Lookthrough", default=False, method="fake_filter")
|
|
467
|
+
|
|
468
|
+
|
|
465
469
|
class AssetPositionUnderlyingInstrumentChartFilter(DateFilterMixin, wb_filters.FilterSet):
|
|
466
470
|
portfolio = wb_filters.ModelMultipleChoiceFilter(
|
|
467
471
|
label="Portfolios",
|
wbportfolio/filters/positions.py
CHANGED
|
@@ -3,15 +3,17 @@ import datetime as dt
|
|
|
3
3
|
from django.db.models import TextChoices
|
|
4
4
|
from pandas.tseries.offsets import BDay
|
|
5
5
|
from wbcore import filters as wb_filters
|
|
6
|
-
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
6
|
+
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
7
|
+
from wbcore.contrib.geography.models import Geography
|
|
7
8
|
from wbcore.pandas.filterset import PandasFilterSetMixin
|
|
8
9
|
from wbcore.utils.date import current_financial_month
|
|
10
|
+
from wbfdm.models import Classification, Instrument
|
|
9
11
|
|
|
10
12
|
from wbportfolio.filters.assets import (
|
|
11
13
|
DateFilterMixin,
|
|
12
14
|
get_latest_end_quarter_date_asset_position,
|
|
13
15
|
)
|
|
14
|
-
from wbportfolio.models import AssetPosition
|
|
16
|
+
from wbportfolio.models import AssetPosition, Portfolio
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class GroupbyChoice(TextChoices):
|
|
@@ -33,16 +35,21 @@ class GroupbyChoice(TextChoices):
|
|
|
33
35
|
"PREFERRED_CLASSIFICATION": "classification_id",
|
|
34
36
|
}[name]
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
def get_repr(self, ids: list[int]) -> dict[int, str]:
|
|
39
|
+
match self:
|
|
40
|
+
case GroupbyChoice.UNDERLYING_INSTRUMENT:
|
|
41
|
+
return dict(Instrument.objects.filter(id__in=ids).values_list("id", "name_repr"))
|
|
42
|
+
case GroupbyChoice.CURRENCY:
|
|
43
|
+
return dict(Currency.objects.filter(id__in=ids).values_list("id", "key"))
|
|
44
|
+
case GroupbyChoice.COUNTRY:
|
|
45
|
+
return dict(Geography.countries.filter(id__in=ids).values_list("id", "name"))
|
|
46
|
+
case GroupbyChoice.PORTFOLIO:
|
|
47
|
+
return dict(Portfolio.objects.filter(id__in=ids).values_list("id", "name"))
|
|
48
|
+
case GroupbyChoice.PRIMARY_CLASSIFICATION:
|
|
49
|
+
return dict(Classification.objects.filter(id__in=ids).values_list("id", "name"))
|
|
50
|
+
case GroupbyChoice.PREFERRED_CLASSIFICATION:
|
|
51
|
+
return dict(Classification.objects.filter(id__in=ids).values_list("id", "name"))
|
|
52
|
+
raise ValueError("invalid self value")
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
def get_latest_date_based_on_multiple_models(field, request, view):
|
wbportfolio/models/asset.py
CHANGED
|
@@ -24,7 +24,7 @@ from django.db.models.functions import Coalesce
|
|
|
24
24
|
from django.db.models.signals import post_save
|
|
25
25
|
from django.dispatch import receiver
|
|
26
26
|
from django.utils.functional import cached_property
|
|
27
|
-
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
27
|
+
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
28
28
|
from wbcore.contrib.io.mixins import ImportMixin
|
|
29
29
|
from wbcore.signals import pre_merge
|
|
30
30
|
from wbcore.utils.enum import ChoiceEnum
|
|
@@ -77,6 +77,24 @@ class AssetPositionDefaultQueryset(QuerySet):
|
|
|
77
77
|
classification_title=Subquery(base_qs.values(ref_title)[:1]),
|
|
78
78
|
)
|
|
79
79
|
|
|
80
|
+
def annotate_hedged_currency_fx_rate(self, hedged_currency: Currency | None) -> QuerySet:
|
|
81
|
+
return self.annotate(
|
|
82
|
+
_is_hedged=Case(
|
|
83
|
+
When(
|
|
84
|
+
underlying_instrument__currency__isnull=False,
|
|
85
|
+
underlying_instrument__currency=hedged_currency,
|
|
86
|
+
then=Value(True),
|
|
87
|
+
),
|
|
88
|
+
default=Value(False),
|
|
89
|
+
output_field=models.BooleanField(),
|
|
90
|
+
),
|
|
91
|
+
hedged_currency_fx_rate=Case(
|
|
92
|
+
When(_is_hedged=True, then=Value(Decimal(1.0))),
|
|
93
|
+
default=F("currency_fx_rate"),
|
|
94
|
+
output_field=models.BooleanField(),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
80
98
|
|
|
81
99
|
class DefaultAssetPositionManager(models.Manager):
|
|
82
100
|
def annotate_classification_for_group(
|
|
@@ -93,7 +111,10 @@ class DefaultAssetPositionManager(models.Manager):
|
|
|
93
111
|
classification_group, classification_height=classification_height
|
|
94
112
|
)
|
|
95
113
|
|
|
96
|
-
def
|
|
114
|
+
def annotate_hedged_currency_fx_rate(self, hedged_currency: Currency | None) -> QuerySet:
|
|
115
|
+
return self.get_queryset().annotate_hedged_currency_fx_rate(hedged_currency)
|
|
116
|
+
|
|
117
|
+
def get_queryset(self) -> AssetPositionDefaultQueryset:
|
|
97
118
|
return AssetPositionDefaultQueryset(self.model).annotate(
|
|
98
119
|
adjusting_factor=Coalesce(
|
|
99
120
|
F("applied_adjustment__cumulative_factor") * F("applied_adjustment__factor"), Decimal(1.0)
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -13,16 +13,12 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
13
13
|
from django.contrib.postgres.fields import DateRangeField
|
|
14
14
|
from django.db import models
|
|
15
15
|
from django.db.models import (
|
|
16
|
-
BooleanField,
|
|
17
|
-
Case,
|
|
18
16
|
Exists,
|
|
19
17
|
F,
|
|
20
18
|
OuterRef,
|
|
21
19
|
Q,
|
|
22
20
|
QuerySet,
|
|
23
21
|
Sum,
|
|
24
|
-
Value,
|
|
25
|
-
When,
|
|
26
22
|
)
|
|
27
23
|
from django.db.models.signals import post_save
|
|
28
24
|
from django.dispatch import receiver
|
|
@@ -629,11 +625,28 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
629
625
|
)
|
|
630
626
|
return df
|
|
631
627
|
|
|
632
|
-
def get_portfolio_contribution_df(
|
|
628
|
+
def get_portfolio_contribution_df(
|
|
629
|
+
self,
|
|
630
|
+
start: date,
|
|
631
|
+
end: date,
|
|
632
|
+
with_cash: bool = True,
|
|
633
|
+
hedged_currency: Currency | None = None,
|
|
634
|
+
only_equity: bool = False,
|
|
635
|
+
) -> pd.DataFrame:
|
|
633
636
|
qs = self._get_assets(with_cash=with_cash).filter(date__gte=start, date__lte=end)
|
|
634
637
|
if only_equity:
|
|
635
638
|
qs = qs.filter(underlying_instrument__instrument_type=InstrumentType.EQUITY)
|
|
636
|
-
|
|
639
|
+
qs = qs.annotate_hedged_currency_fx_rate(hedged_currency)
|
|
640
|
+
df = Portfolio.get_contribution_df(
|
|
641
|
+
qs.select_related("underlying_instrument").values_list(
|
|
642
|
+
"date", "price", "hedged_currency_fx_rate", "underlying_instrument", "weighting"
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
df = df.rename(columns={"group_key": "underlying_instrument"})
|
|
646
|
+
df["underlying_instrument__name_repr"] = df["underlying_instrument"].map(
|
|
647
|
+
dict(Instrument.objects.filter(id__in=df["underlying_instrument"]).values_list("id", "name_repr"))
|
|
648
|
+
)
|
|
649
|
+
return df
|
|
637
650
|
|
|
638
651
|
def check_related_portfolio_at_date(self, val_date: date, related_portfolio: "Portfolio"):
|
|
639
652
|
assets = AssetPosition.objects.filter(
|
|
@@ -730,6 +743,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
730
743
|
):
|
|
731
744
|
rel.portfolio.evaluate_rebalancing(val_date)
|
|
732
745
|
for dependent_portfolio in self.get_child_portfolios(val_date):
|
|
746
|
+
dependent_portfolio.change_at_date(val_date)
|
|
733
747
|
dependent_portfolio.handle_controlling_portfolio_change_at_date(val_date)
|
|
734
748
|
|
|
735
749
|
def evaluate_rebalancing(self, val_date: date):
|
|
@@ -1088,104 +1102,68 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1088
1102
|
return returns.replace([np.inf, -np.inf, np.nan], 0), prices_df.replace([np.inf, -np.inf, np.nan], None)
|
|
1089
1103
|
|
|
1090
1104
|
@classmethod
|
|
1091
|
-
def get_contribution_df(
|
|
1092
|
-
cls,
|
|
1093
|
-
qs,
|
|
1094
|
-
need_normalize=False,
|
|
1095
|
-
groupby_label_id="underlying_instrument",
|
|
1096
|
-
groubpy_label_title="underlying_instrument__name_repr",
|
|
1097
|
-
currency_fx_rate_label="currency_fx_rate",
|
|
1098
|
-
hedged_currency=None,
|
|
1099
|
-
):
|
|
1100
|
-
# qs = AssetPosition.annotate_underlying_instrument(qs)
|
|
1101
|
-
weight_label = "weighting" if not need_normalize else "total_value_fx_portfolio"
|
|
1102
|
-
qs = qs.annotate(
|
|
1103
|
-
is_hedged=Case(
|
|
1104
|
-
When(
|
|
1105
|
-
underlying_instrument__currency__isnull=False,
|
|
1106
|
-
underlying_instrument__currency=hedged_currency,
|
|
1107
|
-
then=Value(True),
|
|
1108
|
-
),
|
|
1109
|
-
default=Value(False),
|
|
1110
|
-
output_field=BooleanField(),
|
|
1111
|
-
),
|
|
1112
|
-
coalesce_currency_fx_rate=Case(
|
|
1113
|
-
When(is_hedged=True, then=Value(Decimal(1.0))),
|
|
1114
|
-
default=F(currency_fx_rate_label),
|
|
1115
|
-
output_field=models.BooleanField(),
|
|
1116
|
-
),
|
|
1117
|
-
).select_related("underlying_instrument")
|
|
1105
|
+
def get_contribution_df(cls, data, need_normalize: bool = False):
|
|
1118
1106
|
df = pd.DataFrame(
|
|
1119
|
-
|
|
1120
|
-
"date",
|
|
1121
|
-
"price",
|
|
1122
|
-
"coalesce_currency_fx_rate",
|
|
1123
|
-
groupby_label_id,
|
|
1124
|
-
groubpy_label_title,
|
|
1125
|
-
weight_label,
|
|
1126
|
-
),
|
|
1107
|
+
data,
|
|
1127
1108
|
columns=[
|
|
1128
1109
|
"date",
|
|
1129
1110
|
"price",
|
|
1130
|
-
"
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
weight_label,
|
|
1111
|
+
"currency_fx_rate",
|
|
1112
|
+
"group_key",
|
|
1113
|
+
"value",
|
|
1134
1114
|
],
|
|
1135
1115
|
)
|
|
1136
1116
|
if not df.empty:
|
|
1137
|
-
df = df[df[
|
|
1117
|
+
df = df[df["value"] != 0]
|
|
1138
1118
|
df.date = pd.to_datetime(df.date)
|
|
1139
|
-
df["price_fx_portfolio"] = df.price * df.
|
|
1119
|
+
df["price_fx_portfolio"] = df.price * df.currency_fx_rate
|
|
1140
1120
|
|
|
1141
|
-
df[["price", "price_fx_portfolio",
|
|
1142
|
-
["price", "price_fx_portfolio",
|
|
1121
|
+
df[["price", "price_fx_portfolio", "value", "currency_fx_rate"]] = df[
|
|
1122
|
+
["price", "price_fx_portfolio", "value", "currency_fx_rate"]
|
|
1143
1123
|
].astype("float")
|
|
1144
1124
|
|
|
1145
|
-
df[
|
|
1146
|
-
df[groubpy_label_title] = df[groubpy_label_title].fillna("N/A")
|
|
1147
|
-
df_static = df[[groupby_label_id, groubpy_label_title]].groupby(groupby_label_id, dropna=False).first()
|
|
1125
|
+
df["group_key"] = df["group_key"].fillna(0)
|
|
1148
1126
|
|
|
1149
1127
|
df = (
|
|
1150
1128
|
df[
|
|
1151
1129
|
[
|
|
1152
|
-
|
|
1130
|
+
"group_key",
|
|
1153
1131
|
"date",
|
|
1154
1132
|
"price",
|
|
1155
1133
|
"price_fx_portfolio",
|
|
1156
|
-
|
|
1157
|
-
"
|
|
1134
|
+
"value",
|
|
1135
|
+
"currency_fx_rate",
|
|
1158
1136
|
]
|
|
1159
1137
|
]
|
|
1160
|
-
.groupby(["date",
|
|
1138
|
+
.groupby(["date", "group_key"], dropna=False)
|
|
1161
1139
|
.agg(
|
|
1162
1140
|
{
|
|
1163
1141
|
"price": "mean",
|
|
1164
1142
|
"price_fx_portfolio": "mean",
|
|
1165
|
-
|
|
1166
|
-
"
|
|
1143
|
+
"value": "sum",
|
|
1144
|
+
"currency_fx_rate": "mean",
|
|
1167
1145
|
}
|
|
1168
1146
|
)
|
|
1169
1147
|
.reset_index()
|
|
1170
1148
|
.set_index("date")
|
|
1171
1149
|
.sort_index()
|
|
1172
1150
|
)
|
|
1173
|
-
df[
|
|
1151
|
+
df["value"] = df["value"].fillna(0)
|
|
1174
1152
|
value = df.pivot_table(
|
|
1175
1153
|
index="date",
|
|
1176
|
-
columns=[
|
|
1177
|
-
values=
|
|
1154
|
+
columns=["group_key"],
|
|
1155
|
+
values="value",
|
|
1178
1156
|
fill_value=0,
|
|
1179
1157
|
aggfunc="sum",
|
|
1180
1158
|
)
|
|
1181
1159
|
weights_ = value
|
|
1182
1160
|
if need_normalize:
|
|
1183
|
-
total_value_price = df[
|
|
1161
|
+
total_value_price = df["value"].groupby("date", dropna=False).sum()
|
|
1184
1162
|
weights_ = value.divide(total_value_price, axis=0)
|
|
1185
1163
|
prices_usd = (
|
|
1186
1164
|
df.pivot_table(
|
|
1187
1165
|
index="date",
|
|
1188
|
-
columns=[
|
|
1166
|
+
columns=["group_key"],
|
|
1189
1167
|
values="price_fx_portfolio",
|
|
1190
1168
|
aggfunc="mean",
|
|
1191
1169
|
)
|
|
@@ -1196,8 +1174,8 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1196
1174
|
rates_fx = (
|
|
1197
1175
|
df.pivot_table(
|
|
1198
1176
|
index="date",
|
|
1199
|
-
columns=[
|
|
1200
|
-
values="
|
|
1177
|
+
columns=["group_key"],
|
|
1178
|
+
values="currency_fx_rate",
|
|
1201
1179
|
aggfunc="mean",
|
|
1202
1180
|
)
|
|
1203
1181
|
.replace(0, np.nan)
|
|
@@ -1220,7 +1198,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1220
1198
|
|
|
1221
1199
|
res = pd.concat(
|
|
1222
1200
|
[
|
|
1223
|
-
df_static,
|
|
1224
1201
|
monthly_perf_prices,
|
|
1225
1202
|
monthly_perf_rates_fx,
|
|
1226
1203
|
contributions_prices,
|
|
@@ -1233,8 +1210,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1233
1210
|
axis=1,
|
|
1234
1211
|
).reset_index()
|
|
1235
1212
|
res.columns = [
|
|
1236
|
-
|
|
1237
|
-
groubpy_label_title,
|
|
1213
|
+
"group_key",
|
|
1238
1214
|
"performance_total",
|
|
1239
1215
|
"performance_forex",
|
|
1240
1216
|
"contribution_total",
|
|
@@ -95,12 +95,15 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
95
95
|
while TradeProposal.objects.filter(
|
|
96
96
|
portfolio=self.portfolio, status=TradeProposal.Status.FAILED, trade_date=pivot_date
|
|
97
97
|
).exists():
|
|
98
|
-
pivot_date
|
|
98
|
+
pivot_date = (pivot_date + BDay(1)).date()
|
|
99
99
|
return pivot_date
|
|
100
100
|
|
|
101
101
|
def is_valid(self, trade_date: date) -> bool:
|
|
102
102
|
if TradeProposal.objects.filter(
|
|
103
|
-
portfolio=self.portfolio,
|
|
103
|
+
portfolio=self.portfolio,
|
|
104
|
+
status=TradeProposal.Status.APPROVED,
|
|
105
|
+
trade_date=trade_date,
|
|
106
|
+
rebalancing_model__isnull=True,
|
|
104
107
|
).exists(): # if a already approved trade proposal exists, we do not allow a re-evaluatioon of the rebalancing (only possible if "replayed")
|
|
105
108
|
return False
|
|
106
109
|
for initial_valid_datetime in self.get_rrule(trade_date):
|
|
@@ -121,12 +124,13 @@ class Rebalancer(ComplexToStringMixin, models.Model):
|
|
|
121
124
|
"rebalancing_model": self.rebalancing_model,
|
|
122
125
|
},
|
|
123
126
|
)
|
|
124
|
-
|
|
125
|
-
f"Getting target portfolio ({self.portfolio}) for rebalancing model {self.rebalancing_model} for trade date {trade_date:%Y-%m-%d}"
|
|
126
|
-
)
|
|
127
|
+
|
|
127
128
|
if trade_proposal.rebalancing_model == self.rebalancing_model:
|
|
128
129
|
trade_proposal.status = TradeProposal.Status.DRAFT
|
|
129
130
|
try:
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Getting target portfolio ({self.portfolio}) for rebalancing model {self.rebalancing_model} for trade date {trade_date:%Y-%m-%d}"
|
|
133
|
+
)
|
|
130
134
|
target_portfolio = self.rebalancing_model.get_target_portfolio(
|
|
131
135
|
self.portfolio, trade_date, trade_proposal.last_effective_date, **self.parameters
|
|
132
136
|
)
|
|
@@ -18,6 +18,7 @@ from wbcore.enums import RequestType
|
|
|
18
18
|
from wbcore.metadata.configs.buttons import ActionButton
|
|
19
19
|
from wbcore.models import WBModel
|
|
20
20
|
from wbcore.utils.models import CloneMixin
|
|
21
|
+
from wbfdm.models import InstrumentPrice
|
|
21
22
|
from wbfdm.models.instruments.instruments import Cash, Instrument
|
|
22
23
|
|
|
23
24
|
from wbportfolio.models.roles import PortfolioRole
|
|
@@ -254,6 +255,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
254
255
|
# we cannot do a bulk-create because Trade is a multi table inheritance
|
|
255
256
|
try:
|
|
256
257
|
trade = self.trades.get(underlying_instrument=instrument)
|
|
258
|
+
trade.weighting = trade_dto.delta_weight
|
|
259
|
+
trade.currency_fx_rate = currency_fx_rate
|
|
260
|
+
trade.status = Trade.Status.DRAFT
|
|
257
261
|
except Trade.DoesNotExist:
|
|
258
262
|
trade = Trade(
|
|
259
263
|
underlying_instrument=instrument,
|
|
@@ -339,7 +343,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
339
343
|
except Exception:
|
|
340
344
|
raise ValueError("We couldn't estimate the number of shares")
|
|
341
345
|
|
|
342
|
-
def get_estimated_target_cash(self, currency: Currency) ->
|
|
346
|
+
def get_estimated_target_cash(self, currency: Currency) -> AssetPosition:
|
|
343
347
|
"""
|
|
344
348
|
Estimates the target cash weight and shares for a trade proposal.
|
|
345
349
|
|
|
@@ -380,7 +384,29 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
380
384
|
with suppress(ValueError):
|
|
381
385
|
total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component)
|
|
382
386
|
|
|
383
|
-
|
|
387
|
+
cash_component = Cash.objects.get_or_create(
|
|
388
|
+
currency=self.portfolio.currency, defaults={"name": self.portfolio.currency.title}
|
|
389
|
+
)[0]
|
|
390
|
+
# otherwise, we create a new position
|
|
391
|
+
underlying_quote_price = InstrumentPrice.objects.get_or_create(
|
|
392
|
+
instrument=cash_component,
|
|
393
|
+
date=self.trade_date,
|
|
394
|
+
calculated=False,
|
|
395
|
+
defaults={"net_value": Decimal(1)},
|
|
396
|
+
)[0]
|
|
397
|
+
return AssetPosition(
|
|
398
|
+
underlying_quote=cash_component,
|
|
399
|
+
portfolio_created=None,
|
|
400
|
+
portfolio=self.portfolio,
|
|
401
|
+
date=self.trade_date,
|
|
402
|
+
weighting=target_cash_weight,
|
|
403
|
+
initial_price=underlying_quote_price.net_value,
|
|
404
|
+
initial_shares=total_target_shares,
|
|
405
|
+
asset_valuation_date=self.trade_date,
|
|
406
|
+
underlying_quote_price=underlying_quote_price,
|
|
407
|
+
currency=cash_component.currency,
|
|
408
|
+
is_estimated=False,
|
|
409
|
+
)
|
|
384
410
|
|
|
385
411
|
# Start FSM logics
|
|
386
412
|
|
|
@@ -414,23 +440,20 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
414
440
|
Trade.objects.bulk_update(trades, ["status", "comment"])
|
|
415
441
|
|
|
416
442
|
# If we estimate cash on this trade proposal, we make sure to create the corresponding cash component
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
)
|
|
443
|
+
estimated_cash_position = self.get_estimated_target_cash(self.portfolio.currency)
|
|
444
|
+
target_portfolio = self.validated_trading_service.target_portfolio
|
|
445
|
+
if estimated_cash_position.weighting:
|
|
446
|
+
if existing_cash_position := target_portfolio.positions_map.get(
|
|
447
|
+
estimated_cash_position.underlying_quote.id
|
|
448
|
+
):
|
|
449
|
+
existing_cash_position += estimated_cash_position
|
|
450
|
+
else:
|
|
451
|
+
target_portfolio.positions_map[estimated_cash_position.underlying_quote.id] = (
|
|
452
|
+
estimated_cash_position._build_dto()
|
|
453
|
+
)
|
|
454
|
+
target_portfolio = PortfolioDTO(positions=tuple(target_portfolio.positions_map.values()))
|
|
430
455
|
|
|
431
|
-
self.evaluate_active_rules(
|
|
432
|
-
self.trade_date, self.validated_trading_service.target_portfolio, asynchronously=True
|
|
433
|
-
)
|
|
456
|
+
self.evaluate_active_rules(self.trade_date, target_portfolio, asynchronously=True)
|
|
434
457
|
|
|
435
458
|
def can_submit(self):
|
|
436
459
|
errors = dict()
|
|
@@ -486,12 +509,23 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
486
509
|
raise ValueError("Non-Rebalanceable portfolio cannot be traded manually.")
|
|
487
510
|
trades = []
|
|
488
511
|
assets = []
|
|
512
|
+
# We do not want to create the estimated cash position if there is not trades in the trade proposal (shouldn't be possible anyway)
|
|
513
|
+
estimated_cash_position = self.get_estimated_target_cash(self.portfolio.currency)
|
|
514
|
+
|
|
489
515
|
for trade in self.trades.all():
|
|
490
516
|
with suppress(ValueError):
|
|
491
|
-
|
|
517
|
+
asset = trade.to_asset()
|
|
518
|
+
# we add the corresponding asset only if it is not the cache position (already included in estimated_cash_position)
|
|
519
|
+
if asset.underlying_quote != estimated_cash_position.underlying_quote:
|
|
520
|
+
assets.append(asset)
|
|
492
521
|
trade.status = Trade.Status.EXECUTED
|
|
493
522
|
trades.append(trade)
|
|
494
523
|
|
|
524
|
+
# if there is cash leftover, we create an extra asset position to hold the cash component
|
|
525
|
+
if estimated_cash_position.weighting and len(trades) > 0:
|
|
526
|
+
estimated_cash_position.pre_save()
|
|
527
|
+
assets.append(estimated_cash_position)
|
|
528
|
+
|
|
495
529
|
Trade.objects.bulk_update(trades, ["status"])
|
|
496
530
|
self.portfolio.bulk_create_positions(assets, evaluate_rebalancer=False)
|
|
497
531
|
if replay and self.portfolio.is_manageable:
|
|
@@ -5,7 +5,7 @@ from unittest.mock import call, patch
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from faker import Faker
|
|
8
|
-
from pandas._libs.tslibs.offsets import BDay,
|
|
8
|
+
from pandas._libs.tslibs.offsets import BDay, BusinessMonthEnd
|
|
9
9
|
|
|
10
10
|
from wbportfolio.models import Portfolio, RebalancingModel, TradeProposal
|
|
11
11
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
@@ -290,6 +290,25 @@ class TestTradeProposal:
|
|
|
290
290
|
assert t2.weighting == Decimal("0.1")
|
|
291
291
|
assert t3.weighting == Decimal("0.6")
|
|
292
292
|
|
|
293
|
+
# build the target portfolio
|
|
294
|
+
new_target_portfolio = PortfolioDTO(
|
|
295
|
+
positions=(
|
|
296
|
+
Position(underlying_instrument=i1.id, date=trade_proposal.trade_date, weighting=Decimal("0.2")),
|
|
297
|
+
Position(underlying_instrument=i2.id, date=trade_proposal.trade_date, weighting=Decimal("0.3")),
|
|
298
|
+
Position(underlying_instrument=i3.id, date=trade_proposal.trade_date, weighting=Decimal("0.5")),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
trade_proposal.reset_trades(target_portfolio=new_target_portfolio)
|
|
303
|
+
# Refetch the trades for each instrument
|
|
304
|
+
t1.refresh_from_db()
|
|
305
|
+
t2.refresh_from_db()
|
|
306
|
+
t3.refresh_from_db()
|
|
307
|
+
# Assert existing trade weights are correctly updated
|
|
308
|
+
assert t1.weighting == Decimal("-0.5")
|
|
309
|
+
assert t2.weighting == Decimal("0")
|
|
310
|
+
assert t3.weighting == Decimal("0.5")
|
|
311
|
+
|
|
293
312
|
# Test replaying trade proposals
|
|
294
313
|
@patch.object(Portfolio, "batch_portfolio")
|
|
295
314
|
def test_replay(self, mock_fct, trade_proposal_factory):
|
|
@@ -303,12 +322,12 @@ class TestTradeProposal:
|
|
|
303
322
|
tp1 = trade_proposal_factory.create(
|
|
304
323
|
portfolio=tp0.portfolio,
|
|
305
324
|
status=TradeProposal.Status.APPROVED,
|
|
306
|
-
trade_date=(tp0.trade_date +
|
|
325
|
+
trade_date=(tp0.trade_date + BusinessMonthEnd(1)).date(),
|
|
307
326
|
)
|
|
308
327
|
tp2 = trade_proposal_factory.create(
|
|
309
328
|
portfolio=tp0.portfolio,
|
|
310
329
|
status=TradeProposal.Status.APPROVED,
|
|
311
|
-
trade_date=(tp1.trade_date +
|
|
330
|
+
trade_date=(tp1.trade_date + BusinessMonthEnd(1)).date(),
|
|
312
331
|
)
|
|
313
332
|
|
|
314
333
|
# Replay trade proposals
|
|
@@ -374,8 +393,6 @@ class TestTradeProposal:
|
|
|
374
393
|
weighting=Decimal("0.2"),
|
|
375
394
|
)
|
|
376
395
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
380
|
-
assert target_cash_weight == Decimal("0.2") + Decimal("1.0") - (Decimal("0.7") + Decimal("0.2"))
|
|
381
|
-
assert total_target_shares == Decimal(1_000_000) * Decimal("0.3")
|
|
396
|
+
target_cash_position = trade_proposal.get_estimated_target_cash(trade_proposal.portfolio.currency)
|
|
397
|
+
assert target_cash_position.weighting == Decimal("0.2") + Decimal("1.0") - (Decimal("0.7") + Decimal("0.2"))
|
|
398
|
+
assert target_cash_position.initial_shares == Decimal(1_000_000) * Decimal("0.3")
|
wbportfolio/viewsets/assets.py
CHANGED
|
@@ -14,6 +14,7 @@ from wbcore.filters import DjangoFilterBackend
|
|
|
14
14
|
from wbcore.pandas import fields as pf
|
|
15
15
|
from wbcore.permissions.permissions import InternalUserPermissionMixin
|
|
16
16
|
from wbcore.serializers import decorator
|
|
17
|
+
from wbcore.utils.date import get_date_interval_from_request
|
|
17
18
|
from wbcore.utils.figures import (
|
|
18
19
|
get_default_timeserie_figure,
|
|
19
20
|
get_hovertemplate_timeserie,
|
|
@@ -28,6 +29,7 @@ from wbportfolio.filters import (
|
|
|
28
29
|
AssetPositionPortfolioFilter,
|
|
29
30
|
AssetPositionUnderlyingInstrumentChartFilter,
|
|
30
31
|
CashPositionPortfolioFilterSet,
|
|
32
|
+
CompositionContributionChartFilter,
|
|
31
33
|
CompositionModelPortfolioPandasFilter,
|
|
32
34
|
ContributionChartFilter,
|
|
33
35
|
)
|
|
@@ -354,19 +356,50 @@ class ContributorPortfolioChartView(UserPortfolioRequestPermissionMixin, viewset
|
|
|
354
356
|
title_config_class = ContributorPortfolioChartTitleConfig
|
|
355
357
|
endpoint_config_class = ContributorPortfolioChartEndpointConfig
|
|
356
358
|
|
|
359
|
+
@cached_property
|
|
360
|
+
def hedged_currency(self) -> Currency | None:
|
|
361
|
+
if "hedged_currency" in self.request.GET:
|
|
362
|
+
with suppress(Currency.DoesNotExist):
|
|
363
|
+
return Currency.objects.get(pk=self.kwargs["hedged_currency"])
|
|
364
|
+
|
|
365
|
+
@cached_property
|
|
366
|
+
def show_lookthrough(self) -> bool:
|
|
367
|
+
return self.portfolio.is_composition and self.request.GET.get("show_lookthrough", "false").lower() == "true"
|
|
368
|
+
|
|
369
|
+
def get_filterset_class(self, request):
|
|
370
|
+
if self.portfolio.is_composition:
|
|
371
|
+
return CompositionContributionChartFilter
|
|
372
|
+
return ContributionChartFilter
|
|
373
|
+
|
|
357
374
|
def get_plotly(self, queryset):
|
|
358
375
|
fig = go.Figure()
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
376
|
+
data = []
|
|
377
|
+
if self.show_lookthrough:
|
|
378
|
+
d1, d2 = get_date_interval_from_request(self.request)
|
|
379
|
+
for _d in pd.date_range(d1, d2):
|
|
380
|
+
for pos in self.portfolio.get_lookthrough_positions(_d.date()):
|
|
381
|
+
data.append(
|
|
382
|
+
[
|
|
383
|
+
pos.date,
|
|
384
|
+
pos.initial_price,
|
|
385
|
+
pos.initial_currency_fx_rate,
|
|
386
|
+
pos.underlying_instrument_id,
|
|
387
|
+
pos.weighting,
|
|
388
|
+
]
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
data = queryset.annotate_hedged_currency_fx_rate(self.hedged_currency).values_list(
|
|
392
|
+
"date", "price", "hedged_currency_fx_rate", "underlying_instrument", "weighting"
|
|
393
|
+
)
|
|
394
|
+
df = Portfolio.get_contribution_df(data).rename(columns={"group_key": "underlying_instrument"})
|
|
365
395
|
if not df.empty:
|
|
366
396
|
df = df[["contribution_total", "contribution_forex", "underlying_instrument"]].sort_values(
|
|
367
397
|
by="contribution_total", ascending=True
|
|
368
398
|
)
|
|
369
|
-
|
|
399
|
+
|
|
400
|
+
df["instrument_id"] = df.underlying_instrument.map(
|
|
401
|
+
dict(Instrument.objects.filter(id__in=df["underlying_instrument"]).values_list("id", "name_repr"))
|
|
402
|
+
)
|
|
370
403
|
df_forex = df[["instrument_id", "contribution_forex"]]
|
|
371
404
|
df_forex = df_forex[df_forex.contribution_forex != 0]
|
|
372
405
|
|
|
@@ -120,7 +120,6 @@ class AssetPositionPandasView(
|
|
|
120
120
|
groupby_classification_height = int(request.GET.get("groupby_classification_height", "0"))
|
|
121
121
|
|
|
122
122
|
groupby_id = GroupbyChoice.get_id(groupby.name)
|
|
123
|
-
grouby_repr = GroupbyChoice.get_repr(groupby.name)
|
|
124
123
|
if groupby == GroupbyChoice.PRIMARY_CLASSIFICATION:
|
|
125
124
|
queryset = queryset.annotate_classification_for_group(
|
|
126
125
|
primary_classification_group, classification_height=groupby_classification_height, unique=True
|
|
@@ -135,18 +134,15 @@ class AssetPositionPandasView(
|
|
|
135
134
|
primary_classification_group, classification_height=groupby_classification_height
|
|
136
135
|
)
|
|
137
136
|
df = Portfolio.get_contribution_df(
|
|
138
|
-
queryset,
|
|
137
|
+
queryset.values_list("date", "price", "currency_fx_rate_usd", groupby_id, "weighting"),
|
|
139
138
|
need_normalize=True,
|
|
140
|
-
groupby_label_id=groupby_id,
|
|
141
|
-
groubpy_label_title=grouby_repr,
|
|
142
|
-
currency_fx_rate_label="currency_fx_rate_usd",
|
|
143
139
|
)
|
|
144
140
|
df = df.rename(
|
|
145
141
|
columns={
|
|
146
|
-
|
|
147
|
-
grouby_repr: "title",
|
|
142
|
+
"group_key": "id",
|
|
148
143
|
}
|
|
149
144
|
)
|
|
145
|
+
df["title"] = df["id"].map(groupby.get_repr(df["id"]))
|
|
150
146
|
if groupby == GroupbyChoice.UNDERLYING_INSTRUMENT.value:
|
|
151
147
|
df_market_shares = pd.DataFrame(
|
|
152
148
|
queryset.values("market_share", "date", "underlying_instrument")
|
|
@@ -164,7 +160,10 @@ class AssetPositionPandasView(
|
|
|
164
160
|
return df
|
|
165
161
|
|
|
166
162
|
def manipulate_dataframe(self, df):
|
|
167
|
-
|
|
163
|
+
if "titl" in df.columns:
|
|
164
|
+
df["title"] = df["title"].fillna("N/A").astype(str)
|
|
165
|
+
# df[df["title"].isnull()] = "N/A"
|
|
166
|
+
return df.fillna(0)
|
|
168
167
|
|
|
169
168
|
|
|
170
169
|
class AggregatedAssetPositionLiquidityPandasView(InternalUserPermissionMixin, ExportPandasAPIViewSet):
|
|
@@ -407,9 +407,13 @@ class TradeTradeProposalModelViewSet(
|
|
|
407
407
|
def get_aggregates(self, queryset, *args, **kwargs):
|
|
408
408
|
agg = {}
|
|
409
409
|
if queryset.exists():
|
|
410
|
-
|
|
410
|
+
cash_target_position = self.trade_proposal.get_estimated_target_cash(
|
|
411
411
|
self.trade_proposal.portfolio.currency
|
|
412
412
|
)
|
|
413
|
+
cash_target_cash_weight, cash_target_cash_shares = (
|
|
414
|
+
cash_target_position.weighting,
|
|
415
|
+
cash_target_position.initial_shares,
|
|
416
|
+
)
|
|
413
417
|
extra_existing_cash_components = Cash.objects.filter(
|
|
414
418
|
id__in=self.trade_proposal.trades.filter(underlying_instrument__is_cash=True).values(
|
|
415
419
|
"underlying_instrument"
|
|
@@ -417,11 +421,9 @@ class TradeTradeProposalModelViewSet(
|
|
|
417
421
|
).exclude(currency=self.trade_proposal.portfolio.currency)
|
|
418
422
|
|
|
419
423
|
for cash_component in extra_existing_cash_components:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
cash_target_cash_weight += extra_cash_target_cash_weight
|
|
424
|
-
cash_target_cash_shares += extra_cash_target_cash_shares
|
|
424
|
+
extra_cash_position = self.trade_proposal.get_estimated_target_cash(cash_component.currency)
|
|
425
|
+
cash_target_cash_weight += extra_cash_position.weighting
|
|
426
|
+
cash_target_cash_shares += extra_cash_position.initial_shares
|
|
425
427
|
noncash_aggregates = queryset.filter(underlying_instrument__is_cash=False).aggregate(
|
|
426
428
|
sum_target_weight=Sum(F("target_weight")),
|
|
427
429
|
sum_effective_weight=Sum(F("effective_weight")),
|
|
@@ -487,7 +489,7 @@ class TradeTradeProposalModelViewSet(
|
|
|
487
489
|
return TradeTradeProposalModelSerializer
|
|
488
490
|
|
|
489
491
|
def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
|
|
490
|
-
if queryset is not None and queryset.exists():
|
|
492
|
+
if queryset is not None and queryset.exists() and self.trade_proposal.status != TradeProposal.Status.APPROVED:
|
|
491
493
|
total_target_weight = queryset.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
|
|
492
494
|
if round(total_target_weight, 3) != 1:
|
|
493
495
|
warning(
|
|
@@ -74,14 +74,14 @@ wbportfolio/factories/trades.py,sha256=-14d_ABN9FEoXb4wm9-YjYHfP2cCEANNM45cMRZtQ
|
|
|
74
74
|
wbportfolio/factories/transactions.py,sha256=jZqTNRpW344DSYjV5tAqU5y30Kc7GRGbZ3BN9hacKpQ,995
|
|
75
75
|
wbportfolio/fdm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
76
|
wbportfolio/fdm/tasks.py,sha256=59hXt3TOJaM8KpUaKdBNAGsO7NFMHaAlNA0T3FTUau0,533
|
|
77
|
-
wbportfolio/filters/__init__.py,sha256=
|
|
78
|
-
wbportfolio/filters/assets.py,sha256=
|
|
77
|
+
wbportfolio/filters/__init__.py,sha256=m44CdyYMtku8rMGyiX8KSmMdvyqaElKW4UMkEqOC1VM,1374
|
|
78
|
+
wbportfolio/filters/assets.py,sha256=gpCwxlje1mY9IGlxfvQyNgyE5RcT-r3B2tI14hB92X4,18114
|
|
79
79
|
wbportfolio/filters/assets_and_net_new_money_progression.py,sha256=dgI3WWlfYjaDg15-C4Sh2HQoY0kQh9mQUWxn4SAPLnM,1403
|
|
80
80
|
wbportfolio/filters/custodians.py,sha256=pStQgPQPhPpnt57_V7BuXbFXmRiZBEAiEeMFuQmt2NE,287
|
|
81
81
|
wbportfolio/filters/esg.py,sha256=sQ5HwiUlhfKeFqJ2O5koXyTscpN21UKRUSi8d90jSlI,688
|
|
82
82
|
wbportfolio/filters/performances.py,sha256=fAMb8HkWSmBl1xg5P4MZ8WJsV5x7uCcBf7ETHdfXjkI,7325
|
|
83
83
|
wbportfolio/filters/portfolios.py,sha256=IqkDj3POQ1MHGZO9k7TNuhf5fZ_yR9vPPcMFELkQsnQ,1469
|
|
84
|
-
wbportfolio/filters/positions.py,sha256=
|
|
84
|
+
wbportfolio/filters/positions.py,sha256=ERdcTCNw7peGstKSl3W5t1wlSUM8a3anoesoycohF98,8548
|
|
85
85
|
wbportfolio/filters/products.py,sha256=_QFL3I132q-3JUDji_Wh2Qkv45DKnQrR3N8_luGK0K0,6492
|
|
86
86
|
wbportfolio/filters/roles.py,sha256=-0tOl-WJDqwXAfcJePpkjQfZB9o13I99euem5XGpMjk,920
|
|
87
87
|
wbportfolio/filters/signals.py,sha256=3GrsxPwZg-6bvvZ222VfF212yNTJYQP4n5hWIEnTtpA,3144
|
|
@@ -245,11 +245,11 @@ wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py,sha256=4
|
|
|
245
245
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
246
246
|
wbportfolio/models/__init__.py,sha256=IIS_PNRxyX2Dcvyk1bcQOUzFt0B9SPC0WlM88CXqj04,881
|
|
247
247
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
248
|
-
wbportfolio/models/asset.py,sha256=
|
|
248
|
+
wbportfolio/models/asset.py,sha256=H4UvrEstrCCbRG_poF68Ih4JFgHE08guMkyhNCZ7QFQ,38636
|
|
249
249
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
250
250
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
251
251
|
wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
|
|
252
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
252
|
+
wbportfolio/models/portfolio.py,sha256=dUj1iPZke17qYk2zSSL-6Mcjr7cKAoq_wIrZtEmTmLg,58666
|
|
253
253
|
wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
|
|
254
254
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
255
255
|
wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
|
|
@@ -275,8 +275,8 @@ wbportfolio/models/transactions/claim.py,sha256=hcx_tJ9luf2-s1qqsUZXtoDEuxFyty2A
|
|
|
275
275
|
wbportfolio/models/transactions/dividends.py,sha256=92-jG8bZN9nU9oDubpu-UDH43Ri7kVjhqE_esOSmOzo,471
|
|
276
276
|
wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
|
|
277
277
|
wbportfolio/models/transactions/fees.py,sha256=ffvqo8I4A0l5rLi00jJ6sGot0jmnkoxaNsbDzdPLwCg,5712
|
|
278
|
-
wbportfolio/models/transactions/rebalancing.py,sha256=
|
|
279
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
278
|
+
wbportfolio/models/transactions/rebalancing.py,sha256=obzgewWKOD4kJbCoF5fhtfDk502QkbrjPKh8T9KDGew,7355
|
|
279
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=a4bxSn6xGuURwsE-WqCJN5gBdwu9TpCf2E8kKn4_RiY,29659
|
|
280
280
|
wbportfolio/models/transactions/trades.py,sha256=NA_hUpFKQ4H-w3B0gMZ5IFkZo6WJTH2S7GBUQC1jUpU,28922
|
|
281
281
|
wbportfolio/models/transactions/transactions.py,sha256=fWoDf0TSV0L0gLUDOQpCRLzjMt1H4MUvUHGEaMsilCc,7027
|
|
282
282
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -384,7 +384,7 @@ wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
|
|
|
384
384
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
385
385
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=1gp_h_CCC4Z_cWHUgrZCjGAxYuT2u8FZdw0krDpESiY,2801
|
|
386
386
|
wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
|
|
387
|
-
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=
|
|
387
|
+
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=P05F1RC53xjkp8BFq8D57k-86eAOkRQw6doLVX5SQlM,17945
|
|
388
388
|
wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0Wq9ycA2oeeEBhO3dzV7sbXEU,9863
|
|
389
389
|
wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
390
390
|
wbportfolio/tests/pms/test_analytics.py,sha256=fAuY1zcXibttFpBh2GhKVyzdYfi1kz_b7SPa9xZQXY0,1086
|
|
@@ -400,7 +400,7 @@ wbportfolio/tests/viewsets/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5J
|
|
|
400
400
|
wbportfolio/tests/viewsets/transactions/test_claims.py,sha256=QEZfMAW07dyoZ63t2umSwGOqvaTULfYfbN_F4ZoSAcw,6368
|
|
401
401
|
wbportfolio/viewsets/__init__.py,sha256=3kUaQ66ybvROwejd3bEcSt4XKzfOlPDaeoStMvlz7qY,2294
|
|
402
402
|
wbportfolio/viewsets/adjustments.py,sha256=5hWCjxSgUIsrPOmJKoDYK3gywdMTI0aYDorEj1FXRVc,1429
|
|
403
|
-
wbportfolio/viewsets/assets.py,sha256=
|
|
403
|
+
wbportfolio/viewsets/assets.py,sha256=FgJhBK13tAkrDyTiWsgnIT0Oq39mlvJTQSq1ztopQhY,24172
|
|
404
404
|
wbportfolio/viewsets/assets_and_net_new_money_progression.py,sha256=Jl4vEQP4N2OFL5IGBXoKcj-0qaPviU0I8npvQLw4Io0,4464
|
|
405
405
|
wbportfolio/viewsets/custodians.py,sha256=CTFqkqVP1R3AV7lhdvcdICxB5DfwDYCyikNSI5kbYEo,2322
|
|
406
406
|
wbportfolio/viewsets/esg.py,sha256=27MxxdXQH3Cq_1UEYmcrF7htUOg6i81fUpbVQXAAKJI,6985
|
|
@@ -410,7 +410,7 @@ wbportfolio/viewsets/portfolio_cash_targets.py,sha256=CvHlrDE8qnnnfRpTYnFu-Uu15M
|
|
|
410
410
|
wbportfolio/viewsets/portfolio_relationship.py,sha256=RGyvxd8NfFEs8YdqEvVD3VbrISvAO5UtCTlocSIuWQw,2109
|
|
411
411
|
wbportfolio/viewsets/portfolio_swing_pricing.py,sha256=-57l3WLQZRslIV67OT0ucHE5JXTtTtLvd3t7MppdVn8,357
|
|
412
412
|
wbportfolio/viewsets/portfolios.py,sha256=1POzE9jrt2iLVMnIY_BWDr0A_zpOlO3Z8tM80TaXkhk,14454
|
|
413
|
-
wbportfolio/viewsets/positions.py,sha256=
|
|
413
|
+
wbportfolio/viewsets/positions.py,sha256=2rzFHB_SI09rXC_EYi58G_eqvzONbk8z61JDkkjt3Ew,13207
|
|
414
414
|
wbportfolio/viewsets/product_groups.py,sha256=YvmuXPPy98K1J_rz6YPsx9gNK-tCS2P-wc1uRYgfyo0,2399
|
|
415
415
|
wbportfolio/viewsets/product_performance.py,sha256=dRfRgifjGS1RgZSu9uJRM0SmB7eLnNUkPuqARMO4gyo,28371
|
|
416
416
|
wbportfolio/viewsets/products.py,sha256=1KXUDXdNmBFYjQcvJkwUCnjIOKZT0cAk_XTihMKRWvw,20459
|
|
@@ -519,9 +519,9 @@ wbportfolio/viewsets/transactions/fees.py,sha256=7VUXIogmRrXCz_D9tvDiiTae0t5j09W
|
|
|
519
519
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
520
520
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
521
521
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=iQpC_Thbj56SmM05vPRsF1JZguGBDaTUH3I-_iCHCV0,5958
|
|
522
|
-
wbportfolio/viewsets/transactions/trades.py,sha256
|
|
522
|
+
wbportfolio/viewsets/transactions/trades.py,sha256=-yJ4j8NJTu2VWyhCq5BXGNND_925Ietoxx9k07SLVh0,21634
|
|
523
523
|
wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
|
|
524
|
-
wbportfolio-1.49.
|
|
525
|
-
wbportfolio-1.49.
|
|
526
|
-
wbportfolio-1.49.
|
|
527
|
-
wbportfolio-1.49.
|
|
524
|
+
wbportfolio-1.49.8.dist-info/METADATA,sha256=tEN7pGOUaI3BhAt-b4IyD7LRCi1jWgqwW6linsZA4zQ,734
|
|
525
|
+
wbportfolio-1.49.8.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
526
|
+
wbportfolio-1.49.8.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
527
|
+
wbportfolio-1.49.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|