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,9 +1,7 @@
1
- from contextlib import suppress
2
- from datetime import date, timedelta
1
+ from datetime import timedelta
3
2
  from decimal import Decimal
4
3
 
5
4
  from celery import shared_task
6
- from django.contrib import admin
7
5
  from django.db import models
8
6
  from django.db.models import (
9
7
  Case,
@@ -19,85 +17,20 @@ from django.db.models import (
19
17
  from django.db.models.functions import Coalesce
20
18
  from django.db.models.signals import post_save
21
19
  from django.dispatch import receiver
22
- from django.utils.functional import cached_property
23
- from django.utils.translation import gettext_lazy as _
24
- from django_fsm import GET_STATE, FSMField, transition
25
- from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet
26
- from wbcore.contrib.icons import WBIcon
27
- from wbcore.enums import RequestType
28
- from wbcore.metadata.configs.buttons import ActionButton
29
- from wbcore.models import WBModel
20
+ from wbcore.contrib.io.mixins import ImportMixin
21
+ from wbcore.signals import pre_merge
30
22
  from wbcore.signals.models import pre_collection
23
+ from wbfdm.models import Instrument
31
24
  from wbfdm.models.instruments.instrument_prices import InstrumentPrice
25
+ from wbfdm.signals import add_instrument_to_investable_universe
32
26
 
33
27
  from wbportfolio.import_export.handlers.trade import TradeImportHandler
34
- from wbportfolio.models.asset import AssetPosition
35
28
  from wbportfolio.models.custodians import Custodian
36
- from wbportfolio.models.roles import PortfolioRole
37
- from wbportfolio.pms.typing import Trade as TradeDTO
38
29
 
39
- from .transactions import ShareMixin, Transaction
30
+ from .transactions import TransactionMixin
40
31
 
41
32
 
42
- class TradeQueryset(OrderedModelQuerySet):
43
- def annotate_base_info(self):
44
- return self.annotate(
45
- last_effective_date=Subquery(
46
- AssetPosition.unannotated_objects.filter(
47
- date__lte=OuterRef("value_date"),
48
- portfolio=OuterRef("portfolio"),
49
- )
50
- .order_by("-date")
51
- .values("date")[:1]
52
- ),
53
- effective_weight=Coalesce(
54
- Subquery(
55
- AssetPosition.unannotated_objects.filter(
56
- underlying_quote=OuterRef("underlying_instrument"),
57
- date=OuterRef("last_effective_date"),
58
- portfolio=OuterRef("portfolio"),
59
- )
60
- .values("portfolio")
61
- .annotate(s=Sum("weighting"))
62
- .values("s")[:1]
63
- ),
64
- Decimal(0),
65
- ),
66
- target_weight=F("effective_weight") + F("weighting"),
67
- effective_shares=Coalesce(
68
- Subquery(
69
- AssetPosition.objects.filter(
70
- underlying_quote=OuterRef("underlying_instrument"),
71
- date=OuterRef("last_effective_date"),
72
- portfolio=OuterRef("portfolio"),
73
- )
74
- .values("portfolio")
75
- .annotate(s=Sum("shares"))
76
- .values("s")[:1]
77
- ),
78
- Decimal(0),
79
- ),
80
- target_shares=F("effective_shares") + F("shares"),
81
- )
82
-
83
-
84
- class DefaultTradeManager(OrderedModelManager):
85
- """This manager is expect to be the trade default manager and annotate by default the effective weight (extracted
86
- from the associated portfolio) and the target weight as an addition between the effective weight and the delta weight
87
- """
88
-
89
- def __init__(self, with_annotation: bool = False, *args, **kwargs):
90
- self.with_annotation = with_annotation
91
- super().__init__(*args, **kwargs)
92
-
93
- def get_queryset(self) -> TradeQueryset:
94
- qs = TradeQueryset(self.model, using=self._db)
95
- if self.with_annotation:
96
- qs = qs.annotate_base_info()
97
- return qs
98
-
99
-
100
- class ValidCustomerTradeManager(DefaultTradeManager):
33
+ class ValidCustomerTradeManager(models.Manager):
101
34
  def __init__(self, without_internal_trade: bool = False):
102
35
  self.without_internal_trade = without_internal_trade
103
36
  super().__init__()
@@ -117,18 +50,11 @@ class ValidCustomerTradeManager(DefaultTradeManager):
117
50
  return qs
118
51
 
119
52
 
120
- class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
53
+ class Trade(TransactionMixin, ImportMixin, models.Model):
121
54
  import_export_handler_class = TradeImportHandler
122
55
 
123
56
  TRADE_WINDOW_INTERVAL = 7
124
57
 
125
- class Status(models.TextChoices):
126
- DRAFT = "DRAFT", "Draft"
127
- SUBMIT = "SUBMIT", "Submit"
128
- EXECUTED = "EXECUTED", "Executed"
129
- CONFIRMED = "CONFIRMED", "Confirmed"
130
- FAILED = "FAILED", "Failed"
131
-
132
58
  class Type(models.TextChoices):
133
59
  REBALANCE = "REBALANCE", "Rebalance"
134
60
  DECREASE = "DECREASE", "Decrease"
@@ -139,55 +65,71 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
139
65
  SELL = "SELL", "Sell"
140
66
  NO_CHANGE = "NO_CHANGE", "No Change" # default transaction subtype if weighing is 0
141
67
 
142
- external_identifier2 = models.CharField(
143
- max_length=255,
144
- null=True,
145
- blank=True,
146
- help_text="A second external identifier that was supplied.",
147
- verbose_name="External Identifier 2",
148
- )
149
-
150
68
  transaction_subtype = models.CharField(
151
69
  max_length=32, default=Type.BUY, choices=Type.choices, verbose_name="Trade Type"
152
70
  )
153
- status = FSMField(default=Status.CONFIRMED, choices=Status.choices, verbose_name="Status")
71
+ transaction_date = models.DateField(
72
+ verbose_name="Trade Date",
73
+ help_text="The date that this transaction was traded.",
74
+ )
75
+ book_date = models.DateField(
76
+ verbose_name="Trade Date",
77
+ help_text="The date that this transaction was booked.",
78
+ )
79
+ shares = models.DecimalField(
80
+ max_digits=15,
81
+ decimal_places=4,
82
+ default=Decimal("0.0"),
83
+ help_text="The number of shares that were traded.",
84
+ verbose_name="Shares",
85
+ )
86
+
154
87
  weighting = models.DecimalField(
155
- max_digits=16,
156
- decimal_places=6,
88
+ max_digits=9,
89
+ decimal_places=8,
157
90
  default=Decimal(0),
158
91
  help_text="The weight to be multiplied against the target",
159
92
  verbose_name="Weight",
160
93
  )
161
- bank = models.CharField(
162
- max_length=255,
163
- help_text="The bank/counterparty/custodian the trade went through.",
164
- verbose_name="Counterparty",
94
+ claimed_shares = models.DecimalField(
95
+ max_digits=15,
96
+ decimal_places=4,
97
+ default=Decimal(0),
98
+ help_text="The number of shares that were claimed.",
99
+ verbose_name="Claimed Shares",
165
100
  )
166
- custodian = models.ForeignKey(
167
- "wbportfolio.Custodian", null=True, blank=True, on_delete=models.SET_NULL, related_name="trades"
101
+ diff_shares = models.GeneratedField(
102
+ expression=F("shares") - F("claimed_shares"),
103
+ output_field=models.DecimalField(max_digits=15, decimal_places=4),
104
+ db_persist=True,
105
+ )
106
+ internal_trade = models.OneToOneField(
107
+ "wbportfolio.Trade",
108
+ null=True,
109
+ blank=True,
110
+ on_delete=models.SET_NULL,
111
+ related_name="internal_subscription_redemption_trade",
168
112
  )
169
113
  marked_for_deletion = models.BooleanField(
170
114
  default=False,
171
115
  help_text="If this is checked, then the trade is supposed to be deleted.",
172
116
  verbose_name="To be deleted",
173
117
  )
174
-
175
- # Only valid for subscription and redemption trade
176
118
  marked_as_internal = models.BooleanField(
177
119
  default=False,
178
120
  help_text="If this is checked, then this subscription or redemption is considered internal and will not be considered in any AUM computation",
179
121
  verbose_name="Internal",
180
122
  )
181
- internal_trade = models.OneToOneField(
182
- "wbportfolio.Trade",
183
- null=True,
184
- blank=True,
185
- on_delete=models.SET_NULL,
186
- related_name="internal_subscription_redemption_trade",
187
- )
188
-
189
123
  pending = models.BooleanField(default=False)
190
124
  exclude_from_history = models.BooleanField(default=False)
125
+ bank = models.CharField(
126
+ max_length=255,
127
+ help_text="The bank/counterparty/custodian the trade went through.",
128
+ verbose_name="Counterparty",
129
+ )
130
+ custodian = models.ForeignKey(
131
+ "wbportfolio.Custodian", null=True, blank=True, on_delete=models.SET_NULL, related_name="trades"
132
+ )
191
133
  register = models.ForeignKey(
192
134
  to="wbportfolio.Register",
193
135
  null=True,
@@ -195,215 +137,26 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
195
137
  related_name="trades",
196
138
  on_delete=models.PROTECT,
197
139
  )
198
-
199
- trade_proposal = models.ForeignKey(
200
- to="wbportfolio.TradeProposal",
140
+ external_id = models.CharField(
141
+ max_length=255,
201
142
  null=True,
202
143
  blank=True,
203
- related_name="trades",
204
- on_delete=models.CASCADE,
205
- help_text="The Trade Proposal this trade is coming from",
144
+ help_text="An external identifier that was supplied.",
145
+ verbose_name="External Identifier",
206
146
  )
207
- claimed_shares = models.DecimalField(
208
- max_digits=15,
209
- decimal_places=4,
210
- default=Decimal(0),
211
- help_text="The number of shares that were claimed.",
212
- verbose_name="Claimed Shares",
213
- )
214
- diff_shares = models.GeneratedField(
215
- expression=F("shares") - F("claimed_shares"),
216
- output_field=models.DecimalField(max_digits=15, decimal_places=4),
217
- db_persist=True,
147
+ external_id_alternative = models.CharField(
148
+ max_length=255,
149
+ null=True,
150
+ blank=True,
151
+ help_text="A second external identifier that was supplied.",
152
+ verbose_name="Alternative External Identifier",
218
153
  )
219
- objects = DefaultTradeManager()
220
- annotated_objects = DefaultTradeManager(with_annotation=True)
154
+
155
+ # Manager
156
+ objects = models.Manager()
221
157
  valid_customer_trade_objects = ValidCustomerTradeManager()
222
158
  valid_external_customer_trade_objects = ValidCustomerTradeManager(without_internal_trade=True)
223
159
 
224
- @transition(
225
- field=status,
226
- source=Status.DRAFT,
227
- target=GET_STATE(
228
- lambda self, **kwargs: (
229
- self.Status.SUBMIT if self.last_underlying_quote_price is not None else self.Status.FAILED
230
- ),
231
- states=[Status.SUBMIT, Status.FAILED],
232
- ),
233
- permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
234
- user.profile, portfolio=instance.portfolio
235
- ),
236
- custom={
237
- "_transition_button": ActionButton(
238
- method=RequestType.PATCH,
239
- identifiers=("wbportfolio:trade",),
240
- icon=WBIcon.SEND.icon,
241
- key="submit",
242
- label="Submit",
243
- action_label="Submit",
244
- # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
245
- )
246
- },
247
- on_error="FAILED",
248
- )
249
- def submit(self, by=None, description=None, **kwargs):
250
- if not self.last_underlying_quote_price:
251
- self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
252
-
253
- def can_submit(self):
254
- pass
255
-
256
- @transition(
257
- field=status,
258
- source=Status.DRAFT,
259
- target=Status.FAILED,
260
- permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
261
- user.profile, portfolio=instance.portfolio
262
- ),
263
- )
264
- def fail(self, **kwargs):
265
- self.comment = f"Trade failed because no price is found for {self.underlying_instrument.computed_str} on {self.transaction_date:%Y-%m-%d}"
266
-
267
- @cached_property
268
- def last_underlying_quote_price(self) -> InstrumentPrice | None:
269
- try:
270
- # we try t0 first
271
- return InstrumentPrice.objects.filter_only_valid_prices().get(
272
- instrument=self.underlying_instrument, date=self.transaction_date
273
- )
274
- except InstrumentPrice.DoesNotExist:
275
- with suppress(InstrumentPrice.DoesNotExist):
276
- # we fall back to the latest price before t0
277
- return (
278
- InstrumentPrice.objects.filter_only_valid_prices()
279
- .filter(instrument=self.underlying_instrument, date__lte=self.value_date)
280
- .latest("date")
281
- )
282
-
283
- @transition(
284
- field=status,
285
- source=Status.SUBMIT,
286
- target=Status.EXECUTED,
287
- permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
288
- user.profile, portfolio=instance.portfolio
289
- ),
290
- custom={
291
- "_transition_button": ActionButton(
292
- method=RequestType.PATCH,
293
- identifiers=("wbportfolio:trade",),
294
- icon=WBIcon.CONFIRM.icon,
295
- key="execute",
296
- label="Execute",
297
- action_label="Execute",
298
- # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
299
- )
300
- },
301
- )
302
- def execute(self, **kwargs):
303
- with suppress(ValueError):
304
- asset = self.get_asset()
305
- AssetPosition.unannotated_objects.update_or_create(
306
- underlying_quote=asset.underlying_quote,
307
- portfolio_created=asset.portfolio_created,
308
- portfolio=asset.portfolio,
309
- date=asset.date,
310
- defaults={
311
- "initial_currency_fx_rate": asset.initial_currency_fx_rate,
312
- "initial_price": asset.initial_price,
313
- "initial_shares": asset.initial_shares,
314
- "underlying_quote_price": asset.underlying_quote_price,
315
- "asset_valuation_date": asset.asset_valuation_date,
316
- "currency": asset.currency,
317
- "is_estimated": asset.is_estimated,
318
- "weighting": asset.weighting,
319
- },
320
- )
321
-
322
- def can_execute(self):
323
- if not self.last_underlying_quote_price:
324
- return {"underlying_instrument": [_("Cannot execute a trade without a valid quote price")]}
325
- if not self.portfolio.is_manageable:
326
- return {
327
- "portfolio": [_("The portfolio needs to be a model portfolio in order to execute this trade manually")]
328
- }
329
-
330
- @transition(
331
- field=status,
332
- source=Status.EXECUTED,
333
- target=Status.CONFIRMED,
334
- permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
335
- user.profile, portfolio=instance.portfolio
336
- ),
337
- custom={
338
- "_transition_button": ActionButton(
339
- method=RequestType.PATCH,
340
- identifiers=("wbportfolio:trade",),
341
- icon=WBIcon.CONFIRM.icon,
342
- key="confirm",
343
- label="Confirm",
344
- action_label="Confirme",
345
- # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
346
- )
347
- },
348
- )
349
- def confirm(self, by=None, description=None, **kwargs):
350
- pass
351
-
352
- def can_confirm(self):
353
- pass
354
-
355
- @transition(
356
- field=status,
357
- source=Status.SUBMIT,
358
- target=Status.DRAFT,
359
- permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
360
- user.profile, portfolio=instance.portfolio
361
- ),
362
- custom={
363
- "_transition_button": ActionButton(
364
- method=RequestType.PATCH,
365
- identifiers=("wbportfolio:trade",),
366
- icon=WBIcon.UNDO.icon,
367
- key="backtodraft",
368
- label="Back to Draft",
369
- action_label="backtodraft",
370
- # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
371
- )
372
- },
373
- )
374
- def backtodraft(self, **kwargs):
375
- pass
376
-
377
- @transition(
378
- field=status,
379
- source=Status.EXECUTED,
380
- target=Status.DRAFT,
381
- permission=lambda instance, user: PortfolioRole.is_portfolio_manager(
382
- user.profile, portfolio=instance.portfolio
383
- ),
384
- custom={
385
- "_transition_button": ActionButton(
386
- method=RequestType.PATCH,
387
- identifiers=("wbportfolio:trade",),
388
- icon=WBIcon.UNDO.icon,
389
- key="reverte",
390
- label="Revert",
391
- action_label="revert",
392
- # description_fields="<p>Start: {{start}}</p><p>End: {{end}}</p><p>Title: {{title}}</p>",
393
- )
394
- },
395
- )
396
- def revert(self, to_date=None, **kwargs):
397
- with suppress(AssetPosition.DoesNotExist):
398
- asset = AssetPosition.unannotated_objects.get(
399
- underlying_quote=self.underlying_instrument,
400
- portfolio=self.portfolio,
401
- date=self.transaction_date,
402
- is_estimated=False,
403
- )
404
- asset.set_weighting(asset.weighting - self.weighting)
405
- asset.save()
406
-
407
160
  @property
408
161
  def product(self):
409
162
  from wbportfolio.models.products import Product
@@ -413,63 +166,13 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
413
166
  except Product.DoesNotExist:
414
167
  return None
415
168
 
416
- @property
417
- @admin.display(description="Last Effective Date")
418
- def _last_effective_date(self) -> date:
419
- if hasattr(self, "last_effective_date"):
420
- return self.last_effective_date
421
- elif (
422
- assets := AssetPosition.unannotated_objects.filter(
423
- underlying_quote=self.underlying_instrument,
424
- date__lte=self.value_date,
425
- portfolio=self.portfolio,
426
- )
427
- ).exists():
428
- return assets.latest("date").date
429
-
430
- @property
431
- @admin.display(description="Effective Weight")
432
- def _effective_weight(self) -> Decimal:
433
- return getattr(
434
- self,
435
- "effective_weight",
436
- AssetPosition.unannotated_objects.filter(
437
- underlying_quote=self.underlying_instrument,
438
- date=self._last_effective_date,
439
- portfolio=self.portfolio,
440
- ).aggregate(s=Sum("weighting"))["s"]
441
- or Decimal(0),
442
- )
443
-
444
- @property
445
- @admin.display(description="Effective Shares")
446
- def _effective_shares(self) -> Decimal:
447
- return getattr(
448
- self,
449
- "effective_shares",
450
- AssetPosition.objects.filter(
451
- underlying_quote=self.underlying_instrument,
452
- date=self.transaction_date,
453
- portfolio=self.portfolio,
454
- ).aggregate(s=Sum("shares"))["s"]
455
- or Decimal(0),
456
- )
457
-
458
- @property
459
- @admin.display(description="Target Weight")
460
- def _target_weight(self) -> Decimal:
461
- return getattr(self, "target_weight", self._effective_weight + self.weighting)
462
-
463
- @property
464
- @admin.display(description="Target Shares")
465
- def _target_shares(self) -> Decimal:
466
- return getattr(self, "target_shares", self._effective_shares + self.shares)
467
-
468
- order_with_respect_to = "trade_proposal"
469
-
470
- class Meta(OrderedModel.Meta):
169
+ class Meta:
471
170
  verbose_name = "Trade"
472
171
  verbose_name_plural = "Trades"
172
+ indexes = [
173
+ models.Index(fields=["underlying_instrument", "transaction_date"]),
174
+ models.Index(fields=["portfolio", "underlying_instrument", "transaction_date"]),
175
+ ]
473
176
  constraints = [
474
177
  models.CheckConstraint(
475
178
  check=models.Q(marked_as_internal=False)
@@ -490,42 +193,34 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
490
193
  ]
491
194
  # notification_email_template = "portfolio/email/trade_notification.html"
492
195
 
493
- def __init__(self, *args, target_weight: Decimal | None = None, **kwargs):
494
- super().__init__(*args, **kwargs)
495
- if target_weight is not None: # if target weight is provided, we guess the corresponding weighting
496
- self.weighting = Decimal(target_weight) - self._effective_weight
497
- self._set_transaction_subtype()
498
-
499
196
  def save(self, *args, **kwargs):
500
- if self.trade_proposal:
501
- self.portfolio = self.trade_proposal.portfolio
502
- self.transaction_date = self.trade_proposal.trade_date
503
- self.value_date = self.trade_proposal.last_effective_date
504
- if not self.portfolio.only_weighting:
505
- with suppress(ValueError):
506
- self.shares = self.trade_proposal.get_estimated_shares(self.weighting, self.underlying_instrument)
197
+ self.pre_save()
198
+ if not self.weighting and (total_asset_value := self.portfolio.get_total_asset_value(self.transaction_date)):
199
+ self.weighting = self.currency_fx_rate * self.price * self.shares / total_asset_value
507
200
 
508
- if not self.custodian and self.bank:
509
- self.custodian = Custodian.get_by_mapping(self.bank)
510
- if self.price is None:
201
+ if abs(self.weighting) < 10e-6:
202
+ self.weighting = Decimal("0")
203
+ if not self.price:
511
204
  # we try to get the price if not provided directly from the underlying instrument
512
- with suppress(Exception):
513
- self.price = self.underlying_instrument.get_price(self.value_date)
205
+ self.price = self.get_price()
514
206
 
515
- self.transaction_type = Transaction.Type.TRADE
207
+ if not self.custodian and self.bank:
208
+ self.custodian = Custodian.get_by_mapping(self.bank)
516
209
 
517
- if self.transaction_subtype is None or self.trade_proposal:
210
+ if self.transaction_subtype is None:
518
211
  # if subtype not provided, we extract it automatically from the existing data.
519
- self._set_transaction_subtype()
212
+ self._set_type()
520
213
  if self.id and hasattr(self, "claims"):
521
- self.claimed_shares = self.trade.claims.filter(status="APPROVED").aggregate(s=Sum("shares"))[
522
- "s"
523
- ] or Decimal(0)
214
+ self.claimed_shares = self.claims.filter(status="APPROVED").aggregate(s=Sum("shares"))["s"] or Decimal(0)
524
215
  if self.internal_trade:
525
216
  self.marked_as_internal = True
217
+ if not self.value_date:
218
+ self.value_date = self.transaction_date
219
+ if not self.book_date:
220
+ self.book_date = self.transaction_date
526
221
  super().save(*args, **kwargs)
527
222
 
528
- def _set_transaction_subtype(self):
223
+ def _set_type(self):
529
224
  if self.weighting == 0:
530
225
  self.transaction_subtype = Trade.Type.NO_CHANGE
531
226
  if self.underlying_instrument.instrument_type.key == "product":
@@ -536,45 +231,17 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
536
231
  self.transaction_subtype = Trade.Type.REDEMPTION
537
232
  elif self.weighting is not None:
538
233
  if self.weighting > 0:
539
- if self._effective_weight:
540
- self.transaction_subtype = Trade.Type.INCREASE
541
- else:
542
- self.transaction_subtype = Trade.Type.BUY
234
+ self.transaction_subtype = Trade.Type.INCREASE
543
235
  elif self.weighting < 0:
544
- if self._target_weight:
545
- self.transaction_subtype = Trade.Type.DECREASE
546
- else:
547
- self.transaction_subtype = Trade.Type.SELL
236
+ self.transaction_subtype = Trade.Type.DECREASE
548
237
  else:
549
238
  self.transaction_subtype = Trade.Type.REBALANCE
550
239
 
551
- def get_transaction_subtype(self) -> str:
552
- """
553
- Return the expected transaction subtype based n
554
-
555
- """
556
-
557
- def get_asset(self) -> AssetPosition:
558
- last_underlying_quote_price = self.last_underlying_quote_price
559
- if not last_underlying_quote_price:
560
- raise ValueError("No price found")
561
- asset = AssetPosition(
562
- underlying_quote=self.underlying_instrument,
563
- portfolio_created=None,
564
- portfolio=self.portfolio,
565
- date=self.transaction_date,
566
- initial_currency_fx_rate=self.currency_fx_rate,
567
- weighting=self._target_weight,
568
- initial_price=self.last_underlying_quote_price.net_value,
569
- initial_shares=None,
570
- underlying_quote_price=self.last_underlying_quote_price,
571
- asset_valuation_date=self.transaction_date,
572
- currency=self.currency,
573
- is_estimated=False,
574
- )
575
- asset.set_weighting(self._target_weight)
576
- asset.pre_save()
577
- return asset
240
+ def get_price(self) -> Decimal:
241
+ try:
242
+ return self.underlying_instrument.get_price(self.transaction_date)
243
+ except ValueError:
244
+ return Decimal("0")
578
245
 
579
246
  def delete(self, **kwargs):
580
247
  pre_collection.send(sender=self.__class__, instance=self)
@@ -584,22 +251,6 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
584
251
  ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
585
252
  return f"{ticker}{self.shares} ({self.bank})"
586
253
 
587
- def _build_dto(self) -> TradeDTO:
588
- """
589
- Data Transfer Object
590
- Returns:
591
- DTO trade object
592
- """
593
- return TradeDTO(
594
- id=self.id,
595
- underlying_instrument=self.underlying_instrument.id,
596
- effective_weight=self._effective_weight,
597
- target_weight=self._target_weight,
598
- instrument_type=self.underlying_instrument.security_instrument_type.id,
599
- currency=self.underlying_instrument.currency,
600
- date=self.transaction_date,
601
- )
602
-
603
254
  def get_alternative_valid_trades(self, share_delta: float = 0):
604
255
  return Trade.objects.filter(
605
256
  Q(underlying_instrument=self.underlying_instrument)
@@ -742,6 +393,10 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
742
393
  def get_representation_label_key(cls):
743
394
  return "{{|:-}}{{transaction_date}}{{|::}}{{bank}}{{|-:}} {{claimed_shares}} / {{shares}} (∆ {{diff_shares}})"
744
395
 
396
+ @classmethod
397
+ def get_representation_value_key(cls):
398
+ return "id"
399
+
745
400
 
746
401
  @shared_task
747
402
  def align_custodian():
@@ -756,3 +411,27 @@ def align_custodian():
756
411
  def compute_claimed_shares_on_claim_save(sender, instance, created, raw, **kwargs):
757
412
  if not raw and instance.trade:
758
413
  instance.trade.save()
414
+
415
+
416
+ @receiver(pre_merge, sender="wbfdm.Instrument")
417
+ def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
418
+ """
419
+ Simply reassign the transactions linked to the merged instrument to the main instrument
420
+ """
421
+ merged_object.trades.update(underlying_instrument=main_object)
422
+
423
+
424
+ @receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
425
+ def add_instrument_to_investable_universe_from_transactions(sender: models.Model, **kwargs) -> list[int]:
426
+ """
427
+ register all instrument linked to assets as within the investible universe
428
+ """
429
+ return list(
430
+ (
431
+ Instrument.objects.annotate(
432
+ transaction_exists=models.Exists(Trade.objects.filter(underlying_instrument=models.OuterRef("pk")))
433
+ ).filter(transaction_exists=True)
434
+ )
435
+ .distinct()
436
+ .values_list("id", flat=True)
437
+ )