wbportfolio 1.56.3__py2.py3-none-any.whl → 1.56.5__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/contrib/company_portfolio/models.py +69 -39
- wbportfolio/contrib/company_portfolio/serializers.py +2 -4
- wbportfolio/contrib/company_portfolio/tasks.py +12 -1
- 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 +73 -25
- wbportfolio/models/orders/orders.py +71 -39
- wbportfolio/pms/trading/handler.py +4 -1
- wbportfolio/risk_management/backends/exposure_portfolio.py +1 -1
- wbportfolio/serializers/orders/orders.py +12 -1
- wbportfolio/tests/models/orders/test_order_proposals.py +8 -5
- wbportfolio/viewsets/orders/configs/displays/orders.py +7 -1
- wbportfolio/viewsets/orders/order_proposals.py +1 -3
- wbportfolio/viewsets/orders/orders.py +4 -3
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.5.dist-info}/METADATA +1 -1
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.5.dist-info}/RECORD +20 -20
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.5.dist-info}/WHEEL +0 -0
- {wbportfolio-1.56.3.dist-info → wbportfolio-1.56.5.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)
|
|
@@ -3,6 +3,7 @@ from datetime import date
|
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
|
|
5
5
|
from django.conf import settings
|
|
6
|
+
from django.core.cache import cache
|
|
6
7
|
from django.db import models
|
|
7
8
|
from django.db.models.signals import post_save
|
|
8
9
|
from django.dispatch import receiver
|
|
@@ -16,7 +17,12 @@ from wbportfolio.models import Claim, Product
|
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def get_total_assets_under_management(val_date: date) -> Decimal:
|
|
19
|
-
|
|
20
|
+
cache_key = f"total_assets_under_management:{val_date.isoformat()}"
|
|
21
|
+
return cache.get_or_set(
|
|
22
|
+
cache_key,
|
|
23
|
+
lambda: sum([product.get_total_aum_usd(val_date) for product in Product.active_objects.all()]),
|
|
24
|
+
60 * 60 * 24,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
def get_lost_client_customer_status():
|
|
@@ -44,9 +50,8 @@ class Updater:
|
|
|
44
50
|
self.val_date = val_date
|
|
45
51
|
self.total_assets_under_management = get_total_assets_under_management(val_date)
|
|
46
52
|
|
|
47
|
-
def update_company_data(self,
|
|
53
|
+
def update_company_data(self, company_portfolio_data) -> tuple[str, str]:
|
|
48
54
|
# save company portfolio data
|
|
49
|
-
company_portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
|
|
50
55
|
if (
|
|
51
56
|
invested_assets_under_management_usd := company_portfolio_data.get_assets_under_management_usd(
|
|
52
57
|
self.val_date
|
|
@@ -55,19 +60,11 @@ class Updater:
|
|
|
55
60
|
company_portfolio_data.invested_assets_under_management_usd = invested_assets_under_management_usd
|
|
56
61
|
if (potential := company_portfolio_data.get_potential(self.val_date)) is not None:
|
|
57
62
|
company_portfolio_data.potential = potential
|
|
58
|
-
company_portfolio_data.save()
|
|
59
63
|
|
|
60
64
|
# update the company object itself
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
company.save()
|
|
65
|
-
|
|
66
|
-
# def update_all_companies(self, val_date: date):
|
|
67
|
-
# for company in tqdm(qs, total=qs.count()):
|
|
68
|
-
# with suppress(CompanyPortfolioData.DoesNotExist):
|
|
69
|
-
# company_portfolio = CompanyPortfolioData.objects.get(company=company)
|
|
70
|
-
# company_portfolio.update_data(date.today())
|
|
65
|
+
tier = company_portfolio_data.get_tiering(self.total_assets_under_management)
|
|
66
|
+
customer_status = company_portfolio_data.get_customer_status()
|
|
67
|
+
return customer_status, tier
|
|
71
68
|
|
|
72
69
|
|
|
73
70
|
class CompanyPortfolioData(models.Model):
|
|
@@ -104,14 +101,6 @@ class CompanyPortfolioData(models.Model):
|
|
|
104
101
|
verbose_name="AUM",
|
|
105
102
|
help_text="The Assets under Management (AUM) that is managed by this company or this person's primary employer.",
|
|
106
103
|
)
|
|
107
|
-
invested_assets_under_management_usd = models.DecimalField(
|
|
108
|
-
max_digits=17,
|
|
109
|
-
decimal_places=2,
|
|
110
|
-
null=True,
|
|
111
|
-
blank=True,
|
|
112
|
-
help_text="The invested Assets under Management (AUM).",
|
|
113
|
-
verbose_name="Invested AUM ($)",
|
|
114
|
-
)
|
|
115
104
|
|
|
116
105
|
investment_discretion = models.CharField(
|
|
117
106
|
max_length=21,
|
|
@@ -120,10 +109,6 @@ class CompanyPortfolioData(models.Model):
|
|
|
120
109
|
help_text="What discretion this company or this person's primary employer has to invest its assets.",
|
|
121
110
|
verbose_name="Investment Discretion",
|
|
122
111
|
)
|
|
123
|
-
|
|
124
|
-
potential = models.DecimalField(
|
|
125
|
-
decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
|
|
126
|
-
)
|
|
127
112
|
potential_currency = models.ForeignKey(
|
|
128
113
|
to="currency.Currency",
|
|
129
114
|
related_name="wbportfolio_potential_currencies",
|
|
@@ -132,6 +117,25 @@ class CompanyPortfolioData(models.Model):
|
|
|
132
117
|
on_delete=models.PROTECT,
|
|
133
118
|
)
|
|
134
119
|
|
|
120
|
+
# Dynamic fields
|
|
121
|
+
invested_assets_under_management_usd = models.DecimalField(
|
|
122
|
+
max_digits=17,
|
|
123
|
+
decimal_places=2,
|
|
124
|
+
null=True,
|
|
125
|
+
blank=True,
|
|
126
|
+
help_text="The invested Assets under Management (AUM).",
|
|
127
|
+
verbose_name="Invested AUM ($)",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
potential = models.DecimalField(
|
|
131
|
+
decimal_places=2, max_digits=19, null=True, blank=True, help_text=potential_help_text
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def update(self):
|
|
135
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
136
|
+
val_date = CurrencyFXRates.objects.latest("date").date
|
|
137
|
+
self.company.customer_status, self.company.tier = Updater(val_date).update_company_data(self)
|
|
138
|
+
|
|
135
139
|
def get_assets_under_management_usd(self, val_date: date) -> Decimal:
|
|
136
140
|
return Claim.objects.filter(status=Claim.Status.APPROVED).filter_for_customer(
|
|
137
141
|
self.company
|
|
@@ -140,16 +144,17 @@ class CompanyPortfolioData(models.Model):
|
|
|
140
144
|
)["invested_aum_usd"] or Decimal(0)
|
|
141
145
|
|
|
142
146
|
def _get_default_potential(self, val_date: date) -> Decimal:
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
if self.assets_under_management:
|
|
148
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
149
|
+
fx = CurrencyFXRates.objects.get(currency=self.assets_under_management_currency, date=val_date).value
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
aum_usd = self.assets_under_management / fx
|
|
152
|
+
aum_potential = Decimal(0)
|
|
153
|
+
for asset_allocation in self.company.asset_allocations.all():
|
|
154
|
+
aum_potential += aum_usd * asset_allocation.percent * asset_allocation.max_investment
|
|
155
|
+
invested_aum = self.invested_assets_under_management_usd or Decimal(0.0)
|
|
151
156
|
|
|
152
|
-
|
|
157
|
+
return aum_potential - invested_aum
|
|
153
158
|
|
|
154
159
|
def get_potential(self, val_date: date) -> Decimal:
|
|
155
160
|
if module_path := getattr(settings, "PORTFOLIO_COMPANY_DATA_POTENTIAL_METHOD", None):
|
|
@@ -225,11 +230,6 @@ class CompanyPortfolioData(models.Model):
|
|
|
225
230
|
verbose_name_plural = "Company Portfolio Data"
|
|
226
231
|
|
|
227
232
|
|
|
228
|
-
@receiver(post_save, sender="directory.Company")
|
|
229
|
-
def create_company_portfolio_data(sender, instance, created, **kwargs):
|
|
230
|
-
CompanyPortfolioData.objects.get_or_create(company=instance)
|
|
231
|
-
|
|
232
|
-
|
|
233
233
|
class AssetAllocationType(WBModel):
|
|
234
234
|
name = models.CharField(max_length=255)
|
|
235
235
|
default_max_investment = models.DecimalField(
|
|
@@ -329,3 +329,33 @@ class GeographicFocus(models.Model):
|
|
|
329
329
|
@classmethod
|
|
330
330
|
def get_representation_label_key(cls):
|
|
331
331
|
return "{{company}}: {{percent}} {{country}}"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@receiver(post_save, sender=AssetAllocation)
|
|
335
|
+
@receiver(post_save, sender=GeographicFocus)
|
|
336
|
+
def post_save_company_data(sender, instance, created, **kwargs):
|
|
337
|
+
company = instance.company
|
|
338
|
+
portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=company)
|
|
339
|
+
if not created:
|
|
340
|
+
portfolio_data.update()
|
|
341
|
+
portfolio_data.save()
|
|
342
|
+
portfolio_data.company.save()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@receiver(post_save, sender="directory.Company")
|
|
346
|
+
def handle_company_portfolio_data(sender, instance, created, **kwargs):
|
|
347
|
+
# create default asset allocation type (equity 50/50)
|
|
348
|
+
if not instance.asset_allocations.exists():
|
|
349
|
+
equity_asset_type = AssetAllocationType.objects.get_or_create(name="Equity")[0]
|
|
350
|
+
AssetAllocation.objects.create(
|
|
351
|
+
company=instance,
|
|
352
|
+
asset_type=equity_asset_type,
|
|
353
|
+
percent=0.5,
|
|
354
|
+
max_investment=0.5,
|
|
355
|
+
)
|
|
356
|
+
if created:
|
|
357
|
+
portfolio_data, created = CompanyPortfolioData.objects.get_or_create(company=instance)
|
|
358
|
+
if not created:
|
|
359
|
+
portfolio_data.update()
|
|
360
|
+
portfolio_data.save()
|
|
361
|
+
portfolio_data.company.save()
|
|
@@ -91,7 +91,7 @@ def update_portfolio_data(company_portfolio_data, portfolio_data):
|
|
|
91
91
|
|
|
92
92
|
if investment_discretion := portfolio_data["investment_discretion"]:
|
|
93
93
|
company_portfolio_data.investment_discretion = investment_discretion
|
|
94
|
-
|
|
94
|
+
company_portfolio_data.update()
|
|
95
95
|
company_portfolio_data.save()
|
|
96
96
|
|
|
97
97
|
|
|
@@ -243,11 +243,9 @@ class PersonModelListSerializer(CompanyPortfolioDataMixin, BasePersonModelListSe
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
class AssetAllocationTypeRepresentationSerializer(serializers.RepresentationSerializer):
|
|
246
|
-
_detail = serializers.HyperlinkField(reverse_name="company_portfolio:assetallocationtyperepresentation-detail")
|
|
247
|
-
|
|
248
246
|
class Meta:
|
|
249
247
|
model = AssetAllocationType
|
|
250
|
-
fields = ("id", "name"
|
|
248
|
+
fields = ("id", "name")
|
|
251
249
|
|
|
252
250
|
|
|
253
251
|
class AssetAllocationTypeModelSerializer(serializers.ModelSerializer):
|
|
@@ -19,5 +19,16 @@ def update_all_portfolio_data(val_date: date | None = None):
|
|
|
19
19
|
has_account=Exists(Account.objects.filter(owner=OuterRef("pk"))),
|
|
20
20
|
has_portfolio_data=Exists(CompanyPortfolioData.objects.filter(company=OuterRef("pk"))),
|
|
21
21
|
)
|
|
22
|
+
company_objs = []
|
|
23
|
+
portfolio_data_objs = []
|
|
22
24
|
for company in tqdm(qs, total=qs.count()):
|
|
23
|
-
|
|
25
|
+
portfolio_data = CompanyPortfolioData.objects.get_or_create(company=company)[0]
|
|
26
|
+
company.customer_status, company.tier = updater.update_company_data(portfolio_data)
|
|
27
|
+
portfolio_data_objs.append(portfolio_data)
|
|
28
|
+
company_objs.append(company)
|
|
29
|
+
if company_objs:
|
|
30
|
+
Company.objects.bulk_update(company_objs, ["customer_status", "tier"])
|
|
31
|
+
if portfolio_data_objs:
|
|
32
|
+
CompanyPortfolioData.objects.bulk_update(
|
|
33
|
+
portfolio_data_objs, ["invested_assets_under_management_usd", "potential"]
|
|
34
|
+
)
|
|
@@ -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
|
|
@@ -172,7 +172,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
172
172
|
|
|
173
173
|
@cached_property
|
|
174
174
|
def portfolio_total_asset_value(self) -> Decimal:
|
|
175
|
-
return self.
|
|
175
|
+
return self.get_portfolio_total_asset_value()
|
|
176
176
|
|
|
177
177
|
@cached_property
|
|
178
178
|
def validated_trading_service(self) -> TradingService:
|
|
@@ -248,6 +248,25 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
248
248
|
# TODO make this dynamically configurable
|
|
249
249
|
return "MARKET_ON_CLOSE"
|
|
250
250
|
|
|
251
|
+
def get_portfolio_total_asset_value(self):
|
|
252
|
+
return self.portfolio.get_total_asset_value(self.last_effective_date)
|
|
253
|
+
# return self.orders.annotate(
|
|
254
|
+
# effective_shares=Coalesce(
|
|
255
|
+
# Subquery(
|
|
256
|
+
# AssetPosition.objects.filter(
|
|
257
|
+
# underlying_quote=OuterRef("underlying_instrument"),
|
|
258
|
+
# date=self.last_effective_date,
|
|
259
|
+
# portfolio=self.portfolio,
|
|
260
|
+
# )
|
|
261
|
+
# .values("portfolio")
|
|
262
|
+
# .annotate(s=Sum("shares"))
|
|
263
|
+
# .values("s")[:1]
|
|
264
|
+
# ),
|
|
265
|
+
# Decimal(0),
|
|
266
|
+
# ),
|
|
267
|
+
# effective_total_value_fx_portfolio=F("effective_shares") * F("currency_fx_rate") * F("price"),
|
|
268
|
+
# ).aggregate(s=Sum("effective_total_value_fx_portfolio"))["s"] or Decimal(0.0)
|
|
269
|
+
|
|
251
270
|
def get_orders(self):
|
|
252
271
|
# TODO Issue here: the cash is subqueried on the portfolio, on portfolio such as the fund, there is multiple cash component, that we exclude in the orders (and use a unique cash position instead)
|
|
253
272
|
# so the subquery returns the previous position (probably USD), but is missing the other cash aggregation. We need to find a way to handle that properly
|
|
@@ -428,7 +447,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
428
447
|
|
|
429
448
|
# 3. Prepare a mapping from instrument IDs to weights for analytic calculations.
|
|
430
449
|
previous_weights = {instrument.id: float(info["weighting"]) for instrument, info in portfolio.items()}
|
|
431
|
-
|
|
432
450
|
# 4. Attempt to fetch analytic returns and portfolio contribution. Default on error.
|
|
433
451
|
try:
|
|
434
452
|
last_returns, contribution = self.portfolio.get_analytic_portfolio(
|
|
@@ -437,7 +455,6 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
437
455
|
last_returns = last_returns.to_dict()
|
|
438
456
|
except ValueError:
|
|
439
457
|
last_returns, contribution = {}, 1
|
|
440
|
-
|
|
441
458
|
positions = []
|
|
442
459
|
total_weighting = Decimal("0")
|
|
443
460
|
|
|
@@ -458,6 +475,27 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
458
475
|
weighting = drifted_weight + row["delta_weight"]
|
|
459
476
|
|
|
460
477
|
# Assemble the position object
|
|
478
|
+
trade_date = self.last_effective_date if use_effective else self.trade_date
|
|
479
|
+
price_data = {}
|
|
480
|
+
with suppress(InstrumentPrice.DoesNotExist):
|
|
481
|
+
instrument_price = instrument.valuations.get(date=trade_date)
|
|
482
|
+
if fx_rate := instrument_price.currency_fx_rate_to_usd:
|
|
483
|
+
price_data = {
|
|
484
|
+
"volume_usd": instrument_price.volume
|
|
485
|
+
* float(instrument_price.net_value)
|
|
486
|
+
/ float(fx_rate.value)
|
|
487
|
+
}
|
|
488
|
+
if instrument_price.market_capitalization:
|
|
489
|
+
price_data["market_capitalization_usd"] = instrument_price.market_capitalization / float(
|
|
490
|
+
fx_rate.value
|
|
491
|
+
)
|
|
492
|
+
if row["shares"] is not None:
|
|
493
|
+
if instrument_price.market_capitalization:
|
|
494
|
+
price_data["market_share"] = (
|
|
495
|
+
float(row["shares"]) * float(row["price"]) / instrument_price.market_capitalization
|
|
496
|
+
)
|
|
497
|
+
if instrument_price.volume_50d:
|
|
498
|
+
price_data["daily_liquidity"] = float(row["shares"]) / instrument_price.volume_50d / 0.33
|
|
461
499
|
positions.append(
|
|
462
500
|
PositionDTO(
|
|
463
501
|
underlying_instrument=instrument.id,
|
|
@@ -466,10 +504,14 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
466
504
|
daily_return=daily_return if use_effective else Decimal("0"),
|
|
467
505
|
shares=row["shares"],
|
|
468
506
|
currency=instrument.currency.id,
|
|
469
|
-
date=
|
|
507
|
+
date=trade_date,
|
|
508
|
+
asset_valuation_date=trade_date,
|
|
470
509
|
is_cash=instrument.is_cash or instrument.is_cash_equivalent,
|
|
471
510
|
price=row["price"],
|
|
472
511
|
currency_fx_rate=row["currency_fx_rate"],
|
|
512
|
+
exchange=instrument.exchange.id if instrument.exchange else None,
|
|
513
|
+
country=instrument.country.id if instrument.country else None,
|
|
514
|
+
**price_data,
|
|
473
515
|
)
|
|
474
516
|
)
|
|
475
517
|
total_weighting += weighting
|
|
@@ -584,12 +626,16 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
584
626
|
target_portfolio = self._get_default_target_portfolio(use_desired_target_weight=use_desired_target_weight)
|
|
585
627
|
if not effective_portfolio:
|
|
586
628
|
effective_portfolio = self._get_default_effective_portfolio()
|
|
629
|
+
if use_desired_target_weight:
|
|
630
|
+
total_target_weight = target_portfolio.total_weight
|
|
631
|
+
else:
|
|
632
|
+
total_target_weight = self.total_expected_target_weight
|
|
587
633
|
if target_portfolio:
|
|
588
634
|
service = TradingService(
|
|
589
635
|
self.trade_date,
|
|
590
636
|
effective_portfolio=effective_portfolio,
|
|
591
637
|
target_portfolio=target_portfolio,
|
|
592
|
-
total_target_weight=
|
|
638
|
+
total_target_weight=total_target_weight,
|
|
593
639
|
)
|
|
594
640
|
if validate_order:
|
|
595
641
|
service.is_valid()
|
|
@@ -601,15 +647,11 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
601
647
|
portfolio_value = self.portfolio_total_asset_value
|
|
602
648
|
for order_dto in orders:
|
|
603
649
|
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
650
|
# we cannot do a bulk-create because Order is a multi table inheritance
|
|
608
651
|
weighting = round(order_dto.delta_weight, Order.ORDER_WEIGHTING_PRECISION)
|
|
609
652
|
daily_return = order_dto.daily_return
|
|
610
653
|
try:
|
|
611
654
|
order = self.orders.get(underlying_instrument=instrument)
|
|
612
|
-
order.currency_fx_rate = currency_fx_rate
|
|
613
655
|
order.daily_return = daily_return
|
|
614
656
|
except Order.DoesNotExist:
|
|
615
657
|
order = Order(
|
|
@@ -618,18 +660,18 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
618
660
|
value_date=self.trade_date,
|
|
619
661
|
weighting=weighting,
|
|
620
662
|
daily_return=daily_return,
|
|
621
|
-
currency_fx_rate=currency_fx_rate,
|
|
622
663
|
)
|
|
623
|
-
order.pre_save()
|
|
624
|
-
order.set_weighting(weighting, portfolio_value)
|
|
625
|
-
order.desired_target_weight = order_dto.target_weight
|
|
626
664
|
order.order_type = Order.get_type(
|
|
627
665
|
weighting, round(order_dto.previous_weight, 8), round(order_dto.target_weight, 8)
|
|
628
666
|
)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
667
|
+
order.pre_save()
|
|
668
|
+
order.set_weighting(weighting, portfolio_value)
|
|
669
|
+
order.desired_target_weight = order_dto.target_weight
|
|
670
|
+
|
|
671
|
+
# if we cannot automatically find a price, we consider the stock is invalid and we sell it
|
|
672
|
+
if not order.price and order.weighting > 0:
|
|
673
|
+
order.price = Decimal("0.0")
|
|
674
|
+
order.weighting = -order_dto.effective_weight
|
|
633
675
|
objs.append(order)
|
|
634
676
|
Order.objects.bulk_create(
|
|
635
677
|
objs,
|
|
@@ -916,7 +958,7 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
916
958
|
# "There is no change detected in this order proposal. Please submit at last one valid order"
|
|
917
959
|
# )
|
|
918
960
|
if len(service.validated_trades) == 0:
|
|
919
|
-
errors_list.append(
|
|
961
|
+
errors_list.append(gettext_lazy("There is no valid order on this proposal"))
|
|
920
962
|
if service.errors:
|
|
921
963
|
errors_list.extend(service.errors)
|
|
922
964
|
if errors_list:
|
|
@@ -1043,22 +1085,26 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
1043
1085
|
errors = dict()
|
|
1044
1086
|
orders = self.get_orders()
|
|
1045
1087
|
if not self.portfolio.can_be_rebalanced:
|
|
1046
|
-
errors["non_field_errors"] = [
|
|
1088
|
+
errors["non_field_errors"] = [gettext_lazy("The portfolio does not allow manual rebalanced")]
|
|
1047
1089
|
if not orders.exists():
|
|
1048
1090
|
errors["non_field_errors"] = [
|
|
1049
|
-
|
|
1091
|
+
gettext_lazy("At least one order needs to be submitted to be able to apply this proposal")
|
|
1050
1092
|
]
|
|
1051
1093
|
if not self.portfolio.can_be_rebalanced:
|
|
1052
1094
|
errors["portfolio"] = [
|
|
1053
|
-
[
|
|
1095
|
+
[
|
|
1096
|
+
gettext_lazy(
|
|
1097
|
+
"The portfolio needs to be a model portfolio in order to apply this order proposal manually"
|
|
1098
|
+
)
|
|
1099
|
+
]
|
|
1054
1100
|
]
|
|
1055
1101
|
if self.has_non_successful_checks:
|
|
1056
|
-
errors["non_field_errors"] = [
|
|
1102
|
+
errors["non_field_errors"] = [gettext_lazy("The pre orders rules did not passed successfully")]
|
|
1057
1103
|
if orders.filter(has_warnings=True).filter(
|
|
1058
1104
|
underlying_instrument__is_cash=False, underlying_instrument__is_cash_equivalent=False
|
|
1059
1105
|
):
|
|
1060
1106
|
errors["non_field_errors"] = [
|
|
1061
|
-
|
|
1107
|
+
gettext_lazy("There is warning that needs to be addresses on the orders before approval.")
|
|
1062
1108
|
]
|
|
1063
1109
|
return errors
|
|
1064
1110
|
|
|
@@ -1122,7 +1168,9 @@ class OrderProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
|
1122
1168
|
errors = dict()
|
|
1123
1169
|
if not self.portfolio.can_be_rebalanced:
|
|
1124
1170
|
errors["portfolio"] = [
|
|
1125
|
-
|
|
1171
|
+
gettext_lazy(
|
|
1172
|
+
"The portfolio needs to be a model portfolio in order to revert this order proposal manually"
|
|
1173
|
+
)
|
|
1126
1174
|
]
|
|
1127
1175
|
return errors
|
|
1128
1176
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
1
2
|
from datetime import date
|
|
2
3
|
from decimal import Decimal
|
|
3
4
|
|
|
@@ -7,7 +8,9 @@ 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
|
|
13
|
+
from wbfdm.models import Instrument
|
|
11
14
|
|
|
12
15
|
from wbportfolio.import_export.handlers.orders import OrderImportHandler
|
|
13
16
|
from wbportfolio.models.asset import AssetPosition
|
|
@@ -137,12 +140,7 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
137
140
|
return getattr(
|
|
138
141
|
self,
|
|
139
142
|
"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),
|
|
143
|
+
self.get_effective_shares(),
|
|
146
144
|
)
|
|
147
145
|
|
|
148
146
|
@property
|
|
@@ -164,29 +162,31 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
164
162
|
def pre_save(self):
|
|
165
163
|
self.portfolio = self.order_proposal.portfolio
|
|
166
164
|
self.value_date = self.order_proposal.trade_date
|
|
165
|
+
self.set_currency_fx_rate()
|
|
167
166
|
|
|
168
167
|
if not self.price:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
self.price = Decimal("1")
|
|
172
|
-
else:
|
|
173
|
-
self.price = self.get_price()
|
|
174
|
-
if not self.portfolio.only_weighting:
|
|
168
|
+
self.set_price()
|
|
169
|
+
if not self.portfolio.only_weighting and not self.shares:
|
|
175
170
|
estimated_shares = self.order_proposal.get_estimated_shares(
|
|
176
171
|
self.weighting, self.underlying_instrument, self.price
|
|
177
172
|
)
|
|
178
173
|
if estimated_shares:
|
|
179
174
|
self.shares = estimated_shares
|
|
175
|
+
if effective_shares := self.get_effective_shares():
|
|
176
|
+
if self.order_type == self.Type.SELL:
|
|
177
|
+
self.shares = -effective_shares
|
|
178
|
+
else:
|
|
179
|
+
self.shares = max(self.shares, -effective_shares)
|
|
180
180
|
super().pre_save()
|
|
181
181
|
|
|
182
182
|
def save(self, *args, **kwargs):
|
|
183
|
+
if self.id:
|
|
184
|
+
self.set_type()
|
|
183
185
|
self.pre_save()
|
|
184
186
|
if not self.underlying_instrument.is_investable_universe:
|
|
185
187
|
self.underlying_instrument.is_investable_universe = True
|
|
186
188
|
self.underlying_instrument.save()
|
|
187
189
|
|
|
188
|
-
if self.id:
|
|
189
|
-
self.set_type()
|
|
190
190
|
super().save(*args, **kwargs)
|
|
191
191
|
|
|
192
192
|
@classmethod
|
|
@@ -205,14 +205,46 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
205
205
|
else:
|
|
206
206
|
return Order.Type.SELL
|
|
207
207
|
|
|
208
|
-
def
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
def get_effective_shares(self) -> Decimal:
|
|
209
|
+
return AssetPosition.objects.filter(
|
|
210
|
+
underlying_quote=self.underlying_instrument,
|
|
211
|
+
date=self.order_proposal.last_effective_date,
|
|
212
|
+
portfolio=self.portfolio,
|
|
213
|
+
).aggregate(s=Sum("shares"))["s"] or Decimal("0")
|
|
213
214
|
|
|
214
215
|
def set_type(self):
|
|
215
|
-
|
|
216
|
+
effective_weight = self._effective_weight
|
|
217
|
+
self.order_type = self.get_type(self.weighting, effective_weight, effective_weight + self.weighting)
|
|
218
|
+
|
|
219
|
+
def set_price(self):
|
|
220
|
+
daily_return = last_price = Decimal("0")
|
|
221
|
+
|
|
222
|
+
effective_date = self.order_proposal.last_effective_date
|
|
223
|
+
if self.underlying_instrument.is_cash or self.underlying_instrument.is_cash_equivalent:
|
|
224
|
+
last_price = Decimal("1")
|
|
225
|
+
else:
|
|
226
|
+
prices, returns = Instrument.objects.filter(id=self.underlying_instrument.id).get_returns_df(
|
|
227
|
+
from_date=effective_date,
|
|
228
|
+
to_date=self.value_date,
|
|
229
|
+
to_currency=self.order_proposal.portfolio.currency,
|
|
230
|
+
use_dl=True,
|
|
231
|
+
)
|
|
232
|
+
with suppress(IndexError):
|
|
233
|
+
daily_return = Decimal(returns.iloc[-1, 0])
|
|
234
|
+
with suppress(KeyError):
|
|
235
|
+
last_price = Decimal(
|
|
236
|
+
prices.get(self.value_date, prices[effective_date])[self.underlying_instrument.id]
|
|
237
|
+
)
|
|
238
|
+
self.daily_return = daily_return
|
|
239
|
+
self.price = last_price
|
|
240
|
+
|
|
241
|
+
def set_currency_fx_rate(self):
|
|
242
|
+
self.currency_fx_rate = Decimal("1")
|
|
243
|
+
if self.order_proposal.portfolio.currency != self.underlying_instrument.currency:
|
|
244
|
+
with suppress(CurrencyFXRates.DoesNotExist):
|
|
245
|
+
self.currency_fx_rate = self.underlying_instrument.currency.convert(
|
|
246
|
+
self.value_date, self.portfolio.currency, exact_lookup=True
|
|
247
|
+
)
|
|
216
248
|
|
|
217
249
|
def set_weighting(self, weighting: Decimal, portfolio_value: Decimal):
|
|
218
250
|
self.weighting = weighting
|
|
@@ -242,28 +274,28 @@ class Order(TransactionMixin, ImportMixin, OrderedModel, models.Model):
|
|
|
242
274
|
|
|
243
275
|
def submit(self, by=None, description=None, portfolio_total_asset_value=None, **kwargs):
|
|
244
276
|
warnings = []
|
|
245
|
-
|
|
246
277
|
# 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
|
-
|
|
278
|
+
if self._target_weight:
|
|
279
|
+
if self.order_proposal and not self.portfolio.only_weighting:
|
|
280
|
+
shares = self.order_proposal.get_round_lot_size(self.shares, self.underlying_instrument)
|
|
281
|
+
if shares != self.shares:
|
|
282
|
+
warnings.append(
|
|
283
|
+
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}"
|
|
284
|
+
)
|
|
285
|
+
shares = round(shares) # ensure fractional shares are converted into integer
|
|
286
|
+
# we need to recompute the delta weight has we changed the number of shares
|
|
287
|
+
if shares != self.shares:
|
|
288
|
+
self.set_shares(shares, portfolio_total_asset_value)
|
|
289
|
+
if abs(self.weighting) < self.order_proposal.min_weighting:
|
|
250
290
|
warnings.append(
|
|
251
|
-
f"{self.underlying_instrument.computed_str}
|
|
291
|
+
f"Weighting for order {self.underlying_instrument.computed_str} ({self.weighting}) is bellow the allowed Minimum Weighting ({self.order_proposal.min_weighting})"
|
|
252
292
|
)
|
|
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)
|
|
293
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
294
|
+
if self.shares and abs(self.total_value_fx_portfolio) < self.order_proposal.min_order_value:
|
|
295
|
+
warnings.append(
|
|
296
|
+
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})"
|
|
297
|
+
)
|
|
298
|
+
self.set_weighting(Decimal("0"), portfolio_total_asset_value)
|
|
267
299
|
if not self.price:
|
|
268
300
|
warnings.append(f"No price for {self.underlying_instrument.computed_str}")
|
|
269
301
|
if (
|
|
@@ -145,17 +145,20 @@ class TradingService:
|
|
|
145
145
|
previous_weight = target_weight = 0
|
|
146
146
|
effective_shares = target_shares = 0
|
|
147
147
|
daily_return = 0
|
|
148
|
+
price = Decimal("0")
|
|
148
149
|
is_cash = False
|
|
149
150
|
if effective_pos := effective_portfolio.positions_map.get(instrument_id, None):
|
|
150
151
|
previous_weight = effective_pos.weighting
|
|
151
152
|
effective_shares = effective_pos.shares
|
|
152
153
|
daily_return = effective_pos.daily_return
|
|
153
154
|
is_cash = effective_pos.is_cash
|
|
155
|
+
price = effective_pos.price
|
|
154
156
|
if target_pos := target_portfolio.positions_map.get(instrument_id, None):
|
|
155
157
|
target_weight = target_pos.weighting
|
|
156
158
|
is_cash = target_pos.is_cash
|
|
157
159
|
if target_pos.shares is not None:
|
|
158
160
|
target_shares = target_pos.shares
|
|
161
|
+
price = target_pos.price
|
|
159
162
|
trade = Trade(
|
|
160
163
|
underlying_instrument=instrument_id,
|
|
161
164
|
previous_weight=previous_weight,
|
|
@@ -165,7 +168,7 @@ class TradingService:
|
|
|
165
168
|
date=self.trade_date,
|
|
166
169
|
instrument_type=pos.instrument_type,
|
|
167
170
|
currency=pos.currency,
|
|
168
|
-
price=Decimal(
|
|
171
|
+
price=Decimal(price) if price else Decimal("0"),
|
|
169
172
|
currency_fx_rate=Decimal(pos.currency_fx_rate),
|
|
170
173
|
daily_return=Decimal(daily_return),
|
|
171
174
|
portfolio_contribution=effective_portfolio.portfolio_contribution,
|
|
@@ -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", "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
|
|
|
@@ -422,7 +422,7 @@ class TestOrderProposal:
|
|
|
422
422
|
mock_fct.assert_has_calls(expected_calls)
|
|
423
423
|
|
|
424
424
|
# Test estimating shares for a trade
|
|
425
|
-
@patch.object(
|
|
425
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
426
426
|
def test_get_estimated_shares(
|
|
427
427
|
self, mock_fct, order_proposal, order_factory, instrument_price_factory, instrument_factory
|
|
428
428
|
):
|
|
@@ -449,7 +449,7 @@ class TestOrderProposal:
|
|
|
449
449
|
== Decimal(1_000_000) * trade.weighting / underlying_quote_price.net_value
|
|
450
450
|
)
|
|
451
451
|
|
|
452
|
-
@patch.object(
|
|
452
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
453
453
|
def test_get_estimated_target_cash(self, mock_fct, order_proposal, order_factory, cash_factory):
|
|
454
454
|
order_proposal.portfolio.only_weighting = False
|
|
455
455
|
order_proposal.portfolio.save()
|
|
@@ -500,7 +500,7 @@ class TestOrderProposal:
|
|
|
500
500
|
instrument.exchange.save()
|
|
501
501
|
assert order_proposal.get_round_lot_size(Decimal("66"), instrument) == Decimal("66")
|
|
502
502
|
|
|
503
|
-
@patch.object(
|
|
503
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
504
504
|
def test_submit_round_lot_size(self, mock_fct, order_proposal, instrument_price_factory, order_factory):
|
|
505
505
|
initial_shares = Decimal("70")
|
|
506
506
|
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
@@ -528,8 +528,10 @@ class TestOrderProposal:
|
|
|
528
528
|
trade.refresh_from_db()
|
|
529
529
|
assert trade.shares == 100 # we expect the share to be transformed from 70 to 100 (lot size of 100)
|
|
530
530
|
|
|
531
|
-
@patch.object(
|
|
532
|
-
def test_submit_round_fractional_shares(
|
|
531
|
+
@patch.object(OrderProposal, "get_portfolio_total_asset_value")
|
|
532
|
+
def test_submit_round_fractional_shares(
|
|
533
|
+
self, mock_fct, instrument_price_factory, order_proposal, order_factory, asset_position_factory
|
|
534
|
+
):
|
|
533
535
|
initial_shares = Decimal("5.6")
|
|
534
536
|
price = instrument_price_factory.create(date=order_proposal.trade_date)
|
|
535
537
|
net_value = round(price.net_value, 4)
|
|
@@ -538,6 +540,7 @@ class TestOrderProposal:
|
|
|
538
540
|
|
|
539
541
|
order_proposal.portfolio.only_weighting = False
|
|
540
542
|
order_proposal.portfolio.save()
|
|
543
|
+
|
|
541
544
|
trade = order_factory.create(
|
|
542
545
|
shares=Decimal("5.6"),
|
|
543
546
|
order_proposal=order_proposal,
|
|
@@ -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,
|
|
@@ -92,10 +92,10 @@ class OrderOrderProposalModelViewSet(
|
|
|
92
92
|
sum_effective_total_value_fx_portfolio=Sum(F("effective_total_value_fx_portfolio")),
|
|
93
93
|
)
|
|
94
94
|
# weights aggregates
|
|
95
|
-
cash_sum_effective_weight = (
|
|
96
|
-
|
|
95
|
+
cash_sum_effective_weight = self.order_proposal.total_effective_portfolio_weight - (
|
|
96
|
+
noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
97
97
|
)
|
|
98
|
-
cash_sum_target_cash_weight = Decimal("1.0") - noncash_aggregates["sum_target_weight"]
|
|
98
|
+
cash_sum_target_cash_weight = Decimal("1.0") - (noncash_aggregates["sum_target_weight"] or Decimal(0))
|
|
99
99
|
noncash_sum_effective_weight = noncash_aggregates["sum_effective_weight"] or Decimal(0)
|
|
100
100
|
noncash_sum_target_weight = noncash_aggregates["sum_target_weight"] or Decimal(0)
|
|
101
101
|
sum_buy_weight = queryset.filter(weighting__gte=0).aggregate(s=Sum(F("weighting")))["s"] or Decimal(0)
|
|
@@ -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
|
|
@@ -40,10 +40,10 @@ wbportfolio/contrib/company_portfolio/dynamic_preferences_registry.py,sha256=k69
|
|
|
40
40
|
wbportfolio/contrib/company_portfolio/factories.py,sha256=LvEYhBURUz3aK5KtTshBZpU66ph_DbLXKKDzf3Sw4Vo,1243
|
|
41
41
|
wbportfolio/contrib/company_portfolio/filters.py,sha256=jLDo3PRhofuqR2EA7G2R2aiAd_pTCZO4BvUZh0AN1HQ,4995
|
|
42
42
|
wbportfolio/contrib/company_portfolio/management.py,sha256=CofBifGGGKfA06uWRDWXY_9y7y-PGNRz3VhsVaDeAz8,857
|
|
43
|
-
wbportfolio/contrib/company_portfolio/models.py,sha256=
|
|
43
|
+
wbportfolio/contrib/company_portfolio/models.py,sha256=0YlWZfpBTUy4nCE0FYY6Kqcx3H28-AbeIVXgxcF6hM0,13963
|
|
44
44
|
wbportfolio/contrib/company_portfolio/scripts.py,sha256=wFfc1L-it0UTaBwNFs8hhGARbJoaNzmnlsXh6KEbslU,2497
|
|
45
|
-
wbportfolio/contrib/company_portfolio/serializers.py,sha256=
|
|
46
|
-
wbportfolio/contrib/company_portfolio/tasks.py,sha256=
|
|
45
|
+
wbportfolio/contrib/company_portfolio/serializers.py,sha256=i4lXu79Mkr_b4zOO4iQ73Tl6DucLK7Aj9npaG8ox7js,12067
|
|
46
|
+
wbportfolio/contrib/company_portfolio/tasks.py,sha256=nIBTqC36JCakCug7YVt1aBqai5bKDsgZd2HS7Jwpf5U,1384
|
|
47
47
|
wbportfolio/contrib/company_portfolio/urls.py,sha256=eyq6IpaDPG1oL8-AqPS5nC5BeFewKPA1BN_HI7M3i1c,1118
|
|
48
48
|
wbportfolio/contrib/company_portfolio/viewsets.py,sha256=o-_D_bEt283xngDbsMwdRQxFyuK7rfSChI8u5dGYyLM,8312
|
|
49
49
|
wbportfolio/contrib/company_portfolio/configs/__init__.py,sha256=AVH0v-uOJBPRKggz7fO7IpRyVJ1YlQiF-if_cMsOvvM,72
|
|
@@ -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=bg0Xv5XKwcSa1cAcGkn4g-krYEBPrpjJ4D5Rh7W0dYU,60716
|
|
303
|
+
wbportfolio/models/orders/orders.py,sha256=zKSMUJV3OqSbqokF2EoxAAGVhP08jZQjayV9IfkNASQ,13188
|
|
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
|
|
@@ -322,7 +322,7 @@ wbportfolio/pms/analytics/portfolio.py,sha256=u_S-e6HUQwAyq90gweDmxyTHWrIc5nd84s
|
|
|
322
322
|
wbportfolio/pms/analytics/utils.py,sha256=EfhKdo9B2ABaUPppb8DgZSqpNkSze8Rjej1xDjv-XcQ,282
|
|
323
323
|
wbportfolio/pms/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
324
324
|
wbportfolio/pms/trading/__init__.py,sha256=R_yLKc54sCak8A1cW0O1Aszrcv5KV8mC_3h17Hr20e4,36
|
|
325
|
-
wbportfolio/pms/trading/handler.py,sha256=
|
|
325
|
+
wbportfolio/pms/trading/handler.py,sha256=p56de1nm-QZh7tfQ7uITp1HuVE2XRaGj4T3oYBtp4Iw,8744
|
|
326
326
|
wbportfolio/rebalancing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
327
327
|
wbportfolio/rebalancing/base.py,sha256=wpeoxdkLz5osxm5mRjkOoML7YkYvwuAlqSLLtHBbWp8,984
|
|
328
328
|
wbportfolio/rebalancing/decorators.py,sha256=162ZmXV2YQGI830LWvEnJ95RexMzHvaCGfcVOnXTOXM,502
|
|
@@ -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=7FOjeT-YAACzKjnJ2Ki2ads7wMq2Td8Zy7ot4TvJ8qs,10014
|
|
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
|
|
@@ -420,7 +420,7 @@ wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo
|
|
|
420
420
|
wbportfolio/tests/models/test_splits.py,sha256=0PvW6WunBByDYRhtFV2LbK-DV92q-1s3kn45NvBL5Es,9500
|
|
421
421
|
wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
|
|
422
422
|
wbportfolio/tests/models/orders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
423
|
-
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=
|
|
423
|
+
wbportfolio/tests/models/orders/test_order_proposals.py,sha256=LjQCw10kA1C_21sI_Mm1pkvQLd7fEDHSwODcdt6HGpc,36605
|
|
424
424
|
wbportfolio/tests/models/transactions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
425
425
|
wbportfolio/tests/models/transactions/test_claim.py,sha256=NG3BKB-FVcIDgHSJHCjImxgMM3ISVUMl24xUPmEcPec,5570
|
|
426
426
|
wbportfolio/tests/models/transactions/test_fees.py,sha256=tAp18x2wCNQr11LUnLtHNbBDbbX0v1DZnmW7i-cEi5Q,2423
|
|
@@ -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=rZ0u1_FnmtJxUM4dkC3Y4BzS6KY1iZRyuNGXitKAies,11797
|
|
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.5.dist-info/METADATA,sha256=RROv2cPMXS4c5q03yrXpQTM5ncGQRYdjH8PR-i62Hfw,751
|
|
571
|
+
wbportfolio-1.56.5.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
572
|
+
wbportfolio-1.56.5.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
573
|
+
wbportfolio-1.56.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|