wbportfolio 1.54.11__py2.py3-none-any.whl → 1.54.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/portfolio.py +14 -21
- wbportfolio/models/transactions/trade_proposals.py +1 -1
- wbportfolio/tests/models/test_portfolios.py +41 -0
- {wbportfolio-1.54.11.dist-info → wbportfolio-1.54.13.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.11.dist-info → wbportfolio-1.54.13.dist-info}/RECORD +7 -7
- {wbportfolio-1.54.11.dist-info → wbportfolio-1.54.13.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.11.dist-info → wbportfolio-1.54.13.dist-info}/licenses/LICENSE +0 -0
wbportfolio/models/portfolio.py
CHANGED
|
@@ -11,15 +11,7 @@ from celery import shared_task
|
|
|
11
11
|
from django.contrib.contenttypes.models import ContentType
|
|
12
12
|
from django.contrib.postgres.fields import DateRangeField
|
|
13
13
|
from django.db import models
|
|
14
|
-
from django.db.models import
|
|
15
|
-
Exists,
|
|
16
|
-
F,
|
|
17
|
-
OuterRef,
|
|
18
|
-
Q,
|
|
19
|
-
QuerySet,
|
|
20
|
-
Sum,
|
|
21
|
-
Value,
|
|
22
|
-
)
|
|
14
|
+
from django.db.models import Case, Exists, F, OuterRef, Q, QuerySet, Sum, Value, When
|
|
23
15
|
from django.db.models.functions import Coalesce
|
|
24
16
|
from django.db.models.signals import post_save
|
|
25
17
|
from django.dispatch import receiver
|
|
@@ -64,7 +56,6 @@ def get_returns(
|
|
|
64
56
|
from_date: date,
|
|
65
57
|
to_date: date,
|
|
66
58
|
to_currency: Currency | None = None,
|
|
67
|
-
ffill_returns: bool = True,
|
|
68
59
|
use_dl: bool = False,
|
|
69
60
|
) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
|
|
70
61
|
"""
|
|
@@ -74,7 +65,6 @@ def get_returns(
|
|
|
74
65
|
from_date: date range lower bound
|
|
75
66
|
to_date: date range upper bound
|
|
76
67
|
to_currency: currency to use for returns
|
|
77
|
-
ffill_returns: whether to ffill returns and prices
|
|
78
68
|
use_dl: whether to get data straight from the dataloader or use the internal table
|
|
79
69
|
|
|
80
70
|
Returns:
|
|
@@ -96,14 +86,14 @@ def get_returns(
|
|
|
96
86
|
)
|
|
97
87
|
else:
|
|
98
88
|
fx_rate = Value(Decimal("1"))
|
|
89
|
+
# annotate fx rate only if the price is not calculated, in that case we assume the instrument is not tradable and we set a forex of None (to be fast forward filled)
|
|
99
90
|
prices = InstrumentPrice.objects.filter(
|
|
100
91
|
instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
|
|
101
|
-
).annotate(fx_rate=fx_rate)
|
|
92
|
+
).annotate(fx_rate=Case(When(calculated=False, then=fx_rate), default=None))
|
|
102
93
|
df = pd.DataFrame(
|
|
103
94
|
prices.filter_only_valid_prices().values_list("instrument", "fx_rate", "net_value", "date"),
|
|
104
95
|
columns=["instrument_id", "fx_rate", "close", "valuation_date"],
|
|
105
96
|
)
|
|
106
|
-
|
|
107
97
|
if df.empty:
|
|
108
98
|
raise InvalidAnalyticPortfolio()
|
|
109
99
|
df = (
|
|
@@ -113,12 +103,15 @@ def get_returns(
|
|
|
113
103
|
)
|
|
114
104
|
ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
|
|
115
105
|
df = df.reindex(ts)
|
|
116
|
-
|
|
117
|
-
df = df.ffill()
|
|
106
|
+
df = df.ffill()
|
|
118
107
|
df.index = pd.to_datetime(df.index)
|
|
119
108
|
prices_df = df["close"]
|
|
120
|
-
|
|
121
|
-
|
|
109
|
+
if "fx_rate" in df.columns:
|
|
110
|
+
fx_rate_df = df["fx_rate"].fillna(1.0)
|
|
111
|
+
else:
|
|
112
|
+
fx_rate_df = pd.DataFrame(np.ones(prices_df.shape), index=prices_df.index, columns=prices_df.columns)
|
|
113
|
+
|
|
114
|
+
returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=True)
|
|
122
115
|
return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
|
|
123
116
|
[np.inf, -np.inf, np.nan], 0
|
|
124
117
|
)
|
|
@@ -358,11 +351,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
358
351
|
instrument.delisted_date = date.today() - timedelta(days=1)
|
|
359
352
|
instrument.save()
|
|
360
353
|
|
|
361
|
-
def _build_dto(self, val_date: date,
|
|
354
|
+
def _build_dto(self, val_date: date, **extra_kwargs) -> PortfolioDTO:
|
|
362
355
|
"returns the dto representation of this portfolio at the specified date"
|
|
363
356
|
assets = self.assets.filter(date=val_date, **extra_kwargs)
|
|
364
357
|
try:
|
|
365
|
-
drifted_weights = self.get_analytic_portfolio(val_date).get_next_weights()
|
|
358
|
+
drifted_weights = self.get_analytic_portfolio(val_date, use_dl=True).get_next_weights()
|
|
366
359
|
except InvalidAnalyticPortfolio:
|
|
367
360
|
drifted_weights = {}
|
|
368
361
|
positions = []
|
|
@@ -397,7 +390,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
397
390
|
)
|
|
398
391
|
|
|
399
392
|
def get_analytic_portfolio(
|
|
400
|
-
self, val_date: date, weights: dict[int, float] | None = None, **kwargs
|
|
393
|
+
self, val_date: date, weights: dict[int, float] | None = None, use_dl: bool = False, **kwargs
|
|
401
394
|
) -> AnalyticPortfolio:
|
|
402
395
|
"""
|
|
403
396
|
Return the analytic portfolio associated with this portfolio at the given date
|
|
@@ -417,6 +410,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
417
410
|
(val_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
|
|
418
411
|
return_date,
|
|
419
412
|
to_currency=self.currency,
|
|
413
|
+
use_dl=use_dl,
|
|
420
414
|
**kwargs,
|
|
421
415
|
)
|
|
422
416
|
if pd.Timestamp(return_date) not in returns.index:
|
|
@@ -842,7 +836,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
842
836
|
(start_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
|
|
843
837
|
end_date,
|
|
844
838
|
to_currency=self.currency,
|
|
845
|
-
ffill_returns=True,
|
|
846
839
|
use_dl=True,
|
|
847
840
|
)
|
|
848
841
|
# Get raw prices to speed up asset position creation
|
|
@@ -196,7 +196,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
196
196
|
previous_weights = dict(map(lambda r: (r[0].id, float(r[1]["weighting"])), portfolio.items()))
|
|
197
197
|
try:
|
|
198
198
|
drifted_weights = self.portfolio.get_analytic_portfolio(
|
|
199
|
-
self.value_date, weights=previous_weights
|
|
199
|
+
self.value_date, weights=previous_weights, use_dl=True
|
|
200
200
|
).get_next_weights()
|
|
201
201
|
except InvalidAnalyticPortfolio:
|
|
202
202
|
drifted_weights = {}
|
|
@@ -1169,6 +1169,47 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
1169
1169
|
assert prices[v3][i2.id] == float(i21.net_value)
|
|
1170
1170
|
assert prices[v4][i2.id] == float(i21.net_value)
|
|
1171
1171
|
|
|
1172
|
+
def test_get_returns_fix_forex_on_holiday(
|
|
1173
|
+
self, instrument, instrument_price_factory, currency_fx_rates_factory, currency_factory
|
|
1174
|
+
):
|
|
1175
|
+
v1 = date(2024, 12, 31)
|
|
1176
|
+
v2 = date(2025, 1, 1)
|
|
1177
|
+
v3 = date(2025, 1, 2)
|
|
1178
|
+
|
|
1179
|
+
target_currency = currency_factory.create()
|
|
1180
|
+
fx_target1 = currency_fx_rates_factory.create(currency=target_currency, date=v1)
|
|
1181
|
+
fx_target2 = currency_fx_rates_factory.create(currency=target_currency, date=v2) # noqa
|
|
1182
|
+
fx_target3 = currency_fx_rates_factory.create(currency=target_currency, date=v3)
|
|
1183
|
+
|
|
1184
|
+
fx1 = currency_fx_rates_factory.create(currency=instrument.currency, date=v1)
|
|
1185
|
+
fx2 = currency_fx_rates_factory.create(currency=instrument.currency, date=v2) # noqa
|
|
1186
|
+
fx3 = currency_fx_rates_factory.create(currency=instrument.currency, date=v3)
|
|
1187
|
+
|
|
1188
|
+
i1 = instrument_price_factory.create(net_value=Decimal("100"), date=v1, instrument=instrument)
|
|
1189
|
+
i2 = instrument_price_factory.create(net_value=Decimal("100"), date=v2, instrument=instrument, calculated=True)
|
|
1190
|
+
i3 = instrument_price_factory.create(net_value=Decimal("200"), date=v3, instrument=instrument)
|
|
1191
|
+
|
|
1192
|
+
prices, returns = get_returns([instrument.id], from_date=v1, to_date=v3, to_currency=target_currency)
|
|
1193
|
+
returns.index = pd.to_datetime(returns.index)
|
|
1194
|
+
assert prices[v1][instrument.id] == float(i1.net_value)
|
|
1195
|
+
assert prices[v2][instrument.id] == float(i2.net_value)
|
|
1196
|
+
assert prices[v3][instrument.id] == float(i3.net_value)
|
|
1197
|
+
|
|
1198
|
+
assert returns.loc[pd.Timestamp(v2), instrument.id] == pytest.approx(
|
|
1199
|
+
float(
|
|
1200
|
+
(i2.net_value * fx_target1.value / fx1.value) / (i1.net_value * fx_target1.value / fx1.value)
|
|
1201
|
+
- Decimal("1")
|
|
1202
|
+
),
|
|
1203
|
+
abs=10e-8,
|
|
1204
|
+
) # as v2 as a calculated price, the forex won't apply to it
|
|
1205
|
+
assert returns.loc[pd.Timestamp(v3), instrument.id] == pytest.approx(
|
|
1206
|
+
float(
|
|
1207
|
+
(i3.net_value * fx_target3.value / fx3.value) / (i2.net_value * fx_target1.value / fx1.value)
|
|
1208
|
+
- Decimal("1")
|
|
1209
|
+
),
|
|
1210
|
+
abs=10e-8,
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1172
1213
|
@patch.object(Portfolio, "compute_lookthrough", autospec=True)
|
|
1173
1214
|
def test_handle_controlling_portfolio_change_at_date(self, mock_compute_lookthrough, weekday, portfolio_factory):
|
|
1174
1215
|
primary_portfolio = portfolio_factory.create(only_weighting=True)
|
|
@@ -258,7 +258,7 @@ wbportfolio/models/asset.py,sha256=b0vPt4LwNrxcMiK7UmBKViYnbNNlZzPTagvU5vFuyrc,4
|
|
|
258
258
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
259
259
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
260
260
|
wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
|
|
261
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
261
|
+
wbportfolio/models/portfolio.py,sha256=5CddquPvVp-ImPlteNMqwDeY_mCFwdWMy7ynMRDDxak,58578
|
|
262
262
|
wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
|
|
263
263
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
264
264
|
wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
|
|
@@ -285,7 +285,7 @@ wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWK
|
|
|
285
285
|
wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
|
|
286
286
|
wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
|
|
287
287
|
wbportfolio/models/transactions/rebalancing.py,sha256=rwePcmTZOYgfSWnBQcBrZ3DQHRJ3w17hdO_hgrRbbhI,7696
|
|
288
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
288
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=1xOxHVOUwgTKCcY7mN4u5SoFtdJrQkNg6xA3O2-f4Yw,38340
|
|
289
289
|
wbportfolio/models/transactions/trades.py,sha256=1gmAdavuWu1Iko90s9prMxsK_NuDKIUBIKMDuHiKzow,34176
|
|
290
290
|
wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
|
|
291
291
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -381,7 +381,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
|
|
|
381
381
|
wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
|
|
382
382
|
wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
|
|
383
383
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
384
|
-
wbportfolio/tests/models/test_portfolios.py,sha256=
|
|
384
|
+
wbportfolio/tests/models/test_portfolios.py,sha256=H3mgrQLdTkrljFZgJLRTXuN6J8fasRtlYoaRCnirdwU,56165
|
|
385
385
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
386
386
|
wbportfolio/tests/models/test_products.py,sha256=IcBzw9hrGiWFMRwPBTMukCMWrhqnjOVA2hhb90xYOW8,9580
|
|
387
387
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
@@ -523,7 +523,7 @@ wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIH
|
|
|
523
523
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
524
524
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=kQCojTNKBEyn2NcenL3a9auzBH4sIgLEx8rLAYCGLGg,6161
|
|
525
525
|
wbportfolio/viewsets/transactions/trades.py,sha256=Y8v2cM0vpspHysaAvu8qqhzt86dNtb2Q3puo4HCJsTI,22629
|
|
526
|
-
wbportfolio-1.54.
|
|
527
|
-
wbportfolio-1.54.
|
|
528
|
-
wbportfolio-1.54.
|
|
529
|
-
wbportfolio-1.54.
|
|
526
|
+
wbportfolio-1.54.13.dist-info/METADATA,sha256=_ndN0PmfKx6Ro3nobejfixoQ24CM1gUlhn8OkG_vsX4,703
|
|
527
|
+
wbportfolio-1.54.13.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
528
|
+
wbportfolio-1.54.13.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
529
|
+
wbportfolio-1.54.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|