wbportfolio 1.47.0__py2.py3-none-any.whl → 1.48.0__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/contrib/company_portfolio/tasks.py +8 -4
- wbportfolio/dynamic_preferences_registry.py +9 -0
- wbportfolio/import_export/resources/trades.py +19 -30
- wbportfolio/metric/backends/base.py +3 -15
- wbportfolio/models/__init__.py +1 -2
- wbportfolio/models/asset.py +1 -1
- wbportfolio/models/portfolio.py +12 -0
- wbportfolio/models/products.py +50 -1
- wbportfolio/models/transactions/trade_proposals.py +28 -20
- wbportfolio/models/transactions/trades.py +18 -14
- wbportfolio/preferences.py +6 -1
- wbportfolio/risk_management/backends/accounts.py +14 -6
- wbportfolio/risk_management/backends/exposure_portfolio.py +7 -5
- wbportfolio/serializers/transactions/trade_proposals.py +2 -13
- wbportfolio/tasks.py +4 -1
- wbportfolio/tests/conftest.py +1 -1
- wbportfolio/tests/models/test_products.py +26 -0
- wbportfolio/viewsets/transactions/trade_proposals.py +21 -2
- {wbportfolio-1.47.0.dist-info → wbportfolio-1.48.0.dist-info}/METADATA +1 -1
- {wbportfolio-1.47.0.dist-info → wbportfolio-1.48.0.dist-info}/RECORD +22 -22
- {wbportfolio-1.47.0.dist-info → wbportfolio-1.48.0.dist-info}/WHEEL +0 -0
- {wbportfolio-1.47.0.dist-info → wbportfolio-1.48.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from datetime import date
|
|
2
2
|
|
|
3
3
|
from celery import shared_task
|
|
4
|
+
from django.db.models import Exists, OuterRef
|
|
4
5
|
from tqdm import tqdm
|
|
5
6
|
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
6
|
-
from wbcore.contrib.directory.models import Company
|
|
7
|
+
from wbcore.contrib.directory.models import Company
|
|
8
|
+
from wbcrm.models import Account
|
|
7
9
|
|
|
8
|
-
from .models import Updater
|
|
10
|
+
from .models import CompanyPortfolioData, Updater
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@shared_task(queue="portfolio")
|
|
@@ -13,7 +15,9 @@ def update_all_portfolio_data(val_date: date | None = None):
|
|
|
13
15
|
if not val_date:
|
|
14
16
|
val_date = CurrencyFXRates.objects.latest("date").date
|
|
15
17
|
updater = Updater(val_date)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
qs = Company.objects.annotate(
|
|
19
|
+
has_account=Exists(Account.objects.filter(owner=OuterRef("pk"))),
|
|
20
|
+
has_portfolio_data=Exists(CompanyPortfolioData.objects.filter(company=OuterRef("pk"))),
|
|
21
|
+
)
|
|
18
22
|
for company in tqdm(qs, total=qs.count()):
|
|
19
23
|
updater.update_company_data(company)
|
|
@@ -56,3 +56,12 @@ class AccountHoldingReconciliationNotificationBodyUpdate(LongStringPreference):
|
|
|
56
56
|
section = portfolio
|
|
57
57
|
name = "account_holding_reconciliation_notification_body_update"
|
|
58
58
|
default = """A reconcilation has been updated and requires your review. Please review the reconciliation for the account {account} on {reconciliation_date}."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@global_preferences_registry.register
|
|
62
|
+
class ProductTerminationNoticePeriod(IntegerPreference):
|
|
63
|
+
section = portfolio
|
|
64
|
+
name = "product_termination_notice_period"
|
|
65
|
+
default = 6 * 30 # approx 6 months
|
|
66
|
+
|
|
67
|
+
verbose_name = "Product Termination notice period"
|
|
@@ -5,7 +5,7 @@ from import_export.widgets import ForeignKeyWidget
|
|
|
5
5
|
from wbcore.contrib.io.resources import FilterModelResource
|
|
6
6
|
from wbfdm.models import Instrument
|
|
7
7
|
|
|
8
|
-
from wbportfolio.models import Trade
|
|
8
|
+
from wbportfolio.models import Trade
|
|
9
9
|
|
|
10
10
|
fake = Faker()
|
|
11
11
|
|
|
@@ -15,48 +15,37 @@ class TradeProposalTradeResource(FilterModelResource):
|
|
|
15
15
|
Trade Resource class to use to import trade from the trade proposal
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def __init__(self, **kwargs):
|
|
19
|
-
self.trade_proposal = TradeProposal.objects.get(pk=kwargs["trade_proposal_id"])
|
|
20
|
-
super().__init__(**kwargs)
|
|
21
|
-
|
|
22
|
-
def before_import(self, dataset, **kwargs):
|
|
23
|
-
Trade.objects.filter(trade_proposal=self.trade_proposal).delete()
|
|
24
|
-
|
|
25
|
-
def get_or_init_instance(self, instance_loader, row):
|
|
26
|
-
try:
|
|
27
|
-
return Trade.objects.get(
|
|
28
|
-
trade_proposal=self.trade_proposal, underlying_instrument=row["underlying_instrument"]
|
|
29
|
-
)
|
|
30
|
-
except Trade.DoesNotExist:
|
|
31
|
-
return Trade(
|
|
32
|
-
trade_proposal=self.trade_proposal,
|
|
33
|
-
underlying_instrument=row["underlying_instrument"],
|
|
34
|
-
transaction_subtype=Trade.Type.BUY if row["weighting"] > 0 else Trade.Type.SELL,
|
|
35
|
-
currency=row["underlying_instrument"].currency,
|
|
36
|
-
transaction_date=self.trade_proposal.trade_date,
|
|
37
|
-
portfolio=self.trade_proposal.portfolio,
|
|
38
|
-
weighting=row["weighting"],
|
|
39
|
-
status=Trade.Status.DRAFT,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
18
|
DUMMY_FIELD_MAP = {
|
|
43
|
-
"
|
|
19
|
+
"underlying_instrument__isin": lambda: rstr.xeger("([A-Z]{2}[A-Z0-9]{9}[0-9]{1})"),
|
|
20
|
+
"underlying_instrument__ticker": "AAA",
|
|
21
|
+
"underlying_instrument__name": "stock name",
|
|
44
22
|
"weighting": 1.0,
|
|
45
23
|
"shares": 1000.2536,
|
|
46
24
|
"comment": lambda: fake.sentence(),
|
|
47
25
|
"order": 1,
|
|
48
26
|
}
|
|
49
|
-
|
|
50
|
-
column_name="
|
|
27
|
+
underlying_instrument__isin = fields.Field(
|
|
28
|
+
column_name="underlying_instrument__isin",
|
|
51
29
|
attribute="underlying_instrument",
|
|
52
30
|
widget=ForeignKeyWidget(Instrument, field="isin"),
|
|
53
31
|
)
|
|
32
|
+
underlying_instrument__name = fields.Field(
|
|
33
|
+
column_name="underlying_instrument__name",
|
|
34
|
+
attribute="underlying_instrument",
|
|
35
|
+
widget=ForeignKeyWidget(Instrument, field="name"),
|
|
36
|
+
)
|
|
37
|
+
underlying_instrument__ticker = fields.Field(
|
|
38
|
+
column_name="underlying_instrument__ticker",
|
|
39
|
+
attribute="underlying_instrument",
|
|
40
|
+
widget=ForeignKeyWidget(Instrument, field="ticker"),
|
|
41
|
+
)
|
|
54
42
|
|
|
55
43
|
class Meta:
|
|
56
|
-
import_id_fields = ("underlying_instrument",)
|
|
57
44
|
fields = (
|
|
58
45
|
"id",
|
|
59
|
-
"
|
|
46
|
+
"underlying_instrument__isin",
|
|
47
|
+
"underlying_instrument__name",
|
|
48
|
+
"underlying_instrument__ticker",
|
|
60
49
|
"weighting",
|
|
61
50
|
"shares",
|
|
62
51
|
"comment",
|
|
@@ -2,12 +2,11 @@ from datetime import date
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
|
-
from django.db.models import
|
|
5
|
+
from django.db.models import QuerySet
|
|
6
6
|
from wbfdm.contrib.metric.backends.base import AbstractBackend, Metric
|
|
7
7
|
from wbfdm.contrib.metric.exceptions import MetricInvalidParameterException
|
|
8
|
-
from wbfdm.models import InstrumentType
|
|
9
8
|
|
|
10
|
-
from wbportfolio.models import AssetPosition,
|
|
9
|
+
from wbportfolio.models import AssetPosition, Portfolio
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class PortfolioMetricBaseBackend(AbstractBackend[Portfolio]):
|
|
@@ -66,18 +65,7 @@ class PortfolioMetricBaseBackend(AbstractBackend[Portfolio]):
|
|
|
66
65
|
raise MetricInvalidParameterException()
|
|
67
66
|
|
|
68
67
|
def get_queryset(self) -> QuerySet[Portfolio]:
|
|
69
|
-
product_portfolios = (
|
|
70
|
-
super()
|
|
71
|
-
.get_queryset()
|
|
72
|
-
.annotate(
|
|
73
|
-
has_product=Exists(
|
|
74
|
-
InstrumentPortfolioThroughModel.objects.filter(
|
|
75
|
-
instrument__instrument_type=InstrumentType.PRODUCT, portfolio=OuterRef("pk")
|
|
76
|
-
)
|
|
77
|
-
)
|
|
78
|
-
)
|
|
79
|
-
.filter(has_product=True, is_active=True)
|
|
80
|
-
)
|
|
68
|
+
product_portfolios = super().get_queryset().filter_active_and_tracked()
|
|
81
69
|
try:
|
|
82
70
|
last_position_date = (
|
|
83
71
|
AssetPosition.objects.filter(portfolio__in=product_portfolios, is_estimated=False).latest("date").date
|
wbportfolio/models/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Import CRM relevant data
|
|
2
|
-
|
|
2
|
+
from .roles import PortfolioRole
|
|
3
3
|
from .adjustments import Adjustment
|
|
4
4
|
from .asset import AssetPosition, AssetPositionGroupBy
|
|
5
5
|
from .custodians import Custodian
|
|
@@ -16,6 +16,5 @@ from .portfolio_cash_targets import PortfolioCashTarget
|
|
|
16
16
|
from .portfolio_cash_flow import DailyPortfolioCashFlow
|
|
17
17
|
from .portfolio_swing_pricings import PortfolioSwingPricing
|
|
18
18
|
from .registers import Register
|
|
19
|
-
from .roles import PortfolioRole
|
|
20
19
|
from .transactions import *
|
|
21
20
|
from .reconciliations import AccountReconciliation, AccountReconciliationLine
|
wbportfolio/models/asset.py
CHANGED
|
@@ -528,7 +528,7 @@ class AssetPosition(ImportMixin, models.Model):
|
|
|
528
528
|
shares=self._shares,
|
|
529
529
|
date=self.date,
|
|
530
530
|
asset_valuation_date=self.asset_valuation_date,
|
|
531
|
-
instrument_type=self.underlying_quote.
|
|
531
|
+
instrument_type=self.underlying_quote.security_instrument_type.id,
|
|
532
532
|
currency=self.underlying_quote.currency.id,
|
|
533
533
|
country=self.underlying_quote.country.id if self.underlying_quote.country else None,
|
|
534
534
|
is_cash=self.underlying_quote.is_cash,
|
wbportfolio/models/portfolio.py
CHANGED
|
@@ -129,6 +129,15 @@ class DefaultPortfolioQueryset(QuerySet):
|
|
|
129
129
|
& (Q(invested_timespan__endswith__gt=val_date) | Q(invested_timespan__endswith__isnull=True))
|
|
130
130
|
)
|
|
131
131
|
|
|
132
|
+
def filter_active_and_tracked(self):
|
|
133
|
+
return self.annotate(
|
|
134
|
+
has_product=Exists(
|
|
135
|
+
InstrumentPortfolioThroughModel.objects.filter(
|
|
136
|
+
instrument__instrument_type=InstrumentType.PRODUCT, portfolio=OuterRef("pk")
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
).filter((Q(has_product=True) | Q(is_manageable=True)) & Q(is_active=True) & Q(is_tracked=True))
|
|
140
|
+
|
|
132
141
|
def to_dependency_iterator(self, val_date: date) -> Iterable["Portfolio"]:
|
|
133
142
|
"""
|
|
134
143
|
A method to sort the given queryset to return undependable portfolio first. This is very useful if a routine needs to be applied sequentially on portfolios by order of dependence.
|
|
@@ -171,6 +180,9 @@ class DefaultPortfolioManager(ActiveObjectManager):
|
|
|
171
180
|
def filter_invested_at_date(self, val_date: date):
|
|
172
181
|
return self.get_queryset().filter_invested_at_date(val_date)
|
|
173
182
|
|
|
183
|
+
def filter_active_and_tracked(self):
|
|
184
|
+
return self.get_queryset().filter_active_and_tracked()
|
|
185
|
+
|
|
174
186
|
|
|
175
187
|
class ActiveTrackedPortfolioManager(DefaultPortfolioManager):
|
|
176
188
|
def get_queryset(self):
|
wbportfolio/models/products.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from contextlib import suppress
|
|
2
|
-
from datetime import date, datetime, time
|
|
2
|
+
from datetime import date, datetime, time, timedelta
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
|
|
5
5
|
from celery import shared_task
|
|
@@ -25,6 +25,9 @@ from pandas.tseries.offsets import BDay
|
|
|
25
25
|
from wbcore.contrib.ai.llm.config import add_llm_prompt
|
|
26
26
|
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
27
27
|
from wbcore.contrib.directory.models import Entry
|
|
28
|
+
from wbcore.contrib.notifications.dispatch import send_notification
|
|
29
|
+
from wbcore.contrib.notifications.utils import create_notification_type
|
|
30
|
+
from wbcore.permissions.shortcuts import get_internal_users
|
|
28
31
|
from wbcore.signals import pre_merge
|
|
29
32
|
from wbcore.utils.enum import ChoiceEnum
|
|
30
33
|
from wbcrm.models.accounts import Account
|
|
@@ -34,6 +37,8 @@ from wbfdm.models.instruments.instruments import InstrumentManager, InstrumentTy
|
|
|
34
37
|
from wbportfolio.models.llm.wbcrm.analyze_relationship import get_holding_prompt
|
|
35
38
|
from wbportfolio.models.portfolio_relationship import InstrumentPortfolioThroughModel
|
|
36
39
|
|
|
40
|
+
from ..preferences import get_product_termination_notice_period
|
|
41
|
+
from . import PortfolioRole
|
|
37
42
|
from .mixins.instruments import PMSInstrument, PMSInstrumentAbstractModel
|
|
38
43
|
|
|
39
44
|
|
|
@@ -316,6 +321,17 @@ class Product(PMSInstrumentAbstractModel):
|
|
|
316
321
|
class Meta:
|
|
317
322
|
verbose_name = "Product"
|
|
318
323
|
verbose_name_plural = "Products"
|
|
324
|
+
notification_types = [
|
|
325
|
+
create_notification_type(
|
|
326
|
+
"wbportfolio.product.termination_notice",
|
|
327
|
+
"Product Termination Notice",
|
|
328
|
+
"Sends a notification when a product is expected termination in the near future",
|
|
329
|
+
True,
|
|
330
|
+
True,
|
|
331
|
+
True,
|
|
332
|
+
is_lock=True,
|
|
333
|
+
),
|
|
334
|
+
]
|
|
319
335
|
|
|
320
336
|
def pre_save(self):
|
|
321
337
|
super().pre_save()
|
|
@@ -341,6 +357,39 @@ class Product(PMSInstrumentAbstractModel):
|
|
|
341
357
|
computed_str += f" ({self.isin})"
|
|
342
358
|
return computed_str
|
|
343
359
|
|
|
360
|
+
def check_and_notify_product_termination_on_date(self, today: date) -> bool:
|
|
361
|
+
"""
|
|
362
|
+
Checks if today is the expected notice date for product termination and sends notifications if applicable.
|
|
363
|
+
|
|
364
|
+
The expected notice date is calculated by subtracting the product termination notice period from the product's delisted date. Notifications are sent to users associated with the product's portfolio roles.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
today (date): The date to check against the expected notice date.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
bool: True if termination is due, False otherwise.
|
|
371
|
+
"""
|
|
372
|
+
if self.delisted_date:
|
|
373
|
+
product_termination_expected_notice_date = self.delisted_date - timedelta(
|
|
374
|
+
days=get_product_termination_notice_period()
|
|
375
|
+
)
|
|
376
|
+
if today == product_termination_expected_notice_date:
|
|
377
|
+
roles = PortfolioRole.objects.filter(
|
|
378
|
+
(models.Q(instrument=self) | models.Q(instrument__isnull=True))
|
|
379
|
+
& (models.Q(start__isnull=True) | models.Q(start__gte=today))
|
|
380
|
+
& (models.Q(end__isnull=True) | models.Q(end__lte=today))
|
|
381
|
+
& models.Q(person__user_account__isnull=False)
|
|
382
|
+
)
|
|
383
|
+
for user in get_internal_users().filter(is_active=True, id__in=roles.values("person__user_account")):
|
|
384
|
+
send_notification(
|
|
385
|
+
code="wbportfolio.product.termination_notice",
|
|
386
|
+
title="Product Termination Notice",
|
|
387
|
+
body=f"The product {self} will be terminated on the {self.delisted_date:%Y-%m-%d}",
|
|
388
|
+
user=user,
|
|
389
|
+
)
|
|
390
|
+
return True
|
|
391
|
+
return False
|
|
392
|
+
|
|
344
393
|
@property
|
|
345
394
|
def white_label_product(self):
|
|
346
395
|
return self.white_label_customers.all().count() > 0
|
|
@@ -15,6 +15,7 @@ from wbcore.contrib.icons import WBIcon
|
|
|
15
15
|
from wbcore.enums import RequestType
|
|
16
16
|
from wbcore.metadata.configs.buttons import ActionButton
|
|
17
17
|
from wbcore.models import WBModel
|
|
18
|
+
from wbcore.utils.models import CloneMixin
|
|
18
19
|
from wbfdm.models.instruments.instruments import Instrument
|
|
19
20
|
|
|
20
21
|
from wbportfolio.models.roles import PortfolioRole
|
|
@@ -30,7 +31,7 @@ logger = logging.getLogger("pms")
|
|
|
30
31
|
SelfTradeProposal = TypeVar("SelfTradeProposal", bound="TradeProposal")
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
class TradeProposal(RiskCheckMixin, WBModel):
|
|
34
|
+
class TradeProposal(CloneMixin, RiskCheckMixin, WBModel):
|
|
34
35
|
trade_date = models.DateField(verbose_name="Trading Date")
|
|
35
36
|
|
|
36
37
|
class Status(models.TextChoices):
|
|
@@ -76,17 +77,17 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
76
77
|
def save(self, *args, **kwargs):
|
|
77
78
|
if not self.trade_date and self.portfolio.assets.exists():
|
|
78
79
|
self.trade_date = (self.portfolio.assets.latest("date").date + BDay(1)).date()
|
|
79
|
-
if not self.rebalancing_model and (rebalancer := getattr(self.portfolio, "automatic_rebalancer", None)):
|
|
80
|
-
self.rebalancing_model = rebalancer.rebalancing_model
|
|
81
80
|
super().save(*args, **kwargs)
|
|
82
81
|
if self.status == TradeProposal.Status.APPROVED:
|
|
83
82
|
self.portfolio.change_at_date(self.trade_date)
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
@property
|
|
85
|
+
def checked_object(self):
|
|
86
|
+
return self.portfolio
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def check_evaluation_date(self):
|
|
90
|
+
return self.trade_date
|
|
90
91
|
|
|
91
92
|
@cached_property
|
|
92
93
|
def validated_trading_service(self) -> TradingService:
|
|
@@ -154,7 +155,7 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
154
155
|
)
|
|
155
156
|
|
|
156
157
|
# Start tools methods
|
|
157
|
-
def
|
|
158
|
+
def _clone(self, **kwargs) -> SelfTradeProposal:
|
|
158
159
|
"""
|
|
159
160
|
Method to clone self as a new trade proposal. It will automatically shift the trade date if a proposal already exists
|
|
160
161
|
Args:
|
|
@@ -176,6 +177,11 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
176
177
|
portfolio=self.portfolio,
|
|
177
178
|
creator=self.creator,
|
|
178
179
|
)
|
|
180
|
+
for trade in self.trades.all():
|
|
181
|
+
trade.id = None
|
|
182
|
+
trade.trade_proposal = trade_proposal_clone
|
|
183
|
+
trade.save()
|
|
184
|
+
|
|
179
185
|
return trade_proposal_clone
|
|
180
186
|
|
|
181
187
|
def normalize_trades(self):
|
|
@@ -263,17 +269,19 @@ class TradeProposal(RiskCheckMixin, WBModel):
|
|
|
263
269
|
last_trade_proposal.portfolio.assets.filter(
|
|
264
270
|
date=last_trade_proposal.trade_date
|
|
265
271
|
).delete() # we delete the existing position and we reapply the trade proposal
|
|
266
|
-
if
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
logger.info("
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
if last_trade_proposal.status == TradeProposal.Status.APPROVED:
|
|
273
|
+
logger.info("Reverting trade proposal ...")
|
|
274
|
+
last_trade_proposal.revert()
|
|
275
|
+
if last_trade_proposal.status == TradeProposal.Status.DRAFT:
|
|
276
|
+
if self.rebalancing_model: # if there is no position (for any reason) or we the trade proposal has a rebalancer model attached (trades are computed based on an aglo), we reapply this trade proposal
|
|
277
|
+
logger.info(f"Resetting trades from rebalancer model {self.rebalancing_model} ...")
|
|
278
|
+
self.reset_trades()
|
|
279
|
+
logger.info("Submitting trade proposal ...")
|
|
280
|
+
last_trade_proposal.submit()
|
|
281
|
+
if last_trade_proposal.status == TradeProposal.Status.SUBMIT:
|
|
282
|
+
logger.info("Approving trade proposal ...")
|
|
283
|
+
last_trade_proposal.approve()
|
|
284
|
+
last_trade_proposal.save()
|
|
277
285
|
next_trade_proposal = last_trade_proposal.next_trade_proposal
|
|
278
286
|
next_trade_date = (
|
|
279
287
|
next_trade_proposal.trade_date - timedelta(days=1) if next_trade_proposal else date.today()
|
|
@@ -487,6 +487,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
487
487
|
super().__init__(*args, **kwargs)
|
|
488
488
|
if target_weight is not None: # if target weight is provided, we guess the corresponding weighting
|
|
489
489
|
self.weighting = Decimal(target_weight) - self._effective_weight
|
|
490
|
+
self._set_transaction_subtype()
|
|
490
491
|
|
|
491
492
|
def save(self, *args, **kwargs):
|
|
492
493
|
if self.trade_proposal:
|
|
@@ -514,19 +515,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
514
515
|
|
|
515
516
|
if self.transaction_subtype is None:
|
|
516
517
|
# if subtype not provided, we extract it automatically from the existing data.
|
|
517
|
-
|
|
518
|
-
if self.shares is not None:
|
|
519
|
-
if self.shares > 0:
|
|
520
|
-
self.transaction_subtype = Trade.Type.SUBSCRIPTION
|
|
521
|
-
elif self.shares < 0:
|
|
522
|
-
self.transaction_subtype = Trade.Type.REDEMPTION
|
|
523
|
-
elif self.weighting is not None:
|
|
524
|
-
if self.weighting > 0:
|
|
525
|
-
self.transaction_subtype = Trade.Type.BUY
|
|
526
|
-
elif self.weighting < 0:
|
|
527
|
-
self.transaction_subtype = Trade.Type.SELL
|
|
528
|
-
else:
|
|
529
|
-
self.transaction_subtype = Trade.Type.REBALANCE
|
|
518
|
+
self._set_transaction_subtype()
|
|
530
519
|
if self.id and hasattr(self, "claims"):
|
|
531
520
|
self.claimed_shares = self.trade.claims.filter(status="APPROVED").aggregate(s=Sum("shares"))[
|
|
532
521
|
"s"
|
|
@@ -535,6 +524,21 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
535
524
|
self.marked_as_internal = True
|
|
536
525
|
super().save(*args, **kwargs)
|
|
537
526
|
|
|
527
|
+
def _set_transaction_subtype(self):
|
|
528
|
+
if self.underlying_instrument.instrument_type.key == "product":
|
|
529
|
+
if self.shares is not None:
|
|
530
|
+
if self.shares > 0:
|
|
531
|
+
self.transaction_subtype = Trade.Type.SUBSCRIPTION
|
|
532
|
+
elif self.shares < 0:
|
|
533
|
+
self.transaction_subtype = Trade.Type.REDEMPTION
|
|
534
|
+
elif self.weighting is not None:
|
|
535
|
+
if self.weighting > 0:
|
|
536
|
+
self.transaction_subtype = Trade.Type.BUY
|
|
537
|
+
elif self.weighting < 0:
|
|
538
|
+
self.transaction_subtype = Trade.Type.SELL
|
|
539
|
+
else:
|
|
540
|
+
self.transaction_subtype = Trade.Type.REBALANCE
|
|
541
|
+
|
|
538
542
|
def get_transaction_subtype(self) -> str:
|
|
539
543
|
"""
|
|
540
544
|
Return the expected transaction subtype based n
|
|
@@ -560,7 +564,7 @@ class Trade(ShareMixin, Transaction, OrderedModel, WBModel):
|
|
|
560
564
|
underlying_instrument=self.underlying_instrument.id,
|
|
561
565
|
effective_weight=self._effective_weight,
|
|
562
566
|
target_weight=self._target_weight,
|
|
563
|
-
instrument_type=self.underlying_instrument.security_instrument_type,
|
|
567
|
+
instrument_type=self.underlying_instrument.security_instrument_type.id,
|
|
564
568
|
currency=self.underlying_instrument.currency,
|
|
565
569
|
date=self.transaction_date,
|
|
566
570
|
)
|
wbportfolio/preferences.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
from dynamic_preferences.registries import global_preferences_registry
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def get_monthly_nnm_target(*args, **kwargs):
|
|
4
|
+
def get_monthly_nnm_target(*args, **kwargs) -> int:
|
|
5
5
|
global_preferences = global_preferences_registry.manager()
|
|
6
6
|
return global_preferences["wbportfolio__monthly_nnm_target"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_product_termination_notice_period(*args, **kwargs) -> int:
|
|
10
|
+
global_preferences = global_preferences_registry.manager()
|
|
11
|
+
return global_preferences["wbportfolio__product_termination_notice_period"]
|
|
@@ -3,6 +3,7 @@ from typing import Generator
|
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
|
+
from django.contrib.humanize.templatetags.humanize import intcomma
|
|
6
7
|
from django.db import models
|
|
7
8
|
from wbcompliance.models.risk_management import backend
|
|
8
9
|
from wbcompliance.models.risk_management.dispatch import register
|
|
@@ -148,15 +149,22 @@ class RuleBackend(backend.AbstractRuleBackend):
|
|
|
148
149
|
report_details = {
|
|
149
150
|
"Period": f"{cts_generator.start_date:%d.%m.%Y} - {cts_generator.end_date:%d.%m.%Y}",
|
|
150
151
|
}
|
|
152
|
+
|
|
153
|
+
# create report detail template
|
|
151
154
|
color = "red" if percentage < 0 else "green"
|
|
155
|
+
number_prefix = ""
|
|
156
|
+
|
|
152
157
|
if self.field == "AUM":
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
158
|
+
key = "AUM Change"
|
|
159
|
+
number_prefix = "$"
|
|
156
160
|
else:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
key = "Shares Change"
|
|
162
|
+
|
|
163
|
+
template = f'{number_prefix}{{}} → {number_prefix}{{}} <span style="color:{color}"><strong>Δ {number_prefix}{{}}</strong></span>'
|
|
164
|
+
start = intcomma(int(start_df.loc[breached_obj_id].round(0)))
|
|
165
|
+
end = intcomma(int(end_df.loc[breached_obj_id].round(0)))
|
|
166
|
+
diff = intcomma(int((end_df.loc[breached_obj_id] - start_df.loc[breached_obj_id]).round(0)))
|
|
167
|
+
report_details[key] = template.format(start, end, diff)
|
|
160
168
|
report_details["Group By"] = self.group_by.value
|
|
161
169
|
yield backend.IncidentResult(
|
|
162
170
|
breached_object=breached_obj,
|
|
@@ -137,11 +137,12 @@ class RuleBackend(
|
|
|
137
137
|
if not (df := self._filter_df(portfolio.to_df())).empty:
|
|
138
138
|
df = df[[self.group_by.value, self.field.value]].groupby(self.group_by.value).sum().astype(float)
|
|
139
139
|
for threshold in self.thresholds:
|
|
140
|
-
dff = df.copy()
|
|
141
140
|
numerical_range = threshold.numerical_range
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
incident_df = df[
|
|
142
|
+
(df[self.field.value] >= numerical_range[0]) & (df[self.field.value] < numerical_range[1])
|
|
143
|
+
]
|
|
144
|
+
if not incident_df.empty:
|
|
145
|
+
for id, row in incident_df.to_dict("index").items():
|
|
145
146
|
obj, obj_repr = self._get_obj_repr(id)
|
|
146
147
|
severity: RiskIncidentType = threshold.severity
|
|
147
148
|
if self.field == self.Field.WEIGHTING:
|
|
@@ -165,9 +166,9 @@ class RuleBackend(
|
|
|
165
166
|
return df
|
|
166
167
|
if self.is_cash is True or self.is_cash is False:
|
|
167
168
|
df = df[df["is_cash"] == self.is_cash]
|
|
168
|
-
|
|
169
169
|
if self.asset_classes:
|
|
170
170
|
df = df[df["instrument_type"].isin(list(map(lambda o: o.id, self.asset_classes)))]
|
|
171
|
+
|
|
171
172
|
if self.countries:
|
|
172
173
|
df = df[(~df["country"].isnull() & df["country"].isin(list(map(lambda o: o.id, self.countries))))]
|
|
173
174
|
if self.currencies:
|
|
@@ -179,6 +180,7 @@ class RuleBackend(
|
|
|
179
180
|
& df["primary_classification"].isin(list(map(lambda o: o.id, self.classifications)))
|
|
180
181
|
)
|
|
181
182
|
]
|
|
183
|
+
|
|
182
184
|
return df
|
|
183
185
|
|
|
184
186
|
def _get_obj_repr(self, pivot_object_id) -> tuple[models.Model | None, str]:
|
|
@@ -10,10 +10,7 @@ from .. import PortfolioRepresentationSerializer, RebalancingModelRepresentation
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
13
|
-
rebalancing_model = wb_serializers.PrimaryKeyRelatedField(
|
|
14
|
-
queryset=RebalancingModel.objects.all(),
|
|
15
|
-
default=DefaultFromView("portfolio.automatic_rebalancer.rebalancing_model"),
|
|
16
|
-
)
|
|
13
|
+
rebalancing_model = wb_serializers.PrimaryKeyRelatedField(queryset=RebalancingModel.objects.all(), required=False)
|
|
17
14
|
_rebalancing_model = RebalancingModelRepresentationSerializer(source="rebalancing_model")
|
|
18
15
|
target_portfolio = wb_serializers.PrimaryKeyRelatedField(
|
|
19
16
|
queryset=Portfolio.objects.all(), write_only=True, required=False, default=DefaultFromView("portfolio")
|
|
@@ -32,15 +29,7 @@ class TradeProposalModelSerializer(wb_serializers.ModelSerializer):
|
|
|
32
29
|
obj = super().create(validated_data)
|
|
33
30
|
|
|
34
31
|
target_portfolio_dto = None
|
|
35
|
-
if (
|
|
36
|
-
target_portfolio
|
|
37
|
-
and not rebalancing_model
|
|
38
|
-
and (
|
|
39
|
-
last_effective_date := target_portfolio.get_latest_asset_position_date(
|
|
40
|
-
obj.trade_date, with_estimated=True
|
|
41
|
-
)
|
|
42
|
-
)
|
|
43
|
-
):
|
|
32
|
+
if target_portfolio and not rebalancing_model and (last_effective_date := obj.last_effective_date):
|
|
44
33
|
target_portfolio_dto = target_portfolio._build_dto(last_effective_date)
|
|
45
34
|
try:
|
|
46
35
|
obj.reset_trades(target_portfolio=target_portfolio_dto)
|
wbportfolio/tasks.py
CHANGED
|
@@ -10,10 +10,13 @@ from .fdm.tasks import * # noqa
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@shared_task(queue="portfolio")
|
|
13
|
-
def
|
|
13
|
+
def daily_active_product_task(today: date | None = None):
|
|
14
|
+
if not today:
|
|
15
|
+
today = date.today()
|
|
14
16
|
qs = Product.active_objects.all()
|
|
15
17
|
for product in tqdm(qs, total=qs.count()):
|
|
16
18
|
update_outstanding_shares_as_task(product.id)
|
|
19
|
+
product.check_and_notify_product_termination_on_date(today)
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
@shared_task(queue="portfolio")
|
wbportfolio/tests/conftest.py
CHANGED
|
@@ -117,7 +117,7 @@ register(ContinentFactory)
|
|
|
117
117
|
|
|
118
118
|
register(CompanyFactory)
|
|
119
119
|
register(PersonFactory)
|
|
120
|
-
register(InternalUserFactory)
|
|
120
|
+
register(InternalUserFactory, "internal_user")
|
|
121
121
|
register(EntryFactory)
|
|
122
122
|
register(CustomerStatusFactory)
|
|
123
123
|
register(CompanyTypeFactory)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
from datetime import timedelta
|
|
2
3
|
from decimal import Decimal
|
|
4
|
+
from unittest.mock import patch
|
|
3
5
|
|
|
4
6
|
import pytest
|
|
5
7
|
from faker import Faker
|
|
@@ -7,6 +9,7 @@ from wbfdm.models import Instrument
|
|
|
7
9
|
|
|
8
10
|
from wbportfolio.models import Product, Trade
|
|
9
11
|
|
|
12
|
+
from ...preferences import get_product_termination_notice_period
|
|
10
13
|
from .utils import PortfolioTestMixin
|
|
11
14
|
|
|
12
15
|
fake = Faker()
|
|
@@ -188,3 +191,26 @@ class TestProductModel(PortfolioTestMixin):
|
|
|
188
191
|
product.initial_high_water_mark = 400
|
|
189
192
|
product.save()
|
|
190
193
|
assert product.get_high_water_mark(p4.date) == 400
|
|
194
|
+
|
|
195
|
+
@patch("wbportfolio.models.products.send_notification")
|
|
196
|
+
def test_check_and_notify_product_termination_on_date(
|
|
197
|
+
self, mock_fct, weekday, product, manager_portfolio_role_factory, internal_user
|
|
198
|
+
):
|
|
199
|
+
assert not product.check_and_notify_product_termination_on_date(weekday)
|
|
200
|
+
|
|
201
|
+
product.delisted_date = weekday + datetime.timedelta(days=get_product_termination_notice_period())
|
|
202
|
+
product.save()
|
|
203
|
+
assert product.check_and_notify_product_termination_on_date(weekday)
|
|
204
|
+
mock_fct.assert_not_called()
|
|
205
|
+
assert not product.check_and_notify_product_termination_on_date(
|
|
206
|
+
weekday + timedelta(days=1)
|
|
207
|
+
) # one day later is not valid anymore
|
|
208
|
+
|
|
209
|
+
manager_portfolio_role_factory.create(person=internal_user.profile)
|
|
210
|
+
product.check_and_notify_product_termination_on_date(weekday)
|
|
211
|
+
mock_fct.assert_called_with(
|
|
212
|
+
code="wbportfolio.product.termination_notice",
|
|
213
|
+
title="Product Termination Notice",
|
|
214
|
+
body=f"The product {product} will be terminated on the {product.delisted_date:%Y-%m-%d}",
|
|
215
|
+
user=internal_user,
|
|
216
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from contextlib import suppress
|
|
2
2
|
from datetime import date
|
|
3
3
|
|
|
4
|
+
from django.contrib.messages import info, warning
|
|
4
5
|
from django.shortcuts import get_object_or_404
|
|
5
6
|
from django.utils.functional import cached_property
|
|
6
7
|
from pandas._libs.tslibs.offsets import BDay
|
|
@@ -8,6 +9,7 @@ from rest_framework import status
|
|
|
8
9
|
from rest_framework.decorators import action
|
|
9
10
|
from rest_framework.response import Response
|
|
10
11
|
from wbcompliance.viewsets.risk_management.mixins import RiskCheckViewSetMixin
|
|
12
|
+
from wbcore import serializers as wb_serializers
|
|
11
13
|
from wbcore import viewsets
|
|
12
14
|
from wbcore.metadata.configs.display.instance_display import (
|
|
13
15
|
Display,
|
|
@@ -60,8 +62,15 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
60
62
|
return TradeProposalModelSerializer
|
|
61
63
|
return ReadOnlyTradeProposalModelSerializer
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
def
|
|
65
|
+
# 2 methods to parametrize the clone button functionality
|
|
66
|
+
def get_clone_button_serializer_class(self, instance):
|
|
67
|
+
class CloneSerializer(wb_serializers.Serializer):
|
|
68
|
+
trade_date = wb_serializers.DateField(default=(instance.trade_date + BDay(1)).date(), label="Trade Date")
|
|
69
|
+
comment = wb_serializers.TextField(label="Comment")
|
|
70
|
+
|
|
71
|
+
return CloneSerializer
|
|
72
|
+
|
|
73
|
+
def get_clone_button_instance_display(self) -> Display:
|
|
65
74
|
return create_simple_display(
|
|
66
75
|
[
|
|
67
76
|
["comment"],
|
|
@@ -69,6 +78,16 @@ class TradeProposalModelViewSet(CloneMixin, RiskCheckViewSetMixin, InternalUserP
|
|
|
69
78
|
]
|
|
70
79
|
)
|
|
71
80
|
|
|
81
|
+
def add_messages(self, request, instance=None, **kwargs):
|
|
82
|
+
if instance and instance.status == TradeProposal.Status.SUBMIT:
|
|
83
|
+
if not instance.portfolio.is_manageable:
|
|
84
|
+
info(request, "This trade proposal cannot be approved the portfolio is considered unmanaged.")
|
|
85
|
+
if not instance.has_all_check_completed_and_succeed:
|
|
86
|
+
warning(
|
|
87
|
+
request,
|
|
88
|
+
"This trade proposal cannot be approved because there is unsuccessful pre-trade checks. Please rectify accordingly and resubmit a valid trade proposal",
|
|
89
|
+
)
|
|
90
|
+
|
|
72
91
|
@classmethod
|
|
73
92
|
def _get_risk_checks_button_title(cls) -> str:
|
|
74
93
|
return "Pre-Trade Checks"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
wbportfolio/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
2
2
|
wbportfolio/apps.py,sha256=tybcoLFiw5tLdGHYV68X96n6jNZqx4BYx7Ao8mPflH8,749
|
|
3
|
-
wbportfolio/dynamic_preferences_registry.py,sha256=
|
|
3
|
+
wbportfolio/dynamic_preferences_registry.py,sha256=SV-4JWBZBYhXt5cNMglx-yRJhxpejP_jo157ghjq2q0,2294
|
|
4
4
|
wbportfolio/permissions.py,sha256=F147DXfitbw6IdMQGEFfymCJkiG5YGkWKsLdVVliPyw,320
|
|
5
|
-
wbportfolio/preferences.py,sha256=
|
|
6
|
-
wbportfolio/tasks.py,sha256=
|
|
5
|
+
wbportfolio/preferences.py,sha256=JpowIFTrnjfomogJNrYmmfv7V35bRZNNGL_zooKzEhc,465
|
|
6
|
+
wbportfolio/tasks.py,sha256=LgDpHpRygwpuhjEPfX1tvur1j-UwpMKR1P-Ksopcjsc,2014
|
|
7
7
|
wbportfolio/urls.py,sha256=YAd1OyvXIyHVwBY6s8E74qIQQ8Pz3Myq89GIjM1A2vY,10765
|
|
8
8
|
wbportfolio/utils.py,sha256=QZ4t0j2NwKYmgWJM77yvXSp-8wVcZmFvNi5_v7YA3ac,750
|
|
9
9
|
wbportfolio/admin/__init__.py,sha256=6mHM6lwUsLOlfMic0OthH_eM4VNlduYOrBptfQUoF9w,595
|
|
@@ -38,7 +38,7 @@ wbportfolio/contrib/company_portfolio/management.py,sha256=CofBifGGGKfA06uWRDWXY
|
|
|
38
38
|
wbportfolio/contrib/company_portfolio/models.py,sha256=dfrIR6sI3He1TIHCjdAP3yIZ0cPvc97ZUX_ZzkkhzGg,12943
|
|
39
39
|
wbportfolio/contrib/company_portfolio/scripts.py,sha256=THDWK967NdEOD9gtAjIgtJzZAvu1FAQjp58VIZLyQpo,2304
|
|
40
40
|
wbportfolio/contrib/company_portfolio/serializers.py,sha256=Y4Osxp0M7m_wy7DSbOt7ESfbob8KrfPBCnbzAuEsmmk,12160
|
|
41
|
-
wbportfolio/contrib/company_portfolio/tasks.py,sha256=
|
|
41
|
+
wbportfolio/contrib/company_portfolio/tasks.py,sha256=ZXezolvlj8_b5hvfcjH5IO5tXvwb8goNG8xPfk6YFNA,835
|
|
42
42
|
wbportfolio/contrib/company_portfolio/urls.py,sha256=eyq6IpaDPG1oL8-AqPS5nC5BeFewKPA1BN_HI7M3i1c,1118
|
|
43
43
|
wbportfolio/contrib/company_portfolio/viewsets.py,sha256=o-_D_bEt283xngDbsMwdRQxFyuK7rfSChI8u5dGYyLM,8312
|
|
44
44
|
wbportfolio/contrib/company_portfolio/configs/__init__.py,sha256=AVH0v-uOJBPRKggz7fO7IpRyVJ1YlQiF-if_cMsOvvM,72
|
|
@@ -190,12 +190,12 @@ wbportfolio/import_export/parsers/vontobel/valuation.py,sha256=iav8_xYpTJchmTa7K
|
|
|
190
190
|
wbportfolio/import_export/parsers/vontobel/valuation_api.py,sha256=WLkZ5z-WqhFraNorWlOhIpSx1pQ2fnjdsLHwSTA7O2o,882
|
|
191
191
|
wbportfolio/import_export/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
192
192
|
wbportfolio/import_export/resources/assets.py,sha256=zjgHlQWpud41jHrKdRyqGUt1KUJQm9Z7pt0uVh4qBWQ,2234
|
|
193
|
-
wbportfolio/import_export/resources/trades.py,sha256=
|
|
193
|
+
wbportfolio/import_export/resources/trades.py,sha256=_uAI2clV_ZjStX2Gf4rl_wWo2R4AHNOsZO52g0XKcM0,1723
|
|
194
194
|
wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql,sha256=BvMnLsVlXIqU5hhwLDbwZ6nBqWULIBDVcgLJ2B4sdS4,4440
|
|
195
195
|
wbportfolio/kpi_handlers/nnm.py,sha256=hCn0oG0C-6dQ0G-6S4r31nAS633NZdlOT-ntZrzvXZI,7180
|
|
196
196
|
wbportfolio/metric/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
197
197
|
wbportfolio/metric/backends/__init__.py,sha256=jfim5By5KqXfraENRFlhXy3i3gIzmElD6sfwL3qK2KM,108
|
|
198
|
-
wbportfolio/metric/backends/base.py,sha256=
|
|
198
|
+
wbportfolio/metric/backends/base.py,sha256=Dv9uTAPizUJtGraTPMaSnrqWpJJSr-ftPid4wttQhkI,2787
|
|
199
199
|
wbportfolio/metric/backends/constants.py,sha256=NU5BlMb_HIiN9NWtw_hSfWK0y0q4W9-HQG7eJoZqI_g,7373
|
|
200
200
|
wbportfolio/metric/backends/portfolio_base.py,sha256=9gC2RhBVt9R38rEIvBmWdgUC6mEOP5vFqEEew_sWIeU,10012
|
|
201
201
|
wbportfolio/metric/backends/portfolio_esg.py,sha256=MSiki-ZN-7YF0gev1kGFDrbfBi8Nfw6yJeLZlSRy6tw,2442
|
|
@@ -241,19 +241,19 @@ wbportfolio/migrations/0072_trade_diff_shares.py,sha256=aTKa1SbIiwmlXaFtBg-ENrSx
|
|
|
241
241
|
wbportfolio/migrations/0073_remove_product_price_computation_and_more.py,sha256=J4puisDFwnbnfv2VLWaiCQ7ost6PCOkin9qKVQoLIWM,18725
|
|
242
242
|
wbportfolio/migrations/0074_alter_rebalancer_frequency_and_more.py,sha256=o01rBj-ADgwCRtAai3e5z27alPGEzaiNxUqCwWm6peY,918
|
|
243
243
|
wbportfolio/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
244
|
-
wbportfolio/models/__init__.py,sha256=
|
|
244
|
+
wbportfolio/models/__init__.py,sha256=IIS_PNRxyX2Dcvyk1bcQOUzFt0B9SPC0WlM88CXqj04,881
|
|
245
245
|
wbportfolio/models/adjustments.py,sha256=osXWkJZOiansPWYPyHtl7Z121zDWi7u1YMtrBQtbHVo,10272
|
|
246
|
-
wbportfolio/models/asset.py,sha256=
|
|
246
|
+
wbportfolio/models/asset.py,sha256=yf4vBPfN1eZlWXG9SowQwr5-goG8rO1yYDitHDLZCBs,37758
|
|
247
247
|
wbportfolio/models/custodians.py,sha256=owTiS2Vm5CRKzh9M_P9GOVg-s-ndQ9UvRmw3yZP7cw0,3815
|
|
248
248
|
wbportfolio/models/exceptions.py,sha256=3ix0tWUO-O6jpz8f07XIwycw2x3JFRoWzjwil8FVA2Q,52
|
|
249
249
|
wbportfolio/models/indexes.py,sha256=iLYF2gzNzX4GLj_Nh3fybUcAQ1TslnT0wgQ6mN164QI,728
|
|
250
|
-
wbportfolio/models/portfolio.py,sha256=
|
|
250
|
+
wbportfolio/models/portfolio.py,sha256=aBlx37tX8msuZx3OL5QqyeuBqX3marOWTQTqfFH_Fk8,58954
|
|
251
251
|
wbportfolio/models/portfolio_cash_flow.py,sha256=2blPiXSw7dbhUVd-7LcxDBb4v0SheNOdvRK3MFYiChA,7273
|
|
252
252
|
wbportfolio/models/portfolio_cash_targets.py,sha256=WmgG-etPisZsh2yaFQpz7EkpvAudKBEzqPsO715w52U,1498
|
|
253
253
|
wbportfolio/models/portfolio_relationship.py,sha256=mMb18UMRWg9kx_9uIPkMktwORuXXLjKdgRPQQvB6fVE,5486
|
|
254
254
|
wbportfolio/models/portfolio_swing_pricings.py,sha256=_LqYC1VRjnnFFmVqFPRdnbgYsVxocMVpClTk2dnooig,1778
|
|
255
255
|
wbportfolio/models/product_groups.py,sha256=qoT8qv6tRdD6zmNDxsHO6dtnWw1-TRnyS5NqOq_gwhI,7901
|
|
256
|
-
wbportfolio/models/products.py,sha256=
|
|
256
|
+
wbportfolio/models/products.py,sha256=5z9OMLVS0ps6aOJbG3JE7gKTf3PlbC4rXIPpv55dD1A,22863
|
|
257
257
|
wbportfolio/models/registers.py,sha256=qA6T33t4gxFYnabQFBMd90WGIr6wxxirDLKDFqjOfok,4667
|
|
258
258
|
wbportfolio/models/roles.py,sha256=34BwZleaPMHnUqDK1nHett45xaNNsUqSHY44844itW8,7387
|
|
259
259
|
wbportfolio/models/utils.py,sha256=iBdMjRCvr6aOL0nLgfSCWUKe0h39h3IGmUbYo6l9t6w,394
|
|
@@ -274,8 +274,8 @@ wbportfolio/models/transactions/dividends.py,sha256=naL5xeDQfUBf5KyGt7y-tTcHL22n
|
|
|
274
274
|
wbportfolio/models/transactions/expiry.py,sha256=vnNHdcC1hf2HP4rAbmoGgOfagBYKNFytqOwzOI0MlVI,144
|
|
275
275
|
wbportfolio/models/transactions/fees.py,sha256=ffvqo8I4A0l5rLi00jJ6sGot0jmnkoxaNsbDzdPLwCg,5712
|
|
276
276
|
wbportfolio/models/transactions/rebalancing.py,sha256=3p5r6m68_7AD783hYAlDZCnUz1TOLVa9mvF5zConTMo,7036
|
|
277
|
-
wbportfolio/models/transactions/trade_proposals.py,sha256=
|
|
278
|
-
wbportfolio/models/transactions/trades.py,sha256=
|
|
277
|
+
wbportfolio/models/transactions/trade_proposals.py,sha256=vLwDdHNUEJdDvqSw6s7jcabp3ldZd-NPWCxF0qzNoGQ,21531
|
|
278
|
+
wbportfolio/models/transactions/trades.py,sha256=C-goHy_f9NJcZko39k3oPm7cGjznruhWyD_Hr1xoBKo,27941
|
|
279
279
|
wbportfolio/models/transactions/transactions.py,sha256=4THsE4xqdigZAwWKYfTNRLPJlkmAmsgE70Ribp9Lnrk,7127
|
|
280
280
|
wbportfolio/pms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
281
281
|
wbportfolio/pms/typing.py,sha256=WKP5tYyYt7DbMo25VM99V4IAM9oDSIPZyR3yXCzeZEA,5920
|
|
@@ -296,9 +296,9 @@ wbportfolio/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
296
296
|
wbportfolio/reports/monthly_position_report.py,sha256=e7BzjDd6eseUOwLwQJXKvWErQ58YnCsznHU2VtR6izM,2981
|
|
297
297
|
wbportfolio/risk_management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
298
298
|
wbportfolio/risk_management/backends/__init__.py,sha256=4N3GYcIb0XgZUQWEHxCejOCRabUtLM6ha9UdOWJKUfI,368
|
|
299
|
-
wbportfolio/risk_management/backends/accounts.py,sha256=
|
|
299
|
+
wbportfolio/risk_management/backends/accounts.py,sha256=VMHwhzeFV2bzobb1RlEJadTVix1lkbbfdpo-zG3YN5c,7988
|
|
300
300
|
wbportfolio/risk_management/backends/controversy_portfolio.py,sha256=ncEHyJMVomv0ehx7LoWcux1YHLV6KYYmiOkIBzJ0P1M,2852
|
|
301
|
-
wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=
|
|
301
|
+
wbportfolio/risk_management/backends/exposure_portfolio.py,sha256=PaJYlv3OCG2ncpp0zLr0eGtUk0C9KtWTrp4pPVa7AqA,9290
|
|
302
302
|
wbportfolio/risk_management/backends/instrument_list_portfolio.py,sha256=Sh8PZHHKFAl4Aw8fOlu17fUSiEFYYemb-B0iltAXc10,4186
|
|
303
303
|
wbportfolio/risk_management/backends/liquidity_risk.py,sha256=cRL7ZXvrC286yXCauNatuvSYikMNB-nmAt5ELmpDg0c,3688
|
|
304
304
|
wbportfolio/risk_management/backends/liquidity_stress_instrument.py,sha256=oitzsaZu-HhYn9Avku3322GtDmf6QGsfyRzGPGZoM1Y,3612
|
|
@@ -340,7 +340,7 @@ wbportfolio/serializers/transactions/claim.py,sha256=kC4E2RZRrpd9i8tGfoiV-gpWDk3
|
|
|
340
340
|
wbportfolio/serializers/transactions/dividends.py,sha256=EULwKDumHBv4r2HsdEGZMZGFaye4dRUNNyXg6-wZXzc,520
|
|
341
341
|
wbportfolio/serializers/transactions/expiry.py,sha256=K3XOSbCyef-xRzOjCr4Qg_YFJ_JuuiJ9u6tDS86l0hg,477
|
|
342
342
|
wbportfolio/serializers/transactions/fees.py,sha256=uPmSWuCeoV2bwVS6RmEz3a0VRBWJHIQr0WhklYc1UAI,1068
|
|
343
|
-
wbportfolio/serializers/transactions/trade_proposals.py,sha256=
|
|
343
|
+
wbportfolio/serializers/transactions/trade_proposals.py,sha256=fiGpL6za5ERLtdbud5wrciCVXHX6-3SjeF8Zaa6Zhzg,3410
|
|
344
344
|
wbportfolio/serializers/transactions/trades.py,sha256=pONV5NSqrXUnoTEoAxovnnQqu37cZGuB33TYvIOK3rE,10009
|
|
345
345
|
wbportfolio/serializers/transactions/transactions.py,sha256=O137zeCndK-nxIWSRLEj7bXbBZDGa4d6qK6pJIIYK3g,4170
|
|
346
346
|
wbportfolio/static/wbportfolio/css/macro_review.css,sha256=FAVVO8nModxwPXcTKpcfzVxBGPZGJVK1Xn-0dkSfGyc,233
|
|
@@ -357,7 +357,7 @@ wbportfolio/templates/portfolio/email/email_base_template.html,sha256=n6DN3QV92t
|
|
|
357
357
|
wbportfolio/templates/portfolio/email/rebalancing_report.html,sha256=Go7joPYXwF_1Q72mblOtJSw4-TbH3NqW7ww_xGBP95k,1206
|
|
358
358
|
wbportfolio/templates/portfolio/macro/macro_review.html,sha256=OxliFeX9pVNKeTVTaPnfNPHmM5YxVMr77PeKC-6y0jY,2273
|
|
359
359
|
wbportfolio/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
360
|
-
wbportfolio/tests/conftest.py,sha256=
|
|
360
|
+
wbportfolio/tests/conftest.py,sha256=zNKZJ_ymjE0M-Q5QyVYQdStSCSdO9Gy_S_jJEBC-6JY,4380
|
|
361
361
|
wbportfolio/tests/signals.py,sha256=5dTpWBO31sHF1NXN5CRnQQUTJuK_X4WR5iDcIS4-_c0,5918
|
|
362
362
|
wbportfolio/tests/tests.py,sha256=utTcVQI7oA7-Yo-IDM7d7BXwXNMIWpRbZNyEWqxkyL4,1033
|
|
363
363
|
wbportfolio/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -374,7 +374,7 @@ wbportfolio/tests/models/test_portfolio_cash_targets.py,sha256=q8QWAwt-kKRkLC0E0
|
|
|
374
374
|
wbportfolio/tests/models/test_portfolio_swing_pricings.py,sha256=kr2AOcQkyg2pX3ULjU-o9ye-NVpjMrrfoe-DVbYCbjs,1656
|
|
375
375
|
wbportfolio/tests/models/test_portfolios.py,sha256=idIUT2kc6Fte-bzwOS9NtOYRVnwTBijSo7deHkRon_k,52194
|
|
376
376
|
wbportfolio/tests/models/test_product_groups.py,sha256=AcdxhurV-n_bBuUsfD1GqVtwLFcs7VI2CRrwzsIUWbU,3337
|
|
377
|
-
wbportfolio/tests/models/test_products.py,sha256=
|
|
377
|
+
wbportfolio/tests/models/test_products.py,sha256=nBEgyUoY-4F_pfHYnAr7KXdNYvdIkSu-PWJrqp5tPHg,9482
|
|
378
378
|
wbportfolio/tests/models/test_roles.py,sha256=4Cn7WyrA2ztJNeWLk5cy9kYo5XLWMbFSvo1O-9JYxeA,3323
|
|
379
379
|
wbportfolio/tests/models/test_splits.py,sha256=ytKcHsI_90kj1L4s8It-KEcc24rkDcElxwQ8q0QxEvk,9689
|
|
380
380
|
wbportfolio/tests/models/utils.py,sha256=ORNJq6NMo1Za22jGZXfTfKeNEnTRlfEt_8SJ6xLaQWg,325
|
|
@@ -515,10 +515,10 @@ wbportfolio/viewsets/transactions/claim.py,sha256=m_Fy4J_QZSve1VlR_sPQrVBDopgCqq
|
|
|
515
515
|
wbportfolio/viewsets/transactions/fees.py,sha256=7VUXIogmRrXCz_D9tvDiiTae0t5j09W9zPUzxXzBGTE,7031
|
|
516
516
|
wbportfolio/viewsets/transactions/mixins.py,sha256=WipvJoi5hylkpD0y9VATe30WAcwIHUIroVkK10FYw7k,636
|
|
517
517
|
wbportfolio/viewsets/transactions/rebalancing.py,sha256=6rIrdK0rtKL1afJ-tYfAGdQVTN2MH1kG_yCeVkmyK8k,1263
|
|
518
|
-
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=
|
|
518
|
+
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=HyffjmSq2KZpCkzvaeEc2PZY6-cJtF7bmMTFOj95at0,5835
|
|
519
519
|
wbportfolio/viewsets/transactions/trades.py,sha256=E0QV6oPohOgeVWzNT6xfWOFvTB4KoP2DzDYnGtm3Hz0,16259
|
|
520
520
|
wbportfolio/viewsets/transactions/transactions.py,sha256=ixDp-nsNA8t_A06rBCT19hOMJHy0iRmdz1XKdV1OwAs,4450
|
|
521
|
-
wbportfolio-1.
|
|
522
|
-
wbportfolio-1.
|
|
523
|
-
wbportfolio-1.
|
|
524
|
-
wbportfolio-1.
|
|
521
|
+
wbportfolio-1.48.0.dist-info/METADATA,sha256=jcITcoDYbFVqqHklU1vzXHkRGeJwqU3FB4kHLWZXbQ0,734
|
|
522
|
+
wbportfolio-1.48.0.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
523
|
+
wbportfolio-1.48.0.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
524
|
+
wbportfolio-1.48.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|