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
@@ -10,3 +10,5 @@ from .registers import RegisterModelAdmin
10
10
  from .roles import PortfolioRoleAdmin
11
11
  from .transactions import DividendAdmin, FeesAdmin, TradeAdmin
12
12
  from .reconciliations import AccountReconciliationAdmin
13
+ from .orders import OrderProposalAdmin
14
+ from .rebalancing import RebalancingModelAdmin, RebalancerAdmin
@@ -0,0 +1,2 @@
1
+ from .orders import OrderTabularInline
2
+ from .order_proposals import OrderProposalAdmin
@@ -0,0 +1,14 @@
1
+ from django.contrib import admin
2
+
3
+ from wbportfolio.models import OrderProposal
4
+
5
+ from .orders import OrderTabularInline
6
+
7
+
8
+ @admin.register(OrderProposal)
9
+ class OrderProposalAdmin(admin.ModelAdmin):
10
+ search_fields = ["portfolio__name", "comment"]
11
+
12
+ list_display = ("portfolio", "rebalancing_model", "trade_date", "status")
13
+ autocomplete_fields = ["portfolio", "rebalancing_model"]
14
+ inlines = [OrderTabularInline]
@@ -0,0 +1,30 @@
1
+ from django.contrib import admin
2
+
3
+ from wbportfolio.models.orders import Order
4
+
5
+
6
+ class OrderTabularInline(admin.TabularInline):
7
+ model = Order
8
+ fk_name = "order_proposal"
9
+
10
+ readonly_fields = [
11
+ "_effective_weight",
12
+ "_target_weight",
13
+ "_effective_shares",
14
+ "_target_shares",
15
+ "total_value",
16
+ "total_value_gross",
17
+ "total_value_fx_portfolio",
18
+ "total_value_gross_fx_portfolio",
19
+ "created",
20
+ "updated",
21
+ ]
22
+
23
+ fields = [
24
+ "underlying_instrument",
25
+ "_effective_weight",
26
+ "_target_weight",
27
+ "weighting",
28
+ "shares",
29
+ "daily_return",
30
+ ]
@@ -20,7 +20,7 @@ class RebalancerAdmin(admin.ModelAdmin):
20
20
  "rebalancing_model",
21
21
  "portfolio",
22
22
  "parameters",
23
- "approve_trade_proposal_automatically",
23
+ "approve_order_proposal_automatically",
24
24
  "activation_date",
25
25
  "frequency",
26
26
  )
@@ -2,4 +2,3 @@ from .claim import ClaimModelAdmin
2
2
  from .dividends import DividendAdmin
3
3
  from .fees import FeesAdmin
4
4
  from .trades import TradeAdmin
5
- from .rebalancing import RebalancingModelAdmin, RebalancerAdmin
@@ -1,6 +1,6 @@
1
1
  from django.contrib import admin
2
2
 
3
- from wbportfolio.models import Trade, TradeProposal
3
+ from wbportfolio.models import Trade
4
4
 
5
5
 
6
6
  @admin.register(Trade)
@@ -8,7 +8,6 @@ class TradeAdmin(admin.ModelAdmin):
8
8
  search_fields = ["portfolio__name", "underlying_instrument__computed_str", "bank"]
9
9
  list_filter = ("portfolio", "pending")
10
10
  list_display = (
11
- "status",
12
11
  "transaction_subtype",
13
12
  "transaction_date",
14
13
  "underlying_instrument",
@@ -23,10 +22,6 @@ class TradeAdmin(admin.ModelAdmin):
23
22
  )
24
23
 
25
24
  readonly_fields = [
26
- "_effective_weight",
27
- "_target_weight",
28
- "_effective_shares",
29
- "_target_shares",
30
25
  "total_value",
31
26
  "total_value_gross",
32
27
  "total_value_fx_portfolio",
@@ -39,7 +34,7 @@ class TradeAdmin(admin.ModelAdmin):
39
34
  "Transaction Information",
40
35
  {
41
36
  "fields": (
42
- ("transaction_subtype", "status"),
37
+ ("transaction_subtype",),
43
38
  ("pending", "marked_for_deletion", "exclude_from_history"),
44
39
  (
45
40
  "portfolio",
@@ -50,8 +45,6 @@ class TradeAdmin(admin.ModelAdmin):
50
45
  ("price", "currency", "currency_fx_rate"),
51
46
  ("total_value", "total_value_gross"),
52
47
  ("total_value_fx_portfolio", "total_value_gross_fx_portfolio"),
53
- ("_effective_weight", "_target_weight", "weighting"),
54
- ("_effective_shares", "_target_shares", "shares"),
55
48
  ("register", "custodian", "bank", "external_id", "external_id_alternative"),
56
49
  ("created", "updated"),
57
50
  ("comment",),
@@ -62,11 +55,3 @@ class TradeAdmin(admin.ModelAdmin):
62
55
  autocomplete_fields = ["portfolio", "underlying_instrument", "currency", "register", "custodian"]
63
56
  ordering = ("-transaction_date",)
64
57
  raw_id_fields = ["import_source", "underlying_instrument", "portfolio", "register", "custodian"]
65
-
66
-
67
- @admin.register(TradeProposal)
68
- class TradeProposalAdmin(admin.ModelAdmin):
69
- search_fields = ["portfolio__name", "comment"]
70
-
71
- list_display = ("portfolio", "rebalancing_model", "trade_date", "status")
72
- autocomplete_fields = ["portfolio", "rebalancing_model"]
@@ -60,7 +60,7 @@ from wbportfolio.factories import (
60
60
  ProductGroupRepresentantFactory,
61
61
  ProductPortfolioRoleFactory,
62
62
  TradeFactory,
63
- TradeProposalFactory,
63
+ OrderProposalFactory,
64
64
  WhiteLabelProductFactory,
65
65
  )
66
66
 
@@ -95,7 +95,7 @@ register(ModelPortfolioFactory)
95
95
  register(ModelPortfolioWithBaseProductFactory, "model_portfolio_with_base_product")
96
96
  register(TradeFactory)
97
97
  register(CustomerTradeFactory)
98
- register(TradeProposalFactory)
98
+ register(OrderProposalFactory)
99
99
  register(DividendTransactionsFactory)
100
100
  register(FeesFactory)
101
101
  register(WhiteLabelProductFactory, "white_label_product")
@@ -21,6 +21,7 @@ from .product_groups import ProductGroupFactory, ProductGroupRepresentantFactory
21
21
  from .products import IndexProductFactory, ProductFactory, WhiteLabelProductFactory, ModelPortfolioWithBaseProductFactory
22
22
  from .reconciliations import AccountReconciliationFactory, AccountReconciliationLineFactory
23
23
  from .roles import ManagerPortfolioRoleFactory, ProductPortfolioRoleFactory
24
- from .trades import CustomerTradeFactory, TradeFactory, TradeProposalFactory
24
+ from .trades import CustomerTradeFactory, TradeFactory
25
+ from .orders import OrderProposalFactory, OrderFactory
25
26
  from .indexes import IndexFactory
26
27
  from .rebalancing import (RebalancingModelFactory, RebalancerFactory)
@@ -0,0 +1,2 @@
1
+ from .order_proposals import OrderProposalFactory
2
+ from .orders import OrderFactory
@@ -0,0 +1,17 @@
1
+ import factory
2
+ from faker import Faker
3
+ from pandas._libs.tslibs.offsets import BDay
4
+
5
+ from wbportfolio.models import OrderProposal
6
+
7
+ fake = Faker()
8
+
9
+
10
+ class OrderProposalFactory(factory.django.DjangoModelFactory):
11
+ class Meta:
12
+ model = OrderProposal
13
+
14
+ trade_date = factory.LazyAttribute(lambda o: (fake.date_object() + BDay(1)).date())
15
+ comment = factory.Faker("paragraph")
16
+ portfolio = factory.SubFactory("wbportfolio.factories.PortfolioFactory")
17
+ creator = factory.SubFactory("wbcore.contrib.directory.factories.PersonFactory")
@@ -0,0 +1,21 @@
1
+ import random
2
+ from decimal import Decimal
3
+
4
+ import factory
5
+ from faker import Faker
6
+
7
+ from wbportfolio.models import Order
8
+
9
+ fake = Faker()
10
+
11
+
12
+ class OrderFactory(factory.django.DjangoModelFactory):
13
+ class Meta:
14
+ model = Order
15
+
16
+ order_proposal = factory.SubFactory("wbportfolio.factories.OrderProposalFactory")
17
+ currency_fx_rate = Decimal(1.0)
18
+ fees = Decimal(0.0)
19
+ underlying_instrument = factory.SubFactory("wbfdm.factories.InstrumentFactory")
20
+ shares = factory.Faker("pydecimal", min_value=10, max_value=1000, right_digits=4)
21
+ price = factory.LazyAttribute(lambda o: random.randint(10, 10000))
@@ -18,6 +18,6 @@ class RebalancerFactory(factory.django.DjangoModelFactory):
18
18
  portfolio = factory.SubFactory("wbportfolio.factories.portfolios.PortfolioFactory")
19
19
  rebalancing_model = factory.SubFactory(RebalancingModelFactory)
20
20
  parameters = dict()
21
- approve_trade_proposal_automatically = False
21
+ approve_order_proposal_automatically = False
22
22
  frequency = "RRULE:FREQ=MONTHLY;"
23
23
  activation_date = None
@@ -4,9 +4,8 @@ from decimal import Decimal
4
4
 
5
5
  import factory
6
6
  from faker import Faker
7
- from pandas._libs.tslibs.offsets import BDay
8
7
 
9
- from wbportfolio.models import Trade, TradeProposal
8
+ from wbportfolio.models import Trade
10
9
 
11
10
  fake = Faker()
12
11
 
@@ -26,17 +25,7 @@ class TradeFactory(factory.django.DjangoModelFactory):
26
25
  marked_for_deletion = False
27
26
  shares = factory.Faker("pydecimal", min_value=10, max_value=1000, right_digits=4)
28
27
  price = factory.LazyAttribute(lambda o: random.randint(10, 10000))
29
- # trade_proposal = factory.SubFactory("wbportfolio.factories.TradeProposalFactory")
30
-
31
-
32
- class TradeProposalFactory(factory.django.DjangoModelFactory):
33
- class Meta:
34
- model = TradeProposal
35
-
36
- trade_date = factory.LazyAttribute(lambda o: (fake.date_object() + BDay(1)).date())
37
- comment = factory.Faker("paragraph")
38
- portfolio = factory.SubFactory("wbportfolio.factories.PortfolioFactory")
39
- creator = factory.SubFactory("wbcore.contrib.directory.factories.PersonFactory")
28
+ # order_proposal = factory.SubFactory("wbportfolio.factories.OrderProposalFactory")
40
29
 
41
30
 
42
31
  class CustomerTradeFactory(TradeFactory):
@@ -0,0 +1 @@
1
+ from .orders import OrderFilterSet
@@ -0,0 +1,11 @@
1
+ from wbcore import filters as wb_filters
2
+
3
+ from wbportfolio.models import Order
4
+
5
+
6
+ class OrderFilterSet(wb_filters.FilterSet):
7
+ has_warnings = wb_filters.BooleanFilter()
8
+
9
+ class Meta:
10
+ model = Order
11
+ fields = {"underlying_instrument": ["exact"], "order_type": ["exact"]}
@@ -25,7 +25,7 @@ class TradeImportHandler(ImportExportHandler):
25
25
  self.instrument_handler = InstrumentImportHandler(self.import_source)
26
26
  self.register_handler = RegisterImportHandler(self.import_source)
27
27
  self.currency_handler = CurrencyImportHandler(self.import_source)
28
- self.trade_proposals = set()
28
+ self.order_proposals = set()
29
29
 
30
30
  def _data_changed(self, _object, change_data: Dict[str, Any], initial_data: Dict[str, Any], **kwargs):
31
31
  if (new_register := change_data.get("register")) and (current_register := _object.register):
@@ -35,20 +35,20 @@ class TradeImportHandler(ImportExportHandler):
35
35
  return super()._data_changed(_object, change_data, initial_data, **kwargs)
36
36
 
37
37
  def _deserialize(self, data: Dict[str, Any]):
38
- from wbportfolio.models import Product, TradeProposal
38
+ from wbportfolio.models import OrderProposal, Product
39
39
 
40
40
  if underlying_instrument := data.get("underlying_instrument", None):
41
41
  data["underlying_instrument"] = self.instrument_handler.process_object(
42
42
  underlying_instrument, only_security=False, read_only=True
43
43
  )[0]
44
44
 
45
- if trade_proposal_id := data.pop("trade_proposal_id", None):
46
- trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
47
- self.trade_proposals.add(trade_proposal)
48
- data["value_date"] = trade_proposal.last_effective_date
49
- data["transaction_date"] = trade_proposal.trade_date
50
- data["trade_proposal"] = trade_proposal
51
- data["portfolio"] = trade_proposal.portfolio
45
+ if order_proposal_id := data.pop("order_proposal_id", None):
46
+ order_proposal = OrderProposal.objects.get(id=order_proposal_id)
47
+ self.order_proposals.add(order_proposal)
48
+ data["value_date"] = order_proposal.last_effective_date
49
+ data["transaction_date"] = order_proposal.trade_date
50
+ data["order_proposal"] = order_proposal
51
+ data["portfolio"] = order_proposal.portfolio
52
52
  data["status"] = "DRAFT"
53
53
  else:
54
54
  if external_id_alternative := data.get("external_id_alternative", None):
@@ -160,13 +160,13 @@ class TradeImportHandler(ImportExportHandler):
160
160
  self.import_source.log += "\nNo trade was successfully matched."
161
161
 
162
162
  def _get_history(self, history: Dict[str, Any]) -> models.QuerySet:
163
- from wbportfolio.models.transactions.trade_proposals import TradeProposal
163
+ from wbportfolio.models.orders.order_proposals import OrderProposal
164
164
 
165
- if trade_proposal_id := history.get("trade_proposal_id"):
166
- # if a trade proposal is provided, we delete the existing history first as otherwise, it would mess with the target weight computation
167
- trade_proposal = TradeProposal.objects.get(id=trade_proposal_id)
168
- trade_proposal.trades.all().delete()
169
- trade_proposal.reset_trades()
165
+ if order_proposal_id := history.get("order_proposal_id"):
166
+ # if a order proposal is provided, we delete the existing history first as otherwise, it would mess with the target weight computation
167
+ order_proposal = OrderProposal.objects.get(id=order_proposal_id)
168
+ order_proposal.trades.all().delete()
169
+ order_proposal.reset_orders()
170
170
  trades = self.model.objects.none()
171
171
  else:
172
172
  trades = self.model.objects.filter(
@@ -201,7 +201,7 @@ class TradeImportHandler(ImportExportHandler):
201
201
  modified_objs: list[models.Model],
202
202
  unmodified_objs: list[models.Model],
203
203
  ):
204
- from wbportfolio.models.transactions.trade_proposals import replay_as_task
204
+ from wbportfolio.models.orders.order_proposals import replay_as_task
205
205
 
206
206
  for instrument in set(
207
207
  map(lambda x: x.underlying_instrument, filter(lambda t: t.is_customer_trade, created_objs + modified_objs))
@@ -209,9 +209,9 @@ class TradeImportHandler(ImportExportHandler):
209
209
  if instrument.instrument_type.key == "product":
210
210
  update_outstanding_shares_as_task.delay(instrument.id)
211
211
 
212
- # if the trade import relates to a trade proposal, we reset the TP after the import to ensure it contains the deleted positions (often forgotten by user)
213
- for changed_trade_proposal in self.trade_proposals:
214
- replay_as_task.delay(changed_trade_proposal.id)
212
+ # if the trade import relates to a order proposal, we reset the TP after the import to ensure it contains the deleted positions (often forgotten by user)
213
+ for changed_order_proposal in self.order_proposals:
214
+ replay_as_task.delay(changed_order_proposal.id)
215
215
 
216
216
  def _post_processing_updated_object(self, _object):
217
217
  if _object.marked_for_deletion:
@@ -231,7 +231,7 @@ class TradeImportHandler(ImportExportHandler):
231
231
  self.import_source.log += (
232
232
  f"{trade.transaction_date:%d.%m.%Y}: {trade.shares} {trade.bank} ==> Marked for deletion"
233
233
  )
234
- if trade.trade_proposal:
234
+ if trade.order_proposal:
235
235
  trade.delete()
236
236
  else:
237
237
  trade.marked_for_deletion = True
@@ -10,9 +10,9 @@ from wbportfolio.models import Trade
10
10
  fake = Faker()
11
11
 
12
12
 
13
- class TradeProposalTradeResource(FilterModelResource):
13
+ class OrderProposalTradeResource(FilterModelResource):
14
14
  """
15
- Trade Resource class to use to import trade from the trade proposal
15
+ Trade Resource class to use to import trade from the order proposal
16
16
  """
17
17
 
18
18
  DUMMY_FIELD_MAP = {
@@ -0,0 +1,93 @@
1
+ # Generated by Django 5.0.14 on 2025-07-17 14:53
2
+
3
+ import django.db.models.deletion
4
+ import django_fsm
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('currency', '0001_initial'),
12
+ ('io', '0008_importsource_resource_kwargs'),
13
+ ('wbfdm', '0031_exchange_apply_round_lot_size_and_more'),
14
+ ('directory', '0013_alter_clientmanagerrelationship_options'),
15
+ ('wbportfolio', '0081_alter_trade_drift_factor'),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.RenameModel(
20
+ old_name='TradeProposal',
21
+ new_name='OrderProposal',
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='orderproposal',
25
+ name='creator',
26
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
27
+ related_name='order_proposals', to='directory.person', verbose_name='Owner'),
28
+ ),
29
+ migrations.AlterField(
30
+ model_name='orderproposal',
31
+ name='portfolio',
32
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='order_proposals',
33
+ to='wbportfolio.portfolio', verbose_name='Portfolio'),
34
+ ),
35
+ migrations.AlterField(
36
+ model_name='orderproposal',
37
+ name='rebalancing_model',
38
+ field=models.ForeignKey(blank=True, help_text='Rebalancing Model that generates the target portfolio',
39
+ null=True, on_delete=django.db.models.deletion.SET_NULL,
40
+ related_name='order_proposals', to='wbportfolio.rebalancingmodel',
41
+ verbose_name='Rebalancing Model'),
42
+ ),
43
+ migrations.AlterModelOptions(
44
+ name='orderproposal',
45
+ options={'verbose_name': 'Order Proposal', 'verbose_name_plural': 'Order Proposals'},
46
+ ),
47
+ migrations.RemoveConstraint(
48
+ model_name='orderproposal',
49
+ name='unique_trade_proposal',
50
+ ),
51
+ migrations.RemoveConstraint(
52
+ model_name='trade',
53
+ name='unique_manual_trade',
54
+ ),
55
+ migrations.RenameField(
56
+ model_name='rebalancer',
57
+ old_name='approve_trade_proposal_automatically',
58
+ new_name='approve_order_proposal_automatically',
59
+ ),
60
+ migrations.RenameField(
61
+ model_name='trade',
62
+ old_name='trade_proposal',
63
+ new_name='order_proposal',
64
+ ),
65
+ migrations.AlterField(
66
+ model_name='portfolio',
67
+ name='is_manageable',
68
+ field=models.BooleanField(default=False,
69
+ help_text='True if the portfolio can be manually modified (e.g. Order Proposal be submitted or total weight recomputed)'),
70
+ ),
71
+ migrations.AddConstraint(
72
+ model_name='orderproposal',
73
+ constraint=models.UniqueConstraint(fields=('portfolio', 'trade_date'), name='unique_order_proposal'),
74
+ ),
75
+ migrations.AddConstraint(
76
+ model_name='trade',
77
+ constraint=models.UniqueConstraint(condition=models.Q(('order_proposal__isnull', False)),
78
+ fields=('portfolio', 'transaction_date', 'underlying_instrument'),
79
+ name='unique_manual_trade'),
80
+ ),
81
+ migrations.AlterField(
82
+ model_name='rebalancer',
83
+ name='approve_order_proposal_automatically',
84
+ field=models.BooleanField(default=False, verbose_name='Apply Order Proposal Automatically'),
85
+ ),
86
+ migrations.AlterField(
87
+ model_name='trade',
88
+ name='order_proposal',
89
+ field=models.ForeignKey(blank=True, help_text='The Order Proposal this trade is coming from', null=True,
90
+ on_delete=django.db.models.deletion.CASCADE, related_name='trades',
91
+ to='wbportfolio.orderproposal'),
92
+ ),
93
+ ]
@@ -0,0 +1,181 @@
1
+ # Generated by Django 5.0.14 on 2025-07-18 06:37
2
+
3
+ import django.db.models.deletion
4
+ import django.db.models.expressions
5
+ import django_fsm
6
+ from decimal import Decimal
7
+ from django.db import migrations, models
8
+ from tqdm import tqdm
9
+ def migrate_order(apps, schema_editor):
10
+ Order = apps.get_model("wbportfolio", "Order")
11
+ Trade = apps.get_model("wbportfolio", "Trade")
12
+ objs = []
13
+ qs = Trade.objects.filter(order_proposal__isnull=False).select_related("order_proposal")
14
+ for order in tqdm(qs, total=qs.count()):
15
+ objs.append(Order(
16
+ order=order.order,
17
+ value_date=order.order_proposal.trade_date,
18
+ currency_fx_rate=order.currency_fx_rate,
19
+ price=order.price,
20
+ price_gross=order.price_gross,
21
+ fees=order.fees,
22
+ comment=order.comment,
23
+ created=order.created,
24
+ updated=order.updated,
25
+ order_type=order.transaction_subtype,
26
+ status=order.status,
27
+ shares=order.shares,
28
+ weighting=order.weighting,
29
+ drift_factor=order.drift_factor,
30
+ import_source=order.import_source,
31
+ portfolio=order.portfolio,
32
+ underlying_instrument=order.underlying_instrument,
33
+ order_proposal=order.order_proposal,
34
+ ))
35
+ Order.objects.bulk_create(objs)
36
+ qs.delete()
37
+
38
+ class Migration(migrations.Migration):
39
+
40
+ dependencies = [
41
+ ('io', '0008_importsource_resource_kwargs'),
42
+ ('wbfdm', '0031_exchange_apply_round_lot_size_and_more'),
43
+ ('wbportfolio', '0082_remove_tradeproposal_creator_and_more'),
44
+ ]
45
+
46
+ operations = [
47
+ migrations.CreateModel(
48
+ name='Order',
49
+ fields=[
50
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
51
+ ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')),
52
+ ('value_date', models.DateField(help_text='The date that this transaction was valuated/paid.', verbose_name='Value Date')),
53
+ ('currency_fx_rate', models.DecimalField(decimal_places=8, default=Decimal('1'), max_digits=14, verbose_name='FOREX rate')),
54
+ ('price', models.DecimalField(decimal_places=4, help_text='The price per share.', max_digits=16, verbose_name='Price')),
55
+ ('price_gross', models.DecimalField(decimal_places=4, help_text='The gross price per share.', max_digits=16, verbose_name='Gross Price')),
56
+ ('fees', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('price_gross'), '-', models.F('price')), output_field=models.DecimalField(decimal_places=4, max_digits=20))),
57
+ ('total_value_gross', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('price_gross'), '*', models.F('shares')), output_field=models.DecimalField(decimal_places=4, max_digits=20))),
58
+ ('total_value', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('price'), '*', models.F('shares')), output_field=models.DecimalField(decimal_places=4, max_digits=20))),
59
+ ('total_value_fx_portfolio', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price')), '*', models.F('shares')), output_field=models.DecimalField(decimal_places=4, max_digits=20))),
60
+ ('total_value_gross_fx_portfolio', models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(django.db.models.expressions.CombinedExpression(models.F('currency_fx_rate'), '*', models.F('price_gross')), '*', models.F('shares')), output_field=models.DecimalField(decimal_places=4, max_digits=20))),
61
+ ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
62
+ ('created', models.DateTimeField(auto_now_add=True)),
63
+ ('updated', models.DateTimeField(auto_now=True)),
64
+ ('order_type', models.CharField(choices=[('REBALANCE', 'Rebalance'), ('DECREASE', 'Decrease'), ('INCREASE', 'Increase'), ('BUY', 'Buy'), ('SELL', 'Sell'), ('NO_CHANGE', 'No Change')], default='BUY', max_length=32, verbose_name='Trade Type')),
65
+ ('status', django_fsm.FSMField(choices=[('DRAFT', 'Draft'), ('SUBMIT', 'Submit'), ('EXECUTED', 'Executed'), ('CONFIRMED', 'Confirmed'), ('FAILED', 'Failed')], default='CONFIRMED', max_length=50, verbose_name='Status')),
66
+ ('shares', models.DecimalField(decimal_places=4, default=Decimal('0.0'), help_text='The number of shares that were traded.', max_digits=15, verbose_name='Shares')),
67
+ ('weighting', models.DecimalField(decimal_places=8, default=Decimal('0'), help_text='The weight to be multiplied against the target', max_digits=9, verbose_name='Weight')),
68
+ ('drift_factor', models.DecimalField(decimal_places=16, default=Decimal('1'), help_text='Drift factor to be applied to the previous portfolio weight to get the actual effective weight including daily return', max_digits=19, verbose_name='Drift Factor')),
69
+ ],
70
+ options={
71
+ 'verbose_name': 'Order',
72
+ 'verbose_name_plural': 'Orders',
73
+ 'ordering': ('order',),
74
+ 'abstract': False,
75
+ },
76
+ ),
77
+ migrations.AddField(
78
+ model_name='order',
79
+ name='order_proposal',
80
+ field=models.ForeignKey(help_text='The Order Proposal this trade is coming from',
81
+ on_delete=django.db.models.deletion.CASCADE, related_name='orders',
82
+ to='wbportfolio.orderproposal'),
83
+ ),
84
+ migrations.AddField(
85
+ model_name='order',
86
+ name='import_source',
87
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
88
+ to='io.importsource'),
89
+ ),
90
+ migrations.AddField(
91
+ model_name='order',
92
+ name='portfolio',
93
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss',
94
+ to='wbportfolio.portfolio', verbose_name='Portfolio'),
95
+ ),
96
+ migrations.AddField(
97
+ model_name='order',
98
+ name='underlying_instrument',
99
+ field=models.ForeignKey(help_text='The instrument that is this transaction.',
100
+ limit_choices_to=models.Q(('children__isnull', True)),
101
+ on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss',
102
+ to='wbfdm.instrument', verbose_name='Underlying Instrument'),
103
+ ),
104
+ migrations.RunSQL(
105
+ sql="SET CONSTRAINTS ALL IMMEDIATE;",
106
+ ),
107
+ migrations.RunPython(migrate_order),
108
+ migrations.RunSQL(
109
+ sql="SET CONSTRAINTS ALL DEFERRED;",
110
+ ),
111
+ migrations.AlterModelOptions(
112
+ name='trade',
113
+ options={'verbose_name': 'Trade', 'verbose_name_plural': 'Trades'},
114
+ ),
115
+ migrations.RemoveConstraint(
116
+ model_name='trade',
117
+ name='unique_manual_trade',
118
+ ),
119
+ migrations.RemoveField(
120
+ model_name='trade',
121
+ name='drift_factor',
122
+ ),
123
+ migrations.RemoveField(
124
+ model_name='trade',
125
+ name='order',
126
+ ),
127
+ migrations.RemoveField(
128
+ model_name='trade',
129
+ name='order_proposal',
130
+ ),
131
+ migrations.AddIndex(
132
+ model_name='order',
133
+ index=models.Index(fields=['underlying_instrument', 'value_date'], name='wbportfolio_underly_051048_idx'),
134
+ ),
135
+ migrations.AddIndex(
136
+ model_name='order',
137
+ index=models.Index(fields=['portfolio', 'underlying_instrument', 'value_date'], name='wbportfolio_portfol_26d2a7_idx'),
138
+ ),
139
+ migrations.AddIndex(
140
+ model_name='order',
141
+ index=models.Index(fields=['order_proposal', 'underlying_instrument'], name='wbportfolio_order_p_637d28_idx'),
142
+ ),
143
+ migrations.AddConstraint(
144
+ model_name='order',
145
+ constraint=models.UniqueConstraint(fields=('order_proposal', 'underlying_instrument'), name='unique_order'),
146
+ ),
147
+ migrations.AlterField(
148
+ model_name='orderproposal',
149
+ name='comment',
150
+ field=models.TextField(blank=True, default='', verbose_name='Order Comment'),
151
+ ),
152
+ migrations.RemoveField(
153
+ model_name='order',
154
+ name='drift_factor',
155
+ ),
156
+ migrations.AddField(
157
+ model_name='order',
158
+ name='daily_return',
159
+ field=models.DecimalField(decimal_places=16, default=Decimal('0'), help_text='The Ex-Post daily return',
160
+ max_digits=19, verbose_name='Daily Return'),
161
+ ),
162
+ migrations.AddIndex(
163
+ model_name='order',
164
+ index=models.Index(fields=['order_proposal'], name='wbportfolio_order_p_630213_idx'),
165
+ ),
166
+ migrations.RemoveField(
167
+ model_name='order',
168
+ name='status',
169
+ ),
170
+ migrations.RemoveField(
171
+ model_name='trade',
172
+ name='status',
173
+ ),
174
+ migrations.AlterField(
175
+ model_name='orderproposal',
176
+ name='status',
177
+ field=django_fsm.FSMField(
178
+ choices=[('DRAFT', 'Draft'), ('SUBMIT', 'Pending'), ('APPROVED', 'Approved'), ('DENIED', 'Denied'),
179
+ ('FAILED', 'Failed')], default='DRAFT', max_length=50, verbose_name='Status'),
180
+ ),
181
+ ]
@@ -17,5 +17,7 @@ from .portfolio_cash_flow import DailyPortfolioCashFlow
17
17
  from .portfolio_swing_pricings import PortfolioSwingPricing
18
18
  from .registers import Register
19
19
  from .transactions import *
20
+ from .orders import *
20
21
  from .reconciliations import AccountReconciliation, AccountReconciliationLine
21
22
  from .signals import *
23
+ from .rebalancing import RebalancingModel, Rebalancer
@@ -0,0 +1,2 @@
1
+ from .orders import Order
2
+ from .order_proposals import OrderProposal