wbportfolio 1.52.0__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 (273) hide show
  1. wbportfolio/admin/__init__.py +3 -1
  2. wbportfolio/admin/indexes.py +1 -1
  3. wbportfolio/admin/orders/__init__.py +2 -0
  4. wbportfolio/admin/orders/order_proposals.py +16 -0
  5. wbportfolio/admin/orders/orders.py +32 -0
  6. wbportfolio/admin/portfolio.py +11 -5
  7. wbportfolio/admin/product_groups.py +1 -1
  8. wbportfolio/admin/products.py +2 -1
  9. wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
  10. wbportfolio/admin/transactions/__init__.py +0 -2
  11. wbportfolio/admin/transactions/dividends.py +40 -4
  12. wbportfolio/admin/transactions/fees.py +24 -14
  13. wbportfolio/admin/transactions/trades.py +34 -27
  14. wbportfolio/analysis/claims.py +5 -6
  15. wbportfolio/api_clients/ubs.py +162 -0
  16. wbportfolio/constants.py +1 -0
  17. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  18. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  19. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  20. wbportfolio/contrib/company_portfolio/models.py +69 -39
  21. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  22. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  23. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  24. wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
  25. wbportfolio/defaults/fees/default.py +7 -15
  26. wbportfolio/factories/__init__.py +2 -2
  27. wbportfolio/factories/assets.py +1 -1
  28. wbportfolio/factories/dividends.py +8 -3
  29. wbportfolio/factories/fees.py +8 -4
  30. wbportfolio/factories/orders/__init__.py +2 -0
  31. wbportfolio/factories/orders/order_proposals.py +21 -0
  32. wbportfolio/factories/orders/orders.py +34 -0
  33. wbportfolio/factories/portfolios.py +2 -1
  34. wbportfolio/factories/product_groups.py +3 -3
  35. wbportfolio/factories/products.py +3 -3
  36. wbportfolio/factories/rebalancing.py +1 -1
  37. wbportfolio/factories/trades.py +12 -16
  38. wbportfolio/filters/assets.py +18 -4
  39. wbportfolio/filters/orders/__init__.py +2 -0
  40. wbportfolio/filters/orders/order_proposals.py +55 -0
  41. wbportfolio/filters/orders/orders.py +11 -0
  42. wbportfolio/filters/portfolios.py +38 -1
  43. wbportfolio/filters/positions.py +0 -1
  44. wbportfolio/filters/transactions/__init__.py +1 -2
  45. wbportfolio/filters/transactions/fees.py +5 -12
  46. wbportfolio/filters/transactions/trades.py +16 -8
  47. wbportfolio/filters/transactions/utils.py +42 -0
  48. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  49. wbportfolio/import_export/backends/ubs/asset_position.py +6 -7
  50. wbportfolio/import_export/backends/ubs/fees.py +10 -20
  51. wbportfolio/import_export/backends/ubs/instrument_price.py +6 -6
  52. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  53. wbportfolio/import_export/backends/utils.py +0 -17
  54. wbportfolio/import_export/handlers/asset_position.py +22 -10
  55. wbportfolio/import_export/handlers/dividend.py +8 -8
  56. wbportfolio/import_export/handlers/fees.py +13 -23
  57. wbportfolio/import_export/handlers/orders.py +71 -0
  58. wbportfolio/import_export/handlers/trade.py +53 -77
  59. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  60. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  61. wbportfolio/import_export/parsers/jpmorgan/fees.py +4 -4
  62. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  63. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  64. wbportfolio/import_export/parsers/leonteq/customer_trade.py +5 -5
  65. wbportfolio/import_export/parsers/leonteq/fees.py +11 -7
  66. wbportfolio/import_export/parsers/leonteq/trade.py +2 -6
  67. wbportfolio/import_export/parsers/natixis/d1_fees.py +2 -2
  68. wbportfolio/import_export/parsers/natixis/dividend.py +4 -9
  69. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  70. wbportfolio/import_export/parsers/natixis/fees.py +7 -9
  71. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  72. wbportfolio/import_export/parsers/sg_lux/customer_trade_pending_slk.py +1 -1
  73. wbportfolio/import_export/parsers/sg_lux/equity.py +10 -10
  74. wbportfolio/import_export/parsers/sg_lux/fees.py +2 -2
  75. wbportfolio/import_export/parsers/sg_lux/perf_fees.py +2 -2
  76. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  77. wbportfolio/import_export/parsers/sg_lux/utils.py +2 -2
  78. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  79. wbportfolio/import_export/parsers/societe_generale/strategy.py +5 -5
  80. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  81. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  82. wbportfolio/import_export/parsers/ubs/api/fees.py +2 -2
  83. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  84. wbportfolio/import_export/parsers/ubs/customer_trade.py +7 -5
  85. wbportfolio/import_export/parsers/ubs/equity.py +3 -2
  86. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  87. wbportfolio/import_export/parsers/vontobel/customer_trade.py +2 -3
  88. wbportfolio/import_export/parsers/vontobel/historical_customer_trade.py +0 -1
  89. wbportfolio/import_export/parsers/vontobel/management_fees.py +12 -20
  90. wbportfolio/import_export/parsers/vontobel/performance_fees.py +5 -8
  91. wbportfolio/import_export/parsers/vontobel/valuation_api.py +4 -1
  92. wbportfolio/import_export/resources/trades.py +3 -3
  93. wbportfolio/import_export/utils.py +3 -1
  94. wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +2 -2
  95. wbportfolio/metric/backends/base.py +2 -2
  96. wbportfolio/migrations/0059_fees_unique_fees.py +1 -1
  97. wbportfolio/migrations/0077_remove_transaction_currency_and_more.py +622 -0
  98. wbportfolio/migrations/0078_trade_drift_factor.py +26 -0
  99. wbportfolio/migrations/0079_alter_trade_drift_factor.py +19 -0
  100. wbportfolio/migrations/0080_alter_trade_drift_factor_alter_trade_weighting.py +19 -0
  101. wbportfolio/migrations/0081_alter_trade_drift_factor.py +19 -0
  102. wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
  103. wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
  104. wbportfolio/migrations/0084_orderproposal_min_order_value.py +25 -0
  105. wbportfolio/migrations/0085_order_desired_target_weight.py +26 -0
  106. wbportfolio/migrations/0086_orderproposal_total_cash_weight.py +19 -0
  107. wbportfolio/migrations/0087_product_order_routing_custodian_adapter.py +94 -0
  108. wbportfolio/migrations/0088_orderproposal_total_effective_portfolio_contribution.py +19 -0
  109. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  110. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  111. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  112. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  113. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  114. wbportfolio/models/__init__.py +2 -0
  115. wbportfolio/models/adjustments.py +1 -1
  116. wbportfolio/models/asset.py +28 -170
  117. wbportfolio/models/builder.py +323 -0
  118. wbportfolio/models/custodians.py +3 -3
  119. wbportfolio/models/exceptions.py +1 -1
  120. wbportfolio/models/graphs/portfolio.py +1 -1
  121. wbportfolio/models/graphs/utils.py +11 -11
  122. wbportfolio/models/mixins/instruments.py +7 -0
  123. wbportfolio/models/mixins/liquidity_stress_test.py +4 -4
  124. wbportfolio/models/orders/__init__.py +2 -0
  125. wbportfolio/models/orders/order_proposals.py +1414 -0
  126. wbportfolio/models/orders/orders.py +410 -0
  127. wbportfolio/models/portfolio.py +311 -289
  128. wbportfolio/models/portfolio_relationship.py +6 -0
  129. wbportfolio/models/products.py +12 -0
  130. wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +40 -27
  131. wbportfolio/models/roles.py +4 -10
  132. wbportfolio/models/transactions/__init__.py +0 -4
  133. wbportfolio/models/transactions/claim.py +7 -6
  134. wbportfolio/models/transactions/dividends.py +42 -5
  135. wbportfolio/models/transactions/fees.py +55 -22
  136. wbportfolio/models/transactions/trades.py +121 -442
  137. wbportfolio/models/transactions/transactions.py +78 -158
  138. wbportfolio/models/utils.py +100 -1
  139. wbportfolio/order_routing/__init__.py +35 -0
  140. wbportfolio/order_routing/adapters/__init__.py +65 -0
  141. wbportfolio/order_routing/adapters/ubs.py +195 -0
  142. wbportfolio/order_routing/router.py +33 -0
  143. wbportfolio/order_routing/tests/__init__.py +0 -0
  144. wbportfolio/order_routing/tests/test_router.py +110 -0
  145. wbportfolio/permissions.py +7 -0
  146. wbportfolio/pms/analytics/portfolio.py +17 -9
  147. wbportfolio/pms/analytics/utils.py +9 -0
  148. wbportfolio/pms/trading/__init__.py +0 -1
  149. wbportfolio/pms/trading/optimizer.py +61 -0
  150. wbportfolio/pms/typing.py +198 -63
  151. wbportfolio/rebalancing/base.py +12 -1
  152. wbportfolio/rebalancing/decorators.py +1 -1
  153. wbportfolio/rebalancing/models/composite.py +4 -8
  154. wbportfolio/rebalancing/models/equally_weighted.py +13 -11
  155. wbportfolio/rebalancing/models/market_capitalization_weighted.py +21 -14
  156. wbportfolio/rebalancing/models/model_portfolio.py +14 -18
  157. wbportfolio/risk_management/backends/__init__.py +1 -0
  158. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  159. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  160. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  161. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  162. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  163. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  164. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  165. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  166. wbportfolio/serializers/__init__.py +1 -0
  167. wbportfolio/serializers/orders/__init__.py +2 -0
  168. wbportfolio/serializers/orders/order_proposals.py +115 -0
  169. wbportfolio/serializers/orders/orders.py +283 -0
  170. wbportfolio/serializers/portfolios.py +7 -7
  171. wbportfolio/serializers/positions.py +2 -2
  172. wbportfolio/serializers/rebalancing.py +1 -1
  173. wbportfolio/serializers/signals.py +9 -12
  174. wbportfolio/serializers/transactions/__init__.py +1 -10
  175. wbportfolio/serializers/transactions/claim.py +2 -2
  176. wbportfolio/serializers/transactions/dividends.py +37 -9
  177. wbportfolio/serializers/transactions/fees.py +39 -10
  178. wbportfolio/serializers/transactions/trades.py +55 -157
  179. wbportfolio/tasks.py +43 -5
  180. wbportfolio/tests/analysis/__init__.py +0 -0
  181. wbportfolio/tests/analysis/test_claims.py +85 -0
  182. wbportfolio/tests/conftest.py +12 -12
  183. wbportfolio/tests/models/orders/__init__.py +0 -0
  184. wbportfolio/tests/models/orders/test_order_proposals.py +1046 -0
  185. wbportfolio/tests/models/test_assets.py +7 -3
  186. wbportfolio/tests/models/test_imports.py +9 -13
  187. wbportfolio/tests/models/test_portfolios.py +102 -95
  188. wbportfolio/tests/models/test_products.py +11 -0
  189. wbportfolio/tests/models/test_splits.py +1 -6
  190. wbportfolio/tests/models/test_utils.py +140 -0
  191. wbportfolio/tests/models/transactions/test_fees.py +7 -13
  192. wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
  193. wbportfolio/tests/models/transactions/test_trades.py +0 -20
  194. wbportfolio/tests/pms/test_analytics.py +22 -3
  195. wbportfolio/tests/rebalancing/test_models.py +51 -57
  196. wbportfolio/tests/signals.py +10 -20
  197. wbportfolio/tests/tests.py +3 -1
  198. wbportfolio/tests/viewsets/test_products.py +1 -0
  199. wbportfolio/urls.py +10 -13
  200. wbportfolio/viewsets/__init__.py +9 -4
  201. wbportfolio/viewsets/assets.py +3 -204
  202. wbportfolio/viewsets/charts/__init__.py +6 -1
  203. wbportfolio/viewsets/charts/assets.py +344 -154
  204. wbportfolio/viewsets/configs/buttons/__init__.py +2 -2
  205. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  206. wbportfolio/viewsets/configs/buttons/mixins.py +4 -4
  207. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  208. wbportfolio/viewsets/configs/buttons/products.py +32 -2
  209. wbportfolio/viewsets/configs/display/__init__.py +2 -5
  210. wbportfolio/viewsets/configs/display/assets.py +6 -19
  211. wbportfolio/viewsets/configs/display/fees.py +3 -3
  212. wbportfolio/viewsets/configs/display/portfolios.py +5 -5
  213. wbportfolio/viewsets/configs/display/products.py +1 -1
  214. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  215. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  216. wbportfolio/viewsets/configs/display/trades.py +1 -189
  217. wbportfolio/viewsets/configs/endpoints/__init__.py +3 -7
  218. wbportfolio/viewsets/configs/endpoints/fees.py +2 -2
  219. wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
  220. wbportfolio/viewsets/configs/menu/__init__.py +1 -1
  221. wbportfolio/viewsets/configs/menu/orders.py +11 -0
  222. wbportfolio/viewsets/configs/titles/__init__.py +2 -3
  223. wbportfolio/viewsets/configs/titles/fees.py +4 -8
  224. wbportfolio/viewsets/esg.py +3 -5
  225. wbportfolio/viewsets/mixins.py +5 -1
  226. wbportfolio/viewsets/orders/__init__.py +6 -0
  227. wbportfolio/viewsets/orders/configs/__init__.py +4 -0
  228. wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
  229. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +188 -0
  230. wbportfolio/viewsets/orders/configs/buttons/orders.py +113 -0
  231. wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
  232. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +157 -0
  233. wbportfolio/viewsets/orders/configs/displays/orders.py +232 -0
  234. wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
  235. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
  236. wbportfolio/viewsets/orders/configs/endpoints/orders.py +28 -0
  237. wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
  238. wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
  239. wbportfolio/viewsets/orders/order_proposals.py +252 -0
  240. wbportfolio/viewsets/orders/orders.py +277 -0
  241. wbportfolio/viewsets/portfolios.py +36 -12
  242. wbportfolio/viewsets/positions.py +3 -2
  243. wbportfolio/viewsets/products.py +6 -6
  244. wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
  245. wbportfolio/viewsets/transactions/__init__.py +3 -14
  246. wbportfolio/viewsets/transactions/fees.py +22 -22
  247. wbportfolio/viewsets/transactions/trades.py +1 -180
  248. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +3 -1
  249. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +252 -203
  250. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  251. wbportfolio/admin/transactions/transactions.py +0 -38
  252. wbportfolio/factories/transactions.py +0 -22
  253. wbportfolio/fdm/tasks.py +0 -13
  254. wbportfolio/filters/transactions/transactions.py +0 -99
  255. wbportfolio/models/transactions/expiry.py +0 -7
  256. wbportfolio/models/transactions/trade_proposals.py +0 -704
  257. wbportfolio/pms/trading/handler.py +0 -161
  258. wbportfolio/serializers/transactions/expiry.py +0 -18
  259. wbportfolio/serializers/transactions/trade_proposals.py +0 -76
  260. wbportfolio/serializers/transactions/transactions.py +0 -85
  261. wbportfolio/tests/models/transactions/test_trade_proposals.py +0 -410
  262. wbportfolio/viewsets/configs/buttons/trade_proposals.py +0 -66
  263. wbportfolio/viewsets/configs/display/trade_proposals.py +0 -100
  264. wbportfolio/viewsets/configs/display/transactions.py +0 -55
  265. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
  266. wbportfolio/viewsets/configs/endpoints/transactions.py +0 -14
  267. wbportfolio/viewsets/configs/menu/transactions.py +0 -9
  268. wbportfolio/viewsets/configs/titles/transactions.py +0 -9
  269. wbportfolio/viewsets/signals.py +0 -43
  270. wbportfolio/viewsets/transactions/trade_proposals.py +0 -139
  271. wbportfolio/viewsets/transactions/transactions.py +0 -122
  272. /wbportfolio/{fdm → api_clients}/__init__.py +0 -0
  273. {wbportfolio-1.52.0.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -9,7 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist
9
9
  from django.db import models
10
10
  from wbcore.contrib.authentication.authentication import User
11
11
  from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
12
- from wbcore.contrib.io.exceptions import DeserializationError
12
+ from wbcore.contrib.currency.models import Currency
13
+ from wbcore.contrib.io.exceptions import DeserializationError, SkipImportError
13
14
  from wbcore.contrib.io.imports import ImportExportHandler
14
15
  from wbcore.contrib.notifications.dispatch import send_notification
15
16
  from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
@@ -30,16 +31,24 @@ class AssetPositionImportHandler(ImportExportHandler):
30
31
  self.instrument_price_handler = InstrumentPriceImportHandler(self.import_source)
31
32
  self.currency_handler = CurrencyImportHandler(self.import_source)
32
33
 
33
- def _deserialize(self, data: Dict[str, Any]):
34
+ def _deserialize(self, data: Dict[str, Any]): # noqa: C901
34
35
  from wbportfolio.models import Portfolio
35
36
 
36
37
  portfolio_data = data.pop("portfolio", None)
37
38
  underlying_quote_data = data.pop("underlying_quote", data.pop("underlying_instrument", None))
38
39
  if "currency" in data:
39
- data["currency"] = self.currency_handler.process_object(
40
- data["currency"], read_only=True, raise_exception=False
41
- )[0]
40
+ currency = self.currency_handler.process_object(data["currency"], read_only=True, raise_exception=False)[0]
41
+ # we do not support GBX in our instrument table
42
+ if currency.key == "GBX":
43
+ currency = Currency.objects.get(key="GBP")
44
+ if "initial_price" in data:
45
+ data["initial_price"] /= 1000
46
+ data["currency"] = currency
42
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)
43
52
  if data.get("asset_valuation_date", None):
44
53
  data["asset_valuation_date"] = datetime.strptime(data["asset_valuation_date"], "%Y-%m-%d").date()
45
54
  else:
@@ -73,16 +82,19 @@ class AssetPositionImportHandler(ImportExportHandler):
73
82
  data["initial_price"] = data["underlying_quote"].get_price(
74
83
  data["date"], price_date_timedelta=self.MAX_PRICE_DATE_TIMEDELTA
75
84
  )
76
- except ValueError:
77
- 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
78
87
 
79
88
  # number type deserialization and sanitization
80
89
  # ensure the provided Decimal field are of type Decimal
81
90
  decimal_fields = ["initial_currency_fx_rate", "initial_price", "initial_shares", "weighting"]
82
91
  for field in decimal_fields:
83
- if not (value := data.get(field, None)) is None:
92
+ if (value := data.get(field, None)) is not None:
84
93
  data[field] = Decimal(value)
85
94
 
95
+ if data["weighting"] == 0:
96
+ raise SkipImportError("exclude position whose weight is 0")
97
+
86
98
  def _process_raw_data(self, data: Dict[str, Any]):
87
99
  if prices := data.get("prices", None):
88
100
  self.import_source.log += "Instrument Prices found: Importing"
@@ -146,7 +158,7 @@ class AssetPositionImportHandler(ImportExportHandler):
146
158
  for position in leftovers_positions:
147
159
  position.delete()
148
160
  for val_date in sorted(dates):
149
- trigger_portfolio_change_as_task.delay(portfolio.id, val_date, recompute_weighting=True)
161
+ trigger_portfolio_change_as_task.delay(portfolio.id, val_date, fix_quantization=True)
150
162
 
151
163
  # check if portfolio as custodian
152
164
  latest_date = max(dates)
@@ -19,7 +19,7 @@ class DividendImportHandler(ImportExportHandler):
19
19
  self.currency_handler = CurrencyImportHandler(self.import_source)
20
20
 
21
21
  def _deserialize(self, data):
22
- data["transaction_date"] = datetime.strptime(data["transaction_date"], "%Y-%m-%d").date()
22
+ data["ex_date"] = datetime.strptime(data["ex_date"], "%Y-%m-%d").date()
23
23
  data["value_date"] = datetime.strptime(data["value_date"], "%Y-%m-%d").date()
24
24
  from wbportfolio.models import Portfolio
25
25
 
@@ -36,18 +36,18 @@ 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
 
43
43
  def _get_instance(self, data, history=None, **kwargs):
44
44
  self.import_source.log += "\nGet DividendTransaction Instance."
45
- self.import_source.log += f"\nParameter: Portfolio={data['portfolio']} Underlying={data['underlying_instrument']} Date={data['transaction_date']}"
45
+ self.import_source.log += f"\nParameter: Portfolio={data['portfolio']} Underlying={data['underlying_instrument']} Date={data['ex_date']}"
46
46
  dividends = history if history is not None else self.model.objects
47
47
 
48
48
  dividends = dividends.filter(
49
49
  portfolio=data["portfolio"],
50
- transaction_date=data["transaction_date"],
50
+ ex_date=data["ex_date"],
51
51
  value_date=data["value_date"],
52
52
  underlying_instrument=data["underlying_instrument"],
53
53
  price_gross=data["price_gross"],
@@ -63,10 +63,10 @@ class DividendImportHandler(ImportExportHandler):
63
63
  def _get_history(self: models.Model, history: Dict[str, Any]) -> models.QuerySet:
64
64
  from wbportfolio.models import Product
65
65
 
66
- val_date = datetime.strptime(history["transaction_date"], "%Y-%m-%d")
66
+ val_date = datetime.strptime(history["value_date"], "%Y-%m-%d")
67
67
  try:
68
68
  product = Product.objects.get(**history.get("product", {}))
69
- dividends = self.model.objects.filter(transaction_date__lte=val_date, portfolio=product.primary_portfolio)
69
+ dividends = self.model.objects.filter(ex_date__lte=val_date, portfolio=product.primary_portfolio)
70
70
  if underlying_instrument_data := history.get("underlying_instrument"):
71
71
  if isinstance(underlying_instrument_data, dict):
72
72
  dividends = dividends.filter(
@@ -83,8 +83,8 @@ class DividendImportHandler(ImportExportHandler):
83
83
  self.import_source.log += (
84
84
  "It was a historical import and the following DividendTransaction have to be deleted:"
85
85
  )
86
- for dividend in history.order_by("transaction_date"):
86
+ for dividend in history.order_by("value_date"):
87
87
  self.import_source.log += (
88
- f"\n{dividend.transaction_date:%d.%m.%Y}: {dividend.shares} {dividend.price} ==> Deleted"
88
+ f"\n{dividend.value_date:%d.%m.%Y}: {dividend.shares} {dividend.price} ==> Deleted"
89
89
  )
90
90
  dividend.delete()
@@ -3,7 +3,6 @@ from datetime import datetime
3
3
  from wbcore.contrib.currency.import_export.handlers import CurrencyImportHandler
4
4
  from wbcore.contrib.io.exceptions import DeserializationError
5
5
  from wbcore.contrib.io.imports import ImportExportHandler
6
- from wbfdm.models.instruments import Cash
7
6
 
8
7
 
9
8
  class FeesImportHandler(ImportExportHandler):
@@ -14,31 +13,22 @@ class FeesImportHandler(ImportExportHandler):
14
13
  self.currency_handler = CurrencyImportHandler(self.import_source)
15
14
 
16
15
  def _deserialize(self, data):
17
- data["transaction_date"] = datetime.strptime(data["transaction_date"], "%Y-%m-%d").date()
18
- data["fee_date"] = data["transaction_date"]
19
- if value_date_str := data.get("value_date", None):
20
- data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
21
- if book_date_str := data.get("book_date", None):
22
- data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
23
-
24
- from wbportfolio.models import Portfolio, Product
16
+ data["fee_date"] = datetime.strptime(
17
+ data.get("fee_date", data.pop("transaction_date", None)), "%Y-%m-%d"
18
+ ).date()
19
+ from wbportfolio.models import Product
25
20
 
26
21
  try:
27
- linked_product_data = data.pop("linked_product", None)
28
- if isinstance(linked_product_data, dict):
29
- data["linked_product"] = Product.objects.get(**linked_product_data)
22
+ product_data = data.pop("product", None)
23
+ if isinstance(product_data, dict):
24
+ data["product"] = Product.objects.get(**product_data)
30
25
  else:
31
- data["linked_product"] = Product.objects.get(id=linked_product_data)
32
- except Product.DoesNotExist:
33
- raise DeserializationError("There is no valid linked product for in this row.")
26
+ data["product"] = Product.objects.get(id=product_data)
27
+ except Product.DoesNotExist as e:
28
+ raise DeserializationError("There is no valid linked product for in this row.") from e
34
29
 
35
- if "porfolio" in data:
36
- data["portfolio"] = Portfolio.all_objects.get(id=data["portfolio"])
37
- else:
38
- data["portfolio"] = data["linked_product"].primary_portfolio
39
- data["underlying_instrument"] = Cash.objects.filter(currency=data["portfolio"].currency).first()
40
30
  if "currency" not in data:
41
- data["currency"] = data["portfolio"].currency
31
+ data["currency"] = data["product"].currency
42
32
  else:
43
33
  data["currency"] = self.currency_handler.process_object(data["currency"], read_only=True)[0]
44
34
  data["currency_fx_rate"] = 1.0
@@ -48,9 +38,9 @@ class FeesImportHandler(ImportExportHandler):
48
38
 
49
39
  def _get_instance(self, data, history=None, **kwargs):
50
40
  self.import_source.log += "\nGet Fees Instance."
51
- self.import_source.log += f"\nParameter: Portfolio={data['portfolio']} Date={data['transaction_date']}"
41
+ self.import_source.log += f"\nParameter: Product={data['product']} Date={data['fee_date']}"
52
42
  fees = self.model.objects.filter(
53
- linked_product=data["linked_product"],
43
+ product=data["product"],
54
44
  fee_date=data["fee_date"],
55
45
  transaction_subtype=data["transaction_subtype"],
56
46
  calculated=data["calculated"],
@@ -0,0 +1,71 @@
1
+ from decimal import Decimal
2
+ from typing import Any, Dict
3
+
4
+ from django.db import models
5
+ from wbcore.contrib.io.exceptions import DeserializationError
6
+ from wbcore.contrib.io.imports import ImportExportHandler, ImportState
7
+ from wbcore.contrib.io.utils import nest_row
8
+ from wbfdm.import_export.handlers.instrument import InstrumentImportHandler
9
+
10
+ from wbportfolio.pms.typing import Portfolio, Position
11
+
12
+
13
+ class OrderImportHandler(ImportExportHandler):
14
+ MODEL_APP_LABEL: str = "wbportfolio.Order"
15
+
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
+ self.instrument_handler = InstrumentImportHandler(self.import_source)
19
+ self.order_proposal = None
20
+
21
+ def process_object(
22
+ self,
23
+ data: Dict[str, Any],
24
+ **kwargs,
25
+ ):
26
+ from wbportfolio.models import OrderProposal
27
+
28
+ data = nest_row(data)
29
+ underlying_instrument = self.instrument_handler.process_object(
30
+ data["underlying_instrument"], only_security=False, read_only=True
31
+ )[0]
32
+ self.order_proposal = OrderProposal.objects.get(id=data.pop("order_proposal_id"))
33
+ weighting = data.get("target_weight", data.get("weighting"))
34
+ shares = data.get("target_shares", data.get("shares", 0))
35
+ if weighting is None:
36
+ raise DeserializationError("We couldn't figure out the target weight column")
37
+ position_dto = Position(
38
+ underlying_instrument=underlying_instrument.id,
39
+ instrument_type=underlying_instrument.instrument_type.id,
40
+ weighting=Decimal(weighting),
41
+ shares=Decimal(shares),
42
+ currency=underlying_instrument.currency,
43
+ date=self.order_proposal.trade_date,
44
+ is_cash=underlying_instrument.is_cash,
45
+ )
46
+ return position_dto, ImportState.CREATED
47
+
48
+ def _get_history(self, history: Dict[str, Any]) -> models.QuerySet:
49
+ from wbportfolio.models.orders.order_proposals import OrderProposal
50
+
51
+ if order_proposal_id := history.get("order_proposal_id"):
52
+ # if a order proposal is provided, we delete the existing history first as otherwise, it would mess with the target weight computation
53
+ order_proposal = OrderProposal.objects.get(id=order_proposal_id)
54
+ order_proposal.orders.all().delete()
55
+ return self.model.objects.none()
56
+
57
+ def _post_processing_objects(self, positions: list[Position], *args, **kwargs):
58
+ total_weight = sum(map(lambda p: p.weighting, positions))
59
+ if cash_weight := Decimal("1") - total_weight:
60
+ cash_component = self.order_proposal.cash_component
61
+ positions.append(
62
+ Position(
63
+ underlying_instrument=cash_component.id,
64
+ instrument_type=cash_component.instrument_type.id,
65
+ weighting=cash_weight,
66
+ currency=cash_component.currency,
67
+ date=self.order_proposal.trade_date,
68
+ is_cash=cash_component.is_cash,
69
+ )
70
+ )
71
+ self.order_proposal.reset_orders(target_portfolio=Portfolio(positions))
@@ -25,7 +25,6 @@ class TradeImportHandler(ImportExportHandler):
25
25
  self.instrument_handler = InstrumentImportHandler(self.import_source)
26
26
  self.register_handler = RegisterImportHandler(self.import_source)
27
27
  self.currency_handler = CurrencyImportHandler(self.import_source)
28
- self.trade_proposals = set()
29
28
 
30
29
  def _data_changed(self, _object, change_data: Dict[str, Any], initial_data: Dict[str, Any], **kwargs):
31
30
  if (new_register := change_data.get("register")) and (current_register := _object.register):
@@ -35,67 +34,59 @@ class TradeImportHandler(ImportExportHandler):
35
34
  return super()._data_changed(_object, change_data, initial_data, **kwargs)
36
35
 
37
36
  def _deserialize(self, data: Dict[str, Any]):
38
- from wbportfolio.models import Product, TradeProposal
37
+ from wbportfolio.models import Product
39
38
 
40
39
  if underlying_instrument := data.get("underlying_instrument", None):
41
40
  data["underlying_instrument"] = self.instrument_handler.process_object(
42
41
  underlying_instrument, only_security=False, read_only=True
43
42
  )[0]
44
43
 
45
- if trade_proposal_id := data.pop("trade_proposal_id", None):
46
- trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
47
- self.trade_proposals.add(trade_proposal)
48
- data["value_date"] = trade_proposal.last_effective_date
49
- data["transaction_date"] = trade_proposal.trade_date
50
- data["trade_proposal"] = trade_proposal
51
- data["portfolio"] = trade_proposal.portfolio
52
- data["status"] = "DRAFT"
53
- else:
54
- if external_identifier2 := data.get("external_identifier2", None):
55
- data["external_identifier2"] = str(external_identifier2)
56
- if transaction_date_str := data.get("transaction_date", None):
57
- data["transaction_date"] = datetime.strptime(transaction_date_str, "%Y-%m-%d").date()
58
- if value_date_str := data.get("value_date", None):
59
- data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
60
- if book_date_str := data.get("book_date", None):
61
- data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
62
- data["portfolio"] = Portfolio._get_or_create_portfolio(
63
- self.instrument_handler, data.get("portfolio", data["underlying_instrument"])
64
- )
44
+ if external_id_alternative := data.get("external_id_alternative", None):
45
+ data["external_id_alternative"] = str(external_id_alternative)
46
+ if transaction_date_str := data.get("transaction_date", None):
47
+ data["transaction_date"] = datetime.strptime(transaction_date_str, "%Y-%m-%d").date()
48
+ if value_date_str := data.get("value_date", None):
49
+ data["value_date"] = datetime.strptime(value_date_str, "%Y-%m-%d").date()
50
+ if book_date_str := data.get("book_date", None):
51
+ data["book_date"] = datetime.strptime(book_date_str, "%Y-%m-%d").date()
52
+ data["portfolio"] = Portfolio._get_or_create_portfolio(
53
+ self.instrument_handler, data.get("portfolio", data["underlying_instrument"])
54
+ )
65
55
 
66
- if currency_data := data.get("currency", None):
67
- data["currency"] = self.currency_handler.process_object(currency_data, read_only=True)[0]
56
+ if currency_data := data.get("currency", None):
57
+ data["currency"] = self.currency_handler.process_object(currency_data, read_only=True)[0]
68
58
 
69
- if register_data := data.get("register", None):
70
- data["register"] = self.register_handler.process_object(register_data)[0]
59
+ if register_data := data.get("register", None):
60
+ data["register"] = self.register_handler.process_object(register_data)[0]
71
61
 
72
- data["marked_for_deletion"] = data.get("marked_for_deletion", False)
62
+ data["marked_for_deletion"] = data.get("marked_for_deletion", False)
73
63
  if underlying_instrument := data.get("underlying_instrument"):
74
64
  if nominal := data.pop("nominal", None):
75
65
  try:
76
66
  product = Product.objects.get(id=underlying_instrument.id)
77
67
  data["shares"] = nominal / product.share_price
78
- except Product.DoesNotExist:
68
+ except Product.DoesNotExist as e:
79
69
  raise DeserializationError(
80
70
  "We cannot compute the number of shares from the nominal value as we cannot find the product share price."
81
- )
71
+ ) from e
82
72
  else:
83
73
  raise DeserializationError("We couldn't find a valid underlying instrument this row.")
84
74
 
85
75
  for field in self.model._meta.get_fields():
86
- 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):
87
77
  q = (
88
78
  1 / (math.pow(10, 4))
89
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
90
80
  data[field.name] = Decimal(value).quantize(Decimal(str(q)))
81
+ if (target_weight := data.pop("target_weight", None)) is not None:
82
+ data["_target_weight"] = target_weight
91
83
 
92
84
  def _create_instance(self, data: Dict[str, Any], **kwargs) -> models.Model:
93
85
  if "transaction_date" not in data: # we might get only book date and not transaction date
94
86
  data["transaction_date"] = data["book_date"]
95
-
96
87
  return self.model.objects.create(**data, import_source=self.import_source)
97
88
 
98
- 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
99
90
  self.import_source.log += "\nGet Trade Instance."
100
91
  if transaction_date := data.get("transaction_date"):
101
92
  dates_lookup = {"transaction_date": transaction_date}
@@ -108,25 +99,23 @@ class TradeImportHandler(ImportExportHandler):
108
99
  if history.exists():
109
100
  queryset = history
110
101
  else:
111
- queryset = self.model.objects.filter(marked_for_deletion=False)
102
+ queryset = self.model.objects.filter(marked_for_deletion=False).exclude(id__in=self.processed_ids)
112
103
 
113
104
  queryset = queryset.filter(
114
105
  models.Q(underlying_instrument=data["underlying_instrument"]) & models.Q(**dates_lookup)
115
106
  )
116
107
  if "shares" in data:
117
108
  queryset = queryset.filter(shares=data["shares"])
118
-
119
109
  if _id := data.get("id", None):
120
110
  self.import_source.log += f"ID {_id} provided -> Load CustomerTrade"
121
111
  return self.model.objects.get(id=_id)
122
112
  # We need to check for external identifiers
123
113
  if external_id := data.get("external_id"):
124
114
  self.import_source.log += f"\nExternal Identifier used: {external_id}"
125
- queryset = queryset.filter(external_id=external_id)
126
- if queryset.count() == 1:
115
+ external_id_queryset = queryset.filter(external_id=external_id)
116
+ if external_id_queryset.count() == 1:
127
117
  self.import_source.log += f"External ID {external_id} provided -> Load CustomerTrade"
128
- return queryset.first()
129
-
118
+ return external_id_queryset.first()
130
119
  if portfolio := data.get("portfolio", None):
131
120
  queryset = queryset.filter(portfolio=portfolio)
132
121
  if queryset.exists():
@@ -152,6 +141,7 @@ class TradeImportHandler(ImportExportHandler):
152
141
  if queryset.exists():
153
142
  # We try to filter by price as well
154
143
  trade = queryset.first()
144
+
155
145
  if queryset.count() == 1:
156
146
  self.import_source.log += f"\nOne Trade found: {trade}"
157
147
  if queryset.count() > 1:
@@ -160,35 +150,30 @@ class TradeImportHandler(ImportExportHandler):
160
150
  self.import_source.log += "\nNo trade was successfully matched."
161
151
 
162
152
  def _get_history(self, history: Dict[str, Any]) -> models.QuerySet:
163
- if trade_proposal_id := history.get("trade_proposal_id"):
164
- # if a trade proposal is provided, we delete the existing history first as otherwise, it would mess with the target weight computation
165
- self.model.objects.filter(trade_proposal_id=trade_proposal_id).delete()
166
- trades = self.model.objects.none()
167
- else:
168
- trades = self.model.objects.filter(
169
- exclude_from_history=False,
170
- pending=False,
171
- transaction_subtype__in=[
172
- self.model.Type.SUBSCRIPTION,
173
- self.model.Type.REDEMPTION,
174
- ], # we cannot exclude marked for deleted trade because otherwise they are never consider in the history
175
- )
176
- if transaction_date := history.get("transaction_date"):
177
- trades = trades.filter(transaction_date__lte=transaction_date)
178
- elif book_date := history.get("book_date"):
179
- trades = trades.filter(book_date__lte=book_date)
180
- if underlying_instrument_data := history.get("underlying_instrument"):
181
- if isinstance(underlying_instrument_data, dict):
182
- trades = trades.filter(
183
- **{f"underlying_instrument__{k}": v for k, v in underlying_instrument_data.items()}
184
- )
185
- else:
186
- trades = trades.filter(underlying_instrument__id=underlying_instrument_data)
187
-
188
- elif "underlying_instruments" in history:
189
- trades = trades.filter(underlying_instrument__id__in=history["underlying_instruments"])
153
+ trades = self.model.objects.filter(
154
+ exclude_from_history=False,
155
+ pending=False,
156
+ transaction_subtype__in=[
157
+ self.model.Type.SUBSCRIPTION,
158
+ self.model.Type.REDEMPTION,
159
+ ], # we cannot exclude marked for deleted trade because otherwise they are never consider in the history
160
+ )
161
+ if transaction_date := history.get("transaction_date"):
162
+ trades = trades.filter(transaction_date__lte=transaction_date)
163
+ elif book_date := history.get("book_date"):
164
+ trades = trades.filter(book_date__lte=book_date)
165
+ if underlying_instrument_data := history.get("underlying_instrument"):
166
+ if isinstance(underlying_instrument_data, dict):
167
+ trades = trades.filter(
168
+ **{f"underlying_instrument__{k}": v for k, v in underlying_instrument_data.items()}
169
+ )
190
170
  else:
191
- raise ValueError("We cannot estimate history without at least the underlying instrument")
171
+ trades = trades.filter(underlying_instrument__id=underlying_instrument_data)
172
+
173
+ elif "underlying_instruments" in history:
174
+ trades = trades.filter(underlying_instrument__id__in=history["underlying_instruments"])
175
+ else:
176
+ raise ValueError("We cannot estimate history without at least the underlying instrument")
192
177
  return trades
193
178
 
194
179
  def _post_processing_objects(
@@ -203,12 +188,6 @@ class TradeImportHandler(ImportExportHandler):
203
188
  if instrument.instrument_type.key == "product":
204
189
  update_outstanding_shares_as_task.delay(instrument.id)
205
190
 
206
- # if the trade import relates to a trade proposal, we reset the TP after the import to ensure it contains the deleted positions (often forgotten by user)
207
- for changed_trade_proposal in self.trade_proposals:
208
- changed_trade_proposal.reset_trades(
209
- target_portfolio=changed_trade_proposal._build_dto().convert_to_portfolio()
210
- )
211
-
212
191
  def _post_processing_updated_object(self, _object):
213
192
  if _object.marked_for_deletion:
214
193
  _object.marked_for_deletion = False
@@ -227,8 +206,5 @@ class TradeImportHandler(ImportExportHandler):
227
206
  self.import_source.log += (
228
207
  f"{trade.transaction_date:%d.%m.%Y}: {trade.shares} {trade.bank} ==> Marked for deletion"
229
208
  )
230
- if trade.trade_proposal:
231
- trade.delete()
232
- else:
233
- trade.marked_for_deletion = True
234
- trade.save()
209
+ trade.marked_for_deletion = True
210
+ trade.save()
@@ -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
 
@@ -34,8 +34,8 @@ def parse(import_source):
34
34
  isin = fee_data["ISIN"]
35
35
  fee_date = fee_data["Date"]
36
36
  base_data = {
37
- "linked_product": {"isin": isin},
38
- "transaction_date": fee_date.strftime("%Y-%m-%d"),
37
+ "product": {"isin": isin},
38
+ "fee_date": fee_date.strftime("%Y-%m-%d"),
39
39
  "calculated": False,
40
40
  }
41
41
  data.append(