wbportfolio 1.55.9__py2.py3-none-any.whl → 1.55.10rc0__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.
- wbportfolio/api_clients/ubs.py +11 -9
- wbportfolio/contrib/company_portfolio/configs/display.py +4 -4
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/import_export/handlers/asset_position.py +3 -3
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +2 -2
- wbportfolio/import_export/handlers/trade.py +3 -3
- wbportfolio/import_export/parsers/default_mapping.py +1 -1
- wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/strategy.py +2 -2
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
- wbportfolio/import_export/parsers/natixis/utils.py +6 -2
- wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
- wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
- wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
- wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
- wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
- wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
- wbportfolio/import_export/parsers/ubs/equity.py +2 -1
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/models/asset.py +2 -1
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/orders/order_proposals.py +2 -2
- wbportfolio/models/portfolio.py +7 -7
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +3 -0
- wbportfolio/models/rebalancing.py +3 -0
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/claim.py +6 -5
- wbportfolio/pms/typing.py +1 -1
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
- wbportfolio/risk_management/backends/controversy_portfolio.py +1 -1
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +1 -1
- wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
- wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/tests/models/test_portfolios.py +5 -5
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/viewsets/charts/assets.py +6 -4
- wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/METADATA +1 -1
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/RECORD +56 -56
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.55.9.dist-info → wbportfolio-1.55.10rc0.dist-info}/licenses/LICENSE +0 -0
wbportfolio/api_clients/ubs.py
CHANGED
|
@@ -62,21 +62,21 @@ class UBSNeoAPIClient:
|
|
|
62
62
|
def _raise_for_status(self, response):
|
|
63
63
|
try:
|
|
64
64
|
response.raise_for_status()
|
|
65
|
-
except HTTPError:
|
|
65
|
+
except HTTPError as e:
|
|
66
66
|
json_response = response.json()
|
|
67
|
-
raise HTTPError(json_response.get("errors", json_response.get("message")))
|
|
67
|
+
raise HTTPError(json_response.get("errors", json_response.get("message"))) from e
|
|
68
68
|
|
|
69
69
|
def get_rebalance_service_status(self) -> dict:
|
|
70
70
|
"""Check API connection status."""
|
|
71
71
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/status"
|
|
72
|
-
response = requests.get(url, headers=self._get_headers())
|
|
72
|
+
response = requests.get(url, headers=self._get_headers(), timeout=10)
|
|
73
73
|
self._raise_for_status(response)
|
|
74
74
|
return self._get_json(response)
|
|
75
75
|
|
|
76
76
|
def get_rebalance_status_for_isin(self, isin: str) -> dict:
|
|
77
77
|
"""Check certificate accessibility and workflow status for given ISIN."""
|
|
78
78
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/status/{isin}"
|
|
79
|
-
response = requests.get(url, headers=self._get_headers())
|
|
79
|
+
response = requests.get(url, headers=self._get_headers(), timeout=10)
|
|
80
80
|
self._raise_for_status(response)
|
|
81
81
|
return self._get_json(response)
|
|
82
82
|
|
|
@@ -90,7 +90,7 @@ class UBSNeoAPIClient:
|
|
|
90
90
|
isin = self._validate_isin(isin, test=test)
|
|
91
91
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/submit/{isin}"
|
|
92
92
|
payload = {"items": items}
|
|
93
|
-
response = requests.post(url, json=payload, headers=self._get_headers())
|
|
93
|
+
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=10)
|
|
94
94
|
self._raise_for_status(response)
|
|
95
95
|
return self._get_json(response)
|
|
96
96
|
|
|
@@ -99,7 +99,7 @@ class UBSNeoAPIClient:
|
|
|
99
99
|
isin = self._validate_isin(isin)
|
|
100
100
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/savedraft/{isin}"
|
|
101
101
|
payload = {"items": items}
|
|
102
|
-
response = requests.post(url, json=payload, headers=self._get_headers())
|
|
102
|
+
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=10)
|
|
103
103
|
self._raise_for_status(response)
|
|
104
104
|
return self._get_json(response)
|
|
105
105
|
|
|
@@ -107,20 +107,20 @@ class UBSNeoAPIClient:
|
|
|
107
107
|
"""Cancel a rebalance request."""
|
|
108
108
|
isin = self._validate_isin(isin)
|
|
109
109
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/cancel/{isin}"
|
|
110
|
-
response = requests.delete(url, headers=self._get_headers())
|
|
110
|
+
response = requests.delete(url, headers=self._get_headers(), timeout=10)
|
|
111
111
|
self._raise_for_status(response)
|
|
112
112
|
return self._get_json(response)
|
|
113
113
|
|
|
114
114
|
def get_current_rebalance_request(self, isin: str) -> dict:
|
|
115
115
|
"""Fetch the current rebalance request for a certificate."""
|
|
116
116
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/currentRebalanceRequest/{isin}"
|
|
117
|
-
response = requests.get(url, headers=self._get_headers())
|
|
117
|
+
response = requests.get(url, headers=self._get_headers(), timeout=10)
|
|
118
118
|
self._raise_for_status(response)
|
|
119
119
|
return self._get_json(response)
|
|
120
120
|
|
|
121
121
|
def get_portfolio_at_date(self, isin: str, val_date: date) -> dict:
|
|
122
122
|
url = f"https://neo.ubs.com/api/ged-amc/external/report/v1/valuation/{isin}/{val_date:%Y-%m-%d}"
|
|
123
|
-
response = requests.get(url, headers=self._get_headers())
|
|
123
|
+
response = requests.get(url, headers=self._get_headers(), timeout=10)
|
|
124
124
|
self._raise_for_status(response)
|
|
125
125
|
return self._get_json(response)
|
|
126
126
|
|
|
@@ -130,6 +130,7 @@ class UBSNeoAPIClient:
|
|
|
130
130
|
url,
|
|
131
131
|
headers=self._get_headers(),
|
|
132
132
|
params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
|
|
133
|
+
timeout=10,
|
|
133
134
|
)
|
|
134
135
|
self._raise_for_status(response)
|
|
135
136
|
return self._get_json(response)
|
|
@@ -140,6 +141,7 @@ class UBSNeoAPIClient:
|
|
|
140
141
|
url,
|
|
141
142
|
headers=self._get_headers(),
|
|
142
143
|
params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
|
|
144
|
+
timeout=10,
|
|
143
145
|
)
|
|
144
146
|
self._raise_for_status(response)
|
|
145
147
|
return self._get_json(response)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from django.utils.translation import gettext as _
|
|
4
|
-
from wbcore.contrib.directory.viewsets.display.entries import CompanyModelDisplay as
|
|
5
|
-
from wbcore.contrib.directory.viewsets.display.entries import PersonModelDisplay as
|
|
4
|
+
from wbcore.contrib.directory.viewsets.display.entries import CompanyModelDisplay as BaseCompanyModelDisplay
|
|
5
|
+
from wbcore.contrib.directory.viewsets.display.entries import PersonModelDisplay as BasePersonModelDisplay
|
|
6
6
|
from wbcore.metadata.configs import display as dp
|
|
7
7
|
from wbcore.metadata.configs.display.instance_display import (
|
|
8
8
|
Inline,
|
|
@@ -62,7 +62,7 @@ AUM_FIELDS = Section(
|
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
class CompanyModelDisplay(
|
|
65
|
+
class CompanyModelDisplay(BaseCompanyModelDisplay):
|
|
66
66
|
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
67
67
|
list_display = super().get_list_display()
|
|
68
68
|
list_display.fields = (
|
|
@@ -134,7 +134,7 @@ class CompanyModelDisplay(CMD):
|
|
|
134
134
|
return instance_display
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
class PersonModelDisplay(
|
|
137
|
+
class PersonModelDisplay(BasePersonModelDisplay):
|
|
138
138
|
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
139
139
|
list_display = super().get_list_display()
|
|
140
140
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
|
|
1
3
|
from wbcore.contrib.directory.viewsets.previews import EntryPreviewConfig
|
|
2
4
|
from wbcore.contrib.icons import WBIcon
|
|
3
5
|
from wbcore.metadata.configs import buttons as bt
|
|
@@ -17,12 +19,10 @@ class CompanyPreviewConfig(EntryPreviewConfig):
|
|
|
17
19
|
["asset_under_management", "invested_assets_under_management_usd"],
|
|
18
20
|
[repeat_field(2, "potential")],
|
|
19
21
|
]
|
|
20
|
-
|
|
22
|
+
with suppress(Exception):
|
|
21
23
|
entry = self.view.get_object()
|
|
22
24
|
if entry.profile_image:
|
|
23
25
|
fields.insert(0, [repeat_field(2, "profile_image")])
|
|
24
|
-
except Exception:
|
|
25
|
-
pass
|
|
26
26
|
|
|
27
27
|
return create_simple_display(fields)
|
|
28
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from wbcore import filters
|
|
2
|
-
from wbcore.contrib.directory.filters import CompanyFilter as
|
|
3
|
-
from wbcore.contrib.directory.filters import PersonFilter as
|
|
2
|
+
from wbcore.contrib.directory.filters import CompanyFilter as BaseCompanyFilter
|
|
3
|
+
from wbcore.contrib.directory.filters import PersonFilter as BasePersonFilter
|
|
4
4
|
from wbcore.contrib.directory.models import Company, Person
|
|
5
5
|
|
|
6
6
|
|
|
@@ -99,29 +99,29 @@ class EntryPortfolioFilter(filters.FilterSet):
|
|
|
99
99
|
return queryset
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
class CompanyFilter(
|
|
102
|
+
class CompanyFilter(BaseCompanyFilter, EntryPortfolioFilter):
|
|
103
103
|
@classmethod
|
|
104
104
|
def get_filter_class_for_remote_filter(cls):
|
|
105
105
|
"""
|
|
106
106
|
Define which filterset class sender to user for remote filter registration
|
|
107
107
|
"""
|
|
108
|
-
return
|
|
108
|
+
return BaseCompanyFilter
|
|
109
109
|
|
|
110
|
-
class Meta(
|
|
110
|
+
class Meta(BaseCompanyFilter.Meta):
|
|
111
111
|
fields = {
|
|
112
|
-
**
|
|
112
|
+
**BaseCompanyFilter.Meta.fields,
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
class PersonFilter(
|
|
116
|
+
class PersonFilter(BasePersonFilter, EntryPortfolioFilter):
|
|
117
117
|
@classmethod
|
|
118
118
|
def get_filter_class_for_remote_filter(cls):
|
|
119
119
|
"""
|
|
120
120
|
Define which filterset class sender to user for remote filter registration
|
|
121
121
|
"""
|
|
122
|
-
return
|
|
122
|
+
return BasePersonFilter
|
|
123
123
|
|
|
124
|
-
class Meta(
|
|
124
|
+
class Meta(BasePersonFilter.Meta):
|
|
125
125
|
fields = {
|
|
126
|
-
**
|
|
126
|
+
**BasePersonFilter.Meta.fields,
|
|
127
127
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import re
|
|
2
3
|
|
|
3
4
|
from wbcore.contrib.currency.models import Currency
|
|
4
5
|
from wbcore.contrib.directory.models import Company
|
|
5
6
|
|
|
7
|
+
logger = logging.getLogger("pms")
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
def get_currency_and_assets_under_management(
|
|
8
11
|
aum_string, currency_mapping, default_currency, multiplier_mapping, default_multiplier
|
|
@@ -72,5 +75,7 @@ def assign_aum():
|
|
|
72
75
|
portfolio_data.assets_under_management_currency = currency
|
|
73
76
|
try:
|
|
74
77
|
portfolio_data.save()
|
|
75
|
-
except Exception:
|
|
76
|
-
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(
|
|
80
|
+
f"while we try to save the customer portfolio aum data for {company}, we encounter the error: {e}"
|
|
81
|
+
)
|
|
@@ -21,10 +21,10 @@ class ProductGroupFactory(InstrumentFactory):
|
|
|
21
21
|
paying_agent = factory.SubFactory("wbcore.contrib.directory.factories.entries.CompanyFactory")
|
|
22
22
|
|
|
23
23
|
@factory.post_generation
|
|
24
|
-
def create_initial_portfolio(
|
|
25
|
-
if
|
|
24
|
+
def create_initial_portfolio(self, *args, **kwargs):
|
|
25
|
+
if self.id and not self.portfolios.exists():
|
|
26
26
|
portfolio = PortfolioFactory.create()
|
|
27
|
-
InstrumentPortfolioThroughModel.objects.create(instrument=
|
|
27
|
+
InstrumentPortfolioThroughModel.objects.create(instrument=self, portfolio=portfolio)
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class ProductGroupRepresentantFactory(factory.django.DjangoModelFactory):
|
|
@@ -40,10 +40,10 @@ class ProductFactory(InstrumentFactory):
|
|
|
40
40
|
instrument_type = factory.LazyAttribute(lambda o: InstrumentTypeFactory.create(name="Product", key="product"))
|
|
41
41
|
|
|
42
42
|
@factory.post_generation
|
|
43
|
-
def create_initial_portfolio(
|
|
44
|
-
if
|
|
43
|
+
def create_initial_portfolio(self, *args, **kwargs):
|
|
44
|
+
if self.id and not self.portfolios.exists():
|
|
45
45
|
portfolio = PortfolioFactory.create()
|
|
46
|
-
InstrumentPortfolioThroughModel.objects.create(instrument=
|
|
46
|
+
InstrumentPortfolioThroughModel.objects.create(instrument=self, portfolio=portfolio)
|
|
47
47
|
|
|
48
48
|
# wbportfolio = factory.SubFactory(PortfolioFactory)
|
|
49
49
|
# portfolio_computed = factory.SubFactory(PortfolioFactory)
|
|
@@ -78,14 +78,14 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
78
78
|
data["initial_price"] = data["underlying_quote"].get_price(
|
|
79
79
|
data["date"], price_date_timedelta=self.MAX_PRICE_DATE_TIMEDELTA
|
|
80
80
|
)
|
|
81
|
-
except ValueError:
|
|
82
|
-
raise DeserializationError("Price not provided but can not be found automatically")
|
|
81
|
+
except ValueError as e:
|
|
82
|
+
raise DeserializationError("Price not provided but can not be found automatically") from e
|
|
83
83
|
|
|
84
84
|
# number type deserialization and sanitization
|
|
85
85
|
# ensure the provided Decimal field are of type Decimal
|
|
86
86
|
decimal_fields = ["initial_currency_fx_rate", "initial_price", "initial_shares", "weighting"]
|
|
87
87
|
for field in decimal_fields:
|
|
88
|
-
if
|
|
88
|
+
if (value := data.get(field, None)) is not None:
|
|
89
89
|
data[field] = Decimal(value)
|
|
90
90
|
|
|
91
91
|
if data["weighting"] == 0:
|
|
@@ -36,7 +36,7 @@ class DividendImportHandler(ImportExportHandler):
|
|
|
36
36
|
data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
|
|
37
37
|
|
|
38
38
|
for field in self.model._meta.get_fields():
|
|
39
|
-
if
|
|
39
|
+
if (value := data.get(field.name, None)) is not None and isinstance(field, models.DecimalField):
|
|
40
40
|
q = 1 / (math.pow(10, 4))
|
|
41
41
|
data[field.name] = Decimal(value).quantize(Decimal(str(q)))
|
|
42
42
|
|
|
@@ -24,8 +24,8 @@ class FeesImportHandler(ImportExportHandler):
|
|
|
24
24
|
data["product"] = Product.objects.get(**product_data)
|
|
25
25
|
else:
|
|
26
26
|
data["product"] = Product.objects.get(id=product_data)
|
|
27
|
-
except Product.DoesNotExist:
|
|
28
|
-
raise DeserializationError("There is no valid linked product for in this row.")
|
|
27
|
+
except Product.DoesNotExist as e:
|
|
28
|
+
raise DeserializationError("There is no valid linked product for in this row.") from e
|
|
29
29
|
|
|
30
30
|
if "currency" not in data:
|
|
31
31
|
data["currency"] = data["product"].currency
|
|
@@ -65,15 +65,15 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
65
65
|
try:
|
|
66
66
|
product = Product.objects.get(id=underlying_instrument.id)
|
|
67
67
|
data["shares"] = nominal / product.share_price
|
|
68
|
-
except Product.DoesNotExist:
|
|
68
|
+
except Product.DoesNotExist as e:
|
|
69
69
|
raise DeserializationError(
|
|
70
70
|
"We cannot compute the number of shares from the nominal value as we cannot find the product share price."
|
|
71
|
-
)
|
|
71
|
+
) from e
|
|
72
72
|
else:
|
|
73
73
|
raise DeserializationError("We couldn't find a valid underlying instrument this row.")
|
|
74
74
|
|
|
75
75
|
for field in self.model._meta.get_fields():
|
|
76
|
-
if
|
|
76
|
+
if (value := data.get(field.name, None)) is not None and isinstance(field, models.DecimalField):
|
|
77
77
|
q = (
|
|
78
78
|
1 / (math.pow(10, 4))
|
|
79
79
|
) # we need that convertion mechanism otherwise there is floating point approximation error while casting to decimal and get_instance does not work as expected
|
|
@@ -7,7 +7,7 @@ def parse(import_source):
|
|
|
7
7
|
if (
|
|
8
8
|
(data_backend := import_source.source.data_backend)
|
|
9
9
|
and (backend_class := data_backend.backend_class)
|
|
10
|
-
and (default_mapping :=
|
|
10
|
+
and (default_mapping := backend_class.DEFAULT_MAPPING)
|
|
11
11
|
):
|
|
12
12
|
df = pd.read_json(import_source.file, orient="records")
|
|
13
13
|
if not df.empty:
|
|
@@ -11,8 +11,8 @@ from wbportfolio.models import Product, Trade
|
|
|
11
11
|
def file_name_parse(file_name):
|
|
12
12
|
dates = re.findall("([0-9]{8})", file_name)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
if len(dates) <= 0:
|
|
15
|
+
raise ValueError("No dates found in the filename")
|
|
16
16
|
parts_dict = {"valuation_date": datetime.datetime.strptime(dates[0], "%Y%m%d").date()}
|
|
17
17
|
|
|
18
18
|
isin = re.findall("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})", file_name)
|
|
@@ -14,8 +14,8 @@ logger = logging.getLogger("importers.parsers.jpmorgan.fee")
|
|
|
14
14
|
def file_name_parse(file_name):
|
|
15
15
|
dates = re.findall("([0-9]{8})", file_name)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if len(dates) != 1:
|
|
18
|
+
raise ValueError("Not exactly 1 date found in the filename")
|
|
19
19
|
return {"valuation_date": datetime.datetime.strptime(dates[0], "%Y%m%d").date()}
|
|
20
20
|
|
|
21
21
|
|
|
@@ -13,8 +13,8 @@ logger = logging.getLogger("importers.parsers.jp_morgan.strategy")
|
|
|
13
13
|
def file_name_parse(file_name):
|
|
14
14
|
dates = re.findall("([0-9]{8})", file_name)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
if len(dates) != 1:
|
|
17
|
+
raise ValueError("Not exactly 1 date found in the filename")
|
|
18
18
|
return {"valuation_date": datetime.datetime.strptime(dates[0], "%Y%m%d").date()}
|
|
19
19
|
|
|
20
20
|
|
|
@@ -13,8 +13,8 @@ logger = logging.getLogger("importers.parsers.jpmorgan.index")
|
|
|
13
13
|
def file_name_parse(file_name):
|
|
14
14
|
dates = re.findall("([0-9]{8})", file_name)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
if len(dates) != 1:
|
|
17
|
+
raise ValueError("Not exactly 1 date found in the filename")
|
|
18
18
|
return {"valuation_date": datetime.datetime.strptime(dates[0], "%Y%m%d").date()}
|
|
19
19
|
|
|
20
20
|
|
|
@@ -10,7 +10,8 @@ from wbportfolio.models import Product
|
|
|
10
10
|
def file_name_parse(file_name):
|
|
11
11
|
isin = re.findall("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})", file_name)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
if len(isin) != 1:
|
|
14
|
+
raise ValueError("Not exactly 1 isin found in the filename")
|
|
14
15
|
|
|
15
16
|
return {"isin": isin[0]}
|
|
16
17
|
|
|
@@ -55,8 +55,12 @@ def _get_underlying_instrument(bbg_code, name, currency, instrument_type="equity
|
|
|
55
55
|
def file_name_parse_isin(file_name):
|
|
56
56
|
dates = re.findall(r"_([0-9]{4}-?[0-9]{2}-?[0-9]{2})", file_name)
|
|
57
57
|
identifier = re.findall(r"([A-Z]{2}(?![A-Z]{10}\b)[A-Z0-9]{10})_", file_name)
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
|
|
59
|
+
if len(dates) != 2:
|
|
60
|
+
raise ValueError("Not 2 dates found in the filename")
|
|
61
|
+
|
|
62
|
+
if len(identifier) != 1:
|
|
63
|
+
raise ValueError("Not exactly 1 identifier found in the filename")
|
|
60
64
|
try:
|
|
61
65
|
valuation_date = datetime.datetime.strptime(dates[0], "%Y-%m-%d").date()
|
|
62
66
|
except ValueError:
|
|
@@ -11,9 +11,10 @@ from wbportfolio.models import ProductGroup
|
|
|
11
11
|
def file_name_parse(file_name):
|
|
12
12
|
dates = re.findall(r"([0-9]{4}-[0-9]{2}-[0-9]{2})", file_name)
|
|
13
13
|
isin = re.findall(r"\.([a-zA-Z0-9]*)_", file_name)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
if len(dates) != 2:
|
|
15
|
+
raise ValueError("Not 2 dates found in the filename")
|
|
16
|
+
if len(isin) != 1:
|
|
17
|
+
raise ValueError("Not exactly 1 isin found in the filename")
|
|
17
18
|
|
|
18
19
|
return {
|
|
19
20
|
"isin": isin[0],
|
|
@@ -10,6 +10,7 @@ files generated by other spreadsheets.
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
|
+
from ast import literal_eval
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Table:
|
|
@@ -185,7 +186,7 @@ class SYLK:
|
|
|
185
186
|
self.cury = int(val)
|
|
186
187
|
|
|
187
188
|
elif ftd == "K":
|
|
188
|
-
val =
|
|
189
|
+
val = literal_eval(self.escape(val))
|
|
189
190
|
# if type(val) == int:
|
|
190
191
|
# if self.currenttype == "date":
|
|
191
192
|
# # value is offset in days from datebase
|
|
@@ -208,20 +209,20 @@ class SYLK:
|
|
|
208
209
|
self.printformats.append((format, self.knownformats[format]))
|
|
209
210
|
else:
|
|
210
211
|
# hack to guess type...
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
212
|
+
has_y = "y" in format
|
|
213
|
+
has_d = "d" in format
|
|
214
|
+
has_h = "h" in format
|
|
215
|
+
has_z = "0" in format
|
|
216
|
+
has_p = "." in format
|
|
217
|
+
if (has_d or has_y) and has_h:
|
|
217
218
|
dtype = "datetime"
|
|
218
|
-
elif
|
|
219
|
+
elif has_d or has_y:
|
|
219
220
|
dtype = "date"
|
|
220
|
-
elif
|
|
221
|
+
elif has_h:
|
|
221
222
|
dtype = "time"
|
|
222
|
-
elif
|
|
223
|
+
elif has_p and has_z:
|
|
223
224
|
dtype = "float"
|
|
224
|
-
elif
|
|
225
|
+
elif has_z:
|
|
225
226
|
dtype = "int"
|
|
226
227
|
else:
|
|
227
228
|
dtype = "string"
|
|
@@ -10,8 +10,10 @@ def file_name_parse(file_name):
|
|
|
10
10
|
dates = re.findall(r"([0-9]{4}-[0-9]{2}-[0-9]{2})", file_name)
|
|
11
11
|
isin = re.findall(r"\.([a-zA-Z0-9]*)_", file_name)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if len(dates) != 2:
|
|
14
|
+
raise ValueError("Not 2 dates found in the filename")
|
|
15
|
+
if len(isin) != 1:
|
|
16
|
+
raise ValueError("Not exactly 1 isin found in the filename")
|
|
15
17
|
|
|
16
18
|
return {
|
|
17
19
|
"isin": isin[0],
|
|
@@ -14,8 +14,8 @@ logger = logging.getLogger("importers.parsers.jp_morgan.strategy")
|
|
|
14
14
|
def file_name_parse(file_name):
|
|
15
15
|
dates = re.findall("([0-9]{8})", file_name)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if len(dates) != 1:
|
|
18
|
+
raise ValueError("Not exactly 1 date found in the filename")
|
|
19
19
|
return {"valuation_date": datetime.datetime.strptime(dates[0], "%Y%m%d").date()}
|
|
20
20
|
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@ def parse(import_source):
|
|
|
23
23
|
data = list()
|
|
24
24
|
prices = list()
|
|
25
25
|
df_dict = pd.read_excel(BytesIO(import_source.file.read()), engine="openpyxl", sheet_name=None)
|
|
26
|
-
for
|
|
26
|
+
for df in df_dict.values():
|
|
27
27
|
xx, yy = np.where(df == "Ticker")
|
|
28
28
|
if len(xx) == 1 and len(yy) == 1:
|
|
29
29
|
df_info = df.iloc[: xx[0] - 1, :].transpose()
|
|
@@ -20,7 +20,8 @@ product_mapping = {
|
|
|
20
20
|
|
|
21
21
|
def file_name_parse(file_name):
|
|
22
22
|
identifier = re.findall("([0-9]{4}).*", file_name)
|
|
23
|
-
|
|
23
|
+
if len(identifier) != 1:
|
|
24
|
+
raise ValueError("Not exactly 1 identifier found in the filename")
|
|
24
25
|
return identifier[0]
|
|
25
26
|
|
|
26
27
|
|
|
@@ -7,9 +7,10 @@ import re
|
|
|
7
7
|
def file_name_parse(file_name):
|
|
8
8
|
dates = re.findall(r"([0-9]{4}-[0-9]{2}-[0-9]{2})", file_name)
|
|
9
9
|
isin = re.findall(r"\.([a-zA-Z0-9]*)_", file_name)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
if len(dates) != 2:
|
|
11
|
+
raise ValueError("Not 2 dates found in the filename")
|
|
12
|
+
if len(isin) != 1:
|
|
13
|
+
raise ValueError("Not exactly 1 isin found in the filename")
|
|
13
14
|
|
|
14
15
|
return {
|
|
15
16
|
"isin": isin[0],
|
|
@@ -7,7 +7,8 @@ import xlrd
|
|
|
7
7
|
def file_name_parse(file_name):
|
|
8
8
|
isin = re.findall("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})", file_name)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
if len(isin) != 1:
|
|
11
|
+
raise ValueError("Not exactly 1 isin found in the filename")
|
|
11
12
|
|
|
12
13
|
return {"isin": isin[0]}
|
|
13
14
|
|
|
@@ -11,7 +11,8 @@ from wbportfolio.import_export.utils import get_file_extension
|
|
|
11
11
|
def file_name_parse(file_name):
|
|
12
12
|
isin = re.findall("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})", file_name)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
if len(isin) != 1:
|
|
15
|
+
raise ValueError("Not exactly 1 isin found in the filename")
|
|
15
16
|
|
|
16
17
|
return {
|
|
17
18
|
"isin": isin[0],
|
|
@@ -24,7 +24,9 @@ def convert_string_to_number(string):
|
|
|
24
24
|
return 0.0
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def parse_date(date, formats=
|
|
27
|
+
def parse_date(date, formats: list | None = None):
|
|
28
|
+
if formats is None:
|
|
29
|
+
formats = []
|
|
28
30
|
if isinstance(date, int) or isinstance(date, float):
|
|
29
31
|
return xldate_as_datetime(int(date), 0).date()
|
|
30
32
|
if isinstance(date, str):
|
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from django.db.models import QuerySet
|
|
6
6
|
from wbfdm.contrib.metric.backends.base import AbstractBackend, Metric
|
|
7
|
-
from wbfdm.contrib.metric.exceptions import
|
|
7
|
+
from wbfdm.contrib.metric.exceptions import MetricInvalidParameterError
|
|
8
8
|
|
|
9
9
|
from wbportfolio.models import AssetPosition, Portfolio
|
|
10
10
|
|
|
@@ -62,7 +62,7 @@ class PortfolioMetricBaseBackend(AbstractBackend[Portfolio]):
|
|
|
62
62
|
try:
|
|
63
63
|
return qs.latest("date").date
|
|
64
64
|
except AssetPosition.DoesNotExist:
|
|
65
|
-
raise
|
|
65
|
+
raise MetricInvalidParameterError() from None
|
|
66
66
|
|
|
67
67
|
def get_queryset(self) -> QuerySet[Portfolio]:
|
|
68
68
|
product_portfolios = super().get_queryset().filter_active_and_tracked()
|
wbportfolio/models/asset.py
CHANGED
|
@@ -5,6 +5,7 @@ from decimal import Decimal, InvalidOperation
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from django.contrib import admin
|
|
8
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
8
9
|
from django.db import models
|
|
9
10
|
from django.db.models import (
|
|
10
11
|
Case,
|
|
@@ -429,7 +430,7 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
429
430
|
):
|
|
430
431
|
try:
|
|
431
432
|
self.underlying_quote = self.underlying_instrument.children.get(is_primary=True)
|
|
432
|
-
except:
|
|
433
|
+
except ObjectDoesNotExist:
|
|
433
434
|
self.underlying_quote = self.underlying_instrument
|
|
434
435
|
|
|
435
436
|
if not getattr(self, "currency", None):
|
wbportfolio/models/custodians.py
CHANGED
|
@@ -32,7 +32,7 @@ class Custodian(WBModel):
|
|
|
32
32
|
|
|
33
33
|
@classmethod
|
|
34
34
|
def get_by_mapping(cls, mapping: str, use_similarity=False, create_missing=True):
|
|
35
|
-
|
|
35
|
+
similarity_score = 0.7
|
|
36
36
|
lower_mapping = mapping.lower()
|
|
37
37
|
try:
|
|
38
38
|
return cls.objects.get(mapping__contains=[lower_mapping])
|
|
@@ -40,7 +40,7 @@ class Custodian(WBModel):
|
|
|
40
40
|
if use_similarity:
|
|
41
41
|
similar_custodians = cls.objects.annotate(
|
|
42
42
|
similarity_score=TrigramSimilarity("name", lower_mapping)
|
|
43
|
-
).filter(similarity_score__gt=
|
|
43
|
+
).filter(similarity_score__gt=similarity_score)
|
|
44
44
|
if similar_custodians.count() == 1:
|
|
45
45
|
custodian = similar_custodians.first()
|
|
46
46
|
print(f"find similar custodian {lower_mapping} -> {custodian.name}") # noqa: T201
|
|
@@ -50,7 +50,7 @@ class Custodian(WBModel):
|
|
|
50
50
|
else:
|
|
51
51
|
similar_companies = Company.objects.annotate(
|
|
52
52
|
similarity_score=TrigramSimilarity("name", lower_mapping)
|
|
53
|
-
).filter(similarity_score__gt=
|
|
53
|
+
).filter(similarity_score__gt=similarity_score)
|
|
54
54
|
if similar_companies.count() == 1:
|
|
55
55
|
print( # noqa: T201
|
|
56
56
|
f"Find similar company {lower_mapping} -> {similar_companies.first().name}"
|
wbportfolio/models/exceptions.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
class
|
|
1
|
+
class InvalidAnalyticPortfolioError(Exception):
|
|
2
2
|
pass
|