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
|
@@ -117,7 +117,7 @@ class TradingService:
|
|
|
117
117
|
if self._effective_portfolio:
|
|
118
118
|
for trade in validated_trades:
|
|
119
119
|
if (
|
|
120
|
-
trade.
|
|
120
|
+
trade.previous_weight
|
|
121
121
|
and trade.underlying_instrument not in self._effective_portfolio.positions_map
|
|
122
122
|
):
|
|
123
123
|
raise ValidationError("All effective position needs to be matched with a validated trade")
|
|
@@ -137,37 +137,38 @@ class TradingService:
|
|
|
137
137
|
|
|
138
138
|
Returns: The normalized trades batch
|
|
139
139
|
"""
|
|
140
|
+
|
|
140
141
|
instruments = effective_portfolio.positions_map.copy()
|
|
141
142
|
instruments.update(target_portfolio.positions_map)
|
|
142
143
|
|
|
143
144
|
trades: list[Trade] = []
|
|
144
145
|
for instrument_id, pos in instruments.items():
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
146
|
+
previous_weight = target_weight = 0
|
|
147
|
+
effective_shares = target_shares = 0
|
|
148
|
+
daily_return = 0
|
|
149
|
+
if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
|
|
150
|
+
previous_weight = effective_pos.weighting
|
|
151
|
+
effective_shares = effective_pos.shares
|
|
152
|
+
daily_return = effective_pos.daily_return
|
|
153
|
+
if target_pos := target_portfolio.positions_map.get(instrument_id, None):
|
|
154
|
+
target_weight = target_pos.weighting
|
|
155
|
+
if target_pos.shares is not None:
|
|
156
|
+
target_shares = target_pos.shares
|
|
157
|
+
trade = Trade(
|
|
158
|
+
underlying_instrument=instrument_id,
|
|
159
|
+
previous_weight=previous_weight,
|
|
160
|
+
target_weight=target_weight,
|
|
161
|
+
effective_shares=effective_shares,
|
|
162
|
+
target_shares=target_shares,
|
|
163
|
+
date=self.trade_date,
|
|
164
|
+
instrument_type=pos.instrument_type,
|
|
165
|
+
currency=pos.currency,
|
|
166
|
+
price=Decimal(pos.price) if pos.price is not None else Decimal("0"),
|
|
167
|
+
currency_fx_rate=Decimal(pos.currency_fx_rate),
|
|
168
|
+
daily_return=Decimal(daily_return),
|
|
169
|
+
portfolio_contribution=effective_portfolio.portfolio_contribution,
|
|
170
|
+
)
|
|
171
|
+
trades.append(trade)
|
|
171
172
|
return TradeBatch(trades)
|
|
172
173
|
|
|
173
174
|
def is_valid(self, ignore_error: bool = False) -> bool:
|
wbportfolio/pms/typing.py
CHANGED
|
@@ -19,7 +19,7 @@ class Position:
|
|
|
19
19
|
weighting: Decimal
|
|
20
20
|
date: date_lib
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
daily_return: Decimal = Decimal("0")
|
|
23
23
|
currency: int | None = None
|
|
24
24
|
instrument_type: int | None = None
|
|
25
25
|
asset_valuation_date: date_lib | None = None
|
|
@@ -73,6 +73,10 @@ class Portfolio:
|
|
|
73
73
|
def total_shares(self):
|
|
74
74
|
return sum([pos.target_shares for pos in self.positions if pos.target_shares is not None])
|
|
75
75
|
|
|
76
|
+
@property
|
|
77
|
+
def portfolio_contribution(self) -> Decimal:
|
|
78
|
+
return sum(map(lambda pos: pos.weighting * (Decimal("1") + pos.daily_return), self.positions))
|
|
79
|
+
|
|
76
80
|
def to_df(self):
|
|
77
81
|
return pd.DataFrame([asdict(pos) for pos in self.positions])
|
|
78
82
|
|
|
@@ -98,7 +102,8 @@ class Trade:
|
|
|
98
102
|
currency_fx_rate: Decimal = Decimal("1")
|
|
99
103
|
effective_shares: Decimal = Decimal("0")
|
|
100
104
|
target_shares: Decimal = Decimal("0")
|
|
101
|
-
|
|
105
|
+
daily_return: Decimal = Decimal("0")
|
|
106
|
+
portfolio_contribution: Decimal = Decimal("1")
|
|
102
107
|
id: int | None = None
|
|
103
108
|
is_cash: bool = False
|
|
104
109
|
|
|
@@ -109,8 +114,9 @@ class Trade:
|
|
|
109
114
|
target_weight=self.target_weight + other.target_weight,
|
|
110
115
|
effective_shares=self.effective_shares,
|
|
111
116
|
target_shares=self.target_shares + other.target_shares,
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
daily_return=self.daily_return,
|
|
118
|
+
portfolio_contribution=self.portfolio_contribution,
|
|
119
|
+
**{
|
|
114
120
|
f.name: getattr(self, f.name)
|
|
115
121
|
for f in fields(Trade)
|
|
116
122
|
if f.name
|
|
@@ -120,7 +126,8 @@ class Trade:
|
|
|
120
126
|
"effective_shares",
|
|
121
127
|
"target_shares",
|
|
122
128
|
"underlying_instrument",
|
|
123
|
-
"
|
|
129
|
+
"daily_return",
|
|
130
|
+
"portfolio_contribution",
|
|
124
131
|
]
|
|
125
132
|
},
|
|
126
133
|
)
|
|
@@ -132,7 +139,11 @@ class Trade:
|
|
|
132
139
|
|
|
133
140
|
@property
|
|
134
141
|
def effective_weight(self) -> Decimal:
|
|
135
|
-
return
|
|
142
|
+
return (
|
|
143
|
+
self.previous_weight * (round(self.daily_return, 16) + 1) / self.portfolio_contribution
|
|
144
|
+
if self.portfolio_contribution
|
|
145
|
+
else self.previous_weight
|
|
146
|
+
)
|
|
136
147
|
|
|
137
148
|
@property
|
|
138
149
|
def delta_weight(self) -> Decimal:
|
|
@@ -210,7 +221,7 @@ class TradeBatch:
|
|
|
210
221
|
underlying_instrument=trade.underlying_instrument,
|
|
211
222
|
instrument_type=trade.instrument_type,
|
|
212
223
|
weighting=trade.target_weight if not use_effective else trade.previous_weight,
|
|
213
|
-
|
|
224
|
+
daily_return=trade.daily_return if use_effective else Decimal("0"),
|
|
214
225
|
shares=trade.target_shares,
|
|
215
226
|
currency=trade.currency,
|
|
216
227
|
date=trade.date,
|
|
@@ -2,7 +2,7 @@ def register(model_name: str):
|
|
|
2
2
|
"""
|
|
3
3
|
Decorator to include when a backend need automatic registration
|
|
4
4
|
"""
|
|
5
|
-
from wbportfolio.models.
|
|
5
|
+
from wbportfolio.models.rebalancing import RebalancingModel
|
|
6
6
|
|
|
7
7
|
def _decorator(backend_class):
|
|
8
8
|
defaults = {
|
|
@@ -3,7 +3,6 @@ from decimal import Decimal
|
|
|
3
3
|
from django.core.exceptions import ObjectDoesNotExist
|
|
4
4
|
from wbfdm.models import InstrumentPrice
|
|
5
5
|
|
|
6
|
-
from wbportfolio.models import Trade
|
|
7
6
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
8
7
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
9
8
|
from wbportfolio.rebalancing.decorators import register
|
|
@@ -14,21 +13,18 @@ class CompositeRebalancing(AbstractRebalancingModel):
|
|
|
14
13
|
@property
|
|
15
14
|
def base_assets(self) -> dict[int, Decimal]:
|
|
16
15
|
"""
|
|
17
|
-
Return a dictionary representation (instrument_id: target weight) of this
|
|
16
|
+
Return a dictionary representation (instrument_id: target weight) of this order proposal
|
|
18
17
|
Returns:
|
|
19
18
|
A dictionary representation
|
|
20
19
|
|
|
21
20
|
"""
|
|
22
21
|
try:
|
|
23
|
-
|
|
22
|
+
latest_order_proposal = self.portfolio.order_proposals.filter(
|
|
24
23
|
status="APPROVED", trade_date__lt=self.trade_date
|
|
25
24
|
).latest("trade_date")
|
|
26
25
|
return {
|
|
27
26
|
v["underlying_instrument"]: v["target_weight"]
|
|
28
|
-
for v in
|
|
29
|
-
.annotate_base_info()
|
|
30
|
-
.filter(status=Trade.Status.EXECUTED)
|
|
31
|
-
.values("underlying_instrument", "target_weight")
|
|
27
|
+
for v in latest_order_proposal.get_orders().values("underlying_instrument", "target_weight")
|
|
32
28
|
}
|
|
33
29
|
except ObjectDoesNotExist:
|
|
34
30
|
return dict()
|
|
@@ -12,6 +12,7 @@ from wbfdm.models import (
|
|
|
12
12
|
InstrumentListThroughModel,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
+
from wbportfolio.pms.analytics.utils import fix_quantization_error
|
|
15
16
|
from wbportfolio.pms.typing import Portfolio, Position
|
|
16
17
|
from wbportfolio.pms.typing import Portfolio as PortfolioDTO
|
|
17
18
|
from wbportfolio.rebalancing.base import AbstractRebalancingModel
|
|
@@ -120,11 +121,12 @@ class MarketCapitalizationRebalancing(AbstractRebalancingModel):
|
|
|
120
121
|
df = self.market_cap_df / self.market_cap_df.dropna().sum()
|
|
121
122
|
df = df[df > self.MIN_WEIGHT]
|
|
122
123
|
df = df / df.sum()
|
|
124
|
+
df = fix_quantization_error(df, 8)
|
|
123
125
|
for underlying_instrument, weighting in df.to_dict().items():
|
|
124
126
|
if np.isnan(weighting):
|
|
125
127
|
weighting = Decimal(0)
|
|
126
128
|
else:
|
|
127
|
-
weighting = Decimal(weighting)
|
|
129
|
+
weighting = round(Decimal(weighting), 8)
|
|
128
130
|
positions.append(
|
|
129
131
|
Position(
|
|
130
132
|
underlying_instrument=underlying_instrument,
|
|
@@ -27,6 +27,7 @@ from .registers import RegisterModelSerializer, RegisterRepresentationSerializer
|
|
|
27
27
|
from .roles import PortfolioRoleModelSerializer, PortfolioRoleProjectModelSerializer
|
|
28
28
|
from .signals import *
|
|
29
29
|
from .transactions import *
|
|
30
|
+
from .orders import *
|
|
30
31
|
from .products import (
|
|
31
32
|
ProductRepresentationSerializer,
|
|
32
33
|
ProductCustomerRepresentationSerializer,
|
|
@@ -6,16 +6,24 @@ from rest_framework.reverse import reverse
|
|
|
6
6
|
from wbcore import serializers as wb_serializers
|
|
7
7
|
from wbcore.serializers import DefaultFromView
|
|
8
8
|
|
|
9
|
-
from wbportfolio.models import Portfolio, RebalancingModel
|
|
9
|
+
from wbportfolio.models import OrderProposal, Portfolio, RebalancingModel
|
|
10
10
|
|
|
11
11
|
from .. import PortfolioRepresentationSerializer, RebalancingModelRepresentationSerializer
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class
|
|
14
|
+
class OrderProposalRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
15
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbportfolio:orderproposal-detail")
|
|
16
|
+
|
|
17
|
+
class Meta:
|
|
18
|
+
model = OrderProposal
|
|
19
|
+
fields = ("id", "trade_date", "status", "_detail")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OrderProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
15
23
|
rebalancing_model = wb_serializers.PrimaryKeyRelatedField(queryset=RebalancingModel.objects.all(), required=False)
|
|
16
24
|
_rebalancing_model = RebalancingModelRepresentationSerializer(source="rebalancing_model")
|
|
17
25
|
target_portfolio = wb_serializers.PrimaryKeyRelatedField(
|
|
18
|
-
queryset=Portfolio.objects.all(), write_only=True, required=False
|
|
26
|
+
queryset=Portfolio.objects.all(), write_only=True, required=False
|
|
19
27
|
)
|
|
20
28
|
_target_portfolio = PortfolioRepresentationSerializer(source="target_portfolio")
|
|
21
29
|
total_cash_weight = wb_serializers.DecimalField(
|
|
@@ -43,10 +51,15 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
43
51
|
obj = super().create(validated_data)
|
|
44
52
|
|
|
45
53
|
target_portfolio_dto = None
|
|
46
|
-
if target_portfolio
|
|
54
|
+
if target_portfolio:
|
|
47
55
|
target_portfolio_dto = target_portfolio._build_dto(obj.trade_date)
|
|
56
|
+
elif rebalancing_model:
|
|
57
|
+
target_portfolio_dto = rebalancing_model.get_target_portfolio(
|
|
58
|
+
obj.portfolio, obj.trade_date, obj.last_effective_date
|
|
59
|
+
)
|
|
60
|
+
|
|
48
61
|
try:
|
|
49
|
-
obj.
|
|
62
|
+
obj.reset_orders(
|
|
50
63
|
target_portfolio=target_portfolio_dto, total_target_weight=Decimal("1.0") - total_cash_weight
|
|
51
64
|
)
|
|
52
65
|
except ValidationError as e:
|
|
@@ -57,21 +70,21 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
57
70
|
@wb_serializers.register_only_instance_resource()
|
|
58
71
|
def additional_resources(self, instance, request, user, **kwargs):
|
|
59
72
|
res = {}
|
|
60
|
-
if instance.status ==
|
|
61
|
-
res["replay"] = reverse("wbportfolio:
|
|
62
|
-
if instance.status ==
|
|
63
|
-
res["reset"] = reverse("wbportfolio:
|
|
64
|
-
res["normalize"] = reverse("wbportfolio:
|
|
65
|
-
res["deleteall"] = reverse("wbportfolio:
|
|
66
|
-
res["
|
|
67
|
-
"wbportfolio:
|
|
73
|
+
if instance.status == OrderProposal.Status.APPROVED:
|
|
74
|
+
res["replay"] = reverse("wbportfolio:orderproposal-replay", args=[instance.id], request=request)
|
|
75
|
+
if instance.status == OrderProposal.Status.DRAFT:
|
|
76
|
+
res["reset"] = reverse("wbportfolio:orderproposal-reset", args=[instance.id], request=request)
|
|
77
|
+
res["normalize"] = reverse("wbportfolio:orderproposal-normalize", args=[instance.id], request=request)
|
|
78
|
+
res["deleteall"] = reverse("wbportfolio:orderproposal-deleteall", args=[instance.id], request=request)
|
|
79
|
+
res["orders"] = reverse(
|
|
80
|
+
"wbportfolio:orderproposal-order-list",
|
|
68
81
|
args=[instance.id],
|
|
69
82
|
request=request,
|
|
70
83
|
)
|
|
71
84
|
return res
|
|
72
85
|
|
|
73
86
|
class Meta:
|
|
74
|
-
model =
|
|
87
|
+
model = OrderProposal
|
|
75
88
|
only_fsm_transition_on_instance = True
|
|
76
89
|
fields = (
|
|
77
90
|
"id",
|
|
@@ -88,6 +101,6 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
88
101
|
)
|
|
89
102
|
|
|
90
103
|
|
|
91
|
-
class
|
|
92
|
-
class Meta(
|
|
93
|
-
read_only_fields =
|
|
104
|
+
class ReadOnlyOrderProposalModelSerializer(OrderProposalModelSerializer):
|
|
105
|
+
class Meta(OrderProposalModelSerializer.Meta):
|
|
106
|
+
read_only_fields = OrderProposalModelSerializer.Meta.fields
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from rest_framework import serializers
|
|
4
|
+
from wbcore import serializers as wb_serializers
|
|
5
|
+
from wbcore.metadata.configs.display.list_display import BaseTreeGroupLevelOption
|
|
6
|
+
from wbfdm.models import Instrument
|
|
7
|
+
from wbfdm.serializers import InvestableInstrumentRepresentationSerializer
|
|
8
|
+
from wbfdm.serializers.instruments.instruments import (
|
|
9
|
+
CompanyRepresentationSerializer,
|
|
10
|
+
SecurityRepresentationSerializer,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from wbportfolio.models import Order
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GetSecurityDefault:
|
|
17
|
+
requires_context = True
|
|
18
|
+
|
|
19
|
+
def __call__(self, serializer_instance):
|
|
20
|
+
try:
|
|
21
|
+
instance = serializer_instance.view.get_object()
|
|
22
|
+
return instance.underlying_instrument.parent or instance.underlying_instrument
|
|
23
|
+
except Exception:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GetCompanyDefault:
|
|
28
|
+
requires_context = True
|
|
29
|
+
|
|
30
|
+
def __call__(self, serializer_instance):
|
|
31
|
+
try:
|
|
32
|
+
instance = serializer_instance.view.get_object()
|
|
33
|
+
security = instance.underlying_instrument.parent or instance.underlying_instrument
|
|
34
|
+
return security.parent or security
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
40
|
+
underlying_instrument = wb_serializers.SlugRelatedField(read_only=True, slug_field="name")
|
|
41
|
+
underlying_instrument_isin = wb_serializers.CharField(read_only=True)
|
|
42
|
+
underlying_instrument_ticker = wb_serializers.CharField(read_only=True)
|
|
43
|
+
underlying_instrument_refinitiv_identifier_code = wb_serializers.CharField(read_only=True)
|
|
44
|
+
underlying_instrument_instrument_type = wb_serializers.CharField(read_only=True)
|
|
45
|
+
|
|
46
|
+
target_weight = wb_serializers.DecimalField(
|
|
47
|
+
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
48
|
+
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
49
|
+
required=False,
|
|
50
|
+
default=0,
|
|
51
|
+
)
|
|
52
|
+
effective_weight = wb_serializers.DecimalField(
|
|
53
|
+
read_only=True,
|
|
54
|
+
max_digits=Order.ORDER_WEIGHTING_PRECISION + 1,
|
|
55
|
+
decimal_places=Order.ORDER_WEIGHTING_PRECISION,
|
|
56
|
+
default=0,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
effective_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
60
|
+
target_shares = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=6, default=0)
|
|
61
|
+
|
|
62
|
+
total_value_fx_portfolio = wb_serializers.DecimalField(read_only=True, max_digits=16, decimal_places=2, default=0)
|
|
63
|
+
effective_total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
64
|
+
read_only=True, max_digits=16, decimal_places=2, default=0
|
|
65
|
+
)
|
|
66
|
+
target_total_value_fx_portfolio = wb_serializers.DecimalField(
|
|
67
|
+
read_only=True, max_digits=16, decimal_places=2, default=0
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
portfolio_currency = wb_serializers.CharField(read_only=True)
|
|
71
|
+
has_warnings = wb_serializers.BooleanField(read_only=True)
|
|
72
|
+
|
|
73
|
+
def validate(self, data):
|
|
74
|
+
data.pop("company", None)
|
|
75
|
+
data.pop("security", None)
|
|
76
|
+
if self.instance and "underlying_instrument" in data:
|
|
77
|
+
raise serializers.ValidationError(
|
|
78
|
+
{
|
|
79
|
+
"underlying_instrument": "You cannot modify the underlying instrument other than creating a new entry"
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
effective_weight = self.instance._effective_weight if self.instance else Decimal(0.0)
|
|
83
|
+
weighting = data.get("weighting", self.instance.weighting if self.instance else Decimal(0.0))
|
|
84
|
+
if (target_weight := data.pop("target_weight", None)) is not None:
|
|
85
|
+
weighting = target_weight - effective_weight
|
|
86
|
+
if (target_weight := data.pop("target_weight", None)) is not None:
|
|
87
|
+
weighting = target_weight - effective_weight
|
|
88
|
+
if weighting >= 0:
|
|
89
|
+
data["order_type"] = "BUY"
|
|
90
|
+
else:
|
|
91
|
+
data["order_type"] = "SELL"
|
|
92
|
+
data["weighting"] = weighting
|
|
93
|
+
return super().validate(data)
|
|
94
|
+
|
|
95
|
+
class Meta:
|
|
96
|
+
model = Order
|
|
97
|
+
percent_fields = ["effective_weight", "target_weight", "weighting"]
|
|
98
|
+
decorators = {
|
|
99
|
+
"total_value_fx_portfolio": wb_serializers.decorator(
|
|
100
|
+
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
101
|
+
),
|
|
102
|
+
"effective_total_value_fx_portfolio": wb_serializers.decorator(
|
|
103
|
+
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
104
|
+
),
|
|
105
|
+
"target_total_value_fx_portfolio": wb_serializers.decorator(
|
|
106
|
+
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
read_only_fields = (
|
|
110
|
+
"order_type",
|
|
111
|
+
"shares",
|
|
112
|
+
"effective_shares",
|
|
113
|
+
"target_shares",
|
|
114
|
+
"total_value_fx_portfolio",
|
|
115
|
+
"effective_total_value_fx_portfolio",
|
|
116
|
+
"target_total_value_fx_portfolio",
|
|
117
|
+
"has_warnings",
|
|
118
|
+
)
|
|
119
|
+
fields = (
|
|
120
|
+
"id",
|
|
121
|
+
"shares",
|
|
122
|
+
"underlying_instrument",
|
|
123
|
+
"underlying_instrument_isin",
|
|
124
|
+
"underlying_instrument_ticker",
|
|
125
|
+
"underlying_instrument_refinitiv_identifier_code",
|
|
126
|
+
"underlying_instrument_instrument_type",
|
|
127
|
+
"order_type",
|
|
128
|
+
"comment",
|
|
129
|
+
"effective_weight",
|
|
130
|
+
"target_weight",
|
|
131
|
+
"weighting",
|
|
132
|
+
"order_proposal",
|
|
133
|
+
"order",
|
|
134
|
+
"effective_shares",
|
|
135
|
+
"target_shares",
|
|
136
|
+
"total_value_fx_portfolio",
|
|
137
|
+
"effective_total_value_fx_portfolio",
|
|
138
|
+
"target_total_value_fx_portfolio",
|
|
139
|
+
"portfolio_currency",
|
|
140
|
+
"has_warnings",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class OrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
|
|
145
|
+
company = wb_serializers.PrimaryKeyRelatedField(
|
|
146
|
+
queryset=Instrument.objects.filter(level=0),
|
|
147
|
+
required=False,
|
|
148
|
+
read_only=lambda view: not view.new_mode,
|
|
149
|
+
default=GetCompanyDefault(),
|
|
150
|
+
)
|
|
151
|
+
_company = CompanyRepresentationSerializer(source="company", required=False)
|
|
152
|
+
|
|
153
|
+
security = wb_serializers.PrimaryKeyRelatedField(
|
|
154
|
+
queryset=Instrument.objects.filter(is_security=True),
|
|
155
|
+
required=False,
|
|
156
|
+
read_only=lambda view: not view.new_mode,
|
|
157
|
+
default=GetSecurityDefault(),
|
|
158
|
+
)
|
|
159
|
+
_security = SecurityRepresentationSerializer(
|
|
160
|
+
source="security",
|
|
161
|
+
optional_get_parameters={"company": "parent"},
|
|
162
|
+
depends_on=[{"field": "company", "options": {}}],
|
|
163
|
+
required=False,
|
|
164
|
+
)
|
|
165
|
+
underlying_instrument = wb_serializers.PrimaryKeyRelatedField(
|
|
166
|
+
queryset=Instrument.objects.all(), label="Quote", read_only=lambda view: not view.new_mode
|
|
167
|
+
)
|
|
168
|
+
_underlying_instrument = InvestableInstrumentRepresentationSerializer(
|
|
169
|
+
source="underlying_instrument",
|
|
170
|
+
optional_get_parameters={"security": "parent"},
|
|
171
|
+
depends_on=[{"field": "security", "options": {}}],
|
|
172
|
+
tree_config=BaseTreeGroupLevelOption(clear_filter=True, filter_key="parent"),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
class Meta(OrderOrderProposalListModelSerializer.Meta):
|
|
176
|
+
fields = list(OrderOrderProposalListModelSerializer.Meta.fields) + [
|
|
177
|
+
"company",
|
|
178
|
+
"_company",
|
|
179
|
+
"security",
|
|
180
|
+
"_security",
|
|
181
|
+
"_underlying_instrument",
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class ReadOnlyOrderOrderProposalModelSerializer(OrderOrderProposalListModelSerializer):
|
|
186
|
+
class Meta(OrderOrderProposalListModelSerializer.Meta):
|
|
187
|
+
read_only_fields = OrderOrderProposalListModelSerializer.Meta.fields
|
|
@@ -34,10 +34,10 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
34
34
|
|
|
35
35
|
last_asset_under_management_usd = wb_serializers.FloatField(read_only=True)
|
|
36
36
|
last_positions = wb_serializers.FloatField(read_only=True)
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
last_order_proposal_date = wb_serializers.DateField(read_only=True)
|
|
38
|
+
next_expected_order_proposal_date = wb_serializers.SerializerMethodField(read_only=True)
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def get_next_expected_order_proposal_date(self, obj):
|
|
41
41
|
if (automatic_rebalancer := getattr(obj, "automatic_rebalancer", None)) and (
|
|
42
42
|
_d := automatic_rebalancer.get_next_rebalancing_date(date.today())
|
|
43
43
|
):
|
|
@@ -107,8 +107,8 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
107
107
|
args=[instance.id],
|
|
108
108
|
request=request,
|
|
109
109
|
)
|
|
110
|
-
additional_resources["
|
|
111
|
-
"wbportfolio:portfolio-
|
|
110
|
+
additional_resources["order_proposals"] = reverse(
|
|
111
|
+
"wbportfolio:portfolio-orderproposal-list",
|
|
112
112
|
args=[instance.id],
|
|
113
113
|
request=request,
|
|
114
114
|
)
|
|
@@ -169,8 +169,8 @@ class PortfolioModelSerializer(wb_serializers.ModelSerializer):
|
|
|
169
169
|
"last_position_date",
|
|
170
170
|
"last_asset_under_management_usd",
|
|
171
171
|
"last_positions",
|
|
172
|
-
"
|
|
173
|
-
"
|
|
172
|
+
"last_order_proposal_date",
|
|
173
|
+
"next_expected_order_proposal_date",
|
|
174
174
|
)
|
|
175
175
|
|
|
176
176
|
|
|
@@ -49,7 +49,7 @@ class RebalancerModelSerializer(wb_serializers.ModelSerializer):
|
|
|
49
49
|
"computed_str",
|
|
50
50
|
"_rebalancing_model",
|
|
51
51
|
"rebalancing_model",
|
|
52
|
-
"
|
|
52
|
+
"approve_order_proposal_automatically",
|
|
53
53
|
"activation_date",
|
|
54
54
|
"frequency",
|
|
55
55
|
"frequency_repr",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from .trade_proposals import TradeProposalModelSerializer, ReadOnlyTradeProposalModelSerializer
|
|
2
1
|
from .claim import (
|
|
3
2
|
ClaimAccountSerializer,
|
|
4
3
|
ClaimAPIModelSerializer,
|
|
@@ -12,8 +11,5 @@ from .dividends import DividendModelSerializer, DividendRepresentationSerializer
|
|
|
12
11
|
from .fees import FeesModelSerializer, FeesRepresentationSerializer
|
|
13
12
|
from .trades import (
|
|
14
13
|
TradeModelSerializer,
|
|
15
|
-
|
|
16
|
-
TradeRepresentationSerializer,
|
|
17
|
-
TradeTradeProposalModelSerializer,
|
|
18
|
-
ReadOnlyTradeTradeProposalModelSerializer,
|
|
14
|
+
TradeRepresentationSerializer
|
|
19
15
|
)
|