wbportfolio 1.46.14__py2.py3-none-any.whl → 1.47.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbportfolio might be problematic. Click here for more details.
- wbportfolio/models/portfolio.py +34 -28
- wbportfolio/tests/models/test_portfolios.py +6 -6
- wbportfolio/viewsets/configs/menu/portfolios.py +2 -4
- wbportfolio/viewsets/transactions/trades.py +1 -1
- {wbportfolio-1.46.14.dist-info → wbportfolio-1.47.0.dist-info}/METADATA +1 -1
- {wbportfolio-1.46.14.dist-info → wbportfolio-1.47.0.dist-info}/RECORD +8 -8
- {wbportfolio-1.46.14.dist-info → wbportfolio-1.47.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.46.14.dist-info → wbportfolio-1.47.0.dist-info}/licenses/LICENSE +0 -0
wbportfolio/models/portfolio.py
CHANGED
|
@@ -85,33 +85,38 @@ class PositionDictConverter:
|
|
|
85
85
|
self.fx_rates[currency][val_date] = fx_rate
|
|
86
86
|
return fx_rate
|
|
87
87
|
|
|
88
|
-
def dict_to_model(self, val_date: date, instrument_id: int, weighting: float) -> AssetPosition:
|
|
88
|
+
def dict_to_model(self, val_date: date, instrument_id: int, weighting: float) -> AssetPosition | None:
|
|
89
89
|
underlying_quote = self._get_instrument(instrument_id)
|
|
90
90
|
currency_fx_rate_portfolio_to_usd = self._get_fx_rate(val_date, self.portfolio.currency)
|
|
91
91
|
currency_fx_rate_instrument_to_usd = self._get_fx_rate(val_date, underlying_quote.currency)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
92
|
+
initial_price = self.prices.loc[pd.Timestamp(val_date), instrument_id]
|
|
93
|
+
# if we cannot find a price for the instrument, we return an empty position
|
|
94
|
+
if initial_price is not None:
|
|
95
|
+
position = AssetPosition(
|
|
96
|
+
underlying_quote=underlying_quote,
|
|
97
|
+
weighting=weighting,
|
|
98
|
+
date=val_date,
|
|
99
|
+
asset_valuation_date=val_date,
|
|
100
|
+
is_estimated=True,
|
|
101
|
+
portfolio=self.portfolio,
|
|
102
|
+
currency=underlying_quote.currency,
|
|
103
|
+
initial_price=self.prices.loc[pd.Timestamp(val_date), instrument_id],
|
|
104
|
+
currency_fx_rate_portfolio_to_usd=currency_fx_rate_portfolio_to_usd,
|
|
105
|
+
currency_fx_rate_instrument_to_usd=currency_fx_rate_instrument_to_usd,
|
|
106
|
+
initial_currency_fx_rate=None,
|
|
107
|
+
underlying_quote_price=None,
|
|
108
|
+
underlying_instrument=None,
|
|
109
|
+
)
|
|
110
|
+
position.pre_save(
|
|
111
|
+
infer_underlying_quote_price=self.infer_underlying_quote_price
|
|
112
|
+
) # inferring underlying quote price is potentially very slow for big dataset of positions, it's not very needed for model portfolio so we disable it
|
|
113
|
+
return position
|
|
111
114
|
|
|
112
115
|
def convert(self, positions: dict[date, dict[int, float]]) -> Iterable[AssetPosition]:
|
|
113
116
|
for val_date, weights in positions.items():
|
|
114
|
-
yield from
|
|
117
|
+
yield from filter(
|
|
118
|
+
lambda x: x, map(lambda row: self.dict_to_model(val_date, row[0], row[1]), weights.items())
|
|
119
|
+
)
|
|
115
120
|
|
|
116
121
|
|
|
117
122
|
class DefaultPortfolioQueryset(QuerySet):
|
|
@@ -371,9 +376,8 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
371
376
|
The instantiated analytic portfolio
|
|
372
377
|
"""
|
|
373
378
|
weights = self.get_weights(val_date)
|
|
374
|
-
instrument_ids = weights.keys()
|
|
375
379
|
return_date = (val_date + BDay(1)).date()
|
|
376
|
-
returns, prices = self.get_returns(
|
|
380
|
+
returns, prices = self.get_returns((val_date - BDay(2)).date(), return_date, **kwargs)
|
|
377
381
|
if pd.Timestamp(return_date) not in returns.index:
|
|
378
382
|
raise InvalidAnalyticPortfolio()
|
|
379
383
|
returns = returns.fillna(0) # not sure this is what we want
|
|
@@ -753,7 +757,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
753
757
|
next_trade_proposal = None
|
|
754
758
|
rebalancing_date = None
|
|
755
759
|
logger.info(f"compute next weights in batch for {self} from {start_date:%Y-%m-%d} to {end_date:%Y-%m-%d}")
|
|
756
|
-
returns, prices = self.get_returns(
|
|
760
|
+
returns, prices = self.get_returns((start_date - BDay(3)).date(), end_date, ffill_returns=True)
|
|
757
761
|
for to_date_ts in pd.date_range(start_date + timedelta(days=1), end_date, freq="B"):
|
|
758
762
|
to_date = to_date_ts.date()
|
|
759
763
|
logger.info(f"Processing {to_date:%Y-%m-%d}")
|
|
@@ -767,7 +771,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
767
771
|
positions[to_date] = weights
|
|
768
772
|
except KeyError: # if no return for that date, we break and continue
|
|
769
773
|
break
|
|
770
|
-
|
|
771
774
|
positions_generator = PositionDictConverter(self, prices)
|
|
772
775
|
positions = list(positions_generator.convert(positions))
|
|
773
776
|
self.bulk_create_positions(positions, delete_leftovers=True, compute_metrics=False, evaluate_rebalancer=False)
|
|
@@ -1003,7 +1006,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1003
1006
|
|
|
1004
1007
|
def get_returns(
|
|
1005
1008
|
self,
|
|
1006
|
-
instruments: Iterable,
|
|
1007
1009
|
from_date: date,
|
|
1008
1010
|
to_date: date,
|
|
1009
1011
|
ffill_returns: bool = True,
|
|
@@ -1013,13 +1015,17 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1013
1015
|
Utility methods to get instrument returns for a given date range
|
|
1014
1016
|
|
|
1015
1017
|
Args:
|
|
1016
|
-
instruments: instrument to get the returns from
|
|
1017
1018
|
from_date: date range lower bound
|
|
1018
1019
|
to_date: date range upper bound
|
|
1019
1020
|
|
|
1020
1021
|
Returns:
|
|
1021
1022
|
Return a tuple of the returns and the last prices series for conveniance
|
|
1022
1023
|
"""
|
|
1024
|
+
instruments = list(
|
|
1025
|
+
self.assets.filter(date__gte=from_date, date__lte=to_date)
|
|
1026
|
+
.values_list("underlying_quote", flat=True)
|
|
1027
|
+
.distinct("underlying_quote")
|
|
1028
|
+
)
|
|
1023
1029
|
prices = InstrumentPrice.objects.filter(
|
|
1024
1030
|
instrument__in=instruments, date__gte=from_date, date__lte=to_date
|
|
1025
1031
|
).annotate(
|
|
@@ -1044,7 +1050,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1044
1050
|
if ffill_returns:
|
|
1045
1051
|
prices_df = prices_df.ffill()
|
|
1046
1052
|
returns = prices_to_returns(prices_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
|
|
1047
|
-
return returns.replace([np.inf, -np.inf, np.nan], 0), prices_df
|
|
1053
|
+
return returns.replace([np.inf, -np.inf, np.nan], 0), prices_df.replace([np.inf, -np.inf, np.nan], None)
|
|
1048
1054
|
|
|
1049
1055
|
@classmethod
|
|
1050
1056
|
def get_contribution_df(
|
|
@@ -1118,7 +1118,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1118
1118
|
res = list(Portfolio.objects.all().to_dependency_iterator(weekday))
|
|
1119
1119
|
assert res == [index_portfolio, dependency_portfolio, dependant_portfolio, undependant_portfolio]
|
|
1120
1120
|
|
|
1121
|
-
def test_get_returns(self, instrument_factory, instrument_price_factory, portfolio):
|
|
1121
|
+
def test_get_returns(self, instrument_factory, instrument_price_factory, asset_position_factory, portfolio):
|
|
1122
1122
|
v1 = date(2025, 1, 1)
|
|
1123
1123
|
v2 = date(2025, 1, 2)
|
|
1124
1124
|
v3 = date(2025, 1, 3)
|
|
@@ -1129,17 +1129,17 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1129
1129
|
i11 = instrument_price_factory.create(date=v1, instrument=i1)
|
|
1130
1130
|
i12 = instrument_price_factory.create(date=v2, instrument=i1)
|
|
1131
1131
|
i13 = instrument_price_factory.create(date=v3, instrument=i1)
|
|
1132
|
+
asset_position_factory.create(date=v1, portfolio=portfolio, underlying_instrument=i1)
|
|
1133
|
+
asset_position_factory.create(date=v3, portfolio=portfolio, underlying_instrument=i2)
|
|
1132
1134
|
i11.refresh_from_db()
|
|
1133
1135
|
i12.refresh_from_db()
|
|
1134
1136
|
i13.refresh_from_db()
|
|
1135
|
-
returns, _ = portfolio.get_returns(
|
|
1136
|
-
[i1.id, i2.id], from_date=v1, to_date=v3, convert_to_portfolio_currency=False
|
|
1137
|
-
)
|
|
1137
|
+
returns, _ = portfolio.get_returns(from_date=v1, to_date=v3, convert_to_portfolio_currency=False)
|
|
1138
1138
|
|
|
1139
1139
|
expected_returns = pd.DataFrame(
|
|
1140
|
-
[[i12.net_value / i11.net_value - 1], [i13.net_value / i12.net_value - 1]],
|
|
1140
|
+
[[i12.net_value / i11.net_value - 1, 0.0], [i13.net_value / i12.net_value - 1, 0.0]],
|
|
1141
1141
|
index=[v2, v3],
|
|
1142
|
-
columns=[i1.id],
|
|
1142
|
+
columns=[i1.id, i2.id],
|
|
1143
1143
|
dtype="float64",
|
|
1144
1144
|
)
|
|
1145
1145
|
expected_returns.index = pd.to_datetime(expected_returns.index)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from wbcore.menus import ItemPermission, MenuItem
|
|
2
2
|
|
|
3
|
-
from wbportfolio.permissions import is_portfolio_manager
|
|
4
|
-
|
|
5
3
|
PortfolioMenuItem = MenuItem(
|
|
6
4
|
label="Portfolios",
|
|
7
5
|
endpoint="wbportfolio:portfolio-list",
|
|
8
|
-
permission=ItemPermission(
|
|
6
|
+
permission=ItemPermission(permissions=["wbportfolio.view_portfolio"]),
|
|
9
7
|
)
|
|
10
8
|
|
|
11
9
|
|
|
@@ -13,5 +11,5 @@ ModelPortfolioMenuItem = MenuItem(
|
|
|
13
11
|
label="Managed Portfolios",
|
|
14
12
|
endpoint="wbportfolio:portfolio-list",
|
|
15
13
|
endpoint_get_parameters={"is_manageable": True},
|
|
16
|
-
permission=ItemPermission(
|
|
14
|
+
permission=ItemPermission(permissions=["wbportfolio.view_portfolio"]),
|
|
17
15
|
)
|
|
@@ -411,7 +411,7 @@ class TradeTradeProposalModelViewSet(
|
|
|
411
411
|
return TradeTradeProposalModelSerializer
|
|
412
412
|
|
|
413
413
|
def add_messages(self, request, queryset=None, paginated_queryset=None, instance=None, initial=False):
|
|
414
|
-
if queryset is not None:
|
|
414
|
+
if queryset is not None and queryset.exists():
|
|
415
415
|
total_target_weight = queryset.aggregate(c=Sum(F("target_weight")))["c"] or Decimal(0)
|
|
416
416
|
if round(total_target_weight, 3) != 1:
|
|
417
417
|
warning(request, "The total target weight does not equals to 1")
|
|
@@ -247,7 +247,7 @@ wbportfolio/models/asset.py,sha256=TEOsSlbog6Yw0PZiMxjnCccp6ApXAAJNd3BIx_wU7D8,3
|
|
|
247
247
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
248
248
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
249
249
|
wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
|
|
250
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
250
|
+
wbportfolio/models/portfolio.py,sha256=rpo5jUTbwrlD_Zx9_LYTy5XNgk73PTRX3yn7j8AG9DE,58447
|
|
251
251
|
wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
|
|
252
252
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
253
253
|
wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
|
|
@@ -372,7 +372,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
|
|
|
372
372
|
wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
|
|
373
373
|
wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
|
|
374
374
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
375
|
-
wbportfolio/tests/models/test_portfolios.py,sha256=
|
|
375
|
+
wbportfolio/tests/models/test_portfolios.py,sha256=idIUT2kc6Fte-bzwOS9NtOYRVnwTBijSo7deHkRon_k,52194
|
|
376
376
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
377
377
|
wbportfolio/tests/models/test_products.py,sha256=5YYmQreFnaKLbWmrSib103wgLalqn8u01Fnh3A0XMz8,8217
|
|
378
378
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
@@ -480,7 +480,7 @@ wbportfolio/viewsets/configs/menu/custodians.py,sha256=W01URxeqDzcu--5R51QCBzbhS
|
|
|
480
480
|
wbportfolio/viewsets/configs/menu/fees.py,sha256=iYAMzavfTKSYo6CBzVrSggxl7kKwE1h_jHGCgV4pYm8,499
|
|
481
481
|
wbportfolio/viewsets/configs/menu/instrument_prices.py,sha256=9bYMHq7AoPnaqdZ_6w3sEpk4X8Ptr_RnTlwxoE8lVow,358
|
|
482
482
|
wbportfolio/viewsets/configs/menu/portfolio_cash_flow.py,sha256=JqTtceY6cH0fldMMT4AbEtrG855LWSJn4SG45-2-xec,343
|
|
483
|
-
wbportfolio/viewsets/configs/menu/portfolios.py,sha256=
|
|
483
|
+
wbportfolio/viewsets/configs/menu/portfolios.py,sha256=ezR5a1gwAbjsdNV9YsfZ-c1f_oRFHpFhRQlU2x-VPV8,467
|
|
484
484
|
wbportfolio/viewsets/configs/menu/positions.py,sha256=ejq6ZMcaYptogh8UMX-jwRdxXi8I2MsCcanYJanxesI,608
|
|
485
485
|
wbportfolio/viewsets/configs/menu/product_groups.py,sha256=c8DK7nW81NCMCdktLyiFmWlIJ7M0q15D_vfrDaREM4U,367
|
|
486
486
|
wbportfolio/viewsets/configs/menu/product_performance.py,sha256=aZ72NGMVuRmARwUB4Z3fGtk15_2RVzw9DzW-rYUbc6k,947
|
|
@@ -516,9 +516,9 @@ wbportfolio/viewsets/transactions/fees.py,sha256=7VUXIogmRrXCz_D9tvDiiTae0t5j09W
|
|
|
516
516
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
517
517
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
518
518
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=gXdJJ00D6UavZfBczvZQb5cYPQlU6-xOg_-TnMBzEO0,4742
|
|
519
|
-
wbportfolio/viewsets/transactions/trades.py,sha256=
|
|
519
|
+
wbportfolio/viewsets/transactions/trades.py,sha256=E0QV6oPohOgeVWzNT6xfWOFvTB4KoP2DzDYnGtm3Hz0,16259
|
|
520
520
|
wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
|
|
521
|
-
wbportfolio-1.
|
|
522
|
-
wbportfolio-1.
|
|
523
|
-
wbportfolio-1.
|
|
524
|
-
wbportfolio-1.
|
|
521
|
+
wbportfolio-1.47.0.dist-info/METADATA,sha256=1sOB9SYCewvb2rY4td8il1iLcdd7bOgB6esL12IbkWI,734
|
|
522
|
+
wbportfolio-1.47.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
523
|
+
wbportfolio-1.47.0.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
524
|
+
wbportfolio-1.47.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|