wbportfolio 2.2.4__py2.py3-none-any.whl → 2.2.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/import_export/backends/refinitiv/adjustment.py +40 -0
- wbportfolio/kpi_handlers/nnm.py +163 -0
- wbportfolio/models/llm/wbcrm/analyze_relationship.py +58 -0
- {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.5.dist-info}/METADATA +1 -1
- {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.5.dist-info}/RECORD +7 -4
- {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.5.dist-info}/WHEEL +0 -0
- {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from django.db import models
|
|
6
|
+
from pandas.tseries.offsets import BDay
|
|
7
|
+
from wbcore.contrib.io.backends import AbstractDataBackend, register
|
|
8
|
+
from wbfdm.import_export.backends.refinitiv.utils import Controller
|
|
9
|
+
from wbportfolio.import_export.backends.utils import (
|
|
10
|
+
get_timedelta_import_instrument_price,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from ..wbfdm.mixin import DataBackendMixin
|
|
14
|
+
|
|
15
|
+
DEFAULT_MAPPING = {"AX": "factor"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register("Adjustment", provider_key="refinitiv")
|
|
19
|
+
class DataBackend(DataBackendMixin, AbstractDataBackend):
|
|
20
|
+
CHUNK_SIZE = 50
|
|
21
|
+
|
|
22
|
+
def __init__(self, import_credential: Optional[models.Model] = None, **kwargs):
|
|
23
|
+
self.controller = Controller(import_credential.username, import_credential.password)
|
|
24
|
+
|
|
25
|
+
def get_files(
|
|
26
|
+
self,
|
|
27
|
+
execution_time: datetime,
|
|
28
|
+
obj_external_ids: list[str] = None,
|
|
29
|
+
**kwargs,
|
|
30
|
+
) -> BytesIO:
|
|
31
|
+
execution_date = execution_time.date()
|
|
32
|
+
start = kwargs.get("start", (execution_date - BDay(get_timedelta_import_instrument_price())).date())
|
|
33
|
+
fields = list(DEFAULT_MAPPING.keys())
|
|
34
|
+
if obj_external_ids:
|
|
35
|
+
df = self.controller.get_data(obj_external_ids, fields, start, execution_date)
|
|
36
|
+
if not df.empty:
|
|
37
|
+
content_file = BytesIO()
|
|
38
|
+
df.to_json(content_file, orient="records")
|
|
39
|
+
file_name = f"adjustment_chunk-{start:%Y-%m-%d}-{execution_date:%Y-%m-%d}_{datetime.timestamp(execution_time)}.json"
|
|
40
|
+
yield file_name, content_file
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
4
|
+
from django.db.models import Exists, OuterRef, Sum
|
|
5
|
+
from django.db.models.expressions import F
|
|
6
|
+
from django.db.models.functions import Coalesce
|
|
7
|
+
from django.db.models.query import QuerySet
|
|
8
|
+
from wbcore import serializers
|
|
9
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
10
|
+
from wbcore.serializers.serializers import Serializer
|
|
11
|
+
from wbcrm.models import Account
|
|
12
|
+
from wbhuman_resources.models.kpi import KPI, KPIHandler
|
|
13
|
+
from wbhuman_resources.serializers import KPIModelSerializer
|
|
14
|
+
from wbportfolio.models.transactions.claim import Claim
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NetNewMoneyKPISerializer(KPIModelSerializer):
|
|
18
|
+
transaction_subtype = serializers.ChoiceField(
|
|
19
|
+
default="all",
|
|
20
|
+
choices=[("SUBSCRIPTION", "Only Subscriptions"), ("REDEMPTION", "Only Redemptions"), ("all", "All")],
|
|
21
|
+
)
|
|
22
|
+
nnm_from = serializers.ChoiceField(
|
|
23
|
+
default="all",
|
|
24
|
+
label="NNM from",
|
|
25
|
+
choices=[("new_clients", "New clients"), ("existing_clients", "Existing clients"), ("all", "All")],
|
|
26
|
+
)
|
|
27
|
+
only_approved = serializers.BooleanField(
|
|
28
|
+
default=False, label="Only Approved claims", help_text="Filter only approve claims"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
creator = serializers.BooleanField(
|
|
32
|
+
default=True, label="Creator of claim", help_text="NNM considered are related to the creator of claim"
|
|
33
|
+
)
|
|
34
|
+
claimant = serializers.BooleanField(
|
|
35
|
+
default=True, label="Claimant of claim", help_text="NNM considered are related to the claimant"
|
|
36
|
+
)
|
|
37
|
+
in_charge_of_customer = serializers.BooleanField(
|
|
38
|
+
default=True,
|
|
39
|
+
label="In charge of customer",
|
|
40
|
+
help_text="NNM considered are related to the persons in charge of customer",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def update(self, instance, validated_data):
|
|
44
|
+
transaction_subtype = validated_data.get(
|
|
45
|
+
"transaction_subtype",
|
|
46
|
+
instance.additional_data["serializer_data"].get("transaction_subtype", "all"),
|
|
47
|
+
)
|
|
48
|
+
only_approved = validated_data.get(
|
|
49
|
+
"only_approved",
|
|
50
|
+
instance.additional_data["serializer_data"].get("only_approved", False),
|
|
51
|
+
)
|
|
52
|
+
nnm_from = validated_data.get(
|
|
53
|
+
"nnm_from",
|
|
54
|
+
instance.additional_data["serializer_data"].get("nnm_from", "all"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
creator = validated_data.get(
|
|
58
|
+
"creator",
|
|
59
|
+
instance.additional_data["serializer_data"].get("creator", True),
|
|
60
|
+
)
|
|
61
|
+
claimant = validated_data.get(
|
|
62
|
+
"claimant",
|
|
63
|
+
instance.additional_data["serializer_data"].get("claimant", True),
|
|
64
|
+
)
|
|
65
|
+
in_charge_of_customer = validated_data.get(
|
|
66
|
+
"in_charge_of_customer",
|
|
67
|
+
instance.additional_data["serializer_data"].get("in_charge_of_customer", True),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
additional_data = instance.additional_data
|
|
71
|
+
additional_data["serializer_data"]["only_approved"] = only_approved
|
|
72
|
+
additional_data["serializer_data"]["transaction_subtype"] = transaction_subtype
|
|
73
|
+
additional_data["serializer_data"]["nnm_from"] = nnm_from
|
|
74
|
+
additional_data["serializer_data"]["creator"] = creator
|
|
75
|
+
additional_data["serializer_data"]["claimant"] = claimant
|
|
76
|
+
additional_data["serializer_data"]["in_charge_of_customer"] = in_charge_of_customer
|
|
77
|
+
|
|
78
|
+
additional_data["list_data"] = instance.get_handler().get_list_data(additional_data["serializer_data"])
|
|
79
|
+
validated_data["additional_data"] = additional_data
|
|
80
|
+
|
|
81
|
+
return super().update(instance, validated_data)
|
|
82
|
+
|
|
83
|
+
class Meta(KPIModelSerializer.Meta):
|
|
84
|
+
fields = (
|
|
85
|
+
*KPIModelSerializer.Meta.fields,
|
|
86
|
+
"transaction_subtype",
|
|
87
|
+
"nnm_from",
|
|
88
|
+
"creator",
|
|
89
|
+
"claimant",
|
|
90
|
+
"in_charge_of_customer",
|
|
91
|
+
"only_approved",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class NetNewMoneyKPI(KPIHandler):
|
|
96
|
+
def get_name(self) -> str:
|
|
97
|
+
return "Net New Money"
|
|
98
|
+
|
|
99
|
+
def get_serializer(self) -> Type[Serializer]:
|
|
100
|
+
return NetNewMoneyKPISerializer
|
|
101
|
+
|
|
102
|
+
def annotate_parameters(self, queryset: QuerySet[KPI]) -> QuerySet[KPI]:
|
|
103
|
+
return queryset.annotate(
|
|
104
|
+
transaction_subtype=F("additional_data__serializer_data__transaction_subtype"),
|
|
105
|
+
nnm_from=F("additional_data__serializer_data__nnm_from"),
|
|
106
|
+
creator=F("additional_data__serializer_data__creator"),
|
|
107
|
+
claimant=F("additional_data__serializer_data__claimant"),
|
|
108
|
+
in_charge_of_customer=F("additional_data__serializer_data__in_charge_of_customer"),
|
|
109
|
+
only_approved=F("additional_data__serializer_data__only_approved"),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def get_list_data(self, serializer_data: dict) -> list[str]:
|
|
113
|
+
return [
|
|
114
|
+
f"Claim Area: {serializer_data['transaction_subtype']}",
|
|
115
|
+
f"NNM from: {serializer_data['nnm_from']}",
|
|
116
|
+
f"Creator: {serializer_data['creator']}",
|
|
117
|
+
f"Claimant: {serializer_data['claimant']}",
|
|
118
|
+
f"In charge of customer: {serializer_data['in_charge_of_customer']}",
|
|
119
|
+
f"Only Approved: {serializer_data['only_approved']}",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def get_display_grid(self) -> list[list[str]]:
|
|
123
|
+
return [
|
|
124
|
+
["nnm_from"] * 3,
|
|
125
|
+
["only_approved"] * 3,
|
|
126
|
+
["transaction_subtype"] * 3,
|
|
127
|
+
["creator", "claimant", "in_charge_of_customer"],
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
def evaluate(self, kpi: "KPI", evaluated_person, evaluation_date=None) -> int:
|
|
131
|
+
serializer_data = kpi.additional_data.get("serializer_data")
|
|
132
|
+
to_date = evaluation_date if evaluation_date else kpi.period.upper
|
|
133
|
+
claims = (
|
|
134
|
+
Claim.objects.exclude(status=Claim.Status.WITHDRAWN)
|
|
135
|
+
.filter(date__gte=kpi.period.lower, date__lte=to_date, account__isnull=False)
|
|
136
|
+
.annotate(
|
|
137
|
+
existing_client=Exists(Claim.objects.filter(date__lte=kpi.period.lower, account=OuterRef("account")))
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if serializer_data.get("only_approved") is True:
|
|
142
|
+
claims = claims.filter(status=Claim.Status.APPROVED)
|
|
143
|
+
|
|
144
|
+
if (nnm_from := serializer_data.get("nnm_from")) and (nnm_from != "all"):
|
|
145
|
+
if nnm_from == "new_clients":
|
|
146
|
+
claims = claims.filter(existing_client=False)
|
|
147
|
+
elif nnm_from == "existing_clients":
|
|
148
|
+
claims = claims.filter(existing_client=True)
|
|
149
|
+
|
|
150
|
+
# TODO This introduced duplicates and can't be removed easily with distinct("id") because of the aggregation
|
|
151
|
+
accounts_ids = list(
|
|
152
|
+
Account.get_managed_accounts_for_entry(evaluated_person.entry_ptr).values_list("id", flat=True)
|
|
153
|
+
)
|
|
154
|
+
for employer in evaluated_person.employers.all():
|
|
155
|
+
accounts_ids.extend(list(Account.get_managed_accounts_for_entry(employer).values_list("id", flat=True)))
|
|
156
|
+
accounts = Account.objects.filter(id__in=accounts_ids)
|
|
157
|
+
claims = claims.filter(account__in=accounts)
|
|
158
|
+
|
|
159
|
+
return claims.annotate(
|
|
160
|
+
net_new_money=F("trade__price") * F("shares"),
|
|
161
|
+
fx_rate=CurrencyFXRates.get_fx_rates_subquery("date", currency="product__currency", lookup_expr="exact"),
|
|
162
|
+
net_new_money_usd=Coalesce(F("net_new_money") * F("fx_rate"), Decimal(0)),
|
|
163
|
+
).aggregate(s=Sum("net_new_money"))["s"] or Decimal(0)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from django.db.models import F, FloatField, Sum
|
|
5
|
+
from django.db.models.functions import Cast, ExtractYear
|
|
6
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
7
|
+
from wbfdm.models import Instrument
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from langchain_core.messages import BaseMessage
|
|
11
|
+
from wbcrm.models import Account
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_holding_prompt(account: "Account") -> list["BaseMessage"]:
|
|
15
|
+
from wbportfolio.models import Product
|
|
16
|
+
from wbportfolio.models.transactions.claim import Claim
|
|
17
|
+
|
|
18
|
+
products = (
|
|
19
|
+
Claim.get_valid_and_approved_claims(account=account)
|
|
20
|
+
.distinct("product")
|
|
21
|
+
.values_list("product", "product__isin")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
performances = {}
|
|
25
|
+
for product_id, product_name in products:
|
|
26
|
+
performances[product_name] = Instrument.extract_annual_performance_df(
|
|
27
|
+
Product.objects.get(id=product_id).get_prices_df()
|
|
28
|
+
).to_dict()
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
SystemMessage(
|
|
32
|
+
"The following products are held by the account holder. Analyze their performances and check correlations between the holdings and their performances/interactions."
|
|
33
|
+
),
|
|
34
|
+
HumanMessage(json.dumps(performances)),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_performances_prompt(account: "Account") -> list["BaseMessage"]:
|
|
39
|
+
from wbportfolio.models.transactions.claim import Claim
|
|
40
|
+
|
|
41
|
+
holdings = (
|
|
42
|
+
Claim.get_valid_and_approved_claims(account=account)
|
|
43
|
+
.annotate(year=ExtractYear("date"))
|
|
44
|
+
.values("year", "product")
|
|
45
|
+
.annotate(
|
|
46
|
+
sum_shares=Cast(Sum("shares"), FloatField()),
|
|
47
|
+
product_name=F("product__name"),
|
|
48
|
+
product_isin=F("product__isin"),
|
|
49
|
+
)
|
|
50
|
+
.values("product_name", "product_isin", "sum_shares", "year")
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
SystemMessage(
|
|
55
|
+
"The following holdings (subscriptions/redemptions) have been found for this account. Please include this data in the analysis and check if there is any correlation between the holding data and the interactions."
|
|
56
|
+
),
|
|
57
|
+
HumanMessage(json.dumps(list(holdings.order_by("year", "product")))),
|
|
58
|
+
]
|
|
@@ -99,6 +99,7 @@ wbportfolio/import_export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
99
99
|
wbportfolio/import_export/utils.py,sha256=mzybnwU2y-NOk43QDNDlOCaSPO-0BFlSX_X8uxplNiY,1083
|
|
100
100
|
wbportfolio/import_export/backends/__init__.py,sha256=Ki5jE95usAbxfV3zeZMkAmjh7G8TodBkKTDBPa_HJX0,40
|
|
101
101
|
wbportfolio/import_export/backends/utils.py,sha256=joFlyCrstrXSFb0RmQ4T7lfRVT7lxb7qe8TePis8wFU,2116
|
|
102
|
+
wbportfolio/import_export/backends/refinitiv/adjustment.py,sha256=ahVjWMHY1FuuDnMeascAlVUa3Mys6rvROG1uKDq6QeA,1554
|
|
102
103
|
wbportfolio/import_export/backends/ubs/__init__.py,sha256=jHm6zfyZPfvdzyMfJGngGbwROxtYWyJdpQorpleVKMA,82
|
|
103
104
|
wbportfolio/import_export/backends/ubs/asset_position.py,sha256=xN9VBsl8wdXDbfQOKS_tOlmlwuNqv-yGGg20QUBbavI,1901
|
|
104
105
|
wbportfolio/import_export/backends/ubs/fees.py,sha256=wKGWyzmtvXloUYeMN6mkfoAwtn4qVqyw0OX3e8qGtrw,2863
|
|
@@ -190,6 +191,7 @@ wbportfolio/import_export/parsers/vontobel/valuation.py,sha256=iav8_xYpTJchmTa7K
|
|
|
190
191
|
wbportfolio/import_export/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
191
192
|
wbportfolio/import_export/resources/assets.py,sha256=mfQY2md1X4JgMxsvSMoPBKaYKmklZOaFFafqntOMswA,2248
|
|
192
193
|
wbportfolio/import_export/resources/trades.py,sha256=gsF7SRL9JrCHSrCekndrEkKrLjTw1YXLltmyXYK2K0Y,1124
|
|
194
|
+
wbportfolio/kpi_handlers/nnm.py,sha256=hCn0oG0C-6dQ0G-6S4r31nAS633NZdlOT-ntZrzvXZI,7180
|
|
193
195
|
wbportfolio/metric/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
194
196
|
wbportfolio/metric/backends/__init__.py,sha256=jfim5By5KqXfraENRFlhXy3i3gIzmElD6sfwL3qK2KM,108
|
|
195
197
|
wbportfolio/metric/backends/base.py,sha256=FlKR00BHrvFEDHG_s1k1_YPyW2gJeJwcI8Bc1UilDys,3222
|
|
@@ -250,6 +252,7 @@ wbportfolio/models/products.py,sha256=d9gst_FnkNzkn4IGUnMJBe6MgyfwSRgAmnfep6dE1B
|
|
|
250
252
|
wbportfolio/models/registers.py,sha256=nusTwh08YFIEPlRq-4Fuv1az72NxvBVaH7V9M_5lhyI,4666
|
|
251
253
|
wbportfolio/models/roles.py,sha256=34BwZleaPMHnUqDK1nHett45xaNNsUqSHY44844itW8,7387
|
|
252
254
|
wbportfolio/models/utils.py,sha256=wYAqgqIfBPafDY9CZWwSCDhSt0ucdyMTrvDwnrnk6H8,393
|
|
255
|
+
wbportfolio/models/llm/wbcrm/analyze_relationship.py,sha256=_y2Myc-M2hXQDkRGXvzsM0ZNC31dmxSHHz5BKMtymww,2106
|
|
253
256
|
wbportfolio/models/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
254
257
|
wbportfolio/models/mixins/instruments.py,sha256=oZoehE8gX0jquMgELZYce2C7p2GPgPC8dAuqOZgRcqY,5485
|
|
255
258
|
wbportfolio/models/mixins/liquidity_stress_test.py,sha256=whkzjtbOyl_ncNyaQBORb_Z_rDgcvfdTYPgqPolu7dA,58865
|
|
@@ -480,7 +483,7 @@ wbportfolio/viewsets/transactions/mixins.py,sha256=i9ICaUXZfryIrbgS-bdCcoBJO-pTn
|
|
|
480
483
|
wbportfolio/viewsets/transactions/trade_proposals.py,sha256=5mgAk60cINZHd2HfTu7StFOc882FGakNcIVZpNSwBCs,3528
|
|
481
484
|
wbportfolio/viewsets/transactions/trades.py,sha256=_rZuPRDjf3MbwZ5xvdCgczCxcIbvI-RvyAf-50Y_Ga4,14744
|
|
482
485
|
wbportfolio/viewsets/transactions/transactions.py,sha256=VgSBUs1g5etsXjt7zi4wQi82XwOGuHsd_zppnsbMZMs,4517
|
|
483
|
-
wbportfolio-2.2.
|
|
484
|
-
wbportfolio-2.2.
|
|
485
|
-
wbportfolio-2.2.
|
|
486
|
-
wbportfolio-2.2.
|
|
486
|
+
wbportfolio-2.2.5.dist-info/METADATA,sha256=5BgM91I5D6LmcjXAmQi44idFcDIHHHgcvVIk4kpKYKg,622
|
|
487
|
+
wbportfolio-2.2.5.dist-info/WHEEL,sha256=aO3RJuuiFXItVSnAUEmQ0yRBvv9e1sbJh68PtuQkyAE,105
|
|
488
|
+
wbportfolio-2.2.5.dist-info/licenses/LICENSE,sha256=jvfVH0SY8_YMHlsJHKe_OajiscQDz4lpTlqT6x24sVw,172
|
|
489
|
+
wbportfolio-2.2.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|