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
@@ -64,12 +64,15 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
64
64
  @wb_serializers.register_only_instance_resource()
65
65
  def additional_resources(self, instance, request, user, **kwargs):
66
66
  res = {}
67
- if instance.status == OrderProposal.Status.APPLIED:
67
+ if instance.status == OrderProposal.Status.CONFIRMED:
68
68
  res["replay"] = reverse("wbportfolio:orderproposal-replay", args=[instance.id], request=request)
69
69
  if instance.status == OrderProposal.Status.DRAFT:
70
70
  res["reset"] = reverse("wbportfolio:orderproposal-reset", args=[instance.id], request=request)
71
71
  res["normalize"] = reverse("wbportfolio:orderproposal-normalize", args=[instance.id], request=request)
72
- res["deleteall"] = reverse("wbportfolio:orderproposal-deleteall", args=[instance.id], request=request)
72
+ if instance.status == OrderProposal.Status.DRAFT or instance.can_be_confirmed:
73
+ res["refresh_return"] = reverse(
74
+ "wbportfolio:orderproposal-refreshreturn", args=[instance.id], request=request
75
+ )
73
76
  res["orders"] = reverse(
74
77
  "wbportfolio:orderproposal-order-list",
75
78
  args=[instance.id],
@@ -90,6 +93,7 @@ class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
90
93
  "comment",
91
94
  "status",
92
95
  "min_order_value",
96
+ "min_weighting",
93
97
  "_rebalancing_model",
94
98
  "rebalancing_model",
95
99
  "target_portfolio",
@@ -1,6 +1,8 @@
1
1
  from decimal import Decimal
2
2
 
3
3
  from rest_framework import serializers
4
+ from rest_framework.reverse import reverse
5
+ from rest_framework.validators import UniqueTogetherValidator
4
6
  from wbcore import serializers as wb_serializers
5
7
  from wbcore.metadata.configs.display.list_display import BaseTreeGroupLevelOption
6
8
  from wbfdm.models import Instrument
@@ -10,7 +12,7 @@ from wbfdm.serializers.instruments.instruments import (
10
12
  SecurityRepresentationSerializer,
11
13
  )
12
14
 
13
- from wbportfolio.models import Order
15
+ from wbportfolio.models import Order, OrderProposal
14
16
 
15
17
 
16
18
  class GetSecurityDefault:
@@ -44,32 +46,50 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
44
46
  underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
45
47
  underlying_instrument_exchange = wb_serializers.CharField(read_only=True)
46
48
 
47
- target_weight = wb_serializers.DecimalField(
49
+ effective_weight = wb_serializers.DecimalField(
50
+ read_only=True,
48
51
  max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
49
52
  decimal_places=Order.ORDER_WEIGHTING_PRECISION,
50
- required=False,
51
53
  default=0,
52
54
  )
53
- effective_weight = wb_serializers.DecimalField(
54
- read_only=True,
55
+ target_weight = wb_serializers.DecimalField(
55
56
  max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
56
57
  decimal_places=Order.ORDER_WEIGHTING_PRECISION,
57
- default=0,
58
+ required=False,
58
59
  )
59
60
 
60
61
  effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
61
- target_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
62
+ target_shares = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=6)
62
63
 
63
- total_value_fx_portfolio = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=2, default=0)
64
64
  effective_total_value_fx_portfolio = wb_serializers.DecimalField(
65
65
  read_only=True, max_digits=16, decimal_places=2, default=0
66
66
  )
67
- target_total_value_fx_portfolio = wb_serializers.DecimalField(
68
- read_only=True, max_digits=16, decimal_places=2, default=0
69
- )
67
+ target_total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
68
+ total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
70
69
 
71
70
  portfolio_currency = wb_serializers.CharField(read_only=True)
71
+ underlying_instrument_currency = wb_serializers.CharField(read_only=True)
72
72
  has_warnings = wb_serializers.BooleanField(read_only=True)
73
+ execution_instruction_parameters_repr = wb_serializers.CharField(read_only=True)
74
+ execution_date = wb_serializers.DateField(read_only=True)
75
+ execution_price = wb_serializers.FloatField(read_only=True)
76
+ execution_traded_shares = wb_serializers.FloatField(read_only=True)
77
+
78
+ @wb_serializers.register_resource()
79
+ def additional_resources(self, instance, request, user):
80
+ if (view := request.parser_context.get("view")) and view.order_proposal.status in [
81
+ OrderProposal.Status.DRAFT,
82
+ OrderProposal.Status.PENDING,
83
+ OrderProposal.Status.APPROVED,
84
+ ]:
85
+ return {
86
+ "execution_instruction": reverse(
87
+ "wbportfolio:orderproposal-order-changeexecutioninstruction",
88
+ args=[view.order_proposal.id, instance.id],
89
+ request=request,
90
+ )
91
+ }
92
+ return {}
73
93
 
74
94
  def validate(self, data):
75
95
  data.pop("company", None)
@@ -80,21 +100,71 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
80
100
  "underlying_instrument": "You cannot modify the underlying instrument other than creating a new entry"
81
101
  }
82
102
  )
103
+
83
104
  effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
84
- weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
85
- if (target_weight := data.pop("target_weight", None)) is not None:
86
- weighting = target_weight - effective_weight
87
- data["desired_target_weight"] = target_weight
88
- if weighting >= 0:
89
- data["order_type"] = "BUY"
90
- else:
91
- data["order_type"] = "SELL"
92
- data["weighting"] = weighting
105
+ effective_shares = self.instance._effective_shares if self.instance else Decimal(0.0)
106
+ portfolio_value = (
107
+ self.context["view"].order_proposal.portfolio_total_asset_value if "view" in self.context else Decimal(0.0)
108
+ )
109
+ if (total_value_fx_portfolio := data.pop("total_value_fx_portfolio", None)) is not None and portfolio_value:
110
+ data["weighting"] = total_value_fx_portfolio / portfolio_value
111
+ if (
112
+ target_total_value_fx_portfolio := data.pop("target_total_value_fx_portfolio", None)
113
+ ) is not None and portfolio_value:
114
+ data["target_weight"] = target_total_value_fx_portfolio / portfolio_value
115
+
116
+ if data.get("weighting") is not None or data.get("target_weight") is not None:
117
+ weighting = data.pop("weighting", None)
118
+ if (target_weight := data.pop("target_weight", None)) is not None:
119
+ weighting = target_weight - effective_weight
120
+ data["desired_target_weight"] = target_weight
121
+ if weighting is not None:
122
+ data["weighting"] = weighting
123
+ data.pop("shares", None)
124
+ data.pop("target_shares", None)
125
+
126
+ if data.get("shares") is not None or data.get("target_shares") is not None:
127
+ shares = data.pop("shares", None)
128
+ if (target_shares := data.pop("target_shares", None)) is not None:
129
+ shares = target_shares - effective_shares
130
+ if shares is not None:
131
+ data["shares"] = shares
93
132
  return super().validate(data)
94
133
 
134
+ def update(self, instance, validated_data):
135
+ weighting = validated_data.pop("weighting", None)
136
+ shares = validated_data.pop("shares", None)
137
+ portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
138
+ if weighting is not None:
139
+ instance.set_weighting(weighting, portfolio_total_asset_value)
140
+ if shares is not None:
141
+ instance.set_shares(shares, portfolio_total_asset_value)
142
+ return super().update(instance, validated_data)
143
+
144
+ def create(self, validated_data):
145
+ weighting = validated_data.pop("weighting", None)
146
+ shares = validated_data.pop("shares", None)
147
+ instance = super().create(validated_data)
148
+ portfolio_total_asset_value = instance.order_proposal.portfolio_total_asset_value
149
+ if weighting is not None:
150
+ instance.set_weighting(weighting, portfolio_total_asset_value)
151
+ if shares is not None:
152
+ instance.set_shares(shares, portfolio_total_asset_value)
153
+ instance.save()
154
+ return instance
155
+
156
+ def get_unique_together_validators(self):
157
+ return [
158
+ UniqueTogetherValidator(
159
+ queryset=Order.objects.all(),
160
+ fields=("order_proposal", "underlying_instrument"),
161
+ message="This instrument is already in the orders list.",
162
+ )
163
+ ]
164
+
95
165
  class Meta:
96
166
  model = Order
97
- percent_fields = ["effective_weight", "target_weight", "weighting"]
167
+ percent_fields = ["effective_weight", "target_weight", "weighting", "desired_target_weight"]
98
168
  decorators = {
99
169
  "total_value_fx_portfolio": wb_serializers.decorator(
100
170
  decorator_type="text", position="left", value="{{portfolio_currency}}"
@@ -105,17 +175,26 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
105
175
  "target_total_value_fx_portfolio": wb_serializers.decorator(
106
176
  decorator_type="text", position="left", value="{{portfolio_currency}}"
107
177
  ),
178
+ "price": wb_serializers.decorator(position="left", value="{{underlying_instrument_currency}}"),
108
179
  }
109
180
  read_only_fields = (
110
181
  "order_type",
111
- "shares",
112
182
  "effective_shares",
113
- "target_shares",
114
- "total_value_fx_portfolio",
115
183
  "effective_total_value_fx_portfolio",
116
- "target_total_value_fx_portfolio",
117
184
  "has_warnings",
185
+ "desired_target_weight",
186
+ "daily_return",
187
+ "currency_fx_rate",
188
+ "price",
189
+ "execution_instruction",
190
+ "execution_instruction_parameters_repr",
191
+ "execution_date",
192
+ "execution_price",
193
+ "execution_traded_shares",
118
194
  )
195
+ extra_kwargs = {
196
+ "price": {"required": False},
197
+ }
119
198
  fields = (
120
199
  "id",
121
200
  "shares",
@@ -138,9 +217,21 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
138
217
  "effective_total_value_fx_portfolio",
139
218
  "target_total_value_fx_portfolio",
140
219
  "portfolio_currency",
220
+ "underlying_instrument_currency",
141
221
  "has_warnings",
142
- "execution_confirmed",
222
+ "desired_target_weight",
223
+ "daily_return",
224
+ "currency_fx_rate",
225
+ "price",
226
+ "execution_status",
227
+ "execution_instruction",
228
+ "execution_instruction_parameters",
143
229
  "execution_comment",
230
+ "execution_instruction_parameters_repr",
231
+ "execution_date",
232
+ "execution_price",
233
+ "execution_traded_shares",
234
+ "_additional_resources",
144
235
  )
145
236
 
146
237
 
@@ -164,6 +255,7 @@ class OrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
164
255
  optional_get_parameters={"company": "parent"},
165
256
  depends_on=[{"field": "company", "options": {}}],
166
257
  required=False,
258
+ select_first_choice=True,
167
259
  )
168
260
  underlying_instrument = wb_serializers.PrimaryKeyRelatedField(
169
261
  queryset=Instrument.objects.all(), label="Quote", read_only=lambda view: not view.new_mode
@@ -173,6 +265,7 @@ class OrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
173
265
  optional_get_parameters={"security": "parent"},
174
266
  depends_on=[{"field": "security", "options": {}}],
175
267
  tree_config=BaseTreeGroupLevelOption(clear_filter=True, filter_key="parent"),
268
+ select_first_choice=True,
176
269
  )
177
270
 
178
271
  class Meta(OrderOrderProposalListModelSerializer.Meta):
@@ -49,8 +49,8 @@ class ClaimAPIModelSerializer(serializers.ModelSerializer):
49
49
  if "isin" in request.data:
50
50
  try:
51
51
  data["product"] = Product.objects.get(isin=request.data["isin"])
52
- except Product.DoesNotExist:
53
- raise ValidationError({"isin": "A product with this ISIN does not exist."})
52
+ except Product.DoesNotExist as e:
53
+ raise ValidationError({"isin": "A product with this ISIN does not exist."}) from e
54
54
  if trade := data.get("trade", None):
55
55
  if not trade.is_claimable:
56
56
  raise ValidationError({"trade": "Only a claimable trade can be selected"})
wbportfolio/tasks.py CHANGED
@@ -1,12 +1,12 @@
1
1
  from contextlib import suppress
2
2
  from datetime import date, timedelta
3
3
 
4
+ from celery import shared_task
4
5
  from django.db.models import ProtectedError, Q
6
+ from tqdm import tqdm
7
+ from wbfdm.models import Controversy, Instrument
5
8
 
6
- from wbportfolio.models import Portfolio, Trade
7
- from wbportfolio.models.products import Product
8
-
9
- from .fdm.tasks import * # noqa
9
+ from wbportfolio.models import AssetPosition, Portfolio, Product, Trade
10
10
 
11
11
 
12
12
  @shared_task(queue="portfolio")
@@ -49,3 +49,41 @@ def update_preferred_classification_per_instrument_and_portfolio_as_task():
49
49
  # - propagate (or update) t-2 asset positions into t-1
50
50
  # - Synchronize wbportfolio at t-1
51
51
  # - Compute Instrument Price estimate at t-1
52
+
53
+
54
+ @shared_task(queue="portfolio")
55
+ def synchronize_portfolio_controversies():
56
+ active_portfolios = Portfolio.objects.filter_active_and_tracked()
57
+ qs = (
58
+ AssetPosition.objects.filter(portfolio__in=active_portfolios)
59
+ .values("underlying_instrument")
60
+ .distinct("underlying_instrument")
61
+ )
62
+ objs = {}
63
+ securities = Instrument.objects.filter(id__in=qs.values("underlying_instrument"))
64
+ securities_mapping = {security.id: security.get_root() for security in securities}
65
+ for controversy in securities.dl.esg_controversies():
66
+ instrument = securities_mapping[controversy["instrument_id"]]
67
+ obj = Controversy.dict_to_model(controversy, instrument)
68
+ objs[obj.external_id] = obj
69
+
70
+ Controversy.objects.bulk_create(
71
+ objs.values(),
72
+ update_fields=[
73
+ "instrument",
74
+ "headline",
75
+ "description",
76
+ "source",
77
+ "direct_involvement",
78
+ "company_response",
79
+ "review",
80
+ "initiated",
81
+ "flag",
82
+ "status",
83
+ "type",
84
+ "severity",
85
+ ],
86
+ unique_fields=["external_id"],
87
+ update_conflicts=True,
88
+ batch_size=10000,
89
+ )