wbportfolio 1.55.8__py2.py3-none-any.whl → 1.59.4__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/admin/orders/order_proposals.py +2 -0
- wbportfolio/admin/orders/orders.py +2 -0
- wbportfolio/admin/portfolio.py +11 -5
- wbportfolio/api_clients/ubs.py +23 -11
- wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
- wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
- wbportfolio/contrib/company_portfolio/filters.py +10 -10
- wbportfolio/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/scripts.py +7 -2
- wbportfolio/contrib/company_portfolio/serializers.py +32 -22
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- wbportfolio/factories/assets.py +1 -1
- wbportfolio/factories/orders/order_proposals.py +3 -1
- wbportfolio/factories/orders/orders.py +8 -3
- wbportfolio/factories/product_groups.py +3 -3
- wbportfolio/factories/products.py +3 -3
- wbportfolio/filters/assets.py +0 -1
- wbportfolio/filters/orders/order_proposals.py +3 -6
- wbportfolio/filters/portfolios.py +18 -1
- wbportfolio/filters/positions.py +0 -1
- wbportfolio/filters/transactions/fees.py +0 -2
- wbportfolio/filters/transactions/trades.py +0 -1
- wbportfolio/import_export/backends/ubs/__init__.py +1 -0
- wbportfolio/import_export/backends/ubs/trade.py +48 -0
- wbportfolio/import_export/handlers/asset_position.py +9 -5
- wbportfolio/import_export/handlers/dividend.py +1 -1
- wbportfolio/import_export/handlers/fees.py +2 -2
- wbportfolio/import_export/handlers/trade.py +4 -4
- 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 +59 -85
- wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
- wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
- wbportfolio/import_export/parsers/natixis/equity.py +22 -4
- wbportfolio/import_export/parsers/natixis/utils.py +13 -19
- 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/api/trade.py +39 -0
- wbportfolio/import_export/parsers/ubs/equity.py +2 -1
- wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
- wbportfolio/import_export/resources/trades.py +1 -1
- wbportfolio/import_export/utils.py +3 -1
- wbportfolio/metric/backends/base.py +2 -2
- wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
- wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
- wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
- wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
- wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
- wbportfolio/models/adjustments.py +1 -1
- wbportfolio/models/asset.py +7 -3
- wbportfolio/models/builder.py +25 -5
- wbportfolio/models/custodians.py +3 -3
- wbportfolio/models/exceptions.py +1 -1
- wbportfolio/models/graphs/portfolio.py +1 -1
- wbportfolio/models/graphs/utils.py +11 -11
- wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
- wbportfolio/models/orders/order_proposals.py +620 -490
- wbportfolio/models/orders/orders.py +237 -75
- wbportfolio/models/portfolio.py +79 -18
- wbportfolio/models/portfolio_relationship.py +6 -0
- wbportfolio/models/products.py +3 -0
- wbportfolio/models/rebalancing.py +4 -1
- wbportfolio/models/roles.py +4 -10
- wbportfolio/models/transactions/claim.py +6 -5
- wbportfolio/models/transactions/dividends.py +1 -0
- wbportfolio/models/transactions/trades.py +4 -0
- wbportfolio/models/transactions/transactions.py +16 -4
- wbportfolio/models/utils.py +100 -1
- wbportfolio/order_routing/__init__.py +16 -0
- wbportfolio/order_routing/adapters/__init__.py +14 -6
- wbportfolio/order_routing/adapters/ubs.py +104 -70
- wbportfolio/order_routing/router.py +33 -0
- wbportfolio/order_routing/tests/test_router.py +110 -0
- wbportfolio/permissions.py +7 -0
- wbportfolio/pms/trading/__init__.py +0 -1
- wbportfolio/pms/trading/optimizer.py +61 -0
- wbportfolio/pms/typing.py +115 -103
- wbportfolio/rebalancing/models/composite.py +1 -1
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
- wbportfolio/risk_management/backends/__init__.py +1 -0
- wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
- wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
- wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
- wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
- wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
- wbportfolio/risk_management/tests/test_exposure_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/orders/order_proposals.py +6 -2
- wbportfolio/serializers/orders/orders.py +119 -26
- wbportfolio/serializers/transactions/claim.py +2 -2
- wbportfolio/tasks.py +42 -4
- wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
- wbportfolio/tests/models/test_portfolios.py +9 -9
- wbportfolio/tests/models/test_splits.py +1 -6
- wbportfolio/tests/models/test_utils.py +140 -0
- wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
- wbportfolio/tests/rebalancing/test_models.py +2 -2
- wbportfolio/tests/viewsets/test_products.py +1 -0
- wbportfolio/urls.py +1 -1
- wbportfolio/viewsets/charts/assets.py +8 -4
- wbportfolio/viewsets/configs/buttons/assets.py +1 -1
- wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
- wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
- wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
- wbportfolio/viewsets/esg.py +3 -5
- wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
- wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
- wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
- wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
- wbportfolio/viewsets/orders/order_proposals.py +92 -21
- wbportfolio/viewsets/orders/orders.py +79 -26
- wbportfolio/viewsets/portfolios.py +24 -0
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
- wbportfolio/fdm/tasks.py +0 -42
- wbportfolio/models/orders/routing.py +0 -54
- wbportfolio/pms/trading/handler.py +0 -211
- /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
- {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|
wbportfolio/filters/assets.py
CHANGED
|
@@ -456,7 +456,6 @@ def get_default_hedged_currency(field, request, view):
|
|
|
456
456
|
|
|
457
457
|
class ContributionChartFilter(wb_filters.FilterSet):
|
|
458
458
|
date = wb_filters.FinancialPerformanceDateRangeFilter(
|
|
459
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
460
459
|
label="Date Range",
|
|
461
460
|
required=True,
|
|
462
461
|
clearable=False,
|
|
@@ -28,14 +28,11 @@ class OrderProposalFilterSet(wb_filters.FilterSet):
|
|
|
28
28
|
return queryset
|
|
29
29
|
|
|
30
30
|
def filter_waiting_for_input(self, queryset, name, value):
|
|
31
|
+
input_status = [OrderProposal.Status.PENDING, OrderProposal.Status.DRAFT, OrderProposal.Status.APPROVED]
|
|
31
32
|
if value is True:
|
|
32
|
-
queryset = queryset.filter(
|
|
33
|
-
status__in=[OrderProposal.Status.PENDING, OrderProposal.Status.DRAFT, OrderProposal.Status.APPROVED]
|
|
34
|
-
)
|
|
33
|
+
queryset = queryset.filter(status__in=input_status)
|
|
35
34
|
elif value is False:
|
|
36
|
-
queryset = queryset.exclude(
|
|
37
|
-
status__in=[OrderProposal.Status.PENDING, OrderProposal.Status.DRAFT, OrderProposal.Status.APPROVED]
|
|
38
|
-
)
|
|
35
|
+
queryset = queryset.exclude(status__in=input_status)
|
|
39
36
|
return queryset
|
|
40
37
|
|
|
41
38
|
def filter_is_automatic_rebalancing(self, queryset, name, value):
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from datetime import date, timedelta
|
|
2
2
|
|
|
3
|
+
from django.db.models import Exists, OuterRef
|
|
3
4
|
from wbcore import filters as wb_filters
|
|
4
5
|
from wbfdm.models import Instrument
|
|
5
6
|
|
|
6
7
|
from wbportfolio.filters.assets import get_latest_asset_position
|
|
7
|
-
from wbportfolio.models import Portfolio
|
|
8
|
+
from wbportfolio.models import AssetPosition, Portfolio
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class PortfolioFilterSet(wb_filters.FilterSet):
|
|
@@ -26,6 +27,15 @@ class PortfolioFilterSet(wb_filters.FilterSet):
|
|
|
26
27
|
label_key=Portfolio.get_representation_label_key(),
|
|
27
28
|
method="filter_modeled_after",
|
|
28
29
|
)
|
|
30
|
+
invests_in = wb_filters.ModelChoiceFilter(
|
|
31
|
+
label="Invests In",
|
|
32
|
+
queryset=Instrument.objects.all(),
|
|
33
|
+
endpoint=Instrument.get_representation_endpoint(),
|
|
34
|
+
value_key=Instrument.get_representation_value_key(),
|
|
35
|
+
label_key=Instrument.get_representation_label_key(),
|
|
36
|
+
filter_params={"is_investable_universe": True},
|
|
37
|
+
method="filter_invests_in",
|
|
38
|
+
)
|
|
29
39
|
|
|
30
40
|
def filter_modeled_after(self, queryset, name, value):
|
|
31
41
|
if value:
|
|
@@ -42,6 +52,13 @@ class PortfolioFilterSet(wb_filters.FilterSet):
|
|
|
42
52
|
return queryset.filter(instruments=value)
|
|
43
53
|
return queryset
|
|
44
54
|
|
|
55
|
+
def filter_invests_in(self, queryset, name, value):
|
|
56
|
+
if value:
|
|
57
|
+
return queryset.annotate(
|
|
58
|
+
invests_in=Exists(AssetPosition.objects.filter(underlying_quote=value, portfolio=OuterRef("pk")))
|
|
59
|
+
).filter(invests_in=True)
|
|
60
|
+
return queryset
|
|
61
|
+
|
|
45
62
|
class Meta:
|
|
46
63
|
model = Portfolio
|
|
47
64
|
fields = {
|
wbportfolio/filters/positions.py
CHANGED
|
@@ -76,7 +76,6 @@ class AssetPositionPandasFilter(DateFilterMixin, PandasFilterSetMixin, wb_filter
|
|
|
76
76
|
date = total_value_fx_usd = total_value_fx_usd__gte = total_value_fx_usd__lte = None
|
|
77
77
|
|
|
78
78
|
date = wb_filters.FinancialPerformanceDateRangeFilter(
|
|
79
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
80
79
|
label="Date Range",
|
|
81
80
|
required=True,
|
|
82
81
|
clearable=False,
|
|
@@ -16,7 +16,6 @@ class FeesFilter(wb_filters.FilterSet):
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
fee_date = wb_filters.DateRangeFilter(
|
|
19
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
20
19
|
label="Date Range",
|
|
21
20
|
initial=get_transaction_default_date_range,
|
|
22
21
|
required=True,
|
|
@@ -52,7 +51,6 @@ class FeesProductFilterSet(FeesFilter):
|
|
|
52
51
|
class FeesAggregatedFilter(PandasFilterSetMixin, wb_filters.FilterSet):
|
|
53
52
|
fee_date = wb_filters.DateRangeFilter(
|
|
54
53
|
label="Date Range",
|
|
55
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
56
54
|
required=True,
|
|
57
55
|
clearable=False,
|
|
58
56
|
initial=current_quarter_date_range,
|
|
@@ -14,7 +14,6 @@ from .utils import get_transaction_default_date_range
|
|
|
14
14
|
|
|
15
15
|
class TradeFilter(OppositeSharesFieldMethodMixin, wb_filters.FilterSet):
|
|
16
16
|
transaction_date = wb_filters.DateRangeFilter(
|
|
17
|
-
method=wb_filters.DateRangeFilter.base_date_range_filter_method,
|
|
18
17
|
label="Date Range",
|
|
19
18
|
initial=get_transaction_default_date_range,
|
|
20
19
|
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import date, datetime
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from django.db import models
|
|
7
|
+
from pandas.tseries.offsets import BDay
|
|
8
|
+
from wbcore.contrib.io.backends import AbstractDataBackend, register
|
|
9
|
+
|
|
10
|
+
from wbportfolio.api_clients.ubs import UBSNeoAPIClient
|
|
11
|
+
|
|
12
|
+
from .mixin import DataBackendMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@register("Trade", provider_key="ubs", save_data_in_import_source=True, passive_only=True)
|
|
16
|
+
class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
17
|
+
def __init__(
|
|
18
|
+
self, import_credential: Optional[models.Model] = None, ubs_bank: Optional[models.Model] = None, **kwargs
|
|
19
|
+
):
|
|
20
|
+
if not ubs_bank:
|
|
21
|
+
raise ValueError("The ubs company objects needs to be passed to this backend")
|
|
22
|
+
self.ubs_bank = ubs_bank
|
|
23
|
+
if not import_credential or not import_credential.authentication_token:
|
|
24
|
+
raise ValueError("UBS backend needs a valid import credential object")
|
|
25
|
+
self.authentication_token = import_credential.authentication_token.replace("Bearer ", "")
|
|
26
|
+
self.token_expiry_date = import_credential.validity_end
|
|
27
|
+
|
|
28
|
+
def get_files(
|
|
29
|
+
self,
|
|
30
|
+
execution_time: datetime,
|
|
31
|
+
start: date = None,
|
|
32
|
+
obj_external_ids: list[str] = None,
|
|
33
|
+
**kwargs,
|
|
34
|
+
) -> BytesIO:
|
|
35
|
+
if not start:
|
|
36
|
+
start = (execution_time - BDay(2)).date()
|
|
37
|
+
end = execution_time.date()
|
|
38
|
+
if obj_external_ids:
|
|
39
|
+
client = UBSNeoAPIClient(self.authentication_token, self.token_expiry_date)
|
|
40
|
+
for external_id in obj_external_ids:
|
|
41
|
+
res_json = client.validate_response(
|
|
42
|
+
client.get_rebalance_reports(external_id, from_date=start, to_date=end)
|
|
43
|
+
)
|
|
44
|
+
if res_json:
|
|
45
|
+
content_file = BytesIO()
|
|
46
|
+
content_file.write(json.dumps(res_json).encode())
|
|
47
|
+
file_name = f"ubs_trades_{external_id}_{start:%Y-%m-%d}-{end:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
|
|
48
|
+
yield file_name, content_file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from contextlib import suppress
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
4
|
from decimal import Decimal
|
|
5
5
|
from itertools import chain
|
|
6
6
|
from typing import Any, Dict, List, Optional
|
|
@@ -31,7 +31,7 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
31
31
|
self.instrument_price_handler = InstrumentPriceImportHandler(self.import_source)
|
|
32
32
|
self.currency_handler = CurrencyImportHandler(self.import_source)
|
|
33
33
|
|
|
34
|
-
def _deserialize(self, data: Dict[str, Any]):
|
|
34
|
+
def _deserialize(self, data: Dict[str, Any]): # noqa: C901
|
|
35
35
|
from wbportfolio.models import Portfolio
|
|
36
36
|
|
|
37
37
|
portfolio_data = data.pop("portfolio", None)
|
|
@@ -45,6 +45,10 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
45
45
|
data["initial_price"] /= 1000
|
|
46
46
|
data["currency"] = currency
|
|
47
47
|
data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
|
|
48
|
+
|
|
49
|
+
# ensure that the position falls into a weekday
|
|
50
|
+
if data["date"].weekday() == 5:
|
|
51
|
+
data["date"] -= timedelta(days=1)
|
|
48
52
|
if data.get("asset_valuation_date", None):
|
|
49
53
|
data["asset_valuation_date"] = datetime.strptime(data["asset_valuation_date"], "%Y-%m-%d").date()
|
|
50
54
|
else:
|
|
@@ -78,14 +82,14 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
78
82
|
data["initial_price"] = data["underlying_quote"].get_price(
|
|
79
83
|
data["date"], price_date_timedelta=self.MAX_PRICE_DATE_TIMEDELTA
|
|
80
84
|
)
|
|
81
|
-
except ValueError:
|
|
82
|
-
raise DeserializationError("Price not provided but can not be found automatically")
|
|
85
|
+
except ValueError as e:
|
|
86
|
+
raise DeserializationError("Price not provided but can not be found automatically") from e
|
|
83
87
|
|
|
84
88
|
# number type deserialization and sanitization
|
|
85
89
|
# ensure the provided Decimal field are of type Decimal
|
|
86
90
|
decimal_fields = ["initial_currency_fx_rate", "initial_price", "initial_shares", "weighting"]
|
|
87
91
|
for field in decimal_fields:
|
|
88
|
-
if
|
|
92
|
+
if (value := data.get(field, None)) is not None:
|
|
89
93
|
data[field] = Decimal(value)
|
|
90
94
|
|
|
91
95
|
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
|
|
@@ -86,7 +86,7 @@ class TradeImportHandler(ImportExportHandler):
|
|
|
86
86
|
data["transaction_date"] = data["book_date"]
|
|
87
87
|
return self.model.objects.create(**data, import_source=self.import_source)
|
|
88
88
|
|
|
89
|
-
def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model:
|
|
89
|
+
def _get_instance(self, data: Dict[str, Any], history: Optional[models.QuerySet] = None, **kwargs) -> models.Model: # noqa: C901
|
|
90
90
|
self.import_source.log += "\nGet Trade Instance."
|
|
91
91
|
if transaction_date := data.get("transaction_date"):
|
|
92
92
|
dates_lookup = {"transaction_date": transaction_date}
|
|
@@ -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
|
|
|
@@ -12,106 +12,80 @@ logger = logging.getLogger("importers.parsers.jp_morgan.strategy")
|
|
|
12
12
|
|
|
13
13
|
def file_name_parse(file_name):
|
|
14
14
|
dates = re.findall("([0-9]{8})", file_name)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return {"valuation_date": datetime.datetime.strptime(dates[0], "%Y%m%d").date()}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def manually_create_100_position(parent_strategies, valuation_date):
|
|
22
|
-
data = []
|
|
23
|
-
from wbportfolio.models import Index, Product
|
|
24
|
-
|
|
25
|
-
for strategy_ticker in parent_strategies:
|
|
26
|
-
if index := Index.objects.filter(ticker=strategy_ticker).first():
|
|
27
|
-
for product in Product.objects.filter(ticker=strategy_ticker):
|
|
28
|
-
valuations = product.valuations.filter(date__lte=valuation_date)
|
|
29
|
-
last_price = 0
|
|
30
|
-
if valuations.exists():
|
|
31
|
-
last_price = float(valuations.latest("date").net_value)
|
|
32
|
-
data.append(
|
|
33
|
-
{
|
|
34
|
-
"underlying_quote": index.id,
|
|
35
|
-
"portfolio": {"instrument_type": "product", "id": product.id},
|
|
36
|
-
"currency__key": index.currency.key,
|
|
37
|
-
"initial_currency_fx_rate": 1.0,
|
|
38
|
-
"weighting": 1.0,
|
|
39
|
-
"is_estimated": True, # this position is not a real position, it is created by the importer.
|
|
40
|
-
"initial_price": last_price,
|
|
41
|
-
"date": valuation_date.strftime("%Y-%m-%d"),
|
|
42
|
-
}
|
|
43
|
-
)
|
|
44
|
-
return data
|
|
15
|
+
if dates:
|
|
16
|
+
return {"valuation_date": datetime.datetime.strptime(dates[0], "%Y%m%d").date()}
|
|
17
|
+
return {}
|
|
45
18
|
|
|
46
19
|
|
|
47
20
|
def parse(import_source):
|
|
48
21
|
# Load file into a CSV DictReader
|
|
49
22
|
|
|
50
23
|
df = pd.read_csv(import_source.file, encoding="utf-16", delimiter=",")
|
|
51
|
-
df = df.replace([np.inf, -np.inf, np.nan], None)
|
|
52
24
|
|
|
53
25
|
# Parse the Parts of the filename into the different parts
|
|
54
26
|
parts = file_name_parse(import_source.file.name)
|
|
55
27
|
|
|
56
28
|
# Get the valuation date from the parts list
|
|
57
|
-
|
|
29
|
+
report_date = parts.get("valuation_date")
|
|
58
30
|
|
|
59
31
|
# Iterate through the CSV File and parse the data into a list
|
|
60
32
|
data = list()
|
|
61
|
-
|
|
33
|
+
if "Date" in df.columns:
|
|
34
|
+
df["Date"] = pd.to_datetime(df["Date"])
|
|
35
|
+
df.replace([np.inf, -np.inf, np.nan], None, inplace=True)
|
|
62
36
|
for strategy_data in df.to_dict("records"):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"name": name,
|
|
93
|
-
"currency__key": position_currency_key,
|
|
94
|
-
"instrument_type": instrument_type.lower(),
|
|
95
|
-
}
|
|
96
|
-
if isin:
|
|
97
|
-
underlying_quote["isin"] = isin
|
|
98
|
-
data.append(
|
|
99
|
-
{
|
|
100
|
-
"underlying_quote": underlying_quote,
|
|
101
|
-
"portfolio": {
|
|
102
|
-
"instrument_type": "index",
|
|
103
|
-
"ticker": strategy,
|
|
104
|
-
"currency__key": strategy_currency_key,
|
|
105
|
-
},
|
|
37
|
+
valuation_date = strategy_data.get("Date", report_date)
|
|
38
|
+
if valuation_date:
|
|
39
|
+
bbg_tickers = strategy_data["BBG Ticker"].split(" ")
|
|
40
|
+
exchange = None
|
|
41
|
+
if len(bbg_tickers) == 2:
|
|
42
|
+
ticker = bbg_tickers[0]
|
|
43
|
+
instrument_type = bbg_tickers[1]
|
|
44
|
+
elif len(bbg_tickers) == 3:
|
|
45
|
+
ticker = bbg_tickers[0]
|
|
46
|
+
exchange = bbg_tickers[1]
|
|
47
|
+
instrument_type = bbg_tickers[2]
|
|
48
|
+
|
|
49
|
+
strategy = strategy_data["Strategy Ticker"].replace("Index", "").strip()
|
|
50
|
+
strategy_currency_key = strategy_data["Strategy CCY"]
|
|
51
|
+
|
|
52
|
+
position_currency_key = strategy_data["Position CCY"]
|
|
53
|
+
|
|
54
|
+
isin = strategy_data["Position ISIN"]
|
|
55
|
+
name = strategy_data["Position Description"]
|
|
56
|
+
initial_price = convert_string_to_number(strategy_data["Prices"])
|
|
57
|
+
initial_currency_fx_rate = convert_string_to_number(strategy_data["Fx Rates"])
|
|
58
|
+
if exchange:
|
|
59
|
+
exchange = {"bbg_exchange_codes": exchange}
|
|
60
|
+
try:
|
|
61
|
+
weighting = convert_string_to_number(strategy_data["Weight In Percent"].replace("%", "")) / 100
|
|
62
|
+
except Exception:
|
|
63
|
+
weighting = 0.0
|
|
64
|
+
underlying_quote = {
|
|
65
|
+
"ticker": ticker,
|
|
106
66
|
"exchange": exchange,
|
|
107
|
-
"
|
|
67
|
+
"isin": isin,
|
|
68
|
+
"name": name,
|
|
108
69
|
"currency__key": position_currency_key,
|
|
109
|
-
"
|
|
110
|
-
"weighting": weighting,
|
|
111
|
-
"initial_price": initial_price,
|
|
112
|
-
"date": valuation_date.strftime("%Y-%m-%d"),
|
|
70
|
+
"instrument_type": instrument_type.lower(),
|
|
113
71
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
72
|
+
if isin:
|
|
73
|
+
underlying_quote["isin"] = isin
|
|
74
|
+
data.append(
|
|
75
|
+
{
|
|
76
|
+
"underlying_quote": underlying_quote,
|
|
77
|
+
"portfolio": {
|
|
78
|
+
"instrument_type": "index",
|
|
79
|
+
"ticker": strategy,
|
|
80
|
+
"currency__key": strategy_currency_key,
|
|
81
|
+
},
|
|
82
|
+
"exchange": exchange,
|
|
83
|
+
"is_estimated": False,
|
|
84
|
+
"currency__key": position_currency_key,
|
|
85
|
+
"initial_currency_fx_rate": initial_currency_fx_rate,
|
|
86
|
+
"weighting": weighting,
|
|
87
|
+
"initial_price": initial_price,
|
|
88
|
+
"date": valuation_date.strftime("%Y-%m-%d"),
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
return {"data": data}
|
|
@@ -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
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from zipfile import BadZipFile
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
|
|
@@ -29,15 +31,28 @@ def _apply_adjusting_factor(row):
|
|
|
29
31
|
def parse(import_source):
|
|
30
32
|
# Parse the Parts of the filename into the different parts
|
|
31
33
|
parts = file_name_parse_isin(import_source.file.name)
|
|
32
|
-
|
|
33
34
|
# Get the valuation date and investment from the parts list
|
|
34
35
|
valuation_date = parts["valuation_date"]
|
|
35
36
|
product_data = parts["product"]
|
|
36
37
|
|
|
37
38
|
# Load file into a CSV DictReader
|
|
38
|
-
|
|
39
|
+
if import_source.file.name.lower().endswith(".csv"):
|
|
40
|
+
df = pd.read_csv(import_source.file, encoding="utf-16", delimiter=";")
|
|
41
|
+
else:
|
|
42
|
+
try:
|
|
43
|
+
df = pd.read_excel(import_source.file, engine="openpyxl", sheet_name="Basket Valuation")
|
|
44
|
+
except BadZipFile:
|
|
45
|
+
df = pd.read_excel(import_source.file, engine="xlrd", sheet_name="Basket Valuation")
|
|
46
|
+
xx, yy = np.where(df.isin(["Ticker", "Code"]))
|
|
47
|
+
if xx.size > 0 and yy.size > 0:
|
|
48
|
+
df = df.iloc[xx[0] :, yy[0] :]
|
|
49
|
+
df = df.rename(columns=df.iloc[0]).drop(df.index[0]).dropna(how="all")
|
|
50
|
+
df["Quotity/Adj. factor"] = 1.0
|
|
51
|
+
df = df.rename(columns={"Code": "Ticker"})
|
|
52
|
+
else:
|
|
53
|
+
return {}
|
|
39
54
|
df = df.rename(columns=FIELD_MAP)
|
|
40
|
-
df = df.dropna(subset=["initial_price"])
|
|
55
|
+
df = df.dropna(subset=["initial_price", "Name"], how="any")
|
|
41
56
|
df["initial_price"] = df["initial_price"].astype("str").str.replace(" ", "").astype("float")
|
|
42
57
|
df["underlying_quote"] = df[["Ticker", "Name", "currency__key"]].apply(
|
|
43
58
|
lambda x: _get_underlying_instrument(*x), axis=1
|
|
@@ -50,7 +65,10 @@ def parse(import_source):
|
|
|
50
65
|
df = df.drop(columns=df.columns.difference(FIELD_MAP.values()))
|
|
51
66
|
|
|
52
67
|
df["portfolio__instrument_type"] = "product"
|
|
53
|
-
|
|
68
|
+
if "isin" in product_data:
|
|
69
|
+
df["portfolio__isin"] = product_data["isin"]
|
|
70
|
+
if "ticker" in product_data:
|
|
71
|
+
df["portfolio__ticker"] = product_data["ticker"]
|
|
54
72
|
df["is_estimated"] = False
|
|
55
73
|
df["date"] = valuation_date.strftime("%Y-%m-%d")
|
|
56
74
|
df["asset_valuation_date"] = pd.to_datetime(df["asset_valuation_date"], dayfirst=True).dt.strftime("%Y-%m-%d")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import re
|
|
3
2
|
|
|
3
|
+
from django.utils.dateparse import parse_date
|
|
4
|
+
|
|
4
5
|
from wbportfolio.models import Product
|
|
5
6
|
|
|
6
7
|
INSTRUMENT_MAP_NAME = {"EDA23_AtonRa Z class": "LU2170995018"}
|
|
@@ -53,21 +54,14 @@ def _get_underlying_instrument(bbg_code, name, currency, instrument_type="equity
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def file_name_parse_isin(file_name):
|
|
56
|
-
dates = re.findall(r"_([0-9]{4}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
generation_date = datetime.datetime.strptime(dates[1], "%Y%m%d").date()
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
"product": {"isin": identifier[0]},
|
|
71
|
-
"valuation_date": valuation_date,
|
|
72
|
-
"generation_date": generation_date,
|
|
73
|
-
}
|
|
57
|
+
dates = re.findall(r"_([0-9]{4}[-_]?[0-9]{2}[-_]?[0-9]{2})", file_name)
|
|
58
|
+
isin = re.findall(r"([A-Z]{2}(?![A-Z]{10}\b)[A-Z0-9]{10})_", file_name)
|
|
59
|
+
ticker = re.findall(r"(NX[A-Z]*)_", file_name)
|
|
60
|
+
if len(dates) == 0:
|
|
61
|
+
raise ValueError("Not dates found in the filename")
|
|
62
|
+
res = {"valuation_date": parse_date(dates[0].replace("_", "-"))}
|
|
63
|
+
if len(isin) >= 1:
|
|
64
|
+
res["product"] = {"isin": isin[0]}
|
|
65
|
+
elif len(ticker) == 1:
|
|
66
|
+
res["product"] = {"ticker": ticker[0]}
|
|
67
|
+
return res
|
|
@@ -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],
|