wbportfolio 1.56.3__py2.py3-none-any.whl → 1.56.4__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbportfolio might be problematic. Click here for more details.
- wbportfolio/api_clients/ubs.py +6 -6
- wbportfolio/import_export/handlers/asset_position.py +6 -2
- wbportfolio/import_export/parsers/natixis/equity.py +9 -9
- wbportfolio/models/asset.py +4 -1
- wbportfolio/models/orders/order_proposals.py +40 -14
- wbportfolio/models/orders/orders.py +41 -25
- wbportfolio/risk_management/backends/exposure_portfolio.py +1 -1
- wbportfolio/serializers/orders/orders.py +12 -1
- wbportfolio/viewsets/orders/configs/displays/orders.py +7 -1
- wbportfolio/viewsets/orders/order_proposals.py +1 -3
- wbportfolio/viewsets/orders/orders.py +1 -0
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.4.dist-info}/METADATA +1 -1
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.4.dist-info}/RECORD +15 -15
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.4.dist-info}/WHEEL +0 -0
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.4.dist-info}/licenses/LICENSE +0 -0
wbportfolio/api_clients/ubs.py
CHANGED
|
@@ -90,7 +90,7 @@ class UBSNeoAPIClient:
|
|
|
90
90
|
isin = self._validate_isin(isin, test=test)
|
|
91
91
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/submit/{isin}"
|
|
92
92
|
payload = {"items": items}
|
|
93
|
-
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=
|
|
93
|
+
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=60)
|
|
94
94
|
self._raise_for_status(response)
|
|
95
95
|
return self._get_json(response)
|
|
96
96
|
|
|
@@ -99,7 +99,7 @@ class UBSNeoAPIClient:
|
|
|
99
99
|
isin = self._validate_isin(isin)
|
|
100
100
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/savedraft/{isin}"
|
|
101
101
|
payload = {"items": items}
|
|
102
|
-
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=
|
|
102
|
+
response = requests.post(url, json=payload, headers=self._get_headers(), timeout=60)
|
|
103
103
|
self._raise_for_status(response)
|
|
104
104
|
return self._get_json(response)
|
|
105
105
|
|
|
@@ -107,14 +107,14 @@ class UBSNeoAPIClient:
|
|
|
107
107
|
"""Cancel a rebalance request."""
|
|
108
108
|
isin = self._validate_isin(isin)
|
|
109
109
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/cancel/{isin}"
|
|
110
|
-
response = requests.delete(url, headers=self._get_headers(), timeout=
|
|
110
|
+
response = requests.delete(url, headers=self._get_headers(), timeout=60)
|
|
111
111
|
self._raise_for_status(response)
|
|
112
112
|
return self._get_json(response)
|
|
113
113
|
|
|
114
114
|
def get_current_rebalance_request(self, isin: str) -> dict:
|
|
115
115
|
"""Fetch the current rebalance request for a certificate."""
|
|
116
116
|
url = f"{self.BASE_URL}/ged-amc/external/rebalance/v1/currentRebalanceRequest/{isin}"
|
|
117
|
-
response = requests.get(url, headers=self._get_headers(), timeout=
|
|
117
|
+
response = requests.get(url, headers=self._get_headers(), timeout=60)
|
|
118
118
|
self._raise_for_status(response)
|
|
119
119
|
return self._get_json(response)
|
|
120
120
|
|
|
@@ -130,7 +130,7 @@ class UBSNeoAPIClient:
|
|
|
130
130
|
url,
|
|
131
131
|
headers=self._get_headers(),
|
|
132
132
|
params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
|
|
133
|
-
timeout=
|
|
133
|
+
timeout=30,
|
|
134
134
|
)
|
|
135
135
|
self._raise_for_status(response)
|
|
136
136
|
return self._get_json(response)
|
|
@@ -141,7 +141,7 @@ class UBSNeoAPIClient:
|
|
|
141
141
|
url,
|
|
142
142
|
headers=self._get_headers(),
|
|
143
143
|
params={"fromDate": from_date.strftime("%Y-%m-%d"), "toDate": to_date.strftime("%Y-%m-%d")},
|
|
144
|
-
timeout=
|
|
144
|
+
timeout=30,
|
|
145
145
|
)
|
|
146
146
|
self._raise_for_status(response)
|
|
147
147
|
return self._get_json(response)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
2
|
from contextlib import suppress
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
4
|
from decimal import Decimal
|
|
5
5
|
from itertools import chain
|
|
6
6
|
from typing import Any, Dict, List, Optional
|
|
@@ -31,7 +31,7 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
31
31
|
self.instrument_price_handler = InstrumentPriceImportHandler(self.import_source)
|
|
32
32
|
self.currency_handler = CurrencyImportHandler(self.import_source)
|
|
33
33
|
|
|
34
|
-
def _deserialize(self, data: Dict[str, Any]):
|
|
34
|
+
def _deserialize(self, data: Dict[str, Any]): # noqa: C901
|
|
35
35
|
from wbportfolio.models import Portfolio
|
|
36
36
|
|
|
37
37
|
portfolio_data = data.pop("portfolio", None)
|
|
@@ -45,6 +45,10 @@ class AssetPositionImportHandler(ImportExportHandler):
|
|
|
45
45
|
data["initial_price"] /= 1000
|
|
46
46
|
data["currency"] = currency
|
|
47
47
|
data["date"] = datetime.strptime(data["date"], "%Y-%m-%d").date()
|
|
48
|
+
|
|
49
|
+
# ensure that the position falls into a weekday
|
|
50
|
+
if data["date"].weekday() == 5:
|
|
51
|
+
data["date"] -= timedelta(days=1)
|
|
48
52
|
if data.get("asset_valuation_date", None):
|
|
49
53
|
data["asset_valuation_date"] = datetime.strptime(data["asset_valuation_date"], "%Y-%m-%d").date()
|
|
50
54
|
else:
|
|
@@ -43,16 +43,16 @@ def parse(import_source):
|
|
|
43
43
|
df = pd.read_excel(import_source.file, engine="openpyxl", sheet_name="Basket Valuation")
|
|
44
44
|
except BadZipFile:
|
|
45
45
|
df = pd.read_excel(import_source.file, engine="xlrd", sheet_name="Basket Valuation")
|
|
46
|
-
xx, yy = np.where(df
|
|
47
|
-
if
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
xx, yy = np.where(df.isin(["Ticker", "Code"]))
|
|
47
|
+
if xx.size > 0 and yy.size > 0:
|
|
48
|
+
df = df.iloc[xx[0] :, yy[0] :]
|
|
49
|
+
df = df.rename(columns=df.iloc[0]).drop(df.index[0]).dropna(how="all")
|
|
50
|
+
df["Quotity/Adj. factor"] = 1.0
|
|
51
|
+
df = df.rename(columns={"Code": "Ticker"})
|
|
52
|
+
else:
|
|
53
|
+
return {}
|
|
54
54
|
df = df.rename(columns=FIELD_MAP)
|
|
55
|
-
df = df.dropna(subset=["initial_price"])
|
|
55
|
+
df = df.dropna(subset=["initial_price", "Name"], how="any")
|
|
56
56
|
df["initial_price"] = df["initial_price"].astype("str").str.replace(" ", "").astype("float")
|
|
57
57
|
df["underlying_quote"] = df[["Ticker", "Name", "currency__key"]].apply(
|
|
58
58
|
lambda x: _get_underlying_instrument(*x), axis=1
|
wbportfolio/models/asset.py
CHANGED
|
@@ -448,7 +448,10 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
448
448
|
net_value = self.initial_price
|
|
449
449
|
# in case the position currency and the linked underlying_quote currency don't correspond, we convert the rate accordingly
|
|
450
450
|
if self.currency != self.underlying_quote.currency:
|
|
451
|
-
|
|
451
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
452
|
+
net_value *= self.currency.convert(
|
|
453
|
+
self.asset_valuation_date, self.underlying_quote.currency
|
|
454
|
+
)
|
|
452
455
|
self.underlying_quote_price = InstrumentPrice.objects.create(
|
|
453
456
|
calculated=False,
|
|
454
457
|
instrument=self.underlying_quote,
|
|
@@ -20,7 +20,7 @@ from django.db.models.functions import Coalesce, Round
|
|
|
20
20
|
from django.db.models.signals import post_save
|
|
21
21
|
from django.dispatch import receiver
|
|
22
22
|
from django.utils.functional import cached_property
|
|
23
|
-
from django.utils.translation import gettext_lazy
|
|
23
|
+
from django.utils.translation import gettext_lazy
|
|
24
24
|
from django_fsm import FSMField, transition
|
|
25
25
|
from pandas._libs.tslibs.offsets import BDay
|
|
26
26
|
from wbcompliance.models.risk_management.mixins import RiskCheckMixin
|
|
@@ -458,6 +458,27 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
458
458
|
weighting = drifted_weight + row["delta_weight"]
|
|
459
459
|
|
|
460
460
|
# Assemble the position object
|
|
461
|
+
trade_date = self.last_effective_date if use_effective else self.trade_date
|
|
462
|
+
price_data = {}
|
|
463
|
+
with suppress(InstrumentPrice.DoesNotExist):
|
|
464
|
+
instrument_price = instrument.valuations.get(date=trade_date)
|
|
465
|
+
if fx_rate := instrument_price.currency_fx_rate_to_usd:
|
|
466
|
+
price_data = {
|
|
467
|
+
"volume_usd": instrument_price.volume
|
|
468
|
+
* float(instrument_price.net_value)
|
|
469
|
+
/ float(fx_rate.value)
|
|
470
|
+
}
|
|
471
|
+
if instrument_price.market_capitalization:
|
|
472
|
+
price_data["market_capitalization_usd"] = instrument_price.market_capitalization / float(
|
|
473
|
+
fx_rate.value
|
|
474
|
+
)
|
|
475
|
+
if row["shares"] is not None:
|
|
476
|
+
if instrument_price.market_capitalization:
|
|
477
|
+
price_data["market_share"] = (
|
|
478
|
+
float(row["shares"]) * float(row["price"]) / instrument_price.market_capitalization
|
|
479
|
+
)
|
|
480
|
+
if instrument_price.volume_50d:
|
|
481
|
+
price_data["daily_liquidity"] = float(row["shares"]) / instrument_price.volume_50d / 0.33
|
|
461
482
|
positions.append(
|
|
462
483
|
PositionDTO(
|
|
463
484
|
underlying_instrument=instrument.id,
|
|
@@ -466,10 +487,14 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
466
487
|
daily_return=daily_return if use_effective else Decimal("0"),
|
|
467
488
|
shares=row["shares"],
|
|
468
489
|
currency=instrument.currency.id,
|
|
469
|
-
date=
|
|
490
|
+
date=trade_date,
|
|
491
|
+
asset_valuation_date=trade_date,
|
|
470
492
|
is_cash=instrument.is_cash or instrument.is_cash_equivalent,
|
|
471
493
|
price=row["price"],
|
|
472
494
|
currency_fx_rate=row["currency_fx_rate"],
|
|
495
|
+
exchange=instrument.exchange.id if instrument.exchange else None,
|
|
496
|
+
country=instrument.country.id if instrument.country else None,
|
|
497
|
+
**price_data,
|
|
473
498
|
)
|
|
474
499
|
)
|
|
475
500
|
total_weighting += weighting
|
|
@@ -601,15 +626,11 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
601
626
|
portfolio_value = self.portfolio_total_asset_value
|
|
602
627
|
for order_dto in orders:
|
|
603
628
|
instrument = Instrument.objects.get(id=order_dto.underlying_instrument)
|
|
604
|
-
currency_fx_rate = instrument.currency.convert(
|
|
605
|
-
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
606
|
-
)
|
|
607
629
|
# we cannot do a bulk-create because Order is a multi table inheritance
|
|
608
630
|
weighting = round(order_dto.delta_weight, Order.ORDER_WEIGHTING_PRECISION)
|
|
609
631
|
daily_return = order_dto.daily_return
|
|
610
632
|
try:
|
|
611
633
|
order = self.orders.get(underlying_instrument=instrument)
|
|
612
|
-
order.currency_fx_rate = currency_fx_rate
|
|
613
634
|
order.daily_return = daily_return
|
|
614
635
|
except Order.DoesNotExist:
|
|
615
636
|
order = Order(
|
|
@@ -618,7 +639,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
618
639
|
value_date=self.trade_date,
|
|
619
640
|
weighting=weighting,
|
|
620
641
|
daily_return=daily_return,
|
|
621
|
-
currency_fx_rate=currency_fx_rate,
|
|
622
642
|
)
|
|
623
643
|
order.pre_save()
|
|
624
644
|
order.set_weighting(weighting, portfolio_value)
|
|
@@ -916,7 +936,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
916
936
|
# "There is no change detected in this order proposal. Please submit at last one valid order"
|
|
917
937
|
# )
|
|
918
938
|
if len(service.validated_trades) == 0:
|
|
919
|
-
errors_list.append(
|
|
939
|
+
errors_list.append(gettext_lazy("There is no valid order on this proposal"))
|
|
920
940
|
if service.errors:
|
|
921
941
|
errors_list.extend(service.errors)
|
|
922
942
|
if errors_list:
|
|
@@ -1043,22 +1063,26 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
1043
1063
|
errors = dict()
|
|
1044
1064
|
orders = self.get_orders()
|
|
1045
1065
|
if not self.portfolio.can_be_rebalanced:
|
|
1046
|
-
errors["non_field_errors"] = [
|
|
1066
|
+
errors["non_field_errors"] = [gettext_lazy("The portfolio does not allow manual rebalanced")]
|
|
1047
1067
|
if not orders.exists():
|
|
1048
1068
|
errors["non_field_errors"] = [
|
|
1049
|
-
|
|
1069
|
+
gettext_lazy("At least one order needs to be submitted to be able to apply this proposal")
|
|
1050
1070
|
]
|
|
1051
1071
|
if not self.portfolio.can_be_rebalanced:
|
|
1052
1072
|
errors["portfolio"] = [
|
|
1053
|
-
[
|
|
1073
|
+
[
|
|
1074
|
+
gettext_lazy(
|
|
1075
|
+
"The portfolio needs to be a model portfolio in order to apply this order proposal manually"
|
|
1076
|
+
)
|
|
1077
|
+
]
|
|
1054
1078
|
]
|
|
1055
1079
|
if self.has_non_successful_checks:
|
|
1056
|
-
errors["non_field_errors"] = [
|
|
1080
|
+
errors["non_field_errors"] = [gettext_lazy("The pre orders rules did not passed successfully")]
|
|
1057
1081
|
if orders.filter(has_warnings=True).filter(
|
|
1058
1082
|
underlying_instrument__is_cash=False, underlying_instrument__is_cash_equivalent=False
|
|
1059
1083
|
):
|
|
1060
1084
|
errors["non_field_errors"] = [
|
|
1061
|
-
|
|
1085
|
+
gettext_lazy("There is warning that needs to be addresses on the orders before approval.")
|
|
1062
1086
|
]
|
|
1063
1087
|
return errors
|
|
1064
1088
|
|
|
@@ -1122,7 +1146,9 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
1122
1146
|
errors = dict()
|
|
1123
1147
|
if not self.portfolio.can_be_rebalanced:
|
|
1124
1148
|
errors["portfolio"] = [
|
|
1125
|
-
|
|
1149
|
+
gettext_lazy(
|
|
1150
|
+
"The portfolio needs to be a model portfolio in order to revert this order proposal manually"
|
|
1151
|
+
)
|
|
1126
1152
|
]
|
|
1127
1153
|
return errors
|
|
1128
1154
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
1
2
|
from datetime import date
|
|
2
3
|
from decimal import Decimal
|
|
3
4
|
|
|
@@ -7,6 +8,7 @@ from django.db.models import (
|
|
|
7
8
|
Sum,
|
|
8
9
|
)
|
|
9
10
|
from ordered_model.models import OrderedModel
|
|
11
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
10
12
|
from wbcore.contrib.io.mixins import ImportMixin
|
|
11
13
|
|
|
12
14
|
from wbportfolio.import_export.handlers.orders import OrderImportHandler
|
|
@@ -137,12 +139,7 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
137
139
|
return getattr(
|
|
138
140
|
self,
|
|
139
141
|
"effective_shares",
|
|
140
|
-
|
|
141
|
-
underlying_quote=self.underlying_instrument,
|
|
142
|
-
date=self.value_date,
|
|
143
|
-
portfolio=self.portfolio,
|
|
144
|
-
).aggregate(s=Sum("shares"))["s"]
|
|
145
|
-
or Decimal(0),
|
|
142
|
+
self.get_effective_shares(),
|
|
146
143
|
)
|
|
147
144
|
|
|
148
145
|
@property
|
|
@@ -164,6 +161,7 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
164
161
|
def pre_save(self):
|
|
165
162
|
self.portfolio = self.order_proposal.portfolio
|
|
166
163
|
self.value_date = self.order_proposal.trade_date
|
|
164
|
+
self.set_currency_fx_rate()
|
|
167
165
|
|
|
168
166
|
if not self.price:
|
|
169
167
|
# we try to get the price if not provided directly from the underlying instrument
|
|
@@ -171,7 +169,7 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
171
169
|
self.price = Decimal("1")
|
|
172
170
|
else:
|
|
173
171
|
self.price = self.get_price()
|
|
174
|
-
if not self.portfolio.only_weighting:
|
|
172
|
+
if not self.portfolio.only_weighting and not self.shares:
|
|
175
173
|
estimated_shares = self.order_proposal.get_estimated_shares(
|
|
176
174
|
self.weighting, self.underlying_instrument, self.price
|
|
177
175
|
)
|
|
@@ -211,9 +209,24 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
211
209
|
except ValueError:
|
|
212
210
|
return Decimal("0")
|
|
213
211
|
|
|
212
|
+
def get_effective_shares(self) -> Decimal:
|
|
213
|
+
return AssetPosition.objects.filter(
|
|
214
|
+
underlying_quote=self.underlying_instrument,
|
|
215
|
+
date=self.value_date,
|
|
216
|
+
portfolio=self.portfolio,
|
|
217
|
+
).aggregate(s=Sum("shares"))["s"] or Decimal("0")
|
|
218
|
+
|
|
214
219
|
def set_type(self):
|
|
215
220
|
self.order_type = self.get_type(self.weighting, self._previous_weight, self._target_weight)
|
|
216
221
|
|
|
222
|
+
def set_currency_fx_rate(self):
|
|
223
|
+
self.currency_fx_rate = Decimal("1")
|
|
224
|
+
if self.order_proposal.portfolio.currency != self.underlying_instrument.currency:
|
|
225
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
226
|
+
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
227
|
+
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
228
|
+
)
|
|
229
|
+
|
|
217
230
|
def set_weighting(self, weighting: Decimal, portfolio_value: Decimal):
|
|
218
231
|
self.weighting = weighting
|
|
219
232
|
price_fx_portfolio = self.price * self.currency_fx_rate
|
|
@@ -222,6 +235,8 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
222
235
|
self.shares = total_value / price_fx_portfolio
|
|
223
236
|
else:
|
|
224
237
|
self.shares = Decimal("0")
|
|
238
|
+
if effective_shares := self.get_effective_shares():
|
|
239
|
+
self.shares = max(self.shares, -effective_shares)
|
|
225
240
|
|
|
226
241
|
def set_shares(self, shares: Decimal, portfolio_value: Decimal):
|
|
227
242
|
if portfolio_value:
|
|
@@ -244,26 +259,27 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
244
259
|
warnings = []
|
|
245
260
|
|
|
246
261
|
# if shares is defined and the underlying instrument defines a round lot size different than 1 and exchange allows its application, we round the share accordingly
|
|
247
|
-
if self.
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
if self._target_weight:
|
|
263
|
+
if self.order_proposal and not self.portfolio.only_weighting:
|
|
264
|
+
shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
|
|
265
|
+
if shares != self.shares:
|
|
266
|
+
warnings.append(
|
|
267
|
+
f"{self.underlying_instrument.computed_str} has a round lot size of {self.underlying_instrument.round_lot_size}: shares were rounded from {self.shares} to {shares}"
|
|
268
|
+
)
|
|
269
|
+
shares = round(shares) # ensure fractional shares are converted into integer
|
|
270
|
+
# we need to recompute the delta weight has we changed the number of shares
|
|
271
|
+
if shares != self.shares:
|
|
272
|
+
self.set_shares(shares, portfolio_total_asset_value)
|
|
273
|
+
if abs(self.weighting) < self.order_proposal.min_weighting:
|
|
250
274
|
warnings.append(
|
|
251
|
-
f"{self.underlying_instrument.computed_str}
|
|
275
|
+
f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
|
|
252
276
|
)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
|
|
260
|
-
)
|
|
261
|
-
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
262
|
-
if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
|
|
263
|
-
warnings.append(
|
|
264
|
-
f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
|
|
265
|
-
)
|
|
266
|
-
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
277
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
278
|
+
if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
|
|
279
|
+
warnings.append(
|
|
280
|
+
f"Total Value for order {self.underlying_instrument.computed_str} ({self.total_value_fx_portfolio}) is bellow the allowed Minimum Order Value ({self.order_proposal.min_order_value})"
|
|
281
|
+
)
|
|
282
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
267
283
|
if not self.price:
|
|
268
284
|
warnings.append(f"No price for {self.underlying_instrument.computed_str}")
|
|
269
285
|
if (
|
|
@@ -160,7 +160,7 @@ class RuleBackend(
|
|
|
160
160
|
|
|
161
161
|
def _process_dto(self, portfolio: PortfolioDTO, **kwargs) -> Generator[backend.IncidentResult, None, None]:
|
|
162
162
|
if not (df := self._filter_df(portfolio.to_df())).empty:
|
|
163
|
-
df = df[[self.group_by.value, self.field.value]].groupby(self.group_by.value).sum().astype(float)
|
|
163
|
+
df = df[[self.group_by.value, self.field.value]].dropna().groupby(self.group_by.value).sum().astype(float)
|
|
164
164
|
for threshold in self.thresholds:
|
|
165
165
|
numerical_range = threshold.numerical_range
|
|
166
166
|
incident_df = df[
|
|
@@ -66,6 +66,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
66
66
|
total_value_fx_portfolio = wb_serializers.DecimalField(required=False, max_digits=16, decimal_places=2)
|
|
67
67
|
|
|
68
68
|
portfolio_currency = wb_serializers.CharField(read_only=True)
|
|
69
|
+
underlying_instrument_currency = wb_serializers.CharField(read_only=True)
|
|
69
70
|
has_warnings = wb_serializers.BooleanField(read_only=True)
|
|
70
71
|
|
|
71
72
|
def validate(self, data):
|
|
@@ -131,7 +132,7 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
131
132
|
|
|
132
133
|
class Meta:
|
|
133
134
|
model = Order
|
|
134
|
-
percent_fields = ["effective_weight", "target_weight", "weighting"]
|
|
135
|
+
percent_fields = ["effective_weight", "target_weight", "weighting", "daily_return", "desired_target_weight"]
|
|
135
136
|
decorators = {
|
|
136
137
|
"total_value_fx_portfolio": wb_serializers.decorator(
|
|
137
138
|
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
@@ -142,12 +143,17 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
142
143
|
"target_total_value_fx_portfolio": wb_serializers.decorator(
|
|
143
144
|
decorator_type="text", position="left", value="{{portfolio_currency}}"
|
|
144
145
|
),
|
|
146
|
+
"price": wb_serializers.decorator(position="left", value="{{underlying_instrument_currency}}"),
|
|
145
147
|
}
|
|
146
148
|
read_only_fields = (
|
|
147
149
|
"order_type",
|
|
148
150
|
"effective_shares",
|
|
149
151
|
"effective_total_value_fx_portfolio",
|
|
150
152
|
"has_warnings",
|
|
153
|
+
"desired_target_weight",
|
|
154
|
+
"daily_return",
|
|
155
|
+
"currency_fx_rate",
|
|
156
|
+
"price",
|
|
151
157
|
)
|
|
152
158
|
fields = (
|
|
153
159
|
"id",
|
|
@@ -171,9 +177,14 @@ class OrderOrderProposalListModelSerializer(wb_serializers.ModelSerializer):
|
|
|
171
177
|
"effective_total_value_fx_portfolio",
|
|
172
178
|
"target_total_value_fx_portfolio",
|
|
173
179
|
"portfolio_currency",
|
|
180
|
+
"underlying_instrument_currency",
|
|
174
181
|
"has_warnings",
|
|
175
182
|
"execution_confirmed",
|
|
176
183
|
"execution_comment",
|
|
184
|
+
"desired_target_weight",
|
|
185
|
+
"daily_return",
|
|
186
|
+
"currency_fx_rate",
|
|
187
|
+
"price",
|
|
177
188
|
)
|
|
178
189
|
|
|
179
190
|
|
|
@@ -157,7 +157,13 @@ class OrderOrderProposalDisplayConfig(DisplayViewConfig):
|
|
|
157
157
|
formatting_rules=ORDER_TYPE_FORMATTING_RULES,
|
|
158
158
|
width=Unit.PIXEL(125),
|
|
159
159
|
),
|
|
160
|
-
dp.Field(key="comment", label="Comment", width=Unit.PIXEL(250)),
|
|
160
|
+
dp.Field(key="comment", label="Comment", show="open", width=Unit.PIXEL(250)),
|
|
161
|
+
dp.Field(
|
|
162
|
+
key="desired_target_weight", label="Desired Target Weight", show="open", width=Unit.PIXEL(100)
|
|
163
|
+
),
|
|
164
|
+
dp.Field(key="daily_return", label="Daily Return", show="open", width=Unit.PIXEL(100)),
|
|
165
|
+
dp.Field(key="currency_fx_rate", label="FX Rate", show="open", width=Unit.PIXEL(100)),
|
|
166
|
+
dp.Field(key="price", label="Price", show="open", width=Unit.PIXEL(100)),
|
|
161
167
|
dp.Field(key="order", label="Order", show="open", width=Unit.PIXEL(100)),
|
|
162
168
|
],
|
|
163
169
|
)
|
|
@@ -2,7 +2,7 @@ from contextlib import suppress
|
|
|
2
2
|
from datetime import date
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
|
|
5
|
-
from django.contrib.messages import
|
|
5
|
+
from django.contrib.messages import warning
|
|
6
6
|
from django.shortcuts import get_object_or_404
|
|
7
7
|
from django.utils.functional import cached_property
|
|
8
8
|
from pandas._libs.tslibs.offsets import BDay
|
|
@@ -87,8 +87,6 @@ class OrderProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
87
87
|
|
|
88
88
|
def add_messages(self, request, instance: OrderProposal | None = None, **kwargs):
|
|
89
89
|
if instance:
|
|
90
|
-
if instance.status == OrderProposal.Status.APPROVED and not instance.portfolio.is_manageable:
|
|
91
|
-
info(request, "This order proposal cannot be approved the portfolio is considered unmanaged.")
|
|
92
90
|
if instance.status == OrderProposal.Status.PENDING and instance.has_non_successful_checks:
|
|
93
91
|
warning(
|
|
94
92
|
request,
|
|
@@ -235,6 +235,7 @@ class OrderOrderProposalModelViewSet(
|
|
|
235
235
|
effective_total_value_fx_portfolio=F("effective_weight") * Value(self.portfolio_total_asset_value),
|
|
236
236
|
target_total_value_fx_portfolio=F("target_weight") * Value(self.portfolio_total_asset_value),
|
|
237
237
|
portfolio_currency=F("portfolio__currency__symbol"),
|
|
238
|
+
underlying_instrument_currency=F("underlying_instrument__currency__symbol"),
|
|
238
239
|
security=F("underlying_instrument__parent"),
|
|
239
240
|
company=F("underlying_instrument__parent__parent"),
|
|
240
241
|
).select_related(
|
|
@@ -30,7 +30,7 @@ wbportfolio/admin/transactions/trades.py,sha256=LanpSm6iaR9cmNvSJwpRFPkOgN46K_Y9
|
|
|
30
30
|
wbportfolio/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
wbportfolio/analysis/claims.py,sha256=wcTniEVdDnmME8LHrYbo-WWpQ5EzNPgAT2KJIGQ0IIA,10688
|
|
32
32
|
wbportfolio/api_clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
wbportfolio/api_clients/ubs.py,sha256=
|
|
33
|
+
wbportfolio/api_clients/ubs.py,sha256=VSNEND87iHEpQgHY-erWAc4QMAa_BzrqYXdK9JHqQFM,6742
|
|
34
34
|
wbportfolio/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
35
|
wbportfolio/contrib/company_portfolio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
wbportfolio/contrib/company_portfolio/admin.py,sha256=roUsuOjYDWX0OGWVBjCcW3MAcwJHJYHnqZ_pLpwakmQ,549
|
|
@@ -118,7 +118,7 @@ wbportfolio/import_export/backends/wbfdm/dividend.py,sha256=iAQXnYPXmtG_Jrc8THAJ
|
|
|
118
118
|
wbportfolio/import_export/backends/wbfdm/mixin.py,sha256=JNtjgqGLson1nu_Chqb8MWyuiF3Ws8ox2vapxIRBYKE,400
|
|
119
119
|
wbportfolio/import_export/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
120
120
|
wbportfolio/import_export/handlers/adjustment.py,sha256=6bdTIYFmc8_HFxcdwtnYwglMyCfAD8XrTIrEb2zWY0g,1757
|
|
121
|
-
wbportfolio/import_export/handlers/asset_position.py,sha256=
|
|
121
|
+
wbportfolio/import_export/handlers/asset_position.py,sha256=ruZoAiwhDmJaiD-OVSZGJ66QrObBeU_JySxsxFmZY30,9305
|
|
122
122
|
wbportfolio/import_export/handlers/dividend.py,sha256=34iu1SfnyCVSObklo9eiF-ECTQhSvkiZn2pUmrWOaV4,4408
|
|
123
123
|
wbportfolio/import_export/handlers/fees.py,sha256=RoOaBwnIMzCx5q7qSzCQbS3L_eOa9Y-RSZ_ZG3QHbNo,2507
|
|
124
124
|
wbportfolio/import_export/handlers/orders.py,sha256=GU3_tIy-tAw9aU-ifsnmMZPBB9sqfkFC_S1d9VziTwg,3136
|
|
@@ -146,7 +146,7 @@ wbportfolio/import_export/parsers/natixis/d1_fees.py,sha256=RmzwlNqLSlGDp7JMTwHu
|
|
|
146
146
|
wbportfolio/import_export/parsers/natixis/d1_trade.py,sha256=JsAVgRH2iBo096Spz8ThJOGPpOCPyYrJ9GJ1XnzjBJQ,2092
|
|
147
147
|
wbportfolio/import_export/parsers/natixis/d1_valuation.py,sha256=M12am0ZenUJxkvEGp6tM1HfeukqYS3b6tfbyfC0XwvY,1216
|
|
148
148
|
wbportfolio/import_export/parsers/natixis/dividend.py,sha256=qx8DXu6NXMy1M_iunA4ITM845_6iNd1aoVCdoAKDNsY,1964
|
|
149
|
-
wbportfolio/import_export/parsers/natixis/equity.py,sha256=
|
|
149
|
+
wbportfolio/import_export/parsers/natixis/equity.py,sha256=oitpx_OYrsGbrYodoF37zjNzsigy2pkTRJ_E6TWpPYU,3341
|
|
150
150
|
wbportfolio/import_export/parsers/natixis/fees.py,sha256=oIC9moGm4s-6525q4_Syt4Yexj3SDgB5kYSfcsdzLcY,1666
|
|
151
151
|
wbportfolio/import_export/parsers/natixis/trade.py,sha256=BJum1W0fYe9emHU7RgL-AjHkiclrckQYkG8VkyCnfvM,2579
|
|
152
152
|
wbportfolio/import_export/parsers/natixis/utils.py,sha256=ImBHHLkC621ZrUDWI2p-F3AGElprT1FVb7xkPwKwvx0,2490
|
|
@@ -274,7 +274,7 @@ wbportfolio/migrations/0090_dividendtransaction_price_fx_portfolio_and_more.py,s
|
|
|
274
274
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
275
275
|
wbportfolio/models/__init__.py,sha256=qU4e7HKyh8NL_0Mg92PcbHTewCv7Ya2gei1DMGe1LWE,980
|
|
276
276
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
277
|
-
wbportfolio/models/asset.py,sha256=
|
|
277
|
+
wbportfolio/models/asset.py,sha256=zVJm3M5FIYYEMLl3mS0GxWfotadRgItOgIpNIto-0jw,39345
|
|
278
278
|
wbportfolio/models/builder.py,sha256=lVYW0iW8-RFL6OEpn1bC4VPUY89dSU1YNuFwC4yGIAo,14027
|
|
279
279
|
wbportfolio/models/custodians.py,sha256=QhSC3mfd6rSPp8wizabLbKmFDrrskZTSkxNdBJMdtCk,3815
|
|
280
280
|
wbportfolio/models/exceptions.py,sha256=EZnqSr5PxikiS4oDknRakEmlninJh_c-tOHRYv3IMjE,57
|
|
@@ -299,8 +299,8 @@ wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
299
299
|
wbportfolio/models/mixins/instruments.py,sha256=SuMPquQ93D4pZMK-4hQbJtV58_NOyf3wVOctQq7LNXQ,7054
|
|
300
300
|
wbportfolio/models/mixins/liquidity_stress_test.py,sha256=I_pgJ3QVFBtq0SeljJerhtlrZESRDNAe4On6QfMHvXc,58834
|
|
301
301
|
wbportfolio/models/orders/__init__.py,sha256=EH9UacGR3npBMje5FGTeLOh1xqFBh9kc24WbGmBIA3g,69
|
|
302
|
-
wbportfolio/models/orders/order_proposals.py,sha256=
|
|
303
|
-
wbportfolio/models/orders/orders.py,sha256
|
|
302
|
+
wbportfolio/models/orders/order_proposals.py,sha256=4t4IoZ8kqaDnQ1KrOoxinzbzabXnyMkTEV-lOt1G4Dc,59686
|
|
303
|
+
wbportfolio/models/orders/orders.py,sha256=v23DQmX2a_v2EKbdXYEQXruOqzbrGOvx6wNlNlPUu2o,12446
|
|
304
304
|
wbportfolio/models/orders/routing.py,sha256=7nu7-3zmGsVA3tyymKK_ywY7V7RtKkGcXkyk2V8dXMw,2191
|
|
305
305
|
wbportfolio/models/reconciliations/__init__.py,sha256=MXH5fZIPGDRBgJkO6wVu_NLRs8fkP1im7G6d-h36lQY,127
|
|
306
306
|
wbportfolio/models/reconciliations/account_reconciliation_lines.py,sha256=QP6M7hMcyFbuXBa55Y-azui6Dl_WgbzMntEqWzQkbfM,7394
|
|
@@ -337,7 +337,7 @@ wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
|
337
337
|
wbportfolio/risk_management/backends/__init__.py,sha256=4N3GYcIb0XgZUQWEHxCejOCRabUtLM6ha9UdOWJKUfI,368
|
|
338
338
|
wbportfolio/risk_management/backends/accounts.py,sha256=VMHwhzeFV2bzobb1RlEJadTVix1lkbbfdpo-zG3YN5c,7988
|
|
339
339
|
wbportfolio/risk_management/backends/controversy_portfolio.py,sha256=aoRt29QGFNWPf_7yr0Dpjv2AwsA0SJpZdNf8NCfe7JY,2843
|
|
340
|
-
wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=
|
|
340
|
+
wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=Tumcg47pX4dVTu1L67bSdgPC9QwOwDewLW_xtyKnAKE,10779
|
|
341
341
|
wbportfolio/risk_management/backends/instrument_list_portfolio.py,sha256=PS-OtT0ReFnZ-bsOCtAQhPjhE0Nc5AyEqy4gLya3fhk,4177
|
|
342
342
|
wbportfolio/risk_management/backends/liquidity_risk.py,sha256=OnuUypmN86RDnch1JOb8UJo5j1z1_X7sjazqAo7cbwM,3694
|
|
343
343
|
wbportfolio/risk_management/backends/liquidity_stress_instrument.py,sha256=oitzsaZu-HhYn9Avku3322GtDmf6QGsfyRzGPGZoM1Y,3612
|
|
@@ -376,7 +376,7 @@ wbportfolio/serializers/roles.py,sha256=T-9NqTldpvaEMFy-Bib5MB6MeboygEOqcMP61mzz
|
|
|
376
376
|
wbportfolio/serializers/signals.py,sha256=hD6R4oFtwhvnsJPteytPKy2JwEelmxrapdfoLSnluaE,7053
|
|
377
377
|
wbportfolio/serializers/orders/__init__.py,sha256=PKJRksA1pWsh8nVfGASoB0m3LyUzVRnq1m9VPp90J7k,271
|
|
378
378
|
wbportfolio/serializers/orders/order_proposals.py,sha256=Jxea2-Ze8Id5URv4UV-vTfCQGt11tjR27vRRfCs0gXU,4791
|
|
379
|
-
wbportfolio/serializers/orders/orders.py,sha256=
|
|
379
|
+
wbportfolio/serializers/orders/orders.py,sha256=WzuUDO907XG55NFMudVhJ5c2q7wrJbuquNzabGrD9yM,10030
|
|
380
380
|
wbportfolio/serializers/transactions/__init__.py,sha256=-7Pan4n7YI3iDvGXff6okzk4ycEURRxp5n_SHCY_g_I,493
|
|
381
381
|
wbportfolio/serializers/transactions/claim.py,sha256=mEt67F2v8HC6roemDT3S0dD0cZIVl1U9sASbLW3Vpyo,11611
|
|
382
382
|
wbportfolio/serializers/transactions/dividends.py,sha256=ADXf9cXe8rq55lC_a8vIzViGLmQ-yDXkgR54k2m-N0w,1814
|
|
@@ -548,15 +548,15 @@ wbportfolio/viewsets/configs/titles/registers.py,sha256=-C-YeGBhGva48oER6EIQ8CxW
|
|
|
548
548
|
wbportfolio/viewsets/configs/titles/roles.py,sha256=9LoJa3jgenXJ5UWRlIErTzdbjpSWMKsyJZtv-eDRTK4,739
|
|
549
549
|
wbportfolio/viewsets/configs/titles/trades.py,sha256=29XCLxvY0Xe3a2tjCno3tN2rRXCr9RWpbWnzurJfnYI,1986
|
|
550
550
|
wbportfolio/viewsets/orders/__init__.py,sha256=N8v9jdEXryOzrLlc7ML3iBCO2lmNXph9_TWoQ7PTvi4,195
|
|
551
|
-
wbportfolio/viewsets/orders/order_proposals.py,sha256=
|
|
552
|
-
wbportfolio/viewsets/orders/orders.py,sha256=
|
|
551
|
+
wbportfolio/viewsets/orders/order_proposals.py,sha256=0z85RnnsJPzH_LVBLqLYY1xuFExkYMbc1gSyNjpvD-4,8110
|
|
552
|
+
wbportfolio/viewsets/orders/orders.py,sha256=uNChI-EIenUZ5rlfKUQuM2VCQhDbCKPqdTWcszdDAxE,11767
|
|
553
553
|
wbportfolio/viewsets/orders/configs/__init__.py,sha256=5MU57JXiKi32_PicHtiNr7YHmMN020FrlF5NFJf_Wds,94
|
|
554
554
|
wbportfolio/viewsets/orders/configs/buttons/__init__.py,sha256=EHzNmAfa0UQFITEF-wxj_s4wn3Y5DE3DCbEUmmvCTIs,106
|
|
555
555
|
wbportfolio/viewsets/orders/configs/buttons/order_proposals.py,sha256=1BPkIYv0-K2DDGa4Gua2_Pxsx7fNurTZ2tYNdL66On0,6495
|
|
556
556
|
wbportfolio/viewsets/orders/configs/buttons/orders.py,sha256=GDO4Y33wkjhDxzpf7B1d_rKzAixegLv5rHam1DV3WkM,290
|
|
557
557
|
wbportfolio/viewsets/orders/configs/displays/__init__.py,sha256=__YJBbz_ZnKpE8WMMDR2PC9Nng-EVlRpGTEQucdrhRA,108
|
|
558
558
|
wbportfolio/viewsets/orders/configs/displays/order_proposals.py,sha256=zLoLEw4N1i_LD0e3hJnxzO8ORN2byFZoCxWnAz8DBx0,7362
|
|
559
|
-
wbportfolio/viewsets/orders/configs/displays/orders.py,sha256=
|
|
559
|
+
wbportfolio/viewsets/orders/configs/displays/orders.py,sha256=JHG7vax5cYWLfeq64U_4SDfBfixfBfE1FPZVBANMFIY,8105
|
|
560
560
|
wbportfolio/viewsets/orders/configs/endpoints/__init__.py,sha256=IB8GEadiEtBDclhkgpcJGXWfCF6qRK_42hxJ4pcdZDU,148
|
|
561
561
|
wbportfolio/viewsets/orders/configs/endpoints/order_proposals.py,sha256=fY3y2YWR9nY-KE8k078NszDsnwOgqOIjHUgLvQjDivU,822
|
|
562
562
|
wbportfolio/viewsets/orders/configs/endpoints/orders.py,sha256=Iic9RWLHiP2zxq6xve99lwVYCSMLX4T2euS7cJh-uOQ,1088
|
|
@@ -567,7 +567,7 @@ wbportfolio/viewsets/transactions/claim.py,sha256=Pb1WftoO-w-ZSTbLRhmQubhy7hgd68
|
|
|
567
567
|
wbportfolio/viewsets/transactions/fees.py,sha256=WT2bWWfgozz4_rpyTKX7dgBBTXD-gu0nlsd2Nk2Zh1Q,7028
|
|
568
568
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
569
569
|
wbportfolio/viewsets/transactions/trades.py,sha256=xBgOGaJ8aEg-2RxEJ4FDaBs4SGwuLasun3nhpis0WQY,12363
|
|
570
|
-
wbportfolio-1.56.
|
|
571
|
-
wbportfolio-1.56.
|
|
572
|
-
wbportfolio-1.56.
|
|
573
|
-
wbportfolio-1.56.
|
|
570
|
+
wbportfolio-1.56.4.dist-info/METADATA,sha256=HFEeWWRQErXdtvuxsVVPWmlUdvtmQPWGOrALtIaZ-lM,751
|
|
571
|
+
wbportfolio-1.56.4.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
572
|
+
wbportfolio-1.56.4.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
573
|
+
wbportfolio-1.56.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|