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,105 +1,94 @@
1
1
  from decimal import Decimal
2
2
 
3
- from django.apps import apps
4
3
  from django.db import models
5
- from django.dispatch import receiver
6
- from wbcore.contrib.io.mixins import ImportMixin
7
- from wbcore.signals import pre_merge
8
- from wbfdm.models.instruments.instruments import Instrument
9
- from wbfdm.signals import add_instrument_to_investable_universe
10
4
 
11
5
 
12
- class ShareMixin(models.Model):
13
- shares = models.DecimalField(
14
- max_digits=15,
15
- decimal_places=4,
16
- default=Decimal("0.0"),
17
- help_text="The number of shares that were traded.",
18
- verbose_name="Shares",
6
+ class TransactionMixin(models.Model):
7
+ value_date = models.DateField(
8
+ verbose_name="Value Date",
9
+ help_text="The date that this transaction was valuated/paid.",
10
+ )
11
+ portfolio = models.ForeignKey(
12
+ "wbportfolio.Portfolio", related_name="%(class)ss", on_delete=models.PROTECT, verbose_name="Portfolio"
13
+ )
14
+ underlying_instrument = models.ForeignKey(
15
+ to="wbfdm.Instrument",
16
+ related_name="%(class)ss",
17
+ limit_choices_to=models.Q(children__isnull=True),
18
+ on_delete=models.PROTECT,
19
+ verbose_name="Underlying Instrument",
20
+ help_text="The instrument that is this transaction.",
21
+ )
22
+ currency = models.ForeignKey(
23
+ "currency.Currency",
24
+ related_name="%(class)ss",
25
+ on_delete=models.PROTECT,
26
+ verbose_name="Currency",
27
+ )
28
+ currency_fx_rate = models.DecimalField(
29
+ max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
19
30
  )
20
31
  price = models.DecimalField(
21
32
  max_digits=16,
22
33
  decimal_places=4,
23
- default=Decimal(
24
- "0.0"
25
- ), # we shouldn't default to anything but we have trade with price=None. Needs to be handled carefully
26
34
  help_text="The price per share.",
27
35
  verbose_name="Price",
28
36
  )
29
-
30
37
  price_gross = models.DecimalField(
31
38
  max_digits=16,
32
39
  decimal_places=4,
33
40
  help_text="The gross price per share.",
34
41
  verbose_name="Gross Price",
35
42
  )
36
-
37
- def save(
38
- self,
39
- *args,
40
- factor: Decimal = Decimal("1.0"),
41
- **kwargs,
42
- ):
43
- if self.price_gross is None:
44
- self.price_gross = self.price
45
-
46
- self.total_value = self.price * self.shares * factor
47
- self.total_value_gross = self.price_gross * self.shares * factor
48
- super().save(*args, **kwargs)
49
-
50
- class Meta:
51
- abstract = True
52
-
53
-
54
- class Transaction(ImportMixin, models.Model):
55
- class Type(models.TextChoices):
56
- # Standart Asset Types
57
- TRADE = "Trade", "Trade"
58
- DIVIDEND_TRANSACTION = "DividendTransaction", "Dividend Transaction"
59
- EXPIRY = "Expiry", "Expiry"
60
- FEES = "Fees", "Fees"
61
-
62
- transaction_type = models.CharField(max_length=255, verbose_name="Type", choices=Type.choices, default=Type.TRADE)
63
-
64
- portfolio = models.ForeignKey(
65
- "wbportfolio.Portfolio", related_name="transactions", on_delete=models.PROTECT, verbose_name="Portfolio"
66
- )
67
-
68
- underlying_instrument = models.ForeignKey(
69
- to="wbfdm.Instrument",
70
- related_name="transactions",
71
- limit_choices_to=models.Q(children__isnull=True),
72
- on_delete=models.PROTECT,
73
- verbose_name="Underlying Instrument",
74
- help_text="The instrument that is this transaction.",
43
+ shares = models.DecimalField(
44
+ max_digits=15,
45
+ decimal_places=4,
46
+ default=Decimal("0.0"),
47
+ help_text="The number of shares held at record date, used to calculate the dividend",
48
+ verbose_name="Shares / Quantity",
75
49
  )
76
-
77
- transaction_date = models.DateField(
78
- verbose_name="Trade Date",
79
- help_text="The date that this transaction was traded.",
50
+ fees = models.GeneratedField(
51
+ expression=models.F("price_gross") - models.F("price"),
52
+ output_field=models.DecimalField(
53
+ max_digits=20,
54
+ decimal_places=4,
55
+ ),
56
+ db_persist=True,
80
57
  )
81
- book_date = models.DateField(
82
- verbose_name="Trade Date",
83
- help_text="The date that this transaction was booked.",
58
+ total_value_gross = models.GeneratedField(
59
+ expression=models.F("price_gross") * models.F("shares"),
60
+ output_field=models.DecimalField(
61
+ max_digits=20,
62
+ decimal_places=4,
63
+ ),
64
+ db_persist=True,
84
65
  )
85
- value_date = models.DateField(
86
- verbose_name="Value Date",
87
- help_text="The date that this transaction was valuated.",
66
+ total_value = models.GeneratedField(
67
+ expression=models.F("price") * models.F("shares"),
68
+ output_field=models.DecimalField(
69
+ max_digits=20,
70
+ decimal_places=4,
71
+ ),
72
+ db_persist=True,
88
73
  )
89
-
90
- currency = models.ForeignKey(
91
- "currency.Currency",
92
- related_name="transactions",
93
- on_delete=models.PROTECT,
94
- verbose_name="Currency",
74
+ price_fx_portfolio = models.GeneratedField(
75
+ expression=models.F("currency_fx_rate") * models.F("price"),
76
+ output_field=models.DecimalField(
77
+ max_digits=20,
78
+ decimal_places=4,
79
+ ),
80
+ db_persist=True,
95
81
  )
96
- currency_fx_rate = models.DecimalField(
97
- max_digits=14, decimal_places=8, default=Decimal(1.0), verbose_name="FOREX rate"
82
+ price_gross_fx_portfolio = models.GeneratedField(
83
+ expression=models.F("currency_fx_rate") * models.F("price_gross"),
84
+ output_field=models.DecimalField(
85
+ max_digits=20,
86
+ decimal_places=4,
87
+ ),
88
+ db_persist=True,
98
89
  )
99
- total_value = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value")
100
- total_value_gross = models.DecimalField(max_digits=20, decimal_places=4, verbose_name="Total Value Gross")
101
90
  total_value_fx_portfolio = models.GeneratedField(
102
- expression=models.F("currency_fx_rate") * models.F("total_value"),
91
+ expression=models.F("currency_fx_rate") * models.F("price") * models.F("shares"),
103
92
  output_field=models.DecimalField(
104
93
  max_digits=20,
105
94
  decimal_places=4,
@@ -107,99 +96,30 @@ class Transaction(ImportMixin, models.Model):
107
96
  db_persist=True,
108
97
  )
109
98
  total_value_gross_fx_portfolio = models.GeneratedField(
110
- expression=models.F("currency_fx_rate") * models.F("total_value_gross"),
99
+ expression=models.F("currency_fx_rate") * models.F("price_gross") * models.F("shares"),
111
100
  output_field=models.DecimalField(
112
101
  max_digits=20,
113
102
  decimal_places=4,
114
103
  ),
115
104
  db_persist=True,
116
105
  )
117
- external_id = models.CharField(
118
- max_length=255,
119
- null=True,
120
- blank=True,
121
- help_text="An external identifier that was supplied.",
122
- verbose_name="External Identifier",
123
- )
124
- comment = models.TextField(default="", verbose_name="Comment", blank=True)
125
106
 
126
- def save(self, *args, **kwargs):
127
- if not self.value_date:
128
- self.value_date = self.transaction_date
129
- if not self.book_date:
130
- self.book_date = self.transaction_date
107
+ comment = models.TextField(default="", verbose_name="Comment", blank=True)
108
+ created = models.DateTimeField(auto_now_add=True)
109
+ updated = models.DateTimeField(auto_now=True)
131
110
 
132
- if not getattr(self, "currency", None) and self.underlying_instrument:
111
+ def pre_save(self):
112
+ if self.underlying_instrument:
133
113
  self.currency = self.underlying_instrument.currency
114
+
115
+ if self.price is not None and self.price_gross is None:
116
+ self.price_gross = self.price
117
+ elif self.price_gross is not None and self.price is None:
118
+ self.price = self.price_gross
134
119
  if self.currency_fx_rate is None:
135
120
  self.currency_fx_rate = self.underlying_instrument.currency.convert(
136
121
  self.value_date, self.portfolio.currency, exact_lookup=True
137
122
  )
138
- if not self.transaction_type:
139
- self.transaction_type = self.__class__.__name__
140
-
141
- if self.total_value_gross is None:
142
- self.total_value_gross = self.total_value
143
-
144
- super().save(*args, **kwargs)
145
-
146
- def __str__(self):
147
- return f"{self.total_value} - {self.transaction_date:%d.%m.%Y} : {str(self.underlying_instrument)} (in {str(self.portfolio)})"
148
-
149
- def get_casted_model(self):
150
- return apps.get_model(app_label="wbportfolio", model_name=self.transaction_type)
151
-
152
- def get_casted_transaction(self) -> models.Model:
153
- """
154
- Cast the asset into its child representative
155
- """
156
- model = self.get_casted_model()
157
- return model.objects.get(pk=self.pk)
158
123
 
159
124
  class Meta:
160
- verbose_name = "Transaction"
161
- verbose_name_plural = "Transactions"
162
- indexes = [
163
- models.Index(fields=["underlying_instrument", "transaction_date"]),
164
- # models.Index(fields=["date", "underlying_instrument"]),
165
- ]
166
-
167
- objects = models.Manager()
168
-
169
- @classmethod
170
- def get_representation_value_key(cls):
171
- return "id"
172
-
173
- @classmethod
174
- def get_representation_label_key(cls):
175
- return "{{total_value}}{{transaction_date}}"
176
-
177
- @classmethod
178
- def get_endpoint_basename(cls):
179
- return "wbportfolio:transaction"
180
-
181
-
182
- @receiver(pre_merge, sender="wbfdm.Instrument")
183
- def pre_merge_instrument(sender: models.Model, merged_object: "Instrument", main_object: "Instrument", **kwargs):
184
- """
185
- Simply reassign the transactions linked to the merged instrument to the main instrument
186
- """
187
- merged_object.transactions.update(underlying_instrument=main_object)
188
-
189
-
190
- @receiver(add_instrument_to_investable_universe, sender="wbfdm.Instrument")
191
- def add_instrument_to_investable_universe_from_transactions(sender: models.Model, **kwargs) -> list[int]:
192
- """
193
- register all instrument linked to assets as within the investible universe
194
- """
195
- return list(
196
- (
197
- Instrument.objects.annotate(
198
- transaction_exists=models.Exists(
199
- Transaction.objects.filter(underlying_instrument=models.OuterRef("pk"))
200
- )
201
- ).filter(transaction_exists=True)
202
- )
203
- .distinct()
204
- .values_list("id", flat=True)
205
- )
125
+ abstract = True
@@ -1,6 +1,17 @@
1
+ import logging
2
+ from datetime import date
3
+ from decimal import Decimal
4
+ from typing import Iterator
5
+
6
+ from celery import shared_task
7
+ from django.db.models import F, QuerySet, Window
8
+ from django.db.models.functions import RowNumber
9
+ from tqdm import tqdm
1
10
  from wbfdm.models import Instrument
2
11
 
3
- from wbportfolio.models import Index, Product
12
+ from wbportfolio.models import AssetPosition, Index, Order, OrderProposal, Portfolio, Product
13
+
14
+ logger = logging.getLogger("pms")
4
15
 
5
16
 
6
17
  def get_casted_portfolio_instrument(instrument: Instrument) -> Product | Index | None:
@@ -11,3 +22,91 @@ def get_casted_portfolio_instrument(instrument: Instrument) -> Product | Index |
11
22
  return Index.objects.get(id=instrument.id)
12
23
  except Index.DoesNotExist:
13
24
  return None
25
+
26
+
27
+ def get_adjusted_shares(old_shares: Decimal, old_price: Decimal, new_price: Decimal) -> Decimal:
28
+ return old_shares * (old_price / new_price)
29
+
30
+
31
+ def adjust_assets(qs: Iterator[AssetPosition], underlying_quote: Instrument):
32
+ objs = []
33
+ logger.info("adjusting asset positions...")
34
+ for a in qs:
35
+ old_price: Decimal = a.initial_price
36
+ a.initial_price = a.underlying_instrument = a.underlying_quote_price = None
37
+ a.underlying_quote = underlying_quote
38
+ a.pre_save()
39
+ if a.initial_shares and a.initial_price and old_price != a.initial_price:
40
+ a.initial_shares = get_adjusted_shares(a.initial_shares, old_price, a.initial_price)
41
+ objs.append(a)
42
+ AssetPosition.objects.bulk_update(
43
+ objs,
44
+ ["underlying_quote", "underlying_quote_price", "underlying_instrument", "initial_price", "initial_shares"],
45
+ batch_size=1000,
46
+ )
47
+
48
+
49
+ def adjust_orders(qs: Iterator[Order], underlying_quote: Instrument):
50
+ objs = []
51
+ logger.info("adjusting orders...")
52
+ for o in qs:
53
+ old_price: Decimal = o.price
54
+ o.underlying_instrument = underlying_quote
55
+ o.set_price()
56
+ if o.price and old_price != o.price and o.shares:
57
+ o.shares = get_adjusted_shares(o.shares, old_price, o.price)
58
+ objs.append(o)
59
+ Order.objects.bulk_update(objs, ["price", "shares", "underlying_instrument"], batch_size=1000)
60
+
61
+
62
+ def adjust_quote(
63
+ old_quote: Instrument,
64
+ new_quote: Instrument,
65
+ adjust_after: date | None = None,
66
+ only_portfolios: QuerySet[Portfolio] | None = None,
67
+ debug: bool = False,
68
+ ):
69
+ if old_quote.currency != new_quote.currency:
70
+ raise ValueError("cannot safely switch quotes that are not of the same currency")
71
+ assets_to_change = AssetPosition.objects.filter(underlying_quote=old_quote)
72
+ orders_to_change = Order.objects.filter(underlying_instrument=old_quote)
73
+ new_quote.import_prices()
74
+ if adjust_after:
75
+ assets_to_change = assets_to_change.filter(date__gt=adjust_after)
76
+ orders_to_change = orders_to_change.filter(value_date__gt=adjust_after)
77
+ if only_portfolios is not None:
78
+ assets_to_change = assets_to_change.filter(portfolio__in=only_portfolios)
79
+ orders_to_change = orders_to_change.filter(order_proposal__portfolio__in=only_portfolios)
80
+ if debug:
81
+ assets_to_change = tqdm(assets_to_change, total=assets_to_change.count())
82
+ orders_to_change = tqdm(orders_to_change, total=orders_to_change.count())
83
+
84
+ # gather the list of order proposal to replay (if the quote led to missing position, we want to replay it to correct automatically the issue)
85
+ latest_orders = orders_to_change.annotate(
86
+ row_number=Window(
87
+ expression=RowNumber(), partition_by=[F("order_proposal__portfolio")], order_by=F("value_date").desc()
88
+ )
89
+ ).filter(row_number=1)
90
+ order_proposals_to_replay = OrderProposal.objects.filter(
91
+ portfolio__is_manageable=True, id__in=latest_orders.values("order_proposal")
92
+ )
93
+
94
+ # Adjust assets to the new quote
95
+ adjust_assets(assets_to_change, new_quote)
96
+
97
+ # Adjust orders to the new quote
98
+ adjust_orders(orders_to_change, new_quote)
99
+
100
+ # replay latest order proposal
101
+ for op in order_proposals_to_replay:
102
+ op.replay(reapply_order_proposal=True)
103
+
104
+
105
+ @shared_task(queue="portfolio")
106
+ def adjust_quote_as_task(
107
+ old_quote_id: int, new_quote_id: int, adjust_after: date | None = None, only_portfolio_ids: list[int] | None = None
108
+ ):
109
+ old_quote = Instrument.objects.get(id=old_quote_id)
110
+ new_quote = Instrument.objects.get(id=new_quote_id)
111
+ only_portfolios = Portfolio.objects.filter(id__in=only_portfolio_ids) if only_portfolio_ids else None
112
+ adjust_quote(old_quote, new_quote, adjust_after=adjust_after, only_portfolios=only_portfolios)
@@ -0,0 +1,35 @@
1
+ from django.db.models import TextChoices
2
+
3
+ class ExecutionStatus(TextChoices):
4
+ PENDING = "PENDING", "Pending"
5
+ IN_DRAFT = "IN_DRAFT", "In Draft"
6
+ COMPLETED = "COMPLETED", "Completed"
7
+ CANCELLED = "CANCELLED", "Cancelled"
8
+ REJECTED = "REJECTED", "Rejected"
9
+ FAILED = "FAILED", "Failed"
10
+ UNKNOWN = "UNKNOWN", "Unknown"
11
+
12
+
13
+
14
+ class ExecutionInstruction(TextChoices):
15
+
16
+ MARKET_ON_CLOSE = "MARKET_ON_CLOSE", "Market On Close" # no parameter
17
+ GUARANTEED_MARKET_ON_CLOSE = "GUARANTEED_MARKET_ON_CLOSE", "Guaranteed Market On Close" # no parameter
18
+ GUARANTEED_MARKET_ON_OPEN = "GUARANTEED_MARKET_ON_OPEN", "Guaranteed Market On Open" # no parameter
19
+ GPW_MARKET_ON_CLOSE = "GPW_MARKET_ON_CLOSE", "GPW Market On Close" # no parameter
20
+ MARKET_ON_OPEN = "MARKET_ON_OPEN", "Market On Open" # no parameter
21
+ IN_LINE_WITH_VOLUME = "IN_LINE_WITH_VOLUME", "In Line With Volume" # 1 parameter "Percentage"
22
+ LIMIT_ORDER = "LIMIT_ORDER", "Limit Order" # 2 parameters "limit and cutoff"
23
+ VWAP = "VWAP", "VWAP" # 2 parameters
24
+ TWAP = "TWAP", "TWAP" # 2 paramters
25
+
26
+
27
+
28
+ class RoutingException(Exception):
29
+ def __init__(self, errors):
30
+ # messages: a list of strings
31
+ super().__init__() # You can pass a summary to the base Exception
32
+ self.errors = errors
33
+
34
+ def __str__(self):
35
+ return str(self.errors)
@@ -0,0 +1,65 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from wbportfolio.order_routing import ExecutionStatus
4
+ from wbportfolio.pms.typing import Order
5
+
6
+ class BaseCustodianAdapter(ABC):
7
+
8
+ def __init__(self, isin: str, **identifiers):
9
+ self.isin = isin
10
+
11
+ @property
12
+ def errors(self):
13
+ if not hasattr(self, '_errors'):
14
+ raise ValueError("is_valid needs to call before accessing errors")
15
+ return
16
+ @abstractmethod
17
+ def authenticate(self) -> bool:
18
+ """
19
+ Authenticate or renew tokens with the custodian API.
20
+ Raises an exception if authentication fails.
21
+ """
22
+ pass
23
+
24
+ @abstractmethod
25
+ def is_valid(self) -> bool:
26
+ """
27
+ Check whether the given isin is valid and can be rebalanced
28
+ """
29
+ pass
30
+
31
+ @abstractmethod
32
+ def serialize_orders(self, orders: list[Order]) -> list[dict[str, str]]:
33
+ pass
34
+
35
+ @abstractmethod
36
+ def deserialize_items(self, items: list[dict[str, str]]) -> list[Order]:
37
+ pass
38
+
39
+ @abstractmethod
40
+ def get_rebalance_status(self) -> tuple[ExecutionStatus, str]:
41
+ """
42
+ Return the rebalance status as a string (in the custodian format)
43
+ """
44
+ pass
45
+
46
+ @abstractmethod
47
+ def submit_rebalancing(self, items: list[dict[str, str]], as_draft: bool = True) -> tuple[list[dict[str, str]], str]:
48
+ """
49
+ Submit a rebalance order for the certificate.
50
+ """
51
+ pass
52
+
53
+ @abstractmethod
54
+ def cancel_current_rebalancing(self) -> bool:
55
+ """
56
+ Cancel an existing rebalance order identified by ISIN.
57
+ """
58
+ pass
59
+
60
+ @abstractmethod
61
+ def get_current_rebalancing(self) -> list[dict[str, str]]:
62
+ """
63
+ Fetch the current rebalance request details for a certificate.
64
+ """
65
+ pass