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.
- wbportfolio/fixtures/product_factsheets.yaml +1 -0
- wbportfolio/fixtures/wbportfolio.yaml.gz +0 -0
- wbportfolio/fixtures/wbrisk_management.yaml.gz +0 -0
- wbportfolio/import_export/backends/refinitiv/adjustment.py +40 -0
- wbportfolio/jinja2/wbportfolio/sql/aum_nnm.sql +119 -0
- wbportfolio/kpi_handlers/nnm.py +163 -0
- wbportfolio/models/llm/wbcrm/analyze_relationship.py +58 -0
- wbportfolio/static/wbportfolio/css/macro_review.css +17 -0
- wbportfolio/static/wbportfolio/markdown/documentation/account_holding_reconciliation.md +16 -0
- wbportfolio/static/wbportfolio/markdown/documentation/aggregate_asset_position_liquidity.md +25 -0
- wbportfolio/static/wbportfolio/markdown/documentation/company.md +78 -0
- wbportfolio/static/wbportfolio/markdown/documentation/earnings_instrument.md +14 -0
- wbportfolio/static/wbportfolio/markdown/documentation/financial_analysis_instrument_ratios.md +94 -0
- wbportfolio/static/wbportfolio/markdown/documentation/financial_statistics.md +44 -0
- wbportfolio/static/wbportfolio/markdown/documentation/person.md +70 -0
- wbportfolio/templates/portfolio/email/customer_report.html +6 -0
- wbportfolio/templates/portfolio/email/customer_trade_notification.html +26 -0
- wbportfolio/templates/portfolio/email/email_base_template.html +420 -0
- wbportfolio/templates/portfolio/email/rebalancing_report.html +34 -0
- wbportfolio/templates/portfolio/macro/macro_review.html +88 -0
- {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.6.dist-info}/METADATA +3 -2
- {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.6.dist-info}/RECORD +24 -4
- {wbportfolio-2.2.4.dist-info → wbportfolio-2.2.6.dist-info}/WHEEL +1 -1
- {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
|
|
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
|
+
$
|