wbportfolio 1.54.13__py2.py3-none-any.whl → 1.54.15__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 (86) hide show
  1. wbportfolio/admin/__init__.py +2 -0
  2. wbportfolio/admin/orders/__init__.py +2 -0
  3. wbportfolio/admin/orders/order_proposals.py +14 -0
  4. wbportfolio/admin/orders/orders.py +30 -0
  5. wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
  6. wbportfolio/admin/transactions/__init__.py +0 -1
  7. wbportfolio/admin/transactions/trades.py +2 -17
  8. wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
  9. wbportfolio/factories/__init__.py +2 -1
  10. wbportfolio/factories/orders/__init__.py +2 -0
  11. wbportfolio/factories/orders/order_proposals.py +17 -0
  12. wbportfolio/factories/orders/orders.py +21 -0
  13. wbportfolio/factories/rebalancing.py +1 -1
  14. wbportfolio/factories/trades.py +2 -13
  15. wbportfolio/filters/orders/__init__.py +1 -0
  16. wbportfolio/filters/orders/orders.py +11 -0
  17. wbportfolio/import_export/handlers/trade.py +20 -20
  18. wbportfolio/import_export/resources/trades.py +2 -2
  19. wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
  20. wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
  21. wbportfolio/models/__init__.py +2 -0
  22. wbportfolio/models/orders/__init__.py +2 -0
  23. wbportfolio/models/{transactions/trade_proposals.py → orders/order_proposals.py} +289 -245
  24. wbportfolio/models/orders/orders.py +243 -0
  25. wbportfolio/models/portfolio.py +17 -20
  26. wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +18 -18
  27. wbportfolio/models/transactions/__init__.py +0 -2
  28. wbportfolio/models/transactions/trades.py +10 -450
  29. wbportfolio/pms/analytics/portfolio.py +10 -6
  30. wbportfolio/pms/analytics/utils.py +9 -0
  31. wbportfolio/pms/trading/handler.py +6 -4
  32. wbportfolio/pms/typing.py +18 -7
  33. wbportfolio/rebalancing/decorators.py +1 -1
  34. wbportfolio/rebalancing/models/composite.py +3 -7
  35. wbportfolio/rebalancing/models/market_capitalization_weighted.py +3 -1
  36. wbportfolio/serializers/__init__.py +1 -0
  37. wbportfolio/serializers/orders/__init__.py +2 -0
  38. wbportfolio/serializers/{transactions/trade_proposals.py → orders/order_proposals.py} +23 -15
  39. wbportfolio/serializers/orders/orders.py +187 -0
  40. wbportfolio/serializers/portfolios.py +7 -7
  41. wbportfolio/serializers/rebalancing.py +1 -1
  42. wbportfolio/serializers/transactions/__init__.py +1 -5
  43. wbportfolio/serializers/transactions/trades.py +1 -182
  44. wbportfolio/tests/conftest.py +4 -2
  45. wbportfolio/tests/models/orders/__init__.py +0 -0
  46. wbportfolio/tests/models/{transactions/test_trade_proposals.py → orders/test_order_proposals.py} +218 -246
  47. wbportfolio/tests/models/test_portfolios.py +11 -10
  48. wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
  49. wbportfolio/tests/models/transactions/test_trades.py +0 -20
  50. wbportfolio/tests/rebalancing/test_models.py +24 -28
  51. wbportfolio/tests/signals.py +10 -10
  52. wbportfolio/tests/tests.py +1 -1
  53. wbportfolio/urls.py +7 -7
  54. wbportfolio/viewsets/__init__.py +2 -0
  55. wbportfolio/viewsets/configs/buttons/__init__.py +2 -3
  56. wbportfolio/viewsets/configs/buttons/trades.py +0 -8
  57. wbportfolio/viewsets/configs/display/__init__.py +0 -2
  58. wbportfolio/viewsets/configs/display/portfolios.py +5 -5
  59. wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
  60. wbportfolio/viewsets/configs/display/trades.py +1 -225
  61. wbportfolio/viewsets/configs/endpoints/__init__.py +0 -3
  62. wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
  63. wbportfolio/viewsets/orders/__init__.py +6 -0
  64. wbportfolio/viewsets/orders/configs/__init__.py +4 -0
  65. wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
  66. wbportfolio/viewsets/{configs/buttons/trade_proposals.py → orders/configs/buttons/order_proposals.py} +22 -21
  67. wbportfolio/viewsets/orders/configs/buttons/orders.py +9 -0
  68. wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
  69. wbportfolio/viewsets/{configs/display/trade_proposals.py → orders/configs/displays/order_proposals.py} +21 -21
  70. wbportfolio/viewsets/orders/configs/displays/orders.py +180 -0
  71. wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
  72. wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
  73. wbportfolio/viewsets/orders/configs/endpoints/orders.py +26 -0
  74. wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
  75. wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
  76. wbportfolio/viewsets/{transactions/trade_proposals.py → orders/order_proposals.py} +46 -45
  77. wbportfolio/viewsets/orders/orders.py +219 -0
  78. wbportfolio/viewsets/portfolios.py +12 -12
  79. wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
  80. wbportfolio/viewsets/transactions/__init__.py +1 -7
  81. wbportfolio/viewsets/transactions/trades.py +1 -199
  82. {wbportfolio-1.54.13.dist-info → wbportfolio-1.54.15.dist-info}/METADATA +1 -1
  83. {wbportfolio-1.54.13.dist-info → wbportfolio-1.54.15.dist-info}/RECORD +85 -58
  84. wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
  85. {wbportfolio-1.54.13.dist-info → wbportfolio-1.54.15.dist-info}/WHEEL +0 -0
  86. {wbportfolio-1.54.13.dist-info → wbportfolio-1.54.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,243 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+
4
+ from django.contrib import admin
5
+ from django.db import models
6
+ from django.db.models import (
7
+ Sum,
8
+ )
9
+ from ordered_model.models import OrderedModel
10
+ from wbcore.contrib.io.mixins import ImportMixin
11
+
12
+ from wbportfolio.import_export.handlers.trade import TradeImportHandler
13
+ from wbportfolio.models.asset import AssetPosition
14
+ from wbportfolio.models.transactions.transactions import TransactionMixin
15
+
16
+
17
+ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
18
+ import_export_handler_class = TradeImportHandler
19
+
20
+ ORDER_WEIGHTING_PRECISION = (
21
+ 8 # we need to match the asset position weighting. Skfolio advices using a even smaller number (5)
22
+ )
23
+ currency = None
24
+
25
+ class Type(models.TextChoices):
26
+ REBALANCE = "REBALANCE", "Rebalance"
27
+ DECREASE = "DECREASE", "Decrease"
28
+ INCREASE = "INCREASE", "Increase"
29
+ BUY = "BUY", "Buy"
30
+ SELL = "SELL", "Sell"
31
+ NO_CHANGE = "NO_CHANGE", "No Change" # default transaction subtype if weighing is 0
32
+
33
+ order_type = models.CharField(max_length=32, default=Type.BUY, choices=Type.choices, verbose_name="Trade Type")
34
+ shares = models.DecimalField(
35
+ max_digits=15,
36
+ decimal_places=4,
37
+ default=Decimal("0.0"),
38
+ help_text="The number of shares that were traded.",
39
+ verbose_name="Shares",
40
+ )
41
+
42
+ weighting = models.DecimalField(
43
+ max_digits=9,
44
+ decimal_places=ORDER_WEIGHTING_PRECISION,
45
+ default=Decimal(0),
46
+ help_text="The weight to be multiplied against the target",
47
+ verbose_name="Weight",
48
+ )
49
+ order_proposal = models.ForeignKey(
50
+ to="wbportfolio.OrderProposal",
51
+ related_name="orders",
52
+ on_delete=models.CASCADE,
53
+ help_text="The Order Proposal this trade is coming from",
54
+ )
55
+ daily_return = models.DecimalField(
56
+ max_digits=ORDER_WEIGHTING_PRECISION * 2
57
+ + 3, # we don't expect any drift factor to be in the order of magnitude greater than 1000
58
+ decimal_places=ORDER_WEIGHTING_PRECISION
59
+ * 2, # we need a higher precision for this factor to avoid float inprecision
60
+ default=Decimal(0.0),
61
+ verbose_name="Daily Return",
62
+ help_text="The Ex-Post daily return",
63
+ )
64
+
65
+ def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
66
+ warnings = []
67
+ # 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
68
+ if self.order_proposal and not self.portfolio.only_weighting:
69
+ shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
70
+ if shares != self.shares:
71
+ warnings.append(
72
+ 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}"
73
+ )
74
+ shares = round(shares) # ensure fractional shares are converted into integer
75
+ # we need to recompute the delta weight has we changed the number of shares
76
+ if shares != self.shares:
77
+ self.shares = shares
78
+ if portfolio_total_asset_value:
79
+ self.weighting = self.shares * self.price * self.currency_fx_rate / portfolio_total_asset_value
80
+
81
+ if not self.price:
82
+ warnings.append(f"No price for {self.underlying_instrument.computed_str}")
83
+ if abs(self._target_weight) < 1e-8:
84
+ warnings.append(f"Negative target weight for {self.underlying_instrument.computed_str}")
85
+ return warnings
86
+
87
+ @property
88
+ def product(self):
89
+ from wbportfolio.models.products import Product
90
+
91
+ try:
92
+ return Product.objects.get(id=self.underlying_instrument.id)
93
+ except Product.DoesNotExist:
94
+ return None
95
+
96
+ @property
97
+ @admin.display(description="Last Effective Date")
98
+ def _last_effective_date(self) -> date:
99
+ if hasattr(self, "last_effective_date"):
100
+ return self.last_effective_date
101
+ elif (
102
+ assets := AssetPosition.unannotated_objects.filter(
103
+ date__lt=self.value_date,
104
+ portfolio=self.portfolio,
105
+ )
106
+ ).exists():
107
+ return assets.latest("date").date
108
+
109
+ @property
110
+ @admin.display(description="Effective Weight")
111
+ def _previous_weight(self) -> Decimal:
112
+ if hasattr(self, "previous_weight"):
113
+ return self.previous_weight
114
+ return AssetPosition.unannotated_objects.filter(
115
+ underlying_quote=self.underlying_instrument,
116
+ date=self._last_effective_date,
117
+ portfolio=self.portfolio,
118
+ ).aggregate(s=Sum("weighting"))["s"] or Decimal(0)
119
+
120
+ @property
121
+ @admin.display(description="Effective Weight")
122
+ def _effective_weight(self) -> Decimal:
123
+ if hasattr(self, "effective_weight"):
124
+ return self.effective_weight
125
+ return self.order_proposal.get_orders().get(id=self.id).effective_weight
126
+
127
+ @property
128
+ @admin.display(description="Effective Shares")
129
+ def _effective_shares(self) -> Decimal:
130
+ return getattr(
131
+ self,
132
+ "effective_shares",
133
+ AssetPosition.objects.filter(
134
+ underlying_quote=self.underlying_instrument,
135
+ date=self.value_date,
136
+ portfolio=self.portfolio,
137
+ ).aggregate(s=Sum("shares"))["s"]
138
+ or Decimal(0),
139
+ )
140
+
141
+ @property
142
+ @admin.display(description="Target Weight")
143
+ def _target_weight(self) -> Decimal:
144
+ return getattr(
145
+ self, "target_weight", round(self._effective_weight + self.weighting, self.ORDER_WEIGHTING_PRECISION)
146
+ )
147
+
148
+ @_target_weight.setter
149
+ def _target_weight(self, target_weight):
150
+ self.weighting = Decimal(target_weight) - self._effective_weight
151
+ self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
152
+
153
+ @property
154
+ @admin.display(description="Target Shares")
155
+ def _target_shares(self) -> Decimal:
156
+ return getattr(self, "target_shares", self._effective_shares + self.shares)
157
+
158
+ order_with_respect_to = "order_proposal"
159
+
160
+ class Meta(OrderedModel.Meta):
161
+ verbose_name = "Order"
162
+ verbose_name_plural = "Orders"
163
+ indexes = [
164
+ models.Index(fields=["order_proposal"]),
165
+ models.Index(fields=["underlying_instrument", "value_date"]),
166
+ models.Index(fields=["portfolio", "underlying_instrument", "value_date"]),
167
+ models.Index(fields=["order_proposal", "underlying_instrument"]),
168
+ # models.Index(fields=["date", "underlying_instrument"]),
169
+ ]
170
+ constraints = [
171
+ models.UniqueConstraint(
172
+ fields=["order_proposal", "underlying_instrument"],
173
+ name="unique_order",
174
+ ),
175
+ ]
176
+ # notification_email_template = "portfolio/email/trade_notification.html"
177
+
178
+ def save(self, *args, **kwargs):
179
+ self.portfolio = self.order_proposal.portfolio
180
+ self.value_date = self.order_proposal.trade_date
181
+
182
+ if abs(self.weighting) < 10e-6:
183
+ self.weighting = Decimal("0")
184
+ if not self.underlying_instrument.is_investable_universe:
185
+ self.underlying_instrument.is_investable_universe = True
186
+ self.underlying_instrument.save()
187
+
188
+ if not self.price:
189
+ # we try to get the price if not provided directly from the underlying instrument
190
+ self.price = self.get_price()
191
+ if self.portfolio.only_weighting:
192
+ estimated_shares = self.order_proposal.get_estimated_shares(
193
+ self.weighting, self.underlying_instrument, self.price
194
+ )
195
+ if estimated_shares:
196
+ self.shares = estimated_shares
197
+ if self.id:
198
+ self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
199
+ super().save(*args, **kwargs)
200
+
201
+ @classmethod
202
+ def get_type(cls, weighting, effective_weight, target_weight) -> Type:
203
+ if weighting == 0:
204
+ return Order.Type.NO_CHANGE
205
+ elif weighting is not None:
206
+ if weighting > 0:
207
+ if abs(effective_weight) > 1e-8:
208
+ return Order.Type.INCREASE
209
+ else:
210
+ return Order.Type.BUY
211
+ elif weighting < 0:
212
+ if abs(target_weight) > 1e-8:
213
+ return Order.Type.DECREASE
214
+ else:
215
+ return Order.Type.SELL
216
+
217
+ def get_asset(self) -> AssetPosition:
218
+ asset = AssetPosition(
219
+ underlying_quote=self.underlying_instrument,
220
+ portfolio_created=None,
221
+ portfolio=self.portfolio,
222
+ date=self.value_date,
223
+ initial_currency_fx_rate=self.currency_fx_rate,
224
+ weighting=self._target_weight,
225
+ initial_price=self.price,
226
+ initial_shares=None,
227
+ asset_valuation_date=self.value_date,
228
+ currency=self.currency,
229
+ is_estimated=False,
230
+ )
231
+ asset.set_weighting(self._target_weight)
232
+ asset.pre_save()
233
+ return asset
234
+
235
+ def get_price(self) -> Decimal:
236
+ try:
237
+ return self.underlying_instrument.get_price(self.value_date)
238
+ except ValueError:
239
+ return Decimal("0")
240
+
241
+ def __str__(self):
242
+ ticker = f"{self.underlying_instrument.ticker}:" if self.underlying_instrument.ticker else ""
243
+ return f"{ticker}{self.weighting}"
@@ -46,7 +46,7 @@ from .exceptions import InvalidAnalyticPortfolio
46
46
 
47
47
  logger = logging.getLogger("pms")
48
48
  if TYPE_CHECKING:
49
- from wbportfolio.models.transactions.trade_proposals import TradeProposal
49
+ from wbportfolio.models.orders.order_proposals import OrderProposal
50
50
 
51
51
  MARKET_HOLIDAY_MAX_DURATION = 15
52
52
 
@@ -97,20 +97,21 @@ def get_returns(
97
97
  if df.empty:
98
98
  raise InvalidAnalyticPortfolio()
99
99
  df = (
100
- df.pivot_table(index="valuation_date", columns="instrument_id", values=["fx_rate", "close"])
100
+ df.pivot_table(index="valuation_date", columns="instrument_id", values=["fx_rate", "close"], dropna=False)
101
101
  .astype(float)
102
102
  .sort_index()
103
103
  )
104
+
104
105
  ts = pd.bdate_range(df.index.min(), df.index.max(), freq="B")
105
106
  df = df.reindex(ts)
106
107
  df = df.ffill()
107
108
  df.index = pd.to_datetime(df.index)
108
109
  prices_df = df["close"]
110
+
109
111
  if "fx_rate" in df.columns:
110
112
  fx_rate_df = df["fx_rate"].fillna(1.0)
111
113
  else:
112
114
  fx_rate_df = pd.DataFrame(np.ones(prices_df.shape), index=prices_df.index, columns=prices_df.columns)
113
-
114
115
  returns = prices_to_returns(fx_rate_df * prices_df, drop_inceptions_nan=False, fill_nan=True)
115
116
  return {ts.date(): row for ts, row in prices_df.to_dict("index").items()}, returns.replace(
116
117
  [np.inf, -np.inf, np.nan], 0
@@ -273,7 +274,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
273
274
 
274
275
  is_manageable = models.BooleanField(
275
276
  default=False,
276
- help_text="True if the portfolio can be manually modified (e.g. Trade proposal be submitted or total weight recomputed)",
277
+ help_text="True if the portfolio can be manually modified (e.g. Order Proposal be submitted or total weight recomputed)",
277
278
  )
278
279
  is_tracked = models.BooleanField(
279
280
  default=True,
@@ -355,17 +356,13 @@ class Portfolio(DeleteToDisableMixin, WBModel):
355
356
  "returns the dto representation of this portfolio at the specified date"
356
357
  assets = self.assets.filter(date=val_date, **extra_kwargs)
357
358
  try:
358
- drifted_weights = self.get_analytic_portfolio(val_date, use_dl=True).get_next_weights()
359
+ last_returns, _ = self.get_analytic_portfolio(val_date, use_dl=True).get_contributions()
360
+ last_returns = last_returns.to_dict()
359
361
  except InvalidAnalyticPortfolio:
360
- drifted_weights = {}
362
+ last_returns = {}
361
363
  positions = []
362
364
  for asset in assets:
363
- drift_factor = (
364
- Decimal(drifted_weights.get(asset.underlying_quote.id, asset.weighting)) / asset.weighting
365
- if asset.weighting
366
- else Decimal(1.0)
367
- )
368
- positions.append(asset._build_dto(drift_factor=drift_factor))
365
+ positions.append(asset._build_dto(daily_return=last_returns.get(asset.underlying_quote.id, Decimal("0"))))
369
366
 
370
367
  return PortfolioDTO(positions)
371
368
 
@@ -447,7 +444,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
447
444
  create_notification_type(
448
445
  "wbportfolio.portfolio.replay_done",
449
446
  "Portfolio Replay finished",
450
- "Sends a notification when a the requested trade proposal replay is done",
447
+ "Sends a notification when a the requested order proposal replay is done",
451
448
  True,
452
449
  True,
453
450
  True,
@@ -784,7 +781,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
784
781
 
785
782
  def evaluate_rebalancing(self, val_date: date):
786
783
  if hasattr(self, "automatic_rebalancer"):
787
- # if the portfolio has an automatic rebalancer and the next business day is suitable with the rebalancer, we create a trade proposal automatically
784
+ # if the portfolio has an automatic rebalancer and the next business day is suitable with the rebalancer, we create a order proposal automatically
788
785
  next_business_date = (val_date + BDay(1)).date()
789
786
  if self.automatic_rebalancer.is_valid(val_date): # we evaluate the rebalancer in t0 and t+1
790
787
  logger.info(f"Evaluate Rebalancing for {self} at {val_date}")
@@ -818,7 +815,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
818
815
 
819
816
  def drift_weights(
820
817
  self, start_date: date, end_date: date, stop_at_rebalancing: bool = False
821
- ) -> tuple[AssetPositionIterator, "TradeProposal"]:
818
+ ) -> tuple[AssetPositionIterator, "OrderProposal"]:
822
819
  logger.info(f"drift weights for {self} from {start_date:%Y-%m-%d} to {end_date:%Y-%m-%d}")
823
820
  rebalancer = getattr(self, "automatic_rebalancer", None)
824
821
  # Get initial weights
@@ -841,7 +838,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
841
838
  # Get raw prices to speed up asset position creation
842
839
  # Instantiate the position iterator with the initial weights
843
840
  positions = AssetPositionIterator(self, prices=prices)
844
- last_trade_proposal = None
841
+ last_order_proposal = None
845
842
  for to_date_ts in pd.date_range(start_date + timedelta(days=1), end_date, freq="B"):
846
843
  to_date = to_date_ts.date()
847
844
  to_is_active = self.is_active_at_date(to_date)
@@ -860,17 +857,17 @@ class Portfolio(DeleteToDisableMixin, WBModel):
860
857
  date=to_date,
861
858
  underlying_instrument=i,
862
859
  weighting=Decimal(w),
863
- drift_factor=Decimal(drifted_weights.get(i, w) / float(w)) if w else Decimal(1.0),
860
+ daily_return=Decimal(last_returns.iloc[-1][i]),
864
861
  )
865
862
  for i, w in weights.items()
866
863
  ]
867
864
  )
868
- last_trade_proposal = rebalancer.evaluate_rebalancing(to_date, effective_portfolio=effective_portfolio)
865
+ last_order_proposal = rebalancer.evaluate_rebalancing(to_date, effective_portfolio=effective_portfolio)
869
866
  if stop_at_rebalancing:
870
867
  break
871
868
  next_weights = {
872
869
  trade.underlying_instrument: float(trade._target_weight)
873
- for trade in last_trade_proposal.trades.all().annotate_base_info()
870
+ for trade in last_order_proposal.get_orders()
874
871
  }
875
872
  positions.add((to_date, next_weights), is_estimated=False)
876
873
  else:
@@ -883,7 +880,7 @@ class Portfolio(DeleteToDisableMixin, WBModel):
883
880
  ) # if we have no return or portfolio is not active anymore, we return an emptied portfolio
884
881
  break
885
882
  weights = next_weights
886
- return positions, last_trade_proposal
883
+ return positions, last_order_proposal
887
884
 
888
885
  def propagate_or_update_assets(self, from_date: date, to_date: date):
889
886
  """
@@ -14,8 +14,8 @@ from wbcore.utils.importlib import import_from_dotted_path
14
14
  from wbcore.utils.models import ComplexToStringMixin
15
15
  from wbcore.utils.rrules import convert_rrulestr_to_dict, humanize_rrule
16
16
 
17
+ from wbportfolio.models.orders.order_proposals import OrderProposal
17
18
  from wbportfolio.models.portfolio import Portfolio
18
- from wbportfolio.models.transactions.trade_proposals import TradeProposal
19
19
  from wbportfolio.pms.typing import Portfolio as PortfolioDTO
20
20
  from wbportfolio.rebalancing.base import AbstractRebalancingModel
21
21
 
@@ -78,8 +78,8 @@ class Rebalancer(ComplexToStringMixin, models.Model):
78
78
  RebalancingModel, on_delete=models.PROTECT, related_name="rebalancers", verbose_name="Rebalancing Model"
79
79
  )
80
80
  parameters = models.JSONField(default=dict, verbose_name="Parameters", blank=True)
81
- approve_trade_proposal_automatically = models.BooleanField(
82
- default=False, verbose_name="Apply Trade Proposal Automatically"
81
+ approve_order_proposal_automatically = models.BooleanField(
82
+ default=False, verbose_name="Apply Order Proposal Automatically"
83
83
  )
84
84
  activation_date = models.DateField(verbose_name="Activation Date")
85
85
  frequency = models.CharField(
@@ -99,19 +99,19 @@ class Rebalancer(ComplexToStringMixin, models.Model):
99
99
 
100
100
  def _get_next_valid_date(self, valid_date: date) -> date:
101
101
  pivot_date = valid_date
102
- while TradeProposal.objects.filter(
103
- portfolio=self.portfolio, status=TradeProposal.Status.FAILED, trade_date=pivot_date
102
+ while OrderProposal.objects.filter(
103
+ portfolio=self.portfolio, status=OrderProposal.Status.FAILED, trade_date=pivot_date
104
104
  ).exists():
105
105
  pivot_date = (pivot_date + BDay(1)).date()
106
106
  return pivot_date
107
107
 
108
108
  def is_valid(self, trade_date: date) -> bool:
109
- if TradeProposal.objects.filter(
109
+ if OrderProposal.objects.filter(
110
110
  portfolio=self.portfolio,
111
- status=TradeProposal.Status.APPROVED,
111
+ status=OrderProposal.Status.APPROVED,
112
112
  trade_date=trade_date,
113
113
  rebalancing_model__isnull=True,
114
- ).exists(): # if a already approved trade proposal exists, we do not allow a re-evaluatioon of the rebalancing (only possible if "replayed")
114
+ ).exists(): # if a already approved order proposal exists, we do not allow a re-evaluatioon of the rebalancing (only possible if "replayed")
115
115
  return False
116
116
  for initial_valid_datetime in self.get_rrule(trade_date):
117
117
  initial_valid_date = initial_valid_datetime.date()
@@ -123,7 +123,7 @@ class Rebalancer(ComplexToStringMixin, models.Model):
123
123
  return False
124
124
 
125
125
  def evaluate_rebalancing(self, trade_date: date, effective_portfolio=None):
126
- trade_proposal, _ = TradeProposal.objects.get_or_create(
126
+ order_proposal, _ = OrderProposal.objects.get_or_create(
127
127
  trade_date=trade_date,
128
128
  portfolio=self.portfolio,
129
129
  defaults={
@@ -132,29 +132,29 @@ class Rebalancer(ComplexToStringMixin, models.Model):
132
132
  },
133
133
  )
134
134
 
135
- if trade_proposal.rebalancing_model == self.rebalancing_model:
135
+ if order_proposal.rebalancing_model == self.rebalancing_model:
136
136
  try:
137
137
  logger.info(
138
138
  f"Getting target portfolio ({self.portfolio}) for rebalancing model {self.rebalancing_model} for trade date {trade_date:%Y-%m-%d}"
139
139
  )
140
140
  target_portfolio = self.rebalancing_model.get_target_portfolio(
141
141
  self.portfolio,
142
- trade_proposal.trade_date,
143
- trade_proposal.value_date,
142
+ order_proposal.trade_date,
143
+ order_proposal.value_date,
144
144
  effective_portfolio=effective_portfolio,
145
145
  **self.parameters,
146
146
  )
147
- trade_proposal.approve_workflow(
148
- approve_automatically=self.approve_trade_proposal_automatically,
147
+ order_proposal.approve_workflow(
148
+ approve_automatically=self.approve_order_proposal_automatically,
149
149
  target_portfolio=target_portfolio,
150
150
  effective_portfolio=effective_portfolio,
151
151
  )
152
152
  except ValidationError as e:
153
153
  logger.warning(f"Validation error while approving the orders: {e}")
154
- # If we encountered a validation error, we set the trade proposal as failed
155
- trade_proposal.status = TradeProposal.Status.FAILED
156
- trade_proposal.save()
157
- return trade_proposal
154
+ # If we encountered a validation error, we set the order proposal as failed
155
+ order_proposal.status = OrderProposal.Status.FAILED
156
+ order_proposal.save()
157
+ return order_proposal
158
158
 
159
159
  @property
160
160
  def rrule(self):
@@ -1,6 +1,4 @@
1
1
  from .claim import Claim
2
2
  from .dividends import DividendTransaction
3
3
  from .fees import FeeCalculation, Fees
4
- from .trade_proposals import TradeProposal
5
- from .rebalancing import RebalancingModel, Rebalancer
6
4
  from .trades import Trade