wbportfolio 1.54.10__py2.py3-none-any.whl → 1.54.12__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/migrations/0081_alter_trade_drift_factor.py +19 -0
- wbportfolio/models/portfolio.py +47 -44
- wbportfolio/models/transactions/trade_proposals.py +3 -1
- wbportfolio/models/transactions/trades.py +4 -2
- wbportfolio/pms/analytics/portfolio.py +8 -3
- wbportfolio/tests/models/test_portfolios.py +41 -0
- wbportfolio/tests/models/transactions/test_trade_proposals.py +20 -0
- wbportfolio/tests/pms/test_analytics.py +21 -3
- wbportfolio/viewsets/assets.py +2 -2
- {wbportfolio-1.54.10.dist-info → wbportfolio-1.54.12.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.10.dist-info → wbportfolio-1.54.12.dist-info}/RECORD +13 -12
- {wbportfolio-1.54.10.dist-info → wbportfolio-1.54.12.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.10.dist-info → wbportfolio-1.54.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Generated by Django 5.0.14 on 2025-07-16 12:39
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('wbportfolio', '0080_alter_trade_drift_factor_alter_trade_weighting'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AlterField(
|
|
15
|
+
model_name='trade',
|
|
16
|
+
name='drift_factor',
|
|
17
|
+
field=models.DecimalField(decimal_places=16, default=Decimal('1'), help_text='Drift factor to be applied to the previous portfolio weight to get the actual effective weight including daily return', max_digits=19, verbose_name='Drift Factor'),
|
|
18
|
+
),
|
|
19
|
+
]
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -11,21 +11,15 @@ 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
|
-
|
|
16
|
-
F,
|
|
17
|
-
OuterRef,
|
|
18
|
-
Q,
|
|
19
|
-
QuerySet,
|
|
20
|
-
Sum,
|
|
21
|
-
)
|
|
14
|
+
from django.db.models import Case, Exists, F, OuterRef, Q, QuerySet, Sum, Value, When
|
|
15
|
+
from django.db.models.functions import Coalesce
|
|
22
16
|
from django.db.models.signals import post_save
|
|
23
17
|
from django.dispatch import receiver
|
|
24
18
|
from django.utils import timezone
|
|
25
19
|
from django.utils.functional import cached_property
|
|
26
20
|
from pandas._libs.tslibs.offsets import BDay
|
|
27
21
|
from skfolio.preprocessing import prices_to_returns
|
|
28
|
-
from wbcore.contrib.currency.models import Currency
|
|
22
|
+
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
29
23
|
from wbcore.contrib.notifications.utils import create_notification_type
|
|
30
24
|
from wbcore.models import WBModel
|
|
31
25
|
from wbcore.utils.importlib import import_from_dotted_path
|
|
@@ -62,7 +56,7 @@ def get_returns(
|
|
|
62
56
|
from_date: date,
|
|
63
57
|
to_date: date,
|
|
64
58
|
to_currency: Currency | None = None,
|
|
65
|
-
|
|
59
|
+
use_dl: bool = False,
|
|
66
60
|
) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
|
|
67
61
|
"""
|
|
68
62
|
Utility methods to get instrument returns for a given date range
|
|
@@ -70,46 +64,55 @@ def get_returns(
|
|
|
70
64
|
Args:
|
|
71
65
|
from_date: date range lower bound
|
|
72
66
|
to_date: date range upper bound
|
|
67
|
+
to_currency: currency to use for returns
|
|
68
|
+
use_dl: whether to get data straight from the dataloader or use the internal table
|
|
73
69
|
|
|
74
70
|
Returns:
|
|
75
|
-
Return a tuple of the
|
|
71
|
+
Return a tuple of the raw prices and the returns dataframe
|
|
76
72
|
"""
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
if use_dl:
|
|
74
|
+
kwargs = dict(from_date=from_date, to_date=to_date, values=[MarketData.CLOSE], apply_fx_rate=False)
|
|
75
|
+
if to_currency:
|
|
76
|
+
kwargs["target_currency"] = to_currency.key
|
|
77
|
+
df = pd.DataFrame(Instrument.objects.filter(id__in=instrument_ids).dl.market_data(**kwargs))
|
|
78
|
+
if df.empty:
|
|
79
|
+
raise InvalidAnalyticPortfolio()
|
|
80
|
+
df = df[["instrument_id", "fx_rate", "close", "valuation_date"]]
|
|
81
|
+
else:
|
|
82
|
+
if to_currency:
|
|
83
|
+
fx_rate = Coalesce(
|
|
84
|
+
CurrencyFXRates.get_fx_rates_subquery_for_two_currencies("date", "instrument__currency", to_currency),
|
|
85
|
+
Decimal("1"),
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
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)
|
|
90
|
+
prices = InstrumentPrice.objects.filter(
|
|
91
|
+
instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
|
|
92
|
+
).annotate(fx_rate=Case(When(calculated=False, then=fx_rate), default=None))
|
|
93
|
+
df = pd.DataFrame(
|
|
94
|
+
prices.filter_only_valid_prices().values_list("instrument", "fx_rate", "net_value", "date"),
|
|
95
|
+
columns=["instrument_id", "fx_rate", "close", "valuation_date"],
|
|
96
|
+
)
|
|
97
97
|
if df.empty:
|
|
98
98
|
raise InvalidAnalyticPortfolio()
|
|
99
|
-
df =
|
|
100
|
-
index="valuation_date", columns="instrument_id", values=["fx_rate", "close"]
|
|
99
|
+
df = (
|
|
100
|
+
df.pivot_table(index="valuation_date", columns="instrument_id", values=["fx_rate", "close"])
|
|
101
|
+
.astype(float)
|
|
102
|
+
.sort_index()
|
|
101
103
|
)
|
|
102
104
|
ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
|
|
103
105
|
df = df.reindex(ts)
|
|
104
|
-
|
|
105
|
-
df = df.ffill()
|
|
106
|
+
df = df.ffill()
|
|
106
107
|
df.index = pd.to_datetime(df.index)
|
|
107
|
-
|
|
108
108
|
prices_df = df["close"]
|
|
109
|
-
|
|
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)
|
|
110
113
|
|
|
111
|
-
returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=
|
|
112
|
-
return {ts.date(): row for ts, row in prices_df.
|
|
114
|
+
returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=True)
|
|
115
|
+
return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
|
|
113
116
|
[np.inf, -np.inf, np.nan], 0
|
|
114
117
|
)
|
|
115
118
|
|
|
@@ -348,11 +351,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
348
351
|
instrument.delisted_date = date.today() - timedelta(days=1)
|
|
349
352
|
instrument.save()
|
|
350
353
|
|
|
351
|
-
def _build_dto(self, val_date: date,
|
|
354
|
+
def _build_dto(self, val_date: date, **extra_kwargs) -> PortfolioDTO:
|
|
352
355
|
"returns the dto representation of this portfolio at the specified date"
|
|
353
356
|
assets = self.assets.filter(date=val_date, **extra_kwargs)
|
|
354
357
|
try:
|
|
355
|
-
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()
|
|
356
359
|
except InvalidAnalyticPortfolio:
|
|
357
360
|
drifted_weights = {}
|
|
358
361
|
positions = []
|
|
@@ -387,7 +390,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
387
390
|
)
|
|
388
391
|
|
|
389
392
|
def get_analytic_portfolio(
|
|
390
|
-
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
|
|
391
394
|
) -> AnalyticPortfolio:
|
|
392
395
|
"""
|
|
393
396
|
Return the analytic portfolio associated with this portfolio at the given date
|
|
@@ -407,6 +410,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
407
410
|
(val_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
|
|
408
411
|
return_date,
|
|
409
412
|
to_currency=self.currency,
|
|
413
|
+
use_dl=use_dl,
|
|
410
414
|
**kwargs,
|
|
411
415
|
)
|
|
412
416
|
if pd.Timestamp(return_date) not in returns.index:
|
|
@@ -832,7 +836,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
832
836
|
(start_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
|
|
833
837
|
end_date,
|
|
834
838
|
to_currency=self.currency,
|
|
835
|
-
|
|
839
|
+
use_dl=True,
|
|
836
840
|
)
|
|
837
841
|
# Get raw prices to speed up asset position creation
|
|
838
842
|
# Instantiate the position iterator with the initial weights
|
|
@@ -849,7 +853,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
849
853
|
drifted_weights = analytic_portfolio.get_next_weights()
|
|
850
854
|
except KeyError: # if no return for that date, we break and continue
|
|
851
855
|
drifted_weights = weights
|
|
852
|
-
|
|
853
856
|
if rebalancer and rebalancer.is_valid(to_date):
|
|
854
857
|
effective_portfolio = PortfolioDTO(
|
|
855
858
|
positions=[
|
|
@@ -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 = {}
|
|
@@ -366,6 +366,8 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
366
366
|
trade.weighting = -trade_dto.effective_weight
|
|
367
367
|
|
|
368
368
|
trade.save()
|
|
369
|
+
# final sanity check to make sure invalid trade with effective and target weight of 0 are automatically removed:
|
|
370
|
+
self.trades.all().annotate_base_info().filter(target_weight=0, effective_weight=0).delete()
|
|
369
371
|
|
|
370
372
|
def approve_workflow(
|
|
371
373
|
self,
|
|
@@ -230,8 +230,10 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
230
230
|
help_text="The Trade Proposal this trade is coming from",
|
|
231
231
|
)
|
|
232
232
|
drift_factor = models.DecimalField(
|
|
233
|
-
max_digits=
|
|
234
|
-
|
|
233
|
+
max_digits=TRADE_WEIGHTING_PRECISION * 2
|
|
234
|
+
+ 3, # we don't expect any drift factor to be in the order of magnitude greater than 1000
|
|
235
|
+
decimal_places=TRADE_WEIGHTING_PRECISION
|
|
236
|
+
* 2, # we need a higher precision for this factor to avoid float inprecision
|
|
235
237
|
default=Decimal(1.0),
|
|
236
238
|
verbose_name="Drift Factor",
|
|
237
239
|
help_text="Drift factor to be applied to the previous portfolio weight to get the actual effective weight including daily return",
|
|
@@ -16,10 +16,10 @@ class Portfolio(BasePortfolio):
|
|
|
16
16
|
)
|
|
17
17
|
return df
|
|
18
18
|
|
|
19
|
-
def get_next_weights(self) -> dict[int, float]:
|
|
19
|
+
def get_next_weights(self, round_precision: int = 8) -> dict[int, float]:
|
|
20
20
|
"""
|
|
21
21
|
Given the next returns, compute the drifted weights of this portfolio
|
|
22
|
-
|
|
22
|
+
round_precision: Round the weight to the given round number and ensure the total weight reflects this. Default to 8 decimals
|
|
23
23
|
Returns:
|
|
24
24
|
A dictionary of weights (instrument ids as keys and weights as values)
|
|
25
25
|
"""
|
|
@@ -29,7 +29,12 @@ class Portfolio(BasePortfolio):
|
|
|
29
29
|
next_weights = weights * (returns + 1.0) / portfolio_returns
|
|
30
30
|
next_weights = next_weights.dropna()
|
|
31
31
|
next_weights = next_weights / next_weights.sum()
|
|
32
|
-
|
|
32
|
+
if round_precision and not next_weights.empty:
|
|
33
|
+
next_weights = next_weights.round(round_precision)
|
|
34
|
+
quantization_error = 1.0 - next_weights.sum()
|
|
35
|
+
largest_weight = next_weights.idxmax()
|
|
36
|
+
next_weights.loc[largest_weight] = next_weights.loc[largest_weight] + quantization_error
|
|
37
|
+
return {i: round(w, round_precision) for i, w in next_weights.items()} # handle float precision manually
|
|
33
38
|
|
|
34
39
|
def get_estimate_net_value(self, previous_net_asset_value: float) -> float:
|
|
35
40
|
expected_returns = self.weights @ self.X.iloc[-1, :].T
|
|
@@ -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)
|
|
@@ -336,6 +336,26 @@ class TestTradeProposal:
|
|
|
336
336
|
assert t2.weighting == Decimal("0")
|
|
337
337
|
assert t3.weighting == Decimal("0.5")
|
|
338
338
|
|
|
339
|
+
def test_reset_trades_remove_invalid_trades(self, trade_proposal, trade_factory, instrument_price_factory):
|
|
340
|
+
# create a invalid trade and its price
|
|
341
|
+
invalid_trade = trade_factory.create(trade_proposal=trade_proposal, weighting=0)
|
|
342
|
+
instrument_price_factory.create(
|
|
343
|
+
date=invalid_trade.transaction_date, instrument=invalid_trade.underlying_instrument
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# create a valid trade and its price
|
|
347
|
+
valid_trade = trade_factory.create(trade_proposal=trade_proposal, weighting=1.0)
|
|
348
|
+
instrument_price_factory.create(
|
|
349
|
+
date=valid_trade.transaction_date, instrument=valid_trade.underlying_instrument
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
trade_proposal.reset_trades()
|
|
353
|
+
assert trade_proposal.trades.get(underlying_instrument=valid_trade.underlying_instrument).weighting == Decimal(
|
|
354
|
+
"1"
|
|
355
|
+
)
|
|
356
|
+
with pytest.raises(Trade.DoesNotExist):
|
|
357
|
+
trade_proposal.trades.get(underlying_instrument=invalid_trade.underlying_instrument)
|
|
358
|
+
|
|
339
359
|
# Test replaying trade proposals
|
|
340
360
|
@patch.object(Portfolio, "drift_weights")
|
|
341
361
|
def test_replay(self, mock_fct, trade_proposal_factory):
|
|
@@ -16,9 +16,27 @@ def test_get_next_weights():
|
|
|
16
16
|
portfolio = Portfolio(X=pd.DataFrame([returns]), weights=pd.Series(weights))
|
|
17
17
|
next_weights = portfolio.get_next_weights()
|
|
18
18
|
|
|
19
|
-
assert next_weights[0] == pytest.approx(w0 * (r0 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-
|
|
20
|
-
assert next_weights[1] == pytest.approx(w1 * (r1 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-
|
|
21
|
-
assert next_weights[2] == pytest.approx(w2 * (r2 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-
|
|
19
|
+
assert next_weights[0] == pytest.approx(w0 * (r0 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-8)
|
|
20
|
+
assert next_weights[1] == pytest.approx(w1 * (r1 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-8)
|
|
21
|
+
assert next_weights[2] == pytest.approx(w2 * (r2 + 1) / (w0 * (r0 + 1) + w1 * (r1 + 1) + w2 * (r2 + 1)), abs=10e-8)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_get_next_weights_solve_quantization_error():
|
|
25
|
+
w0 = 0.33333334
|
|
26
|
+
w1 = 0.33333333
|
|
27
|
+
w2 = 0.33333333
|
|
28
|
+
weights = [w0, w1, w2]
|
|
29
|
+
returns = [1.0, 1.0, 1.0] # no returns
|
|
30
|
+
portfolio = Portfolio(X=pd.DataFrame([returns]), weights=pd.Series(weights))
|
|
31
|
+
next_weights = portfolio.get_next_weights(round_precision=8) # no rounding as number are all 8 decimals
|
|
32
|
+
assert sum(next_weights.values()) == 1.0
|
|
33
|
+
next_weights = portfolio.get_next_weights(
|
|
34
|
+
round_precision=7
|
|
35
|
+
) # we expect the weight to be rounded to 6 decimals, which would lead to a total sum of 0.999999
|
|
36
|
+
|
|
37
|
+
assert next_weights[0] == 0.3333334
|
|
38
|
+
assert next_weights[1] == 0.3333333
|
|
39
|
+
assert next_weights[2] == 0.3333333
|
|
22
40
|
|
|
23
41
|
|
|
24
42
|
def test_get_estimate_net_value():
|
wbportfolio/viewsets/assets.py
CHANGED
|
@@ -133,7 +133,7 @@ class AssetPositionModelViewSet(
|
|
|
133
133
|
weighting = queryset.aggregate(s=Sum(F("weighting")))["s"]
|
|
134
134
|
aggregates.update(
|
|
135
135
|
{
|
|
136
|
-
"weighting": {"Σ": format_number(weighting, decimal=
|
|
136
|
+
"weighting": {"Σ": format_number(weighting, decimal=8)},
|
|
137
137
|
"total_value_fx_usd": {"Σ": format_number(total_value_fx_usd)},
|
|
138
138
|
}
|
|
139
139
|
)
|
|
@@ -204,7 +204,7 @@ class AssetPositionPortfolioModelViewSet(InstrumentMetricMixin, AssetPositionMod
|
|
|
204
204
|
total_value_fx_portfolio = queryset.aggregate(s=Sum(F("total_value_fx_portfolio")))["s"]
|
|
205
205
|
aggregates = super().get_aggregates(queryset, paginated_queryset)
|
|
206
206
|
aggregates["total_value_fx_portfolio"] = {"Σ": format_number(total_value_fx_portfolio)}
|
|
207
|
-
aggregates["weighting"] = {"Σ": format_number(weighting, decimal=
|
|
207
|
+
aggregates["weighting"] = {"Σ": format_number(weighting, decimal=8)}
|
|
208
208
|
return aggregates
|
|
209
209
|
|
|
210
210
|
def get_queryset(self):
|
|
@@ -250,6 +250,7 @@ wbportfolio/migrations/0077_remove_transaction_currency_and_more.py,sha256=Yf4a3
|
|
|
250
250
|
wbportfolio/migrations/0078_trade_drift_factor.py,sha256=26Z3yoiBhMueB-k2R9HaIzg5Qr7BYpdtzlU-65T_cH0,999
|
|
251
251
|
wbportfolio/migrations/0079_alter_trade_drift_factor.py,sha256=2tvPecUxEy60-ELy9wtiuTR2dhJ8HucJjvOEuia4Pp4,627
|
|
252
252
|
wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py,sha256=vCPJSrM7x24jUJseGkHEVBD0c5nEpDTrb9-zkJ-0QoI,569
|
|
253
|
+
wbportfolio/migrations/0081_alter_trade_drift_factor.py,sha256=rF3HA1MQJ0hltr0dExJAx47w8XxUCWRbDUyxQLoQB2I,656
|
|
253
254
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
254
255
|
wbportfolio/models/__init__.py,sha256=HSpa5xwh_MHQaBpNrq9E0CbdEE5Iq-pDLIsPzZ-TRTg,904
|
|
255
256
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
@@ -257,7 +258,7 @@ wbportfolio/models/asset.py,sha256=b0vPt4LwNrxcMiK7UmBKViYnbNNlZzPTagvU5vFuyrc,4
|
|
|
257
258
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
258
259
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
259
260
|
wbportfolio/models/indexes.py,sha256=gvW4K9U9Bj8BmVCqFYdWiXvDWhjHINRON8XhNsZUiQY,639
|
|
260
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
261
|
+
wbportfolio/models/portfolio.py,sha256=5CddquPvVp-ImPlteNMqwDeY_mCFwdWMy7ynMRDDxak,58578
|
|
261
262
|
wbportfolio/models/portfolio_cash_flow.py,sha256=uElG7IJUBY8qvtrXftOoskX6EA-dKgEG1JJdvHeWV7g,7336
|
|
262
263
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
263
264
|
wbportfolio/models/portfolio_relationship.py,sha256=ZGECiPZiLdlk4uSamOrEfuzO0hduK6OMKJLUSnh5_kc,5190
|
|
@@ -284,13 +285,13 @@ wbportfolio/models/transactions/claim.py,sha256=SF2FlwG6SRVmA_hT0NbXah5-fYejccWK
|
|
|
284
285
|
wbportfolio/models/transactions/dividends.py,sha256=mmOdGWR35yndUMoCuG24Y6BdtxDhSk2gMQ-8LVguqzg,1890
|
|
285
286
|
wbportfolio/models/transactions/fees.py,sha256=wJtlzbBCAq1UHvv0wqWTE2BEjCF5RMtoaSDS3kODFRo,7112
|
|
286
287
|
wbportfolio/models/transactions/rebalancing.py,sha256=rwePcmTZOYgfSWnBQcBrZ3DQHRJ3w17hdO_hgrRbbhI,7696
|
|
287
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
288
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
288
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=1xOxHVOUwgTKCcY7mN4u5SoFtdJrQkNg6xA3O2-f4Yw,38340
|
|
289
|
+
wbportfolio/models/transactions/trades.py,sha256=1gmAdavuWu1Iko90s9prMxsK_NuDKIUBIKMDuHiKzow,34176
|
|
289
290
|
wbportfolio/models/transactions/transactions.py,sha256=XTcUeMUfkf5XTSZaR2UAyGqCVkOhQYk03_vzHLIgf8Q,3807
|
|
290
291
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
291
292
|
wbportfolio/pms/typing.py,sha256=BV4dzazNHdfpfLV99bLVyYGcETmbQSnFV6ipc4fNKfg,8470
|
|
292
293
|
wbportfolio/pms/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
293
|
-
wbportfolio/pms/analytics/portfolio.py,sha256=
|
|
294
|
+
wbportfolio/pms/analytics/portfolio.py,sha256=QaYArF-8Dk9MY0ZLUZ1IHaz3T-uYG81P5SqS_jSar8A,1950
|
|
294
295
|
wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
295
296
|
wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
|
|
296
297
|
wbportfolio/pms/trading/handler.py,sha256=ZOwgnOU4ScVIhTMRQ0SLR2cCCZP9whmVv-S5hF-TOME,8593
|
|
@@ -380,7 +381,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
|
|
|
380
381
|
wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
|
|
381
382
|
wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
|
|
382
383
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
383
|
-
wbportfolio/tests/models/test_portfolios.py,sha256=
|
|
384
|
+
wbportfolio/tests/models/test_portfolios.py,sha256=H3mgrQLdTkrljFZgJLRTXuN6J8fasRtlYoaRCnirdwU,56165
|
|
384
385
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
385
386
|
wbportfolio/tests/models/test_products.py,sha256=IcBzw9hrGiWFMRwPBTMukCMWrhqnjOVA2hhb90xYOW8,9580
|
|
386
387
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
@@ -390,10 +391,10 @@ wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
|
|
|
390
391
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
391
392
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
|
|
392
393
|
wbportfolio/tests/models/transactions/test_rebalancing.py,sha256=fZ5tx6kByEGXD6nhapYdvk9HOjYlmjhU2w6KlQJ6QE4,4061
|
|
393
|
-
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=
|
|
394
|
+
wbportfolio/tests/models/transactions/test_trade_proposals.py,sha256=INZb4J7qodXiT8FwbRNjEoXOm9HjO6W-vkA9uW8e3CI,30804
|
|
394
395
|
wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0Wq9ycA2oeeEBhO3dzV7sbXEU,9863
|
|
395
396
|
wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
396
|
-
wbportfolio/tests/pms/test_analytics.py,sha256=
|
|
397
|
+
wbportfolio/tests/pms/test_analytics.py,sha256=KzgZqZ9yYB1gsokw6IU-uKYWr1eFHWyFte8RSpMVRg8,1897
|
|
397
398
|
wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
398
399
|
wbportfolio/tests/rebalancing/test_models.py,sha256=QMfcYDvFew1bH6kPm-jVJLC_RqmPE-oGTqUldx1KVgg,8025
|
|
399
400
|
wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -406,7 +407,7 @@ wbportfolio/tests/viewsets/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5J
|
|
|
406
407
|
wbportfolio/tests/viewsets/transactions/test_claims.py,sha256=QEZfMAW07dyoZ63t2umSwGOqvaTULfYfbN_F4ZoSAcw,6368
|
|
407
408
|
wbportfolio/viewsets/__init__.py,sha256=3kUaQ66ybvROwejd3bEcSt4XKzfOlPDaeoStMvlz7qY,2294
|
|
408
409
|
wbportfolio/viewsets/adjustments.py,sha256=ugbX4aFRCaD4Yj1hxL-VIPaNI7GF_wt0FrkN6mq1YjU,1524
|
|
409
|
-
wbportfolio/viewsets/assets.py,sha256=
|
|
410
|
+
wbportfolio/viewsets/assets.py,sha256=MCE81rmDwbMGxO_LsD8AvU9tHWWgi-OkX5ecLFH-KGY,24634
|
|
410
411
|
wbportfolio/viewsets/assets_and_net_new_money_progression.py,sha256=Jl4vEQP4N2OFL5IGBXoKcj-0qaPviU0I8npvQLw4Io0,4464
|
|
411
412
|
wbportfolio/viewsets/custodians.py,sha256=CTFqkqVP1R3AV7lhdvcdICxB5DfwDYCyikNSI5kbYEo,2322
|
|
412
413
|
wbportfolio/viewsets/esg.py,sha256=27MxxdXQH3Cq_1UEYmcrF7htUOg6i81fUpbVQXAAKJI,6985
|
|
@@ -522,7 +523,7 @@ wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIH
|
|
|
522
523
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
523
524
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=kQCojTNKBEyn2NcenL3a9auzBH4sIgLEx8rLAYCGLGg,6161
|
|
524
525
|
wbportfolio/viewsets/transactions/trades.py,sha256=Y8v2cM0vpspHysaAvu8qqhzt86dNtb2Q3puo4HCJsTI,22629
|
|
525
|
-
wbportfolio-1.54.
|
|
526
|
-
wbportfolio-1.54.
|
|
527
|
-
wbportfolio-1.54.
|
|
528
|
-
wbportfolio-1.54.
|
|
526
|
+
wbportfolio-1.54.12.dist-info/METADATA,sha256=8ZNVEtEaAspKx8rNxEGdkon_4d95uFFaJnwzVcHUxpc,703
|
|
527
|
+
wbportfolio-1.54.12.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
528
|
+
wbportfolio-1.54.12.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
529
|
+
wbportfolio-1.54.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|