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
@@ -0,0 +1,410 @@
1
+ from contextlib import suppress
2
+ from datetime import date
3
+ from decimal import Decimal
4
+
5
+ from django.contrib import admin
6
+ from django.db import models
7
+ from django.db.models import (
8
+ Sum,
9
+ )
10
+ from django.dispatch import receiver
11
+ from ordered_model.models import OrderedModel
12
+ from pandas import Timestamp
13
+ from wbcore.contrib.currency.models import CurrencyFXRates
14
+ from wbcore.contrib.io.mixins import ImportMixin
15
+ from wbfdm.models import Instrument
16
+
17
+ from wbportfolio.import_export.handlers.orders import OrderImportHandler
18
+ from wbportfolio.models.asset import AssetPosition
19
+ from wbportfolio.models.transactions.transactions import TransactionMixin
20
+ from wbportfolio.order_routing import ExecutionInstruction
21
+ from wbportfolio.pms.typing import Position as PositionDTO
22
+
23
+
24
+ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
25
+ import_export_handler_class = OrderImportHandler
26
+
27
+ ORDER_WEIGHTING_PRECISION = (
28
+ 8 # we need to match the asset position weighting. Skfolio advices using a even smaller number (5)
29
+ )
30
+ currency = None
31
+
32
+ class Type(models.TextChoices):
33
+ REBALANCE = "REBALANCE", "Rebalance"
34
+ DECREASE = "DECREASE", "Decrease"
35
+ INCREASE = "INCREASE", "Increase"
36
+ BUY = "BUY", "Buy"
37
+ SELL = "SELL", "Sell"
38
+ NO_CHANGE = "NO_CHANGE", "No Change" # default transaction subtype if weighing is 0
39
+
40
+ class ExecutionStatus(models.TextChoices):
41
+ PENDING = "PENDING", "Pending"
42
+ CONFIRMED = "CONFIRMED", "Confirmed"
43
+ EXECUTED = "EXECUTED", "Executed"
44
+ FAILED = "FAILED", "Failed"
45
+ IGNORED = "IGNORED", "Ignored"
46
+
47
+ order_type = models.CharField(max_length=32, default=Type.BUY, choices=Type.choices, verbose_name="Trade Type")
48
+ shares = models.DecimalField(
49
+ max_digits=15,
50
+ decimal_places=4,
51
+ default=Decimal("0.0"),
52
+ help_text="The number of shares that were traded.",
53
+ verbose_name="Shares",
54
+ )
55
+ desired_target_weight = models.DecimalField(
56
+ max_digits=9,
57
+ decimal_places=ORDER_WEIGHTING_PRECISION,
58
+ default=Decimal(0),
59
+ help_text="Desired Target Weight (for compliance and audit)",
60
+ verbose_name="Desired Target Weight",
61
+ )
62
+ weighting = models.DecimalField(
63
+ max_digits=9,
64
+ decimal_places=ORDER_WEIGHTING_PRECISION,
65
+ default=Decimal(0),
66
+ help_text="The weight to be multiplied against the target",
67
+ verbose_name="Weight",
68
+ )
69
+ order_proposal = models.ForeignKey(
70
+ to="wbportfolio.OrderProposal",
71
+ related_name="orders",
72
+ on_delete=models.CASCADE,
73
+ help_text="The Order Proposal this trade is coming from",
74
+ )
75
+ daily_return = models.DecimalField(
76
+ max_digits=ORDER_WEIGHTING_PRECISION * 2
77
+ + 3, # we don't expect any drift factor to be in the order of magnitude greater than 1000
78
+ decimal_places=ORDER_WEIGHTING_PRECISION
79
+ * 2, # we need a higher precision for this factor to avoid float inprecision
80
+ default=Decimal(0.0),
81
+ verbose_name="Daily Return",
82
+ help_text="The Ex-Post daily return",
83
+ )
84
+ quantization_error = models.DecimalField(
85
+ max_digits=9,
86
+ decimal_places=ORDER_WEIGHTING_PRECISION,
87
+ default=Decimal(0),
88
+ verbose_name="Quantization Error",
89
+ )
90
+ execution_status = models.CharField(
91
+ max_length=12,
92
+ default=ExecutionStatus.PENDING.value,
93
+ choices=ExecutionStatus.choices,
94
+ verbose_name="Execution Status",
95
+ )
96
+ execution_instruction = models.CharField(
97
+ max_length=26,
98
+ choices=ExecutionInstruction.choices,
99
+ default=ExecutionInstruction.MARKET_ON_CLOSE.value,
100
+ verbose_name="Execution Instruction",
101
+ )
102
+ execution_instruction_parameters = models.JSONField(
103
+ default=dict, blank=True, verbose_name="Execution Instruction Parameters"
104
+ )
105
+ execution_comment = models.TextField(default="", blank=True, verbose_name="Execution Comment")
106
+
107
+ execution_trade = models.OneToOneField(
108
+ to="wbportfolio.Trade",
109
+ related_name="order",
110
+ on_delete=models.SET_NULL,
111
+ blank=True,
112
+ null=True,
113
+ help_text="The executed Trade",
114
+ )
115
+ order_with_respect_to = "order_proposal"
116
+
117
+ class Meta(OrderedModel.Meta):
118
+ verbose_name = "Order"
119
+ verbose_name_plural = "Orders"
120
+ indexes = [
121
+ models.Index(fields=["order_proposal"]),
122
+ models.Index(fields=["underlying_instrument", "value_date"]),
123
+ models.Index(fields=["portfolio", "underlying_instrument", "value_date"]),
124
+ models.Index(fields=["order_proposal", "underlying_instrument"]),
125
+ # models.Index(fields=["date", "underlying_instrument"]),
126
+ ]
127
+ constraints = [
128
+ models.UniqueConstraint(
129
+ fields=["order_proposal", "underlying_instrument"],
130
+ name="unique_order",
131
+ ),
132
+ ]
133
+ # notification_email_template = "portfolio/email/trade_notification.html"
134
+
135
+ @property
136
+ def product(self):
137
+ from wbportfolio.models.products import Product
138
+
139
+ try:
140
+ return Product.objects.get(id=self.underlying_instrument.id)
141
+ except Product.DoesNotExist:
142
+ return None
143
+
144
+ @property
145
+ @admin.display(description="Last Effective Date")
146
+ def _last_effective_date(self) -> date:
147
+ if hasattr(self, "last_effective_date"):
148
+ return self.last_effective_date
149
+ elif (
150
+ assets := AssetPosition.unannotated_objects.filter(
151
+ date__lt=self.value_date,
152
+ portfolio=self.portfolio,
153
+ )
154
+ ).exists():
155
+ return assets.latest("date").date
156
+
157
+ @property
158
+ @admin.display(description="Effective Weight")
159
+ def _previous_weight(self) -> Decimal:
160
+ if hasattr(self, "previous_weight"):
161
+ return self.previous_weight
162
+ return AssetPosition.unannotated_objects.filter(
163
+ underlying_quote=self.underlying_instrument,
164
+ date=self._last_effective_date,
165
+ portfolio=self.portfolio,
166
+ ).aggregate(s=Sum("weighting"))["s"] or Decimal(0)
167
+
168
+ @property
169
+ @admin.display(description="Effective Weight")
170
+ def _effective_weight(self) -> Decimal:
171
+ if hasattr(self, "effective_weight"):
172
+ return self.effective_weight
173
+ return self.order_proposal.get_orders().get(id=self.id).effective_weight
174
+
175
+ @property
176
+ @admin.display(description="Effective Shares")
177
+ def _effective_shares(self) -> Decimal:
178
+ return getattr(
179
+ self,
180
+ "effective_shares",
181
+ self.get_effective_shares(),
182
+ )
183
+
184
+ @property
185
+ @admin.display(description="Target Weight")
186
+ def _target_weight(self) -> Decimal:
187
+ return getattr(
188
+ self, "target_weight", round(self._effective_weight + self.weighting, self.ORDER_WEIGHTING_PRECISION)
189
+ )
190
+
191
+ @property
192
+ @admin.display(description="Target Shares")
193
+ def _target_shares(self) -> Decimal:
194
+ return getattr(self, "target_shares", self._effective_shares + self.shares)
195
+
196
+ def __str__(self):
197
+ ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
198
+ return f"{ticker}{self.weighting}"
199
+
200
+ def pre_save(self):
201
+ self.portfolio = self.order_proposal.portfolio
202
+ self.value_date = self.order_proposal.trade_date
203
+ self.set_currency_fx_rate()
204
+
205
+ if not self.price:
206
+ self.set_price()
207
+ if not self.portfolio.only_weighting and not self.shares:
208
+ estimated_shares = self.order_proposal.get_estimated_shares(
209
+ self.weighting, self.underlying_instrument, self.price
210
+ )
211
+ if estimated_shares:
212
+ self.shares = estimated_shares
213
+ if effective_shares := self.get_effective_shares():
214
+ if self.order_type == self.Type.SELL:
215
+ self.shares = -effective_shares
216
+ else:
217
+ self.shares = max(self.shares, -effective_shares)
218
+ super().pre_save()
219
+
220
+ def save(self, *args, **kwargs):
221
+ if self.id:
222
+ self.set_type()
223
+ self.pre_save()
224
+ if not self.underlying_instrument.is_investable_universe:
225
+ self.underlying_instrument.is_investable_universe = True
226
+ self.underlying_instrument.save()
227
+
228
+ super().save(*args, **kwargs)
229
+
230
+ @classmethod
231
+ def get_type(cls, weighting, effective_weight, target_weight) -> Type:
232
+ if weighting == 0:
233
+ return Order.Type.NO_CHANGE
234
+ elif weighting is not None:
235
+ if weighting > 0:
236
+ if abs(effective_weight) > 1e-8:
237
+ return Order.Type.INCREASE
238
+ else:
239
+ return Order.Type.BUY
240
+ elif weighting < 0:
241
+ if abs(target_weight) > 1e-8:
242
+ return Order.Type.DECREASE
243
+ else:
244
+ return Order.Type.SELL
245
+
246
+ def get_effective_shares(self) -> Decimal:
247
+ return AssetPosition.objects.filter(
248
+ underlying_quote=self.underlying_instrument,
249
+ date=self.order_proposal.last_effective_date,
250
+ portfolio=self.portfolio,
251
+ ).aggregate(s=Sum("shares"))["s"] or Decimal("0")
252
+
253
+ def set_type(self):
254
+ effective_weight = self._effective_weight
255
+ self.order_type = self.get_type(self.weighting, effective_weight, effective_weight + self.weighting)
256
+
257
+ def _get_price(self) -> tuple[Decimal, Decimal]:
258
+ daily_return = last_price = Decimal("0")
259
+
260
+ effective_date = self.order_proposal.last_effective_date
261
+ if self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent:
262
+ last_price = Decimal("1")
263
+ else:
264
+ try:
265
+ last_price = Decimal(self.portfolio.builder.prices[self.value_date][self.underlying_instrument.id])
266
+ daily_return = self.portfolio.builder.returns.loc[
267
+ Timestamp(self.value_date), self.underlying_instrument.id
268
+ ]
269
+ except KeyError:
270
+ prices, returns = Instrument.objects.filter(id=self.underlying_instrument.id).get_returns_df(
271
+ from_date=effective_date,
272
+ to_date=self.value_date,
273
+ to_currency=self.order_proposal.portfolio.currency,
274
+ use_dl=True,
275
+ )
276
+ with suppress(IndexError):
277
+ daily_return = Decimal(returns.iloc[-1, 0])
278
+ with suppress(KeyError, TypeError):
279
+ last_price = Decimal(
280
+ prices.get(self.value_date, prices[effective_date])[self.underlying_instrument.id]
281
+ )
282
+ return last_price, daily_return
283
+
284
+ def set_price(self):
285
+ last_price, daily_return = self._get_price()
286
+ self.daily_return = daily_return
287
+ self.price = last_price
288
+
289
+ def set_currency_fx_rate(self):
290
+ self.currency_fx_rate = Decimal("1")
291
+ if self.order_proposal.portfolio.currency != self.underlying_instrument.currency:
292
+ with suppress(CurrencyFXRates.DoesNotExist):
293
+ self.currency_fx_rate = self.underlying_instrument.currency.convert(
294
+ self.value_date, self.portfolio.currency, exact_lookup=True
295
+ )
296
+
297
+ def set_weighting(self, weighting: Decimal, portfolio_value: Decimal):
298
+ self.weighting = weighting
299
+ price_fx_portfolio = self.price * self.currency_fx_rate
300
+ if price_fx_portfolio and portfolio_value:
301
+ total_value = self.weighting * portfolio_value
302
+ self.shares = total_value / price_fx_portfolio
303
+ else:
304
+ self.shares = Decimal("0")
305
+
306
+ def set_shares(self, shares: Decimal, portfolio_value: Decimal):
307
+ if portfolio_value:
308
+ price_fx_portfolio = self.price * self.currency_fx_rate
309
+ self.shares = shares
310
+ total_value = shares * price_fx_portfolio
311
+ self.weighting = total_value / portfolio_value
312
+ else:
313
+ self.weighting = self.shares = Decimal("0")
314
+
315
+ def set_total_value_fx_portfolio(self, total_value_fx_portfolio: Decimal, portfolio_value: Decimal):
316
+ price_fx_portfolio = self.price * self.currency_fx_rate
317
+ if price_fx_portfolio and portfolio_value:
318
+ self.shares = total_value_fx_portfolio / price_fx_portfolio
319
+ self.weighting = total_value_fx_portfolio / portfolio_value
320
+ else:
321
+ self.weighting = self.shares = Decimal("0")
322
+
323
+ def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
324
+ warnings = []
325
+ # if shares is defined and the underlying instrument defines a round lot size different than 1 and exchange allows its application, we round the share accordingly
326
+ if self._target_weight:
327
+ if self.order_proposal and not self.portfolio.only_weighting:
328
+ shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
329
+ if shares != self.shares:
330
+ warnings.append(
331
+ f"{self.underlying_instrument.computed_str} has a round lot size of {self.underlying_instrument.round_lot_size}: shares were rounded from {self.shares} to {shares}"
332
+ )
333
+ shares = round(shares) # ensure fractional shares are converted into integer
334
+ # we need to recompute the delta weight has we changed the number of shares
335
+ if shares != self.shares:
336
+ self.set_shares(shares, portfolio_total_asset_value)
337
+ if abs(self.weighting) < self.order_proposal.min_weighting:
338
+ warnings.append(
339
+ f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
340
+ )
341
+ self.set_weighting(Decimal("0"), portfolio_total_asset_value)
342
+ if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
343
+ warnings.append(
344
+ f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
345
+ )
346
+ self.set_weighting(Decimal("0"), portfolio_total_asset_value)
347
+ if not self.price:
348
+ warnings.append(f"No price for {self.underlying_instrument.computed_str}")
349
+ if (
350
+ not self.underlying_instrument.is_cash
351
+ and not self.underlying_instrument.is_cash_equivalent
352
+ and self._target_weight < -1e-8
353
+ ): # any value below -1e8 will be considered zero
354
+ warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
355
+ self.desired_target_weight = self._target_weight
356
+ return warnings
357
+
358
+ def to_dto(self) -> PositionDTO:
359
+ return self.create_dto(
360
+ self.underlying_instrument,
361
+ self._target_weight,
362
+ self.price,
363
+ self.value_date,
364
+ shares=self._target_shares,
365
+ currency_fx_rate=self.currency_fx_rate,
366
+ daily_return=self.daily_return,
367
+ )
368
+
369
+ @classmethod
370
+ def create_dto(
371
+ cls,
372
+ instrument: Instrument,
373
+ weighting: Decimal,
374
+ price: Decimal,
375
+ trade_date: date,
376
+ shares: Decimal | None = None,
377
+ **extra_param,
378
+ ) -> PositionDTO:
379
+ price_data = {}
380
+
381
+ return PositionDTO(
382
+ underlying_instrument=instrument.id,
383
+ instrument_type=instrument.security_instrument_type.id,
384
+ weighting=weighting,
385
+ shares=shares,
386
+ currency=instrument.currency.id,
387
+ date=trade_date,
388
+ asset_valuation_date=trade_date,
389
+ is_cash=instrument.is_cash or instrument.is_cash_equivalent,
390
+ price=price,
391
+ exchange=instrument.exchange.id if instrument.exchange else None,
392
+ country=instrument.country.id if instrument.country else None,
393
+ **price_data,
394
+ **extra_param,
395
+ )
396
+
397
+
398
+ @receiver(models.signals.post_save, sender="wbportfolio.Trade")
399
+ def link_trade_to_order(sender, instance, created, raw, **kwargs):
400
+ """Gets or create the fees for a given price and updates them if necessary"""
401
+ if not raw and created and not instance.underlying_instrument.is_cash and not instance.is_customer_trade:
402
+ with suppress(Order.DoesNotExist):
403
+ order = Order.objects.get(
404
+ portfolio=instance.portfolio,
405
+ underlying_instrument=instance.underlying_instrument,
406
+ value_date=instance.book_date,
407
+ )
408
+ order.execution_trade = instance
409
+ order.execution_status = Order.ExecutionStatus.EXECUTED
410
+ order.save()