wbcommission 2.2.1__tar.gz
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 wbcommission might be problematic. Click here for more details.
- wbcommission-2.2.1/.gitignore +181 -0
- wbcommission-2.2.1/PKG-INFO +11 -0
- wbcommission-2.2.1/pyproject.toml +38 -0
- wbcommission-2.2.1/wbcommission/__init__.py +1 -0
- wbcommission-2.2.1/wbcommission/admin/__init__.py +4 -0
- wbcommission-2.2.1/wbcommission/admin/accounts.py +22 -0
- wbcommission-2.2.1/wbcommission/admin/commission.py +85 -0
- wbcommission-2.2.1/wbcommission/admin/rebate.py +7 -0
- wbcommission-2.2.1/wbcommission/analytics/__init__.py +0 -0
- wbcommission-2.2.1/wbcommission/analytics/marginality.py +181 -0
- wbcommission-2.2.1/wbcommission/apps.py +5 -0
- wbcommission-2.2.1/wbcommission/dynamic_preferences_registry.py +0 -0
- wbcommission-2.2.1/wbcommission/factories/__init__.py +9 -0
- wbcommission-2.2.1/wbcommission/factories/commission.py +100 -0
- wbcommission-2.2.1/wbcommission/factories/rebate.py +16 -0
- wbcommission-2.2.1/wbcommission/filters/__init__.py +7 -0
- wbcommission-2.2.1/wbcommission/filters/rebate.py +187 -0
- wbcommission-2.2.1/wbcommission/filters/signals.py +44 -0
- wbcommission-2.2.1/wbcommission/generators/__init__.py +2 -0
- wbcommission-2.2.1/wbcommission/generators/rebate_generator.py +93 -0
- wbcommission-2.2.1/wbcommission/migrations/0001_initial.py +299 -0
- wbcommission-2.2.1/wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
- wbcommission-2.2.1/wbcommission/migrations/0003_alter_commission_account.py +24 -0
- wbcommission-2.2.1/wbcommission/migrations/0004_rebate_audit_log.py +19 -0
- wbcommission-2.2.1/wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
- wbcommission-2.2.1/wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
- wbcommission-2.2.1/wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
- wbcommission-2.2.1/wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
- wbcommission-2.2.1/wbcommission/migrations/__init__.py +0 -0
- wbcommission-2.2.1/wbcommission/models/__init__.py +9 -0
- wbcommission-2.2.1/wbcommission/models/account_service.py +217 -0
- wbcommission-2.2.1/wbcommission/models/commission.py +679 -0
- wbcommission-2.2.1/wbcommission/models/rebate.py +319 -0
- wbcommission-2.2.1/wbcommission/models/signals.py +45 -0
- wbcommission-2.2.1/wbcommission/permissions.py +6 -0
- wbcommission-2.2.1/wbcommission/reports/__init__.py +0 -0
- wbcommission-2.2.1/wbcommission/reports/audit_report.py +51 -0
- wbcommission-2.2.1/wbcommission/reports/customer_report.py +299 -0
- wbcommission-2.2.1/wbcommission/reports/utils.py +30 -0
- wbcommission-2.2.1/wbcommission/serializers/__init__.py +3 -0
- wbcommission-2.2.1/wbcommission/serializers/commissions.py +26 -0
- wbcommission-2.2.1/wbcommission/serializers/rebate.py +87 -0
- wbcommission-2.2.1/wbcommission/serializers/signals.py +27 -0
- wbcommission-2.2.1/wbcommission/tests/__init__.py +0 -0
- wbcommission-2.2.1/wbcommission/tests/analytics/__init__.py +0 -0
- wbcommission-2.2.1/wbcommission/tests/analytics/test_marginality.py +253 -0
- wbcommission-2.2.1/wbcommission/tests/conftest.py +89 -0
- wbcommission-2.2.1/wbcommission/tests/models/__init__.py +0 -0
- wbcommission-2.2.1/wbcommission/tests/models/mixins.py +22 -0
- wbcommission-2.2.1/wbcommission/tests/models/test_account_service.py +293 -0
- wbcommission-2.2.1/wbcommission/tests/models/test_commission.py +587 -0
- wbcommission-2.2.1/wbcommission/tests/models/test_rebate.py +136 -0
- wbcommission-2.2.1/wbcommission/tests/signals.py +0 -0
- wbcommission-2.2.1/wbcommission/tests/test_permissions.py +66 -0
- wbcommission-2.2.1/wbcommission/tests/viewsets/__init__.py +0 -0
- wbcommission-2.2.1/wbcommission/tests/viewsets/test_rebate.py +76 -0
- wbcommission-2.2.1/wbcommission/urls.py +42 -0
- wbcommission-2.2.1/wbcommission/viewsets/__init__.py +7 -0
- wbcommission-2.2.1/wbcommission/viewsets/buttons/__init__.py +2 -0
- wbcommission-2.2.1/wbcommission/viewsets/buttons/rebate.py +46 -0
- wbcommission-2.2.1/wbcommission/viewsets/buttons/signals.py +53 -0
- wbcommission-2.2.1/wbcommission/viewsets/commissions.py +21 -0
- wbcommission-2.2.1/wbcommission/viewsets/display/__init__.py +5 -0
- wbcommission-2.2.1/wbcommission/viewsets/display/commissions.py +21 -0
- wbcommission-2.2.1/wbcommission/viewsets/display/rebate.py +117 -0
- wbcommission-2.2.1/wbcommission/viewsets/endpoints/__init__.py +4 -0
- wbcommission-2.2.1/wbcommission/viewsets/endpoints/commissions.py +0 -0
- wbcommission-2.2.1/wbcommission/viewsets/endpoints/rebate.py +21 -0
- wbcommission-2.2.1/wbcommission/viewsets/menu/__init__.py +1 -0
- wbcommission-2.2.1/wbcommission/viewsets/menu/commissions.py +0 -0
- wbcommission-2.2.1/wbcommission/viewsets/menu/rebate.py +13 -0
- wbcommission-2.2.1/wbcommission/viewsets/mixins.py +39 -0
- wbcommission-2.2.1/wbcommission/viewsets/rebate.py +481 -0
- wbcommission-2.2.1/wbcommission/viewsets/titles/__init__.py +1 -0
- wbcommission-2.2.1/wbcommission/viewsets/titles/commissions.py +0 -0
- wbcommission-2.2.1/wbcommission/viewsets/titles/rebate.py +11 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
~
|
|
2
|
+
|
|
3
|
+
# Docker volumes
|
|
4
|
+
volumes/
|
|
5
|
+
# Poetry auth file
|
|
6
|
+
auth.toml
|
|
7
|
+
|
|
8
|
+
media/*
|
|
9
|
+
media/
|
|
10
|
+
mediafiles/
|
|
11
|
+
mediafiles/*
|
|
12
|
+
test/*
|
|
13
|
+
staticfiles/*
|
|
14
|
+
staticfiles/
|
|
15
|
+
#
|
|
16
|
+
# Byte-compiled / optimized / DLL files
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*$py.class
|
|
20
|
+
|
|
21
|
+
# C extensions
|
|
22
|
+
*.so
|
|
23
|
+
|
|
24
|
+
# Distribution / packaging
|
|
25
|
+
.Python
|
|
26
|
+
build/
|
|
27
|
+
develop-eggs/
|
|
28
|
+
dist/
|
|
29
|
+
info/
|
|
30
|
+
downloads/
|
|
31
|
+
eggs/
|
|
32
|
+
.eggs/
|
|
33
|
+
lib/
|
|
34
|
+
lib64/
|
|
35
|
+
parts/
|
|
36
|
+
sdist/
|
|
37
|
+
var/
|
|
38
|
+
wheels/
|
|
39
|
+
share/python-wheels/
|
|
40
|
+
*.egg-info/
|
|
41
|
+
.installed.cfg
|
|
42
|
+
*.egg
|
|
43
|
+
MANIFEST
|
|
44
|
+
|
|
45
|
+
# PyInstaller
|
|
46
|
+
# Usually these files are written by a python script from a template
|
|
47
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
48
|
+
*.manifest
|
|
49
|
+
*.spec
|
|
50
|
+
|
|
51
|
+
# Installer logs
|
|
52
|
+
pip-log.txt
|
|
53
|
+
pip-delete-this-directory.txt
|
|
54
|
+
|
|
55
|
+
# Unit test / coverage reports
|
|
56
|
+
htmlcov/
|
|
57
|
+
.tox/
|
|
58
|
+
.nox/
|
|
59
|
+
.coverage
|
|
60
|
+
.coverage.*
|
|
61
|
+
.cache
|
|
62
|
+
.dccache
|
|
63
|
+
nosetests.xml
|
|
64
|
+
coverage.xml
|
|
65
|
+
*.cover
|
|
66
|
+
*.py,cover
|
|
67
|
+
.hypothesis/
|
|
68
|
+
.pytest_cache/
|
|
69
|
+
cover/
|
|
70
|
+
report.xml
|
|
71
|
+
*/report.xml
|
|
72
|
+
|
|
73
|
+
# Translations
|
|
74
|
+
*.mo
|
|
75
|
+
*.pot
|
|
76
|
+
|
|
77
|
+
# Django stuff:
|
|
78
|
+
*.log
|
|
79
|
+
local_settings.py
|
|
80
|
+
*.sqlite3
|
|
81
|
+
db.sqlite3-journal
|
|
82
|
+
|
|
83
|
+
# Flask stuff:
|
|
84
|
+
instance/
|
|
85
|
+
.webassets-cache
|
|
86
|
+
|
|
87
|
+
# Scrapy stuff:
|
|
88
|
+
.scrapy
|
|
89
|
+
|
|
90
|
+
# Sphinx documentation
|
|
91
|
+
docs/_build/
|
|
92
|
+
|
|
93
|
+
# PyBuilder
|
|
94
|
+
.pybuilder/
|
|
95
|
+
target/
|
|
96
|
+
|
|
97
|
+
# Jupyter Notebook
|
|
98
|
+
.ipynb_checkpoints
|
|
99
|
+
*.ipynb
|
|
100
|
+
# IPython
|
|
101
|
+
profile_default/
|
|
102
|
+
ipython_config.py
|
|
103
|
+
|
|
104
|
+
# pyenv
|
|
105
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
106
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
107
|
+
# .python-version
|
|
108
|
+
|
|
109
|
+
# pipenv
|
|
110
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
111
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
112
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
113
|
+
# install all needed dependencies.
|
|
114
|
+
#Pipfile.lock
|
|
115
|
+
|
|
116
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
117
|
+
__pypackages__/
|
|
118
|
+
|
|
119
|
+
# Celery stuff
|
|
120
|
+
celerybeat-schedule
|
|
121
|
+
celerybeat.pid
|
|
122
|
+
|
|
123
|
+
# SageMath parsed files
|
|
124
|
+
*.sage.py
|
|
125
|
+
|
|
126
|
+
# Environments
|
|
127
|
+
.env
|
|
128
|
+
.envrc
|
|
129
|
+
.venv
|
|
130
|
+
env/
|
|
131
|
+
venv/
|
|
132
|
+
ENV/
|
|
133
|
+
env.bak/
|
|
134
|
+
venv.bak/
|
|
135
|
+
.vscode/
|
|
136
|
+
.idea/
|
|
137
|
+
.idea.bkp/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
crm/
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# Gitlab Runner
|
|
164
|
+
builds
|
|
165
|
+
builds/
|
|
166
|
+
|
|
167
|
+
# Integrator Office 365 : reverse proxy tunnel for outlook365
|
|
168
|
+
ngrok
|
|
169
|
+
*/ngrok
|
|
170
|
+
/modules/**/system/
|
|
171
|
+
|
|
172
|
+
/modules/wbmailing/files/*
|
|
173
|
+
/modules/wbmailing/mailing/*
|
|
174
|
+
|
|
175
|
+
/projects/*/requirements.txt
|
|
176
|
+
public
|
|
177
|
+
|
|
178
|
+
# Ignore archive localization generated folder
|
|
179
|
+
backend/modules/**/archive/*
|
|
180
|
+
**/**/requirements.txt
|
|
181
|
+
CHANGELOG-*
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: wbcommission
|
|
3
|
+
Version: 2.2.1
|
|
4
|
+
Summary: A workbench module for managing human resources.
|
|
5
|
+
Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
|
|
6
|
+
Requires-Dist: reportlab==3.*
|
|
7
|
+
Requires-Dist: wbcompliance
|
|
8
|
+
Requires-Dist: wbcore
|
|
9
|
+
Requires-Dist: wbcrm
|
|
10
|
+
Requires-Dist: wbfdm
|
|
11
|
+
Requires-Dist: wbnews
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wbcommission"
|
|
3
|
+
description = "A workbench module for managing human resources."
|
|
4
|
+
authors = [{ name = "Christopher Wittlinger", email = "c.wittlinger@stainly.com"}]
|
|
5
|
+
dynamic = ["version"]
|
|
6
|
+
|
|
7
|
+
dependencies = [
|
|
8
|
+
"wbcore",
|
|
9
|
+
"wbcrm",
|
|
10
|
+
"wbnews",
|
|
11
|
+
"wbfdm",
|
|
12
|
+
"wbcompliance",
|
|
13
|
+
"reportlab == 3.*",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.uv.sources]
|
|
17
|
+
wbcore = { workspace = true }
|
|
18
|
+
wbcrm = { workspace = true }
|
|
19
|
+
wbnews = { workspace = true }
|
|
20
|
+
wbfdm = { workspace = true }
|
|
21
|
+
wbcompliance = { workspace = true }
|
|
22
|
+
|
|
23
|
+
[tool.uv]
|
|
24
|
+
package = true
|
|
25
|
+
|
|
26
|
+
[tool.hatch.version]
|
|
27
|
+
path = "../../pyproject.toml"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.sdist]
|
|
30
|
+
include = ["wbcommission/*"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["wbcommission"]
|
|
34
|
+
only-packages = true
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from wbcommission.models.rebate import manage_rebate_as_task
|
|
3
|
+
from wbcrm.admin.accounts import AccountModelAdmin as BaseAccountModelAdmin
|
|
4
|
+
from wbcrm.models.accounts import Account
|
|
5
|
+
|
|
6
|
+
from .commission import CommissionTabularInline
|
|
7
|
+
|
|
8
|
+
admin.site.unregister(Account)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@admin.register(Account)
|
|
12
|
+
class AccountModelAdmin(BaseAccountModelAdmin):
|
|
13
|
+
def make_rebates(self, request, queryset):
|
|
14
|
+
for account in queryset:
|
|
15
|
+
manage_rebate_as_task.delay(account.id)
|
|
16
|
+
|
|
17
|
+
def get_assets_under_management(self, request, queryset):
|
|
18
|
+
for account in queryset:
|
|
19
|
+
account.get_assets_under_management()
|
|
20
|
+
|
|
21
|
+
actions = list(BaseAccountModelAdmin.actions) + [make_rebates, get_assets_under_management]
|
|
22
|
+
inlines = BaseAccountModelAdmin.inlines + [CommissionTabularInline]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from wbcommission.models import (
|
|
3
|
+
Commission,
|
|
4
|
+
CommissionExclusionRule,
|
|
5
|
+
CommissionRole,
|
|
6
|
+
CommissionRule,
|
|
7
|
+
CommissionType,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@admin.register(CommissionType)
|
|
12
|
+
class CommissionTypeModelAdmin(admin.ModelAdmin):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommissionRuleTabularInline(admin.TabularInline):
|
|
17
|
+
ordering = ("timespan__startswith", "assets_under_management_range__startswith")
|
|
18
|
+
fields = (
|
|
19
|
+
"timespan",
|
|
20
|
+
"assets_under_management_range",
|
|
21
|
+
"percent",
|
|
22
|
+
)
|
|
23
|
+
model = CommissionRule
|
|
24
|
+
extra = 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommissionRoleTabularInline(admin.TabularInline):
|
|
28
|
+
model = CommissionRole
|
|
29
|
+
autocomplete_fields = ["person"]
|
|
30
|
+
fields = (
|
|
31
|
+
"person",
|
|
32
|
+
"commission",
|
|
33
|
+
)
|
|
34
|
+
extra = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CommissionTabularInline(admin.TabularInline):
|
|
38
|
+
extra = 0
|
|
39
|
+
model = Commission
|
|
40
|
+
fields = [
|
|
41
|
+
"order",
|
|
42
|
+
"crm_recipient",
|
|
43
|
+
"account_role_type_recipient",
|
|
44
|
+
"portfolio_role_recipient",
|
|
45
|
+
"commission_type",
|
|
46
|
+
"net_commission",
|
|
47
|
+
"is_hidden",
|
|
48
|
+
"exclusion_rule_account_role_type",
|
|
49
|
+
]
|
|
50
|
+
readonly_fields = ["order"]
|
|
51
|
+
ordering = ["order"]
|
|
52
|
+
autocomplete_fields = ["account", "crm_recipient"]
|
|
53
|
+
show_change_link = True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@admin.register(Commission)
|
|
57
|
+
class CommissionModelAdmin(admin.ModelAdmin):
|
|
58
|
+
list_display = [
|
|
59
|
+
"account",
|
|
60
|
+
"crm_recipient",
|
|
61
|
+
"portfolio_role_recipient",
|
|
62
|
+
"account_role_type_recipient",
|
|
63
|
+
"order",
|
|
64
|
+
"net_commission",
|
|
65
|
+
"commission_type",
|
|
66
|
+
"is_hidden",
|
|
67
|
+
"exclusion_rule_account_role_type",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
autocomplete_fields = ["account", "crm_recipient"]
|
|
71
|
+
|
|
72
|
+
inlines = [CommissionRoleTabularInline, CommissionRuleTabularInline]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@admin.register(CommissionExclusionRule)
|
|
76
|
+
class CommissionExclusionRuleAdmin(admin.ModelAdmin):
|
|
77
|
+
list_display = [
|
|
78
|
+
"product",
|
|
79
|
+
"commission_type",
|
|
80
|
+
"account_role_type",
|
|
81
|
+
"timespan",
|
|
82
|
+
"overriding_percent",
|
|
83
|
+
"overriding_net_or_gross_commission",
|
|
84
|
+
]
|
|
85
|
+
autocomplete_fields = ["product", "account_role_type"]
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from django.db.models import Case, OuterRef, Subquery, Value, When
|
|
7
|
+
from django.db.models.functions import Coalesce
|
|
8
|
+
from wbcommission.models import CommissionType, Rebate
|
|
9
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
10
|
+
from wbfdm.models import InstrumentPrice
|
|
11
|
+
from wbportfolio.models import Fees
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MarginalityCalculator:
|
|
15
|
+
FEE_MAP = {
|
|
16
|
+
"MANAGEMENT": "management",
|
|
17
|
+
"PERFORMANCE": "performance",
|
|
18
|
+
"PERFORMANCE_CRYSTALIZED": "performance",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def __init__(self, products, from_date: date, to_date: date):
|
|
22
|
+
products = products.annotate(
|
|
23
|
+
fx_rate=Coalesce(
|
|
24
|
+
Subquery(
|
|
25
|
+
CurrencyFXRates.objects.filter(
|
|
26
|
+
currency=OuterRef("currency"), date=OuterRef("last_valuation_date")
|
|
27
|
+
).values("value")[:1]
|
|
28
|
+
),
|
|
29
|
+
Decimal(1.0),
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
self.fx_rates = (
|
|
34
|
+
pd.DataFrame(products.values_list("id", "fx_rate"), columns=["id", "fx_rate"])
|
|
35
|
+
.set_index("id")["fx_rate"]
|
|
36
|
+
.astype(float)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# compute net marginality
|
|
40
|
+
self.df_aum = pd.DataFrame(
|
|
41
|
+
InstrumentPrice.objects.annotate_base_data()
|
|
42
|
+
.filter(instrument__in=products, date__gte=from_date, date__lte=to_date)
|
|
43
|
+
.values_list("calculated", "net_value_usd", "date", "outstanding_shares", "instrument"),
|
|
44
|
+
columns=["calculated", "net_value_usd", "date", "outstanding_shares", "instrument"],
|
|
45
|
+
).rename(columns={"instrument": "id"})
|
|
46
|
+
self.df_aum["date"] = pd.to_datetime(self.df_aum["date"])
|
|
47
|
+
self.df_aum = (
|
|
48
|
+
self.df_aum.sort_values(by="calculated")
|
|
49
|
+
.groupby(["id", "date"])
|
|
50
|
+
.agg({"net_value_usd": "last", "outstanding_shares": "first"})
|
|
51
|
+
)
|
|
52
|
+
self.df_aum = (self.df_aum.net_value_usd * self.df_aum.outstanding_shares).astype(float)
|
|
53
|
+
self.df_aum = self.df_aum.reindex(
|
|
54
|
+
pd.MultiIndex.from_product(
|
|
55
|
+
[
|
|
56
|
+
self.df_aum.index.levels[0],
|
|
57
|
+
pd.date_range(
|
|
58
|
+
self.df_aum.index.get_level_values("date").min(),
|
|
59
|
+
self.df_aum.index.get_level_values("date").max(),
|
|
60
|
+
),
|
|
61
|
+
],
|
|
62
|
+
names=["id", "date"],
|
|
63
|
+
),
|
|
64
|
+
method="ffill",
|
|
65
|
+
).dropna()
|
|
66
|
+
|
|
67
|
+
# Build the fees dataframe where product id is the index and colum are the every fees type available and value are the amount.
|
|
68
|
+
|
|
69
|
+
fees = Fees.valid_objects.filter(
|
|
70
|
+
transaction_date__lte=to_date,
|
|
71
|
+
transaction_date__gte=from_date,
|
|
72
|
+
transaction_subtype__in=self.FEE_MAP.keys(),
|
|
73
|
+
linked_product__in=products,
|
|
74
|
+
).annotate(
|
|
75
|
+
fee_type=Case(
|
|
76
|
+
*[When(transaction_subtype=k, then=Value(v)) for k, v in self.FEE_MAP.items()],
|
|
77
|
+
default=Value("management"),
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
self.df_fees = pd.DataFrame(
|
|
81
|
+
fees.values_list("linked_product", "fee_type", "total_value", "transaction_date", "calculated"),
|
|
82
|
+
columns=["linked_product", "fee_type", "total_value", "transaction_date", "calculated"],
|
|
83
|
+
).rename(columns={"linked_product": "id", "transaction_date": "date"})
|
|
84
|
+
self.df_fees["date"] = pd.to_datetime(self.df_fees["date"])
|
|
85
|
+
|
|
86
|
+
self.df_fees = (
|
|
87
|
+
self.df_fees[["fee_type", "total_value", "id", "date"]]
|
|
88
|
+
.pivot_table(index=["id", "date"], columns="fee_type", values="total_value", aggfunc="sum")
|
|
89
|
+
.astype("float")
|
|
90
|
+
.round(4)
|
|
91
|
+
)
|
|
92
|
+
self.df_fees["total"] = self.df_fees.sum(axis=1)
|
|
93
|
+
self.df_fees = self.df_fees.reindex(self.df_aum.index, fill_value=0)
|
|
94
|
+
self.df_fees = self._rolling_average_monday(self.df_fees)
|
|
95
|
+
|
|
96
|
+
# Build the fees dataframe where product id is the index and colum are the every fees type available and value are the amount.
|
|
97
|
+
self.df_rebates = pd.DataFrame(
|
|
98
|
+
Rebate.objects.filter(date__gte=from_date, date__lte=to_date, product__in=products).values_list(
|
|
99
|
+
"product", "value", "date", "commission_type__key"
|
|
100
|
+
),
|
|
101
|
+
columns=["product", "value", "date", "commission_type__key"],
|
|
102
|
+
).rename(columns={"product": "id"})
|
|
103
|
+
self.df_rebates["date"] = pd.to_datetime(self.df_rebates["date"])
|
|
104
|
+
|
|
105
|
+
self.df_rebates = (
|
|
106
|
+
pd.pivot_table(
|
|
107
|
+
self.df_rebates,
|
|
108
|
+
index=["id", "date"],
|
|
109
|
+
columns="commission_type__key",
|
|
110
|
+
values="value",
|
|
111
|
+
aggfunc="sum",
|
|
112
|
+
fill_value=0,
|
|
113
|
+
)
|
|
114
|
+
.astype("float")
|
|
115
|
+
.round(4)
|
|
116
|
+
)
|
|
117
|
+
self.df_rebates = self.df_rebates.reindex(self.df_aum.index, fill_value=0)
|
|
118
|
+
self.df_rebates["total"] = self.df_rebates.sum(axis=1)
|
|
119
|
+
self.df_rebates = self._rolling_average_monday(self.df_rebates)
|
|
120
|
+
# Iniliaze basic column
|
|
121
|
+
|
|
122
|
+
self.empty_column = pd.Series(0.0, dtype="float64", index=self.df_fees.index)
|
|
123
|
+
self._set_basics_statistics()
|
|
124
|
+
|
|
125
|
+
def _set_basics_statistics(self):
|
|
126
|
+
groupby_fees = self.df_fees.groupby(level=0).sum(numeric_only=True)
|
|
127
|
+
groupby_rebates = self.df_rebates.groupby(level=0).sum(numeric_only=True)
|
|
128
|
+
for key in [*CommissionType.objects.values_list("key", flat=True), "total"]:
|
|
129
|
+
fees = groupby_fees.get(key, self.empty_column)
|
|
130
|
+
rebates = groupby_rebates.get(key, self.empty_column)
|
|
131
|
+
fees_usd = fees * self.fx_rates
|
|
132
|
+
rebates_usd = rebates * self.fx_rates
|
|
133
|
+
marginality = fees - rebates
|
|
134
|
+
marginality_usd = fees_usd - rebates_usd
|
|
135
|
+
marginality_percent = (fees - rebates) / fees.replace(0, np.nan)
|
|
136
|
+
marginality_percent_usd = (fees_usd - rebates_usd) / fees_usd.replace(0, np.nan)
|
|
137
|
+
|
|
138
|
+
setattr(self, f"{key}_fees", fees.rename(f"{key}_fees"))
|
|
139
|
+
setattr(self, f"{key}_rebates", rebates.rename(f"{key}_rebates"))
|
|
140
|
+
setattr(self, f"{key}_fees_usd", fees_usd.rename(f"{key}_fees_usd"))
|
|
141
|
+
setattr(self, f"{key}_rebates_usd", rebates_usd.rename(f"{key}_rebates_usd"))
|
|
142
|
+
setattr(self, f"{key}_marginality", marginality.rename(f"{key}_marginality"))
|
|
143
|
+
setattr(self, f"{key}_marginality_usd", marginality_usd.rename(f"{key}_marginality_usd"))
|
|
144
|
+
setattr(self, f"{key}_marginality_percent", marginality_percent.rename(f"{key}_marginality_percent"))
|
|
145
|
+
setattr(
|
|
146
|
+
self,
|
|
147
|
+
f"{key}_marginality_percent_usd",
|
|
148
|
+
marginality_percent_usd.rename(f"{key}_marginality_percent_usd"),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _rolling_average_monday(self, df):
|
|
152
|
+
"""
|
|
153
|
+
This utility method take a dataframe and assum the values on Mondays are accumulated over the weekend. So we need to average every Saturday, Sunday and Monday together.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
monday = df[df.index.get_level_values("date").weekday == 0] / 3
|
|
157
|
+
monday = monday.reindex(df.index, method="bfill")
|
|
158
|
+
df[df.index.get_level_values("date").weekday.isin([5, 6, 0])] = monday[
|
|
159
|
+
df.index.get_level_values("date").weekday.isin([5, 6, 0])
|
|
160
|
+
]
|
|
161
|
+
return df
|
|
162
|
+
|
|
163
|
+
def get_net_marginality(self, type: str) -> pd.Series:
|
|
164
|
+
return (
|
|
165
|
+
((self.df_fees.get(type, self.empty_column) - self.df_rebates.get(type, self.empty_column)) / self.df_aum)
|
|
166
|
+
.groupby("id")
|
|
167
|
+
.mean()
|
|
168
|
+
) * 360
|
|
169
|
+
|
|
170
|
+
def get_aggregated_net_marginality(self, type: str) -> pd.Series:
|
|
171
|
+
# we compute the total net marginality for management for the aggregate function
|
|
172
|
+
total_aum = self.df_aum.groupby(level=1).sum()
|
|
173
|
+
return (
|
|
174
|
+
(
|
|
175
|
+
(
|
|
176
|
+
self.df_fees.get(type, self.empty_column).groupby(level=1).sum()
|
|
177
|
+
- self.df_rebates.get(type, self.empty_column).groupby(level=1).sum()
|
|
178
|
+
)
|
|
179
|
+
/ total_aum
|
|
180
|
+
).mean(axis=0)
|
|
181
|
+
) * 360
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
import factory
|
|
4
|
+
from psycopg.types.range import DateRange
|
|
5
|
+
from wbcommission.models.commission import (
|
|
6
|
+
Commission,
|
|
7
|
+
CommissionExclusionRule,
|
|
8
|
+
CommissionRole,
|
|
9
|
+
CommissionType,
|
|
10
|
+
)
|
|
11
|
+
from wbportfolio.models.roles import PortfolioRole
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommissionTypeFactory(factory.django.DjangoModelFactory):
|
|
15
|
+
name = "MANAGEMENT"
|
|
16
|
+
key = factory.LazyAttribute(lambda x: x.name.lower())
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
model = CommissionType
|
|
20
|
+
django_get_or_create = ["key"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommissionFactory(factory.django.DjangoModelFactory):
|
|
24
|
+
account = factory.SubFactory("wbcrm.factories.AccountFactory")
|
|
25
|
+
crm_recipient = factory.SubFactory("wbcore.contrib.directory.factories.entries.EntryFactory")
|
|
26
|
+
|
|
27
|
+
portfolio_role_recipient = None
|
|
28
|
+
account_role_type_recipient = None
|
|
29
|
+
order = 0
|
|
30
|
+
commission_type = factory.SubFactory(CommissionTypeFactory)
|
|
31
|
+
net_commission = True
|
|
32
|
+
is_hidden = False
|
|
33
|
+
|
|
34
|
+
class Meta:
|
|
35
|
+
model = Commission
|
|
36
|
+
|
|
37
|
+
@factory.post_generation
|
|
38
|
+
def rule_timespan(self, create, extracted, **kwargs):
|
|
39
|
+
if not create:
|
|
40
|
+
return
|
|
41
|
+
if extracted:
|
|
42
|
+
v = self.rules.first()
|
|
43
|
+
v.timespan = extracted
|
|
44
|
+
v.save()
|
|
45
|
+
|
|
46
|
+
@factory.post_generation
|
|
47
|
+
def rule_aum(self, create, extracted, **kwargs):
|
|
48
|
+
if not create:
|
|
49
|
+
return
|
|
50
|
+
if extracted:
|
|
51
|
+
v = self.rules.first()
|
|
52
|
+
v.assets_under_management_range = extracted
|
|
53
|
+
v.save()
|
|
54
|
+
|
|
55
|
+
@factory.post_generation
|
|
56
|
+
def rule_percent(self, create, extracted, **kwargs):
|
|
57
|
+
if not create:
|
|
58
|
+
return
|
|
59
|
+
if extracted:
|
|
60
|
+
v = self.rules.first()
|
|
61
|
+
v.percent = extracted
|
|
62
|
+
v.save()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PortfolioRoleCommissionFactory(CommissionFactory):
|
|
66
|
+
crm_recipient = None
|
|
67
|
+
account_role_type_recipient = None
|
|
68
|
+
portfolio_role_recipient = factory.Iterator([role_choice[0] for role_choice in PortfolioRole.RoleType.choices])
|
|
69
|
+
|
|
70
|
+
class Meta:
|
|
71
|
+
model = Commission
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AccountTypeRoleCommissionFactory(CommissionFactory):
|
|
75
|
+
crm_recipient = None
|
|
76
|
+
portfolio_role_recipient = None
|
|
77
|
+
account_role_type_recipient = factory.SubFactory("wbcrm.factories.AccountRoleTypeFactory")
|
|
78
|
+
|
|
79
|
+
class Meta:
|
|
80
|
+
model = Commission
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CommissionRoleFactory(factory.django.DjangoModelFactory):
|
|
84
|
+
commission = factory.SubFactory(CommissionFactory)
|
|
85
|
+
person = factory.SubFactory("wbcore.contrib.directory.factories.entries.PersonFactory")
|
|
86
|
+
|
|
87
|
+
class Meta:
|
|
88
|
+
model = CommissionRole
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CommissionExclusionRuleFactory(factory.django.DjangoModelFactory):
|
|
92
|
+
product = factory.SubFactory("wbportfolio.factories.products.ProductFactory")
|
|
93
|
+
commission_type = factory.SubFactory(CommissionTypeFactory)
|
|
94
|
+
overriding_percent = factory.Faker("pydecimal", min_value=0, max_value=1, right_digits=2)
|
|
95
|
+
overriding_net_or_gross_commission = "DEFAULT"
|
|
96
|
+
account_role_type = None
|
|
97
|
+
timespan = DateRange(date.min, date.max)
|
|
98
|
+
|
|
99
|
+
class Meta:
|
|
100
|
+
model = CommissionExclusionRule
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import factory
|
|
2
|
+
from wbcommission.models import Rebate
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RebateFactory(factory.django.DjangoModelFactory):
|
|
6
|
+
date = factory.Faker("date_object")
|
|
7
|
+
|
|
8
|
+
commission = factory.SubFactory("wbcommission.factories.CommissionFactory")
|
|
9
|
+
account = factory.LazyAttribute(lambda o: o.commission.account)
|
|
10
|
+
commission_type = factory.LazyAttribute(lambda o: o.commission.commission_type)
|
|
11
|
+
product = factory.SubFactory("wbportfolio.factories.ProductFactory")
|
|
12
|
+
recipient = factory.SubFactory("wbcore.contrib.directory.factories.entries.PersonFactory")
|
|
13
|
+
value = factory.Faker("pydecimal", positive=True, max_value=1000000, right_digits=4)
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
model = Rebate
|