wbportfolio 1.49.0__py2.py3-none-any.whl → 1.49.2__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/factories/dividends.py +1 -0
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +1 -1
- wbportfolio/import_export/handlers/portfolio_cash_flow.py +1 -1
- wbportfolio/import_export/handlers/trade.py +5 -2
- wbportfolio/import_export/parsers/sg_lux/custodian_positions.py +1 -1
- wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py +1 -1
- wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py +126 -0
- wbportfolio/models/portfolio.py +19 -6
- wbportfolio/models/transactions/claim.py +8 -7
- wbportfolio/models/transactions/dividends.py +3 -20
- wbportfolio/models/transactions/trade_proposals.py +23 -20
- wbportfolio/models/transactions/trades.py +9 -12
- wbportfolio/models/transactions/transactions.py +37 -37
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +13 -16
- wbportfolio/serializers/portfolios.py +1 -1
- wbportfolio/tests/models/test_portfolios.py +1 -1
- wbportfolio/tests/rebalancing/test_models.py +2 -2
- wbportfolio/viewsets/assets.py +1 -1
- wbportfolio/viewsets/configs/display/assets.py +0 -11
- wbportfolio/viewsets/configs/display/portfolios.py +2 -2
- wbportfolio/viewsets/configs/display/products.py +0 -13
- wbportfolio/viewsets/configs/display/trades.py +1 -9
- wbportfolio/viewsets/configs/endpoints/transactions.py +6 -0
- wbportfolio/viewsets/portfolios.py +17 -1
- wbportfolio/viewsets/transactions/trade_proposals.py +6 -4
- wbportfolio/viewsets/transactions/trades.py +21 -3
- {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.2.dist-info}/METADATA +1 -1
- {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.2.dist-info}/RECORD +31 -30
- {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.2.dist-info}/WHEEL +0 -0
- {wbportfolio-1.49.0.dist-info → wbportfolio-1.49.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,5 +11,6 @@ class DividendTransactionsFactory(TransactionFactory):
|
|
|
11
11
|
class Meta:
|
|
12
12
|
model = DividendTransaction
|
|
13
13
|
|
|
14
|
+
retrocession = 1.0
|
|
14
15
|
shares = factory.LazyAttribute(lambda o: random.randint(10, 10000))
|
|
15
16
|
price = factory.LazyAttribute(lambda o: random.randint(10, 10000))
|
|
@@ -23,7 +23,7 @@ class DividendImportHandler(ImportExportHandler):
|
|
|
23
23
|
data["value_date"] = datetime.strptime(data["value_date"], "%Y-%m-%d").date()
|
|
24
24
|
from wbportfolio.models import Portfolio
|
|
25
25
|
|
|
26
|
-
data["portfolio"] = Portfolio.
|
|
26
|
+
data["portfolio"] = Portfolio.all_objects.get(id=data["portfolio"])
|
|
27
27
|
instrument = self.instrument_handler.process_object(
|
|
28
28
|
data["underlying_instrument"], only_security=False, read_only=True
|
|
29
29
|
)[0]
|
|
@@ -26,7 +26,7 @@ class FeesImportHandler(ImportExportHandler):
|
|
|
26
26
|
|
|
27
27
|
data["linked_product"] = Product.objects.get(id=data["linked_product"])
|
|
28
28
|
if "porfolio" in data:
|
|
29
|
-
data["portfolio"] = Portfolio.
|
|
29
|
+
data["portfolio"] = Portfolio.all_objects.get(id=data["portfolio"])
|
|
30
30
|
else:
|
|
31
31
|
data["portfolio"] = data["linked_product"].primary_portfolio
|
|
32
32
|
data["underlying_instrument"] = Cash.objects.filter(currency=data["portfolio"].currency).first()
|
|
@@ -19,7 +19,7 @@ class DailyPortfolioCashFlowImportHandler(ImportExportHandler):
|
|
|
19
19
|
|
|
20
20
|
def _deserialize(self, data):
|
|
21
21
|
data["value_date"] = datetime.strptime(data["value_date"], "%Y-%m-%d").date()
|
|
22
|
-
data["portfolio"] = Portfolio.
|
|
22
|
+
data["portfolio"] = Portfolio.all_objects.get(id=data["portfolio"])
|
|
23
23
|
if "cash" in data:
|
|
24
24
|
data["cash"] = Decimal(data["cash"])
|
|
25
25
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import math
|
|
1
2
|
from datetime import datetime
|
|
2
3
|
from decimal import Decimal
|
|
3
4
|
from typing import Any, Dict, Optional
|
|
@@ -71,7 +72,10 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
71
72
|
|
|
72
73
|
for field in self.model._meta.get_fields():
|
|
73
74
|
if not (value := data.get(field.name, None)) is None and isinstance(field, models.DecimalField):
|
|
74
|
-
|
|
75
|
+
q = (
|
|
76
|
+
1 / (math.pow(10, 4))
|
|
77
|
+
) # we need that convertion mechanism otherwise there is floating point approximation error while casting to decimal and get_instance does not work as expected
|
|
78
|
+
data[field.name] = Decimal(value).quantize(Decimal(str(q)))
|
|
75
79
|
|
|
76
80
|
def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
|
|
77
81
|
if "transaction_date" not in data: # we might get only book date and not transaction date
|
|
@@ -81,7 +85,6 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
81
85
|
|
|
82
86
|
def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
|
|
83
87
|
self.import_source.log += "\nGet Trade Instance."
|
|
84
|
-
|
|
85
88
|
if transaction_date := data.get("transaction_date"):
|
|
86
89
|
dates_lookup = {"transaction_date": transaction_date}
|
|
87
90
|
elif book_date := data.get("book_date"):
|
|
@@ -29,7 +29,7 @@ def get_portfolio_id(row: pd.Series) -> int:
|
|
|
29
29
|
Raises: Portfolio.DoesNotExist: We raise an error intentionally if the portfolio does not exist to make the import fail
|
|
30
30
|
"""
|
|
31
31
|
iban = str(IBAN.generate(BANK_COUNTRY_CODE, bank_code=BANK_CODE, account_code=str(row["account_number"])))
|
|
32
|
-
return Portfolio.
|
|
32
|
+
return Portfolio.all_objects.get(bank_accounts__iban=iban).id
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def parse(import_source: "ImportSource") -> dict:
|
|
@@ -13,7 +13,7 @@ def parse(import_source):
|
|
|
13
13
|
|
|
14
14
|
def get_portfolio_id(row) -> int | None:
|
|
15
15
|
with suppress(Portfolio.DoesNotExist):
|
|
16
|
-
return Portfolio.
|
|
16
|
+
return Portfolio.all_objects.get(instruments__children__isin__in=[row["Isin"]]).pk
|
|
17
17
|
|
|
18
18
|
df = df[~df["Isin"].isnull()]
|
|
19
19
|
df["Trade date"] = df["Trade date"].ffill()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Generated by Django 5.0.13 on 2025-03-21 12:30
|
|
2
|
+
|
|
3
|
+
import django.db.models.expressions
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
def migrate_default_values(apps, schema_editor):
|
|
8
|
+
Transaction = apps.get_model("wbportfolio", "Transaction")
|
|
9
|
+
Trade = apps.get_model("wbportfolio", "Trade")
|
|
10
|
+
Expiry = apps.get_model("wbportfolio", "Expiry")
|
|
11
|
+
DividendTransaction = apps.get_model("wbportfolio", "DividendTransaction")
|
|
12
|
+
Transaction.objects.filter(book_date__isnull=True).update(book_date=models.F("transaction_date"))
|
|
13
|
+
Transaction.objects.filter(value_date__isnull=True).update(value_date=models.F("transaction_date"))
|
|
14
|
+
Trade.objects.filter(price_gross__isnull=True).update(price_gross=models.F("price"))
|
|
15
|
+
Expiry.objects.filter(price_gross__isnull=True).update(price_gross=models.F("price"))
|
|
16
|
+
DividendTransaction.objects.filter(price_gross__isnull=True).update(price_gross=models.F("price"))
|
|
17
|
+
Transaction.objects.filter(total_value_gross__isnull=True).update(total_value_gross=models.F("total_value"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Migration(migrations.Migration):
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
('wbportfolio', '0075_portfolio_initial_position_date_and_more'),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
operations = [
|
|
27
|
+
migrations.RunPython(migrate_default_values),
|
|
28
|
+
migrations.AlterField(
|
|
29
|
+
model_name='transaction',
|
|
30
|
+
name='book_date',
|
|
31
|
+
field=models.DateField(help_text='The date that this transaction was booked.',
|
|
32
|
+
verbose_name='Trade Date'),
|
|
33
|
+
preserve_default=False,
|
|
34
|
+
),
|
|
35
|
+
migrations.AlterField(
|
|
36
|
+
model_name='transaction',
|
|
37
|
+
name='value_date',
|
|
38
|
+
field=models.DateField(help_text='The date that this transaction was valuated.',
|
|
39
|
+
verbose_name='Value Date'),
|
|
40
|
+
preserve_default=False,
|
|
41
|
+
),
|
|
42
|
+
migrations.AlterField(
|
|
43
|
+
model_name='dividendtransaction',
|
|
44
|
+
name='price',
|
|
45
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The price per share.', max_digits=16, verbose_name='Price'),
|
|
46
|
+
),
|
|
47
|
+
migrations.AlterField(
|
|
48
|
+
model_name='dividendtransaction',
|
|
49
|
+
name='price_gross',
|
|
50
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The gross price per share.', max_digits=16, verbose_name='Gross Price'),
|
|
51
|
+
preserve_default=False,
|
|
52
|
+
),
|
|
53
|
+
migrations.AlterField(
|
|
54
|
+
model_name='dividendtransaction',
|
|
55
|
+
name='shares',
|
|
56
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The number of shares that were traded.', max_digits=15, verbose_name='Shares'),
|
|
57
|
+
),
|
|
58
|
+
migrations.AlterField(
|
|
59
|
+
model_name='expiry',
|
|
60
|
+
name='price',
|
|
61
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The price per share.', max_digits=16, verbose_name='Price'),
|
|
62
|
+
),
|
|
63
|
+
migrations.AlterField(
|
|
64
|
+
model_name='expiry',
|
|
65
|
+
name='price_gross',
|
|
66
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The gross price per share.', max_digits=16, verbose_name='Gross Price'),
|
|
67
|
+
preserve_default=False,
|
|
68
|
+
),
|
|
69
|
+
migrations.AlterField(
|
|
70
|
+
model_name='expiry',
|
|
71
|
+
name='shares',
|
|
72
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The number of shares that were traded.', max_digits=15, verbose_name='Shares'),
|
|
73
|
+
),
|
|
74
|
+
migrations.AlterField(
|
|
75
|
+
model_name='trade',
|
|
76
|
+
name='price',
|
|
77
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The price per share.', max_digits=16, verbose_name='Price'),
|
|
78
|
+
),
|
|
79
|
+
migrations.AlterField(
|
|
80
|
+
model_name='trade',
|
|
81
|
+
name='price_gross',
|
|
82
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The gross price per share.', max_digits=16, verbose_name='Gross Price'),
|
|
83
|
+
preserve_default=False,
|
|
84
|
+
),
|
|
85
|
+
migrations.AlterField(
|
|
86
|
+
model_name='trade',
|
|
87
|
+
name='shares',
|
|
88
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The number of shares that were traded.', max_digits=15, verbose_name='Shares'),
|
|
89
|
+
),
|
|
90
|
+
|
|
91
|
+
migrations.AlterField(
|
|
92
|
+
model_name='transaction',
|
|
93
|
+
name='total_value',
|
|
94
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), max_digits=20, verbose_name='Total Value'),
|
|
95
|
+
preserve_default=False,
|
|
96
|
+
),
|
|
97
|
+
migrations.AlterField(
|
|
98
|
+
model_name='transaction',
|
|
99
|
+
name='total_value_gross',
|
|
100
|
+
field=models.DecimalField(decimal_places=4, default=Decimal('0.0'), max_digits=20, verbose_name='Total Value Gross'),
|
|
101
|
+
preserve_default=False,
|
|
102
|
+
),
|
|
103
|
+
migrations.RemoveField(
|
|
104
|
+
model_name='transaction',
|
|
105
|
+
name='total_value_fx_portfolio',
|
|
106
|
+
),
|
|
107
|
+
migrations.RemoveField(
|
|
108
|
+
model_name='transaction',
|
|
109
|
+
name='total_value_gross_fx_portfolio',
|
|
110
|
+
),
|
|
111
|
+
migrations.AddField(
|
|
112
|
+
model_name='transaction',
|
|
113
|
+
name='total_value_fx_portfolio',
|
|
114
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(
|
|
115
|
+
models.F('currency_fx_rate'), '*', models.F('total_value')),
|
|
116
|
+
output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
117
|
+
preserve_default=False,
|
|
118
|
+
),
|
|
119
|
+
migrations.AddField(
|
|
120
|
+
model_name='transaction',
|
|
121
|
+
name='total_value_gross_fx_portfolio',
|
|
122
|
+
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('total_value_gross')), output_field=models.DecimalField(decimal_places=4, max_digits=20)),
|
|
123
|
+
preserve_default=False,
|
|
124
|
+
),
|
|
125
|
+
|
|
126
|
+
]
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -411,7 +411,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
411
411
|
)
|
|
412
412
|
|
|
413
413
|
def __str__(self):
|
|
414
|
-
return f"{self.id:06}
|
|
414
|
+
return f"{self.id:06}: {self.name}"
|
|
415
415
|
|
|
416
416
|
class Meta:
|
|
417
417
|
verbose_name = "Portfolio"
|
|
@@ -853,7 +853,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
853
853
|
adjusted_weighting,
|
|
854
854
|
adjusted_currency_fx_rate,
|
|
855
855
|
adjusted_is_estimated,
|
|
856
|
-
portfolio_created=None,
|
|
857
856
|
path=None,
|
|
858
857
|
):
|
|
859
858
|
if not path:
|
|
@@ -866,7 +865,18 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
866
865
|
position.is_estimated = (adjusted_is_estimated or position.is_estimated) and not (
|
|
867
866
|
position.weighting == 1.0
|
|
868
867
|
)
|
|
869
|
-
position
|
|
868
|
+
# to get from which portfolio this position is created, we need to differantiate between:
|
|
869
|
+
# * Composition portfolio: where the portfolio created is the second encountered portfolio
|
|
870
|
+
# * Other: portfolio created is the last encountered portfolio
|
|
871
|
+
# If `path` is empty, we use None as portfolio_created
|
|
872
|
+
try:
|
|
873
|
+
if self.is_composition:
|
|
874
|
+
position.portfolio_created = path[1]
|
|
875
|
+
else:
|
|
876
|
+
position.portfolio_created = path[-1]
|
|
877
|
+
except IndexError:
|
|
878
|
+
position.portfolio_created = None
|
|
879
|
+
|
|
870
880
|
setattr(position, "path", path)
|
|
871
881
|
position.initial_shares = None
|
|
872
882
|
if portfolio_total_asset_value:
|
|
@@ -881,7 +891,6 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
881
891
|
position.weighting,
|
|
882
892
|
position.currency_fx_rate,
|
|
883
893
|
position.is_estimated,
|
|
884
|
-
portfolio_created=child_portfolio,
|
|
885
894
|
path=path.copy(),
|
|
886
895
|
)
|
|
887
896
|
elif position.weighting: # we do not yield position with weight 0 because of issue with certain multi-thematic portfolios which contain duplicates
|
|
@@ -979,7 +988,11 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
979
988
|
for position in positions:
|
|
980
989
|
position.portfolio = self
|
|
981
990
|
update_dates.add(position.date)
|
|
982
|
-
|
|
991
|
+
|
|
992
|
+
# we need to delete the existing estimated portfolio because otherwise we risk to have existing and not
|
|
993
|
+
# overlapping positions remaining (as they will not be updating by the bulk create). E.g. when someone
|
|
994
|
+
# change completely the trades of a portfolio model and drift it.
|
|
995
|
+
self.assets.filter(date__in=update_dates, is_estimated=True).delete()
|
|
983
996
|
leftover_positions_ids = list(
|
|
984
997
|
self.assets.filter(date__in=update_dates).values_list("id", flat=True)
|
|
985
998
|
) # we need to get the ids otherwise the queryset is reevaluated later
|
|
@@ -1014,7 +1027,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
|
|
|
1014
1027
|
@classmethod
|
|
1015
1028
|
def _get_or_create_portfolio(cls, instrument_handler, portfolio_data):
|
|
1016
1029
|
if isinstance(portfolio_data, int):
|
|
1017
|
-
return Portfolio.
|
|
1030
|
+
return Portfolio.all_objects.get(id=portfolio_data)
|
|
1018
1031
|
instrument = portfolio_data
|
|
1019
1032
|
if isinstance(portfolio_data, dict):
|
|
1020
1033
|
instrument = instrument_handler.process_object(instrument, only_security=False, read_only=True)[0]
|
|
@@ -14,6 +14,7 @@ from django.db.models import (
|
|
|
14
14
|
)
|
|
15
15
|
from django.db.models.functions import Greatest
|
|
16
16
|
from django.dispatch import receiver
|
|
17
|
+
from django.utils.translation import gettext_lazy as _
|
|
17
18
|
from django_fsm import FSMField, transition
|
|
18
19
|
from wbcore.contrib.ai.llm.config import add_llm_prompt
|
|
19
20
|
from wbcore.contrib.authentication.models import User
|
|
@@ -335,13 +336,13 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
335
336
|
errors = dict()
|
|
336
337
|
|
|
337
338
|
if not self.trade:
|
|
338
|
-
errors["trade"] = ["With this status, this has to be provided."]
|
|
339
|
+
errors["trade"] = [_("With this status, this has to be provided.")]
|
|
339
340
|
|
|
340
341
|
if not self.product:
|
|
341
|
-
errors["product"] = ["With this status, this has to be provided."]
|
|
342
|
+
errors["product"] = [_("With this status, this has to be provided.")]
|
|
342
343
|
|
|
343
344
|
if not self.account:
|
|
344
|
-
errors["account"] = ["With this status, this has to be provided."]
|
|
345
|
+
errors["account"] = [_("With this status, this has to be provided.")]
|
|
345
346
|
|
|
346
347
|
# check if the specified product have a valid nav at the specified date
|
|
347
348
|
if (
|
|
@@ -350,13 +351,13 @@ class Claim(ReferenceIDMixin, WBModel):
|
|
|
350
351
|
and not product.valuations.filter(date=claim_date).exists()
|
|
351
352
|
):
|
|
352
353
|
if (prices_qs := product.valuations.filter(date__lt=claim_date)).exists():
|
|
353
|
-
errors["date"] =
|
|
354
|
+
errors["date"] = [
|
|
354
355
|
f"For product {product.name}, the latest valid valuation date before {claim_date:%Y-%m-%d} is {prices_qs.latest('date').date:%Y-%m-%d}: Please select a valid date."
|
|
355
|
-
|
|
356
|
+
]
|
|
356
357
|
else:
|
|
357
|
-
errors["date"] =
|
|
358
|
+
errors["date"] = [
|
|
358
359
|
f"There is no valuation before {claim_date:%Y-%m-%d} for product {product.name}: Please select a valid date."
|
|
359
|
-
|
|
360
|
+
]
|
|
360
361
|
return errors
|
|
361
362
|
|
|
362
363
|
@transition(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
1
3
|
from django.db import models
|
|
2
4
|
|
|
3
5
|
from wbportfolio.import_export.handlers.dividend import DividendImportHandler
|
|
@@ -10,23 +12,4 @@ class DividendTransaction(Transaction, ShareMixin, models.Model):
|
|
|
10
12
|
retrocession = models.FloatField(default=1)
|
|
11
13
|
|
|
12
14
|
def save(self, *args, **kwargs):
|
|
13
|
-
|
|
14
|
-
self.shares is not None
|
|
15
|
-
and self.price is not None
|
|
16
|
-
and self.retrocession is not None
|
|
17
|
-
and self.total_value is None
|
|
18
|
-
):
|
|
19
|
-
self.total_value = self.shares * self.price * self.retrocession
|
|
20
|
-
|
|
21
|
-
if self.price is not None and self.price_gross is None:
|
|
22
|
-
self.price_gross = self.price
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
self.price_gross is not None
|
|
26
|
-
and self.retrocession is not None
|
|
27
|
-
and self.shares is not None
|
|
28
|
-
and self.total_value_gross is None
|
|
29
|
-
):
|
|
30
|
-
self.total_value_gross = self.shares * self.price_gross * self.retrocession
|
|
31
|
-
|
|
32
|
-
super().save(*args, **kwargs)
|
|
15
|
+
super().save(*args, factor=Decimal(self.retrocession), **kwargs)
|
|
@@ -8,6 +8,7 @@ from celery import shared_task
|
|
|
8
8
|
from django.core.exceptions import ValidationError
|
|
9
9
|
from django.db import models
|
|
10
10
|
from django.utils.functional import cached_property
|
|
11
|
+
from django.utils.translation import gettext_lazy as _
|
|
11
12
|
from django_fsm import FSMField, transition
|
|
12
13
|
from pandas._libs.tslibs.offsets import BDay
|
|
13
14
|
from wbcompliance.models.risk_management.mixins import RiskCheckMixin
|
|
@@ -166,7 +167,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
166
167
|
Returns:
|
|
167
168
|
The cloned trade proposal
|
|
168
169
|
"""
|
|
169
|
-
trade_date = kwargs.get("
|
|
170
|
+
trade_date = kwargs.get("clone_date", self.trade_date)
|
|
170
171
|
|
|
171
172
|
# Find the next valid trade date
|
|
172
173
|
while TradeProposal.objects.filter(portfolio=self.portfolio, trade_date=trade_date).exists():
|
|
@@ -174,7 +175,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
174
175
|
|
|
175
176
|
trade_proposal_clone = TradeProposal.objects.create(
|
|
176
177
|
trade_date=trade_date,
|
|
177
|
-
comment=kwargs.get("
|
|
178
|
+
comment=kwargs.get("clone_comment", self.comment),
|
|
178
179
|
status=TradeProposal.Status.DRAFT,
|
|
179
180
|
rebalancing_model=self.rebalancing_model,
|
|
180
181
|
portfolio=self.portfolio,
|
|
@@ -228,8 +229,6 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
228
229
|
"""
|
|
229
230
|
Will delete all existing trades and recreate them from the method `create_or_update_trades`
|
|
230
231
|
"""
|
|
231
|
-
if self.status != TradeProposal.Status.DRAFT:
|
|
232
|
-
raise ValueError("Cannot reset non-draft trade proposal. Revert this trade proposal first.")
|
|
233
232
|
# delete all existing trades
|
|
234
233
|
last_effective_date = self.last_effective_date
|
|
235
234
|
# Get effective and target portfolio
|
|
@@ -303,7 +302,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
303
302
|
)
|
|
304
303
|
last_trade_proposal = overriding_trade_proposal or next_trade_proposal
|
|
305
304
|
|
|
306
|
-
def get_estimated_shares(self, weight: Decimal, underlying_quote: Instrument) -> Decimal
|
|
305
|
+
def get_estimated_shares(self, weight: Decimal, underlying_quote: Instrument) -> Decimal:
|
|
307
306
|
"""
|
|
308
307
|
Estimates the number of shares for a trade based on the given weight and underlying quote.
|
|
309
308
|
|
|
@@ -332,8 +331,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
332
331
|
if price_fx_portfolio:
|
|
333
332
|
return trade_total_value_fx_portfolio / price_fx_portfolio
|
|
334
333
|
except Exception:
|
|
335
|
-
|
|
336
|
-
return None
|
|
334
|
+
raise ValueError("We couldn't estimate the number of shares")
|
|
337
335
|
|
|
338
336
|
def get_estimated_target_cash(self, currency: Currency) -> tuple[Decimal, Decimal]:
|
|
339
337
|
"""
|
|
@@ -373,7 +371,8 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
373
371
|
)[0]
|
|
374
372
|
|
|
375
373
|
# Estimate the target shares for the cash component
|
|
376
|
-
|
|
374
|
+
with suppress(ValueError):
|
|
375
|
+
total_target_shares = self.get_estimated_shares(target_cash_weight, cash_component)
|
|
377
376
|
|
|
378
377
|
return target_cash_weight, total_target_shares
|
|
379
378
|
|
|
@@ -428,7 +427,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
428
427
|
errors = dict()
|
|
429
428
|
errors_list = []
|
|
430
429
|
if self.trades.exists() and self.trades.exclude(status=Trade.Status.DRAFT).exists():
|
|
431
|
-
errors_list.append("All trades need to be draft before submitting")
|
|
430
|
+
errors_list.append(_("All trades need to be draft before submitting"))
|
|
432
431
|
service = self.validated_trading_service
|
|
433
432
|
try:
|
|
434
433
|
service.is_valid(ignore_error=True)
|
|
@@ -437,7 +436,7 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
437
436
|
# "There is no change detected in this trade proposal. Please submit at last one valid trade"
|
|
438
437
|
# )
|
|
439
438
|
if len(service.validated_trades) == 0:
|
|
440
|
-
errors_list.append("There is no valid trade on this proposal")
|
|
439
|
+
errors_list.append(_("There is no valid trade on this proposal"))
|
|
441
440
|
if service.errors:
|
|
442
441
|
errors_list.extend(service.errors)
|
|
443
442
|
if errors_list:
|
|
@@ -487,15 +486,17 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
487
486
|
def can_approve(self):
|
|
488
487
|
errors = dict()
|
|
489
488
|
if not self.portfolio.can_be_rebalanced:
|
|
490
|
-
errors["non_field_errors"] = "The portfolio does not allow manual rebalanced"
|
|
489
|
+
errors["non_field_errors"] = [_("The portfolio does not allow manual rebalanced")]
|
|
491
490
|
if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
|
|
492
|
-
errors["non_field_errors"] =
|
|
491
|
+
errors["non_field_errors"] = [
|
|
492
|
+
_("At least one trade needs to be submitted to be able to approve this proposal")
|
|
493
|
+
]
|
|
493
494
|
if not self.portfolio.can_be_rebalanced:
|
|
494
|
-
errors["portfolio"] =
|
|
495
|
-
"The portfolio needs to be a model portfolio in order to approve this trade proposal manually"
|
|
496
|
-
|
|
495
|
+
errors["portfolio"] = [
|
|
496
|
+
[_("The portfolio needs to be a model portfolio in order to approve this trade proposal manually")]
|
|
497
|
+
]
|
|
497
498
|
if self.has_non_successful_checks:
|
|
498
|
-
errors["non_field_errors"] = "The pre trades rules did not passed successfully"
|
|
499
|
+
errors["non_field_errors"] = [_("The pre trades rules did not passed successfully")]
|
|
499
500
|
return errors
|
|
500
501
|
|
|
501
502
|
@transition(
|
|
@@ -526,7 +527,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
526
527
|
def can_deny(self):
|
|
527
528
|
errors = dict()
|
|
528
529
|
if self.trades.exclude(status=Trade.Status.SUBMIT).exists():
|
|
529
|
-
errors["non_field_errors"] =
|
|
530
|
+
errors["non_field_errors"] = [
|
|
531
|
+
_("At least one trade needs to be submitted to be able to deny this proposal")
|
|
532
|
+
]
|
|
530
533
|
return errors
|
|
531
534
|
|
|
532
535
|
@transition(
|
|
@@ -589,9 +592,9 @@ class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
589
592
|
def can_revert(self):
|
|
590
593
|
errors = dict()
|
|
591
594
|
if not self.portfolio.can_be_rebalanced:
|
|
592
|
-
errors["portfolio"] =
|
|
593
|
-
"The portfolio needs to be a model portfolio in order to revert this trade proposal manually"
|
|
594
|
-
|
|
595
|
+
errors["portfolio"] = [
|
|
596
|
+
_("The portfolio needs to be a model portfolio in order to revert this trade proposal manually")
|
|
597
|
+
]
|
|
595
598
|
return errors
|
|
596
599
|
|
|
597
600
|
# End FSM logics
|
|
@@ -20,6 +20,7 @@ from django.db.models.functions import Coalesce
|
|
|
20
20
|
from django.db.models.signals import post_save
|
|
21
21
|
from django.dispatch import receiver
|
|
22
22
|
from django.utils.functional import cached_property
|
|
23
|
+
from django.utils.translation import gettext_lazy as _
|
|
23
24
|
from django_fsm import GET_STATE, FSMField, transition
|
|
24
25
|
from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet
|
|
25
26
|
from wbcore.contrib.icons import WBIcon
|
|
@@ -320,9 +321,11 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
320
321
|
|
|
321
322
|
def can_execute(self):
|
|
322
323
|
if not self.last_underlying_quote_price:
|
|
323
|
-
return {"underlying_instrument": "Cannot execute a trade without a valid quote price"}
|
|
324
|
+
return {"underlying_instrument": [_("Cannot execute a trade without a valid quote price")]}
|
|
324
325
|
if not self.portfolio.is_manageable:
|
|
325
|
-
return {
|
|
326
|
+
return {
|
|
327
|
+
"portfolio": [_("The portfolio needs to be a model portfolio in order to execute this trade manually")]
|
|
328
|
+
}
|
|
326
329
|
|
|
327
330
|
@transition(
|
|
328
331
|
field=status,
|
|
@@ -498,22 +501,16 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
498
501
|
self.transaction_date = self.trade_proposal.trade_date
|
|
499
502
|
self.value_date = self.trade_proposal.last_effective_date
|
|
500
503
|
if not self.portfolio.only_weighting:
|
|
501
|
-
|
|
504
|
+
with suppress(ValueError):
|
|
505
|
+
self.shares = self.trade_proposal.get_estimated_shares(self.weighting, self.underlying_instrument)
|
|
502
506
|
|
|
503
507
|
if not self.custodian and self.bank:
|
|
504
508
|
self.custodian = Custodian.get_by_mapping(self.bank)
|
|
505
509
|
if self.price is None:
|
|
506
510
|
# we try to get the price if not provided directly from the underlying instrument
|
|
507
|
-
with suppress(
|
|
508
|
-
self.price = self.underlying_instrument.
|
|
509
|
-
if self.price is not None and self.price_gross is None:
|
|
510
|
-
self.price_gross = self.price
|
|
511
|
-
|
|
512
|
-
if self.price is not None and self.shares is not None and self.total_value is None:
|
|
513
|
-
self.total_value = self.price * self.shares
|
|
511
|
+
with suppress(Exception):
|
|
512
|
+
self.price = self.underlying_instrument.get_price(self.value_date)
|
|
514
513
|
|
|
515
|
-
if self.price_gross is not None and self.shares is not None and self.total_value_gross is None:
|
|
516
|
-
self.total_value_gross = self.price_gross * self.shares
|
|
517
514
|
self.transaction_type = Transaction.Type.TRADE
|
|
518
515
|
|
|
519
516
|
if self.transaction_subtype is None or self.trade_proposal:
|
|
@@ -13,16 +13,16 @@ class ShareMixin(models.Model):
|
|
|
13
13
|
shares = models.DecimalField(
|
|
14
14
|
max_digits=15,
|
|
15
15
|
decimal_places=4,
|
|
16
|
-
|
|
17
|
-
blank=True,
|
|
16
|
+
default=Decimal("0.0"),
|
|
18
17
|
help_text="The number of shares that were traded.",
|
|
19
18
|
verbose_name="Shares",
|
|
20
19
|
)
|
|
21
20
|
price = models.DecimalField(
|
|
22
21
|
max_digits=16,
|
|
23
22
|
decimal_places=4,
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
default=Decimal(
|
|
24
|
+
"0.0"
|
|
25
|
+
), # we shouldn't default to anything but we have trade with price=None. Needs to be handled carefully
|
|
26
26
|
help_text="The price per share.",
|
|
27
27
|
verbose_name="Price",
|
|
28
28
|
)
|
|
@@ -30,12 +30,23 @@ class ShareMixin(models.Model):
|
|
|
30
30
|
price_gross = models.DecimalField(
|
|
31
31
|
max_digits=16,
|
|
32
32
|
decimal_places=4,
|
|
33
|
-
null=True,
|
|
34
|
-
blank=True,
|
|
35
33
|
help_text="The gross price per share.",
|
|
36
34
|
verbose_name="Gross Price",
|
|
37
35
|
)
|
|
38
36
|
|
|
37
|
+
def save(
|
|
38
|
+
self,
|
|
39
|
+
*args,
|
|
40
|
+
factor: Decimal = Decimal("1.0"),
|
|
41
|
+
**kwargs,
|
|
42
|
+
):
|
|
43
|
+
if self.price_gross is None:
|
|
44
|
+
self.price_gross = self.price
|
|
45
|
+
|
|
46
|
+
self.total_value = self.price * self.shares * factor
|
|
47
|
+
self.total_value_gross = self.price_gross * self.shares * factor
|
|
48
|
+
super().save(*args, **kwargs)
|
|
49
|
+
|
|
39
50
|
class Meta:
|
|
40
51
|
abstract = True
|
|
41
52
|
|
|
@@ -68,14 +79,10 @@ class Transaction(ImportMixin, models.Model):
|
|
|
68
79
|
help_text="The date that this transaction was traded.",
|
|
69
80
|
)
|
|
70
81
|
book_date = models.DateField(
|
|
71
|
-
null=True,
|
|
72
|
-
blank=True,
|
|
73
82
|
verbose_name="Trade Date",
|
|
74
83
|
help_text="The date that this transaction was booked.",
|
|
75
84
|
)
|
|
76
85
|
value_date = models.DateField(
|
|
77
|
-
null=True,
|
|
78
|
-
blank=True,
|
|
79
86
|
verbose_name="Value Date",
|
|
80
87
|
help_text="The date that this transaction was valuated.",
|
|
81
88
|
)
|
|
@@ -89,17 +96,23 @@ class Transaction(ImportMixin, models.Model):
|
|
|
89
96
|
currency_fx_rate = models.DecimalField(
|
|
90
97
|
max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
|
|
91
98
|
)
|
|
92
|
-
total_value = models.DecimalField(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
total_value = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value")
|
|
100
|
+
total_value_gross = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value Gross")
|
|
101
|
+
total_value_fx_portfolio = models.GeneratedField(
|
|
102
|
+
expression=models.F("currency_fx_rate") * models.F("total_value"),
|
|
103
|
+
output_field=models.DecimalField(
|
|
104
|
+
max_digits=20,
|
|
105
|
+
decimal_places=4,
|
|
106
|
+
),
|
|
107
|
+
db_persist=True,
|
|
108
|
+
)
|
|
109
|
+
total_value_gross_fx_portfolio = models.GeneratedField(
|
|
110
|
+
expression=models.F("currency_fx_rate") * models.F("total_value_gross"),
|
|
111
|
+
output_field=models.DecimalField(
|
|
112
|
+
max_digits=20,
|
|
113
|
+
decimal_places=4,
|
|
114
|
+
),
|
|
115
|
+
db_persist=True,
|
|
103
116
|
)
|
|
104
117
|
external_id = models.CharField(
|
|
105
118
|
max_length=255,
|
|
@@ -118,28 +131,15 @@ class Transaction(ImportMixin, models.Model):
|
|
|
118
131
|
|
|
119
132
|
if not getattr(self, "currency", None) and self.underlying_instrument:
|
|
120
133
|
self.currency = self.underlying_instrument.currency
|
|
121
|
-
if
|
|
134
|
+
if self.currency_fx_rate is None:
|
|
122
135
|
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
123
136
|
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
124
137
|
)
|
|
125
138
|
if not self.transaction_type:
|
|
126
139
|
self.transaction_type = self.__class__.__name__
|
|
127
|
-
if (
|
|
128
|
-
self.total_value is not None
|
|
129
|
-
and self.currency_fx_rate is not None
|
|
130
|
-
and self.total_value_fx_portfolio is None
|
|
131
|
-
):
|
|
132
|
-
self.total_value_fx_portfolio = self.total_value * self.currency_fx_rate
|
|
133
|
-
|
|
134
|
-
if self.total_value is not None and self.total_value_gross is None and self.total_value_gross is None:
|
|
135
|
-
self.total_value_gross = self.total_value
|
|
136
140
|
|
|
137
|
-
if
|
|
138
|
-
self.
|
|
139
|
-
and self.total_value_gross is not None
|
|
140
|
-
and self.total_value_gross_fx_portfolio is None
|
|
141
|
-
):
|
|
142
|
-
self.total_value_gross_fx_portfolio = self.total_value_gross * self.currency_fx_rate
|
|
141
|
+
if self.total_value_gross is None:
|
|
142
|
+
self.total_value_gross = self.total_value
|
|
143
143
|
|
|
144
144
|
super().save(*args, **kwargs)
|
|
145
145
|
|
|
@@ -2,13 +2,12 @@ from decimal import Decimal
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
|
-
from django.db.models import
|
|
5
|
+
from django.db.models import QuerySet
|
|
6
6
|
from wbfdm.enums import MarketData
|
|
7
7
|
from wbfdm.models import (
|
|
8
8
|
Classification,
|
|
9
9
|
Exchange,
|
|
10
10
|
Instrument,
|
|
11
|
-
InstrumentClassificationThroughModel,
|
|
12
11
|
InstrumentListThroughModel,
|
|
13
12
|
)
|
|
14
13
|
|
|
@@ -31,9 +30,11 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
31
30
|
target_currency=self.TARGET_CURRENCY,
|
|
32
31
|
)
|
|
33
32
|
)
|
|
34
|
-
self.exchange_df =
|
|
35
|
-
instruments.values_list("id", "exchange"), columns=["id", "exchange"]
|
|
36
|
-
|
|
33
|
+
self.exchange_df = (
|
|
34
|
+
pd.DataFrame(instruments.values_list("id", "exchange"), columns=["id", "exchange"])
|
|
35
|
+
.set_index("id")
|
|
36
|
+
.replace({np.nan: None})
|
|
37
|
+
)
|
|
37
38
|
instrument_ids = list(instruments.values_list("id", flat=True))
|
|
38
39
|
try:
|
|
39
40
|
self.market_cap_df = (
|
|
@@ -41,6 +42,7 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
41
42
|
.set_index("instrument_id")["market_capitalization"]
|
|
42
43
|
.reindex(instrument_ids)
|
|
43
44
|
)
|
|
45
|
+
self.market_cap_df = self.market_cap_df
|
|
44
46
|
except (IndexError, KeyError):
|
|
45
47
|
self.market_cap_df = pd.Series(dtype="float64", index=instrument_ids)
|
|
46
48
|
|
|
@@ -59,17 +61,10 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
59
61
|
if not instrument_ids:
|
|
60
62
|
instrument_ids = []
|
|
61
63
|
if classification_ids:
|
|
62
|
-
classifications = set()
|
|
63
64
|
for classification in Classification.objects.filter(id__in=classification_ids):
|
|
64
65
|
for children in classification.get_descendants(include_self=True):
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
list(
|
|
68
|
-
InstrumentClassificationThroughModel.objects.filter(
|
|
69
|
-
classification__in=classifications
|
|
70
|
-
).values_list("id", flat=True)
|
|
71
|
-
)
|
|
72
|
-
)
|
|
66
|
+
for company in children.instruments.all():
|
|
67
|
+
instrument_ids.append(company.get_primary_quote().id)
|
|
73
68
|
if instrument_list_id:
|
|
74
69
|
instrument_ids.extend(
|
|
75
70
|
list(
|
|
@@ -84,8 +79,10 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
84
79
|
self.portfolio.assets.filter(date=self.trade_date).values_list("underlying_instrument", flat=True)
|
|
85
80
|
)
|
|
86
81
|
|
|
87
|
-
return
|
|
88
|
-
(
|
|
82
|
+
return (
|
|
83
|
+
Instrument.objects.filter(id__in=instrument_ids)
|
|
84
|
+
.filter(exchange__isnull=False)
|
|
85
|
+
.filter_active_at_date(self.trade_date)
|
|
89
86
|
)
|
|
90
87
|
|
|
91
88
|
def is_valid(self) -> bool:
|
|
@@ -85,7 +85,7 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
85
85
|
request=request,
|
|
86
86
|
)
|
|
87
87
|
if user.profile.is_internal:
|
|
88
|
-
additional_resources["
|
|
88
|
+
additional_resources["instruments_list"] = reverse(
|
|
89
89
|
"wbportfolio:portfolio-instrument-list",
|
|
90
90
|
args=[instance.id],
|
|
91
91
|
request=request,
|
|
@@ -33,7 +33,7 @@ class TestPortfolioModel(PortfolioTestMixin):
|
|
|
33
33
|
assert portfolio.id is not None
|
|
34
34
|
|
|
35
35
|
def test_str(self, portfolio):
|
|
36
|
-
assert str(portfolio) == f"{portfolio.id:06}
|
|
36
|
+
assert str(portfolio) == f"{portfolio.id:06}: {portfolio.name}"
|
|
37
37
|
|
|
38
38
|
def test_get_assets(self, portfolio, product, cash, asset_position_factory):
|
|
39
39
|
asset_position_factory.create_batch(4, portfolio=portfolio, underlying_instrument=product)
|
|
@@ -136,8 +136,8 @@ class TestMarketCapitalizationRebalancing:
|
|
|
136
136
|
|
|
137
137
|
last_effective_date = (weekday - BDay(1)).date()
|
|
138
138
|
|
|
139
|
-
i1 = instrument_factory()
|
|
140
|
-
i2 = instrument_factory()
|
|
139
|
+
i1 = instrument_factory(inception_date=weekday)
|
|
140
|
+
i2 = instrument_factory(inception_date=weekday)
|
|
141
141
|
instrument_price_factory.create(instrument=i1, date=weekday)
|
|
142
142
|
instrument_price_factory.create(instrument=i2, date=weekday)
|
|
143
143
|
return MarketCapitalizationRebalancing(portfolio, weekday, last_effective_date, instrument_ids=[i1.id, i2.id])
|
wbportfolio/viewsets/assets.py
CHANGED
|
@@ -323,7 +323,7 @@ class CashPositionPortfolioPandasAPIView(
|
|
|
323
323
|
val_date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
324
324
|
else:
|
|
325
325
|
val_date = date.today()
|
|
326
|
-
active_products = Product.active_objects.
|
|
326
|
+
active_products = Product.active_objects.filter_active_at_date(val_date)
|
|
327
327
|
return (
|
|
328
328
|
super()
|
|
329
329
|
.get_queryset()
|
|
@@ -148,17 +148,6 @@ class AssetPositionInstrumentDisplayConfig(DisplayViewConfig):
|
|
|
148
148
|
items=[dp.LegendItem(icon=WBIcon.UNFILTER.icon, label="Not Invested", value=True)],
|
|
149
149
|
)
|
|
150
150
|
],
|
|
151
|
-
formatting=[
|
|
152
|
-
dp.Formatting(
|
|
153
|
-
column="is_invested",
|
|
154
|
-
formatting_rules=[
|
|
155
|
-
dp.FormattingRule(
|
|
156
|
-
icon=WBIcon.UNFILTER.icon,
|
|
157
|
-
condition=("==", False),
|
|
158
|
-
)
|
|
159
|
-
],
|
|
160
|
-
)
|
|
161
|
-
],
|
|
162
151
|
)
|
|
163
152
|
|
|
164
153
|
|
|
@@ -103,8 +103,8 @@ class PortfolioDisplayConfig(DisplayViewConfig):
|
|
|
103
103
|
create_simple_section(
|
|
104
104
|
"instruments_section",
|
|
105
105
|
_("Linked Instruments"),
|
|
106
|
-
[["
|
|
107
|
-
"
|
|
106
|
+
[["instruments_list"]],
|
|
107
|
+
"instruments_list",
|
|
108
108
|
collapsed=True,
|
|
109
109
|
),
|
|
110
110
|
create_simple_section(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from django.utils.translation import gettext_lazy as _
|
|
4
|
-
from wbcore.contrib.icons import WBIcon
|
|
5
4
|
from wbcore.enums import Unit
|
|
6
5
|
from wbcore.metadata.configs import display as dp
|
|
7
6
|
from wbcore.metadata.configs.display.formatting import Condition, Operator
|
|
@@ -27,18 +26,6 @@ class ProductDisplayConfig(DisplayViewConfig):
|
|
|
27
26
|
label=_("Information"),
|
|
28
27
|
open_by_default=False,
|
|
29
28
|
children=[
|
|
30
|
-
dp.Field(
|
|
31
|
-
key="is_invested",
|
|
32
|
-
label="",
|
|
33
|
-
formatting_rules=[
|
|
34
|
-
dp.FormattingRule(
|
|
35
|
-
icon=WBIcon.UNFILTER.icon,
|
|
36
|
-
condition=Condition(Operator("=="), False),
|
|
37
|
-
),
|
|
38
|
-
],
|
|
39
|
-
width=30,
|
|
40
|
-
show="open",
|
|
41
|
-
),
|
|
42
29
|
dp.Field(key="name_repr", label="Name", width=250),
|
|
43
30
|
dp.Field(key="parent", label="Parent"),
|
|
44
31
|
dp.Field(key="isin", label="ISIN"),
|
|
@@ -267,15 +267,6 @@ class SubscriptionRedemptionDisplayConfig(TradeDisplayConfig):
|
|
|
267
267
|
)
|
|
268
268
|
],
|
|
269
269
|
),
|
|
270
|
-
dp.Formatting(
|
|
271
|
-
column="pending",
|
|
272
|
-
formatting_rules=[
|
|
273
|
-
dp.FormattingRule(
|
|
274
|
-
icon=WBIcon.FOLDERS_ADD.icon,
|
|
275
|
-
condition=("==", True),
|
|
276
|
-
)
|
|
277
|
-
],
|
|
278
|
-
),
|
|
279
270
|
],
|
|
280
271
|
)
|
|
281
272
|
|
|
@@ -311,6 +302,7 @@ class TradeTradeProposalDisplayConfig(DisplayViewConfig):
|
|
|
311
302
|
dp.Field(
|
|
312
303
|
key="underlying_instrument_refinitiv_identifier_code", label="RIC", width=Unit.PIXEL(100)
|
|
313
304
|
),
|
|
305
|
+
dp.Field(key="underlying_instrument_instrument_type", label="Asset Class", width=Unit.PIXEL(125)),
|
|
314
306
|
],
|
|
315
307
|
),
|
|
316
308
|
dp.Field(
|
|
@@ -3,6 +3,12 @@ from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class TransactionEndpointConfig(EndpointViewConfig):
|
|
6
|
+
def get_endpoint(self, **kwargs):
|
|
7
|
+
return None
|
|
8
|
+
|
|
9
|
+
def get_list_endpoint(self, **kwargs):
|
|
10
|
+
return reverse("wbportfolio:transaction-list", request=self.request)
|
|
11
|
+
|
|
6
12
|
def get_instance_endpoint(self, **kwargs):
|
|
7
13
|
model = "{{transaction_url_type}}"
|
|
8
14
|
return f"{self.request.scheme}://{self.request.get_host()}/api/portfolio/{model}/"
|
|
@@ -67,7 +67,23 @@ class PortfolioModelViewSet(UserPortfolioRequestPermissionMixin, InternalUserPer
|
|
|
67
67
|
queryset = Portfolio.objects.all()
|
|
68
68
|
|
|
69
69
|
search_fields = ("currency__key", "name")
|
|
70
|
-
ordering_fields =
|
|
70
|
+
ordering_fields = (
|
|
71
|
+
"name",
|
|
72
|
+
"currency",
|
|
73
|
+
"hedged_currency",
|
|
74
|
+
"updated_at",
|
|
75
|
+
"initial_position_date",
|
|
76
|
+
"last_position_date",
|
|
77
|
+
"last_asset_under_management_usd",
|
|
78
|
+
"last_positions",
|
|
79
|
+
"automatic_rebalancer",
|
|
80
|
+
"last_trade_proposal_date",
|
|
81
|
+
"is_manageable",
|
|
82
|
+
"is_tracked",
|
|
83
|
+
"only_weighting",
|
|
84
|
+
"is_lookthrough",
|
|
85
|
+
"is_composition",
|
|
86
|
+
)
|
|
71
87
|
ordering = ["name"]
|
|
72
88
|
|
|
73
89
|
display_config_class = PortfolioDisplayConfig
|
|
@@ -65,16 +65,18 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
65
65
|
# 2 methods to parametrize the clone button functionality
|
|
66
66
|
def get_clone_button_serializer_class(self, instance):
|
|
67
67
|
class CloneSerializer(wb_serializers.Serializer):
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
clone_date = wb_serializers.DateField(
|
|
69
|
+
default=(instance.trade_date + BDay(1)).date(), label="Trade Date"
|
|
70
|
+
) # we need to change the field name from the trade proposa fields, otherwise fontend conflicts
|
|
71
|
+
clone_comment = wb_serializers.TextField(label="Comment")
|
|
70
72
|
|
|
71
73
|
return CloneSerializer
|
|
72
74
|
|
|
73
75
|
def get_clone_button_instance_display(self) -> Display:
|
|
74
76
|
return create_simple_display(
|
|
75
77
|
[
|
|
76
|
-
["
|
|
77
|
-
["
|
|
78
|
+
["clone_comment"],
|
|
79
|
+
["clone_date"],
|
|
78
80
|
]
|
|
79
81
|
)
|
|
80
82
|
|
|
@@ -367,10 +367,22 @@ class TradeTradeProposalModelViewSet(
|
|
|
367
367
|
"trade_proposal",
|
|
368
368
|
"order",
|
|
369
369
|
)
|
|
370
|
-
ordering_fields = (
|
|
370
|
+
ordering_fields = (
|
|
371
|
+
"underlying_instrument__name",
|
|
372
|
+
"underlying_instrument_isin",
|
|
373
|
+
"underlying_instrument_ticker",
|
|
374
|
+
"underlying_instrument_refinitiv_identifier_code",
|
|
375
|
+
"underlying_instrument_instrument_type",
|
|
376
|
+
"target_weight",
|
|
377
|
+
"effective_weight",
|
|
378
|
+
"effective_shares",
|
|
379
|
+
"target_shares",
|
|
380
|
+
"shares",
|
|
381
|
+
"weighting",
|
|
382
|
+
)
|
|
371
383
|
IDENTIFIER = "wbportfolio:tradeproposal"
|
|
372
384
|
search_fields = ("underlying_instrument__name",)
|
|
373
|
-
filterset_fields = {}
|
|
385
|
+
filterset_fields = {"status": ["exact"]}
|
|
374
386
|
queryset = Trade.objects.all()
|
|
375
387
|
|
|
376
388
|
display_config_class = TradeTradeProposalDisplayConfig
|
|
@@ -497,5 +509,11 @@ class TradeTradeProposalModelViewSet(
|
|
|
497
509
|
underlying_instrument_isin=F("underlying_instrument__isin"),
|
|
498
510
|
underlying_instrument_ticker=F("underlying_instrument__ticker"),
|
|
499
511
|
underlying_instrument_refinitiv_identifier_code=F("underlying_instrument__refinitiv_identifier_code"),
|
|
500
|
-
underlying_instrument_instrument_type=
|
|
512
|
+
underlying_instrument_instrument_type=Case(
|
|
513
|
+
When(
|
|
514
|
+
underlying_instrument__parent__is_security=True,
|
|
515
|
+
then=F("underlying_instrument__parent__instrument_type__short_name"),
|
|
516
|
+
),
|
|
517
|
+
default=F("underlying_instrument__instrument_type__short_name"),
|
|
518
|
+
),
|
|
501
519
|
)
|
|
@@ -58,7 +58,7 @@ wbportfolio/factories/adjustments.py,sha256=aOHXJLy7DW4k1hcRBZteEe2mKe_wQg7z0bVn
|
|
|
58
58
|
wbportfolio/factories/assets.py,sha256=1i7xDT6bpvnYQiEJpmukWadSvS1asQC-QuUJ1PCll0U,3173
|
|
59
59
|
wbportfolio/factories/claim.py,sha256=LQzz8EbIveOYpMY-JOMR8ba-taoX3u53zjBwxiydtrM,1629
|
|
60
60
|
wbportfolio/factories/custodians.py,sha256=XrNcpNhE4Rbv0y159E_MbP1TAQwe9POab-_RHOHy1Uk,311
|
|
61
|
-
wbportfolio/factories/dividends.py,sha256=
|
|
61
|
+
wbportfolio/factories/dividends.py,sha256=ET2kp0A7U1x8WhNVUn6FM1GF4jfr5NwdULBiIQkSLh0,404
|
|
62
62
|
wbportfolio/factories/fees.py,sha256=2B2ebj06a0ZI1ra2-TXtW5e0aQ9sXT-Plw6CzV-3_1c,400
|
|
63
63
|
wbportfolio/factories/indexes.py,sha256=sInyNARyfUNbScxr_ng1HhxtJXIsK3awFf9BF1gr9zU,440
|
|
64
64
|
wbportfolio/factories/portfolio_cash_flow.py,sha256=e8LWkk5Gy0FU4HoCL9xPseCtLlVIkMrvAmey5swqcfc,727
|
|
@@ -111,11 +111,11 @@ wbportfolio/import_export/backends/wbfdm/mixin.py,sha256=JNtjgqGLson1nu_Chqb8MWy
|
|
|
111
111
|
wbportfolio/import_export/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
112
|
wbportfolio/import_export/handlers/adjustment.py,sha256=6bdTIYFmc8_HFxcdwtnYwglMyCfAD8XrTIrEb2zWY0g,1757
|
|
113
113
|
wbportfolio/import_export/handlers/asset_position.py,sha256=5wFnHcbq_zGp9rBUec_JEpzjCA0_v17VrV9F8Ps1ETs,8645
|
|
114
|
-
wbportfolio/import_export/handlers/dividend.py,sha256=
|
|
115
|
-
wbportfolio/import_export/handlers/fees.py,sha256=
|
|
116
|
-
wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=
|
|
114
|
+
wbportfolio/import_export/handlers/dividend.py,sha256=aIayzCn0n3h17_uHcNMLQK_QndRJSU5FHaEewD8jH9U,4002
|
|
115
|
+
wbportfolio/import_export/handlers/fees.py,sha256=phNZ3zc_RKxryj0IE81HUdiFJ8AFiWnVyZ_xtB_x3xY,2828
|
|
116
|
+
wbportfolio/import_export/handlers/portfolio_cash_flow.py,sha256=W7QPNqEvvsq0RS016EAFBp1ezvc6G9Rk-hviRZh8o6Y,2737
|
|
117
117
|
wbportfolio/import_export/handlers/register.py,sha256=sYyXkE8b1DPZ5monxylZn0kjxLVdNYYZR-p61dwEoDM,2271
|
|
118
|
-
wbportfolio/import_export/handlers/trade.py,sha256=
|
|
118
|
+
wbportfolio/import_export/handlers/trade.py,sha256=Vp3nkeg5VvK3oTOiI0V8e0aBg5B2MySypuRx_fMd1VM,11575
|
|
119
119
|
wbportfolio/import_export/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
120
120
|
wbportfolio/import_export/parsers/default_mapping.py,sha256=KrO-X5CvQCeQoBYzFDxavoQGriyUSeI2QDx5ar_zo7A,1405
|
|
121
121
|
wbportfolio/import_export/parsers/jpmorgan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -145,7 +145,7 @@ wbportfolio/import_export/parsers/natixis/valuation.py,sha256=mLjIw1GBlPPlzHJkxg
|
|
|
145
145
|
wbportfolio/import_export/parsers/refinitiv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
146
146
|
wbportfolio/import_export/parsers/refinitiv/adjustment.py,sha256=64AQoLOZQWn5HCLpflJ4OgQB3gCB3w_6Y4YLhcmuClg,819
|
|
147
147
|
wbportfolio/import_export/parsers/sg_lux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
148
|
-
wbportfolio/import_export/parsers/sg_lux/custodian_positions.py,sha256=
|
|
148
|
+
wbportfolio/import_export/parsers/sg_lux/custodian_positions.py,sha256=hl-LqJbwojQOe0zGEH7W31i4hNTVme5L7nwXwyk6XBY,2483
|
|
149
149
|
wbportfolio/import_export/parsers/sg_lux/customer_trade.py,sha256=gTEUIaxlZvXXCgYyg9FD3D3aQhffhruYmeZlaAghrK8,2617
|
|
150
150
|
wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py,sha256=4KSxUGyflf3kY5BVJqMAF1aFkeSWEJegfDtLiA1BM6c,5065
|
|
151
151
|
wbportfolio/import_export/parsers/sg_lux/customer_trade_slk.py,sha256=24e-vonaBAkvX-F_Gw4hLg4FBjgbpCTUbiFC_dZ-czw,2958
|
|
@@ -154,7 +154,7 @@ wbportfolio/import_export/parsers/sg_lux/equity.py,sha256=w13LAMNWReZzqmdFyXdazV
|
|
|
154
154
|
wbportfolio/import_export/parsers/sg_lux/fees.py,sha256=xyPO9sYQyJW5w-1XhUMizY2RbH1Pr4n7LAUE-E67Yj4,1911
|
|
155
155
|
wbportfolio/import_export/parsers/sg_lux/perf_fees.py,sha256=lH1gyswfefwo3Lxi0syydhJYl9EDGsrL_a6fLmpHh8w,1691
|
|
156
156
|
wbportfolio/import_export/parsers/sg_lux/portfolio_cash_flow.py,sha256=ftLKWukUl9AcGWHQmJZnShVZpkO4rOl04PGlLcalDdQ,889
|
|
157
|
-
wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py,sha256=
|
|
157
|
+
wbportfolio/import_export/parsers/sg_lux/portfolio_future_cash_flow.py,sha256=2kkEOwZz1hwiPB9rlMp_vkv5azMrJUfyWl7GMaAwGUc,1074
|
|
158
158
|
wbportfolio/import_export/parsers/sg_lux/registers.py,sha256=5Zaum-6XuQL8-p1MkwA0sN3z8MCymSz4nNzB8MPQDCA,6112
|
|
159
159
|
wbportfolio/import_export/parsers/sg_lux/sylk.py,sha256=1ThyjneFFe_gNLDHeXQFdWIyanyktxm62P-Ta5Sm1v4,7656
|
|
160
160
|
wbportfolio/import_export/parsers/sg_lux/utils.py,sha256=YzYIEDQ3CpkOuRWh2eEFRm-0SERu3SRAmx0nnqQS_og,1301
|
|
@@ -241,6 +241,7 @@ wbportfolio/migrations/0072_trade_diff_shares.py,sha256=aTKa1SbIiwmlXaFtBg-ENrSx
|
|
|
241
241
|
wbportfolio/migrations/0073_remove_product_price_computation_and_more.py,sha256=J4puisDFwnbnfv2VLWaiCQ7ost6PCOkin9qKVQoLIWM,18725
|
|
242
242
|
wbportfolio/migrations/0074_alter_rebalancer_frequency_and_more.py,sha256=o01rBj-ADgwCRtAai3e5z27alPGEzaiNxUqCwWm6peY,918
|
|
243
243
|
wbportfolio/migrations/0075_portfolio_initial_position_date_and_more.py,sha256=rXRpsraVlmueAlO2UpBZV4qMf7dtPuptrhfLblZcJDo,1099
|
|
244
|
+
wbportfolio/migrations/0076_alter_dividendtransaction_price_and_more.py,sha256=4g3ok79nw8mTAxHFAqBAdpGKetaPAjv06YSywywt4aU,6106
|
|
244
245
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
245
246
|
wbportfolio/models/__init__.py,sha256=IIS_PNRxyX2Dcvyk1bcQOUzFt0B9SPC0WlM88CXqj04,881
|
|
246
247
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
@@ -248,7 +249,7 @@ wbportfolio/models/asset.py,sha256=yf4vBPfN1eZlWXG9SowQwr5-goG8rO1yYDitHDLZCBs,3
|
|
|
248
249
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
249
250
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
250
251
|
wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
|
|
251
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
252
|
+
wbportfolio/models/portfolio.py,sha256=KtuPMWtYoKrIOwp9T_REnOjT9CI2kjtEijF3uA0Twsc,59595
|
|
252
253
|
wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
|
|
253
254
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
254
255
|
wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
|
|
@@ -270,14 +271,14 @@ wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7h
|
|
|
270
271
|
wbportfolio/models/reconciliations/account_reconciliations.py,sha256=_teOTNePxua9C894auy8CvdOFsmUnQiOM2_TZAHvzY4,3955
|
|
271
272
|
wbportfolio/models/reconciliations/reconciliations.py,sha256=kF-BNhUoT4TCn1RIgPSkdEk1iX4NQeZlGGFd_ZulAZU,686
|
|
272
273
|
wbportfolio/models/transactions/__init__.py,sha256=R-4fHylrUf3773kGSEt09vtYj3LUlia5yf7rxHXjvHA,295
|
|
273
|
-
wbportfolio/models/transactions/claim.py,sha256=
|
|
274
|
-
wbportfolio/models/transactions/dividends.py,sha256=
|
|
274
|
+
wbportfolio/models/transactions/claim.py,sha256=hcx_tJ9luf2-s1qqsUZXtoDEuxFyty2A1rXSDkljoQo,25890
|
|
275
|
+
wbportfolio/models/transactions/dividends.py,sha256=92-jG8bZN9nU9oDubpu-UDH43Ri7kVjhqE_esOSmOzo,471
|
|
275
276
|
wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
|
|
276
277
|
wbportfolio/models/transactions/fees.py,sha256=ffvqo8I4A0l5rLi00jJ6sGot0jmnkoxaNsbDzdPLwCg,5712
|
|
277
278
|
wbportfolio/models/transactions/rebalancing.py,sha256=nrBi6x6PssCtkLtOpV-2OoAHDUKnnYyrM6xH1PmnjSo,7240
|
|
278
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
279
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
280
|
-
wbportfolio/models/transactions/transactions.py,sha256=
|
|
279
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=9ggUkwzDgMKa8cC8dNZ4IHlleiGpRN0WU-oXGDSiuaw,26730
|
|
280
|
+
wbportfolio/models/transactions/trades.py,sha256=P9cSTEpZBsqKBvw25XY_RNctTiOBEzCvcuy0r8IkedE,28013
|
|
281
|
+
wbportfolio/models/transactions/transactions.py,sha256=fWoDf0TSV0L0gLUDOQpCRLzjMt1H4MUvUHGEaMsilCc,7027
|
|
281
282
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
282
283
|
wbportfolio/pms/typing.py,sha256=b2pBWYt1E8ok-Kqm0lEFIakSnWJ6Ib57z-VX3C3gkQc,6081
|
|
283
284
|
wbportfolio/pms/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -291,7 +292,7 @@ wbportfolio/rebalancing/decorators.py,sha256=JhQ2vkcIuGBTGvNmpkQerdw2-vLq-RAb0KA
|
|
|
291
292
|
wbportfolio/rebalancing/models/__init__.py,sha256=AQjG7Tu5vlmhqncVoYOjpBKU2UIvgo9FuP2_jD2w-UI,232
|
|
292
293
|
wbportfolio/rebalancing/models/composite.py,sha256=uyF1n3NAFprnAZ3Gl5pNJl0GajelqJspC2NufZ7Tf0s,1636
|
|
293
294
|
wbportfolio/rebalancing/models/equally_weighted.py,sha256=U29MOHJMQMIg7Y7W_8t5K3nXjaznzt4ArIxQSiv0Xok,863
|
|
294
|
-
wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=
|
|
295
|
+
wbportfolio/rebalancing/models/market_capitalization_weighted.py,sha256=7vdr7SMNswi3Emc8RpLPJQqQw39xrznp3Ps9Pf9YEZ4,4789
|
|
295
296
|
wbportfolio/rebalancing/models/model_portfolio.py,sha256=XQdvs03-0M9YUnL4DidwZC4E6k-ANCNcZ--T_aaOXTQ,1233
|
|
296
297
|
wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
297
298
|
wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
|
|
@@ -327,7 +328,7 @@ wbportfolio/serializers/portfolio_cash_flow.py,sha256=vBRZnUKq4XAqcvjyk-xMsw5v4x
|
|
|
327
328
|
wbportfolio/serializers/portfolio_cash_targets.py,sha256=_GdsFXoBda1JHJnfZbaVlNFdnXlvGuy_FEcEZH-UThY,605
|
|
328
329
|
wbportfolio/serializers/portfolio_relationship.py,sha256=UR_GIF2LHw82a8mv6L8OXYvoCp5-QWtubt33Y4mqFow,1933
|
|
329
330
|
wbportfolio/serializers/portfolio_swing_pricing.py,sha256=a21st7MmcugbAO_xc90QWSeXwU1ivk-y2PWlMGKySJY,655
|
|
330
|
-
wbportfolio/serializers/portfolios.py,sha256=
|
|
331
|
+
wbportfolio/serializers/portfolios.py,sha256=qEbrTAVepeVIPcs9XaWCDJ0ZxSQX8-M9HmRh6ky4KuY,7684
|
|
331
332
|
wbportfolio/serializers/positions.py,sha256=UK9dvE1wiILw2E209InrsUhme52vfUYDDERBeKCcn_k,2415
|
|
332
333
|
wbportfolio/serializers/product_group.py,sha256=klPGmAb1MvJpyiKKMRb8uYFIMm6SCZkMFQDu5EH-mkY,3680
|
|
333
334
|
wbportfolio/serializers/products.py,sha256=BDMizM77Iohe7JEUncll2ET4GtU8hicQ6c4Vm0srqd4,12374
|
|
@@ -373,7 +374,7 @@ wbportfolio/tests/models/test_merge.py,sha256=sdsjiZsmR6vsUKwTa5kkvL6QTeAZqtd_EP
|
|
|
373
374
|
wbportfolio/tests/models/test_portfolio_cash_flow.py,sha256=X8dsXexsb1b0lBiuGzu40ps_Az_1UmmKT0eo1vbXH94,5792
|
|
374
375
|
wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E05GyRhF_TTQXIi8bdHjXVU0fCV0,965
|
|
375
376
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
376
|
-
wbportfolio/tests/models/test_portfolios.py,sha256=
|
|
377
|
+
wbportfolio/tests/models/test_portfolios.py,sha256=IxJy0fLkUx3m4qKzvkqixYl_0lR7TU-cQJ_DhTkh8V8,52207
|
|
377
378
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
378
379
|
wbportfolio/tests/models/test_products.py,sha256=nBEgyUoY-4F_pfHYnAr7KXdNYvdIkSu-PWJrqp5tPHg,9482
|
|
379
380
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
@@ -388,7 +389,7 @@ wbportfolio/tests/models/transactions/test_trades.py,sha256=vqvOqUY_uXvBp8YOKR0W
|
|
|
388
389
|
wbportfolio/tests/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
389
390
|
wbportfolio/tests/pms/test_analytics.py,sha256=fAuY1zcXibttFpBh2GhKVyzdYfi1kz_b7SPa9xZQXY0,1086
|
|
390
391
|
wbportfolio/tests/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
391
|
-
wbportfolio/tests/rebalancing/test_models.py,sha256=
|
|
392
|
+
wbportfolio/tests/rebalancing/test_models.py,sha256=qyZkNG1WF1lZdYwvjUElkkNey8b5ZMGSlWpzxujCuNg,7549
|
|
392
393
|
wbportfolio/tests/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
393
394
|
wbportfolio/tests/serializers/test_claims.py,sha256=vQrg73xQXRFEgvx3KI9ivFre_wpBFzdO0p0J13PkvdY,582
|
|
394
395
|
wbportfolio/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -399,7 +400,7 @@ wbportfolio/tests/viewsets/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5J
|
|
|
399
400
|
wbportfolio/tests/viewsets/transactions/test_claims.py,sha256=QEZfMAW07dyoZ63t2umSwGOqvaTULfYfbN_F4ZoSAcw,6368
|
|
400
401
|
wbportfolio/viewsets/__init__.py,sha256=3kUaQ66ybvROwejd3bEcSt4XKzfOlPDaeoStMvlz7qY,2294
|
|
401
402
|
wbportfolio/viewsets/adjustments.py,sha256=5hWCjxSgUIsrPOmJKoDYK3gywdMTI0aYDorEj1FXRVc,1429
|
|
402
|
-
wbportfolio/viewsets/assets.py,sha256=
|
|
403
|
+
wbportfolio/viewsets/assets.py,sha256=CrAMuyJDyUcSZlckrhy5D6-vVRXD7I4EG9C3ZKA5Z08,22735
|
|
403
404
|
wbportfolio/viewsets/assets_and_net_new_money_progression.py,sha256=Jl4vEQP4N2OFL5IGBXoKcj-0qaPviU0I8npvQLw4Io0,4464
|
|
404
405
|
wbportfolio/viewsets/custodians.py,sha256=CTFqkqVP1R3AV7lhdvcdICxB5DfwDYCyikNSI5kbYEo,2322
|
|
405
406
|
wbportfolio/viewsets/esg.py,sha256=27MxxdXQH3Cq_1UEYmcrF7htUOg6i81fUpbVQXAAKJI,6985
|
|
@@ -408,7 +409,7 @@ wbportfolio/viewsets/portfolio_cash_flow.py,sha256=jkBfdZRQ3KsxGMJpltRjmdrZ2qEFJ
|
|
|
408
409
|
wbportfolio/viewsets/portfolio_cash_targets.py,sha256=CvHlrDE8qnnnfRpTYnFu-Uu15MDbF5d5gTmEKth2S24,322
|
|
409
410
|
wbportfolio/viewsets/portfolio_relationship.py,sha256=RGyvxd8NfFEs8YdqEvVD3VbrISvAO5UtCTlocSIuWQw,2109
|
|
410
411
|
wbportfolio/viewsets/portfolio_swing_pricing.py,sha256=-57l3WLQZRslIV67OT0ucHE5JXTtTtLvd3t7MppdVn8,357
|
|
411
|
-
wbportfolio/viewsets/portfolios.py,sha256=
|
|
412
|
+
wbportfolio/viewsets/portfolios.py,sha256=1POzE9jrt2iLVMnIY_BWDr0A_zpOlO3Z8tM80TaXkhk,14454
|
|
412
413
|
wbportfolio/viewsets/positions.py,sha256=MDf_0x9La2qE6qjaIqBtfV5VC0RfJ1chZIim45Emk10,13198
|
|
413
414
|
wbportfolio/viewsets/product_groups.py,sha256=YvmuXPPy98K1J_rz6YPsx9gNK-tCS2P-wc1uRYgfyo0,2399
|
|
414
415
|
wbportfolio/viewsets/product_performance.py,sha256=dRfRgifjGS1RgZSu9uJRM0SmB7eLnNUkPuqARMO4gyo,28371
|
|
@@ -436,24 +437,24 @@ wbportfolio/viewsets/configs/buttons/trade_proposals.py,sha256=eZBfYk5ZhancCVcu7
|
|
|
436
437
|
wbportfolio/viewsets/configs/buttons/trades.py,sha256=X2B1l0iEIdHb3ZMf9rLVoiX_H8lSyLD12wopgumOIX4,2318
|
|
437
438
|
wbportfolio/viewsets/configs/display/__init__.py,sha256=SmazY-YEp-Xf8G08Uz1-CzePZRCRtHrziRMIYYIGpCk,2176
|
|
438
439
|
wbportfolio/viewsets/configs/display/adjustments.py,sha256=jIOEc23OCYBguLaZRlZxC916kocYT35ZV9Jsiocs9nk,3334
|
|
439
|
-
wbportfolio/viewsets/configs/display/assets.py,sha256=
|
|
440
|
+
wbportfolio/viewsets/configs/display/assets.py,sha256=SkWwzvrn4gOVbCThGJMV63V-iJh517qfUs4f4Wt3dBA,10573
|
|
440
441
|
wbportfolio/viewsets/configs/display/claim.py,sha256=MRtEdxWjsDjt1k6O-ltM-SpPmHWBk85sCycsRgzd9jI,12041
|
|
441
442
|
wbportfolio/viewsets/configs/display/custodians.py,sha256=R-tnktfY48K-8Zz-Fg_M6IXKHv42J1ZeSpom7lvYVS8,801
|
|
442
443
|
wbportfolio/viewsets/configs/display/esg.py,sha256=W8uetCPN2TjHtU2kvQjKOmkq7uoaYvzX2iLN6Nz78Pk,3111
|
|
443
444
|
wbportfolio/viewsets/configs/display/fees.py,sha256=_8HDJADh5bA5VMVPkhKObPmNNdPHWR8Oz23PEgd04F0,5834
|
|
444
445
|
wbportfolio/viewsets/configs/display/portfolio_cash_flow.py,sha256=CS_UGhKZSXnX-0SZ0RXzbG1WMJm6NX3GUljwIa4RWBk,5025
|
|
445
446
|
wbportfolio/viewsets/configs/display/portfolio_relationship.py,sha256=8DvsYWFCX29qj5wUf1wCtjlPwzKRd_E7JDuo_CopaJ0,1294
|
|
446
|
-
wbportfolio/viewsets/configs/display/portfolios.py,sha256=
|
|
447
|
+
wbportfolio/viewsets/configs/display/portfolios.py,sha256=HFUFYajD7l-7WhwsxEeBp8QvJ5ZZY03EA8Am-DQ6f6w,10726
|
|
447
448
|
wbportfolio/viewsets/configs/display/positions.py,sha256=yolWLxzGPIpSQSiVhVQChURqbomPt5kSjkYrmXT1Mik,3123
|
|
448
449
|
wbportfolio/viewsets/configs/display/product_groups.py,sha256=PwI-A0_ofShT2pub9-C1HqreiqpHxKMHd51JYwEzvbM,2500
|
|
449
450
|
wbportfolio/viewsets/configs/display/product_performance.py,sha256=6Mme48JBn_okwClR44dBK2OK26ejvdasDvBa5DI33_0,10070
|
|
450
|
-
wbportfolio/viewsets/configs/display/products.py,sha256=
|
|
451
|
+
wbportfolio/viewsets/configs/display/products.py,sha256=WraPeMIS87Vd3wstA4Vt3zi41vgLUxK5eoqCIQLyr8k,10679
|
|
451
452
|
wbportfolio/viewsets/configs/display/rebalancing.py,sha256=yw9X1Nf2-V_KP_mCX4pVKnJSjSstHMoocMkyO73KpkU,1112
|
|
452
453
|
wbportfolio/viewsets/configs/display/reconciliations.py,sha256=YvMAuwmpX0HExvGsuf5UvcRQxe4eMo1iyNJX68GGC_k,6021
|
|
453
454
|
wbportfolio/viewsets/configs/display/registers.py,sha256=1np75exIk5rfct6UkVN_RnfJ9ozvIkcWJgFV4_4rJns,3182
|
|
454
455
|
wbportfolio/viewsets/configs/display/roles.py,sha256=SFUyCdxSlHZ3NsMrJmpVBSlg-XKGaEFteV89nyLMMAQ,1815
|
|
455
456
|
wbportfolio/viewsets/configs/display/trade_proposals.py,sha256=sRLSUjKlarBhnTwg7tX_Juldor3beswJlyvZfFPvNEk,4315
|
|
456
|
-
wbportfolio/viewsets/configs/display/trades.py,sha256=
|
|
457
|
+
wbportfolio/viewsets/configs/display/trades.py,sha256=3rvIYMCe3IOyDw5HTrXcPvstdDg6Zt13oskh1yKQyRY,16613
|
|
457
458
|
wbportfolio/viewsets/configs/display/transactions.py,sha256=DOM3eV1DxBwX6Iiw3C2sJamWh6A_3ZSYC9447Jc3Wmo,2586
|
|
458
459
|
wbportfolio/viewsets/configs/endpoints/__init__.py,sha256=E13AYY3CIW4CZtmqwBVMPDYA5zNyKJeRZtiXKtad68Y,2871
|
|
459
460
|
wbportfolio/viewsets/configs/endpoints/adjustments.py,sha256=9CcnfNuFcxsZ8YvfUitSeyCvpLxY-jU-gIw3GG0mIp4,697
|
|
@@ -473,7 +474,7 @@ wbportfolio/viewsets/configs/endpoints/reconciliations.py,sha256=xriq1KSctxQa6AE
|
|
|
473
474
|
wbportfolio/viewsets/configs/endpoints/roles.py,sha256=5Nwi2sdUYc85YL0wJ7SUtt87ZBTOVdFnEexUgVrmcX4,367
|
|
474
475
|
wbportfolio/viewsets/configs/endpoints/trade_proposals.py,sha256=sno7SOHWjKvvF_YdgYx5KthntM--F3DX-9hsrY5vRTM,783
|
|
475
476
|
wbportfolio/viewsets/configs/endpoints/trades.py,sha256=5NKMSYaGWDd8OcV16HLRSlAgYYXObLikLf2w4ix2DoE,3483
|
|
476
|
-
wbportfolio/viewsets/configs/endpoints/transactions.py,sha256=
|
|
477
|
+
wbportfolio/viewsets/configs/endpoints/transactions.py,sha256=6u2GWm67o4g1PoAtZXLdLyu3J4qhyOA1dm9Otu1i9f8,813
|
|
477
478
|
wbportfolio/viewsets/configs/menu/__init__.py,sha256=gzUpv5f8FAK1Bdhn0AhcMLJR3r9RXHRXcHg21vuvGxs,1184
|
|
478
479
|
wbportfolio/viewsets/configs/menu/adjustments.py,sha256=sQ25Jp2N8nsLSUOEJTJUBE0Y7bIPHK4Y6LE0e1OzX38,316
|
|
479
480
|
wbportfolio/viewsets/configs/menu/assets.py,sha256=FkddF1_mu01e1mnkJkXPQcE0izI0Q3wU7VBFIkeT-Qw,340
|
|
@@ -517,10 +518,10 @@ wbportfolio/viewsets/transactions/claim.py,sha256=m_Fy4J_QZSve1VlR_sPQrVBDopgCqq
|
|
|
517
518
|
wbportfolio/viewsets/transactions/fees.py,sha256=7VUXIogmRrXCz_D9tvDiiTae0t5j09W9zPUzxXzBGTE,7031
|
|
518
519
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
519
520
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
520
|
-
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=
|
|
521
|
-
wbportfolio/viewsets/transactions/trades.py,sha256=
|
|
521
|
+
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=iQpC_Thbj56SmM05vPRsF1JZguGBDaTUH3I-_iCHCV0,5958
|
|
522
|
+
wbportfolio/viewsets/transactions/trades.py,sha256=xeEzx7GP34aBNPlDmiUmT86labsbb8_f1U2RCN1Jatg,21494
|
|
522
523
|
wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
|
|
523
|
-
wbportfolio-1.49.
|
|
524
|
-
wbportfolio-1.49.
|
|
525
|
-
wbportfolio-1.49.
|
|
526
|
-
wbportfolio-1.49.
|
|
524
|
+
wbportfolio-1.49.2.dist-info/METADATA,sha256=cFNyn_eGy7Dwxdfb_oqZH6Z2zj79fYUOsKj6cTy5XNo,734
|
|
525
|
+
wbportfolio-1.49.2.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
526
|
+
wbportfolio-1.49.2.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
527
|
+
wbportfolio-1.49.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|