wbportfolio 1.49.5__py2.py3-none-any.whl → 1.49.7__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 -3
- wbportfolio/models/portfolio.py +44 -68
- wbportfolio/models/transactions/rebalancing.py +9 -5
- wbportfolio/models/transactions/trade_proposals.py +112 -52
- wbportfolio/models/transactions/trades.py +38 -15
- wbportfolio/rebalancing/models/composite.py +6 -2
- wbportfolio/rebalancing/models/equally_weighted.py +12 -1
- wbportfolio/rebalancing/models/model_portfolio.py +11 -5
- wbportfolio/tests/models/transactions/test_rebalancing.py +6 -2
- wbportfolio/tests/models/transactions/test_trade_proposals.py +25 -8
- wbportfolio/tests/rebalancing/test_models.py +18 -12
- wbportfolio/viewsets/assets.py +40 -7
- wbportfolio/viewsets/configs/display/portfolios.py +1 -3
- wbportfolio/viewsets/positions.py +7 -8
- wbportfolio/viewsets/transactions/trades.py +9 -7
- {wbportfolio-1.49.5.dist-info → wbportfolio-1.49.7.dist-info}/METADATA +1 -1
- {wbportfolio-1.49.5.dist-info → wbportfolio-1.49.7.dist-info}/RECORD +22 -22
- {wbportfolio-1.49.5.dist-info → wbportfolio-1.49.7.dist-info}/WHEEL +0 -0
- {wbportfolio-1.49.5.dist-info → wbportfolio-1.49.7.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)
|
|
@@ -511,7 +532,6 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
511
532
|
self.initial_shares = new_weighting * self.get_portfolio_total_asset_value()
|
|
512
533
|
else:
|
|
513
534
|
self.initial_shares = (new_weighting / self.weighting) * self.initial_shares
|
|
514
|
-
self.save()
|
|
515
535
|
|
|
516
536
|
def get_portfolio_total_asset_value(self) -> Decimal:
|
|
517
537
|
return self.portfolio.get_total_asset_value(self.date)
|
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
|
|
@@ -80,8 +81,6 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
80
81
|
if not self.trade_date and self.portfolio.assets.exists():
|
|
81
82
|
self.trade_date = (self.portfolio.assets.latest("date").date + BDay(1)).date()
|
|
82
83
|
super().save(*args, **kwargs)
|
|
83
|
-
if self.status == TradeProposal.Status.APPROVED:
|
|
84
|
-
self.portfolio.change_at_date(self.trade_date)
|
|
85
84
|
|
|
86
85
|
@property
|
|
87
86
|
def checked_object(self):
|
|
@@ -256,6 +255,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
256
255
|
# we cannot do a bulk-create because Trade is a multi table inheritance
|
|
257
256
|
try:
|
|
258
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
|
|
259
261
|
except Trade.DoesNotExist:
|
|
260
262
|
trade = Trade(
|
|
261
263
|
underlying_instrument=instrument,
|
|
@@ -272,35 +274,43 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
272
274
|
|
|
273
275
|
def replay(self):
|
|
274
276
|
last_trade_proposal = self
|
|
277
|
+
last_trade_proposal_created = False
|
|
275
278
|
while last_trade_proposal and last_trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
279
|
+
if not last_trade_proposal_created:
|
|
280
|
+
logger.info(f"Replaying trade proposal {last_trade_proposal}")
|
|
281
|
+
last_trade_proposal.portfolio.assets.filter(
|
|
282
|
+
date=last_trade_proposal.trade_date
|
|
283
|
+
).delete() # we delete the existing position and we reapply the trade proposal
|
|
284
|
+
if last_trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
285
|
+
logger.info("Reverting trade proposal ...")
|
|
286
|
+
last_trade_proposal.revert()
|
|
287
|
+
if last_trade_proposal.status == TradeProposal.Status.DRAFT:
|
|
288
|
+
if self.rebalancing_model: # if there is no position (for any reason) or we the trade proposal has a rebalancer model attached (trades are computed based on an aglo), we reapply this trade proposal
|
|
289
|
+
logger.info(f"Resetting trades from rebalancer model {self.rebalancing_model} ...")
|
|
290
|
+
with suppress(
|
|
291
|
+
ValidationError
|
|
292
|
+
): # we silent any validation error while setting proposal, because if this happens, we assume the current trade proposal state if valid and we continue to batch compute
|
|
293
|
+
self.reset_trades()
|
|
294
|
+
logger.info("Submitting trade proposal ...")
|
|
295
|
+
last_trade_proposal.submit()
|
|
296
|
+
if last_trade_proposal.status == TradeProposal.Status.SUBMIT:
|
|
297
|
+
logger.info("Approving trade proposal ...")
|
|
298
|
+
last_trade_proposal.approve(replay=False)
|
|
299
|
+
last_trade_proposal.save()
|
|
296
300
|
next_trade_proposal = last_trade_proposal.next_trade_proposal
|
|
301
|
+
|
|
297
302
|
next_trade_date = (
|
|
298
303
|
next_trade_proposal.trade_date - timedelta(days=1) if next_trade_proposal else date.today()
|
|
299
304
|
)
|
|
300
305
|
overriding_trade_proposal = last_trade_proposal.portfolio.batch_portfolio(
|
|
301
306
|
last_trade_proposal.trade_date, next_trade_date
|
|
302
307
|
)
|
|
303
|
-
|
|
308
|
+
if overriding_trade_proposal:
|
|
309
|
+
last_trade_proposal_created = True
|
|
310
|
+
last_trade_proposal = overriding_trade_proposal
|
|
311
|
+
else:
|
|
312
|
+
last_trade_proposal_created = False
|
|
313
|
+
last_trade_proposal = next_trade_proposal
|
|
304
314
|
|
|
305
315
|
def get_estimated_shares(self, weight: Decimal, underlying_quote: Instrument) -> Decimal:
|
|
306
316
|
"""
|
|
@@ -333,7 +343,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
333
343
|
except Exception:
|
|
334
344
|
raise ValueError("We couldn't estimate the number of shares")
|
|
335
345
|
|
|
336
|
-
def get_estimated_target_cash(self, currency: Currency) ->
|
|
346
|
+
def get_estimated_target_cash(self, currency: Currency) -> AssetPosition:
|
|
337
347
|
"""
|
|
338
348
|
Estimates the target cash weight and shares for a trade proposal.
|
|
339
349
|
|
|
@@ -374,7 +384,29 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
374
384
|
with suppress(ValueError):
|
|
375
385
|
total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component)
|
|
376
386
|
|
|
377
|
-
|
|
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
|
+
)
|
|
378
410
|
|
|
379
411
|
# Start FSM logics
|
|
380
412
|
|
|
@@ -398,30 +430,30 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
398
430
|
},
|
|
399
431
|
)
|
|
400
432
|
def submit(self, by=None, description=None, **kwargs):
|
|
401
|
-
self.trades.update(comment="", status=Trade.Status.DRAFT)
|
|
402
433
|
self.reset_trades(target_portfolio=self._build_dto().convert_to_portfolio())
|
|
434
|
+
trades = []
|
|
403
435
|
for trade in self.trades.all():
|
|
404
|
-
trade.
|
|
405
|
-
trade.
|
|
436
|
+
trade.status = Trade.Status.SUBMIT
|
|
437
|
+
trade.comment = ""
|
|
438
|
+
trades.append(trade)
|
|
439
|
+
|
|
440
|
+
Trade.objects.bulk_update(trades, ["status", "comment"])
|
|
406
441
|
|
|
407
442
|
# If we estimate cash on this trade proposal, we make sure to create the corresponding cash component
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
)
|
|
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()))
|
|
421
455
|
|
|
422
|
-
self.evaluate_active_rules(
|
|
423
|
-
self.trade_date, self.validated_trading_service.target_portfolio, asynchronously=True
|
|
424
|
-
)
|
|
456
|
+
self.evaluate_active_rules(self.trade_date, target_portfolio, asynchronously=True)
|
|
425
457
|
|
|
426
458
|
def can_submit(self):
|
|
427
459
|
errors = dict()
|
|
@@ -475,11 +507,27 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
475
507
|
# We validate trade which will create or update the initial asset positions
|
|
476
508
|
if not self.portfolio.can_be_rebalanced:
|
|
477
509
|
raise ValueError("Non-Rebalanceable portfolio cannot be traded manually.")
|
|
478
|
-
|
|
479
|
-
|
|
510
|
+
trades = []
|
|
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
|
+
|
|
480
515
|
for trade in self.trades.all():
|
|
481
|
-
|
|
482
|
-
|
|
516
|
+
with suppress(ValueError):
|
|
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)
|
|
521
|
+
trade.status = Trade.Status.EXECUTED
|
|
522
|
+
trades.append(trade)
|
|
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
|
+
|
|
529
|
+
Trade.objects.bulk_update(trades, ["status"])
|
|
530
|
+
self.portfolio.bulk_create_positions(assets, evaluate_rebalancer=False)
|
|
483
531
|
if replay and self.portfolio.is_manageable:
|
|
484
532
|
replay_as_task.delay(self.id)
|
|
485
533
|
|
|
@@ -584,10 +632,22 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
584
632
|
def revert(self, **kwargs):
|
|
585
633
|
with suppress(KeyError):
|
|
586
634
|
del self.__dict__["validated_trading_service"]
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
635
|
+
trades = []
|
|
636
|
+
assets = []
|
|
637
|
+
for trade in self.trades.all():
|
|
638
|
+
trade.status = Trade.Status.DRAFT
|
|
639
|
+
trades.append(trade)
|
|
640
|
+
with suppress(AssetPosition.DoesNotExist):
|
|
641
|
+
asset = AssetPosition.unannotated_objects.get(
|
|
642
|
+
underlying_quote=trade.underlying_instrument,
|
|
643
|
+
portfolio=trade.portfolio,
|
|
644
|
+
date=trade.transaction_date,
|
|
645
|
+
is_estimated=False,
|
|
646
|
+
)
|
|
647
|
+
asset.set_weighting(asset.weighting - trade.weighting)
|
|
648
|
+
assets.append(asset)
|
|
649
|
+
Trade.objects.bulk_update(trades, ["status"])
|
|
650
|
+
self.portfolio.bulk_create_positions(assets, evaluate_rebalancer=False)
|
|
591
651
|
|
|
592
652
|
def can_revert(self):
|
|
593
653
|
errors = dict()
|
|
@@ -299,25 +299,47 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
299
299
|
)
|
|
300
300
|
},
|
|
301
301
|
)
|
|
302
|
+
def to_asset(self) -> AssetPosition:
|
|
303
|
+
last_underlying_quote_price = self.last_underlying_quote_price
|
|
304
|
+
if not last_underlying_quote_price:
|
|
305
|
+
raise ValueError("No price found")
|
|
306
|
+
asset = AssetPosition(
|
|
307
|
+
underlying_quote=self.underlying_instrument,
|
|
308
|
+
portfolio_created=None,
|
|
309
|
+
portfolio=self.portfolio,
|
|
310
|
+
date=self.transaction_date,
|
|
311
|
+
initial_currency_fx_rate=self.currency_fx_rate,
|
|
312
|
+
weighting=self._target_weight,
|
|
313
|
+
initial_price=self.last_underlying_quote_price.net_value,
|
|
314
|
+
initial_shares=None,
|
|
315
|
+
underlying_quote_price=self.last_underlying_quote_price,
|
|
316
|
+
asset_valuation_date=self.transaction_date,
|
|
317
|
+
currency=self.currency,
|
|
318
|
+
is_estimated=False,
|
|
319
|
+
)
|
|
320
|
+
asset.set_weighting(self._target_weight)
|
|
321
|
+
asset.pre_save()
|
|
322
|
+
return asset
|
|
323
|
+
|
|
302
324
|
def execute(self, **kwargs):
|
|
303
|
-
|
|
304
|
-
asset
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
with suppress(ValueError):
|
|
326
|
+
asset = self.to_asset()
|
|
327
|
+
AssetPosition.unannotated_objects.update_or_create(
|
|
328
|
+
underlying_quote=asset.underlying_quote,
|
|
329
|
+
portfolio_created=asset.portfolio_created,
|
|
330
|
+
portfolio=asset.portfolio,
|
|
331
|
+
date=asset.date,
|
|
309
332
|
defaults={
|
|
310
|
-
"initial_currency_fx_rate":
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"
|
|
317
|
-
"
|
|
333
|
+
"initial_currency_fx_rate": asset.initial_currency_fx_rate,
|
|
334
|
+
"initial_price": asset.initial_price,
|
|
335
|
+
"initial_shares": asset.initial_shares,
|
|
336
|
+
"underlying_quote_price": asset.underlying_quote_price,
|
|
337
|
+
"asset_valuation_date": asset.asset_valuation_date,
|
|
338
|
+
"currency": asset.currency,
|
|
339
|
+
"is_estimated": asset.is_estimated,
|
|
340
|
+
"weighting": asset.weighting,
|
|
318
341
|
},
|
|
319
342
|
)
|
|
320
|
-
asset.set_weighting(self._target_weight)
|
|
321
343
|
|
|
322
344
|
def can_execute(self):
|
|
323
345
|
if not self.last_underlying_quote_price:
|
|
@@ -402,6 +424,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
402
424
|
is_estimated=False,
|
|
403
425
|
)
|
|
404
426
|
asset.set_weighting(asset.weighting - self.weighting)
|
|
427
|
+
asset.save()
|
|
405
428
|
|
|
406
429
|
@property
|
|
407
430
|
def product(self):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
3
|
from django.core.exceptions import ObjectDoesNotExist
|
|
4
|
+
from wbfdm.models import InstrumentPrice
|
|
4
5
|
|
|
5
6
|
from wbportfolio.models import Trade
|
|
6
7
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
@@ -20,7 +21,7 @@ class CompositeRebalancing(AbstractRebalancingModel):
|
|
|
20
21
|
"""
|
|
21
22
|
try:
|
|
22
23
|
latest_trade_proposal = self.portfolio.trade_proposals.filter(
|
|
23
|
-
status="APPROVED",
|
|
24
|
+
status="APPROVED", trade_date__lt=self.trade_date
|
|
24
25
|
).latest("trade_date")
|
|
25
26
|
return {
|
|
26
27
|
v["underlying_instrument"]: v["target_weight"]
|
|
@@ -33,7 +34,10 @@ class CompositeRebalancing(AbstractRebalancingModel):
|
|
|
33
34
|
return dict()
|
|
34
35
|
|
|
35
36
|
def is_valid(self) -> bool:
|
|
36
|
-
return
|
|
37
|
+
return (
|
|
38
|
+
len(self.base_assets.keys()) > 0
|
|
39
|
+
and InstrumentPrice.objects.filter(date=self.trade_date, instrument__in=self.base_assets.keys()).exists()
|
|
40
|
+
)
|
|
37
41
|
|
|
38
42
|
def get_target_portfolio(self) -> Portfolio:
|
|
39
43
|
positions = []
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
|
+
from wbfdm.models import InstrumentPrice
|
|
4
|
+
|
|
3
5
|
from wbportfolio.pms.typing import Portfolio
|
|
4
6
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
5
7
|
from wbportfolio.rebalancing.decorators import register
|
|
@@ -7,8 +9,17 @@ from wbportfolio.rebalancing.decorators import register
|
|
|
7
9
|
|
|
8
10
|
@register("Equally Weighted Rebalancing")
|
|
9
11
|
class EquallyWeightedRebalancing(AbstractRebalancingModel):
|
|
12
|
+
def __init__(self, *args, **kwargs):
|
|
13
|
+
super().__init__(*args, **kwargs)
|
|
14
|
+
self.assets = self.portfolio.assets.filter(date=self.last_effective_date)
|
|
15
|
+
|
|
10
16
|
def is_valid(self) -> bool:
|
|
11
|
-
return
|
|
17
|
+
return (
|
|
18
|
+
self.assets.exists()
|
|
19
|
+
and InstrumentPrice.objects.filter(
|
|
20
|
+
date=self.trade_date, instrument__in=self.assets.values("underlying_quote")
|
|
21
|
+
).exists()
|
|
22
|
+
)
|
|
12
23
|
|
|
13
24
|
def get_target_portfolio(self) -> Portfolio:
|
|
14
25
|
positions = []
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from wbfdm.models import InstrumentPrice
|
|
2
|
+
|
|
1
3
|
from wbportfolio.pms.typing import Portfolio
|
|
2
4
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
3
5
|
from wbportfolio.rebalancing.decorators import register
|
|
@@ -18,11 +20,15 @@ class ModelPortfolioRebalancing(AbstractRebalancingModel):
|
|
|
18
20
|
return model_portfolio_rel.dependency_portfolio
|
|
19
21
|
|
|
20
22
|
def is_valid(self) -> bool:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if model_portfolio := self.model_portfolio:
|
|
24
|
+
assets = model_portfolio.get_positions(self.last_effective_date)
|
|
25
|
+
return (
|
|
26
|
+
assets.exists()
|
|
27
|
+
and InstrumentPrice.objects.filter(
|
|
28
|
+
date=self.trade_date, instrument__in=assets.values("underlying_quote")
|
|
29
|
+
).exists()
|
|
30
|
+
)
|
|
31
|
+
return False
|
|
26
32
|
|
|
27
33
|
def get_target_portfolio(self) -> Portfolio:
|
|
28
34
|
positions = []
|
|
@@ -11,11 +11,15 @@ from wbportfolio.models import AssetPosition, TradeProposal
|
|
|
11
11
|
|
|
12
12
|
@pytest.mark.django_db
|
|
13
13
|
class TestRebalancingModel:
|
|
14
|
-
def test_get_target_portfolio(
|
|
14
|
+
def test_get_target_portfolio(
|
|
15
|
+
self, rebalancing_model, portfolio, weekday, asset_position_factory, instrument_price_factory
|
|
16
|
+
):
|
|
17
|
+
trade_date = (weekday + BDay(1)).date()
|
|
15
18
|
with pytest.raises(ValidationError): # trigger value error because rebalancing not valid (no position yet)
|
|
16
|
-
rebalancing_model.get_target_portfolio(portfolio,
|
|
19
|
+
rebalancing_model.get_target_portfolio(portfolio, trade_date, weekday)
|
|
17
20
|
a1 = asset_position_factory(weighting=0.7, portfolio=portfolio, date=weekday)
|
|
18
21
|
a2 = asset_position_factory(weighting=0.3, portfolio=portfolio, date=weekday)
|
|
22
|
+
instrument_price_factory.create(instrument=a1.underlying_quote, date=trade_date)
|
|
19
23
|
target_portfolio = rebalancing_model.get_target_portfolio(portfolio, (weekday + BDay(1)).date(), weekday)
|
|
20
24
|
target_positions = target_portfolio.positions_map
|
|
21
25
|
assert target_positions[a1.underlying_instrument.id].weighting == 0.5
|
|
@@ -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")
|
|
@@ -16,9 +16,11 @@ class TestEquallyWeightedRebalancing:
|
|
|
16
16
|
|
|
17
17
|
return EquallyWeightedRebalancing(portfolio, (weekday + BDay(1)).date(), weekday)
|
|
18
18
|
|
|
19
|
-
def test_is_valid(self, portfolio, weekday, model, asset_position_factory):
|
|
19
|
+
def test_is_valid(self, portfolio, weekday, model, asset_position_factory, instrument_price_factory):
|
|
20
20
|
assert not model.is_valid()
|
|
21
|
-
asset_position_factory.create(portfolio=model.portfolio, date=model.last_effective_date)
|
|
21
|
+
a = asset_position_factory.create(portfolio=model.portfolio, date=model.last_effective_date)
|
|
22
|
+
assert not model.is_valid()
|
|
23
|
+
instrument_price_factory.create(instrument=a.underlying_quote, date=model.trade_date)
|
|
22
24
|
assert model.is_valid()
|
|
23
25
|
|
|
24
26
|
def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
|
|
@@ -38,7 +40,7 @@ class TestModelPortfolioRebalancing:
|
|
|
38
40
|
|
|
39
41
|
return ModelPortfolioRebalancing(portfolio, (weekday + BDay(1)).date(), weekday)
|
|
40
42
|
|
|
41
|
-
def test_is_valid(self, portfolio, weekday, model, asset_position_factory):
|
|
43
|
+
def test_is_valid(self, portfolio, weekday, model, asset_position_factory, instrument_price_factory):
|
|
42
44
|
assert not model.is_valid()
|
|
43
45
|
asset_position_factory.create(portfolio=model.portfolio, date=model.last_effective_date)
|
|
44
46
|
assert not model.is_valid()
|
|
@@ -48,7 +50,9 @@ class TestModelPortfolioRebalancing:
|
|
|
48
50
|
dependency_portfolio=model_portfolio,
|
|
49
51
|
type=PortfolioPortfolioThroughModel.Type.MODEL,
|
|
50
52
|
)
|
|
51
|
-
asset_position_factory.create(portfolio=model.model_portfolio, date=model.last_effective_date)
|
|
53
|
+
a = asset_position_factory.create(portfolio=model.model_portfolio, date=model.last_effective_date)
|
|
54
|
+
assert not model.is_valid()
|
|
55
|
+
instrument_price_factory.create(instrument=a.underlying_quote, date=model.trade_date)
|
|
52
56
|
assert model.is_valid()
|
|
53
57
|
|
|
54
58
|
def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
|
|
@@ -76,15 +80,15 @@ class TestCompositeRebalancing:
|
|
|
76
80
|
|
|
77
81
|
return CompositeRebalancing(portfolio, (weekday + BDay(1)).date(), weekday)
|
|
78
82
|
|
|
79
|
-
def test_is_valid(self, portfolio, weekday, model, asset_position_factory):
|
|
83
|
+
def test_is_valid(self, portfolio, weekday, model, asset_position_factory, instrument_price_factory):
|
|
80
84
|
assert not model.is_valid()
|
|
81
85
|
|
|
82
86
|
trade_proposal = TradeProposalFactory.create(
|
|
83
|
-
portfolio=model.portfolio, trade_date=model.
|
|
87
|
+
portfolio=model.portfolio, trade_date=model.last_effective_date, status=TradeProposal.Status.APPROVED
|
|
84
88
|
)
|
|
85
|
-
TradeFactory.create(
|
|
89
|
+
t1 = TradeFactory.create(
|
|
86
90
|
portfolio=model.portfolio,
|
|
87
|
-
transaction_date=model.
|
|
91
|
+
transaction_date=model.last_effective_date,
|
|
88
92
|
transaction_subtype=Trade.Type.BUY,
|
|
89
93
|
trade_proposal=trade_proposal,
|
|
90
94
|
weighting=0.7,
|
|
@@ -92,23 +96,25 @@ class TestCompositeRebalancing:
|
|
|
92
96
|
)
|
|
93
97
|
TradeFactory.create(
|
|
94
98
|
portfolio=model.portfolio,
|
|
95
|
-
transaction_date=model.
|
|
99
|
+
transaction_date=model.last_effective_date,
|
|
96
100
|
transaction_subtype=Trade.Type.BUY,
|
|
97
101
|
trade_proposal=trade_proposal,
|
|
98
102
|
weighting=0.3,
|
|
99
103
|
status=Trade.Status.EXECUTED,
|
|
100
104
|
)
|
|
105
|
+
assert not model.is_valid()
|
|
106
|
+
instrument_price_factory.create(instrument=t1.underlying_instrument, date=model.trade_date)
|
|
101
107
|
assert model.is_valid()
|
|
102
108
|
|
|
103
109
|
def test_get_target_portfolio(self, portfolio, weekday, model, asset_position_factory):
|
|
104
110
|
trade_proposal = TradeProposalFactory.create(
|
|
105
|
-
portfolio=model.portfolio, trade_date=model.
|
|
111
|
+
portfolio=model.portfolio, trade_date=model.last_effective_date, status=TradeProposal.Status.APPROVED
|
|
106
112
|
)
|
|
107
113
|
asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
|
|
108
114
|
asset_position_factory(portfolio=portfolio, date=model.last_effective_date) # noise
|
|
109
115
|
t1 = TradeFactory.create(
|
|
110
116
|
portfolio=model.portfolio,
|
|
111
|
-
transaction_date=model.
|
|
117
|
+
transaction_date=model.last_effective_date,
|
|
112
118
|
transaction_subtype=Trade.Type.BUY,
|
|
113
119
|
trade_proposal=trade_proposal,
|
|
114
120
|
weighting=0.8,
|
|
@@ -116,7 +122,7 @@ class TestCompositeRebalancing:
|
|
|
116
122
|
)
|
|
117
123
|
t2 = TradeFactory.create(
|
|
118
124
|
portfolio=model.portfolio,
|
|
119
|
-
transaction_date=model.
|
|
125
|
+
transaction_date=model.last_effective_date,
|
|
120
126
|
transaction_subtype=Trade.Type.BUY,
|
|
121
127
|
trade_proposal=trade_proposal,
|
|
122
128
|
weighting=0.2,
|
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
|
|
|
@@ -205,13 +205,11 @@ class TopDownPortfolioCompositionPandasDisplayConfig(DisplayViewConfig):
|
|
|
205
205
|
effective_column_label += f" ({self.view.last_effective_date:%Y-%m-%d})"
|
|
206
206
|
return dp.ListDisplay(
|
|
207
207
|
fields=[
|
|
208
|
-
dp.Field(key="instrument", label="Instrument"),
|
|
208
|
+
dp.Field(key="instrument", label="Instrument", pinned="left"),
|
|
209
209
|
dp.Field(key="rebalancing_weights", label=rebalancing_column_label),
|
|
210
210
|
dp.Field(key="effective_weights", label=effective_column_label),
|
|
211
211
|
],
|
|
212
212
|
tree=True,
|
|
213
|
-
tree_group_pinned="left",
|
|
214
213
|
tree_group_field="instrument",
|
|
215
|
-
tree_group_label="Instrument",
|
|
216
214
|
tree_group_parent_pointer="parent_row_id",
|
|
217
215
|
)
|
|
@@ -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,9 +275,9 @@ 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=
|
|
280
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
278
|
+
wbportfolio/models/transactions/rebalancing.py,sha256=obzgewWKOD4kJbCoF5fhtfDk502QkbrjPKh8T9KDGew,7355
|
|
279
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=a4bxSn6xGuURwsE-WqCJN5gBdwu9TpCf2E8kKn4_RiY,29659
|
|
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
|
|
283
283
|
wbportfolio/pms/typing.py,sha256=b2pBWYt1E8ok-Kqm0lEFIakSnWJ6Ib57z-VX3C3gkQc,6081
|
|
@@ -290,10 +290,10 @@ wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
|
290
290
|
wbportfolio/rebalancing/base.py,sha256=NwTGZtBm1f35gj5Jp6iTyyFvDT1GSIztN990cKBvYzQ,637
|
|
291
291
|
wbportfolio/rebalancing/decorators.py,sha256=JhQ2vkcIuGBTGvNmpkQerdw2-vLq-RAb0KAPjOrETkw,515
|
|
292
292
|
wbportfolio/rebalancing/models/__init__.py,sha256=AQjG7Tu5vlmhqncVoYOjpBKU2UIvgo9FuP2_jD2w-UI,232
|
|
293
|
-
wbportfolio/rebalancing/models/composite.py,sha256=
|
|
294
|
-
wbportfolio/rebalancing/models/equally_weighted.py,sha256=
|
|
293
|
+
wbportfolio/rebalancing/models/composite.py,sha256=XEgK3oMurrE_d_l5uN0stBKRrtvnKQzRWyXNXuBYfmc,1818
|
|
294
|
+
wbportfolio/rebalancing/models/equally_weighted.py,sha256=FCpSKOs49ckNYVgoYIiHB0BqPT9OeCMuFoet4Ixbp-Y,1210
|
|
295
295
|
wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=6ZsR8iJg6l89CsxHqoxJSXlTaj8Pmb8_bFPrXhnxaRs,5295
|
|
296
|
-
wbportfolio/rebalancing/models/model_portfolio.py,sha256=
|
|
296
|
+
wbportfolio/rebalancing/models/model_portfolio.py,sha256=DNg9vEDYDUwXTOnIpk26FQSPHC0qxkuvW2sJWX0VodQ,1489
|
|
297
297
|
wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
298
298
|
wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
|
|
299
299
|
wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -383,13 +383,13 @@ wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xL
|
|
|
383
383
|
wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
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
|
-
wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=
|
|
387
|
-
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=
|
|
386
|
+
wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
|
|
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
|
|
391
391
|
wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
392
|
-
wbportfolio/tests/rebalancing/test_models.py,sha256=
|
|
392
|
+
wbportfolio/tests/rebalancing/test_models.py,sha256=_gT_7UtpOWceDwT7FbTUW6P6ZpCVLBpgXWM0goIljWc,8090
|
|
393
393
|
wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
394
394
|
wbportfolio/tests/serializers/test_claims.py,sha256=vQrg73xQXRFEgvx3KI9ivFre_wpBFzdO0p0J13PkvdY,582
|
|
395
395
|
wbportfolio/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -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
|
|
@@ -444,7 +444,7 @@ wbportfolio/viewsets/configs/display/esg.py,sha256=W8uetCPN2TjHtU2kvQjKOmkq7uoaY
|
|
|
444
444
|
wbportfolio/viewsets/configs/display/fees.py,sha256=_8HDJADh5bA5VMVPkhKObPmNNdPHWR8Oz23PEgd04F0,5834
|
|
445
445
|
wbportfolio/viewsets/configs/display/portfolio_cash_flow.py,sha256=CS_UGhKZSXnX-0SZ0RXzbG1WMJm6NX3GUljwIa4RWBk,5025
|
|
446
446
|
wbportfolio/viewsets/configs/display/portfolio_relationship.py,sha256=8DvsYWFCX29qj5wUf1wCtjlPwzKRd_E7JDuo_CopaJ0,1294
|
|
447
|
-
wbportfolio/viewsets/configs/display/portfolios.py,sha256=
|
|
447
|
+
wbportfolio/viewsets/configs/display/portfolios.py,sha256=FDL0wucfqnyehrPg_vJqZ1JJ_1S37C_TAbON165-07w,10660
|
|
448
448
|
wbportfolio/viewsets/configs/display/positions.py,sha256=yolWLxzGPIpSQSiVhVQChURqbomPt5kSjkYrmXT1Mik,3123
|
|
449
449
|
wbportfolio/viewsets/configs/display/product_groups.py,sha256=PwI-A0_ofShT2pub9-C1HqreiqpHxKMHd51JYwEzvbM,2500
|
|
450
450
|
wbportfolio/viewsets/configs/display/product_performance.py,sha256=6Mme48JBn_okwClR44dBK2OK26ejvdasDvBa5DI33_0,10070
|
|
@@ -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.7.dist-info/METADATA,sha256=0ZEA14Z_uhIVWclnAduNyf2USQ1Oowir18aoAdikcdA,734
|
|
525
|
+
wbportfolio-1.49.7.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
526
|
+
wbportfolio-1.49.7.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
527
|
+
wbportfolio-1.49.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|