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
@@ -61,6 +61,9 @@ class InstrumentPortfolioThroughModel(models.Model):
61
61
  models.UniqueConstraint(fields=["instrument", "portfolio"], name="unique_portfolio_relationship"),
62
62
  ]
63
63
 
64
+ def __str__(self) -> str:
65
+ return f"{self.instrument} - {self.portfolio}"
66
+
64
67
  @classmethod
65
68
  def get_portfolio(cls, instrument):
66
69
  with suppress(InstrumentPortfolioThroughModel.DoesNotExist):
@@ -99,6 +102,9 @@ class PortfolioInstrumentPreferredClassificationThroughModel(models.Model):
99
102
  related_name="preferred_classification_group_throughs",
100
103
  )
101
104
 
105
+ def __str__(self) -> str:
106
+ return f"{self.portfolio} - {self.instrument}: ({self.classification})"
107
+
102
108
  def save(self, *args, **kwargs) -> None:
103
109
  if not self.classification_group and self.classification:
104
110
  self.classification_group = self.classification.group
@@ -4,6 +4,7 @@ from decimal import Decimal
4
4
 
5
5
  from celery import shared_task
6
6
  from django.contrib import admin
7
+ from django.contrib.contenttypes.fields import GenericRelation
7
8
  from django.contrib.postgres.constraints import ExclusionConstraint
8
9
  from django.contrib.postgres.fields import DateRangeField, RangeOperators
9
10
  from django.db import models
@@ -33,6 +34,7 @@ from wbcore.utils.enum import ChoiceEnum
33
34
  from wbcrm.models.accounts import Account
34
35
  from wbfdm.models.instruments.instrument_prices import InstrumentPrice
35
36
  from wbfdm.models.instruments.instruments import InstrumentManager, InstrumentType
37
+ from wbreport.models import Report
36
38
 
37
39
  from wbportfolio.models.portfolio_relationship import InstrumentPortfolioThroughModel
38
40
 
@@ -188,6 +190,9 @@ class FeeProductPercentage(models.Model):
188
190
  ),
189
191
  ]
190
192
 
193
+ def __str__(self) -> str:
194
+ return f"{self.product.name} ({self.type})"
195
+
191
196
  @property
192
197
  def net_percent(self) -> Decimal:
193
198
  return self.percent
@@ -198,6 +203,8 @@ class FeeProductPercentage(models.Model):
198
203
 
199
204
 
200
205
  class Product(PMSInstrumentAbstractModel):
206
+ reports = GenericRelation(Report)
207
+
201
208
  share_price = models.PositiveIntegerField(
202
209
  default=100,
203
210
  verbose_name="Share Price",
@@ -344,6 +351,11 @@ class Product(PMSInstrumentAbstractModel):
344
351
 
345
352
  self.is_managed = True
346
353
 
354
+ def save(self, *args, **kwargs):
355
+ super().save(*args, **kwargs)
356
+ if self.delisted_date and self.delisted_date <= date.today():
357
+ self.reports.update(is_active=False)
358
+
347
359
  def get_title(self):
348
360
  if self.parent:
349
361
  return f"{self.parent.name} ({self.name})"
@@ -14,8 +14,8 @@ from wbcore.utils.importlib import import_from_dotted_path
14
14
  from wbcore.utils.models import ComplexToStringMixin
15
15
  from wbcore.utils.rrules import convert_rrulestr_to_dict, humanize_rrule
16
16
 
17
+ from wbportfolio.models.orders.order_proposals import OrderProposal
17
18
  from wbportfolio.models.portfolio import Portfolio
18
- from wbportfolio.models.transactions.trade_proposals import TradeProposal
19
19
  from wbportfolio.pms.typing import Portfolio as PortfolioDTO
20
20
  from wbportfolio.rebalancing.base import AbstractRebalancingModel
21
21
 
@@ -43,9 +43,16 @@ class RebalancingModel(models.Model):
43
43
  return import_from_dotted_path(self.class_path)
44
44
 
45
45
  def get_target_portfolio(
46
- self, portfolio: Portfolio, trade_date: date, last_effective_date: date, **kwargs
46
+ self,
47
+ portfolio: Portfolio,
48
+ trade_date: date,
49
+ last_effective_date: date,
50
+ effective_portfolio: PortfolioDTO | None = None,
51
+ **kwargs,
47
52
  ) -> PortfolioDTO:
48
- model = self.model_class(portfolio, trade_date, last_effective_date, **kwargs)
53
+ model = self.model_class(
54
+ portfolio, trade_date, last_effective_date, effective_portfolio=effective_portfolio, **kwargs
55
+ )
49
56
  if not model.is_valid():
50
57
  raise ValidationError(model.validation_errors)
51
58
  return model.get_target_portfolio()
@@ -71,8 +78,8 @@ class Rebalancer(ComplexToStringMixin, models.Model):
71
78
  RebalancingModel, on_delete=models.PROTECT, related_name="rebalancers", verbose_name="Rebalancing Model"
72
79
  )
73
80
  parameters = models.JSONField(default=dict, verbose_name="Parameters", blank=True)
74
- approve_trade_proposal_automatically = models.BooleanField(
75
- default=False, verbose_name="Apply Trade Proposal Automatically"
81
+ apply_order_proposal_automatically = models.BooleanField(
82
+ default=False, verbose_name="Apply Order Proposal Automatically"
76
83
  )
77
84
  activation_date = models.DateField(verbose_name="Activation Date")
78
85
  frequency = models.CharField(
@@ -82,6 +89,9 @@ class Rebalancer(ComplexToStringMixin, models.Model):
82
89
  help_text=_("The Evaluation Frequency in RRULE format"),
83
90
  )
84
91
 
92
+ def __str__(self) -> str:
93
+ return f"{self.portfolio.name} ({self.rebalancing_model})"
94
+
85
95
  def save(self, *args, **kwargs):
86
96
  if not self.activation_date:
87
97
  try:
@@ -92,19 +102,19 @@ class Rebalancer(ComplexToStringMixin, models.Model):
92
102
 
93
103
  def _get_next_valid_date(self, valid_date: date) -> date:
94
104
  pivot_date = valid_date
95
- while TradeProposal.objects.filter(
96
- portfolio=self.portfolio, status=TradeProposal.Status.FAILED, trade_date=pivot_date
105
+ while OrderProposal.objects.filter(
106
+ portfolio=self.portfolio, status=OrderProposal.Status.FAILED, trade_date=pivot_date
97
107
  ).exists():
98
108
  pivot_date = (pivot_date + BDay(1)).date()
99
109
  return pivot_date
100
110
 
101
111
  def is_valid(self, trade_date: date) -> bool:
102
- if TradeProposal.objects.filter(
112
+ if OrderProposal.objects.filter(
103
113
  portfolio=self.portfolio,
104
- status=TradeProposal.Status.APPROVED,
114
+ status=OrderProposal.Status.CONFIRMED,
105
115
  trade_date=trade_date,
106
116
  rebalancing_model__isnull=True,
107
- ).exists(): # if a already approved trade proposal exists, we do not allow a re-evaluatioon of the rebalancing (only possible if "replayed")
117
+ ).exists(): # if a already applied order proposal exists, we do not allow a re-evaluatioon of the rebalancing (only possible if "replayed")
108
118
  return False
109
119
  for initial_valid_datetime in self.get_rrule(trade_date):
110
120
  initial_valid_date = initial_valid_datetime.date()
@@ -115,8 +125,8 @@ class Rebalancer(ComplexToStringMixin, models.Model):
115
125
  break
116
126
  return False
117
127
 
118
- def evaluate_rebalancing(self, trade_date: date):
119
- trade_proposal, _ = TradeProposal.objects.get_or_create(
128
+ def evaluate_rebalancing(self, trade_date: date, effective_portfolio=None):
129
+ order_proposal, _ = OrderProposal.objects.get_or_create(
120
130
  trade_date=trade_date,
121
131
  portfolio=self.portfolio,
122
132
  defaults={
@@ -124,27 +134,30 @@ class Rebalancer(ComplexToStringMixin, models.Model):
124
134
  "rebalancing_model": self.rebalancing_model,
125
135
  },
126
136
  )
127
-
128
- if trade_proposal.rebalancing_model == self.rebalancing_model:
129
- trade_proposal.status = TradeProposal.Status.DRAFT
137
+ order_proposal.portfolio = self.portfolio
138
+ if order_proposal.rebalancing_model == self.rebalancing_model:
130
139
  try:
131
140
  logger.info(
132
141
  f"Getting target portfolio ({self.portfolio}) for rebalancing model {self.rebalancing_model} for trade date {trade_date:%Y-%m-%d}"
133
142
  )
134
143
  target_portfolio = self.rebalancing_model.get_target_portfolio(
135
- self.portfolio, trade_date, trade_proposal.last_effective_date, **self.parameters
144
+ self.portfolio,
145
+ order_proposal.trade_date,
146
+ order_proposal.value_date,
147
+ effective_portfolio=effective_portfolio,
148
+ **self.parameters,
136
149
  )
137
- trade_proposal.reset_trades(target_portfolio)
138
- trade_proposal.submit()
139
- if self.approve_trade_proposal_automatically and self.portfolio.can_be_rebalanced:
140
- trade_proposal.approve(replay=False)
141
- except ValidationError:
142
- # If we encountered a validation error, we set the trade proposal as failed
143
- trade_proposal.status = TradeProposal.Status.FAILED
144
-
145
- trade_proposal.save()
146
-
147
- return trade_proposal
150
+ order_proposal.apply_workflow(
151
+ apply_automatically=self.apply_order_proposal_automatically,
152
+ target_portfolio=target_portfolio,
153
+ effective_portfolio=effective_portfolio,
154
+ )
155
+ except ValidationError as e:
156
+ logger.warning(f"Validation error while approving the orders: {e}")
157
+ # If we encountered a validation error, we set the order proposal as failed
158
+ order_proposal.status = OrderProposal.Status.FAILED
159
+ order_proposal.save()
160
+ return order_proposal
148
161
 
149
162
  @property
150
163
  def rrule(self):
@@ -52,16 +52,10 @@ class PortfolioRole(models.Model):
52
52
  return f"{self.role_type} {self.person.computed_str}"
53
53
 
54
54
  def save(self, *args, **kwargs):
55
- assert (
56
- self.role_type in [self.RoleType.MANAGER, self.RoleType.RISK_MANAGER] and not self.instrument
57
- ) or self.role_type in [
58
- self.RoleType.PORTFOLIO_MANAGER,
59
- self.RoleType.ANALYST,
60
- ], self.default_error_messages["manager"].format(model="instrument")
61
-
62
- assert (self.start and self.end and self.start < self.end) or (
63
- not self.start or not self.end
64
- ), self.default_error_messages["start_end"]
55
+ if self.role_type in [self.RoleType.MANAGER, self.RoleType.RISK_MANAGER] and self.instrument:
56
+ raise ValueError(self.default_error_messages["manager"].format(model="instrument"))
57
+ if self.start and self.end and self.start > self.end:
58
+ raise ValueError(self.default_error_messages["start_end"])
65
59
 
66
60
  super().save(*args, **kwargs)
67
61
 
@@ -1,8 +1,4 @@
1
1
  from .claim import Claim
2
2
  from .dividends import DividendTransaction
3
- from .expiry import Expiry
4
3
  from .fees import FeeCalculation, Fees
5
- from .trade_proposals import TradeProposal
6
- from .rebalancing import RebalancingModel, Rebalancer
7
4
  from .trades import Trade
8
- from .transactions import Transaction
@@ -259,9 +259,10 @@ class Claim(ReferenceIDMixin, WBModel):
259
259
  return f"{self.reference_id} {self.product.name} ({self.bank} - {self.shares:,} shares - {self.date}) "
260
260
 
261
261
  def save(self, *args, auto_match: bool = True, **kwargs):
262
- assert (
263
- self.shares is not None or self.nominal_amount is not None
264
- ), f"Either shares or nominal amount have to be provided. Shares={self.shares}, Nominal={self.nominal_amount}"
262
+ if self.shares is None and self.nominal_amount is None:
263
+ raise ValueError(
264
+ f"Either shares or nominal amount have to be provided. Shares={self.shares}, Nominal={self.nominal_amount}"
265
+ )
265
266
  if self.product:
266
267
  if self.shares is not None:
267
268
  self.nominal_amount = self.shares * self.product.share_price
@@ -447,7 +448,7 @@ class Claim(ReferenceIDMixin, WBModel):
447
448
  return self.can_approve()
448
449
 
449
450
  def auto_match(self) -> Trade | None:
450
- SHARES_EPSILON = 1 # share
451
+ shares_epsilon = 1 # share
451
452
  auto_match_trade = None
452
453
  # Obvious filtering
453
454
  trades = Trade.valid_customer_trade_objects.filter(
@@ -458,7 +459,7 @@ class Claim(ReferenceIDMixin, WBModel):
458
459
  trades = trades.filter(underlying_instrument=self.product)
459
460
  # Find trades by shares (or remaining to be claimed)
460
461
  trades = trades.filter(
461
- Q(diff_shares__lte=self.shares + SHARES_EPSILON) & Q(diff_shares__gte=self.shares - SHARES_EPSILON)
462
+ Q(diff_shares__lte=self.shares + shares_epsilon) & Q(diff_shares__gte=self.shares - shares_epsilon)
462
463
  )
463
464
  if trades.count() == 1:
464
465
  auto_match_trade = trades.first()
@@ -477,7 +478,7 @@ class Claim(ReferenceIDMixin, WBModel):
477
478
  # Find trades by external_id
478
479
  if not auto_match_trade and self.external_id and trades.count() > 1:
479
480
  trades = trades.filter(
480
- Q(external_id__icontains=self.external_id) | Q(external_identifier2__icontains=self.external_id)
481
+ Q(external_id__icontains=self.external_id) | Q(external_id_alternative__icontains=self.external_id)
481
482
  )
482
483
  if trades.count() == 1:
483
484
  auto_match_trade = trades.first()
@@ -1,15 +1,52 @@
1
- from decimal import Decimal
2
-
3
1
  from django.db import models
2
+ from wbcore.contrib.io.mixins import ImportMixin
4
3
 
5
4
  from wbportfolio.import_export.handlers.dividend import DividendImportHandler
6
5
 
7
- from .transactions import ShareMixin, Transaction
6
+ from .transactions import TransactionMixin
8
7
 
9
8
 
10
- class DividendTransaction(Transaction, ShareMixin, models.Model):
9
+ class DividendTransaction(TransactionMixin, ImportMixin, models.Model):
11
10
  import_export_handler_class = DividendImportHandler
11
+
12
+ class DistributionMethod(models.TextChoices):
13
+ PAYMENT = "Payment", "Payment"
14
+ REINVESTMENT = "Reinvestment", "Reinvestment"
15
+
16
+ ex_date = models.DateField(
17
+ verbose_name="Ex-Dividend Date",
18
+ help_text="The date on which the stock starts trading without the dividend",
19
+ )
20
+ record_date = models.DateField(
21
+ verbose_name="Record Date",
22
+ help_text="The date on which the holder must own the shares to be eligible for the dividend",
23
+ )
24
+ distribution_method = models.CharField(
25
+ max_length=255, verbose_name="Type", choices=DistributionMethod.choices, default=DistributionMethod.PAYMENT
26
+ )
12
27
  retrocession = models.FloatField(default=1)
28
+ price = models.DecimalField(
29
+ max_digits=15,
30
+ decimal_places=4,
31
+ help_text="The amount paid per share",
32
+ verbose_name="DPS",
33
+ )
34
+ total_value_gross = models.GeneratedField(
35
+ expression=models.F("price") * models.F("shares") * models.F("retrocession"),
36
+ output_field=models.DecimalField(
37
+ max_digits=20,
38
+ decimal_places=4,
39
+ ),
40
+ db_persist=True,
41
+ )
13
42
 
14
43
  def save(self, *args, **kwargs):
15
- super().save(*args, factor=Decimal(self.retrocession), **kwargs)
44
+ self.pre_save()
45
+ if not self.record_date and self.ex_date:
46
+ self.record_date = self.ex_date
47
+ elif self.record_date and not self.ex_date:
48
+ self.ex_date = self.record_date
49
+ super().save(*args, **kwargs)
50
+
51
+ def __str__(self):
52
+ return f"{self.total_value} - {self.value_date:%d.%m.%Y} : {str(self.underlying_instrument)} (in {str(self.portfolio)})"
@@ -1,17 +1,17 @@
1
1
  import importlib
2
2
  from contextlib import suppress
3
+ from decimal import Decimal
3
4
 
4
5
  from celery import shared_task
5
6
  from django.db import models
6
7
  from django.db.models import Exists, OuterRef, Q, QuerySet
7
8
  from django.dispatch import receiver
9
+ from wbcore.contrib.io.mixins import ImportMixin
8
10
  from wbfdm.models.instruments.instrument_prices import InstrumentPrice
9
11
 
10
12
  from wbportfolio.import_export.handlers.fees import FeesImportHandler
11
13
  from wbportfolio.models.products import Product
12
14
 
13
- from .transactions import Transaction
14
-
15
15
 
16
16
  class ValidFeesQueryset(QuerySet):
17
17
  def filter_only_valid_fees(self) -> QuerySet:
@@ -22,7 +22,7 @@ class ValidFeesQueryset(QuerySet):
22
22
  real_fees_exists=Exists(
23
23
  self.filter(
24
24
  transaction_subtype=OuterRef("transaction_subtype"),
25
- linked_product=OuterRef("linked_product"),
25
+ product=OuterRef("product"),
26
26
  fee_date=OuterRef("fee_date"),
27
27
  calculated=False,
28
28
  )
@@ -43,7 +43,7 @@ class ValidFeesManager(DefaultFeesManager):
43
43
  return super().get_queryset().filter_only_valid_fees()
44
44
 
45
45
 
46
- class Fees(Transaction):
46
+ class Fees(ImportMixin, models.Model):
47
47
  import_export_handler_class = FeesImportHandler
48
48
 
49
49
  class Type(models.TextChoices):
@@ -57,31 +57,61 @@ class Fees(Transaction):
57
57
  transaction_subtype = models.CharField(
58
58
  max_length=255, verbose_name="Fees Type", choices=Type.choices, default=Type.MANAGEMENT
59
59
  )
60
-
61
- fee_date = models.DateField() # needed for indexing
60
+ fee_date = models.DateField(
61
+ verbose_name="Fees Date",
62
+ help_text="The date that this fee was paid.",
63
+ ) # needed for indexing
64
+ product = models.ForeignKey(
65
+ "wbportfolio.Product",
66
+ related_name="fees",
67
+ on_delete=models.PROTECT,
68
+ verbose_name="Product",
69
+ )
70
+ currency = models.ForeignKey(
71
+ "currency.Currency",
72
+ related_name="fees",
73
+ on_delete=models.PROTECT,
74
+ verbose_name="Currency",
75
+ )
76
+ currency_fx_rate = models.DecimalField(
77
+ max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
78
+ )
79
+ total_value = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value")
80
+ total_value_gross = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value Gross")
81
+ total_value_fx_portfolio = models.GeneratedField(
82
+ expression=models.F("currency_fx_rate") * models.F("total_value"),
83
+ output_field=models.DecimalField(
84
+ max_digits=20,
85
+ decimal_places=4,
86
+ ),
87
+ db_persist=True,
88
+ )
89
+ total_value_gross_fx_portfolio = models.GeneratedField(
90
+ expression=models.F("currency_fx_rate") * models.F("total_value_gross"),
91
+ output_field=models.DecimalField(
92
+ max_digits=20,
93
+ decimal_places=4,
94
+ ),
95
+ db_persist=True,
96
+ )
62
97
  calculated = models.BooleanField(
63
98
  default=True,
64
99
  help_text="A marker whether the fees were calculated or supplied.",
65
100
  verbose_name="Is calculated",
66
101
  )
67
-
68
- linked_product = models.ForeignKey(
69
- "wbportfolio.Product",
70
- related_name="transactionfees",
71
- on_delete=models.PROTECT,
72
- verbose_name="Product",
73
- )
102
+ created = models.DateTimeField(auto_now_add=True)
103
+ updated = models.DateTimeField(auto_now=True)
74
104
 
75
105
  class Meta:
76
106
  verbose_name = "Fees"
77
107
  verbose_name_plural = "Fees"
78
108
  indexes = [
79
- models.Index(fields=["linked_product"]),
80
- models.Index(fields=["transaction_subtype", "linked_product", "fee_date", "calculated"]),
109
+ models.Index(fields=["product"]),
110
+ models.Index(fields=["transaction_subtype", "product", "fee_date", "calculated"]),
81
111
  ]
82
112
  constraints = [
83
113
  models.UniqueConstraint(
84
- fields=["linked_product", "fee_date", "transaction_subtype", "calculated"], name="unique_fees"
114
+ fields=["product", "fee_date", "transaction_subtype", "calculated"], name="unique_fees"
85
115
  ),
86
116
  ]
87
117
 
@@ -89,11 +119,14 @@ class Fees(Transaction):
89
119
  valid_objects = ValidFeesManager()
90
120
 
91
121
  def save(self, *args, **kwargs):
92
- self.fee_date = self.transaction_date
122
+ if self.total_value_gross is None and self.total_value is not None:
123
+ self.total_value_gross = self.total_value
124
+ elif self.total_value is None and self.total_value_gross is not None:
125
+ self.total_value = self.total_value_gross
93
126
  super().save(*args, **kwargs)
94
127
 
95
128
  def __str__(self):
96
- return f"{self.transaction_date:%d.%m.%Y} - {self.Type[self.transaction_subtype]}: {self.portfolio.name}"
129
+ return f"{self.fee_date:%d.%m.%Y} - {self.Type[self.transaction_subtype]}: {self.product.name}"
97
130
 
98
131
  @classmethod
99
132
  def get_endpoint_basename(cls):
@@ -111,8 +144,8 @@ class FeeCalculation(models.Model):
111
144
  calculation_module = importlib.import_module(import_path)
112
145
  for new_fees in calculation_module.fees_calculation(price.id):
113
146
  Fees.objects.update_or_create(
114
- linked_product=new_fees.pop("linked_product"),
115
- transaction_date=new_fees.pop("transaction_date"),
147
+ product=new_fees.pop("product"),
148
+ fee_date=new_fees.pop("fee_date"),
116
149
  transaction_subtype=new_fees.pop("transaction_subtype"),
117
150
  calculated=True,
118
151
  defaults=new_fees,
@@ -145,10 +178,10 @@ def update_or_create_fees_post(sender, instance, created, raw, **kwargs):
145
178
  # .filter(
146
179
  # transaction_date=instance.transaction_date,
147
180
  # transaction_subtype=instance.transaction_subtype,
148
- # linked_product=instance.linked_product,
181
+ # product=instance.product,
149
182
  # )
150
183
  # .exists()
151
184
  # ):
152
185
  # raise ValueError(
153
- # f"A fees object already exists with date, type and product = {instance.transaction_date}, {instance.transaction_subtype}, {instance.linked_product}"
186
+ # f"A fees object already exists with date, type and product = {instance.transaction_date}, {instance.type}, {instance.product}"
154
187
  # )