wbportfolio 1.46.12__py2.py3-none-any.whl → 1.46.13__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/exceptions.py +2 -0
- wbportfolio/models/portfolio.py +26 -15
- wbportfolio/tests/models/test_portfolios.py +29 -27
- {wbportfolio-1.46.12.dist-info → wbportfolio-1.46.13.dist-info}/METADATA +1 -1
- {wbportfolio-1.46.12.dist-info → wbportfolio-1.46.13.dist-info}/RECORD +7 -6
- {wbportfolio-1.46.12.dist-info → wbportfolio-1.46.13.dist-info}/WHEEL +0 -0
- {wbportfolio-1.46.12.dist-info → wbportfolio-1.46.13.dist-info}/licenses/LICENSE +0 -0
wbportfolio/models/portfolio.py
CHANGED
|
@@ -52,6 +52,7 @@ from wbportfolio.pms.analytics.portfolio import Portfolio as AnalyticPortfolio
|
|
|
52
52
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
53
53
|
|
|
54
54
|
from . import ProductGroup
|
|
55
|
+
from .exceptions import InvalidAnalyticPortfolio
|
|
55
56
|
|
|
56
57
|
logger = logging.getLogger("pms")
|
|
57
58
|
|
|
@@ -373,6 +374,8 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
373
374
|
instrument_ids = weights.keys()
|
|
374
375
|
return_date = (val_date + BDay(1)).date()
|
|
375
376
|
returns, prices = self.get_returns(instrument_ids, (val_date - BDay(2)).date(), return_date, **kwargs)
|
|
377
|
+
if pd.Timestamp(return_date) not in returns.index:
|
|
378
|
+
raise InvalidAnalyticPortfolio()
|
|
376
379
|
returns = returns.fillna(0) # not sure this is what we want
|
|
377
380
|
return (
|
|
378
381
|
AnalyticPortfolio(
|
|
@@ -733,10 +736,9 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
733
736
|
},
|
|
734
737
|
)
|
|
735
738
|
if (
|
|
736
|
-
val_date == instrument.
|
|
739
|
+
val_date == instrument.last_price_date
|
|
737
740
|
): # if price date is the latest instrument price date, we recompute the last valuation data
|
|
738
741
|
instrument.update_last_valuation_date()
|
|
739
|
-
instrument.update_last_valuation_date()
|
|
740
742
|
|
|
741
743
|
def batch_portfolio(self, start_date: date, end_date: date):
|
|
742
744
|
"""
|
|
@@ -758,9 +760,13 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
758
760
|
if rebalancer and rebalancer.is_valid(to_date):
|
|
759
761
|
rebalancing_date = to_date
|
|
760
762
|
break
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
763
|
+
try:
|
|
764
|
+
last_returns = returns.loc[[to_date_ts], :]
|
|
765
|
+
analytic_portfolio = AnalyticPortfolio(weights=weights, X=last_returns)
|
|
766
|
+
weights = analytic_portfolio.get_next_weights()
|
|
767
|
+
positions[to_date] = weights
|
|
768
|
+
except KeyError: # if no return for that date, we break and continue
|
|
769
|
+
break
|
|
764
770
|
|
|
765
771
|
positions_generator = PositionDictConverter(self, prices)
|
|
766
772
|
positions = list(positions_generator.convert(positions))
|
|
@@ -797,14 +803,15 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
797
803
|
self.is_tracked and not self.is_lookthrough and not is_target_portfolio_imported and from_is_active
|
|
798
804
|
): # we cannot propagate a new portfolio for untracked, or look-through or already imported or inactive portfolios
|
|
799
805
|
logger.info(f"computing next weight for {self} from {from_date:%Y-%m-%d} to {to_date:%Y-%m-%d}")
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
806
|
+
with suppress(InvalidAnalyticPortfolio):
|
|
807
|
+
analytic_portfolio, to_prices = self.get_analytic_portfolio(from_date)
|
|
808
|
+
if not to_prices.empty:
|
|
809
|
+
weights = analytic_portfolio.get_next_weights()
|
|
810
|
+
positions_generator = PositionDictConverter(
|
|
811
|
+
self, to_prices, infer_underlying_quote_price=True
|
|
812
|
+
).convert({to_date: weights})
|
|
813
|
+
positions = list(map(lambda a: _parse_position(a), positions_generator))
|
|
814
|
+
self.bulk_create_positions(positions, delete_leftovers=True, compute_metrics=True)
|
|
808
815
|
|
|
809
816
|
def get_lookthrough_positions(
|
|
810
817
|
self,
|
|
@@ -1030,7 +1037,9 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1030
1037
|
.astype(float)
|
|
1031
1038
|
.sort_index()
|
|
1032
1039
|
)
|
|
1033
|
-
|
|
1040
|
+
if prices_df.empty:
|
|
1041
|
+
raise InvalidAnalyticPortfolio()
|
|
1042
|
+
ts = pd.bdate_range(prices_df.index.min(), prices_df.index.max(), freq="B")
|
|
1034
1043
|
prices_df = prices_df.reindex(ts)
|
|
1035
1044
|
if ffill_returns:
|
|
1036
1045
|
prices_df = prices_df.ffill()
|
|
@@ -1225,7 +1234,9 @@ def default_estimate_net_value(val_date: date, instrument: Instrument) -> float
|
|
|
1225
1234
|
last_price := instrument.get_latest_price(previous_val_date)
|
|
1226
1235
|
)
|
|
1227
1236
|
):
|
|
1228
|
-
with suppress(
|
|
1237
|
+
with suppress(
|
|
1238
|
+
IndexError, InvalidAnalyticPortfolio
|
|
1239
|
+
): # we silent any indexerror introduced by no returns for the past days
|
|
1229
1240
|
analytic_portfolio, _ = portfolio.get_analytic_portfolio(previous_val_date)
|
|
1230
1241
|
return analytic_portfolio.get_estimate_net_value(float(last_price.net_value))
|
|
1231
1242
|
|
|
@@ -428,8 +428,10 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
428
428
|
next_day = (weekday + BDay(1)).date()
|
|
429
429
|
|
|
430
430
|
portfolio = active_product.portfolio
|
|
431
|
-
|
|
431
|
+
portfolio.currency = instrument.currency
|
|
432
|
+
portfolio.save()
|
|
432
433
|
|
|
434
|
+
instrument_price_factory.create(date=(weekday - BDay(1)).date(), instrument=instrument)
|
|
433
435
|
a1 = asset_position_factory.create(
|
|
434
436
|
portfolio=portfolio, date=weekday, underlying_instrument=instrument, currency=instrument.currency
|
|
435
437
|
)
|
|
@@ -1016,41 +1018,45 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1016
1018
|
assert a
|
|
1017
1019
|
|
|
1018
1020
|
def test_batch_portfolio_with_rebalancer(
|
|
1019
|
-
self,
|
|
1021
|
+
self,
|
|
1022
|
+
weekday,
|
|
1023
|
+
rebalancer_factory,
|
|
1024
|
+
portfolio,
|
|
1025
|
+
asset_position_factory,
|
|
1026
|
+
instrument_price_factory,
|
|
1027
|
+
instrument_factory,
|
|
1020
1028
|
):
|
|
1021
1029
|
middle_date = (weekday + BDay(1)).date()
|
|
1022
1030
|
rebalancing_date = (middle_date + BDay(1)).date()
|
|
1023
1031
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
instrument_price_factory.create(instrument=
|
|
1030
|
-
instrument_price_factory.create(instrument=
|
|
1031
|
-
instrument_price_factory.create(instrument=
|
|
1032
|
-
instrument_price_factory.create(instrument=
|
|
1033
|
-
instrument_price_factory.create(instrument=
|
|
1034
|
-
instrument_price_factory.create(instrument=
|
|
1032
|
+
i1 = instrument_factory.create(currency=portfolio.currency)
|
|
1033
|
+
i2 = instrument_factory.create(currency=portfolio.currency)
|
|
1034
|
+
asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i1, weighting=0.7)
|
|
1035
|
+
asset_position_factory.create(date=weekday, portfolio=portfolio, underlying_instrument=i2, weighting=0.3)
|
|
1036
|
+
|
|
1037
|
+
instrument_price_factory.create(instrument=i1, date=(weekday - BDay(1)).date())
|
|
1038
|
+
instrument_price_factory.create(instrument=i1, date=weekday)
|
|
1039
|
+
instrument_price_factory.create(instrument=i1, date=middle_date)
|
|
1040
|
+
instrument_price_factory.create(instrument=i1, date=rebalancing_date)
|
|
1041
|
+
instrument_price_factory.create(instrument=i2, date=(weekday - BDay(1)).date())
|
|
1042
|
+
instrument_price_factory.create(instrument=i2, date=weekday)
|
|
1043
|
+
instrument_price_factory.create(instrument=i2, date=middle_date)
|
|
1044
|
+
instrument_price_factory.create(instrument=i2, date=rebalancing_date)
|
|
1035
1045
|
|
|
1036
1046
|
rebalancer_factory.create(portfolio=portfolio, frequency="RRULE:FREQ=DAILY;", activation_date=rebalancing_date)
|
|
1037
1047
|
rebalancing_trade_proposal = portfolio.batch_portfolio(weekday, rebalancing_date)
|
|
1038
1048
|
|
|
1039
1049
|
# check that the position before the rebalancing date were created
|
|
1040
|
-
assert portfolio.assets.get(date=middle_date, underlying_instrument=
|
|
1041
|
-
assert portfolio.assets.get(date=middle_date, underlying_instrument=
|
|
1050
|
+
assert portfolio.assets.get(date=middle_date, underlying_instrument=i1)
|
|
1051
|
+
assert portfolio.assets.get(date=middle_date, underlying_instrument=i2)
|
|
1042
1052
|
with pytest.raises(
|
|
1043
1053
|
AssetPosition.DoesNotExist
|
|
1044
1054
|
): # there is no asset position because the rebalancing stopped it:
|
|
1045
1055
|
portfolio.assets.get(date=rebalancing_date)
|
|
1046
1056
|
|
|
1047
1057
|
# we expect a equally rebalancing (default) so both trades needs to be created
|
|
1048
|
-
t1 = rebalancing_trade_proposal.trades.get(
|
|
1049
|
-
|
|
1050
|
-
)
|
|
1051
|
-
t2 = rebalancing_trade_proposal.trades.get(
|
|
1052
|
-
transaction_date=rebalancing_date, underlying_instrument=a2.underlying_instrument
|
|
1053
|
-
)
|
|
1058
|
+
t1 = rebalancing_trade_proposal.trades.get(transaction_date=rebalancing_date, underlying_instrument=i1)
|
|
1059
|
+
t2 = rebalancing_trade_proposal.trades.get(transaction_date=rebalancing_date, underlying_instrument=i2)
|
|
1054
1060
|
assert t1._target_weight == Decimal("0.5")
|
|
1055
1061
|
assert t2._target_weight == Decimal("0.5")
|
|
1056
1062
|
|
|
@@ -1060,12 +1066,8 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1060
1066
|
rebalancing_trade_proposal.save()
|
|
1061
1067
|
|
|
1062
1068
|
# check that the rebalancing was applied and position reflect that
|
|
1063
|
-
assert portfolio.assets.get(
|
|
1064
|
-
|
|
1065
|
-
).weighting == Decimal("0.5")
|
|
1066
|
-
assert portfolio.assets.get(
|
|
1067
|
-
date=rebalancing_date, underlying_instrument=a2.underlying_instrument
|
|
1068
|
-
).weighting == Decimal("0.5")
|
|
1069
|
+
assert portfolio.assets.get(date=rebalancing_date, underlying_instrument=i1).weighting == Decimal("0.5")
|
|
1070
|
+
assert portfolio.assets.get(date=rebalancing_date, underlying_instrument=i2).weighting == Decimal("0.5")
|
|
1069
1071
|
|
|
1070
1072
|
def test_bulk_create_positions(self, portfolio, weekday, asset_position_factory, instrument_factory):
|
|
1071
1073
|
portfolio.is_manageable = False
|
|
@@ -245,8 +245,9 @@ wbportfolio/models/__init__.py,sha256=PDLJry5w1zE4N4arQh20_uFi2v7gy9QyavJ_rfGE21
|
|
|
245
245
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
246
246
|
wbportfolio/models/asset.py,sha256=TEOsSlbog6Yw0PZiMxjnCccp6ApXAAJNd3BIx_wU7D8,37749
|
|
247
247
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
248
|
+
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
248
249
|
wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
|
|
249
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
250
|
+
wbportfolio/models/portfolio.py,sha256=oW1wUCT8wgXRamhaqjnPjiU0eYC3dbPWbR63OMAQmFQ,57994
|
|
250
251
|
wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
|
|
251
252
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
252
253
|
wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
|
|
@@ -371,7 +372,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
|
|
|
371
372
|
wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
|
|
372
373
|
wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
|
|
373
374
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
374
|
-
wbportfolio/tests/models/test_portfolios.py,sha256=
|
|
375
|
+
wbportfolio/tests/models/test_portfolios.py,sha256=ZXFqvCI2ozJmVTPmhtAEVaBaTz2BW73re1afsmKXJNc,52003
|
|
375
376
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
376
377
|
wbportfolio/tests/models/test_products.py,sha256=5YYmQreFnaKLbWmrSib103wgLalqn8u01Fnh3A0XMz8,8217
|
|
377
378
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
@@ -517,7 +518,7 @@ wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQV
|
|
|
517
518
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=gXdJJ00D6UavZfBczvZQb5cYPQlU6-xOg_-TnMBzEO0,4742
|
|
518
519
|
wbportfolio/viewsets/transactions/trades.py,sha256=mo5b1wFm0twvGVp-CYnzpGLYMqPcHN8GjH4G_WwFFwc,16237
|
|
519
520
|
wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
|
|
520
|
-
wbportfolio-1.46.
|
|
521
|
-
wbportfolio-1.46.
|
|
522
|
-
wbportfolio-1.46.
|
|
523
|
-
wbportfolio-1.46.
|
|
521
|
+
wbportfolio-1.46.13.dist-info/METADATA,sha256=Z2xctYKKSO27XMnm51D_LmzDTxkXe2MfH0jsjzeu150,735
|
|
522
|
+
wbportfolio-1.46.13.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
523
|
+
wbportfolio-1.46.13.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
524
|
+
wbportfolio-1.46.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|