wbportfolio 2.2.4__py2.py3-none-any.whl → 2.2.6__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.

Files changed (24) hide show
  1. wbportfolio/fixtures/product_factsheets.yaml +1 -0
  2. wbportfolio/fixtures/wbportfolio.yaml.gz +0 -0
  3. wbportfolio/fixtures/wbrisk_management.yaml.gz +0 -0
  4. wbportfolio/import_export/backends/refinitiv/adjustment.py +40 -0
  5. wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +119 -0
  6. wbportfolio/kpi_handlers/nnm.py +163 -0
  7. wbportfolio/models/llm/wbcrm/analyze_relationship.py +58 -0
  8. wbportfolio/static/wbportfolio/css/macro_review.css +17 -0
  9. wbportfolio/static/wbportfolio/markdown/documentation/account_holding_reconciliation.md +16 -0
  10. wbportfolio/static/wbportfolio/markdown/documentation/aggregate_asset_position_liquidity.md +25 -0
  11. wbportfolio/static/wbportfolio/markdown/documentation/company.md +78 -0
  12. wbportfolio/static/wbportfolio/markdown/documentation/earnings_instrument.md +14 -0
  13. wbportfolio/static/wbportfolio/markdown/documentation/financial_analysis_instrument_ratios.md +94 -0
  14. wbportfolio/static/wbportfolio/markdown/documentation/financial_statistics.md +44 -0
  15. wbportfolio/static/wbportfolio/markdown/documentation/person.md +70 -0
  16. wbportfolio/templates/portfolio/email/customer_report.html +6 -0
  17. wbportfolio/templates/portfolio/email/customer_trade_notification.html +26 -0
  18. wbportfolio/templates/portfolio/email/email_base_template.html +420 -0
  19. wbportfolio/templates/portfolio/email/rebalancing_report.html +34 -0
  20. wbportfolio/templates/portfolio/macro/macro_review.html +88 -0
  21. {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.6.dist-info}/METADATA +3 -2
  22. {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.6.dist-info}/RECORD +24 -4
  23. {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.6.dist-info}/WHEEL +1 -1
  24. {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1 @@
1
+ [{"model": "wbreport.colorgradient", "pk": 1, "fields": {"title": "Default Color Palette", "colors": "[\"#ffb166\", \"#d1c554\", \"#8cd66b\", \"#5fd785\", \"#05d6a1\", \"#00c1ab\", \"#01abaa\", \"#38c1d7\", \"#70d6ff\", \"#00b1ff\", \"#0585ff\", \"#2a5df2\", \"#5724d9\", \"#823edf\", \"#a359e5\", \"#e13da9\", \"#ef476f\"]"}}, {"model": "wbreport.reportasset", "pk": 1, "fields": {"key": "font-default", "description": "", "text": "", "asset": "report/assets/Roboto-Light.ttf"}}, {"model": "wbreport.reportasset", "pk": 2, "fields": {"key": "font-bd", "description": "", "text": "", "asset": "report/assets/Roboto-Medium.ttf"}}, {"model": "wbreport.reportasset", "pk": 3, "fields": {"key": "font-it", "description": "", "text": "", "asset": "report/assets/Roboto-LightItalic.ttf"}}, {"model": "wbreport.reportcategory", "pk": 1, "fields": {"title": "Test", "order": 0}}, {"model": "wbreport.reportclass", "pk": 1, "fields": {"title": "Base Product Report Class", "class_path": "wbreport.defaults.factsheets.base"}}, {"model": "wbreport.reportclass", "pk": 2, "fields": {"title": "Multindex Report Class", "class_path": "wbreport.defaults.factsheets.multitheme"}}, {"model": "wbreport.reportclass", "pk": 3, "fields": {"title": "Menu Report Class", "class_path": "wbreport.defaults.factsheets.menu"}}, {"model": "wbreport.report", "pk": 1, "fields": {"content_type": 67, "object_id": 566, "file_content_type": "PDF", "category": null, "parent_report": 5, "is_active": true, "file_disabled": false, "base_color": "#70D6FF", "is_private": false, "mailing_list": null, "report_class": 1, "title": "Base Index Factsheet", "namespace": "base-index-factsheet", "logo_file": "", "color_palette": 1}}, {"model": "wbreport.report", "pk": 2, "fields": {"content_type": 67, "object_id": 567, "file_content_type": "PDF", "category": null, "parent_report": 5, "is_active": true, "file_disabled": false, "base_color": "#05D6A1", "is_private": false, "mailing_list": null, "report_class": 1, "title": "Base Product Factsheet", "namespace": "base-product-factsheet", "logo_file": "", "color_palette": 1}}, {"model": "wbreport.report", "pk": 3, "fields": {"content_type": 67, "object_id": 629, "file_content_type": "PDF", "category": null, "parent_report": 5, "is_active": true, "file_disabled": false, "base_color": "#FFB166", "is_private": false, "mailing_list": null, "report_class": 2, "title": "Multi Indexes Factsheet", "namespace": "multi-indexes", "logo_file": "", "color_palette": 1}}, {"model": "wbreport.report", "pk": 4, "fields": {"content_type": 67, "object_id": 613, "file_content_type": "PDF", "category": null, "parent_report": 5, "is_active": true, "file_disabled": false, "base_color": "#EF476F", "is_private": false, "mailing_list": null, "report_class": 1, "title": "Share class Factsheet", "namespace": "share-class-factsheet", "logo_file": "", "color_palette": 1}}, {"model": "wbreport.report", "pk": 5, "fields": {"content_type": null, "object_id": null, "file_content_type": "PDF", "category": null, "parent_report": null, "is_active": true, "file_disabled": false, "base_color": "#FFF000", "is_private": false, "mailing_list": null, "report_class": 3, "title": "Factsheet", "namespace": "factsheet", "logo_file": "", "color_palette": 1}}, {"model": "wbreport.reportversion", "pk": 1, "fields": {"uuid": "9ebd8023-7ebb-4389-a484-410c7c4aad2a", "lookup": "august-2021", "title": "August 2021", "parameters": {"end": "2021-08-31", "start": "2021-07-30"}, "context": {}, "version_date": "2021-11-09", "creation_date": "2021-11-09T10:15:28.539Z", "update_date": "2021-11-09T13:00:00.665Z", "comment": "", "is_primary": true, "disabled": false, "report": 5}}, {"model": "wbreport.reportversion", "pk": 2, "fields": {"uuid": "8a675145-249e-4183-8f19-5cb023fce8c7", "lookup": "factsheet-august-2021", "title": "Share class - August 2021", "parameters": {"end": "2021-08-31", "start": "2021-07-30"}, "context": {}, "version_date": "2021-11-09", "creation_date": "2021-11-09T10:18:20.567Z", "update_date": "2021-11-09T13:00:01.418Z", "comment": "", "is_primary": true, "disabled": false, "report": 4}}, {"model": "wbreport.reportversion", "pk": 4, "fields": {"uuid": "8e186d05-5298-4ab3-bc0b-6a81398e622a", "lookup": "factsheet-mutli-index-august-2021", "title": "Mutli Index - August 2021", "parameters": {"end": "2021-08-31", "start": "2021-07-30"}, "context": {}, "version_date": "2021-11-09", "creation_date": "2021-11-09T10:19:05.102Z", "update_date": "2021-11-09T13:00:01.962Z", "comment": "", "is_primary": true, "disabled": false, "report": 3}}, {"model": "wbreport.reportversion", "pk": 6, "fields": {"uuid": "825c101e-54ec-4197-ad8a-678bcd6431dd", "lookup": "factsheet-base-product-august-2021", "title": "Base product - August 2021", "parameters": {"end": "2021-08-31", "start": "2021-07-30"}, "context": {}, "version_date": "2021-11-09", "creation_date": "2021-11-09T10:19:41.464Z", "update_date": "2021-11-09T13:00:02.271Z", "comment": "", "is_primary": true, "disabled": false, "report": 2}}, {"model": "wbreport.reportversion", "pk": 7, "fields": {"uuid": "5e1014f0-f211-4b81-b272-4d5284d7ef8c", "lookup": "factsheet-base-index-august-2021", "title": "Base Index - August 2021", "parameters": {"end": "2021-08-31", "start": "2021-07-30"}, "context": {}, "version_date": "2021-11-09", "creation_date": "2021-11-09T10:19:54.422Z", "update_date": "2021-11-09T13:00:02.616Z", "comment": "", "is_primary": true, "disabled": false, "report": 1}}]
Binary file
@@ -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,119 @@
1
+ -- CTE for prices that fetch prices per product converted to USD
2
+ WITH prices AS (
3
+ SELECT
4
+ price.date AS date,
5
+ price.instrument_id AS instrument_id,
6
+ price.net_value AS net_value,
7
+ price.net_value * (1 / fx.value) AS net_value_usd
8
+ FROM wbfdm_instrumentprice AS price
9
+ JOIN currency_currencyfxrates AS fx ON price.currency_fx_rate_to_usd_id = fx.id
10
+ WHERE price.calculated = FALSE
11
+ ),
12
+ -- CTE for claims, that fetches approved claims
13
+ claims AS (
14
+ SELECT
15
+ claim.shares,
16
+ claim.date,
17
+ claim.product_id,
18
+ account.tree_id AS tree_id,
19
+ trade.marked_as_internal AS marked_as_internal
20
+ FROM wbportfolio_claim AS claim
21
+ JOIN wbcrm_account AS account
22
+ ON claim.account_id = account.id
23
+ JOIN wbportfolio_trade AS trade
24
+ ON claim.trade_id = trade.transaction_ptr_id
25
+ WHERE claim.status = 'APPROVED'
26
+ ),
27
+ -- CTE for claims, that fetches them with their shares and price at transaction date
28
+ claims_with_nav AS (
29
+ SELECT
30
+ claims.date,
31
+ claims.shares,
32
+ claims.product_id,
33
+ nav.net_value_usd,
34
+ accounts.tree_id AS tree_id,
35
+ trade.marked_as_internal as marked_as_internal
36
+ FROM wbportfolio_claim AS claims
37
+ JOIN wbportfolio_trade AS trade
38
+ ON claims.trade_id = trade.transaction_ptr_id
39
+ LEFT JOIN wbcrm_account AS accounts
40
+ ON claims.account_id = accounts.id
41
+ LEFT JOIN LATERAL (
42
+ SELECT
43
+ price.net_value * (1 / cc.value) AS net_value_usd
44
+ FROM wbfdm_instrumentprice AS price
45
+ LEFT JOIN currency_currencyfxrates AS cc
46
+ ON cc.id = price.currency_fx_rate_to_usd_id
47
+ WHERE
48
+ price.instrument_id = claims.product_id
49
+ AND price.date <= claims.date
50
+ AND claims.status = 'APPROVED'
51
+ AND claims.account_id IS NOT NULL
52
+ AND price.calculated = FALSE
53
+ ORDER BY price.date DESC
54
+ LIMIT 1
55
+ ) AS nav ON TRUE
56
+ -- CTE to generate a series with year and month already extracted
57
+ ), series AS (
58
+ SELECT
59
+ (DATE_TRUNC('month', dts) + INTERVAL '1 month - 1 day')::DATE AS val_date,
60
+ EXTRACT(YEAR FROM dts) AS year,
61
+ EXTRACT(MONTH FROM dts) AS month
62
+ FROM GENERATE_SERIES('{{ from_date | identifier }}'::DATE, '{{ to_date | identifier }}'::DATE, '1 month'::INTERVAL) AS dts
63
+ )
64
+
65
+ SELECT
66
+ series.year AS year,
67
+ series.month AS month,
68
+ SUM(NAV.net_value_usd * HOLDINGS.sum_shares) AS total_assets,
69
+ SUM(COALESCE(NNM.nnm_usd, 0)) AS net_new_money
70
+
71
+ FROM series
72
+ -- Cartesian product with products, to get each item of the series
73
+ -- for each product
74
+ CROSS JOIN wbportfolio_product AS product
75
+
76
+ -- If there is not product_id it means we filter for all products
77
+ -- This means we have to filter out all products that are not
78
+ -- invested to avoid double accounting
79
+
80
+ -- Foreach product/date combination, get the last NAV date available
81
+ LEFT JOIN LATERAL (
82
+ SELECT prices.net_value_usd
83
+ FROM prices
84
+ WHERE
85
+ prices.date <= series.val_date
86
+ AND prices.instrument_id = product.instrument_ptr_id
87
+ ORDER BY prices.date DESC
88
+ LIMIT 1
89
+ ) AS NAV ON TRUE
90
+ -- For each product/date combination, get the number of shares
91
+ LEFT JOIN LATERAL (
92
+ SELECT SUM(claims.shares) AS sum_shares
93
+ FROM claims
94
+ WHERE claims.date <= series.val_date
95
+ AND claims.product_id = product.instrument_ptr_id
96
+ {% if account_tree_id %}AND claims.tree_id={{ account_tree_id }}{% endif %}
97
+ {% if not product_id %}AND claims.marked_as_internal=False{% endif %}
98
+ ) AS HOLDINGS ON TRUE
99
+ -- For each product/date combination, get the NNM
100
+ LEFT JOIN LATERAL (
101
+ -- Get the number of shares that were created in the month of date (from the series)
102
+ SELECT
103
+ SUM(claims.shares * claims.net_value_usd) AS nnm_usd,
104
+ SUM(claims.shares) as shares
105
+ FROM claims_with_nav as claims
106
+ WHERE
107
+ EXTRACT(YEAR FROM claims.date) = series.year
108
+ AND EXTRACT(MONTH FROM claims.date) = series.month
109
+ AND claims.product_id = product.instrument_ptr_id
110
+ {% if account_tree_id %}AND claims.tree_id={{ account_tree_id }}{% endif %}
111
+ {% if not product_id %}AND claims.marked_as_internal=False{% endif %}
112
+ ) AS NNM ON TRUE
113
+
114
+ -- if we pass in the product_id, we only display the data for this particular product
115
+ {% if product_id %}
116
+ WHERE product.instrument_ptr_id = {{ product_id }}
117
+ {% endif %}
118
+
119
+ GROUP BY series.year, series.month
@@ -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
+ ]
@@ -0,0 +1,17 @@
1
+ .page-header {
2
+ background-color: #051f2d;
3
+ text-align: center;
4
+ font-size: 30px;
5
+ color: #fff;
6
+ border-radius: 0;
7
+ }
8
+
9
+ strong {
10
+ color: #f25d47;
11
+ font-weight: 700;
12
+ white-space: nowrap;
13
+ }
14
+
15
+ .page-content {
16
+ padding: 0 30px;
17
+ }
@@ -0,0 +1,16 @@
1
+ ### Welcome to the Account Holding Reconciliation. Please confirm your holdings:
2
+
3
+ * **Confirm Holdings**: Press the **'Agree'** button.
4
+ * **Disagree with Holdings**: Adjust your holdings and press the **'Communicate Differences'** button. (This button will appear after adjustments are made.)
5
+
6
+ ### How to Adjust Your Holdings:
7
+
8
+ * Select the **shares** or **nominal value** cell in the "Your Input" column.
9
+ * Press **Enter**, adjust the value, and press **Enter** again.
10
+ * Alternatively, right-click the cell and select **'Edit'** to adjust the value.
11
+
12
+ ### Additional Options:
13
+
14
+ * **View all Subscriptions/Redemptions**: Press the **'Show Subscriptions/Redemptions'** button.
15
+ * **Add a Subscription/Redemption**: Press the **'Add Subscription/Redemption'** button.
16
+ * **Single Product Actions**: Right-click any row below to display all Subscriptions/Redemptions or add a new Subscription/Redemption for a specific product.
@@ -0,0 +1,25 @@
1
+ # Aggregate AssetPosition Liquidity Table View
2
+
3
+ The purpose of this view is to observe how fast an aggregated asset position
4
+ can be sold across all portfolios. We have two columns for two different dates.
5
+ One column shows the velocity and the other one shows the relative percent of
6
+ the total AuM (Asset under Management), "Days to Liquidate" and "Percent AUM"
7
+ respectively.
8
+
9
+ Some good things to know:
10
+
11
+ - We use the mean volume for the last 50 days.
12
+
13
+ - One third of the average volume for an asset position is
14
+ used to estimate the velocity. This number is subjective and attempts
15
+ to represent the number of trades that can be made.
16
+
17
+ - If we have a value for more than 3 days to sell an asset position,
18
+ the latter will be displayed in yellow. If this value is greater than 5,
19
+ it will be in red.
20
+
21
+ - We can use a filter that only displays asset positions that have a date
22
+ to liquidate greater than a given value for the Historic Date.
23
+
24
+ - The "Compared Date" is used to compare the asset position with the Historic Date.
25
+ Therefore, the values are not sorted for the last two columns.
@@ -0,0 +1,78 @@
1
+ # Companies
2
+ A list of every company saved in the database.
3
+
4
+ ## Columns:
5
+ Each column title has three lines on the right if you hover over it. Click on them to show options for that column. The second tab of the options menu will allow you to filter the column, the third to completely hide entire columns. Click on anywhere else on the column to order it, cycling between ascending, descending and no ordering. Hold shift while clicking to order multiple columns with individual weights.
6
+
7
+ ### Name:
8
+ The name of the company.
9
+
10
+ ### City:
11
+ The city of the company's primary address. Hover over the name to display the full address.
12
+
13
+ ### Type:
14
+ The company's type. Company types can be added, edited and deleted by managers under CRM > Administration > Company Types.
15
+
16
+ ### Tier:
17
+ The company's tier. Tiers range from 1 to 5 and are automatically computed like this:
18
+ _Company is of status client or TPM:_
19
+ | Invested AUM / AUM | Tier |
20
+ | -------- | --------|
21
+ | > 0.1 | Tier 1 |
22
+ | 0.05 - 0.1 | Tier 2 |
23
+ | 0.02 - 0.05 | Tier 3 |
24
+ | 0.01 - 0.02 | Tier 4 |
25
+ | default | Tier 5 |
26
+
27
+ _Company is *not* of status client or TPM:_
28
+ | AUM | Tier |
29
+ | -------- | --------|
30
+ | > 10,000,000,000 $ | Tier 1 |
31
+ | 5,000,000,000 \$ - 10,000,000,000 $ | Tier 2 |
32
+ | 1,000,000,000 \$ - 5,000,000,000 $ | Tier 3 |
33
+ | 500,000,000 \$ - 1,000,000,000 $ | Tier 4 |
34
+ | default | Tier 5 |
35
+
36
+ ### Status:
37
+ The company's status. Company statuses can be added, edited and deleted by managers under CRM > Administration > Customer Status.
38
+
39
+ ### AUM Invested:
40
+ The company's assets under management managed by the main company.
41
+
42
+ ### AUM:
43
+ Total assets under management for the company.
44
+
45
+ ### Potential:
46
+ The potential reflects how much potential a company (regardless if client/propective) has. Formula: AUM * (Asset Allocation * Asset Allocation Max Investment) - Invested AUM. The field is populated automatically.
47
+
48
+ ### Primary Relationship Manager:
49
+ The primary relationship manager of this company. Hover over the name to display further information about the person.
50
+
51
+ ### Last Activity:
52
+ The name and end date of the company's last activity. Hover over it to display further information about the activity. Column can also be ordered by end date of the activity.
53
+
54
+ ### Activity Heat:
55
+ A visual representation of the company's activeness. The data representation ranges from 0 to 1 which needs to be kept in mind to be able to apply a proper filter to the column.
56
+
57
+ ## Filters:
58
+
59
+ ### No Activity:
60
+ Filter companies with no activity for x amount of time.
61
+
62
+ ### Companies' Employee:
63
+ Display employers of the selected person.
64
+
65
+ ## Search Field:
66
+ Typing in the search field allows to filter the companies by name.
67
+
68
+ ## Buttons:
69
+ You can right-click each row to reveal three buttons:
70
+
71
+ ### Activities:
72
+ Displays activities for the row's company.
73
+
74
+ ### Employees:
75
+ Displays a list of the company's employees.
76
+
77
+ ### Relationship Managers:
78
+ Displays all relationship managers of the row's company.
@@ -0,0 +1,14 @@
1
+ # Earnings Analysis
2
+
3
+ Consensus $ EPS
4
+
5
+ ## Filters
6
+
7
+ A set of filters that control the analysis is available:
8
+
9
+ 1. **Analysis**: EPS ($)
10
+ - **$ EPS [default view]**: By default shows an aggregated $ EPS (weighted).
11
+
12
+ 2. **Period**: Common periods (TTM, FTM, FY+1, FY+2) are available with **FTW** set by default.
13
+
14
+ 3. **Show related**: Adds lines for all specified peers if available
@@ -0,0 +1,94 @@
1
+ # Relative valuation
2
+
3
+ Classic and commonly used relative ratios PE, PEG, PS, PFCF, EV-EBITDA
4
+ Additional comparative metrics: growth (EPS, Sales, FCF), EV, Market Cap
5
+
6
+ ## Filters
7
+
8
+ A set of filter is available:
9
+ 1. **Output**: Chart, Table (Last Value), Table (Time-series)
10
+ - **Chart [default view]**: By default shows all ratios for the instrument in question in one figure with PEG on a secondary/right axis.
11
+ - **Table (Last Value)**: By default shows the last available value for all ratios for the instrument in question in one table.
12
+ - **Table (Time-series)**: Shows all available values for all ratios for the instrument in question in one table.
13
+
14
+ 2. **Period**: Common periods (TTM, FTW, FY+1, FY+2) are available with **FTW** set by default.
15
+
16
+ 3. **Versus related (not available in the Time-series view):**
17
+ - if the view "**Table (Last Value)**" is set, then all available related instruments (peers, partners, suppliers, etc.) will be shown in the same table, including Mean and Median values.
18
+ - if the view "**Chart**" is set, then all available related instruments (peers, partners, suppliers, etc.) will be shown in the same chart. Additionally, the user may choose which variables to show on X,Y,and Bubble Size axis. Median line may be disactivated, if desired.
19
+
20
+ 4. **Draw ranges**: if the view "**Chart**" is set, then each ratio will be shown on a separate subplot with historic min-max ranges shown with dashed lines.
21
+
22
+ 5. **Range type**: if the view "**Chart**" is set and "**Draw ranges**" is activated, then a user may choose the type of "min-max" range: Min-Max (entire period) or Rolling.
23
+ - **"Min-Max (entire period)"** looks for the min and max values for each ratio across the entire period in questions and sets it for the entire graph.
24
+ - **"Rolling"** looks for the min and max values for each ratio across the "Rolling Period" specified in a separate filter.
25
+
26
+ 6. **Rolling period**: works with "**Range type**" filter if the view "**Chart**" is set and "**Draw ranges**" is activated.
27
+
28
+ 7. **Clean data** (more "treatment"): significant and heavy data cleaning will be applied to all variables. For **each** series:
29
+ - All raw input data is forward and then backward filled.
30
+ - All empty rows are deleted and remaining blanks for all variables are filled by linear interpolation.
31
+ - Ratios are then normally calculated.
32
+ - The resulting series are cleaned for outliers (values in the top and bottom 5% are deleted).
33
+ - All negative (incl. EV/S) and NaN values are set to Null (deleted as "non-meaningful").
34
+ - All values above a certain threshold (by default 100) are deleted. **Exceptions** are:
35
+ - PEG above 15
36
+ - PS above 40
37
+ - The remaining series are forward and backward filled to clean missing values for smoother series.
38
+ - All series are smoothed (3-day simple moving average). The **PEG** series is smoothed differently (10-day simple moving average).
39
+
40
+ 8. **X-Axis**: works if the view "**Chart**" is set and "**Versus related**" is activated. Controls the variable shown on the x-axis.
41
+
42
+ 9. **Y-Axis**: works if the view "**Chart**" is set and "**Versus related**" is activated. Controls the variable shown on the y-axis.
43
+
44
+ 10. **Z-Axis (bubble size)**: works if the view "**Chart**" is set and "**Versus related**" is activated. Controls the variable shown on the z-axis/bubble size.
45
+
46
+ 11. **Median**: works if the view "**Chart**" is set and "**Versus related**" is activated. Calculates the median for **X-Axis** and **Y-Axis** variables across the entire period range.
47
+
48
+ ## Formulas
49
+
50
+ #### FY+1 and FY+2
51
+
52
+ $$
53
+ Ratio_{t} = Price_{t} / Ratio\: Item_{t}
54
+ $$
55
+
56
+ $
57
+ where \\
58
+ $
59
+ $$
60
+ t = day \\
61
+ $$
62
+ $
63
+ Ratio\: Item =
64
+ $
65
+ $$
66
+ EPS = Daily\: Consensus\: Net\: Profit\: Forecast / Number\:of\:shares\\
67
+ or \\
68
+ Revenue\: per\: share = Daily\: Consensus\: Revenue\: Forecast / Number\:of\:shares\\
69
+ or \\
70
+ EBITDA\: per\: share = Daily\: Consensus\: EBITDA\; Forecast / Number\:of\:shares\\
71
+ or \\
72
+ FCF = Daily\: Consensus\: FreeCashFlow\; Forecast / Number\:of\:shares\\
73
+ $$
74
+
75
+ #### Trailing Twelve Month (TTM) and Next Twelve Month (NTM) *also known as Forward Twelve Months (FTM)*
76
+ \
77
+ $
78
+ Ratio\: Item =
79
+ $
80
+ $$
81
+ EPS = Daily\: EPS\: TTM\: \\
82
+ or \\
83
+ Revenue\: or\: EBITDA\: or\: FCF\: per\: share = Interpolated\: between\: consensus\: FY+2\:,\: FY+1 \\
84
+ Revenue\: or\: EBITDA\: or\: FCF\:per\: share =Interpolated\: between\: FY+1\:(current)\:,\: FY0\:(last)\:,\: FY-1\:(previous) \\
85
+ $$
86
+
87
+ #### Trailing Twelve Month (TTM) and Next Twelve Month (NTM) *also known as Forward Twelve Months (FTM)*
88
+ \
89
+ $
90
+ DailyPEG_{fy+1} = Price / EPS_{fy+1} / Growth_{fy+1\: vs\: fy0} \\
91
+ DailyPEG_{fy+2} = Price / EPS_{fy+2} / Growth_{geoavg(fy+2\: vs\: fy1,\:fy+1\: vs\: fy0)} \\
92
+ DailyPEG_{NTM} = Price / EPS_{NTM} / Growth_{ntm\: vs\: ttm} \\
93
+ DailyPEG_{TTM} = Price / EPS_{TTM} / Growth_{ttm\: vs\: ttm_{t-1}} \\
94
+ $