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,232 @@
1
+ from typing import Optional
2
+
3
+ from django.shortcuts import get_object_or_404
4
+ from django.utils.translation import gettext_lazy as _
5
+ from wbcore.contrib.color.enums import WBColor
6
+ from wbcore.enums import Unit
7
+ from wbcore.metadata.configs import display as dp
8
+ from wbcore.metadata.configs.display.instance_display.shortcuts import (
9
+ Display,
10
+ create_simple_display,
11
+ )
12
+ from wbcore.metadata.configs.display.instance_display.utils import repeat_field
13
+ from wbcore.metadata.configs.display.view_config import DisplayViewConfig
14
+
15
+ from wbportfolio.models import Order, OrderProposal
16
+
17
+ ORDER_STATUS_LEGENDS = dp.Legend(
18
+ key="has_warnings",
19
+ items=[
20
+ dp.LegendItem(icon=WBColor.YELLOW_DARK.value, label=_("Warning"), value=True),
21
+ ],
22
+ )
23
+
24
+ ORDER_STATUS_FORMATTING = dp.Formatting(
25
+ column="has_warnings",
26
+ formatting_rules=[
27
+ dp.FormattingRule(
28
+ style={"backgroundColor": WBColor.YELLOW_DARK.value},
29
+ condition=("==", True),
30
+ )
31
+ ],
32
+ )
33
+ ORDER_TYPE_FORMATTING_RULES = [
34
+ dp.FormattingRule(
35
+ style={"color": WBColor.RED_DARK.value, "fontWeight": "bold"},
36
+ condition=("==", Order.Type.SELL.name),
37
+ ),
38
+ dp.FormattingRule(
39
+ style={"color": WBColor.RED.value, "fontWeight": "bold"},
40
+ condition=("==", Order.Type.DECREASE.name),
41
+ ),
42
+ dp.FormattingRule(
43
+ style={"color": WBColor.GREEN.value, "fontWeight": "bold"},
44
+ condition=("==", Order.Type.INCREASE.name),
45
+ ),
46
+ dp.FormattingRule(
47
+ style={"color": WBColor.GREEN_DARK.value, "fontWeight": "bold"},
48
+ condition=("==", Order.Type.BUY.name),
49
+ ),
50
+ dp.FormattingRule(
51
+ style={"color": WBColor.GREY.value, "fontWeight": "bold"},
52
+ condition=("==", Order.Type.NO_CHANGE.name),
53
+ ),
54
+ ]
55
+
56
+ VALUE_FORMATTING_RULES = [
57
+ dp.FormattingRule(
58
+ style={"color": WBColor.RED_DARK.value, "fontWeight": "bold"},
59
+ condition=("<", 0),
60
+ ),
61
+ dp.FormattingRule(
62
+ style={"color": WBColor.GREEN_DARK.value, "fontWeight": "bold"},
63
+ condition=(">", 0),
64
+ ),
65
+ ]
66
+
67
+
68
+ class OrderOrderProposalDisplayConfig(DisplayViewConfig):
69
+ def get_list_display(self) -> Optional[dp.ListDisplay]:
70
+ order_proposal = get_object_or_404(OrderProposal, pk=self.view.kwargs.get("order_proposal_id", None))
71
+ fields = [
72
+ dp.Field(
73
+ label="Instrument",
74
+ open_by_default=False,
75
+ key=None,
76
+ children=[
77
+ dp.Field(key="underlying_instrument", label="Name", width=Unit.PIXEL(250)),
78
+ dp.Field(key="underlying_instrument_isin", label="ISIN", width=Unit.PIXEL(125)),
79
+ dp.Field(key="underlying_instrument_ticker", label="Ticker", width=Unit.PIXEL(100), show="open"),
80
+ dp.Field(
81
+ key="underlying_instrument_refinitiv_identifier_code",
82
+ label="RIC",
83
+ width=Unit.PIXEL(100),
84
+ show="open",
85
+ ),
86
+ dp.Field(
87
+ key="underlying_instrument_instrument_type",
88
+ label="Asset Class",
89
+ width=Unit.PIXEL(125),
90
+ show="open",
91
+ ),
92
+ dp.Field(
93
+ key="underlying_instrument_exchange", label="Exchange", width=Unit.PIXEL(125), show="open"
94
+ ),
95
+ ],
96
+ ),
97
+ dp.Field(
98
+ label="Weight",
99
+ open_by_default=False,
100
+ key=None,
101
+ children=[
102
+ dp.Field(key="effective_weight", label="Effective Weight", show="open", width=Unit.PIXEL(150)),
103
+ dp.Field(key="target_weight", label="Target Weight", show="open", width=Unit.PIXEL(150)),
104
+ dp.Field(
105
+ key="weighting",
106
+ label="Delta Weight",
107
+ formatting_rules=VALUE_FORMATTING_RULES,
108
+ width=Unit.PIXEL(150),
109
+ ),
110
+ ],
111
+ ),
112
+ ]
113
+ if not order_proposal.portfolio.only_weighting:
114
+ fields.append(
115
+ dp.Field(
116
+ label="Shares",
117
+ open_by_default=False,
118
+ key=None,
119
+ children=[
120
+ dp.Field(key="effective_shares", label="Effective Shares", show="open", width=Unit.PIXEL(150)),
121
+ dp.Field(key="target_shares", label="Target Shares", show="open", width=Unit.PIXEL(150)),
122
+ dp.Field(
123
+ key="shares",
124
+ label="Shares",
125
+ formatting_rules=VALUE_FORMATTING_RULES,
126
+ width=Unit.PIXEL(150),
127
+ ),
128
+ ],
129
+ )
130
+ )
131
+ fields.append(
132
+ dp.Field(
133
+ label="Total Value",
134
+ open_by_default=False,
135
+ key=None,
136
+ children=[
137
+ dp.Field(
138
+ key="effective_total_value_fx_portfolio",
139
+ label="Effective Total Value",
140
+ show="open",
141
+ width=Unit.PIXEL(150),
142
+ ),
143
+ dp.Field(
144
+ key="target_total_value_fx_portfolio",
145
+ label="Target Total Value",
146
+ show="open",
147
+ width=Unit.PIXEL(150),
148
+ ),
149
+ dp.Field(
150
+ key="total_value_fx_portfolio",
151
+ label="Total Value",
152
+ formatting_rules=VALUE_FORMATTING_RULES,
153
+ width=Unit.PIXEL(150),
154
+ ),
155
+ ],
156
+ )
157
+ )
158
+ fields.append(
159
+ dp.Field(
160
+ label="Information",
161
+ open_by_default=False,
162
+ key=None,
163
+ children=[
164
+ dp.Field(
165
+ key="order_type",
166
+ label="Direction",
167
+ formatting_rules=ORDER_TYPE_FORMATTING_RULES,
168
+ width=Unit.PIXEL(125),
169
+ ),
170
+ dp.Field(
171
+ key="desired_target_weight", label="Desired Target Weight", show="open", width=Unit.PIXEL(100)
172
+ ),
173
+ dp.Field(key="daily_return", label="Daily Return", show="open", width=Unit.PIXEL(100)),
174
+ dp.Field(key="currency_fx_rate", label="FX Rate", show="open", width=Unit.PIXEL(100)),
175
+ dp.Field(key="price", label="Price", show="open", width=Unit.PIXEL(100)),
176
+ dp.Field(key="order", label="Order", show="open", width=Unit.PIXEL(50)),
177
+ dp.Field(key="comment", label="Comment", show="open", width=Unit.PIXEL(250)),
178
+ ],
179
+ )
180
+ )
181
+ execution_fields = [
182
+ dp.Field(
183
+ label="Instruction",
184
+ open_by_default=False,
185
+ key=None,
186
+ children=[
187
+ dp.Field(key="execution_instruction", label="Type", width=Unit.PIXEL(125)),
188
+ dp.Field(
189
+ key="execution_instruction_parameters_repr",
190
+ label="Parameters",
191
+ width=Unit.PIXEL(125),
192
+ show="open",
193
+ ),
194
+ ],
195
+ )
196
+ ]
197
+
198
+ if order_proposal.execution_status:
199
+ execution_fields.extend(
200
+ [
201
+ dp.Field(key="execution_status", label="Status", width=Unit.PIXEL(100)),
202
+ dp.Field(key="execution_comment", label="Comment", width=Unit.PIXEL(150), show="open"),
203
+ dp.Field(
204
+ label="Trade",
205
+ open_by_default=False,
206
+ key=None,
207
+ children=[
208
+ dp.Field(key="execution_date", label="Date", width=Unit.PIXEL(100), show="open"),
209
+ dp.Field(key="execution_price", label="Price", width=Unit.PIXEL(100), show="open"),
210
+ dp.Field(key="execution_traded_shares", label="Shares", width=Unit.PIXEL(100)),
211
+ ],
212
+ ),
213
+ ]
214
+ )
215
+ fields.append(dp.Field(label="Execution", open_by_default=False, key=None, children=execution_fields))
216
+ return dp.ListDisplay(
217
+ fields=fields,
218
+ legends=[ORDER_STATUS_LEGENDS],
219
+ formatting=[ORDER_STATUS_FORMATTING],
220
+ )
221
+
222
+ def get_instance_display(self) -> Display:
223
+ order_proposal = get_object_or_404(OrderProposal, pk=self.view.kwargs.get("order_proposal_id", None))
224
+
225
+ fields = [
226
+ ["company", "security", "underlying_instrument"],
227
+ ["effective_weight", "target_weight", "weighting"],
228
+ ]
229
+ if not order_proposal.portfolio.only_weighting:
230
+ fields.append(["effective_shares", "target_shares", "shares"])
231
+ fields.append([repeat_field(3, "comment")])
232
+ return create_simple_display(fields)
@@ -0,0 +1,2 @@
1
+ from .order_proposals import OrderProposalEndpointConfig, OrderProposalPortfolioEndpointConfig
2
+ from .orders import OrderOrderProposalEndpointConfig
@@ -0,0 +1,21 @@
1
+ from django.shortcuts import get_object_or_404
2
+ from rest_framework.reverse import reverse
3
+ from wbcore.metadata.configs.endpoints import EndpointViewConfig
4
+
5
+ from wbportfolio.models import OrderProposal
6
+
7
+
8
+ class OrderProposalEndpointConfig(EndpointViewConfig):
9
+ def get_delete_endpoint(self, **kwargs):
10
+ if pk := self.view.kwargs.get("pk", None):
11
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
12
+ if order_proposal.status in [OrderProposal.Status.DRAFT, OrderProposal.Status.DENIED]:
13
+ return super().get_endpoint()
14
+ return None
15
+
16
+
17
+ class OrderProposalPortfolioEndpointConfig(OrderProposalEndpointConfig):
18
+ def get_endpoint(self, **kwargs):
19
+ return reverse(
20
+ "wbportfolio:portfolio-orderproposal-list", args=[self.view.kwargs["portfolio_id"]], request=self.request
21
+ )
@@ -0,0 +1,28 @@
1
+ from rest_framework.reverse import reverse
2
+ from wbcore.metadata.configs.endpoints import EndpointViewConfig
3
+
4
+ from wbportfolio.models import OrderProposal
5
+
6
+
7
+ class OrderOrderProposalEndpointConfig(EndpointViewConfig):
8
+ def get_endpoint(self, **kwargs):
9
+ if order_proposal_id := self.view.kwargs.get("order_proposal_id", None):
10
+ order_proposal = OrderProposal.objects.get(id=order_proposal_id)
11
+ if order_proposal.status == OrderProposal.Status.DRAFT:
12
+ return reverse(
13
+ "wbportfolio:orderproposal-order-list",
14
+ args=[self.view.kwargs["order_proposal_id"]],
15
+ request=self.request,
16
+ )
17
+ return None
18
+
19
+ def get_update_endpoint(self, **kwargs):
20
+ if order_proposal_id := self.view.kwargs.get("order_proposal_id", None):
21
+ order_proposal = OrderProposal.objects.get(id=order_proposal_id)
22
+ if order_proposal.status == OrderProposal.Status.DRAFT or order_proposal.can_be_confirmed:
23
+ return reverse(
24
+ "wbportfolio:orderproposal-order-list",
25
+ args=[self.view.kwargs["order_proposal_id"]],
26
+ request=self.request,
27
+ )
28
+ return None
File without changes
File without changes
@@ -0,0 +1,252 @@
1
+ from contextlib import suppress
2
+ from datetime import date
3
+ from decimal import Decimal
4
+
5
+ from django.contrib.messages import error, warning
6
+ from django.shortcuts import get_object_or_404
7
+ from django.utils.functional import cached_property
8
+ from pandas._libs.tslibs.offsets import BDay
9
+ from rest_framework import status
10
+ from rest_framework.decorators import action
11
+ from rest_framework.response import Response
12
+ from wbcompliance.viewsets.risk_management.mixins import RiskCheckViewSetMixin
13
+ from wbcore import serializers as wb_serializers
14
+ from wbcore import viewsets
15
+ from wbcore.metadata.configs.display.instance_display import (
16
+ Display,
17
+ create_simple_display,
18
+ )
19
+ from wbcore.permissions.permissions import InternalUserPermissionMixin
20
+ from wbcore.utils.views import CloneMixin
21
+
22
+ from wbportfolio.models import AssetPosition, Order, OrderProposal
23
+ from wbportfolio.models.orders.order_proposals import (
24
+ execute_orders_as_task,
25
+ push_model_change_as_task,
26
+ replay_as_task,
27
+ )
28
+ from wbportfolio.serializers import (
29
+ OrderProposalModelSerializer,
30
+ OrderProposalRepresentationSerializer,
31
+ ReadOnlyOrderProposalModelSerializer,
32
+ )
33
+
34
+ from ...filters.orders import OrderProposalFilterSet
35
+ from ...order_routing import ExecutionStatus, RoutingException
36
+ from ...permissions import IsPortfolioManager
37
+ from ..mixins import UserPortfolioRequestPermissionMixin
38
+ from .configs import (
39
+ OrderProposalButtonConfig,
40
+ OrderProposalDisplayConfig,
41
+ OrderProposalEndpointConfig,
42
+ OrderProposalPortfolioEndpointConfig,
43
+ )
44
+
45
+
46
+ class OrderProposalRepresentationViewSet(InternalUserPermissionMixin, viewsets.RepresentationViewSet):
47
+ IDENTIFIER = "wbportfolio:trade"
48
+ queryset = OrderProposal.objects.all()
49
+ serializer_class = OrderProposalRepresentationSerializer
50
+
51
+
52
+ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserPermissionMixin, viewsets.ModelViewSet):
53
+ IDENTIFIER = "wbportfolio:order"
54
+ ordering_fields = ("trade_date",)
55
+ ordering = ("-trade_date",)
56
+ search_fields = ("comment",)
57
+ filterset_fields = {"trade_date": ["exact", "gte", "lte"], "status": ["exact"]}
58
+
59
+ queryset = OrderProposal.objects.select_related("rebalancing_model", "portfolio")
60
+ serializer_class = OrderProposalModelSerializer
61
+ filterset_class = OrderProposalFilterSet
62
+ display_config_class = OrderProposalDisplayConfig
63
+ button_config_class = OrderProposalButtonConfig
64
+ endpoint_config_class = OrderProposalEndpointConfig
65
+
66
+ def get_serializer_class(self):
67
+ if self.new_mode or (
68
+ "pk" in self.kwargs and (obj := self.get_object()) and obj.status == OrderProposal.Status.DRAFT
69
+ ):
70
+ return OrderProposalModelSerializer
71
+ return ReadOnlyOrderProposalModelSerializer
72
+
73
+ # 2 methods to parametrize the clone button functionality
74
+ def get_clone_button_serializer_class(self, instance):
75
+ class CloneSerializer(wb_serializers.Serializer):
76
+ clone_date = wb_serializers.DateField(
77
+ default=(instance.trade_date + BDay(1)).date(), label="Trade Date"
78
+ ) # we need to change the field name from the trade proposa fields, otherwise fontend conflicts
79
+ clone_comment = wb_serializers.TextField(label="Comment")
80
+
81
+ return CloneSerializer
82
+
83
+ def get_clone_button_instance_display(self) -> Display:
84
+ return create_simple_display(
85
+ [
86
+ ["clone_comment"],
87
+ ["clone_date"],
88
+ ]
89
+ )
90
+
91
+ def add_messages(self, request, instance: OrderProposal | None = None, **kwargs):
92
+ if instance:
93
+ if instance.status == OrderProposal.Status.PENDING and instance.has_non_successful_checks:
94
+ warning(
95
+ request,
96
+ "This order proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid order proposal",
97
+ )
98
+ if (
99
+ instance.status == OrderProposal.Status.EXECUTION
100
+ and instance.orders.exclude(shares=0, weighting=0)
101
+ .filter(execution_status=Order.ExecutionStatus.FAILED)
102
+ .exists()
103
+ ):
104
+ warning(request, "Some orders failed confirmation. Check the list for further details.")
105
+ if instance.execution_status in [
106
+ ExecutionStatus.REJECTED,
107
+ ExecutionStatus.FAILED,
108
+ ExecutionStatus.UNKNOWN,
109
+ ]:
110
+ warning(
111
+ request,
112
+ f"The execution status is {ExecutionStatus[instance.execution_status].label}. Detail: {instance.execution_comment}",
113
+ )
114
+ elif instance.can_be_executed and instance.approver == request.user.profile:
115
+ warning(
116
+ request,
117
+ "As the approver of these orders, you are not authorized to execute them yourself. Please assign execution to another qualified individual.",
118
+ )
119
+
120
+ @classmethod
121
+ def _get_risk_checks_button_title(cls) -> str:
122
+ return "Pre-Trade Checks"
123
+
124
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
125
+ def reset(self, request, pk=None):
126
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
127
+ use_desired_target_weight = request.GET.get("use_desired_target_weight") == "true"
128
+ if order_proposal.status == OrderProposal.Status.DRAFT:
129
+ order_proposal.orders.all().update(weighting=0)
130
+ order_proposal.reset_orders(use_desired_target_weight=use_desired_target_weight)
131
+ return Response({"send": True})
132
+ return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
133
+
134
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
135
+ def normalize(self, request, pk=None):
136
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
137
+ total_cash_weight = Decimal(request.data.get("total_cash_weight", Decimal("0.0")))
138
+ if order_proposal.status == OrderProposal.Status.DRAFT:
139
+ order_proposal.normalize_orders(total_cash_weight)
140
+ return Response({"send": True})
141
+ return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
142
+
143
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
144
+ def replay(self, request, pk=None):
145
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
146
+ if order_proposal.portfolio.is_manageable:
147
+ replay_as_task.delay(order_proposal.id, user_id=self.request.user.id)
148
+ return Response({"send": True})
149
+ return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
150
+
151
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
152
+ def pushmodelchange(self, request, pk=None):
153
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
154
+ only_for_portfolio_ids = list(
155
+ map(lambda o: int(o), filter(lambda r: r, request.data.get("only_for_portfolio_ids", "").split(",")))
156
+ )
157
+ approve_automatically = request.data.get("approve_automatically") == "true"
158
+ if order_proposal.status == OrderProposal.Status.APPROVED and order_proposal.portfolio.is_model:
159
+ push_model_change_as_task.delay(
160
+ order_proposal.id,
161
+ request.user.id,
162
+ only_for_portfolio_ids=only_for_portfolio_ids,
163
+ approve_automatically=approve_automatically,
164
+ )
165
+ return Response({"send": True})
166
+ return Response(
167
+ {"status": "Order Proposal needs to be approved and linked to be a model portfolio"},
168
+ status=status.HTTP_400_BAD_REQUEST,
169
+ )
170
+
171
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
172
+ def execute(self, request, pk=None):
173
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
174
+ if order_proposal.can_execute(request.user):
175
+ prioritize_target_weight = request.data.get("prioritize_target_weight") == "true"
176
+ order_proposal.execution_status = ExecutionStatus.PENDING
177
+ order_proposal.execution_comment = "Waiting for custodian confirmation"
178
+ order_proposal.save()
179
+ execute_orders_as_task.delay(order_proposal.id, prioritize_target_weight=prioritize_target_weight)
180
+ return Response({"send": True})
181
+ return Response({"status": "Order Proposal is not Draft"}, status=status.HTTP_400_BAD_REQUEST)
182
+
183
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
184
+ def cancelexecution(self, request, pk=None):
185
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
186
+ if order_proposal.execution_status and order_proposal.execution_status != ExecutionStatus.CANCELLED:
187
+ try:
188
+ if not order_proposal.cancel_rebalancing():
189
+ warning(
190
+ request,
191
+ "We could not cancel the rebalancing. It is probably already executed. Please refresh status or check with an administrator.",
192
+ )
193
+ except (RoutingException, ValueError) as e:
194
+ error(request, f"Could not cancel orders proposal {order_proposal}: {str(e)}")
195
+ return Response({"send": True})
196
+ return Response(
197
+ {"status": "Order Proposal is not in an execution phase, therefore, it cannot be cancelled."},
198
+ status=status.HTTP_400_BAD_REQUEST,
199
+ )
200
+
201
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
202
+ def updateexecutionstatus(self, request, pk=None):
203
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
204
+ if order_proposal.execution_status:
205
+ try:
206
+ if not order_proposal.custodian_router:
207
+ raise RoutingException(
208
+ "There is no custodian router for this portfolio. Please check with an administrator."
209
+ )
210
+ order_proposal.refresh_execution_status()
211
+ except (RoutingException, ValueError) as e:
212
+ error(request, f"Could not update rebalancing status: {str(e)}")
213
+ return Response(
214
+ {
215
+ "send": True,
216
+ }
217
+ )
218
+ return Response(
219
+ {"status": "Order Proposal is not in an execution phase, therefore, its status cannot be fetched."},
220
+ status=status.HTTP_400_BAD_REQUEST,
221
+ )
222
+
223
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
224
+ def refreshreturn(self, request, pk=None):
225
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
226
+ order_proposal.refresh_returns()
227
+ return Response(
228
+ {"status": "Returns were refreshed with success"},
229
+ status=status.HTTP_200_OK,
230
+ )
231
+
232
+ @action(detail=True, methods=["PATCH"], permission_classes=[IsPortfolioManager])
233
+ def refreshpretradechecks(self, request, pk=None):
234
+ order_proposal = get_object_or_404(OrderProposal, pk=pk)
235
+ if order_proposal.status == OrderProposal.Status.DRAFT:
236
+ order_proposal.evaluate_pretrade_checks()
237
+ return Response(
238
+ {"status": "Evaluate pretrade checks"},
239
+ status=status.HTTP_200_OK,
240
+ )
241
+
242
+
243
+ class OrderProposalPortfolioModelViewSet(UserPortfolioRequestPermissionMixin, OrderProposalModelViewSet):
244
+ endpoint_config_class = OrderProposalPortfolioEndpointConfig
245
+
246
+ @cached_property
247
+ def default_trade_date(self) -> date | None:
248
+ with suppress(AssetPosition.DoesNotExist):
249
+ return (self.portfolio.assets.latest("date").date + BDay(1)).date()
250
+
251
+ def get_queryset(self):
252
+ return OrderProposal.objects.filter(portfolio=self.kwargs["portfolio_id"])