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.

Files changed (128) hide show
  1. wbportfolio/admin/orders/order_proposals.py +2 -0
  2. wbportfolio/admin/orders/orders.py +2 -0
  3. wbportfolio/admin/portfolio.py +11 -5
  4. wbportfolio/api_clients/ubs.py +23 -11
  5. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  6. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  7. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  8. wbportfolio/contrib/company_portfolio/models.py +69 -39
  9. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  10. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  11. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  12. wbportfolio/factories/assets.py +1 -1
  13. wbportfolio/factories/orders/order_proposals.py +3 -1
  14. wbportfolio/factories/orders/orders.py +8 -3
  15. wbportfolio/factories/product_groups.py +3 -3
  16. wbportfolio/factories/products.py +3 -3
  17. wbportfolio/filters/assets.py +0 -1
  18. wbportfolio/filters/orders/order_proposals.py +3 -6
  19. wbportfolio/filters/portfolios.py +18 -1
  20. wbportfolio/filters/positions.py +0 -1
  21. wbportfolio/filters/transactions/fees.py +0 -2
  22. wbportfolio/filters/transactions/trades.py +0 -1
  23. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  24. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  25. wbportfolio/import_export/handlers/asset_position.py +9 -5
  26. wbportfolio/import_export/handlers/dividend.py +1 -1
  27. wbportfolio/import_export/handlers/fees.py +2 -2
  28. wbportfolio/import_export/handlers/trade.py +4 -4
  29. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  30. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  31. wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
  32. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  33. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  34. wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
  35. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  36. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  37. wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
  38. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  39. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  40. wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
  41. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  42. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  43. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  44. wbportfolio/import_export/parsers/ubs/equity.py +2 -1
  45. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  46. wbportfolio/import_export/resources/trades.py +1 -1
  47. wbportfolio/import_export/utils.py +3 -1
  48. wbportfolio/metric/backends/base.py +2 -2
  49. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  50. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  51. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  52. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  53. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  54. wbportfolio/models/adjustments.py +1 -1
  55. wbportfolio/models/asset.py +7 -3
  56. wbportfolio/models/builder.py +25 -5
  57. wbportfolio/models/custodians.py +3 -3
  58. wbportfolio/models/exceptions.py +1 -1
  59. wbportfolio/models/graphs/portfolio.py +1 -1
  60. wbportfolio/models/graphs/utils.py +11 -11
  61. wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
  62. wbportfolio/models/orders/order_proposals.py +620 -490
  63. wbportfolio/models/orders/orders.py +237 -75
  64. wbportfolio/models/portfolio.py +79 -18
  65. wbportfolio/models/portfolio_relationship.py +6 -0
  66. wbportfolio/models/products.py +3 -0
  67. wbportfolio/models/rebalancing.py +4 -1
  68. wbportfolio/models/roles.py +4 -10
  69. wbportfolio/models/transactions/claim.py +6 -5
  70. wbportfolio/models/transactions/dividends.py +1 -0
  71. wbportfolio/models/transactions/trades.py +4 -0
  72. wbportfolio/models/transactions/transactions.py +16 -4
  73. wbportfolio/models/utils.py +100 -1
  74. wbportfolio/order_routing/__init__.py +16 -0
  75. wbportfolio/order_routing/adapters/__init__.py +14 -6
  76. wbportfolio/order_routing/adapters/ubs.py +104 -70
  77. wbportfolio/order_routing/router.py +33 -0
  78. wbportfolio/order_routing/tests/test_router.py +110 -0
  79. wbportfolio/permissions.py +7 -0
  80. wbportfolio/pms/trading/__init__.py +0 -1
  81. wbportfolio/pms/trading/optimizer.py +61 -0
  82. wbportfolio/pms/typing.py +115 -103
  83. wbportfolio/rebalancing/models/composite.py +1 -1
  84. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
  85. wbportfolio/risk_management/backends/__init__.py +1 -0
  86. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  87. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  88. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  89. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  90. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  91. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  92. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  93. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  94. wbportfolio/serializers/orders/order_proposals.py +6 -2
  95. wbportfolio/serializers/orders/orders.py +119 -26
  96. wbportfolio/serializers/transactions/claim.py +2 -2
  97. wbportfolio/tasks.py +42 -4
  98. wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
  99. wbportfolio/tests/models/test_portfolios.py +9 -9
  100. wbportfolio/tests/models/test_splits.py +1 -6
  101. wbportfolio/tests/models/test_utils.py +140 -0
  102. wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
  103. wbportfolio/tests/rebalancing/test_models.py +2 -2
  104. wbportfolio/tests/viewsets/test_products.py +1 -0
  105. wbportfolio/urls.py +1 -1
  106. wbportfolio/viewsets/charts/assets.py +8 -4
  107. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  108. wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
  109. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  110. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  111. wbportfolio/viewsets/esg.py +3 -5
  112. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
  113. wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
  114. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
  115. wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
  116. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
  117. wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
  118. wbportfolio/viewsets/orders/order_proposals.py +92 -21
  119. wbportfolio/viewsets/orders/orders.py +79 -26
  120. wbportfolio/viewsets/portfolios.py +24 -0
  121. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
  122. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
  123. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  124. wbportfolio/fdm/tasks.py +0 -42
  125. wbportfolio/models/orders/routing.py +0 -54
  126. wbportfolio/pms/trading/handler.py +0 -211
  127. /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
  128. {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(product_group, *args, **kwargs):
25
- if product_group.id and not product_group.portfolios.exists():
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=product_group, portfolio=portfolio)
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(product, *args, **kwargs):
44
- if product.id and not product.portfolios.exists():
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=product, portfolio=portfolio)
46
+ InstrumentPortfolioThroughModel.objects.create(instrument=self, portfolio=portfolio)
47
47
 
48
48
  # wbportfolio = factory.SubFactory(PortfolioFactory)
49
49
  # portfolio_computed = factory.SubFactory(PortfolioFactory)
@@ -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 = {
@@ -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
  )
@@ -1,3 +1,4 @@
1
1
  from .asset_position import *
2
2
  from .fees import *
3
3
  from .instrument_price import *
4
+ from .trade import *
@@ -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 not (value := data.get(field, None)) is None:
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 not (value := data.get(field.name, None)) is None and isinstance(field, models.DecimalField):
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 not (value := data.get(field.name, None)) is None and isinstance(field, models.DecimalField):
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 := getattr(backend_class, "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
- assert len(dates) > 0
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
- assert len(dates) == 1, "Not exactly 1 date found in the filename"
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
- assert len(dates) == 1, "Not exactly 1 date found in the filename"
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
- valuation_date = parts["valuation_date"]
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
- parents_strategies = set()
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
- bbg_tickers = strategy_data["BBG Ticker"].split(" ")
64
- exchange = None
65
- if len(bbg_tickers) == 2:
66
- ticker = bbg_tickers[0]
67
- instrument_type = bbg_tickers[1]
68
- elif len(bbg_tickers) == 3:
69
- ticker = bbg_tickers[0]
70
- exchange = bbg_tickers[1]
71
- instrument_type = bbg_tickers[2]
72
-
73
- strategy = strategy_data["Strategy Ticker"].replace("Index", "").strip()
74
- strategy_currency_key = strategy_data["Strategy CCY"]
75
-
76
- position_currency_key = strategy_data["Position CCY"]
77
-
78
- isin = strategy_data["Position ISIN"]
79
- name = strategy_data["Position Description"]
80
- initial_price = convert_string_to_number(strategy_data["Prices"])
81
- initial_currency_fx_rate = convert_string_to_number(strategy_data["Fx Rates"])
82
- if exchange:
83
- exchange = {"bbg_exchange_codes": exchange}
84
- try:
85
- weighting = convert_string_to_number(strategy_data["Weight In Percent"].replace("%", "")) / 100
86
- except Exception:
87
- weighting = 0.0
88
- underlying_quote = {
89
- "ticker": ticker,
90
- "exchange": exchange,
91
- "isin": isin,
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
- "is_estimated": False,
67
+ "isin": isin,
68
+ "name": name,
108
69
  "currency__key": position_currency_key,
109
- "initial_currency_fx_rate": initial_currency_fx_rate,
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
- parents_strategies.add(strategy)
116
- manual_data = manually_create_100_position(parents_strategies, valuation_date)
117
- return {"data": data + manual_data}
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
- assert len(dates) == 1, "Not exactly 1 date found in the filename"
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
- assert len(isin) == 1, "Not exactly 1 isin found in the filename"
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
- df = pd.read_csv(import_source.file, encoding="utf-16", delimiter=";")
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
- df["portfolio__isin"] = product_data["isin"]
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}-?[0-9]{2}-?[0-9]{2})", file_name)
57
- identifier = re.findall(r"([A-Z]{2}(?![A-Z]{10}\b)[A-Z0-9]{10})_", file_name)
58
- assert len(dates) == 2, "Not 2 dates found in the filename"
59
- assert len(identifier) == 1, "Not exactly 1 identifier found in the filename"
60
- try:
61
- valuation_date = datetime.datetime.strptime(dates[0], "%Y-%m-%d").date()
62
- except ValueError:
63
- valuation_date = datetime.datetime.strptime(dates[0], "%Y%m%d").date()
64
- try:
65
- generation_date = datetime.datetime.strptime(dates[1], "%Y-%m-%d").date()
66
- except ValueError:
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
- assert len(dates) == 2, "Not 2 dates found in the filename"
16
- assert len(isin) == 1, "Not exactly 1 isin found in the filename"
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],