wbportfolio 1.55.8__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 (128) hide show
  1. wbportfolio/admin/orders/order_proposals.py +2 -0
  2. wbportfolio/admin/orders/orders.py +2 -0
  3. wbportfolio/admin/portfolio.py +11 -5
  4. wbportfolio/api_clients/ubs.py +23 -11
  5. wbportfolio/contrib/company_portfolio/configs/display.py +22 -10
  6. wbportfolio/contrib/company_portfolio/configs/previews.py +3 -3
  7. wbportfolio/contrib/company_portfolio/filters.py +10 -10
  8. wbportfolio/contrib/company_portfolio/models.py +69 -39
  9. wbportfolio/contrib/company_portfolio/scripts.py +7 -2
  10. wbportfolio/contrib/company_portfolio/serializers.py +32 -22
  11. wbportfolio/contrib/company_portfolio/tasks.py +12 -1
  12. wbportfolio/factories/assets.py +1 -1
  13. wbportfolio/factories/orders/order_proposals.py +3 -1
  14. wbportfolio/factories/orders/orders.py +8 -3
  15. wbportfolio/factories/product_groups.py +3 -3
  16. wbportfolio/factories/products.py +3 -3
  17. wbportfolio/filters/assets.py +0 -1
  18. wbportfolio/filters/orders/order_proposals.py +3 -6
  19. wbportfolio/filters/portfolios.py +18 -1
  20. wbportfolio/filters/positions.py +0 -1
  21. wbportfolio/filters/transactions/fees.py +0 -2
  22. wbportfolio/filters/transactions/trades.py +0 -1
  23. wbportfolio/import_export/backends/ubs/__init__.py +1 -0
  24. wbportfolio/import_export/backends/ubs/trade.py +48 -0
  25. wbportfolio/import_export/handlers/asset_position.py +9 -5
  26. wbportfolio/import_export/handlers/dividend.py +1 -1
  27. wbportfolio/import_export/handlers/fees.py +2 -2
  28. wbportfolio/import_export/handlers/trade.py +4 -4
  29. wbportfolio/import_export/parsers/default_mapping.py +1 -1
  30. wbportfolio/import_export/parsers/jpmorgan/customer_trade.py +2 -2
  31. wbportfolio/import_export/parsers/jpmorgan/fees.py +2 -2
  32. wbportfolio/import_export/parsers/jpmorgan/strategy.py +59 -85
  33. wbportfolio/import_export/parsers/jpmorgan/valuation.py +2 -2
  34. wbportfolio/import_export/parsers/leonteq/trade.py +2 -1
  35. wbportfolio/import_export/parsers/natixis/equity.py +22 -4
  36. wbportfolio/import_export/parsers/natixis/utils.py +13 -19
  37. wbportfolio/import_export/parsers/sg_lux/equity.py +4 -3
  38. wbportfolio/import_export/parsers/sg_lux/sylk.py +12 -11
  39. wbportfolio/import_export/parsers/sg_lux/valuation.py +4 -2
  40. wbportfolio/import_export/parsers/societe_generale/strategy.py +3 -3
  41. wbportfolio/import_export/parsers/tellco/customer_trade.py +2 -1
  42. wbportfolio/import_export/parsers/tellco/valuation.py +4 -3
  43. wbportfolio/import_export/parsers/ubs/api/trade.py +39 -0
  44. wbportfolio/import_export/parsers/ubs/equity.py +2 -1
  45. wbportfolio/import_export/parsers/ubs/valuation.py +2 -1
  46. wbportfolio/import_export/resources/trades.py +1 -1
  47. wbportfolio/import_export/utils.py +3 -1
  48. wbportfolio/metric/backends/base.py +2 -2
  49. wbportfolio/migrations/0089_orderproposal_min_weighting.py +71 -0
  50. wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py +44 -0
  51. wbportfolio/migrations/0091_remove_order_execution_confirmed_and_more.py +32 -0
  52. wbportfolio/migrations/0092_order_quantization_error_alter_orderproposal_status.py +49 -0
  53. wbportfolio/migrations/0093_remove_portfolioportfoliothroughmodel_unique_primary_and_more.py +35 -0
  54. wbportfolio/models/adjustments.py +1 -1
  55. wbportfolio/models/asset.py +7 -3
  56. wbportfolio/models/builder.py +25 -5
  57. wbportfolio/models/custodians.py +3 -3
  58. wbportfolio/models/exceptions.py +1 -1
  59. wbportfolio/models/graphs/portfolio.py +1 -1
  60. wbportfolio/models/graphs/utils.py +11 -11
  61. wbportfolio/models/mixins/liquidity_stress_test.py +1 -1
  62. wbportfolio/models/orders/order_proposals.py +620 -490
  63. wbportfolio/models/orders/orders.py +237 -75
  64. wbportfolio/models/portfolio.py +79 -18
  65. wbportfolio/models/portfolio_relationship.py +6 -0
  66. wbportfolio/models/products.py +3 -0
  67. wbportfolio/models/rebalancing.py +4 -1
  68. wbportfolio/models/roles.py +4 -10
  69. wbportfolio/models/transactions/claim.py +6 -5
  70. wbportfolio/models/transactions/dividends.py +1 -0
  71. wbportfolio/models/transactions/trades.py +4 -0
  72. wbportfolio/models/transactions/transactions.py +16 -4
  73. wbportfolio/models/utils.py +100 -1
  74. wbportfolio/order_routing/__init__.py +16 -0
  75. wbportfolio/order_routing/adapters/__init__.py +14 -6
  76. wbportfolio/order_routing/adapters/ubs.py +104 -70
  77. wbportfolio/order_routing/router.py +33 -0
  78. wbportfolio/order_routing/tests/test_router.py +110 -0
  79. wbportfolio/permissions.py +7 -0
  80. wbportfolio/pms/trading/__init__.py +0 -1
  81. wbportfolio/pms/trading/optimizer.py +61 -0
  82. wbportfolio/pms/typing.py +115 -103
  83. wbportfolio/rebalancing/models/composite.py +1 -1
  84. wbportfolio/rebalancing/models/market_capitalization_weighted.py +1 -5
  85. wbportfolio/risk_management/backends/__init__.py +1 -0
  86. wbportfolio/risk_management/backends/controversy_portfolio.py +2 -2
  87. wbportfolio/risk_management/backends/esg_aggregation_portfolio.py +64 -0
  88. wbportfolio/risk_management/backends/exposure_portfolio.py +4 -4
  89. wbportfolio/risk_management/backends/instrument_list_portfolio.py +3 -3
  90. wbportfolio/risk_management/tests/test_esg_aggregation_portfolio.py +49 -0
  91. wbportfolio/risk_management/tests/test_exposure_portfolio.py +1 -1
  92. wbportfolio/risk_management/tests/test_stop_loss_instrument.py +2 -2
  93. wbportfolio/risk_management/tests/test_stop_loss_portfolio.py +1 -1
  94. wbportfolio/serializers/orders/order_proposals.py +6 -2
  95. wbportfolio/serializers/orders/orders.py +119 -26
  96. wbportfolio/serializers/transactions/claim.py +2 -2
  97. wbportfolio/tasks.py +42 -4
  98. wbportfolio/tests/models/orders/test_order_proposals.py +345 -48
  99. wbportfolio/tests/models/test_portfolios.py +9 -9
  100. wbportfolio/tests/models/test_splits.py +1 -6
  101. wbportfolio/tests/models/test_utils.py +140 -0
  102. wbportfolio/tests/models/transactions/test_rebalancing.py +1 -1
  103. wbportfolio/tests/rebalancing/test_models.py +2 -2
  104. wbportfolio/tests/viewsets/test_products.py +1 -0
  105. wbportfolio/urls.py +1 -1
  106. wbportfolio/viewsets/charts/assets.py +8 -4
  107. wbportfolio/viewsets/configs/buttons/assets.py +1 -1
  108. wbportfolio/viewsets/configs/buttons/mixins.py +2 -2
  109. wbportfolio/viewsets/configs/buttons/portfolios.py +45 -1
  110. wbportfolio/viewsets/configs/display/reconciliations.py +4 -4
  111. wbportfolio/viewsets/esg.py +3 -5
  112. wbportfolio/viewsets/orders/configs/buttons/order_proposals.py +74 -15
  113. wbportfolio/viewsets/orders/configs/buttons/orders.py +104 -0
  114. wbportfolio/viewsets/orders/configs/displays/order_proposals.py +30 -30
  115. wbportfolio/viewsets/orders/configs/displays/orders.py +56 -17
  116. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +1 -1
  117. wbportfolio/viewsets/orders/configs/endpoints/orders.py +10 -8
  118. wbportfolio/viewsets/orders/order_proposals.py +92 -21
  119. wbportfolio/viewsets/orders/orders.py +79 -26
  120. wbportfolio/viewsets/portfolios.py +24 -0
  121. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/METADATA +1 -1
  122. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/RECORD +125 -115
  123. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/WHEEL +1 -1
  124. wbportfolio/fdm/tasks.py +0 -42
  125. wbportfolio/models/orders/routing.py +0 -54
  126. wbportfolio/pms/trading/handler.py +0 -211
  127. /wbportfolio/{fdm → order_routing/tests}/__init__.py +0 -0
  128. {wbportfolio-1.55.8.dist-info → wbportfolio-1.59.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,54 +0,0 @@
1
- # TBD: Should this stay a service or should we extend the order proposal model (fat model approach) ?
2
- from contextlib import suppress
3
- from typing import TYPE_CHECKING
4
-
5
- from django.conf import settings
6
- from django.core.exceptions import ObjectDoesNotExist
7
-
8
- from wbportfolio.order_routing import ExecutionStatus
9
-
10
- if TYPE_CHECKING:
11
- from wbportfolio.models import OrderProposal
12
-
13
-
14
- def _should_route_as_draft() -> bool:
15
- """Determine whether orders should be routed as drafts."""
16
- return getattr(settings, "DEBUG", True) or getattr(settings, "ORDER_ROUTING_AS_DRAFT", True)
17
-
18
-
19
- def _update_orders_with_confirmations(order_proposal, confirmations, rebalancing_comment):
20
- """Update all orders in the proposal based on confirmed executions."""
21
-
22
-
23
- def execute_orders(order_proposal: "OrderProposal") -> tuple[ExecutionStatus, str]:
24
- """
25
- Executes the prepared orders of an order proposal via its custodian adapter.
26
- Updates execution statuses and handles routing errors gracefully.
27
- """
28
- orders = order_proposal.prepare_orders_for_execution()
29
- as_draft = _should_route_as_draft()
30
- adapter = order_proposal.custodian_adapter
31
- order_confirmations, rebalancing_comment = adapter.submit_rebalancing(orders, as_draft=as_draft)
32
- leftover_orders = order_proposal.orders.all()
33
-
34
- for confirmed in order_confirmations:
35
- with suppress(ObjectDoesNotExist):
36
- order = leftover_orders.get(id=confirmed.id)
37
- order.execution_confirmed = True
38
- order.execution_comment = order.comment
39
- order.save()
40
- leftover_orders = leftover_orders.exclude(id=order.id)
41
-
42
- # Orders without confirmation
43
- leftover_orders.update(execution_confirmed=False, execution_comment="No confirmation received from the custodian")
44
- return ExecutionStatus.IN_DRAFT if as_draft else ExecutionStatus.PENDING, rebalancing_comment
45
-
46
-
47
- def get_execution_status(order_proposal: "OrderProposal") -> tuple[ExecutionStatus, str]:
48
- adapter = order_proposal.custodian_adapter
49
- return adapter.get_rebalance_status()
50
-
51
-
52
- def cancel_rebalancing(order_proposal: "OrderProposal") -> bool:
53
- adapter = order_proposal.custodian_adapter
54
- return adapter.cancel_current_rebalancing()
@@ -1,211 +0,0 @@
1
- import math
2
- from datetime import date
3
- from decimal import Decimal
4
-
5
- import cvxpy as cp
6
- import numpy as np
7
- from django.core.exceptions import ValidationError
8
-
9
- from wbportfolio.pms.typing import Portfolio, Trade, TradeBatch
10
-
11
-
12
- class TradeShareOptimizer:
13
- def __init__(self, batch: TradeBatch, portfolio_total_value: float):
14
- self.batch = batch
15
- self.portfolio_total_value = portfolio_total_value
16
-
17
- def optimize(self, target_cash: float = 0.99):
18
- try:
19
- return self.optimize_trade_share(target_cash)
20
- except ValueError:
21
- return self.floor_trade_share()
22
-
23
- def optimize_trade_share(self, target_cash: float = 0.01):
24
- prices_fx_portfolio = np.array([trade.price_fx_portfolio for trade in self.batch.trades])
25
- target_allocs = np.array([trade.target_weight for trade in self.batch.trades])
26
-
27
- # Decision variable: number of shares (integers)
28
- shares = cp.Variable(len(prices_fx_portfolio), integer=True)
29
-
30
- # Calculate portfolio values
31
- portfolio_values = cp.multiply(shares, prices_fx_portfolio)
32
-
33
- # Target values based on allocations
34
- target_values = self.portfolio_total_value * target_allocs
35
-
36
- # Objective: minimize absolute deviation from target values
37
- objective = cp.Minimize(cp.sum(cp.abs(portfolio_values - target_values)))
38
-
39
- # Constraints
40
- constraints = [
41
- shares >= 0, # No short selling
42
- cp.sum(portfolio_values) <= self.portfolio_total_value, # Don't exceed budget
43
- cp.sum(portfolio_values) >= (1.0 - target_cash) * self.portfolio_total_value, # Use at least 99% of budget
44
- ]
45
-
46
- # Solve
47
- problem = cp.Problem(objective, constraints)
48
- problem.solve(solver=cp.CBC)
49
-
50
- if problem != "optimal":
51
- raise ValueError(f"Optimization failed: {problem.status}")
52
-
53
- shares_result = shares.value.astype(int)
54
- return TradeBatch(
55
- [
56
- trade.normalize_target(target_shares=shares_result[index])
57
- for index, trade in enumerate(self.batch.trades)
58
- ]
59
- )
60
-
61
- def floor_trade_share(self):
62
- return TradeBatch(
63
- [trade.normalize_target(target_shares=math.floor(trade.target_shares)) for trade in self.batch.trades]
64
- )
65
-
66
-
67
- class TradingService:
68
- """
69
- This class represents the trading service. It can be instantiated either with the target portfolio and the effective portfolio or given a direct list of trade
70
- In any case, it will compute all three states
71
- """
72
-
73
- def __init__(
74
- self,
75
- trade_date: date,
76
- effective_portfolio: Portfolio | None = None,
77
- target_portfolio: Portfolio | None = None,
78
- total_target_weight: Decimal = Decimal("1.0"),
79
- ):
80
- self.trade_date = trade_date
81
- if target_portfolio is None:
82
- target_portfolio = Portfolio(positions=())
83
- if effective_portfolio is None:
84
- effective_portfolio = Portfolio(positions=())
85
- # If effective portfolio and trades batch is provided, we ensure the trade batch contains at least one trade for every position
86
- self.trades_batch = self.build_trade_batch(effective_portfolio, target_portfolio).normalize(
87
- total_target_weight
88
- )
89
- self._effective_portfolio = effective_portfolio
90
-
91
- @property
92
- def errors(self) -> list[str]:
93
- """
94
- Returned the list of errors stored during the validation process. Can only be called after is_valid
95
- """
96
- if not hasattr(self, "_errors"):
97
- msg = "You must call `.is_valid()` before accessing `.errors`."
98
- raise AssertionError(msg)
99
- return self._errors
100
-
101
- @property
102
- def validated_trades(self) -> list[Trade]:
103
- """
104
- Returned the list of validated trade stored during the validation process. Can only be called after is_valid
105
- """
106
- if not hasattr(self, "_validated_trades"):
107
- msg = "You must call `.is_valid()` before accessing `.validated_trades`."
108
- raise AssertionError(msg)
109
- return self._validated_trades
110
-
111
- def run_validation(self, validated_trades: list[Trade]):
112
- """
113
- Test the given value against all the validators on the field,
114
- and either raise a `ValidationError` or simply return.
115
- """
116
- if self._effective_portfolio:
117
- for trade in validated_trades:
118
- if (
119
- trade.previous_weight
120
- and trade.underlying_instrument not in self._effective_portfolio.positions_map
121
- ):
122
- raise ValidationError("All effective position needs to be matched with a validated trade")
123
-
124
- def build_trade_batch(
125
- self,
126
- effective_portfolio: Portfolio,
127
- target_portfolio: Portfolio,
128
- ) -> TradeBatch:
129
- """
130
- Given combination of effective portfolio and either a trades batch or a target portfolio, ensure all theres variables are set
131
-
132
- Args:
133
- effective_portfolio: The effective portfolio
134
- target_portfolio: The optional target portfolio
135
- trades_batch: The optional trades batch
136
-
137
- Returns: The normalized trades batch
138
- """
139
-
140
- instruments = effective_portfolio.positions_map.copy()
141
- instruments.update(target_portfolio.positions_map)
142
-
143
- trades: list[Trade] = []
144
- for instrument_id, pos in instruments.items():
145
- previous_weight = target_weight = 0
146
- effective_shares = target_shares = 0
147
- daily_return = 0
148
- is_cash = False
149
- if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
150
- previous_weight = effective_pos.weighting
151
- effective_shares = effective_pos.shares
152
- daily_return = effective_pos.daily_return
153
- is_cash = effective_pos.is_cash
154
- if target_pos := target_portfolio.positions_map.get(instrument_id, None):
155
- target_weight = target_pos.weighting
156
- is_cash = target_pos.is_cash
157
- if target_pos.shares is not None:
158
- target_shares = target_pos.shares
159
- trade = Trade(
160
- underlying_instrument=instrument_id,
161
- previous_weight=previous_weight,
162
- target_weight=target_weight,
163
- effective_shares=effective_shares,
164
- target_shares=target_shares,
165
- date=self.trade_date,
166
- instrument_type=pos.instrument_type,
167
- currency=pos.currency,
168
- price=Decimal(pos.price) if pos.price is not None else Decimal("0"),
169
- currency_fx_rate=Decimal(pos.currency_fx_rate),
170
- daily_return=Decimal(daily_return),
171
- portfolio_contribution=effective_portfolio.portfolio_contribution,
172
- is_cash=is_cash,
173
- )
174
- trades.append(trade)
175
- return TradeBatch(trades)
176
-
177
- def is_valid(self, ignore_error: bool = False) -> bool:
178
- """
179
- Validate the trade batch against a set of default rules. Populate the validated_trades and errors property.
180
- Ignore error by default
181
- Args:
182
- ignore_error: If true, will raise the error. False by default
183
-
184
- Returns: True if the trades batch is valid
185
- """
186
- if not hasattr(self, "_validated_trades"):
187
- self._validated_trades = []
188
- self._errors = []
189
- # Run validation for every trade. If a trade is not valid, we simply exclude it from the validated trades list
190
- for _, trade in self.trades_batch.trades_map.items():
191
- try:
192
- trade.validate()
193
- self._validated_trades.append(trade)
194
- except ValidationError as exc:
195
- self._errors.append(exc.message)
196
- try:
197
- # Check the overall validity of the trade batch. If this fail, we consider all trade invalids
198
- self.run_validation(self._validated_trades)
199
- except ValidationError as exc:
200
- self._validated_trades = []
201
- self._errors.append(exc.message)
202
-
203
- if self._errors and not ignore_error:
204
- raise ValidationError(self.errors)
205
-
206
- return not bool(self._errors)
207
-
208
- def get_optimized_trade_batch(self, portfolio_total_value: float, target_cash: float):
209
- return TradeShareOptimizer(
210
- self.trades_batch, portfolio_total_value
211
- ).floor_trade_share() # TODO switch to the other optimization when ready
File without changes