wbportfolio 1.54.14__py2.py3-none-any.whl → 1.54.16__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.
- wbportfolio/admin/__init__.py +2 -0
- wbportfolio/admin/orders/__init__.py +2 -0
- wbportfolio/admin/orders/order_proposals.py +14 -0
- wbportfolio/admin/orders/orders.py +30 -0
- wbportfolio/admin/{transactions/rebalancing.py → rebalancing.py} +1 -1
- wbportfolio/admin/transactions/__init__.py +0 -1
- wbportfolio/admin/transactions/trades.py +2 -17
- wbportfolio/contrib/company_portfolio/tests/conftest.py +2 -2
- wbportfolio/factories/__init__.py +2 -1
- wbportfolio/factories/orders/__init__.py +2 -0
- wbportfolio/factories/orders/order_proposals.py +17 -0
- wbportfolio/factories/orders/orders.py +21 -0
- wbportfolio/factories/rebalancing.py +1 -1
- wbportfolio/factories/trades.py +2 -13
- wbportfolio/filters/orders/__init__.py +1 -0
- wbportfolio/filters/orders/orders.py +11 -0
- wbportfolio/import_export/handlers/trade.py +20 -20
- wbportfolio/import_export/resources/trades.py +2 -2
- wbportfolio/migrations/0082_remove_tradeproposal_creator_and_more.py +93 -0
- wbportfolio/migrations/0083_order_alter_trade_options_and_more.py +181 -0
- wbportfolio/models/__init__.py +2 -0
- wbportfolio/models/orders/__init__.py +2 -0
- wbportfolio/models/{transactions/trade_proposals.py → orders/order_proposals.py} +304 -264
- wbportfolio/models/orders/orders.py +243 -0
- wbportfolio/models/portfolio.py +16 -19
- wbportfolio/models/{transactions/rebalancing.py → rebalancing.py} +18 -18
- wbportfolio/models/transactions/__init__.py +0 -2
- wbportfolio/models/transactions/trades.py +10 -450
- wbportfolio/pms/analytics/portfolio.py +10 -6
- wbportfolio/pms/analytics/utils.py +9 -0
- wbportfolio/pms/trading/handler.py +28 -27
- wbportfolio/pms/typing.py +18 -7
- wbportfolio/rebalancing/decorators.py +1 -1
- wbportfolio/rebalancing/models/composite.py +3 -7
- wbportfolio/rebalancing/models/market_capitalization_weighted.py +3 -1
- wbportfolio/serializers/__init__.py +1 -0
- wbportfolio/serializers/orders/__init__.py +2 -0
- wbportfolio/serializers/{transactions/trade_proposals.py → orders/order_proposals.py} +30 -17
- wbportfolio/serializers/orders/orders.py +187 -0
- wbportfolio/serializers/portfolios.py +7 -7
- wbportfolio/serializers/rebalancing.py +1 -1
- wbportfolio/serializers/transactions/__init__.py +1 -5
- wbportfolio/serializers/transactions/trades.py +1 -182
- wbportfolio/tests/conftest.py +4 -2
- wbportfolio/tests/models/orders/__init__.py +0 -0
- wbportfolio/tests/models/{transactions/test_trade_proposals.py → orders/test_order_proposals.py} +214 -250
- wbportfolio/tests/models/test_portfolios.py +11 -10
- wbportfolio/tests/models/transactions/test_rebalancing.py +5 -5
- wbportfolio/tests/models/transactions/test_trades.py +0 -20
- wbportfolio/tests/rebalancing/test_models.py +24 -28
- wbportfolio/tests/signals.py +10 -10
- wbportfolio/tests/tests.py +1 -1
- wbportfolio/urls.py +7 -7
- wbportfolio/viewsets/__init__.py +2 -0
- wbportfolio/viewsets/configs/buttons/__init__.py +2 -3
- wbportfolio/viewsets/configs/buttons/trades.py +0 -8
- wbportfolio/viewsets/configs/display/__init__.py +0 -2
- wbportfolio/viewsets/configs/display/portfolios.py +5 -5
- wbportfolio/viewsets/configs/display/rebalancing.py +2 -2
- wbportfolio/viewsets/configs/display/trades.py +1 -225
- wbportfolio/viewsets/configs/endpoints/__init__.py +0 -3
- wbportfolio/viewsets/configs/endpoints/trades.py +0 -41
- wbportfolio/viewsets/orders/__init__.py +6 -0
- wbportfolio/viewsets/orders/configs/__init__.py +4 -0
- wbportfolio/viewsets/orders/configs/buttons/__init__.py +2 -0
- wbportfolio/viewsets/{configs/buttons/trade_proposals.py → orders/configs/buttons/order_proposals.py} +21 -21
- wbportfolio/viewsets/orders/configs/buttons/orders.py +9 -0
- wbportfolio/viewsets/orders/configs/displays/__init__.py +2 -0
- wbportfolio/viewsets/{configs/display/trade_proposals.py → orders/configs/displays/order_proposals.py} +21 -21
- wbportfolio/viewsets/orders/configs/displays/orders.py +180 -0
- wbportfolio/viewsets/orders/configs/endpoints/__init__.py +2 -0
- wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py +21 -0
- wbportfolio/viewsets/orders/configs/endpoints/orders.py +26 -0
- wbportfolio/viewsets/orders/configs/titles/__init__.py +0 -0
- wbportfolio/viewsets/orders/configs/titles/orders.py +0 -0
- wbportfolio/viewsets/{transactions/trade_proposals.py → orders/order_proposals.py} +46 -46
- wbportfolio/viewsets/orders/orders.py +219 -0
- wbportfolio/viewsets/portfolios.py +12 -12
- wbportfolio/viewsets/{transactions/rebalancing.py → rebalancing.py} +2 -2
- wbportfolio/viewsets/transactions/__init__.py +1 -7
- wbportfolio/viewsets/transactions/trades.py +1 -199
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/METADATA +1 -1
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/RECORD +85 -58
- wbportfolio/viewsets/configs/endpoints/trade_proposals.py +0 -18
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/WHEEL +0 -0
- {wbportfolio-1.54.14.dist-info → wbportfolio-1.54.16.dist-info}/licenses/LICENSE +0 -0
wbportfolio/admin/__init__.py
CHANGED
|
@@ -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,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
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
2
|
|
|
3
|
-
from wbportfolio.models import Trade
|
|
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",
|
|
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
|
-
|
|
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(
|
|
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
|
|
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,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
|
-
|
|
21
|
+
approve_order_proposal_automatically = False
|
|
22
22
|
frequency = "RRULE:FREQ=MONTHLY;"
|
|
23
23
|
activation_date = None
|
wbportfolio/factories/trades.py
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
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.
|
|
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
|
|
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
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
data["value_date"] =
|
|
49
|
-
data["transaction_date"] =
|
|
50
|
-
data["
|
|
51
|
-
data["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.
|
|
163
|
+
from wbportfolio.models.orders.order_proposals import OrderProposal
|
|
164
164
|
|
|
165
|
-
if
|
|
166
|
-
# if a
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
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
|
|
213
|
-
for
|
|
214
|
-
replay_as_task.delay(
|
|
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.
|
|
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
|
|
13
|
+
class OrderProposalTradeResource(FilterModelResource):
|
|
14
14
|
"""
|
|
15
|
-
Trade Resource class to use to import trade from the
|
|
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
|
+
]
|
wbportfolio/models/__init__.py
CHANGED
|
@@ -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
|