wbportfolio 1.54.9__py2.py3-none-any.whl → 1.54.11__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/import_export/handlers/trade.py +2 -0
- wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
- wbportfolio/models/portfolio.py +46 -29
- wbportfolio/models/transactions/trade_proposals.py +5 -4
- wbportfolio/models/transactions/trades.py +10 -9
- wbportfolio/pms/analytics/portfolio.py +8 -3
- 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/viewsets/transactions/trades.py +18 -9
- {wbportfolio-1.54.9.dist-info → wbportfolio-1.54.11.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.9.dist-info → wbportfolio-1.54.11.dist-info}/RECORD +14 -13
- {wbportfolio-1.54.9.dist-info → wbportfolio-1.54.11.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.9.dist-info → wbportfolio-1.54.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -88,6 +88,8 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
88
88
|
1 / (math.pow(10, 4))
|
|
89
89
|
) # we need that convertion mechanism otherwise there is floating point approximation error while casting to decimal and get_instance does not work as expected
|
|
90
90
|
data[field.name] = Decimal(value).quantize(Decimal(str(q)))
|
|
91
|
+
if (target_weight := data.pop("target_weight", None)) is not None:
|
|
92
|
+
data["_target_weight"] = target_weight
|
|
91
93
|
|
|
92
94
|
def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
|
|
93
95
|
if "transaction_date" not in data: # we might get only book date and not transaction date
|
|
@@ -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
|
@@ -18,14 +18,16 @@ from django.db.models import (
|
|
|
18
18
|
Q,
|
|
19
19
|
QuerySet,
|
|
20
20
|
Sum,
|
|
21
|
+
Value,
|
|
21
22
|
)
|
|
23
|
+
from django.db.models.functions import Coalesce
|
|
22
24
|
from django.db.models.signals import post_save
|
|
23
25
|
from django.dispatch import receiver
|
|
24
26
|
from django.utils import timezone
|
|
25
27
|
from django.utils.functional import cached_property
|
|
26
28
|
from pandas._libs.tslibs.offsets import BDay
|
|
27
29
|
from skfolio.preprocessing import prices_to_returns
|
|
28
|
-
from wbcore.contrib.currency.models import Currency
|
|
30
|
+
from wbcore.contrib.currency.models import Currency, CurrencyFXRates
|
|
29
31
|
from wbcore.contrib.notifications.utils import create_notification_type
|
|
30
32
|
from wbcore.models import WBModel
|
|
31
33
|
from wbcore.utils.importlib import import_from_dotted_path
|
|
@@ -54,6 +56,8 @@ logger = logging.getLogger("pms")
|
|
|
54
56
|
if TYPE_CHECKING:
|
|
55
57
|
from wbportfolio.models.transactions.trade_proposals import TradeProposal
|
|
56
58
|
|
|
59
|
+
MARKET_HOLIDAY_MAX_DURATION = 15
|
|
60
|
+
|
|
57
61
|
|
|
58
62
|
def get_returns(
|
|
59
63
|
instrument_ids: list[int],
|
|
@@ -61,6 +65,7 @@ def get_returns(
|
|
|
61
65
|
to_date: date,
|
|
62
66
|
to_currency: Currency | None = None,
|
|
63
67
|
ffill_returns: bool = True,
|
|
68
|
+
use_dl: bool = False,
|
|
64
69
|
) -> tuple[dict[date, dict[int, float]], pd.DataFrame]:
|
|
65
70
|
"""
|
|
66
71
|
Utility methods to get instrument returns for a given date range
|
|
@@ -68,45 +73,53 @@ def get_returns(
|
|
|
68
73
|
Args:
|
|
69
74
|
from_date: date range lower bound
|
|
70
75
|
to_date: date range upper bound
|
|
76
|
+
to_currency: currency to use for returns
|
|
77
|
+
ffill_returns: whether to ffill returns and prices
|
|
78
|
+
use_dl: whether to get data straight from the dataloader or use the internal table
|
|
71
79
|
|
|
72
80
|
Returns:
|
|
73
|
-
Return a tuple of the
|
|
81
|
+
Return a tuple of the raw prices and the returns dataframe
|
|
74
82
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
if use_dl:
|
|
84
|
+
kwargs = dict(from_date=from_date, to_date=to_date, values=[MarketData.CLOSE], apply_fx_rate=False)
|
|
85
|
+
if to_currency:
|
|
86
|
+
kwargs["target_currency"] = to_currency.key
|
|
87
|
+
df = pd.DataFrame(Instrument.objects.filter(id__in=instrument_ids).dl.market_data(**kwargs))
|
|
88
|
+
if df.empty:
|
|
89
|
+
raise InvalidAnalyticPortfolio()
|
|
90
|
+
df = df[["instrument_id", "fx_rate", "close", "valuation_date"]]
|
|
91
|
+
else:
|
|
92
|
+
if to_currency:
|
|
93
|
+
fx_rate = Coalesce(
|
|
94
|
+
CurrencyFXRates.get_fx_rates_subquery_for_two_currencies("date", "instrument__currency", to_currency),
|
|
95
|
+
Decimal("1"),
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
fx_rate = Value(Decimal("1"))
|
|
99
|
+
prices = InstrumentPrice.objects.filter(
|
|
100
|
+
instrument__in=instrument_ids, date__gte=from_date, date__lte=to_date
|
|
101
|
+
).annotate(fx_rate=fx_rate)
|
|
102
|
+
df = pd.DataFrame(
|
|
103
|
+
prices.filter_only_valid_prices().values_list("instrument", "fx_rate", "net_value", "date"),
|
|
104
|
+
columns=["instrument_id", "fx_rate", "close", "valuation_date"],
|
|
105
|
+
)
|
|
106
|
+
|
|
95
107
|
if df.empty:
|
|
96
108
|
raise InvalidAnalyticPortfolio()
|
|
97
|
-
df =
|
|
98
|
-
index="valuation_date", columns="instrument_id", values=["fx_rate", "close"]
|
|
109
|
+
df = (
|
|
110
|
+
df.pivot_table(index="valuation_date", columns="instrument_id", values=["fx_rate", "close"])
|
|
111
|
+
.astype(float)
|
|
112
|
+
.sort_index()
|
|
99
113
|
)
|
|
100
114
|
ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
|
|
101
115
|
df = df.reindex(ts)
|
|
102
116
|
if ffill_returns:
|
|
103
117
|
df = df.ffill()
|
|
104
118
|
df.index = pd.to_datetime(df.index)
|
|
105
|
-
|
|
106
119
|
prices_df = df["close"]
|
|
107
120
|
fx_rate_df = df["fx_rate"]
|
|
108
121
|
returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=ffill_returns)
|
|
109
|
-
return {ts.date(): row for ts, row in prices_df.
|
|
122
|
+
return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
|
|
110
123
|
[np.inf, -np.inf, np.nan], 0
|
|
111
124
|
)
|
|
112
125
|
|
|
@@ -400,7 +413,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
400
413
|
weights = self.get_weights(val_date)
|
|
401
414
|
return_date = (val_date + BDay(1)).date()
|
|
402
415
|
_, returns = get_returns(
|
|
403
|
-
list(weights.keys()),
|
|
416
|
+
list(weights.keys()),
|
|
417
|
+
(val_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
|
|
418
|
+
return_date,
|
|
419
|
+
to_currency=self.currency,
|
|
420
|
+
**kwargs,
|
|
404
421
|
)
|
|
405
422
|
if pd.Timestamp(return_date) not in returns.index:
|
|
406
423
|
raise InvalidAnalyticPortfolio()
|
|
@@ -822,10 +839,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
822
839
|
|
|
823
840
|
prices, returns = get_returns(
|
|
824
841
|
instrument_ids,
|
|
825
|
-
(start_date - BDay(
|
|
842
|
+
(start_date - BDay(MARKET_HOLIDAY_MAX_DURATION)).date(),
|
|
826
843
|
end_date,
|
|
827
844
|
to_currency=self.currency,
|
|
828
845
|
ffill_returns=True,
|
|
846
|
+
use_dl=True,
|
|
829
847
|
)
|
|
830
848
|
# Get raw prices to speed up asset position creation
|
|
831
849
|
# Instantiate the position iterator with the initial weights
|
|
@@ -842,7 +860,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
842
860
|
drifted_weights = analytic_portfolio.get_next_weights()
|
|
843
861
|
except KeyError: # if no return for that date, we break and continue
|
|
844
862
|
drifted_weights = weights
|
|
845
|
-
|
|
846
863
|
if rebalancer and rebalancer.is_valid(to_date):
|
|
847
864
|
effective_portfolio = PortfolioDTO(
|
|
848
865
|
positions=[
|
|
@@ -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,
|
|
@@ -397,7 +399,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
397
399
|
if approve_automatically and self.portfolio.can_be_rebalanced:
|
|
398
400
|
self.approve(replay=False, broadcast_changes_at_date=broadcast_changes_at_date)
|
|
399
401
|
|
|
400
|
-
def replay(self,
|
|
402
|
+
def replay(self, broadcast_changes_at_date: bool = True):
|
|
401
403
|
last_trade_proposal = self
|
|
402
404
|
last_trade_proposal_created = False
|
|
403
405
|
while last_trade_proposal and last_trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
@@ -405,7 +407,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
405
407
|
logger.info(f"Replaying trade proposal {last_trade_proposal}")
|
|
406
408
|
last_trade_proposal.approve_workflow(
|
|
407
409
|
silent_exception=True,
|
|
408
|
-
force_reset_trade=
|
|
410
|
+
force_reset_trade=True,
|
|
409
411
|
broadcast_changes_at_date=broadcast_changes_at_date,
|
|
410
412
|
)
|
|
411
413
|
last_trade_proposal.save()
|
|
@@ -803,9 +805,8 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
803
805
|
) # we delete the existing portfolio as it has been reverted
|
|
804
806
|
for trade in self.trades.all():
|
|
805
807
|
trade.status = Trade.Status.DRAFT
|
|
806
|
-
trade.drift_factor = Decimal("1")
|
|
807
808
|
trades.append(trade)
|
|
808
|
-
Trade.objects.bulk_update(trades, ["status"
|
|
809
|
+
Trade.objects.bulk_update(trades, ["status"])
|
|
809
810
|
|
|
810
811
|
def can_revert(self):
|
|
811
812
|
errors = dict()
|
|
@@ -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",
|
|
@@ -439,7 +441,7 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
439
441
|
method=RequestType.PATCH,
|
|
440
442
|
identifiers=("wbportfolio:trade",),
|
|
441
443
|
icon=WBIcon.UNDO.icon,
|
|
442
|
-
key="
|
|
444
|
+
key="revert",
|
|
443
445
|
label="Revert",
|
|
444
446
|
action_label="revert",
|
|
445
447
|
# description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
|
|
@@ -518,6 +520,11 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
518
520
|
self, "target_weight", round(self._effective_weight + self.weighting, self.TRADE_WEIGHTING_PRECISION)
|
|
519
521
|
)
|
|
520
522
|
|
|
523
|
+
@_target_weight.setter
|
|
524
|
+
def _target_weight(self, target_weight):
|
|
525
|
+
self.weighting = Decimal(target_weight) - self._effective_weight
|
|
526
|
+
self._set_type()
|
|
527
|
+
|
|
521
528
|
@property
|
|
522
529
|
@admin.display(description="Target Shares")
|
|
523
530
|
def _target_shares(self) -> Decimal:
|
|
@@ -558,12 +565,6 @@ class Trade(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
558
565
|
]
|
|
559
566
|
# notification_email_template = "portfolio/email/trade_notification.html"
|
|
560
567
|
|
|
561
|
-
def __init__(self, *args, target_weight: Decimal | None = None, **kwargs):
|
|
562
|
-
super().__init__(*args, **kwargs)
|
|
563
|
-
if target_weight is not None: # if target weight is provided, we guess the corresponding weighting
|
|
564
|
-
self.weighting = Decimal(target_weight) - self._effective_weight
|
|
565
|
-
self._set_type()
|
|
566
|
-
|
|
567
568
|
def save(self, *args, **kwargs):
|
|
568
569
|
if abs(self.weighting) < 10e-6:
|
|
569
570
|
self.weighting = Decimal("0")
|
|
@@ -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
|
|
@@ -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):
|
|
@@ -445,14 +445,20 @@ class TradeTradeProposalModelViewSet(
|
|
|
445
445
|
|
|
446
446
|
agg = {
|
|
447
447
|
"effective_weight": {
|
|
448
|
-
"Cash": format_number(cash_sum_effective_weight, decimal=
|
|
449
|
-
"Non-Cash": format_number(noncash_sum_effective_weight, decimal=
|
|
450
|
-
"Total": format_number(
|
|
448
|
+
"Cash": format_number(cash_sum_effective_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
|
|
449
|
+
"Non-Cash": format_number(noncash_sum_effective_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
|
|
450
|
+
"Total": format_number(
|
|
451
|
+
noncash_sum_effective_weight + cash_sum_effective_weight,
|
|
452
|
+
decimal=Trade.TRADE_WEIGHTING_PRECISION,
|
|
453
|
+
),
|
|
451
454
|
},
|
|
452
455
|
"target_weight": {
|
|
453
|
-
"Cash": format_number(cash_sum_target_cash_weight, decimal=
|
|
454
|
-
"Non-Cash": format_number(noncash_sum_target_weight, decimal=
|
|
455
|
-
"Total": format_number(
|
|
456
|
+
"Cash": format_number(cash_sum_target_cash_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
|
|
457
|
+
"Non-Cash": format_number(noncash_sum_target_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
|
|
458
|
+
"Total": format_number(
|
|
459
|
+
cash_sum_target_cash_weight + noncash_sum_target_weight,
|
|
460
|
+
decimal=Trade.TRADE_WEIGHTING_PRECISION,
|
|
461
|
+
),
|
|
456
462
|
},
|
|
457
463
|
"effective_total_value_fx_portfolio": {
|
|
458
464
|
"Cash": format_number(cash_sum_effective_total_value_fx_portfolio, decimal=6),
|
|
@@ -471,9 +477,12 @@ class TradeTradeProposalModelViewSet(
|
|
|
471
477
|
),
|
|
472
478
|
},
|
|
473
479
|
"weighting": {
|
|
474
|
-
"Cash Flow": format_number(
|
|
475
|
-
|
|
476
|
-
|
|
480
|
+
"Cash Flow": format_number(
|
|
481
|
+
cash_sum_target_cash_weight - cash_sum_effective_weight,
|
|
482
|
+
decimal=Trade.TRADE_WEIGHTING_PRECISION,
|
|
483
|
+
),
|
|
484
|
+
"Buy": format_number(sum_buy_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
|
|
485
|
+
"Sell": format_number(sum_sell_weight, decimal=Trade.TRADE_WEIGHTING_PRECISION),
|
|
477
486
|
},
|
|
478
487
|
"total_value_fx_portfolio": {
|
|
479
488
|
"Cash Flow": format_number(
|
|
@@ -113,7 +113,7 @@ wbportfolio/import_export/handlers/dividend.py,sha256=F0oLfNt2B_QQAjHBCRpxa5HSkf
|
|
|
113
113
|
wbportfolio/import_export/handlers/fees.py,sha256=BOFHAvSTlvVLaxnm6KD_fcza1TlPc02HOR9J0_jjswI,2495
|
|
114
114
|
wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
|
|
115
115
|
wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
|
|
116
|
-
wbportfolio/import_export/handlers/trade.py,sha256=
|
|
116
|
+
wbportfolio/import_export/handlers/trade.py,sha256=_-P1ImDX6jfObm1WKiLtzz7RXEnXjtARAw7cxoHReCM,12826
|
|
117
117
|
wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
118
118
|
wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
|
|
119
119
|
wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -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=5gOPWWv-_ke-p-B14S9ZNKRvvGQVHNBzR2r5xL-AKm0,58341
|
|
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=Iilb67WdUh4XuBhxMqnnxTj_43NxTloGNNtJQh1izD8,38327
|
|
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
|
|
@@ -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
|
|
@@ -521,8 +522,8 @@ wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0
|
|
|
521
522
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
522
523
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
523
524
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=kQCojTNKBEyn2NcenL3a9auzBH4sIgLEx8rLAYCGLGg,6161
|
|
524
|
-
wbportfolio/viewsets/transactions/trades.py,sha256=
|
|
525
|
-
wbportfolio-1.54.
|
|
526
|
-
wbportfolio-1.54.
|
|
527
|
-
wbportfolio-1.54.
|
|
528
|
-
wbportfolio-1.54.
|
|
525
|
+
wbportfolio/viewsets/transactions/trades.py,sha256=Y8v2cM0vpspHysaAvu8qqhzt86dNtb2Q3puo4HCJsTI,22629
|
|
526
|
+
wbportfolio-1.54.11.dist-info/METADATA,sha256=PhblDUHcEGHQmY_KCEcvlc3oIl98hhageKfJGRshiPg,703
|
|
527
|
+
wbportfolio-1.54.11.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
528
|
+
wbportfolio-1.54.11.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
529
|
+
wbportfolio-1.54.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|